专栏名称: Linux内核之旅
Linux内核之旅
目录
相关文章推荐
Linux内核之旅  ·  从ELF文件到Linux进程 ·  3 天前  
Linux内核之旅  ·  进程如何使用内存 学习笔记 ·  5 天前  
Linux内核之旅  ·  公平CFS调度类:SCHED_NORMAL、 ... ·  2 周前  
Linux内核之旅  ·  公平调度类的延伸:EEVDF ·  2 周前  
Linux内核之旅  ·  eBPF Talk: tailcall 问题知多少 ·  1 周前  
51好读  ›  专栏  ›  Linux内核之旅

进程如何使用内存 学习笔记

Linux内核之旅  · 公众号  · linux  · 2025-01-09 11:57

正文

作者简介:马宜萱,西安邮电大学计算机专业研二学生,热衷于探索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,也包括各级页表项。
struct vm_fault {  const struct {    struct vm_area_struct *vma; /* 缺页 VMA */    unsigned long 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对象。
  unsigned long mmap_region(struct file *file, unsigned long addr,      unsigned long len, vm_flags_t vm_flags, unsigned long 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对象。
  static int do_brk_flags(unsigned long addr, unsigned long len, unsigned long 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。
static int do_brk_flags(unsigned long addr, unsigned long len, unsigned long flags, struct list_head *uf){  struct mm_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;

// 申请新的vma vma = vm_area_alloc(mm); vma->vm_start = addr; vma->vm_end = addr + len; ......}
一个使用了brk系统调用的例子:
#include #include #include #include 
int main(){ 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();
return 0;}
可以看到brk调用执行后的变化:
cat /proc/3454/maps5556dc96e000-5556dc98f000 rw-p 00000000 00:00 0                          [heap]
cat /proc/3454/maps5556dc96e000-5556dc990000 rw-p 00000000 00:00 0 [heap]
cat /proc/3454/maps5556dc96e000-5556dc98f000 rw-p 00000000 00:00 0 [heap]
三、进程栈内存的使用
1.进程栈的初始化

static int bprm_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对站的虚拟空间进行补充。
int expand_stack(struct vm_area_struct *vma, unsigned long address){  return expand_downwards(vma, address);}int expand_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, const pthread_attr_t *attr,          void *(*start_routine) (void *), void *arg){    struct pthread *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对应唯一一个线程。
struct pthread{    pid_t tid;        void *stackblock;    size_t stackblock_size;}
  • tid对象存储了线程ID值
  • stackblock指向线程栈内存
  • stackblock_size栈内存大小
2.确定栈空间大小
static intallocate_stack (const struct 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.申请用户栈
static intallocate_stack (const struct pthread_attr *attr, struct pthread **pdp,    void **stack, size_t *stacksize){    struct pthread *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)分配区
使用分配区管理从操作系统申请的内存。
  •  用静态变量的方式定义全局的主分配区
  static struct malloc_state main_arena =  {    .mutex = _LIBC_LOCK_INITIALIZER,    .next = &main_arena,    .attached_threads = 1  };
  • 分配区的数据类型malloc_state
  struct malloc_state  {    __libc_lock_define (, mutex);    struct malloc_state *next;  };
  在分配区中需要有一个锁来应对多线程申请内存时的竞争问题。
 

(2)内存块
内存分配的基本单位是malloc_chunk,包含header和body两部分。
struct malloc_chunk {

INTERNAL_SIZE_T mchunk_prev_size; /* 上一个内存块的大小(如果为空闲块时有效)。*/ INTERNAL_SIZE_T mchunk_size; /* 当前内存块的大小(包含管理开销的字节数)。*/

struct malloc_chunk* fd; /* 双向链表的前向指针,仅在空闲时使用。*/ struct malloc_chunk* bk; /* 双向链表的后向指针,仅在空闲时使用。*/

/* 仅用于大块内存:指向下一个较大内存块的指针。*/ struct malloc_chunk* fd_nextsize; /* 双向链表的前向指针,仅在空闲时使用。*/ struct malloc_chunk* bk_nextsize; /* 双向链表的后向指针,仅在空闲时使用。*/};
用malloc申请内存时,分配器会分配大小合适的chunk,把body的user data地址返回。

使用free释放内存,chunk对象会由glibc管理起来,body的fd和bk指向前后空闲的chunk。
(3)空闲内存块链表
根据内存块的大小,分成fastbins、smallbins、largebins和unsortedbins四类。
struct malloc_state {
/* 快速分配区(fastbins) */ mfastbinptr fastbinsY[NFASTBINS]; /* 位于堆顶的最大内存块的起始地址,当所有空闲链表都申请不到合适大小的时候,会到这里申请 */ mchunkptr top; /* 最近一次小块请求的剩余部分 */ mchunkptr last_remainder; /* 常规 bin,按特定结构进行打包 */ mchunkptr bins[NBINS * 2 - 2]; /* bin 的位图,用于快速检查 bin 状态 */ unsigned int binmap[BINMAPSIZE];};
1. fastbins
fastbins成员定义的是尺寸最小元素的链表。每个链表管理的chunk元素大小分别是32字节、48字节……128字节。
 fastbin_index函数可以快速找到申请的内存大小对应的fastbins数组下标。
   #define fastbin_index(sz) \     ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
2.smallbins
smallbins由bin成员管理,两个相邻的smallbin中chunk大小相差16字节,管理的内存块大小是32字节、48字节……1008字节。
   #define in_smallbin_range(sz)  \     ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
只要小于MIN_LARGE_SIZE都属于smallbins管理范围。
快速计算smallbins下标的函数:
   #define smallbin_index(sz) \     ((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\      + SMALLBIN_CORRECTION)
3.largebins
largebins管理的内存从1024字节开始,两个相邻的largebins之间内存块大小不是固定的等差数列,Largebin_index_64函数计算largebins下标。
   #define largebin_index_64(sz)                                                \     (((((unsigned long) (sz)) >> 6) <= 48) ?  48 + (((unsigned long) (sz)) >> 6) :\      ((((unsigned long) (sz)) >> 9) <= 20) ?  91 + (((unsigned long) (sz)) >> 9) :\      ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\      ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\      ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\      126)
4. unsortedbins
不固定的内存块大小,用来做缓冲区。释放堆块后,会先进入unsortedbins,再次分配时,优先检查这个链表中有没有合适的堆块。
5. top chunk
   当所有空闲链表都申请不到合适大小的时候,会到这里申请。
2.malloc内存分配过程
内存申请核心是__int_malloc。
static void *_int_malloc (mstate av, size_t bytes){    INTERNAL_SIZE_T nb;               /* 归一化的请求大小 */    nb = checked_request2size (bytes);  /* 检查并计算合适的分配大小 */
/* 如果请求大小在 fastbin 范围内,尝试从 fastbin 分配 */ if ((unsigned long) (nb) <= (unsigned long) (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等系统调用来申请内存。
static void *sysmalloc (INTERNAL_SIZE_T nb, mstate av){    mm = sysmalloc_mmap (nb, mp_.hp_pagesize, mp_.hp_flags, av);}


推荐文章
Linux内核之旅  ·  从ELF文件到Linux进程
3 天前
Linux内核之旅  ·  进程如何使用内存 学习笔记
5 天前
Linux内核之旅  ·  公平调度类的延伸:EEVDF
2 周前
Linux内核之旅  ·  eBPF Talk: tailcall 问题知多少
1 周前
书法在线  ·  无法重来的一生 (写得真好)
7 年前
光电与显示  ·  TPK所期待的王者归来,OLED外挂TP
7 年前