Linux系统下fd分配的方法

最近几天在公司里写网络通讯的代码比较多,自然就会涉及到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 # 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);

20 }

  进一步分析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;

50 }

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

  /*

  * Expand files.

  * This function will expand the file structures, if the requested size exceeds

  * the current capacity and there is room for expansion.

  * Return <0 error code on error; 0 when nothing done; 1 when files were

  * expanded and execution may have blocked.

  * The files->file_lock should be held on entry, and will be held on exit.

  */

  此处对其实现就不做深究了,回到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吧!!

最新内容请见作者的GitHub页:http://qaseven.github.io/

时间: 2024-10-27 13:42:37

Linux系统下fd分配的方法的相关文章

Linux系统下Telnet的设置方法

  Linux系统下Telnet的设置方法 Linux系统下Telnet服务的配置步骤: 一.安装Telnet软件包(通常要两个) 1. Telnet-clIEnt (或 Telnet),这个软件包提供的是 Telnet 客户端程序; 2. Telnet-server ,这个软件包提供的是 Telnet 服务器端程序; 安装之前先检测是否这些软件包已安装,方法如下: [root@echo root]#rpm –q Telnet或[root@wljs root]#rpm –q Telnet-cli

关于网络抓包,linux系统下kismet的使用方法

问题描述 关于网络抓包,linux系统下kismet的使用方法 我想知道k?i?smet怎么用呢?我看到它抓的包了,但怎么保存成文件?只看到它扫描到附近的Wi-Fi,能抓到客户端的包么? 解决方案 使用XILINX SDK工具通过网络进行基于linux系统的远程调试方法Linux下抓包工具tcpdump使用方法Linux下的网络HOOK实现以及使用方法

Linux系统下软件包的制作方法和过程

  总的情况下Linux下软件安装主要有三种方式: 一种是源码安装,需要用户自己手动编译 另一种是RPM包(RedHat Linux Packet Manager),通过RPM命令就可以实现安装 还有一种为*.bin文件,安装方法与Windows下的安装过程类似 通常情况下Linux 安装软件主要通过以下两种方式 ① 文件名形如xxx.i386.rpm ,软件包以二进制形式发布. ② 文件名形如xxx.tar.gz ,软件包多以源码的形式发布. Linux软件包管理可以分为三类:二进制包的管理,

linux系统下sudo命令使用方法详解

介绍 sudo是linux系统管理指令,是允许系统管理员让普通用户执行一些或者全部的root命令的一个工具,如halt,reboot,su等等.这样不仅减少了root用户的登录和管理的时间,同样也提高了安全性.sudo不是对shell的一个代替,它是面向每个命令的. sudo命令 sudo -K -L -V -h -k -l -vsudo [-HPSb] [-a auth_type] [-c class-] [-p prompt] [-u username#uid] {-e file [...]

Linux系统下ADSL拨号上网方法

本指南将帮助你在Ubuntu 6.06 LTS (Dapper Drake)下用PPPoE以太网modem建立ADSL网络连接. 1. 简介 虽然用路由器联网很常见,我们有时也需要用PPPoE直接连接到ADSL(有时也叫DSL) modem. 当然首先你要付好钱给网通或电信什么的 网络运营商,把线路接好.modem上的"DSL"灯亮,一般表示你的线路通畅. 你要有账号的用户名和密码.Modem和以太网卡之间还得用合适的网络线连接起来. 2. 用命令行配置PPPoE 设置modem要用终

SuSE Linux系统下设定Apache的方法

Apache需要设定成虚拟网站空间方式. 新版SuSE的Apache已经改成模组化. 所以只要修改几个档案就可以在建立帐号时,网站就对应的到位置. 而不用去修改http.conf一个个建对应/home的位置,建完还要重新启动Apache . 1.修改/etc/sysconfig/apache2 ]#vi /etc/sysconfig/apache2 找APACHE_MODULES 在最后面加上vhost_alias 存档重新编译apach2的设定档让apache2载入vhost模组 ]#SuSE

Linux系统下安装Memcache的方法

1.分别把memcached和libevent下载回来,放到 /tmp 目录下: # cd /tmp # wget http://www.danga.com/memcached/dist/memcached-1.2.0.tar.gz # wget http://www.monkey.org/~provos/libevent-1.2.tar.gz 2.先安装libevent: # tar zxvf libevent-1.2.tar.gz # cd libevent-1.2 # ./configur

Linux系统下设置时钟简单方法

1.显示时钟 [root@****~]# clock --show 2011年09月27日 星期二 11时21分09秒 -0.223408 seconds 2.设置时钟(clock --set --date="月/日/年 时:分:秒") [root@**** ~]# clock --set --date="05/06/2008 11:26:00" 3.与硬件时钟同步(默认重启系统就会与硬件时钟同步) [root@**** ~]# clock --systohc 4.

Linux系统下的历史记录删除方法

  1.修改/etc/profile将HISTSIZE=1000改成0或1 清除用户home路径下.bash_history 2.立即清空里的history当前历史命令的记录 history -c 3.bash执行命令时不是马上把命令名称写入history文件的,而是存放在内部的buffer中,等bash退出时会一并写入. 不过,可以调用'history -w'命令要求bash立即更新history文件. history -w