背景
作为一个Linux内核开发的程序员,时常会被问到内核收报的处理过程,很多时候可以快速讲出一个大概,但关注的重点多在内核协议栈的报文处理过程,对于报文从到达网卡再到上送协议栈之间的处理过程总是很模糊,因此想就此过程进行学习研究,故有了本篇博客。
收包过程
网络收报处理的过程分为几个步骤:
1)硬件接收,网卡通过物理层或者数据链路层接收到数据帧
2) DMA传输,网卡通过DMA引擎将报文拷贝到ring_buffer缓冲区,并触发硬件中断,通知CPU有报文到来。
3)CPU硬件中断处理,将报文从ring_buffer中拷贝到内核报文缓冲区skb_buffer中,并放入报文接收队列中。
4)触发收报软中断NET_RX_ACTION
5)软中断处理,将报文从接收队列移入处理队列,并上送内核协议栈
6)内核协议栈报文处理
这里需要说明的是Linux 内核只负责L2-L4层的内容,L1物理层硬件负责,L4以上应用层负责。
主题分析
网络收报离不开网卡驱动,以Ubuntu 为例,常用的Intel网卡驱动为e1000模块,这里以e1000为例,介绍网卡收报过程,在此之前需要对驱动加载、网卡探测、请求中断等一系列过程有个了解。
在Linux 内核中,驱动以模块的形式存在,即驱动实质是一个内核模块,在Linux 内核启动,模块初始化过程中加载注册驱动,函数调用过程如下:
驱动注册过程如下:
static int __init e100_init_module(void)
{
if (((1 << debug) - 1) & NETIF_MSG_DRV) {
pr_info("%s, %s\n", DRV_DESCRIPTION, DRV_VERSION);
pr_info("%s\n", DRV_COPYRIGHT);
}
return pci_register_driver(&e100_driver);
}
由此可知e1000驱动为e100_driver,通过pci_register_driver注册到内核中,e100_driver定义如下:
static struct pci_driver e100_driver = {
.name = DRV_NAME, /* 驱动名 */
.id_table = e100_id_table,
.probe = e100_probe, /* 网卡探测函数 */
.remove = e100_remove, /* 网卡移除时调用,主要释放网卡相关资源 */
#ifdef CONFIG_PM
/* Power Management hooks */
.suspend = e100_suspend,
.resume = e100_resume,
#endif
.shutdown = e100_shutdown, /* 网卡shutdown */
.err_handler = &e100_err_handler,
};
pci_register_driver注册的实质就是将对应网卡的驱动挂载在相应的总线下,这样当网卡插入pci总线时,总线启动扫描并遍历下面挂载的所有驱动,依据设备信息(如厂商Id,设备ID信息等)去匹配相应驱动id_table内容,匹配后调用驱动的probe函数为网卡请求中断号,注册中断处理函数等一系列操作。
从e100_driver的定义可以出,驱动的几个核心函数为:e100_probe、e100_remove、e100_shutdown,这里我们主要介绍和收报有关的e100_probe 函数的处理逻辑。
probe函数
probe函数处理逻辑如下:
static int e100_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev;
struct nic *nic;
int err;
/* 申请并分配网络设备结构体变量netdev */
if (!(netdev = alloc_etherdev(sizeof(struct nic))))
return -ENOMEM;
/* 设置网络设备硬件特征 */
netdev->hw_features |= NETIF_F_RXFCS;
netdev->priv_flags |= IFF_SUPP_NOFCS;
netdev->hw_features |= NETIF_F_RXALL;
/* 设置网络设备操作函数: 如open ,close,tx_xmit, do_ioctl等 */
netdev->netdev_ops = &e100_netdev_ops;
SET_ETHTOOL_OPS(netdev, &e100_ethtool_ops); /*设置网络设备配置操作接口 */
netdev->watchdog_timeo = E100_WATCHDOG_PERIOD;
strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1);
nic = netdev_priv