Linux内核链表:结点的插入、删除以及链表的遍历
1.
Linux内核链表的核心思想是:在用户自定义的结构A中声明list_head类型的成员p,这样每个结构类型为A的变量a中,都拥有同样的成员p,如下:
struct
A{
int
property;
struct
list_head p;
}
其中,list_head结构类型定义如下:
struct
list_head {
struct
list_head *next,*prev;
};
2. 下面这段程序,完成结点的插入、删除以及链表的遍历:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Leonard");
MODULE_DESCRIPTION("List Module");
MODULE_ALIAS("A list test module");
struct mylist{
char data[100];
int num;
struct list_head list;
};
struct list_head *entry;
struct list_head mylist_head;
struct mylist *list_k;
struct mylist *list_k_tmp;
int mylist_init()
{
int i;
INIT_LIST_HEAD(&mylist_head);
list_k = kmalloc((sizeof(struct
mylist))*8,GFP_KERNEL);
memset(list_k,0,sizeof(struct mylist)*8);
for(i=0;i<8;i++)
{
sprintf(list_k[i].data,"Leonard%d",i+1);
list_k[i].num = i+1;
if(i<4)
list_add(&(list_k[i].list),&mylist_head);
}
list_add(&(list_k[4].list),&(list_k[0].list));
list_add(&(list_k[5].list),&(list_k[1].list));
list_add(&(list_k[6].list),&(list_k[2].list));
list_add_tail(&(list_k[7].list),&mylist_head);
//list_add(&(list_k[7].list),&mylist_head);
list_for_each(entry,&mylist_head) {
list_k_tmp =
list_entry(entry,struct mylist,list);
printk("<0>Num:%d Data:%s
\n",list_k_tmp->num,list_k_tmp->data);
}
return 0;
}
void mylist_exit()
{
int i;
for(i=0;i<8;i++)
list_del(&(list_k[i].list));
kfree(list_k);
printk("<0>Module Exit!List Delete
OK!\n");
}
module_init(mylist_init);
module_exit(mylist_exit);
分析:
语句 list_k = kmalloc((sizeof(struct
mylist))*8,GFP_KERNEL);为链表分配合适的内存空间,
然后开始插入链表节点:
for(i=0;i<8;i++)
{
sprintf(list_k[i].data,"Leonard%d",i+1);
list_k[i].num = i+1;
if(i<4)
list_add(&(list_k[i].list),&mylist_head);
}
list_add(&(list_k[4].list),&(list_k[0].list));
list_add(&(list_k[5].list),&(list_k[1].list));
list_add(&(list_k[6].list),&(list_k[2].list));
list_add_tail(&(list_k[7].list),&mylist_head);
对上面稍加分析可知,链表节点内容为:4,3,7,2,6,1,5,8。运行结果的确如此:
注意在mylist_exit()中,删除节点用的是for循环,没有问题。
但是如果用list_for_each(),使用不当容易导致系统死机,具体分析见下一小节。
3.遍历时节点删除
在[include/linux/list.h]中,list_for_each()宏是这么定义的:
#define list_for_each(pos, head) \
for (pos = (head)->next,
prefetch(pos->next); pos != (head); \
pos = pos->next, prefetch(pos->next))
它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。
这个实验需要思考的问题是,在链表遍历删除的时候,稍不小心就容易引起系统死机。
前面介绍了用于链表遍历的宏,是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。也就是将该节点的prev指向next,next指向prev,将本节点跨过去然后将本节点的next和prev都赋值为一个无意义的值。因此,调用者可以自己缓存prev指针使遍历操作能够连贯起来,这里如果想让循环继续下去的话,需要先将本节点的prev节点暂存起来,然后执行list_del,然后再将prev节点恢复成当前节点。正确的方法应该改为如下:
void mylist_exit()
{
//int i;
//for(i=0;i<8;i++)
//list_del(&(list_k[i].list));
struct list_head *tmp1;
list_for_each(entry,&mylist_head)
{
list_k_tmp =
list_entry(entry,struct mylist,list); tmp1 =
entry->prev;
list_del(entry);
entry =
tmp1; printk("<0>Delete:
Num:%d Data:%s \n",list_k_tmp->num,list_k_tmp->data);
}
kfree(list_k);
printk("<0>Module Exit!List Delete
OK!\n");
}
这样就不会出现死循环的问题了。
此外,为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos,
n, head)、list_for_each_entry_safe(pos, n, head,
member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。
参考文章: