概说《TCP/IP详解 卷2》第16章 插口I/O(一)

260 阅读24分钟

原文链接:概说《TCP/IP详解 卷2》第16章 插口I/O

本文要点

  • 引言

  • 插口缓存

  • write/send相关系统调用

  • sendmsg系统调用

  • sendit函数

  • sosend函数

  • 可靠的协议缓存

  • 不可靠的协议缓存

  • sosend函数小结

  • read/recv相关系统调用

  • recvmsg系统调用

  • recvit函数

  • soreceive函数

  • 外带数据

  • 举例

  • 其它接收操作选项

  • 接收缓存组织:报文边界

  • 接收缓存组织:没有报文边界

  • 控制信息和外带数据

  • soreceive代码

  • select系统调用

  • 小结

引言

本文讨论有关从网络连接上读写数据的系统调用,主要分三个部分:

第一部分介绍四个用来发送数据的系统调用:write、writev、sendto和sendmsg。第二部分介绍四个用来接收数据的系统调用:read、readv、recvfrom和recvmsg。第三部分介绍select系统调用,select调用的作用是监控通用描述符和特殊描述符(插口)的状态。

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

插缓存概说《TCP/IP详解 卷2》第15章 插口层图2socket的定义中,我们已经知道,每个插口都有一个发送缓存和一个接收缓存。缓存的类型为sockbuf,其结构定义如图1所示。

图1 sockbuf结构

72~78 每一个缓存均包含控制信息和指向存储数据的mbuf链的指针。sb_mb指向mbuf链的第一个mbuf,sb_cc的值等于存储在mbuf链中的数据字节数。sb_hiwat和sb_lowat用来调用插口的流控算法。sb_mbcnt等于分配给缓存中的所有mubf的数量。

每一个mbuf可存储0~2048个字节的数据(如果使用了外部簇)。sb_mbmax是分配给插口mbuf缓存的数量的上限。默认的上限在socket系统调用中发送PRU_ATTACH请求时由协议设置。只要内核要求的每个插口缓存的大小不超过262144个字节的限制(sb_max),进程就可以修改缓存的上限和下限。图2显示了Internet协议的默认设置。

图2 Internet协议的默认的插口缓存限制

因为每一个进入的UDP报文的源地址同数据一起排队,所以UDP协议的sb_hiwat的默认设置为能容纳40个1k字节长的数据报和相应的sockaddr_in结构(每个16字节)。

79 sb_sel是一个用来实现select系统调用的selinfo结构(图56)。

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

图3 sb_flags的值

81~82 sb_timeo用来限制一个进程在读写调用中被阻塞的时间,单位为时钟滴答。默认为0,进程无限期等待。SO_SNDTIMEO和SO_RCVTIMEO插口选项可以改变或读取so_timeo的值。

有许多宏和函数用来管理插口的发送和接收缓存。图4中列出了与缓存加锁和同步有关的宏和函数。

图4 与缓存加锁和同步有关的宏和函数

图5和图6显示了设置插口资源限制、往缓存中写数据和从缓存中删除数据的宏和函数。在该表中,m、m0、n和control都是指向mbuf链的指针。sb指向插口的发送或者接收缓存。

图5 与插口缓存分配与操作有关的宏和函数

图6 与插口缓存分配与操作有关的宏和函数

write、writev、sendto和sendmsg系统调用

我们将write、writev、sendto和sendmsg四个系统调用统称为写系统调用,它们的作用是往网络连接上发送数据。相对于最一般的调用sendmsg而言,前三个系统调用是比较简单的接口。

所有的写系统调用都要直接或间接地调用sosend。sosend的功能是将进程来的数据复制到内核,并将数据传递给插口相关的协议。图7给出了sosend的工作流程。

图7 所有的插口输出均由sosend处理

本文将讨论图7中三个阴影的函数,从上至下依次是sendmsg、sendit和sosend。

图8说明了这四个库系统调用和一个相关的库函数(send)的特点。

图8 写系统调用

从图8中的第二栏可以看出,write和writev系统调用适用于任何描述符,而其它的系统调用只适用于插口描述符。

从图8的第二栏可以看出,writev和sendmsg系统调用可以接收从多个缓存中来的数据。向多个缓存中写数据称为收集(gathering),同它相应的读操作称为分散(scattering)。执行收集操作时,内核按序接收类型为iovec的数组中指定的缓存中的数据。数组最多有UIO_MAXIOV个单元。图9显示了类型iovec的结构。

图9 iovec的结构

41~44 在图9中,iov_base指向长度为iov_len个字节的缓存的开始。

