Interrupts and device drivers

A driver is the code in an operating system that manages a particular device:

  1. configures the device hardware
  2. tells the device to perform operations
  3. handles the resulting interrupts
  4. interacts with processes that may be waiting for I/O from the device

Driver code can be tricky because a driver executes concurrently with the device that it manages.

In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

如果devices需要让操作系统对某些事情做出响应,就要采取中断的方法。在kerneltrap中,内核响应中断,并且根据设备类型来决定中断处理函数。

image-20230115160523827

这段对设备中断的概述总结得非常到位

也就是说,一个device driver可以分为两部分实现,一部分是接收请求,然后开启read/write;另一部分是接收中断,这个中断有可能是设备完成IO,也可能是设备需要IO,它会通知设备具体怎么做,它也会唤醒恰当的进程。

Code: Console input

console driver是driver structure的一个实现案例。

上层逻辑

shell获取用户输入console的信息是通过系统调用read()实现的。read通过文件描述符,最终转向consoleread()来实现具体的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// in file.c fileread()
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
return -1;
r = devsw[f->major].read(1, addr, n);// 在这里转向console
}
// in console.c consoleinit()
void
consoleinit(void)
{
initlock(&cons.lock, "cons");

uartinit();

// 在这里完成devsw的初始化
// connect read and write system calls
// to consoleread and consolewrite.
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].write = consolewrite;
}

对console的读写事实上是对cons结构体里buf的读写。这个buf则是由底层逻辑管理的。consoleread()每次读取buf中的一行,当未读满一行且无字符输入时会阻塞,直到底层逻辑将字符放入buf。读满了一行后,consoleread将该行copy进用户空间,随后返回read

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// in kernel/console.c 

#define INPUT_BUF_SIZE 128
struct {
struct spinlock lock;
char buf[INPUT_BUF_SIZE];
uint r; // Read index
uint w; // Write index
uint e; // Edit index
} cons;


// 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)
{
uint target;
int c;
char cbuf;

target = n;
acquire(&cons.lock);
while(n > 0){
// wait until interrupt handler has put some
// input into cons.buffer.
// read和write的index一样,说明此时没有数据输入,阻塞
while(cons.r == cons.w){
if(killed(myproc())){
release(&cons.lock);
return -1;
}
sleep(&cons.r, &cons.lock);
}
// 产生数据输入,接收数据
c = cons.buf[cons.r++ % INPUT_BUF_SIZE];

if(c == C('D')){ // end-of-file
if(n < target){
// Save ^D for next time, to make sure
// caller gets a 0-byte result.
// 这样下一次也能访问到eof
cons.r--;
}
break;
}

// copy the input byte to the user-space buffer.
cbuf = c;
if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
break;

dst++;
--n;

if(c == '\n'){
// a whole line has arrived, return to
// the user-level read().
break;
}
}
release(&cons.lock);

return target - n;
}

底层逻辑

底层逻辑维护了与上层逻辑交互的buf。

console接收数据对buf的读,是通过中断来实现的。

当用户输入字符,UART硬件检测到读,会向操作系统发送中断。中断在kerneltrap()中被接收处理,然后通过devintr()对该中断分门别类地进行转发。console的转发路径为devintr->uartintr->consoleintr。

UART

UART的全称是Universal Asynchronous Receiver and Transmitter,即异步发送和接收。它的软件上的表示形式是a set of memory-mapped control registers。CPU通过物理地址与这些寄存器交互,也即它们跟RAM是同一个地址空间。在xv6中,UART的地址空间从UART0(0x1000 0000)开始。这些寄存器地址关于UART0的偏移量定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// the UART control registers.
// some have different meanings for read vs write.
// see http://byterunner.com/16550.html
#define RHR 0 // 接收寄存器receive holding register (for input bytes)
#define THR 0 // 发送寄存器transmit holding register (for output bytes)
#define IER 1 // 开关中断寄存器
#define IER_RX_ENABLE (1<<0) // 如果该位被设置,则在接收寄存器有数据,即想向外界发送数据时,UART会搓出一个中断
#define IER_TX_ENABLE (1<<1) // 如果该位被设置,则在发送寄存器有数据,即外界向硬件发送数据时,UART会搓出一个中断
#define FCR 2 // FIFO control register
#define FCR_FIFO_ENABLE (1<<0)
#define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs
// ...
#define LCR 3 // line control register
// ...
#define LSR 5 // line status register
#define LSR_RX_READY (1<<0) // input is waiting to be read from RHR
#define LSR_TX_IDLE (1<<5) // THR can accept another character to send

