1,
内核除了管理本身的内存外,还必须管理用户空间中进程的内存
我们称这个内存为进程地址空间,也就是系统中每个用户空间进程所看到的内存
进程地址空间由进程可寻址的虚拟内存组成,而且更为重要的特点是内核允许进程使用这种虚拟内存中的地址。
平坦:地址空间范围是一个独立的连续区间
段式:这些地址空间并非连续的,而是被分段的
通常情况下,每个进程都有唯一的这种平坦地址空间
2,
一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,实际上也彼此互不相干。我们称这样的进程为线程
内存地址是一个给定的值,它要在地址空间范围之内。
在地址空间,我们更关心的是一些虚拟内存空间,比如:
0x08048000 —— 0x0804c000
它们可被进程访问
这些可被访问的合法地址空间称之为内存区域
通过内核,进程可以给自己的地址空间动态地添加或减少内存区域
3,
内存区域可以包含各种内存对象:
·可执行文件代码的内存映射,称为代码段
·可执行文件的已初始化全局变量的内存映射,称为数据段
·包含未初始化全局变量,也就是bss段的零页的内存映射
·用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射
·每一个诸如C库或动态链接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间
·任何内存映射文件
·任何共享内存段
·任何匿名内存映射,比如由malloc()分配的内存
BSS Block started by symbol 。因为未初始化的变量没有对应值,所以不需要存放在可执行对象中。但是C标准强制规定未初始化的全局变量要被赋予特殊的默认值(基本上都是0),所以内核要将变量(未赋值的)从可执行代码载入到内存中,然后将零页映射到该片内存上,于是这些未初始化变量就被赋予了0值。这样就避免了在目标中显式的初始化,减少了空间浪费
最新版本的glibc中,通过mmap()和brk()来实现malloc()函数
4,
进程地址空间中的任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。可以看到,在执行的进程中,每个不同的内存片段都对应一个独立的内存区域:栈、对象代码、全局变量、被映射的文件等
5,
struct mm_struct
{
struct vm_area_struct *mmap; 内存区域链表
struct rb_root mm_rb; VMA形成的红黑树
struct vm_area_struct *mmap_cache; 最近使用的内存区域
unsigned long free_area_cache; 地址空间第一个空洞
pgd_t *pgd; 页全局目录
atomic_t mm_user; 使用地址空间的用户数
atomic_t mm_count; 主使用计数器
int map_count; 主存区域个数
struct rw_semaphore mmap_sem; 内存区域的信号量
spinlock_t page_table_lock; 页表锁
struct list_head mmlist; 所有mm_struct形成的链表
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 total_vm; 全部页面数目
unsigned long locked_vm; 上锁的页面数目
unsigned long saved_auxv[AT_VECTOR_SIZE]; 保存的auxv
cpumask_t cpu_vm_mask; 懒惰TLB交换掩码
mm_context_t context; 体系结构特殊数据
unsigned long flags; 状态标志
int core_waiters; 内核转储等待线程
struct core_state *core_state; 核心转储的支持
spinlock_t ioctx_lock; AIO I/O链表锁
struct hlist_head ioctx_list; AIO I/O 链表
};
mm_count是mm_user的增量
mm_user= 0 则mm_count=0
mm_count = 0说明已经没有任何指向该mm_struct结构体的引用了,这时结构体被撤销。
mmap和mm_rb描述的对象是相同的:该地址空间中的全部内存区域。前者链表形式,后者红黑树形式
内核并没有复制mm_struct结构体,而仅仅被包含其中。
所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间。另外操作该链表的时候需要使用mmlist_lock锁来防止并发问题
6,
fork()函数利用copy_mm()复制父进程的内存描述符,也就是current->mm域给其子进程,而子进程中的mm_struct结构体实际是通过allocate_mm()宏从mm_cachep slab缓存中分配得到的。
每个进程拥有唯一的mm_struct结构体,唯一的进程地址空间
7,
如果父进程希望和其子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们把这样的进程称作线程。
是否共享地址空间几乎是进程和Linux中所谓的线程间本质上的唯一区别。除此之外,Linux内核并不区别对待它们,线程对内核来说仅仅是一个共享特定资源的进程而已。
当CLONE_VM被指定后,内核就不再需要调用allocate_mm()函数了,而仅仅需要在调用copy_mm函数中将mm域指向其父进程的内存描述符就可以了
if(clone_flags & CLONE_VM)
{
current 是父进程而tsk在fork()执行期间是子进程
atomic_inc(¤t->mm_mm_users);
tsk->mm = current->mm;
}
8,
当进程退出时,内核会调用exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量。其中该函数会调用mmput()函数减少内存描述符中的mm_users用户计数,如果用户计数降到零,将调用mmdrop()函数,减少mm_count使用计数。如果使用计数也等于零了,说明该内存描述符不再有使用者了,那么调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中
9,
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中的mm域为空。事实上,这也正是内核线程的真正含义——它没有用户上下文
省了进程地址空间再好不过了,因为内核线程并不需要访问任何用户空间的内存而且因为内核线程在用户空间中没有任何页,所以实际上它们并不需要有自己的内存描述符和页表。尽管如此,即使访问内核内存,内核线程也还是需要使用一些数据结构的,比如页表。为了避免内核线程为内存描述符和页表浪费内存,也为了当新内核线程运行时,避免浪费处理器周期向新的地址空间进行切换,内核线程将直接使用前一个进程的内存描述符
当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。
内核线程没有地址空间,所以mm域为NULL。
于是,当一个内核线程被调度时,内核发现它的mm域为空,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程内存描述符。所以在需要时,内核线程便可以使用前一个进程的页表。
因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程的完全相同
10,
内存区域由vm_area_struct结构体描述,内存区域在Linux内核中也经常称作(virtual memory Area,VMA)
struct vm_area_struct
{
struct mm_struct *vm_mm; 相关的mm_struct结构体
unsigned long vm_start; 区间的首地址
unsigned long vm_end; 区间的尾地址
struct vm_area_struct *vm_next; VMA链表
pgprot_t vm_page_prot; 访问控制权限
unsigned long vm_flags; 标志
struct rb_node vm_rb; 树上该VMA的节点
union 或是关联与address_space->i_mmap字段,或者是关联 于address_space->i_mmap_nonlinear字段
{
struct
{
struct list_head list;
void *parent;
struct vm_area_struct *head;
}vm_set;
struct prio_tree_node prio_tree_node;
}shared;
struct list_head anon_vma_node; anon_vma项
struct anon_vma *anon_vma; 匿名VMA对象
struct vm_operations_struct *vm_ops; 相关操作表
unsigned long vm_pgoff; 文件中的偏移量
struct file vm_file; 被映射的文件(如果存在)
void *vm_private_data; 私有数据
};
每个内存描述符都对应于进程地址空间之中的唯一区间
11,
vm_mm域指向和VMA相关的mm_struct结构体。
注意,每个VMA对其相关的mm_struct结构体来说都是唯一的,所以即使两个独立的进程将统一文件映射到各自的地址空间,它们分别都会有一个vm_area_struct 结构体来标志自己的内存区域;
反过来,如果两个线程共享一个地址空间,那么它们也同时共享其中的所有vm_area_struct结构
12,
VMA标志是一种位标志,标志了内存区域所包含的页面的行为和信息。和物理页面的访问权限不同,VMA标志反映了内核处理页面所需要遵守的行为准则,而不是硬件要求。而且vm_flags同时也包含了内存区域中的每个页面的信息,或整个内存区域的整体信息,而不是具体的独立页面
13,
struct vm_operations_struct
{
void (*open)(struct vm_area_struct *);
void(*close)(struct vm_area_struct *);
int (*fault)(struct vm_area_struct *,struct vm_fault *);
int (*page_mkwrite)(struct vm_area_struct *vma,
struct vm_fault *vmf);
int(*access)(struct vm_area_struct *,unsigned long ,
void *,int ,int );
};
14,
void open(struct vm_area_struct *area)
把指定的内存区域加入到一个地址空间
void close(struct vm_area_struct *area)
指定的内存区域从地址空间删除
int fault(struct vm_area_struct *area,struc vm_fault *vmf)
当没有出现物理内存中的页面被访问时,该函数被页面故障处理调用
int page_mkwrite(struct vm_area_struct *area,struct vm_fault*vmf)
当某个页面为只读页面时,该函数被页面故障处理调用
int access(struct vm_area_struct *vma,unsigned long address,
void *buf,int len, int write)
当get_user_pages()函数调用失败时,该函数被access_process_vm()函数调用
15,
通过cat /proc/<pid>/maps输出信息
开始-结束 访问权限 偏移 主设备号:次设备号 i节点 文件
pmap pid 和上面效果差不多
16,
如果一片内存范围是共享的或不可写的,那么内核只需要在内核中文件保留一份映射。
没有映射文件的内存区域的设备标志位:00:00,索引节点标志也为0,这个区域就是0页。
如果将零页映射到可写的内存区域,那么该区域将全部被初始化为0
17,
18,
struct vm_area_struct * find_vma(struct mm_struct * mm,
unsigned long addr)
该函数在指定的地址空间搜索第一个vm_end大于addr的内存区域
struct vm_area_struct *find_vma_prev(struct mm_struct *mm,
unsigned long addr,struct vm_area_struct **pprev)
返回第一个小于addr的VMA
19,
返回第一个和指定区间相交的VMA(个人感觉该程序有问题)
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct *mm,unsigned long start_addr,unsigned long end_addr)
{
struct mm_area_struct *vma;
vma = find_vma(mm,start_addr);
if(vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}
20,
内核使用do_mmap()函数创建一个新的线性地址空间。
说该函数创建了一个新的VMA并不准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,两个区间将合并为一个。如果不能合并,就确实创建了一个新的VMA了。
但无论哪种情况,do_mmap()函数都会将一个地址区间加入到进程的地址空间中——无论是扩展与存在的内存区域还是创建一个新的区域。
unsigned long do_mmap(struct file *file,unsigned long addr,
unsigned long len,unsigned long prot,
unsigned long flag,unsigned long offset)
该函数映射由file指定的文件,具体映射的是文件中从偏移offset处开始,长度为len字节的范围内的数据。
如果file为NULL并且offset参数也是 0,那么就代表这次映射没有和文件相关,该情况称作匿名映射。
如果指定了文件名和偏移量,那么该映射称为文件映射
addr为可选参数,它指定了搜索空闲区域的起始位置
prot参数指定内存区域中页面的访问权限
flag参数指定了VMA标志,这些标志指定了类型并改变映射的行为
21,
如果系统调用do_mmap()的参数有无效参数,那么它返回一个负值;否则它会在虚拟内存中分配一个合适的新内存区域。
如果可能的话,将新区域和邻近区域进行合并,否则内核从vm_area_cachep长字节(slab)缓存中分配一个vm_area_struct结构体,并且使用vm_link()函数将新分配的内存区域添加到地址空间的内存区域链表和红黑树中,随后还要更新内存描述符中total_vm域,然后才返回新分配的地址区间的初始地址
22,
用户空间通过mmap()获取内核函数do_mmap()的功能
void * mmap2(void *start,size_t length,int prot,int flags,int fd,
off_t pgoff)
由于该系统调用是mmap()调用的第二种变种,所以起名mmap2()。最原始的mmap()调用最后一个参数是字节偏移量,而目前这个函数使用页面偏移作为最后一个参数。
使用页面偏移量可以映射更大的文件和更大的偏移位置。
23,
do_mummap()函数从特定的进程地址空间删除指定的地址区间:
int do_mummap(struct mm_struct *mm,unsigned long start,
size_t len)
从mm地址空间中删除从start开始长度为len字节的地址区间
用户空间:
int munmap(void *start,size_t length)
从自身地址空间中删除指定地址区间
asmlinkage long sys_munmap(unsigned long addr,size_t len)
{
int ret;
struct mm_struct *mm;
mm=current->mm;
down_write(&mm->mmap_sem);
ret = do_mummap(mm,addr,len);
up_write(&mm->mmap_sem);
return ret;
}