Linux 信号(一):初步认识信号

1. 生活角度的信号

当你正在看电视的时候,手机铃声突然响了。这就说明有一个事件发生,你收到了一个通知。(信号产生)

即使电话还没响之前,你也知道如何接电话,以及接电话后该怎么办。例如:接听电话、挂断电话、静音不管。也就是说,处理方式在电话来之前就已经确定好了。(预定义处理动作)

电话打进来的事件是不确定的,你无法预测别人什么时候给你打电话。对你而言,这是一个异步事件。(异步机制)

电话响起时,你可能正在做更重要的事件。例如:开会、打游戏等。因此你不会立刻接电话,而是先让电话继续响着。(在合适的时间处理)

从电话响起到你真正接听电话之间,存在一个时间窗口。在这段时间里:电话已经来了;你知道有人给你打电话;但你还没处理。本质是就是:你记录了一个待处理事件。(信号保存)

当你有空之后,开始处理这个电话。常见的处理方式有:1.接听电话(默认处理)2.直接挂断(忽略处理)3.根据来点人采取不同处理方式(自定义捕捉)(信号处理)

基本结论

手机来电 Linux信号
电话打进来 信号产生
未接电话记录 信号保存
接电话 信号处理
不知道什么时候来电 异步事件
接听/挂断/特殊处理 不同信号处理方式

补充:

  1. 进程为什么能识别信号?

Linux 预先定义了信号编号,内核和进程共同遵守这套规则,因此进程能够识别不同信号所表示的含义。

  1. 进程为什么能知道处理对应的信号方法?

Linux 预先定义了每种信号对应的默认处理方式,同时进程也可以注册自己的处理函数,因此信号到来后能够按照预先设定好的方式进行处理。

  1. 信号产生后,不一定会被立即处理,而是在满足特定条件时由进程处理。

  2. 同步和异步

同步:某个事件什么时候发生,由当前执行流程主动决定。

异步:某个事件什么时候发生,不由当前执行流程决定,而是由外部决定。

信号异步的原因:进程正在执行自己的逻辑,突然就会收到信号。对于进程而言,信号什么时候到来是无法提前预测的,因此信号本质是一种异步事件通知机制。

理解了信号的异步通知特性之后,接下来我们先认识 Linux 中的信号。

2 Linux 中的信号

2.1 信号的基本概念

信号是 Linux 为进程提供的一种异步事件通知机制,用于告知进程某个特定事件已经发生。
当特定事件发生时,内核会向目标进程发送对应信号,进程收到信号后再按照预先设定好的方式进行处理。

进一步解释:

信号本质是一种软件层面的中断机制。与硬件中断通知 CPU 某个事件发生类似,信号用于通知进程某个软件事件已经发生。

信号的基本特征

  1. 信号是一种通知机制

信号只负责通知事件发生,不负责传递大量数据

  1. 信号是异步的

进程无法提前预测信号何时到来,信号的产生往往由外部事件决定。

  1. 信号由内核发送

当特定事件发生时,内核会为目标进程产生并发送对应信号。

  1. 信号具有预定义含义

每种信号都对应一种特定事件。

  1. 信号处理方式提前确定

默认处理 忽略 自定义捕捉

在了解信号的基本概念后,接下来先看看 Linux 为我们定义了哪些信号。

2.2 查看信号

Linux 为每种信号都分配了唯一编号和名称,可以通过kill -l查看系统支持的信号列表。

这些信号的名称和编号在 signal.h 头文件中进行了定义。

cpp 复制代码
/* Signals.  */
#define	SIGHUP		1	/* Hangup (POSIX).  */
#define	SIGINT		2	/* Interrupt (ANSI).  */
#define	SIGQUIT		3	/* Quit (POSIX).  */
#define	SIGILL		4	/* Illegal instruction (ANSI).  */
#define	SIGTRAP		5	/* Trace trap (POSIX).  */
#define	SIGABRT		6	/* Abort (ANSI).  */
#define	SIGIOT		6	/* IOT trap (4.2 BSD).  */
#define	SIGBUS		7	/* BUS error (4.2 BSD).  */
#define	SIGFPE		8	/* Floating-point exception (ANSI).  */
#define	SIGKILL		9	/* Kill, unblockable (POSIX).  */
#define	SIGUSR1		10	/* User-defined signal 1 (POSIX).  */
#define	SIGSEGV		11	/* Segmentation violation (ANSI).  */
#define	SIGUSR2		12	/* User-defined signal 2 (POSIX).  */
#define	SIGPIPE		13	/* Broken pipe (POSIX).  */
#define	SIGALRM		14	/* Alarm clock (POSIX).  */
#define	SIGTERM		15	/* Termination (ANSI).  */
#define	SIGSTKFLT	16	/* Stack fault.  */
#define	SIGCLD		SIGCHLD	/* Same as SIGCHLD (System V).  */
#define	SIGCHLD		17	/* Child status has changed (POSIX).  */
#define	SIGCONT		18	/* Continue (POSIX).  */
#define	SIGSTOP		19	/* Stop, unblockable (POSIX).  */
#define	SIGTSTP		20	/* Keyboard stop (POSIX).  */
#define	SIGTTIN		21	/* Background read from tty (POSIX).  */
#define	SIGTTOU		22	/* Background write to tty (POSIX).  */
#define	SIGURG		23	/* Urgent condition on socket (4.2 BSD).  */
#define	SIGXCPU		24	/* CPU limit exceeded (4.2 BSD).  */
#define	SIGXFSZ		25	/* File size limit exceeded (4.2 BSD).  */
#define	SIGVTALRM	26	/* Virtual alarm clock (4.2 BSD).  */
#define	SIGPROF		27	/* Profiling alarm clock (4.2 BSD).  */
#define	SIGWINCH	28	/* Window size change (4.3 BSD, Sun).  */
#define	SIGPOLL		SIGIO	/* Pollable event occurred (System V).  */
#define	SIGIO		29	/* I/O now possible (4.2 BSD).  */
#define	SIGPWR		30	/* Power failure restart (System V).  */
#define SIGSYS		31	/* Bad system call.  */

