概说《TCP/IP详解 卷2》第15章 插口层

298 阅读38分钟

本文要点

  • 引言

  • socket结构

  • 系统调用

  • 进程、描述符和插口

  • socket系统调用

    • socreate函数

    • 超级用户特权

  • getsock和sockargs函数

  • bind系统调用

  • listen系统调用

  • sleep和wakeup函数

  • accept系统调用

  • sonewconn和soisconnected函数

  • connect系统调用

  • shutdown系统调用

  • close系统调用

    • soo_close函数

    • soclose函数

  • 小结

引言

    插口层的主要功能是将进程发送的与协议有关的请求 映射到产生插口时指定的与协议相关的实现,好好理解一下这句话。

    为了允许标准的Unix I/O系统调用,如write和read,也能读写网络连接,在BSD版本中将文件系统和网络功能集成在系统调用级。与通过一个描述符访问一个打开的文件一样,进程也是通过一个描述符来访问插口上的网络连接。这个特点使得标准的文件系统调用,如read和write,以及与网络有关的系统调用,如sendmsg和recvmsg,都能通过描述符来处理插口。

    本文的重点是插口及相关的系统调用的实现,而不是讨论如何使用插口层来实现网络应用。图1说明了进程中的插口与内核中的协议实现之间的层次关系。

图1 插口层将一般的请求转换为指定的协议操作

socket结构

    插口代表一条通信链路的一端,存储或指向与链路有关的所有信息。这些信息包括:使用的协议、协议的状态信息(源和目的地址)、到达的连接队列、数据缓存和可选标志。图2中给出了插口与插口相关的缓存的定义。

图2 socket定义

41~42 so_type由产生插口的进程来指定,它指明插口和相关协议支持的通信语义 。so_type的值等于概说《TCP/IP详解 卷2》第7章 域和协议图5中所示的py_type。对于UDP,so_type等于SOCK_DGRAM,而对于TCP,so_type则等于SOCK_STREAM。

43 so_options是一组改变插口行为的标志,图3列出了这些标志。

图3 so_options的值

    通过getsockopt和setsockopt系统调用进程能修改除SO_ACCEPTCONN外所有的插口选项。当在插口上发送listen系统时,SO_ACCEPTCONN被内核设置。

44 so_linger等于当关闭一条连接时插口继续发送数据的时间间隔,单位为一个时间滴答。

45 so_state表示插口的内部状态和一些其它的特点。图4列出了so_state可能的取值。

图4 so_state的值

    从图4中的第二列可以看出,进程可能通过fcntl和ioctl系统调用直接修改SS_ASYNC和SS_NBIO。对于其它的标志,进程只能在系统调用的执行过程中间接修改。例如,如果进程调用connect,当连接被建立时,SS_ISCONNECTED标志就会被内核设置。

SS_NBIO和SS_ASYNC标志

    在默认情况下,进程在发出I/O请求后会等待资源。例如,对一个插口发read系统调用,如果当前没有网络上来的数据,则read系统调用就会被阻塞。同样,当一个进程调用write系统调用时,如果内核中没有缓存来存储发送的数据,则内核将阻塞进程。如果设置了SS_NBIO,在对插口执行I/O操作且请求的资源不能得到时,内核并不阻塞进程,而是返回EWOULDBLOCK。

    如果设置了SS_ASYNC,当因为下列情况之一而使插口状态发生变化时,内核发送SIGIO信息给so_pgid标识的进程或进程组:

  • 连接请求已完成;

  • 断连请求已被启动;

  • 断连请求已完成;

  • 连接的一个通道已被关闭;

  • 插口上有数据到达;

  • 数据已被发送(即输出缓存中有闲置空间);

  • UDP或者TCP插口上出现一个异步差错。

46 so_pcb指向协议控制块,协议控制块包含与协议有关的状态信息和插口参数。每一种协议都定义了自己的控制块结构,所以so_pcb被定义成一个通用的指针。图5列出了我们讨论的控制块结构。

图5 协议控制块

47 so_proto指向进程在socket系统调用中选择的协议的protosw结构。

48~64 设置了SO_ACCEPTCONN标志的插口维护两个连接队列。还没有完全建立的连接(如TCP的三次握手还没完成)被放在队列so_q0中。已经建立的连接或被接受的连接(例如TCP的三次握手已经完成)被放入到so_q中。队列的长度分别为so_q0len和so_qlen。每个被排队的连接由它自己的插口来表示。在每个被排队的插口中,so_head指向设置了SO_ACCEPTCONN的源插口。

    插口上可排队的连接数通过so_qlimit来控制,进程可以通过listen系统调用来设置so_qlimit。内核隐含设置的上限为5(SOMAXCONN)和下限0。图27中显示了有点晦涩的公式使用so_qlimit来控制排队的连接数。

    图6显示了有三个连接将被接受、一个连接已经被建立的情况下的队列内容。

