计算机体系结构中的核心问题之一就是如何有效的进行内存寻址。因为所有运算的前提都是要先从内存中取地数据。所以内存寻址技术在一定程度上代表了计算机技术。
图灵机:图灵机是一种通用自动的机器模型,通过二段无线延申的纸带作为存储装置,输入输出和状态转移函数是机器的三要素,这三要素组合变形可成为一切机器的原型,可解决一切图灵机能解决的问题。

冯诺依曼体系结构:图灵机的实现,基于图灵机的数据连续存储和选择读取思想,是目前我们使用的几乎所有机器运行背后的灵魂。

以IntelX86结构为例,因为这是我们最为熟悉的结构之一。
在微处理器的是历史上,第一款微处理芯片是4004,由Intel推出的4位机器,之后又推出了一款8位微处理芯片8008。
这个时期没有段的概念,访问内存需要通过绝对地址。程序中的地址必须要进行硬编码,给出具体的物理地址,而且难以重定位。
8086处理器的时代,引入了段的概念。8086的目标是寻址空间达到了1M,但数据总线只有16位,因此需要分为数个64k的段来进行管理。
段描述了一块有限的内存区域,区域的起始位置存在专门的寄存器(段寄存器)中。

把16位的段地址,左移4位后再与16位的偏移量相加,获得一个20位的内存地址。
实模式:从16位内存地址到20位实际地址的转换(映射)。
80286的的地址总线增加到了24位,引入了一个全新的理念(保护模式)
保护模式:访问内存不能直接从段寄存器中获得段的起始地址,而是需要进行额外的转换和检查。
保护模式有很多沿用至今的机制: 内存保护, 分页系统, 虚拟内存等. 大部分现今基于x86的操作系统都是在保护模式下运行的.
为了让文章不显的臃肿, 保护模式的相关概念就不过多介绍了, 感兴趣的可以自行搜索, 这里贴几个相关链接:
以32位CPU80386为例。
Intel选择在段寄存器的基础上构置保护模式,保留段寄存器依然是16位。在保护模式下,段范围不受限于64K,可以达到4GB。
把386以后的处理器称为X86, 这个时候, 保护模式才算是真正的体现出了强大的作用.
分段和分页这个计算机科班出身的应该都在操作系统课程上学过相关的理论知识, 这里就不多bb了. 如果忘了也没关系, 贴一个链接, 第二章
简单来说, i386之后的设备, 有三种不同的地址做区分
为了更直观的了解分段机制和分页机制, 我们从一个简单的"Hello World"程序说起
#include
int main(){
printf("Hello World!\n");
return 0;
}
通过编译, 汇编, 链接, 装在和执行, 最后反汇编
gcc -S helloworld.c -o helloworld.s // 编译: 编译成汇编文件
gcc -c helloworld.s -o helloworld.o // 汇编: 汇编成二进制文件
gcc helloworld.c -o helloworld.out // 链接: 将调用的库进行链接, 输出可执行文件
./helloworld.out // 装载到内存并执行
objdump -d helloworld.out // 反汇编
有三个问题:
下面和下下面将在理论和实践中进行回答.
编译之后形成的虚地址,就是cpu要访问的地址
cpu把虚地址送入MMU(内存管理单元), MMU把虚地址转成物理地址送给存储器
MMU分为两个阶段:

Linux主要采用分页机制来实现虚拟存储管理. Linux分段机制使所有的进程都使用相同的段寄存器, 所有的进程使用同样的线性地址空间
过程可以用一张图来概述: 这张图中很清晰的描述了虚地址是如何转为实地址的, 先通过分段机制转为线性地址, 再通过分页机制转为物理地址.(补充: 在Linux中,段的基地址都为0, 所有程序共享同样的线性空间, 虚地址和线性地址在数值上就相同了)

在3.10.0版本的内核中, centos7采用了4级分页模式, 在/arch/x86/include/asm/pgtable_types.h文件中可以看到, 共pte,pmd,pud和pgd四部分组成.
分别为


