第18章 Linux系统对中断的处理
18.1 进程、线程、中断的核心:栈
中断中断,中断谁?
中断当前正在运行的进程、线程。
进程、线程是什么?内核如何切换进程、线程、中断?
要理解这些概念,必须理解栈的作用。
18.1.1 18.1.1 ARM处理器程序运行的过程
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
- 对内存只有读、写指令
- 对于数据的运算是在CPU内部实现
- 使用RISC指令的CPU复杂度小一点,易于设计
比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:

细看这几个步骤,有些疑问:
- 读a,那么a的值读出来后保存在CPU里面哪里?
- 读b,那么b的值读出来后保存在CPU里面哪里?
- a+b的结果又保存在哪里?
我们需要深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。
注意:如果想入理解ARM处理器架构,应该从裸机开始学习。我们即将写好近30个裸机程序的文档,估计还3月底发布。
注意:为了加快学习速度,建议先不看裸机。

CPU运行时,先去取得指令,再执行指令:
- 把内存a的值读入CPU寄存器R0
- 把内存b的值读入CPU寄存器R1
- 把R0、R1累加,存入R0
- 把R0的值写入内存a
18.1.2 程序被中断时,怎么保存现场
从上图可知,CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。
保存在哪里?内存,这块内存就称之为栈。
程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。
这个场景并不局限于中断,下图可以概括程序A、B的切换过程,其他情况是类似的:

- 函数调用 :
- 在函数A里调用函数B,实际就是中断函数A的执行。
- 那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;
- 再去执行函数B;
- 函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。
- 中断处理
- 进程A正在执行,这时候发生了中断。
- CPU强制跳到中断异常向量地址去执行,
- 这时就需要保存进程A被中断瞬间的CPU寄存器值,
- 可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。
- 中断处理完毕,要继续运行进程A之前,恢复这些值。
- 进程切换
- 在所谓的多任务操作系统中,我们以为多个程序是同时运行的。
- 如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。
- 怎么切换?
- 切换过程是发生在内核态里的,跟中断的处理类似。
- 进程A的被切换瞬间的CPU寄存器值保存在某个地方;
- 恢复进程B之前保存的CPU寄存器值,这样就可以运行进程B了。
所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。进程的调度也是使用栈来保存、恢复现场:

18.1.3 进程、线程的概念
假设我们写一个音乐播放器,在播放音乐的同时会根据按键选择下一首歌。把事情简化为2件事:发送音频数据、读取按键。那可以这样写程序:
int main(int argc, char **argv)
{
int key;
while (1)
{
key = read_key();
if (key != -1)
{
switch (key)
{
case NEXT:
select_next_music(); // 在GUI选中下一首歌
break;
}
}
else
{
send_music();
}
}
return 0;
}
这个程序只有一条主线,读按键、播放音乐都是顺序执行。
无论按键是否被按下,read_key函数必须马上返回,否则会使得后续的send_music受到阻滞导致音乐播放不流畅。
读取按键、播放音乐能否分为两个程序进行?可以,但是开销太大:读按键的程序,要把按键通知播放音乐的程序,进程间通信的效率没那么高。
这时可以用多线程之编程,读 取按键是一个线程,播放音乐是另一个线程,它们之间可以通过全局变量传递数据,示意代码如下:
int g_key;
void key_thread_fn()
{
while (1)
{
g_key = read_key();
if (g_key != -1)
{
switch (g_key)
{
case NEXT:
select_next_music(); // 在GUI选中下一首歌
break;
}
}
}
}
void music_fn()
{
while (1)
{
if (g_key == STOP)
stop_music();
else
{
send_music();
}
}
}
int main(int argc, char **argv)
{
int key;
create_thread(key_thread_fn);
create_thread(music_fn);
while (1)
{
sleep(10);
}
return 0;
}
这样,按键的读取及GUI显示、音乐的播放,可以分开来,不必混杂在一起。
按键线程可以使用阻塞方式读取按键,无按键时是休眠的,这可以节省CPU资源。
音乐线程专注于音乐的播放和控制,不用理会按键的具体读取工作。并且这2个线程通过全局变量g_key传递数据,高效而简单。
在Linux中:资源分配的单位是进程,调度的单位是线程。
也就是说,在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。
而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。如下图示:

18.2 18.2 Linux系统对中断处理的演进
从2005年我接触Linux到现在15年了,Linux中断系统的变化并不大。比较重要的就是引入了threaded irq:使用内核线程来处理中断。
Linux系统中有硬件中断,也有软件中断。对硬件中断的处理有2个原则:不能嵌套,越快越好。
参考资料:
https://blog.csdn.net/myarrow/article/details/9287169
18.2.1 Linux对中断的扩展:硬件中断、软件中断
Linux系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
为方便理解,你可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:

注意:上图是简化的,Linux中这个数组复杂多了。
当发生A中断时,对应的irq_function_A函数被调用。硬件导致该函数被调用。
相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:

注意:上图是简化的,Linux中这个数组复杂多了。
问题来了:
- 软件中断何时生产?
由软件决定,对于X号软件中断,只需要把它的flag设置为1就表示发生了该中断。
- 软件中断何时处理?
软件中断嘛,并不是那么十万火急,有空再处理它好了。
什么时候有空?不能让它一直等吧?
Linux系统中,各种硬件中断频繁发生,至少定时器中断每10ms发生一次,那取个巧?
在处理完硬件中断后,再去处理软件中断?就这么办!
- 有哪些软件中断?
查内核源码include/linux/interrupt.h

怎么触发软件中断?最核心的函数是raise_softirq,简单地理解就是设置softirq_veq[nr]的标记位:

怎么设置软件中断的处理函数:
extern void open_softirq(int nr, void (*action) (struct soft_action*));

后面讲到的中断下半部tasklet就是使用软件中断实现的。