e1000_transmit:
- 首先通过读取发送尾指针对应的寄存器 regs[E1000_TDT] 获取到软件可以写入的位置, 也就是后续放入下一个数据帧的发送队列的索引.
- 检查尾指针指向的描述符是否在状态中写入了 E1000_TXD_STAT_DD标志位. 根据开发手册 3.3.3.2 节关于 DD 标志位的描述, 该标志位会在描述符的数据被处理完成后设置. 因此未设置该标志位的描述符其数据仍未被硬件完成传输, 因此此时还不能进行数据写入, 返回失败.
- 这里实验指导中说是检查"溢出(overflowing)", 但是并未通过检查队满条件 (Tail+1)%Size==Head 判断的, 而是通过 DD 标志位, 根据开发手册, Head 指向的是由硬件所有的描述符的起始位置, 等待发送, 其 DD 标志位理论上还未被设置, 所以若遇到 DD 位未被设置的描述符, 则可说明队列已满.
- 检查尾指针执行的描述符对应的缓冲区是否被释放, 若未被释放使用 mbuffree() 进行释放.
- 更新尾指针指向的描述符的 addr 字段指向数据帧缓冲区的头部 m->head, length 字段记录数据帧的长度 m->len. 在上文也提到过, e1000_transmit() 是网络栈最后进行调用, 在上层 sockwrite() 中分配的缓冲区 m, 同时在一层层封装时会更新缓冲区头部(首地址)的位置字段 m->head
- 更新尾指针指向的描述符的 cmd 字段. 在 kernel/e1000_dev.h 中, 关于描述符 command 定义了 E1000_TXD_CMD_EOP 和 E1000_TXD_CMD_RS 两个标志位, 因此容易想到很可能设置的标志位与这两个有关. 根据开发手册 3.3.3.1 节, 容易判断出 EOP 标志位表示数据包的结束, 因此需要被设置
- RS 字段用于报告状态信息, 只有设置了该字段, 描述符的 status 字段才是有效的, 而网卡将数据包发送完成后会设置 DD 状态, 因此此处也需要对描述符设置 RS 标志位.
- 将数据帧缓冲区 m 记录到缓冲区队列 mbuf 中用于之后的释放. 这里是为步骤 3 做准备的. 数据帧的缓冲区 m 在此时还未被网卡硬件发送因此不能释放, 因此会将其记录到描述符对应的缓冲区队列中, 当后续尾指针又指向该位置时再将其释放.
- 最后即更新发送尾指针,而在这之前需要使用 __sync_synchronize() 来设置内存屏障. 这么做的原因是确保描述符的 cmd 字段设置完成后才会更新尾指针, 避免可能的指令重排导致描述符还未更新完毕就移动了尾指针.
- 由于 e1000_transmit() 上层由 sockwrite() 调用, 可能有多个进程同时调用该函数, 因此需要保证发送队列的指针的并发安全. 结合 e1000_init() 中初始化的 e1000_lock, 此处需要在整个操作过程前后加上锁, 以保证过程中只能有一个进程对队列尾指针进行访问及加载数据到发送队列.
int
e1000_transmit(struct mbuf *m)
{
uint32 tail;
struct tx_desc *desc;
acquire(&e1000_lock);
tail=regs[E1000_TDT];
desc=&tx_ring[tail];
if ((desc->status&E1000_TXD_STAT_DD)==0){
release(&e1000_lock);
return -1;
}
if (tx_mbufs[tail]){
mbuffree(tx_mbufs[tail]);
}
desc->addr=(uint64)m->head;
desc->length=m->len;
desc->cmd=E1000_TXD_CMD_EOP|E1000_TXD_CMD_RS;
tx_mbufs[tail]=m;
__sync_synchronize();
regs[E1000_TDT]=(tail+1)%TX_RING_SIZE;
release(&e1000_lock);
return 0;
}
e1000_recv:
- 首先通过读取接收尾指针对应的寄存器并加 1 取余 (regs[E1000_RDT]+1)%RX_RING_SIZE 获取到软件可以读取的位置, 也就是接收且未被软件处理的第一个数据帧在接收队列的索引. 该位置即软件需要解封装的数据帧的描述符.
- 与发送数据类似, 需要检查数据帧状态的 E1000_RXD_STAT_DD 标志位, 以确定当前的数据帧已被网卡硬件处理完毕, 可以由内核解封装. 否则则停止.
- 接收缓冲区 rx_mbufs[idx] 中为待处理的数据帧, 首先将其长度记录到描述符的 length 字段, 然后调用 net_rx() 传递给网络栈进行解封装.
- 调用 mbufalloc() 分配一个新的接收缓冲区替代发送给网络栈的缓冲区, 并更新描述符的 addr 字段, 指向新的缓冲区.
- 这里也能看出与发送缓冲区队列不同的地方, 发送缓冲区队列初始时全为空指针, 而缓冲区实际由 sockwrite() 分配, 在最后时绑定到缓冲区队列中, 主要是为了方便后续释放缓冲区.
- 而接收缓冲区队列在初始化时全部都已分配, 由内核解封装后释放内存.
- 而此处由于缓冲区已经交由网络栈去解封装, 因此需要替换成一个新的缓冲区用于下一次硬件接收数据.
- 由于替换了接收缓冲区, 此时描述符相当于更新为一个用于后续硬件接收数据的新的描述符, 因此需要清空 status 状态字段.
- 在实验指导中指出可能之前到达的数据包超过队列大小, 需要进行处理. 这里笔者考虑的是通过在 e1000_recv() 添加循环, 从而让一次中断触发后网卡软件会一直将可解封装的数据传递到网络栈, 以避免队列中的可处理数据帧的堆积来避免上述情况. 而终止条件即当前描述符的 DD 标志位未被设置, 则证明当前数据还未由硬件处理完毕.
- 最后更新接收尾指针 RDT. 需要注意的是, 正如前文所述, 尾指针需要指向最后一个已被软件处理的描述符, 是终止上述循环时的描述符的前一个. 此外, 此处没有使用 __sync_synchronize() 添加内存屏障(实际上可以添加到更新尾指针前), 原因是考虑到 while 循环的存在, 理论上更新尾指针的语句不会发生指令重排.
- 而关于锁的问题, 与发送数据不同, 接收时该函数只会被中断处理函数 e1000_intr() 调用, 因此不会出现并发的情况; 此外, 网卡的接收和发送的数据结构是独立的, 没有共享, 因此无需加锁. 且需要注意的是, 此处也不能使用 e1000_lock 进行加锁, 因为当接收到 ARP 报文时, 会在解封装的同时调用 net_tx_arp() 发送回复报文, 便会导致死锁 .
static void
e1000_recv(void)
{
uint32 tail=(regs[E1000_RDT]+1)%RX_RING_SIZE;
struct rx_desc *desc=&rx_ring[tail];
while(desc->status&E1000_RXD_STAT_DD){
if (desc->length>MBUF_SIZE){
panic("e1000 len");
}
rx_mbufs[tail]->len=desc->length;
net_rx(rx_mbufs[tail]);
rx_mbufs[tail]=mbufalloc(0);
desc->addr=(uint64)rx_mbufs[tail]->head;
desc->status=0;
tail=(tail+1)%RX_RING_SIZE;
desc=&rx_ring[tail];
}
regs[E1000_RDT]=(tail-1)%RX_RING_SIZE;
}