学习目标
推荐使用sigaction,因为不同平台是signal可能不一样
信号的特点
简单,不能携带大量信息,满足特定条件才会发生。信号也叫软中断,有可能会有延迟。
信号的实现机制
信号实际上是由内核发送,内核来处理收到的信号。收到信号的进程,必须对信号做出处理(忽略,捕获,默认动作都行)
产生信号
信号状态
- 产生
- 递达——信号到达并且处理完
- 未决——信号被阻塞
信号的默认处理方式
- 忽略
- 默认动作
- 捕获
信号的四要素
- 编号
- 事件
- 名称
- 默认处理动作
默认处理动作有五个(linux 输入 man 7 signal 即可查看),如图所示:
注意:9号,19号信号,不能捕捉,不能忽略,不能阻塞。
阻塞信号集和未决信号集
处理了就把二进制位设置为0,未处理就设置为1.
阻塞信号集的作用是影响未决信号集,相当于给他挡了一堵墙。
如果我们把2号信号设置成阻塞(即在阻塞信号集的对应位置设为1),那么来一个2号信号,则未信号集的对应值置为1,什么时候阻塞信号集中的对应位置变成0了,什么时候未决信号集才能去处理之前被阻塞的那个信号。
信号的产生
系统api产生信号
kill函数
int kill(pid_t pid, int sig);
- pid > 0 ,要发送进程ID
- pid = 0, 代表当前调用进程中内所有进程
- pid = -1, 代表有权限发送的所有进场
- pid < 0,代表 -pid 对应的组内所有进程
- sig 对应的信号
练习,让3号子进程杀死父进程
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<signal.h>
int main(){
int i;
for(i=0; i<5; i++){
pid_t pid=fork();
if(pid==0){
break;
}
}
//我们使3号子进程杀死父进程
if(i==2){
printf("I will kill my father after 5 second!\n");
sleep(5);
kill(getppid(),SIGKILL);
}
if(i==5){
while(1){
printf("I am father, very happy!\n");
sleep(1);
}
}
return 0;
}
运行结果
再来一个父进程把3号子进程杀死
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
int main(){
int i=0;
pid_t pid3, pid;
for(i=0; i<5; i++){
pid=fork();
if(pid==0) break;
if(i==2) pid3=pid;
}
if(i<5){
while(1){
printf("I am child, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(3);
}
}
else if(i==5){
printf("I am father,pid=%d, I will kill xiao san pid3=%d\n", getpid(), pid3);
sleep(5);
kill(pid3, SIGKILL);
while(1) sleep(1);
}
return 0;
}
运行结果:pid15537不见了
raise——给自己发送信号
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
int main(){
printf("I will die!\n");
sleep(2);
raise(SIGKILL);//kill(getpid(), sig)
return 0;
}
运行结果:
实际上调用的是kill
abort——直接给自己发送异常信号,直接退出
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
printf("I will die!\n");
sleep(2);
abort();//kill(getpid(), sig)
return 0;
}
运行结果:
时钟信号
alarm
- 定时给自己发送SIGALRM
- 几秒后发送信号
- 返回值——上次闹钟剩余的秒数
- 特别的,如果传入参数秒为0,代表取消闹钟
#include<stdio.h>
#include<unistd.h>
int main(){
alarm(6);
while(1){
printf("lai da wo!\n");
sleep(1);
}
return 0;
}
运行结果
我们来看alarm函数的返回值
#include<stdio.h>
#include<unistd.h>
int main(){
int ret=0;
ret=alarm(6);
printf("ret=%d\n", ret);
sleep(2);
ret=alarm(5);
printf("ret=%d\n", ret);
while(1){
printf("lai da wo!\n");
sleep(1);
}
return 0;
}
运行结果
setitimer函数——周期性的发送信号
结构体——结构体中嵌套了两个结构体
函数原型
int settime(int which, const struct itimerval *new_value, struct itimerval *old_value);)
- which
- ITIMER_REAL 自然定时法 SIGALRM
- ITIMER_VIRTUAL 计算进程执行时间 SIGVTALRM
- ITIMER_PROF 程序执行时间+调度时间 ITIMER_VIRTUAL
- new_value 要设置的闹钟时间
- old_value 原闹钟时间
setitimer实现alarm功能
#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>
int main(){
//setitimer是一个结构体中嵌套了两个结构体
//两个结构体分别代表:周期性的时间设置,下次的闹钟时间
//每个结构体里。,第一个参数是秒,第二个参数是微妙
//这里定义的意思就是:三秒之后发送SIGALRM信号
struct itimerval myit={{0,0},{3,0}};
setitimer(ITIMER_REAL, &myit, NULL);
while(1){
printf("Who can kill me!\n");
sleep(1);
}
return 0;
}
运行结果
测试周期性发送信号功能
周期性发送SIGALRM信号杀死进程,进程利用catch_sig函数来捕获SIGALRM信号
#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>
#include<signal.h>
void catch_sig(int num){
printf("cat %d sig\n", num);
}
int main(){
signal(SIGALRM, catch_sig);
//第一次等待五秒,之后每隔三秒
struct itimerval myit={{3,0},{5,0}};
setitimer(ITIMER_REAL, &myit, NULL);
while(1){
printf("Who can kill me!\n");
sleep(1);
}
return 0;
}
运行结果
setitimer实现alarm
#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
unsigned int myalarm(unsigned int seconds){
struct itimerval oldit,myit={{0,0},{0,0}};
//second秒之后,给我发送一个SIGALRM信号
myit.it_value.tv_sec=seconds;
//old_value 原闹钟时间
setitimer(ITIMER_REAL, &myit, &oldit);
//打印秒和微秒
printf("tv_sec=%ld, tv_microsec=%ld\n",
oldit.it_value.tv_sec, oldit.it_value.tv_usec);
return oldit.it_value.tv_sec;
}
int main(){
int ret=0;
ret=myalarm(5);
printf("ret=%d\n", ret);
sleep(3);
ret=myalarm(3);
printf("ret=%d\n", ret);
while(1){
printf("lai da wo!\n");
sleep(1);
}
return 0;
}
信号集的操作函数
输入可查看信号集出来函数的man手册
man sigemptyset
- int sigemptyset(sigset_t *set);——清空信号集
- int sigfillset(sigset_t *set);——填充信号集
- int sigaddset(sigset_t *set, int signum);——添加某个信号到信号集
- int sigdelset(sigset_t *set, int signum);——从集合中删除某个信号
- int sigismember(const sigset_t *set, int signum——判断是否为集合里的成员——返回 1 代表在集合中
- int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);——用来设置或者解除阻塞信号集
- int sigpending(sigset_t *set);——获取未决信号集
输入命令,查看另一个信号集函数——sigprocmask——用来设置或者解除阻塞信号
man sigprocmask
- how
- SIG_BLOCK 设置阻塞
- SIG_UNBLOCK 解除阻塞
- SIG_SETMASK 设置set为新的阻塞信号集
- set 传入的信号集
- oldset 旧信号集,传出
输入命令,查看另一个信号集函数——sigpending——用来获取未决信号集
man sigpending
- set 传出参数,当前的未决信号集
把所有常规信号的未决信号集打到屏幕上
将2号信号(ctrl+c)放在阻塞信号集上,这样该信号在未决信号集上会进行保留
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main(){
sigset_t pend, sigproc;
//设置阻塞信号,等待按键产生信号
//先清空
sigemptyset(&sigproc);
//将2号信号(ctrl+c)放在集合里
sigaddset(&sigproc, SIGINT);
//设置阻塞信号集
sigprocmask(SIG_BLOCK, &sigproc, NULL);
//循环取未决信号集中的信号
while(1){
//循环读取未决信号,打印
sigpending(&pend);
for(int i=0; i<32; i++){
//存在信号集中
if(sigismember(&pend, i)==1){
printf("1");
}else{
printf("0");
}
}
printf("\n");
sleep(1);
}
return 0;
}
信号捕捉
防止进程意外死亡
输入查看函数原型
man signal
- typedef void (*sighandler_t)(int);
- sighandler_t signal(int signum, sighandler_t handler);
- signum 要捕捉的信号
- handler 要执行的捕捉函数指针,函数应该声明 void func(int);//函数名可变
输入查看函数原型
man sigaction
- int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- signum 捕捉的信号
- act 传入的动作
- oldact 原动作
捕捉我们定时给自己发送的14号自杀信号
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/time.h>
//定义捕捉函数
void catch_sig(int num){
printf("catch %d sig\n", num);
}
int main(){
//注册捕捉函数
struct sigaction act;
//说明为你使用的是sigaction结构体中的第一个捕捉函数
act.sa_flags=0;
//那个捕捉函数的函数指针指向我们上面自己写的捕捉函数catch_sig
act.sa_handler=catch_sig;
//清空信号集
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
//setitimer 5秒之后每隔3秒来一次信号
struct itimerval myit={{3,0},{5,0}};
setitimer(ITIMER_REAL, &myit, NULL);
while(1){
printf("Who can kill me!\n");
sleep(1);
}
return 0;
}
信号捕捉的特性
内核实现信号捕捉过程
利用SIGCHLD回收子进程
子进程在暂停或者退出的时候会发送SIGCHLD信号,我们可以通过捕捉SIGCHLD信号来回收子进程。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void catch_sig(int num){
pid_t pid=waitpid(-1, NULL, WNOHANG);
if(pid>0){
printf("wait child %d ok\n", pid);
}
}
int main(){
int i=0;
pid_t pid;
for(i=0; i<10; i++){
pid=fork();
if(pid==0){
break;
}
}
if(i==10){
//father
struct sigaction act;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=catch_sig;
sigaction(SIGCHLD, &act, NULL);
while(1){
sleep(1);
}
}else if(i<10){
printf("I am %d child, pid=%d\n", i, getpid());
sleep(i);
}
return 0;
}
上面的程序是有问题的,如果我十个孩子不是依次死去,而是一起死,就会出现僵尸进程,因为信号不排队的。
改进一下上面的代码,让他不出僵尸进程。我们即将信号收集函数catch_sig里面用一个while循环来循环处理pid收集到的各个SIGCHLD信号。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void catch_sig(int num){
pid_t pid;
//此时,如果多个子进程一起死,pid对获得多个信号
while((pid=waitpid(-1, NULL, WNOHANG))>0) {
if(pid>0){
printf("wait child %d ok\n", pid);
}
}
}
int main(){
int i=0;
pid_t pid;
for(i=0; i<10; i++){
pid=fork();
if(pid==0){
break;
}
}
if(i==10){
//father
struct sigaction act;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=catch_sig;
sigaction(SIGCHLD, &act, NULL);
while(1){
sleep(1);
}
}else if(i<10){
printf("I am %d child, pid=%d\n", i, getpid());
}
return 0;
}
如果注册sigaction之前,子进程就死了,那么还是会产生僵尸进程。如何避免?
子进程死之前,如果main函数还没注册sigaction,就把SIGCHLD信号屏蔽即可。屏蔽信号的工作,应该在创建子进程之前就开始去做,如此可以避免极端情况下出现差错。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void catch_sig(int num)
{
pid_t wpid ;
while( (wpid = waitpid(-1,NULL,WNOHANG)) > 0 ){
printf("wait child %d ok\n",wpid);
}
}
int main()
{
int i =0;
pid_t pid ;
//在创建子进程之前屏蔽SIGCHLD信号
sigset_t myset,oldset;
sigemptyset(&myset);
sigaddset(&myset,SIGCHLD);
//oldset 保留现场,设置了SIGCHLD到阻塞信号集
sigprocmask(SIG_BLOCK,&myset,&oldset);
for(i = 0 ; i < 10 ; i ++){
pid =fork();
if(pid == 0 ){
break;
}
}
if(i == 10 ){
//parent
//模拟注册晚于子进程死亡
sleep(2);
struct sigaction act ;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = catch_sig;
sigaction(SIGCHLD,&act,NULL);
//解除屏蔽现场
sigprocmask(SIG_SETMASK,&oldset,NULL);
while(1){
sleep(1);
}
}else if(i < 10){
printf("I am %d child,pid = %d\n",i,getpid());
}
return 0;
}
作业:利用SIGUSR1和SIGUSR2在父子进程之间进行消息传递,实现父子进程交替报数(间隔1s)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include <signal.h>
#include<sys/types.h>
pid_t pid;
int count =0;
int flag = 0;
void cat_sig_father(int num)
{
sleep(1);
//父进程将自己的flag改成1
flag = 1;
printf("%d\n",count);
count +=2;
// kill(pid,SIGUSR2);
}
void cat_sig_child(int num)
{
sleep(1);
//子进程中的flag变成1
flag = 1;
printf("%d\n",count);
//因为交替数数,所以cout+2
count +=2;
// kill(getppid(),SIGUSR1);
}
int main(int argc,char *argv[]){
pid = fork();
if(pid == 0) {
//son
count=1;
//捕捉SIGUSR2信号,捕捉到之后,执行函数cat_sig_child
signal(SIGUSR2,cat_sig_child);
//获取父进程id
pid_t ppid = getppid();
while(1){
//flag的值为1时,向父进程发送SIGUSR1信号
if(flag == 1){
kill(ppid,SIGUSR1);
flag = 0;
}
}
}
else{
//父进程睡了10微秒
usleep(10);
//count的值父子进程不共享
count=2;
//注册SIGUSR1
signal(SIGUSR1,cat_sig_father);
//给子进程发送SIGUSR2信号
kill(pid,SIGUSR2);
//printf("begin ...\n");
while(1){
if(flag == 1){
//给子进程发送SIGUSR2信号
kill(pid,SIGUSR2);
flag = 0;
}
}
}
return 0;
}
将上面函数cat_sig_father和cat_sig_child中的sleep()都注释掉之后,会发生卡死的情况。
因此,当系统负荷严重的时候,信号极不稳定
产生卡死的原因:
当子进程执行完kill函数之后,此时发生中断,flag=0这句还没执行。
while(1){
//flag的值为1时,向父进程发送SIGUSR1信号
if(flag == 1){
kill(ppid,SIGUSR1);
flag = 0;
}
}
此时父进程收到了信号,执行cat_sig_father函数,将自己的flag改成1,然后发送信号SIGUER2给子进程,父进程的flag变为0。
此时获取了cpu的子进程收到了信号,收到信号要优先处理信号,于是去执行上面的signal函数
//捕捉SIGUSR2信号,捕捉到之后,执行函数cat_sig_child
signal(SIGUSR2,cat_sig_child);
//获取父进程id
pid_t ppid = getppid();
while(1){
//flag的值为1时,向父进程发送SIGUSR1信号
if(flag == 1){
kill(ppid,SIGUSR1);
flag = 0;
}
}
而此时子进程的flag值为0,进程无法进入while循环,也就无法向父进程发送信号,此时父子进程的flag的值都为0,因此就卡死了。