进程地址空间是啥?
进程
一个进程在内存中是什么样的?
一个进程就是一段运行的代码,那运行的代码会有哪些东西呢?
- 正文段,也就是代码,一个程序要运行肯定需要代码的
- 数据段,我们在写代码时总是会定义各种数据结构,而数据结构自然要占据内存,因此我们将其表示为数据段,数据段一般是可增长的,因为我们经常会在代码中动态分配内存如
malloc
函数 - 堆栈段,堆栈段不同于数据段和代码段,它一般保存着一些与程序控制有关的东西,如一个
linux
命令,cp src dst
,这一行命令表示拷贝src
文件到dst
文件,那进程的堆栈段会保存这三个参数,另外,进程的堆栈段还会保存struct thread_info
(根据该结构可以找到进程的进程描述符),该结构放在进程堆栈段的顶端和底端(与堆栈段增长方向相反),这样内核就能很快找到每一个进程的struct task_struct
,加快内核的运行速度
现在我们知道了一个运行的代码是如何使用内存的了
虚拟内存
为什么需要虚拟内存?
可以看一下内存使用这篇文章,简单来说就是我们希望将整个物理内存分成每一页称为页框,然后程序运行时需要的内存称为页面,**这样有什么用呢?**有了分页的概念,那么我们其实可以在程序运行时动态的调入和调出页面,因为一个程序的运行其实并不需要所有的所有的页面都在内存中,它不需要的页面我们就可以调出,需要页面时就调入,还有一个好处就是虚拟内存让每一个程序以为自己独占整个内存(只要将虚拟地址映射到物理地址即可),甚至虚拟内存还能大于物理内存
一个进程是如何使用虚拟内存的?
页表: 通过页表,程序就能将一段虚拟地址转换成物理地址,举一个简单的例子,假设一个32
位计算机,那么它的寻址能力就是0~2^32
也就是0~4G
,现在假设计算机内存分页大小为4KB
,也就是2^12
,那我们知道页框号范围就是2^(32-12)
即2^20
,如果我们使用二级页表,那么我先通过32
地址的前十位在页表中找到页表号,接下来根据找到的页表号所指的另一个页表,找到该页表后我们再根据32
位地址的中间10~20
位找到页表项,通过该页表项我们就能得到页框号了,最后根据页内偏移(32位地址的最后12位)
找到该虚拟地址的物理地址,这就是进程使用虚拟地址的过程
具体还可以看这篇博客通过一个变量的虚拟地址找到物理地址并通过修改物理地址对应的值来改变变量的值
内存管理
基于linux2.6
内核
linux
维护一个mem_map
数组(页描述符数组),每一个页描述符都有一个指针在页面非空闲时指向它所属的地址空间,另有一对指针与其他页描述符组成双向链表
页面分配算法通过伙伴算法,简单来说就是将内存以二分法划分,然后根据请求的页面大小再分配,如果两个相邻的页面被释放,就将其合并成一个大的页面,如果还有更小的页面请求就通过slab
分配(如进程的task_struct
结构体)
进程地址空间
到了这一步进程地址空间是啥应该很清楚了
进程地址空间就是该进程虚拟地址上被映射到物理地址的那一部分
我们看一下linux2.6
中进程描述符struct task_struct
的定义
struct task_struct {
volatile long state; //说明了该进程是否可以执行,还是可中断等信息
/*
*/
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
/*
....
*/
void *journal_info;
};
为了突出重点,我把其中一部分定义省略掉了,如果想看完整的定义可以查看linux2.6
的源码
我们可以在task_struct
中看到struct mm_struct * mm
该元素
sturtc mm_struct
:linux
内核表示地址空间的结构
我们看一下该结构体的定义
struct mm_struct{
struct vm_area_struct * mmap; /* 内存区域链表 */
sturct rb_root mm_rb; /* VMA形成的红黑数 */
/*
...
*/
}
同样的,我省略了一部分定义,我们只需要关注vm_area_struct
和rb_root
就可以了
linux
内核用mm_struct
来表示一个进程的地址空间,而一个进程的地址空间又分成各个连续的地址区间,我们将其称为虚拟内存区域,地址空间不一定是连续的,但是虚拟内存区域是连续的,因此当我们得到一个虚拟地址时,第一步并不是通过页表转换成物理地址,而是先通过进程描述符中的mm_struct
来查找该虚拟地址是否在地址空间中,只有该地址在进程空间我们才能通过页表来转换,不然就会引起一个缺页中断,而mm_struct
就是通过mmap
和mm_rb
来查找的
我们先看一些vm_area_struct
的定义
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链表
/*
...
*/
struct rb_node vm_rb //该区间在红黑树rb_root上的位置
/*
...
*/
}
vm_start
和vm_end
就表示了vma
的区间
至此
我们已经知道进程的地址空间是什么了,以及知道在linux2.6
内核中是如何管理进程地址空间的(主要通过两个结构体struct mm_struct
和struct vm_area_struct
)
现在我们考虑一下,当进程要访问一个虚拟地址时,linux
内核是如何找到它是否包含在地址空间中
- 首先可以根据
VMA
链表查找,VMA
链表是线性地址递增的vm_area_struct
结构体构成的链表 - 遍历链表查找效率太低,如果
VMA
太多会影响性能,我们可以通过红黑树查找,如果你学过数据结构的搜索二叉树,那你肯定很容易理解红黑树是什么,红黑树的每一个节点的左孩子数值都要小于右孩子的数值,这样在查找时我们可以根据以下规则查找- 如果当前线性地址
addr
大于VMA
中的vm_end
,说明包含addr
的区域在右子树那边,这时我们往右子树向下查找 - 如果
addr
小于VMA
中的vm_end
,那么只要VMA
中的vm_start
小于addr
就说明该线性地址包含在该VMA
中
- 如果当前线性地址
如果发现addr
没有包含在进程地址空间上,说明该虚拟地址没有映射,这时候应该引起一个缺页中断,如果addr
包含在地址空间上,那么我们就可以通过页表转换位物理地址然后访问实际的物理内存了
可以看一下linux2.6
中查找一个线性地址在哪个内存区域函数的实现
struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct * vma = null;
if(mm)
{
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;
vm_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if(vmp_tmp->vm_end > addr)
{
vmp = vmp_tmp;
if(vmp_tmp->vm_start <= addr) //包含在VMA中,退出循环
break;
rb_node = rb_node->rb_left; //向左孩子找
}
else
{
rb_node = rb_node->rb_right; //向右孩子找
}
}
if(vmp)
mm->mmap_cache = vma; //保存在缓存中
}
}
}
代码中的缓存提高了连续的地址请求的查找效率
里面提到的红黑树和虚拟内存机制后面我可能会专门写博客来讲,这里只是简单说一下,主要是讲进程地址空间,如果前面部分觉得不好理解可以等我后面写完虚拟内存机制后再来看
参考
《Linux 内核设计与实现》
《现代操作系统》
下一篇: kdevtmpfsi木马清除