目录
IO操作
一种说法:不需要cpu参与的都是IO操作,IO操作一般耗时比较长,所以任务大部分时间都在等待IO操作完成,所以当IO操作时,CPU都会异步去执行其他事情。
一种对IO操作新的解释:
类unix世界中,不管socket还是FIFO、管道、终端,一切皆文件,而文件就是一串二进制流而已。在信息交换的过程中,我们都是对这些流进行数据的收发操作,简称IO操作。
流中读出数据,系统调用read,写入数据,系统调用write。流通过文件描述符(fd)进行操作,一个fd就是一个整数,所以对这个整数的操作就是对这个文件(流)的操作。
总结:数据的收发就是IO操作,而IO操作相当费时
阻塞/非阻塞、同步/异步
上面四个名词都是针对网络IO的解释,一个典型的IO有两个阶段:数据就绪(以何种方法监听数据)和数据读写(数据过来如何读和如何写)
在数据就绪阶段就有阻塞和非阻塞;在数据读写阶段有同步和异步
阻塞:在监听数据时,内核缓冲区没有数据时调用IO方法的线程或者进程挂起
非阻塞:在监听数据时,通过IO函数返回值判断有无数据,无数据直接返回,不会改变进程或线程的状态。
同步:当内核缓冲区有数据时,对该数据的读写操作由用户程序自己来完成
异步:当内核缓冲区有数据时,对该数据的读写操作由内核来完成(一般非阻塞)
一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪” 和 “数据读写”,数据就绪阶段分为 阻塞和非阻塞,表现得结果就是,阻塞当前线程或是直接返回。
同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是 由请求方A自己来完成的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络IO接口时 (或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以 处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。
注:1、阻塞时,进程或线程挂起不占用CPU,此时CPU忙其他的去了
2、举例,非阻塞情况下利用recv函数,那么将根据返回值判断返回,具体如下:
其中-1并不一定时出错了,还要判断是否是EINTR(被信号中断)、EAGAIN或EWOULDBLOCK(没有数据传入)
3、数据就绪是在TCP的内核缓冲区,数据读写是在用户区,比如,同步就是自己将TCP的内核缓冲区数据读到自定义buf中,而异步是是内核帮我们进行数据的读
4、异步有专门的IO函数,LINUX是AIO,Window是IOCP,.NET是BeginInvoke/EndlNVOke,操作系统在将数据读写完成后通常利用SIGIO信号进行通知。并且AIO也是模拟的异步,并且不支持网络传输
5、只有同步才会有阻塞和非阻塞,异步一般是非阻塞的
真正的异步IO需要CPU的深度参与。换句话说,只有用户线程在操作IO的时候根本不去考虑IO的执行全部都交给CPU去完成,而自己只等待一个完成信号的时候,才是真正的异步IO。所以,拉一个子线程去轮询、去死循环,或者使用select、poll、epool,都不是异步。
6、异步一般需要读写数据的文件描述符,装数据的buf,通知方式,例如sigio信号
7、线程同步:保证线程串行执行,和这里的同步异步不同
两种高效的事件处理模式
服务器程序通常处理三类事件:I/O事件、信号及定时事件。
两种高效的事件处理模式:Reactor和Proactor模式,还有一种很重要的模拟Proactor模式。其中Reactor模式使用同步IO实现,Proactor模式使用异步IO实现,模拟Proactor模式使用同步IO实现异步。
Reactor模式
主线程(I/O处理单元)只监听文件描述符是否有事件发生,有的话就立即将事件通知工作线程,读写数据、接受新的连接、处理客户请求均在工作线程完成。
使用同步I/O(epoll)实现Reactor模式的工作流程:
1. 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
2. 主线程调用 epoll_wait 等待 socket 上有数据可读。
3. 当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
4. 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
5. 当主线程调用 epoll_wait 等待 socket 可写。
6. 当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
7. 睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。
Proactor模式
所有的IO操作均交给主线程和内核完成,工作线程仅负责业务逻辑。(编程复杂,并且linux下不支持网络socket)
模拟 Proactor 模式
使用同步 I/O 方式模拟出 Proactor 模式。主线程负责所有I/O操作,读写完成后主线程向工作线程通知这一“完成事件”。从工作线程角度,是直接获取到了数据读写结果,工作线程只负责业务逻辑。
1. 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
2. 主线程调用 epoll_wait 等待 socket 上有数据可读。
3. 当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更 多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
4. 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事 件表中注册 socket 上的写就绪事件。
5. 主线程调用 epoll_wait 等待 socket 可写。
6. 当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。
同步 I/O 模拟 Proactor 模式的工作流程: