所有的网卡驱动都有共同之处,至少有以下共同点:
1、由于报文大并且可能会很频繁收发,CPU频繁处理该外设将大大降低CPU处理其他事情的效率,对于所以所有的以太网卡,报文的收发都是采取DMA的方式;
2、所有网卡驱动和内核TCP/IP协议栈收发报文接口都是一样的;
3、在所有的网卡驱动中,创建代表该物理设备的接口(net_device),接口的概念见链路层一章的7.1节;
不同以太网卡驱动的不同之处都体现在该以太网卡的额外硬件功能,体现在接口描述符net_device结构体的priv成员,其他成员都是一样;
新项目中所使用的marvell网卡硬件功能强大,所以其代码也相对复杂,而旧项目中的普然(OPL-06750)网卡驱动非常合适用于理解网卡驱动,它的额外硬件功能几乎为0,所以非常适合理解。
所谓初始化的准备,就是把网卡硬件资源提供给即将诞生的网卡驱动,本质就是在platform总线下分别制造一个device和一个driver,然后下一步再实现probe;
普然网卡的platform总线下的device如下:
可见普然网卡的硬件资源是它的中断和网卡控制器寄存器,多数网卡驱动的硬件资源也就是这两个。
platform总线下driver的probe方法,目的就是初始化硬件参数和软件机制,在内核创建网络设备,硬件参数和软件机制因不同的网卡硬件和软件机制会各异,在内核创建的网络设备也会不同,但大致道理都是初始化硬件参数使网卡能够正常运转,并为后续的收发包、访问机制做软件机制的准备。
1、硬件参数初始化:
普然网卡的硬件参数初始化同样没有普遍意义,初始化了一些以太网链路层的参数(如速率双工、端口模式、MAC地址等等),比较有特点是初始化设置了DMA的入限速,限制为512packet/s,这是为了避免被海量报文导致CPU重启的现象,限速与不限速的现象可通过向CPU灌大量ARP报文(或其他可进CPU的报文)中发现。
2、软件机制初始化:
普然网卡的软件机制的特点体现在它的某一物理端口的报文收发方面,普然网卡有两个DMA,分别是DMA0和DMA1,却有3个物理端口,分别是两个千兆网口和一个百兆网口,;两个千兆口都由DMA0负责向CPU转发报文,百兆网卡由DMA1向CPU转发报文,如下图:
实际使用场景是,下行的要进CPU的报文由千兆网口1进入CPU,下行的普通业务报文(不进CPU)也由千兆网口1进入普然芯片,由千兆网口2下行流出,即千兆网口1兼做带内带外端口;千兆网口2用作带内端口,基本上只进入流出普通业务报文,上行的需进CPU的报文由百兆网口进入CPU,即百兆网口为带外端口,实际上这是一个交换机典型的模型,千兆网口2和百兆网口都连接交换芯片,交换芯片将把普通业务报文导向千兆网口2,把需进CPU的报文导向百兆网口。
所以可以想象导向百兆网口的报文就是各种已知情况的二层以太网业务相关的协议报文,诸如环路检测(以太网类型8898)、伙伴发现(880A)、组播协议报文(L4协议号为2)、pppoe报文(8863)等;
这些报文都被同一个进程处理,所以无需让这些报文一一进入内核TCP/IP协议栈再由用户进程由原始套接字获取,而可以由统一的方式让用户进程获取到,早期的方式是将这些报文在网卡驱动中识别后将其以太网类型统一修改成同一特殊值,用户进程统一获取该系列报文,然后再根据报文其他字段判断是哪种报文,这可以简化接收步骤,但用户进程还需在收到报文后再解析到底是哪一类协议报文,发送时还需改回来,也有弊端;
后来改成了一种更有效率的方式,使用系统调用的方式,通过在网卡驱动的接收报文流程中加入自定义的系统调用hook,让用户进程通过系统调用的方式获取报文,同样是统一接收报文,但无需再解析字段辨别报文类型,这样进一步简化了报文的处理;
增加的软件机制也就在这里,增加系统调用并在网卡驱动中加入相应hook,用户进程在没有报文时会被阻塞,有报文后被唤醒。
3、在内核创建网络设备:
虽然有三个物理网口,但事实上是由两个DMA收发,所以在内核实际创建的网络设备也是两个,分别是eth0和eth1。
在内核的每个网络设备以net_device结构体描述,该结构体非常复杂,但都有一个叫priv的成员用于描述该网络设备的私有特性,简单说来就是该网卡的自身特殊的功能,普然网卡的自身特殊功能几乎为0,包括:控制器寄存器基址、中断号、中断下半部工作队列(仅用于非NAPI模式)、收发buffer描述符、收发缓冲区等。
所以普然网卡驱动的创建网络设备的流程相对简单,如下:当配置eth0或eth1状态为up时,才可以通过ifconfig观察到它们,所有驱动一般都是在up时实际使能该网卡的收发功能,包括使能DMA、收发中断、挂接收发处理函数(中断下半部)等等,每个net_device在up时,调用它的open方法。
每个net_device在up时,调用它的open方法,主要内容包括:
1、在内核里挂中断,收发中断共用一个中断线(DMA0为25,DMA1为26),在中断处理函数中判断是接收中断还是发生中断;
2、使能对应的DMA,并使能网卡的硬件中断;
3、如果当前定制为非NAPI模式,还要初始化用于中断下半部处理的收发工作队列;
最后调用netif_start_queue使能该net_device,然后可以通过ifconfig观察到它们。
1、报文接收流程:
报文的接收是由网卡控制器收到报文后,通过DMA通道,把报文搬移到2.3时指定的内存位置处,并给CPU中断控制器产生一个相应的中断通知CPU,CPU在收到中断后,执行中断处理函数,通过读取网卡控制器的中断状态寄存器,可分辨出是发生了接收中断,再根据当前是否定制为NAPI模式,决定何种接收报文的方式。
对于NAPI模式,简单的说,就是根据报文的接收个数的多或少,决定是否继续接收,NAPI机制可有效的减少中断的次数,这可以提高CPU运行其他事务的时间,后面会着重描述普然网卡的NAPI实现。
上面是报文接收处理的大致流程,可用下图描述:
2、NAPI模式的好处:
事实上,NAPI模式的好处,也就是非NAPI模式的坏处,比如以下两种情况:
1、不停的收到报文:非NAPI模式将不停的产生中断,CPU不停的被中断,比如不停的收到1000个报文,那么NAPI模式每次中断发生轮询最多300个报文,这样4次中断可全部接收完,而非NAPI模式将固定的按照64个报文为单位,将至少产生16次中断;
2、在没有什么报文的时候,NAPI模式也没有浪费时间去轮询,它一样是在中断产生时才去轮询,当发现报文接收不多时一样会返回而不会继续轮询,效率和非NAPI模式也是一样的;
综上所述,NAPI模式不论在报文频繁时还是不频繁时,都不吃亏,在频繁时明显比非NAPI模式减少中断次数。关于NAPI模式的好处的描述,推荐下面的链接的文章,比较贴切:http://challengezcy.blog.163.com/blog/static/69229272201071022958988/
3、报文处理:
前面的2.2节描述了,对于从DMA1(物理端口是百兆网口)收到的报文会直接以唤醒用户进程被阻塞的系统调用的方式处理,我们的报文处理就是这样做的,并且还有一些其他的处理机制,如下图:
可见,很多报文都是送至系统调用,具体说来包括:
1、 OAM报文 + RPC报文 + pppoe报文(以太网类型8809、fefe、8863),DMA0下行;
2、 DMA0上下行的DHCP协议报文(广播发现、单播回应、广播客户去向、服务器回复);
3、 DMA1的上行所有报文,包括loopdetect、partner、igmp协议报文等;
其他报文还是通过TCP/IP协议栈处理或给用户进程处理。
除了以太网卡可以作为网卡设备外,还有很多其他非以太网卡设备可以作为网卡设备,比如wlan无线网卡、usb设备,都可以作为一个“网卡设备”,只要它们传输的报文可以符合IEEE规定的以太网报文格式,所以,能够作为一个“网卡设备”,只要它们传输的报文可以符合IEEE规定的以太网报文格式,或者在其设备驱动中整理成可以符合IEEE规定的以太网报文格式。
在linux中,所有的网络传输媒介,都用一个叫接口的东西来定义,如eth0、wlan1、usb2、vlan3、br4、eth0.1、lo等等,所谓传输媒介,可以是一个实际存在的以太网卡设备比如eth0,也可以是其他可以传输以太网格式报文的“网卡设备”比如wlan1、usb2,也可以是vlan或桥比如vlan3、eth0.1、br4等依赖于某个有物理设备支持的宿主接口,还可以是根本不存在物理设备支撑的东西如lo,但无论哪种,在内核中均以接口来定义,由结构体struct net_device描述,它包括了一切的接口的属性和方法;
这是一个庞大的结构体,它抽象了每个接口的属性和能力,诸如open、close、ioctl、接收、发送等等,也就是说每个接口的属性和能力都封装在结构体net_device中,这样做的好处是网卡驱动的上一层链路层乃至路由转发层、传输层、socket层都可以根据接口来区分和描述自己要操作的传输媒介,典型的如原始套接字就是在socket层通过接口来指定对哪个物理网卡设备中收发报文、路由转发层的每条路由表条目就包括了该种报文从哪个接口来、从哪个接口发送;
报文的实际收发动作是在网卡驱动中完成的,不同情况的网卡会有不同的驱动,即便都是以太网卡,具体驱动也是各式各样,就更不用说usb网卡、wlan无线网卡等形式的“网卡驱动”,但它们最终发送出去的肯定必须是以太网格式的报文,它们收到的数据不论最开始是否是以太网格式的报文,最终交给链路层时肯定必须是以太网格式的报文,这些都是网卡驱动编写者的工作,
具体的,网卡驱动必须在初始化时创建和注册其所代表的物理网卡设备对应的接口及其相关的基本属性和方法,如以太网卡驱动应初始化创建其对应的接口(如名为eth0的接口)并初始化其open、close、ioctl、接收、发送等等必需的属性和方法,可能的还包括MAC地址查改、组播相关、poll等属性和方法,这是由物理网卡设备的能力决定的,然后由函数register_netdev将该接口注册进内核。