很多很多年前,我被面试 为什么select调用最多只支持1024个文件描述符?
我没有答出来,我甚至不知道select到底是干什么的。
又过了很多年,我用这个问题面试了别人…
在当时,我心里已经有了会令自己满意的预期答案,我预期的大概就是:
- Linux内核的宏限制了fd_set最多只支持1024…
为了避免talk is cheap,我还能show you the code:
// include/uapi/linux/posix_types.h
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
嗯,是的,那段时间我也和很多人一样,读过几段Linux内核源码,并且读懂了,就开始觉得自己什么都懂了。
言归正传,如果你想突破fd_set此1024的限制,重新编译内核咯!
事情已经过去很多年了,事后想想这事,感觉有点丢人,我竟然曾经以我读过Linux内核源码而唬人,我曾经也是一个源码分析者,在还没有深入理解一个问题时,就片面地以源码为依据信口开河。
竟然扯什么Linux源码,竟然让人家去看文档,还要什么重新定义__FD_SETSIZE的值之后重新编译内核,丢人啊丢人!
这么简单的事情竟然没有想到自己去试试!自己试一下不就知道了吗?干嘛天天听别人怎么说就认为那样就是对的。嗯,我确实应该猛怼那时的自己了,如果我能遇见,我定然掘心自食!
妥妥学院派转变为了工程派。
我早就已经无实验不作文了,所以今天我作文,那必定是有些可以看得见摸得着的东西的。
select真的受1024限制吗?
这么简单的事情,试一下就知道了。以下的实验先以Linux平台为例。
我们从fd_set的定义以及FD_SET宏的定义可知,fd_set就是一个位图数组,而FD_SET就是一个位图set操作,我不分析源码,直接做下面的实验:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int i = 2000, j;
unsigned char gap = 0x12; // 该值作为锚点,被位运算覆盖。
fd_set readfds;
unsigned char *addr = ⪆
int sd[2500];
unsigned long dist;
unsigned total;
printf("gap value is :0x%x\n", gap);
// dist的含义就是readfds和附近gap之间的空间大小,即readfds最大的可用空间。
dist = (unsigned long)&gap - (unsigned long)&readfds;
FD_ZERO(&readfds);
// dist*8 + 1即让readfds越界1个bit。
// 由于gap为0x12,二进制10010,越界1个bit,可以预期FD_SET会置位0x12的最低位。
// 结果就是0x13
for (j = 0; j < dist*8 + 1; j++) {
sd[j] = j;
FD_SET(sd[j], &readfds);
}
printf("j %d .", j);
printf("after FD_SET. gap value is :0x%01x bytes space:%d\n", gap, dist);
}
看下执行结果:
[root@localhost select]# ./set
gap value is :0x12
j 1145 .after FD_SET. gap value is :0x13 bytes space:143
符合预期。
这意味着,实际上 FD_SET宏根本不管是否越界以及越界的后果,fd_set也并非严格限制在1024.
事实上,如果你真的去分析源码,也确实如此:
// /usr/include/sys/select.h
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
// /usr/include/sys/select.h
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
// /usr/include/bits/select.h
#define __FD_SET(d, set) \
((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
可以看出,没有任何limit 1024的限制!只是单纯的去置位!
如果上面的例子没能展示越界覆盖的效果,那么看下面的:
#include <stdio.h>
#include <stdlib.h>
char stub = 0x65; // ascii码的'e'
int main(int argc, char **argv)
{
int i = 2000, j;
unsigned char *pgap = &stub;
fd_set readfds;
int sd[2500];
unsigned long dist;
unsigned total;
// 我们从头到尾不去touch pgap
printf("gap value is :%c\n", *pgap);
FD_ZERO(&readfds);
for (j = 0; j < dist*8 + 1; j++) {
sd[j] = j;
FD_SET(sd[j], &readfds);
}
printf("gap value is :%c\n", *pgap);
}
至始至终我们没有去操作pgap这个指针,预期FD_SET会越界覆盖掉pgap指针:
[root@localhost select]# ./null
gap value is :e
段错误
覆盖掉了pgap指针,当然就段错误了。
至于readfds和其stack下到底有有多少空间,那就看1024和对齐限制共同起作用了。在我们的实验里,它就是:
&pgap - &readfds;
具体如何覆盖,覆盖哪些变量,取决于局部变量在stack上的布局。
现在的结论是, FD_SET超过1024的值,会造成越界,只是这个越界可能不会有致命的后果(比如你再也不会touch gap,pgap…)。
这就是所谓select的manual上的以下说辞:
The behavior of these macros is undefined if a descriptor value is less than zero or
greater than or equal to FD_SETSIZE, which is normally at least equal to the maximum num-
ber of descriptors supported by the system.
OK,我们已经知道FD_SET会越界,那么下一步,FD_SET设置了超过1024的文件描述符后,它会正常起作用码?
再来个实验即可验证,以下的实验,我们把变量从stack上拿开,以避开fd_set越界的影响:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/socket.h>
#define SIZE 1200
// 这些变量不再放在栈上,以防被覆盖。
int i = 1001, j;
int sd[SIZE];
struct sockaddr_in serveraddr;
int main(int argc, char **argv)
{
// 使readfds在第一个,覆盖掉我们不再care about的内存.
fd_set readfds;
int childfd;
FD_ZERO(&readfds);
for (j = 0; j < SIZE; j++) {
sd[j] = socket(AF_INET, SOCK_STREAM, 0);
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(i++);
bind(sd[j], (struct sockaddr *) &serveraddr, sizeof(serveraddr));
listen(sd[j], 5);
FD_SET(sd[j], &readfds);
}
while (1) {
// select 超过1024的...
if (select(1100, &readfds, 0, 0, 0) < 0) {
perror("ERROR in select");
}
for (j = 0; j < SIZE; j++) {
if (FD_ISSET(sd[j], &readfds)) {
childfd = accept(sd[j], NULL, NULL);
printf("#### %d\n", j);
close(childfd);
}
}
}
}
很显然,select的参数1100超过了1024,那么结果如何呢?
[root@localhost ~]# telnet 127.0.0.1 2050
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Connection closed by foreign host.
成功连接,事实说明,文件描述符超过了1024依然OK:
[root@localhost select]# ./selectserver
#### 1049
到底有多少文件描述符传入了select调用,取决于其第一个参数:
#include <stdio.h>
#include <stdlib.h>
int num = 1024;
int i;
int main(int argc, char **argv)
{
fd_set readfds;
num = atoi(argv[1]);
FD_ZERO(&readfds);
for (i = 0; i < num; i++) {
FD_SET(i, &readfds);
}
if (select(num, &readfds, 0, 0, 0) < 0) {
perror("ERROR in select");
}
}
执行便知道:
[root@localhost select]# strace -e trace=select ./num 1234
select(1234, [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233], NULL, NULL, NULL) = -1 EBADF (Bad file descriptor)
# 忽略这个error,因为我并没有真的创建socket
ERROR in select: Bad file descriptor
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffffffffff07} ---
+++ killed by SIGSEGV +++
这一切均发生在用户态。
1024的限制,只是POSIX的约定,你不遵守,那就自行承受越界吧!
内核态如何?说实话,内核态看到的fd_set只是位图本身,它没有做任何限制。
如果你想突破1024的限制,如何来做?
- 使用malloc/mmap分配堆内存即可!要多大有多大!
我们来试一下:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/socket.h>
int num = 1024;
int main(int argc, char **argv)
{
// 我们把变量搬回stack,因为不会被覆盖了!
unsigned char *pgap = (unsigned char *)#
fd_set *readfds;
int childfd;
int i = 1000, j;
int sd[10000];
struct sockaddr_in serveraddr;
readfds = (fd_set *)malloc(8000/8);
num = atoi(argv[1]);
FD_ZERO(readfds);
printf("pgap :%p\n", pgap);
for (j = 0; j < num; j++) {
sd[j] = socket(AF_INET, SOCK_STREAM, 0);
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(i++);
bind(sd[j], (struct sockaddr *) &serveraddr, sizeof(serveraddr));
listen(sd[j], 5);
FD_SET(sd[j], readfds);
}
printf("after setting, pgap :%p\n", pgap);
while (1) {
if (select(num, readfds, 0, 0, 0) < 0) {
perror("ERROR in select");
}
for (j = 0; j < num; j++) {
if (FD_ISSET(sd[j], readfds)) {
childfd = accept(sd[j], NULL, NULL);
printf("#### %d\n", j);
close(childfd);
}
}
}
}
来来来:
[root@localhost select]# ulimit -a |grep open
open files (-n) 20000
[root@localhost select]# ./a.out 5000
pgap :0x601084
after setting, pgap :0x6010840xb1b010
TCP连接一下:
[root@localhost ~]# telnet 127.0.0.1 3050
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Connection closed by foreign host.
[root@localhost select]# ./a.out 5000
pgap :0x601084
after setting, pgap :0x6010840xb1b010
#### 2050
月雪皮鞋!
接下来,我们看看Windows平台select的行为。
我并没有Windows的环境,平时也根本不会涉及Windows平台的开发和调试,可能事情进展的有点犹豫,结果或许狼狈,见谅。
如果说Linux平台下都喜欢以读过Kernel源码而自诩,那么Windows对应平台下,那就是MSDN了,和不喜欢Linux源码分析一样,我也同样不喜欢看MSDN文档。(当然,我没有资格评论Windows平台任何形而上的东西,所以就少说点。)
所以我只能在我的Win8虚拟机里下载一个Dev-C++来简单折腾。我的代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int var = 0x1234; // Linux测试代码复制而来,不必关注。
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char *argv[]) {
fd_set fset;
printf("size of fset:%d %d\n", sizeof(fset), FD_SETSIZE);
FD_ZERO(&fset); // 此处下断点来观测FD_SET的行为!
FD_SET(0, &fset);
FD_SET(1, &fset);
FD_SET(3, &fset);
return 0;
}
其实一开始的测试代码不是上面这个,而是Linux的那个覆盖测试,然而当我发现无论我怎么折腾,都无法达到覆盖的效果后,我决定先把Windows平台的fd_set结构体的简单操作摸清楚再说,于是我就改成了上面的代码,先把fset的大小以及FD_SETSIZE打印出来看看,确认一下Windows平台到底有没有1024的限制。
令我惊讶的是,Windows平台的FD_SETSIZE竟然只有64(而不是1024)!然而fd_set却有520字节那么大!
约莫估算一下,大概就是64*8=512字节的量级,还差8字节到520,我大致可以猜到Windows是用 字节图 实现了Linux位图的效果。
所谓的字节图其实和位图原理差不多,只是更加高层,可以用统一的方式实现字节操作,毕竟高效率的位操作要考虑很多跨平台的特征。
让我们走debug模式在FD_ZERO处设断点,然后单步跟踪确认一下:
OK,完美!这么简单的数据结构,有点经验的就可以将数据结构直接hack出来。
由此可见,Windows确实限制了select的最大文件描述符个数,即FD_SETSIZE=64这么多。如果我们想突破这个限制,怎么办呢?
这玩意儿比Linux更简单,不就是个FD_SETSIZE宏定义吗?改了便是!
OK,这下谜底彻底揭开了。
我们来简单总结一下:
- Linux平台的select fd_set
- Posix接口限制为1024个文件描述符。
- fd_set的位索引就是文件描述符索引。
- Linux 内核没有任何限制。
- Posix的1024限制很容易在栈上突破,但可能会越界造成数据覆盖。
- Posix的1024限制需要用堆内存突破限制,想要多大有多大。
- select调用中fd_set的大小取决于第一个参数。
- Windows
- windows.h默认限制为64个文件描述符。
- fd_set的数组元素为文件描述符,数组下标为文件描述符索引。
- 内核是否限制未知,没debug过,搞不定。
- windows.h的64限制无法直接突破,需要在include头文件前重新定义FD_SETSIZE。
- select调用中fd_set大小取决于其结构体的fd_count字段。
…
经过这番探究,不要再相信什么 Linux内核源码摆在眼前,一览无余 之类的言论了,根本不是这样子的好吗,除了Linux内核源码,还有glibc,还有各种中间的库,甚至还有你的bug,甚至你都不一定使用Linux,…事情的复杂性远超Linux内核源码覆盖的范围。
所以,确实,能读懂Linux内核,写两句注释作为源码分析,没什么大不了的。
我在Linux平台的实验全部基于2.6.8,2.6.18,2.6.32以及3.10,Windows平台的实验基于Windows XP,Win7/8,甚至玩DoS,都是老式的平台,不求新,不发patch不挖洞,不玩儿社区不求闻达于经理,坦坦荡荡手艺人。
浙江温州皮鞋湿,下雨进水不会胖。
来源:oschina
链接:https://my.oschina.net/u/4313784/blog/4262700