页面目录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_startvm_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 内核源代码情景分析——几个重要的数据结构和函数的更多相关文章

  1. Linux内核源代码情景分析系列

    http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统  5.1 概述 构成一个操作系统最重要的就 ...

  2. Linux内核源代码情景分析-fork()

    父进程fork子进程: child = fork() fork经过系统调用.来到了sys_fork.具体过程请參考Linux内核源码情景分析-系统调用. asmlinkage int sys_fork ...

  3. linux 内核源代码情景分析——地址映射的全过程

    linux 内核采用页式存储管理.虚拟地址空间划分成固定大小的"页面",由MMU在运行时将虚拟地址映射成某个物理内存页面中的地址.页式内存管理比段式内存管理有很多好处,但是由于In ...

  4. linux 内核源代码情景分析——linux 内存管理的基本框架

    386 CPU中的页式存管的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射.这种映射模式在大多数情况下可以节省页面表所占用的空间.因为大多数进程不会用到整个虚存空间,在虚存空 ...

  5. linux 内核源代码情景分析——linux 内核源代码中的C语言代码

    linux 内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc.GNU对C语言本身作了不少扩充. 1) gcc 从 C++ 语言中吸收了"inline"和" ...

  6. linux 内核源代码情景分析——用户堆栈的扩展

    上一节中,我们浏览了一次因越界访问而造成映射失败从而引起进程流产的过程,不过有时候,越界访问时正常的.现在我们就来看看当用户堆栈过小,但是因越界访问而"因祸得福"得以伸展的情景. ...

  7. Linux内核源代码情景分析-中断半

    一.中断初始化 1.中断向量表IDT初始化 void __init init_IRQ(void) { int i; #ifndef CONFIG_X86_VISWS_APIC init_ISA_irq ...

  8. linux 内核源代码情景分析——越界访问

    页式存储管理机制通过页面目录和页面表将每个线性地址转换成物理地址,当遇到下面几种情况就会使CPU产生一次缺页中断,从而执行预定的页面异常处理程序: ① 相应的页面目录或页表项为空,也就是该线性地址与物 ...

  9. linux 内核源代码情景分析——linux 内核源码中的汇编语言代码

    1. 用汇编语言编写部分核心代码的原因: ① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分: ② CPU中的一些特殊指令也没有对应的C语言成 ...

随机推荐

  1. CodeForce-797C Minimal string(贪心模拟)

    Minimal string CodeForces - 797C Petya 收到一个长度不超过 105 的字符串 s.他拿了两个额外的空字符串 t 和 u 并决定玩一个游戏.这个游戏有两种合法操作: ...

  2. CodeForce-812B Sagheer, the Hausmeister(DFS)

    Sagheer, the Hausmeister CodeForces - 812B 题意:有一栋楼房,里面有很多盏灯没关,为了节约用电小L决定把这些灯都关了. 这楼有 n 层,最左边和最右边有楼梯. ...

  3. Java多线程-1(3)

    本份随记主要为狂神老师的Java多线程教学的学习笔记,记载了视频中一些有关基础概念以及部分代码示例.随机分为1-3共三份,知识点记录的不是很深入,以后的学习过程中随时补充. 1 有关基础概念 1.1 ...

  4. Linux系列(26) - 强制杀死进程

    查进程 ps  -ef ps -aux #上述两个均可 例子:ps -ef | grep "vim canshu2" 强杀进程 kill -s 9 进程id #命令格式 例子:ki ...

  5. linux中创建公私钥

    linux中创建公私钥要再~(root)目录下ssh-keygencd /root/.ssh/lsid_rsa 是私钥id_rsa.pub 是公钥把 authorized_keys删除掉,重新建aut ...

  6. jmeter设置为中文

    我的jmeter安装路径是在D:\Jmeter\apache-jmeter-5.1.1\bin. 设置中文有2种方法: 1.第一种方法:点击jmeter.bat进入jmeter界面,点击[option ...

  7. supervisor + celery 的简单配置与报错处理

    ubuntu服务器下使用 supervisor 和 celery supervisor 的卸载过程: sudo apt purge supervisor whereis supervisord如果有用 ...

  8. PolarDB PostgreSQL 架构原理解读

    背景 PolarDB PostgreSQL(以下简称PolarDB)是一款阿里云自主研发的企业级数据库产品,采用计算存储分离架构,兼容PostgreSQL与Oracle.PolarDB 的存储与计算能 ...

  9. Chrome安装Postman以及启动的方式

    Postman一个web开发人员必不可少的接口调试神器 Chrome安装Postman的方法网上很多,就不一一列举了我个人使用的方式目前常用的两种方式 方式一:下载插件安装包使用开发者模式安装 推荐一 ...

  10. Django整理(二) - 视图和模板的初步使用

    Django中的视图 · Django使用视图来编写web应用的业务逻辑 · Django的视图也就是一个函数,可称为视图函数 · 视图定义在应用的view.py文件中 · 视图需要绑定一个URL地址 ...