Linux 网络设备驱动整理
一. PCI设备注册
每个PCI设备驱动都有一个pci_driver变量,它描述了一个PCI驱动的信息,例如本文中的rtl8125_pci_driver;
pci_driver结构体中都有一个id_table成员变量,记录了当前这个驱动所能够进行驱动的那些设备的ID值,本文中对应的id_table为rtl8125_pci_tbl, 其中详细描述了该驱动所能够支持的PCI设备的ID号(0x8125,0x8162,0x3000)。
Rtl8125驱动程序的初始化是函数rtl8125_init_module,在这个函数中,调用了pci_register_driver函数,对rtl8125_pci_driver这个驱动进行注册。PCI的注册就是将PCI驱动程序挂载到其所在的总线的drivers链,同时扫描PCI设备,将它能够进行驱动的设备挂载到driver上的devices链表上来。
后续函数调用路径:
a: pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->pci_bus_match()->pci_match_device() ->pci_match_one_device
b: pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->really_probe()->pci_device_probe()->__pci_device_probe()->pci_call_probe()
driver_register调用bus_add_driver()函数,这个函数的功能就是将这个驱动加到其所在的总线的驱动链上;
bus_add_driver()函数中会调用driver_attach()函数,该函数会调用bus_for_each_dev遍历这个驱动所在的总线上的所有设备,然后将这些设备与当前驱动进行匹配,以检测这个驱动是否能够支持某个设备,也即是将设备与驱动联系起来,然后将每个设备以及当前驱动这两个指针传递给__driver_attach函数;
__driver_attach()函数是将驱动与设备联系起来的函数,也即是判断当前设备是否已经注册了一个驱动,如果没有注册驱动,则调用driver_probe_device()函数;
driver_probe_device首先会调用总线上的match函数( drv->bus->match:pci_bus_match),以判断当前的PCI驱动能否支持该PCI设备,执行路径pci_bus_match()->pci_match_device()->pci_match_one_device();
pci_match_one_device函数的作用是将一个PCI设备与PCI驱动进行比较,以查看它们是否相匹配(验证vendor,id,class等信息),如果相匹配,则返回匹配的pci_device_id结构体指针;
此时,如果该PCI驱动已经找到了相匹配的PCI设备,则返回,然后再退回到之前的driver_probe_device函数中。在该函数后将调用really_probe函数,将device_driver与device结构体指针作为参数传递到这个函数中;
此时的dev->bus为pci_bus_type,其probe函数则对应为:pci_device_probe;
pci_device_probe函数中会获得当前的PCI设备的pci_dev结构体指针以及PCI驱动程序的pci_driver结构体指针,继续调用函数__pci_device_probe,在该函数中还会调用函数pci_call_probe;
pci_call_probe会调用pci_driver的probe函数,对于这里的rtl8125驱动来说,它的probe函数是开始注册的rtl8125_init_one函数,在该函数中会完成对网卡设备net_device的初始化等操作。
static struct pci_device_id rtl8125_pci_tbl[] = { { PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8125), }, { PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8162), }, { PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x3000), }, {0,}, }; static struct pci_driver rtl8125_pci_driver = { .name = MODULENAME, .id_table = rtl8125_pci_tbl, .probe = rtl8125_init_one, .remove = __devexit_p(rtl8125_remove_one), #if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,11) .shutdown = rtl8125_shutdown, #endif #ifdef CONFIG_PM #if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,29) .suspend = rtl8125_suspend, .resume = rtl8125_resume, #else .driver.pm = RTL8125_PM_OPS, #endif #endif }; static int __init rtl8125_init_module(void) { int ret = 0; #ifdef ENABLE_R8125_PROCFS rtl8125_proc_module_init(); #endif #if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,0) ret = pci_register_driver(&rtl8125_pci_driver); #else ret = pci_module_init(&rtl8125_pci_driver); #endif return ret; }
注意:拥有硬件探测机制的总线,例如USB ,PCI总线上的设备不需要dts描述;没有探测机制的总线,如I2C 设备应该用dts描述。
PCIE网卡因为属于pci设备,不需要dts描述;但是PCIE控制器本身一般是CPU前端总线上的设备,这个总线不具备探测设备的能力,dts必须描述PCIE控制器。
static int __devinit rtl8125_init_one(struct pci_dev *pdev, const struct pci_device_id *ent) { struct net_device *dev = NULL; struct rtl8125_private *tp; void __iomem *ioaddr = NULL; static int board_idx = -1; int rc; assert(pdev != NULL); assert(ent != NULL); board_idx++; if (netif_msg_drv(&debug)) printk(KERN_INFO "%s 2.5Gigabit Ethernet driver %s loaded\n", MODULENAME, RTL8125_VERSION); rc = rtl8125_init_board(pdev, &dev, &ioaddr); if (rc) goto out; tp = netdev_priv(dev); assert(ioaddr != NULL); tp->set_speed = rtl8125_set_speed_xmii; tp->get_settings = rtl8125_gset_xmii; tp->phy_reset_enable = rtl8125_xmii_reset_enable; tp->phy_reset_pending = rtl8125_xmii_reset_pending; tp->link_ok = rtl8125_xmii_link_ok; rc = rtl8125_try_msi(tp); if (rc < 0) { dev_err(&pdev->dev, "Can't allocate interrupt\n"); goto err_out_1; } #ifdef ENABLE_PTP_SUPPORT spin_lock_init(&tp->lock); #endif rtl8125_init_software_variable(dev); RTL_NET_DEVICE_OPS(rtl8125_netdev_ops); #if LINUX_VERSION_CODE > KERNEL_VERSION(2,4,22) SET_ETHTOOL_OPS(dev, &rtl8125_ethtool_ops); #endif dev->watchdog_timeo = RTL8125_TX_TIMEOUT; dev->irq = rtl8125_get_irq(pdev); dev->base_addr = (unsigned long) ioaddr; rtl8125_init_napi(tp); #ifdef CONFIG_R8125_VLAN if (tp->mcfg != CFG_METHOD_DEFAULT) { dev->features |= NETIF_F_HW_VLAN_TX | NETIF_F_HW_VLAN_RX; #if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,22) dev->vlan_rx_kill_vid = rtl8125_vlan_rx_kill_vid; #endif //LINUX_VERSION_CODE < KERNEL_VERSION(2,6,22) } #endif /* There has been a number of reports that using SG/TSO results in * tx timeouts. However for a lot of people SG/TSO works fine. * Therefore disable both features by default, but allow users to * enable them. Use at own risk! */ tp->cp_cmd |= RTL_R16(tp, CPlusCmd); if (tp->mcfg != CFG_METHOD_DEFAULT) { dev->features |= NETIF_F_IP_CSUM; #if LINUX_VERSION_CODE < KERNEL_VERSION(3,0,0) tp->cp_cmd |= RxChkSum; #else dev->features |= NETIF_F_RXCSUM; dev->hw_features = NETIF_F_SG | NETIF_F_IP_CSUM | NETIF_F_TSO | NETIF_F_RXCSUM | NETIF_F_HW_VLAN_TX | NETIF_F_HW_VLAN_RX; dev->vlan_features = NETIF_F_SG | NETIF_F_IP_CSUM | NETIF_F_TSO | NETIF_F_HIGHDMA; #if LINUX_VERSION_CODE >= KERNEL_VERSION(3,15,0) dev->priv_flags |= IFF_LIVE_ADDR_CHANGE; #endif //LINUX_VERSION_CODE >= KERNEL_VERSION(3,15,0) dev->hw_features |= NETIF_F_RXALL; dev->hw_features |= NETIF_F_RXFCS; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,22) dev->hw_features |= NETIF_F_IPV6_CSUM | NETIF_F_TSO6; dev->features |= NETIF_F_IPV6_CSUM; netif_set_gso_max_size(dev, LSO_64K); #if LINUX_VERSION_CODE >= KERNEL_VERSION(3,18,0) dev->gso_max_segs = NIC_MAX_PHYS_BUF_COUNT_LSO2; #if LINUX_VERSION_CODE < KERNEL_VERSION(4,7,0) dev->gso_min_segs = NIC_MIN_PHYS_BUF_COUNT; #endif //LINUX_VERSION_CODE < KERNEL_VERSION(4,7,0) #endif //LINUX_VERSION_CODE >= KERNEL_VERSION(3,18,0) #endif //LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,22) #endif //LINUX_VERSION_CODE < KERNEL_VERSION(3,0,0) #ifdef ENABLE_RSS_SUPPORT if (tp->EnableRss) { dev->hw_features |= NETIF_F_RXHASH; dev->features |= NETIF_F_RXHASH; } #endif } #ifdef ENABLE_DASH_SUPPORT if (tp->DASH) AllocateDashShareMemory(dev); #endif #ifdef ENABLE_LIB_SUPPORT ATOMIC_INIT_NOTIFIER_HEAD(&tp->lib_nh); #endif rtl8125_init_all_schedule_work(tp); rc = rtl8125_set_real_num_queue(tp); if (rc < 0) goto err_out; rtl8125_exit_oob(dev); rtl8125_powerup_pll(dev); rtl8125_hw_init(dev); rtl8125_hw_reset(dev); /* Get production from EEPROM */ rtl8125_eeprom_type(tp); if (tp->eeprom_type == EEPROM_TYPE_93C46 || tp->eeprom_type == EEPROM_TYPE_93C56) rtl8125_set_eeprom_sel_low(tp); rtl8125_get_mac_address(dev); tp->fw_name = rtl_chip_fw_infos[tp->mcfg].fw_name; tp->tally_vaddr = dma_alloc_coherent(&pdev->dev, sizeof(*tp->tally_vaddr), &tp->tally_paddr, GFP_KERNEL); if (!tp->tally_vaddr) { rc = -ENOMEM; goto err_out; } rtl8125_tally_counter_clear(tp); pci_set_drvdata(pdev, dev); rc = register_netdev(dev); if (rc) goto err_out; printk(KERN_INFO "%s: This product is covered by one or more of the following patents: US6,570,884, US6,115,776, and US6,327,625.\n", MODULENAME); rtl8125_disable_rxdvgate(dev); device_set_wakeup_enable(&pdev->dev, tp->wol_enabled); netif_carrier_off(dev); printk("%s", GPL_CLAIM); out: return rc; err_out: if (tp->tally_vaddr != NULL) { dma_free_coherent(&pdev->dev, sizeof(*tp->tally_vaddr), tp->tally_vaddr, tp->tally_paddr); tp->tally_vaddr = NULL; } #ifdef CONFIG_R8125_NAPI rtl8125_del_napi(tp); #endif rtl8125_disable_msi(pdev, tp); err_out_1: rtl8125_release_board(pdev, dev); goto out; }
rtl8125_init_one 中一些重要函数
rtl8125_init_board
完成rtl8125 pci硬件相关的初始化
rtl8125_try_msi
当一个数据帧通过 DMA 写到内核内存 ringbuffer 后,网卡通过硬件中断(IRQ)通知其他系统。 设备有三种方式触发一个中断:
(1)MSI-X
(2)MSI
(3)legacy interrupts
设备驱动的实现也因此而异。驱动必须判断出设备支持哪种中断方式,然后注册相应的中断处理函数,这些函数在中断发生的时候会被执行。
MSI-X 中断是比较推荐的方式,尤其是对于支持多队列的网卡。 因为每个 RX 队列有独立的 MSI-X 中断,因此可以被不同的CPU处理(通过irqbalance 方式,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity)。后面会看到 ,处理中断的 CPU 也是随后处理这个包的 CPU。这样的话,从网卡硬件中断的层面就可以设置让收到的包被不同的 CPU 处理。
如果不支持 MSI-X,那 MSI 相比于传统中断方式仍然有一些优势,驱动仍然会优先考虑它。8125支援多队列,一般是使用MSI-X的中断方式。
rtl8125_init_napi
NAPI是综合中断方式与轮询方式的技术。数据量低时采用中断,数据量高时采用轮询。平时是中断方式,当有数据到达时,会触发中断处理函数执行,中断处理函数关闭中断开始处理。如果此时有数据到达,则没必要再触发中断了,因为中断处理函数中会轮询处理数据,直到没有新数据时才打开中断。这里分别init了tx和rx的poll函数。
rtl8125_init_all_schedule_work
这里是在初始化工作队列,它是将操作(或回调)延期异步执行的一种机制。工作队列可以把工作推后,交由一个内核线程去执行,并且工作队列是执行在线程上下文中,因此工作执行过程中可以被重新调度、抢占、睡眠。
register_netdev
分配好net_device对象并进行初始化后,驱动程序就可以通过register_netdev()向系统注册该网络设备对象了。
二.网络设备驱动
Linux网络设备驱动程序的体系结构如下图所示,从上到下可以分为4层,依次为网络协议接口层、网络设备接口层、提供实际功能的设备驱动功能层以及网络设备与媒介层,各层作用如下:
网络协议接口层: 向网络层协议提供统一的数据包收发接口,不论上层协议是ARP,还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据,发送和接收数据的单位都是sk_buff。这一层的存在使得上层协议独立于具体的设备。
网络设备接口层: 向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层中各函数的容器。网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。
设备驱动功能层: 设备驱动功能层的各函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。
网络设备与媒介层: 完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的函数在物理上驱动。对于Linux系统而言,网络设备和媒介都可以是虚拟的。
net_device 结构体在内核中指代一个网络设备,它定义于include/linux/netdevice.h 中,网络设备驱动程序只需通过填充 net_device 的具体成员并注册 net_device 即可实现硬件操作函数与内核的挂接,其主要的信息有:
(1)全局信息:网络设备的名称;
(2)硬件信息:设备所使用的共享内存的起始和结束地址,网络设备I/O基地址,设备使用的中断,DMA通道等;
(3)接口信息:网络设备的硬件头长度(ETH_HLEN),接口的硬件类型,最大传输单元(MTU),设备的硬件地址(dev_addr),网络接口标志(flags);
(4)设备操作函数:该结构体(netdev_ops)是网络设备的一系列硬件操作行数的集合,rtl8125驱动中设置的操作函数如下:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,29) static const struct net_device_ops rtl8125_netdev_ops = { .ndo_open = rtl8125_open, .ndo_stop = rtl8125_close, .ndo_get_stats = rtl8125_get_stats, .ndo_start_xmit = rtl8125_start_xmit, .ndo_tx_timeout = rtl8125_tx_timeout, .ndo_change_mtu = rtl8125_change_mtu, .ndo_set_mac_address = rtl8125_set_mac_address, #if LINUX_VERSION_CODE < KERNEL_VERSION(5,15,0) .ndo_do_ioctl = rtl8125_do_ioctl, #else .ndo_siocdevprivate = rtl8125_siocdevprivate, .ndo_eth_ioctl = rtl8125_do_ioctl, #endif //LINUX_VERSION_CODE < KERNEL_VERSION(5,15,0) #if LINUX_VERSION_CODE < KERNEL_VERSION(3,1,0) .ndo_set_multicast_list = rtl8125_set_rx_mode, #else .ndo_set_rx_mode = rtl8125_set_rx_mode, #endif #if LINUX_VERSION_CODE < KERNEL_VERSION(3,0,0) #ifdef CONFIG_R8125_VLAN .ndo_vlan_rx_register = rtl8125_vlan_rx_register, #endif #else .ndo_fix_features = rtl8125_fix_features, .ndo_set_features = rtl8125_set_features, #endif #ifdef CONFIG_NET_POLL_CONTROLLER .ndo_poll_controller = rtl8125_netpoll, #endif }; #endif
ndo_open( ) 函数的作用是打开网络接口设备,获得设备需要的I/O地址、IRQ、DMA通道等。stop( )函数的作用是停止网络接口设备,与 open( ) 函数的作用相反。
ndo_start_xmit( ) 函数会启动数据包的发送, 当系统调用驱动程序的 xmit 函数时,需要向其传入一个 sk_buff 指针,使得驱动程序能获取从上层传递下来的数据包。
ndo_tx_timeout( ) 当数据包的发送超时时该函数会被调用,该函数需采取重新启动数据包发送过程 或 重新启动硬件等措施来恢复网络设备到正常状态。
ndo_get_stats( ) 函数用于获得网络设备的状态信息,它返回一个 net_device_stats结构体指针。net_device_stats 结构体保存了详细的网络设备流量统计信息,如发送和接收的数据包数、字节数等。
ndo_do_ioctl( ) 函数用于进行设备特定的 I/O 控制。
ndo_set_mac_address( ) 函数用于设置设备的 MAC 地址。
设备注册与设备除名一般有 register_netdev和unregister_netdev完成。这两个是包裹函数,负责上锁,真正起作用的是其调用的register_netdevice和unregister_netdevice
register_netdevice开始设备注册工作,并调用net_set_todo,而net_set_todo最终会调用netdev_run_todo完成注册。
- 初始化net_device的部分字段
- 如果内核支持Divert功能,则用alloc_divert_blk分配该功能所需的数据空间块,并连接至dev->divert
- 如果设备驱动已经对dev->init进行初始化,则执行此函数。
- 由dev_new_index分配给设备一个识别码。
- 把net_device插入到全局表dev_base,以及两张哈希表dev_name_head,dev_index_head。
- 检查功能标识是否有无效的组合。
- 设置dev->state中的__LINK_STATE_PRESENT标识,使得设备能为内核所用。
- 用dev_init_scheduler初始化设备队列规则,以便流量控制用于实现Qos。
- 通过netdev_chain通知表链通知所有对本设备注册感兴趣的子系统。