信号的捕捉
在信号的相关概念中曾提到如果一个信号没有被Block,但被Pending,但不会立即递达,而是在合适的时候,这里的合适的时候是指:当进程从内核态返回用户态时,会对信息进行检测处理。
正如下图所示,是系统在对信号进行捕捉时经历的过程:
可以简化为:
首先先来介绍一下什么是信号捕捉:
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为信号捕捉那么内核是如何实现信号捕捉的呢??
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这是发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,他们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。sigaction函数
头文件:#include<signal.h>
函数原型:int sigaction(int signo,const struct sigaction *act,struct sigaction *oact);
函数功能:可以读取和修改与指定信号相关联的处理动作。
返回值:调用成功返回0,出错返回-1。
参数:signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体
struct sigaction结构体
struct sigaction {
void (*sa_handler)(int);
//老类型的信号函数处理指针
void (*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号函数处理指针
sigset_t sa_mask;//将要被阻塞的信号集合
int sa_flags;//信号处理方式掩码
void (*sa_restorer)(void);//保留,不要使用
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点掌握:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数),用于指向原型为void handler(int)的信号处理函数地址。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。
- pause函数
头文件:#include<unsitd.h>
函数原型:int pause(void);
函数功能:使调用进程挂起直到有信号递达
如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作默认是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数后pause返回-1,errno设置为EINTR(被信号中断),所以pause只有出错的返回值
什么叫把进程(PCB)挂起??
把进程状态设置为T状态,把进程放到等待队列里
挂起不能被调度,只有R状态才能被调度
下面我们用alarm和pause实现mysleep函数
思想:
1.首先main函数调用mysleep函数,后者调用sigaction注册了SIGALAM信号的处理函数sig_alrm
2.调用alarm(n)设定闹钟
3.调用pause等待,内核切换到别的进程运行
4.n秒后,闹钟超时,内核发送SIGALRM信号给该进程
5.从内核态返回用户态之前处理未决信号,发现有SIGALRM信号,处理函数是sig_alrm
6.切换到用户态执行sig_alrm函数,进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程
7.pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理动作代码实现如下
#include <stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alarm(int signo)
{
(void)signo;
}
unsigned int mysleep(unsigned int n)
{
struct sigaction New,Old;
unsigned int unslept = 0;
New.sa_handler = sig_alarm;
sigemptyset(&New.sa_mask);
New.sa_flags = 0;
sigaction(SIGALRM,&New,&Old);//注册信号处理函数
alarm(n);//设置闹钟
pause();
unslept = alarm(0);//清空闹钟
sigaction(SIGALRM,&Old,NULL);//恢复默认信号处理动作
return unslept;
}
int main()
{
while(1)
{
mysleep(3);
printf("hello world!\n");
}
return 0;
}
- 运行结果如图:
但是系统运行的时序并不像我们写程序时所设想的那样。虽然alarm(n)紧接进行pause(),但是无法保证pause()一定会在调用alarm(n)之后的n秒之内被调用。由于异步事件在任何时候都有可能发生,如果我们写程序时考虑不周全,就可能由于时序问题而导致错误,这叫做竞态条件。
解决以上问题的方法就是将“解除信号屏蔽”和“挂起等待信号”这两步合并成一个原子操作,这是就要使用sigsuspend函数了。
sigsuspend函数
头文件:#include<signal.h>
函数原型:int sigsuspend(const sigset_t *sigmask)
函数功能:解除对信号的屏蔽并挂起
可重入函数:
首先解释一下什么是重入:
当一个函数被不同的执行流程控制,有可能在第一次调用还没返回时就再次进入此函数,就称为重入
- 那什么样的函数是可重入的呢??
如果一个函数只访问自己的局部变量或参数,数据和状态不会被破坏,则称为可重入函数。反知,如果一个函数访问全局链表,有可能因为重入而发生错乱,不能保证函数的行为一致和结果相同,像这样的函数就是不可重入的
可重入和线程安全是两个不同的概念:可重入函数一定是线程安全的;线程安全的函数可能是重入的,也可能是不重入的;线程不安全的函数一定是不可重入的
为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
在线程之中,线程虽然强调资源共享,但是他们的栈却是独有的,所以访问它的同一个局部变量或参数就不会造成错乱。
如果一个函数满足一下条件之一就是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的
2.调用了标准I/O库函数,标准I/O库的实现都以不可重入的方式使用全局数据结构
3.进行了浮点运算,许多处理器/编译器,浮点运算都是不可重入的