1.在很多OS里面几乎都会用到链表 , 一般的链表的节点(结构体)都维护着一个list(结构体) , 如果是单向链表 , 则list里面只有一个元素next指针 , 指向下一个节点的list .如果是双向链表 , 则list里面由两个元素:previous指针和next指针 ,其中previous指向上一个节点的list , next指向下一个节点的list .
下面我画出了双向链表的典型结构:
2.由以上的结构可以发现组成链表的结构使用的node里面的list .那问题就出现了 , 如何才能从获得的list结构来得到相应的node指针呢 ?
3.分析
这里我们先做一些声明 , 下面的讨论都用listAddr来表示list的地址 , nodeAddr来表示node的地址.
1)既然我们知道了listAddr了, 如果要得到对应的nodeAddr, 很简单的公式如下:
listAddr= nodeAddr + offset ==> nodeAddr = listAddr - offset ;
2)其中 listAddr 已知 , 我们只需要求出offset 就行了;
3)offset 如何求呢 ? linux里面有关于获得这个offset的方法 , 也是很多程序都采用的方法 . 这里也根据linux里面的方法来分析;
记得以前的上数学课的时候 , 老师跟我们说过 , 在做选择题的时候某些题是可以直接把一些变量用简单的常数代进去求解得到我们想要的结果 . 这个方法其实也可以用在这里 . 我们知道一个已知结构体的起始地址跟该结构体里面的某个成员的偏移量是不会变的 , 即 offset是个常数 . 也就是说无论node地址取什么值 ,offset都是不变 . 这样我们就可以用一个常数强制转换为node指针(这样nodeAddr地址就知道了) , 然后根据listAddr-nodeAddr 推出 offset .
(3)offset知道了 , 由listAdd - offset 得真正的nodeAddr .
4.以下程序及打印结果验证了这个思路是可行的:
打印结构如下:
5.既然验证了这个思路是可行的 , 那么就得来设计一个已知结构体类型 ,其中一个元素名称,该元素的地址 的函数或者宏定义 . 但是函数实现太不现实了 , 我想不出有什么办法可以通过以上3个已知条件来返回这个类型的结构体指针 . 所以只能通过宏定义来实现了 .
2)宏定义的实现:
1>宏定义名称 : getStructAddr(struct_type , member_name ,member_ptr)
其中struct_type 是结构体类型 ; member_name 是成员名称 ; member_ptr 是成员地址 .
2>宏内容 (可以有两种形式,如下):
a.使用({})形式的宏内容 , 这种方式可以直接在宏调用时作为赋值操作的右值 , 但想要的返回值必须是最后一条语句的运行结果 .
实现如下;
打印结果如下:
b.使用do{...}while(0)形式的宏内容 , 这种方式不可以在宏调用时作为赋值操作的右值 , 所以只能通过给宏多加一个参数用于存放需要返回的值 .
实现如下:
打印结果如下:
以下是linux3.0.1内核include/linux/kernel.h里的一段宏定义:
638/**
639 * container_of - cast a memberof a structure out to the containing structure
640 * @ptr: the pointer to the member.
641 * @type: the type of the container struct this isembedded in.
642 * @member: the name of the member within the struct.
643 *
644 */
645#definecontainer_of(ptr,type,member) ({ \
646 const typeof( ((type *)0)->member ) *__mptr = (ptr); \
647 (type *)( (char *)__mptr - offsetof(type,member) );})
里面有个地方搞不明白的就是linux里面为什么要搞得这么复杂 , 定义一个指向只读变量指针__mptr指向ptr所指的内容(就是member) ,估计是怕在这个宏里面更改到ptr里的内容才这样做把 . 关于(char *)__mptr是为了使每该指针加减1都对应一个字节跨度 . 因为offsetiof宏取的偏移量是以字节为单位的.