Linux信号_信号的保存_信号捕捉

我们知道向进程发送信号,进程并不是立即处理,而是等合适的时机进行处理。那么就需要保存信号。在信号的产生中说过信号保存在进程PCB里面的信号位图里,那信号位图到底是什么?

一.信号保存

我们先补充一些概念

1.阻塞 忽略概念

实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。(只把信号保存,但还没有进行处理)
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态(保存信号,但不让处理),直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略(处理信号,但行为就是忽略)是在递达之后可选的一种处理动作。

2.block pending信号集 handler信号处理器表

我们之前说的信号位图就是下面三个表中的pending表,那这三个表有什么用呢?

1.pending位图,当前进程收到的信号列表。bit位的位置表示信号编号,1/0表示是否收到信号。

2.block位图,表示哪些信号正在被阻塞。bit位的位置表示信号编号,1/0表示该信号是否被阻塞(如果被阻塞,在pending表中对应信号即使为1,也不会对信号进行处理,等到阻塞消失,才会完成消息递达)。

3.handler信号处理表(函数指针数组),表示对应信号要进行的行为(可以是系统默认的,也可以是signal()函数自定义行为)。信号编号-1就是要执行动作函数指针的下标

sigset_t 信号集

我们知道block pending表是位图,但他们的类型是什么?是int吗?

其实它们的类型是一个结构体sigset_t

#include <signal.h>

typedef struct {
    unsigned long __val[2];  // 通常是一个长度为 2 的 unsigned long 数组
} sigset_t;

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态.

阻塞信号集(block)也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

3.信号集操作函数

我们知道block pending表是位图,但不建议直接用位操作来更改bit位。而是用信号集操作函数来实现。

增删查改

#include <signal.h>
int sigemptyset(sigset_t *set); //将set位图bit位全置为0
int sigfillset(sigset_t *set);  //将set位图bit位全置为1
int sigaddset (sigset_t *set, int signo); //向信号集中添加一个信号,下标signo-1置1
int sigdelset(sigset_t *set, int signo);  //从信号集中删除一个信号,下标signo-1置0
//这四个函数都是成功返回0,出错返回-1。
int sigismember(const sigset_t *set, int signo); //查找signo信号是否属于给定的信号集。
//如果信号在集合中,返回 1;如果不在集合中,返回 0;如果出错,返回 -1。

sigprocmask 更改block

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

1.int how 如何更改

2.set 指向需要操作的信号集。

3.oset 用于存储原来的信号屏蔽字(可选)。

成功时返回 0,失败时返回 -1。

eg.

sigset_t set, oldset;
sigemptyset(&set);//初始化信号集 bit位全置0
sigaddset(&set, SIGINT);

// 在内核PCB中block中 阻塞 SIGINT 信号
sigprocmask(SIG_BLOCK, &set, &oldset);

// 恢复屏蔽字
sigprocmask(SIG_SETMASK, &oldset, NULL);

sigpending 读取未决信号集(pending)

#include <signal.h>

int sigpending(sigset_t *set);

set: 指向 sigset_t 类型的变量,用于存储当前进程的未决信号集。调用成功后,该变量将包含当前进程未决信号的集合。

如果调用成功,返回 0。出错,返回 -1,并将 errno 设置为具体的错误值。

补充:操作系统是如何运行的

1.硬件中断

当我们用键盘输入信息,操作系统怎么知道键盘要输入信息的?又是怎么知道其它外设有资源要处理呢?

1.中断触发。当外设准备好时,就会发起中断每一个外设都对应一个中断号。(eg.键盘输入时会触发中断号1)

2.保存上下文。收到中断请求时,CPU会保护现场,暂停当前的程序执行,保存当前的执行状态(即程序计数器、寄存器等)。

3.查找中断向量。根据中断号,操作系统查找中断向量表,获取对应的中断处理程序地址,并执行对应方法。

4.恢复现场:中断处理完成后,恢复先前的执行状态(程序计数器、寄存器等),并继续执行被中断的程序。

2.时钟中断

现在我们知道了每当外设有资源要处理时,会通过中断的方式让CPU进行处理。但这和操作系统运行有什么关系呢?

其实有一个硬件时钟源,它会每隔很短的时间向操作系统发送中断,所以操作系统就会根据它的中断号来查找中断向量表,执行它对应的方法。但时钟源对应的中断服务就是进程调度这样操作系统,就可以在硬件时钟的推动下,自动调度了。

因为时钟源会频繁向系统发送中断,这样会占用大量中断控制器资源,降低响应速度。所以一般把时钟源集成到CPU内部减少中断传播延迟。

时钟源发送中断,引起的中断服务:进程调度 并不意味着要进行进程切换。

比如说执行一个进程的时间片1s int count=1000,时钟源每隔1微秒中断一次,count--。当count==0时就意味着时间片耗尽,要切换下一个进程。