如果没有这种接口,一个进程将不得不将多个缓存复制到一个大的缓存中,或者调用多个写系统调用来发送多个缓存来的数据。相对于用一个系统调用传送类型为iovec的数组,这两种方法的效率更低。对于数据报协议而言,调用一次writev就是发送一个数据报,数据报的发送不能用多个写动作来实现。

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

图10 writev系统调用中的iovec参数

数据报协议要求每一个写调用必须指定一个目的地址。因为write、writev和send调用接口不支持对目的地址的指定,因此这些调用只能在调用connect将目的地址同一个无连接的插口联系起来后才能被调用。调用sendto或sendmsg时必须提供目的地址,或者在调用它们之前调用connect来指定目的地址。

图8中的第五栏显示send_xxx系统调用接收一个可选的控制标志,这些标志在图11中定义。

图11 send_xxx系统调用:flags值

如图8最后一栏所示,只有sendmsg系统调用支持控制信息。控制信息和另外几个参数是通过结构msghdr(图12)一次传递给sendmsg,而不是分别传递。

图12 msghdr结构

msg_name应该被说明成一个指向socdaddr结构的指针,因为它包含网络地址。

228~236 msghdr结构包含一个目的地址(msg_name和msg_namelen)、一个分散/收集数组(msg_iov和msg_iovlen)、控制信息(msg_control和msg_controllen)和接收标志(msg_flags)。控制信息的类型为cmsghdr结构,如图13所示。

图13 cmsghdr结构

251~256 插口层并不解释控制信息,但是报文的类型被置为cmsg_type,且报文长度为cmsg_len。多个控制报文可能出现在控制信息缓存中。

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

图14 sendmsg系统调用的msghdr结构

sendmsg系统调用

只有通过sendmsg系统调用才能访问到与插口API的输出有关的所有功能。sendmsg和sendit函数准备sosend所需的数据结构,然后由sosend将报文发送给相应的协议。对SOCK_DGRAM协议而言,报文就是数据报。对于SOCK_STREAM协议而言,报文是一串字节流。对于SOCK_SEQPACKET协议而言,报文可能是一个完整的记录(隐含的记录边界)或者一个大的记录的一部分(显示的记录边界)。对于SOCK_PDM协议而言,报文总是一个完整的记录。

图15显示了sendmsg系统调用的代码。

图15 sendmsg系统调用

307~319 sendmsg的参数uap包含:插口描述符;指向msghdr结构的指针;几个控制标志。函数copyin将msghdr结构从用户空间复制到内核。

a. 复制iov数组

320~334 一个有8个元素(UIO_SMALLIOV)的iovec数组从栈中自动分配。如果分配的数组不够大,sendmsg将调用MALLOC分配更大的数组。如果进程指定的数组单元大于1024(UIO_MAXIOV),则返回EMSGSIZE。copyin将iovec数组从用户空间复制到栈中的数组或一个更大的动态分配的数组中。

b. sendit和cleanup

335~340 如果sendit返回,则表明数据已经发送给相应的协议或者出现差错。sendmsg释放iovec数组,并且返回sendit调用返回的结构。

sendit函数

sendit函数是被sendto和sendmsg调用的公共函数。sendit初始化一个uio结构,将控制和地址信息从进程空间复制到内核。在讨论sosend之前,我们必须先了解uiomove函数和uio结构。

uiomove函数的原型为:

int uiomove(caddr_t cp, int n, struct uio *uio);

uiomove函数的功能是在由cp指向的缓存与uio结构中指向的类型为iovec的数组中的多个缓存之间传送n个字节。图16说明了uio结构的定义,该结构控制和记录uiomove的行为。

图16 uio结构

45~61 在uio结构中,uio_iov指向类型为iovec结构的数组,uio_offset记录uiomove传送的字节数,uio_resid记录剩余的字节数。每次调用uiomove,uio_offset增加n,uio_resid减去n。同时,uiomove根据传送的字节数调用uio_iov数组中的基指针和缓存长度,从而从缓存中删除每次调用的传送的字节。最后,每当从uio_iov中传送一块缓存,uio_iov数组的每个单元就向前进一个数组单元。uio_seqflg指向uio_iov数组的基指针指向的缓存的位置。uio_rw指定数据传送的方向。缓存可能在用户数据空间,用户指令空间或者内核数据空间。图17对uiomove函数的操作进行了小结。图中对操作的描述用到了uiomove函数原型中的参数名。

图17 uiomove操作

图18显示了一个调用uiomove之前的uio结构。

图18 调用uiomove前的uio结构

uio_iov指向iovec数组的第一个单元。iov_base指针数组的每一个单元分别指向它们在进程地址空间中的缓存的起始地址。uio_offset等于0,uio_resid等于三块缓存的总的大小。cp指向内核中的一块缓存,一般来说,这块缓存是一个mbuf的数据区。图19显示了调用uiomove之后同一个uio结构的内容。

