菜鸟笔记
提升您的技术认知

进程地址空间

进程地址空间:

系统中每个每个用户空间进程所看到的内存
linux采用虚拟内存技术,所有进程之间以虚拟方式共享内存,即对于每个进程来说,可以访问整个系统的所有物理内存
即使一个单独进程,所拥有的地址空间远远大于系统物理内存

一、内存描述符:

内核使用描述符结构体表示进程的地址空间,包含了和进程地址空间有关的全部信息
结构体定义在文件<linux/sheched.h>
struct mm_struct
{
atomic_t mm_users; // 记录使用该地址的进程数目
atomic_t mm_count; // 主引用计数
struct vm_area_struct *mmap; // 地址空间的全部内存区域,以链表方式存放
struct rb_root mm_rb; // 地址空间的全部内存区域,以红黑树方式存放
sruct list_head mmlist // 包含全部mm_struct链表
active_mm
};
只要mm_users不为0,则mm_count为1
当mm_users减为0,则mm_count变为0,则此时说明没有任何指向该mm_struct的引用了
同时使用两个计数器,为了区别使用计数(mm_count)器和使用该地址空间的进程数目(mm_users)
mmap 作为链表,便于简单高效的遍历所有元素
mm_rb 作为红黑树,适合搜素指定元素
所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,链首元素为init_mm内存描述符,代表init进程的地址空间

1.1.1、分配内存描述符

