• [ Linux ] Linux信号概述 信号的产生


    目录

    0.问题引入:

    0.1 将进程设置为后台进程

    0.2 查看后台进程并将后台进程提至前台

    0.3 将前台进程设置为后台进程

    1.信号的概念

    2.查看信号列表

    3.信号处理的常见方式

    4.信号的产生

    4.1 用户层产生信号的方式

    4.1.1通过终端按键产生信号

    4.1.2调用系统函数向进程发信号(kill,raise,abort)

    4.1.3由软件条件产生信号

    4.1.4由硬件异常产生信号


    0.问题引入:

    在曾经我们学习Linux的经历中,我们也是多次使用信号的。比如:当我们在使用xshell时,在命令行中按Ctrl+c,这个键盘输入产生了一个硬件中断,被操作系统获取,解释成信号,发送给目标前台进程。前台进程因为收到了信号,进而引起进程退出。

    注意:

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

    0.1 将进程设置为后台进程

    ./进程名 &

    0.2 查看后台进程并将后台进程提至前台

    我们发现进程一旦被设置成为后台进程是无法杀掉的,此时在一直死循环的打印hello myproc.那么我们怎么杀掉这个进程呢? 有一种方法是将后台进程设置成为前台进程,再使用ctrl +c 关闭。这种方法也比较好理解。因此哦我们在此处将使用这种方法关闭后台进程。

    首先我们要查看此时都有什么任务在执行,我们可以输入 jobs 来查看当前任务列表

    其次我们将这个进程提至提至前台,使用 fg 1 ,其中1是任务编号,也就是 [ ]+ 中的数字,注意fg 和 1之间需要带空格。

    0.3 将前台进程设置为后台进程

    我们刚刚知道了将后台进程提至前台使用 fg 任务号,f是front的意思。因此我们如果想把前台进程设置为后台进程,可以使用bg 任务号,我们可以来测试一下

    1.信号的概念

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

    2.查看信号列表

    使用kill -l 命令可以查看系统定义的信号列表

    • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如定义 #define SIGINT 2
    • 编号34以上的是实时信号。在此我们只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生默认动作是什么,在signal(7)中都有详细说明:man 7 signal

    3.信号处理的常见方式

    由于信号产生时是异步的,当产生信号的时候,对应的进程可能正在做着更重要的事情,因此这个进程可以暂时不处理这个信号!进程正在做着更重要的事情说明进程可能不需要理解处理这个信号!但是不代表这个信号不会被处理。进程是一定要记住这个信号已经来了(信号有吗?什么信号?)。因此信号处理的常见方式有以下三种:

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

    4.信号的产生

    我们在前面提到了我们现在只谈论1-31号信号。那么进程要处理一个信号,肯定是先描述,再组织。那么进程是如何记住这个信号的呢?当然是保存在进程的PCB中。由于我们只考虑1-31号进程,因此在进程的tast_struct中有一个uint32_t sig来表示信号。这里使用了位图的思想。什么信号产生我们使用的是比特位的位置。那么怎么判断有没有比特位的产生我们通过比特位的内容,1表示产生,0表示没有产生。因此一个uint_32足以表示1-31个信号了。

    而tast_struct是内核的数据结构,因此只有操作系统有权利获得进程的所有属性。所以进程的整个生命周期,无论信号怎么产生,只能是操作系统帮我们进行信号的设置。

    4.1 用户层产生信号的方式

    4.1.1通过终端按键产生信号

    产生信号的第一种方式是通过终端按键产生信号,也就是键盘。系统也为我们提供了一个signal函数,可以捕捉我们产生的信号,接下来我们将使用signal这个函数验证以下终端按键是可以产生信号的。

    SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump.现在我们可以来验证以下。

    我们可以使用函数产生信号。我们可以使用signal函数向进程发送信号。

    其中handler是函数指针(回调方法),是可以让我们用户自定义处理信号的接口(处理信号的第三种方式)

    我们在C++代码中看看如何使用signal函数,在下面这段代码中,我们设置了如果进程接受到了SIGINT信号,就会调用handler函数从而输出指定内容。我们都是SIGINT是2号信号,键盘上按Ctrl +C 本质就是给前台进程发送2号信号,因此当进程跑起来的时候,我们按Ctrl + C时,程序会调用hanlder方法打印指定内容,我们来看看结果吧。

    1. #include <iostream>
    2. #include <signal.h>
    3. #include <unistd.h>
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout<<"我是一个进程,刚刚获取了一个信号,信号编号是: "<<signo <<endl;
    8. }
    9. int main()
    10. {
    11. signal(SIGINT,handler);
    12. sleep(3);
    13. cout<<"进程已经设置完了"<<endl;
    14. sleep(3);
    15. while(true)
    16. {
    17. cout<<"我是一个运行中的进程,我的pid: "<<getpid()<<endl;
    18. sleep(2);
    19. }
    20. return 0;
    21. }

    注意:这里当SIGINT产生的时候,才会调用hanlder方法。

    在2号信号被我们自定义设置的时候,进程按2号信号是不会执行默认动作的,因此此时我们向终止进程不能使用ctrl + c了,我们可以按ctrl + \ , ctrl + \ 是给进程产生3号信号,我们也可以自定义3好信号。按这样的道理,我们可以将31个信号都自定义,那么这个进程是不是就可以不被kill 掉呢? 其实不是的!其中9号进程是不能被自定义设置的。因为操作系统要保护自己所以肯定不能将所有的信号都自定义。

    1. #include <iostream>
    2. #include <signal.h>
    3. #include <unistd.h>
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout<<"我是一个进程,刚刚获取了一个信号,信号编号是: "<<signo <<endl;
    8. }
    9. int main()
    10. {
    11. for(int sig=1;sig<=31;sig++)
    12. {
    13. signal(sig,handler);
    14. }
    15. //signal(SIGINT,handler);
    16. sleep(3);
    17. cout<<"进程已经设置完了"<<endl;
    18. sleep(3);
    19. while(true)
    20. {
    21. cout<<"我是一个运行中的进程,我的pid: "<<getpid()<<endl;
    22. sleep(2);
    23. }
    24. return 0;
    25. }

    那么我们知道了键盘可以产生信号,那么是谁给进程发送的信号呢? 答案当然是OS操作系统

    4.1.2调用系统函数向进程发信号(kill,raise,abort

    使用kill函数发送信号

    我们知道kill命令可以给一个进程发信号,例如我们之前经常使用kill -9 pid 来杀死进程编号为pid的进程。同时kill也是一个系统函数。我们也可以通过软件的方式,调用kill函数从而给指定进程发信号。

    我们使用一段C++代码来演示一下kill函数是如何使用的:我们的计划是通过调用kill函数给指定进程发信号,模拟实现kill命令。因此当程序跑完是,如果输入./mykill 9 pid时候可以杀掉指定pid进程。

    1. #include <iostream>
    2. #include <cstring>
    3. #include <errno.h>
    4. #include <signal.h>
    5. #include <sys/types.h>
    6. #include <unistd.h>
    7. using namespace std;
    8. void handler(int signo)
    9. {
    10. cout<<"我是一个进程,刚刚获取了一个信号,信号编号是: "<<signo <<endl;
    11. }
    12. static void Usage(const std::string &proc)
    13. {
    14. cerr<<"Usage:\n\t" << proc << "signo pid"<<endl;
    15. }
    16. //我想写一个kill命令
    17. // ./mykill 9 pid
    18. int main(int argc,char* argv[])
    19. {
    20. if(argc != 3)
    21. {
    22. Usage(argv[0]);
    23. exit(1);
    24. }
    25. if(kill(static_cast<pid_t>(atoi(argv[2])),atoi(argv[1])) == -1)
    26. {
    27. cerr<<"kill :" <<strerror(errno) << endl;
    28. }
    29. return 0;
    30. }

    使用raise函数

    raise是一个自取函数,是进程自己给自己发送信号。因此我们在代码中使用一下,加入我们要给自己发送2号信号。

    1. #include <iostream>
    2. #include <cstring>
    3. #include <errno.h>
    4. #include <signal.h>
    5. #include <sys/types.h>
    6. #include <unistd.h>
    7. using namespace std;
    8. void handler(int signo)
    9. {
    10. cout<<"我是一个进程,刚刚获取了一个信号,信号编号是: "<<signo <<endl;
    11. }
    12. static void Usage(const std::string &proc)
    13. {
    14. cerr<<"Usage:\n\t" << proc << "signo pid"<<endl;
    15. }
    16. //我想写一个kill命令
    17. // ./mykill 9 pid
    18. int main(int argc,char* argv[])
    19. {
    20. signal(2,handler);//没有调用对应的handler方法,仅仅是注册
    21. while(true)
    22. {
    23. sleep(1);
    24. raise(2);//自己给自己发送2号信号
    25. }
    26. return 0;
    27. }

    使用abort函数

    abort是想自己发送SIGABRT信号 -- 6号信号 我们也在代码中调用使用abort函数,并且捕捉以下该信号。

    1. #include <iostream>
    2. #include <cstdlib>
    3. #include <cstring>
    4. #include <errno.h>
    5. #include <signal.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. using namespace std;
    9. void handler(int signo)
    10. {
    11. cout<<"我是一个进程,刚刚获取了一个信号,信号编号是: "<<signo <<endl;
    12. }
    13. static void Usage(const std::string &proc)
    14. {
    15. cerr<<"Usage:\n\t" << proc << "signo pid"<<endl;
    16. }
    17. //我想写一个kill命令
    18. // ./mykill 9 pid
    19. int main(int argc,char* argv[])
    20. {
    21. signal(6,handler);//没有调用对应的handler方法,仅仅是注册
    22. while(true)
    23. {
    24. sleep(1);
    25. abort();
    26. //raise(2);//自己给自己发送2号信号
    27. }
    28. return 0;
    29. }

    通过结果发现:我们刚刚确实捕捉到了6号信号,但是进程依然退出了。因此除了9号信号可以让进程退出。6号信号也可以。

    4.1.3由软件条件产生信号

    alarm

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

    1. #include <iostream>
    2. #include <cstdlib>
    3. #include <cstring>
    4. #include <errno.h>
    5. #include <signal.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. using namespace std;
    9. int main(int argc,char* argv[])
    10. {
    11. alarm(3);//3秒后alarm
    12. int cnt = 0;
    13. while(true)
    14. {
    15. cout<< "我是一个进程 cnt : "<<cnt++<<endl;
    16. sleep(1);
    17. }
    18. return 0;
    19. }

    我们调用了alarm函数,经过3秒后闹钟会响,我们没有改变alarm的动作,因此当闹钟响起时,alarm函数会执行默认的终止进程的操作,因此经过3秒后,进程会自动退出。

    这里有张图片 但是出不来  我把结果用代码的形式写到这里

    1. [Lxy@VM-20-12-centos 11-28]$ ./mykill
    2. 我是一个进程 cnt : 0
    3. 我是一个进程 cnt : 1
    4. 我是一个进程 cnt : 2
    5. Alarm clock

    4.1.4由硬件异常产生信号

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

    当我们日常在写代码的时候,如果写了一个/0错误,进程会怎么样呢?

    1. int a = 10;
    2. cout<< a/0<<endl;

    当我们运行时,直接会告诉我们浮点数异常!因此我们之前所写的C/C++代码,我们说程序崩溃了,现在我们站在系统的角度上面我们可以知道,所谓的程序崩溃说法不是很准确,其实是进程崩溃,进程崩溃的本质是该进程收到了异常信号! 我们写两个进程崩溃的例子。

    1. #include <iostream>
    2. #include <cstdlib>
    3. #include <cstring>
    4. #include <errno.h>
    5. #include <signal.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. using namespace std;
    9. int cnt = 0;
    10. void handler(int signo)
    11. {
    12. cout<<"我是一个进程,刚刚获取了一个信号,信号编号是: "<<signo << endl;
    13. }
    14. static void Usage(const std::string &proc)
    15. {
    16. cerr<<"Usage:\n\t" << proc << "signo pid"<<endl;
    17. }
    18. //我想写一个kill命令
    19. // ./mykill 9 pid
    20. int main(int argc,char* argv[])
    21. {
    22. for(int sig = 1;sig<=31;sig++) signal(sig,handler);
    23. int a = 10;
    24. cout<< a/0<<endl;
    25. return 0;
    26. }

    我们再来写一个数组的越界访问,看看效果

    1. int a[100];
    2. a[10000000] = 100;

    (本篇完)

  • 相关阅读:
    c++ 中string、char*、char a[]
    Python 中堪称神仙的6个内置函数
    【网络教程】IPtables官方教程--学习笔记4
    Linux-多路转接-select/poll
    【c#】log4net用法
    【已解决】goland每次都自动删除我import的包
    简单的python爬虫工具,B站视频爬虫
    RFSoC应用笔记 - RF数据转换器 -05- RFSoC关键配置之RF-ADC内部解析(三)
    MySQL——多版本并发控制(MVCC)
    Python之json的dump和dumps方法
  • 原文地址:https://blog.csdn.net/qq_58325487/article/details/128129994