相关概念
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即(用户感觉是立即,其实是要等待一个从用户区进入内核区的契机,站在CPU的时间量级上具有延迟性)停止运行,转而去处理信号,等信号处理结束,再继续执行后续指令。
每个进程收到的所有信号,都是由内核发送并处理的
与信号相关的事件和状态
产生信号:
- 按键产生,如:ctrl+c、ctrl+z、ctrl+\
- 系统(函数)调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill命令
递达:内核产生信号后,递送并且送达到进程,直接被内核处理
未决:产生与递达之间的状态,主要由于阻塞(屏蔽)导致该状态
从宏观上看,可以把递达与处理划等号,信号被递达了内核就处理,但从cpu时间量级的角度看,每个信号都会经历未决状态,因为当信号产生后,未决信号集中描述该信号的位立刻翻转为1(此时该信号就处于未决状态),如果此信号在阻塞信号集中未被屏蔽,未决信号集中描述该信号的位会再翻转为0,内核就去处理该信号,这个过程及其短暂。
信号处理方式:
- 执行默认处理动作(每一个信号都有自己的默认处理动作)
- 忽略(丢弃)
- 捕捉(自定义处理函数)
Linux内核的进程控制块PCB是一个结构体,除了包含进程id、状态、工作目录、用户id、组id、
文件描述符表,还包含了信号相关的信息,主要是阻塞信号集和未决信号集
阻塞信号集(信号屏蔽字):本质是位图,用来记录信号的屏蔽状态。被屏蔽的信号(0→1)会一直处于未决态,直到解除屏蔽(1→0)再对该信号进行处理
未决信号集:本质是位图,用来记录信号的处理状态。信号产生后由于某些原因(主要是阻塞)不能抵达(即未被处理),这类信号的集合称为未决信号集。当信号产生,未决信号集中描述该信号的位立刻翻转为1(默认为0),表示信号处于未决状态,当信号被处理后再翻转为0,这一时刻非常短暂。
信号四要素
- 编号(即value,不同操作系统可能不同)
- 可通过命令
kill -l
查看各个信号的名称和对应编号 - 1~31是常规信号,34~64是实时信号
- 可通过命令
- 名称:宏名(可屏蔽平台差异)
- 对应事件(只有对应事件发生后,该信号才会被内核发送,但不一定被递达,也就无法处理)
- 默认处理动作
- Term:终止进程
- Ign:忽略信号(默认即时对该种信号忽略操作)
- Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
特别强调,9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作,甚至不能将其设置为阻塞
常规信号一览表
可通过命令man 7 signal
查看帮助文档获取各个信号的四要素
信号的产生
终端按键产生信号
- Ctrl+c → 2)SIGINT(终止/中断) “INT”即interrupt
- Ctrl+z → 20)SIGTSTP(暂停/停止) “T”即terminal
- Ctrl+\ → 3)SIGQUIT(退出)
硬件异常产生信号
非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
函数调用产生信号
kill函数
给指定进程发送指定信号(不一定是杀死)
int kill(pid_t pid, int signum
)
参数:
- signum:待发送的信号(不推荐使用数字,应该使用宏名,因为不同操作系统信号编号可能不同,但名称一致)
- pid:被发送信号的进程的PID
- >0:发送信号给指定的进程
- =0:发送信号给与调用kill函数进程属于同一进程组的所有进程
- <0:取绝对值,发送信号给该绝对值所对应的进程组的所有进程
- =-1:发送信号给系统中有权限发送的所有进程
返回值:
- 0:成功
- -1:失败
kill命令产生信号:kill -SIGKILL pid
比如:kill -9 10426
raise函数
int raise(int sig);
给当前进程发送指定信号
abort函数
void abort(void);
给当前进程发送SIGABRT信号
软件条件产生信号
alarm函数
unsigned int alarm(unsigned int seconds);
使用自然计时法,定时发送14)SIGALRM信号给当前进程
- 参数seconds:定时秒数
- 返回值:上次定时的剩余时间(无错误情况)
常用alarm(0)
来取消定时器
ps:每个进程都有且只有唯一一个定时器
time 命令 : 查看程序执行时间,比如:time ./my_alarm
实际时间 = 用户时间 + 内核时间 + 等待时间(最长) ——> 程序运行的瓶颈在于IO,因此优化程序,首先优化IO
setitimer函数
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
设置定时器,可以实现周期定时,时间控制可以达到微秒级别
参数:
- which
- ITIMER_REAL: 采用自然计时 ——> 14)SIGALRM 即计算自然时间
- ITIMER_VIRTUAL: 采用用户(虚拟)空间计时 ——> 26)SIGVTALRM 即计算进程占用cpu的时间
- ITIMER_PROF: 采用内核+用户空间计时 ——> 27)SIGPROF 即计算进程占用cpu以及执行系统调用的时间
- old_value:传出参数,上次定时剩余时间
- new_value:定时秒数
struct itimerval {
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}it_interval;---> 周期定时秒数
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_value; ---> 第一次定时秒数
};
返回值:
- 成功:0
- 失败:-1,errno
信号集操作函数
信号集设定
sigset_t set; 自定义信号集
sigemptyset(sigset_t *set); 信号集中所有位全部置0
sigfillset(sigset_t *set); 信号集中所有位全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中,0→1
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除,1→0
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中, 在→1, 不在→0
sigprocmask函数
设置信号屏蔽字或者解除屏蔽
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
- how:
- SIG_BLOCK:设置阻塞
- SIG_UNBLOCK:取消阻塞
- SIG_SETMASK:用自定义set替换mask
- 传入参数set:自定义的set
- 传出参数oldset:将老的mask保存
返回值:
- 成功:0
- 失败:-1,设置errno
sigpending函数
读取当前进程的未决信号集(未决信号集不能直接修改,只能通过修改阻塞信号集来影响它)
int sigpending(sigset_t *set);
传出参数set:传出的未决信号集
返回值:
- 成功:0
- 失败:-1,设置errno
编程练习:使用信号集操作函数把Ctrl+z设置为屏蔽
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
void sys_err(const char *str) {
perror(str);
exit(1);
}
void print_set(const sigset_t *set) {
int i = 0;
for(i = 1; i <= 32; i++) {
if(sigismember(set, i)) putchar('1');
else putchar('0');
}
printf("\n");
}
int main(int argc, char *argv[]) {
sigset_t set, oldset, pedset;
sigemptyset(&set);
sigaddset(&set, SIGTSTP);
int ret = 0;
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);
}
return 0;
}
ps:这时使用Ctrl+z已经无法终止该程序了,只能先ps aux
找到进程PID,再用kill -9 pid
来杀死
※信号捕捉
signal函数
注册一个信号捕捉函数
typedef void (*sighandler_t)(int);
//函数指针
sighandler_t signal(int signum, sighandler_t handler);
ps:signal函数使用的不是通用标准,因此尽量避免使用,用更规范的sigaction函数
※sigaction函数
修改信号处理动作(通常在Linux中用它来注册一个信号捕捉函数)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
返回值:
- 成功:0
- 失败:-1,设置errno
参数:
- signum:要修改的信号
- 传入参数act:新处理动作
- 传出参数oldact:旧处理动作(如果不需要旧处理动作,可以传NULL)
结构体:
struct sigaction {
void (*sa_handler)(int); //捕捉函数名(同signal函数的参数),也可以用宏:SIG_IGN表忽略,SIG_DFL表执行默认动作
void (*sa_sigaction)(int, siginfo_t *, void *); //指定带参数的信号捕捉函数(基本用不到)
sigset_t sa_mask; //只作用于信号捕捉函数执行期间的信号屏蔽字
int sa_flags; //一般设为0,表示本信号屏蔽
};
编程练习:用sigaction函数完成信号捕捉
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
void sys_err(const char *str) {
perror(str);
exit(1);
}
void sig_catch(int signo) { //捕捉回调函数
printf("catch you!%d\n", signo);
return ;
}
int main(int argc, char *argv[]) {
struct sigaction act, oldact;
act.sa_handler = sig_catch; //设置捕捉回调函数
sigemptyset(&act.sa_mask); //设置屏蔽字清空,只在捕捉函数工作时有效
act.sa_flags = 0; //设置默认属性
int ret = sigaction(SIGTSTP, &act, &oldact); //注册信号捕捉函数
if(ret == -1) {
sys_err("sigaction error!");
}
while(1);
return 0;
}
信号捕捉特性
- 捕捉函数执行期间,信号屏蔽字由PCB中默认的mask转为sa_mask,捕捉函数执行结束后,再恢复回mask
- 捕捉函数执行期间,被捕捉的这个信号自动被屏蔽(前提是设置sa_flags = 0)
- 捕捉函数执行期间,被屏蔽的信号即使发送多次,解除屏蔽后只处理一次,即常规信号不支持排队
内核实现信号捕捉
SIGCHLD信号
SIGCHLD的产生条件
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接收到SIGCONT后唤醒时
总结:子进程基本上稍微有点动静就要发SIGCHLD信号告诉他爹
借助SIGCHLD信号回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略,可以捕捉该信号,在捕捉函数中完成子进程状态的回收
使用SIGCHLD
信号回收子进程比直接调用wait
函数来回收子进程有几个明显的优势:
- 非阻塞操作:在等待子进程结束时,如果直接调用
wait
,父进程会被阻塞,直到有子进程终止。而使用SIGCHLD
信号,父进程可以继续执行其他任务,而不必等待子进程结束。 - 响应性:通过使用信号机制,父进程可以对子进程的状态变化作出更迅速的反应。例如,当子进程终止时,父进程可以立即处理这个事件,而不需要定期检查子进程的状态。
- 避免忙等:如果使用轮询的方法(例如循环调用
wait
),可能会导致 CPU 资源的浪费。使用SIGCHLD
信号可以避免这种情况,因为它是基于事件驱动的。
※编程练习:借助SIGCHLD信号回收子进程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
void sys_err(const char *str) {
perror(str);
exit(1);
}
void catch_child(int signo) { //有子进程终止,发送SIGCHLD信号时,该函数会被内核回调
pid_t wpid;
int status;
while((wpid = waitpid(-1, &status, 0)) != -1) { //循环回收,防止多个子进程同时死亡,导致僵尸进程
if(WIFEXITED(status)) {
printf("-------catch child, id = %d, ret = %d\n", wpid, WEXITSTATUS(status)); //回收成功
}
}
return ;
}
int main(int argc, char *argv[]) {
pid_t pid;
//先把SIGCHLD信号设置阻塞,防止父进程捕捉函数还没注册成功,子进程就先死了
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
int i = 0;
for(i = 0; i < 5; i++) {
if((pid = fork()) == 0) break;
}
if(i == 5) {
struct sigaction act;
act.sa_handler = catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction (SIGCHLD, &act, NULL);
//如果在捕捉函数注册成功前,有子进程死亡,但是SIGCHLD信号已经被屏蔽
//因此父进程不予理会,等待函数注册完毕,解除阻塞后,如果有多个SIGCHLD信号发送给父进程
//处理程序也只会会被调用一次,因为普通信号不支持排队,但是因为给wait()函数加上了while循环
//这一次调用就可以处理在那个时间段所有已终止的子进程
//捕捉函数注册好了,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("i am parent, pid = %d\n", getpid());
while(1); //让父进程先别结束
} else {
printf("i am child, pid = %d\n", getpid());
return i;
}
return 0;
}
慢速系统调用被信号中断
系统调用可分为两类:
- 慢速系统调用:可能会使进程永远阻塞的一类,如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期),也可以设定系统调用是否重启。比如:
read
、write
、pause
、wait
等等 - 其他系统调用:
getpid
、getppid
、fork
等等
慢速系统调用阻塞期间,中断慢速系统调用的信号不能被屏蔽、忽略,该信号必须被捕捉,信号处理完再回到系统调用
被信号中断后返回-1,设置errno为EINTR,可以修改sa_flags
参数来设置被信号中断后是否重启系统调用:
- 不重启:SA_INTERRURT
- 重启:SA_RESTART