很多很多年前,我被面试 为什么select调用最多只支持1024个文件描述符?
我没有答出来,我甚至不知道select到底是干什么的。
又过了很多年,我用这个问题面试了别人…
在当时,我心里已经有了会令自己满意的预期答案,我预期的大概就是:
- Linux内核的宏限制了fd_set最多只支持1024…
为了避免talk is cheap,我还能show you the code:
1 2 3 4 5 6 | // 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操作,我不分析源码,直接做下面的实验:
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 26 27 | #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); } |
看下执行结果:
1 2 3 | [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.
事实上,如果你真的去分析源码,也确实如此:
1 2 3 4 5 6 7 8 9 10 11 | // /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的限制!只是单纯的去置位!
如果上面的例子没能展示越界覆盖的效果,那么看下面的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #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指针:
1 2 3 | [root@localhost select]# ./null gap value is :e 段错误 |
覆盖掉了pgap指针,当然就段错误了。
至于readfds和其stack下到底有有多少空间,那就看1024和对齐限制共同起作用了。在我们的实验里,它就是:
1 | &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越界的影响:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #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,那么结果如何呢?
1 2 3 4 5 | [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:
1 2 | [root@localhost select]# ./selectserver #### 1049 |
到底有多少文件描述符传入了select调用,取决于其第一个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #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"); } } |
执行便知道:
1 2 3 4 5 6 | [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分配堆内存即可!要多大有多大!
我们来试一下:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | #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); } } } } |
来来来:
1 2 3 4 5 | [root@localhost select]# ulimit -a |grep open open files (-n) 20000 [root@localhost select]# ./a.out 5000 pgap :0x601084 after setting, pgap :0x6010840xb1b010 |
TCP连接一下:
1 2 3 4 5 6 7 8 9 10 | [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++来简单折腾。我的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #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不挖洞,不玩儿社区不求闻达于经理,坦坦荡荡手艺人。
浙江温州皮鞋湿,下雨进水不会胖。