xv6 Lab (net)
Nowherechan 学徒

网络驱动这一章节,需要实现一个网络驱动。实际上写的过程中我主要是看 lab 官网的 hint,只有在遇到不确定的内容的时候再去看技术手册。

xv6 的网络结构

xv6 实现了简单的 udp、ip、arp,并抽象成 socket。下面以发送 udp 数据报的角度,自上而下简单进行一些介绍。

众所周知,访问网络是通过 socket 这一比较特殊的 file descriptor。在进行 write 系统调用时,会进入 kernel/file.c/filewrite 函数中,该函数会判断文件类型是否为 socket,然后进行 sockwrite 的调用。

1
2
3
4
5
// kernel/file.c/filewrite:188
#ifdef LAB_NET
else if(f->type == FD_SOCK){
ret = sockwrite(f->sock, addr, n);
}

sockwrite 函数在 kernel/sysnet.c 文件中。在 sockwrite 函数中,会通过 mbufalloc 函数来申请一个 mbuf,用来储存数据包的全部内容。mbuf 的定义在 kernel/net.h 文件中。

1
2
3
4
5
6
7
8
9
10
// kernel/net.h/mbuf:8
struct mbuf {
struct mbuf *next; // the next mbuf in the chain
char *head; // the current start position of the buffer
unsigned int len; // the length of the buffer
char buf[MBUF_SIZE]; // the backing store
};

// kernel/sysnet.c/sockwrite:188
m = mbufalloc(MBUF_DEFAULT_HEADROOM);

当创建 mbuf 时,head 指针并未指向 mbuf.buf 头部,而是中间的某个部分;而当数据包不断向底层传递,在数据外包裹一层又一层头的时候,head 指针就会不断往前移动;最终发送时候就是以 head 为起始的内容。

socketwrite 会调用 net_tx_udp,这个函数在 kernel/net.c 中,这个文件中包含的函数大都是协议内容,包括插入、去除每层的数据包头部。以插入(发送)为例,每一层都首先通过 mbufpushhdr 这个宏定义来移动 head 指针,然后将包(从 head 开始)转化为特定的数据结构以方便插入特定内容。

1
2
3
4
5
6
7
8
9
10
11
12
// kernel/net.c
#define mbufpushhdr(mbuf, hdr) (typeof(hdr)*)mbufpush(mbuf, sizeof(hdr))

char *
mbufpush(struct mbuf *m, unsigned int len)
{
m->head -= len;
if (m->head < m->buf)
panic("mbufpush");
m->len += len;
return m->head;
}

其实详细内容可以不用管。然后 net_tx_udp 调用 net_tx_ip 来塞入 ip 层的头,net_tx_ip 调用 net_tx_eth 来塞入链路层的头,最后再调用 e1000_transmit,而这个 e1000_transmit 即网络设备驱动所提供的函数,用来控制网卡设备,真正完成包的发送。

网卡驱动

发送

这里还是以发送数据包为例,首先说明网卡驱动究竟要“做一些什么事情”才能让包发送出去。

首先网卡是有芯片的,它会自己做一定的事情,我们所需要做的只是给网卡的一些控制寄存器赋值,从而告诉它应该做哪些事情以及怎样做。这些寄存器由于总线,被映射为特定的物理地址。因此给特定内存赋值其实就完成了设备的配置。实际上这些东西就跟嵌入式开发类似。

网卡驱动维护了 tx_ring 和 tx_mbufs,前者用于管理要发送数据,后者用于清除掉以及发送的包。

e1000 网卡自带 DMA 芯片,会自动从内存中读某些区域的数据然后当作包来发送,然后设置特定的位,但是需要操作系统来定义特定的结构体类型,并且维护特定的数据结构。这里,tx_ring 即维护的数据结构,这是个数组,每个项内存的是 tx_desc 结构体。

读 e1000 的初始化函数可以获得关于该设备的大量信息。首先通过 tx_ring 是一个循环队列,E1000_TDT 的内容记录着这个循环队列的尾部,E1000_TDH 记录头部。每需要发送一个包,就将尾部往后移动一位,然后在空出写进相关信息。网卡自己每发送完成一个包,就会把头部往后移动一位,然后把刚刚发送的包的状态进行设置(如果有 RS 的命令)。如果状态显示 DD 位是 1,那么说明这个地方是空的(之前放在这里的包已经发送完了),可以使用,而且这个时候还需要使用 mbuffree 来将 tx_mbufs 中保存的该位置的 mbuf 进行 free 来释放内存。

transmit 函数需要将上层传来的 mbuf 中的要发送数据写进某个 tx_desc 中(实际上只写了个地址,事后网卡会自己读那个地址中的内容,也就是数据),然后将 mbuf 的地址写进 tx_mbufs 的某个项中,以便于该包传完了,free 掉这个 mbuf。除此之外,还需要在写数据包地址的 tx_desc 中设置相关的包的描述信息,例如将 status 的 E1000_TXD_STAT_DD 位标记为 0,设置 length 为 mbuf 中的 len,设置 cmd 为 EOP(因为是 udp) 和 RS(因为需要记录下 DD)等。

接收

接收也是类似,同样是循环队列,同样有队头队尾,只不过,接收函数需要调用 net_rx 函数,然后 net_rx 从链路层到 IP 层再到 UDP 层,层层解包,并进行处理得到真正的数据,是一个自下而上的过程。

一套动作完成后,mbuf 在高层被释放掉,因此这里需要新创建一个 mbuf 填到该 rx_ring 中来让网卡接收到的包有地方去写。

代码

 Comments