Linux内核链表浅析及简单运用

本文介绍了Linux内核链表的定义、初始化、判断空、添加删除节点、遍历以及获取数据信息等基础知识,并通过实例展示了如何结合链表实现一个FIFO队列,探讨了数据结构扩展和指针偏移的巧妙运用。
摘要由CSDN通过智能技术生成

        作为一个c++新手,我也开始接触底层的东西鸟。

        Linux内核中有很多的数据结构,链表就是其中之一,但是这个链表和普通我们用的链表是有很大区别的,区别就在于,它只有指针域,却没有数据域,这样就有很大的一个好处,很方面我们来扩展它,我们只要自己定义一种数据结构,指定自己的数据域,使用它作为指针域就OK了。而且它还实现了各种底层的方法,方面我们运用。

1.链表的定义

        Linux内核链表定义在include/linux/list.h头文件中,是用的一个结构体:

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

        可以看出Linux内核链表是一个双向链表。

2.链表的初始化

        提供了一个方法和两个宏来初始化:

#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;
}

        都是将节点的前后指针指向自己。

3.链表是否为空

static inline int list_empty(const struct list_head *head)
{
	return head->next == head;
}

        由上面看出初始化是将指针都指向自己,如果链表为空的话,那么头结点就是指向自己,头结点是没有任何意义的,只是一个标识。

4.链表添加结点

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;
}

        这是一个很底层的方法,将new这个结点添加到prev和next之间。我们经常要将一个结点添加到另一个结点之前或者之后,只要将该结点的前结点或者后结点带进去:

static inline void list_add(struct list_head *new, struct list_head *head)
{
	_list_add(new, head, head->next);
}

static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	_list_add(new, head->prev, head);
}

        head为头结点,所以list_add_tail()方法就是将结点new添加到链表的尾端。

5.链表删除结点

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)
{
	_list_del(entry->prev, entry->next);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

static inline void list_del_init(struct list_head *entry)
{
	_list_del(entry->prev, entry->next);
	INIT_LIST_HEAD(entry);
}

        方法list_del调用后,将entry的前后指针指向不可访问的的位置,作用也就相当于free(),list_del_init()方法调用后,将entry这个结点又初始化一次,指向自己。

6.链表的遍历

        Linux内核提供了很多的遍历链表的宏,其实基础的就2个,也就是向两个方向遍历:

#define list_for_each(pos, head) \
	for(pos = (head)->next; prefetch(pos->next), pos != (head); pos = pos->next)

#define list_for_each_prev(pos, head) \
	for(pos = (head)->prev; prefetch(pos->prev), pos != (head); pos = pos->prev)

        从中可以看出,是根据指针的移动来遍历整个链表,从头结点开始,到再次回到头结点为止。但是当我们遍历达到某个条件要删除结点的时候,就不能这么遍历了,不然会指针会出错。那么可以用下面提供的宏来实现:

#define list_for_each_safe(pos, n, head) \
	for(pos = (head)->next, n = pos->next; pos != (head); pos = n, n = pos->next)

#define list_for_each_prev_safe(pos, n, head) \
	for(pos = (head)->prev, n = pos->prev; pos != (head); pos = n, n = n->prev)

        这里多出了一个局部临时指针n,它的作用就是当我们删除某个结点后,不至于找指针的时候会出错,有一个安全的保证。

7.得到链表结构的数据信息

        这个是最难理解的地方,也是最重要地方。说它难理解就是因为它很巧妙的使用了0地址,还有地址的偏移,说它重要是因为我们用链表的话必须存数据,而Linux内核提供的链表是没有数据域的,必须我们自己加上,但是指针指向的还是指针域地址而不是我们扩展后的对象地址,而提供的这个借口能让我们得到扩展后的对象地址。这里可能有点难理解,后面会解释到:

#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

// 这个container_of定义在include/linux/kernel.h头文件中:
#define container_of(ptr, type, member)({ \
	const typeof(((type *)0)->member) *_mptr = (ptr); \
	(type *)((char *)_mptr - offsetof(type, member));})

//而offsetof定义为:
#define offsetof(TYPE, MEMBER)((size_t)&((TYPE *)0)->MEMBER)

       这个看上去太复杂了,简化一下其实就是:

