linux内核数据结构.

转载:http://www.cnblogs.com/wang_yb/archive/2013/04/16/3023892.html

http://blog.csdn.net/tigerjibo/article/details/8450995

本文是在以上基础上转载而成,内核基础内容,就不必浪费时间重新写了.

========================================================

内核数据结构贯穿于整个内核代码中,这里介绍4个基本的内核数据结构。

利用这4个基本的数据结构,可以在编写内核代码时节约大量时间。

主要内容:

  • 链表
  • hash 链表
  • 队列
  • 映射
  • 红黑树

1. 链表

链表是linux内核中最简单,同时也是应用最广泛的数据结构。

内核中定义的是双向链表。

 

1.1 头文件简介

内核中关于链表定义的代码位于: include/linux/list.h

list.h文件中对每个函数都有注释,这里就不详细说了。

其实刚开始只要先了解一个常用的链表操作(追加,删除,遍历)的实现方法,

其他方法基本都是基于这些常用操作的。

 

1.2 链表代码的注意点

在阅读list.h文件之前,有一点必须注意:linux内核中的链表使用方法和一般数据结构中定义的链表是有所不同的。

一般的双向链表一般是如下的结构,

  • 有个单独的头结点(head)
  • 每个节点(node)除了包含必要的数据之外,还有2个指针(pre,next)
  • pre指针指向前一个节点(node),next指针指向后一个节点(node)
  • 头结点(head)的pre指针指向链表的最后一个节点
  • 最后一个节点的next指针指向头结点(head)

具体见下图:

 

传统的链表有个最大的缺点就是不好共通化,因为每个node中的data1,data2等等都是不确定的(无论是个数还是类型)。

linux中的链表巧妙的解决了这个问题,linux的链表不是将用户数据保存在链表节点中,而是将链表节点保存在用户数据中。

linux的链表节点只有2个指针(pre和next),这样的话,链表的节点将独立于用户数据之外,便于实现链表的共同操作。

具体见下图:

 

linux链表中的最大问题是怎样通过链表的节点来取得用户数据?

和传统的链表不同,linux的链表节点(node)中没有包含用户的用户data1,data2等。

1.3 使用示例

构造了一个内核模块来实际使用一下内核中的链表,代码在CentOS6.3 x64上运行通过。

C代码:

#include<linux/init.h>

#include<linux/slab.h>

#include<linux/module.h>

#include<linux/kernel.h>

#include<linux/list.h>

 

MODULE_LICENSE("Dual BSD/GPL");

struct student

{

    int id;

    char* name;

    struct list_head list;

};

 

void print_student(struct student*);

 

static int testlist_init(void)

{

    struct student *stu1, *stu2, *stu3, *stu4;

    struct student *stu;

   

    // init a list head

    LIST_HEAD(stu_head);

 

    // init four list nodes

    stu1 = kmalloc(sizeof(*stu1), GFP_KERNEL);

    stu1->id = 1;

    stu1->name = "wyb";

    INIT_LIST_HEAD(&stu1->list);

 

    stu2 = kmalloc(sizeof(*stu2), GFP_KERNEL);

    stu2->id = 2;

    stu2->name = "wyb2";

    INIT_LIST_HEAD(&stu2->list);

 

    stu3 = kmalloc(sizeof(*stu3), GFP_KERNEL);

    stu3->id = 3;

    stu3->name = "wyb3";

    INIT_LIST_HEAD(&stu3->list);

 

    stu4 = kmalloc(sizeof(*stu4), GFP_KERNEL);

    stu4->id = 4;

    stu4->name = "wyb4";

    INIT_LIST_HEAD(&stu4->list);

 

    // add the four nodes to head

    list_add (&stu1->list, &stu_head);

    list_add (&stu2->list, &stu_head);

    list_add (&stu3->list, &stu_head);

    list_add (&stu4->list, &stu_head);

 

    // print each student from 4 to 1

    list_for_each_entry(stu, &stu_head, list)

    {

        print_student(stu);

    }

    // print each student from 1 to 4

    list_for_each_entry_reverse(stu, &stu_head, list)

    {

        print_student(stu);

    }

 

    // delete a entry stu2

    list_del(&stu2->list);

    list_for_each_entry(stu, &stu_head, list)

    {

        print_student(stu);

    }

 

    // replace stu3 with stu2

    list_replace(&stu3->list, &stu2->list);

    list_for_each_entry(stu, &stu_head, list)

    {

        print_student(stu);

    }

 

    return 0;

}

 