寻页机制的代码实践, 需要用到内核提供的一些函数, 因此需要通过编写内核模块的方式实现.
实现思路就是模拟MMU的寻页过程:
注意!!! 新手在服务器或者物理机上直接操作的话一定一定一定要谨慎!!!避免给内核写崩之后重启丢失数据(强烈推荐在虚拟机上搞, 我写崩了好几次内核了, 都需要重启系统, 在虚拟机上试崩了好几次了, 不过有部分原因是我虚拟机内存开小了然后内存越界出错了)
#include
#include
#include // 内存映射
#include
#include
// #include
#include // 多级页表项
/*
在内核中先申请一个页面,
利用内核提供的函数,
利用寻页步骤一步步查询各级页目录,
最终找到所对应的物理地址.
等价于手动模拟MMU单元的寻页过程
*/
static unsigned long cr0, cr3;
static unsigned long vaddr = 0;
/* get_pgtable_macro():
打印页机制中的一些重要参数, 例如:
CR3寄存器的值, 通过read_cr3_pa函数获取
*/
static void get_pgtable_macro(void){
cr0 = read_cr0();
// cr3 = read_cr3_pa();
cr3 = read_cr3();
// _SHIFT的宏是指示线性地址中 相应字段所能映射区域大小的对数
// PAGE_SHIFT指page offset字段所能映射区域大小的对数(映射的是一个页面的大小)
// 一个页面大小是4k(1<<12)
printk("cr0 = 0x%lx, cr3 = 0x%lx\n", cr0, cr3);
printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
// printk("P4D_SHIFT = %d\n", P4D_SHIFT);
printk("PUD_SHIFT = %d\n", PUD_SHIFT);
printk("PMD_SHIFT = %d\n", PMD_SHIFT);
printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);
//PTRS_PER_x 这些宏是用来指示相应页目录表中项的个数
printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
// printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
// page_mask 页内偏移掩码, 屏蔽page offset字段
// 为了方便寻页时进行位运算
printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}
static unsigned long vaddr2paddr(unsigned long vaddr){
pgd_t *pgd;
// p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
unsigned long paddr = 0;
unsigned long page_addr = 0;
unsigned long page_offset = 0;
pgd = pgd_offset(current->mm, vaddr);
printk("pdg_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd), pgd_index(vaddr));
if (pgd_none(*pgd)){
printk("not mapped in pgd\n");
return -1;
}
// p4d = p4d_offset(pgd, vaddr);
// printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d), p4d_index(vaddr));
// if (p4d_none(*p4d)){
// printk("not mapped in p4d\n");
// return -1;
// }
// pud = pud_offset(p4d, vaddr);
pud = pud_offset(pgd, vaddr);
printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud), pud_index(vaddr));
if (pud_none(*pud)){
printk("not mapped in pud\n");
return -1;
}
pmd = pmd_offset(pud, vaddr);
printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd), pmd_index(vaddr));
if (pmd_none(*pmd)){
printk("not mapped in pmd\n");
return -1;
}
pte = pte_offset_kernel(pmd, vaddr);
printk("pte_val = 0x%lx, pte_index = %lu\n", pte_val(*pte), pte_index(vaddr));
if (pte_none(*pte)){
printk("not mapped in pte\n");
return -1;
}
page_addr = native_pte_val(*pte) & PAGE_MASK;
page_offset = vaddr & ~PAGE_MASK;
paddr = page_addr | page_offset;
printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
return paddr;
}
static int __init v2p_init(void){
unsigned long vaddr = 0;
printk("vaddr to paddr module is running...\n");
get_pgtable_macro();
printk("\n");
// vaddr = __get_free_page(GFP_KERNEL);
vaddr = __get_free_page(___GFP_HIGHMEM);
if (vaddr == 0){
printk("__get_free_page failed..\n");
return 0;
}
sprintf((char*)vaddr, "hello world from kernel\n");
printk("get_page_vaddr = 0x%lx\n", vaddr);
vaddr2paddr(vaddr);
return 0;
}
static void __exit v2p_exit(void){
printk("vaddr to paddr module is leaving..\n");
free_page(vaddr);
}
module_init(v2p_init);
module_exit(v2p_exit);
MODULE_LICENSE("GPL");
Makefile文件内容
obj-m:= paging_lowmem.o
PWD:= $(shell pwd)
LINUX_KERNEL_PATH := /usr/src/kernels/$(shell uname -r)
all:
make -C $(LINUX_KERNEL_PATH) M=$(PWD) modules
clean:
@rm -rf *.o *.mod.c *.mod.o *.ko *.order *.symvers .*.cmd .tmp_versions
然后就是内核编程的老操作了:
make命令进行编译,生成paging_lowmem.ko文件insmod paging_lowmem.ko命令装载lsmod命令查看已装在的模块列表,通过dmesg命令查看printk输出的日志rmmod paging_lowmem命令卸载每个人的输出结果可能不同, 这里给出我的输出结果提供一下参考, 顺便方便下面的细节介绍给出示例:
[17024.193831] vaddr to paddr module is running...
[17024.193879] cr0 = 0x80050033, cr3 = 0x3a1a4000
[17024.193880] PGDIR_SHIFT = 39
[17024.193881] PUD_SHIFT = 30
[17024.193882] PMD_SHIFT = 21
[17024.193883] PAGE_SHIFT = 12
[17024.193884] PTRS_PER_PGD = 512
[17024.193885] PTRS_PER_PUD = 512
[17024.193886] PTRS_PER_PMD = 512
[17024.193887] PTRS_PER_PTE = 512
[17024.193887] PAGE_MASK = 0xfffffffffffff000
[17024.193891] get_page_vaddr = 0xffff8caaf497b000
[17024.193893] pdg_val = 0x26aa6067, pgd_index = 281
[17024.193894] pud_val = 0x26aa7067, pud_index = 171
[17024.193895] pmd_val = 0x3498b063, pmd_index = 420
[17024.193896] pte_val = 0x800000003497b063, pte_index = 379
[17024.193897] page_addr = 800000003497b000, page_offset = 0
[17024.193898] vaddr = ffff8caaf497b000, paddr = 800000003497b000
[17038.643975] vaddr to paddr module is leaving..
很多函数都是第一次使用,在这里简单介绍一下:
注意内核版本!
extern unsigned long __force_order;
static inline unsigned long native_read_cr0(void){
unsigned long val;
asm volatile("mov %%cr0,%0\n\t" : "=r" (val), "=m" (__force_order));
return val;
}
| 字段 | 二进制 | 十进制 | 十六进制 |
|---|---|---|---|
| vaddr | 1111 1111 1111 1111 1000 1100 1010 1010 1111 0100 1001 0111 1011 0000 0000 0000 | / | 0xffff8caaf497b000 |
| PGD | 1000 1100 1 | 281*8B | 8c0 |
| PUD | 010 1010 11 | 171*8B | 558 |
| PMD | 11 0100 100 | 420*8B | d20 |
| PTE | 1 0111 1011 | 379*8B | bd8 |
dd if=/dev/mem的方式, 都可以通过物理地址进行访问. 感兴趣的可以自行查看.#include
static void watch_addr(void){
int i=0;
unsigned long addr_base = 0x00000081c6b9000;
for(; i<=25; ++i){
unsigned long addr = addr_base + i;
void *reg_base = ioremap(addr, 1);
char p = __raw_readl(reg_base);
printk("0x8%lx: %c\n", addr, p);
iounmap(reg_base);
}
}