图19 调用uiomove后的uio结构

在上述调用中,n包含第一块缓存中的所有字节和第二块缓存中的部分字节,即n0<n<n0+n1。

调用uiomove后,第一块缓存的长度变为0,且它的基指针指向缓存的末端。uio_iov现在指向iovec数组的下一个单元。单元指针也前进了一个单元,长度也减少了,减少的字节数等级缓存中被传送的字节数。同时,uio_offset增加了n,uio_resid减少了n。数据已经从进程中传送到内核缓存,因为uio_rw等于UIO_WRITE。

在上述调用中,n包含第一块缓存中的所有字节和第二块缓存中的部分字节,即n0<n<n0+n1。

调用uiomove后,第一块缓存的长度变为0,且它的基指针指向缓存的末端。uio_iov现在指向iovec数组的下一个单元。单元指针也前进了一个单元,长度也减少了,减少的字节数等级缓存中被传送的字节数。同时,uio_offset增加了n,uio_resid减少了n。数据已经从进程中传送到内核缓存,因为uio_rw等于UIO_WRITE。

现在开始讨论sendit的代码,如图20所示。

图20 sendit函数

a. 初始化auio

341~368 sendit调用getsock函数获取描述符对应的file结构,初始化uio结构,并将进程指定的输出缓存中数据收集到内核缓存中。传送的数据的长度通过一个for循环来计算,并将结构保存在uio_resid。循环内的第一个if保证缓存的长度非负。第二个if保证uio_resid不溢出,因为uio_resid是一个有符号的整数,且iov_len要求非负。

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

369~385 如果进程提供了地址和控制信息,则sockargs将地址和控制信息复制到内核缓存中。

c. 发送数据和清除缓存

386~401 为了防止sosend不接受所有数据而无法计算传送的字节数,将uio_resid的值保存在len中。将插口、目的地址、uio结构、控制信息和标志全部传给函数sosend。当sosend返回后,sendit响应如下:

  • 如果sosend传送了部分数据后,传送被信息或者阻塞条件中断,差错被丢弃,报告传送了部分数据。

  • 如果sosend返回EPIPE,则发送信息SIGPIPE给进程。error设置成非0,所以如果进程捕捉到了该信号,并且从信息处理程序中返回,或进程忽略信息,写调用返回EPIPE。

  • 如果没有差错出现,则计算传送的字节数,并将其保存在*retsize中。如果sendit返回0,syscall返回*retsize给进程而不是返回差错代码。

  • 如果任何其它类型的差错出现,返回相应差错码给进程。

在返回之前,sendit释放包含目的地址的缓存。sosend负责释放control缓存。

sosend函数

sosend是插口层最复杂的函数之一。图7中的四个系统调用最终都调用sosend。sosend的功能就是:根据插口指明的协议支持的语义和缓存的限制,将数据和控制信息传递给插口指明的协议的pr_usrreq函数。sosend从不将数据放在发送缓存中;存储和移走数据应该由协议来完成。

sosend对发送缓存的so_hiwat和sb_lowat的值的解释,取决于对应的协议是否实现了可靠或不可靠的数据传送功能。

1. 可靠的协议缓存

对于提供可靠的数据传送协议,发送缓存保存了还没发送的数据和已经发送但还没有被确认的数据。sb_cc等于发送缓存的数据的字节数,且0<=sb_cc<=sb_hiwat。如果有外带数据发送,则sb_cc有可能暂时超过sb_hiwat。

sosend应该确保在通过pr_usrreq函数将数据传递给协议层之前有足够的发送缓存。协议层将数据放到发送缓存中。sosend通过下面两种方式之一将数据传送给协议层:

  • 如果设置了PR_ATOMIC,sosend就必须保护进程和协议层之间的边界。在这种情况下,sosend等待得到足够的缓存来存储整个报文。当获取到足够的缓存后,构造存储整个报文的mbuf链,并用pr_usrreq函数一次性传送给协议层。

  • 如果没有设置PR_ATOMIC,sosend每次传送一个存储报文的mbuf给协议,可能传送部分mbuf给协议层以防止超过上限。这种方法在SOCK_STREAM类协议(如TCP)和SOCK_SEQPACKET类协议(如TP4)中被采用。

TCP应用程序对外出的TCP报文段的大小没有控制。例如,在TCP插口上发送一个长度为4096字节的报文,假定发送缓存中有足够的缓存,则插口层将该报文分成两个部分,每一部分长度为2048个字节,分别存放在一个外部簇的mbuf中。然后,在协议处理时,TCP将根据连接上的最大报文段大小将数据分段,通常情况下,最大报文段大小为2048个字节。

