Linux信号

信号概念

信号是进程之间事件异步通知的一种方式,属于软中断,在Linux系统下,可以使用指令 kill -l查看系统定义的信号列表:
在这里插入图片描述
我们可以看到每一个信号都有一个数字编号(注意没有0,32,33)和一个宏定义名称,这些宏定义可以在头文件<signal.h>中找到,我们在使用信号时既可以使用数字编号也可以使用宏定义名称,其中34及其以上的信号为实时信号,一旦产生就必须立即处理且要处理完,其余信号为标准信号,在本文章中,我们只讨论标准信号的产生和处理。

可以使用指令man 7 signal查看到各个信号的信息(一些信号由于某些原因并未列出):
在这里插入图片描述

每一个信号都有一个信号到来后默认的处理动作,该默认动作由内核指定,用户也可以自己指定进程对一个信号到来后的处理动作,在上图中Action列表示该信号的默认动作:
Term:终止进程
Core:终止进程并生成核心转储文件(core dump)
Ignore:忽略该信号
Stop:停止(挂起)进程
Cont:继续执行被停止的进程

这里解释一下核心转储(core dump)标志:
我们已经知道进程结束后进程的执行情况和执行结果等信息已经放在waitpid()的参数status中了,不过我们只关心他的低16位比特位,status的信息如下:
在这里插入图片描述
core dump标志位表示是否发生了核心转储,发生核心转储即在磁盘中生成core文件或core.pid(pid为当前进程的标识符)文件,该文件包括进程的执行情况、为什么出错等信息,一般与调试有关。
而要发生核心转储,需要满足2个条件:
①该信号的处理动作为Core
②Core功能处于打开状态

ulimit -a :该指令用于检查当前进程的Core功能是否打开,如果
core file size 的大小为0表示Core功能处于关闭状态
ulimit -c xxx:该指令用于打开当前进程的Core功能,xxx表示转储文件的大小,由用户自定义,如果将其设为0,表示关闭Core功能

云服务器一般默认是Core退出,但将Core功能关闭,因为如果核心转储文件是core.pid,由于每次重启进程的pid会不一样,某些未知的错误就会不断生成核心转储文件,最后导致服务器磁盘被占满,如果核心转储文件是core就不必担心这个问题。

信号产生

信号产生的方式可以分为5类:

1.通过kill命令产生
例如kill -9 xxx命令用于向指定进程发送9号信号从而杀死进程,kill -2 xxx命令用于向指定进程发送2号信号,从而终止进程。

2.通过键盘产生
可以通过键盘的一些组合键产生特定的信号,其本质是操作系统捕获到键盘的输入,将其解释成信号列表的对应的信号最后将其发送给当前进程。例如ctrl+c被OS解释成2号信号,用于终止前台进程(无法终止后台进程),ctrl+\被解释成3号信号用于终止进程(包括后台进程)。

3.系统调用产生
kill指令产生信号本质是通过调用kill函数实现的,kill函数可以给指定进程发送指定信号,raise函数只能给当前进程发送指定信号。
①kill函数

#include<signal.h>
int kill(pid_t pid,int signo);
//向进程标识符为pid的进程发送signo信号

②raise函数

#include<signal.h>
int raise(int signo);
//向当前进程发送signo信号

以上2个系统调用成功返回0,失败返回-1

③abort函数

#include<stdlib.h>
void abort(void);

该函数用于向当前进程发送6号信号从而使当前进程异常终止,该函数总会执行成功。

4.软件条件产生
在程序内部当满足某些条件时产生某些特定的信号,例如alarm函数用于在特定时间后向当前进程发送26号信号,默认动作是终止当前进程。

#include<unistd.h>
unsigned int alarm(unsigned int seconds);

在seconds秒后向当前进程发送26号信号,返回值为0或者以前设定的闹钟剩余的秒数,例如第一个闹钟设置为60秒之后响,但在40秒后又设置了第2闹钟,那么第2个闹钟的返回值为20,如果第2个闹钟的seconds设为0,表示取消以前的闹钟。

5.异常
某些硬件异常由硬件检测到并将异常通知内核,由内核向当前进程发送适当的信号,例如发生除0错误时,cpu运算单元产生异常,内核解释该异常并向当前进程发送8号信号,类似的还有访问越界(野指针)时,MMU产生异常,内核解释该异常并向当前进程发送11号信号等等。

信号保存

