【Linux】-- 进程信号(处理、内核)

news2024/11/26 9:32:20

上篇:【Linux】-- 进程信号(认识、应用)_川入的博客-CSDN博客


目录

信号其他相关常见概念

pending

handler

block

信号处理的过程

sigset_t

sigset_t使用

系统接口

sigpending

sigprocmask

捕捉方法

sigaction

struct sigactio

sa_mask

补充

可重入函数

volatile

SIGCHLD信号


信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
    • 信号递达:可能是默认、可能是忽略、可能是自定义捕捉。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
    • 信号产生,进程不是立即处理这个信号,不代表其不会处理。意味着未来会处理,于是从收到信号到未来准备处理时:信号存在但是没有被处理 —— 信号被临时保存(PCB的位图中),此时就被称为信号未决 —— Pending位图。

融汇贯通的理解:

        所以,前面所提的临时存储概念是不准确的,应该称作为信号未决(Pending)

  • 进程可以选择阻塞 (Block)某个信号。
    • 进程是可以将处于未决的信号就是不递达其,屏蔽某些信号 —— 阻塞 (Block)。

忽略和阻塞的区别:

        忽略:已经递达了,已经处理该信号了,只不过处理动作是忽略。

        阻塞:根本就不会进行抵达,不进入信号处理流程。(阻塞即永远不递达)

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

        为了支持递达、未决、阻塞三个概念,其在内核中有对应的表现 —— 内核当中有对应的三张表

pending

        pending就是之前所提的位图操作系统就是修改pending位图中指定的位置,来完成信号的发送过程。

handler

        当收到了一个信号,操作系统会在pending中修改对应的位图。处理信号的时候就会根据为1的信号,拿着信号去handler数值中索引。对应调用handler中的函数指针的方法,去完成信号捕捉就可以了。

        所以,signal是两个参数的原因也在此,是根据第一个元素signum找到handler数组对应的位置,将第二个参数handler作为数据存入。

但是需要注意,进程对于信号的处理方式有三种

  • 默认(进程自带的,程序员写好的逻辑)
  • 忽略(也是信号的一种处理方式)
  • 自定义动作(捕捉信号)

        所以,此处的handler中并不只是signal函数这么简单。

(路径:/usr/include/bits/signum.h

block

        block位图,结构和pending一摸一样。位图中的内容,代表的含义是对应的信号是否被阻塞。

信号处理的过程

信号处理的大致流程图可以画为:

1. 向操作系统向pending中发送信号。

2. 处理信号,遍历pending找到为1的信号。

3. 找到之后,去对应的block中查看是否为1。block为1,该信号永远不递达。block为0,该信号合适的时候递达。

4. block为0且需要抵达,根据handler数组中的数据处理信号。

sigset_t

        为了支持我们更好的编程,信号在内核当中是一个位图,不可能让我们直接操作其,也不可能让我们操作。操作系统为我们提供了一个类型sigset_t

        因为,操作系统提供的类型,需要与操作系统提供的.h文件相对应,也就是与系统调用接口相对应。因为有的接口不允许用户传语言层的参数,需要传一个结构体、一个位图等。于是操作系统必须提供对应的类型。

融汇贯通的理解:

        其实语言级的.h、.hpp也一样,因为只要涉及硬件级别的操作,就必须通过操作系统,那么就需要使用操作系统提供的.h以及操作系统提供的类型 —— 语言级的.h、.hpp一定对操作系统提供的.h与类型有着包含。最典型的就是文件操作。

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

#endif

        sigset_t是位图结构,操作系统提供的类型。

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

sigset_t使用

1. sigset_t —— 不允许用户自己进行位操作 —— 操作系统给我们提供了对应的操作位图的方法。

#include <signal.h>
系统接口意义返回值
int sigemptyset(sigset_t *set);
初始化set所指向的信号集,所有信号位清0,表示该信号集不包含任何有效信号。

成功时返回0

错误时返回-1

int sigfillset(sigset_t *set);
初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
int sigaddset (sigset_t *set, int signo);
向set所指向的信号位,添加某种有效信号。
int sigdelset(sigset_t *set, int signo);
向set所指向的信号位,删除某种有效信号。
int sigismember(const sigset_t *set, int signo);

判断在set所指向的信号集中是否包含某种信号.

包含返回1

不包含返回0

错误返回-1

2.  sigset_t —— user是可以直接使用该类型 —— 和内置类型 && 自定义类型没有任何差别。

3.  sigset_t —— 一定需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象。

系统接口

sigpending

#include <signal.h>

-

int sigpending(sigset_t *set);

获取pending位图。

-

参数:

        set:类型为sigset_t的位图。

返回值:(如果发生错误,将设置errno以指示原因)

  • 成功时返回0。
  • 错误时返回-1。

为什么pending只有获取,为什么没有设置?因为其实前面讲的信号产生就是在设置。

sigprocmask

#include <signal.h>

-

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-

参数:

        how:参数更改信号屏蔽字。

        set:类型为sigset_t的位图。

        oldset:将原来的信号屏蔽字备份到oldset里。

返回值:(如果发生错误,将设置errno以指示原因)

  • 成功时返回0。
  • 错误时返回-1。

how参数(宏):下列宏没有任何交集,不自持按位|、按位&。

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

#问:如果我们对所有的信号都进行了自定义捕捉,是不是就写了一个不会被异常或者用户杀掉的进程?

        不是,也不可能,操作系统的设计者也考虑到了。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>

static void handler(int signum)
{
    std::cout << "捕捉 信号: " << signum << std::endl;
    // 不要终止进程,exit
}

int main()
{
    // 将block全部设置
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }

    return 0;
}

        9号信号与19号信号属于管理员信号,我们是无法设定自定义捕捉动作的。为的就是防止我们将所有的31个信号全部捕捉。

