解决 Windows 下 Python 的 too many file descriptors in select () 报错
使用 Python 进行 Socket 编程时,如果向轮询列表中注册过多的 file descriptors (fd / 文件描述符), 执行 poll
操作则会抛出 too many file descriptors in select () 的错误,然而这个问题并不是 Windows 出现了偏差,并且是可以解决的。
Windows 中的 Socket 模型
在 Windows 下进行 Socket 编程时,共有六种模型可供选择,分别是
- select 选择
- WSAAsyncSelect 异步选择
- WSAEventSelect 事件选择
- Overlapped I/O 事件通知
- Overlapped I/O 完成例程
- IOCP (I/O Completion Port) 完成端口
我们经常听说到的 Linux 下的 I/O 复用模型 epoll 其性能优秀,同时支持水平触发和边缘触发,只通知就绪的 fd,而 IOCP 同样拥有这些优点,那么为什么还会遇到 too many file descriptors 这种问题?
遗憾的是, Python 官方在 Windows 平台中仅提供了 select 的实现。
相对于通知机制,select 会将部分时间浪费在轮询上,并且在用户态 / 内核态中多次复制也会造成性能下降。
问题定位
是巨硬的锅?
有人会说,select 函数是有 fd 限制的,那么去看看 MSDN 上的文档 select function
1 | int select( |
select 函数有 5 个参数,但是对于第一个参数,MSDN 指出:
nfds [in] Ignored. The nfds parameter is included only for compatibility with Berkeley sockets.
在注释中同样写到:
The select function returns the number of sockets meeting the conditions. A set of macros is provided for manipulating an fd_set structure. These macros are compatible with those used in the Berkeley software, but the underlying representation is completely different.
显然第一个参数并不生效,虽然它描述了 fd 的数量,但 Windows 下的实现方式并不与 Berkeley Unix 相同。
在备注的末尾,指出了关键的部分:
The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.)
在宏中定义的 FD_SETSIZE
看起来确实会决定着文件描述符的最大数量限制,虽然其默认值是 64,但是我们可以通过预先定义 FD_SETSIZE
的方式来改变这个数量限制。
在 MSDN 的另一篇文章中,类似于 Winsock 编程指南一样的手册里,提到了关于这个最大数量限制的事情 Maximum Number of Sockets Supported:
- The maximum number of sockets supported by a particular Windows Sockets service provider is implementation specific. The Microsoft Winsock provider limits the maximum number of sockets supported only by available memory on the local computer.
- However, third-party Winsock providers may have limitations on the numbers of sockets supported. An application should make no assumptions about the availability of a certain number of sockets.
- The maximum number of sockets that a Windows Sockets application can use is not affected by the manifest constant FD_SETSIZE.
翻译一下:
- 由 Windows Sockets 支持服务提供的 sockets 最大连接数限制是基于特定实现的。Microsoft Winsock provider 仅用可用的最大内存来限制 sockets 最大连接数。
- 第三方 Winsock provider 可能会在提供的 sockets 支持上做出自己的限制。
- Windows Sockets 程序能够使用的最大 sockets 连接数不受 manifest 中的常量 FD_SETSIZE 限制。
既然没有限制,那么来看看 WinSock2.h
中的定义
1 | typedef struct fd_set { |
显而易见, fd_count
才是指明了当前已设置的 fd 数量的关键,也就是说,只要我们自己定义 FD_SETSIZE
宏即可解除这个限制。
是 Python 的锅?
找到 Python 的源码 cpython,在 cpython/Modules/selectmodule.c
中实现了所用到的 select.pyd 动态库。
在文件一开头的注释中,就能看到:
select - Module containing unix select(2) call.
Under Win32, select only exists for sockets.
也就是说, Python for Windows 确实只有 select 这一个实现。。。。
直接找到报错行,可以看到:
1 |
|
Python 对于 fd_count
并没有过多的处理,仅仅是简单判断后即返回,并且 FD_SETSIZE
的默认值是 512。
那么 Python 的二进制分发是不是一样的呢?
反汇编 select.pyd,直接找到关键块
1 | .text:1D1110F0 cmp [ebp+var_4], 200h |
看到前两行
1 | cmp [ebp+var_4], 200h |
200h 即十进制数 512, cmp 后立刻是 jge,也就是说当 ebp+var_4 所指向的变量大于等于 512 时,跳转到 1D111120 处,与 selectmodule.c 中的源码相同。
那么解决方案很显然了,只要修改 selectmodule.c 中的宏定义即可。
环境搭建
在 cpython/PCbuild/readme.txt
中,给出了编译的步骤,只需要一步步来即可
*
Python3 可直接使用 Visual Studio 2017 编译
- 卸载当前已安装的任何 Microsoft Visual C++ 2010 Redistributable Package
- 安装 Windows SDK 7.1
- 安装 Microsoft Visual C++ 2008 任意版本
- 安装 Microsoft Visual C++ 2010 任意版本
- 安装 TortoiseSVN (注意要勾选 command line client tools)
- 安装 Git for Windows
一切准备妥当之后,先下载依赖,配置环境
1 | call cpython\PCbuild\get_externals.bat |
打开 Microsoft Visual C++ 2010,新建一个 Solution,把 pythoncore.vcxproj 和 select.vcxproj 添加进解决方案,把两个项目的 Platform Toolset
都改成 Windows7.1SDK,即可编译完成。
Cheers~