信号保存和处理

把上一篇回顾一下吧:共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存的数据结构:

cpp 复制代码
struct shmid_ds 
{        
    struct ipc_perm shm_perm;       /* operation perms */    
    int shm_segsz;                  /* size of segment (bytes) */    
    __kernel_time_t shm_atime;      /* last attach time */    
    __kernel_time_t shm_dtime;      /* last detach time */    
    __kernel_time_t shm_ctime;      /* last change time */    
    __kernel_ipc_pid_t shm_cpid;    /* pid of creator */    
    __kernel_ipc_pid_t shm_lpid;    /* pid of last operator */    
    unsigned short shm_nattch;      /* no. of current attaches */    
    unsigned short shm_unused;      /* compatibility */    
    void *shm_unused2;              /* ditto - used by DIPC */    
    void *shm_unused3;              /* unused */
};

shmget函数

功能:用来创建共享内存

原型

cpp 复制代码
int shmget(key_t key, size_t size, int shmflg);

参数

key:这个共享内存段名字

size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

shmat函数

功能:将共享内存段连接到进程地址空间

原型

cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数

shmid: 共享内存标识

shmaddr:指定连接的地址

shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY

返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址

shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。

shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)

shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

shmdt函数

功能:将共享内存段与当前进程脱离

原型

cpp 复制代码
int shmdt(const void *shmaddr);

参数

shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl函数

功能:用于控制共享内存

原型

cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

shmid:由shmget返回的共享内存标识码

cmd:将要采取的动作(有三个可取值)

buf:指向一个保存着共享内存的模式状态和访问权限的数据结构返回值:成功返回0;失败返回-1

消息队列

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

IPC资源必须删除,否则不会自动清除(重启可以)system V IPC资源的生命周期随内核

信号量

五个概念捏

多个执行流(进程)能看到的一份资源是共享资源

被保护起来的资源是临界资源(同步和互斥)

进程互斥

由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥

系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源

在进程中涉及到互斥资源的程序段叫临界区

IPC资源必须删除,否则不会自动清除(重启可以)system V IPC资源的生命周期随内核

互斥:任何时刻只有一个进程在访问共享资源

资源要被程序员访问的,通过代码访问,代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区)

所谓的对共享资源进行保护(临界资源),本质是对访问共享资源的代码进行保护(临界区)

进入临界区加锁,出了临界区要解锁捏

对于信号量理论的理解

信号量,信号灯是为了保护临界资源的(code)

信号量:本质是一个计数器

看电影买票的本质是对资源的预定机制

所以流程就很明了了

先买票

再让执行流和资源进行一一对应(程序员编码实现)

电影院相当于临界资源(共享资源)

买票相当于申请信号量

总的票数:信号量的初始值

申请信号量的本质是对公共资源的一种预定机制

申请信号量 -- 访问共享内存 -- 释放信号量

对共享资源的整体使用是资源只有一个

计数只有1 or 0的被称作二元信号量,互斥

不能用一个全局变量来当做计数器

为什么捏?

全局变量能被所有进程看到么?

不能捏

根本不能实现我们的要求捏,不是原子的

IPC信号量和共享内存、消息队列一样,要让不同的进程看到同一个计数器

信号量也是一个公共资源,保护临界资源安全的前提是自己是安全的

信号量操作

信号量--要是安全的 --> P

信号量++也要是安全的 --> V

PV操作通过安全保证原子性(以结果为导向,就比如鸡婆的棕褐色猎犬打炉石,打了一晚上,但是最后switch只看他是否上去分了,不会因为他哪局运气不好操作失误卡掉而同情它,也不会做出干预,只看最后的结果)

申请信号量用什么接口捏?

cpp 复制代码
int semget(key_t key,int nsems,int semflg);

信号量集由数组来维护

信号量不用了怎么办呢?

cpp 复制代码
int semctl(int semid, int semnum, int cmd, ...);

op是对PV操作的封装

cpp 复制代码
int semop(int semid, struct sembuf *sops, size_t nsops);
信号量指令
bash 复制代码
ipcs -s

删掉指定的信号量:

bash 复制代码
ipcrm -s semid

OS是如何把共享内存,消息队列,信号量统一管理起来的呢?

cpp 复制代码
struct ipc_id_ary
{
    int size;
    struct kern_ipc_pern* p[0];
    ...
}

把它们的第一个元素都存到struct kern_ipc_perm *XXX[n],就意味着我们把IPC的资源统一管理了

shmid就是数组的下标

所以检测是否冲突只需要遍历数组来检查

这种技术叫多态!

IPC_perm里必须要有字段指向对应的类型

信号

信号和信号量有什么区别。。。

有什么关系

两个没什么关系,就跟鸡婆的棕褐色猎犬和炉石传说高手没什么关系一样,八竿子打不到一块

信号在生活中随时可以产生,信号的产生和进程是异步的

