当前位置:   article > 正文

linux 1.0源代码分析_Linux内核源代码情景分析-存储

linux1.0源码

总览

932207d001a3163195a0fa9627aaa58f.png

一图胜千言,自己看吧

Ch2 存储管理

基本框架

使用多级目录进行索引,实现线性地址到物理地址的映射。多级的好处是可以节省页面表所占的内存空间,如何节省?通常虚存中有空洞(free但是不释放造成),将其对应的目录项置为空,就能省去一大块空间。2.4版本的内存分配如图所示

dc5a3f01d475e061f3e4a3969813330f.gif

PMD PGD定义在pgtable-2level.h

  1. /*
  2. * traditional i386 two-level paging structure:
  3. */
  4. #define PGDIR_SHIFT 22 //pgd下标起始地址
  5. #define PTRS_PER_PGD 1024 //pgd表种的指针个数
  6. /*
  7. * the i386 is two-level, so we don't really have any
  8. * PMD directory physically.
  9. */
  10. #define PMD_SHIFT 22 //和pgd起始地址相同
  11. #define PTRS_PER_PMD 1 //pmd中只有一个表项
  12. #define PTRS_PER_PTE 1024

要想实现三层映射落实到二层映射,这里令pmd长度为0,表项数为1。实际上就是直接保持原值不变。

32位地址意味4G空间,实际上分为两块,0-3G为各个进程的用户空间,3-4G为内核的系统空间。从进程角度看,每个进程都有4G虚拟内存空间,其中3G为自己的用户空间,最高1G为所有进程与内核的共享空间。

定义在page.h

  1. #define __PAGE_OFFSET (0xC0000000)
  2. #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) //转为物理地址
  3. #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))//转为虚拟地址
  4. //processor.h
  5. #define TASK_SIZE (PAGE_OFFSET) //进程用户空间的上限就通过他来定义

地址映射的全过程

段映射

首先看段寄存器CS DS ES SS结构

1429faa123a0b4adcaf82e1ff5310b1a.png

Ti =0 GDT ; Ti =1 LDT

看看CS的定义

  1. #define start_thread(regs, new_eip, new_esp) do {
  2. __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));
  3. set_fs(USER_DS);
  4. regs->xds = __USER_DS;
  5. regs->xes = __USER_DS;
  6. regs->xss = __USER_DS;
  7. regs->xcs = __USER_CS;
  8. regs->eip = new_eip;
  9. regs->esp = new_esp;
  10. } while (0)
  11. //xds是ds的映像,其余类推

可以看到这里实际只有USER_DS USER_CS两项,实际linux内核中ds es ss不分

  1. #define __KERNEL_CS 0x10
  2. #define __KERNEL_DS 0x18
  3. #define __USER_CS 0x23
  4. #define __USER_DS 0x2B

将这几个按照二进制展开,对应段寄存器的图,可以发现一一对应,而且RPL也只用了0级和3级。

初始GDT内容定义

  1. ENTRY(gdt_table)
  2. .quad 0x0000000000000000 /* NULL descriptor */
  3. .quad 0x0000000000000000 /* not used */
  4. .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
  5. .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
  6. .quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
  7. .quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
  8. .quad 0x0000000000000000 /* not used */
  9. .quad 0x0000000000000000 /* not used */

可以看到,第一项第二项都不用, 3 4 5 6项对应上面的段寄存器的四个值。

重要的数据结构

首先是PGD PMD PT

  1. //page.h
  2. #if CONFIG_X86_PAE //36位地址
  3. typedef struct { unsigned long pte_low, pte_high; } pte_t;
  4. typedef struct { unsigned long long pmd; } pmd_t;
  5. typedef struct { unsigned long long pgd; } pgd_t;
  6. #define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
  7. #else //32位地址
  8. typedef struct { unsigned long pte_low; } pte_t;
  9. typedef struct { unsigned long pmd; } pmd_t;
  10. typedef struct { unsigned long pgd; } pgd_t;
  11. #define pte_val(x) ((x).pte_low)
  12. #endif
  13. #define PTE_MASK PAGE_MASK
  14. typedef struct { unsigned long pgprot; } pgprot_t; //页表项低12位,其中9位是标志位

3c417f66e9febbe5a403e178665e4a3e.png

上图就是一个页表项

  1. //pgtable.h
  2. //对低12位的定义,图片对不到是内核版本问题
  3. #define _PAGE_PRESENT 0x001 //存在位
  4. #define _PAGE_RW 0x002
  5. #define _PAGE_USER 0x004
  6. #define _PAGE_PWT 0x008
  7. #define _PAGE_PCD 0x010
  8. #define _PAGE_ACCESSED 0x020
  9. #define _PAGE_DIRTY 0x040
  10. #define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */
  11. #define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */

如何得到一个完整地址?

  1. //pgtable.h
  2. #define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
  3. //页面序号左移12+12位,__pte是转换成页表项的格式
  4. //page.h
  5. #define pgprot_val(x) ((x).pgprot)
  6. #define __pte(x) ((pte_t) { (x) } )

内核中有个全局量mem_map指针,指向page结构的数组,整个数组代表系统中全部的物理页面。

  1. //pgtable-2level.h
  2. #define set_pte(pteptr, pteval) (*(pteptr) = pteval) //设置页表项
  3. #define pte_none(x) (!(x).pte_low) //页表项为0
  4. //pgtable.h
  5. #define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))

页表项为0说明尚未建立映射,存在位为0说明映射的物理页面不在内存中

  1. //pgtable.h
  2. //下面存在的前提是存在位为1
  3. static inline int pte_dirty(pte_t pte) { return (pte).pte_low & _PAGE_DIRTY; }
  4. static inline int pte_young(pte_t pte) { return (pte).pte_low & _PAGE_ACCESSED; }
  5. static inline int pte_write(pte_t pte) { return (pte).pte_low & _PAGE_RW; }

地址的高20位可以看成mem_map的下标,下面就是根据下标找到对应的page

  1. #define pte_page(pte)
  2. (mem_map + (unsigned long) ((pte_val(pte) & _PFN_MASK)>> PAGE_SHIFT))

下面是根据虚存找到物理的page结构

#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))

page结构定义如下

  1. typedef struct page {
  2. struct list_head list;
  3. struct address_space *mapping;
  4. unsigned long index; //页面在文件中的序号|页面去向
  5. struct page *next_hash;
  6. atomic_t count;
  7. unsigned long flags; /* atomic flags, some possibly updated asynchronously */
  8. struct list_head lru;
  9. unsigned long age;
  10. wait_queue_head_t wait;
  11. struct page **pprev_hash;
  12. struct buffer_head * buffers;
  13. void *virtual; /* non-NULL if kmapped */
  14. struct zone_struct *zone;
  15. } mem_map_t; //物理页帧

page又被划分到管理区ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。每个管理区都有一个数据结构zone_struct。

  1. typedef struct zone_struct {
  2. /*
  3. * Commonly accessed fields:
  4. */
  5. spinlock_t lock;
  6. unsigned long offset; // 表示当前区在mem_map中的起始页面号
  7. unsigned long free_pages;
  8. unsigned long inactive_clean_pages;
  9. unsigned long inactive_dirty_pages;
  10. unsigned long pages_min, pages_low, pages_high;
  11. /*
  12. * free areas of different sizes
  13. */
  14. struct list_head inactive_clean_list;
  15. free_area_t free_area[MAX_ORDER]; // 用于伙伴分配算法
  16. /*
  17. * rarely used fields:
  18. */
  19. char *name;
  20. unsigned long size;
  21. /*
  22. * Discontig memory support fields.
  23. */
  24. struct pglist_data *zone_pgdat; //指向所属的节点
  25. unsigned long zone_start_paddr;
  26. unsigned long zone_start_mapnr;
  27. struct page *zone_mem_map;
  28. } zone_t;

管理区又要被归类到更大的节点。节点是NUMA的概念,非均匀存储结构。于是在zone_struct基础上还有一层pglist_data。

  1. typedef struct pglist_data {
  2. zone_t node_zones[MAX_NR_ZONES];//三个管理区
  3. zonelist_t node_zonelists[NR_GFPINDEX];//分配策略
  4. struct page *node_mem_map; //指向具体节点的page数组
  5. unsigned long *valid_addr_bitmap;
  6. struct bootmem_data *bdata;
  7. unsigned long node_start_paddr;
  8. unsigned long node_start_mapnr;
  9. unsigned long node_size;
  10. int node_id;
  11. struct pglist_data *node_next; //形成单链队列
  12. } pg_data_t; //存储节点,用于NUMA

463ad2614370fdd099f54c1c83d0d76b.png

再往下延申,pglist_data里有要给数组node_zonelists[],定义如下

  1. typedef struct zonelist_struct {
  2. zone_t * zones[MAX_NR_ZONES+1]; // 指针数组,各元素按特定次序指向具体的页面管理区
  3. int gfp_mask;
  4. } zonelist_t; //提供了分配策略
  5. #define NR_GFPINDEX 0x100 //zonelist_t最多有100种分配策略

