Linux基础内容(21)—— 进程消息队列和信号量_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/130770830?spm=1001.2014.3001.5501
目录
1.定义
1.介绍
2.解释
例子
操作系统信号
实现的大致思路
2.信号的产生方式
1.通过终端按键产生信号
2.调用系统函数向进程发信号
3.硬件异常产生信号
4.软件条件
3.核心转储问题
1.终止的形式
4.保存信号
1.术语
2.信号位图
3.处理信号对应的方法
5.信号的捕捉
1.基础扫盲
2.捕捉信号流程
3.代码
1.sigset_t
2.sigprocmask
3.sigpending
4.捕捉信号的方法
1.signal
2.sigaction
1.定义
1.介绍
kill -l:查看信号与其对应的宏
【1,31】:普通信号
【34,64】:实时信号
2.解释
例子
过马路时,红路灯就是一种信号。红灯代表路人不能通过,绿灯表示可以通过,而人们之所以知道这些都是因为这些信号已经被人们记住并且标记了,那么当碰到不同的信号时,会有不同的反应。不过在处理出现的信号前,可能有一些更加重要的事情,使得人们不得不先干其他的事情,那么此时收到绿灯信号也可以先不急着处理,这段反应时间内人们依然得记得接收到的信号。此外还有进行处理信号的方式有所不同。
操作系统信号
1.通过编码内置不同的信号,使得进程能够辨认它
2.信号出现时,进程可能在处理其他的东西,那么此时信号不一定立即被处理
3.信号不被立即处理,则需要对信号进行保存
4.进程收到信号后有三种当作:默认,自定义,忽略
实现的大致思路
1.内置不同的信号:其实就是将一个位图作为信号的集合。那么自然的0和1就是表示信号的不同状态,读取信号在pcb中寻找。
2.发送信号让pcb进行处理的本质就是将pcb中的信号位图进行修改处理
3.pcb是内核数据,那么操作系统担任执行信号的发送。也就是说,用户只是通过操作系统的接口进行调用
man 7 signal:查询信号
2.信号的产生方式
1.通过终端按键产生信号
ctrl + c -- 终止进程表示信号2
ctrl + / -- 终止进程表示信号3
这些都是键盘输入后,操作系统对其收到的指令进行解读。能知道现在正在对进程发送信号,让进程能够执行信号给予的命令操作
man 2 signal:接收signal信号,并且对接收到信号进行处理hander函数
void hander(int signo) { std::cout << "获取到一个信号,信号编号是: " << signo << std::endl; exit(1); } int main() { while(true) { std::cout<<"我是一个进程:"<<getpid()<<std::endl; sleep(1); signal(SIGINT, hander); } return 0; }
根据上面的代码,原本执行时会不断打出pid的值。一旦我们输入ctrl + c这样的热键, signal立刻读取该信号,一旦调用signal函数,就会打出ctrl + c传入的信号意义
这里我们可能会疑惑,如果我们将所有的信号都自定义而不去退出。那么会不会造成进程无发被人为控制呢?其实不可以的,因为9号信号是不可能被篡改的
2.调用系统函数向进程发信号
kill:指定给指定任意进程(通过pid)传入指定的信号sig
//mytest.cc int main() { while(true) { std::cout << "我是一个正在运行的进程, pid: " << getpid() << std::endl; sleep(1); } } //mysignal.cc static void Usage(const std::string &proc) { std::cout << "\nUsage: " << proc << " pid signo\n" << std::endl; } int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(1); } pid_t pid = atoi(argv[1]); int signo = atoi(argv[2]); int n = kill(pid, signo); if(n != 0) { perror("kill"); } return 0; }
mytest运行时不断循环打印自己pid
mysignal的实现是通过传入对应mytest的pid和信号,将mytest进行处理
随后mytest停止循环打印
这个kill的调用就是信号的发送方式之一
raise:给自己发信号
int main(int argc, char *argv[]) { int cnt = 0; while(cnt <= 10) { printf("cnt: %d, pid: %d\n", cnt++, getpid()); sleep(1); if(cnt >= 5) raise(9); // kill(getpid(), signo) } }
cnt到5时给自己发9号信号 等价于kill(getpid(), signo)
abort:给自己发送指定的信号abort
int main(int argc, char *argv[]) { int cnt = 0; while(cnt <= 10) { printf("cnt: %d, pid: %d\n", cnt++, getpid()); sleep(1); if(cnt >= 5) abort(); // kill(getpid(), signo) } }
cnt到5时给自己发6号信号,kill(getpid(), signo)
信号处理行为的理解:
1.进程收到大部分的信号的默认处理信号动作都是终止进程。
2.信号的意义并不是终止进程这个行为,而是传入的信号本身的意义代表不同的事件。
3.硬件异常产生信号
信号的产生并不一定需要用户参与发送的行为,信号会在操作系统内部自动产生。
void catchSig(int signo) { std::cout << "获取到一个信号,信号编号是: " << std::endl; } signal(SIGFPE, catchSig); while (true) { std::cout << "我在运行中...." << std::endl; sleep(1); int a = 10; a /= 0; }
除0在软件中会出现报错直接退出,而我们疑惑的是为什么会报错,怎么终止进程的呢?
1.当前进程在除0后会出现来自操作系统的信号,给我们发送了8(SIGFPE)号信号
2.cpu运算时,不仅将计算的结果算出,还要保证正常运行。通过状态寄存器来判断是否正常运行,除0状态寄存器溢出标记位变为1,即本次运算结果无意义,操作系统捕捉到cpu异常,返回信号8
3.本次的结果会一直打印信号编号8,是因为cpu的内容虽然只有一份,但是它属于进程的上下文。此时没有能力将异常变为正常,用户无法操作,那么就进程一直没有退出,则进程在切换时,cpu会不断切换回复和保存,则信号会不断打出
4.语法问题反映到硬件上,得到异常信号捕获
void catchSig(int signo) { std::cout << "获取到一个信号,信号编号是: " << std::endl; exit(1); } int main(int argc, char *argv[]) { // 3.硬件异常产生信号 signal(SIGFPE, catchSig); while (true) { std::cout << "我在运行中...." << std::endl; sleep(1); int *p = nullptr; *p = 100; } }
操作系统怎么知道野指针了呢?
指针其实都是在单个虚拟地址空间的值,那么其实都是要去寻找磁盘中真正映射的位置对应的值。那么就有了pcb中的虚拟内存空间对应页表,页表对应cpu中的mmu,使得找到硬盘的地址。但是非法访问地址cpu会输出信号,操作系统捕获信号则出现对应的策略。因此最后我们读取信号为11。
4.软件条件
1.管道
之前管道的使用,其实有一个设计,就是当一端进程关闭管道文件,那么另一端也会结束。这是因为操作系统在管理管道的时,一旦一端结束了,管道软件就会就会发出信号,操作系统读到对应的信号从而结束进程
2.计时器
alarm:定时间返回sigalrm,终止进程
int main(int argc, char *argv[]) { alarm(1); int cnt = 0; while(true) { std::cout<<"cnt: "<<cnt++<<std::endl; } }
进程工作一秒后,alarm发出信号终止了进程。
int cnt = 0; void catchSig(int signo) { std::cout << "cnt: " << cnt << std::endl; } int main(int argc, char *argv[]) { // 4. 软件条件 -- "闹钟"其实就是用软件实现的 signal(SIGALRM, catchSig); alarm(1); while(true) { cnt++; } return 0; }
不需要每次让进程打印cnt,会发现cnt的数量级远远大于之前的代码,这是因为IO需要耗费的时间,此外连接的是云服务器,网络上也有所消耗。此外进程收到消耗不像之前除0错误一直打印,而是只打印一次,说明alarm函数只给了进程一次发送信号。
那么为什么计时器就是软件条件出现的信号呢?
首先计时器要知道也是一个软件,既然是一个软件就会被其他的进程使用,使用就必须被操作系统管理,那么操作系统通过先组织后管理的思路将其管理好。那么这些计数器就会根据时间戳来表示一个进程是否需要接收alarm信号,那么操作系统在管理过程中定期访问这些时间戳,一旦有进程过了设定的时间戳,那计时器就会给出信号,进程会在之后接受到信号并且做出对应的反馈。
3.核心转储问题
1.终止的形式
首先我们知道其实信号在被接收到后,进程一般的决策都是终止。但是既然都是终止,为什么还要分出不同的终止模式呢?比如上面的信号终止图片介绍中,term和core都是终止,但是又什么不同呢?
trem:是一种正常的终止决策
core:核心转储的终止决策
不过,如果是云端linux,其core的决策不会直接表面,因此需要对操作系统进行设置
ulimit -a可以查看操作系统中核心转储的设置
ulimit -c 指定大小:改变核心转储的大小
那么此时如果一个进程终止条件就是core,那么会生成一个核心转储文件。
1.核心转储:进程出现异常时,进程的有效数据就会被存储到磁盘上。
2.当调试出错进程时,只要调用核心转储文件gdb上下文就会到错误的位置
4.保存信号
1.术语
1.实际执行信号的处理动作称为信号递达
2.信号从产生到递达之间的状态,称为信号未决
3.进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
2.信号位图
pcb中有两个位图,一个是pending表,一个是block表。
pending表:表示比特位对应的位置是否接收到对应的信号
block表:表示比特位对应的信号是否被阻塞
如果pending位和block位同时置为一,那么信号阻塞;如果pending位置为一block位没有,那么此时信号能被递达
3.处理信号对应的方法
除了两个表以外,还会有一个指针数组,用来存储对应信号的处理方式。操作系统把处理方法的函数地址存储到数组中。
这三个数据结构就能保存信号和处理了
1.block表在信号没有产生之前就可以设定
2.信号只会设置一次,一旦一个信号被接收,那么其他普通信号就会被丢失
3.实时信号可以做到不丢失
5.信号的捕捉
信号产生时,不会被立即处理,而会在合适(从内核态到用户态的时候)的时间处理。
1.基础扫盲
1.用户态其实就是一般执行到不访问操作系统或者硬件的代码的进程时的状态。
2.不过我们一定会涉及到调用操作系统接口,那么怎么样界定我们在用户态还是内核态呢?cpu中有两类寄存器,一类是可见的寄存器,一类是不可见寄存器。无论可见不可见寄存器,只要跟进程强相关的都被叫做是进程的上下文。此外,寄存器有一个CR3寄存器表示当前进程的运行等级,0表示内核级,3表示用户级。那么我们能知道确实存在状态的表示
3.那么用户写入代码有关于系统调用的接口时,其实这些操作都是操作系统完成的,我们只是调用了对应的接口,那么调用接口的过程就是由用户态转为内核态后进行的。那么来回的切换也会浪费时间,一般的决策是少用系统调用。
4.进程又是如何读取操作系统的执行方法呢?其实在进程的虚拟地址空间中,有分为内核空间和用户空间。一般的堆啊栈啊都是用户空间的,但是系统相关的都在内核空间中。要知道用户空间有页表来对应磁盘中的实际空间,那么其实内核空间也有页表来对应实际的磁盘空间位置,但是要注意的是所有的虚拟地址空间中对应的内核空间其实都是一样的,那么我们的页表也没有必要做出很多个进行对应,我们只需要有一个页表即可。
5.那么进程一旦调用到了操作系统的接口,此时还处于用户态,操作系统会帮进程转变状态为内核态,一旦变为内核态,就能到内核空间中执行调用接口对应的操作了。反之如果系统没有检测到现在的状态是内核级,那么进程就算有这部分的接口也无法执行对应的操作。
6.进入到内核态的方法有很多,比如:系统调用,进程切换
2.捕捉信号流程
1.进程在用户态时要访问系统调用接口,操作系统将cpu的状态设置为内核态,随后调用操作系统的调用接口代码
2.进入到内核态,操作系统顺带将信号的判定也比较了。首先对信号的三大结构(pending表,block表,处理方法指针数组)进行对比,随后得到信号想要处理的方式。如果是忽略或者是默认,则直接进行执行,然后要么转用户态,要么直接终止进程了。如果是自定义,则会继续往下。
3.根据指针数组找到对应函数的指针,此时为了执行hander的方法。需要注意的是,内核态的等级虽然高,但是不意味着它会帮着处理用户自己提供的方法,它为了安全性特意将内核态转变为用户态进行执行
4.由于此时cpu的上下文是关于自定义方法的,我们还需要回到内核态,将原来中断的上下文重新填入cpu的上下文。
5.此时依然在内核态,那么自然会顺带判断信号的三大结构判断是否需要执行。
6.最后回答用户态执行后续的代码
7.其中,有两次用户态转内核态,两次内核态转用户态的过程。
3.代码
1.sigset_t
1.每个信号只有一个bit的未决标志,非0即1,体现在位图上。
2.由于未决和阻塞的表都是位图,那么标志可以用相同的数据类型sigset_t来存储,这个类型可以表示每个信号的“有效”或“无效”状态
3.在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
2.sigprocmask调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集),即调用block位图
set:传入设定的位图
oldset:输出参数,得到原先未改变的位图
3.sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
4.捕捉信号的方法
1.signal
捕捉特定信号执行对应的操作
2.sigaction
act:输入的操作结构体
oldact:输出原来的结构体
sigaction结构体
1.sa_handler:处理方法的指针
2.为了理解sa_mask,先看一段代码
代码
#include <iostream> #include <signal.h> #include <cstdio> #include <unistd.h> using namespace std; void Count(int cnt) { while(cnt) { printf("cnt: %2d\r",cnt); fflush(stdout); cnt--; sleep(1); } printf("\r"); } void handler(int signo) { cout << "get a signo: " << signo << endl; sleep(20); } int main() { struct sigaction act, oldact; act.sa_handler = handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGINT, &act, &oldact); while (true) { sleep(1); } return 0; }
此时我们向进程发送相同信号很多次,会发现:
1.相同类型的信号在同一个期间,不会被递达,也就是说不能下一个发过来就立马进行下一个。这是因为当前信号正在被捕捉,pending位图由1变为0,系统会自动将当前处理的信号加入到信号屏蔽字,block中。
2.发送相同信号很多次,只会处理两次。是因为系统完成信号捕捉后,系统会自动将当前处理的信号解除屏蔽,而多次加入的信号在位图的表现上就是pending表中的一次记录,所以很多次信号同时发送,也只会接受到一次。换句话说其实就是,解除屏蔽后会再检查一次pending表中是否存在信号。
3.进程处理信号的原则是串行的,不允许递归式处理,即只判断一次,一个一个处理;而不是循环判断信号,多次处理。
4.操作系统只会屏蔽当前被处理的信号
sa_mask:处理信号时,会将其他的信号屏蔽。