您现在的位置是:首页 >技术教程 >【Linux】进程信号网站首页技术教程
【Linux】进程信号
目录
一、预备知识
1.信号是什么??
生活中,存在各种各样的信号,诸如,红绿灯、下课铃声、闹钟响等等。
信号发出后,我们接收到信号,我们会做出相应的动作,诸如“红灯停绿灯行”等。为什么?因为我们从小就被如此教育,这已经形成了我们的潜意识,即使没有看到红绿灯,我们也知道应该“红灯停绿灯行”!!
2.信号产生后,我们能够认识并处理它——我们能识别信号。
那么,进程信号和我们所理解的信号有什么不同吗??——答案是,没有区别!!进程就相当于我们自己,进程信号就是信号。进程能识别进程信号——程序员早就设置好了。
3.信号随时可能产生,在信号产生前,我们可能在做优先级更高的事情,即比起对接收到的信号做出相应的动作,我们更应该继续做原本正在做的动作。同理,进程信号也随时可能产生,在接收到信号之前,进程可能正在做优先级更高的事情,接收到信号时,不会立马对信号做出相应的动作,而是到一个合适的时机,才做出反应。
——如此,进程信号需要保存,进程必须有记录信号的能力。
4.信号的产生对于进程而言是异步的。(即信号的产生与进程无关,进程不会等待信号的产生)
5.进程如何记录产生的信号,记录在哪里??
先描述再组织——>怎么描述一个信号,用什么数据结构管理信号??
信号只需要保存有无产生——>用位图进行管理——>task_struct中必须有一个位图结构,用int表示。
6.发送信号的本质是写入信号,直接修改特定进程中信号位图中特定比特位的值0->1。
比特位的位置——信号的编号;比特位的值——是否收到信号。
7.task_struct是内核数据结构,只能由OS来修改——无论有多少种信号产生的方式,最终都是OS来完成。
二、信号产生
1.查看信号——kill -l
2.信号捕捉——signal()
1.函数定义
1.1函数功能解释:
signal()将信号信号的处置设置为处理程序,该处理程序可以是SIG IGN、SIG DFL或程序员定义的函数("signalhandler")的地址。另外,信号SIGKILL 和SIGSTOP不能被捕捉或忽略,会立即强制执行。
1.2函数返回值
signal()返回信号处理程序的前一个值,或者在错误时SIG ERR。如果发生错误,则设置errno来指示原因。
2.举例运用
signal()函数:可以对进程信号进行自定义捕捉。
2.1如下,以2号信号(SIGINT)的自定义捕捉为例:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
//自定义方法
//signo:特定信号被发送给当前进程时,执行handler()方法时,要自动填充对应信号给handler方法
//我们甚至可以给所有的信号设置同一个方法
void handler(int signo)
{
cout << "接收到信号:"<< signo <<endl;
//exit(signo);
}
int main()
{
//2号信号的默认动作是终止进程,可以通过signal()函数自定义2号信号对应的动作,即进程信号的自定义捕捉。
signal(2,handler);//该函数执行时,更改了2号信号对应的动作(执行函数的映射关系),没有调用函数handler()
//后续过程中,当进程接收到2号信号时,进程会调用handler()函数
while(true)
{
cout << "我是一个进程,我正在运行," << "我是:" <<getpid() << endl;
sleep(1);
}
}
注:./mysignal生成的是前台进程,可以被ctrl+C杀死;./mysignal & 产生的是后台进程,不可以被ctrl+C杀死。
2.2尝试自定义捕捉所有的普通信号
//尝试对所有普通信号进行自定义捕捉
for(int i = 1;i <= 31;i++)
{
signal(i,handler);
}
经测试,几乎所有的普通信号都能被自定义捕捉,有两个例外——9号信号,即
以及19号信号,即
。这是操作系统设置的,防止用户自定义捕捉,导致进程无法被杀死或暂停。
3.信号产生
3.1键盘输入
平时在输入数据时,计算机如何知道键盘输入了数据呢??——硬件中断
简单来说,按键时,中断控制器会将键盘输入的行为传送给CPU(对应的针脚),然后通知系统,我们的键盘已经开始输入数据了。不同的硬件对应在CPU中不同的阵脚,对应不同的中断号,对应中断向量表中不同的区域。而后,操作系统从对应硬件中读取数据(硬盘什么位置被按下)。(之后要学习的网卡等其他外设的数据读取也是同理)。
例如:键盘输入ctrl+C时产生2号信号(SIGINT)。
补充:信号的概念:信号是进程之间事件异步通知的一种方式,属于软中断。(先不解释)
3.2 系统调用
3.2.1 kill()
函数功能:向进程pid为kill()函数的一个参数值的进程发送信号sig。
命令行进行命令输入kill命令的本质也是调用系统调用kill()
//mykill.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>
#include <cstring>
#include <cerrno>
using namespace std;
void Usage(string proc)
{
cout << " Usage:
";
cout << proc << "信号信号编号 目标进程
" << endl;
}
//./mykill signo target_id
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signo = atoi(argv[1]);
int target_id = atoi(argv[2]);
int n = kill(target_id,signo);
if(n != 0)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(2);
}
return 0;
}
可以在命令行通过 ./mykill 指定信号 指定进程 给目标进程发送指定信号。(mykill是上面程序编译后得到的可执行程序名)
3.2.2 raise()
函数功能:向调用该函数的进程发送sig信号(自己给自己发信号)
//mykill.cc
int main(int argc,char* argv[])
{
sleep(1);
raise(2);//向自己发送2号信号
return 0;
}
3.2.3 abort()
该函数是C语言接口。
函数功能:想自己发送6号信号(SIGABRT),该信号可以被捕捉,但是还是会终止进程。
//mykill.cc
void handler(int signo)
{
cout << "进程" << getpid() << " 接收到信号:"<< signo <<endl;
//exit(signo);
}
int main(int argc,char* argv[])
{
signal(SIGABRT,handler);
cout << "begin" << endl;
sleep(1);
abort();//自己给自己发信号SIGABRT,可以被捕捉,但仍会终止当前进程
cout << "end" << endl;//进程运行时不会打印end
return 0;
}
3.3 软件条件
SIGPIPE和SIGALRM都是由软件条件产生的信号。
3.3.1 SIGPIPE(学习管道时已经学习过)
3.3.2 alarm()
闹钟设置,一个进程只能设置一个闹钟,重复设置会产生覆盖的结果;但是多个进程都可以设置闹钟,同时可能存在多个闹钟,所以操作系统需要对闹钟进行管理。
函数功能:定闹钟,在seconds秒后终止进程,seconds =0 时取消前面所有的闹钟。
返回值:
返回先前闹钟的剩余时间,如果先前没定闹钟,则返回0。
闹钟结束时给自己发送14号信号SIGALRM,该信号可以被捕捉。闹钟重新设定后进程退出的时间也会发生改变。
int count = 0;
void handler(int signo)
{
cout << "接收到信号: " << signo << ", count = " << count << endl;
int n = alarm(10);//n的值恒为0,因为handler方法是在进程接收到信号时调用的函数,此时闹钟已经响了,
//但是进程不会结束,因为每次闹钟响,都会重新设定一个10s的闹钟
cout << "重新设定闹钟," <<"alarm的返回值是: " << n << endl;
}
int main(int argc,char* argv[])
{
//signal(SIGALRM, handler);//捕捉SIGALRM信号
cout << "我是一个进程, 我的pid是 :" << getpid() << endl;
//alarm(1);//时间到,闹钟响,执行handler方法
//while(true) count++;//count的第一次打印的值为计算机的算力( 1S 我们的计算机会将一个整数累计到多少,算力)
int n1 = alarm(5);
cout << n1 << endl;//0
sleep(3);
int n2 = alarm(10);
cout << n2 << endl;//2
sleep(8);
int n3 = alarm(2);
cout << n3 << endl;//2
//以上接连设定了三个闹钟,每次重新设定的闹钟都会覆盖前面的闹钟(一个进程在某一时刻至多存在一个自己设定的闹钟)
//不过可以有多个进程为当前进程设定了闹钟(发送SIGALRM信号)
return 0;
}
运行截图1(main()函数中被注释的部分取消注释,并将下面的代码注释掉):
运行截图2:
3.4 硬件异常
硬件异常有很多种,常见的有CPU硬件异常和MMU硬件异常。
3.4.1CPU硬件异常
例如:除数为0时程序崩溃(进程异常退出)。
该信号虽然可以被捕捉,但是如果捕捉后不退出进程的话,CPU会一直向进程发送8号信号。
3.4.2MMU硬件异常(MMU也在CPU内)
例如:野指针问题导致程序崩溃(进程异常退出)。
诸多信号在被进程接收到后,进程都会被杀死,结果都一样,为什么要有这么多种信号呢?——表征进程退出的原因(进程异常退出的原因)。
总结:信号产生后都需要借操作系统的手向进程PCB写信号位图(0 --> 1)
三、信号保存
1.阻塞信号
信号阻塞:信号写入后,不立马执行信号,进程做的就是信号阻塞(进程可以选择阻塞某个信号)。
信号未决:信号阻塞设置后,在进程解除阻塞之前,信号一直处于未决状态。
信号递达:信号的执行动作,忽略就是信号递达的一种表现形式。
2.信号的保存
block表:位图,记录某个信号是否被阻塞;对应二进制位为1则该信号被阻塞,反之没被阻塞。
pending表:位图,记录进程是否接收到某个信号,对应二进制位为1则接收到了,反之没收到。
handler表:函数指针数组,决定进程处理对应信号的动作。
信号在内核中的表示:
1.每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号 产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子 中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。(这里只讨论常规信号)
四、信号的处理
1.sigset_t (信号集类型)
block表和pending表均是位图, 信号的阻塞和未决标志都只有一个比特位来表示,非0即1。 因此,阻塞和未决标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
注意: sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用系统调用函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
2.信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
函数功能:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有
效信号。(成功返回0,出错返回-1)int sigfillset(sigset_t *set);
函数功能:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系
统支持的所有信号。(成功返回0,出错返回-1)int sigaddset (sigset_t *set, int signo);
函数功能:将signo信号添加set所指向的信号集。(成功返回0,出错返回-1)int sigdelset(sigset_t *set, int signo);
函数功能:将signo信号从set所指向的信号集中移除。(成功返回0,出错返回-1)int sigismember(const sigset_t *set, int signo);
函数功能:用于判断set所指向的信号集的有效信号中是否包含signo信号。
若包含则返回1,不包含则返回0,出错返回-1。
注意:在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
3. sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); //set oldset(输出型参数)
函数功能:读取或更改进程的信号屏蔽字(阻塞信号集)
返回值:若成功则为0,若出错则为-1说明:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出;
如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
how的可能传参及其功能:
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
4. sigpending
#include <signal.h>
int sigpending(sigset_t *set);//set:信号集输出型参数
函数功能:获取当前进程的未决信号集,通过set参数传出。
返回值:调用成功返回0,失败返回-1,并设置错误信息errno。
举例有助理解:
#include <iostream>
#include <signal.h>
#include <cassert>
#include <cstring>
#include <unistd.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{
for(int i = 1;i <= 31;i++)
{
//判断i号信号是否在pending表内
if(sigismember(&pending,i)) cout << "1";
else cout << "0";
}
cout<<endl;
}
int main()
{
//1.设置block表
//1.1定义信号集
sigset_t set,oset;
//1.2初始化信号集
sigemptyset(&set);
sigemptyset(&oset);
//1.3将2号信号(SIGINT)添加到set信号集
//sigaddset(&set,SIGINT);
//将1~31号信号都添加到set信号集,以便测试有哪些信号不能被阻塞
for(int i = 1;i <= 31;i++)
{
sigaddset(&set,i);
}
//1.4将新的信号屏蔽字设置进进程
sigprocmask(SIG_BLOCK,&set,&oset);
//2.查看pending表
while(true)
{
//获取进程pending表
sigset_t pending;
int n = sigpending(&pending);
assert(n == 0);
(void)n;
cout << "当前进程的pending表内容为: ";
PrintPending(pending);//打印pending表
sleep(1);
}
return 0;
}
运行可执行程序,向进程发送1~31号信号,测试得到,普通信号的9号信号和19号信号无法被屏蔽(前面以及学过,9号和19号信号也不能被捕捉(可以定义自定义动作,但进程收到信号后仍会终止进程)
另外:在执行信号时,对应信号的block表bit位会被置为1,以防止信号递归递达;即正在执行信号动作时,如果又收到该信号,该信号的pending表bit位变1,但该信号不会被递达,此时block表bit位为1。动作完成后,block表bit位自动置为0,该信号被立即递达。
信号可以被立即处理吗?——可以
如果一个信号之前被阻塞(对应block表bit位为1),当解除阻塞时,信号会被立即递达。
5.捕捉信号
系统调用sigaction()函数实现信号的捕捉。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
函数功能:重定义信号执行函数handler;基本功能同signal()函数.
返回值:成功返回0,失败返回-1,失败码设置errno.
The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
#include <iostream>
#include <signal.h>
#include <cassert>
#include <cstring>
#include <unistd.h>
using namespace std;
void PrintPending(const sigset_t& pending)
{
for(int i = 1;i <= 31;i++)
{
//判断i号信号是否在pending表内
if(sigismember(&pending,i)) cout << "1";
else cout << "0";
}
cout<<endl;
}
void sigcb(int signo)
{
cout << "接收到信号:"<< signo <<endl;
sigset_t pending;
int cnt = 10;
while(true)
{
int n = sigpending(&pending);
assert(n == 0);
(void)n;
cout << "当前进程的pending表内容为: ";
PrintPending(pending);//打印pending表
sleep(1);
if(cnt-- == 0)
break;
}
//exit(signo);
}
/*
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
*/
int main()
{
struct sigaction act,oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = sigcb;
act.sa_flags = 0;
//设置act.sa_mask的值
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
sigaction(2,&act,&oldact);
while(true)
{
cout << "我是一个进程,我正在运行," << "我是:" <<getpid() << endl;
sleep(1);
}
return 0;
}
注意: ctrl + c 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
信号捕捉到底是如何做到的呢?——用户态和内核态。
什么是用户态和内核态??
1.用户态——执行用户写的代码的时候进程所处的状态;
2.内核态——执行OS所有代码的时候进程的状态。
例如:进程时间片到了,需要切换,就要执行进程切换逻辑(OS所有代码);
系统调用;
进程地址空间:(32位为例)
0~3G——用户代码和数据;3~4G——操作系统代码和数据
(用户级页表) (内核级页表)
<----虚拟内存
内核空间内存储的是OS的代码和数据的地址,具体的代码和数据存储在物理内存。内核空间是所有进程都有的,且所有进程虚拟空间的内核空间里的内容完全相同,通过内核级页表映射到物理内存-->内核级页表是所有进程共用的。
如何做到用户态和内核态的切换:CR3寄存器,修改执行级别(软硬件结合,CR3是CPU内的一个部件)
3 —— 用户态;0 —— 内核态
信号捕捉的原理:
6.处理信号的方式
6.1执行默认动作
不对handler表做任何修改,当进程接收到信号时,若信号对应block表的比特位为0,则执行默认动作。
6.2忽略信号
信号对应block表中比特位为0,pending为1,信号递达后,进程选择忽略对应的信号。SIG_DFL、SIG_IGN等。
6.3执行自定义动作
通过信号捕捉signal()或sigaction()自定义动作。
7.可重入函数
7.1 定义
被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称 为重入;
有可能因为重入而造成错乱的函数称为不可重入函数;反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数 。
7.2 不可重入函数
如果一个函数符合以下条件之一则是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
8.volatile
功能:保证内存的可见性。
若没有在flag全局变量前声明volatile关键字,则
1.未优化:向进程发送2号信号后退出while循环,执行完printf语句后结束进程;
2.优化后:向进程发送2号信号,flag的值变为1,但是由于while循环只需检测flag的值而不需要对flag的值做修改,CPU为了节省时间,不会去访问内存中所在的空间,而是直接访问寄存器中的内容;导致while循环的条件一直为真,进程不会自动结束。
声明volatile关键字后:
表示不允许优化,(优化是编译器做的,本质是修改代码,即编程语言代码转化为汇编语言时会被编译器进行修改),优化不一定是好的,必要的时候可以用volatile关键字进行声明,避免不需要的优化导致预想不到的后果。
//test.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int flag = 0;
void handler(int signo)
{
printf("对2号信号进行捕捉, 并将flag的值从0置1
");
flag = 1;
sleep(5);
}
int main()
{
signal(2,handler);
while(!flag);//使得操作系统只取寄存器中读取数据(只需做数据的扫描)
printf("进程退出
");
return 0;
}
//makefile
//-O2是优化编译的一种选项(默认是-O0,没有优化)
test:test.c
gcc -o test test.c -O2
.PHONY:clean
clean:
rm -f test
优化后:
优化前:
9.SIGCHID信号
子进程退出时,会向父进程发送SIGCHID信号(17号信号),该信号的默认处理动作是忽略。
1.可以通过信号捕捉,执行自定义动作来等待子进程退出;
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void handler(int signo)
{
printf("执行自定义动作处理%d信号
",signo);
pid_t id = waitpid(-1,NULL,0);
printf("子进程%d等待成功
", id);
sleep(5);
}
int main()
{
//方法一:
signal(SIGCHLD,handler);
pid_t id = fork();
//子进程
if(id == 0)
{
printf("我是子进程, 我的进程pid是%d, 我的父进程是%d
",getpid(),getppid());
sleep(5);
exit(1);
}
while(1)
{
printf("我是父进程, 我的进程pid是%d
",getpid());
sleep(1);
}
return 0;
}
2.事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。
//方法二: signal(SIGCHLD,SIG_IGN);//SIG_DFL为信号默认处理方式; SIG_IGN设置为忽略处理