Linux内核链表

Linux内核的很神奇,它是一个很大的C程序,带有一些汇编代码。为什么Linux内核如此吸引人?首先Linux内核是由世界上最好的程序员编写的,源码可以证实。Linux内核结构良好,细节一丝不苟,巧妙的解决方案在代码中出处可见。
Linux内核代码规模之大,覆压三百余里,隔离天日。关系错综复杂,廊腰缦回,檐牙高啄,各抱地势,勾心斗角。阿房宫在规模和结构上给人的震撼可能与Linux有异曲同工之妙。

Linux内核链表

Linux内核链表在Linux内核源码中广泛使用,在/include/linux/list.h有链表通用函数实现。
在阅读这篇文章之前,默认已经在数据结构与算法课程上学习过基础链表的知识,而且自己动手写过一些链表的代码。如果没有学习,建议学习一下;如果已经忘记,建议复习一下。

链表是由结点链接而成,基本的数据结构就是表示结点的结构体。通过链接这些结构体的函数,即链表的创建函数,将一个一个结点链接起来。链接形成以后,可以在链表上面定义增删改查的函数。最后就可以使用链表这个数据结构存储数据。

以前没有学习Linux内核链表的数据结构和在此数据结构之上的函数的时候,自己写过很多链表的代码,总感觉链表不过如此。直到我阅读到内核链表的数据结构和函数,真的是叹为观止。Linus写的不是代码,写的是艺术品。

内核链表数据结构的设计哲学就是:既然链表不能包含万物,那么就让万物来包含链表。这些代码真的是艺术品,每一处设计都蕴涵很多哲学思想。
为什么这么设计?Linux内核的源码中,很多地方需要用到链表这种数据结构,但是每个结点结构体又不同。如果按照大学教材中的写法,每种结构体都要写一次增删改查函数,这里面很多重复的工作,因为链表的增删改查的函数是非常类似的。Linux内核链表的做法就是将通用的操作写在list.h里面。最后还是要自己定义增删改查的函数,但是工作量变小了。

开始内核链表结构的旅程

内核链表结构的设计思想是将链表中指向前后结点的指针存储的数据分离开。写在不同的结构体里面。只是实现链表通用的函数结构,使用者自己将链表嵌入到不同的数据结构中。我们在数据结构与算法的课程学习到是指向前后结点与存储的数据写在一个结构体结点。

阅读Linux内核代码,在/include/linux/list.h中有链表的通用函数的实现。
这里面运用很多宏,因为宏是字符串替换,通过宏可以实现嵌入不同的数据类型,每每阅读到这里,都为之拍案叫绝。Linux内核通过宏,让我到有点C++ template的味道在里面。未来可以通过内核链表的数据结构和函数来构造和实现不同类型的新链表。

内核链表并不维护数据,只维护数据结构。

内核链表里面没有一个具体的存储数据,只是维护一个内核链表数据结构和在这个数据结构之上的函数操作。这个内核链表的数据结构和函数与C++ STL的list容器和算法非常相似。list容器是对链表的一个抽象,容器有使用者传入不同的数据类型,与容器相配套的是基于容器之上的很多算法函数。内核链表数据结构是对链表的一个抽象,将数据结构和存储的数据分离开,由使用者自己定义数据类型,与内核链表数据结构相配套是基于此结构之上的很多算法函数。

使用者可以通过内核链表数据结构和算法函数结合存储的数据类型构造新的链表结构和函数。这个存储的数据类型可以是基本数据类型,比如int类型。如果有很多基本数据类型,可以考虑将其封装在一个结构体中。

字节对齐

#include <stdio.h>
#include <stdlib.h>
struct list_head {
    struct list_head *next, *prev;
};
typedef struct Data {
    int num;
} Data;
typedef struct Node {
    struct Data data;
    struct list_head list;

} Node;

