一、宏与数据结构:从链表到通用容器
说实话,我刚开始学C语言那会儿,觉得宏就是简单的文本替换。直到我在一个嵌入式项目中,需要管理几百个动态节点——任务队列、定时器列表、事件回调……那时候我才意识到,宏的真正威力在于它能帮你写出“通用”的数据结构代码。
今天咱们聊聊Linux内核里那些经典的数据结构宏。你想想看,内核里成千上万的链表、哈希表、红黑树,它们是怎么做到既高效又通用的?答案就在宏里面。
1.1 链表宏:Linux 内核的 list_head
先看最基础的。传统链表长这样:
struct my_list {
int data;
struct my_list *next;
struct my_list *prev;
};
问题很明显——每个数据结构都得自己写一套链表操作。我当年维护过一个项目,里面有七八种链表,每种都重复实现了插入、删除、遍历……代码量翻倍,bug也翻倍。
Linux内核的做法很巧妙:把链表指针从数据里抽出来。
struct list_head {
struct list_head *next, *prev;
};
然后通过宏把它嵌入到你的结构体里:
struct task {
int id;
char name[32];
struct list_head node; // 链表节点
};
这里有个关键问题:我拿到一个 list_head 指针,怎么找到它所属的 task 结构体?
答案就是大名鼎鼎的 container_of 宏:
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
核心思想:通过成员变量的地址,反推出结构体的起始地址。说白了就是“已知房间号,反推楼栋号”。
有了这个基础,链表操作宏就水到渠成了:
// 初始化
#define INIT_LIST_HEAD(head) do { \
(head)->next = (head); \
(head)->prev = (head); \
} while(0)
// 插入
#define list_add(new, head) __list_add(new, head, head->next)
// 遍历
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
我的习惯:在实际项目中,我通常把 list_head 放在结构体的第一个字段。这样 container_of 的偏移量就是0,调试时少踩一个坑。
1.2 哈希表宏:hlist 的设计哲学
哈希表跟链表不太一样。哈希桶通常只需要单向遍历,而且表头很小——毕竟可能有上万个桶。
Linux 内核的 hlist 做了个有意思的设计:表头只有一个 next 指针,没有 prev。
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev; // 注意:二级指针
};
为什么用二级指针?我举个例子你就明白了。删除一个节点时,需要修改前一个节点的 next 指针。但如果是单向链表,你没法直接找到前一个节点。用 pprev 指向“前一个节点的 next 指针的地址”,就能直接修改它。
#define hlist_del(n) \
(*(n)->pprev = (n)->next, \
if ((n)->next) (n)->next->pprev = (n)->pprev)
我曾经踩过的坑:hlist 的 pprev 是二级指针,不是一级指针。刚开始用的时候我老把它当成普通指针,结果解引用一次就拿到了错误地址。调试了一下午才发现问题。
1.3 红黑树宏:rbtree 的平衡之道
红黑树比链表复杂得多。但内核的 rbtree 宏设计得相当精巧——它只提供核心的插入、删除、查找框架,具体的比较逻辑交给使用者。
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
};
struct rb_root {
struct rb_node *rb_node;
};
插入操作的典型用法:
int insert_node(struct rb_root *root, struct my_data *data) {
struct rb_node **new = &root->rb_node;
struct rb_node *parent = NULL;
while (*new) {
struct my_data *this = container_of(*new, struct my_data, node);
parent = *new;
if (data->key < this->key)
new = &(*new)->rb_left;
else if (data->key > this->key)
new = &(*new)->rb_right;
else
return -EEXIST; // 已存在
}
rb_link_node(&data->node, parent, new);
rb_insert_color(&data->node, root);
return 0;
}
你看,比较逻辑完全由你控制。想按整数比较?写 if (a < b)。想按字符串比较?写 strcmp。内核不替你做决定。
1.4 实战:构建通用容器
好了,理论说完了。咱们动手做个通用容器——一个支持任意数据类型的动态数组。
先定义接口:
struct generic_array {
void *data; // 数据指针
size_t elem_size; // 元素大小
size_t capacity; // 容量
size_t size; // 当前元素个数
};
#define ARRAY_INIT(elem_size, capacity) \
{ .data = NULL, .elem_size = (elem_size), \
.capacity = (capacity), .size = 0 }
// 宏封装:获取元素
#define array_get(arr, type, index) \
((type *)((arr).data + (index) * (arr).elem_size))
// 宏封装:遍历
#define array_for_each(arr, type, elem, i) \
for (i = 0, elem = array_get(arr, type, 0); \
i < (arr).size; \
i++, elem = array_get(arr, type, i))
使用示例:
struct point {
int x, y;
};
struct generic_array points = ARRAY_INIT(sizeof(struct point), 10);
// 添加元素
struct point p = {1, 2};
array_push(&points, &p);
// 遍历
struct point *pt;
size_t i;
array_for_each(points, struct point, pt, i) {
printf("(%d, %d)\n", pt->x, pt->y);
}
关键点:宏在这里的作用是“类型擦除”。C语言没有泛型,但通过 void * + 宏封装,我们实现了类似泛型的效果。说白了就是——运行时用 void * 存数据,编译时用宏恢复类型信息。
1.5 知识体系总览
下面这张图总结了本章的核心逻辑:
这张图把咱们讲的内容串起来了。你看,三种数据结构虽然形态各异,但底层都依赖 container_of 和宏封装。这就是C语言宏的威力——它让你在保持性能的同时,写出真正通用的代码。
我的建议:别急着在项目里用红黑树。先从 list_head 开始,把它用熟。等你发现“链表遍历太慢,需要快速查找”的时候,再考虑哈希表或红黑树。循序渐进,别一步登天。
公众号:蓝海资料掘金营,微信deep3321