Advanced File I/O Scatter/Gather

2019-08-14  本文已影响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_event结构体中的events字段列出了要监视给定文件描述符上的事件。多个事件可以按位或在一起。以下是有效值:

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()时是可读的,调用也不会返回,直到数据是 写在管子上。

上一篇下一篇

猜你喜欢

热点阅读