驱动

来源:互联网 发布:perl 数据库 编辑:程序博客网 时间:2024/04/29 21:48
来源:Linux社区  作者:smilestone322
本篇文章来源于 Linux公社网站(
www.linuxidc.com)  原文链接:http://www.linuxidc.com/Linux/2011-03/33606.htm
很早以前就想写点东西来总结windows 下usb开发和linux 下usb 驱动开发的异同了,今天主要从相同点和不同点进行讲解吧,对于两个平台下usb驱动开发,usb协议是相同的,咱们先从usb协议开始谈起,然后讲解windows下usb驱动开发的方法,主要介绍3种方法,dw 3.2+DDK;WDM下DDK开发和WDF下KMDF Usb驱动开发。
虽然dw已经被WDF赶下台了,但是我这里还是要讲讲dw开发驱动程序的东西,因为我的第一个驱动程序就是采用dw+ddk 2600开发的,至今我都记得当时开发驱动的情景,那种感觉真的希望神仙来指路啊。在开发第一个usb驱动程序之前我没有做过驱动的经验,甚至都不知道,驱动是干嘛的。但是临危受命,只有往前冲,没有回头路,因为回头就意味着枪毙(被辞职,08年的流行语,被)。没有回头路,那就往前走呗。

没有文档,没有原理图,甚至连数据手册都没有给我,就跟我讲了讲这个项目要做什么工作。当时一片茫然,真希望神仙来指路。拿到项目需求,其实是口头上的,呵呵,没有 一行字的文档。没有办法,一边学习,一边做需求分析,请教做过这些事情的同事和同学。到csdn和国外的论坛发帖狂问。一边搜开发驱动的工具,比如ddk,dw等等,那个时候国内还没有人用WDF模型做驱动开发。
那时的水平真的就是一个字来形容,菜。搜了一大通,感觉还是用dw 吧,感觉还是挺比较容易用,选用了开发工具后,就不分昼夜的学习。有时工作到凌晨2点,雷锋同志说的好,时间就像乳沟,要挤还是有的,呵呵,中午也不休息了,干活,干活,干活。一个星期过去了没有一点进展,2个星期过去了,终于把需求搞明白了,这个时候,对dw+ddk+vc的开发环境也搭起来了,也买了几本书,每一本书都讲到关键的地方没了,呵呵,真气人啊。
神阿,救救我吧!老大每天都过来问一下,有什么进展了吗?唉,我每次都对着他笑笑,还好,老大还对我不错,要我好好的静下心来,理一理思路。当时他也帮我找找资料,但是后面证明都是没用的,呵呵,因为公司没有人做过usb驱动。
3个星期过去了,柳岸花明又一村,我终于可以往硬件发送控制命令了,但是硬件还没有给我回送数据。没办法,用bus hound 一抓包,看到有数据了,我兴奋了一吧。呵呵,终于看到了希望,看到了未来,那时候感谢党,感谢毛主席,心情澎湃了一个小时。
实验证明,希望越大,失望越大,我以为就要做出来了,但是没想到还是万里长征走完第一步,后面的路还很长,我们必须保留……。我后面就去写函数去读端点中的数据,但是一直都读不到,呵呵,调啊 调啊。希望会有的,面包会有的,我一直坚信自己,从来不怀疑自己的能力。
第4个星期的早上,我早早的就去上班了,还是坐在那里,安安静静的写代码,调程序,也不和别人说话,不断地看别人的代码,看dw上的例子,看ddk的例子。当时一个星期比我一个月看的东西还要多,总结一句话,静下心来看源码,源码就会像看故事会那样简单。第4个星期的星期3了。也是上午快吃中饭的时候,终于看到硬件不断的给我传数据了,我还是用bus hound抓包,数据是那么的正确,当时的心情是怎样的激动,我无法形容,现在都过去几年了,都清楚地记得,我当时吼了一声,公司的人都看着我,现在回想起来是那么的幼稚。一个驱动程序,一段不寻常的经历,让我想起一句话:没有压力,没有动力,不经历风雨怎能见彩虹。
以上就是我第一次做usb驱动的一段经历,那时流行采用dw+ddk开发驱动程序,现在开发dw软件的公司都停止开发了,多么可惜的一件事情啊,这么优秀的软件,这么易用的软件,就这样被淘汰了,最终将它的版本定格在3.2。但是国内现在还有一大拨人在使用dw开发驱动程序,我看网上有本书,叫圈圈教你玩usb,就是采用dw开发驱动的,粉丝还挺多的,证明dw还有一定的生命力,还有一定的市场,所以本文还是会讲到怎样采用dw做驱动开发。
虽然现在正规的商用软件都不采用dw了,因为它的版本不更新,出了bug都没有人维护了。但是用dw做研究还是挺不错的。开场白说完了。开始进入正题,本文的主要安排如下:首先介绍usb通信协议,同时介绍一款最简单的usb芯片,68013。然后接着讲解windows下的usb驱动开发。采用3种不同的方法开发usb驱动程序,最后讲解linux下usb驱动开发,当然两个平台开发usb驱动的异同点是贯穿全文的,不然就跑题了。
作为驱动开发人员,不需要你有多高的算法基础,因为驱动里面的算法都是基本的算法,只要你熟悉原理图,熟悉datesheet,然后学习DDK,WDK下自带的例子,就可以轻松上手,对于linux,同样也是这样,还有linux的源码是公开的,你可以编译,安装,就可以在/usr/src看到各种驱动的源码了,学习这些源码,不但可以提高我们的编程水平,而且使得我们写的驱动更有健壮性。下面我讲解的思路也是这样的,先将usb协议,在讲讲cy7c68013芯片,然后讲解dw,ddk,wdk下的例子,只要将这些例子都学会,可以说,你具有写驱动的水平了,至于要成为高手,就必须多总结经验,多积累经验。
1 Usb通信协议
Usb驱动程序是PC中最主要的设备驱动程序,usb总线协议比PCI总线协议复杂,但是对于我们usb驱动开发者来说,并需要对usb通信协议每个东东都搞清楚,比如usb物理层协议,usb传输层协议等等,只要了解即可。
1.1)usb总线协议
USB最多支持127个外设,但是考虑到Hub的情况,连接的外设没有这么多,因为Hub也当成了外设,对于每个PC都有一个或多个Host控制器设备,该Host控制器和一个根Hub作为一个整体,跟Hub下可以连接多个Hub,这样形成级连设置,Host分别有2种驱动,一种是1.0,另一种是2.0,分别对应着USB协议1.0和USB协议2.0。虽然存在Hub,但是对于用户来说,这些都是透明的,就好象USB设备和直接连接到USB Host控制器上一样。当USB设备插入和删除都会发电信号给系统,这样可以枚举USB设备。
USB 的连接模式是Host和Devcie的连接模式,所有的请求必须是Host向Device发出,这就使Host端设计相对复杂,而Device端设计相对简单。在USB的通信中,可以看成是一个分层的协议。分为三个层次,即最底层USB总线接口层、USB设备层、功能通信层,如图1-1所示。
 
图1 usb逻辑和控制图
对于每个USB设备,都有一个或者多个的接口(Interface),每个Interface都有多个端点(Endpoints),每个端点通过管道(Pipes)和USB Host控制器连接。每个USB设备都会有一个特殊的端点,即Endpoint0,它负责传输设备的描述信息,同时也负责传输PC与设备之间的控制信息,如图1-2所示:
 
图1-2 USB 管道与端点
现在讲讲usb传输吧,Usb传输一般分为3个部分:令牌传输,数据传输,握手阶段,传输的方式有4种:控制传输,这个是传输控制信息,Bulk传输,这个是批处理传输,中断传输,同步传输。
对于控制传输来说,一般采用端点0进行控制传输,主要用来配置设备、获取设备信息、发送命令或者获取设备的状态报告。总线驱动程序为它保留了10%的带宽,使得控制传输能在1ms内完成。
对于控制端点:
端点所传的数据净负荷的长度必须小于或等于其wMaxPacketSize,当传输的数据大于wMaxPacketSize时,必须分多次进行传送。除最后一次传输外,其它都应达到最大长度。最后一次传输含最后剩下的数据。控制传送的数据阶段结束的标志如下:
·已传了由Setup阶段指定的数据量。
·传了一个数据包,它的长度为0或它的数据区长度小于最大长度。
中断传输:主要用来以一个固定的速度传送少量的数据,对于中断传输,系统会预留带宽。
Bulk传输:用来传送大量的数据,数据传送是可靠的,当总线上的空间不足以发送整个批量包时,会划分成多个包传送。缺点:是当有多个usb设备时,bulk传输,由于系统没有给他预留usb带宽,有可能被其它以中断或同步传输的usb设备抢占带宽,造成bulk传输的设备不能正常使用。
Bulk传输也像控制传输一样,端点所传的数据净负荷的长度必须小于或等于其wMaxPacketSize,当传输的数据大于wMaxPacketSize时,必须分多次进行传送。除最后一次传输外,其它都应达到最大长度。最后一次传输含最后剩下的数据。控制传送的数据阶段结束的标志如下:
·已传了由bulk传输指定的数据量。
·传了一个数据包,它的长度为0或它的数据区长度小于最大长度。
这里要注意了,硬件设计的时候,主要必须能给PC端传送short包,什么意思呢,因为usb bulk传输结束的条件有2个,当数据没有全部传送成功时,我们可以根据short包或zero包来判断这次传输的结束。
同步传输:也叫等时传输,用来传送大量不可靠传输,不保证数据的到达,但能保证恒定的数据流,一般用于摄像机等视频设备的数据采集。
Usb协议中除了上面的讲解的内容,还有usb描述符,usb描述符包括,usb设备描述符,usb配置描述符,usb接口描述符和usb端点描述符。Usb设备描述符长度为18个字节,主要用来枚举设备使用,其中重要的字节是PID/VID,因为它可以用来枚举设备。还有一个字节不得不将,也是面试时,考官容易考试的,SerialNumber字段,各位看官看好了,估计你不知道这个字节的作用了吧。你做usb驱动有过在一个usb口安装usb驱动后,到另外一个口还提示你安装驱动的经验吗,呵呵,这就是SerialNumber了,如何设置,就不说了。各位看官自己研究吧。
Usb配置描述符和usb接口描述符都不重点讲解了,它们的长度都是9个字节,下面在讲讲usb端点描述符,usb端点描符的长度为7个字节,其中重要的字段为端点地址,如果是0x82表示端点是一个端点号为2的IN端点,而0x02表示一个端点号为2的OUT端点。还有bmAttributes的低2位表示端点的类型,如0表示控制端点,而1表示同步端点,2表示bulk端点,3表示中断端点。还有一个就是wMaxpacketsize,不用我说,大家看到这个知道它表示什么意思了,对了,就是表示该端点最大的传输size,如果总的传输大小比这个大,就必须分割成多个块进行传输,每个块的size为wMaxpacketsize,最后剩余的数据放在最后一个块里面输出。剩下的就是bInterval字节的,它只对同步和中断端点有效,对于中断端点,该值得范围为1-255ms,代表2次巡检间的最大时间间隔。对于同步端点,固定为1,表示每隔一个帧周期(1ms)都应该巡检。
 