下面看虚拟空间管理,以进程为基础,而没有总的仓库这种概念。vm_area_struct是其中的重要结构。

  1. struct vm_area_struct {
  2. struct mm_struct * vm_mm; /* VM area parameters */ // [start,end)
  3. unsigned long vm_start;
  4. unsigned long vm_end;
  5. /* linked list of VM areas per task, sorted by address */
  6. struct vm_area_struct *vm_next; //区间按照地址顺序排列
  7. //权限属性,同一区间里面的页面都应该相同
  8. pgprot_t vm_page_prot;
  9. unsigned long vm_flags;
  10. /* AVL tree of VM areas per task, sorted by address */
  11. //AVL树搜索更快
  12. short vm_avl_height;
  13. struct vm_area_struct * vm_avl_left;
  14. struct vm_area_struct * vm_avl_right;
  15. /* For areas with an address space and backing store,
  16. * one of the address_space->i_mmap{,shared} lists,
  17. * for shm areas, the list of attaches, otherwise unused.
  18. */
  19. struct vm_area_struct *vm_next_share;
  20. struct vm_area_struct **vm_pprev_share;
  21. struct vm_operations_struct * vm_ops;
  22. unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
  23. struct file * vm_file;
  24. unsigned long vm_raend;
  25. void * vm_private_data; /* was vm_pte (shared mem) */
  26. };

区间的划分不仅取决于地址的连续性,还取决于区间的其他属性。如果一个区间前一半和后一半的访问权限不同,也得分成两个部分。同一进程的所有区间都要按地址的高低连接在一起,用到vm_next。

两种情况下虚存会与磁盘文件交互。

1.盘区交换,按需分配的页式虚拟内存管理

2.mmap将磁盘文件映射到内存的用户空间。

虚存空间另一个重要结构是上面的vm_ops

  1. //用于虚存区间的开关和建立映射
  2. struct vm_operations_struct {
  3. void (*open)(struct vm_area_struct * area);
  4. void (*close)(struct vm_area_struct * area);
  5. struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int write_access);
  6. };
  7. //都是函数指针

还有一个指针vm_mm,指向mm_struct

  1. //sched.h
  2. //每个进程只有一个
  3. struct mm_struct {
  4. struct vm_area_struct * mmap; /* list of VMAs */
  5. struct vm_area_struct * mmap_avl; /* tree of VMAs */ /
  6. struct vm_area_struct * mmap_cache; /* last find_vma result */
  7. pgd_t * pgd;
  8. atomic_t mm_users; /* How many users with user space? */
  9. atomic_t mm_count; //有几个虚存区间
  10. int map_count; /* number of VMAs */
  11. struct semaphore mmap_sem;
  12. spinlock_t page_table_lock;
  13. struct list_head mmlist; /* List of all active mm's */
  14. unsigned long start_code, end_code, start_data, end_data;
  15. unsigned long start_brk, brk, start_stack;
  16. unsigned long arg_start, arg_end, env_start, env_end;
  17. unsigned long rss, total_vm, locked_vm;
  18. unsigned long def_flags;
  19. unsigned long cpu_vm_mask;
  20. unsigned long swap_cnt; /* number of pages to swap on next pass */
  21. unsigned long swap_address;
  22. /* Architecture-specific MM context */
  23. mm_context_t context;
  24. };

可以将mm_struct看作进程整个用户空间的抽象。虽然一个进程只有一个mm_struct,但是一个mm_struct可以被多个进程共享。

在内核中,给定一个属于某进程的虚拟地址,找到其所属的区间和对应的vm_area_struct很常见,使用find_vma来寻找。该函数查找第一个结束地址在addr之后的内存域(VMA)实例。

  1. struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
  2. {
  3. struct vm_area_struct *vma = NULL;
  4. if (mm) {
  5. /* Check the cache first. */
  6. /* (Cache hit rate is typically around 35%.) */
  7. vma = mm->mmap_cache;//首先看缓存是否命中
  8. if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
  9. if (!mm->mmap_avl) {
  10. /* Go through the linear list. */
  11. vma = mm->mmap;
  12. while (vma && vma->vm_end <= addr)
  13. vma = vma->vm_next;
  14. } else {
  15. /* Then go through the AVL tree quickly. */
  16. struct vm_area_struct * tree = mm->mmap_avl;
  17. vma = NULL;
  18. for (;;) {
  19. if (tree == vm_avl_empty)
  20. break;
  21. if (tree->vm_end > addr) {
  22. vma = tree;
  23. if (tree->vm_start <= addr)
  24. break;
  25. tree = tree->vm_avl_left;
  26. } else
  27. tree = tree->vm_avl_right;
  28. }
  29. }
  30. if (vma)
  31. mm->mmap_cache = vma;
  32. }
  33. }
  34. return vma;
  35. }

如果函数返回0,说明地址尚未建立,那么就要新建虚存区进行插入。

  1. void insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
  2. {
  3. lock_vma_mappings(vmp); //新区间的锁
  4. spin_lock(&current->mm->page_table_lock);//整个虚存空间的锁
  5. __insert_vm_struct(mm, vmp); //调用主体
  6. spin_unlock(&current->mm->page_table_lock);
  7. unlock_vma_mappings(vmp);
  8. }

下面是insert_vm_struct的调用主体

  1. void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
  2. {
  3. struct vm_area_struct **pprev; //链表结构通常使用双指针
  4. struct file * file;
  5. if (!mm->mmap_avl) {
  6. //重点看下面三行
  7. pprev = &mm->mmap;//下图可以看出mmap的位置
  8. while (*pprev && (*pprev)->vm_start <= vmp->vm_start)
  9. pprev = &(*pprev)->vm_next;
  10. } else {
  11. struct vm_area_struct *prev, *next;
  12. avl_insert_neighbours(vmp, &mm->mmap_avl, &prev, &next);
  13. pprev = (prev ? &prev->vm_next : &mm->mmap);
  14. if (*pprev != next)
  15. printk("insert_vm_struct: tree inconsistent with listn");
  16. }
  17. vmp->vm_next = *pprev;
  18. *pprev = vmp;
  19. mm->map_count++;
  20. if (mm->map_count >= AVL_MIN_MAP_COUNT && !mm->mmap_avl)
  21. build_mmap_avl(mm);
  22. //file暂时略过
  23. }

bee6e2ce30f2e1729330854b703491a9.png

下面是大头,越界访问do_page_fault

首先看看整体流程图

4dba2d5c3ca209e20c869bd433082c48.png

vmalloc在最高的内核系统空间,可以看前一篇文章。

首先该函数产生的原因是什么?页式存储是将虚拟地址映射为物理地址,如果映射失败产生缺页异常,就会调用该函数。异常有下面三种情况:

1.映射尚未建立,或者使用时已撤销

2.物理页面不在内存中

3.访问页面的权限不符,例如只读

