Linux设备驱动核心理论(三)

浏览: 189 发布日期: 2016-11-27 分类: linux

10.中断与时钟

        10.1 中断与定时器

                所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停执行当前程序,转去处理突发事件,处理完毕后CPU又返回原程序被中断的位置并继续执行。

                根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断来源来自CPU内部(软件中断、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断的中断来源来自CPU外部,由外设提出请求。

                根据中断是否可以屏蔽分为可屏蔽中断与不屏蔽中断(NMI),可屏蔽中断可以通过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应,而不屏蔽中断不能被屏蔽。

                根据中断入口跳转方法的不同,分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行,不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供提供中断服务程序入口地址。

                嵌入式系统以及X86' PC中大多数包含可编程中断控制器(PIC)、许多MCU内部就集成了PIC。如在80386中PIC是两片i8259Ax芯片的级联。通过读写PIC的寄存器,程序员可以屏蔽/使能某中断及获得中断状态,前者一般通过中断MASK寄存器完成,后者一般通过中断PEND寄存器完成。

                定时器在硬件上也依赖中断来实现,它接受一个时钟输入,当时钟脉冲到来时,将目前计数值增1并与预先设置的计数值(计数目标)比较,若相等,证明计数周期满,产生定时器中断并复位目前计数值。下图为典型的嵌入式处理内可编程间隔定时器(PIT)的工作原理。

               

        10.2 Linux中断处理程序架构

                设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能的短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。下图描述了Linux内核的中断处理机制。

                       

                为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。

                顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“等级中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去,这样,顶半部执行的速度就会很快,可以服务更多的中断请求。

                现在,中断处理工作的重心就落在了底半部的头上,它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。

                尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分为两个半部则是不对的,如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。

                其他操作系统中对中断的处理也采用了类似于Linux的方法,真正的硬件中断服务程序都应该尽可能短。因此,许多操作系统就提供了中断上下文和非中断上下文相结合的机制,将中断的耗时工作保留到非中断上下文去执行。例如,在VxWorks中,网络设备包含接收终端到来后,中断服务程序会通过netJobAdd()函数将耗时的包接收和上传工作交给tNetTask任务去执行。

                在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,在单处理器的系统中,第1列是中断号,第2列是向CPU0产生该中断的次数,之后的是对于中断的描述。

        10.3 Linux中断编程

                10.3.1 申请和释放中断

                         在Linux设备驱动中,使用中断的设备需要申请和释放对应的中断,分别使用内核提供的request_irq()和free_irq()函数。

                         1.申请IRQ

                                  int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)

                                  irq是要申请的硬件中断号。

                                  handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev_id参数将被传递给它。

                                  irqflags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等,在处理方式方面,若设置了IRQF_DIABLED,表明中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序则不会屏蔽其他设备的驱动;若设置了IRQF_SHARED,则表示多个设备共享中断,dev_id在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。

                                  request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。

                                  顶半部handler的类型irq_handler_t定义为:

                                          typedef irqreturn_t (*irq_handler_t)(int, void*)

                                          typedef int irqreturn_t;

                        2.释放IRQ

                                与request_irq()对应的函数为free_irq(),free_irq()的原型为:void free_irq(unsigned int irq, void *dev_id);

                                free_irq()中参数的定义与request_irq()相同。

                10.3.2 使能和屏蔽中断

                        下列3个函数用于屏蔽一个中断源

                                void disable_irq(int irq);

                                void disable_irq_nosync(int irq);

                                void enable_irq(int irq);

                         disable_irq_nosync与disable_irq的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq会等待指定的中断被处理完,因此如果n号中断的顶半部用disable_irq(n),会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。

                         下列两个函数(或宏,具体实现依赖于CPU体系结构)将屏蔽本CPU内的所有中断:

                                 #define local_irq_save(flags)

                                 void local_irq_disable(void);

                        前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针),后者直接禁止中断而不保存状态。

                        与上述两个禁止中断对应的恢复中断的函数(或宏)是:

                                #define local_irq_restore(flags)

                                void local_irq_enable(void);

                        以上各local_开头的方法的作用范围是本CPU内。

                10.3.3 底半部机制

                        Linux实现底半部的机制主要有tasklet、工作队列和软中断。

                        1.tasklet

                                 tasklet的使用较简单,我们只需要定义tasklet及其处理函数并将两者关联,例如:

                                         void my_tasklet_func(unsigned long);        /*定义一个处理函数*/

                                         DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);        /*定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联*/

                                 代码DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了定义名称为my_tasklet的tasklet并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。

                                 在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:tasklet_schedule(&my_tasklet);

                        2.工作队列

                                工作队列的使用方法和tasklet非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数:

                                        struct work_struct my_wq;         /*定义一个工作队列*/

                                        void my_wq_func(unsigned long);        /*定义一个处理函数*/

                                        INIT_WORK(&my_wq, (void(*)(void*)) my_wq_func, NULL);        /*初始化工作队列并将其与处理函数绑定*/

                                        schedule_work(&my_wq);        /*调度工作队列执行*/

                        3.软中断

                                软中断(softirq)也是传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。

                                在Linux内核中,用sofrirq_action结构体表征一个软中断,这个结构体中包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。

                                软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队则运行于进程上下文。因此,软中断和tasklet处理函数不能睡眠,而工作队列处理函数中允许睡眠。

                                local_bh_diable()和local_bh_enable()是内核中用于禁止和使能软中断和tasklet底半部机制的函数。

                                内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般来说,驱动的编写者不会也不宜直接使用softirq。

                                硬中断。、软中断和信号的区别:硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某个进程的中断。在论及系统调用的场合,人们也常说通过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这个地方说的softirq是两个完全不同的概念。

                10.3.4 实例:S3C6410实时钟中断

        10.4 中断共享

                多个设备共享一根硬件中断线的情况咋实际的硬件系统中广泛存在,Linux 2.6支持这种中断共享。下面是中断共享的使用方法。

                        (1)共享中断的多个设备在申请中断时,都应该使用TRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都是以IRQF_SHARED标志申请该中断。

                        (2)尽管内核模块可访问的全局地址都可以作为request_irq(..., void *dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。

                        (3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应迅速地根据硬件寄存器中的信息比照传入的dev_id参数判断是否是设备的中断,若不是,应迅速返回IRQ_NONE。

                               

        10.5 内核定时器

                10.5.1 内核定时器编程

                        软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后执行检测个定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行,实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。

                        在Linux设备驱动编程中,可以利用Linux内核中提供的一组函数和数据结构来完成定时出发工作或者完成某周期性的事务。这组函数和数据结构使得驱动工程师多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。

                        Linux内核所提供的用于操作定时器的数据结构和函数如下:

                                1.timer_list

                                         struct timer_list{

                                                 struct list_head entry;        /*定时器列表*/

                                                 unsigned long expires;        /*定时器到期时间*/

                                                 void (*function)(unsigned long);    /*定时器处理函数*/

                                                 unsigned long data;        /*作为参数被传入定时器处理函数*/

                                                 struct timer_base_s ×base;

                                                 ...

                                         };

                                         在Linux内核中,timer_list结构体的一个实例对应一个定时器。定义方式:struct timer_list my_timer;

                                2.初始化定时器

                                        void init_timer(struct timer_list * timer);

                                        上述init_timer()函数初始化timer_list的entry的next为NULL,并给base指针赋值。

                                        TIMER_INITIALIZER(_function, _expires, _data)宏用于赋值定时器结构体的function、expires、data和base成员,这个宏的定义为:

                                                #define TIMER_INITALIZER(_function, _expires, _data){    \

                                                                .entry={.prev=TIMER_ENTRY_STATIC},    \

                                                                 .function=(_function),    \

                                                                 .expires=(_expires),    \

                                                                 .data=(_data),    \

                                                                 .base=&boot_tvec_bases,    \
                                                          }

                                        DEFINE_TIMER(name, _function, _expires, _data)宏是定义并初始化定时器成员的“快捷方式”,这个宏定义为:

                                                #define DEFINE_TIMER(_name, _function, _expires, _data)    \

                                                                 struct timer_list_name=TIMER_INITIALIZER(_function, _expires, _data)

                                        此外,setup_timer()也可用于初始化定时器并复制其成员,其源代码为:

                                                static inline void setup_timer(struct timer_list *timer, void (*function)(unsigned long), unsigned long data)

                                                {

                                                        timer->function=function;

                                                        timer->data=data;

                                                        init_timer(timer);

                                                }

                                3.增加定时器

                                        void add_timer(struct timer_list *timer);

                                        上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。

                                4.删除定时器

                                        int del_timer(struct timer_list *timer);

                                        del_timere_sync()是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的调用不能发生在中断上下文。

                                5.修改定时器的expire

                                        int mod_timer(struct timer_list *timer, unsigned long expires);

                                        上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。

                                在定时器处理函数中,在做完相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链表,以便定时器能再次被触发。

                10.5.2 内核中延迟的工作delayed_work

                        注意,对于这种周期性的任务,Linux内核还提供了一套封装好的快捷机制,其本质利用工作队列和定时器实现,这套快捷机制就就是delayed_work。delayed_work结构体的定义如下:

                                struct delayed_work{

                                        struct work_struct work;

                                        struct timer_list timer;

                                };

                                struct work_struct{

                                        atomic_long_t data;

                                        #define WORK_STRUCT_PENDING 0

                                        #define WORK_STRUCT_FLAG_MASK(3UL)

                                        #define WORK_STRUCT_WQ_DATA_MASK(~WORK_STRUCT_FLAG_MASK)

                                        struct list_head entry;

                                        work_func_t func;

                                        #ifdef CONFLG_LOCKDEP

                                        struct lockdep_map lockdep_map;

                                        #endif

                                };

                        我们通过如下调度一个delayed_work在指定的延时后执行:int schedule_delayed_work(struct delayed_work *work, unsigned long delay);

                        当指定的delay到来时delayed_work结构体中work成员的work_func_t类型成员func()会被执行。work_func_t类型定义为:

                                typedef void (*work_func_t)(struct work_struct *work);

                         其中delay参数的单位是jiffies,因此一种常见的方法如下:

                                 schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));

                         其中的msecs_to_jiffies()用于将毫秒转化为jiffies。

                         如果要周期性的执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。

                         如下函数用来取消delayed_work:

                                 int cancel_delayed_work(struct delayed_work *work);

                                 int cancel_delayed_work_sync(struct delayed_work *work);

                10.5.3实例:秒字符设备

        10.6 内核延时

                10.6.1 短延迟

                        Linux内核中提供了如下3个函数分别进行纳秒、微妙和毫秒延迟:

                                void ndelay(unsigned long nsecs);

                                void udelay(unsigned long usecs);

                                void mdelay(unsigned long msecs);

                        上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。有时候,人们在软件中进行这样的延迟:

                                void delay(unsigned int time)

                                {

                                        while(time--);

                                }

                        ndelay()、udelay()和mdelay()函数的实现方式机理与此类似。内核在启动时,会运行一个延迟测试程序(delay loop calibration),计算lpj(loops per jiffy)。

                        毫秒时延(以及更大的秒时延)已经比较大,在内核中,最好不要直接使用mdelay()函数,这将无谓地耗费CPU资源,对于毫秒级以上时延,内核提供了下述函数:

                                void msleep(unsigned int milisecs);

                                unsigned long msleep_interruptible(unsigned int millisecs);

                                void ssleep(unsigned int seconds);

                        上述函数将使得调用它的进程睡眠参数指定的时间,msleep()、ssleep()不能被打断,而msleeo_interruptible()则可以被打断。

                        受系统Hz以及进程调度的影响,msleep()类似函数的精度是有限的。

                10.6.2 长延迟

                        内核中进行延迟的一个很直观的方法是比较当前的jiffies和目标jiffies(设置为当前jiffies加上时间间隔的jiffies),直到未来的jiffies达到目标jiffies。

                        与time_befor()对应的还有一个timer_after(),他们在内核中定义为(实际上只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):

                                #define time_after(a, b) (typecheck(unsigned long, a) && typecheck(unsigned long, b) && ((long)(b) - (long)(a) < 0))

                                #define time_befor(a,b) time_after(b.a)

                        为了防止time_before()和time_after()的比较过程中编译器对jiffies的优化,内核将其定义为volatile变量,这将保证它每次都被重新读取。

                10.6.3 睡着延迟

                        睡着延迟无疑是比较忙等待更好的方式,睡着延迟在等待的时间到来之间进程处于睡眠状态,CPU资源被其他进程使用。schedule_timeout()可以使当前任务睡眠指定的jiffies之后重新被调度执行,msleep()和msleep_interruptible()本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()实现的。

                        实际上,schedule_timeout()的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒参数对应的进程。

                        下面两个函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程将被唤醒(后者可以在超时前被打断):

                                sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);

                                interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);

11. 内存与I/O访问

        11.1 CPU与内存和I/O

                11.1.1内存空间与I/O空间

                        在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。端口号标识了外设的寄存器地址。Intel语法的in、out指令格式如下:

                                IN 累加器, {端口号|DX]

                                OUT {端口号|DX},累加器

                        目前,大多数嵌入式微控制器如ARM、PowerPC等中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。

                        内存地址可以直接由C语言操作,例如在I86处理器中执行如下代码:

                                unsigned char *p=(unsigned char *)0xF000FF00;

                                *p=11;

                        以上程序的意义为在绝对地址0xF0000+0xFF00(186处理器使用16位段地址和16位偏移地址)写入11。

                        而在ARM、PowerPC等未采用段地址的处理器中,p指向的内存空间就是0xF0000FFF0,而*p=11就是在该地址写入11。

                        再如,186处理器启动后会在绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:

                                typedef void (*lpFunction)();    /*定义一个无参数,无返回值类型的函数指针类型*/

                                lpFunction lpReset=(lpFunction)0xF000FFF0;    /*定义一个函数指针,指向CPU启动后所执行第一条指令的位置*/

                                lpRest();    /*调用函数*/

                        在以上程序中,没有定义任何一个函数实体,但是程序中却执行了这样的函数调用:lpReset(),它实际上起到了“软重启”的作用,跳转到CPU启动后第一条要执行的指令的位置。因此,可以通过函数指针调用一个没有函数体的“函数”,本质上只是换一个地址开始执行。

                        即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。因此,内存空间是必须的,而I/O空间是可选的。下面给出了内存空间和I/O空间的对比:

                               

                11.1.2内存管理单元MMU

                        高性能处理器一般会提供一个内存管理单元(MMU),该单元辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。操作系统内核借助MMU,可以让用户感觉到好像程序可以使用非常强大的内存空间,从而使得编程人员在写程序时不用考虑计算机中的物理内存的实际容量。

                        为了理解基本的MMU操作原理,需先明晰几个概念。

                                (1)TLB:Translation Lookaside Buffer,即转换旁路缓存,TLB是MMU的核型部件,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache,因此也经常被称为“快表”。

                                 (2)TTW:Translation Table walk,即转换表漫游,当TLB中没有缓冲对应的地址转换关系时,需要通过对内存中转换表(大多数处理器的转换表为多级页表)的访问来获得虚拟地址和物理地址的对应关系。TTW成功后。结果应写入TLB。

                                         

                        下面是一个典型的ARM处理器访问内存的过程,其他处理器也执行类似过程。当ARM要访问存储器时,MMU先查找TLB中的虚拟地址表。如果ARM的结构支持分开的数据TLB(DTLB)和指令TLB(ITLB),则除取指令使用ITLB外,其他的都是用DTLB。ARM处理器的MMU如下图所示:

                              

                        若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器中的转换表中获取地址转换信息和访问权限(即执行TTW),同时将这些信息放入TLB。他或者被放在一个没有使用的入口或者替换一个已经存在的入口。之后,在TLB条目中控制信息的控制下,当访问权限允许时,对真实物理地址的访问将在Cache或者在内存中发生。如下图所示:

                               

                        ARM中的TLB条目中的控制信息用于控制对对应地址的访问权限以及Cache的操作。

                                C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否高速缓存。

                                访问权限和域位用来控制读写访问是否被允许。如果不允许,则MMU将向ARM处理器发送一个存储器异常,否则访问将被允许进行。

                        上述描述的MMU机制针对的虽然是ARM处理器,但PowerPC、MIPS等其他处理器也均有类似的操作。

                        MMU具有虚拟地址和物理地址转换、内存访问权限保护等功能,这将使得Linux操作系统能单独为系统的每个用户进程分配独立的内存空间并保证用户空间不能访问内核空间的地址,为操作系统的虚拟内存管理模块提供硬件基础。

                        Linux内核使用了三级页表PGD、PMD和PTE,对于许多体系结构而言,PMD这一级实际上只有一个入口。

                        类型为struct mm_struct的参数mm用于描述Linux进程所占有的内存资源。pgd_offset、pmd_offset分别用于得到一级页表和二级页表的入口,最后通过pte_offset_map得到目标页表项。但是MMU并非所所有处理器都是必须的。

        11.2 Linux内存管理

                对于包含MMU的处理器而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。

                在Linux系统中,进程的4GB内存空间被分为两个部分--用户空间与内核空间。用户空间地址一般分布为0~3GB(即PAGE OFFSET,在0x86中他等于0xC0000000),这样,剩下的3~4GB为内核空间,如下图所示。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只有通过系统调用(代表用户进程在内核态执行)等方式才可以访问到内核空间。

                       

                每个进程的用户空间都是完全独立的、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程该表,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。

                Linux中1GB的内核空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域,如下图所示:

                       

                一般情况下,物理内存映射区最大长度为896MB,系统的物理内存被顺序映射在内核空间的这个区域中,当系统物理内存大于896MB时,超过物理内存映射区的那部分成为高端内存(而未超过物理内存映射区的内存通常被称为常规内存),内核在存取高端内存时必须将它们映射到高端页面映射区。

                Linux保留内核空间最顶部FIXADDR TOP~4GB的区域作为保留区。

                紧接着最顶端的保留区以下的一段区域为专用页面映射区(FIXADDR_START~FIXADDR TOP),它的尺寸和每一页的用途由fixed_address枚举结构在编译时预定义,用__fix_to_virt(index)可获取专用区内预定义页面的逻辑地址。其开始地址和结束地址宏定义如下:

                        #define FIXADDR_START        (FIXADDR_TOP - __FIXADDR_SIZE)

                        #define FIXADDR_TOP            ((unsigned long)__FIXADDR_TOP)

                        #define __FIXADDR_TOP        0xfffff000

                接下来,如果系统配置了高端内存,则位于专用页面映射区之下的就是一段高端内存映射区,其起始地址为PKMAP_BASE,定义如下:

                        #define PKMAP_BASE((FIXADDR_BOOT_START-PAGE_SIZE*(LAST_PKMAP+1)&PDM_MASK)

                其中所涉及的宏定义如下:

                        #define FIXADDR_BOOT_START(FIXADDR_TOP-__FIXADDR_BOOT_SIZE)

                        #define LAST_PKMAP        PTRS_PER_PTE

                        #define PTRS_PER_PTE        512

                        #define PMD_MASK        (~(PMD_SIZE-1))

                        #define PMD_SIZE        (1UL << PMD_SHIFT)

                        #define PMD_SHIFT        21

                在物理区和高端映射区之间为虚拟内存分配区(VMALLOC_START~VMALLOC_END),用于vmalloc()函数,它的前部与物理内存映射区有一个隔离带,后部与高端映射区也有一个隔离带,vmalloc区域定义如下:

                        #define VMALLOC_OFFSET(8*1024*1024)

                        #define VMALLOC_START(((unsigned long) high_memory+vmalloc_earlyreserve + 2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET -1))

                        #ifdef CONFIG_HIGHMEM        /*支持高端内存*/

                        #define VMALLOC_END(PKMAP_BASE-2*PAGE_SIZE)

                        #else        /*不支持高端内存*/

                        #define VMALLOC_END        (FIXADDR_START-2*PAGE_SIZE)

                        #endif

                当系统物理内存超过4GB时,必须使用CPU的扩展分页(PAE)模式所提供的64位页目录项才能存取到4GB以上的物理内存,这需要CPU的支持。加入了PAE功能的Intel Pentium Pro及其后的CPU允许内存最大可配置到64GB,具备36位物理地址空间寻址能力。

                由此可见,在3~4GB之间的内核空间中,从低地址到高地址依次为:物理内存映射区--隔离带--vmalloc虚拟内存分配器--隔离带--高端内存映射区--专用页面映射区--保留区。

        11.3内存存取

                11.3.1 用户空间内存动态申请

                        在用户空间动态申请内存的函数为malloc(),这个函数在各种操作系统上的使用时一致的,malloc()申请的内存的释放函数为free()。

                        malloc()的内存一定要被free(),否则会造成内存泄漏。理想情况下,malloc()和free()应成对出现,即谁申请,就由谁释放。

                        完全让malloc()和free()成对出现有时候很难做到,即便如此,也应尽力将malloc()申请内存的释放限制在本模块范围之内。

                        对于Linux内核而言,C库的malloc()函数通常通过brk()和mmap()两个系统调用来实现。

                11.3.2 内核空间内存动态申请

                        在Linux内核空间申请内存设计的函数主要包括kmalloc()、__get_free_pages()和vmalloc等。kmalloc()和__get_free_pages()(及其类似函数)申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系。而vmalloc()在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc()申请的虚拟内存和物理内存之间也没有简单的换算关系。

                                1.kmalloc()

                                         void *kamlloc(size_t size, int flags);

                                         给kmalloc()的第一个参数是要分配的块的大小,第二个参数为分配标志,用于控制kmallockmalloc()的行为。

                                         最常用的分配标志是GFP_KERNEL,其含义是在内核空间的进程中申请内存。kmalloc()的底层依赖__get_free_pages()实现,分配标志的前缀GFP正好是这个底层函数的缩写。使用GFP_KERNEL标志申请内存时,若暂时不能满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用GPF_KENNEL申请内存。

                                         在中断处理函数、tasklet和内核定时器等非进程上下文中不能阻塞,此时驱动应当使用GFP_ATOMIC标志来申请内存。当使用GFP_ATOMIC标志申请内存时,若不存在空闲页,则不等待,直接返回。

                                         其他的相对不常用的申请标志还包括GFP_USER(用来为用户空间页分配内存,可能阻塞)、GFP_HIGHUSER(类似GFP_USER,但是从高端内存分配)、GFP_MOIO(不允许任何I/O初始化)、GFP_NOFS(不允许进行任何文件系统调用)、__GFP_DMA(要求分配在能够DMA的内存区)、__GFP_HIGHMEM(指示分配的内存可以位于高端内存)__GFP_COLD(请求一个较长时间不访问的页)、__GFP_NOWWRN(当一个分配无法满足时,阻止内核发出警告)、__GFP_HIGH(高优先级请求,允许获得被内核保留给紧急状况使用的最后的内存页)、__GFP_REPEAT(分配失败则尽力重复尝试)、__GFP_NOFAIL(标志只须申请成功,不推荐)和__GFP_NORETRY(若申请不到,则立即放弃)。

                                       使用kmalloc()申请的内存应使用kfree()释放,这个函数的用法和用户空间的free()类似。

                                2.__get_free_pages()

                                        __get_free_pages()系列函数/宏是Linux内核本质上最底层的用于获取空闲内存的方法,因为底层的伙伴算法以page的2的n次幂为单位管理空闲内存,所以最底层的内存申请总是以页为单位的。
                                        __get_free_pages()系列函数/宏包括get_zeroed_page()、__get_free_page()和__get_free_pages()。

                                        get_zeroed_page(unsigned int flags);        该函数返回一个指向新页的指针并且将该页清零。

                                        __get_free_page(unsigned int falgs);        该宏返回一个指向新页的指针但是该页不清零,它实际上为:#define __get_free_page(gfp_mask)  __get_free_pages((gfp_mask), 0)就是调用下面的__get_free_pages()申请1页。

                                        __get_free_pages(unsigned int flags, unsigned int order); 该函数可分配多个也并返回内存的首地址,分配的页数为2的order幂次,分配的页也不清零。order允许的最大值是10(即1024页)或者11(即2048页),依赖于具体的硬件平台。

                                        __get_free_pages()和get_zeroed_page()的实现中调用了alloc_pages()函数,alloc_pages()既可以在内核空间分配,也可以在用户空间分配,其原型为:struct page * alloc_pages(int gfp_mask, unsigned long order);参数含义与__get_free_pages()类似,但它返回分配的第一页的描述符而非首地址。

                                       使用__get_free_pages()系列函数/宏申请的内存应使用下列函数释放:

                                               void free_page(unsigned long addr);

                                               void free_page(unsigned long addr, unsigned long order);

                                       如果申请和释放的order不一样,则会引起内存的混乱。

                                       __get_free_pages等函数在使用时,其申请标志的值为kmalloc()完全一样,各标志的含义也与kmalloc()完全一致,最常见的是GFP_KERNEL和GFP_ATOMIC。

                                3.vmalloc()

                                        vmalloc()一般用于在为只存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存,vamlloc()远大于__get_free_pages()的开销,为了完成vmalloc()。因此,只是调用vmalloc()来分配少量的内存(如1页)是不妥的。

                                        vmalloc()申请的内存应使用vfree()释放,vmalloc()和vfree()的函数原型如下:

                                                void *vmalloc(unsigned long size);

                                                void *vfree(void *addr);

                                        vmalloc()不能用在原子上下文中,因为它的内部实现使用了标志为GFP_KERNEL的kmalloc()。

                                        使用vmalloc()不能用在原子上下文中,因为它的内部实现使用了标志为GFP_KERNEL的kmalloc()。

                                        使用vmalloc函数的一个例子函数是create_module()系统调用,它利用vmalloc()函数来获取被创建模块需要的内存空间。

                                 4.slab与内存池

                                         一方面,完全使用页为单元申请和释放内存容易导致浪费(如果要申请少量字节也需要1页);另一方面,在操作系统的运作过程中,经常会涉及大量对象的重复生成、使用和释放内存问题。在Linux系统中所用到的对象,比较典型的例子是inode、task_struct等。如果我们能够用合适的方法使得在对象前后两次被使用时分配在一块内存或同一类内存空间且保留了基本的数据结构,就可以大大提高效率。slab算法就是针对上述特点设计的。实际上kmalloc()即是使用slab机制实现的。

                                         (1)创建slab缓存

                                                struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*, struct kmem_cache*, unsigned long), void(*dtor)(void*, struct kmem_cache*, unsigned long));

                                                kmem_cache_create()用于创建一个slab缓存,它是一个可以驻留任意数目全部同样大小的后备缓存。参数size是要分配的每个数据结构的大小,参数flags是控制如何进行分配的位掩码,包括SLAB_NO_REAP(即使内存紧缺也不自动收缩这块缓存)、SLAB_HWCACHE_ALIGN(每个数据对对象被对齐到一个缓存行)、SLAB_CACHE_DMA(要求数据对象在DMA内存区分配)等。

                                        (2)分配slab缓存

                                                void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

                                                上述函数在kmem_cache_create()创建的slab后备缓存中分配一块并返回首地址指针。

                                        (3)释放slab缓存

                                                void kmem_cache_free(struct kmem_cache *cachep, void *objp);

                                                上述函数释放由kmem_cache_alloc()分配的缓存。

                                        (4)收回slab缓存

                                                int kmem_cache_destory(struct kmem_cache *cachep);

                                         在系统中通过/proc/slabinfo节点可以获得当前slab的分配和使用情况。

                                 注意,slab不是要替代__get_free_pages(),其在最底层仍然依赖于__get_free_pages(),slab在底层每次申请1页或多页,之后再分隔这些页为更小的单元进行管理,从而节省了内存,也提高了slab缓冲对象的访问效率。

                                 除了slab以外,在Linux内核中还包含对内存池的支持,内存池技术也是一种非常经典的用于分配大量小对象的后备缓存技术。

                                 Linux内核中,与内存池相关的操作包括如下几种。

                                         (1)创建内存池

                                                 mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data);

                                                 mempool_create()函数用于创建一个内存池,min_nr参数是需要预分配对象的数目,alloc_fn和free_fn是指向内存池机制提供的标准对象分配和回收函数的指针,其原型分别为:typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);和typedef void (mempool_free_t)(void *element, void *pool_data);

                                                 pool_data是分配和回收函数用到的指针,gfp_mask是分配标识。只有当__GFP_WAIT标记被指定时,分配函数才会休眠。

                                         (2)分配和回收对象

                                                 在内存池中分配和回收对象需由以下函数来完成:

                                                         void *mempool_alloc(mempool_t *pool, int gfp_mask);

                                                         void mempool_free(void *element, mempool_t *pool);

                                                 mempool_alloc()用来分配对象,如果内存池分配器无法提供内存,那么就可以用预分配的池。

                                         (3)回收内存池

                                                 void mempool_destroy(mempool_t *pool);

                                                 mempool_create()函数创建的内存池需由mempool_destroy()来回收。

                11.3.3虚拟地址与物理地址关系

                         对于内核物理内存映射区的虚拟内存,使用virt_to_phys()可以实现内核虚拟地址转化为物理地址,virt_to_phys()的实现是体系结构相关的,对于ARM而言,virt_to_phys()的定义如下代码所示:

                                 static inline unsigned long virt_to_phys(void *x)

                                 {

                                         return __virt_to_phys((unsigned long)(x));

                                 }

                                 #define __virt_to_phys(x)        ((x)-PAGE_OFFSET+PHYS_OFFSET)

                         上面转换过程的PAGE_OFFSET通常为3GB,而PHYS_OFFSET则定义为系统DRAM内存的基地址。

                         与之对应的函数为phys_to_virt(),它将物理地址转化为内核虚拟地址,phys_to_virt()的定义如下代码所示:

                                 static inline void *phys_to_virt(unsigned long x)

                                 {

                                         return (void*)(__phys_to_virt((unsigned long)(x)));

                                 }

                                 #define __phys_to_virt(x)        ((x)-PHYS_OFFSET+PAGE_OFFSET)

                         注意,上述virt_to_phys()和phys_to_virt()方法仅适用于896MB以下的低端内存,高端内存的虚拟地址与物理内存之间不存在如此简单的换算关系。

        11.4 设备I/O端口和I/O内存的访问

                设备通常会提供一组寄存器来用于控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O空间,也可能位于内存空间。当位于I/O空间时,通常被称为I/O端口,位于内存空间时,对应的内存空间被称为I/O内存。

     

返回顶部