前言
做一个网络服务器,若使用多线程,每一个请求都是一个线程,但会有CPU上下文切换,代价比较高,因此考虑用单线程处理并发问题,这时候需要网络模型。
Unix的IO读写过程
等待数据准备
把数据从系统内核拷贝到用户进程中
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
良好的跨平台支持,几乎支持所有平台
原理
- 当用户进程调用了select函数后会阻塞(IO执行的第一个阶段发生阻塞)。select函数可以等待多个socket,把fd从用户态的rset拷贝到内核台,由内核判断fdset是否有数据:
- 如果没有数据,会一直处于阻塞状态。
- 当其中一个socket的数据(fd)准备(有数据可读、可写或者有异常)好,或者超时,则进行FD置位,select函数立即返回数据(进行可读、可写等)。
- 当select函数返回后,可以通过遍历fdset寻找就绪的fd(即哪个一fd被置位了)。
- 找到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 | struct poolfd { |
原理
poll和select没有本质的区别:
当用户进程调用了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)
参考
- 聊聊Linux 五种IO模型
- 聊聊IO多路复用之select、poll、epoll详解
- https://www.bilibili.com/video/BV1qJ411w7du/?spm_id_from=333.999.0.0&vd_source=d85f289ebf4d31920c7178bbb563a0a4