Linux下Rtl8139too网卡设备驱动程序关键函数剖析

static int __devinit rtl8139_init_one (struct pci_dev *pdev, const struct pci_device_id *ent)

函数功能描述:赋给pci_driver结构体中的probe函数指针,用于当PCI核心检测到一个需要控制的pci_dev时,对相应的设备进行始化工作。

附注:本函数的主要任务是创建并初始化net_device结构,该结构是网卡设备的抽象。

函数流程:

1.         调用alloc_etherdev函数,创建以太网类型的网卡设备。它是对alloc_netdev函数的封装,内核中给出了用于设置以太网通用属性的setup函数。

2.         使能当前PCI设备,包括唤醒、热插拔等功能的建立。

3.         读取PCI配置空间中的Base Address RegisterPCI local bus specification规定,PCI配置空间有632位寄存器保存基地址(即bar0-bar5)。8139too网卡驱动程序使用了其中的两个来分别表示I/O空间基地址映射和内存空间基地址映射。

4.         接下来调用pci_request_regions函数申请资源。该函数实际上调用了request_regionrequest_mem_region,它们分别负责分配I/O端口和内存资源。(内核中资源一般分成四类,分别是I/O 端口,内存,DMA通道和中断线,它们都由resource结构描述,不同的是每一类资源拥有共同的基类对象,这一点上体现出了内核设计上的OO思想。实际上,典型的面向对象思想如封装、继承、多态在内核中都有体现。每一类资源都以树状结构进行组织,源码显示,这种树状结构实际上是一种“双亲表示法”和“孩子、兄弟表示法”的融合)。

5.         设置dev->base_addr字段,它视驱动程序采用I/O空间抑或内存空间而定。如果采用I/O空间,还要将I/O端口地址映射成内存地址(这是一种比较好的做法);否则,把PCI配置空间中bar1寄存器中的内存地址进行重映射,只有经过重映射,驱动程序才能够访问该内存区域,这里涉及到页表项的添加。

6.         复位芯片。

7.         初始化net_device结构中的部分回掉函数,它们负责完成数据包的接收和传输等任务。

static int rtl8139_open (struct net_device *dev)

函数功能描述:申请资源,启动硬件和开启监控线程。

函数流程:

1.         调用request_irq函数,以可共享的方式(IRQF_SHARED)分配给网卡设备中断线,中断号存放在dev->irq中。

2.         申请用于DMA的设备内存。调用pci_alloc_consistent函数,它返回两个地址,一个是供设备驱动程序使用的内核虚地址;另一个是供DMA控制器使用的总线地址(它在某些体系结构下等同与物理地址)。用该函数申请到的设备内存物理上必须是连续的,因为这里DMA操作是将成块物理上连续的数据在主存与网卡的FIFO缓存间传输。pci_alloc_consistent函数实际上是调用__get_free_pages函数获取内存页,并使用vrit_to_phys函数将得到的内存页虚地址转换成物理地址。

3.         初始化ring buffer.

4.         启动硬件。首先复位网卡设备,接着将从EEPROM设备中读取的硬件地址写入相应的寄存器,然后把第三步中获得的输入输出DMA缓冲区的总线地址赋予相应的寄存器,最后设置组播列表、使能所有已知的中断。

5.         启动传输队列,接受来自网络层的数据包。

6.         启动监控线程,该线程负责在传输超时的情况下做相应处理。该监控线程用工作队列实现。通过使用schedule_delayed_work函数,该线程函数可以在规定的时间点上被执行。

static int rtl8139_start_xmit (struct sk_buff *skb, struct net_device *dev)

函数功能描述:将网络层获得数据拷贝到DMA缓冲区

函数流程:

1.         sk_buf结构中的数据拷贝到四路DMA输出缓冲区中的一个,保证包的大小至少为ETH_ZLEN

2.         判断四路缓冲区是否已被用完,如果是,则调用netif_stop_queue函数来暂停网络层传输队列。这里要注意的是,完成sk_buf结构的拷贝后,要更新tp->rx_curr指针,它是一个unsigned long类型,同时,驱动程序还维护tp->rx_dirty指针。这两个指针负责四路DMA输出缓冲区的数据同步。它们都是不断递增的,当二者之差等于NUM_TX_DESC的时候,意味着四路DMA输出缓冲区都已被填满而且尚未被传输,这时候要做的自然是停止网络层继续向驱动程序发送skb

