在没有引入pmd用户态网卡驱动之前, 网卡在收到报文后,网卡驱动会将报文从网卡缓冲区拷贝到内核, 接着内核在把报文拷贝到应用层,整个过程需要2次的拷贝以及系统调用。当应用层需要发送数据时,应用层将报文拷贝到内核,接着内核拷贝到网卡缓冲区,由网卡负责发送,整个过程也需要2次的拷贝以及系统调用。 不管接收还是发送报文,系统调用以及内存拷贝都是需要消耗性能的。 在引入了pmd用户态驱动后,情况就完全不一样了。pmd为用户态驱动,这是应用层自己实现的一个网卡驱动程序,运行在应用层, 网络报文不经过内核,相当于把内核旁路了。pmd通过轮询的方式直接从dma控制器获取报文,而无需经过内核,也就减少了系统调用以及用户态与内核态频繁数据拷贝的性能开销。应用层要发送报文时,也是直接将报文交给dma控制器,由dma控制器负责报文的发送。
在分析pmd用户态驱动之前,先来介绍下如何学习pmd驱动。
一、如何学习pmd驱动
虽然pmd运行在用户态,但也是一个网卡驱动程序。 既然是网卡驱动程序也就少不了对网卡硬件进行配置操作,例如设置网卡接收缓冲区,发送缓冲区的大小。这里说的对网卡进行配置, 也就是对网卡寄存器进行配置。每个网卡都有自己的配置空间,配置空间里面有很多的寄存器,每种寄存器各自负责不同的功能。例如接收控制寄存器,用于对网卡接收到报文的一些设置; 中断寄存器,用于设置允许产生哪里中断事件,例如链路中断。在pmd驱动代码e1000中,可以看到网卡驱动所做的事情绝大部分是对寄存器进行设置或者读取操作。这就需要有一个手册,来介绍这些寄存器的使用,如果没有这些手册,则分析pmd驱动是绝对看不懂的,鬼知道往某个寄存器写入的数值代表什么意思。每种网卡型号都有自己的datasheet数据手册, 可以自行从intel官网下载。文章末尾也给出了82574; 82583两款网卡的数据手册的下载链接。
1、寄存器的位置在哪里
在每一个网卡的配置目录下,都有一个resouce文件,里面记录了这个网卡的配置空间的开始位置,结束位置,以及配置空间的总大小。这个配置空间里面有一堆的寄存器,可以对这些寄存器进行设置,读取操作。
在e1000网卡驱动eth_igb_dev_init初始化函数中,会将网卡硬件结构的hw_addr指针直接指向网卡的配置空间。
static int eth_igb_dev_init(__attribute__((unused)) struct eth_driver *eth_drv,
struct rte_eth_dev *eth_dev)
{
//指向网卡的配置空间
hw->hw_addr= (void *)pci_dev->mem_resource[0].addr;
}
2、网卡寄存器的读写
当要从网卡中读取某个寄存器的内容时,可以调用E1000_READ_REG,例如E1000_READ_REG(hw, E1000_ICR)读取中断寄存器,进而知道当前发生了哪些中断事件; 当需要设置某个网卡寄存器时,可以调用E1000_WRITE_REG,例如E1000_WRITE_REG(hw, E1000_IMS, intr->mask)将设置开启哪些中断。 不管是读取,还是设置,可以看出最终都会调用E1000_PCI_REG_ADDR,访问的是网卡的配置空间。
//网卡寄存器的地址
#define E1000_PCI_REG_ADDR(hw, reg) ((volatile uint32_t *)((char *)(hw)->hw_addr + (reg)))
//读取某个网卡寄存器
#define E1000_READ_REG(hw, reg) e1000_read_addr(E1000_PCI_REG_ADDR((hw), (reg)))
//设置某个网卡寄存器
#define E1000_WRITE_REG(hw, reg, value) E1000_PCI_REG_WRITE(E1000_PCI_REG_ADDR((hw), (reg)), (value))
3、网卡有哪些寄存器
每种网卡型号的数据手册中,都会描述当前网卡支持哪些寄存器,寄存器中的每个位代表什么意思。下面是82574网卡支持的寄存器列表部分截图。
以CTRL设备控制寄存器为例,这个寄存器一共由32位组成,也就是4个字节。每一个位都代表着不同功能,例如是否支持全双工, 是否开启自动协商速率等。往这些位写入1表示设置,写入0表示清除设置。
4、网卡配置空间的格式
Base Address 0指向的位置就是网卡的配置寄存器的地址列表,里面包含了很多的寄存器信息,这些寄存器也就是上图列出的 那些。至于这个网卡配置空间每个字段的格式,读者自行查看网卡的数据手册吧,里面对每一个字段都进行了详细的解释。
二、网卡数据空间的开辟
pmd里面会维护一个网卡数组,对于每一个网卡结构,都会维护这个网卡的接收发送数据的回调,关联的驱动等信息。以此同时,对于每一个网卡,都会创建一个以之一一对应的网卡数据空间结构,这个结构维护了网卡 的接收队列,发送队列信息。后续报文的收发,都会用到这个网卡数据空间结构。
static int rte_eth_dev_init(struct rte_pci_driver *pci_drv, struct rte_pci_device *pci_dev)
{
//从内存池上开辟所有网卡的数据空间结构
eth_dev = rte_eth_dev_allocate(ethdev_name);
}
struct rte_eth_dev * rte_eth_dev_allocate(const char *name)
{
//从内存池上开辟所有网卡的数据空间结构
rte_eth_dev_data_alloc();
//从网卡数据空间中获取一个结构,交由网卡
eth_dev = &rte_eth_devices[nb_ports];
eth_dev->data = &rte_eth_dev_data[nb_ports];
snprintf(eth_dev->data->name, sizeof(eth_dev->data->name), "%s", name);
eth_dev->data->port_id = nb_ports++;
}
接下里将来分析下网卡驱动的初始化过程。e1000网卡的驱动,对于的初始化函数为eth_igb_dev_init。对于82571, 82583等网卡,驱动初始化都是这个接口。
static int rte_eth_dev_init(struct rte_pci_driver *pci_drv, struct rte_pci_device *pci_dev)
{
//网卡初始化,例如e1000网卡驱动的接口为eth_igb_dev_init
diag = (*eth_drv->eth_dev_init)(eth_drv, eth_dev);
}
三、pmd驱动初始化
每个网卡都有mac层,phy物理层,nvm层,vbx层。pmd用户态主要是为这些层提供一个初始化接口。后续应用层调用接口实现某些功能时,例如对某些寄存器的设置,函数内部会调用这些层的接口来对寄存器进行配置。先来看下整体的框架结构
mac层提供了一个统一的抽象接口,例如
//mac层操作接阔
struct e1000_mac_operations
{
s32 (*init_params)(struct e1000_hw *);
s32 (*id_led_init)(struct e1000_hw *);
s32 (*blink_led)(struct e1000_hw *);
...
}
而e1000_mac.c这个文件择主要是对mac层的各个接口提供默认的实现,通常是一个空函数。
对于每一个网卡型号,网卡自己都可以重新实现这个mac层的各个接口,相当于重载。当然,如果具体的网卡不提供这个mac层的接口实现, 则还是会使用mac层的默认实现方式。例如e1000_82571.c这个文件,就是对这个网卡重新实现mac层的各个接口功能。
相应的,nvm层,vbx层都是这样的一种框架结构,掌握了mac层,其他层也是类似的。而e1000_api.c是一个调度器,调度默认实现的各个层以及调用具体网卡的实现。
s32 e1000_setup_init_funcs(struct e1000_hw *hw, bool init_device)
{
//注册mac层通用操作接口, 后续可以重载
e1000_init_mac_ops_generic(hw);
//注册物理层通用操作接口,后续可以重载
e1000_init_phy_ops_generic(hw);
//注册nvm层通用操作接口,后续可以重载
e1000_init_nvm_ops_generic(hw);
//注册mbx层通用操作接口,后续可以重载
e1000_init_mbx_ops_generic(hw);
}
现在对于pmd驱动的框架已经分析完了,现在从代码层上分析下几个需要注意的地方。驱动初始化开始的时候,会注册一个报文接收回调, 同时还会注册一个报文发送回调。关于pmd驱动实现报文收发,后续会有一篇文章专门分析,这里只需要知道这两个接口是在驱动初始化的时候设置的就好了。
//驱动初始化入口
static int eth_igb_dev_init(__attribute__((unused)) struct eth_driver *eth_drv,
struct rte_eth_dev *eth_dev)
{
//注册报文接口回调
eth_dev->rx_pkt_burst = ð_igb_recv_pkts;
//注册报文发送回调
eth_dev->tx_pkt_burst = ð_igb_xmit_pkts;
}
另一个需要注意的是, pmd既然是一个驱动,必然会提供一些给应用层调用的接口。例如:当应用层调用rte_eth_dev_configure配置网卡时,这个函数内部会调用dev_configure接口; 当应用层调用rte_eth_rx_queue_setup设置接收队列时,函数内部会调用rx_queue_setup。 pmd提供了一个抽象接口层,例如:
struct eth_dev_ops
{
eth_dev_configure_t dev_configure; /**< 配置网卡,例如eth_igb_configure,Configure device. */
eth_dev_start_t dev_start; /**< 启用网卡,例如eth_igb_start,Start device. */
eth_dev_stop_t dev_stop; /**< 停用网卡,Stop device. */
eth_dev_set_link_up_t dev_set_link_up; /**< 设置链路up,Device link up. */
eth_dev_set_link_down_t dev_set_link_down; /**< 设置链路down,Device link down. */
}
不同的网卡类型,都需要实现这个接口,是不是很像c++中的多态设计思想。例如:e1000网卡的实现接口为eth_igb_ops; ixgbe万M网卡的实现接口为ixgbe_eth_dev_ops。
下一篇文章将来分析应用层用来设置网卡,或者获取网卡信息的接口