做为开放源代码 (Open Source) 运动重要组成部分,Linux操作系统吸引了数以万计的程序员共同开发。由于Linux比较完整的继承了各种UNIX版本的稳定和高效,并且克服和改进了传统UNIX中的很多缺点,更因为其开放的开发模式,Linux成为一个具有强大网络服务功能的操作系统。它支持主流的TCP/IP以及IPX/SPX、 NETBEUI等众多网络协议,无论在嵌入式系统,服务器还是桌面操作系统领域,Linux都取得了广泛的应用。网络驱动程序和网络硬件设备实现网络协议栈中的数据链路层和物理层,对上层协议提供支持,是网络协议栈的重要组成部分,对Linux的网络性能起着决定作用。本文主要讨论基于USB总线的 Linux网络驱动程序的设计和实现方法。
2. Linux体系结构
出于稳定性和安全性的考虑,现代处理器往往具有至少两个运行级别。权限较低的级别无法访问所有的寄存器,不能对硬件直接操作。而权限较高的级别能够进行所有的硬件操作和访问任何系统资源。Linux设计充分利用了现代处理器的上述特性,其内核部分运行于高权限级别,应用程序运行于低权限级别。作为宏内核结构的操作系统,Linux将进程管理、内存管理、网络协议栈、设备驱动和文件系统等服务都集成在内核中,而应用程序则通过系统调用与内核通讯,内核结构如图1所示。
图1
图2
Linux网络子系统基本可以分为系统调用接口、BSD套接字、INET套接字、TCP/IP以及数据链路层。其中,BSD套接字由INET套接字层提供支持,而INET套接字管理着基于IP的TCP或UDP协议端,实现IP分组排序以及控制网络子系统效率等功能。各种网络驱动程序位于IP层之下,它们具有访问硬件设备的能力,实现数据链路层的功能。网络子系统的结构如图2所示。
3. Linux网络驱动程序结构
一个完整的驱动程序是一组回调(Callback)函数的集合。内核根据用户或自身的需要来调用驱动程序提供的函数指针,将控制或数据请求交给相应的驱动程序。驱动程序负责了解相应硬件设备的访问和控制方式,将内核的请求翻译成设备可以理解的操作。这样的层次结构使得内核不必了解硬件设备的访问机制和细节,驱动程序也无需明白内核的控制策略,大大提高了驱动程序的兼容性,同时也方便了程序的调试。根据驱动程序类型的不同,内核要求驱动程序提供的回调函数也不同。
Linux下的Ethernet驱动程序需要注册的回调函数分为“必要”和“可选”两类。“必要”的回调函数是指一个Ethernet驱动程序正常工作所需要的回调函数最小集合,而“可选”的回调函数则是在“必要”的基础上提供更丰富的特性和功能。“必要”的回调函数如表1所示。
函数名称 |
功能 |
open |
打开网络接口;将硬件设备配置为正常工作状态。 |
stop |
关闭网络接口;停止硬件设备工作,释放打开该接口时分配的系统资源。 |
tx_timeout |
当上层协议认为某个数据包发送超时的时候调用;该函数应该解决数据包发送超时问题,并保证函数返回以后,数据包能够正常发送。 |
hard_start_xmit |
由上层协议在希望发送数据包时调用;将来自上层协议的数据包转换为硬件设备能够处理的格式,发送至硬件设备。 |
get_stats |
当上层希望了解驱动程序控制的网络设备的统计信息的时候被调用。 |
hard_header |
根据网络子系统先前地址解析的结果,构造数据包的硬件包头。 |
rebuild_header |
在ARP解析完成之后,由上层协议在发送任何新数据包之间调用,重新构造硬件包头。 |
set_config |
改变网络接口的配置,例如中断号和I/O地址等。 |
表1
为了方便Ethernet驱动程序的设计,Linux内核为hard_header、rebuild_header和set_config提供了通用的回调函数。如果对硬件包头或设备配置没有特殊的要求,通用的回调函数就能够满足网络子系统的要求。
4. 基于USB总线的Linux网络驱动程序设计
4.1 USB设备的访问和控制
与PCI、ISA等设备不同,USB、1394等新一代总线没有IO/MEM映射、中断和DMA硬件资源。取而代之的,是抽象出来的硬件资源概念。对于USB设备来说,资源主要包括配置(configuration)、接口(interface)和端点(endpoint)。三者之间的关系如图3所示。[page]
图3
图4
这些资源中,端点对于USB设备有着最重要的意义,实际的数据传输就是通过对端点的读写来实现的。驱动程序通过描述符来获取这些资源。在初始化时,USB驱动程序从设备端点0读取描述符,经过解析后保存这些资源的属性,为传输数据做准备。
由于采用了抽象的硬件资源,Linux下的USB设备管理也采用了与网络子系统类似的栈结构,如图4所示。
USB Core对USB驱动程序屏蔽了不同USB主机控制器之间的差异,使它们对于USB驱动程序来说具有统一的接口。USB驱动程序通过发送URB(USB Request Block)与USB Core交换数据,USB Core解释URB,并将URB中包含的数据请求通过USB主机控制器发送给对应的USB设备。另一方面,USB Core负责检测USB设备的插入和拔出等事件,当这些事件发生时,USB Core通知内核,使内核能够调用驱动程序的相应回调函数来通知驱动程序对这些事件做出响应。
4.2 USB网络设备驱动程序设计
除了必要的回调函数以外,Linux下的每一种驱动程序都必须有初始化函数和卸载函数。初始化函数需要根据相应的硬件设备,向内核注册不同的数据结构,来声明自己对该设备的支持。对于USB设备来说,初始化函数中需要注册struct usb_driver,该数据结构中的关键域分别为:owner,用于内核维护模块使用计数;name,驱动程序名称;probe,设备初始化函数指针;disconnect,设备删除函数指针;id_table,驱动程序支持设备列表。设备列表指明该驱动程序所支持的设备标识,对于USB设备来说,一般是Vendor ID和Product ID。每当一个USB设备插入系统,内核将查找现有的所有USB设备列表,判断应该调用哪个驱动程序所注册的probe函数来完成设备初始化。当USB设备拔出时,相应的disconnect函数也会被调用,来处理驱动程序的卸载。因此,USB网络驱动程序应在probe函数中初始化设备和注册网络接口。在disconnect函数中注销网络接口。
probe函数的主要代码如下:
ether_setup(netdev); //使用内核通用的以太网回调函数设定hard_header等函数
SET_MODULE_OWNER(netdev); //设定模块拥有者,用于维护使用计数
netdev->open = thu_plc_open; //设定open函数
netdev->stop = thu_plc_close; //设定stop函数
netdev->tx_timeout = thu_plc_tx_timeout; //设定超时函数
netdev->hard_start_xmit = thu_plc_start_xmit; //设定发送函数
netdev->get_stats = thu_plc_netdev_stats; //设定状态统计函数
netdev->watchdog_timeo = THU_PLC_TX_TIMEOUT; //设定超时值
netdev->mtu = THU_PLC_MTU; //配置网络接口的MTU
……
if(!thu_plc_config_dev(dev, intf, id)) { //配置USB网络设备
printk("couldn't configure the device\n");
break;
}
……
if(register_netdev(netdev) != 0) { //注册ethernet接口
printk("couldn't register the device\n");
break;
}
……
其中thu_plc_config_dev函数用来检测和配置USB设备。当probe函数成功返回时,驱动程序已经完成了USB设备的检测和网络接口的注册。而网络接口的正式启用还需要用户或应用程序使能该接口。例如用户可以使用ifconfig命令来启用网络接口。当接口被正式启用时,驱动程序的open回调函数被调用,由于USB设备没有类似于硬件中断的异步通知方式,需要主机主动查询是否有数据需要读取,而网络设备则需要有能力来异步通知操作系统数据包的到达,因此,在open函数中需要向USB Core发送一个读请求的URB,使得当USB设备需要将数据包输入主机时,Linux能够及时响应。[page]
open回调函数的主要代码如下:
……
usb_fill_bulk_urb(dev->rx_urb, //构造读请求的URB
dev->udev,
usb_rcvbulkpipe(dev->udev, 6), //指定读端点
dev->rx_skb->data,
512,
read_bulk_callback, //使用read_bulk_callback做为URB的
dev //回调函数。
);
if((result = usb_submit_urb(dev->rx_urb, GFP_KERNEL))){ //将URB发送给
…… //USB Core
}
netif_start_queue(netdev); //使能网络传输队列
……
当读请求URB完成时,意味着主机收到了一个数据包或该URB超时,此时read_bulk_callback将会被内核调用。无论是哪种情况,为了将来可能到来的数据包能够及时得被主机读取,驱动程序都应该再发送一个读请求URB给USB Core。而在主机收到数据包的情况下,read_bulk_callback函数构造一个skb_buff数据结构来描述数据包,并调用 netif_rx函数,把该数据包交给上层协议,从而完成一次接受过程。
与接受过程相比,发送数据包的过程简单了很多。当网络子系统准备发送一个数据包时,上层协议将会构造一个skb_buff数据结构来描述数据包,并且调用网络驱动程序注册的hard_start_xmit回调函数来发送该数据包。由于该函数被调用时内核持有xmit_lock自旋锁,因而驱动程序可以不必考虑对设备写操作的同步问题。hard_start_xmit函数根据数据包的长度将其拆分为USB设备可以传输的长度,然后构造相应的写请求 URB,发送至USB Core即可。
hard_start_xmit回调函数的主要代码如下:
……
usb_fill_bulk_urb(dev->tx_urb, //构造写请求的URB
dev->udev,
usb_sndbulkpipe(dev->udev, 2), //指定写端点
skb->data,
512,//count,
write_bulk_callback, //使用write_bulk_callback做为URB的回调函数。
dev
);
if((result = usb_submit_urb(dev->tx_urb, GFP_ATOMIC))){ //将URB发送给
…… //USB Core
}
写请求URB完成时,write_bulk_callback回调函数被内核调用。该函数判断写请求URB是否成功完成。根据URB的完成情况,驱动程序需要更新网络接口的相应统计数据,例如成功/失败发送包的数目等。
5. 小结
本文从工程应用出发,介绍了Linux的体系结构及其网络子系统,并结合USB设备在Linux下的访问机制,研究了USB驱动程序实现异步通知的方法,并给出了USB网络驱动程序的设计框架和实例。在实际测试中,本文分析的驱动程序运行稳定,并且达到了预期的网络传输速度。
参考文献:
[1] J. Corbet, A. Rubini, and G. Kroah-Hartman. Linux Device Drivers, Third Edition. 2005, O'Reilly Media, Inc.
[2]毛德操 胡希明. Linux内核源代码情景分析. 2001. 浙江大学出版社.
[3]Daniel P. Bovet, M. Cesati. Understanding the Linux Kernel, Second Edition. 2002, O'Reilly Media, Inc.
[4]李少甫 何小庆 江文瑞.The Development of Embedded Wireless LAN Application System Based on MontaVista Linux.微计算机信息. 2002年11期49-51
上一篇:嵌入式WEB服务器中TCP/IP协议栈的设计与实现
下一篇:基于嵌入式系统的彩色液晶显示驱动控制
推荐阅读最新更新时间:2024-03-16 12:59