image-20230115170107044

例如,LSR寄存器包含指示输入字符是否正在等待软件读取的位。这些字符(如果有的话)可用于从RHR寄存器读取。每次读取一个字符,UART硬件都会从等待字符的内部FIFO寄存器中删除它,并在FIFO为空时清除LSR中的“就绪”位。UART传输硬件在很大程度上独立于接收硬件;如果软件向THR写入一个字节,则UART传输该字节。

kerneltrap

1
2
3
4
5
6
7
// in kerneltrap()
// 在此处的devintr对不同的设备进行不同的处理方式
if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}

devintr

devintr处在trap.c中,作用是对中断归类,然后分门别类地转发到下一层级的handler。

注:

  1. 外中断和内中断

    外部中断和内部中断详解

    根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。

    外部中断一般是指由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备的中断请求。

    内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。

    软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序。例如:ROM BIOS中的各种外部设备管理中断服务程序(键盘管理中断、显示器管理中断、打印机管理 中断等,)以及DOS的系统功能调用(INT 21H)等都是软件中断。【比如说系统调用之类的】

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
45
// 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()
{
// 获取scause,辨析中断类型
uint64 scause = r_scause();

// 如果来自外中断(在这里应该只指device interrupt)
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// this is a supervisor external interrupt, via PLIC.

// irq indicates which device interrupted.
// 通过PLIC硬件获取中断设备信息
int irq = plic_claim();

// 分别转发
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){
// ...
return 2;
} else {
return 0;
}
}

uartintr

这代码其实乍一看是看不懂的,这是因为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 devintr().
void
uartintr(void)
{
// read and process incoming characters.
while(1){
int c = uartgetc();
// return -1 if none is waiting,说明读完了
if(c == -1)
break;
// 每读入一个字符就转交给console
consoleintr(c);
}

// send buffered characters.
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}

consoleintr

向buf中放入字符c

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
void
consoleintr(int c)
{
acquire(&cons.lock);

switch(c){
case C('P'): // Print process list.
// ...一堆特殊情况处理...
default:
if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
c = (c == '\r') ? '\n' : c;

// echo back to the user.
consputc(c);

// store for consumption by consoleread().
cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;

if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
// wake up consoleread() if a whole line (or end-of-file)
// has arrived.
// 中断处理并不会做很多事情,只是会与缓冲区交互
// 涉及到复杂的事情,比如说将数据拷贝到用户空间
//就唤醒上层逻辑来做
cons.w = cons.e;
wakeup(&cons.r);
}
}
break;
}

release(&cons.lock);
}

Code: Console output

外部通过write这个系统调用来对console写。

uartputc

最先到达这里。

uart内置了一个缓冲区。

1
char uart_tx_buf[UART_TX_BUF_SIZE];

用户仅需通过uartputc对buf进行写入即可,具体的buf数据向UART转移由uartputc通过调用uartstart实现。

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
// 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().这段话很有意思,说它由于会阻塞所以最好别在中断的时候用。
void
uartputc(int c)
{
acquire(&uart_tx_lock);

if(panicked){
for(;;)
;
}
// 阻塞
while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
// buffer is full.
// wait for uartstart() to open up space in the buffer.
sleep(&uart_tx_r, &uart_tx_lock);
}
uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
uart_tx_w += 1;
uartstart();
release(&uart_tx_lock);
}

uartstart

uartstart的作用是从缓冲区取数据向UART硬件发送。不阻塞。

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
// 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.
// 当缓冲区满没有选择阻塞,而是先结束
// 当UART硬件准备好继续接收的时候,UART会发送transmit complete中断,到时候会再继续从buf读取
return;
}

// 一个字符一个字符写
int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
uart_tx_r += 1;

// maybe uartputc() is waiting for space in the buffer.
wakeup(&uart_tx_r);

WriteReg(THR, c);
}
}

当传输过程非常流畅,UART硬件没有阻塞时,以上的代码就能完美阐述发送的过程。但是当UART硬件的transmit阻塞时,过程就会有许多改动。

transmit complete interrupt

uartstart中,当UART硬件的transmit满,uartstart就直接return了。

当UART硬件的transmit空,就会发送transmit complete中断。中断在kerneltrap被接收,经过devintr转发,最终来到了uartintr:

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 devintr().
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();
release(&uart_tx_lock);
}

此时,第一个while循环会直接退出,因为压根没有get到字符。所以,这时候,就会去执行uartstart,然后继续读未完成读取的缓冲区。

等到所有都读完了,最后一次发送transmit complete中断时,会在uartstart进入该分支:

1
2
3
4
if(uart_tx_w == uart_tx_r){
// transmit buffer is empty.
return;
}

然后就不会再发送transmit中断了。

感觉这点是真的牛逼。uartintr这个函数完美兼顾了两种情况【这也归功于uartstart做得很健壮】:1. 外部输入数据到console,2. 接收数据未结束,继续接收

Concurrency in drivers

用户进程与设备之间的读写交流,比如说上面的console,重点依靠于uart_tx_bufcons.buf这两个的正确性。因而,就需要保障它们的并发安全。在上面的代码中,使用到这两个的地方都被锁保护着。

在kernel中还需要格外注意的一点并发是,一个进程A在等待来自设备的中断,但此时另一个进程B在运行。这时候设备发出中断信号,CPU转入中断处理程序处理中断。此时,中断处理程序的执行不应该涉及到当前被中断进程的代码。例如,中断处理程序不能安全地使用当前进程的页表调用copyout(页表正是跟当前进程息息相关的)。中断处理程序通常做相对较少的工作(例如,只需将输入数据复制到缓冲区),并唤醒上半部分代码来完成其余工作。

Timer interrupts

Xv6 uses timer interrupts to maintain its clock and to enable it to switch among compute-bound processes; the yield calls in usertrap and kerneltrap cause this switching.

1
2
3
4
// in kernel/trap.c usertrap()
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
1
2
3
4
// in kernel/trap.c kerneltrap()
// give up the CPU if this is a timer interrupt.
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();

RISC-V requires that timer interrupts be taken in machine mode, not supervisor mode. As a result, xv6 handles timer interrupts completely separately from the trap mechanism laid out above.

xv6启动时调用过start.cstart.c处于机器态,并准备向内核态过渡。start.c中就对时钟进行了初始化timeinit()。要做的有以下几件事:

  1. program the CLINT hardware (core-local interruptor) to generate an interrupt after a certain delay.
  2. set up a scratch area to help the timer interrupt handler save registers and the address of the CLINT registers
  3. start sets mtvec to timervec and enables timer interrupts.
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
// arrange to receive timer interrupts.
// they will arrive in machine mode at
// at timervec in kernelvec.S,
// which turns them into software interrupts for
// devintr() in trap.c.
void
timerinit()
{
// each CPU has a separate source of timer interrupts.
int id = r_mhartid();

// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

// prepare information in scratch[] for timervec.
// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id);
scratch[4] = interval;
w_mscratch((uint64)scratch);

// set the machine-mode trap handler.
w_mtvec((uint64)timervec);

// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);

// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
}

计时器中断处理程序必须保证不干扰中断的内核代码。基本策略是处理程序要求RISC-V发出“软件中断”并立即返回。RISC-V用普通陷阱机制将软件中断传递给内核,并允许内核禁用它们。处理由定时器中断产生的软件中断的代码可以在devintr (kernel/trap.c:204)中看到:

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
// in kernel/trap.c devintr()
} else if(scause == 0x8000000000000001L){
// software interrupt from a machine-mode timer interrupt,
// forwarded by timervec in kernelvec.S.

// 只看其中一个CPU的时钟中断计数的意思吗?确实,要是好几个一起来加倍了非常不合理
if(cpuid() == 0){
clockintr();
}

// acknowledge the software interrupt by clearing
// the SSIP bit in sip.
w_sip(r_sip() & ~2);

return 2;
}

void
clockintr()
{
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}

注意,w_sip(r_sip() & ~2);就对应着“RISC-V用普通陷阱机制将软件中断传递给内核”。【应该吧个人理解】

来源:rCore 手册(rCore tutorial doc)

riscv 中的中断寄存器

