概说《TCP/IP详解 卷2》第17章 插口选项

662 阅读14分钟
原文链接: mp.weixin.qq.com

本文要点

  • 引言

  • setsockopt系统调用

  • getsockopt系统调用

  • fcntl和ioctl系统调用

    • fcntl代码

    • ioctl代码

  • getsockname系统调用

  • getpeername系统调用

  • 小结

引言

    本文主要讨论几个修改插口行为的系统调用。setsockopt和getsockopt系统调用已经在概说《TCP/IP详解 卷2》第8章 IP:网际协议中介绍过,主要描述访问IP特点的选项。在本文中,将介绍这两个系统调用的实现以及通过它们来控制的插口级选项。

    ioctl函数在概说《TCP/IP详解 卷2》第4章 接口:以太网中已经描述了用于网络接口配置的与协议无关的ioctl命令。在 概说《TCP/IP详解 卷2》第6章 IP编址中介绍了用来分配网络掩码以及单播、多播和目的地址的IP专用的ioctl命令。本文我们将介绍ioctl的实现和fcntl函数的相关特点。

    最后,我们介绍getsockname和getpeername系统调用,它们用来返回插口和连接的地址信息。

    图1列出了实现插口选项系统调用的函数,本文介绍带阴影的函数。

图1 setsockopt和getsockopt系统调用(图中两个均为getsocketopt有误)

setsockopt系统调用

    图2列出了setsockopt函数在SOL_SOCKET级的选项。

图2 setsockopt和getsockopt选项

    setsockopt函数原型为:

    int setsockopt(int s, int level, int optname, 

                            void *optval, int optlen);

    图3显示了setsockopt调用的源代码。

图3 setsockopt系统调用

565~597 getsock返回插口描述符的file结构。如果val非空,则将valsize个字节的数据从进程复制到m_get分配的mbuf中。与选项对应的数据长度不能超过MLEN个字节,所以,如果valsize大于MLEN,则返回EINVAL。调用sosetopt,并返回其值。

    sosetopt函数处理所有插口级的选项,并将其它的选项传给与插口关联的协议的pr_ctloutput函数。图4列出了sosetopt函数的部分代码。

图4 sosetopt函数

752~764 如果选项不是插口级的选项,则给底层协议发送PRCO_SETOPT请求。注意:调用的是协议的pr_ctloutput函数,而不是它的pr_usrreq函数。图5说明了Internet协议调用的pr_ctloutput函数。

图5 pr_ctloutput函数

765 switch语句处理插口级的选项。

841~844 对于不认识的选项,在保存它的mbuf被释放后返回ENOPROTOOPT。

845~855 如果没有出现差错,则总是会执行到switch。在switch语句中,如果协议层需要响应请求或者插口层,则将选项传送给相应的协议。Internet协议中没有一个预期处理插口级的选项。

    注意,如果协议收到未预期的选项,则直接将其pr_ctloutput函数的返回丢弃。并将m置空,以免调用m_free,因为协议负责缓存。

    图6说明了linger选项和在插口结构中设置单一标志的选项。

图6 sosetopt函数:linger和标志选项

766~772 linger选项要求进程传入linger结构:

    struct linger {

        int l_onoff;

        int l_linger;

    }

    确保进程已传入长度为linger结构大小的数据后,将结构成员l_linger复制到so_linger中。在下一组case语句后决定是使能还是关闭该选项。so_linger和close系统在概说《TCP/IP详解 卷2》第15章 插口层已介绍过。

773~789 当进程传入一个非0值时,设置选项对应的布尔标志;当进程传入的是0时,将对应标志清除。第一次检查确保一个整数大小(或更大)的对象在mbuf中,然后设置或对应的选项。

    图7显示了插口缓存选项的处理。

图7 sosetopt函数:linger和标志选项

790~815 这组选项改变插口的发送和接收缓存的大小。第一个if语句确保提供给四个选项的变量是整型。对于SO_SNDBUF和SO_RCVBUF,sbreserve只调整缓存的高水位标记而不分配缓存。对于SO_SNDLOWAT和SO_RCVLOWAT,调整缓存的低水位标记。

    图8说明超时选项。

图8 sosetopt函数:超时选项

816~824 进程在timeval结构中设置SO_SNDTIMEO和SO_RCVTIMEO选项的超时值。如果传入的数值不正确,则返回EINVAL。

825~830 存储在timeval结构中的时间间隔不能太大,因为sb_timeo是一个短整数,当时间间隔值的单位为一个时间滴答时,时间间隔值的大小就不能走过一个短整数的最大值。

    第826行代码是不正确的。在下列条件下,时间间隔不能表示为一个短整数:

tv_sec x hz + (tv_usec/tick) > SHRT_MAX

