本文整合自三篇参考资料,具体引用见文末。
iovec结构体
struct iovec定义了一个向量元素。通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。且iovec结构是用于scatter/gather IO的。readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。 iovec结构体的定义如下:
代码语言:javascript复制struct iovec {
/* Starting address (内存起始地址)*/
void *iov_base;
/* Number of bytes to transfer(这块内存长度) */
size_t iov_len;
};
linux中使用这样的结构体变量作为参数的函数很多,常见的有:
代码语言:javascript复制 #include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt,off_t offset);
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
简介readv()和writev()
readv和writev作为read与write函数的衍生函数,在一个原子操作中读取或是写入多个缓冲区。readv和writev函数中的各参数的含义如下: 函数原型:
123 | ssize_t readv(int fd, const struct iovec *iov, int iovcnt);ssize_t writev(int fd, const struct iovec *iov, int iovcnt); |
---|
这两个函数需要三个参数:
- 要在其上进行读或是写的文件描述符fd
- 读或写所用的I/O向量(vector)
- 要使用的向量元素个数(count)
这些函数的返回值是readv所读取的字节数或是writev所写入的字节数。如果有错误发生,就会返回-1,而errno存有错误代码。注意,也其他I/O函数类似,可以返回错误码EINTR来表明他被一个信号所中断。
举个例子,对于readv(),假如传入3个缓冲区每个缓冲区大小为16,当前文件偏移量为20,那么内核会把当前文件的偏移量[20,36)存入iovec[0],[36,52)存入iovec[1],[52,68)存入iovec[2].
对于写入操作同理。
readv()系列详解
read和pread是最基础的对文件读取的系统调用。read会从描述符为fd的文件中读取count个字节存入buf中,而pread则是从描述符为fd的文件中,从offset位置开始,读取count个字节存入buf中。如果读取成功,这两个系统调用都将返回读取的字节数。因此,这两个系统调用主要的区别就在于读取的位置,其它功能均类似。
有几点需要注意:
首先,是从哪开始读。pread64没有问题,就是从offset的位置开始读。而对于read,如果它读取的描述符对应的文件支持seek,那么它是从文件描述符中存储的文件偏移(file offset)处继续读。
对于支持seek的文件来说,read是从文件偏移的位置继续读,pread是从offset的位置开始读。
我们知道,更改文件偏移有单独的系统调用lseek,因此,如果我们要从某个特定的位置读取数据,可以lseek read,也可以pread。但是,系统调用实际上是一个复杂的耗时操作,所以pread就用一次系统调用解决了两个系统调用的问题。
第二,是读多少的问题。read和pread读取的字节数一定不大于count,但有可能小于count。假设说我们的二进制文件只有上述6个字节。那么,如果我们read了8个字节,后2个字节自然是无法被读取的。因此,只能读取到6个字节,read也将返回6。除此之外,还有很多可能会让read和pread读取的字节小于count。比如说,从一个终端读取(输入的字节小于其需求的字节),或者在读取时被某些信号中断。
此外,除了读的字节小于count之外,read和pread还有可能读取失败。此时的返回值将是-1。我们可以用errno查看其错误。文件描述符不可读或无效(EBADF),buf不可使用(EFAULT),文件描述符是目录而非文件(EISDIR)等等,这些都有可能直接造成读取的错误。对于以非阻塞形式打开的文件,还可能返回EAGAIN或EWOULDBLOCK,详情请见open。
第三,是读完当read或pread读取结束后的工作。read会更新文件描述符中的文件偏移,它们读了多少字节,就向后移动多少字节。但是,值得注意的是,pread并不会更新文件偏移。pread不更新文件偏移这一点对于多线程的程序来说极其有用。我们知道,多条线程有可能共用同一个文件描述符,但文件偏移是存储在文件描述符中。如果我们在多线程中使用read,会导致文件偏移混乱;但是,如果我们使用pread,则会完满避免这个问题。
第四,是如何读。在Linux的哲学中,如何读并不是read和pread能决定的,而是由文件描述符本身决定的。文件描述符在创建的时候,就决定了它将被如何读取,比如说是否阻塞等等。
我们在上面提到,pread除了在多线程中发挥大作用之外,也可以将两次系统调用lseek read化为一次系统调用。而这一节所讲的系统调用,则是更进一步。read和pread是将文件读取到一块连续内存中,那如果我们想要将文件读取到多块连续内存中(也就是说,有多块内存,内存内部连续,但内存之间不连续),就得多次使用这些系统调用,造成很大的开销。而readv, preadv, preadv2则是为了解决这样的问题。
readv, preadv, preadv2的第二个参数iov是一个iovec结构体组成的数组,其元素个数由第三个参数iovcnt给出。这三个系统调用的作用就是“分散读”(scatter input),将一块连续的文件内容,按顺序读入多块连续区域中。
在preadv和preadv2中,我们可以看到,其系统调用接口含有两个参数pos_l与pos_h,但glibc封装后只有一个参数offset。这是因为,考虑到64位地址的问题,pos_l和pos_h分别包含了offset的低32位和高32位。
此外,还需要注意,这里的读取虽然说是“向量化”,但实际上,缓冲区是按数组顺序处理的,也就是说,只有在iov[0]被填满之后,才会去填充iov[1]。
同样类似read与pread,这三个系统调用也是返回读取的字节数,同样可能会小于iov->iov_len之和。
与read和pread不同的是,这三个系统调用是原子性的,它们读取的文件内容永远是连续的,也就是说不会因为文件偏移被别的线程改变而混乱。比如说,我们想将文件中的内容读入三块缓冲区中。如果我们是使用三次read,但是在第一次read结束之后,第二次read开始之前,另外一个线程对这个文件描述符的文件偏移进行了改变,那么接下来的两次read读出的数据与第一次read读出的数据是不连续的。但是,如果我们用readv,读出的数据一定是连续的。
而preadv与preadv2的区别,主要在于最后一个参数。它通过一些标志位来改变读取的行为。具体可以看其手册preadv2。
关于文件偏移的更新,readv和read一样,在结束之后会更新文件偏移;preadv和pread一样,在结束之后不会更新文件偏移;对于preadv2来说,如果offset为-1,其会使用当前的文件偏移而不是前往指定的文件偏移,并且在结束后会更新文件偏移,但是如果其不为-1,则不会更新文件偏移。
writev()系列详解
write和pwrite是最基础的对文件写入的系统调用。write会将buf中count个字节写入描述符为fd的文件中,而pread则会将buf中count个字节写入描述符为fd的文件从offset开始的位置中。如果写入成功,这两个系统调用都将返回写入的字节数。因此,这两个系统调用主要的区别就在于写入的位置,其它功能均类似。
注意点:
首先,是文件偏移的问题:
·写入前位置
显然,pwrite是从文件偏移为offset的位置开始写入,但是write的问题则比较特殊。一般来说,write开始写入时的文件偏移就是当前的文件偏移,但是,当文件描述符是通过open系统调用创建,且创建时使用了O_APPEND标志位的话,每次write开始写入前,都会默认将文件偏移移到文件末尾。
·写入后位置
同read和pread类似,write在成功写入n个字节后,会将文件偏移更新n个字节;但pwrite则不会更新文件偏移,因此和pread一起常用于多线程的代码中。
和readv
, preadv
, preadv2
类似,writev(), pwritev(), pwritev2()是为了解决一次性从多个连续内存向一个文件描述符写入的问题,这三个系统调用被称为“聚合写”(gather output)。
这三个函数的特性与readv
, preadv
, preadv2
十分类似,这里不再赘述。
参考资料
- https://blog.csdn.net/baidu_15952103/article/details/109888362
- https://tangyilong.com/2019/02/20/struct-iovec/
- https://evian-zhang.github.io/introduction-to-linux-x86_64-syscall/src/filesystem/write-pwrite64-writev-pwritev-pwritev2.html
- https://evian-zhang.github.io/introduction-to-linux-x86_64-syscall/src/filesystem/read-pread64-readv-preadv-preadv2.html