2024-12-22 update 时隔一年,偶然看到一个相当好的回答:
内核就是一个由interrupt驱动的程序。这个interrupt可以是一个系统调用(x86下,很多OS的系统调用是靠software interrupt实现的),可以是一个用户程序产生的异常,也可以是一个硬件产生的事件中断。 于是你的问题解决了:一个用户程序运行的时候,Linux就在内存里呆着,等着一个中断的到来。在这个中断的处理过程中,来做“调度”。 而一般的时分系统里,都会有个timer interrupt每隔一段时间到来,也就是楼上说的“时间片”。
驱动是针对硬件的软件,中断是为了实现操作系统必要功能的方式。
xv6介绍Interrupt的时候是借由数据键盘读取,屏幕显示来执行的,其中键盘和屏幕作为硬件输入输出设备是和uart芯片
相连的,因此xv6软件中的读写是针对uart芯片驱动而言的,对于该芯片,xv6使用console这层抽象作为软件驱动(这里的console即使相当于把键盘和显示器分别作为输入输出设备的虚拟控制台而言的驱动),console的底层是uart驱动,两者相互配合完成读写、中断工作。
这里要分清楚什么是软件部分,什么是硬件部分
,两者之间的交互关系又是如何。而这一部分的内容比较琐碎,因此分条列举:
-
设备寄存器被映射到了内存地址,对这部分内存的读写会反映到设备上,这就是前面page table中kernel page table做的事情,当然也要与真正的硬件做一些配合
-
driver包含了两个part
- top part,工作在内核环境下,负责软件主动的一些操作,例如该如何读(read),如何写(write),具体过程如下:
- read->sys_read->fileread->devsw[].read(指定设备的read)->
consoleread
(从内核缓存中读数据到用户空间,此处为cons.buf),此时就有个问题:谁把数据放到内核缓存中的? - wirte->sys_write->filewrite->devsw[].write->
consolewrite
(从用户空间读取数据到内核空间)->uartputc(放到内核写缓存,此处为uart_tx_buf)->uartstart(启动uart硬件)
- read->sys_read->fileread->devsw[].read(指定设备的read)->
- bottom part对于操作系统而言是被动的,当硬件完成任务后会借由
PLIC
(Platform-Level Interrupt Controller,根据优先级来分发路由中断)来向CPU发起中断请求,而CPU则会调用硬件驱动部分(console.c和uart.c)中的中断处理程序来进行一部分的中断处理。
当然中断不是唯一选项,对于一些变化速度比较快的设备还是
轮询(polling)
的策略比较好,当然目前的一些设备也有两者兼而有之的 - top part,工作在内核环境下,负责软件主动的一些操作,例如该如何读(read),如何写(write),具体过程如下:
-
这些外设是如何初始化的?实际上从软件的视角来看,
只能够直接沟通到UART硬件(芯片)
,该硬件是直接连接到键盘和显示器的,也就是所谓的console,因此我们不仅需要UART的驱动(uart.c)也需要console的驱动(console.c)- 在consoleinit()里调用uartinit()来初始化硬件,因为
能够直接配置的硬件只有uart芯片
。并且配置了console这个抽象硬件所对应的读写函数,此后所有针对console这个设备的读写都会被定位至特定的函数。 - 在uartinit()中打开两个中断:
transmit and receive interrupts
. 其中传输完成中断代表uart硬件已经完成了向外输出数据的任务,而receive中断则代表CPU需要处理外部数据
- 在consoleinit()里调用uartinit()来初始化硬件,因为
-
用户通过键盘(此时键盘是抽象的console的一部分)输入到console驱动读取并回显的过程:
- 键盘输入,通过uart芯片接收并发起一个中断,陷入操作系统,进入
trap过程
- trampoline正常一套,进入usertrap函数
- 判断是外部中断引起的trap,此时通过
devintr()
函数对中断进行处理 - 通过
plic_claim
查询是哪个设备产生的中断 - 随后进入对应的处理流程进行处理,在此是
uartintr()
函数(因为是uart硬件中断,这里处理函数理应叫这个名字) - 由于uart硬件发生中断有两种可能性(传输完成或者接收,也就是上面所说的两个中断)
- 需要从外部接收的情况则会从约定好的内存位置读取数据(即从外部寄存器读取),并交由专门的函数(consoleintr)进行处理,consoleintr负责将数据放到内核缓存,回显(具体的回显则是操作系统输出到屏幕上)和特殊字符处理
- 对于传输完成的情况(uart硬件完成了上次的传输任务,uart很空闲),则继续调用uartstart将buffer的数据传输出去
- 在consoleintr()中则是处理特殊字符,并将正确的字符放到输入缓存中,并且回显该字符到屏幕上,最后如果到达了一整行就唤醒之前在consoleread()中
sleep
的进程,来让之前睡眠的进程处理这些数据
- 键盘输入,通过uart芯片接收并发起一个中断,陷入操作系统,进入
-
针对设备的read系统调用则是会通过consoleread来进行处理,如果读buffer是空的,则需要sleep等待后面键盘输入将其唤醒,否则就从内核buffer复制到用户buffer
-
软件的数据如果需要打印到屏幕上(或者说向抽象的console写数据)则是通过以下过程:
write系统调用
- 因为在操作系统中所有外部设备都被视为文件,因此write会调用filewrite,而filewrite则会根据文件描述符(fd)来使用文件配套的函数(如果是正常文件就正常读写,如果
是设备文件就用指定的读写函数
,可见上面的3.1) - 在该情况下,此时运行到了consolewrite函数,该函数从用户空间copyin数据,并通过uartputc将其传递给buffer里,然后启动uartstart
- uartstart则是直接将其写入寄存器,由uart硬件传递给正确的硬件
-
PLIC处理中断的具体流程:
- 某个CPU运行
plic_claim
来获取一个中断来接收,而该中断则不会被其他CPU所处理 - 处理完成之后调用
plic_complete
来标识已经完成了该中断
- 某个CPU运行