void testStruct() {
    struct Data data = {2023};
    Node *newPointer = (Node*)malloc(sizeof (Node));
    newPointer->data = data;

    printf("Node pointer: %p\n", newPointer);
    printf("Data pointer: %p\n",&(newPointer->data));
    printf("list_head: %p\n",&(newPointer->list));
    printf("Address starts from 0, list pinter: %d\n", &((Node*)0)->list);
}
int main() {
    /*printf("Hello, World!\n");*/
    testStruct();
    return 0;
}
Node pointer: 0000000000B26D70
Data pointer: 0000000000B26D70
list_head: 0000000000B26D78
Address starts from 0, list pinter: 8

因为我的机器的64位,所以字节对齐的大小是8字节。这里((Node*)0)->list就是list结构体的地址对于Node结构体偏移量的大小。这个写法在后面会用到,这里简要介绍一下。

基础知识

C语言中 typeof 关键字是用来定义变量数据类型的。在Linux内核源代码中广泛使用。
#define min(x, y) ({            \
    typeof(x) _min1 = (x);      \
    typeof(y  _min2 = (y);      \
    (void) ( &_min1 == &_min2); \
    _min1<_min2 ? _min1 : _min2;\
})
当x的类型为int时,_min1 变量的数据类型为int。
当x为一个表达式时,比如 x = 3-4, _min1变量的数据类型为这个表达式结果的数据类型。
typeof 括号中也可以是函数
int function(int, int)
typeof(function(1,2)) val
此时 val 的数据类型为函数 function(int, int) 返回值的数据类型,即int类型。
注意,typeof并不会执行函数function。
volatile 类型限定符
volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。
通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。
例如,一个地址上可能存储这当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。
或者一个地址用于接受另一台计算机传入的信息。

volatile int local; /* local 是一个易变的位置。 */
volatile int *ploc; /* ploc是一个指向易变位置的指针 */
以上代码把local声明为volatile变量,把ploc声明为指向volatile变量的指针。
volatile是不是一个可有可无的概念,为何ANSI委员把volatile关键字放入标准?
原因是它涉及编译器的优化。例如,假如有下面的代码
#include <stdio.h>>
#include <stdlib.h>

int gval = 2022;
int main()
{
    int val1 = gval;
    // 一些不使用 gval的代码
    int val2 = gval;
    return 0;
}
智能的(进行优化的)编译器会注意到以上代码使用了两次gval,但并未改变它的值。于是编译器把gval的值
临时存储在寄存器中,然后在val2需要使用gval时,才从寄存器中(而不是从原始内存位置上)读取gval的值,
以节约时间。这个过程被称为高速缓存(caching)。
通常,高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了gval的值,就不能这样优化了。
如果没有volatile关键字,编译器就不知道这种事情是否会发生。
因此,为安全起见,编译器不会进行高速缓存。这是在ANSI之前的情况。
现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。
可以同时使用constvolatile 限定一个值。
例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这是用volatile。
只能在声明中同时使用这两个限定符,它们的顺序不重要,如下所示:

volatile const int local;
const volatile int *ploc;

初始化宏

