我们写个简单的死循环代码:
1 #include
2 #include
3 using namespace std;
4
5 int main()
6 {
7 while(1)
8 {
9 cout<<"This is signal"<<endl;
10 sleep(1);
11 }
12 return 0;
13 }
为什么我们按下键盘的Ctrl+c就终止掉程序了呢?
我们按下Ctrl+c时键盘输入产生一个硬件中断,被操作系统获取,然后操作系统解释成信号(2号信号)发送给进程,进程收到2号信号后退出。我们可以用signal函数来测试进程是不是收到了2号信号。使用该函数要传入2个参数,第1个参数是信号编号,第2个参数是你要怎么处理这个信号。
确实是收到了2号信号,但是为什么没有退出呢?因为我们把它默认退出改成了打印。
Ctrl+c只能发送给前台进程。
注意:系统当中,打开一个终端一个bash中只允许有一个前台进程。
把进程放在后台运行命令:
要运行的程序名+&
前台进程和后台进程的区别
前台进程我们敲命令是没用的,可以终止进程
后台进程我们输入命令还可以执行,但是却无法用Ctrl+c干掉。
干掉后台进程可以用fg转成前台进程在干掉,或者用kill + pid干掉它。
这样就干掉了后台进程。
信号是进程之间事件异步通知的一种方式,属于软中断。俗话说就是通知事件发生。
kill-l //查看信号列表
信号处理方式有3种:
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump,也就是核心转储功能。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
用命令查看资源限制:
ulimit -a
默认core文件是关闭的,为了测试我们在云服务中开启coer文件,用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K
ulimit -c 1024
10 int main()
11 {
12
13 while(1)
14 {
15 cout<<"This is signal"<<endl;
16 sleep(1);
17 }
18 return 0;
19 }
ctrl+c和ctrl+\都可以终止掉进程,但我们打开core,ctrl+\产生了core dumped文件
ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。
10 int main()
11 {
12 // signal(2,handler);
13 int a = 10/0;
14 return 0;
15 }
我们来个整数除0,使用一下core文件
core-file core.+进程pid
使用调试时要加上-g选项,Liunx用的是release版本
核心转储功能:我们可以通过调试找到代码的问题所在。这种调试这叫做事后调试。
通过kill命令向进程发送信号
kill -l +信号 +进程pid
kill -信号编号 +进程pid
kill命令是通过调用kill函数实现的
函数原型:
int kill(pid_t pid, int signo);
成功返回0,失败返回-1.
6 void handler(int sig)
7 {
8 printf("catch a sig:%d\n",sig);
9 }
10
11 int main(int argc,char* argv[])
12 {
13 if(argc == 3)
14 {
15 kill(atoi(argv[1]),atoi(argv[2]));
16 }
17 return 0;
18 }
rasie函数
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
成功返回0,错误返回-1.
1 #include
2 #include
3 #include
4 #include
5 void handler(int sig)
6 {
7 printf("catch a sig:%d\n",sig);
8 }
9
10
11 int main()
12 {
13
14 signal(3,handler);
15 while(1)
16 {
17 raise(3);
18 sleep(1);
19 }
20 return 0;
21 }
abort函数使当前进程接收到信号而异常终止。
函数原型:
#include
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值。
1 #include
2 #include
3 #include
4 #include
5 #include
6 void handler(int sig)
7 {
8 printf("catch a sig:%d\n",sig);
9 }
10
11
12 int main()
13 {
14
15 signal(6,handler);
16 while(1)
17 {
18 abort();
19 sleep(1);
20 }
21 return 0;
22 }
SIGPIPE是一种由软件条件产生的信号,在“管道”中遇见过了。
alarm函数
#include
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
11 int main()
12 {
13 int count = 0;
14 alarm(1);
15 while(1)
16 {
17 count++;
18 printf("count:%d\n",count);
19 }
20
21 return 0;
22 }
到4万多就退了,而我们的cpu运算是很快的,我们的代码是++一次打印到屏幕一次涉及到IO,效率就会很低,我们可以等+的时间到了在打印数据,定义一个全局的count++。
1 #include
2 #include
3 #include
4 #include
5 #include
6 int count = 0;
7 void handler(int sig)
8 {
9 printf("catch a sig:%d\n",sig);
10 printf("count=%d\n",count);
11 exit(0);
12 }
13
14 int main()
15 {
16 signal(SIGALRM,handler);
17 alarm(1);
18 while(1)
19 {
20 count++;
21 }
22
23 return 0;
24 }
这次的count就非常大了,达到了4亿多,足以看出IO的效率很低。
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
14 int main()
15 {
16 int *p;
17 *p = 10;
18 return 0;
19 }
pending和block都是位图,有32个比特位。
pending位图保存信号,有2个意思。是谁?是否?是谁表示是哪个信号,是否收到。block位图记录信号被屏蔽的信息。它们2个比特位的位置是一样的,但是比特位的内容是不一样的。handler是数组,数组内容是函数指针,自己的函数地址填入叫做自定义捕捉信号。
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
也就是说我们可以用sigset来设置信号操作,但是不建议使用,建议使用下面的函数进行操作。
下面的一系列函数供我们来操作。
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
成功返回0,出错返回-1。
参数说明:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
注意:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
int sigpending(sigset_t *set);
做一个测试:
1.利用上述的函数将2号信号屏蔽
2.向进程发送2号信号
3.此时2号信号被屏蔽,处于pending状态
4.通过sigpending函数来读取pending信号集来查看验证。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 #include<stdlib.h>
6
7 void printsigset(sigset_t *set)
8 {
9 int i = 1;
10 for(;i<32;++i)
11 {
12 if(sigismember(set,i))
13 {
14 printf("1");
15 }
16 else
17 {
18 printf("0");
19 }
20 }
21 printf("\n");
22 }
23
24 int main()
25 {
26 sigset_t set,oset;
27 sigemptyset(&set);//初始化信号集对象
28 sigemptyset(&oset);
29 sigaddset(&set,SIGINT);//发送2号信号
30
31 sigprocmask(SIG_BLOCK,&set,NULL);//阻塞2号信号
32 sigset_t pending;
33 sigemptyset(&pending);//pending位图置空
34
35 while(1)
36 {
37 sigpending(&pending);//获取未决信号集
38 printsigset(&pending);
39 sleep(1);
40 }
41 return 0;
42
43 }
效果如下:
一开始是没有收到任何信号的,当给它发送2号信号,第2个比特位由0变成了1。为了看到2号信号递达后pending的变化,我们可以设置一段时间后解除对2号信号的屏蔽,并且我们对2号信号进行捕捉自定义执行我们自己的动作。
效果如下:
当解除2号信号时,它执行我们自定义的动作,第2个比特位也从1变为了0.
(在32位下)程序地址空间中有1-3GB是用户区,3-4GB是内核区。我们的进程映射的物理空间用的用户级页表,每个进程都会有自己的用户级页表。内核区用的是内核级页表映射达到物理内存,所有的进程用的是同样一张的内核页表。用户是没有权限随意的访问系统的代码和数据的。
一个信号被递达:是在内核态切换到用户态是进行相关检测的。
那内核是怎么捕捉信号的呢?
为了方便好记可以画个简化的图:
就像数学中的正无穷的符号差不多,但是相交的点是在信号检测,是在内核区的。和横线的4个交点就说明进行了4的状态切换。
信号到用户自定义的函数,为什么切换到用户在执行呢?内核是由权限执行用户的代码
因为如果是非法的代码由内核来执行就会容易中病毒,因为内核具有高的权限的,所以系统进行切换到用户区执行,用户态的权限是微小的。
还可以用sigaction函数进行信号捕捉。
函数原型:
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数说明:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
第2个和第5个成员是关于实时信号我们不用管。
sa_handler:收到信号,做什么动作
sa_mask:要屏蔽的信号,默认为0
sa_flags:默认设为0
来个例子测试一手:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 #include<stdlib.h>
6
7 void handler(int sig)
8 {
9 printf("get a sig:%d\n",sig);
10 }
11
12
13 int main()
14 {
15 struct sigaction act,oact;
16
17 act.sa_handler = handler;
18 act.sa_flags = 0;
19 sigemptyset(&act.sa_mask);
20
21 sigaction(2,&act,&oact);
22 while(1)
23 {
24 printf("i am pid\n");
25 sleep(1);
26 }
27 return 0;
28 }
我们按下Ctrl+c,进程收到了2号信号。由于是我们自定义处理所以它没有退出。
当我们插入链表时,先插入node1,刚让node1->next指向新节点时候来了一个信号,这个信号也是让我们进行插入操作。此时从用户态到内核态中处理,插入完毕后回到用户态回到main函数里继续执行插入操作。此时head从指向node2变成了了指向node1,但是node2却被丢了造成了内存泄漏问题。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
当一个函数符合以下条件之一则是不可重入的:
先来看一段代码:
1 #include
2 #include
3 #include
4 #include
5
6 int flag= 0;
7
8 void handler(int sig)
9 {
10 printf("flag is to 1\n");
11 flag = 1;
12 }
13 int main()
14 {
15
16 signal(2,handler);
17 while(!flag);
18 printf("i am quit!\n");
19
20 return 0;
21 }
定义1个全局flag变量,不发送2号信号会在死循环,当我们按下Ctrl+c是进程退出。
我们在编译是加上-O2选项
我们按Ctrl+c但是进程却没有退出。我们加了选项把flag的值优化到了CPU的寄存器中,while循环检查的flag不是内存中最新的flag,就会出现二义性的问题。此时就要用volatile。
即使有优化,当我们按Ctrl+c时进程退出了。
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
本篇文章到这就已结束了。