首页 > 代码库 > 《TCP/IP详解卷2:实现》笔记--插口I/O

《TCP/IP详解卷2:实现》笔记--插口I/O

本文介绍有关网络连接上读写数据的系统调用,分三部分:

第一部分介绍四个用来发送数据的系统调用:write,writev,sendto和sendmsg。第二部分介绍四个用来接收数据的系统

调用:read、readv、recvfrom和recvmsg。第三部分介绍select系统调用,select调用的作用是监视通用描述符和特殊描述

符的状态。

插口层的核心是两个函数:sosend和soreceive。这两个函数负责处理所有插口层和协议层之间的I/O操作。


1.插口缓存

每一个插口都有一个发送缓存和一个接受缓存。缓存的类型为sockbuf。下图列出了sockbuf结构的定义。


其中sb_hiwat和sb_lowat用来调整插口的流控算法。本文的以后部分会进行说明。下图显示了Internet协议的默认设置。


因为每一个进入的UDP报文的源地址同数据一起排队,所以UDP协议的sb_hiwat的默认值设置为能容纳40个1K字节长

的数据包和相应sockaddr_in结构(每个16字节)。

sb_sel是一个用来实现select系统调用的selinfo结构。

下图列出了sb_flags的所有可能的值。


sb_timeo用来限制一个进程在读写调用中被阻塞的时间,单位为时钟滴答。默认为0,表示进程无限期的等待。SO_SNDTIMEO

和SO_RCVTIMEO插口选项可以改变或读取sb_timeo的值。


2.write、writev、sendto和sendmsg系统调用

所有这些写系统调用都要直接或间接地调用sosend。sosend的功能是将进程来的数据复制到内核,并将数据传递给与插口

相关的协议。下图给出了sosend的工作流程。



write和writev系统调用适用于任何描述符,而其他的系统调用值适用于插口描述符;writev和sendmsg系统调用可以接受

从多个缓存中来的数据。从多个缓存中写数据称为收集,同它相对应的读操作成为分散。执行收集操作时,内核按序接收

类型为iovec的数组中指定的缓存中的数据。数组最多有UIO_MAXIOV个单元。下图显示了类型iovec的结构。


iov_base指向长度为iov_len个字节的缓存的开始。

如果没有这种接口,一个进程将不得不将多个缓存复制到一个大的缓存中,或调用多个写系统调用来发送从多个缓存来的

数据。下图说明了iovec结构在writev系统调用中的应用,图中,iovp指向数组的第一个元素,iocnt等于数组的大小。


数据报协议要求每一个写调用必须指定一个目的地址。因为write、writev和send调用接口不支持对目的地址的指定,因此

这些调用只能在调用connect将目的地址同一个无连接的插口联系起来后才被调用。调用sendto或sendmsg时必须提供目的

地址,或在调用他们之前调用connect来指定目的地址。

下图显示了sendmsg系统调用接收一个可选的控制标志。


只有sendmsg系统调用支持控制信息,控制信息和另外几个参数是通过结构msghdr一次传递给sendmsg,而不是分别传递。


控制信息的类型为cmsghdr结构。


下图说明了调用sendmsg时msghdr的结构。



3.sendmsg系统调用

sendmsg和sendit函数准备sosend系统调用所需的数据结构,然后sosend调用将报文发送给相应的协议。对SOCK_DGRAM

协议而言,报文就是数据报,对SOCK_STREAM协议而言,报文是一串字节流。对于SOCK_SEQPACKET协议而言,报文

可能是一个完整的记录或一个大的记录的一部分。sendmsg系统调用的声明如下:


sendmsg函数的大概处理流程如下:

1.将iovec数组从用户空间复制到栈中的数组或一个更大的动态分配的数组中。

2.调用sendit函数。


4.sendit函数

sendit初始化一个uio结构,将控制和地址信息从进程空间复制到内核。首先必须先介绍下uio结构。uio结构中包含了iovec

结构数组和其他一些信息。

sendit函数的大概处理流程如下:

1.初始化uio结构。将进程指定的输出缓存中的数据收集到内核缓存中。

2.从进程复制地址和控制信息。

3.发送数据和和清除缓存。将插口,目的地址,uio结构,控制信息和标志全部传给函数sosend。在返回之前,sendit释放

包含目的地址的缓存。sosend负责释放控制信息缓存。


5.sosned函数

sosend是插口层中最复杂的函数之一。sosend的功能是:根据插口指明的协议支持的语义和缓存的限制,将数据和控制

信息传递给插口指明的协议的pr_usrreq函数。sosend从不将数据放在发送缓存中,存储和移走数据应由协议来完成。

可靠的协议缓存

对于提供可靠的数据传送协议,发送缓存保存了还没有发送的数据和已经发送但还没有被确认的数据。sb_cc等于发送缓存

的数据的字节数,且0<=sb_cc<=sb_hiwat。如果有带外数据发送,则sb_cc有可能暂时超过sb_hiwat。

sosend应该确保在通过pr_usrreq函数将数据传递给协议层之前有足够的发送缓存。协议层将数据放到发送缓存中,sosend

通过下面两种方式之一将数据传送给协议层:

1.如果设置了PR_ATOMIC(protosw结构中的pr_flags),sosend就必须保护进程和协议层之间的边界。sosend等待得到足够

的缓存来存储整个报文,当获取到足够的缓存后,构造存储整个报文的mbuf链,并用pr_usrreq函数一次性传送给协议层。

RDP和SPP就是这种类型的协议。

2.如果没有设置PR_ATOMIC,sosend每次传送一个存有报文的mbuf,可能传送部分mbuf给协议层以防止超过上限,在

SOCK_STREAM类协议如TCP中和SOCK_SEQPACKET类协议如TP4中被采用。

当一个报文因为太大而没有足够的缓存时,协议允许报文被分成多段,但sosend仍然不将数据传送给协议层直到缓存中的

闲置空间大小大于sb_lowat。

不可靠的协议缓存

对于提供不可靠的数据传输的协议(UDP)而言,发送缓存不需要保存任何数据,也不等待任何确认。每一个报文一旦被

排队等待发送到相应的网络设备,插口层立即将它送到协议。这种情况下,sb_cc总是等于0,sb_hiwat指定每一次写的

最大长度,间接指明数据报的最大长度。

sosend函数的详细情况将分四个部分来描述。

  • 初始化
  • 差错和资源检查
  • 数据传送
  • 协议处理
大概的处理流程如下:
1.计算传送数据字节数。
2.关闭路由。如果仅仅要求对这么报文不通过路由表进行路由选择,则设置不要进行路由。
3.差错检查。在一下几种情况下返回错误:
  • 插口输出被禁止。
  • 插口正处于差错状态。
  • 协议请求连接且连接还没有建立或连接请求还没有启动
  • 在无连接协议中没有指定目的地址。
4.计算可用空间。计算发送缓存中剩余的闲置空间字节数,目的是防止太多的小报文消耗太多的mbuf缓存。通过放宽缓存
限制到1024个字节来给予带外数据更高的优先级。
5.强制实施报文大小限制。如果atomic被置位,并且报文大于高水位标记(sb_hiwat),则返回错误。报文因为太大而不能
被协议接收,即使缓存是空的。如果控制信息的长度大于高水位标记,同样返回错误。
6.是否等待更多的空间。如果发送缓存中的空间不够,数据来源于进程,并且下面条件之一成立,则sosend必须等待更多空间:
  • 报文必须一次传送给协议。
  • 报文可以分段传送,但闲置空间大小低于低水位标记。
  • 报文可以分段传送,但可用空间存放不小控制信息。
当数据通过top(指向mbuf数据链)传送给sosend时,数据已经在mbuf缓存中。因此,sosend忽略缓存高、低水位标记
限制,因为不需要附加的缓存来保存数据。
如果sosend必须等待缓存且插口是非阻塞的,则返回错误。否则,缓存锁被释放,sosend调用sbwait等待,直到缓存状
态发生变化,当sbwait返回后,sosend重新使能协议处理,并且重新获取缓存锁,检查差错和缓存空间。
7.分配分组首部或标准mbuf。当atomic被置位时,在第一次分配一个分组首部,随后分配标准的mbuf。如果atomic没有被
置位,则总是分配一个分组首部。
8.尽可能用簇。如果不用簇,存储在mbuf中的字节数受到下面三个量中最小的一个量的限制:

  • mbuf中的可用空间。
  • 报文的字节数。
  • 缓存的空间。
