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


//接前一章,本节主要介绍线性区以及相关线性区的操作。

线性区

Linux通过类型为vm_area_struct的对象实现线性区。

vm_area_struct:

struct vm_area_struct {
	struct mm_struct * vm_mm;	/* The address space we belong to. */
	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next;

 	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
	unsigned long vm_flags;		/* Flags, listed below. */

 	struct rb_node vm_rb;

	union {
		 struct {
			struct list_head list;
			void *parent;	/* aligns with prio_tree_node parent */
			struct vm_area_struct *head;
		} vm_set;

		struct raw_prio_tree_node prio_tree_node;
 	} shared;

	struct list_head anon_vma_node;	/* Serialized by anon_vma->lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
 	struct vm_operations_struct * vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units, *not* PAGE_CACHE_SIZE */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */
	unsigned long vm_truncate_count;/* truncate_count or restart_addr */
};
vm_area_struct字段:

类型

字段

说明

struct mm_struct *

vm_mm

指向线性区所在的内存描述符

unsigned long

vm_start

线性区内的第一个线性地址

unsigned long

vm_end

线性区后的第一个线性地址

struct vm_area_struct *

vm_next

进程拥有的线性区链表中的下一个线性区

pgprot_t

vm_page_prot

线性区中页框的访问许可权

unsigned long

vm_flags

线性区的标志

struct rb_node

vm_rb

用于红黑树的数据

union

shared

链接到反映射所使用的数据结构

struct list_head

anon_vma_node

指向匿名线性区链表的指针

struct anon_vma *

anon_vma

指向anon_vma数据结构的指针

struct vm_operation_struct *

vm_ops

指向线性区的方法

unsigned long

vm_pgoff

在映射文件中的偏移量

struct file *

vm_file

指向映射文件的文件对象

void *

vm_private_data

指向内存区的私有数据

unsigned long

vm_truncate_count

释放非线性文件内存映射中的一个线性地址区间时使用

每个线性区描述符表示一个线性地址区间。

vm_start字段指向线性区的第一个线性地址,而vm_end字段指向线性区之后的第一个线性地址。

vm_end - vm_start表示线性区的长度。

vm_mm字段指向拥有这个区间的进程的mm_struct内存描述符。

mmap_cache字段保存进程最后一次引用线性区的描述符地址,引用此字段可减少查找一个给定线性地址所在线性区花费的时间。

进程所拥有的线性区从来不重叠,并且内核尽力把新分配的线性区与紧邻的现有线性区进行合并。两个相邻区的访问权限如果相匹配,就能把它们合并在一起。

增加或删除一个线性区,如图:

\

vm_ops字段指向vm_operations_struct数据结构,该结构中存放的是线性区的方法。

*作用于线性区的方法:(可应用于UMA系统)

方法

说明

open

当把线性区增加到进程所拥有的线性区集合时调用

close

当从进程所拥有的线性区集合删除线性区时调用

nopage

当进程试图访问RAM中不存在的页,但该页的线性地址属于线性区时,由缺页异常处理程序调用

populate

设置线性区的线性地址所对应的页表项时调用。

线性区数据结构:

进程所拥有的所有线性区是通过一个简单的链表连接在一起的。每个vm_area_struct元素的vm_next字段指向链表的下一个元素。

内核通过进程的内存描述符的nmap字段来查找线性区,其中nmap字段指向链表中的第一个线性区描述符。

内存描述符中的map_count字段存放进程所拥有的线性区数目。默认情况下,一个进程可以最多拥有65536个不同的线性区。

进程地址空间、内存描述符和线性区链表之间的关系:

\

红黑树存储内存描述符的原因:

内核频繁执行查找包含指定线性地址的线性区。用链表进行查找操作其时间复杂度为O(N),而用红黑树则其时间复杂度为O(logN)。可见,在N很大时,用红黑树能极大地节省查找时间。

 

为了存放进程的线性区,Linux既使用了链表,也使用了红黑树。这两种数据结构包含指向同一线性区描述符的指针,当插入或删除一个线性区描述符时,内核通过红黑树搜索前后元素,并用搜索结果快速更新链表而不用扫描链表。

