本文是 6.S081 操作系统课程学习最后一个 lab,编写一个 intel 的 e1000 网卡的驱动在 xv6 下。需要复习知识有:操作系统知识,计算机组成原理 DMA 相关,循环缓冲区的概念,e1000 的粗略 spec 和其具备两个环形缓冲区和其引发中断的方式,理论上感觉做这个 lab 只需要 livelock 课程的前面讲网络的基础知识部分记忆和通读 lab 的 handout(包括 hint) 就能很快做出来。下面记录以下具体过程,穿插学习 Linux 下如何编写网卡驱动的 Real World 实现(毕竟 xv6 只是个 toy)。希望写完本文的时候能够具备一些 Linux low level 的驱动知识。

PCI 标准驱动实现和规格

Peripheral Component Interconnect 外部互联标准,PCI 总线是一种并行同步系统总线,集中式独立请求仲裁(每个 dev 都有一条请求线和总线使用线),具体仲裁优先级和算法由 PCI 具体实现。同步获取总线是利用REQ#和GNT#两个信号线实现的,前者用于某一个设备占用总线的请求,后者允许某一设备占用总线和应答。

先看 PCI 接口 DMA 技术网卡的模型:

其中 Packet  Buffer 就是 RAM 了, 网卡就是右边那块 TX 和 RX 的 MAC 硬件,这里网卡通过 DMA Engine 来把卡上的存储之类的东西的内容一次复制到内存中去。而 PCI 的作用就是负责管理这些外设卡。

PCI 编址

PCI 的地址编码来访问不同的设备。

直接看 xv6 的 pci.c 我们探测 PCI 设备的时候直接遍历 dev 和 func 等。注意这里的 bus 是总线编号,观察一下 window 的设备管理器有惊喜,可以发现核显和主板自带的一些外设件都在 bus 0 下,而涉及主板上的 PCIE 口(笔记本pcie网卡和独立显卡)都接到 PCI bus 1,2去了(当然 PCIE 和 PCI 的机制不一样)如下图:

我们把 PCI 的 bus 0 叫做 up-stream 总线,bus 1 到以后的(还有到 bus 20 的)涉及一些下级的桥接,他再桥接到 bus 0 上的叫做 down-stream, 具体不究太多了。我们这里认为 Intel 的这个 e1000 是接到 bus0 上的, 实际上我们需要 PCI probe 所有的设备的,这个 lab 我们直接指定了。然后 function 的编号是因为一个 device 可能有多个 function,不过我一开始以为笔记本的 pci或者pcie网卡的 wifi 和 蓝牙 是按这个分 function 走的,结果发现实际是 wlan 走 pcie,蓝牙走 usb(minipcie、ngff 的 pci 接口都自带兼容 usb 接口),实际是两个分开的芯片。所以 function 这方面很难举例了。工业上比如4通道的采样卡就是用 function 实现多通道数据并行传输的。

直接看代码,我们根据上面的 PFA 来遍历 Bus Device Function 来查找我们要的卡。探测卡的信息就涉及到一个 convention 了,PCI 约定了 pci 上的地址的低位 offset 的一部分地址空间用于登记设备的信息,具体实现就不是 O/S 做的了, 他大概是 PCI 相关的南桥来搞的. (这一点保留意见).

    // PCI address: //   |31 enable bit|30:24 Reserved|-//  -|23:16 Bus num|15:11 Dev num|10:8 func num|7:2 off|1:0 0|.
uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);

PCI 设备元数据规格

我们看 Intel 的 dev manual 给的 PCI 的设备信息(针对本 e1000 网卡). 但是头部的 Device ID 等内容是通用的. 所以我们很容易能读到 0h offset 的一行的 ID 信息来判断并装入驱动. 我们应当要记住,PCI 的作用就是完成 register 的 mapping 从而实现能够让 C 程序通过读取内存(vm)来访问设备的寄存器从而实现控制设备,之后的数据传输则是通过操纵那些寄存器来实现的(即控制设备)。一句话就是 PCI 在内存建了一个控制台之后程序就只用操作控制台了。

