前几天我写了讲解 Linux 0.11 信号原理的文章,第48回 | 信号,从不知道信号的实现原理,到理解了它的原理,再到终写成文章,我只用了一个多小时的时间就搞定了。
当然不是说很深入的那种,但我觉得从不了解到了解并写成文章理顺了这个过程所花的时间,应该算是很短的了。
后来我复盘了一下,为什么我能在很短的时间完成这些事呢?
------
我理解并讲解信号原理的切入点是,为什么按下 CTRL + C 后程序就退出了。
首先我脑子里一定是先有了一个大概的判断,就是按下 CTRL + C 后,一定触发了某段程序,这个程序又给进程"发送"了一个叫信号这个概念的"东西",然后又一定有另一段程序,对"信号"做出了反应,使得进程退出。
关于按下 CTRL + C 后怎么触发了某段程序这一点,我在写 第42回 | 用键盘输入一条命令 时就已经搞清楚了这一点。
所以这块对我来说是没有任何障碍的。
直接顺藤摸瓜找到了我想看到的代码,就是给进程"发送"了一个叫信号这个概念的"东西"这段代码。
// kernel/chr_drv/tty_io.c
void tty_intr (struct tty_struct *tty, int mask) {
int i;
...
for (i = ; i < NR_TASKS; i++) {
if (task[i] && task[i]->pgrp == tty->pgrp) {
task[i]->signal |= mask;
}
}
}
这段代码中把进程 task_struct 结构中的 signal 中的某一位进行了改变。
这同样也非常好理解,因为在 第44回 | 进程的阻塞与唤醒 中就是通过修改 task_struct 中的 state 字段来改变了进程的状态,然后由另外一段程序通过读取这个状态来产生不同的行为。
此外,在 认认真真的聊聊"硬"中断 与 认认真真的聊聊"软"中断 中,硬中断与软中断的触发,与信号的触发是更为类似的,都是通过修改某一位的值,来达到似乎实时触发的效果。
所以这块对我来说是依然是没有额外的理解成本的。
再往后,不同信号的处理方式是不同的,这也和不同中断的处理方式是不同的是一个道理。中断的处理是寻找中断处理函数,那么信号的处理也一定是寻找信号处理函数,是不是很容易想象?
比如软中断通过软中断标志位确定是哪个软中断,再通过软中断向量表确定执行哪个软中断处理程序。
所以信号也一定是一样的,tast_struct 中的 signal 就是信号的标志位,那么一定有另一个类似信号向量表的东西,存储着信号处理函数。
这块的逻辑对我来说也是合理的猜测。
顺藤摸瓜,这个东西也存储在 tast_struct 中,叫 sigaction 数组,同样,通过处理信号的代码也可以佐证这一点。
// kernel/signal.c
void do_signal (long signr ...) {
...
struct sigaction *sa = current->sigaction + signr - 1;
sa_handler = (unsigned long) sa->sa_handler;
// 如果信号处理函数为空,则直接退出
if (!sa_handler) {
...
do_exit (1 << (signr - 1));
...
}
// 否则就跳转到信号处理函数的地方运行
*(&eip) = sa_handler;
...
}
可以看到,如果信号处理函数为空,那么就 do_exit 导致进程退出,如果不为空,就执行相应的处理函数。
所以按下 CTRL + C 导致退出,一定是因为信号处理函数为空导致的,这就基本解惑了。
那么,按下 CTRL + C 后触发的信号是什么,以及信号都有哪些种类,在写 第42回 | 用键盘输入一条命令 时提到了 UNIX 的 termios 标准,通过 cc_t 字段的类型以及 Linux 0.11 源码可知。
CTRL + C 表示 INTR 字符,而这个字符会触发 SIGINT 信号。
再通过这两张表格,我们还可扩展得知其它字符模式以及信号种类,这一块知识体系就慢慢建立起来了。
还有很多其它的细节,比如执行信号处理函数的方式,是通过给 eip 寄存器赋值。
// kernel/signal.c
void do_signal (long signr ...) {
...
*(&eip) = sa_handler;
...
}
这和 execve 变换到一个新程序运行的方式是一样的,这个在 第35回 | execve 加载并执行 shell 程序 时解释 execve 原理的时候就讲过了。
再比如,信号和管道都属于进程间通信的一种方式,而管道的原理,我通过 第45回 | 解析并执行 shell 命令 时就讲到了,因此相当于对和信号这个概念平级的一个概念有了提前的了解。
等等等等,还有很多。
------
所以,我想说的是,为什么学习底层知识对上层知识有帮助,为什么基本功扎实了学习新东西的速度会变快。
很多人不相信这个道理,总觉得我知道操作系统原理了,对我理解 SpringBoot 能有啥帮助。
这个问题确实不好解释,也没必要解释。你看我之所以理解信号的原理这么快,正是因为我已经对键盘输入、中断、管道、进程调度、execve 等等和信号看似没有太大关系的知识有所了解。
这些周边知识,有的是直接帮助理解信号,比如键盘输入流程,我就不用因为要学信号而重新看一遍了。有的是间接帮助,比如软中断的流程和原理和信号有很大相似的部分,这就加速了我对信号的理解。
当然还有很重要的原因,是我通过不断翻看 Linux 0.11 源码,已经对其各个部分很熟悉了,所以能够快速定位到我想看的逻辑,并快速跳过我已经知道的代码,看未知的部分。
所以,为什么你学得比别人慢?我还是没有给出直接正面的回答,你心里有答案了么?
这个道理,也影响了我的生活,我们总认为这个没用,那个没用,其实我们看到的很多都是没有直接作用,但是,往往终起关键作用的,正是那些参与间接帮助的事情。
对于这点,我现在深信不疑。