写点什么

Linux 之 select、poll、epoll 讲解

作者:java易二三
  • 2023-08-14
    湖南
  • 本文字数:3006 字

    阅读完需:约 10 分钟

1 select、poll、epoll

1.1 引言

操作系统在处理 io 的时候,主要有两个阶段:

等待数据传到 io 设备

io 设备将数据复制到 user space

我们一般将上述过程简化理解为:

等到数据传到 kernel 内核 space

kernel 内核区域将数据复制到 user space(理解为进程或者线程的缓冲区)

select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间

1.2 IO 和 Linux 内核发展

1.2.1 整体概述

整体关系流程:

查看进程文件描述符:

获取 pid 进程号

ps -ef

查看文件描述符

cd /proc/进程号/fd ; ll

或者查看当前进程的 fd

$$ 表示 Shell 本身的 PID (ProcessID)

cd /proc/$$/fd ; ll

1.2.2 阻塞 IO

计算机是有内核(kernel)的,内核向下连接很多的客户端,内核向上连接进程或线程,早先内核通过 read 命令读取文件描述符(fd),在这个时期 socket 是 blocking(阻塞的)BIO。

如下图所示:线程通过内核读取文件 fd8,读取到用户空间后,在通过内核写入文件 fd9,如果 fd8 阻塞了,它会阻挡后面的操作

1.2.3 非阻塞 IO

socket fd nonblock(非阻塞),进程/线程用一个,用循环遍历文件描述符(轮询发生在用户空间),这个时期是同步非阻塞时期 NIO;

这是由于内核 socket 本身就是 nio,同步非阻塞 IO

1.2.4 select

如果有 1000 个文件描述符 fd,代表用户进程轮询调用 1000 次内核(kernel),造成成本很大的问题。于是在内核中增加了一个系统调用 select,用户空间调用新的系统调用,统一将所有的文件描述符传给 select,内核监控文件描述符的完成度,文件描述符完成之后返回,返回之后还有系统调用,再调用 read(有数据的文件描述符),这个叫多路复用 NIO,在这个时期,文件描述符考来考去成为累赘;

1.2.5 共享空间

共享空间是进程用户空间一部分,也是内核空间的一部分

引入一个共享空间 mmap,将文件描述符放在共享空间里,文件描述符放在共享空间的红黑树里,将资源齐全的文件描述符放到链表里

1.2.6 零拷贝

什么是零拷贝

在操作系统中,使用传统的方式,数据需要经历几次拷贝,还要经历用户态/内核态切换

从磁盘复制数据到内核态内存;

从内核态内存复制到用户态内存;

然后从用户态内存复制到网络驱动的内核态内存;

最后是从网络驱动的内核态内存复制到网卡中进行传输。

所以,可以通过零拷贝的方式,减少用户态与内核态的上下文切换和内存拷贝的次数,用来提升 I/O 的性能。零拷贝比较常见的实现方式是 mmap,这种机制在 Java 中是通过 MappedByteBuffer 实现的。

sendfile,是完成零拷贝的命令,两个参数一个写出 io,一个读入 io

在之前是先读取文件到用户空间,再写到内核中去,有了 sendfile 后,用这一个命令就可以了,不用读取写入

1.3 select

1.3.1 简介

单个进程就可以同时处理多个网络连接的 io 请求(同时阻塞多个 io 操作)。基本原理就是程序呼叫 select,然后整个程序就阻塞状态,这时候,kernel 内核就会轮询检查所有 select 负责的文件描述符 fd,当找到其中那个的数据准备好了文件描述符,会返回给 select,select 通知系统调用,将数据从 kernel 内核复制到进程缓冲区(用户空间)

下图为 select 同时从多个客户端接受数据的过程

虽然服务器进程会被 select 阻塞,但是 select 会利用内核不断轮询监听其他客户端的 io 操作是否完成

1.3.2 select 缺点

select 的几大缺点:

每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大

同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大

select 支持的文件描述符数量太小,默认是 1024

select 返回的是含有整个句柄的数组, 应用程序需要遍历整个数组才能发现哪些句柄发生了事件

1.4 poll 介绍

1.4.1 与 select 差别

poll 的原理与 select 非常相似,差别如下:

文件描述符 fd 集合的方式不同,poll 使用 pollfd 结构而不是 select 结构 fd_set 结构,所以 poll 是链式的,没有最大连接数的限制

poll 有一个特点是水平触发,也就是通知程序 fd 就绪后,这次没有被处理,那么下次 poll 的时候会再次通知同个 fd 已经就绪。

1.4.2 poll 缺点

poll 的几大缺点:

每次调用 poll,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大

每次调用 poll 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大

1.5 epoll

1.5.1 epoll 相关函数

epoll:提供了三个函数:

int epoll_create(int size);

建立一个 epoll 对象,并传回它的 id

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

事件注册函数,将需要监听的事件和需要监听的 fd 交给 epoll 对象

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待注册的事件被触发或者 timeout 发生

1.5.2 epoll 优点

epoll 解决的问题:

epoll 没有 fd 数量限制

epoll 没有这个限制,我们知道每个 epoll 监听一个 fd,所以最大数量与能打开的 fd 数量有关,一个 g 的内存的机器上,能打开 10 万个左右

cat /proc/sys/fs/file-max 可以查看文件数量

epoll 不需要每次都从用户空间将 fd 复制到内核 kernel

epoll 在用 epoll_ctl 函数进行事件注册的时候,已经将 fd 复制到内核中,所以不需要每次都重新复制一次

select 和 poll 都是主动轮询机制,需要遍历每一个 fd;

epoll 是被动触发方式,给 fd 注册了相应事件的时候,我们为每一个 fd 指定了一个回调函数,当数据准备好之后,就会把就绪的 fd 加入一个就绪的队列中,epoll_wait 的工作方式实际上就是在这个就绪队列中查看有没有就绪的 fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。

虽然 epoll 需要查看是否有 fd 就绪,但是 epoll 之所以是被动触发,就在于它只要去查找就绪队列中有没有 fd,就绪的 fd 是主动加到队列中,epoll 不需要一个个轮询确认。

换一句话讲,就是 select 和 poll 只能通知有 fd 已经就绪了,但不能知道究竟是哪个 fd 就绪,所以 select 和 poll 就要去主动轮询一遍找到就绪的 fd。而 epoll 则是不但可以知道有 fd 可以就绪,而且还具体可以知道就绪 fd 的编号,所以直接找到就可以,不用轮询。

我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 list 链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。所以,epoll_wait 非常高效

这个准备就绪 list 链表是怎么维护的呢?

当我们执行 epoll_ctl 时,除了把 socket 放到 epoll 文件系统里 file 对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪 list 链表里;当一个 socket 上有数据到了,内核在把网卡上的数据 copy 到内核中后就来把 socket 插入到准备就绪链表里了

一颗红黑树,一张准备就绪句柄链表,少量的内核 cache,就帮我们解决了大并发下的 socket 处理问题。执行 epoll_create 时,创建了红黑树和就绪链表,执行 epoll_ctl 时,如果增加 socket 句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行 epoll_wait 时立刻返回准备就绪链表里的数据即可

用户头像

java易二三

关注

还未添加个人签名 2021-11-23 加入

还未添加个人简介

评论

发布
暂无评论
Linux之select、poll、epoll讲解_Linux_java易二三_InfoQ写作社区