进程能认识信号,可以识别并处理信号

可以把到来的信号暂不处理

在合适的时候再处理

1~31是普通信号,后面是实时信号

信号是Linux系统提供的一种向指定进程发送特定事件的方式,做识别和处理

信号的产生是异步的

异步就是,比如墨墨酱正在上课,但是她的外卖到了,为了避免别人偷她外卖,所以她派遣励志轩去帮她取外卖,墨墨酱上她的课,同时励志轩帮她取外卖,互不耽误

所以总结一下就是信号是Linux提供的一种,向指定进程发送特定事件的方式,做识别和处理

信号处理有三个动作:默认动作、忽略动作、自定义处理(信号的处理)

使用信号最直观的一个接口就是

cpp 复制代码
man 7 signal
bash 复制代码
sighandler_t signal(int signum,sighandler_t handler);

对信号的自定义捕捉只需要捕捉一次,后续就会一直有效

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<unistd.h>

void hander(int sig)
{
    std::cout << "get a sig" << sig <<std::endl;
}

int main()
{
    signal(2,hander);
    while (true)
    {
        std::cout << "hello world,pid: " << getpid() <<std::endl;
        sleep(1);
    }
    return 0;
}

可以发现捏

kill -2就相当于是Ctrl+c了

信号产生是通过kill命令向进程发送信号捏

键盘也可以发送信号

还有一种方式可以产生信号:系统调用

我们如何理解信号的发送与保存呢?

进程有对应的task_struct,这是成员变量,而我们是通过位图来保存收到的信号的

cpp 复制代码
uint32_t signals;

发送信号就是修改指定进程PCB中的信号的指定位图

内核数据结构对象,只有OS有资格修改

祝贺我姐喜提新键盘咯

看着感觉很像那种敲起来像什么奶油轴的声音

使用kill也是可以实现系统调用的捏:

来演示一下:

testsig.cc:

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

//./mykill 2 pid
int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << "signum pid" << std::endl;
        return 1;
    }
    pid_t pid = std::stoi(argv[2]);
    int signum = std::stoi(argv[1]);

    kill(pid,signum);
}

process.cc:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

void hander(int sig)
{
    std::cout << "get a sig" << sig <<std::endl;
}

int main()
{
    signal(2,hander);
    while (true)
    {
        std::cout << "hello world,pid: " << getpid() << std::endl;
        sleep(1);
    }
    
    return 0;
}

makefile:

其实每次感觉很没必要,但是那是代码哎

交一下吧,谁说

makefile不算代码呢

bash 复制代码
.PHONY:all
all: client server

client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf client server

除了kill之外还有别的接口:

raise是给自己当前的进程发送信号:

cpp 复制代码
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

void hander(int sig)
{
    std::cout << "get a sig" << sig <<std::endl;
}

int main()
{
    int cnt = 0;
    signal(3,hander);
    while (true)
    {
        sleep(2);
        raise(3);
    }
}

还有个接口:abort

abort是终止进程,可以向进程发送指定信号

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

void hander(int sig)
{
    std::cout << "get a sig" << sig <<std::endl;
}

int main()
{
    int cnt = 0;
    signal(3,hander);
    while (true)
    {
        sleep(2);
        abort();
    }
}

abort允许捕捉,但是进程还是要终止(因为abort就是用于异常终止的)

那如果我把所有信号都捕捉完,是不是就不能终止我的进程了?

铁子你在想什么

9号信号不允许自动捕捉啊

直接

bash 复制代码
kill -9 pid

就老实了

信号是由他们产生的,那么真正发送信号的是谁?

只有一个:操作系统!

因为发送信号的本质是修改进程PCB中的位图,而只有操作系统有这个资格这样做

软件条件要是管道的读关闭,写一直进行的话

操作系统就会向写进程发送sigpipe信号

我们还可以通过一个函数:alarm,来充当一个闹钟的作用

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

void hander(int sig)
{
    std::cout << "get a sig" << sig <<std::endl;
    exit(0);
}

int main()
{
    int cnt = 1;

    signal(SIGALRM,hander);
    alarm(1);       //设置一秒后的闹钟
    while (true)
    {
        std::cout << "cnt: " << cnt << std::endl;
        cnt++;
    }
    return 0;
}

又是新bug

假如不打印呢?

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

int cnt = 1;

void hander(int sig)
{
    std::cout << "cnt: " << cnt << "get a sig: " << sig << std::endl;
    exit(1);
}

int main()
{
    signal(SIGALRM,hander);
    alarm(1);       
    while (true)
    {
        cnt++;
    }
    return 0;
}

我们可以发现,IO是一件很慢的事情

如果单纯++是内存级的数据递增,但是要是打印到显示器上,显示器毕竟是外设,我们还用的云服务器,当云服务器上代码跑完再推送到本地,这个工作量是不可估量的

