C语言之list_head双向链表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013904227/article/details/78014341

阅读原文

对于嵌入式开发者来说,双向链表是用的非常多的一种数据结构之一,在linux内核里面有一个叫做list_head的结构体,专门用来做双向链表的种种操作,掌握并理解双向链表以及list_head的实现方式对于嵌入式开发来说是非常有帮助的。

为什么要用链表

在嵌入式开发实际编程当中,会经常有数据管理之类的需求,比如一个视频设备,在采集视频帧的时候需要一个buffer队列来进行视频帧的缓存管理,这个时候就需要用到一些队列管理类的数据结构。再比如说,设备的管理也需要用到双向链表(linux内核的设备驱动管理)。

  • 数组
    数组当然可以实现数据管理这个需求,常见的方式使用数组来管理一个具有固定序列的数据列表,比如说一个有固定数量的设备,数量序号从0~3,在这种情况下,不需要做队列循环的操作,在创建一个设备的时候只需要将相应的设备按照标号放入到对应的数组位置当中即可。这种方式可以快速的定位数据所在的位置,而不需要从头到尾去顺序查询。
  • 链表
    有些没有固定数量,处于不断地更新变化的数据,比如一个需要异步执行并且不可丢失的命令序列、一个需要进行实时加载与卸载的驱动,无序且数量未知,这个时候就需要链表结构来协助完成数据的管理。链表不需要过度关注数据的顺序,还可以方便快捷地在任意一个地方插入删除一个元素,并且不会影响到其它的元素。
  • 哈希表
    哈希表融合了数组与链表的优点,既可以快速查询到指定元素,又可以十分灵活的添加或者删除相关的元素。不过哈希表不在本文的讨论范围之内,暂且略过不表。

链表以极强的灵活性而被内核作为广泛使用的数据结构,通常用于各种驱动列表的管理,还有各种拓扑结构上也使用到了链表:比如V4L2驱动框架。链表相较于数组更适合用于这些管理的原因有:
1. 链表适合于做队列循环,链表做一次队列循环只需要对两个元素进行操作,而数组需要对所有的元素进行操作。
2. 链表可以即时即刻,在任意一个位置新建、插入、删除元素而不至于影响到其它的元素,而数组想在一个地方插入一个元素,假设元素个数为n,在第m个位置插入元素,则至少需要挪动n-m+1个元素,并且数组的空间拓展相较于链表更加繁琐一点。
3. 链表的存储空间拓展比较灵活,随用随分配,而数组通常情况下得提前定好长度,这样极有可能会造成空间的浪费或者空间不足。不过可以通过另外一种方式实现变长数组,有一种不完全类型可以实现变长的数组,这个变长的数组叫做柔性数组,使用结构体可以实现(文章结尾会介绍柔性数组),但即使是变长的,也是增加空间容易,减少空间困难,不如链表。

链表

单向链表

  • 原型
struct simplex
{
    int value;
    struct simplex *next;
};
  • 结构图
    单向循环链表

双向链表

  • 原型
struct dlist
{
    struct dlist *next;
    struct dlist *prev;
};
  • 结构图
    双向循环链表

根据上面的两种简单链表结构可看出,单向循环链表的遍历顺序是单一的,从链表的某一个成员开始,如果要匹配的成员已知在链表遍历方向的反方向,整个遍历过程就会显得十分繁琐,不够灵活。而双向链表可以在任意一个位置双向遍历,比单项链表显得更加灵活。

list_head结构体

简述

在linux内核当中,一个随处可见的结构体就是list_head结构体,其原型是:

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

此结构体在linux内核中被大量的引用,几乎所有内核当中需要构成链表结构的地方都用到了这个结构体。例如内核的设备驱动管理就广泛使用到了这个结构体。该结构体相关的代码可以在include/linux/list.h内核文件当中看到。

  • 一个问题
    Q:为什么linux内核里面不使用类似双向链表一节所描述的链表结构?
    A:因为内核里面的结构体众多,每个结构体的类型都不一样,如果在每个结构体里面都实现一个特定的双向链表,不仅结构体的代码量增加了,而且每个结构体都需要单独实现特定的链表管理方法,这样会在内核里面增加大量繁琐、冗余的代码。

  • 解决方法
    实现一个list_head双向链表元结构体,然后在每一个需要进行队列管理的结构体里面嵌入这个list_head元结构体,使用统一的接口来对众多的结构体链表进行管理。

  • 如何使用list_head结构体

    1. 在需要管理的结构体里面嵌入类似struct list_head list的代码。
    2. 使用linux提供的统一的接口进行链表增删、遍历操作。

实例:

#include <stdio.h>
#include <list.h>

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

struct test_list
{
    unsigned char *name;
    struct list_head test;
    int value;
};

static struct test_list lists[] = {
    [0] = {
        .value = 0,
        .name = "apple"
    },
    [1] = {
        .value = 1,
    },
    [2] = {
        .value = 2,
        .name = "xiaomi"
    },
};

int main(int argv, char *argc[])
{
    struct list_head *mod;
    unsigned int i = 0;

    INIT_HEAD(head);

    list_add_tail(&lists[0].test, &head);
    list_add_tail(&lists[1].test, &head);
    list_add_tail(&lists[2].test, &head);

    list_for_each(mod, &head)
    {
        struct test_list *inode;

        inode = list_entry(mod, struct test_list, test);
        printf("%d %s\n", inode->value, inode->name);
    }

    return 0;
}

该段程序编译运行的结果是:

0 apply
1 (null)
2 xiaomi

实现方法

  • 链表头的初始化
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
    struct list_head name = LIST_HEAD_INIT(name)

此时name的结构就如下图所示:
链表头部初始化

  • 节点的添加
__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;
}

图形化描述:
添加一个节点

  • list_head结构体成员找到父结构体
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)
#define container_of(ptr, type, member) ({          \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

把上面例程代码段中的list_entry(mod, struct test_list, test);展开就得到:

{
    const typeof( ((struct test_list *)0)->test ) *__mptr = (mod);
    (struct test_list *)( (char *)__mptr - ((size_t) &((struct test_list *)0)->test));
}

/* test_list结构体原型 */
struct test_list
{
    unsigned char *name;
    struct list_head test;
    int value;
};

/* mod 变量定义*/
struct list_head *mod;

拆解一下代码步骤可得:
1. ((struct test_list *)0):将0转换为struct test_list类型的指针。
2. typeof( ((struct test_list *)0)->test ):获取test_list结构体内的test成员的类型,也就是list_head类型。
3. typeof( ((struct test_list *)0)->test ) *__mptr = (mod):定义一个struct list_head类型的指针,名为__mptr,指向mod所指向的内存(mod为整个链表中的某一个list_head节点)。
4. ((size_t) &((struct test_list *)0)->test:获取test_list结构体头到test成员的字节长度。
5. (char *)__mptr - ((size_t) &((struct test_list *)0)->test)list_head节点地址减去test_list结构体头到test成员的字节长度(也就是该list_head节点的父结构体test_list的地址)。
6. (struct test_list *)( (char *)__mptr - ((size_t) &((struct test_list *)0)->test)):强制转换为struct test_list类型的指针返还给调用者。
container_of

当然也可以自己实现:

#define mlist_entry(list_head, type, member_name) \
            (type *)((unsigned int)list_head - (unsigned int)(&(((type*)(0))->member_name)))

内核当中还有很多的链表操作方法,例如list_move_tail(链表元素的移动),还有链表的遍历(前向遍历、后向遍历)等等,这些不在赘述,可以去内核的list.h文件里面看看,有了上面的知识,其它的理解起来就轻而易举了。

柔性数组

什么是柔性数组

柔性数组是C99标准特性,GNU的GCC编译器也允许这样的写法。具体为在一个结构体最后可以加上一个类似于char str[0];的声明,代表可变长的数组,在编译的时候并不占用具体的内存空间,str并不是一个指针,而是代表着str这个数组在其结构体中的偏移量,这个在下面的代码中可以看出来。在使用的时候动态分配内存,使用完之后随手释放。

代码测试

#include <stdio.h> 
#include <stdlib.h>
#include <wchar.h>

struct p_test
{
    int age;
    int height;
    char name[0];   //如果放在中间或者前面的话,则name代表的偏移量与紧跟其后的变量地址是一样的,那就造成了变量重叠的后果
}__attribute ((packed));    //禁止编译器进行字节对齐

struct p_test1
{
    int age;
    int height;
    char *name;
}__attribute ((packed));

struct p_test2
{
    int age;
    int height;
    char name[3];
}__attribute ((packed));

struct p_test3
{
    int age;
    int height;
    char name[];
}__attribute ((packed));    //不进行对齐

int main(int argv, char *argc[])
{
    struct p_test *example;

    example = (struct p_test *)malloc(sizeof(struct p_test) + 20);

    example->age = 30;
    example->height = 176;

    memcpy(example->name, "job geo", 8);

    printf("age = %d\nheight = %d\nname = %s\n", example->age, example->height, example->name);
    printf("sizeof p_test : %d\n", sizeof(struct p_test));
    printf("sizeof p_test1 : %d\n", sizeof(struct p_test1));
    printf("sizeof p_test2 : %d\n", sizeof(struct p_test2));
    printf("sizeof p_test3 : %d\n", sizeof(struct p_test3));

    free(example);

    return 0;
}

编译执行结果:
打印结果

height = 176
name = job geo
sizeof p_test : 8
sizeof p_test1 : 12
sizeof p_test2 : 11
sizeof p_test3 : 8

--------------------------------

可以看到,上面的p_test1p_test2结构体的长度都是12,因为p_test1里面的char *name;中的name是一个指针,既然是指针,那么在内存当中就会占用空间(而p_test里面的name却不占用空间),并且其地址不一定是与结构体上面的成员紧密相连,这样就会造成内存的碎片化。并且使用name的时候需要对其分配空间,在释放的时候需要先把name指向的内容释放,并且把其结构体也释放掉,而柔性数组则是一次性分配完毕,一次性释放完毕。形如char name[3];这样的定义在结构体初始化的时候就指定了长度,有些时候如果我们用不到那么多的空间就会造成空间的浪费,所以不如柔性数组来的有效。
  通过上面一段描述,那么问题来了,为什么不用指针来代替柔性数组呢?毕竟指针也可以完成同样的操作啊,而且指针更加容易理解与使用。当我们使用指针的时候,需要先释放结构体内部指针成员的内存,然后再释放结构体本身,而柔性数组则不要前一步,只需要释放结构体本身即可,不用担心因为释放顺序错误或者忘记释放结构体内成员而造成内存泄漏的问题。


如果觉得本文章不错,请关注微信公众号-YellowMax多多支持,查看更多文章

欢迎转发、关注、点赞一波

微信公众号

没有更多推荐了,返回首页