1.2)68013 芯片资料
Cypress 68013的芯片数据手册在这里不一一列出了,具体的资料大家给到它的官网上下载,下面是56脚CY7C68013A接口图:
 
l     单片集成USB2.0收发器、SIE和增强型8051微处理器。
l     软件:从内部RAM运行的8051程序来自于:
——通过USB接口下载,或
——从EEPROM下载
——外部储存器设备(仅对128脚配置)
l     4个可编程的批量/中断/同步端点
——缓冲器可选:双倍、三倍和四倍。
l     8位或16位外部数据接口。
l     GPIF
——允许直接连接到大部分并行接口;8位或16位。
——通过可编程的波形描述器和配置寄存器来定义波形。
——支持多就绪(RDY)和控制(CTL)输出。
——高达48 MHz的时钟速率。
——每指令周期4个时钟。
——两个UARTS。
——三个定时器/计数器。
——扩展的中断系统。
——两个数据指针。
l     通过枚举支持总线供电应用。
l     3.3V操作电压。
l     灵巧的串行接口引擎。
l     USB中断向量。
l     对控制传输的设置(SETUP)和数据(DATA)部分使用独立的数据缓冲器。
l     集成的I2C兼容控制器,运行速率100或400 kHz。
l     8051的时钟频率为48MHz, 24MHz, 或12MHz。
l     4个集成的FIFO。
——以更低的系统开销组合FIFO。
——自动转换到/自16位总线。
——支持主或从操作。
——FIFO可以使用外部提供的时钟或异步选通。
——容易与ASIC和DSP芯片接口。
l     对FIFO和GPIF接口的特殊自动中断向量。
l     多达40个通用I/O接口。
l     四种封装可选—128脚TQFP, 100脚TQFP, 56脚QFN和56脚SSOP。

2 驱动基础知识
   68013带有自己的驱动的程序的,但是在这里为了讲解usb驱动程序的开发方法,我们还是自己的驱动程序吧,我自己的驱动程序比它自带的驱动程序效果更好,呵呵,传输速度更快,网上有网友说,Ezusb 驱动程序的缺点一大堆,下面讨论驱动程序的编写方法,在本文中,widows环境下我主要想讲解不同的工具的开发方法和技巧。
 
2.1 windows 驱动开发基础
    windows下Pc开发驱动的方法主要有,windriver,driverstudio 3.2+ddk,ddk和WDK这4种,windriver开发驱动最简单,但它开发的驱动不符合微软的wdm模型的思想,在本文中,不对windriver进行讲解,主要讲解driverstudio 3.2(dw 3.2)+ddk,及ddk,和wdk进行驱动开发。
       Driverstudio 简单来说,就像api的MFC,它是对ddk的进一步的封装,采用driverstudio的driverworks能够很方便的生成程序框架。而且生成的程序满足wdm模型,因此它以它的易用性影响了一代人。下面首先讲解采用vc6.0+driverstudio 3.2 +ddk搭建驱动开发环境。
1.装系统XP->安装VC 6.0—>安装DDK->driver studio 3.2
2.如果安装的是DDK2600,则需要安装补丁,ntstrsafe和csq,到网上下载文件ntstrsafe.lib+csq.lib.rar,把解压出来的两个库文件拷贝到WinXP_DDK的安装目录下的库目录中(我的是E:\WINDDK\2600\lib\wxp\i386)。
3.对driver studio,VC6.0和ddk进行简单的设置;
  (1)在VC6.0 菜单的DriverStudio菜单下的DDK Build Settings,在弹出的对话框中选择已经安装的DDK目录(我的是E:\WINDDK\2600);
   (2) VC6.0-->Tools-->Options,点击"Directories"选项卡:)“Show directories for:"下选择Include files,然后检查有没有包含ddk的头文件目录(我的是E:\WINDDK\2600\inc\wxp),如果没有则加上;Show directories for:"下选择Library files,然后检查有没有包含ddk的库文件目录(我的是E:\WINDDK\2600\lib\wxp\i386),如果没有则加上;
   (3)启动driverstudio,在develop下的DDK Building Settings中确保“DDK Root Directory”下方的内容是ddk的安装目录(我的是E:\WINDDK\2600),然后点击下方的"Luanch Program"正式启动vc6的开发环境。
   (4) VC6.0进入菜单File-->Open Workspace(打开位于DriverStudio3.2安装目录的\DriverWorks\Source\vdwlibs.dsw)-->进入菜单Build-->batch Build,点击“Select x86"按钮只选中全部的32位库(对于32位的电脑一定不要选中64位的库,否则后面编译会出错)-->点击按钮"Rebuild AlL”开始编译。
(5)编译一个DriverStudio自带的实例,启动VC6.0,点击菜单File-->Open Workspace,打开项目文件:
C:\Program Files\Compuware\DriverStudio\DriverWorks\Examples\wdm\
hellowdm\HelloWdm.dsw,然后编译,如果没有报错,那说明安装和配置成功。
  (6)应用driverstudio的Driver wizard生成驱动程序框架:此后系统会一步一步引导你完成设置,最后自动生产的驱动程序框架。设置好后将生成驱动文件,然后用VC6.0进行编译:进行Build菜单,Rebuild AlL将生成.sys文件,说明驱动模块编译成功!
 
    讲解完dw3.2的环境搭建后,接着谈谈wdm的驱动到底有哪些内容,本文主要讲解的驱动都是功能驱动,过滤驱动本文不在进行讲解。无论是dw3.2还是ddk或wdk开发驱动都包括以下内容:
1. pnp   2.电源管理,3.内存管理等。
    对于dw3.2+ddk或单独采用ddk开发驱动来说,还有派遣函数等内容,而对于wdk来说,就没有派遣函数了,都用队列来处理。
    本文主要针对usb驱动进行讲解,在dw3.2中,与usb驱动相关的几个类如下:
        1.KUsbLowerDevice :主要用于逻辑设备的编程;
     2.KusbInterface:主要用于接口设备的编程;
     3.KusbPipe:主要用于管道的编程;
 
       对于将dw3.2+ddk怎么开发usb驱动程序,首先采用driverworks的DriverWizard生成usb驱动框架,生成的流程在这里不进行讲解了,网上有10分钟开发一个usb驱动程序等资料。
      
对于DDK和WDK的最大的不同是DDK采用wdm模型,而WDK采用的是WDF模型,WDM中的Irp在WDF中WDFREQUEST对象表示,WDFREQUEST是对Irp的封装,而wdm中的i/o对象在wdk中不再是一个设备对象,wdk用i/o target表示,i/o target 对象比设备对象(device_object)表示的范围更大,可以表示驱动,和队列对象等。
其次DDK和WDK的不同就是在ddk中,用队列用的比较少,而在wdk中无处不见队列,而wdm的对象都是杂乱的并行队列,而wdf中队列分成了3种,串行队列,并行队列,手动队列。
DDK和WDK的另外一个不同就是电源管理和Pnp了,WDK几乎不要用户在写电源管理和Pnp代码了。
呵呵,总结一句,WDK比DDK的代码更加简洁,少了很多代码。这些不同,我会在后面的举例中,都可以看到。其中ddk的usb讲解,我就采用ddk下面的例子,bulkusb进行讲解,而对于wdk usb讲解,我采用的是wdk下的例子,usbsamp。
Window驱动的开发基础先介绍到这里了,对于最基本的DriverEntry,Adddevice等,我还会在后面的例子中进行讲解。另外WDF又分成2种,第一种是KMDF,第二种是UMDF,KMDF是内核的驱动,UMDF是用户层的驱动。
 
2.2 应用程序与驱动之间的通信
当应用程序通过USB管道发送或接收数据时,它首先调用Win32 API(如ReadFile,CreateFile,WriteFile,DeviceIoControl)调用使得功能驱动程序收到一个IRP。而驱动程序的工作是把客户的请求(应用软件)引导到正确端点的管道上。它把请求提交到总线驱动程序,总线驱动程序再把请求分解成多个事物(transaction),然后这些事务被送到总线。
应用程序可以调用ReadFile,CreateFile,WriteFile,DeviceIoControl等API通过IRP来和驱动程序进行通信。Windows中,应用程序实现与WDM通信的过程是:应用程序先用CreateFile函数打开设备,然后用DeviceIoControl和WDM通信,包括从WDM中读数据和写数据给WDM。也可以使用ReadFile从WDM中读数据或用WriteFile写数据给WDM,当应用程序退出时,用CloseHandle关闭设备。对应的IRP如下所示:
 
表1 Win32 API 对应的IRP
Win32函数
 IRP主功能代码(IRP_MJ_xxx)
 KDevice类成员函数
 
CreateFile
 CREATE
 Create
 
ReadFile
 READ
 Read
 
WriteFile
 WRITE
 Write
 
DeviceIoControl
 DEVICE_CONTROL
 DeviceControl
 
CloseHandle
 CLOSE CLENUP
 Close CleanUp
 
 
2.3 驱动程序与底层硬件之间的通信
驱动程序(FDO)和硬件之间的通信主要是在功能驱动层,首先构造USB请求包,然后将USB请求包发送到底层驱动总线上,具体的流程如下:
(1)     当设备插入到PC时,驱动程序调用DriverEntry()例程,主要用于负责驱动程序的初始化,用于初始化驱动程序范围的数据结构和资源。DriverEntry()例程包括主要有以下3个功能。设置AddDevice,Unload,Dispath和其它例程的入口指针;
(2)PC给硬件发送控制命令,即厂商请求。
对于dw3.2来说,发送厂商请求的函数为:BuildVendorRequest,然后构建URB将厂商请求发给硬件;发送Urb的命令为:SubmitUrb函数;
对于ddk来说,发送厂商请求的函数如下:
IoBuildDeviceIoControlRequest,它创建一个I/O控制码的Irp,然后将URB作为Irp的参数,用IoCallDriver发送到底层总线驱动,然后转发到硬件,对硬件设备进行控制;
对于wdk,发送厂商请求分为同步和异步2种方式:
 
(3)对设备进行读写;
dw3.2对设备进行读的函数如下:
要对usb设备进行进行读(read),首先必须发送控制请求,然后在将该请求以urb发给底层usb总线,然后获取硬件返回的数据。
发送请求有以下几种方式:
1)      对于KUsbLowerDevice类来说,发送厂商请求,函数如下:BuildVendorRequest
PURB BuildVendorRequest(
   PUCHAR TransferBuffer,               //为驱动程序存放传输数据的内存
   ULONG TransferBufferLength,   //传输的字节数
   UCHAR RequestTypeReservedBits,  //为类别请求字节的保留位
   UCHAR Request,                          //具体的请求数值
   USHORT Value,                            //为数值
   BOOLEAN bIn=FALSE,                //False表示主机到设备
   BOOLEAN bShortOk=FALSE,       //传输字节数是否可以少于指定的字节数
   PURB Link=NULL                        //指为连接下一个传输的URB
   UCHAR Index=0,                          //为索引值
   USHORT Function=URB_FUNCTION_VENDOR_DEVICE,  //类别请求
   PURB pUrb=NULL                     //NULL表示分配一个新的URB
);
 
然后将URB发送给底层USB总线,提交的函数原型如下:
NTSTATUS SubmitUrb(
   PURB pUrb,              //创建的URB
   PIO_COMPLETION_ROUTINE CompletionRoutine=NULL,  //完成例程
   PVOID CompletionContext=NULL,           //传替给完成例程的参数
   ULONG mSecTimeOut=0                        //同步调用的超时参数
);
 
在dw 3.2下有个usbtherm的例子,里面读取数据的函数,如下:
NTSTATUS UsbThermometer::ReadRamAsynch( UCHAR offset, PIO_COMPLETION_ROUTINE pCompRoutine)
{
     t << "Entering UsbThermometer::ReadRamAsynch\n";
 
     KIrp I = KIrp::Allocate( m_Usb.StackRequirement() );
     if ( I.IsNull() )
         return STATUS_INSUFFICIENT_RESOURCES;
     // allocate a new context structure
     THERMO_READ_COMPLETION_INFO* pCompInfo = new (NonPagedPool) THERMO_READ_COMPLETION_INFO;
     // make sure it succeeded
     if ( pCompInfo == NULL )
     {
         KIrp::Deallocate(I);
         return STATUS_INSUFFICIENT_RESOURCES;
     }
     RtlZeroMemory(pCompInfo,sizeof(THERMO_READ_COMPLETION_INFO));
     // initialize the context structure
     pCompInfo->m_pClass = this;
     pCompInfo->m_OffsetRead = offset;
     // allocate and initialize an URB, and store the pointer in the context structure
     pCompInfo->m_pUrb =
              m_Usb.BuildVendorRequest(
                   pCompInfo->m_buffer,             // transfer buffer
                   8,                                   // transfer buffer size
                   0,                                   // request reserved bits
                  2,                                   // request
                   offset,                              // Value
                   TRUE,                                // In
                   FALSE,                               // Short Ok
                   NULL,                                // link urb
                   0,                                   // index
                   URB_FUNCTION_VENDOR_ENDPOINT     // function
                   );
     if ( pCompInfo->m_pUrb == NULL )
     {
         delete pCompInfo;
         KIrp::Deallocate(I);
         return STATUS_INSUFFICIENT_RESOURCES;
     }
     // submit the URB to USBD
     return m_Usb.SubmitUrb(I, pCompInfo->m_pUrb, pCompRoutine, pCompInfo);
}
 
 
对于KusbPipe类来说,构建urb又多了几个函数,其实都差不多,BuildControlTransfer,BuildInterruptTransfer,BuildBulkTransfer,BuildIsochronousTransfer,分别对应的控制传输,中断传输,块传输,同步传输。这些都在usb驱动开发的dw 3.2驱动开发中以例子的形式进行讲解,同时对于usb写,其实是一样的,都是这些函数,只是传输方向变了,改变一个参数就行。
 
ddk对设备进行读时的函数如下:UsbBuildInterruptOrBulkTransferRequest,然后调用IoCallDriver将URB请求转发给底层总线驱动,在转发给硬件设备。
 
wdk对设备进行读写时的函数如下(分为同步和异步2种方式):
 
1)对于usb设备:
同步控制命令(控制传输):
WdfUsbTargetDeviceSendControlTransferSynchronously
函数功能:builds a USB control transfer request and sends it synchronously to an I/O target.
NTSTATUS WdfUsbTargetDeviceSendControlTransferSynchronously(
  [in]           WDFUSBDEVICE UsbDevice,
  [in, optional]   WDFREQUEST Request,
  [in, optional]   PWDF_REQUEST_SEND_OPTIONS RequestOptions,
  [in]           PWDF_USB_CONTROL_SETUP_PACKET SetupPacket,
  [in, optional]   PWDF_MEMORY_DESCRIPTOR MemoryDescriptor,
  [out, optional]  PULONG BytesTransferred
);
 
 
举例如下:
 
WDF_USB_CONTROL_SETUP_PACKET  controlSetupPacket;
WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR(
                                         &controlSetupPacket,
                                         BmRequestHostToDevice,
                                         BmRequestToDevice,
                                         USBFX2LK_REENUMERATE,
                                         0,
                                         0
                                         );
 
status = WdfUsbTargetDeviceSendControlTransferSynchronously(
                                         UsbDevice,
                                         WDF_NO_HANDLE,
                                         NULL,
                                         &controlSetupPacket,
                                         NULL,
                                         NULL
                                         );
 
异步操作(控制传输):
WdfUsbTargetDeviceFormatRequestForControlTransfer
NTSTATUS
    WdfUsbTargetDeviceFormatRequestForControlTransfer(
    IN WDFUSBDEVICE  UsbDevice,
    IN WDFREQUEST  Request,
    IN PWDF_USB_CONTROL_SETUP_PACKET  SetupPacket,
    IN OPTIONAL WDFMEMORY  TransferMemory,
    IN OPTIONAL PWDFMEMORY_OFFSET  TransferOffset
    );
该函数和同步控制传输函数的区别是只构建控制传输请求,但是不发送控制传输请求,而同步操作WdfUsbTargetDeviceSendControlTransferSynchronously构建完控制传输请求的同时,也发送给I/O target。
 
2)对于usb管道:
同步写:
WdfUsbTargetPipeWriteSynchronously函数,函数原型如下:
The WdfUsbTargetPipeWriteSynchronously method builds a write request and sends it synchronously to a specified USB output pipe.
NTSTATUS
  WdfUsbTargetPipeWriteSynchronously(
    IN WDFUSBPIPE  Pipe,
    IN OPTIONAL WDFREQUEST  Request,
    IN OPTIONAL PWDF_REQUEST_SEND_OPTIONS  RequestOptions,
    IN OPTIONAL PWDF_MEMORY_DESCRIPTOR  MemoryDescriptor,
    OUT OPTIONAL PULONG  BytesWritten
    );
异步写:
usb 写数据发送请求的函数为WdfUsbTargetPipeFormatRequestForWrite,然后将WdfRequestSend将请求发送出去。
NTSTATUS WdfUsbTargetPipeFormatRequestForWrite(
    IN WDFUSBPIPE  Pipe,
    IN WDFREQUEST  Request,
    IN OPTIONAL WDFMEMORY  WriteMemory,
    IN OPTIONAL PWDFMEMORY_OFFSET  WriteOffset
    );
 
该函数和同步写函数WdfUsbTargetPipeWriteSynchronously的区别是异步写函数需要调用WdfRequestSend发送请求。WdfRequestSend函数原型如下:
BOOLEAN
  WdfRequestSend(
    IN WDFREQUEST  Request,
    IN WDFIOTARGET  Target,
    IN OPTIONAL PWDF_REQUEST_SEND_OPTIONS  RequestOptions
    );
 
同步读:
WdfUsbTargetPipeReadSynchronously函数,函数原型如下:
NTSTATUS WdfUsbTargetPipeReadSynchronously(
    IN WDFUSBPIPE  Pipe,
    IN OPTIONAL WDFREQUEST  Request,
    IN OPTIONAL PWDF_REQUEST_SEND_OPTIONS  RequestOptions,
    IN OPTIONAL PWDF_MEMORY_DESCRIPTOR  MemoryDescriptor,
    OUT OPTIONAL PULONG  BytesRead
    );
 
异步读:
WdfUsbTargetPipeFormatRequestForRead函数原型如下:
NTSTATUS
  WdfUsbTargetPipeFormatRequestForRead(
    IN WDFUSBPIPE  Pipe,
    IN WDFREQUEST  Request,
    IN OPTIONAL WDFMEMORY  ReadMemory,
    IN OPTIONAL PWDFMEMORY_OFFSET  ReadOffset
    );
 
同步读和异步读的差别和同步写与异步写的区别类似。
待续。。。。。。

本篇文章来源于 Linux公社网站(www.linuxidc.com)  原文链接:http://www.linuxidc.com/Linux/2011-03/33606p5.htm

Linux字符设备驱动程序工作机理分析

1 本文主题
    本文主要分析Linux字符设备驱动程序的工作机理。主要内容以及代码片段来源于《LDD3》,俺只是从另外一个角度来讲述。
    见过很多关于驱动程序的书,基本上都是告诉你怎么做,然后你STEP BY STEP,然后运行完后结果就出来了,可是其背后到底是如何工作的呢?虽说《LDD3》也讲了很多原理性的东西,但是我觉得这个问题其描述得并不明确。
2 关于scull
    scull是《LDD3》的一个字符设备驱动程序,其加载之后会在文件系统下生成/dev/scull文件,在shell下可以对其进行一系列的操作,例如可以使用cp、dd或者输入输出重定向等命令来访问这个文件,也就是访问这个字符设备。
3 /dev/scull是如何生成的
    该文件是在驱动程序模块加载时生成的,具体实现是在scull_load(可以到
www.oreilly.com下载,或者《LDD3_中文》的P51)这个脚本里。
    在shell下insmod了scull.ko之后,系统会在/proc/devices文件里生成设备名以及与之对应的主设备号信息,scull_load脚本根据该信息mknod了/dev/scull文件节点。
    注意下mknod时给的参数信息,其中注明了主设备号以及次设备号信息。
4 shell下操作/dev/scull是如何关联到我们自己编写的scull模块的
    这个问题一直困扰了我很久,不过现在我基本上算是弄明白了。
4.1 先回顾下scull模块程序的初始化操作里做了什么事情。
    
    搞驱动的都知道,初始化的时候需要进行设备注册,核心代码如下:
    static void scull_setup_cdev(struct scull_dev *dev, int index)
    {
        int err, devno = MKDEV(scull_major, scull_minor + index);
        cdev_init(&dev->cdev, &scull_fops);
        dev->cdev.owner = THIS_MODULE;
        dev->cdev.ops = &scull_fops;
        err = cdev_add(&dev->cdev, devno, 1);
        ...
    }
    
    scull_fops是一个数组,其中保存了我们的scull实际的操作:
    struct file_operation scull_fops = {
        ...
        .open = scull_open,
        .read = scull_read,
        .write = scull_write,
        .release = scull_release,
        ...
    }
    
    cdev是struct cdev类型(该类型由系统定义)的变量,其嵌入在struct scull_dev(该类型由scull模块定义)中:
    struct scull_dev{
        ...
        struct cdev cdev;
        ...
    }
    
    从这句代码
    cdev_add(&dev->cdev, devno, 1);
    我们可以发现,设备号devno已经和内核内部结构cdev关联起来了。
4.2 /dev/scull设备的打开操作
    /dev/scull是文件系统中的一个设备文件,其在内核中由sturct inode结构表示。
    
    一般说来,inode结构下由两个元素和驱动程序有关:
    dev_t i_rdev;                 这个是设备号
    struct cdev *i_cdev;          这里是一个指针,struct cdev是字符设备在内核里的表示,注意我们在4.1小节里也提到了这个结构。
    
    也就是说,系统在打开/dev/scull之前仅知道inode信息。在4.1小节里已经说过了,设备号和内核内部结构cdev关联起来了,因此cdev下的ops也和设备号关联起来了,ops已经初始化为scull_fops。所以我们才能够通过inode中的设备号i_rdev定位到此时打开的是哪一个设备,从而去调用相应设备的open操作。
    看看open函数的原型吧:
    int (*open)(struct inode*, struct file*);
    第一个参数是struct inode类型的指针,该指针由内核传递过来,是设备文件在内核中的表示。
    第二个参数struct file类型的指针,open之后,设备文件在内核中就以struct file结构来表示了。
    看看open操作在scull里的实现吧:
    int scull_open(struct inode *inode, struct file *filp)
    {
        struct scull_dev *dev;
        dev = container_of(inode->i_cdev, struct scull_dev, cdev);
        filp->private_data = dev;
        ...
    }
    第一句代码通过宏container_of把包裹inode->i_cdev的结构提取出来了,该结构就是在scull模块里定以的struct scull_dev,其中包含了scull模块所需要的私有信息。
    第二句代码把该私有信息加入内核数据结构struct file,之前说过了,open后的文件在内核里以struct file结构表示。
