Linux内核剖析之进程地址空间(一)


绪论

内核获取内存方式——直接了当:

1. 从分区页框分配器获取内存(__get_free_pages()或alloc_pages());

2. 使用slab分配器为专用或通用对象分配内存(kmem_cache_alloc()或kmalloc());

3. 使用vmalloc或vmalloc_32获取一块非连续内存区。

如果申请的内存得以满足,这些函数返回一个页描述符地址或线性地址。

*内核申请内存使用这些简单方法基于以下两个原因:

1.内核是操作系统中优先级最高的成分。如果内核申请动态内存,那么必定有正当理由。因此,没有理由推迟处理这个请求。

2.内核信任自己。所有内核函数都是假定没有错误的,因此内核函数不必插入针对编程错误的任何保护措施。

*当给用户态进程分配内存时,情况完全不同:

1.进程对动态内存的请求被认为是不紧迫的。内核总是尽量推迟给用户态进程分配动态内存。

2.用户进程是不可信任的,因此,内核必须能随时准备捕捉用户进程引起的所有寻址错误。

主要内容:

*内核如何实现对进程动态内存的推迟分配?

——使用一种新的资源(线性地址区)

当用户进程请求动态内存时,并没有获得请求的页框,而是获得了一个新的线性地址的使用权。这线性地址区域就成为进程地址空间的一部分。

*为什么要推迟分配?

*进程怎样看待动态内存。

*进程地址空间的基本组成。

*缺页异常处理程序在推迟给进程分配页框中所起的作用。

*内核怎样创建和删除进程的整个地址空间?

*与进程的地址空间管理有关的API和系统调用。

====>>>>>

1. 进程地址空间

2. 内存描述符

3. 线性区

4. 缺页异常处理程序

5. 创建和删除进程的地址空间

6. 堆的管理

进程地址空间

进程的地址空间(Address Space)由允许进程使用的全部线性地址组成。一个进程使用的进程地址空间与另一个进程使用的进程地址空间之间没有关系。如果进程之间共享相同的地址空间,则被称为线程。

内核可以通过增加或删除某些线性地址区间来动态修改进程的地址空间。

内核通过所谓线性区的资源来表示线性地址空间,线性地址空间由起始线性地址、长度以及相应访问权限来描述。

起始地址和长度都是4096(一页)的整数倍——提高效率。

*进程创建新的线性区的典型情况(六种):

程序执行

exec()函数

缺页异常处理程序

内存映射

IPC共享内存

malloc()函数—heap

*与创建、删除线性区相关的系统调用:

系统调用

说明

brk()

改变进程堆的大小

execve()

装入可执行文件,从而改变进程的地址空间

_exit()

结束当前进程并撤销它的进程地址空间

fork()

创建一个新的进程,并创建新的地址空间。

mmap(),mmap2()

为文件创建一个内存映射,从而扩大进程的地址空间

mremap()

扩大或缩小线性区

remap_file_pages()

为文件创建非线性映射

munmap()

撤销对文件的内存映射,从而缩小进程的地址空间

shmat()

创建一个共享线性区

shmdt()

撤销一个共享线性区

确定进程所拥有的线性地址区是内核的任务,这使得缺页处理程序能够有效的处理以下两种无效的线性地址:

1. 由编程错误引发的非法地址访问(如数组越界、非法指针);

2. 由缺页(物理页)引发的无效线性地址,即使这个线性地址属于进程地址空间,但是内核还未分配物理页。——请求调页。

内存描述符

与进程地址空间的全部信息都包含在一个叫做内存描述符(Memory Descriptor)的数据结构中,这个结构的类型是mm_struct,进程描述符的mm字段指向此结构。

无论是内核线程还是用户进程,对于内核来说,都是task_struct这个数据结构的一个实例,task_struct被称为进程描述符(process descriptor),因为它记录了这个进程所有的上下文(context)。其中有一个被称为“内存描述符”(memory descriptor)的数据结构 mm_struct,该结构抽象并描述了Linux视角下管理进程地址空间的所有信息。

