目录
从生活中入手,例如闹钟,红绿灯,狼烟,LOL游戏信号等等,这些都是信号。信号必须都是动态的,像路标就不能称之为信号。
以红绿灯为例,一看到红绿灯我们就知道红灯行,绿灯停,我们不仅能认识它是一个红绿灯,而且还知道应该产生什么样的行为,这样才算是能够识别红绿灯。识别 = 认识 + 行为产生。
对于红绿等这个信号,我们需要有如下几个共识:
现在将生活中红绿灯的例子迁移到进程中:信号是发给进程的。进程之所以能够识别信号,是因为程序员将对应的信号种类和逻辑已经写好了的。
当信号发给进程后,进程不一定要立刻去处理,可能有更加紧急的任务,会在合适的时候去处理。进程收到信号到处理信号之前会有一个窗口期,这个期间要将收到的信号进行保存。
处理信号的方式有三种:默认动作,自定义动作,忽略。
再举个例子:
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:
- 1.执行默认动作(幸福的打开快递,使用商品)
- 2.执行自定义动作(快递是零食,你要开始吃了)
- 3.忽略快递(快递拿上来之后,扔到一边,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
我们学习信号是学习它的整个生命周期,分为产生信号,保存信号,处理信号。但是在这之前先需要学习一些预备知识。
用户输入命令, 在Shell下启动一个前台进程。
用户按下Ctrl C, 这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出:
对比上面例子,这里进程就是你,操作系统就是快递员,信号就是快递。
注意:
再看什么是Linux信号?
Linux信号本质是一种通知机制,用户 或 操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。
结合进程得出的信号结论
- ① 进程要处理信号,必须具备信号“识别”的能力 (看到 + 处理动作作)。
- ② 凭什么进程能够“识别”信号呢? 程序员。
- ③ 信号产生是随机的,进程可能正在忙自己的事情,信号的后续处理,可能不是立即处理的。
- ④ 信号会临时的记录下对应的信号,方便后续进行处理。
- ⑤ 在什么时候处理呢? 合适的时候。
- ⑥ 一般而言,信号的产生相对于进程而言是异步的。
信号是进程之间事件异步通知的一种方式,属于软中断。
用kill - l命令可以察看系统定义的信号列表:
这其中没有32号和33号信号,所以一共有62个信号。而且这里我们只学习普通信号,对实时信号暂不做研究。
在使用这些信号时,可以用信号名,也可以用信号编号,它是一样的,都是宏定义后的结果。
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal然后下滑:
根据对Linux的了解,信号存放在哪里呢?既然信号是给进程的,而进程是通过内核数据结构来管理的,所以我们可以推断出,信号放在进程的task_struct结构体中。
既然它是在PCB中,而且数量是31个,task_struct中必定不会设置31个变量来存放信号,数组还有可能,但是信号的状态只分为有和没有两种,所以再次推断,31个信号放在一个32位的整形变量中,每个比特位代表一个信号。写一段伪代码来示意一下:
- struct task_struct
- {
- // 进程属性
- unsigned int signal;
- // .......
- }
就像在学习基础IO和进程间通信的时候,那些flags标志中的不同的比特位代表着不同的意义,这31个信号量也是这种方式:
问题来了,内核数据结构的修改,这个工作是由谁来完成的?毫无疑问是操作系统,因为task_struct就是它维护的,而且是存在于内存中的,只有操作系统才有权力去修改它,用户是无法直接操作的,因为操作系统不相信任何人。
所以说,无论哪个信号,最后的本质都是由操作系统发生给进程的,这里的发送本质就是在修改task_struct中存放信号哪个变量的比特位。
信号发送的本质就是在修改PCB中的信号位图。
无论未来我们学习了多少中发送信号的方式,本质都是通过操作系统向目标进程发送信号。所以操作系统一定会提供相关的系统调用,比如我们之前使用过的信号:
- kill -9 pid值 //停止某个进程
- kill -19 pid值 //暂停某个进程
它们的底层一定是在调用相关的系统调用,来让操作系统修改PCB中的信号位图。
信号处理常见方式:
- ① 忽略此信号。
- ② 执行该信号的默认处理动作。
- ③ 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。
所谓的注册,就是告诉操作系统,当某个进程接收到某个信号后的处理方式。
既然是告诉操作系统,那么肯定会用到系统调用,该系统调用的名字是signal,man 2 signal:
参数:
可以将信号的处理方式写成一个函数,然后将函数名传递个signal,此时当进程接收到signum指定的信号编号时,就会执行我们定义的函数。
有了上面的知识以后,就可以正式来研究信号了,先来看看产生信号的几种方式。
也就是在键盘上按一些热键,来给进程发送相应的信号,比如上面讲的ctrl c,它产生的是2号信号SIGINT,还有常用按键ctrl \,它产生的是3号信号SIGQUIT。怎么验证呢?
写个正经点的代码:
Makefile
- mykill:mykill.cc
- g++ -o $@ $^ -std=c++11 -g
- .PHONY:clean
- clean:
- rm -f mykill
mykill.cc
- #include
- #include
- #include
- using namespace std;
-
- void catchSig(int signum)
- {
- cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
- }
-
- int main()
- {
- signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个
- signal(SIGQUIT, catchSig);
- // signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作
- // 如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用
- while(1)
- {
- cout << "I am a process,my pid: " << getpid()<< endl;
- sleep(1);
- }
- return 0;
- }
如图,得证,也演示了还可以用其它信号终止进程。
和命令一样名字的系统调用kill(),man 2 kill:
该系统调用是一个进程给另一个进程发送指定信号,可以向任意进程发送任意信号。
mykill.cc
- #include
- #include
- #include
- #include
- #include
- using namespace std;
-
- // void catchSig(int signum)
- // {
- // cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
- // }
-
- static void Usage(string proc)
- {
- cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
- }
-
- int main(int argc, char *argv[])
- {
- if(argc != 3) // ./mykill 9 pid
- {
- Usage(argv[0]);
- exit(1);
- }
-
- int signumber = atoi(argv[1]);
- int procid = atoi(argv[2]); // 获取两个命令行参数并转化
-
- int ret = kill(procid, signumber);
- if(ret != 0)
- {
- cerr << errno << ": " << strerror(errno) << endl;
- }
- return 0;
- }
首先创建了一个休眠进程,然后用自己写的mykill给其发了9号信号。
给自己发信号的系统调用raise(),man raise:
编译运行:
可以发现并没有执行raise后面的代码
给自己发送6号信号的系统调用abort(),man abort:
编译运行:
虽然有3个系统调用来产生信号,但是归根到底都是在使用kill系统调用。
验证一下管道当读端关闭的时候,写端所在进程就会收到编号为13的SIGPIPE信号结束进程:
pipe.cc
- #include
- #include
// C++包C语言头文件常用的方法,和.h效果一样 - #include
- #include
- #include
// pipe + close + read + write - #include
// waitpid两个头文件 - #include
- #include
-
- using namespace std;
-
- void catchSig(int signum)
- {
- cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
- }
-
- int main()
- {
- int pipefd[2];
- int ret = pipe(pipefd); // 一.创建管道
- if(ret < 0)
- {
- cerr << errno << ": " << strerror(errno) << endl;
- }
- // cout << "pipefd[0]: " << pipefd[0] << endl; // 3
- // cout << "pipefd[1]: " << pipefd[1] << endl; // 4
-
- pid_t id = fork(); // 二.创建子进程
- assert(id != -1);
- if (id == 0) // 子进程,读,关闭写
- {
- close(pipefd[1]);
- // 三. 子进程读
- while (true)
- {
- int cnt = 5;
- char buffer[1024 * 8];
- ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // read读
-
- cout << "我是子进程,我的pid: " << getpid()<< endl;
-
- if(cnt--) // 5秒后关闭读端
- {
- close(pipefd[0]);
- exit(0);
- }
- sleep(1);
- }
-
- }
-
- signal(SIGPIPE, catchSig); // 特定信号的处理动作,一般只有一个
-
- close(pipefd[0]); // 父进程,写,关闭读
- // 四. 父进程写
- char send_buffer[1024 * 8];
- while (true)
- {
- cout << "我是父进程,我的pid: " << getpid()<< endl;
- ssize_t s = write(pipefd[1], send_buffer, strlen(send_buffer));
- sleep(1); // 父进程一直写,不关闭写端
- }
- return 0;
- }
编译运行:
在读端关闭以后,写端的自定义处理方式中就接收到了系统发给的SIGPIPE信号,编号为13。
(管道,读端不光不读,而且还关闭了,写端一直在写,会发生什么问题?写没有意义,OS会自动终止对应的写端进程,通过发送13信号SIGPIPE的方式,这就是软件条件产生信号)
下面介绍一下alarm函数和SIGALRM信号
闹钟触发的信号:
闹钟就是系统中的定时器,使用的时候同样需要通过系统调用实现:
验证1s之内,一共会进行多少次count++:定时1秒钟,在循环中进行疯狂加1,设置自定义处理方式,打印定时到后收到的信号编号,并且统计这一秒中内进行了多少次加1操作。
编译运行:
有点延迟的打印了四万多次,这是包括IO的,如果单纯向计算算力呢?:
五亿多,这就发现我们计算机是计数很快的。
上面代码也写了闹钟自动触发就移除了,我们在任务里重设一个闹钟,就实现了定时器的功能:
编译运行:
成功实现,你也可以把int cnt改成无符号的。
如何理解软件条件给进程发送信号?:
OS先识别到某种软件条件触发或者不满足,然后构建信号,发送给指定进程。
除0操作导致的硬件异常:
编译运行:
这其实就是一种硬件异常产生的信号。
CPU中有很多的寄存器,例如eax,ebx,eip等等。CPU会从内存中将代码中的变量拿到寄存器中进行运算,如果有必要,还会将运算的结果放回到内存中。
还有一个状态寄存器,如果CPU在运算的时候发现了除0操作,就会将状态寄存器的溢出标志位置一。此时就意味着硬件产生了异常。而操作系统是一个进行软硬件资源管理的软件,CPU的中状态寄存器的溢出标志位置一后,操作系统可以第一时间拿到。除0导致硬件异常以后,操作系统会给对应的进程发送SIGFPE信号。
当进程接收到SIGFPE信号以后,默认的处理方式就是结束进程。
现在我们对这个SIGFPE信号注册一个自定义处理方式:
编译运行:
怎么这个信号被操作系统不停的发送给这个进程?
进程收到信号后进程不退出,随着CPU时间片的轮转就会再次被调到。
CPU中只有一份寄存器,但是寄存器中的内容属于当前进程的上下文。
当进程被切换的时候,就有无数次的状态寄存器被保存和恢复的过程。
而除0操作导致的溢出标志位置一的数据还会被恢复到CPU中。
所以每一次恢复的时候,操作系统就会识别到,并且给对应进程发送SIGFPE信号。所以就会导致上面不停调用自定义处理函数,不停打印接收到的信号编号。
如何理解除0呢?:
- ① 进行计算的是CPU,这个硬件。
- ② CPU内部是有寄存器的,状态寄存器(位图),有对应的状态标记位、 溢出标记位,OS会自动进行计算完毕之后的检测,如果溢出标记位是1,OS里面识别到有溢出问题,立即只要找到当前谁在运行提取PID,OS完成信号发送的过程,进程会在合适的时候,进行处理。
- ③ 一旦出现硬件异常,进程一定会退出吗?不一定,一般默认是退出,但是我们即便不退出,我们也做不了什么
- ④ 为什么会死循环?寄存器中的异常一直没有被解决。
解引用空指针导致的硬件异常:
编译运行:
上面代码中存在对空指针的解引用操作,空指针的本质是(void*)0,而0地址处是不允许我们用户进行访问的,这部分属于内核空间。
这同样是一种硬件异常产生的信号。
当进程接收到编号为11的SIGSEGV信号以后,默认的处理动作就是结束进程。
将这个信号注册自定义处理方式,同样打印接收到的信号编号,但是不结束进程,可以看到,和除0操作一样,不停的打印。
如何理解野指针或者越界问题?:
- ① 都必须通过地址,找到目标位置。
- ② 我们语言上面的地址,全部都是虚拟地址。
- ③ 将虚拟地址转成物理地址。
- ④ 页表+ MMU((Memory Manager Unit是硬件)
- ⑤ 野指针,越界, 使用非法地址,MMU转化的时候,一定会报错。
硬件异常产生的信号并不会显示发送,而是由操作系统自动发送的。
产生信号总结思考:
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者。
信号的处理是否是立即处理的?
在合适的时候才处理。
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
是的,记录在PCB对应的信号位图当中。
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
能知道,因为这是程序员帮我们写好的。
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
OS去修改位图,根据信号编号修改特定比特位,由比特位由0置1。
如何理解系统调用接口?
系统调用接口执行OS对应的系统调用代码,然后OS提取参数,或者设置特定的数值,再然后OS向目标进程写信号,修改对应进程的信号标记位,进程后续会处理信号,最后执行对应的处理动作。
之前在Linux_10_进程等待讲的:
学了上面信号的知识,是否有一个疑问,31个信号的默认处理方式都是结束进程,并且还可以自定义处理方式,那么为什么要这么多信号呢?一个信号不就行了吗?
man 7 signal 介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因:
我们可以根据这个表找到不同信号产生所对应的不同原因。
以信号2和3为例,他们的默认处理方式一个是Term,一个是Core。
那么这两个方式的区别在哪里呢?Term方式仅仅是结束进程,结束了以后就什么都不干了。但是Core不仅结束进程,而且还会保存一些信息。
比如刚才使用了野指针收到的11号信号的默认处理方式就是Core,退出了,但会保存一些信息。
在云服务器上,默认情况下是看不到Core退出的现象的,这是因为云服务器默认关闭了core file选项,ulimit -a:
看到第一行,core file size的大小是0,意味着这个选项是关闭的。
为了能够看到Core方式的明显现象,我们需要将core file选项打开,ulimit -c 1024:
此时该选项就打开了,表示的意思就是核心转储文件的大小是1024个数据块。
再运行使用野指针的程序,但是不捕捉信号了:
同样会收到11号信号停止。但是在当前目录下会多出一个文件,如下图。
对于一个奔溃的程序,我们最关心的是它为什么崩溃,在哪里崩溃?
当进程出现异常的时候,将进程在对应的时刻,在内存中的有效数据转储到磁盘中:核心转储。核心转储的文件我们可以拿着它进行调试,快速定位到出现异常而崩溃的位置。
这就是核心转储的重要意义,它相比Term方式,能够让我们快速定位出现异常的位置。
再看Core Dump:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:$ ulimit -c 1024
首先介绍几个新的概念:
注意: 阻塞和忽略是不同的,只要信号被阻塞就不会被递达,但是忽略是在递达之后进行的一种处理动作。
信号是保存在内核数据结构中的,下面来看它具体的储存模型:
当我们使用signal注册一个自定义处理方式时,操作系统会将我们定义的函数指针放在handler表中,在信号递达后调用。如果是默认处理方式,会调用handler默认的初始函数指针所对应的函数。
信号产生后,操作系统就会修改pending位图,使信号处于未决状态。
操作系统会按照一定的顺序来检查block表和pending表,然后去调用相应信号编号的处理方式来完成信号递达。大概逻辑(伪代码):
- if(1<<(signo - 1) & pcb->block)
- {
- //signo信号被阻塞,不会被递达
- }
- else
- {
- if(1<<(signo - 1) & pcb->pending)
- {
- //信号递达,处理该信号
- handler[signo - 1];
- }
- }
操作系统在对信号进行检测的时候,先检测的是信号的block位图,如果对应信号的比特位被置一,说明该信号被阻塞,就不再去检测pending位图。如果没有被阻塞,才会去检测pending位图,如果pending位图相应的位被置一,再去调用handler表中的处理函数。
所以如果一个信号没有产生,但是并不妨碍它被阻塞。被阻塞的信号,在产生之后就会一直处于未决状态,不会被递达,只有当阻塞被解除后才会被递达。
- 默认情况下,所有信号都是不被阻塞的,所有信号都没有产生,也就是block位图和pending位图都是0。
pending图,block图以及handler表是存放在内核数据结构中的,所以只能由操作系统来修改,我们用户如果要修改也能通过操作系统来实现,所以操作系统同样给我们提供了系统调用。
handler表中的函数指针可以通过系统调用signal来设置。
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来表示,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
用户在设置pending位图和block位图的时候,并不能直接让系统调用将内核中对于的比特位置1或清0,而是需要预先在一个变量中表达出我们的意愿,然后将这个变量通过系统调用给到操作系统,再由操作系统去修改内核数据结构。
操作系统给我们提供了一个sigset_t的变量类型,用户只需要对这个变量进行预设置,然后再交给操作系统。
系统提供的信号集操作函数操作的也是也是这个预先处理的变量,之所以也用系统调用来处理这个变量,是因为这个变量不单单是一个32位的整形变量,它的结构和内核是对应的,所以操作也要按照相应的规则。
从使用者的角度不必关心具体是如何操作的,只需要使用信号集操作函数来操作sigset_t变量即可。sigset_t变量用其他方式是无法操作的,比如用printf去打印,这是没有意义的。
对于block位图和pending位图的修改,操作系统提供了一族系统调用,称为信号集操作函数。
man sigemptyset:
- sigemptyset:使所有信号对应的bit清零,表示该信号集不包含任何有效信号。
- sigfillset:使所有信号对应的bit置位,表示该信号集的有效信号包括系统支持的所有信号。
- sigaddset:使指定信号所对应的bit置位,表示该信号集中对应信号有效。
- sigdetset:使指定信号所对应的bit清零,表示该信号集中对应信号无效。
- sigismember:判断指定信号所对应的bit是否有效,返回类型是bool类型。
在使用sigset_t类型的变量之前,一定要调用sigemptyset进行初始化,使信号集处于确定状态。
此时我们已经对sigset_t变量预处理好了,下一步就是把这个变量交给操作系统了,操作系统同样提供了对应的系统调用。
sigprocmask()
该系统调用是专门用来修改内核数据结构中的block位图的。man sigprocmask:
sigpending()
这是专门用来获取内核数据结构中的pending位图的。man sigpending:
前面的:man 7 signal 介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因:
(注意到表格下面的一句话,SIGKILL and SIGSTOP不能被捕捉,阻塞,忽略,这里9号信号就是管理员信号,就是防止你把所有信号都设定自定义动作,导致进程不能退出的情况,可以自己做一个实验验证)
我们还可以利用上面的系统调用做一个小的实验,来验证某个信号被阻塞后,它的pengding位图会被置一,但是不会被递达。mykill.cc:
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- using namespace std;
-
- void catchSig(int signum)
- {
- cout << "进程捕捉到了一个信号: " << signum << " Pid: " << getpid() << endl;
- }
-
- static void showPending(sigset_t &pending)
- {
- for (int sig = 1; sig <= 31; sig++)
- {
- if (sigismember(&pending, sig)) //在信号集里,输出1
- cout << "1";
- else
- cout << "0";
- }
- cout << endl;
- }
-
- int main(int argc, char *argv[])
- {
- // 0. 方便测试,捕捉2号信号,不要退出
- signal(2, catchSig);
- // 1. 定义信号集对象
- sigset_t bset, obset; // b是block,o是old
- sigset_t pending;
- // 2. 初始化
- sigemptyset(&bset);
- sigemptyset(&obset);
- sigemptyset(&pending);
- // 3. 添加要进行屏蔽的信号
- sigaddset(&bset, 2 /*SIGINT*/);
- // 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
- int n = sigprocmask(SIG_BLOCK, &bset, &obset); // sigset_t变量和老的sigset_t变量
- assert(n == 0); // sigprocmask成功了返回0
- (void)n; // 强转一下,防止relese下出现变量未被使用的警告
-
- cout << "block 2 号信号成功..., pid: " << getpid() << endl;
- // 5. 重复打印当前进程的pending信号集
- int count = 0;
- while (true)
- {
- // 5.1 获取当前进程的pending信号集
- sigpending(&pending);
- // 5.2 显示pending信号集中的没有被递达的信号
- showPending(pending);
- sleep(1);
- count++;
- if (count == 20) // 20秒后恢复2号信号的block,1->0
- {
- // 默认情况下,恢复对于2号信号的block的时候,确实会进行递达
- // 但是2号信号的默认处理动作是终止进程,需要对2号信号进行捕捉->第0步
- cout << "开始解除对于2号信号的block" << endl;
- int n = sigprocmask(SIG_SETMASK, &obset, nullptr); // 用老的set恢复
- assert(n == 0);
- (void)n;
- cout << "解除对于2号信号的block成功" << endl;
- }
- }
-
- return 0;
- }
这里放一些前面的所有测试代码,很多注释起来了:
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- using namespace std;
-
- // int cnt = 0;
- void catchSig(int signum)
- {
- cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
- // cout << "final cnt: " << cnt << " 信号: " << signum << " Pid: " << getpid() << endl;
- // alarm(1); // 重设闹钟 -> 定时器 -> 你可以实现任意任务
- }
-
- // static void Usage(string proc)
- // {
- // cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
- // }
-
- static void showPending(sigset_t &pending)
- {
- for (int sig = 1; sig <= 31; sig++)
- {
- if (sigismember(&pending, sig)) //在信号集里,输出1
- cout << "1";
- else
- cout << "0";
- }
- cout << endl;
- }
-
- int main(int argc, char *argv[])
- {
- // 0. 方便测试,捕捉2号信号,不要退出
- signal(2, catchSig);
- // 1. 定义信号集对象
- sigset_t bset, obset; // b是block,o是old
- sigset_t pending;
- // 2. 初始化
- sigemptyset(&bset);
- sigemptyset(&obset);
- sigemptyset(&pending);
- // 3. 添加要进行屏蔽的信号
- sigaddset(&bset, 2 /*SIGINT*/);
- // 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
- int n = sigprocmask(SIG_BLOCK, &bset, &obset); // sigset_t变量和老的sigset_t变量
- assert(n == 0); // sigprocmask成功了返回0
- (void)n; // 强转一下,防止relese下出现变量未被使用的警告
-
- cout << "block 2 号信号成功..., pid: " << getpid() << endl;
- // 5. 重复打印当前进程的pending信号集
- int count = 0;
- while (true)
- {
- // 5.1 获取当前进程的pending信号集
- sigpending(&pending);
- // 5.2 显示pending信号集中的没有被递达的信号
- showPending(pending);
- sleep(1);
- count++;
- if (count == 20) // 20秒后恢复2号信号的block,1->0
- {
- // 默认情况下,恢复对于2号信号的block的时候,确实会进行递达
- // 但是2号信号的默认处理动作是终止进程,需要对2号信号进行捕捉->第0步
- cout << "开始解除对于2号信号的block" << endl;
- int n = sigprocmask(SIG_SETMASK, &obset, nullptr); // 用老的set恢复
- assert(n == 0);
- (void)n;
- cout << "解除对于2号信号的block成功" << endl;
- }
- }
-
- // cout << "my pid: " << getpid() << endl;
- // for (int sig = 1; sig <= 31; sig++)
- // {
- // signal(sig, catchSig);
- // }
-
- // while (true)
- // {
- // sleep(1);
- // }
-
- // signal(SIGSEGV, catchSig);
- // cout << "my pid: " << getpid() << endl;
- // int *p = nullptr;
- // *p = 100;
-
- // while (true)
- // {
- // sleep(1);
- // }
-
- // signal(SIGFPE,catchSig);
-
- // int cnt = 0;
- // while(true)
- // {
- // cout << "正在运行的进程" << cnt++ << endl;
-
- // int result = 7;
- // result /= 0;
- // sleep(1);
- // }
-
- // signal(SIGALRM,catchSig);
- // alarm(1); // 先设定了一个闹钟,这个闹钟一旦触发,就自动移除了
-
- // while(true)
- // {
- // ++cnt;
- // }
-
- // cout << "我开始运行咯" << endl;
- // sleep(1);
- // abort(); // 通常用来进行终止进程。等于raise(6) 等于kill(getpid(), 6)
- // // raise(9); // 等于kill(getpid(), 8)
- // cout << "运行结束咯" << endl;
-
- // if(argc != 3) // ./mykill 9 pid
- // {
- // Usage(argv[0]);
- // exit(1);
- // }
-
- // int signumber = atoi(argv[1]);
- // int procid = atoi(argv[2]); // 获取两个命令行参数并转化
-
- // int ret = kill(procid, signumber);
- // if(ret != 0)
- // {
- // cerr << errno << ": " << strerror(errno) << endl;
- // }
-
- // signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个
- // signal(SIGQUIT, catchSig);
- // // signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作
- // // 如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用
- // while(1)
- // {
- // cout << "I am a process,my pid: " << getpid()<< endl;
- // sleep(1);
- // }
- return 0;
- }
下一篇继续进程信号的内容:处理信号部分和一些信号的笔试面试题,再就是多线程的内容了。
下一篇:零基础Linux_20(进程信号)内核态和用户态+处理信号+不可重入函数+volatile。
(穿越回来复习顺便贴个下篇链接:零基础Linux_20(进程信号)内核态和用户态+处理信号+不可重入函数+volatile_linux malloc 不可重入-CSDN博客)