图6 插口连接队列

65 so_timeo用作accept、connect和close处理期间的等待通道。

66 so_error保存差错代码,直到在引用该该插口的下一个系统调用期间差错代码能发给进程。

67 如果插口的SS_ASYNC被设置,则SIGIO信息被发送给进程(如果so_pgid大于0)或进程组(如果so_pgid小于0)。可能通过ioctl的SIOCSPGRP和SIOCGPGRP命令来修改或者检查so_pgid的值。

68 so_oobmark标识在输出数据流中最近收到的外带数据的开始点;第16章讨论插口对外带数据的支持,第29章讨论TCP中的外带数据的语义。

69~82 每个插口包括两个数据缓存,so_rev和so_snd,分别用来缓存接收或者发送的数据。so_rcv和so_snd是包含在插口结构中的结构而不是指向结构的指针。

83~86 在Net/3中不使用so_tpcb。so_upcall和so_upcallart也仅用于Net/3中的NFS软件。

    图7是几个简化的插口层代码的宏和函数的描述。

图7 插口的宏和函数

系统调用

    进程同内核交互是通过一组定义好的函数来进行的,这些函数称为系统调用。在讨论支持网络的系统调用之前,我们先来看看系统调用机制的本身。

    从进程到内核中的受保护的环境的转换是与机器和实现相关的。我们将使用Net/3在386上的实现来说明如何实现有关的操作。

    在BSD内核中,每个系统调用均被编号,当进程执行一个系统调用时,硬件被配置成仅传送控制给一个内核函数。将标识系统调用的整数作为参数传给该内核函数。在386实现中,这个内核函数为syscall;利用系统调用的编号,syscall在表中找到请求的系统调用的sysent结构。表中的每一个单元均为一个sysent结构。

    struct sysent {

        int sy_narg;

        int ( *sy_call ) ( );

    };

    表中有几个项是从syent数组中来的,该数组定义在kern/init_sysent.c中。

    struct sysent sysent[ ] = {

         /* ... */

        { 3, recvmsg }, // 27=recvmsg

        { 3, sendmsg }, // 28=sendmsg

        { 6, recvfrom }, // 29=recvfrom

        { 3, accept }, // 30=accept

        { 3, getpeername }, // 31=getpeername

        { 3, getsockname }, // 32=getsockname

        /* ... */

    }

    例如,recvmsg系统调用在系统调用表中的第27个项,它有两个参数,利用内核中的recvmsg函数实现。

    syscall将参数从调用进程复制到内核中,并且分配一个数组来保存系统调用的结构。然后当系统调用执行完成后,syscall将结构返回给进程。syscall将控制交给与系统调用相对应的内核函数。在386实现中,调用有点像:

    struct sysent *callp;

    error = (*callp->sy_call) (p, args, rval);

这里指针callp指向相关的sysent结构;指针p则指向调用系统调用的进程的进程表项;args作为参数传给系统调用,它是一个32bit长的字数组;而rval则是用来保存系统调用的返回结果的数组,数组有两个元素,每个元素是一个32bit长的字。当我们用“系统调用”这个词时,我们指的是被syscall调用的内核中的函数,而不是应用调用的进程中的函数。

    syscall期望系统调用函数(sy_call指向的函数)在没有差错时返回0,否则返回非0的差错代码。如果没有差错出现,内核将rval中的值作为系统调用的返回值传送给进程。如果有差错,syscall忽略rval中的值,并以与机器相关的方式返回差错代码给进程,使得进程能从外部变量errno中得到差错信息。

    总之,实现系统调用的函数有两个“返回”值:一个返回给syscall函数;另外一个在没有差错的情况,syscall将另一个(rval)返回给调用进程。

    socket系统调用的原型是:

    int socket(int domain, int type, int protocal);

    实现socket系统调用的内核函数的原型是:

    struct socket_args {

        int domain;

        int type;

        int protocol;

    };

    socket( struct proc *p, struct socket_args*uap, 

                 int *retval) ;

    当一个应用调用socket时,进程用系统调用机制将三个独立的整数传给内核。syscall将参数复制到32bit的数组中,并将数组指针作为第二个参数传给sokcet的内核版。内核版的socket将第二个参数作为指向socket_args结构的指针。图8显示了上述过程。