\

[start_code,end_code)表示代码段的地址空间范围。

[start_data,end_data)表示数据段的地址空间范围。

[start_brk,brk)分别表示heap段的起始空间和当前的heap指针。

[start_stack,end_stack)表示stack段的地址空间范围。

mmap_base表示memory mapping段的起始地址。

具体结构图:

\

内存描述符:

mm_strcut定义:

struct mm_struct {
	struct vm_area_struct * mmap;		/* list of VMAs */
	struct rb_root mm_rb;
	struct vm_area_struct * mmap_cache;	/* last find_vma result */
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
	void (*unmap_area) (struct vm_area_struct *area);
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long free_area_cache;		/* first hole */
	pgd_t * pgd;
	atomic_t mm_users;			/* How many users with user space */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	int map_count;				/* number of VMAs */
	struct rw_semaphore mmap_sem;
	spinlock_t page_table_lock;		/* Protects page tables, mm->rss, mm->anon_rss */

	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 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 rss, anon_rss, total_vm, locked_vm, shared_vm;
	unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;

	unsigned long saved_auxv[42]; /* for /proc/PID/auxv */

	unsigned dumpable:1;
	cpumask_t cpu_vm_mask;

	/* Architecture-specific MM context */
	mm_context_t context;

	/* Token based thrashing protection. */
	unsigned long swap_token_time;
	char recent_pagein;

	/* coredumping support */
	int core_waiters;
	struct completion *core_startup_done, core_done;

	/* aio bits */
	rwlock_t		ioctx_list_lock;
	struct kioctx		*ioctx_list;

	struct kioctx		default_kioctx;

	unsigned long hiwater_rss;	/* High-water RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */
};
mm_struct字段:

类型

字段

说明

struct vm_area_struct*

mmap

指向线性区域对象的链表头

struct rb_root

mm_rb

指向线性区对象的红黑树的根

struct vm_area_struct*

mmap_cache

指向最后一个引用的线性区对象

unsigned long (*) ()

get_unmapped_area

在进程地址空间中搜索有效线性地址区

void (*) ()

unmap_area

释放线性地址区间时调用的方法

unsigned long

free_area_cache

内核从这个地址开始搜索进程地址空间中线性地址的空闲区域

pgd_t *

pdg

指向页全局目录

atomic_t

mm_users

次使用计数器

atomic_t

mm_count

主使用计数器

int

map_count

线性区的个数

struct rw_semaphore

mmap_sem

线性区的读/写信号量

spinlock_t

page_table_lock

线性区的自旋所和页表的自旋锁

struct list_head

mmlist

指向内存描述符链表中的相邻元素

unsigned long

start_code

可执行代码的起始地址

unsigned long

end_code

可执行代码的最后地址

unsigned long

start_data

已初始化数据的起始地址

unsigned long

end_data

已初始化数据的最后地址

unsigned long

start_brk

堆的起始地址

unsigned long

brk

堆的当前最后地址

unsigned long

start_stack

用户堆栈的起始地址

unsigned long

arg_start

命令行参数的起始地址

unsigned long

arg_end

命令行参数的最后地址

unsigned long

env_start

环境变量的起始地址

unsigned long

env_end

环境变量的最后地址

unsigned long

rss

分配给进程的页框数

unsigned long

anon_rss

非配给匿名内存映射的页框数

unsigned long

total_vm

进程地址空间的大小(页数)

unsigned long

locked_vm

锁住而不能换出的页的个数

unsigned long

shared_vm

共享文件内存映射中的页数

unsigned long

exec_vm

可执行内存映射中的页数

unsigned long

stack_vm

用户堆栈中的页数

unsigned long

reserved_vm

在保留区中的页数或在特殊线性区中的页数

unsigned long

def_flags

线性区默认的访问标志

unsigned long

nr_ptes

进程的页表数

unsigned long []

saved_auxv

开始执行ELF程序时使用

unsigned int

dumpable

表示是否可以产生内存转储信息的标志

cpumask_t

