当前位置: 首页 > news >正文

【C】Linux 内核“第一宏”:container_of

📘 Linux 内核“第一宏”:container_of 完全学习手册(含链表实现对比)

✍️ author : 北国无红豆
📅 date : Sep 10, 2025
🎯 适用人群:C语言进阶者、Linux内核初学者、系统编程爱好者


本文从 Linux 内核“第一宏” container_of 的定义、原理、使用方法出发,深入讲解其在内核与用户空间链表中的应用,并对比三种主流“通过链表节点访问宿主结构体”的实现方式。所有示例统一使用 node_t 作为链表节点名,便于理解与实践。


🎯 一、Linux 内核“第一宏”:container_of

在 Linux 内核源码中,根据社区习惯、历史渊源和重要性,通常人们提到“Linux 内核第一宏”时,指的是:

container_of


🎯 为什么 container_of 被称为“第一宏”?

  1. 极其重要且广泛使用
    container_of 是 Linux 内核中用于通过结构体成员地址反推结构体起始地址的核心宏。在面向对象风格的内核代码(如设备驱动、链表操作)中无处不在。

  2. 体现内核设计哲学
    它展示了内核如何巧妙利用 C 语言特性(指针运算、offsetof)实现“面向对象”机制,是理解内核数据结构(如 list_head)的关键。

  3. 历史悠久,地位稳固
    自早期 Linux 内核版本就存在,几乎每个内核开发者都会用到或必须理解它。

  4. 面试/笔试高频题
    在内核或驱动开发岗位面试中,container_of 的实现和原理几乎是必考题,因此被开发者戏称为“第一宏”。


🔍 container_of 宏定义(位于 include/linux/kernel.h

#define container_of(ptr, type, member) ({                      \const typeof( ((type *)0)->member ) *__mptr = (ptr);    \(type *)( (char *)__mptr - offsetof(type, member) );})
参数说明:
  • ptr:指向结构体成员的指针。
  • type:结构体类型。
  • member:成员名。
示例:
struct student {int id;char name[20];struct node_t list;  // 👈 链表头命名为 node_t
};struct node_t *pos = ...; // 指向某个 student 的 list 成员
struct student *stu = container_of(pos, struct student, list);

🧮 二、container_of 如何“使用 offset”?

虽然 container_of 本身不直接“计算 offset”,但它依赖 offsetof 宏来获取成员在结构体中的偏移量,然后通过指针运算“反推”结构体起始地址。


🔧 container_of 完整定义回顾

#define container_of(ptr, type, member) ({                      \const typeof( ((type *)0)->member ) *__mptr = (ptr);    \(type *)( (char *)__mptr - offsetof(type, member) );})

📐 核心:offsetof 是如何计算偏移量的?

定义在 include/linux/stddef.h

#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
✅ 原理:
  • (TYPE *)0:把 0(空指针)强制转换成指向 TYPE 类型的指针。
  • ->MEMBER:访问该结构体的成员 MEMBER
  • &(...):取这个成员的地址。
  • 因为基地址是 0,所以这个地址值就等于成员相对于结构体起始地址的偏移量(offset)

⚠️ 注意:这看起来像“解引用空指针”,但因为没有真正访问内存(只是做地址运算),编译器会优化成常量偏移值,是安全且标准的做法(C 标准也认可 offsetof 的这种实现)。


🧮 实际计算过程图解

假设我们有:

struct student {int id;        // offset 0char name[20]; // offset 4int age;       // offset 24
};

现在我们有一个指向 age 成员的指针:

struct student *stu = malloc(sizeof(struct student));
int *p_age = &stu->age;

我们要通过 p_age 找到 stu —— 这就是 container_of 的任务!

Step 1: 计算 agestruct student 中的偏移量
offsetof(struct student, age)&((struct student *)0)->age 
→ 假设是 24(字节)
Step 2: 用 p_age 的地址减去偏移量,得到结构体起始地址
(char *)p_age - 24 → 得到 struct student 的起始地址
→ 再强转为 (struct student *)

这就是:

(type *)( (char *)__mptr - offsetof(type, member) )

🖥️ 完整示例 + 打印验证

#include <stdio.h>
#include <stddef.h>// 模拟 offsetof
#define my_offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)// 模拟 container_of
#define my_container_of(ptr, type, member) \((type *)((char *)(ptr) - my_offsetof(type, member)))struct demo_struct {int a;char b;long c;
};int main() {struct demo_struct my_struct = { .a = 10, .b = 'X', .c = 999 };long *ptr_to_c = &my_struct.c;// 使用 container_of 通过 &my_struct.c 找回 &my_structstruct demo_struct *recovered = my_container_of(ptr_to_c, struct demo_struct, c);printf("Original struct address: %p\n", (void *)&my_struct);printf("Recovered struct address: %p\n", (void *)recovered);printf("Offset of 'c': %zu\n", my_offsetof(struct demo_struct, c));if (&my_struct == recovered) {printf("✅ container_of worked correctly!\n");}return 0;
}
输出示例:
Original struct address: 0x7ffd12345670
Recovered struct address: 0x7ffd12345670
Offset of 'c': 8
✅ container_of worked correctly!

