信号:
信号是UNIX系统响应某些状况而产生的事件,进程在接收到信号时会采取相应的行动。
信号是因为某些错误条件而产生的,比如内存段冲突、浮点处理器错误或者非法指令等。
信号是在软件层次上对中断的一种模拟,所以通常把它称为是软中断。
信号和中断的区别:
相似点:
采用了相同的异步通信方式。
当检测出有信号或者中断请求时,都暂停正在执行的程序而转去执行相应的处理程序。
都在处理完毕后返回到原来的断点。
对信号和中断都可以进行屏蔽。
区别:
中断有优先级,而信号没有优先级,所有的信号都是平等的。
信号处理程序都是在用户态下运行的,而中断处理程序是在核心态下运行。
中断响应是及时的,而信号响应通常都有较大的延迟。
常用的信号如下所示:
进程对信号的三种响应如下:
man 7 signal可以查看信号的默认动作和信号的含义。
signal用于安装一个信号处理函数,原型如下:
__sighandler_t signal(int signum, __sighandler_t handler)
signal是一个带signum和handler两个参数的函数,准备捕捉或者屏蔽的信号由参数signum给出,接收到指定信号时将要调用的函数由handler给出。
handler这个函数必须有一个int型参数(即接收到的信号代码),它本身的类型是void。
handler也可以是下面两个特殊值:
SIG_IGN 忽略该信号
SIG_DFL 恢复默认行为
示例程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <signal.h> 4 5 6 void handler(int num) 7 { 8 printf("recv signal num = %d ", num); 9 } 10 11 12 int main() 13 { 14 pid_t pid; 15 16 signal(SIGCHLD, SIG_IGN); 17 18 pid = fork(); 19 20 if(pid == -1) 21 { 22 perror("fork error"); 23 exit(0); 24 } 25 26 if(pid == 0) 27 { 28 printf("child ... "); 29 exit(0); 30 } 31 while(1) 32 { 33 pause(); 34 } 35 36 return 0; 37 }
程序中,我们注册信号时,表示父进程忽略子进程的退出信号,因此,执行结果如下:
默认情况下(当我们不使用signal信号时),父进程会在退出的时候给子进程收尸(前提是子进程先死),可用如下程序证明:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 7 void handler(int num) 8 { 9 printf("recv signal num = %d ", num); 10 } 11 12 13 int main() 14 { 15 pid_t pid; 16 17 //signal(SIGCHLD, SIG_IGN); 18 19 pid = fork(); 20 21 if(pid == -1) 22 { 23 perror("fork error"); 24 exit(0); 25 } 26 27 if(pid == 0) 28 { 29 printf("child ... "); 30 exit(0); 31 } 32 33 sleep(5); 34 //while(1) 35 //{ 36 // pause(); 37 //} 38 39 return 0; 40 }
我们让父进程睡眠5秒,保证子进程先死,当父进程还在睡眠时,我们在另一个中断执行ps -ef,结果如下:
可以看到,父进程睡眠期间,子进程已经死了,而且处于僵尸状态,但是当父进程也结束时,子进程的僵尸状态消失了,说明父进程给它收尸了。当把17行的注释打开时,就是告诉内核,子进程死的时候让内核给他收尸,因此,我们看不到子进程处于僵尸状态的现象了(内核收尸太快了)。
下面我们演示注册一个真正的信号处理函数和恢复默认行为的程序:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 7 void handler(int num) 8 { 9 printf("recv signal num = %d ", num); 10 } 11 12 13 int main() 14 { 15 pid_t pid; 16 char c; 17 18 signal(SIGINT, handler); 19 20 while( (c = getchar()) != 'a') 21 { 22 pause(); 23 } 24 25 signal(SIGINT, SIG_DFL); 26 while(1) 27 { 28 pause(); 29 } 30 31 return 0; 32 }
SIGINT代表ctrl+c信号,18行将这个信号的处理函数注册为handler,当程序停在20行时,我们按下ctrl+c,handler会得到执行。 输入a使程序执行到26行,这时候SIGINT信号的行为恢复到了默认行为(25行中的SIG_DFL表示恢复默认行为),我们再按下ctrl+c,程序直接退出了(这就是默认行为)。执行现象如下:
signal函数执行成功时,返回默认的处理函数,执行失败时返回SIG_ERR。我们将上述程序改成以下方式:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 void handler(int num) 7 { 8 printf("recv signal num = %d ", num); 9 } 10 11 int main() 12 { 13 pid_t pid; 14 char c; 15 16 __sighandler_t old = signal(SIGINT, handler); 17 18 if(SIG_ERR == old) 19 { 20 perror("signal error"); 21 } 22 23 while( (c = getchar()) != 'a') 24 { 25 pause(); 26 } 27 28 signal(SIGINT, old); 29 printf("huan yuan "); 30 while(1) 31 { 32 pause(); 33 } 34 35 return 0; 36 }
使用signal注册信号处理函数时,将默认的处理函数返回到old中,并在下面进行了错误处理,28行恢复默认行为,执行结果如下:
信号的分类:可靠与不可靠信号,实时与非实时信号。实时信号都是可靠信号,非实时信号都是不可靠信号。1-31号都是不可靠信号。不可靠信号就是向应用程序发送了多次信号,应用程序可能只接收到了一次。可靠信号就是向应用程序发几次信号都能保证全接收到。早期unix系统每接收到一个信号就将处理程序恢复到默认行为。现在的linux中的不可靠信号主要指信号可能会丢失。
信号发送函数kill和raise,如下所示:
kill可以向指定进程发送信号,raise向自身发送信号。 kill是示例程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 void my_handler(int num) 7 { 8 if(SIGINT == num) 9 { 10 printf("recv signal SIGINT "); 11 } 12 else if(SIGUSR1 == num) 13 { 14 printf("recv signal SIGUSR1 "); 15 } 16 else 17 { 18 printf("recv signal num = %d ", num); 19 } 20 } 21 22 int main() 23 { 24 pid_t pid; 25 26 if(signal(SIGINT, my_handler) == SIG_ERR) 27 { 28 perror("signal error"); 29 } 30 31 if(signal(SIGUSR1, my_handler) == SIG_ERR) 32 { 33 perror("signal error"); 34 } 35 36 pid = fork(); 37 38 if(pid == -1) 39 { 40 perror("fork error"); 41 exit(0); 42 } 43 44 if(pid == 0) 45 { 46 pid = getppid(); 47 kill(pid, SIGUSR1); 48 exit(0); 49 } 50 51 int nsleep = 5; 52 53 do 54 { 55 printf("parent process begin sleep "); 56 nsleep = sleep(nsleep); 57 printf("parent process end sleep "); 58 }while(nsleep > 0); 59 60 return 0; 61 }
子进程通过kill向父进程发信号,父进程在睡眠,当收到信号时就去执行信号处理函数,执行完之后,发现nsleep不为0,也即没有睡够指定的时间,因此再次进入睡眠,直到睡完为止。执行结果如下:
小知识:
getpgrp()获取进程组id, kill(pid, SIGINT)向进程组发送SIGINT。父进程注册的信号处理函数也会复制给子进程(在fork之前注册信号)。sleep函数返回值是剩余的秒数。sleep能被信号打断,是可中断睡眠,处理信号函数返回以后就不再睡眠了,继续向下执行。wait使进程进入的睡眠也是可中断睡眠。
pause函数:
将进程置为可中断睡眠状态,然后它调用内核函数schedule(),使linux进程调度器找到另一个进程来运行。
pause使调用者进程挂起,直到一个信号被捕获。
alarm函数:
设置一个闹钟,延迟发送信号,告诉linux内核n秒钟以后,给本进程发送SIGALRM信号。示例程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 7 void my_handler(int num) 8 { 9 printf("recv signal num = %d ", num); 10 } 11 12 int main() 13 { 14 if(signal(SIGALRM, my_handler) == SIG_ERR) 15 { 16 perror("signal error"); 17 exit(0); 18 } 19 20 alarm(3); 21 22 pause(); 23 printf("return from pause "); 24 25 return 0; 26 }
执行结果如下所示:
可重入与不可重入:
示例程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 typedef struct _Teacher 7 { 8 int age; 9 int num; 10 }Teacher; 11 12 Teacher g_t; 13 14 void printGlobalTeacher() 15 { 16 printf("g_t.age = %d ", g_t.age); 17 printf("g_t.num = %d ", g_t.num); 18 } 19 20 void my_handler(int num) 21 { 22 printf("recv signal num = %d ", num); 23 printGlobalTeacher(); 24 alarm(1); 25 } 26 27 int main() 28 { 29 Teacher t1,t2; 30 t1.age = 30; 31 t1.num = 30; 32 t2.age = 40; 33 t2.num = 40; 34 35 if(signal(SIGALRM, my_handler) == SIG_ERR) 36 { 37 perror("signal error"); 38 exit(0); 39 } 40 41 alarm(1); 42 43 while(1) 44 { 45 g_t = t1; 46 g_t = t2; 47 //printf("return from pause "); 48 } 49 return 0; 50 }
上述程序中在信号处理函数中调用printGlobalTeacher函数,这个函数是不可重入的函数,因为函数内部访问了全局变量g_t, 因此打印结果有可能出错。执行结果如下:
可以看到出现了age和num不相等的情况,这就是出错了。