高级IO函数

在I/O操作上设置超时

在涉及套接字的I/O操作上设置超时的方法有以下三种。
(1)调用alarm,它在指定超时期满时产生SIGALRM信号。这个方法涉及信号处理,而信号处理在不同的实现上存在差异,而且可能干扰进程中现有的alarm调用。
(2)在select中阻塞等待I/O (select有内置的时间限制),以此代替直接阻塞在read或 write 调用上。
(3)使用较新的SO_RCVTIMEO和SO_SNDTIMEO套接字选项。这个方法的问题在于并非所有实现都支持这两个套接字选项。

前两个技术适用于任何描述符,而第三个技术仅仅使用于套接字描述符。

recv和send函数

#include<sys/socket.h>

ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

ssize_t send(int sockfd, const void *buff,size_t nbytes,int flags);
	返回:若成功则为读入或写入的字节数,若出错则为-1

flags参数的值或为0,或为图14-6列出的一个或多个常值的逻辑或。
高级IO函数
MSG_DONTROUTE 本标志告知内核目的主机在某个直接连接的本地网络上,因而无需执行路由表查找。
MSG_DONTWAIT 本标志在无需打开相应套接字的非阻塞标志的前提下,把单个I/O操作临时指定为非阻塞,接着执行I/O操作,然后关闭非阻塞标志。
MSG_OOB 对于send,本标志指明即将发送带外数据。对于recv,本 标志指明即将读入的是带外数据而不是普通数据。
MSG_PEEK 本标志适用于recv和recvfrom,它允许我们查看已可读取的数据,而且系统不在recv,recvfrom返回后丢弃这些数据。
MSG_WAITALL 它告知内核不要在尚未读入请求数目的字节之前让一个读操作返回。注意:即使指定了MSG顼APTALL,如果发生下列情况之一:(a)捕获一个信号,(b)连接被终止,©套接字发生一个错误,相应的读函数仍有可能返回比所请求字节数要少的数据。

readv和writev函数

这两个函数类似read和write,不过readvffwritev允许单个系统调用读入到或写出自一 个或多个缓冲区。**这些操作分别称为分散读(scatter read)和集中写(gather write),**因为来自读操作的输入数据被分散到多个应用缓冲区中,而来自多个应用缓冲区的输出数据则被集中提供给单个写操作。

#include<sys/uio.h>

ssize_t readv(int filedes, const struct iovec *iov,int iovcnt);
ssize_t writev(int filedes,const struct iovec *iov,int iovcnt);

	返回:若成功则为读入或写出的字节数,若出错则为-1
struct iovec{
	void *iov_base;
	size_t iov_len;
	}

这两个函数的第二个参数都是指向某个iovec结构数组的一个指针,其中iovec结构在头文件<sys/uio.h>中定义。
iovec结构数组中元素的数目存在某个限制,具体取决于实现。举例来说,4.3BSD和Linux 均最多允许1024个,而HP-UX最多允许2100个。POSIX要求在头文件<sys/uio.h>中定义 IOV_MAX常值,而且其值至少为16。
readv和writev这两个函数可用于任何描述符,而不仅限于套接字。另外writsev是一个原子操作,意味着对于一个基于记录的协议(例如UDP)而言,一次writev调用只产生单个UDP 数据报。
一个4字节的write跟一个396字节的write可能触发Nagle算法,首选办法之一是针对这两个缓冲区调用 writev。

recvmsg和sendmsg函数

这两个函数是最通用的I/O函数。实际上我们可以把所有read、readv、recv和recvfrom调用替换成recvmsg调用。类似地,各种输出函数调用也可以替换成sendmsg调用。

#include<sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr *msg,int flags);
	返回:若成功则为读入或写出的字节数,若出错则为-1

struct msghdr{
	void		 *msg_name;		/* protocol address*/
	socklen_t	  msg_namelen;	/* size of protocol address*/
	struct iovec *msg_iov;		/* scatter/gather array*/
	int			  msg_iovlen;		/* # elements in msg_iov*/
	void		 *msg_control;	/* ancillary data (cmsghdr struct)*/
	socklen_t	  msg_controllen;	/* length of ancillary data*/
	int			  msg_flags;		/* flags returned by recvmsg()*/
	}

