Linux 0.11源码阅读笔记-内存管理

内存管理

Linux内核使用段页式内存管理方式。

  • 内存池

物理页:物理空闲内存被划分为固定大小(4k)的页

内存池:所有空闲的物理页面形成一个内存池,并逐页分配和回收。并通过位图记录每个物理页是否空闲,位图的下标对应于物理页号。

[En]

Memory pool: all free physical pages form a memory pool and are allocated and recycled on a page-by-page basis. And record whether each physical page is free through the bitmap, and the subscript of the bitmap corresponds to the physical page number.

  • 分页内存管理

虚拟页:进程虚地址空间被划分为固定大小(4k)的页

分页内存管理:通过页目录和页表维护进程虚拟页号到物理页号的映射。设置好页目录、页表之后,虚拟地址到物理地址之间的转换通过内存管理单元(MMU)自动完成转换。若访问的虚拟页没有实际分配物理页,则放生缺页中断,内核会为其分配物理页。

  • 分段内存管理

分段:进程虚拟地址空间被划分为多个逻辑段、代码段、数据段、堆栈段等,每个段都有一个段号。过程代码不直接使用虚拟地址,而是使用段号+段内的二维逻辑地址偏移量。

[En]

Segmentation: the process virtual address space is divided into multiple logical segments, code segments, data segments, stack segments, etc., each with a segment number. The process code does not directly use the virtual address, but a two-dimensional logical address offset within the segment number + segment.

分段内存管理:通过段表维护每个段的信息,段表项包括段基址和段限长。设置好段表之后,段号+段内偏移二维逻辑地址到虚拟线性地址的转换由MMU单元自动完成。

  • 相关代码文件

page.s:仅包含内存缺页中断处理程序

memory.c:内存管理的核心文件,用于内存池的初始化操作、页目录和页表的管理和内核其他部分对内存的申请处理过程。

物理内存管理

除了内核占用的内存外,其余占用的内存由内存池管理,用于动态分配和回收。

[En]

In addition to the memory occupied by the kernel, the remaining occupied memory is managed by a memory pool for dynamic allocation and recycling.

Linux 0.11源码阅读笔记-内存管理

内存池初始化

mem_init初始化空闲内存。将空闲内存划分为4k大小页,并在位图mem_map中标记为空闲。位图中还包含物理页的引用计数,支持内存共享机制。

void mem_init(long start_mem, long end_mem)
{
    int i;

    HIGH_MEMORY = end_mem;

    # 在位图中,设置所有页面为占用状态
    for (i=0 ; i>= 12;             // 主内存区中的总页面数
    while (end_mem-->0)
        mem_map[i++]=0;         // 主内存区页面对应字节值清零
}

内存分配回收

内核代码通过get_free_page和free_page函数分配和回收物理内存页。

  • 分配

get_free_page函数用于分配物理页。在位图中查找空闲物理页,并标记为占用,然后返回一个空闲的页物理地址。

// 不要陷入代码细节
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"   // 置方向位,al(0)与对应每个页面的(di)内容比较
    "jne 1f\n\t"                    // 如果没有等于0的字节,则跳转结束(返回0).
    "movb $1,1(%%edi)\n\t"          // 1 => [1+edi],将对应页面内存映像bit位置1.
    "sall $12,%%ecx\n\t"            // 页面数*4k = 相对页面其实地址
    "addl %2,%%ecx\n\t"             // 再加上低端内存地址,得页面实际物理起始地址
    "movl %%ecx,%%edx\n\t"          // 将页面实际其实地址->edx寄存器。
    "movl $1024,%%ecx\n\t"          // 寄存器ecx置计数值1024
    "leal 4092(%%edx),%%edi\n\t"    // 将4092+edx的位置->dei(该页面的末端地址)
    "rep ; stosl\n\t"               // 将edi所指内存清零(反方向,即将该页面清零)
    "movl %%edx,%%eax\n"            // 将页面起始地址->eax(返回值)
    "1:"
    :"=a" (__res)
    :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
    "D" (mem_map+PAGING_PAGES-1)
    );
return __res;           // 返回空闲物理页面地址(若无空闲页面则返回0).

}
  • 回收

free_page函数用于释放物理页。释放物理地址addr处的物理页,并在位图中标记为未占用状态。

void free_page(unsigned long addr)
{
    // 判断地址是否在合法范围内
    if (addr < LOW_MEM) return;
    if (addr >= HIGH_MEMORY)
        panic("trying to free nonexistent page");

    addr -= LOW_MEM;
    addr >>= 12;
    if (mem_map[addr]--) return;
    mem_map[addr]=0;
    panic("trying to free free page");
}

分页内存管理

  • 多级页表

多级页表用于将虚拟页映射到物理页,进程基于多级页表管理其占用的物理内存页。