static void testlist_exit(void)

{

    printk(KERN_ALERT "*************************
");

    printk(KERN_ALERT "testlist is exited!
");

    printk(KERN_ALERT "*************************
");

}

 

void print_student(struct student *stu)

{

    printk (KERN_ALERT "======================
");

    printk (KERN_ALERT "id  =%d
", stu->id);

    printk (KERN_ALERT "name=%s
", stu->name);

    printk (KERN_ALERT "======================
");

}

 

module_init(testlist_init);

module_exit(testlist_exit);

  

Makefile:

obj-m += testlist.o

 

#generate the path

CURRENT_PATH:=$(shell pwd)

#the current kernel version number

LINUX_KERNEL:=$(shell uname -r)

#the absolute path

LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)

#complie object

all:

    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules

    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned

#clean

clean:

    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

  

安装,卸载内核模块以及查看内核模块的运行结果:

insmod testlist.ko

rmmod testlist

dmesg | tail -100

2.hash 链表

hash链表的结构和双链表一样,也是内嵌到需要作为hash链表表元素的结构中。不同的是,双链表的表头和表元素是用相同的结构表示的(struct list_head),hash链表使用了不同的结构.

2.1 hash链表结构

hash链表代码位于include/linux/list.h中,3.0内核中将其数据结构定义放在了include/linux/types.h中
哈希表的数据结构定义如图:

 

struct hlist_head

{

    struct hlist_node *first;

}

struct hlist_node        

    struct hlist_node *next,**pprev;

}

观察hash链表的组织形式,以及head node 和node定义,其中有两个关注点需要注意.

a. hash表头:struct hlist_head{ struct hlist_node *first;};表头里面只存放一个hlist_node的指针,指向链表。

b. hash节点:struct hlist_node{struct hlist_node *next; struct hlist_node **pprev;};有两个指针,所以链表是双链表。但和一般的双链表又有点不一样,next自然是指向链表的下一个节点,但pprev则不是指向当前节点的前一个节点,而是指向当前节点的前一个节点的next指针。所以ppre是二级指针。

 

思考:为什么采用单向链表,即头结点中没有prev变量?
散列表的目的是为了方便快速的查找,所以散列表通常是一个比较大的数组,否则“冲突”的概率会非常大,这样就失去了散列表的意义。如何来做到既能维护一张大表,又能不占用过多的内存呢?此时只能对于哈希表的每个entry(表头结点)它的结构体中只能存放一个指针。【简言之,节省空间】

思考:*****为什么要采用pprev,而不采用一级指针?********
由于hlist不是一个完整的循环链表,在list中,表头和结点是同一个数据结构,直接用prev是ok的。在hlist中,表头中没有prev,只有一个first。
1> 为了能统一地修改表头的first指针,即表头的first指针必须修改指向新插入的结点,hlist就设计了pprev. hlist结点的pprev不再是指向前一个结点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是first)指针,从而在表头插入的操作中可以通过一致的node->pprev访问和修改前结点的next(或first)指针。
2>还解决了数据结构不一致,hlist_node巧妙的将pprev指向上一个节点的next指针的地址,由于hlist_head和hlist_node指向的下一个节点的指针类型相同,就解决了通用性。

2.2 hash链表常用操作

接下来的内容只涉及hash链表的API接口应用,具体实现比较简单,阅读内核代码即可.

对哈希表头结点进行初始化

实际上,struct hlist_head只定义了链表结点,并没有专门定义链表头,可以使用如下三个宏
#define HLIST_HEAD_INIT { .first = NULL}
#define HLIST_HEAD(name) struct hlist_head name = {.first = NULL}
#define INIT_HLIST_HEAD(ptr)   ((ptr->first)=NULL))
1>name 为结构体 struct hlist_head{}的一个结构体变量。
2>HLIST_HEAD_INIT 宏只进行初始化
Eg: struct hlist_head my_hlist = HLIST_HEAD_INIT
调用HLIST_HEAD_INIT对my_hlist哈希表头结点只进行初始化,将表头结点的fist指向空。
3>HLIST_HEAD(name)函数宏既进行声明并且进行初始化。
Eg:  HLIST_HEAD(my_hlist);
调用HLIST_HEAD函数宏对my_hlist哈希表头结点进行声明并进行初始化。将表头结点的fist指向空。
4>HLIST_HEAD宏在编译时静态初始化,还可以使用INIT_HLIST_HEAD在运行时进行初始化
Eg:
INIT_HLIST_HEAD(&my_hlist);
调用INIT_HLIST_HEAD俩将my_hlist进行初始化,将其first域指向空即可。

l  对哈希表结点进行初始化

1>Linux 对哈希表结点初始化提供了一个接口:
static iniline void INIT_HLIST_NODE(struct hlist_node *h)
(1) h:为哈希表结点
2>实现:
static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
h->next = NULL;
       h->pprev = NULL;

改内嵌函数实现了对struct hlist_node 结点进行初始化操作,将其next域和pprev都指向空,实现其初始化操作。

l  判断哈希链表是否为空

1>function:函数判断哈希链表是否为空,如果为空则返回1.否则返回0
2>函数接口:
static inline  int hlist_empty(const struct hlist_head *h)
h:指向哈希链表的头结点。
3>函数实现:
static inline  int hlist_empty(const struct hlist_head *h)
{
return !h->first;
}
通过判断头结点中的first域来判断其是否为空。如果first为空则表示该哈希链表为空。

l  判断节点是否在hash表中

1>function:判断结点是否已经存在hash表中。
2>函数接口:
static inline int hlist_unhashed(const struct hlist_node *h)
h:指向哈希链表的结点
3>函数实现:
static inline int hlist_unhashed(const struct hlist_node *h)
{
return !h->pprev
}
通过判断该结点的pprev是否为空来判断该结点是否在哈希链表中。 h->pprev等价于h节点的前一个节点的next域。如果前一个节点的next域为空,说明 改节点不在哈希链表中。

l  哈希链表的删除操作

1>function:将一个结点从哈希链表中删除。
2>函数接口:
static inline void hlist_del(struct hlist_node *n)
Ø n: 指向hlist的链表结点
static inline void hlist_del_init(struct hlist_node *n)
Ø n: 指向hlist的链表结点

l  添加哈希结点

1>function:将一个结点添加到哈希链表中。
hlist_add_head:将结点n插在头结点h之后。
hlist_add_before:将结点n插在next结点的前面(next在哈希链表中)
hlist_add_after:将结点next插在n之后(n在哈希链表中)

 2、linux 内核kfifo

kfifo设计的非常巧妙,代码很精简,对于入队和出对处理的出人意料。首先看一下kfifo的数据结构:

struct kfifo {
    unsigned char *buffer;     /* the buffer holding the data */
    unsigned int size;         /* the size of the allocated buffer */
    unsigned int in;           /* data is added at offset (in % size) */
    unsigned int out;          /* data is extracted from off. (out % size) */
    spinlock_t *lock;          /* protects concurrent modifications */
};

 kfifo实现为循环队列,首先,先看看初始化代码.

8 struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock)
 69 {       
 70         unsigned char *buffer;
 71         struct kfifo *ret;
 72         
 73         /*
 74          * round up to the next power of 2, since our 'let the indices
 75          * wrap' technique works only in this case.
 76          */
 77         if (!is_power_of_2(size)) {
 78                 BUG_ON(size > 0x80000000);
 79                 size = roundup_pow_of_two(size);
 80         }
 81         
 82         buffer = kmalloc(size, gfp_mask);
 83         if (!buffer)
 84                 return ERR_PTR(-ENOMEM);
 85         
 86         ret = kfifo_init(buffer, size, gfp_mask, lock);
 87         
 88         if (IS_ERR(ret))
 89                 kfree(buffer);
 90         
 91         return ret;
 92 }
 93 EXPORT_SYMBOL(kfifo_alloc);




216 /* is x a power of 2? */
217 #define is_power_of_2(x)        ((x) != 0 && (((x) & ((x) - 1)) == 0))

  

这里值得一提的是,kfifo->size的值总是在调用者传进来的size参数的基础上向2的幂扩展,这是内核一贯的做法。这样的好处不言而喻--对kfifo->size取模运算可以转化为与运算,如下:

kfifo->in % kfifo->size 可以转化为 kfifo->in & (kfifo->size – 1)

在宏定义is_power_of_2中,使用size & (size – 1)来判断size 是否为2幂,如果条件为真,则表示size不是2的幂,然后调用roundup_pow_of_two将之向上扩展为2的幂。 这些都是很常用的技巧,只不过大家没有将它们结合起来使用而已,下面要分析的__kfifo_put和__kfifo_get则是将kfifo->size的特点发挥到了极致。

__kfifo_put 和 __kfifo_get 函数实现机制是一样的,下面以__kfifo_put函数为例进行分析.

119 unsigned int __kfifo_put(struct kfifo *fifo,
120                         const unsigned char *buffer, unsigned int len)
121 {
122         unsigned int l;
123 
124         len = min(len, fifo->size - fifo->in + fifo->out);
125 
126         /*
127          * Ensure that we sample the fifo->out index -before- we
128          * start putting bytes into the kfifo.
129          */
130         
131         smp_mb();
132 
133         /* first put the data starting from fifo->in to buffer end */
134         l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));
135         memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);
136 
137         /* then put the rest (if any) at the beginning of the buffer */
138         memcpy(fifo->buffer, buffer + l, len - l);
139 
140         /*
141          * Ensure that we add the bytes to the kfifo -before-
142          * we update the fifo->in index.
143          */
144 
145         smp_wmb();
146 
147         fifo->in += len;
148 
149         return len;
150 }
151 EXPORT_SYMBOL(__kfifo_put);

  向 kfifo 中 put数据的时候,分别调用了两次min() 函数。第一次调用 min函数,是比较期望放入kfifo中数据长度len和kfifo中剩余空间大小两者的最小值,将最小值放入len中,这个比较好理解。第二次调用min值和两次memcpy进行数据拷贝,对应着两种数据存放情况,如下图:

