TCPIP架构设计及应用Linux版.docx
- 文档编号:23920496
- 上传时间:2023-05-22
- 格式:DOCX
- 页数:60
- 大小:533.93KB
TCPIP架构设计及应用Linux版.docx
《TCPIP架构设计及应用Linux版.docx》由会员分享,可在线阅读,更多相关《TCPIP架构设计及应用Linux版.docx(60页珍藏版)》请在冰豆网上搜索。
TCPIP架构设计及应用Linux版
第1章
引言
利用Linux实现网络互联是开发人员最常使用的一种选择。
Linux不仅在服务器市场占有一定的地位,而且在小型嵌入式网络操作系统市场中,Linux也是最常用的一种选择。
所有这些都需要人们对TCP/IP代码有很好的理解。
有些产品需要实现防火墙;有些产品需要实现IPSec;有些产品需要修改TCP连接代码,以实现集群运算环境中的负载均衡;有些产品则需要提高SMP机器的可扩展性。
这些产品中大部分都是嵌入式系统,而互联是嵌入式系统必然要考虑的因素。
实时嵌入式产品对互联有着非常独特的需求,这类产品需要考虑缓冲区管理的要求,或者性能的要求。
所有这些都需要人们对协议栈实现以及相关支持框架有完整的了解。
如前所述,由于内存的限制,有些嵌入式互联产品只能编译很小一部分代码,这就要求人们对Linux源码组织结构有很好的理解,从而能够轻易地找出其中的相关代码。
几乎所有网络应用采用的都是非常基本的客户端-服务器技术。
服务器在某保留端口监听连接请求,客户端则将连接请求发送到服务器,而导致该基本技术比较复杂的原因是人们对安全或者负载均衡的考虑,这些因素提高了客户端/服务器技术的复杂度。
但是,该技术自身的基本实现非常简单,只需要实现客户端和服务器之间的简单对话即可。
例如,telnet和ftp服务都通过inet程序进行访问,隐藏了服务的全部细节。
不过在调整TCP/IP连接时,有很多可供调整的参数,从而使开发人员能够在不进行系统范围调整的情况下,最好地实现对连接的微调。
大部分网络应用的主要目的是数据交换。
一旦建立连接,客户端向服务器发送数据,或者服务器向客户端发送数据或者数据双向流动。
通过连接接收和发送数据的方法有很多种,这些方法的不同之处在于套接字连接在接收或者发送数据后,网络应用的阻塞方式。
本书仅讨论TCP协议,不讨论其他传输层协议。
因此,需要对TCP连接的建立过程有很好的理解。
TCP是一种面向连接的协议,它有一套初始化连接的过程,以及一套完全关闭连接的过程。
由于需要在初始化和关闭连接时进行握手,因此,TCP需要维护连接的状态,而完全理解TCP连接过程则需要对TCP状态有很好的了解。
本章将呈现Linux中TCP/IP协议栈的实现。
具体讨论Linux操作系统,包括进程、线程、系统调用、内核同步机制,以更好地理解TCP/IP协议栈的实现,不过由于篇幅所限,对这些内容并不做详细阐述。
此外还将介绍应用编程接口,以使读者能够了解如何使用TCP/IP协议栈进行数据传输。
最后,本章阐述了TCP状态,包括打开连接的三步握手过程,以及关闭连接的四步握手过程。
1.1TCP/IP协议栈概述
图1-1网络缓冲区sk_buff
下面介绍如何在Linux中实现TCP/IP协议栈。
首先要理解代表报文的Linux网络缓冲区。
sk_buff代表Linux中的报文结构(如图1-1所示)。
sk_buff携带了所有报文相关的信息,以及报文处理过程指针。
head、data、tail、end分别指向数据块的开始、实际数据的头、实际数据的结尾,以及数据块的结尾。
skb_shared_info对象附在sk_buff头的结尾,保存分页数据区的附加信息。
实际报文包含在数据块中,通过data和tail指针进行操作。
有关网络代码和网络驱动都要使用该缓冲区,第5章将对此进行详细的讨论。
下面来看一下协议栈在Linux中的实现,首先沿着协议栈向下,讨论报文从套接层到驱动层的处理过程,然后说明沿着协议栈向上的报文处理过程。
同时举例阐述如何沿着协议栈向下发送TCP数据。
一般来说,其他传输协议或多或少都将使用相同的协议栈,但是,本章所述内容将仅限于TCP。
1.1.1沿协议栈向下的处理过程
当某应用试图通过TCP套接字写数据时,内核通过VFS访问套接字(如图1-2所示)。
inode代表某类型套接字的文件,包含一个套接字对象,该套接字对象指向网络协议栈的起始点(详情参考3.2节)。
该套接字对象含有一个ops指针域,该域指向相应类型套接字所要执行的一套特定操作。
对象proto_ops有一个指针,指向特定套接字所要执行的操作。
在本书中,套接字类型为INET,系统调用send最终将通过VFS调用内核中的inet_sendmsg()。
下一步将调用特定协议的发送例程,是因为INET套接字注册有多种不同的协议(参考3.1节),本书重点介绍TCP传输层协议,而inet_sendmsg()将调用该协议相关的发送操作。
特定协议的套接字由套接字对象中的sk域表示,sk是一个sock对象指针。
特定协议的操作集合由sock对象的prot域表示,prot指向proto对象。
inet_sendmsg()调用特定协议的发送例程,即tcp_sendmsg()。
图1-2TCP报文沿协议栈向下的处理过程
在tcp_sendmsg()中,用户数据被分割成一个个的TCP分段单元。
TCP分段单元将大的用户数据分成多个小块,并将每个小块复制到sk_buff中。
这些sk_buff将被复制到套接字发送缓冲区中,然后利用TCP状态机将套接字发送缓冲区中的数据传送出去。
如果TCP状态机由于某种原因不允许发送新的数据,那么该过程将返回。
在本书中,数据传输将由TCP状态机完成,11.3.11节将进行详细讨论。
如果TCP状态机能够发送sk_buff,它将一个数据段发送到IP层,以进行下一阶段的处理。
如果是TCP协议,将调用sk→tp→af_specific→queue_xmit,它指向ip_queue_xmit()。
该例程将构造一个IP头,然后让IP数据报通过防火墙策略。
如果策略允许,那么IP层将检查是否要对输出的报文使用NAT/Masquerading处理。
在进行相应处理后,最终将通过调用dev_queue_xmit()将报文传送给设备,该设备引用了一个由net_device对象表示的网络接口,Linux协议栈在此处要实施QOS处理,排队规则在设备级实现。
报文(sk_buff)在设备中根据优先级和排队规则进行排队。
下一步是根据不同优先级报文的带宽,将报文从设备队列中发送出去。
在此之前,要预先给该报文封装链路层头,然后调用特定设备的硬件传输例程发送帧。
如果无法发送该帧,那么需要在设备队列上对该报文重新进行排队,然后通过TxsoftIRQ中断来在CPU传输队列中添加设备。
稍后在处理TX中断时,从设备队列中取出相应帧并发送出去。
1.1.2沿协议栈向上的处理过程
沿协议栈向上的处理过程可以参考图1-3。
网络接口开始接收报文,一旦报文通过DMA缓冲区存储到驱动的Rx环缓冲区中,就将生成一个中断(详情参考18.5节)。
中断处理程序只是简单地将帧从环缓冲区中删除,然后到CPU输入队列中排队。
使用CPUI表示CPU处于中断状态。
很明显,每个CPU在此处都有一个输入队列,一旦报文在CPU输入队列中排队,那么CPU将产生一个RxNETsoftIRQ中断,以调用netif_rx()。
可以看出,每个CPU再次产生并处理softIRQ中断。
稍后在处理RxsoftIRQ中断时,报文从CPU接收队列中提出,并依次进行处理。
报文在到达这个目的地后才处理完成,也就是说,只有当TCP数据段在套接字接收队列排队时,才表示TCP数据报文处理完成,下面将阐述不同的协议层如何进行报文处理。
每个报文的RxsoftIRQ中断处理过程将会调用netif_receive_skb()。
首先要判定每个报文所属的网络协议族,该过程又称为报文协议交换。
如果设备的原始套接字打开,那么报文将发送到原始套接字。
一旦识别了报文所属协议族,就调用相应的协议处理例程,本书中是IP协议族。
对于IP而言,其处理过程是ip_rcv()例程。
如果需要,则ip_rcv()将尝试对报文进行反向NAT或者反向masquerade处理。
报文的路由决策也在此处完成。
如果需要在本地交付,那么将需要通过本地可接受IP报文的防火墙策略。
如果上述过程都正确,就调用ip_local_deliver_finish()来查找报文的下一个协议层。
图1-3TCP报文沿协议栈向上的处理过程
ip_local_deliver_finish()实现了INET协议交换代码。
一旦识别了INET协议,就会调用相应的处理程序来对IP数据报进行进一步的处理,该IP数据报可能属于ICMP、UDP和TCP。
本书的讨论仅限于TCP,因此,其协议处理程序是tcp_v4_rcv()。
TCP处理程序的首要工作是找出该TCP报文的套接字,该报文可能是一个新的套接字打开请求,或者是在已建立套接字上传输的报文。
因此,完成该工作需要查询各种hash表。
如果报文属于已建立的套接字,那么TCP引擎会处理TCP数据段。
如果有多个按序的TCP数据段,那么这些数据将在套接字的接收队列排队。
如果套接字中有要发送的数据,那么所接收数据的ACK应当将随这些要发送的数据一起发送出去。
最后,当应用对该TCP套接字执行读操作时,内核通过从套接字的接收队列中提取数据来处理该请求。
Linux协议栈与OSI网络参考模型的映射关系如图1-4所示。
图1-4Linux协议栈和OSI模型
1.2Linux2.4.20的源码组织结构
内核源码的树结构如图1-5所示。
图1-5内核源码树
网络代码的源代码组织结构
内核网络代码的结构如图1-6所示。
图1-6内核网络处理源码树
1.3TCP/IP协议栈和内核控制路径
本节将介绍Linux内核如何处理TCP数据。
总体上来说,内核在处理报文中存在多条不同的内核控制路径和处理器上下文。
通过TCP套接字写数据要发起写/发送系统调用(如图1-7所示)。
系统调用使进程由用户区进入内核,内核代表进程执行一些操作,如图中的实灰线所示。
下面介绍不同的内核控制路径,它们的不同之处在于发送TCP数据的内核线程跳出点不同。
图1-7不同内核控制路径下的报文传输
内核控制路径1在该内核控制路径中,内核线程根据TCP/IP协议栈处理TCP数据成功,并在通过物理接口发送数据后返回。
内核控制路径2该内核控制路径根据TCP/IP协议栈处理数据时,由于无法取得设备锁,因而传输数据失败。
在这种情况下,内核线程在产生TxsoftIRQ中断后返回。
softIRQ中断将延迟到稍后传输设备排队数据时处理,softIRQ处理详情参考17.1节。
内核控制路径3该内核控制路径在进行TCP层的处理时,由于QOS策略不允许传输数据,因而无法进行下一步的处理。
发生这种情况,可能是因为其他人在处理该报文缓冲区队列,也可能是队列限额已满。
在后一种情况下,将产生一个定时器,以便于在以后处理该队列。
内核控制路径4该内核控制路径在处理数据通过TCP层,但无法进一步处理并从该处返回。
原因可能是TCP状态机或者拥塞算法不允许数据的进一步传输。
这些数据将在生成某TCP事件后,由TCP状态机继续进行处理。
内核控制路径5该内核控制路径可能在中断上下文或者内核上下文中执行操作。
内核上下文可能来自于softIRQ守护程序,该程序是内核的一个线程,且没有用户上下文。
内核上下文可能也来自于某内核线程,其相对应的用户进程会在CPU中产生softIRQ中断,调用spin_unlock_bh()。
更多详情参考17.6节。
该内核控制路径处理所有控制路径2产生的排队数据。
内核控制路径6该内核控制路径执行softIRQ中断中优先级高的任务。
该路径也可能在中断上下文或者内核上下文中执行。
该路径处理由控制路径3产生的排队数据。
内核控制路径7该内核控制路径在处理TCP传入报文时,执行类似于softIRQ的操作。
当接收到某报文时,由RxsoftIRQ进行处理。
当由softIRQ处理某TCP报文时,可能生成一个事件以处理发送队列中未处理的数据传输。
该内核控制路径传输由控制路径4产生的排队数据。
在接收方,报文分两步处理(如图1-8所示)。
中断处理过程接收来自DMA环缓冲区的报文,然后将其送到CPU专用输入队列排队,并产生RxsoftIRQ中断,RxsoftIRQ在稍后时间点由softIRQ守护程序或者中断上下文中进行处理。
直到TCP数据报文在套接字的接收队列中排队,或者由应用接收完毕后,才表示其由RxsoftIRQ完全处理完毕。
TCPACK报文由TCP状态机进行处理,softIRQ只有在处理由传入ACK生成的事件后才返回。
图1-8报文接收处理以及不同的内核控制路径
1.4版本2.4之后Linux内核的可抢占性
首先定义可抢占(preemptive)的含义,然后阐述其对Linux内核的影响。
一般来说,可抢占一般是指在特定条件下,当前的执行上下文可以让步给其他的CPU执行上下文。
其最成功的应用是多任务操作系统。
在多任务操作系统中,很多用户进程同时在一个CPU上运行,这些进程有一定的CPU配额,它们将占有CPU,直到耗尽其配额。
一旦当前运行进程的配额耗尽,其他可运行进程就会占有CPU,即使该进程在内核调度器看来仍然未执行完。
因此,这种情况可以称为进程是可抢占的。
可以肯定的一点是,用户进程的可抢占性,为其他进程提供了在CPU上运行的机会。
本书并不讨论实时进程的调度,仅讨论采用分片调度策略的常规优先级进程。
采用这种方法,内核可以抢占用户区进程。
本节将阐述的内容与之前差别较大。
内核如何可以被抢占?
假定CPU在执行某内核控制路径,由于错误而进入无限循环,那么内核能否被自身所取代,进而脱离无限循环,将CPU交给其他可运行进程。
(说明:
我曾经试验过内核的无限循环,仅是为了解释可抢占的含义,但是在此处的目的则不同。
一般来说,内核代码不会以这种状态结束)。
内核控制路径通过调用调度器将CPU交给其他可执行进程。
首先必须了解什么事件会导致CPU使用权的转移,这通过CPU的定时器中断来实现,该定时器中断在有限的时间间隔内产生,并且是不可屏蔽的。
该中断执行所有必要的计算,以判定当前执行上下文的持续时间。
如果该执行上下文的限额超时,那么它将为该进程设置一个schedulingneeded标志,当从中断返回时,仅在用户模式中断时才检查该标志(从本质上来说,这意味着当定时器中断发生时,CPU在执行用户区代码)。
arch/i386/kernel/entry.S
256ENTRY(ret_from_intr)
257GET_CURRENT(%ebx)
258ret_from_exception:
259movlEFLAGS(%esp),%eax#mixEFLAGSandCS
260movbCS(%esp),%al
261testl$(VM_MASK|3),%eax#returntoVM86modeornon-supervisor?
262jneret_from_sys_call
263jmprestore_all
264
265ALIGN
266reschedule:
267callSYMBOL_NAME(schedule)#test
268jmpret_from_sys_call
源码1-1中断返回处理
在源码1-1中,当从中断返回时,控制将移交给第256行的汇编代码。
第257行首先得到指向ebx%中当前进程(与用户区进程相对应的内核线程)的指针。
第259行从栈指针(%esp)得到当前进程的EFLAGS标志,并保存到eax%中。
第260行从栈指针中得到一个代码段字节,并保存到eax%中。
第261行在CPU中断时,检查当前执行模式是用户模式还是内核模式,可以通过第260行复制到eax%中的代码段来验证。
如果CPU处于内核模式,那么将跳转到第263行的restore_all。
restore_all将通过载入保存在栈中的寄存器值来切换到内核执行上下文,然后开始从中断处执行。
如果在用户模式产生了中断,那么控制将移交给ret_from_sys_call。
ret_from_sys_call执行很多检查。
例如,如果当前进程有一个未处理的信号,就需要重新调度等,然后采取适当的动作。
如果当前进程并没有用完其时间片,那么它将在用户模式继续执行;否则,其他可运行进程将得到CPU的控制权。
如图1-9(a)所示,处理中断要切换到内核模式。
前面讲述了定时器中断,不过其他中断也可能导致当前用户进程将CPU控制权移交给其他进程。
例如,网络中断将会唤醒某些等待数据连接的进程。
I/O密集型进程通常比CPU密集型进程有更高的优先级,因此,传送数据的网络中断可能会导致当前进程将CPU让给等待处理该连接I/O的进程。
在有些情况中,当前进程可能并没有消耗完它的时间片。
不过,只要没有接收到结束该进程的信号,那么该进程将继续在CPU上运行。
图1-9(a)用户模式下的中断处理过程
如图1-9(b)所示,CPU处于内核模式执行。
当在这时发生定时器中断,控制将移交给处理该中断的内核路径,这可以使内核能够在返回用户空间之前,完成其所要执行的任务。
该设计能够保证一点:
除非内核通过调用schedule()交出CPU控制权,否则CPU将继续运行在内核模式下。
没有什么能够强迫内核交出CPU,即使是中断和异常。
这么做的目的非常简单:
保证数据一致性,不过这种做法导致Linux内核是不可抢占的。
例如,如果某驱动存在漏洞,使内核执行处于无限循环当中,将会导致整个CPU系统始终处于冻结状态。
图1-9(b)内核模式下的中断处理过程
简而言之,Linux内核2.4及其较低版本并没考虑实时性的需要,由于内核的不可抢占性,其所带来的延时可能非常大。
Linux2.6内核已经开始考虑该问题,尝试使其向前可抢占,但不是很完善,我们将在本书的修正版中阐述该问题。
1.4.1Linux系统调用
本节将阐述运行在IntelX86体系结构上的Linux系统调用的实现。
任何Unix系统都至少实现一个系统调用,以保证用户级应用程序能够请求内核服务。
下面讨论一个比较简单的系统调用open。
当某应用试图打开一个文件来读写时,其首要的一步是发出一个系统调用open。
在Unix系统中,如常规文件、管道、FIFO、套接字、设备等都被当做特殊文件处理,并使用系统调用open来进一步处理I/O。
为什么要用内核服务来打开文件呢?
原因在于文件系统相关的信息是由内核维护的。
文件系统相关的数据结构由内核维护,并且只有在处理器特权级时才能访问,这是为了保证一致性和执行的不可中断性。
在维护数据一致性方面,内核在编程实现时必须考虑各种可能的因素,执行代码必须阻塞那些可屏蔽的中断。
同样,内核是不可抢占的。
因此,即使内核被高优先级的中断打断,也可以在处理器返回时,保证控制返回到内核离开的那一点继续开始执行。
内核控制路径可以交出CPU,但没有进程可以抢占内核进程。
对于文件系统而言,驻留在内核内部的最主要的原因是文件系统并不是一个独立的子系统。
文件系统必须与其他子系统进行交互,如虚拟内存、网络、设备控制器、分页、调度,由于前述原因,所有这些子系统都不能在用户区运行。
因此,系统调用往往在内核内执行(参考图1-10),处理器需要从用户模式切换到特权模式,以访问内核代码和数据结构。
这可以通过软中断0x80来实现,该软中断是一个开放的库例程。
系统调用号载入到eax中,参数载入到ebx、ecx、edx、寄存器中。
处理器通过加载ss和eps寄存器来决定进程的内核栈。
处理器控制单元将用户上下文保存在栈中,一旦上述准备工作完成,控制将移交给系统调用处理程序。
图1-10Linux上的系统调用实现
系统调用处理程序查找系统调用表sys_call_table,通过该表,可以基于系统调用号来检索系统调用处理过程向量,然后控制移交给特定的系统调用处理过程,并在系统调用处理完成之后,返回值存储在eax中。
1.4.2添加新的系统调用
下面介绍如何在系统中添加新的系统调用。
首先需要为该系统调用申请一个新的号码,然后向系统注册特定的系统调用处理过程。
文件include/asm-i386/unistd.h中的宏__NR_sys定义了系统调用号,其中sys表示系统调用的名称(如图1-11所示)。
定义新的系统调用需要在该文件中添加一行。
include/asm-i386/unistd.h
8#define__NR_exit1
9#define__NR_fork2
10#define__NR_read3
11#define__NR_write4
12#define__NR_open5
13#define__NR_close6
14#define__NR_waitpid7
15#define__NR_creat8
......
图1-11系统调用相关号码
下一步是在内核源代码树的合适地方编写系统调用例程,例如,如果系统调用与调度相关,就应当在kernel/sys.c中添加。
按照惯例,该例程的名称应当以sys_开头。
一旦分配了系统调用号,并且将系统调用例程加入内核源代码中,则需要使用宏SYMBOL_NAME()在系统调用表中添加系统调用例程,应该在文件arch/i386/kernel/entry.S(如图1-12所示)中添加一行,为新系统调用所添加的一行,在sys_call_table中的行号应当与其系统调用号精确匹配。
因此,在为新的系统调用分配系统调用号时,该号码最好就是下一个可用的号码,而该系统调用的入口应当在sys_call_table表的结尾。
最后,重新编译内核,并将新生成的内核放到合适的位置。
arch/i386/kernel/entry.S
.data
ENTRY(sys_call_table)
.longSYMBOL_NAME(sys_ni_syscall)/*0-old"setup()"systemca
.longSYMBOL_NAME(sys_exit)
.longSYMBOL_NAME(sys_fork)
.longSYMBOL_NAME(sys_read)
.longSYMBOL_NAME(sys_write)
.longSYMBOL_NAME(sys_open)/*5*/
......
图1-12内核中的系统调用表
如何在应用程序中访问新的系统调用?
可以使用syscall()或者syscall*()来调用新的系统调用。
使用syscall()需要传递与所注册系统调用相对应的系统调用号。
如果使用syscall()接口,那么不能传递任何参数给系统调用。
如果系统调用有一个参数,则可以使用syscall1();如果有两个参数,则可以使用syscall2()等,使用这些接口可以传递4个参数。
syscall1的实现如图1-13所示。
它是一个在/usr/include/asm/unistd.h中实现为宏,它可以接收一个参数arg1。
该宏采用inline汇编代码实现,在第293行生成一个软中断0x80。
第294行表明结果需要存储在eax%中。
这里有两个输入:
eax%包含系统调用号,即第294行的__NR_##name;ebx%包含该系统调用的第一个参数值。
/usr/include/asm/unistd.h
289#define_syscall1(type,name,typel,argl)\
290typename(typelargl)\
291{\
292long__res;\
293__asm__volatile("int$0x80”\
294:
"=a"(__res)\
295:
"0"(__NR_##name),"b"((long)(argl)));\
296__syscall_return(type,__res);\
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- TCPIP 架构 设计 应用 Linux