一、信号产生、处理、分类
二、signal信号处理机制
三、sigaction 高级版的信号处理函数
四、全程阻塞 sigprocmask
五、信号相关函数
1.kill函数
2.sleep函数
3.alarm函数
4.pause函数
六、计时器
信号就像古代战场上打仗,摇什么旗子摆什么阵。双方已经约定好。能不使用信号就不要使用信号,因为是异步。
信号是硬件中断的软件模拟(软中断)。
kill -1 查看信号列表
man 7 signal 主要查看信号行为
需要掌握:1到31号信号。
2 SIGINT ctrl c siginterupt
3 SIGQUIT ctrl \
9 SIGKILL kill -9 夺命信号,大王!不能捕捉也不能忽略
19 SIGSTOP ctrl z 夺命信号,小王!不能捕捉也不能忽略
-------------------------------------------------------------------------------------------------------------------------------------------------------------
一、信号产生、处理、分类:
信号的产生:
信号的生成来自内核,让内核生成信号的请求来自3个地方:
1.用户:用户能够通过输入CTRL+c、Ctrl+\,或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
2.内核:当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规)、浮点数溢出等;
3.进程:一个进程可以通过系统调用kill给另一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信。
信号的处理:
进程接收到信号以后,可以有如下3种选择进行处理:
1.接收默认处理:接收默认处理的进程通常会导致进程本身消亡。例如连接到终端的进程,用户按下CTRL+c,将导致内核向进程发送 一个SIGINT的信号,进程如果不对该信号做特殊的处理,系统将采用默认的方式处理该信号,即终止进程的执行;
2.捕捉信号并处理:进程可以事先注册信号处理函数,当接收到信号时,由信号处理函数自动捕捉并且处理信号。
9号信号(kill -9)和19号信号(ctrl z)不能被捕捉也不能被忽略。
进程接收到这两个信号后,只能接受系统的默认处理,即终止进程.
钩子:回调函数
默认处理的话,一般会把程序搞崩溃。
进程事先注册信号处理函数。当信号来了的时候
信号的分类:
同步信号:由进程的某个操作产生的信号,例如除0、内存访问违规;
异步信号:由像用户击键这样的进程外部事件产生的信号,再如,ctrl+z.
二、signal信号处理机制(最基本的信号处理函数)
可以使用函数signal()注册一个信号处理函数。原型为:
typedef void (*
sighandler_t)(int);
//函数指针。定义一种sighandler_t的类型。这个类型是指向某种函数的指针。这个函数以int型为参
数,并返回void类型。
sighandler_t
signal(
int signum, sighandler_t
handler);
//信号处理函数。signum是要捕捉的信号。handler是函数指针,也就是函数的地
址,
而函数名就是该函数所占内存的首地址。所以可以直接传入函数名。该
参数也可
以是SIG_DFL或SIG_IGN。若为SIG_DFL:表示交由系统缺省处理,相当于白注册
了。若为:SIG_IGN,表示忽略该信号。
例1:signal的使用,捕捉ctrl+c产生的信号
#include <signal.h> //signal()的头函数
void sig_func(int sig){
//信号处理函数,sig是传进去的几号信号。信号处理函数,必须固定这种格式。因为这是内核
printf("sig=%d is coming\n",sig);
调用的。
内核只传给我一个参数让我用。在信号处理函数中,如果想使用别的参数,只有一种
方法:使用全局变量。
}
int main(){
if(
signal(SIGINT,sig_func)==SIG_ERR){
//返回值是一个函数指针 。SIGINT就是2。
perror("signal");
}
while(1);
//这样就可以在函数的执行过程中给函数发信号了。函数执行到某一句时给他发信号,这是跳转
过去执行信号。执行完成信号后,回到跳转的那一句,接着跑。比如在这一句ctrl c之后,先去
执行sif_func,执行完之后会继续循环。
return 0;
}
例2:忽略信号的例子
int main(){
signal(
SIGINT,
SIG_IGN);
//当输入2号信号时,内核接受到以后不做任何处理。
while(1);
return 0;
signal(
SIGINT,SIG_DFL);
//到这个地方又不想忽略了。
}
例3:signal的重入
2号信号正在执行。2号又来了,怎么办?不能重入。
2号信号正在执行,3号又来了,怎么办?可以重入。
2号在执行,3号来了打断,此时有来了个2号,则后面的2号不能重入。
内核存储:
按一次ctrl+c,然后在ctrl+c处理函数执行过程中连续多次按ctrl+c。则后面多次ctrl+c只执行一次处理函数,也就是总共执行两次处理函数。原理:
内核针对信号,存储每个信号只有一个位置。存放在信号队列中。第一个2号信号来了之后,队列中对应位置立即从0变为1,然后内核看到有1,就拿走并执行。此时来了第二个2号信号,发现对应位置是0,于是填上1。之后又来了多个2号信号。因为内核还在执行第一个2号信号。所以第二个2号信号填上的1没有拿走,对应的位置还是1.这时后面的多个2号信号只能在对应位置置1(不变)。等第一个处理程序处理完之后,会把1拿走,去执行第二次。
#include <signal.h>
void
sig_func(int sig){
printf("before:sig=%d is coming\n",sig);
sleep(3);
printf("after:sig=%d is coming\n",sig);
}
int main(){
if(
signal(SIGINT,sig_func)==SIG_ERR){
//2号信号:ctrl c
perror("signal");
}
if(
signal(SIGQUIT,sig_func)==SIG_ERR){
//3号信号:ctrl \
perror("signal");
}
while(1);
return 0;
}
三、sigaction 高级版的信号处理函数
1.希望
信号处理函数只执行一次,下次重新注册。而signal函数是永久注册,每次都有效,不满足需求。这时就需要signaction.
2.一个
信号正在处理信号,又出现了同一类型的信号,而我想要递归去执行这个信号。因为signal函数是不能打断的,且最多执行2次,不满足需求。这时就需要signaction.
3.一个
信号正在处理信号,又出现了不同类型的信号,但是我不希望被打断。在signal中会去执行不同类型的处理函数,无法满足要求。这时我就需要signaction.
4.如果程序阻塞在内核态,如read(stdin)中。这时我给了一个信号。这个时候有两种情况:第一种是,系统调用错误返回,去执行信号。第二种是不错误返回,执行信号,执行完之后回到read()继续执行。signal默认是第二种,不错误返回。这个用处不大。一般在信号处理函数中就退出了。
int sigaction(int signum,
const struct sigaction *act, struct sigaction *oldact);
//signum是注册的是几号信号。最后一个参数是保存旧的行 为,往往不保存旧的行为,所以写NULL。第二个参数act是
新的
行为。
struct sigaction {
//新的行为act结构体
void (*sa_handler)(int); //老类型的信号处理函数指针。
不需要用,用的话还不如用signal函数。
void (*sa_sigaction)(int, siginfo_t *, void *); //新类型的信号处理函数指针。
我们用新的,高级版的。我们要实现这个函数。这是一
个指向一个函数的指针。该函数有三个参数。返回值为void。
sigset_t sa_mask; //
将要被阻塞的信号集合
int sa_flags; //掩码。
SA_RESETHAND表示处理完一次之后就把自己废掉。基本不用这种场景。
SA_NODEFER表示自己可以重入自己,通过无限压栈来实现。自己可以重入自己, 自己可以重入任何人。不写这个参数时,默认自己是不能重入自的。
SA_RESTART: 当程序挣阻塞在某个系统调用时,如果不写这个参数,会默认返回失败。即,上面的 第一种情况。如果写了这个参数,就不会失败。
SA_SIGINFO如果不写就表示使用的 时signal,写了就表示使用高级版的sigaction.所以一定要写上。
void (*sa_restorer)(void);
//保留,不使用。
};
字段
sa_mask是一个包含信号集合的
结构体,
该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。针对sigset_t结构体,有一组专门的函数对它进行处理,它们是:
#include <signal.h>
int sigemptyset(sigset_t *set); //清空信号集合set
int sigfillset(sigset_t *set); //将所有信号填充进set中
int sigaddset(sigset_t *set, int signum); //往set中添加信号signum
int sigdelset(sigset_t *set, int signum); //从set中移除信号signum
int sigismember(const sigset_t *set, int signum); //判断signum是否包含在set中(是:返回1,否:0)
int sigpending(sigset_t *set); //将被阻塞的信号集合由参数set指针返回(挂起信号)
例1:sigaction的使用
void sig(int signum,siginfo_t*p,void* p1){
//信号处理函数。
printf("signum=%d is coming\n",signum);
}
int main(){
struct sigaction act;
//定义新的行为
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
//sigaction为一个信号处理函数指针,指向信号处理函数sig()
act.sa_flags=SA_SIGINFO;
//设置掩码
int ret=
sigaction(SIGINT,&act,NULL);
//调用sigaction函数。注册的是2号信号。新的行为是act。不保存就的行为。
if(-1==ret){
perror("sigaction");
return -1;
}
while(1);
//这时候就可以发信号了
return 0;
}
例2:参数sa_flags为SA_RESTART的使用:阻塞后继续执行。
void sig(int signum,siginfo_t* p,void* p1){
printf("signum=%d is coming\n",signum);
}
int main(){
struct sigaction act;
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
act.sa_flags=
SA_SIGINFO|SA_RESTART;
//如果不写这个参数,会默认返回失败。也就是:执行到read的时候,程序阻塞。这
时
候
我给了2这个信号,会执行sig函数,sig函数执行完之后,程序结束。并不会打印出:
I am over这句话。如果写了这个参数,程序执行完信号处理函数后,会回到刚才执行的
地方继续执行。也就是:执行到read的时候,程序阻塞。这时候给了2这个信号,会执
行sig函数,sig函数执行完以后,程序回到刚才的语句,继续等待输入。输入字符后,
会打印出"I am over",然后程序正常退出。
int ret=sigaction(SIGINT,&act,NULL);
if(-1==ret){
perror("sigaction");
return -1;
}
char buf[128]={0};
ret=
read(0,buf,sizeof(buf));
//从标准输入读数据,如果没有数据会阻塞。
printf("I am over!\n");
return 0;
}
例3:参数sa_flags为SA_NODEFER :自己可以重入自己,无限压栈。
sleep原理:alarm信号就是把进城唤醒。如果有信号来,则被提前唤醒。
提前醒来。
void sig(int signum,siginfo_t* p,void* p1){
printf("before sleep,signum=%d is coming\n",signum);
sleep(3);
printf("after sleep,signum=%d is coming\n",signum);
}
int main(){
struct sigaction act;
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
act.sa_flags=
SA_SIGINFO|SA_NODEFER;
int ret=sigaction(SIGINT,&act,NULL);
if(-1==ret){
perror("sigaction");
return -1;
}
ret=sigaction(SIGQUIT,&act,NULL);
if(-1==ret){
perror("sigaction");
return -1;
}
while(1);
return 0;
}
例4:参数sa_flags为:SA_RESETHAND //处理函数只用一次就失效。
void sig(int signum,siginfo_t* p,void* p1){
printf("before sleep,signum=%d is coming\n",signum);
sleep(3);
printf("after sleep,signum=%d is coming\n",signum);
}
int main(){
struct sigaction act;
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
act.sa_flags=
SA_SIGINFO|SA_RESETHAND;
int ret=sigaction(SIGINT,&act,NULL);
//输入第一次ctrl +c时,可以执行信号处理函数。第二次ctrl +c 直接退出。
if(-1==ret){
perror("sigaction");
return -1;
}
ret=sigaction(SIGQUIT,&act,NULL);
if(-1==ret){
perror("sigaction");
return -1;
}
while(1);
return 0;
}
例5:参数sa_mask:增加阻塞信号 //在信号的执行过程中屏蔽其他信号
本来没有加屏蔽的时候,在执行2号的信号处理函数的时候,3号可以打断2号。但是在设置了屏蔽以后,在2号执行处理函数时,3号会被阻塞,不会打断2号。当2号执行完成后3号再执行。
注意:一、是在2号处理函数执行的过程中,才阻塞。没有2号信号的时候,不影响3号处理函数的执行。二、阻塞不是忽略。在二号执行完以后会再执行3号的处理函数。
void sig(int signum,siginfo_t* p,void* p1){ //2号信号处理函数
printf("before sleep signum=%d is coming\n",signum);
sleep(3);
printf("after sleep signum=%d is coming\n",signum);
}
int main(){
struct sigaction act;
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
act.sa_flags=SA_SIGINFO;
int ret;
ret=
sigaddset(&act.sa_mask,SIGQUIT);
//act是新的行为。act.sa_mask是将要被阻塞的信号集合。这句话指向集合中添加3号信号。
if(-1==ret){
perror("sigaddset");
return -1;
}
ret=sigaction(SIGINT,&act,NULL);
//设置了2号的信号处理函数。
if(-1==ret){
perror("sigaction");
return -1;
}
while(1);
return 0;
}
例6:查看
有哪些进程在阻塞
void sig(int signum,siginfo_t* p,void* p1)
{
printf("before sleep signum=%d is coming\n",signum);
sleep(3);
sigset_t set;
sigemptyset(&set); //首先将set清空,用来存放sigpending的返回值
sigpending(&set);
//将被阻塞的信号集合由参数set指针返回
if(sigismember(&set,SIGQUIT))
{
printf("SIGQUIT is pending\n");
}else{
printf("SIGQUIT is not pending\n");
}
printf("after sleep signum=%d is coming\n",signum);
}
int main(){
struct sigaction act;
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
act.sa_flags=SA_SIGINFO;
int ret;
ret=sigaddset(&act.sa_mask,SIGQUIT);
if(-1==ret){
perror("sigaddset");
return -1;
}
ret=sigaction(SIGINT,&act,NULL);
if(-1==ret){
perror("sigaction");
return -1;
}
while(1);
return 0;
}
例7:信号处理函数中参数siginfo_t的使用:查看是谁给你发的信号
通过man sigaction 查看siginfo_t结构体,如下:
siginfo_t{
pid_t si_pid; //进程的pid
uid_t si_uid; //进程的uid
.....
}
void sig(int signum,
siginfo_t* p,void* p1){
printf("signum=%d is coming\n",signum);
printf("sending signal process id=%d\n",
p->si_pid);
//查看给我发信号的进程的pid和uid
printf("sending signal user id=%d\n",
p->si_uid);
}
int main(){
struct sigaction act;
bzero(&act,sizeof(act));
act.sa_sigaction=sig;
act.sa_flags=SA_SIGINFO;
int ret=sigaction(SIGINT,&act,NULL);
if(-1==ret){
perror("sigaction");
return -1;
}
while(1);
return 0;
}
四、全程阻塞 sigprocmask
sigaction只是在信号处理过程中对其他信号进行阻塞。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//oldset没用,填NULL。
参数how的值为如下3者之一:
a:SIG_BLOCK ,将参数2的信号集合添加到进程原有的阻塞信号集合中
b:SIG_UNBLOCK ,从进程原有的阻塞信号集合移除参数2中包含的信号
c:SIG_SETMASK,重新设置进程的阻塞信号集为参数2的信号集
针对sigset_t结构体(与sigaction中的一样),有一组专门的函数对它进行处理,它们是:
#include <signal.h>
int sigemptyset(sigset_t *set); //清空信号集合set
int sigfillset(sigset_t *set); //将所有信号填充进set中
int sigaddset(sigset_t *set, int signum); //往set中添加信号signum
int sigdelset(sigset_t *set, int signum); //从set中移除信号signum
int sigismember(const sigset_t *set, int signum); //判断signum是否包含在set中(是:返回1,否:0)
int sigpending(sigset_t *set); //将被阻塞的信号集合由参数set指针返回(挂起信号)
例1:sigprocmask的使用
int main(){
sigset_t set;
sigemptyset(&set);
//清空信号集合set
sigaddset(&set,SIGINT);
//在set中添加2号信号
int ret;
ret=
sigprocmask(SIG_BLOCK,&set,NULL);
//将set信号集合添加到进程原有的阻塞信号集合中
if(-1==ret){
perror("sigprocmask");
return -1;
}
sleep(5);
ret=
sigismember(&set,SIGINT);
//判断2号进程是否在set中。
if(ret !=1){
return -1;
}
ret=sigprocmask(
SIG_UNBLOCK,&set,NULL);
//从进程原有的阻塞信号集合移除set中包含的信号 。在接触之前输入2号信号,2 号信号会被阻塞,程序继续运行。当移除之后,如果阻塞队列上有2号信号,会执 行。如果没有,后面出现了2号信号的时候也会执行。
if(-1==ret){
perror("sigprocmask");
return -1;
}
while(1);
return 0;
}
例2:查看sigpromask中有哪些进程在阻塞
int main(){
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGINT);
int ret;
ret=sigprocmask(SIG_BLOCK,&set,NULL);
if(-1==ret){
perror("sigprocmask");
return -1;
}
sleep(5);
sigemptyset(&set);
//把set清空
sigpending(&set);
//将被阻塞的信号集合由参数set指针返回
ret=sigismember(&set,SIGINT);
if(ret ==1){
printf("SIGINT is pending\n");
}else{
printf("SIGINT is not pending\n");
}
ret=sigprocmask(SIG_UNBLOCK,&set,NULL);
if(-1==ret){
perror("sigprocmask");
return -1;
}
return 0;
}
五、信号相关函数
1.kill函数
int kill(pid_t pid, int sig);
sig是要发的信号,如SIGINT等。pid是对方进程的pid,有以下几种情况:
pid > 0 //将信号发给ID为pid的进程
pid = = 0 //将信号发送给与发送进程属于
同一个进程组的所有进程
pid < 0 //将信号发送给
进程组ID等于pid绝对值的所有进程
pid = = -1 //将信号发送给该进程有权限发送的系统里的所有进程
例1:
int main(int argc,char* argv[]){
if(argc !=2){
printf("error args\n");
return -1;
}
pid_t pid;
pid=
atoi(argv[1]);
//将参数转化成数字。
int ret;
ret=
kill(pid,SIGINT);
//给pid发2号信号。
if(-1==ret){
perror("kill");
return -1;
}
return 0;
}
例2:
int main(int argc,char* argv[]){
if(!fork()){
printf("I am child\n");
while(1);
}else{
printf("I am parent\n");
sleep(10);
int ret;
ret=kill(
0,SIGINT);
//子进程和父进程在同一个进程组,所以发信号后,子进程也可以收到。一个.c中有两个进程在跑,
然后ctrl+c ,两个进程都关了,这就是因为我们给进程组发的信号。
if(-1==ret){
perror("kill");
return -1;
}
return 0;
}
}
2.sleep函数
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec); //u秒
sleep()函数设定多长时间以后,让系统给我当前进程发信号。
sleep相当于进行了两步操作:一、调用alarm函数,秒数作为alarm的参数。即,多少秒之后自己给自己发送
SIGALRM信号。
二、然后再放弃CPU。
所以如果提前收到SIGALRM信号,或其他把自己唤醒的信号。那么自己就不再睡眠了。
3.alarm函数
unsigned int alarm(unsigned int seconds);
在多少秒之后,自己给自己发送一个
SIGALRM信号。
例:
void sig_handle(int sig){
printf("sig=%d is coming\n",sig);
}
int main(){
signal(SIGALRM,sig_handle);
//捕捉SIGALRM信号 。
alarm(3);
//3秒以后自己给自己的进程发送SIGALRM信号。结果为,3秒之后程序执行
sig_handle函数。
sleep函数就是这个机制,首先调用alarm(t),t秒后给自己发送信号,然后主动放弃CPU进入到睡
眠队列。
while(1);
return 0;
}
4.pause函数
linux下没有pause命令。所以在程序中也就不能使用system("pause");这条语句。因为这条语句就是在c语言中嵌套了脚本。写程序很忌讳嵌套别的语言,相当于重新起了一个进程,很影响效率。
int pause(void); //将自身进程挂起,直到有信号发生时才从pause返回。不管什么信号。
int main(){
pause();
//执行到这会暂停。
return 0;
}
六、计时器 可以用于测程序性能
Linux为每个进程维护3个计时器,分别是
真实计时器、虚拟计时器和实用计时器。 分三个计时器,是因为linux分为用户态和内核态。
自己的程序运行在用户态,当系统调用时会到内核态执行。服务器编程,没法控制内核态运行时间。
真实计时器:程序运行的实际时间。从程序开始执行到退出的时间。
虚拟计时器:程序运行在用户态时所消耗的时间。做嵌入式开发的时候会用。
实用计时器:程序在R状态的时间。相当于统计用了多少时间片。比较常用。
真实计时器时间=实用计时器时间+睡眠时间。
实用计时器时间=用户态运行时间+内核态运行时间。
虚拟计时器时间=用户态运行时间。
例如:有一程序运行,在用户态运行了5秒,在内核态运行了6秒,还睡眠了7秒,则真实计算器计算的结果是18秒,虚拟计时器计算的是5秒,实用计时器计算的是11秒。
int getitimer(int which, struct itimerval *value); //获取计时器的设置
int
setitimer(int
which,
const struct itimerval *value, struct itimerval *ovalue);
//设置计时器。参数which指定哪个计时器,可选项为
ITIMER_REAL(真实计时器)、ITIMER_VIRTUAL(虚拟计时器)、ITIMER_PROF(实用计时器).
//value为出入参数,指定计时器的初始间隔时间和重复间隔时间。
ovalue为传出参数,用于传出旧的时间设置,一般不用,写NULL。
//用指定的初始间隔和重复间隔时间为进程设定好一个计时器后,该计时器就会定时地向进
程
发
送时钟信号。3个计时器发送的时钟信号分别为:SIGALRM,SIGVTALRM和SIGPROF。因
为sleep函数和alarm函数也都是发送的SIGALRM信号。所以不要跟真实计时器一起用。
struct itimerval {
struct timeval
it_interval; //next value
重复间隔
struct timeval
it_value; //current value
初始间隔
};
struct timeval {
long tv_sec; //seconds 时间的秒数部分
long tv_usec; //microseconds 时间的微秒部分
};
例1:真实计时器的使用
void sig_handle(int sig){
time_t t;
time(&t);
struct tm* p=gmtime(&t);
printf("%2d:%2d:%d\n",p->tm_hour,p->tm_min,p->tm_sec);
}
int main(){
signal(SIGALRM,sig_handle);
//捕捉SIGALRM信号。
kill(0,SIGALRM);
//通过kill函数给同一进程组的进程(这里也就是自己)发送信号。打印出时间。
struct itimerval rt;
memset(&rt,0,sizeof(rt));
//清零,这样就不用设置初始间隔的微妙部分和重复间隔的微妙部分。
rt.it_value.tv_sec=5;
//初始间隔的秒数部分为5.
rt.it_interval.tv_sec=2;
//重复间隔的秒数部分为2.
int ret=
setitimer(
ITIMER_REAL,
&rt,
NULL);
//使用真实计时器。
if(-1==ret){
perror("setitimer");
return -1;
}
while(1);
//程序执行后,先打印时间。然后过5秒后打印时间。之后每隔2秒打印时间。即程 序的运行结果为:0 5 7 9 11 ...
return 0;
}
例2:实用计时器
void sig_handle(int sig){
time_t t;
//晚上好好整理一下时间。
time(&t);
struct tm* p=gmtime(&t);
printf("%2d:%2d:%d\n",p->tm_hour,p->tm_min,p->tm_sec);
}
int main(){
signal(SIGPROF,sig_handle);
struct itimerval rt;
memset(&rt,0,sizeof(rt));
rt.it_value.tv_sec=5;
//初始间隔的秒数部分为5.
rt.it_interval.tv_sec=2;
//重复间隔的秒数部分为5.
int ret=setitimer(
ITIMER_PROF,&rt,NULL);
//实用计时器。
if(-1==ret){
perror("setitimer");
return -1;
}
kill(0,SIGPROF);
//先打印出时间,开始计时
sleep(5);
//因为实用计时器不统计sleep的时间,所以要等到sleep完才开始计算初始间隔。
所以
程序的时间结果为:0 10 12 14 16...
while(1);
return 0;
}