Linux内核中的双向链表struct list_head
一、双向链表list_head
Linux内核驱动开发会经常用到Linux内核中经典的双向链表list_head,以及它的拓展接口和宏定义:list_add、list_add_tail、list_del、list_entry、list_for_each等。
在内核源码中,list_head结构体的定义在文件source_code/include/linux/types.h文件中,结构体定义如下:
struct list_head { struct list_head *next, *prev; };
通过这个结构体开始构建链表,然后插入、删除节点,遍历整个链表等,内核已经提供好了现成的调用接口。
1、创建链表
Linux内核中提供了下面的接口来进行双向链表初始化:
#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name)
可以通过LIST_HEAD(my_lsit)来进行一个双向链表的初始化,初始化后,my_list的prev和next指针都将指向自己,如下:
struct list_head my_list = {&my_list, &my_list};
正常的链表都是为了遍历结构体中其它有意义的字段而创建的,但是上面定义的my_list中只有prev和next指针,因此没有实际意义的字段数据,所以,需要创建一个宿主结构,然后在此结构中嵌套my_list字段,宿主结构中又有其它的字段,例如:
struct my_task_lsit { int val; struct list_head my_list; }
然后通过下面的code创建出第一个节点:
struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(first_task.my_list) };
这样子,宿主结构中的my_list的prev和next指针分别指向了自己,如下所示:
2、添加节点
在Linux内核中已经提供了添加双向链表节点的函数接口了。
(1)list_add()函数
该函数的接口代码如下所示:
/** * list_add - add a new entry * @new: new entry to be added * @head: list head to add it after * * Insert a new entry after the specified head. * This is good for implementing stacks. */ static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); }
根据注释可以知道,是在链表头head的后方插入一个新的节点,而且该接口函数能利用实现堆栈,同时list_add函数再调用__list_add函数,函数代码如下所示:
/* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); //prev->next = new; }
函数实现的功能其实就是在head链表头和链表头后的第一个节点之间插入一个新节点,然后这个新的节点就变成了链表头后的第一个节点。
接下来,开始创建一个链表头head_task,并使用LIST_HEAD(head_task);进行初始化,如下所示:
然后创建实际的第一个节点:
struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(first_task.my_list) };
创建完成后,通过使用list_add接口将这个first_task节点插入到head_task之后:
list_add(&first_task.my_list, &head_task.my_list);
完成后,双向链表的指针指向如下所示:
然后继续创建第二个节点,同样把它插入到head_task之后:
struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(second_task.my_list) };
然后调用list_add()添加节点:
list_add(&second_task.my_list, &head_task.my_list);
插入后指针指向如下所示:
以此类推,每次插入一个新的节点,都是紧靠着head_task节点的,而之前插入的节点以此排序靠后,所以最后的哪个节点是第一个插入head_task的哪个节点,所以结论是:先来的节点靠后,而后来的节点靠前,也就是先进后出,后进先出,类似于栈的结构。
(2)list_add_tail()函数
list_add接口是从链表头head后添加节点,同样,Linux内核也提供了从链表尾处向前添加节点的接口list_add_tail,具体实现代码如下所示:
/** * list_add_tail - add a new entry * @new: new entry to be added * @head: list head to add it before * * Insert a new entry before the specified head. * This is useful for implementing queues. */ static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
从注释可以知道,该接口函数是从一个链表头前面插入一个节点,并且该接口适合于队列的实现,__list_add()函数展开如下所示:
/* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); //prev->next = new; }
由代码可以知道,list_add_tail就相当于在链表头的前方依次插入新的节点(也可以理解为在链表尾开始插入新的节点,head节点就是尾节点,保持不变)。
接下来,先开始创建一个链表头(链表尾),同时调用LIST_HEAD进行初始化:
struct my_task_list head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&head_task.my_list) };
结构图形如下所示:
接下来,创建第一个节点first_task,并且调用list_add_tail函数将节点插入到链表头前面:
struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(&first_task.my_list) }; list_add_tail(&first_task.my_list, &head_task.my_list);
数据结构图形如下所示:
然后,继续创建第二个节点second_task,并且调用list_add_tail函数将节点添加到链表里面
struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(&second_task.my_list) }; list_add_tail(&second_task.my_list, &head_task.my_list);
添加完成后,数据结构图如下所示:
依次类推,每次插入的新节点都是紧靠着head的表尾,而插入的第一个节点first_task排在了第一位,second_task排在了第二位,也就是:先插入的节点排在前面,后插入的节点排在后面,也就是先进先出,后进后出,类似于队列的结构。
3、删除节点
Linux内核里面同样提供了删除双向链表节点的的接口list_del()函数,代码如下所示:
static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; }
list_del()函数里面继续调用了__list_del_entry()接口,如下:
/** * list_del - deletes entry from list. * @entry: the element to delete from the list. * Note: list_empty() on entry does not return true after this, the entry is * in an undefined state. */ static inline void __list_del_entry(struct list_head *entry) { if (!__list_del_entry_valid(entry)) return; __list_del(entry->prev, entry->next); }
__list_del()的函数如下所示:
/* * Delete a list entry by making the prev/next entries * point to each other. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; WRITE_ONCE(prev->next, next); }
利用list_del()就可以删除掉链表中的任意节点,但是需要注意的是,前提条件是这个节点是已知的,也就是在链表中是真实存在的,其节点的prev和next指针都不为NULL。
接下来,使用下面的代码创建一个简单的带头节点,包含三个元素节点的链表:
/* 定义带头节点空链表 */ struct my_task_list head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&head_task.my_list) }; /* 定义并插入第一个节点 */ struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(&first_task.my_list) }; list_add_tail(&first_task.my_list, &head_task.my_list); /* 定义并插入第二个节点 */ struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(&second_task.my_list) }; list_add_tail(&second_task.my_list, &head_task.my_list); /* 定义并插入第三个节点 */ struct my_task_list third_task = { .val = 3, .my_list = LIST_HEAD_INIT(&third_task.my_list) }; list_add_tail(&third_task.my_list, &head_task.my_list);
构建出来的双向链表如下所示:
接下来,使用list_del()函数接口将某个元素节点进行删除,代码如下:
/* 将链表的第二个元素节点进行删除 */ list_del(&second_task.my_list);
代码执行后,链表的最终结构如下所示:
以上就是Linux内核提供的list_del()进行双向链表list_head删除链表节点的功能。
4、链表的遍历
Linux内核里面同样提供了对list_head链表进行遍历的接口,分别有两种情况,第一种是同过next指针,从前向后进行遍历,函数接口为list_for_each,如下所示:
/** * list_for_each - iterate over a list * @pos: the &struct list_head to use as a loop cursor. * @head: the head for your list. */ #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next)
该接口为一个宏定义,有两个参数。第一个参数为struct list_head的循坏因子,第二个参数为链表头。另外一个接口是通过prev指针进行遍历的,如下:
/** * list_for_each_prev - iterate over a list backwards * @pos: the &struct list_head to use as a loop cursor. * @head: the head for your list. */ #define list_for_each_prev(pos, head) \ for (pos = (head)->prev; pos != (head); pos = pos->prev)
传入参数和第一个接口类似。
5、节点替换
Linux内核里面提供了对链表的节点进行替换的接口,函数的实现如下所示:
/** * list_replace - replace old entry by new one * @old : the element to be replaced * @new : the new element to insert * * If @old was empty, it will be overwritten. */ static inline void list_replace(struct list_head *old, struct list_head *new) { new->next = old->next; new->next->prev = new; new->prev = old->prev; new->prev->next = new; }
list_replace接口的参数有两个,分别是旧的节点,也就是需要被替换的节点,第二个参数是新的节点,也就是替换的的节点,其实就是通过改变prev和next指针的指向从而达到替换的效果。
接下来,使用下面的代码构建一个带头节点,两个元素节点的双向链表:
/* 定义带头节点空链表 */ struct my_task_list head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&head_task.my_list) }; /* 定义并插入第一个节点 */ struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(&first_task.my_list) }; list_add_tail(&first_task.my_list, &head_task.my_list); /* 定义并插入第二个节点 */ struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(&second_task.my_list) }; list_add_tail(&second_task.my_list, &head_task.my_list);
构建后的双向链表结构如下所示:
接下来,使用下面的代码定义一个新的元素节点,并对second_task节点进行替换:
/* 定义新节点并替换掉原第二个节点 */ struct my_task_list new_second_task = { .val = 200, .my_list = LIST_HEAD_INIT(&new_second_task.my_list) }; list_replace(&second_task.my_list, &new_second_task.my_list);
替换元素节点后的双向链表如下所示:
以上就是使用list_replace()进行双向链表元素节点替换的功能。
6、节点移位
Linux内核中提供了对双向链表节点进行移位的函数接口,通过该接口可以实现将一个节点从一个链表移动到另外一个链表,主要分为两种类型,分别是头部搬移和尾部搬移,搬移的本质其实就是先从一个链表中删除某个节点,然后添加插入到另外一个链表,相关函数代码如下所示:
/** * list_move - delete from one list and add as another's head * @list: the entry to move * @head: the head that will precede our entry */ static inline void list_move(struct list_head *list, struct list_head *head) { __list_del_entry(list); list_add(list, head); } /** * list_move_tail - delete from one list and add as another's tail * @list: the entry to move * @head: the head that will follow our entry */ static inline void list_move_tail(struct list_head *list, struct list_head *head) { __list_del_entry(list); list_add_tail(list, head); }
7、判断是否最后一个节点
/** * list_is_last - tests whether @list is the last entry in list @head * @list: the entry to test * @head: the head of the list */ static inline int list_is_last(const struct list_head *list, const struct list_head *head) { return list->next == head; }
通过该接口可以判断某个节点是否是链表的最后的一个节点。
8、判断链表是否为空
/** * list_empty - tests whether a list is empty * @head: the list to test. */ static inline int list_empty(const struct list_head *head) { return READ_ONCE(head->next) == head; }
通过该接口可以判断某个链表是否为空链表。
9、将链表旋转到左边
在Linux内核中提供了链表旋转的接口,使用list_rotate_left()函数可以实现,该接口将链表的第一个节点移动到链表的尾部,该函数的实现如下所示:
/** * list_rotate_left - rotate the list to the left * @head: the head of the list */ static inline void list_rotate_left(struct list_head *head) { struct list_head *first; if (!list_empty(head)) { first = head->next; list_move_tail(first, head); } }
接下来使用下面的代码构建一个双向链表:
/* 定义带头节点空链表 */ struct my_task_list head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&head_task.my_list) }; /* 定义并插入第一个节点 */ struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(&first_task.my_list) }; list_add(&first_task.my_list, &head_task.my_list); /* 定义并插入第二个节点 */ struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(&second_task.my_list) }; list_add(&second_task.my_list, &head_task.my_list); /* 定义并插入第三个节点 */ struct my_task_list third_task = { .val = 3, .my_list = LIST_HEAD_INIT(&third_task.my_list) }; list_add(&third_task.my_list, &head_task.my_list);
链表效果如下所示:
使用下面代码进行链表旋转:
list_rotate_left(&head_task.my_list);
最终链表效果如下:
10、判断链表是否只有一个元素节点
/** * list_is_singular - tests whether a list has just one entry. * @head: the list to test. */ static inline int list_is_singular(const struct list_head *head) { return !list_empty(head) && (head->next == head->prev); }
在Linux内核中,使用list_is_singular()可以判断某个链表是否只有一个元素节点。
11、链表一分为二
在Linux内核中,使用list_cut_position()函数接口,可以将某个链表一分为二,该接口的函数定义如下:
/** * list_cut_position - cut a list into two * @list: a new list to add all removed entries * @head: a list with entries * @entry: an entry within head, could be the head itself * and if so we won't cut the list * * This helper moves the initial part of @head, up to and * including @entry, from @head to @list. You should * pass on @entry an element you know is on @head. @list * should be an empty list or a list you do not care about * losing its data. * */ static inline void list_cut_position(struct list_head *list, struct list_head *head, struct list_head *entry) { if (list_empty(head)) return; if (list_is_singular(head) && (head->next != entry && head != entry)) return; if (entry == head) INIT_LIST_HEAD(list); else __list_cut_position(list, head, entry); }
在函数list_cut_position()中调用了__list_cut_position()来将链表一分为二,该函数的定义如下:
static inline void __list_cut_position(struct list_head *list, struct list_head *head, struct list_head *entry) { struct list_head *new_first = entry->next; list->next = head->next; list->next->prev = list; list->prev = entry; entry->next = list; head->next = new_first; new_first->prev = head; }
接下来,使用下面的代码构建一个双向链表:
/* 定义带头节点空链表 */ struct my_task_list head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&head_task.my_list) }; /* 定义并插入第一个节点 */ struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(&first_task.my_list) }; list_add(&first_task.my_list, &head_task.my_list); /* 定义并插入第二个节点 */ struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(&second_task.my_list) }; list_add(&second_task.my_list, &head_task.my_list); /* 定义并插入第三个节点 */ struct my_task_list third_task = { .val = 3, .my_list = LIST_HEAD_INIT(&third_task.my_list) }; list_add(&third_task.my_list, &head_task.my_list);
该链表构建后效果如下:
接下来使用下面的代码将链表一分为二:
/* 定义一个新的带头节点空链表 */ struct my_task_list new_head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&new_head_task.my_list) }; /* 将链表一分为二 */ list_cut_position(&new_head_task.my_list, &head_task.my_list, &third_task.my_list);
最终的链表拆分效果如下所示:
12、链表合并
在Linux内核中,提供了双向链表的合并接口,通过该接口可以将两个链表合并为一个链表,非常方便,由于可以从头部合并或者从尾部合并,因此Linux内核提供了两个函数接口,分别是list_splice()和list_splice_tail()。
首先是list_splice()函数,该函数的代码如下所示:
/** * list_splice - join two lists, this is designed for stacks * @list: the new list to add. * @head: the place to add it in the first list. */ static inline void list_splice(const struct list_head *list, struct list_head *head) { if (!list_empty(list)) __list_splice(list, head, head->next); }
可以看到,该函数调用了__list_splice()函数,并传入了三个参数,第一参数为新添加的链表,第二个参数为第一个链表要添加到的链表的头部,第三个参数为要添加到的链表的第一个节点,函数的定义如下所示:
static inline void __list_splice(const struct list_head *list, struct list_head *prev, struct list_head *next) { struct list_head *first = list->next; struct list_head *last = list->prev; first->prev = prev; prev->next = first; last->next = next; next->prev = last; }
接下来,对链表的合并进行简单演示,首先,需要构建两个简单的链表,代码如下所示:
/* 定义一个带头节点空链表 */ struct my_task_list head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&head_task.my_list) }; /* 定义并插入第一个节点 */ struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD_INIT(&first_task.my_list) }; list_add(&first_task.my_list, &head_task.my_list); /* 定义并插入第二个节点 */ struct my_task_list second_task = { .val = 2, .my_list = LIST_HEAD_INIT(&second_task.my_list) }; list_add(&second_task.my_list, &head_task.my_list); /* 定义并插入第三个节点 */ struct my_task_list third_task = { .val = 3, .my_list = LIST_HEAD_INIT(&third_task.my_list) }; list_add(&third_task.my_list, &head_task.my_list); /* 定义另外一个带头节点空链表 */ struct my_task_list new_head_task = { .val = 0, .my_list = LIST_HEAD_INIT(&new_head_task.my_list) }; /* 定义并插入一个节点到另外一个链表 */ struct my_task_list fourth_task = { .val = 4, .my_list = LIST_HEAD_INIT(&fourth_task.my_list) }; list_add(&fourth_task.my_list, &new_head_task.my_list); /* 定义并插入一个节点到另外一个链表 */ struct my_task_list fifth_task = { .val = 5, .my_list = LIST_HEAD_INIT(&fifth_task.my_list) }; list_add(&fifth_task.my_list, &new_head_task.my_list);
两个链表构建后,效果如下所示:
接下来,使用下面的代码将两个链表进行合并:
list_splice(&new_head_task.my_list, &head_task.my_list);
链表合并后的最终效果如下所示:
从上面的效果可以看到,要被合并的链表直接被并入到了另外一个链表的头部,有点类似于栈先进后出的结构,该函数list_splice()是专门为栈结构去设计的。
链表合并的另外一个函数是list_splice_tail(),该函数的代码如下:
/** * list_splice_tail - join two lists, each list being a queue * @list: the new list to add. * @head: the place to add it in the first list. */ static inline void list_splice_tail(struct list_head *list, struct list_head *head) { if (!list_empty(list)) __list_splice(list, head->prev, head); }
可以看到该函数内部也是调用的是__list_splice(),只不过传入的参数有点差别而已,最终的效果是,要并入的链表将被合并到另外一个链表的尾部,该函数可以用于队列的实现。
13、宿主结构
(1)找出宿主结构list_entry(ptr, type, member)
上面的操作都是基于struct list_head这个链表进行的,涉及到的结构体也都是如下结构体:
struct list_head { struct list_head *next, *prev; };
但是,应该真正关心的是包含list_head结构体字段的宿主结构体,因为只有定位到了宿主结构体的起始地址,才能够对宿主结构体中其它有意义的字段进行操作。
struct my_task_list { int val; struct list_head my_list; };
如何根据my_list这个字段的地址,寻找到my_task_list的地址呢?
在Linux内核中可以通过一个经典的宏定义:container_of(ptr, type, member),list_entry()宏定义如下:
/** * list_entry - get the struct for this entry * @ptr: the &struct list_head pointer. * @type: the type of the struct this is embedded in. * @member: the name of the list_head within the struct. */ #define list_entry(ptr, type, member) \ container_of(ptr, type, member)
container_of宏定义如下所示:
/** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */ #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \ !__same_type(*(ptr), void), \ "pointer type mismatch in container_of()"); \ ((type *)(__mptr - offsetof(type, member))); })
其宏的注释为:
根据结构体中的一个成员变量地址导出包含这个成员变量member的struct结构体地址。
参数:
ptr:成员变量member的地址
type:包含成员变量member的宿主结构体类型
member:宿主结构体中member成员变量的名称
使用举例为前面的strucr my_task_list:
struct my_task_list { int val; struct list_head my_list; }; struct my_task_list first_task = { .val = 1, .my_list = LIST_HEAD(first_task.my_list) };
若使用container_of宏去寻找fitst_task的地址,则这样使用:
ptr = container_of(&first_task.my_list, struct my_task_list, my_list);
因此container_of宏的功能就是根据first_task.my_list字段得到的地址得出first_task结构的真实地址。
offsetof宏定义如下所示:
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
将宏进行展开,并将ptr、type和member里面的值代入,可以得到下面的表达式:
#define container_of(&first_task.my_list, struct my_task_list, my_list) ({ \ void *__mptr = (void *)(&first_task.my_list); \ ((struct my_task_list *)(__mptr - ((size_t) &((struct my_task_list *)0)->my_list))); })
因此,container_of这个宏就是包含了两个语句,第一个语句如下:
void *__mptr = (void *)(&first_task.my_list);
该语句的作用为,提取出first_task_list中my_list的地址,强制转换为(void *)类型后,并赋值给__mptr指针。
第二个语句如下:
((struct my_task_list *)(__mptr - ((size_t) &((struct my_task_list *)0)->my_list)));
第二个语句的作用为,使用__mptr这个地址减去my_task_list中的偏移(该偏移量是通过将0地址强制转换为struct my_task_list指针类型,然后取出my_list中的地址,也就是相对于0地址的偏移,也就是my_list字段相对于宿主结构struct my_task_list的偏移),两者相减之后,得到的就是first_task宿主结构的起始地址,最后将地址强制转换为(struct my_task_list *)类型并返回。
(2)宿主结构的遍历
在上面可以知道,通过container_of这个宏,能根据结构体中成员变量的地址找到宿主结构的地址,并且能通过对成员变量提供的链表进行遍历,同时,也可以通过Linux内核接口对宿主结构进行遍历。
list_for_each_entry()能够对宿主结构进行遍历,其定义如下:
/** * list_for_each_entry - iterate over list of given type * @pos: the type * to use as a loop cursor. * @head: the head for your list. * @member: the name of the list_head within the struct. */ #define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ &pos->member != (head); \ pos = list_next_entry(pos, member))
该宏需要传入三个参数:
pos:宿主结构体类型指针
head:链表的表头
member:结构体内链表成员变量的名称
宏内还调用了另外两个宏,分别是list_first_entry和list_next_entry,其定义如下:
/** * list_first_entry - get the first element from a list * @ptr: the list head to take the element from. * @type: the type of the struct this is embedded in. * @member: the name of the list_head within the struct. * * Note, that list is expected to be not empty. */ #define list_first_entry(ptr, type, member) \ list_entry((ptr)->next, type, member)
由注释可以知道该宏是获取嵌入链表内第一个宿主结构体的地址。
/** * list_next_entry - get the next element in list * @pos: the type * to cursor * @member: the name of the list_head within the struct. */ #define list_next_entry(pos, member) \ list_entry((pos)->member.next, typeof(*(pos)), member)
该宏是获取嵌入链表内下一个宿主结构体的地址,通过这两个宏,便可以实现宿主结构的遍历。
#define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ &pos->member != (head); \ pos = list_next_entry(pos, member))
首先,pos定位到第一个宿主结构的地址,然后循坏获取下一个宿主结构的地址,判断宿主结构中的member成员变量(宿主结构中struct list_head定义的字段)地址是否为head,是的话,退出循坏,从而实现了宿主结构的遍历,通过遍历,能对宿主结构的其它成员变量进行操作,例如:
struct my_task_list *pos_ptr = NULL;
list_for_each_entry(pos_ptr, &head_task.my_list, my_list) { printk(KERN_INFO "val = %d\n", pos_ptr->val); }
参考:
《LINUX设备驱动程序(第3版)》
《Linux设备驱动开发详解:基于最新的Linux 4.0内核》
《深入Linux内核架构》
https://blog.csdn.net/wanshilun/article/details/79747710
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?