I/O多路转接服务器设计(select,poll,epoll)
select和poll服务器代码:https://github.com/zzaiyuyu/select-poll
理解IO
IO就是用户向操作系统索取或发送数据的过程,linux系统实现的是缓存IO。
缓存IO就是底层数据到来时,先被拷贝到内核缓冲区,然后再从内核缓冲区拷贝到程序的地址空间。
所以当调用read等系统调用,是会切换到内核态然后进行等待数据就绪,然后再将数据拷贝到进程的地址空间。
在上述等待期间,只要数据没有就绪,read调用不会返回,这中方式称为阻塞IO。文件描述符的读取默认都是阻塞形式。
下面是另外4种IO模型
非阻塞IO区别就在于等待数据时,如果没有就绪,那么read调用立刻返回错误码。这样为了保证能读到数据就得轮询,反复读取文件描述符。
IO多路转接利用select同时等待多个文件描述符,只要有一个文件描述符数据就绪,select就返回。
异步IO类似于回调机制,进程不需要等待数据就绪,内核完成数据拷贝后通知应用程序用数据就可以了。信号驱动IO是内核告诉进程何时数据已经就绪。
结论:所有IO都需要有两个步骤,内核等待数据就绪,拷贝数据到进程地址空间。高效IO就是想办法减少等待数据的时间。
阻塞和非阻塞
设置文件描述符为阻塞,那么调用read读取就会切换内核态开始等待数据就绪,只要数据没来就挂起当前进程直到有数据来才唤醒进程并返回read调用。
这样看来阻塞是在说进程等待read调用时自身的运行状态!非阻塞就是不挂起的等待调用。
异步和同步
同步与异步主要是从消息通知机制角度来说的。
比如阻塞IO,read返回后的值一定能给调用者数据或者错误信息,调用者根据返回值决定下一步改怎么做。这就是同步通信。
而异步IO,调用aio_read立即返回,不关心这个返回值,当操作系统做完数据拷贝后用信号通知进程去处理。这就是异步通信。
总结:同步是调用者主动等待调用返回值,异步是系统通知调用者调用完成了。
二者的关键区别就是效率问题,同步方式总是需要检测调用结果的,很浪费资源。
https://www.jianshu.com/p/aed6067eeac9
以上是IO概念的同步异步,在进程中,同步是指各个进程执行的顺序制约关系。
select服务器的设计思路
普通服务器思路:
- 设置监听套接字(创建套接字,绑定地址)
- accept读取监听套接字,若有连接来建立连接并处理数据请求
关键在于accept读取监听套接字是阻塞的,如果没有连接来则进程会挂起。
使用select同时读取监听套接字和数据套接字,只要有一个套接字数据就绪就进行处理,这种方式就是IO多路转接,就绪事件通知机制。
总结:让select进行数据等待,数据就绪后select返回。这是异步阻塞的形式。
若有某个文件描述符就绪则通知select返回,这个过程是异步的。而把timeout设为NULL是把select设为阻塞等待。
编写细节:select的参数是输入输入型,利用位图输入时让用户告诉操作系统关心哪些文件描述符上的读/写事件就绪。输出时操作系统告诉用户哪些文件描述符就绪。
epoll服务器
epoll是性能非常好的多路IO就绪事件通知机制。
使用epoll的关键步骤:
- 调用epoll_create创建epoll模型
- 调用epoll_ctl告诉内核用户关心哪些文件描述符上的事件
- epoll_wait是内核告诉用户哪些文件描述符就绪了,此时再进行数据操作
epoll模型是操作系统维护的一个结构,一个就绪队列,一个红黑树和一个回调机制。
使用epoll_ctl就是操纵红黑树增删改结点,结点是key-value的,用文件描述符作为key值。在有文件描述符事件就绪时,驱动的回调机制将红黑树结点放入就绪队列,同时epoll_wait发现队列不为空,则取出文件描述符进行处理。
epoll的优点——对应select,poll
select的优点
- 是相对于多进程多线程说的,处理多个连接请求没有进程切换带来的开销
- 可以做到单进程处理多连接请求。
select的缺点
- select设计上有同时等待文件描述符上限
- 在select调用前循环设置关心的文件描述符,在select返回后要循环检测哪些文件描述符就绪,这些都需要系统调用切换到内核态,随着连接数增加是有很大开销的
poll改进了select的接口,将文件描述符和关心的事件绑定到结构体里,之后定义一个全局结构体数组,对数组增删改就可以。select的输入输出型位图参数需要在调用select前重新设置,poll接口没有这个弊端。但是未解决根本问题,依旧需要循环检测。
epoll的优点
- 文件描述符没上限,利用红黑树结点
- 利用回调机制将就绪的文件描述符加入就绪队列,即使文件描述符增多,不会影响判断就绪的性能
- epoll_wait发现就绪队列不空则返回,此时在队列中取走已经就绪的文件描述符。不在需要循环检测,是一个O(1)操作
网上很多资料说epoll还有一个mmap内存映射,但个人认为不正确。若是直接将就绪队列映射到用户空间,那么epoll_wait将不需要传入一个buf空间去获取就绪队列内容。而且考虑安全问题,内核也不应该把就绪队列映射到用户空间。