9.从进程复制数据。从进程复制字节到mbuf。传送完毕后,更新mbuf的长度,前面的mbuf连接到新的mbuf,更新mbuf链
的长度。
10.是否写另一个缓存。当atomic没有被设置时,一次只传送一个mbuf给协议。如果设置了atomic,只有当足够的缓存空间
来存放整个报文时才进行缓存的写入。
11.传输数据和控制mbuf给插口指定的协议。如果进程传送的是带外数据,则发送PRU_SENDOOB请求;否则,它发送
PRU_SEND请求,同时将地址和控制mbuf传给协议。


6.read、readv、recvfrom和recvmsg系统调用

我们将这些系统调用成为读系统调用,从网络连接上接收数据,同recvmsg相比,前三个系统调用比较简单。下图给出了

这四个系统调用和一个库函数recv的特点。


只有read和readv系统调用适用于各类描述符,其他的调用只适用于插口描述符。同写调用一样,通过iovec结构数组来

指定多个缓存。对数据报协议,recvfrom和recvmsg返回每一个收到的数据报的源地址。对于面向连接的协议,getpeername

返回连接对方的地址。

下图说明读系统调用的流程。



7.recvmsg系统调用

recvmsg函数是最通用的读系统调用。函数的大概处理流程如下:

1.复制iov数组。同sendmsg一样,recvmsg将msghdr结构复制到内核,如果自动分配的数组aiov太小,则分配一个更大的

iovec数组,并且将数组单元从进程复制到iov指向的内核数组。

2.recvit和释放缓存。recvit收完数据后,将更新过的缓存长度和标志的msghdr结构再复制到进程。如果分配了一个更大的iovec

结构,则返回之前释放它。


8.recvit函数

recvit函数被recv、recvfrom和recvmsg调用,基于recv xxx调用提供的msghdr结构,recvit函数为soreceive的处理准备了一个

uio结构。

recvit函数的大概处理流程如下:

1.初始化uio结构,该结构描述从内核到进程之间的一次数据传送。

2.调用soreceive函数。

3.将地址和控制信息复制到进程。如果进程传入了一个存放地址或控制信息或两者都有的缓存,则recvit将结果写入该缓存,

并且根据soreceive返回的结果调整它们的长度。如果缓存太小,则地址信息可能被截掉。

4.释放存储源地址和控制信息的mbuf缓存。


9.soreceive函数

soreceive函数将数据从插口的接收缓存传送到进程指定的缓存。某些协议还提供发送者的地址,地址可以同可能的附加控制

信息一起返回。

recvmsg是唯一返回标志字段给进程的读系统调用。在其他的系统调用中,控制返回给进程之前,这些信息被内核丢弃,下图

列出了msghdr中recvmsg能设置的标志。


9.1.带外数据

带外数据(OOB)在不同的协议中有不同的含义。一般来说,协议利用已经建立的通信连接来发送OOB数据。OOB数据可能

与发送的正常数据同序。插口层支持两种与协议无关的机制来实现对OOB数据的处理:标记和同步。本文讨论插口层实现的

抽象的OOB机制。UDP不支持OOB数据。TCP的紧急数据机制与插口层的OOB数据之间有关系。

发送进程通过sendxxx调用设置MSG_OOB标志将数据标记为OOB数据。sosend将这个消息传递给插口协议,插口层收到这

个消息后,对数据进行特殊处理,如加快发送数据或使用另一种排队策略。

当一个协议收到OOB数据后,并不将它放进插口的接收缓存而是放到其他地方。进程通过设置recvxxx调用中的MSG_OOB标志

来接收到达的OOB数据。另一种方法是,通过设置SO_OOBINLINE插口选项,接收进程可以要求协议将OOB数据放到正常的

