- Interrupt中断概述
- 驱动(管理设备的代码)架构:Handler + Bottom
- interrupt Handler : 即 Bottom
- 例子 : kernel读取用户字符传给shell,并显示在终端上
- PLIC路由中断;UART传输字符
- Interrupt的并发性
- 发出Interrupt的设备与cpu并行
- 中断会停止当前进程(user -> uservec / kernel -> kernelvec)
- 驱动的top和bottom是并行的.(一个cpu上跑top , 一个cpu上跑bottom)
本篇有待复习、总结
问题 及 解释
interrupt是如何保存上下文以及将上下文重新加载的?(Interrupt也会进入uservec/kernelvec吗?会)
- 和trap机制一样。都是用uservec或者kernelvec。
console显示 $应当是不需要中断,而console上显示用户输入的字符则需要中断?
- console显示$ 就是驱动将$送入了uart,uart送入了console。
- console显示用户输入字符,则是
- shell 读取 用户输入的字符(这过程中涉及中断),顺带驱动将读入的字符送入uart,再送到console。
- 所涉及的中断是,键盘键入字符给uart,也即,键盘上有字符输入需要kernel处理,也即键盘需要得到os关注,因此发出个中断。中断经由PLIC路由到os。kernel中的Interrupt Handler对此进行处理,
- shell 读取 用户输入的字符(这过程中涉及中断),顺带驱动将读入的字符送入uart,再送到console。
uart到底是个什么?
- 应该是个芯片吧。
- 我现在理解就是一个字符的转发站。一端是kernel code,一端是设备。在这两方之间进行字符的转发。
- UART硬件可以看作一组映射到内存中的控制寄存器,对硬件的控制可以直接通过load和store特定内存来完成。UART内存映射地址开始于0x10000000或UART0(定义于memlayout.h)。每个控制寄存器的大小为1byte,偏移量定义于uart.c。
- uart和网卡这些东西是什么关系?
中断到底指的是什么?停止当前正在运行的进程?然后去处理来的字符?
- 中断 是 设备希望得到 os关注时,所采取的手段。即向os发送一个中断,表示“我需要你处理一下”。不一定是设备产生了字符,也可能是其他事件。比如网卡接收了packet。
uart和cpu是并行的吗?
- 对。
- 剩下的问题请 搜索 “未解决问题” 或者 “QUES”
总结
- 一句话说明:user在键盘上敲下的按键,是怎么使得console上显示出来敲入的字符的?
- 先前shell 调用read … -> sys_read … -> (驱动的top)consoleread(相对于InputBuffer是个Consumer的角色), 此时 inputBuffer为空,sleep等待。
- user按下按键,键盘发起中断,经PLIC路由达到一个CPU核。cpu核接收中断 保存上下文 跳转到相应的处理代码处,这里比方说是uservec -> usertrap,usertrap根据PLIC(plic_claim)提供的信息,得知是哪个设备发出了中断,进行相应处理。这里是uart中断,故进入(驱动的Bottom)(即Interrupt Handler)(同时相对于inputBuffer来说是一个Producer的角色)uartintr()。uartintr中,调用uartget()从uart的reg中读出字符c,然后调用consoleintr(c),其中1. 通过consputc(c)(最终到writeReg(c)将字符写入uart寄存器)来讲user键入的字符送入uart,然后uart负责送到console上;2. 将c放入驱动的inputBuffer,并唤醒之前由于inputBuffer为空而睡眠的consoleread。而后结束中断处理。层层返回,返回到usertrap,然后到了usertrapret,然后userretvec,继续shell。
- 如图
- 对于uart来说可以看成有两个驱动。
- 驱动1:一个是top是consolewrite(负责往outputBuffer中放字符) , buffer是outputBuffer , bottom是uartintr中的uartstart()(其实并不是一个Interrupt Handler,而仅仅是一个操作设备的代码,负责outputBuffer中的字符送入uart,uart将字符送入console)
- 驱动2:一个top是consoleread(负责从inputBuffer中读) , buffer是 inputBuffer,bottom是uartintr。(是个Interrupt Handler)(负责处理设备发出的中断,具体是读取uart送来的字符,放入inputBuffer,唤醒consoleread,并将字符echo到uart进而到console上)
- 对于该驱动,其top和bottom是可以并行的。
- 如一个cpu核在consoleread,另一个cpu核接收了uart发出的中断,故执行uartintr。
- 关于我以为的uart,PLIC的作用。正确性未知,反正我这么理解的。有待验证。
Interrupt
Interrup 与 syscall区别
中断场景:硬件希望得到操作系统的关注。
- 网卡收到一个packet,网卡会生成一个中断
- 用户按下键盘,键盘会产生一个中断。
中断发生时,os需要做的是
- 保存当前工作
- 处理中断
- 这里的保存和恢复工作,与之前学的系统调用使用的trap很像。
- 所以系统调用,page fault,中断,都使用相同的机制。
- 处理完成后,恢复之前的工作。
中断Interrupt 和 系统调用syscall 的区别
- asynchronous
- Interrupt: 硬件发出Interrupt时,os的Interrupt handler与当前在cpu上运行的process没有任何关联。
- syscall: syscall发生在process的上下文中。
- concurrency
- Interrupt: CPU和产生Interrupt的设备是真正并行的。我们必须管理这里的并行
- 比如网卡自己独立的处理来自网络的packet,然后在某个时间点产生中断,但是同时,CPU也在运行。
- program device
- 我们这节课主要关注外部设备,例如网卡,UART,而这些设备需要被编程。
- asynchronous
PLIC 与 CPU
- PLIC : 是个cpu核旁边的硬件,用于管理来自于外设的中断
- CPU是通过Platform Level Interrupt Control,简称PLIC来处理设备中断。
- PLIC位置如图:在cpu核旁边
- PLIC具体结构如下
- 设备发出的中断到达PLIC时,PLIC会将中断路由到某个CPU核。如果所有cpu核都在处理中断,PLIC会保留中断直到有一个CPU核处理中断。
- PLIC管理中断流程:
- PLIC会向CPU通知当前有一个待处理的中断。
- 其中一个CPU核会claim接收中断,这样PLIC就不会把中断发给其他CPU处理。
- CPU核处理完中断后,CPU会通知PLIC。
- PLIC将不再保存中断信息。
驱动
什么是驱动
- 驱动
- 管理设备的代码称为驱动
- 所有驱动都在内核中
- 下面介绍uart设备的驱动
典型架构
典型架构:大部分驱动可分为两部分:bottom和top
bottom : Interrupt Handler
- bottom部分通常是Interrupt Handler。
- 当PLIC将中断送至CPU,CPU接收该中断,然后CPU会调用相应的Interrupr Handler。
- Interrupt Handler 就是对 设备发出的中断进行响应,也即在操作发出中断的设备或者其他响应设备。它也并不运行在任何特定process的context,只是处理中断。
top : 用户进程 / 内核的其他部分调用的接口
- 对uart来说,top是read/write接口,这些接口可被更高级代码调用。
驱动的典型架构如下
- 通过buffer queue使得并行的cpu和设备解耦。
- QUES1 : 这句话是想说明什么?老师的这句话是在说top部分不能直接从interrupt handler中读取数据,而是要从queue中读取的原因吧?
通常对于Interrupt handler来说存在一些限制,因为它并没有运行在任何进程的context中,所以进程的page table并不知道该从哪个地址读写数据,也就无法直接从Interrupt handler读写数据。
Interrupt Handler中如何对设备进行操作、如何编程?
通常来说,编程是通过memory mapped I/O完成的。在SiFive的手册中,设备地址出现在物理地址的特定区间内,这个区间由主板制造商决定。操作系统需要知道这些设备位于物理地址空间的具体位置,然后再通过普通的load/store指令对这些地址进行编程。load/store指令实际上的工作就是读写设备的控制寄存器。
例如,对网卡执行store指令时,CPU会修改网卡的某个控制寄存器,进而导致网卡发送一个packet。所以这里的load/store指令不会读写内存,而是会操作设备。
中断开启
- 开启中断,也即使得CPU可以接受来自设备的中断。
- 我觉着大致可以分为三步
- 令设备可以发出中断
- 令PLIC可以接收并路由中断
- 令CPU可以接受中断
前置知识
- 前置知识:RISCV中 与中断相关的寄存器
- $SIE (Supervisor Interrupt Enable)
- 有3个bit。
- bit(E) : 代表UART外部设备的中断。
- bit(S) : 代表软件中断
- bit(T) : 代表定时器中断
- 有3个bit。
- $SSTATUS (Supervisor Statue)
- 有1个bit 负责 打开、关闭中断。
- $SIE负责单独控制特定的中断;$SSTAUS 负责控制所有中断
- 每个CPU核都有独立的$SIE $SSTATUS。
- $SIP (Supervisor Interrupt Pending)
- 此时发生的中断类型
- $SCAUSE
- 表明当前进入kernel的原因是中断
- $STVEC
- trap / page fault / Interrupt发生时,此时运行的user process的程序计数器。
- $SIE (Supervisor Interrupt Enable)
系统一开始是如何开启中断的
entry.S -> start.c : void start()
- 所有的中断都设置在Supervisor mode
- 设置$SIE来接收External,软件和定时器中断
- 初始化定时器
start.C -> main.c : int main(){}
- consoleinit:设置好uart芯片,使其可以产哼中断
- plicinit,plicinithart : 使得PLIC可以接受相应中断,并路由到cpu。
- scheduler -> inrt_on : 设置$SSTATUS,使得CPU可以接收中断。
- 至此,中断被完全打开,cpu可以接受来自设备的中断。
前置知识:发生中断时如何进入相应C code
- 发生中断时,进入kernel C Code之前的处理:(所有都是这样)
- 清除$SIE
- 防止CPU核被其他中断打扰。CPU核完成当前中断后,再恢复$SIE
- 保存当前PC到$SEPC
- 保存当前mode
- 设置mode为Supervisor mode
- PC设置为$STVEC。即跳转到相应的处理代码处。
- user态时发生的中断即跳转到uservec,kernel处发生的中断即跳转到kernelvec。
- uservec -> usertrap ; kernelvec -> kerneltrap
console如何显示 $
当没发生中断时
- 打印$流程
- init.c -> sh.c -> getcmd -> fprintf($) -> write(fd, &c, 1); -> sys_write -> filewrite -> devsw[CONSOLE].write -> consolewrite -> uartputc(c) -> uartstart()
该驱动要将字符传送到uart最后传送到console上,需要有个缓冲区。这里是个循环buffer。我称之为outputBuffer (queue)吧。
- code如下
- uart_tx_r 指向当前要从buffer中读走的字符
- uart_tx_w 指向当前要写入字符到buffer中的位置。
1
2
3
4
5
6// the transmit output buffer.
struct spinlock uart_tx_lock;
char uart_tx_buf[UART_TX_BUF_SIZE];
int uart_tx_w; // write next to uart_tx_buf[uart_tx_w++]
int uart_tx_r; // read next from uart_tx_buf[uar_tx_r++]
TOP : consolewrite -> void uartputc(int c)
- 将c写入给buffer queue。
- 从buffer中取出char给uart,通知uart处理char。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// add a character to the output buffer and tell the
// UART to start sending if it isn't already.
// blocks if the output buffer is full.
// because it may block, it can't be called
// from interrupts; it's only suitable for use
// by write().
// 将字符C写入驱动的buffer queue中。
void
uartputc(int c)
{
acquire(&uart_tx_lock);
if(panicked){
for(;;)
;
}
while(1){
// 循环buffer已经满了 则sleep,将CPU让给其他进程。
if(((uart_tx_w + 1) % UART_TX_BUF_SIZE) == uart_tx_r){
// buffer is full.
// wait for uartstart() to open up space in the buffer.
sleep(&uart_tx_r, &uart_tx_lock);
} else {
// 将char c 写入buffer,更新w指针。
uart_tx_buf[uart_tx_w] = c;
uart_tx_w = (uart_tx_w + 1) % UART_TX_BUF_SIZE;
// 通知uart进行操作
uartstart();
release(&uart_tx_lock);
return;
}
}
}
- 从buffer中取出char给uart,通知uart处理char。
BOTTOM : uartintr -> consoleintr ,uartstart()
这里直接调用了其BOTTOM中的一部分:uartstart,并不是为了相应设备发出的中断,只是为了操作下设备。
- 从uart_tx_buf中取出一个char
- int c = uart_tx_buf[uart_tx_r++];
- 将该char送入uart.
- WriteReg(THR, c);
- 也即,告诉uart,我这里有一个字节需要你来发送。
- 数据送到uart后,系统调用即返回,user shell继续执行.
- 与此同时,uart将数据送出,送到指定设备(console?)。
如果没发生中断的话,这里就是正常输出了$吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// if the UART is idle, and a character is waiting
// in the transmit buffer, send it.
// caller must hold uart_tx_lock.
// called from both the top- and bottom-half.
// 通知设备执行操作
void
uartstart()
{
while(1){
if(uart_tx_w == uart_tx_r){
// transmit buffer is empty.
return;
}
if((ReadReg(LSR) & LSR_TX_IDLE) == 0){
// the UART transmit holding register is full,
// so we cannot give it another byte.
// it will interrupt when it's ready for a new byte.
return;
}
// 从uart_tx_buf中取出一个char,将该char送入uart
int c = uart_tx_buf[uart_tx_r];
uart_tx_r = (uart_tx_r + 1) % UART_TX_BUF_SIZE;
// maybe uartputc() is waiting for space in the buffer.
// 唤醒等待空闲空间的uartputc()函数
wakeup(&uart_tx_r);
// 将char写入uart的寄存器中
// 也即,告诉uart,我这里有一个字节需要你来发送。
// 数据送到uart后,系统调用即返回,user shell继续执行
// 与此同时,uart将数据送出,送到指定设备(应该是console)。
WriteReg(THR, c);
}
}
当发生中断时
发生中断时,进入kernel C Code之前的处理:(所有都是这样)
- 如上所述。
发生中断时,进入kernel C Code之后的流程
- 举例 ,当在我们向Console输出字符时,发生中断。
- shell运行在user态,故跳转到usertrap。
- usertrap -> devintr -> uartintr -> consoleintr , uartstart
- usertrap
1
2
3
4
5
6
7if(){
...
}else if((which_dev = devintr()) != 0){
// ok
}else{
...
} - int devintr()
- 获取中断号 : int irq = plic_claim();
- 根据中断号判断是哪个设备的中断,进行相应处理。我们的例子中会进入uartintr()
- if(irq == UART0_IRQ) uartintr();
- if(irq == VIRTIO0_IRQ) virtio_disk_intr();
- if(irq) printf(“unexpected interrupt irq=%d\n”, irq);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// check if it's an external interrupt or software interrupt,
// and handle it.
// returns 2 if timer interrupt,
// 1 if other device,
// 0 if not recognized.
int
devintr()
{
uint64 scause = r_scause();
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// this is a supervisor external interrupt, via PLIC.
// irq indicates which device interrupted.
// 获取中断号
int irq = plic_claim();
// 鉴定为uart发出中断
if(irq == UART0_IRQ){
uartintr();
} else if(irq == VIRTIO0_IRQ){
virtio_disk_intr();
} else if(irq){
printf("unexpected interrupt irq=%d\n", irq);
}
// the PLIC allows each device to raise at most one
// interrupt at a time; tell the PLIC the device is
// now allowed to interrupt again.
if(irq)
plic_complete(irq);
return 1;
} else if(scause == 0x8000000000000001L){
// software interrupt from a machine-mode timer interrupt,
// forwarded by timervec in kernelvec.S.
if(cpuid() == 0){
clockintr();
}
// acknowledge the software interrupt by clearing
// the SSIP bit in sip.
w_sip(r_sip() & ~2);
return 2;
} else {
return 0;
}
}
- 驱动的BOTTOM Interrupt Handler: void uartintr()
- consoleintr()
- uartstart()
- 将shell存储在驱动中的buffer queue中的字符取出,送到uart的寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from trap.c.
void
uartintr(void)
{
// read and process incoming characters.
while(1){
int c = uartgetc();
if(c == -1) // 中断没字符 不读
break;
consoleintr(c); // 读取终端输入的字符
}
// send buffered characters.
acquire(&uart_tx_lock);
uartstart(); // 将buffer queue中的char送入uart的寄存器。uart将char送到console
release(&uart_tx_lock);
} - 实际上在提示符“$”之后,Shell还会输出一个空格字符,write系统调用可以在UART发送提示符“$”的同时,并发的将空格字符写入到buffer中。所以UART的发送中断触发时,可以发现在buffer中还有一个空格字符,之后会将这个空格字符送出。??还不太理解。
- 将shell存储在驱动中的buffer queue中的字符取出,送到uart的寄存器
- 由上述可见,驱动的top部分和bottom部分解耦开。(通过buffer queue)
- top : consolewrite->uartput(c)
- bottom : void uartintr() : consoleintr() , uartstart()
shell 如何 读取用户键入的 字符 并 传给shell程序 / 并 显示在终端上
- 类似于上面的驱动要将字符传送到uart最后传送到console上。那个驱动结构中有一个循环缓冲区。我就称之为outputBuffer queue吧。
- 同理,这里要将keyboard传给uart上的字符读到驱动中,驱动中也有一个循环缓冲区。我就称之为inputBuffer queue吧
- code如下,原理和那个outputBuffer一样。
1
2
3
4
5
6
7// input
char buf[INPUT_BUF];
uint r; // Read index
uint w; // Write index
uint e; // Edit index
} cons;
- code如下,原理和那个outputBuffer一样。
- 如图
shell 从console中读取用户输入
- 这一部分还没发生中断。
- 函数流程: (sh.c)gets -> read -> fileread -> devsw[f->major].read -> consoleread(int user_dst, uint64 dst, int n) ->阻塞的读完指定bytes
- TOP : consoleread 核心逻辑如下:
- 是驱动架构中的TOP
- 读取buffer queue中的字符,遇到’\n’或者读完n bytes之后就返回到用户sh.c
- 当buffer queue中没有字符时,sleep等待。等待被唤醒。即等待bottom的InterruptHandler处理键盘的中断,将字符放入buffer queue。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//
// user read()s from the console go here.
// copy (up to) a whole input line to dst.
// user_dist indicates whether dst is a user
// or kernel address.
//
int consoleread(int user_dst, uint64 dst, int n)
{
acquire(&cons.lock);
while(n > 0)
{
// wait until interrupt handler has put some input into cons.buffer.
// 这不就是条件变量配合锁使用。
while(cons.r == cons.w)
{
// 等待buffer中有可读数据
sleep(&cons.r, &cons.lock);
}
c = cons.buf[cons.r++ % INPUT_BUF];
// copy the input byte to the user-space buffer.
cbuf = c;
// 将字符c传给user层地址空间
either_copyout(user_dst, dst, &cbuf, 1);
dst++;--n;
if(c == '\n')
{
// a whole line has arrived, return to
// the user-level read().
break;
}
}
release(&cons.lock);
return target - n;
}
用户键入键盘,触发中断
- 用户键入键盘,触发中断,kernel的uartintr作为InterrupHandler进行处理。
- usertrap -> devintr -> BOTTOM : uartintr -> uartgetc , consoleintr , uartstart。
- BOTTOM Interrupt Handler: uartintr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from trap.c.
void
uartintr(void)
{
// 其实我觉着这read 和 send两部分可以分成两个函数吧。。。。
// read and process incoming characters.
// 这里操作的buffer是inputBuffer(对于shell来说) 是一个为了从uart读入字符给上层的buffer
while(1){
int c = uartgetc();
if(c == -1)
break;
consoleintr(c);
}
// send buffered characters.
// 这里操作的buffer是outputBuffer(对于shell来说) 是一个为了从将字符从上层送入uart的buffer。
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}- uartgetc : 从uart中读取1个字符
- consoleintr : 将从uart读取的字符填充到buffer queue中,并唤醒consoleread。可以看出consoleintr是个producer的角色。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void consoleintr(int c)
{
acquire(&cons.lock);
switch(c){
// ....
default:
if(c != 0 && cons.e-cons.r < INPUT_BUF){
c = (c == '\r') ? '\n' : c;
// echo back to the user.
consputc(c);
// store for consumption by consoleread().
cons.buf[cons.e++ % INPUT_BUF] = c;
// 读取了一行 / 全部读取完了。
// 那么 唤醒等待条件变量cons.r的consoleread
if(c == '\n' || c == C('D') || cons.e == cons.r+INPUT_BUF){
// wake up consoleread() if a whole line (or end-of-file)
// has arrived.
cons.w = cons.e;
wakeup(&cons.r);
}
}
}
} - uartstart() : 感觉在这里没啥用处。或者说跟上面读取的键盘的字符没啥关系。因为uartstart把output buffer queue中的字符给uart。让其送到console。和我们上述读取键盘的字符时所用的buf不是一个buf。
- 之后Consumer consoleread被唤醒。从input buffer queue 中读取字符到user态。返回到user层。
Inetrrupt的并发
设备与CPU是并行运行的
- 如CPU将字符送入UART的reg之后就会返回Shell,而与此同时,UART正在并行的将字符传送给Console。此时Shell可能正在并行的调用write,向buffer queue中加入字符。
中断会停止当前运行的程序
- 对于user代码,无妨,因为中断会导致跳转到uservec trampoline,进行和syscall trap时一样的对用户上下文的保存。
- QUES2:对于kernel,不成?
我不理解。不是也有kernelvec吗
驱动的top和bottom部分是并行运行的
- 例如,Shell会在传输完提示符“$”之后再调用write系统调用传输空格字符,代码会走到UART驱动的top部分(注,uartputc函数),将空格写入到buffer中;但是同时在另一个CPU核,可能会收到来自于UART的中断,进而执行UART驱动的bottom部分,查看相同的buffer。如图
- 所以一个驱动的top和bottom部分可以并行的在不同的CPU上运行。这里我们通过lock来管理并行。因为这里有共享的数据,我们想要buffer在一个时间只被一个CPU核所操作
- input/outputbuffer queue存在于内存中,并且只有一份,所以,所有的CPU核都并行的与这一份数据交互。所以我们才需要lock。
- 例如,Shell会在传输完提示符“$”之后再调用write系统调用传输空格字符,代码会走到UART驱动的top部分(注,uartputc函数),将空格写入到buffer中;但是同时在另一个CPU核,可能会收到来自于UART的中断,进而执行UART驱动的bottom部分,查看相同的buffer。如图
Interrupt的演进
- 直接摘mit
概括
由于发生中断时,cpu要经过多步才能处理中断数据。因此,如果设备高速的产生中断,那么cpu将很难及时处理。因此,现在产生中断之前,硬件会执行大量操作以减轻cpu负担。因此硬件现在更复杂。
最后我想介绍一下Interrupt在最近几十年的演进。当Unix刚被开发出来的时候,Interrupt处理还是很快的。这使得硬件可以很简单,当外设有数据需要处理时,硬件可以中断CPU的执行,并让CPU处理硬件的数据。
而现在,中断相对处理器来说变慢了。从前面的介绍可以看出来这一点,需要很多步骤才能真正的处理中断数据。如果一个设备在高速的产生中断,处理器将会很难跟上。?所以如果查看现在的设备,可以发现,现在的设备相比之前做了更多的工作。所以在产生中断之前,设备上会执行大量的操作,这样可以减轻CPU的处理负担。所以现在硬件变得更加复杂。
概括
对于高性能设备,会很高速的产生中断,cpu不能即使处理。
因此,引入polling。通过轮询设备来获取数据(可以经常拿到数据),节省了中断的代价,而不是只通过interrupt。
对于高性能网卡,如果大量包传入,那么会使用polling,而非interrupt。
如果你有一个高性能的设备,例如你有一个千兆网卡,这个网卡收到了大量的小包,网卡每秒可以生成1.5Mpps,这意味着每一个微秒,CPU都需要处理一个中断,这就超过了CPU的处理能力。那么当网卡收到大量包,并且处理器不能处理这么多中断**的时候该怎么办呢?
这里的解决方法就是使用polling。除了依赖Interrupt,CPU可以一直读取外设的控制寄存器,来检查是否有数据。对于UART来说,我们可以一直读取RHR寄存器,来检查是否有数据。现在,CPU不停的在轮询设备,直到设备有了数据。
这种方法浪费了CPU cycles,当我们在使用CPU不停的检查寄存器的内容时,我们并没有用CPU来运行任何程序。在我们之前的例子中,如果没有数据,内核会让Shell进程sleep,这样可以运行另一个进程。
所以,对于一个慢设备,你肯定不想一直轮询它来得到数据。我们会在没有数据的时候切换出来运行一些其他程序。但是如果是一个快设备,那么Interrupt的overhead也会很高,那么我们在polling设备的时候,是经常能拿到数据的,这样可以节省进出中断的代价。
所以对于一个高性能的网卡,如果有大量的包要传入,那么应该用polling。对于一些精心设计的驱动,它们会在polling和Interrupt之间动态切换(注,也就是网卡的NAPI)。