linux 内核源代码情景分析——几个重要的数据结构和函数
页面目录PGD、中间目录PMD和页面表PT分别是由表项pgd_t、pmd_t和pte_t构成的数组,而这些表项都是数据结构
1 /*
2 * These are used to make use of C type-checking..
3 */
4 #if CONFIG_X86_PAE
5 typedef struct { unsigned long pte_low, pte_high; } pte_t;
6 typedef struct { unsigned long long pmd; } pmd_t;
7 typedef struct { unsigned long long pgd; } pgd_t;
8 #define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
9 #else
10 typedef struct { unsigned long pte_low; } pte_t;
11 typedef struct { unsigned long pmd; } pmd_t;
12 typedef struct { unsigned long pgd; } pgd_t;
13 #define pte_val(x) ((x).pte_low)
14 #endif
15 #define PTE_MASK PAGE_MASK
可见,采用32位地址时,pgd_t、pmd_t和pte_t实际上就是长整数,采用36位地址时,就是long long整数,之所以不直接定义成长整数的原因在于这样可以让gcc在编译时加以更严格的类型检查。
由于表项PTE作为指针实际上只需要它的高20位,并且所有的物理页面都是跟4K字节的边界对齐的,因而,物理页面起始地址的高20位又可以看做是物理页面的序号,但内核并没有在pte_t的低12位定义有关页面状态信息和访问权限的位段,而是又额外定义了一个来说明页面保护的结构pgprot_t:
typedef struct { unsigned long pgprot;} pgprot_t;
参数pgprot 的值与i386MMU的页面表项的低12位对应,其中9位时标志位,表示所映射的页面的当前状态和访问权限,它和pte中的指针部分和在一起就得到实际用于页面表中的表项:
#define __mk_pte(page_nr, pgprot) \
__pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
#define pgprot_val(x) ((x).pgprot)
#define __pte(x) ((pte_t) {(x)})
内核中有个全局变量mem_map,是一个指针,指向一个page数据结构的数组,每个page数据结构代表着衣蛾物理页面,整个数组就代表着系统中的全部物理页面。因此表项的高20位对于软件和MMU硬件有着不同的意义,对于软件是一个物理页面的序号,将这个序号用作下标就可以从mem_map找到代表这个物理页面的page数据结构,对于硬件,则就是物理页面的起始地址的高20位.
把表项的值设置到页面表项中
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
判断为真表示尚未为这个表项建立映射
#define pte_none(x) (!(x).pte_low)
如果页面表项不为0,但P标志位为0,则表示映射已建立,但是所映射的物理页面不在内存中
#define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
若pte所指的页面已经被写过,则返回true
static inline int pte_dirty(pte_t pte) \
{ return (pte).pte_low & _PAGE_DIRTY; }
若pte所指的页面已经被访问过,则返回true
static inline int pte_young(pte_t pte) \
{ return (pte).pte_low & _PAGE_ACCESSED; }
返回0表示只读,非0表示可写,这些标志位只有在P标志位为1时才有意义
static inline int pte_write(pte_t pte) \
{ return (pte).pte_low & _PAGE_RW; }
根据页面表项在page数据结构中找到代表目标物理页面的数据结构
#define pte_page(x) \
(mem_map + ((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
根据虚拟地址找到相应物理页面的page数据结构
#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))

系统在初始化时会根据物理内存的大小建立一个page结构数组,每个page对应一个物理页面,page在这个数组的下标就是该物理页面的序号(物理地址的高20位)。系统把整个物理页面划分为ZONE_DMA和ZONE_NORMAL两个管理区(还可能有第三个管理区ZONE_HIGHMEM,用于物理地址超过1GB的存储空间)。
管理区ZONE_DMA里的页面专供DMA使用,原因是:DMA使用的页面是磁盘I/O所必需的,如果物理页面都分配光了,那就无法进行页面和盘区的交换了,还有就是DMA不经过MMU提供的地址映射。
每个管理区都用zone_struct结构来表示,在zone_struct数据结构中有一组“空闲区间”(free_area_t)队列,为什么是一组队列而不是一个队列呢?因为常常需要成块的分配在物理空间内连续的多个页面,所以要按块的大小加以管理。因此,在管理区数据结构中既要有一个队列来保持一些连续长度为1(离散)的物理页面,也需要一个队列来保持一些连续长度为2的页面块以及连续长度为4、8、16、……、直至1024个页面块,即4M字节,这两个数据结构定义如下:


如果CPU访问整个物理空间的任何一个地址所需的时间都相同,就称为“均质存储结构”简称UMA,有些系统其物理存储空间虽然地址连续,“质地”却不一致,称为“非均质存储结构”,简称NUMA。在NUMA结构的系统中,分配连续的若干物理页面时一般要求分配在质地相同的区间(称为node,即节点)。节点的数据结构如下:

上面几个数据结构都是用于物理空间管理的,现在来看看虚拟空间的管理,也就是虚存页面的管理。虚存空间的管理是以进程为基础的,每个进程都有各自的虚存空间,对虚存空间抽象得出一个数据结构vm_area_struct

结构中的vm_start和vm_end决定了一个虚存区间,vm_start是包含在区间内的,vm_end不包含在内
如果一个地址范围内的前一半页面和后一般页面有不同的访问权限或其他属性,就得要分成两个区间,所以,包含在同一个区间里的所有页面都应有相同的访问权限,这就是结构中成员vm_page_prot和vm_flags的用途
属于同一个进程的所有区间都要按虚存地址的高低次序链接在一起,结构中的vm_next就是这个目的
由于区间的划分并不仅仅取决于地址的连续性,一个进程的虚存空间可能会被划分成大量的区间,内核给定一个虚拟地址而要找出其所属的区间是一个频繁的操作,如果每次都要顺着vm_next在链中查找的话会影响到效率,所以除了通过vm_next指针把所有区间串成一个线性队列外,还可以在区间数量较大时建立一个AVL树,结构中的vm_avl_height,vm_avl_left以及vm_avl_right就是用于AVL树来表示本区间在AVL树中的位置的
在两种情况下虚存页面会跟磁盘文件发生关系,一种是盘区交换(swap),当内存页面不够分配时,那些就未使用的页面可能被交换到磁盘上去,腾出物理页面一共更急需的进程使用,另一种情况是将一个磁盘文件映射到一个进程的用户空间中。由于虚存区间与磁盘文件的这种联系,结构中mapping、vm_next_share、vm_pprev_share、vm_file等用于记录和管理这种联系
vm_ops是一个指向vm_operation_struct数据结构的指针,定义如下:
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int write_access);
};
此结构中全是函数指针,nopage 表示当虚存页面不在内存中而引起“页面出错”异常时所应调用的函数
最后,vm_area_struct中还有一个指针vm_mm,该指针指向一个mm_struct数据结构,定义如下:

这是在比vm_area_struct更高层次上使用的数据结构,每个进程只有一个mm_struct结构,在每个进程的“进程控制块”,即task_struct中,有一个指针指向该进程的mm_struct结构,可以说,mm_struct数据结构是进程整个用户空间的抽象,也是总的控制结构。
结构中的头三个指针都是关于虚存空间的,第一个mmap用来建立一个虚存区间结构的单链线性队列,第二个mmap_avl用来建一个虚存区间结构的AVL树,第三个指针mmap_cache,用来指向最近一次用到的那个虚存区间结构,这是因为程序中用到的地址常常带有局部性,最近一次用到的区间很可能就是下一次要用到的区间,这样可以提高效率。
map_count说明在队列中(或AVL树中)有几个虚存区间的结构,也就是说该进程有几个虚存区间
pgd指向该进程的页面目录的,当内核调度一个进程进入运行时,就将这个指针转换成物理地址,并写入控制寄存器CR3
由于mm_struct结构及其下属的vm_area_struct结构都有可能在不同的上下文中受到访问,而这些访问必须互斥,所以在结构在中设置了用于P、V操作的信号量mmap_sem
虽然一个进程只是用一个mm_struct结构,反过来一个mm_struct结构却可能为多个进程所共享,所以在mm_struct结构中还设置了计数器mm_users和mm_count
如前所述,mm_struct结构及其属下的各个vm_area_struct只是表明了对虚存空间的需求,一个虚拟地址有相应的虚存空间存在,并不保证该地址所在的页面已经映射到某一个物理页面,更不保证该页面在内存中。当一个未经映射的页面受到访问时,就会产生一个“page fault”异常。从这个意义上说,mm_struct和vm_area_struct说明了对页面的需求,前面的page、zone_struct等结构说明了对页面的供应,而页面目录、中间目录以及页面表则是二者中间的桥梁,下图说明了用于进程虚存管理的各种数据结构之间的联系
给定一个属于某个进程的虚拟地址,要求找到其所属的区间以及响应的vma_area_struct结构,是由find_vma()来实现的:
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)) {
if (!mm->mmap_avl) {
/* Go through the linear list. */
vma = mm->mmap;
while (vma && vma->vm_end <= addr)
vma = vma->vm_next;
} else {
/* Then go through the AVL tree quickly. */
struct vm_area_struct * tree = mm->mmap_avl;
vma = NULL;
for (;;) {
if (tree == vm_avl_empty)
break;
if (tree->vm_end > addr) {
vma = tree;
if (tree->vm_start <= addr)
break;
tree = tree->vm_avl_left;
} else
tree = tree->vm_avl_right;
}
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
当我们说一个特定的用户空间虚拟地址时,必须说明是哪一个进程的虚存空间中的地址,所以地址有两个,一个是地址,一个是指向该进程的mm_struct结构的指针。首先看一下这个地址是否恰好在上一次访问过的同一个区间中。如果没有命中的话,就要搜索了。如果已经建立AVL结构,就在AVL树中搜索,否则就在线性队列中搜索,最后,如果找到的话,就把mmap_cache指针设置成指向所找到的vm_area_struct结构。若函数返回值为零,表示该地址所属的区间还没建立,此时通常就得要建立一个新的虚存区间结构,再调用insert_vm_struct()将其插入到mm_struct中的线性队列或AVL树中去。
linux 内核源代码情景分析——几个重要的数据结构和函数的更多相关文章
- Linux内核源代码情景分析系列
http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统 5.1 概述 构成一个操作系统最重要的就 ...
- Linux内核源代码情景分析-fork()
父进程fork子进程: child = fork() fork经过系统调用.来到了sys_fork.具体过程请參考Linux内核源码情景分析-系统调用. asmlinkage int sys_fork ...
- linux 内核源代码情景分析——地址映射的全过程
linux 内核采用页式存储管理.虚拟地址空间划分成固定大小的"页面",由MMU在运行时将虚拟地址映射成某个物理内存页面中的地址.页式内存管理比段式内存管理有很多好处,但是由于In ...
- linux 内核源代码情景分析——linux 内存管理的基本框架
386 CPU中的页式存管的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射.这种映射模式在大多数情况下可以节省页面表所占用的空间.因为大多数进程不会用到整个虚存空间,在虚存空 ...
- linux 内核源代码情景分析——linux 内核源代码中的C语言代码
linux 内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc.GNU对C语言本身作了不少扩充. 1) gcc 从 C++ 语言中吸收了"inline"和" ...
- linux 内核源代码情景分析——用户堆栈的扩展
上一节中,我们浏览了一次因越界访问而造成映射失败从而引起进程流产的过程,不过有时候,越界访问时正常的.现在我们就来看看当用户堆栈过小,但是因越界访问而"因祸得福"得以伸展的情景. ...
- Linux内核源代码情景分析-中断半
一.中断初始化 1.中断向量表IDT初始化 void __init init_IRQ(void) { int i; #ifndef CONFIG_X86_VISWS_APIC init_ISA_irq ...
- linux 内核源代码情景分析——越界访问
页式存储管理机制通过页面目录和页面表将每个线性地址转换成物理地址,当遇到下面几种情况就会使CPU产生一次缺页中断,从而执行预定的页面异常处理程序: ① 相应的页面目录或页表项为空,也就是该线性地址与物 ...
- linux 内核源代码情景分析——linux 内核源码中的汇编语言代码
1. 用汇编语言编写部分核心代码的原因: ① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分: ② CPU中的一些特殊指令也没有对应的C语言成 ...
随机推荐
- 微信公众号授权获取code带多个参数 丢失参数
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&re ...
- ecshop首页调用团购说明
要在首页调用购买. 发现在首页还不能直接调用团购说明.查看了一下代码发现要修改下才能调 打开根目录的 index.php 文件找到 $sql = 'SELECT gb.act_id AS group_ ...
- Django边学边记--状态保持(cookie和session)
Cookie 概念: Cookie,也叫Cookies,指某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据(通常经过加密),好比会员卡或餐票. 特点: Cookie是由服务 ...
- 浅析Java中的static关键字
关键点 <Java编程思想>对static方法的描述:"static方法就是没有this的方法.在static方法内部不能调用非静态方法,反过来是可以的.而且可以在没有创建对象的 ...
- css 样式设定
阴影: --可以同时设定多个阴影.用逗号隔开 http://www.fly63.com/article/detial/4726 div { box-shadow: 10px 10px 5px #888 ...
- Mybatis-技术专区-Criteria的and和or进行联合条件查询
之前用Mybatis框架反向的实体,还有实体里面的Example,之前只是知道Example里面放的是条件查询的方法,可以一直不知道怎么用,到今天才开始知道怎么简单的用.在我们前台查询的时候会有许多的 ...
- 【Golang】三个点(...)用法
众所周知,Go语言是严格类型语言,而开发的时候又遇到传入参数不定的情况,怎么办? 这里的三个点(-),就给我们编程人员带来很大的灵活性,具体如下 在Golang中,三个点一共会用在四个地方(话说三个点 ...
- react之组建通信
父组件与子组件通信 父组件将自己的状态传递给子组件,子组件当做属性来接收,当父组件更改自己状态的时候,子组件接收到的属性就会发生改变 父组件利用ref对子组件做标记,通过调用子组件的方法以更改子组件的 ...
- 基于TLS证书手动部署kubernetes集群
一.简介 Kubernetes是Google在2014年6月开源的一个容器集群管理系统,使用Go语言开发,Kubernetes也叫K8S. K8S是Google内部一个叫Borg的容器集群管理系统 ...
- 【JAVA】【作业向】第一题:本学期一班级有n名学生,m门课程。现要求对每门课程的成绩进行统计:平均成绩、最高成绩、最低成绩,并统计考试成绩的分布律。
1.预备知识:动态数组Array实现: 2.解题过程需要理解的知识:吧唧吧唧吧唧吧唧 不想做了 就用了最简单的方法 和c语言类似 java版本 `import java.util.Scanner; / ...