操作系统进程空间

进程地址空间

每个程序编译链接后形成的二进制映像文件有一个代码段(Text)和数据段(BSS和Data)。

进程运行时须有独占的堆(Heap)和栈(Stack)空间。堆是进程运行时动态申请的一个内存空间

程序在运行过程中还需要依赖动态链接库,这些动态链接库以 .so 文件的形式存放在磁盘中,这些动态链接库也有自己的对应的代码段,数据段,BSS 段,也需要一起被加载进内存中,还有用于内存文件映射的系统调用 mmap,会将文件与内存进行映射,那么映射的这块内存(虚拟内存)也需要在虚拟地址空间中有一块区域存储。这些动态链接库中的代码段,数据段,BSS 段,以及通过 mmap 系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做文件映射与匿名映射区。

栈用来存放调用的函数,调用函数的参数,一些局部变量等

代码段用来存放二进制文件中的机器指令

Linux 把进程的用户空间划分为若干个区,便于管理,这些区间称为虚拟内存域(简称vma )。 一个进程的用户地址空间主要由 mm_struct 结构(虚拟空间整体描述)和 vm_area_struct 结构来描述。

mm_struct内存描述符

每个进程都有自己的用户空间,但fork创建进程时,调用clone创建内核线程时会共享父进程的用户空间。

linux内存映像不会把全部内容加载到内存,只是建立个映射链接,等发生访问缺页异常,缺页异常陷入内核,分配物理地址空间,并将可执行文件内容装载到该内存页中,与用户态虚拟地址建立映射关系,也就是填充页表。

找到所需的内容在可执行文件中的位置,将指令寄存器设置为可执行文件入口,然后把控制权交换给进程,启动运行。

mm_struct结构描述了一个进程的整个虚拟地址空间。较高层次的结构vm_area_truct描述了虚拟地址空间的一个区间(简称虚拟区)

每个进程有自己的3g用户空间,1g的内核空间是共享的

task_struct(也就是pcb)和mm_struct的联系

task_struct就是通过这两个成员和mm_struct联系的,每个mm_sturct也有自己的一个页表

为什么一些进程给了很少资源却可以正常运行,因为局部性的原理,访问过的地方很可能再次访问,所以可以很少的资源给一个大程序却可以正常的运行

当操作系统内存不够时,可以通过系统释放内存或者手动释放内存

手动释放内存

echo 1 > /proc/sys/vm/drop_caches --释放网页缓存 echo 2 > /proc/sys/vm/drop_caches --释放目录项和索引 echo 3 > /proc/sys/vm/drop_caches --释放网页缓存,目录项和索引

当提示权限不足时,可以通过以下命令

sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches

sync用来同步pagecahe中的脏数据,tee命令

小结

进程空间中代码段,数据段,动态链接库(共享文件映射),mmap 共享匿名映射都存在于 cache 中,但是这些内存页都有被进程引用,所以是不能释放的,基于 tmpfs 的 ipc 进程间通信机制的生命周期是随内核,因此也是不能通过 drop_caches 释放。

虽然上述提及的cache不能释放,但是后面有提到,当内存不足时,这些内存是可以 swap out 的。(交换区的功能)

因此 drop_caches 能释放的就是当从磁盘读取文件时的缓存页以及某个进程将某个文件映射到内存之后,进程退出,这时映射文件的的缓存页如果没有被引用,也是可以被释放的。

文件映射:代码段,数据段,动态链接库共享存储段以及用户程序的文件映射段;

匿名映射:bbs段,堆,以及当 malloc 用 mmap 分配的内存,还有mmap共享内存段;

linux 内核自动回收内存原理,内核有一个 kswapd (内核守护线程)会周期性的检查内存使用情况,如果发现空闲内存(空闲页数)定于 pages_low,则 kswapd 会对 lru_list 前四个 lru 队列进行扫描,在活跃链表中查找不活跃的页,并添加不活跃链表。

然后再遍历不活跃链表,逐个进行回收释放出32个页,知道 free page 数量达到 pages_high,针对不同的页,回收方式也不一样。

文件页:

如果是脏页,则直接回写进磁盘,再回收内存。

如果不是脏页,则直接释放回收。

