• <Linux系统复习>信号


    一、本章重点

    1、什么是信号?

    2、查看信号列表

    3、信号捕捉

    4、信号产生的5种方式

    5、介绍CoreDump

    6、信号处理的方式

    7、如何理解信号产生到处理的过程

    8、sigpending、sigprocmask、sigaction函数的使用

    9、信号处理的时机

    10、SIGCHLD信号

    11、可重入函数

    01 什么是信号?

    生活中的信号:红绿灯、下课铃声、闹钟铃声等等。

    当信号出现的时候,我们之前就知道信号该如何处理,并且不被要求立即处理该信号,因为有时我们正在处理重要的事情。(类比闹钟铃声)

    技术应用角度的信号:当进程执行除0代码的时候是如何被操作系统终止的?本质是因为进程收到了信号,在合适的时机处理该信号的时候被操作系统终止。

    02 查看信号列表

    如何查看Linux中信号列表?

    使用命令:kill -l

     需要知道的是没有32、33号信号,总共就62个信号。其中1-31是普通信号,34-64是实时信号。

    03 信号捕捉

    1、什么是信号捕捉?

    简单来说就是改变信号的默认处理方式

    介绍signal函数

     功能:对特定信号进行捕捉

    使用方式:signal(要捕捉的信号编号,要执行的处理方式)

    04 信号产生的5种方式

    1、代码异常(如:除零错误、对野指针解引用)

    2、命令行产生(如:kill -信号编号 pidkillall -信号编号 进程名

    3、键盘组合键产生(如:ctrl + c、ctrl + z、ctrl + \ ,分别对应2,20,3号信号)

    4、系统调用

    5、软件条件

    代码演示

    1、异常

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main()
    6. {
    7. int cnt = 5;
    8. while(true)
    9. {
    10. cout<<"I am a process"<
    11. if(cnt == 0)
    12. {
    13. int num = 3/0;//除零错误
    14. }
    15. cnt--;
    16. sleep(1);
    17. }
    18. return 0;
    19. }

     错误信息和信号列表对比大概可以看出除零错误属于8号信号

    用signal函数对8号信号进行捕捉,执行我们的自定义的处理方式

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout<<"收到了8号信号"<
    8. while(1)
    9. {
    10. cout<<"I am a process"<
    11. sleep(1);
    12. }
    13. }
    14. int main()
    15. {
    16. signal(8,handler);
    17. int cnt = 5;
    18. while(true)
    19. {
    20. cout<<"I am a process"<
    21. if(cnt == 0)
    22. {
    23. int num = 3/0;
    24. }
    25. cnt--;
    26. sleep(1);
    27. }
    28. return 0;
    29. }

     

    2、命令行产生

    ①kill -信号编号 pid

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main()
    6. {
    7. int cnt = 5;
    8. while(true)
    9. {
    10. cout<<"I am a process"<
    11. sleep(1);
    12. }
    13. return 0;
    14. }

    ②killall -信号编号 进程名

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main()
    6. {
    7. int cnt = 5;
    8. while(true)
    9. {
    10. cout<<"I am a process"<
    11. sleep(1);
    12. }
    13. return 0;
    14. }

    3、键盘组合键产生

    ①ctrl + c

    ②ctrl + z(强制当前进程转为后台,并使之停止)

    jobs可以显示后台进程,fg + 后台编号可以将进程从后台放置到前台运行。

     

     ./test是前台运行该进程,./test &代表的是后台运行该进程

    在后台运行进程时,解释器还能够解释命令,但由于都向同一个显示器打印,会造成互相干扰的情况,键盘组合键只能对前台进程有效,后台进程无效。

    要想杀掉后台进程有两种方式:1、先fg 1 再 ctrl +c       2、kill -2 pid

    ③ctrl + \

    如何知道ctrl + c、ctrl + z、ctrl + \分别给进程发的是什么信号?

    答:对所有信号捕捉,然后再发送信号,信号捕捉函数就可以打印发送的信号。

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void handler(int signo)
    7. {
    8. cout<<"进程收到了"<"号信号"<
    9. sleep(1);
    10. }
    11. int main()
    12. {
    13. for(int i = 1; i <= 31; i++)
    14. {
    15. signal(i,handler);
    16. }
    17. while(true)
    18. {
    19. cout<<"I am a process pid:"<<getpid()<
    20. sleep(1);
    21. }
    22. return 0;
    23. }

     ctrl + c:向进程发送2号信号

    ctrl + z:向进程发送20号信号

    ctrl + \:向进程发送3号信号(注:有的朋友可能ctrl + \没有反映,具体原因可以百度解决

    上述代码,还证明了并不上所有信号都能够被捕捉,比如9号信号就不能被捕捉,因为所有信号都能够被捕捉,那么这个进程真就“刀枪不入”了,连操作系统也拿它没办法了。

    4、系统调用产生

    ①kill

    功能:向任意进程发送任意信号

     写一个kill命令 

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main(int argc,char** argv)
    6. {
    7. if(argc!=3)
    8. {
    9. cout<<"Usage: kill -信号编号 pid"<
    10. return 1;
    11. }
    12. int signo = argv[1][1] - '0';
    13. //cout<
    14. int pid = atoi(argv[2]);
    15. //cout<
    16. kill(pid,signo);
    17. return 0;
    18. }

    ②raise

    功能:向当前进程发送任意信号

     

    ③abort

    功能:向当前进程发送abort信号

     

     这个函数有点特殊,因为你就算捕捉了该信号还是会终止进程。

    5、软件条件产生

    1、管道的读端关闭,写端会收到SIGPIPE信号,处理该信号时被操作系统杀掉。

    common.h

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #define PATH "./.fifo"
    11. using namespace std;

     server.cc

    1. #include "common.h"
    2. int main()
    3. {
    4. umask(0);
    5. if(mkfifo(PATH,0666)!=0)
    6. {
    7. cerr<<"mkfifo error"<
    8. return 1;
    9. }
    10. int fd = open(PATH,O_RDONLY);
    11. if(fd < 0)
    12. {
    13. cerr<<"open error"<
    14. return 2;
    15. }
    16. #define SIZE 1024
    17. char buf[1024] = {0};
    18. while(true)
    19. {
    20. ssize_t s = read(fd,buf,sizeof(buf)-1);
    21. if(s==0)
    22. {
    23. cout<<"写端关闭"<
    24. unlink(".fifo");
    25. close(fd);
    26. exit(0);
    27. }
    28. else if(s>0)
    29. {
    30. buf[s] = '\0';
    31. cout<<"client->server: "<
    32. }
    33. else
    34. {
    35. cerr<<"read error"<
    36. close(fd);
    37. exit(3);
    38. }
    39. }
    40. return 0;
    41. }

    client.cc

    1. #include "common.h"
    2. void handler(int signo)
    3. {
    4. cout<<"写端收到了"<"号信号"<
    5. exit(1);
    6. }
    7. int main()
    8. {
    9. signal(SIGPIPE,handler);
    10. int fd = open(PATH,O_WRONLY | O_APPEND);
    11. if(fd<0)
    12. {
    13. cerr<<"open error"<
    14. }
    15. while(true)
    16. {
    17. char msg[1024] = {0};
    18. cout<<"请输入信息# ";
    19. fflush(stdout);
    20. if(fgets(msg,sizeof(char)*1024,stdin)!=nullptr)
    21. {
    22. //msg为 xxxx\n\0
    23. //下面的write我只传了xxxx
    24. write(fd,msg,strlen(msg)-1);
    25. }
    26. else
    27. {
    28. exit(0);
    29. }
    30. }
    31. return 0;
    32. }

    2、alarm函数

    功能:自定义多少秒之后向进程发送一个alarm信号,该信号的默认处理方式是终止进程。

    测试1秒钟服务器能够对cnt++多少次

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. int cnt = 0;
    7. void handler(int signo)
    8. {
    9. cout<
    10. exit(1);
    11. }
    12. int main()
    13. {
    14. alarm(1);
    15. signal(SIGALRM,handler);
    16. while(true)
    17. {
    18. cnt++;
    19. }
    20. return 0;
    21. }

    05 介绍CoreDump

    1、什么是CoreDump?(核心转储)

    coredump是指当程序出错而异常中断时,OS会把程序工作的当前状态存储成一个coredump文件。然后我们可以通过该文件来定位异常的地方。

    2、一般线上生产环境都不会自动打开CoreDump,需要我们手动打开。

    ulimit -c 文件大小(一般1024的整数倍)

     

     3、试验一下CoreDump

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main()
    6. {
    7. cout<<"my pid is "<<getpid()<
    8. int* p = nullptr;
    9. *p = 3;
    10. return 0;
    11. }

    发生段错误,生成了core.25647文件,这个25647是什么意思呢?

    本质其实就是异常进程的pid

    生成了core.25647,我们可以通过gdb来分析CoreDump文件,定位异常。

     使用core-file core.26293可以导入coredump文件,然后gdb就可对该文件进行分析。

    分析结果:进程终止是因为收到了11号信号,异常的地方是在test.cc文件的第10行。

    06 信号处理的方式

    1、默认处理方式

    一般是终止该进程,可通过man 7 signal查看各个信号的默认处理方式

    2、忽略

    什么都不处理,只是将该信号从收到设置为未收到(ignore的缩写)

    3、自定义

    可通过signal函数或者sigaction函数自定义处理方式。

    07 信号屏蔽字&&信号未决表&&信号处理函数

    task_struct中存在三张表,分别是block表、pending表、handler表。

     block是一个32位的位图结构,从上到下0代表该信号没被阻塞,1代表该信号被阻塞。

    pedding也是一个32位的位图结构,从上到下0代表该信号没被收到,1代表该信号被收到。

    handler是一个函数指针数组,从上到下依次为每个信号对应的处理方式,SIG_DFL代表默认处理方式,SIG_IGN代表忽略。

    pedding表说白了就是记录进程收到了哪些信号。

    block表为了防止信号被处理而设计的,当某个信号被阻塞后,该信号就是递达了它都不会被处理,直到它被解除阻塞才能够被处理。

    解释一下,给进程test使用组合键ctrl + c产生信号,操作系统做了什么?

    首先,ctrl + c产生的是2号信号,因此操作系统会修改test进程控制块中的pending位图,将2号信号由0置为1,然后在合适时机处理该信号,当合适时机到来时,进程test执行2号信号的处理函数,由于该信号的处理函数是默认处理方式,那么操作系统会将test进程改为z状态并释放部分资源,最后被父进程回收test进程。

    08 sigpending、sigprocmask、sigaction函数的使用

    1、sigprocmask

    功能:可以读取或更改进程的信号屏蔽字

    成功返回0,出错返回-1

     其中how选项为SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK

    set为输入型参数,设置新的阻塞信号集。

    oldset为输出型参数,获取老的阻塞信号集。

    在此之前需要介绍一下sigset_t 类型

    sigset_t是一个32位的位图结构

    对它进行操作的常用函数有:

    ①sigemptyset(sigset_t* set)

    ②sigfillset(sigset_t* set)

    ③sigaddset(sigset_t* set , signo)

    ④sigdelset(sigset_t* set , signo)

    ⑤sigismember(const sigset_t* set , signo)

    现在我们使用sigprocmask函数对所有信号进行屏蔽

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main()
    6. {
    7. sigset_t new_block;
    8. sigset_t old_block;
    9. sigfillset(&new_block);
    10. sigemptyset(&old_block);
    11. sigprocmask(SIG_SETMASK,&new_block,&old_block);
    12. while(true)
    13. {
    14. cout<<"I am process"<
    15. sleep(1);
    16. }
    17. return 0;
    18. }

     可以发现,绝大多数信号可以被屏蔽,但9号信号是无法被屏蔽的

    使用一下sigprocmask中老的信号集

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main()
    6. {
    7. sigset_t new_block;
    8. sigset_t old_block;
    9. sigfillset(&new_block);
    10. sigemptyset(&old_block);
    11. sigprocmask(SIG_SETMASK,&new_block,&old_block);
    12. int cnt = 0;
    13. while(true)
    14. {
    15. cout<<"I am process"<
    16. sleep(1);
    17. if(cnt == 5)
    18. {
    19. cout<<"信号屏蔽字已经恢复到原来"<
    20. sigprocmask(SIG_SETMASK,&old_block,nullptr);
    21. }
    22. cnt++;
    23. }
    24. return 0;
    25. }

     从上述现象可以看出,当有多个信号同时到来的时候,先处理信号编号小的。

    2、sigpending

    成功返回0,失败返回-1。

    功能:读取当前进程的未决信号集

    代码测试:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void ShowPending(const sigset_t& pending)
    6. {
    7. for(int i = 1; i<=31; i++)
    8. {
    9. if(sigismember(&pending,i))
    10. {
    11. cout<<"1";
    12. }
    13. else
    14. {
    15. cout<<"0";
    16. }
    17. }
    18. cout<
    19. }
    20. int main()
    21. {
    22. sigset_t block;
    23. sigfillset(&block);
    24. sigprocmask(SIG_SETMASK,&block,nullptr);//阻塞所有信号
    25. sigset_t pending;
    26. sigemptyset(&pending);
    27. while(true)
    28. {
    29. sigpending(&pending);
    30. ShowPending(pending);
    31. sleep(1);
    32. }
    33. return 0;
    34. }

    3、sigaction

    成功返回0,失败返回-1

    功能:可以读取和修改与指定信号相关联的处理动作

     sa_handler:设置普通信号的处理方式

     sa_sigaction:实时信号的处理方式,这里我们不管。

     sa_mask:在信号处理时,设置额外需要屏蔽的信号

     sa_flags:用来处理普通信号,设置为0就行。

     sa_restirer:不管。

    代码测试

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout<<"收到"<"号信号"<
    8. }
    9. int main()
    10. {
    11. //signal(2,handler);
    12. struct sigaction act;
    13. act.sa_handler = handler;
    14. act.sa_flags = 0;
    15. sigaction(2,&act,nullptr);//相当于signal(2,handler);
    16. while(true)
    17. {
    18. cout<<"I am a process"<
    19. sleep(1);
    20. }
    21. return 0;
    22. }

    09 信号处理的时机

    信号处理的时机:从内核态切换回用户态时会做信号检测,检测到有信号就会处理。

     当检测到有信号要处理时,就要处理信号。

    对默认处理方式是终止进程,修改pending位图,然后返回用户态。

    对忽略处理方式是修改pending位图,返回返回用户态。

    对自定义的处理方式则需返回到用户态模式下处理该自定义函数,然后在返回用户态,在进行信号检测,如果没有信号,则返回用户态继续进行向下执行。

    1、为什么要返回用户态默认执行自定义函数?

    因为自定义函数是用户写的,必须要防止用户冒用操作系统的身份执行非法行为。

    2、处理完自定义函数,为什么不能直接返回用户态继续执行剩下的代码?

    因为你需要返回到内核态完成返回工作,比如你需要恢复上下文或者系统调用的返回值,这些都需要更高的权限来做,必须返回到内核态再回到用户态。

    10 SIGCHLD信号

    子进程退出时会给父进程发送SIGCHLD信号,对该信号的默认处理方式是忽略。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. void handler(int signo)
    8. {
    9. cout<<getpid()<<"收到了"<"号信号"<
    10. }
    11. int main()
    12. {
    13. signal(SIGCHLD,handler);
    14. if(fork() == 0)
    15. {
    16. int cnt = 0;
    17. while(true)
    18. {
    19. cout<<"I am a child pid: "<<getpid()<
    20. if(cnt == 5)
    21. {
    22. exit(1);
    23. }
    24. cnt++;
    25. sleep(1);
    26. }
    27. }
    28. else
    29. {
    30. while(true)
    31. {
    32. cout<<"I am a father pid: "<<getpid()<
    33. sleep(1);
    34. }
    35. }
    36. return 0;
    37. }

     但这种对SIGCHLD信号的处理方式在多个子进程同时退出则不能很好的处理。因为对于普通信号而言只有一个比特位来记录信号是否被收到(实时信号会被链表链接起来)。

    多个子进程同时退出的现象

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void handler(int signo)
    9. {
    10. cout<<getpid()<<"收到了"<"号信号"<
    11. waitpid(-1,nullptr,0);
    12. }
    13. int main()
    14. {
    15. signal(SIGCHLD,handler);
    16. for(int i = 0;i <= 5; i++)
    17. {
    18. if(fork() == 0)
    19. {
    20. int cnt = 0;
    21. while(true)
    22. {
    23. cout<<"I am a child pid: "<<getpid()<
    24. if(cnt == 5)
    25. {
    26. exit(1);
    27. }
    28. cnt++;
    29. sleep(1);
    30. }
    31. }
    32. }
    33. while(true)
    34. {
    35. cout<<"I am a father pid: "<<getpid()<
    36. sleep(1);
    37. }
    38. return 0;
    39. }

    这个代码的弊端:

    ①无法处理多个子进程同时退出的情况

    ②当子进程永远不退出时,则会一直阻塞住,因为waitpid使用的是0,代表阻塞等待。

    改进后

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void handler(int signo)
    9. {
    10. while(true)
    11. {
    12. int ret = waitpid(-1,nullptr,WNOHANG);
    13. if(ret > 0)
    14. {
    15. cout<<"等待成功->"<
    16. }
    17. else if(ret == 0)
    18. {
    19. cout<<"有的进程还未退出"<
    20. break;
    21. }
    22. else if(ret < 0)
    23. {
    24. cout<<"进程已全部退出"<
    25. break;
    26. }
    27. }
    28. }
    29. int main()
    30. {
    31. signal(SIGCHLD,handler);
    32. for(int i = 0;i <= 5; i++)
    33. {
    34. if(fork() == 0)
    35. {
    36. int cnt = 0;
    37. while(true)
    38. {
    39. cout<<"I am a child pid: "<<getpid()<
    40. if(cnt == 5)
    41. {
    42. exit(1);
    43. }
    44. cnt++;
    45. sleep(1);
    46. }
    47. }
    48. }
    49. while(true)
    50. {
    51. cout<<"I am a father pid: "<<getpid()<
    52. sleep(1);
    53. }
    54. return 0;
    55. }

    最常用的做法是忽略SIGCHLD信号,当它退出时自动被操作系统回收。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. int main()
    9. {
    10. signal(SIGCHLD,SIG_IGN);
    11. for(int i = 0;i <= 5; i++)
    12. {
    13. if(fork() == 0)
    14. {
    15. int cnt = 0;
    16. while(true)
    17. {
    18. cout<<"I am a child pid: "<<getpid()<
    19. if(cnt == 5)
    20. {
    21. exit(1);
    22. }
    23. cnt++;
    24. sleep(1);
    25. }
    26. }
    27. }
    28. while(true)
    29. {
    30. cout<<"I am a father pid: "<<getpid()<
    31. sleep(1);
    32. }
    33. return 0;
    34. }

     有人或许有疑惑,SIGCHLD信号的默认处理方式不是忽略吗?你使用SIG_IGN处理方式不还是忽略吗?为啥手动写就能让子进程自动回收呢?

    可以把这里当成一种特殊的情况,暂且只能记住了,或许之后会找到更好的答案。

    11 可重入函数

    要理解重入函数,先得理解重入的概念。

    重入:一个执行流还未执行完毕,另一个执行就开始执行。

    可重入函数:当执行流重入函数时,该函数不会出现不同的现象或者任何问题,则称这个函数是可重入的,否则是不可重入的。

    大多数函数都是不可重入的,比如链表的头插函数。

     

    所有的STL容器都是不可重入的,要想变得可重入得加锁保护,但加锁会损害STL容器的效率,这对以效率著称的STL是破坏性的,因此STL容器没有加锁。

  • 相关阅读:
    oracle 迁移PG 博客
    【Azure 应用服务】应用代码需要客户端证书进行验证,部署到App Service后,如何配置让客户端携带证书呢?
    神经网络 深度神经网络,深度神经网络应用实例
    Python(2)数据类型
    ES6 新增功能复盘梳理
    微服务知识03
    Red Hat Enterprise Linux RHEL 8.6 下载安装
    《时代》专访ChatGPT之父:人工智能影响经济还需要很多年
    反向迭代器------封装的力量
    OpenStack与CloudStack
  • 原文地址:https://blog.csdn.net/m0_62171658/article/details/128044492