[En]

The multi-level page table is used to map the virtual page to the physical page, and the process manages the physical memory pages it occupies based on the multi-level page table.

使用单级页表实现虚拟页到物理页的映射会浪费较多的内存空间,将 单级页表划分为固定的大小(4k)的页表,并使用 页目录登记页表,从而实现 两级页表,进一步可实现 多级页表。使用多级页表的好处在于节省空闲页表占用的内存空间,当4k大小页表没有页项使用时,可以不为其申请内存空间。

Linux 0.11源码阅读笔记-内存管理
  • 线性虚拟地址翻译

线性地址可分为页目录项、页表项和页内偏移量。

[En]

Linear addresses can be divided into page catalog entries, page table entries, and intra-page offsets.

页目录项:作为下标访问页目录表项,表项记录页表信息

页表项:作为下标访问页表项,也表项记录物理页信息

页内偏移:作为物理页内偏移访问具体的物理地址单元

Linux 0.11源码阅读笔记-内存管理
  • 复制页表

copy_page_tables函数用于复制当前进程的页目录和页表。首先会申请内存作为页目录和也表的存储空间,然后进行复制,复制后的两个进程的目标共享实际物理内存。fork新进程程时,会调用该函数为新进程从原进程复制页表。

int copy_page_tables(unsigned long from,unsigned long to,long size)
{
    unsigned long * from_page_table;
    unsigned long * to_page_table;
    unsigned long this_page;
    unsigned long * from_dir, * to_dir;
    unsigned long nr;

    if ((from&0x3fffff) || (to&0x3fffff))
        panic("copy_page_tables called with wrong alignment");
    from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
    to_dir = (unsigned long *) ((to>>20) & 0xffc);
    size = ((unsigned) (size+0x3fffff)) >> 22;

    // 第一层循环处理页目录
    for( ; size-->0 ; from_dir++,to_dir++) {
        if (1 & *to_dir)
            panic("copy_page_tables: already exist");
        if (!(1 & *from_dir))
            continue;

        from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
        if (!(to_page_table = (unsigned long *) get_free_page()))
            return -1;  /* Out of memory, see freeing */
        *to_dir = ((unsigned long) to_page_table) | 7;
        nr = (from==0)?0xA0:1024;

        // 第二层循环处理页表
        for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
            this_page = *from_page_table;
            if (!(1 & this_page))
                continue;
            this_page &= ~2;
            *to_page_table = this_page;

            if (this_page > LOW_MEM) {
                *from_page_table = this_page;
                this_page -= LOW_MEM;
                this_page >>= 12;
                mem_map[this_page]++;   //增加物理页引用计数
            }
        }
    }
    invalidate();
    return 0;
}
  • 分配物理页

put_page函数为指定虚拟页分配物理页,并在页表中登记映射关系。

//为进程虚页分配分配物理页,主要过程
//1. 调用get_free_page分配一个物理页
//2. 调用put_page在页表中修改页项,建立虚页到物理页的映射
void get_empty_page(unsigned long address)
{
    unsigned long tmp;

    // 如果不能取得有一空闲页面,或者不能将所取页面放置到指定地址处,则显示内存不够信息。
    if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
        free_page(tmp);     /* 0 is ok - ignored */
        oom();
    }
}

//将物理页映射到地址address中
unsigned long put_page(unsigned long page,unsigned long address)
{
    unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */
    if (page < LOW_MEM || page >= HIGH_MEMORY)
        printk("Trying to put page %p at %p\n",page,address);
    if (mem_map[(page-LOW_MEM)>>12] != 1)
        printk("mem_map disagrees with %p at %p\n",page,address);

    page_table = (unsigned long *) ((address>>20) & 0xffc);
    if ((*page_table)&1)
        page_table = (unsigned long *) (0xfffff000 & *page_table);
    else {
        if (!(tmp=get_free_page()))
            return 0;
        *page_table = tmp|7;
        page_table = (unsigned long *) tmp;
    }

    page_table[(address>>12) & 0x3ff] = page | 7;   //登记页表项
/* no need for invalidate */
    return page;
}
  • 释放物理页

free_page_tables函数释放连续一到多个虚拟页,并修改页表。

