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

[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第1张图片

文章目录

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

信号入门

生活角度的信号

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

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


技术应用角度的信号

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

#include 
#include 

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)的。

信号概念

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

[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第2张图片

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

[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第3张图片

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在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 非法的系统调用。


信号的三个阶段

学习信号需要学习信号产生前、信号产生中和信号产生后三个阶段。
[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第4张图片


信号产生前

信号捕捉初识:

signal 函数

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

测试代码:

#include 
#include 
#include 

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 
#include 
#include 

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。
[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第5张图片

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是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

运行结果:

[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第6张图片

模拟野指针情况:

#include 
#include 
#include 

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调试工具调试:

[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第7张图片

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


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

kill函数

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

#include 
#include 
功能:
	向进程发送指定信号
原型:
	int kill(pid_t pid, int sig);
参数:
	pid:要发送信号的进程pid
	sig:向进程发送信号的号码

测试代码:

#include 
#include 
#include 
#include 

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 
功能:
	向进程发送指定信号
原型:
	int raise(int sig);
参数:
	sig:向进程发送信号的号码

测试代码:

#include 
#include 
#include 

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 
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

测试代码:

#include 
#include 

int main()
{
    abort();

    return 0;
}

运行结果:
[cwx@VM-20-16-centos signal]$ ./mytest 
Aborted

由软件条件产生信号

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

alarm函数:

#include 
unsigned int alarm(unsigned int seconds);

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

测试代码:

#include 
#include 

// 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信号发送给进程。

[Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)_第8张图片


模拟野指针异常:

#include 
#include 
#include 

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 
#include 
#include 

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等其它所有的算术的错误。


你可能感兴趣的:(Linux,linux,运维,服务器)