信号的概念
信号在我们的生活中随处可见, 如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…
他们都有共性: 1. 简单 2. 不能携带大量信息 3. 满足某个特设条件才发送。
信号是信息的载体, Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。
Unix 早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T 都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。 POSIX.1 对可靠信号例程进行了标准化
信号的机制
A 给 B 发送信号, B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
与信号相关的事件和状态
产生信号
- 按键产生,如: Ctrl+c、 Ctrl+z、 Ctrl+\
- 系统调用产生,如: kill、 raise、 abort
- 软件条件产生,如:定时器 alarm
- 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如: kill 命令
递达
递送并且到达进程。(我们认为当信号被递达即算作被处理)
未决
产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方法
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用户处理函数)
Linux 内核的进程控制块 PCB 是一个结构体, task_struct, 除了包含进程 id,状态,工作目录,用户 id,组 id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽 x 信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集
- 信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号的编号
可以使用 kill –l 命令查看当前系统可使用的信号有哪些。
不存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号), 34-64 称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同。
信号 4 要素
与变量三要素类似的,每个信号也有其必备 4 要素,分别是:
1.编号 2. 名称 3. 事件 4. 默认处理动作
可通过 man 7 signal 查看帮助文档获取。
在标准信号中,有一些信号是有三个“值”,第一个值通常对 alpha 和 sparc 架构有效,中间值针对 x86、 arm和其他架构,最后一个应用于 mips 架构。一个‘-’表示在对应架构上尚未定义该信号。
不同的操作系统定义了不同的系统信号。因此有些信号出现在 Unix 系统内,也出现在 Linux 中,而有的信号出现在 FreeBSD 或 Mac OS 中却没有出现在 Linux 下。这里我们只研究 Linux 系统中的信号。
特别强调: 9) SIGKILL 和 19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
Linux 常规信号一览表
编号 | 名称 | 事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT | 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
3 | SIGQUIT | 当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 | 终止进程 |
4 | SIGILL | CPU 检测到某进程执行了非法指令 | 终止进程,并产生 core 文件 |
5 | SIGTRAP | 该信号由断点指令或其他 trap 指令产生 | 终止进程,并产生core文件 |
6 | SIGABRT | 调用 abort 函数时产生该信号 | 终止进程,并产生 core 文件 |
7 | SIGBUS | 非法访问内存地址,包括内存对齐出错 | 终止进程,并产生core文件 |
8 | SIGFPE | 在发生致命的运算错误时发出(不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误) | 终止进程,并产生core文件 |
9 | SIGKILL | 无条件终止进程。(本信号不能被忽略,处理和阻塞) | 终止进程 |
10 | SIGUSR1 | 用户定义 的信号(即程序员可以在程序中定义并使用该信号) | 终止进程 |
11 | SIGSEGV | 指示进程进行了无效内存访问 | 终止进程,并产生core文件 |
12 | SIGUSR2 | 用户自定义信号(即程序员可以在程序中定义并使用该信号) | 终止进程 |
13 | SIGPIPE | Broken pipe 向一个没有读端的管道写数据 | 终止进程 |
14 | SIGALRM | 定时器超时(超时的时间由系统调用 alarm 设置) | 终止进程 |
15 | SIGTERM | 程序结束信号(与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号) | 终止进程 |
16 | SIGSTKFLT | Linux 早期版本出现的信号,现仍保留向后兼容 | 终止进程 |
17 | SIGCHLD | 子进程状态发生变化时,父进程会收到这个信号 | 忽略 |
18 | SIGCONT | 如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP | 停止(暂停)进程的执行(信号不能被忽略,处理和阻塞) | 暂停进程 |
20 | SIGTSTP | 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 | 暂停进程 |
21 | SIGTTIN | 后台进程读终端控制台 | 暂停进程 |
22 | SIGTTOU | 在后台进程要向终端输出数据时发生(该信号类似于 SIGTTIN) | 暂停进程 |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。(如网络带外数据到达) | 忽略 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程 | 终止进程 |
25 | SIGXFSZ | 超过文件的最大长度设置 | 终止进程 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号(类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间) | 终止进程 |
27 | SGIPROF | 类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间 | 终止进程 |
28 | SIGWINCH | 窗口变化大小时发出 | 忽略 |
29 | SIGIO | 此信号向进程指示发出了一个异步 IO 事件 | 忽略 |
30 | SIGPWR | 关机 | 忽略 |
31 | SIGSYS | 无效的系统调用 | 终止进程,并产生core文件 |
34) SIGRTMIN | ~ (64) SIGRTMAX | LINUX 的实时信号,它们没有固定的含义(可以由用户自定义) | 所有的实时信号的默认动作都为终止进程 |
信号的产生
终端按键产生信号
Ctrl + c → (2) SIGINT(终止/中断) “INT” ----Interrupt
Ctrl + z → (20) SIGTSTP(暂停/停止) “T” ----Terminal 终端。
Ctrl + \ → (3) SIGQUIT(退出)
硬件异常产生信号
除 0 操作 → 8) SIGFPE (浮点数例外) “F” -----float 浮点数。
非法访问内存 → 11) SIGSEGV (段错误)
总线错误 → 7) SIGBUS
kill 函数/命令产生信号
kill 命令产生信号:
kill -SIGKILL pid
kill -9 pid
kill 函数:给指定进程发送指定信号(不一定杀死)
int kill(pid_t pid, int sig);
/*
成功: 0;
失败: -1 (ID 非法,信号非法,普通用户杀 init 进程等权级问题),设置 errno
参数1:
sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
参数2:
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给 与调用 kill 函数进程属于同一进程组的所有进程。
pid < -1: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
*/
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组 ID 与进程组长 ID 相同。
权限保护: super 用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root 用户的 pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。
普通用户基本规则是:发送者实际或有效用户 ID == 接收者实际或有效用户 ID
软件条件产生信号
alarm函数
例子:alarm.cpp
设置定时器(闹钟)。在指定seconds(秒)后,内核会给当前进程发送
14)SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一一个定时器。
unsigned int alarm(unsigned int seconds);
/*
* 返回0或者剩余的秒数。无失败。
*
* 常用:alarm(0):取消定时器,返回旧闹钟剩余秒数。
*/
例如:
alarm(5)[定时5s]->过去3秒->alarm(4)[重新定时4s][返回值为5-3=2]->过去2s->alarm(10)[重新定时10s]->alarm(0)[取消闹钟]
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm 都计时。
time 命令k可以查看程序执行的时间
time ./a.out
实际执行时间 = 系统时间 + 用户时间 + 等待时间
程序运行的瓶颈在于 IO,优化程序,首选优化 IO。
setitimer 函数
例子: setitimer.cpp
设置定时器(闹钟)。 可代替 alarm 函数。精度微秒 us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
/*
* 成功: 0;失败: -1,设置errno
*
* 参数: 1. which:指定定时方式
* 1) 自然定时: ITIMER_REAL → 14) SIGLARM 计算自然时间
* 2) 虚拟空间计时(用户空间): ITIMER_VIRTUAL → 26) SIGVTALRM 只计算进程占用 cpu 的时间
* 3) 运行时计时(用户+内核): ITIMER_PROF → 27) SIGPROF 计算占用 cpu 及执行系统调用的时间
*
* 2. new_value
* 1) it_interval 定时的时长
* 2) it_value 用来设定两次定时任务之间间隔的时间
* 即it_value为第一次定时时长,it_interval为第一次之后的每一次定时时长,因此可以实现周期定时,相当于do...while循环。
* (两个参数都设置为 0,即清 0 操作,取消定时)
*
* 3. old_value
* 传出参数,返回旧闹钟剩余时间。
*/
struct itimerval结构体
(在/usr/include下使用grep命令查找)
struct itimerval
{
/* Value to put into `it_value' when the timer expires. */
struct timeval it_interval;
/* Time to the next timer expiration. */
struct timeval it_value;
};
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。
我们可以在应用程序中通过改变自定义 set 来改变 mask。已达到屏蔽指定信号的目的。
信号集设定的函数
// typedef unsigned long sigset_t
sigset_t set; // 本质是位图
// 将某个信号集清0
// 0 成功: 0;失败: -1
int sigemptyset(sigset_t *set); // 将自定义信号屏蔽字集全置0
// 将某个信号集置1
// 成功: 0;失败: -1
int sigfillset(sigset_t *set); // 将自定义信号屏蔽字集全置1
// 将某个信号加入信号集
// 成功: 0;失败: -1
int sigaddset(sigset_t *set, int signum); // 参1:自定义信号屏蔽字集, 参2:需要屏蔽的信号(将此信号所在位置1)
// 将某个信号清出信号集
// 成功: 0;失败: -1
int sigdelset(sigset_t *set, int signum); // 参1:自定义信号屏蔽字集, 参2:需要去除屏蔽的信号(将此信号所在位置0)
// 判断某个信号是否在信号集中
// 返回值:在集合: 1;不在: 0;出错: -1
int sigismember(const sigset_t *set, int signum);
sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
sigprocmask 函数
用来屏蔽信号、 解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB 中)
注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*
* 成功: 0;失败: -1,设置 errno
*
* 参数:
* 参1:how (假设当前的信号屏蔽字为 mask,自定义信号屏蔽字集为set)
* 1.SIG_BLOC(屏蔽信号), mask中原有的屏蔽信号不变,对set中已有的信号进行屏蔽,相当于 mask = mask | set
* 2.SIG_UNBLOCK(解除屏蔽), 对set中已有的信号取消屏蔽,相当于 mask = mask & ~set
* 3.IG_SETMASK(替换), 表示用set 替代原始屏蔽mask,相当于 mask = set
*
* 参2:set,传入参数,是一个位图, set 中那一位置1,就表示当前进程屏蔽哪个信号。
*
* 参3:oldset:传出参数,保存旧的信号屏蔽集。(在需要新的信号屏蔽集结束后,记得将旧的信号屏蔽集替换回来)
*/
sigpending 函数
读取当前进程的未决信号集
int sigpending(sigset_t *set);
/*
* set 传出参数。
*
* 返回值:成功: 0;失败: -1,设置 errno
*/
信号捕捉
signal函数
例子: signal.cpp
注册一个信号捕捉函数,信号捕捉实际由内核完成。
typedef void (*sighandler_t)(int); // 返回值为void,只有一个参数int的函数指针
sighandler_t signal(int signum, sighandler_t handler);
该函数由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中可能有不同的行为。 因此应该尽量避免使用它, 取而代之使用 sigaction 函数。
sigaction函数
修改信号处理动作(通常在 Linux 用其来注册一个信号的捕捉函数)
int sigaction(int signum, const *act, struct sigaction *oldact);
/*
* 成功: 0;失败: -1,设置 errno
*
* 参数:
* 参1:signum,需要捕捉的信号(建议不直接使用数字,而使用宏, 例如:2号信号使用SIGING
*
* 参2:act:传入参数,新的处理方式。
*
* 参3:oldact:传出参数,旧的处理方式。
*/
struct sigaction结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
/*
* sa_restorer:该元素是过时的,不应该使用, POSIX.1 标准将不指定该元素。 (弃用)
* sa_sigaction:当 sa_flags 被指定为 SA_SIGINFO 标志时,使用该信号处理程序。 (很少使用)
*
* sa_handler:指定名(即注册函数)。也可赋值为 SIG_IGN 表忽略 或 SIG_DFL 表执行默认动作
* sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。(若没有需要新增的屏蔽信号,通常使用sigempty(&act.sa_mask))
* sa_flags:通常设置为 0,表使用默认属性(捕捉到的信号,在信号捕捉后的处理函数中默认屏蔽) (设置默认属性是为了不让在处理函数中再次捕捉到此信号再重新执行处理函数,造成死循环)
*/
sigaction函数信号捕捉特性
- 进程正常运行时,默认 PCB 中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了
某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽
的信号不由☆来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复为☆。 - XXX 信号捕捉函数执行期间, XXX 信号自动被屏蔽。(atc.sa_flags = 0, 使用默认属性)
- 阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)
内核实现信号捕捉过程
SIGCHLD信号
SIGCHLE信号产生的条件
子进程状态发送改变(子进程终止时、子进程接收到 SIGSTOP 信号停止时、子进程处在停止态,接受到 SIGCONT 后唤醒时)
借助 SIGCHLD 信号回收子进程
子进程结束运行, 其父进程会收到 SIGCHLD 信号。 该信号的默认处理动作是忽略。 可以捕捉该信号, 在捕捉函数中完成子进程状态的回收。
SIGCHLD 信号注意问题
- 子进程继承父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集 spending。
- 注意注册信号捕捉函数的位置。
- 应该在 fork 之前,阻塞 SIGCHLD 信号。注册完捕捉函数后解除阻塞。
中断系统调用
系统调用可分为两类:慢速系统调用和其他系统调用
- 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如, read、 write、 pause(使调用进程挂起,直至捕捉到一个信号)、 wait…
- 其他系统调用: getpid、 getppid、 fork…
可修改 sa_flags 参数来设置被信号中断后系统调用是否重启。 SA_INTERRURT 不重启。 SA_RESTART 重启。
扩展了解
sa_flags 还有很多可选参数, 适用于不同情况。 如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将 sa_flags 设置为 SA_NODEFER,除非 sa_mask 中包含该信号。