UNIX下的IO模型

Posted by Liao on 2020-06-03

前言

做一个网络服务器,若使用多线程,每一个请求都是一个线程,但会有CPU上下文切换,代价比较高,因此考虑用单线程处理并发问题,这时候需要网络模型。

Unix的IO读写过程

  1. 等待数据准备

  2. 把数据从系统内核拷贝到用户进程

Unix下网络的IO模型(五种)

同步IO

  • 阻塞式IO (Blocking IO)
    • 同步阻塞
  • 非阻塞式IO (NonBlocking IO)
    • 同步非阻塞(就是IO复用 select poll)
  • IO多路复用 (IO multiplexing)
  • 信号驱动型IO (Signal Driven IO) 比较少用

异步IO

  • 异步IO (Asychronous IO)

    • 异步阻塞
    • 异步非阻塞(高性能)

同步阻塞IO

在餐厅点了餐之后,只能在餐厅等,什么事情都做不了。直到菜上了,吃完饭,才可以去逛街shopping(阻塞)

原理

当用户进程执行一个系统调用recv()/recvform(),操作系统要把数据拷贝到操作系统内核的缓存区(缓存IO),这个过程中用户进程会被阻塞,处于一个等待的状态。直到数据准备好,就会把数据从缓存区拷贝到用户内存,然后内核返回结果后,用户进程才接触阻塞状态,重新运行。

同步非阻塞IO(轮询polling)

在等餐过程中,不愿把时间浪费在等待上,而是在等待的期间逛了商场,又回来问服务员菜好了没。如果没有又继续逛,期间又回来问……就这样循环多次的询问

原理

非同步阻塞就是一种轮询的模式,当用户进程调用recvform() (或者说是read操作),如果内核还没有准备好,不会立刻阻塞用户进程,而是返回一个错误。用户进程立即收到结果是错误时,就可以发送下一次的操作。

一旦内核准备好数据,并且接收到用户进程的轮询(system call),就立刻把数据拷贝到用户内存中,返回成功的提示。

缺点:

轮询会消耗CPU的大量时间

IO多路复用之select、poll、epoll

点了菜之后,可以通过小程序看到点餐进度,就可以在等待上菜的时候去逛街,不需要询问服务员菜好了没

IO多路复用是通过一种机制,使得一个用户进程可以监听多个文件描述符(file descriptor,简写fd),当某个fd就绪(读就绪或写就绪),就能通知程序进行相应的读写操作。但select、pselect、poll、epoll本质都是都是同步IO,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞(要等待)的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select

  • select是POSIX所规定

  • select函数的调用相当于内核级别的,可以等待多个socket,实现监听多个IO端口

  • select函数监听的fd有三种,分别是writefds,readfds,exceptfds

  • 良好的跨平台支持,几乎支持所有平台

原理
  1. 当用户进程调用了select函数后会阻塞(IO执行的第一个阶段发生阻塞)。select函数可以等待多个socket,把fd从用户态的rset拷贝到内核台,由内核判断fdset是否有数据:
    • 如果没有数据,会一直处于阻塞状态。
    • 当其中一个socket的数据(fd)准备(有数据可读、可写或者有异常)好,或者超时,则进行FD置位,select函数立即返回数据(进行可读、可写等)。
  2. 当select函数返回后,可以通过遍历fdset寻找就绪的fd(即哪个一fd被置位了)。
  3. 找到fd之后,用户进程就可调用recvform(),把数据报从内核拷贝到用户进程缓冲区,此时进行进程的IO阻塞(此次阻塞发生在IO执行的第二个阶段)。扫描的时间复杂度时O(n)
1
select(max+1, &rset, NULL, NULL, NULL); // 参数:最大的文件描述符+1,读文件描述符集合,写文件描述符集合,异常的文件描述符集合,超时时间

rset是一个bitmap(位图)结构,默认是有1024位,用于表示哪个一文件描述符被启用的、被监听的,如12579...则bitmap是0110010101000...

缺点
  • 在单线程下能监视(打开)fd的数量有限制,在32位的Linux下FD_SETSIZE是1024。(虽然可以通过修改宏定义,甚至重新编译内核的方式提升这一限制,但是会造成效率降低)
  • select对socket的扫描采用轮询方式,效率低
  • 需要维护一个用来存放大量fd的数据结构,这会使得用户空间和内核空间在传递该结构时的复制开销大
  • 每次需要给bitmap置位,导致bitmap不可重用,每次while都要创建和初始化。

poll

但相比起select,epoll没有最大连接数的限制,因为它是基于链表来存储的。

1
2
3
4
5
struct poolfd {
int fd;
short events;
short revents;
}
原理

poll和select没有本质的区别:

  1. 当用户进程调用了poll之后,会阻塞poll会将用户传递进来的数组拷贝到内核空间,然后查询每个fd的设备状态。

    如果有一个或多个fd有数据,则会将poolfd.revents置位,poll数据返回。 就绪则在设备等待序列加入一项,又继续进行遍历。如果没有就绪的fd,则挂起当前线程,知道设备就绪或者主动超时。被唤醒后又继续遍历fd。在这个过程中经历了多次无谓的遍历,扫描的时间复杂度时O(n)

缺点
  • 大量的fd的数组被整体复制于用户态和内核地址空间之间
  • poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll

epoll是在2.6内核中提出的,epoll相当于是select和poll的增强版,Linux 内部使用的就是 epoll

epoll提供了三个函数,分别是epoll_create()创建epoll事件,epoll_ctl()注册要监听的事件类型和epoll_wait()等待事件的产生

原理

epoll采取“事件”就绪的通知方式,避免线性扫描。

1、poll_create(),从linux内核申请B+树的文件系统,返回epoll对象(fd对象)

2、通过epoll_ctl()注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait()便可以收到通知。epoll只通知进程哪些fd刚刚变为就绪态,并且只会通知一次,并把fd添加到就绪链表

epoll的优点

  • 没有最大fd连接数限制
  • epoll不采用轮询的方式,epoll只管“活跃”的连接,而与连接总数无关
  • epoll把所有事件挂载在红黑树上,搜索效率高
  • 利用mmap()文件映射内存,加速用户空间和内核空间的消息传递,减少复制开销

使用场景

  • redis
  • nginx
  • java中的NIO(linux)

参考