int free_page_tables(unsigned long from,unsigned long size)
{
    unsigned long *pg_table;
    unsigned long * dir, nr;

    if (from & 0x3fffff)
        panic("free_page_tables called with wrong alignment");
    if (!from)
        panic("Trying to free up swapper memory space");
    size = (size + 0x3fffff) >> 22;
    dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */

    for ( ; size-->0 ; dir++) {
        if (!(1 & *dir))
            continue;
        pg_table = (unsigned long *) (0xfffff000 & *dir);  // 取页表地址
        for (nr=0 ; nr

分段内存管理

虚拟内存分为多个逻辑段、代码段、只读数据段等。不同的数据段具有不同的属性,便于管理和保护安全。

[En]

Virtual memory is divided into multiple logical segments, code segments, read-only data segments and so on. Different data segments have different attributes, which is convenient to manage and protect security.

全局描述符表(GDT)和局部描述符表(LDT)用于记录段信息,包含段基址和段限长等。GDT用于记录内核使用的各种数据段,仅有一个;LDT用于记录进程使用的各种数据段,一个进程对应一个。

寄存器GDTR和LDTR分别用于存储GDT首地址和当前运行进程的LDT首地址。运行于用户态时,地址翻译使用LDTR寄存器指向的进程段表;运行于内核态时,地址翻译使用LDTR寄存器指向的内核段表。

Linux 0.11源码阅读笔记-内存管理

段页式内存管理

前面分别介绍了分页内存管理和分段内存管理,及两者各自地址翻译过程,此处总结linux段页式内存翻译的整个流程,并介绍一些相关的寄存器和TLB快表。

地址翻译过程主要分为两个部分:段+偏移二维逻辑地址转化为虚拟线性地址;虚拟线性地址转化为物理地址。第一部分翻译过程依赖数据结构GDT或LDT,其中记录了段信息;第二部分翻译过程依赖页表数据结构,记录了虚拟页到物理页的映射关系,CR3寄存器存储当前进程页目录地址。

  • MMU:设置好寄存器GDTR、LDTR、CR3寄存器后,MMU内存管理单元只懂执行地址翻译过程。
  • TLB:多级页表导致地址翻译过程较慢,使用TLB快表可缓存页表项,加快地址翻译过程。

Linux 0.11源码阅读笔记-内存管理

页面出错异常

缺页或者写时拷贝会都会引起页面出错异常( page_fault int14),但错处码不同。page_fault中断处理函数根据出错码调用do_no_page处理缺页中断,或者调用do_wp_page处理写时拷贝。

缺页处理

进程访问虚地址内存时,若未分配物理内存,将导致页面出错异常( page_fault int14),并调用异常处理函数 do_no_page()

do_no_page将为虚拟页分配物理页,并从磁盘调入相应数据(若该虚页对应磁盘数据)。

void do_no_page(unsigned long error_code,unsigned long address)
{
    int nr[4];
    unsigned long tmp;
    unsigned long page;
    int block,i;

    address &= 0xfffff000;
    tmp = address - current->start_code;

    if (!current->executable || tmp >= current->end_data) {
        get_empty_page(address);
        return;
    }
    if (share_page(tmp))
        return;
    if (!(page = get_free_page()))
        oom();

    //执行映像文件中(外存中),读入内存块对应的数据
    /* remember that 1 block is used for header */
    block = 1 + tmp/BLOCK_SIZE;
    for (i=0 ; iexecutable,block);
    bread_page(page,current->executable->i_dev,nr);

    //文件末尾数据可能不足一个内存块,剩下的内存空间清0
    i = tmp + 4096 - current->end_data;
    tmp = page + 4096;
    while (i-- > 0) {
        tmp--;
        *(char *)tmp = 0;
    }
    // 最后把引起缺页异常的一页物理页面映射到指定线性地址address处。若操作成功
    // 就返回。否则就释放内存页,显示内存不够。
    if (put_page(page,address))
        return;
    free_page(page);
    oom();
}

写时拷贝

fork新进程时,父子进程共享相同的物理内存页,并设置共享内存页 只读。当父子进程中的一个写共享内存时,将导致页面出错异常( page_fault int14),并调用异常处理函数 do_wp_page()处理。

do_wp_page会对共享内存页取消共享,并复制出一个新的内存页,使用父子进程各拥有一份自己的物理页面,可正常读写。

void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
    if (CODE_SPACE(address))
        do_exit(SIGSEGV);
#endif
    // 调用上面函数un_wp_page()来处理取消页面保护。
    un_wp_page((unsigned long *)
        (((address>>10) & 0xffc) + (0xfffff000 &
        *((unsigned long *) ((address>>20) &0xffc)))));

}

// 取消保护页函数
void un_wp_page(unsigned long * table_entry)
{
    unsigned long old_page,new_page;

    old_page = 0xfffff000 & *table_entry;
    if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
        *table_entry |= 2;
        invalidate();
        return;
    }

    if (!(new_page=get_free_page()))    //分配新页
        oom();
    if (old_page >= LOW_MEM)
        mem_map[MAP_NR(old_page)]--;
    *table_entry = new_page | 7;
    invalidate();
    copy_page(old_page,new_page);       //复制物理页
}

Original: https://www.cnblogs.com/lazyfiish/p/16073554.html
Author: LazyFish
Title: Linux 0.11源码阅读笔记-内存管理

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/523932/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 部署tomcat

    tomcat tomcat 一、tomcat是什么 二、tomcat部署 1.实现访问java测试网页 2.能够成功登录到tomcat首页中的host manager、server…

    Linux 2023年6月6日
    0127
  • JavaScript 的闭包(closure)

    关于JavaScript 的闭包(closure)的笔记 以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「englyf」 https://www.cnblogs.c…

    Linux 2023年6月6日
    0120
  • Git、TortoiseGit中文安装教程,如何注册Gitee账号进行代码提交,上传代码后主页贡献度没显示绿点(详解)

    今天给大家分享的是 Git 软件和 TortoiseGit 图形化软件的详细安装教程以及如何在 gitee 上进行代码提交。 首先我也是个刚接触 gitee 的一个小白用户,这些都…

    Linux 2023年6月6日
    089
  • Python 获取字典中的第一个键

    提供两种方法: 使用 list 将字典的 key 转换成列表,然后取第一个元素 [0]。如果想要最后一个 key 的话,就取最后一个元素 [-1]。 >>> my…

    Linux 2023年6月7日
    074
  • python入门基础知识六(函数)

    函数要先定义,再调用! 一、函数参数的类型: 1. 形式参数和实际参数: def funct_name(arguments): codes… codes… …

    Linux 2023年6月7日
    087
  • 文件的压缩与打包

    文件的压缩与打包 常用文件拓展名 *.tar.gz tar程序打包的文件,并且经过gzip的压缩 *.tar.bz2 tar程序打包的文件,并且经过bzip2的压缩 tar 命令,…

    Linux 2023年6月11日
    086
  • 如何快速提高英飞凌单片机编译器 TASKING TriCore Eclipse IDE 编译速度

    1、前言 使用英飞凌单片机编译器 TASKING TriCore Eclipse IDE 开发编译时,想必感受最深刻的就是编译速度,那是非常慢了,如果是部分修改的源文件编译还好,不…

    Linux 2023年6月7日
    087
  • 理想汽车 x JuiceFS:从 Hadoop 到云原生的演进与思考

    理想汽车在 Hadoop 时代的技术架构 首先简单回顾下大数据技术的发展,基于我个人的理解,将大数据的发展分了4个时期: 第一个时期: 2006 年到 2008 年。2008 年左…

    Linux 2023年6月14日
    093
  • 每周一个linux命令(tar)

    基础环境 tar命令介绍 tar命令是linux非常使用频率非常高的一个命令,比如:离线软件包的解压缩、将一个目录打包备份、将一个压缩包解压到一个指定的目录。tar命令主要用来将一…

    Linux 2023年6月8日
    093
  • zabbix监控配置

    zabbix监控配置 zabbix监控配置 zabbix通过web界面配置邮件告警 zabbix配置客户端监控 创建主机组 创建监控主机并将主机加入主机组 添加监控项 配置触发器 …

    Linux 2023年6月13日
    098
  • 小白上手Linux系统安装Tomcat教程

    1.准备阶段: 要有JDK环境,在安装好JDK后再配置tomcat,JDK安装详情在我博客中可以看到。 3.导入 进入到Xshell输入在自己的文件中(cd /home/lzh)好…

    Linux 2023年6月13日
    0105
  • 实验

    编写程序实现以下功能 编写程序,打印99乘法表 将一面额为10元倍数的整钱( 输入一行字符,统计其中单词的个数。各单词之间用空格分隔,空格数可以是多个。 输入输出示例 Input …

    Linux 2023年6月7日
    094
  • [apue] linux 文件访问权限那些事儿

    说到 linux 上的文件权限,其实我们在说两个实体,一是文件,二是进程。一个进程能不能访问一个文件,其实由三部分内容决定: 下面先简单说明一下这些基本概念,最后再说明它们是如何相…

    Linux 2023年6月6日
    098
  • bash脚本-周末定时备份mysql数据库

    1.脚本如下 #usr/bin/sh #program:用于每周日定时备份全天候mysql文件 #author:sundz #version:v1 20220521 #定义文件夹和…

    Linux 2023年6月7日
    0105
  • redis cli命令

    redis安装后,在src和/usr/local/bin下有几个以redis开头的可执行文件,称为redis shell,这些可执行文件可做很多事情。 可执行文件 作用 redis…

    Linux 2023年5月28日
    076
  • git的 .gitignore 配置概述

    学习背景:自己在使用git时发现有时会上传很多无用的配置文件,或者在项目中已经包含一个本地的git仓库,导致上一级项目上传总是报错,所以学习采用gitignore配置忽略包含的子项…

    Linux 2023年6月13日
    075
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球