Chinaunix首页 | 论坛 | 博客
  • 博客访问: 255354
  • 博文数量: 52
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1538
  • 用 户 组: 普通用户
  • 注册时间: 2013-04-24 07:45
个人简介

生活就像海洋,只有意志坚强的人,才能到达彼岸。

文章存档

2013年(52)

分类: LINUX

2013-08-17 15:32:47

一、Linux内存管理

1>内存管理子系统

    内存是Linux内核所管理的最重要的资源之一,内存管理子系统是操作系统中最重要的部分之一。对于立志从事内核开发的工程师来说,熟悉Linux的内存管理系统非常重要。

2>地址类型

 a、物理地址

    物理地址是指出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。

 b、线性地址(虚拟地址)

    线性地址又名虚拟地址,在32位CPU架构下,可以表示4G的地址空间,用16进制表示就是0x00000000到0xffffffff。

 c、逻辑地址

    程序代码经过编译后,出现在汇编程序中的地址。

 d、地址转换

1>段式管理
    CPU要将一个逻辑地址转换为物理地址,需要两步:首先CPU利用段式内存管理单元(Semented unit),将逻辑地址转换成线性地址,再利用页式内存管理单元(Paging unit),把线性地址最终转换为物理地址

 e、什么是段式管理、页式管理?

    16位CPU内部拥有20位的地址线,它的寻址范围就是2的20次,也就是1M的内存空间。但是16位的CPU用于存放地址的寄存器(IP、SP.....)只有16位,因此只能访问65536个存储单元,64K。为了能够访问1M的内存空间,CPU就采用内存分段的管理模式,并在CPU内部加入了段寄存器。16位CPU把1M内存空间分为若干个逻辑段,每个逻辑段的要求如下:

1、逻辑段的起始地址(段地址)必须是16的倍数,即最后4个二进制位必须全为0.
2、逻辑段的最大容量为64K

 f、物理地址的形成方式:

    由于段地址必须是16的倍数,所以值得一般形式为XXXX0H,即前16位二进制是变化的,可以只保存前16位二进制位来保存整个段基地址,所以每次使用时要用段寄存器左移补4个0(乘以16)来得到实际的段地址。在确定某个存储单元所属的段后,只是知道了存储单元所属的范围(段弟子->段地址+65536),如果想确定该内存单元的具体位置,还必须知道该单元在段内的偏移。有了段地址偏移量,就可以唯一的确定内存单元在存储器中的具体位置。

    逻辑地址=段基地址+段内偏移量
    PA=段寄存器的值*16+逻辑地址

段寄存器时为了对内存进行分段管理而增加的,16位CPU有四个段寄存器,程序可同时访问四个不同的含义的段。

1)CS + IP:用于代码段的访问,CS指向存放程序的段基址,IP指向下条要执行的指令在CS段的偏移量,用这两个寄存器就可以得到一个内存物理地址,该地址存放着一条要执行的指令。

2)SS + SP:用于堆栈段的访问,SS指向堆栈的基地址,SP指向栈顶,可以通过SS和SP两个寄存器直接访问栈顶单元的内存物理位置。

3)DS + BX:用于数据段的访问。DS中的值左移四位得到数据段的起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。

4)ES + BX:用于附加段的访问。ES中的值左移四位得到附加段的起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。

32位的pc的内存管理任然采用“分段”的管理模式,逻辑地址同样由段地址和偏移量量部分组成,32位pc的内存管理和16位的pc内存管理有相同之处也有不同之处,因为32位的pc采用过了两种不同的工作方式:实模式保护模式

1)实模式
    在实模式下,32位CPU的内存管理与16位的CPU是一致的。

2)保护模式
    段基地址长达32位,每个段的最大容量可达4G,段寄存器的值时段地址的“选择器"(selector)用该"选择器"从内存中得到一个32位的段地址,存储单元的物理地址就是该段地址加上段内偏移量。

