Unix/Linux编程:分散输入和集中输出------readv() 、 writev()

readv()和write()系统调用分别实现了分散输入和集中输出的功能:

NAME
       readv,  writev, preadv, pwritev, preadv2, pwritev2 
            - 读取或写入数据到多个缓冲区 
       		- 这些系统调用并非只针对单个缓存区进行读写操作,而是一次可以传输多个缓存区的数据

SYNOPSIS
		/*
		* 参数: fd    文件描述符
		*       iov    指向iovec结构数组的一个指针
		*       iovcnt  指定了iovec的个数
		* 返回值:函数调用成功时返回读、写的总字节数,失败时返回-1并设置相应的errno。
		* 			writev返回输出的字节总数,通常应等于所有缓冲区长度之和。
		* 			readv返回读到的字节总数。如果遇到文件尾端,已无数据可读,则返回0。
		*/
       #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);

	   size_t pwritev2(int fd, const struct iovec *iov, int iovcnt,
                        off_t offset, int flags);

		glibc的功能测试宏要求 (另请参阅feature_test_macros(7))
			
			preadv(), pwritev():
           Since glibc 2.19:
               _DEFAULT_SOURCE
           Glibc 2.19 and earlier:
               _BSD_SOURCE

DESCRIPTION
		readv() 系统调用从关联的文件读取iovcnt缓冲区文件描述符fd
		        放入iov描述的缓冲区(“分散输入”)

        writev() 系统调用将iov描述的数据的iovcnt缓冲区写入与文件描述符fd
       			相关联的文件(“聚集输出”)

		结构体指针iov定义了一组用来传输数据的缓存区.
		整数iovcnt则指定了iov的成员个数。
		iov中的每个成员都是如下形式的数据结构

		#include <sys/uio.h>
		struct iovec {
               void  *iov_base;    /* 缓冲区首地址 */
               size_t iov_len;     /*缓冲区长度 */
         };

		readv()系统调用的工作方式与read(2)相同,只是要填充多个缓冲区。
		
		writev()系统调用的工作方式与write(2)相同,只是要写出多个缓冲区 

		缓冲区以数组顺序处理。 这意味着readv()在完全填充iov[0]之前不会
		进入iov[1],依此类推。(如果数据不足,则可能不会填充iov指向的
		所有缓冲区)。类似地,writev()在进行iov[1]之前写出iov[0]的全部内容,
		依此类推。 
		
		readv()writev()执行的数据传输是原子的:writev()写入的数据作为
		单个块写入,而不是与其他进程中的写入输出混合(例外请参见pipe(7));
		类似地,readv()保证从文件中读取连续的数据块,而不管在其他线程或进程
		中执行的读取操作,这些线程或进程具有引用相同打开文件描述的文件描述符
		(请参阅open(2))。
		
	preadv()pwritev()
		preadv()系统调用结合了readv()pread(2)。 它执行与readv()相同的任务,
		但是添加了第四个参数offset,该参数指定要在其上执行输入操作的文件偏移量

		pwritev()系统调用结合了writev()pwrite(2)。 它执行与writev()相同的任务,
		但是添加了第四个参数offset,该参数指定要在其上执行输出操作的文件偏移量

		这些系统调用不会更改文件偏移量。 fd引用的文件必须能够seek。

	preadv2()pwritev2() 
		。。。

RETURN VALUE	
		成功时,readv()preadv()preadv2()返回读取的字节数;
		        writev()pwritev()pwritev2()返回写入的字节数。	

		请注意,成功调用传输的字节数少于请求的字节数并不是错误的(请参阅read(2)write(2))。

		出现错误时,返回-1,并正确设置errno
		
  • SUSv3 标准允许系统实现对 iov 中的成员个数加以限制。系统实现可以通过定义<limits.h>文件中 IOV_MAX 来通告这一限额,程序也可以在系统运行时调用sysconf (_SC_ IOV_MAX)来获取这一限额。SUSv3 要求该限额不得少于 16。Linux将 IOV_MAX 的值定义为 1024,这是与内核对该向量大小的限制(由内核常量 UIO_MAXIOV定义)相对应的。
  • 然而,glibc 对 readv()和 writev()的封装函数还悄悄做了些额外工作。若系统调用因iovcnt 参数值过大而失败,外壳函数将临时分配一块缓冲区,其大小足以容纳 iov 参数所有成员所描述的数据缓冲区,随后再执行 read()或 write()调用(参见后文对使用 write()实现writev()功能的讨论)

