【Linux】信号-下

在这里插入图片描述

欢迎来到Cefler的博客
博客主页:折纸花满衣
个人专栏:题目解析
推荐文章:【LeetCode】winter vacation training

【Linux】信号-下_第1张图片


目录

  • 信号递达,信号未决,信号阻塞
    • block表 ,pending表 ,handler表(信号保存)
    • sigset_t类型
  • 信号集操作函数
    • 常见信号集操作函数
    • sigaction函数
    • sigprocmask函数
    • sigpending函数
  • CPU、进程地址空间、内核级页表、内核级页表、物理内存它们之间的联系
  • 信号捕捉流程
  • 可重入函数和不可重入函数
  • volatile
  • SIGCHLD信号

信号递达,信号未决,信号阻塞

在操作系统中,进程之间可以通过信号进行通信。当一个进程向另一个进程发送信号时,会出现以下几种情况:信号递达、信号未决和信号阻塞。

  1. 信号递达(Signal Delivery):当一个进程向目标进程发送信号时,操作系统会将该信号递送给目标进程。递达的信号会触发目标进程相应的信号处理函数(signal handler)执行,或者引起默认的信号处理行为。这意味着信号已经成功传递到了目标进程。

  2. 信号未决(Signal Pending):如果目标进程正在处理一个信号,而此时又有一个相同的信号递达给它,那么这个信号就会被标记为未决状态。未决信号是一种被记录下来但尚未被处理的信号

  3. 信号阻塞(Signal Blocking):进程可以选择性地阻塞某些信号,使得它们在阻塞状态下不会递达到该进程。当信号被阻塞时,即使有该信号的递达请求,进程也不会接收到该信号。当解除对信号的阻塞时,之前被阻塞的信号会立即递达到进程。

信号被阻塞——>信号一定是未决状态

block表 ,pending表 ,handler表(信号保存)

在内核中,信号是一种异步事件,可能随时被发送给进程,并且进程需要及时响应。为了管理信号的处理过程,内核维护了三个重要的数据结构:block表、pending表和handler表。
【Linux】信号-下_第2张图片

  1. block表:用于记录被阻塞的信号集合。当一个进程调用sigprocmask函数时,它可以指定一组要阻塞的信号。这些信号会被放入该进程对应的block表中,使得它们在阻塞状态下不会递达到该进程。当进程需要解除对某个信号的阻塞时,可以再次调用sigprocmask函数来修改block表。

  2. pending表:用于记录未决的信号集合。当一个信号递达到目标进程时,如果目标进程正在处理另外一个相同的信号,那么这个信号就会被标记为未决状态,放入该进程对应的pending表中。当当前信号处理完毕后,内核会检查pending表中是否有未决的信号,如果有,则将它们取出并递达到进程。

  3. handler表:用于记录每种信号对应的信号处理函数。当一个信号递达到目标进程时,内核会根据该信号对应的handler表中的处理函数来执行相应的操作。如果进程没有安装该信号的处理函数,则会执行默认的信号处理行为。

这些表都是在进程控制块(PCB)中维护的,并且可以通过一些系统调用和库函数来修改和查询。例如,sigprocmask函数可以修改block表,sigpending函数可以查询pending表,signal和sigaction函数可以修改handler表。

sigset_t类型

sigset_t 是一个数据类型,用于表示一组信号的集合。在 POSIX 标准中,sigset_t 被定义为一个整数数组。

使用 sigset_t 可以方便地管理进程的信号屏蔽字和未决信号集合。进程的信号屏蔽字是一个 sigset_t 类型的变量,它用于标记哪些信号是被阻塞的。当一个信号被阻塞时,即使该信号有递达请求,也不会发送给进程。我们可以使用 sigprocmask 系统调用来修改进程的信号屏蔽字(信号屏蔽集合)。

另外,sigset_t 还可以用于查询进程的未决信号集合。未决信号集合记录了已经递达到进程,但尚未被处理的信号。我们可以使用 sigpending 系统调用来查询未决信号集合。该函数会将未决信号集合写入 sigset_t 类型的变量中返回。

在使用 sigset_t 时,我们可以使用一些辅助函数来对信号集合进行操作。例如:

  • sigemptyset:将一个信号集合清空,即将该集合中所有信号都设置为未包含状态。
  • sigfillset:将一个信号集合填充满,即将该集合中所有信号都设置为包含状态。
  • sigaddset:将一个信号添加到信号集合中。
  • sigdelset:将一个信号从信号集合中删除。

这些辅助函数可以方便我们对信号集合进行操作,从而更好地管理进程的信号处理。

信号集操作函数

常见信号集操作函数

当使用 sigset_t 表示信号集合时,可以使用以下函数进行信号集合的操作:

  1. int sigemptyset(sigset_t *set)
    该函数用于清空信号集合,将所有信号都设置为未包含状态(比特位清0)。它会将 set 指向的信号集合清空,成功返回0,失败返回-1。

  2. int sigfillset(sigset_t *set)
    该函数用于填充信号集合,将所有信号都设置为包含状态(比特位置为1)。它会将 set 指向的信号集合填充满,成功返回0,失败返回-1。

  3. int sigaddset(sigset_t *set, int signum)
    该函数用于将指定的信号添加到信号集合中。set 是要操作的信号集合的指针,signum 是要添加的信号编号。成功返回0,失败返回-1。

  4. int sigdelset(sigset_t *set, int signum)
    该函数用于从信号集合中删除指定的信号。set 是要操作的信号集合的指针,signum 是要删除的信号编号。成功返回0,失败返回-1。

  5. int sigismember(const sigset_t *set, int signum)
    该函数用于检查指定的信号是否在信号集合中。set 是要检查的信号集合的指针,signum 是要检查的信号编号。如果信号在信号集合中,返回1;如果信号不在信号集合中,返回0;如果发生错误,返回-1。

这些函数可以帮助我们方便地对信号集合进行操作,例如创建空的信号集合将所有信号添加到信号集合中从信号集合中删除特定的信号检查信号是否在信号集合中等。


以下是每个函数的用法示例

  1. int sigemptyset(sigset_t *set)
    该函数用于清空信号集合,将所有信号都设置为未包含状态。
#include 

int main() {
    sigset_t set;
    sigemptyset(&set);  // 清空信号集合
    return 0;
}
  1. int sigfillset(sigset_t *set)
    该函数用于填充信号集合,将所有信号都设置为包含状态。
#include 

int main() {
    sigset_t set;
    sigfillset(&set);  // 填充信号集合
    return 0;
}
  1. int sigaddset(sigset_t *set, int signum)
    该函数用于将指定的信号添加到信号集合中。
#include 

int main() {
    sigset_t set;
    sigemptyset(&set);  // 清空信号集合
    sigaddset(&set, SIGINT);  // 将 SIGINT 信号添加到信号集合中
    return 0;
}
  1. int sigdelset(sigset_t *set, int signum)
    该函数用于从信号集合中删除指定的信号。
#include 

int main() {
    sigset_t set;
    sigfillset(&set);  // 填充信号集合
    sigdelset(&set, SIGINT);  // 从信号集合中删除 SIGINT 信号
    return 0;
}
  1. int sigismember(const sigset_t *set, int signum)
    该函数用于检查指定的信号是否在信号集合中。
#include 
#include 

int main() {
    sigset_t set;
    sigemptyset(&set);  // 清空信号集合
    sigaddset(&set, SIGINT);  // 将 SIGINT 信号添加到信号集合中

    if (sigismember(&set, SIGINT)) {
        printf("SIGINT is a member of the signal set\n");
    } else {
        printf("SIGINT is not a member of the signal set\n");
    }

    return 0;
}

sigaction函数

sigaction函数是用于设置和修改信号处理函数的系统调用。通过调用该函数,可以为特定的信号注册一个信号处理函数,并指定信号处理的行为。

sigaction函数的原型如下:

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

参数说明:

  • signum:表示要注册的信号。
  • act:一个指向struct sigaction类型的指针,用于指定新的信号处理函数以及信号处理的行为。
  • oldact:一个指向struct sigaction类型的指针,用于保存之前的信号处理函数以及信号处理的行为。

函数返回值为0表示成功,返回-1表示出错。

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作若oact指针非 空,则通过oact传
出该信号原来的处理动作
。act和oact指向sigaction结构体:

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

下面是一个简单的示例程序,演示了如何使用sigaction函数:

#include 
#include 

void handler(int sig)
{
    printf("Received signal %d\n", sig);
}

int main()
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    // 注册信号处理函数
    sigaction(SIGINT, &sa, NULL);

    while(1) {}

    return 0;
}

在这个示例程序中,首先定义了一个信号处理函数handler来处理 SIGINT 信号。然后创建了一个struct sigaction类型的结构体sa,并将其中的sa_handler成员设置为handler,其他成员设置为默认值。接着调用sigaction函数,为 SIGINT 信号注册了信号处理函数sa。

最后程序进入一个无限循环,等待 SIGINT 信号的到来。当进程收到 SIGINT 信号时,会调用事先注册的信号处理函数handler来处理该信号,并打印一些信息。

通过使用sigaction函数,可以为特定的信号注册一个信号处理函数,并指定信号处理的行为。与signal函数相比,sigaction函数提供了更多的灵活性和可靠性。



struct sigaction 是一个结构体,用于在 Linux 中设置和修改信号处理函数的行为。通过 sigaction() 系统调用可以使用这个结构体注册对信号的处理方式。

下面是 struct 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);
};
  • sa_handler:指定信号处理函数的地址。当 sa_flags 没有设置 SA_SIGINFO 标志时,使用该字段设置信号处理函数。

  • sa_sigaction:指定带有三个参数的信号处理函数的地址。当 sa_flags 设置了 SA_SIGINFO 标志时,使用该字段设置信号处理函数。

  • sa_mask:指定在信号处理过程中要添加到进程的信号屏蔽字中的信号集。即,在执行信号处理函数期间会阻塞这些信号。

  • sa_flags:指定信号处理的一些标志。常见的标志包括:

    • SA_RESTART:如果信号中断了系统调用,系统调用将自动重启。
    • SA_NODEFER:不会在信号处理函数执行期间阻止当前信号。
    • SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler 作为信号处理函数。
  • sa_restorer:在某些平台上已经被弃用,一般不需要设置。

通过填充和设置 struct sigaction 结构体的各个字段,可以控制信号的处理方式和行为,以及指定一些与信号处理相关的属性。使用 sigaction() 系统调用可以将这个结构体应用到具体的信号上,从而实现对信号处理函数的定制化配置。

sigprocmask函数

sigprocmask函数是一个用于设置和修改进程信号屏蔽字的系统调用。通过调用该函数,可以控制进程对特定信号的处理方式。

sigprocmask函数的原型如下:

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

参数说明:

  • how:表示如何修改信号屏蔽字的方式,可以取以下值:

    • SIG_BLOCK:将set中的信号添加到当前进程的信号屏蔽字中,即阻塞这些信号。(oldset原有的基础上添加新的信号屏蔽字)
    • SIG_UNBLOCK:从当前进程的信号屏蔽字中移除set中的信号,即解除对这些信号的阻塞。
    • SIG_SETMASK:将当前进程的信号屏蔽字设置为set中的值,即使用set中的信号屏蔽字替换当前进程的信号屏蔽字。
      【Linux】信号-下_第3张图片
  • set:一个指向sigset_t类型的指针,用于指定要修改的信号集合。

  • oldset:一个指向sigset_t类型的指针,用于保存之前的信号屏蔽字。

函数返回值为0表示成功,返回-1表示出错。

下面是一个简单的示例程序,演示了如何使用sigprocmask函数:

#include 
#include 
#include 

void handler(int sig)
{
    printf("Received signal %d\n", sig);
}

int main()
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    // 注册信号处理函数
    sigaction(SIGINT, &sa, NULL);

    printf("Blocking SIGINT...\n");

    // 阻塞 SIGINT 信号
    sigset_t set, oldset;
    sigemptyset(&set);
     sigemptyset(&oldset);//先都清空
    sigaddset(&set, SIGINT);
    sigprocmask(SIG_BLOCK, &set, &oldset);

    printf("SIGINT is blocked. Sleeping for 10 seconds...\n");
    sleep(10);

    printf("Unblocking SIGINT...\n");

    // 解除对 SIGINT 信号的阻塞
    sigprocmask(SIG_SETMASK, &oldset, NULL);

    printf("SIGINT is unblocked. Sleeping for 10 seconds...\n");
    sleep(10);

    return 0;
}

在这个示例程序中,首先注册了一个信号处理函数handler来处理 SIGINT 信号。然后调用sigprocmask函数,将 SIGINT 信号添加到进程的信号屏蔽字中,即阻塞 SIGINT 信号。之后程序会打印一些信息,并休眠10秒钟。在这段时间内,如果进程收到 SIGINT 信号,则该信号会被暂时挂起,直到解除对该信号的阻塞。

接着,程序调用sigprocmask函数解除对 SIGINT 信号的阻塞,再次打印一些信息并休眠10秒钟。在这段时间内,如果进程收到 SIGINT 信号,则会调用事先注册的信号处理函数handler来处理该信号。

sigpending函数

sigpending函数用于获取当前进程未决的信号集,即已经发送但尚未被进程处理的信号集。

sigpending函数的原型如下:

int sigpending(sigset_t *set);

参数说明:

  • set:一个指向sigset_t类型的指针,用于保存未决信号集。

函数返回值为0表示成功,返回-1表示出错。

下面是一个简单的示例程序,演示了如何使用sigpending函数:

#include 
#include 
#include 

void handler(int sig)
{
    printf("Received signal %d\n", sig);
}

int main()
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    // 注册信号处理函数
    sigaction(SIGUSR1, &sa, NULL);

    // 发送 SIGUSR1 信号
    printf("Sending SIGUSR1...\n");
    kill(getpid(), SIGUSR1);

    // 检查未决信号集
    sigset_t set;
    sigpending(&set);

    if (sigismember(&set, SIGUSR1)) {
        printf("SIGUSR1 is pending\n");
    } else {
        printf("SIGUSR1 is not pending\n");
    }

    return 0;
}

在这个示例程序中,首先定义了一个信号处理函数handler来处理 SIGUSR1 信号。然后创建了一个struct sigaction类型的结构体sa,并将其中的sa_handler成员设置为handler,其他成员设置为默认值。接着调用sigaction函数,为 SIGUSR1 信号注册了信号处理函数sa。

然后程序通过kill函数向当前进程发送了 SIGUSR1 信号。接着调用sigpending函数,获取当前进程的未决信号集,并将结果保存在set中。然后使用sigismember函数判断 SIGUSR1 是否在未决信号集中,如果是,则打印相应的信息。

通过使用sigpending函数,可以获取当前进程未决的信号集,即已经发送但尚未被进程处理的信号集。这对于需要了解当前进程是否收到某个信号以及是否处理了该信号非常有用。

CPU、进程地址空间、内核级页表、内核级页表、物理内存它们之间的联系

CPU、进程地址空间、内核级页表、用户级页表和物理内存之间有着密切的联系,它们共同协作实现了操作系统的内存管理功能:

  1. CPU:中央处理器负责执行指令和处理数据。CPU通过地址总线和数据总线与其他硬件设备进行通信,包括内存等。

  2. 进程地址空间:每个进程都有自己独立的虚拟地址空间,用于存储程序的代码、数据和堆栈等信息。进程地址空间是虚拟的,需要通过内存管理单元(MMU)将虚拟地址转换为物理地址才能在实际的物理内存中找到对应的数据。

  3. 内核级页表:内核级页表是操作系统内核维护的页表结构,用于管理整个系统的物理内存。内核级页表包含了操作系统内核的代码和数据的地址映射信息,确保操作系统内核能够访问到系统的所有物理内存。

  4. 用户级页表:每个用户进程都有自己的用户级页表,用于将进程的虚拟地址空间映射到实际的物理内存。用户级页表只包含当前进程的地址映射信息,受到进程的权限限制,可以被用户进程修改。

  5. 物理内存:物理内存是实际存储数据的硬件内存,包括RAM等。内核级页表和用户级页表的作用就是将进程的虚拟地址空间映射到物理内存,在CPU执行指令时,MMU会根据页表将虚拟地址转换为物理地址,从而访问到实际的数据。

总的来说,CPU通过内核级页表和用户级页表实现对进程地址空间到物理内存的映射,确保不同进程之间的内存空间相互隔离,同时实现对系统内存的有效管理和保护。这些组件共同工作,构成了操作系统的内存管理机制。


内核级页表整个OS只有一张,而用户级页表有多张,是因为内核级页表用于管理整个系统的物理内存,而用户级页表用于管理每个用户进程的虚拟地址空间


用户态:只能访问进程地址空间的【0,3GB】空间
内核态:可以让用户以OS的身份访问【3,4GB】

在用户态下,进程只能访问自己的地址空间,也就是虚拟地址空间的低 3GB 区域,这个区域也被称为用户空间(User Space),它包含了进程的代码、数据和堆栈等信息。进程可以通过系统调用(System Call)等方式请求操作系统提供服务,但是进程无法直接访问内核的地址空间。

而在内核态下,CPU 拥有更高的权限,可以访问所有的物理内存空间,包括虚拟地址空间的高 1GB 区域,也就是内核空间(Kernel Space)。在内核态下,操作系统可以以自己的身份执行各种系统级任务,并且可以直接控制硬件设备。

需要注意的是,进程必须通过系统调用等方式才能从用户态切换到内核态,也就是进入操作系统的上下文环境。这是因为操作系统为了保证系统的安全性和稳定性,只允许特权级别更高的内核代码访问系统资源。当进程需要执行一些特殊的操作,如访问硬件设备或在内存中分配大块的内存等时,就必须通过系统调用等方式请求操作系统提供服务,从而进入内核态执行相应的代码。


linux环境下操作系统怎么判别是用户态还是内核态?
在 Linux 环境下,操作系统通过使用特殊的 CPU 指令来判断当前是用户态还是内核态。关键的指令是 sysentersysexit

当进程需要从用户态切换到内核态时,它通常会执行一个系统调用指令(例如 int 0x80syscall),这会触发一个中断。操作系统的中断处理程序会检测到这个中断,并执行相应的内核代码。在处理中断之前,操作系统会将进程的上下文信息保存到内核栈中。

在内核态中,操作系统会根据被中断的进程的上下文信息进行相应的处理。完成处理后,操作系统会将进程的上下文信息从内核栈中恢复回来,然后执行 sysexit 指令,这会导致 CPU 从内核态切换回用户态。sysexit 指令会使用保存在内核栈中的返回地址,将 CPU 的特权级别从内核态切换到用户态,并从保存的返回地址继续执行用户进程的代码。

总的来说,Linux 环境下,操作系统利用特殊的 CPU 指令和中断机制来切换用户态和内核态。系统调用指令触发从用户态到内核态的切换,而 sysexit 指令触发从内核态到用户态的切换。在这个过程中,操作系统会保存和恢复进程的上下文信息,保证进程在切换后能够继续执行。

信号捕捉流程

在 Linux 中,信号的捕捉流程通常涉及以下几个步骤:

  1. 注册信号处理函数:进程可以使用 signal()sigaction() 等函数来注册信号处理函数。这些函数用于指定当特定信号到达时要执行的处理函数。

  2. 触发信号:信号可以由多种方式触发,例如硬件异常、其他进程发送信号、操作系统事件等。一旦信号被触发,操作系统会将其发送给目标进程。

  3. 信号处理:当信号到达进程时,操作系统会暂停目标进程的执行,并在进程的上下文中执行注册的信号处理函数。这意味着当前进程的代码会被中断,转而执行信号处理函数。

  4. 执行信号处理函数:在信号处理函数中,进程可以执行特定的逻辑来处理接收到的信号。这可以包括清理资源、处理异常情况、修改进程行为等操作。

  5. 信号返回:一旦信号处理函数执行完成,进程会从处理信号的地方继续执行。如果信号处理函数中没有显式调用 exit() 或者 longjmp() 等函数来终止进程,则进程会继续执行原先被中断的代码。

【Linux】信号-下_第4张图片

sigreturn
sigreturn 是一个系统调用,用于从信号处理函数返回到原始上下文。当进程收到一个信号时,操作系统会调用相应的信号处理函数来处理该信号。在信号处理函数执行期间,操作系统会保存当前进程的上下文信息,并按照信号处理函数的需求进行相应的处理。

而一旦信号处理函数执行完成,通过 sigreturn 系统调用将会返回到原始的上下文,继续执行被中断的代码。sigreturn 会将之前保存的上下文信息恢复回来,使得进程能够继续执行。

在 Linux 中,sigreturn 实际上是通过 rt_sigreturn 系统调用来实现的。这个系统调用会将之前保存的信号屏蔽字、信号处理函数的栈帧和信号处理函数的返回地址等信息恢复回来,然后返回到原始的上下文。

需要注意的是,sigreturn 是一个非常底层的系统调用,一般由 C 库提供的高级接口(如 sigaction)来处理信号的注册和处理。在正常情况下,无需直接使用 sigreturn 系统调用,而是由操作系统自动处理信号的返回。

总结起来,sigreturn 是一个系统调用,用于从信号处理函数返回到原始上下文。它恢复之前保存的上下文信息,使得进程能够继续执行被中断的代码。

可重入函数和不可重入函数

可重入函数(reentrant function)是指在函数执行过程中,不会使用任何全局变量或静态变量,因此可以安全地被多个线程同时调用而不会产生冲突。可重入函数通常使用参数传递所有需要的数据,而不依赖于外部状态。