图8 socket参数处理

    同socket类似,每一个实现系统调用的内核函数将args说明成一个与系统调用有关的结构指针,而不是一个指向32bit的字的数组指针。

    syscall在执行内核系统调用函数之前将返回值置为0。如果没有差错出现,系统调用函数直接返回而不需要清除*retval,syscall返回0给进程。

    图9对与网络有关的系统调用进行了小结。我们将在本文讨论建立、服务器、客户和终止类系统调用。输入、输出类系统调用将在第16章介绍,管理类系统调用将在第17章介绍。

图9 Net/3中的网络系统调用

    图10画出了应用使用这些系统调用的顺序。大方块中的I/O系统调用可以在任何时候调用。该图不是一个完整的状态流程图,因为一些正确的转换在本图中没有画出,这里仅显示了一些常见的转换。

图10 网络系统调用流程图

进程、描述符和插口

    在描述插口系统调用之前,我们先介绍将进程、描述符和插口联系在一起的数据结构。图11给出了这些结构以及与我们讨论有关的结构成员。

图11 进程、文件和插口结构

    系统调用的第一个参数总为p,即指向调用进程的proc结构的指针。内核利用proc结构记录进程的有关信息。在proc结构中,p_fd指向filedesc结构,该结构的主要功能是管理fd_ofile指向的描述符表。描述符表的大小是动态变化的,由一个指向file结构的指针数组组成。每个file结构描述一个打开的文件,该结构可被多个进程共享。

    图11中仅显示了一个file结构。通过p->p_fd->fd_ofiles[fd]访问到该结构。在file结构中,有两个结构成员是我们感兴趣的:f_ops和f_data。I/O系统调用(如read和write)的实现因描述符中的I/O对象类型的不同而不同。f_ops指向fileops结构,该结构包含一张实现了read、write、ioctl、select和close系统调用的函数指针表。图11显示了f_ops指向一个全局的fileops结构,即socketops,该结构包含指向插口用的函数的指针。

    f_data指向相关I/O对象的专用数据。对于插口而言,f_data指向与描述符相关的socket结构。最后,socket结构中的so_proto指向产生插口时选中的协议的protosw结构。回想一下,每一个protosw结构是由与该协议关联的所有插口共享的。

socket系统调用

    socket系统调用产生一个新的插口,并将插口同进程在参数domain、type和protocol中指定的协议联系起来。该函数(图12)分配一个新的描述符,用来在后续的系统调用中标识插口,并将描述符返回给进程。

图12 socket系统调用

42~55 在每一个系统调用的前面,都定义了一个进程传递给内核的参数结构。在这种情况下,参数是通过socket_args传入的。所有插口层系统调用都有三个参数:p,指向调用进程的proc结构;uap,指向进程传递给系统调用的参数结构;retval,用来接收系统调用的返回值。在通常情况下,忽略参数p和retval,引用uap所指的结构中的内容。

56~60 falloc分配一个新的file结构 和fd_ofiles数组(图11)中的一个元素。fp指向新分配的file结构,fd则为结构在数组fd_ofiles中的索引。socket将file结构设置成可读、可写,并且作为一个插口。将所有插口共享的全局fileops结构socketops连接到f_ops指向的file结构中。socketopt变量在编译时被初始化,如图13所示。

图13 socketopt:插口用的全局fileops结构

60~69 soceate分配并初始化一个socket结构。如果socreate执行失败,将差错码赋给error,释放file结构,清除存储在描述符中的数组元素。如果socreate执行成功,将f_data指向socket结构,建立插口和描述符之间的联系。通过*retval将fd返回给进程;socket返回0或者差错代码。

1. socreate函数

    大多数插口系统调用至少被分成两个函数,与socket和socreate类似,第一个函数从进程那里获取需要的数据,调用第二个函数soxxx来完成功能处理,然后返回结构给进程。这种分成多个函数的做法是为了第二个函数能直接被基于内核的网络协议调用。socreate函数的代码如图14所示。

图14 socreate函数

43~52 socreate共有四个参数:dom,请求的协议域(如PF_INET);aso,保存指向一个新的socket结构的指针;type,请求的插口类型(如SOCK_STREAM);proto,请求的协议。

    a. 发现协议交换表

53~60 如果proto等于非0值,pffindproto查找进程请求的协议。如果proto等于0,pffindtype用由type指定的语言在指定域中查找一种协议。 两个函数均返回一个指向匹配协议的protosw结构的指针或者空指针。

    b. 分配并初始化socket结构

61~66 socreate分配一个新的socket结构,并将结构内容全清0,记录下type。如果调用进程有超级用户权限,则设置插口结构中的SS_PRIV标志。

    c. PRU_ATTACH请求

