进程如何使用内存 学习笔记
作者简介:马宜萱,西安邮电大学计算机专业研二学生,热衷于探索linux内核。一、虚拟内存和物理页1.虚拟地址空间虚拟内存的管理以进程为单位,每个进程都有一个虚拟地址空间。进程的task_struct都有mm_struct类型的mm,用来代表进程的虚拟地址空间。在虚拟地址空间中,每段已经分出去的虚拟内存区域用VMA表示,结构体为vm_area_struct。2.缺页中断当进程申请内存的时候,申请到的是vm_area_struct,只是一段地址范围。不会立即分配,而是要等到实际访问的时候。等进程在运行中在栈上开始分配和访问变量的时候,如果物理页还没有分配,会触发缺页中断,分配真正的内存。用户态缺页中断的入口函数为do_user_addr_fault。在这个函数中调用find_vma找到地址所在vma对象。
找到正确的vma之后,do_user_addr_fault会依次调用handle_mm_fault->__handle_mm_fault来完成真正的物理内存申请。在__handle_mm_fault中,将参数统一到了一起vm_fault,包括缺页的内存地址address,也包括各级页表项。[*][*][*][*][*][*][*][*][*]structvm_fault {conststruct {structvm_area_struct *vma;/* 缺页 VMA */unsignedlong address; /* 缺页地址 */};pmd_t *pmd; /* 二级页表项 */pud_t *pud; /* 三级页表项 */pte_t *pte; /* 四级页表项 */};使用四级页表进行映射,在实际申请物理页面前,先检查一遍各级页表是否存在,不存在需要申请。申请好各级页表之后进入do_anonymous_page进行处理。在handle_pte_fault中会进行很多内存缺页处理,开发者申请的内存是匿名映射页处理,进入do_anonymous_page函数。do_anonymous_page函数调用alloc_zeroed_user_highpage_movable分配一个可移动的匿名物理页,在底层调用alloc_pages进行实际物理页面的分配。
二、虚拟内存使用方式整个进程的运行过程是对虚拟内存的分配和使用。使用方式包括几类:[*]操作系统加载程序时对虚拟内存进行设置和使用。- 程序启动时,将程序代码段、数据段通过mmap映射到虚拟地址空间。
- 对新进程初始化栈区和堆区。
[*]开发语言运行时通过new、malloc等从堆中分配内存。依赖操作系统提供的虚拟地址空间相关的mmap、brk等系统调用实现。1.进程启动时对虚拟内存的使用在解析完ELF文件后:[*]为进程创建地址空间,准备大小为4kb的栈。[*] 将依赖的so库通过elf_map映射到虚拟地址空间中。[*] 对进程堆区进行初始化。每一种内存使用方式,都是通过申请vm_area_struct来实现的:[*]对于栈,是在execve中依次调用do_execve_common、bprm_mm_init,最后在__bprm_mm_init中申请vm_area_struct对象。[*][*][*][*][*][*][*]static int __bprm_mm_init(struct linux_binprm *bprm){ struct mm_struct *mm = bprm->mm; bprm->vma = vma = vm_area_alloc(mm); vma->vm_end = STACK_TOP_MAX; vma->vm_start = vma->vm_end - PAGE_SIZE;}[*]对于可执行文件及进程所依赖的各种so动态链接库,是execve依次调用do_execve_common、search_binary_handler、load_elf_binary、elf_map,然后调用mmap_region申请vm_area_struct对象。[*][*][*][*][*][*][*][*][*]unsignedlongmmap_region(struct file *file, unsignedlong addr,unsignedlong len, vm_flags_t vm_flags, unsignedlong pgoff, struct list_head *uf){ vma = vm_area_alloc(mm); vma->vm_start = addr; vma->vm_end = addr + len;return addr;}[*] 对于堆内存,是在load_elf_binary的最后set_brk初始化堆时,依次调用vm_brk_flags、do_brk_flags,最后申请vm_area_struct对象。[*][*][*][*][*][*][*][*][*]staticintdo_brk_flags(unsignedlong addr, unsignedlong len, unsignedlong flags, struct list_head *uf){ vma = vm_area_alloc(mm); vma_set_anonymous(vma); vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_pgoff = pgoff; vma->vm_flags = flags;}2.sbrk和brk进程启动后,exec系统调用给进程初始化虚拟地址中的堆区,设置好sbrk和brk等指针。
sbrk和brk系统调用在sbrk和brk指针基础上工作,sbrk系统调用返回mm_struct->brk指针的值,brk系统调用是修改mm_struct->brk。函数是do_brk_flags。[*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*]staticintdo_brk_flags(unsignedlong addr, unsignedlong len, unsignedlong flags, struct list_head *uf){structmm_struct *mm = current->mm;// 在现有的vma上进行扩展vma = vma_merge(mm, prev, addr, addr + len, flags,NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);if (vma)goto out;
// 申请新的vmavma = vm_area_alloc(mm);vma->vm_start = addr;vma->vm_end = addr + len; ......}一个使用了brk系统调用的例子:[*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*]#include#include#include#include
intmain(){void *curr_brk, *tmp_brk = NULL;// sbrk(0) 获取当前 program break 位置 tmp_brk = curr_brk = sbrk(0);getchar();
// 使用 brk 增加 program break 位置 brk(curr_brk+4096);curr_brk = sbrk(0);getchar();
// 使用 brk 减小 program break 位置 brk(tmp_brk);curr_brk = sbrk(0);getchar();
return0;}可以看到brk调用执行后的变化:[*][*][*][*][*][*][*][*]cat /proc/3454/maps5556dc96e000-5556dc98f000 rw-p 0000000000:000
cat /proc/3454/maps5556dc96e000-5556dc990000 rw-p 0000000000:000
cat /proc/3454/maps5556dc96e000-5556dc98f000 rw-p 0000000000:000 三、进程栈内存的使用1.进程栈的初始化
[*][*][*][*][*]staticintbprm_mm_init(struct linux_binprm *bprm){bprm->mm = mm = mm_alloc();err = __bprm_mm_init(bprm);};申请完地址空间后,就给进程申请一页大小的虚拟地址空间,作为进程的栈内存,把栈的指针保存到bprm->p中。[*][*][*][*][*][*][*][*]static int __bprm_mm_init(struct linux_binprm *bprm){bprm->vma = vma = vm_area_alloc(mm);vma->vm_end = STACK_TOP_MAX;vma->vm_start = vma->vm_end - PAGE_SIZE;
bprm->p = vma->vm_end - sizeof(void *);}
接下来使用load_elf_binary加载可执行二进制程序,把准备的进程栈地址空间指针设置到新的进程mm对象上。2.栈的自动增长如果栈内存vma开始地址比要访问的address大,要调用expand_stack对站的虚拟空间进行补充。[*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*]intexpand_stack(struct vm_area_struct *vma, unsigned long address){return expand_downwards(vma, address);}intexpand_downwards(struct vm_area_struct *vma, unsigned long address){ ...// 计算栈扩大后的大小 size = vma->vm_end - address;// 计算需要扩展几个页面 grow = (vma->vm_start - address) >> PAGE_SHIFT;
...// 判断是否允许扩充 error = acct_stack_growth(vma, size, grow); ...// 开始扩充 vma->vm_start = address; ...}acct_stack_growth函数进行了一些限制判断,这些限制都能通过ulimit命令查看。
四、线程栈是如何使用内存的同一个进程下所有线程使用的都是同一块内存,各个线程栈区独立,每个线程在并行调用时在栈上独立的进栈和出栈。线程包含了两部分:[*]用户态glibc库:创建线程的pthread_create就是在glibc库中,glibc库完全是在用户态运行的。[*]内核态的clone系统调用:通过clone可以创建和父进程共享内存的用户进程。pthread_create函数调用__pthread_create_2_1:[*]定义线程对象指针[*]确定栈空间大小[*]allocate_stack申请用户栈内存[*]create_thread调用内核clone系统调用创建线程[*][*][*][*][*][*][*][*][*][*]int__pthread_create_2_1 (pthread_t *newthread, constpthread_attr_t *attr,void *(*start_routine) (void *), void *arg){structpthread *pd = NULL;int err = allocate_stack (iattr, &pd, &stackaddr, &stacksize);
retval = create_thread (pd, iattr, &stopped_start, stackaddr, stacksize, &thread_ran);}1.glibc线程对象用户态内存资源,包含线程栈,用户资源的数据结构是struct pthread,每个pthread对应唯一一个线程。[*][*][*][*][*][*][*]structpthread{pid_t tid;
void *stackblock;size_t stackblock_size;}[*]tid对象存储了线程ID值[*]stackblock指向线程栈内存[*]stackblock_size栈内存大小2.确定栈空间大小[*][*][*][*][*][*][*][*][*]staticintallocate_stack (conststruct pthread_attr *attr, struct pthread **pdp,void **stack, size_t *stacksize){if (attr->stacksize != 0) size = attr->stacksize;else size = __default_pthread_attr.internal.stacksize;}[*]确定栈空间大小[*]申请栈内存线程栈大小被限制在32MB以内。3.申请用户栈[*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*]staticintallocate_stack(const struct pthread_attr *attr, struct pthread **pdp,void **stack, size_t *stacksize){structpthread *pd;
pd = get_cached_stack (&size, &mem);if (pd == NULL){ mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); pd = (struct pthread *) ((((uintptr_t) mem + size - tls_static_size_for_stack) & ~tls_static_align_m1) - TLS_PRE_TCB_SIZE); pd->stackblock = mem; pd->stackblock_size = size; } __nptl_stack_list_add (&pd->list, &GL (dl_stack_used));}[*]get_cached_stack获取一段缓存[*]如果没取到缓存,使用mmap申请一段匿名空间[*]pthread对象先放到栈上[*]把栈添加到链表进行管理申请到内存后,mem指针指向新内存的低地址,通过mem和size算出高地址,先把struct pthread放进去。线程栈内存有两个用途,存储struct pthread和真正的线程栈内存。
4.创建线程在create_thread调用do_clone系统调用创建线程。创建的进程和线程,都生成task_struct对象。对于线程,创建的时候使用了flag,所以内核在创建task_struct时不在申请mm_struct、fs_struct、打开文件列表files_struct,新线程的这些都和创建它的任务共享。五、进程堆内存管理应用开发者和内核间有一个内存分配器,允许应用开发者随时申请和释放各种大小内存。1.内存分配器定义以glibc中ptmalloc内存分配器为例。(1)分配区使用分配区管理从操作系统申请的内存。[*] 用静态变量的方式定义全局的主分配区[*][*][*][*][*][*]staticstructmalloc_statemain_arena ={ .mutex = _LIBC_LOCK_INITIALIZER, .next = &main_arena, .attached_threads = 1};[*]分配区的数据类型malloc_state[*][*][*][*][*]structmalloc_state{ __libc_lock_define (, mutex);structmalloc_state *next;};在分配区中需要有一个锁来应对多线程申请内存时的竞争问题。
(2)内存块内存分配的基本单位是malloc_chunk,包含header和body两部分。[*][*][*][*][*][*][*][*][*][*][*][*][*][*][*]structmalloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size;/* 上一个内存块的大小(如果为空闲块时有效)。*/INTERNAL_SIZE_T mchunk_size; /* 当前内存块的大小(包含管理开销的字节数)。*/
structmalloc_chunk* fd;/* 双向链表的前向指针,仅在空闲时使用。*/structmalloc_chunk* bk;/* 双向链表的后向指针,仅在空闲时使用。*/
/* 仅用于大块内存:指向下一个较大内存块的指针。*/structmalloc_chunk* fd_nextsize;/* 双向链表的前向指针,仅在空闲时使用。*/structmalloc_chunk* bk_nextsize;/* 双向链表的后向指针,仅在空闲时使用。*/};用malloc申请内存时,分配器会分配大小合适的chunk,把body的user data地址返回。
使用free释放内存,chunk对象会由glibc管理起来,body的fd和bk指向前后空闲的chunk。(3)空闲内存块链表根据内存块的大小,分成fastbins、smallbins、largebins和unsortedbins四类。[*][*][*][*][*][*][*][*][*][*][*][*][*]structmalloc_state {
/* 快速分配区(fastbins) */mfastbinptr fastbinsY;/* 位于堆顶的最大内存块的起始地址,当所有空闲链表都申请不到合适大小的时候,会到这里申请 */mchunkptr top;/* 最近一次小块请求的剩余部分 */mchunkptr last_remainder;/* 常规 bin,按特定结构进行打包 */mchunkptr bins;/* bin 的位图,用于快速检查 bin 状态 */unsignedint binmap;};1. fastbinsfastbins成员定义的是尺寸最小元素的链表。每个链表管理的chunk元素大小分别是32字节、48字节……128字节。 fastbin_index函数可以快速找到申请的内存大小对应的fastbins数组下标。[*][*]#define fastbin_index(sz) \ ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)2.smallbinssmallbins由bin成员管理,两个相邻的smallbin中chunk大小相差16字节,管理的内存块大小是32字节、48字节……1008字节。[*][*]#define in_smallbin_range(sz)\ ((unsignedlong) (sz) 只要小于MIN_LARGE_SIZE都属于smallbins管理范围。快速计算smallbins下标的函数:[*][*][*]#define smallbin_index(sz) \ ((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\ + SMALLBIN_CORRECTION)3.largebinslargebins管理的内存从1024字节开始,两个相邻的largebins之间内存块大小不是固定的等差数列,Largebin_index_64函数计算largebins下标。[*][*][*][*][*][*][*]#define largebin_index_64(sz) \ (((((unsignedlong) (sz)) >> 6) > 6) :\ ((((unsignedlong) (sz)) >> 9) > 9) :\ ((((unsignedlong) (sz)) >> 12) > 12) :\ ((((unsignedlong) (sz)) >> 15) > 15) :\ ((((unsignedlong) (sz)) >> 18) > 18) :\126)4. unsortedbins不固定的内存块大小,用来做缓冲区。释放堆块后,会先进入unsortedbins,再次分配时,优先检查这个链表中有没有合适的堆块。5. top chunk 当所有空闲链表都申请不到合适大小的时候,会到这里申请。2.malloc内存分配过程内存申请核心是__int_malloc。[*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*]staticvoid *_int_malloc (mstate av, size_t bytes){ INTERNAL_SIZE_T nb; /* 归一化的请求大小 */ nb = checked_request2size (bytes);/* 检查并计算合适的分配大小 */
/* 如果请求大小在 fastbin 范围内,尝试从 fastbin 分配 */if ((unsignedlong) (nb) get_max_fast ())) { ...... }/* 如果请求大小在 smallbin 范围内,尝试从 smallbin 分配 */if (in_smallbin_range (nb)) { ...... }/* 循环查找unsortedbins */for (;; ) {/* 从无序链表中查找符合要求的内存块 */while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) {if (++iters >= MAX_ITERS)break; } use_top:/* 如果没有找到合适的块,从堆顶分配空间 */ victim = av->top; size = chunksize (victim);/* 获取堆顶块的大小 *//* 如果堆顶空间不足,调用系统分配更多内存 */void *p = sysmalloc (nb, av); }}
在sysmalloc中,是通过mmap等系统调用来申请内存。[*][*][*][*][*]staticvoid *sysmalloc(INTERNAL_SIZE_T nb, mstate av){ mm = sysmalloc_mmap (nb, mp_.hp_pagesize, mp_.hp_flags, av);}end
一口Linux
关注,回复【1024】海量Linux资料赠送
精彩文章合集
文章推荐
?【专辑】ARM?【专辑】粉丝问答?【专辑】所有原创?【专辑】linux入门?【专辑】计算机网络?【专辑】Linux驱动?【干货】嵌入式驱动工程师学习路线?【干货】Linux嵌入式所有知识点-思维导图
页:
[1]