epoll详解_select、poll、epoll 是什么?有什么作用?

套接字编程杂谈

昨天提到腾讯的第 4 次初试,自然需要着手准备。首先要准备的是前 3 次初试都问到的 I/O 复用方法 epoll。

当时,在第 1 次、第 2 次初试过后,都在搜索引擎上搜“epoll”,试图在一天之内了解 epoll 的“精髓”。然而,最后结果是“一知半解”,背下来“select、poll 需要线性扫描而 epoll 不需要”的结论,却并不了解最基础的概念。本文不会替代网上有关 epoll 的各种“原理详解”和“深入理解”,而是会作一些必要的铺垫。铺垫过后,再去看 epoll 的各种“原理详解”。

首先,需要先复习一下使用 C 语言和 POSIX api 进行套接字编程的方法。

以下是服务器的代码:

#include 

其中,大写的 Socket、Bind 等函数,是“包裹函数”,见《UNIX 网络编程 卷1》1.4 节。整个程序是《UNIX 网络编程 卷1》5.2、5.3 节的 TCP 回射服务器程序的简化版本,没有使用 fork 产生多进程,也没有处理 EINTR 错误。

整个程序涉及两个文件描述符: socket_fd 和 connection_fd。每个文件描述符和 TCP 的四元组有关:{ server_address: server_ip, client_address: client_ip } 。socket_fd 是监听套接字的文件描述符,对应的四元组是: { * : 9999, * : * } ;而 connection_fd 对应具体 TCP 连接的四元组,例如 { 192.168.1.11 : 9999, 192.168.1.12 : 36500 } 。在这种情况下,如果一个 TCP 分节来自 192.168.1.12 的 36500 端口,目的地为 192.168.1.11 的 9999 端口,它就会与 connection_fd 相关联。其它目的端口为 9999 的 TCP 分节都会与 socket_fd 相关联。每当 socket_fd 接受一个连接,它将会产生一个 connection_fd,并由它来处理数据传输。《UNIX 网络编程 卷1》2.10 节详细地描述了这些概念。

socket_fd 和 connection_fd 与其它文件描述符并无二致,就像使用 <fcntl.h> 里的 open 函数打开文件时,返回的文件描述符一样。实际上,在 Linux 系统中,每个进程打开的文件描述符可以在 /proc/<pid>/fd/ 文件夹下找到。

运行这个程序,观察到控制台有如下输出:

pid: 3104
socket_fd: 3

打开新的控制台,进入 root 用户,然后定位到 /proc/3104/fd/ 下。使用 ls -l 命令,观察到如下输出:

total 0
lrwx------ 1 z z 0 Apr 11 17:44 0 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 17:44 1 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 17:44 2 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 17:44 3 -> 'socket:[4290]'

如果某个客户端连接到此服务器,则会创建 connection_fd 为 4 的描述符。ls -l 命令的输出如下:

total 0
lrwx------ 1 z z 0 Apr 11 17:44 0 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 17:44 1 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 17:44 2 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 17:44 3 -> 'socket:[4290]'
lrwx------ 1 z z 0 Apr 11 17:54 4 -> 'socket:[4762]'

/proc/3104/fd/3 是一个类型为 socket 的文件。(准确来说,它是一个符号链接,链接到一个类型为 socket 的文件。)可在 root 权限下运行以下程序来测试:

#include 

有关 UNIX 系统的文件类型,在《UNIX 环境高级编程》4.3 节中有详细介绍。

本文写的整个程序运行在单进程、单线程下。当 TCP 服务器接收到连接并产生 connection_fd 时,它将“全力”处理当前的这个连接,对 socket_fd 则“不闻不问”。直至当前连接关闭,服务器才会继续监听 socket_fd 是否有新的连接。这个弊端是很大的。一个自然的想法是,使用 <unistd.h> 里的 fork 函数,让子进程处理 connection_fd(关闭 socket_fd),让父进程处理 socket_fd(关闭 connection_fd)。这也是《UNIX 网络编程 卷1》从 2.10 节开始介绍,到第 5 章都在讲解的办法。不过,为了应付面试,我跳过这个部分,直接进入第 6 章的 I/O 复用。

直观来说,I/O 复用的作用就是:让程序能够在单进程、单线程的模式下,同时处理 socket_fd 和 connection_fd 这两个文件。select 函数为这个想法提供了支持:当 socket_fd 和 connection_fd 中有一个已经“准备好”时,就会返回。进程首先检查 socket_fd 和 connection_fd 中的哪个已经准备好,对已经准备好的文件描述符再执行相应的操作。如果 connection_fd 准备好,就处理数据;如果 socket_fd 准备好,就接受连接并产生新的 connection_fd。

显然,从上面的描述可以看到,仅有一个 connection_fd 是不够的,所以有必要把它改造为数组:

#define MAXCLIENT (1024)

参照《UNIX 网络编程 卷1》6.8 节,将本文上述代码“改造”为 select 后,如下:

int 

这样,服务器就可以在单进程、单线程的模型下同时处理多个连接了。使用三个客户端连接到服务器之后,使用 ls -l 命令查看 /proc/3664/fd/ ,如下:

total 0
lrwx------ 1 z z 0 Apr 11 20:49 0 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 20:49 1 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 20:49 2 -> /dev/tty1
lrwx------ 1 z z 0 Apr 11 20:50 3 -> 'socket:[5069]'
lrwx------ 1 z z 0 Apr 11 20:50 4 -> 'socket:[5101]'
lrwx------ 1 z z 0 Apr 11 20:50 5 -> 'socket:[5132]'
lrwx------ 1 z z 0 Apr 11 20:50 6 -> 'socket:[5163]'

与 select 函数使用 fd_set 相比,poll 函数则使用 pollfd 数组作为参数。与 select 函数编写方法类似:

int 

select 和 poll 都有一个缺点,就是只知道有多少个文件描述符已准备好,却不知道具体是哪些,因此需要使用线性扫描来确定,效率较低。试想:有没有别的函数,能不仅仅返回数量,并且一并返回已经准备好的文件描述符呢?

Linux 的 epoll 函数解决了这个问题。epoll 的用法和 select、poll 是类似的:将 socket_fd 加入监视,将新生成的连接加入监视,将已完成的连接退出监视。参考 Linux 控制台命令 man epoll,可继续将上述服务器的代码“改造”如下:

int 

相信到这里,对 select、poll、epoll 的概念有了大致的了解了。那么,现在再去看网上有关 epoll 原理的文章,会有更好的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值