2>页式管理

    从管理和效率的角度出发,线性地址被分为固定长度的组,称为页(page),例如32位的机器,线性地址最大可为4G,如果用4KB为一个页来划分,这样整个线性地址就被划分为2的20次方个页。

    另一类“页”,称之为物理页,或者是页框‘页帧。分页单元把所有的物理内存划分为固定长度的管理单位,它的长度一般与线性地址页是相同的。

1、装入进程的页目录地址(操作系统在调度进程时,把这个地址装入CR3)

2、根据线性地址的前十位,在页目录中,找到对应的索引项,页目录中的项是一个页表的地址

3、根据线性地址的中间十位,在页表中找到页的起始地址

4、将页的起始地址与线性地址的最后12位相加,得到物理地址

问题:这样的二级模式是否能够覆盖4G的物理地址空间?为什么?

答:页目录共有:2^10项,也就是说有这么多个页表;每个页表对应了:2^10页;每个页中可寻址:2^12个字节。  2^32=4GB.

Linux内核的设计没有采用intel所提供的段机制,仅仅是有限度地使用了分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。所有段的基地址均为0,由此可以得出,每个段的逻辑地址空间范围为0-4GB。因为每个段的基地址为0,因此,逻辑地址线性地址保持一致(即逻辑地址的偏移量字段的值与线性地址的值总是相同的),在Linux中所提到的逻辑地址线性地址(虚拟地址),可以认为是一致的,看来,Linux巧妙地把段机制给绕过去了,而完全利用了分页机制

Linux 2.6.29内核为每种CPU提供统一的界面,采用了四级页管理架构,来兼容二级、三级、四级管理架构的CPU。

二、Linux进程地址空间

1>虚拟内存
    
    Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间是大小

3G,用户看到和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保

护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。Linux将4G的

虚拟地址空间划分为两个部分——用户空间内核空间。用户空间从0到0xffffffff,内核空间从3G

到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程

通过系统调用访问内核空间。

2>进程空间

    用户空间进程,所以每当进程切换,用户空间就会跟着变化,每个进程的用户空间都是完全独

立、互不相干的。把一个程序同时运行10次会看到10个进程使用的线性地址一模一

样。cat/proc//maps

    创建进程fork()、程序载入execve()、动态内存分配malloc()等进程相关的操作都需要分配内存

给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。实际的物理内存只有当进程真的去

访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,才会进入分配实际页框的程序

该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去为进程分配物理页,并建立对应的页

表,这之后虚拟地址才实实在在地映射到了物理地址上。

3>内核内存分配

    在应用程序中,常使用malloc函数进行动态内存分配,而在Linux内核中,通常使用kmalloc来动

态分配内存。kmalloc的原型:

#include

void* kmalloc(size_t size, int flags)

参数:
    
    size:要分配的内存大小。


    flags:分配标志,它控制kmalloc的行为。
    
    分配标志:
    
GFP_ATOMIC:用来在进程上下文之外的代码(包括中断处理)中分配内存,从不睡眠。

GFP_KERNEL:进程上下文中的分配。可能睡眠。(16M-896M)

__GFP_DMA:这个标志要求分配能够DMA的内存区(物理地址在16M以下的页帧)

__GFP_HIGHMEM:这个标志标志分配的内存位于高端内存。(896M以上)

4>按页分配

    如果模块需要分配大块的内存,那么使用面向页的分配技术会更好。

get_zerode_page(unsigned int flags)
返回指向新页的指针,并将页面清零。

__get_free_page(unsigned int flags)
和get_free_page类似,但不清零页面。

__get_free_pages(unsigned int flags, unsigned int order)
分配若干个连续的页面,返回指向该内存区域的指针,但也不清零这段内存区域。

当程序用完这些页,可以使用下列函数之一来释放它们:

void free_page(unsigned long addr)

void free_pages(unsigned long addr, unsigned long order)

如果释放的和先前分配数目不等的页面,会导致系统错误

5>内存使用


三、Linux内核地址空间

1>:
    内核空间是由负责映射,并不会跟着进程改变,是固定的。

    物理内存896MB以上的部分称之为高端内存。