list_head结构体的声明在 /include/linux/types.h文件中
struct list_head{
	struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

这是用来初始化的宏,LIST_HEAD_INIT宏设计非常精妙,
本身不包含任何数据类型,没有限定唯一的数据类型,这让我感觉到C++泛型编程的味道在里面。

如何理解上面的代码
struct list_head name = LIST_HEAD_INIT(name)
展开
struct list_head name = { &(name), &(name) }

相当于
struct list_head name;
name.next = &name;
name.prev = &name;

相当于
struct list_head name = {
     .next = &name,
     .prev = &name
};
这是Linux内核源代码中常用的初始化结构体变量的方式。
由此观之,宏LIST_HEAD(name)将结构体变量name初始化,将name里面的next, prev 指针指向name。

理解内核链表最关键的两个宏

/*

1. 获取MEMBER成员地址相对于TYPE结构体起始地址的偏移量(回到字节对齐再看一遍)
((TYPE *)0)将0转换为TYPE类型的结构体指针,就是让编译器认为这个结构体是开始于程序段起始位置0。
开始于0地址的话,我们得到的成员变量的地址就是成员变量地址相对于结构体地址的偏移量。
size_t是标准C库中定义的
在32位架构中一般被定义为:typedef unsigned int size_t;
在64为架构中一般被定义为:typedef unsigned long size_t;
从定义中可以看到,size_t是一个非负数,所以size_t通常用来计数,因为计数不需要负数区。
为了使程序有很好的移植性,因此Linux内核使用size_t, 而不是 int, unsigned。
#define offsetof(TYPE, MEMBER)  ((size_t) &((TYPE *)0)->MEMBER)

2. 通过结构体成员的指针获取结构体的指针

#define container_of(ptr, type, member) ({             \
     const typeof( ((type *)0)->member ) *__mptr =  (ptr); \
     (type *)( (char *)__mptr - offsetof(type, member) );})

const typeof(((type *)0) ->member)* __mptr = (ptr);
这行代码的意思是用typeof()获取结构体里member成员的类型。
然后定义一个该类型的临时指针变量 __mptr,并将ptr所指向的member的地址赋给 __ptr。
为什么不直接使用ptr而要多此一举?
可能是为了避免对 ptr 以及 ptr指向的内存造成破坏。

 (type *)( (char *)__mptr - offsetof(type, member) );})
这行代码的意思是把 __mptr 转换成 char* 类型。因为 offsetof 得到的偏移量是以字节为单位的。
两者相减得到结构体的起始位置,再强制转换成 type*类型。

入口的选择的宏

/**
 * 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)

/**
 * 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)

遍历链表的宏

static inline int list_is_head(const struct list_head *list, const struct list_head *head)
{
    return list == head;
}
/**
 * list_entry_is_head - test if the entry points to the head of the list
 * @pos:	the type * to cursor
 * @head:	the head for your list.
 * @member:	the name of the list_head within the struct.
 */
#define list_entry_is_head(pos, head, member)				\
	(&pos->member == (head))
/**
 * 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; !list_is_head(pos, (head)); pos = pos->next)
/**
 * list_for_each_safe - iterate over a list safe against removal of list entry
 * @pos:	the &struct list_head to use as a loop cursor.
 * @n:		another &struct list_head to use as temporary storage
 * @head:	the head for your list.
 */
#define list_for_each_safe(pos, n, head) \
	for (pos = (head)->next, n = pos->next; \
	     !list_is_head(pos, (head)); \
	     pos = n, n = pos->next)
	    
/**
 * 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);	\
	     !list_entry_is_head(pos, head, member);			\
	     pos = list_next_entry(pos, member))
/**
 * 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_head within the struct.
 */
#define list_for_each_entry_safe(pos, n, head, member)			\
	for (pos = list_first_entry(head, typeof(*pos), member),	\
		n = list_next_entry(pos, member);			\
	     !list_entry_is_head(pos, head, member); 			\
	     pos = n, n = list_next_entry(n, member))

函数

初始化函数

/**
 * INIT_LIST_HEAD - Initialize a list_head structure
 * @list: list_head structure to be initialized.
 *
 * Initializes the list_head to point to itself.  If it is a list header,
 * the result is an empty list.
 */
static inline void INIT_LIST_HEAD(struct list_head *list)
{
    /* 
    list->next = list;
    list->prev = list;
    */
    WRITE_ONCE(list->next, list);
    WRITE_ONCE(list->prev, list);
}
函数的功能:将list中的next和prev指针指向自己。WRITE_ONCE宏的功能是为了避免编译器优化。这里就不详细展开了。
我后面使用的时候,会将WRITE_ONCE(list->next, list)改为list->next = list; WRITE_ONCE(list->prev, list);改为list->prev = list;。不考虑编译器的优化,专注于内核链表数据结构和算法的思想,其他类似的编译器优化也是这样做。

如果想要了解的同学可以自己去查阅相关的资料。这里给出我查阅的一些资料。
Linux kernel: CPU访问相同内存的说明详见官方文档:
https://www.kernel.org/doc/Documentation/memory-barriers.txt
stackoverflow上关于WRITE_ONCE的讨论:
https://stackoverflow.com/questions/34988277/write-once-in-linux-kernel-lists

链表增加结点的函数

/*
 * 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 - 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);
}

尾插法

/**
 * 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);
}

链表删除结点的函数

/*
 * 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);
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
    return true;
}
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 - 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(struct list_head *entry)
{
    __list_del_entry(entry);
    entry->next = LIST_POISON1;
    entry->prev = LIST_POISON2;
}

链表判空函数

/**
 * 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;
}

实战

我们通过修改一下Linux内核的源码来使用这个内核的链表。

/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include "list_test.h"

int main() {
    printf("Hello, World!\n");
    testPrint();
    testStruct();
    testKernelList();
    return 0;
}
/* list_test.h */
//
// Created by mo on 2023/3/15.
//

#ifndef LINKLIST_LIST_TEST_H
#define LINKLIST_LIST_TEST_H

#include "list_simple.h"

struct Data {

    int num;

};
struct Node {
    struct Data data;

    struct list_head list;
};

// 头插法
void insertNode(struct list_head *head, struct Data data);

// 尾插法
void insertNodeTail(struct list_head *head, struct Data data);

// 删除
void delNode(struct list_head *head, struct Data data);

// 查找
struct Node *findNode(struct list_head *head, struct Data data);

// 修改
void modifyNode(struct list_head *head, struct Data old_data, struct Data new_data);

// 遍历
void printList(struct list_head *head);
void printListTest(struct list_head *head);
void testPrint();
void testKernelList();
void testStruct();
#endif //LINKLIST_LIST_TEST_H

/* list_test.c */
//
// Created by mo on 2023/3/15.
//


#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "list_test.h"

// 头插法
void insertNode(struct list_head *head, struct Data data) {
    struct Node *newPointer = (struct Node *) malloc(sizeof(struct Node));
    newPointer->data = data;
    list_add(&(newPointer->list), head);
}

// 尾插法
void insertNodeTail(struct list_head *head, struct Data data) {
    struct Node *newPointer = (struct Node *) malloc(sizeof(struct Node));
    newPointer->data = data;
    list_add_tail(&(newPointer->list), head);
}

// 删除
void delNode(struct list_head *head, struct Data data) {
    struct Node *pos = NULL;
    struct Node *n = NULL;
    if (!list_empty(head)) {
        list_for_each_entry_safe(pos, n, head, list) {
            if (pos->data.num == data.num) {
                list_del(&pos->list);
                free(pos);
                printf("delete successful\n");
            }
        }
    }
}


// 查找
struct Node *findNode(struct list_head *head, struct Data data) {
    struct Node *pos = NULL;
    list_for_each_entry(pos, head, list) {
        if (pos->data.num == data.num) {
            printf("find successful\n");
            return pos;
        }
    }
    return pos;
}

// 修改
void modifyNode(struct list_head *head, struct Data old_data, struct Data new_data) {
    struct Node *ret = findNode(head, old_data);
    if (ret != NULL) {
        ret->data.num = new_data.num;
        printf("modify successful\n");
    } else {
        printf("modify failed\n");
    }
}


// 遍历
void printList(struct list_head *head) {
    struct Node *pos = NULL;
    list_for_each_entry(pos, head, list) {
        printf("%d ", pos->data.num);
    }
    printf("\n");
}

void printListTest(struct list_head *head) {
    struct list_head *pos = NULL;
    struct list_head *n = NULL;
    printf("list_for_each_safe\n");
    list_for_each_safe(pos, n, head) {
        printf("list_head address %p \n", pos);
    }
    printf("list_for_each\n");
    list_for_each(pos, head) {
        printf("list_head address %p \n", pos);
    }
    printf("\n");
}
void testPrint(){
    struct list_head *head = (struct list_head *) malloc(sizeof(struct list_head));
    INIT_LIST_HEAD(head);
    for (int i = 0; i < 16; i++) {
        struct Data data;
        data.num = rand() % 10;
        insertNode(head, data);
    }
    printListTest(head);
}
void testKernelList() {
    struct list_head *head1 = (struct list_head *) malloc(sizeof(struct list_head));
    struct list_head *head2 = (struct list_head *) malloc(sizeof(struct list_head));
    INIT_LIST_HEAD(head1);
    INIT_LIST_HEAD(head2);
    printf("initial\n");
    srand((unsigned int) time(NULL));
    for (int i = 0; i < 16; i++) {
        struct Data data;
        data.num = rand() % 10;
        insertNode(head1, data);
        insertNodeTail(head2, data);
    }
    printf("insert\n");

    printList(head1);
    printList(head2);

    struct Data data = {
            .num= 3
    };
    delNode(head1, data);
    printList(head1);
    struct Data old_data = {
            .num= 6
    };
    struct Data new_data = {
            .num= 2023
    };
    modifyNode(head1, old_data, new_data);
    printList(head1);
}

void testStruct() {
    struct Data data = {2023};
    struct Node *newPointer = (struct Node *) malloc(sizeof(struct Node));
    newPointer->data = data;

    printf("Node pointer: %p\n", newPointer);
    printf("Data pointer: %p\n", &(newPointer->data));
    printf("list_head: %p\n", &(newPointer->list));
    printf("Address starts from 0, list pinter: %d\n", &((struct Node *) 0)->list);

    printf("container_of\n"); // 通过成员变量的地址获得结构体的地址
    struct Node *pos = NULL;
    pos = container_of(&(newPointer->data), struct Node, data);
    printf("Node address by data: %p\n", pos);
    pos = container_of(&(newPointer->list), struct Node, list);
    printf("Node address by list: %p\n", pos);
}
/* list_simple.h */
//
// Created by mo on 2023/3/15.
//

#ifndef LINKLIST_LIST_SIMPLE_H
#define LINKLIST_LIST_SIMPLE_H

#include <stdbool.h>
# define POISON_POINTER_DELTA 0
#define LIST_POISON1  ((void *) 0x100 + POISON_POINTER_DELTA)
#define LIST_POISON2  ((void *) 0x122 + POISON_POINTER_DELTA)

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

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
//    WRITE_ONCE(list->next, list);
//    WRITE_ONCE(list->prev, list);
    list->next = list;
    list->prev = list;
}
static inline bool __list_add_valid(struct list_head *new,
                                    struct list_head *prev,
                                    struct list_head *next)
{
    return true;
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
    return true;
}
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;
}
static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
    __list_add(new, head->prev, head);
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
//    WRITE_ONCE(prev->next, next);
    prev->next = next;
}
static inline void __list_del_entry(struct list_head *entry)
{
    if (!__list_del_entry_valid(entry))
        return;

    __list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
    __list_del_entry(entry);
    entry->next = LIST_POISON1;
    entry->prev = LIST_POISON2;
}
static inline int list_empty(const struct list_head *head)
{
//    return READ_ONCE(head->next) == head;
    return head->next ==  head;
}
static inline int list_is_head(const struct list_head *list, const struct list_head *head)
{
    return list == head;
}

