学习 Binder 的预备知识2 —— Linux 内核常用数据结构

1,362 阅读6分钟

Binder 是一个 Linux 驱动,驱动代码会涉及很多 Linux 内核中的数据结构,接下来我们就来看看 Linux 中的常用数据结构的基本使用:

  • 双向链表 list_head
  • hash 表 hlist
  • 红黑树 rbroot

1. 双向链表

list_head 是内核中定义的双向链表:

// kernel/inclue/linux/types.h

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

内核中提供了链表初始化的宏:

//初始化一个 list_head, 其 next prev 指针均指向自己
#define LIST_HEAD_INIT(name) { &(name), &(name) }

//同上,多定义了一个 list_head 变量
#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)
 
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	WRITE_ONCE(list->next, list);  //线程安全赋值
	list->prev = list;
}

常用 LIST_HEAD 进行初始化一个链表:

LIST_HEAD(head);

head 的 prev 和 next 指针都是指向自己。

但是如果只是利用 list_head 这样的结构体实现链表就没有什么实际意义了,因为正常的链表都是为了遍历结构体中的其它有意义的字段而创建的,而我们 list_head 中只有 prev 和 next 指针,却没有实际有意义的字段数据,所以毫无意义。我们可以创建一个宿主结构,然后在此结构中再嵌套 list_head 字段,宿主结构又有其它的字段(进程描述符 task_struct,页面管理的page结构等就是采用这种方法创建链表的)。为简便理解,定义如下:

//定义链表节点
struct list_node_student {
    char *name;
    int age;
    int score;
    struct list_head list;
};

接着我们可以创建第一个节点:

struct my_task_list first_task = 
{ 
	.val = 1,
	.mylist = LIST_HEAD_INIT(first_task.mylist)
};

可以通过 list_add 方法在链表头插入新的数据:

//定义并初始化链表头
LIST_HEAD(header);

//链表插入节点,加入链表头
list_add(&jack.list, &header);

也可以通过 list_add_tail 方法在链表尾插入新的数据:

list_add_tail(&bob.list, &header);

删除节点也是链表的一个常用操作:

//删除节点
list_del(&bob.list);

很多时候我们需要从 list_head 找到其宿主结构,linux 内核中提供了 list_entry 来完成这个工作(具体是通过 container_of 宏实现):

list_entry(&jack.list, struct list_node_student, list);

链表的另一个重要操作是遍历:

//list_head 遍历
struct list_head *pos;
list_for_each(pos, &header) {
        
}

list_for_each_prev(pos, &header) {
        
}

//宿主结构的遍历
struct list_node_student *student;
list_for_each_entry(student, &header, list) {
        
}

2. hash 表

hlist 是 linux 内核中基于双向链表实现的 hash 表,相关的数据结构有两个:

//hash桶的头结点
struct hlist_head {
	struct hlist_node *first;//指向每一个hash桶的第一个结点的指针
};

//hash桶的普通结点
struct hlist_node {
	struct hlist_node *next;//指向下一个结点的指针
	struct hlist_node **pprev;//指向上一个结点的next指针的地址
};

hash 表的结构如下:

  • 使用 hlist 通常会定义一个 hlist_head 的数组,
  • hlist_head 结构体只有一个域,即 first。 first 指针指向该 hlist 链表的第一个节点。
  • hlist_node 结构体有两个域,next 和 pprev。 next 指针很容易理解,它指向下个 hlist_node 结点,倘若该节点是链表的最后一个节点,next 指向 NULL。
  • pprev 是一个二级指针,它指向前一个节点的 next 指针的地址

pprev 为什么要是一个执行向前一个节点的 next 指针地址的二级指针? 直接指向上一个节点会不会更简单一点?

这里应该是一个设计取向问题,因为 hash 桶的类型是 hlist_head,为了减少数据结构额外内存开销,其内部有一个只有一个指针,如果 hlist_node 采用传统的 next,prev指针,对于第一个节点和后面其他节点的处理会不一致。这样并不优雅。

hlist_node 巧妙地将 pprev 指向上一个节点的 next 指针的地址,由于 hlist_head 的 first 域指向的结点类型和 hlist_node 指向的下一个结点的结点类型相同,这样就解决了通用性!

这种编码的方式是值得我们学习的。

接着我们来看一下,如何初始化一个 hash 表并插入数据:

 //定义宿主结构体
struct hdata_node {
    int data;
    struct hlist_node list;
};

//hash 数组
struct hlist_head htable[256];

struct hdata_node *hnode;

//初始化
for (int i = 0; i < 256; ++i) {
    INIT_HLIST_HEAD(&htable[i]);
    hnode = kmalloc(sizeof(struct hdata_node), GFP_KERNEL);
    INIT_HLIST_NODE(&(hnode->list));
    hnode->data = i * 9;
    //链表中插入数据
    //自定义 hash 算法,这里简单取余
    int key = hnode->data % 256;
    //添加到链表首部
    hlist_add_head(&hnode->list, &htable[key]);
}

