linux 内核源代码情景分析——几个重要的数据结构和函数
阅读原文时间:2023年07月08日阅读:1

页面目录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树中去。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章