Linux内核中的container_of宏详解

🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习

🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发

❄️作者主页:一个平凡而乐于分享的小比特的个人主页

✨收录专栏:Linux,本专栏目的在于,记录学习Linux操作系统的总结

欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖

Linux内核中的container_of宏详解

1. 什么是container_of宏?

1.1 基本概念

container_of是Linux内核中一个非常巧妙且常用的宏,它的作用是通过结构体成员的地址反向推导出包含该成员的结构体的地址。

1.2 类比理解

想象一下这样的场景:

  • 你只知道一栋大楼(结构体)里某个房间(成员变量)的具体位置
  • 你需要找出这栋大楼的入口地址(结构体起始地址)
  • container_of就是这个"导航工具"

2. 为什么需要container_of

2.1 问题背景

在Linux内核编程中,经常使用链表来管理各种数据结构。一个典型的设计模式是:

c 复制代码
// 示例:内核链表节点结构
struct list_head {
    struct list_head *next, *prev;
};

// 业务数据结构
struct my_struct {
    int data;
    struct list_head list;  // 嵌入的链表节点
};

问题 :当我们遍历链表时,只能拿到list_head的指针,如何获取包含它的my_struct结构体呢?

2.2 对比表格:传统方法 vs container_of

特性 传统方法(直接访问) container_of方法
数据组织 结构体包含指针指向数据 数据包含链表节点
内存效率 需要额外指针,效率低 内存紧凑,效率高
代码复杂度 简单直观 需要宏包装,但复用性高
典型场景 普通应用编程 内核、嵌入式等系统编程
可维护性 修改时需要同步多处 结构体修改影响小

3. container_of的原型与实现

3.1 宏定义

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

或者更简洁的版本:

c 复制代码
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

3.2 参数解释

复制代码
参数说明表格:
┌─────────┬─────────────────────────────────────────┐
│ 参数    │ 说明                                    │
├─────────┼─────────────────────────────────────────┤
│ ptr     │ 结构体成员的指针                        │
│ type    │ 包含该成员的结构体类型                  │
│ member  │ 成员在结构体中的名称                    │
└─────────┴─────────────────────────────────────────┘

4. 实现机制逐步解析

4.1 核心原理图示

复制代码
内存布局示意图:
┌─────────────────────────────────────────────┐
│            struct container                 │
│  ┌──────────────────────────────────────┐  │
│  │ 成员1                                │  │
│  ├──────────────────────────────────────┤  │
│  │ 成员2                                │  │
│  ├──────────────────────────────────────┤  │
│  │ ...                                  │  │
│  ├──────────────────────────────────────┤  │
│  │ member (我们已知它的地址: ptr)       │←─┘ 已知点
│  │                                      │
│  ├──────────────────────────────────────┤
│  │ 其他成员...                          │
│  └──────────────────────────────────────┘
└─────────────────────────────────────────────┘
↑
container_of计算出的结构体起始地址

4.2 关键步骤拆解

步骤1:计算成员偏移量(offsetof)
c 复制代码
// offsetof宏计算成员在结构体中的偏移
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)

工作原理

  1. (TYPE *)0:将0地址强制转换为TYPE类型指针
  2. &((TYPE *)0)->MEMBER:获取成员在"假想"结构体中的地址
  3. 因为结构体起始地址为0,成员的地址就是它的偏移量

示例计算

c 复制代码
struct example {
    int a;      // 偏移量:0
    char b;     // 偏移量:4(假设int为4字节)
    double c;   // 偏移量:8(考虑内存对齐)
};

// offsetof(struct example, c) = 8
步骤2:反向计算结构体地址
复制代码
计算公式:
结构体地址 = 成员地址 - 成员偏移量
           ↓
container = ptr - offsetof(type, member)

4.3 完整计算示例

c 复制代码
// 实际数据结构
struct person {
    int age;
    char name[20];
    struct list_head node;  // 链表节点
};

// 假设我们知道node的地址
struct list_head *node_ptr = &some_person.node;

// 使用container_of
struct person *person_ptr = container_of(node_ptr, struct person, node);

// 展开计算过程:
// 1. offsetof(struct person, node) = 
//    (age:4字节 + name:20字节) = 24字节(假设对齐)
// 2. 结构体地址 = node_ptr - 24

5. 图解计算过程

复制代码
内存地址可视化:
地址       内容                        说明
0x1000     [age]                      person结构体开始
0x1004     [name[0..19]]              name字段
0x1018     [node.next]                ← node成员开始
0x1020     [node.prev]                我们已知这里的地址:ptr=0x1018
          
计算过程:
ptr = 0x1018 (node的地址)
offset = 0x18 (24字节,node在person中的偏移)
person地址 = 0x1018 - 0x18 = 0x1000 ✓

6. 实际应用示例

6.1 链表遍历示例

c 复制代码
#include <stdio.h>
#include <stddef.h>

// 简化版offsetof和container_of
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

// 链表节点
struct list_head {
    struct list_head *next;
};

// 业务数据
struct task {
    int pid;
    int priority;
    struct list_head list;  // 嵌入的链表节点
};

int main() {
    // 创建一些任务
    struct task task1 = {.pid = 100, .priority = 1};
    struct task task2 = {.pid = 101, .priority = 2};
    
    // 链接它们(简化版)
    task1.list.next = &task2.list;
    
    // 遍历链表:从list_head获取完整的task结构
    struct list_head *current = &task1.list;
    
    while (current != NULL) {
        // 关键步骤:通过list_head找到包含它的task
        struct task *task_ptr = container_of(current, struct task, list);
        
        printf("Found task: pid=%d, priority=%d\n", 
               task_ptr->pid, task_ptr->priority);
        
        current = current->next;
    }
    
    return 0;
}

6.2 内核中的实际使用

c 复制代码
// Linux内核中的真实示例(简化)
struct inode {
    // ... 各种字段 ...
    struct hlist_node i_hash;  // 哈希链表节点
};

// 在哈希表中查找inode
struct hlist_node *node = ...;  // 从哈希表获取的节点

// 通过节点找到inode
struct inode *inode = container_of(node, struct inode, i_hash);

7. 技术细节与注意事项

7.1 类型安全版本

c 复制代码
// 内核中的实际实现包含类型检查
#define container_of(ptr, type, member) ({              \
    void *__mptr = (void *)(ptr);                       \
    BUILD_BUG_ON(!__same_type(*(ptr), ((type *)0)->member) && \
                 !__same_type(*(ptr), void));           \
    ((type *)(__mptr - offsetof(type, member))); })

7.2 内存对齐考虑

复制代码
内存对齐示例:
struct aligned_example {
    char a;      // 地址:0
    // 填充3字节(假设int需要4字节对齐)
    int b;       // 地址:4
    short c;     // 地址:8
};

offsetof(struct aligned_example, c) = 8
而不是 1 + 4 = 5

7.3 优势总结

优势 说明
类型安全 编译时检查类型匹配
高效 编译时计算偏移,运行时只有减法操作
通用 适用于任何结构体和成员类型
内存友好 不需要为链表单独分配节点内存

8. 常见问题解答

Q1: 为什么用char*指针做减法?

A: char*指针的加减法以字节为单位,而其他类型指针的加减法以类型大小为步长。

Q2: 这个宏安全吗?

A: 如果正确使用是安全的,但需要确保:

  1. ptr确实指向type结构体中的member
  2. member确实是type的成员

Q3: 在用户空间可以使用吗?

A: 可以,但需要自己实现或使用<stddef.h>中的offsetof

9. 扩展应用

9.1 嵌套结构体

c 复制代码
struct inner {
    int data;
};

struct outer {
    char tag;
    struct inner embed;
    float value;
};

struct inner *inner_ptr = &some_outer.embed;
struct outer *outer_ptr = container_of(inner_ptr, struct outer, embed);

9.2 多个链表节点

c 复制代码
struct complex_struct {
    int id;
    struct list_head hash_node;   // 用于哈希表
    struct list_head lru_node;    // 用于LRU缓存
    struct list_head free_node;   // 用于空闲列表
};
// 同一个结构体可以同时存在于多个链表中

总结

container_of宏是Linux内核中嵌入式数据结构模式的核心工具,它体现了C语言指针运算的强大能力。通过巧妙的偏移量计算,实现了从部分到整体的逆向查找,是内核高效链表实现的基础。

核心要点记忆

  1. 已知:成员地址 + 成员名 + 结构体类型
  2. 计算:偏移量 = offsetof(type, member)
  3. 结果:结构体地址 = 成员地址 - 偏移量
  4. 关键:所有计算都在编译时或简单的运行时完成,效率极高

这种设计模式不仅在Linux内核中广泛应用,在许多嵌入式系统、高性能服务器和底层库中都有重要应用。

相关推荐
lcreek8 小时前
Linux信号机制详解:阻塞信号集与未决信号集
linux·操作系统·系统编程
shandianchengzi9 小时前
【记录】Tailscale|部署 Tailscale 到 linux 主机或 Docker 上
linux·运维·docker·tailscale
John Song9 小时前
Linux机器怎么查看进程内存占用情况
linux·运维·chrome
sichuanwuyi9 小时前
Wydevops工具的价值分析
linux·微服务·架构·kubernetes·jenkins
持戒波罗蜜10 小时前
ubuntu20解决intel wifi 驱动问题
linux·驱动开发·嵌入式硬件·ubuntu
不做无法实现的梦~10 小时前
使用ros2来跑通mid360的驱动包
linux·嵌入式硬件·机器人·自动驾驶
点云SLAM10 小时前
C++内存泄漏检测之Windows 专用工具(CRT Debug、Dr.Memory)和Linux 专业工具(ASan 、heaptrack)
linux·c++·windows·asan·dr.memory·c++内存泄漏检测·c++内存管理
LuiChun10 小时前
Docker Compose 容器服务查询与文件查看操作指南(Windows Docker Desktop 版)【一】
linux·运维·windows·docker·容器
${王小剑}11 小时前
在离线ubuntu上布置深度学习环境
linux·运维·ubuntu