67~69 在与协议无关的插口层中发送与协议有关的请求的第一个例子出现在socreate中。在图11中。so->so_proto->pr_usrreq是一个指向与插口so相关联的协议的用户请求函数指针。每个协议均提供了一个这样的函数来 处理从插口层来的通信请求。函数原型是:

    int pr_usrreq(struct socket *so, int req, 

                         struct mbuf *m0, *m2, *m3);

    第一个参数是一个指向相关插口的指针,req是一相标识请求的常数。后面三个(m0, m1, m2)因请求不同而不同。

    图15列出了pr_usrreq函数提供的通信请求,每一个请求的语言于服务请求的协议。

图15 pr_usrreq函数

    d. 退出处理

70~77 回到socreate,函数将协议交换表连接到插口,发送PRU_ATTACH请求通知协议已建立一个新的连接端点。该请求引起大多数协议,如TCP和UDP,分配并初始化所有支持新的连接端点的数据结构。

2. 超级用户特权

    图16列出了要求超级用户权限的网络操作。

图16 Net/3中的超级用户特权

    图16中,“进程”栏表示请求必须由超级用户进程来发起,“插口”栏表示请求必须是针对由超级用户产生的插口(也就是说,进程不需要超级用户权限,而只需要有访问插口的权限)。因为rip_usrreq在用socreate产生插口后立即检查SS_PRIV标志,所以我们认为只有超级用户进程才能访问这个函数。

getsock和sockargs函数

    这两个函数重复出现在插口系统调用中。getsock的功能是将描述符映射到一个文件表项中,sockarg将进程传入的参数复制到内核中的一个新分配的mbuf中。这两个函数都要检查参数的正确性,如果参数不合法,则返回相应的非0差错码。

    图17列出了getsock函数的代码。

图17 getsock函数

754~767 getsock函数利用fdp查找描述符fdes指定的文件表项,fdp是指向filedesc结构的指针。getsock将打开的文件结构指针赋给fpp,并返回,或者当出现下列情况时返回差错码:描述符的值越过了范围而不是指向一个打开的文件;描述符没有同插口建立联系。

    图18列出了sockargs函数的代码。

图18 sockargs函数

768~783 sockargs将进程传给系统调用的参数的指针从进程复制到内核而不是复制指针指向的数据,这样做是因为每个参数的语义只有相应的系统调用才知道,而不是针对所有的系统调用。多个系统调用在调用sockargs复制参数指针后,将指针指向的数据从进程复制到内核中新分配的mbuf中。例如,sockargs将bind的第二个参数(指向的本地插口地址)从进程复制到一个mbuf中。

    如果数据不能存入一个mbuf中或无法分配mbuf,则sockargs返回EINVAL或ENOBUFS。注意,这里使用的是标准的mbuf而不是分组首部的mbuf。coppyin的功能是将数据从进程复制到mbuf中。copyin返回的最常见的差错是EACCES,它表示进程提供的地址不正确。

784~785 当出现差错时,丢弃mbuf,并返回差错码。如果没有差错,通过mp返回指向mbuf的指针,sockargs返回0。

786~794 如果type等于MT_SONAME,则进程传入的是一个sockaddr结构。sockargs将刚复制的参数的长度赋给内部长度变量sa_len。这一点确保即使进程没有正确地初始化结构,结构内的大小也是正确的。

bind系统调用

    bind系统调用将一个本地的网络运输层地址和插口联系起来。一般来说,作为客户的进程并不关心它的本地地址是什么。在这种情况下,进程在进行通信之间没有必要调用bind;内核会自动为其选择一个本地地址。

    服务器进程则总是需要到一个已知的地址上。所以,进程在接受连接(TCP)或者接收数据报(UDP)之间必须调用bind,因为客户进程需要同已知的地址建立连接或者发送数据报到已知地址。

    插口的外部地址由connect指定或者由允许指定外部的写调用(sendto或sendmsg)指定。

    图19列出了bind调用的代码。

图19 bind函数

70~82 bind调用的参数有(在bind_args结构中):s,插口描述符;name,包含传输地址(如sockaddr_in结构)的缓存指针;namelen,缓存大小。

83~90 getsock返回描述符的file结构,sockargs将本地地址复制到mbuf中,sobind将进程指定的一插口联系起来。在bind返回sobind的结果之前,释放保存地址的mbuf。

    sobind函数如图20所示,它是一个封装器,它给与插口相关的协议发送PRU_BIND请求。

图20 sobind函数

78~89 sobind发送PRU_BIND请求。如果请求成功,将本地地址nam同插口关联起来;否则返回差错码。

listen系统调用

    listen系统调用的功能是通知协议,进程准备接收插口上的连接请求,如图21所示。它同时也指定插口上可以排队等待的连接数的门限值。超过门限值时,插口层将拒绝进入的连接请求排队等待。当这种情况出现时,TCP将忽略进入的连接请求。进程可以通过调用accept来得到队列中的连接。

图21 listen系统调用

91~98 listen系统调用有两个参数:一个指定插口描述符;另一个指定连接队列门限值。

99~105 getsock返回描述符s的file结构,solisten将请求传递给协议层。

    solisten函数发送PRU_LISTEN请求,并使插口准备接收连接,如图22所示。

图22 solisten函数

90~109 在solisten发送PRU_LISTEN请求且pr_usrreq返回后,标识插口处理准备接收连接状态。如果当pr_usrreq返回时有连接正在连接队列中,则不设置SS_ACCEPTCONN标志。

    计算存放进入连接队列的最大值,并赋给so_qlimit。Net/3默认设置下限为0,上限为5(SOMAXCONN)条连接。

tsleep和wakeup函数

    当一个在内核中执行的进程因为得不到内核资源而不能继续执行时,它就调用tsleep等待,tsleep的原型是:

    int tsleep(caddr_t chan, int pri, 

                   char *mesg, int timeo);

    tsleep的第一个参数chan,被称之为等待通道。它标志进程等待的特定资源或事件。许多进程能同时在同一个等待通道上睡眠。当资源可用或者事件出现时,内核调用wakeup,并将等待通道作为唯一的参数传入。wakeup的原型是:

    void wakeup(caddr_t chan);

    所有等待在该通道上的进程均被唤醒,并被设置成运行状态。当每一个进程均恢复执行时,内核安排tsleep返回。

    当进程被唤醒时,tsleep的第二个参数pri指定被唤醒进程的优先级。pri中还包括几个用于tsleep的可选的控制标志。通过设置pri中的PCATCH标志,当一个信号出现时,tsleep也返回。mesg是一个说明调用tsleep的字符串,它将被放在调用报文或者ps的输出中。timeo设置睡眠间隔的上限值,其单位为时钟滴答。

    图23列出了tsleep的返回值。

图23 tsleep的返回值

    因为所有等待在同一等待通道上的进程均被wake唤醒,所以我们总是看到在一个循环中调用tsleep。每一个被唤醒的进程在继续执行之前必须检查等待的资源是否可得到,因为另一个被唤醒的进程可能已经先一步得到了资源。如果仍然得不到资源,进程再调用tsleep等待。

    多个进程在一个插口上睡眠的情况是不多见的,所以通常情况下,一次调用wakeup只有一个进程被内核唤醒。

accept系统调用

    调用listen后,进程调用accept等待连接请求。accept返回一个新的描述符,指向一个连接到客户端的新的插口。原来的插口s是未连接的,准备接收下一个连接。如果name指向一个正确的缓存,accept就会返回对方的地址。

    处理连接的细节由与插口相关的协议来完成。对于TCP而言,当一条连接已经被建立时,即完成三次握手,就通知插口层。对于其它的协议,如OSI的TP4,只要一个连接请求到达,tsleep就返回。当进程通过在插口上发送或者接收数据来显示证实连接后,连接则算完成。

    图24说明了accept的实现。

图24 accept系统调用

106~114 accept有三个参数:s为插口描述符;name为缓存指针,accept将把外部主机的运输地址填入该缓存;anamelen是一个保存缓存大小的指针。

    a. 验证参数

116~134 accept将缓存大小*anamelen赋值给namelen,getsock返回插口的file结构。如果插口还没准备好接收连接,即还没调用listen,或者已经请求了非阻塞的I/O,且没有连接被送入队列,则分别返回EINVAL或EWOULDBLOCK。

    b. 等待连接

135~145 当出现下列情况时,while循环退出:有一条连接到达;出现差错;插口不能再接收数据。当信号被捕获之后(tsleep返回EINTR),accept并不自动重新启动。当协议层通过 sonewconn将一条连接插入队列后,唤醒进程。

    在循环内,进程在tsleep中等待,当有连接到达时,tsleep返回0。如果tsleep被信息中断或插口被设置成非阻塞,则accept返回EINTR或EWOULDBLOCK。

    c. 异步差错

146~151 如果进程在睡眠期间出现差错,则将插口中的差错代码赋给accept中的返回码,清除插口中的差错码后,accept返回。

    异步事件改变插口状态是比较常见的。协议处理层通过设置so_error或唤醒插口上等待的所有进程来通知插口层插口状态的改变。因为这一点,插口层必须在每次被唤醒后检验so_error,查看是否在进程睡眠期间有差错出现。

    d. 将插口同描述符关联

