Linux-进程与信号
目录
第一节:信号是什么
第二节:信号处理
2-1.默认处理
2-2.忽略
2-3.捕捉
第三节:信号产生
3-1.命令产生信号
3-2.进程产生命令
3-2-1.kill 给其他进程发送信号
3-2-2.raise 给自己发送信号
3-2-3.alarm 隔一段时间给自己发送SIGALRM(14)信号
3-3.操作系统自动产生信号
第四节:进程中信号的保存
4-1.信号集介绍
4-1-1.pending 未决信号集
4-1-2.handler 信号处理函数集
4-1-3.block 阻塞信号集
4-2.获得/设置block
4-3.获得pending
4-4.位图操作
第五节:使用信号集
第六节:信号屏蔽
总结:
第一节:信号是什么
信号是由操作系统直接产生的,向某个进程发送的一种事件,进程收到信号后,会对信号进行相应的处理
一个进程也可以通过系统调用让操作系统产生信号,并把信号发送出去。
使用 kill -l 命令可以查看所有信号:
前面的序号是信号代表的值,后面是信号的名字,类似于:
#define SIGHUP 1
所以它们是可以混用的。
第二节:信号处理
2-1.默认处理
进程收到信号后,会对信号进行处理,每个信号都有自己的默认处理方式,如果不对信号做任何设置,进程就会使用信号的默认处理。
2-2.忽略
忽略就是进程收到信号后不做任何处理(不处理实际上也是一种处理方式),有些信号的默认处理就是忽略。
2-3.捕捉
捕捉就是设置信号的自定义处理,即进程如何处理信号由程序员决定,当然也可以将信号的处理方式设置成忽略。
需要注意的是不能捕获SIGKILL(9)信号,因为它是操作系统强制终止进程的信号,防止一些进行逃避终止。
第三节:信号产生
信号是由操作系统直接产生的,我们也可以使用系统调用和命令使操作系统产生某个信号。
接下来我们以SIGINT(2)信号为例子,改信号的默认处理是终止进程,但是会给进程执行必要清理工作的时间。
3-1.命令产生信号
先编写一个无限循环的程序,并运行起来:
#include <thread> #include <iostream> int main() { while(true) { // 休眠2s std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "我是进程,我正在运行" << std::endl; } return 0; }
此时进程就会一直向屏幕打印信息,说明它是运行起来的:
然后新打开一个会话,先输入命令:
ps axj | head -1 && ps axj | grep signal_test | grep -v grep
其中的 signal_test 就是程序名,它的作用是查看查看上述程序产生的进程的信息:
找到pid:3767755,然后使用如下命令向进程3767755发送SIGINT(2):
kill -2 3767755 // 或者 kill -SIGINT 3767755
此时该进程就会停止打印了,而且进程的信息也没有了:
3-2.进程产生命令
3-2-1.kill 给其他进程发送信号
使用 kill 系统调用可以让操作系统产生信号,它的第一个参数是目标进程的pid,第二个参数是发送哪个信号,我们让父进程产生SIGINT(2)信号使子进程终止:
#include <thread> #include <iostream> #include <unistd.h> // 信号相关系统调用头文件 #include <sys/types.h> #include <signal.h> int main() { // 创建子进程 pid_t pid = fork(); if(pid == -1) { std::cout << "子进程创建失败!" << std::endl; } else if(pid == 0) // 子进程 { while(true) { // 休眠2s std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "我是子进程,我正在执行,pid:" << getpid() <<std::endl; } } else // 父进程 { // 休眠10s后,向子进程发送2号信号 std::this_thread::sleep_for(std::chrono::seconds(10)); kill(pid,SIGINT); } return 0; }
编译并执行。
刚开始的时候,两个进程都存在:
10s后,子进程因为收到2号信号而终止了,父进程发完信号后也退出了:
此时子进程打印了4次内容,第五次打印的时候被终止了:
3-2-2.raise 给自己发送信号
#include <thread> #include <iostream> #include <unistd.h> // 信号相关系统调用头文件 #include <sys/types.h> #include <signal.h> int main() { while(true) { std::cout << "我是进程,我正在执行,pid:" << getpid() <<std::endl; // 给自己发送信号 raise(SIGINT); } return 0; }
进程打印了一遍之后就收到2号信号而终止了。
3-2-3.alarm 隔一段时间给自己发送SIGALRM(14)信号
这个信号的默认处理是终止进程,但是它可被忽略或者捕获,那么就可以自定义处理,让进程在特定时间间隔后执行特定的任务。
使用signal捕获14号信号,并设置特定任务函数:
#include <thread> #include <iostream> #include <unistd.h> // 信号相关系统调用头文件 #include <sys/types.h> #include <signal.h> // 自定义处理 void handler(int sig) { std::cout << "要起床了!!!" <<std::endl; } int main() { // 捕获信号 signal(14,&handler); // 第二个参数传入SIG_ING就是忽略信号 // 设置闹钟:10s后提醒我起床 alarm(10); //...执行其他任务 std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; }
执行结果:
alarm设置的时间是10s,进程的退出时间是30s,但是只打印了一次,说明alarm只触发一次。如果要多次处理,做成类似起床闹钟的程序,就需要在handler中再次调用alarm:
void handler(int sig) { std::cout << "要起床了!!!" <<std::endl; // 再次定时10s alarm(10); }
只要进程不退出,这个闹钟就会一直运行下去了。
一个进程只能有一个alarm,之后设置的alarm会覆盖前面的alarm。
使用 alarm(0) 可以取消闹钟,返回上一个闹钟的剩余时间。
3-3.操作系统自动产生信号
操作系统自己也会产生信号,加强对进程的管理,维持整个系统的稳定。
进程产生异常时,系统就会向进程发送终止信号,使进程终止,这就是所谓的程序崩溃了。
程序崩溃的原因:
(1)除0错误:当程序除以0时,因为是除不尽的,结果的位数就会越来越多,CUP 寄存器的溢出标志位就会被占用,此时操作系统就会向进程发送SIGFPE(8)信号使进程终止。
(2)非法访问内存:如果程序对野指针解引用并赋值,CPU的MMU会先查看这个地址是不是被这个程序使用的地址,如果不是,就会将这个异常地址放到CR2,操作系统识别到CR2中的内容后就会向对应进程发送SIGSEGV(11)信号使进程终止。
上述两中信号都可以被忽略或捕捉,这样程序崩溃时不会退出,但是不建议这么做。
如果这样做,异常进程会不断重复剥离、载入CUP的操作,操作系统也会不断地向异常进程发送信号。
第四节:进程中信号的保存
进程执行信号处理的动作叫做信号递达,信号从产生到递达之前的状态叫做信号未决。
进程可以选择性的阻塞某些信号,这些被阻塞的信号可以被进程接收到,但是永远无法递达,直到阻塞解除。
所以进程有3张表保存信号的上述信息:pending,handler,block。它们都以位图的形式保存信号的信息
4-1.信号集介绍
4-1-1.pending 未决信号集
它保存进程收到的、还未递达的信号,无论是否阻塞。收到的信号的对应比特位会置1,没有收到的信号置0。
当一个信号被递达了,相应比特位又置0。
子进程不会继承父进程的pending 。
4-1-2.handler 信号处理函数集
它保存信号的处理函数指针,一个pending中的未决信号要进行递达时,先会去handler获取信号的处理函数进行信号递达。
子进程会拷贝一份父进程的handler。
4-1-3.block 阻塞信号集
它记录哪些信号被阻塞了,被阻塞的信号的对应比特位置1,被阻塞的信号永不递达,如果一个被阻塞信号到来,那么它在pending的比特位永远是1。
子进程会拷贝一份父进程的block。
3个信号集在父进程、子进程是各自私有一份的。
4-2.获得/设置block
int sigprocmask(int how,const sigset_t* set,sigset_t* oset)
how:对block的具体操作
(1)SIG_SETMASK:用set覆盖block的原始位图(MASK)
(2)SIG_BLOCK:在原始位图中添加新的阻塞,即:MASK=MASK|set
(3)SIG_UNBLOCK:在原始位图中删除阻塞,即:MASK=MASK&~set
set:输入型参数,程序员传入的位图,具体操作由how决定
oset:输出型参数,返回修改之前的位图
4-3.获得pending
int sigpending(sigset_t* oset)
oset:输出型参数,返回pending的位图
4-4.位图操作
清空位图:
int sigempty(sigset_t* set);
添加信号到位图:
int sigaddset(sigset_* set,int signum);
判断一个信号是否已经被添加到位图中了,存在返回1,不存在返回0:
int sigismember(const sigset_t* set,int signum);
第五节:使用block信号集
我们还是让父进程对子进程发送SIGINT(2)信号,不同的是子进程要对2号信号进行阻塞处理:
#include <thread> #include <iostream> #include <unistd.h> // 信号相关系统调用头文件 #include <sys/types.h> #include <signal.h> int main() { pid_t pid = fork(); if(pid == -1) { std::cout << "子进程创建失败" << std::endl; } else if(pid == 0) { // 设置位图 sigset_t set; sigaddset(&set,2); // 添加2号信号 sigprocmask(SIG_BLOCK,&set,nullptr); // 添加2号信号到block中 while(true) { std::cout << "我是子进程,我正在运行" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } } else { std::this_thread::sleep_for(std::chrono::seconds(3)); // 向子进程发送2号信号 kill(pid,2); // 防止子进程变成孤儿进程 while(true) { std::this_thread::sleep_for(std::chrono::seconds(2)); } } }
如果子进程没有阻塞2号信号,那么打印两三次就不会打印了,子进程却一直在打印,说明子进程阻塞了2号信号。
此时要退出子进程不能使用ctrl+c,因为ctrl+c的实质是向前台进程发送2号命令,此时要查找子进程的pid,使用如下命令强制终止子进程:
kill -9 pid
父进程没有阻塞2号信号,正常ctrl+c终止即可。
因为被阻塞的信号仍然会保存在pending中,所以如果被阻塞的2号信号在之后解了阻塞,仍然会进行递达:
#include <thread> #include <iostream> #include <unistd.h> // 信号相关系统调用头文件 #include <sys/types.h> #include <signal.h> int main() { pid_t pid = fork(); if(pid == -1) { std::cout << "子进程创建失败" << std::endl; } else if(pid == 0) { // 设置位图 sigset_t set; sigaddset(&set,2); // 添加2号信号 sigprocmask(SIG_BLOCK,&set,nullptr); // 添加2号信号到block中 std::this_thread::sleep_for(std::chrono::seconds(5)); // 解除2号阻塞 sigprocmask(SIG_UNBLOCK,&set,nullptr); } else { // 父进程等待子进程捕获2号信号后再发送信号 std::this_thread::sleep_for(std::chrono::seconds(3)); // 向子进程发送2号信号 kill(pid,2); // 防止子进程变成孤儿进程 while(true) { std::this_thread::sleep_for(std::chrono::seconds(2)); } } }
这样看到子进程的STAT变成Z了,意味着它变成僵尸进程了,说明子进程已经退出了,它在等待父进程退出,然后回收它自己。
第六节:信号屏蔽
进程在以下几种情况下不会接收信号:
(1)信号在pending,已经存在了。即进程只能记录收到了哪些信号,不能记录收到的信号的次数。
(2)信号正在递达,此时无法收到相同类型的信号。比如进程收到2号信号,进行进程终止的默认处理,此时进程就不能接收其他的2号信号了。
父进程同时向子进程发送4次2号信号,子进程的处理方式是打印信息:
#include <thread> #include <iostream> #include <unistd.h> // 信号相关系统调用头文件 #include <sys/types.h> #include <signal.h> void handler(int) { std::cout << "信号递达\n"; std::this_thread::sleep_for(std::chrono::seconds(5)); } int main() { pid_t pid = fork(); if(pid == -1) { std::cout << "子进程创建失败" << std::endl; } else if(pid == 0) { signal(2,handler); std::this_thread::sleep_for(std::chrono::seconds(10)); } else { // 父进程等待子进程捕获2号信号后再发送信号 std::this_thread::sleep_for(std::chrono::seconds(1)); // 多次向子进程发送2号信号 kill(pid,2); kill(pid,2); kill(pid,2); kill(pid,2); // 防止子进程变成孤儿进程 std::this_thread::sleep_for(std::chrono::seconds(20)); } }
结果只打印了一次信息,说明在2号信号递达时屏蔽了其他2号信号。
总结:
信号给了操作系统和用户一种向进程发送特定事件的方式,而且是不受进程正在执行的代码所影响的,这极大地提升了管理进程的灵活性。