static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance, struct pt_regs *regs)

函数功能描述:中断处理

附注:通常情况下,中断的来临意味着FIFO中的数据已发送或DMA已将接受到的数据由FIFO拷贝到DMA缓冲区。更具体的来说,在禁止early mode的前提下,中断的发生表示一个完整的数据包被发送到了网络介质或者一个完整的数据包已通过DMA的传送到了内存缓冲区。为了不致使大量小数据包的接收给系统带来的频繁中断降低系统性能,Linux内核中提供了NAPI,即在关闭接收中断的状态下,以轮循的方式接收数据。

函数流程:

1.         读取中断状态寄存器,检查该中断是否属于当前网卡设备。由于以可共享的方式使用中断线,这一步是必须的。

2.         如果接收中断位被设置,首先调用netif_rx_schedule_prep函数,将设备对象的dev->poll方法加入到网络层的poll队列,如果函数执行成功,则清除中断状态寄存器的相应位(方法是在要清除的位写1),接着在设备对象上调用__netif_rx_schedule函数,它会在必要的时候出发一个软中断,从而通知网络层开始接受数据包(即调用dev->poll函数)。

3.         如果传输中断被位设置,则对已传送的数据包进行确认。具体的讲,就是通过读取相应的传输状态寄存器,获取传送的信息。芯片手册中规定,当驱动程序将数据包大小写入该寄存器的低12位的同时必须将其13位置0,而当传输DMA操作完成时,该位被自动置1,由此看来,这是与DMA控制器之间的握手操作。这里要明确指出的是,数据包由网络层传递给驱动程序并放置在DMA传输缓冲区的操作由上面谈到的rtl8139_start_xmit函数负责,中断处理函数中仅仅是对已发送的数据包的确认,同时记录一些信息,如发送的数据总量、发送的总数据包数、丢弃的总数据包数等等。

4.         步骤123都应在自旋锁的保护下进行。

static int rtl8139_poll(struct net_device *dev, int *budget)

函数功能描述:以轮循的方式将DMA输入缓冲区中的数据拷贝到sk_buf结构中并交付给网络层做进一步处理。

附注:本函数被赋值给net_device结构中的poll字段,它由软中断出发并不断被调用直到数据被处理完毕。整个过程要在输入自旋锁的保护下进行。

函数流程:

1.         清除中断状态寄存器的相应位。

2.         rx_ring输入DMA缓冲区中读取数据包,申请并初始化sk_buf结构,同时用rx_ring中的数据包填充之,接着调用netif_receive_skb函数把sk_buf结构交付给网络层。这里应该注意的是,驱动程序通过在rtl8139_private结构中的unsigned int变量cur_rx来维护rx_ring中多个数据包的处理序列。独立于每个包的头4个字节放置的是接收数据包的状态和长度。

3.         更新CAPRCurrent Address of Packet Read)寄存器,地址要双字对齐,应此,在计算cur_rx值时,使用表达式cur_rx = (cur_rx + rx_size + 4 + 3) & ~3

4.         重复步骤2中的操作直到处理完rx_ring中的所有数据包。

5.         使用__netif_rx_complete函数将当前设备在poll队列中清除,接着使能接收数据中断。由于接收中断和__netif_rx_complete函数间存在竞争,即将设备从poll队列中清除的同时可能会有新的接收中断到来,应此这两个操作要在屏蔽本地cpu中断的状态下进行。

后记:

写这样的文章,对与我来说是比较有压力的。一方面,在Linux设备驱动开发领域,我算是一个新手;另一方面,光是看了几天代码写点总结未免有些纸上谈兵;更重要的是,我对芯片手册中的部分内容还不是很理解,例如early mode的用途等等,因此本文的错误是在所难免的,希望朋友们不吝赐教。此外,这几天惊闻06级硕士要改回2年半,心里颇有不快,那就借此拙文慰藉一下自己和实验室的同胞吧。借用方师兄的qq签名档:改变能够改变的,接收不能改变的。



你可能感兴趣的:(Linux下Rtl8139too网卡设备驱动程序关键函数剖析)