IO模型是什么?

个人理解IO模型就是应用程序进行IO操作时和操作系统的通信和协作方式。

为什么要学习IO模型?

我个人在学习Netty、Mina等网络编程框架时,搜索网上资料以及看书的过程中,经常会碰到一些名词:同步、异步、阻塞、非阻塞、事件驱动、轮询等等。这些名词乍一看,貌似我都懂,但是仔细分析时又发现似懂非懂,其实根本原因还是对于Unix的IO模型没有完全理解。

Unix中有哪些IO模型?

Unix中总共定义了五种IO模型:

  1. 阻塞式IO模型
  2. 非阻塞式IO模型
  3. IO复用模型
  4. 信号驱动式IO模型
  5. 异步IO模型

图例说明

下面的五幅IO模型图均出自《Unix网络编程卷一:套接字API》。这些图均是以UDP编程API为例子来说明,所以不会看到熟悉的TCP方法。理解这些图,首先需要对于Unix操作系统有一定深度的理解,内核空间和用户空间要明白各自是什么?并且要明白,应用程序和内核有各自的缓冲区,这也是理解异步IO的关键所在!

1.阻塞式IO模型

2.非阻塞式IO模型

阻塞和非阻塞的关键在于应用进程的系统调用是否立即返回?如果立即返回,则称之为非阻塞;反之亦然。对于非阻塞,显然是以应用进程的反复的系统调用为代价来换取及时响应,以达到释放应用进程的目的。从性能角度讲,单从一次完整的IO调用(从发起调用到获得结果)来看,非阻塞的多次系统调用会增加CPU的负载;但是如果从大量的IO调用来看,阻塞IO会占用大量的进程,而进程又是一个稀缺资源,并且进程的增加同样会因为CPU进行大量的上下文切换而严重增加CPU的负载!看来量变引起质变的原理,同样适用于这里!

3.IO复用模型

很多人对于IO复用模型和非阻塞模型比较困惑,没有理解两者之间的那点区别!其实,IO复用的重点是“复用”二字,其核心概念就是一个进程来监控多个Socket的IO事件;而对于非阻塞IO来说,一个进程只能监控一个Socket的IO事件。在编码层面,就是利用轮询函数来实现对于多个Socket的监听,目前的轮询函数包括:select()、poll()、epoll(),这三个轮询函数的区别,后续会单独成文《Linux学习系列-轮询函数》。

4.信号驱动式IO模型

信号驱动式IO模型其实很简单,就是利用IO事件触发的SIGIO信号和其回调函数,在回调函数进行IO处理或者通过回调函数通知应用进程主循环进行IO处理。我们经常在一些网络框架中看到其支持“统一事件源“,其实就是IO复用模型和信号驱动模型的整合,在轮询函数中不但监听IO事件,而且监听信号(事件)。对于”统一事件源“会在《Linux学习系列-轮询函数》中做详细阐述。

5.异步IO模型

从理论上讲,阻塞IO、IO复用和信号驱动IO都是同步模型。因为这三种IO模型中,IO读写操作(将数据从内核缓冲区读入用户缓冲区或将数据从用户缓冲区写入内核缓冲区),都是在IO事件发生之后,由应用程序来完成的。而POSIX规范所定义的异步IO模型则不同。对异步IO而言,用户可以直接对IO进行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及IO操作完成之后内核通知应用程序的方式。异步IO的读写操作总是立即返回,而不论IO是否阻塞的,因为真正的读写操作已经由内核接管。也就是说,同步IO模型要求用户代码自行执行IO操作(将数据从内核缓冲区读入用户缓冲区或将数据从用户缓冲区写入内核缓冲区),而异步IO机制则由内核来执行IO操作(数据在内核缓冲区和用户缓冲区见的移动是有内核在后台完成的)。可以这样理解,同步IO向程序通知的是IO就绪事件,而异步IO向应用程序通知的IO完成事件。

理解这三个轮询函数差异的关键在于理解其轮询的文件描述符(socket也是文件)的数据结构。

select轮询函数

函数定义:

1
2
3
4
5
6
7
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exeptfds, struct timeval *timeout);

