看到这个标题你可能会懵圈,没事,只是名字有点恐怖而已。在英文中,它们被称为 scatter read和 gather write.
1. 引例
看下面一段代码:
char buf1[10]; char buf2[20]; char buf3[15]; write(fd, buf1, 10); write(fd, buf2, 20); write(fd, buf3, 15);
上面这样的代码实际上很常见,不知道你是否还记得我们在讲解 TCP_NODELAY 套接字选项的时候,遇到过一种 write-write-read 的情形,和这个很类似。
接下来我想说的是,有没有一种办法,只执行一次 write 就可以把所有 buf 中的数据写入?最容易想到的就是将三个 buf 缓冲中的数据合并成一个 buf,一次写入。linux 为我们提供了另一个更加方便的函数——writev,它意为 write vector,即一次写入很多个 buf,比如可以这样:
writev(fd, buf1, 10, buf2, 20, buf3, 15);
当然啦,上面这种方法只是为了方便描述,实际上 writev 函数的参数是一个结构体数组。
writev 函数和 readv 函数原型:
ssize_t readv(int fd, const struct iovec *iov, int iovcnt); ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
这两个函数参数相同,下面只讲解 writev 函数的用法,readv 函数雷同。
2. writev 函数
- 函数原型
ssize_t writev(int fd, const struct iovec *iov, int iovcnt); struct iovec { void *iov_base; /* 缓冲区地址 */ size_t iov_len; /* 要读写多少字节 */ };
writev 函数的第二个参数是一个 iovec 类型数组,第三个参数是数组的大小。
如果将第 1 节中的程序改为 writev 方式实现,应该像下面这样:
struct iovec iov[3]; iov[0].iov_base = buf1; iov[0].iov_len = 10; iov[1].iov_base = buf2; iov[1].iov_len = 20; iov[2].iov_base = buf3; iov[2].iov_len = 15; writev(fd, iov, 3);
是不是非常简单?writev 有一个好处是它的操作是原子的,意味着它在内部会将所有缓冲区中的数据一次性发送出去,对于 UDP 来说,它只产生一个 UDP 数据报。
3. writev 函数示例
下面这段程序,将 buf 缓冲中的下标为偶数位置数据写入文件 filename 中。
void write_routine() { int i, fd, nw; struct iovec iov[10]; char buf[64]; strcpy(buf, "abcdefghijklmnopqrstuvwxyz"); for (i = 0; i < 10; ++i) { iov[i].iov_base = buf + 2*i; iov[i].iov_len = 1; } fd = open(filename, O_WRONLY | O_CREAT, 0666); if (fd < 0) ERR_EXIT("open"); nw = writev(fd, iov, 10); if (nw < 0) ERR_EXIT("writev"); }
最后,文件 filename 中的内容是:
acegikmoqs
4. 程序运行结果
代码托管在 gitos 上,请使用下面的命令获取:
git clone https://git.oschina.net/ivan_allen/unp.git
如果你已经 clone 过这个代码了,请使用 git pull 更新一下。本节程序所使用的程序路径是 unp/program/advcio/readv_writev/rwv.cc
.
程序 rwv 包含了 writev 和 readv 的例子,使用命令行参数控制。
- readv 演示
readv 从文件 filename 中读取数据,并写入到 iovec 指定的缓冲区中。在此 demo 中,将数据定入到了 buf 缓冲区中下标为偶数的位置中。
图1 readv 演示结果
- writev 演示
writev 函数示例和第 3 节中描述的一致。
图2 writev 函数演示
5. 总结
- 掌握 writev 和 readv 函数