匿名页: 因为匿名页没有回写的地方,如果释放掉,那么就找不到数据了,所以匿名页的回收是采取 swap out 到磁盘,并在页表项做个标记,下次缺页异常在从磁盘 swap in 进内存。

swap 换进换出其实是很占用系统IO的,如果系统内存需求突然间迅速增长,那么cpu 将被io占用,系统会卡死,导致不能对外提供服务,因此系统提供一个参数,用于设置当进行内存回收时,执行回收 cache 和 swap 匿名页的,这个参数为:

意思就是说这个值越高,越可能使用 swap 的方式回收内存,最大值为100,如果设为0,则尽可能使用回收 cache 的方式释放内存。

创建用户动态内存

有malloc和calloc两种方法,申请成功都会返回ptr指针,指向申请的空间的起始地址

void *malloc(size_t size);

void *calloc(size_t nmemb, size_t size);

mallloc以字节为单位,callloc以nmemb字节为单位

calloc调用成功时,会把申请的空间内所有位置零

void free(void *ptr);

用来释放malloc或calloc申请的动态空间,释放后调用ptr会发生错误

void*realloc(void *ptr, size_t size);

用来调整已申请的空间的内存大小

如果动态申请内存后不及时清理,会发生内存泄漏,一直存在堆内存中,没有返回内存给系统,造成可用内存的减小,就会造成内存泄漏

在内核空间申请动态内存

用kmalloc或vmalloc

void *kmalloc(size_t size, gfp_t flags);

   kmalloc() 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。

较常用的 flags(分配内存的方法):

(1)GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;

(2)GFP_KERNEL —— 正常分配内存;

(3)GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。

内核内存释放函数

void kfree(const void *objp);

高端物理地址的分配采用vmalloc/vfree这组函数进行。

如果这部分内存有一部分不能直接映射到地址空间,那么这部分虚拟地址空间称为高端内存

由于直接内存映射区(3GB ~ 3GB+896MB)是直接映射到物理地址(0 ~ 896MB)的

void *vmalloc(unsigned long size);是用来申请大块内存的,申请的虚拟空间是连续的,但物理空间不一定连续

vmalloc() 函数的实现代码如下:

static inline void * vmalloc(unsigned long size)

{

return vmalloc(size, GFP_KERNEL|GFP_HIGHMEM, PAGE_KERNEL);

}

从上面代码可以看出,vmalloc() 函数直接调用了 __vmalloc() 函数

__vmalloc() 函数主要工作有两点:

(1)调用 get_vm_area() 函数申请一个合法的虚拟内存地址。

(2)调用 vmalloc_area_pages() 函数把虚拟内存地址映射到物理内存地址。

内存释放函数vfree原型:

void vfree(const void *addr);

参数addr指向要释放的内存地址。

注意:vmalloc() 和 vfree() 可以睡眠,因此不能从中断上下文调用。

mmap映射文件到内存

mmap就是把文件映射到进程的用户空间同时创建一个虚拟区

内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。

进程之间通过的共享内存时,共享内存时只需要拷贝两次内存,从文件到内存,从内存到文件。共享内存中的内容一般只有解除映射才会写回文件

对于共享内存时的缺页异常,首先在swap-cache中找,没有则到swap-area,最后才是导入物理内存写入页表

对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页 面,并返回相应地址,同时,进程页表也会更新。

mmap的出现使得进程对文件的访问和访问内存一样,不用通过write,read等操作

具有亲缘关系的进程一般用匿名内存映射来进行进程间的通信,就像父子进程,子进程和父进程共同维护映射空间

Linux下两个特殊文件

dev/zero 读取它的时候会提供无限的空字符,实际上产生连续不断的null流,写入它的输出会丢失不见,永远输出0的设备文件,可以用来创造新文件或者用来覆盖旧文件

dd if=/dev/zero of= fileblock count =10 bs =1024

和dev/null -空设备 它会丢弃一且写入它的数据,读取它会立即得到一个EOF

虚拟内存管理

创建新进程时,系统调用clone或Vfork,子进程和父进程会共享虚拟内存空间,这样子进程就变成了我们的线程

我们调用 fork() 函数创建进程的时候,表示进程地址空间的 mm_struct 结构会随着进程描述符 task_struct 的创建而创建。