152~164 falloc为新的连接分配一个描述符;调用soqremque将插口从接收队列中删除,放到描述符的file结构中。

    e. 协议处理

167~179 accept分配一个新的mbuf来保存外部地址,并调用soaccept来完成协议处理。在连接处理期间产生的新的插口的分配和排队在下一节描述。如果进程提供了一个缓存来接收外部地址,copyout将地址和地址长度分别从name和namelen中复制给进程。如果有必要,copyout还可能将地址截掉,如果进程提供的缓存不够大。最后,释放mbuf,使能协议处理,accept返回。

    因为仅仅分配了一个mbuf来存放外部地址,运输地址必须能放入一个mbuf中。因为Unix域地址是文件系统中的路径名,最长可达1023个字节,所以要受到这个限制。但是对Internet域中的16字节的sockaddr_in没影响。

    soaccept函数通过协议层获得新的连接的客户端地址,如图25所示。

图25 soaccept函数

184~197 soaccept确保插口与一个描述符相连,并发送PRU_ACCEPT请求给协议。pr_usrreq返回后,nam中包含外部插口的名字。

sonewconn和soisconnected函数

    从图24中可以看出,accept等待协议层处理进入的连接请求,并且将它们放入so_q中。图26利用TCP来说明这个过程。

图26 处理进入的TCP连接

    在图26的左上角,accept调用tsleep等待进入的连接。在左下角,tcp_input调用sonewconn为新的连接产生一个插口来处理进入的TCP SYN。sonewconn将产生的插口放入so_q0排队,因为三次握手还没完成。

    当TCP三次握手协议的最后一个ACK到达时,tcp_input调用soisconnected来更新产生的插口,并将它从so_q0中移动so_q中,唤醒所有调用accept等待进入的连接的进程。

    图的右上角说明我们在图24中描述的函数。当tsleep返回时,accept从so_q中取得连接,发送PRU_ATTACH请求。插口同一个新的文件描述符建立了联系,accept也返回到调用进程。

    图27显示了sonewconn函数。

图27 sonewconn函数

123~129 协议层将head(指向正在接收连接的插口的指针)和connstatus(指示新连接的状态的标志)传给sonewconn。对于TCP而言,connstatus总是等于0。

    a. 限制进入的连接

130~131 当下面的不等式成立时,sonewconn不再接收任何连接:

so_qlen + so_q0len > (3 x so_qlimit) / 2

    该不等式确保listen(fd, 0)时,允许一条连接。

    b. 分配一个新的插口

132~143 一个新的socket结构被分配和初始化。如果进程对处理连接状态的插口调用了setsockopt,则新产生的socket继承好几个插口选项,因为so_options、so_linger、so_pgid和sb_hiwat的值被复制到新的socket结构中。

    c. 排队连接

144 在129行代码中,根据connstatus的值设置soqueue。如果soqueue为0(如TCP连接),则将新的插口插入到so_q0中;若connstatus非0,则将其插入到so_q中。

    d. 协议处理

145~150 发送PRU_ATTACH请求,启动协议层对新的连接的处理。如果处理失败,则将插口从队列中删除并丢弃,然后sonewconn返回一个空指针。

    e. 唤醒进程

151~157 如果connstatus非0,所有在accept中睡眠或者查询插口的可读性的进程均被唤醒。将connstatus对so_state执行或者操作。TCP协议从来不会执行这段代码,因为对于TCP而言,connstatus总是为0.

    某些将进入的连接首先插入so_q0队列中的协议在连接建立阶段完成时调用soisconnected,如TCP。对于TCP,当第二个SYN被应答时,就出现这种情况。

    图28显示了soisconnected的代码。

图28 soisconnected函数

    f. 排队示完成的连接

78~87 通过修改插口的状态来表明连接已经完成。当对进入的连接调用soisconnected(即本地进程正在调用accept)时,head非空。

    如果soqremque返回1,就将插口放入so_q排队,sorwakeup唤醒通过调用select测试插口的可读性来监控插口上连接到达的进程。如果进程在accept中因等待连接而阻塞,则wakeup使得相应的tsleep返回。

    g. 唤醒等待新连接的进程

88~93 如果head为空,就不需要调用soqremque,因为进程用connect系统调用初始化连接,且插口不在此队列中。如果head非空,且soqremque返回0,则插口已经在so_q队列中。在某些协议中,如TP4,就出现这种情况,因为在TP4k ,连接完成之前就已插入到so_q队列中。wakeup唤醒所有阻塞在connect中的进程,sorwakeup和sowwakeup负责唤醒那些调用select等待连接完成的进程。

