问题缘由,在学习 rt-thread 内核的时候遇到了这么一行代码:
to_thread = rt_list_entry(rt_thread_priority_table[0].next,
struct rt_thread,
tlist);
而 rt_list_entry 的宏定义如下:
/* 已知一个结构体里面的成员的地址,反推出该结构体的首地址 */
#define rt_container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
#define rt_list_entry(node, type, member) \
rt_container_of(node, type, member)
但是不理解 &((type *)0)->member 这段代码的意义,于是在网上搜集了一些资料后,总结如下:
该语句可以在不生成结构体实例的情况下计算结构体成员的偏移量,结构体变量的某成员的地址等于该结构体变量的基址加上结构体成员变量在结构体中的偏移量。而 (type*)0 就是假设地址 0 处存放的是一个 type 类型的结构体变量,这样的话这个结构体变量的基址就是 0,所以结构体成员 ((type *)0)->member 的地址的大小在数值上就等于该结构体成员在结构体中的偏移量。
下面是一个简单的例子:
struct test {
short a; // 偏移 0 个字节,占用 2 个字节
short b; // 偏移 2 个字节,占用 2 个字节
int c; // 偏移 4 个字节,占用 4 个字节
int d; // 偏移 8 个字节,占用 4 个字节
};
int main(void) {
printf("a 偏移 %d 个字节, 占用 %d 个字节\n", (unsigned long) &((struct test *)0)->a, (unsigned long) sizeof(((struct test *)0)->a));
printf("b 偏移 %d 个字节, 占用 %d 个字节\n", (unsigned long) &((struct test *)0)->b, (unsigned long) sizeof(((struct test *)0)->b));
printf("c 偏移 %d 个字节, 占用 %d 个字节\n", (unsigned long) &((struct test *)0)->c, (unsigned long) sizeof(((struct test *)0)->c));
printf("d 偏移 %d 个字节, 占用 %d 个字节\n", (unsigned long) &((struct test *)0)->d, (unsigned long) sizeof(((struct test *)0)->d));
return 0;
}
可以看到结果和我们预想的一样:
因为在 C 中,结构体内成员的偏移量是不会发生变化的,所以知道了一个结构体中的成员的地址和偏移量,我们就可以计算出结构体的地址,如下面这个例子:
struct test {
short a; // 偏移 0 个字节,占用 2 个字节
short b; // 偏移 2 个字节,占用 2 个字节
int c; // 偏移 4 个字节,占用 4 个字节
int d; // 偏移 8 个字节,占用 4 个字节
};
int main(void) {
struct test t1;
t1.a = 1;
t1.b = 2;
t1.c = 3;
t1.d = 4;
// 假设我们只知道 t1 结构体中 d 的地址
int * d_ptr = &t1.d;
// 那么我们可以通过如下方式获取到 t1 结构体的地址
struct test *t2 = (struct test *)((char *)d_ptr - (unsigned long)(&((struct test *)0)->d));
printf("a = %d, b = %d, c = %d, d = %d\n", t2->a, t2->b, t2->c, t2->d);
return 0;
}
最终的结果如下:
在 rt-thread 中,链表结构体很简单,只有前向指针和后向指针两个参数:
如果有结构体需要使用链表结构的话,直接把上述结构体包含进来即可:
在遍历链表的时候,我们只需要用到 rt_list_t 结构体,找到对应的结构体后,我们再通过 rt_list_t 结构体及其偏移计算出 rt_thread 结构体的地址,是不是很灵活!
另外,在 linux 内核中,链表也是用这种方式实现的。