9号信号:

19号信号:

#问:如果我们将对所有的信号都进行了block,是不是就写了一个不会被异常或者用户杀掉的进程?

        不是,也不可能,操作系统的设计者也考虑到了。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>

// 打印pending
static void showPending(sigset_t &pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}

// 设置block
static void blockSig(int sig)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, sig);
    int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
    assert(n == 0);
    (void)n;
}

int main()
{
    // 将block全部设置
    for(int sig = 1; sig <= 31; sig++)
    {
        blockSig(sig);
    }

    // 循环打印pending
    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }
    return 0;
}

        9号信号与19号信号属于管理员信号,我们是无法设定block。为的就是防止我们将所有的31个信号全部block。

 19号信号:

Note:

        9号信号与19号信号,永远不会被阻塞、永远不会被捕捉。

捕捉方法

1. 信号在合适的时候处理(什么时候?)

       合适的时候:内核态返回到用户态的时候处理。

#问:什么叫做用户态?什么叫做内核态?

        我们调用某些系统调用(或者是时间片到了、或者是我们主导的调用了陷入内核的汇编指令)。以此,进入操作系统,在操作系统内,执行操作系统的代码。其中执行操作系统底层的代码的状态,就称作为内核态 —— 同理:执行用户层代码时称作为用户态。 

  • 用户态:是一个受管控的状态。
  • 内核态:是一个操作系统执行自己代码的一个状态,具备非常高的优先级。

#问:从内核态返回到用户态,即必须要进入过内核态。为什么要进入内核态?如何进入的内核态?

        在操作系统或者是在硬件CPU上执行代码的时候,执行的一大批代码都在内存之中保存着(二进制)。CPU执行的时候区分是用户的代码还是内核的代码,即执行用户的代码就是用户态,执行内核的代码就是内核态。

内核范畴:

  • 相关数据在操作系统内部,普通用户没有权利去进行检测。

内核状态:系统调用接口。

用户状态:hello world。

我们大部分执行的是我们的代码,所以我们是用户态。

进入内核态最典型的方式:

  • 进行系统调用。
  • 而有时候缺陷、陷阱、异常等,也可能进入内核态。

#问:我们怎么进入内核态和用户态?(我们不用担心)

        在汇编语言中,有一个中断编号80,有一个汇编指令int。以int 80可以让我们陷入内核,即:代码的执行权限,由我下达给操作系统,让操作系统去执行。

