Linux 下的五种 I/O 模型
image.png
总的来说,阻塞 IO 就是 JDK 里的 BIO 编程,IO 复用就是 JDK 里的 NIO 编程,Linux 下异
步 IO 的实现建立在 epoll 之上,是个伪异步实现,而且相比 IO 复用,没有体现出性能优势,
使用不广。非阻塞 IO 使用轮询模式,会不断检测是否有数据到达,大量的占用 CPU 的时间,
是绝不被推荐的模型。信号驱动 IO 需要在网络通信时额外安装信号处理函数,使用也不广
泛。
image.png
阻塞 IO 模型
image.png
I/O 复用模型
比较上面两张图,IO 复用需要使用两个系统调用(select 和 recvfrom),而 blocking IO 只
调用了一个系统调用(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。
所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用
multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。select/epoll 的优势
并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
Linux 下的 IO 复用编程
select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制,一个进
程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序
进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事
件就绪后自己负责进行读写,并等待读写完成。
select int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval
*timeout);
select 函数监视的文件描述符分 3 类,分别是 writefds、readfds、和 exceptfds。调用后
select 函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有 except),或者超时
(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,
可以 通过遍历 fdset,来找到就绪的描述符。
select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select 的
一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,
可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现。
pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方
式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。 和 select 函数
一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
epoll
epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll
来说,可以看到 epoll 做了更细致的分解,包含了三个方法,使用上更加灵活。
int epoll_create(int size);//创建
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//注册事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//等待感兴趣的事件跟nio的选择器功能相似
select、poll、epoll 的比较
select,poll,epoll 都是 操作系统实现 IO 多路复用的机制。 我们知道,I/O 多路复用
就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),
能够通知程序进行相应的读写操作。那么这三种机制有什么区别呢
1、支持一个进程所能打开的最大连接数
select 底层基于数组(连接数有限),默认1024个
单个进程所能打开的最大连接数有 FD_SETSIZE 宏定义,其大小是 32
个整数的大小(在 32 位的机器上,大小就是 32*32,同理 64 位机器上
FD_SETSIZE 为 32*64),当然我们可以对进行修改,然后重新编译内核,
但是性能可能会受到影响
poll 底层基于链表
poll 本质上和 select 没有区别,但是它没有最大连接数的限制,原因
是它是基于链表来存储的
epoll
虽然连接数基本上只受限于机器的内存大小
2、FD 剧增后带来的 IO 效率问题
select
因为每次调用时都会对连接进行线性遍历,所以随着 FD 的增加会造
成遍历速度慢的“线性下降性能问题”。
poll 同select一样
epoll 回调函数实现
因为 epoll 内核中实现是根据每个 fd 上的 callback 函数来实现的,只
有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,
使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活
跃的情况下,可能会有性能问题
3、 消息传递方式
select/poll 内核需要将消息传递到用户空间,都需要内核拷贝动作
epoll
epoll 通过内核和用户空间共享一块内存来实现的。
==============================================
从网卡接收数据说起
网卡收到网线传来的数据;经过硬件电路的传输;最终将数据写入到内存中的某个地址
上。这个过程涉及到 DMA 传输、IO 通路选择等硬件有关的知识,但我们只需知道:网卡会
把接收到的数据写入内存。操作系统就可以去读取它们。
如何知道接收了数据?
CPU 和操作系统如何知道网络上有数据要接收?使用中断机制。
进程阻塞
cpu调度工作队列上的进程,当cpu时间片切换后将进程的引用指向socket的等待队列列表中
同时监视多个 socket 的简单方法
image.png
epoll 的原理和流程
当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中
epfd 所代表的对象)。eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等
待队列。
创建 epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 socket。以添加 socket 为例,
如下图,如果通过 epoll_ctl 添加 sock1、sock2 和 sock3 的监视,内核会将 eventpoll 添加到
这三个 socket 的等待队列中。
image.png
当 socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。中断程
序会给 eventpoll 的“就绪列表”添加 socket 引用。如下图展示的是 sock2 和 sock3 收到数
据后,中断程序让 rdlist 引用这两个 socket。
image.png
eventpoll 对象相当于是 socket 和进程之间的中介,socket 的数据接收并不直接影响进
程,而是通过改变 eventpoll 的就绪列表来改变进程状态。
当程序执行到 epoll_wait 时,如果 rdlist 已经引用了 socket,那么 epoll_wait 直接返回,
如果 rdlist 为空,阻塞进程。
假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。如
下图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。
image.png
当 socket 接收到数据,中断程序一方面修改 rdlist,另一方面唤醒 eventpoll 等待队列中
的进程,进程 A 再次进入运行状态。也因为 rdlist 的存在,进程 A 可以知道哪些 socket 发生
了变化。
数据结构
epoll 使用双向链表来实现就绪队列(rdlist等待队列)
红黑树存储 epoll_ctl 传入的soket
总结
当某一进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,在内核
cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 rdllist 双向
链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 rdllist 双向链表里有
没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据
也返回。
同时,所有添加到 epoll 中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是
说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做 ep_poll_callback,
它会把这样的事件放到上面的 rdllist 双向链表中。
当调用 epoll_wait 检查是否有发生事件的连接时,只是检查 eventpoll 对象中的 rdllist
双向链表是否有 epitem 元素而已,如果 rdllist 链表不为空,则这里的事件复制到用户态内
存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此 epoll_waitx 效率非常
高,可以轻易地处理百万级别的并发连接。
网友评论