Linux 系统编程导读 - 高级文件I/O

散布/聚集 I/O

目的是在单次系统调用(I/O操作)中操作多个缓冲区。按我们平时的读写操作,** read ** 以及 ** write *,都是将数据读入单个缓冲区,从单个缓冲区中写出。
同样也有对应的操作接口:

1
2
3
#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);

都包含三个参数,fd是需要操作的文件;第二个是iovec结构的数组,即缓冲区,第三个指示数量。
iovec结构
1
2
3
4
struct iovec {
void *iov_base;
size_t iov_len;
};

用基址和长度来描述一个缓冲区。读入的数据将放入iov_base~iov_base+iov_len这片内存区域中。
segment用来表示一个缓冲区。
*
读操作 *:从文件中读入count个segment到 iov指向的缓冲区数组中。
*
写操作 **;从iov指向的缓冲区数组中读取count个segment写入到指定的文件中。
返回值:读写的字节数,操作成功将返回count个segment的所有iov_len之和

EPOLL

EPOLL还是用于在一个进程中同时监控多个文件描述符相关的事件,是相对于POLL和SELECT的优化。最大的优势就是在事件发生后能够知道是与哪一个文件描述符相关的事件,而POLL将需要遍历一次所有文件描述符才能确定是对哪个文件描述符的操作,因此效率很低。
下面是关于EPOLL的使用方法:

创建EPOLL

1
2
#include <sys/epoll.h>
int epoll_create (int size)

size仅为通知内核需要监听的文件描述符个数,并不代表是最大值。

控制EPOLL

1
2
#include <sys/epoll.h>
int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event);

* epoll_ctl *用于控制EPOLL实例加入或者删除文件描述符。参数op用于指定需要的操作,包括:
1
2
3
EPOLL_CTL_ADD:增加一个文件描述符操作
EPOLL_CTL_DEL:删除一个文件描述符操作
EPOLL_CTL_MOD:修改一个文件描述符上监听的事件

指定需要监听的事件,epoll_event;
1
2
3
4
5
6
7
8
9
struct epoll_event {
__u32 events; /* events */
union {
void *ptr;
int fd;
__u32 u32;
__u64 u64;
} data;
};

events参数指定了需要对文件描述符监听的事件。
一个简单的例子,来自书上:
1
2
3
4
5
6
7
8
struct epoll_event event;
int ret;
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”);

等待EPOLL事件

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

* epoll_wait *用于等待EPOLL实例epfd中文件描述符所包含的事件,时限为timeout毫秒(若timeout=0,则立刻返回;timeout=-1则一直等待)。
成功返回时,events指向包含epoll_event结构体的内存。
失败返回-1。

边沿触发事件和水平触发事件

水平触发:默认选项,在状态发生时触发。
边沿触发:设置events项为EPOLLE,在状态改变时触发。

存储映射

将文件中的内容与内存进行映射,使得访问文件和访问内存一致。

1
2
#include <sys/mman.h>
void * mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset);

* mmap * 用于将文件fd从offset偏移往后 len个字节内容映射到内存中。prot指示内存的访问权限。flags指示映射的一些行为。
当映射一个文件时,其文件描述符引用计数会加1,即映射后关闭文件仍能够访问文件;当取消映射后,文件描述符引用计数会减1
调用成功,返回映射区的地址
调用失败,返回MAP_FAILED

两个信号

SIGBUS,当尝试访问一块无效的映射区域时触发
SIGSEGV,当尝试写一块只读区域时触发

页大小

页是内存中允许不同权限和行为操作的最小单元。
页是内存映射的基本块,也是进程内存空间的基本块。
因此 mmap 映射对应的addr和len都需按页对齐,即页的整数倍,对于多余的内存空间用0填充。
几种获取页大小的方法:
* sysconf * POSIX标准接口

1
2
3
4
#include <unistd.h>
long sysconf (int name);

long page_size = sysconf (_SC_PAGESIZE);

* getpagesize * Linux提供的接口,并不是所有的unix系统都支持。
1
2
#include <unistd.h>
int getpagesize (void);

* PAGE_SIZE * 宏定义
1
int page_size= PAGE_SIZE ;

munmap

* munmap *用于取消映射

1
2
#include <sys/mman.h>
int munmap (void *addr, size_t len);

成功返回0,失败返回-1。
当映射解除之后,之前关联的内存区域不再有效,如果尝试访问将产生 SIGSEGV信号。

调整映射大小 mremap

* mremap *是Linux特有的操作
Flag的值可以是0或者 MREMAP_MAYMOVE,指定在大小调整时,是否可以移动映射以达到大小调整的目的。

1
2
3
4
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/mman.h>
void * mremap (void *addr, size_t old_size, size_t new_size, unsigned long flags);

成功返回新映射区的指针,失败返回MAP_FAILED
glibc基于mremap实现高效的realloc,用于调整malloc所分配的大小

改变映射区权限 mprotect