(int 80内置在系统调用函数中,我们不用管)

        用户态是一个受管控的状态:即不管是哪一个用户,都叫做普通用户。其启动的进程或者是任务,都是需要以用户态来运行的。受管控:受访问权限的约束、资源限制等如果,基本不受任何资源的约束,也就是不受权限的管控。

页表分为:

  • 用户级页表:用于映射用户所写的代码和数据所对应的物理内存位于用户空间:0 ~ 3G
  • 内核级页表:用于映射操作系统的代码和数据所对应的物理内存(位于内核空间:3 ~ 4G )

        内核级页表不同于用户级页表。对于用户级页表而言,因为不同进程的代码和数据是不相同的,所以需要每一个进程有属于自己的用户级页表(进程的独立性的体现)对于内核级页表而言,因为所有的进程都是在一个操作系统上跑的(只有一份操作系统),所以内核级页表只有一份

        当我们的代码中有类似open的系统调用时,根本不用担心,因为操作系统也在进程地址空间里。所以,无非就是跳转到open对应的内核地址空间里。

        即:操作系统的代码可以找到,因为整个操作系统被映射进了所有进程的3~4G中。所以,所有进程想调系统调用,只是在自己的地址空间上,通过函数调用跳转到系统里就可以了。


融会贯通的理解:

#问:进程切换的过程是什么?

        操作系统内有一个switch process对应的函数,然后当我们对应的进程时间片到了,操作系统底层硬件给我们发时钟中断。操作系统就直接,在CPU找当执行的进程,然后通过其的地址空间,找到对应的进程切换的函数,因为在当前进程中切换,所以可以将CPU内所有的临时数据压到PCB当中,即切换成功。(下面一个进程,运用其进程地址空间中3~4G,与内核级页表,找到恢复上下文的代码和数据。)


        时钟中断执行的频率很高:100次/秒,时钟中断的主要工作是处理和时间有关的所有信息、决定是否执行调度程序以及处理下半部分。 和时间有关的所有信息包括系统时间、进程的时间片、延时、使用CPU的时间、各种定时器,进程更新后的时间片为进程调度提供依据,然后在时钟中断返回时决定是否要执行调度程序。

#问:我们凭什么有权利执行操作系统的代码?

        凭的是我们是处于内核态还是用户态。因为CPU里面的寄存器一般分为两类:一套可见(程序员可用),一套不可见(权限、控制等,CPU自己用)。其中有一个寄存器,叫做CR3寄存器 —— 表示当前CPU的执行权限。

        只有内核态才可以访问操作系统里,结合前面所提:其实执行open的时候,就会执行其本身自带的指令int 80,该指令第一件事就会将CR3寄存器由用户态变为内核态,于是后面权限检查发现是内核态,于是就可以访问操作系统(使用内核级页表),于是跳转到操作系统执行open的代码。

#问:为什么从用户态 -> 内核态?

        因为有时候有一些功能,大部分情况下无法在用户态无法去执行的。因为操作系统是软硬件资源的管理者,换句话说,就是任何普通用户无法(不能、不可以)绕过操作系统去访问对应的软硬件资源。

  • 用户需要通过访问软硬件资源,达到自身的目的。

        用户需要,操作系统不允许,所以便有了:先变成内核,然后用户通过其访问 —— 系统调用接口

#问:怎么从用户态 -> 内核态?

        目前最常见就是,系统调用接口。通过特定的接口陷入内核。

#问:为什么从内核态 -> 用户态?

  1. 当前用户的代码还没有执行完
  2. 当前用户层还有若干个进程没有被调度完。

        计算机当中,操作系统是为了给用户提供服务的,所以执行用户的代码为主执行操作系统的代码为辅,以此协同完成。所以是必定要从内核态 -> 用户态,不反回用户态就无法给用户提供完整的服务 —— 可以说:进入内核态是一种临时的状态。

#问:CPU如何知晓的当前其执行的代码是用户的还是内核的?

