抄写Linux源码(Day16:内存管理)
回忆我们需要做的事情:
为了支持 shell 程序的执行,我们需要提供:
1.缺页中断(不理解为什么要这个东西,只是闪客说需要,后边再说)
2.硬盘驱动、文件系统 (shell程序一开始是存放在磁盘里的,所以需要这两个东西)
3.fork,execve, wait 这三个系统调用,也可以说是 进程调度 (否则无法 halt shell 程序并且启动另外的程序)
4.键盘驱动、VGA/console/uart 驱动、中断处理 (支持键盘输入和屏幕显示)
5.内存管理 (shell 启动其它进程时,不能共用内存,而是切换其它进程的页表)
6.为了写代码方便,我们需要从 MBR 进入到 main 函数,这也是从 汇编 切换到 C 语言 — 已经完成
7.应用程序申请内存的接口
现在已经进入 main 函数了,那么,进入 main 函数后我们要怎么实现上面提到的,还没完成的 6 个要求呢?我们要实现它们才能启动 shell、
用户空间需要有内存管理机制。同样的,内核空间的内存也需要管理,比如我们需要给磁盘分配高速缓存,为了方便管理内核空间内存,我们会去实现如 kfree 和 kalloc 之类的内核函数。
继续看闪客文章第12回
https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247500061&idx=1&sn=6cb3382d7ac35ebeac52bbba3a89db4e&chksm=c2c5bbb0f5b232a6e1b2c7b1f55a7b7057d29ea11348068e122a03b75db220ffe19ea1e8fc24&scene=178&cur_album_id=2123743679373688834#rd
书接上回,上回书咱们回顾了一下 main.c 函数之前我们做的全部工作,给进入 main 函数做了一个充分的准备。
那今天我们就话不多说,从 main 函数的第一行代码开始读。
还是把 main 的全部代码都先写出来,很少。
void main(void) {ROOT_DEV = ORIG_ROOT_DEV;drive_info = DRIVE_INFO;memory_end = (1<<20) + (EXT_MEM_K<<10);memory_end &= 0xfffff000;if (memory_end > 16*1024*1024)memory_end = 16*1024*1024;if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024;else if (memory_end > 6*1024*1024)buffer_memory_end = 2*1024*1024;elsebuffer_memory_end = 1*1024*1024;main_memory_start = buffer_memory_end;mem_init(main_memory_start,memory_end);trap_init();blk_dev_init();chr_dev_init();tty_init();time_init();sched_init();buffer_init(buffer_memory_end);hd_init();floppy_init();sti();move_to_user_mode();if (!fork()) { /* we count on this going ok */init();}for(;;) pause();
}
我们今天就看这第一小段。
首先,ROOT_DEV 为系统的根文件设备号,DRIVE_INFO 为之前 setup.s 程序获取并存储在内存 0x90000 处的设备信息,我们先不管这俩,等之后用到了再说。
我们看后面这一坨很影响整体画风的一段代码。
void main(void) {...memory_end = (1<<20) + (EXT_MEM_K<<10);memory_end &= 0xfffff000;if (memory_end > 16*1024*1024)memory_end = 16*1024*1024;if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024;else if (memory_end > 6*1024*1024)buffer_memory_end = 2*1024*1024;elsebuffer_memory_end = 1*1024*1024;main_memory_start = buffer_memory_end;...
}
这一坨代码和后面规规整整的 xxx_init 平级的位置,要是我们这么写代码,肯定被老板批评,被同事鄙视了。但 Linus 写的,就是经典,学就完事了。
这一坨代码虽然很乱,但仔细看就知道它只是为了计算出三个变量罢了。
main_memory_start
memory_end
buffer_memory_end
而观察最后一行代码发现,其实两个变量是相等的,所以其实仅仅计算出了两个变量。
main_memory_start
memory_end
然后再具体分析这个逻辑,其实就是一堆 if else 判断而已,判断的标准都是 memory_end 也就是内存最大值的大小,而这个内存最大值由第一行代码可以看出,是等于 1M + 扩展内存大小。(即,内存最小得有 1M)
那 ok 了,其实就只是针对不同的内存大小,设置不同的边界值罢了,为了理解它,我们完全没必要考虑这么周全,就假设总内存一共就 8M 大小吧。
那么如果内存为 8M 大小,memory_end 就是
8 * 1024 * 1024
也就只会走倒数第二个分支,那么 buffer_memory_end 就为
2 * 1024 * 1024
那么 main_memory_start 也为
2 * 1024 * 1024
那这些值有什么用呢?一张图就给你说明白了。
(我们之前把 system 放在 0x0,把栈指针放在 0x9FF00,所以可以认为,内核程序占用内存为 1M)
你看,其实就是定了三个箭头所指向的地址的三个边界变量,具体主内存区是如何管理和分配的,要看下面代码的功劳。
void main(void) {...mem_init(main_memory_start, memory_end);...
}
而缓冲区是如何管理和分配的,就要看
void main(void) {...buffer_init(buffer_memory_end);...
}
是如何折腾的了。
那我们今天就不背着这两个负担了,仅仅需要知道这三个参数的计算,以及后面是为谁效力的,就好啦,是不是很轻松?后面我们再讲,如何利用这三个参数,来做到内存的管理。
预知后事如何,且听下会分解。
看闪客文章 “操作系统就用一张大表管理内存?”
今天我们不聊具体内存管理的算法,我们就来看看,操作系统用什么样的一张表,达到了管理内存的效果。
我们以 Linux 0.11 源码为例,发现进入内核的 main 函数后不久,有这样一坨代码。
void main(void) {...memory_end = (1<<20) + (EXT_MEM_K<<10);memory_end &= 0xfffff000;if (memory_end > 16*1024*1024)memory_end = 16*1024*1024;if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024;else if (memory_end > 6*1024*1024)buffer_memory_end = 2*1024*1024;elsebuffer_memory_end = 1*1024*1024;main_memory_start = buffer_memory_end;mem_init(main_memory_start,memory_end);...
}
除了最后一行外,前面的那一大坨的作用很简单。
其实就只是针对不同的内存大小,设置不同的边界值罢了,为了理解它,我们完全没必要考虑这么周全,就假设总内存一共就 8M 大小吧。
那么如果内存为 8M 大小,memory_end 就是
8 * 1024 * 1024
也就只会走倒数第二个分支,那么 buffer_memory_end 就为
2 * 1024 * 1024
那么 main_memory_start 也为
2 * 1024 * 1024
你仔细看看代码逻辑,看是不是这样?
当然,你不愿意细想也没关系,上述代码执行后,就是如下效果而已。
你看,其实就是定了三个箭头所指向的地址的三个边界变量。具体主内存区是如何管理和分配的,要看 mem_init 里做了什么。
void main(void) {...mem_init(main_memory_start, memory_end);...
}
而缓冲区是如何管理和分配的,就要看再后面的 buffer_init 里干了什么。
void main(void) {...buffer_init(buffer_memory_end);...
}
不过我们今天只看,主内存是如何管理的,很简单,放轻松。
进入 mem_init 函数。
#define LOW_MEM 0x100000
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = { 0, };// start_mem = 2 * 1024 * 1024
// end_mem = 8 * 1024 * 1024
void mem_init(long start_mem, long end_mem)
{int i;HIGH_MEMORY = end_mem;for (i=0 ; i<PAGING_PAGES ; i++)mem_map[i] = USED;i = MAP_NR(start_mem);end_mem -= start_mem;end_mem >>= 12;while (end_mem-->0)mem_map[i++]=0;
}
发现也没几行,而且并没有更深的方法调用,看来是个好欺负的方法。
仔细一看这个方法,其实折腾来折腾去,就是给一个 mem_map 数组的各个位置上赋了值,而且显示全部赋值为 USED 也就是 100,然后对其中一部分又赋值为了 0。
赋值为 100 的部分就是 USED,也就表示内存被占用,如果再具体说是占用了 100 次,这个之后再说。剩下赋值为 0 的部分就表示未被使用,也即使用次数为零。
是不是很简单?就是准备了一个表,记录了哪些内存被占用了,哪些内存没被占用。这就是所谓的“管理”,并没有那么神乎其神。
那接下来自然有两个问题,每个元素表示占用和未占用,这个表示的范围是多大?初始化时哪些地方是占用的,哪些地方又是未占用的?
还是一张图就看明白了,我们仍然假设内存总共只有 8M。
可以看出,初始化完成后,其实就是 mem_map 这个数组的每个元素都代表一个 4K 内存是否空闲(准确说是使用次数)。
4K 内存通常叫做 1 页内存,而这种管理方式叫分页管理,就是把内存分成一页一页(4K)的单位去管理。
1M 以下的内存这个数组干脆没有记录,这里的内存是无需管理的,或者换个说法是无权管理的,也就是没有权利申请和释放,因为这个区域是内核代码所在的地方,不能被“污染”。
1M 到 2M 这个区间是缓冲区,2M 是缓冲区的末端,缓冲区的开始在哪里之后再说,这些地方不是主内存区域,因此直接标记为 USED,产生的效果就是无法再被分配了。
2M 以上的空间是主内存区域,而主内存目前没有任何程序申请,所以初始化时统统都是零,未来等着应用程序去申请和释放这里的内存资源。
那应用程序如何申请内存呢?我们本讲不展开,不过我们简单展望一下,看看申请内存的过程中,是如何使用 mem_map 这个结构的。
在 memory.c 文件中有个函数 get_free_page(),用于在主内存区中申请一页空闲内存页,并返回物理内存页的起始地址。
比如我们在 fork 子进程的时候,会调用 copy_process 函数来复制进程的结构信息,其中有一个步骤就是要申请一页内存,用于存放进程结构信息 task_struct。
int copy_process(...) {struct task_struct *p;...p = (struct task_struct *) get_free_page();...
}
我们看 get_free_page 的具体实现,是内联汇编代码,看不懂不要紧,注意它里面就有 mem_map 结构的使用。
unsigned long get_free_page(void) {register unsigned long __res asm("ax");__asm__("std ; repne ; scasb\n\t""jne 1f\n\t""movb $1,1(%%edi)\n\t""sall $12,%%ecx\n\t""addl %2,%%ecx\n\t""movl %%ecx,%%edx\n\t""movl $1024,%%ecx\n\t""leal 4092(%%edx),%%edi\n\t""rep ; stosl\n\t""movl %%edx,%%eax\n""1:":"=a" (__res):"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),"D" (mem_map + PAGING_PAGES-1):"di","cx","dx");return __res;
}
就是选择 mem_map 中首个空闲页面,并标记为已使用。
好了,本讲就这么多,只是填写了一张大表而已,简单吧?之后的内存申请与释放等骚操作,统统是跟着张大表 mem_map 打交道而已,你一定要记住它哦。
看完了闪客文章 “操作系统就用一张大表管理内存?”
TODO:here
相关文章:

抄写Linux源码(Day16:内存管理)
回忆我们需要做的事情: 为了支持 shell 程序的执行,我们需要提供: 1.缺页中断(不理解为什么要这个东西,只是闪客说需要,后边再说) 2.硬盘驱动、文件系统 (shell程序一开始是存放在磁盘里的,所以需要这两个东…...

Cookie和Session详解以及结合生成登录效果
目录 引言 1.Cookie中的数据从哪来数据长啥样? 2.Cookie有什么作用? 3.cookie与session的工作关联? 4.Cookie到哪去? 5.Cookie如何存? 6.Session 7.Cookie与Session的关联与区别 8.通过代码理解 8.1 相关代码 8.2…...

Spring基础以及核心概念(IoC和DIQ)
1.Spring是什么 Spring是包含了众多工具方法的IoC容器 2.loC(Inversion of Control )是什么 IoC:控制反转,Spring是一个控制反转容器(控制反转对象的生命周期) Spring是一个loC容器,我们之前学过的List/Map就是数据存储的容器,to…...
《C和指针》笔记32:多维数组初始化
文章目录 使用括号进行初始化初始化省略维度 使用括号进行初始化 我们可以给数组赋值一个长长的列表: int matrix[2][3] { 100, 101, 102, 110, 111, 112 };它等价于 matrix[0][0]100; matrix[0][1]101; matrix[0][2]102; matrix[1][0]110; matrix[1][1]111; ma…...

零食食品经营小程序商城的作用是什么
零食几乎可以涵盖每个年龄阶段,同时又是市场中常见的零售批发商品,在多个场景中都有销售/购买属性,对消费者来说,购买零食的渠道多种多样,无论线下还是线上,都可随心而购。 庞大市场升级促进下,…...
Java泛型--什么是泛型?
https://www.bilibili.com/video/BV1xJ411n77R?p5&vd_sourcebb1fced25254581cf052adea5e87a1ff 1.泛型类、接口 1.1.泛型类 泛型类的定义 class 类名称 <泛型标识, 泛型标识, ...> {private 泛型标识 变量名;...... }常用的泛型标识:T、E、K、V jav…...

LabVIEW工业虚拟仪器的标准化实施
LabVIEW工业虚拟仪器的标准化实施 创建计算机化的测试和测量系统,从计算机桌面控制外部测量硬件设备,以及在计算机屏幕上显示的类似仪器的面板上查看来自外部设备的测试或测量数据,所有这些都需要虚拟仪器系统软件。该软件允许用户执行所有这…...

JavaScript系列从入门到精通系列第十七篇:JavaScript中的全局作用域
文章目录 前言 1:什么叫作用域 一:全局作用域 1:全局变量的声明 2:变量声明和使用的顺序 3:方法声明和使用的顺序 前言 1:什么叫作用域 可以起作用的范围 function fun(){var a 1; } fun();consol…...

汇编指令集合
...

TinyWebServer整体流程
从main主函数开始: 一、定义MySQL数据库的账号、密码和用到的数据库名称。 二、调用Config获得服务器初始化属性 在这一步确定触发模式端口等信息。 三、创建服务器实例对象 设置根目录、开辟存放http连接对象的空间,开辟定时器空间。 四、利用Confi…...

【Java项目推荐之黑马头条】自媒体文章实现异步上下架(使用Kafka中间件实现)
自媒体文章上下架功能完成 需求分析 流程说明 接口定义 说明接口路径/api/v1/news/down_or_up请求方式POST参数DTO响应结果ResponseResult DTO Data public class WmNewsDto {private Integer id;/*** 是否上架 0 下架 1 上架*/private Short enable;}ResponseResult 自媒…...

自学(黑客)技术方法————网络安全
如果你想自学网络安全,首先你必须了解什么是网络安全!,什么是黑客!! 1.无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面性,例如 Web 安全技术,既有 Web 渗透2.也有 Web 防…...
python+playwright 学习-84 Response 接口返回对象
Response 是获取接口响应对象,根据Response 对象可以获取响应的状态码,响应头部,响应正文等内容。 Response 相关操作方法 all_headers 所有响应HTTP标头, 返回Dict 类型 response.all_headers()body 获取 bytes 类型body内容 response.body()json 返回响应主体的 JS…...

GCN详解
a ⃗ \vec{a} a 向量 a ‾ \overline{a} a 平均值 a ‾ \underline{a} a下横线 a ^ \widehat{a} a (线性回归,直线方程) y尖 a ~ \widetilde{a} a a ˙ \dot{a} a˙ 一阶导数 a \ddot{a} a 二阶导数 H(l)表示l层的节点的特征 W(l)表示l层的参数 D ~ \widet…...

总结二:linux面经
文章目录 1、 Linux中查看进程运行状态的指令、查看内存使用情况的指令、tar解压文件的参数。2、文件权限怎么修改?3、说说常用的Linux命令?4、说说如何以root权限运行某个程序?5、 说说软链接和硬链接的区别?6、说说静态库和动态…...
12、【Qlib】【主要组件】Qlib Recorder:实验管理
11、【Qlib】【主要组件】Qlib Recorder:实验管理 简介Qlib RecorderExperiment ManagerExperimentRecorderRecord Template简介 Qlib包含一个名为QlibRecorder的实验管理系统,旨在帮助用户以高效的方式处理实验并分析结果。 该系统有三个组件: 实验管理器(ExperimentMan…...

三一充填泵:煤矿矸石无害化充填,煤炭绿色高效开采的破局利器
富煤贫油少气是我国的能源禀赋特征,决定了我国以煤炭为主的能源结构,煤炭为国民经济发展提供了重要的基础。煤炭开采过程会对土地、地下水、空气等环境造成较大的污染,但大宗固废煤矸石无害化充填的技术手段可以有效改善这样的情况࿰…...

医疗器械标准目录汇编2022版共178页(文中附下载链接!)
为便于更好地应用医疗器械标准,国家药监局医疗器械标准管理中心组织对现行1851项医疗器械国家和行业标准按技术领域,编排形成《医疗器械标准目录汇编(2022版)》 该目录汇编分为通用技术领域和专业技术领域两大类,通用…...

C#和Excel文件的读写交互
C#和Excel文件的读写交互是一项重要的技术,在许多应用程序开发中起着关键作用。C#作为一种现代的面向编程语言,提供了丰富的库和功能,使开发人员能够轻松地处理Excel文件,并进行数据的读取和写入。 首先,让我们了解一下…...

Pytorch目标分类深度学习自定义数据集训练
目录 一,Pytorch简介; 二,环境配置; 三,自定义数据集; 四,模型训练; 五,模型验证; 一,Pytorch简介; PyTorch是一个开源的Python机…...

龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
鸿蒙(HarmonyOS5)实现跳一跳小游戏
下面我将介绍如何使用鸿蒙的ArkUI框架,实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...

CSS 工具对比:UnoCSS vs Tailwind CSS,谁是你的菜?
在现代前端开发中,Utility-First (功能优先) CSS 框架已经成为主流。其中,Tailwind CSS 无疑是市场的领导者和标杆。然而,一个名为 UnoCSS 的新星正以其惊人的性能和极致的灵活性迅速崛起。 这篇文章将深入探讨这两款工具的核心理念、技术差…...

【QT控件】显示类控件
目录 一、Label 二、LCD Number 三、ProgressBar 四、Calendar Widget QT专栏:QT_uyeonashi的博客-CSDN博客 一、Label QLabel 可以用来显示文本和图片. 核心属性如下 代码示例: 显示不同格式的文本 1) 在界面上创建三个 QLabel 尺寸放大一些. objectName 分别…...
Linux信号保存与处理机制详解
Linux信号的保存与处理涉及多个关键机制,以下是详细的总结: 1. 信号的保存 进程描述符(task_struct):每个进程的PCB中包含信号相关信息。 pending信号集:记录已到达但未处理的信号(未决信号&a…...