#define list_entry(ptr, type, member) \
	((type *)((char *)(ptr) - ((unsigned long)(&((type *)0)->member - 0)))

        这个看上去就简单多了,ptr为指向指针域的指针,type为我们扩展过后的数据结构类型,member就是指针域的属性,比如我们扩展了一个链表,增加一个int型的数据域:

struct list{
	int data;
	struct list_head pointer;
};

        比如我们有一个list型对象my_list,那么my_list.next得到的是my_list指向的下一个结点的pointer,而不是list,那么我们怎么得到这个list的地址呢?如图:


      我们用链表就是要得到这个data,但是我们的next和prev指针得到的都是指针的地址,那么我们怎样才能得到整个对象的首地址呢,看下图:


        加入我们在0地址这里创建一个list对象,那么member这里的地址就为((list *)0)->pointer,那么这个差值也就是偏移量就为member这里的地址减去0地址有木有,那么当我们知道ptr这里的地址的时候,是不是拿ptr地址减去差值就得到了目标地址呢。也就是我们想要得到的list的首地址,得到过后->data是不是就是我们要的data呢。这样想想是不是还有点小激动呢。

        Linux内核链表最基本的东西大概就是这些,当然还提供了很多的接口,但是这些都是基于以上接口实现的,比如结点的move(),链表的合并,搞清楚以上这些接口并灵活运用,那么其他的接口都可以很简单的实现。

        下面是我基于Linux内核链表自己实现一个先进先出的队列,其中我还增加了一个int型属性id,用来统计队列的大小,这样就不用遍历了++来得到大小:

queue_3.h:

#ifndef _QUEUE_3_H_
#define _QUEUE_3_H_

#include "mylist.h" // mylist.h是我自己写的一个头文件,只截取了部分Linux内核链表的接口

struct queue_node
{
	int id; // 用来统计队列大小,就像数组索引一样,尾结点的id就是队列的大小
	int data; // 数据域
	struct list_head list; // 指针域
};

class queue
{
private:
	struct queue_node head; // 头结点
public:
	queue();
	void init();
	void push(struct queue_node *node);
	int pop();
	int size();
	queue_node find(int nData);
}

#endif /*_QUEUE_3_H_*/
queue_3.cpp:

#include "queue_3.h"

queue::queue()
{
	head.id = 0;
	head.data = 0;
	init(); 
}

void queue::init()
{
	INIT_LIST_HEAD(&head.list);
}

void queue::push(struct queue_node *node)
{
	struct queue_node *rear = list_entry(head.list.prev, struct queue_node, list); // 尾结点
	node->id = rear->id + 1; // id加1,作为尾结点
	list_add(&node->list, &rear->list);
}

int queue::pop()
{
	if(list_empty(&head.list)) // 如果队列是空的,就返回0
	{
		return 0;
	}
	struct queue_node *rear = list_entry(head.list.prev, struct queue_node, list); // 尾结点
	rear->id = rear->id - 1; // 尾结点id减1
	struct queue_node *result = list_entry(head.list.next, struct queue_node, list);
	list_del_init(&result->list);
	return result->data;
}

int queue::size()
{
	struct queue_node *rear = list_entry(head.list.prev, struct queue_node, list); // 尾结点
	return rear->id;
}

queue_node queue::find(int nData) // 没做元素数据重复判定(待完善)
{
	struct list_head *pos;
	struct queue_node *result;
	list_for_each(pos, &head.list) // 遍历,查找
	{
		result = list_entry(pos, struct queue_node, list);
		if(result->data == nData)
		{
			return *result;
		}
	}
	return head; // 没查找到就返回头结点
}
queuetest_3.cpp:

#include <iostream>
#include "queue_3.h"
using namespace std;

int main()
{
	cout << "queue!" << endl;
	queue q;
	cout << "size of the queue : " << q.size() << endl;
	cout << "get NO.1 of queue : " << q.pop() << endl << endl;

	struct queue_node node1, node2, node3, node4, node5;
	node1.data = 1;
	node2.data = 2;
	node3.data = 3;
	node4.data = 4;
	node5.data = 5;
	INIT_LIST_HEAD(&node1.list);
	INIT_LIST_HEAD(&node2.list);
	INIT_LIST_HEAD(&node3.list);
	INIT_LIST_HEAD(&node4.list);
	INIT_LIST_HEAD(&node5.list);
	q.push(&node1);
	q.push(&node2);
	q.push(&node3);
	q.push(&node4);
	q.push(&node5);

	struct queue_node result = q.find(3);
	cout << "find 3, (3 exist) the result : " << result.data << endl;
	result = q.find(10);
	cout << "find 10, (10 not exist) the result : " << result.data << endl;

	cout << "size of the queue : " << q.size() << end;
	cout << "get NO.1 of the queue : " << q.pop() << endl;
	cout << "after pop NO.1 size of the queue : " << q.size() << endl;
	cout << "get NO.2 of the queue : " << q.pop() << endl;
	cout << "after pop NO.2 size of the queue : " << q.size() << endl;
	cout << "get NO.3 of the queue : " << q.pop() << endl;
	cout << "after pop NO.3 size of the queue : " << q.size() << endl;
	cout << "get NO.4 of the queue : " << q.pop() << endl;
	cout << "after pop NO.4 size of the queue : " << q.size() << endl;
	cout << "get NO.5 of the queue : " << q.pop() << endl;
	cout << "after pop NO.5 size of the queue : " << q.size() << endl;
	cout << "get NO.6 of the queue : " << q.pop() << endl;
	cout << "after pop all element size of the queue : " << q.size() << endl << endl;

	return 0;
}
测试结果:


        c++新手代码,如有不足请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值