链表:链表的头由内存描述符的nmap字段所指向。任何线性区对象都在vm_next字段存放指向链表下一个元素的指针。

红黑树:红黑树的首部由内存描述符中的mm_rb字段所指向。任何线性区对象都在类型为rb_node的vm_rb字段中存放节点颜色以及指向双亲、左孩子和右孩子的指针。

struct rb_node
{
	struct rb_node *rb_parent;
	int rb_color;
#define	RB_RED		0
#define	RB_BLACK	1
 	struct rb_node *rb_right;
	struct rb_node *rb_left;
};
一般来说,红黑树用来确定含有指定地址的线性区,而链表通常在需要扫描整个线性区集合时来使用。

 

线性区访问权限:

每个线性区都是由一组号码连续的页所构成。

与页有关的标志:

1.每个页表项中存放的标志,如Read/Write、Present和User/Supervisor.

——80x86硬件用来检查能否执行所请求的寻址类型;

2.每个页描述符flags字段中的一组标志。

——Linux用于许多不同的目的;

3.与线性区的页相关的那些标志。存放在vm_area_struct描述符的vm_flags字段中。

——部分标志将线性区页的信息提供给内核,另外的用来描述线性区自身特性。

/* vm_flags */
#define VM_READ		0x00000001	/* currently active flags */
#define VM_WRITE	0x00000002
#define VM_EXEC		0x00000004
#define VM_SHARED	0x00000008

#define VM_MAYREAD	0x00000010	/* limits for mprotect() etc */
#define VM_MAYWRITE	0x00000020
#define VM_MAYEXEC	0x00000040
#define VM_MAYSHARE	0x00000080

#define VM_GROWSDOWN	0x00000100	/* general info on the segment */
#define VM_GROWSUP	0x00000200
#define VM_SHM		0x00000400	/* shared memory area, don't swap out */
#define VM_DENYWRITE	0x00000800	/* ETXTBSY on write attempts.. */

#define VM_EXECUTABLE	0x00001000
#define VM_LOCKED	0x00002000
#define VM_IO           0x00004000	/* Memory mapped I/O or similar */

					/* Used by sys_madvise() */
#define VM_SEQ_READ	0x00008000	/* App will access data sequentially */
#define VM_RAND_READ	0x00010000	/* App will not benefit from clustered reads */

#define VM_DONTCOPY	0x00020000      /* Do not copy this vma on fork */
#define VM_DONTEXPAND	0x00040000	/* Cannot expand with mremap() */
#define VM_RESERVED	0x00080000	/* Don't unmap it from swap_out */
#define VM_ACCOUNT	0x00100000	/* Is a VM accounted object */
#define VM_HUGETLB	0x00400000	/* Huge TLB Page VM */
#define VM_NONLINEAR	0x00800000	/* Is non-linear (remap_file_pages) */
线性区描述符所包含的页访问权限可以任意组合。

*** 页访问权限表示何种类型的访问应该产生一个缺页异常。

页表标志的初值存放在vm_area_struct描述符的vm_page_prot字段中。当增加一个页时,内核根据vm_page_prot字段的值设置相应页表项中的标志。

如果内核没有被编译成支持PAE,那么Linux采取以下规则以克服80x86微处理器的硬件限制:

*读访问权限总是隐含着执行访问权限,反之亦然。

*写访问权限总是隐含着读访问权限。

反之,如果内核被编译成支持PAE,而且CPU有NX标志,Linux就采取不同的规则:

*执行访问权限总是隐含着读访问权限。

*写访问权限总是隐含着读访问权限。

===>>>

NX位(全名“No eXecute bit”,即“禁止执行位”),是应用在CPU中的一种安全技术。支持NX技术的系统会把内存中的区域分类为只供存储处理器指令集与只供存储数据使用的两种。任何标记了NX位的区块代表仅供存储数据使用而不是存储处理器的指令集,处理器将不会将此处的数据作为代码执行,以此这种技术可防止大多数的缓存溢出式攻击。

读+写+执行+共享访问权限有16种可能组合,可根据规则进行精简。