connect系统调用

    服务器进程调用listen和accept系统调用等待远程进程初始化连接。如果进程想自己初始化一条连接(即客户端),则调用connect。

    对于面向连接的协议如TCP,connect建立一条与指定的外部地址的连接。如果进程没有调用bind来地址,则内核选择并隐式地一个地址到插口。

    对于无连接协议如UDP和ICMP,connect记录外部地址,以便发送数据报时使用。任何以前的外部地址均被新的地址所代替。

    图29显示了UDP和TCP调用connect时涉及到的函数。

图29 connect处理过程

    图29的左边说明connect如果处理无连接协议,如UDP。在这种情况下,协议层调用soisconnected后connect系统调用立即返回。

    图29右边说明connect如何处理面向连接的协议,如TCP。在这种情况下,协议层开始建立连接,调用soisconnecting指示连接将在某个时候完成。如果插口是非阻塞的,soconnect调用tsleep等待连接完成。对于TCP,当三次握手完成时,协议层调用soisconnected将插口标识为已连接,然后调用wakeup唤醒等待的进程,从而完成connect系统调用。

    图30列出了connect系统调用的代码。

图30 connect系统调用

180~188 connect的三个参数(在connect_args结构中):s为插口描述符;name是一个指针,指向存放外部地址的缓存;namelen为缓存的长度。

189~200 getsock获取插口描述符对应的file结构。可能有连接请求在非阻塞的插口上,基出现这种情况,则返回EALREADY。函数sockargs将外部地址从进程复制到内核。

    a. 开始连接处理

201~208 连接是从调用soconnect开始的。如果soconnect报告差错出现,connect跳转到bad。如果soconnect返回时连接还没完成且使能了非阻塞I/O,则立即返回EINPROGRESS以免等待连接完成。因为通常情况下,建立连接要涉及同远程系统交换几个分组,因而这个过程可能需要一些时间才能完成。如果连接没完成,则下次对connect调用就返回 EALREADY。当连接完成时,soconnect返回EISCONN。

    b. 等待连接建立

208~217 while循环直到连接已经建立或出现差错时才退出。splnet防止connect在测试插口状态和调用tsleep之间错过wakeup。循环完成后,error为0或者tsleep中的差错码或者插口中的差错码。

218~224 清除SS_ISCONNECTING标志,因为连接已完成或者连接请求已失败。释放存储外部地址的mbuf,返回差错码。

    图31列出了soconnect函数的代码。soconnect函数确保插口处于正确的连接状态。如果插口没有连接或者连接没有被挂起,则连接请求总是正确的。如果插口已经连接或者连接正等待处理,则新的连接请求将被面向连接的协议(如TCP协议)拒绝。对于无连接协议,如UDP,多个连接是允许的,但是每一个新的请求中的外部地址会取代原来的外部地址。

图31 soconnect函数

198~222 如果插口被标识准备接收连接,则soconnect返回EOPNOTSUPP,因为如果已经对插口调用了listen,则进程不能再初始化连接。如果协议是面向连接的,且一条连接已经被初始化,则返回EISCONN。对于无连接协议,任何已有的同外部地址的联系都被sodisconnect切断。

    PRU_CONNECT请求启动相应的协议处理来建立连接或关联。

    切断无连接插口与外部地址的关联

    对于无连接协议,可以通过调用connect,并传入一个不正确的name参数,如指向内容为全0的结构指针或者大小不对的结构,来丢弃同插口相关的外部地址。sodisconnect删除同插口相的外部地址,PRU_CONNECT返回差错码,如EAFNOSUPPORT或EADDRNOTAVAIL,留下没有外部地址的插口。这种方式虽然有点晦涩,但却是一种比较有用的方式,在无连接插口和外部之间断连,而不是替换。

shutdown系统调用

    shutdown系统调用关闭连接的读通道、写通道或读写通道,如图32所示。对于读通道,shutdown 丢弃 所有进程 还没读走的数据 以及调用 shutdown之后到达的数据 。对于写通道,shutdown使 协议作相应的处理 。对于TCP,所有剩余的数据将被发送,发送完成后发送FIN。这就是TCP半关闭的特点。

    为了删除插口和释放描述符,必须调用close。可以在没有调用shutdown的情况下,直接调用close。同所有描述符一样,当进程结束时,内核将调用close,关闭所有还没有被关闭的插口。

图32 shutdown系统调用

550~557 在shutdown_args结构中,s为插口描述符,how指明关闭连接的方式。图33列出了how和how++的期望值。

图33 shutdown系统调用选项

