进程信号


在进程控制中有一个“信号量”的东西,它是内核中的一个计数器,用于实现进程间的同步与互斥,而本篇的“信号”和这个“信号量”并不是同一个东东。

信号是什么?

信号是一个软件中断。

作用:

操作系统通过信号告诉进程发生了某个事件,打断进程当前的操作,去处理这个时间。举个栗子,当我们正在上课的时候,听到了下课铃声,我们就下课了就去玩了,这个下课铃声就是一个信号。

一个信号对应一个事件,并且我们能够识别这个信号。
操作系统中的信号同样如此:通过 kill -l命令查看系统中的信号种类 – 62种。
在这里插入图片描述

1 – 31号信号:从Unix借鉴而来的,没个信号都有具体对应的系统事件。(非可靠信号 — 有可能会信号丢失 – 事件丢失)
34 – 64号信号:后期扩充的,因为没有具体对应的事件,因此命名比较草率。

信号的生命周期:产生->在进程中注册->在进程中注销->处理。(可靠信号 — 不会丢失信号)

信号的产生

硬件:Ctrl + C / Ctrl + Z / Ctrl + |
软件:kill -signum pid 命令(signum是信号值,pid是进程ID),kill 默认发送15号信号,kill(int pid,int signum) 给指定进程发送指定信号,raise(int signum) 给自身进程发送指定信号,abort() 给自己发送SIGABRT信号,通常用与异常通知,alarm(int seconds)几秒钟之后发送SIGALARM信号 – 通常称作定时器。

kill 杀死一个进程的原理就是,向进程发送一个信号,信号有对应的事件,进程放下手头工作去处理这个事件,然而事件的处理结果就是让进程退出

信号的注册

信号在进程中注册:如何让进程知道自己收到了某个信号?在pcb中有个结构体 struct sigpending,这个结构体中的结构体 struct sigset_t这个结构体中只有一个数组成员,这个数组用来实现一个位图 — 称之为未决信号集合 – 收到了但是没有处理的信号集合,给进程发送一个信号,就会在这个位图中对应位置置 1,表示进程当前收到了这个信号,但是位图只有 0 / 1,也就是只能表示是否收到了这个信号,但是无法表示收到了多少个这样的信号,信号的注册其实不仅会修改位图,还会为信号组织一个sigqueue节点添加到pcb的sigqueue链表中。

上边说 1 – 31号信号是非可靠信号,34 – 64号信号是可靠信号就是因为:
1 – 31号信号若信号注册的时候位图为 0,则会创建一个sigqueue节点并修改位图为1,但是若位图为 1,则什么都不做。
34 – 64号信号注册的时候不管位图是否为 0,都会创建一个节点,添加到链表中,并修改位图

信号的注销

为了保证一个信号只会被处理一次,因此是先注销再处理 – 在pcb中删除当前信号信息,将pending位图置 0,删除信号节点

非可靠信号注销:因为非可靠信号只会有一个节点,因此删除节点后,位图直接置 0.

可靠信号注销:因为可靠信号有可能注册多次,有多个节点,因此删除节点后,需要判断是否还有相同节点,若没有才会将位图置 0.

信号的处理

  1. 默认处理方式:操作系统中原定义好的每个信号的处理方式
  2. 忽略处理方式:处理方式就是忽略,什么都不做
  3. 自定义处理方式:自己定义一个事件函数,使用这个函数替换内核中默认的处理函数,信号到来就会调用我们自己定义的函数了

自定义处理方式:
使用函数 sighandler_t signal(int signum,sighandler_t handler);
signum就是要自定义处理的信号
handler :SIG_DFL – 默认处理方式 / SIG_IGN – 忽略处理方式 / 用户自己定义一个没有返回值,有一个int 型参数的函数地址。

信号阻塞:

步骤:

  1. 将一些信号的处理函数自定义
  2. 将所有的信号都阻塞
  3. 解除阻塞之前,给进程发送信号
  4. 解除阻塞,查看信号的处理过程

并不是不接受信号。信号依然可以注册,只是表示哪些信号暂时不处理

在pending和handler中间还有一个阻塞信号集合 – block,当pending收到信号在block中是阻塞的就不去回调handler,如果没有阻塞,咋就去调用handler执行相关操作,这个过程中,信号依然可以在pending中注册。

在内核中的表示

在这里插入图片描述

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里

如何阻塞一个信号

