Linux内核入门(三)—— C语言基本功

iOS和macOS内核的关系?内核只用C语言,可以完成吗?内核代码只有用C语言这种高级语言吗?内核和驱动的关系? iOS和macOS的内核都基于XNU(X is Not Unix),早期是一个混合内核,结合了微内核和宏内核的特性。底层微内核架构MACH作为内核的核心,内核需要向它发送消息完成底层调度,但是,这种效率不高。新版本都已经改成宏内核,和Windows内核、Linux内核一样的宏内核。 阅读详情

 

  linux是一个复杂的软件系统,其内核源代码往往会用到一些在应用程序设计中部常见语言成分和编程技巧,这些都是分析内核源代码的基本功,希望大家能重点掌握。

 首先,gcc编译器从语言里吸收了“inline”和“const”。inline函数的大量使用,有利于提高运行效率,由此相当一部分代码从.c文件移入了.h文件。

 gcc增加了一种新的基本数据类型“long long int”用于支持64位CPU结构。

许多C语言都支持一些“属性描述符”,如“aligned”,“packed”等。相当于一些新的保留字。但是,在原来的C语言这些词并非保留字,而是一 些普通的变量,这样就会产生一些冲突。例如:inline在老的代码中已经是作为变量在使用了,为了解决这个问题,gcc将作为保留字的“inline” 设计成__inline__,这样就不会冲突了。

gcc还支持一个保留字“attribute”,用来作属性描述。如:attribute__ <<packed>>,这样packed就成为属性描述符(一种特殊保留字)而非变量了。

gcc中有大量对宏操作的使用,大家肯定会对内核代码中的一些宏操作的定义方式感到不解,如

#define DUMP_WRITE(addr, nr) do{ \

   memcpy(bufp,addr,nr);\
   bufp+=nr;\

}while(0)

这是必须的,是为了防止在IF-ELSE语句中使用该宏定义时发生错误。

 

linux内核代码中大量使用链表,但其使用方法与我们在《数据结构》中学到的不大一样,大家可以回顾一下我们在课堂上学习链表的时候,通常除了对其数据 结构进行了定义,还定义了若干对该结构的操作。但对于大量使用链表的linux内核来说,如果定义了一个结构就要定义其相关的操作的话,显然代码量不小。 为了提高效率,内核采用了一套通用的,一般的,可以用到各种不同数据结构的队列操作。

在include/linux/ list.h中,有如下申明:

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

于是,其余数据结构如inode等将其引用而作为一个成员,就可以完成下面将要讲到的若干操作而不去单独定义了。引用该数据结构的结构我们称为宿主结构 。

1.定义和初始化

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) /
    struct list_head name = LIST_HEAD_INIT(name)

 

需要注意的是,Linux 的每个双循环链表都有一个链表头,链表头也是一个节点, 只不过它不嵌入到宿主数据结构中,即不能利用链表头定位到对应的宿主结构,但可以由之获得虚拟的宿主结构指针。 LIST_HEAD()宏可以同时完成定义链表头,并初始化这个双循环链表为空。 静态定义一个list_head 类型变量,该变量一定为头节点。 name为struct list_head{}类型的一个变量, &(name)为该结构体变量的地址。用name结构体变量的始地址将该结构体变量进行初始化。

 

#define INIT_LIST_HEAD(ptr) do { /
    (ptr)->next = (ptr); (ptr)->prev = (ptr); /
} while (0)

动态初始化一个已经存在的list_head对象,ptr为一个结构体的指针, 这样可以初始化堆栈以及全局区定义的list_head对象。 ptr使用时候,当用括号,(ptr),避免ptr为表达式时宏扩展带来的异常问题。 此宏很少用于动态初始化内嵌的list对象,主要是链表合并或者删除后重新初始化头部。 若是在堆中申请了这个链表头,调用INIT_LIST_HEAD()宏初始化链表节点, 将next和prev指针都指向其自身,我们就构造了一个空的双循环链表。

2.6内核中内联函数版本如下:
static inline void INIT_LIST_HEAD(struct list_head *list)
 {
            list->next = list;
            list->prev = list;
  }
此时的参数有明确的类型信息struct list_head,同时可以看出其为指针,list无须象宏中那样(), 即使参数为表达式,其也是求值后再作为参数传入的。内联函数有严格的参数类型检查, 同时不会出现宏函数扩展带来的异常问题,但是运行效率和空间效率与宏函数一致。

2.通过对列头head链入一个新队列new

所有链表(包括添加、删除、移动和拼接等)操作都是针对数据结构list_head进行的。 提供给用户的的添加链表的操作有两种:表头添加和表尾添加。注意到, Linux双循环链表中有一个链表头,表头添加是指添加到链表头之后,而表尾添加则是添加到链表头的prev所指链表节点之后。

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

普通的在两个非空结点中插入一个结点,注意new、prev、next都不能是空值。 Prev可以等于next,此时在只含头节点的链表中插入新节点。 参数new指向将要链入队列的宿主数据结构(如inode、page等)内部指定的list_head数据结构。

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

在head和head->next两指针所指向的结点之间插入new所指向的结点。 即:在head指针后面插入new所指向的结点。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);
}