进程的进程描述符中,mm域存放进程使用的内存描述符,所以,current->mm指向当前进程的内存描述符
fork()函数利用copy_mm()函数复制父进程的内存描述符,也就是current->mm域给子进程
而子进程中的mm_struct结构体实际是通过文件kemel/fork.c中的allocate_mm()宏从mm_cachep缓存中得到的
通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间
若父进程希望和子进程共享地址空间,则在调用clone()时,设置CLONE_VM标志,这样的进程称为线程
linux内核并不区别对待进程,线程,线程对内核来说仅仅是一个共享特定资源的进程,本质上的唯一区别是:是否共享地址空间
当CLONE_VM被指定后,内核不需要调用allocate_mm函数,只需要在调用copy_mm()函数中将mm域指向其父进程的内存描述符即可
if(colne_fiags&CLONE_VM
{
current_inc(*current->mm->mm_users);
tak->mm=current->mm;
}

1.1.2、销毁内存描述符

进程退出->内核调用exit_mm()函数(销毁同时更新统计量),
exit_mm()函数,调用mmput()函数减少内存描述符中mm_users用户计数
如果用户计数降到零,继续调用mmdrop()函数,减少mm_count使用计数
如果计数为0,则该内存描述符不再有任何使用者了,调用free_mm()宏通过kmen_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中

1.1.3、mm_struct和内核线程

内核线程没有进程地址空间,也没有相关的内存描述符,所以内核线程对应的进程描述符的m域为空,也就是内核线程的真是含义:没有用户上下文
内核线程并不需要访问任何用户空间的内存,且因为内核线程在用户空间没有任何页,所以不需有自己的内存描述符和页表,但在访问内核内存,内核线程还是需要使用一些数据,页表
当一个进程被调度时,该进程的mm域指向的地址空间被加载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。
内核线程没有地址空间,所以mm域为NULL,所以,当一个内核线程被调度时,发现他的mm域为NULL,就会保留前一个进程的地址空间,随后内核更新内核线程对应进程描述符中的active_mm域,使其指向前一个进程的内存描述符,所以在需要时,内核线程可使用前一个进程的页表,
因为内核线程不访问用户空间的内存,所以他们仅仅使用地址空间和内核内存相关的信息,而这些信息的含义和普通进程完全相同

二、内存区域

内存区域由vm_area_struct结构体描述,定义在头文件<linux/mm.h>中,在内核中,也称为虚拟内存区域或VMA
vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围。
内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,比如访问权限等,且操作都一致。
这种管理方式采用面向对象的方法使VMA结构体代表多种类型的区域,比如:内存映射文件或用户的用户空间栈等
struct vm_area_struct
{
}
vm_start域:指向区间的首地址(最低地址),即内存区间的开始位置(它本身在区间内)
vm_end域:指向区间的尾地址(最高地址)之后的第一个字节,即内存区间的结束位置(它本身在区间外)
则:vm_end-vm_start 的大小就是内存区间的长度,内存区域的位置在[vm_start,wm_end]之中,且同一地址空间内的不同内存区间不能重叠
vm_mm域指向和VMA相关的mm_struct结构体,每个VMA对于相关的结构体都是唯一的,所以即使两个独立的进程将同一个文件映射到不同的地址空间,那么他们同时共享其中所有vm_area_struct结构体

2.2.1、VMA标志

VMA标志是一种位标志,定义见<linux/mm.h>.包含在vm_flags域中,标志了内存区域所包含的页面的行为和信息
和物理页的访问权限不同,VMA标志反映了内核处理页所需要遵守的行为准则,而不是硬件要求,该标志同时包含了内存区域中页面的信息,或内存区域的整体信息
VM_READ、VM_WRITE、VM_EXEC 内存区域页面的读,写和执行权限,这些组合构成VMA的访问控制权限,访问VMA需查看其权限
VM_SHARED 指明内存区域包含的映射是否可在多进程间共享,若被设置,则称其为共享映射,若未被设置,则只有一个进程可以使用该映射的内容,称为私有映射
VM_IO标志 内存区域包含对设备I/O空间的映射,标志在设备驱动程序执行mmap()函数进行I/O空间映射时才被设置,该标志同时表示该内存不能被包含在任何进程的存放转存(coredump)中。
VM_REASERVED标志 内存区域不能被换出,也是在设备驱动程序进行映射时被设置
VM_SEQ_READ标志 暗示内核应用程序执行有序的(线性和连续的)读操作,VM_RAND_READ标志 真好相反,暗示内核应用程序执行随机的(非有序的)读操作,因此内核可以有意的减少或彻底取消文件预读,所以这两个标志可以通过系统调用madvise()设置,设置参数分别是MADV_SEQUENTIAL和MADV_RANDDOM.
文件预读是读数据时,有意按顺序多读一些本次请求以外的数据,希望多读的数据能够很快被需要,预读行为对顺序读取数据的应用程序有好处,但若数据访问是随机的,则显得多余了

2.2.2、VMA操作

vm_area_struct结构体中的vm_ops域指向域指定内存有关的的操作函数表,内核使用表中的方法操作vma;
vm_area_struct作为通用对象代表了任何类型的内存区域,而操作表描述针对特定的对象实例的特定方法
操作函数表由vm_operations_struct结构体表示,定义在<linux/mm.h>中。
struct vm_opreations_struct
{
void (*open)(struct vm_area_struct *);
void (*close)(struct vm_area_struct );
struct page
(*nopage)(struct vm_area_struct,unsigined long,int);
int(*populate)(struct vm_area_struct *,unsigned long,unsigned long,pgprot_t,unsigned long,int);
};
void (*open)(struct vm_area_struct *); 指定的内存区域被加入到一个地址空间时,函数调用
void (*close)(struct vm_area_struct ); 指定的内存区域从地址空间被删除时,函数调用
struct page
(*nopage)(struct vm_area_struct,unsigined long,int);访问的页不在物理内存时,函数被页错误处理程序调用
int(*populate)(struct vm_area_struct *,unsigned long,unsigned long,pgprot_t,unsigned long,int);函数被系统调用recmp_pages()调用来为将来要发生的却也中断映射一个新映射

2.2.3 内存区域的树形结构和内存区域的链表结构

可通过内存描述符的mmap域和mm_rb域之一访问内存,两个域各自独立指向与内存描述符相关的全体内存区域对象,他们包含相同的vm_area_struct结构体指针,仅仅是组织方式不同
mmap域使用单独链表连接所有的内存区域。每一个vm_area_struct结构体通过自身的vm_next域被连入链表,区域按照地址增长的方向排序,mmap域指向链表中的第一个内存区域,链中最后一个VMA结构体指针指向空
mm_rb域使用红——黑树连接所有的内存区域对象。每一个vm_rb域指向红——黑树的根节点,地址空间的每一个vm_area_struct结构体通过自身的vm_rb域连接到树中,
链表用于需要遍历全部节点的时候,而红-黑树适用于在地址空间中定位特定内存区域的时候,内核为了内存区域上的各种不停操作都能获得高性能,同时使用这两种数据结构

2.2.4 实际使用中的内存区域

可使用/ proc文件系统和pmap工具查看给定进程的内存空间和其中所含的内存区域。
数据段和bss段(都包含全局变量)可读,可写但不可执行权限,而堆栈则可读,可写,甚至可执行
共享不可写的方法节约了大量内存空间。
零页 映射文件的内存区域的设备标志位00:00,索引节点标志也为0,这个区域就是零页,零页映射的内容全为零,如果将零页映射到可写的内存区域,那么该区域将被初始化为0,bass段需要的就是全0的内存区域。
由于内存未被共享,所以只要一有进程写该处数据,那么该处数据就将被拷贝出来(即写时拷贝),然后才被更新
每个和进程相关的内存区域都对应一个vm_area_struct结构体,进程和线程不同,进程结构体task_struct包含唯一的mm_struct被引用

三、操作内存区域

内核时需要判断进程地址空间的内存区域是否满足一些条件,比如,某个地址是否包含在某个内存区域中

3.1 find_vma()

函数定义在文件<mm/mmap.c>,该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域,即寻找第一个包含addr或首地址大于addr的内存区域,若没有这样的区域,函数返回null;否则返回指向匹配的内存区域的vm_area_struct结构体指针
由于返回的VMA首地址可能大于addr,所以指定的地址并不一定就包含在返回的VMA中,由于在对VMA操作后,很有可能有其他对VMA操作员,所以find_vma()函数返回的结果被缓存在内存描述符的mmap_cache域中。
而且,被缓存的的VMA有较高的命中率,且检查被缓存的VMA速度很快,如果指定的地址不在缓存中,通过红黑树搜索和内存描述符相关的所有区域
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. /
/
*

  • 查找给定地址的最邻近区。
  • 它查找线性区的vm_end字段大于addr的第一个线性区的位置。并返回这个线性区描述符的地址。
  • 如果没有这样的线性区存在,就返回NULL。
  • 由find_vma函数所选择的线性区并不一定要包含addr,因为addr可能位于任何线性区之外。
  • mm-进程内存描述符地址
    
  • addr-线性地址。
    

*/
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%.) */
	/**
	 * mmap_cache指向最后一个引用的线性区对象。
	 * 引入这个附加的字段是为了减少查找一个给定线性地址所在线性区而花费的时间。
	 * 程序中引用地址的局部性使这种情况出现的可能性非常大:
	 *    如果检查的最后一个线性地址属于某一给定的线性区。那么下一个要检查的线性地址也属于这一个线性区。
	 */
	vma = mm->mmap_cache;
	/**
	 * 首先检查mmap_cache指定的线性区是否包含addr,如果是就返回这个线性区描述符的指针。
	 */
	 检查vm_end是否大于addr,不能保证该VMA是第一个大于addr的内存区域,若缓存区想发挥作用,必须要求指定地址在被缓存的VMA中,这也是连续操作同一VMA必然发生的情况
	if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
		/**
		 * 进入这里,说明mmap_cache中没有包含addr。就扫描进程的线性区。并在红黑树中查找线性区。
		 */
		struct rb_node * rb_node;

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

		while (rb_node) {
			struct vm_area_struct * vma_tmp;

			/**
			 * rb_entry从指向红黑树中一个节点的指针导出相应线性区描述符的指针。
			 */
			vma_tmp = rb_entry(rb_node,
					struct vm_area_struct, vm_rb);

			/**
			 * 视情况在左右子树中查找。
			 */
			if (vma_tmp->vm_end > addr) {
				vma = vma_tmp;
				/**
				 * 当前线性区包含addr,退出循环并返回vma.
				 */
				if (vma_tmp->vm_start <= addr)
					break;
				/**
				 * 否则在左子树中继续
				 */
				rb_node = rb_node->rb_left;
			} else/* 在右子树中继续 */
				rb_node = rb_node->rb_right;
		}
		/**
		 * 如果有必要,记录下mmap_cache。这样,下次就从mmap_cache继续查找。
		 */
		if (vma) 
			mm->mmap_cache = vma;
	}如果没有包含的VMA被找到,该函数继续搜素树 且返回大于addr的第一个VMA
}若不存在满足要求的VMA,函数返回NULL
return vma; 

}

