作者简介:马宜萱,西安邮电大学计算机专业研二学生,热衷于探索linux内核。
虚拟内存的管理以进程为单位,每个进程都有一个虚拟地址空间。进程的task_struct都有mm_struct类型的mm,用来代表进程的虚拟地址空间。在虚拟地址空间中,每段已经分出去的虚拟内存区域用VMA表示,结构体为vm_area_struct。当进程申请内存的时候,申请到的是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;
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等系统调用实现。
- 将依赖的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;
}
进程启动后,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_merge(mm, prev, addr, addr + len, flags,
NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma)
goto out;
vma = vm_area_alloc(mm);
vma->vm_start = addr;
vma->vm_end = addr + len;
......
}
#include
#include
#include
#include
int main()
{
void *curr_brk, *tmp_brk = NULL;
tmp_brk = curr_brk = sbrk(0);
getchar();
brk(curr_brk+4096);
curr_brk = sbrk(0);
getchar();
brk(tmp_brk);
curr_brk = sbrk(0);
getchar();
return 0;
}
cat /proc/3454/maps
5556dc96e000-5556dc98f000 rw-p 00000000 00:00 0 [heap]
cat /proc/3454/maps
5556dc96e000-5556dc990000 rw-p 00000000 00:00 0 [heap]
cat /proc/3454/maps
5556dc96e000-5556dc98f000 rw-p 00000000 00:00 0 [heap]
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对象上。如果栈内存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:- 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);
}
用户态内存资源,包含线程栈,用户资源的数据结构是struct pthread,每个pthread对应唯一一个线程。struct pthread
{
pid_t tid;
void *stackblock;
size_t stackblock_size;
}
static int
allocate_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;
}
static int
allocate_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));
}
申请到内存后,mem指针指向新内存的低地址,通过mem和size算出高地址,先把struct pthread放进去。线程栈内存有两个用途,存储struct pthread和真正的线程栈内存。
在create_thread调用do_clone系统调用创建线程。创建的进程和线程,都生成task_struct对象。对于线程,创建的时候使用了flag,所以内核在创建task_struct时不在申请mm_struct、fs_struct、打开文件列表files_struct,新线程的这些都和创建它的任务共享。应用开发者和内核间有一个内存分配器,允许应用开发者随时申请和释放各种大小内存。 static struct malloc_state main_arena =
{
.mutex = _LIBC_LOCK_INITIALIZER,
.next = &main_arena,
.attached_threads = 1
};
struct malloc_state
{
__libc_lock_define (, mutex);
struct malloc_state *next;
};
在分配区中需要有一个锁来应对多线程申请内存时的竞争问题。内存分配的基本单位是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。根据内存块的大小,分成fastbins、smallbins、largebins和unsortedbins四类。struct malloc_state {
mfastbinptr fastbinsY[NFASTBINS];
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2 - 2];
unsigned int binmap[BINMAPSIZE];
};
fastbins成员定义的是尺寸最小元素的链表。每个链表管理的chunk元素大小分别是32字节、48字节……128字节。 fastbin_index函数可以快速找到申请的内存大小对应的fastbins数组下标。 #define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
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管理范围。 #define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)
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)
不固定的内存块大小,用来做缓冲区。释放堆块后,会先进入unsortedbins,再次分配时,优先检查这个链表中有没有合适的堆块。 当所有空闲链表都申请不到合适大小的时候,会到这里申请。static void *
_int_malloc (mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb;
nb = checked_request2size (bytes);
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) {
......
}
if (in_smallbin_range (nb)) {
......
}
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);
}