【linux内核中的双向链表-01】list_head、list_entry

内核版本:2.6.34
OS:x86_64

一、list_head

list_head本质上是一个双向链表,被广泛应用于各类数据结构的管理和维护。而系统中各个进程的描述符task_struct也是通过list_head进行管理的。内核中关于list_head的定义都在include/linux/list.h中。

1. list_head的定义:

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

很简单,包含两个指针变量,*next指向后驱节点,*prev指向前驱节点。

2. list_head初始化API

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

方法一:直接通过LIST_HEAD()来创建一个名为name的链表并完成初始化(next和prev都指向自己)

LIST_HEAD(node);

// 通过打印发现三个地址是一样的
printf("&node = %#x\n", &node);
printf("node.prev = %#x\n", node.prev);
printf("node.next = %#x\n", node.next);

方法二:

  • 先定义一个list_head;
  • 再通过INIT_LIST_HEAD()完成初始化(同样还是next和prev都指向自己)
struct list_head list;
INIT_LIST_HEAD(&list);

// 通过打印发现三个地址是一样的
printf("list = %#x\n", &list);
printf("list.prev = %#x\n", list.prev);
printf("list.next = %#x\n", list.next);

3. 对list_head进行操作的API

1)添加节点:list_add、list_add_tail

static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}

// 在链表头head节点后面插入一个新的节点new
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

// 在链表尾head->prev节点后面插入一个新的节点new
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

2)删除指定节点:list_del

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	prev->next = next;
}

static inline void list_del(struct list_head *entry)
{
	/* 将待删除节点的前驱节点prev和后驱节点next传入__list_del
	   __list_del会将prev节点和next节点连起来,即丢弃了待删除节点。  */ 
	__list_del(entry->prev, entry->next);
	
	/* 对被删除节点进行清除操作,两个宏代表防止被使用,这样就不会被访问到了。 */
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

3)其它API原理类似,不再赘述

二、普通的双向链表DNode

我们在学习C语言的双链表DNode的时候,接触到的双链表实际上和list_head是有差别的。差别就是一般DNode除了有前驱节点指针、后驱节点指针,还有数据域data。下面是一个常用的DNode定义。

struct DNode{
    struct DNode *prev;
    int data;
    struct DNode *next;
};

其使用方法是通过遍历链表找到节点,然后访问其中的数据data。即数据域是嵌入在链表节点中的。
而list_head是一种侵入式链表,没有数据域,这样的好处是不用再关心数据域的类型、结构等等,变得更加通用。list_head是嵌入到结构体内部的,比如大名鼎鼎的进程描述符task_struct中就嵌入了多个list_head。

// include/linux/sched.h
struct task_struct {
	volatile long state;
	void *stack;
	......
	struct list_head rcu_node_entry;
	struct list_head tasks;
	struct list_head children;
	struct list_head sibling;
	......
}

那么怎么通过list_head,访问其所在结构体的其它数据域呢?例如:拿到了某个进程的tasks节点,怎么访问该进程的state值呢?这就需要通过结构体成员地址,找到结构体首地址,转成结构体指针,再去访问其它成员。内核中的API为:list_entry()。

三、list_entry

// include/linux/stddef.h
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif

// include/linux/kernel.h
#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

// include/linux/list.h
#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

list_entry(ptr, type, member)参数说明:

  • ptr:结构体中member成员的地址;
  • type:结构体类型;
  • member:结构体中名为member的成员;

通过下面测试例程来说明:

#include "stdio.h"
#include "stdlib.h"

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

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

struct Grade {
	short Chinese;
	short Math;
	short English;
};

struct Student {
	char name[20];	
	char sex;		
    short age;		
	struct Grade gra;
	char addr[50];	
};

int main(void)
{
	struct Student stu = {
		"jack",
		'F',
		20,
		{99, 98, 97},
		"North Garden Road"
	};
	
	printf("%#x\n", &stu);	
	struct Student *stu_ = list_entry(&stu.gra, struct Student, gra);
	printf("%#x\n", stu_);	// stu_ should be equal to &stu
	
	printf("name = %s\n", stu_->name);
	printf("sex = %c\n", stu_->sex);
	printf("age = %d\n", stu_->age);
	printf("gra = %d %d %d\n", stu_->gra.Chinese, stu_->gra.Math, stu_->gra.English);
	printf("addr = %s\n", stu_->addr);
	
	return 0;
}

已知stu.gra,通过list_entry得到了stu_。stu_实际上就是变量stu的首地址,然后就可以使用stu_来访问其它成员了。

执行结果如下:

0x5dd100f0
0x5dd100f0
name = jack
sex = F
age = 20
gra = 99 98 97
addr = North Garden Road

原理解析

list_entry直接拿来用就可以了,但是研究下它的实现原理,更有意思。

先来看offsetof():

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

将0地址,强转成 TYPE 类型的指针,然后指向 MEMBER 成员,再取其地址—— 这不就是 MEMBER 成员相对0地址的偏移量吗?!

再看:

const typeof( ((type *)0)->member ) *__mptr = (ptr);

将0地址强转成 type 类型的指针,然后指向其 member 成员,再调用 typeof() 获取其类型,也就是 member 成员的类型。再用其定义一个同类型的指针 __mptr 并指向ptr,其实就是将 ptr 地址暂存到了临时变量 __mptr 中。
这有个疑问:为什么要再定义一个临时指针存放 ptr 呢?直接用 ptr 不就好了?这样container_of()就可以更加简化了。有没有大神能解惑呢?

再看

(type *)( (char *)__mptr - offsetof(type,member) );

__mptr 即 ptr,即结构体成员member的地址,减去“member成员相对0地址的偏移量”,那不就是结构体的首地址嘛。

结合上面的测试例程,示意图如下:
在这里插入图片描述

参考:
list_head介绍
进程管理(六)–进程初始化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值