Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

彻底理解 selectfd_set #4

Open
JemmyH opened this issue May 26, 2021 · 2 comments
Open

彻底理解 selectfd_set #4

JemmyH opened this issue May 26, 2021 · 2 comments
Assignees
Labels
documentation Improvements or additions to documentation done Implementation by Go

Comments

@JemmyH
Copy link
Owner

JemmyH commented May 26, 2021

看源码,fd_set 这个结构体实际上是一个 long 型的数组,但是数组的长度依赖于系统中 typedef long int __fd_mask 的长度。当我去调试的时候,经常打印出一些很奇怪的值,有时候还会溢出。

此 issue 旨在抛开 select 相关的功能,彻底搞明白 fd_set 的存储原理、FD_SET() 等函数到底实现了什么效果。

@JemmyH
Copy link
Owner Author

JemmyH commented May 26, 2021

本次调试机器:

$ 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 数组中的内容。需要注意两点:

  1. 数组长度是 16,是如何确定的?答:在处理过预编译指令之后,__FD_SETSIZE 的值是 1024__NFDBITS 的值是 64,计算得到数组长度为 16;

  2. 类型的长度如何确定?答:在最开始使用了 typedef long int __fd_masklong int 其实就是 long,即 long signed integer type。熟悉 C语言的同学都知道,当我们描述 shortintlong 的长度时,只有对 short 的长度是肯定的,而对后两者都使用了 可能 的说法:可能长度为 8。这是因为 C语言 没有对其做出严格的规定,只做了宽泛的限制:short2字节int 建议为机器字长,64 位机器下占4字节2 ≤ short ≤ int ≤ long,如上述代码中打印结果所示,在我测试的这台机器上,long8字节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 04611686033459871886 转成二进制为 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 013835058070314647694 转换成二进制后为 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

@JemmyH JemmyH closed this as completed May 26, 2021
@JemmyH JemmyH reopened this May 26, 2021
@JemmyH JemmyH self-assigned this May 26, 2021
@JemmyH JemmyH added documentation Improvements or additions to documentation done labels May 26, 2021
@JemmyH
Copy link
Owner Author

JemmyH commented Jun 3, 2021

我用 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
}

@JemmyH JemmyH changed the title 理解 fd_set 及其用法 彻底理解 selectfd_set Jun 4, 2021
Repository owner locked as resolved and limited conversation to collaborators Jun 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
documentation Improvements or additions to documentation done Implementation by Go
Projects
None yet
Development

No branches or pull requests

1 participant