a. in的位置在out之后:

b. in 的位置在out之前:

之所以会出现上面两种情况,是因为kernel中的kfifo是循环使用的,需要注意的是,不管怎么样,in的值总是大于out的。同样,不出意料,在include/kfifo.h中对提供了加锁的函数kfifo_put.我还未遇到内核调用  kfifo的列子,一下调用实例摘自网络:

#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");
struct student
{
    int id;
    char* name;
};

static void print_student(struct student*);

static int testkfifo_init(void)
{
    struct kfifo *fifo;
    struct student *stu1, *stu2, *stu3, *stu4;
    struct student *stu_tmp;
    char* c_tmp;
    int i;
    // !!importent  init a unlocked lock
    spinlock_t sl = SPIN_LOCK_UNLOCKED;

    // init kfifo
    fifo = kfifo_alloc(4*sizeof(struct student), GFP_KERNEL, &sl);
    
    stu1 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb1";
    kfifo_put(fifo, (char *)stu1, sizeof(struct student));

    stu2 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu2->id = 1;
    stu2->name = "wyb2";
    kfifo_put(fifo, (char *)stu2, sizeof(struct student));

    stu3 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu3->id = 1;
    stu3->name = "wyb3";
    kfifo_put(fifo, (char *)stu3, sizeof(struct student));

    stu4 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu4->id = 1;
    stu4->name = "wyb4";
    kfifo_put(fifo, (char *)stu4, sizeof(struct student));

    c_tmp = kmalloc(sizeof(struct student), GFP_KERNEL);
    printk(KERN_ALERT "current fifo length is : %d
", kfifo_len(fifo));
    for (i=0; i < 4; i++) {

        kfifo_get(fifo, c_tmp, sizeof(struct student));
        stu_tmp = (struct student *)c_tmp;
        print_student(stu_tmp);
        printk(KERN_ALERT "current fifo length is : %d
", kfifo_len(fifo));
    }
    
    printk(KERN_ALERT "current fifo length is : %d
", kfifo_len(fifo));
    kfifo_free(fifo);
    kfree(c_tmp);
    return 0;
}

static void print_student(struct student *stu)
{
    printk(KERN_ALERT "=========================
");
    print_current_time(1);
    printk(KERN_ALERT "id = %d
", stu->id);
    printk(KERN_ALERT "name = %s
", stu->name);
    printk(KERN_ALERT "=========================
");
}

static void testkfifo_exit(void)
{
    printk(KERN_ALERT "*************************
");
    printk(KERN_ALERT "testkfifo is exited!
");
    printk(KERN_ALERT "*************************
");
}

module_init(testkfifo_init);
module_exit(testkfifo_exit);

  

原文地址:https://www.cnblogs.com/3me-linux/p/6423207.html