4.3 /dev/scull的读写操作
    先看下读写操作的函数原型:
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file*, char __user *, size_t, loff_t *);
    其参数内容我不做过多解释,注意其第一个参数是struct file类型的指针,该指针由内核传入,也就是open之后的那个文件指针。
    通过这个指针,能够找到scull模块的私有数据结构struct scull_dev,并对其操作,我们可以从read/write操作在scull里的实现看到这个做法:
    ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
    {
        sturct scull_dev *dev = filp->private_data;
        int quantum = dev->quantum, qset = dev->qset;
        ...
    }
    
    ssize_t scull_write(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
    {
        sturct scull_dev *dev = filp->private_data;
        int quantum = dev->quantum, qset = dev->qset;
        ...
    }
5 联系我
    作者:  原野之狼
    Email:  y_y_z_l AT 163.com  (AT换成@,无空格)
6 版权声明
    欢迎转载,但是务必保留本文的完整性。

------------------ Linux操作系统网络驱动程序编写 ------------------- 
------------ Contact the author by mailto:bordi@bordi.dhs.org ------ 
Linux操作系统网络驱动程序编写 
一.Linux系统设备驱动程序概述 
1.1 Linux设备驱动程序分类 
1.2 编写驱动程序的一些基本概念 
二.Linux系统网络设备驱动程序 
2.1 网络驱动程序的结构 
2.2 网络驱动程序的基本方法 
2.3 网络驱动程序中用到的数据结构 
2.4 常用的系统支持 
三.编写Linux网络驱动程序中可能遇到的问题 
3.1 中断共享 
3.2 硬件发送忙时的处理 
3.3 流量控制(flow control) 
3.4 调试 
四.进一步的阅读 
五.杂项 
一.Linux系统设备驱动程序概述 
1.1 Linux设备驱动程序分类 
Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日 
益增加,主要是驱动程序的增加。在Linux内核的不断升级过程中,驱动程序的结构 
还是相对稳定。在2.0.xx到2.2.xx的变动里,驱动程序的编写做了一些改变,但是 
从2.0.xx的驱动到2.2.xx的移植只需做少量的工作。 
Linux系统的设备分为字符设备(char device),块设备(block device)和网络 
设备(network device)三种。字符设备是指存取时没有缓存的设备。块设备的读写 
都有缓存来支持,并且块设备必须能够随机存取(random access),字符设备则没有 
这个要求。典型的字符设备包括鼠标,键盘,串行口等。块设备主要包括硬盘软盘 
设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。 
网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket 

机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系 
统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。 
1.2 编写驱动程序的一些基本概念 
无论是什么操作系统的驱动程序,都有一些通用的概念。操作系统提供给驱动 
程序的支持也大致相同。下面简单介绍一下网络设备驱动程序的一些基本要求。 
1.2.1 发送和接收 
这是一个网络设备最基本的功能。一块网卡所做的无非就是收发工作。所以驱 
动程序里要告诉系统你的发送函数在哪里,系统在有数据要发送时就会调用你的发 
送程序。还有驱动程序由于是直接操纵硬件的,所以网络硬件有数据收到最先能得 
到这个数据的也就是驱动程序,它负责把这些原始数据进行必要的处理然后送给系 
统。这里,操作系统必须要提供两个机制,一个是找到驱动程序的发送函数,一个 
是驱动程序把收到的数据送给系统。 
1.2.2 中断 
中断在现代计算机结构中有重要的地位。操作系统必须提供驱动程序响应中断 
的能力。一般是把一个中断处理程序注册到系统中去。操作系统在硬件中断发生后 
调用驱动程序的处理程序。Linux支持中断的共享,即多个设备共享一个中断。 
1.2.3 时钟 
在实现驱动程序时,很多地方会用到时钟。如某些协议里的超时处理,没有中 
断机制的硬件的轮询等。操作系统应为驱动程序提供定时机制。一般是在预定的时 
间过了以后回调注册的时钟函数。在网络驱动程序中,如果硬件没有中断功能,定 
时器可以提供轮询(poll)方式对硬件进行存取。或者是实现某些协议时需要的超时 
重传等。 
二.Linux系统网络设备驱动程序 
2.1 网络驱动程序的结构 
所有的Linux网络驱动程序遵循通用的接口设计时采用的是面向对象的方法。 
一个设备就是一个对象(device 结构),它内部有自己的数据和方法。每一个设备的 
方法被调用时的第一个参数都是这个设备对象本身。这样这个方法就可以存取自身 
的数据(类似面向对象程序设计时的this引用)。 
一个网络设备最基本的方法有初始化、发送和接收。 
------------------- --------------------- 
|deliver packets | |receive packets queue| 
|(dev_queue_xmit()) | |them(netif_rx()) | 
------------------- --------------------- 
| | / 
/ | | 
------------------------------------------------------- 
| methods and variables(initialize,open,close,hard_xmit,| 
| interrupt handler,config,resources,status...) | 
------------------------------------------------------- 
| | / 
/ | | 
----------------- ---------------------- 
|send to hardware | |receivce from hardware| 
----------------- ---------------------- 
| | / 
/ | | 
----------------------------------------------------- 
| hardware media | 
----------------------------------------------------- 
初始化程序完成硬件的初始化、device中变量的初始化和系统资源的申请。发送 
程序是在驱动程序的上层协议层有数据要发送时自动调用的。一般驱动程序中不对发 
送数据进行缓存,而是直接使用硬件的发送功能把数据发送出去。接收数据一般是通 
过硬件中断来通知的。在中断处理程序里,把硬件帧信息填入一个skbuff结构中,然 
------------------ Linux操作系统网络驱动程序编写 ------------------- 
------------ Contact the author by mailto:bordi@bordi.dhs.org ------ 
后调用netif_rx()传递给上层处理。 
2.2 网络驱动程序的基本方法 
网络设备做为一个对象,提供一些方法供系统访问。正是这些有统一接口的方法, 

掩蔽了硬件的具体细节,让系统对各种网络设备的访问都采用统一的形式,做到硬件 
无关性。 
下面解释最基本的方法。 
2.2.1 初始化(initialize) 
驱动程序必须有一个初始化方法。在把驱动程序载入系统的时候会调用这个初 
始化程序。它做以下几方面的工作。检测设备。在初始化程序里你可以根据硬件的 
特征检查硬件是否存在,然后决定是否启动这个驱动程序。配置和初始化硬件。在 
初始化程序里你可以完成对硬件资源的配置,比如即插即用的硬件就可以在这个时 
候进行配置(Linux内核对PnP功能没有很好的支持,可以在驱动程序里完成这个功 
能)。配置或协商好硬件占用的资源以后,就可以向系统申请这些资源。有些资源是 
可以和别的设备共享的,如中断。有些是不能共享的,如IO、DMA。接下来你要初始 
化device结构中的变量。最后,你可以让硬件正式开始工作。 
2.2.2 打开(open) 
open这个方法在网络设备驱动程序里是网络设备被激活的时候被调用(即设备状 
态由down-->up)。所以实际上很多在initialize中的工作可以放到这里来做。比如资 
源的申请,硬件的激活。如果dev->open返回非0(error),则硬件的状态还是down。 
open方法另一个作用是如果驱动程序做为一个模块被装入,则要防止模块卸载时 
设备处于打开状态。在open方法里要调用MOD_INC_USE_COUNT宏。 
2.2.3 关闭(stop) 
close方法做和open相反的工作。可以释放某些资源以减少系统负担。close是在 
设备状态由up转为down时被调用的。另外如果是做为模块装入的驱动程序,close里 
应该调用MOD_DEC_USE_COUNT,减少设备被引用的次数,以使驱动程序可以被卸载。 
另外close方法必须返回成功(0==success)。 
2.2.4 发送(hard_start_xmit) 
所有的网络设备驱动程序都必须有这个发送方法。在系统调用驱动程序的xmit 
时,发送的数据放在一个sk_buff结构中。一般的驱动程序把数据传给硬件发出去。 
也有一些特殊的设备比如loopback把数据组成一个接收数据再回送给系统,或者 
dummy设备直接丢弃数据。 
如果发送成功,hard_start_xmit方法里释放sk_buff,返回0(发送成功)。如果 
设备暂时无法处理,比如硬件忙,则返回1。这时如果dev->tbusy置为非0,则系统 
认为硬件忙,要等到dev->tbusy置0以后才会再次发送。tbusy的置0任务一般由中断 
完成。硬件在发送结束后产生中断,这时可以把tbusy置0,然后用mark_bh()调用通 
知系统可以再次发送。在发送不成功的情况下,也可以不置dev->tbusy为非0,这样 
系统会不断尝试重发。如果hard_start_xmit发送不成功,则不要释放sk_buff。 
传送下来的sk_buff中的数据已经包含硬件需要的帧头。所以在发送方法里不需 
要再填充硬件帧头,数据可以直接提交给硬件发送。sk_buff是被锁住的(locked), 
确保其他程序不会存取它。 
2.2.5 接收(reception) 
驱动程序并不存在一个接收方法。有数据收到应该是驱动程序来通知系统的。 
一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块 
sk_buff(skb),从硬件读出数据放置到申请好的缓冲区里。接下来填充sk_buff中 
的一些信息。skb->dev = dev,判断收到帧的协议类型,填入skb->protocol(多协 
议的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要 
设置skb->pkt_type,标明第二层(链路层)数据类型。可以是以下类型: 
PACKET_BROADCAST : 链路层广播 
PACKET_MULTICAST : 链路层组播 
PACKET_SELF : 发给自己的帧 
PACKET_OTHERHOST : 发给别人的帧(监听模式时会有这种帧) 
最后调用netif_rx()把数据传送给协议层。netif_rx()里数据放入处理队列然后返 
回,真正的处理是在中断返回以后,这样可以减少中断时间。调用netif_rx()以后, 
驱动程序就不能再存取数据缓冲区skb。 
2.2.6 硬件帧头(hard_header) 
硬件一般都会在上层数据发送之前加上自己的硬件帧头,比如以太网(Ethernet) 
就有14字节的帧头。这个帧头是加在上层ip、ipx等数据包的前面的。驱动程序提供 
一个hard_header方法,协议层(ip、ipx、arp等)在发送数据之前会调用这段程序。 
硬件帧头的长度必须填在dev->hard_header_len,这样协议层回在数据之前保留好 
硬件帧头的空间。这样hard_header程序只要调用skb_push然后正确填入硬件帧头就 
可以了。 
在协议层调用hard_header时,传送的参数包括(2.0.xx):数据的sk_buff, 
device指针,protocol,目的地址(daddr),源地址(saddr),数据长度(len)。数据 
长度不要使用sk_buff中的参数,因为调用hard_header时数据可能还没完全组织好。 
saddr是NULL的话是使用缺省地址(default)。daddr是NULL表明协议层不知道硬件目 
的地址。如果hard_header完全填好了硬件帧头,则返回添加的字节数。如果硬件帧 
头中的信息还不完全(比如daddr为NULL,但是帧头中需要目的硬件地址。典型的情 
况是以太网需要地址解析(arp)),则返回负字节数。hard_header返回负数的情况 
下,协议层会做进一步的build header的工作。目前Linux系统里就是做arp 
(如果hard_header返回正,dev->arp=1,表明不需要做arp,返回负,dev->arp=0, 
做arp)。 
对hard_header的调用在每个协议层的处理程序里。如ip_output。 
2.2.7 地址解析(xarp) 
有些网络有硬件地址(比如Ethernet),并且在发送硬件帧时需要知道目的硬件 
地址。这样就需要上层协议地址(ip、ipx)和硬件地址的对应。这个对应是通过地址 
解析完成的。需要做arp的的设备在发送之前会调用驱动程序的rebuild_header方 
法。调用的主要参数包括指向硬件帧头的指针,协议层地址。如果驱动程序能够解 
析硬件地址,就返回1,如果不能,返回0。 
对rebuild_header的调用在net/core/dev.c的do_dev_queue_xmit()里。 
2.2.8 参数设置和统计数据 
在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般 
只有超级用户(root)权限才能对设备参数进行设置。设置方法有: 
dev->set_mac_address() 
当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般 
对mac地址的设置没有太大意义的。 
dev->set_config() 
------------------ Linux操作系统网络驱动程序编写 ------------------- 
------------ Contact the author by mailto:bordi@bordi.dhs.org ------ 
当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config 
方法。用户会传递一个ifmap结构包含需要的I/O、中断等参数。 
dev->do_ioctl() 
如果用户调用ioctl时类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间,系统 
会调用驱动程序的这个方法。一般是设置设备的专用数据。 
读取信息也是通过ioctl调用进行。除次之外驱动程序还可以提供一个 
dev->get_stats方法,返回一个enet_statistics结构,包含发送接收的统计信息。 
ioctl的处理在net/core/dev.c的dev_ioctl()和dev_ifsioc()里。 
2.3 网络驱动程序中用到的数据结构 
最重要的是网络设备的数据结构。定义在include/linux/netdevice.h里。它 
的注释已经足够详尽。 
struct device 

/* 
* This is the first field of the "visible" part of this structure 
* (i.e. as seen by users in the "Space.c" file). It is the name 
* the interface. 
*/ 
char *name; 
/* I/O specific fields - FIXME: Merge these and struct ifmap into one */ 
unsigned long rmem_end; /* shmem "recv" end */ 
unsigned long rmem_start; /* shmem "recv" start */ 
unsigned long mem_end; /* shared mem end */ 
unsigned long mem_start; /* shared mem start */ 
unsigned long base_addr; /* device I/O address */ 
unsigned char irq; /* device IRQ number */ 
/* Low-level status flags. */ 
volatile unsigned char start, /* start an operation */ 
interrupt; /* interrupt arrived */ 
/* 在处理中断时interrupt设为1,处理完清0。 */ 
unsigned long tbusy; /* transmitter busy must be 
long 
for 
bitops */ 
struct device *next; 
/* The device initialization function. Called only once. */ 
/* 指向驱动程序的初始化方法。 */ 
int (*init)(struct device *dev); 
/* Some hardware also needs these fields, but they are not part of the 
usual set specified in Space.c. */ 
/* 一些硬件可以在一块板上支持多个接口,可能用到if_port。 */ 
unsigned char if_port; /* Selectable AUI, TP,..*/ 
unsigned char dma; /* DMA channel */ 
struct enet_statistics* (*get_stats)(struct device *dev); 
/* 
* This marks the end of the "visible" part of the structure. All 
* fields hereafter are internal to the system, and may change at 
* will (read: may be cleaned up at will). 
*/ 
/* These may be needed for future network-power-down code. */ 
/* trans_start记录最后一次成功发送的时间。可以用来确定硬件是否工作正常。*/ 

unsigned long trans_start; /* Time (in jiffies) of last Tx */ 
unsigned long last_rx; /* Time of last Rx */ 
/* flags里面有很多内容,定义在include/linux/if.h里。*/ 
unsigned short flags; /* interface flags (a la BSD) */ 
unsigned short family; /* address family ID (AF_INET) */ 
unsigned short metric; /* routing metric (not used) */ 
unsigned short mtu; /* interface MTU value */ 
/* type标明物理硬件的类型。主要说明硬件是否需要arp。定义在 
include/linux/if_arp.h里。 */ 
unsigned short type; /* interface hardware type */ 
/* 上层协议层根据hard_header_len在发送数据缓冲区前面预留硬件帧头空间。*/ 
unsigned short hard_header_len; /* hardware hdr length */ 
/* priv指向驱动程序自己定义的一些参数。*/ 
void *priv; /* pointer to private data */ 
/* Interface address info. */ 
unsigned char broadcast[MAX_ADDR_LEN]; /* hw bcast add */ 
unsigned char pad; /* make dev_addr ali 
gned 
to 8 
bytes */ 
unsigned char dev_addr[MAX_ADDR_LEN]; /* hw address */ 
unsigned char addr_len; /* hardware address length */ 
unsigned long pa_addr; /* protocol address */ 
unsigned long pa_brdaddr; /* protocol broadcast addr */ 
unsigned long pa_dstaddr; /* protocol P-P other side addr */ 
unsigned long pa_mask; /* protocol netmask */ 
unsigned short pa_alen; /* protocol address length */ 
struct dev_mc_list *mc_list; /* Multicast mac addresses */ 
int mc_count; /* Number of installed mcasts */ 
struct ip_mc_list *ip_mc_list; /* IP multicast filter chain */ 
__u32 tx_queue_len; /* Max frames per queue allowed */ 
------------------ Linux操作系统网络驱动程序编写 ------------------- 
------------ Contact the author by mailto:bordi@bordi.dhs.org ------ 
/* For load balancing driver pair support */ 
unsigned long pkt_queue; /* Packets queued */ 
struct device *slave; /* Slave device */ 
struct net_alias_info *alias_info; /* main dev alias info */ 
struct net_alias *my_alias; /* alias devs */ 
/* Pointer to the interface buffers. */ 
struct sk_buff_head buffs[DEV_NUMBUFFS]; 
/* Pointers to interface service routines. */ 
int (*open)(struct device *dev); 
int (*stop)(struct device *dev); 
int (*hard_start_xmit) (struct sk_buff *skb, 
struct device *dev); 
int (*hard_header) (struct sk_buff *skb, 
struct device *dev, 
unsigned short type, 
void *daddr, 
void *saddr, 
unsigned len); 
int (*rebuild_header)(void *eth, struct device *dev, 
unsigned long raddr, struct sk_buff *skb); 
#define HAVE_MULTICAST 
void (*set_multicast_list)(struct device *dev); 
#define HAVE_SET_MAC_ADDR 
int (*set_mac_address)(struct device *dev, void *addr) 

#define HAVE_PRIVATE_IOCTL 
int (*do_ioctl)(struct device *dev, struct ifreq *ifr, 
int 
cmd); 
#define HAVE_SET_CONFIG 
int (*set_config)(struct device *dev, struct ifmap *ma 
p); 
#define HAVE_HEADER_CACHE 
void (*header_cache_bind)(struct hh_cache **hhp, struct 
dev 
ce 
*dev, unsigned short htype, __u32 daddr); 
void (*header_cache_update)(struct hh_cache *hh, struct 
dev 
ce 
*dev, unsigned char * haddr); 
#define HAVE_CHANGE_MTU 
int (*change_mtu)(struct device *dev, int new_mtu); 
struct iw_statistics* (*get_wireless_stats)(struct device *dev); 
}; 
2.4 常用的系统支持 
2.4.1 内存申请和释放 
include/linux/kernel.h里声明了kmalloc()和kfree()。用于在内核模式下申 
请和释放内存。 
void *kmalloc(unsigned int len,int priority); 
void kfree(void *__ptr); 
与用户模式下的malloc()不同,kmalloc()申请空间有大小限制。长度是2的整 
次方。可以申请的最大长度也有限制。另外kmalloc()有priority参数,通常使用 
时可以为GFP_KERNEL,如果在中断里调用用GFP_ATOMIC参数,因为使用GFP_KERNEL 
则调用者可能进入sleep状态,在处理中断时是不允许的。 
kfree()释放的内存必须是kmalloc()申请的。如果知道内存的大小,也可以用 
kfree_s()释放。 
2.4.2 request_irq()、free_irq() 
这是驱动程序申请中断和释放中断的调用。在include/linux/sched.h里声明。 
request_irq()调用的定义: 
int request_irq(unsigned int irq, 
void (*handler)(int irq, void *dev_id, struct pt_regs *regs 
), 
unsigned long irqflags, 
const char * devname, 
void *dev_id); 
irq是要申请的硬件中断号。在Intel平台,范围0--15。handler是向系统登记 
的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参 
数包括硬件中断号,device id,寄存器值。dev_id就是下面的request_irq时传递 
给系统的参数dev_id。irqflags是中断处理的一些属性。比较重要的有SA_INTERRUPT, 

标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置 
SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有 
一个SA_SHIRQ属性,设置了以后运行多个设备共享中断。dev_id在中断共享时会用 
到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id 
找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对应的设备。 
void free_irq(unsigned int irq,void *dev_id); 
2.4.3 时钟 
时钟的处理类似中断,也是登记一个时间处理函数,在预定的时间过后,系统 
会调用这个函数。在include/linux/timer.h里声明。 
struct timer_list { 
struct timer_list *next; 
struct timer_list *prev; 
unsigned long expires; 
unsigned long data; 
void (*function)(unsigned long); 
}; 
void add_timer(struct timer_list * timer); 
int del_timer(struct timer_list * timer); 
void init_timer(struct timer_list * timer); 
使用时钟,先声明一个timer_list结构,调用init_timer对它进行初始化。 
time_list结构里expires是标明这个时钟的周期,单位采用jiffies的单位。 
jiffies是Linux一个全局变量,代表时间。它的单位随硬件平台的不同而不同。 
系统里定义了一个常数HZ,代表每秒种最小时间间隔的数目。这样jiffies的单位 
就是1/HZ。Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最小时间 
间隔了。所以expires/HZ就是以秒为单位的这个时钟的周期。 
function就是时间到了以后的回调函数,它的参数就是timer_list中的data。 
data这个参数在初始化时钟的时候赋值,一般赋给它设备的device结构指针。 
在预置时间到系统调用function,同时系统把这个time_list从定时队列里清 
除。所以如果需要一直使用定时函数,要在function里再次调用add_timer()把这 
------------------ Linux操作系统网络驱动程序编写 ------------------- 
------------ Contact the author by mailto:bordi@bordi.dhs.org ------ 
个timer_list加进定时队列。 
2.4.4 I/O 
I/O端口的存取使用: 
inline unsigned int inb(unsigned short port); 
inline unsigned int inb_p(unsigned short port); 
inline void outb(char value, unsigned short port); 
inline void outb_p(char value, unsigned short port); 
在include/adm/io.h里定义。 
inb_p()、outb_p()与inb()、outb_p()的不同在于前者在存取I/O时有等待 
(pause)一适应慢速的I/O设备。 
为了防止存取I/O时发生冲突,Linux提供对端口使用情况的控制。在使用端口 
之前,可以检查需要的I/O是否正在被使用,如果没有,则把端口标记为正在使用, 
使用完后再释放。系统提供以下几个函数做这些工作。 
int check_region(unsigned int from, unsigned int extent); 
void request_region(unsigned int from, unsigned int extent,const char *n 
ame) 
void release_region(unsigned int from, unsigned int extent); 
其中的参数from表示用到的I/O端口的起始地址,extent标明从from开始的端 
口数目。name为设备名称。 
2.4.5 中断打开关闭 
系统提供给驱动程序开放和关闭响应中断的能力。是在include/asm/system.h 
中的两个定义。 
#define cli() __asm__ __volatile__ ("cli": 
#define sti() __asm__ __volatile__ ("sti": 
2.4.6 打印信息 
类似普通程序里的printf(),驱动程序要输出信息使用printk()。在include 
/linux/kernel.h里声明。 
int printk(const char* fmt, ...); 
其中fmt是格式化字符串。...是参数。都是和printf()格式一样的。 
2.4.7 注册驱动程序 
如果使用模块(module)方式加载驱动程序,需要在模块初始化时把设备注册 
到系统设备表里去。不再使用时,把设备从系统中卸除。定义在drivers/net/net_init 
.h 
里的两个函数完成这个工作。 
int register_netdev(struct device *dev); 
void unregister_netdev(struct device *dev); 
dev就是要注册进系统的设备结构指针。在register_netdev()时,dev结构一 
般填写前面11项,即到init,后面的暂时可以不用初始化。最重要的是name指针和 
init方法。name指针空(NULL)或者内容为''或者name[0]为空格(space),则系统 
把你的设备做为以太网设备处理。以太网设备有统一的命名格式,ethX。对以太网 
这么特别对待大概和Linux的历史有关。 
init方法一定要提供,register_netdev()会调用这个方法让你对硬件检测和 
设置。 
register_netdev()返回0表示成功,非0不成功。 
2.4.8 sk_buff 
Linux网络各层之间的数据传送都是通过sk_buff。sk_buff提供一套管理缓冲 
区的方法,是Linux系统网络高效运行的关键。每个sk_buff包括一些控制方法和一 
块数据缓冲区。控制方法按功能分为两种类型。一种是控制整个buffer链的方法, 
另一种是控制数据缓冲区的方法。sk_buff组织成双向链表的形式,根据网络应用 
的特点,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff的控制 
方法都很短小以尽量减少系统负荷。(translated from article written by Alan 
Cox) 
常用的方法包括: 
.alloc_skb() 申请一个sk_buff并对它初始化。返回就是申请到的sk_buff。 
.dev_alloc_skb()类似alloc_skb,在申请好缓冲区后,保留16字节的帧头空 
间。主要用在Ethernet驱动程序。 
.kfree_skb() 释放一个sk_buff。 
.skb_clone() 复制一个sk_buff,但不复制数据部分。 
.skb_copy()完全复制一个sk_buff。 
.skb_dequeue() 从一个sk_buff链表里取出第一个元素。返回取出的sk_buff, 
如果链表空则返回NULL。这是常用的一个操作。 
.skb_queue_head() 在一个sk_buff链表头放入一个元素。 
.skb_queue_tail() 在一个sk_buff链表尾放入一个元素。这也是常用的一个 
操作。网络数据的处理主要是对一个先进先出队列的管理,skb_queue_tail() 
和skb_dequeue()完成这个工作。 
.skb_insert() 在链表的某个元素前插入一个元素。 
.skb_append() 在链表的某个元素后插入一个元素。一些协议(如TCP)对没按 
顺序到达的数据进行重组时用到skb_insert()和skb_append()。 
.skb_reserve() 在一个申请好的sk_buff的缓冲区里保留一块空间。这个空间 
一般是用做下一层协议的头空间的。 
.skb_put() 在一个申请好的sk_buff的缓冲区里为数据保留一块空间。在 
alloc_skb以后,申请到的sk_buff的缓冲区都是处于空(free)状态,有一个 
tail指针指向free空间,实际上开始时tail就指向缓冲区头。skb_reserve() 
在free空间里申请协议头空间,skb_put()申请数据空间。见下面的图。 
.skb_push() 把sk_buff缓冲区里数据空间往前移。即把Head room中的空间移 
一部分到Data area。 
.skb_pull() 把sk_buff缓冲区里Data area中的空间移一部分到Head room中。 
-------------------------------------------------- 
| Tail room(free) | 
-------------------------------------------------- 
After alloc_skb() 
-------------------------------------------------- 
| Head room | Tail room(free) | 
-------------------------------------------------- 
After skb_reserve() 
-------------------------------------------------- 
| Head room | Data area | Tail room(free) | 
-------------------------------------------------- 
After skb_put() 
-------------------------------------------------- 
|Head| skb_ | Data | Tail room(free) | 
|room| push | | | 
| | Data area | | 
-------------------------------------------------- 
After skb_push() 
-------------------------------------------------- 
| Head | skb_ | Data area | Tail room(free) | 
| | pull | | | 
| Head room | | | 
-------------------------------------------------- 
After skb_pull() 
------------------ Linux操作系统网络驱动程序编写 ------------------- 
------------ Contact the author by mailto:bordi@bordi.dhs.org ------ 
三.编写Linux网络驱动程序中需要注意的问题 
3.1 中断共享 
Linux系统运行几个设备共享同一个中断。需要共享的话,在申请的时候指明 
共享方式。系统提供的request_irq()调用的定义: 
int request_irq(unsigned int irq, 
void (*handler)(int irq, void *dev_id, struct pt_regs *regs 
), 
unsigned long irqflags, 
const char * devname, 
void *dev_id); 
如果共享中断,irqflags设置SA_SHIRQ属性,这样就允许别的设备申请同一个 
中断。需要注意所有用到这个中断的设备在调用request_irq()都必须设置这个属 
性。系统在回调每个中断处理程序时,可以用dev_id这个参数找到相应的设备。一 
般dev_id就设为device结构本身。系统处理共享中断是用各自的dev_id参数依次调 
用每一个中断处理程序。 
3.2 硬件发送忙时的处理 
主CPU的处理能力一般比网络发送要快,所以经常会遇到系统有数据要发,但 
上一包数据网络设备还没发送完。因为在Linux里网络设备驱动程序一般不做数据 
缓存,不能发送的数据都是通知系统发送不成功,所以必须要有一个机制在硬件不 
忙时及时通知系统接着发送下面的数据。 
一般对发送忙的处理在前面设备的发送方法(hard_start_xmit)里已经描述过, 
即如果发送忙,置tbusy为1。处理完发送数据后,在发送结束中断里清tbusy,同 
时用mark_bh()调用通知系统继续发送。 
但在具体实现我的驱动程序时发现,这样的处理系统好象并不能及时地知道硬 
件已经空闲了,即在mark_bh()以后,系统要等一段时间才会接着发送。造成发送 
效率很低。2M线路只有10%不到的使用率。内核版本为2.0.35。 
我最后的实现是不把tbusy置1,让系统始终认为硬件空闲,但是报告发送不成 
功。系统会一直尝试重发。这样处理就运行正常了。但是遍循内核源码中的网络驱 
动程序,似乎没有这样处理的。不知道症结在哪里。 
3.3 流量控制(flow control) 
网络数据的发送和接收都需要流量控制。这些控制是在系统里实现的,不需要 
驱动程序做工作。每个设备数据结构里都有一个参数dev->tx_queue_len,这个参数 
标明发送时最多缓存的数据包。在Linux系统里以太网设备(10/100Mbps) 
tx_queue_len一般设置为100,串行线路(异步串口)为10。实际上如果看源码可以 
知道,设置了dev->tx_queue_len并不是为缓存这些数据申请了空间。这个参数只是 
在收到协议层的数据包时判断发送队列里的数据是不是到了tx_queue_len的限度, 
以决定这一包数据加不加进发送队列。发送时另一个方面的流控是更高层协议的发 
送窗口(TCP协议里就有发送窗口)。达到了窗口大小,高层协议就不会再发送数据。 
接收流控也分两个层次。netif_rx()缓存的数据包有限制。另外高层协议也会 
有一个最大的等待处理的数据量。 
发送和接收流控处理在net/core/dev.c的do_dev_queue_xmit()和netif_rx() 
中。 
3.4 调试 
很多Linux的驱动程序都是编译进内核的,形成一个大的内核文件。但对调试 
来说,这是相当麻烦的。调试驱动程序可以用module方式加载。支持模块方式的 
驱动程序必须提供两个函数:int init_module(void)和void cleanup_module(void)。 

init_module()在加载此模块时调用,在这个函数里可以register_netdev()注册 
设备。init_module()返回0表示成功,返回负表示失败。cleanup_module()在驱动 
程序被卸载时调用,清除占用的资源,调用unregister_netdev()。 
模块可以动态地加载、卸载。在2.0.xx版本里,还有kerneld自动加载模块, 
但是2.2.xx中已经取消了kerneld。手工加载使用insmod命令,卸载用rmmod命令, 
看内核中的模块用lsmod命令。 
编译驱动程序用gcc,主要命令行参数-DKERNEL -DMODULE。并且作为模块加载 
的驱动程序,只编译成obj形式(加-c参数)。编译好的目标文件放在/lib/modules 
/2.x.xx/misc下,在启动文件里用insmod加载。 
四.进一步的阅读 
Linux程序设计资料可以从网上获得。这就是开放源代码的好处。并且没有什 
么“未公开的秘密”。我编写驱动程序时参阅的主要资料包括: 
Linux内核源代码 
<[td]> by Michael K. Johnson 
<> by Ori Pomerantz 
<> by olly in BBS水木清华站 
可以选择一个模板作为开始,内核源代码里有一个网络驱动程序的模板, 
drivers/net/skeleton.c。里面包含了驱动程序的基本内容。但这个模板是以以太 
网设备为对象的,以太网的处理在Linux系统里有特殊“待遇”,所以如果不是以 
太网设备,有些细节上要注意,主要在初始化程序里。 
最后,多参照别人写的程序,听听其他开发者的经验之谈大概是最有效的帮助 
了。

Linux的源码里,网络接口的实现部份是非常值得一读的,通过读源码,不仅对网络协议会有更深的了解,也有助于在网络编程的时候,对应用函数有更精确的了解和把握。

  本文把重点放在网络接口程序的总体结构上,希望能作为读源码时一些指导性的文字。

  本文以Linux2.4.16内核作为讲解的对象,内核源码可以在
http://www.kernel.org上下载。我读源码时参考的是http://lxr.linux.no/这个交差参考的网站,我个人认为是一个很好的工具,如果有条件最好上这个网站。

  二.网络接口程序的结构

  Linux的网络接口分为四部份:网络设备接口部份,网络接口核心部份,网络协议族部份,以及网络接口socket层。
  网络设备接口部份主要负责从物理介质接收和发送数据。实现的文件在linu/driver/net目录下面。

  网络接口核心部份是整个网络接口的关键部位,它为网络协议提供统一的发送接口,屏蔽各种各样的物理介质,同时有负责把来自下层的包向合适的协议配送。它是网络接口的中枢部份。它的主要实现文件在linux/net/core目录下,其中linux/net/core/dev.c为主要管理文件。

  网络协议族部份是各种具体协议实现的部份。Linux支持TCP/IP,IPX,X.25,AppleTalk等的协议,各种具体协议实现的源码在linux/net/目录下相应的名称。在这里主要讨论TCP/IP(IPv4)协议,实现的源码在linux/net/ipv4,其中linux/net/ipv4/af_inet.c是主要的管理文件。

  网络接口Socket层为用户提供的网络服务的编程接口。主要的源码在linux/net/socket.c

  三.网络设备接口部份

  物理层上有许多不同类型的网络接口设备, 在文件include/linux/if_arp.h的28行里定义了ARP能处理的各种的物理设备的标志符。网络设备接口要负责具体物理介质的控制,从物理介质接收以及发送数据,并对物理介质进行诸如最大数据包之类的各种设置。这里我们以比较简单的3Com3c501 太网卡的驱动程序为例,大概讲一下这层的工作原理。源码在Linux/drivers/net/3c501.c。

  我们从直觉上来考虑,一个网卡当然最主要的是完成数据的接收和发送,在这里我们来看看接收和发送的过程是怎么样的。

  发送相对来说比较简单,在Linux/drivers/net/3c501.c的行475 开始的el_start_xmit()这个函数就是实际向3Com3c501以太网卡发送数据的函数,具体的发送工作不外乎是对一些寄存器的读写,源码的注释很清楚,大家可以看看。

  接收的工作相对来说比较复杂。通常来说,一个新的包到了,或者一个包发送完成了,都会产生一个中断。Linux/drivers/net/3c501.c的572开始el_interrupt()的函数里面,前半部份处理的是包发送完以后的汇报,后半部份处理的是一个新的包来的,就是说接收到了新的数据。el_interrupt()函数并没有对新的包进行太多的处理,就交给了接收处理函数el_receive()。el_receive()首先检查接收的包是否正确,如果是一个“好”包就会为包分配一个缓冲结构(dev_alloc_skb()),这样驱动程序对包的接收工作就完成了,通过调用上层的函数netif_rx()(net/core/dev.c1214行) ,把包交给上层。

  现在驱动程序有了发送和接收数据的功能了,驱动程序怎么样和上层建立联系呢?就是说接收到包以后怎么送给上层,以及上层怎么能调用驱动程序的发送函数呢?

  由下往上的关系,是通过驱动程序调用上层的netif_rx()(net/core/dev.c 1214行)函数实现的,驱动程序通过这个函数把接到的数据交给上层,请注意所有的网卡驱动程序都需要调用这个函数的,这是网络接口核心层和网络接口设备联系的桥梁。

  由上往下的关系就复杂点。网络接口核心层需要知道有多少网络设备可以用,每个设备的函数的入口地址等都要知道。网络接口核心层会大声喊,“嘿,有多少设备可以帮我发送数据包?能发送的请给我排成一队!”。这一队就由dev_base开始,指针structnet_device *dev_base (Linux/include/linux/netdevice.h 436行)就是保存了网络接口核心层所知道的所有设备。对于网络接口核心层来说,所有的设备都是一个net_device结构,它在include/linux/netdevice.h,line 233里被定义,这是从网络接口核心层的角度看到的一个抽象的设备,我们来看看网络接口核心层的角度看到的网络设备具有的功能:

  struct net_device {
  ………
  open()
  stop()
  hard_start_xmit()
  hard_header()
  rebuild_header()
  set_mac_address()
  do_ioctl()
  set_config()
  hard_header_cache()
  header_cache_update()
  change_mtu()
  tx_timeout()
  hard_header_parse()
  neigh_setup()
  accept_fastpath()
  ………
  }

  如果网络接口核心层需要由下层发送数据的时候,在dev_base找到设备以后,就直接调dev->hard_start_xmit()的这个函数来让下层发数据包。 

  驱动程序要让网络接口核心层知道自己的存在,当然要加入dev_base所指向的指针链,然后把自己的函数以及各种参数和net_device里的相应的域对应起来。加入dev_base所指向的指针链是通过函数register_netdev(&dev_3c50)(linux/drivers/net/net_init.c, line 532)

  建立的。而把自己的函数以和net_device里的相应的域及各种参数关系的建立是在el1_probe1()(Linux/drivers/net/3c501.c)里进行的:

  el1_probe1(){
  ………
  dev->open = &el_open;
  dev->hard_start_xmit = &el_start_xmit;
  dev->tx_timeout = &el_timeout;
  dev->watchdog_timeo = HZ;
  dev->stop = &el1_close;
  dev->get_stats = &el1_get_stats;
  dev->set_multicast_list = &set_multicast_list;
  ………
  ether_setup(dev);
  ………

  }

  进一步的对应工作在ether_setup(dev) (drivers/net/net_init.c, line 405 )里进行。我们注意到dev->hard_start_xmit =&el_start_xmit,这样发送函数的关系就建立了,上层只知道调用dev->hard_start_xmit这个来发送数据,上面的语句就把驱动程序实际的发送函数告诉了上层。

  四.网络接口核心部分

  刚才谈论了驱动程序怎么和网络接口核心层衔接的。网络接口核心层知道驱动程序以及驱动程序的函数的入口是通过*dev_base指向的设备链的,而下层是通过调用这一层的函数netif_rx()(net/core/dev.c
1214行) 把数据传递个这一层的。

  网络接口核心层的上层是具体的网络协议,下层是驱动程序,我们以及解决了下层的关系,但和上层的关系没有解决。先来讨论一下网络接口核心层和网络协议族部份的关系,这种关系不外乎也是接收和发送的关系。

  网络协议,例如IP,ARP等的协议要发送数据包的时候会把数据包传递给这层,那么这种传递是通过什么函数来发生的呢?网络接口核心层通过dev_queue_xmit()(net/core/dev.c,line975)这个函数向上层提供统一的发送接口,也就是说无论是IP,还是ARP协议,通过这个函数把要发送的数据传递给这一层,想发送数据的时候就调用这个函数就可以了。dev_queue_xmit()做的工作最后会落实到dev->hard_start_xmit(),而dev->hard_start_xmit()会调用实际的驱动程序来完成发送的任务。例如上面的例子中,调用dev->hard_start_xmit()实际就是调用了el_start_xmit()。

  现在讨论接收的情况。网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据,这时候当然要把数据包往上层派送。所有的协议族的下层协议都需要接收数据,TCP/IP的IP协议和ARP协议,SPX/IPX的IPX协议,AppleTalk的DDP和AARP协议等都需要直接从网络接口核心层接收数据,网络接口核心层接收数据是如何把包发给这些协议的呢?这时的情形和于下层的关系很相似,网络接口核心层的下面可能有许多的网卡的驱动程序,为了知道怎么向这些驱动程序发数据,前面以及讲过时,是通过*dev_base这个指针指向的链解决的,现在解决和上层的关系是通过static struct packet_ptype_base[16]( net/core/dev.c line 164)这个数组解决的。这个数组包含了需要接收数据包的协议,以及它们的接收函数的入口。 

  从上面可以看到,IP协议接收数据是通过ip_rcv()函数的,而ARP协议是通过arp_rcv()的,网络接口核心层只要通过这个数组就可以把数据交给上层函数了。

  如果有协议想把自己添加到这个数组,是通过dev_add_pack()(net/core/dev.c, line233)函数,从数组删除是通过dev_remove_pack()函数的。Ip层的注册是在初始化函数进行的void __init ip_init(void) (net/ipv4/ip_output.c, line 1003)

  {
  ………
  dev_add_pack(&ip_packet_type);
  ………

  }

  重新到回我们关于接收的讨论,网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据,看看这个函数做了些什么。 

  由于现在还是在中断的服务里面,所有并不能够处理太多的东西,剩下的东西就通过cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ)

  交给软中断处理, 从open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL)可以知道NET_RX_SOFTIRQ软中断的处理函数是net_rx_action()(net/core/dev.c, line 1419),net_rx_action()根据数据包的协议类型在数组ptype_base[16]里找到相应的协议,并从中知道了接收的处理函数,然后把数据包交给处理函数,这样就交给了上层处理,实际调用处理函数是通过net_rx_action()里的pt_prev->func()这一句。例如如果数据包是IP协议的话,ptype_base[ETH_P_IP]->func()(ip_rcv()),这样就把数据包交给了IP协议。

 五.网络协议部分

  协议层是真正实现是在这一层。在linux/include/linux/socket.h里面,Linux的BSD
