美文网首页
Advanced File I/O Scatter/Gather

Advanced File I/O Scatter/Gather

作者: 无无吴 | 来源:发表于2019-08-14 08:56 被阅读0次

    Scatter/Gather I/O

    Scatter/gather I/O 是一种输入输出的方法。它要么是单个的系统调用从缓冲区vector中写入到单个的数据流中,或是从单个数据流读入缓冲区vector。
    这种类型的I/O之所以这么命名,是因为数据分散在从给定的缓冲区vector或从给定的缓冲区vector中收集。

    readv() and writev()

    readv()从文件描述符中读取count个segment到iov指代的buffer中。
    writev()是把iov指代的buffer写入到文件描述符中,最多写count个。

    #include <sys/uio.h>
    ssize_t readv(int fd, const struct iovec *iov, int count);
    ssize_t writev(int fd, const struct iovec *iov, int count);
    struct iovec{
            void *iov_base; /* pointer to start of buffer */
            size_t iov_len; /* size of buffer in bytes */
    };
    
    

    成功后,readv()和writev()将返回读或写的字节数。这个数字应该是所有iov_len值的总和。错误时,系统调用返回−1并视情况而定设置errno。
    此外,标准还定义了另外两种错误情况:

    1. 由于返回类型是ssize_t,如果所有计数iov_len值的总和大于SSIZE_MAX,则不会传输任何数据,将返回−1,并将errno设置为EINVAL。
    2. POSIX要求count这个参数必须大于零,小于或等于IOV_MAX,IOV_MAX是在<limits.h>中定义的。在Linux中,IOV_MAX当前为1024。如果计数为0,则系统调用返回0。 如果计数大于IOV_MAX,则不传输数据,调用返回−1,并将errno设置为EINVAL。
      count 的阈值时8,如果大于8,性能会降低。

    writev() example

    int writevExample() {
        struct iovec iov[3];
        ssize_t nr;
        int fd, i;
        char *buf[] = {
                "The term buccaneer comes from the word boucan.\n",
                "A boucan is a wooden frame used for cooking meat.\n",
                "Buccaneer is the West Indies name for a pirate.\n"
        };
    
        fd = open("buccaneer.txt", O_WRONLY | O_CREAT | O_TRUNC);
        if(fd == -1){
            perror("open");
            return 1;
        }
        for(i = 0; i < 3; ++i){
            iov[i].iov_base = buf[i];
            iov[i].iov_len = strlen(buf[i]) + 1;
        }
        nr = writev(fd, iov, 3);
        if(nr == -1){
            perror("writev");
            return 1;
        }
        printf("write %d bytes\n", nr);
        if(close(fd)){
            perror("close");
            return 1;
        }
        return 0;
    }
    
    int main()
    {
        writevExample();
        return 0;
    }
    
    writev() Example

    readv example

    int readvExample() {
        char foo[48], bar[51], baz[49];
        struct iovec iov[3];
        ssize_t nr;
        int fd, i;
        fd = open("buccaneer.txt", O_RDONLY);
        if(fd == -1){
            perror("open");
            return 1;
        }
        iov[0].iov_base = foo;
        iov[0].iov_len = sizeof(foo);
        iov[1].iov_base = bar;
        iov[1].iov_len = sizeof(bar);
        iov[2].iov_base = baz;
        iov[2].iov_len = sizeof(baz);
    
        nr = readv(fd, iov, 3);
        if(nr == -1){
            perror("readv");
            return 1;
        }
        for(i = 0; i < 3; ++i){
            printf("%d: %s", i, (char*)iov[i].iov_base);
        }
        if(close(fd)){
            perror("close");
            return 1;
        }
        return 0;
    }
    
    int main()
    {
        readvExample();
        return 0;
    }
    
    readv() Example

    Event Poll

    poll()和select()都需要完整的文件描述符列表来监视每次调用。然后内核必须遍历要监视的每个文件描述符的列表。当这个列表越来越大的时候,它可能包含数百个甚至是数千个的文件描述符的时候,在每次调用中遍历列表就成了一个瓶颈。

    Epoll通过将监控器注册与实际监视分离来避免此问题。一个系统调用初始化epoll context,另一个系统调用将监视文件描述符从context中添加或删除,第三个执行实际事件等待。

    Creating a New Epoll Instance

    通过epoll_create1()创建epoll context:

    #include <sys/epoll.h>
    int epoll_create1(int flags);
    

    对epoll_create1()的成功调用实例化了一个新的epoll实例,并返回与实例关联的文件描述符。此文件描述符与实际文件无关;它只是使用epoll工具与后续调用一起使用的句柄。flag参数允许修改epoll行为。目前,只有EPOLL_CLOEXEC是有效标志。它使能了close-on-exec行为。

    close_on_exec是一个进程所有文件描述符的标记位图,每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄 (参见include/fcntl.h)。

       当一个程序使用fork()函数创建了一个子进程时,往往会在该子进程中调用execve()函数加载执行另一个新程序,此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。同时子进程会拷贝父进程的文件描述符表,这样父子进程就有可能同时操作同一打开文件,如果不想子进程操作该文件描述符,则可将close_on_exec中的对应比特位被设置为1,那么在执行execve()时该描述符将被关闭,否则该描述符将始终处于打开状态。当打开一个文件时,默认情况下文件句柄在子进程中也处于打开状态
    

    典型调用:

    int epfd;
    epfd = epoll_create1(0);
    if(epfd < 0)
        perror("epoll_create1");
    

    Controlling Epoll

    可以使用epoll_ctl()系统调用向给定的epoll context中添加文件描述符并删除文件描述符:

    #include <sys/epoll.h>
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    struct epoll_event{
          __u32 events;
          union {
                  void *ptr;
                  int fd;
                  __u32 u32;
                  __u64 u64; 
          } data;
    };
    

    参数op指定对与fd关联的文件采取的操作。
    event参数进一步描述操作的行为。

    下面是OP参数的有效值:

    • EPOLL_CTL_ADD
      根据event中定义的events,将与文件描述符fd关联的文件上的监控器添加到与epfd关联的epll实例。
    • EPOLL_CTL_DEL
      删除文件上的监控器与epfd关联的epoll实例中的文件描述符fd关联。
    • EPOLL_CTL_MOD
      使用事件指定的更新事件修改fd的现有监视器。

    epoll_event结构体中的events字段列出了要监视给定文件描述符上的事件。多个事件可以按位或在一起。以下是有效值:

    • EPOLLERR
      文件上出现错误情况。即使未指定此事件,也始终对其进行监视。
    • EPOLLET
      启用文件监视器的边缘触发行为。默认行为是水平触发的。
    • EPOLLHUP
      文件上出现挂起。即使未指定此事件,也始终对其进行监视。
    • EPOLLIN
      文件是可以在不阻塞的情况下阅读。
    • EPOLLONESHOT
      在生成和读取事件后,将不再自动监视该文件。必须通过EPOLL_CTL_MOD指定新的事件掩码以重新启用监控。
    • EPOLLOUT
      文件是可以非阻塞写的。
    • EPOLLPRI
      有紧急的带外数据可供阅读。

    event_poll结构体中的data字段供用户专用。当收到所请求的事件时,内容将返回给用户。通常的做法是将event.data.fd设置为 fd,这使得查找导致事件的文件描述符变得很容易。

    //add a new watch
    struct epoll_event event;
    int ret;
    
    event.data.fd = fd;
    event.events = EPOLLIN |  EPOLLOUT;
    
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    if(ret)
        perror("epoll_ctl");
    
    // modify an existing event
    struct epoll_event event;
    int ret;
    
    event.data.fd = fd;
    event.events = EPOLLIN;
    
    ret = epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event);
    if(ret)
        perror("epoll_ctl");
    
    // remove an existing event 
    struct epoll_event event;
    int ret;
    
    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, *event);
    if(ret)
        perror("epoll_ctl");
    

    Waiting for Events with Epoll

    系统调用epoll_wait()等待与给定epoll实例关联的文件描述符上的事件:

    #include <sys/epoll.h>
    int epoll_wait(int epfd, struct epoll_event *events,
              int maxevents, int timeout);
    

    对epoll_wait()的调用将等待与epoll实例epfd相关联的文件上的事件的超时毫秒。成功后,事件指向包含epoll_event结构的内存 将每个事件(例如准备写入或读取的文件)拆分到最大的maxEvents。返回值是事件数,或错误时的−1,并设置errno。
    如果超时为0,则调用立即返回,即使没有可用的事件,在这种情况下,调用将返回0。如果超时为−1,则在事件可用之前调用不会返回。

    //epoll_wait() example
    #define MAX_EVENTS 64
    
    struct epoll_event *events;
    int nr_events, i, epfdl
    events = malloc(sizeof(struct epoll_event)* MAX_EVENTS);
    if(!events){
        perror("malloc");
        return 1;
    }
    nr_events = epoll_wait(epfd, events, MAX_EVENTS, -1);
    if(nr_events < 0){
        perror("epoll_wait");
        free(events);
        return 1;
    }
    for(i = 0; i < nr_events; ++i){
        printf ("event=%ld on fd=%d\n",
            events[i].events,
            events[i].data.fd);
       /*
        * We now can, per events[i].events, operate on
        * events[i].data.fd without blocking.
        */
    }
    
    

    Edge - Versus Level - Triggered Events 边缘与水平触发事件

    如果EPOLLET值是在传递给epoll_ctl()的event参数的events字段中设置,则fd上的监视是边缘触发的,而不是水平触发的。
    考虑在Unix管道上通信的生产者和使用者之间的下列事件:
    1.生产者将1 KB的数据写入管道。
    2.使用者对管道执行epoll_wait(), 等待管道收集数据,从而可读。

    对于水平触发的监视,步骤2中对epoll_wait()的调用将立即返回,显示管道已准备好读取。
    使用边缘触发的监视,此调用将在步骤1发生后才返回.也就是说,即使管道在调用epoll_wait()时是可读的,调用也不会返回,直到数据是 写在管子上。

    相关文章

      网友评论

          本文标题:Advanced File I/O Scatter/Gather

          本文链接:https://www.haomeiwen.com/subject/ualwjctx.html