Linux系统下fd分配的方法

系统 Linux
在windows上面单个fd_set中容纳的socket handle个数不能超过FD_SETSIZE(在win32 winsock2.h里其定义为64,以VS2010版本为准),并且fd_set结构使用一个数组来容纳这些socket handle的,每次FD_SET宏都是向这个数组中放入一个socket handle

[[119930]]

最近几天在公司里写网络通讯的代码比较多,自然就会涉及到IO事件监测方法的问题。我惊奇的发现select轮训的方法在那里居然还大行其道。我告诉他们现在无论在Linux系统下,还是windows系统下,select都应该被废弃不用了,其原因是在两个平台上select的系统调用都有一个可以说是致命的坑。

在windows上面单个fd_set中容纳的socket handle个数不能超过FD_SETSIZE(在win32 winsock2.h里其定义为64,以VS2010版本为准),并且fd_set结构使用一个数组来容纳这些socket handle的,每次FD_SET宏都是向这个数组中放入一个socket handle,并且此过程中是限定了不能超过FD_SETSIZE,具体请自己查看winsock2.h中FD_SET宏的定义。

此处的问题是

若本身fd_set中的socket handle已经达到FD_SETSIZE个,那么后续的FD_SET操作实际上是没有效果的,对应socket handle的IO事件将被遗漏!!!

而在Linux系统下面,该问题其实也是处在fd_set的结构和FD_SET宏上。此时fd_set结构是使用bit位序列来记录每一个待检测IO事件的fd。记录的方式稍微复杂,如下

/usr/include/sys/select.h中

  1. typedef long int __fd_mask;  
  2. #define __NFDBITS    (8 * sizeof (__fd_mask))  
  3. #define    __FDELT(d)    ((d) / __NFDBITS)  
  4.  
  5. #define    __FDMASK(d)    ((__fd_mask) 1 << ((d) % __NFDBITS))  
  6.  
  7. typedef struct  
  8.   {  
  9.     /* XPG4.2 requires this member name.  Otherwise avoid the name  
  10.        from the global namespace.  */  
  11. #ifdef __USE_XOPEN  
  12.     __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];  
  13. # define __FDS_BITS(set) ((set)->fds_bits)  
  14. #else  
  15.     __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];  
  16. # define __FDS_BITS(set) ((set)->__fds_bits)  
  17. #endif  
  18.   } fd_set;  
  19.  
  20. #define    FD_SET(fd, fdsetp)    __FD_SET (fd, fdsetp) 

/usr/include/bits/select.h中

  1. 1 # define __FD_SET(d, set)    (__FDS_BITS (set)[__FDELT (d)] |= __FDMASK (d)) 

可以看出,在上面的过程,实际上每个bit在fd_set的bit序列中的位置对应于fd的值。而fd_set结构中bit位个数是__FD_SETSIZE定义的,__FD_SETSIZE在/usr/include/bits/typesize.h(包含关系如下sys/socket.h -> bits/types.h -> bits/typesizes.h)中被定义为1024。

现在的问题是,当fd>=1024时,FD_SET宏实际上会引起内存写越界。而实际上在man select中对已也有明确的说明,如下

NOTES

 

An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or
larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.

这一点包括之前的我,是很多人没有注意到的,并且云风大神有篇博文《一起 select 引起的崩溃》也描述了这个问题。

可以看出在Linux系统select也是不安全的,若想使用,得小心翼翼的确认fd是否达到1024,但这很难做到,不然还是老老实实的用poll或epoll吧。

扯得有点远了,但也引出了本片文章要叙述的主题,就是Linux系统下fd值是怎么分配确定,大家都知道fd是int类型,但其值是怎么增长的,在下面的内容中我对此进行了一点分析,以2.6.30版本的kernel为例,欢迎拍砖。

