1. 什么是进程?
从内核的角度,进程是系统分配资源的单位。当一个程序(静态)被加载到内存,操作系统为程序分配一个PCB(进程控制块)。
PCB:Process Control Block。在Linux中PCB叫做task_struct的结构体,我们称之为“进程描述符”。它里面包含了进程执行的所有信息,所以操作系统CPU对task_struct进行管理,相当于对进程进程管理。每一个进程都有一个PCB。
2. 信号的内核表示与信号阻塞
- 信号递达:实际执行信号的处理动作。
- 信号未决:信号从产生到递达之间的状态
- 进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直接进行解除对信号的阻塞,才会执行递达的动作
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达时可选的一种处理动作
3. 信号在内核中的表示
系统中的信号是发送给进程的,对于进程而言,信号是给进程的PCB(task_struct)发的,PCB使用位图结构(block位图、pending位图)结构记录收到的信号,这样进程执行时就知道自己收到什么信号了。
信号为什么要被保存在进程里?因为进程收到信号后,可能不会立即处理。这个信号从产生到递达之间的时间窗口内,进程需要记录该信号已经产生了,等到处理的时候才能知道哪些信号已经发生了。
- 比特位的内容为1或0,表示是否收到某个信号
- 比特位的位置(第几个),表示信号的编号c
- 所谓的“发信号”,本质就是操作系统区修改tack_struct的信号位图的对应比特位,发送信号实际上就是写信号。
使用kill -l查看操作系统有哪些信号
kill -l
操作系统是进程的管理者,只有它才有资格去修改task_struct内部的属性,即操作系统需要提供相应的系统调用以实现信号的发送、阻塞等功能。
task_struct中针对信号,包含了两张位图和一张函数指针表。分别是block位图、pending位图和handler处理函数指针表。
- block位图记录某个信号是否被阻塞,如果某个信号对应的比特为1表示该信号被阻塞,为0表示没有阻塞;对于被阻塞的信号,即使该信号产生了,进程也不会对该信号做任何处理。
- pending位图记表示收到了哪些信号。如果收到某个信号,则会将对应的比特位置1;处理完某个信号后,会将对应的比特位置0。由于每个比特位只能表示信号的有无,若在信号产生到信号递达的时间窗口内,重复收到多个同样的信号,最终也只会递达一次。
- handler函数指针数组用于记录对各个信号的处理方式。如果设置为SIG_DFL表示执行系统默认处理函数,设置为SIG_IGN表示忽略该信号;设置为用户空间的某个函数时,待信号递达时,则会从内核态切换回用户态以执行该部分代码
4. 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) SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
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的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
5. 信号集操作函数
int sigemptyset(sigset_t *set) | 将传入的set位图的各个位清零 |
int sigfillset(sigset_t *set) | 将传入的set位图的各个位置设置为1 |
int sigaddset(sigset_t *set, int signum) | 将signum信号在set中对应的比特位置设置为1 |
int sigdelset(sigset_t *set, inst signum) | 将signum信号在set中对应的比特位置设置为0 |
sigismember(const sigset_t *set, int signum) | 查看signum对应的比特位在set中是否为1,为1则返回1,否则返回0 |
注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后,可以调用sigaddset和sigdelset在该信号集中添加或删除某种信号。
上述接口只是对sigset_t这个位图结构做操作,并未对block和pending位图直接做操作,下面介绍关于这两个位图的系统调用函数:sigprocmask和sigpending
5.1 sigprocmask读取/修改进程的信号屏蔽字(阻塞信号集/blocking位图)
第一个参数:how
how可以取值SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK.
- SIG_BLOCK: set包含我们希望添加到当前信号屏蔽字中的信号,相当于mask=mask|set
- SIG_UNBLOCK: set包含我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
- SIG_SETMASK: 设置当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
第二个参数:set
用户自己定义的sigset_t类型的set
第三个参数:oldset
oldset是传出参数,调用sigprocmask系统调用函数之后,会传出调用该函数之前的进程的mask位图。
5.2 获取当前进程的信号屏蔽字
#include <stdio.h>
#include <signal.h>int main(){sigset_t oldset;sigprocmask(SIG_BLOCK,NULL,&oldset);for (int i = 1; i < NSIG; i++){if(sigismember(&oldset,i))printf("1");elseprintf("0");}printf("\n");return 0;
}
注意:NSIG的取值是最大的信号数+1
5.3 信号集操作函数练习
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
void sys_err(const char *str){perror(str);exit(1);
}void print_set(sigset_t *set){int i;for (i = 1; i < 32; i++){if (sigismember(set,i))putchar('1');elseputchar('0');}printf("\n");
}int main(){sigset_t set, oldset, pedset;int ret = 0;sigemptyset(&set);sigaddset(&set,SIGINT);ret = sigprocmask(SIG_BLOCK,&set,&oldset);if (ret == -1)sys_err("sigprocmask error");while(1){ret = sigpending(&pedset);if (ret == -1)sys_err("sigpending error");print_set(&pedset);sleep(1);}}
6. signal实现信号捕捉
注册一个信号捕捉函数。
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void sig_catch(int signo){printf("catch you!!%d\n",signo);return;
}int main(){signal(SIGINT,sig_catch);while(1);return 0;}
7. sigaction实现信号捕捉
注意sigaction的信号屏蔽字sa_mask,其作用域只存在于信号捕捉函数执行期间生效。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void sig_catch(int signo){printf("catch you!!%d\n",signo);return;
}int main(){struct sigaction act, oldact;act.sa_handler = sig_catch;sigemptyset(&(act.sa_mask));act.sa_flags = 0;int ret = sigaction(SIGINT,&act,&oldact);while(1);return 0;}
8. 信号捕捉的特性
- 进程正常运行时,默认PCB中有一个信号屏蔽字,假定为△,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由△来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为△。
- XXX信号捕捉函数执行期间,XXX信号自动被屏蔽(sa_flags = 0)
- 阻塞的常规信号不支持排队,产生多次只记录一次。
9. 内核实现信号捕捉间析
10. 借助信号捕捉回收子进程
SIGCHILD的产生条件:
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止位置,接收到SIGCONT后唤醒时
借助SIGCHILD信号回收子进程:
子进程结束运行,其父进程会收到SIGCHILD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。