1. I/O概述
1.1 文件类型
UNIX将系统所有的内容都视为文件,其中文件类型包括如下几种:
-
普通文件(regular file):这是最常用的文件类型,对于这种数据是文件还是二进制数据,对于UNIX内核而言并无差别。
-
目录文件(directory file):这种文件包含了其它文件的名字以及指向与这些文件有关信息的指针。
-
块特殊文件(block special file):这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
-
字符特殊文件(character special file):这种 类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
-
FIFO:这种类型的文件用于进程间通信,有时也成为命名管道(named pipe)。
-
套接字(socket):这种类型的文件用于进程间的网络通信。桃姐也可用于在一台宿主机上进程之间的非网络通信。
-
符号连接(symbolic link):这种类型的文件指向另一个文件。
1.2 I/O模型
所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call)。库函数是指为了迎合程序员使用,而编制的通用库函数,虽然这些函数可能会调用一个或多个内核的系统滴啊用,但是它们并不是内核的入口点。例如,printf函数会调用write(系统调用)输出一个字符串。
从实现者的角度来看,系统调用和库函数之间有根本的区别,但从用户角度来看,其区别并不重要。但是需要注意的是:我们可以替换库函数,而通常不能替换系统调用。
在UNIX下可用如下的几种I/O模型:
-
阻塞式I/O模型
-
非阻塞式I/O模型
-
I/O复用:
-
信号驱动I/O(基于套接字的异步I/O机制)
-
异步I/O
-
存储映射I/O
2. 阻塞式I/O模型
阻塞式模型I/O是指当调用此类函数时,就会发生阻塞,直至函数返回。如下图所示:
阻塞式模型又可以分为两种模型:不带缓冲的函数和带缓冲的函数(标准IO)。
2.1 不带缓冲的函数
不带缓冲是指每个read和write都调用内核中的一个系统调用。其中有:open、read、write、lseek以及close。这些不带缓冲的I/O函数不是ISO C的组成部分,而是POSIX.1和Single UNIX Specification的组成部分。
2.2 带缓冲的函数
标准I/O库是一类带缓冲的函数,其是ISO C的标准,标准I/O库是对上述的不带缓冲的系统调用函数进行封装和调用,其不属于UNIX中的系统函数,但标准I/O不仅在UNIX中能得到使用,在其它系统也能使用。
(一)缓冲
标准I/O提供了3种类型的缓冲:
1) 全缓冲: 即只在填满缓冲区后才进行实际I/O操作。
对于驻留在磁盘上的文件通常是全缓冲的,
2) 行缓冲:是指当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。
这允许我们一次输出一个字符,但只有在写了一行之后才进行实际I/O操作。当涉及一个 终端时(如标准输入和标准输出),通常使用行缓冲。
3) 不带缓冲:标准I/O库不对字符进行缓冲存储,即马上进行I/O操作。
例如标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符立即输出。其中标准错误流stderr通常是不带缓冲的。
小结:标准错误时不带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的。
(二)标准I/O函数
标准I/O函数也是先打开(open)对流文件,然后进行读或写,其中一旦打开了流,则可进行3中不同类型的非格式化I/O操作:
-
每次一个字符的I/O:一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理了所有缓冲。
-
每次一行的I/O:使用fgets和fputs可以一次读写一行,每行都以一个换行符终止。
-
直接I/O:fread和fwrite是每次读写某种数量的对象。
3. 非阻塞式I/O模型
3.1 阻塞的原因
系统调用分成两类:"低速"系统调用和其它。但低速的系统调用可能会使进程永远阻塞的一类系统调用,包括:
3.2 定义
非阻塞I/O是指我们发出的open、read和write等I/O操作,并使这些操作不会永远阻塞。如果这样的操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。如下图所示:
前三次调用recvfrom时没有数据可返回,因此内核立即返回一个EWOLDBLOCK错误,第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。
3.3 使用方式
对于给定的描述符(文件描述符),有两种为其指定费阻塞I/O的方法:
-
如果调用open获得描述符,则可指定O_NONBLOCK标志。
-
对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志
非阻塞式I/O可以应用到管道、FIFO、套接字、终端、伪终端以及其他一些类型的设备上。其中的文件描述符可以是由open函数的返回值,也可以是网络编程的socket值,如《UNP》343对TCP设置为非阻塞的方式:
int val,sockfd;
sockfd = socket(…)//打开socket
val = fcntl(sockfd, F_GETFL, 0); //获取文件状态标志
fcntl(sockfd, F_SETFL, val | O_NONBLOCK); //在原来的状态标志中添加非阻塞模型
4. I/O多路复用
I/O多路复用是指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。主要由select,poll和epoll来实现这种I/O多路复用的机制。但select,poll,epoll本质上都是同步I/O(即会阻塞等待),因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
其中需先了解两种通知模式
如下表总结了I/O多路复用、信号驱动I/O以及epoll所采用的通知模型:
|
I/O模式 |
水平触发 |
边缘触发 |
|
select(),poll() |
l |
|
|
信号驱动I/O |
|
l |
|
epoll() |
l |
l |
4.1 select函数
select函数关注的问题是:在指定的时间内所关心的描述符集是否准备就绪。
4.1.1 函数原型
int select(int maxfd, fd_set *readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * tvptr)
返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1
? maxfd:最大文件描述符编号值加1;
? readfds 、writefds、exceptfds:分别指向所关心的描述符集的指针;
? tvptr:指定愿意等待的时间长度。
4.1.2 注意问题
-
readfds等是值结果参数,会被函数修改;
-
要注意计算maxfd,其是最大文件描述符编号值加1;
-
tvptr如果为NULL表示阻塞等;如果tvptr指向的时间为0,表示非阻塞;否则表示select的超时时间;
-
select返回-1表示错误,返回0表示超时时间到没有监听到的事件发生,返回正数表示监听到的所有事件数(包括可读,可写,异常);
-
Linux的实现中select返回时会将tvptr修改为剩余时间,所以重复使用tvptr需要注意。
4.1.3 select的缺点
-
由于描述符集合set的限制,每个set最多只能监听FD_SETSIZE(在Linux上是1024)个句柄(不同机器可能不一样);
-
返回的可读集合是个fd_set类型,需要对所有的监听读句柄进行FD_ISSET的一一测试来判断是否可读;
-
每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
4.2 poll函数
poll的功能和select类似,只是修改了select的函数原型,poll不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。
4.2.1 函数原型
pollfd结构
struct pollfd {
int fd; /* file descriptor to check, or <0 to ignore */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};
poll函数原型为:
int poll(struct pollfd fdarrays[], nfds_t nfds, int timeout);
返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1
4.2.2 注意问题
-
nfds表示监听的fdarrays的长度,如果fdarrays [i].fd < 0,则poll忽略这样的pollfd;
-
timeout是以ms为单位的超时时间;若为-1,表示永远等下去;0表示立即返回不等待;
-
poll在处理流设备时能提供额外的信息;
-
使用poll不需要显式地监听异常事件,使用poll如果pollfd异常,则内核会设置revents的POLLERR位;
4.2.3 和select相比,poll的优点是
-
不再局限于FD_SETSIZE个监听描述符,只要能打开的描述符,都可以监听;
-
timeout参数不会被函数修改,分辨率较低些(但实际上没有影响);
-
监听描述符集合不再是值结果参数,而是event表示监听事件,revents表示触发的事件;
-
poll的效率比select稍高(poll只遍历输入的监听数组中的描述符,如果数组中的fd<0,则poll忽略fd,当监听的描述符离散时效率稍高于select,比如监听0和1000两个句柄,则poll只需要遍历两个描述符,而select需要遍历1001个描述符;当监听描述符连续时,poll和select效率相当,底层实现也是一致的)。
4.2.4 poll缺点
poll和select共同的问题是性能较差:遍历所有的文件描述符,当监听描述符个数增加时,监听效率降低,并且select和poll每次都要在用户态和内核态拷贝监听的描述符参数。
4.3 epoll函数
epoll是Linux所独有的,所以epoll的移植性没有select和poll好,但epoll既支持水平模式,又支持触发模式。
epoll API的核心数据结构称作epoll实例,它和一个打开的文件描述符相关联。通过这个描述符实现如下的目的:
4.3.1 epoll操作函数
epoll API由一下3个系统调用组成。
-
epoll_create创建一个epoll实例,返回代表该实例的文件描述符。
-
epoll_ctl操作epoll描述符实例相关联的兴趣列表。
-
epoll_wait返回epoll实例相关联的就绪列表中的成员。
1) epoll_create函数:创建epoll实例
int epoll-create(int size);
成功返回描述符,错误返回-1
参数size指定了内部数据结构的划分初始大小,而不是最大上限(从Linux2.6.8版就忽略该值)。
2) epoll_ctl函数:修改epoll兴趣列表
int epoll_ctl(int epfd, int op, struct epoll_event* ev)
成功返回0,错误返回-1
参数fd指定感兴趣列表中的哪一个文件描述符的设定。op值可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL分表对epoll兴趣列表进行增加、修改和删除操作。ev是指向epoll_event的指针,结构体的定义如下:
struct epoll_event{
unit32_t events;
epoll_data_t data;
}
其中的data是如下的一个联合体类型:
typdef union epoll_data {
void *ptr;
int fd;
unit32_t u32;
unit64_t u64;
}epoll_data_t;
其中ev为文件描述符fd所做的设置如下:
-
events是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合。
-
data是一个联合体,当描述符fd稍后成为就绪态时,联合体的成员可用来指定传回给调用进程的信息。
3) epoll_wait函数:事件等待
系统调用epoll_wait()返回epoll实例中处于就绪状态的文件描述符信息。单个epoll_wait()调用能返回多个就绪态文件描述符的信息。
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout)
返回准备就绪的文件描述符,0超时,-1出错
evlist所指向的结构体数组中返回的就绪文件描述符信息,maxevents是指定的evlist数组的大小。
在数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。
timeout用来确定epoll_wait()的阻塞行为,有如下几种。
4.3.2 epoll的优点
epoll解决了select和poll的几个性能上的缺陷:
-
不限制监听的描述符个数(poll也是),只受进程打开描述符总数的限制;
-
监听性能不随着监听描述符数的增加而增加,是O(1)的,不再是轮询描述符来探测事件,而是由描述符主动上报事件;
-
使用共享内存的方式,不在用户和内核之间反复传递监听的描述符信息;
-
返回参数中就是触发事件的列表,不用再遍历输入事件表查询各个事件是否被触发。
-
epoll显著提高性能的前提是:监听大量描述符,并且每次触发事件的描述符文件非常少。
5. 信号驱动I/O
5.1 定义
信号驱动I/O是指请求数据的进程可以利用信号,向内核声明一个信号处理例程,让内核在描述符就绪时发送SIGIO信号(默认情况下)通知进程;而进程在向内核声明后,能够处理其他的任务,当I/O操作可执行时通过接受信号来获得通知。如下图所示:
5.2 步骤
要使用信号驱动I/O,程序需要按照如下步骤来执行:
-
为内核发送的通知信号安装(通过sigaction函数)一个信号处理例程。默认情况下,这个通知信号为SIGIO.
-
设定文件描述符的属主,即当文件描述符上可执行I/O时,接收到通知信号的进程或进程组。通常让调用进程成为属主,并通过fcntl()的F_SETOWN操作完成属主的设定:
fcntl(fd, F_SETOWN, pid);
-
通过设定O_NONBLOCK标志使能非阻塞I/O。
-
通过打开O_ASYNC标志使能信号驱动I/O。可以与上一步合并为一个操作。
flags= fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags |ASYNC | NONBLOCK);
-
调用进程现在可以执行其他的任务了。当I/O操作就绪时,内核为进程发送一个信号,然后调用在第1步中安装好的信号处理例程。
-
信号驱动I/O提供的是边缘触发通知。这表示一旦进程被通知I/O就绪,它就应该尽可能多地执行I/O(如尽可能多地读取字节)。
6. 异步IO
从历史上信号驱动I/O有时也称为异步I/O,但是,如今的异步I/O(POSIX AIO规范)机制是进程请求内核执行一次I/O操作,内核启动该操作之后立刻将控制权还给调用进程,并且当内核在整个操作完成(包括将数据从内核复制到进程的缓冲区)或错误发生时,该进程会得到通知。如下图所示:
其中信号驱动I/O与异步I/O的区别是:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作(即何时就绪);而异步I/O模型是通知我们I/O操作何时完成。
7. 存储映射I/O
7.1 定义
存储映射I/O 能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中读取数据时,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样,就可以在不适用read和write的情况下执行I/O。
映射分为两种类型:
并且进程之间可以共享存储映射区域,所以又可以将映射的区域分为私有映射和共享映射:
7.2 使用
为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。
void *mmap(void* addr, size_t len, int prot, int floag, int fd, off_t off);
返回值:若成功,返回映射区的起始地址;若出错,返回MAP_FAILED
当进程终止时,会自动解除存储映射区的映射,货值直接调用munmap函数也可以解除映射区。
int munmap(void* addr, size_t len);
返回值:若成功,返回0;若出错,返回-1;
Linux IO
原文:http://www.cnblogs.com/hlwfirst/p/5004611.html