Linux 学习笔记19 信号
信号
信号概述
为什么要是使用信号——为了实现进程的有序退出
信号是进程运行过程中,由自身产生或者由进程外部发来的消息。信号是硬件中断的软件模拟(软中断)
- signal信号处理机制
-
typedef void (*sighandler_t)(int); //函数指针 sighandler_t signal(int signum, sighandler_t handler);
- 参数解析:
signum 表示要捕捉的信号,
第 2 个参数是个函数指针(回调函数),表示要对该信号进行捕捉的函数,该参数也可以是 SIG_DFL (表示交由系统缺省处理)或 SIG_IGN (表示忽略掉该信号而不做任何处理)。signal 如果调用成功,返回以前该信号的处理函数的地址,否则返回 SIG_ERR。 - 信号处理机制代码如下:
signal 例子:
#include <func.h>
void sigfunc(int signum)//回调函数
{
printf("sig num%d is coming\n",signum);//显示几号信号
}
int main(int argc,char * argv[])
{
int ret;
signal(SIGINT,sigfunc);
char buf[128]={0};//以下为写一个read,为了使程序阻塞,查看信号的效果
read(0,buf,sizeof(buf));//用while(1)也可达到同样的效果
return 0;
}
执行效果如下:
signal 设定多种信号:
#include <func.h>
//设定2号信号和3号信号
//模拟特殊情况
void sigfunc(int signum)
{
printf("%d is coming\n",signum);
}
int main(int argc,char * argv[])
{
signal(SIGINT,sigfunc);
signal(SIGQUIT,sigfunc);
char buf[128]={0};
read(0,buf,sizeof(buf));
return 0;
}
执行效果如下:
signal的特殊情况:
#include <func.h>
//演示例一:2号信号处理流程会被3号信号打断
//例二:相同信号不会打断原有信号处理流程,如2号信号不会打断之前2号信号的处理流程
//例三:发送多次相同信号,只会多响应一次相同信号。
void sigfunc(int signum)
{//sigfunc中代码为信号处理流程
printf("before sleep %d is coming\n",signum);
sleep(3);
printf("sig %d is awake\n",signum);
}
int main(int argc,char * argv[])
{
int ret;
signal(SIGINT,sigfunc);
signal(SIGQUIT,sigfunc);
char buf[128]={0};//以下为写一个read,为了使程序阻塞,查看信号的效果
read(0,buf,sizeof(buf));//用while(1)也可达到同样的效果
return 0;
}
特殊情况1:在2号信号执行时,同类型(2号)的信号再发生,无论发生多少次,只会再多执行一次。
特殊情况2:2号信号正在处理时,发生了3号信号,此时会跳转执行3号信号,之后再执行剩下的2号信号
特殊情况3:程序read正在阻塞,2号信号产生了,此时输入一个回车键,则系统会等待2号信号处理完毕后,再读取回车键
SIG_IGN 和 SIG_DFL 举例:
SIG_IGN:
#include <func.h>
//设定2号信号和3号信号
//模拟特殊情况
void sigfunc(int signum)
{
printf("%d is coming\n",signum);
}
int main(int argc,char * argv[])
{
signal(SIGINT,SIG_IGN);//将2号信号忽略
signal(SIGQUIT,sigfunc);
char buf[128]={0};
read(0,buf,sizeof(buf));
return 0;
}
执行效果如下:
SIG_DFL:
#include <func.h>
void sigfunc(int signum)
{
printf("%d is coming\n",signum);
}
int main(int argc,char * argv[])
{
signal(SIGINT,SIG_IGN);
printf("start sleep\n");//信号将被忽略
sleep(10);
printf("sigfunc awake\n");//此时信号将可以被响应
signal(SIGINT,SIG_DFL);//SIG_DFL,信号只能被响应一次
sleep(2);
return 0;
}
执行效果如下:
- 信号处理机制的特殊情况
1注册一个信号处理函数,并且处理完毕一个信号之后,不需要重新注册,才能够捕捉下一个
信号;
2、 如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个同类型的信号,这时
会挨着执行(如:2号信号之后又发生了一个2号信号,就会多执行一次)
3、 如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个不同类型的信号,此时会跳转去执行另一个信号,之后再执行剩下的没有处理完的信号(如:2号信号正在处理时,发生了3号信号,此时会跳转执行3号信号,之后再执行剩下的2号信号)
4、 如果程序阻塞在一个系统调用(如 read(…))时,发生了一个信号,会先跳转到信号处理函数,等信号处理完毕后,系统调用再返回。(如:程序read正在阻塞,信号2号产生了,此时输入一个回车键,则系统会等待2号信号处理完毕后,再读取回车键)
sigaction信号处理注册
-
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
参数解析:
signum为要捕捉的信号
act是一个结构体里面包含信号处理函数地址、处理方式等信息
oldact 是一个传出参数, 为上一次设置的信号行为,通常为 NULL(不需要取出上一次的行为)。
结构体 struct sigaction的原型为: -
struct sigaction { void (*sa_handler)(int); //老类型的信号处理函数指针,因为类似signal,一般不用 void (*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号处理函数指针,用这个 sigset_t sa_mask; //将要被阻塞的信号集合 int sa_flags; //信号处理方式掩码 ( SA_SIGINFO ,具体见下方表格) void (*sa_restorer)(void); //保留,不要使用 };
结构体分析:
sa_falgs是一组掩码的合成值,指示信号处理时所应该采取的一些行为
掩码 | 描述 |
---|---|
SA_RESETHAND | 处理完毕要捕捉的信号后,将自动撤消信号处理函数的注册,该选项不符合一般的信号处理流程,目前已经不再使用,仅了解即可 |
SA_NODEFER | 在处理信号时,如果又发生了其它的信号,则立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号,即递归地处理。(不常用)无限重入 |
SA_RESTART | 如果在发生信号时,程序正阻塞在某个系统调用,例如调用read()函数,则在处理完毕信号后,接着从阻塞的系统返回。如果不指定该参数,中断处理完毕之后,read 函数读取失败。 |
SA_SIGINFO | 指示结构体的信号处理函数指针是哪个有效,如果 sa_flags 包含该掩码,则 sa_sigaction 指针有效,否则是 sa_handler 指针有效。(常用) |
- sigaction SA_SIGINFO 例子如下
#include <func.h>
//针对signal五种特殊情况可采用sigaction
void sigfunc(int signum,siginfo_t *p,void *q)
{//第二个参数之后分析,第三个参数基本用不到
printf("%d is coming\n",signum);
}
int main(int argc,char * argv[])
{
struct sigaction act;
//memset(&act,0,sizeof(act));//memset和bzero效果一样,使结构体初始化为0
bzero(&act,sizeof(act));
act.sa_sigaction=sigfunc;//赋函数指针
act.sa_flags=SA_SIGINFO;//当掩码为SA_SIGINFO时,sigaction生效,否则为结构体中的handler生效
int ret;
ret=sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
while(1);
return 0;
}
执行效果如下:
- SA_RESETHAND 信号捕捉生效一次后,回归到默认设置。很少用 ,代码如下:
#include <func.h>
//针对signal五种特殊情况可采用sigaction
void sigfunc(int signum,siginfo_t *p,void *q)
{//第二个参数之后分析,第三个参数基本用不到
printf("%d is coming\n",signum);
}
int main(int argc,char * argv[])
{
struct sigaction act;
//memset(&act,0,sizeof(act));//memset和bzero效果一样,使结构体初始化为0
bzero(&act,sizeof(act));
act.sa_sigaction=sigfunc;//赋函数指针
act.sa_flags=SA_SIGINFO|SA_RESETHAND;//信号第一次发的时候有效,第二次发时回归到默认
int ret;
ret=sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
while(1);
return 0;
}
执行效果如下:
- SA_NODEFER 信号不断重入,一个新的信号来了,就打断前一个信号,先执行新的信号,包括自身(即2号也能打断2号信号),代码如下:
#include <func.h>
//针对signal五种特殊情况可采用sigaction
void sigfunc(int signum,siginfo_t *p,void *q)
{//第二个参数之后分析,第三个参数基本用不到
printf("before sleep sig %d is coming\n",signum);
sleep(3);
printf("sig %d is awake\n",signum);
}
int main(int argc,char * argv[])
{
struct sigaction act;
//memset(&act,0,sizeof(act));//memset和bzero效果一样,使结构体初始化为0
bzero(&act,sizeof(act));
act.sa_sigaction=sigfunc;//赋函数指针
act.sa_flags=SA_SIGINFO|SA_NODEFER;//不断重入
int ret;
ret=sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
while(1);
return 0;
}
执行效果如下:
- SA_RESTART 对具有阻塞的接口有影响,如read,scanf,如果不加则不会阻塞 ,处理完毕信号之后直接跳过 read 或者 scanf
以下为 signal 和 sigaction 对比 代码如下:
#include <func.h>
//signal_read
void sigfunc(int signum)
{
printf("sig %d is coming\n",signum);
}
int main(int argc,char * argv[])
{
signal(SIGINT,sigfunc);
char buf[128];
read(0,buf,sizeof(buf));
printf("buf is %s\n",buf);
return 0;
}
执行效果如下:
#include <func.h>
//针对signal五种特殊情况可采用sigaction
void sigfunc(int signum,siginfo_t *p,void *q)
{//第二个参数之后分析,第三个参数基本用不到
printf("%d is coming\n",signum);
}
int main(int argc,char * argv[])
{
struct sigaction act;
//memset(&act,0,sizeof(act));//memset和bzero效果一样,使结构体初始化为0
bzero(&act,sizeof(act));
act.sa_sigaction=sigfunc;//赋函数指针
act.sa_flags=SA_SIGINFO|SA_RESTART; //测试2
//act.sa_flags=SA_SIGINFO;//测试1
int ret;
ret=sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
char buf[128]={0};
ret=read(0,buf,sizeof(buf));
printf("ret=%d,buf=%s\n",ret,buf);
return 0;
}
执行效果如下:
-
sa_mask是是一个包含信号集合(sigset_t)的结构体,该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。(难点)
-
sigset_t //结构体处理函数 int sigemptyset(sigset_t *set); //清空信号集合 set int sigfillset(sigset_t *set); //将所有信号填充进 set 中,将所有信号置为1 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 指针返回 与上述函数不同,上述函数是位操作,此函数是系统调用 常用
挂起:信号已经到达,但是没有被响应称之为挂起
-
实现2号信号不被3号信号打断,先执行完2号信号再执行3号信号的效果——使用 sigset_t 信号集合,代码如下:
#include <func.h>
//例:如何使用sa_mask
//实现2号信号处理时,屏蔽3号信号,使3号信号无法打断2号信号。
//接口 sigaddset
void sigfunc(int signum,siginfo_t *p,void *q)
{//第二个参数之后分析,第三个参数基本用不到
printf("before sleep sig %d is coming\n",signum);
sleep(3);
printf("sig %d is awake\n",signum);
}
int main(int argc,char * argv[])
{
struct sigaction act;
//memset(&act,0,sizeof(act));//memset和bzero效果一样,使结构体初始化为0
bzero(&act,sizeof(act));
act.sa_sigaction=sigfunc;//赋函数指针
act.sa_flags=SA_SIGINFO;
sigaddset(&act.sa_mask,SIGQUIT);//使3号信号被屏蔽
int ret;
ret=sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
sigaction(SIGQUIT,&act,NULL);
while(1);
return 0;
}
执行效果:
- 信号挂起 sigpending 可用于查看2号信号处理时是否有其他信号发生,代码如下:
#include <func.h>
//例:如何使用sigpending 信号挂起
void sigfunc(int signum,siginfo_t *p,void *q)
{//第二个参数之后分析,第三个参数基本用不到
printf("before sleep sig %d is coming\n",signum);
sleep(3);
sigset_t pend_signal;//挂起信号集
sigpending(&pend_signal);//将信号集挂起
if(sigismember(&pend_signal,SIGQUIT))//检测在2号信号处理时,是否有3号信号发生
{
printf("SIGQUIT is pending\n");
}else
{
printf("SIGQUIT isn't pending\n");
}
printf("sig %d is awake\n",signum);
}
int main(int argc,char * argv[])
{
struct sigaction act;
//memset(&act,0,sizeof(act));//memset和bzero效果一样,使结构体初始化为0
bzero(&act,sizeof(act));
act.sa_sigaction=sigfunc;//赋函数指针
act.sa_flags=SA_SIGINFO;
sigaddset(&act.sa_mask,SIGQUIT);//使3号信号被屏蔽
int ret;
ret=sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
sigaction(SIGQUIT,&act,NULL);
while(1);
return 0;
}
执行效果如下:
sigprocmask信号阻塞
使用场景:加锁—关键代码—解锁,为了使信号不打断加解锁中的关键代码,让代码先执行再接收信号,需要阻塞信号sigprocmask。需要改造成 阻塞—加锁—代码—解锁—解除阻塞,sigprocmask为全程阻塞,常用于保护关键代码。
概念:sigprocmask 是全程阻塞,在 sigprocmask 中设置了阻塞集合后,被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号集合。
-
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数解析:
- how为以下三者之一:
参数 | 解析 |
---|---|
SIG_BLOCK | 将参数 2 的信号集合添加到进程原有的阻塞信号集合中 |
SIG_UNBLOCK | 从进程原有的阻塞信号集合移除参数 2 中包含的信号 |
SIG_SETMASK | 重新设置进程的阻塞信号集为参数 2 的信号集 |
- *set:想要阻塞的信号集,用 sigset_t 设定。
- *oldset:原有进程的信号集,一般填NULL。
sigprocmask 代码如下:
#include <func.h>
//sigprocmask 例子
//验证无论是否有信号打断,关键代码都会被执行
//
int main(int argc,char * argv[])
{
int ret;
sigset_t mask;//定义一个信号集合
sigemptyset(&mask);//清空mask信号集
sigaddset(&mask,SIGINT);//填入想要阻塞的信号
ret=sigprocmask(SIG_BLOCK,&mask,NULL);//开始阻塞
ERROR_CHECK(ret,-1,"sigprocmask");
//以下为想要保护的关键代码
printf("This is Key programe\n");
sleep(3);
ret=sigprocmask(SIG_UNBLOCK,&mask,NULL);//解除阻塞
ERROR_CHECK(ret,-1,"sigprocmask");
return 0;
}
执行效果如下:
可见,由信号中断时,关键代码不受影响,仍然会执行打印。
sigprocmask 和 sigpending ,查看在全程阻塞的过程中是否有信号发生,代码如下:
#include <func.h>
//sigprocmask 例子
//利用sigpending查看在全程阻塞的过程中是否有信号发生
int main(int argc,char * argv[])
{
int ret;
sigset_t mask;//定义一个信号集合
sigemptyset(&mask);//清空mask信号集
sigaddset(&mask,SIGINT);//填入想要阻塞的信号
ret=sigprocmask(SIG_BLOCK,&mask,NULL);//开始阻塞
ERROR_CHECK(ret,-1,"sigprocmask");
//以下为想要保护的关键代码
printf("This is Key programe\n");
sleep(3);
sigset_t pend;
sigemptyset(&pend);
sigpending(&pend);
if(sigismember(&pend,SIGINT))
{
printf("SIGINT is pending\n");
}else
{
printf("SIGINT isn't pending\n");
}
ret=sigprocmask(SIG_UNBLOCK,&mask,NULL);//解除阻塞
ERROR_CHECK(ret,-1,"sigprocmask");
return 0;
}
执行效果如下:
kill信号
-
int kill(pid_t pid,int sig);//如 kill(pid,SIGINT)
- 参数解析
pid<0 将信号发送给进程组 ID 等于 pid 绝对值的所有进程,
pid=0 将信号发送给与此进程同一个进程组的所有进程
pid>0 将信号发给 ID 为 pid 的进程
pid=-1 将信号发送给该进程有权限发送的系统里的所有进程,如果是 root 就会发送给所有进程
sig为想要发送的信号 - kill 举例:
#include <func.h>
int main(int argc,char * argv[])
{
ARGS_CHECK(argc,2);
pid_t pid=atoi(argv[1]);
int ret=kill(pid,SIGINT);
ERROR_CHECK(ret,-1,"kill");
return 0;
}
- 子进程自身使自己终止与pid=0时的代码如下:
#include <func.h>
int main(int argc,char * argv[])
{
if(!fork())
{
int ret=kill(getpid(),SIGINT);//子进程使自身终止 kill杀死自己时不会产生Z进程
//int ret=kill(0,SIGINT);//pid=0时,父子进程都将终止,属于同一进程组
while(1);
}else
{
wait(NULL);
while(1);
}
return 0;
}
执行效果如下:
- pid<0举例:
#include <func.h>
int main(int argc,char * argv[])
{
if(!fork())
{
while(1);
}else
{
wait(NULL);
while(1);
}
return 0;
}
执行效果如下:
![kill_pid](https://img-blog.csdnimg.cn/20200410214619904.png
-13655 代表 pid<0 会删除进程组 ID 为13655 的所有进程。
- pid=-1 举例:
-
kill -2 -1 //注意最好不要用 root 删除,否则操作系统会注销所有进程
sleep函数
- sleep实现原理:sleep内部是由信号处理机制进行处理的,sleep采用两个接口实现
-
usigned int alarm(unsigned int seconds); // 告诉自身的进程再seconds秒后自动产生一个SIGALRM(14号)的信号
-
int pause(void); //将自身进程挂起,直到信号发生时,才从pause返回
-
kill -l // 查看所有信号的命令
-
man 7 signal //查看信号默认行为
- alarm代码如下:
#include <func.h>
//演示 alram 是怎么运作的
void sigfunc(int signum)
{
printf("sig %d is coming\n",signum);
}
int main(int argc,char * argv[])
{
alarm(3);
char buf[128]={0};
read(0,buf,sizeof(buf));
return 0;
}
alarm执行效果:
- pause 的实现:
#include <func.h>
//pause 的实现
int main(int argc,char * argv[])
{
pause();
return 0;
}
执行效果如下:
可见,pause 将进程放入睡眠队列中。
- sleep 函数的实现:
#include <func.h>
//sleep 的实现
void sigfunc(int signum)
{
}
int main(int argc,char * argv[])
{
//signal(SIGALRM,SIG_IGN);//测试忽略(SIG_IGN)的时候能不能让进程唤醒——不能
signal(SIGALRM,sigfunc);//空函数,使sleep函数实现
alarm(3);//3秒后将进程唤醒
pause();
return 0;
}
时钟处理
Linux为每个进程维护3个计时器,分别是真实计时器、虚拟计时器和实用计时器
真实计时器计算的是程序运行的实际时间。
实用计时器程序处于R状态(运行)时所消耗的时间之和,即统计进程对时间片消耗的累计值。
虚拟计时器计算的是程序运行在用户态所消耗的时间。
-
int getitimer(int which, struct itimerval *curr_value);//获取计时器的设置,不常用
-
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);//设置计时器(常用)
setitimer参数解析:
-
which 指定哪个计时器,可选项为: ITIMER_REAL(真实计时器)、ITIMER_VIRTUAL(虚拟计时器)d、ITIMER_PROF(实用计时器)
-
new_value 为一结构体的传入参数,指定该计时器的初始间隔时间和重复间隔时间
-
old_value 为一结构体传出参数,用于传出以前的计时器时间设置。
结构体struct itimerval 如下: -
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 */ //微秒 };
-
真实计时器 ITIMER_REAL 代码如下:
#include <func.h>
//使用 ITIMER_REAL
void sigfunc(int signum)
{
//打印当前时间
time_t now;
now=time(NULL);
printf("%s\n",ctime(&now));
}
int main(int argc,char * argv[])
{
signal(SIGALRM,sigfunc);
sigfunc(0);//打印时间
struct itimerval t;
bzero(&t,sizeof(t));
t.it_value.tv_sec=2;//初始间隔为2s
t.it_interval.tv_sec=3;//重复间隔为3s
int ret;
ret=setitimer(ITIMER_REAL,&t,NULL);
ERROR_CHECK(ret,-1,"setitimer");
char buf[128];
read(0,buf,sizeof(buf));//read使程序阻塞
return 0;
}
执行效果:
无论进程处于R 或者 S 真实计时器都在计时
- 实用计时器 ITIMER_PROF ,代码如下:
#include <func.h>
//使用 ITIMER_REAL
void sigfunc(int signum)
{
//打印当前时间
time_t now;
now=time(NULL);
printf("%s\n",ctime(&now));
}
int main(int argc,char * argv[])
{
signal(SIGPROF,sigfunc);//使用信号SIGPROF
sigfunc(0);//打印时间
struct itimerval t;
bzero(&t,sizeof(t));
t.it_value.tv_sec=2;//初始间隔为2s
t.it_interval.tv_sec=3;//重复间隔为3s
int ret;
ret=setitimer(ITIMER_PROF,&t,NULL);//实用计时器
ERROR_CHECK(ret,-1,"setitimer");
char buf[128];
read(0,buf,sizeof(buf));//read使程序阻塞
//sleep(5);//例2,验证系统调用sleep不算在实用计时器计算范围内
while(1);//在用户态运行
return 0;
}
执行效果:
- 虚拟计时器 ITIMER_VIRTUAL 代码如下:
#include <func.h>
//使用 ITIMER_VIRTURAL
void sigfunc(int signum)
{
//打印当前时间
time_t now;
now=time(NULL);
printf("%s\n",ctime(&now));
}
int main(int argc,char * argv[])
{
signal(SIGVTALRM,sigfunc);//使用SIGVTALRM信号
sigfunc(0);//打印时间
struct itimerval t;
bzero(&t,sizeof(t));
t.it_value.tv_sec=2;//初始间隔为2s
t.it_interval.tv_sec=3;//重复间隔为3s
int ret;
ret=setitimer(ITIMER_VIRTUAL,&t,NULL);//使用ITIMER_VIRTUAL
ERROR_CHECK(ret,-1,"setitimer");
sleep(5);
while(1);//用户态运行
return 0;
}
执行效果如下: