29

从Nginx源码中学习C语言位域的使用 - DaemonCoder

 4 years ago
source link: https://www.daemoncoder.com/a/%E4%BB%8ENginx%E6%BA%90%E7%A0%81%E4%B8%AD%E5%AD%A6%E4%B9%A0C%E8%AF%AD%E8%A8%80%E4%BD%8D%E5%9F%9F%E7%9A%84%E4%BD%BF%E7%94%A8/4d6a453d?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

位域长什么样

如果你阅读过Nginx源码的话,可以发现大量的位域的使用。如 ngx_process_t 结构体,定义如下:
typedef struct {
ngx_pid_t pid;
int status;
ngx_socket_t channel[2];

ngx_spawn_proc_pt proc;
void *data;
char *name;

unsigned respawn:1;
unsigned just_spawn:1;
unsigned detached:1;
unsigned exiting:1;
unsigned exited:1;
} ngx_process_t;
Nginx的 master 进程在管理所有的 worker 进程时,会用一个数组来记录每个 worker 进程的相关数据,数组的每一项都是一个上述 ngx_process_t 结构体的实例。如果你对Nginx不太熟悉也没有关系,我们不关心Nginx实现的细节,本文我们只关注位域的使用。
仔细观察上述结构体的定义可以看到,最后几个成员变量的定义有些特殊,变量名之后还有一个冒号和一个数字,这个数字指定了变量在存储时占用的位数,这就是我们本文要说的位域(又叫位段)。结构体中的成员变量可以指定位域,指定位域的几个相邻的字段,可以被压缩存储。

为什么要用位域

再回到上述的例子,用到位域的几个字段(respawn、just_spawn、detached、exiting、exited),取值都只有两种情况:0和1。以 exited 字段为例,1表示已经退出,0表示没有退出。我们平时在写程序时,也会经常遇到这种场景,值只有很少的几种情况,而我们通常会把每个字段定义为 int 类型,或者像上面例子中的 unsigned 类型,这样每个字段都需要4字节来存储,明明一位就可以搞定的地方却用了32位,这是一种没必要的浪费。
前面我们也提到了,结构体中指定位域的几个相邻的字段,可以压缩存储。上面 ngx_process_t 结构体最后5个字段可以被存储在一个 unsigned 类型所占的内存中,总共占了32位,也就是4字节(为什么不是5位而是32位呢?这里是出于内存对齐的考虑)。如果不用位域,最后几个字段总共占用 5*32=160 位,也就是20字节。内存占用的差距一目了然。

位域只能使用在特写的类型上

使用位域时需要注意,C语言标准中规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int,到了 C99,_Bool 也被支持了。但是编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型。所以对char等类型指定位域的代码虽然不符合C语言标准,但它依然能够被编译器支持。
这个是一个使用位域的示例:
// 未使用位域
printf("%lu\n", sizeof(struct {
    unsigned a;
    unsigned b;
    unsigned c;
}));    // 输出12,三个unsigned类型的大小

// 使用位域,大小被压缩成一个unsigned类型的大小
printf("%lu\n", sizeof(struct {
    unsigned a:1;
    unsigned b:1;
    unsigned c:1;
}));    // 输出4,一个unsigned类型的大小

位域指定的位数,不能超过类型本身的大小

如我们给一个 unsigned int 类型指定位域为64时,会在编译时报错:
struct {
unsigned a:64;
}
// error: width of bit-field 'a' (64 bits) exceeds width of its type (32 bits)

只有结构体中相邻的指定位域字段才可以被压缩存储

看下面的例子:
printf("%lu\n", sizeof(struct {
unsigned a:16;
unsigned b;
unsigned c:16;
})); // 输出:12
字段b没有用位域,所以a和c不能被压缩到一块存储,整体依旧是占用了12字节。

当共用的空间不足时,开启一个新的共用空间

看下面示例:
printf("%lu\n", sizeof(struct {
unsigned a:16;
unsigned b:1;
unsigned c:16;
})); // 输出:8
a、b、c 三个字段都是指定了位域,分别占用16位、1位、16位,前两个字段a和b会共用一个unsigned类型大小的空间,也就是32位,剩余32-16-1=15位空闲,字段c需要16位来存储,之前的剩余空间已经不足,所以会用一个新的unsigned大小来存储c,所以总共占用了两个unsigned大小,也就是8字节。
使用位域时,可以用没有变量名的无名位域,示例如下:
printf("%lu\n", sizeof(struct {
unsigned a:16;
unsigned :14;
unsigned b:1;
unsigned c:16;
})); // 输出:8
上前一个示例相同,只不过用一个无名位域填充了第一个unsigned最后空闲的14位,这样b和c就共用了一块空间。
位域还可以指定位数为0,但是此时只能使用无名位域。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK