From: http://blog.chinaunix.net/uid-26822401-id-3158225.html
readv和write函数让我们在单个函数调用里从多个不连续的缓冲里读入或写出。这些操作被称为分散读(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。
两个函数的第二个参数都是一个iovec结构体数组的指针:
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
iov数组里的元素数量由iovcnt指定。它限制于IOV_MAX(第二章)。下图显示了这两个函数的参数和iovec结构体的关系:
writev函数把缓冲的输出数据按顺序集合到一起:iov[0]、iov[1]、到iov[iovcnt-1];writev返回输出字节的总数量,它应该等于所有缓冲长度的和。
readv函数把数据按顺序分散到缓冲里,问题在处理下一个缓冲时填满第一个。readv返回被读的字节总数。如果没有更多数据和碰到文件末尾时返回0的计数。
这两个函数起源于4.2BSD,后来加入到SVR4。这两个函数被SUS的XSI扩展包含。
尽管SUS定义了缓冲地址为一个void *,然而许多未跟上标准的实现仍使用char *代替。
在20.8节里,函数_db_writeidx里,我们需要连续地写两个缓冲到一个文件。第二个要输出的缓冲是一个传给调用者的参数,而第一个缓冲是我们创建的,包含第二个缓冲的长度和文件里其它信息的一个文件偏移量。我们可以有三种方法做这个。
1、调用write两次,一个缓冲一次;
2、分配一个我们自己的缓冲,它足够大来包含两个缓冲,然后把两者拷贝到新缓冲。我们然后为这个新缓冲调用write一次。
3、调用writev来输出两个缓冲。
我们在20.8节使用的解决方案是使用writev,但是把它跟其它两种方案比较是有指导性的。
下表显示了提到的三种方法的结果。
操作 | Linux (Intel x86) | Mac OS X (PowerPC) | ||||
用户 | 系统 | 时钟 | 用户 | 系统 | 时钟 | |
两次write | 1.29 | 3.15 | 7.39 | 1.60 | 17.40 | 19.84 |
缓冲拷贝,然后一次write | 1.03 | 1.98 | 6.47 | 1.10 | 11.09 | 12.54 |
一次writev | 0.70 | 2.72 | 6.41 | 0.86 | 13.58 | 14.72 |
我们测量的测试程序输出一个100字节的头,接着一个200字节的数据。这被完成1048576次,产生一个300M的文件。测试程序有三个分离的条件--上表中每个测量的技术是一个。我们使用times(8.16节)来得到用户CUP时间,系统CPU系统和挂钟时间,在write前后。所有三个时间以秒显示。
正如我们意料的,系统时间当我们调用两次write时增加,对比于一次的write或writev。这和3.9节的测量结果对应。
接着,注意CPU时间的总和(用户加上系统),当我们执行缓冲拷贝接着单个write比单个writev调用要少。对于单个write,我们在用户层拷贝缓冲到一个分段运输的缓冲,然后在我们调用write时内核会把数据拷贝到它内部的缓冲。对于writev,我们应该做更少的拷贝,因为内核只需要直接把数据拷贝到它的分段运输缓冲里。然而,为如此少的数据使用writev的固定花费,比所得要大。当我们需要拷贝的数据量增加时,在我们程序里拷贝缓冲会便耗时,而writev替代地将更具吸引力。
小必不要被上表中Linux相对于Mac的性能影响太多。这两个电脑非常不同:它们有不同的处理器架构、不同的RAM量、不同速度的磁盘。为了公平比较两个操作系统,我们需要使用相同的硬件。
总而言之,我们应该总是尝试使用所需的最少次数的系统调用来完成工作。如果我们写少量数据,那么我们将发现自己拷贝数据和使用单个write而不是使用writev会更不耗时。然而,我们可能发现,性能的好处比不上需要管理我们自己的分段运输缓冲的复杂性代价。