• Linux 进程信号


    目录

    一.信号

    1.介绍

    2.信号概念

    3.查看系统定义的信号列表

    4.信号处理的方式

    二.信号产生前

    1.用户层产生信号的方式

    三.产生信号

    1.通过按键产生信号

    2.调用系统函数向进程发信号

    (1)kill 

    (2)raise 

    (3)abort

    3.软体条件产生信号

    4.硬件异常产生信号

    四.阻塞信号

    1.信息其它相关概念

    2.信号在内核中的表示:

    3.sigset_t

    4.信号集操作函数

    5.sigprocmask

    6.sigpending

    五.捕捉信号

    1.如何实现信号的捕捉

    2.sigaction

    六.可重入函数

     七.volatile

    八.SIGCHLD


    前言:这一篇将介绍Linux进程信号,包括产生信号前、产生信号时和产生信号后3个阶段,已经可重入函数、volatile等。            

    一.信号

    1.介绍

            信号是给进程发送的,进程要具备处理信号的能力。

    ① 该能力一定是预先早就已经有了的(程序员写的 ---- OS提供)

    ② 进程能够识别对应的信号

    ③ 进程能够处理对应信号

    对于进程来讲,即使是信号还没有产生,进程已经具有识别和处理这个信号的能力了。

    1. #include
    2. #include
    3. int main()
    4. {
    5. while(1)
    6. {
    7. printf("hello linux\n");
    8. sleep(1);
    9. }
    10. return 0;
    11. }

    正常运行一个进程,直接用ctrl+c就可以终止,并且按任何命令都是没有效果的。

    但是如果我们用在后面加一个&,就会将这个进程变成后台进程了:

            看这张图,在后面加了&后,会发现按ctrl+c无法终止。但是按命令是有效果的,因为这是后台进程,不会影响命令。

            如果想要查看该后台进程,可以按jobs,看到当前后台进程。

            如果想要将后台进程变为前台进程,可以按fg [jobs中对应的号],这里对应的是1,就是fg 1。这时,按ctrl+c就可以结束进程了。

    jobs

    fg [jobs中对应的号]

            下面我们去掉打印。

    1. #include
    2. #include
    3. int main()
    4. {
    5. while(1)
    6. {
    7. // printf("hello linux\n");
    8. sleep(1);
    9. }
    10. return 0;
    11. }

    这里可以通过jobs查看所有创建的后台进程,然后这里把3进程转为前台进程,并结束。

            如果按了fg之后,又不想转换成后台进程了,就可以先按ctrl+z,这里通过jobs可以看到进程从Running变成了Stopped,然后再用bg [jobs中对应的号],即可变回Running。

    bg [jobs中对应的号]

    ① Ctrl-c 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

    ② Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。

    ③ 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

    2.信号概念

            信号是进程之间事件异步通知的一种方式,属于软中断。

    3.查看系统定义的信号列表

    kill -l

    注意看这里31到34之间没有信号,32、33都是没有的,0号信号也没有。

    因此呢,这个信号是可以分成两组的,一组是1号到31号,另一组的34号到64号。

    1号到31号是普通信号,34号到64号是实时信号。

    4.信号处理的方式

            因为信号产生是异步的,所以当信号产生的时候,对应的进程可能正在做更重要的事情,而这时我们进程就可以暂时不处理这个信号。

            进程暂时不处理这个信号,不代表这个信号不会被处理。OS是一定会记住这个信号的。

            信号的捕捉,递达处理动作(信号的处理):

    ① 默认动作

    ② 忽视

    ③ 自定义动作

    二.信号产生前

    1.用户层产生信号的方式

            信号是会被进程记住的,这个信号会被记录在进程的PCB中,包括了这个信号的所有属性。

            信号有没有产生:比特位在位图中的内容是1代表产生,0代表没有

            什么信号产生:比特位在位图中的位置

    take_struct{

            uint32_t sig

    }

            只有OS能之间修改这个take_struct内的数据位图。

            OS是进程的管理者,进程的所有的属性的获取和设置,只能由OS来操作。

            无论信号怎么产生,最终一定只能是OS来进行信号的设置。

    三.产生信号

    1.通过按键产生信号

     ctrl-c其实就是一种信号,是2号信号

    sighandler_t signal(int signum, sighandler_t handler)

    sighandler_t:返回值为void,参数为int的一个函数指针

    signum:对哪个信号设置捕捉动作

    handler:是一个函数指针,这个函数允许用户自定义对信号的处理动作

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    8. }
    9. int main()
    10. {
    11. // 2号信号
    12. // 这里不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
    13. // 如果不产生SIGINT,该方法不会被调用
    14. // ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
    15. // 现在更改了对2号信号的处理,设置了用户自定义处理动作
    16. signal(SIGINT, handler);
    17. sleep(3);
    18. cout << "进程已经设置完了" << endl;
    19. sleep(3);
    20. while (true)
    21. {
    22. cout << "我是一个正在运行中的进程: " << getpid() << endl;
    23. sleep(1);
    24. }
    25. // while(1)
    26. // {
    27. // // printf("hello linux\n");
    28. // sleep(1);
    29. // }
    30. return 0;
    31. }

    这里的SIGINT就是2号信号: 

    结果为: 


        signal不是调用hander方法,这里只是设置了一个会回调,让SIGINT产生的时候,该方法才会被调用
        如果不产生SIGINT,该方法就不会被调用
        ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
        现在更改了对2号信号的处理,设置了用户自定义处理动作

    下面再试一下3号信号

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    8. }
    9. int main()
    10. {
    11. // 2号信号
    12. // 这里不是调用hander方法,这里只是设置了一个会回调,让SIGINT(2)产生的时候,该方法才会被调用
    13. // 如果不产生SIGINT,该方法不会被调用
    14. // ctrl+c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
    15. // 现在更改了对2号信号的处理,设置了用户自定义处理动作
    16. signal(SIGINT, handler);
    17. sleep(3);
    18. cout << "进程已经设置完了" << endl;
    19. sleep(3);
    20. while (true)
    21. {
    22. cout << "我是一个正在运行中的进程: " << getpid() << endl;
    23. sleep(1);
    24. }
    25. // while(1)
    26. // {
    27. // // printf("hello linux\n");
    28. // sleep(1);
    29. // }
    30. return 0;
    31. }

    这里我们就会发现无论是按ctrl+c,还是按ctrl+\都无法结束进程,都会去调用自定义处理动作。 

    那么如何结束呢:

            如果把9号信号也给signal自定义处理动作了,那就无法kill -9来结束了,那么把1到31的信号全部都用signal,自定义处理动作了,我们如何结束?

            其实这个是无法对9号进程signal自定义处理的,OS决定的。

    用户层产生信号的方式:

    ① 键盘产生

            注意这个是键盘产生的信号,不是键盘发送的信号,是OS发送的信号。

    那么OS是怎么发送信号的呢?

            OS能找到每个进程的take_struct,也能找到当前显示器上前台进程的take_struct,每一个进程的take_struct内部都有一个位图,OS在拿到了对应的信号后,将这个对应的位置由0设为1,OS就完成了信号的发送(OS发送信号,也可以说成是写入信号)

    2.调用系统函数向进程发信号

    (1)kill 

    这个kill不只是一个指令,也是一个函数,下面来介绍该函数:

    int kill(pid_t pid, int sig)

    作用:向指定进程发送指定信号

    成功了就是0,失败了就是-1 

    接下来实现一个我们自己的kill:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. static void Usage(const std::string& proc)
    10. {
    11. cerr << "Usage:\n\t" << proc << " signo pid" << endl;
    12. }
    13. // 写一个kill命令,mykill 9 ...
    14. int main(int argc, char *argv[])
    15. {
    16. if(argc != 3)
    17. {
    18. Usage(argv[0]);
    19. exit(1);
    20. }
    21. if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
    22. {
    23. cerr << "kill: " << strerror(errno) << endl;
    24. exit(2);
    25. }
    26. }

    结果如下: 

    通过运行我们的进程,并且输入对应的参数,就可以实现kill指令的作用。

    如果我们随便kill一个进程,是没有权限的。

    (2)raise 

    int raise(int sig);

    作用:给自己发任意信号

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void handler(int signo)
    10. {
    11. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    12. }
    13. int main()
    14. {
    15. signal(2, handler);// 自定义捕捉动作handler
    16. while(1)
    17. {
    18. sleep(1);
    19. raise(2);
    20. }
    21. }

    这里可以看到,使用了raise,就一直给自己发送上面自定义捕捉动作的2号信号。

    (3)abort

    void abort(void);

    作用:终止进程(给自己发6号信号: SIGABRT)

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void handler(int signo)
    10. {
    11. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    12. }
    13. int main()
    14. {
    15. signal(2, handler);// 自定义捕捉动作handler
    16. while(1)
    17. {
    18. sleep(1);
    19. abort(2);
    20. }
    21. }

    调用abort,之间就终止了进程。

            这个6号信号是可以被捕捉的,但是捕捉后依旧会退出。

    3.软体条件产生信号

    unsigned int alarm(unsigned, int seconds);

    作用:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. int cnt = 0;
    10. int main()
    11. {
    12. // 统计这个进程1s,cnt++多少次
    13. signal(SIGALRM, handler);
    14. alarm(1);
    15. while(1)
    16. {
    17. printf("hello : %d\n", cnt++);
    18. }
    19. }

    这里可以看到,printf了3万多次后,就结束了,说明在这1s内进行了3万多次的printf

    我们试试不带IO,测试一下:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. int cnt = 0;
    10. void handler(int signo)
    11. {
    12. cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl;
    13. exit(1);
    14. }
    15. int main()
    16. {
    17. // 统计这个进程1s,cnt++多少次
    18. signal(SIGALRM, handler);
    19. alarm(1);
    20. while(1)
    21. {
    22. cnt++;
    23. }
    24. }

    这个次数已经达到了4亿多,是带printf的1万多倍,这完全可以说明IO的慢。

    4.硬件异常产生信号

    我们一定见过进程崩溃的样子,那么崩溃的本质是是什么呢?

            进程崩溃的本质,是该进程收到了异常信号。

    下面介绍2个因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void handler(int signo)
    10. {
    11. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    12. exit(1);
    13. }
    14. // 写一个kill命令,mykill 9 ...
    15. int main(int argc, char *argv[])
    16. {
    17. // 进程崩溃的本质,是该进程收到了异常信号
    18. for(int sig = 1; sig <= 31; sig++)
    19. {
    20. signal(sig, handler);
    21. }
    22. // int a[10];
    23. // a[10000] = 1000;
    24. // int *p = nullptr;
    25. // *p = 100;
    26. // int b = 10;
    27. // b /= 0;
    28. }

    这里我们依次测试下面注释掉的内容: 

            根据测试结果,我们可以看到,第一个越界和第二个野指针错误,都是11号信号,而第三个除零错误是8号信号。

    除零:

            当我们除零的时候,CPU内的状态寄存器会被设置成为:有报错:浮点数越界

            CPU的内部寄存器是硬件,OS就会识别到CPU内有报错了。

            OS知道了是谁报的错,并且是什么报错后,就会让OS->构造信号->向目标进程发送信号->目标进程在合适的时候,处理信号->终止进程

    越界&&野指针:

            在语言层面使用的地址(指针),其实都是虚拟地址->转换成物理地址->访问物理内存->读取对应的数据和代码

            如果虚拟地址有问题,地址转换的工作是由(MMU(硬件) + 页表(软件))做的,转化过程就会引起问题->表现在硬件MMU上->OS就发现硬件出现了问题

            OSOS知道了是谁出的问题,并且是什么问题后,就会让OS->构造信号->向目标进程发送信号->目标进程在合适的时候,处理信号->终止进程

    那么崩溃了,一定会导致进程终止吗?

            不一定。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void handler(int signo)
    10. {
    11. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    12. //exit(1);
    13. }
    14. int main(int argc, char *argv[])
    15. {
    16. // 进程崩溃的本质,是该进程收到了异常信号
    17. for(int sig = 1; sig <= 31; sig++)
    18. {
    19. signal(sig, handler);
    20. }
    21. int b = 10;
    22. b /= 0;
    23. return 0;
    24. }

    这里我们把自定义捕捉动作中的exit去掉

    根据结果可以看到,虽然崩溃了,但是进程一直在运行。 

            因为我们对其进行了自定义捕捉,修改了默认动作,所以进程没有退出。如果我们不对其进行捕捉,采用默认动作,那么进程还是会退出的。

            因此,出现了异常,进程是否被终止,并不是完全由OS决定的,是由用户来决定的。

            在C++的异常中,我们一般抛出异常后就直接调用exit等函数退出了,那么如果我们不退出会怎么样呢?

            进程就不会被终止,需要我们手动对进程进行终止。

    四.阻塞信号

    1.信息其它相关概念

    递达:实际执行信号的处理动作称为信号递达(Delivery)

    未决:信号从产生到递达之间的状态,称为信号未决(Pending)。

    阻塞:进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

    注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

    2.信号在内核中的表示:

    block:阻塞信号集,和pending都是位图,对应的比特位为1,就会拦截对应的信号去递达对应的方法,即使pending为1也没用。

    pending:如果pending为1,表示该信号不能被递达,不会被处理。如果pending为0,表示该信号可以被递达,可以被处理。

    handler:用来处理信号,信号的编号就作为这个函数指针的数组下标,直接可以访问到对应的自定义的方法,或者系统默认的处理方法。

    这3个应该先看pending位,如果pending为1,再去看block,如果block为0,再去看handler。

            这里提到了阻塞信号,而我们之前学知道忽略信号,这两个有什么区别呢?

    忽略信号:处理信号的一种,处理的方式是忽略,什么都不做,将pending位图由1置0。

    阻塞信号:不是处理信号,是拦截信号,不允许去处理信号。

    3.sigset_t

            每个信号只有一个bit的未决标志,非0即1,不会记录该信号产生了多少次,阻塞标志也是这样表示的。
            因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。

    4.信号集操作函数

            sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

    ① 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);

            第1个是清空位图,第2个是将位图全置1,第3个是添加一个信号,第4个是删除一个信号,第5个是判断某个。

    5.sigprocmask

     int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

    作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)

    第一个参how数有3个选择:

    ① SIG_BLOCK:添加信号屏蔽字,相当于 mask = mask | set

    ② SIG_UNBLOCK:从信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set

    ③ SIG_SETMASK:设置当前信号屏蔽字为set所值的值,相当于mask = set

    第二个参数set是输入型参数:代表的是新增、删除或覆盖式的重新添加。

    第三个参数oset是输出型参数:返回老的信号屏蔽字

            如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

    6.sigpending

    int  sigpending(sigset_t *set);

    作用:获取进程的pending信号集,通过set参数传出。

    调用成功就返回0,出错则返回-1

    下面我们来看一下当前进程的pending信号集:

            如果直接获取2号,会因为递达的太快,我们没等看到打印出来,就已经结束了,所以这里我们利用信号屏蔽字,把2号信号屏蔽,来看:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. void handler(int signo)
    11. {
    12. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    13. exit(1);
    14. }
    15. static void showPending(sigset_t *pendings)
    16. {
    17. for(int sig = 1; sig <= 31; ++sig)
    18. {
    19. if(sigismember(pendings, sig))
    20. {
    21. cout << "1";
    22. }
    23. else
    24. {
    25. cout << "0";
    26. }
    27. }
    28. cout << endl;
    29. }
    30. int main()
    31. {
    32. // 3.屏蔽2号信号
    33. sigset_t bsig, obsig;
    34. sigemptyset(&bsig);
    35. sigemptyset(&obsig);
    36. // 3.1添加2号信号到信号屏蔽字中
    37. sigaddset(&bsig, 2);
    38. // 3.2设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
    39. sigprocmask(SIG_SETMASK, &bsig, &obsig);
    40. // 2.signal
    41. signal(2, handler);
    42. // 1.不断的获取当前进程的pending信号集
    43. sigset_t pendings;
    44. while(true)
    45. {
    46. // 1.1 清空信号集
    47. sigemptyset(&pendings);
    48. // 1.2 获取当前进程(谁调用,获取谁)的pending信号集
    49. if(sigpending(&pendings) == 0)
    50. {
    51. // 1.3 打印当前进程的pending信号集
    52. showPending(&pendings);
    53. }
    54. sleep(1);
    55. }
    56. return 0;
    57. }

            这里就可以看到,我们调用2号信号时,因为信号屏蔽字的原因,pending里对应的位置变成了1,但是却并没有去调用2号信号,因为2号信号被屏蔽了。

            那么如果我们把信号全部屏蔽了会怎么样呢?

            从1开始屏蔽,一直到8都可以成功,但是到9的时候,就会直接把进程killed,而不会被屏蔽。

    这里我们把信号全部屏蔽了,然后后面再把2号信号解除,来看一下测试结果:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. int cnt = 0;
    11. void handler(int signo)
    12. {
    13. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    14. // exit(1);
    15. }
    16. static void showPending(sigset_t *pendings)
    17. {
    18. for(int sig = 1; sig <= 31; ++sig)
    19. {
    20. if(sigismember(pendings, sig))
    21. {
    22. cout << "1";
    23. }
    24. else
    25. {
    26. cout << "0";
    27. }
    28. }
    29. cout << endl;
    30. }
    31. int main()
    32. {
    33. cout << "pid: " << getpid() << endl;
    34. // 3.屏蔽2号信号
    35. sigset_t bsig, obsig;
    36. sigemptyset(&bsig);
    37. sigemptyset(&obsig);
    38. // // 3.1添加2号信号到信号屏蔽字中
    39. // sigaddset(&bsig, 2);
    40. // 全屏蔽
    41. for(int sig = 1; sig <= 31; ++sig)
    42. {
    43. sigaddset(&bsig, sig);
    44. signal(sig, nullptr);
    45. }
    46. // 3.2设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
    47. sigprocmask(SIG_SETMASK, &bsig, &obsig);
    48. // 2.signal
    49. // signal(2, handler);
    50. // 1.不断的获取当前进程的pending信号集
    51. sigset_t pendings;
    52. while(true)
    53. {
    54. // 1.1 清空信号集
    55. sigemptyset(&pendings);
    56. // 1.2 获取当前进程(谁调用,获取谁)的pending信号集
    57. if(sigpending(&pendings) == 0)
    58. {
    59. // 1.3 打印当前进程的pending信号集
    60. showPending(&pendings);
    61. }
    62. sleep(1);
    63. ++cnt;
    64. if(cnt == 20)
    65. {
    66. cout << "解除对2号信号的block..." << endl;
    67. sigset_t sigs;
    68. sigemptyset(&sigs);
    69. sigaddset(&sigs, 2);
    70. sigprocmask(SIG_UNBLOCK, &sigs, nullptr);
    71. }
    72. }
    73. return 0;
    74. }

            根据结果可以看到,我们在前面kill 一直到了7都没有任何反应,而等到了20s之后,就解除了对2号信号的屏蔽,2号信号就可以递达了,显示器上便出现了自定义捕捉动作的2号信号。

    五.捕捉信号

    1.如何实现信号的捕捉

    进程处理信号,不是立即处理的,是在合适的时候。

    那么这个合适的时候,是什么时候呢?

            当 当前进程从内核态,切换会用户态的时候,进行信号的检测与处理。

            OS是在内存中被加载的。

            无论进程怎么切换,都可以找到内核的代码和数据,前提是你只要能够有权利访问。

    当前进程如何具备权利,访问这个内核页表,乃至访问内核数据呢?

            要进行身份切换。

            进程如果是用户态的--只能访问用户级页表。

            进程如果是内核态的--访问内核级和用户级的页表。

    那么我们怎么知道自己是用户态还是内核态的呢?

            CPU内部有对应的状态寄存器CR3,有比特位标识当前进程的状态,0:内核态;3:用户态

    什么时候能切换到内核态呢?

            ① 系统调用的时候

            ② 时间片到了,进程间切换

            ③ 等等

    内核态:可以访问所有的代码和数据(不是意味着它一定要访问所有的) -- 具备更高权限

    用户态:只能访问自己的。

            我们的进程,会无数次直接或者间接的访问系统级软硬件资源(管理者是OS),本质上,我们并没有自己去操作这些软硬件资源,而是必须通过OS->无数次的陷入内核(1.切换身份 2.切换页表)->调用内核的代码->完成访问的动作->结果返回给用户(1.切换身份 2.切换页表)->得到结果。

            即使是像while(1)这样的代码也是会进行内核态和用户态的切换的。

            因为它有自己的时间片->时间片到了的时候->内核态,更换内核级页表->保护上下文,执行调度算法->选择了新的进程->恢复新进程的上下文 ->用户态,更换成用户级页表->CPU执行的就是新进程的代码。        

            如果有一些系统调用,就会从用户态切换到内核态。

            调用完该接口后,去识别信号,如果pending表为1,block表为0,会去进行自定义处理动作,就会从内核态切换到用户态(注意:这里一定要切换,虽然内核态可以完成用户态的操作,但是如果用户写的是一段恶意代码,那么因为内核态的权限过大,无论什么代码都会执行,就会导致OS受到恶意攻击,而切换成用户态就可以因为权限小而不会去执行该代码)。

            把该动作由用户态执行完,就又会从用户态切换回内核态。

            都处理完之后,就要返回了,而这返回又要从内核态切换回用户态。

    简而言之,上面的过程就相当于数学中的无穷大符号∞

    2.sigaction

    int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

    作用:检查并且更改一个信号的动作。(设置对特定信号的特定处理的动作)

    signo:对哪个信号实施信号自定义捕捉

    act:要设置成什么动作

    oact:老的动作,如果不要就设为nullptr;如果要就传入一个对象

    void (*sa_handler) :对信号的处理方法,充当回调

    sa_mask:需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

            mask作用:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字。当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段。

     下面我们来使用这个sigaction:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
    8. }
    9. int main()
    10. {
    11. struct sigaction act, oact;
    12. act.sa_handler = handler;
    13. act.sa_flags = 0;
    14. sigemptyset(&act.sa_mask);
    15. sigaction(2, &act, &oact);
    16. while(true)
    17. {
    18. sleep(1);
    19. }
    20. return 0;
    21. }

    成功对2号信号进行了自定义捕捉。

    下面改一下act.sa_handler:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
    8. }
    9. int main()
    10. {
    11. struct sigaction act, oact;
    12. act.sa_handler = SIG_IGN;
    13. act.sa_flags = 0;
    14. sigemptyset(&act.sa_mask);
    15. sigaction(2, &act, &oact);
    16. while(true)
    17. {
    18. sleep(1);
    19. }
    20. return 0;
    21. }

    这里无论怎么产生2号信号,都会忽略,SIG_IGN就是忽略该信号。

    再改一下act.sa_handler:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
    8. }
    9. int main()
    10. {
    11. struct sigaction act, oact;
    12. act.sa_handler = SIG_DFL;
    13. act.sa_flags = 0;
    14. sigemptyset(&act.sa_mask);
    15. sigaction(2, &act, &oact);
    16. while(true)
    17. {
    18. sleep(1);
    19. }
    20. return 0;
    21. }

    这里产生2号信号就直接退出了,SIG_DFL就是采取默认动作。

            上面说到当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,这里来测试一下:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
    8. sigset_t pending;
    9. // 增加handler信号的时间,永远都会处理2号信号
    10. while(true)
    11. {
    12. cout << "." << endl;
    13. sigpending(&pending);
    14. for(int i = 1; i <= 31; ++i)
    15. {
    16. if(sigismember(&pending, i))
    17. {
    18. cout << "1";
    19. }
    20. else
    21. {
    22. cout << "0";
    23. }
    24. }
    25. cout << endl;
    26. sleep(1);
    27. }
    28. }
    29. int main()
    30. {
    31. struct sigaction act, oact;
    32. act.sa_handler = handler;
    33. act.sa_flags = 0;
    34. sigemptyset(&act.sa_mask);
    35. sigaction(2, &act, &oact);
    36. while(true)
    37. {
    38. cout << "main running" << endl;
    39. sleep(1);
    40. }
    41. return 0;
    42. }

    这里虽然pending中有了,但是被屏蔽了,无法发送信号。

    再把3号信号加入进去:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "获取到一个信号,信号的编号是: " << signo << endl;
    8. sigset_t pending;
    9. // 增加handler信号的时间,永远都会处理2号信号
    10. while(true)
    11. {
    12. cout << "." << endl;
    13. sigpending(&pending);
    14. for(int i = 1; i <= 31; ++i)
    15. {
    16. if(sigismember(&pending, i))
    17. {
    18. cout << "1";
    19. }
    20. else
    21. {
    22. cout << "0";
    23. }
    24. }
    25. cout << endl;
    26. sleep(1);
    27. }
    28. }
    29. int main()
    30. {
    31. struct sigaction act, oact;
    32. act.sa_handler = handler;
    33. act.sa_flags = 0;
    34. sigemptyset(&act.sa_mask);
    35. sigaddset(&act.sa_mask, 3);
    36. sigaction(2, &act, &oact);
    37. while(true)
    38. {
    39. cout << "main running" << endl;
    40. sleep(1);
    41. }
    42. return 0;
    43. }

    可以看到ctrl+\也因为被屏蔽无法退出了。

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. // 局限性,需要对所有的信号进行捕捉
    7. std::vector<sighandler_t> handler;
    8. void Handler2()
    9. {
    10. cout << "hello 2" << endl;
    11. }
    12. void Handler3()
    13. {
    14. cout << "hello 3" << endl;
    15. }
    16. void Handler4()
    17. {
    18. cout << "hello 4" << endl;
    19. }
    20. void Handler5()
    21. {
    22. cout << "hello 5" << endl;
    23. }
    24. void Handler(int signo)
    25. {
    26. switch (signo)
    27. {
    28. case 2:
    29. Handler2();
    30. break;
    31. case 3:
    32. Handler3();
    33. break;
    34. case 4:
    35. Handler4();
    36. break;
    37. case 5:
    38. Handler5();
    39. break;
    40. default:
    41. break;
    42. }
    43. }
    44. int main()
    45. {
    46. signal(2, Handler);
    47. signal(3, Handler);
    48. signal(4, Handler);
    49. signal(5, Handler);
    50. return 0;
    51. }

    这种写法可以让耦合性更低。

    六.可重入函数

            main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是:main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

            像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

    如果一个函数符合以下条件之一则是不可重入的:

    ① 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
    ② 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

     七.volatile

    先看下面这段代码:

    1. #include
    2. #include
    3. int flags = 0;
    4. void handler(int signo)
    5. {
    6. flags = 1;
    7. printf("更改flags: 0->1\n");
    8. }
    9. int main()
    10. {
    11. signal(2, handler);
    12. while(!flags);
    13. printf("进程是正常退出的!\n");
    14. return 0;
    15. }

    这里的结果和我们的预期是相同的,因为发送了2号信号,就将flags由0置1了。

    但是这是在gcc编译器中,如果在别个编译器中就不一定是这种效果了。

            某些编译器会把这个flags保存到CPU的寄存器中,去调用flags时也时去调用这个寄存器里的,而这里更改的是内存中的flags,不是寄存器中的,这时就与我们的预期不同。

    这里我们更改一下编译器的编译层度:

    这时就无法按我们预期进行了。 

    volatile的作用就是保持内存的可见性:

            告诉编译器,不准对flags做任何优化

            每次CPU计算的时候,都必须从内存中拿数据。

    这里加上了volatile:

    1. #include
    2. #include
    3. // 保持内存的可见性
    4. volatile int flags = 0;
    5. void handler(int signo)
    6. {
    7. flags = 1;
    8. printf("更改flags: 0->1\n");
    9. }
    10. int main()
    11. {
    12. signal(2, handler);
    13. while(!flags);
    14. printf("进程是正常退出的!\n");
    15. return 0;
    16. }

    结果就又与预期相同了。

    volatile和const也并不冲突,可以同时使用。

    八.SIGCHLD

    子进程退出的时候,不是默默的退出,会自动给父进程发送SIGCHLD

    下面我们来验证一下:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "子进程退出了,我收到了信号: " << signo << "我是" << getpid() << endl;
    8. }
    9. int main()
    10. {
    11. signal(SIGCHLD, handler);
    12. pid_t id = fork();
    13. if(id == 0)
    14. {
    15. while(true)
    16. {
    17. cout << "我是子进程: " << getpid() << endl;
    18. sleep(1);
    19. }
    20. exit(0);
    21. }
    22. // parent
    23. while(true)
    24. {
    25. cout << "我是父进程: " << getpid() << endl;
    26. sleep(1);
    27. }
    28. return 0;
    29. }

     

    可以看到,子进程被杀掉之后,确实向父进程发送了SIGCHLD信号。

    其实,不只是子进程退出的时候会发送SIGCHLD信号,暂停和唤醒都会发送: 

    这里的 kill -19就是暂停,kill -18就是运行,都发送了SIGCHLD信号。

    之前我们所写的父子进程的进程等待,父进程是要自己主动等待的:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. int main()
    7. {
    8. pid_t id = fork();
    9. if(id == 0)
    10. {
    11. // 子进程
    12. int cnt = 10;
    13. while(cnt)
    14. {
    15. cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
    16. sleep(1);
    17. }
    18. cout << "子进程退出,进入僵尸状态" << endl;
    19. exit(0);
    20. }
    21. // 父进程,是要自己等待的
    22. if(waitpid(id, nullptr, 0) > 0)
    23. {
    24. cout << "父进程等待子进程成功" << endl;
    25. }
    26. return 0;
    27. }

    这里父进程会一直等待子进程退出。

    那么我们怎么才能让父进程不只是在等待子进程,可以让父进程去做别的事呢:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChild(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. pid_t id = waitpid(-1, nullptr, 0);
    12. if(id > 0)
    13. {
    14. cout << "父进程等待成功, child pid: " << id << endl;
    15. }
    16. }
    17. int main()
    18. {
    19. signal(SIGCHLD, FreeChild);
    20. pid_t id = fork();
    21. if(id == 0)
    22. {
    23. // 子进程
    24. int cnt = 10;
    25. while(cnt)
    26. {
    27. cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
    28. sleep(1);
    29. }
    30. cout << "子进程退出,进入僵尸状态" << endl;
    31. exit(0);
    32. }
    33. while(true)
    34. {
    35. cout << "我是父进程,我正在运行: " << getpid() << endl;
    36. sleep(1);
    37. }
    38. return 0;
    39. }

            按照上面的写法,是可以做到父进程不只是在等待子进程的,但是这种写法是有bug的,如果多个进程同时运行,就容易出现:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChild(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. pid_t id = waitpid(-1, nullptr, 0);
    12. if(id > 0)
    13. {
    14. cout << "父进程等待成功, child pid: " << id << endl;
    15. }
    16. }
    17. int main()
    18. {
    19. signal(SIGCHLD, FreeChild);
    20. for(int i = 0; i < 10; ++i)
    21. {
    22. pid_t id = fork();
    23. if(id == 0)
    24. {
    25. // 子进程
    26. int cnt = 10;
    27. while(cnt)
    28. {
    29. cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
    30. sleep(1);
    31. }
    32. cout << "子进程退出,进入僵尸状态" << endl;
    33. exit(0);
    34. }
    35. }
    36. while(true)
    37. {
    38. cout << "我是父进程,我正在运行: " << getpid() << endl;
    39. sleep(1);
    40. }
    41. return 0;
    42. }

            这时,我们子进程正常是应该已经全部退出了的,但是看右边,会发现还有好几个子进程处于僵尸状态,因为在同时退出的时候,一下发送了很多个SIGCHLD信号,当正在处理某一个信号时,其它一些信号就可能会被丢掉,导致bug。

    这里又会出现子进程没有都退出,但是父进程却不再运行的bug了。

            因为waitpid在等待子进程的时候,把前7个信号全部回收了,当它回收第8个进程时,第8个进程并没有退出,这时调用waitpid会被阻塞住。

    这里我们可以给waitpid传入WNOHANG,然后再进行修改,即可处理:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChild(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. while(true)
    12. {
    13. // 如果没有子进程了,waitpid 才会调用失败
    14. // -1: 等待任意一个子进程
    15. pid_t id = waitpid(-1, nullptr, WNOHANG);
    16. if(id > 0)
    17. {
    18. cout << "父进程等待成功, child pid: " << id << endl;
    19. }
    20. else if(id == 0)
    21. {
    22. // 还有子进程,但是现在没退出
    23. cout << "还有子进程,但是现在还没退出,父进程要去做自己的事情了" << endl;
    24. break;
    25. }
    26. else
    27. {
    28. cout << "父进程等待所有子进程结束" << endl;
    29. break;
    30. }
    31. }
    32. }
    33. int main()
    34. {
    35. signal(SIGCHLD, FreeChild);
    36. for(int i = 0; i < 10; ++i)
    37. {
    38. pid_t id = fork();
    39. if(id == 0)
    40. {
    41. // 子进程
    42. int cnt = 0;
    43. if(i < 7)
    44. cnt = 5;
    45. else
    46. cnt = 20;
    47. while(cnt)
    48. {
    49. cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
    50. sleep(1);
    51. }
    52. cout << "子进程退出,进入僵尸状态" << endl;
    53. exit(0);
    54. }
    55. }
    56. while(true)
    57. {
    58. cout << "我是父进程,我正在运行: " << getpid() << endl;
    59. sleep(1);
    60. }
    61. return 0;
    62. }

            如果我们不想麻烦,不想获取状态,也不想自己去管,想让子进程退出时直接退出僵尸进程,就可以这么写:

     signal(SIGCHLD, SIG_IGN);

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChild(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. while(true)
    12. {
    13. // 如果没有子进程了,waitpid 才会调用失败
    14. // -1: 等待任意一个子进程
    15. pid_t id = waitpid(-1, nullptr, WNOHANG);
    16. if(id > 0)
    17. {
    18. cout << "父进程等待成功, child pid: " << id << endl;
    19. }
    20. else if(id == 0)
    21. {
    22. // 还有子进程,但是现在没退出
    23. cout << "还有子进程,但是现在还没退出,父进程要去做自己的事情了" << endl;
    24. break;
    25. }
    26. else
    27. {
    28. cout << "父进程等待所有子进程结束" << endl;
    29. break;
    30. }
    31. }
    32. }
    33. int main()
    34. {
    35. signal(SIGCHLD, SIG_IGN);
    36. for(int i = 0; i < 10; ++i)
    37. {
    38. pid_t id = fork();
    39. if(id == 0)
    40. {
    41. // 子进程
    42. int cnt = 0;
    43. if(i < 7)
    44. cnt = 5;
    45. else
    46. cnt = 20;
    47. while(cnt)
    48. {
    49. cout << "我是子进程, pid: "<< getpid() << " 当前的cnt: " << cnt-- << endl;
    50. sleep(1);
    51. }
    52. cout << "子进程退出,进入僵尸状态" << endl;
    53. exit(0);
    54. }
    55. }
    56. while(true)
    57. {
    58. cout << "我是父进程,我正在运行: " << getpid() << endl;
    59. sleep(1);
    60. }
    61. return 0;
    62. }

            既然子进程退出的时候,默认的信号处理就是忽略,那么我们调用signal/sigaction SIG_IGN的意思在哪呢?

            SIG_IGN手动设置还是与默认的不一样的,默认的可能还会给父进程发送信号,但是手动设置,让子进程退出时,就不会给父进程发送信号了,并且会自动释放。

  • 相关阅读:
    K-means聚类算法一文详解+Python代码实例
    「分享购」2022年独特的商业模式,让平台实现流量增长
    AI斗地主-按键精灵
    Vue中巧用computed配合watch实现监听多个属性的变化
    第五十二天 数论
    ESP8266-Arduino编程实例-BME280环境传感器驱动
    MFC Windows 程序设计[228]之拖拽列表(附源码)
    阿里巴巴JAVA开发手册----(一)
    国内MES系统应用研究报告 | MES系统如何选型?
    【Linux系统化学习】进程的父子关系 | fork 进程
  • 原文地址:https://blog.csdn.net/qq_60750110/article/details/127808659