在结点指针head所指向结点的前面插入new所指向的结点。当head指向头节点时,也相当于在尾结点后面增加一个new所指向的结点。 注意:head->prev不能为空,即若head为头结点,其head->prev当指向一个数值,一般为指向尾结点,构成循环链表。

上述三个函数实现了添加一个节点的任务,其中__list_add()为底层函数,“__”通常表示该函数是底层函 数,供其他模块调用,此处实现了较好的代码复用,list_add和list_add_tail虽然原型一样,但调用底层函数__list_add时传递 了不同的参数,从而实现了在head指向节点之前或之后添加新的对象

3.脱链的操作

如果要从链表中删除某个链表节点,则可以调用list_del或list_del_init。 需要注意的是,上述操作均仅仅是把节点从双循环链表中拿掉, 用户需要自己负责释放该节点对应的数据结构所占用的空间,而这个空间本来就是用户分配的。

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

在prev和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所指的结点,同时将entry所指向的结点指针域封死。 对LIST_POISON1,LIST_POISON2的解释说明: Linux 内核中解释:These are non-NULL pointers that will result in page faults under normal circumstances, used to verify that nobody uses non-initialized list entries.
#define LIST_POISON1 ((void *) 0x00100100)
#define LIST_POISON2 ((void *) 0x00200200)
常规思想是:entry->next = NULL; entry->prev = NULL; 保证不可通过该节点进行访问。

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

删除entry所指向的结点,同时调用LIST_INIT_HEAD()把被删除节点为作为链表头构建一个新的空双循环链表。

 

4.移动节点操作

Linux还提供了两个移动操作:list_move和list_move_tail。

static inline void list_move(struct list_head *list,  struct list_head *head)
{
     __list_del(list->prev, list->next);
     list_add(list, head);
}

将list结点前后两个结点互相指向彼此,删除list指针所指向的结点, 再将此结点插入head,和
head->next两个指针所指向的结点之间。 即:将list所指向的结点移动到head所指向的结点的后面。

static inline void list_move_tail(struct list_head *list,   struct list_head *head)
{
     __list_del(list->prev, list->next);
     list_add_tail(list, head);
}

删除了list所指向的结点, 将其插入到head所指向的结点的前面,如果head->prev指向链表的尾结点的话,就是将list所指向的结点插入到链表的结尾。

 

5.链表判空

Linux中由list-head构成的双向循环链表中,通常有一个头节点,其不含有有效信息, 初始化时prev和next都指向自身。判空操作是判断除了头节点外是否有其他节点。

static inline int list_empty(const struct list_head *head)
{
     return head->next == head;
}

测试链表是否为空,如果是只有一个结点,head,head->next,head->prev都指向同一个结点, 则这里会返回1,表示空;但这个空不是没有任何结点,而是只有一个头结点, 因为头节点只是纯粹的list节点,没有有效信息,故认为为空。

static inline int list_empty_careful(const struct list_head *head)
{
     struct list_head *next = head->next;
     return (next == head) && (next == head->prev);
}

1.只有一个头结点head,这时head指向这个头结点,head->next,head->prev指向head, 即:head==head->next==head->prev,这时候list_empty_careful()函数返回1。

2.有两个结点,head指向头结点,head->next,head->prev均指向后面那个结点, 即:head->next==head->prev,而head!=head->next,head!=head->prev. 所以函数将返回0。

3.有三个及三个以上的结点,这是一般的情况,自己容易分析了。 注意:这里empty list是指只有一个空的头结点,而不是毫无任何结点。并且该头结点必须其head->next==head->prev==head。

   

 6.链表合并

Linux还支持两个链表的拼接,提供给用户的具体函数是list_splice和list_splice_init:

static inline void __list_splice(struct list_head *list, 
                                  struct list_head *head)
{
     struct list_head *first = list->next;
     struct list_head *last = list->prev;
     struct list_head *at = head->next;
     
     first->prev = head;
     head->next = first;
     
     last->next = at;
     at->prev = last;
}

将一个非空链表(list)插入到另外一个链表的head位置之后中。不作链表是否为空的检查,由调用者默认保证。 因为每个链表只有一个头节点,将空链表插入到另外一个链表中是没有意义的。但被插入的链表可以是空的。

static inline void list_splice(struct list_head *list,
                                struct list_head *head)
{
     if (!list_empty(list))
         __list_splice(list, head);
}

这种情况会丢弃list所指向的头结点,这是特意设计的,因为两个链表有两个头结点,要去掉一个头结点。只要list非空链,head无任何限制,该程序都可以实现链表合并。

static inline void list_splice_init(struct list_head *list,
                                     struct list_head *head)
{
     if (!list_empty(list)){
         __list_splice(list, head);
         INIT_LIST_HEAD(list);
     }
}

将一个链表的有效信息合并到另外一个链表后,重新初始化空的链表头。

 

 

7.返回宿主结构操作(重点)

如果需要有某种数据结构的队列,就在这种数据结构定义内部放上一个list_head数据结构。 例如,建立数据结构foo链表的方式是,在foo的定义中,嵌入了一个list_head成员list。 这里foo就是所指的"宿主":

typedef struct foo {
         …
        struct list_head list;
         …
    };

但是,如何通过list_head成员访问到宿主结构项呢?毕竟list_head不过是个连接件,而我们需要的是一个"特定"的数据结构链表。先介绍几个基本宏:offsetof、typeof、containerof。

-------/linux/stddef.h----------------
#define __compiler_offsetof(a,b) __builtin_offsetof(a,b)
而__builtin_offsetof()宏就是在编译器中已经设计好了的函数,直接调用即可。
    -------------------------------
    #undef offsetof //取消先前的任何定义,可以保证下面的定义生效
    #ifdef __compiler_offsetof
     #define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
     #else
     #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
     #endif

解释:一共4步
1. ( (TYPE *)0 ) 0地址强制 "转换" 为 TYPE结构的指针;
2. ((TYPE *)0)->MEMBER 访问结构中的数据成员;
3. &( ( (TYPE *)0 )->MEMBER)取出数据成员的地址;
4. (size_t)(&(((TYPE*)0)->MEMBER))结果转换类型.巧妙之处在于将0转换成(TYPE*),结构以内存空间首地址0作为起始地址,则成员地址自然为偏移地址;

==============================================================================================

((size_t) &((TYPE *)0)->MEMBER)
 ~~~~~~~~    ~ ~~~~~~~~~~~~~~~~~

转成无符号整数 取址     

也许不少人和我一样,有一个疑惑,地址 0 转换成指针,不是 NULL 吗,用它去访问成员,不是非法的吗?我一开始也百思不得其解,后来明白了,取成员的地址,并不等于访问该成员。我们可以用下面一段程序来验证一下:

#include <stdio.h>

struct st

{

  int a;     

  char b;    

  int c;     

};

 

int main()

{

  printf("c addr  : %d\n", &((struct st *)0)->c);

  printf("c value : %d\n",  ((struct st *)0)->c);

  return 0;

}

在 ubuntu 12.04 32bit 上用 GCC 4.6.3 编译后运行的结果:

ubuntu:~$ ./a.out

c addr  : 8

段错误 (核心已转储)

我是这么来理解的,地址 0 开始的这一段内存空间,就像是透明的充满机关的盒子,当里面放着结构体时,即使我们不打开这个盒子,从外面也可以知道里面的某个成员的位置(即地址),但你想打开盒子取出某个成员看看它的具体的值是多少时,却是万万不可的,会中箭而亡!

====================================================================================================================

举例说明:
#include<stdio.h>
typedef struct _test
{
     char i; 
     int j;
     char k;
}Test;
int main()
{
     Test *p = 0;
     printf("%p/n", &(p->k));
}
这里使用的是一个利用编译器技术的小技巧(编译器自动算出成员的偏移量),即先求得结构成员变量在结构体中的相对于结构体的首地址的偏移地址,然后根据结 构体的首地址为0,从而得出该偏移地址就是该结构体变量在该结构体中的偏移,即:该结构体成员变量距离结构体首的距离。在offsetof()中,这个 member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。对于给定一个结 构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

---------------------typeof()--------------------
unsigned int i; 
typeof(i) x; 
x=100; 
printf("x:%d/n",x); 
typeof() 是 gcc 的扩展,和 sizeof() 类似。
------------------------
在 container_of 宏中,它用来给 typeof() 提供参数,以获得 member 成员的数据类型;

---------------container_of()--------------------
container_of() 来自/linux/kernel.h
内核中的注释:

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

自己分析:

    1.(type *)0->member为设计一个type类型的结构体,起始地址为0,编译器将结构体的起始的地址加上此结构体成员变量的偏移得到此结构体成员变 量的地址,由于结构体起始地址为0,所以此结构体成员变量的偏移地址就等于其成员变量在结构体内距离结构体开始部分的偏移量。即:&(type *)0->member就是取出其成员变量的偏移地址。而其等于其在结构体内的偏移量:即为:(size_t)(& ((type *)0)->member)经过size_t的强制类型转换后,其数值为结构体内的偏移量。该偏移量这里由offsetof()求出。

    2.typeof( ( (type *)0)->member )为取出member成员的变量类型。用其定义__mptr指针;ptr为指向该成员变量的指针。__mptr为member数据类型的常量指针,其指向ptr所指向的变量处。

    3.(char *)__mptr转换为字节型指针。(char *)__mptr - offsetof(type,member) )用来求出结构体起始地址(为char *型指针),然后(type *)( (char *)__mptr - offsetof(type,member) )在(type *)作用下进行将字节型的结构体起始指针转换为type *型的结构体起始指针。

这就是从结构体某成员变量指针来求出该结构体的首指针。指针类型从结构体某成员变量类型转换为该结构体类型。

介绍了上面的几种基本宏后,对list_entry的理解就容易了。 list_entry()宏,获取当前list_head链表节点所在的宿主结构项。 第一个参数为当前list_head节点的指针,即指向宿主结构项的list_head成员。 第二个参数是宿主数据结构的定义类型。 第三个参数为宿主结构类型定义中list_head成员名。

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

扩展替换即为:

#define list_entry(ptr, type, member) /
        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

例:page = list_entry(curr, struct page, list)
则:page = ((struct page *)((char *)(curr)-(unsigned long)(&((struct page *)0)->list)))

curr是page结构内部成分list的地址,而我们要找的是list所处page结构本身的地址,所以要从curr减去一个位移量,即成份list在 page内部的位移量:&((struct page *)0)->list表示当结构page正好在地址0上时list的(绝对)地址。

 

 

 

 

获取宿主对象指针的原理如上图所示。我们考虑list_head类型成员member相对于宿主结构(类型为type)起始地址的偏移量。对于所有 该类型的宿主对象,这个偏移量是固定的。并且可以在假设宿主对象地址值为0,通过返回member成员的地址获得,即等于(unsigned long)(&((type *)0)->member)。这样,将当前宿主对象的"连接件"地址(ptr)减去这个偏移量,得到宿主对象地址,再将它转换为宿主数据结构类型的 指针。

需要重申的是,链表头没有被嵌入到宿主对象中,因此对链表头执行宿主对象指针获取操作是没有意义的。

 

8.遍历

(一)List-head链表遍历

遍 历是双循环链表的基本操作,为此Linux定义了一些宏。 list_for_each对遍历链表中的所有list_head节点,不涉及到对宿主结构的处理。list_for_each实际是一个 for 循环,利用传入的指向list_head结构的指针作为循环变量,从链表头开始(并跳过链表头),逐项向后移动指针,直至又回到链表头。 :

#define list_for_each(pos, head) /
     for (pos = (head)->next; prefetch(pos->next), pos != (head); /
         pos = pos->next)

head为头节点,遍历过程中首先从(head)->next开始,当pos==head时退出,故head节点并没有访问,这和list 结构设计有关,通常头节点就是纯粹的list结构,不含有其他有效信息,或者头节点含有其他信息,如内核PCB链表中的头节点为idle任务,但其不参予 比较优先级,因此此时头节点只是作为双向链表遍历一遍的检测标志。

为提高遍历速度,还使用了预取:

-----asm-x86_64/processor.h---prefetch()---------
static inline void prefetch(void *x) 
{ 
     asm volatile("prefetcht0 %0" :: "m" (*(unsigned long *)x));
}

将x指针作强制类型转换为unsigned long *型,然后取出该内存操作数,送入高速缓存。

 

----------------__list_for_each()-----------------
#define __list_for_each(pos, head) /
     for (pos = (head)->next; pos != (head); pos = pos->next)  

list_for_each()有prefetch()用于复杂的表的遍历,而__list_for_each()无prefetch()用于简单的表的遍历,此时表项比较少,无需缓存。

#define list_for_each_prev(pos, head) /
     for (pos = (head)->prev; prefetch(pos->prev), pos != (head); /          pos = pos->prev)    

 

反向遍历节点

如果在遍历过程中,包含有删除或移动当前链接节点的操作,由于这些操作会修改遍历指针,这样会导致遍历的中断。这种情况下,必须使用list_for_each_safe宏,在操作之前将遍历指针缓存下来:

/*
  * list_for_each_safe - iterate over a list safe against removal of list entry
  */
#define list_for_each_safe(pos, n, head) /
     for (pos = (head)->next, n = pos->next; pos != (head); /
         pos = n, n = pos->next)

在for循环中n暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。也就是说你可以遍历完当前节点后将其删除,同时可以接着访问下一个节 点,遍历完毕后就只剩下一个头节点。这就叫safe。十分精彩。典型用途是多个进程等待在同一个等待队列上,若事件发生时唤醒所有进程,则可以唤醒后将其 依次从等待队列中删除。

 

 

(二)遍历宿主对象

如果只提供对list_head结构的遍历操作是远远不够的,我们希望实现的是对宿主结构的遍历,即在遍历时直接获得当前链表节点所在的宿主结构 项,而不是每次要同时调用list_for_each和list_entry。对此,Linux提供了list_for_each_entry()宏,第 一个参数为传入的遍历指针,指向宿主数据结构,第二个参数为链表头,为list_head结构,第三个参数为list_head结构在宿主结构中的成员 名。

#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))  

我们举个例子,这是用于嵌套的结构体中的宏:
    struct example_struct
     {
         struct list_head list;
         int priority;
         ... //其他结构体成员
    };
     struct example_struct *node = list_entry(ptr,struct example_struct,list);

对比list_entry(ptr,type,member)可知有以下结果: 其中list相当于member成员,struct example_struct相当于type成员,ptr相当于ptr成员。而list{}成员嵌套于example_struct{}里面。ptr指向 example_struct{}中的list成员变量的。在list_entry()作用下,将ptr指针回转指向struct example_struct{}结构体的开始处。

pos当指向外层结构体,比如指向struct example_struct{}的结点,最开始时候,head指向链表结构体struct list_head{}的头结点,头节点不含有有效信息,(head)->next则指向第一个外层结点的内嵌的链表结点struct list_head{} list,由此得出的pos当指向第一个有效结点。member即是指出该 list为其内嵌的结点。

思路:用pos指向外层结构体的结点,用head指向内层嵌入的结构体的结点。用 (head)->next,pos->member.next(即:ptr->list.next)来在内嵌的结构体结点链表中遍历。 每遍历一个结点,就用list_entry()将内嵌的pos->member.next指针回转为指向该结点外层结构体起始处的指针,并将指针进 行指针类型转换为外层结构体型pos。&pos->member! = (head)用pos外层指针引用member即:list成员,与内层嵌入的链表之头结点比较来为循环结束条件。

当遍历到头节点时,此时并没有pos这样一个type类型数据指针,而是以member域强制扩展了一个type类型的pos指针,此时其member域的地址就是head指针所指向的头节点,遍历结束,头节点的信息没有被访问。

#define list_for_each_entry_reverse(pos, head, member) /
     for (pos = list_entry((head)->prev, typeof(*pos), m+ember); /
         prefetch(pos->member.prev), &pos->member != (head); /
         pos = list_entry(pos->member.prev, typeof(*pos), member))  

遍历倒置,分析与前类似。

如果遍历不是从链表头开始,而是从已知的某个pos结点开始,则可以使用 list_for_each_entry_continue(pos,head,member)。但为了确保pos的初始值有效,Linux专门提供了一 个list_prepare_entry(pos,head,member)宏,如果pos有值,则其不变;如果没有,则从链表头强制扩展一个虚pos指 针。将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

内核中的list_prepare_entry()的代码:

#define list_prepare_entry(pos, head, member) /
    ((pos) ? : list_entry(head, typeof(*pos), member)) 

分析: :前面是个空值,即:若pos不为空,则pos为其自身。等效于: (pos)? (pos): list_entry(head,typeof(*pos),member) 注意内核格式::前后都加了空格。

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

此时不是从头节点开始遍历的,但仍然是以头节点为结束点的,即没有遍历完整个链表。 要注意并不是从pos开始的,而是从其下一个节点开始的,因为第一个有效pos是从pos->member.next扩展得到的。

/**
  * 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 counter.
  * @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))

它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

#define list_for_each_entry_safe_continue(pos, n, head, member) /
     for (pos = list_entry(pos->member.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))

 

9.hlist哈希链表

 在include/Linux/list.h中有list链表与hlist哈希链表结构的定义,下面都列出它们的定义,可以对比一下:

struct list_head {
     struct list_head *next, *prev;
};
struct hlist_head {
     struct hlist_node *first;
};
struct hlist_node {
     struct hlist_node *next, **pprev;
};

 双头(next,prev)的双链表对于Hash表来说“过于浪费”,因而另行设计了一套Hash表专用的hlist数据结构——单指针表头双循 环链表,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的Hash表中存储的表头就能减少一半的空间消耗。

pprev因为hlist不是一个完整的循环链表而不得不使用。在list中,表头和节点是同一个数据结构,直接用prev没问题;在hlist 中,表头没有prev,也没有next,只有一个first。为了能统一地修改表头的first指针,即表头的first指针必须修改指向新插入的节 点,hlist就设计了pprev。hlist节点的pprev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则 是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的“*(node->pprev)”访问和修改前节点的next(或first)指针。

下面是hlist中常用的几个宏:

#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
#define INIT_HLIST_NODE(ptr) ((ptr)->next = NULL, (ptr)->pprev = NULL) 

 下面只列出hlist_add_before操作函数,其他hlist链表操作函数操作方法类似。这个函数中的参数next不能为空。它在next前面加入了n节点。函数的实现与list中对应函数类似。

static inline void hlist_add_before(struct hlist_node *n,
                                     struct hlist_node *next)
{
     n->pprev = next->pprev;
     n->next = next;
     next->pprev = &n->next;
     *(n->pprev) = n;
}

 

10.RCU操作保护的链表

RCU(Read-Copy Update)通过延迟写操作来提高同步性能,具体请参见第13章。这里只分析具有RCU的链表。

RCU常用来保护读操作占多数的链表与数组。具有RCU的链表的操作函数与普通链表操作函数的区别是在函数名后加上了_rcu,如list_for_each_rcu函数。

函数list_for_each_rcu的功能是遍历一个rcu保护的链表。其中,参数pos表示用来做链表位置计数的&struct list_head结构,参数head表示链表头。只要遍历被rcu_read_lock()保护,使用诸如list_add_rcu()的函数对链表同 时访问是安全的。

函数List_for_each_rcu列出如下:

#define list_for_each_rcu(pos, head) /
     for (pos = (head)->next, prefetch(pos->next); pos != (head); /
         pos = rcu_dereference(pos->next), prefetch(pos->next))

函数rcu_dereference在RCU读临界部分中取出一个RCU保护的指针。在需要内存屏障的体系中进行内存屏障(目前只有Alpha体系需要)。

 

11.Linux双循环链表综合实例

本文例子来自 http://isis.poly.edu/kulesh/stuff/src/klist/ ,只是对其中注释部分作了翻译。

#include <stdio.h>
#include <stdlib.h>
#include "list.h"
struct kool_list{
     int to;
     struct list_head list;
     int from;
};
int main(int argc, char **argv){
     struct kool_list *tmp;
     struct list_head *pos, *q;
     unsigned int i;
     struct kool_list mylist;
     INIT_LIST_HEAD(&mylist.list);
     /* 您也可以使用宏LIST_HEAD(mylist)来声明并初始化这个链表 */
     /*向链表中添加元素*/
     for(i=5; i!=0; --i){
         tmp= (struct kool_list *)malloc(sizeof(struct kool_list));
         /*INIT_LIST_HEAD(&tmp->list); 调用这个函数将初始化一个动态分配的list_head。也可以不调用它,因为在后面调用的add_list()中将设置next和prev域。*/
         printf("enter to and from:");
         scanf("%d %d", &tmp->to, &tmp->from);
         /*将tmp添加到mylist链表中*/
         list_add(&(tmp->list), &(mylist.list));
         /*也可以使用list_add_tail()将新元素添加到链表的尾部。*/
     }
     printf("/n");
     /*现在我们得到了数据结构struct kool_list的一个循环链表,我们将遍历这个链表,并打印其中的元素。*/
     /*list_for_each()定义了一个for循环宏,第一个参数用作for循环的计数器,换句话说,在整个循环过程中它指向了当前项的list_head。第二个参数是指向链表的指针,在宏中保持不变。*/
     printf("traversing the list using list_for_each()/n");
     list_for_each(pos, &mylist.list){
         /*此刻:pos->next指向了下一项的list变量,而pos->prev指向上一项的list变量。而每项都是 struct kool_list类型。但是,我们需要访问的是这些项,而不是项中的list变量。因此需要调用list_entry()宏。*/
         tmp= list_entry(pos, struct kool_list, list);
         /*给定指向struct list_head的指针,它所属的宿主数据结构的类型,以及它在宿主数据结构中的名称,list_entry返回指向宿主数据结构的指针。例如,在上面 一行, list_entry()返回指向pos所属struct kool_list项的指针。*/
         printf("to= %d from= %d/n", tmp->to, tmp->from);
     }
     printf("/n");
     /* 因为这是一个循环链表,我们也可以向前遍历。只需要将list_for_each替换为list_for_each_prev。我们也可以使用list_for_each_entry()遍历链表,在给定类型的项间进行循环。例如:*/
     printf("traversing the list using list_for_each_entry()/n");
     list_for_each_entry(tmp, &mylist.list, list)
     printf("to= %d from= %d/n", tmp->to, tmp->from);
     printf("/n");
     /*下面将释放这些项。因为我们调用list_del()从链表中删除各项,因此需要使用list_for_each()宏的"安全"版本,即 list_for_each_safe()。务必注意,如果在循环中有删除项(或把项从一个链表移动到另一个链表)的操作,必须使用这个宏。*/
     printf("deleting the list using list_for_each_safe()/n");
     list_for_each_safe(pos, q, &mylist.list){
         tmp= list_entry(pos, struct kool_list, list);
         printf("freeing item to= %d from= %d/n", tmp->to, tmp->from);
         list_del(pos);
         free(tmp);
     }
     return 0;
}

注意:上述代码在使用gcc编译时需要加上__KERNEL__定义。

linux内核代码par6(C语言编写) 没有加入商家辅助代码的Linux内核代码,完全C语言编写,开源的杰作 ,值得研究。 立即下载

相关推荐

DLMS—Cryptographic algorithms—(Green-Book)加密算法

密码学是数学的一个分支,基于数据的转换,并且可以用来提供多种安全服务:保密性、数据完整性、认证、授权和不可否认性。密码学依赖于两个基本组成部分:算法(或密码学方法)和密钥。算法是一个数学函数,而密钥是在转换过程中使用的参数。密码算法和密钥用于对数据应用密码保护(例如,加密数据或生成数字签名),以及移除或检查保护(例如,解密加密的数据或验证数字签名)。密码哈希函数:这类函数不需要密钥(尽管它们可以以使用密钥的模式使用)。哈希函数通常用作算法的一个组成部分,以提供安全服务。见9.2.3.2节。对称密钥算法。

一个bug是bug,多个bug能work 1706

用C语言编写你自己内核

当我们想在屏幕上打印它们时需要它们。如果您在实际硬件上运行此程序,则增加 tic_tac_toe.c 中的 launch_game() 和 kernel.c 中的 kernel_entry() 中的 sleep() 函数的值,如您所见,将每个值分配给 VGA 缓冲区是一种开销,因此我们可以为此编写一个函数,该函数可以在屏幕上打印我们的字符串(意味着将字符串中的每个字符值分配给 VGA 缓冲区)。现在你有一个VGA数组,你只需要根据屏幕上打印的内容为数组的每个索引分配特定的值,就像我们通常在分配值到数组。..

Meta.Qing的博客 1703

大漠插件找图找字避坑手册:从ziku0.txt字库制作到易语言精准识别(附综合工具实操)

本文深入解析大漠插件在图像识别中的核心难点,提供从ziku0.txt字库精准制作到易语言高效调用的完整避坑指南。重点讲解如何利用大漠综合工具科学设置偏色与二值化参数,生成高质量色彩描述字符串,并分享识别失败的系统排查方法与进阶优化策略,旨在实现99%的稳定识别率。

weixin_29264201的博客 1204

C语言高级-C内核

C有操作系统静态内存分配,动态内存分配使用API,例如mallocfree接口。对于单片机而言,C语言程序内存是用来存储可变数据(变量)的,常量存储在flash当中。本博客参考《嵌入式Linux与物联网软件开发C语言内核深度解析》C++new对象分配内存,delete删除对象。C无操作系统通过编译器提供的变量名来管理内存。Java/C#虚拟机。........................

Joyer_cc 的博客 952

若依RuoYi文件上传漏洞实战:如何安全配置/profile/upload路径(附避坑指南)

本文深入解析若依RuoYi框架中/profile/upload路径的文件上传漏洞,提供详细的安全配置方案和实战指南。通过权限修正、输入验证强化和存储隔离等多层防御策略,有效防范未授权访问和恶意文件上传风险,保障系统安全。

pink的博客 776

抖音视频数据抓取

最近经常有人问我抓取抖音视频的数据,问我一些很小白的问题,算法给他也不会用。 还是先抓包,获取需要的接口和参数,相关的算法在其它文章里面有描述。 搜索个视频看看 查看抓的包信息: 可以看到,这里是post请求...

2501_91048859的博客 3097

一步步编写操作系统 44 用c语言编写内核1

先来个简单的,欢迎我们神秘嘉宾——main.c。这是我们第一个c语言代码。 1 int main(void) { 2 while(1); 3 return 0; 4 } 它没法再简单啦,简单的程序似乎能帮助咱们更容易的理解所学的知识,哈哈,我说的是似乎,其实,再长的代码,编译后生成的文件结构也是由那几个部分组成,万变不离其宗。这里所说的文件结构是指将来要说的elf文件格式,在此不多说,...

sinolover的专栏 2501

告别屏幕闪烁:手把手教你用示波器抓取并分析MIPI DSI信号(以RK平台为例)

本文详细介绍了如何利用示波器抓取并分析MIPI DSI信号,以解决LCD屏幕闪烁、花屏等问题。以RK平台为例,从工具准备、示波器参数配置到信号捕获与波形解读,提供了一套完整的调试方案,帮助硬件工程师快速定位和解决显示接口信号完整性问题。

weixin_33704591的博客 653

一步步编写操作系统 42 用c语言编写内核

在这之前,我们一直用汇编语言直接与机器对话,如果大家不知道这个世界上有高级语言的话,我想大家也不会觉得写汇编代码的过程很辛苦,哈哈,幸福确实是比较出来的。相对于汇编语言,用c 语言写内核是非常爽的事,马上我们就要步入内核实践中啦,所以现在和大伙儿聊聊c语言写内核的体会。 通常,我们写的代码都是直接编译成可执行文件,那是因为我们是在写用户程序,操作系统为咱们提供了很多便利,所以编译和链接一气呵成,...

sinolover的专栏 4120

SAP FICO 中的 F-02 :概念、使用场景及案例解析

在 SAP FICO (财务会计与管理会计) 模块中,F-02 是一个用于录入总账凭证的事务代码。这个事务代码主要用于手动输入总账凭证,适用于各类财务交易,例如资产购入、费用支付、收入确认等。F-02 的重要性在于,它允许财务人员在 SAP 系统中创建凭证,并确保这些凭证能够正确影响总账账户。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2060

工业质检终极方案:C#上位机融合 YOLO+Blob分析(一站式尺寸测量+缺陷检测)

本次上位机界面严格遵循工业现场操作规范简洁、直观、按钮大、文字清晰、参数可视化调节,操作人员为车间工人/质检员,无需复杂操作,所有功能一目了然。所有控件均为WinForms原生控件,拖拽即可完成布局,无任何自定义控件,开发效率拉满,核心布局永久复用本次实战完成的C#上位机+YOLO+Blob融合质检方案,是工业规则零件全尺寸质检的终极落地方案,完美解决了单一检测方案的所有短板,具备以下不可替代的工业价值。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1341

Linux内核入门(三)—— C语言基本功

    linux是一个复杂的软件系统,其内核源代码往往会用到一些在应用程序设计中部常见语言成分和编程技巧,这些都是分析内核源代码的基本功,希望大家能重点掌握。    首先,gcc编译器从语言里吸收了“inline”和“const”。inline函数的大量使用,有利于提高运行效率,由此相当一部分代码从.c文件移入了.h文件。    gcc增加了一种新的基本数据类型“long

云(存储),松(耦合) 8717

CANoe制作网关实现CAN(FD)报文故障注入(报文长度/timeout/信号错误/E2E)2

CANoe制作网关实现CAN报文故障注入(报文长度/timeout/信号错误/E2E) 2.新建CANoe工程 3.透传功能 4.故障注入panel面板 5.CAPL代码 5.1故障注入开始、停止、定时 5.2DLC故障注入 5.3 Timeout故障注入 5.4 信号故障注入

天赐好车的博客 3673

【Linux内核设计思想】一、Linux内核介绍

Linux内核设计思想与源码分析一:Linux内核相关知识介绍。

仅以此博客记录日常学习工作中所思所得,水平有限,难登大雅,万望海涵。 2850

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

Linux 内核 C 语言深度解析

课程简介 在看一些 GNU 开源软件,或者阅读 Linux 内核、驱动源码时,相信大家和我一样,经常会遇到一些“看似熟悉”,但一仔细分析又不是很懂的 C 语言“稀奇古怪”的语法。其实这些都是 GNU C 对 C 语言语法的扩展。一般的教材中基本不会讲到这些,所以看不懂这些特殊的 C 语法应用也很正常。 此达人课将带领大家,一起学习 Linux 内核或 GNU 开源软件中对 C 语言各种扩展语法的使...

GitChat 4615

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Linux内核入门—— C语言基本功

linux是一个复杂的软件系统,其内核源代码往往会用到一些在应用程序设计中部常见语言成分和编程技巧,这些都是分析内核源代码的基本功,希望大家能重点掌握。     首先,gcc编译器从语言里吸收了“inline”和“const”。inline函数的大量使用,有利于提高运行效率,由此相当一部分代码从.c文件移入了.h文件。     gcc增加了一种新的基本数据类型“long long in

CRLFreedom的专栏 812

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

何为 Linux 内核开发,怎么学好 Linux 内核?

此文包含 Linux 系统的学习路径和书籍推荐。 我觉得学习 Linux 系统,内核驱动时,最开始只需要 ‘Know what, not know how ’。 不用去探究细节,只需要知道整体的框架,知道有哪些需要我们重视的内容即可。 何为 Linux 内核开发? 首先,初步认识下 Linux kernel Linux 内核的框架如上图。 设备子系统负责和硬件打交道。 大部分工作集中在设备子系统部分。 内核开发是什么? 广义上讲,新增或修改上图中内核部分的所有子系统。 非 Linux 源码贡献者.

C/C++Linux、音视频、DPDK 4756

Linux内核中常用的C语言技巧

Linux内核中常用的C语言技巧 相信读者在阅读本文之前已经学习过C语言了,但是想精通C语言还需要下一番苦功夫。Linux内核是基于C语言编写的,熟练掌握C语言是深入学习Linux内核的基本要求。本文摘自《奔跑吧Linux内核 入门篇》。 GNU C语言的扩展 GCC的C编译器除了支持ANSI C标准之外,还对C语言进行了很多的扩充。这些扩充对代码优化、目标代码布局以及安全检查等方面提供了很...

CAESAR的专栏 2720

Linux内核学习路线 有入门到深入

学习linux内核,这个可不像学一门语言,c或者java一个月或者3月你就能精通掌握。学习linux内核是需要一步一步循序渐进,掌握正确的linux内核学习路线对学习至关重要,本篇文章就来分享学习linux内核的一些建议吧。 1. 了解操作系统基本概念。如果不会,可以学习《操作系统:设计与实现》Andrew S.Tanenbaum 写的那本。以MINIX为例子讲解操作系统的概念。非常推荐。 2. 有了操作系统的基本概念以后,可以了解Linux的机制了。推荐《Linux内核设计与实现》Robert L.

终码一生 6247

Linux内核入门:C语言技巧篇

在Linux内核代码中使用的 C语言技巧与拓展用法远不止这一点,而想要真正去了解学习这些用法,就必须得通过大量地代码阅读与自我试验,而这只是学习底层内核代码的门槛而已,加油!如有错误,欢迎指正!

qq_52267946的博客 1293

写给三弟的 C语言编程 快速入门

三弟,你要的C语言快速入门,已整理好 请接收。

码刀攻城 3926

【Linux内核设计思想】三、Linux内核的启动过程

欢迎关注博主或加入【】一起学习和分享Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术。关注公粽号回复关键词 “python项目实战” 即可获取美哆商城视频资源!博主介绍:优快云博客专家,优快云优质创作者,优快云实力新星,优快云内容合伙人;阿里云社区专家博主;华为云社区云享专家;

仅以此博客记录日常学习工作中所思所得,水平有限,难登大雅,万望海涵。 1896

C语言与操作系统内核模块开发:Linux内核模块编写、设备驱动开发与内核调试(一)

操作系统内核作为计算机系统的指挥中心,其重要性不言而喻。它是操作系统中最为核心的部分,负责管理硬件资源、调度进程、提供系统服务、维护安全边界以及协调用户程序与硬件设备之间的交互。内核的性能与稳定性直接决定了整个系统的效率与可靠性,是支撑现代计算技术发展的基石。

weixin_56154577的博客 1589

从Linux内核中学习高级C语言宏技巧

Linux内核可谓是集C语言大成者,从中我们可以学到非常多的技巧,本文来学习一下宏技巧,文章有点长,但耐心看完后C语言level直接飙升。

闪耀大叔的博客 2311
上一篇: Linux内核入门(二)——用户态向内核态切换
下一篇: Linux内核入门(四)—— 内核汇编语言规则
废言Pro
博客等级 码龄16年 1092粉丝 142原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值