3.软件中断

上面都是因为硬件触发的中断,有没有因为软件来触发中断的?

eg.1.系统调用 为了让操作系统支持系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),让CPU在内部触发中断逻辑。

2.缺页中断 /0 野指针操作

1.陷阱:系统调用

int 0x80 是一种在 x86 架构(尤其是 32 位系统)中触发软件中断的指令,常用于执行 系统调用(system call)。

int 触发一个中断,后面加中断号

0x80 作为中断号,在 32 位 x86 系统中约定为触发 系统调用的入口。

1.int 0x80,触发软件中断。系统根据后面0x80中断号,在中断向量表中找到对应的处理程序。

2.再根据系统调用号作为下标查找系统调用表中的对应函数指针。

3.返回函数执行结果。

系统调用号哪来的?寄存器EAX中

在系统调用的过程中,把要调用的系统调用号写入寄存器EAX中

(系统调用参数一般也是通过寄存器传的  返回值通常存放在寄存器中,如 EAX(32位架构)或 RAX(64位架构))

所以系统调用也是通过中断完成的

由此看来,Linux内核提供的系统调用接口,不是C语言,而是系统调用号+传递参数 返回值寄存器 +int 0x80 / syscall实现的。

我们平常用的都是C语言封装的调用

movl $SYS_ify(vfork),%eax 把系统调用好放到eax寄存器里

int $0x80 软中断进行系统调用

2.异常:缺页中断 /0 野指针操作

除了系统调用会触发软中断,像缺页中断 /0 野指针等异常操作也会触发软中断。

为什么说/0 访问野指针,系统能知道。就是因为触发了软中断,让操作系统找中断向量表,找到对应的执行程序。

1.CPU内部触发的软中断,int 0x80 syscall ,我们叫做陷阱。

2./0 野指针等 我们叫做异常

二.信号捕捉

1.虚拟地址空间用户区 内核区

每个进程的虚拟地址空间都分为[0~3G]用户区 [3~4G]内核区,不同进程的用户区通过用户页表映射到自己的私有数据,但不同进程的内核区通过内核页表映射到的是同一块物理地址,同一个操作系统 也就是 无论操作系统怎么切换进程都可以找到同一个操作系统。

既然每个进程虚拟地址空间中都有内核区,我们是不是就可以随意访问内核区映射的数据了呢?

其实,并不是。

现代处理器通常有两种运行模式:用户模式和内核模式。

在用户模式下,只能访问用户区。

当转换为内核模式才能访问内核区。

1.系统怎么区分用户模式 内核模式的?

在CPU中有一个标识CPL(用CS寄存器的2个bit位表示),0代表内核模式 3代表用户模式。

2.怎么从用户模式变为内核模式?

中断:

1.时钟/外设中断

2.异常(/0 野指针)

3.陷阱(int 0x80 syscall)

2.信号捕捉流程

0.通过中断由用户态进入内核态

1.看pending表是否有1 (是否收到信号)

2.有 就看对应block表是否有1 有1表示被阻塞 不处理 

3.为0 如果信号对应的行为是用户自定义的行为 就返回用户态 并执行自定义行为的函数

4.返回内核态

5.再返回用户态 继续执行下文

执行完自定义行为的函数并恢复上下文,需要进行4次用户态和内核态的切换(红圈处)

1.为什么要返回用户态执行对应的自定义行为的函数?

保证内核资源安全。在内核态访问自定义行为函数就可以访问内核数据,造成安全隐患。

2.以用户态执行完后,为什么还要返回内核态再返回用户态?

恢复上下文,继续执行下文代码。 从调用的定义行为的函数不能直接返回int main()函数内,要先返回到之前调用它的函数内再继续返回到main函数中

3.信号捕捉操作

sigaction

和signal一样,重新定义在接收到特定信号时应采取的行为。

#include <signal.h>

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

signum:信号编号,指定要处理的信号(如 SIGINT、SIGTERM 等)。
act:指向 struct sigaction 的指针,定义了信号的处理方式。
oldact:指向 struct sigaction 的指针,若非 NULL,它用来返回先前该信号的处理方式。

struct sigaction 结构体

struct sigaction {
    void (*sa_handler)(int);      // 信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *); // 更复杂的信号处理函数
    sigset_t sa_mask;             // 该信号处理期间要屏蔽的信号集
    int sa_flags;                 // 信号处理的标志
    void (*sa_restorer)(void);    // 保留字段,现代 Linux 中通常为 NULL
};

void (*sa_handler)(int);要执行的自定义行为

sigset_t sa_mask; 信号集 里面是要进行阻塞的信号编号(信号处理期间)

也就是在信号处理期间除了阻塞该信号,还可以阻塞其它信号

1.如果我们在某个信号处理期间再发一个该信号会怎么样?
会被阻塞。OS在处理该信号期间会把该信号对应的block表bit位置1,信号处理完成后会自动解除。