下图展示了一个关于 iov、iovcnt 以及 iov 指向缓冲区之间关系的示例:

在这里插入图片描述

readv和write

  • readv/writevrecvmsg/sendmsg的简化版,主要针对与文件IO(对read/write的优化)
  • readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读和聚集写

在一次函数调用中:
① writev以顺序iov[0]、iov[1]至iov[iovcnt-1]从各缓冲区中聚集输出数据到fd
② readv则将从fd读入的数据按同样的顺序散布到各缓冲区中,readv总是先填满一个缓冲区,然后再填下一个

为什么引入readv()和writev()

(1) 使用read函数将数据读到不连续的内存,或者wirte将不连续的内存发送出去,要多次调用read、write

如果要从文件中读一片连续的数据至进程的不同区域,有两种方案
① 使用read()一次将它们读至一个较大的缓冲区中,然后将它们分成若干部分复制到不同的区域;
② 调用read()若干次分批将它们读至不同区域。

同样,如果想将程序中不同区域的数据块连续地写至文件,也必须进行类似的处理。

(2) UNIX提供了另外两个函数—readv()和writev(),它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。

分散输入

readv()实现了分散输入的功能:从文件描述符fd所指代的文件中读取一片连续的字节,然后将其分散放置到iov指定的缓存区中。这一散置动作从 iov[0]开始,依次填满整个缓存区

readv()是原子性的。

调用 readv()成功将返回读取的字节数,若文件结束1将返回 0。调用者必须对返回值进行检查,以验证读取的字节数是否满足要求。若数据不足以填充所有缓冲区,则只会占用2部分缓冲区,其中最后一个缓冲区可能只存有部分数据

例子1:读文件

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>

