1.基本概念
1)什么是信号?
事件(信号)
过程(进程)1---------v--------->
/ \
/事件\
过程(进程)2 /---v---\
/ \
/ \
过程(进程)3 /--- ---\
信号是提供异步事件处理机制的软件中断。这些异步事件可能来自硬件设备,也可能来自系统内核,甚至可能来自用户程序。进程之间可以相互发送信号,这使信号成为一种进程间通信(Inter-Process Communication, IPC)的基本手段。信号的异步特性不仅表现为它的产生是异步的,对它的处理同样也是异步的。程序设计者不可能也不需要精确地预见什么时候触发什么信号,也同样无法预见该信号究竟在什么时候会被处理。一切尽在内核操控下异步地发生。
2)什么是信号处理?
每一个信号都有其生命周期:
产生:信号被生成,并被发送至系统内核
未决:信号被内核缓存,而后被递送至目标进程
递送:内核已将信号发送至目标进程
忽略 - 什么也不做。
捕获 - 暂停当前的执行过程,转而调用一个事先写好的信号处理函数,待该函数完成并返回后,再继续之前被中断的过程。
默认 - 既不忽略该信号,也不用自己定义处理方式,而是按照系统默认的方式予以响应。
激励(信号)->响应(信号处理)
3)信号的名称和编号
信号名称:形如SIGXXX的字符串或宏定义,提高可读性。
信号编号:整数
通过kill -l命令查看当前系统所支持的全部信号名称及其编号。
1~31,31个不可靠信号,也叫非实时信号。
34~64, 31个可靠信号,也叫实时信号。
共62个信号,注意没有32和33信号。
SIGHUP(1),控制终端关闭,终止
SIGINT(2),用户产生中断符(Ctrl+C),终止 kill -2/-SIGINT 进程PID
SIGQUIT(3),用户产生退出符(Ctrl+\),终止+转储 kill -3/-SIGQUIT 进程PID
SIGBUS(7),硬件或内存对齐错误,终止+转储
SIGKILL(9),不能被捕获,忽略和默认,终止
SIGSEGV(11),无效内存访问,终止+转储
SIGPIPE(13),向读端已关闭的管道写入,终止
SIGALRM(14),alarm函数设置的闹钟到期,终止
SIGTERM(15),可被捕获和忽略,终止 kill 进程PID,默认发送15信号
SIGCHLD(17),子进程终止,忽略
SIGIO(29),异步I/O事件,终止
2.捕获信号
#include <signal.h>
typedef void (*sighandler_t) (int);
|
函数指针,指向一个接受整型参数且无返回值的函数
设置针对特定信号的处理方式,即捕获特定的信号:
sighandler_t signal(int signum, sighandler_t handler);
成功返回原信号处理方式,失败返回SIG_ERR(sighandler_t类型的-1)。
signum - 信号编号
handler - 信号处理函数指针,也可以取以下值:
SIG_IGN - 忽略信号
SIG_DFL - 默认操作
...
// 定义信号处理函数
void sigint (int signum) {
SIGINT(2)信号的处理代码
}
...
// 捕获SIGINT(2)信号
if (signal(SIGINT, sigint) == SIG_ERR) {
perror("signal");
return -1;
}
...
SIGINT(2)
v
SIGINT(2)/PID/sigint->系统内核
v
目标进程中的sigint函数
signal(SIGINT, SIG_DFL); // 按默认方式处理
signal(SIGINT, SIG_IGN); // 忽略信号
当一个可靠信号正在被处理的过程中,相同的信号再次产生,该信号会被阻塞,直到前一个信号处理完成,即从信号处理函数中返回,后一个被阻塞的信号才被递送,进而再次执行信号处理函数。当一个不可靠信号正在被处理的过程中,多个相同的信号再次产生,只有第一个信号会被阻塞,其它信号直接丢弃,如果是可靠信号,都会被阻塞,并按照信号产生的先后顺序依次被递送。
信号处理函数及被其调用的函数都有可能发生重入,由此可能引发无可预知的风险。
global = 0;
...
++global;
...
所有标准I/O函数都是不可重入函数。在信号处理的过程中要慎用。
A信号->A信号处理函数 \ 打印AAAAAA
> printf(调试信息);
B信号->B信号处理函数 / 打印BBBBBB
AAABBBBBBAAA
代码:signal.c
/*signal.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sigint(int signum) {
signal(SIGINT, SIG_DFL);
printf("%d进程:收到%d信号!\n", getpid(), signum);
signal(SIGINT, sigint);
}
void sigterm(int signum) {
printf("%d进程:收到%d信号!\n", getpid(), signum);
printf("妥当善后...\n");
exit(0);
}
int main(void) {
if (signal(/*2*/SIGINT, sigint) == SIG_ERR) {
perror("signal");
return -1;
}
if (signal(/*3*/SIGQUIT, SIG_IGN) ==SIG_ERR) {
perror("signal");
return -1;
}
if (signal(/*9*//*SIGKILL*/15/*SIGTERM*/,
/*SIG_IGN*/sigterm/*SIG_DFL*/)==SIG_ERR){
perror("signal");
return -1;
}
for (;;);
return 0;
}
3.信号捕获流程
中断
主控制流程--------- v ----------->
/ \
信号处理函数 / ------> \ 用户空间
/ \ / \ ---------
/ \ / \ 内核空间
内核处理流程-----------> ---------->
do_signal system_call
handle_signal sys_sigreturn
setup_frame restore_sigcontext
信号的本质是一个中断的处理过程,单线程,而非多线程的并发过程。
线程安全的函数未必是可重入函数。
--------- -------
锁机制 局部化
4.信号捕获的一次性问题
在某些非Linux操作系统上,存在信号捕获的一次性问题:
即使设置了对某个信号的捕获,只有设置后的第一个该信号被递送时,信号处理函数会被执行,以后再来相同的信号,均按默认方式处理。如果希望对信号的捕获具有持久性,可以在信号处理函数返回前再次设置对该信号的捕获。
5.太平间信号
通过SIGCHLD(17)信号高效地回收子进程僵尸。
高效:及时性,适时性。
6.信号处理的继承与恢复
1)fork/vfork函数创建的子进程会继承父进程的信号处理方式,直到子进程调用exec函数创建新进程替代其自身为止。
2)exec函数创建的新进程会将原进程中被设置为捕获的信号还原为默认处理。在原进程中被忽略的信号于新进程中继续被忽略。
7.发送信号
1)通过键盘向当前拥有控制终端的前台进程发送信号
Ctrl+C -> SIGINT(2),默认终止进程
Ctrl+\ -> SIGQUIT(3),默认终止进程且转储
Ctrl+Z -> SIGTSTP(20),默认停止(挂起)进程
2)来自硬件或者内核的错误和异常引发的信号
SIGILL(4),进程试图执行非法指令
SIGBUS(7),硬件或总线对齐错误
SIGFPE(8),浮点异常
SIGSEGV(11),无效内存访问
SIGPIPE(13),向无读端的管道写入
SIGSTKFLT(16),浮点数协处理器栈错误
SIGXFSZ(25),文件资源超限
SIGPWR(30),断电
SIGSYS(31),无效系统调用
3)通过kill命令发送信号
kill [-信号] PIDs
|
缺省发送SIGTERM(15)信号
超级用户可以给任何进程发信号,普通用户只能给自己的进程发信号。
4)调用函数发送信号
向特定的进程(组)发送信号:
int kill(pid_t pid, int signum);
成功(至少发出去一个信号)返回0,失败返回-1。
pid - 进程(组)标识,可取以下值:
<-1:向-pid进程组中的所有进程发送信号
-1: 向系统中的所有进程发送信号
0: 向调用进程同组的所有进程发送信号
>0: 向进程标识为pid的特定进程发送信号
signum - 信号编号,取0用于检查pid进程是否存在,如果不存在,kill函数会返回-1,且置errno为ESRCH。
向调用进程自己发送信号:
int raise(int signum);
成功返回0,失败返回-1。
signum - 信号编号
raise(SIGINT);
kill(getpid(), SIGINT);
通过raise或kill向调用进程自己发送信号时,如果该信号被捕获,则要等到信号处理函数返回后,这两个函数才会返回。
但是用kill向其他进程发送信号时,不用等信号处理函数返回,可以连续向其他进程发信号。
8.暂停、睡眠和闹钟
暂停,即不受时间限制的睡眠:
int pause(void);
成功阻塞,失败返回-1。
该函数使调用进程进入无时限的睡眠状态,即不参与内核调度,直到有信号终止了调用进程或被捕获。如果有信号被调用进程捕获,当信号处理函数返回以后,pause函数才会返回,且返回值为-1,同时置errno为EINTR,表示阻塞的系统调用被信号中断。pause函数要么不返回,要么返回-1,不会返回0。当信号被忽略会一直阻塞
受时间限制的睡眠:
unsigned int sleep(unsigned int seconds);
返回0或剩余秒数。
seconds - 以秒为单位的睡眠时限
该函数使调用进程睡眠seconds秒,除非有信号终止了调用进程或被其捕获。如果有信号被调用进程捕获,在信号处理函数返回以后,sleep函数才会返回,且返回值为剩余秒数,否则该函数返回0,表示睡眠充足。
int usleep(useconds_t usec);
睡够了返回0,睡不够返回-1,同时置errno为EINTR。
usec - 为微秒为单位的睡眠时限
1微秒=10^-6秒
Intel CPU:50~55毫秒
设置闹钟
unsigned int alarm(unsigned int seconds);
返回0或先前闹钟的剩余时间。
seconds - 以秒为单位的闹钟时间
alarm函数使系统内核在该函数被调用以后seconds秒的时候,向调用进程发送SIGALRM(14)信号。若在调用该函数前已设过闹钟且尚未到期,则该函数会重设闹钟,并返回先前所设闹钟的剩余秒数,否则返回0。若seconds参数取0,则取消之前设置过且未到期的闹钟。
通过alarm函数所设置的定时只是一次性的,即在定时到期时发送一次SIGALRM(14)信号,此后不会再发送该信号。如果希望获得周期性的定时效果,可以在SIGALRM(14)信号处理函数中继续调用alarm函数,完成下一个定时的设置。
9.信号集(位集)
#include <signal.h>
typedef __sigset_t sigset_t;
#include <sigset.h>
typedef struct {
unsigned long int __val[_SIGSET_NWORDS];
32 个元素
32 X 32 = 1024位
} __sigset_t;
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
|<-1024位->|
- - - - - - - - -
4 3 2 1
void foo(int a[10]) {}
void foo(int a[]) {}
void foo(int* a) {}
struct Student { ... };
void bar(struct Student s) { ... }
int main(void) {
int a[10] = { ... };
foo(a);
...
struct Student s = { int a[10]; }; //避免函数传参为野指针
bar(s);
}
int sigfillset(sigset_t* sigset); //填满信号集,即将信号集的全部信号位置1
int sigemptyset(sigset_t* sigset); //清空信号集,即将信号集的全部信号位置0
int sigaddset(sigset_t* sigset, int signum); //加入信号,即将信号集中的特定信号位置1
int sigdelset(sigset_t* sigset, int signum); //删除信号,即将信号集中的特定信号位置0
成功返回0,失败返回-1。
int sigismember(sigset_t* sigset, int signum); //检查信号,即判断信号集中的特定信号位是否为1
有则返回1,无则返回0,失败返回-1。
#include <stdio.h>
#include <signal.h>
void printb(char byte) {
for (int i = 0; i < 8; ++i)
printf("%c", byte & 1 << 7 - i ? '1' : '0');
printf(" ");
}
void printm(void* buff, size_t size) {
for (int i = 0; i < size; ++i) {
printb(((char*)buff)[size-i-1]);
if ((i + 1) % 4 == 0 || i == size - 1)
printf("\n");
}
}
int main(void) {
sigset_t sigset;
printf("满信号集:\n");
sigfillset(&sigset);
printm(&sigset, sizeof(sigset));
printf("空信号集:\n");
sigemptyset(&sigset);
printm(&sigset, sizeof(sigset));
printf("加入SIGINT(2)信号:\n");
sigaddset(&sigset, SIGINT);
printm(&sigset, sizeof(sigset));
printf("加入SIGQUIT(3)信号:\n");
sigaddset(&sigset, SIGQUIT);
printm(&sigset, sizeof(sigset));
printf("删除SIGQUIT(3)信号:\n");
sigdelset(&sigset, SIGQUIT);
printm(&sigset, sizeof(sigset));
printf("%d\n", sigismember(&sigset,SIGINT));
printf("%d\n", sigismember(&sigset,SIGQUIT));
return 0;
}
10.信号屏蔽
1)递送、未决和掩码
当信号产生时,系统内核在目标进程的进程表项中,以信号位置1的方式,存储该信号,这个过程就叫做递送。信号从产生到完成递送之间存在一定的时间间隔,处于该间隔状态的信号就是属于未决信号。每个进程都有一个信号掩码,它实际上是一个信号集,位于该信号集中的信号一旦产生,并不会被递送给相应的进程,而是会被阻塞于未决状态。
当进程正在执行类似更新数据库、设置硬件状态等敏感任务时,可能不希望被某些信号中断。这是可以通过信号掩码暂时屏蔽而非忽略这些信号,使其一旦产生即被阻塞于未决状态,待特定任务完成以后,在恢复对这些信号的处理。
2)设置信号掩码
int sigprocmask(int how, const sigset_t* sigset, sigset_t* oldset);
成功返回0,失败返回-1。
how - 信号掩码的修改方式,可取以下值:
SIG_BLOCK - 将sigset中的信号加入当前掩码
SIG_UNBLOCK - 从当前掩码中删除sigset中的信号
SIG_SETMASK - 将sigset设置为当前掩码
sigset - 信号集,取NULL则忽略参数
oldset - 输出原信号掩码,取NULL则忽略此参数
3)获取未决信号
int sigpending(sigset_t* sigset);
成功返回0,失败返回-1。
sigset - 输出未决信号集
4)不可靠信号最多被信号掩码屏蔽一次,在屏蔽期间再有更多的相同信号,一律被丢弃。可靠信号会全部被保留下来,且按照发送的顺序排成队列。