前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >进程间通信的信号艺术:机制、技术与实战应用深度剖析

进程间通信的信号艺术:机制、技术与实战应用深度剖析

作者头像
绝活蛋炒饭
发布2024-12-16 16:25:38
发布2024-12-16 16:25:38
11900
代码可运行
举报
文章被收录于专栏:绝活编程学习绝活编程学习
运行总次数:0
代码可运行

1 什么是信号

Linux提供的让用户(进程)给其他进程发送异步信息的一种方式。

  1. 1.Linux保证了信号还没有发出的时候,我们就知道怎么去处理。
  2. 2.信号能够被认识,是因为信号早就被在我们的大脑里设置好了。

前两点保证我们能够识别信号,并且知道怎么去处理信号

  1. 信号到来的时候,我们还在处理其他更重要的事情,我们暂时还不能处理信号,这时就需要我们能够去保存信号。
  2. 信号到了,可以不立即处理,可以在合适的时候处理。
  3. 信号的产生是随机产生的,我们无法准确预料到,所以信号是异步发送的(信号是由别人(用户/进程)发出的,此时,我在忙我自己的事情) 。

2 为什么要有信号

系统要求进程有随时相应外界的能力,然后做出反应。

3 对于信号的反应

对于信号的反应即,OS对于信号的处理凡是:

  1. 默认行为
  2. 自定义行为---捕捉
  3. 忽略信号

注:忽略信号也算处理了信号,处理方式就是忽略。


3.1 默认行为

下面就是利用 kill命令向进程当中发生2号信号,OS执行默认行为,将进程停下。

3.2 signal()函数 -- 自定义行为对信号做出反应

参数

  • signum:指定要捕获的信号编号。例如,SIGINT 表示中断信号(通常由 Ctrl+C 产生),SIGSEGV 表示段错误信号。
  • handler:指定信号处理函数,它是一个接受单个整数(信号编号)作为参数的函数。如果传递 SIG_IGN,则忽略该信号;如果传递 SIG_DFL,则使用默认的信号处理行为。

返回值

  • 成功时,返回之前的信号处理函数指针。
  • 失败时,返回 SIG_ERR,并设置 errno 以指示错误原因。
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;