// fd_set操作宏
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_ISSET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
  • nfds参数指定被监听的文件描述符总数;
  • readfds、writefds、exeptfds参数分别指向可读、可写和异常事件的accept到的对应的文件描述符集合;fd_set中用一个整型数组的每一个元素的每一位(bit)来表示每一个文件描述符是否有相应的事件发生(0表示有;1表示没有),那么理论上fd_set就可以表示【数组长度*32】个文件描述符,而数组长度由FD_SETSIZE 来设置,一般是1024。在select之前,每次先调用FD_SET()设置accept到的文件描述符标记位为1,表示需要监听这个文件描述符需要监听可读、可写或者异常事件;当有可读、可写和异常事件发生时,在select调用过程中,内核会修改相应的这三个文件描述符集合中的没有事件发生的文件描述符标记位为0;在select之后,应用程序就可以轮询accept到的所有文件描述符,并通过FD_ISSET来判断该文件描述符是否有事件发生(fd_set中对应的文件描述符标记位是1表示有事件发生)。
  • timeout就是轮询时间片,表示多久轮询一次。

这种函数设计实现,有三个缺陷:

  1. 监听的事件类型有限,只有3种;
  2. readfds、writefds和exeptfds有两重角色,第一层是当做要监听的文件描述符集合来传给内核监听;第二层是当做发生事件的文件描述符结果集合。因此每次轮询处理完事件,需要重新设置需要监听的文件描述符;
  3. 事件和文件描述符没有绑定,因此要处理事件需要轮询所有的已accept到的文件描述符。

poll轮询函数

函数定义:

1
2
3
4
5
6
7
8
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd
{
int fd; //文件描述符
short events; //注册的事件
short revents //实际发生的事件,由内核填充
}

  • fds是需要监听的文件描述符集合,它是一个pollfd结构体的数组。
  • nfds是监听的文件描述符的数量,也就是fds数组的长度。

说明:与select相比这种函数设计清晰简洁,将文件描述符和事件通过pollfd结构体来进行了绑定,并且区分了注册的时间和实际发生的事件。但是其和select一样,最终轮询的结果都是所有已经注册的文件描述符的集合。

epoll轮询函数

函数定义:

1
2
3
4
5
6
/* 创建内核事件表,返回的就是事件表的文件描述符 */
int epoll_create(int size);
/* 往内核事件表注册、修改、删除指定fd上的事件 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/* 轮询检测事件 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

  • epoll和select、poll有明显不同,其提供了一套函数。它会在内核开辟一个事件表用于存储注册的事件。
  • epoll_ctl中的epfd就是epoll_create的返回值,表示内核事件表的文件描述符;op就是操作,包括注册、删除、修改;fd表示要操作的文件描述符;event表示要注册到fd上的事件,需要特别之处epoll_event结构实现了事件和文件描述符的绑定。
  • epoll_wait是轮询函数,如果检测到事件,就将所有就绪的事件从内核事件表中的拷贝到第二个参数指向的数组events中。这和select、poll有明显区别,这提高了应用程序轮询就绪事件的效率。
  • epoll还有一个特殊的地方在于,其存在两种模式:LT(电平触发)和ET(边沿触发)。LT是默认工作模式,而当往内核事件表注册一个文件描述符的EPOLLET事件时,epoll将以ET模式来操作文件描述符。ET是一种高效模式。LT模式下,当epoll_wait检测到事件时,应用程序可以不用立即处理,下次epoll_wait时还会再次通告应用程序;ET模式下,应用程序必须立即处理事件,因此避免了epoll_wait重复触发事件的次数,因此较高效。

统一事件源

我们这里讲的统一事件源中的事件源是指IO事件和信号。信号原理可以参见《Linux学习系列-信号》。通常我们通过轮询函数来处理IO事件,既然要统一,那么自然也要使用轮询函数来处理信号。典型的处理方案是:信号发生时,信号处理函数一般通过管道来通知程序主循环信号值,那么主循环中的轮询函数就可以通过轮询函数来监听管道上的IO事件即可。这样就实现了IO事件和信号的统一处理。

注:EXT2文件系统是早期Unix系统采用的文件系统,目前比较新的EXT3也是继承了EXT2大部分特性拓展而来,因此学习Linux文件从EXT2开始会比较好入手。

文件系统是什么?

标准定义参见维基百科【文件系统】。说说我自己作为程序员的理解,从我工作的角度看,狭义点说,文件系统就是专指信息在硬盘上的存储和组织方式。文件是文件系统中组织信息的最小逻辑单元,目录其实也是一种特殊的文件(其内容存储的是普通文件列表)。文件其实是由目录项、i节点和数据块组成的。目录项又是由文件名和i节点编号组成的;i节点中包含了文件的修改时间、类型、数据块指针等等信息。

文件系统是怎样存储信息的呢?

下边的图是根据自己看书的理解绘制而成。这里扯点闲话,自己动手绘图对于加深理解相当重要,绘图的过程其实就是思考和总结的过程,而且比纯文字总结来得更加印象深刻。哪怕是照着书本中的图重新绘制一遍,也有很大作用。