558~564 shutdown是函数soshutdown的包装函数。由getsock返回与描述符相关联的插口,调用soshutdown,并返回其值。

    关闭连接的读通道是由插口层调用sorflush处理的,写通道的关闭是由协议层的PRU_SHUTDOWN请求处理的。soshutdown函数如图34所示。

图34 soshutdown函数

720~732 如果是关闭插口的读通道,则sorflush丢弃插口接收的缓存中的数据,禁止读连接。如果是关闭插口的写通道,则给协议发送PRU_SHUTDOWN请求。

     图35显示了sorflush的代码。

图35 sorflush函数

733~747 进程等待给缓存加锁。因为SB_NOINTR被设置,所以当中断出现时,sblock并不返回。在修改插口状态时,splimp阻塞网络中断的协议请求处理,因为协议层在接收到进入的分组时可以要访问接收缓存。

    socantrcvmore标识插口拒绝接收进入的分组。将sockbuf结构保存在asb中,当splx恢复中断的,要使用asb。调用bzero清除原始的sockbuf结构,使得接收队列为空。

748~751 当shutdown被调用时,存储在接收队列中的控制信息可能引用了一些内核资源。通过sockbuf结构的副本中的sb_mb仍然可以访问mbuf链。

    如果协议支持访问权限,且注册了一个dom_dispose函数,则调用该函数来释放这些资源。

    当sbrelease释放接收队列中的所有mbuf时,丢弃所有调用shutdown时还没被处理的数据。

    注意,连接的读通道关闭完全由插口层来处理,连接的写通道的关闭通过发送PRU_SHUTDOWN请求交由协议处理。TCP协议收到PRU_SHUTDOWN请求后,发送所有排队的数据,然后发送一个FIN来关闭TCP连接的写通道。

close系统调用

    close系统调用用来关闭各类描述符。当fd是引用对象的最后的描述符时,与对象有关的close函数被调用:

    error = (*fp->f_ops->fo_close)(fp, p)

   如图11所示,插口的fp->f_ops->fo_close是soo_close函数。

1. soo_close函数

    soo_close函数是soclose函数的封装器,如图36所示。

图36 soo_close函数

152~161 如果socket结构与file相关联,则调用soclose,清除f_data,返回差错码。

2. soclose函数

    soclose函数取消插口上所有未完成的连接,等待数据被传输到外部系统,释放不需要的数据结构。

    函数soclose的代码如图37所示。

图37 soclose函数

    a. 丢弃未完成的连接

129~141 如果插口正在接收连接,soclose遍历两个连接队列,并且调用soabort取消每一个挂起的连接。如果协议控制块为空,则协议已同插口分离,soclose跳转到discard进行退出处理。

    soabort发送PRU_ABORT请求给协议,并返回结果。

    b. 断开已建立的连接或关联

142~157 如果插口没有同任何外部地址相连接,则跳转到drop处继续执行。否则,必须断开插口与对等地址之间的连接。如果断连没有开始,则sodisconnect启动断连进程。如果设置了SO_LINGER插口选项,soclose可能要等到断连完成后才返回。对于一个非阻塞的插口,从来不需要等待断连完成,所以在这种情况下,soclose立即跳转到drop。否则,连接终止正在进行且SO_LINGER选项指示soclose必须等待一段时间才能完成操作。直到出现下列情况时while才退出:断连完成;拖延时间(so_linger)到;进程收到一个信息。

    c. 释放数据结构

158~173 如果插口仍然同协议相连,则发送PRU_DETACH请求断开插口与协议的联系。最后,插口被标记为同任何描述符都没有关联,这意味着可以调用sofree释放插口。

    sofree函数代码如图38所示。

图38 sofree函数

    d. 如果插口仍在用则返回

110~114 如果仍然有协议同插口相连,或如果插口仍然同描述符相连,则sofree立即返回。

    e. 从连接队列中删除插口

115~119 如果插口仍在连接队列上(so_head非空),则插口的队列应该为空。如果不空,则插口代码和内核panic中有差错。如果队列为空,清除so_head。

    f. 释放发送和接收队列中的缓存

120~123 sorelease释放发送队列中的所有缓存,sorflush释放接收队列中的所有缓存。最后,释放插口本身。

小结

    本文我们讨论了所有与网络操作有关的系统调用。描述了系统调用机制,并且跟踪系统调用直到它们通过pr_usrreq函数进入协议处理层。

    在讨论插口层时,我们避免涉及地址格式、协议语义和协议实现问题。在后续文章中,我们将通过协议处理层中的Internet协议的实现将链路层处理和插口层处理联系在一起。