[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)

文章目录

  • 信号入门
  • 生活角度的信号
  • 技术应用角度的信号
  • 信号概念
  • 信号的三个阶段
  • 信号产生前
  • 信号产生的方式
  • 通过终端按键产生信号
  • Core Dump
  • 调用系统函数向进程发信号
  • kill函数
  • raise函数
  • abort函数
  • 由软件条件产生信号
  • 硬件异常产生信号
  • 信号入门

    生活角度的信号

    生活中有许许多多的信号,比如闹钟,红绿灯,信号枪和鸡叫声等等,当我们接收到这些信号后,我们会立即做出反应,闹钟响了就要起床,红灯停绿灯行,也就是说在接受这些信号之前,我们已经知道收到信号后所对应的措施。同样的,进程具有识别信号并处理信号的能力,远远早于信号的产生,也就是进程在没有收到信号前,就已经知道什么信号对应什么处理动作,这在编写操作系统源代码时工程师已经设置好的。

    而且,在生活中我们收到信号,不一定是立即处理的,比如当我们接到外卖员的电话,这是一个信号,但是如果我们现在正在做更重要的事,就不会立即去处理这个信号。同样的,进程的信号随时可能产生(异步产生),但是进程有可能在做更重要的工作,这跟进程的优先级有关。既然信号有可能不能及时被处理,就应该被保存起来,而信号的本质也是数据,收到信号后进程就会往进程的task_struct写入信号数据,信号底层都是通过操作系统发送的。


    技术应用角度的信号

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

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
        while (1)
        {
            printf("process wait signal\n");
            sleep(1);   
        }
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    process wait signal
    process wait signal
    process wait signal
    process wait signal
    ^C
    [cwx@VM-20-16-centos signal]$ 
    
  • Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  • Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

  • 信号概念

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

    kill -l查看系统定义的信号列表:

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • 编号34以上的是实时信号,只讨论编号34以下的信号,不讨论实时信号,这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
    1. SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业,
      这时它们与控制终端不再关联。

    2. SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

    3. SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制.
      进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

    1. SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

    2. SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用。

    3. SIGABRT 调用abort函数生成的信号。

    4. SIGBUS 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,
      但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

    5. SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

    6. SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

    7. SIGUSR1 留给用户使用

    8. SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

    9. SIGUSR2 留给用户使用

    10. SIGPIPE
      管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

    11. SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

    12. SIGTERM 程序结束(terminate)信号,
      与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

    1. SIGCHLD 子进程结束时, 父进程会收到这个信号。

      如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程
      来接管)。

    1. SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞.
      可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

    2. SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束,
      只是暂停执行. 本信号不能被阻塞, 处理或忽略.

    3. SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

    4. SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

    5. SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

    6. SIGURG 有”紧急”数据或out-of-band数据到达socket时产生.

    7. SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

    8. SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制。

    9. SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

    10. SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

    11. SIGWINCH 窗口大小改变时发出.

    12. SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作.

    13. SIGPWR Power failure

    14. SIGSYS 非法的系统调用。


    信号的三个阶段

    学习信号需要学习信号产生前、信号产生中和信号产生后三个阶段。


    信号产生前

    信号捕捉初识:

    signal 函数

    #include <signal.h>
    功能:
    	捕捉信号
    原型:
    	sighandler_t signal(int signum, sighandler_t handler);
    	typedef void (*sighandler_t)(int);
    参数:
    	signum:要捕捉的信号号码
    	handler:函数指针,捕捉指定信号后执行该指针指向的函数
    

    测试代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    
    void handler(int signal)
    {
        printf("get signal: %d\n", signal);
    }
    
    int main()
    {
        signal(2, handler);
    
        while(1)
        {
            printf("process wait signal\n");
            sleep(1);
        }
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    process wait signal
    process wait signal
    process wait signal
    process wait signal
    ^Cget signal: 2
    process wait signal
    process wait signal
    ^Cget signal: 2
    

    信号产生的方式

    通过终端按键产生信号

    ctrl+C 通知前台进程组终止进程。SIGINT(2号信号)
    ctrl+\ 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。SIGQUIT(3号信号)
    ctrl+Z 停止(stopped)进程的执行。SIGSTOP(20号信号)

    测试代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    
    void handler(int signal)
    {
        printf("get signal: %d\n", signal);
    }
    
    int main()
    {
        int sig = 1;
        for(; sig <= 31; sig++){
            signal(sig, handler);    
        }
    
        while(1)
        {
            printf("process wait signal\n");
            sleep(1);
        }
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    process wait signal
    ^Cget signal: 2
    process wait signal
    ^Zget signal: 20
    process wait signal
    process wait signal
    ^\get signal: 3
    

    Core Dump

    在学习进程等待时,waitpid方法中的status变量中有大小为一字节的Core Dump标志位。进程异常退出时,core dump位会被设置为1。

    当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。

    用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为10240K:

    $ ulimit -c 10240
    

    查看详情:

    $ ulimit -a
    

    core文件关闭:

    $ ulimit -c 0
    

    运行结果:

    模拟野指针情况:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    
    int main()
    {
        int *p = NULL;
        *p = 100;
        while(1)
        {
            printf("process wait signal\n");
        }
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    Segmentation fault (core dumped)
    [cwx@VM-20-16-centos signal]$ ll
    total 144
    -rw------- 1 cwx cwx 249856 Aug 21 15:18 core.13166
    -rw-rw-r-- 1 cwx cwx     69 Aug 21 15:16 makefile
    -rwxrwxr-x 1 cwx cwx   9464 Aug 21 15:18 mytest
    -rw-rw-r-- 1 cwx cwx    659 Aug 21 15:18 mytest.c
    

    设置允许产生core文件后,运行出错后,会生成core文件,后面的数字是进程的pid,core文件可以通过gdb调试工具调试:

    通过core文件和gdb调试工具就可以快速定位到出现异常的行数。


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

    kill函数

    kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号。

    #include <sys/types.h>
    #include <signal.h>
    功能:
    	向进程发送指定信号
    原型:
    	int kill(pid_t pid, int sig);
    参数:
    	pid:要发送信号的进程pid
    	sig:向进程发送信号的号码
    

    测试代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    #include <sys/types.h>
    
    void handler(int signo)
    {
        printf("get a signal: %d\n", signo);
    }
    
    int main()
    {
        signal(2, handler);
        printf("process wait NO.2 signal...\n");
    
        sleep(3); // 三秒后给进程发送2号信号
        kill(getpid(), 2);
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    process wait NO.2 signal...
    get a signal: 2
    

    raise函数

    #include <signal.h>
    功能:
    	向进程发送指定信号
    原型:
    	int raise(int sig);
    参数:
    	sig:向进程发送信号的号码
    

    测试代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    
    void handler(int signo)
    {
        printf("get a signal: %d\n", signo);
    }
    
    int main()
    {
        signal(2, handler);
        printf("process wait NO.2 signal...\n");
    
        sleep(3); // 三秒后给进程发送2号信号
        raise(2);
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    process wait NO.2 signal...
    get a signal: 2
    

    abort函数

    abort函数使当前进程接收到信号而异常终止。

    #include <stdlib.h>
    void abort(void);
    就像exit函数一样,abort函数总是会成功的,所以没有返回值。
    

    测试代码:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        abort();
    
        return 0;
    }
    
    运行结果:
    [cwx@VM-20-16-centos signal]$ ./mytest 
    Aborted
    

    由软件条件产生信号

    软件条件产生信号介绍alarm函数和SIGPIPE信号。SIGPIPE是一种由软件条件产生的信号,在匿名管道中,如果读端关闭,写端会受到操作系统发送到SIGPIPE信号。

    alarm函数:

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

    测试代码:

    #include <stdio.h>
    #include <unistd.h>
    
    // 1s之内count不断累加,1s被信号SIGALRM信号终止
    int main()
    {
        int count = 0;
        alarm(1);
    
        while(1){
            printf("count = %d\n", count++);
        }
    
        return 0;
    }
    
    运行结果:
    ...
    count = 18257
    count = 18258
    count = 18259
    count = 18260
    count = 18261
    count = 18262
    count = 18263
    count = 18264Alarm clock
    

    硬件异常产生信号

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


    模拟野指针异常:

    #include <stdio.h>
    #include <signal.h>
    #include <stdlib.h>
    
    void handler(int signo)
    {
        printf("get a signal: %d\n", signo);
        exit(1);
    }
    
    int main()
    {
        //int sig = 1;
        //for(; sig <= 31; sig++)
        //{
        //    signal(sig, handler);
        //}
    
        int* p = NULL;
        *p = 100;
    
        return 0;
    }
    运行结果(捕捉信号):
    [cwx@VM-20-16-centos signal]$ ./mytest 
    get a signal: 11
    运行结果(未捕捉信号):
    [cwx@VM-20-16-centos signal]$ ./mytest 
    Segmentation fault
    

    SIGSEGV (11号信号)试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。


    模拟除0错误异常:

    #include <stdio.h>
    #include <signal.h>
    #include <stdlib.h>
    
    void handler(int signo)
    {
        printf("get a signal: %d\n", signo);
        exit(1);
    }
    
    int main()
    {
        int sig = 1;
        for(; sig <= 31; sig++)
        {
            signal(sig, handler);
        }
    
        int a = 10;
        a /= 0;
    
        return 0;
    }
    运行结果(捕捉信号):
    [cwx@VM-20-16-centos signal]$ ./mytest 
    get a signal: 8
    运行结果(未捕捉信号):
    [cwx@VM-20-16-centos signal]$ ./mytest 
    Floating point exception
    

    SIGFPE (8号信号) 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。


    物联沃分享整理
    物联沃-IOTWORD物联网 » [Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)

    发表回复