1.为什么是16种?——排列组合

2.精简规则有哪些?

==>>

*如果页具有写和共享两种访问权限,则Read/Write位置位;

*如果页具有读或执行访问权限,但没有写和共享访问权限,则Read/Write位清零;

*如果CPU支持NX位,且页没有执行访问权限,则NX位置位;

*如果页没有任何访问权限,则Present位清零以产生缺页异常。同时,为了区分真正的页框不存在的情况,Linux还把Page Size位置位。

访问权限的每种组合所对应的精简后的保护位存放在protection _map数组的16个元素中。

线性区的处理:

操作函数

说明

find_vma()

查找给定地址的最邻近区

find_vma_intersection()

查找一个给定的地址区间相重叠的线性区

get_unmapped_area()

查找一个空闲的地址空间

insert_vm_struct

在内存描述符表中插入一个线性区

do_mmap()

分配线性地址空间

do_munmap()

释放线性地址空间

spilt_vma()

把与线性地址区间交叉的线性区划分成两个较小的区,一个在线性地址区间外部,另一个在区间内部。

unmap_region()

遍历线性区链表并释放它们的页框

 

1.find_vma()——查找给定地址的最邻近区

/* Look up the first VMA which satisfies  addr < vm_end,  NULL if none. */
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
	struct vm_area_struct *vma = NULL;

	if (mm) {
		/* Check the cache first. */
		/* (Cache hit rate is typically around 35%.) */
		vma = mm->mmap_cache;
 	        if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
			struct rb_node * rb_node;

			rb_node = mm->mm_rb.rb_node;
			vma = NULL;

			while (rb_node) {
				struct vm_area_struct * vma_tmp;
				vma_tmp = rb_entry(rb_node,
						struct vm_area_struct, vm_rb);

				if (vma_tmp->vm_end > addr) {
					vma = vma_tmp;
					if (vma_tmp->vm_start <= addr)
						break;
					rb_node = rb_node->rb_left;
				} else
					rb_node = rb_node->rb_right;
			}
			if (vma)
				mm->mmap_cache = vma;
		}
 	}
 	return vma;
}
目的:查找线性区的vm_end字段大于addr的第一个线性区的位置(addr不一定在该线性区中),并返回这个线性区描述符的地址;如果没有这样的线性区存在,就返回NULL指针。

参数:进程内存描述符地址mm和线性地址addr。

步骤:

*检查mmap_cache所指定的线性区是否包含addr。如果是,返回此线性区描述符的指针。

*否则,扫描进程线性区,在红黑树中查找线性区。如果找到,返回线性区描述符指针;否则,返回NULL指针。

其中,函数使用宏rb_entry,从指向红黑树中一个节点的指针导出相应线性区描述符的地址。

拓展:函数find_vma_prev()和find_vma_prepare()。

find_vma_prev():把函数选中的前一个线性区描述符的指针赋给附加参数*pprev。

find_vma_prev(struct mm_struct *mm, 
             unsigned long addr, 
             struct vm_area_struct **pprev)
find_vma_prepare():确定新叶子节点在与给定线性地址对应的红黑树中的位置,并返回前一个线性区描述符的地址和要插入的叶子节点的父节点地址。
static struct vm_area_struct *
find_vma_prepare(struct mm_struct *mm, unsigned long addr,
		struct vm_area_struct **pprev, struct rb_node ***rb_link,
		struct rb_node ** rb_parent)
{
	struct vm_area_struct * vma;
	struct rb_node ** __rb_link, * __rb_parent, * rb_prev;
	__rb_link = &mm->mm_rb.rb_node;
	rb_prev = __rb_parent = NULL;
	vma = NULL;
        while (*__rb_link) {
		struct vm_area_struct *vma_tmp;

		__rb_parent = *__rb_link;
		vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);

		if (vma_tmp->vm_end > addr) {
			vma = vma_tmp;
			if (vma_tmp->vm_start <= addr)
				return vma;
			__rb_link = &__rb_parent->rb_left;
		} else {
			rb_prev = __rb_parent;
			__rb_link = &__rb_parent->rb_right;
		}
	}

	*pprev = NULL;
	if (rb_prev)
		*pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
	*rb_link = __rb_link;
	*rb_parent = __rb_parent;
	return vma;
}