💡 注意:由于内存对齐,c 的偏移可能是 8 而不是 5(int 4 + char 1 + 3 字节填充)。


🧠 为什么用 typeof 和临时变量 __mptr

const typeof( ((type *)0)->member ) *__mptr = (ptr);

这是为了类型安全检查

  • 如果你传入的 ptr 类型与 member 类型不匹配,编译器会警告或报错。
  • 避免误用(比如传了个 int* 但成员是 char*)。

✅ 总结:container_of 如何“使用 offset”

  1. 调用 offsetof(type, member) 获取成员偏移量 —— 核心!
  2. 把成员指针转为 char *(字节单位)
  3. 减去偏移量 → 得到结构体起始地址
  4. 强转回 (type *)

🎯 所以,container_of 本身不计算 offset,而是依赖 offsetof 计算出的 offset 来做指针反推


🧩 附加:手动模拟 container_of

你可以自己写一个简化版:

#define my_container_of(ptr, type, member) \((type *)((char *)(ptr) - ((char *)&((type *)0)->member - (char *)0)))

或者更简单(因为 (char *)0 是 0):

#define my_container_of(ptr, type, member) \((type *)((char *)(ptr) - (size_t)&((type *)0)->member))

这就是 container_of 的本质!


✅ 掌握了 container_of + offsetof,你就掌握了 Linux 内核数据结构(如链表、红黑树、设备模型等)的底层魔法钥匙!


🧱 三、在普通链表中实现 container_of 思想!

你想在用户空间 C 程序中实现类似 Linux 内核的设计 —— 这正是“侵入式链表”的精髓!


✅ 核心思想:侵入式链表(Intrusive List)

“把链表节点嵌入到数据结构中” —— 而不是“把数据挂到链表节点上”。


🔧 1. 用户空间版 offsetofcontainer_of

// mylist.h
#ifndef MYLIST_H
#define MYLIST_H#include <stddef.h>// 计算成员偏移量
#define my_offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)// 通过成员指针反推结构体指针
#define my_container_of(ptr, type, member) \((type *)((char *)(ptr) - my_offsetof(type, member)))// 链表节点结构(命名为 node_t)
struct node_t {struct node_t *next, *prev;
};// 初始化宏
#define NODE_T_INIT(name) { &(name), &(name) }
#define NODE_T_HEAD(name) struct node_t name = NODE_T_INIT(name)// 初始化函数
static inline void INIT_NODE_T(struct node_t *list) {list->next = list;list->prev = list;
}// 插入节点(头插)
static inline void node_t_add(struct node_t *new, struct node_t *head) {new->next = head->next;new->prev = head;head->next->prev = new;head->next = new;
}// 删除节点
static inline void node_t_del(struct node_t *entry) {entry->prev->next = entry->next;entry->next->prev = entry->prev;entry->next = NULL;entry->prev = NULL;
}// 判断是否为空
static inline int node_t_empty(const struct node_t *head) {return head->next == head;
}// 遍历宏
#define node_t_for_each(pos, head) \for (pos = (head)->next; pos != (head); pos = pos->next)// 遍历并获取宿主结构体(核心!)
#define node_t_for_each_entry(pos, head, member)                        \for (pos = my_container_of((head)->next, typeof(*pos), member);     \&pos->member != (head);                                        \pos = my_container_of(pos->member.next, typeof(*pos), member))#endif

