曾经写过一篇关《Linux数据结构之链表》的文章,在这篇文章中,只是简单的分析了如何通过链表内元素的指针获取链表的指针,并没有深究。关于这个contain_of宏,我有一些想法和疑问,这也是我写这篇文章的原因。

       首先简单谈一下它的原理:在C语言中,结构体的成员在内存中所处的地址是一整块的,container_of宏通过指向成员的指针以及成员相对于结构体开始地址的偏移大小来计算出该结构体的指针。

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

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

       当我分析到({const typeof(((type *)0)->member) *__ptr = (ptr)这行代码时,觉得可以将其“优化”一下,不需要用typeof(((type *)0)->member),直接可以通过求ptr指向的类型来定义一个指针。那么就可以将container_of宏改写为:

#define container_of_2(ptr, type, member) \
        ({typeof(*(ptr)) * __ptr = (ptr); \
         (type *)((char *) __ptr - offsetof(type, member));})

       当分析到(type *)((char *) __ptr - offsetof(type, member))这行代码时,让我似乎感觉第一步定义的指针似乎不需要,于是最终将container_of宏“优化”成如下。

#define container_of_1(ptr, type, member) \
        ((type *)((char *)(ptr) - offsetof(type, member)))

       为了验证我的“优化”是否可行,于是编写了如下的测试程序。

struct al_list_head{
    struct al_list_head *next, *prev;
};

struct al_node{
        int count;
        int flag;
        double len;
        char c;
        struct al_list_head node;
        int value;
};

int main()
{
        struct al_node mynode = {
                .count = 1,
                .flag = 1,
                .len = 1.1,
                .c = 'A',
                .node.next = NULL,
                .node.prev = NULL,
                .value = 10
        };

        struct al_list_head *ptr = &mynode.node;
        struct al_node *p = container_of(ptr, struct al_node, node);
        struct al_node *p1 = container_of_1(ptr, struct al_node, node);
        struct al_node *p2 = container_of_2(ptr, struct al_node, node);
        
        printf("value = %d\n",p->value);
        printf("value = %f\n",p1->len);
        printf("value = %c\n",p2->c);

        return 0;
}

经过测试,可以正常的运行。

       为了进一步分析上述main函数里面的操作步骤,通过gcc将其转化成汇编,这样可以清晰的看出底层的具体操作过程。

;main函数的部分汇编代码
;初始化
movl	$1, 48(%esp)
movl	$1, 52(%esp)
fldl	.LC0
fstpl	56(%esp)
movb	$65, 64(%esp)
movl	$0, 68(%esp)
movl	$0, 72(%esp)
movl	$10, 76(%esp)

;ptr=&mynode.node
leal	48(%esp), %eax
addl	$20, %eax
movl	%eax, 24(%esp)

;p = container_of(ptr, struct al_node, node);
movl	24(%esp), %eax
movl	%eax, 28(%esp)
movl	28(%esp), %eax	
subl	$20, %eax
movl	%eax, 32(%esp)

;p1 = container_of_1(ptr, struct al_node, node);
movl	24(%esp), %eax
subl	$20, %eax
movl	%eax, 36(%esp)

;p2 = container_of_2(ptr, struct al_node, node);
movl	24(%esp), %eax
movl	%eax, 40(%esp)
movl	40(%esp), %eax
subl	$20, %eax
movl	%eax, 44(%esp)

       通过汇编代码,可以很明显的看出container_ofcontainer_of_2在汇编层面上是没有任何区别的。而container_of_1宏是要比container_of宏少一步操作,也减少了一个变量。

       分析到这,我是有一些疑问的。当我阅读Linux内核的一些源码时,发现其实有些是多余的操作,为什么没有进行优化?另外,我写的container_of_1宏是否真的是一种不错的选择?container_of_1宏是一个很简单的宏,我相信那些Linux内核编写者肯定是可以想出来的,他们使用container_of宏也会有他们的道理的。但这些问题,我还没有能力做出合理的解释,这也是我接下来要学习和探索的地方。