• linux——信号


    ✅<1>主页::我的代码爱吃辣
    📃<2>知识讲解:Linux——进程等待
    ☂️<3>开发环境:Centos7
    💬<4>前言:生活中处处有信号,linux中也有很多信号,OS使用来通知进程,控制进程,更好的管理进程。

    目录

    一.生活中的信号

    二.技术应用角度的信号

    三.信号的捕捉

     四.信号的产生

     1. 通过终端按键产生信号

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

    3.由软件条件产生信号

     4. 硬件异常产生信号

     五.核心转储


    一.生活中的信号

    1. 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”。
    2. 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
    3. 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”。
    4. 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
    5. 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。

    二.技术应用角度的信号

    1. 用户输入命令,在Shell下启动一个前台进程。
    2. 用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程。
    3. 前台进程因为收到信号,进而引起进程退出。

     测试代码:

    1. #include
    2. #include
    3. int main()
    4. {
    5. while (1)
    6. {
    7. printf("I am a process, I am waiting signal!\n");
    8. sleep(1);
    9. }
    10. }

    测试结果:

     注意:

    1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
    2. Shell可以同时运行一个前台进程和任意多个后台进程能接到像,只有前台进程才 Ctrl-C 这种控制键产生的信号。
    3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

    查看信号列表:

    kill -l 

    1. Ctrl + C 就是给前台进程发送 2号信号 SIGINT。
    2. 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2。
    3. 编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。
    4. 这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal。

    三.信号的捕捉

    当我们按下Ctrl + C 前台进程会收到 2 号信号。那我们如何验证进程收到了2号信号呢?

     介绍一个系统调用:

    头文件: #include

    typedef void (*sighandler_t)(int);

    接口定义:sighandler_t signal(int signum, sighandler_t handler)。

    作用:捕捉信号signnum,将signum的默认动作修改为 hander。

    参数:1.signum信号编号   2.handler回调的方法。

    测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void handle(int signum)
    7. {
    8. cout << "I get a signal:" << signum << endl;
    9. }
    10. int main()
    11. {
    12. signal(2, handle);
    13. while (1)
    14. {
    15. printf("I am a process, I am waiting signal!\n");
    16. sleep(1);
    17. }
    18. }

    测试结果:

    1. 忽略此信号。
    2. 执行该信号的默认处理动作。
    3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

    在上述的代码中2号信号的默认动作是终止进程,当我们对2好信号进行捕捉,并且将2号信号的默认动作修改为我们自定义的一个函数。因此我们再次Ctrl + C,进程收到2号信号就不会终止,而是执行我们设计的函数。我们继续可以使用Ctrl + \ 终止进程。

     四.信号的产生

     1. 通过终端按键产生信号

     中断按键,就是我们的键盘,当我们按下Ctrl + C 时OS会向前台进程发送2号信号。还有Ctrl + \ 可以向前台进程发送。

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

    #include

    #include

    int kill(pid_t pid, int signo);向指定进程发指定信号。
    int raise(int signo);向调用进程发送指定信号。
    这两个函数都是成功返回0,错误返回-1。

    #include
    void abort(void);
    就像exit函数一样,abort函数总是会成功的,所以没有返回值,给进程发送6号信号,6号直接终止进程。

    kill-测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. void handle(int signum)
    8. {
    9. cout << "I am child process,my PID :" << getpid() << ",I get a signal:" << signum << endl;
    10. // 进程退出
    11. exit(0);
    12. }
    13. int main()
    14. {
    15. // 创建子进程
    16. pid_t pid = fork();
    17. if (pid == 0)
    18. {
    19. // 让子进程捕捉2号信号
    20. signal(2, handle);
    21. while (1)
    22. {
    23. printf("I am child process, my PID :%d,I am waiting signal!\n", getpid());
    24. sleep(1);
    25. }
    26. }
    27. int count = 5;
    28. while (count)
    29. {
    30. printf("%d秒之后我将给%d号进程发2号信号\n", count--, pid);
    31. sleep(1);
    32. }
    33. // 给子进程发送2号信号
    34. kill(pid, 2);
    35. sleep(1);
    36. return 0;
    37. }

    测试结果:

     raise-测试代码

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. void handle(int signum)
    8. {
    9. cout << "I am process,I get a signal:" << signum << endl;
    10. // 进程退出
    11. exit(0);
    12. }
    13. int main()
    14. {
    15. // 让进程捕捉2号信号
    16. signal(2, handle);
    17. int count = 5;
    18. while (count)
    19. {
    20. printf("%d秒之后我将给自己发2号信号\n", count--);
    21. sleep(1);
    22. }
    23. // 给子进程发送2号信号
    24. raise(2);
    25. sleep(1);
    26. return 0;
    27. }

    测试结果:

    abort-测试代码:

     代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void handle(int signum)
    9. {
    10. cout << "I am process,I get a signal:" << signum << endl;
    11. // 进程退出
    12. exit(0);
    13. }
    14. int main()
    15. {
    16. // 让进程捕捉6号信号
    17. signal(6, handle);
    18. int count = 5;
    19. while (count)
    20. {
    21. printf("%d秒之后我将给自己发6号信号\n", count--);
    22. sleep(1);
    23. }
    24. // 给子进程发送6号信号
    25. abort();
    26. sleep(1);
    27. return 0;
    28. }

    测试结果:

    3.由软件条件产生信号

     SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。

    #include
    unsigned int alarm(unsigned int seconds);

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

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. int main()
    9. {
    10. // 设置闹钟
    11. alarm(5);
    12. int count = 5;
    13. while (count)
    14. {
    15. printf("%d秒之后闹钟响,进程终止\n", count--);
    16. sleep(1);
    17. }
    18. sleep(15);
    19. return 0;
    20. }

    测试结果:

     4. 硬件异常产生信号

    硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。 

    除0异常模拟:

    测试代码:

    1. int main()
    2. {
    3. cout << "begin" << endl;
    4. cout << "begin" << endl;
    5. int num = 10;
    6. int tmp = num / 0;
    7. cout << "end" << endl;
    8. cout << "end" << endl;
    9. return 0;
    10. }

     说明:在运行到除0的地方,进程直接终止了,原因是进程收到了8号信号。

    野指针异常模拟:

    测试代码:

    1. int main()
    2. {
    3. cout << "begin" << endl;
    4. cout << "begin" << endl;
    5. int *point = nullptr;
    6. *point = 100;
    7. cout << "end" << endl;
    8. cout << "end" << endl;
    9. return 0;
    10. }

    测试结果:

    由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

     总结:

    • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者。
    • 信号的处理是否是立即处理的?在合适的时候。
    • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?进程PCB中。
    • 一个进程在没有收到信号的时候,能不能知道,自己应该对合法信号作何处理呢?知道。
       

     五.核心转储

    我们在进程控制时,讲到进程等待,我们与遇到过这个概念:

    首先解释什么是核心转储(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 -c1024

    1. SIGINT信号Action是Term,代表直接退出不进行多余的动作
    2. SIGQUIT信号Action是Core,代表退出前进行核心转储。

     使用命令$:ulimit -a 可以查看进程的资源分配信息,其中也包括core 文件大小。

     使用命令:ulimit -c 10240,给core dump添加大小。

     测试代码:

    1. int main()
    2. {
    3. pid_t pid = fork();
    4. if (pid == 0)
    5. {
    6. cout << "begin" << endl;
    7. cout << "begin" << endl;
    8. int *point = nullptr;
    9. *point = 100;
    10. cout << "end" << endl;
    11. cout << "end" << endl;
    12. }
    13. int status = 0;
    14. waitpid(pid, &status, 0);
    15. cout << "core dump:" << ((status >> 7) & 1) << endl;
    16. cout << "signal:" << (status & 0x3f) << endl;
    17. return 0;
    18. }

     测试结果:

     说明:

    • 当我们放开核心转储以后,之后执行的程序一旦收到core信号,就会在当前目录下生成一个core.XXX核心转储文件。
    • 这个文件是一个二进制文件。
    • 可以使用gdb对debug可执行程序进行调试,然后使用core-file core.XXX查看核心转储信息。

    查看核心转储文件:

    1. 使用debug编译文件。
    2. 使用gdb调试可执行程序。
    3. 在gdb中使用core-file产看core.XXX文件按

  • 相关阅读:
    RabbitMQ之集群管理
    算法与数据结构 - 散列表
    DMP(Dynamic Movement Primitives)动态运动基元算法收敛性证明
    OpenSSL在i.MX8MP开发板上的应用
    贪心算法——知识点总结
    【Linux 】向Shell脚本传递参数、getopts、getopt
    数据结构:堆排序
    Intellij IDEA--根据maven依赖名查找它是哪个pom.xml引入的
    github下载源码失败(mac)
    C#面:C#面向对象的思想主要包括什么?
  • 原文地址:https://blog.csdn.net/qq_63943454/article/details/133522225