数据之内。当SO_OOBINLINE被设置时,协议将收到OOB数据放到正常数据的接收缓存,在这种情况下,MSG_OOB不用来

接收OOB数据,读调用要么返回所有的正常数据,要么返回所有的OOB数据。两种类型的数据从来不会再一个输入调用的输入

缓存中混淆。进程使用recvmsg来接收数据时,可以通过检查MSG_OOB标志来决定返回的数据是正常数据还是OOB数据。


9.2.接收缓存的组织:报文边界

对于支持报文边界的协议,每一个报文存放在一个mbuf链中。下图说明了由三个mbuf组成的UDP接收缓存的结构。



9.3.接收缓存的组织:没有报文边界

当协议不需要维护报文边界(及SOCK_STREAM协议,如TCP)时,数据被加到缓存中的最后一个mbuf链的尾部。如果进入

的数据长度大于缓存的长度,则数据将被截掉。下图说明了仅仅包含正常数据的TCP接收缓存的结构。



9.4.控制信息和带外数据

不像TCP,一些流协议支持控制信息,并且将控制信息的相关数据作为一个新的mbuf链加入接收缓存,如果协议支持内含

OOB数据,则插入一个新的mbuf链到任何包含OOB数据的mbuf之后,但在任何包含正常数据的mbuf之前,这一点确保进入

的OOB数据总是排在正常数据之前。下图说明了包含控制信息和OOB数据的接受缓存的结构。



10.soreceive函数代码

在接收数据时,soreceive必须检查报文边界,处理地址和控制信息以及读标志所指定的任何特殊操作。一般来说,soreceive

的一次调用只处理一个记录,并且尽可能返回要求读的字节数。函数的大概处理流程如下:

1.接收OOB数据,因为OOB数据不存放在接收缓存中,所以soreceive为其分配一块标准的mbuf,并给协议发送PRU_RCVOOB

请求。while循环将协议返回的数据复制到指定缓存中。

2.如果需要,等待数据。soreceive要检查几种情况,如果需要,它可能要等待接收更多的数据才能往下执行。如果soreceive

在这里进入睡眠状态,则它醒来后会查看是否有足够的数据到达。这个过程一直继续,直到收到足够的数据为止。

3.处理地址和控制信息。在传输之前,首先处理地址信息和控制信息。

4.建立数据传送。因为只有OOB数据或正常数据是在一次soreceive调用中传送,所以必须记住队列前段的数据类型,这样在类型

改变时,soreceive能够停止传送。

5.传送数据循环。只要缓存中还有mbuf,请求的数据还没有传送完毕,且没有差错出现,循环就不会退出。

6.退出处理。主要是更新指针、标志和偏移;释放插口缓存锁;使能协议处理并返回。


11.selecct系统调用

下图列出了select能够监控的插口状态。

select函数的大概处理是:扫描进程指示的文件描述符,当一个或多个描述符处于就绪状态或定时器超时或信号出现时返回。


11.1.selscan函数

select函数的核心是selscan函数。对于任意一个描述符集合中设置的每一个比特,selscan找出同它相关联的描述符,并且

将控制分散给与描述符相关联的so_select函数。对于插口而言,就是soo_select函数。


11.2.soo_select函数

对于selscan在输入描述符集合中发现的每一个状态就绪的描述符,selscan调用与描述符相关的fileops结构中的fo_select指针

引用的函数。函数判断插口的可读、可写或例外情况,调用selrecord函数。


11.3.selrecord函数

该函数记录了足够的信息,使得缓存内容发生变化时协议处理层能够唤醒进程。


11.4.selwakeup函数

当协议处理改变插口缓存的状态,并且只有一个进程选择了该缓存时,Net/3就能根据selrecord中记录的信息立即将该进程

放入运行队列。

每一个调用select的进程在调用tsleep时使用selwait作为等待通道。这意味着对应的wakeup将唤醒所有阻塞在select上的

进程。

下图说明如何调用selwakeup。

当改变插口状态的事件出现时,协议处理层负责调用上图底部列出的函数来通知插口层。这些函数都导致selwakeup被调用,

在插口上选择的任何进程被调度运行。

《TCP/IP详解卷2:实现》笔记--插口I/O