int sigprocmask(int how , sigset_t *set,sigset_t *old)
how :
SIG_BLOCK — 将set集合中的信号添加到内核中的block阻塞信号集合中,使用old保存原来的阻塞信息便于还原 — 阻塞set集合中的信号
SIG_UNBLOCK — 将set集合中的信号从内核中的block阻塞信号集合中移除 – 对set集合中的信号解除阻塞
SIG_SETMASK — 将内核中的block阻塞信号集合内容设置为set集合中的信息 — 阻塞set集合中的信号

int sigemptyset(sigset_t *set) 清空set集合 – 使用一个变量的时候初始化过程
int sigaddset(sigset_t *set,int signum) 向set集合中添加指定信号
int sigfillset(sigset_t *set) 将所有信号添加到set集合中
int sigdelset(sigset_t *set,int signum) 从set集合中移除指定的信号
int sigismember(const sigset_t *set,int signum) 判断指定信号是否在set集合中

在所有的信号中,有两个信号比较特殊:SIGKILL -9 / SIGSTOP -19,这两个信号不可被阻塞,不可被忽略,不可被自定义

信号阻塞的应用

实例:

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

void wait_child() {
	printf("child return\n");
	while(waitpid(-1, NULL, WNOHANG) == 0);
}

int main() {
	pid_t pid = fork();
	// 子进程3秒后退出
	if(pid == 0) {
		sleep(3);
		exit(0);
	}
	//接受到子进程退出的信号,进行 waitpid 处理
	signal(SIGCHLD, wait_child);

	while(1);
	return 0;
}

僵尸进程中信号的应用:子进程退出后会向父进程发送SIGCHLD信号通知父进程,子进程状态改变,但是因为SIGCHLD信号,默认的处理方式是忽略,因此之前的程序中若不进行进程等待则不知道子进程的退出,如果要进行进程等待,而且不让父进程阻塞,就可以自定义SIGCHLD信号的处理方式,在自定义回调函数中调用waipid,处理僵尸进程,父进程就不用了一直等待。但是SIGCHLD信号是一个非可靠信号,如果多个子进程同时退出,有可能造成信号丢失。

关键字 volatile: 用于修饰一个变量,保持变量的内存可见性(CPU在处理的时候每次都从新从内存获取数据)防止编译器过度优化
CPU处理一个数据的时从内存中将数据加载到寄存器上进行处理
gcc编译器,在编译程序的时候,如果使用了代码优化 -Oleve 选项,发现某个变量使用频率相当高,为了提高效率,则直接将变量的值设置为某个寄存器的值,以后访问的时候直接从寄存器访问,则减少了内存访问的过程,提高了效率。
但是这种优化有时会造成代码的逻辑混乱,因此使用volatile关键字修饰变量,让cpu无论如何每次都重新到内存中获取数据。
示例程序:

int a = 1;
void sigcb() {
	a = 0;
	printf("a = %d",a);
}
int main() {
	signal(SIGINT,sigcb);
	while(a) {
	}
	printf("exit a = %d\n",a);
	return 0;
}

正常情况下,按一下Ctrl+C,程序会停止,a = 0,但是如果程序优化之后,就不会退出。

函数的可重入与不可重入

函数的重入:在多个执行流程中,同时进入一个函数运行。

函数的可重入:指的是函数重入之后,不会造成数据二义或者逻辑混乱

函数的不可重入:指的是函数重入之后,有可能造成数据二义或者逻辑混乱

函数是否可重入的判断基准:这个函数中是否对全局变量进行了非原子操作,若有,则不可重入。

操作的原子性:操作以此完成,中间不会被打断。

以后我们实现函数的时候,或者使用别人的函数的时候,最好能够考虑一下是否可重入的问题,防止使用的时候出现问题
理解代码:

int a = 1,b = 1;
int test() {
	a++;
	sleep(2);
	b++;
	return a+b;
}
void sigcb(int no) {
	printf("signal sum:%d\n",test());
}
int main() {
	signal(SIGINT,sigcb);
	printf("main sum:%d\n",test());
	return 0;
}

正常执行流程结果是 6,但是如果执行中按下Ctrl+C,就会打印出 signal sum = 5,main sum = 6,因为主控流程中 a++完等于 2了,然后sleep的时候,按了下ctrl+c,进入了信号流程,信号流程直接进入test得到的a = 2了,再a++,b++结果a+b就等于5了,但是主控流程还是6,这就造成了数据二义,这就是一个函数的重入。