S 态的中断寄存器主要有 sie(Supervisor Interrupt Enable,监管中断使能), sip (Supervisor Interrupt Pending,监管中断待处理)两个,其中 s 表示 S 态,i 表示中断, e/p 表示 enable (使能)/ pending (提交申请)。 处理的中断分为三种:

  1. SI(Software Interrupt),软件中断
  2. TI(Timer Interrupt),时钟中断
  3. EI(External Interrupt),外部中断

比如 sie 有一个 STIE 位, 对应 sip 有一个 STIP 位,与时钟中断 TI 有关。当硬件决定触发时钟中断时,会将 STIP 设置为 1,当一条指令执行完毕后,如果发现 STIP 为 1,此时如果时钟中断使能,即 sieSTIE 位也为 1 ,就会进入 S 态时钟中断的处理程序。

可能SSIP跟这里的STIP差不多吧,都是时钟中断的标志。如果把SSIP clear掉,那么则说明不是时钟中断了,而是软中断了。

Real world

UART驱动程序读取UART控制寄存器,一次检索一字节的数据;因为软件驱动数据移动,这种模式被称为程序I/O(Programmed I/O)。程序I/O很简单,但速度太慢,无法在高数据速率下使用。需要高速移动大量数据的设备通常使用直接内存访问(DMA)。DMA设备硬件直接将传入数据写入内存,并从内存中读取传出数据。现代磁盘和网络设备使用DMA。DMA设备的驱动程序将在RAM中准备数据,然后使用对控制寄存器的单次写入来告诉设备处理准备好的数据。

当一个设备在不可预知的时间需要注意时,中断是有意义的,而且不是太频繁。但是中断有很高的CPU开销。因此,如网络和磁盘控制器的高速设备,使用一些技巧减少中断需求。一个技巧是对整批传入或传出的请求发出单个中断。另一个技巧是驱动程序完全禁用中断,并定期检查设备是否需要注意。这种技术被称为轮询(polling)。如果设备执行操作非常快,轮询是有意义的,但是如果设备大部分空闲,轮询会浪费CPU时间。一些驱动程序根据当前设备负载在轮询和中断之间动态切换。

UART驱动程序首先将传入的数据复制到内核中的缓冲区,然后复制到用户空间。这在低数据速率下是可行的,但是这种双重复制会显著降低快速生成或消耗数据的设备的性能。一些操作系统能够直接在用户空间缓冲区和设备硬件之间移动数据,通常带有DMA。

Lab: networking

In this lab you will write an xv6 device driver for a network interface card (NIC).

这个概述光是听起来就让人觉得热血沸腾。网络的本质其实就是IO设备,这一点我一直觉得很牛逼,而现在我居然要亲手实现网络……That’s very cool.

On this emulated LAN, xv6 (the “guest”) has an IP address of 10.0.2.15.

Qemu also arranges for the computer running qemu to appear on the LAN with IP address 10.0.2.2.

When xv6 uses the E1000 to send a packet to 10.0.2.2, qemu delivers the packet to the appropriate application on the (real) computer on which you’re running qemu (the “host”).

We’ve added some files to the xv6 repository for this lab.

The file kernel/e1000.c contains initialization code for the E1000 as well as empty functions for transmitting and receiving packets, which you’ll fill in.

kernel/e1000_dev.h contains definitions for registers and flag bits defined by the E1000 and described in the Intel E1000 Software Developer’s Manual.

kernel/net.c and kernel/net.h contain a simple network stack that implements the IP, UDP, and ARP protocols.

These files also contain code for a flexible data structure to hold packets, called an mbuf.

Finally, kernel/pci.c contains code that searches for an E1000 card on the PCI bus when xv6 boots.

Your job:

Your job is to complete e1000_transmit() and e1000_recv(), both in kernel/e1000.c, so that the driver can transmit and receive packets. You are done when make grade says your solution passes all the tests.

感想

说实话,一开始看题的时候真是感觉非常地哈人……但其实文档看着看着,心中也逐渐有了个大概,最后再结合下指导书的提示【当然不是后面那些保姆级的Hints】,最后写的也就八九不离十了。总体上来说,我觉得这次实验的代码还是很简单的,它主要难在探究过程,也就是从一开始什么也不懂,然后去阅读硬件设备的文档,结合代码尝试去理解,最后一步步写出来的过程。本次实验耗时六小时,我觉得肯定有不少于一半,甚至可能达到2/3的时间都耗费在理解上。这种从零开始探究的过程给了我很大的收获,同时也稍微提高了我面对挫折的能力。

