看 select 源码,fd_set 这个结构体实际上是一个 long 型的数组,但是数组的长度依赖于系统中 typedef long int __fd_mask 的长度。当我去调试的时候,经常打印出一些很奇怪的值,有时候还会溢出。
本文旨在抛开 select 相关的功能,彻底搞明白 fd_set 的存储原理、FD_SET() 等函数到底实现了什么效果。
本次调试机器:
$ uname -a
Linux localhost 4.15.0-52-generic #56-Ubuntu SMP Tue Jun 4 22:49:08 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
fd_set 的源码中有非常多的预编译指令:
// /usr/include/x86_64-linux-gnu/sys/select.h
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* Some versions of <linux/posix_types.h> define this macros. */
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FD_MASK(d) ((__fd_mask) (1UL << ((d) % __NFDBITS)))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#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;
我简单整理一下,去掉和本系统无关的,结果如下:
typedef long int __fd_mask; // 8
#define __NFDBITS (8 * (int) sizeof (__fd_mask) // 64
#define __FD_SETSIZE 1024
typedef struct
{
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; // 长度为 1024/64=16,类型为 long
} fd_set;
接着,我们看一个 demo:
#include <stdio.h>
#include <sys/select.h>
void print_set1(fd_set *fdset);
void print_set2(fd_set *fdset);
int main()
{
fd_set fdset;
FD_ZERO(&fdset);
printf("sizeof long int: %ld\n", sizeof(long int)); // 8
printf("sizeof int: %ld\n", sizeof(int)); // 4
printf("sizeof short: %ld\n", sizeof(short)); // 2
FD_SET(1, &fdset);
FD_SET(2, &fdset);
FD_SET(3, &fdset);
FD_SET(7, &fdset);
print_set1(&fdset); // 10001110 -> 第 1 2 3 7 位分别设置成了 1
FD_SET(15, &fdset);
FD_SET(16, &fdset);
FD_SET(31, &fdset); // 10000000000000011000000010001110 -> 长度为 32
FD_SET(32, &fdset);
FD_SET(33, &fdset);
FD_SET(62, &fdset); // 100000000000000000000000000001110000000000000011000000010001110 0->长度为 63
print_set2(&fdset);
FD_SET(63, &fdset); // 1100000000000000000000000000001110000000000000011000000010001110 0 -> 长度为 64
print_set2(&fdset);
FD_SET(64, &fdset); // 1100000000000000000000000000001110000000000000011000000010001110 1 -> 长度还是 64,但产生了进位
print_set2(&fdset);
FD_SET(128, &fdset); // 1100000000000000000000000000001110000000000000011000000010001110 1 1-> 长度还是 63,但是在 64 和 128 的时候产生了进位
FD_SET(129, &fdset); // 1100000000000000000000000000001110000000000000011000000010001110 1 3-> 长度还是 63,但是在 64 和 128 的时候产生了进位
FD_SET(1023, &fdset); // 13835058070314647694 1 3 0 0 0 0 0 0 0 0 0 0 0 0 9223372036854775808
FD_SET(1024, &fdset); // 13835058070314647694 1 3 0 0 0 0 0 0 0 0 0 0 0 0 9223372036854775808
FD_SET(1025, &fdset); // 13835058070314647694 1 3 0 0 0 0 0 0 0 0 0 0 0 0 9223372036854775808
print_set2(&fdset);
int isset = FD_ISSET(7, &fdset);
printf("isset = %d\n", isset);// 输出 1,代表第 7 位被设置
FD_CLR(7, &fdset);
print_set2(&fdset); // 1100000000000000000000000000001110000000000000011000000000001110 1 3 0 0 0 0 0 0 0 0 0 0 0 0 9223372036854775808 -> 第 7 位的 1 变成了 0
isset = FD_ISSET(7, &fdset);
printf("isset = %d\n", isset);// 输出 0,代表第 7 位没有被设置
return 0;
}
void print_set2(fd_set *fdset)
{
int i;
for (i = 0; i < 16; i++)
{
printf("%llu ", (unsigned long long)fdset->__fds_bits[i]);
}
printf("\n");
}
void print_set1(fd_set *fdset)
{
int i;
for (i = 0; i < 16; i++)
{
printf("%ld ",fdset->__fds_bits[i]);
}
printf("\n");
}
print_set() 函数是我自己添加的,用于打印出 fd_set 中的 __fds_bits 数组中的内容。需要注意两点:
数组长度是 16,是如何确定的?答:在处理过预编译指令之后,
__FD_SETSIZE的值是1024,__NFDBITS的值是64,计算得到数组长度为 16;类型的长度如何确定?答:在最开始使用了
typedef long int __fd_mask,long int其实就是long,即long signed integer type。熟悉 C语言的同学都知道,当我们描述short、int和long的长度时,只有对short的长度是肯定的,而对后两者都使用了 可能 的说法:可能长度为8。这是因为 C语言 没有对其做出严格的规定,只做了宽泛的限制:short占2字节;int建议为机器字长,64 位机器下占4字节;2 ≤ short ≤ int ≤ long,如上述代码中打印结果所示,在我测试的这台机器上,long占8字节即64位。
接下来我们看 main() 中的代码:
在调用 FD_SET() 设置 1 2 3 7 后,我们调用print_set1()打印结果,输出:142 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0,数组第一位竟然是 142?!哪来的?但我们将 142 转成二进制就一下子了然了:10001110, 总共占 8 位,从右往左从 0 开始数,只有第 1 2 3 7 位被设置成了 1, 这个二进制对应的数就是 142,因为 142 完全在一个 long 的范围(64位)内,所以正常表示了。那如果我们对一个超过 long 范围的数调用 FD_SET(),会是什么效果?
代码继续走到 FD_SET(62, &fdset),我们调用 print_set1(),输出 4611686033459871886 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0,4611686033459871886 转成二进制为 100000000000000000000000000001110000000000000011000000010001110,字符串长度为 63,可以看到,依旧在 long 的范围之内;执行到 FD_SET(63,&fdset) 呢,调用 print_set1(),输出 -4611686003394903922 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0,出现了负数。我们想一下原因。只执行FD_SET(62,&fdset) 会将第 63 位处设置为 1,对应的二进制为 100000000000000000000000000000000000000000000000000000000000000(长度为 63);根据 FD_SET() 的功能,我们可以猜测,FD_SET(63,&fdset) 会将第 64 位设置为 1,其对应的二进制应该是 1000000000000000000000000000000000000000000000000000000000000000(长度为 64)。
在这里我们补充一个知识:参考这里 Wiki-无符号数,在计算机底层,有符号数可以表示特定范围内的整数(包括负数),无符号数只能表示非负数(0 以及正数),有符号数能够表示负数的原因是,最高位被用来当做符号位,1 表示负数,0 表示正数,代价就是所表示的数的范围少了一半,举个例子,8 位可以表示无符号数 [0,255](最小
00000000;最大11111111,对应的十进制就是 255),对应的有符号数范围就是 [-127,127]。
再回头看 1000000000000000000000000000000000000000000000000000000000000000,__fd_mask 的类型是 long,是一个有符号数,最高位的 1 表示负数,后面的才是真正的值,于是这个二进制转成有符号十进制的结果就是 0,而且还是个 -0。为了验证我们的想法,我们将 print_set1() 换成 print_set2(),这两个函数唯一的不同是,将数组中的每一位的类型强转成了无符号数,这下结果就成了 9223372036854775808,符合我们的预期。 所以后面的调试,我们都使用 print_set2() 这个函数。
刚才的 FD_SET(63,&fdset) 已经到达一个 long 的最大可 表示范围了,如果再多一位,会发生什么?我们看下 FD_SET(64, &fdset),输出 13835058070314647694 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0,13835058070314647694 转换成二进制后为 1100000000000000000000000000001110000000000000011000000010001110(长度为 64),和 设置 63 时的一样,但数组的第二位变成了 1。按照前面的推测,单独执行 FD_SET(64, &fdset),应该将第 65 位设置成 1,长度为 65,但是 65 显然超过了 long 的可表示的长度(64位),于是,产生了“进位”,这个进位的基本单位就是 64位,即 __NFDBITS 的值。于是,可以用如下 python 代码表示(python 对大数处理非常好,一般不会出现溢出):
#!/usr/local/env python3
# coding:utf-8
# get_fd_set_bin 返回 fd_set 表示的真实二进制(从右往左方向)
# every_fd_bits 表示数组中每个元素代表多少位
# set_array 表示 fd_set 的 long 数组
def get_fd_set_bin(every_fd_bits, set_array):
int_value = 0
for idx in range(len(set_array)):
int_value = int_value | (set_array[idx] << every_fd_bits * idx)
return bin(int_value)[2:] # 输出 "0bxxxxxx",为了方便展示,去掉前缀
# print_bin 将二进制按照 step 为一组打印
def print_bin(output, step=64):
le = len(output)
m = le % step
padding = step - m if m != 0 else 0
output = output.zfill(le + padding)
print(' '.join(''.join(output[idx * step:(idx + 1) * step]) for idx in range((le + padding) // step)))
在我们当前的例子中,every_fd_bits = 64, set_array 的长度为 16。测试一下我们的代码:
# 输入(相当于设置了 1 2 3 7)
get_fd_set_bin(64, [142,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
# 输出
0000000000000000000000000000000000000000000000000000000010001110
# 输入(相当于设置了 1 2 3 7 64)
get_fd_set_bin(64, [142,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
# 输出
0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000000000000000000000000000000000000010001110
我用 Golang 做了简单实现:
package main
import (
"math/big"
"strings"
)
type FdSet interface {
FD_CLR(fd int) error
FD_ISSET(fd int) (bool, error)
FD_SET(fd int) error
FD_ZERO() error
FdSetString() string
}
type fdType uint64
const (
maxFdSetSize = 1024
fdMask = 8 // sizeof long int in c
fdBits = 8 * fdMask
)
type FdSetGolang struct {
data [maxFdSetSize / fdBits]fdType
}
func NewGdSetGolang() *FdSetGolang {
return &FdSetGolang{data: [maxFdSetSize / fdBits]fdType{}}
}
func (fs *FdSetGolang) FD_CLR(fd int) error {
fs.clear(fs.getMask(fd))
return nil
}
func (fs *FdSetGolang) FD_ISSET(fd int) (bool, error) {
return fs.isSet(fs.getMask(fd)), nil
}
func (fs *FdSetGolang) FD_SET(fd int) error {
fs.set(fs.getMask(fd))
return nil
}
func (fs *FdSetGolang) FD_ZERO() error {
for i := range fs.data {
fs.data[i] = 0
}
return nil
}
func (fs *FdSetGolang) FdSetString() string {
tmp := make([]string, 0, len(fs.data))
for i := len(fs.data) - 1; i >= 0; i-- {
if v := fs.uintBin(uint64(fs.data[i])); v != "" {
tmp = append(tmp, v)
}
}
// 最左边的那个,可以将前面的 0 全部去掉
if len(tmp) > 0 {
t := tmp[0]
if t != "" {
for i := 0; i < len(t); i++ {
if t[i] != '0' {
tmp[0] = t[i:]
break
}
}
}
}
return "-->" + strings.Join(tmp, " ") + "<--"
}
func (fs *FdSetGolang) getMask(fd int) (idx int, n int) {
return fd / fdBits, fd % fdBits
}
// set 将数组下标为 idx 的数的从右往左数的第(n+1)位设置为 1
func (fs *FdSetGolang) set(idx int, n int) {
old := fs.data[idx]
fs.data[idx] = 1<<n | old
}
// clear 将数组下标为 idx 的数的从右往左数的第(n+1)位设置为 0
func (fs *FdSetGolang) clear(idx int, n int) {
if fs.isSet(idx, n) {
fs.data[idx] ^= 1 << n
}
}
func (fs *FdSetGolang) isSet(idx, n int) bool {
old := fs.data[idx]
this := 1 << n
if int(old)&this == this {
return true
}
return false
}
// uintBin 输出 n 的二进制表示
func (fs *FdSetGolang) uintBin(n uint64) string {
if n == 0 {
return ""
}
s := big.NewInt(0).SetUint64(n).Text(2)
return strings.Repeat("0", fdBits-len(s)) + s
}