首先得知道是哪个函数进行fd分配,对此我以pipe为例,它是分配fd的一个典型的syscall,在fs/pipe.c中定义了pipe和pipe2的syscall实现,如下

  1. SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)  
  2. {  
  3.     int fd[2];  
  4.     int error;  
  5.  
  6.     error = do_pipe_flags(fd, flags);  
  7.     if (!error) {  
  8.         if (copy_to_user(fildes, fd, sizeof(fd))) {  
  9.             sys_close(fd[0]);  
  10.             sys_close(fd[1]);  
  11.             error = -EFAULT;  
  12.         }  
  13.     }  
  14.     return error;  
  15. }  
  16.  
  17. SYSCALL_DEFINE1(pipe, int __user *, fildes)  
  18. {  
  19.     return sys_pipe2(fildes, 0);  

进一步分析do_pipe_flags()实现,发现其使用get_unused_fd_flags(flags)来分配fd的,它是一个宏

#define get_unused_fd_flags(flags) alloc_fd(0, (flags)),位于include/linux/fs.h中

好了咱们找到了主角了,就是alloc_fd(),它就是内核章实际执行fd分配的函数。其位于fs/file.c,实现也很简单,如下

  1. int alloc_fd(unsigned start, unsigned flags)  

  2. {  

  3.     struct files_struct *files = current->files;  

  4.     unsigned int fd;  

  5.     int error;  

  6.     struct fdtable *fdt;  

  7.  

  8.     spin_lock(&files->file_lock);  

  9. repeat:  

  10.     fdt = files_fdtable(files);  

  11.    fd = start;  

  12.     if (fd < files->next_fd)  

  13.         fd = files->next_fd;  

  14.  

  15.     if (fd < fdt->max_fds)  

  16.         fd = find_next_zero_bit(fdt->open_fds->fds_bits,  

  17.                        fdt->max_fds, fd);  

  18.  

  19.     error = expand_files(files, fd);  

  20.     if (error < 0)  

  21.         goto out;  

  22.  

  23.     /*  

  24.      * If we needed to expand the fs array we  

  25.      * might have blocked - try again.  

  26.      */  

  27.     if (error)  

  28.         goto repeat;  

  29.  

  30.     if (start <= files->next_fd)  

  31.         files->next_fd = fd + 1;  

  32.  

  33.     FD_SET(fd, fdt->open_fds);  

  34.     if (flags & O_CLOEXEC)  

  35.         FD_SET(fd, fdt->close_on_exec);  

  36.     else  

  37.         FD_CLR(fd, fdt->close_on_exec);  

  38.     error = fd;  

  39. #if 1  

  40.     /* Sanity check */  

  41.     if (rcu_dereference(fdt->fd[fd]) != NULL) {  

  42.         printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);  

  43.         rcu_assign_pointer(fdt->fd[fd], NULL);  

  44.     }  

  45. #endif  

  46.  

  47. out:  

  48.     spin_unlock(&files->file_lock);  

  49.     return error;  

在pipe的系统调用中start值始终为0,而中间比较关键的expand_files()函数是根据所给的fd值,判断是否需要对进程的打开文件表进行扩容,其函数头注释如下

  1. /*  
  2.  * Expand files.  
  3.  * This function will expand the file structures, if the requested size exceeds  
  4.  * the current capacity and there is room for expansion.  
  5.  * Return <0 error code on error; 0 when nothing done; 1 when files were  
  6.  * expanded and execution may have blocked.  
  7.  * The files->file_lock should be held on entry, and will be held on exit.  
  8.  */ 

此处对其实现就不做深究了,回到alloc_fd(),现在可以看出,其分配fd的原则是

每次优先分配fd值最小的空闲fd,当分配不成功,即返回EMFILE的错误码,这表示当前进程中fd太多。

到此也印证了在公司写的服务端程序(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()
open系统调用也是用get_unused_fd_flags(),这里就不列举了。

现在想回头说说开篇的select的问题。由于Linux系统fd的分配规则,实际上是已经保证每次的fd值尽量的小,一般非IO频繁的系统,的确一个进程中fd值达到1024的概率比较小。因而对此到底是否该弃用select,还不能完全地做绝对的结论。如果设计的系统的确有其他措施保证fd值小于1024,那么用select无可厚非。

但在网络通讯程序这种场合是绝不应该作此假设的,所以还是尽量的不用select吧!!

原文链接:http://www.cnblogs.com/lanyuliuyun/p/3946564.html

责任编辑:牛小雨 来源: lanyuliuyun的博客
相关推荐

2021-09-14 09:49:12

Linuxsocket fd网络

2009-11-11 10:22:15

linux系统时间操作系统

2020-11-04 11:25:33

Linux目录命令

2011-08-22 15:39:38

Linux

2011-04-21 09:54:14

Linuxiptables

2011-08-16 14:18:09

UbuntuLinuxLinuxwindows

2009-09-07 09:56:02

Linux系统LVM扩充Linux

2011-03-09 13:02:15

LinuxLAMP安装

2009-07-01 09:13:10

Linux

2009-08-06 19:37:17

2009-06-25 08:53:03

Linux操作系统

2009-03-30 14:32:15

LinuxNetware服务器

2012-02-06 09:40:25

2009-06-30 10:35:36

Linux

2009-06-29 09:21:41

Linux

2009-04-28 19:05:28

Linux系统服务器

2009-06-25 08:58:03

Linux

2010-05-28 11:05:56

Linux下dhcp测

2012-11-16 13:26:16

2011-01-11 16:00:13

Linux软件安装
点赞
收藏

51CTO技术栈公众号