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

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

    struct list_head {

    struct list_head *next, *prev;

    };

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

    struct GList {

    gpointer data;

    GList *next;

    GList *prev;

    };

通常来说,链表结构体要包括一个指向数据的指针,不过 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 的实现:

    #1216.www.qixoo.qixoo.com/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

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

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

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

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

  2. linux内核数据结构之链表

    linux内核数据结构之链表 1.前言 最近写代码需用到链表结构,正好公共库有关于链表的.第一眼看时,觉得有点新鲜,和我之前见到的链表结构不一样,只有前驱和后继指针,而没有数据域.后来看代码注释发现该 ...

  3. linux内核数据结构学习总结

    目录 . 进程相关数据结构 ) struct task_struct ) struct cred ) struct pid_link ) struct pid ) struct signal_stru ...

  4. linux内核数据结构--进程相关

    linux里面,有一个结构体task_struct,也叫“进程描述符”的数据结构,它包含了与进程相关的所有信息,它非常复杂,每一个字段都可能与一个功能相关,所以大部分细节不在我的研究范围之内,在这篇文 ...

  5. Linux内核中的双向链表struct list_head

    一.双向链表list_head Linux内核驱动开发会经常用到Linux内核中经典的双向链表list_head,以及它的拓展接口和宏定义:list_add.list_add_tail.list_de ...

  6. linux内核数据结构之kfifo

    1.前言 最近项目中用到一个环形缓冲区(ring buffer),代码是由linux内核的kfifo改过来的.缓冲区在文件系统中经常用到,通过缓冲区缓解cpu读写内存和读写磁盘的速度.例如一个进程A产 ...

  7. linux内核数据结构之kfifo【转】

    1.前言 最近项目中用到一个环形缓冲区(ring buffer),代码是由linux内核的kfifo改过来的.缓冲区在文件系统中经常用到,通过缓冲区缓解cpu读写内存和读写磁盘的速度.例如一个进程A产 ...

  8. Linux内核数据结构之kfifo详解

    本文分析的原代码版本: 2.6.24.4 kfifo的定义文件: kernel/kfifo.c kfifo的头文件: include/linux/kfifo.h kfifo是内核里面的一个First ...

  9. linux内核数据结构之链表【转】

    转自:http://www.cnblogs.com/Anker/p/3475643.html 1.前言 最近写代码需用到链表结构,正好公共库有关于链表的.第一眼看时,觉得有点新鲜,和我之前见到的链表结 ...

随机推荐

  1. Android layout_weight理解

    layout_weight属性只能用于LinearLayout布局,不能用于RelativeLayout等其他布局: layout_weight属性如果不设定,默认值为0: layout_weight ...

  2. nginx 访问目录403

    centos7.2默认安装好nginx后,会在/usr/share/nginx/html下作为主目录 但是如果想访问下面的目录会发现没有权限,返回403错误 这时候要注意在/etc/nginx/ngi ...

  3. You cannot change a partition into an extended one or vice versa Delete it first

    在Linux扩展LVM时,使用fdisk创建分区时,在磁盘上新建扩展分区(逻辑分区),修改分区格式,指定分区类型为8e时,报错"You cannot change a partition i ...

  4. linux c++编译问题和虚拟机网络通信

    1.gcc main.cpp -lstdc++ -o test5 2. service network stop service NetworkManager restart

  5. 删除Windows中隐藏的物理网卡和网络虚拟化失败后的虚拟网卡

    Windows环境下,在更换硬件服务器主板和网卡等硬件.恢复操作系统或者网络虚拟化失败后,可能会出现网卡方面的问题.例如,设备管理器中多了不应该存在的网卡:因命名冲突无法重命名当前网络连接:IP地址冲 ...

  6. 通过例子学习 Keystone - 每天5分钟玩转 OpenStack(19)

    上一节介绍了 Keystone 的核心概念.本节我们通过“查询可用 image”这个实际操作让大家对这些概念建立更加感性的认识. User admin 要查看 Project 中的 image 第 1 ...

  7. Java 对象初始化

      对象A的创建过程: 1. 构造器实际上是静态方法.当首次创建对象A 或者 A类的静态方法/静态域首次被访问时,Java解释器查找类路径,以定位     A.class文件.(当程序创建第一个对类的 ...

  8. C语言中结构体赋值问题的讨论

    今天帮师姐调一个程序的BUG,师姐的程序中有个结构体直接赋值的语句,在我印象中结构体好像是不能直接赋值的,正如数组不能直接赋值那样,我怀疑这个地方有问题,但最后证明并不是这个问题.那么就总结一下C语言 ...

  9. 【JavaScript Demo】回到顶部功能实现

    随着网站的不断发展,需要展示的内容也越来越丰富,这导致网页上能展示的内容越来越多.当内容堆积影响了用户体验,就需考虑如何提升用户体验.在这一系列的改动中,“回到顶部”的功能成为了一个经典. 1.页面布 ...

  10. BZOJ3130: [Sdoi2013]费用流[最大流 实数二分]

    3130: [Sdoi2013]费用流 Time Limit: 10 Sec  Memory Limit: 128 MBSec  Special JudgeSubmit: 960  Solved: 5 ...