Linux(7)(下)

https://blog.csdn.net/qscftqwe/article/details/156190314

这是上节课链接,大家可以点击进行观看

一.信号发送和保存

1.信号发送

信号在进程中的管理,每个进程的 task_struct 中通过位图 (sigset_t)记录收到的普通信号(1~31)。

所谓"发信号",就是操作系统在目标进程的信号位图中将对应信号的位置为 1,表示该信号已到达。

1.1 信号保存

1.信号其他相关常见概念

  • 执行信号的处理动作称为信号送达(Delivery)(handler)

  • 信号从产生到送达之间的状态(保存),称为信号未决 (Pending)

  • 进程可以选择阻塞(屏蔽) (Block) 某个信号

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

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

2.在内核中的表示

block:0/1表示不阻塞/阻塞(9号和19号不能阻塞)

pending 位图:某信号位为 1 表示已收到但未处理(未决),0 表示没收到或已处理。
handler:记录该信号的处理方式------可以是默认动作(如终止)、忽略(SIG_IGN)或自定义函数地址(用户注册的 handler)。

3.信号集操作函数(系统给用户级使用的数据类型)

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

1.sigemptyset

cpp 复制代码
// 初始化信号集 使其中所有信号对应位清零(无信号被包含)
#include <signal.h>

int sigemptyset(sigset_t *set);

// 参数:
//   set - 指向信号集的指针

// 返回值:
//   成功:返回 0
//   失败:返回 -1

// 案例:
    // 创建一个空信号集
    sigemptyset(&set);

2.sigfillset

cpp 复制代码
// 初始化信号集 使其中所有信号对应位置位(包含所有有效信号)
#include <signal.h>

int sigfillset(sigset_t *set);

// 参数:
//   set - 指向信号集的指针

// 返回值:
//   成功:返回 0
//   失败:返回 -1

// 案例:
    // 创建一个包含所有信号的信号集
    sigfillset(&set);

3.sigaddset

cpp 复制代码
// 向信号集中添加一个指定信号
#include <signal.h>

int sigaddset(sigset_t *set, int signo);

// 参数:
//   set   - 指向信号集的指针
//   signo - 要添加的信号(如 SIGINT #2)

// 返回值:
//   成功:返回 0
//   失败:返回 -1

// 案例:
    // 添加 SIGINT(#2 中断信号)到信号集
    sigaddset(&set, SIGINT);

4.sigdelset

cpp 复制代码
// 从信号集中删除一个指定信号
#include <signal.h>

int sigdelset(sigset_t *set, int signo);

// 参数:
//   set   - 指向信号集的指针
//   signo - 要删除的信号(如 SIGTERM #15)

// 返回值:
//   成功:返回 0
//   失败:返回 -1

// 案例:
    // 从信号集中移除 SIGTERM(#15 终止请求)
    sigdelset(&set, SIGTERM);

5.sigismember

cpp 复制代码
// 判断信号集中是否包含指定信号
#include <signal.h>

int sigismember(const sigset_t *set, int signo);

// 参数:
//   set   - 指向信号集的指针
//   signo - 要检查的信号(如 SIGALRM #14)

// 返回值:
//   包含:返回 1
//   不包含:返回 0
//   出错:返回 -1

// 案例:
    // 检查是否包含 SIGALRM(#14 定时信号)
    if (sigismember(&set, SIGALRM)) { /* ... */ }

要点:在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态

1.2 认识两大重要函数

1.sigprocmask

cpp 复制代码
// 修改当前进程的信号屏蔽字(阻塞或解除阻塞指定信号)
// 常用于临界区保护 防止信号中断关键操作
#include <signal.h>

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

// 参数:
//   how     - 操作类型:
//             SIG_BLOCK:将 set 中的信号加入阻塞集(追加)
//             SIG_UNBLOCK:从阻塞集中移除 set 中的信号
//             SIG_SETMASK:直接用 set 替换当前阻塞集
//   set     - 指向要操作的信号集(可为 NULL)
//   oldset  - 若非 NULL 则保存修改前的信号屏蔽字

// 返回值:
//   成功:返回 0
//   失败:返回 -1

// 案例:
    // 阻塞 SIGINT(#2 中断信号)
    sigprocmask(SIG_BLOCK, &set, NULL);

2.sigpending

cpp 复制代码
// 获取当前被阻塞且处于未决状态(pending)的信号集
// 用于检查哪些信号已发送但因被阻塞而尚未处理
#include <signal.h>

int sigpending(sigset_t *set);

// 参数:
//   set - 指向信号集的指针 用于接收未决信号

// 返回值:
//   成功:返回 0
//   失败:返回 -1

// 案例:
    // 获取当前未决信号
    sigpending(&pending_set);

1.3 案例

