0725------Linux基础----------信号
1.基础知识巩固
1.1 中断分为两类:
a)硬中断,就是通常所说的中断,中断处理程序运行在内核态,需要一定的硬件支持;
b)软中断,是在软件层次上对中断的一种模拟,就是常说的信号,它的处理程序运行在用户态。它是软件级别的,不需要特定的硬件支持。
1.2 常见的信号:(用kill -l 和 man 7 signal 命令可查看)
a)SIGINT : CTRL+C , 2
b)SIGQUIT: CTRL+\ , 3
c) SIGKILL: kill -9 pid 命令 ,9
d)SIGPIPE: 与关闭的TCP连接有关,13
e)SIGCHLD: 子进程消亡,出现僵尸进程,17
f)SIGALARM: alarm 函数,14
1.3 关于信号的术语:
a)发送信号;
b)接收信号。
1.4 对信号的处理方式有三种(这里注意,杀死一个进程的方法是,kill -9 pid 或者 pkill filaname)
a)自行编写handler
b)忽略
c)采用系统默认的行为
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * 处理 ctrl + C 信号 * 忽略 ctrl + \ 信号 */ void handler(int signum){ printf(" catch Ctrl + C\n"); } int main(int argc, const char *argv[]) { if(signal(SIGINT, handler) == SIG_ERR){ ERR_EXIT("signal"); } if(signal(SIGQUIT, SIG_IGN) == SIG_ERR){ //ctrl + \ ERR_EXIT("signal"); } for(;;) pause(); //暂停程序直到收到信号 return 0; }
2. 发送和接收信号
2.1 发送信号的方法:
a)kill -signum pid 命令:向pid进程发送signum信号,例如,kill -9 pid 就是向某进程发送SIGKILL信号杀死进程,kill -2 pid就是向某个进程发送CTRL + C 信号,使进程终止;
b)使用系统调用kill 函数;
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * kill 函数 向一个进程发送某个信号 */ void handler(int signum){ printf(" catch Ctrl + C\n"); } int main(int argc, const char *argv[]) { if(signal(SIGINT, handler) == SIG_ERR){ ERR_EXIT("signal"); } sleep(3); //睡三秒后向本进程发送 SIGINT 信号 if(kill(getpid(), SIGINT) == -1){ ERR_EXIT("kill"); } for(;;) pause(); //暂停程序直到收到信号 return 0; }
c)alarm 函数,给自己发送一个SIGALRM 信号,注意每个进程只能有一个alarm计时器,新的会取代旧的。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * alarm 函数 给自己发送 SIGALAM 函数 */ void handler(int signum){ printf(" catch sigalrm\n"); } int main(int argc, const char *argv[]) { if(signal(SIGALRM, handler) == SIG_ERR){ ERR_EXIT("signal"); } alarm(3); for(;;) pause(); //暂停程序直到收到信号 return 0; }
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * alarm 计时器 */ void handler(int signum){ static int beeps = 0; printf("beep\n"); if(++beeps < 5){ alarm(1); } else{ printf("boom\n"); exit(EXIT_SUCCESS); } } int main(int argc, const char *argv[]) { if(signal(SIGALRM, handler) == SIG_ERR){ ERR_EXIT("signal"); } alarm(3); for(;;) pause(); return 0; }
2.2 sleep函数可以被信号中断。
2.2.1 如本例,程序先睡10s,若在这10s内按了ctrl + C 即向程序发送了该信号,信号处理完毕后,此时程序不会再重新执行sleep,而是从sleep 的下一行开始执行。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * sleep 函数能被 信号打断 */ void handler(int signum){ printf(" catch Ctrl + C\n"); } int main(int argc, const char *argv[]) { if(signal(SIGINT, handler) == SIG_ERR){ ERR_EXIT("signal"); } sleep(10);
printf("after handler\n); return 0; }
2.2.2 因为sleep函数返回剩余还没有睡眠的秒数,因此,可以使用一个循环,使得程序被打断后继续睡眠,直到结束。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * sleep 返回剩余没有睡眠的秒数 * 因此可以使用一个循环 来继续被打断的sleep */ void handler(int signum){ printf(" catch Ctrl + C\n"); } int main(int argc, const char *argv[]) { if(signal(SIGINT, handler) == SIG_ERR){ ERR_EXIT("signal"); } //sleep(10); //sleep 期间若有信号 直接结束 int n = 10; do{ n = sleep(n); printf("interupted left %d seconds\n", n); }while(n > 0); return 0; }
2.3 已经发送但是还未被接收的信号称为待处理信号。系统采用一个向量表示待处理信号的集合,所以每种类型的信号最多只有一个待处理信号。一个待处理信号最多只能被接收一次。
3.信号处理
3.1 信号处理带来的一些问题:
a)待处理信号会被阻塞:如果程序正在处理 SIGINT,那么此时到达一个SIGINT,会被阻塞,成为待处理信号,一直到上一个SIGINT处理程序返回。
b)待处理信号不会排队等待:每种类型的信号最后只有一个待处理信号,如果程序中已经有一个SIGINT待处理信号,此时到达一个SIGINT,会被丢弃。
c)系统调用可以被handler打断,即EINTR,例如,read(fd...)是一个慢速的系统调用,此时如果有信号到来,会切换到该信号的handler中,当从该handler返回的时候,read也返回 -1, errno 置为 EINTR。
3.2 例子。父进程产生 10 个子进程,每个子进程执行完一些操作后自动退出,此时父进程收到 10 个 SIGCHLD 信号(这10个信号几乎是同一时间发出的)。然后父进程开始处理第一个 SIGCHLD 的handler,里面sleep 2s,在这期间,第二个信号到来,被阻塞为待处理信号,后面的信号都被简单的丢弃。因此,本程序后面的子进程都变成了僵尸进程。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #define N 10 #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * 待处理信号只能有一个 * 其他的 会被丢弃 */ //处理 sigchld 信号,防止产生僵尸进程 void handler(int signum){ pid_t pid; if((pid = waitpid(-1, NULL, 0)) < 0){ ERR_EXIT("WAITPID"); } printf("handler process child %d\n", pid); sleep(2); //故意阻塞别的SIGCHLD 信号,只有一个SIGCHLD信号处理完毕才能处理下一个 } int main(int argc, const char *argv[]) { if(signal(SIGCHLD, handler) == SIG_ERR){ ERR_EXIT("signal"); } pid_t pid; int i; for(i = 0; i < N; i++){ if((pid = fork()) < 0){ ERR_EXIT("fork"); } else if(pid == 0){ printf(" child pid: %d\n", getpid()); sleep(2); exit(EXIT_SUCCESS); } } for(;;) pause(); //暂停程序直到收到信号 return 0; }
3.3 解决以上问题的关键思想:
a)信号对应的向量置为 1,说明至少存在一个该信号;
b)当处理 SIGCHLD 信号时,应该尽可能多处理子进程。
3.4 例子。对上例的改进。程序分析:
a)10个子进程几乎同时消亡,发出10个 SIGCHLD 信号;
b)父进程接收到一个SIGCHLD,执行waitpid,回收第一个子进程,然后睡眠2 s;
c)在这 2 s 内,第二个 SIGCHLG 变成待处理,其余丢失;
d)睡眠结束,此时存在9个僵尸进程,然后执行 9+1 次while 循环,全部处理完毕;
e)以上都属于第一个 SIGCHLD 的handler;
f)处理第二个 SIGCHLD,此时没有子进程,waitpid 失败,退出。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> #define N 3 #define ERR_EXIT(m) \ do { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) /* * 多处理 sigchld 信号 */ void handler(int signum){ pid_t pid; while((pid = waitpid(-1, NULL, 0)) > 0){ printf("handler process child %d\n", pid); sleep(2); } printf("end\n"); } int main(int argc, const char *argv[]) { if(signal(SIGCHLD, handler) == SIG_ERR){ ERR_EXIT("signal"); } pid_t pid; int i; for(i = 0; i < N; i++){ if((pid = fork()) < 0){ ERR_EXIT("fork"); } else if(pid == 0){ printf(" child pid: %d\n", getpid()); sleep(2); exit(EXIT_SUCCESS); } } int n; char buf[1024] = {0}; //阻塞在这里 //Linux 自动重启系统调用 if((n = read(STDIN_FILENO, buf, 1024)) < 0){ ERR_EXIT("read"); } printf("parents process input\n"); return 0; }
3.5 处理僵尸进程:
a)在父进程中编写同步代码,可以按照顺序或者异步的方式逐个回收子进程。这种方法的缺陷是需要准确把握子进程的消亡时机。
b)处理SIGCHLD信号,这个方法提供了一种异步处理能力。其中要注意:处理SIGCHLD信号时,应该使用while循环尽可能多去处理子进程,这里主要是防止信号的阻塞和丢失问题。