2.我们知道接收到一个信号pending表会把对应bit位置1,什么时候变0呢?是信号处理完后,还是信号处理完前?

信号处理完前。如果信号处理完后变0,那1是原本的信号,还是处理期间接收到的信号呢?所以在信号处理完成前置0,如果处理完后还是1说明处理期间还接收到了信号,就再次处理。

三.可重入函数

可重入函数是指在执行过程中能够被中断并且可以安全地再进入(重入),即在同一时间内可以被多个执行线程或中断执行,而不会导致程序的不一致或错误。

一个函数被两个以上的执行流同时进入  (重入)

出问题 --不可重入函数

没问题 --可重入函数

1.什么是执行流?
执行流”通常指的是程序在运行时的指令执行顺序。

顺序执行流:代码按顺序执行。
条件分支执行流:根据条件判断分支,选择不同路径。
循环执行流:代码在满足条件时多次执行。
跳转执行流:使用跳转语句改变执行路径。
函数调用执行流:程序调用函数时改变执行流。
异常处理执行流:通过异常机制处理错误,改变执行流。
并发执行流:多线程并行执行,线程间交替执行。
异步执行流:任务异步执行,按回调或事件触发继续执行。(eg.信号处理)

2.可重入函数和不可重入函数

可重入函数

int add(int a, int b) {
    return a + b;
}

这个函数是可重入的,因为它没有依赖于外部的状态或资源,每次调用都是独立的,不会因为中断或重入调用而导致错误

不可重入函数

int counter() {
    static int count = 0;
    count++;
    return count;
}

这个函数不是可重入的,因为它使用了静态变量count,如果在执行过程中被中断并重入调用,可能会导致数据冲突或不一致。

一般返回的count的值为1,但如果在该函数在执行过程中再进入一个执行流,并对count进行修改就会影响count的值

四.volatile 易变关键字

volatile 是 C/C++ 中的一个关键字,用于告诉编译器某个变量的值可能会在程序执行过程中被外部因素(如硬件、信号处理程序或其他线程)改变,因此编译器不能对这个变量进行优化。

#include <stdio.h>

volatile int flag = 0;  // 防止编译器优化

void signal_handler(int sig) {
    flag = 1;  // 外部信号处理程序改变 flag 的值
}

int main() {
    while (!flag) {  // 持续检查 flag
        // 其他任务
    }
    printf("Flag is set!\n");
    return 0;
}

如果不对flag变量加volatile,编译器就会认为flag在while循环中不会被修改,把它们缓存到寄存器中,而不是中内存中读取。等到另一个执行流修改了flag,内存中的flag==1,但寄存器的flag仍为0,会导致循环一直持续下去。

五.SIGCHLD信号

SIGCHLD信号是它在子进程结束或停止时由内核发送给父进程

当父进程收到 SIGCHLD 信号时,默认行为是忽略此信号。

子进程结束或者停止时,父进程就会接收到SIGCHLD信号。我们可以重新定义SIGCHLD信号的行为,进行回收子进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

// 处理 SIGCHLD 信号的函数
void handle_sigchld(int sig) {
    int status;
    pid_t pid;
    
    // 使用 waitpid 来回收子进程的状态
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("子进程 %d 正常退出,退出状态 %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程 %d 异常退出,信号 %d\n", pid, WTERMSIG(status));
        } else if (WIFSTOPPED(status)) {
            printf("子进程 %d 被暂停,信号 %d\n", pid, WSTOPSIG(status));
        }
    }
}

int main() {
    // 设置信号处理函数
    signal(SIGCHLD, handle_sigchld);

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程部分
        printf("子进程启动,PID: %d\n", getpid());
        sleep(2); // 模拟一些工作
        exit(0);  // 子进程正常退出
    } else {
        // 父进程部分
        printf("父进程等待子进程完成...\n");
        sleep(5); // 模拟父进程等待
        printf("父进程完成。\n");
    }

    return 0;
}

1.如果有n个子进程同时退出,父进程就会同时接收到多个SIGCHLD信号,但信号位图只能记录有没有,不能记录有多少个。所以我们处理信号时要循环处理。直到没有子进程退出。

2.如果n个子进程中有一个子进程一直循环不退出怎么办?我们循环处理的时候,等待子进程不能阻塞等待,要选择非阻塞等待waitpid(-1, &status, WNOHANG) 

返回0时,说明没有子进程退出,继续执行父进程。

返回-1,说明没有子进程要进行等待,子进程全部回收完成。

如果需要回收子进程,但不想处理它的退出状态。

signal(SIGCHLD, SIG_IGN); 

​​​​​​​父进程告诉操作系统:当子进程结束时,我不需要处理它的退出状态。这样,操作系统会自动回收子进程的资源,避免僵尸进程的产生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值