### 信号的概念 信号进程间通信的一种方式,用于通知进程发生了某种特定事件。在 Linux 系统中,有多种常见的信号,每种信号都有其特定的编号和名称,例如 SIGINT(编号 2,通常由 Ctrl+C 触发)、SIGTERM(编号 15,用于正常终止进程)等。信号的管理涉及信号的产生、存储、处理等多个方面,信号的存储结构与进程的状态相关,主要涉及 block 表、pending 表和 handler 表 [^1][^2][^3]。 ### 信号的产生 - **通过键盘产生信号**:用户在终端输入特定的组合键可以产生信号,如 Ctrl+C 会产生 SIGINT 信号,用于终止当前前台进程;Ctrl+\ 会产生 SIGQUIT 信号 [^3]。 - **调用系统函数向进程发送信号**: - **kill**:可以给任意进程发送任意信号,函数原型为 `int kill(pid_t pid, int sig);`,其中 `pid` 是目标进程的 ID,`sig` 是要发送的信号编号 [^1][^3]。 - **raise**:用于给进程本身发送任意信号,函数原型为 `int raise(int sig);`,相当于 `kill(getpid(), sig)` [^1][^3]。 - **abort**:使当前进程异常终止,它会发送 SIGABRT 信号给当前进程,函数原型为 `void abort(void);` [^3]。 - **硬件异常产生信号**:当硬件出现错误时,会产生相应的信号。例如,除零错误会产生 SIGFPE 信号,访问非法内存会产生 SIGSEGV 信号 [^2][^3]。 - **软件条件产生信号**:某些软件条件满足时会产生信号,如 `alarm` 函数可以设置一个定时器,当定时器超时后会产生 SIGALRM 信号 [^1][^3]。 ### 信号的处理方式 - **默认处理**:每个信号都有其默认的处理动作,如终止进程、忽略信号、暂停进程等。 - **忽略处理**:进程可以选择忽略某些信号,即不做任何处理。 - **自定义处理**:进程可以通过信号处理函数来对特定信号进行自定义处理。在 Linux 中,可以使用 `signal` 或 `sigaction` 函数来设置信号的处理动作 [^1][^3]。 ### 信号的捕捉 - **自定义捕捉**:可以使用 `signal` 或 `sigaction` 函数来注册自定义的信号处理函数。例如,使用 `signal` 函数的示例代码如下: ```c #include <stdio.h> #include <signal.h> void handler(int signum) { printf("Received signal %d\n", signum); } int main() { signal(SIGINT, handler); while (1) { // 主循环 } return 0; } ``` - **无法被捕捉的信号**:有些信号是无法被捕捉的,如 SIGKILL(编号 9)和 SIGSTOP(编号 19),它们用于强制终止或暂停进程 [^3]。 - **内核如何实现信号的捕捉**:当信号产生时,内核会在适当的时候检查进程信号处理表,根据信号的处理方式进行相应的处理。如果是自定义处理,会调用注册的信号处理函数 [^3]。 ### 信号的阻塞 - **信号的状态**:信号有三种状态,分别是产生、未决和递达。信号产生后,如果被阻塞,则处于未决状态;当阻塞解除后,信号才会递达并进行处理 [^3][^5]。 - **信号在内核中的表示**:信号在内核中通过 block 表、pending 表和 handler 表来表示。block 表用于记录哪些信号被阻塞,pending 表用于记录哪些信号已经产生但尚未递达,handler 表用于记录信号的处理动作 [^2][^3][^5]。 - **sigset_t 信号集**:`sigset_t` 是一种数据类型,用于表示信号集。可以使用信号集操作函数来对信号集进行操作,如 `sigemptyset`、`sigfillset`、`sigaddset`、`sigdelset` 等 [^2][^3]。 - **sigprocmask 设置阻塞信号集**:`sigprocmask` 函数用于设置进程的阻塞信号集,函数原型为 `int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);` [^1][^3]。 - **sigpending 获取未决信号集**:`sigpending` 函数用于获取当前进程的未决信号集,函数原型为 `int sigpending(sigset_t *set);` [^3]。 ### 信号的补充 - **volatile 关键字**:`volatile` 关键字用于保持内存的可见性,在信号处理函数中使用 `volatile` 可以确保变量的不会被编译器优化而导致的问题 [^2][^3]。 - **SIGCHLD 信号**:当子进程终止或停止时,会向父进程发送 SIGCHLD 信号。父进程可以通过捕捉该信号来进行资源回收等操作 [^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值