linux内核之offset_of和container_of理解

无意间在腾讯课堂上看到有个老师讲解linux内核链表,开始就讲这两个宏。
这篇文章主要是为了记录对这两个宏的使用和理解。

测试环境:

  • win10 64bit 家庭版
  • gcc version 6.3.0 (MinGW.org GCC-6.3.0-1)

为了描述方便, 示例代码中会使用这样的结构:

typedef struct Node {
    double d;
    int i;
} Node;

下面正式开始

宏定义概述

offsetof:
宏定义:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

功能: 获取结构体中成员变量MEMBER相对于结构体的地址偏移量

container_of:
宏定义:

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

功能: 获取结构体成员所在的结构体变量的地址

为了有一个比较深刻的理解, 用代码逐步演示.

offsetof

  • (TYPE *)0理解
    这写法比较少见, 因为平时写得最多的可能是这样:

    #include <stdio.h>
    
    int main()
    {
        // 地址:低 ---------------> 高
        // 小端存储(16进制):  45, 44, 43, 42
        int i = 0x42434445;
    
        int* pi = &i;
        char* pc = (char*)pi; // 类型转换
    
        // 61ff24, 61ff24
        printf("%x, %x\n", pi, pc);
    
        // E, D, C, B
        printf("%c, %c, %c, %c\n", *pc, *(pc + 1), *(pc + 2), *(pc + 3));
        return 0;
    }
    

    上面(char*)pi是将一个int*转成char*, 什么意思呢?
    就是将pi处开始的内存用char类型来解释, i变量占了4个字节的内存,
    其值按小端存储分别为(16进制): 45, 44, 43, 42, 此时pc的值就是45所在的内存地址,
    即*pc的值为'E'(ASCII为45), 从运行结果也可以看出.

    再来看(TYPE *)0, 假想0是某一个int类型指针p的值, 这不就是将p转换成TYPE类型的指针吗?
    有人可能说, 0地址处不是操作系统占用了吗? 这样会不会报错?
    不会, 因为只做了转换, 并没有向0地址处进行读写操作.
    那么, (TYPE *)0相当于0地址处存了一个TYPE类型的结构体, &((TYPE *)0)->MEMBER就是求MEMBER成员的地址.
    再回头看看offsetof宏, 就是为了求出变量MEMBER相对于结构体的地址偏移量.

  • offsetof使用示例:

    Node n = {
        .d = 3.14,
        .i = 6,
    };
    
    Node* pNode = NULL;
    // Node中: d在第一个, i在d后面定义, 成员i的偏移为一个double的长度, 
    assert(offsetof(Node, i) == sizeof(double));
    // d定义在Node中第一个位置, 所以偏移为0
    assert(offsetof(Node, d) == 0);
    assert(&n.i == (char*)&n + sizeof(double));
    // 事实上, 由于成员d偏移为0, 所以d的地址和n的地址一样
    assert(&n.d == &n);
    // i的地址偏移其实就是一个double
    assert(&n.i == (char*)&n + sizeof(double));
    

这个offsetof好像没啥用啊? 平时不用它也工作得很好啊.
其实它是为了container_of服务的, 直接用它的情况确实很少.

container_of

  • container_of(ptr, type, member)
    先说说三个参数:

    • ptr: 结构体变量的指针(比如上面的 &n.i)
    • type: 结构体类型(如Node)
    • member: 结构体成员名(如i)
  • typeof 关键字
    typeof是GNUC的扩展语法, 用它可以获取变量的类型, 如:

    int j = 0;
    typeof(j) j2 = 1;
    printf("%d\n", j2); // 1
    
  • ({})
    container_of定义中({stmt1; stmt2;})是GNUC的扩展语法, 其值是最后一个语句的运算结果. 如:

    int a = ({
        int i = 10;
        int j = 100;
        i > j ? i : j;
    });
    printf("%d\n", a); // 100
    
  • container_of实现原理

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

    其中 const typeof( ((type *)0)->member ) *__mptr = (ptr);是为了做类型转换, 暂时不管, 后面会说到.
    因为正常情况下我们给第一个参数传递的就是&n.i这样值, 也就是对应成员变量的指针.
    那么container_of最后的结果其实就相当于:
    (type *)( (char *)ptr- offsetof(type,member) );
    用成员变量的地址, 减去这个成员变量相对于其所在结构体的地址偏移, 不就是其所在结构体变量的地址吗?
    看代码更好懂:

    Node n = {
        .d = 3.14,
        .i = 6,
    };
    
    Node* pNode = NULL;
    // 通过n.i的地址反推n的地址
    pNode = container_of(&n.i, Node, i);
    assert(pNode == &n);
    assert(pNode->i == 6);
    

嗯! container_of其实也好简单嘛. 但是有些细节还是要说下.

  • 感觉没用的第一句(const typeof... = (ptr))
    对于const typeof( ((type *)0)->member ) *__mptr = (ptr); 这个在正常使用时好像看不出来有什么用.
    其实它的作用时,在编译时就能知道ptr与member的类型是否匹配.

    char tmp = 'a';
    char* ptmp = &tmp;
    pNode = container_of(ptmp, Node, i);
    // assert(pNode == &n); // assert failed
    

    这样在编译时会产生警告:

    warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]|

    当然此时计算的pNode的值与&n不相等.

  • 为啥是(char *)
    有人会问(包括我)为啥(char *)__mptr - offsetof(type,member)这里要用char*强转一下.
    想像一下, 如果__mptr此时是int*, 然后offsetof(type,member)的值为4, 那么就相当于回退了16, 而预期的是回退4.

    由于我的测试环境中, 一个int占4个字节, 一个double占8个字节, 则i相对于Node的偏移为8,
    所以&n.i - 2相当于i的地址值减8.

    assert(&n.i - 2 == &n); // success
    assert(&n.i - sizeof(double) == &n);  // failed
    

    所以char*强转是为了保证回退时步长为1. 可以将原宏中的(char *)__mptr - offsetof(type,member)改成__mptr - offsetof(type,member)试试就知道了.

注意

  • offsetof也提醒我们, struct一旦定义, 成员变量位置就不要乱动了, 可能会引起不必要的问题.
  • 内存对齐问题也要注意

参考:

https://ke.qq.com/course/223662#term_id=100264105
https://blog.csdn.net/npy_lp/article/details/7010752

欢迎补充指正.

  • 2
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值