关于container_of和list_for_each_entry 及其相关函数的分析

本文详细解析了Linux内核中的链表操作函数,包括container_of、list_for_each_entry及其安全版本list_for_each_entry_safe的原理与用法。通过深入浅出的方式介绍了offsetof宏的计算逻辑,container_of如何定位结构体指针,以及list_for_each_entry如何遍历链表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

 

Linux代码看的比较多了,经常会遇到container_of和list_for_each_entry,特别是list_for_each_entry比较多,因为Linux经常用到链表,虽然知道这些函数的大概意思,但一旦出现一个类似的函数比如list_for_each_entry_safe就又会感到头大,所以下定决心分析总结一下这些函数的用法,以后再看到这些面孔的时候也会轻松很多,读Linux代码的时候不会那么吃力。

我们知道list_for_each_entry会用到list_entry,而list_entry用到container_of,所以首先讲讲container_of。

 

在讲container_of之前我们不得不提到offsetof,因为在container_of中会使用到它,所以我们看下来,把list_for_each_entry函数的用法理顺我们对整个Linux中经常用到的一些函数就会比较清楚了。

  1. offsetof                                                                                                                                                                                                                                                                                        /**/                                                                                                                                                                                                                                                                                                 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)                                                                                                                                                                           理解offsetof的关键在于&((TYPE *)0)->MEMBER,几乎可以说只要理解了这一部分,后面的几个函数都能够解决,那么我们看看这一部分究竟完成了怎样的工作。根据优先级的顺序,最里面的小括号优先级最高,TYPE *将整型常量0强制转换为TYPE型的指针,且这个指针指向的地址为0,也就是将地址0开始的一块存储空间映射为TYPE型的对象,接下来再对结构体中MEMBER成员进行取址,而整个TYPE结构体的首地址是0,这里获得的地址就是MEMBER成员在TYPE中的相对偏移量。再将这个偏移量强制转换成size_t型数据(无符号整型)。                                                                                                                                                                                                            所以整个offsetof的功能就是获取MEMBER成员在TYPE型数据中的偏移量。接下来我们可以讲一下container_of了。
  2. 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) ({                      \
         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
         (type *)( (char *)__mptr - offsetof(type,member) );})                                                                                                                                                                                                    首先可以看出container_of被预定义成一个函数,函数的第一句话,通过((type *)0)->member定义一个MEMBER型的指针__mptr,这个指针指向ptr,所以第一句话获取到了我们要求的结构体,它的成员member的地址,接下来我们用这个地址减去成员member在结构体中的相对偏移量,就可以获取到所求结构体的地址, (char *)__mptr - offsetof(type,member)就实现了这个过程,最后再把这个地址强制转换成type型指针,就获取到了所求结构体指针,define预定义返回最后一句话的值,将所求结构体指针返回。                                                                                                                                                                                                                                                                                           所以整个container_of的功能就是通过指向结构体成员member的指针ptr获取指向整个结构体的指针。container_of清楚了,那list_entry就更是一目了然了。
  3. 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_struct within the struct.
    */
    #define list_entry(ptr, type, member) \
           container_of\(ptr,type,member)                                                                                                                                                                                                                                       list_entry的功能等同于container_of。接下来分析我们最终想要知道的list_for_each_entry的实现过程。                                                           
  4. 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_struct within the struct.
    */
    #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_entry的基础上分析list_for_each_entry本来是一件比较轻松的事情,但在这里还是要强调一下双向链表及链表头的概念,否则对list_for_each_entry的理解还是一知半解。建立一个双向链表通常有一个独立的用于管理链表的链表头,链表头一般是不含有实体数据的,必须用INIT_LIST_HEAD()进行初始化,表头建立以后,就可以将带有数据结构的实体链表成员加入到链表中,链表头和链表的关系如图所示:                                                                                                                                                                                                                             链表头和链表的关系清楚了,我们才能完全理解list_for_each_entry。list_for_each_entry被预定义成一个for循环语句,for循环的第一句话获取(head)->next指向的member成员的数据结构指针,也就是将pos初始化为除链表头之外的第一个实体链表成员,for的第三句话通过pos->member.next指针遍历整个实体链表,当pos->member.next再次指向我们的链表头的时候跳出for循环。整个过程没有对链表头进行遍历(不需要被遍历),所以使用list_for_each_entry遍历链表必须从链表头开始。                                                                                                                                                                                                                                                                                          因此可以看出,list_for_each_entry的功能就是遍历以head为链表头的实体链表,对实体链表中的数据结构进行处理。
  5. list_for_each_entry_safe                                                                                                                                                                                                                                                       /**
    * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
    * @pos:     the type * to use as a loop cursor.
    * @n:          another type * to use as temporary storage
    * @head:     the head for your list.
    * @member:     the name of the list_struct within the struct.
    */
    #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))
    相比于list_for_each_entry,list_for_each_entry_safe用指针n对链表的下一个数据结构进行了临时存储,所以如果在遍历链表的时候可能要删除链表中的当前项,用list_for_each_entry_safe可以安全的删除,而不会影响接下来的遍历过程(用n指针可以继续完成接下来的遍历, 而list_for_each_entry则无法继续遍历)。

 

 

 

