Linux 内核数据结构:双向链表

Linux 内核提供一套双向链表的实现,你可以在 include/linux/list.h 中找到。我们以双向链表着手开始介绍 Linux 内核中的数据结构 ,因为这个是在 Linux 内核中使用最为广泛的数据结构,具体你可以 查看 这里。

首先让我们看一下主要的结构体:

struct list_head {

struct list_head *next, *prev;

};

你可以看到其与常见的结构体实现有显著不同,比如 glib 中所使用到的双向链表实现。

struct GList {

gpointer data;

GList *next;

GList *prev;

};

通常来说,链表结构体要包括一个指向数据的指针,不过1216.www.qixoo.qixoo.com/ Linux 内核的链表却不包含此实现。那么首要的疑问:链表是用什么方式存储数据的?。Linux 内核所实现的是一种被称为侵入式的链表(Intrusive list),这种链表并不在链表结构中包含数据,而仅提供用于维护前向与后向访问结构的指针。这种实现方式使得链表数据结构非常通用,因为它并不需要关注链表所维护的具体数据类型。

比如:

struct nmi_desc {

spinlock_t lock;

struct list_head head;

};

接下来让我们看一些内核使用 list_head 的具体例子。正如在前文所述的,Linux 内核中诸多模块都使用了 list_head。这里我们以内核杂项字符设备驱动(miscellaneous character drivers)部分实现为例。驱动的 API 在 drivers/char/misc.c 中,其实现了简单硬件外设以及虚拟设备的驱动,这个驱动共享主设备号(Major number):

#define MISC_MAJOR              10

每个设备有自己的次设备号,具体可以看这个列子:

现在我们看看设备驱动是如何使用链表维护设备列表的,首先,我们看一下 miscdevice 的 struct 定义:

struct miscdevice

{

int minor;

const char *name;

const struct file_operations *fops;

struct list_head list;

struct device *parent;

struct device *this_device;

const char *nodename;

mode_t mode;

};

可以看到 miscdevice 的第四个成员 list ,这个就是用于维护已注册设备链表的结构。在源代码文的首部,我们可以看到以下定义:

static LIST_HEAD(misc_list);

这个定义宏展开,可以看到是用于定义 list_head 类型变量:

#define LIST_HEAD(name)

struct list_head name = LIST_HEAD_INIT(name)

LIST_HEAD_INIT 这个宏用于对定义的变量进行双向指针的初始化:

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

现在我看可以看一下函数 misc_register 是如何进行设备注册的。首先是用 INIT_LIST_HEAD 对 miscdevice->list 成员变量进行初始化:

INIT_LIST_HEAD(&misc->list);

这个操作与 LIST_HEAD_INIT 宏一致:

static inline void INIT_LIST_HEAD(struct list_head *list)

{

list->next = list;

list->prev = list;

}

接下来,在通过函数 device_create 进行设备创建,同时将设备添加到 Misc 设备列表中:

list_add(&misc->list, &misc_list);

内核的 list.h 文件提供向链表添加节点的 API,这里是添加操作的实现:

static inline void list_add(struct list_head *new, struct list_head *head)

{

__list_add(new, head, head->next);

}

函数实现很简单,就是入参转换为三个参数后调用内部 __list_add :

  • new – new entry;
  • head – list head after which will be inserted new item;
  • head->next – next item after list head.

_list_add 函数的实现更加简单:

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;

}

这里设置了新添加结点的 prev 与 next 指针,通过这些操作,就将先前使用 LIST_HEAD_INIT 所定义的 misc 链表的双向指针与 miscdevice->list 结构关联起来。

这里还有一个问题,就是如何获取链表中的数据,list_head 提供了一个特殊的宏用于获取数据指针。

#define list_entry(ptr, type, member)

container_of(ptr, type, member)

这里有三个参数

  • ptr:list_head 结构指针
  • type:数据对应的 struct 类型
  • member:数据中 list_head 成员对应的成员变量名

举例如下:

const struct miscdevice *p = list_entry(v, struct miscdevice, list)

接下来我们就够访问 miscdevice 的各个成员,如 p->minor、p->name 等等,我们看一下 list_entry 的实现:

#define list_entry(ptr, type, member)

container_of(ptr, type, member)

其实现非常简单,就是使用入参调用 container_of 宏,宏的实现如下:

#define container_of(ptr, type, member) ({

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

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

注意,宏使用了大括号表达式,对于大括号表达式,编译器会展开所有表达式,同时使用最后一个表达式的结果进行返回。

举个例子:

#include <stdio.h>

int main() {

int i = 0;

printf("i = %dn", ({++i; ++i;}));

return 0;

}

输出结果为 2 。

另一个关键是 typeof 关键字,这个非常简单,这个正如它的名字一样,这个关键字返回的结果是变量的类型。当我第一次看到这个宏时,最让我觉得奇怪的是表达式 ((type*)0) 中的 0 值,实际上,使用 0 值作为地址这个是成员变量取得 struct 内相对偏移地址的巧妙实现,我们再来看个例子:

#include <stdio.h>

struct s {

int field1;

char field2;

char field3;

};

int main() {

printf("%pn", &((struct s*)0)->field3);

return 0;

}

输出结果为 0x5 。

还有一个专门用于获取结构体中某个成员变量偏移的宏,其实现与前面提到的宏非常类似:

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

这里对 container_of 宏做个综述,container_of 宏通过 struct 中的 list_head 成员返回 struct 对应数据的内存地址。在宏的第一行定义了指向 list_head 成员的指针 __mptr ,并将 ptr 地址赋给 __mptr 。从技术实现的角度来看,实际并不需要这一行定义,但这个对于类型检查而言非常有意义。这一行代码确保结构体( type )中存在 member 对应的成员。第二行使用 offsetoff 宏计算出包含 member 的结构体所对应的内存地址,就是这么简单。