编号34以上的是实时信号,本文只讨论编号 1 ~ 31 的普通信号,不讨论实时信号。

通过 man 7 signal 可以查看信号的产生条件以及信号的默认处理动作。

Action 列表示信号的默认处理方式:

Term:终止进程

Core:终止进程并生成 Core 文件

Stop:暂停进程

Cont:继续运行进程

Ign :忽略信号

虽然 Linux 定义了很多信号,

但目前不需要全部记住。

先通过一个简单例子观察信号的产生与处理过程。

2.3 一个样例

cpp 复制代码
#include <iostream>
#include <unistd.h>

int main()
{
    while(1)
    {
        std::cout << "I am process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

用户输入命令,在Shell下启动一个前台进程。

用户按下 Ctrl + C ,键盘发生硬件中断,被 OS 获取并解释成信号,发送给前台进程。

前台进程收到信号,进行信号处理引起进程退出。

2.4 一个系统调用函数

cpp 复制代码
NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号
handler:函数指针,表示更改进程对信号的处理动作,当收到对应的信号,就执行 handler 方法

函数功能:更改进程对指定信号的处理动作,使其不调用默认处理,而是调用用户自己写的处理方法(自定义捕捉)

上面的样例,Ctrl + C 的本质是向前台进程发送 SIGINT 信号,即 2 号信号。

验证过程

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void sighander(int signum)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signum <<std::endl;
}

int main()
{
    signal(SIGINT,sighander);
    std::cout << "我是进程: " << getpid() << std::endl;
    while(1)
    {
        std::cout << "I am process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

现象:在键盘中输入 Crtl + C ,只能向前台进程发送信号,不能使其退出了。

原因:用户对 2 号信号进行了捕捉,更改了它的默认处理动作,使得处理方式由用户来确定。

退出方式:1. kill -9 进程pid -> 向目标进程发送 9 号信号 2. Ctrl + \ 向前台进程发送 3 号信号

补充:

  1. signal 函数只是设置了特定信号的处理方式,并不是直接调用处理方式。如果后续特定信号没有产生或者产生但被阻塞,设置的自定义捕捉函数不会被调用。

  2. 对于很多信号的默认处理方式都是终止当前进程,终止的方式有两种:Term 和 Core

  3. 前台进程:占用终端输入进程,能接收键盘输入(Ctrl + C、Ctrl + \ 、Ctrl + Z),一个终端同一时刻只能有一个前台进程。

cpp 复制代码
./test

后台进程:不占用终端输入,在后台运行的进程。

后台进程分为两类:

(一) 作业式后台进程(终端关联后台进程)

cpp 复制代码
./test &

进程可以在前台和后台之间进行切换。例如:当我们在命令行运行一个进程时,Shell 会阻塞变成后台进程,直到进程结束,才变为前台进程。

常用命令:

jobs:查看当前终端所有后台作业

fg %作业号:将后台进程拉回前台

bg %作业号:把暂停的前台进程放入后台继续运行

Ctrl + Z:向前台进程发送SIGTSTP(20) 信号,默认处理动作:暂停前台进程。

(二) 守护进程(完全脱离终端的后台进程)---- 网络部分说明

  1. SIGKILL(9) 和 SIGSTOP(19)信号不可捕捉、不可忽略、不可阻塞

2.5 信号处理

自定义捕捉

cpp 复制代码
​
#include <iostream>
#include <unistd.h>
#include <signal.h>

void sighander(int signum)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signum <<std::endl;
}

int main()
{
    signal(SIGINT,sighander);
    std::cout << "我是进程: " << getpid() << std::endl;
    while(1)
    {
        std::cout << "I am process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

​

默认处理

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void sighander(int signum)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signum <<std::endl;
}

int main()
{
    signal(SIGINT, SIG_DFL);
    std::cout << "我是进程: " << getpid() << std::endl;
    while(1)
    {
        std::cout << "I am process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}

忽略

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void sighander(int signum)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signum <<std::endl;
}

int main()
{
    signal(SIGINT, SIG_IGN);
    std::cout << "我是进程: " << getpid() << std::endl;
    while(1)
    {
        std::cout << "I am process, I am waiting signal!" << std::endl;
        sleep(1);
    }
    return 0;
}
cpp 复制代码
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
// 其实SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型