🎯 2. 定义你的数据结构 —— “嵌入”链表节点(命名为 node_t

// student.h
#include "mylist.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>struct student {int id;char name[32];struct node_t list;  // 👈 重点:嵌入 node_t
};

🛠️ 3. 使用示例:创建、插入、遍历学生链表

// main.c
#include "student.h"int main() {NODE_T_HEAD(student_list);  // 初始化链表头// 创建学生struct student *s1 = malloc(sizeof(struct student));s1->id = 1001;strcpy(s1->name, "Alice");INIT_NODE_T(&s1->list);struct student *s2 = malloc(sizeof(struct student));s2->id = 1002;strcpy(s2->name, "Bob");INIT_NODE_T(&s2->list);struct student *s3 = malloc(sizeof(struct student));s3->id = 1003;strcpy(s3->name, "Charlie");INIT_NODE_T(&s3->list);// 插入(头插)node_t_add(&s1->list, &student_list);node_t_add(&s2->list, &student_list);node_t_add(&s3->list, &student_list);// 遍历struct student *stu;printf("遍历学生链表:\n");node_t_for_each_entry(stu, &student_list, list) {printf("ID: %d, Name: %s\n", stu->id, stu->name);}// 删除 Bobprintf("\n删除 Bob...\n");node_t_del(&s2->list);free(s2);printf("再次遍历:\n");node_t_for_each_entry(stu, &student_list, list) {printf("ID: %d, Name: %s\n", stu->id, stu->name);}// 清理内存while (!node_t_empty(&student_list)) {struct node_t *first = student_list.next;struct student *tmp = my_container_of(first, struct student, list);node_t_del(first);free(tmp);}return 0;
}

✅ 4. 输出结果

遍历学生链表:
ID: 1003, Name: Charlie
ID: 1002, Name: Bob
ID: 1001, Name: Alice删除 Bob...再次遍历:
ID: 1003, Name: Charlie
ID: 1001, Name: Alice

🌟 5. 这种设计的好处

传统链表(非侵入式)侵入式链表(container_of 思想)
每个节点包含 data 指针数据结构直接包含 node_t 节点
需要为每种类型写不同链表一套链表代码通吃所有类型!
插入时需分配节点+数据数据本身就是节点,零额外开销
删除需两次 free删除节点即操作数据,一次搞定
遍历需解引用 data 指针直接通过 container_of 获取宿主结构体
内存不连续,缓存不友好数据和节点在一起,缓存友好
代码冗余,类型不安全类型安全(借助 typeof + 宏)

最大优势:通用性 + 零开销抽象 + 高性能


🧩 6. 高级技巧:一个结构体嵌入多个 node_t

struct task {char name[32];int priority;struct node_t run_queue;   // 在运行队列中struct node_t wait_queue;  // 在等待队列中struct node_t all_tasks;   // 在全局任务链表中
};

你可以把同一个 struct task 挂到多个链表中:

struct task *t = my_container_of(pos, struct task, run_queue);
struct task *t2 = my_container_of(pos2, struct task, wait_queue);

💡 同一个对象,多视角管理 —— 这是内核调度器、设备模型等大量使用的设计!


🆚 四、三种访问宿主结构体的方法对比

✅ 在 C 语言中,通过链表节点访问宿主结构体主要有三种方法:


✅ 三种方法概览

方法名称描述
方法一节点置于结构体开头(强转法)node_t 作为结构体第一个成员,直接强转指针
方法二使用 container_of通过成员偏移量反推结构体起始地址(Linux 内核标准)
方法三节点中保存 void *containernode_t 中显式保存指向宿主结构体的指针(非侵入式)

🧭 方法一:节点置于结构体开头(强转法)

✍️ 实现方式

