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