前言
今天看到select中关于 FD_CLR,FD_ISSET,FD_SET,FD_ZERO四个宏定义,顺便看看其内部如何实现。 其实现主要靠的是位数组和位运算。 本文主要记录在linux上分析这四个宏实现的源码。
一、FD_四个宏介绍
通过select文档,可以看到几个宏的相关声明(通过man命令查询文档)在sys/select.h文件,其主要是为select以及pselect两个多路IO服务。 而select使用fd_set数据结构管理描述符集合,那么将涉及另外问题,如何将一个描述符添加到fd_set集合或者从改集合删除,四个宏FD_就是来完成这个任务的。下面简单描述这四个宏。
- FD_CLR宏将描述符fd从集合fd_set中移除
- FD_ISSET宏判断描述符fd是否在集合fd_set
- FD_SET宏将描述符fd添加到集合fd_set
- FD_ZERO宏将清空描述符集合fd_set
1 | SELECT(2) Linux Programmer's Manual |
二、FD_宏相关实现
在/usr/include/x86_64-linux-gnu/sys/select.h文件中查看FD_宏实现,其又依赖于__FD_相关宏。
1 | /* Access macros for `fd_set'. */ |
关于__FD相关定义,__FD_ZERO宏定义有两个版本(一个是GNU CC标准,一个是非GNU CC),在/usr/include/x86_64-linux-gnu/bits/select.h文件中可查看 _FD定义。
1 |
|
__FD宏定义都依赖于 _FDS_BITS。在/usr/include/x86_64-linux-gnu/sys/select.h, 可以看其实现以及fd_set结构。
1 | /* The fd_set member is required to be an array of longs. */ |
从中可以知道: (假如在64位操作系统)
fd_set集合中包含一个成员fds_bits,是一个数组,元素类型为__fd_mask,其实是long int 类型(在64位操作系统中,代表8个字节,32位操作系统4个字节),数组大小为 _FD_SETSIZE / __NFDBITS。因此fd_set结构主要就是为了维护一个数组(本质实现为位数组)
__FD_SETSIZE值是系统默认定义为1024,表示最多可以存放1024个描述符(位于文件:/usr/include/x86_64-linux-gnu/bits/typesizes.h)
__NFDBITS值依赖__fd_mask类型字节,将__fd_mask字节数*8, 那是因为一个字节刚好有8位,__fd_mask是8个字节( 使用二进制表示一共有2^64位),__NFDBITS值为64。
__FD_SETSIZE/__NFDBITS 即表示为1024/64=16, 因此fd_set结构中成员fd_bits数组大小为16, 而每一个元素可以表示64位,每一位将代表一个描述符, 所以16个元素可以表示1024个描述符。
1 |
- __FD_ELT(d), 表示d/__NFDBITS, 即在位数组中第几个元素。 假如描述符d=8, 那么会将d描述符保存在fds_bits数组中0号元素。d=351, 351/64 = 5, 那么保存d描述符到fds_bits数组5号元素。
1 |
- __FD_MASK(d), 表示将1左移d%__NFDBITS位, 值记为r,即表示描述符d存在fds_bits数组某个元素中的第r位。假如d=8, r = 8, 表示描述符即将存在fds_bits数组中0号元素中第8位(第0号元素二进制第8位为1),假如d=351, r = 31, 表示描述符即将存在fds_bits数组中5号元素中第31位(第5号元素二进制第31位为1)。
1 |
- __FDS_BITS(set), 即表示引用set结构中的fds_bits成员, 即可以直接引用fd_set集合中的fds_bits成员。
1 | # define __FDS_BITS(set) ((set)->fds_bits) |
接下来,就是将上面的介绍的串起来。
对于FD_SET宏,定义如下
1 |
接着看, 下面的转换
1 |
|
等价于
1 | void (set)->fds_bits[__FD_ELT (d)] |= __FD_MASK (d))) |
意味着__FD_set(d , set), 将描述符d存放到结构set中的fds_bits中,第(d) / __NFDBITS号元素中第1UL << ((d) % __NFDBITS))位赋值位1。(详细前面有分析)
同理可分析__FD_CLR, __FD_ISSET宏。注意区分他们各自使用的位运算.
对于__FD_ZERO宏,主要将描述符集合fd_set清空(即所有位置0)。 其实现有两种不同方式。 (基于我们对前面结构的了解,如果我们自己实现也不难)。第一个是GNU CC标准,目前看不太懂。第二个版本简单遍历数组,将每个元素置0.
1 |
|
三、总结
为什么FD_相关宏实现使用了位数组?
我觉得主要是为了节约内存。每一个二进制位即可用来表示一个描述符, 1个字节可用来表示8个描述符。
fd_set数据结构里面只有包含了一个数组成员, 为什么要使用一个结构来封装这个数组,而不直接使用这个数组即可?
我觉得主要是为了方便表示,使用一个变量和使用一个数组,显然使用一个变量更加方便。而且在底层数据拷贝的时候,拷贝一个数据结构变量,明显比拷贝一个数组方便。
对于信号集函数,实现也类似,使用了位数组。 其声明函数如下。
1 | SIGSETOPS(3) Linux Programmer's Manual SIGSETOPS(3) |
四、完整源码
头文件/usr/include/x86_64-linux-gnu/sys/select.h
1 | /* POSIX 1003.1g: 6.2 Select from File Descriptor Sets <sys/select.h> */ |
头文件/usr/include/x86_64-linux-gnu/bits/select.h
1 |
|