概念:

  1. 信号递达 :实际执行信号的处理动作称为信号递达
  2. 信号未决 :信号从产生到递达之间的状态称为信号未决

进程可以阻塞一些不能立即被处理的信号,被阻塞的信号处于未决状态,直至进程解除对该信号的阻塞才会执行递达的动作。而被阻塞的信号。
进程在内核中维护着2个位图(block位图、pending位图)和一张方法表(handler表):
在这里插入图片描述
block位图 :block位图位置表示信号的编号,内容表示该信号是否阻塞,其中1表示阻塞,该信号到来时不能递达,0表示不阻塞,该信号到来时直接递达。

pending位图 :pending位图位置表示信号的编号,内容表示该信号是否到来,1表示产生了该信号,此时该信号处于未决状态,直到信号递达才会消除该标志(先置0,再递达),0表示未产生该信号。

handler表 :handler表是一个函数指针数组,位置表示信号的编号,内容表示该信号递达时的执行动作,如果用户不指定,则执行默认的动作。

block位图与pending位图之间互不影响,即使进程没有接收到信号,block位图也可以将该信号对应位置置1阻塞该信号,如果在进程解除某个信号的阻塞之前,该信号多次产生,Linux的处理方法为:对标准信号无论在这期间产生多少次,都只计一次,对实时信号,其会被依次放到一个队列里面。
只有某个信号在block位图中内容置0,在pending位图中内容置1,该信号才会递达去执行handler表中对用的方法。

在实际中,为了方便拓展,block位图和pending位图都是用一个结构体sigset_t表示,其成员只有一个整数,该结构体被称为信号集,用来表示block位图的称为阻塞信号集(也称为当前进程的信号屏蔽字),用来表示pending位图的称为未决信号集。
系统提供了5个函数对信号集进行操作:

#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() :函数sigemptyset初始化set所指向的信号集set,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号,成功返回0,失败返回-1。

sigfillset() :函数sigfillset初始化set所指向的信号集set,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号,成功返回0,失败返回-1。

sigaddset() :函数sigaddset在该信号集set中添加有效信号signo,成功返回0,失败返回-1。

sigdelset() : 函数sigdelset在该信号集set中去除有效信号signo,成功返回0,失败返回-1。

sigismember() :检查信号signo是否出现在信号集set中,在返回1,不在返回0,出错返回-1。

在定义完一个信号集后,应先使用sigemptyset()sigfillset() 对其进行初始化再进行其他操作。

有了对信号集的操作,用户就可以对block位图、pending位图和handler表进行操作了:

1.对block位图进行操作

#include<signal.h>
sigprocmask(int how,const sigset_t *set,sigset_t *oset);

该系统调用用于对block位图进行操作,成功返回0,出错返回-1,参数:
set :set是我们希望添加到当前信号屏蔽字的信号集。

oset :oset是一个输出型参数,如果其未非空,则读取当前进程的信号屏蔽字通过oset传出,一般设为nullptr即可。

how :how参数有3个可选值:SIG_BLOCK ,SIG_UNBLOCK ,SIG_SETMASK,假设当前信号屏蔽字为cur,最后设置的信号屏蔽字为result

  • ①SIG_BLOCK:
    表示我们想在当前信号集中再添加set信号集中的信号,即:result=cur|set
  • ②SIG_UNBLOCK:
    表示我们想在当前信号集中去除set信号集中的信号,即:
    result=cur&~set
  • ③SIG_SETMASK:
    表示我们想将当前信号集中设置为set信号集中的信号,即:
    result=set

需要注意的时LInux不允许用户对9和19号信号进行阻塞,以防止非法程序将这2个信号阻塞,OS发送的杀死非法进行的信号一直无法递达。
2.对pending位图进行操作

#include<signal.h>
int sigpending(sigset_t *set);

用户只能对pending位图进行查看操作,该系统调用用于查看当前进程的未决信号集,通过参数set传出。成功返回0,失败返回-1.

3.对handler位图进行操作

①signal()

#include<signal.h>
sighandler_t signal(int signum, sighandler_t handler);

该函数用于将信号signum的处理动作改为执行函数handler,成功返回该信号之前的处理函数的指针,失败返回SIG_ERR,其中handler函数返回值要为空,参数要求为一个整型,系统自动将触发执行该函数的信号(即函数signal()的参数signum)作为参数传给handler。

void handler(int signum);

如果给函数signal()的handler参数传SIG_IGN表示忽略signum信号。

