当前位置: 首页 > 科技观察

linux系统下fd分配的方法

时间:2023-03-12 08:31:49 科技观察

这几天在公司写了很多网络通信的代码,自然涉及到IO事件监听方法的问题。我很惊讶地发现选择轮换训练的方法在那里仍然很流行。我告诉他们不管在linux系统下还是windows系统下都要放弃select。原因是两个平台的select系统调用都有一个致命的陷阱。windows上单个fd_set容纳的socket句柄数不能超过FD_SETSIZE(在win32winsock2.h中定义为64个,以VS2010版本为准),fd_set结构使用数组来容纳这些socket句柄,每个FD_SET宏将一个套接字句柄放到这个数组中,在这个过程中,限制不能超过FD_SETSIZE。详情请查看winsock2.h中FD_SET宏的定义。这里的问题是,如果fd_set中的sockethandle个数已经达到FD_SETSIZE,那么后面的FD_SET操作实际上是没有效果的,sockethandle对应的IO事件会丢失!!!在linux系统下,问题其实出在fd_set的结构和FD_SET宏上。此时fd_set结构体使用一个位序列来记录每个要检测的IO事件的fd。记录方法略复杂,在/usr/include/sys/select.h中如下typedeflongint__fd_mask;#define__NFDBITS(8*sizeof(__fd_mask))#define__FDELT(d)((d)/__NFDBITS)#define__FDMASK(d)((__fd_mask)1<<((d)%__NFDBITS))typedefstruct{/*XPG4.2requiresthismembername.否则避免使用全局命名空间中的名称。*/#ifdef__USE_XOPEN__fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];[__FD_SETSIZE/__NFDBITS];#define__FDS_BITS(set)((set)->__fds_bits)#endif}fd_set;#defineFD_SET(fd,fdsetp)__FD_SET(fd,fdsetp)/usr/include/bits/select.h中1#define__FD_SET(d,set)(__FDS_BITS(set)[__FDELT(d)]|=__FDMASK(d))可以看出,在上面的过程中,实际上fd_set的比特序列中的每一位的位置都对应着fd值的位置。fd_set结构中的位数由__FD_SETSIZE定义,__FD_SETSIZE在/usr/include/bits/typesize.h中(包含关系如下:sys/socket.h->bits/types.h->bits/typesizes.h)中定义为1024。现在的问题是当fd>=1024时,FD_SET宏实际上会导致内存写越界。其实在manselect中有明确的描述,如下:NOTESAnfd_setisafixedsizebuffer。使用负值或等于或大于FD_SETSIZE的fd值执行FD_CLR()或FD_SET()将导致未定义的行为。此外,POSIX要求fd是一个有效的文件描述符。这包括我之前,很多人都没有注意到,云峰大神有一篇博文《一起 select 引起的崩溃》也描述了这个问题。可见select在linux系统中并不安全。如果要使用,必须仔细确认fd是否达到1024,但这很难做到,否则还是老老实实用poll或者epoll吧。扯的有点远,不过也引出了本文的主题,就是linux系统下fd值是如何分配的。大家都知道fd是int类型,但是它的值是如何增长的。下面的内容我对此做了一点分析,以2.6.30版本的内核为例,欢迎拍砖。首先你要知道是哪个函数分配了fd。我将以管道为例。这是一个典型的fd分配系统调用。pipe和pipe2的syscall实现定义在fs/pipe.c中,如下SYSCALL_DEFINE2(pipe2,int__user*,fildes,int,flags){intfd[2];错误;错误=do_pipe_flags(fd,标志);if(!error){if(copy_to_user(fildes,fd,sizeof(fd))){sys_close(fd[0]);系统关闭(fd[1]);错误=-EFAULT;}}返回错误;}SYSCALL_DEFINE1(pipe,int__user*,fildes){返回sys_pipe2(fildes,0);get_unused_fd_flags(flags)分配fd,它是一个宏#defineget_unused_fd_flags(flags)alloc_fd(0,(flags)),位于include/linux/fs.h好了,我们找到主角了,它就是alloc_fd(),是kernelchapter实际进行fd分配的函数。其位于fs/file.c,实际也很简单,如下intalloc_fd(unsignedstart,unsignedflags){structfiles_struct*files=current->files;无符号整数;恐吓;structfdtable*fdt;自旋锁(&文件->文件锁);重复:fdt=files_fdtable(files);fd=开始;如果(fd<文件->next_fd)fd=文件->next_fd;如果(fdmax_fds)fd=find_next_zero_bit(fdt->open_fds->fds_bits,fdt->max_fds,fd);错误=扩展文件(文件,fd);如果(错误<0)gotoout;/**如果我们需要扩展fs数组,我们*可能会再次阻塞尝试。*/if(error)gotorepeat;if(start<=files->next_fd)files->next_fd=fd+1;FD_SET(fd,fdt->open_fds);如果(标志&O_CLOEXEC)FD_SET(fd,fdt->close_on_exec);否则FD_CLR(fd,fdt->close_on_exec);错误=fd;#if1/*完整性检查*/if(rcu_dereference(fdt->fd[fd])!=NULL){printk(KERN_WARNING"alloc_fd:slot%dnotNULL!\n",fd);rcu_assign_pointer(fdt->fd[fd],NULL);}#endifout:spin_unlock(&files->file_lock);返回错误;}在管道的系统调用中start值始终为0,中间关键的expand_files()函数是根据给定的fd值判断是否需要对进程打开的文件表进行扩容。函数头注释如下/**Expandfiles.*如果请求的大小超过*当前容量并且有扩展空间,此功能将扩展文件结构。*返回<0errorcodeonerror;0whennothingdone;1whenfileswere*expandedandexecutionmayhaveblocked.*Thefiles->file_lockshouldbeheldonentry,andwillbeheldonexit.原则是每次优先使用fd值最小的freefd。当分配不成功时,会返回EMFILE的错误码,表示当前进程中的fd过多。这也印证了公司写的server程序(kernel是2.6.18),每次client连接对应的fd值都是变化的。如果给一个新连接分配的fd值是8,那么在关闭之后,给下一个新连接分配的fd也是8,然后新连接的链接的fd值逐渐增加1。为此,继续查找socket对应的fd分配方式,发现最后是alloc_fd(0,(flags)),调用顺序如下:socket(sys_call)->sock_map_fd()->sock_alloc_fd()->get_unused_fd_flags()opensystemcall还要用到get_unused_fd_flags(),这里就不一一列举了,现在想回过头来说一下开头的选择问题。由于linux的分布规则系统fd,其实已经保证了每次的fd值尽可能的小,一般非IO频繁的系统,确实一个进程中fd值达到1024的概率比较小。因此,不是有可能对是否应该放弃select做出绝对的结论。如果the设计的系统确实有其他措施保证fd值小于1024,那么使用select就没有问题了。但是在网络通信程序的情况下,绝对不应该做这个假设,所以尽量不要使用select!!原文链接:http://www.cnblogs.com/lanyuliuyun/p/3946564.html