★ Linux ★ 信号

Ciallo~(∠・ω< )⌒☆ ~ 今天,我将和大家一起学习 linux 的信号~

❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️

澄岚主页:椎名澄嵐-CSDN博客

Linux专栏:★ Linux ★ _椎名澄嵐的博客-CSDN博客

❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️


目录

[壹 信号基础概念](#壹 信号基础概念)

[贰 信号产生](#贰 信号产生)

[2.1 键盘产生信号](#2.1 键盘产生信号)

[2.2 系统调用产生信号](#2.2 系统调用产生信号)

[2.2.1 kill](#2.2.1 kill)

[2.2.2 raise](#2.2.2 raise)

[2.2.3 abort](#2.2.3 abort)

[2.3 调用系统命令向进程发信号kill](#2.3 调用系统命令向进程发信号kill)

[2.4 异常产生信号](#2.4 异常产生信号)

[2.5 软件条件产生信号](#2.5 软件条件产生信号)

[叁 信号保存](#叁 信号保存)

[3.1 相关概念](#3.1 相关概念)

[3.2 在内核中的表示](#3.2 在内核中的表示)

[3.3 sigset_t](#3.3 sigset_t)

[3.4 信号集操作函数](#3.4 信号集操作函数)

[3.4.1 sigprocmask](#3.4.1 sigprocmask)

[3.4.2 sigpending](#3.4.2 sigpending)

[3.4.3 整合代码](#3.4.3 整合代码)

[肆 信号处理](#肆 信号处理)

[4.1 初步结论](#4.1 初步结论)

[4.2 操作系统是怎么运行的](#4.2 操作系统是怎么运行的)

[4.2.1 硬件中断](#4.2.1 硬件中断)

[4.2.2 时钟中断](#4.2.2 时钟中断)

[4.2.3 软中断](#4.2.3 软中断)

[4.3 内核态和用户态](#4.3 内核态和用户态)

[4.4 sigaction](#4.4 sigaction)

[伍 可重入函数](#伍 可重入函数)

[陆 volatile](#陆 volatile)

[柒 SIGCHLD信号](#柒 SIGCHLD信号)

[~ 完 ~](#~ 完 ~)


壹 信号基础概念

信号是一种给进程发送的,用来进行事件异步通知的机制。

信号的产生相对于进程的运行是异步的。

信号是发送给进程的。

可以类比于闹钟,红绿灯,上课铃,人的脸色等。人就相当于进程。这些东西会中断人正在做的事情。

信号要点:

  1. 进程在信号还没有产生的时候,就知道如何处理了。

  2. 处理信号不是立即处理,也可以等一会处理或者等合适的时候处理。

  3. OS程序员设计进程的时候,进程已经内置了对于信号的识别和处理方式。

  4. 给进程产生信号的的信号源非常多。


贰 信号产生

产生信号的方式很多:

2.1 键盘产生信号

1. 键盘怎么产生信号

ctrl+c 就是给目标进程发送2号信号的,相当一部分的信号处理方法是让自己终止。

信号查看:每个信号都有对应的数字和宏。34-64为实时信号(需要立即处理),1-31为普通信号(可以不被立即处理)。

进程收到信号后,会在合适的时候处理信号,处理三选一:默认处理动作,自定义信号处理动作,忽略处理。ctrl+c 发送的2号信号的默认处理动作就是默认处理动作。

更改自定义信号处理

cpp 复制代码
#include <signal.h>
typedef void (*sighandler_t)(int); // 函数指针 类型
sighandler_t signal(int signum, sighandler_t handler);
// 第一个参数为1-31 34-64(信号),第二个参数为一个返回值为void的函数指针
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handerSig(int sig)//会把signal函数的第一个参数传过来
{
    std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
    signal(SIGINT, handerSig);
    int cnt = 0;
    while(true)
    {
        std::cout << "Ciallo!~ " << cnt++ << std::endl;
        sleep(1);
    }
    return 0;
}

man 7 signal可以查看具体信号:Term为终止,Ign为忽略...

当所有信号都被自定义时:(9号信号无法被自定义捕捉,仍然可以终止进程)

cpp 复制代码
for (int i = 1; i < 31; i++)
    signal(i, handerSig);

2. 那什么是目标进程呢~
进程有前台进程和后台进程

./XXX 为前台进程 键盘产生的信号只能发给前台进程

./XXX & 为后台进程 CTRL+C不做处理

命令行shell进程默认就是前台进程

后台进程无法从标准输入(键盘)中获取内容,前台进程可以(前台进程本质就是要从键盘获取数据,组合键也是键盘输入 )。但都可以向标准输出打印。前台进程只有一个,后台进程可以很多个。屏幕相当于共享资源。

bash 复制代码
jobs       // 查看所有后台进程
fg 任务号  // 后台转前台
ctrl Z     // 把进程切换到后台
bg 任务号  // 让后台进程恢复运行

3. 信号不是立即处理的,需要记录下来,等待合适的时候处理。记录在哪,怎么记录:

task_struct 中的 unsigned int sigs;把这个整数当成位图:

比特位位置为信号编号。

比特位内容为是否收到。

比如0000.......0010为收到了一个二号信号。

发送信号的本质就是:向目标进程写信号,就是修改位图,pid和信号编号。

task_struct属于OS内的数据结构对象,修改位图就是修改内核数据,而内核数据只有OS才能改,所以不管信号怎么产生,必须让给OS发送,OS必须提供系统调用,kill就是系统调用。

2.2 系统调用产生信号

系统调用也可以产生信号。

2.2.1 kill

cpp 复制代码
NAME
       kill - send signal to a process
SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>
       int kill(pid_t pid, int sig);
cpp 复制代码
// mykill.cc
#include <iostream>
#include <sys/types.h>
#include <signal.h>
// ./mykill signumber pid
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "./mykill signumber pid" << std::endl;
    }
    int signum = std::stoi(argv[1]);
    pid_t target = std::stoi(argv[2]);

    int n = kill(target, signum);
    if (n == 0)
    {
        std::cout << "send " << signum << " to " << target << "success ~" << std::endl;
        return 0;
    }
    return 0;
}


2.2.2 raise

cpp 复制代码
NAME
       raise - send a signal to the caller
SYNOPSIS
       #include <signal.h>
       int raise(int sig);
cpp 复制代码
int main()
{
    for (int i = 1; i < 32; i++)
        signal(i, handerSig);
    for (int i = 1; i < 32; i++)
    {
        sleep(1);
        if (i == 9 || i == 19)
            continue;
        raise(i);     // 给进程发i号信号
    }
    return 0;
}

可以验证到9号和19号信号不能被自定义。

2.2.3 abort

cpp 复制代码
NAME
       abort - cause abnormal process termination
SYNOPSIS
       #include <stdlib.h>
       void abort(void);
cpp 复制代码
int main()
{
    for (int i = 1; i < 32; i++)
        signal(i, handerSig);
    int cnt = 0;
    while(true)
    {
        std::cout << "Ciallo!~ " << cnt++ << " pid: " << getpid() <<  std::endl;
        abort();
        sleep(1);
    }
    return 0;
}

可以看到使用abort时,已经被自定义的6号信号会被恢复成默认的终止

2.3 调用系统命令向进程发信号kill

bash 复制代码
kill -信号号 pid

2.4 异常产生信号

比如当运行的程序发生除零错误时会收到8号信号SIGFPE:

cpp 复制代码
int main()
{
    for (int i = 1; i < 32; i++)
        signal(i, handerSig);

    int cnt = 0;
    while(true)
    {
        std::cout << "Ciallo!~ " << cnt++ << " pid: " << getpid() <<  std::endl;
        // 发生除零异常
        int a = 10;
        a /= 0;
        sleep(1);
    }
    return 0;
}

同样,野指针会收到11号信号,SIGSEGV段错误。

在信号的处理动作中,有Core和Term两种终止方式,有什么区别呢~

Term:直接退出

Core :核心,会在当前路径下形成一个文件,进程异常退出时,进程在内存中的核心数据从内存拷贝到磁盘, 形成一个文件,叫做核心转储,支持Debug。云服务器上,core和dump功能默认是被禁止的。

可以 ulimit -c 40960 打开此功能~ 若打开了此功能,遇到异常,会终止进程并core dump,在运行崩溃后,再gdb时输入core-file core就可以查看错误~ 叫做事后调试

cpp 复制代码
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(2);
        printf("Ciallo~\n");
        printf("Ciallo~\n");
        printf("Ciallo~\n");
        printf("Ciallo~\n");
        printf("Ciallo~\n");
        int a = 10;
        a /= 0;
        printf("Ciallo~\n");
        printf("Ciallo~\n");
        printf("Ciallo~\n");
        exit(1);
    }
    int status = 0;
    waitpid(id, &status, 0);
    printf("signal: %d, exitcode: %d, core dump: %d\n", (status&0x7F), (status>>8)&0xFF, (status>>7)&0x1); 
    return 0;
}

2.5 软件条件产生信号

比如一个管道文件,把读端关闭时,写端就会自动崩溃,发送信号SIGPIPE ~
管道文件是一种软件,读端关闭就是软件条件不满足,会终止进程~

再举个例子:

alarm:设置闹钟:

alarm(5): 设定时间5s结束,OS就会给进程发信号~~

alarm(0): 取消闹钟~

函数返回值为0或者以前设定的剩余秒数。

cpp 复制代码
NAME
   alarm - set an alarm clock for delivery of a signal
SYNOPSIS
   #include <unistd.h>
   unsigned int alarm(unsigned int seconds);
cpp 复制代码
void handerSig(int sig)//会把signal函数的第一个参数传过来
{
    std::cout << "获得了一个信号: " << sig << std::endl;
    exit(13);
}
int main()
{
    for (int i = 0; i <= 32; i++)
        signal(i, handerSig);
    alarm(1);//闹钟1秒后响
    int cnt = 0;
    while (true)
    {
        std::cout << "count: " << cnt++ << std::endl; // IO打印效率低
        cnt++;
    }
    return 0;
}

会发现程序一秒内循环的次数很少,是因为IO效率很低,可以更改为下代码:

cpp 复制代码
int cnt = 0;
void handerSig(int sig)
{
    std::cout << "获得了一个信号: " << sig << "cnt: " << cnt << std::endl;
    exit(13);
}
int main()
{
    signal(SIGALRM, handerSig);
    alarm(1);//闹钟1秒后响
    while (true)
        cnt++;
    return 0;
}

现在想要实现一个程序,进程一直暂停着,一旦收到一个信号,就唤醒一次,完成一些任务,可以这样实现:

cpp 复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
////////////////////func///////////////////////////////
void Sched()
{
    std::cout << "进程调度" << std::endl;
}
void MemManger()
{
    std::cout << "内存管理" << std::endl;
}
void Fflush()
{
    std::cout << "刷新程序" << std::endl;
}
///////////////////////////////////////////////////////
using func_t = std::function<void()>; // 返回值为void的函数 命名为func_t类型
std::vector<func_t> funcs; // 返回值为void的函数列表

// 每隔一秒,完成一些任务
void handerSig(int sig) //会把signal函数的第一个参数传过来
{
    std::cout << "##############################################" << std::endl;
    for (auto f : funcs) // 执行函数列表内的函数
    {
        f();
    }
    std::cout << "##############################################" << std::endl;
    int n = alarm(1); // 重新设定一个闹钟,成为循环
}
int main()
{
    funcs.push_back(Sched);
    funcs.push_back(MemManger);
    funcs.push_back(Fflush);
    // 遇到SIGALRM信号,执行handerSig,handerSig中又会设置下一个闹钟
    signal(SIGALRM, handerSig); 
    alarm(1); // 初始闹钟
    while (true)
    {
        // 进程一直暂停,一旦收到一个信号,唤醒一次
        pause();
    }
    return 0;
}

操作系统就是这种类型的程序,进程暂停着,以信号作为驱动。

操作系统中又很多闹钟,可以将这些闹钟管理起来,链表结构体内容为:

cpp 复制代码
struct timer_list {
    struct list_head entry; // 闹钟列表起始
    unsigned long expires; // 最短的闹钟剩余时间
    void (*function)(unsigned long); // 收到信号后执行的任务
    unsigned long data;
    struct tvec_t_base_s *base;
};

用堆结构将闹钟剩余时间最短的闹钟,成为堆头,保存在结构体中,到时间后给进程发SIGALRM信号。

所以闹钟属于软件条件的一种。


叁 信号保存

3.1 相关概念

实际执行信号的处理动作称为信号递达 (Delivery)
信号递达有三种 :自定义,默认,忽略。

信号从产生到递达之间的状态,称为信号未决(Pending)。信号还在位图中,未被处理。

进程可以选择阻塞 (Block )某个信号 。也叫屏蔽信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

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

3.2 在内核中的表示

pending表是位图,用来保存收到的信号 ,unsigned int pending有32个比特位对应32种信号,叫做未决表 。比特位位置为信号编号。比特位内容为是否收到

block表也是一张位图,用来保存要阻塞的信号 ,比特位位置为信号编号。比特位内容为是否阻塞

所以pending & (~block)哪些信号能被递达

handler表是一个函数指针数组 ,数组下标为信号编号,内容为32种信号对应的函数方法。signal函数就是在修改handler表。把对应的函数地址填入数组。SIG_DFL为默认处理,SIG_IGN为忽略处理。

3张表共同完成了对32个信号的描述。

3.3 sigset_t

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

3.4 信号集操作函数

cpp 复制代码
#include <signal.h>
// 位图全置0 , 1
int sigemptyset(sigset_t *set);  
int sigfillset(sigset_t *set);
// 把位图第几号信号置1 ,0
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
// 判断位图第几号信号是否为1
int sigismember(const sigset_t *set, int signo);

3.4.1 sigprocmask

此函数用于更改或设置block表:

cpp 复制代码
NAME
       sigprocmask, rt_sigprocmask - examine and change blocked signals
SYNOPSIS
       #include <signal.h>
       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
RETURN VALUE
       sigprocmask() returns 0 on success and -1 on error. 

第一个参数有下三种:

  • SIG_BLOCK:想要阻塞哪位就在set中把哪位置1
  • SIG_UNBOLOCK:想要取消阻塞哪位就在set中把哪位置1
  • SIG_SETMASK:把原BLOCK表替换为set,最方便

第三个参数为输出型参数,返回旧的BLOCK表,以防改错~

3.4.2 sigpending

获取当前pending信号集(不修改)

cpp 复制代码
NAME
       sigpending, rt_sigpending - examine pending signals
SYNOPSIS
       #include <signal.h>
       int sigpending(sigset_t *set);
RETURN VALUE
       sigpending() returns 0 on success and -1 on error. 

pending表的修改就是上5种信号产生的方式~

3.4.3 整合代码

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void PrintPending(sigset_t pending)
{
    printf("我是一个进程( %d ), pending: ", getpid());
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

void handler(int sig)
{
    std::cout << "#######################################" << std::endl;
    std::cout << "递达" << sig << "信号 ~"<< std::endl;
    sigset_t pending;
    int m = sigpending(&pending);
    PrintPending(pending);
    // 若是0000 0010 处理完2号才置0
    // 若是0000 0000 处理前2号就置0
    std::cout << "#######################################" << std::endl;
}

int main()
{
    signal(SIGINT, handler);
    // 1. 屏蔽2号信号, block信号集2号位置1
    sigset_t block, oldblock;
    sigemptyset(&block);
    sigemptyset(&oldblock);
    sigaddset(&block, SIGINT); // 到此只改了变量, 需要更新到进程中

    // for (int i = 0; i <= 31; i++) // 会屏蔽所有信号, 除9外
    // {
    //     sigaddset(&block, i);
    // }
    int n = sigprocmask(SIG_SETMASK, &block, &oldblock);
    (void)n;

    // 4. 重复获取打印
    int cnt = 0;
    while (true)
    {
        // 2. 获取pending信号集
        sigset_t pending;
        int m = sigpending(&pending);
        // 3. 打印
        PrintPending(pending);
        if (cnt == 10)
        {
            // 5. 恢复对2号信号的阻塞, 会直接递达2号信号
            // 若不对2号信号做处理, 就会按默认直接终止进程
            std::cout << "解除对2号信号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &oldblock, nullptr);
        }
        sleep(1);
        cnt++;
    }
    return 0;
}

若不改变处理动作:

最后结果:

可以发现在递达前,pending表的对应位置就置0了。


肆 信号处理

到这里会有两个问题:

信号处理有三种动作,是怎么被处理的?

信号在合适的时间被处理,什么是合适的时间?

4.1 初步结论

信号处理的方式如下:

在执行main函数时,会因为系统调用等原因从用户态进入内核态 。在内核处理完后就会用do_signal函数检查是否有待处理的信号 (三个表中pending位为1,block位为0),如果有就会进行处理:
默认处理 :如2号信号,内核直接终止进程。
忽略处理 :返回用户态跳转的位置继续运行。
自定义处理:从内核态返回用户态,以用户身份执行自定义的函数,执行完后调用系统调用sigreturn返回内核态,在内核态执行sys_sigreturn后返回用户态跳转的位置继续运行。

自定义处理的整个流程是一个无限大符号 的样子:

流程中会有四次身份切换在**态只能以**身份执行 ,防止自定义函数中有非法操作,内核权限又很高。中间的交点是检查是否有待处理的信号的时间节点。

所有的进程都会被调度 ,当一个进程的时间片耗尽时,会被操作系统 拿下,这时就会进入内核态被检查信号~

4.2 操作系统是怎么运行的

4.2.1 硬件中断

在硬件视角 ,会有一个中断控制器连接着许多外设,每当外设准备好了,就会发送硬件中断 ,替换中断号n,中断控制器被激活会通知CPU,CPU得知有中断会到中断控制器获取中断号。
在软件方面 ,操作系统中会有一张中断向量表IDT ,其中维护着外设设备对应的函数指针数组,数组下标对应中断控制器中的中断号n,CPU得知那个外设设备准备好了,就执行那个中断服务。

操作系统不关心外设设备是否准备好,而是准备好会叫操作系统。

发中断->发信号 保存中断->记录信号

中断号->信号编号 处理中断->处理信号+自定义捕捉
软件中的信号就是在模仿硬件中的中断的思想 ~

当程序发生除零错误时,CPU内部会有一个溢出标志位EFLAGS,此标志位有效时,成为一种CPU内部触发的中断,在中断向量表中选择并执行中断服务处理异常,给目标进程发信号。

其他野指针,重复释放,缺页中断等都会被转化成硬件中断,执行对应的方法。

4.2.2 时钟中断

操作系统 没有硬件中断要处理的时候,在干嘛呢~

答案是一直暂停着 ,但它还有调度进程等功能,就需要有一个时钟源 ,以固定的频率向CPU发中断~执行对应的shedule函数了。

所以操作系统本质是在硬件驱动下进行调度的操作系统是一个基于中断进行工作的软件。

时钟源后来被集成到CPU中,成为了主频。

4.2.3 软中断

CPU内部可以让软件触发中断吗~

在x86_64下CPU的指令集中会有一个int 0x80 或 syscall,可以自动让CPU触发一次中断。

我们以前用的open,fork,umask等系统调用其实不是OS提供的,OS只提供syscall和系统调用号

在操作系统中有一个全部系统调用的函数指针表 ,每一个系统调用都有唯一的下标,这个下标叫做系统调用号

cpp 复制代码
// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};

从用户层面调用open方法时其实干了两件事:
move eax 5 :把函数指针表中的5为下标的sys_open函数地址放到eax寄存器中。
int 0x80或syscall : 把执行地址改到0x80位置。

在内核层面,中断向量表 中就有0x80为地址的函数CallSystem ,此函数也干了两件事:
获取系统调用号n : eax寄存器中已经存了目标函数指针的下标5。
调用系统调用方法 :sys_call_table[5](); _system_call :call [_sys_call_table+eax*4]

这样就完成了open的系统调用,open是被glibc封装的。(其他的也是如此)

CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱

CPU内部的软中断,比如除零/野指针等,我们叫做 异常

4.3 内核态和用户态

所以系统调用的过程也是在进程地址空间上进行的 。只不过从用户区跳转到内核区。
所有函数调用都是地址空间的跳转

下图可知,无论进程如何调度,都能找到操作系统。因为所有进程的内核区经过自同一个内核页表。

用户和内核都在同一个地址空间上了,用户能不能直接访问内核代码呢?
不能~ OS有自保功能只允许系统调用方式进行访问。CPU中的cs寄存器的后两位(CPL当前权限级别)(前面是代码段地址)表示在什么区(00为内核,11为用户,int 0x80 syscall就是在改这两位改变身份的)

用户态 :以用户身份,只能访问自己的【0,3GB】
内核态:以内核身份,通过系统调用的方式访问【3,4GB】

4.4 sigaction

cpp 复制代码
NAME
   sigaction, rt_sigaction - examine and change a signal action
SYNOPSIS
   #include <signal.h>
   int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
   // 第一个参数信号编号,第二个参数输入型,第三个参数输出型
RETURN VALUE
   sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

struct sigaction { // 实时信号也可以,不管
   void     (*sa_handler)(int); // 和signal第二个参数一样
   void     (*sa_sigaction)(int, siginfo_t *, void *); // 不管
   sigset_t   sa_mask; // 信号集类型
   int        sa_flags; // 不管
   void     (*sa_restorer)(void); // 不管
};

作用同signal,只不过功能更多。

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

当前处理的信号会被自动屏蔽,那想要处理目标信号的时候屏蔽其他信号就可以使用sa_mask~

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>void handler(int signum)
{
    std::cout << "hello signal: " << signum << std::endl;
    while (true)
    {
        // 不断获取pending表
        sigset_t pending;
        sigpending(&pending);
        for (int i = 31; i >= 1; i--)
        {
            if (sigismember(&pending, i))
                std::cout << "1";
            else
                std::cout << "0";
        }
        std::cout << std::endl;
        sleep(1);
    }
    exit(0);
}

int main()
{
    struct sigaction act, oldact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, &oldact); // 捕捉2号信号,234都被屏蔽
    while (true)
    {
        std::cout << "Ciallo~" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

伍 可重入函数

上图是带头单链表的头插,插入第一个节点的函数进行到一半时,收到了信号捕捉处理,刚好信号捕捉处理中又有insert函数,去插入第二个节点了。最终导致了node2丢失,内存泄漏

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

不可重入函数:

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

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

可重入函数:

函数中只有自己的临时变量。


陆 volatile

以下代码会根据编译的优化等级产生不同的结果:

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>

int flag = 0;

void handler(int signum)
{
    std::cout << "更改全局变量 ~ flag: " << flag << "-> 1" << std::endl;
    flag = 1;
}

int main()
{
    signal(2, handler); // 2号信号处理改成更改flag值~
    while (!flag); // flag = 0时死循环
    std::cout << "procress quit normal~" << std::endl;
    return 0;
}

在O1以上的优化下,flag变量会被优化到CPU的寄存器中,更改时只改变了物理内存中flag的值。导致了flag在物理内存中值为1,CPU中值为0。**寄存器覆盖了进程看到变量的真实情况。**一直在死循环。

cpp 复制代码
volatile int flag = 0; // 保证内存空间的可见性

在全局变量前加上volatile关键字 ,可以保证内存空间的可见性,每次从内存中检测变量的值。这样优化到满,也可以正常运行。


柒 SIGCHLD信号

sigchld 编号17 ,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <sys/wait.h>

void WaitAll(int num)
{
    while (true)
    {
        pid_t n = waitpid(-1, nullptr, WNOHANG); // -1为任意一个子进程,WNOHANG非阻塞轮询,防止有子进程没退出卡住
        if (n == 0)
        {
            break;
        }
        else if (n < 0)
        {
            std::cout << "waitpid error" << std::endl;
            break;
        }
    }
    std::cout << "父进程收到信号: " << num << std::endl;
}

int main()
{
    // 父进程
    signal(SIGCHLD, WaitAll);
    for (int i = 10; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程
            sleep(3);
            std::cout << "我是子进程~" << std::endl;
            if (i <= 6)
                exit(3);
            else
                pause();
        }
        while (true)
        {
            // 父进程
            std::cout << "我是父进程~" << std::endl;
            sleep(1);
        }
    }
    return 0;
}

WNOHANG 可以使多个子进程退出一些个,还剩几个子进程的时候不在waitpid处卡住,使父进程不能继续运行。(非阻塞轮询

SIGCHILD的默认处理动作(SIG_DEF)是忽略(ign) ,子进程退出后会僵尸。
若把SIGCHILD的处理动作改成忽略(SIG_IGN),系统会自动回收子进程,父进程拿不到退出码。

cpp 复制代码
int main()
{
    // 父进程
    signal(SIGCHLD, SIG_IGN);
    for (int i = 10; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程
            sleep(3);
            std::cout << "我是子进程~" << std::endl;
            exit(3);
        }
        while (true)
        {
            // 父进程
            std::cout << "我是父进程~" << std::endl;
            sleep(1);
        }
    }
    return 0;
}

~ 完 ~

相关推荐
李白你好3 小时前
Auto_CVE - 自动化漏洞挖掘系统
运维·自动化
赵谨言3 小时前
基于Python的二手房价格数据分析预测系统
开发语言·经验分享·python
普通网友3 小时前
C++构建缓存加速
开发语言·c++·算法
带鱼吃猫3 小时前
高并发内存池(三):手把手从零搭建ThreadCache线程缓存
数据结构·c++·链表·visual studio
牛奶咖啡133 小时前
Linux中实现可执行文件或脚本在全局可用
linux·设置可执行程序全局可用·设置脚本全局可用·linux默认执行目录·linux环境变量
情深不寿3173 小时前
传输层————TCP
linux·网络·c++·tcp/ip
大翻哥哥3 小时前
Python 2025:新型解释器与性能优化实战
开发语言·python
christine-rr4 小时前
【25软考网工】第五章(10) Internet应用
linux·网络·经验分享·笔记·软考
触想工业平板电脑一体机4 小时前
【触想智能】工业一体机在金融领域的应用优势和具体注意事项
运维·人工智能·安全·金融·机器人·自动化