在一次IO操作中,数据不会直接拷贝到程序的内存缓冲区,而是分为以下两个阶段:
- 等待数据准备好,然后复制到内核缓冲区
- 从内核缓冲区复制到应用程序缓冲区
IO模型
IO模型主要有五种
阻塞式I/O
当数据未准备好时,进程会一直阻塞,直到数据准备好并从内核态复制到用户态。
非阻塞式I/O
若数据未准备好,则会返回,可以执行其他任务;过段时间再次询问;若准备好则从内核态复制到用户态。
这种非阻塞式循环请求的方式称为轮询。
I/O复用 (select,poll,epoll等)
利用内核来监视文件描述符,可以通过一个进程同时处理多个网络连接的IO。
信号驱动式I/O(SIGIO)
通过信号处理函数来反馈是否准备好数据,若未准备好,则返回,进程可以执行其他任务;若准备好则内核返回信号,将数据从内核态拷贝到用户态。
异步I/O(POSIX的aio_系列函数)
进程完成系统调用后,无论数据是否准备好都会返回。在内核态中若数据准备好,则从内核态拷贝到用户态,拷贝完成才返回信号给指定进程。
什么是IO复用
简而言之,内核提供一种单线程或单进程同时监测若干个文件描述符是否可以进行IO操作的能力
什么情况下使用IO复用
- 客户需要处理多个描述符
- 一个客户处理多个套接字
- 一个TCP服务器既需要监听套接字,也需要处理已连接的套接字
- 一个服务器既需要处理TCP也需要处理UDP
- 一个服务器需要处理多个服务或者多个协议
注:IO复用不只限于网路编程
为什么要使用IO复用
在最原始的情况下就是使用多进程并发模型,每一个进程负责一个IO流,每进来一个新的IO流则创建一个新的进程进行管理,通过CPU时分复用来完成。
但是,成本较高。进程和线程的创建成本,CPU线程和进程切换的成本,多线程竞争的成本
为了缩小成本,提高效率提出了IO复用,通过单个线程记录和跟踪每一个socket的状态来同时管理多个IO流
IO复用模型有哪些
select
缺点:
- 单个进程可以监听的描述符数量有限,bitmap也就是fdset只有1024个
- 对于整个fd_set而言,每次select返回之后,需要重新返回全部置为0,fdset是不可重用的
- rset用户态到内核态的转换,是有一定开销的
- 每次得知有一个或几个描述符被置位后,需要遍历一遍来查找哪个描述符被置位.时间复杂度为O(N)
- select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,这个sock不用,select 不支持收回,若关掉这个sock, select的标准行为是不可预测的
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
poll
- poll 去掉了1024个链接的限制
- poll 从设计上来说,不再修改传入数组,具体情况依据使用而定
解决了select的单进程文件描述符数量少和Fd_set不能重用的问题.
epoll
- epoll 现在是线程安全的。
- epoll 可以得知具体哪个socket改变
epoll解决了select和poll中文件描述符拷贝的问题和时间复杂度的问题,每次只需要遍历指定大小的文件描述符.
目前常用的IO复用方案
epoll和poll只支持Linux
IOCP支持windows
select两者都支持