彻底解释Linux select的1024限制(select真的被1024限制了吗?

 2024-01-26 03:02:49  阅读 0

很多很多年前,我接受采访,为什么调用最多只支持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 *)&num;
	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。 它们都是老式平台。 他们不求新,也不发布补丁。 挖洞,不玩,社区不问知识,达经理,坦诚相待,做个工匠。

浙江温州的皮鞋会湿,下雨的时候湿了就不会胖。

标签: 源码 内核 字节

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码