进程间通信(四)信号
信号的基本概念
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
产生信号的事件
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
-
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
-
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
-
系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU时间超限,或者该进程的某个子进程退出。
-
运行 kill 命令或调用 kill 函数。
信号的目的与特点
使用信号有两个目的:第一,让进程知道已经发生了一个特定的事件;第二,强迫进程执行它自己代码中的信号处理程序。
信号的四个特点:
- 简单:使用简单,但内核实现复杂
- 不能大量信息:仅能携带信号值,一般不用做进程通信
- 满足某个特定条件才能发送
- 优先级较高
各种信号
查看系统定义的信号列表
kill -l
其中,前31个信号为常规信号,其余为实时信号。
编号 | 信号名称 | 对应事件 | 默认动作 |
---|---|---|---|
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~64 | SIGRTMIN~SIGRTMAX | LINUX的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
SIGKILL 和 SIGSTOP信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
信号的状态及默认处理动作
信号的5中处理动作:
- Trem :终止进程
- Ign :当前进程忽略掉这个信号
- Core :默认动作是终止进程,并产生一个 Core文件(核心转储文件)
- Stop :暂停当前进程
- Cont :继续执行当前被暂停的进程
信号有三种状态:产生、未决、递达。与传送快递方式相同,首先产生一个快递订单,状态为产生;然后在传送过程中为 未决 状态;当快递到达时,状态为 递达状态。
信号相关函数
kill(), raise(), abort()
- 函数原型
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
-
功能:给任何进程或者进程组,发送指定信号。
-
参数
pid
:需要发送的进程id。pid > 0 :将信号发送给指定进程。pid = 0:将信号发送给当前的进程的进程组所有进程。pid = -1:将信号发送给每一个有权限接受这个信号的进程。pid < -1:进程组id 取反。sig
:发送信号的编号 / 宏值,0 表示不发送任何信号
- 函数原型
#include <signal.h>
int raise(int sig);
-
功能:给当前进程发送信号
-
参数:要发送的信号
-
返回值:成功返回0,失败返回非0值。
-
实现:
kill(getpid(), sig)
- 函数原型
#include <stdlib.h>
void abort(void);
-
功能:发送
SIGABRT
信号给当前进程,默认杀死当前进程 -
实现:
kill(getpid(), SIGABRT
kill 示例
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(){
pid_t pid = fork();
if (pid == 0){
for (int i = 0; i < 5; i++){
printf("child process\n");
sleep(1);
}
}
else if (pid > 0){
printf("parent process\n");
sleep(2);
printf("kill child pricess now\n");
kill(pid, SIGINT);
}
return 0;
}
子进程未执行结束,父进程将其kill
alarm() 定时器
- 函数原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
功能:设计定时器。函数调用开始倒计时,倒计时为0时,函数会给当前进程发送一个信号:
SIGALARM
。 -
参数:
seconds
:倒计时的市场,单位为秒。如果参数为0,定时器无效。取消一个定时器,使用alarm(0)
- 返回值:之前没有定时器,返回0;之前有定时器,返回之前的定时器剩余的时间
SIGALARM
:默认终止当前的进程,每一个进程有且只有唯一的一个定时器。
alarm(10); 返回0
// 过1s
alarm(5); 返回9
示例2:alarm 定时器
#include <unistd.h>
#include <stdio.h>
int main(){
int seconds = alarm(5);
printf("seconds = %d\n", seconds); // 第一次调用返回0
sleep(3);
seconds = alarm(1);
printf("seconds = %d\n", seconds); // 非第一次调用返回上一个倒计时的剩余时间
while (1){}
return 0;
}
在第一次调用时,返回值为0,在接下来的调用时,返回值为上一次的倒计时剩余时间。可以看到 alarm
是非阻塞的,在计时结束后,发送 SIGALARM
信号,终止进程。
setitimer() 定时器
- 函数原型
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
-
功能:设置定时器,可以替代alarm(),精度更高。可以实现周期定时。
-
参数
which
:定时器以什么时间计时。ITIMEER_REAL
:现实时间,时间到达后,发送SIGALRM
;ITIMER_VIRUTAL
:用户时间,时间到达后,发送SIGVTALRM
;ITIMER_PROF
以该进程在用户态和内核态所消耗的时间来计算,时间到达,发送SIGPROF
信号。new_value
:设置定时器的属性。
truct itimerval {
struct timeval it_interval; // 定时器时间间隔
struct timeval it_value; // 定时器执行的延迟时间
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
old_value
:记录上次的定时的时间参数,一般不使用,指定为NULL
- 返回值
- 成功返回0
- 失败返回
-1
,设置errorno
示例3:setitimer()
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
// 过3s后,每隔2s定时一次
int main(){
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟时间
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞
printf("clock start\n");
if (ret == -1){
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
程序在过 3s 后,发送 SIGALARM
程序结束。
信号捕捉函数(signal(), sigaction())
- 函数原型
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-
函数功能:设置某个信号的捕捉行为。
SIGKILL
和SIGSTOP
不能被捕捉,不能被忽略。 -
参数
signum
:要捕捉的信号,一般使用宏值handler
:捕捉到信号的处理函数。SIG_IGN
表示忽略信号;SIG_DFL
表示信号默认的行为;回调函数:该函数由内核调用,该函数用于捕捉到信号后,系统如何去处理该信号。
- 返回值
- 成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL
- 失败返回
SIG_ERR
,设置错误号
回调函数:需要程序员实现,函数类型根据实际需求,看函数指针的定义。在信号产生后,由内核调用。
函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置即可。
示例3:捕捉信号 SIGALRM
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num){// 表示捕捉的信号的值
printf("Get signal value : %d\n", num);
printf("----------------------\n");
}
// 过3s后,每隔2s定时一次
int main(){
// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
signal(SIGALRM, myalarm);
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟时间
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞
printf("clock start\n");
if (ret == -1){
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
首先注册信号捕捉SIGALRM
函数,在延迟时间 3s 后,捕捉信号,程序继续开始间隔2s发送 SIGALRM
信号,通过回调函数捕捉信号。
- 函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
-
函数功能:检查或者修改的信号的处理。
-
参数
signum
:需要捕捉的信号的编号或者宏值act
:捕捉到信号后的处理动作oldact
:上一次对信号捕捉相关的设置,一般不使用,传递NULL
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
// 不常用,
void (*sa_sigaction)(int, siginfo_t *, void *);
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号
sigset_t sa_mask;
// 使用哪一个信号处理函数。0表示使用 `sq_handler` ,`SA_SIGINFO` 表示使用 sa_sigaction
int sa_flags;
void (*sa_restorer)(void); // 无用
};
- 返回值:成功返回0,失败返回-1,设置
errorno
示例4:sigaction() 信号捕捉函数
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num){// 表示捕捉的信号的值
printf("Get signal value : %d\n", num);
printf("----------------------\n");
}
// 过3s后,每隔2s定时一次
int main(){
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask); // 清空临时阻塞信号集
// 注册信号捕捉
sigaction(SIGALRM, &act, NULL);
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟时间
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞
printf("clock start\n");
if (ret == -1){
perror("setitimer");
exit(0);
}
// getchar();
while (1);
return 0;
}
内核信号捕捉过程
SIGCHLD 信号
SIGCHLD
信号产生的条件:
- 子进程终止时
- 子进程接收到
SIGSTOP
信号停止时 - 子进程处在停止态时,接收到
SIGCONT
后唤醒。
以上上中条件都会向父进程发送sigchld
信号,父进程默认会忽略该信号。
在子进程执行结束后,父进程未回收子进程资源,使得子进程为僵尸进程。父进程可以通过捕捉 SIGCHLD
信号,释放子进程资源来解决僵尸进程的问题。
示例5:利用 SIGCHLD
回收子进程资源
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
// 回收子进程PCB资源
void myFun(int num){
printf("get signal: %d\n", num);
// 第一个子进程死亡后,发送SIGCHLD信号,未决信号集中 17 位设为1,在其他子进程死亡后,也相继发送17号信号,
// 但信号集只能保存一个
wait(NULL);
// 可以释放所有子进程的资源,但是父进程阻塞在这里,无法向下执行
// while(1){
// wait(NULL);
// }
// 父进程不会阻塞,可以释放所有子进程资源,
// 但可能发生:信号没有注册成功,所有子进程已经结束,会 core dump
// 解决方案:提前设置好阻塞信号集
while (1){
int ret = waitpid(-1, NULL, WNOHANG);
if (ret > 0){ // 某个子进程结束
printf("child die, pid = %d\n", ret);
}
else if (ret == 0){ // 还有子进程在执行
break;
}
else if (ret == -1){// 所有子进程回收完毕
break;
}
}
}
int main(){
// 提前设置好信号阻塞
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIG_BLOCK);
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 = myFun;
sigemptyset(&act.sa_mask);
// 捕捉子进程结束时,发送的 SIGCHLD 信号
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while (1){
printf("parent process pid : %d\n", getpid());
sleep(2);
}
}
else if (pid == 0){
// 子进程
printf("child process pid: %d\n", getpid());
}
return 0;
}
信号集
未决信号集与阻塞信号集
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为未决信号集 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
- 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
- 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
- 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
阻塞信号集与未决信号集在信号处理时如何共同操作信号?
- 用户通过键盘
Ctrl + C
,产生2号信号SIGINT
(信号被创建)
- 信号产生但是没有被处理(未决)
- 在内核中将所有没有被处理的信号存储在一个集合中(未决信号集)
SIGINT
信号被存储在第2个标志位,该标志位的值为0,说明信号不是未决状态;该标志位为1,说明信号集处于未决状态
- 未决状态的信号处理之前,需要与另一个信号集(阻塞信号集)进行比较。
- 阻塞信号集默认不阻塞任何信号
- 如需阻塞某些信号,可通过系统调用设置阻塞信号
- 在处理的时候和阻塞信号集中的标志位进行查询,查看是否对该信号设置阻塞
- 如果没有阻塞,则处理该信号
- 如果阻塞了,则喜好继续处于未决状态,直至阻塞接触,处理该信号
自定义信号集相关函数
- 函数原型
int sigemptyset(sigset_t *set); // 将信号集所有标志位设为0
int sigfillset(sigset_t *set); // 将信号集所有标志位设为1
int sigaddset(sigset_t *set, int signum); // 将某一个信号对应标志位设为1,阻塞该信号
int sigdelset(sigset_t *set, int signum); // 将某一个信号对应标志位设为0,不阻塞该信号
// 给出信号集中某个信号是否阻塞,返回1 表示 signum 阻塞,返回 0 表示signum 不阻塞
int sigismember(const sigset_t *set, int signum);
-
功能:清空信号集中的数据,将信号集中的所有标志位置为0
-
参数:传出参数,需要操作的信号集
-
返回值
- 成功返回0,失败返回-1,设置
errorno
示例6:信号集使用
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main(){
// 创建一个信号集
sigset_t set;
// 清空信号集
sigemptyset(&set);
// 判断 SIGINT 是否在信号集 set 中
int ret = sigismember(&set, SIGINT);
if (ret == 1){
printf("SIGINT block\n");
}else if (ret == 0){
printf("SIGINT no block\n");
}
else{
perror("sigismember");
exit(0);
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 判断是否在信号集中
ret = sigismember(&set, SIGINT);
if (ret == 1){
printf("SIGINT block\n");
}else if (ret == 0){
printf("SIGINT no block\n");
}
else{
perror("sigismember");
exit(0);
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断是否在信号集中
ret = sigismember(&set, SIGQUIT);
if (ret == 1){
printf("SIGQUIT block\n");
}else if (ret == 0){
printf("SIGQUIT no block\n");
}
else{
perror("sigismember");
exit(0);
}
return 0;
}
首先对自定义信号集 set
置0,然后将 SIGINT
与 SIGQUIT
设为 1。执行结果如下
系统信号集操作函数
- 函数原型
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
函数功能:查看或者修改阻塞信号集,将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
-
参数
-
how
:如何对内核阻塞信号集处理
SIG_BLOCK
(mask | set):将用户设置的默认信号集添加到内核中,内核中原来的数据不变。
SIG_UNBLOCK
(mask &= ~set):根据用户设置的数据,对内核中的数据进行解除阻塞。
SIG_SETMASK
:覆盖内核中原来的值 -
set
:初始化好的用户自定义的信号集 -
oldset
:保存设置之前的内核中的阻塞信号集的状态,一般设为NULL
- 返回值
- 成功返回0
- 失败返回-1,设置错误号
- 函数原型
int sigpending(sigset_t* set); // 获取内核中的未决信号
示例7:设置并查看内核阻塞信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
sigset_t set;
sigemptyset(&set);
// 将2 号 3号 信号添加到信号集
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int n = 0;
while (1){
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
for (int i = 1 ; i <= 31; i++){
if (sigismember(&pendingset, i) == 1){
printf("1");
}
else if (sigismember(&pendingset, i) == 0){
printf("0");
}
else{
perror("sigismember");
}
}
printf("\n");
sleep(1);
if (++n == 10){
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
首先将2号信号与3号信号设置为阻塞,所以在接受到 Ctrl +c
和 Ctrl + \
,第2个和第三个标志位为 1,信号被阻塞,处于未决状态。循环10次后,解除阻塞,处理SIGINT
信号,退出程序。