查询数据:

//查询
    int search = 67 * 9;
    int key = search % 256;

    if (hlist_empty(&htable[key])) {
        //没有需要查询的项
    } else {
        //遍历查询
        hlist_for_each_entry(hnode, &htable[key], list) {
            if (hnode->data == search) {
                //找到了
                break;
            }
        }
    }

删除数据:

//删除
    int delete = 88 * 9;
    int key2 = search % 256;
    struct hlist_node *n;

    if (hlist_empty(&htable[key])) {
        //没有需要查询的项
    } else {
        //遍历查询
        hlist_for_each_entry_safe(hnode, n ,&htable[key], list) {
            if (hnode->data == search) {
                //找到了
                hlist_del(hnode);
                break;
            }
        }
    }

内存清理:

 //退出程序前释放资源
    for(i=0; i < 256; i++){
        //遍历每一个槽,有结点就删除
        hlist_for_each_entry_safe(hnode, n, &htable[i], list){
            hlist_del(&hnode->list);
            kfree(hnode);
            hnode = NULL;
        }
    }

3. 红黑树

红黑树,从理论到实现都是相对复杂的数据结构,但是实际编码中一般不需要我们去做实现,把它看成一个插入数据慢点,查找数据快点的链表即可。从使用上来说,红黑树主要又以下特点:

  • 插入、删除、查找的时间复杂度接近 O(logN),N 是节点个数;是一种性能非常稳定的二叉树!
  • 中序遍历的结果是从小到大排好序的

接着我们来看下 linux 内核中,红黑树的基本使用:

内核中定义了以下几个红黑树相关的数据结构:

//红黑树节点
struct rb_node {
	unsigned long  __rb_parent_color;
	struct rb_node *rb_right;
	struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));

//红黑树根节点
struct rb_root {
	struct rb_node *rb_node;
};

接下来我们看看如何如何使用内核中的红黑树:

//定义宿主结构体
struct my_tree_node {
    int data;
    struct rb_node node;
};

//内核中没有提供现成的插入,查找函数,需要使用者自己实现
int rb_insert(struct rb_root *root, struct my_tree_node *insert_node) {
    struct rb_node **n = &(root->rb_node);
    struct rb_node *parent = NULL;
    while (*n) {
        struct my_tree_node *thiz = container_of(*n, struct my_tree_node, node);
        parent = *n;
        if (thiz->data > insert_node->data) {
            n = &((*n)->rb_left);
        } else if (thiz->data < insert_node->data) {
            n = &((*n)->rb_right);
        } else {
            return -1;
        }
    }

    rb_link_node(&insert_node->node, parent, n);
    rb_insert_color(&insert_node->node, root);
}

//定义节点查询函数
struct my_tree_node *rb_search(struct rb_root *root, int new) {
    struct rb_node *node = root->rb_node;
    while (node) {
        struct my_tree_node *my_node = container_of(node, struct my_tree_node, node);

        if (my_node->data > new) {
            node = node->rb_left;
        } else if (my_node->data < new) {
            node = node->rb_right;
        } else {
            return my_node;
        }
    }

    return NULL;
}

 struct my_tree_node *data;
    struct rb_node *node;
    
    struct rb_root mytree = RB_ROOT;
    
    //插入元素
    for (int j = 0; j < 10; ++j) {
        data = kmalloc(sizeof(struct my_tree_node), GFP_KERNEL);
        data->data = i * 36;
        rb_insert(&mytree, data);
    }
    
    //遍历红黑树
    for(node = rb_first(&mytree); node; node = rb_next(node)) {
        printk("key=%d\n", rb_entry(node, struct my_tree_node, node)->data);
    }
    
    //红黑树内存清理
    for(node = rb_first(&mytree); node; node = rb_next(node)) {
        data = rb_entry(node, struct my_tree_node, node);
        if (data) {
            rb_erase(&data->node, &mytree);
            kfree(data);
        }
    }

参考资料

关于

我叫阿豪,2015 年毕业于国防科技大学,毕业后,在某单位从事信息化装备的研发工作。主要研究方向为 Android Framework 与 Linux Kernel。目前已退伍定居成都,主要做工程机械相关的投资,同时也在做 Android Framework 相关的技术分享。

如果你对 Framework 感兴趣或者正在学习 Framework,可以参考我总结的Android Framework 学习路线指南,也可关注我的微信公众号,我会在公众号上持续分享我的经验,帮助正在学习的你少走一些弯路。学习过程中如果你有疑问或者你的经验想要分享给大家可以添加我的微信,我拉你进技术交流群。