当一个报文因为太大而没有足够的缓存时,协议允许报文被分成多段,但sosend仍然不将数据传送给协议层直到缓存的闲置大小大于so_lowat。对于TCP而言,so_lowat的默认值为2048(图2),从而阻止插口层在发送缓存快满时用小块数据干扰TCP。

2. 不可靠的协议缓存

对于提供不可靠的数据传输协议(如UDP)而言,发送缓存不需要保存任何数据,也不等待任何确认。每一个报文一旦被排队等待发送到相应的网络设备,插口层立即将它传送到协议。在这种情况下,sb_cc总是等于0,sb_hiwat指定每一次写的最大长度,间接指明数据报的大长度。

图2显示了UDP协议的sb_hiwat的默认值为9216(9x1024)。如果进程没有通过SO_SNDBUF插口选项改变sb_hiwat的值,则发送长度大于9216个字节的数据报将导致差错。不仅如此,其它的协议限制也可能不允许一个进程发送大的数据报报文。

图21显示了sosend函数的概况。下面分别讨论图中四个带阴影的部分。

图21 sosend函数:概述

271~278 sosend的参数:so,指向相应插口的指针;addr,指向目的地址的指针;uio,指向描述用户空间的I/O缓存的uio结构;top,保存将要发送的数据的mbuf链;control,保存将要发送的控制信息的mbuf链;flags,包含本次写调用的一些选项。

正常情况下,进程通过uio机制将数据提供给插口层,top为空。当内核本身正在使用插口层时(如NFS),数据将作为一个mbuf链传送给sosend,top指向该mbuf链,而uio为空。

279~304 初始化代码,后面详细介绍。

a. 给发送缓存加锁

305~308 sosend的主循环从restart开始,在循环的开始调用sblock给发送缓存加锁。M_NOWAIT告知sblock,如果不能立即加锁,则返回EWOULDBLOCK。

主循环直到将所有数据都传送给协议(即resid=0)后才退出。

b. 检查空间

309~341 在传送数据给协议之前,需要对各种差错情况进行检查,并且sosend实现前面讨论的流控制和资源控制算法。如果sosend阻塞等待输出缓存中的更多空间,则它跳回restart等待。

c. 使用top中的数据

342~350 一旦有了足够的空间并且sosend也获得了发送缓存上的锁,则是准备传送给协议的数据。如果uio等于空(即数据在top指向的mbuf链中),则sosend检查MSG_EOR,并且在链中设置M_EOR来标志逻辑记录的结束。mbuf链是准备发送给协议层的。

d. 从进程复制数据

351~396 如果uio不空,则sosend必须从进程中复制数据。当PR_ATOMIC被设置时(如UDP),循环继续,直到所有数据都被复制到一个mbuf链中。当sosend从进程得到所有数据后,通过循环中的break跳出循环。跳出循环后,sosend将整个数据链一次传送给相应协议。

e. 传送数据给协议

395~414 对于PR_ATOMIC协议,当整个数据链被传送给协议后,resid总是等于0,并且控制跳出两个循环后至release处。如果PR_ATOMIC没有被置位,且当还有数据要发送并有缓存空间时,则sosend继续往mbuf中写数据。如果缓存中没有闲置空间,但仍然有数据要发送,则sosend回到循环开始,等待闲置空间来写下一个mbuf。如果所有数据都发送完,则两个循环结束。

f. 释放缓存

414~422 当所有数据都传送给协议后,给插口缓存解锁,释放多余的mbuf缓存,然后返回。

sosend的详细情况将分四个部分来描述:

  • 初始化(图22)

  • 差错和资源检查(图23)

  • 数据传送(图24)

  • 协议处理(图25)

sosend的第一部分初始化变量,如图22所示。

图22 sosend函数:初始化

g. 计算传送大小和语义

279~284 如果sosendallatonce等于true(任何设置了PR_ATOMIC的协议)或数据已经通过top中的mbuf链传送给sosend,则将设置atomic。这个标志控制数据作为一个mbuf链还是作为多个独立的mbuf传送给协议。

285~297 resid等于iovec缓存中的数据字节数或top中的mbuf链中的数据字节数。

h. 关闭路由

298~303 如果仅仅要求对这个报文不通过路由表进行路由选择,则设置dontroute。clen等于在可选的控制缓存中的字节数。

图23显示的sosend代码功能是检验差错条件和等待发送缓存中的闲置空间。

图23 sosend函数:差错和资源检查

