轻松Linux-10.进程信号

国庆来啦!

[1. 进程信号序言](#1. 进程信号序言)

[1.1 进程信号的认识](#1.1 进程信号的认识)

[1.2 进程的概念](#1.2 进程的概念)

[1.2.1 标准信号](#1.2.1 标准信号)

[1.2.2 实时信号](#1.2.2 实时信号)

[1.2.3 注意事项](#1.2.3 注意事项)

[2. 信号产生的条件](#2. 信号产生的条件)

[2.1 信号与中断](#2.1 信号与中断)

[2.1.1 硬件中断(简介)](#2.1.1 硬件中断(简介))

[2.1.2 信号处理流程](#2.1.2 信号处理流程)

[2.1.3 硬件中断与信号对比](#2.1.3 硬件中断与信号对比)

[2.2 系统中函数产生的信号](#2.2 系统中函数产生的信号)

[2.2.1 终端按键产生的信号](#2.2.1 终端按键产生的信号)

[2.2.2 系统函数发送信号](#2.2.2 系统函数发送信号)

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

[2.2.4 硬件产生的信号](#2.2.4 硬件产生的信号)

[2.2.5 Core Dump(简介)​编辑](#2.2.5 Core Dump(简介)编辑)

[3. 信号的保存](#3. 信号的保存)

[3.1 信号在内核中的保存](#3.1 信号在内核中的保存)

[3.1.1 信号的保存机制](#3.1.1 信号的保存机制)

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

[3.2.1 常规函数](#3.2.1 常规函数)

[3.2.2 sigprocmask函数](#3.2.2 sigprocmask函数)

[3.2.3 sigpending函数](#3.2.3 sigpending函数)

[4. 信号的捕获](#4. 信号的捕获)

[4.1 信号捕获的流程​编辑](#4.1 信号捕获的流程编辑)

[4.2 sigaction函数](#4.2 sigaction函数)

[4.3 中断下操作系统](#4.3 中断下操作系统)

4.3.1硬件中断相关

[4.3.2 时钟中断与CPU主频](#4.3.2 时钟中断与CPU主频)

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

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


1. 进程信号序言

1.1 进程信号的认识

信号在我们生活中可以说是无处不在,我们几乎身边的每一种信息都是一种信号,早晨的阳光传递了天亮的信号,蜻蜓低飞是雨前雨后的信号,新生开学代表新学年的信号......对于这些信号,我们几乎可以瞬间解读,这是为什么呢?因为我们经历过,有相关的知识储备,所以我们可以做出响应。

对于计算机中的信号,我们可以类比,计算机中的进程也可以发出和响应信号,因此我们可以知道计算机中的进程是已经具备处理信号的能力 的。所以我们可以得出一个小结论:即,信号是一种进程间事件异步通知的机制

进程之所以可以识别信号,因为内核程序员设计的内置特性,并且信号都是在合适的时候进行处理的。

1.2 进程的概念

在linux中的信号,使用kill -l可以看到linux给出的信号,共有64个,其中编号从34开始的信号,都是实时信号,另外1 - 31的是标准信号。信号上面提到是进程之间的事件异步通知的一种机制,它也属于软中断。

1.2.1 标准信号

标准信号 ,即1 - 31编号的信号,它们是有预定义行为的,比如我们平时用的ctrl + c就是上面的SIGINT(2号)信号,它的作用就是中止进程。

标准信号 也有一些缺点

  • 信号丢失:短时间内多次触发同一信号,可能仅记录一次(无排队机制)。

  • 竞态条件 :信号处理函数可能被其他信号中断,导致复杂逻辑出错。

  • 数据传递限制:无法像实时信号那样附带额外数据,仅能通过全局变量或 共享内存 传递信息。

  • 优先级固定:所有标准信号的优先级相同,无法区分紧急程度。

cpp 复制代码
捕获标准信号的例子

#include <stdio.h>
#include <signal.h>
#include <unistd.h>


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

    //如果接收到的是SIGINT信号
    if (sig == SIGINT) 
    {
        printf("Ctrl+C pressed, exiting...\n");

        _exit(0);
    }
}



int main() {
    // 设置信号处理函数
    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    printf("PID: %d, waiting for signals...\n", getpid());

    while (1) 
    {
        pause(); // 等待信号
    }
    return 0;
}

signal函数介绍

用于自定义信号的处理函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

第一个参数为信号编号,第二个参数为SIG_IGN(忽略信号的宏,设置该参数则进程忽略前面设置的信号)
或自定义的void函数,其中这个void函数要带一个int参数

我们可以在shell中使用kill + -(信号编号) + 进程pid 来使用这些信号。

1.2.2 实时信号

在Linux中,实时信号 (34 - 64)属于POSIX标准定义中的特殊信号,它旨在满足实时系统对事件通知的高可靠性、确定性以及高优先级处理需求。所以它有以下特征:

  • 可靠性、有序性:它支持排队机制,即使系统繁忙,发送的实时信号也可以按顺序进行处理,确保多次触发信号不会丢失。

  • 优先级:实时信号有明确的优先级,其优先级比标准信号高,可以通过 sigaction 函数进行设定。

  • 可携带附加数据:可通过 sigqueue 函数发送信号时附带额外信息(如整型或指针),接收进程通过 siginfo_t 结构体获取数据,实现更复杂的 进程间通信

  • 支持自定义处理:支持通过 sigaction 指定自定义处理函数,而标准信号的处理函数选择有限。

cpp 复制代码
发送实时信号并携带数据的例子
#include <signal.h>
#include <unistd.h>
 
int main() {
    pid_t pid = 1234; // 目标进程ID
    int sig_num = SIGRTMIN + 1; // 选择实时信号编号
    union sigval val;
    val.sival_int = 12345; // 附加整型数据
    if (sigqueue(pid, sig_num, val) == -1) {
        perror("sigqueue failed");
        return 1;
    }
    printf("Signal sent successfully\n");
    return 0;
}
cpp 复制代码
在siginfo_t这个结构体中,通过这个union联合体来实现附加数据的携带。
union {
    // ... 其他成员 ...

    // kill() 信号的附加数据 
    struct {
        pid_t _pid;    // 发送者的 pid 
        __ARCH_SI_UID_T _uid; // 发送者的 uid
    } _kill;

    // POSIX.1b 定时器信号的附加数据 
    struct {
        timer_t _tid;   // 定时器 ID 
        int _overrun;   // 超限计数 
        // ... 其他字段 ...
    } _timer;

    // POSIX.1b 实时信号的附加数据 
    struct {
        pid_t _pid;    // 发送者的 pid 
        __ARCH_SI_UID_T _uid; // 发送者的 uid 
        sigval_t _sigval; // 信号值 
    } _rt;

    // SIGCHLD 信号的附加数据 
    struct {
        pid_t _pid;    // 子进程的 pid 
        __ARCH_SI_UID_T _uid; // 发送者的 uid 
        int _status;   // 退出状态 
        clock_t _utime; // 用户 CPU 时间 
        clock_t _stime; // 系统 CPU 时间 
    } _sigchld;

    /* SIGSEGV 等信号的附加数据 */
    struct {
        void _addr; // 引发故障的指令/内存地址 
#ifdef __ARCH_SI_TRAPNO
        int _trapno; // 导致信号的中断号/陷阱号 
#endif
    } _sigfault;

    // SIGPOLL 信号的附加数据 
    struct {
        __ARCH_SI_BAND_T _band; // 轮询带 
        int _fd; // 文件描述符 
    } _sigpoll;
} _sifields;

1.2.3 注意事项

  • 在上面提到的 signal 函数只是定义了对特定信号的捕获后续动作,实际上并没有直接去发送或触发信号,若后续没有接收到相关信号,则不会触发捕获后的动作。

  • 在shell中是有一个前台进程和n个后台进程的,我们使用的ctrl + c,实际是发送信号给前台进程,如果是后台进程则无法生效,要使用kill函数来操作。

  • 上面发送信号与进程接收信号的过程也是异步的,进程在运行时,接收到了我们发送的信号才会进行后续处理,而不是在等待我们发送信号。


2. 信号产生的条件

2.1 信号与中断

在我们认识了信号之后,就要来学习一下信号是如何产生的了。说到信号,就不得不说一说中断了,信号这个概念是从软件角度来理解的,信号真正做的其实是在模拟硬件中断。只是硬件中断是向CPU发的,而信号是给进程发的。虽然它们相似,但它们的层次不同。

2.1.1 硬件中断(简介)

什么是硬件中断呢,硬件中断就是外部硬件设备(键盘、硬盘等的硬件设备)和内部硬件条件(内存检验错误等等)触发的异步信号。当外部设备向CPU发送要处理的事件时(比如键盘输入),这个外部硬件就需要向CPU发送中断信号,这个中断信号会通过中断控制器(如APIC、NVIC)向CPU发送中断请求,CPU在接收到中断信号后,会先暂停目前的工作,去执行中断服务程序(ISR)。

处理流程

  1. **中断触发:**设备通过中断控制器向CPU发送请求。

  2. **保存上下文:**CPU自动保存当前任务的寄存器状态(如程序计数器PC、状态寄存器PSR)到栈中。

  3. **跳转执行:**CPU根据中断向量表找到对应ISR的地址,并跳转到该地址执行。

  4. **中断服务:**ISR读取设备状态寄存器,执行必要操作(如读取键盘输入、处理网络数据包)。

  5. **清除标志位:**在ISR中清除触发中断的硬件标志位,防止重复触发。

  6. 恢复上下文: CPU从栈中恢复之前保存的寄存器状态,继续执行被中断的主程序。

2.1.2 信号处理流程

  1. **信号发送:**通过kill命令、系统调用(如raise)或硬件中断(如除零错误)发送信号。

  2. **信号登记:**内核将信号登记到目标进程的信号域中。

  3. **预置处理方式:**进程可通过signal()或sigaction()预置对信号的处理方式(如忽略、终止、执行自定义函数)。

  4. **信号处理:**当进程从核心态(后面会讲,先用着这个名词)返回用户态时,内核检查是否有信号到达。若有,则暂停当前执行,转去执行信号处理程序。

  5. **恢复执行:**信号处理程序执行完毕后,进程返回到原来的断点继续执行。

2.1.3 硬件中断与信号对比

|------|-------------------|-----------------|
| 维度 | 硬件中断 | 信号 |
| 触发源 | 外部硬件设备或内部硬件条件 | 硬件中断、软件条件、用户命令 |
| 触发方式 | 异步,由硬件主动发起 | 异步,但触发源更广泛 |
| 处理程序 | 核心态下运行 | 用户态下运行 |
| 响应速度 | 实时响应,延迟低 | 通常有较大延迟 |
| 典型应用 | 键盘输入、网络数据包接收、时钟中断 | 进程终止、异常处理、进程间通信 |

其实硬件中断和信号是互补的

  • 硬件中断也需要触发信号,比如在键盘按下的时候,硬件中断通知CPU读取按键信息,CPU再通过信号将按键信息传递给当前的终端进程。

  • 同时信号处理也需要依赖硬件中断,比如在使用系统调用(软中断)的时候,需要通过中断机制来陷入内核,执行系统调用服务。

  • Linux也将硬件处理分为上、下两个部分------上部分:用于快速处理关键操作;下部分:用于软中断延时处理复杂任务,比如网络协议栈的NET_RX_SOFTIRQ(Linux 内核中用于处理网络数据包接收的软件中断)。

2.2 系统中函数产生的信号

2.2.1 终端按键产生的信号

几个常见的按键信号:

  • CTRL + C(SIGINT):杀死前端进程。

  • CTRL + Z(SIGSTP):发送停止信号,将前端进程挂起到后端。

  • CTRL + \(SIGQUIT):中止进程,并生成core dump文件(用于事后调试)。

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

void handle(int sig)
{
    if(SIGINT == sig)
    {
        std::cout << "这是SIGINT信号..." << std::endl;
    }
    else if(SIGQUIT == sig)
    {
        std::cout << "这是SIGQUIT信号..." << std::endl;
    }
    else if(SIGTSTP == sig)
    {
        std::cout << "这是SIGSTP信号..." << std::endl;
        std::cout << "5s后退出..." << std::endl;
        sleep(5);
        exit(0);
    }

}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGQUIT, handle);
    signal(SIGINT, handle);
    signal(SIGTSTP, handle);

    while(true)
    {
        std::cout << "等待信号中..." << std::endl;
        sleep(1);
    }

    return 0;
}

2.2.2 系统函数发送信号

系统提供的一些函数也可以发送信号。

  • killkill指令在shell上可以使用,它同样是个函数
cpp 复制代码
man 2 手册kill
NAME
       kill - send signal to a process
SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);
cpp 复制代码
自己实现的kill指令myKill.cpp
./myKill -signumber pid

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
        return 1;
    }
    int number = std::stoi(argv[1]+1); // 去掉
    pid_t pid = std::stoi(argv[2]);
    int n = kill(pid, number);
    return n;
}
  • raise:给调用它的进程发送信号(给自己发),作用等同于kill(getpid(), sig);
cpp 复制代码
NAME
       raise - send a signal to the caller
SYNOPSIS
       #include <signal.h>
       int raise(int sig);
RETURN VALUE
       raise() returns 0 on success, and nonzero for failure.
返回0表示成功,非0则调用失败。
  • abort:给当前进程发送信号,使其异常终止。在调用该函数时,会先解除对SIGABRT信号的阻塞,然后再发送SIGABRT信号给当前进程,如果设置了对SIGABRT信号的捕获,则调用自定义的函数。
cpp 复制代码
NAME
       abort - cause abnormal process termination
SYNOPSIS
       #include <stdlib.h>
       void abort(void);

2.2.3 软件条件产生的信号

我们之前提过SIGPIPE信号,它也是由软件条件产生的信号。这里讲一下alarm函数和SIGALRM信号。

  • SIGPIPE的产生:比如进程向一个已经关闭的管道(pipe)或者网络套接字(socket)写入数据时,内核检测到该文件描述符无效的时候,会发送SIGPIPE信号,该信号默认会关闭进程,所以可能会导致服务器退出。

  • alarm函数:alarm函数是一个用于设置定时器的系统调用,它设置的定时器具有进程唯一性(一个进程只能设置一个,与sleep原理类似,不建议与sleep一起使用)、异步性(不会阻塞进程)。

cpp 复制代码
NAME
       alarm - set an alarm clock for delivery of a signal
SYNOPSIS
       #include <unistd.h>
       unsigned int alarm(unsigned int seconds);
参数:
unsigned int seconds:设置秒数,如果设置为0,则取消当前的定时器并返回剩余时间,非0则设置新定时器覆盖之前的定时器。
返回值:
- 如果之前没有设置定时器或已经超时了,会返回0。
- 如果之前有定时器且未超时,则返回剩余秒数。
- 失败时返回-1。

在用alarm函数设置了定时器后,内核会在seconds秒后给进程发送一个SIGALRM信号,SIGALRM的默认动作是结束进程,若想取消定时器,可以使用alarm(0)。

进程既然可以使用定时器,那么定时器在内核中必然有相应的数据结构对它进行管理。

  • **entry:**通过链表结构将多个定时器组织到内核的定时器队列中,便于高效管理。

  • expires 绝对超时时间,值为当前jiffies(系统启动以来的时钟中断次数)加上延迟时间(如jiffies + HZ表示1秒后触发)。

  • function: 回调函数指针,到期时由内核调度执行,参数通过data传递。

  • **data:**用户自定义数据,通常用于向回调函数传递上下文信息(如设备指针)。

  • **base:**指向定时器基址结构,负责定时器的分组存储和到期检查。

**小结一下:**软件条件就是操作系统内、外部软件操作而产生的信号。信号的软件条件就是软件内部状态或特殊的操作触发的信号产生机制。

2.2.4 硬件产生的信号

我们平时比较常见的硬件异常信号就是除0和野指针访问,前者是当前程序执行了除0的操作,导致CPU中的运算单元产生异常,内核将此异常定义为SIGFEP,并将SIGFPE这个信号发送给进程;后者是通过野指针访问了非法内存地址,使MMU(内存管理单元,负责与操作系统共同完成,虚拟内存到物理内存的映射)产生异常,内核将这个异常定义为SIGSEGV,并发送SIGSEGV信号给进程(segement falut)。

2.2.5 Core Dump(简介)

我们前面说到SIGQUIT这个信号在结束进程时会生成core dump。在这个进程异常终止或收到相关信号终止时,可以选择把用户进程空间中的数据保存到磁盘中,其文件名一般为core,所以叫core dump文件。

为什么需要core dump文件呢?我们的进程异常退出时,通常是因为有Bug(越界访问等等),事后可以用调试器查core文件来找错误的原因,这就是Post-mortem Debug(事后调试)。

一般的程序时不允许生成core文件的,因为core文件里可能有隐私相关的信息,这个限制信息在PCB中Resource Limit中保存着。在开发测试阶段,我们可以使用ulimit来修改限制信息,比如修改core文件的大小ulimit -c 数字(kb),我们可以通过ulimit -a来查看。


3. 信号的保存

3.1 信号在内核中的保存

在Linux中,信号的保存和处理 是通过位图 结构信号操作函数 以及内核态到用户态的切换机制实现的。这里侧重于讲标准信号。

3.1.1 信号的保存机制

在内核的task_struct中保存着与信号相关的三张表,它们分别是 block表、pending表、handler表。

  • **block表:**以位图形式存储,记录被阻塞的信号。若信号被阻塞,即使产生也不会递达,直到解除阻塞。

  • pending表: 内核为每个进程维护一个pending位图(如sigpending字段),每个位对应一个信号,信号产生时,对应位被置为1,表示信号处于未决状态。

  • **handler表:**函数指针数组,保存每个信号的处理方法(默认、忽略或自定义)。

cpp 复制代码
struct task_struct {          //Linux 2.6.13内核
.....
/* signal handlers */
    struct signal_struct *signal;
    struct sighand_struct *sighand; //handler表

    sigset_t blocked, real_blocked;  //block表
    struct sigpending pending;       //pending表

    unsigned long sas_ss_sp;
    size_t sas_ss_size;
    int (*notifier)(void *priv);
    void *notifier_data;
    sigset_t *notifier_mask;
    void *security;
    struct audit_context *audit_context;
    seccomp_t seccomp;
.....
};

struct sighand_struct {
    atomic_t count;                  // 引用计数
    struct k_sigaction action[_NSIG]; // 信号处理函数数组(64个标准信号)
    spinlock_t siglock;              // 保护信号操作的自旋锁
};

struct sigpending {
    struct list_head list;
    sigset_t signal;
};

typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;

上面的sigset_t称为信号集,每一个bit位,用于表示有效 或者无效 ,其中阻塞信号集也称作信号屏蔽字(Signal Mask)。对于我们来说,并不需要关心它的实现,只要使用相关的函数操作控制即可。

我们这样来理解block表和pending表之间的运作关系。

  1. 信号发送时会先检查该进程的block表,如果block表中它相关的位被置1(它被阻塞了),那么它不会被立即处理,而是会别标志为未决状态(pending表中相关的位置1)。

  2. 如果发送时是0则直接执行handler表中与它对应的函数。

  3. 阻塞也是动态的,在进程运行时,解除了对该信号的阻塞时,如果它处于未决状态(pending表为1,我更想叫它为待办状态),那么该信号就去执行handler表中对应的函数;如果它并未处于未决状态(之前处理过了或者该信号没有被接收到),则无需处理。

如下图:

  • SIGHUB信号发送给当前进程时,block表为0,直接去执行handler表中对应的函数。对应上面第2点。

  • SIGINT信号发送到当前进程时,block表为1,那么将它设置为未决状态(待办状态)----pending表置1。对应上面第1点。

  • SIGQUIT信号的处理则是,之前被阻塞了,但后面解除了阻塞,但处于未决(待办)状态 ----pending表为1,这是就可以去handler表执行相关的函数了。对应上面的第3点。

如果在进程解除 对某信号的阻塞之前 这种信号产生过多次,将如何处理?

POSIX.1允许系统递送该信号一次或多次。

Linux则是:标准信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

3.2 信号集操作函数

3.2.1 常规函数

对于上面讲到的sigset_t信号集,Linux提供了相关的控制函数。

cpp 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set); //清空信号集,将所有位设为0。
int sigfillset(sigset_t *set);   //填充信号集,将所有位设为1。
int sigaddset(sigset_t *set, int signo);  //将指定信号signo添加到信号集。
int sigdelset(sigset_t *set, int signo);  //将指定信号signo从信号集中移除。
以上返回值都是成功返回0,失败返回-1。
int sigismember(const sigset_t *set, int signo); //检查信号signo是否在信号集中。
该函数返回值,包含返回1,不包含返回0,失败返回-1。

在使用sigset_t类型变量时,最好先使用sigemptyset或者sigfillset函数初始化,一定要让其处于确定的状态,以免出现Bug。

3.2.2 sigprocmask函数

sigprocmask是Linux/POSIX系统中管理进程级信号阻塞的核心函数,调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 参数说明:
  - how:定义操作模式(必选其一):
    - SIG_BLOCK:将set中的信号添加到当前阻塞集(blocked |= set)。
    - SIG_UNBLOCK:将set中的信号移除出当前阻塞集(blocked &= ~set)。
    - SIG_SETMASK:直接设置当前阻塞集为set的值(blocked = set)。
  - set:指向待操作信号集的指针(可为NULL,表示不修改阻塞集)。
  - oldset:用于保存修改前的阻塞集(可为NULL,表示不保存)。
- 返回值:
  - 成功返回0;失败返回-1并设置errno(如EINVAL表示无效how或信号编号)。

底层逻辑:

  1. 用户调用sigprocmask(SIG_BLOCK, &set, NULL)

  2. 内核将set中的信号添加到task_struct->blocked位掩码。

  3. 若被阻塞的信号已处于pending集,则保留;若未处于,则新信号发送时会被加入 pending

看一段代码

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

void handler(int signo) {
    printf("处理信号: %d\n", signo);
}

int main() {
    sigset_t mask, old;
    struct sigaction sa;

    // 注册SIGINT处理函数
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    // 阻塞SIGINT
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigprocmask(SIG_BLOCK, &mask, &old);
    printf("已阻塞SIGINT\n");

    // 发送SIGINT(进入pending集)
    kill(getpid(), SIGINT);
    sleep(1);  // 模拟其他操作

    // 解除阻塞并处理pending信号
    sigprocmask(SIG_UNBLOCK, &mask, NULL);
    printf("已解除阻塞,信号将被处理\n");

    return 0;
}

输出信息:
SigBlk: 0000000000000004  # SIGINT被阻塞(第2位)
SigPnd: 0000000000000004  # SIGINT处于未决状态

3.2.3 sigpending函数

sigpending()是Linux/POSIX系统中查询进程未决信号集的核心函数,,用于获取当前已被触发但尚未处理的信号(阻塞中的待办信号)。

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);
- 参数说明:
  - set:输出型参数,用于存储当前进程的未决信号集
(task_struct->pending或signal_struct->shared_pending的副本)。
  
  - 返回值:成功返回0;失败返回-1并设置errno(如EFAULT表示set为无效地址)。
cpp 复制代码
可拿这段代码去运行一下加深理解
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int signo) {
    sigset_t pending;
    sigpending(&pending);  // 在处理函数中查询未决信号
    if (sigismember(&pending, SIGUSR1)) {
        printf("处理信号 %d 时,SIGUSR1 未决\n", signo);
    }
}

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

    // 阻塞SIGUSR1并发送信号
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);
    sigprocmask(SIG_BLOCK, &mask, NULL);
    kill(getpid(), SIGUSR1);

    // 触发SIGINT处理,内部检查SIGUSR1未决状态
    kill(getpid(), SIGINT);
    return 0;
}

4. 信号的捕获

4.1 信号捕获的流程

3这个步骤的条件是被处理的信号有对应的自定义函数,如果没有,则执行信号的默认动作,在直接走第5步。(在信号递达时发送)

举个例子(信号有自己的自定义函数):

  1. 用户注册(自定义)了SIGINT信号的sighandler函数。

  2. main在执行时遇到了中断或异常,保存上下文,陷入内核态。

  3. 中断处理完毕,检查是否有信号递达,发现有SIGINT信号递达。

  4. 内核切换回到用户态调用SIGINT的自定义的sighandler函数。(main与sighandler是处于不同的堆栈空间的)

  5. sighandler 函数返回后自动执行特殊的系统调用 sigreturn再次进入内核态。

  6. 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。

4.2 sigaction函数

Linux的sigaction函数是POSIX标准定义的信号处理接口,用于精确控制进程对信号的响应行为,相较于传统signal函数具有更高灵活性(直观来看参数更多了)。

cpp 复制代码
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 参数:
  - signum:指定操作的信号(如SIGINT、SIGCHLD),不可为SIGKILL或SIGSTOP(无法捕获/忽略)。
  - act:指向新信号处理配置的结构体指针(新的信号处理动作),可为NULL(表示恢复默认处理)。
  - oldact:用于获取原信号配置的指针(获取原来的信号处理动作),可为NULL(不关心旧配置)。
- 返回值:0成功,-1失败(错误时设置errno)。

必须接上sigaction结构体
struct sigaction {
    union {
        void (*sa_handler)(int);         // 简单处理函数(单参数)
        void (*sa_sigaction)(int, siginfo_t *, void *); // 带参数的实时处理函数
    } __sa_handler;
    sigset_t sa_mask;                   // 处理期间阻塞的信号集合
    int sa_flags;                       // 标志位(如SA_RESTART、SA_SIGINFO)
    void (*sa_restorer)(void);          // 已废弃,无需使用
};

- sa_handler vs sa_sigaction:
  - sa_handler:传统单参数函数(如void handler(int signo)),仅接收信号编号。
  - sa_sigaction:需设置sa_flags |= SA_SIGINFO,可接收siginfo_t结构体(含信号来源、PID等附加信息,前面讲过)和上下文指针,适用于实时信号(如SIGRTMIN)。
- sa_mask:指定在信号处理函数执行期间自动阻塞的信号,防止嵌套信号中断。可以使用上面说的sigaddset函数设置,来屏蔽信号。
- sa_flags:关键标志包括:
  - SA_RESTART:系统调用被信号中断后自动重启(如read/write)。
  - SA_SIGINFO:启用sa_sigaction多参数处理函数。
  - SA_NOCLDWAIT:子进程退出后不成为僵尸进程(需配合SIGCHLD)。

两个使用参考(现在看看就行):

cpp 复制代码
处理子进程退出(SIGCHLD):
void sigchld_handler(int signo) {
    waitpid(-1, NULL, WNOHANG);  // 非阻塞回收子进程
}

struct sigaction sa;
sa.sa_handler = sigchld_handler;
sa.sa_flags = SA_RESTART | SA_NOCLDWAIT;  // 重启系统调用,避免僵尸进程
sigaction(SIGCHLD, &sa, NULL);
cpp 复制代码
实时信号与附加信息(需SA_SIGINFO):
void rt_handler(int signo, siginfo_t *info, void *context) {
    printf("接收信号:%d,从进程(%d)接收\n", signo, info->si_pid);
}

struct sigaction sa;
sa.sa_sigaction = rt_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGRTMIN, &sa, NULL);

想要了解更多相关的信息,可以去查资料,这里不过多赘述。

4.3 中断下操作系统

4.3.1硬件中断相关

在操作系统中还有一张中断向量表,它在系统启动时,就加载到内存中了,通过硬件中断和中断向量表,操作系统就不需要周期性检测中断信号。

cpp 复制代码
//Linux内核0.11源码 ------网上找的资料
void trap_init(void)
{
    int i;
    set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
    set_trap_gate(1,&debug); 
    set_trap_gate(2,&nmi); 
    set_system_gate(3,&int3); /* int3-5 can be called from all */ 
    set_system_gate(4,&overflow); 
    set_system_gate(5,&bounds); 
    set_trap_gate(6,&invalid_op); 
    set_trap_gate(7,&device_not_available); 
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS);  
    set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment);   
    set_trap_gate(13,&general_protection); set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved); set_trap_gate(16,&coprocessor_error); 
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
    for (i=17;i<48;i++) 
         set_trap_gate(i,&reserved); 
    set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。 
    outb_p(inb_p(0x21)&0xfb,0x21);  // 允许主8259A 芯片的IRQ2 中断请求。    
    outb(inb_p(0xA1)&0xdf,0xA1);   // 允许从8259A 芯片的IRQ13 中断请求。 
    set_trap_gate(39,¶llel_interrupt);// 设置并行口的陷阱门。 
}
void rs_init (void) 
{
    set_intr_gate (0x24, rs1_interrupt); // 设置串行口1 的中断门向量(硬件IRQ4 信 号)。 
    set_intr_gate (0x23, rs2_interrupt); // 设置串行口2 的中断门向量(硬件IRQ3 信 号)。 
    init (tty_table[1].read_q.data); // 初始化串行口1(.data 是端口号)。 
    init (tty_table[2].read_q.data); // 初始化串行口2。 
    outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯片的IRQ3,IRQ4 中断信号请求。 
} 

这段代码是Linux 0.11内核中中断初始化(trap_init)和串口初始化(rs_init)的核心部分,很古早了,不需要记住,看一遍理解一下,现代操作系统有更新的机制。

4.3.2 时钟中断与CPU主频

时钟中断:时钟中断是计算机系统中的周期性事件,由硬件定时器(如8254/8259芯片)触发,用于通知CPU执行特定任务(如任务调度、时间维护)。

CPU主频:CPU主频(时钟频率)是CPU内核工作的基准频率,表示每秒产生的时钟脉冲数,单位为Hz(通常以MHz或GHz表示),主频越高,CPU在单位时间内能执行的指令越多,单线程任务(如游戏渲染、科学计算)速度越快。

操作系统:本质是死循环,没错我们的操作系统在开机后就一直运行,直至我们发送关机信号,它才会停止。

cpp 复制代码
void main(void)
{                      //在startup 程序(head.s)中就是这样假设的
    ......
    for (;;) pause(); 
} // end main

这里的pause(),就意味着我们必须接收到信号才会返回就绪运行态。当然任务0除外,因为任务0在任何空闲时间里都会被激活(没有其它任务运行的时候),任务0就意味查看是否有其它任务可运行,没有的话就一直执行pause()

怎么看这个任务0呢?

cpp 复制代码
// Linux 内核0.11    ---bro在网上找的资料
// main.c
sched_init();  // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c)
// 调度程序的初始化子程序。
void sched_init(void)
{
    .......
    set_intr_gate(0x20, &timer_interrupt);
    // 修改中断控制器屏蔽码,允许时钟中断。
    outb(inb_p(0x21) & ~0x01, 0x21); 
    // 设置系统调用中断门。
    set_system_gate(0x80, &system_call);
    ......
}
// system_call.s 
_timer_interrupt:
    ......
;    // do_timer(CPL)执行任务切换、计时等工作,在kernel/shched.c,305 行实现。 
    call _do_timer ;     // 'do_timer(long CPL)' does everything from 
// 调度入口
void do_timer(long cpl) 
{
    ......
    schedule(); 
}

void schedule(void) 
{
    .......
    switch_to(next); // 切换到任务号为next 的任务,并运行之
}

4.3.3 软中断

我们知道操作系统在执行系统调用时,是需要先进入内核态的,那么肯定有相关的指令可以让操作系统进入内核态,CPU设计了相关的汇编指令(int 0x80syscall),可让CPU内部触发中断逻辑。

为什么我们平时没有见到过像int 0x80syscall这样的指令呢?因为被Linux和C函数标准库封装起来了。如果有兴趣的话,可以去看看C标准库的系统调用相关的函数,可以找到。

在系统的中断向量表中,有一个中断服务叫处理软中断,这里操作系统就是操作系统处理系统调用的一个"入口",操作系统根据传入的系统调用号,来查询系统调用对应的执行方法。

这里的系统调用号是一个数组下标。

执行系统调用时,会先执行 int 0x80 或 syscall陷入内核,也就是触发软中断,然后CPU就会自动执 行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。

用户给操作系统传递系统调用号,以及操作系统给用户传递返回值,都是通过相关的寄存器或缓冲区来完成的。

cpp 复制代码
// sys.h        ---bro去网上找到的
// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137) 
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208) 
extern int sys_read (); // 读文件。 (fs/read_write.c, 55) 
extern int sys_write (); // 写文件。 (fs/read_write.c, 83) 
extern int sys_open (); // 打开文件。 (fs/open.c, 138) 
extern int sys_close (); // 关闭文件。 (fs/open.c, 192) 
extern int sys_waitpid (); // 等待进程终止。 (kernel/exit.c, 142) 
extern int sys_creat (); // 创建文件。 (fs/open.c, 187) 
extern int sys_link (); // 创建一个文件的硬连接。 (fs/namei.c, 721) 
extern int sys_unlink (); // 删除一个文件名(或删除文件)。 (fs/namei.c, 663) 
extern int sys_execve (); // 执行程序。 (kernel/system_call.s, 200) 
extern int sys_chdir (); // 更改当前目录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102) 
extern int sys_mknod (); // 建立块/字符特殊文件。 (fs/namei.c, 412) 
extern int sys_chmod (); // 修改文件属性。 (fs/open.c, 105) 
extern int sys_chown (); // 修改文件宿主和所属组。 (fs/open.c, 121) 
extern int sys_break (); // (-kernel/sys.c, 21) 
extern int sys_stat (); // 使用路径名取文件的状态信息。 (fs/stat.c, 36) 
extern int sys_lseek (); // 重新定位读/写文件偏移。 (fs/read_write.c, 25) 
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348) 
extern int sys_mount (); // 安装文件系统。 (fs/super.c, 200) 
extern int sys_umount (); // 卸载文件系统。 (fs/super.c, 167) 
extern int sys_setuid (); // 设置进程用户id。 (kernel/sys.c, 143) 
extern int sys_getuid (); // 取进程用户id。 (kernel/sched.c, 358) 
extern int sys_stime (); // 设置系统时间日期。 (-kernel/sys.c, 148) 
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26) 
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338) 
extern int sys_fstat (); // 使用文件句柄取文件的状态信息。(fs/stat.c, 47) 
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102) 
extern int sys_mknod (); // 建立块/字符特殊文件。 (fs/namei.c, 412) 
extern int sys_chmod (); // 修改文件属性。 (fs/open.c, 105) 
extern int sys_chown (); // 修改文件宿主和所属组。 (fs/open.c, 121) 
extern int sys_break (); // (-kernel/sys.c, 21) 
extern int sys_stat (); // 使用路径名取文件的状态信息。 (fs/stat.c, 36) 
extern int sys_lseek (); // 重新定位读/写文件偏移。 (fs/read_write.c, 25) 
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348) 
extern int sys_mount (); // 安装文件系统。 (fs/super.c, 200) 
extern int sys_umount (); // 卸载文件系统。 (fs/super.c, 167) 
extern int sys_setuid (); // 设置进程用户id。 (kernel/sys.c, 143) 
extern int sys_getuid (); // 取进程用户id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间日期。 (-kernel/sys.c, 148) 
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26) 
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338) 
extern int sys_fstat (); // 使用文件句柄取文件的状态信息。(fs/stat.c, 47) 
extern int sys_ulimit (); // (-kernel/sys.c, 97) 
extern int sys_uname (); // 显示系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认文件创建属性码。 (kernel/sys.c, 230) 
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90) 
extern int sys_ustat (); // 取文件系统信息。 (fs/open.c, 19) 
extern int sys_dup2 (); // 复制文件句柄。 (fs/fcntl.c, 36) 
extern int sys_getppid (); // 取父进程id。 (kernel/sched.c, 353) 
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201) 
extern int sys_setsid (); // 在新会话中运行程序。 (kernel/sys.c, 206) 
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63) 
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15) 
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20) 
extern int sys_setreuid (); // 设置真实与/或有效用户id。 (kernel/sys.c,118) 
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51) 