当然 list_add 与 list_entry 并非是 <linux/list.h> 中的全部函数,对于双向链表 list_head ,内核还提供了以下的接口:

  • list_add
  • list_add_tail
  • list_del
  • list_replace
  • list_move
  • list_is_last
  • list_empty
  • list_cut_position
  • list_splice

未了,需要说的是,内核代码中并不仅仅只有上述这些接口。

时间: 2024-10-25 18:49:08

Linux 内核数据结构:双向链表的相关文章

linux内核数据结构学习总结(undone)

本文旨在整理内核和应用层分别涉及到的数据结构,从基础数据结构的角度来为内核研究作准备,会在今后的研究中不断补充 目录 1. 进程相关数据结构 1) struct task_struct 2. 内核中的队列/链表对象 3. 内核模块相关数据结构 2) struct module 1. 进程相关数据结构 0x1: task_struct 我们知道,在windows中使用PCB(进程控制块)来对进程的运行状态进行描述,对应的,在linux中使用task_struct结构体存储相关的进程信息,task_

Go语言移植Linux内核数据结构hlist

hlist(哈希链表)可以通过相应的Hash算法,迅速找到相关的链表Head及节点. 在有些应用场景,比Go标准库提供的list(一种双向链表)更合适. 依照list.h中的源码,我实现了一个Go语言版本的hlist例子. 首先说下hlist的构成:             在hlist(哈希链表)中, 头结点使用struct hlist_head来表示,hlist_head仅一个first指针. 普通节点使用struct hlist_node来表示. 源码中有几个特别的地方: 1. 在stru

Linux内核中双向链表的经典实现

Linux内核中双向链表的经典实现 概要 前面一章"介绍双向链表并给出了C/C++/Java三种实现",本章继续对双向链表进行探讨,介绍的内容是Linux内核中双向链表的经典实现和用法.其中,也会涉及到Linux内核中非常常用的两个经典宏定义offsetof和container_of.内容包括:1. Linux中的两个经典宏定义2. Linux中双向链表的经典实现 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3562146.html 更多

AT&amp;T汇编语言与GCC内嵌汇编,Linux内核数据结构之链表

最近在看<Linux内核源代码情景分析>,作者毛德操.书中刚开始介绍了AT&T汇编语言与GCC内嵌汇编,以及Linux内核数据结构之链表.可惜书中介绍的不够全面.因为推荐大家阅读下面两篇文章.很不错. AT&T汇编语言与GCC内嵌汇编:http://grid.hust.edu.cn/zyshao/Teaching_Material/OSEngineering/Chapter2.pdf. Linux内核数据结构之链表:http://www.cnblogs.com/Anker/p/

Linux 内核数据结构:Linux 双向链表

Linux 内核提供一套双向链表的实现,你可以在 include/linux/list.h 中找到.我们以双向链表着手开始介绍 Linux 内核中的数据结构 ,因为这个是在 Linux 内核中使用最为广泛的数据结构,具体你可以 查看 这里. 首先让我们看一下主要的结构体: struct list_head { struct list_head *next, *prev; }; 你可以看到其与常见的结构体实现有显著不同,比如 glib 中所使用到的双向链表实现. struct GList { gp

Linux内核数据结构——链表

目录 目录 简介 单向链表 双向链表 环形链表 Linux内核中的链表实现 offsetof container_of container_of 第一部分 container_of 第二部分 链表初始化 向链表中增加一个节点 删除节点 移动节点 判断链表是否为空 遍历链表 Demo测试 mlisth mlistc 执行结果 简介 最近在学习Android Binder驱动程序实现的时候,发现里面的数据结构用到了struct list_head.而我google发现struct list_head

Linux内核数据结构之链表

与经典双向链表比较 ??经典双向链表如图.其中有一个pre指针和一个next指针,数据是在链表的节点内. ??内核链表如图.每一个链表节点内只有一个pre指针和一个next指针,整个链表节点嵌入到了一个需要使用链表的结构体内. 内核链表介绍 ??内核链表节点结构体定义如图.其中next指针指向下一个链表节点,prev指针指向前一个链表节点. ??前面已经说过,内核链表节点是嵌入到数据节点内的,那么就产生了一个问题,如何访问到链表所在结构体的指针呢? ??内核链表中通过list_entry宏来访问

linux内核数据结构之kfifo

1.前言 最近项目中用到一个环形缓冲区(ring buffer),代码是由linux内核的kfifo改过来的.缓冲区在文件系统中经常用到,通过缓冲区缓解cpu读写内存和读写磁盘的速度.例如一个进程A产生数据发给另外一个进程B,进程B需要对进程A传的数据进行处理并写入文件,如果B没有处理完,则A要延迟发送.为了保证进程A减少等待时间,可以在A和B之间采用一个缓冲区,A每次将数据存放在缓冲区中,B每次冲缓冲区中取.这是典型的生产者和消费者模型,缓冲区中数据满足FIFO特性,因此可以采用队列进行实现.

linux内核数据结构之红黑树

首先我先回顾一下二叉树 然后回顾一下二叉搜索树 下面是重头戏 自平衡二叉搜索树满足二叉搜索树的条件.即每个节点左边的节点值都要比自己小,然后满足平衡,即树(包括子树)的末尾节点深度相差小于1,这样的树称为平衡二叉搜索树 最后红黑树 红黑树有着插入,删除,搜索非常快的优点,特别是插入和删除要比平衡二叉搜索树要快,所以在有频繁的插入和删除操作的情况下,使用红黑树进行存储是非常有效的. linux内核中提供了红黑树的基本算法,我们只需要构造自己的插入,删除,和搜索函数就可以根据自己的需求使用红黑树了.