309 当检查差错情况时,为防止缓存发生改变,协议处理被挂起。在每一次数据传送之前,sosend要检查以下几种差错情况:

  • 310~311 如果插口输出被禁止(例如TCP连接的写通道被关闭),则返回EPIPE。

  • 312~313 如果插口正处于差错状态,则返回so_error。如果差错出现之前数据已经被收到,则sendit忽略这个差错(图20的第389行)。

  • 314~318 如果协议请求连接且连接还没有建立或者连接请求还没启动,则返回ENOTCONN。sesend允许只有控制信息但没有数据的写操作,即使连接还没建立。

  • 319~321 如果在无连接协议中没有指定目的地址(例如,进程调用send但并没有用connect建立目的地址),则返回EDESTADDREQ。

i. 计算可用空间

322~324 sbspace函数计算发送缓存中剩余的闲置空闲字节数。这是一个基于缓存高水位标记的管理上的限制,但也是sb_mbmax对它的限制,其目的是为了防止太多的小报文消耗太多的mbuf缓存。sosend通过放宽缓存限制到1024个字节来给予外带数据更高的优先级。

j. 强制实施报文大小限制

325~327 如果atomic被置位,并且报文大于高水位标记(high-watermark),则返回EMSGSIZE;报文因为太大而不被协议接受,即使缓存是空的。如果控制信息的长度大于高水位标记,同样返回EMSGSIZE。这是限制数据或者记录大小的测试代码。

k. 等待更多的空间吗?

328~329 如果发送缓存中的空间不够,数据来源于进程(而不是来源于内核中的top),并且下列条件之一成立,则sosend必须等待更多的空间:

  • 报文必须一次传给协议(atomic为真);

  • 报文可以分段传送,但闲置空间大小低于低水位标记;

  • 报文可以分段传送,但可用空间存放不下控制信息。

当数据通过top传送给sosend(即uio为空)时,数据已经在mbuf缓存中。因此,sosend忽略缓存高、低水位标记限制,因为不需要附加的缓存来保存数据。

l. 等待空间

330~338 如果sosend必须等待缓存且插口是非阻塞的,则返回EWOULDBLOCK。同时,缓存锁被释放,sosend调用sbwait等待,直到缓存状态发生变化。当sbwait返回后,sosend重新使能协议处理,并且跳转到restart获取缓存锁,检查差错和缓存空间。如果条件满足,则继续执行。

默认情况下,sbwait阻塞直到可以发送数据。通过SO_SNDTIMEO插口选项改变缓存中的sb_timeo,进程可以设置等待时间的上限。如果定时器超时,则返回EWOULDBLOCK。回想一下图20,如果数据已经被成功发送给协议,则sendit忽略这个差错。这个定时器并不限制整个调用的时间,而仅仅是限制写两个mbuf缓存之间的不活动时间。

339~341 在这点上,sosend已经知道一些数据已传送给协议。splx使能中断,因为sosend从进程复制数据到内核相对较长的时间间隔内不应该被阻塞。mp包含一个指针,用来构造mbuf链。在sosend从进程复制任何数据之前,可用缓存的数量减去控制信息的大小(clen)。

图24显示了sosend从进程复制数据到一个或多个内核中的mbuf中去的代码段。 图24 sosend函数:数据传送

m. 分配分组首部或者标准mbuf

351~360 当atomic被置位时,这段代码在第一次循环时分配一个分组首部,随后分配标准的mbuf缓存。如果atomic没有被置位,则这段代码总是分配一个分组首部,因为进入循环之前,top总是被清除。

n. 尽可能用簇

361~371 如果报文足够大使得为其分配一个簇是值得的,并且space大于或等于MCLBYTES,则调用MCLGET分配一个簇同mbuf连在一起。当space小于MCLBYTES时,额外的2048个字节将超过缓存分配限制,因为即使resid小于MCLBYTES,整个簇也将被分配。

如果调用MCLGET失败,sosend跳转到nopages,用一个标准的mbuf代替一个外部簇。对于MINCLSIZE的测试应该是>,而不是>=,因为208(MINCLSIZE)个字节的写操作只适合小于两个mbuf的情况。

如果atomic被置位(如UDP),则mbuf链表示一个数据报或记录,并且在第一个簇的前面为协议首部保留max_hdr个字节。而后续的簇因为是同一条链的一部分,所以不需要再为协议首部空间。

如果atomic没有被置位(如TCP),则不需要保留空间,因为sosend不知道协议如果将发送的数据进行分段。

需要注意的是,space由簇大小(2048字节)而不是len来决定,len等于放在簇中的数据的字节数。

o. 准备mbuf

372~382 如果不用簇,存储在mbuf中的字节数受下面三个量中最小的一量的限制:mbuf中的可用空间;报文的字节数;缓存的空间。

