readv/writev 函数及存储映射 I/O
程序员文章站
2024-01-31 20:39:04
...
readv 和 writev 函数可用于在一次函数调用中读、写多个非连续缓冲区,有时也称这两个函数为散布读(scatter read)和聚集写(gather write)。
这两个函数的第二个参数 iov 都是指向 iovec 结构数组的一个指针,该数组中的元素由 iovcnt 指定,其最大值受限于 IOV_MAX。readv 函数将读入的数据按 iov[0]、iov[1] 直至 iov[iovcnt-1] 的顺序散布到缓冲区中。它总是先填满一个缓冲区后再填写下一个。writev 函数则按照同样的顺序将缓冲区中的数据聚集输出到文件中。
存储映射 I/O 能将一个磁盘文件映射到存储空间中的一个缓冲区上,通过操作该缓冲区可以在不使用 read 和 write 的情况下间接地操作底层文件。为使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这可用 mmap 函数来实现。
其中,addr 参数用于指定映射存储区的起始地址,通常将其设置为 0,表示由系统来选择。len 参数是映射的字节数,fd 参数是要映射文件的描述符,off 参数要映射字节在文件中的起始偏移量。
prot 参数指定了映射存储区的保护要求,其可选值如下表所示。
prot 可设置为 PROT_NONE,也可设置为另外三个的按位或,但对指定映射存储区的保护要求不能超过文件 open 模式访问权限。
flag 参数可以影响映射存储区的多种属性,其可取值如下(其他实现可能还有另外一些,可参考具体实现的 mmap(2) 手册页)。
* MAP_FIXED:要求返回值必须等于 addr,因为不利于移植,所以不鼓励使用。如果未指定此标志,而且 addr 非 0,则内核只把 addr 视为在何处设置映射区的一种建议,但不保证会使用所要求的地址。将 addr 设置为 0 可获得最大可移植性。
* MAP_SHARED:此标志指定存储操作将修改映射文件。必须指定本标志或者下一个标志(MAP_PRIVATE)。
* MAP_PRIVATE:此标志说明对映射区的存储操作会导致创建该映射文件的一个私有副本,所有后来对该映射区的引用都是引用该副本。该标志的一种用途是用于调试程序,它将程序文件的正文部分映射至存储区,但允许用户修改其中的指令,而不影响原文件。
下图所示是典型的存储映射文件的基本情况,图中的“起始地址”是 mmap 的返回值。
off 和 addr(如果指定了 MAP_FIXED)的值通常被要求是系统虚拟存储页长度的倍数(虚拟存储页长可用带参数 _SC_PAGESIZE 或 _SC_PAGE_SIZE 的 sysconf 函数得到,通常为 4096 字节)。如果映射区的长度不是页长的整数倍时,那么为了 mmap 能将数据添加到文件中,可能需要先加长该文件。不过因为这两个参数常常指定为 0,所以这种要求一般不重要。
与映射区相关的信号有 SIGSEGV 和 SIGBUS。SIGSEGV 信号通常用于指示进程试图访问对它不可用的存储区。当进程试图将数据存入一个被 mmap 指定为只读的映射存储区时,也会产生此信号。而如果映射区的某个部分在访问时已不存在,就会产生 SIGBUS 信号,比如进程试图访问一个被截断了的文件的已截去部分的映射区时。
子进程能通过 fork 继承存储映射区,因为它复制了父进程的地址空间,而存储映射区是该地址空间中的一部分。但新程序则由于同样的原因不能通过 exec 继承存储映射区。
映射存储区分配好后,就可以使用下面一组函数来操作。
mprotect 函数可以更改一个现有映射的权限。如果修改的页是通过 MAP_SHARED 标志映射到地址空间中的,那么修改不会立即写回到文件中,何时写回脏页是由内核的守护进程根据系统负载和用来限制在系统失败事件中的数据损失的配置参数决定的。因此,即使只修改了一页中的一个字节,当修改被写回到文件中时也会写整个页。
msync 函数可以将共享映射中被修改的页冲洗到映射的文件中,私有映射不会修改被映射的文件。flags 参数可以控制如何冲洗存储区:MS_ASYNC 标志可用来简单地调试要写的页,MS_SYNC 标志可让函数在返回之前等待写操作完成。这两个标志必须指定一个。还有一个可选的标志 MS_INVALIDATE 可用来通知操作系统丢弃那些与底层存储器没有同步的页。
进程终止或者直接调用 munmap 函数都可以解除映射区,而关闭映射存储区使用的文件描述符不会解除映射区。调用 munmap 不会使映射区的内容写到磁盘文件上。对于 MAP_SHARED 区磁盘文件的更新,会在将数据写到存储映射区后的某个时刻,按内核虚拟存储算法自动进行。在存储区解除映射后,对 MAP_PRIVATE 存储区的修改会被丢弃。
下面这个示例使用存储映射 I/O 来模拟 cp 命令复制文件。
注意,本示例中如果不使用 ftruncate 或类似的函数来设置输出文件的长度,则对输出文件调用 mmap 虽然也可以,但是对相关存储区的第一次引用将会产生 SIGBUS 信号。在映射文件中的后一部分数据前,需要先解除前一部分数据的映射。在从输入缓冲区 src 取数据时,内核自动读输入文件;在将数据存入输出缓冲区 dst 时,内核自动将数据写到输出文件中。
对比系统调用 read 和 write,这两者将数据从内核缓冲区中复制到应用缓冲区(read),然后再把数据从应用缓冲区复制到内核缓冲区(write),而 mmap 和 memcpy 是直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区,但当引用尚不存在的内存页时,这样的复制过程就会作为处理页错误(错页读或错页写)的结果而出现。如果系统调用及额外的复制操作的开销和页错误的开销不同,那么这两种方法中就会有一种比另一种表现更好。
#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); /* 两个函数的返回值:已读或已写的字节数;若出错,返回 -1 */ struct iovec{ void *iov_base; // starting address of buffer size_t iov_len; // size of buffer };
这两个函数的第二个参数 iov 都是指向 iovec 结构数组的一个指针,该数组中的元素由 iovcnt 指定,其最大值受限于 IOV_MAX。readv 函数将读入的数据按 iov[0]、iov[1] 直至 iov[iovcnt-1] 的顺序散布到缓冲区中。它总是先填满一个缓冲区后再填写下一个。writev 函数则按照同样的顺序将缓冲区中的数据聚集输出到文件中。
存储映射 I/O 能将一个磁盘文件映射到存储空间中的一个缓冲区上,通过操作该缓冲区可以在不使用 read 和 write 的情况下间接地操作底层文件。为使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这可用 mmap 函数来实现。
#include <sys/mman.h> void * mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off); /* 返回值:若成功,返回映射区的起始地址;否则,返回 MAP_FAILED */
其中,addr 参数用于指定映射存储区的起始地址,通常将其设置为 0,表示由系统来选择。len 参数是映射的字节数,fd 参数是要映射文件的描述符,off 参数要映射字节在文件中的起始偏移量。
prot 参数指定了映射存储区的保护要求,其可选值如下表所示。
prot 可设置为 PROT_NONE,也可设置为另外三个的按位或,但对指定映射存储区的保护要求不能超过文件 open 模式访问权限。
flag 参数可以影响映射存储区的多种属性,其可取值如下(其他实现可能还有另外一些,可参考具体实现的 mmap(2) 手册页)。
* MAP_FIXED:要求返回值必须等于 addr,因为不利于移植,所以不鼓励使用。如果未指定此标志,而且 addr 非 0,则内核只把 addr 视为在何处设置映射区的一种建议,但不保证会使用所要求的地址。将 addr 设置为 0 可获得最大可移植性。
* MAP_SHARED:此标志指定存储操作将修改映射文件。必须指定本标志或者下一个标志(MAP_PRIVATE)。
* MAP_PRIVATE:此标志说明对映射区的存储操作会导致创建该映射文件的一个私有副本,所有后来对该映射区的引用都是引用该副本。该标志的一种用途是用于调试程序,它将程序文件的正文部分映射至存储区,但允许用户修改其中的指令,而不影响原文件。
下图所示是典型的存储映射文件的基本情况,图中的“起始地址”是 mmap 的返回值。
off 和 addr(如果指定了 MAP_FIXED)的值通常被要求是系统虚拟存储页长度的倍数(虚拟存储页长可用带参数 _SC_PAGESIZE 或 _SC_PAGE_SIZE 的 sysconf 函数得到,通常为 4096 字节)。如果映射区的长度不是页长的整数倍时,那么为了 mmap 能将数据添加到文件中,可能需要先加长该文件。不过因为这两个参数常常指定为 0,所以这种要求一般不重要。
与映射区相关的信号有 SIGSEGV 和 SIGBUS。SIGSEGV 信号通常用于指示进程试图访问对它不可用的存储区。当进程试图将数据存入一个被 mmap 指定为只读的映射存储区时,也会产生此信号。而如果映射区的某个部分在访问时已不存在,就会产生 SIGBUS 信号,比如进程试图访问一个被截断了的文件的已截去部分的映射区时。
子进程能通过 fork 继承存储映射区,因为它复制了父进程的地址空间,而存储映射区是该地址空间中的一部分。但新程序则由于同样的原因不能通过 exec 继承存储映射区。
映射存储区分配好后,就可以使用下面一组函数来操作。
#include <sys/mman.h> int mprotect(void *addr, size_t len, int prot); int msync(void *addr, size_t len, int flags); int munmap(void *addr, size_t len); /* 三个函数的返回值:若成功,返回 0;否则,返回 -1 */
mprotect 函数可以更改一个现有映射的权限。如果修改的页是通过 MAP_SHARED 标志映射到地址空间中的,那么修改不会立即写回到文件中,何时写回脏页是由内核的守护进程根据系统负载和用来限制在系统失败事件中的数据损失的配置参数决定的。因此,即使只修改了一页中的一个字节,当修改被写回到文件中时也会写整个页。
msync 函数可以将共享映射中被修改的页冲洗到映射的文件中,私有映射不会修改被映射的文件。flags 参数可以控制如何冲洗存储区:MS_ASYNC 标志可用来简单地调试要写的页,MS_SYNC 标志可让函数在返回之前等待写操作完成。这两个标志必须指定一个。还有一个可选的标志 MS_INVALIDATE 可用来通知操作系统丢弃那些与底层存储器没有同步的页。
进程终止或者直接调用 munmap 函数都可以解除映射区,而关闭映射存储区使用的文件描述符不会解除映射区。调用 munmap 不会使映射区的内容写到磁盘文件上。对于 MAP_SHARED 区磁盘文件的更新,会在将数据写到存储映射区后的某个时刻,按内核虚拟存储算法自动进行。在存储区解除映射后,对 MAP_PRIVATE 存储区的修改会被丢弃。
下面这个示例使用存储映射 I/O 来模拟 cp 命令复制文件。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <sys/stat.h> #include <sys/mman.h> #define COPYINCR (1024*1024) // 1 GB = 256 * 4096 KB #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) int main(int argc, char *argv[]){ if(argc < 3){ printf("Usage: %s <fromFile> <toFile>\n", argv[0]); exit(1); } int fdin = open(argv[1], O_RDONLY); int fdout = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE); struct stat fin_stat; fstat(fdin, &fin_stat); if( ftruncate(fdout, fin_stat.st_size) < 0){ printf("ftruncate error\n"); exit(1); } void *src, *dst; off_t fsz = 0; // 要为虚拟存储页(一般为4096字节)的倍数 while(fsz < fin_stat.st_size){ size_t copysz = fin_stat.st_size - fsz; if(copysz > COPYINCR) copysz = COPYINCR; if((src=mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz)) == MAP_FAILED){ printf("Failed to map input file\n"); exit(1); } if((dst=mmap(0, copysz, PROT_WRITE|PROT_READ, MAP_SHARED, fdout, fsz)) == MAP_FAILED){ printf("Failed to map output file\n"); exit(1); } memcpy(dst, src, copysz); // does the file copy munmap(src, copysz); munmap(dst, copysz); fsz += copysz; } exit(0); }
注意,本示例中如果不使用 ftruncate 或类似的函数来设置输出文件的长度,则对输出文件调用 mmap 虽然也可以,但是对相关存储区的第一次引用将会产生 SIGBUS 信号。在映射文件中的后一部分数据前,需要先解除前一部分数据的映射。在从输入缓冲区 src 取数据时,内核自动读输入文件;在将数据存入输出缓冲区 dst 时,内核自动将数据写到输出文件中。
对比系统调用 read 和 write,这两者将数据从内核缓冲区中复制到应用缓冲区(read),然后再把数据从应用缓冲区复制到内核缓冲区(write),而 mmap 和 memcpy 是直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区,但当引用尚不存在的内存页时,这样的复制过程就会作为处理页错误(错页读或错页写)的结果而出现。如果系统调用及额外的复制操作的开销和页错误的开销不同,那么这两种方法中就会有一种比另一种表现更好。
上一篇: 通过CORS配置实现JS跨域访问
下一篇: 线程(下)