// 系统调用函数指针表。用于系统调用中断处理程序(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 
};
// 调度程序的初始化子程序。 
void sched_init(void) 
{ 
    ... 
    // 设置系统调用中断门。 
    set_system_gate(0x80, &system_call); 
} 
cpp 复制代码
void trap_init(void) 
{ 
    int i; 
    set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。  
    set_trap_gate(1,&debug); 
    set_trap_gate(2,&nmi); 
    set_system_gate(3,&int3); /* int3-5 can be called from all */ 
    set_system_gate(4,&overflow);   //溢出错误
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);   //非法行为
    set_trap_gate(7,&device_not_available); 
    set_trap_gate(8,&double_fault); //双重错误(两次异常错误)
    set_trap_gate(9,&coprocessor_segment_overrun); 
    set_trap_gate(10,&invalid_TSS); 
    set_trap_gate(11,&segment_not_present); 
    set_trap_gate(12,&stack_segment); 
    set_trap_gate(13,&general_protection); 
    set_trap_gate(14,&page_fault);   //缺页中断
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error); 
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。 
    for (i=17;i<48;i++) 
        set_trap_gate(i,&reserved); 
    set_trap_gate(45,&irq13); // 设置协处理器的陷阱门。 
    outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。 
    outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。  
    set_trap_gate(39,&parallel_interrupt);// 设置并行口的陷阱门。 
}