如果atomic被置位,则利用MH_ALIGN可知数据在链中的第一个缓存的尾部。如果数据占居整个mbuf,则忽略MH_ALIGN。这一点可能导致没有足够的空间来存放协议首部,主要取决于有多少数据存放在mbuf中。如果atomic没有被置位,则没有为协议首部保留空间。

p. 从进程复制数据

385~395 uiomove从进程复制len个字节的数据到mbuf。传送完成后,更新mbuf的长度,前面的mbuf连接到新的mbuf(或top指向第一个mbuf),更新mbuf链的长度。如果在传送过程中发生差错,则sosend跳转到release。

一旦最后一个字节传送完毕,如果进程设置了MSG_EOR,则设置分组中的M_EOR,然后sosend跳出循环。

MSG_EOR仅用于有显式的记录边界的协议,如OSI协议簇中的TP4。TCP不支持记录因而忽略MSG_EOR标志。

q. 写另一个缓存吗?

396 如果设置了atomic,sosend回到循环开始,写另一个mbuf。

对于space>0的测试好像无关紧要。当atomic没有被设置时,space也是无关紧要的,因为一次只传送一个mbuf给协议。如果设置了atomic,只有当有足够的缓存空间存放整个报文时才进入这个循环。

sosend的最后一段代码的功能是传送数据和控制mbuf给插口指定的协议,如图25所示。

图25 sosend函数:协议分散

397~405 在传送数据到协议层的前后,可能通过SO_DONTROUTE选项选择是否利用路由表为这个报文选择路由。这是唯一的一个针对单个报文的选项,如图22所示,在写期间通过MSG_DONTROUTE标志来控制路由选择。

为了防止协议在处理报文期间pr_usrreq阻塞中断,pr_usrreq被放在splet函数和splx函数之间执行。一些协议(如UDP)可能在进行输出处理期间并不阻塞中断,但插口层得不到这些信息。

如果进程传送的是外带数据,则sosend发磅PRU_SENDOOB请求;否则,它发送PRU_SEND请求。同时将地址和控制mbuf传送给协议。

406~413 因为控制信息只需要传送给协议一次,所以将clen、control、top和mp初始化,然后为传送报文的下一部分构造新的mbuf链。只有atomic没有被设置时(如TCP),resid才可能等于非0.在这种情况下,如果缓存中仍然有空间,则sosend回到循环开始,继续写另一个mbuf。如果没有可用空间,则sosend回到循环开始,等待可用空间(图23)。

对于不可靠协议,如UDP,立即将数据排队等待发送;对于可靠协议,如TCP,将数据放到插口发送缓存直到数据被发送和确认。

3. sosend函数小结

sosend是一个比较复杂复杂的函数,共142行,包含3个循环嵌套,一个利用goto实现的循环,两个基于是否设置PR_ATOMIC的代码分支,两个并行锁。像其它软件一样,复杂性是多年积累的结果,需要耐心地读。

如图24所描述的,sosend尽可能地以mbuf为单位将报文传送到协议层。与将一个报文用一个mbuf链的形式一次建立并传送给协议层的方法相比,这种做法导致了更多的调用,但是这种做法增加了并行性,因而获取了较好的性能。

一次传送一个mubf(2048字节)允许CPU在网络硬件传送数据的同时准备一个分组。同发送一个大的mbuf链相比:构造一个大的mbuf链的同时,网络和接收系统是空闲的。所以这种方式增加了并行性,性能更好。

read、readv、recvfrom和recvmsg系统调用

我们将read、readv、recvfrom和recvmsg系统调用统称为读系统调用,从网络连接上接收数据。同recvmsg相比,前三个系统调用比较简单。recv因为比较能用而复杂得多。图26给出了这四个系统调用和一个库函数(recv)的特点。

图26 读系统调用

在Net/3中,recv是一个库函数,通过调用recvfrom来实现的。只有read和readv系统调用适用于各类描述符,其它的调用只适用于插口描述符。

同写调用一样,通过iovec结构数组来指定多个缓存。对数据报协议,recvfrom和recvmsg返回每一个收到的数据报的源地址。对于面向连接的协议,getpeername返回连接对方的地址。

同写调用一样,读调用利用一个公共函数soreceive来做所有工作。图27说明读系统调用的流程。我们仅讨论带阴影的函数,从上到下依次是recvmsg、recvit、soreceive。

图27 所有插口输入都由soreceive处理

recvmsg系统调用

recvmsg函数是最通用的读系统调用。如果一个进程使用任何一个其它的读系统调用,且地址、控制信息和接收标志的值还未定,则系统可能在没有任何通知的情况下丢弃它们。图28显示了recvmsg函数。

图28 recvmsg系统调用

433~445 recvmsg的三个参数是:插口描述符;类型为msghdr的结构指针;几个控制标志。

a. 复制iov数组

