信号是Linux进程间通信的最古老的一种方式。信号是软件中断,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某个突发事件。
一旦产生信号,就要执行信号处理函数,处理完信号处理函数,再回来执行主函数,这就是中断。
一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号的进程中的销毁,执行信号处理函数。
注意:这里信号的产生、注册、注销都是信号的内部机制,而不是信号的函数实现。
可以通过kill -l
命令查看系统定义的信号列表:
可以看到,不存在编号为0的信号。其中1-31号信号成为常规信号(也叫普通信号和标准信号),34-64称为实时信号,驱动编程与硬件相关。名字上差别不大,而前32个名字各不相同。
如果你想了解某个信号的产生条件和默认处理动作,可以通过指令man signal_id signal
例如: man 2 signal 就是查看二号信号的作用
可以看到有一些信号具有三个“value”,第一个值通常对alpha和sparc架构有用,中间值针对x86和arm架构,最后一个应用于mips架构。
不同的操作系统定义了不同的系统信号,这里我们之研究linux系统中的信号。
Action为默认动作:
当用户按下某些终端按键的时候,将产生信号
Ctrl C
可以发送2号信号(SIG_INT),默认处理动作是终止进程Ctrl \
,发送3号信号(SIG_QUIT),默认处理动作是终止进程并且Core DumpCtrl z
查安生终端信号SIGSTOP ,默认动作是暂停进程的执行Core Dump是什么?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。我们可以通过使用gdb调试查看core文件查看进程退出的原因,这也叫事后调试。
下面介绍三个系统函数
(1)kill函数
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
功能:给指定进程发送指定信号(不一定杀死)
参数:
pid:取值有4种情况:
pid > 0:将信号传送给进程ID为pid的进程。
pid = 0:将信号传送给当前进程所在进程组中的所有进程。
pid = -1:将信号传送给系统内所有的进程。
pid < -1:将信号传给指定进程组的所有进程。这个进程组号等于pid的绝对值。
sig:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过
命令kill -l进行相应查看。不准荐直接使数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
返回值.
成功:0
失败:-1
代码示例:
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid;
//创建一个子进程
pid = fork();
{
printf("child process do work.....\n");
sleep(1);
}
exit(0);//子进程退出
}
else
{
//父进程
sleep(3);
printf("子进程不听话了,该退出了....\n");
kill(pid,15);
printf("父进程该结束了,已经完成了它的使命\n");
}
return 0;
}
运行结果如下:
(2)raise函数
#include<signal.h>
int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于kill(getpid(),sig)
参数:
sig:信号编号
返回值:成功:0
失败:非0值
代码示例:
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int i = 1;
while(1)
{
printf("do working %d\n",i);
//给自己发送一个信号
if(i == 4)
{
//自己给自己发送编号为15的信号
raise(SIGTERM);
}
i ++;
sleep(1);
}
return 0;
}
运行结果如下:
(3)abort函数
#include<stdlib.h>
void abort(void);
功能:给自己发送异常终止信号6)SIGABRT,并且产生core文件,等价于kill(getpid(),SIGABRT);
参数:无
返回值:无
代码示例:
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int i = 1;
while(1)
{
printf("do working %d\n",i);
//给自己发送一个信号
if(i == 4)
{
//给自己发送一个编号为6的信号,默认的行为就是终止进程
abort();
}
i ++;
sleep(1);
}
return 0;
}
运行结果如下:
在上一篇博客介绍过,管道如果读端不读了,存储系统会发生SIGPIPE 信号给写端进程,终止进程。这个信号就是由一种软件条件产生的,这里再介绍一种由软件条件产生的信号SIGALRM(时钟信号)。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
功能:
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号,进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
取消定时器alarm(0),返回旧闹钟余下秒数。
参数:
seconds:指定的时间,以秒为单位
返回值:
返回0或剩余的秒数
代码示例:
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
unsigned int ret = 0;
//第一次设置闹钟5秒之后就超时 发送对应的信号
ret = alarm(5);
printf("上一次闹钟剩下的时间是%u\n",ret);
sleep(2);
//之前没有超时的闹钟被新的闹钟给覆盖
ret = alarm(4);
printf("上一次闹钟剩下的时间是%u\n",ret);
printf("按下任意键继续...");
getchar();
return 0;
}
运行结果:
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
这里给大家介绍两个硬件异常:CPU产生异常 和 MMU产生异常
CPU产生异常 发生除零错误,CPU运行单元会产生异常,内核将这个异常解释为信号,最后OS发送SIGFPE信号给进程。
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
// 由软件条件产生信号 alarm函数和SIGPIPE
// CPU运算单元产生异常,内核将这个异常处理为SIGFPE信号发送给进程
int a = 10;
int b = 0;
printf("%d", a/b);
return 0;
}
运行结果小伙伴们自己下去运行一下吧~这里就不截图了
MMU产生异常: 当进程访问非法地址时,mmu想通过页表映射来将虚拟转换为物理地址,此时发现页表中不存在该虚拟地址,此时会产生异常,然后OS将异常解释为SIGSEGV信号,然后发送给进程
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
// MMU硬件产生异常,内核将这个异常处理为SIGSEGV信号发送给进程
signal(11, handler);
int* p = NULL;
printf("%d\n", *p);
return 0;
}
#include<signal.h>
typedef void(*sighandler_t)(int);//它定义了一个类型sighandler_t,表示指向返回值为void型(参数为int型)的函数(的)指针
sighandler_t signal(int signum,sighandler_t handler);
功能:
注册信号处理函数(不可用于SIGKILL、SIGSTOP信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
参数:
signum:信号的编号,这里可以填数字编号,也可以填信号的宏义。
handler:取值有3种情况:
SIG_IGN:忽略该信号
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数,如:func
回调函数的定义如下:
void func(int signo)
{
//signo为触发的信号,为signal()第一个参数的值
}
返回值:
成功:第一次返回NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
失败:返回SIG_ERR
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
//信号处理函数
void fun1(int signum)
{
printf("捕捉到信号:%d\n",signum);
}
//信号处理函数2
void fun2(int signum)
{
printf("捕捉到信号:%d\n",signum);
}
//信号注册函数
int main()
{
//信号注册
//ctrl+c
signal(SIGINT,fun1);
//ctrl+\
signal(SIGQUIT,fun2);
while(1)
{
sleep(1);
}
return 0;
}
运行结果如下:
信号先注册,进程不要退出,然后等待信号的到达,信号到达之后就会执行信号处理函数。
了解几个概念:
实际执行信号的处理动作称为信号递达
信号递达的三种方式:默认、忽略和自定义捕捉
信号从产生到递达之间的状态,称为信号未决(Pending)
进程可以选择阻塞 (Block )某个信号
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
注意:
每个进程都存在一个PCB,在PCB中存在两个集合,一个是未决信号集,一个是阻塞信号集。
以SIGINT为例说明未决信号集和阻塞信号集的关系:
注意:
未决信号集在内核中,要对内核进行操作只能通过系统调用,但是没有提供这样的方法,所以只能对未决信号集进行读操作,但是可以对阻塞信号集进行读写操作。
问题1:所有信号的产生都要由OS来进行执行,这是为什么?
信号的产生涉及到软硬件,且OS是软硬件资源的管理者,还是进程的管理者。
问题2:进程在没有收到信号的时候,能否知道自己应该如何对合法信号进行处理呢?
答案是能知道的。每个进程都可以通过task_struct找到表示信号的三张表。此时该进程的未决信号集表中哪些信号对应的那一位比特位是为0的,且进程能够查看阻塞信号集表知道如果收到该信号是否需要阻塞,可以查看handler表知道对该信号的处理动作。
问题3:OS如何发生信号?
OS给某一个进程发送了某一个信号后,OS会找到信号在进程中未决信号集表对应的那一位比特位,然后把那一位比特位由0置1,这样OS就完成了信号发送的过程。
sigset_t: 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,也被定义为一种数据类型。这个类型可以表示每个信号状态处于何种状态(是否被阻塞,是否处于未决状态)。阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。
实际上两个信号集在都是内核使用位图机制来实现的,想了解的可以自己去了解下,但是操作系统不允许我们直接对其操作。而需要自定义另外一个集合,借助于信号集操作函数来对PCB中的这两个信号集进行修改。
信号集操作函数: sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释。
注意: 对应sigset类型的变量,我们不可以直接使用位操作来进行操作,而是一个严格实现系统给我们提供的库函数来对这个类型的变量进行操作。
下面是信号集操作函数的原型:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
注意: 在实现这些函数之前,需要使用sigemptyset 或sigfillset对信号集进行初始化。前四个函数的返回值是成功返回0,失败返回-1。最后一个函数的返回值是真返回1,假返回-1
阻塞信号集操作函数——sigprocmask:
#include<signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
功能:
检查或修改信号阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由set指定,而原先的信号阻塞集合由oldset保存。
参数:
how:信号阻塞集合的修改方法,有3种情况:
SIG_BLOCK:向信号阻塞集合中添加set信号集,新的信号掩码是set和旧信号掩码的并集。相当于mask=mask丨set。
SIG_UNBLOCK:从信号阻塞集合中删除set信号集,从当前信号掩码中去除set中的信号。相当于mask=mask&~set。
SIG_SETMASK:将信号阻塞集合设为set信号集,相当于原来信号阻塞集的内容清空,然后按照set中的信号重新设置信号阻塞集。相当于mask=set。
set:要操作的信号集地址。
若set为NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中。
oldset: 保存原先信号阻塞集地址。
返回值:成功:0 失败:-1
未决信号集操作函数——sigpending:
#include<signal.h>
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
set:未决信号集
返回值:
成功:0 失败:-1
代码示例:
实验一:把进程中信号屏蔽字2号信号进行阻塞,然后每隔1s对未决信号集进行打印,观察现象。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t* pend)
{
int i = 0;
for (i = 1; i < 32; ++i)
{
if (sigismember(pend, i)){
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
int main()
{
sigset_t set, oset;
sigset_t pending;
// 使用系统函数对信号集进行初始化
sigemptyset(&set);
sigemptyset(&oset);
sigemptyset(&pending);
// 阻塞2号信号
// 先用系统函数对set信号集进行设置
sigaddset(&set, 2);
// 使用sigprocmask函数更改进程的信号屏蔽字
// 第一个参数,三个选项:SIG_BLOCK(mask |= set) SIG_UNBLOCK(mask &= ~set) SIG_SETMASK(mask = set)
sigprocmask(SIG_BLOCK, &set, &oset);
int flag = 1; // 表示已经阻塞2号信号
int count = 0;
while (1){
// 使用sigpending函数获取pending信号集
sigpending(&pending);
// 打印pending位图
PrintPending(&pending);
sleep(1);
}
return 0;
}
运行结果如下:
可以看到,进程收到2号信号时,且该信号被阻塞,处于未决状态,未决信号集中2号信号对应的比特位由0置1
实例2: 将上面的代码进行修改,进行运行10s后,我们将信号屏蔽字中2号信号解除屏蔽
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t* pend)
{
int i = 0;
for (i = 1; i < 32; ++i)
{
if (sigismember(pend, i)){
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
int main()
{
sigset_t set, oset;
sigset_t pending;
// 使用系统函数对信号集进行初始化
sigemptyset(&set);
sigemptyset(&oset);
sigemptyset(&pending);
// 阻塞2号信号
// 先用系统函数对set信号集进行设置
sigaddset(&set, 2);
// 使用sigprocmask函数更改进程的信号屏蔽字
// 第一个参数,三个选项:SIG_BLOCK(mask |= set) SIG_UNBLOCK(mask &= ~set) SIG_SETMASK(mask = set)
sigprocmask(SIG_BLOCK, &set, &oset);
int flag = 1; // 表示已经阻塞2号信号
int count = 0;
while (1){
// 使用sigpending函数获取pending信号集
sigpending(&pending);
// 打印pending位图
PrintPending(&pending);
if (++count == 10){
// 两种方法都可以
sigprocmask(SIG_UNBLOCK, &set, &oset);
//sigprocmask(SIG_SETMASK, &oset, NULL);
}
sleep(1);
}
return 0;
}
运行结果如下:
信号2被阻塞之后就变成了未决状态,当该信号从阻塞集合中解除的时候,该信号就会被处理,该信号被处理后,该信号的未决信号集的标志位将从1置为0。
一个进程收到一个信号的时候,可以用以下的方法进行处理:
(1)执行系统默认动作:对大多数信号来说,系统默认动作就是来终止进行。
(2)忽略此信号(丢弃):接收到此信号后没有任何动作
(3)执行自定义信号处理函数(捕获):用户定义的信号处理函数处理该信号
注意:SIGKILL和SIGSTOP不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。
先思考一个问题:信号是什么时候被进程处理的?
首先,不是立即被处理的。而是在合适的时候,这个合适的时候,具体指的是进程从用户态切换回内核态时进行处理
这句话如何理解,什么是用户态?什么是内核态?
注意: 操作系统中有一个cr寄存器来记录当前进程处于何种状态
进程空间分为用户空间和内核空间。此前我们介绍的页表都是用户级页表,其实还有内核级页表。进程的用户空间是通过用户级页表映射到物理内存上,内核空间是通过内核级页表映射到物理内存上,如下面简图所示:
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
最高 1G 的内核空间是被所有进程共享的!
进程有不同的用户空间,但是只有一个内核空间,不同进程的用户空间的代码和数据是不一样的,但是内核空间的代码和数据是一样的。
上面这些主要是想说:进程处于用户态访问的是用户空间的代码和数据,进程处于内核态,访问的是内核空间的代码和数据。
信号捕捉的整个过程:
从上面的图可以看出,进程是在返回用户态之前对信号进行检测,检测pending位图,根据信号处理动作,来对信号进行处理。这个处理动作是在内核态返回用户态后进行执行的,所以这里也就回答了开始提出的那一个问题了。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
检查或修改指定信号的设置(或同时执行两种操作)
参数:
signum:要操作的函数
act:要设置的对信号的新处理方式(传入方式)
oldact:原来对信号的处理方式(传出参数)
如果act指针非空,则要改变指定信号的处理(设置),如果oldact指针非空,则系统将此前指定信号的处理方式入oldact
返回值:
成功:0
失败:-1
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);//已经弃用
};
(1)sa_handler、sa_sigaction:信号处理函数指针,和signal里面的函数指针用法是一样的,根据情况给两个指针赋值。
a)SIG_IGN:忽略该信号
b)SIG_DFL:执行系统默认的动作
c)处理函数名:自定义信号处理函数
(2)sa_mask:信号阻塞集,在执行信号处理函数的时候,用来临时的屏蔽信号
(3)sa_flags:用于指定信号处理的行为,通常设置为0,表示使用默认的属性。它可以是一下值的“按位”或“组合”:
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
printf("catch a signal: %d\n", signo);
}
int main()
{
struct sigaction act, oact;
act.sa_flags = 0;// 选项 设置为0
sigfillset(&act.sa_mask);
act.sa_handler = handler;
// 对2号信号修改处理动作
sigaction(2, &act, &oact);
while (1){
raise(2);
sleep(1);
}
return 0;
}
运行结果如下:
代码示例:旧的信号处理函数
sa_flags标志为0代表使用的是旧的信号处理函数
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
printf("catch a signal: %d\n", signo);
}
int main()
{
int ret = -1;
struct sigaction act;
//标志为0,代表使用的是旧的信号处理函数指针
act.sa_flags = 0;
//给阻塞集初始化
sigfillset(&act.sa_mask);
act.sa_handler = handler;
// 信号注册
ret = sigaction(SIGINT, &act, NULL);
if(ret == -1)
{
perror("sigaction");
return 1;
}
printf("按下任意键退出.....\n");
getchar();
return 0;
}
运行结果如下:
代码示例:新的信号处理函数
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo,siginfo_t *info,void *context)
{
printf("catch a signal: %d\n", signo);
}
int main()
{
int ret = -1;
struct sigaction act;
//使用新的信号处理函数指针
act.sa_flags = 0;
//给阻塞集初始化
sigfillset(&act.sa_mask);
act.sa_handler = handler;
// 信号注册
ret = sigaction(SIGINT, &act, NULL);
if(ret == -1)
{
perror("sigaction");
return 1;
}
printf("按下任意键退出.....\n");
getchar();
return 0;
}
先看下面一段代码:
#include <stdio.h>
#include <signal.h>
int a = 10;
void SelfAdd(int n)
{
a = a + n;
a = a + n;
}
void handler(int signo)
{
SelfAdd(signo);
}
int main()
{
signal(2, handler);
SlefAdd(2);
printf("%d\n", a);
return 0;
}
上面我写了一个比较简单的代码,我们慢慢分析,当我们在主函数中执行调用SelfAdd时,进入该函数,执行完函数中int a = a + n这句代码后,a变成了12,此时收到2号信号,发生中断
最后打印a结果是16,其实正常调用该函数的话,打印的应该是18。
像上面这样的因为重入导致结果错乱的函数就叫做不可重入函数。其中a是一个全局变量。如果一个函数值访问自己的局部变量或参数,那么这样的函数就叫做可重入函数。
说的通俗点,不可重入的意思是,如果你定义了一个全局变量,在函数1里面这个变量应该是10,但是有一个函数2改变了这个变量的值,此时本来函数1用的是10 ,你把他改变了,这就是不安全的,这就是不可重入函数。
思考一个问题:为什么两个不同的控制流程调用同一个函数,访问同一个局部变量或参数不会造成错乱?
在多线程中,每个线程虽然是资源共享,但是他们的栈却是独有的,所以说局部变量不会造成错乱。
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
函数体内使用了静态的数据结构。
保证函数可重入性的方法:
SIGCHLD信号
产生条件:1)子进程终止时
2)子进程接收到SIGSTOP信号停止的时候
3)子进程处于停止态,接收到SIGCONT后唤醒时
如何避免僵尸进程:
1)最简单的方法,父进程通过wait和waitpid等待子进程函数结束,但是,这会导致父进程挂起。
2)如果父进程要处理的事情很多,不能挂起,通过signal()函数人为处理信号SIGCHLD,只有在子进程退出自动调用制定好的回调函数,因为子进程结束后,父进程会收到信号SIGCHLD,可以在回调函数里面用wait或waitpid回收资源。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include<sys/types.h>
#include<sys/wait.h>
void sig_child(int signo)
{
pid_t pid;
//处理僵尸进程,-1代表等待任意一个子进程,WNOHANG代表不阻塞
while((pid=waitpid(-1,NULL,WNOHANG))>0)
{
printf("孩子进程被杀死 %d\n",pid);
}
}
int main()
{
pid_t pid;
//创建捕捉子进程退出信号
//只要子进程退出,触发SIGSIGCHLD,自动调用sig_child()
signal(SIGCHLD,sig_child());
//创建进程
pid = fork();
if(pid<0)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
//子进程
printf("我是子进程,pid id :%d.我正在退出\n",getpid());
exit(0);
}
else if(pid>0)
{
//父进程
sleep(2);//保证子进程先运行
printf("我是父亲,我正在退出\n");
system("ps -ef|grep defunct");//查看有没有僵尸进程
}
return 0;
}
运行结果:
3)如果父进程不关心子进程时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收,并不再给父进程发送信号。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
//忽略子进程退出信号的信号
//那么子进程结束之后,内核会回收,并不再给父进程发送信号
signal(SIGCHLD,SIG_IGN);
//创建进程
pid = fork();
if(pid<0)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
//子进程
printf("我是子进程,pid id :%d.我正在退出\n",getpid());
exit(0);
}
else if(pid>0)
{
//父进程
sleep(2);//保证子进程先运行
printf("我是父亲,我正在退出\n");
system("ps -ef|grep defunct");//查看有没有僵尸进程
}
return 0;
}
运行结果:
手机扫一扫
移动阅读更方便
你可能感兴趣的文章