代码较长,分段看

  1. //fault.c
  2. asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
  3. {
  4. struct task_struct *tsk;
  5. struct mm_struct *mm;
  6. struct vm_area_struct * vma;
  7. unsigned long address;
  8. unsigned long page;
  9. unsigned long fixup;
  10. int write;
  11. siginfo_t info;
  12. /* get the address */
  13. __asm__("movl %%cr2,%0":"=r" (address)); // 获取发生错误的虚拟地址
  14. tsk = current; //当前进程的task_struct
  15. if (address >= TASK_SIZE) // 不是用户空间的地址
  16. goto vmalloc_fault;
  17. mm = tsk->mm;
  18. info.si_code = SEGV_MAPERR;
  19. if (in_interrupt() || !mm)//见下文
  20. goto no_context;
  21. down(&mm->mmap_sem); //加锁
  22. vma = find_vma(mm, address); // 查找第一个结束地址比address大的vma
  23. if (!vma)
  24. goto bad_area;
  25. if (vma->vm_start <= address) // address在vma管理的范围内
  26. goto good_area;
  27. if (!(vma->vm_flags & VM_GROWSDOWN)) // 如果vma不是栈空间, 那说明用户访问了错误的内存地址
  28. goto bad_area;

为什么要用汇编取地址,因为发生异常时,CPU将导致映射失败的线性地址放在CR2寄存器,但c语言无法读。

传进来的两个参数

regs:异常前夕保存的cpu中各寄存器副本

error_code:映射失败的具体原因

还有两个须检测的特殊情况:

1.in_interrupt()!=0,说明映射失败发生在某中断服务中,与当前进程无关

2.mm==NULL ,说明映射尚未建立,也不可能与当前进程有关

如果二者都与当前进程无关,那么是什么?仍然是某个中断/异常,只不过in_interrput()无法检测到,no_context暂时不讨论。

以上两步知道了映射失败的地址和进程,下面应该干什么?应该弄清楚该地址是否落在某个已经建立起映射的区间且指出是哪个区间(vm_area_struct)。因此调用find_vma()。

如果找到了一个区间,且其起始地址不高于给定地址,说明映射已经建立,就转向good_area找失败原因。

最后一种情况是落在两个区间中间的空洞里,说明映射尚未建立或已经撤销。

空洞有两种:

1.堆栈以下的大空洞,是动态分配仍未分配的空间

2.映射区被撤销而留下,或者建立映射时跳过一段地址。

如何分辨?堆栈区向下伸展,如果区间的vm_flags的标志位VM_GROWSDOWN为0,说明上方并非堆栈区,这就是情况2。

继续看do_page_fault()

  1. bad_area:
  2. up(&mm->mmap_sem);//不再需要互斥,退出临界区
  3. bad_area_nosemaphore:
  4. /* User mode accesses just cause a SIGSEGV */
  5. if (error_code & 4) { // 用户空间触发的虚拟内存地址越界访问, 发送SIGSEGV信息(段错误)
  6. tsk->thread.cr2 = address;
  7. tsk->thread.error_code = error_code;
  8. tsk->thread.trap_no = 14;
  9. info.si_signo = SIGSEGV;
  10. info.si_errno = 0;
  11. /* info.si_code has been set above */
  12. info.si_addr = (void *)address;
  13. force_sig_info(SIGSEGV, &info, tsk);
  14. return;
  15. }
  16. /*
  17. * Pentium F0 0F C7 C8 bug workaround.
  18. */
  19. if (boot_cpu_data.f00f_bug) {
  20. unsigned long nr;
  21. nr = (address - idt) >> 3;
  22. if (nr == 6) {
  23. do_invalid_op(regs, 0);
  24. return;
  25. }
  26. }

这里不再对mm_struct操作,所以不再需要互斥。为什么mm_struct和互斥相关?多进程可能共享一个mm_struct。

下面是error_code的解释

  1. /*
  2. * This routine handles page faults. It determines the address,
  3. * and the problem, and then passes it off to one of the appropriate
  4. * routines.
  5. *
  6. * error_code:
  7. * bit 0 == 0 means no page found, 1 means protection fault
  8. * bit 1 == 0 means read, 1 means write
  9. * bit 2 == 0 means kernel, 1 means user-mode
  10. */

bit2==1用户模式,是我们要讨论的。

下面讲用户堆栈拓展

考虑一种情况,用户堆栈过小,访问越界,但是却得以拓展空间。假设堆栈指针esp指向堆栈区的起始地址,而堆栈是自顶向下延申的,堆栈下方是空洞。现在需要将某个地址压入esp-4,就会落入空洞,下面看代码。

  1. if (!(vma->vm_flags & VM_GROWSDOWN))
  2. // 如果vma不是栈空间, 那说明用户访问了错误的内存地址
  3. goto bad_area;
  4. if (error_code & 4) { // 如果在用户态
  5. /*
  6. * accessing the stack below %esp is always a bug.
  7. * The "+ 32" is there due to some instructions (like
  8. * pusha) doing post-decrement on the stack and that
  9. * doesn't show up until later..
  10. */
  11. if (address + 32 < regs->esp)
  12. goto bad_area;
  13. }
  14. if (expand_stack(vma, address)) // 扩大栈空间的vma管理范围
  15. goto bad_area;

VM_GROWSDOWN为1说明空洞上方是堆栈空间,所以继续往前执行。当error_code的bit2==1说明映射失败在用户空间,这里因为堆栈操作越界需要被特殊对待,32的范围内可以延申超过则会报错。

那么如何拓展?使用expand_stack()从空洞顶部分配若干页面建立映射,并且合并入堆栈区间。

  1. // mm.h
  2. static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)
  3. { //vma代表一个区间
  4. unsigned long grow;
  5. address &= PAGE_MASK; //边界对齐
  6. grow = (vma->vm_start - address) >> PAGE_SHIFT; //需要增长的页面数
  7. if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur ||
  8. ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)
  9. return -ENOMEM; //rlim资源分配限制
  10. vma->vm_start = address;
  11. vma->vm_pgoff -= grow;
  12. vma->vm_mm->total_vm += grow;
  13. if (vma->vm_flags & VM_LOCKED)
  14. vma->vm_mm->locked_vm += grow;
  15. return 0;
  16. }

参数vma代表用户空间堆栈所在区间。这里expand_stack()只改变了堆栈区的vm_area_struct结构但是没用建立起新拓展页面对物理内存的映射,需要在下面的good_area完成

  1. /*
  2. * Ok, we have a good vm_area for this memory access, so
  3. * we can handle it..
  4. */
  5. good_area:
  6. info.si_code = SEGV_ACCERR;
  7. write = 0;
  8. switch (error_code & 3) /* 错误是由写访问引起的 */
  9. //无权写 ??
  10. default: /* 3: write, present */
  11. #ifdef TEST_VERIFY_AREA
  12. if (regs->cs == KERNEL_CS)
  13. printk("WP fault at %08lxn", regs->eip);
  14. #endif
  15. //写访问出错
  16. case 2: /* write, not present */
  17. if (!(vma->vm_flags & VM_WRITE))
  18. goto bad_area;
  19. write++;
  20. break;
  21. case 1: /* read, present */
  22. goto bad_area;
  23. case 0: /* read, not present */
  24. if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
  25. goto bad_area;
  26. }
  27. /**
  28. * 线性区的访问权限与引起异常的类型相匹配,调用handle_mm_fault分配一个新页框。
  29. * handle_mm_fault中会处理请求调页和写时复制两种机制。
  30. */
  31. switch (handle_mm_fault(mm, vma, address, write)) { // 这里是进行物理内存映射的地方
  32. case 1:
  33. tsk->min_flt++;
  34. break;
  35. case 2:
  36. tsk->maj_flt++;
  37. break;
  38. case 0:
  39. goto do_sigbus;
  40. default:
  41. goto out_of_memory;
  42. }

