事件的起因
由于业务上的需要,为了还原应用本身的 IO 复用行为,以 select
函数为例,其函数原型为:
|
|
其参数 timeout 指 select
函数本身的超时时间,从逻辑上讲,在 timeout 不为 0 的情况下,可以把单次 select
调用拆分为多次调用,例如 timeout 为 5s 可以拆分为 5 次 timeout 为 1s 的 select
;若返回值为 0,那么每次重新调用 select
函数前都需要将参数 fds 重置为第一次调用前的值,如何重置呢?调用 FD_COPY
函数,它封装了 bcopy
函数,其函数原型为:
|
|
那么对于 FD_COPY
函数,参数 n 是如何得到的呢?通过源码可知是由 sizeof(*src)
得到的,通常情况下这并不会有什么问题,然而当 src 是由 malloc
动态分配时,灾难就发生了。
sizeof 是什么?
sizeof
并不是一个函数,而是一个 一元操作符(unary operator),用它可以得到一个表达式或 数据类型 的存储大小,例如在 64 位机器上:
|
|
那么对于 bcopy
中使用 sizeof(*src)
得到的大小是多少呢?有人会说应该是指针指向的那块内存地址的大小,实际上 sizeof
只关心数据的类型,并不关心数据本身在内存中的具体存储大小,以下是实际的测试结果:
|
|
可以看到变量 a, b, str1, str2 的类型大小都为 1 字节,显然其类型全为 char,sizeof
得到的值只与其类型有关。
开头我们提到重复调用 select
函数需要重置 fds,而 fds 是一个结构体,那么对结构体求 sizeof
的值是什么呢?
结构体的类型大小
我们先定义一个简单的结构体进行测试:
|
|
int_set 结构体中只有一个容量为 32 的数组成员变量,因此其类型大小为 32 * sizeof(int)
即 128 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,此时的内存对齐情况如何呢?
|
|
short 类型为 2 Bytes,根据内存对齐的第 1 条规则,此时应当在 name 后补 1 Byte,而后紧跟 age,示意图如下:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| num |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| name | 1B | age |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| sex |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
好了扯远了,在实际应用过程中我们通常都是以 struct fd_set readfds;
的形式定义一个结构体变量,但已 fd_set 结构体为例,在 fd 可控的情况下我们其实不需要用满结构体成员变量的所有容量,转而使用 malloc
对内存分配做更精细化的控制,此时若我们仍用原来的方式(例如 FD_COPY
)操作这样的变量就容易出现难以定位的问题。
用 malloc 定义结构体
直接进行测试:
|
|
输出结果为:
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
动态分配的变量