- 一、进程的独立与协作
- 二、进程间的通信
- 三、什么是信号
- 四、信号如何发生
- 五、信号传递
- 六、信号的接收与处理
- 七、中断向量表的由来
- 八、编程控制信号
- 九、特殊的一些信号
- 十、信号的淹没
- 十一、可重入函数
- 十二、mySleep-->模仿sleep()函数
进程在操作系统中运行时,每一个进程都是高度独立和封闭的!但是有些程序问题需要多个进程来协作完成。这时候就出现了一个矛盾。
进程高度独立且封闭,要达成进程的协作,就不得不让进程之间能够进行交流。如何解决?
- 打破进程的独立性和封闭性;这样做带来的后果和风险是:
- 缺乏安全性;
- 进程间的耦合度增加。由此可知该解决方案是完全不可行的。
- 为进程提供若干媒介,进程通过访问这些媒介来达到相互的通信目的,从而实现进程间的协作。
进程间的协作问题就变换成了进程间的通信问题。
进程间的通信有若干手段和方法。常用的方法有:
- 第一个可以使用的进程间通信的重要手段就是:文件系统中的文件;有如下的缺陷:
- 访问规则的控制不明确;
- 进行访问规则约束时并不能达到强制;
- 低速。(CPU>内存>文件)
- 在文件基础上就出现了一种新的方式:管道(特殊的内存区域)。在文件的基础上做了如下的改进。
- 增加了强制的访问规则FIFO;
- 为了提速,用内存模仿文件(用文件的方式操作内存区域)。管道的通信方式的缺陷:不灵活。
- IPC通信方式。提供了三种的进程间通信的模式:
- 共享内存;
- 信号量;
- 消息队列。
使用内存构建一个和所有进程的物理内存独立的一块儿特殊区域。给定3种强制规则来访问这个内存区域;三种规则要完成的通信任务是不一样的。
- 共享内存:在多个进程间共享数据。
- 信号量:标志进程的共享资源的个数,同时给出锁机制。
- 消息队列:完成类似于管道的任务,数据的单向流动。
- 信号通信方式。打断式通信方式,信号又被称之为中断。信号要通信的信息就只有一个:某某事件发生。
信号通信方式。打断式通信方式,信号又被称之为中断。信号要通信的信息就只有一个:某某事件发生。
单纯的信号是没有任何意义的,信号需要和信号处理函数(中断函数)。其意义就是:当某某事件(信号)发生时,则需要处理什么。
中断:打断当前进程正在执行的任务,创建一个断点(保护现场),然后去执行中断信号所对应的中断处理函数,当执行完中断处理函数之后则返回断点(恢复现场),继续执行之前的任务。
中断的一个最大特征:中断处理函数的调用时机是完全不确定的,它取决于中断信号的发生时间,发生次数。
信号的发生分为两类:i>硬件引起的;ii>软件引起的。根据这种特性,将信号分为:软中断和硬中断。
- 硬中断,例如:CPU时间片段,总线的若干中断,磁盘读写中断等等...
- 软中断,例如:段错误中断,终端中断,终止进程运行中断...
- 软中断通过操作系统内核或用户进程所产生。
在用户进程的层面上讲,中断信号产生之后一定要发送给指定的进程!
信号一定是要进程收到然后做相关的处理。
第一步,进程可以选择处理该信号或者屏蔽信号;从这个角度信号又被划分为2种:i>可屏蔽信号(中断),ii>不可屏蔽信号(中断)。
第二步,如果进程选择不屏蔽信号,那么就处理该信号--->也就是调用与其对应的信号处理函数(信号与信号处理函数一一对应)。于是在进程内部就需要一个信号编号与信号处理函数的对应表--->中断向量表。
中断向量表主要包含两样内容:信号的编号和信号处理函数的函数地址。
此外,信号的屏蔽与否是通过一个8个字节64位的信号屏蔽字控制的。每一位都代表一个信号,所以整个进程可以使用的信号只有不到64个可用信号。信号的编号就是对应的二进制的下标。
信号编号 | 函数首地址 |
---|---|
1 | exit |
2 | exit |
... | |
11 | perror()exit() |
... | |
31 | |
34 | |
... | |
64 |
通过kill -l命令可以查看系统中所提供的可用信号的编号。
问题:中断信号为何不设计为可扩展的?
每一个进程都拥有属于自己的中断向量表。每一个进程都可以维护和管理自己的中断向量表。进程在被创建的一开始,就拥有一个完整的中断向量表。一部分信号被预先定义了中断处理函数,例如信号2,其对应的就是exit(0)函数调用。其余的信号没有被预定义中断处理函数,那些预定义终端处理函数的信号对应于是一个类似于空函数的函数。
问题:fork一个子进程时,子进程的中断向量表与父进程是否相同?
子进程会继承父进程的中断向量表,前提是没有调用execv函数族的功能。
如果fork得到的子进程执行了execv函数族的系统调用,则会在execv的执行中将中断向量表重建,根据操作系统内核提供的中断向量表的模板重建。
编程控制信号主要做这几个方面的操作:
- 信号的捕获;
- 信号的产生;
- 信号编程的注意事项。
就是信号到达进程时,进程能够捕获、识别信号,并执行信号对应的处理函数。其实只需要控制进程中的中断向量表即可。
信号达到时,进程立即执行对应的信号处理函数的这个调度过程是由计算机硬件控制的--中断处理芯片。所以在编程的时候,所谓的信号捕获只不过是设置对应信号编号的信号处理函数,即就是修改中断向量表。
编程中只需要使用signal()函数即可。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数:信号的编号,第二个参数:函数指针,执行我们自己写的信号处理函数(就不执行原先系统的信号处理函数)。返回值,保存原先中断向量表的处理函数的地址。
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void catch_SIGINT(int sig){
printf("Ctrl + C has happened, sig=%d\n", sig); //参数就是对应传过来的信号编号
}
int main(void){
signal(SIGINT, catch_SIGINT); //写信号的宏或对应信号的编号都可以。
while(1){
printf("Yes I am still alive. \n");
sleep(5);
}
}
运行结果:
通过编程的方式让一个进程产生的信号是软中断信号。信号产生一定要有一个信号被传递的目标。
信号的产生方法有多个API
函数名称 | 说明 |
---|---|
kill() | int kill(pid_t pid, int sig);向pid指定的进程发送信号sig |
raise() | 给当前进程发送信号等价于kill(getpid(), 信号) |
alarm() | 定时产生一个SIGALRM信号,调用alarm方法之后,只会产生一次该信号。 |
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void catch_SIGINT(int sig){
printf("Ctrl + C has happened, sig=%d\n", sig);
}
void catch_SIGALRM(int sig){
printf("闹铃响了,该起床了\n");
}
int main(void){
signal(SIGINT, catch_SIGINT); //写信号的宏或对应信号的编号都可以。
signal(SIGALRM, catch_SIGALRM);
while(1){
printf("Yes I am still alive. \n");
// kill(getpid(), SIGINT);
raise(SIGINT); //这两个函数在这里等价
alarm(2); //没有写这个的处理函数的话,按照系统的exit(0),将退出当前进程。
sleep(5);
}
}
此时,将会只停留2秒往后循环进行,不在是睡眠5秒。
alarm()有我们自己写的信号处理函数,不然的话,2秒一到,将退出当前进程(系统处理此信号函数为:exit(0))。
SIGCHLD信号,该信号在子进程结束的时候产生,该信号发送给了该子进程的父进程。这是因为操作系统需要让父进程知道子进程的结束。父进程需要在此时调用wait()方法,以回应操作系统,确认已经收到子进程结束的消息
wait()方法只能在具有子进程的进程中使用,如果一个进程没有子进程,调用wait()方法会失败!
wait()方法是用于处理子进程结束状态的,同时明确告知操作系统内核已经处理了子进程的结束,从而使得操作系统能够彻底回收子进程的PCB信息。
但是,wait()方法会导致父进程阻塞停顿,如何解决?
方案:
- 在父进程中捕获SIGCHLD信号,在信号处理函数中调用wait即可(就是当子进程结束时,接收到SIGCHLD信号时,方才调用wait(),父进程此时才阻塞)。这样既可以避免僵尸进程的产生,又可以父进程不被阻塞。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void catch_SIGCHLD(int sig){
printf("one child has gone pid = [%d], %d\n", getpid(), sig);
int ret = wait(NULL);
if(ret < 0){
perror("");
}
}
int main(void){
pid_t pid;
signal(SIGCHLD, catch_SIGCHLD);//父子进程都可以捕获该信号
pid = fork();
if(pid == 0){
printf("This is child %d\n", getpid());
sleep(3);
}else if(pid > 0){
while(1){
printf("This is father %d\n", getpid());
sleep(1);
}
}else{
perror("");
}
return 0;
}
运行结果
- 没有子进程时调用wait()方法。
- wait()方法写在信号处理函数中,解决父进程的阻塞(此时不会产生僵尸进程)。
子进程的结束,发送信号SIGCHLD被父进程所捕获。
信号产生并发送给一个进程之后,能够提供给进程处理该信号的时间非常短,只有几个CPU时钟周期。如果在这个十分有限的时间内,进程没有明确的处理该信号,则这个信号就会立即消失。
当某一个信号被捕获并正在执行的时候,相同的该信号就不能立即被触发,若正在处理该信号时确实有产生了该信号,则新的信号将被抛弃。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void catch_SIGUSR1(int sig){
printf("This is %d signal \n", sig);
sleep(1);
}
int main(void){
signal(SIGUSR1, catch_SIGUSR1);
int i = 0;
pid_t pid;
pid = fork();
if(pid == 0){
sleep(1);
for(; i < 50; i++){
kill(getppid(), SIGUSR1);
}
}else{
while(1){
sleep(1);
}
}
return 0;
运行结果:
50次结果的信号捕获并打印在屏幕上,信号被淹没了,所有只打印了一次。
在信号面前,信号具有2种类型,可重入函数和非可重入函数。最经典的非可重入函数就是malloc();
假设程序中调用malloc()方法,当正在调用malloc()的时候发生了中断,此时就需要执行该中断对应的处理函数,巧合的是,中断处理函数中也调用了malloc()函数,此时是否有问题?
重入性:因为中断的存在,以及中断发生时机的随机性,可能出现这样的局面:某一个函数在还没有调用完成的时候,在中断处理函数中又将该函数调用一次(相当于,在该函数中把自己又调用了一次)。
在这种情况下,某些函数因为这种局面变的不稳定和容易出错,这种函数就是不可重入的函数。
在这种情况下,某些函数可以适应这种局面,这种函数就是可重入函数。
实现代码:
include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
void catch_SIGALRM(int sig){ //捕获alarm信号的函数处理
}
void mySleep(int seconds){
alarm(seconds); //写上alarm信号的捕获函数,不然调用系统的exit(0)将会退出。
pause(); //暂停
}
int main(void){
signal(SIGALRM, catch_SIGALRM);
while(1){
printf("will sleep seconds by mySleep\n");
mySleep(1);
}
return 0;
}
运行结果:
这个模仿sleep()函数漏洞很多的。