msg_name和msg_namelen这两个成员用于套接字未连接的场合(譬如未连接UDP套接字)。它们类似recvfrom和sendto的第五个和第六个参数: msg_name指向一个套接字地址结构,调用者在其中存放接收者(对于sendmsg调用)或发送者(对于recvmsg调用)的协议地址如果无需指明协议地址(例如对于TCP套接字或已连接UDP套接字),msg_name应置为空指针。msg_namelen对于sendmsg是一个值参数,对于recvmsg却是一个值-结果参数。
msg_iov和msg_iovlen这两个成员指定输入或输出缓冲区数组(即iovec结构数组),类似readv或writev的第二个和第三个参数。msg_control和msg_controllen这两个成员指定可选的辅助数据的位置和大小。msg_controllen对于recvmsg是一个值-结果参数。
对于recvmsg和sendmsg,我们必须区别它们的两个标志变量,一个是传递值的flags参数,另一个是所传递msghdr结构的msg_flags成员,它传递的是引用,因为传递给函数的是该结构的地址。

  • 只有recvmsg使用msg_flags成员。recvmsg被调用时,flags参数被复制到msg_flags成员(TCPv2第502页),并由内核使用其值驱动接收处理过程。内核还依据recvmsg的结果更新msg_flags成员的值。
  • sendmsg则忽略msg_flags成员,因为它直接使用flags参数驱动发送处理过程。这一点意味着如果想在某个sendmsg调用中设置MSG_DONTWAIT标志,那就把flags参数设置为该值,把msg flags成员设置为该值不起作用。
    图14-7汇总了内核为输入和输出函数检查的flags参数值以及recvmsg可能返回的msg_flags成员值。其中没有sendmsg msg_flags一栏,因为我们已提及本组合无效。

高级IO函数
这些标志中,内核只检查而不返回前4个标志,既检查又返回接下来的2个标志,不检查而 只返回后4个标志。recvmsg返回的7个标志解释如下。

MSG_BCAST 本标志随BSD/OS引入,相对较新。它的返回条件是本数据报作为链 路层广播收取或者其目的IP地址是一个广播地址。与工P_RECVD- STADDR 套接字选项相比,本标志是用于判定一个UDP数据报是否发 往某个广播地址的更好方法。
MSG_MCAST 本标志随BSD/OS引入,相对较新。它的返回条件是本数据报作为链 路层多播收取。
MSG_TRUNC 本标志的返回条件是本数据报被截断,也就是说,内核预备返回的数据超过进程事先分配的空间(所有成员之和)。
MSG_CTRUNC 本标志的返回条件是本数据报的辅助数据被截断,也就是说,内核 预备返回的辅助数据超过进程事先分配的空间(msg_controllen)0
MSG_EOR 本标志的返回条件是返回数据结束一个逻辑记录。TCP不使用本标 志,因为它是一个字节流协议。
MSG_OOB 本标志绝不为TCP带外数据返回。它用于其他协议族(例如OSI协议族)。
MSG.NOTIFICATION 本标志由SCTP接收者返回,指示读入的消息是一个事件通知,而不是数据消息。

高级IO函数
图中给协议地址分配了16字节,给辅助数据分配了20字节。为缓冲数据初始化了一个由3个iovec,结构构成的数组:第一个指定一个100字节的缓冲区,第二个指定一个60字节的缓冲区,第三个指定一个80字节的缓冲区。我们还假设已为这个套接字设置了IP_RECVDSTADDR套接字选项,以接收所读取UDP数据报的目的IP地址。
我们接着假设从192.6.38.100端口2000到达一个170字节的UDP数据报,它的目的地是我们的UDP套接字,目的IP地址为206.168.112.96。图14-9展示了reawmsg,返回时msghdr结构中的所有信息。

高级IO函数

图中被recvmsg修改过的字段标上了阴影。从图14-8到图14-9的变动包括以下几点。

  • 由msg_name成员指向的缓冲区被填以一个网际网套接字地址结构,其中有所收到数据报的源IP地址和源UDP端口号。
  • msg_namelen成员(一个值-结果参数)被更新为存放在msg_name所指缓冲区中的数据量。本成员并无变化,因为recvmsg调用前和返回后其值均为16。
  • 所收取数据报的前100字节数据存放在第一个缓冲区,中60字节数据存放在第二个缓冲区,后10字节数据存放在第三个缓冲区。最后那个缓冲区的后70字节没有改动。recvmsg函数的返回值(即170)就是该数据报的大小。
  • 由msg_control成员指向的缓冲区被填以一个cmsghdr结构。(我们将在14.6节详细讨论辅助数据,在22.2节详细讨论IP_RECVDSTADDR套接字选项。)该cmsghdr结构中,cmsg_len成员值为16,cmsg_level成员值为IPPROTO_IP,cmsg_type成员值为IP_RECVDSTADDR,随后4字节存放所收到UDP数据报的目的IP地址。这个20字节缓冲区的后4字节没有改动。
  • msg_controllen成员被更新为所存放辅助数据的实际数据量。本成员也是一个值-结果参数,recvmsg返回时其结果为16。
  • msg_flags成员同样被recvmsg更新,不过没有标志返回给进程。
    图14-10汇总了我们已讲述的5组IO函数之间的差异。
    高级IO函数

辅助数据