下面结合代码来看:

  voidpci_init(){// we'll place the e1000 registers at this address.// vm.c maps this range.uint64 e1000_regs = 0x40000000L; // qemu -machine virt puts PCIe config space here.// vm.c maps this range.uint32  *ecam = (uint32 *) 0x30000000L;// look at each possible PCI device on bus 0.for(int dev = 0; dev < 32; dev++){int bus = 0;int func = 0;int offset = 0;// PCI address: //   |31 enable bit|30:24 Reserved|-//  -|23:16 Bus num|15:11 Dev num|10:8 func num|7:2 off|1:0 0|.uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);volatile uint32 *base = ecam + off;// PCI address space header:// Byte Off   |   3   |   2   |   1   |   0   |//          0h|   Device ID   |   Vendor ID   |uint32 id = base[0]; // read the first line.// 10 0e (device id):80 86(vendor id)  is an e1000if(id == 0x100e8086){// PCI address space header:// Byte Off   |   3   |   2    |   1     |   0    |//         4h |Status register | command register |// command and status register.// bit 0 : I/O access enable// bit 1 : memory access enable// bit 2 : enable masteringbase[1] = 7;__sync_synchronize();for(int i = 0; i < 6; i++){// Byte Off              |   3   |   2    |   1     |   0    |// 16b/4b = 4        10h |           Base Address 0          |//          5        14h |           Base Address 1          |//          6        18h |           Base Address 2          |//          7    1ch~24h |          .... 3, 4, 5             |uint32 old = base[4+i];// writing all 1's to the BAR causes it to be// replaced with its size.base[4+i] = 0xffffffff;__sync_synchronize();// if we need a dynamic allocation, we can read the base[4+i] again, remove the low bits// and calc it's one's complement then plus 1 to get it's BAR size (a dma area).base[4+i] = old;}// tell the e1000 to reveal its registers at// physical address 0x40000000.base[4+0] = e1000_regs;e1000_init((uint32*)e1000_regs);}}}

计算的部分和用 uint32 读 bytes 我看代码注释都很清楚了,下面讲解其中几个要点。第一个是 0xffffffff 的意义。网卡内部有 flash 的而且拷贝数据不可能一个 bit 或者 byte 地拷贝效率太慢了,他的编地址机制应该是整数对齐的,所以会有一部分 bit 必须是0,我们写的时候无论你 low bits 填了1还是0,之后再 load 就会发现 low bits 始终 hard-wired to be 0b(b是二进制计数的意思…)(见下面表格的 Description). 这里 base address 的意思是注册一个内存地址给 PCI 设备,让他把 register 和NIC 的 flash 缓存内容往这个内存地址去 map。下面看一下 Intel 的 Manual 里面是怎么说这个 0xffffffff 的意思的。

这是上面的那个表格的部分详细版,然后看具体的字段意思。

这就很好理解了这个东西了。顺便摘录一段方便理解:

The Base Address Registers (or BARs) are used to map the Ethernet con-troller’s register space and flash to system memory space. In PCI-X mode or in PCI mode when the BAR32 bit of the EEPROM is 0b, two registers are used for each of the register space and the flash memory in order to map 64-bit addresses. In PCI mode, if the BAR32 bit in the EEPROM is 1b, one register is used for each to map 32-bit addresses.

初始化完 PCI 完成了一些 vm 的 mapping,之后就能够通过访问 vm address 来访问 register 了(注意看上面的表格,我们10h offset 即代码中的 base+4是 register 的地方放入 e1000_regs),之后我们转到 e1000_init() 去看怎么完成另一部分的初始化。

E1000 网卡驱动实现

e1000_init 做的事情主要有以下亿点:

  • reset 网卡,关闭网卡中断。(记住 e1000 是通过 interrupt 来告知操作系统他的一个 DMA 操作完成了)。
  • lazy allocate 地把 tx_ring 里面的全部状态设置为 done(即可以支持 OS 传来新的 tx 任务)。
  • allocate 所有的 rx_mbuf 以及设置 rx_ring 对应的 addr。
  • 设置网卡的记录 rx_ring 和 tx_ring 的 register,以及登记各种和循环缓冲区有关的 control registers 值。
  • 设置 MAC 地址
  • 在网卡上做一个空的多播表
  • 通过设置控制位来启动网卡 Transmit 部分和 Receive 部分(开机)。
  • 允许接受 Interrupt,即开网卡中断。

具体的这些到底是怎么样的详细机制我们下文再议,这里网卡的 init 完成之后,就会启动网卡。之后 lab 需要编写的 transmit 和 recv 函数到底在哪被调用呢? 我们上面讲了 e1000 是 DMA 到 buf 之后引发一个中断的,所以对!我们需要回到 trap.c 。

e1000_intr 要做的也很简单,就是调用 recv 来把 buf 的内容拿走。让 buffer 能够满足一个流动的条件。那么还有一个问题 transmit 是谁调用的? 这就是涉及我们的网络栈的部分了。我们从 lab 提供的 nettests.c 自顶向下来看。

首先看 ping 函数,该函数通过调用一个 syscall 来创建一个 file descriptor 来读写。

connect(dst, sport, dport))

