写在前面
终于,到了大三
估计不久之后也要开始崩溃的考研时光了(悲
那么接下来,开始我的炸弹秀——
啊对对对,是咋俩的炸弹秀。
讲到哪了?
哦,对了,上一回刚刚讲到子进程和fork,其中提到了一些:
信号
我们之前提到的SIGSTOP / SIGSTEP / SIGTTIN / SIGTTOU
信号,其实只是信号的一部分。信号的定义,实际上就是内核向进程系统发送的消息,提示系统进程发生了,或者计划发生某些事件。比方说一个进程可以通过内核发送向另一个进程的SIGKILL
信号,来终止目标进程;再比如内核发出SIGXCPU
信号的时候,是一个进程TLE(Time Limit Exceed,运行超时,准确来讲是在CPU中执行指令的时候并没有抛出异常却卡住了)了。
至于这些信号的确切作用,你可以在《深入理解》的原书中找到,本文不再赘述。
信号的运作
既然提到信号,那么就不得不提信号的收发。概念很简单,发送信号就是内核将信号发送到进程,接收信号就是进程对内核发出的信号做出反应(并且因为内核的层级高于进程,实际上在通常情况下,这个接受过程是受迫的)。
是否发送取决于内核是否对检测到的系统事件进行反应,或者说系统有没有收到来自进程的kill请求。最直观的就是当你的程序卡住不动,尝试操作又导致超时的时候,windows任务管理器会发来一个提示,让你决定是否中断程序,当你选择中断,那么内核才会对这个异常信号反应,或者接受kill的调用请求,选择继续等待时则会绕过这一次的决策。
同样的,有无法发送的情况,就有无法接收的情况。有被动的情况,一个发出但没有被接收的信号叫待处理信号,一旦一个进程有一条对应的待处理信号,那么与这条信号同类的所有信号都会在传入的时候被拦截,而这也就意味着,待处理信号有对于当前进程的唯一性,即一个进程的同一类待处理信号只能有一个。
也有主动的情况,就是一个进程可以选择阻塞某类信号,这些被阻塞的信号会处于一种“如收”的状态,即收了但没执行,直到进程取消阻塞行为。实际上,对于待处理信号的处理过程就是一种隐式的阻塞,相对而言,使用**sigporcmask
函数之类的手段进行调用的阻塞则是显式**阻塞,具体而言可以翻阅《深入理解》原书522到533页,本文不再赘述。
内核在每个进程中维护两个分别叫”pending“和”blocked“的位向量,一个维护当前进程的待处理信号,一个维护被阻塞的信号类别.当内核发送一个信号给进程时,它将会修改进程的pending位向量,譬如说,当内核发送一个SIGINT
信号给进程,那么它会将进程的pending[SIGINT]
的值设置成1。同样地,当进程屏蔽掉一个信号时,那么它会修改blocked位向量。实际上,比起将其当作向量理解,学OI学多的人更可能会将其当作bool数组来理解,不过这都是后话了。
发信与收信的方式
虽然我很想先把发信方式讲了,但是,我突然意识到,我在介绍进程的时候漏了个概念:
进程和进程组
众所周知,每个进程有唯一对应的编号,但是,打过OI或者学过数据结构的人应该明白,对于大批量且具有明确上下级关系的结构,用并查集维护(简单来讲就是对于在同一个连通块的数据,全部归为一组进行管理,方便对组数据的批处理操作)往往比直接用数组更为高效。
行行行,考虑到大家都是学CTF的,不谈OI了,谈数据结构吧。
就单纯为了方便管理,在进程之上,还有进程组的概念,即父进程以及所有与之直接或间接关联的子进程。众所周知,是个并查集都需要一个单射序列,来确定所有的集合。同样地,进程组也需要一个唯一对应的标识,也就是**进程组ID(PGID)**。当一个新的进程组产生,其进程组ID往往是其根进程,也就是第一个加入该进程组的进程的PID。
一个进程对自身所在的进程组,可以用**getpgrp
进行查询,也可以用set-pgid
**函数来更改自身或其它进程的进程组,set-pgid若设定pgid为0,则以目标进程为根进程,新创建一个进程组。
回到正题
发信的方式有很多,**/bin/kill
** 程序或者编程语言中系统库的kill函数,可以向任意进程发送SIGKILL
,也就是所谓“杀程序”的信号;系统快捷键也可以向正在运行中的进程发送信号,比方说在运行时 Ctrl+C 就是发送强制终止的信号;进程也可以通过调用alarm函数给自己发送SIGALRM
信号,etc。
信号的收发,论其根源实际上都是内核到进程的单线控制。当内核完成上下文切换或者从系统调用中返回进程时,控制权由内核移交给进程,此时,若我们之前提到的,内核维护的待处理信号集合/序列为非空状态,内核会根据序列中信号在系统中编号的大小,从小到大发送信号。进程在接收信号,完成行为之后就继续执行下一条指令——如果它没有被信号影响而产生一些无法逆转的后果的话。
进程在接收信号的时候可以通过 signal
函数来改变某类信号相关联的行为。简单来讲,如之前所说,每一个信号种类其实对应着序列上的一个元素,而这个元素又与一条编码序列,也就是 信号编码 形成单射,signal就是将编码对应的信号调用转移到其它函数,或者将它已经变更过的调用转移到原信号上。
举个并不很实际,但却很直观的例子,假设今天小娜想特别给自己插张全新N卡,她的SIGUSR1(允许用户自定义的信号)
被设定为自己的导航程序的启动函数:
signal(10, *loc_tar_start);
这时候,她的思考进程捕获到了SIGUSR1
信号,启动了导航程序,让她能够一路畅通无阻的杀向电子城。但是,她盘算了自己的钱包,显然连这个月给自己充电用的电费都快付不起了。她只好想着法子克制自己的欲望,于是她灵机一动,把代码改成了这个样子:
sighandler_t signal(10, SIG_IGN);
当第二个参数为SIG_IGN
时,SIGUSR1
会被忽略,这样她就找不到地,也就用一个非常简单的方式保住了自己的钱包。
好吧,下回给你多发点……
不过,如果想要达到忽略信号的效果,阻塞其实也不失为一种办法,这就要利用 sigporcmask
函数了。
非本地跳转
当然,并不是只有内核才能处理异常,异常也不是只提供给内核的操作手段。非本地跳转通过**setjump
和longjump
**函数,提供了令用户层不需要通过内核,便能在进程间转移的手段。
setjump
通过在栈上的env缓冲区保存当前调用环境,也就是在栈上某个可用区域存储你目前用到的运行环境相关的变量,包括程序的计数器、栈指针和通用目的寄存器。而longjump
负责从env缓冲区中恢复这些状态。具体实例还是得翻阅原书,毕竟这里只是个笔记,抛砖引玉用的。
综上所述,我们终于大概弄明白了整一个进程操作和控制流传递的过程,也大致明白了你的计算机究竟是怎样在多个问题中思考,寻求解决方案的。不出意外,下一期,我们就要去干内存了。所以……
–ARE YOU READY???–