辅助数据(ancillary data)可通过调用sendmsg和recvmsg这两个函数,使用msghdr结构中的msg_control和msg_controllen这两个成员发送和接收。辅助数据的另一个称谓是控制信息。
高级IO函数
辅助数据由一个或多个辅助数据对象(ancillary data object)构成,每个对象以一个定义在 头文件<sys/socket.h>中的cmsghdr结构开头。

struct cmsghdr{
	socklen_t cmsg_len;		/*	length in bytes, including	this structure */
	int		  cmsg_level;	/*	originating protocol */
	int		  cmsg_type;	/*	protocol-specific type */
		/* followed by unsigned char cmsg_data[] */
	}

由msg_control指向的辅助数据必须为csmghdr结构适当地对齐。
高级IO函数
msg_control指向第一个辅助数据对象,辅助数据的总长度则由msg_controllen指定。每 个对象开头都是一个描述该对象的cmsghdr结构。在cmsg_type成员和实际数据之间可以有填充字节,从数据结尾处到下一个辅助数据对象之前也可以有填充字节。
图14-13展示了通过一个Unix域套接字传递描述符(15.7节)或传递凭证(15.8节)时所用 cmsghdr结构的格式。
图14-13中我们假设cmsghdr结构的每个成员(总共3个)都占用4字节,而且在cmsghdr结 构和实际数据之间没有填充字节。当传递描述符时,数组的内容是真正的描述符值。 图中只展示了一个待传递的描述符,然而一般总能传递多个描述符(这种情况下cmsg_len的值为12加上4乘以描述符的数目,这里假设每个描述符占据4字节)。
高级IO函数
由recvmsg返回的辅助数据可含有任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充字节,头文件<sys/socket.h>中定义了以下5个宏,以简化对辅助数据的处理。

#include<sys/socket.h>
#include<sys/param.h>

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr);
	返回:指向第一个cmsghdr结构的指针,若无辅助数据则为NULL
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsgptr);
	返回:指向下一个cmsghdr结构的指针,若不再有辅助数据对象则为NULL
unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr);
	返回:指向与cmsghdr结构关联的数据的第一个字节的指针
unsigned int CMSG_LEN(unsigned int length);
	返回:给定数据量下存放到cmsg_len中的值
unsigned int CMSG_SPACE(unsigned int length);
	返回:给定数据量下一个辅助数据对象总的大小

CMSG_LEN和CMSG_SPACE的区别在于,前者不计辅助数据对象中数据部分之后可能的填充 字节,因而返回的是用于存放在cmsg_len成员中的值,后者计上结尾处可能的填充字节,因而返回的是为辅助数据对象动态分配空间的大小值。

排队的数据量

有3个技术可用于获悉己排队的数据量。
(1)如果获悉己排队数据量的目的在于避免读操作阻塞在内核中(因为没有数据可读时我们 还有其他事情可做),那么可以使用非阻塞式I/O。
(2) 如果我们既想查看数据,又想数据仍然留在接收队列中以供本进程其他部分稍后读取, 那么可以使用MSG_PEEK标志。如果我们想这样做,然而不能肯定是否真有数据可读, 那么可以结合非阻塞套接字使用该标志,也可以组合使用MSG_DONTWAIT标志和MSG_PEEK标志。
(3) 一些实现支持ioctl的FIONREAD命令。该命令的第三个ioctl参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接字接收队列的当前字节数(TCPv2第553页)。该值是已排队字节的总和,对于UDP套接字而言包括所有己排队的数据报。还要注意的是,在源自Berkeley的实现中,为UDP套接字返回的值还包括一个套接字地址结构的空间,其中含有发送 者的IP地址和端口号(对于IPv4为16字节,对于IPv6为24字节)。

套接字和标准I/O

标准I/O函数可用于套接字,不过需要考虑以下几点。
•通过调用fdopen,可以从任何一个描述符创建出一个标准I/O流。类似地,通过调用fileno,可以获取一个给定标准I/O流对应的描述符。我们第一次遇到fileno是在图6-9 中,当时我们想在一个标准I/O流上调用select。select只能用于描述符,因此我们不得不获取那个标准I/O流的描述符。
• TCP和UDP套接字是全双工的。标准I/O流也可以是全双工的:只要以匕+类型打开流即可, r+意味着读写。然而在这样的流上,我们必须在调用一个输出函数之后插入一个fflush、 fseek、fsetpos或rewind调用才能接着调用一个输入函数。类似地,调用一个输入函数后也必须插入一个fseek、fsetpos或:rewind调用才能调用一个输出函数,除非输入函数遇到一个EOF。fseek、fsetpos和rewind这3个函数的问题是它们都调用Iseek, 而Iseek用在套接字上只会失败。
•解决上述读写问题的最简单方法是为一个给定套接字打开两个标准I/O流:一个用于读, 一个用于写。

来源:UNIX网络编程卷一第十四章

上一篇:Redis中String数据类型的SDS设计精妙


下一篇:测试服mysql突然崩溃