其中,tick=1000000/hz,SHRT_MAX=32767,所以如果下列不等式成立,则返回。

tv_sec > SHRT_MAX/hz - tv_usec/(tick x hz) = SHRT_MAX/hz - tv_usec/100000

等式的最后一项不是代码指明的hz。正确的测试代码应该是:

    if (tv-tv_sec * hz + tv->tv_usec/tick){

        error=EDOM;

    }

831~840 将转换后的时间,val,保存在请求的发送或接收缓存中。sb_timeo限制了进程等待接收缓存中的数据或发送缓存中的闲置空间的时间。

    超时值是传给tsleep的最后一个参数,因为tsleep要求超时值为一个整数,所以进程最多只能等待65535个时钟滴答。假设时钟频率为100Hz,则等待时间小于11分钟。

getsockopt系统调用

    getsockopt返回进程请求的插口和协议选项,函数原型是:

    int getsockopt(int s, int level, int name, 

                            caddr_t val, int *valsize);

    该调用的源代码如图9所示。

图9 getsockopt系统调用

598~633 getsock获取插口的file结构,将选项缓存的大小复制到内核,调用sogetopt来获取选项的值。将sogetopt返回的数据复制到进程提供的缓存,可能还需要修改缓存长度。如果进程提供的缓存不够大,则返回的数据可能会被截掉。通常情况下,存储选项数据的mbuf在函数返回后被释放。

    同sosetopt一样,sogetopt函数处理所有插口级的选项,并将其它的选项传给与插口关联的协议。图10列出了sogetopt函数的开始和结束部分的代码。

图10 sogetopt函数:概述

856~871 同sosetopt一样,函数将那些与插口级选项无关的选项立即通过PRCO_GETOPT协议请求传给相应的协议层。协议将被请求的选项保存在mp指向的mbuf中。

    对于插口级的选项,分配一块标准的mbuf缓存来保存选项值,选项值通常是一个整数,所以将m_len设成整数大小。相应的选项值通过switch语句复制到mbuf中。

918~925 如果执行的是switch中的default情况下的语句,则释放mbuf,并返回ENOPROTOOPT。否则,switch语句执行完成后,将指向mbuf的指针赋给*mp。当函数返回后,getsockopt从该mbuf中将数据复制到进程提供的缓存,并释放mbuf。

    图11说明了对SO_LINGER选项和作为布尔型标志实现的选项的处理。

图11 sogetopt选项:SO_LINGER选项和布尔选项

872~877 SO_LINGER选项请求返回两个值:一个是标志值,赋给l_onoff;另一个是拖延时间,赋给l_linger。

878~887 其余的选项作为布尔标志实现。将so_options和optname执行逻辑与操作如果选项被打开,则与操作的结构为非0值;反之则结果为0。注意:标志被打开并不表示返回值等于1。

    sogetopt的下一部分代码(图12)将整型值选项的值复制到mbuf中。

图12 sogetopt函数:整型值选项

888~906 将每一个选项作为一个整数复制到mbuf中。注意:有些选项在内核中是作为一个短整数存储的(如缓存高低水位标记),但是作为整数返回。一旦将so_error复制到mbuf中后,即清除so_error,这是唯一的一次getsockopt调用修改插口状态。

    图13列出了sogetopt的第三和第四部分代码,它们的作用分别是处理SO_SNDTIMEO和SO_RCVTIMEO选项。

图13 sogetopt函数:超时选项

907~917 将发送或接收缓存中的sb_timeo值赋给var。基于val中的时钟滴答数,在mbuf中构造一个timeval结构。

    计算tv_usec的代码有一个差错。表达式应该为:

(val % hz) * tick

fcntl和ioctl系统调用

    由于历史原因,插口API的几个特点既能通过ioctl也能通过fcntl来访问。图14显示了本文描述的函数。

图14 fcntl和ioctl函数

    ioctl和fcntl的原型分别为:

    int ioctl(int fd, unsigned long result, char *argp)

    int fcntl(int fd, int cmd, ...)

    图15总结了这两个系统调用与插口有关的特点;同时还列出了一些传统的常数,因为它们出现在代码中。

图15 fcntl和ioctl命令

1. fcntl代码

    图16列出了fcntl函数的部分代码。

图16 fcntl系统调用:概况

133~153 验证完指向打开文件的描述符的正确性后,switch语句处理请求的命令。

253~257 对于不能识别的命令,fcntl返回EINVAL。

    图17仅显示fcntl中与插口有关的代码。

图17 fcntl系统调用:插口处理

168~185 F_GETFL返回与描述符相关的当前文件状态标志,F_SETFL设置状态标志。通过调用fo_ioctl将FNONBLOCK和FASYNC的新设置传递给对应的插口,而插口的新设置是通过图19中描述的soo_ioctl函数来传递的。只有在第二个so_ioctl调用失败后,才第三次调用fo_ioctl。该调用的功能是清除FNONBLOCK标志,但是应该改为将这个标志恢复到原来的值。

186~194 F_GETOWN返回与插口相关联的进程或者进程组的标识符,so_pgid。对于非插口描述符,将TIOCGPGRP ioctl命令传给对应的fo_ioctl函数。F_SETOWN的功能是给so_pgid赋一个新值。

2. ioctl代码

    我们跳过ioctl系统调用本身而无从soo_ioctl开始讨论,如图19所示,因为ioctl的代码中大部分是从图16所示的代码中复制的。我们已经介绍过,soo_ioctl函数将选路命令发送给rtioctl,接口命令发送给ifioctl,其它任何命令发送给底层协议的pr_usrreq函数。

55~68 有几个命令是由soo_ioctl直接处理的。如果*data非空,则FIONBIO打开非阻塞方式,否则关闭非阻塞方式。正于我们了解的,这个标志将影响到accept、connect和close系统调用,也包括其它的读和写系统调用。

69~79 FIOASYNC使能或禁止异步I/O通知功能。如果设置了SS_ASYNC,则无论什么时候插口上有活动,就调用sowakeup,将信号SIGIO发送给相应的进程或进程组。

80~88 FIONREAD返回接收缓存中的可读字节数。SIOCSPGRP设置与插口相关的进程组,SIOCGPGRP则是得到它。so_pgid作为我们刚讨论过的SIGGIO信号的目标进程或进程组,当有外带灵气到达插口时,则作为SIGURG信号的目标进程或进程组。

89~92 如果插口正处于外带数据的同步标记,则SIOCATMARK返回真;否则返回假。

    ioctl命令,FIOxxx和SIOxxx常量,有一个内部结构,如图18所示。

图18 ioctl命令的内部结构

图19 soo_ioctl函数

    如果将ioctl的第三个参数作为输入,则设置input。如果该参数作为输出,则output被置位。如果不用该参数,void被置位。length是参数的字节数。相关的命令在同一个group中但每个命令在组中都有各自的number。图20中的宏用来解析ioctl命令中的元素。

图20 ioctl命令宏

93~104 宏IOCGROUP从命令中得到8bit的group。接口命令由ifioctl处理。选路命令由rtioctl处理。通过PRU_CONTROL请求将的有其它命令传递给插口协议。

getsockname系统调用

    getsockname系统调用的原型是:

    int getsockname(int fd, caddr_t asa, int *alen)

    getsockname得到绑定在插口fd上的本地地址,并将之存入asa指向的缓存中。当在一个隐式的绑定内核选择了一个地址,或在一个显式的bind调用中进程指定了一个通配符地址时,该函数就很有用。getsockname系统调用如图21所示。

图21 getsockname系统调用

682~715 getsock返回描述符的file结构。将进程指定的缓存的长度赋值给len。这是我们第一次看到对m_getclr的调用:该函数分配一个标准mbuf,并调用bzero清零。当协议收到PRU_SOCKADDR请求时,协议处理层负责将本地地址存入m。

    如果地址长度大于进程提供的缓存的长度,则返回的地址将被截断。*alen等于实际返回的字节数,最后释放mbuf并返回。

getpeername系统调用

    getpeername系统调用的原型是:

    int getpeername(int fd, caddr_t asa, int *alen)

    getpeername系统调用返回指定插口上连接的远端地址。当一个调用accept的进程通过fork和exec启动一个服务器时,经常要调用这个函数。服务器不能得到accept返回的远端地址,而只能调用getpeername。通常,要在应用的访问地址表查找返回地址,如果返回地址不在访问表中,则连接将被关闭。

    某些协议,如TP4,利用这个函数来确定是否拒绝或证实一个进入的连接。在TP4中,accept返回的插口上的连接是不完整的,必须经证实之后才算连接成功。基于getpeername返回的地址,服务器能够关闭连接或通过发送或接收数据来间接证实连接。这一特点与TCP无关,因为TCP必须在三次握手完成后,accept才能建立连接。图22列出了getpeername函数的代码。

图22 getpeername系统调用

719~753 getsock获取插口对应的file结构,如果插口还没同对方建立连接或连接还同证实(如TP4),则返回ENOTCONN。如果已建立连接,则从进程那里得到缓存的大小,并分配一块mbuf来存储地址。发送PRU_PEERADDR请求给协议层来获取远端地址。将地址和地址的长度从内核中的mbuf中复制到进程提供的缓存中,最后释放mbuf并返回。

小结

    本文讨论了六个修改插口功能的函数。插口选项由setsockopt和getsockopt函数处理。其它的选项,不仅仅限于插口,由fcntl和ioctl处理。最后,通过getsockname和getpeername来获取连接信息。