一、信号的捕捉处理
信号保存后会在合适的时间进行处理;
1.1信号处理时间
进程会在操作系统的调度下处理信号,操作系统只管发信号,即信号处理是由进程完成的;
1.信号处理首先进程得检查是否有信号;2.进程要处于内核状态才能处理信号;
即进程会在内核态返回用户态的时候检查并处理信号;
对于程序的执行有一部分是自己写的,有一部分是库提供的,还有一部分是操作系统提供的;执行操作系统提供的代码需要进行身份的切换,执行自己写的和库提供的一般是以用户态的身份执行,执行系统调用,或者是进入操作系统内部(如硬件中断,根据中断号执行内核的终端表方法)需要以内核态的身份进行;
操作系统可以响应外部硬件的中断,也可以响应内部软件产生的中断;int 80(是一条汇编也是CPU可以认识的指令)就是一种软件中断,功能是让进程从用户态陷入内核态;
总结:程序运行时,会从代码区开始执行,执行到函数调用时触发int 80将ecs后两位由11变成00,进入内核态,根据用户级页表映射进入内核空间执行代码,根据内核级页表映射到内存空间,执行代码,返回时要先执行一次do signal(当前进程对信号做一次检测,对pending表,block表的检测),如果不需要处理,直接变成用户态,将ecs寄存器的后两位由00变成11,并且返回到原先用户空间执行代码处;否则先将pending表对应信号位置零,对于忽略方式直接返回,默认方式直接执行,都是在内核空间处理,而对于自定义处理需要先将身份转换为用户态(因为操作系统不信任用户的代码,有风险)然后执行完使用sigreturn(使用函数压栈的方式传入的此函数,所以可以执行)返回内核态跳转之前的位置,然后在跳转回用户态执行的地方;
要注意main和sighandler是两个不同的执行流不是调用和被调用的关系;
对于自定义信号处理进行了4次身份切换,两次信号的检测,而默认和忽略方式处理信号只是进行了2次身份切换,1次信号检测;
由于进程是会被调度的,进程上下文的恢复和进程调度的实现都是在内核空间的,所以一定会由频繁的身份切换,所以一定会对信号进行检测;
1.2信号处理接口
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//既可以处理普通信号也可以处理实时信号
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);//不关心,处理实时信号
sigset_t sa_mask;//,除了当前信号自动屏蔽,还可以选择将多个信号屏蔽
int sa_flags;//不关心,默认设为0
void (*sa_restorer)(void);//不关心
};
//与signal使用类似只不过需要传入结构体对象;
信号处理前会先将pending位图对应位置的1置为0;
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
上述方式可以防止信号捕捉函数被重复调用;
二、补充话题
2.1可重入函数
当头插节点时,有三个位置newnode1,prev,cur三个节点指针,刚完成刚完成newnode1和cur节点的链接,还没有执行prev和newnode1的链接,因为信号的处理跳转到新的执行流,刚好信号处理也是头插,使用的是newnode2,这次执行是完整的,返回用户态是继续执行会将指向newnode2的prev指向了newnode1,这样会导致newnode2丢失导致内存泄漏;
如上现象就是函数被重入了,对于函数如果重入了出错,则该函数就是不可重入函数,反之就是可重入函数;
信号处理和main执行流并不是和多线程一样一旦创建线程执行流就开始运行,并且和main执行流是并行的,而是取决于信号并且执行信号流时会使得main执行流被暂停,是一个进程的不同执行流;
2.2volatile
volatile作用是保持内存可见性;进行运算(算术运算或者逻辑运算),都会进入CPU的运算器进行;
在优化条件下如果只是对变量读取不进行修改,可能变量会被优化到寄存器里面,而不是内存中,这样优化是的不需要进行访存,提高了效率;
如:设置全局变量,main执行流只是进行了读取,而信号执行流进行了写入,这时候编译器优化,main执行流不从内存中读取,而是从寄存器读取,但是寄存器中的内容并不会被修改,信号执行流进行了写入,内存中实实在在的被修改了;这时候就会导致全局变量即使被修改了,但是main在执行流不可见;所以需要在全局变量前加volatile关键字修饰,保持内存的可见性,使得main执行流从内存中进行读取;
register是一个建议性关键字用来进行优化,还是会创建变量,但是可能会将变量内容放到寄存器;
2.2.1Linux gcc/g++优化
gcc -O .c文件
#选项包括-O0到-O3;0表示没有优化,1-3从低到高进行优化;
2.3SIGCHID
子进程退出时,父进程必须进行等待,否则子进程就会变成僵尸状态;
等待的目的:1.使得子进程可以被回收;2.获得子进程的退出信息;3.由于子进程的退出是未知的,所以父进程需要阻塞或者是非阻塞的方式进行等待,要保证父进程是最后一个退出的;
子进程并不是直接就退出了,而是会向父进程发送信号的;此信号就叫做SIGCHID(17号)信号;
进程等待可以采用基于信号的方式实现异步等待;要保证父进程在这期间是一直运行的;
使用waitpid非阻塞的方式或者是基于信号的等待都可以使得父进程继续运行,不用阻塞;
关于多个子进程等待,使用-1来接受任意多个子进程,使用WNOHANG防止等待时,有进程不退出导致阻塞;
对于信号实现子进程的等待也可以不调用waitpid;事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用;
17号信号的默认处理方式是忽略,而忽略方式是自动清理子进程;