copy_process 函数中创建 task_struct 结构,可以看出子进程在新创建出来之后它的虚拟内存空间是和父进程的虚拟内存空间一模一样的,直接拷贝过来

进程的核心数据结构 task_struct,进程的内存空间描述符 mm_struct,以及虚拟内存区域描述符 vm_area_struct

编译后的二进制文件映射到虚拟内存空间

elf格式

磁盘文件中的段我们叫做 Section,内存中的段我们叫做 Segment,也就是内存区域。

通过load_elf_binary函数实现

加载内核,启用第一个用户态进程init,解析完elf文件格式进行内存映射都通过这个函数实现

static int load_elf_binary(struct linux_binprm *bprm) { ...... 省略 ........ *// 设置虚拟内存空间中的内存映射区域起始地址 mmap_base* setup_new_exec(bprm);

...... 省略 ........ *// 创建并初始化栈对应的 vm_area_struct 结构。* *// 设置 mm->start_stack 就是栈的起始地址也就是栈底,并将 mm->arg_start 是指向栈底的。* retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack);

...... 省略 ........ *// 将二进制文件中的代码部分映射到虚拟内存空间中* error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);

...... 省略 ........ *// 创建并初始化堆对应的的 vm_area_struct 结构* *// 设置 current->mm->start_brk = current->mm->brk,设置堆的起始地址 start_brk,结束地址 brk。 起初两者相等表示堆是空的* retval = set_brk(elf_bss, elf_brk, bss_prot);

...... 省略 ........ *// 将进程依赖的动态链接库 .so 文件映射到虚拟内存空间中的内存映射区域* elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata);

...... 省略 ........ *// 初始化内存描述符 mm_struct* current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p;

...... 省略 ........ }

内核虚拟空间内存

内核态虚拟内存空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。

在总共大小 1G 的内核虚拟内存空间中,位于最前边有一块 896M 大小的区域,我们称之为直接映射区或者线性映射区,地址范围为 3G -- 3G + 896m 。这896M的区域与物理地址一一对应,而且是连续的

前 1M 已经在系统启动的时候被系统占用,例如loader,mbr这一类的。1M 之后的物理内存存放的是内核代码段,数据段,BSS 段(这些信息起初存放在 ELF格式的二进制文件中,在系统启动的时候被加载进内存)。

内核会为每个进程分配一个固定大小的内核栈(一般是两个页大小,依赖具体的体系结构),每个进程的整个调用链必须放在自己的内核栈中,内核栈也是分配在直接映射区。内核栈大小是固定的,

高端内存

本例中我们的物理内存假设为 4G,高端内存区域为 4G - 896M = 3200M,那么这块 3200M 大小的 ZONE_HIGHMEM 区域该如何映射到内核虚拟内存空间中呢?

而物理内存 896M 以上的区域被内核划分为 ZONE_HIGHMEM 区域,我们称之为高端内存。

由于内核虚拟内存空间中的前 896M 虚拟内存已经被直接映射区所占用,而在 32 体系结构下内核虚拟内存空间总共也就 1G 的大小,这样一来内核剩余可用的虚拟内存空间就变为了 1G - 896M = 128M。

剩下的3200M不能全部映射,只能分批的动态映射,运行完就解除映射

永久映射区可以建立内核与高端内存的长期映射

固定映射区,可以自由映射到高端物理地址,但内核的虚拟内存地址是固定的

kmalloc和vmalloc

kmalloc() 用于申请较小的、连续的物理内存,kmalloc()所分配内核空间中的地址称为内核逻辑地址;

vmalloc() 用于申请较大的内存空间,虚拟内存是连续的,物理内存不一定连续,vmalloc()分配的内核空间中的地址称为内核虚拟地址

1

不,vmf-address 和 vma-start 不一定相同。虽然它们都与进程的虚拟内存布局有关,但目的不同,意义也不同。

vmf-address 指的是页面的虚拟地址,也就是页面在进程虚拟地址空间中可见的地址。MMU(内存管理单元)使用该地址将页面帧映射到进程的虚拟地址空间。

另一方面,vma-start 指的是虚拟内存区域(VMA)的起始地址。该地址用于标识进程虚拟地址空间中 VMA 的起始地址。

