Advanced File I/O Scatter/Gather
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。
此外,标准还定义了另外两种错误情况:
- 由于返回类型是ssize_t,如果所有计数iov_len值的总和大于SSIZE_MAX,则不会传输任何数据,将返回−1,并将errno设置为EINVAL。
- 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()时是可读的,调用也不会返回,直到数据是 写在管子上。