进程以下的那些事儿
硬件驱动程序的最底层
硬件驱动程序的最底层是什么?是直接操作硬件的部分。直接与硬件交互的方法有2大类:
- CPU主动发起,读写硬件的寄存器。这里面又分为2种,PIO和MMIO。
- PIO的方式在x86汇编语言里,就是用io和out汇编语句,读写硬件控制器上的寄存器。
- MMIO就是硬件会把自己的寄存器映射到主机的内存空间。也就是说,在程序员看来,访问MMIO寄存器就好像访问普通内存一样,可以直接用汇编mov语句操作。在C语言可以用声明为volatile的指针,然后直接用赋值语句就可以读写硬件寄存器。
- 另外,还有一种是DMA。但它不是独立的,还是要先通过上述2种方法之一,CPU向硬件发出命令。接下来的过程才是,硬件自己读写指定的内存区域。读写过程不需要CPU参与。
- 另外一种方式是IRQ。它是由硬件主动发起,打断CPU正在执行的程序。CPU会跳到IRQ号对应的中断号所对应的中断服务程序。执行完中断服务程序再返回到原来的程序。
所以,驱动程序往往包括这么几个部分:初始化(设置硬件状态,安装中断服务例程等等)、读操作、写操作、中断服务例程。
中断服务例程
中断服务例程(ISR)如果什么都不做,直接IRET,就可以返回到被打断的程序。(x86有一些中断会有error code,要先弹出error code,这样ESP才会指向返回地址。)
问题是ISR不能什么都不做啊,还是要做事的。这就需要保存原进程的上下文,做完事后,恢复上下文,然后iret,原来的进程才能继续执行。保存和恢复上下文是操作系统要做的事(硬件也会做一部分,写OS的人要搞清楚硬件做了什么,还缺什么)
进程的上下文其实就是程序(CS:EIP)、堆栈(ESP)、数据(DS),还有当前的CPU状态(从软件开发者角度看,主要是通用寄存器的值。其他的硬件负责保存和恢复)。如果有分页,就还涉及到页表的切换。
IRET本身只能负责恢复CS和EIP,而且前提是执行IRET前,ESP必须指向返回地址。
系统调用
操作系统给应用程序提供功能,也是通过中断服务例程。比如linux的系统调用。就是应用程序执行INT 80h。CPU就会跳到80号中断服务例程。这个ISR根据eax的值,决定具体调用哪个c函数(sys_open、sys_read、sys_write等等)。
stackoverflow: int 80h已经过时了
并发
假如没有中断,就没有并发了。
因为没有其他办法可以让正在执行的进程停下来,除非他自己愿意。
而且进程他自己愿意停下来,也是用INT指令,触发软中断(系统调用system call)。机制和硬件中断类似。
所以,没有中断,就没有并发。
时钟
怎么让进程在非自愿的情况下停下来?要用到时钟中断。可编程时钟(Intel 8253 - Programmable Interval Timer)是每个PC上都有硬件,操作系统可以设定让时钟定时发出中断。操作系统在时钟中断的服务例程里,就可以切换进程。
多核
多核处理器每个核(或者说每个hyperthread)都有自己的APIC timer。
硬件中断可以路由到指定的一个或多个核,其他核不受影响。
critical section、关中断、锁
如果程序要进入critical section,它不希望被非自愿地打断,他可以关中断。关中断这只能OS使用,不可以给普通进程用,否则OS怎么保持控制权?关中断只能关1个核的中断,其他核仍然在运作。
linux早期曾经用软件实现过关所有cpu的中断。后来发现这个很慢,就废弃了。多核同步改用锁来实现。
所有锁的实现基础都是spinlock。需要硬件提供支持,比如使用x86的xchg指令。
(待续)
补充:
-
Minix2和Linux其实只有一个内核栈,xv6是每个进程各自有一个内核栈。但是并没有本质不同,对于各个进程的内核态来说,大家还是井水不犯河水,仿佛内核栈是自己的。
内核执行的时候会被打断,或者会阻塞。它的context也是保存在这个栈上。
- IRET有时候也会切换堆栈指针(ESP)——就是如果中断发生前,运行的是用户进程,那么就涉及到特权级别转换。从用户态,切换到内核态。为了更安全,防止用户堆栈溢出导致系统崩溃。x86的CPU的硬件设计者,就这么设计:另外设置一个栈来存放用户进程的context和返回地址,由硬件来保证这个堆栈空间是够的。这个栈由TSS指定。
- 所以,Minix2在切换上下文的时候,有时就涉及了3个栈,内核栈,TSS指定的栈(保存用户进程的context),用户栈。中间那个栈只是用户态和内核态的过渡。