IO的介绍:
- 从计算机结构的视角:I/O描述了计算机系统与外部设备之间通信的过程。
- 从应用程序的视角:应用程序对操作系统的内核发起IO调用(系统调用),操作系统负责的内核执行具体的IO操作。
概念介绍:
- 同步:是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
- 异步:是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数;
- 阻塞:是指IO操作需要彻底完成后才返回到用户线程;
- 非阻塞:是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
BIO(同步阻塞IO模型)
应用程序发起read调用后,会一直阻塞直到内核把数据拷贝到用户空间。
同步非阻塞IO模型
- 应用程序会一直发起read调用,通过轮询操作,避免了一直堵塞。
- 但是等待数据从内核空间拷贝的用户空间的这段时间里,线程仍然是堵塞的,直到内核把数据拷贝到用户空间。
缺点:应用程序不断进行I/O系统调用轮询数据是否已经准备好的过程是十分消耗CPU资源的。
IO多路复用模型
文件描述符的概念:
每一个进程都有一个数据结构task_struct,该结构体里有一个指向【文件描述符数组】的成员指针。该数组里列出这个进程打开的所有文件的文件描述符。数组下标就是文件描述符,是一个整数,而数组的内容是一个指针,指向内核中所有打开的文件的列表;也就是说内核可以通过文件描述符找到对应打开的文件。
IO多路复用的概念:
- I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
- 线程首先发起select调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起read调用。
- read调用的过程(数据从内核空间-->用户空间)还是阻塞的。
重点介绍一下select、poll、epoll的区别?
select
基本原理:
- 将已连接的socket都放在一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核中,让内核通过遍历文件描述符集合的方式来检查是否有网络事件的产生,当检查到有事件产生后,将此socket标记为可读或可写,接着把整个文件描述符集合拷贝会用户态,然后用户态还需要在通过遍历的方式找到可读或可写的socket,然后再对其处理。
缺点:
- 需要进行2次遍历文件描述符集合(一次在内核态,一次在用户态),同时还会发生两次拷贝文件描述符集合(从用户空间到内核空间,内核修改后再传回到用户空间);
- 文件描述符集合使用BitsMap存储,受到内核中FD_SESIZE限制,默认值是1024;
- 使用【线性结构】存储进程关注的socket集合,因此需要遍历文件描述符集合来找到可读或可写的socket,时间复杂度为O(n),并且需要在用户态与内核态之间拷贝文件描述符集合。
poll
- 本质上跟select没有什么区别,不同的是没有最大连接数的限制,原因是基于链表来存储的。
epoll
基本原理:
- epoll在内核中使用红黑树跟踪进程所有待检测的文件描述符。所以我们把需要监控的socket通过epoll_ctl()函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般的时间复杂度为O(logn);也就是只需要传入一个待检测的socket而不是传入文件描述符集合,减少了内核和用户空间的大量数据的拷贝和内存分配;
- epoll使用事件驱动机制,内核里维护了一个链表来记录就绪事件,当某个socket有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait()函数时,只会返回有事件发生的文件描述符的个数,同时在用户态不需要像select/poll那样轮询扫描整个socket集合,大大提高了检测的效率。
调用过程:
两种模式:
- LT(默认模式):只要这个FD还有数据可读,每次epoll_wait都会返回它的事件,提醒用户程序去操作。
系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
- ET(边缘模式):只会提示一次,直到下次再有数据流入之前都不会再提示了,无论FD中是否还有数据可读。所以在ET模式下,read一个FD的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者遇到eagain错误。
这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
总结:
AIO(异步IO模型)
异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,在后台处理完成,操作系统会通知相应的线程进行后续的操作。