Chapter 5.4-5.6 - 中断机制 & 系统调用 & 系统时间和定时
Created by : Mr Dk.
2019 / 08 / 03 11:41
Nanjing, Jiangsu, China
5.4 中断机制
5.4.1 中断操作原理
可编程中断控制器 (Programmable Interrupt Controller) 是微机系统中设备中断请求的管理者。
- 连接到设备的中断请求引脚,接受中断请求信号
- 在同时收到几个中断的情况下,PIC 会对它们进行优先级比较,选出最高优先级中断进行处理
- PIC 向 CPU 的 INT 引脚发出中断信号,通过数据总线发送与中断请求对应的中断号
- CPU 停下当时所做的事情,根绝中断号查询中断表,并开始执行中断服务程序
中断也可由软件产生:使用 INT
指令,并用操作数指明中断号。
5.4.2 80X86 微机的中断子系统
每个 8259A 芯片可以管理 8 个中断源,多片级联,最多可以管理 64 个中断向量系统。在 PC/AT 系列兼容机中,使用了两片 8259A,可管理 15 级中断向量,从片的 INT 引脚级联到了主片的 IRQ2 上,主片的原 IRQ2 被接到了从片的 IRQ9 上。BIOS 中的软件把 IRQ9 的中断 INT 71 定向到 IRQ2 的中断 INT 0x0A 上
为了兼容。。。
在总线控制器控制下,8259A 芯片可以处于编程状态和操作状态:
- 编程状态:CPU 使用 IN 或 OUT 指令对芯片进行初始化编程的状态
- 操作状态:芯片随时响应外部请求
5.4.3 中断向量表
80X86 支持 256 个中断。在实地址模式下,每个中断向量由 4B 组成,中断向量指明了对应中断服务程序的段和段内偏移,因此整个中断向量表的长度为 1024B。80X86 微机启动时,ROM BIOS 会在物理内存 0 处初始化中断向量表,各中断默认的中断服务程序则在 BIOS 中给出。在 BIOS 初始化操作中,设置了 8259A 芯片支持的 15 个中断向量,对于实际没有使用的向量,填入临时的哑中断服务程序地址。
对于 Linux,在刚开始加载内核时需要用到 BIOS 提供的显示和磁盘读中断,之后 Linux 会重新初始化 8259A 芯片,并重新设置一张中断向量表。即 Linux 在内核正常运行之后完全抛弃了 BIOS 所提供的中断服务功能。
5.4.4 Linux 内核的中断处理
对于 Linux 内核来说,中断信号分为:
- 硬件中断
- 软件中断 (异常)
每个中断由 0-255 之间的数字来标识,前 32 个中断号由 Intel 公司固定设定或保留,属于软件中断;中断 INT32 - INT255 可以由用户自己设定。在 Linux 系统中,将 INT32 - INT47 对应于 8259A 的 IRQ0 - IRQ15,并把用户程序发出的软件中断设置为 INT128 (0x80):系统调用 (System Call),是用户程序使用 OS 资源的唯一界面接口。
系统初始化时,内核使用哑中断向量对 IDT 中的 256 个描述符进行默认设置。哑中断向量指向默认的无中断处理过程。INT0 - INT31 在 traps.c
中进行了重新设置,INT128 则在调度程序初始化函数中进行了重新设置。设置 IDT 时,Linux 内核使用了中断门和陷阱门两种描述符:
- 中断门描述符执行的中断会复位 IF 标志,避免其它中断干扰当前中断处理过程
- 陷阱门中执行的中断不会影响 IF 标志
5.4.5 标志寄存器的中断标志
为了避免竞争条件和中断对临界代码区的干扰,Linux 0.12 内核代码中许多地方使用了 CLI 和 STI 指令进行开中断 / 关中断。
5.5 Linux 的系统调用
5.5.1 系统调用接口
系统调用是 Linux 内核与上层应用程序进行交互通信的唯一接口。用户程序通过直接或间接 (库函数) 调用中断 INT 0x80,并在 eax 寄存器中指定系统调用功能号。通常应用程序都是使用具有标准接口定义的 C 库函数间接地使用系统调用,系统调用通过函数的形式调用,因此可以附带参数。执行结果会在返回值中表示:通常负值表示错误,0 表示成功。出错时,错误的类型码会被存放在全局变量 errno 中,通过调用库函数 perror()
可以打印出对应出错的字符串信息。
在 Linux 内核中,每个系统调用都具有唯一的系统调用功能号。0.12 中有 87 个系统调用功能,定义在 include/unistd.h
中,对应于 include/linux/sys.h
定义的系统调用处理程序指针数组表 sys_call_table[]
中的索引。
5.5.2 系统调用处理过程
寄存器 eax 中存放系统调用号,参数可依次放在 ebx、ecx 和 edx 中。因此 Linux 0.12 内核中,用户程序能够向内核最多直接传递三个参数。若需要传递大块数据,可以传递这块数据的指针。内核源码中定义了宏函数 _syscalln()
,n
代表携带参数的个数,分别是 0 - 3。
对于 read()
系统调用,定义是:
int read(int fd, char *buf, int n);
对应的宏形式:
_syscall3(int, read, int, fd, char *, buf, int n)
- 第一个参数对应返回值类型
- 第二个参数对应系统调用名称
- 之后是每个参数的参数类型和名称
这个宏会被扩展成包含内嵌汇编的 C 函数。
对于多于三个参数的系统调用,通常使用 _syscall1()
,把参数 buffer 的指针传给内核即可。
进入内核的系统调用处理程序 kernel/sys_call.s
后,首先检查 eax 中系统调用功能号是否在有效系统调用号范围内,然后根据 sys_call_table[]
函数指针表执行相应的系统调用处理程序。
5.5.3 Linux 系统调用的参数传递方式
Linux 系统使用的是通用寄存器传递方法。优点:当进入中断服务程序而保存寄存器值时,这些传递参数寄存器也被自动放在了内核堆栈上,不用再专门对传递参数的寄存器进行特殊处理。在每个系统调用处理程序中,应当对传递的参数进行验证,保证所有参数都合法有效。
5.6 系统时间和定时
5.6.1 系统时间
PC/AT 微机系统中提供了用电池供电的实时钟 RT (Real Time) 电路,这部分电路与保存系统信息的 CMOS RAM 集成在一个芯片上:RT/CMOS RAM 电路 - 比如 Motorola 的 MC146818 芯片。内核初始化时,通过 init/main.c
中的 time_init()
函数读取芯片上的当前时间,通过 kernel/mktime.c
中的 kernel_mktime()
函数转换为 Unix 日历时间。
该时间确定了系统开始运行的日历时间,保存在全局变量 startup_time
中供内核所有代码会用,通过系统调用 time()
可以读取这个值,super user 可以通过系统调用 stime()
修改该值。
从系统启动开始计数的系统滴答值 - jiffies
:程序可以唯一确定当前时间。每个滴答由系统定时芯片产生 - 10ms。因此内核代码中定义了一个宏,方便代码对当前时间的访问:
#define CURRENT_TIME (startup_time + jiffies/HZ)
HZ = 100,是内核系统时钟的频率。
5.6.2 系统定时
Linux 0.12 内核初始化过程中,PC 中的 Intel 8253/8254 芯片的计数器通道 0 被设置在运行于方式 3 下。每隔 10ms 在通道 0 的输出端 OUT 发出一个方波上升沿,OUT 被连接到 PIC 的 0 级上,因此每 10ms 会产生一个时钟中断 IRQ0。这个时间节拍是 OS 的脉搏:一次中断被称为一个系统滴答或一个系统时钟周期,每经过一个系统时钟周期,系统就会调用一次时钟中断处理程序。时钟中断处理程序通过 jiffies
变量来累积系统启动以来的滴答数:每发生一次时钟中断,jiffies
就加 1,然后调用 do_time()
进行进一步处理。
do_timer()
函数根据特权级对当前进程运行时间作累积
- CPL == 0,那么进程被中断于内核态,进程的内核态运行时间 + 1
- 否则把进程的用户态运行时间 + 1
时间片 是一个进程被切换掉之前能持续运行 CPU 的时间,以系统滴答为单位,每发生一次时钟中断,时间片 - 1。若进程时间片递减后依旧 > 0,说明时间片还没有用完 - 退出 do_timer()
继续运行当前进程;如果时间片减至 0,内核根据被中断程序的级别做进一步处理:
- 如果被中断进程运行在用户态,
do_timer()
调用调度程序schedule()
切换到其它进程 - 如果被中断进程运行在内核态,
do_timer()
立刻退出 - 决定了进程在内核态运行时是不可抢占的,在用户态运行时是可以被抢占的