当前位置: 首页 > article >正文

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

文章目录

  • 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.kler.cn/a/531653.html

相关文章:

  • 【数据分析】案例04:豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask)
  • 金山打字游戏2010绿色版,Win7-11可用DxWnd完美运行
  • 重新刷题求职2-DAY1
  • Spring Cloud工程搭建
  • csapp笔记3.6节——控制(1)
  • 截止到2025年2月1日,Linux的Wayland还有哪些问题是需要解决的?
  • 语言月赛 202412【正在联系教练退赛】题解(AC)
  • 电动汽车常见概念
  • e2studio开发RA2E1(5)----GPIO输入检测
  • Deepseek 数据蒸馏、芯片禁售引发中美AI 之战
  • 嵌入式学习---蜂鸣器篇
  • LeetCode:53.最大子序和
  • 数据 类型
  • 【LeetCode 刷题】回溯算法(3)-子集问题
  • 基于脉冲响应不变法的IIR滤波器设计与MATLAB实现
  • 10.8 LangChain Output Parsers终极指南:从JSON解析到流式处理的规范化输出实践
  • 【R语言】环境空间
  • 【最后203篇系列】006 -使用ollama运行deepseek-r1前后端搭建
  • Java中的常见对象类型解析
  • 想学习Python编程,应该如何去学习呢
  • ChatGPT怎么回事?
  • Linux环境下的Java项目部署技巧:Nginx 详解
  • powershell编写一个简易的http服务器httpServer
  • 《基于deepseek R1开源大模型的电子数据取证技术发展研究》
  • 计算机组成原理——存储系统(二)
  • 大一计算机的自学总结:数据结构设计相关题