一、信号基本概念
1.用户输入命令,在shell启动一个前台进程。
用户按下Ctrl-C产生一个硬件中断。
如果CPU当前正在执行这个进程代码,该进程用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
终端驱动程序将Ctrl-C解释成SIGINT信号,记在该进程PCB中。
当某个时刻要信号要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动态是终止进程,所有直接终止进程而不再返回它的用户空间代码执行。
2.kill -l命令可查看系统定义的信号列表
这些信号各自在什么条件下产生,默认处理动作是什么,在signal(7)有详细说明。
①Term:终止当前进程
②Core:终止当前进程并且Core Dump
③lgn:忽略该信号
④Stop:停止当前进程
⑤Cont:继续执行先前停止的进程
3.产生信号条件有?
用户在终端按下某些键时,终端驱动程序会发送信号给前台进程.
如Ctrl-C产生SIGINT信号,Ctrl-Z产生SIGTSTP信号。
硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当信号。
一个进程调用kill(2)函数可以发送信号给另一个进程。
二、如何产生信号?
1.Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core。
2.调用系统函数向进程发信号
kill命令是调用kill函数实现的。
kill函数可给一个指定进程发送指定信号。
raise函数可给当前进程发送指定信号(自己给自己发)。
abort函数使当前进程接收到SIGABRT信号而异常终止。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid) {
sleep(3);
if (kill(pid, SIGQUIT) < 0) {
perror("kill");
exit(1);
}
int sts;
wait(&sts);
if (WIFSIGNALED(sts)) {
printf("child terminate by signal %d\n", WTERMSIG(sts));
} else {
printf("child exit with other reason\n");
}
} else {
while (1) {
printf("child sleep 1 sec\n");
sleep(1);
}
}
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
int pfd[2];
if (pipe(pfd) < 0) {
perror("pipe");
exit(1);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid) {
close(pfd[0]);
close(pfd[1]);
int sts;
wait(&sts);
if (WIFSIGNALED(sts)) {
printf("signal = %d\n", WTERMSIG(sts));
} else {
printf("exit other ways\n");
}
} else {
sleep(3);
close(pfd[0]);
write(pfd[1], "hello\n", 6);
sleep(3);
}
return 0;
}
3.由软件条件产生信号
调用alarm函数可设定一个闹钟(告诉内核在几秒后给当前进程发SIGALRM信号)
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
alarm(5);
alarm(3);
unsigned int left = alarm(5);
printf("left = %d\n", left);
char i;
for (i = 0; i > -1; i++) {
printf("i = %d\n", i);
}
printf("i = %d\n", i);
return 0;
}
三、阻塞信号
1.信号递达:实际执行信号的处理动作。
2.信号未决:信号从产生到递达之间的状态。
3.进程可选择阻塞某个信号。被阻塞的信号产生时保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。
每个信号都有两个标志位分别表示:阻塞和未决,还有一个函数执行表示处理未决信号产生时,内核在进程控制块中设置该信号未决标志,直到信号递达该标志。
4.如果在进程解除对某信号的阻塞之前这种信号产生过多次,如何处理?
Linux实现:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
阻塞信号集也叫:当前进程的信号屏蔽字(signal mask)
5.信号集操作函数
sigset_t类型对每种信号用一个bit表示有效或无效状态,至于这个类型内部如何存储这些bit依赖于系统实现,从使用者角度是不必关心的,使用者只能调用一下函数来操作sigset_t变量:
6.信号屏蔽字读写函数
调用函数sigprocmask可读取或更改进程的信号屏蔽字
如果调用sigprocmask解除对当前若干未决信号的阻塞,在sigprocmask返回前至少将其中一个信号递达。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_SETMASK, &oldset, NULL);
int n = 10;
while (n > 0) {
sleep(1);
printf("proc sleep 1s\n");
n--;
}
return 0;
}
7.未决信号集
sigpending读取当前进程的未决信号集,通过set参数传出。
调用成功返回0,出错返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void print_sig(const sigset_t *set) {
for (int i = 1; i < 32; i++) {
sigismember(set, i) ? putchar('1') : putchar('0');
}
putchar(10);
return;
}
int main(void) {
sigset_t set, oldset, pset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
sigprocmask(SIG_BLOCK, &set, &oldset);
int n = 10;
while (n > 0) {
sigpending(&pset);
print_sig(&pset);
sleep(1);
n--;
}
sigprocmask(SIG_SETMASK, &oldset, NULL);
return 0;
}
四、捕捉信号
1.捕捉信号概念:
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数。
由于信号处理函数的代码是在用户空间,处理过程比较复杂。
2.sigaction
读取和修改与指定信号相关联的处理动作。
当某个信号处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证在处理某个信号时,如果这个信号再次发生,那么它会被阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽外,还希望自动屏蔽另外一些信号,用sa_mask字段说明这些额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void undead(int signo) {
printf("get signo=%d, I'm alive\n", signo);
return;
}
int main(void) {
struct sigaction newact, oldact;
newact.sa_handler = undead;
newact.sa_flags = 0;
sigemptyset(&newact.sa_mask);
sigaction(SIGINT, &newact, &oldact);
int n = 20;
while (n > 0) {
sleep(1);
n--;
}
sigaction(SIGINT, &oldact, NULL);
return 0;
}
3.pause函数
使调用进程挂起直到有信号递达。
①如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回。
②如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回。
③如果信号的处理动作是捕捉,则调用了信号处理函数之后,pause返回-1,errno设置为EINTR:被信号终端,pause只有出错的返回值。
4.可重入函数
当捕捉信号时,不论进程的主控制流程当前执行到哪,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。
信号处理函数一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。
引入信号处理函数使一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源,就有可能出现冲突。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void sig_alarm(int signo) {
return;
}
unsigned int mysleep(unsigned int sec) {
struct sigaction newact, oldact;
newact.sa_handler = sig_alarm; //SIG_DFL
newact.sa_flags = 0;
sigemptyset(&newact.sa_mask);
sigaction(SIGALRM, &newact, &oldact);
alarm(sec);
pause();
int unsleep = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
return unsleep;
}
int main(void) {
int n = 5;
while (n) {
printf("hello my sleep 1s\n");
mysleep(1);
n--;
}
return 0;
}
5.竞态条件与sigsuspend函数
1.在调用pause之前屏蔽SIGALRM信号使它不能体检递达就可以。
2.sigsuspend包含pause的挂起等待功能,同时解决竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause。
sigsuspend没有成功返回值,只有执行一个信号处理函数之后才返回,返回-1,errno设置为EINTR。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void sig_alarm(int signo) {
return;
}
unsigned int mysleep(unsigned int sec) {
struct sigaction newact, oldact;
sigset_t newmask, oldmask, susmask;
//替换ALARM的信号处理函数
newact.sa_handler = sig_alarm; //SIG_DFL
newact.sa_flags = 0;
sigemptyset(&newact.sa_mask);
sigaction(SIGALRM, &newact, &oldact);
//阻塞闹钟信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
//开启闹钟
alarm(sec);
//读取老的信号屏蔽字
susmask = oldmask;
//删除里面的alarm信号
sigdelset(&susmask, SIGALRM);
//临时设置信号屏蔽字为susmask,并且挂起程序等待信号到来
sigsuspend(&susmask);
perror("sigsuspend");
int unsleep = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unsleep;
}
int main(void) {
alarm(2);
unsigned int unslept = mysleep(10);
printf("unslept = %d\n", unslept);
return 0;
}
6.关于SIGCHLD信号
用wait和waitpid函数清理僵尸进程,①父进程可阻塞等待子进程结束,②也可非阻塞地查询是否有子进程结束等待清理(轮询方式)。
①:父进程阻塞了就不能处理自己的工作了。
②:父进程处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程,子进程终止时通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
1.编写一个程序完成功能:
父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void sig_child(int signo) {
int sts;
wait(&sts);
if (WIFEXITED(sts)) {
printf("exit with code = %d\n", WEXITSTATUS(sts));
} else {
printf("Lemon\n");
}
return;
}
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid) {
struct sigaction newact, oldact;
newact.sa_handler = sig_child;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGCHLD, &newact, &oldact);
int n = 10;
while (n--) {
printf("work!\n");
sleep(1);
}
} else {
sleep(3);
exit(2);
}
return 0;
}