文章主要来自彻底解释Linux select的1024限制(select真的受1024限制吗?不!),原作者通过源码和实验解释select真的是受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(看完源码真的以为是这里限制select为1024,但是那片文章的作者,给了新的解释,确实很有道理。)
二、FD_SET是否限制1024
首先作者分析FD_SET是否会对1024进行越界检查,或者限制在1024 bit。但是原作者的代码表述的逻辑没什么问题,似乎是g++编译过程中对内存分布作了一定的改变,导致原作值的代码无法复线结果,下面代码作了一点修改,可以得到原作者的结果。
// 原作者代码
#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);
}
// 修改后代码
#include <stdio.h>
#include <stdlib.h>
struct mst{
fd_set readfds;
unsigned char gap = 0x12;
};
int main(int argc, char **argv)
{
int i = 2000, j;
mst st;
unsigned char *addr = &st.gap;
int sd[2500];
unsigned long dist;
unsigned total;
printf("gap value is :0x%x\n", st.gap);
// dist的含义就是readfds和附近gap之间的空间大小,即readfds最大的可用空间。
dist = (unsigned long)&(st.gap) - (unsigned long)&(st.readfds);
FD_ZERO(&(st.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], &(st.readfds));
}
printf("j %d .", j);
printf("after FD_SET. gap value is :0x%01x bytes space:%d\n", st.gap, dist);
}
作者是希望gap作为readfds的锚点(或者说“哨兵”),而readfds是128字节1024bit,那么当通过FD_SET设置1025bit的时候会越界到gap的bit上,从而使得gap从0x12变为0x13.但是源代码的问题是,readfds和gap在内存上并不相连,
理论上来说,gap地址是0x7fffffffb44f,而readfds 128字节,地址应该是0x7fffffffb44f - 0x80,但是显然不是。不知道为什么,感觉是不是g++编译过程中的问题,或者,链接GUN动态库时的问题。目前还不清楚是什么原因。
修改后就可以得到期望的结果
这个示例是为了说明,FD_SET并不会限制在1024 bit内,并且不会检查数组越界问题。
----------------------------------------------------------------------------
其实这里开始还遇到一个问题,就是core dump文件没办法在分区创建:If you run the core file in a mounted drive.The core file can't be written to a mounted drive but must be written to the local drive.
-------------------------------------------------------------------------------
三、突破1024限制
// 原本代码
#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);
}
}
}
}
说实话,原本代码有点不知道他在写些什么,但是意思到时可以理解。
// 修改代码
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.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);
if(sd[j] < 0){
perror("socket");
}
struct sockaddr_in serveraddr;
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);
}
}
}
return 0;
}
运行后(别忘了设置进程可开启的最大文件描述符数量),通过
nc 127.0.0.1 2025
可以看到,确实打开了超过1024项文件。那么为什么呢,
在源码里,确实定义了1024那个常量,但是在select实际使用中并没有受到那个常量的限制
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
if (size > (SIZE_MAX / 6))
goto out_nofds;
alloc_size = 6 * size;
bits = kvmalloc(alloc_size, GFP_KERNEL);
if (!bits)
goto out_nofds;
}
运行中首先是在栈上尝试构建6个bitmap,如果大于栈上的内存,就会重新malloc。但是受到SIZE_MAX限制。
然后从用户台拷贝数据的时候
get_fd_set(n, inp, fds.in)
static inline
int get_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
nr = FDS_BYTES(nr);
if (ufdset)
return copy_from_user(fdset, ufdset, nr) ? -EFAULT : 0;
memset(fdset, 0, nr);
return 0;
}
依然没有受到1024的限制,用到的是传入的文件数量n,然后拷贝的时候是给出用户态数据地址和 内核数据地址,和要拷贝数据的字节数量。所以,实际上就如那篇文章中说的:可以超过1024,但是FD_SET超过1024的值,会造成越界,需要承担越界之后可能出现的各种后果。但同时,也不会无限制的允许监听文件的数量,SIZE_MAX限制文件描述符的数量。