1.信号的概念:
信号的特点:
1简单。2 不能携带大量数据。3满足某种特殊条件才发送。
信号的特质:
信号是通过软件方法实现的,其实现手段导致信号有很强的延时性,但对于用户来说,这个延时很短并不会被察觉。
注:每个进程的所有信号,都是由内核发送内核处理。
能够产生信号的事件:
一.按键产生。比如我们经常使用的 ctrl+c都是产生一个信号
二.系统调用产生。比如 kill (函数)
三.软件条件产生。比如定时器 alarm
四.硬件产生的异常。比如非法访问内存(段错误)
五.命令产生 ,比如:kill命令
专业术语:
递达:信号产生并且到达;(就是说信号产生并且被成功接收 )
未决:产生和递达之间的状态,主要用于阻塞(屏蔽)导致该状态
未决信号集:就像他的名字一样,这是一个集合,无许且不重复。它存在于进程的pcb中,一个进程的未决信号集是一个类似清空的数组(也就是这个数组全为零)。而且这个数组只有8个字节也就是64位(由于信号的种类只有64种,每种信号对应一个0),当产生信号的时候,pcb会将该信号对应下标的0变为1,说明该信号产生需要被处理。然后如果该信号未被阻塞的情况下,内核会立即对信号作出处理,然后再将信号对应的1变为0。
就像前面说的这是该信号没有被阻塞的情况下,那么阻塞的情况下呢?这时候我们就需要引出另外的一个专业术语----->信号阻塞集(也叫信号屏蔽字)
信号阻塞集:他跟信号未决信号集一样也是一个集合而且也是类似一个64位的数组,存放于pcb中,最开始全为零也就是不对任何信号屏蔽。对于它的作用就是阻值某些信号在未决信号集中从1变成0的过程。而判断阻止哪一种信号的依据就是对应信号阻塞集的0变为1.
总的来说就是,信号阻塞集影响未决信号集。
信号的处理方式:
1.默认处理动作(由内核设定好的)。
2.忽略(丢弃)注意:这是处理的方式为忽略 而不是不处理(注意理解问题)。
3.捕捉(调用信号的处理方式)。
注意一般信号都有三种处理方式可以根据目的做出改变。但是唯独9号信号(SIGKILL)和19号信号(SIGSTOP)只能执行默认处理动作,甚至不能将其设置为阻塞。(毕竟这两个是用来关闭进程的,如果这两个信号都被修改,比如修改为忽略处理,那么内核不就失去管理进程的能力了,也挺容易理解的)
信号的四要素
1.信号的编号
2.信号的名称
3.事件
4.默认处理动作
前三个要素都好理解,这里介绍一下Linux包含的默认处理动作:
Term: 终止进程
Ign:忽略信号(这里指的是对信号的处理方式为忽略)
Core:终止进程,生成一个Core文件。(主要用于gdb调试)
Stop:停止进程
Cont:让停止进程继续执行。
注意 上面五种处理方式相当于处理方式1(默认处理)的子集而不是与处理方式并列。
产生信号:
kill函数:
第一个参数为对应收信号的进程的进程号,第二个参数为信号的编号
(注意一下,第二个参数最好使用宏名而不是使用编号,这有利于跨平台并且可读性增强)
第一个参数:
当pid>0时,表示将信号发送给pid对应进程号的进程
pid=0时,表示将信号发送给和调用kill函数的进程属于同一进程组(下面有解释)的所有进程。
pid<0时,表示将信号发送给-pid对应的进程组。
pid=-1时,表示发送给进程有权限发送的所有系统中所有的进程
(进程组:每个进程都属于一个进程组,进程组是一个或者多个进程集合,他们互相关联,共同实现一个实体任务,每个进程组都有一个组长,默认的组ID和进程组长相同)
raise函数:只有一个参数就是信号的编号,因为他的功能就是给自己发信号,所以不需要传入特定的进程号。
raise(sig)=kill(getpid(),sig);
abort函数:给自己发送异常终止信号,也就是六号信号SIGABRT,并产生Core文件。没有参数没有返回值。
软件条件产生信号:
alarm函数
设置定时器,在指定时间(second)后,内核将给当前进程发送一个 14)SIGALRM信号,进程接收到这信号,默认动作终止。
(注意:每个进程只有一个唯一的定时器)
函数返回值:返回0或者上一次定闹钟还差的时间(就比如你先alarm(5),然后再两秒之后你瞬间int ret =alarm(3);ret=3,就是你定时的时间减去已经执行的两秒)
由于alarm的参数设定的闹钟时间,所以经常使用alarm(0)来结束闹钟。
setitimer函数:
先看man文档:
首先setitimer其实就是对alarm的增强版,也是用到进程中惟一的定时器。
首先第一个参数:
是三个宏用来指定运行方式
① 自然定时:ITIMER_REAL → 14)SIGLARM 计算自然时间
② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间
③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF 计算占用cpu及执行系统调用的时间
(重点掌握第一种即可)
第二个参数是一个结构体,根据const修饰以及是个指针就很容易猜到这是一个传入参数,是微秒级的
这个结构体的两个参数还是结构体,这两个结构体分别表示下一次等待的时间和这一次等待的时间。如果仅用来实现类似alarm的功能就可以直接将第一个参数赋值为零
至于对应的结构体,也就是下面列出来的类型,struct timeval,第一个参数表示秒级第二个参数表示微秒级
第三个参数就是alarm的返回值,是一个传出参数。不过对应的灵敏度也比alarm更灵活
以下是一个用setitimer实现alarm的代码(可以参考使用)
1 #include<stdio.h>
2 #include<iostream>
3 #include<unistd.h>
4 #include<sys/time.h>
5 using namespace std;
6 itimerval it,oldit;
7 int my_alarm(int num){
8 it.it_value.tv_sec=num;
9 it.it_value.tv_usec=0;
10 it.it_interval.tv_sec=0;
11 it.it_interval.tv_usec=0;
12 setitimer(ITIMER_REAL,&it,&oldit);
13 return oldit.it_value.tv_sec;
14 }
15 int main(){
16
17 int temp,i=0;
18 scanf("%d",&temp);
19 printf("start\n");
20 int ans=my_alarm(temp);
21 while(1){
22 printf("%d\n",i++);
23 }
24
25
26 return 0;
27 }
信号集处理:
如何设置一个信号集
sigset_t set;//定义集合
int sigemptyset(sigsset_t *set)//将某个信号集清零
int sigfillset(sigset_t *set)//将某个信号集置为1
int sigaddset(sigset_t *set, int signum);//将某个信号集加入信号集
int sigdelset(sigset_t *set, int signum);//将某个信号从信号集删除
//以上函数都是 成功返回0 失败返回 -1
int sigismember(const sigset_t *set,int signum)//判断是否在信号集合中,在集合返回1,不在返回0,失败返回-1
首先我们的为什么要设定信号集?
是因为我们想对上面讲过的信号屏蔽字进行操作(信号屏蔽字的功能就是影响未决信号集)。来达成我们相应的目的。
那很明显,我们下一步的目的就是如何将信号屏蔽字设置为我们的信号集合。
这时候就需要用到另一个函数:sigprocmask函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
这时候我们应该清楚,我们的信号集合其实就是一种位图,我们对其进行的操作类似于与或运算,对原有的信号屏蔽字进行操作。但如果我们再深入考虑一步的话其实也就是,对信号进行阻塞和撤销阻塞两种操作。对,参数一的功能就是实现这一目的(不过这里提供了第三种就是将原来的信号屏蔽字直接替换为当前传入的信号集合,简单粗暴,爱了爱了~~)。
how参数取值: 假设当前的信号屏蔽字为mask
1.SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
2.SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
3.SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
第二个参数就是一个传入参数,将我们设定的信号集合传入。
第三个参数是一个传出参数,就是将原来的信号屏蔽字导出(如果单纯只想查看一下当前的信号屏蔽字可以利用sigpending(sigset_t *set);,set就是一个传出参数,将原来的信号屏蔽字导出。)
这里给出一个打印信号屏蔽子的代码可以参考一下:
1#include<iostream>
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<signal.h>
5 #include<unistd.h>
6 using namespace std;
7 void sig_show(sigset_t* pset)
8 {
9 for(int i=0;i<32;i++)
10 {
11 if(sigismember(pset,i)==1)
12 {
13 printf("1");
14 }
15 else{
16 cout<<"0";
17 }
18 }
19
20 cout<<endl;
21 }
22 int main()
23 {
24
25 sigset_t set,oldset;
26
27 sigemptyset(&set);
28 sigemptyset(&oldset);
sigaddset(&set,SIGQUIT);//将三号信号屏蔽,可以用ctrl+/触发
31
32 sigprocmask(SIG_BLOCK,&set,&oldset);
33
34 while(1)
35 {
36 sigpending(&set);
37 sig_show(&set);
38 sleep(1);
39 }
40
41 return 0;
42 }
可见当我用ctrl+\产生信号后,信号屏蔽字就发生改变。
信号捕捉函数的注册:
signal函数
这个函数有两个参数,第一个参数为信号的类型,第二个参数为自己写的处理函数(非常容易使用,所以不再赘述)。
sigaction函数 首先看一下man
首先 第一个参数是一个信号的编号,也就是我们想要捕捉的信号,
第二个参数,我们从const 修饰词以及 指针,就很容易判别这是一传入参数。
至于第三个参数,也很容易看出这是一个传出参数用来保留原来的处理方式。
我们了解完功能之后,来深剖一下结构体的各个参数:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
(值得注意的是,这个结构体的名字和函数的名字一样,所以你需要定义类型的时候加上struct,用来区别,否则会报错)
第一个 sa_handler 很熟悉吧,没错这是一个以int类型为参数的函数指针,也就是我们的注册捕捉函数。当你想把该信号的处理方式设为忽略处理,你可以直接将SIG_IGN直接挂上去表示执行忽略,或者SIG_DEF表示执行默认动作。(重点)
第二个参数 当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
第三个参数: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。
(注意这里这个时表示暂时屏蔽。举个例子:就是假如你的处理函数很慢需要执行很长时间,那么在处理的过程中,你不希望被其他的信号打扰,你就可以将不想打扰的信号设置为1,然后函数在执行的时候就可以把这些信号屏蔽,当函数结束的时候才可以处理这些信号)(重点)(如果不理解可以看一下,下面的参考代码)
第四个参数:通常直接设置为零,表示默认属性。(当你执行注册函数的时候假如你捕捉的信号再次到来也不会处理)(重点)
第五个参数:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
1 #include<iostream>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<signal.h>
5 using namespace std;
6 void sol(int sig)
7 {
8 printf("catch\n");
9 sleep(5);//睡五秒来体现sa_mask的作用
10 }
11 int main()
12 { struct sigaction sig,oldsig;
13 sigset_t temp;
14
15
16 sigaddset(&temp,SIGINT);//在执行的时候将SIGINT屏蔽掉
17 sig.sa_mask=temp;
18 sig.sa_handler=sol;
19 sig.sa_flags=0;
20 sigaction(SIGQUIT,&sig,&oldsig);
21 while(1);
22 return 0;
23 }
~
执行结果:
这里解释一下为何这样测试以及为啥会有这样的结果:
首先:我们的捕捉函数是捕捉了SIGQUIT信号可以由 Ctrl+\触发,并且注册的函数为捕捉到就打印catch,sa_mask是屏蔽了可以由Ctrl+c触发的SIGINT信号。
1.我输入Ctrl+,产生了SIGQUIT信号打印了catch这个字符串。
2.我再次输入Ctrl+,再次产生了SIGQUIT信号,但是由于我们采用默认属性所以,Ctrl+\会被屏蔽,不会再执行注册函数。所以没有反应。
3.我又输入Ctrl+c由于,注册函数仍然在执行,而且SIGINT被放在sa_mask里面,所以程序不会对SIGINT做出反应。
4.我第二次输入Ctrl+c时,注册函数已经结束,所以程序响应信号。程序结束。(如果还没理解请在评论区留言)
这里总结一下信号捕捉的特性:
1.进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
2.XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
3.阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
信号一览表
1) SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
- SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动
作为终止进程。 - SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信
号。默认动作为终止进程。 - SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
- SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
- SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
- SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
- SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
- SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
- SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
- SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
- SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
- SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
- SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
- SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
- SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
- SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
- SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
- SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
- SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
- SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
- SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
- SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
- SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
- SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
- SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
- SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
- SIGPWR:关机。默认动作为终止进程。
- SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
- SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
附录:
运行程序的瓶颈在于IO,优化程序的效率优先考虑IO,比如你可以将输出重定向到一个文件,你会发现CPU的效率会很高。
time +./a.out 会统计real:程序实际运行的时间,user: 程序运行在用户空间的时间,sys:程序运行在系统的时间。real=user+sys+等待时间。
告诫:
做个好人,没毛病!