<think>我们正在讨论Linux内核链表中的两个函数list_del_initlist_add_tail。根据引用[2]中的内容,我们可以知道这两个函数都是操作双向链表的函数。首先,我们来看list_add_tail:引用[2]中提到了list_add_tail函数,它是用来将结点添加到链表的尾部。具体来说,它是在头节点之前插入,因为内核链表是循环链表,所以头节点的前一个节点就是链表的尾节点。因此,list_add_tail实际上是在头节点尾节点之间插入新节点,即新节点成为新的尾节点。然后,list_del_init函数:引用[2]中提到了list_del函数,但并没有直接提到list_del_init。不过,根据内核链表的常规实现,list_del_init函数不仅将节点从链表中删除,还会将节点的nextprev指针指向自己,即初始化节点,使得该节点成为一个空链表(只有一个节点且自环)。这样做的目的是为了确保该节点可以被再次加入链表,同时避免出现悬空指针。由于引用[2]中并没有给出这两个函数的具体实现,我们可以根据经验来写出它们的实现(注意:我们参考的是Linux内核链表的标准实现)。下面我们分别说明:1. list_add_tail功能:将新节点添加到链表的尾部(在头节点之前插入,因为循环链表尾节点后面就是头节点)原型:voidlist_add_tail(struct list_head *new,struct list_head *head);实现原理:a.找到头节点的前一个节点(即尾节点)b.将新节点插入到头节点尾节点之间:即新节点的next指向头节点,prev指向尾节点;然后调整尾节点的next指向新节点,头节点的prev指向新节点。2.list_del_init功能:从链表中删除一个节点,并将该节点初始化为一个空链表(即自环)原型:voidlist_del_init(struct list_head *entry);实现原理:a.首先,将节点从链表中删除(断开前后节点的链接)b.然后,初始化该节点:将nextprev都指向自己。下面我们用代码来展示这两个函数的实现(参考Linux内核链表):注意:我们假设链表节点结构为:struct list_head {structlist_head*next, *prev;};根据引用[2]中的基础函数INIT_LIST_HEAD,我们可以写出初始化节点的函数:staticinline voidINIT_LIST_HEAD(struct list_head *list){list->next= list;list->prev =list;}那么,list_add_tail的实现(参考引用[2]中提到的__list_add):staticinline voidlist_add_tail(struct list_head *new,struct list_head *head){__list_add(new,head->prev,head);}//而__list_add的实现是:static inlinevoid __list_add(struct list_head *new,structlist_head*prev,struct list_head *next){next->prev= new;new->next =next;new->prev= prev;prev->next =new;}这样,list_add_tail(new, head)实际上是将new节点插入到head->prev(即尾节点)head之间。对于list_del_init,我们可以这样实现:staticinline voidlist_del_init(struct list_head *entry){__list_del(entry->prev,entry->next);//先删除节点INIT_LIST_HEAD(entry);//然后初始化该节点}//其中__list_del的实现(引用[2]中提到):staticinline void__list_del(struct list_head *prev,struct list_head *next){next->prev= prev;prev->next =next;}所以,list_del_init(entry)做了两件事:-将entry的前一个节点的next指向entry的后一个节点-将entry的后一个节点的prev指向entry的前一个节点-然后将entry的nextprev都指向自己,这样entry就成为一个空链表。这样,我们就解释了这两个函数的用法实现原理。注意:在使用这些函数时,要确保链表节点已经初始化(比如头节点使用INIT_LIST_HEAD初始化),并且删除节点后如果需要重新加入链表,使用list_del_init可以安全地初始化节点。下面我们给出一个简单的示例代码,展示如何使用这两个函数:假设我们有一个包含链表节点的结构体:struct my_data {intvalue;struct list_head list;};初始化头节点:structlist_headhead;INIT_LIST_HEAD(&head);添加节点到尾部:struct my_data *data1= kmalloc(sizeof(*data1),GFP_KERNEL);data1->value=1;INIT_LIST_HEAD(&data1->list); //初始化新节点的链表list_add_tail(&data1->list, &head);//添加到尾部//同理添加data2structmy_data*data2 =kmalloc(sizeof(*data2), GFP_KERNEL);data2->value =2;INIT_LIST_HEAD(&data2->list);list_add_tail(&data2->list, &head);删除节点并重新初始化(以便后续可能再次加入链表):list_del_init(&data1->list); //从链表中删除data1,并将data1->list初始化为空链表这样,data1节点就被移除了,并且它的list成员被初始化,可以再次使用。注意:在实际使用中,删除节点后,我们可能需要释放节点内存,但list_del_init并不负责释放内存,它只是将节点从链表移除并初始化。所以,如果节点不再使用,应该在list_del_init之后释放内存;如果节点还要再次加入链表,则不需要释放。以上便是这两个函数的用法实现原理。</think>### `list_del_init` `list_add_tail` 函数详解 #### 一、链表基础结构 Linux 内核链表的核心是 `struct list_head`(双向循环链表): ```c struct list_head { struct list_head *next, *prev; }; ``` 所有链表操作都基于此结构,通过`container_of`宏获取宿主结构体指针[^2]。 --- #### 二、`list_add_tail` 函数 **功能**:将新节点插入到链表尾部(头节点之前) **函数原型**: ```c void list_add_tail(struct list_head *new, struct list_head *head); ``` **实现原理**: ```c static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); // 关键操作 } // 核心添加逻辑 static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; // 步骤1:后继节点prev指向新节点 new->next = next; // 步骤2:新节点next指向后继 new->prev = prev; // 步骤3:新节点prev指向前驱 prev->next = new; // 步骤4:前驱节点next指向新节点 } ``` **操作流程**: 1. 将新节点插入到 `head->prev`(尾节点) `head`(头节点)之间 2. 在循环链表中实现 $O(1)$ 时间复杂度插入 3. 插入后链表状态:`[head] ←→ [node1] ←→ ... ←→ [new] ←→ [head]` **应用场景**: - 实现先进先出(FIFO)队列 - 日志记录按时间顺序存储 - 设备驱动注册顺序管理 --- #### 三、`list_del_init` 函数 **功能**:安全删除节点并初始化其链表指针 **函数原型**: ```c void list_del_init(struct list_head *entry); ``` **实现原理**: ```c static inline void list_del_init(struct list_head *entry) { __list_del_entry(entry); // 从链表移除 INIT_LIST_HEAD(entry); // 初始化节点指针 } // 核心删除逻辑 static inline void __list_del(struct list_head *prev, struct list_head *next) { next->prev = prev; // 后继prev指向前驱 prev->next = next; // 前驱next指向后继 } // 初始化节点 static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; // 自环next指针 list->prev = list; // 自环prev指针 } ``` **安全特性**: 1. 删除后立即初始化指针,防止野指针错误 2. 节点可安全重用(避免 `use-after-free`) 3. 符合内核安全编码规范 **典型应用**: ```c // 安全删除节点示例 struct task_struct *task; list_for_each_entry_safe(task, tmp, &task_list, list) { if (task->exit_flag) { list_del_init(&task->list); // 安全删除 kfree(task); // 释放内存 } } ``` --- #### 四、对比总结 | **特性** | `list_add_tail` | `list_del_init` | |------------------|----------------------------------|----------------------------------| | **操作类型** | 插入 | 删除 + 初始化 | | **时间复杂度** | $O(1)$ | $O(1)$ | | **关键操作** | 修改前驱/后继指针 | 断开链接 + 指针自环 | | **安全特性** | 无竞争条件 | 防止野指针 | | **典型应用** | 队列管理、时序数据 | 安全删除、节点重用 | --- #### 五、使用注意事项 1. **头节点初始化** 使用前必须初始化头节点: ```c struct list_head my_list; INIT_LIST_HEAD(&my_list); // 创建自环头节点[^1] ``` 2. **删除期间加锁** 多线程环境需同步: ```c spin_lock(&list_lock); list_del_init(&node->list); spin_unlock(&list_lock); ``` 3. **遍历安全删除** 使用 `list_for_each_safe` 避免遍历中断: ```c struct list_head *pos, *n; list_for_each_safe(pos, n, &head) { if (condition) list_del_init(pos); } ``` > 通过 `container_of(ptr, type, member)` 可获取宿主结构体指针,例如: > ```c > struct my_data { > int value; > struct list_head list; // 嵌入链表节点 > }; > > // 获取宿主结构体 > list_entry(entry_ptr, struct my_data, list); > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值