select跨平台(windows/linux)底层实现区别和陷阱

目的:
说到select大家可能都很熟悉了,但是在使用过程中还是会有一些比较隐蔽的坑,这篇文章主要是讲一下在使用select实现跨平台通讯库组件的过程中,遇到的因为不同平台(windows、linux)间select底层实现差异导致一个莫名其妙崩溃。

问题描述:
linux平台用select实现的网络通讯库在高并发的情况下莫名其妙的崩溃,windows平台却没有任何问题,经过阅读linux fd_set数据结构找到了答案,描述如下:

windows、linux平台fd_set数据结构:

1 windows平台fd_set结构体如下:

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

2 linux平台fd_set结构体如下:

文件位置:/usr/include/i386-linux-gnu/sys/select.h
#undef _FD_SETSIZE
#define _FD_SETSIZE        1024

typedef long int __fd_mask;
#define __NFDBITS	(8 * (int) sizeof (__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
	__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits)
#else
	__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;

上述结构体可以看出,windows平台上fd_set结构体本质是一个socket数组,而linux平台上则不同,fd_set其实是一个位图映射表,每个socket通过映射到对应bit位的方式设置事件(如:fd=3则映射到2bit;fd=1000,则映射到第999bit位);也就是说,fd_set同时处理的连接数收到fd_set位图表大小的限制,在linux下,sizeof(fd_set)的大小是128byte,也就是1024bit位;从而可以推到出默认情况下,linux系统下的select只能通知处理1024-3=1021个fd连接,这时候很多人估计会说,我可以通过调整linux内核进程文件描述符的方式突破这个限制,说这话的人估计是人云亦云了(说这话的根本原因是把进程文件描述符限制和select fd_set实现限制的概念混淆了,或者说根本就没搞懂这两个概念,只是别人说自己也理所当然的认为就是这样),测试过你就知道,就算你调整进程文件描述符限制也是没用的,sizeof(fd_set)还是等于128。
在这里插入图片描述调整进程文件描述符限制后,fd_set大小测试截图

结论:
其实要验证调整进程文件描述符限制后,是否能真正突破select1024并发数量的测试也很简单,只需要先打开1024个文件(linux系统中,进程内文件fd和socket对系统来说是一样的,所有打开的fd都会占用进程fd名额),然后再打开一个socket让fd=1025,再用如下结构测试即可:

typedef struct
{
	fd_set test_fd_set;
	int c;
}my_fd_set;

int main(int argc, char *argv[]) {
	int port = atoi(argv[1]);
	
	for(int i = 0; i<port; ++i)
		FILE * fp=fopen("noexist","a+");
		
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0) {return -1;}
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = INADDR_ANY;
	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {return 2}
	if (listen(sockfd, 5) < 0) {return 3;}
	
	my_fd_set test_rfds;
	test_rfds.c = 0;
	FD_ZERO(&(test_rfds.test_fd_set));
	FD_SET(sockfd, &(test_rfds.test_fd_set));
	printf("sockfd,  rfds.c: %d:%d\n", sockfd, test_rfds.c);
	
	return 0;
}

测试结果如下:
select跨平台(windows/linux)底层实现区别和陷阱_第1张图片当fd>1024后,fd_set紧接着的内存空间就会被写入数据破坏,一旦其他地方再用到这块空间,将会出现崩溃的情况,或者部分数据进程中的部分数据无缘无故就被改了,这也是很多程序崩溃后难以查找分析根本问题的原因(试想一下,一个内存被写坏了,过了几个月之后恰好别的地方在访问的时候就会非常难查)

你可能感兴趣的:(linux)