446~461 同sendmsg一样,recvmsg将msghdr结构复制到内核,如果自动分配的数组aiov太小,则动态分配一个更大的iovec数组,并且将数组单元从进程复制到iov指向的内核数组。将第三个参数复制到msghdr结构中。

b. recvit和释放缓存

462~470 recvit收完数据后,将更新过的缓存长度和标志的msghdr结构再复制到进程。如果分配了一个更大的iovec结构,则返回之前释放它。

recvit函数

recvit函数被recv、recvfrom和recvmsg调用,图如29所示。基于recv_xxx调用提供的msghdr结构,recvit函数为soreceive的处理准备了一个uio结构。

图29 recvit函数:初始化uio结构

471~500 getsock为描述符s返回一个file结构,然后recvit初始化一个uio结构,该结构描述从内核到进程之间的一次数据传送。通过对iovec数组中的msg_iovlen字段求和得到传送的字节数。结构保留在uio_resid中的len中。

recvit的第二部分调用soreceive,并且将结构复制到进程,如图30所示。

图30 recvit函数:返回结果

a. 调用soreceive

501~510 soreveive实现从插口缓存中接收数据的最复杂的功能。传送的字节数保存在*retsize中,并且返回给进程。如果有些数据已经被复制到进程后信号出现或阻塞出现(len不等于uio_resid),则忽略差错,并返回已经传送的字节。

b. 将地址和控制信息复制到进程

511~542 如果进程传入了一个存放地址或者控制信息或者两个都有的缓存,则recvit将结果写入该缓存,并且根据soreceive返回的结果调整它们的长度。如果缓存太小,则地址信息可能被截掉。如果进程在发送读调用之前保留缓存的长度,将该长度同内核返回的namelenp变量(或sockaddr结构的长度域)相比较就可以发现这个差错。通过设置msg_flags中的MSG_CTRUNC标志来报告这种差错。

c. 释放缓存

543~549 从out开始,释放存储源地址和控制信息的mbuf缓存。

soreceive函数

soreceive函数将数据从插口的接收缓存传送到进程指定的缓存。某些协议还提供发送者的地址,地址可以同可能的附加控制信息一起返回。在讨论它的代码之前,先介绍接收操作、外带数据和插口接收缓存的组织的含义。

图31列出了在执行soreceive期间内核知道的一些标志。

图31 recv_xxx系统调用:传递给内核的标志值

recvmsg是唯一返回标志字段给进程的读系统调用。在其它系统调用中,控制返回给进程之前,这些信息被内核丢弃。图32列出了在msghdr中recvmsg能设置的标志。 图32 recvmsg系统调用:内核返回的msg_flags值

1. 外带数据

外带数据(OOB)在不同的协议中有不同的含义。一般来说,协议利用已建立的通信连接来发送OOB数据。OOB数据可能与已发送的正常数据同序。插口层支持两种与协议无关的机制来实现对OOB数据的处理:标志和同步。本文讨论插口层实现的抽象的OOB机制。UDP不支持OOB数据。TCP的紧急数据机制与插口层的OOB数据之间的关系在TCP一章中介绍。

发送进程通过在sendxxx调用中设置MSG_OOB标志将数据标记为OOB数据。sosend将这个信息传递给插口协议,插口层收到这个信息后,对数据进行特殊处理,如加快发送数据或者使用另一种排队策略。

当一个协议收到OOB数据后,并不将它放进插口的接收缓存而是放在其它地方。进程通过设置recvxxx调用中的MSG_OOB标志来接收到达的OOB数据。另一种方法是,通过设置SO_OOBINLINE插口选项,接收进程可以要求协议将OOB数据放在正常的数据之内。当SO_OOBINLINE被设置时,协议将收到的OOB数据放进正常数据的接收缓存。在这种情况下,MSG_OOB不用来接收OOB数据。读调用要么返回所有的正常数据,要么返回所有OOB数据。两种类型的数据从来不会在一个输入调用的输入缓存中混淆。进程使用recvmsg来接收数据时,可以通过检查MSG_OOB标志来决定返回的数据是正常数据还是OOB数据。

插口层支持OOB数据和正常数据的同步接收,采用的方法是允许协议在正常数据流中标志OOB数据起始点。接收者可以在每一个读系统调用的后面,通过SIOCATMRK iocl命令来检查是否已经达到OOB数据的起始点。当接收正常的数据时,插口层确保在一个报文中只有在标记前的正常数据才会收到,使得接收者接收的数据不会超过标志。如果在接收都到达标志之前收到一些附加的OOB数据,标记就自动向前移。

2. 举例

图33说明两种接收外带数据的方法。在两个例子中,字节AI作为正常数据接收,字节J作为外带数据接收,字节KL作为正常数据接收。接收进程已接收了A之前(不包括A)的所有数据。