3.2 find_vma_prev()

返回第一个小于addr的VMA,函数的定义和声明分别在mm/mmap.c和文件<linux.h>中。
find_vma_prev(struct mm_struct *mm, unsigned long addr,
struct vm_area_struct **pprev)
pprev参数存放指向先于addr的VMA指针

3.3 find_vma_intersection()

返回第一个和指定地址区间相交的VMA,函数是内联函数,定义在文件<linux/mm.h>
struct vm_area_struct *
find_vma_prev(struct mm_struct *mm,
unsigned long start_addr,
unsigned long end_addr)
{
struct vm_area_struct *vma;

vma=finf_vma(mm,struct_addr)
if(vma&&end_addr<=vma->vm_start)
vma=NULL;
return ma;	

}

第一个参数mm:要搜素的地址空间,start_addr:区间的开始首位置;end_addr;区间的尾位置}
则 若find_vma()返回NULL,find_vma_interesting()也返回NULL;
但若finf_vma()返回有效的VMA,find_vma_interesting()只有在该VMA的起始位置给定的地址空间结束位置之前,才将其返回
如果VMA的起始位置大于指定地址范围的结束位置,则函数返回NULL

四、 mmap()和do_mmap() 创建地址空间

如果创建的的地址区间和一个已经存在的地址区间相邻,并且它们都具有相同的访问权限,则两个区间合并为一个,如果不能合并,则需要创建一个新的VMA
do_mmap()函数会将一个地址区间加入到进程的地址空间中——无论是扩展已存在的区域还是创建一个新的区域
do_mmap()函数定义在<linux/mm.h>中。
unsigned long do_mmap(struct file * file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff)
函数映射由file指定的文件 映射从文件偏移offset处开始,长度为len字节的范围内的数据
若file参数是NULL并且offset参数也是0,则代表这次映射没有和文件相关,称作匿名映射
如果指明了指定了文件名的偏移量,则称为文件映射
aadr 是可选参数,指定搜索空闲区域的起始位置
prot参数 指定在内存区域中页面的访问权限 访问权限标志定义在文件<asm/mman.h>
不同体系结构标志的定义有所不同,flag参数指定了VMA标志,这些标志定义在<asm/mman.h>
如果系统调用do_mmap()的函数参数中有无效参数,那么它返回一个负值,否则会在虚拟内存中分配一个合适的新内存区域
若有可能,将新区和临近区域进行合并,否则内核从vm_area_cachep长字节缓存中分配一个vm_area_struct结构体,并且使用vm_link()函数将新分配的内存区域添加到地址空间的内存区域链表和红黑树中,随后更新内存描述符中的total_vm域,然后返回新分配的地址区间的初始地址

mmap()系统调用
用户空间通过mmap()系统调用获取内核do_mmap(),函数系统调用定义如下:
void *mmap2(void *start,size_t length,int prot,int flags,int fd,off_t pgoff)
系统调用的是mmap()调用的第二种,所以起名2,原始的最后一个参数为字节偏移量,而mmap2()使用页面偏移作最后一个参数
使用页面偏移量可映射更大的文件和更大的偏移位置,c库中中仍可使用原版映射,将字节偏移转化为页面偏移,对mmap2()调用

五、 mumap()和do_munmap() 删除地址空间

do_mumap() 从特定的地址空间中删除指定地址区间,函数定义在文件<linux/mm.h>中。
int do_munmap(struct mm_struct mm, unsigned long start, size_t len)
mm 指定要删除区域所在的地址空间,删除从地址start开始,长度为len字节的地址区间,若成功,返回0,否则,返回负的错误码
munmap()系统调用
给用户空间程序提供了一种从自身地址空间中删除指定区间的方法,和系统调用mmap()作用相反
int do_munmap(struct void
start, size_t length)
定义在文件mm/mmap中,是对do_munmap()的一个简单封装
asmlinkage long sys_munmap(unsigned long addr, size_t len)
{
int ret;
struct mm_struct *mm = current->mm;

profile_munmap(addr);

down_write(&mm->mmap_sem);
ret = do_munmap(mm, addr, len);
up_write(&mm->mmap_sem);
return ret;

}

六、页表

应用程序操作映射在物理内存上的虚拟内存,处理器操作物理内存
所以需要将虚拟地址转换为物理地址,然后处理器才能解析地址访问请求 通过查询二级或者三级页表转换