精华内容
下载资源
问答
  • 2021-05-26 01:32:15

    list_entry: 原来C 程序可以这样写

    最近还是在看代码,越看越觉蹊跷和有意思。

    遇到一些list相关的问题,本来以为也就是双向循环链表的基本操作呢,结果。。。

    list_entry这样定义:

    #define list_entry(ptr, type, member) \

    ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

    解释:找到成员member所在容器的地址。如果是结构体的话,就是找到结构体成员变量member所在结构体type的地址。

    摘抄的解释例子(有修改):

    typedef struct

    {

    int i;

    int j

    } exp;

    这个exp结构体占用8个字节.

    假设声明一个变量。

    exp e1;

    那么假如已知 e1.j 的地址,想知道 e1 的地址该如何办呢?只要知道 j 在 e1 中的偏移,然后把 j 的地址减去这个偏移就是 e1 的地址了。在这里, i 占据的前4个字节,所以 j 占据了5-8的字节。

    现在我们用 list_entry 来解释一下。

    int *p = e1.j;

    假设 e1 的地址是 0x100,那么 p 就是 0x104。可是如何才能比较方便的知道 p 减去 4 就是 e1 的地址呢?尤其是我们可能有时候不知道 exp 这个结构体里面具体什么样子的。使用  list_entry(p, exp, j),则变成:

    (exp *)((char *)p-(unsigned long)(&((exp *)0)->j))

    (exp *)0 在 0 地址上面建立 8 个字节的 exp 结构体, ->j 取出这个0地址上exp结构体里的j成员, &((exp *)0)->j) 把这个成员地址取出来,由于 j 在这个结构体里是在5-8字节,所以从0地址数5个字节就是 j 所在的位置。这种方法省去了我们需要预先知道结构体具体什么样子,结构体里成员的位置怎么安排的。p 的地址再减去我们刚算出来j所在的位置,就得到 e1 的地址。也就是:

    &e1 == list_entry(p, exp, j)

    开眼界了! 原来程序可以这样写! 这段时间的体会是,写那些库函数的才是牛人!

    延伸阅读:

    深入分析 Linux 内核链表.  http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/

    更多相关内容
  • 终于理解list_entrylist_for_each_entry

    千次阅读 2019-08-08 18:43:19
    struct list_head { struct list_head *next, *prev; }; 一般将该数据结构嵌入到其他的数据结构中,从而使得内核可以通过链表的方式管理新的数据结构,看一个例子: struct example { member a; struc...

    内核中经常采用链表来管理对象,先看一下内核中对链表的定义

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

    一般将该数据结构嵌入到其他的数据结构中,从而使得内核可以通过链表的方式管理新的数据结构,看一个例子:

      struct example {
            member a;
            struct list_head list;
            member b;
        };

     1、链表的定义和初始化

    可以通过两种方式来定义和初始化一个链表头结点,例如,您想定义一个链表头结点mylist,那么您可以这么做:
    ① LIST_HEAD(mylist);  // 使用LIST_HEAD宏定义并初始化一个链表

    ② struct list_head mylist;  // 定义一个链表
        INIT_LIST_HEAD(&mylist); // 使用INIT_LIST_HEAD函数初始化链表

    可以看出方式①稍微简单一点,我们先来分析一下LIST_HEAD宏:

      #define LIST_HEAD_INIT(name) { &(name), &(name) }
      #define LIST_HEAD(name) /
             struct list_head name = LIST_HEAD_INIT(name)
    
     很容易看出LIST_HEAD(mylist);会被扩展为:
      struct list_head mylist = { &(mylist), &(mylist) };

    list_head结构只有两个成员:next和prev。从上面的代码可以看出,next和prev都被赋值为链表mylist的地址,也就是说,链表初始化后next和prev都是指向自己的。

    大多数情况下,list_head是被嵌入到其他数据结构中的,比如上面的example结构里的list成员,那么如何对list成员进行初始化?通过调用INIT_LIST_HEAD函数:

    struct example test;
    INIT_LIST_HEAD(&test.list); 
    该函数简单地将list成员的prev和next指针指向自己。

    可以看出链表结点在初始化时,都将prev和next指向自己。注意:对链表的初始化非常重要,因为如果使用一个未被初始化的链表结点,很有可能会导致内核异常。

    2、对链表常用的操作

    对链表常用的操作无非就是增加、删除、遍历等。当然内核还提供很多其他的操作,如替换某个结点、将某个结点移动到链表尾端等等,这些操作都是通过调用基本的增加、删除等操作完成的。

    2.1 增加:list_add和list_add_tail
    调用list_add可以将一个新链表结点插入到一个已知结点的后面
    调用list_add_tail可以将一个新链表结点插入到一个已知结点的前面

    下面分析它们的具体实现,它们都以不同的参数调用了相同的函数__list_add:

    static inline void __list_add(struct list_head *new,
                 struct list_head *prev,
                 struct list_head *next)
        {
             next->prev = new;
             new->next = next;
             new->prev = prev;
             prev->next = new;
        }
    static inline void list_add(struct list_head *new, struct list_head *head)
        {
             __list_add(new, head, head->next);
        }

    list_add函数中以new、head、head->next为参数调用__list_add,将new结点插入到head和head->next之间,也就是将new结点插入到特定的已知结点head的后面;

    static inline void list_add_tail(struct list_head *new, struct list_head *head)
    {
         __list_add(new, head->prev, head);
    }

    而list_add_tail函数则以new、head->prev、head为参数调用__list_add,将new结点插入到head->prev和head之间,也就是将new结点插入到特定的已知结点head的前面。

    有了list_add和list_add_tail,我们可以很方便地实现栈(list_add)和队列(list_add_tail)

    2.2 删除:list_del和list_del_init
    调用list_del函数删除链表中的一个结点; 
    调用list_del_init函数删除链表中的一个结点,并初始化被删除的结点(也就是使被删除的结点的prev和next都指向自己);

    下面分析它们的具体实现,它们都调用了相同的函数__list_del:

     static inline void __list_del(struct list_head * prev, struct list_head * next)
        {
             next->prev = prev;
             prev->next = next;
        }

    该函数实际的作用是让prev结点和next结点互相指向;

      static inline void list_del(struct list_head *entry)
        {
             __list_del(entry->prev, entry->next);
             entry->next = LIST_POISON1;
             entry->prev = LIST_POISON2;
        }

    该函数中以entry->prev和entry->next为参数调用__list_del函数,使得entry结点的前、后结点绕过entry直接互相指向,然后将entry结点的前后指针指向LIST_POISON1和LIST_POISON2,从而完成对entry结点的删除。此函数中的LIST_POISON1和LIST_POISON2有点让人费解,因为一般情况下我们删除entry后,应该让entry的prev和next指向NULL的,可是这里却不是,原因有待调查。

    static inline void list_del_init(struct list_head *entry)
        {
             __list_del(entry->prev, entry->next);
             INIT_LIST_HEAD(entry);
        }

    与list_del不同,list_del_init将entry结点删除后,还会对entry结点做初始化,使得entry结点的prev和next都指向自己。

    3、几个重要的宏

    内核提供了一组宏,以方便对链表进行管理。

    3.1 list_entry
    前面说过,list_head结构通常被嵌入到其他数据结构中,以便内核可以通过链表的方式管理这些数据结构。假设这样一种场景:我们已知类型为example的对象的list成员的地址ptr(struct list_head *ptr),那么我们如何通过ptr来得到该example对象的地址呢?答案很明显,使用container_of宏。不过,在这样的情况下我们应该通过使用list_entry宏来完成container_of宏的功能,因为这样更容易理解一点。其实list_entry宏很简单:#define list_entry(ptr, type, member)  container_of(ptr, type, member) ......

    上述情况,我们可以这样: list_entry(ptr, struct example, list); 来获取example对象的指针。

    3.2 list_for_each_entry
     对链表的一个重要的操作就是对链表进行遍历,以达到某种应用目的,比如统计链表结点的个数等等。先来看看内核中对该宏的定义:

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

    其中,pos是指向宿主结构的指针,在for循环中是一个迭代变量;head是要进行遍历的链表头指针;member是list_head成员在宿主结构中的名字。

    4 .链表使用例程

    查看例程

    展开全文
  • 为了代码简介高效,可以方便的被多个链表连接...typedef struct List { struct List* next; struct List* pre; //注:这里面没有数据域 }List_t; typedef struct Student { char name[10]; int age; int high;

    为了代码简介高效,可以方便的被多个链表连接起来,而且这个链表可以很方便的被各种不同类型数据域复用,我们实现单双链表时候(链表节点中不需要数据域),可以像下面这样子:

    typedef struct List
    {
        struct List* next;
        struct List* pre;
        //注:这里面没有数据域
    }List_t;
    
    
    
    typedef struct Student
    {
        char name[10];
        int age;
        int high;
        
        //该学生可能存在于多个链表中,比如在男生链表,又在班级链表
        List_t node1;      
        List_t node2; 
    }Student_t;
    
    
    
    /**
    *  @fn     ListInsert
    *  @brief  链表插入新节点
    *  @param  newNode 新节点
    *  @param  posNode 插入位置节点
    *  @return 无
    *  @note   默认插入到posNode后面
    */
    void ListInsert(List_t* newNode, List_t* posNode)
    {
    	newNode->pre = posNode;
    	newNode->next = posNode->next;
    	posNode->next->pre = newNode;
    	newNode->next = newNode;
    }
    
    
    void main()
    {
        Student_t s1,s2;
    
        //s1和s2连接成链表1  
        ListInsert(&(s1.node1), &(s2.node1));
    
        //s1和s2连接成另一个链表2
        ListInsert(&(s1.node2), &(s2.node2));
    }
    
    
    

    此时,当我们得到了s1.node1的地址 (设为p1) 时候,现在想拿到s1的首地址(s1.node1所在结构体),怎么办呢,这时候,过程如下:

    1. 获得Student结构体首地址和它的成员node1的地址差,如下

    p = &( (Student*)0 -> node1 )    //思想:假设现在Student结构体的首地址假设为0,此时成员node1的地址就是相对偏移啦

    2. 拿s1.node1的地址减去得到的地址差即可

    p1 - p 就得到了我们想要的Student结构体的首地址。这个就是list_enty宏定义的实现原理,完整实现如下:

    #define list_entry(ptr, type, member) \
    	container_of(ptr, type, member)
    
    
    #define container_of(ptr, type, member) ((type *)((u8*)ptr - offsetof(type,member)))
    
    #define offsetof(type, member) ((u32) &((type *)0)->member)  

    Linux系统内核中的链表就是这样子实现滴喔。

    参考文章:

    Linux内核链表及list_entry解析_Hugo的博客-CSDN博客_linux list_entry

    函数指针及其定义和用法,C语言函数指针详解

    展开全文
  • Linux内核链表及list_entry解析

    千次阅读 2018-01-07 21:23:39
    linux提供了list_entry这个宏,ptr是指向该数据中list_head成员的指针,type是节点的类型,member是节点类型中list_head成员的变量名。   C代码  /**   * list_entry - get the struct ...

    链表是一些包含数据的独立数据结构的集合,链表中的每一个节点通过链或者指针连接在一起。程序通过指针访问链表中的节点。链表一般分为单链表和双链表。

    1.单链表

    单链表中,每个节点包含指向下一个节点的指针。链表最有一个节点的指针字段值为NULL,表明链表后面不再有其它节点。下面是一张单链表的图:

    对应的数据结构为:

    C代码   收藏代码
    1. typedef struct NODE  
    2. {  
    3.     int value;  
    4.     struct NODE *next;        
    5. }Node;  

    2.双链表

    在一个双链表中,每个节点都包含两个指针——指向前一个节点的指针和指向后一个节点的指针。这样的好处是我们可以从任何方向遍历双链表。

    对应的节点数据类型为:

    C代码   收藏代码
    1. typedef struct NODE  
    2. {  
    3.     int value;  
    4.     struct NODE *fwd;         
    5.     struct NODE *bwd;  
    6. }Node;  

    3.linux内核链表

    此处以2.6.31.13内核版本作为分析基础。不同版本之间的区别不大。链表结构定义为(节选自include/linux/list.h):

    C代码   收藏代码
    1. struct list_head {  
    2.     struct list_head *next, *prev;  
    3. };  

      内核链表包含指向next和prev的指针,是一个双链表,不过不同于一般的双链表,内核链表不包含数据域,通常被用作双循环链表,当需要用到十字链表时,使用内核链表也很方便。

    3.1 声明和初始化

    linux内核提供了两种方式初始化链表。一种是使用LIST_HEAD()这个宏:

    C代码   收藏代码
    1. #define LIST_HEAD_INIT(name) { &(name), &(name) }  
    2.   
    3. #define LIST_HEAD(name) \  
    4.         struct list_head name = LIST_HEAD_INIT(name)  

     

    另外有一个内联函数用于运行时初始化:

    C代码   收藏代码
    1. static inline void INIT_LIST_HEAD(struct list_head *list)  
    2. {  
    3.     list->next = list;  
    4.     list->prev = list;  
    5. }  

    3.2 添加、删除

    下面都是些很基本的操作,只要弄清楚了链表的原理,都很容易理解。

    C代码   收藏代码
    1. /* 
    2. * Insert a new entry between two known consecutive entries. 
    3. * 
    4. * This is only for internal list manipulation where we know 
    5. * the prev/next entries already! 
    6. */  
    7. static inline void __list_add(struct list_head *new,  
    8.                               struct list_head *prev,  
    9.                               struct list_head *next)  
    10. {  
    11.         next->prev = new;  
    12.         new->next = next;  
    13.         new->prev = prev;  
    14.         prev->next = new;  
    15. }  
    16.   
    17. /** 
    18. * list_add - add a new entry 
    19. * @new: new entry to be added 
    20. * @head: list head to add it after 
    21. * 
    22. * Insert a new entry after the specified head. 
    23. * This is good for implementing stacks. 
    24. */  
    25. static inline void list_add(struct list_head *newstruct list_head *head)  
    26. {  
    27.         __list_add(new, head, head->next);  
    28. }  
    29.   
    30. /** 
    31. * list_add_tail - add a new entry 
    32. * @new: new entry to be added 
    33. * @head: list head to add it before 
    34. * 
    35. * Insert a new entry before the specified head. 
    36. * This is useful for implementing queues. 
    37. */  
    38. static inline void list_add_tail(struct list_head *newstruct list_head *head)  
    39. {  
    40.         __list_add(new, head->prev, head);  
    41. }  
    42.   
    43. static inline void __list_del(struct list_head * prev, struct list_head * next)  
    44. {  
    45.         next->prev = prev;  
    46.         prev->next = next;  
    47. }  
    48.   
    49. static inline void list_del(struct list_head *entry)  
    50. {  
    51.         __list_del(entry->prev, entry->next);  
    52.         entry->next = LIST_POISON1;  
    53.         entry->prev = LIST_POISON2;  
    54. }  
    55.   
    56. static inline void list_del_init(struct list_head *entry)  
    57. {  
    58.         __list_del(entry->prev, entry->next);  
    59.         INIT_LIST_HEAD(entry);  
    60. }  
    61.   
    62. static inline void list_move(struct list_head *list, struct list_head *head)  
    63. {  
    64.         __list_del(list->prev, list->next);  
    65.         list_add(list, head);  
    66. }  
    67.   
    68. static inline void list_move_tail(struct list_head *list,  
    69.                                   struct list_head *head)  
    70. {  
    71.         __list_del(list->prev, list->next);  
    72.         list_add_tail(list, head);  
    73. }  
    74.   
    75. static inline int list_empty(const struct list_head *head)  
    76. {  
    77.         return head->next == head;  
    78. }  

    3.3 获取链表节点

    linux链表中仅保存了list_head成员变量的地址,那么我们如何通过这个list_head的成员访问到它所有者节点的数据呢?linux提供了list_entry这个宏,ptr是指向该数据中list_head成员的指针,type是节点的类型,member是节点类型中list_head成员的变量名。

     

    C代码   收藏代码
    1. /** 
    2. * list_entry - get the struct for this entry 
    3. * @ptr:        the &struct list_head pointer. 
    4. * @type:       the type of the struct this is embedded in. 
    5. * @member:     the name of the list_struct within the struct. 
    6. */  
    7. #define list_entry(ptr, type, member) \  
    8.         container_of(ptr, type, member)  

      container_of宏定义在include/linux/kernel.h

    C代码   收藏代码
    1. /** 
    2. * container_of - cast a member of a structure out to the containing structure 
    3. * @ptr:        the pointer to the member. 
    4. * @type:       the type of the container struct this is embedded in. 
    5. * @member:     the name of the member within the struct. 
    6. * 
    7. */  
    8. #define container_of(ptr, type, member) ({                      \  
    9.         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \  
    10.         (type *)( (char *)__mptr - offsetof(type,member) );})  

    offsetof在include/linux/stddef.h中

    C代码   收藏代码
    1. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  

    获得节点对象指针的原理图如下所示:

    ((type *)0)->member,它将0地址强制转换为type结构的指针,再访问到type结构中的member成员。offsetof取得list_head成员msg_node相对于结构体的偏移量。将指向当前节点对象member的地址减去偏移量,就可以得到节点地址,再将它转成指向节点结构类型的指针。

    linux链表的基本操作已经完成了,其它如链表遍历的操作可查看list.h源码,有很详细的说明。

    展开全文
  • list_for_each_entry和list_for_each_entry_safe

    万次阅读 多人点赞 2018-08-09 18:49:53
    看内核代码都会发现,内核链表的操作常用的二个宏list_for_each_entry和list_for_each_entry_safe 循序渐进,先从最底层的函数container_of函数说起,其内核定义如下: 先看offsetof宏,根据优先级的顺序,最里面...
  • list_entry通俗理解方法

    万次阅读 多人点赞 2018-08-20 00:06:35
    大家都知道list_entry时kernel里面经常遇到的一个函数,其定义为: #define list_entry(ptr, type, member) \ container_of(ptr, type, member) #define container_of(ptr, type, member) ({ \ const typeof...
  • LIST_ENTRY数据结构

    千次阅读 2016-09-07 21:34:17
    一个常见的 Windows 2000 数据类型是 LIST_ENTRY 结构。内核使用该结构将所有对象维护在一个双向链表中。一个对象分属多个链表是很常见的, Flink 成员是一个向前链接,指向下一个 LIST_ENTRY 结构, Blink 成员则是...
  • list_entry()详解

    千次阅读 2017-09-18 11:05:21
    转载地址:... Linux内核中,获取节点地址的函数list_entry()非常常用,由于其定义有点晦涩,先解析如下: list_entry的宏定义: #define list_entry(ptr, type, member) /  ((type *)((char *
  • list_for_each与list_for_each_entry【转】

    千次阅读 2018-08-02 22:23:59
    1.list_for_each和list_for_each_entry都是遍历链表的两个宏,本质上都是for循环。 2.他们做的事情本质上都一样,A.获取链表头,B.判断链表项是不是链表头,C.指向链表的下一项。 3.他们的区别:list_for_each遍...
  • LIST_ENTRY定义如下: typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; // 指向下一个节点 struct _LIST_ENTRY *Blink; // 指向前一个节点 } LIST_ENTRY, *PLIST_ENTRY; 由LIST_ENTRY的定义可以知道这...
  • list_for_each_entry_safe ( ) :相当于遍历整个双向循环链表,遍历时会存下下一个节点的数据结构,方便对当前项进行删除。 linux的内存分配 物理页管理(伙伴系统) 物理内存被划分为固定长度的连续内存块...
  • Windows内核编程基础之使用LIST_ENTRY

    千次阅读 2015-08-27 18:56:56
    LIST_ENTRY 是一个双向链表结构。它总是在使用的时候被插入到已有的数据结构中。Windows内核中使用LIST_ENTRY作为i链表,这个结构随处可见。  看看下面的代码,构建了一个链表,每个节点是又一个文件名和一个文件...
  • 介绍了上面的几种基本宏后,对list_entry的理解就容易了。 ----------------list_entry()-------------------- list_entry()宏,获取当前list_head链表节点所在的宿主结构项。第一个参数为当前list_head节点的...
  • 今天在尝试用list_head结构体和list_entry写一个链表测试程序时,发现头文件无法读取。 首先我通过命令:find / -name list.h查找到list存在的位置,追踪进去确实发现了list_head的定义,在补全路径的情况下仍无果。...
  • LIST_ENTRY结构

    千次阅读 2016-03-04 13:20:34
    在Windows驱动相关编程中,会用到该结构。Windows的源代码中大量使用了该结构。该结构用来组成常见的数据结构——双...LIST_ENTRY结构如下:typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_EN
  • linux 链表(list_entry) 用法

    千次阅读 2015-10-16 17:31:11
    Linux内核中,获取节点地址的函数list_entry(): list_entry的宏定义: #define list_entry(ptr, type, member) /  ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))  这个倒是不难理解...
  • 看内核代码都会发现,内核链表的操作常用的二个宏list_for_each_entry和list_for_each_entry_safe 循序渐进,先从最底层的函数container_of 函数说起,其内核定义如下: 先看offsetof宏,根据优先级的顺序,最...
  • list_entry

    千次阅读 2018-05-13 23:56:01
    #define rt_list_entry(node, type, member) \ ((type *)((char *)(node) - (unsigned long)(&((type *)0)->member))) 其最终返回的是type结构体的地址。 在Linux内核中,获取节点地址的函数list_...
  • list_entry的使用说明

    千次阅读 2014-07-30 17:01:24
    Linux内核中,获取节点地址的函数list_entry()非常常用,由于其定义有点晦涩,先解析如下: list_entry的宏定义: #define list_entry(ptr, type, member) /  ((type *)((char *)(ptr)-(unsigned long...
  • LIST_ENTRY链表学习

    千次阅读 2014-08-22 00:27:44
    要使用链表,需要用到一个LIST_ENTRY的结构,其定义如下: typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; // 指向下一个节点 struct _LIST_ENTRY *Blink; // 指向前一个节点 } LIST_ENTR
  • list_for_each_entry宏函数解析(上)

    万次阅读 2015-06-02 10:42:11
    在内核中,经常用到list_for_each_entry函数, container_of
  • 内核中经常采用链表来管理对象,先看一下内核中对链表的定义 ... struct list_head {  struct list_head *next, *prev;  };    一般将该数据结构嵌入到其他的数据结构中,从而使得内核
  • list_entry更简洁的写法list_for_each_entrylist.h中,处理每个节点,宏list_entry是个好选择,但是需要用到for循环,而宏list_for_each_entry把for和list_entry结合起来了,使用更加方便。 list_for_each_...
  • list_for_each与list_for_each_entry

    千次阅读 2015-05-25 18:42:57
    1.list_for_each和list_for_each_entry都是遍历链表的两个宏,本质上都是for循环。 2.他们做的事情本质上都一样,A.获取链表头,B.判断链表项是不是链表头,C.指向链表的下一项。 3.他们的区别:list_for_each遍历...
  • LIST_ENTRY 使用练习

    千次阅读 2022-03-06 08:25:04
    //链表数据结构体 //根据实际需要,自行调整 typedef struct _LIST_ENTRY_DATA { LIST_ENTRY ListEntry; ULONG64 number; ULONG64 data; ULONG64 ProcessId; ULONG64 ImageBase; CHAR* ImagePath; }LIST_ENTRY...
  • list_for_each_entry深入理解

    千次阅读 2016-08-03 14:49:58
    list_for_each_entry
  • #define list_entry(ptr, type, member)

    千次阅读 2014-05-16 14:29:11
    #define list_entry(ptr, type, member) \  ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))  ptr是指向list_head类型链表的指针,type为一个结构,而member为结构type中的一个域,类型为...
  • Linux内核中list_for_each_entry浅析

    千次阅读 2017-04-19 12:12:00
    一、list_for_each_entry的作用与定义 二、list_for_each_entry的简单实现 一、list_for_each_entry的作用与定义 在这篇博文中通过list_head来构造了链表,虽然方便实用,但也带来了一些额外的问题,比如如何对...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 347,305
精华内容 138,922
关键字:

list_entry