进程信号


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

信号是什么?

信号是一个软件中断。

作用:

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

一个信号对应一个事件,并且我们能够识别这个信号。
操作系统中的信号同样如此:通过 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,这就造成了数据二义,这就是一个函数的重入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值