Linux源代码—中断

来源:互联网 发布:手机视频剪辑合并软件 编辑:程序博客网 时间:2024/05/29 03:18

(本文部分图文参考了《Linux内核源代码情景分析》)
本文主要讨论中断基本过程。主要内容如下:
1、前期知识介绍:中断向量表;
2、中断响应过程;
3、系统调用;

1、中断向量表
1.1
这里写图片描述
这里写图片描述
这里写图片描述
1.2 CPU中一共有四种门:任务门,中断门,陷阱门,调用门
1.2.1 任务门
这里写图片描述
1.2.2 除任务门外,其他三种门结构基本相同,均为64位。与任务门不同的是:在任务门中不需要使用段内位移,因为任务门并不指向某一个子程序的入口,TSS本身是作为一个段来对待的,而中断门、陷阱门和调用门则都是要指向一个子程序,所以必须结合使用段选择码和段内位移。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

1.2.3 保护模式下中断机制采取中断门或陷阱门时的结构:
这里写图片描述
2 中断响应过程
这里写图片描述
此时,随CPU返回do_IEQ():
{

/* 结束后,会返回ret_from_intr函数. */
return 1;
}
到 现在,从逻辑的角度说对中断请求的服务似乎已经完毕,可以返回了。
可是 Linux 内核在这里有个特殊的考虑,这就是所谓 softirq,即“ (在时间上)软性的中断请求”,以前称为“ bottom half”。
在 Linux 中,设备驱动程序的设计人员可以将中断服务分成两“部分”。第一部分是必须立即执行,一般是在关中断条件下执行的,并且必须是对每次请求都单独执行的。而另一部分,即“后半” 部分,是可以稍后在开中断条件下执行的,并且往往可以将若干次中断服务程序中剩下来的部分合并起来执行。这些操作往往是比较费时的,因而不适宜在关中断条件下执行,或者不适宜一次占据 CPU 时间太长而影响对其他中断请求的服务。这就是所谓的“后半”,在内核代码中常简称为 bh。
执行 bh 的机制是内核中的一项“基础设施”,此处不多介绍,只需要知道有这部分即可。在 do_softirq()中执行完相关的 bh 函数(如果有的话)以后,就会从 do_IRQ()返回到 ret_from_intr函数;
在ret_from_intr函数中:

00273: ENTRY(ret_from_intr)00274: GET_CURRENT(%ebx)00275: movl EFLAGS(%esp),%eax # mix EFLAGS and CS00276: movb CS(%esp),%al00277: testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor?00278: jne ret_with_reschedule00279: jmp restore_all00280:

这里的 GET_CURRENT( %ebx)将指向当前进程的 task_struct 结构的指针置入寄存器 EBX。 275行和 276 行则在寄存器 EAX 中拼凑起由中断前夕寄存器 EFLAGS 的高 16 位和代码段寄存器 CS 的( 16位)内容构成的 32 位长整数。其目的是要检验:中断前夕 CPU 是否运行于 VM86 模式; 中断前夕 CPU 运行于用户空间还是系统空间。

如果中断发生于系统空间,控制就直接转移到 restore_all,而如果发生于用户空间(或 VM86 模式)则转移到 ret_with_reschedule。这里我们假定中断发生于用户空间,因为从 ret_with_reschedule 最终还会到达 restore_all。
需要注意的是,在ret_with_reschedule中还会再检查是否需要进行进程调度(如果当前进程的 task_struct 结构中的 need_resched 字段为非 0,即表示需要进行调度)。同样,如果当前进程的 task_struct 结构中的 sigpending 字段为非 0,就表示该进程有“信号”等待处理,要先处理了这些待处理的信号才最后从中断返回,处理完信号以后,控制还是restore_all。实际上, ret_from_sys_call 最后还回到 ret_from_intr,最终殊途同归都会到达 restore_all,并从那里执行中断返回。
最后进行出栈操作:宏操作 RESTORE_ALL 的定义在同一文件( entry.S)中:

00101: #define RESTORE_ALL \00102: popl %ebx; \00103: popl %ecx; \00104: popl %edx; \00105: popl %esi; \00106: popl %edi; \00107: popl %ebp; \00108: popl %eax; \00109: 1: popl %ds; \00110: 2: popl %es; \00111: addl $4,%esp; \00112: 3: iret; \

显然,这是与进入内核时执行的宏操作 SAVE_ALL 遥相对应的。

这样,当 CPU 到达 iret 指令时,系统堆栈又恢复到刚进入中断门时的状态,而 iret 则使 CPU从中断返回。根进入中断时相对应,如果是从系统态返回到用户态就会将当前堆栈切换到用户堆栈。
此时,中断的一系列动作彻底完成。

3 系统调用
系统调用就是 CPU 主动地、同步地进入系统空间的手段。这里所谓“主动”,是指 CPU“自愿”的、事先计划好了的行为。而“同步”则是说, CPU(确切地知道在执行哪一条指令以后就一定会进入系统空间。
在使 CPU的运行状态从用户态转入系统态,也就是从用户空间转入系统空间这一点上系统调用和中断是一致的。当然,中断有可能发生在 CPU 已经运行在系统空间的时候,而系统调用却只发生于用户空间,这是二者不同之处。
Linux下系统调用时通过指令int 0x80实现的,本文以系统调用int sethostname(const char *name, size_t len)(设置计算机在网络中的主机名)为例讲述。

3.1 sethostname()实际上是一个库函数(在 usr/lib/libc.a 中),而实际的系统调用就是此函数中发出的。该函数汇编代码如下:

00034: 00000000 <sethostname>:00035: 0: 89 da movl %ebx, %edx/**进入函数 sethostname()以后,堆栈指针%esp 指向返回地址*堆栈指针的内容加 4 的地方是调用该函数时的第一个参数( name),*加 8 的地方为第二个参数 len,依此类推*指令“ movl 0x8(%esp,1), %ecx”表示将相对于寄存器%esp 的位移位 0x8(位移单位为 1)处的内*容*存入寄存器%ecx*/00036: 2: 8b 4c 24 08 movl 0x8(%esp,1), %ecx00037: 6: 8b 5c 24 04 movl 0x4(%esp,1), %ebx/*将代表 sethostname()的系统调用号 0x4a 存入寄存器%eax */00038: a: b8 4a 00 00 00 movl $0x4a, %eax00039: f: cd 80 int $0x80/*此时已从系统调用返回*首先是从%edx 中恢复%ebx 原先的内容(系统调用之前已保存在%edx*检查系统调用的返回值(在寄存器%eax 中)。*如%eax 中的内容是 0xfffff0010xffffffff 之间,*也就是-1 至-4095 之间,那就是出错了,就要转向__syscall_error()并从那里返回*/00040: 11: 89 d3 movl %edx, %ebx00041: 13: 3d 01 f0 ff ff cmpl $0xfffff001, %eax/**la: R_386_PC32 表示地址 sethostname+0x1a 处为重定位信息,*在连接时会把地址__syscall_error()填入该处*/00042: 18: 0f 83 fc ff ff jae la<sethostname+0x1a>00043: 1d: ff00044: 1a: R_386_PC32 __syscall_error00045: 1e: c3 ret

注意:1、Linux 内核在系统调用时是通过寄存器而不是通过堆栈传的参数的。这是因为进入系统调用时会切换堆栈,用栈传参比较费时。
2、SAVE_ALL 中可以看到被压入堆栈的寄存器依次为: %es、%ds、 %eax、 %ebp、 %edi、 %esi、 %edx、 %ecx 和%ebx。这里的%eax 持有系统调用号(与 orig_ax 相同),显然不能用来传的参数;而%ebp 是用作子程序调用过程中的“帧”( frame)指针的,也不能用来传的参数。这样,实际上就只有最后 5 个寄存器可以用来传递参数,所以,在系统调用中独立传递的参数不能超过 5 个。

3.2 在__syscall_error 中,先取出%eax 的内容的负值,使其数值变成 1~4095 之间,这就是出错代码,并将其压入堆栈。接着,又调用__errno_location(),将全局量 errno 的地址取入%eax。然后从堆栈中抛出出错代码至%ecx、并将其写入全局量 errno。最后,在返回之前,将%eax 的内容改成-1。这样,通过寄存器%eax 返回到用户进程的数值便是-1,而 errno 则含有具体的出错代码。这是对大部分系统调用(返回整数的调用)返回值的约定。
3.3 接下来进入内核,也就是系统空间。 CPU 穿过陷阱门的过程与发生中断时穿过中断门的过程相同,不同之处在于:因外部中断而穿过中断门时是不检查中断门与所规定的准入级别的,而在通过 INT 指令穿越中断门或陷阱门时,则要核对所规定的准入级别与 CPU 的当前运行级别。为系统调用设置的陷阱门的准入级别 DPL 为 3(最低,所以不会阻挡系统调用的进行)。寄存器 IDTR 指向当前的中断向量表 IDT,而 IDT 表中对应于 0x80 的表项就是为 INT 0x80 设置的陷阱门,其中的函数指针指向 system_call()。当 CPU 到达 system_call()时,已经从用户态切换到了系统态,并且从用户堆栈换成了系统堆栈,相当于 CPU 在发生于用户空间的外部中断过程中到达 IRQ0xYY_interrupt 时的状态。
要注意的是, CPU 在穿越陷阱门进入系统内核时并不自动关中断,所以系统调用的过程是可中断的。
以下为函数 system_call()的代码:

ENTRY(system_call)/*将寄存器%eax 的内容压入堆栈,保存系统调用号*在外部中断过程中用来保存(经过变形的)中断请求号*/    pushl %eax          # save orig_eax/*在中断过程中, SAVE_ALL 以后,堆栈中的内容是作为一个 pt_regs 数据结构,当成参数传递给 do_IRQ(),然后又传递给具体的服务程序的,*在系统调用中就不同了,堆栈中每个寄存器的内容可作为独立的参数传递给具体的服务程序。*/    SAVE_ALL/*宏调用 GET_THREAD_INFO(%ebp)使寄存器%ebp 指向当前进程的 task_struct 结构*/    GET_THREAD_INFO(%ebp)                   # system call tracing in operation/*判断是否需要跟踪子进程系统调用*/    testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)/*如果需要跟踪,就跳转到syscall_trace_entry*在syscall_trace_entry中可以看到,如果需要跟踪子进程系统调用,就调用函数do_syscall_trace*/    jnz syscall_trace_entry/*检查寄存器%eax 中的系统调用号是否超出了范围*/    cmpl $(nr_syscalls), %eax    jae syscall_badsyssyscall_call:/*这是一条 call 指令,所 call 的地址在一个函数指针中,*这个函数指针在数组 sys_call_table[]中以%eax 的内容为下标、单位为 4 个字节的元素中。*表达式(, %eax, 4)的第一个逗号前面为空,表示在%eax 的基础上并没有其他的位移,*而 4 则表示计算位移( %eax相对于 sys_call_table)时的单位为 4 字节。*系统调用跳转表 sys_call_table[]是一个函数指针数组*/    call *sys_call_table(,%eax,4)    movl %eax,EAX(%esp)     # store the return value

在系统调用跳转表 sys_call_table中凡是内核不支持的系统调用号全部都指向 sys_ni_call(),这个函数只是返回一个出错代码-ENOSYS,表示该系统调用尚未实现。结合前面讲过的 libc.a 中的处理,可知此时用户程序会得到返回值-1,而全局量 errno 的值为 ENOSYS。
3.4 跳转表中位移为 0x4a,也就是 74 处的函数指针为 sys_sethostname,所以在我们这个情景中就进入了 sys_sethostname(),对于此函数内部运行本文不多说,有一点需要注意的是:在多处理器系统中,同时可以有多个进程在不同的 CPU 上运行。这样,就有可能发生两个进程同时调用 sethostname(),而形成race condition”(抢道)现象。为了防止这种情况发生,就要将对函数内部某些操作放在受到“信号量”( semaphore)保护的“临界区”中。
sys_sethostname()运行完后,现在回到本节开头的 system_call()。 CPU 从具体系统调用的服务程序返回时,由服务程序准备好的返回值在寄存器%eax 中,所以在system_call()的最后一行(movl %eax,EAX(%esp))将它写入到堆栈中与%eax对应的地方,这样在 RESTORE_ALL 以后,这个返回值仍通过%eax 传回用户空间。这以后, CPU 就到达了 ret_from_sys_call。

[system_call -> ret_from_sys_call]00205: ENTRY(ret_from_sys_call)00206: #ifdef CONFIG_SMP00207: movl processor(%ebx),%eax00208: shll $CONFIG_X86_L1_CACHE_SHIFT,%eax00209: movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active00210: testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask00211: #else00212: movl SYMBOL_NAME(irq_stat),%ecx # softirq_active00213: testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask00214: #endif00215: jne handle_softirq00216:00217: ret_with_reschedule:00218: cmpl $0,need_resched(%ebx)00219: jne reschedule00220: cmpl $0,sigpending(%ebx)00221: jne signal_return00222: restore_all:00223: RESTORE_ALL……………………00282: handle_softirq:00283: call SYMBOL_NAME(do_softirq)00284: jmp ret_from_intr

以上过程类似中断,在此不在赘述。