cpu_vm_mask

用于惰性TLB交换的位掩码

mm_context_t

context

指向有关特定体系结构信息的表

unsigned long

swap_token_time

进程在这个时间将有资格获得交换标志

char

recent_pagein

最近发生了主缺页,则设置该标志

int

core_waiters

正在把进程地址空间的内容转储到core文件中的轻量级进程的数目

struct completion *

core_startup_done

指向创建内存转储文件的补充原语

struct completion

core_done

指向创建内存转储文件的补充原语

rwlock_t

ioctx_list_lock

用于保护异步I/O上下文链表的锁

struct kioctx *

ioctx_list

异步I/O上下文链表

struct kioctx

default_kioctx

默认的异步I/O上下文

unsigned long

hiwater_rss

进程所拥有的最大页框数

unsigned long

hiwater_vm

进程线性区中的最大页数

所有的内存描述符存放在一个双向链表中。每个内存描述符在mmlist字段存放链表相邻元素的地址。链表的第一个元素是init_mm的mmlist字段,init_mm是初始化阶段进程0使用的内存描述符。

注意理解和比较两个字段:mm_users和mm_count字段。

# mm_users字段存放共享mm_struct数据结构的轻量级进程的个数。

# mm_count字段是内存描述符的主使用计数器,在mm_users次使用计数器中的所有用户在mm_count中只作为一个单元。每当mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内存描述符,因为不再有用户使用它。

===>为什么要设置mm_count字段?见下文。

内核线程的内存描述符

内核线程就能运行在内核态,因此,它们永远不会访问低于TASK_SIZE(等于PAGE_OFFSET)的地址。与普通进程相反,内核线程不用线性区。因此描述符的很多字段对内核线程是没有意义的。

内核线程使用前一个进程的内存描述符。

在每个进程描述符中包含了两种内存描述符指针:mm和active_mm。

进程描述符中的mm字段指向进程所拥有的内存描述符,而active_mm字段指向进程运行时所使用的内存描述符。对于普通进程,这两个字段存放相同的指针。但是,对于内核线程,由于内核线程不拥有任何内存描述符,因此,它们的mm字段总是NULL。当内核线程运行时,它的active_mm字段被初始化为前一个运行进程的active_mm值。

这里,可以解释上面mm_users字段和mm_count字段的区别:

<<<====>>>

mm_users字段记录共享该内存描述符的普通进程的个数,mm_count的目的在于支持内核线程级别。

对Linux来说,用户进程和内核线程(kernel thread)都是task_struct的实例(tsk),唯一的区别是内核线程是没有进程地址空间的,内核线程也没有mm内存描述符,所以内核线程的tsk->mm域(其中,struct task_struct * tsk)是空(NULL)。当内核scheduler在执行进程上下文切换(context switching)时,会根据tsk->mm判断即将调度的进程是用户进程还是内核线程。虽然内核线程不访问用户进程地址空间,但是仍然需要通过page table来访问内核线程的内核地址空间。幸运的是,对于任何用户进程来说,它们的内核空间都是完全相同的(3G-4G),所以内核可以“借用”上一个被调用的用户进程的内存描述符mm中的页表来访问内核地址,并将此mm记录在active_mm。简而言之,对于用户进程,tsk->mm == tsk->active_mm;而对于内核线程,tsk->mm == NULL表示自己内核线程的身份,而tsk->active_mm是使用上一个用户进程的mm,通过此mm的page table来访问内核空间。

为了支持内核线程级别,mm_struct里面引入了另外一个counter,主使用计数器mm_count。前面有mm_users表示这个进程地址空间被多少普通进程共享或者引用,而mm_count则表示这个地址空间被内核线程引用的次数+1(这里的1,指的是在mm_users次使用计数器中的所有用户在mm_count中只作为一个单元)。内核不会因为mm_users == 0而销毁此mm_struct,内核只会当mm_count == 0时才会释放mm_struct,因为这个时候既没有用户进程使用这个地址空间,也没有内核线程引用这个地址空间。

相关内容