1. 综述
io 操作是服务端开发人员必须掌握的操作,因此,掌握Io的原理特别重要。
2. 异步写操作原理
2.1 pagecache
linux 系统默认使用page cache,即下文中的buffer,解决磁盘Io的性能瓶颈。
内核延迟写到buffer。当一个进程发起一个写请求,数据拷贝到buffer。buffer被标记为脏页。
写请求当写到buffer后就立即返回。如果另一个写请求写同样的数据块,则这个buffer被更新为新的数据。
最终。脏页需要被提交到磁盘。这被称为写回磁盘
2.1.1 写回磁盘满足的条件
写回磁盘需要满足两个条件
- 当剩余内存小于配置的阀值时,脏页被写回磁盘为了释放内存。
- 当脏页的时间大于阀值时,脏页被写回磁盘,避免数据无限的驻留在buffer中。
写入磁盘时,linux使用pdfflush线程
。pdflush (page dirty flush)线程
当linux初始化多pdflush线程并行的写多个脏页到磁盘中。
2.2.延迟写的弊端
延迟写虽然是很快的,但是当突然断电时,数据可能被丢失。
一些关键的应用,为了保证数据不被丢失。应该使用同步I/O
2.3 write函数定义
ssize_t write (int fd, const void *buf, size_t count);
一个write调用把buf 中的count个字节到文件的当前位置。
write调用成功将返回写入的字节数,并更新文件的当前位置。-1代表写入错误。write可能返回0,但是返回0不代表任何意义,仅代表当前有0个字节被写入
2.4 写入时产生的错误
部分写
产生的原因是由于在写入的过程中信号中断。部分写只存在网络Io操作。
磁盘文件不存在部分写,总是完整的写入。因此不需要执行循环写。要么成功,要么失败。
2.5 对于socket等非磁盘文件,需要重视部分写
ssize_t ret, nr;
while (len != 0 && (ret = write (fd, buf, len)) != 0) {
if (ret == -1) {
//信号中断
if (errno == EINTR)
continue;
perror ("write");
break;
}
len -= ret;
buf += ret;
}
2.6 应用程序如何控制pagecache的数据写到磁盘
fsync函数
int fsync(int fd);
fsync保证写回数据及元数据,阻塞直到成功。
fsync的调用保证所有的脏数据对应于文件描述符fd的讲被直接写会到磁盘
fdatasync函数
fdatasync仅仅刷新数据到磁盘。不刷新metadata。因此fdatasync更加高效。
fync及fdatasync的缺点
这两个函数都不能保证任何包含更新文件的目录文件同步到磁盘。这隐藏的问题是一个文件最近被更新,并且写到磁盘。但是文件目录里对此文件有可能是不可达的。
为了保证对文件目录中任何文件项的更新被提交到磁盘。需要在打开的文件目录上再次调用fync。保证文件目录的元数据被提交到磁盘。
sync函数
sync函数不需要任何参数。他讲同步所有的buffers到磁盘。
sync在一个非常忙的系统上可能非常耗时。
2.7 写append模式
1.写append 模式
当一个文件被打开与O_APPEND模式时。文件position总是出现在文件尾部。
如果不使用O_APPEND模式,当两个进程同时写一个文件时在文件尾部,可能前一个进程的写会被后一个进程的写搽掉,因此存在条件竞争。
append的优点
append模式可以避免上面的问题,即两个进程同时写的条件竞争。append模式保证文件的position总是设置到文件的尾部。当有多个进程append文件时,你可以认为每一次写请求对文件的位置总是原子更新。
append模式的使用场景
append模式时候更新log 文件。
2.8 同步写操作
2.8.1 直接IO
为了避免在电源突然关闭时,丢失缓存中的数据。linux系统提供了直接IO
直接IO绕过pagecache缓存区。所有的写请求都是同步的,直接写到磁盘。操作讲被阻塞知道完成。
2.8.2 使用场景
关系型数据库自己在应用层实现了缓存,绕开pagecache缓存区。所有的写请求直接写到磁盘。
2.8.3 API
当使用open时提供O_DIRECT选项表示直接使用直接IO
2.8.4 限制
当使用直接IO时,请求的长度,buffer对齐,文件的offset必须是底层磁盘扇区的整数倍。
3.文件读
3.1read函数
ssize_t read (int fd, void *buf, size_t len);
read函数从文件的当前位置读len个字节到buf中。一旦成功,返回写入buf中的字节数;一旦失败,返回-1 并且errno被设置。文件的position增加读入buf中的字节数。
3.2 read函数返回部分数据
read函数返回小于len的字节是合理的。这发生有以下原因:
本来可读的字节小于len
read()函数进行系统调用时被signal信号中断,如果是pipe则会被中断返回。
3.3 read返回0(EOF)代表读到文件尾部在阻塞模式
如果一个read调用len个字节,这个调用讲阻塞直到bytes变成可用。
3.4 read返回0代表没有文件内容可读在非阻塞模式下
3.5举例
ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
if (ret == -1) {
//发送信号中断
if (errno == EINTR)
continue;
perror ("read");
break;
}
len -= ret;
buf += ret;
}
//非阻塞读
char buf[BUFSIZ];
ssize_t nr;
start:
nr = read (fd, buf, BUFSIZ);
if (nr == -1) {
if (errno == EINTR)
goto start; /* oh shush */
if (errno == EAGAIN)
/* resubmit later */
else
/* error */
}
4.高级io epoll
4.1为什么linux需要epoll接口
早期linux提高了select(),poll()接口。select(),poll()在内核中维护一个文件描述符列表进行监控。系统每次调用select,poll都要遍历一遍文件描述符列表,因此当文件列表符变大后,存在水平扩展的问题。
4.2 epoll 原理
epoll解决了select,poll的问题。
通过系统调用创建epoll的上下文
添加被监控或者移除被监控的文件描述符从上下文中。
等待实际事件的发生。不去进行轮询。
创建epoll上下文
int epoll_create (int size)
epoll_create创建成功后返回一个文件描述符,不是真正的文件。因此在epoll结束后需要调用close()方法关闭epoll_create创建的文件。
添加或者删除被监控的文件描述符及需要监控的事件
int epoll_ctl (int epfd,
int op,
int fd,
struct epoll_event *event);
struct epoll_event event;
int ret;
//实践中通常把fd赋值给data。这样当事件产生后,就可以知道哪个文件描述符产生了事件。
event.data.fd = fd; /* return the fd to us later */
event.events = EPOLLIN | EPOLLOUT;
ret = epoll_ctl (epfd, EPOLL_CTL_ADD, fd, &event);
if (ret)
perror ("epoll_ctl");
等待事件发生
int epoll_wait (int epfd,
struct epoll_event *events,
int maxevents,
int timeout);
epoll_wait等待timeout直到超时,一旦事件产生,那么events指向内存中的epoll_event结构,直到超过maxevents数。
返回值是事件的个数。
举例
#define MAX_EVENTS 64
struct epoll_event *events;
int nr_events, i, epfd;
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.
*/
}
free (events);
5. 分散 聚集io
5.1 分散IO
通过一次系统读请求把一个流分散到多个buffer。
5.2聚集IO
在Linux系统可以通过一次系统写调用进把多个buffer聚集成单一的系统流。
5.3分散/聚集IO的优点
减少系统调用的次数。提高系统性能。
分散/聚集io的操作是原子的。
5.4 优化buffer的count数
由于buffer的count是变化的,因此linux内核会使用动态分配内存划分内存为了存储buffer数据段。
为了优化性能。如果count足够小(<=8),那么linux内存会在栈上维护一个小的segment数组用来存放buffer数据段。
因此选择一个<=8的buffer将能提高性能。