硬件上:

        CPU寄存器分为两类:一套可见,一套不可见。执行内核代码中,其第一件事就是自行自带的指令int 80,该指令第一件事就会将CR3寄存器由用户态变为内核态,于是后面权限检查发现是内核态。

软件上:

        除用户级页表(将虚拟的用户地址空间进行物理内存的实质化,同时每一个进程由独立的用户级页表,用以保证进程的独立性)之外,还有内核级页表,用于将进程地址空间中的内核地址空间映射到物理内存中存储的操作系统(开机启动电脑,无非就是将操作系统加载到物理内存,用以运行)。以此达到进程对于操作系统的代码和数据的访问,由于操作系统只有一份(只需要一份:Windows、Linux)。所以,所有进程只需要看见同一份资源,即同一份内核级页表即可。

融会贯通的理解:

        所有进程都在一个操作系统下跑 —— 必须看见同一份操作系统 —— 同一份内核级页表。

        CPU执行代码的时候都是进程的代码进程,而如何切换软硬件体系都可以找到操作系统,并且所有的操作系统执行都是在地址空间中 —— 执行系统调用 —— 在当前进程地址空间内执行跳转 —— 与动态库类似(区别:更改实行级别、状态、权限等)

2. 信号处理的整个流程:

        内核态处理默认、忽略,是水到渠成的事情。

融会贯通的理解:

        进程由内核态 -> 用户态的时,遍历检查pending中处于1的信号,然后其block中为0,即进行handler中的递达。

  • 忽略:即pengding中的1置0,而后直接返回上层的用户代码处继续执行。
  • 默认:大部分是终止,直接将当前进程进入终止逻辑(不调度该进程了),将进程对应的PCB、地址空间、页表释放,无需返回用户态继续运行。
    • 进程终止_exit():就是直接终止,说白了就是这种终止无需返回到用户态。
    • 进程终止exit():上层用户层由刷新行为(特殊处理),操作系统需要返回到用户态,将缓冲区数据进行刷新,然后再进行终止。
      • 终止了如何还能执行用户层代码?—— 操作系统提供有一些相关接口。(进程终止之前,帮我们返回到用户态,执行特定的方法)
    • 进程暂停:当前进程在内核态,将进程PCB的状态由运行态改为T状态,然后无需返回用户态,直接执行调度算法,将进程放入等待队列里。然后从新选择进程调度。

        默认和忽略都是处于内核态。

捕捉动作是最难的,下图以捕捉动作为例:

#问:执行我们所写的捕捉动作时,处于什么状态?

        此时,处于信号检测,信号处理 -> 所以是处于内核态的。

#问:当前的状态,能不能执行user handler方法的?

        能执行,但是操作系统不想执行。因为不要认为操作系统内核态不能访问用户层的代码、数据。只要操作系统愿意,想访问就访问。

融汇贯通的理解:

以文件操作中为例:

        用户通过read读取文件中的内容,而文件中的内容就是属于用户层的数据,但是read是系统接口,是属于内核层

        所以:我们获取文件内容的方式,就是通过我们自己写的用户级缓冲区buffer,获取数据,而缓冲区的数据就是操作系统拷贝进的

        文件数据读取,操作系统愿意做,因为数据只是拷贝没有什么问题,但是如果以操作系统内核态的身份,如果user handler有非法的操作,那就完了 —> 操作系统是不会相信普通用户任何人的 —> 不能用内核态执行用户的代码。

        进行user handler方法时,从内核态切回用户态 —— 以用户态的身份执行我们所写的方法,如此,所有行为我们自己负责。

        在完成之后,在用户的层面上,没有能力跳转回执行内核层的代码,然后继续向后执行。

        因为递达完成,需要将pending置0,此操作需要以内核态身份执行。并且当时在哪被中断进入内核,这个位置只有操作系统知道。

图像简易化

  • 1 -> 2 -> 3 -> 4 -> 5
  • 1次的信号检测
  • 4次的状态的切换

sigaction

#include <signal.h>

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

检查并更改信号动作。

参数:

        signum:信号的编号

        act:输入型参数

  • 一个结构体,至少包含对于信号的处理 —— 回调函数

        oldact:输出型参数

  • 一个结构体,曾经对于这个信号的老的处理方法。