Socket定义了多至32支持的协议族,其中PF_INET就是我们最熟悉的TCP/IP协议族(IPv4, 以下没有特别声明都指IPv4)。以这个协议族为例,看看这层是怎么工作的。实现TCP/IP协议族的主要文件在inux/net/ipv4/目录下面,Linux/net/ipv4/af_inet.c为主要的管理文件。

  在Linux2.4.16里面,实现了TCP/IP协议族里面的的IGMP,TCP,UDP,ICMP,ARP,IP。我们先讨论一下这些协议之间的关系。IP和ARP协议是需要直接和网络设备接口打交道的协议,也就是需要从网络核心模块(core)
接收数据和发送数据的。而其它协议TCP,UDP,IGMP,ICMP是需要直接利用IP协议的,需要从IP协议接收数据,以及利用IP协议发送数据,同时还要向上层Socket层提供直接的调用接口。可以看到IP层是一个核心的协议,向下需要和下层打交道,又要向上层提供所以的传输和接收的服务。

  先来看看IP协议层。网络核心模块(core) 如果接收到IP层的数据,通过ptype_base[ETH_P_IP] 数组的IP层的项指向的IP协议的ip_packet_type->ip_rcv()函数把数据包传递给IP层,也就是说IP层通过这个函数ip_rcv()(linux/net/ipv4/ip_input.c)接收数据的。ip_rcv()这个函数只对IP数据保做了一些checksum的检查工作,如果包是正确的就把包交给了下一个处理函数ip_rcv_finish()(注意调用是通过NF_HOOK这个宏实现的)。现在,ip_rcv_finish()这个函数真正要完成一些IP层的工作了。IP层要做的主要工作就是路由,要决定把数据包往那里送。路由的工作是通过函数ip_route_input()(/linux/net/ipv4/route.c,line 1622)实现的。对于进来的包可能的路由有这些:

  属于本地的数据(即是需要传递给TCP,UDP,IGMP这些上层协议的) ;
  需要要转发的数据包(网关或者NAT服务器之类的);
  不可能路由的数据包(地址信息有误);

  我们现在关心的是如果数据是本地数据的时候怎么处理。ip_route_input()调用ip_route_input_slow()(net/ipv4/route.c, line 1312),在ip_route_input_slow()里面的1559行rth->u.dst.input=

  ip_local_deliver,这就是判断到IP包是本地的数据包,并把本地数据包处理函数的地址返回。好了,路由工作完成了,返回到ip_rcv_finish()。ip_rcv_finish()最后调用拉skb->dst->input(skb),从上面可以看到,这其实就是调用了ip_local_deliver()函数,而ip_local_deliver(),接着就调用了ip_local_deliver_finish()。现在真正到了往上层传递数据包的时候了。

  现在的情形和网络核心模块层(core) 往上层传递数据包的情形非常相似,怎么从多个协议选择合适的协议,并且往这个协议传递数据呢?网络网络核心模块层(core) 通过一个数组ptype_base[16]保存了注册了的所有可以接收数据的协议,同样网络协议层也定义了这样一个数组struct net_protocol*inet_protos[MAX_INET_PROTOS](/linux/net/ipv4/protocol.c#L102),它保存了所有需要从IP协议层接收数据的上层协议(IGMP,TCP,UDP,ICMP)的接收处理函数的地址。我们来看看TCP协议的数据结构是怎么样的:

  linux/net/ipv4/protocol.c line67
  static struct inet_protocol tcp_protocol = {
  handler: tcp_v4_rcv,// 接收数据的函数
  err_handler: tcp_v4_err,// 出错处理的函数
  next: IPPROTO_PREVIOUS,
  protocol: IPPROTO_TCP,
  name: "TCP"
  };

  第一项就是我们最关心的了,IP层可以通过这个函数把数据包往TCP层传的。在linux/net/ipv4/protocol.c的上部,我们可以看到其它协议层的处理函数是igmp_rcv(),
udp_rcv(), icmp_rcv()。同样在linux/net/ipv4/protocol.c,往数组inet_protos[MAX_INET_PROTOS] 里面添加协议是通过函数inet_add_protocol()实现的,删除协议是通过 inet_del_protocol()实现的。inet_protos[MAX_INET_PROTOS]初始化的过程在linux/net/ipv4/af_inet.c inet_init()初始化函数里面。

  inet_init(){
  ……
  printk(KERN_INFO "IP Protocols: ");
  for (p = inet_protocol_base; p != NULL;) {
  struct inet_protocol *tmp = (struct inet_protocol *) p->next;
  inet_add_protocol(p);// 添加协议
  printk("%s%s",p->name,tmp?", ":"\n");
  p = tmp;
  ………
  }


  如果你在Linux启动的时候有留意启动的信息, 或者在linux下打命令dmesg就可以看到这一段程序输出的信息:
  IP Protocols: ICMP,UDP,TCP,IGMP也就是说现在数组inet_protos[]里面有了ICMP,UDP,TCP,IGMP四个协议的inet_protocol数据结构,数据结构包含了它们接收数据的处理函数。

  Linux 2.4.16在linux/include/linux/socket.h里定义了32种支持的BSDsocket协议,常见的有TCP/IP,IPX/SPX,X.25等,而每种协议还提供不同的服务,例如TCP/IP协议通过TCP协议支持连接服务,而通过UDP协议支持无连接服务,面对这么多的协议,向用户提供统一的接口是必要的,这种统一是通过socket来进行的。

  在BSD socket网络编程的模式下,利用一系列的统一的函数来利用通信的服务。例如一个典型的利用TCP协议通信程序是这样:

  sock_descriptor = socket(AF_INET,SOCK_STREAM,0);
  connect(sock_descriptor, 地址,) ;
  send(sock_descriptor,”hello world”);
  recv(sock_descriptor,buffer,1024,0);

  第一个函数指定了协议Inet协议,即TCP/IP协议,同时是利用面向连接的服务,这样就对应到TCP协议,以后的操作就是利用socket的标准函数进行的。

  从上面我们可以看到两个问题,首先socket层需要根据用户指定的协议族(上面是AF_INET)
从下面32种协议中选择一种协议来完成用户的要求,当协议族确定以后,还要把特定的服务映射到协议族下的具体协议,例如当用户指定的是面向连接的服务时,Inet协议族会映射到TCP协议。

  从多个协议中选择用户指定的协议,并把具体的出理交给选中的协议,这和一起网络核心层向上和向下衔接的问题本质上是一样的,所以解决的方法也是一样的,同样还是通过数组。在Linux/net/socket.c定义了这个数组staticstruct net_proto_family *net_families[NPROTO] 。数组的元素已经确定了,net_families[2] 是TCP/IP协议,net_families[3]

  是X.25协议,具体那一项对应什么协议,在include/linux/socket.h有定义。但是每一项的数据结构net_proto_family的ops是空的,也就是具体协议处理函数的地址是不知道的。协议的处理函数和ops建立联系是通过sock_register()(Linux/net/socket.c)这个函数建立的,例如TCP/IP协议的是这样建立关系的:

  int __init inet_init(void) (net/ipv4/af_inet.c)
  {
  (void) sock_register(&inet_family_ops);

  }

  只要给出AF_INET(在宏里定义是2),就可以找到net_failies[2] 里面的处理函数了。

  协议的映射完成了,现在要进行服务的映射了。上层当然不可能知道下层的什么协议能对应特定的服务,所以这种映射自然由协议族自己完成。在TCP/IP协议族里,这种映射是通过struct
list_head inetsw[SOCK_MAX]( net/ipv4/af_inet.c)

  这个数组进行映射的,在谈论这个数组之前我们来看另外一个数组inetsw_array[](net/ipv4/af_inet.c)

  static struct inet_protosw inetsw_array[] =
  {
  {
  type: SOCK_STREAM,
  protocol: IPPROTO_TCP,
  prot: &tcp_prot,
  ops: &inet_stream_ops,
  capability: -1,
  no_check: 0,
  flags: INET_PROTOSW_PERMANENT,
  },

  {
  type: SOCK_DGRAM,
  protocol: IPPROTO_UDP,
  prot: &udp_prot,
  ops: &inet_dgram_ops,
  capability: -1,
  no_check: UDP_CSUM_DEFAULT,
  flags: INET_PROTOSW_PERMANENT,
  },

  {
  type: SOCK_RAW,
  protocol: IPPROTO_IP, /* wild card */
  prot: &raw_prot,
  ops: &inet_dgram_ops,
  capability: CAP_NET_RAW,
  no_check: UDP_CSUM_DEFAULT,
  flags: INET_PROTOSW_REUSE,
  }
  };


  我们看到,SOCK_STREAM映射到了TCP协议,SOCK_DGRAM映射到了UDP协议,SOCK_RAW映射到了IP协议。现在只要把inetsw_array里的三项添加到数组inetsw[SOCK_MAX]就可以了,添加是通过函数inet_register_protosw()实现的。在inet_init()

  (net/ipv4/af_inet.c) 里完成了这些工作。

  还有一个需要映射的就是socket其它诸如accept,send(),

  connect(),release(),bind()等的操作函数是怎么映射的呢?我们来看一下上面的数组的TCP的项
  {
  type: SOCK_STREAM,
  protocol: IPPROTO_TCP,
  prot: &tcp_prot,
  ops: &inet_stream_ops,
  capability: -1,
  no_check: 0,
  flags: INET_PROTOSW_PERMANENT,
  },

  我们看到这种映射是通过ops,和prot来映射的,我们再来看看 tcp_prot这一项:

  struct proto tcp_prot = {
  name: "TCP",
  close: tcp_close,
  connect: tcp_v4_connect,
  disconnect: tcp_disconnect,
  accept: tcp_accept,
  ioctl: tcp_ioctl,
  init: tcp_v4_init_sock,
  destroy: tcp_v4_destroy_sock,
  shutdown: tcp_shutdown,
  setsockopt: tcp_setsockopt,
  getsockopt: tcp_getsockopt,
  sendmsg: tcp_sendmsg,
  recvmsg: tcp_recvmsg,
  backlog_rcv: tcp_v4_do_rcv,
  hash: tcp_v4_hash,
  unhash: tcp_unhash,
  get_port: tcp_v4_get_port,
  };


  所以的映射都已经完成了,用户调用connect()函数,其实就是调用了tcp_v4_connect()函数,按照这幅图,读起源码来就简单了很多了。
  六 Socket层

  上一节把socket层大多数要讨论的东西都谈论了,现在只讲讲socket 层和用户的衔接。

  系统调用socket(),bind(),connect(),accept,send(),release()等是在Linux/net/socket.c里面的实现的,系统调用实现的函数是相应的函数名加上sys_的前缀。

  现在看看当用户调用socket()这个函数,到底下面发生了什么。

  Socket(AF_INET,SOCK_STREAM,0)调用了sys_socket(),sys_socket()接着调用socket_creat(),socket_creat()就要根据用户提供的协议族参数在net_families[]里寻找合适的协议族,如果协议族没有被安装就要请求安装该协议族的模块,然后就调用该协议族的create()函数的处理句柄。根据参数AF_INET,inet_creat()就被调用了,在inet_creat()根据服务类型在inetsw[SOCK_MAX]

  选择合适的协议,并把协议的操作集赋给socket就是了,根据SOCK_STREAM,TCP协议被选中,
  inet_creat(){
  answer=inetsw [用户要求服务服务] ;
  sock->ops = answer->ops;
  sk->prot = answer->prot
  }

  到此为止,上下都打通了,该是大家都源码的时候了。


0 0
原创粉丝点击