2>内核空间分布

       3G                                                          4G
        直接映射区 | 8M | 动态映射区 | 8K | KMAP区 | 固定映射区 | 4K
        
        896M(max)|   120M(min)   |     4M      |       4M

1)直接映射区(Direct Memory Region):从3G开始,最大896M的线性地址区间,我们称作直接内存

映射区,这是因为该区域的线性地址和物理地址之间存在线性转换关系
    

    线性地址 = 3G + 物理地址

2)动态内存映射区(Vmalloc Region):该区域的地址由内核函数vmalloc来进行分配,其特点是线

性空间连续,但对应的物理空间不一定连续。vmalloc分配的线性地址所对应的物理页可能处于低端

内存,也可能处于高端内存。

3)永久内存映射区(PKMap Region):对于896M以上的高端内存,可使用该区域来访问,访问方法:

    1、使用alloc_page(__GFP_HIGHMEM)分配高端内存页。

    2、使用kmap函数将分配到高端内存映射到该区域。

4)固定映射区(Fixing Mapping Region):PKMap区上面,有4M的线性空间,被称作固定映射区

它和4G顶端只要4K的隔离带。固定映射区中每个地址项都服务于特定的用途,如ACPI_BASE等。

四、Linux内核链表

1>内核链表的简介

    通常链表数据结构至少包含两个域数据域和指针域,数据域用于存储数据,指针域用于建立与

下一个结点的联系,按照指针域的组织以及各结点之间的联系形式,链表又可以分为单链表,双链

表,循环链表等多种类型。在linux内核中使用了大量的链表结构组织数据。这些链表大多采用了

[include/linux/list.h]中实现的一套精彩的链表数据结构。

2>内核链表

链表数据结构的定义:

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

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功

能,实际上,通常它都组织成双向循环链表

3>链表操作

1)初始化链表头

INIT_LIST_HEAD(list_head* head)

2)插入节点

list_add(struct list_head* new, struct list_head* head)

list_add_tail(struct list_head* new,struct list_head* head)

3)删除节点

list_del(struct list_head* entry)

4)提取数据结构

list_entry(ptr,type,member)

已知数据结构中的节点指针ptr,找出数据结构,例:list_entry(aup,struct autofs,list)

5)遍历

list_for_each(struct list_head* pos, struct list_head* head)

例:
    struct list_head* entry;
    struct list_head cs46xx_devs;//链表头

    list_for_each(entry, &cs46xx_devs)
    {
        card = list_entry(entry,struct cs_card, list);
        if(card->dev_midi == minor)
            break;
    
    }


点击(此处)折叠或打开

  1. /*******************mylist.c实例分析****************************/

  2. #include <linux/kernel.h>
  3. #include <linux/module.h>
  4. #include <linux/init.h>
  5. #include <linux/slab.h>
  6. #include <linux/list.h>

  7. MODULE_LICENSE("GPL");
  8. MODULE_AUTHOR("David Xie");
  9. MODULE_DESCRIPTION("List Module");
  10. MODULE_ALIAS("List module");

  11. struct student
  12. {
  13.     char name[100];
  14.     int num;
  15.     struct list_head list;
  16. };

  17. struct student *pstudent;
  18. struct student *tmp_student;
  19. struct list_head student_list;
  20. struct list_head *pos;

  21. int mylist_init(void)
  22. {
  23.     int i = 0;
  24.     
  25.     INIT_LIST_HEAD(&student_list);
  26.     
  27.     pstudent = kmalloc(sizeof(struct student)*5,GFP_KERNEL);
  28.     memset(pstudent,0,sizeof(struct student)*5);
  29.     
  30.     for(i=0;i<5;i++)
  31.     {
  32.      sprintf(pstudent[i].name,"Student%d",i+1);
  33.         pstudent[i].num = i+1;
  34.         list_add( &(pstudent[i].list), &student_list);
  35.     }
  36.     
  37.     
  38.     list_for_each(pos,&student_list)
  39.     {
  40.         tmp_student = list_entry(pos,struct student,list);
  41.         printk("<0>student %d name: %s\n",tmp_student->num,tmp_student->name);
  42.     }
  43.     
  44.     return 0;
  45. }


  46. void mylist_exit(void)
  47. {    
  48.     int i ;
  49.     /* 实验:将for换成list_for_each来遍历删除结点,观察要发生的现象,并考虑解决办法 */
  50.     for(i=0;i<5;i++)
  51.     {
  52.         list_del(&(pstudent[i].list));
  53.     }
  54.     
  55.     kfree(pstudent);
  56. }

  57. module_init(mylist_init);
  58. module_exit(mylist_exit);
