上次讲到如何从pci核心驱动一步一步的进入了rtl8139网络驱动,并且调用的第一个函数是驱动的probe函数,即rtl8139_init_one,本文就从这里入手,简单的介绍rtl8139网络驱动的相关原理和源码分析。
1 rtl8139_init_one
上文讲到当实现了驱动和设备的匹配后,需要设备和驱动做一些相应的工作,如正常使用前的初始化操作等,rtl8139_init_one就实现了一些初始化操作,原则上probe函数应该尽可能的短,尽量避免执行耗时的操作。rtl8139_init_one仅仅实现了两个结构体struct net_device和struct rtl8139_private的初始化。前一篇文章中也提到了数据结构的抽象层次的问题,在网络子系统中,所有的网络设备都用net_device来表示,但是并不是所有的网络设备都有相同的属性,因此,对应不同的网络设备增加一个private数据结构来描述,这里就是struct rtl8139_private。
rtl8139_init_one主要函数和功能分析
(1)dev = rtl8139_init_board (pdev);
a). dev = alloc_etherdev (sizeof (*tp)); --> 分配struct rtl8139_private数据结构,并进行预初始化,之所以称之为预初始化是因为只进行了某些固定数据成员的初始化。
b). 调用pci核心驱动的接口函数:pci_enable_device (),pci_enable_device也是一个内核开发出来的接口,代码在drivers/pci/pci.c中,笔者跟踪发现这个函数主要就是把PCI配置空间的Command域的0位和1位置成了1,从而达到了开启设备的目的,因为rtl8139的官方datasheet中,说明了这两位的作用就是开启内存映射和I/O映射,如果不开的话,那我们以上讨论的把控制寄存器空间映射到内存空间的这一功能就被屏蔽了。
pci_resource_[start|end|flags|len]:在硬件加电初始化时,BIOS固件统一检查了所有的PCI设备,并统一为他们分配了一个和其他互不冲突的地址,让他们的驱动程序可以向这些地址映射他们的寄存器,这些地址被BIOS写进了各个设备的配置空间,因为这个活动是一个PCI的标准的活动,所以自然写到各个设备的配置空间里而不是他们风格各异的控制寄存器空间里。当然只有BIOS可以访问配置空间。当操作系统初始化时,他为每个PCI设备分配了pci_dev结构,并且把BIOS获得的并写到了配置空间中的地址读出来写到了pci_dev中的resource字段中。这样以后我们在读这些地址就不需要在访问配置空间了,直接跟pci_dev要就可以了,我们这里的四个函数就是直接从pci_dev读出了相关数据,代码在include/linux/pci.h中。具体参见PCI配置空间相关的介绍。
c). rc = pci_request_regions (pdev, DRV_NAME);通知内核该设备对应的IO端口和内存资源已经使用,其他的PCI设备不要再使用这个区域
d). 获得当前pci设备对应的IO端口和IO内存的基址。
2. rtl8139_open
此函数在网络设备端口被打开时调用,例如执行命令ifconfigeth0up,就会触发这个函数,此函数是真正的rtl8139网络设备的初始化函数。这个函数主要做了三件事。
①注册这个设备的中断处理函数。
当网卡发送数据完成或者接收到数据时,是用中断的形式来告知的,比如有数据从网线传来,中断也通知了我们,那么必须要有一个处理这个中断的函数来完成数据的接收。关于Linux的中断机制不是我们详细讲解的范畴,但是有个非常重要的资源我们必须注意,那就是中断号的分配,和内存地址映射一样,中断号也是BIOS在初始化阶段分配并写入设备的配置空间的,然后Linux在建立pci_dev时从配置空间读出这个中断号然后写入pci_dev的irq成员中,所以我们注册中断程序需要中断号就是直接从pci_dev里取就可以了。
retval=request_irq(dev->irq,rtl8139_interrupt,SA_SHIRQ,dev->name,dev);
if(retval){
returnretval;
}
我们注册的中断处理函数是rtl8139_interrupt,也就是说当网卡发生中断(如数据到达)时,中断控制器8259A把中断号发给CPU,CPU根据这个中断号找到处理程序,这里就是rtl8139_interrupt,然后执行。rtl8139_interrupt也是在我们的程序中定义好了的,这是驱动程序的一个重要的义务,也是一个基本的功能。request_irq的代码在arch/i386/kernel/irq.c中。
②分配发送和接收的缓存空间
根据官方文档,发送一个数据包的过程是这样的:先从应用程序中把数据包拷贝到一段连续的内存中(这段内存就是我们这里要分配的缓存),然后把这段内存的地址写进网卡的数据发送地址寄存器(TSAD)中,这个寄存器的偏移量是TxAddr0=0x20。在把这个数据包的长度写进另一个寄存器(TSD)中,它的偏移量是TxStatus0=0x10。然后就把这段内存的数据发送到网卡内部的发送缓冲中(FIFO),最后由这个发送缓冲区把数据发送到网线上。
好了现在创建这么一个发送和接收缓冲内存的目的已经很显然了。
上面这段代码负责把发送缓冲区虚拟空间进行了分割。
重新RESET设备后,我们要激活设备的发送和接收的功能,上面这行代码就是向相关寄存器中写入相应值,激活了设备的这些功能。
static const unsigned int rtl8139_tx_config =
TxIFG96 | (TX_DMA_BURST << TxDMAShift) | (TX_RETRY << TxRetryShift);
RTL_W32 (TxConfig, rtl8139_tx_config);
上面这行代码是向网卡的TxConfig(位移是0x44)寄存器中写入TX_DMA_BURST<<TxDMAShift这个值,翻译过来就是6<<8,就是把第8到第10这三位置成110,查阅管法文档发现6就是110代表着一次DMA的数据量为1024字节。
3. 网络数据包的收发过程
当一个网络应用程序要向网络发送数据时,它要利用Linux的网络协议栈来解决一系列问题,找到网卡设备的代表net_device,由这个结构来找到并控制这个网卡设备来完成数据包的发送,具体是调用net_device的hard_start_xmit成员函数,这是一个函数指针,在我们的驱动程序里它指向的是rtl8139_start_xmit,正是由它来完成我们的发送工作的,下面我们就来剖析这个函数。它一共做了四件事。
①检查这个要发送的数据包的长度,如果它达不到以太网帧的长度,必须采取措施进行填充。
②把包的数据拷贝到我们已经建立好的发送缓存中。
主要实现了把skb结构中的数据拷贝到tp->tx_buf[entry]指向的发送缓冲区中。
在拷贝函数中需要注意几个问题:
a.如何计算要拷贝的skb的数据的长度,即这里的csstart的计算,这里参考下面的公式:
Ifskbis linear (i.e.,skb->data_len == 0), the length ofskb->dataisskb->len.
Ifskbis not linear (i.e.,skb->data_len != 0), the length ofskb->datais(skb->len) - (skb->data_len)for the head ONLY. The rest must seestruct skb_shared_info->frags[i].sizeandstruct skb_shared_info->frag_list, which contains a linked-list ofstruct sk_buffbecause, deducing from [2],
The rest of the data is not stored as a separateskbif the length of the data permits, but as an array ofstruct skb_frag_structinstruct skb_shared_info([4]: To allow 64K frame to be packed as singleskbwithoutfrag_list).struct skb_frag_structcontainsstruct page *to point to the true data. If the length of the data is longer than that that can be contained in the array,struct skb_shared_info->frag_listwill be used to contain a linked-list ofstruct sk_buff(i.e., the data undergo fragmentation because, according to [1], thefrag_listis used to maintain a chain of SKBs organized for fragmentation purposes, it isnotused for maintaining paged data.)
As an additional information,skb->truesize = skb->len + sizeof(struct sk_buff). Don't forget thatskb->lencontains the length of the total data space that theskbrefers to taking into accountSKB_DATA_ALIGN()and non-linear condition.
skb->lenis modified when doingskb_pull(),skb_push()orskb_put().
③光有了地址和数据还不行,我们要让网卡知道这个包的长度,才能保证数据不多不少精确的从缓存中截取出来搬运到网卡中去,这是靠写发送状态寄存器(TSD)来完成的。
我们把这个包的长度和一些控制信息一起写进了状态寄存器,使网卡的工作有了依据。
④判断发送缓存是否已经满了,如果满了在发就覆盖数据了,要停发。
谈完了发送,我们开始谈接收,当有数据从网线上过来时,网卡产生一个中断,调用的中断服务程序是rtl8139_interrupt,它主要做了三件事。
①从网卡的中断状态寄存器中读出状态值进行分析,status = RTL_R16 (IntrStatus);
if((status&(PCIErr|PCSTimeout|RxUnderrun|RxOverflow|
RxFIFOOver|TxErr|TxOK|RxErr|RxOK))==0)
gotoout;
上面代码说明如果上面这9种情况均没有的表示没什么好处理的了,退出。
②NAPI接收机制
napi_schedule_prep(&tp->napi)判断以下当前驱动是否支持NAPI或者NAPI需要的前提条件是否满足,如果满足,设置中断屏蔽字,屏蔽之后产生的中断,然后激活一个软中断,具体代码如下:(至于list_add_tail将会稍后分析)
在软中断注册的轮询函数中完成网络数据包的接收操作。
③发送中断处理
如果是传输完成的信号,就调用rtl8139_tx_interrupt进行发送善后处理。
下面我们先来看看接收中断处理函数rtl8139_rx,在这个函数中主要做了下面四件事
①这个函数是一个大循环,循环条件是只要接收缓存不为空就还可以继续读取数据,循环不会停止,读空了之后就跳出。
intring_offset=cur_rx%RX_BUF_LEN;
rx_status=le32_to_cpu(*(u32*)(rx_ring+ring_offset));
rx_size=rx_status>>16;
上面三行代码是计算出要接收的包的长度。
②根据这个长度来分配包的数据结构
pkt_size = rx_size - 4;
skb = netdev_alloc_skb_ip_align(dev, pkt_size);
③如果分配成功就把数据从接收缓存中拷贝到这个包中
这里采用了wrap_copy和skb_copy_to_linear_data两个拷贝函数,实质还是调用了memcpy()。
const unsigned int len)
现在我们已经熟知,&rx_ring[ring_offset+4]就是接收缓存,也是源地址,而skb->data就是包的数据地址,也是目的地址,一目了然。
④把这个包送到Linux协议栈去进行下一步处理
在netif_receive_skb (skb)函数执行完后,这个包的数据就脱离了网卡驱动范畴,而进入了Linux网络协议栈里面,把这些数据包的以太网帧头,IP头,TCP头都脱下来,最后把数据送给了应用程序,不过协议栈不再本文讨论范围内。