信号的基本概念
在Linux环境下,有一重要概念信号(signal),说它重要是因为它在进程管理中占有着相当重要的低位。整个进程之间的管理切换,都是通过信号的方式实现的。首先举个简单例子。
生活中,当我们过马路时,总是可以看到交通灯,从小耳熟能详的一局童谣“红灯停,绿灯行,黄灯亮了等一等”,遇到红灯,我们都会下意识地等待,绿灯亮起,我们会选择穿过马路,这就是一种信号。暂且不讨论闯红灯的行为。考虑一下,为什么我们会这么做?红绿灯给我们的只有颜色上的信号,我们却做出了一系列的反应,并不是因为红灯有多么大的能力,而是在我们脑海中有这样的一种意识,看到它之后我们就理所当然的要这样。
信号也是这样,在Linux操作系统中,进程的控制大都需要信号来实现,并不是信号指使进程进行某一项动作,而是进程在信号来临之前就已经知道了,如果遇到某个信号该怎么做。有了这个概念之后,我们从操作系统的角度来看看,信号是如何工作的。
首先要得到一个信号,这由终端的命令或程序代码可以实现,不是我们讨论的重点,这些命令或函数被执行之后,根据冯诺依曼体系结构,首先输入的信息要传递给内存,这时CPU从原来的用户态切换为内核态,分析解释该信息,得到信号,接下来这些信号会保存在该进程的PCB当中(注意,这个很重要!!!)。等待CPU从内核态转换为用户态,重新处理该进程时,会首先处理PCB中记录的信号,这个和线程切换有点类似,发现待处理的信号,接下来就会去执行它,从而使进程产生相应的行为。
了解了这些东西之后,我们需要从宏观角度来认识一下信号,信号是一种通知机制,告诉进程某些事情发生了,进程针对信号产生特定的行为。(进程对这些信号会产生的默认行为是已知的)同时信号的产生是异步产生的,完全随机,可能在进程运行过程中的任何时间。
之前谈进程的时候,说过kill命令,这个后面会用到
kill -l # 查看当前系统所有可用信号
信号产生的条件:
1、终端组合键,只能产生少量信号,仅适用于前台进程
2、硬件异常,硬件检测并通知内核
3、软件方式,指令或函数接口。kill命令(例闹钟超时SIGALRM信号)
关于条件2,要多说一点的是,硬件异常产生的信号,由操作系统解释,例如除0操作产生SIGFPE,访问非法内存地址;还有就是MMU内存管理单元异常。MMU是用来结合页表进行虚拟地址到物理地址转化,与MMU搭配使用的还有TLB, 做后备缓存,用来缓存映射之后的硬件缓存结果。
那当进程得到信号之后会怎么处理呢?还是当我们遇到交通灯一样,遇到红灯,每个人都知道要停下来,默认的动作应该是在原地等待,但依旧会有些人闯红灯,同时,有些人没有老老实实地在等,而是在打电话,进程也一样,有三种处理方式:
信号处理方式:
1、忽略信号
2、执行信号默认动作
3、自定义动作, 提供一个信号处理函数,也叫作信号捕捉(catch)【使用signal函数,后面会提到】
关于如何去实现,后面会提到。
信号的产生
在Linux下信号的产生主要有三种方式。
1、终端通过按键组合键产生
常见的组合键有以下几个:
ctrl+c:SIGINT(2) 终止前台进程 ctrl+z:SIGSTOP(19) 停止前台进程,同时将该进程放到后台 ctrl+\:SIGQUIT(3) 终止进程并Core Dump
这里多说一点关于core dump的东西。Core Dump, 即核心转储,当一个进程被异常终止时,可以选择性的将用户空间的内存数据全部保存到磁盘上,默认在当前路径下生成一个文件名为 core.****的文件,****通常为进程ID,在运行结束之后,可以使用gdb进行调试,找到异常所在。默认情况下是不会产生core文件的,原因有两个,一是容易将用户的私人信息也保存到了磁盘上,造成信息的不安全,二就是如果在用户不知情的情况下,产生core文件,会占用磁盘大量空间,因此即使是在实际开发中,core dump的使用也很少见。接下来给出两天命令,关于查看和调整core文件的属性
# 查看系统资源上限: [root@localhost mySemaphore]# ulimit -a # 更改core文件大小上限: [root@localhost mySemaphore]# ulimit -c 1024 # 以block为基本单位 使用gdb调试,-g编译选项 (gdb) core-file core文件
使用ulimit命令改变了shell的ResourceLimit,而当前进程的PCB有shell复制而来,所以也就具有了和shell相同的Resource Limit值,从而产生了core dump。
core测试代码:
//test.c #include#include int main() { int count = 0; while(1) { printf("hello world\n"); if(count == 5){ int c = 3/0; } count++; sleep(1); } return 0; }
2、 调用系统函数想进程发送信号
这里我们会提到三个函数 kill,raise, bort
#include#include int kill(pid_t pid, int sig); # 给一个指定的进程发送指定信号(成功返回0, 失败返回-1) #include int raise(int sig); # 给自己发送指定信号,用父进程wait验证(成功返回0, 失败返回-1) #include void bort(void); # 使当前进程异常终止,abort函数总是成功的,所以没有返回值
关于kill 函数中的进程ID,和信号,我们可以通过命令行参数的方式获得,代码测试如下:
kill测试代码:
// 终端1 // mysignal.c #include#include int main(int argc, char* argv[]) { if(argc != 3){ printf("Isn't form: ./file pid signo \n"); return 1; } else{ int pid = atoi(argv[1]); int sig = atoi(argv[2]); kill(pid, sig); } return 0; } //终端2 // test.c #include int main() { while(1); return 0; } // 终端3 [muhui@bogon ~]$ ps aux | grep test // 执行顺序 先执行test,终端3使用命令查看test进程id 然后执行mysignal, [muhui@bogon mysignal]$ ./mysignal 3773 9 观察终端2的显示结果,并再次运行终端3的执行
raise测试代码:
// mysignal.c #include#include #include int main(int argc, char* argv[]) { int count = 0; while(1){ printf("hello world\n"); if(count == 5) raise(9); sleep(1); count++; } return 0; }
abort测试代码:
// mysignal.c #include#include #include #include int main(int argc, char* argv[]) { int count = 0; while(1){ printf("hello world\n"); if(count == 5) abort(); sleep(1); count++; } return 0; }
3、软件条件产生信号
关于这种信号的产生方式,我们以SIGALRM信号为例。
SIGALRM是14号信号,也叫闹钟信号,软件通过alarm函数产生,可以以秒为单位进行定时,当时间到达之后,终止进程,alarm函数定义如下:
#includeunsigned int alarm(unsigned int seconds); # 在seconds秒之后给当前进程发送SIGALRM信号 # 传入0,停止闹钟 # 返回值为0,或剩余秒数 # 默认处理动作是终止当前进程
alarm测试代码:
// myalarm.c // 测试一秒钟能打印多少次 #include#include int main() { int count = 0; alarm(1); while(1){ printf("count = %d\n", count++); } return 0; }
然后呢,这里就要提到上面的一个函数,signal函数,用来自定义进程对信号的动作,网上很多人都把这个函数当做一个单独的模块来讲,这里我用一种比较简单的方式,尽量最容易地说清楚这个函数。
首先看函数的定义:
#includetypedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); # 接收信号之后自定义动作 # 参数1,要处理的信号 # 参数2,通常有三种形式 SIG_ING,表示忽略前面的信号,即没有动作。 SIG_DFL,表示执行该信号的默认动作,换句话说,如果使用这个选项,完全可以 不适用这个函数,因为本身就是执行默认动作 自定义函数名,即函数指针
接下来看一段基于上面alarm函数的测试代码。
// my_alarm.c #include#include #include int count = 0; void my_sig(int) { alarm(1); printf("count = %d\n", count); } int main() { signal(SIGALRM, my_sig); // 首先要声明该信号的处理方式,这是使用自定义函数 alarm(1); while(1){ count++; } return 0; }
我们可以发现,alarm是一次性定时闹钟,一次结束之后,如果没有特殊声明,进程直接终止。
对比两次输出的count,会发现相差数万被,这里就体现出了I/O速度和内存的速度之间的差距。
我们可以试着将上面自定义函数中的alarm(1);去掉,代码如下:
#include#include #include int count = 0; void my_sig(int i) { printf("count = %d\n", count); } int main() { signal(SIGALRM, my_sig); alarm(1); while(1) { count++; } return 0; }
运行结果我们会发现,打印一次之后,进程会卡住不再运行,当我们在另一个终端下执行ps命令,会看到
[muhui@bogon my_alarm]$ ps aux | grep myalarm
muhui 3024 87.0 0.0 1872 376 pts/0 R+ 09:03 0:04 ./myalarm
muhui 3026 0.0 0.0 5984 728 pts/1 S+ 09:03 0:00 grep myalarm
进程一直在运行!!
不要想的太多,这里自定义行为之后,并不是就卡在了自定义函数中,而是执行完毕自定义函数,就又调回原来程序跳出的地方,继续执行原来的while(1),打破了默认的退出当前进程的行为。
关于本文中所有的源码,全部打包上传,下载连接:
https://github.com/muhuizz/Linux/tree/master/Linux%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B/%E4%BF%A1%E5%8F%B7/code
--------muhuizz整理