虽然这两个地址之间可能会有一些重叠(例如,如果一个 VMA 跨多个页面),但它们通常并不相同。例如,如果一个 VMA 从虚拟地址 0x1000 开始并跨越四个页面帧,那么每个页面帧都将有一个与其在 VMA 中的位置相对应的 vmf-address 地址(如 0x1000、0x1010、0x1020、0x1030)。不过,vma-start 地址仍为 0x1000,表示虚拟地址空间中 VMA 的起始地址。

MMU工作原理

负责将虚拟地址转换和翻译成物理地址的一个硬件模块

mmu包含tlb快表和twu页表遍历模式

TLB未命中时用TWU来查询页表

向内核申请空间就是申请内核分配虚拟区间和若干物理页面,并建立映射关系

fork创建进程时对父进程用户空间的创建是对mm_struct架构的复制,包括vma和页目录和页表的建立

exit会销毁进程的用户空间和所有虚拟区,可执行文件存储在磁盘中,只有当程序用到malloc函数,虚拟空间才会出现堆虚拟区

伙伴算法来解决页面分配与回收的问题,

空闲的页块会加入到数组free_area的相应链表中

get_free_page内核函数用到伙伴算法

满足以下三个条件的称为伙伴:

块指内存块

1)两个块大小相同; 2)两个块地址连续; 3)两个块必须是同一个大块中分离出来的;

伙伴算法分配原理

假如系统需要4(2x2)个页面大小的内存块,该算法就到free_area[2]中查找,如果链表中有空闲块,就直接从中摘下并分配出去。如果没有,算法将顺着数组向上查找free_area[3],如果free_area[3]中有空闲块,则将其从链表中摘下,分成等大小的两部分,前四个页面作为一个块插入free_area[2],后4个页面分配出去,free_area[3]中也没有,就再向上查找,如果free_area[4]中有,就将这16(2x2x2x2)个页面等分成两份,前一半挂如free_area[3]的链表头部,后一半的8个页等分成两等分,前一半挂free_area[2]的链表中,后一半分配出去。假如free_area[4]也没有,则重复上面的过程,知道到达free_area数组的最后,如果还没有则放弃分配。

页框分配函数

当分配的块比原先申请的多,使用分裂函数expand来处理多余的内存块

 在linux内核中,所有的物理内存都用struct page结构来描述,这些对象以数组形式存放,而这个数组的地址就是mem_map。

伙伴算法的释放原理

内存的释放是分配的逆过程,也可以看作是伙伴的合并过程。当释放一个块时,先在其对应的链表中考查是否有伙伴存在,如果没有伙伴块,就直接把要释放的块挂入链表头;如果有,则从链表中摘下伙伴,合并成一个大块,然后继续考察合并后的块在更大一级链表中是否有伙伴存在,直到不能合并或者已经合并到了最大的块(222222222个页面)。

slab分配机制

每种对象的高速缓存是由若干个slab组成,每个slab由若干个页框组成

虽然slab分配器可以分配比单个页框更小的内存块,但它所需的所有内存都是通过伙伴算法分配的。每个对象可以视为一个数据结构

当然!当进程使用 mmap 系统调用创建一个新的虚拟内存区时,内核需要分配物理内存以支持该虚拟内存区。为此,内核会使用 do_vma 函数查找合适的物理内存块。该函数会检查空闲页面列表,查看是否有足够的连续内存来满足请求。如果没有,它将尝试通过拆分现有页面或从其他进程回收未使用的内存来分配更多内存。物理内存分配完毕后,内核会更新页表和其他数据结构,以反映新的映射。

页面的换出

进程的代码段和全局量都在用户空间,所占的内存页面都是动态的,使用前要经过分配,最后都会被释放,中途可能被换出而回收后另行分配。

内核在执行过程中使用的页面要经过动态分配,但永驻内存 。此类页面根据其内容和性质可以分为两类:

-内核调用kmalloc()或vmalloc()为内核中临时使用的数据结构而分配的页用完立即释放。但是,由于一个页面中存放有多个同种类型的数据结构,所以要到整个页面都空闲时才把该页面释放。

内核中通过调用get_free_pages为某些临时使用和管理目的而分配的页面,例如,每个进程的内核栈所占的两个页面、从内核空间复制参数时所使用的页面等等,这些页面也是一旦使用完毕便无保存价值,所以立即释放。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值