概述
在前一章我们知道
- 伙伴算法通过__get_free_pages()或alloc_pages()从分区页框中获得页框
- slab分配器通过kmem_cache_alloc()或kmalloc()为专用或通用得对象分配块
- vmalloc通过vmalloc()或vmalloc_32()获得一块连续的非连续的内存区。
如果所请求的内存区得以满足,这些函数都返回一个页描述符地址或线性地址(即所分配动态内存的起始地址)
之所以得以分配,是因为:
- 内核是操作系统中优先级最高的成分
- 内核信任自己
但是当给用户态进程分配内存时,情况完全不同:
- 进程对动态内存的请求呗认为时不紧迫的,内核总是尽量推迟给用户态进程分配动态内存
- 由于用户进程是不可信任的,因此,内核必须能随时准备波混用户态进程引起的所有寻址错误
内核使用一种新的资源成功实现了对进程动态内存的推迟分配,当用户台进程请求动态内存时,并没有获得请求的页框,而仅仅u后的对一个新的线性地址区域的使用权,这个线性地址区间就成为进程地址空间的一部分。叫做“线性区”(memory region)
一、进程地址空间的基本组成
所谓进程地址空间(process address space),就是从进程的视角看到的地址空间,是进程运行时所用到的虚拟地址的集合。
以IA-32处理器为例,其虚拟地址为32位,因此其虚拟地址空间的范围为0~2 ^ 32-1,Linux系统将地址空间按3:1比例划分,其中用户空间(user space)占3GB,内核空间(kernel space)占1GB。
假设物理内存也是4GB(事实上,虚拟地址空间的范围不一定需要和物理地址空间的大小相同),则虚拟地址空间和物理地址空间的转换如下图所示:
用户空间的进程只能访问整个虚拟地址空间的0 ~ 3GB部分,不能直接访问3G~4GB的内核空间部分,但出于对性能方面的考虑,Linux中内核使用的地址也是映射到进程地址空间的(被所有进程共享),因此进程的虚拟地址空间可视为整个4GB(虽然实际只有3GB)。
进程的地址空间由允许进程使用的全部线性地址组成。每个进程所看到的线性地址集合是不同的,一个进程所使用的地址与另一个进程所使用的地址之间没有什么关系。内核可以通过增加过着删除某些线性地址区间动态地修改进程的地址空间。
下面是进程获得新线性区的一些典型情况:
与创建、删除线性区相关的系统调用
我们会再”缺页异常处理程序“ 一节中看到,确定一个进程当前所拥有的线性区(即进程的地址空间)是内核的基本任务,因为这可以让缺页异常程序有效地区分引发这哥异常处理程序的两种不同类型的无效线性地址:
- 有编程错误引发的无效线性地址
- 有缺页引发的无效线性地址,即使这个线性地址属于进程的地址空间,但是对应于这个地址的页框仍然有待分配
从进程的观点来看,后一种地址不是无效的,内核要利用这种缺页以实现请求调页:内核通过提供页框来处理这种缺页,并让进程继续执行。
二、进程空间分布
C程序一般分为:
1.代码段:程序段为程序代码在内存中的映射.一个程序可以在内存中多有个副本.
2.初始化过的数据(data段):在程序运行值初已经对变量进行初始化的
3.未初始化过的数据(BSS 段):在程序运行初未对变量进行初始化的数据
4.堆(stack):存储局部,临时变量,在程序块开始时自动分配内存,结束时自动释放内存.存储函数的返回指针.
5.栈(heap):存储动态内存分配,需要程序员手工分配,手工释放.
参考:https://blog.csdn.net/wangxiaolong_china/article/details/6844325
程序内存空间(代码段、数据段、堆栈段)
参考:https://blog.csdn.net/zztong77/article/details/80957005
那么我们思考一下,进程地址空间每一个进程都有一个,凭什么每一个进程都会有4G的大小,那么是内存不够用的。
父子进程中的数据独有,代码共享,但是通过代码实现可以在父子进程中实现对全局变量进行修改,也可以通过打印的方式看出父子进程中的同一变量的地址相同,那为什么一个一块空间可以有两个值?——虚拟地址相同,物理地址不同
其实,在程序中所获得的变量地址都是假地址。因为这个地址空间都是假的。所以我们叫它 进程的虚拟地址空间。
进程虚拟地址空间: 是操作系统为进程描述的一个假的地址空间。目的是为了让进程认为自己拥有一块连续的线性的完整的地址空间。但是实际上一个进程使用的内存并非连续存储的,而是通过页表映射了虚拟地址与物理地址之间的关系。为了让进程通过页表获取物理地址,进而实现数据的离散式存储。
进程的虚拟地址空间都是通过页表来访问自己映射的那块内存,其它的程序不能访问,这样就保证进程之间相互独立,独立就代表着稳定。
三、内存描述符
- Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息,如进程状态,进程标识符(PID) 等
- 而对于进程的地址空间,内核用两个数据结构表示进程地址空间: struct mm_struct(内存描述符) 和 struct vm_area_struct(线性区描述符) 表示。
- mm_struct,描述一个进程的整个虚拟地址空间。
- vm_area_struct,描述虚拟地址空间的一个区间(简称线性区)。
- 内存描写符mm_struct指向全部虚拟空间,而vm_area_struct只是指向了虚拟空间的一段。
- task_struct结构体包括指向mm_struct结构的指针,mm_struct用来描述进程的虚拟地址空间。mm_struct包括装入的可履行映像信息和进程的页目录指针pgd,还包括有指向vm_area_struct结构的几个指针,每一个vm_area_struct代表进程的1个虚拟地址区间。
内核用两个数据结构表示进程地址空间: struct mm_struct(内存描述符) 和 struct vm_area_struct(线性区描述符) 表示。
3.1 struct mm_struct(内存描述符)
struct mm_struct {
struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
/* Base adresses for compatible mmap() */
unsigned long mmap_compat_base;
unsigned long mmap_compat_legacy_base;
#endif
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
#ifdef CONFIG_MEMBARRIER
atomic_t membarrier_state;
#endif
atomic_t mm_users;
atomic_t mm_count;
#ifdef CONFIG_MMU
atomic_long_t pgtables_bytes; /* PTE page table pages */
#endif
int map_count; /* number of VMAs */
spinlock_t page_table_lock; /* Protects page tables and some
* counters
*/
struct rw_semaphore mmap_sem;
struct list_head mmlist; /* List of maybe swapped mm's. These
* are globally strung together off
* init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
atomic64_t pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
unsigned long def_flags;
spinlock_t arg_lock; /* protect the below fields */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
struct mm_rss_stat rss_stat;
struct linux_binfmt *binfmt;
/* Architecture-specific MM context */
mm_context_t context;
unsigned long flags; /* Must use atomic bitops to access */
struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
spinlock_t ioctx_lock;
struct kioctx_table __rcu *ioctx_table;
#endif
#ifdef CONFIG_MEMCG
struct task_struct __rcu *owner;
#endif
struct user_namespace *user_ns;
/* store ref to file /proc/<pid>/exe symlink points to */
struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_NUMA_BALANCING
unsigned long numa_next_scan;
/* Restart point for scanning and setting pte_numa */
unsigned long numa_scan_offset;
/* numa_scan_seq prevents two threads setting pte_numa */
int numa_scan_seq;
#endif
atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
/* See flush_tlb_batched_pending() */
bool tlb_flush_batched;
#endif
struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
atomic_long_t hugetlb_usage;
#endif
struct work_struct async_put_work;
} __randomize_layout;
unsigned long cpu_bitmap[];
};
mm_struct定义在include/linux/mm_types.h中,参考标志着堆、栈,数据段等的虚拟地址位置,其中的域抽象了进程的地址空间,如下图所示:
用户进程和内核进程的区别
对于Linux来说,有两个概念叫做内核空间和用户空间,以32位x86架构的Linux为例,Linux的虚拟地址空间为4GB,其中前1GB称为内核空间,后3GB称为用户空间,进程运行在内核空间时称为内核态,运行在用户空间称之为用户态。对于用户态进程来说,出于程序设计方便和内存安全的角度等原因,为每个用户态进程引入了独立的虚拟地址空间,其被映射到用户空间。
-
用户进程,平时运行在用户态,有自己的虚拟地址空间,但是可以通过中断、系统调用等内陷到内核态。
-
内核进程,没有独立的地址空间,所有内核线程的地址空间都是一样的,没有自己的地址空间,所以内核线程的task_struct 的mm域为空,其运行在内核空间,本身就是内核的一部分或者说是内核的分身。
但是刚才说过,内核线程还有核心堆栈,没有mm怎么访问它的核心堆栈呢?这个核心堆栈跟task_struct的 thread_info共享8k的空间,所以不用mm描述。
但是内核线程总要访问内核空间的其他内核啊,没有mm域毕竟是不行的。
所以内核线程被调用时,内核会将其task_strcut 的active_mm指向前一个被调度出的进程的mm域 ,在需要的时候,内核线程可以使用前一个进程的内存描述符。
因为内核线程不访问用户空间,只操作内核空间内存,而所有进程的内核空间都是一样的。这样就省下了一个mm域的内存。
1.内核经常需要在后台执行一些操作。这种任务可以通过内核线程 (kernel thread)完成。
2.内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,(实际它的mm指针被设置为NULL)
3.内核线程只在内核空间运行,从来不切换到用户空间去。内核进程和 普通进程一样,可以被调度,也可以被抢占
4.内核线程也只能由其他内核线程创建。在现有内核线程中创建一个新的内核线程的方法如下:intkernel_thread(int (*fn)(void *),void *arg, unsigned long flags)
3.2 vm_area_struct(线性区描述符)
Linux通过类型为vm_area_struct的对象实现线性区,字段如下:
/*
* This struct defines a memory VMM memory area. There is color: black; background-color: #a0ffff;">vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/* For areas with an address space and backing store,
* font-size: 10px;">vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};
vm_area_struct和mm_struct、task_struct的关系如下:
vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。
mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。
3.2.1 线性区数据结构
参考:https://blog.csdn.net/yunsongice/article/details/5637501
进程所拥有的所有线性区是通过一个简单的链表链接在一起的。出现在链表中的线性区是按内存地址的升序排列的;不过,每两个线性区可以由未用的内存地址区隔开。每个vm_area_struct元素的vm_next字段指向链表的下一个元素。内核通过进程的内存描述符的mmap字段来查找线性区,其中mmap字段指向链表中的第一个线性区描述符。
内存描述符的map_count字段存放进程所拥有的线性区数目。默认情况下,一个进程可以最多拥有65536个不同的线性区,系统管理员可以通过写/proc/sys/vm/max_map_count文件来修改这个限定值。
下图显示了进程的地址空间、它的内存描述符以及线性区链表三者之间的关系。
内核频繁执行的一个操作就是查找包含指定线性地址的线性区。由于链表是经过排序的,因此,只要在指定线性地址之后找到一个线性区,搜索就可以结束。
然而,仅当进程的线性区非常少时使用这种链表才是很方便的,比如说只有一二十个线性区。在链表中查找元素、插入元素、删除元素涉及许多操作,这些操作所花费的时间与链表的长度成线性比例。
尽管多数的Linux进程使用的线性区的数量非常少,但是诸如面向对象的数据库,或malloc()的专用调试器那样过于庞大的大型应用程序可能会有成百上千的线性区。在这种情况下,线性区链表的管理变得非常低效,因此,与内存相关的系统调用的性能就降低到令人无法忍受的程度。
因此,Linux 2.6把内存描述符存放在叫做红-黑树(red-black tree)的数据结构中。
红-黑树算法
红-黑树是一个扩展了的平衡二叉树。我们先来回忆回忆二叉排序树的概念:每个元素(或说节点)通常有两个孩子:左孩子和右孩子。树中的元素被排序。对关键字为N的节点,它的左子树上的所有元素的关键字都比N小;相反,它的右子树上的所有元素的关键字都比N大【如图(a)所示】;节点的关键字被写入节点内部。而除了具有基本的二叉排序树的特点以外,红-黑树必须满足下列5条规则:
1、每个节点必须或为黑或为红。
2、树的根必须为黑。
3、新插入的节点必须为红色。
4、红节点的孩子必须为黑。
5、从一个节点到后代叶子节点的每个路径都包含相同数量的黑节点。当统计黑节点个数时,空指针也算作黑节点。
这4条规则确保具有n个内部节点的任何红一黑树其高度最多为2 × log(n+l)。
在红-黑树中搜索一个元素因此而变得非常高效,因为其操作的执行时间与树大小的对数成线性比例。换句话说,双倍的线性区个数只多增加一次循环。
例如,假如值为4的一个元素必须插入到图(a)所示的红一黑树中。它的正确位置是关键值为3的节点的右孩子,但是,一旦把它插入,值为3的红节点就具有红孩子,因此而违背了规则3。为了满足这条规则,值为3、4、7的节点颜色就得改变。但是,这种操作又会违背规则5,因此,算法在以关键值为19的节点为根节点的子树上执行“旋转”操作,产生如图(b)所示的新红一黑树。这看起来较复杂,但是,在红-黑树上插人或删除一个元素只需要少量的操作——这个操作的复杂度仅仅与树大小的对数成线性比例。
因此,为了存放进程的线性区,Linux既使用了链表,也使用了红-黑树。这两种数据结构包含指向同一线性区描述符的指针,当插入或删除一个线性区描述符时,内核通过红-黑树搜索前后元素,并用搜索结果快速更新链表而不用扫描链表。
链表的头由内存描述符的mmap字段所指向。任何线性区对象都在vm_next字段存放指向链表下一个元素的指针。红-黑树的首部由内存描述符的mm_rb字段所指向。任和线性区对象都在类型为rb_node的vm_rb字段中存放节点颜色以及指向双亲、左孩子和右孩子的指针。
一般来说,红-黑树用来确定含有指定地址的线性区,而链表通常在扫描整个线性区集合时来使用。
3.2.2 线性区的操作
下面只列了几个,其他得参考《深入理解linux内核》p376
查找给定地址的最邻近区:find_vma()
查找一个空闲的地址区间:get_unmapped_area()
分配线性地址空间:do_mmap()
释放线性地址空间:do_munmmap()
4.linux缺页异常处理
缺页异常被触发通常有两种情况——
1.程序设计的不当导致访问了非法的地址
2.访问的地址是合法的,但是该地址还未分配物理页框
- 对于第二种情况,尽管每个进程独立拥有3GB的可访问地址空间,但是这些资源都是内核开出的空头支票,也就是说进程手握着和自己相关的一个个虚拟内存区域(vma),但是这些虚拟内存区域并不会在创建的时候就和物理页框挂钩,由于程序的局部性原理,程序在一定时间内所访问的内存往往是有限的,因此内核只会在进程确确实实需要访问物理内存时才会将相应的虚拟内存区域与物理内存进行关联(为相应的地址分配页表项,并将页表项映射到物理内存),也就是说这种缺页异常是正常的
- 而第一种缺页异常是不正常的,内核要采取各种可行的手段将这种异常带来的破坏减到最小。
4.1缺页异常的原理:
参考:https://www.cnblogs.com/arnoldlu/p/8335475.html#handle_pte_fault
参考:https://zhuanlan.zhihu.com/p/66046257
参考:https://blog.csdn.net/u012489236/article/details/111415668
在Linux中,进程和内核都是通过页表PTE访问一个物理页面的,如果无法访问到正确的地址,将产生page fault(缺页异常)。
什么是page fault
page fault 又分为几种,major page fault、 minor page fault、 invalid(segment fault)。
-
major page fault 也称为 hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap 回到物理内存也是 hard page fault。
-
minor page fault 也称为 soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。
当一个进程在调用 malloc 获取虚拟空间地址后,首次访问该地址会发生一次soft page fault。通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault -
invalid fault 也称为 segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报 segment fault错误。
page fault的流程
当有中断到来时,硬件会做一些处理;对于软件来说,要做的事情是从中断向量表开始。
__vectors_start是中断异常处理的起点,具体到缺页异常路径是:
__vectors_start–>vector_dabt–>__dabt_usr/__dabt_svc–>dabt_helper–>v7_early_abort–>do_DataAbort–>fsr_info–>do_translation_fault/do_page_fault/do_sect_fault。
异常向量表:
.section .vectors, "ax", %progbits
__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, __vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt--------------------------数据异常向量
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
关于异常向量表的汇编流程,参考:https://blog.csdn.net/ee230/article/details/16862685
和https://www.cnblogs.com/arnoldlu/p/8335475.html
直接看到fsr_info:
static struct fsr_info fsr_info[] = {
/*
* The following are the standard ARMv3 and ARMv4 aborts. ARMv5
* defines these to be "precise" aborts.
*/...
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" },-----段转换错误,即找不到二级页表
{ do_bad, SIGBUS, 0, "external abort on linefetch" },
{ do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault" },---------------页表错误,没有对应的物理地址
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
{ do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault" },
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
{ do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" },
{ do_bad, SIGBUS, 0, "external abort on translation" },
{ do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault" },-------------段权限错误,二级页表权限错误
{ do_bad, SIGBUS, 0, "external abort on translation" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault" },------------页权限错误
...
}
do_page_fault的流程
do_page_fault是缺页中断的核心函数。主要工作交给__do_page_fault处理,然后进行一些异常处理__do_kernel_fault和__do_user_fault。工作大概简要如下:
- 获取当前进程的task_struct和内存管理结构体mm_struct
- 当前状态是否处于中断上下文或者禁止抢占,如果是跳转到no_context;如果当前进程没有mm,说明是一个内核线程,跳转到no_context。即跳转到__do_kernel_fault,需要调用__do_kernel_fault函数来发送Oops错误(oops参考:https://www.cnblogs.com/wwang/archive/2010/11/14/1876735.html)
- 如果发生在内核空间,且没有在exception tables查询到该地址,跳转到no_context。
- 执行到__do_page_fault(mm, addr, fsr, flags, tsk);到这里基本确认是一个用户进程
/kernel/msm-4.9/arch/arm/mm/fault.c
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
struct task_struct *tsk;
struct mm_struct *mm;
int fault, sig, code;
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
if (notify_page_fault(regs, fsr))
return 0;
tsk = current;-------------------------------------------获取当前进程的task_struct
mm = tsk->mm;-------------------------------------------获取进程内存管理结构体mm_struct
/* Enable interrupts if they were enabled in the parent context. */
if (interrupts_enabled(regs))
local_irq_enable();
/*
* If we're in an interrupt or have no user
* context, we must not take the fault..
*/
if (in_atomic() || !mm)----------------------------------in_atomic判断当前状态是否处于中断上下文或者禁止抢占,如果是跳转到no_context;如果当前进程没有mm,说明是一个内核线程,跳转到no_context。
goto no_context;
if (user_mode(regs))
flags |= FAULT_FLAG_USER;
if (fsr & FSR_WRITE)
flags |= FAULT_FLAG_WRITE;
/*
* As per x86, we may deadlock here. However, since the kernel only
* validly references user space from well defined areas of the code,
* we can bug out early if this is from code which shouldn't.
*/
if (!down_read_trylock(&mm->mmap_sem)) {
if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))----------发生在内核空间,且没有在exception tables查询到该地址,跳转到no_context。
goto no_context;
retry:
down_read(&mm->mmap_sem);---------------------------用户空间则睡眠等待锁持有者释放锁。
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case, we'll have missed the might_sleep() from
* down_read()
*/
might_sleep();
#ifdef CONFIG_DEBUG_VM
if (!user_mode(regs) &&
!search_exception_tables(regs->ARM_pc))
goto no_context;
#endif
}
fault = __do_page_fault(mm, addr, fsr, flags, tsk);
/* If we need to retry but a fatal signal is pending, handle the
* signal first. We do not need to release the mmap_sem because
* it would already be released in __lock_page_or_retry in
* mm/filemap.c. */
if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current))
return 0;
/*
* Major/minor page fault accounting is only done on the
* initial attempt. If we go through a retry, it is extremely
* likely that the page will be found in page cache at that point.
*/
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
if (fault & VM_FAULT_MAJOR) {
tsk->maj_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
regs, addr);
} else {
tsk->min_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
regs, addr);
}
if (fault & VM_FAULT_RETRY) {
/* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk
* of starvation. */
flags &= ~FAULT_FLAG_ALLOW_RETRY;
flags |= FAULT_FLAG_TRIED;
goto retry;
}
}
up_read(&mm->mmap_sem);
/*
* Handle the "normal" case first - VM_FAULT_MAJOR / VM_FAULT_MINOR
*/
if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))----没有错误,说明缺页中断处理完成。
return 0;
/*
* If we are in kernel mode at this point, we
* have no context to handle this fault with.
*/
if (!user_mode(regs))-----------------------------------判断CPSR寄存器的低4位,CPSR的低5位表示当前所处的模式。如果低4位位0,则处于用户态。见下面CPSRM4~M0细节。
goto no_context;------------------------------------进行内核空间错误处理
if (fault & VM_FAULT_OOM) {
/*
* We ran out of memory, call the OOM killer, and return to
* userspace (which will retry the fault, or kill us if we
* got oom-killed)
*/
pagefault_out_of_memory();--------------------------进行OOM处理,然后返回。
return 0;
}
if (fault & VM_FAULT_SIGBUS) {
/*
* We had some memory, but were unable to
* successfully fix up this page fault.
*/
sig = SIGBUS;
code = BUS_ADRERR;
} else {
/*
* Something tried to access memory that
* isn't in our memory map..
*/
sig = SIGSEGV;
code = fault == VM_FAULT_BADACCESS ?
SEGV_ACCERR : SEGV_MAPERR;
}
__do_user_fault(tsk, addr, fsr, sig, code, regs);------用户模式下错误处理,通过给用户进程发信号:SIGBUS/SIGSEGV。
return 0;
no_context:
__do_kernel_fault(mm, addr, fsr, regs);----------------错误发生在内核模式,如果内核无法处理,此处产生oops错误。
return 0;
}
__do_page_fault查找合适的vma(虚拟内存地址),
- 如果vma为NULL,说明addr之后没有vma,所以这个addr是个错误地址
- 如果addr后面有vma,但不包含addr,不能断定addr是错误地址,还需检查
- 权限错误也要返回,比如缺页报错(由参数fsr标识)报的是不可写/不可执行的错误,但addr所属vma线性区本身就不可写/不可执行,那么就直接返回,因为问题根本不是缺页,而是vma就已经有问题
- 然后就进入到handle_mm_fault()函数
static int __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
unsigned int flags, struct task_struct *tsk)
{
struct vm_area_struct *vma;
int fault;
vma = find_vma(mm, addr);
fault = VM_FAULT_BADMAP;
if (unlikely(!vma))
goto out;
if (unlikely(vma->vm_start > addr))
goto check_stack;
/*
* Ok, we have a good vm_area for this
* memory access, so we can handle it.
*/
good_area:
if (access_error(fsr, vma)) {
fault = VM_FAULT_BADACCESS;
goto out;
}
return handle_mm_fault(vma, addr & PAGE_MASK, flags);
check_stack:
/* Don't allow expansion below FIRST_USER_ADDRESS */
if (vma->vm_flags & VM_GROWSDOWN &&
addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
goto good_area;
out:
return fault;
}
当触发异常的虚拟地址属于某个vma,并且拥有触发页错误异常的权限时,会调用到handle_mm_fault函数
进入到了handle_mm_fault()函数,该函数为触发缺页异常的地址address分配各级的页目录,也就是说现在已经拥有了一个和address配对的pte(页表 Page Table Entry)了,但是这个pte如何去映射物理页框,内核又得根据pte的状态进行分类和判断
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
if (unlikely(is_vm_hugetlb_page(vma)))
return hugetlb_fault(mm, vma, address, flags);
pgd = pgd_offset(mm, address);------------------------------------获取当前address在当前进程页表项PGD页面目录项。
pud = pud_alloc(mm, pgd, address);--------------------------------获取当前address在当前进程对应PUD页表目录项。
if (!pud)
return VM_FAULT_OOM;
pmd = pmd_alloc(mm, pud, address);--------------------------------找到当前地址的PMD页表目录项
if (!pmd)
return VM_FAULT_OOM;
if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) {
int ret = VM_FAULT_FALLBACK;
if (!vma->vm_ops)
ret = do_huge_pmd_anonymous_page(mm, vma, address,
pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *pmd;
int ret;
barrier();
if (pmd_trans_huge(orig_pmd)) {
unsigned int dirty = flags & FAULT_FLAG_WRITE;
/*
* If the pmd is splitting, return and retry the
* the fault. Alternative: wait until the split
* is done, and goto retry.
*/
if (pmd_trans_splitting(orig_pmd))
return 0;
if (pmd_protnone(orig_pmd))
return do_huge_pmd_numa_page(mm, vma, address,
orig_pmd, pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = do_huge_pmd_wp_page(mm, vma, address, pmd,
orig_pmd);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(mm, vma, address, pmd,
orig_pmd, dirty);
return 0;
}
}
}
/*
* Use __pte_alloc instead of pte_alloc_map, because we can't
* run pte_offset_map on the pmd, if an huge pmd could
* materialize from under us from a different thread.
*/
if (unlikely(pmd_none(*pmd)) &&
unlikely(__pte_alloc(mm, vma, pmd, address)))
return VM_FAULT_OOM;
/* if an huge pmd materialized from under us just retry later */
if (unlikely(pmd_trans_huge(*pmd)))
return 0;
/*
* A regular pmd is established and it can't morph into a huge pmd
* from under us anymore at this point because we hold the mmap_sem
* read mode and khugepaged takes it in write mode. So now it's
* safe to run pte_offset_map().
*/
pte = pte_offset_map(pmd, address);-------------------------------根据address从pmd中获取pte指针
return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}
handle_mm_fault的核心又是handle_pte_fault。
handle_pte_fault中根据也是否存在分为几类:do_fault(文件映射缺页中断)、do_anonymous_page(匿名页面缺页中断)、do_swap_page()和do_wp_page(写时复制)。
handle_pte_fault对各种缺页异常进行了区分,然后进行处理。
异常处理总结:
5. 创建和删除进程的地址空间
参考:https://blog.csdn.net/yunsongice/article/details/5640034
回忆一下“进程的创建 —— do_fork()函数详解 ”博文:当创建一个新的进程时内核调用**copy_mm()**函数。这个函数通过建立新进程的所有页表和内存描述符来创建进程一的地址空间
当进程结束时,内核调用exit_mm()函数释放进程的地址空间
6. 堆的管理
每个Unix进程都拥有一个特殊的线性区,这个线性区就是所谓的堆(heap),堆用于满足进程的动态内存请求。内存描述符的start_brk与brk字段分别限定了这个区的开始地址和结束地址。
进程可以使用下面的API来请求和释放动态内存:
malloc(size):请求size个字节的动态内存
calloc(n,size):请求含n个大小为size的元素的一个数组。
realloc(ptr,size):该表由前面的malloc()或calloc()分配内存区字段的大小。
free(addr):释放由malloc()和calloc()分配的其实地址为addr的线性区。
brk(addr):直接修改堆的大小。
sbrk(incr):类似于brk()
最后
以上就是热心冬瓜为你收集整理的linux内核学习3:linux的进程地址空间一、进程地址空间的基本组成二、进程空间分布三、内存描述符4.linux缺页异常处理5. 创建和删除进程的地址空间6. 堆的管理的全部内容,希望文章能够帮你解决linux内核学习3:linux的进程地址空间一、进程地址空间的基本组成二、进程空间分布三、内存描述符4.linux缺页异常处理5. 创建和删除进程的地址空间6. 堆的管理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复