这个实验确实设计得很有教育意义。除了我上面说的它锻炼了我的能力以外,它其实还具有比较深刻的工业意义。在看书的时候,书中这么写道:

In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

本次实验正是上述描述的简化版:E1000的文档很详细,并且我们只用掌握一部分它的功能就行了。但虽然简化了,其探究过程的内在逻辑还是不会改变的。

总之,我很喜欢这次实验的设计。我的评价是牛逼。

思路

正确思路

Hints写得很详细,不做赘述了。主要就是明确一下数据结构的问题:

  1. rx_ring和tx_ring是两个分开的队列

    它们只是结构一模一样,都是阴影部分表示software持有,白色部分表示硬件持有。

    因而,对于rx来说,白色部分表示需要传给协议栈的包,因而我们需要把白色部分转化为阴影部分;对于tx来说,白色部分表示网卡将要发送的包,因而我们需要把阴影部分转化为白色部分。

    image-20230220234406239

  2. rx_mbufs和tx_mbufs

    一开始不知道这俩是啥,后来才意识到,这俩和第1点的那俩其实是下标一一对应的关系。也就是说rx_ring[i]这个descriptor接收到的数据存在rx_mbufs[i],tx_ring[i]要发送的数据存在tx_mbufs[i]。知道了这个之后,代码就简单了。

    忏悔:我一开始真没反应过来。计网我记得是有一模一样的结构的,看来算是白做了2333

个人的推理过程

一开始就先懵懵懂懂地看指导书,直到看到这句话:

Browse the E1000 Software Developer’s Manual.

然后我这时连自己要干什么都迷迷糊糊,但姑且还是按他下面说的,准备先浏览第二章了。然而,我发现要我看我也还是看不懂啊,所以我就直接放弃了。【经验1:看不懂就算了,别死磕了

我放弃了第二章后,就再次从头开始细细看了一遍这句话之前的指导书,也结合了一下它给的代码。这次总算是差不多弄懂这次要做什么了:

实现driver的两个函数,从而实现对网卡进行数据的取出和送入。数据是eth frame。数据取出后要通过net_rx传递给上层协议栈。数据是mbuf类型的。

所以我们只需实现协议栈最底下的部分,也即从网卡读写数据,其他一些别的东西比如协议栈什么的都已经写好了。

但是那些什么rx_ring,还有各种奇奇怪怪的寄存器,我都看不懂,所以我就去看第三章了。初次略过一遍感觉还是一脸懵逼不知道干什么,但我带着“我们要做的是driver”这样的想法,在第二遍细看的时候有意区分开什么是网卡硬件帮我们做的,什么是我们的driver软件需要做的(经验2:明确要做什么。我们需要做的是软件部分,它的文档一般会说Software should XXX,密切关注这部分就行),就差不多有了点实现的雏形:

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
45
46
47
48
49
for recv:
// 通过net_rx,网络包可以发送到udp顶层.
// 所以说,我们在这里的目的就是,通过与硬件网卡e1000进行交互,
// 取出e1000所接收到的数据包,检查数据的完整性,然后再把数据封装进mbuf结构体中,再通过net_rx传到上层

// 取出数据包
// 数据包存储在网卡的缓冲区中
// 一是获取网卡缓冲区长度的长度
// 网卡缓冲区长度存储在RCTL.BSIZE & RCTL.BSEX中
/*
*RCTL.BSEX = 0b:
00b = 2048 Bytes.
01b = 1024 Bytes.
10b = 512 Bytes.
1b1 = 256 Bytes.
RCTL.BSEX = 1b:
00b = Reserved; software should not program this value.
01b = 16384 Bytes.
10b = 8192 Bytes.
11b = 4096 Bytes
*
* */
// 二是获取数据包存放在哪个地址
// 数据包的buffer cache的地址存储在descriptor的字段中
// 必须读取多个descriptor以确定跨越多个接收缓冲区的数据包的完整长度。
// 那么我们要读取的这些descriptor存放在哪呢?
// 看文档,似乎差不多意思是这些descriptor被以环形队列的形式组织在一起,也许正是
// 本文件内的rx_ring这个数组。
// 当有descriptor到达e1000,e1000就会把它从host memory中取出来,存入到descriptor ring
// 也即我们rx_ring数组
//
// 所以我们要做的,就是遍历rx_ring数组,如果rx_ring数组中的元素是used的,那么表明它就是数据包的一部分
// 也即它地址所指向的buf里存放的是数据包的一部分数据
//
// 那么我们怎么知道这个rx_ring的元素有没有used,以及它是第几个呢?
// 检查descriptor有没有used:status字段不为全0则为used
// 并且硬件要求,我们在发现这个descriptor的status不为0,并且用完这个descriptor之后,需要将
// 其status字段置零,以供硬件使用
// Status information indicates whether the descriptor has been used and whether the referenced
// buffer is the last one for the packet.