2.find_vma_intersection()——查找一个给定的地址区间相重叠的线性区

static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
/*线性地址strat_addr和end_addr指定线性区地址区间*/
{
	struct vm_area_struct * vma = find_vma(mm,start_addr);
	if (vma && end_addr <= vma->vm_start)
		vma = NULL;
	return vma;
}
目的:查找一个给定的地址区间相重叠的线性区。

参数:mm参数指向进程的内存描述符,线性地址start_addr和end_addr指定线性区地址区间。

步骤:

*在mm内存描述符中的线性区中,调用find_vma()函数找到vm_end字段大于start_addr的第一个线性区,并将返回值存放在vma指针中。

*如果vma指针不为NULL,检查end_addr与vm_start,如果end_addr不大于vm_start,则说明整个由start_addr和end_addr界定的线性区间不属于vma,返回NULL;否则,返回vma。

3.get_unmapped_area()——查找一个空闲的地址空间

unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
                             unsigned long pgoff, unsigned long flags)
{
	 if (flags & MAP_FIXED) {
		unsigned long ret;

 		if (addr > TASK_SIZE - len)
			return -ENOMEM;
		if (addr & ~PAGE_MASK)
			return -EINVAL;

		if (file && is_file_hugepages(file))  {
                        ret = prepare_hugepage_range(addr, len);
		} else {
		        ret = is_hugepage_only_range(addr, len);
	}
		if (ret)
			return -EINVAL;
		return addr;
	}

	if (file && file->f_op && file->f_op->get_unmapped_area)
		return file->f_op->get_unmapped_area(file, addr, len, pgoff, flags);

	return current->mm->get_unmapped_area(file, addr, len, pgoff, flags);
}
解析:如果参数addr不等于NULL,函数检查所指定的地址是否在用户态空间并与页边界对齐。接下来,函数根据线性地址区间是否应该用于文件内存映射或匿名内存映射,调用两个方法(get_unmapped_area文件操作(访问文件)和内存描述符的get_unmapped_area方法)中的一个。

对于内存描述符的get_unmapped_area方法,由函数arch_get_unmapped_area()或arch_get_unmapped_area_topdown()实现get_unmapped_area方法。通过系统调用mmap()创建新的内存映射,每个进程都可能获得两种不同形式的线性区:一种从线性地址0x4000000开始向高端地址增长,另一种从用户态堆栈开始向低端地址增长。

arch_get_unmapped_area():

unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,
		unsigned long len, unsigned long pgoff, unsigned long flags)
{	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma;
	unsigned long start_addr;
        if (len > TASK_SIZE)
		return -ENOMEM;

	if (addr) {
        /* in file /source/include/asm-i386/page.h :  */
        /* to align the pointer to the (next) page boundary */
        /* #define PAGE_ALIGN(addr)   (((addr)+PAGE_SIZE-1)&PAGE_MASK)*/
		addr = PAGE_ALIGN(addr);
        /* Equal to  “addr = (addr + 0xfff) & 0xfffff000”  */
		vma = find_vma(mm, addr);
		if (TASK_SIZE - len >= addr &&
		    (!vma || addr + len <= vma->vm_start))
			return addr;
	}
	start_addr = addr = mm->free_area_cache;

full_search:
	for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
		/* At this point:  (!vma || addr < vma->vm_end). */
		if (TASK_SIZE - len < addr) {
            /* #define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3))*/
			if (start_addr != TASK_UNMAPPED_BASE) {
				start_addr = addr = TASK_UNMAPPED_BASE;
				goto full_search;
			}
			return -ENOMEM;
		}
		if (!vma || addr + len <= vma->vm_start) {
			mm->free_area_cache = addr + len;
			return addr;
		}
		addr = vma->vm_end;
	}
}
目的:分配从低端地址向高端地址移动的线性区时使用的函数。

参数:len参数指定区间长度,addr参数指定必须从哪个地址开始进行查找。

步骤:

*函数检查区间长度是否在用户态下线性地址区间的限长TASK_SIZE(通常为3GB)之内。如果addr不为0,函数就试图从addr开始分配区间,同时把addr值调整为4KB的倍数。

*如果addr等于NULL或前面的搜索失败返回NULL,函数arch_get_unmapped_area()扫描用户态线性地址空间以查找一个可以包含新区的足够大的线性地址范围。如果函数找不到一个合适的线性地址范围,就从用户态地址空间的三分之一的开始处重新搜索,直到搜索到返回地址。

*更新free_area_cache字段。

4.insert_vm_struct——在内存描述符表中插入一个线性区

/* Insert vm structure into process list sorted by address and into the inode's i_mmap tree. 
 * If vm_file is non-NULL then i_mmap_lock is taken here. */
int insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)
{
	struct vm_area_struct * __vma, * prev;
	struct rb_node ** rb_link, * rb_parent;

	if (!vma->vm_file) {
		BUG_ON(vma->anon_vma);
		vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
	}
	__vma = find_vma_prepare(mm,vma->vm_start,&prev,&rb_link,&rb_parent);
	if (__vma && __vma->vm_start < vma->vm_end)
		return -ENOMEM;
        vma_link(mm, vma, prev, rb_link, rb_parent);
	return 0;
}
目的:向内存描述符链表中插入一个线性区。

参数:mm指定进程内存描述符地址,vmp指定要插入的vm_area_struct对象的地址。

步骤:

*函数调用find_vma_prepare()在红黑树mm->mm_rb中查找vma应该位于何处。

*insert_vm_struct()调用vma_link函数,执行以下操作:

在mm->mmap所指向的链表中插入线性区;在红黑树mm->mm_rb中插入线性区;如果线性区是匿名的,就将此线性区插入以相应anon_vma数据结构作为头结点的链表中;递增mm->map_count计数器。

函数vma_link()代码:

static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
			struct vm_area_struct *prev, struct rb_node **rb_link,
 			struct rb_node *rb_parent)
{
	struct address_space *mapping = NULL;
	if (vma->vm_file)
		 mapping = vma->vm_file->f_mapping;
	if (mapping) 
        {
		spin_lock(&mapping->i_mmap_lock);
		vma->vm_truncate_count = mapping->truncate_count;
        }
        anon_vma_lock(vma);
	__vma_link(mm, vma, prev, rb_link, rb_parent);
	__vma_link_file(vma);
	anon_vma_unlock(vma);
	if (mapping)
		spin_unlock(&mapping->i_mmap_lock);
	mm->map_count++;
        validate_mm(mm);
}

5.do_mmap()——分配线性地址区间

static inline unsigned long do_mmap(struct file *file, unsigned long addr,
	unsigned long len, unsigned long prot,
	unsigned long flag, unsigned long offset)
{
	unsigned long ret = -EINVAL;
	if ((offset + PAGE_ALIGN(len)) < offset)
		goto out;
	if (!(offset & ~PAGE_MASK))
		ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
	return ret;
}
目的:为当前进程创建并初始化一个新的线性区。不过,分配成功之后,可以把这个新的线性区与进程已有的其他线性区进行合并。

参数:

file和offset:如果新的线性区把一个文件映射到内存,使用文件描述符指针file和文件偏移量offset。

addr:指定从何处开始查找一个空闲的区间。

len:线性地址区间的长度。

prot:指定此线性区所包含的页的访问权限。

flag:指定线性区的其他标志。

步骤:

**do_mmap()函数对offset值进行初步检查。

**执行do_mmap_pgoff()函数:

*检查参数的值是否正确,所提的要求是否能被满足,尤其是要检查不能满足请求的条件;

*调用get_unmapped_area()获得新线性区的线性地址区间;

*通过把存放在prot和flags参数中的值进行组合来计算新线性区描述符的标志;

vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
		mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_LOCKED) {
	if (!can_do_mlock())
		return -EPERM;
	vm_flags |= VM_LOCKED;
}
*调用find_vma_prepare()确定处于新区间之前的线性区对象的位置,以及在红黑树中新线性区的位置;
munmap_back:
	vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
	if (vma && vma->vm_start < addr + len) {
		if (do_munmap(mm, addr, len))
			return -ENOMEM;
		goto munmap_back;
	}
*检查插入新的线性区是否引起进程地址空间的大小超过存放在进程描述符signal->rlim[RLIMIT_AS].rlim_cur字段中的阈值。如果是,返回出错码-ENOMEM;
/* Check against address space limit. */
	if ((mm->total_vm << PAGE_SHIFT) + len > current->signal->rlim[RLIMIT_AS].rlim_cur)
		return -ENOMEM;
*如果在flags参数中没有设置MAP_NORESERVE标志,新的线性区包含私有可写项,并没有足够的空闲页框,则返回出错码-ENOMEM。此过程由security_vm_enough_memory()函数实现;
         if (accountable && (!(flags & MAP_NORESERVE) ||
			    sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
		if (vm_flags & VM_SHARED) {
			vm_flags |= VM_ACCOUNT;
		} else if (vm_flags & VM_WRITE) {
			charged = len >> PAGE_SHIFT;
			if (security_vm_enough_memory(charged))
				return -ENOMEM;
			vm_flags |= VM_ACCOUNT;
		}
	}
*如果新区间是私有的(VM_SHARED未被设置),且映射的不是磁盘上的文件,则调用vma_merge()检查前一个线性区是否可以以这样的方式进行扩展以包含新的区间(线性区的扩展);
       if (!file && !(vm_flags & VM_SHARED) &&
	     vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, NULL, pgoff, NULL))
		goto out;
*调用slab分配函数kmem_cache_alloc()为新的线性区分配一个vm_area_struct数据结构;
        vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
	if (!vma) {
		error = -ENOMEM;
		goto unacct_error;
	}
*初始化新的线性区对象(由vma指向);
	memset(vma, 0, sizeof(*vma));	
        vma->vm_mm = mm;
	vma->vm_start = addr;
	vma->vm_end = addr + len;
 	vma->vm_flags = vm_flags;
	vma->vm_page_prot = protection_map[vm_flags & 0x0f];
	vma->vm_pgoff = pgoff;
*如果VM_SHARED标志被设置(以及新的线性区不映射磁盘上的文件),则该线性区是一个共享区:调用shmem_zero_setup()对它进行初始化工作。共享匿名主要用于进程间通信;
	if (file) {
		error = -EINVAL;
		if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
			goto free_vma;
		if (vm_flags & VM_DENYWRITE) {
			error = deny_write_access(file);
			if (error)
				goto free_vma;
			correct_wcount = 1;
		}
		vma->vm_file = file;
		get_file(file);
		error = file->f_op->mmap(file, vma);
		if (error)
			goto unmap_and_free_vma;
	} 
    else if (vm_flags & VM_SHARED) {
		error = shmem_zero_setup(vma);
		if (error)
			goto free_vma;
	}
*调用vma_link()把新线性区插入到线性区链表和红黑树中;
if (!file || !vma_merge(mm, prev, addr, vma->vm_end,
 		vma->vm_flags, NULL, file, pgoff, vma_policy(vma))) {
		file = vma->vm_file;
		vma_link(mm, vma, prev, rb_link, rb_parent);
		if (correct_wcount)
			atomic_inc(&inode->i_writecount);
} 
*增加存放在内存描述符total_vm字段中的进程地址空间的大小;
mm->total_vm += len >> PAGE_SHIFT;
*如果设置了VM_LOCKED标志,调用make_pages_present()连续分配线性区的所有页,并把它们锁在RAM中;
	if (vm_flags & VM_LOCKED) {
		mm->locked_vm += len >> PAGE_SHIFT;
		make_pages_present(addr, addr + len);
	}
*返回新线性区的线性地址。 

6.do_munmap()——从当前进程的地址空间中删除一个线性地址区间

/* Munmap is split into 2 main parts -- this part which finds
 * what needs doing, and the areas themselves, which do the
 * work.  This now handles partial unmappings.
 * Jeremy Fitzhardinge <jeremy@goop.org>
 */
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
	unsigned long end;
	struct vm_area_struct *mpnt, *prev, *last;

	if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
		return -EINVAL;

	if ((len = PAGE_ALIGN(len)) == 0)
		return -EINVAL;

 	/* Find the first overlapping VMA */
	mpnt = find_vma_prev(mm, start, &prev);
	if (!mpnt)
		return 0;
	/* we have  start < mpnt->vm_end  */

	/* if it doesn't overlap, we have nothing.. */
	end = start + len;
	if (mpnt->vm_start >= end)
		return 0;

	if (start > mpnt->vm_start) {
		int error = split_vma(mm, mpnt, start, 0);
		if (error)
			return error;
		prev = mpnt;
	}

	/* Does it split the last one? */
	last = find_vma(mm, end);
	if (last && end > last->vm_start) {
		int error = split_vma(mm, last, end, 1);
		if (error)
			return error;
	}
	mpnt = prev? prev->vm_next: mm->mmap;

	/* Remove the vma's, and unmap the actual pages */
	detach_vmas_to_be_unmapped(mm, mpnt, prev, end);
	spin_lock(&mm->page_table_lock);
	unmap_region(mm, mpnt, prev, start, end);
	spin_unlock(&mm->page_table_lock);

	/* Fix up all other VM information */
	unmap_vma_list(mm, mpnt);

	return 0;
}
目的:从当前进程的地址空间中删除一个线性地址区间。

参数:进程内存描述符的地址mm,地址区间的起始地址start和长度len。

步骤:

*对参数值进行初步检查:如果线性地址区间所含的地址大于TASK_SIZE,如果start不是4096的倍数,或者如果线性地址区间的长度为0,则函数返回一个错误代码-EINVAL;

*调用函数find_vma_prev()确定要删除的线性地址区间之后第一个线性区mpnt的位置;

*如果没有这样的线性区,也没有与线性地址区间重叠的线性区,就什么都不做,因为在该区间上没有线性区;

*如果线性区的起始地址在线性区mpnt内,调用split_vma()把线性区mpnt划分为两个较小的区:一个区在线性地址区间外部,另一个区在线性地址区间内部;

*如果线性地址区间的结束地址在另一个线性区内部,就再次调用split_vma()把最后重叠的那个线性区划分为两个较小的区:一个区在线性地址区间内部,另一个区在线性地址区间外部;

*更新mpnt的值,是它指向线性地址区间的第一个线性区。如果prev为NULL,即没有上述线性区,就从mm->mmap获得第一个线性区的地址;

*调用detach_vmas_to_be_unmapped()从进程的线性地址空间中删除位于线性地址区间中的线性区;

/*
 * Create a list of vma's touched by the unmap, removing them from the mm's
 * vma list as we go..
 */
static void detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma,
	struct vm_area_struct *prev, unsigned long end)
{
	struct vm_area_struct **insertion_point;
	struct vm_area_struct *tail_vma = NULL;

	insertion_point = (prev ? &prev->vm_next : &mm->mmap);
	do {
		rb_erase(&vma->vm_rb, &mm->mm_rb);
		mm->map_count--;
		tail_vma = vma;
		vma = vma->vm_next;
	} while (vma && vma->vm_start < end);
	*insertion_point = vma;
	tail_vma->vm_next = NULL;
	mm->mmap_cache = NULL;		/* Kill the cache. */
}
*获得mm->page_table_lock()自旋锁;

*调用unmap_region()清除与线性地址区间对应的页表项并释放相应的页框;

*释放mm->page_table_lock()自旋锁;

*调用unmap_vma_list()方法释放在detach_vmas_to_be_unmapped()时建立链表时收集的线性区描述符;

static void unmap_vma_list(struct mm_struct *mm,
	struct vm_area_struct *mpnt)
{
	do {
		struct vm_area_struct *next = mpnt->vm_next;
		unmap_vma(mm, mpnt);
		mpnt = next;
	} while (mpnt != NULL);
	validate_mm(mm);
}
*返回0(成功)。

7.spilt_vma()

/*
 * Split a vma into two pieces at address 'addr', a new vma is allocated
 * either for the first part or the the tail.
 */
int split_vma(struct mm_struct * mm, struct vm_area_struct * vma,
	      unsigned long addr, int new_below)
{
	struct mempolicy *pol;
	struct vm_area_struct *new;

	if (is_vm_hugetlb_page(vma) && (addr & ~HPAGE_MASK))
		return -EINVAL;

	if (mm->map_count >= sysctl_max_map_count)
		return -ENOMEM;

	new = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
	if (!new)
		return -ENOMEM;

	/* most fields are the same, copy all, and then fixup */
	*new = *vma;

	if (new_below)
		new->vm_end = addr;
	else {
		new->vm_start = addr;
		new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT);
	}

	pol = mpol_copy(vma_policy(vma));
	if (IS_ERR(pol)) {
		kmem_cache_free(vm_area_cachep, new);
		return PTR_ERR(pol);
	}
	vma_set_policy(new, pol);

	if (new->vm_file)
		get_file(new->vm_file);

	if (new->vm_ops && new->vm_ops->open)
		new->vm_ops->open(new);

	if (new_below)
		vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff +
			      ((addr - new->vm_start) >> PAGE_SHIFT), new);
	else
		vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);

	return 0;
}
目的:把与线性地址区间交叉的线性区划分为两个较小的区,一个在线性地址区间外部,另一个在区间内部。

参数:内存描述符指针mm,线性区描述符vma(标识要被划分的线性区),表示区间与线性区间之间交叉点的地址addr,以及表示区间与线性区间之间交叉点在区间起始处还是结束处的标志new_below。

步骤:

*调用kmem_cache_alloc()获得线性区描述符vm_area_struct,并把它的地址存在新的局部变量中,如果没有可用的空闲空间,返回-ENOMEM;

*用vma描述符的字段值初始化新描述符的字段;

*如果标志new_below为0,说明线性地址区间的起始地址在vma线性区的内部,因此必须把新线性区放在vma线性区之后,函数把new->vm_end字段赋值为addr。相反,如果标志new_below为1,说明线性地址区间的结束在vma线性区的内部,因此必须把新线性区放在vma线性区之前,函数把new->vm_start字段都赋值为addr;

*如果定义了新线性区的open方法,函数就执行它;

*通过vma_adjust()函数将新线性区描述符链接到线性区链表和红黑树;

*返回0(成功)。

8.unmap_region()

/*
 * Get rid of page table information in the indicated region.
 * Called with the page table lock held.
 */
static void unmap_region(struct mm_struct *mm,
	struct vm_area_struct *vma,
	struct vm_area_struct *prev,
	unsigned long start,
	unsigned long end)
{
	struct mmu_gather *tlb;
	unsigned long nr_accounted = 0;

	lru_add_drain();
	tlb = tlb_gather_mmu(mm, 0);
	unmap_vmas(&tlb, mm, vma, start, end, &nr_accounted, NULL);
	vm_unacct_memory(nr_accounted);

	if (is_hugepage_only_range(start, end - start))
		hugetlb_free_pgtables(tlb, prev, start, end);
	else
		free_pgtables(tlb, prev, start, end);
	tlb_finish_mmu(tlb, start, end);
}
目的:遍历线性区链表并释放它们的页框。

参数:内存描述符mm,指向第一个被删除线性区描述符的指针vma,指向进程链表中vma前面的线性区的指针prev,以及两个地址start和end,它们界定被删除线性地址区间的范围。

步骤:

*调用lru_add_drain()函数;

*调用tlb_gather_mmu()函数初始化每CPU变量mmu_gathers;

*把mmu_gathers变量的地址保存在局部变量tlb中;

*调用unmap_vmas()扫描线性地址空间的所有页表项:如果只有一个有效CPU,函数就调用free_swap_and_cache()反复释放相应的页;否则,函数就把相应页描述符的指针保存在局部变量mmu_gathers中;

*调用free_pgtables()回收上一步已清空的进程页表;

*调用tlb_finish_mmu()函数结束unmap_region()函数的工作。

相关内容