返回值:

  • 成功时,返回0
  • 出现错误时,返回-1,并设置errno(错误码)以指示错误。

struct sigactio

        结构体,操作系统提供的数据类型。(结构体名称能与函数相同 —— 不建议)

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 <unistd.h>

void handler(int signum)
{
    std::cout << "获取了一个信号: " << signum << std::endl;
}

int main()
{
    std::cout << "getpid: " << getpid() << std::endl; 

    // 内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask); // 清空原理后面讲解
    act.sa_handler = handler;

    // 设置进当前调用进程的pcb中
    sigaction(2, &act, &oact);

    std::cout << "default action : " << oact.sa_handler << std::endl;



    while(true) sleep(1);
    return 0;
}

        Linux的设计方案:在任意时刻,只能处理一层信号 —— 不允许信号正在被处理时又来信号需要处理 —— 信号什么时候来挡不住,但是可以挡得住信号什么时候被处理。

sa_mask

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

#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signum)
{
    std::cout << "获取了一个信号: " << signum << std::endl;
    sleep(10);
}

int main()
{
    std::cout << "getpid: " << getpid() << std::endl; 

    // 内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);  // 保证初始状态下,信号屏蔽字全为0
    act.sa_handler = handler;

    // 设置进当前调用进程的pcb中
    sigaction(2, &act, &oact);

    std::cout << "default action : " << oact.sa_handler << std::endl;



    while(true) sleep(1);
    return 0;
}

         实验在信号处理期间,会屏蔽该信号。

#include <iostream>
#include <signal.h>
#include <unistd.h>