void handler(int sig)
{
     cout << "I get sidnal, the signalnum:  " << sig<< endl;
}
int main()
{
    signal(2,handler);//这个自定义行为,只要被设置过就改变了,不需要写入循环当中。被设置的时候,不会触发行为,只有当进程收到信号是,才会产生行为
    while (1)
    {
        cout << "my pid:  " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

你看上面的的进程,在对信号进行捕获了之后,就不在执行默认行为,反而执行自定义行为

3.3 对信号进行忽略

同样也是signal()系统调用,只不过参数不同

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;

void handler(int sig)
{
     cout << "I get sidnal, the signalnum:  " << sig<< endl;
}
int main()
{
    signal(2, SIG_IGN);//ignore
    //这个自定义行为,只要被设置过就改变了,不需要写入循环当中。被设置的时候,不会触发行为,只有当进程收到信号是,才会产生行为
    while (1)
    {
        cout << "my pid:  " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

上面的进程就对2号信号,毫无反应


4 信号的产生的类型

4.1 kill命令

下面就是利用 kill命令向进程当中发生2号信号,OS执行默认行为,将进程停下。

4.2 键盘输入产生信号

键盘输入:也算是信号,如:ctrl + c 和 ctrl + \

其实上ctrl + c 就等于输入 2 号信号

我们都知道,ctrl + c可以终止进程,但是为什么说他被解释为信号呢?

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;

void handler(int sig)
{
     cout << "I get sidnal, the signalnum:  " << sig<< endl;
}
int main()
{
    signal(2, handler);//ignore
    //这个自定义行为,只要被设置过就改变了,不需要写入循环当中。被设置的时候,不会触发行为,只有当进程收到信号是,才会产生行为
    while (1)
    {
        cout << "my pid:  " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

这里输入了ctrl + c 触发了对于2号信号的自定义行为。

4.3 系统调用接口

4.3.1 kill()

参数

  • pid:要发送信号的进程的进程 ID(PID)。可以是以下几种特殊值之一:
    • 0:向属于调用进程所在进程组的所有进程发送信号。
    • -1:向除了进程组 1 和调用进程本身之外的所有进程发送信号(需要超级用户权限)。
    • < -1:向进程组 ID 为 -pid 的所有进程发送信号。
  • sig:要发送的信号。例如,SIGKILL 用于强制终止进程,SIGTERM 用于请求进程正常终止。

返回值

  • 成功时返回 0
  • 失败时返回 -1,并设置 errno 以指示错误类型。

这里如果是简单的使用一下这个系统调用,就未免有点太简单了,我们利用系统调用接口封装一个自己的my_kill

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include<error.h>
#include<cstring>
using namespace std;

// mykill -9 pid
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << " usage error: " << argv[0] << " -signumber pid" << endl;
    }

    pid_t pid = stoi(argv[2]);
    int sig = stoi(argv[1]+1);
    int n = kill(pid, sig);

    if (n < 0)
    {
        cerr << "kill error, " << strerror(errno) << endl;
    }

    return 0;
}

上面是先形成可执行文件,在将路径加到环境变量PATH 中,这样就不用我们输入路径了

4.3.2 raise() 函数

这个也没什么好讲的就是一个,底层封装了kill()系统调用的函数。

功能:对自己发送任意信号

4.4 软件条件

我们以alarm()为例,以这个闹钟函数模拟软件条件

参数:

seconds:闹钟定时的时间,单位是秒

返回值:

alarm() 函数的返回值是上一次 alarm() 设置的定时器剩余的时间(以秒为单位)。具体行为如下:

  1. 首次调用或之前没有设置定时器:如果这是第一次调用 alarm() 函数,或者之前没有设置过定时器(即之前调用 alarm(0) 取消了定时器),则 alarm() 函数返回 0。
  2. 定时器正在运行:如果之前已经设置了一个定时器,并且该定时器还没有到期,那么再次调用 alarm() 函数时,它会返回上一次设置的定时器剩余的时间。例如,如果之前设置了 10 秒的定时器,然后在 5 秒后再次调用 alarm(),那么它将返回 5(表示还有 5 秒定时器才会到期)。
  3. 定时器已到期:如果之前设置的定时器已经到期(即已经发送了 SIGALRM 信号),那么再次调用 alarm() 函数时,它将返回 0,因为此时没有正在运行的定时器。
  4. 取消定时器:如果调用 alarm(0),这将取消当前正在运行的定时器(如果有的话),并且 alarm() 函数将返回被取消的定时器剩余的时间。如果没有正在运行的定时器,则返回 0。

需要注意的是,在某些系统实现中,如果 alarm() 函数调用失败(例如,由于系统资源不足),它可能会返回一个非零的负值(如 -1),但这种情况比较少见。在大多数情况下,alarm() 函数都会成功执行并返回上述描述的值。

此外,alarm() 函数设置的定时器是进程级别的,即每个进程只有一个 alarm() 定时器。如果在同一个进程中多次调用 alarm(),则之前的定时器设置将被新的设置替换。

代码语言:javascript
代码运行次数:0
复制
void handler(int sig)
{
    std::cout << "get a sig: " << sig << " g_cnt: " << g_cnt << std::endl;

    unsigned int n = alarm(5);
    // 定时器正在运行:如果之前已经设置了一个定时器,并且该定时器还没有到期,那么再次调用 alarm() ,函数时,它会返回上一次设置的定时器剩余的时间。
    // 例如,如果之前设置了 10 秒的定时器,然后在 5 秒后再次调用 alarm(),那么它将返回 5(表示还有 5 秒定时器才会到期)。

    cout << "还剩多少时间: " << n << endl;
    // exit(0);
}

int main()
{
    //设定一个闹钟
    signal(13, handler);//捕获13号信号,正面alarm()发送的是13号信号。

    alarm(5); // 响一次

    while (true)
    {
        g_cnt++; // 纯内存级
    }

    // int cnt = 0;
    // while (true)
    // {
    //     sleep(1);
    //     cout << "cnt : " << cnt++ << ", pid is : " << getpid() << endl; // IO其实很慢
    //     if (cnt == 2)
    //     {
    //         int n = alarm(0); // alarm(0): 取消闹钟
    //         cout << " alarm(0) ret : " << n << endl;
    //     }
    // }
}

谈及到闹钟的概念,其实上OS系统内部也有很多闹钟,其实上就是OS系统自己设定的。

也不用多说:先描述,后组织

然后,利用结构体指针,在最小堆中组织起来。

4.5 异常

这里就简单讲两个异常:8)SIGFPE

代码除零了:11)SIGSEGV


5. 信号产生的原因

5.1 键盘输入转化成信号的过程

首先,我们要明白的是内核怎么知道,键盘要输入的,如果是内核不断地去询问的话,按照我们现在这种我们几乎感觉不到的延迟的话,对于内核的资源要求很高,而且,也不只一个硬件等待内核的询问,还有网卡,硬盘...,所以肯定是键盘通知内核来读,内核才来读

首先就是键盘一直都是处于通电状态的,这个毫无疑问,那么怎么表示有输入了呢?那就是输入一个高电频,表示有数据输入。 还有就是,键盘与CPU上的一个针脚(每一个针脚都有编号)相连,就通过这个针脚向CPU传递高电频。

这个过程就是:

键盘有内容输入,向CPU传递高电频,CPU检测到是从2号针脚传递上来的,将2号记录在寄存器内,传递给OS,OS在中断向量表里的arr[2]中读取数据,发现是一个函数指针,然后调用该函数(去键盘里读数据)。

读取到数据之后:

判断是文本内容(abcd)还是信号(ctrl + c)

如果是文本内容,就直接输入标准输入流当中

如果是信号,就写入进程当中的pending位图当中,等待进程响应。

5.2 代码除零发出异常信号的过程

5.3 野指针发出异常信号的过程


6 关于core dump标志位

man 7 signal

我们发现60%--70%的信号都是终止进程,但是终止进程却又Core和Term两种行为。那么这两种行为又有什么区别。

Core的终止进程的行为,会在当前路径下形成一个Core文件,里面就存储着进程的退出信息,在调试的时候可以直接帮助我们定位到错误位置。(这个就叫核心转储功能)

如果你默认是云服务器,那么云服务器的Core是默认关闭的。即Core dump标志位默认是0.

ulimit -a 查看核心转储

默认情况下,核心转储功能是没有打开的。即这个core文件大小是0

ulimit -c 10240

ulimit -c :查看资源


7 信号的保存

实际执行信号的处理动作称为信号递达(Delivery):处理动作就是之前提到的,三个动作

信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞 (Block )某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

我们现在就看到三张表

block表:其实就是block位图

  1. 比特位的位置表示:信号的编号
  2. 比特内容表示:该信号是否被阻塞

pending表:其实就是pending位图

  1. 比特位的位置表示:信号的编号
  2. 比特内容表示:该信号是否收到该信号

handler表

里面就存储着函数指针,指向信号的默认处理方法。

如果,程序员对信号进行了捕获,那么handler表里的函数指针就指向你写的函数方法

阻塞一个信号,和是否收到了指定信号,有关系吗?--- 没有关系

7.2 三张表的匹配操作和系统调用

7.2.1 sigset_t

sigset_t是一个在Unix和类Unix系统(如Linux)中使用的数据类型,它用于表示信号集。信号集本质上是一个信号的集合,可以用来指定多个信号。

一、定义与用途

  • 定义sigset_t是一个数据类型,一般来讲是位图,用于存储一组信号的集合。
  • 用途:信号集主要用于与信号阻塞、信号等待等操作相关。通过使用sigset_t,程序员可以方便地管理一组信号,而无需单独处理每个信号。

二、相关函数

sigset_t相关的函数主要包括信号集的初始化、添加/删除信号、检查信号集是否包含某个信号等。以下是一些常用的函数:

sigemptyset(sigset_t *set):初始化信号集,将其置为空集。

返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。

sigfillset(sigset_t *set):初始化信号集,将其置为包含系统支持的所有信号的集合。

返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。

sigaddset(sigset_t *set, int signum):向信号集中添加指定的信号。

返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。

sigismember(const sigset_t *set, int signum):检查信号集是否包含指定的信号。

  • 返回值:如果signum是set中的一个成员,则返回非零值(通常为 1);如果signum不是set的成员,则返回 0;出现错误时返回 -1 并设置 errno以指示错误的原因。

三、注意事项

  • 在使用sigset_t类型的变量之前,需要先将其初始化为一个空集或包含所有信号的集合,这可以通过sigemptysetsigfillset函数来完成。
  • 在向信号集中添加或删除信号时,应使用sigaddsetsigdelset函数。
  • 在检查信号集是否包含某个信号时,应使用sigismember函数。
  • 信号集通常与信号阻塞和信号等待等操作一起使用,以实现对信号的有效管理。

综上所述,sigset_t是一个在Unix和类Unix系统中用于表示信号集的数据类型,通过相关的函数可以方便地管理一组信号。

7.2.2 sigprocmask

sigprocmask是一个系统调用函数,用于修改当前进程的信号屏蔽字集合。信号屏蔽字是一个位掩码,每个位对应一个特定的信号。当某个信号对应的位被置为1时,表示该信号被阻塞,不会被传递给进程进行处理。

一、参数说明

how:指定如何修改当前进程的信号屏蔽字。它可以是以下三个值之一:

SIG_BLOCK

将set中指定的信号添加到当前进程的信号屏蔽字中。

SIG_UNBLOCK

从当前进程的信号屏蔽字中移除set中指定的信号。

SIG_SETMASK

将当前进程的信号屏蔽字设置为set中指定的信号集合

set:指向一个信号集合,用于指定需要修改的信号集合。如果howSIG_BLOCKSIG_UNBLOCK,则set指向的信号集合表示待添加或移除的信号;如果howSIG_SETMASK,则set指向的信号集合将替代当前进程的信号屏蔽字。

oldset:是一个可选的输出参数,用于获取调用sigprocmask函数前的旧的信号屏蔽字。如果不需要保存旧的信号屏蔽字,可以将其设置为NULL

二、返回值

  • 成功时,sigprocmask返回0。
  • 失败时,返回-1,并设置errno以指示错误原因。
7.2.3 sigpending

sigpending函数,用于查询当前进程的未决信号集合。未决信号集合是指那些已经发送给进程但尚未被处理的信号,这些信号可能因为被屏蔽而无法立即传递给进程。

一、函数原型

代码语言:javascript
代码运行次数:0
复制

二、参数说明

  • set:指向一个sigset_t类型的变量,用于存储当前进程的未决信号集合。调用sigpending函数后,该函数会将当前进程的未决信号集合复制到set指针所指向的信号集合中。

三、返回值

  • 成功时,sigpending返回0。
  • 失败时,返回-1,并设置errno以指示错误原因。
7.2.4 signal

signal函数是C语言标准库中的一个函数,用于设置特定信号的处理方式。它允许程序在接收到特定信号时执行自定义的处理函数,或者采用默认的处理方式,也可以选择忽略该信号。以下是对signal函数的详细解释:

一、函数原型

代码语言:javascript
代码运行次数:0
复制

二、参数说明

  • signum:指定要处理的信号编号。常见的信号有SIGINT(通常由Ctrl+C产生)、SIGTERM(通常用于请求程序终止)等。不同的操作系统可能支持不同的信号集。
  • handler:指定信号的处理方式。它可以是一个指向自定义信号处理函数的指针,也可以是两个特殊的常量:SIG_DFL(表示使用默认的信号处理方式)或SIG_IGN(表示忽略该信号)。

三、返回值

  • 成功时,signal函数返回之前的信号处理函数的指针。
  • 失败时,返回SIG_ERR,并设置errno以指示错误原因。

四、信号处理函数

  • 信号处理函数应该尽量简单快速,避免执行复杂的操作或长时间的阻塞操作。因为信号可能在任何时候中断程序的执行,如果信号处理函数执行时间过长,可能会影响程序的响应性。
  • 信号处理可能会被其他信号中断,所以在信号处理函数中要考虑到这种情况。例如,如果在处理一个信号时,又接收到了另一个信号,可能需要采取适当的措施来确保正确的处理顺序。

五、注意事项

  • 在使用signal函数之前,需要包含<signal.h>头文件。
  • 不同的操作系统对信号的处理可能会有所不同,所以在跨平台开发时需要注意兼容性问题。
  • 一旦设置了信号处理函数,它将在程序的整个生命周期内有效,除非再次调用signal函数来改变信号的处理方式。
  • 有些信号(如SIGKILL和SIGSTOP)是不能被捕获或忽略的。
代码语言:javascript
代码运行次数:0
复制
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

void print(sigset_t pending)
{
    for (int num = 31; num > 0; num--)
    {
        if (sigismember(&pending, num))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int sig)
{
    sigset_t set;
    sigisemptyset(&set);
    sigfillset(&set);

    int n = sigpending(&set); // 我正在处理2号信号哦!!
    // 将二号信号再次设置进入pending标准,模拟一直受到二号信号
    assert(n == 0);

    // 3. 打印pending位图中的收到的信号
    std::cout << "递达中...: ";
    print(set); //  递达之前,pending 2号已经被清0  先清零,再递达
    std::cout << sig << " 号信号被递达处理..." << std::endl;
}

int main()
{
    // 屏蔽2号信号
    signal(2, handler);

    // 初始化sigset_t
    // 定义一个sigset_t,并将其初始化,因为sigset_t是一个封装的结构体,我们也需要用封装的方法对其进行初始化
    // 其实他并没有被设定进入blick表中
    sigset_t ss, old_ss;
    sigisemptyset(&ss);
    sigfillset(&ss);
    sigaddset(&ss, 2);//添加2号信号

    // 此时才是调用系统调用接口,将信号集写入内核
    //将屏蔽二号信号的block表写入内核
    int n = sigprocmask(SIG_BLOCK, &ss, &old_ss);
    assert(!n);

    cout << "signal 2 getin block" << endl;
    cout << "pid : " << getpid() << endl;

    int cnt = 0;
    while (true)
    {
        //获取当前的pending表
        sigset_t pend;
        sigisemptyset(&pend);
        sigpending(&pend);

        // 3. 打印pending位图中的收到的信号
        print(pend);
        cnt++;

        // 4. 解除对2号信号的屏蔽
        if (cnt == 20)
        {
            std::cout << "解除对2号信号的屏蔽" << std::endl;
            n = sigprocmask(SIG_UNBLOCK, &ss, &old_ss); // 2号信号会被立即递达, 默认处理是终止进程
            assert(n == 0);
        }
        // 我还想看到pending 2号信号 1->0 : 递达二号信号!
        sleep(1);
    }
}

8 信号的处理

信号的处理必须在一个合适的时候处理 ---- 进程从内核态切换回用户态的时候,信号会被检测并且处理。

可以简单的认为,内核态就是系统在执行系统调用时,要对内核数据进行处理时的状态(OS所处的状态,权限较高)

用户态就是用户在简单的执行C语言代码时,没有对内核数据进行处理时,所处的状态就是用户态(用户所处的状态,就是我)。

但是,是不是没有使用系统调用就不会处于内核态,一直处于用户态呢?--- 不是的,当你的进程被调度,出让CPU和进入CPU的时候,都是处于内核态的。

  1. 就是当程序在运行的时候,如果收到某条指令的时候,就会进入内核态处理异常 1->2
  2. 当进程进入内核态的时候,处理完了刚刚收到的信号,不会马上退出,而会检测该进程是否有被递达的信号,(遍历pending位图,如果,为1,并且对应的block上为0,处于未阻塞的状态)2->3
  3. 此时有可以被递达的信号,如果默认动作时SIG_DFL,就直接把进程删除;如果默认动作时SIG_IGN,就直接忽略,回到1(3->1)返回用户态执行程序;如果默认动作时STOP,就直接把进程的状态设置为S;如果是自定义动作,就要到用户态去执行自定义函数(3->4)
  4. 当进入用户态,去执行代码内自定义的函数时,执行完了不能直接回到刚刚中断的代码,而要重新进入内核态(4->5)
  5. 回到了内核态之后,在回到用户态,刚刚代码中断的地方。

将进程切换简单的抽象为一个横置的数字8

问题1:进程切换的步骤4是否有存在的必要?既然在权限小的用户态都能够执行代码,那么权限更大的内核态不也能够执行代码吗?

有必要存在,用户写的代码就应该让用户执行,让用户更小的权限去约束代码的行为。

如果让内核态去执行,用户写的代码,如果代码有一些非法的操作,在内核态极大的权限下,就可能被执行,导致操作系统出错。

问题2:就是既然60%的信号都是杀掉进程,为什么还要让进程去识别一下自己是否要被直接杀掉。

因为,进程可能还在处理一些比较重要的事情,如果不通知进程直接让他退出可能会有一些未知的错误,正确做法应该是通知进程,让进程判断自己要不要先把完成当下这个动作在退出。

9 用户态和内核态(进程地址空间第三讲)

我们使用系统调用的时候,进行跳转,本质上还是在自己的进程地址空间内进行跳转

不同的进程之间使用的都是同一个页表,因为操作系统只有一份加载进入内存,所以每一份进程的内核空间内的虚拟地址都是完全一致的。(这意味着进程无论怎么切换,进程都能找到OS)

所以,我们访问OS,本质上就是通过我的进程的地址空间的内核空间[3,4]来访问即可

9.2 内核态和用户态的标志

10 操作系统是怎么样运行起来的

信号技术本来就是通过软件的方式,来拟的硬性中断 谁让OS运行起来呢? 非常高频率的,每个非常短的时间,就给CPU发送中断,CPU不断地进行处理中断 OS的周期时钟中断(利用硬件周期性的发生终断) 操作系统是一个死循坏,不断在接受的外部的其他硬件中断

对于中断的反应行为集成在中断向量表

在调用系统调用的流程

11 信号捕捉的又一个系统调用sigaction

二、参数说明

  • signum:要设置或获取处理程序的信号编号。该参数可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号。因为这两个信号定义了自己的处理函数,所以为它们指定自己的处理函数将导致信号安装错误。
  • act:指向sigaction结构体的指针,在结构体实例中指定了对特定信号的处理方式。该参数可以为空,此时进程会以缺省方式对信号处理。
  • oldact:指向sigaction结构体的指针,用于保存原来对相应信号的处理方式。该参数可以指定为NULL。

三 特性

当某个信号的处理函数被调用时,OS自动将当前信号加入到进程的信号屏蔽字中,直到信号处理函数返回时,解除对当前信号的屏蔽,这样防止了信号被嵌套式的捕捉处理,如果此信号再次被产生时,它会被阻塞到当前处理结束为止。 如果调用信号处理函数时,除了屏蔽当前信号之外,还希望自动屏蔽其他信号,就可以用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

在调用信号的时候,出了当前信号被自动屏蔽之外,还希望屏蔽其他的信号,则可以用sa_mask字段说明

代码语言:javascript
代码运行次数:0
复制
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

void PrintSig(sigset_t pending)
{
    cout << "pending bitmap: ";

    for (int i = 31; i > 0; i--)
    {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
    sleep(1);
}

void handler(int signo)
{
    cout << "get signo: " << signo << endl;
    sigset_t pending;
    sigemptyset(&pending);
    while (true)
    {

        int n3 = sigpending(&pending);
        assert(n3 == 0);

        PrintSig(pending);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    // for(int signo = 1; signo <= 31; signo++) // 9, 19号信号无法被屏蔽, 18号信号会被做特殊处理
    //     sigaddset(&block, signo); // SIGINT --- 根本就没有设置进当前进程的PCB block位图中
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);

    int n = sigaction(2, &act, &oact);
    assert(n == 0);

    cout << "I am a process! pid: " << getpid() << endl;
    while (true)
        sleep(1);

    return 0;
}

12 可重入函数

该函数被执行流重复进入导致了产生问题,这样的函数就叫做不可重入函数,否则就叫做可重入函数。

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

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

13 volatile关键字

  1. 编译器(gcc、g++)提供了多个级别的优化,-O0不启用优化、-O1启用基本的优化、-O2启用更高级别的优化,-O. . . ,数字越大,优化级别越高。
  2. 当编译器对代码进行优化时,它会减少访问内存的次数,以提高程序的运行速度。
代码语言:javascript
代码运行次数:0
复制
// volatile int g_flag = 0;
int g_flag = 0;

void changeflag(int signo)
{
    (void)signo;
    printf("将g_flag,从%d->%d\n", g_flag, 1);
    g_flag = 1;
}

int main()
{
    signal(2, changeflag);

    while(!g_flag); // 故意写成这个样子, 编译器默认会对我们的代码进行自动优化!
    // {
    //     printf("hello world\n");
    //     sleep(1);
    // }

    printf("process quit normal\n");
    return 0;
}

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

代码语言:javascript
代码运行次数:0
复制
volatile int g_flag = 0;
// int g_flag = 0;

void changeflag(int signo)
{
    (void)signo;
    printf("将g_flag,从%d->%d\n", g_flag, 1);
    g_flag = 1;
}

int main()
{
    signal(2, changeflag);

    while (!g_flag)
        ; // 故意写成这个样子, 编译器默认会对我们的代码进行自动优化!
    // {
    //     printf("hello world\n");
    //     sleep(1);
    // }

    printf("process quit normal\n");
    return 0;
}

正常来讲:编译器不做任何优化的情况下,CPU无论是进行数据运算,还是逻辑运算都要从内存中读取数据再进行运算。

但是呢,编译器为了提高效率,一般会自作主张的进行一下优化,就是每次运算不从CPU中拿数据了,就直接使用CPU寄存器上的数据,这样就导致了第一中情况,明明改变了g_val(改变的是内存上的g_val,因为优化,CPU不再从内存上读取,转而一直使用寄存器上的)所以导致了循环一直执行。(这种情况叫,寄存器屏蔽内存)

而volatile就是阻止优化产生(保存内存的可见性)。

14 SIGCHLD信号

子进程退出,父进程不wait(),子进程就会变成僵尸进程。

子进程退出的时候,不是默默退出的,是会向父进程发送信号的,-17 SIGCHLD

代码语言:javascript
代码运行次数:0
复制
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int sig)
{
    cout << "I am father process get message: " << sig << endl;
   
}
int main()
{

    signal(17, handler);
    pid_t id = fork();

    if (0 == id)
    {
        int cnt = 5;
        while (cnt)
        {
            cout << "I am child process pid: " << getpid() << endl;
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    while (1)
        ;
    cout << "quit normal" << getpid() << endl;
    return 0;
}

既然,子进程退出,是发送信号的,那我们能不能利用这个信号,让父进程不用主动的去等待,反而是等到子进程发送了信号,再去回收子进程

代码语言:javascript
代码运行次数:0
复制
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include<sys/wait.h>
using namespace std;

void handler(int sig)
{
    cout << "I am father process get message: " << sig << endl;

    while (1)//第一个问题的解决方法,就是直接使用循环,来给回收子进程也套上循环
    {
        pid_t rid = waitpid(-1,nullptr,WNOHANG);//第二个问题的解决方法:让回收子进程变成轮询模式,如果没有等到子进程,就直接退出循环,去执行自己的代码

        if(rid>0)
        {
            cout << "wait success "  << endl;
        }
        else
        {
            break;
        }
    }
    
}
int main()
{

    signal(17, handler);

    for (int i = 0; i < 100; i++)//第一个问题:创建100个线程,同时退出,进程又只能记录下一个信号,根本来不及处理 父进程该怎么回收呢?
    {                            //第二个问题:如果只退出其中的50个进程,而另外的50个直接不退出了,此时,父进程一直阻塞在回收子进程的循环中怎么办?

        pid_t id = fork();

        if (0 == id)
        {
            int cnt = 5;
            while (cnt)
            {
                cout << "I am child process pid: " << getpid() << endl;
                cnt--;
                sleep(1);
            }
            exit(0);
        }
    }
    while (1)
        ;
    cout << "quit normal" << getpid() << endl;
    return 0;
}

15 特例

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

什么意识呢?就是把17号信号的默认行为改为忽略,这样就可以让OS帮你自动回收变成僵尸的子进程。

但是,也要注意,这样你的父进程也就得不到任何子进程退出的信息了。

我们发现,就是系统层面的SIGCHLD默认动作也是IGN,那么为什么我们设置一下的IGN就会有这个特性呢?

其实上,在Linux底层,这两个IGN也是有区别的,两个IGN一个系统的在底层是0 强转为函数指针

我们设置的是1强转为函数指针。目前只在Linux下有这个差异。其实上也就在Linux这个特殊情况下有差异。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 什么是信号
  • 2 为什么要有信号
  • 3 对于信号的反应
    • 3.1 默认行为
    • 3.2 signal()函数 -- 自定义行为对信号做出反应
    • 3.3 对信号进行忽略
  • 4 信号的产生的类型
    • 4.1 kill命令
    • 4.2 键盘输入产生信号
    • 4.3 系统调用接口
      • 4.3.1 kill()
      • 4.3.2 raise() 函数
    • 4.4 软件条件
    • 4.5 异常
  • 5. 信号产生的原因
    • 5.1 键盘输入转化成信号的过程
    • 5.2 代码除零发出异常信号的过程
    • 5.3 野指针发出异常信号的过程
  • 6 关于core dump标志位
  • 7 信号的保存
    • 7.2 三张表的匹配操作和系统调用
      • 7.2.1 sigset_t
      • 7.2.2 sigprocmask
      • 7.2.3 sigpending
      • 7.2.4 signal
  • 8 信号的处理
  • 9 用户态和内核态(进程地址空间第三讲)
    • 9.2 内核态和用户态的标志
  • 10 操作系统是怎么样运行起来的
  • 11 信号捕捉的又一个系统调用sigaction
  • 12 可重入函数
  • 13 volatile关键字
  • 14 SIGCHLD信号
  • 15 特例
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档