Linux内核设计与实现ch5-7 系统调用
Linux内核设计与实现第五章—系统调用
5.1 与内核通讯
中间层位于用户空间进程与硬件设备之间,由系统调用添加。该层的作用主要有三个
- 为用户空间提供硬件的抽象接口,可以对用户屏蔽不同硬件之间的差异
- 保证了系统的稳定性与安全性,内核可以基于权限,用户类型等对需要进行的访问进行裁决,防止它做出对系统有危害的事情
- 为运行在虚拟内存中的进程提供统一接口
系统调用是用户除了系统异常和陷入外访问内核的唯一手段和唯一合法入口。
5.3 系统调用
系统调用通常通过程序在C 库中的函数调用来进行。内核并没有规定这些调用要如何实现。只是希望最后的结果正确就可以了。
系统调用的get_pid()在内核中被定义为sys_getpid(),这是Linux中所有系统调用都应该遵守的规则。例如bar()也实现为sys_bar()。
5.3.1 系统调用号
Linux中的每个系统都被分配了一个系统调用号。当用户空间的进程执行一个系统调用的时候,系统调用号就可以用来指明要执行哪个系统调用;进程不会提及系统调用的名称。
系统调用号相当重要,一旦分配后就不能有任何变更。否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除。它占用的系统调用号也不能被回收。否则一些其他已经编译的代码会调用这些系统调用,但实际上却获得了另外一个系统调用。为了处理系统调用被删除的情况,Linux有一个”未实现的“系统调用sys_ni_syscall
函数。当无效的系统调用产生时,就会调用这个函数,并且只会返回-ENOSYS
。
内核会产生一个记录所有已注册过的系统调用的列表。存储在sys_call_table
中。每种体系结构中都明确定义了这个表。在x86-64中,这个表位于/arch/i386/kernel/syscall_64.c
中,主要为每个有效的系统调用指定唯一的系统调用号。
5.3.2 系统调用的性能
Linux系统调用比其他许多系统执行的要快。主要原因在于Linux的上下文切换时间很短。进出内核得益于此都变得简洁高效。另一个原因也有系统调用处理程序和每个系统调用本身都非常整洁的原因。
5.4 系统调用处理程序
由于内核驻留在受保护的地址空间上,所以进程无法(也不应该)直接调用内核空间中的函数。那么就需要有一种方式使得应用程序能够通知系统,告诉内核执行一个系统调用,并且希望系统切换到内核态。这样内核就可以代表用户程序在内核空间内执行系统调用。
通知内核的机制依靠软中断来实现。应用程序通过引发一个异常使系统切换到内核态,并且执行异常处理程序。这里的异常处理程序就是系统调用处理程序。
x86的软中断的中断号为128。通过int $0x80来触发中断。这个系统处理程序称为system_call()
5.4.1 指定恰当的系统调用
由于所有的系统中断陷入方式都相同。所以还需要通过系统调用号才能知道具体要执行什么系统调用。x86上的系统调用号使通过eax寄存器来传递给内核的。这个数据由应用程序在执行系统调用之前放入。这样系统调用一旦执行,就可以通过eax上的系统调用号来执行对应的系统调用。
系统调用表项是以64位类型存放的。所以内核需要将给定的系统调用号乘四就能找到表中的位置。
5.4.2 传递参数
系统调用的参数与常规函数调用时传参方式相同,前六个参数分别保存在寄存器%rdi、%rsi、%rdx、%rcx、%r8、%r9中,剩余的参数保存在栈上。
5.5 系统调用的实现
记住Linux的格言:提供机制而非策略
5.5.1 实现系统调用
实现系统调用的时候需要注意以下几点
- 决定系统调用需要做什么,Linux不提倡采用多用途的系统调用
- 参数、返回值和错误码都应该是什么,接口应该力求简洁,参数尽可能少
- 考虑向前兼容和未来发展(系统的可移植性,是否能向前兼容,是不是对函数做了不必要的限制)
5.5.2 参数验证
为了系统的安全性与稳定性,系统调用必须仔细检查他们所有的参数是否合法有效。(比如进程的PID,IO的文件描述符等等)进程不应该让内核访问它无权访问的资源。
最重要的是检查用户提供的指针是否有效。在接受用户空间的指针之前,内核必须保证:
- 指针指向的内存区域属于用户空间。进程绝不能哄骗内核去读内核空间数据
- 指针指向的内存区域属于进程的地址空间里。进程不能哄骗内核去读其他进程的数据
- 进程不能绕过内存访问的限制,应该与内存标记的读,写,执行相同
内核提供了两种方法来完成必须的检查和内核空间与用户空间之间来回的内存拷贝。两种方法中必须经常有一个被使用。
向用户空间写入数据的
copy_to_user()
。需要三个参数:进程空间中目的内存地址,内核空间的源地址,拷贝所需长度。从用户空间读出数据的
copy_from_user()
。参数与上一个函数相同。
执行失败时返回的是未能完成拷贝的字节数,成功返回0。出现错误时,系统调用返回标准EFAULT
要注意,这两个函数都可能会引起堵塞。比如包含用户数据的页被换出到硬盘上之后。此时进程会休眠,直到缺页处理将该页重新换入内存。
最后一项检查是检查合法权限。内核提供了函数capable()
来进行检查。例如capable(CAP_SYS_NICE)
可以检查调用者是否有权修改其他进程的nice值。
include/linux/capability.h中包含一份所有这些权能及其对应的权限列表。
5.6 系统调用上下文
由于内核可以被休眠可以被抢占。所以说系统调用也像用户空间的进程一样可以被其他进程抢占。在新的进程中一样可以执行新的系统调用。所以必须要小心来保证系统调用是可重入的。这也是对称多处理中同样要关心的问题。
系统调用返回至用户态的时候,控制权还在system_call()
中。它最终会负责切换到用户空间,让用户进程继续执行下去。
5.6.1 绑定一个系统调用的最后步骤
编写一个系统调用后,我们总需要将其注册成为一个正式的系统调用。这个步骤如下
- 在系统调用表的最后增加一个表项,对每种支持该系统的硬件体系均需要做这样的操作。(一般位于entry.s中)
- 对于支持的各种体系结构,系统调用号必须定义在
<asm/unistd.h>
中。 - 系统调用只能被编译为内核映像而非模块。需要将它放入
kernel/
下的文件中。
5.6.2 从用户空间访问系统调用
通常来讲,系统调用依靠C库来支持,但如果仅仅写出系统调用,C库是不做支持的。
好在LInux本身提供了一组用来对系统调用进行访问的宏。它会设置好寄存器并调用陷入指令。这个宏是_syscalln()
,这里的n的范围是0-6,用来表示需要传递给系统调用的参数个数。参数个数要按照次序来书写。以下以open()为例举个例子:
1 | //正常来说open()的定义是这样的 |
对于每个宏来说,都具有2+2*n
个参数。第一个对应了系统调用的返回值类型,第二个是系统调用的名称。后面是按顺序的每个形参的类型和名称。_NR_open
在asm/unistd.h
中定义,为系统调用号
5.6.3 为什么不通过系统调用的功能实现某些功能
新建系统调用的好处
- 容易,使用方便
- 性能高
但是需要面临如下问题:
- 系统调用号由于其普遍适用性,需要内核处于开发版本时由官方分配
- 稳定内核中的系统调用会被固化,为了避免应用程序崩溃,它的接口不允许被改动
- 每个支持的体系结构均需要对系统调用进行注册
- 系统调用无法通过脚本或者文件系统中直接访问。
- 仅仅是为了信息交换,系统调用过于大材小用。
所以说想要实现一个设备节点,作为系统调用的替代,需要实现read()、write()
函数,使用ioctl()
对特定的设置进行操作或者对特定信息进行检索。
- 信号量这样的接口,可以用文件描述符来表示
- 将增加的信息作为文件放入sysfs的合适位置
Linux内核设计与实现第六章—内核数据结构
这章由于比较基础。所以仅介绍与传统实现中不太相似的地方。其余的API referance请参考书上或者详见内核api文档
6.1 链表
Linux2.1的内核中,官方首次引入了内核链表的实现。自此内核中所有的链表都在官方链表中实现出来。该代码处于头文件<linux/types.h>
中。它的实现方式与众不同。并非将数据结构塞入链表,而是将链表塞入数据结构。
其数据结构定义如下:
1 | struct list_head { |
那具体怎么使用呢?这里举个小例子
1 | struct list{ |
这样就构成了一个链表。虽然现在链表已经可用了,但是还不太方便。所幸在<linux/list.h>
中还有一组API可供使用。他们都只接受list_head
结构作为参数。所以为了从指针找到父结构中所包含的变量,我们可以使用container_of()
宏来达到。所以内核中简单定义了一个函数来返回list_head
的父类型结构体。
1 | /** |
还有一个需要注意的地方是链表的遍历。内核中提供了几个宏来实现链表的遍历操作
list_for_each_entry(pos, head, member)
,pos是一个指向list_head
的指针,可以看为list_entry
的返回值,head是指向头节点的指针,也就是遍历开始位置。举一个实际的例子(来自inotify—内核文件系统更新通知机制):
1 | static struct inotify_watch *inode_find_handle(struct inode *inode, struct inotify_handle *ih) |
但是在这个遍历的同时是不能删除的。删除需要用另外一个api list_for_each_entry_safe(pos, next, head, member)
.
6.2 队列
队列的操作位于lib/kfifo.c
中,头文件在include/linux/kfifo.h
中
6.3 映射
Linux提供了映射这种键到值的关系,映射支持三个操作
1 | Add(key, value) |
与C++的map操作不同,Linux并非使用了自平衡二叉搜索树来进行实现,而是映射了一个唯一表示数(UID)到一个指针。
6.4 红黑树
红黑树是一种二叉自平衡树。也是Linux中主要的平衡二叉树结构。红黑树具有特殊的着色属性,红色/黑色。因遵守着下面几个特此那个,所以能维持半平衡结构。
- 所有的节点要么红色,要么黑色
- 叶子节点都是黑色
- 叶子节点不包含数据
- 所有非叶子节点都有两个子节点
- 如果一个节点是红色的,那么他的子节点都是黑色的
- 在一个节点到其叶子节点的路径中,如果总是包含同样数目的黑色节点,则该路径相比其他路径是最短的。
上述条件保证了最深的叶子节点深度不会大于两倍最浅叶子节点深度。所以黑红树总是半平衡的。
rbtree的实现没有提供搜索和插入例程,这些例程希望用户自己定义。
Linux内核设计与实现第七章—中断和中断处理
7.1 中断
中断使得硬件得以发出给处理器。中断本质是一种特殊的电信号,由硬件设备发送给处理器,处理器接收到中断后,会向操作系统反应信号的到来,然后就由操作系统来处理这些数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步,也就是说中断随时可能发生。
不同设备对应的中断不同。每个中断都有唯一的一个数字标志,从而使得操作系统能够对中断加以区分,并且知道哪个硬件设备产生了哪个中断,这样操作系统才能给不同的中断提供对应的处理程序。
中断值通常被称为中断请求(IRQ)线。每个IRQ线都会关联一个数值——-例如经典的PC机上IRQ0是时钟中断,IRQ1是键盘中断。但并非所有中断信号均如此严格定义。在PCI总线上的设备就是动态分配的。一些非PC的系统结构也存在动态分配中断的特性。特定的中断与特定的设备关联,并且内核要知道这些信息。
异常
讨论中断就不能不提起异常。异常与中断不同,在产生时就必须要考虑时钟同步。实际上,异常也常常被称为同步中断。在处理器执行到由于变成失误导致的错误指令,或者执行期间出现特殊情况(缺页)时,处理器就会产生一个异常。很多处理器处理异常和中断的手段类似,所以内核对他们的处理也很类似。
第五章中通过软中断实现系统调用就是一种异常(系统调用处理程序的异常)。中断的工作方式与之类似,其差异只是在于中断由硬件而非软件引起。
7.2 中断处理程序
在处理中断的时候,内核会执行一个函数。该函数名为中断处理程序(interrupt handler)
,或者叫中断服务例程(interrupt service routine, ISR)
。产生中断的每个设备都有一个相应的中断处理程序。这个程序是它设备驱动的一部分。
中断程序与普通的函数不同的方面在于,中断程序要按照特定的类型进行声明,以便内核能够以标准的方式传递处理信息。
中断处理程序不是和特定设备关联,而是和特定中断关联的。如果一个设备可以产生多种不同的中断,那么该设备就对应多个中断处理程序,相应的,设备的驱动程序也就需要准备多个这样的函数。
7.3 上半部分与下半部分对比
中断程序的运行速度与其能完成的工作量是抵触的。所以为了分析到这两个矛盾的均衡点,我们将中断处理切分为两个部分:中断处理程序(top half),接收到中断后就开始执行。但只进行有严格时限的工作,例如对接受的中断进行应答或者复位硬件。能够稍微推迟的工作都会被推迟到下半部分去。此后在适当的时机,下半部分会被开中断执行。
以网卡为例,网卡接收到来自网络的数据包时需要通知内核。为例优化网络吞吐量和传输周期,避免超时,网卡需要立即中断,并希望内核快速应答。这时中断开始执行,通知硬件拷贝网络数据包到内存,然后读取网卡更多的数据包。这些任务是很紧急的。因为网卡的缓冲区固定,所以拷贝工作一旦延迟,就会导致网卡缓存溢出,后续到来的报文只能丢弃。当网络数据包拷贝到内存上后,中断的任务也就完成了。这时可以将控制权交还给中断原来运行的程序。处理数据包的工作可以在下半部运行。(下半部在第八章详述)
7.4 注册中断处理程序
驱动程序可以通过request_irq()
函数注册一个中断处理程序(声明在linux/interrupt.h
中)。并且激活给定的中断线,以处理中断:
1 | static inline int __must_check |
第一个形参表示要分配的中断号。对于大多数设备来说,这个值或者是探测获取,或者通过编程动态决定。
第二个形参irq_handler_t
是一个指针,指向实际的中断处理程序。只要操作系统接收到中断,该函数就被调用。handler
的函数接受两个形参并具有返回值,我们将在后面讨论。
7.4.1 中断处理程序标志
第三个参数flags
可以为0,或者为interrupt.h
中的一个或多个标志位掩码。这些标志中最重要的是:
IRQF_DISABLED——该标志在设置后,要求内核在处理中断应用程序本身期间要禁止其他所有的中断 。多数中断进程不会设置该位
IRQF_SAMPLE_RANDOM——该标志表明设备产生的中断对内核墒池有贡献,内核墒池提供从各种事件导出的随机数。若指定该标志,则该设备的中断时间间隔就会作为墒填入到墒池。如果设备产生中断的速率不可预知,那么可以成为一个很好的墒源
IRQF_TIMER——该标志是特别为系统定时器中断处理准备的。
IRQF_SHARED——标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定该标志,否则每条线上只能有一个处理程序。
第四个参数name
是与中断相关设备的ASCII文本表示。
第五个参数dev
用来共享中断线。当一个中断处理程序需要释放时,dev将提供唯一的标志信息cookie
,以便从共享中断线的诸多中断处理程序中删除指定的那一个。无需共享时设为NULL
即可。另外,内核每次调用中断处理程序时都会将这个指针传给它。这个指针是唯一的,能在中断处理程序中被用到。
request_irq()
成功执行会返回0,若返回非0值,则证明有错误发生。该种情况下,指定的中断处理程序不会被注册。该函数也可能会睡眠,所以不能在中断上下文或者其他不允许阻塞的代码中调用该函数。
在注册的过程中,内核需要在
/proc/irq
文件中创建一个与中断对应的选项。调用的是proc_mkdir()
来实现。该函数调用proc_create()
对新的profs项进行设置。而proc_create()
会调用函数kmalloc()
分配内存。而函数kmalloc()
是可以睡眠的。这也就是request_irq()
函数会导致堵塞的原因。
7.4.2 释放中断处理程序
卸载驱动程序时需要同时卸载相应的中断处理程序,并释放中断线。
1 | void free_irq(unsigned int irq, void *dev) |
如果中断线没有共享,那么函数删除处理程序的同时会将中断线禁用。如果中断线共享,则仅删除dev
对应的处理程序。而对于共享中断线,需要唯一信息来区分上面的多个处理程序,不管哪种情况下,如果dev
非空,它都需要删除与其匹配的处理程序
7.5 编写中断程序
7.5.1 共享的中断处理程序
共享的处理程序与非共享的处理程序在注册和运行方式上比较相似,其差异主要有三处:
request_irq()
的参数flags
必须设置为IRQF_SHARED
- 对于每个注册的中断处理程序来说,dev参数必须唯一。指向任意结构设备指针就可以满足其要求:但通常会选择设备结构。
- 中断处理程序必须能够区分设备是否真的产生中断,这需要硬件以及处理程序中相关逻辑的支持。如果硬件不支持这一功能,中断处理程序会束手无策。它没办法知道是对应设备发出中断,还是共享中断线的其他设备发出中断。
所有共享中断线的程序都必须满足以上要求。只要有一个设备没有按规则共享,那么中断线就无法共享。
7.5.2 中断处理程序实例
(参考价值不大,掠过)
7.6 中断上下文
当执行一个中断时,内核处于中断上下文中,中断上下文与进程上下文不同。进程上下文可以睡眠,也可以调用调度程序。而中断上下文与进程没有什么瓜葛。与current宏不相关。因为没有后备进程,所以中断上下文不可以被睡眠。因此不能从中断上下文调用一些函数。
中断上下文具有比较严格的时间限制,因为它打断了其他代码(甚至可能是其他中断线的另一中断程序),所以中断上下文应迅速,简洁。尽量不要用循环去处理繁重的工作。这一点非常重要。因此要将下半部分分离,在更适合的情况下执行。
中断处理程序栈的设置是一个配置选项。从2.6内核开始,可以配置将内核栈的大小从两页减少到一页,以减轻内存压力。所以中断处理程序要尽量节省内核栈空间。
7.7 中断处理机制的实现
中断处理十分依赖体系结构。当设备产生中断时,会通过总线将信号发送给中断控制器,如果中断总线时激活的,那么中断控制器就会将中断发送给处理器。(在大多体系结构中,这一工作就是在某一特定管脚发送一个电平信号。)处理器接受到中断信号后会立刻停止自己正在做的事情并且关闭中断系统,跳跃到中断处理程序的入口点开始执行代码。
对于每条中断线,处理器都会跳到对应的一个唯一位置,这样内核就能知道所接收中断的IRQ号了。初始入口点只是在栈中保存该中断号,并存放当前寄存器的值。然后内核调用函数do_IRQ()
。从这里开始执行中断。这个函数的声明为unsigned int do_IRQ(struct pt_regs regs)
因为C的调用惯例是将函数参数放在栈顶,因此pt_regs
结构包含了原始寄存器。这些值是以前在汇编入口例程中保存在栈中的。中断的值也会得到保存。所以do_IRQ()
可以将这些提取出来。
提取出中断号后,do_IRQ()
对接受的中断进行应答,通过mask_and_ack_8259A()
来完成的。然后调用handle_irq_event()
来运行中断线所安装的中断处理程。具体流程看下面三个函数。
1 | irqreturn_t handle_irq_event(struct irq_desc *desc) |
在7.4.1中我们提到过,IRQF_DISABLE
表示处理程序必须在中断禁止的情况下运行。因为处理器禁止中断,这里需要将他们打开。每个潜在的处理程序通过for_each_action_of_desc
依次执行,如果这条线不是共享的,第一次执行后就会退出循环,否则所有的处理程序都要被执行。之后调用函数add_interrupt_randomness
去处理循环中出现的flags。(这个函数主要适用了中断时间间隔为随机数产生器产生熵)。这个函数返回到do_IRQ
函数中,函数清理工作并发挥到初始入口点,然后再从这个入口点跳到函数ret_from_intr()
。
ret_from_intr()
例程类似于初始入口代码,使用汇编语言编写,例程检查重新调度是否正在挂起(也就是设置了need_resched
)。如果重新调度正在挂起,并且内核正在返回用户空间(也就是用户进程被中断)。那么schedule()
被调用。如果内核本身被中断,也就是内核在返回内核空间,只有当preempt_count=0
时,schedule()
才会被调用。否则抢占内核就是不安全的。
在schedule()
返回后,如果没有挂起的工作,那么原来的寄存器就被恢复,内核恢复到曾经中断的点。
在x86上,初始的汇编例程位于
arch/x86/kernel/entry_64.S
(32位位于entry_32.S
),C方法位于arch/x86/kernel/irq.c
其他所支持的接口与此类似。
7.8 /proc/interrupts
procfs
是一个虚拟的文件系统,它只存在于内核的内存,安装在/proc目录,在procfs
中读写文件都要调用内核函数,用于模拟真实文件中的读和写。下面来看看/proc/interrupts文件,这个文件中存放的是系统中与中断相关的信息。下面是本机输出的信息:
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 CPU8 CPU9 CPU10 CPU11
8: 0 0 0 0 0 0 0 0 0 0 0 0 IO-APIC 8-edge rtc0
9: 0 0 0 0 0 0 0 0 0 0 0 0 IO-APIC 9-fasteoi acpi
24: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 805306368-edge virtio0-config
25: 0 0 0 211 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 805306369-edge virtio0-requests
26: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 1073741824-edge virtio1-config
27: 0 0 0 0 0 284035 0 0 0 0 0 0 Hyper-V PCIe MSI 1073741825-edge virtio1-requests
28: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 1610612736-edge virtio2-config
29: 0 0 0 0 0 0 0 4381354 0 0 0 0 Hyper-V PCIe MSI 1610612737-edge virtio2-requests
30: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 1744830464-edge virtio3-config
31: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 1744830465-edge virtio3-requests
32: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI -2147483648-edge virtio4-config
33: 0 0 0 0 0 0 0 0 0 0 0 3 Hyper-V PCIe MSI -2147483647-edge virtio4-requests
34: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 1744830464-edge virtio5-config
35: 0 3 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 1744830465-edge virtio5-requests
36: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 268435456-edge virtio6-config
37: 0 0 0 3 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI 268435457-edge virtio6-requests
38: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI -536870912-edge virtio7-config
39: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V PCIe MSI -536870911-edge virtio7-requests
NMI: 0 0 0 0 0 0 0 0 0 0 0 0 Non-maskable interrupts
LOC: 0 0 0 0 0 0 0 0 0 0 0 0 Local timer interrupts
SPU: 0 0 0 0 0 0 0 0 0 0 0 0 Spurious interrupts
PMI: 0 0 0 0 0 0 0 0 0 0 0 0 Performance monitoring interrupts
IWI: 0 0 0 0 0 0 0 0 0 0 0 0 IRQ work interrupts
RTR: 0 0 0 0 0 0 0 0 0 0 0 0 APIC ICR read retries
RES: 238566 160419 826807 87772 133205 292473 1405401 4125 300538 17385 1331221 127239 Rescheduling interrupts
CAL: 16282 553 334 193 172 445 229 170 272 176 536 166 Function call interrupts
TLB: 0 0 0 0 0 0 0 0 0 0 0 0 TLB shootdowns
HYP: 85674 1242 304 1427827 2050789 13810 83381 167424 20897 93224 13809 0 Hypervisor callback interrupts
HRE: 0 0 0 0 0 0 0 0 0 0 0 0 Hyper-V reenlightenment interrupts
HVS: 98089 1939 67599 245456 408649 535351 67820 44652 991468 5533 956024 3248 Hyper-V stimer0 interrupts
ERR: 0
MIS: 0
PIN: 0 0 0 0 0 0 0 0 0 0 0 0 Posted-interrupt notification event
NPI: 0 0 0 0 0 0 0 0 0 0 0 0 Nested posted-interrupt event
PIW: 0 0 0 0 0 0 0 0 0 0 0 0 Posted-interrupt wakeup event
第一列是中断线,第二列开始是每个CPU接受了中断数目的计数器,中断计数器后面标识了处理这个中断的中断控制器。最后一列则是产生中断的设备。这个名字是通过参数devname
提供给函数request_irq()
的。如果中断是共享的,则中断线上注册的所有设备都会列出来。
procfs的代码位于fs/proc中,这些函数与体系结构相关。其中提供/proc/interrupts的函数叫做show_interrupts。
7.9 中断控制
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口提供了能够禁止当前处理器的中断系统或者屏蔽一整条中断线的能力。这些接口位于<asm/system.h>
以及<asm/irq.h>
中。
一般来说,中断控制系统归根结底是要提供同步。通过禁止中断来保证某个中断处理程序不会抢占当前代码。此外也可以用禁止中断来禁止内核抢占。但是这两方面都没有提供防止来自其他处理器的并发访问的保护机制。所以为了避免其他处理器访问共享数据,内核还需要获取某种锁,这种锁要提供本地以及其他中断程序的并发访问。
7.9.1 禁止和激活中断
1 | local_irq_disable(); |
这两个函数通常使用对应体系的汇编函数实现。实际上基本上是对clear和set允许中断标志的汇编调用。在发出中断的处理器上它们将禁止和激活中断的传递。
如果在调用local_irq_disable()
之前已经禁止了中断,那么无条件的禁止中断可能会带来风险。对于某些情况来说,更需要内核提供一种恢复中断的机制。也就是要将中断状态保存,在特定情况下将其恢复。幸运的是,内核中也提供了相应的方式来将内核恢复到原来的状态。
1 | local_irq_save(flags); |
上面这两种方法至少部分要使用宏的方式实现,所以在我们看到的形参是传值而非传址的。该参数包含具体体系结构的数据(也就是中断的状态)。flag值必须驻留在同一栈帧中。所以对local_irq_save(flags)
和local_irq_restore(flags)
的调用必须在统一函数中进行。
7.9.2 禁止指定中断栈
有些情况下,我们并不需要禁止整个处理器上所有的中断,而是只需要禁用整个系统中的某一条中断线。(例如想在对中断状态操作之前禁止设备终端的传递)。为此,Linux提供了以下几种接口。
1 | void disable_irq(unsigned int irq); |
前两个函数禁止中断控制器上指定的中断线,即禁止给定中断向系统中所有处理器的传递。另外,函数只有在当前正在执行的所有处理程序完成后,disable_irq
才可以返回。所以说,调用者不仅要确保不再指定线上传递新的中断,同时还要确保所有已经开始执行的处理程序全部退出。而函数disable_irq_nosync()
则不会等待当前中断处理程序执行完毕。
函数synchornize_irq()
等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。
对这些函数的调用可以嵌套。但在一条指定的中断线上,对disable_irq
和disable_irq_nosync
的每次调用,都要相应的调用一次enable_irq()
。只有最后一次对enable_irq()
的调用完成后,才真正重新激活了中断线。
这三个函数可以从中断或进程上下文中调用,而且不会睡眠,但如果从中断上下文调用的时候就要小心。防止处理程序的中断线在进行处理时被屏蔽。
禁止中断线相当于禁止了线上所有设备的中断传递,因此,使用新设备的驱动程序应该倾向于不使用这些接口,特别是PCI总线,所有设备应该共享中断。所以根本不应该用这些接口。
7.9.3 中断系统的状态
Linux为了解中断系统的状态提供了几种接口:
函数 | 说明 |
---|---|
irqs_disable() | 本地处理器的中断系统被禁止时返回非0 |
in_interrupt() | 内核处于任何形式的中断时返回非0(中断处理或者下半部都算) |
in_irq() | 内核确实在执行中断处理程序中时返回非零 |