Linux TCP-IP
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Linux对TCP/IP的支持浅析
( 北京工业大学魏勇100022 )
本文着重分析了Linux中网络协议栈TCP/IP的实现,重点放在协议栈的整体结构和Linux网络设备驱动程序的分析上面。
下面的介绍中将以Linux中的ne2000网络设备驱动程序为例来分析Linux对网络的支持。
1. Linux网络支持的基本原理
Linux的网络实现是以BSD为模型的,它支持BSD sockets(及一些扩展)。
Linux选用这个编程接口是因为它很流行,并且有助于应用程序从Linux平台移植到其它Unix 平台。
Linux下的TCP/IP 网络协议栈的各层之间是通过一系列互相连接的层次结构来实现Internet地址族的,具体结构层次如图1所示。
图1 Linux下结构层次示意图
其中:
BSD socket层由专门用来处理BSD Socket的通用套接字管理软件来处理,它由INET socket 层来支持。
INET Socket为基于IP的协议TCP和UDP管理传输端点。
在IP层,实现了Internet协议的代码。
这些代码要给传输的数据加上一个IP头,并且知道如
何把传入的IP包送给TCP或者UDP协议。
在IP层以下,就是具体得型号各异的各种网络设备,如PLIP、SLIP和以太网。
不过要注意,这些网络设备不一定都是物理设备,可以使用像Loopback这样的虚拟网络设备,纯粹是用软件来实现的。
在Linux中,为了简化对设备的管理,将所有的外围设备都归结为三类:字符设备(如键盘,鼠标等)、块设备(如硬盘、软驱等)和网络设备(如网卡、串口等等)。
为了将网络环境中的物理网络设备的多样性屏蔽,Linux对所有的网络物理设备抽象并且定义了一个统一的概念:接口(Interface)。
对于所有的网络硬件的访问都是通过接口进行访问的,接口实际上提供了一个对于所有类型的网络硬件的一致化的操作集合,用来处理对数据的发送和接收。
对于每一个已经驱动了的网络设备,都用一个struct device的数据结构表示。
在内核启动或者驱动模块插入时,通过网络驱动程序,向系统注册检测到的网络设备。
在进行网络数据传输的时候,网络驱动程序需要负责通过标准的接口将数据发送到相应的网络层,或者向网络发送数据包。
需要注意的是,网络接口并不存在于Linux的文件系统中,而是定义在内核中的一个struct device的一个数据结构中。
每一个device的数据结构都是在驱动的时候创建的,而不像字符设备或者块设备那样,即使不存在物理设备也存在有这个设备文件。
这是在Linux 2..2内核版本之后做的改动,在这个版本之前,一旦网络驱动程序被激活,在/dev目录下就会出现eth0的文件,对这个设备文件的读写就完成了网络传输;这种做法现在还是用在字符设备和块设备上。
现在的版本是数据包的发送和接收都是通过直接对接口进行访问来完成的。
对于在系统中的所有网络设备,都是通过一张网络接口管理表dev_base统一进行管理的。
dev_base是一个指向的struct device结构的链表,在系统初始化完成之后,系统检测到的所有网络设备都将自动的保存在这张链表中,链表中的每一个字节都代表着一个系统检测到的网络设备。
当系统需要发送数据的时候,网络子系统根据系统路由表选择相应的网络接口进行数据传输;而当接收到数据包时,通过驱动程序登记的中断服务程序进行数据的接收(纯软件实现的网络设备除外)。
2.结构
这个数据结构是在系统中每一个设备的代表。
每一个被系统检测到的网络设备都应该使用struct device类型的节点存在于dev_base中。
dev_base是内核中存在的外部变量。
如果采取的是内核检测网卡设备的方式,那么在整个内核没有开始进行网络设备的检测时候,dev_base中是所有内核可能支持的网络设备的struct device节点;在网络驱动程序检测完之后,将不存在的网络设备对应的节点删除,剩下的就是检测到并且得到正确驱动的网络设备。
在这个数据结构里面,主要定义的成员变量是init函数指针,这个函数指针初始化为设备驱动程序中提供的用来初始化device结构的程序,这个程序实际上就是用来检测和驱动网络设备的。
在检测进行之前,每一个节点的init函数都指向对应的初始化函数;检测的时候对每一个节点都分别调用init函数指针,如果成功返回的话,将该节点保留。
同时,在这个结构里面还定义了对硬件设备的打开和关闭函数指针(open, close);硬件头的建立函数指针(hard_header);硬件上数据的传输过程(hard_start_xmit)。
当网络设备打开的时候,就可以通过这个网络设备开始传输数据了,传输出来的数据存放在struct sk_buff结构里面。
hard_start_xmit函数指针是和某一种具体的硬件相关的,通过dev_queue_xmit这个外部函数调用hard_start_xmit函数指针完成网络数据的发送过程。
3. 模块驱动方法
驱动网络设备的方法有两种:模块驱动方法和内核启动时自动检测的方法。
通过模块驱动的方法是Linux中使用模块设计的一种方案。
我们知道,Linux的内核是将所有的支持编译在一起的,并不是微内核技术。
如果对Linux内核增加一项功能,就把它的实现直接放在内核的代码中。
不过为了让Linux的内核体积不至于过于庞大,采用了编译成模块的方式。
在需要用到这个模块的时候,用shell命令的insmod将该模块插入到内核运行空间;如果不需要了,可以用rmmod命令将该模块卸载。
insmod触发的是程序里面的init_module()函数;而rmmod命令触发的是cleanup_module()函数。
在init_module()函数里面,会调用到这种网络设备的init函数指针,如果检测到了这种网络设备,并且初始化成功,那么就将这个网络设备对应的device结构插入到dev_base链表里面。
在ne.c里面,init_module()函数首先初始化dev设备的init函数指针,然后调用register_netdev()函数在系统中登记该设备。
如果登记成功,那么模块插入成功,否则就返回出错信息。
在register_netdev()里面,首先检查该网络设备名是否已经确定,如果没有,就赋给一个缺省的名称。
然后,调用网络设备驱动程序中的init_function,也就是dev->init函数指针来检测网卡设备是否存在,并且做dev的初始化工作。
如果初始化成功,将dev插入到dev_base链表的尾部。
整个调用流程参看图2。
4. 内核启动驱动方法
内核启动的驱动方法和模块驱动的方法不同,前者要对所有内核支持的网络设备进行检测和初始化,而后者只需要检测和初始化被装载的网络设备。
为了实现在启动的时候对所有可能存在的网络设备都检测一遍,系统会在启动之前将所有支持的网络设备对应的device结构都挂在dev_base链表上,然后使用net_dev_init()函数依次对dev_base里面每一个节点都运行init函数指针,如果返回成功,那么该节点对应的设备存在;否则就将该节点删除,最后在链表中剩下的所有网络设备都是存在的,并且已经完成了初始化。
这里需要解释一下dev_base的组成,在drivers/net/Space.c里面定义了一张由device结构组成的链表:
//Space.c
670 static struct device eth7_dev = {
671 "eth7", 0,0,0,0,ETH_NOPROBE_ADDR /* I/O 地址*/, 0,0,0,0, NEXT_DEV, ethif_probe};
672 static struct device eth6_dev = {
673 "eth6", 0,0,0,0,ETH_NOPROBE_ADDR /* I/O 地址*/, 0,0,0,0, ð7_dev, ethif_probe };
……
685 static struct device eth0_dev = {
686 "eth0", 0, 0, 0, 0, ETH0_ADDR, ETH0_IRQ, 0, 0, 0, ð1_dev, ethif_probe };/*eth0进行自动检测*/ 687
688 # undef NEXT_DEV
689 # define NEXT_DEV (ð0_dev)/*将eth_dev串起来*/
690
691 #if defined(SLIP) || defined(CONFIG_SLIP)
692/* To be exact, this node just hooks the initialization
693routines to the device structures. */
694 extern int slip_init_ctrl_dev(struct device *);
695 static struct device slip_bootstrap = {
696 "slip_proto", 0x0, 0x0, 0x0, 0x0, 0, 0, 0, 0, 0, NEXT_DEV, slip_init_ctrl_dev, };
697 #undef NEXT_DEV
698 #define NEXT_DEV (&slip_bootstrap)/*将slip串起来*/
699 #endif /* SLIP */
…………/*将所有其它的设备串起来,省略*/
915 struct device *dev_base = &loopback_dev;/*dev_base的头*/
_____________________________________________________________________ 这样就形成了dev_base链表。
当系统转入内核后,start_kernel会创建一个init进程,这个init进程会通过系统调用sys_setup 进行所有尚未初始化的设备的初始化(内存,PCI等已经在此之前初始化过了)。
在sys_setup中调用device_setup,进而调用net_dev_init检测和初始化所有的网络设备。
net_dev_init内部调用所有dev->init函数指针,进行具体的物理设备的初始化工作。
5. 网卡初始化函数分析
在内核启动初始化和模块驱动初始化两种方式中,驱动网卡的过程中都调用了网卡驱动程序的init函数,也就是网卡的初始化函数。
在这个函数里面主要要完成如下的任务:
(1) 检测该设备是否存在;
(2) 自动检测该设备的I/O地址和中断号;
(3) 填写该设备对应device结构所需要的大部分域段;
(4) 在内核内存空间中申请需要的内存空间。
对ne.c驱动程序来说,初始化函数是ne_probe()函数。
我们来看一下他的源代码:
_________________________________________________________________ne.c
/*drivers/net/ne.c*/
180 __initfunc(int ne_probe(struct device *dev))
181 {
182 int base_addr = dev ? dev->base_addr : 0;
183 /*如果制定IO地址,就不自动检测*/
184 /* First check any supplied i/o locations. User knows best. <cough> */
185 if (base_addr > 0x1ff) /* Check a single specified location. */
186 return ne_probe1(dev, base_addr);/*不自动检测*/
187 else if (base_addr != 0) /* Don't probe at all. */
188 return ENXIO;/*不检测,也不指定*/
189
190 #ifdef CONFIG_PCI
191 /* Then look for any installed PCI clones */
192 if (probe_pci && pci_present() && (ne_probe_pci(dev) == 0))
193 return 0;/*检测ne2000 PCI,此处不分析*/
194 #endif
195
196 #ifndef MODULE/*非模块化*/
197 /* Last resort. The semi-risky ISA auto-probe. *//*自动检测*/
198 for (base_addr = 0; netcard_portlist[base_addr] != 0; base_addr++) { 199 int ioaddr = netcard_portlist[base_addr];
200 if (check_region(ioaddr, NE_IO_EXTENT))
201 continue;
202 if (ne_probe1(dev, ioaddr) == 0)
203 return 0;
204 }/*自动检测IO地址的原理是将portlist里面每一个都尝试一遍*/ 205 #endif
206
207 return ENODEV;
208 }
____________________________________________________________________ _________________________________________________________________ne.c 246 __initfunc(static int ne_probe1(struct device *dev, int ioaddr))
……
/*检测并初始化8390芯片,*/
……
426 if (dev->irq < 2)/*中断号无效,需要自动检测*/
427 {
428。
438 } else if (dev->irq == 2)/*设定2号中断可能就是9号中断*/
439 /* Fixup for users that don't know that IRQ 2 is really IRQ 9,
440 or don't know which one to set. */
441 dev->irq = 9;
……/*中断驱动程序为ei_interrupt,用来响应中断,接收数据*/
459 int irqval = request_irq(dev->irq, ei_interrupt,
460 pci_irq_line ? SA_SHIRQ : 0, name, dev);
……/*初始化dev结构,并且向系统登记IO地址*/
……
_____________________________________________________________________ 至此,网卡驱动完成。
网卡驱动完成后,如果系统需要使用该网卡传输网络数据,那么使用dev->hard_start_xmit()函数指针发送;根据注册的中断处理程序ei_interrupt(),接收数据,并且将数据传送到上层协议。
在这一层网络数据传输的工作情况可以参看图3。
另外,关于在网络驱动程序中的其它函数实现,如设备打开,设备关闭,数据包传输的过程
等等分析,在这里因为篇幅的关系不进行介绍,可以参看相关的源代码或其他参考书籍。
6. 结语
本文对Linux网络协议栈作了一下整体分析,这对于在嵌入式系统中实现网络协议栈有重要意义。
在嵌入式系统中一方面可以利用现有的成果,复用Linux现成的网络代码,以获得对TCP/IP 以及其它网络协议栈的支持,很大程度上减少开发工作量和开发成本。
另一方面可以通过对Linux 网络协议栈的分析,可在自己的微内核的基础上编写自己需要的网络协议栈,从而可根据具体的功能要求尽量精简代码,减少占用的内存空间,提高执行效率。
这是在大部分嵌入式系统开发中需要完成的内容,并且具有一定的难度。
不管如何,在Linux嵌入式系统中实现TCP/IP协议栈,分析已有的Linux的整个网络协议栈代码是非常必要的。
作者地址:
100022 北京工业大学429#信箱’99研魏勇
E-mail: wei_yong001@
Tel: (010)62077500转231。