②sigaction()

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

该系统调用用于检查或修改与指定信号相关联的处理动作,比 signal() 函数提供了更强大和更灵活的信号处理机制,执行成功时返回 0,失败返回 -1 并设置errno以指示错误,参数:

  • signum:需要操作的信号编号
  • act:指向 sigaction 结构体的指针,如果非空,则该结构体描述了新的信号处理行为。
  • oldact:指向 sigaction 结构体的指针,如果非空,则该结构体用于返回信号之前的处理行为,一般设为nullptr即可

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);
};

sigaction 结构体的成员说明:

  • sa_handler:这是一个函数指针,指向信号处理函数。如果设置为 SIG_IGN,信号将被忽略;如果设置为 SIG_DFL,则恢复默认行为。
  • sa_sigaction:另一个信号处理函数指针,提供了比 sa_handler 更多的信息,比如信号的来源。
  • sa_mask:在信号处理函数执行期间,这个信号集里的信号将被阻塞。
  • sa_flags:影响信号处理行为的标志,比如 SA_RESTART 可以使被信号打断的系统调用自动重新发起,设为0即可。
  • sa_restorer:这是一个已废弃的成员,不应该使用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字,sa_sigaction()是实时信号的处理函数,这里不做讨论。

信号处理

信号的处理方式有3种:

  1. 忽略此信号
  2. 执行该信号的默认动作
  3. 用户自定义处理(捕捉信号)

信号处理的时机也是有要求的,只有计算机进入内核态,从内核态返回用户态时才会检测并处理信号,而计算机一般会因中断、异常、系统调用等原因进入内核态。

捕捉信号

如果信号的处理动作是用户自定义的函数,在信号递达时会就会调用这个函数,这被称为捕捉信号,其涉及到4次用户态和内核态之间的相互转换:
在这里插入图片描述

例如用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行主控制流程。
对于默认处理动作或忽略信号计算机在内核态就直接处理了,不必返回用户态,而用户自定义处理却要返回用户态是因为内核态权限比用户态高,而用户自定义处理动作是有可能越权的。

补充

内核态和用户态

在Linux种页表可以分为用户级页表和内核级页表(其实本质只有一张页表,只不过有一个权限位标志该部分是用户的还是内核的),用户级页表映射当前进程的用户数据,内核级页表映射OS的数据:
在这里插入图片描述
用户级页表随进程不同而不同,但内核级页表都是一样的,这意味着无论进程怎么切换,总能通过进程3G-4G的虚拟地址找到并访问OS,为了不让用户通过3G-4G进程地址空间直接访问OS,就必须对用户的运行模式进行区分,因此将用户运行模式划分为内核态和用户态(本质是通过CS寄存器来标记),只用运行模式为内核态才可以通过3G-4G虚拟地址空间访问OS。

SIGCHLD信号

我们知道可以用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。其实我们也可以利用信号回收子进程:
方法1
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

//简略的使用方法
#include <iostream>
#include <signal.h>

void handler(int sig)
{
	 pid_t id;
	 while( (id = waitpid(-1, NULL, WNOHANG)) > 0)
	 {
		//处理
	 }
}
int main()
{
	 signal(SIGCHLD, handler);
	 return 0;
}

handler()函数中waitpid()要循环的原因是:可能会有多个子进程向父进程发送了 SIGCHLD 信号,但父进程还没来得及处理,由于系统只记录了一次 SIGCHLD 信号,如果不循环的话父进程只回收了一个子进程。
handler()函数中waitpid()要选用非阻塞等待 WNOHANG 是因为有可能有一部分子进程并没有退出,如果选用阻塞等待父进程就得一直等待子进程退出。

方法2
由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法,父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction()函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用

可重入函数

假设现在我有一条链表,现在我向链表插入一个结点node1,该节点的插入操作只执行到一般,就由于时间片到了等原因切换到另一个执行流中,该执行流也要对该链表进行插入结点node2,最后会发生什么呢?
在这里插入图片描述
我们发现链表插入出错了,像上面这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。判断一个函数是否是可重入函数,只需要判断在多执行流中该函数是否会出错。
如果一个函数符合以下条件之一则是不可重入的:

  1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

volatile用于保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。我们在编译时可以选择优化级数:

g++ filename.cpp -o0//不优化
g++ filename.cpp -o1
g++ filename.cpp -o2
g++ filename.cpp -o3
//数字越大表示优化程度越高
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值