用于改变内存区域的权限,prot可取的值为:PROT_NONE、PROT_READ、PROT_WRITE、PROT_EXEC。Linux下mprotect可以操作任意区域的内存,也就类似于Windows下的VirtualProtect,可以绕过NX

1
2
#include <sys/mman.h>
int mprotect (const void *addr, size_t len, int prot);

映射机制同步文件 msync

用于将映射在内存中的文件写回磁盘,同步内存和文件,addr通常为mmap的返回值
flags 控制同步操作的行为:
MS_ASYNC:同步操作异步执行
MS_INVALIDATE:指定该块映射的其他所有拷贝都将失效,未来对该文件任意映射操作将同步到磁盘
MS_SYNC;同步操作同步执行,必须写回磁盘再返回

1
2
#include <sys/mman.h>
int msync (void *addr, size_t len, int flags);

成功返回0,失败返回-1

映射提示

让进程在如何访问映射区域上** 给内核一定的提示 **,相当于给内核发送一些提示消息。

1
2
#include <sys/mman.h>
int madvise (void *addr, size_t len, int advice);

普通文件I/O提示

普通文件I/O操作时,** 给内核一定的提示 **
两个接口 posix_fadvise 和 readahead(Linux特有)

1
2
3
4
#include <fcntl.h>
int posix_fadvise (int fd, off_t offset, off_t len, int advice);

ssize_t readahead (int fd, off64_t offset, size_t count);

一个很使用的场景,当读取一个文件的大部分内容时,可以先通知内核 要进行预读数据,设置POSIX_FADV_SEQUENTIAL,提示要大量预读;若是只读很少的数据,后面没有读操作,那么设置POSIX_FADV_RANDOM提示禁止预读效果会更好。

同步(Synchronized), 同步(Synchronous) 和 异步(Asynchronous)

Synchronized 写操作把数据写回硬盘,并确保硬盘上的数据和内核缓冲区中的数据是同步的;
Synchronous 写操作保证数据全部写入到内核缓冲区,并不涉及到磁盘

Synchronous 读操作确保数据写道应用程序在用户空间缓冲区(即写入用户缓冲区)
Synchronized 确保返回的是最新的数据到变量内存

异步I/O

Linux 下的 aio库,用于异步读写等操作接口

1
2
3
4
5
6
#include <aio.h>
int aio_read (struct aiocb *aiocbp);
int aio_write (struct aiocb *aiocbp);
int aio_error (const struct aiocb *aiocbp);
int aio_return (struct aiocb *aiocbp);
(......)

Linux 只支持使用O_DIRECT标志打开文件上的aio。

I/O调度器 和 I/O性能

主要是在对磁盘中数据的访问效率很低,通过一些方式来优化对磁盘的操作。

磁盘寻址

硬盘基于 柱面(cylinders)、磁头(heads)、扇区(section)来确定地址,CHS寻址
一个硬盘看成是一个圆柱体,可以看成是有多个圆形成,每个圆称之为一个盘片。柱面则是所有与圆中心距离相等的点所组成的一个圆表面。
** 寻址 **:柱面确定了一个子圆柱表面;磁头确定了所处的盘片;扇区则确定了该盘片中的确切位置。

调度器的优化

调度器的优化包括2个部分:

  1. 合并,对一些相邻块的请求,进行合并一次操作
  2. 排序,按照块号递增的顺序依次访问磁盘,这样减少了磁盘头的移动距离

    改善读请求

    读请求的几种优化算法
  3. Deadline I/O 调度器
    维护了三个队列,一个标准的队列,是经过排序的I/O等待列表(一般都是按照块号排序);两个FIFO队列,一个处理读操作,一个处理写操作。
    当来一个请求后,会首先插入到标准队列中,并依据读写,放在指定的FIFO队列队尾,对于FIFO队列的每一个元素都将设置一个过期时间。一般情况,硬盘总是先从标准队列队头选择一个操作,若FIFO队列头超过了过期时间,将优先处理FIFO队列中的请求。
  4. Anticipatory I/O 调度器
    该方法是为了优化这么一个情况:当延迟很小的时候,那么就会经常性的出现过期,那么就需要处理FIFO的请求;但是稍微等待一会会有另一个相邻位置的请求存在,那么就可以一起处理节省寻道时间了。
    其主要思想就是在过期时间到来后,不立马响应而是等待一会,如果在指定时间内收到同一部分的另一次读请求,将立马被响应;若没有响应,则认为预测失败,回归正常操作。
  5. CFQ I/O 调度器
    CFQ(complete Fair Queuing) 主要是应用于多进程的处理,对于每一个进程提供一个队列。
  6. Noop I/O 调度器
    只做简单的合并,不做排序操作。

** 配置I/O调度器 ** 在启动时通过内核参数iosched来指定,有效选项为 as、cfq、deadline和noop;运行时通过下面操作设置。

1
#echo cfq >/sys/block/hda/queue/scheduler

* /sys/block/device/queue/iosched * 目录下包含管理员可以获得和设置的I/O调度器选项。

ollydbg run trace Linux 系统编程导读 - 缓冲输入输出
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×