void showPending(sigset_t* pending)
{
    for(int sig = 1; sig <= 31; sig++)
    {
        if(sigismember(pending, sig)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

void handler(int signum)
{
    std::cout << "获取了一个信号: " << signum << std::endl;

    sigset_t pending;
    int c = 7;
    while(true)
    {
        sigpending(&pending);
        showPending(&pending);
        c--;
        if(!c) break;
        sleep(1);
    }
}

int main()
{
    std::cout << "getpid: " << getpid() << std::endl; 

    // 内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);  // 保证初始状态下,信号屏蔽字全为0
    act.sa_handler = handler;

    // 设置进当前调用进程的pcb中
    sigaction(2, &act, &oact);

    std::cout << "default action : " << oact.sa_handler << std::endl;

    while(true) sleep(1);
    return 0;
}

        这也就是为什么会有block或者信号屏蔽字,这样的字段。也就是为了支持操作系统内处理普通信号,防止其进行递归式的处理。让其在自己的处理周期内只会调用一层,不出现太多的调用层次。

在处理2号信号的期间,捎带的屏蔽一下信号:3、4、5、6

#include <iostream>
#include <signal.h>
#include <unistd.h>

void showPending(sigset_t* pending)
{
    for(int sig = 1; sig <= 31; sig++)
    {
        if(sigismember(pending, sig)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

void handler(int signum)
{
    std::cout << "获取了一个信号: " << signum << std::endl;

    sigset_t pending;
    int c = 20;
    while(true)
    {
        sigpending(&pending);
        showPending(&pending);
        c--;
        if(!c) break;
        sleep(1);
    }
}

int main()
{
    std::cout << "getpid: " << getpid() << std::endl; 

    // 内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);  // 保证初始状态下,信号屏蔽字全为0
    act.sa_handler = handler;

    // 在处理2号信号的期间,捎带的屏蔽一下信号:3、4、5、6
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 6);

    // 设置进当前调用进程的pcb中
    sigaction(2, &act, &oact);

    std::cout << "default action : " << oact.sa_handler << std::endl;

    while(true) sleep(1);
    return 0;
}

        因为,我们只是将2号信号设置为自定义捕捉,其他信号是默认。所以在执行完2号信号的自定义捕捉后,处于被block状态的信号,才会被递达。(此处最后递达的是:4号信号)

Note:

        信号捕捉,并没有创建新的进程或者线程。信号的处理整个流程都是单进程的,就是这一个进程处理信号时,处于此进程的上下文中处理。

补充

可重入函数

        main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。于是便出现了经典的内存泄漏问题

        排查代码的时候,可以发现,我们的代码写的没有任何问题。对应的main函数、单链表头插insert、信号捕捉sighandler、函数调用都没有问题 —— 这个问题的产生严格来说并不是代码问题,而是因为操作系统调度导致的进程时序的变化 —— 时序问题。

        此问题存在且,非常不容易排查。

        像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

可重入函数 VS 不可重入函数

        是函数的一种特征,目前我们用的90%函数,都是不可重入的。

  • 不可重入函数:好编写。
  • 可重入函数:不好编写,书写成本高。

如果一个函数符合以下条件之一则是不可重入的:  

  • 调用了new、malloc或free,因为new、malloc也是用全局链表来管理堆的。
  • 99%的STL容器,都是不可重入的。
  • 函数里面带static的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

可重入函数:需要保证函数其是独立的,没有访问任何的全局数据。

volatile

        编译做优化的时候,自作聪明,它一看flag是一个全局的数据,发现在main函数里没有任何一个语句是改flag的。其认为每一次检测flag都需要访问内存,并将数据拷贝进CPU的寄存器中,于是自作聪明,将第一次的flag的值一直放在edx中,后面的检测就不去内存拿了,继续看edx中的数据,就是0,于是一直检测不过。

        所以为了解决这个问题,一些可能被优化的字段,我们显性的告诉编译器,不要这么优化。

SIGCHLD信号

(只有Linux采用了这样的方案)

        在进程等待中,用  wait  和  waitpid  函数清理僵尸进程, 父进程可以阻塞等待子进程结束, 也可以非阻塞地查询是否有子进程结束等待清理 (也就是轮询的方式) 。采用第一种方式, 父进程阻塞了就不能处理自己的工作了; 采用第二种方式, 父进程在处理自己的工作的同时还要记得时不时地轮询一下, 程序实现复杂。其实, 子进程在终止时会给父进程发 SIGCHLD 信号, 该信号的默认处理动作是忽略, 父进程可以自定义 SIGCHLD 信号的处理函数,这样父进程只需专心处理自己的工作, 不必关心子进程了, 子进程终止时会通知父进程, 父进程在信号处理函数中调用wait 清理子进程即可。
        本质上也就是:子进程通过操作系统来给父进程写入信号,逻辑同文上。
  • 验证子进程在终止时会给父进程发SIGCHLD信号:
#include <cstdio>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

void handler(int sig)
{
    printf("子进程退出:%d\n", sig);
}

// 证明:子进程退出,会向父进程发送信号
int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { 
        // child
        sleep(1);
        exit(0);
    }
    while(true) sleep(1);
    return 0;
}

  • 父进程在信号处理函数中调用 wait / waitpid 清理子进程:

Note:

        我们需要注意,如果我们有10个子进程,而如果其中5个子进程在一个时刻退出。由于普通信号只会有与没有,没有个数的表达,而对于第6个子进程,也是需要进行判断是否退出的(我们知道5个子进程退出是我们站在上帝的视角)。此时我们使用常规的 wait / waitpid 去使用,信号处理就会阻塞在那里。


解决方法:

  • 使用全局变量记录所有子进程的pid,用以遍历的非阻塞等待。
  • 使用waitpid第一个参数为-1(等待任意一个子进程),并非阻塞等待。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>

void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    {
        // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }

    //father
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}
  • 父进程不关心子进程的任何推出信息:
#include <iostream>
#include <unistd.h>
#include <signal.h>

// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{
    // OS 默认就是忽略的
    signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略

    if(fork() == 0)
    {
        std::cout << "child: " << getpid() << std::endl;
        sleep(5);
        exit(0);
    }

    while(true)
    {
        std::cout << "parent: " << getpid() << " 执行我自己的任务!" << std::endl;
        sleep(1);
    }
}

#问:操作系统默认就是忽略的,在此处我们还自己写一个忽略?

        也看得出来差别是很大的。忽略的概念是不一样的,可以理解为对信号的处理有第四方案:操作系统级别的忽略。可以理解为我们的忽略为1,操作系统的忽略为2,是不同级别的忽略。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/345487.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小白入门模拟IC设计,如何快速学习?

众所周知&#xff0c;模拟电路很难学。以最普遍的晶体管来说&#xff0c;我们分析它的时候必须首先分析直流偏置&#xff0c;其次在分析交流输出电压。可以说&#xff0c;确定工作点就是一项相当麻烦的工作&#xff08;实际中来说&#xff09;&#xff0c;晶体管的参数多、参数…

JavaScript 计时事件

JavaScript 计时事件 通过使用 JavaScript&#xff0c;我们有能力做到在一个设定的时间间隔之后来执行代码&#xff0c;而不是在函数被调用后立即执行。我们称之为计时事件。 在 JavaScript 中使用计时事件是很容易的&#xff0c;两个关键方法是: setInterval() - 间隔指定的…

【CNN记录】tensorflow中depth_to_space

功能把depth维的数据移到space上&#xff0c;与spacetodepth刚好是相反的操作&#xff0c;depth对应channel&#xff0c;space对应height和width&#xff0c;而该操作是把depth上的数据分给height和width上&#xff0c;所以对应有一个参数block_size&#xff0c;要求原tensor的…

CSDN竞赛28期参赛体验

1、小Q的鲜榨柠檬汁 1、题目名称&#xff1a;小Q的鲜榨柠檬汁 团建活动是大家所想要的。 小Q给大家准备了鲜橙汁。 现在有n个朋友买回了k瓶饮料&#xff0c;每瓶有l毫升的饮料&#xff0c;同时还买回 了c个柠檬&#xff0c; 每个柠檬可以切成d片&#xff0c;p克盐。 已知每个朋…

Python基础学习笔记 —— 数据结构与算法

数据结构与算法1 数据结构基础1.1 数组1.2 链表1.3 队列1.4 栈1.5 二叉树2 排序算法2.1 冒泡排序2.2 快速排序2.3 &#xff08;简单&#xff09;选择排序2.4 堆排序2.5 &#xff08;直接&#xff09;插入排序3 查找3.1 二分查找1 数据结构基础 本章所需相关基础知识&#xff1a…

第七届蓝桥杯省赛——1有奖猜谜

题目&#xff1a; 小明很喜欢猜谜语。 最近&#xff0c;他被邀请参加了X星球的猜谜活动。 每位选手开始的时候都被发给777个电子币。 规则是&#xff1a;猜对了&#xff0c;手里的电子币数目翻倍&#xff0c; 猜错了&#xff0c;扣除555个电子币, 扣完为止。 小明一共猜了15…

入门深度学习——基于全连接神经网络的手写数字识别案例(python代码实现)

入门深度学习——基于全连接神经网络的手写数字识别案例&#xff08;python代码实现&#xff09; 一、网络构建 1.1 问题导入 如图所示&#xff0c;数字五的图片作为输入&#xff0c;layer01层为输入层&#xff0c;layer02层为隐藏层&#xff0c;找出每列最大值对应索引为输…

云原生周刊 | 开源领导者应该如何应对碎片化挑战?

Linux Fundation 发布了一份关于开源开发中的碎片化问题的报告《实现全球协作&#xff1a;开源领导者如何应对碎片化挑战》&#xff0c;该报告由华为在美国的研发部门 Futurewei 赞助。报告指出&#xff0c;虽然开源社区越来越国际化&#xff0c;但美国对开源共享和开发进行了过…

源码项目中常见设计模式及实现

原文https://mp.weixin.qq.com/s/K8yesHkTCerRhS0HfB0LeA 单例模式 单例模式是指一个类在一个进程中只有一个实例对象&#xff08;但也不一定&#xff0c;比如Spring中的Bean的单例是指在一个容器中是单例的&#xff09; 单例模式创建分为饿汉式和懒汉式&#xff0c;总共大概…

Linux内核驱动开发(一)

Linux内核初探 linux操作系统历史 开发模式 git 分布式管理git clone 获取git push 提交git pull 更新 邮件组 mailing list patch 内核代码组成 Makfile arch 体系系统架构相关 block 块设备 crypto 加密算法 drivers 驱动&#xff08;85%&#xff09; atm 通信bluet…

MAC文件误删怎么办?mac数据恢复,亲测很好用的方法

电脑文件误删&#xff0c;应该很多人都经历过。之前分享了很多关于Windows电脑文件误删如何恢复的方法&#xff0c;那么MAC电脑文件误删该怎么办&#xff1f;有什么好方法可以使得mac数据恢复回来吗&#xff1f;下面就给大家分享一些亲测好用的方法&#xff01; 一、MAC电脑的文…

使用Proxifier+burp抓包总结

一、微信小程序&网页抓包 1. Proxifier简介 Proxifier是一款功能非常强大的socks5客户端&#xff0c;可以让不支持通过代理服务器工作的网络程序能通过HTTPS或SOCKS代理或代理链。 2. 使用Proxifier代理抓包 原理&#xff1a;让微信相关流量先走127.0.0.1:80到burp。具体…

Final Cut Pro 10.6.5

软件介绍Final Cut Pro 10.6.5 已通过小编安装运行测试 100%可以使用。Final Cut Pro 10.6.5 破解版启用了全新的矩形图标&#xff0c;与最新的macOS Ventura设计风格统一&#xff0c;支持最新的macOS 13 文图拉系统&#xff0c;支持Apple M1/M2芯片。经过完整而彻底的重新设计…

数据结构之单链表

一、链表的组成 链表是由一个一个的节点组成的&#xff0c;节点又是一个一个的对象&#xff0c; 相邻的节点之间产生联系&#xff0c;形成一条链表。 例子&#xff1a;假如现在有两个人&#xff0c;A和B&#xff0c;A保存了B的联系方式&#xff0c;这俩人之间就有了联系。 A和…

HashMap底层实现原理概述

原文https://blog.csdn.net/fedorafrog/article/details/115478407 hashMap结构 常见问题 在理解了HashMap的整体架构的基础上&#xff0c;我们可以试着回答一下下面的几个问题&#xff0c;如果对其中的某几个问题还有疑惑&#xff0c;那就说明我们还需要深入代码&#xff0c…

ubuntu 20.04 安装 flameshot截图工具

ubuntu 20.04 安装 flameshot截图工具安装命令使用命令设置快捷键效果图安装命令 sudo apt-get install flameshot安装日志 $ sudo apt-get install flameshot [sudo] password for huifeimao: Reading package lists… Done Building dependency tree Reading state informat…

【零基础入门前端系列】—表格(五)

【零基础入门前端系列】—表格&#xff08;五&#xff09; 一、表格 表格在数据展示方面非常简单&#xff0c;并且表现优秀&#xff0c;通过与CSS的结合&#xff0c;可以让数据变得更加美观和整齐。 单元格的特点&#xff1a;同行等高、同列等宽。 表格的基本语法&#xff1…

性能测试之tomcat+nginx负载均衡

nginx tomcat 配置准备工作&#xff1a;两个tomcat 执行命令 cp -r apache-tomcat-8.5.56 apache-tomcat-8.5.56_2修改被复制的tomcat2下conf的server.xml 的端口号&#xff0c;不能与tomcat1的端口号重复&#xff0c;不然会启动报错 ,一台电脑上想要启动多个tomcat&#xff0c…

自定义bean 加载到spring IOC容器中

自定义bean加载到spring容器中的两种方式&#xff1a; 1.在类上添加注解Controller、RestController&#xff08;本质是Controller&#xff09;、Service、Repository、Component2.使用Configuration和Bean 这篇文章主要介绍第二种方式原理&#xff08;因为在实际使用中&#…

SteaLinG:一款针对社工的开源安全渗透测试框架

关于SteaLinG SteaLinG是一款功能强大的开源渗透测试框架&#xff0c;该框架专为社会工程学研究人员设计&#xff0c;可以帮助广大研究人员或组织内的安全专家测试目标设备的安全性。该工具基于Python开发&#xff0c;因此具备良好的跨平台特性。在使用时&#xff0c;我们只需…