Loading

內存寻址(二) —— Linux下的内存寻址

1. Linux更喜欢分页

1.分段可以把逻辑地址分成不同的线性地址,而分页则可以把相同的线性地址映射到不同的页框中。Linux为了能够使所有进程“看到”相同的线性地址而选择非常有限地使用分段机制,这样简化了内存管理的难度。
2.Linux设计的目标是能够移植到多种流行的平台,而使用RISC的处理器对段机制支持不佳。

2. Linux的分段

Linux只有在80x86处理器上才使用分段,由于Linux下所有进程使用同样的逻辑地址空间,那么段的总数就可以被限定在很小的数目,所有的段放在GDT中也变得容易。Linux用到如下的段:

  • 内核代码段

  • 内核数据段

  • 用户态下由所有进程共享的用户代码段(由于是用户态,描述符特权等级DPL应为3)

    这里强调一下,DPL虽然有2位,特权等级可以分为0-3,0为最高级3为最低级,然而大多数的OS(包括LINUX)只使用最高级0和最低级3。

  • 用户态下由所有进程共享的用户数据段(和上一段仅TYPE域不同)

  • 任务状态段(TSS)类型域设为9或11,DPL为0

  • 被所有进程共享的LDT段,每个进程的LDT段描述符均指向它,它通常只包含一个空段描述符表项,如果一个进程需要真正的LDT,则创建一个4096字节的段。

    这里强调一下,尽管系统调用允许进程创建自己的LDT,但内核中没有使用局部描述符表。

上述是Linux使用的6个主要的段描述符,还有4个段描述符覆盖了高级电源管理功能(APM),GDT还有4个表项空闲。因此总数为14个。
当进程被创建时,其TSS和LDT被装入GDT中。

在GDT中可以使用的最多表项数是12+2×NR_TASKS,NR_TASKS表示进程最大个数。GDT最多有213=8192个表项,故NR_TASKS不能大于(8192-12)/12=4090。

内核初始化函数把首个进程的TSS描述符用下面的方法插入到GDT中:

set_tss_desc(0, &init_task.tss);

后面每一个进程都是现有进程的子进程。clone()和fork()函数使用系统调用,翻译的库函数copy_thread()创建新进程,它使用同样的方法设置TSS:

set_tss_desc(nr, &task[nr]->tss);

3. Linux的分页

Linux采用三级分页模式,这样的分页适用64位的系统。其定义有三种类型的页表(2.6kernal以后采用4级分页模式):

  • 页全局目录(Page Global Directory)
  • 页中间目录(Page Middle Directory)
  • 页表(Page Table)

image

每级页表都有三个关键的宏:

  • SHFIT
  • SIZE
  • MASK
#define PAGE_SHIFT      12
#define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK       (~(PAGE_SIZE-1))

①根据当前虚拟地址和当前进程的mm_struct获取pgd项的宏定义如下:

#define PAGE_SHIFT      12
#define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK       (~(PAGE_SIZE-1))

②根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项

/* Find an entry in the second-level page table.. */
#define pmd_offset(dir, addr)   ((pmd_t *)(dir))  

③根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址)

#ifndef CONFIG_HIGHPTE
#define __pte_map(pmd)      pmd_page_vaddr(*(pmd))
#define __pte_unmap(pte)    do { } while (0)
#else
#define __pte_map(pmd)      (pte_t *)kmap_atomic(pmd_page(*(pmd)))
#define __pte_unmap(pte)    kunmap_atomic(pte)
#endif

#define pte_index(addr)     (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))

#define pte_offset_map(pmd,addr)    (__pte_map(pmd) + pte_index(addr))
#define pte_unmap(pte)          __pte_unmap(pte)

#define pte_pfn(pte)        (pte_val(pte) >> PAGE_SHIFT)
#define pfn_pte(pfn,prot)   __pte(__pfn_to_phys(pfn) | pgprot_val(prot))

#define pte_page(pte)       pfn_to_page(pte_pfn(pte))
#define mk_pte(page,prot)   pfn_pte(page_to_pfn(page), prot)

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)

4. 保留的页框

内核代码和数据结构存放在一组保留的页框中,这些页框所含的页不会被分配或交换到磁盘上。
Linux内核一般被安装在RAM物理地址0x00100000开始的地方,也就是说,从第二个MB开始。这是因为

  • 通常页框0由BIOS使用;
  • 物理地址0x000a0000到0x000fffff的范围被留作BIOS程序使用;
  • 同时,IBM ThinkPad等机型会把前1M用作特定的计算机模式。

image

BIOS保留的第一个物理地址对应的线性地址被保存在i386_endbase变量中,通常是0x0009f000,这个变量在BIOS上电时被写入一个初值。

5. 进程页表

一个进程的线性地址空间被分成两部分:

  1. 0x00000000到0xc0000000-1,用户态进程和内核态进程都能寻址;
  2. 0xc0000000到0xffffffff的线性地址只有内核态的进程才能寻址。
    所以线性地址的第四个GB留给内核,前3GB供内核和用户程序同时访问。
posted @ 2021-12-15 00:05  ZT丶  阅读(157)  评论(0编辑  收藏  举报