缺页中断、多重错误、内存碎片处理、除零野指针错误等等的问题,全部都会被转换成为CPU内部的软中断, 然后走中断处理例程,完成所有处理。

4.4 内核态和用户态

x86架构中,CPU分为4个特权级(Ring 0-3),内核态通常运行在Ring 0,用户态在Ring 3。

系统调用是在进程地址空间内的。⽤⼾态就是执⾏⽤⼾0 - 3GB时所处的状态,内核态就是执⾏内核3 - 4GB时所处的状态,它们通过CPL来区分------全称是CurrentPrivilegeLevel,即当前特权级别。

在执行软中断(int &0x80syscall)时,CPL会在校验后自动切换。


相关推荐
用户31187945592182 小时前
libopenssl1_0_0-1.0.2p-3.49.1.x86_64安装教程(RPM包手动安装步骤+依赖解决附安装包下载)
linux
不爱学习的老登2 小时前
从零开始搭建私有服务器并部署网站
运维·服务器
驱动探索者2 小时前
linux 学习平台 arm+x86 搭建
linux·arm开发·学习
深思慎考2 小时前
【新版】Elasticsearch 8.15.2 完整安装流程(Linux国内镜像提速版)
java·linux·c++·elasticsearch·jenkins·框架
微电子爱好者2 小时前
TCP和UDP调试工具的介绍和使用
linux·tcp/ip·udp
mxpan2 小时前
VirtualBox中ubuntu1804虚拟机共享文件夹设置
linux·运维·服务器
别多香了2 小时前
项目实战:ecshop
linux·运维·服务器
来碗原味的小米粥吧3 小时前
sql题目基础50题
linux·数据库·sql
de之梦-御风4 小时前
【Linux】 MediaMTX测试是否运行
linux·运维·服务器