目的:
说到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;
}
测试结果如下:
当fd>1024后,fd_set紧接着的内存空间就会被写入数据破坏,一旦其他地方再用到这块空间,将会出现崩溃的情况,或者部分数据进程中的部分数据无缘无故就被改了,这也是很多程序崩溃后难以查找分析根本问题的原因(试想一下,一个内存被写坏了,过了几个月之后恰好别的地方在访问的时候就会非常难查)