由MAC地址谈到网卡驱动

近日,有个需求是,通过u-boot进行PXE启动,采用的硬件为Xilinx的Zedboard.

再这里,碰到个严重的问题,mac地址冲突。对于绝大多数的嵌入式系统而言,其mac地址冲突是个经常发生的问题。如果mac地址冲突,则会造成2层交换机的工作不正常。表现在外面的现象则是,两台冲突机器同时ping包,同时,只有一台机器能够正常通信。

这种不正常现象是做高层开发的难以遇见的情况。本文就不再分析mac地址冲突对网络造成的影响,因为不同的硬件交换机对mac地址冲突所采取的处理方式不同。

OK,返回正题,让俺们深入mac地址,谈谈内核及uboot中的内核驱动。

首先,提一个问题,mac地址是否可以修改?绝大多数以教科书为范本的“学霸”的答案是“不可以”,不少喜欢折腾的同学的答案是“可以”。但是俺的答案是“不一定可以”。

为什么不可以,则要从ifconfig这个程序说起。大家可以轻松地找到如何修改MAC地址。

sudo ifconfig eth0 hw ether 08:00:00:00:00:01

那么问题来了,这个指令到底深入到内核去,它干了啥?深入到内核,则必然联系到驱动这个层面,当然在Unix/Linux世界里面,通常是module来替代。

一个不干活的驱动

首先,俺们来看一个简单的rt8139的驱动
其实准确地说,下面根本就不是驱动代码。

如上的代码可以通过cc -I/usr/src/linux-2.4/include/ -Wall -c rtl8139.c的指令编译后,通过insmod进入内核,便能够通过ifconfig -a看到该设备。当然,代码太古老,以至于现在难以编译通过。不过通过如上的代码可以看到网络驱动的大概。

关注46行,
register_netdev
将 rtl8139 注册为网络设备,如果成功,则网络设备就注册成功了,这时,利用ifconfig就能看到rtl8139这个设备了。

关注40行,就能找到这个设备。40行init里面赋,值为rtl8139_init函数,该函数接受一个net_device作为参数,

关注41行,注意到驱动编写者需要做的,直接利用内核传入的net_device勾住初始化代码。将net_device结构内的open,stop,hard_start_xmit函数hook在rtl8139_open,rtl8139_release,rtl8139_xmit上面。

其中open/rtl8139_open函数会在用户调用ifconfig rtl8139 up时被调用。stop/rtl8139_release会在用户调用ifconfig rtl8139 down时调用。而hard_start_xmit/rtl8139_xmit会在协议栈向驱动注入数据时,请求发送时使用,例如socket接口中的write函数。

当然,上面的代码实际上并非rtl8139的驱动的代码,因为实际上,所有的hook的函数,实际上除了打印一些信息以外什么也没干。

看完以上,诸位似乎已经大体地了解到什么是一个网卡驱动程序了。不过现实的网卡驱动程序可没有这么简单。首先,聪明的读者似乎已经注意到,俺的网卡驱动里面似乎并没有涉及到数据的接收过程,其次,俺的网卡驱动里面也没有任何的MAC地址的配置信息。

解剖Realtek 8139驱动

接下来,俺利用Kernel 4.3的Realtek 8139的驱动程序8139too.c来讲解MAC地址与驱动的关系。代码由于过长,就不再贴出来,可以自行点击上方的超链接下载。(注:所有的代码行均缘于8139too.c文件,本文限于篇幅,只粘贴重要代码及其函数)

烦人的MAC地址

关注2238行开始的函数rtl8139_set_mac_address,该函数接受两个参数,第一个参数是net_device结构体,用以描述该网络设备,第二个参数是个空指针,用于传送用户空间传来的MAC地址值。
可以看到验证完以太网地址后,直接利用
memcpy(dev->dev_addr, addr->sa_data, dev->addr_len);
将用户的值设置进入net_device对象的dev_addr内部,这一步就直接告诉内核,用户想要改变网卡设备的地址。

在配置完内核维护的网络设备的地址后,立即看到一把spinlock加锁,然后利用各种宏开始写寄存器。之后进行锁的释放。

实际上,写寄存器的主要目的在于使物理的MAC芯片能够过滤非本机的数据包,而不在于发送数据包的校验。

因此,在解析完此段代码后,俺想大部分读者已经能理解,为什么俺在文章开始的时候提到了MAC地址不一定可以被修改的原因。简单地说,如果驱动程序直接就return了,实际上是达不到修改MAC地址的功能的。

那么肯定会有读者会问,既然MAC地址能够被写入,那俺们教科书上所谓的与网卡绑定的MAC地址又是在哪里呢?

这个问题的答案在rtl8139_init_one函数中1004-1007行。循环内部不断地写入dev->dev_addr这个变量,而这个变量正是内核用以保存MAC地址的数据结构。

解锁收包逻辑

接下来,俺们来了解下数据接收的功能。即网卡驱动怎么拿数据,再怎么将数据递交个上层。这一块由于linux内核的NAPI使得问题分析变得复杂。

让俺们定位到1333行,rtl8139_open中,调用了request_irq,注册中断处理函数。
retval = request_irq(irq, rtl8139_interrupt, IRQF_SHARED, dev->name, dev);
将中断的函数的第二个参数为rtl8139_interrupt,即中断处理函数。

继续追踪rtl8139_interrupt函数,2197-2202行。

这几行核心代码用于收取数据包。其中8139的驱动中,直接调用__napi_schedule函数进行处理。而这个__napi_schedule函数所做的,就是将将本数据包的处理交给NAPI系统。等待NAPI系统进行统一调度。具体的细节可直接看考Linux Foudation的关于NAPI的这篇文章

总之上,等待调度时间到了,  NAPI会调用某个hook的poll函数,8139通过1013行的这段代码进行hook。
netif_napi_add(dev, &tp->napi, rtl8139_poll, 64);
那么真正的处理函数便交给了rtl8139_poll函数。

该函数中核心在于rtl8139_rx函数的调用,通过调用这个函数,直接进入最后的skb打包并递交上层的流程。

rtl8139_rx函数大部分工作是从从环形队列中取出数据包,并打包上传。
定位到rtl8139_rx的2038-2067行。就能找到最重要的两个调用:
skb->protocol = eth_type_trans (skb, dev);
netif_receive_skb (skb);
分别解析数据包,并调用上层IP或ARP逻辑。
至此,驱动所有工作就完成了。接下来正式进入TCP/IP协议栈进行处理。这里就不在本文的涉及范围。

总结:
俺在分析网卡驱动逻辑的时候,被NAPI系统给折腾得不轻,目前的Linux已经不像是书本的设计了,本来interrupt函数中可以直接进行skb打包,但是NAPI的介入,使得调用逻辑不再连贯。而绝大多数的针对于网卡驱动的分析是建立在旧驱动的基础上。因此,俺写了本篇文章,希望大家避免走弯路。

最后,贴出我所参考的4.3内核中的源代码文件8139too.c,留以备份。
谢谢观看。


No comments:

Post a Comment