Linux内核设计与实现——读书笔记(4)内核数据结构

  

1、链表

1.1、内核中链表的结构

  Linux的链表代码在<linux/list.h>中:

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

  通常将链表的结构放在另一个结构体中,使用 list_entity() 可以返回包含list_head的父类型结构体; list_entity() 的定义如下所示:

#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) );})

  使用container_of() 宏,可以很方便地找到包含的父结构。因为一个给定结构的变量在编译后地址和大小就已经确定下来了。container_of参考链接.

1.2、定义和初始化一个链表

  链表需要在使用之前初始化,通常把链表定义在一个结构体中:

struct list_test{
	unsigned int length;
	unsigned int value;
	struct list_head list;
}

  使用INIT_LIST_HEAD() 对已经定义的链表进行初始化:

	struct list_test *mylist;
	mylist = kmalloc(sizeof(*mylist),GFP_KERNEL);
	mylist->length = 10;
	mylist->value = 10;
	INIT_LIST_HEAD(&mylist->list);

  或者使用LIST_HEAD_INIT 在编译器静态创建:

struct list_test mylist = {
	.length = 10,
	.value = 10,
	.list = LIST_HEAD_INIT(mylist.list),
};

  使用LIST_HEAD() 可以快速创建并初始化一个链表头:

#define LIST_HEAD(name) \
    struct list_head name = LIST_HEAD_INIT(name)

1.3、链表操作

  内核提供了一组操作链表的函数,这些函数都是以内联的形式在<linux/list.h>中实现

1.3.1、向链表中增加一个节点

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

  list_add() 函数向head节点后插入一个new节点。
  注意!!!:默认是采用头插法往链表中添加节点,并且链表默认是环状的

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

  list_add_tail() 函数向head节点前插入一个new节点。

1.3.2、从链表中删除一个节点

static inline void list_del(struct list_head *entry)

  list_del() 函数不会释放entry占用的内存,仅仅是将entry从链表中移走。

static inline void list_del_init(struct list_head *entry)

  list_del_init() 从链表中删除entry节点,并重新初始化entry节点。

1.3.3、移动和合并节点

static inline void list_move(struct list_head *list, struct list_head *head)

  list_move() 函数从一个链表中移除list节点,然后将其添加到另一个链表的head节点后面。

static inline void list_move_tail(struct list_head *list,
                  struct list_head *head)

  list_move_tail() 函数从一个链表中移除list节点,然后将其添加到另一个链表的head节点前面。

static inline int list_empty(const struct list_head *head)

  list_empty() 检查链表是否为空,为空返回非0,空返回0。

static inline void list_splice(struct list_head *list, struct list_head *head)

  list_splice() 函数将list链表插入到另一个链表的head节点后面。这里lis节点会作为表头被丢弃,但是list节点中的数据没有改变

static inline void list_splice_init(struct list_head *list,
                    struct list_head *head)

  list_splice_init() 函数将list链表插入到另一个链表的head节点后面。这里lis节点会作为表头被丢弃,但是list节点被初始化,指向自身

1.3.4、遍历链表

1.3.4.1、list_for_each
#define list_for_each(pos, head) \
    for (pos = (head)->next; prefetch(pos->next), pos != (head); \
            pos = pos->next)

  遍历链表使用list_for_each()宏 ,该宏接受两个参数,第一个为临时的链表结构指针,第二个为要遍历的链表头。宏展开后为一个for语句,使用时候要加{}括号。通常我们要遍历的不是链表,而是包含链表的结构,具体的操作如下:

struct list_test *q = NULL;		//带链表的结构的指针
struct list_head *p = NULL;		//链表指针

	list_for_each(p,&mylist.list){
		/* 获取包含链表的list_test结构的地址 */
		q = list_entry(p,struct list_test,list);
	}
1.3.4.2、正向遍历list_for_each_entry

  还可以使用list_for_each_entry() 宏,这个宏将list_entry包含在其中。

#define list_for_each_entry(pos, head, member)              \
    for (pos = list_entry((head)->next, typeof(*pos), member);  \
         &pos->member != (head);    \
         pos = list_entry(pos->member.next, typeof(*pos), member))

  list_for_each_entry() 宏的使用方式如下:

struct list_test *q  = NULL;		//链表指针
	list_for_each_entry(q,&mylist.list,list){
		/* q指向每一个遍历到的list_test结构 */
	}

在这里插入图片描述

1.3.4.3、反向遍历list_for_each_entry_reverse

  使用list_for_each_entry_reverse() 宏可以反向遍历链表。使用反向遍历链表的可能原因:性能和顺序。

1.3.4.4、遍历时删除

  list_for_each_entry_safe() 宏并没有删除功能,只是保证删除后,在下一次遍历时可以安全地指向下一个元素。定义如下:

#define list_for_each_entry_safe(pos, n, head, member)          \
    for (pos = list_entry((head)->next, typeof(*pos), member),  \
        n = list_entry(pos->member.next, typeof(*pos), member); \
         &pos->member != (head);                    \
         pos = n, n = list_entry(n->member.next, typeof(*n), member))

  pos为临时的链表结构指针;
  n和pos的结构一样,n是用来临时存放被删除的元素的next和prev指针。因为有这个参数的存在,所以可以保证正常的遍历下去
  书里提供了list_for_each_entry_safe() 的使用例子:
在这里插入图片描述
  这个例子遍历删除了inotifu_watch链表中的所有项。

1.3.4.5、反向遍历时删除

  list_for_each_entry_safe_reverse() 参考上面的。

2、队列

  Linux通用队列称为kfifo,在 <linux/kfifo.h> 文件中定义。

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 */
};