cpp 复制代码
int main()
{
   // 1.1 先对 2 号信号进行屏蔽
   sigset_t bset, oset; // 用户栈上的信号集
   sigemptyset(&bset);   // 初始化为空信号集
   sigemptyset(&oset);   // 初始化为空信号集(为保存旧屏蔽掩码做准备)
   sigaddset(&bset, 2);  // 添加 2 号信号到屏蔽集
   
   // 1.2 设置屏蔽掩码(内核级操作)
   sigprocmask(SIG_SETMASK, &bset, &oset); 

   // 1.3 发送 2 号信号(此时信号会被放入 pending,但因屏蔽不会送达)
   kill(getpid(), 2); 

   // 2. 循环查看 pending 状态(应看到 2 号信号被置位)
   sigset_t pending;
   int cnt = 0;
   while (true)
   {
       sigpending(&pending); // 获取当前 pending 状态
       PrintPending(pending); // 打印 pending 位图
       //0000,0000,0000,0000,0000,0010打印了20次这个,21次信号传达->进程终止
       // 21次如果打印的话就是:0000,0000,0000,0000,0000,0000
       cnt++;
       
       // 20次后解除屏蔽
       if (cnt == 20)
       {
           sigprocmask(SIG_SETMASK, &oset, nullptr); // 恢复旧屏蔽掩码
       }
   }
}

1.4 信号捕获处理

1.预备知识

内核空间通过内核级页表映射到物理内存

通过时钟中断,可以让操作系统获得 CPU 控制权。
**CPU 通过 CS 寄存器的最后两位判断当前状态:00 表示内核态,11 表示用户态。**可以通过 int 0x80 陷入内核态。

2.信号什么时候被处理

  • 当我们进程从内核态返回到用户态的时候,进行信号检测和处理

  • 调用系统调用------操作系统会自动做"身份"切换,即从用户态变成内核态

  • 用户态只能访问用户的代码和数据,内核态同理

下面这个图是简易版本,帮助大家理解

程序从红色出发!

即使代码里没有使用系统调用,仍可能因中断(如时钟中断)或异常而切换到内核态。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

用户程序注册了 SIGQUIT 信号的处理函数 sighandler。当前正在执行 main 函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的 main 函数之前,检查到有信号 SIGQUIT 递达。

内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数。

sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

**sighandler 函数返回后,会自动触发 sigreturn 系统调用再次进入内核态。**如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

1.5 信号捕获函数

1.signal

cpp 复制代码
// 注册信号处理函数(简易接口)
// 当进程收到指定信号时 调用用户定义的处理函数
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

// 参数:
//   signum  - 信号编号(如 SIGINT、SIGTERM)
//   handler - 信号处理函数指针 或特殊值 SIG_IGN / SIG_DFL

// 返回值:
//   成功:返回之前的信号处理函数指针
//   失败:返回 SIG_ERR

// 特殊 handler 值:
//   SIG_IGN - 忽略该信号
//   SIG_DFL - 恢复默认行为(如终止、忽略、暂停等)

// 案例:
    void sigint_handler(int sig) 
    {
        write(2, "Caught SIGINT\n", 14);
        _exit(0);
    }
    signal(SIGINT, sigint_handler);

2.sigaction

cpp 复制代码
// 注册可靠、可移植的信号处理函数(推荐替代 signal)
// 支持精确控制信号掩码、标志和旧处理函数
#include <signal.h>

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

// 参数:
//   signum  - 信号编号(如 SIGINT、SIGTERM,不能为 SIGKILL 或 SIGSTOP)
//   act     - 指向新处理配置(若为 NULL 则不修改)
//   oldact  - 若非 NULL 则保存原有配置

// 返回值:
//   成功:返回 0
//   失败:返回 -1 并设置 errno

// struct sigaction 关键成员:
//   sa_handler - 处理函数指针(void (*)(int))或 SIG_IGN / SIG_DFL
//   sa_mask    - 调用处理函数时额外屏蔽的信号集
//   sa_flags   - 行为标志(如 SA_RESTART、SA_RESETHAND)

// 案例:
    void handler(int sig) 
    {
        write(STDERR_FILENO, "Signal received\n", 16);
        _exit(0);
    }

    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

sigaction的处理状态

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字 ,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号 ,则用 sa_mask 字段说明这些需要额外屏蔽的信号 ,当信号处理函数返回时自动恢复原来的信号屏蔽字。

二.可重入函数

概念:可重入函数与不可重入函数,本质是函数的一种特点即被多执行流执行是否会出现问题

当main中调用insert方法时,执行流(main)进入insert里面经行调用,如果这时候出现异常了那么会产生信号进入sighandller,然后执行流(sighandller)进入insert里面进行调用,当调用结束后返回sighandller再返回mian执行流进入insert的那次然后继续调用。然后就出现无指针指向node2从而内存泄漏发生问题

三.volatile

作用:让该变量内存可见

为什么要有这个呢,因为再C与C++中当一个变量只做判断的时候,它可能被优化,原先CPU应该是去内存访问但是优化后它会把变量放到寄存器中这样就能更快访问,但是如果此时这个变量修改后CPU还是读取该变量之前的值,而这个就叫内存不可见。

本章重点主要在第一模块,至于第二模块,大家重点记忆的就是可重入函数与不可重入函数区别,然后再那个图片其实我们会接触到一个原子性的问题,那关于什么是原子性,就留给后面的章节带着大家了解了。

相关推荐
UIU11434 分钟前
递归算法与汉诺塔
c++·学习·算法·c#·递归
律宏阔6 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔6 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴6 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千6 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei376 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei376 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei376 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW6 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW6 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux