【Linux】25.进程信号(2)

embedded/2025/2/5 4:56:03/

文章目录

  • 4.捕捉信号
    • 4.1 重谈地址空间
    • 4.2 内核如何实现信号的捕捉
    • 4.3 sigaction
    • 4.4 可重入函数
    • 4.5 volatile
    • 4.6 SIGCHLD信号(了解)


4.捕捉信号

4.1 重谈地址空间

fdeb1e02c9a416380118e84ff616ef0a

  1. 用户页表有几份?

    有几个进程,就有几份用户级页表–进程具有独立性

  2. 内核页表有几份?

    1份

  3. 每一个进程看到的3~4GB的东西都是一样的。整个系统中进程再怎么切换,3,4GB的空间的内容是不变的。

  4. 进程视角:我们调用系统中的方法,就是在我自己的地址空间中进行执行的。

  5. 操作系统视角:任何一个时刻,都有有进程执行。我们想执行操作系统的代码,就可以随时执行。

  6. 操作系统的本质:基于时钟中断的一个死循环。

  7. 计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断


4.2 内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandlermain函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

92c6c6fb5d1b186954273d41583c30a1


4.3 sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。actoact指向sigaction结构体。

  • sa_handler赋值为常数SIG_IGN,传给sigaction表示忽略信号。赋值为常数SIG_DFL,表示执行系统默认动作。赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

sigaction()函数中的两个指针参数 *act*oldact 有不同的用途:

  1. *act (新动作):

    • 指向要设置的新的信号处理方式

    • 如果不为NULL,系统会按照这个结构体设置新的信号处理方式

    • 用于指定我们"想要"的信号处理方式

  2. *oldact (旧动作):

    • 用于保存信号的原有处理方式

    • 如果不为NULL,系统会将原来的信号处理方式保存在这个结构体中

    • 常用于之后恢复原有的信号处理方式

示例:

struct sigaction new_action, old_action;// 设置新的处理方式
new_action.sa_handler = my_handler;    // 设置处理函数
sigemptyset(&new_action.sa_mask);      // 清空信号掩码
new_action.sa_flags = 0;               // 设置标志// 设置SIGINT的处理方式,同时保存原有设置
sigaction(SIGINT, &new_action, &old_action);// ... 一段时间后 ...// 恢复原有的处理方式
sigaction(SIGINT, &old_action, NULL);

注意:

  • 如果只想设置新的处理方式,可以将oldact设为NULL
  • 如果只想查询当前的处理方式,可以将act设为NULLoldact指向一个结构体

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

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

