进程之信号
文章目录
- 进程信号
- 中断的概念
- 信号是异步事件
- Linux信号
- 信号发生的来源
- 信号的处理方式
- signal函数
- 示例--使用signal函数对常见的信号进行捕获、执行默认操作、忽略三种方式处理
- SIGCHLD信号
- 示例--使用SIGCHLD信号来避免子进程退出
- kill函数
- raise函数
- 示例--使用kill函数给指定进程发送信号
- 示例--使用kill函数和raise函数向当前进程发送信号
- 示例--使用`kill`和`signal`函数练习
- 定时器
- alarm函数
- 示例--使用alarm函数设置一个定时器
- 示例--使用定时器每隔一个定时时间翻转引脚状态
- setitimer函数
- 示例--使用setitimer函数设置定时器
进程信号
信号是Linux系统中的一种通信机制,常见的通信机制还有文件锁、管道、消息队列、共享内存等。
中断的概念
-
硬件中断
- 在单片机中常常能看到有硬件中断,中断说白了就是一种机制,中断的存在允许当程序在运行的时候能够转而去执行别的操作,在单片机中,当发生硬件中断的时候,CPU首先会停下当前的工作,然后将寄存器等状态保存在堆栈中,然后转而去执行中断服务程序。当程序执行完以后,继续返回之前被中断的地方继续执行。
-
软件中断
- 软件中断就是通过一种信号来出发它的中断,例如这里的信号。信号作为一种软件中断它的原理是,当一个进程给另外一个进程发送信号的时候,另外一个进程停下当前正在做的事情,然后转而去处理信号函数。但是这种是通过信号机制来中断当前正在执行的程序,而非是由于硬件中断所引起的,所以信号是一种软件中断。
信号是异步事件
- 异步事件:异步事件就是说当进程1发送信号给进程2以后不管去管对方到底有没有接收到,进程1都要往下执行,这种异步操作能够通过减少等待时间来提高程序的响应性和性能。
- 同步事件:以这里的信号为例,如果是同步事件的话,当进程1给进程2发信号以后必须收到来自进程2的回复进程1才能够继续往下执行。
异步事件和同步事件对比:经过对比发现异步事件比同步实践的执行效率更好,但是可能执行精度上较低于同步事件。同步事件虽然资源利用效率低下,但是能够保持较高的精度,一旦出错以及停止执行然后进行相应的处理。所以对于两种方式的选择要进行合理的选择才能达到预期的效果。
Linux信号
在Linux系统中的信号都是继承了Unix系统的信号,在Linux系统中可以使用kill -l
来查看系统中的所有信号。
信号发生的来源
- 硬件来源:当按下键盘的
Ctrl+C
或Ctrl+Z
的时候会产生SIGINT
和SIGTSTP
信号,这些信号都是由硬件驱动程序产生,当内核的驱动程序捕捉到硬件按下对应的按键以后从而触发对应的信号。- 软件来源:最常用发送信号的系统函数是
kill()
,raise()
,alarm()
,setitimer()
等函数,软件来源还包括一些非法运算等操作(如除零操作、段错误),软件设置条件(如:gdb
调试),信号由内核产生。
- 软件来源:最常用发送信号的系统函数是
信号的处理方式
-
忽略信号
SIGKILL
和SIGSTOP
永远不能忽略- 进程启动时
SIGUSR1
和SIGUSR2
两个信号被忽略 - 这里的忽略信号需要使用一个宏
SIG_IGN
来将指定的信号忽略
-
执行默认操作
- 每个信号都有默认动作,大部分信号的默认动作是终止进程
- 执行默认操作使用
SIG_DFL
宏来将指定的信号执行默认操作
-
捕获信号
- 使用
signal()
函数来将指定的信号捕获,然后转而去执行自己定义的信号处理函数 SIGKILL
和SIGSTOP
不能被捕获
SIGKILL
和SIGSTOP
信号不能被忽略或捕获的原因是:它们两个相当于一种保护机制,当Ctrl+C(SIGINT)
和Ctrl+Z(SIGTSTP)
等信号被捕获转而去执行用户自定义的函数时,必须保证有一个信号能够将此进程杀死或者停止,所以这里SIGKILL
和SIGSTOP
的底层就写好了它们不能被忽略也不能被捕获。 - 使用
signal函数
上边说的几种信号的处理方式可以通过Linux系统提供的一个函数来实现:
#include <signal.h>void (*signal(int sig, void (*func)(int)))(int);//功能:向内核注册信号处理函数,当发生对应的信号时做出相应的操作
//参数1:要捕获的信号
//参数2:函数指针,指向用户自定义的信号处理函数
//返回值:如果成功执行,将返回一个指向先前信号处理函数的指针。若执行失败,则返回值为SIG_ERR
这里对signal
函数的原型进行详解:
signal
函数的第一个参数sig
是要捕捉的信号,例如SIGINT SIGTSTP
等,然后对捕捉到的信号进行相应的操作,这个操作函数就是它的第二个参数。这里的sig
可以用编号来表示也可以用它具体的信号来表示(如SIGINT
信号对应的编号是2)signal
函数的第二个参数void (*func)(int)
是一个函数指针,它是使用typedef
定义的类型别名,这个函数指针指向了一个函数,该函数是一个无返回值,具有一个整型参数的函数。- 它的第二个参数还可以是两个宏:
SIG_ING
和SIG_DFL
分别表示忽略信号和执行该信号的默认操作 signal
函数的返回值是一个函数指针,它指向了一个函数,该函数是一个无返回值,具有一个整型参数的函数。实际上它的返回值就是信号处理函数。
示例–使用signal函数对常见的信号进行捕获、执行默认操作、忽略三种方式处理
#include "header.h"void sig_handler(int signum)
{switch(signum){case 1:printf("process %d catches signal is SIGHUP\n",getpid());break;case 2:printf("process %d catches signal is SIGINT\n",getpid());break;case 9:printf("process %d catches signal is SIGKILL\n",getpid());break;case 10:printf("process %d catches signal is SIGUSR1\n",getpid());break;case 12:printf("process %d catches signal is SIGUSR2\n",getpid());break;case 19:printf("process %d catches signal is SIGSTOP\n",getpid());break;default:printf("there is no such signal\n");}
}int main(void)
{printf("process pid is %d\n",getpid());//向内核登记信号处理函数以及需要捕获的信号if(signal(SIGINT, sig_handler) == SIG_ERR){perror("signal SIGINT error");}if(signal(SIGHUP, sig_handler) == SIG_ERR){perror("signal SIGHUP error");}if(signal(SIGUSR1, sig_handler) == SIG_ERR){perror("signal SIGUSR1 error");}if(signal(SIGUSR2, sig_handler) == SIG_ERR){perror("signal SIGUSR2 error");}if(signal(SIGKILL, sig_handler) == SIG_ERR){perror("signal SIGKILL error");}if(signal(SIGSTOP, sig_handler) == SIG_ERR){perror("signal SIGSTOP error");}while(1) sleep(1);return 0;
}
通过编译执行,发现signal
函数将SIGHUP
和SIGINT
信号捕获,而且执行了用户自定义的函数,这里由于SIGHUP
函数不能通过键盘等硬件方式去向内核发送信号,所以采用kill
指令去发送信号。这里使用kill
给进程发送编号为3的信号,程序退出,由此也验证了大部分信号的默认处理机制是将程序结束。
当使用signal
函数去捕获SIGKILL
和SIGSTOP
信号的时候,可以看到这里在运行的时候就直接报错了,显示非法的参数。并且使用SIGKILL
和SIGSTOP
信号还能杀死和停止进程。SIGUSR1
和SIGUSR2
信号虽然在进程启动的时候被忽略了,但是可以使用signal
函数对它们进行捕获执行相应的函数。
如果这里将signal
函数的第二个参数设置为SIG_IGN
或SIG_DFL
的话,它们分别会将捕获的信号忽略和执行默认操作,例如这里将SIGINT
信号设置为默认的操作,它就会将正在运行的进程中断。如果将signal
函数设置为SIG_IGN
它的处理是将信号忽略不做任何处理。
SIGCHLD信号
前边进程那一章有讲过当子进程优于父进程退出的时候,子进程的资源无法完全释放,在内核中还留有进程表项。所以当时为了避免子进程成为僵尸进程共有三种方法:
- 在父进程中使用
wait
函数等待子进程退出,然后让父进程通知内核去回收子进程的进程表项; - 当运行起来的时候,将父进程杀死。使子进程变成一个孤儿进程然后由
init
或systemd
进程收留,最后由它们去释放子进程的资源; - 当子进程退出的时候会产生
SIGCHLD
信号,此时如果父进程捕获到子进程发送此信号就表明子进程退出了,调用wait
函数去回收子进程的资源。
通过前边的案例不难发现当父进程调用wait
函数以后父进程就处于阻塞状态了,所以父进程一直在等待子进程退出,中间是不能做任何事情的,这其实是资源的浪费。所以这里介绍一些应该怎么使用SIGCHLD
信号来避免子进程变成僵尸进程,通过在使用SIGCHLD
信号的时候父进程也不是处于阻塞状态,父进程可以干自己的事,相比之前直接使用wait
函数等待子进程退出效果更好一点。
示例–使用SIGCHLD信号来避免子进程退出
#include "header.h"void sig_handler(int signum)
{printf("process %d receive a signal and the number is %d\n",getpid(),signum);wait(NULL);
}void count_func(int num)
{int i;for(i=0;i<num;i++){printf("process: %d i:%d\n",getpid(),i);sleep(1);}
}int main(void)
{//向内核登记捕获SIGCHLD信号,一旦产生这个信号就去执行相应的信号处理函数if(signal(SIGCHLD, sig_handler) == SIG_ERR){perror("signal SIGCHLD error");}pid_t pid;if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid == 0){printf("this is child process and the pid is %d\n",getpid());count_func(10);}else{printf("this is parent process and the pid is %d\n",getpid());count_func(40);}return 0;
}
通过编译执行可以发现,在子进程退出的时候会发送一个SIGCHLD
信号,在此时调用wait
函数可以将子进程的资源进行回收,使用ps
指令去查看子进程状态也没有变成僵尸进程,说明子进程成功被回收。
kill函数
在上边的终端里使用kill
指令发送信号给特定的进程,在Linux系统里又一个kill
函数和kill
指令的功能是一样的。
#include <signal.h>int kill(pid_t pid, int sig);//功能:向指定进程发送信号
//参数1:指定进程的PID号
//参数2:向指定进程发送的信号
//返回值:如果成功执行返回0,否则返回非零值并设置errno
raise函数
#include <signal.h>int raise(int sig);//功能:向执行此函数的当前进程发送指定的信号
//参数:要发送的信号
//返回值:如果成功执行返回0,否则返回非零值并设置errno
示例–使用kill函数给指定进程发送信号
#include "header.h"int main(int argc, char **argv)
{if(argc < 3){fprintf(stderr,"usage: %s [pid] [signum]\n",argv[0]);exit(EXIT_FAILURE);}//从外部传入要发送进程的PID号和信号编号pid_t pid = atoi(argv[1]);int signum = atoi(argv[2]);if((kill(pid,signum)) != 0){perror("kill perror");exit(EXIT_FAILURE);}return 0;
}
经过代码编译执行发现kill
函数的使用和kill
指令无异,都可以使用它给指定进程发送信号。
示例–使用kill函数和raise函数向当前进程发送信号
#include "header.h"void sig_handler(int signum)
{printf("process %d receive a signal is %d\n",getpid(),signum);switch (signum){case 1:printf("receive signal is SIGHUP\n");break;case 2:printf("receive signal is SIGINT\n");break;case 10:printf("receive signal is SIGUSR1\n");break;case 12:printf("receive signal is SIGUSR2\n");break;default:printf("no sign to kernel\n");break;}
}int main(int argc, char **argv)
{if(argc < 2){fprintf(stderr,"usage: %s [signum]\n",argv[0]);exit(EXIT_FAILURE);}int signum = atoi(argv[1]);if(signal(SIGUSR1, sig_handler) == SIG_ERR){perror("signal SIGUSR1 error");}if(signal(SIGUSR2, sig_handler) == SIG_ERR){perror("signal SIGUSR2 error");}if(signal(SIGHUP, sig_handler) == SIG_ERR){perror("signal SIGHUP error");}if(signal(SIGINT, sig_handler) == SIG_ERR){perror("signal SIGINT error");}//将当前进程的PID号传进去给当前进程发信号if(kill(getpid(), signum) != 0){perror("kill perror");exit(EXIT_FAILURE);}if(raise(signum) != 0){perror("raise perror");exit(EXIT_FAILURE);}return 0;
}
通过编译执行可以发现kill
函数可以向当前进程发送信号,kill(getpid(), signum)
这个等同于raise(signum)
,它们两个都可以向当前进程发送信号。
这里有一个比较有意思的案例:父进程使用fork
函数创建子进程,在子进程中调用信号处理函数,然后在父进程中使用kill
函数向子进程发送信号
示例–使用kill
和signal
函数练习
#include "header.h"void sig_handler(int signum)
{printf("pid:%d signum:%d\n",getpid(),signum);
}int main()
{pid_t pid;if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid == 0) //子进程向内核注册信号处理函数{printf("this is child process,pid:%d ppid:%d\n",getpid(),getppid());if(signal(SIGHUP, sig_handler) == SIG_ERR){perror("signal error");}int i = 0;while(i < 15){printf("pid:%d i = %d\n",getpid(),i);i++;sleep(1);}}else //父进程向子进程发送信号{printf("parent process,pid:%d child's pid:%d\n",getpid(),pid);if(kill(pid, SIGHUP) != 0){perror("kill error");exit(EXIT_FAILURE);}wait(NULL);}
}
通过编译执行,发现并没有想象的父进程向子进程发送信号,然后子进程捕获对应的信号执行相应的操作。实际的运行效果是父进程执行,子进程直接退出了。这里先说一下原因:当父进程先运行的时候,就会打印父进程和子进程的pid
,然后通过kill
函数给子进程发送信号。通过上边的内容可知信号是一种异步机制,所以这里父进程给子进程发送信号已经发送成功了,只不过这时候子进程可能还没有调用signal
函数向内核登记信号处理函数,所以父进程向子进程发送的信号执行它的默认操作。由于大部分信号的默认操作都是结束该进程,所以这里的子进程直接被结束掉了,这也就是子进程为什么没有运行的原因。所以这里要想让子进程收到来自父进程的信号就要让子进程先开始运行,让父进程后运行,保证子进程已经向内核登记了信号处理函数以后才让父进程发送信号给子进程。只需在父进程开始时加一句sleep(1)
就可以实现这个功能,修改后的执行结果如下:
定时器
在Linux系统中有一种定时器信号SIGALRM
,用于通知进程一个定时器已经到期。当设置了一个定时器(使用setitimer()
或alarm()
函数),并且时间到达时,操作系统会向进程发送SIGALRM
信号。进程可以选择忽略这个信号,或者捕获它并执行相应的处理程序。
alarm函数
#include <unistd.h>unsigned alarm(unsigned seconds);//功能:用于在指定的时间后由内核向调用进程发送SIGALRM信号
//参数:以秒为单位的要设定的时间
//返回值:返回0或以前设置的定时器时间余留秒数
//注意:alarm这个函数是一次性的,调用一次只产生一次alarm信号,如果想要产生周期性的信号,要在信号处理函数里再次调用
示例–使用alarm函数设置一个定时器
#include "header.h"void sig_handler(int signum)
{if(signum == 14)printf("receive a signal is SIGALRM,time out\n");alarm(5); //当进入到这个函数里就说明触发了SIGALRM信号,此时要再次设置定时时间来产生周期性的定时时间
}int main(void)
{pid_t pid;//向内核登记SIGALRM信号处理函数,如果产生了这个信号就去执行相应的信号处理函数if(signal(SIGALRM, sig_handler) == SIG_ERR){perror("signal error");exit(EXIT_FAILURE);}if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid == 0){printf("child's pid:%d ppid:%d\n",getpid(),getppid());int i = 0;while(i < 15){printf("pid:%d i = %d\n",getpid(),++i);sleep(1);} }else{printf("parent's pid:%d child's pid:%d\n",getpid(),pid);alarm(5); //设定5秒后产生SIGALRM信号wait(NULL);}return 0;
}
通过编译执行,可以发现通过alarm()
函数可以来设置一个定时器,功能和单片机的硬件定时器类似。到时间以后就产生SIGALRM
信号,由于之前使用signal
函数向内核登记了该信号和信号处理函数,所以当该信号产生的时候就去信号处理函数里进行相应的操作。
示例–使用定时器每隔一个定时时间翻转引脚状态
#include "header.h"
#include <wiringPi.h>#define PIN 1void sig_handler(int signum)
{static int i = 0;if(signum == 14)printf("receive a signal is SIGALRM,time out\n");alarm(5); //当进入到这个函数里就说明触发了SIGALRM信号,此时要再次设置定时时间来产生周期性的定时时间if(++i % 2 == 0) //当i对2取余等于0的时候,将引脚设置为高电平反之设置为低电平digitalWrite(PIN,HIGH);elsedigitalWrite(PIN,LOW);
}int main(void)
{pid_t pid;//向内核登记SIGALRM信号处理函数,如果产生了这个信号就去执行相应的信号处理函数if(signal(SIGALRM, sig_handler) == SIG_ERR){perror("signal error");exit(EXIT_FAILURE);}if(wiringPiSetup() == -1) //初始化wiringPi库{perror("init wiring error");exit(EXIT_FAILURE);}//将引脚配置为输出模式pinMode(PIN,OUTPUT);if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid == 0){printf("child's pid:%d ppid:%d\n",getpid(),getppid());int i = 0;while(i < 15){printf("pid:%d i = %d\n",getpid(),++i);sleep(1);} }else{printf("parent's pid:%d child's pid:%d\n",getpid(),pid);alarm(5); //设定5秒后产生SIGALRM信号wait(NULL);}return 0;
}
由于这里用到了wiringPi
库,所以要将-lwiringPi
选项,执行的时候要用超级用户权限
这里使用gpio readall
这个指令来查看引脚的输入输出模式以及引脚的电平状态,通过对比发现它每五秒切换一次引脚号为1的电平状态,通过alarm
函数实现了定时器的用法。
setitimer函数
#include <sys/time.h>int setitimer(int which, const struct itimerval *restrict value,struct itimerval *restrict ovalue);struct itimerval {struct timeval it_interval; /* Interval for periodic timer */struct timeval it_value; /* Time until next expiration */};
struct timeval {time_t tv_sec; /* seconds */suseconds_t tv_usec; /* microseconds */};//功能:用于设置定时器,以实现延时和定时功能。该函数可以代替alarm函数,并具有更高的精度,支持微秒级的定时控制,它的工作机制是在指定的时间后发送特定的信号(如SIGALRM、SIGVTALRM或SIGPROF)给进程。
//参数1:which参数用于指定定时器的类型,//ITIMER_REAL:真实时间计时器,以系统实时时间为准,一旦时间到达就发送信号。//ITIMER_VIRTUAL:虚拟时间计时器,以进程在用户态消耗的时间为准。//ITIMER_PROF:CPU时间计时器,以进程在用户态和内核态所消耗的总时间为准。
//参数2:新的定时器设置值(value):这是一个指向struct itimerval结构体的指针,该结构体包含了定时器的间隔时间和总时间。//it_value:首次触发定时器前的等待时间。(定时器开始工作前需要的时间)//it_interval:定义了两次触发之间的时间间隔。如果设置为0,则定时器只触发一次。(要设定的时间)//在struct itimerval中又嵌入了一个结构体,这两个结构体的成员分别是tv_sec和tv_usec,用来设置秒和微秒。
//参数3:旧的定时器设置值(old_value):这也是一个指向struct itimerval结构体的指针,用来存储当前定时器的设置值。如果不需要获取当前值,可以将其设置为NULL。
//返回值:setitimer函数的返回值0表示成功,返回-1表示出错,出错时可以通过errno变量获取具体的错误信息。
示例–使用setitimer函数设置定时器
#include "header.h"
#include <wiringPi.h>
#include <sys/time.h>#define PIN 1void sig_handler(int signum)
{static int i = 0;if(signum == 14)printf("receive a signal is SIGALRM,time out\n");if(++i % 2 == 0) //当i对2取余等于0的时候,将引脚设置为高电平反之设置为低电平digitalWrite(PIN,HIGH);elsedigitalWrite(PIN,LOW);
}int main(void)
{pid_t pid;//向内核登记SIGALRM信号处理函数,如果产生了这个信号就去执行相应的信号处理函数if(signal(SIGALRM, sig_handler) == SIG_ERR){perror("signal error");exit(EXIT_FAILURE);}if(wiringPiSetup() == -1) //初始化wiringPi库{perror("init wiring error");exit(EXIT_FAILURE);}//将引脚配置为输出模式pinMode(PIN,OUTPUT);if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid == 0){printf("child's pid:%d ppid:%d\n",getpid(),getppid());int i = 0;while(i < 15){printf("pid:%d i = %d\n",getpid(),++i);sleep(1);} }else{printf("parent's pid:%d child's pid:%d\n",getpid(),pid);struct itimerval timer;timer.it_interval.tv_sec = 5; //设定定时时间,支持微秒级timer.it_interval.tv_usec = 0;timer.it_value.tv_sec = 5; //设定定时器在启动前经过的时间timer.it_value.tv_usec = 0;if(setitimer(ITIMER_REAL,&timer,NULL) == -1) //设置定时器参数,使用真实时间计时器进行定时{perror("setitimer error");exit(EXIT_FAILURE);}wait(NULL);}return 0;
}
通过上边的编译结果可以看出setitimer
函数也可以用来设置定时器,相比alarm
函数来说,它还支持毫秒级的定时,使用起来更加精确一点。