本文最后更新于:2021年4月8日 晚上
概览:Linux信号。
信号
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
产生信号
发往进程的诸多信号,通常都是源于内核,可以引发内核为进程产生信号的各类事件:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
- 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
信号的特点
- 使用简单
- 不能够传递大量信息
- 满足某个特定条件时才发送
- 优先级比较高
使用信号的主要目的
- 让进程知道已经发生了一个特定的事情
- 强迫进程执行它自己代码中的信号处理程序。
查看系统定义的信号列表命令:kill -l
其中前31个为常规信号,其余为实时信号。
编号 |
信号名称 |
对应事件 |
默认动作 |
2 |
SIGINT |
ctrl+c组合键,用户终端向正在运行中的由终端启动的程序发送信号 |
终止进程 |
9 |
SIGKILL |
无条件的终止进程,该信号不能够被忽略、处理和阻塞 |
终止进程,可以杀死任何进程 |
11 |
SIGSEGV |
指示进程进行了无效的内存访问(段错误) |
终止进程并产生core文件 |
13 |
SIGPIPE |
Borken pipe向一个没有读端的管道写数据 |
终止进程 |
14 |
SIGALRM |
定时器超时,超时的时间由系统调用alarm来设置 |
终止进程 |
17 |
SIGCHLD |
子进程结束时,父进程会收到这个信号 |
忽略这个信号 |
18 |
SIGCONT |
如果进程已经停止,则使其继续运行 |
继续/忽略 |
19 |
SIGSTOP |
停止进程的执行,信号不能被忽略、处理和阻塞 |
为终止进程 |
- 信号的5种默认处理动作
- TERM 终止进程
- IGN 当前进程忽略掉这个信号
- CORE 终止进程,并生成一个Core文件
- STOP 暂停当前进程
- CONT 继续执行当前被暂停的进程
- 信号的几种状态:产生、未决、递达。
kill相关函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <sys/types.h> #include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);
void abort(void);
|
尝试杀死自己。 while(1){ … ; kill(getpid(),9); }看看效果
alarm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <unistd.h> unsigned int alarm(unsigned int seconds);
|
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int main(){
int second = alarm(5); printf("second = %d \n",second);
sleep(2);
second = alarm(2); printf("second = %d \n",second);
while(1){ }
return 0; }
|
运行结果:
1 2 3
| second = 0 second = 3 Alarm clock
|
- 从上述结果中可以发现,实际上在进程sleep的过程中,alarm仍然在执行。
我们程序实际的时间由三部分组成:
- 内核时间,系统调用就需要在内核中完成
- 用户时间,程序代码执行的时间
- 其他消耗的时间,比如IO
重点:
- 定时器与进程的状态无关。
- 一个进程只有一个定时器!
setitimer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
|
实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <unistd.h> #include <signal.h> #include <stdlib.h>
static int times = 0; void mysignal(int num){ printf("times : %d -- recv sifnal : %d\n",times++,num); }
int main(){ signal(SIGALRM,mysignal);
struct itimerval neval; neval.it_interval.tv_sec = 2; neval.it_interval.tv_usec = 0;
neval.it_value.tv_sec = 5; neval.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL,&neval,NULL); printf("定时器开始了\n");
if(ret == -1){ perror("settimer"); exit(0); }
getchar();
return 0; }
|
信号捕捉-signal
SIGKILL
, SIGSTOP
不能被捕捉,不能被忽略。
- 针对接收到的某个信号,定义一些特殊的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
|
另外一个处理函数
1
| int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
|
信号集
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为“未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
阻塞信号集和未决信号集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决) - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态 3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较 - 这个标志位的值为1, 说明信号处于要被阻塞 - 阻塞信号集默认不阻塞任何的信号 - 如果想要阻塞某些信号需要用户调用系统的API
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了 - 如果没有阻塞,这个信号就被处理 - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
|
自定义信号机 - 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 以下信号集相关的函数都是对自定义的信号集进行操作。
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
|
修改pcb的阻塞集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
|
案例:阻塞2、3号位,输出未决信号集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h>
int main(){
sigset_t set; sigemptyset(&set);
sigaddset(&set,SIGINT); sigaddset(&set,SIGQUIT);
sigprocmask(SIG_BLOCK,&set,NULL);
while(1){ sigset_t pendingset; sigemptyset(&pendingset); sigpending(&pendingset);
for(int i=1;i<32;i++){ if(sigismember(&pendingset,i) == 1){ printf("1"); }else if(sigismember(&pendingset,i) == 0){ printf("0"); }else{ perror("sigismember"); exit(0); } } printf("\n"); sleep(1); }
return 0; }
|
在程序执行中,ctrl + c
以及ctrl + \
均被阻塞,程序将会打印出未决信号,其中2、3号位为1.
案例:定时器、信号处理、以及阻塞的综合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/time.h>
void mysignal(int num){ printf("解除阻塞\n"); sigset_t set; sigemptyset(&set);
sigaddset(&set,SIGINT); sigaddset(&set,SIGQUIT);
sigprocmask(SIG_UNBLOCK,&set,NULL);
}
int main(){
signal(SIGALRM,mysignal); struct itimerval timer; timer.it_value.tv_sec = 10; timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 5; timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL,&timer,NULL);
sigset_t set; sigemptyset(&set);
sigaddset(&set,SIGINT); sigaddset(&set,SIGQUIT);
sigprocmask(SIG_BLOCK,&set,NULL);
while(1){ sigset_t pendingset; sigemptyset(&pendingset); sigpending(&pendingset);
for(int i=1;i<32;i++){ if(sigismember(&pendingset,i) == 1){ printf("1"); }else if(sigismember(&pendingset,i) == 0){ printf("0"); }else{ perror("sigismember"); exit(0); } } printf("\n"); sleep(1); }
return 0; }
|
当按了ctrl + c
之后,会被阻塞,未决状态集打印出1,然后定时器到时之后,发送信号被捕捉,打印话语之后,解除阻塞,内核检测到信号时SIGINT
,之后就结束程序。
信号捕捉——sigaction()
sigaction
比signal
更通用!因为signal
是ANSI的标准,有多个版本,可能行为不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
|
实例:信号捕捉与处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void myalarm(int num){ printf("收到信号 :%d \n",num); }
int main(){
struct sigaction act; act.sa_flags = 0; act.sa_handler = myalarm; sigemptyset(&act.sa_mask);
sigaction(SIGALRM,&act,NULL);
struct itimerval newtime; newtime.it_interval.tv_sec = 2; newtime.it_interval.tv_usec = 0;
newtime.it_value.tv_sec = 5; newtime.it_value.tv_usec=0;
int ret = setitimer(ITIMER_REAL,&newtime,NULL); printf("定时器开始\n"); if(ret == -1){ perror("setitimer"); exit(0); }
while(1); return 0; }
|
内核捕捉信号的处理流程
SIGCHLD信号
SIGCHLD信号产生的条件
- 子进程终止时
- 子进程接收到 SIGSTOP 信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
以上条件发生时,内核都会给父进程发送信号,而父进程默认时阻塞信号的。
实例:通过SIGCHLD回收子进程
如果使用wait()
回收子进程的话,父进程会进入阻塞,这样效率很低,毕竟父进程也要有自己的事情去做。
可以使用信号捕捉,注册信号捕捉函数,在内部执行waitpid(NOHONG)
的代码来回收子进程。
普通情况——产生僵尸进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| int main(){
pid_t pid; for(int i=0;i<20;i++){ pid = fork(); if(pid == 0){ break; } }
if(pid > 0){
while(1){ printf("parent process\n"); sleep(1); } }else if(pid == 0){ printf("child process:%d\n",getpid()); } return 0; }
|
使用wait
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void mysigchld(int num){ printf("收到信号:%d\n",num); wait(NULL); }
int main(){
pid_t pid; for(int i=0;i<20;i++){ pid = fork(); if(pid == 0){ break; } }
if(pid > 0){
struct sigaction act; act.sa_flags = 0; act.sa_handler = mysigchld; sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,NULL);
while(1){ printf("parent process\n"); sleep(1); } }else if(pid == 0){ printf("child process:%d\n",getpid()); } return 0; }
|
执行之后,使用ps aux
查看可以发现,其实仍然可能会有一部分进程没有被回收,变成了僵尸进程。
这其实与信号集有关,当子进程死亡之后,会产生SIGCHLD信号,此时可能会有多个进程死亡,但是信号集只能说明有信号发送到了,未决状态。有的信号就会被丢弃,从而导致有部分子进程没有被回收。
改进方式1:
1 2 3
| while(1){ wait(NULL); }
|
但是不建议这样做,毕竟父进程还要去做其他事情,这样会停滞在信号处理函数中。
改进方式2:
1 2 3 4 5 6 7 8 9 10 11 12
| while(1){ int ret = waitpid(-1,NULL,WNOHANG); if(ret > 0){ printf("child process die: %d\n",ret); }else if(ret == 0){ break; }else if(ret == -1){ printf("all child die\n"); break; } }
|
对于方式2,大部分情况下,都可以正常运行,但是某些情况之下,仍然会产生问题,会出现段错误。
出现这种情况的原因在于,如果全部的子进程都运行完了,而此时的父进程的信号捕捉函数还没有注册成功,那么就会出现问题。
疑问:为什么会出现问题?难道是因为子进程全部死掉后,OS发现不会产生SIGCHLD信号而产生错误的吗?否则的话,只是不会触发而已,为什么会是段错误?
???
总体解决思路
在整个程序运行之前,先阻塞SIGCHLD信号,然后等父进程注册号信号之后,再打开SIGCHLD信号,从而解决问题。
必须是整个程序运行前,而非产生完子进程之后,否则可能面临同样的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| void mysigchld(int num){ printf("收到信号:%d\n",num); while(1){ int ret = waitpid(-1,NULL,WNOHANG); if(ret > 0){ printf("child process die: %d\n",ret); }else if(ret == 0){ break; }else if(ret == -1){ printf("all child die\n"); break; } } }
int main(){
sigset_t set; sigemptyset(&set); sigaddset(&set,SIGCHLD); sigprocmask(SIG_BLOCK,&set,NULL);
pid_t pid; for(int i=0;i<20;i++){ pid = fork(); if(pid == 0){ break; } }
if(pid > 0){ struct sigaction act; act.sa_flags = 0; act.sa_handler = mysigchld; sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,NULL);
sigprocmask(SIG_UNBLOCK,&set,NULL);
while(1){ printf("parent process\n"); sleep(1); } }else if(pid == 0){ printf("child process:%d\n",getpid()); } return 0; }
|
补充
CSAPP内容
疑问
- 僵尸进程产生是因为 子进程停止而父进程还没有回收他的资源,那么父进程终止后,子进程的资源就会被回收吗?
- 信号集 父子进程共享吗?
- 父进程回收子进程的哪一部分资源?
- 信号处理函数,