很多很多年前,我接受采访,为什么调用最多只支持1024个文件描述符?
我没有回答,我也不知道这是为了什么。
许多年后,我用这个问题去采访人们……
那时,我已经有了一个令我满意的预期答案。 我所期待的大概是:
为了避免空谈,我还可以向您展示代码:
// 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内核源码,看懂之后,我开始感觉自己什么都懂了。
言归正传,如果想突破这个1024的限制,就重新编译内核吧!
很多年过去了,事后想起来,还是觉得有些不好意思。 我实际上曾经通过说我读过Linux内核源代码来愚弄人们。 我曾经是一名源码分析员,当我没有完全理解一个问题时,我就片面了。 根据源码很容易乱说。
他们在谈论Linux源代码,要求人们阅读文档,并要求重新定义值,然后重新编译内核。 真是太丢人了!
我从来没有想过自己尝试这么简单的事情! 你自己尝试一下不就知道了吗? 为什么你每天听别人说的话就认为是对的? 嗯,当时我真的应该狠狠地责骂一下自己。 如果我能遇见他,我一定会把它吃掉!
学院已经彻底转变为工程学院。
我早就不再写没有实验的文章了,所以今天写一篇文章,一定要有看得见、摸得着的实实在在的东西。
真的有1024的限制吗?
这么简单的事情,只要尝试一下就知道了。 下面的实验以Linux平台为例。
从and宏的定义可知,它是一个位图数组,是位图集合操作。 我就不分析源码了,直接做如下实验:
#include
#include
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 :0xx 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
符合预期。
这意味着,实际上宏并不关心是否越界以及越界的后果,也没有严格限制在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)))
可见1024是没有限制的! 只需简单设定位置即可!
如果上面的例子没有显示出越界覆盖的效果,那么看下面的:
#include
#include
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指针进行操作,就预料到pgap指针会被越界覆盖:
[root@localhost select]# ./null
gap value is :e
段错误
如果pgap指针被覆盖,当然会发生段错误。
至于栈下还有多少空间,取决于1024和对齐限制的结合。 在我们的实验中,它是:
&pgap - &readfds;
如何覆盖以及覆盖哪些变量取决于局部变量在堆栈上的布局。
目前的结论是超过1024的值会导致出界,但是这个出界可能不会产生致命的后果(比如你永远不会碰gap、pgap……)。
这就是所谓的以下修辞:
其中如果一个值小于零或
大于或等于 ,它至少等于 num-
误码率由 .
OK,我们已经知道会越界,那么下一步,设置超过1024的文件描述符后,是否还能正常工作呢?
我们再做一个实验来验证一下。 在下面的实验中,我们将变量从堆栈中移除,以避免越界的影响:
#include
#include
#include
#include
#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);
}
}
}
}
显然,参数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,仍然可以:
[root@localhost select]# ./selectserver
#### 1049
到底有多少文件描述符传递到调用中取决于它的第一个参数:
#include
#include
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协议。 如果不遵守,那就要承担越界的风险!
内核态怎么样? 说实话,内核态看到的是位图本身,它并没有施加任何限制。
如果想超过1024的限制,怎么办?
让我们尝试一下:
#include
#include
#include
#include
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
雪地皮鞋!
接下来,我们看看平台的行为。
我没有环境,平时根本不涉及平台的开发和调试。 我可能会对事情的进展有些犹豫,结果可能会很尴尬。 我对此感到抱歉。
如果说Linux平台上的大家都喜欢夸自己读过源码的话,那么对应的平台就是MSDN了。 正如我不喜欢Linux源码分析一样,我也不喜欢阅读MSDN文档。 (当然,我没有资格评论平台的任何形而上的东西,所以我就少说点。)
所以我只能在我的Win8虚拟机里下载一个Dev-C++,简单乱搞。 我的代码如下:
#include
#include
#include
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覆盖率测试。 然而,当我发现无论我如何努力,都无法达到覆盖效果时,我决定先了解一下平台结构的简单操作。 所以我把它改成了上面的代码。 首先打印出fset的大小,看看平台是否有1024的限制。
令我惊讶的是,该平台只有 64 个(而不是 1024 个)! 然而,它有520字节那么大!
只是粗略估计一下,大概是64*8=512字节的量级,比520还少了8个字节。我可以粗略猜测,字节图是用来实现Linux位图的效果的。
所谓字节图,其实和位图的原理类似,但层次更高,可以统一实现字节操作。 毕竟,高效的位运算必须考虑很多跨平台的特性。
让我们进入调试模式并在各处设置断点,然后单步跟踪以确认:
好的,完美! 有了如此简单的数据结构,有一定经验的人就可以直接破解数据结构。
可见文件描述符的最大数量确实是有限的,即=64。 如果我们想突破这个限制怎么办?
这东西比Linux简单,不就是一个宏定义吗? 就改变它吧!
好啦,现在谜底彻底揭晓了。
我们简单总结一下:
……
经过这次调查,别相信Linux内核源代码就在眼前,一眼就能看出来。 事实并非如此。 除了Linux内核源代码之外,还有glibc,各种中间库,甚至还有你的bug,你甚至不一定使用Linux,...事情的复杂性远远超出了Linux内核的范围源代码。
所以,确实,如果你能了解Linux内核,写出两条注释进行源码分析,也没什么大不了的。
我在Linux平台上的实验都是基于2.6.8、2.6.18、2.6.32和3.10。 平台实验基于XP、Win7/8,甚至玩DoS。 它们都是老式平台。 他们不求新,也不发布补丁。 挖洞,不玩,社区不问知识,达经理,坦诚相待,做个工匠。
浙江温州的皮鞋会湿,下雨的时候湿了就不会胖。