一种神奇的双向循环链表C语言实现

最近在看ucore操作系统的实验指导。里面提要一个双向循环链表的数据结构,挺有意思的。

其实这个数据结构本身并不复杂。在普通链表的基础上加一个前向指针,我们就得到了双向链表,再把头尾节点连起来就是双向循环链表了。一般的实现方式如下:

typedef struct link_node {
ele_type element;
link_node *prev, *next;
} link_node; ... // 相关的操作比较简单,这里就不实现了

但是这样有一定的局限性,就是里面的数据域(element)是固定类型的,如果有很多种成员变量都需要这种链表结构,就免不了重复编码。

记得之前看redis代码的时候也有这种类似用C语言实现多态的情况,那里面是用一个void *ptr的指针来指向具体的底层实现。具体实现如下:

typedef struct link_node_r {
void *ptr
link_node_r *prev, *next;
} link_node_r;

然后,在真正使用的时候把这个类型强转一下。


而ucore里面采用了一种不同的实现方式。实现如下:

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

可以看到,这个链表里面并没有数据域。是的你没有看错,这个数据结构里没有数据域!可是如果没有数据域的话,这个数据结构有什么实用价值呢?这里是把链表域放到需要使用链表的具体结构了,算是逆向思维吧。 具体实现如下:

/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
struct Page {
atomic_t ref; // page frame's reference counter
……
list_entry_t page_link; // free list link
};

可以看到,这里是需要使用双向循环链表的数据类型应用了list_entry_t结构,这样子,链表的结构和操作就不需要变了。

但是,这里有一个问题。当我们查找到链表中某一个节点是,怎么获取到其宿主结构,也就是说,要怎么拿到这个对象的其他部分。正常思维都是从一个结构里拿到它的子结构xx->xxx或者xx.xxx。这里我们要用到一个le2page宏。

le2page宏的使用相当简单:

// convert list entry to page
#define le2page(le, member) \
to_struct((le), struct Page, member)

而相比之下,它的实现用到的to_struct宏和offsetof宏则有一些难懂:

/* Return the offset of 'member' relative to the beginning of a struct type */
#define offsetof(type, member) \
((size_t)(&((type *)0)->member)) /* *
* to_struct - get the struct from a ptr
* @ptr: a struct pointer of member
* @type: the type of the struct this is embedded in
* @member: the name of the member within the struct
* */
#define to_struct(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

这里采用了一个利用gcc编译器技术的技巧,即先求得数据结构的成员变量在本宿主数据结构中的偏移量,然后根据成员变量的地址反过来得出属主数据结构的变量的地址。

我们首先来看offsetof宏,size_t最终定义与CPU体系结构相关,本实验都采用Intel X86-32 CPU,顾szie_t等价于 unsigned int。 ((type *)0)->member的设计含义是什么?其实这是为了求得数据结构的成员变量在本宿主数据结构中的偏移量。为了达到这个目标,首先将0地址强制"转换"为type数据结构(比如struct Page)的指针,再访问到type数据结构中的member成员(比如page_link)的地址,即是type数据结构中member成员相对于数据结构变量的偏移量。在offsetof宏中,这个member成员的地址(即“&((type *)0)->member)”)实际上就是type数据结构中member成员相对于数据结构变量的偏移量。对于给定一个结构,offsetof(type,member)是一个常量,to_struct宏正是利用这个不变的偏移量来求得链表数据项的变量地址。接下来再分析一下to_struct宏,可以发现 to_struct宏中用到的ptr变量是链表节点的地址,把它减去offsetof宏所获得的数据结构内偏移量,即就得到了包含链表节点的属主数据结构的变量的地址。

上一篇:1.Go语言copy函数、sort排序、双向链表、list操作和双向循环链表


下一篇:c语言编程之双向循环链表