在Linux2.4内核以及高版本的Linux中,采用的是基本的页式管理机制,为什么这么说呢?因为在Linux内核启动的时候实际上已经让段式管理机制不起作用了。在i386机器中一般是不能跳过段式管理直接到页式管理的,Linux也不例外,所以在启动进程(线程)的时候将代码段数据段等进行如下设置(include/asm-i386/processor.h):
#define start_thread(regs, new_eip, new_esp) do {
} while (0)
也就是将代码段设置为__USER_CS,将数据段、堆栈段、附加段等设置为__USER_DS,在include/asm-i386-segment.h中如下定义各个段:
#define __KERNEL_CS
#define __KERNEL_DS
#define __USER_CS
#define __USER_DS
也就是将内核代码段选择子定义为0x10,将内核数据段定义为0x18,将用户代码段定义为0x23,将用户数据段定义为0x2B。这几个段的描述符在arch/i386/kernel/head.S中定义:
ENTRY(gdt_table)
可以看到,全局段描述符的前两项为空,未用,第三项为从0开始的4GB内核态代码段,第四项为从0开始的4GB内核态数据段,第五项为从0开始的4GB用户态代码段,第六项为从0开始的4GB用户态数据段。
Linux如此设置让每个进程的用户态和内核态的代码段、数据段一致,让段式管理实际上不起作用,而直接进行页式管理模块。
当然,我们在使用Linux的时候会有一些软件比如WINE来模拟Windows环境,运行部分Windows程序,这时候还是会用到段式管理的,这都不是重点,就不多说了。
以前说的页式存储管理是使用三层页表机制,即页目录表PGD,页表PT,和具体页内偏移。而在实际上Linux2.4内核中是在PGD和PT之间加了一层PMD,但是在32位系统中,PMD实际上也是不起作用,只有在64位时候才起到其作用。所以在此真正说的还是按照以前的方式来说。
当配置内核选择32位系统的时候将CONFIG_X86_PAE设置为0,在include/asm-i386/pagetable.h中一个定义如下:
#ifndef __ASSEMBLY__
#if CONFIG_X86_PAE
# include <asm/pgtable-3level.h>
#else
# include <asm/pgtable-2level.h>
#endif
#endif
也就是说引用的是asm/pgtable-2level.h头文件,在此头文件中有定义如下:
#define PGDIR_SHIFT
#define PTRS_PER_PGD
定义了一个32位线性地址从第bit22开始为页目录项,每个页目录表中有1024个页目录项。
在Linux的页式管理中,操作系统实际上是给每个进程分配了4GB的虚拟存储空间。每个进程都有系统空间和用户空间。从0~3GB为用户程序空间,从3GB~4GB为系统空间,在此还是重点讲32位的系统。
进程的3GB自己的虚拟空间是私有的,而最高的1GB系统空间是公共的。如下:
在实际物理内存中,内核空间总是从绝对地址0开始的,而且对于内核空间来说,映射就是简单的线性映射,不会出现虚拟地址A大于虚拟地址B但是实际映射到的物理地址A<B的情况。所以对于内核空间来说,虚拟地址和物理地址之差就是0XC0000000。定义于include/asm-i386/page.h中:
#define __PAGE_OFFSET
内核区间线性地址和物理地址的转化如下:
#define PAGE_OFFSET
#define __pa(x)
#define __va(x)
__pa(x)就是将线性地址转化为物理地址,就是线性地址减去__PAGE_OFFSET,__va(x)就是将物理地址转化为线性地址,就是物理地址加上__PAGE_OFFSET。
同时在include/asm-i386/processor.h中有如下定义:
#define TASK_SIZE
表明最大进程大小为3GB,并不包括系统空间。
在Linux2.4内核中,内存管理是比较复杂的,所以数据结构也就比较复杂,当然这里说的管理还是指的页式管理。
最底层的数据结构当然就是最基本的页目录表和页表之类,定义在include/asm-i386/page.h中:
typedef
typedef
typedef
其中,pte_t是页表项,pgd_t是页目录项,pmd_t是中间项(在此不用)。
紧随下面还定义了一个数据结构:
typedef
pgprot_t是用来说明保护结构的,只用到低12位,因为像pte_t和pgd_t的高20位存物理地址,低12位存储的是一些属性等,pgprot_t就是用来说明这些属性的,当把一个物理地址的低12位清0再与pgprot_t相“或”就得出pte_t或者pgd_t项的值。其中页表/页目录表中的低12位属性如下所示(include/asm-i386/pgtable.h):
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
其中0x080位在此不用。
1、物理空间管理
在系统中,每个物理页面都有一个记录信息与之相对应,在内核中的数据结构就是page,定义于include/linux/mm.h中:
typedef
struct
struct
unsigned
struct
atomic_t
unsigned
struct
unsigned
wait_queue_head_t
struct
struct
void
struct
}
在内存中,page使用list_head数据结构组成一个双向循环链表,list_head是一个通用的数据结构,贯穿在Linux的内核连接上,它只有两个指针定义如下:
struct
struct
};
在page数据结构中mapping和index都和文件和内存交换有关,在后面会详解。next_hash是自身指针,将自身连接成一个链表。atomic_t实际上就是int型,count可以理解为目前正在使用的本块内存的程序数量(实际上是用于页面交换的计数,若页面为空闲则为0,分配就赋值1,每建立或恢复一次映射就加1,断开映射就减一
在page中有一个非常重要的struct
在系统初始化的时候就根据系统内存大小建立了一个page类型的数组,每个page数据结构代表着一个页面,数组的下标就是实际物理内存的页面号。
对于每个管理区都有一个数据结构与之相对应,定义在include/linux/mmzone.h中:
typedef
spinlock_t lock;
unsigned
unsigned
unsigned
unsigned
unsigned
struct
free_area_t free_area[MAX_ORDER];
char *name;
unsigned
struct
unsigned
unsigned
struct
}
当初始化完成管理区建立完成以后,某个页面就永久属于某个管理区,不能更改。其中offset是指在mem_map中的起始页面号,从第几个物理页面开始。
在zone_struct中有一个空闲区free_area[MAX_ORDER],它的定义也在mmzone.h中如下:
#define
typedef
struct
unsigned
}
它是指内存中空闲区双向队列,在讲page数据结构的时候有一个struct
在zone_struct中还有一个数据结构:struct
typedef
zone_t
zonelist_t
struct
unsigned
struct
unsigned
unsigned
unsigned
int
struct
}
其中几个常数定义如下:
#define
#define
#define
#define
#define
这个数据结构是用来描述“存储节点”的,什么叫存储节点?简单的说来,我们的内存都不是均匀的,包括了RAM、ROM、显卡RAM等等还有不同CPU模块上的内存等等,Linux使用存储节点来对内存进行划分,使同一存储节点内的内存均匀相同。
node_zones[]就是该节点的最多三个管理区。node_mem_map指向具体节点的page结构数组。zone_struct中的zone_pgdat指向它属于的存储节点(一个管理区只属于一个存储节点?我理解为可以有多个。因为存储节点链是连续的,所以只需要提供指针。所以一个管理区可以通过指针跨越多个存储节点)。node_next指针将存储节点链接成一个单链队列。
在pglist_data中还有一种数据结构:zonelist_t,它也是定义在本文件中:
typedef
zone_t
int
}
它其实是一种策略数据结构,一个数据结构表示的是一种内存分配策略,分配内存的时候先从zones[0]管理区里面找符合条件的内存,找不到就从zones[1]里面找,以此类推。每一个数据结构表示一个寻找内存的顺序,每一个存储节点可以有0x100中这种寻找策略(顺序),所以在pglist_data中定义了一个数组:zonelist_t
以上说的都是物理空间管理,好了,来理清一下上面的数据结构我们用如下图来表示:
2、虚拟空间管理
除了物理空间管理以外,Linux的一个强大之处还在于其虚拟空间的管理。前面也说过,Linux为每个进程分配了4GB的虚拟空间,其中,用户空间占了3GB。虽然当物理空间不连续的时候还是可以使用页表机制将其映射成连续的虚拟空间,但是实际上使用的时候虚拟空间也不一定是连续的。所以在Linux内核中还有一个数据结构来管理成块的虚拟空间(include/linux/mm.h):
struct
struct
unsigned
unsigned
struct
pgprot_t
unsigned
short
struct
struct
struct
struct
struct
unsigned
struct
unsigned
void
};
每一个vm_area_struct管理一块连续的虚拟空间(注意:不同进程的虚拟用户空间一般不互相影响),vm_start和vm_end分别表示虚拟空间的开始和结束地址,注意这里的开始地址vm_start是包含在自身的管理区间内的,而vm_end是不包含在内的。
vm_next是用来连接统一进程的所有vm_area_struct虚拟链,是按照高低次序进行连接的。
当一个进程的虚存块划分地较少的时候可以使用链式连接查询,但是如果太多的话这样查询效率会降低,所以在块数较多的时候采用AVL树进行存储,也就是平衡二叉树。vm_area_struct中的vm_avl_height、vm_avl_left和vm_avl_right都是和AVL树有关。
vm_next_share、vm_pprev_share、vm_ops等都和磁盘文件以及内存换出等有关,在以后会说到。
在vm_area_struct开头有一个定义:struct
struct
struct
struct
struct
pgd_t
atomic_t
atomic_t
int
struct
spinlock_t
struct
unsigned
unsigned
unsigned
unsigned
unsigned
unsigned
unsigned
unsigned
mm_context_t
};
每个进程都有一个唯一的mm_struct数据结构,在进程的PCB中,有一个链接到其自己的mm_struct。mm_struct是整个用户空间的抽象,也是总的控制结构。其中:
mmap用来建立一个虚存空间的单链队列;
mmap_avl用来建立虚存空间的AVL树;
mmap_cache用来指向最近一次用到的虚存区间结构;
pgd用来指向该进程的页目录表;
一个进程的mm_struct可以为多个进程共享,mm_user和mm_count就是用来计数的;
map_count用来记录进程有几个虚存空间;
mmap_sem和page_table_lock是用来定义P、V操作的信号量。
另外start_code、end_code等就是该进程的代码等起始、结束地址。
总的来说,一个进程的访问图如下:
关于这些结构的处理方法以后再说。
以下所说皆是在内核中使用的方法:
在include/asm-i386/page.h中定义如下:
#define
这个是用来说明用户空间和系统空间的位置偏移的。
#define
#define
#define
其中,__pa(x)是在内核中给出线性地址求出物理地址的。
__va(x)是在内核中给出物理地址求出线性地址的。
在page.h中有定义如下:
#define
#define
#define
#define
#define
#define
#define
都是用来求出实际值或者强制转换的。
在include/asm-i386/pgtable-2level.h中有定义如下:
#define
这个是给出页面号和属性来求出实际页面表项的。
在include/asm-i386/pgtable.h中有定义如下:
#define
这个是用来根据物理地址和属性求出实际页面表项的。
在include/asm-i386/pgtable-2level.h中有如下定义:
#define
根据值设置页面表项。
在pgtable-2level.h中还定义了一些检测页表的方法:
#define
用来检测页表项是否为空。
在pgtable.h中定义了如下方法:
#define
用来表示是否存在,如果页面表项不为0但是存在位为0表示映射已经建立,但是页面不在内存中。
在pgtable.h中还有如下定义:
static
static
static
分别表示是否读脏,是否已被访问和是否可写。
在pgtable-2level.h中有定义如下:
#define
用来给出页表项求出对应的page项。
在page.h中有如下定义:
#define
用来给出虚存地址(线性地址)求出对应的page项(page数据结构)。
以下为虚拟地址部分
在mm/mmap.c中有定义如下:
struct
{
struct
if
vma
if
if
vma
while
vma
}
struct
vma
for
if
break;
if
vma
if
break;
tree
}
tree
}
}
if
mm->mmap_cache
}
}
return
}
此函数有两个参数,一个是地址,一个是指向mm_struct结构的指针,返回的是此地址所在的vm_area_struct区域数据结构的指针。
在同一文件中还有定义如下:
void
{
struct
struct
if
pprev
while
pprev
}
struct
avl_insert_neighbours(vmp,
pprev
if
printk("insert_vm_struct:
}
vmp->vm_next
*pprev
mm->map_count++;
if
build_mmap_avl(mm);
file
if
struct
struct
struct
if
atomic_dec(&inode->i_writecount);
head
if
head
if((vmp->vm_next_share
(*head)->vm_pprev_share
*head
vmp->vm_pprev_share
}
}
void
{
lock_vma_mappings(vmp);
spin_lock(¤t->mm->page_table_lock);
__insert_vm_struct(mm,
spin_unlock(¤t->mm->page_table_lock);
unlock_vma_mappings(vmp);
}
用来往进程的mm_struct结构中新加入虚拟块vm_area_struct。