由于业务上的需要,为了还原应用本身的 IO 复用行为,以 select
函数为例,其函数原型为:
1
2
int select ( int nfds , fd_set * restrict readfds , fd_set * restrict writefds ,
fd_set * restrict errorfds , struct timeval * restrict timeout );
复制
其参数 timeout 指 select
函数本身的超时时间,从逻辑上讲,在 timeout 不为 0 的情况下,可以把单次 select
调用拆分为多次调用,例如 timeout 为 5s 可以拆分为 5 次 timeout 为 1s 的 select
;若返回值为 0,那么每次重新调用 select
函数前都需要将参数 fds 重置为第一次调用前的值,如何重置呢?调用 FD_COPY
函数,它封装了 bcopy
函数,其函数原型为:
1
void bcopy ( const void * src , void * dest , size_t n );
复制
那么对于 FD_COPY
函数,参数 n 是如何得到的呢?通过源码可知是由 sizeof(*src)
得到的,通常情况下这并不会有什么问题,然而当 src 是由 malloc
动态分配时,灾难就发生了。
sizeof
并不是一个函数,而是一个一元操作符 (unary operator),用它可以得到一个表达式或数据类型 的存储大小,例如在 64 位机器上:
1
2
3
4
5
sizeof ( char ) // 1 Bytes
sizeof ( int ) // 4 Bytes
sizeof ( long ) // 8 Bytes
sizeof ( double ) // 8 Bytes
sizeof ( char * ) // 8 Bytes
复制
那么对于 bcopy
中使用 sizeof(*src)
得到的大小是多少呢?有人会说应该是指针指向的那块内存地址的大小,实际上 sizeof
只关心数据的类型,并不关心数据本身在内存中的具体存储大小,以下是实际的测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试环境:
// Ubuntu 22.04, gcc-11
#include <stdio.h>
int main ( void ) {
char * a = "a" ;
char * b = "bb" ;
char * str1 = "abcd" ;
char str2 [ 4 ] = "abcd" ;
printf ( "%lu %lu %lu %lu \n " , sizeof ( * a ), sizeof ( * b ), sizeof ( * str1 ),
sizeof ( * str2 ));
return 0 ;
}
// 输出为:1 1 1 1
复制
可以看到变量 a, b, str1, str2 的类型大小都为 1 字节,显然其类型全为 char,sizeof
得到的值只与其类型有关。
开头我们提到重复调用 select
函数需要重置 fds,而 fds 是一个结构体,那么对结构体求 sizeof
的值是什么呢?
我们先定义一个简单的结构体进行测试:
1
2
3
4
struct int_set {
int set [ 32 ];
};
// sizeof(struct int_set) 为 128 Bytes
复制
int_set 结构体中只有一个容量为 32 的数组成员变量,因此其类型大小为 32 * sizeof(int)
即 128 Bytes,那么成员变量稍多的结构体如何呢?
1
2
3
4
5
6
struct student {
int num ;
char name ;
int sex ;
};
// sizeof(struct student) 为 12 Bytes
复制
结构体 student 有 3 个成员变量,分别为 2 个 int 类型和 1 个 char 类型,2 * sizeof(int) + sizeof(char)
应当为 9 Bytes 才对为什么输出为 12 Bytes 呢?这涉及到编译过程中的内存对齐,至于为什么要对齐我们先不深入讨论,一般来讲,有硬件兼容与性能提升这两个原因。
结构体内存对齐遵循以下 2 条规则:
成员变量的偏移量为其自身类型大小的整数倍。
结构体的总大小为最大成员变量类型大小的整数倍。
由于 int 类型比 char 类型大 3 Bytes,因此需要在 char 类型后补 3 Bytes 对齐到 4 Bytes,符合上述内存对齐规则中的第 2 条,那么我们在成员变量 name 后添加一个类型为 short 的成员变量 age,此时的内存对齐情况如何呢?
1
2
3
4
5
6
7
struct student {
int num ;
char name ;
short age ;
int sex ;
};
// sizeof(struct student) 仍为 12 Byte
复制
short 类型为 2 Bytes,根据内存对齐的第 1 条规则,此时应当在 name 后补 1 Byte,而后紧跟 age,示意图如下:
1
2
3
4
5
6
7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| num |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| name | 1B | age |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| sex |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
复制
好了扯远了,在实际应用过程中我们通常都是以 struct fd_set readfds;
的形式定义一个结构体变量,但已 fd_set 结构体为例,在 fd 可控的情况下我们其实不需要用满结构体成员变量的所有容量,转而使用 malloc
对内存分配做更精细化的控制,此时若我们仍用原来的方式(例如 FD_COPY
)操作这样的变量就容易出现难以定位的问题。
直接进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct int_set {
int set [ 10 ];
} int_set ;
int main ( void ) {
int_set ints1 ;
for ( int i = 0 ; i < 10 ; ++ i ) {
// init
ints1 . set [ i ] = i ;
}
int_set * ints2 = ( int_set * ) malloc ( sizeof ( int_set ));
int_set * ints3 = ( int_set * ) malloc ( 6 * sizeof ( int ));
memcpy ( ints2 , & ints1 , 6 * sizeof ( int ));
memcpy ( ints3 , & ints1 , 6 * sizeof ( int ));
for ( int i = 0 ; i < 10 ; ++ i ) {
printf ( "ints1[%d] = %d, ints2[%d] = %d, ints3[%d] = %d \n " , i , ints1 . set [ i ],
i , ints2 -> set [ i ], i , ints3 -> set [ i ]);
}
return 0 ;
}
复制
输出结果为:
1
2
3
4
5
6
7
8
9
10
ints1[0] = 0, ints2[0] = 0, ints3[0] = 0
ints1[1] = 1, ints2[1] = 1, ints3[1] = 1
ints1[2] = 2, ints2[2] = 2, ints3[2] = 2
ints1[3] = 3, ints2[3] = 3, ints3[3] = 3
ints1[4] = 4, ints2[4] = 4, ints3[4] = 4
ints1[5] = 5, ints2[5] = 5, ints3[5] = 5
ints1[6] = 6, ints2[6] = 0, ints3[6] = 1041
ints1[7] = 7, ints2[7] = 0, ints3[7] = 0
ints1[8] = 8, ints2[8] = 0, ints3[8] = 1937010281
ints1[9] = 9, ints2[9] = 0, ints3[9] = 1563974449
复制
可以看到当前环境下,使用 malloc
动态分配内存的大小小于结构体类型的大小时,就不能用 sizeof
去操作一个变量了,很「遗憾」的是 memcpy
参数大于其变量本身大小时在编译过程中编译器是不会报错的,然而后续的内存空间已经被不幸覆盖,最终在运行过程中酿成「血案」。
sizeof
是运算符 而不是函数
结构体需要内存对齐
谨慎使用 sizeof
操作 malloc
动态分配的变量
sizeof - Wikipedia
Struct memory layout in C - Stack Overflow
Mac OS X Manual Page For FD_COPY(2) - Apple Developer