【Linux】进程信号

文章目录

    • 一、生活信号
    • 二、进程信号
    • 三、查看信号kill -l与信号解释man 7 signal
    • 四、信号的产生
      • 1.按键产生
        • ctrl+c——2号信号
        • ctrl+\——3号信号
      • 2.系统调用
        • kill——向进程发送任意信号
        • raise——进程给自己发送任意信号
        • abort——进程给自己发6号信号
      • 3.硬件异常产生信号
        • 除零发送8号信号
        • 野指针发送11号信号
      • 4.软件条件
        • 管道——13号信号SIGPIPE
        • 定时器——4号信号SIGALRM
      • 5.小结
    • 五、捕捉信号的方法
        • signal
        • sigaction
    • 六、核心转储
    • 七、信号的保存——位图
      • 1.相关概念
      • 2.内核中的表示
      • 3.信号集——sigset_t
      • 4.信号集操作函数
    • 八、信号的捕捉
      • 1.内核态与用户态
      • 2.信号捕捉过程
    • 九、可重入函数
    • 十、关键字volatile
    • 十一、SIGCHLD信号

一、生活信号

生活中有很多的信号,比如闹钟、消息提醒、手机铃声,红绿灯。但是人是怎么识别红绿灯的,识别信号的?通过认识产生行为:有人通过教育的手段让我们在大脑中记住了对应的红绿灯属性或者行为;但是当信号到来的时候,我们不一定会马上去处理这个信号:信号可以随时产生(异步),而我们可能会做更重要的事情;信号到来的时候在到信号被处理一定会有时间窗口,必须得记住这个信号;

默认动作、自定义动作、忽略动作

处理信号会有默认动作,比如红灯停,绿灯行。但是也会有自定义动作:比如过红绿灯跟别人不一样。也有忽略动作:忽略红绿灯,忽略闹钟等。


二、进程信号

通过生活信号联系到进程信号

信号是给进程发的,比如我们之前使用过的指令:kill -9 pid

而进程又是如何识别信号的?认识+动作

进程本身是被程序员编写的属性和逻辑的组合,由程序员编码完成的;当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定被处理;进程本身必须要对于信号的保存能力;进程在处理信号的时候一般有三种动作:默认、自定义、忽略,处理信号也可被称为信号被捕捉

如果信号是发给进程的,而进程是要保存的,那么应该保存在哪里?task_struct结构里,如何保存?保存是否收到了指定的信号,信号:用比特位的位置代表信号的编号,比特位的内容代表是否收到该信号,0表示没有,1表示有

如何理解信号的发送?发送信号的本质就是修改PCB中的信号位图。PCB是内核维护的数据结构对象,所以PCB的管理者是OS,所以只有OS能修改PCB中的内容,无论未来我们学习多少中发送信号的方式,本质都是通过OS向目标进程发送的信号!也就是OS必须要提供发送信号处理信号的相关系统调用!所以对于我们之前所使用的kill命令 也是调用了对应的系统调用


三、查看信号kill -l与信号解释man 7 signal

用kill -l命令可以察看系统定义的信号列表 :

【Linux】进程信号_第1张图片

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到

man 7 signal可以查看信号详细信息的命令

【Linux】进程信号_第2张图片

Term是正常结束,OS不会做额外的工作,Core代表OS初了终止的工作,还有其他工作。详细请见本篇第点!!!


四、信号的产生

1.按键产生

ctrl+c——2号信号

ctrl+c:热键,ctrl+c实际一个组合键,OS会将ctrl+c解释成2号信号:

【Linux】进程信号_第3张图片

对于默认2号信号的行为是终止进程,打开man 7 signal下滑:

        Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard
       SIGQUIT       3       Core    Quit from keyboard
       SIGILL        4       Core    Illegal Instruction
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating point exception
       SIGKILL       9       Term    Kill signal
       SIGSEGV      11       Core    Invalid memory reference
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
                                     readers
       SIGALRM      14       Term    Timer signal from alarm(2)
       SIGTERM      15       Term    Termination signal
       SIGUSR1   30,10,16    Term    User-defined signal 1
       SIGUSR2   31,12,17    Term    User-defined signal 2
       SIGCHLD   20,17,18    Ign     Child stopped or terminated
       SIGCONT   19,18,25    Cont    Continue if stopped
       SIGSTOP   17,19,23    Stop    Stop process
       SIGTSTP   18,20,24    Stop    Stop typed at terminal
       SIGTTIN   21,21,26    Stop    Terminal input for background process
       SIGTTOU   22,22,27    Stop    Terminal output for background process


所以当我们ctrl+c的时候该进程直接进入结束状态

ctrl+\——3号信号

ctrl+|实际上是发送3号信号

int main()
{
    while(true)
    {
        cout<<"hello world"<

直接按下按键ctrl+:

【Linux】进程信号_第4张图片

也可以kill -3 +pid:

【Linux】进程信号_第5张图片

键盘是硬件,通过组合键按下给OS识别,OS将组合键解释成信号,向目标进程发信号,目标进程在合适的时候处理这个信号,对于2号和3号信号处理动作默认为终止进程

2.系统调用

除了键盘向前台进程发送信号之外,前台进程会影响shell,linux规定跟shell交互的时候只允许有一个前台进程,默认情况下bash也是一个进程。而实际上当我们自己运行进程时,我们的进程就变成前台进程了,而bash自动被切到后台。

kill——向进程发送任意信号

kill:可以向任意进程发送任意信号

NAME
       kill - send signal to a process

SYNOPSIS
       #include 
       #include 
       int kill(pid_t pid, int sig);
RETURN VALUE
    On  success  (at  least  one  signal was sent), zero is returned.  On error, -1 is returned, and errno is set appropriately.

发送信号的能力是OS的,但是有这个能力并不一定有使用这个能力的权力,一般情况下用户决定OS向目标进程发信号。所以OS有这个能力,那么对外提供能力只能通过系统调用的接口的方式来让用户向目标进程发送信号。

代码实现:通过kill与命令行参数相结合:

static void Usage(const string&proc)
{
    cout<<"\nUsage:"<

【Linux】进程信号_第6张图片kill命令底层实际上就是kill系统调用,信号的发送由用户发起而OS执行的。

raise——进程给自己发送任意信号

raise:给自己发送任意信号

NAME
       raise - send a signal to the caller

SYNOPSIS
       #include 
       int raise(int sig);

RETURN VALUE
     raise() returns 0 on success, and nonzero for failure.

代码实现:

int main(int argc,char*argv[])
{
    //raise()给自己发送任意信号
    int cnt = 0;
    while(cnt<=10)
    {
        printf("cnt:%d\n",cnt++);
        sleep(1);
        if(cnt>=5)
        {
            raise(3);//kill(getpid(),signo);
        }
    }
}

【Linux】进程信号_第7张图片

abort——进程给自己发6号信号

abort:终止进程的方式,给自己发送指定的信号SIGABRT

NAME
       abort - cause abnormal process termination

#include 
void abort(void);

int main(int argc,char*argv[])
{
    int cnt = 0;
    while(cnt<=10)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        sleep(1);
        if(cnt>=5)
        {
           abort();//kill(getpid(),SIGABRT)
        }
    }
}

【Linux】进程信号_第8张图片

关于信号处理的行为的理解:有很多的情况进程收到大部分的信号,默认处理动作都是终止进程。

信号的意义:信号的不同代表不同的事件,都是对事件发生之后的处理动作是可以一样的。

3.硬件异常产生信号

除零发送8号信号

信号产生,不一定非得用户显示的发送,有些情况下信号会在OS内部自动产生。

int main(int argc,char*argv[])
{
    //3.产生信号的方式:硬件异常产生信号
    //信号产生,不一定非得用户显示的发送
    while(true)
    {
        cout<<"我在运行中..."<

【Linux】进程信号_第9张图片

为什么/0会终止进程:除0当前进程会受到来自OS系统的信号SIGFPE

证明:通过signal接口,把SIGFPE信号自定义捕捉:

void catchSig(int signo)
{
    cout<<"获取到一个信号,信号编号是:"<

【Linux】进程信号_第10张图片

OS 系统如何得知应该给当前进程发送8号信号的?CPU异常,除0理解:

CPU内有很多寄存器eax,edx等,执行int a=10,a/=0;CPU内除了数据保存,还得保证运算有没有问题,所以还有状态寄存器,状态寄存器衡量这次的运算结果,10/0.相当于10乘以无穷大,结果无穷大,引起状态寄存器溢出标记位由0变成1,CPU发生了运算异常,OS得知CPU发生运算异常,就要识别异常:状态寄存器的标记位置为1,由当前进程导致的,在向目标进程发送信号,最后就终止进程了。

我们可以看到上面的结果:收到信号不一会引起进程的退出

**收到信号不一定会引起进程退出!**进程没有退出,则还有可能还会被调度,CPU内部的寄存器只有一份,但是寄存器中的内容属于当前进程的上下文,一旦出现异常我们没有能力去修正这个问题,所以当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1。

野指针发送11号信号

野指针

int main(int argc,char*argv[])
{
    while(true)
    {
        cout<<"我在运行中..."<

image-20230226091058363

野指针崩溃:收到了11号信号

void catchSig(int signo)
{
    cout<<"获取到一个信号,信号编号是:"<

image-20230226091657131

OS会给当前进程发送11号信号,11号信号代表非法的内存引用(man 7 signal).OS又怎么知道野指针:野指针的时候也会引起虚拟地址到物理内存之间转化时对应的MMU报错,进而OS识别到报错,转换成信号

【Linux】进程信号_第11张图片

4.软件条件

管道——13号信号SIGPIPE

比如我们之前所说的管道,如果读端关闭,写端一直在写,写的数据没有读就没有意义了,OS不允许这样子,会终止这个进程,向写进程发送13号信号SIGPIPE。管道跟OS发信号的原因是因为读端关闭软件条件触发的。

定时器——4号信号SIGALRM

定时器软件条件:alarm():设定闹钟,调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

NAME
       alarm - set an alarm clock for delivery of a signal
#include 

unsigned int alarm(unsigned int seconds);
int main(int argc,char*argv[])
{
    //软件条件
    alarm(1);
    int cnt = 0;
    while(true)
    {
        cout<<"cnt:"<

【Linux】进程信号_第12张图片

这份代码的意义在于可以统计1S左右,我们的计算机能够将数据累计多少次。实际上这种方法是比较慢的,为什么?打印时是要进行输出的,输出是外设,外设IO较慢。如果没有打印:

int cnt = 0;
void catchSig(int signo)
{
    cout<<"获取到一个信号,信号编号是:"<

【Linux】进程信号_第13张图片

理解闹钟为软件条件:

“闹钟”其实就是用软件实现的,任意一个进程都可以通过alarm系统调用在内核中设置闹钟,那么OS内可能会存在很多的闹钟,OS则需要管理闹钟:先描述,再组织,所以OS内部设置闹钟的时候,要为闹钟创建特定的数据结构对象。

【Linux】进程信号_第14张图片

内核管理闹钟比如最大堆、最小堆:比如100个闹钟可以把100个闹钟的when建小堆,最小的就在堆顶,只要堆顶的没有超时那其余的自然没有超时,所以只需要检查堆顶即可,就可以管理好闹钟。

5.小结

上面所说的所有信号产生,最终都要有OS来进行执行,因为OS是进程的管理者
信号的处理在合适的时候处理的
信号如果不是被立即处理,那么信号需要暂时被进程记录下来,记录在PCB中
一个进程在没有收到信号的时候能知道自己应该对合法信号作何处理,程序员默认在系统中写好的
理解OS向进程发送信号:OS直接修改目标进程的PCB信号位图


五、捕捉信号的方法

signal

signal:通过signum方法设置回调函数,设置某一信号的对应动作

#include 
typedef void (*sighandler_t)(int);//函数指针

sighandler_t signal(int signum, sighandler_t handler);

直接通过代码来理解signal这个接口:

void handler(int signal)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<

【Linux】进程信号_第15张图片

ctrl+c的时候并没有终止进程,这是我们把默认动作设置成自定义动作,想让其终止:exit(0),或者直接请上大杀器:kill -9 +pid

image-20230225170904669

sigaction

sigaction的作用域signal一模一样,对特定信号设置特定的回调方法。

#include 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//act:结构体对象;oldact:输出型参数,获取特定信号老的处理方法
struct sigaction {
               void     (*sa_handler)(int);//回调方法
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;//信号集
               int        sa_flags;
               void     (*sa_restorer)(void);
           };
RETURN VALUE
     sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

一个进程在运行时,未来会收到大量同类型的信号,如果收到同类型的信号,当前正在处理某个信号信号时,会发生什么?OS会不会允许频繁进行信号提交?

#include 
#include 
#include 
#include 
using namespace std;
void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
void handler(int signo)
{
    cout<<"get a signo:"<

【Linux】进程信号_第16张图片

1.当我们进行正在递达某一个信号期间,同类型信号无法递达——当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字,在block表中自动将2号信号屏蔽。

而当信号完成捕捉动作,系统又会自动解除对该信号的屏蔽

【Linux】进程信号_第17张图片

一般一个信号被解除屏蔽的时候,会自动进行递达当前屏蔽信号,如果该信号已经被pending的话,没有就不做任何动作

进程处理信号的原则是串行的处理同类的信号,不允许递归式处理

小细节:屏蔽2号的同时还想屏蔽3号,只需要加上:

sigemptyset(&act.sa_mask);//当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigaddset(&act.sa_mask,3);

六、核心转储

数组越界不一定会导致程序崩溃,实际数组编译器在编译代码时在栈上开辟多大空间与编译器强相关,数组大小是10个元素在栈帧结构上分配的字节数可能很大,数组越界可能还是在有效的栈区中,所以没有报错,OS在识别越界可能识别不出来。

int main()
{
    //核心转储
    while(true)
    {
        int a[10];
        //a[100]=10;//没报错
        a[10000] = 10;
    }
}

image-20230226143212722

man 7 signal

【Linux】进程信号_第18张图片

Term是正常结束,OS不会做额外的工作,Core代表OS初了终止的工作,还有其他工作。

在云服务器上,默认如果进程是core退出的,我们暂时看不到明显的现象,想看到现象,我们需要打开ulimit -a:查看系统给当前用户设置各种资源上限:

【Linux】进程信号_第19张图片

core file size设置成了0,这是云服务默认关闭了core file选项,想看到现象:ulimit -c

image-20230226144201056

此时我们重新运行./mysignal:

image-20230226144336602

【Linux】进程信号_第20张图片

输出报错多了core dumped:core代表核心,dumped:转储,核心转储,转储到:在当前目录下以core命名,后面跟了数字:引起core问题的进程的pid。

核心转储是当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中

形成核心转储的意义:一旦进程出现崩溃的情况,我们更想知道为什么会崩溃,在哪里崩溃,所以OS为了方便调试,会在进程崩溃的上下文数据全部dump到磁盘当中,用来支持调试。

如何支持:gdb

【Linux】进程信号_第21张图片

这种直接快速进行调试的方式就叫事后调试,在gdb中上下稳重直接core-file core.xxxx。因为是核心转储,在进程终止时,只会检测core方式终止的进程

以core退出的是可以被核心转储的,后续可以快速定位问题。以Term终止的,一般是正常下的终止进程

至此,核心转储结束。


七、信号的保存——位图

1.相关概念

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

2.内核中的表示

在进程内部要保存信号周边的信息,有3种数据结构与之是强相关的,第一个是pending表,pending表就是位图。如何理解:进程可能在任何时候收到OS给它发送的信号,该信号可能暂时不被处理,所以需要暂时被保存,进程为了保存信号采用位图来保存,这个位图就是pending位图,对应的信号被置于pending位图的信号就是该信号处于未决状态。

所以OS向进程发信号就是向目标进程的peding位图设置比特位,从0到1就是当前进程收到该信号,所以发信号应该是写信号,PCB属于OS内核结构,只有OS有权力修改pending位图,所以发送信号的载体只能是OS。

除了pending位图之外,还存在block位图:block位图比特位的位置代表信号标号,比特位的内容代表是否阻塞了该信号

此外,还有一个:typedef void(*handler_t)(int signo),handler_t handler[32]={0},这个就是函数指针数组,这个数组在内核中有指针指向它,这个数组称为当前进程所匹配的信号递达的所有方法,数组是有下标的,数组的位置(下标)代表信号的编号,数组下标对应的内容表示对应信号的处理方法、

也就是下面这一张图:在内核中,信号的基本数据结构构成

【Linux】进程信号_第22张图片

我们之前所谈到的信号接口signal(signo,handler)的本质就是在做拿到信号在对应的数组找到对应的位置,然后将用户层设置的handler函数的地址填充进对应下标处,未来信号产生时候,修改比特位,并且该比特位没有被阻塞,OS立马拿到信号根据信号位置得到信号的编号,进而访问数组得到方法。

因为是内核数组结构,所以OS可以对应使用对应的系统接口来对数据结构任意访问。

结论:如果一个信号没有产生,并不妨碍它可以先被阻塞。进程能够识别信号是因为程序员在设置体系的时候在内核中为每个进程设置好了这3种结构能够识别信号

3.信号集——sigset_t

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

4.信号集操作函数

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

#include 
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);

sigprocmask :读取或更改进程的信号屏蔽字(阻塞信号集)

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

sigpending

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

八、信号的捕捉

前面说过,信号产生的时候,信号可能不会立即处理,会在合适的时候处理。合适的时候就是从内核态返回用户态的时候进程处理,这也说明了曾经一定是先进入了内核态,最典型的就是系统调用与进程切换

1.内核态与用户态

用户代码和内核代码,平时我们自己写的代码是属于用户态的代码,但是用户态难免会访问OS自身的资源(getpid,waitpid…),硬件资源(比如printf,write,red…),用户自己写的代码为了访问资源必须直接或间接访问OS提供的接口,必须通过系统调用来完成访问。系统调用是OS提供的接口,而普通用户不能以用户态的身份执行系统调用,必须让自己的身份变成内核态。

实际执行系统调用的“人”是“进程“,但是身份其实是内核。从用户态调内核态需要身份的切换,还要调OS内部的代码,所以一般系统调用比较费时间一些。我们应该尽量避免频繁调用系统调用。

一个进程在执行时必须把上下文信息投递到CPU中,CPU中有大量的寄存器,寄存器可分为可见寄存器(eax,ebx…),不可见寄存器(状态寄存器…),凡是和当前进程强相关的,是上下文数据。

寄存器中还有非常多的寄存器在进程中有特定作用,寄存器可以指向进程PCB,也可以保存当前用户级的页表,指向页表起始地址

寄存器中还有CR3寄存器:表征当前进程的运行级别:0表示内核态,3表示用户态,这就能够辨别是用户态还是内核态了

如何理解我是一个进程怎么跑到OS中执行方法呢?

以前所说的进程地址空间0-3G是用户级页表,通过用户级页表映射到不同的物理空间处,而除了用户级页表之外,还有内核级页表,OS为了维护从虚拟到物理之间的OS级别的代码所构成的内核级映射表,开机时OS加载到内存中,OS在物理内存中只会存在一份,因为OS只有一份,所以OS的代码和数据在内存中只有独一份,当前进程从3-4GB映射的时候将当前内核的代码和数据映射到我们所对应的当前进程的3-4G,此时使用内核级页表就行了,所以内核级页表只有一份就可以了。所以每个进程都可以自己特定的区域内以内核级页表的方式访问OS的代码和数据

3G-4G是OS内部的映射,所以进程建立映射的时候不仅仅把用户的代码和数据和进程产生关联,每一个进程都要通过用户级页表和OS产生关联,而每一个进程都有自己的地址空间,其中用户空间独占,而内核空间是被映射到了每一个进程的3-4G空间,每一个进程都可以通过页表映射到OS,而且每个进程看到的OS都是一样的,所以进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了

每一个进程都有3-4GB,都会共享一个内核级页表,无论进程如何切换,都不会更改任何的3-4GB。用户通过什么能够执行访问内核的接口或者数据呢?OS读取CPU中的CR3寄存器,读取运行状态,当是0内核态时才能去进行访问,所以系统调用接口起始的位置会帮我们把用户态变成内核态,从3号状态改成0号状态。所以系统调用的前半段是在用户态跑的,OS是如何通过系统调用把用户态变成内核态的:中断汇编指令int 80就是陷入内核,简单理解把状态由用户态改成内核态。调用结束时在切回来

【Linux】进程信号_第23张图片

无论是用户态还是内核态,一定是当前进程正在运行,无非就是当前执行级别是用户态还是内核态,页表是用户级页表还是内核级页表,包括访问的资源。

2.信号捕捉过程

通过系统调用,陷入内核,从用户态进入内核态,按理来说也会直接从内核态进入用户态,但是并不是直接返回用户态,陷入内核比较费时间,进去之后OS会做其他工作,所以OS会在进程的上下文中搜索,拿到task_struct找到进程,查3张表,先查block表:block为0说明没被阻塞,继续看pending,pending为0继续下一个…

【Linux】进程信号_第24张图片

这个过程太过于复杂,我们可以简化一下:

【Linux】进程信号_第25张图片


九、可重入函数

一般而言,我们认为main执行流和信号捕捉执行流是两个执行流!

如果在main中和在handler中,该函数被重复进入,此时问题,则该函数(比如insert)称为不可重入函数

如果在main中和在handler中,该函数被重复进入,此时不出问题,则该函数(比如insert)称为可重入函数

而我们目前大部分情况下用的接口,全部都是不可重入的,重入不重入是特性。

比如典型的insert函数就是不可重入函数:

【Linux】进程信号_第26张图片

main函数调用insert,向链表head插入Node1,insert只做了第一步,然后就被中断(或者因为信号原因执行信号捕捉),此时进程挂起,然后唤醒在次回到用户态检查有信号待处理,于是切换到sighandler方法,sighandler也调用了insert函数,要把Node2头插到链表里,Node2的next结点指向下一个结点位置,下一步就是head指向Node2,完成Node2的头插,信号捕捉完之后就成功把Node2插入,接下来回到main执行流,对Node1完成插入的第二步动作,此时把head指向Node1,最后只有Node1真正插入到链表之中,而Node2结点找不到了,发生内存泄漏,出现问题。

不可重入函数:

调用了malloc或free,因为malloc也是用全局链表来管理堆的。

调用了标准I/O库 函数。标准I/O库的汗多实现都以不可重入的方式使用全局数据结构。


十、关键字volatile

对代码进行优化后(-03),通过信号自定义方法handler修改全局q,但是程序不会退出。

#include 
#include 
int quit = 0;
void handler(int signo)
{
    printf("%d 号号信号,正在被捕捉\n",signo);
    printf("quit:%d",quit);
    quit = 1;
    printf("->%d\n",quit);
}
int main()
{
    signal(2,handler);
    while(!quit);
    printf("注意,我是正常退出的\n");
    return 0;
}

【Linux】进程信号_第27张图片

O3优化时:编译器认为quit在main执行流中只被检测,没有被修改,所以编译器对quit做了优化,将quit放在了寄存器中,这样后续执行时就不用再去内存中读取q了,提高了程序运行效率。虽然handler中修改了内存中的quit,hadler只是改了内存中的quit,与CPU内保存的预加载优化到寄存器的无关,所以无论怎么改,寄存器中的quit值一直不变,一直为0,while循环因为代码优化,检测时一直读取寄存器的值,而不会去内存中读取数据,逻辑反就是为真,一直循环(寄存器中的quit值是临时值),所以结果如上所示。

这就相当于寄存器的存在遮盖了物理内存当中quit变量存在的事实。

volatile保持内存可见性!

解决:给quit加volatile关键字,quit通过内存读取而不是寄存器,保持变量quit的内存可见性!

#include 
#include 
volatile int quit = 0;
void handler(int signo)
{
    printf("%d 号号信号,正在被捕捉\n",signo);
    printf("quit:%d",quit);
    quit = 1;
    printf("->%d\n",quit);
}
int main()
{
    signal(2,handler);
    while(!quit);
    printf("注意,我是正常退出的\n");
    return 0;
}

image-20230314123224869


十一、SIGCHLD信号

子进程退出时,会向父进程发送17号信号SIGCHLD的。

简单证明:

#include 
#include 
#include 
#include 
void handler(int signo)
{
    printf("pid:%d, %d 号信号,正在被捕捉!\n",getpid(),signo);
}
int main()
{
    signal(SIGCHLD,handler);//17号信号
    printf("我是父进程:%d,ppid:%d\n",getpid(),getppid());
    pid_t id = fork();
    if(id==0)
    {
        printf("我是子进程:%d,ppid:%d,我要退出了\n",getpid(),getppid());
        exit(1);
    }
    while(1)
        sleep(1);
    return 0;
}

image-20230314182554557

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

signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);

image-20230315080210802

注意:虽然SIGCHLD的默认动作就是忽略,但是与手动设置表现的不一样,默认是收到信号就进行处理,该等还得等,而如果我们手动设置了SIG_IGN,子进程退出时发送给父进程的信号会被父进程忽略,但是子进程会被OS回收,这是有所区别的。含义不一样

你可能感兴趣的:(学好Linux,linux,运维,服务器)