暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Linux内存问题探究(上篇)

码农的修炼之道 2021-09-14
579

    本文主要聊聊Linux系统中,内存是如何通过页表进行管理的,以及虚拟地址和实际物理地址之间关系。

  • 虚拟内存和物理内存关系。

  • 探究如何拿到实际物理内存地址。


     内存管理的知识非常庞大,比如 linux 三驾马车,CPU、IO、内存,内存可以说是这里面最复杂的,与 CPU 和 IO 的性能有着千丝万缕的关系,搞懂了内存问题,才可以真正的搞清楚很多 Linux 性能相关的问题。


一、Linux内存的管理原理

  • 虚拟内存与物理内存

      首先我们来先看看虚拟内存与物理内存,虚拟内存和物理内存的关系印证了一句名言,「操作系统中的任何问题都可以通过一个抽象的中间层来解决」,虚拟内存正是如此(在应用层和架构设计的时候也是如此)。

        没有虚拟内存,进程直接就可能修改其它进程的内存数据,虚拟内存的出现对内存使用做好了隔离,每个进程拥有独立的、连续的、统一的虚拟地址空间(好一个错觉)。像极了一个恋爱中的男人,拥有了她,仿佛拥有了全世界。

       应用程序看到的都是虚拟内存,通过 MMU 进行虚拟内存到物理内存的映射,我们知道 linux 内存是按 4k 对齐,4k = 2^12 ,虚拟地址中的低 12 位其实是一个偏移量(物理内存按页管理,一页4K)。

       现在我们把页表想象为一个一维数组,对于虚拟地址中的每一页,都分配数组的一个槽位,这个槽位指向物理地址中的真正地址。那么有这么一个虚拟内存地址 0x1234010,那 0x010 就是页内偏移量,0x1234 是虚拟页号,CPU 通过 MMU 找到 0x1234 映射的物理内存页地址,假定为 0x2b601000,然后加上页内偏移 0x010,就找到了真正的物理内存地址 0x2b601010。如下图所示。

 

  • Linux 四级页表

        但是这种方式有一个很明显的问题,虚拟地址空间可能会非常大,就算拿 32 位的系统为例,虚拟地址空间为 4GB,用户空间内存大小为 3GB,每页大小为 4kB,数组的大小为 786432(1024 * 1024)。每个页表项用 4 个字节来存储,这样 4GB 的空间映射就需要 3MB 的内存来存储映射表。(备注:这里很多资料说的是 4M,也没有太大的问题,我这里的考虑是内核空间是共用的,不用太过于纠结。)

    对于单个进程来说,占用 3M 看起来没有什么,但是页表是进程独占的,每个进程都需要自己的页表,如果有一百个进程,就会占用 300MB 的内存,这还仅仅是做地址映射所花的内存。如果考虑 64 位系统超大虚拟地址空间的情况,这种一维的数组实现的方式更加不切实际。

    为了解决这个问题,人们使用了level的概念,页表的结构分为多级,页表项的大小只与虚拟内存空间中真正使用的多少有关。之前一维数组表示的方式页表项的多少与虚拟地址空间的大小成正比,这种多级结构的方式使得没有使用的内存不用分配页表项。

    于是人们想出了多级页表的形式,这种方式非常适合,因为大部分区域的虚拟地址空间实际上是没有使用的,使用多级页表可以显著的减少页表本身的内存占用。在 64 位系统上,Linux 采用了四级页表。

 

  • PGD:Page Global Directory,页全局目录,是顶级页表。

  • PUD:Page Upper Directory,页上级目录,是第二级页表

  • PMD:Page Middle Derectory,页中间目录,是第三级页表。

  • PTE:Page Table Entry,页面表,最后一级页表,指向物理页面。


       应用程序看到的只有虚拟内存,是看不到物理地址的。当然是有办法可以通过一些手段通过虚拟地址拿到物理地址。比如我们 malloc 一个 1M 的空间,返回了一个虚拟地址 0x7ffff7eec010,怎么知道这个虚拟地址对应的物理内存地址呢?


二、如何取得实际物理内存地址

      应用层用malloc分配的内存地址是虚拟地址,我们无法通过应用层拿到实际的物理地址,只能通过内核得到实际的物理地址,原理还是通过上面的四级页表计算出实际的物理地址。

     参考网络上的示例代码,内核模块主要过程是写module_init和module_exit钩子函数。如下所示:
int my_module_init(void) {
    unsigned long pa = 0;
pgd_t *pgd = NULL;
pud_t *pud = NULL;
pmd_t *pmd = NULL;
pte_t *pte = NULL;


struct pid *p = NULL;
struct task_struct *task_struct = NULL;


    p = find_vpid(pid); //根据pid号查找进程
    if (p == NULL) {
        printk("find_vpid() return null\n");
        return -1;
}


    task_struct = pid_task(p, PIDTYPE_PID);//得到进程task_struct结构
if (task_struct == NULL) {
        printk("pid_task() return null\n");
        return -1;
}
    //下面开始通过task_struct中的mm获取pgd
pgd = pgd_offset(task_struct->mm, va);
printk("pgd_val = 0x%lx\n", pgd_val(*pgd));
printk("pgd_index = %lu\n", pgd_index(va));


if (pgd_none(*pgd)) {
printk("Not mapped in pgd.\n");
        return -1;
}
    pud = pud_offset(pgd, va);  //获得pud
printk("pud_val = 0x%lx\n", pud_val(*pud));
printk("pud_index = %lu\n", pud_index(va));


    if (pud_none(*pud)) {
printk("Not mapped in pud.\n");
        return 0;
}
    pmd = pmd_offset(pud, va); //获得pmd
printk("pmd_val = 0x%lx\n", pmd_val(*pmd));
printk("pmd_index = %lu\n", pmd_index(va));


    if (pmd_none(*pmd)) {
printk("Not mapped in pmd.\n");
        return 0;
}
    pte = pte_offset_kernel(pmd, va); //获得pte
printk("pte_val = 0x%lx\n", pte_val(*pte));
printk("pte_index = %lu\n", pte_index(va));


    if(pte_none(*pte)) {
printk("Not mapped in pte.\n");
return 0;
}
if (!pte_present(*pte)) {
printk("pte not in RAM.\n");
        return 0;
}
unsigned long page_addr = 0;
unsigned long page_offset = 0;
page_addr = pte_val(*pte) & PAGE_MASK; //页面的物理地址
    page_addr &= 0x7fffffffffffffULL; //转为无符号数字


    page_offset = va & ~PAGE_MASK; //获取线性地址偏移量
    pa = page_addr | page_offset; //实际物理地址


printk("page_addr=0x%lx,page_offset=0x%03lx",page_addr,page_offset);
printk("virtual address 0x%lx in RAM Page is 0x%lx", va, pa);
return 0;
}
然后写一个简单的makefile文件即可:
obj-m += my_mem.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
insmod:
sudo insmod my_mem.ko
rmmod:
sudo rmmod my_mem.ko
   编译生成.ko文件,然后通过insmod my_mem.ko pid=2621 va=0x7ffff7eec010加载ko文件。最后执行dmesg -T 就可以看到真正的物理地址。


三、总结

      Linux下内存是通过MMU单元对虚拟地址进行管理的,考虑到每个进程都要加载页表项,所以采用了四级页表进行管理的方式来减少实际的页表项大小,在不同的平台,四级页表也可以退化为三级、二级页表。要想得到应用层分配的实际物理地址,只能通过内核得到四级页表值,然后计算实际物理值。
文章转载自码农的修炼之道,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论