switch传进去的error_code在前面已有定义,在这里bit1==1代表写操作,bit0==0代表没有物理页面。假设上述两种情况满足,那么就要检查相应区间是否允许写入,堆栈段允许,接下来就要调用handle_mm_fault,

分配新的页框。

  1. /**
  2. * 当程序缺页时,调用此过程分配新的页框。
  3. * mm-异常发生时,正在CPU上运行的进程的内存描述符
  4. * vma-指向引起异常的线性地址所在线性区的描述符。
  5. * address-引起异常的地址。
  6. * write_access-如果tsk试图向address写,则为1,否则为0。
  7. */
  8. int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
  9. unsigned long address, int write_access)
  10. {
  11. int ret = -1;
  12. pgd_t *pgd;
  13. pmd_t *pmd;
  14. pgd = pgd_offset(mm, address); // 页目录项
  15. pmd = pmd_alloc(pgd, address); // 页中间项(x86与页目录相同, 详细参考:include/asm-i386/pgalloc-2level.h), pmd == pgd
  16. if (pmd) {
  17. pte_t * pte = pte_alloc(pmd, address);
  18. if (pte)
  19. ret = handle_pte_fault(mm, vma, address, write_access, pte);
  20. }
  21. return ret;
  22. }

其中pgd_offset pmd_alloc定义如下

  1. #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
  2. //PGDIR_SHIFT 22 PTRS_PER_PGD 1024
  3. //本质是取高10
  4. #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
  5. //mm是数组的初始地址
  6. extern inline pmd_t * pmd_alloc(pgd_t *pgd, unsigned long address)
  7. {
  8. if (!pgd)
  9. BUG();
  10. return (pmd_t *) pgd; //因为只使用了两层映射,所以pmd只含一个表项,保持原值不变
  11. }

找到了pgd,分配了pmd,接下来该去页面表中寻找页表项。页表项可能为空,所以需要使用下面的函数预先分配。

  1. extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address)
  2. {
  3. address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
  4. //PAGE_SHIFT 12
  5. //先将地址转为页表中的下标
  6. if (pmd_none(*pmd)) // 如果页表还没有申请
  7. goto getnew;
  8. if (pmd_bad(*pmd))
  9. goto fix;
  10. return (pte_t *)pmd_page(*pmd) + address; // 获取页表项地址
  11. getnew:
  12. {
  13. unsigned long page = (unsigned long) get_pte_fast(); // 快速申请一个页表,缓存机制
  14. if (!page)
  15. return get_pte_slow(pmd, address); //页面之前被交换到磁盘上所以slow
  16. set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page)));
  17. // 设置页中间项的页表地址,实际写入的是pgd
  18. return (pte_t *)page + address;
  19. }
  20. fix:
  21. __handle_bad_pmd(pmd);
  22. return NULL;
  23. }

剩下的是物理内存页面处理,handle_pte_fault

  1. static inline int handle_pte_fault(struct mm_struct *mm,
  2. struct vm_area_struct * vma, unsigned long address,
  3. int write_access, pte_t * pte)
  4. {
  5. pte_t entry;
  6. /*
  7. * We need the page table lock to synchronize with kswapd
  8. * and the SMP-safe atomic PTE updates.
  9. */
  10. spin_lock(&mm->page_table_lock);
  11. entry = *pte;
  12. if (!pte_present(entry)) { // 如果内存页不在物理内存中
  13. /*
  14. * If it truly wasn't present, we know that kswapd
  15. * and the PTE updates will not touch it later. So
  16. * drop the lock.
  17. */
  18. spin_unlock(&mm->page_table_lock);
  19. if (pte_none(entry)) // 内存页还没申请
  20. return do_no_page(mm, vma, address, write_access, pte);
  21. return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access); // 内存页被交换到磁盘中
  22. }
  23. // 到这里表示内存页在物理内存中
  24. if (write_access) { // 如果因为写内存页导致的
  25. if (!pte_write(entry)) // 如果因为没有写权限
  26. return do_wp_page(mm, vma, address, pte, entry);
  27. // 这里有没有可能发生呢? 答案是有的, 因为在多核CPU系统中, 有可能多个CPU同时在修复这个错误
  28. // 当其中一个CPU修复好后, 另外的CPU就会进入这里
  29. entry = pte_mkdirty(entry); // 设置内存页为脏的
  30. }
  31. entry = pte_mkyoung(entry); // 设置内存页为年轻的
  32. establish_pte(vma, address, pte, entry);
  33. spin_unlock(&mm->page_table_lock);
  34. return 1;
  35. }