图33 接收外带数据

在第一个例子中,进程能够正确读出字节AI,或者如果设置MSG_OOB,也能讲出字节J。即使读请求的长度大于9个字节(AI),插口层也只返回9个字节,以免超过外带数据的同步标记。当读出I后,SIOCATMARK为真;对于到达外带数据标记的进程,不必读出字节J。

在第二个例子中,在SIOCATMARK为真时只能读字节AI。第二次调调用读字节JL。

在图33中,字节J不是TCP的紧急数据指针指示的字节。在本例中,紧急指针指向的是字节K。

3. 其它的接收操作选项

进程能够通过设置标志MSG_PEEK来查看是否有数据到达。而数据仍然留在接收队列中,被下一个不设置MSG_PEEK的读调用读出。

标志MSG_WAITALL指示读调用只有在读到指定数据的数据后才返回。即使soreceive中有一些数据可以返回给进程,但它仍然要等到收到剩余的数据后才返回。

当标志MSG_WAITALL被设置后,soreceive只有在下列情况下可以在没有读完指定长度的数据时返回:

  • 连接的读通道被关闭

  • 插口的接收缓存小于所读数据的大小

  • 在进程等待剩余的数据时出现差错

  • 外带数据到达

  • 在读缓存被写满之前,一个逻辑记录的结尾出现

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

对于支持报文边界的协议,每一个报文存放在一个mubf链中。接收缓存中的多报文通过m_nextpkt指针链接成一个mbuf队列。协议处理层加数据到接收队列,插口层从接收队列中移走数据。接收缓存的高水位标记限制了存储在缓存中的数据量。

如果PR_ATOMIC没有被置位,协议层尽可能多地在缓存中存放数据,丢弃输入数据中不合要求的部分。对于TCP,这就意味着到达的任何数据如果在接收窗口之外都将被丢弃。如果PR_ATOMIC被置位,缓存必须能够容纳整个报文,否则协议层将丢弃整个报文。对于UDP而言,如果接收缓存已满,则进入的报文都将被丢弃,缓存满的原因可是进程读数据报的速度不够快。

PR_ADDR被置位的协议使用sbappendaddr构造一个mbuf链,并将其加入的到接收队列。缓存链包含一个存放报文源地址的mbuf,0个或多个控制mbuf,后面跟着0个或者更多的包含数据的mbuf。

对于SOCK_SEQPACKET和SOCK_RDM协议,它们为每一个记录建立一个mbuf链。如果PR_ATOMIC被置位,则调用sbappendrecord,将记录加到接收缓存的尾部。如果PR_ATOMIC没有被置位,则用sbappendrecord产生一个新的记录其余的数据用sbappend加到这个记录中。

图34说明了由三个mbuf链(即三个数据报)组成的UDP接收缓存的结构。每一个mbuf中都有m_type的值。

在图34中,第三个数据报中有一些控制信息。三个UDP插口选项能够导致控制信息被存入接收缓存。

图34 包含三个数据报的UDP接收缓存

对于PR_ATOMIC协议,当收到数据时,sb_lowat被忽略。当没有设置PR_ATOMIC时,sb_lowat的值等于读系统调用返回的最小的字节数。但也有一些例外,如图40所示。

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

当协议不需要维护报文边界(即SOCK_STREAM协议,如TCP)时,通过sbappend将进入的数据加到缓存中的最后一个mbuf链的尾部。如果进入的数据长度大于缓存的长度,则数据将被截掉,sb_lowat为一个读系统调用返回的字节数设置了一个下限。

图35说明了仅仅包含正常数据的TCP接收缓存的数据结构。

图35 TCP的so_rcv缓存

6. 控制信息和外带数据

不像TCP,一些流协议支持控制信息,并且调用sbappendcontrol将控制信息和相关数据作为一个新的mbuf链加入接收缓存。如果协议支持内含OOB数据,则调用sbinseroob插入一个新的mbuf链到任何包含OOB数据的mbuf链之后,但在任何包含正常数据的mbuf链之前。这一点确保进入的OOB数据总是排在正常数据之前的。

图36说明包含控制信息和OOB数据的接收缓存的结构。

图36 带有控制信息和OOB数据的so_rcv缓存

TCP既不支持控制信息,也不支持MT_OOBDATA形式的外带数据。如果TCP的紧急指针指向的字节存储在数据内(SO_OOBINLINE被设置),那么该字节是正常数据而不是OOB数据。TCP对紧急指针和相关数据的处理在29章讨论。

以上就是第16章 插口I/O的第一部分,下一篇我们将介绍第二部分。

更多最新文章,请关注公众号:大白爱爬山