就拿中指举例,我竖中指再拍照片,蓝色米老鼠存照片,再添加为表情包并发送,这个工作量是难以估量的,所以建议大家直接竖中指

我们的电脑里面有一块小电池,电脑没电了它都有电,专门用来看时间的

而针对于闹钟,操作系统要对它做管理

闹钟是个结构体对象

cpp 复制代码
struct alarm
{
    time_t expired;    //未来的时间 = seconds + Now();
    pid_t pid;
    fuc_t f;
    ...
}

用最小堆进行管理捏

alarm(0)表示取消闹钟,返回值表示上一个闹钟的剩余时间

闹钟默认只触发一次

cpp 复制代码
#include<iostream>

int main()
{
    int *p = nullptr;
    *p = 100;
    return 0;
}

致命三连问:程序为什么会崩溃?崩溃了为什么会退出?可不可以不退出?

是因为非法访问导致OS给进程发送信号啦!!!

那我们怎么证明呢?

还是老样子,对信号做自定义捕捉就好啦

cpp 复制代码
#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

void hander(int sig)
{
    std::cout << "get a sig: " << sig << std::endl;
    exit(1);
}

int main()
{
    signal(SIGSEGV,hander);
    // int a = 10;
    // a /= 0;
    int *p = nullptr;
    *p = 100;
    return 0;
}

对于野指针,信号是11

对于非法的计算(比如除以0),信号是8

程序崩溃的时候也会向进程发送信号

我崩溃的时候也会向chat发送信号

进程会给OS发信号,最好是退出捏

CPU会帮我们进行两种计算

有种寄存器叫eflag,状态寄存器,是 x86 架构中用于保存处理器标志的寄存器

它是一个 32 位寄存器,其中的每一位代表了处理器的某个状态或控制标志

MMU 主要指的是 "内存管理单元"(Memory Management Unit),它是计算机中的一个重要硬件组件,负责管理计算机系统中的内存。MMU 主要有以下功能:

  1. 地址转换:将虚拟地址转换为物理地址。这对于虚拟内存系统非常重要,因为程序通常使用虚拟地址,而计算机硬件则使用物理地址
  2. 内存保护:通过设置访问权限,防止程序或进程非法访问或修改其他程序或进程的内存区域
  3. 缓存控制:优化内存访问速度,减少对主内存的访问需求

OS是软硬件资源的管理者,要随时处理这种硬件问题,所以会向进程发送信号

出问题了告知你

CR3寄存器保存的是页表的起始地址,还有个寄存器是CR2,当虚拟地址向物理内存中转化失败的时候,失败信息会存放到CR2寄存器里(CR2:页故障线性地址寄存器)

栈溢出是越界捏

core和term

term是异常终止,core也是异常终止,但是会帮我们形成一个debug文件

但是我们运行有问题的程序,却查不到core

这是因为我们核心转储可能被禁用了:

bash 复制代码
ulimit -c

这个可以查看当前核心转储功能有没被启用

为0就是没有启用,可以通过这个命令来启用:

bash 复制代码
ulimit -c unlimited

几个梗图:

我们的编译要是在-g下的,这样可以获得更多调试信息

而core的生成不是在当前目录下的,这样可以让core生成在当前目录:

bash 复制代码
echo "core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

这样就生成了 ,可以用gdb辅助我们进行错误的排查

gdb的具体使用方法都总结在这篇博客里了:

炫酷gdb-CSDN博客https://blog.csdn.net/chestnut_orenge/article/details/138551058 为什么我的显示出来是这个

我真的

懒得喷了

这谁能看得懂啊,我讲个蛋我也不会我下了

我真红温了

爱谁用谁用

相关推荐
只会copy的搬运工15 分钟前
Jenkins 持续集成部署——Jenkins实战与运维(1)
运维·ci/cd·jenkins
o(╥﹏╥)26 分钟前
linux(ubuntu )卡死怎么强制重启
linux·数据库·ubuntu·系统安全
娶不到胡一菲的汪大东31 分钟前
Ubuntu概述
linux·运维·ubuntu
阿里嘎多学长40 分钟前
docker怎么部署高斯数据库
运维·数据库·docker·容器
Yuan_o_43 分钟前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
云云3211 小时前
怎么通过亚矩阵云手机实现营销?
大数据·服务器·安全·智能手机·矩阵
那就举个栗子!1 小时前
Ubuntu 20.04下Kinect2驱动环境配置与测试【稳定无坑版】
linux·ubuntu
灯火不休➴1 小时前
[Xshell] Xshell的下载安装使用、连接linux、 上传文件到linux系统-详解(附下载链接)
linux·运维·服务器
Lukea111 小时前
【新教程】Ubuntu server 24.04配置无线网WiFi
linux·ubuntu·教程
小峰编程1 小时前
独一无二,万字详谈——Linux之文件管理
linux·运维·服务器·云原生·云计算·ai原生