// 三是获取数据包的数据
// 我们需要获取decriptor的该字段,然后再从这个地址读取数据包数据
// 网卡和内存统一编址,这个数据实际上就是网卡的buffer
// 我们应该直接通过read这个系统调用就可以对其进行读写了

// check数据包
// 检查RDESC.ERRORS位,如果包发生了错误,再检查,如果发现RCTL.SBP、RCTL.UPE/MPE都被标记,
// 就接收这个包,否则直接丢弃

可以看到,跟正确思路虽然很多细节理解上有点问题,但是大体框架还是大差不差。然后再阅读指导书:

When the E1000 receives each packet from the ethernet, it first DMAs the packet to the mbuf pointed to by the next RX (receive) ring descriptor, and then generates an interrupt. 【这句话可得知,descriptor们存放在代码中的rx_ring中。】

Your e1000_recv() code must scan the RX ring and deliver each new packet’s mbuf to the network stack (in net.c) by calling net_rx(). You will then need to allocate a new mbuf and place it into the descriptor, so that when the E1000 reaches that point in the RX ring again it finds a fresh buffer into which to DMA a new packet.

就差不多是正确思路了。transmit的实现也是同理

代码

以下代码不知道为什么过不了test,我跟别人的逻辑一模一样也还是不行emmm

它的问题是,不会接收到外界的返ping,导致进程一直等待网卡IO,所以kerneltrap一直触发不了,无法正常网卡读写,从而导致fileread会一直处于sleep等待状态,整个系统就沉睡了【】我感觉应该是transmit没发成功。

等以后有精力再来看看吧。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int
e1000_transmit(struct mbuf *m)
{
acquire(&e1000_lock);
struct tx_desc tx = tx_ring[regs[E1000_TDT]];
if((tx.status & 1) == 0){
release(&e1000_lock);
return -1;
}
if(tx_mbufs[regs[E1000_TDT]] != 0) mbuffree(tx_mbufs[regs[E1000_TDT]]);
tx.addr = (uint64) m->head;
tx.length = m->len;
tx.status |= 1;// EOP
tx.cmd |= 1;//EOP
tx.cmd |= 8;//RS
tx_mbufs[regs[E1000_TDT]] = m;
regs[E1000_TDT] = (regs[E1000_TDT]+1)%TX_RING_SIZE;
// printf("send successful!\n");
release(&e1000_lock);
return 0;
}

static void
e1000_recv(void)
{
printf("go into e1000_recv\n");
acquire(&e1000_lock);
while(1){
//while(regs[E1000_RDT]!=regs[E1000_RDH]){
printf("go into while\n");
regs[E1000_RDT] = (regs[E1000_RDT] + 1)%RX_RING_SIZE;
int i=regs[E1000_RDT];
if(rx_ring[i].status != 0){
// 包含所需数据包
// 检查是否发生了错误
//if((rx_ring[i].status & 1) !=0 && (rx_ring[i].status & 2) != 0){
// // error字段有效
// if(rx_ring[i].errors != 0){
// 发生错误,直接丢弃
// goto end;
// }
if((rx_ring[i].status & 1) == 0){
release(&e1000_lock);
return ;
}
// 将地址对应数据包发送
struct mbuf* m = rx_mbufs[i];
m->len = rx_ring[i].length;
net_rx(m);
rx_ring[i].status = 0;
struct mbuf* mbuf = mbufalloc(MBUF_DEFAULT_HEADROOM);
rx_ring[i].addr = (uint64) mbuf->head;
rx_mbufs[i] = mbuf;
}
}

release(&e1000_lock);
}