做淘宝团购的网站,企业网站建设费用财务处理,站长推荐自动跳转,wordpress的标题字怎么变目录
一#xff0c;什么是信号
进程面对信号常见的三种反应概述
二#xff0c;产生信号
1.终端按键产生信号
signal
2. 进程异常产生信号
核心转储
3. 系统调用函数发送信号
kill
raise
abort
小结#xff1a;
4. 由软件条件产生
alarm
5. 硬件异常产生信号…目录
一什么是信号
进程面对信号常见的三种反应概述
二产生信号
1.终端按键产生信号
signal
2. 进程异常产生信号
核心转储
3. 系统调用函数发送信号
kill
raise
abort
小结
4. 由软件条件产生
alarm
5. 硬件异常产生信号
三信号其他概念
1. 进程中储存信号的内核结构
2. sigset_t类型——信号集类型
3. sigpending接口
4. sigprocmask接口
5. 重新理解进程在计算机中的运行
四捕捉信号
1. 捕捉信号流程
编辑
2. sigaction
关键字——volatile
SIGCHLD信号 嘿收到一张超美的风景图希望你每天都能开心顺心 一什么是信号
操作系统中的信号是一种在进程间传递信息和通知的机制。它可以用来通知进程发生了某种事件比如用户按下了某个键盘按键、进程收到了某个信号或者发生了某个错误等。
生活中的例子
手机收到新短信或来电时会发出提示音这就是一种信号通知用户有新的事件发生。交通信号灯会发出红、黄、绿三种不同的信号指示车辆和行人何时可以通行。火灾报警器发出警报声通知人们有火灾发生需要立即疏散。门铃响起通知主人有人来访。警报器在发现入侵者时会发出警报声通知屋主有危险。 进程面对信号常见的三种反应概述 可选的处理动作有以下三种 : 1. 忽略此信号。 2. 执行该信号的默认处理动作。 3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。 这里我们以kill 信号为例
我们平时手动结束一个进程: kill -9 PID, 本质上是让操作系统向目标进程发送 9信号从而结束该进程。下面是kill 相关的信号表 以上普通信号我们认识七八个即可 指令man 7 signal 通过man 7 signal, 我们可以查看信号的详细信息 试问如何理解键盘中组合键如何实现其功能
答 首先我们得知道键盘的工作方式通过中断的方式产生信息。同时操作系统中也一定有识别该组合键产生信息的记录表。
一个进程在接受信号后必然会对信号进程储存。进程则是通过PCBtask_struct中位图unsigned int来记录信号是否存在。
而PCB又是内核数据结构能修改PCB的也就只有OS自身。 即信号发送的本质是OS对目标进程的PCB中信号位图的修改。 回到组合键的问题 二产生信号
1.终端按键产生信号
signal signum: 捕获该进程信号
handler : 信号处理方法函数指针
比如这样
void signalmain(int signal)
{cout 信号处理中... : signal gitpid : getpid() endl;
}int main()
{signal(SIGINT, signalmain);while ( 1){cout 接受信号中...... endl; sleep(1);}return 0;
}
运行过程中我们不断通过ctrl c的组合键进行操作。运行结果如下 从上面我们可以得出2个点1.键盘的组合键确实是系统向当前进程发送信号。 2. 可以通过signal注册信号处理函数。 signal使用须知signal接口并不是调用就会触发信号处理方法它只是提前注册了信号处理函数只有捕捉到特定信号时才会调用特定方法; signal接口一般出现在main函数开始。 2. 进程异常产生信号
核心转储
这个在进程控制waitpid函数status参数中提到过 大概就是这样在进程发生异常退出时能进行核心转储形成一个二进制的特殊文件。 解释一下为什么云服务器核心转储默认是关闭因为服务器的管理是有另外一层服务负责他们的任务是将异常挂掉的服务进程自动重启如果因为进程老是异常挂掉这会导致磁盘中存在大量的转储文件会导致资源浪费。
我们在终端再次打开 man 7 signal, 文档中core的意思就是核心转储。 3. 系统调用函数发送信号
kill 我们在终端输入kill -9 PID等等信号在底层是调用了kill系统函数。kill也很简单。 kill : 进程 PID sig: 信号编号 raise 功能很简单就是让OS向自身进程发送信号 abort 功能 终止自身进程相当于向自身进程发送kill -6 小结 上面这些接口本质上都是利用系统接口调用执行OS对应的系统调用代码OS在PCB中设置或者是修改特定的值进程对信号进行处理。 4. 由软件条件产生
例子
我们以曾经的管道为例如果读端不仅没读而且将读端关闭写端一直在写。作为单向通信写已经没有意义了OS会终止写进程发送SIGPIPE13这构成不了软件级的条件。 alarm 功能就是过 seconds 秒后OS将自动向该进程发送SIGALRM14信号。
注意当进程开始当alarm被触发后向进程发送信号但我们要注意alarm只是发送SIGALRM信号并不会阻断进程正常进行。 5. 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。 例如当前进程执行了除以0的指令, CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。 void func(int st)
{cout 接受到信号: st endl;
}int mian()
{signal(SIGFPF, func);int count 150;z count / 0;while(1) {sleep(1);}
} 现象会一直打印 “接受到信号 8” 1. 首先我们得思考这个信号是谁发出的呢 CPU运算单位异常详细解释CPU内部有寄存器其中一个状态寄存器位图存储信息有对应的状态标记位溢出标记位。OS会自动进行计算检测先检测再计算如果状态标记位是1(可以理解为是否异常)OS会立即调取进程PID向目标进程发送信号然后就是进程会选择合适的时机处理信号。因此 一些运算并不都是软件层产生的信号一些则是硬件层产生的信号。 2硬件出现异常进程就一定会退出吗 不一定如果我们没有捕获信号那么进程默认退出而捕获后我们的就可以控制进程是否退出。 3. 为什么上面代码会进入死循环 在捕获信号后信号处理中并未退出进程该进程还在CPU运行队列中将会被再次调度当再次调度时OS还会继续检测会继续发送信号然后被捕获处理接着继续在CPU运行队列中。 再比如当前进程访问了非法内存地址,MMU(硬件)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。 注 野指针的异常常常会有段错误 Segmentation fault
void func(int st)
{cout 接受到信号: st endl;
}int mian()
{signal(SIGSEGV, func);int *count nullptr;*count 100;while(1) {sleep(1);}
}
现象还是死循环打印 “接受到信号: 11
1. 如何理解地址访问 首先我们访问一个数据目标我们一定得访问其物理地址。那么中间会有一段虚拟地址转换为物理地址的过程由页表(并不是软件结构而是一种硬件) MMU(Memory Manager Unit, 是一种硬件) , 当错误的地址被MMU硬件寄存器读取后一定会报错OS将MMU的报错转换为信号发送给进程让其退出。
2. 死循环原因 因为状态寄存器储存进程的状态在信号发送完一次后再次被调度时检测到进程状态寄存器中的异常则又会发送信号然后被切换保存进程上下文就这样一直继续下去。 三信号其他概念
实际执行信号的处理动作称为 信号递达(Delivery) 信号从产生到递达之间的状态称为 信号未决(Pending)。 进程可以选择 阻塞 (Block )某个信号 一般情况下进程不会阻塞任何一种信号。 被阻塞的信号产生时将 保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。 注意阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。 1. 进程中储存信号的内核结构 我们可以回想之前是使用的signal系统接口
结合上图可以这么理解sig就是pending中对应信号的位置第二参数位我们暂时叫func, func就是对handler[sig]中设置处理函数的地址。
上面是对信号的自定义处理。
而执行默认处理
signal(SIGSEGV, SIG_DFL); // 对应的下标是0
忽略处理
signal(SIGSEGV, SIG_IGN); // 下标为1
这有一点需要注意的是当OS检测到进程的一个信号下标值为signalsum并不是直接handler[signalsum]直接访问而是先比较是否是SIG_DEL(0)或者SIG_IGN(1)再比较自定义处理。 2. sigset_t类型——信号集类型
从上图来看每个信号只有一个bit的未决标志,非0即1不记录该信号产生了多少次阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储 sigset_t称为信号集这个类型可以表示每个信号的“有效”或“无效”状态在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask)这里的“屏蔽”应该理解为阻塞而不是忽略。 总结block, pending都可以用sigset_t类型表示 sigset_t类型的理解 sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从 使用者的角度是不必关心的,使用者只能 调用函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用 printf直接打印sigset_t变量是没有意义的。 信号集处理相关函数 #include signal.h int sigemptyset(sigset_t *set); // 将信号集全设置为0信号集初始化。 int sigfillset(sigset_t *set); // 将信号集设置为1 int sigaddset (sigset_t *set, int signo); // 添加某一信号 int sigdelset(sigset_t *set, int signo); // 删除某一信号 int sigismember const sigset_t *set, int signo); // 判断一个信号集的有效信号中是否包含某种 信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1 3. sigpending接口 功能 读取当前进程的未决信号集 , 通过 set参数传出 。调用成功则返回 0, 出错则返回 -1 。 4. sigprocmask接口 功能调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。 set: 是我们自定义的一个信号集。 how: 传入的set对该进程的阻塞信号集进行怎样的操作比如添加屏蔽字删除屏蔽字覆盖屏蔽字。 oset: 传入一个新set, 储存修改前旧的阻塞信号集。 how可选值 实践
void cmpshow(const sigset_t set)
{for (int i 1; i 31; i){if (sigismember(set, i)){cout 1;}else cout 0;}cout endl;
}int main()
{sigset_t set, oset;sigemptyset(set);sigemptyset(oset);sigaddset(set, 2); // 目标阻塞信号 2sigpending(oset);int n sigprocmask(SIG_BLOCK, set, oset);assert(n 0); // n 必然0(void)n; // 目的是调用一次n,避免在release版本中n未被调用的警告while (1){sigset_t tmp;sigemptyset(tmp);sigpending(tmp);cmpshow(tmp);sleep(1);} return 0;
} 问为什么没有设置pending信号集的接口 答没必要像kill, raise, abort指令接口都可以修改pending。 小结我们的进程中的信号都有各自接口负责管理处理函数——signal; 信号未决表——sigpending; 阻塞信号集——sigprocmask。 代码知识加餐 int n sigprocmask(SIG_BLOCK, set, oset);assert(n 0); // n 必然0(void)n; // 目的是调用一次n,避免在release版本中n未被调用的警告 问题1既然进程可以自己捕捉信号那我们让进程能捕获任何信号并且全部阻塞信号那这样就可以制作一个无法被动退出的进程了吗 回答OS的设计者已经考虑到这种情况了所以解决方法是kill -9 PID 这个信号是管理者信号无法被阻塞也无法修改其处理方法指结束进程。 5. 重新理解进程在计算机中的运行 四捕捉信号
1. 捕捉信号流程 如果信号的处理动作是用户自定义函数 , 在信号递达时就调用这个函数 , 这称为捕捉信号。由于信号处理函数的代码是在用户空间的, 处理过程比较复杂 , 举例如下 : 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。 当前正在执行main函数 , 这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复 main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间 , 它们之间不存在调用和被调用的关系, 是 两个独立的控制流程 。 sighandler 函数返回后自动执行特殊的系统调用sigreturn 再次进入内核态。 如果没有新的信号要递达 , 这次再返回用户态就是恢复main函数的上下文继续执行了 疑问为什么在内核层山时不直接调用信号处理函数呢
答如果信号处理函数中存在非法操作那么贸然让计算机内核进行访问数据计算机数据安全无法得到保证。 2. sigaction 功能 检查或者修改信号处理方法。平时用的最多的是signal 用法简单易上手。
sig : 目标信号
struct sigaction * 一种放多种信息的结构体其中就包括自定义函数处理方法sa_handler #include stdio.h
#include stdlib.h
#include signal.hvoid sigint_handler(int signo) {printf(Caught SIGINT, exiting...\n);exit(1);
}int main() {struct sigaction sa;// 对结构体内数据初始化sa.sa_handler sigint_handler;sigemptyset(sa.sa_mask);sa.sa_flags 0; if (sigaction(SIGINT, sa, NULL) -1) {perror(sigaction);exit(1);}printf(Press CtrlC to send SIGINT...\n);while (1) {// Do some work}return 0;
}当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 关键字——volatile
该关键字在C当中我们已经有所涉猎今天我们站在信号的角度重新理解一下。 示例代码 int tmp 0;void func(int sig)
{cout tmp: tmp --;tmp 1;cout tmp endl;
}int main()
{signal(2, func);while (!tmp);return 0;
}// 编译
signal : signal.ccg -stdc11 -o $ $^ -O3 -g# -O3 ————编译器对代码进行三级优化./PHONY: clean
clean: rm -rf signal现象进程开始运行后进行ctrl c进程结束一切正常。但未来我们的代码会跑在各种各样的编译器上其中一些优化就会影响这个过程。这里就直接说了在编译时添加 -o3 进行三级优化由于tmp没有进行写入操作寄存器直接用0代替了tmp这就会导致我们使用 ctrl c,无法终止循环。而 volatile(易变的) 就是提醒计算机请不要优化该数据。 因此用 volatile 修饰即可tmp即可。 SIGCHLD信号 进程一章讲过用wait和 waitpid函数清理僵尸进程父进程可以阻塞等待子进程结束 也可以非阻塞地查询是否有子进程结束等待清理( 也就是轮询的方式——查看子进程是否发来信号 ) 。 采用第一种方式 父进程阻塞了就不能处理自己的工作了 采用第二种方式 父进程在处理自己的工作的同时还要记得时不时地轮询一 下 程序实现复杂。 其实 子进程在终止时会给父进程发 SIGCHLD 信号 该信号的默认处理动作是忽略 父进程可以自定义SIGCHLD 信号的处理函数 这样父进程只需专心处理自己的工作 不必关心子进程了 子进程终止时会通知父进程 父进程在信号处理函数中调用wait 清理子进程即可。 下期多线程 结语 本小节就到这里了感谢小伙伴的浏览如果有什么建议欢迎在评论区评论如果给小伙伴带来一些收获请留下你的小赞你的点赞和关注将会成为博主创作的动力。