struct student {struct node_t node;  // 必须是第一个成员!int id;char name[32];
};// 遍历时直接强转:
struct node_t *pos = ...;
struct student *stu = (struct student *)pos;  // 地址相同

✅ 优点

  • 零开销:无需计算偏移,直接强转。
  • 简单直观:代码易懂。
  • 缓存友好:节点和数据在一起。

❌ 缺点

  • 脆弱:一旦 node_t 不是第一个成员,程序崩溃。
  • 无类型检查:强转易出错。
  • 扩展性差:无法同时挂多个链表(除非用 union)。

📉 适用场景

  • 简单项目、学习用途
  • 对性能要求极高,且结构体布局严格可控

🧭 方法二:使用 container_of 宏(推荐)

✍️ 实现方式

struct student {int id;char name[32];struct node_t list;  // 可放任意位置
};struct node_t *pos = ...;
struct student *stu = my_container_of(pos, struct student, list);

✅ 优点

  • 位置无关node_t 可放任意位置。
  • 类型安全:宏内 typeof 编译期检查。
  • 高扩展性:可嵌入多个 node_t,挂多个链表。
  • 零运行时开销:偏移量是编译期常量。
  • 缓存友好:数据和节点在一起。

❌ 缺点

  • 理解门槛高:需理解指针运算和宏。
  • 调试稍复杂:对新手不友好。

📈 适用场景

  • Linux 内核、驱动开发
  • 高性能系统级程序
  • 需要一个对象挂多个链表的场景

工业级推荐方案


🧭 方法三:节点中保存 void *container(非侵入式)

✍️ 实现方式

struct node_t {void *container;     // 显式指向宿主结构体struct node_t *next;
};struct student {int id;char name[32];
};// 使用:
struct node_t *node = malloc(sizeof(struct node_t));
node->container = stu;// 访问:
struct student *stu = (struct student *)node->container;

✅ 优点

  • 简单直观:逻辑清晰。
  • 结构体无需修改:侵入性低。

❌ 缺点

  • 类型不安全void* 无编译检查 → 运行时崩溃。
  • 内存开销大:每个节点多 8 字节指针(64位)。
  • 内存碎片:数据与节点分离 → 缓存不友好。
  • 管理复杂:需分别释放节点和数据 → 易泄漏。
  • 扩展性差:挂多个链表需多个指针。

📉 适用场景

  • 快速原型、教学示例
  • 宿主结构体不可修改
  • 对性能要求不高

🆚 综合对比表

特性方法一:节点在开头(强转)方法二:container_of(推荐)方法三:void *container(非侵入)
性能⭐⭐⭐⭐⭐(最快)⭐⭐⭐⭐⭐(编译期偏移)⭐⭐⭐(多一次指针跳转)
内存开销0 额外开销0 额外开销+8 字节/节点(64 位)
缓存友好度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐(数据与节点分离)
类型安全❌(强转无检查)✅(宏内 typeof 检查)❌(void* 无检查)
成员位置要求❗必须第一个成员✅ 任意位置✅ 任意位置
多链表支持❌(除非用 union)✅(嵌入多个 node_t)❌(需多个 container 指针)
内存管理复杂度✅(节点即数据)✅(节点即数据)❌(需分别释放)
适用场景简单、高性能、可控系统编程、内核、工业级项目教学、原型、不可修改结构体

🧠 举个形象的例子:

想象你要管理一群“士兵”,每个士兵要同时出现在“步兵队列”和“射击训练队列”。

  • 方法一:士兵必须“站在队列最前面”才能被识别 → 不现实。
  • 方法二:士兵身上别着“步兵徽章”和“射击徽章”,通过徽章位置能找到本人 → 灵活、安全、高效 ✅
  • 方法三:每个队列里放一张“纸条”写士兵住址 → 要跑腿去家里找人,还可能写错地址 → 低效、易错 ❌

✅ 最终建议:

你的情况推荐方法
学习 / 教学 / 快速原型方法三(简单)
性能关键 + 结构体可控 + 单链表方法一(极致性能)
工业级项目 / 多链表 / 安全性方法二(container_of)✅

💡 Linux 内核选择方法二,不是偶然 —— 它在性能、安全、灵活性上达到了最佳平衡。