int main(int argc, char *argv[]){
    int fd;
    struct iovec iov[3];
    struct stat myStruct;       /* First buffer */
    int x;                      /* Second buffer */
#define STR_SIZE 100
    char str[STR_SIZE];         /* Third buffer */
    ssize_t numRead, totRequired;

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        fprintf(stderr, "%s file\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd = open(argv[1], O_RDONLY);
    if(fd == -1){
        fprintf(stderr, "open\n");
        exit(EXIT_FAILURE);
    }

    totRequired = 0;

    iov[0].iov_base = &myStruct;
    iov[0].iov_len = sizeof(struct stat);
    totRequired += iov[0].iov_len;

    iov[1].iov_base = &x;
    iov[1].iov_len = sizeof(x);
    totRequired += iov[1].iov_len;

    iov[2].iov_base = str;
    iov[2].iov_len = STR_SIZE;
    totRequired += iov[2].iov_len;

    numRead = readv(fd, iov, 3);
    if (numRead == -1){
        fprintf(stderr, "readv\n");
        exit(EXIT_FAILURE);
    }

    if (numRead < totRequired)
        printf("Read fewer bytes than requested\n");

    printf("total bytes requested: %ld; bytes read: %ld\n",
           (long) totRequired, (long) numRead);

    printf("%s", str);
    exit(EXIT_SUCCESS);
}
#include <stdio.h>
#include <sys/uio.h>
#include <fcntl.h>
 
int main(void){
        char buf1[5],buf2[10];
        struct iovec iov[2];
        iov[0].iov_base = buf1;
        iov[0].iov_len = 5;
        iov[1].iov_base = buf2;
        iov[1].iov_len = 10;
 
        int fd = open("a.txt",O_RDWR);
        if(fd < 0){
                perror("open");
                return -1;
        }
        int rsize = readv(fd, iov, 2);  // 从文件a.txt中读取数据,存到iov[2]中的buf1、buf2
        printf("rsize = %d\n",rsize);
 
        close(fd);
 
        fd = open("b.txt", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd < 0){
                perror("open");
                return -1;
        }
 
        int wsize = writev(fd,iov,2);  // 将iov[2]中的buf1、buf2,写入到文件b.txt
        printf("wsize = %d\n",wsize);
 
        close(fd);
        return 0;
}

集中输出

writev()系统调用实现了集中输出:将iov所指定的所有缓存区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符fd指代的文件中。对缓冲区中数据的“集中”始于iov[0]所指定的缓冲区,并按数组顺序展开

像readv()调用一样,writev()调用也属于原子操作,即所有数据将一次性的从用户内存传入到fd指代的文件中。因此,在向普通文件写入数据时,writev()调用会把所有的请求数据连续写入文件,而即不受其他进(线)程改变文件偏移量的影响

如同 write()调用,writev()调用也可能存在部分写的问题。因此,必须检查 writev()调用的返回值,以确定写入的字节数是否与要求相符。

readv()调用和 writev()调用的主要优势在于便捷。如下两种方案,任选其一都可替代对writev()的调用。

  • 编码时,首先分配一个大缓冲区,随即再从进程地址空间的其他位置将数据复制过来,最后调用 write()输出其中的所有数据。
  • 发起一系列 write()调用,逐一输出每个缓冲区中的数据

尽管方案一在语义上等同于 writev()调用,但需要在用户空间内分配缓冲区,进行数据复制,很不方便(效率也低)。

方案二在语义上就不同于单次的 writev()调用,因为发起多次 write()调用将无法保证原子性。更何况,执行一次 writev()调用比执行多次 write()调用开销要小(参见 3.1 节关于系统调用的讨论)

应当指出,readv()和 writev()会改变打开文件句柄的当前文件偏移量

例子1:writev:指定了两个缓冲区,str0和str1,内容输出到标准输出,并打印实际输出的字节数

// writevex.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/uio.h>

int main()
{
    char *str0 = "hello ";
    char *str1 = "world\n";
    struct iovec iov[2];
    ssize_t nwritten;

    iov[0].iov_base = str0;
    iov[0].iov_len = strlen(str0) + 1;
    iov[1].iov_base = str1;
    iov[1].iov_len = strlen(str1) + 1;

    nwritten = writev(STDOUT_FILENO, iov, 2);
    printf("%ld bytes written.\n", nwritten);

    exit(EXIT_SUCCESS);
}

例子2:读写文件

#include <stdio.h>
#include <sys/uio.h>
#include <fcntl.h>
 
int main(void){
        char buf1[5],buf2[10];
        struct iovec iov[2];
        iov[0].iov_base = buf1;
        iov[0].iov_len = 5;
        iov[1].iov_base = buf2;
        iov[1].iov_len = 10;
 
        int fd = open("a.txt",O_RDWR);
        if(fd < 0){
                perror("open");
                return -1;
        }
        int rsize = readv(fd, iov, 2);  // 从文件a.txt中读取数据,存到iov[2]中的buf1、buf2
        printf("rsize = %d\n",rsize);
 
        close(fd);
 
        fd = open("b.txt", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd < 0){
                perror("open");
                return -1;
        }
 
        int wsize = writev(fd,iov,2);  // 将iov[2]中的buf1、buf2,写入到文件b.txt
        printf("wsize = %d\n",wsize);
 
        close(fd);
        return 0;
}

preadv、pwritev

在指定的文件偏移量处执行分散输入/集中输出

Linux 2.6.30 版本新增了两个系统调用:preadv()、pwritev(),将分散输入/集中输出和于指定文件偏移量处的 I/O 二者集于一身。它们并非标准的系统调用,但获得了现代 BSD 的支持

preadv()和 pwritev()系统调用所执行的任务与 readv()和 writev()相同,但执行 I/O 的位置将由 offset 参数指定(类似于 pread()和 pwrite()系统调用)

对于那些既想从分散-集中 I/O 中受益,又不愿受制于当前文件偏移量的应用程序(比如,多线程的应用程序)而言,这些系统调用恰好可以派上用场。

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值