Hello World

行走即是圆梦,回望亦是前行。

0%

linux五种IO模型

前言

这里简单记录下linux五种IO模型总结

基本概念说明

参考博客

用户空间和内核空间
进程切换
进程阻塞
文件描述符fd
缓存I/O

用户空间和内核空间

现代操作系统都是采用虚拟存储器,对32位操作系统而言,它的寻址空间(虚拟存储空间)为4GB(232)。操作系统的核心是内核(kernel),独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分:内核空间和用户空间。针对linux操作系统而言,将最高的1GB字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间;而将降低的3GB字节(从虚拟地址0x00000000到0xBFFFFFFF),供应用程序使用,称为用户空间。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程执行,这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
进程切换,即从一个进程的运行切换到另一个进程上的运行,这个过程经过下面的变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器;
  2. 更新PCB(Process Control Block,进程控制块)信息;
  3. 把PCB移入相应的队列,如就绪、在某事件阻塞等队列;
  4. 选择另一个进程执行,并更新其PCB;
  5. 更新内存管理的数据结构;
  6. 恢复处理机的上下文。

总而言之,进程切换就是很消耗资源。

进程阻塞

正在执行的进程,由于等待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据未达到或无新任务执行等,则由系统自动执行阻塞原语(Block),使进程由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态(获得CPU)的进程,才可能将其转为阻塞状态。当进入阻塞状态,进程不占用CPU资源

文件描述符fd

文件描述符(File Descriptor)是计算机科学中的一个术语,是用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开,但是文件描述符只适用于unix和linux操作系统。

缓存I/O

缓存I/O又被称作标准I/O,大多数文件系统的默认I/O都是缓存I/O。在linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存I/O的缺点:
数据在传输过程中需要在用户空间和内核空间之间进行多次的数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。

五种IO模型

参考博客
参考书籍:《unix网络编程 卷一》第2部分 6.2 I/O复用:select和poll函数

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓存区。第二步就是把数据从内核缓冲区拷贝到应用进程缓冲区中。通常包含两个不同的阶段:

  1. 等待数据准备好;
  2. 从内核向进程拷贝数据。

正因为这两个阶段,linux系统产生了以下五种网络模型:

阻塞IO模型(blocking IO)
非阻塞IO模型(nonblocking IO)
IO多路复用模型(IO multiplexing)
信号驱动IO模型(signal driven IO)
异步IO模型(asynchronous IO)

阻塞IO模型(blocking IO)

A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。

A拿的鱼竿就是一个文件描述符
在内核将数据报准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。这个模型是我们最常见的,程序调用和我们编写的基本程序是一致的,程序的read必须在write之后执行,当write阻塞住,read就不能执行,一直处于等待状态。

1
2
3
4
auto fd = connect();
write(fd);
read(fd);
close(fd);

非阻塞IO模型(nonblocking IO)

B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。

B在检查鱼竿是否有鱼,是一个轮询的过程。
每次用户询问内核是否有数据报准备好,即文件描述符缓存区是否就绪。当有数据报准备好时,就进行拷贝数据报的操作。当没有数据报准备好时,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一个轮询。
但是,轮询对于CPU来说是较大的浪费,一般只有在特定的场景下才使用。

IO多路复用模型(IO multiplexing)

C同样也在河边钓鱼,但是C生活水平比较好,C拿了很多的鱼竿,一次性有很多鱼竿在等,C不断的查看每个鱼竿是否有鱼上钩,有鱼上钩即可将鱼钓上来。

IO多路复用是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。
其中,select只负责等待,recvform只负责拷贝。
IO多路复用属于阻塞IO,但是可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。

信号驱动IO模型(signal driven IO)

D也在河边钓鱼,但与A、B不同的是,D比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,D就会将鱼钓上来。

D鱼竿上的铃铛就是一个信号
信号驱动IO模型,应用进程告诉内核,当数据报准备好的时候,给应用进程发送一个信号,对SIGIO信号进行捕捉,并且调用应用进程的信号处理函数来获取数据报。

异步IO模型(asynchronous IO)

E也想钓鱼,但E有事情,于是他雇来了F,让F帮他拿着鱼竿钓鱼,一旦有鱼上钩,F就会把鱼钓上来通知给E,并把鱼送给E。

当应用程序调用aio_read时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。
当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回aio_read中定义好的函数处理程序。
很少有linux系统支持异步IO,windows的IOCP就是该模型。

各种IO模型的比较

总结:阻塞程度由高到低,效率由低到高

1
阻塞IO > 非阻塞IO > IO多路复用 > 信号驱动IO > 异步IO

IO多路复用模型(重点)

参考博客

select

1
int select (int maxfdp1, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

【参数说明】
【返回值】

poll

1
int poll (struct pollfd* fds, unsigned int nfds, int timeout);

【参数说明】
【返回值】

epoll

1
2
3
int epoll_create(int size);
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);

【参数说明】
【返回值】

三种机制比较

selectpollepoll
操作方式遍历遍历回调
底层实现数组链表哈希表
时间复杂度O(n)O(n)O(1)
IO效率每次调用都进行线性遍历每次调用都进行线性遍历事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面
最大连接数1024(x86)/2048(x64)无上限无上限
fd拷贝每次调用select,都需要把fd集合从用户态拷贝到内核态每次调用poll,都需要把fd集合从用户态拷贝到内核态调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