2.1、创建队列

  创建kfifo队列的方式有静态和动态两种,动态创建使用如下函数:

struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock)

  kfifo_alloc() 函数的参数和书里的不同,具体看内核中源文件的定义。书里的kfifo_alloc() 函数如下所示:
在这里插入图片描述
  使用kfifo_alloc() 函数实现动态分配的方式如下(摘自内核源码):

	struct kfifo *fifo;		//创建kfifo结构指针
	spinlock_t *lock;		//创建自旋锁指针。可以不使用自旋锁,那么在传递参数给kfifo_alloc的时候第三个参数直接给NULL
	fifo = kfifo_alloc(PIGE_SIZE, GFP_KERNEL,lock);		
    if (IS_ERR(fifo)) {
        printk("kfifo_alloc failed\n");
        error = PTR_ERR(fifo);
        goto xxx;
    }    

  也可以自己指定缓冲区动态分配给kfifo。使用如下函数:

struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size,
             gfp_t gfp_mask, spinlock_t *lock)

  buffer 参数为我们指定的缓存区;
  令人操蛋的是,这个函数也和书里的不同(反正还是看具体内核的实现):
在这里插入图片描述
  size 参数必须是2的幂
  另外,还有静态声明kfifo的宏,但是我在内核版本2.6.24中没有找到:
在这里插入图片描述

2.2、推入队列数据

在这里插入图片描述
  emmmm,这个函数在2.6.24版本里没有实现,是用 __kfifo_put() 函数实现了推入功能,参数都是一样的:

/* __kfifo_put - puts some data into the FIFO, no locking version */
unsigned int __kfifo_put(struct kfifo *fifo,
             unsigned char *buffer, unsigned int len)

2.3、摘取队列数据

在这里插入图片描述

  kfifo_out() 函数在2.6.24内核中为kfifo_get()

/* __kfifo_put - puts some data into the FIFO, no locking version  */
static inline unsigned int kfifo_get(struct kfifo *fifo,
                     unsigned char *buffer, unsigned int len)

  类似kfifo_out_peek() 的函数就没有找到。

2.4、获取队列长度

在这里插入图片描述

  在2.6.24内核中,只实现了kfifo_len() 函数,函数的作用是返回已推入数据的大小。

2.5、重置和撤销队列

static inline void kfifo_reset(struct kfifo *fifo)

  使用kfifo_reset() 可以重置(清除)队列中所有内容。

void kfifo_free(struct kfifo *fifo)

  如果使用kfifo_alloc() 函数分配队列空间,则要是用kfifo_free() 函数释放。

3、 映射

  Linux提供了一种映射功能,可以映射一个UID到一个指针上,通过这个UID可以快速访问指针。

struct idr_layer {
    unsigned long        bitmap; /* A zero bit means "space here" */
    struct idr_layer    *ary[1<<IDR_BITS];
    int          count;  /* When zero, we can release it */
};

struct idr {
    struct idr_layer *top;
    struct idr_layer *id_free;
    int       layers;
    int       id_free_cnt;
    spinlock_t    lock;
};

3.1、初始化一个idr

void idr_init(struct idr *idp)
{                                                                                                                                                                                                                                                                             
    init_id_cache();
    memset(idp, 0, sizeof(struct idr));
    spin_lock_init(&idp->lock);
}

struct idr myidr;
idr_init(&myidr);

3.2、分配一个新的UID

  告诉idr你需要分配新的UID,idr可能要调整后备树的大小。使用idr_pre_get() 函数,idr_pre_get() 函数将在需要时调整idp参数指向的idr的大小。:

int idr_pre_get(struct idr *idp, gfp_t gfp_mask)

  idr_pre_get() 函数成功返回1,失败返回0;

  使用idr_get_new() 函数分配一个UID将其加到idr中,并且将UID关联到指定的指针上:

int idr_get_new(struct idr *idp, void *ptr, int *id)

  idr_get_new() 函数成功返回0,得到的UID存于参数id上。失败返回非0错误码。错误码 -EAGAIN ,表示需要再一次调用idr_get_new() 函数,错误码 -ENOSPC 表示idr已满。
  例子:
在这里插入图片描述

  使用函数idr_get_new_above() 可以限制返回的最小UID。

int idr_get_new_above(struct idr *idp, void *ptr, int starting_id, int *id);

  idr_get_new_above() 确保返回的UID大于或等于starting_id。这样可以确保UID不会被重用。

3.3、查找UID关联的指针

  使用idr_find() 函数,函数的定义很简单:

void *idr_find(struct idr *idp, int id);

  传输参数为指定的idr结构指针和id,函数会返回相应的指针。如果出错,返回NULL,所以,不要将空指针映射到UID中

3.4、删除UID

  从idr中删除UID使用idr_remove() 函数:

void idr_remove(struct idr *idp, int id);

  成功则将与id关联的指针从映射中删除,由于是void型返回,失败没有提示。

3.5、撤销idr

  调用idr_destroy() 函数可以释放idr中未使用的内存,它并不释放当前分配给UID使用的内存。

void idr_destroy(struct idr *idp)

  通常,内核不会撤销idr,除非关闭和卸载,而且在没有其他用户(也就没有更多UID)时删除。
  如果要释放idr占用的所有内存,可以先使用idr_remove_all() 函数删除所有UID后,在使用idr_destroy() 函数。
  

4、总结

  如果遍历数据是主要操作、存储的大小不确定,则使用链表;
  如果符合生产者/消费者之类结构,则使用队列;
  处理发给用户空间的描述符,使用映射。
  
  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_zhangsq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值