代码:

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;// 信号处理函数(被注释掉的版本)
// 不是必须调用wait,但建议调用以避免僵尸进程
// void handler(int signo)
// {
//     sleep(5);  // 模拟信号处理需要一定时间
//     pid_t rid;
//     // WNOHANG: 非阻塞等待,如果没有子进程退出立即返回0
//     while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
//     {
//         cout << "I am proccess: " << getpid() << " catch a signo: " << signo 
//              << "child process quit: " << rid << endl;
//     }
// }int main()
{// 忽略SIGCHLD信号(17),避免产生僵尸进程// SIG_DFL是默认处理方式,SIG_IGN是忽略信号signal(17, SIG_IGN); // 创建10个子进程for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0)  // 子进程{while (true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!" << endl;exit(0);  // 子进程退出}sleep(1);  // 父进程每隔1秒创建一个子进程}// 父进程循环while (true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}// 以下是被注释的其他示例代码:// volatile关键字示例
// volatile int flag = 0;  // volatile防止编译器优化// void handler(int signo)
// {
//     cout << "catch a signal: " << signo << endl;
//     flag = 1;
// }// int main()
// {
//     signal(2, handler);  // 设置SIGINT(Ctrl+C)的处理函数
//     // flag可能被优化到CPU寄存器中,volatile防止这种优化
//     while(!flag);  // flag为0时循环继续//     cout << "process quit normal" << endl;
//     return 0;
// }// 信号pending示例
// pending位图从1变为0的时机:在执行信号处理函数之前清零
// 处理信号时会将该信号添加到block表中,防止信号处理函数被重入// 打印当前进程的pending信号集
// void PrintPending()
// {
//     sigset_t set;
//     sigpending(&set);  // 获取当前pending的信号集//     // 打印1-31号信号的pending状态
//     for (int signo = 1; signo <= 31; signo++)
//     {
//         if (sigismember(&set, signo))
//             cout << "1";
//         else
//             cout << "0";
//     }
//     cout << "\n";
// }// void handler(int signo)
// {
//     cout << "catch a signal, signal number : " << signo << endl;
//     while (true)
//     {
//         PrintPending();
//         sleep(1);
//     }
// }// sigaction使用示例
// int main()
// {
//     // struct sigaction act, oact;
//     // memset(&act, 0, sizeof(act));
//     // memset(&oact, 0, sizeof(oact));//     // sigemptyset(&act.sa_mask);  // 清空信号屏蔽字
//     // sigaddset(&act.sa_mask, 1); // 添加要屏蔽的信号
//     // sigaddset(&act.sa_mask, 3);
//     // sigaddset(&act.sa_mask, 4);
//     // act.sa_handler = handler;    // 设置信号处理函数
//     // sigaction(2, &act, &oact);   // 设置SIGINT的处理方式//     // while (true)
//     // {
//     //     cout << "I am a process: " << getpid() << endl;
//     //     sleep(1);
//     // }//     return 0;
// }

这段代码主要演示了:

  1. 信号处理和僵尸进程避免
  2. 进程创建和父子进程通信
  3. volatile关键字的使用
  4. 信号的pending机制
  5. sigaction的使用方法

4.4 可重入函数

2e6f9bb11fd6b3bc5cf95107300f3e9b

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

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

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

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

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

不可重入函数特征:

  1. 使用静态或全局变量
  2. 返回指向静态变量的指针
  3. 调用malloc/free
  4. 调用不可重入的系统函数

可重入函数特征:

  1. 仅使用局部变量
  2. 数据通过参数传递
  3. 不调用不可重入函数
  4. 不依赖共享资源

4.5 volatile

  1. 基本作用:
// 没有volatile的问题
int flag = 0;
while (!flag) {// 编译器可能优化为死循环// 因为编译器认为没人修改flag
}// 使用volatile解决
volatile int flag = 0;
while (!flag) {// 每次都会从内存重新读取flag// 而不是使用寄存器中的值
}

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

  1. 常见场景:
// 1. 信号处理
volatile sig_atomic_t signal_flag = 0;void signal_handler(int signo) {signal_flag = 1;  // 修改共享变量
}int main() {signal(SIGINT, signal_handler);while (!signal_flag) {// 等待信号处理程序修改flag}
}// 2. 硬件寄存器访问
volatile uint32_t* hardware_reg = (uint32_t*)0x20000000;
*hardware_reg = 0x1;  // 每次都直接写入硬件
  1. volatile的特性:
volatile int counter = 0;void example() {// 1. 防止优化删除counter++;  // 不会被优化掉// 2. 保证读写顺序int temp = counter;  // 确保在counter++之后读取// 3. 每次都访问内存for (int i = 0; i < 10; i++) {counter++;  // 每次都读写内存}
}
  1. volatile的局限:
// volatile不能保证原子性
volatile int shared = 0;// 多线程访问时仍需要互斥锁
mutex mtx;
void thread_func() {lock_guard<mutex> lock(mtx);shared++;
}

主要作用:

  1. 防止编译器优化
  2. 保证每次都从内存读取
  3. 保证代码执行顺序
  4. 用于多线程共享或硬件访问

不能做到:

  1. 不保证原子性
  2. 不保证线程安全
  3. 不是线程同步工具

4.6 SIGCHLD信号(了解)

之前讲过用waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。


http://www.ppmy.cn/embedded/159660.html

相关文章

在Qt中,slots 关键字有什么用?

有下面的Qt代码&#xff1a; #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent nullptr…

汽车自动驾驶AI

汽车自动驾驶AI是当前汽车技术领域的前沿方向&#xff0c;以下是关于汽车自动驾驶AI的详细介绍&#xff1a; 技术原理 感知系统&#xff1a;自动驾驶汽车通过多种传感器&#xff08;如激光雷达、摄像头、雷达、超声波传感器等&#xff09;收集周围环境的信息。AI算法对这些传感…

LeetCode 344: 反转字符串

LeetCode 344: 反转字符串 - C语言题解 这道题的目标是反转一个字符数组&#xff08;字符串&#xff09;。我们将通过双指针法来实现这一功能。 代码实现 #include <stdio.h>void reverseString(char* s, int sSize) {int left 0, right sSize - 1; // 定义左右指针…

技术架构师成长路线(2025版)

目录 通用知识 计算机原理&#xff08;1 - 2 个月&#xff09; 数据结构&#xff08;2 - 3 个月&#xff09; 网络编程&#xff08;1 - 2 个月&#xff09; 软件工程&#xff08;1 个月&#xff09; 基础知识 Java 编程语言基础&#xff08;2 - 3 个月&#xff09; JVM&…

二叉树的最大深度(遍历思想+分解思想)

Problem: 104. 二叉树的最大深度 文章目录 题目描述思路复杂度Code 题目描述 思路 遍历思想(实则二叉树的先序遍历) 1.欲望求出最大的深度&#xff0c;先可以记录一个变量res&#xff0c;同时记录每次当前节点所在的层数depth 2.在递的过程中&#xff0c;每次递一层&#xff0…

QT:多窗口设计(主窗口点击按钮打开子窗口)

目录 一、新建QT工程 二、添加新文件 三、mainwindow.h部分 四、mainwindow.ui部分 五、mainwindow.cpp部分 六、效果演示 七、改进与完善 子窗口设计后来发现有一个更简单的方法实现&#xff08;用QDialog实现&#xff09;&#xff1a;传送门 一、新建QT工程 新建一个…

使用IDEA社区版搭建Springboot、jsp开发环境

1&#xff0c;感觉传统的JSP可以放弃&#xff0c;直接前端JS后端就可以了 2&#xff0c;建议不要用低版本的springboot&#xff0c;版本兼容搭配太麻烦 如果仅仅是搭建springboot开发环境&#xff0c;没什么难度&#xff0c;本文主要记录以下几个问题的解决&#xff1a; 1&…

DeepSeek-R1 论文. Reinforcement Learning 通过强化学习激励大型语言模型的推理能力

论文链接&#xff1a; [2501.12948] DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning 实在太长&#xff0c;自行扔到 Model 里&#xff0c;去翻译去提问吧。 工作原理&#xff1a; 主要技术&#xff0c;就是训练出一些专有用途小模型&…