Linux进程信号

Posted 小倪同学 -_-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程信号相关的知识,希望对你有一定的参考价值。

文章目录

信号入门

生活角度的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取“。
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是礼物,你要送给你的朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

技术应用角度的信号

运行如下程序

#include<iostream>
#include<sys/types.h>
#include<unistd.h>

int main()

        while(true)
                std::cout<<"I am a process:"<<getpid()<<std::endl;
                sleep(1);
         
        return 0;

Makefile文件

在程序运行的时候我们可以用Ctrl+C终止程序

为什么使用Ctrl+C后,该进程就终止了?

当用户按下Ctrl+C后,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程.,前台进程因为收到信号,进而引起进程退出。

注意

  1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

我们可以用 kill -l 指令查看所有信号

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • 编号1~31号信号是普通信号,34以上的是实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

Ctrl+C传送的信号就是2号信号SIGINT

每个信号的含义

信号处理常见方式

  • 默认方式(部分是终止进程部分有特定功能)
  • 忽略信号
  • 自定义方式:捕捉信号
    可以对大部分信号进行捕捉,但是个别无法捕捉,如9号信号(也不能被忽略)

信号的本质

因为信号不是立即处理的所以信号一定要先被保存起来!

在哪里保存?
进程的PCB,进程控制快task_stcuct

如何保存?
对进程而言,“是否有信号”+“是谁”(位图)

谁发的,如何发?
发送信号的本质,就相当于写对应进程的tast_struct信号位图。
OS是进程的管理者,对进程数据做修改,OS是有这个能力和义务的。
信号是OS发送的,通过修改对应进程的信号位图(0->1),完成信号发送。

产生信号

通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump

Core Dump(核心转储)
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024

在云服务器中,核心转储是默认被关掉的,我们可以通过使用ulimit -a命令查看当前资源限制的设定。


第一行显示core文件的大小表示可以核心转储文件的大小。

我们可以通过ulimit -c size命令来设置core文件的大小。

core文件的大小设置完毕后,就相当于将核心转储功能打开了,此时我们再发送部分信号,会发现终止进程后会显示core dumped。

此时会在当前路径下生成core文件

核心转储的作用

当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的。如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的。

当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因。而在某些特殊情况下,我们会用到核心转储,核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件,一般命名为core.pid。

用核心转储调试

看如下代码

#include<iostream>
#include<sys/types.h>
#include<unistd.h>

int main()

        while(1)
                std::cout<<"I am a process:"<<getpid()<<std::endl;
                sleep(1);
                int a=1;
                int b=0;
                int c=a/b;
        
        return 0;

该代码存在除0错误,该程序运行1秒后崩溃。

使用gdb对当前可执行程序进行调试,然后直接使用core-file core文件命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码。

core dump标志

还记得status吗

status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,不同比特位所代表的信息不同,具体细节如下(只需关注status低16位比特位):

编写如下代码,通过status可以获取进程退出的相关信息

#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
 
using namespace std;
 
int main()

        pid_t id=fork();
        if(id==0)
                int count=5;
                while(count)
                        cout<<"I am a child:"<<getpid()<<"count:"<<count<<endl;
                        count--;
                        sleep(1);
                
                int a=10;
                int b=0;
                a/=b;
                exit(0);
        
        int status=0;
        pid_t ret=waitpid(id,&status,0);
        if(ret==id)
                cout<<"wait success!"<<endl;
                cout<<"exit code:"<<((status>>8)&0xFF)<<endl;
                cout<<"exit signal:"<<(status&0x7F)<<endl;
                cout<<"code dump:"<<((status>>7)&0x1)<<endl;
        
        return 0;
 

运行结果

当你的进程触发错误(除零,野指针,越界)的时候,也会由操作系统识别到,然后给目标进程发送信号,来达到终止进程的目的!

操作系统如何具备识别异常的能力?
操作系统是硬件的管理者,而每个错误都有对应的软硬件如
除零错误:CPU->状态寄存器
越界/野指针:内存和页表

调用系统函数向进程发信号

使用kill命令向一个进程发送信号,我们可以以kill -信号名 进程ID的形式进行发送。

由软件条件产生信号

SIGALRM信号

alarm函数可以设定一个闹钟,到时间后会向进程发送SIGALRM信号。

alarm函数的函数原型如下

unsigned int alarm(unsigned int seconds);

使用示例

#include<iostream>
#include<unistd.h>
#include<sys/wait.h>

using namespace std;
 
int main()

        alarm(1);// 1秒后,会给目标进程发送SIGALRM,注意:设定时不会有明显的现象
        int count=0;
        while(1)
                cout<<count++<<endl;
        

        return 0;

运行结果如下

我们发现1秒钟只运行了十万多次,这是为什么呢?

因为我们不断的向显示屏打印数据,而操作系统与显示屏交互是很耗时间的,其次,由于我当前使用的是云服务器,因此在累加操作后还需要将累加结果通过网络传输将服务器上的数据发送过来,因此最终显示的结果要比实际一秒内可累加的次数小得多。

我们可以先让count变量一直执行累加操作,直到一秒后进程收到SIGALRM信号后再打印累加后的数据来查看1秒钟服务器能加到多少。

#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
 
using namespace std;

int count=0;
void handler(int sigo)

        cout<<"get a signo:"<<sigo<<endl;
        cout<<"count is:"<<count<<endl;
        exit(1);

int main()

        signal(SIGALRM,handler);

        alarm(1);// 1秒后,会给目标进程发送SIGALRM,注意:设定时不会有明显的现象
        while(1)
                count++;
        

        return 0;

结果如下

还可以向进程发送信号的函数有

void abort(void);// 向进程发送SIGABRT信号
int raise(int sig);// 可以向进程发送任意信号

kill函数也可以向进程发送信号

int kill(pid_t pid, int sig);

kill函数用于向进程ID为pid的进程发送sig号信号,如果信号发送成功,则返回0,否则返回-1。

下面模拟实现kill命令

#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<cstdlib>
#include<sys/types.h>
 
using namespace std;

int count=0;
void handler(int sigo)

        cout<<"get a signo:"<<sigo<<endl;
        cout<<"count is:"<<count<<endl;
        exit(1);

int main(int argc,char* argv[])

        if(argc!=3)
                cerr<<"Usage:"<<argv[0]<<"signum pid"<<endl;
                exit(1);
        

        kill(atoi(argv[2]),atoi(argv[1]));


由硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

总结思考

  1. 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

OS是进程的管理者

  1. 信号的处理是否是立即处理的?

不是,在合适的时候会进行处理

  1. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

需要被记录,记录在进程控制块中

  1. 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

能知道,进程能识别信号

  1. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

修改目标进程PCB中对应位图的比特位

阻塞信号

信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)(默认,忽略,自定义捕捉)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。(进程可以允许某些信号不会被递达,直到解除阻塞方可递达)
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

在内核中的表示

信号在内核中的表示示意图如下:

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会在改变处理动作之后再接触阻塞。
  • SIGQUIT信号未产生过,但一旦产生SIGQUIT信号,该信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前,这种信号产生过多次,POSIX.1允许系统递达该信号一次或多次。Linux是这样实现的:普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里,这里只讨论普通信号。

注意:

  1. 在block位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞。
  2. 在pending位图中,比特位的位置代表某一个信号,比特位的内容代表是否收到该信号。
  3. handler表本质上是一个函数指针数组,数组的下标代表某一个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。
  4. block、pending和handler这三张表的每一个位置是一一对应的。

sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • sigemptyset函数:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • sigfillset函数:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
  • sigaddset函数:在set所指向的信号集中添加某种有效信号。
  • sigdelset函数:在set所指向的信号集中删除某种有效信号。
  • sigismember函数:判断在set所指向的信号集中是否包含某种信号,若包含则返回1,不包含则返回0,调用失败返回-1。

注意:

  • 前四个函数都是成功返回0,出错返回-1,
  • 在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

sigprocmask

功能: 读取或更改进程的信号屏蔽字(阻塞信号集)
函数原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

参数说明:

  • 如果oset是非空指针,则读取进程当前的信号屏蔽字通过oset参数传出。
  • 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字为mask,下表说明了how参数的可选值及其含义:

选项含义
SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

返回值 成功返回0,出错返回-1。

注意: 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少将其中一个信号递达。

sigpending

功能: 读取进程的未决信号集
函数原型

int sigpending(sigset_t *set);

sigpending函数读取当前进程的未决信号集,并通过set参数传出。该函数调用成功返回0,出错返回-1。

应用

#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>

using namespace std;

void show_pending(sigset_t *pending)

        for(int i=1;i<=31;i++)
                if(sigismember(pending,i))
                        cout<<"1";
                
                else
                        cout<<"0";
                
        
        cout<<endl;

int main()

        sigset_t in,out;
        sigemptyset(&in);
        sigemptyset(&out);

        sigaddset(&in,2);

        sigprocmask(SIG_SETMASK,&in,&out);//阻塞2号信号

        sigset_t pending;
        while(true)
                sigpending(&pending);//获取pending
                show_pending(&pending); //打印pending位图(1表示未决)
                sleep(1);
        
        return 0;

程序刚运行时,进程没有收到任何信号,此时pending表一直是全0,当我们使用kill命令向该进程发送2号信号后,由于2号信号是阻塞的,因此2号信号一直处于未决状态,pending表中的第二个数字变为了1。

为了看到pending表的变化,我们设置在10秒后解除2号信号阻塞状态,注意2号信号的功能是终止进程,为了看到pending表需要对2号信号捕捉。

#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>

using namespace std;

void show_pending(sigset_t *pending)

        for(int i=1;i<=31;i++)
                if(sigismember(pending,i))
                        cout<<"1";
                
                else
                        cout<<"0";
                
        
        cout<<endl;

void handler(int sigo)

        cout<<"get a signo:"<<sigo<<endl;

int main()

		signal(2,handler);// 捕捉2号信号
        sigset_t in,out;
        sigemptyset(&in);
        sigemptyset(&out);

        sigaddset(&in,2);

        sigprocmask(SIG_SETMASK,&in,&out);//阻塞2号信号
		
		int count=0;
        sigset_t pending;
        while(true)
                sigpending(&pending);//获取pending
                show_pending(&pending); //打印pending位图(1表示未决)
                sleep(1);
                if(count==20)
                        sigprocmask(SIG_SETMASK,&out,&in);//恢复2号信号
                        cout<<"my:";
                        show_pending(&in);
                        cout<<"recover default:";
                        show_pending(&out);
                
                count++;
                          
        
        return 0;

运行结果

捕捉信号

内核空间与用户空间

每一个进程都有自己的进程地址空间,该进程地址空间由内核空间和用户空间组成:

  • 用户所写的代码和数据位于用户空间,通过用户级页表与物理内存之间建立映射关系。
  • 内核空间存储的实际上是操作系统代码和数据,通过内核级页表与物理内存之间建立映射关系。

内核级页表是一个全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容。

注意: 当你访问用户空间时你必须处于用户态,当你访问内核空间时你必须处于内核态。

内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

当识别到信号的处理动作是自定义时,能直接在内核态执行用户空间的代码吗?

原则上来说是可以的,但是内核态是一种权限非常高的状态,用户可能会利用内核态做一些非法操作,为了避免该类情况发生,内核态需要切换到用户态才能自定义处理,即操作系统不信任用户。

sigaction

功能: 对信号进行捕捉(功能同signal函数)

函数原型

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

sigaction函数可以读取和修改与指定信号相关联的处理动作,该函数调用成功返回0,出错返回-1。

参数说明:

  1. signum代表指定信号的编号。
  2. 若act指针非空,则根据act修改该信号的处理动作。
  3. 若oldact指针非空,则通过oldact传出该信号原来的处理动作。

参数act和oldact都是结构体指针变量,该结构体的定义如下:

struct sigaction 
	void(*sa_handler)(int);
	void(*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask以上是关于Linux进程信号的主要内容,如果未能解决你的问题,请参考以下文章

linux中nohup 与 & 的区别

linux系统上信号发送和信号接收讲解

linux下 进程信号量和线程信号量的区别和联系是啥

进程信号(Linux)

linux进程信号

linux进程信号