#define offsetof(TYPE, MEMBER)  ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({             \
     const typeof( ((type *)0)->member ) *__mptr =  (ptr); \
     (type *)( (char *)__mptr - offsetof(type, member) );})


#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

#define list_first_entry(ptr, type, member) \
	list_entry((ptr)->next, type, member)

#define list_next_entry(pos, member) \
	list_entry((pos)->member.next, typeof(*(pos)), member)

#define list_entry_is_head(pos, head, member)				\
	(&pos->member == (head))

#define list_for_each(pos, head) \
	for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next)

#define list_for_each_safe(pos, n, head) \
	for (pos = (head)->next, n = pos->next; \
	     !list_is_head(pos, (head)); \
	     pos = n, n = pos->next)

#define list_for_each_entry(pos, head, member)				\
	for (pos = list_first_entry(head, typeof(*pos), member);	\
	     !list_entry_is_head(pos, head, member);			\
	     pos = list_next_entry(pos, member))

#define list_for_each_entry_safe(pos, n, head, member)			\
	for (pos = list_first_entry(head, typeof(*pos), member),	\
		n = list_next_entry(pos, member);			\
	     !list_entry_is_head(pos, head, member); 			\
	     pos = n, n = list_next_entry(n, member))

#endif //LINKLIST_LIST_SIMPLE_H

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值