linux io 原理

内容纲要

1. 综述

io 操作是服务端开发人员必须掌握的操作,因此,掌握Io的原理特别重要。

2. 异步写操作原理

2.1 pagecache

linux 系统默认使用page cache,即下文中的buffer,解决磁盘Io的性能瓶颈。

内核延迟写到buffer。当一个进程发起一个写请求,数据拷贝到buffer。buffer被标记为脏页。

写请求当写到buffer后就立即返回。如果另一个写请求写同样的数据块,则这个buffer被更新为新的数据。

最终。脏页需要被提交到磁盘。这被称为写回磁盘

2.1.1 写回磁盘满足的条件

写回磁盘需要满足两个条件

  1. 当剩余内存小于配置的阀值时,脏页被写回磁盘为了释放内存。
  2. 当脏页的时间大于阀值时,脏页被写回磁盘,避免数据无限的驻留在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将能提高性能。

发表评论

邮箱地址不会被公开。 必填项已用*标注