1. 掌握Linux信号的基本概念
1进程必须 识别+能够处理信号---信号没有产生也要具备处理信号的能力---信号的处理能力,属于进程内置功能的一部分
2 进程即便是没有收到信号,也要具备处理信号的能力
3 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,合适的时候去处理
4 一个进程必须当信号产生,到信号开始被处理,一定就会有时间窗口,进程具有临时保存那些信号已经发生了的能力
ctrl+c为什么能够杀掉我们的前台进程?(./ a.out & 这样的指令就是运行一个后台进程)Linux中,一次登录中,一个终端,一般只会配上一个bash,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程
两者区别: 谁来获得键盘的输入,前台进程会获得键盘的输入,ctrl+c能被进程获取,被杀掉
而后台进程不会获得键盘的输入,bash是获得键盘的输入(所以ls ....等等指令仍可以被bash 接收到,ctrl+c进程是获得不到的,所以杀不掉进程)
信号的处理方式:1 默认动作
2 忽略
3 自定义动作(信号的捕捉)
信号捕捉的系统调用signum:信号标号
handler:自定义动作,当得到这个信号的时候,去执行这个自定义动作
其实这个信号就是跟我们嵌入式当中的终端差不多,软件模拟的硬件中断
键盘其实就是通过硬件中断来判断数据输入
信号的产生和我们自己的代码的运行是异步的
2. 掌握信号产生的一般方式
1. 通过键盘组合键
ctrl + c : 会产生2号信号
ctrl + z : 会产生3号信号
2. 通过kill 命令 keil -singlenum id
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定 的信号(自己给自己发信号)。
3 调用系统函数向进程发信号
注意,并不是所有的进程都可以被捕捉,9 和 19不呢被自定义捕捉,9:是进程的杀掉任务,19:是任务的暂停任务,也不能被捕捉
给自己发信号
让自己退出的信号 -6
无论信号是如何产生的,最终一定是操作系统给进程发信号
4. 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除 以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非 法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
MMU:内存管理单元---->查页表的映射就是这个单元来做
如果发生异常的时候,cpu状态寄存器会置位,cpu检测到这个状态寄存器的变化就会将这个正在运行的出错误的进程说有的进程上下文打包,进程给切换了,所以这也是说明为什么一个代码出错误,cpu并不会崩溃,一出错误就会切换别的进程的上下文去正常运行
5. 由软件条件产生信号
返回值是这个闹钟响的剩余时间
这个闹钟也只会响一次
这个闹钟默认发送的是14号信号,如果不去用signal函数去重写这个信号的话,默认收到信号是去结束进程
打开系统的core dump功能,一旦进程出现异常,OS 会将进程在内存中的运行信息,给我dump(转储)到进程的当前目录(磁盘),形成core.pid文件,:核心转储(core dump)功能但是云服务器默认是关闭这个功能的(要不然一错误就会有文件产生,这样会对磁盘冲击)
用ulimit -a 查看core dump功能是否打开
再用ulimit -c 10240 : 打开这个功能,如果要关闭就ulimit -c 0就可以了
生成的core.pid 可以在debug中用core-file导入这个文件,然后这个文件就包含你出吸纳的错误(事后调试)
3. 理解信号递达和阻塞的概念,原理。
对于普通信号而言,对于进程而言,自己有没有收到一个信号,其实是给进程的pcb发的,通过task_struct中的一个变量:int singal,是通过位图的方式管理信号的
1比特位的内容是0还是1,表明是否收到
2 比特位的位置(第几个),表示信号的编号
3 所谓的发送信号,本质就是OS去修改task_struct的信号位图对应的比特位
进程收到信号之后,可能不会立即处理这个信号,信号不会被处理,就要有一个时间窗口
1. 信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2. 在内核中的表示
所有的操作都离不开这三张表
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号 产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子 中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。本章不讨论实时信号。
3. sigset_t
注意这个类型是内核数据结构,你不能对定义的这个数据类型进行任何操作,只能通过系统调用来进行操作
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。
4. 信号集操作函数
sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统 实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做 任何解释,比如用printf直接打印sigset_t变量是没有意义的
cpp#include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo);函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有 效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系 统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。
初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
5 sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
6 sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程 序如下:
4. 掌握信号捕捉的一般方式。
信号什么时候被处理?
当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理(就是遍历pending和block表,看看那些需要去执行)
而且要注意:不是只有系统调用才会去进行信号检测,因为cpu是以时间片的方式去执行代码
当把一个进程从cpu上拿走,把另外一个进程从等待队列中放到cpu上面,操作系统执行都是以内核太来进行操作的,所以信号检测的处理会很及时
当调用系统调用的时候,操作系统自动会进行"身份"切换的---->用户身份变成内核身份(或者反过来)
内核态:允许你访问操作系统的数据
用户态:只能访问自己的代码和数据
重谈地址空间!
其实页表也分为内核级页表和用户级页表
用户级页表,有几个进程,就有几个用户级页表----进程具有独立性
内核级页表有几份?只有一份
进程角度:我们调用系统中的方法,就是在我自己的地址空间中进行执行的
操作系统视角:任何一个时刻,都有进程执行,我们想执行操作系统的代码,就可以随时执行
操作系统的本质:基于时钟中断的一个死循环
计算机硬件中,有一个时钟芯片,每隔很短时间,响计算机发送一次时钟中断

1. 内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。
2. sigaction
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。(就是防止重复调用) 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都 把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。
pending位图,什么时候0--->1进行改变,执行信号递达的时候,调用handler方法之前就会清零
3 volatile
volatile 是 C/C++ 中的一个关键字 ,用于告诉编译器:这个变量的值可能会在程序"不知情"的情况下被改变 (比如被硬件、中断、其他线程修改),因此禁止对该变量进行某些优化,每次访问都必须从内存中重新读取,而不是使用寄存器中的缓存值

其实就是在优化条件下,flag变量可能直接被优化到cpu的寄存器中,那么你在程序中改变flag的时候(改变的是内存中的变量,而不是寄存器中的变量,因为优化,这个变量在判断的时候不会再去访问内存),判断可能会失败(只是一种可能a)
g++ -O(0/1/2/3) : 这个g++的一个-O选项,其实就是优化级别的选项,如-O0就是不做优化,-O3这个就是最高级别的优化
5. 重新了解可重入函数的概念。
一个函数在执行头插入的时候,信号来了,会导致一些指向问题
如图所示node2节点丢失,导致内存泄漏
所以一个函数,被重复进入的情况之下,出错误了,或者可能出错误,这个函数就叫做不可重入函数,否则就叫做可重入函数(我们学到的大部分函数都是不可重入函数)
6. 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制
子进程在退出的时候,不是静悄悄的退出
子进程在退出的时候,会主动向父进程发送SIGCHLD信号
所以父进程进程等待子进程的退出的时候,我们可以采用基于信号的方式进行等待
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进 程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父 进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号 的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理 函数中调用wait清理子进程即可。
请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数, 在其中调用wait获得子进程的退出状态并打印。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽 略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。









