帮助文档
专业提供香港服务器、香港云服务器、香港高防服务器租用、香港云主机、台湾服务器、美国服务器、美国云服务器vps租用、韩国高防服务器租用、新加坡服务器、日本服务器租用 一站式全球网络解决方案提供商!专业运营维护IDC数据中心,提供高质量的服务器托管,服务器机房租用,服务器机柜租用,IDC机房机柜租用等服务,稳定、安全、高性能的云端计算服务,实时满足您的多样性业务需求。 香港大带宽稳定可靠,高级工程师提供基于服务器硬件、操作系统、网络、应用环境、安全的免费技术支持。
服务器资讯 / 香港服务器租用 / 香港VPS租用 / 香港云服务器 / 美国服务器租用 / 台湾服务器租用 / 日本服务器租用 / 官方公告 / 帮助文档
Linux--进程信号
发布时间:2024-03-11 08:11:49   分类:帮助文档
Linux--进程信号 前言         无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事情,而不是让烦恼和焦虑毁掉你不就不多的热情和定力。心可以碎,手不能停,该干什么干什么,在崩溃中继续努力前行,这才是一个成年人的素养。                                                                                                                 --余华         与大家分享余华老师的名言,希望大家能在学习疲惫时调整好心态,继续砥砺前行!那么今日主题进程信号,以信号的产生-信号的保存-信号的处理为时间线进行讲解,后面也从信号中衍生出来的话题,比如可重入函数,volatile关键字等。 信号入门 信号 信号概念 信号是进程之间事件异步通知的一种方式,属于软中断。 在Linux终端中,通过kill -l查看信号,我们发现信号总数并不是64,它的范围是[1-31]和[34-64]。一般把[1-31]的信号称之为普通信号,[34-64]称之为实时信号。 [hongxin@VM-8-2-centos ~]$ kill -l  1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP  6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1 11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM 16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP 21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ 26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR 31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3 38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8  43) SIGRTMIN+9  44)     SIGRTMIN+10     45) SIGRTMIN+11    46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14   49)  SIGRTMIN+15   50) SIGRTMAX-14   51) SIGRTMAX-13 52) SIGRTMAX-12  53)   SIGRTMAX-11   54) SIGRTMAX-10    55) SIGRTMAX-9  56) SIGRTMAX-8    57) SIGRTMAX-7   58)    SIGRTMAX-6     59) SIGRTMAX-5       60) SIGRTMAX-4  61) SIGRTMAX-3  62)  SIGRTMAX-2  63)     SIGRTMAX-1     64) SIGRTMAX  生活角度的信号 在生活中,我们理解的信号:当一个信号产生时,首先我们知道其意思,并且能产生对应的行为。         举例:红绿灯                 1.首先我们能够识别红绿灯,认识它。  我们为什么能够认识信号,这原因是,从小老师的教导,我们记住对红绿灯星号发出后做出相对应的行为。                 2.当红绿灯亮起时,红灯停,绿灯行。这是产生行为-走/停。 当绿灯亮起时,我们必须走吗,可以不走,你可以选择下一个的红绿灯,也可以选择跳个舞再走。所以得出一个结论,当信号(随时)产生时,但可以不(立即)执行。                 3.当信号产生,时间窗口将它保存后,信号被处理。 如何处理:①可以默认处理(红灯停,绿灯行)②初略处理(当灯亮时,不做出任何行为)③自定义处理(当灯亮时,选择跳舞)。     技术应用角度的信号  将上面的例子和概念迁移到进程中         1.进程的识别:需要先认识(先组织后描述)再产生行为 (处理信号)。         2.进程本身是被程序员编写的属性和逻辑的集合。         3.当进程收到信号时,进程可能正在执行更重要的代码,所以信号不一定会被立即处理。               4.进程本身必须要有对信号的保存能力         5.进程处理信号有三种方式:默认,自定义,忽略【信号被捕捉】 我们知道信号不是被立即处理的,所以信号是需要被保存起来的。那么它是保存在哪里?又是如何保存的呢? 关于信号保存在哪里是不难理解的,因为我们发现信号时发送给进程的,例如我们熟知的kill -9 pid。当进程进入僵尸状态了,我们就可以使用它将其“杀死”。而进程需要识别信号,那么信号是不是应该被保存在PBC(tack_strcut)中的。 对于如何保存,在tack_strcut中建立32位的位图,比特位的位置代表:信号的编号。比特位的内容代表:是否收到信号,0未收到,1收到信号。如图: 发生信号的本质,其实不是发送,而是修改。将位图0置1,进程接受到信号。 谁来维护位图呢?很显然不可能是用户,pbc的数据是不可能让用户随意修改的。只能OS(操作系统),修改位图也只能是OS。 无论未来我们学习多少种信号的发送,本质都是OS向目标进程发送的信号(修改位图)! 回过来,当我们不能直接对PCB进行修改数据,那么当我们发送信号时,OS肯定会提供发送信号处理信号的相关系统调用。 当我们知道信号需要发生,保存,处理。我们可以画出它的生命周期,如图 : 为了更好的观察信号,当用户输入命令,在Shell下启动一个前台进程。用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程 。 前台进程因为收到信号,进而引起进程退出 。代码如下: #include #include int main() { while(true) { std::cout<< "I am process!" << getpid() << std::endl; sleep(1); } return 0; } 终端指令如下: [hongxin@VM-8-2-centos 2023-4-3]$ make g++ -o mysignal mysignal.cc -std=c++11 [hongxin@VM-8-2-centos 2023-4-3]$ ll total 20 -rw-rw-r-- 1 hongxin hongxin   82 Apr  3 21:26 makefile -rwxrwxr-x 1 hongxin hongxin 9184 Apr  3 21:30 mysignal -rw-rw-r-- 1 hongxin hongxin  179 Apr  3 21:29 mysignal.cc [hongxin@VM-8-2-centos 2023-4-3]$ ./mysignal  I am process!25658 I am process!25658 I am process!25658 I am process!25658 I am process!25658 ^C [hongxin@VM-8-2-centos 2023-4-3]$  当按下Ctrl + c时进程终端,其本质是Ctrl + c是一个组合键,是被操作系统识别,Ctrl + c被操作系统解释为2号信号,2) SIGINT 。 如果想了解SIGINT,就可以通过手册查询:man 7 signal         SIGINT        2       Term    Interrupt from keyboard         Ctrl + c这里是被默认处理,这里默认处理就是Term->terminal  终止进程。键盘上获取Ctrl + c然后终止进程。我们也讲过自定义处理。提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。信号捕捉函数signal。 通过man手册进行了解signal函数,man 2 signal 进入手册:        #include        typedef void (*sighandler_t)(int);        sighandler_t signal(int signum, sighandler_t handler); signal参数:          signum     //信号数 例如SIGINT 为 2           handler    //自定义方法,写一个 handler函数,调用 handler里的方法 返回值:                 成功返回信号处理程序的前一个值,或错误时返回SIG_ERR。发生错误时,errno设置为指示原因。      测试现象:当我们没有按下Ctrl + c时,代码一直运行;按下Ctrl + c后信号被捕捉,进程退出。 #include #include #include void myhandler(int signal) { std::cout<< "进程捕捉到了一个信号,信号编号是:"<< signal<< std::endl; exit(0); } int main() { signal(2,myhandler); while(true) { std::cout<< "I am process!" << getpid() << std::endl; sleep(1); } return 0; } [hongxin@VM-8-2-centos 2023-4-3]$ ./mysignal  I am process!8099 I am process!8099 I am process!8099 I am process!8099 I am process!8099 I am process!8099 I am process!8099 I am process!8099 I am process!8099 ^C进程捕捉到了一个信号,信号编号是:2 所以signal(2,myhandler);这里是signal函数的调用,并不是myhandler的调用,仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了,一般这个方法不会执行,除非收到对应的信号!当捕捉到2号信号后才执行myhandler方法。 产生信号 通过终端按键产生信号 在上述介绍的Ctrl + c就是从键盘产生信号,不光Ctrl + c,我们也通过Ctrl + \也能对进程发生信号。 [hongxin@VM-8-2-centos 2023-4-3]$ ./mysignal  I am process!12795 I am process!12795 I am process!12795 I am process!12795 I am process!12795 I am process!12795 I am process!12795 I am process!12795 ^\Quit 通过kill -l,我们可以发现, Ctrl + \其实就是3号信号( SIGQUIT)。所以我们通过kill -3 pid将该进程终止。  调用系统函数向进程发信号 下面我们通过代码测试来理解调用系统函数向进程发信号,这里可以用到调用系统函数中其中一个kill函数,通过man 2 kill查看        #include        #include        int kill(pid_t pid, int sig); 参数:         pid:进程的pid,sig:第几个信号 返回值:         zero is returned.  On error, -1  is returned, and errno is set appropriately. 该测试代码,用自己的mian调用kill函数,实现用 mykill向进程发送信号,然后处理。 mysignal.cc #include #include #include #include #include #include //调用main参数错误后,打印 static void Usage(const std::string &proc) { std::cout<< "/n Usage: " << proc << "pid signo"<< std::endl; } //argc表示程序运行时发送给main函数的命令行参数的个数(包括可执行程序以及传参) //argv[]是字符指针数组,它的每个元素都是字符指针,指向命令行中每个参数的第一个字符。 //argv[0]指向可执行程序 //argv[1]指向可执行程序后的第一个字符串。 //argv[2]指向可执行程序后的第二个字符串。 //argv[argc]为NULL int main(int argc ,char *argv[]) { //系统调用向目标进程发送信号 if(argc != 3) { Usage(argv[0]); exit(1); } //将mian第二参数字符串转换成pid_t,得到的pid pid_t pid =atoi(argv[1]); //字符串转成pid_t,signo=几号信号 pid_t signo =atoi(argv[2]); //调用kill函数 int n = kill(pid,signo); if(n != 0) { perror("kill fail"); } // while(true) // { // std::cout<< "I am process !" << getpid() << std::endl; // sleep(1); // } return 0; } mytest.cc #include #include #include //一直运行的程序,用于测试 int main() { while (true) { std::cout<< "我是一个正在运行的进程,pid: " << getpid() <        int raise(int sig); //如果cnt==5调用信号3,终止程序 int cnt=0; while(cnt <= 10) { printf("cnt :%d\n",cnt++); if(cnt==5) raise(3); } 结果:当打印到5时,调用信号3,退出进程。 [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal  cnt :0 cnt :1 cnt :2 cnt :3 cnt :4 Quit raise(3)的实现,其实也可以用kill替换。kill(getpid(),sig)。 第三个函数abort,通man手册来查看它的用法: 功能:给自己发送指定的信号:6) SIGABRT       #include        void abort(void); 代码的实现和结果 int cnt=0; while(cnt <= 10) { printf("cnt :%d\n",cnt++); if(cnt==6) abort(); } [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal  cnt :0 cnt :1 cnt :2 cnt :3 cnt :4 cnt :5 Aborted  为了证实abort给自己发送指定的信号,是不是6) SIGABRT,那么我们可以通过上述写的mytest,将进程运行,然后用kill调用6号信号,看是否一样(Aborted )。结果很是这样的,abort我们也可以直接做封装,kill(getpid,Aborted )。 硬件异常产生信号 看了上述,调用系统函数向进程发信号。我们发现一个问题:信号处理的行为,很多的情况,进程收到大部分的信号,默认处理动作都是终止进程。 那么信号的意义是什么?         举个例子:在刚开始编写程序时,经常会出现各种错误,很多时候的处理方式都是进程终止,但是我们可以通过错误码对应的错误信息,找到错误。 所以说信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样! 下面通过代码来理解,我们知道操作系统是不能除0的,其原因不是说操作系统不能算,它是可以进行除0计算的,但是算出是非常大的值(一个错误值),所以系统直接将它设置浮点数错误。这里我们写一段关于除0的代码,观察会出现什么情况。 while(true) { std::cout<< " 我正在运行....."<< std::endl; sleep(1); int a = 10; a /= 0; } [hongxin@VM-8-2-centos 2023-4-4]$ make g++ -o mysignal mysignal.cc -std=c++11 -g mysignal.cc: In function ‘int main(int, char)’: mysignal.cc:31:11: warning: division by zero [-Wdiv-by-zero]          a /= 0;            ^ g++ -o mytest mytest.cc -std=c++11 [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal   我正在运行..... Floating point exception 不出意外,报了浮点数错误。并且它还将进程终止了,除0后会将进程终止呢?         因为当前进程会受到来自OS系统的信号(告知),8)SIGFPE 为了能够证明,Floating point exception 实质就是向系统发送了SIGFPE,下面通过代码进行证明,代码逻辑:当除0时,操作系统会向进程发送SIGFPE信号,此时通过signal()函数捕捉到SIGFPE时,通过自定义函数catchSig打印出捕捉到的信号。 void catchSig(int signo) { std::cout<< " 获取一个信号吗,信号编号是:" << signo < unsigned int alarm(unsigned int seconds); 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动 作是终止当前进程。 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数 (自己验证一下?)   int main(int argc ,char *argv[]) { int cnt=0; alarm(1); while(true) { printf("cnt:%d\n",cnt); cnt++; } return 0; } 在一分钟后闹钟响起,操作系统向进程发送信号 14) SIGALRM  这份代码的意义是什么呢?         统计1s左右,我们计算机能够将数据累加多少次! //多次运行,统计结果 cnt:111965Alarm clock (1) cnt:124696Alarm clock (2) cnt:131017Alarm clock (3) cnt:128791Alarm clock (4) 将代码调整后,观察:首先将cnt调整成全局变量,再设置signal捕捉,循环中只++,signal调用的函数进行打印。  //多次打印后的结果 [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal   获取一个信号吗,信号编号是:561830771 [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal   获取一个信号吗,信号编号是:563495923 [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal   获取一个信号吗,信号编号是:562496315 [hongxin@VM-8-2-centos 2023-4-4]$ ./mysignal   获取一个信号吗,信号编号是:562106150 为什么第二次++的次数与第一次++的次数相差几乎500倍呢?         因为第一次打印比较多,就会进行多次I/O操作,I/O会花费大量的时间。得出结论:IO非常慢 还有一个很有小细节,当调用的catchSig时,没有写exit函数时,运行后也打印一次。相当于这个闹钟只响一次,一次过后不再响。 void catchSig(int signo) { std::cout<< " 获取一个信号吗,信号编号是:" << cnt <这个598--引起core问题进程的pid;core.589一般是以core+引起core问题进程pid命名文件,该文件存在磁盘中。 为什么需要有核心转储呢?         目的是为了支持调试,如何支持呢?直接在gdb的上下文中core-file core.xxx 作为对面,还是用上述代码,不报段错误,直接死循环用kill -2 pid终止进程(Term)。然后观察终止后会不会产生core文件,发生核心转储。  SIGINT        2       Term    Interrupt from keyboard 结论:         以core退出的可以被核心转储,Term退出是没有被核心转储即为正常退出。核心转储其目的是为了更便于调试。  最后关系信号产生,最后一个问题:         如果将全部信号捕捉,然后自定义处理后,是不是该进程一直被执行,就不能被终止。 为了探究这个问题,下面进行代码测试,实验见证真理。 然后我们发现即使其他进程都被自定义处理后,但是kill -9 还是能将进程终止的。9号信号是管理员信号,在操作系统内是静止对9号信号做捕捉的。 信号的保存--阻塞信号 信号其他相关常见概念         ●实际执行信号的处理动作称为信号递达(Delivery)         ●信号从产生到递达之间的状态,称为信号未决(Pending)。         ●进程可以选择阻塞 (Block )某个信号。         ●被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.         ●注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。 对于抵达,未决,阻塞这三个名词是非常有必要了解其意思。相信大家很好理解抵达和未决。对于阻塞是不易理解的。         例:整个这个信号的过程,我们可以用生活中的例子理解,上课时老师发出信号,说:大家把书上例题1,7,13勾画上,有时间去把做了。这个时候由于老师需要继续上课,我们有更要的事情需要处理,就在书上记录(勾画)上题,但是一直没有做。这段时间老师发出的信号就叫做未决。 当回家后我们觉得这个老师讲的知识点很难,我们不想做,该信号阻塞(记录但不做)。但是过一段时间你认为老师很严格,不做后果很严重,就选择把例题做了,该信号抵达。 还有一种情况,老师上课说:把这个例题算出来(信号的产生),大家不需要记录,直接就做。在这个过程不需要保存信号,发出信号直接执行(抵达)。 注意,阻塞和未决是不一样的,阻塞是需要保存这个信号,未决是在发出信号,不管你保不保存信号都是未决状态。 注意,阻塞和忽略也是不同。比如当老师布置了例题(发出信号),我们认为做不做都不影响时,我们直接不记录这些例题(忽略)。在未保存信号下,我们执行的策略是(忽略)。在发出信号到未做这段时间处于未决状态,阻塞是在未决状态之间的。忽略是在未决之后,更是在抵达(如何执行老师的信号)之后。所以说忽略是在递达之后可选的一种处理动作。 在内核中的表示      信号在内核中的表示示意图         ●每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针(handler)表示处理动作。         ●在代码运行过程中,在用户层调用signal函数,对信号进行捕捉,若有信号被捕捉到,操作系统向进程发送信号,进程中pending位图置1,如果操作系统判断该信号可以立即执行,则不需要保存信号,例如  SIGINT 信号--正常处理(Term )。         ●如果是SIGQUIT信号需要保存处理--( Core),一旦产生SIGQUIT信号将被阻塞,当它的处理动作是用户自定义函数sighandler。此时需要接触对该信号的阻塞,然后用内核态转入到用户态,对用户态的handler进行执行。         ●如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。 地址空间第二讲 关于上述介绍,我们发现程序员写的代码是在用户态那一层,PCB则是在内核态一层。什么时候需要访问内核态呢?内核资源是通过什么访问的呢? 在这个进程中,有两块空间,其中一块是让用户使用的,另外的一块是让操作系统使用的。         例:这好比在学校,作为学生我们只能在自己的班级上课,而不能去其他班级。作为校长就可以随便去哪个班级。我们则是用户,校长就好比操作系统。 用户为了访问内核或者硬件资源都必须通过系统调用。但系统调用往往都比较耗时,因为系统调用会进行大量操作,所以我们应该尽量避免频繁的调用系统操作。                                                                                                                        这里介绍了进程中有用户态和内核态,也知道了如果用户态如果想访问操作系统本身的资源或者硬件资源时,需要通过系统调用访问,而且系统调用比较耗时。 系统调用是用户用相关系统调用接口实现访问内核资源,这个调用过程是CPU完成的,所以我们还是不理解,操作系统是如何跟进程联系起来的呢? 我们知道在CPU中有大量的寄存器         1.可见寄存器,如exa,exd等通用寄存器。         2.不可见寄存器,如状态寄存器,CR3等寄存器。 凡是这些寄存器与当前进程相关,进程就会存储寄存器的上下文数据--(保存了程序运行时寄存器的当中的内容:如一个进程在运行过程中被切换出去,上下文信息就保存了寄存器的信息,直到这个进程重新拥有cpu资源)。 在CPU中的诸多寄存器中,有指定寄存器保存task_struct的起始地址实现直接跳转到进程中,也有指定寄存器保存页表起始地址,还有CR3表示当前进程的运行级别的指定寄存器。上下文数据也有专门的寄存器保存。 知道了他们之间的联系,那么他们又是如何执行的呢?比如我是一个进程,怎么就跑到操作系统中去执行方法呢? 如上图,则是进程是如何调用到系统资源的原理图。         ●每个进程的数据都被保存到相应的寄存器中,当在用户空间执行程序时,相关上下文的寄存器运行。当系统识别到用户通过系统调用访问内核数据时,在CPU中这个系统调用接口,在起始的位置就会帮你调整进程的运行级别,系统调用接口会通过Int 80-陷入内核(在设计系统接口时就已经编写好的),Int80就会用到CR3寄存器。改变运行级别:将级别0变成1。         ●而且每个进程都有3-4G的内核空间,都会共享内核级页表,无论进程如何切换都不会改变3-4的内核数据资源。所以在CPU中的指定寄存器中改变运行级别后,直接在mm_struct直接实现跳转获取相关的内核数据。 sigset_t 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。 sigset_t实质就是结构体封装的一个数组,在c++中bitset也讲过--位图。 # define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct   {     unsigned long int __val[_SIGSET_NWORDS];   } __sigset_t; #endif 信号集操作函数  sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统 实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做 任何解释,比如用printf直接打印sigset_t变量是没有意义的 。 #include int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo);         ●函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。         ●函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。         ●注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。         ●这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。   sigprocmask  调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)--block。 #include int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。  如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。 sigpending #include int sigpending(sigset_t *set); 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。  面用刚学的几个函数做个实验。程序如下: #include #include #include #include //#define BLOCK_SIGNAL 2 #define MAX_SIGNUM 31 static std::vector sigarr = {2}; static void show_pending(const sigset_t &pending) { for(size_t signal=MAX_SIGNUM; signal > 0; --signal) { if(sigismember(&pending,signal)) { std::cout<< "1"; } else { std::cout<< "0"; } } std::cout<        int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); signum:         指定信号的编号,并且可以是除SIGKILL和SIGSTOP之外的任何有效信号。 act,oldact,sigaction :         若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传 出该信号原来的处理动作。act和oact指向sigaction结构体 : struct sigaction {                void     (*sa_handler)(int);                void     (*sa_sigaction)(int, siginfo_t *, void *);                sigset_t   sa_mask;                int        sa_flags;                void     (*sa_restorer)(void);            };                 ●将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。                 ●sa_sigaction和sa_restorer一般设置为nullptr,sa_flags设置为0,都不管它。                 ●sa_mask其定义类型sigset_t   ,在上诉中已经讲过,本质是数组,用结构体封装的数组。其中可包括定义block,pending位图的信号集。 return val:         returns 0 on success; on error, -1 is returned, and errno is set to indicate the error. 下面通过代码测试来熟悉sigaction,我们代码大致实现的功能:调用sigaction对SIGINT信号进行捕捉,捕捉到SIGINT信号后调用handler方法,在SIGINT信号发生打印确认捕捉,细节睡眠10秒。程序如下: #include #include #include using namespace std; void Count(int cnt) { while (cnt--) { cout<< cnt<<" "; fflush(stdout); sleep(1); } cout<next=head。但此时调用了handler方法,第二步:就从head = node1变成了node2->next=head,最后head会先:head=node2,然后head=node1,在同一步进行了,就会出现head只链接head1的问题。 具体解释如下:         ●main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。         ●像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称 为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的 控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?         一般而言,main执行流与信号捕捉是两个执行流         1.如果在main中,和在handler中,该函数被重复进入,出现问题--该函数(insert)不可重入函数            1.如果在main中,和在handler中,该函数被重复进入,未出现问题--该函数(insert)可重入函数 首先我们应该明白不可重入函数不是一个问题,而是在单执行流下的特性。因为在很多场景下我们是在单执行流下调用,该函数的起始目的也不是为了在多执行流下调用的。所以不是不可重入函数出现了问题,而是用户调用是没有想明白而已。 volatile  相信大家在c语言就已经对volatile关键字涉猎了,一段代码中加volatile与不加volatile查看汇编代码后,得出结论是:volatile忽略编译器的优化,保持内存可见性。 在gcc中也有编译器的优化级别,我们通过man gcc查看 name=value -O1,-O2,-O3,-Os,-Ofast 这里先看不被优化的程序: #include #include int quit =0; void handler(int signo) { printf("%d 信号已经被捕捉!\n",signal); printf("quit -> %d\n",quit); quit=1; printf("-> %d\n",quit); } int main() { signal(2,handler); while(!quit); printf("注意,我是正常退出!\n"); return 0; } 该代码:如果未发送SIGINT,程序一直循环,当发送SIGINT信号,信号被捕捉,进入handler将quit置1,然后正常退出。 [hongxin@VM-8-2-centos 2023-4-7]$ ./mysignal  ^C4195520 信号已经被捕捉! quit -> 0 -> 1 注意,我是正常退出! 当我们将gcc的运行级别改成-O不退出3时,我们再观察发现,改代码 [hongxin@VM-8-2-centos 2023-4-7]$ ./mysignal  ^C4195520 信号已经被捕捉! quit -> 0 -> 1 | 操作系统中与CPU的关系,CPU相当于毛坯房,操作系统是装修。那么对于CPU会进行以下几个步骤:         1.取指令         2.分析指令          3.执行命令         4.将结果写会对应的内存 其原理图如下: 如何解决这个问题呢?我们直接在quit前volatile,程序正常。 volatile int quit =0; [hongxin@VM-8-2-centos 2023-4-7]$ ./mysignal  ^C4195520 信号已经被捕捉! quit -> 0 -> 1 注意,我是正常退出! volatile:保持内存可见性。         由于gcc被优化,在代码中如果需要访问内存数据,就需要加volatile,其目的是为了保持内存的可见性,让寄存器能够访问内存数据。相反,不能不保持内存可见性,那么在用户态中quit的临时数据就不会被改变,也不会向内存中访问被修改的数据。 SIGCHLD信号 - 选学了解 进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。 请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。 #include #include #include #include int quit = 0; void handler(int signo) { printf("pid : %d,%d信号,正在被捕捉\n",getpid(),getppid()); } void Count(int cnt) { while (cnt) { printf("cnt: %2d\r", cnt); fflush(stdout); cnt--; sleep(1); } printf("\n"); } int main() { // 显示的设置对SIGCHLD进行忽略 signal(SIGCHLD, handler); //signal(SIGCHLD, SIG_DFL); printf("我是父进程, %d, ppid: %d\n", getpid(), getppid()); pid_t id = fork(); if (id == 0) { printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid()); Count(5); exit(1); } while (1) sleep(1); return 0; } 测试结果: [hongxin@VM-8-2-centos 2023-4-8]$ ./mysignal  我是父进程, 2945, ppid: 21647 我是子进程, 2946, ppid: 2945,我要退出啦 cnt:  1 pid : 2945,21647信号,正在被捕捉 该上面是证明了子进程退出会向父进程发送 SIGCHLD信号,但未对父进程在信号处理函数中调用wait清理子进程,下面就是在handler中wait清理子进程的代码和解释。 void handler(int signo) { //1.有很多子进程,在同一个时刻退出 //--在同一时刻退出也必须依次退出,必须while退完 //2.有很多子进程,在同一时刻只有一步部分退出 //--尽管只有一部分退出,对于系统而言,它是不知道到底有多少个进程需要退出,那么只有退完之后才知道 //--在waitpid中默认的是阻塞是等待,如果没有退出完,就会发生僵尸 //--所以我们将等待改成非阻塞, while (1) { //如果指定了WNOHANG,并且存在一个或多个由pid指定的子(ren),但尚未更改状态,则返回0。出现错误时,返回-1。 pid_t ret = waitpid(-1,NULL,WNOHANG); if (ret == 0) { //ret==0 则就是 waitpid调用成功 && 子进程没退出 //子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.那么继续等待退出即可 break; } else if(ret > 0) { //waitpid调用成功 && 子进程退出成功 printf("wait child success %d\n ",ret); } printf("child is quit! %d\n",getpid()); } printf("pid : %d,%d信号,正在被捕捉\n",getpid(),getppid()); } 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。 显示的设置对SIGCHLD进行忽略,在signal中将处理动作设置为SIG_IGN,代码段如下:                                 signal(SIGCHLD, SIG_DFL); 为了更好的观察,用两个进程来演示,一个用来跑./mysignal,一个用来跑脚本代码,脚本代码如下:                       while :; do ps ajx | head -1 | ps ajx| grep  mysignal; sleep 1; echo "----------------"; done 这里需要注意的是,grep本身调用也是一个进程,这里我们不把它参考进来。其主要效果是mysignal运行是有父进程和子进程,但五秒后,子进程被系统自动回收。 最后一个问题,通过man 7 signal查看SIGURG,不是说17号信号本身属性就是lgn吗?为什么调用signal时还要加上SIG_IGN处理方法?          SIGCHLD   20,17,18    Ign     Child stopped or terminated 默认设置和手动设置表现出来的特性是不一样的,SIGCHLD就好像是操作系统自动去识别默认,进行默认处理;SIG_IGN是告诉操作系统默认回收。    [ 作者 :   includeevey  📃  [ 日期 :   2023 / 4 / 1     [ 代码 :  卿洪欣 (hong-xin-qing) - Gitee.com   📜  [ 声明 : 到这里就该说再见了,若本文有错误和不准确之处,恳望读者批评指正!                     有则改之无则加勉!若认为文章写的不错,一键三连加关注!           
香港云服务器租用推荐
服务器租用资讯
·广东云服务有限公司怎么样
·广东云服务器怎么样
·广东锐讯网络有限公司怎么样
·广东佛山的蜗牛怎么那么大
·广东单位电话主机号怎么填写
·管家婆 花生壳怎么用
·官网域名过期要怎么办
·官网邮箱一般怎么命名
·官网网站被篡改怎么办
服务器租用推荐
·美国服务器租用
·台湾服务器租用
·香港云服务器租用
·香港裸金属服务器
·香港高防服务器租用
·香港服务器租用特价