不可重入函数(non-reentrant function)是指在函数执行过程中使用了全局变量或静态变量,多个线程同时调用这个函数可能会导致数据竞争和不确定的结果。由于不可重入函数使用了共享状态,因此在多线程环境下可能会出现问题。

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

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

区分可重入函数和不可重入函数的关键在于函数内部是否使用了共享状态。可重入函数是线程安全的,因为每次调用都是独立的,不会受到其他线程的影响。而不可重入函数可能需要额外的同步机制来保证在多线程环境下的正确性。

在并发编程中,应尽量使用可重入函数,以避免潜在的线程安全问题。如果必须使用不可重入函数,需要考虑添加适当的同步控制来确保线程安全。

volatile

volatile 是 C 和 C++ 中的关键字,用于告诉编译器不要对被声明为 volatile 的变量进行优化,因为这些变量可能会在程序的控制之外被修改。volatile 告诉编译器不要对变量进行优化,以确保每次对变量的访问都是真实地发生,而不是从缓存或寄存器中读取。

在多线程编程中,volatile 关键字通常用于多线程共享的变量,告诉编译器不要对这些变量的访问进行优化,以确保线程之间能够正确地读取和写入这些变量的最新值。

在嵌入式系统开发中,volatile 也经常用于声明与外部硬件相关的寄存器,因为这些寄存器的值可能会被硬件直接修改,而不是通过代码。

需要注意的是,volatile 并不能解决所有的并发编程问题,它只能告诉编译器不要对变量进行优化,以确保变量的读取和写入操作能够正确地反映程序运行时的实际情况。在多线程编程中,还需要使用适当的同步机制来保证线程安全。


CPU一直检测寄存器中的数据情况
【Linux】信号-下_第5张图片
flag在寄存器中一直都是0,而信号处理函数只在内存上将flag改为1,但是CPU只读取寄存器上的flag值,所以此时会卡在while循环那。

而为了解决这个问题,我们就可以在flag前用volatile修饰,这样CPU就不会从寄存器中读取了

SIGCHLD信号

SIGCHLD 是一个由操作系统发送给父进程的信号用于通知父进程其子进程的状态发生了变化。当一个子进程终止或停止时,操作系统会向其父进程发送 SIGCHLD 信号。

父进程可以通过设置信号处理函数来捕获和处理 SIGCHLD 信号。一般情况下,父进程会调用 wait()waitpid() 系统调用来等待并获取子进程的状态信息。

使用 SIGCHLD 信号和相关的系统调用,父进程可以实现对子进程的管理和监控。例如,父进程可以检查子进程的退出状态处理僵尸进程(已终止但父进程尚未调用 wait() 获取状态的子进程)以及启动新的子进程等。

下面是一个简单的示例代码,演示了如何使用 SIGCHLD 来处理子进程的状态变化:

#include 
#include 
#include 
#include 
#include 
#include 

void sigchld_handler(int signum) {
    int status;
    pid_t pid;
    
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("Child process %d exited with status %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child process %d terminated by signal %d\n", pid, WTERMSIG(status));
        }
    }
}

int main() {
    // 设置 SIGCHLD 信号的处理函数为 sigchld_handler
    signal(SIGCHLD, sigchld_handler);
    
    pid_t child_pid = fork();

    if (child_pid == 0) {
        // 子进程
        printf("Child process\n");
        sleep(2);
        exit(0);
    } else if (child_pid > 0) {
        // 父进程
        printf("Parent process\n");
        sleep(5);
    } else {
        // fork 出错
        perror("fork");
        exit(EXIT_FAILURE);
    }

    return 0;
}

在上面的示例中,父进程创建了一个子进程,并设置了 SIGCHLD 信号的处理函数为 sigchld_handler。当子进程终止时,sigchld_handler 会被调用,并通过调用 waitpid() 来获取子进程的状态信息。

请注意,在实际的程序中,需要适当处理信号处理函数的并发执行以及可能的竞态条件。此外,还可以使用 sigaction() 函数来注册信号处理函数,以获得更精确的控制和更强大的功能。

总结来说,SIGCHLD 是一个由操作系统发送给父进程的信号,用于通知父进程其子进程的状态发生了变化。父进程可以通过设置信号处理函数来捕获和处理 SIGCHLD 信号,以实现对子进程的管理和监控。


事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程


如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注❤️ ,学海无涯苦作舟,愿与君一起共勉成长

【Linux】信号-下_第6张图片
在这里插入图片描述

你可能感兴趣的:(Linux,linux,信号)