五、Linux内核定时器

1>度量时间差

    时钟中断由系统的定时时硬件以周期性的时间间隔产生,这个间隔(即频率)由内核根据HZ来确

定,HZ是一个与体系结构无关的常数,可配置(50-1200),在X86平台,默认值为1000。每当时钟中

断发生时,全局变量jifiies(unsigned long)就加1,因此jiffies记录了字Linux启动后时钟中断

发生的次数。驱动程序利用jiffies来计算不同事件间的时间间隔。

2>延迟执行

    如果延迟的精度要求不高,最简单的实现方法如下--忙等待:

unsigned long j = jiffies + jit_delay*HZ;

while(jiffies < j )
{
    /*do nothing*/
}

3>内核定时器

    定时器用于控制某个函数(定时器处理函数)在未来的某个特定时间执行。内核定时器注册的处

理函数只执行一次——不是循环执行的。

    内核定时器被组织成双向链表,并使用struct timer_list结构描述:

struct timer_list{
    struct list_head entry;/*内核使用*/
    unsigned long expires;/*超时的jiffies*/
    void (*funnction)(unsigned long);/*超时处理函数*/
    unsigned long data;/*超时处理函数参数*/
    struct tvec_base* base;/*内核使用*/
}

    操作定时器的有如下函数:

void init_timer(struct timer_list* timer); 初始化定时器队列结构。

void add_timer(struct timer_list* timer);启动定时器。

int del_timer(struct timer_list* timer);在定时器超时前将它删除。当定时器超时后,系统会自

动的将它删除。

点击(此处)折叠或打开

  1. /************timer.c实例分析****************************/

  2. #include <linux/kernel.h>
  3. #include <linux/module.h>
  4. #include <linux/init.h>
  5. #include <linux/timer.h> /*timer*/
  6. #include <asm/uaccess.h> /*jiffies*/

  7. MODULE_LICENSE("GPL");
  8. MODULE_AUTHOR("David Xie");
  9. MODULE_DESCRIPTION("Timer Module");
  10. MODULE_ALIAS("timer module");

  11. static int times = 1;
  12. module_param(times, int, S_IRUGO);
  13. struct timer_list timer;

  14. void timer_function(unsigned long para)
  15. {
  16.     static int count=1;
  17.     if (count <= para){
  18.         if (1==count)
  19.             printk("<1>\nTimer counter begins:");
  20.         printk("<1>count = %d", count);    
  21.         mod_timer(&timer, jiffies + (HZ/2));
  22.     }else{
  23.         printk("<1>###");    
  24.         printk("<1>Timer expired %ld(specified by para) times,\n"
  25.                 "and you should remove the module to destroy it\n", para);    
  26.     }
  27.     count++;
  28. }

  29. int __init timer_init(void)
  30. {
  31.     init_timer(&timer);
  32.     timer.data = times;
  33.     timer.expires = jiffies + (HZ/2);
  34.     timer.function = timer_function;
  35.     add_timer(&timer);
  36.     
  37.     return 0;
  38. }

  39. void __exit timer_exit(void)
  40. {
  41.     del_timer(&timer);
  42.     printk("<1>Timer has been destroyed\n");    
  43. }

  44. module_init(timer_init);
  45. module_exit(timer_exit);




阅读(1799) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~