🎁 Bonus:方法二的现代演进

  • C++boost::intrusive::list
  • Rust:通过 Pin + 自引用实现安全侵入式链表
  • Zig@fieldParentPtr 内置函数,功能类似 container_of

说明这种设计思想是跨语言、跨时代的系统编程最佳实践


✅ 总结的三种方法非常精准,理解到这个层次,你已经超越了 90% 的 C 程序员!


文章转载自:

http://l4yYeVhZ.mLnbd.cn
http://7Y7S9fG3.mLnbd.cn
http://v1Om47Ck.mLnbd.cn
http://c4W7DF3G.mLnbd.cn
http://3Z93jhr9.mLnbd.cn
http://bpxjcpNq.mLnbd.cn
http://AEKDmnz5.mLnbd.cn
http://evEmdVYO.mLnbd.cn
http://DjEf2XXy.mLnbd.cn
http://lDfacqPp.mLnbd.cn
http://kvnNvB2G.mLnbd.cn
http://ZyzpMWXW.mLnbd.cn
http://q2fT3qWj.mLnbd.cn
http://wL43kgRN.mLnbd.cn
http://JX3Fx0ws.mLnbd.cn
http://scKVJMzK.mLnbd.cn
http://yrCgvli6.mLnbd.cn
http://xPotvPL6.mLnbd.cn
http://1O9nI62G.mLnbd.cn
http://Zt0hVodi.mLnbd.cn
http://PAdxePD0.mLnbd.cn
http://lZGLSYFN.mLnbd.cn
http://MpruBTmJ.mLnbd.cn
http://47aq5z71.mLnbd.cn
http://g7VnfbAR.mLnbd.cn
http://LzI8ZU5W.mLnbd.cn
http://EmS1llwd.mLnbd.cn
http://1P2S15IL.mLnbd.cn
http://4zOl8gNj.mLnbd.cn
http://NM4BptyJ.mLnbd.cn
http://www.dtcms.com/a/378994.html

相关文章:

  • Dinky 是一个开箱即用的一站式实时计算平台
  • Vue3内置组件Teleport/Suspense
  • Python打印格式化完全指南:掌握分隔符与行结尾符的高级应用
  • 实体不相互完全裁剪,请检查您的输入
  • 分数阶傅里叶变换(FRFT)的MATLAB实现
  • ARM (6) - I.MX6ULL 汇编点灯迁移至 C 语言 + SDK 移植与 BSP 工程搭建
  • unsloth微调gemma3图文代码简析
  • 【ECharts ✨】ECharts 自适应图表布局:适配不同屏幕尺寸,提升用户体验!
  • wpf依赖注入驱动的 MVVM实现(含免费源代码demo)
  • Python的f格式
  • 技术视界 | 末端执行器:机器人的“手”,如何赋予机器以生命?
  • 从零开始使用 axum-server 构建 HTTP/HTTPS 服务
  • 简直有毒!索伯ACL撕裂,雷霆四年报销三个新秀!
  • 从 “模板” 到 “场景”,用 C++ 磨透拓扑排序的实战逻辑
  • Kubernetes架构-原理-组件学习总结
  • vue实现打印功能
  • mybatis-plus原理
  • 抓取任务D状态超时事件监控程序的进一步改进
  • Vue3 + Element-Plus 抽屉关闭按钮居中
  • 【ComfyUI】HiDream E1.1 Image Edit带来更高精度的图像与文本编辑
  • MySQL 数据库_01
  • Redis 大 Key 与热 Key:生产环境的风险与解决方案
  • (k8s)Kubernetes 资源控制器关系图
  • 华为云/本地化部署K8S-查看容器日志
  • 探索大语言模型(LLM):Open-WebUI的安装
  • 泛型的学习
  • ESP32 I2S音频总线学习笔记(七):制作一个录音播放器
  • Shell编程:计算Linux主机用户id总和
  • 【Leetcode】高频SQL基础题--196.删除重复的电子邮箱
  • SpreadJS V18.0 Update2 重磅发布:实时协作、视觉定制与效率升级