本文要点
-
引言
-
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来获取连接信息。