我们进入看他作为 syscall 就是调用了 sockalloc 来创建 socket 接口。然后再 read 和 write 的时候调用 sockread 或者 sockwrite(sysnet.c 下)。sockwrite 将会调用 net_tx_udp 来完成一个 buffer 数据的写入。结论是 xv6 的 write 作用于 socket file 只支持 udp 调用(net.c 只实现了 udp)。我们再来看 net_tx_udp 不过是 encapsulate 一些 udp 头,还是通过 net_tx_ip 封装下层,然后 net_tx_eth 封装 ethernet frame, 进入 net_tx_eth 就看到了 e1000_transmit() 的调用了。

xv6 用户网络栈与驱动的调用结构

摘要 xv6 代码结构的图以下方便理解:

Linux 中的网络驱动

这里我要讲一个问题,这里 network stack 里 udp 怎么能直接调用 e1000 的函数呢,这对于计算机多样性(思考支持多种网卡的系统应该使用一种抽象封装的通用函数调用方案)而言是不好的。我们事实上 Linux 的实现必须用一套驱动管理系统。下面就来分析 Linux 的 RealWorld 版本的 network device driver。

由于这里我不打算 dive deep into the linux kernel,这里我们假定某些 infrastructure 已经给好了。我们需要提供一个驱动文件给 kernel 用。首先是对于 linux 的一些给 driver 用的 api 说明以下。

Linux 内核模块简介

首先是内核模块的概念,对于驱动我们是以内核模块的形式加载进入的,每个驱动的程序就编程层一个内核模块。Linux 在运行的时候 start_kernel 时会加载那些内核模块,其通过一个 do_initcalls 函数把一系列的 module_init() / init_module() 函数给调用了(他们两的区别暂且不管,涉及东西太多了,实际就是宏和入口的区别而已)。下面给出一个模块的例子(Linux Kernel Development 3rd):

这里的 module_init(hello_init) 就是把一个函数注册为模块的入口。当然也可以直接编写一个 init_module() 函数作为入口(这一点对于 main 函数经过 C runtime 包装后作为入口异曲同工)。至于怎么加载内核模块则太 technical 这里不讲了。当然这个 hello module 只有在加载和卸载的时候 print 一些东西。(至于学网卡驱动有什么用考虑虚拟网卡的好处)对于驱动而言,我们需要提供更多注册动作。

Linux 网卡驱动的层次结构

我们需要注册 net_device 结构体登记网卡信息,在不同的 Linux 内核版本中,这些结构体的内容多种多样,我选取其中一种来讲解。思想实验可以想到我们规定一个结构体来存储一些网卡信息同时存储一些在模块里的函数指针即可,然后利用订阅机制来给内核添加一个网卡。我写一部分伪代码在这里:

 struct net{ // in kernel.struct info some_info;struct pointer some_pointer;}struct net my_net;void send(){do_send();}void recv(){do_recv();}int init_module(){// PCI api 探测出网卡的地址my_card = pci_probe(id, vendor);// 进行上面提到的那些 register 的 vm mappingmap_registers(my_card);//写入一些信息如 MAC 地址混淆模式,多播广播信息等set_info(my_net);// 注册事件处理器(发送和接受)my_net.some_info.send =  send;my_net.some_info.recv =  recv;// 把网卡注册到内核里register_netdev(my_net);return 0;}void exit_module(){unregister_netdev(my_net);}

当然具体还会涉及一些数据结构(如 xv6 的 mbuf),但是这些编程太 dirty 太多 spec 内容(而且不同 linux 版本千差万别,比如你可以把一个 net_device 来存所有的 info 和 function pointers 或者分开来(net_device_ops),对 interrupt recv 的实现可以规定一个默认入口,也可以同样使用 function pointer 等等等等) 了,我们还要做 lab,这部分就不看下去了。讲解 Linux 的具体实现思路是因为 xv6 的过于简陋了思想实验就无法令人接受,也顺带帮助了解一下 Linux kernel module 的知识。

上文我们说具体的这些到底是怎么样的详细机制我们下文再议,好现在就来做这个 lab 了。本质上还是练习一个 lock 数据结构的访问的编程练习。所以这下我们的重点回到数据结构上。目前对那个循环的 buffer 实际上是有一个模糊的印象而已。我们必须分开来分析和编程。先从 tx 开始吧。

Ring Buffer 数据结构分析

lecture 上已经讲过了 network stack 的内容了,我这里也不想再做笔记了。下面给出 circular buffer 的结构以及要用的 register 指针的宏定义(红色字样为相应寄存器在 regs 数组的索引宏别名)。

我们这里要用到 TDT,因为 TDT 是他发送出去的一个空位置。正常来说全程由我们软件跟踪(因为他负责把包发送出去,所以硬件递增的只有 Head,Tail 只是标记让硬件暂停 transmitting 的一个 flag)所以看到 init 的时候把 TDT 和 TDH 都设置为 0.

然后我们读这里的操作 HINT 。

  • First ask the E1000 for the TX ring index at which it's expecting the next packet, by reading the E1000_TDT control register.
  • Then check if the the ring is overflowing. If E1000_TXD_STAT_DD is not set in the descriptor indexed by E1000_TDT, the E1000 hasn't finished the corresponding previous transmission request, so return an error.
  • Otherwise, use mbuffree() to free the last mbuf that was transmitted from that descriptor (if there was one).
  • Then fill in the descriptor. m->head points to the packet's content in memory, and m->len is the packet length. Set the necessary cmd flags (look at Section 3.3 in the E1000 manual) and stash away a pointer to the mbuf for later freeing.
  • Finally, update the ring position by adding one to E1000_TDT modulo TX_RING_SIZE.
  • If e1000_transmit() added the mbuf successfully to the ring, return 0. On failure (e.g., there is no descriptor available to transmit the mbuf), return -1 so that the caller knows to free the mbuf.

解释一下我们的数据结构,这里由一个 status 数组来跟踪我们的 circular buffer,他不负责数据。为了能保持跟踪我们的 mbuf,还要设置一个 mbuf 指针数组,这是回想我们 transmit 的 api 是上层用户提供一个  mbuf 给我们发的,但是我们放到到 ring buffer 的时候只是 local comitting,只有等到他的那个对应的 status 被网卡更新了(remote push,不过 spec 说了你可以指定网卡一 copy 到 flash 就 update status,也可以指定等到 sent 之后再 update)才能 free 掉我们的 mbuf 原件(销毁本地备份)。这个 status 是由硬件写进来的(handout 说的 the E1000 sets the E1000_TXD_STAT_DD bit in the descriptor to indicate this)。所以具体的数据结构如下:

其中 mbuf 指针数组 tx_mbufs 做的事情不过是做 hint 里要求的 stash away pointers to the mbufs presented in tx_rings 而已。(感觉这部分全部不写好让自己写反而更方便做这个 lab?因为 mbuf 的一些字段好像就没用到,为了理解这个好像有点花时间,不过这样就要涉及更多的读 specification 的工作了)代码如下给出:

 int e1000_transmit(struct mbuf* m) {//// Your code here.//// the mbuf contains an ethernet frame; program it into// the TX descriptor ring so that the e1000 sends it. Stash// a pointer so that it can be freed after sending.//acquire(&e1000_lock);uint32 tail = regs[E1000_TDT];// overflowif (tx_ring[tail].status != E1000_TXD_STAT_DD) {release(&e1000_lock);return -1;}if(tx_mbufs[tail]){mbuffree(tx_mbufs[tail]);}tx_ring[tail].length = (uint16)m->len;tx_ring[tail].addr = (uint64)m->head;tx_ring[tail].cmd = 9;tx_mbufs[tail] = m;regs[E1000_TDT] = (tail+1)%TX_RING_SIZE;release(&e1000_lock);return 0;}

recv 的则类似这里不赘述了,上图,

对 Intel Spec 里面的这幅图我也是物语了

6.S081 lab: networking e1000 网卡驱动 附 Linux 网卡驱动编写分析相关推荐

  1. STM32MP157驱动开发——Linux 网络设备驱动

    STM32MP157驱动开发--Linux 网络设备驱动 一.简介 STM32MP1 GMAC 接口简介 YT8511C 详解 二.驱动开发 1.网络外设的设备树 2.设备驱动 三.测试 网速测试 参 ...

  2. linux 网卡的驱动程序,Linux网卡驱动程序代码

    广告 100%的CPU性能,计算能力不会降低!选择最主流的云服务器来满足各种业务需求,有数百种流行的云产品和8888元起价套餐,可帮助行业恢复工作! 获取网卡信息的代码示例. 通过命令获取arp(地址 ...

  3. VM虚拟机虚拟网卡设置和Linux网卡配置

    VM虚拟机虚拟网卡设置和Linux网卡配置 首先理清虚拟机中的配置和本地电脑之间的关系. 这是三种虚拟机的网络链接模式,当使用vm虚拟机的时候,会选择一种模式作为网络连接的方法.这些模式分别在物理机上 ...

  4. 【驱动】linux设备驱动·字符设备驱动开发

    Preface 前面对linux设备驱动的相应知识点进行了总结,现在进入实践阶段! <linux设备驱动入门篇>:http://infohacker.blog.51cto.com/6751 ...

  5. STM32MP157驱动开发——Linux IIO驱动(上)

    STM32MP157驱动开发--Linux IIO驱动(上 ) 0.前言 一.IIO 子系统简介 1.iio_dev 结构体 2.iio_dev 申请与释放 3.iio_dev 注册与注销 4.iio ...

  6. STM32MP157驱动开发——Linux 音频驱动

    STM32MP157驱动开发--Linux 音频驱动 一.简介 1.CS42L51 简介 2.I2S总线 3.STM32MP1 SAI 总线接口 二.驱动开发 1.音频驱动 1)修改设备树 i2c 接 ...

  7. STM32MP157驱动开发——Linux IIO驱动(下)

    STM32MP157驱动开发--Linux IIO驱动(下) 0.前言 一.IIO 触发缓冲区 1.IIO 触发器 2.申请触发器 3.释放触发器 4.注册触发器 5.注销触发器 6. IIO 缓冲区 ...

  8. linux双网卡驱动配置,linux网卡驱动安装、双网卡绑定

    本次课程包含RAID0/1/5/6/10/50/60配置实验(使用Dell R720服务器实验).Redhat/CentOS/ubuntu/windows操作系统安装.windows/linux网卡绑 ...

  9. 80C51并行口结构与驱动 [附:按键消抖分析]

    80C51单片机有4个8位的并行I/O接口,分别是P0.P1.P2和P3.各口都是由口锁存器.输出驱动器和输入缓冲器组成.各口编址于特殊功能寄存器中,既有字节地址又有位地址.对各口锁存器的读写,就可以 ...

最新文章

  1. Structured Streaming编程 Programming Guide
  2. 解决uni-app ios唤起扫码操作,总是要刷新才可以唤起的问题
  3. html5新年网页做给父母的,2018春节给父母的简短祝福语
  4. CHM文件不能正确显示
  5. 用费曼技巧自学编程,香不香?
  6. BugkuCTF-Misc:做个游戏(08067CTF)
  7. 奇异值分解 VS 特征值分解
  8. 一个能够保护个人收藏夹隐私的Chrome扩展
  9. Java的@Serial批注
  10. VUE中父子组件传参(简单明了)
  11. 批处理脚本手动双击可以执行,但计划任务中执行失败
  12. 蓝桥杯 ALGO-94 算法训练 新生舞会
  13. 备库由于表无主键导致延迟
  14. 多线程NSObjectNSThreadNSOperationGCD
  15. 32个参数累加_「机械设计教程」滚珠丝杠选型过程中考虑的9个参数
  16. Win10系统更新后旧系统清理
  17. 什么算法计算地图上从A点到B点的方向?
  18. android 信号检测,卫星、手机信号都能测!安卓神器你值得拥有
  19. 风险评估-HEAVENS
  20. 笔记本硬盘直接安装win7系统教程(不用U盘和PE)

热门文章

  1. mysql导出权限授权_本文实例讲述了mysql数据库创建账号、授权、数据导出、导入操作。分享给大家供大家参考,具体如下:1、账号创建及授权grant all privileg...
  2. 在父亲节到来之际,强烈推荐德国幽默大师的连环漫画《父与子》,父子亲情跃然纸上(多图)...
  3. MySQL之MYISAM和INODB
  4. 极客日报:阿里将投入1000亿元助力共同富裕;Siri偷听用户对话被起诉 ;Linux Lite 5.6最终版正式发布
  5. 程序员的新年计划,你选择几个?
  6. 并发编程 定时线程池ScheduledThreadPoolExecutor学习总结
  7. 新路由D1 网件R6400 测速
  8. IDEA报错:Error: java: 错误:不支持发行版本5
  9. QQ,MSN,skype,goolge TALK,雅虎通,贸易通,淘宝旺旺在线客服代码
  10. [推荐]中国联通推出3G新套餐,基本套餐最低46元