• 一张图说明Linux内核

    2014-04-04 13:03:09
    这是很久以前从网上找到的一张图片,近来看看,挺有意思的,就发出来吧。

    这是很久以前从网上找到的一张图片,近来看看,挺有意思的,就发出来吧。

     

    展开全文
  • Linux内核使用最广泛的开源内核,在服务器和智能手机领域处于统治地位,物联网、大数据、云计算和人工智能等热点技术也离不开Linux内核。对于商业公司而言,采用开源的Linux内核可以享受很多好处,比如节约成本,...

    Linux内核是使用最广泛的开源内核,在服务器和智能手机领域处于统治地位,物联网、大数据、云计算和人工智能等热点技术也离不开Linux内核。对于商业公司而言,采用开源的Linux内核可以享受很多好处,比如节约成本,可以利用行业先进的技术,还可以根据自己的需求定制、修改内核。对于个人而言,从Linux内核中可以学习先进的设计方法和编程技术,为内核贡献代码可以证明自己的技术实力。

    可是,当我们准备学习Linux内核时,会发现Linux内核的代码庞大而复杂,在没有专业书籍指导的情况下,读懂代码是一件非常困难的事情。想要深入理解Linux内核的你,需要入手这样一本书《Linux内核深度解析》。


    点此进入样章试读【点此购买】

    对于从事应用程序开发的软件工程师,是否有必要学习内核呢?

    应用程序通常使用封装好的库,看起来似乎和内核没有关系,但是库是在内核提供的系统调用的基础上做了一层封装。读者如果研究了库函数和内核配合实现库函数提供的功能,那么对软件运行过程的理解将会更深刻,个人的技术水平也将会提升到新的高度—能够设计开发出高质量的应用程序,在软件运行过程中出现问题时可以快速地分析定位。另外,内核代表了软件行业的最高编程技术,这些编程技术也适用于应用程序。所以答案当然是肯定的啦!

    这是一本基于ARM64架构的Linux 4.x内核的书,大量图例描述数据结构之间的关系和函数的执行流程;结合代码分析,以通俗化语言全面剖析Linux内核。

    学习《Linux内核深度解析》,需要具备ARM64处理器的基础知识,推荐以下两篇文档,读者可以从ARM公司的网站下载。

    (1)“ARM Cortex-A Series Programmer’s Guide for ARMv8-A”:这篇文档接近300页,适合入门学习。

    (2)“ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile”:这篇文档有6000多页,写得很详细,适合当作工具书来查询。

    《Linux内核深度解析》特色:

    本书介绍4.12版本的Linux内核,建议读者在阅读本书时到Linux内核的官方网站中下载一份代码,对照代码学习。推荐使用“Source Insight”软件阅读代码。

    Linux内核支持多种处理器架构,处理器架构特定的代码放在“arch”目录下。ARM处理器在手机和平板电脑等移动设备上处于统治地位。ARM处理器从ARMv7演进到支持64位的ARMv8,ARM公司重新设计了处理器架构,ARMv8定义了AArch64和AArch32两种执行状态,AArch64是64位架构;AArch32是32位架构,兼容ARMv7。因为ARMv8和ARMv7的差别很大,所以Linux内核把ARMv8和ARMv7当作两种不同的处理器架构,ARMv7架构的代码放在“arch/arm”目录下,ARMv8架构的代码放在“arch/arm64”目录下。人们通常把ARMv8架构的AArch64执行状态称为ARM64架构。本书在介绍Linux内核时选择ARM64处理器架构。

    学习内核,关键是要理解数据结构之间的关系和函数调用关系。内核中数据结构之间的关系错综复杂,函数调用层次深,有些函数中的分支非常多,一个函数就可能涉及很多技术,这些都是初学者学习中的障碍。作者建议读者在学习时抓住主要线索,弄清楚执行流程,刚开始不要过多关注函数的细节。为了方便学习,作者绘制了很多图来描述数据结构之间的关系和函数的执行流程。另外,作者在介绍每种技术时会先介绍使用方法,从使用方法开始学习技术,相信会对读者理解技术有很大的帮助。

    这本书写了什么?

    第1章介绍内核的引导过程,本书选择常用的引导程序U-Boot,读者可以从德国DENX软件工程中心的网站下载U-Boot的代码,对照学习。

    第2章介绍内核管理和调度进程的技术原理。

    第3章介绍内核管理虚拟内存和物理内存的技术原理。

    第4章介绍内核处理异常和中断的技术原理,以及系统调用的实现方式。

    第5章介绍内核实现的各种保护临界区的互斥技术。

    第6章介绍内核的虚拟文件系统,内核使用虚拟文件系统支持各种不同的文件系统。

    适用人群:

    本书适用于维护或者开发Linux内核的软件工程师、基于Linux内核开发设备驱动程序的软件工程师,以及想要学习了解Linux内核的软件工程师和学生。

    “行是知之始,知是行之成”,学习Linux内核技术尤其如此。要真正消化理解Linux内核,离不开大量的工程实践。希望本书可以成为你前进路上的好帮手!

    展开全文
  • 做内核驱动开发经常会使用linux内核最经典的双向链表 list_head, 以及它的拓展接口(或者宏定义): list_add , list_add_tail, list_del , list_entry ,list_for_each , list_for_each_entry ...... 每次看到这些...

          做内核驱动开发经常会使用linux内核最经典的双向链表 list_head, 以及它的拓展接口(或者宏定义): list_add , list_add_tail, list_del , list_entry ,list_for_each , list_for_each_entry ...... 

          每次看到这些接口,感觉都很像,今天专门研究了一下内核,对它们做一些总结,希望为后续开发提供方便。

    首先找到list_head 结构体定义,kernel/inclue/linux/types.h  如下:  

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

           然后就开始围绕这个结构开始构建链表,然后插入、删除节点 ,遍历整个链表等等,其实内核已经提供好了现成的接口,接下来就让我们进入 kernel/include/linux/list.h中:

    一. 创建链表

        内核提供了下面的这些接口来初始化链表:

    #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);
    	list->prev = list;
    }

    如:  可以通过 LIST_HEAD(mylist) 进行初始化一个链表,mylist的prev 和 next 指针都是指向自己。


       struct list_head mylist = {&mylist,  &mylist} ;   

           但是如果只是利用mylist这样的结构体实现链表就没有什么实际意义了,因为正常的链表都是为了遍历结构体中的其它有意义的字段而创建的,而我们mylist中只有 prev和next指针,却没有实际有意义的字段数据,所以毫无意义。

          综上,我们可以创建一个宿主结构,然后在此结构中再嵌套mylist字段,宿主结构又有其它的字段(进程描述符 task_struct,页面管理的page结构,等就是采用这种方法创建链表的)。为简便理解,定义如下:

    struct  my_task_list {
        int val ;
        struct list_head mylist;
    }

    创建第一个节点

    struct my_task_list first_task = 
    { .val = 1,
      .mylist = LIST_HEAD_INIT(first_task.mylist)
    };

    这样mylist 就prev 和 next指针分别指向mylist自己了,如下图:



    二. 添加节点

    内核已经提供了添加节点的接口了

    1.  list_add 

        如下所示。 根据注释可知,是在链表头head后方插入一个新节点new。

        并且还说了一句:这个接口利用实现堆栈  (why? 稍后再做分析)

    /**
     * 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再调用__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);
    }

    其实就是在head 链表头后和链表头后第一个节点之间插入一个新节点。然后这个新的节点就变成了链表头后的第一个节点了。

    依然用上面的my_task_list结构体举例子

    首先我们创建一个链表头   header_task 

     LIST_HEAD(header_task);

       

    然后再创建实际的第一个节点

    struct my_task_list my_first_task = 
    { .val = 1,
      .mylist = LIST_HEAD_INIT(my_first_task.mylist)
    };

    接着把这个节点插入到header_task之后

    list_add(&my_first_task.mylist,  &header_task);



    然后在创建第二个节点,同样把它插入到header_task之后

    struct my_task_list my_second_task = 
    { .val = 2,
      .mylist = LIST_HEAD_INIT(my_second_task.mylist)
    };
    其实还可以用另外一个接口 INIT_LIST_HEAD 进行初始化(参数为指针变量), 如下:
    struct my_task_list my_second_task;
    my_second_task.val = 2;
    INIT_LIST_HEAD(&my_second_task.mylist);

    list_add(&my_second_task.mylist, &header_task)


    以此类推,每次插入一个新节点,都是紧靠着header节点,而之前插入的节点依次排序靠后,那最后一个节点则是第一次插入header后的那个节点。最终可得出:先来的节点靠后,而后来的节点靠前,“先进后出,后进先出”。所以此种结构类似于 stack“堆栈”, 而 header_task就类似于内核stack中的栈顶指针esp, 它都是紧靠着最后push到栈的元素。

    2. list_add_tail 接口 

    上面所讲的list_add接口是从链表头header后添加的节点。 同样,内核也提供了从链表尾处向前添加节点的接口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);
    }

    从注释可得出:(1)在一个特定的链表头前面插入一个节点

    (2)这个方法很适用于队列的实现 (why?)

    进一步把__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);
    }

        所以,很清楚明了, list_add_tail就相当于在链表头前方依次插入新的节点(也可理解为在链表尾部开始插入节点,此时,header节点既是为节点,保持不变)

        利用上面分析list_add接口的方法可画出数据结构图形如下。

    (1)创建一个 链表头(实际上应该是表尾), 同样可调用 LIST_HEAD(header_task);

        


    (2)插入第一个节点 my_first_task.mylist , 调用 list_add_tail(& my_first_task.mylist,  & header_task);

        

    (3) 插入第二个节点my_second_task.mylist,调用list_add_tail(& my_second_task.mylist,  &header_task);



    依此类推,每次插入的新节点都是紧挨着 header_task表尾,而插入的第一个节点my_first_task排在了第一位,my_second_task排在了第二位,可得出:先插入的节点排在前面,后插入的节点排在后面,“先进先出,后进后出”,这不正是队列的特点吗(First in First out)!

    三. 删除节点

          内核同样在list.h文件中提供了删除节点的接口 list_del(), 让我们看一下它的实现流程

    static inline void list_del(struct list_head *entry)
    {
    	__list_del_entry(entry);
    	entry->next = LIST_POISON1;
    	entry->prev = LIST_POISON2;
    }
    /*
     * 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 - 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(struct list_head *entry) 接口就可以删除链表中的任意节点了,但需注意,前提条件是这个节点是已知的,既在链表中真实存在,切prev,next指针都不为NULL。

        

    四. 链表遍历

         内核是同过下面这个宏定义来完成对list_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; pos != (head); pos = pos->next)
        上面这种方式是从前向后遍历的,同样也可以使用下面的宏反向遍历:
    /**
     * 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)

       而且,list.h 中也提供了list_replace( 节点替换)  list_move(节点移位)  ,翻转,查找等接口,这里就不在一一分析了。

    五. 宿主结构

    1.找出宿主结构  list_entry(ptr, type, member)

        上面的所有操作都是基于list_head这个链表进行的,涉及的结构体也都是:

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

            其实,正如文章一开始所说,我们真正更关心的是包含list_head这个结构体字段的宿主结构体,因为只有定位到了宿主结构体的起始地址,我们才能对对宿主结构体中的其它有意义的字段进行操作。

    struct  my_task_list {
        int val ;
        struct list_head mylist;
    }
          那我们如何根据mylist这个字段的地址而找到宿主结构my_task_list的位置呢???

          做linux驱动开发的同学是不是想到了LDD3这本书中经常使用的一个非常经典的宏定义呢!那就是:

    container_of(ptr, type, member)

        没错就是它,在LDD3这本书中的第三章字符设备驱动,以及第十四章驱动设备模型中多次提到,所以我觉得这个宏应该是内核最经典的宏之一。 那list.h中使用什么接口实现的这个转换功能呢?

    /**
     * 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.h中提供了list_entry宏来实现对应地址的转换,但最终还是调用了container_of宏,所以container_of宏的伟大之处不言而喻。 那接下来让我们揭开她的面纱:

        此宏在内核代码 kernel/include/linux/kernel.h中定义(此处kernel版本为3.10;新版本4.13之后此宏定义改变,但实现思想保持一致)

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

    而offsetof定义在 kernel/include/linux/stddef.h ,如下:

    #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

    看下container_of宏的注释:

    (1)根据结构体重的一个成员变量地址导出包含这个成员变量mem的struct地址。

    (2)参数解释:

             ptr  : 成员变量mem的地址    

            type: 包含成员变量mem的宿主结构体的类型

            member: 在宿主结构中的mem成员变量的名称

            如果用我们之前定义的结构体struct my_task_list举例

    struct  my_task_list {
        int val ;
        struct list_head mylist;
    }
    struct my_task_list first_task = 
    { .val = 1,
      .mylist = LIST_HEAD_INIT(first_task.mylist)
    };

        ptr   :    &first_task.mylist

        type  :  struct my_task_list

        member :  mylist

    而container_of宏的功能就是根据 first_task.mylist字段的地址得出first_task结构的其实地址。

    把上面offsetof的宏定义代入container_of宏中,可得到下面定义:

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

    再把宏中对应的参数替换成实参:

       const typeof( ((struct my_task_list *)0)->mylist ) *__mptr = (&first_task.mylist); \  
        (struct my_task_list *)( (char *)__mptr - ((size_t) &((struct my_task_list *)0)->mylist) );})  

    typeof 是 GNU对C新增的一个扩展关键字,用于获取一个对象的类型 ,比如这里((struct my_task_list *)0)->mylist 是把0地址强制转换成struct my_task_list 指针类型,然后取出mylist元素。 然后再对mylist元素做typeof操作,其实就是获取 my_task_list结构中mylist字段的数据类型struct list_head,所以这行语句最后转化为:

    const struct list_head *__mptr  = (&first_task.mylist);

    第二条语句中在用 __mptr这个指针 减去 mylist字段在 my_task_list中的偏移(把0地址强制转换成struct my_task_list指针类型,然后取出mylist的地址,此时mylist的地址也是相对于0地址的偏移,所以就是mylist字段相对于宿主结构类型struct my_task_list的偏移) 正好就是宿主结构的起始地址。C语言的灵活性得到了很好的展示!!!


    2. 宿主结构的遍历

            我们可以根据结构体中成员变量的地址找到宿主结构的地址, 并且我们可以对成员变量所建立的链表进行遍历,那我们是不是也可以通过某种方法对宿主结构进行遍历呢?    

            答案肯定是可以的,内核在list.h中提供了下面的宏:

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

     其中,list_first_entry 和  list_next_entry宏都定义在list.h中,分别代表:获取第一个真正的宿主结构的地址; 获取下一个宿主结构的地址。它们的实现都是利用list_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,则退出,从而实现了宿主结构的遍历。如果要循环对宿主结构中的其它成员变量进行操作,这个遍历操作就显得特别有意义了。

            我们用上面的 my_task_list结构举个例子:

    struct my_task_list *pos_ptr = NULL ; 
    list_for_each_entry (pos_ptr, & header_task, mylist ) 
        { 
             printk ("val =  %d\n" , pos_ptr->val); 
        }

        

    参考文档:https://kernelnewbies.org/FAQ/LinkedLists

                    《Understanding linux kernel》

                    《Linux device drivers》

    展开全文
  • Linux内核中DMA分析

    2017-09-04 20:39:46
     DMA---直接内存访问 用来在设备内存与主存RAM之间直接进行...--内核为设备驱动程序提供了统一的DMA接口,这些接口屏蔽了不同平台之间的差异。 --一致性映射类型的dma_alloc_coherent/流式映射类型的dma_map_si
    

    DMA---直接内存访问
    用来在设备内存与主存RAM之间直接进行数据交换,这个过程无需CPU干预,
    对于系统中有大量数据交换的设备而言,如果能够充分利用DMA特性,可以大大提高系统性能。

    1.内核中DMA层

    --内核为设备驱动程序提供了统一的DMA接口,这些接口屏蔽了不同平台之间的差异。

    --一致性映射类型的dma_alloc_coherent/流式映射类型的dma_map_single

    不同的平台(X86/ARM)提供各自的struct dma_map_ops对象来实现对应的DMA映射。

    2.物理地址和总线地址

    物理地址是指cpu地址信号线上产生的地址。

    总线地址可以认为从设备的角度看到的地址,不同类型的总线具有不同类型的总线地址。

    DMA地址--用来在设备和主存之间寻址,虽然是总线地址,但是从内核代码的角度来看,被称为DMA地址,

    与之相对应的数据结构是dma_addr_t(-->typedef u32 dma_addr_t;)。

    3.DMA映射
    3.1 基本原理
     DMA映射主要为在设备与主存之间建立DMA数据传输通道时,在主存中为该DMA通道分配内存空间的行为,该内存空间
    也称为DMA缓冲区。这个任务原本可以很简单,但是由于现代处理器cache的存在,使得事情变得复杂。

    3.2 RAM与cache内容的一致性问题

    1.出现问题原因
    现代处理器为了提升系统性能,在CPU与RAM之间加入了高速缓存cache,
    所以当在RAM中为一个DMA通道建立一段缓冲区时,
    必须仔细考虑RAM与cache内容的一致性问题。

    /*具体的分析*/
    如果RAM与Device之间的一次数据交换改变了RAM中DMA缓冲区的内容,
    而cache中缓存了DMA缓冲区对应的RAM中一段内存块。
    如果没有机制保护cache中的内容被新的DMA缓冲区数据更新(或者无效),
    那么cache和他对应的RAM中的一段内存块在内容上出现了不一致,
    此时如果CPU去读取device传到RAM的DMA缓冲区中的数据,
    它将直接从cache获得数据,这些数据显然不是它所期望的,
    因为cache对应的RAM中的数据已经更新了。


    2.解决问题--
    就cache一致性问题,不同的体系架构有不同的策略,有些是在硬件层面予以保证(x86平台)
    有些没有硬件支持而需要软件的参与(ARM品台)。
    --Linux内核中的通用DMA尽力为设备驱动程序提供统一的接口来处理cache缓存一致性的问题,
    而将大量品台相关的代码对设备驱动程序隐藏起来。

    3.3 DMA映射三种情况

    1. 一致性DMA映射

    linux内核DMA层为一致性DMA映射提供的接口函数为dma_alloc_coherent()
    -->
    /*
     * Allocate DMA-coherent memory space and return both the kernel remapped
     * virtual and bus address for that space.
     */
    void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)

    函数分配的一致性DMA缓冲区的总线地址(也是DMA地址)由参数handle带回,
    函数返回的则是映射到DMA缓冲区的虚拟地址的起始地址


    接着调用->__dma_alloc(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp,pgprot_t prot)
    {
     struct page *page;
     void *addr;

     *handle = ~0;
     size = PAGE_ALIGN(size);

     page = __dma_alloc_buffer(dev, size, gfp); /*分配大小为size的一段连续的物理内存页,并且对应得虚拟地址范围已经使cache失效*/
     if (!page)
      return NULL;

     if (!arch_is_coherent())/*arch_is_coherent确定体系结构是否通过硬件来保证cache一致性(arm不是,所以函数返回0)*/
      addr = __dma_alloc_remap(page, size, gfp, prot);/*在(?--0xffe0 0000)之间寻找一段虚拟地址段,将其重建新映射到page,
    --------------------------------------------------------->由于关闭了cache功能所以保证了DMA操作时不会出现cache一致性问题*/-
     else
      addr = page_address(page);

     if (addr)
      *handle = pfn_to_dma(dev, page_to_pfn(page));

     return addr;
    }

    一致性所获得的DMA缓冲区的大小都是页面的整数倍,如果驱动程序需要更小的DMA一致性的DMA缓冲区,则应该使用内核提供的DMA池(pool)机制

    /*释放一致性DMA缓冲区*/
    void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle)
    /*cpu_addr表示要释放的DMA缓冲区的起始虚拟地址,参数bus表示DMA缓冲区的总线地址*/

    对于一致性DMA映射,在分配DMA缓冲区时各平台相关代码已经从根本上解决了cache一致性问题.

    但是,一致性映射也会遇到无法克服的困难,主要是指驱动程序中使用的DMA缓冲区并非由驱动程序分配,

    而是来自其他模块(如网络设备驱动程序中用于数据包传输的skb->data所指向的缓冲区),此时需要流式DMA映射。

    2. 流式DMA映射

    流式DMA映射的特点是DMA传输通道使用的缓冲区不是由当前驱动程序自身分配的,
    而且往往对每次DMA传输都会重新建立一个流式映射的缓冲区,所以使用流式DMA映射时,
    设备驱动程序必须小心负责处理可能出现的cache一致性。

    linux内核DMA层为设备驱动提供的建立流式DMA映射的函数---dma_map_single

    //dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, NULL)

    static inline dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
      size_t size, enum dma_data_direction dir)
              
    /*dev-->设备对象指针,cpu_addr-->cpu的虚拟地址,
    size-->流式空间的范围,dir-->表明当前流式映射中DMA传输通道中的数据方向*/
    函数返回数据类型-->dma_addr_t即表示DMA操作中的源地址和目的地址。

    /*下面分析ARM的平台*/
    /*将cpu_addr表示的段虚拟地址映射到DMA缓冲区中,返回该缓冲区的起始地址*/
    2.1
    static inline dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
      size_t size, enum dma_data_direction dir)
    {
     dma_addr_t addr;

     addr = __dma_map_single(dev, cpu_addr, size, dir);////
     
     return addr;
    }

    2.2
    static inline dma_addr_t __dma_map_single(struct device *dev, void *cpu_addr,
      size_t size, enum dma_data_direction dir)
    {
     __dma_single_cpu_to_dev(cpu_addr, size, dir);
     return virt_to_dma(dev, cpu_addr);
    }

    2.3
    void ___dma_page_cpu_to_dev(struct page *page, unsigned long off,
     size_t size, enum dma_data_direction dir)
    {
     unsigned long paddr;

     dma_cache_maint_page(page, off, size, dir, dmac_map_area);

     paddr = page_to_phys(page) + off;
     
     if (dir == DMA_FROM_DEVICE) {
      outer_inv_range(paddr, paddr + size); /*保证读数据使得时候,使得(paddr, paddr + size)对应的cache失效*/
     } else {
      outer_clean_range(paddr, paddr + size); 
     }
    }

    /*
    sync_single_for_cpu方法用于数据从设备传到主存的情况:
    为了避免cache的介入导致CPU读到的只是cache中旧的数据,
    驱动程序需要在CPU读取之前调用该函数---->使得cache无效,这样处理器将直接从主存中获得数据。

    sync_single_for_device方法用于数据从主存传到设备,
    在启动DMA操作之前,CPU需要将数据放在位于主存的DMA缓冲区中,
    为了防止write buffer的介入,导致数据只是临时写到write buffer中,
    驱动程序需要在CPU往主存写数据之后启动DMA操作之前调用该函数。
    */


    3. 分散/聚集DMA映射

    分散/聚集DMA映射通过将虚拟地址上分散的DMA缓冲区通过一个struct scatterlist的数组或链表组织起来,
    然后通过一次的DMA传输操作在主存RAM与设备之间传输数据。

    分散/聚集DMA映射本质上是通过一次DMA操作把内存中分散的数据块在主存与设备之间进行传输,对于其中的每个数据块
    内核都会建立对应的一个流式DMA映射。---》需要设备的支持。


    3.4 回弹缓冲区

    如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作,那么需要建立回弹缓冲区,相当于一个 中转站,把数据往设备传输时,
    驱动程序需要把CPU给的数据拷贝到回弹缓冲区,然后再启动DMA操作。


    3.5 DMA池

    由于DMA映射所建立的缓冲区是单个页面的整数倍,
    如果驱动程序需要更小的一致性映射的DMA缓冲区,
    可以使用内核提供的DMA池机制(非常类似于Linux内存管理中的slab机制).

    struct dma_pool就是内核用来完成该任务的数据结构
    struct dma_pool {  /* the pool */
     struct list_head page_list; /*用来将一致性DMA映射建立的页面组织成链表*/
     spinlock_t lock;   /*自旋锁*/
     size_t size;    /*该DMA池用来分配一致性DMA映射的缓冲区的大小,也称为块大小*/
     struct device *dev;   /*进行DMA操作的 设备对象指针*/
     size_t allocation; 
     size_t boundary;
     char name[32];    /*dma池的名称*/
     wait_queue_head_t waitq; /*等待队列*/
     struct list_head pools;  /*用来将当前DMA池对象加入到dev->dma_pools链表中*/
    };

    /*相关操作*/
    1--创建dma_pool,并初始化
     /* dma_pool_create - Creates a pool of consistent memory blocks, for dma.
     * @name: name of pool, for diagnostics
     * @dev: device that will be doing the DMA
     * @size: size of the blocks in this pool.
     * @align: alignment requirement for blocks; must be a power of two
     * @boundary: returned blocks won't cross this power of two boundary
     * Context: !in_interrupt()
     */
    struct dma_pool *dma_pool_create(const char *name, struct device *dev,
         size_t size, size_t align, size_t boundary)

    2--释放DMA池中的DMA缓冲块
    /**
     * dma_pool_free - put block back into dma pool
     * @pool: the dma pool holding the block
     * @vaddr: virtual address of block
     * @dma: dma address of block
    **/
    void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma)

    3--销毁dma_pool
    /* dma_pool_destroy - destroys a pool of dma memory blocks.
     * @pool: dma pool that will be destroyed
     */
    void dma_pool_destroy(struct dma_pool *pool)


    //参考文献:陈雪松-深入Linux设备驱动程序内核机制

    展开全文
  • Linux内核的中断子系统 Linux中断子系统主要包括了三个部分,一部分和体系结构相关,位于最底层,主要负责在中断发生之后保护CPU现场,调用内核统一的中断处理入口函数,负责从中断处理过程恢复到中断之前...
    Linux内核的中断子系统
    

    Linux中断子系统主要包括了三个部分,一部分和体系结构相关,位于最底层,主要负责在中断发生之后保护CPU现场,调用内核统一的中断处理入口函数,负责从中断处理过程恢复到中断之前的流程等比较底层的工作。第二部分是内核的中断系统框架层,这部分为内核中断处理提供了一个统一的框架,对于靠上层的驱动程序,它提供中断程序注册的接口。对于体系结构相关的底层,它提供一个统一的中断处理架构,底层只需要准备好相应的中断号等参数,调用统一的中断处理入口程序即可。第三层是驱动注册的中断处理函数,这些函数由相应驱动提供,完成具体的中断处理。本文主要从这三个方面进行叙述。

    http://alloysystem.blog.chinaunix.net

    Andy.yx.deng#gmail.com(#->@)

    1. Linux中断概述

    之前我已经分析过ARM体系的处理器在发生中断时,CPU状态的变化和linux系统的跳转处理。分析的内容主要还是和ARM体系结构相关的处理过程,实际上当CPU完成这部分工作之后会跳转到和体系结构不相关的linux内核中断子系统中,完成具体的中断事务。我将linux中断处理过程分成三个部分,如下图所示。

    wps_clip_image-7213

    第一个部分就是体系结构相关的代码,这部分代码和CPU对中断的响应密切相关。主要完成工作有:

    1. 保存中断现场(为恢复CPU现场,恢复到被中断的工作做准备)

    2. 切换到中断执行的环境(切换到内核堆栈中)

    3. 获取中断号,并调用中断子系统中的中断例程

    4. 最后,恢复CPU现场,切换执行原来被中断的工作

    这部分内容可以在之前的ARM中断处理分析中有详细的描述,在此不在复述,这里关注的是linux内核相关的中断子系统的分析。

    第二个部分就是linux内核的中断子系统。这个系统设计了一种中断处理的框架,对于下层(体系结构相关的代码)只需要遵照这个架构准备相关的环境并调用内核中断处理过程的入口函数。对于驱动程序,只需要向这个系统中注册中断处理程序。

    第三个部分就是驱动注册的中断处理程序。这个部分是驱动为了完成相关中断事务向内核中断子系统注册的中断例程。也就是我们平时见到比较多的调用request_irq向内核注册一个中断处理函数。

    2. 内核中断子系数据结构

    Linux内核中断子系统的主要数据结构和组织如下图所示。主要涉及的数据结构有三个:

    struct irq_desc

    struct irq_chip

    struct irqaction

    在下面的文章中,主要围绕这三个数据结构分析内核的中断子系统

    wps_clip_image-24437

    首先内核中有一个名为irq_desc数组,数组的名字是“中断描述符”的意思,翻阅其它参考资料,好像中文中没有这样说的。但是,事实上数组中的每一个项就描述了对应中断号的一个硬件中断。以前听供职于百度的一个牛人说过,看技术书籍要直接看英文的,读中文的书籍得到的是第二手的信息,信息的准确性值得怀疑。我很支持这个看法,好比上面irq_desc这样的名字,只要看到这个英文的命名,我们就能联想理解这个数字的意思和作用,但是硬要把这个名称翻译成中文,实在找不到很好的翻译。

    struct irq_desc 

        unsigned int        irq; 
    #ifdef CONFIG_SPARSE_IRQ 
        struct timer_rand_state *timer_rand_state; 
        unsigned int            *kstat_irqs; 
    # ifdef CONFIG_INTR_REMAP 
        struct irq_2_iommu      *irq_2_iommu; 
    # endif 
    #endif  
        irq_flow_handler_t    handle_irq; 
        struct irq_chip        *chip; 
        struct msi_desc        *msi_desc; 
        void            *handler_data; 
        void            *chip_data; 
        struct irqaction    *action;    /* IRQ action list */  
        unsigned int        status;        /* IRQ status */  
      
        unsigned int        depth;        /* nested irq disables */  
        unsigned int        wake_depth;    /* nested wake enables */  
        unsigned int        irq_count;    /* For detecting broken IRQs */  
        unsigned long        last_unhandled;    /* Aging timer for unhandled count */  
        unsigned int        irqs_unhandled; 
        spinlock_t        lock; 
    #ifdef CONFIG_SMP 
        cpumask_t        affinity; 
        unsigned int        cpu; 
    #endif  
    #ifdef CONFIG_GENERIC_PENDING_IRQ 
        cpumask_t        pending_mask; 
    #endif  
    #ifdef CONFIG_PROC_FS 
        struct proc_dir_entry    *dir; 
    #endif  
        const char        *name; 
    } ____cacheline_internodealigned_in_smp; 
     

    Irq_desc中最关键的几个域是handle_irqchipactionHandle_irq是一个函数指针,也就是处理不同类型中断的统一入口函数。Linux系统将各种中断抽象成几种类型,有电平触发的中断、上升/下降沿触发的中断、还有一种简单中断处理模型等等。根据这些中断类型,linux内核抽象出了一套中断处理流程。不同类型的中断,只需要在handle_irq上安装不同的处理函数,而不需要完全重新完成这个过程的编程。

    打一个比方,如果我要完成一个wifi子卡的中断程序,首先我要搞清楚芯片上中断控制器对这个中断号配置了什么样的中断类型。如果是电平触发的中断,我就需要将handle_irq函数指针设置成&handle_level_irqhandle_level_irq函数是linux内核中断子系统中抽象出来的对电平触发的中断的统一处理流程,之后的文章中还会详细分析这个函数。

    上文中我们还看到了struct irq_chip数据结构,这个结构体中大部分是一些函数指针的定义,不同的CPU会在其中安装一些和体系结构相关的函数,完成类似于开启、关闭中断这样的操作。实际上它提供了linux内核中断子系统控制CPU中断硬件的操作接口函数。

    struct irq_chip 

        const char    *name; 
        unsigned int    (*startup)(unsigned int irq); 
        void        (*shutdown)(unsigned int irq); 
        void        (*enable)(unsigned int irq); 
        void        (*disable)(unsigned int irq); 
      
        void        (*ack)(unsigned int irq); 
        void        (*mask)(unsigned int irq); 
        void        (*mask_ack)(unsigned int irq); 
        void        (*unmask)(unsigned int irq); 
        void        (*eoi)(unsigned int irq); 
      
        void        (*end)(unsigned int irq); 
        void        (*set_affinity)(unsigned int irq, 
                                    const struct cpumask *dest); 
        int        (*retrigger)(unsigned int irq); 
        int        (*set_type)(unsigned int irq, unsigned int flow_type); 
        int        (*set_wake)(unsigned int irq, unsigned int on); 
      
        /* Currently used only by UML, might disappear one day.*/  
    #ifdef CONFIG_IRQ_RELEASE_METHOD 
        void        (*release)(unsigned int irq, void *dev_id); 
    #endif  
        /* 
         * For compatibility, ->typename is copied into ->name. 
         * Will disappear. 
         */  
        const char    *typename
    };

    还有一个数据需要介绍一下就是irqaction,我在前面的文章中说过linux中断处理分为三层,最上面的一层就是驱动程序注册的中断处理函数。事实上,中断函数注册中断函数,就是分配一个struct irqaction结构体,将相应的处理函数赋值到handler中,并将这个结构体挂接到对应中断号的irq_desc结构体中。

    3. 中断子系统情景分析

    上面已经将linux内核中断子系统相关的数据结构做了说明。有图有真相!下面会结合一个中断发生后,内核中断子系统的处理过程,详细剖析linux中断处理过程。还是按照上面行文的风格,首先放一张中断处理过程的函数调用关系图,然后对中间涉及到的函数做逐一的分析。从事技术研发的过程中,我发现图例传递的信息量是很大的,特别是在IT开发过程中,一张图有时候比几页的文字传递的信息要多得多。生活中也是这样吗,俗话说“有图有真相!”“No PP, NO way!”,由此可见图片的重要性。

    wps_clip_image-3525

    中断子系统入口点

    从上面的函数调用流程来分析,首先是体系结构相关的代码在获取了中断号,CPU寄存器集合之后,调用asm_do_IRQ函数。这个函数也就是内核的中断子系统提供给体系结构相关代码的一个中断处理统一过程的入口点。

    asm_do_IRQ函数中,通过判断irq是不是大于最大的中断号,如果没有就直接调用generic_handle_irq,一般这个中断号是不会超过最大中断号的,所有我们只分析generic_handle_irq函数。在完成中断过程的处理后,调用irq_exit函数,该函数中调用了invoke_softirq函数触发软件中断的处理。

    struct irqaction 

        irq_handler_t handler; 
        unsigned long flags; 
        cpumask_t mask; 
        const char *name; 
        void *dev_id; 
        struct irqaction *next; 
        int irq; 
        struct proc_dir_entry *dir; 
    };

    generic_handle_irq函数很简单,只是generic_handle_irq函数的一个封装而已,只是通过irq中断号,从irq_desc数组中得到了中断的描述符。

    #define irq_to_desc(irq) (&irq_desc[irq])

    generic_handle_irq_desc也是比较简单,直接调用handle_irq函数指针,这个函数指针根据中断的种类,指向handle_level_irq等函数。

    /* 
    * do_IRQ handles all hardware IRQ's.  Decoded IRQs should not 
    * come via this function.  Instead, they should provide their 
    * own 'handler' 
    */  
    asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

        struct pt_regs *old_regs = set_irq_regs(regs); 
      
        irq_enter(); 
      
        /* 
         * Some hardware gives randomly wrong interrupts.  Rather 
         * than crashing, do something sensible. 
         */  
        if (irq >= NR_IRQS) 
            handle_bad_irq(irq, &bad_irq_desc); 
        else  
            generic_handle_irq(irq); 
      
        /* AT91 specific workaround */  
        irq_finish(irq); 
      
        irq_exit(); 
        set_irq_regs(old_regs); 
    }

    电平触发中断类型

    handle_level_irq函数是对电平触发中断的处理函数。下面对这种中断类型的处理做详细分析。该函数首先屏蔽掉对应中断号的中断,同时清除该中断源的pendding位(ack操作)。然后设置中断线状态为正在处理(IRQ_INPROGRESS)。这个时候,可以调用handle_IRQ_event执行驱动注册的中断处理程序。等处理完成后,清除中断线状态IRQ_INPROGRESS,开启中断线的中断(关掉对该中断线的屏蔽)。

    边缘触发中断处理过程

    Handle_edge_irq函数处理边缘触发中断。在边缘触发中断处理函数中,不屏蔽掉中断,因此同源中断可能会在调用handle_IRQ_event函数处理驱动注册的中断的过程中在其它CPU被触发。在这种情况下,其它CPU上会进入handle_edge_irq函数中,其它CPU判断该中断正在处理过程中就会设置pending标志,并屏蔽掉中断。之后,Handle_edge_irq函数在while循环中,会检查是否设置了pending标志,也就判断是否有同源中断在handle_IRQ_event函数执行期间再次被触发。如果再次被触发,这个时候while循环会继续循环执行驱动注册的中断函数。

    为什么在该中断第二次被触发的时候需要屏蔽掉中断呢?这是因为第二次被触发时,可以通过设置pending位来记录中断发生过,如果这个时候不屏蔽掉中断,有可能第三次同源中断也可能发生,当第三次中断发生时,无法再通过设置pending标志来记录该中断,所以需要屏蔽中断。当while循环为pending位执行过程中(由于设置pending标志再次循环),这时已经确定了上一次并发的中断肯定触发调用handle_IRQ_event后,可以重新开启中断,允许在执行handle_IRQ_event接受同源中断请求。

    static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc) 

    #ifdef CONFIG_GENERIC_HARDIRQS_NO__DO_IRQ 
        desc->handle_irq(irq, desc); 
    #else  
        if (likely(desc->handle_irq)) 
            desc->handle_irq(irq, desc); 
        else  
            __do_IRQ(irq); 
    #endif  
    }

    不管是边缘触发中断类型还是电平触发中断类型,最后都会调用handle_IRQ_event函数来处理驱动注册的中断处理函数。这个函数的逻辑很简单,只需要依次调用action中的函数指针,并依次记录函数调用的结果。

    /** 
    *    handle_level_irq - Level type irq handler 
    *    @irq:    the interrupt number 
    *    @desc:    the interrupt description structure for this irq 

    *    Level type interrupts are active as long as the hardware line has 
    *    the active level. This may require to mask the interrupt and unmask 
    *    it after the associated handler has acknowledged the device, so the 
    *    interrupt line is back to inactive. 
    */  
    void  
    handle_level_irq(unsigned int irq, struct irq_desc *desc) 

        struct irqaction *action; 
        irqreturn_t action_ret; 
      
        spin_lock(&desc->lock); 
        /*屏蔽掉该中断,并清除掉pendding的中断位*/  
        mask_ack_irq(desc, irq); 
        desc = irq_remap_to_desc(irq, desc); 
      
        if (unlikely(desc->status & IRQ_INPROGRESS)) 
            goto out_unlock; 
        desc->status &= ~(IRQ_REPLAY | IRQ_WAITING); 
        kstat_incr_irqs_this_cpu(irq, desc); 
      
        /* 
         * If its disabled or no action available 
         * keep it masked and get out of here 
         */  
        action = desc->action; 
        if (unlikely(!action || (desc->status & IRQ_DISABLED))) 
            goto out_unlock; 
      
        /*设置中断为正在处理状态*/  
        desc->status |= IRQ_INPROGRESS; 
        spin_unlock(&desc->lock); 
      
        /*调用handle_IRQ_event处理驱动注册的中断处理函数*/  
        action_ret = handle_IRQ_event(irq, action); 
        if (!noirqdebug) 
            note_interrupt(irq, desc, action_ret); 
      
        spin_lock(&desc->lock); 
        /*清除掉中断正在处理的标记*/  
        desc->status &= ~IRQ_INPROGRESS; 
        /*开启中断屏蔽,允许中断*/  
        if (!(desc->status & IRQ_DISABLED) && desc->chip->unmask) 
            desc->chip->unmask(irq); 
    out_unlock: 
        spin_unlock(&desc->lock); 

    EXPORT_SYMBOL_GPL(handle_level_irq);

    今天南京的气温出奇的低,坐在家里已经打摆子了。没有空调怎么办?

    展开全文
  • 本文分析基于Linux Kernel 3.2.1原创作品,转载请标明http://blog.csdn.net/yming0221/article/details/7979838更多请查看专栏http://blog.csdn.net/column/details/linux-kernel-net.html作者:闫明Linux内核中协议...
  • 每次Linux内核新版本的发布,都会伴随着一批设备驱动进入内核。在Linux内核里,驱动程序的代码量占有了相当大的比重。下图是我在网络上搜索到的一幅Linux内核代码量的统计图,对应的内核版本是2.6.29。 我们可以很...
  • 一、内核通知链意义 通知链的原型就是一个单向链表,内核提供的通知链机制主要用于不同子系统之间通信,基于事件和优先级。考虑这么一种场景:对于网卡驱动子系统来说,经常会发生的情况就是什么?网卡IP地址有变化...
  • 在“Linux内核的整体架构”中,蜗蜗有提到,由于Linux支持世界上几乎所有的、不同功能的硬件设备(这是Linux的优点),导致Linux内核中有一半的代码是设备驱动,而且随着硬件的快速升级换代,设备驱动的代码量也在...
  • 来源 | 异步 | 文末赠书2019 年微软 Build 开发者大会在雷德蒙德召开。继将 Bash shell、原生 OpenSSH、WSL 引入 Windows,以及在...
  • 本文分析基于Linux Kernel 3.2.1原创作品,转载请标明http://blog.csdn.net/yming0221/article/details/7984238更多请查看专栏http://blog.csdn.net/column/details/linux-kernel-net.html作者:闫明1、系统初始化...
  • 2019 年微软 Build 开发者大会在雷德蒙德召开。继将 Bash shell、原生 OpenSSH、WSL 引入 Windows,以及在微软商店提供 Ubuntu、...
  • 在上一节中,蜗蜗大神有提到,由于Linux支持世界上几乎所有的、不同功能的硬件设备(这是Linux的优点),导致Linux内核中有一半的代码是设备驱动,而且随着硬件的快速升级换代,设备驱动的代码量也在快速增长。...
  • 一、文件系统的基本组成和文件系统结构 1.Linux 系统的基本组成图例: Linux系统由Linux内核和一系列GNU库及程序组成。根据其所处的位置,从内向外依次分为:内核、运行期库和系统程序、shell、实用工具程序四个层次...
  • Android框架: IOS框架(简): web_os框架: ...linux框架(简): ...linux框架(完整): ...不管阅读哪一个系统的源码,最为关键的便是要对其大体的框架形成一...下面为从百度百科摘录的关于单内核与微内核的简介: ...
  • 章强 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
  • 最常见,最频繁使用的基础命令如下: 系统经典语录: 1、命令操作完没有任何消息信息, 就是最好的消息 2、系统一切从根开始 3、系统中...
  • Linux-0.11 内核定时器

    2014-12-11 16:18:09
    Linux-0.11中的内核定时器它是一个软定时器,还是由jiffies来实现的,最多同时可支持64个内核定时器,内核定时器数据结构定义如下:#define TIME_REQUESTS 64 static struct timer_list { long jiffies; void (*...
1 2 3 4 5 ... 20
收藏数 583
精华内容 233