该情景里面,无论页面表是新分配还是原来就有的,相应页表项都为空。所以!present可以满足,进一步pte_none()也可以满足,因此必定进入do_no_page。

  1. /**
  2. * 当被访问的页不在主存中时,如果页从没有访问过,或者映射了磁盘文件
  3. * 那么pte_none宏会返回1,handle_pte_fault函数会调用本函数装入所缺的页。
  4. */
  5. static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
  6. unsigned long address, int write_access, pte_t *page_table)
  7. {
  8. struct page * new_page;
  9. pte_t entry;
  10. /**
  11. * vma->vm_ops || !vma->vm_ops->nopage,这是判断线性区是否映射了一个磁盘文件。
  12. * 这两个值只要某一个为空,说明没有映射磁盘文件。也就是说:它是一个匿名映射。
  13. * nopage指向装入页的函数。
  14. * 当没有映射时,就调用do_anonymous_page获得一个新的页框。
  15. */
  16. if (!vma->vm_ops || !vma->vm_ops->nopage)
  17. /**
  18. * do_anonymous_page获得一个新的页框。分别处理写请求和读讨还。
  19. */
  20. return do_anonymous_page(mm, vma, page_table, write_access, address);
  21. //下面的略过
  22. }

当多个进程将同一个文件映射到自己的虚存空间时,采用了COW(写时复制)的技术。当mmap()将一块虚存空间与一个已打开文件建立起映射后,调用我们之前提到的vm_ops可以将对内存的操作转为对文件的操作。

vma->vm_ops->nopage是预先给虚存空间指定分配物理内存页面的操作。

如果二者都为空,就要分配新的页框

  1. static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table, int write_access, unsigned long addr)
  2. {
  3. struct page *page = NULL;
  4. pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));//下文
  5. if (write_access) {
  6. page = alloc_page(GFP_HIGHUSER); // 申请一个内存页
  7. if (!page)
  8. return -1;
  9. clear_user_highpage(page, addr);
  10. entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));//下文分析
  11. mm->rss++;
  12. flush_page_to_ram(page);
  13. }
  14. set_pte(page_table, entry);
  15. /* No need to invalidate - it was non-present before */
  16. update_mmu_cache(vma, addr, entry);
  17. return 1; /* Minor fault */
  18. }

如果引起页面异常的是读操作,则mk_pte()构筑的映射项通过pte_wrprotect修正

如果引起页面异常的是写操作,则mk_pte()构筑的映射项通过pte_mkwrite 修正

  1. static inline pte_t pte_wrprotect(pte_t pte)
  2. { (pte).pte_low &= ~_PAGE_RW; return pte; }
  3. //将RW位置0,表示只能读
  4. static inline int pte_write(pte_t pte)
  5. { return (pte).pte_low & _PAGE_RW; }
  6. //将RW位置1
  7. static inline pte_t pte_mkwrite(pte_t pte)
  8. { (pte).pte_low |= _PAGE_RW; return pte; }

对于读操作,所映射的物理页面总是ZERO_PAGE

  1. extern unsigned long empty_zero_page[1024];
  2. #define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))
  3. //页面内容全为0

只读的页面,一开始都映射到同一个empty_zero_page页面,不管虚拟地址是什么,且页面内容全为0.

最后说一个概念,CPU从一次页面异常返回到用户空间时,会先执行因为映射失败而停止的指令,然后才往下执行。而中断和自陷,cpu则将下一条本该执行的指令地址压入堆栈作为中断的返回地址,下次从下一条指令开始。

物理页面的使用与周转

盘区交换的本质是时间换空间。

物理内存的管理使用了page对每个内存页面编号,并且使用mem_map指向该page数组。

类似的,交换设备(磁盘/文件)的每个物理页面也要在内存中有数据结构,不过该结构的本质是计数,表示该页面是否被使用,有几个用户在共享。盘上页面的管理按照文件或磁盘设备来进行。

  1. struct swap_info_struct {
  2. unsigned int flags;
  3. kdev_t swap_device;
  4. spinlock_t sdev_lock;
  5. struct dentry * swap_file;
  6. struct vfsmount *swap_vfsmnt;
  7. unsigned short * swap_map; // 交换区位图(每个交换区页面用一个short来计数)
  8. unsigned int lowest_bit; // 可用的交换页面开始位置
  9. unsigned int highest_bit; // 可用的交换页面结束位置
  10. unsigned int cluster_next;
  11. unsigned int cluster_nr;
  12. int prio; /* swap priority */
  13. int pages; // 交换区有多少个页面
  14. unsigned long max; // 交换区最大的页面号
  15. int next; /* next entry on swap list */
  16. };

swap_map对应mem_map,指向盘上的一个物理页面,数组大小看pages

通常设备上的第一个页面swap_map[0]不用于交换,包含设备自身的信息和表明哪些页面可供使用的位图。

存储介质是磁盘,地址连续存储不一定高效,所以按集群cluster方式进行,即cluster_next,cluster_nr的用途。类似内存的zone对page的管理。

下图是cluster对swap_map的管理

7a713f6638fc7bd3c2314318f301da8d.png

内核允许使用多个页面交换设备/文件,于是建立了swap_info_struct结构的数组swap_info[]。

struct swap_info_struct swap_info[MAX_SWAPFILES];

如何链接起这些不同的设备/文件?使用swap_list按照优先级高低来进行。初始时队列为空,头尾都初始为-1,当系统调用swap_on()指定将一个设备/文件用于交换时,该设备/文件就联入链表。

  1. struct swap_list_t swap_list = {-1, -1}; //初始化
  2. struct swap_list_t {
  3. int head; /* head of priority-ordered swapfile list */
  4. int next; /* swapfile to be used next */
  5. }; //定义

在物理->虚拟内存的映射中通过pte_t建立联系,这里类似的通过swp_entry_t在设备/文件和内存间建立联系。

  1. +------------------------+-------+-+
  2. |xxxxxxxxxxxxxxxxxxxxxxxx|xxxxxxx|0|
  3. +------------------------+-------+-+
  4. ________offset__________/_type_/
  5. 因为交换区页面一定不在物理内存中, 所以最低位类似pte存在位一定是0
  6. */
  7. typedef struct {
  8. unsigned long val;
  9. } swp_entry_t;//本质是32位无符号整数

swp_entry_t的高24位为offset,低7位为type,最低位一直为0.

定义如下

  1. #define SWP_TYPE(x) (((x).val >> 1) & 0x3f)
  2. #define SWP_OFFSET(x) ((x).val >> 8)
  3. #define SWP_ENTRY(type, offset) ((swp_entry_t) { ((type) << 1) | ((offset) << 8) })
  4. #define pte_to_swp_entry(pte) ((swp_entry_t) { (pte).pte_low })
  5. #define swp_entry_to_pte(x) ((pte_t) { (x).val })

offset代表页面在一个磁盘/文件中的位置,type代表在哪个设备/文件中。

下面看看常用函数

  1. //释放磁盘页面
  2. /*
  3. * Caller has made sure that the swapdevice corresponding to entry
  4. * is still around or has not been recycled.
  5. */
  6. void __swap_free(swp_entry_t entry, unsigned short count)
  7. {
  8. struct swap_info_struct * p;
  9. unsigned long offset, type;
  10. if (!entry.val)
  11. goto out;
  12. type = SWP_TYPE(entry);
  13. if (type >= nr_swapfiles)
  14. goto bad_nofile;
  15. p = & swap_info[type];//p是swap_info_struct在swap_info中的地址
  16. if (!(p->flags & SWP_USED))
  17. goto bad_device;

entry.val==0,在设备/文件中页面0不用于页面交换,所以goto out

type代表在哪个设备/文件中,即swap_info_struct在swap_info[]中的下标。

  1. offset = SWP_OFFSET(entry);//页面在文件中的位置
  2. if (offset >= p->max)
  3. goto bad_offset;
  4. if (!p->swap_map[offset])
  5. goto bad_free;
  6. swap_list_lock();
  7. if (p->prio > swap_info[swap_list.next].prio)
  8. swap_list.next = type;
  9. swap_device_lock(p);
  10. if (p->swap_map[offset] < SWAP_MAP_MAX) {//不能超过最大分配计数
  11. if (p->swap_map[offset] < count)
  12. goto bad_count;
  13. if (!(p->swap_map[offset] -= count)) {
  14. if (offset < p->lowest_bit)//超出范围就调整为当前值
  15. p->lowest_bit = offset;
  16. if (offset > p->highest_bit)
  17. p->highest_bit = offset;
  18. nr_swap_pages++;
  19. }
  20. }
  21. swap_device_unlock(p);
  22. swap_list_unlock();
  23. out:
  24. return;

p->swap_map[offset]是该页面的分配和使用计数,0代表尚未分配

SWAP_MAP_MAX为最大分配计数 传进的参数count表示有几个使用者释放该页面,从技术中减去count,计数为0说明页面空闲

实际上释放磁盘页面的操作不涉及磁盘操作,只是内存中“账面”上的操作,所以花费代价极小。

以上都是内存页面和盘上页面的管理,下面看看内存页面的周转。分两种情况:

1.页面的分配,使用和回收

2.盘区交换,最终目的也是页面回收

实际上,只有映射到用户空间的页面才会被换出,系统空间的不会。在内核中可以访问所有的物理页面,换言之所有的物理页面在系统空间中都有映射。

下面讲一些概念:

用户空间的页面分配分下面几种:

1.普通的用户空间页面,包括进程的代码段 数据段 堆栈段 堆

2.mmap()映射到用户空间的文件内容

3.进程间的共享内存区

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/508463
推荐阅读
相关标签
  

闽ICP备14008679号