linux0.11内核源码修仙传第五章——内存初始化(主存与缓存)

🚀 前言

前面已经讲了操作系统的框架,本文首先对内存的初始化代码进行讲解,包括主存与缓存。希望各位给个三连,拜托啦,这对我真的很重要!!!

目录

🏆 内存边界

在上一篇博客(linux0.11源码分析第四弹------操作系统的框架代码)中提到,最开始初始化之前有一长段的if else判断用于设定内存边界,具体代码如下所示:

c 复制代码
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;
	else
		buffer_memory_end = 1*1024*1024;
	main_memory_start = buffer_memory_end;
	...
}

这段代码设置了三个变量:memory_end, main_memory_start, buffer_memory_end,但是最后一句表明 main_memory_start = buffer_memory_end,即只设置了两个变量:memory_end以及buffer_memory_end。这一长串的if...else判断标准只有memory_end,即内存最大值的大小。

内存最大值具体是多少呢,根据上面代码里最开始的两行可以看出来,其值为1MB(1<<20)+扩展内存(EXT_MEM_K <<10)。扩展内存信息EXT_MEM_K 通过上一篇博客读取设备信号同样的方法从内存中读取,其所处位置在#define EXT_MEM_K (*(unsigned short *)0x90002)即在0x90002,根据之前内存中存放临时变量可以得到。

假设内存为8MB,memory_end的值就是8*1024*1024,首先第一个if中,并没有超过16*1024*1024,跳过,下一段if可以根据条件判断进入第二个分支,将buffer_memory_end 设置为2*1024*1024,同时main_memory_start也设置为了2*1024*1024。现在内存如下所示:

至此可以看到,内存划分为了三个部分:内核程序,缓冲区,主存区。接下来就看主存区和缓冲区的初始化。

🏆 主存初始化------mem_init

初始化主存的函数为mem_init(),其函数内容以及用到的宏定义,数组与传入参数如下所示:

c 复制代码
#define LOW_MEM 0x100000
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
static unsigned char mem_map [ PAGING_PAGES ] = {0,};
#define USED 100

// start_mem 是 main_memory_start = 2*1024*1024
// end_men 是 memory_end = 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;
}

这段代码首先是定义了HIGH_MEMORY,这个变量暂且按下不表,主要用于后面内存的申请与释放函数,接下来就是设置mem_map数组的值为USED,即100,表示这块内存不可以被使用。之前我们在进入main函数之前已经开启了分页管理(linux0.11源码分析第三弹------head.s内容),数组mem_map的作用就是管理内存中的每一页是否被使用,PAGING_PAGES就是页数,一页是4KB 。内存为15MB(为什么是15不是16呢,因为最下面的1MB用于存放内核代码,是无权也无需管理的,但是问题不大,因为这里我们假设内存为8MB),右移12位相当于除以 2^12(4KB,一页的大小),这样就是可以得到15MB里面有多少个4KB,即页的数量。

后面通过宏定义函数MAP_NR以及start_mem(即上一节中的main_memory_start)可以计算出在内存2MB处对应的页数下标,后面的end_mem同理,计算内存最后一页对应的下标,最后就是将这些地方置0(本来是USED),表示 未被使用 。用图来表示就如下图所示:

所谓的主存初始化其实就是做了两件事,第一件事就是将每一页的使用情况构建了一个数组mem_map,其次就是把2MB到内存末端(假设为8MB)清0表示未使用,等待未来有应用程序来申请和释放这里的资源。把1MB2MBUSED表示已使用,当然这部分属于缓冲区,下一节需要再管理。 注意,最后1MB是内核代码,无权也无需管理!!

🏆 缓冲区初始化------buffer_init

📃什么是缓冲区

缓冲区顾名思义,用于暂存需要使用的数据,在外部存储与内存之间充当一个准备去,说白一点就是某些交通灯处的左转待转区。

其主要作用如下:

  • 减少I/O操作频率与平衡速度差异。I/O操作频率即读写操作频率。众所周知,I/O是需要时间的,尤其是外部设备与内存之间的传输速度存在较大差异,此时就可以一次性将多个字节读入缓冲区,而不是每次都进行一次I/O操作,可以大大减少I/O操作次数。同时这样也平衡了读写速度差异,避免了高效的内存操作与低效的I/O操作之间的冲突。
  • 提高性能。通过缓存常用数据或延迟某些数据的写入操作,缓冲区能够显著提高数据处理的效率,尤其是在文件读写或网络通讯中。

📃初始化缓冲区

设置缓冲区的代码,需要用到的宏定义,变量等如下所示:

c 复制代码
int NR_BUFFERS = 0;

#define BLOCK_SIZE 1024
#define NR_BUFFERS nr_buffers
#define NR_HASH 307

// buffer_end 是 buffer_memory_end = 2*1024*1024
void buffer_init(long buffer_end)
{
	struct buffer_head * h = start_buffer;
	void * b;
	int i;

	if (buffer_end == 1<<20)
		b = (void *) (640*1024);
	else
		b = (void *) buffer_end;
	while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
		h->b_dev = 0;
		h->b_dirt = 0;
		h->b_count = 0;
		h->b_lock = 0;
		h->b_uptodate = 0;
		h->b_wait = NULL;
		h->b_next = NULL;
		h->b_prev = NULL;
		h->b_data = (char *) b;
		h->b_prev_free = h-1;
		h->b_next_free = h+1;
		h++;
		NR_BUFFERS++;
		if (b == (void *) 0x100000)
			b = (void *) 0xA0000;
	}
	h--;
	free_list = start_buffer;
	free_list->b_prev_free = h;
	h->b_next_free = free_list;
	for (i=0;i<NR_HASH;i++)
		hash_table[i]=NULL;
}	

虽然代码很长,但是主要内容在while循环中,熟悉数据结构的读者一眼就可以看出来很像在设置链表。事实也是如此,在函数第一行首先定义了链表h,代表缓冲头,其初始化的结果采用了start_buffer,又定义了一个void指针b,代表缓冲块,指向缓冲区的结尾,也就是2MB的位置。用白话来说,就是h是个链表数据结构,b链表里面存放的数据,之后整个函数就是构建链表h,同时让h里面的b_data指向对应的b。start_buffer的定义如下:

c 复制代码
extern int end;
struct buffer_head * start_buffer = (struct buffer_head *) &end;

📃内核代码区与缓冲区的分界线

代码里面使用了一个外部变量end,其并不是操作系统代码写的,是由连接器ld在链接整个程序时设置的一个外部变量,计算好整个内核代码的末尾地址,在此之前是内核代码区域。为什么上一节说1MB,是为了让大家不要纠结这个分界线,专注于理解主内存的设置。实际上内核代码区和缓冲区之间的分界线并不是固定不变的, 代码改动一点对应的分界线也会发生变化 ,因此最保险的方式就是程序编译链接时由链接器程序计算出内核代码末端地址,并作为外部变量end供我们使用。现在内存如下所示:

📃缓冲区链表

回归正题,链表定义如下所示:

c 复制代码
struct buffer_head {
	char * b_data;			// 指向实际数据块
	unsigned long b_blocknr;	// 数据块对应的下标
	unsigned short b_dev;		// 数据块所属的设备号
	unsigned char b_uptodate;	// 0 表示需要从设备重新读取,1 表示数据最新的
	unsigned char b_dirt;		// 0 表示数据未被修改,1 表示已修改,需要写回设备
	unsigned char b_count;		// 0 表示没有被使用,可被回收或重新分配
	unsigned char b_lock;		// 0 表示缓冲区可用,1 表示上锁,不可用
	struct task_struct * b_wait;	// 指向等待该缓冲区解锁的进程,当缓冲区被锁定时,其他想要访问该缓冲区的进程会被放入等待队列,通过该指针关联等待的进程。
	struct buffer_head * b_prev;
	struct buffer_head * b_next;	// 双向链表的两个指针,将正在被使用的缓冲区连接起来
	struct buffer_head * b_prev_free;
	struct buffer_head * b_next_free;	// 将当前系统中所有空闲的缓冲区连接起来,用于分配新的缓冲区
};

从结构体定义上可以看到这个链表是一个双向链表,且有两个链表,一个指向空闲的区域,一个指向被使用的区域。

📃缓冲区链表构建

先来回顾一下我们都有什么,有一个代表缓冲头的结构体h,指向的内核代码区的末端地址也就是缓冲区开头;还有一个代表缓冲块的b,是缓冲区结尾,也就是2MB。

接下来看看具体的设置,也是整个函数最多的部分,只看链表构建的过程其实做的工作也很简单,就只剩下如下的几行,其他的设置参考链表中参数的含义。

c 复制代码
void buffer_init(long buffer_end)
{
	···
	while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
		···
		h->b_data = (char *) b;
		h->b_prev_free = h-1;
		h->b_next_free = h+1;
		h++;
		if (b == (void *) 0x100000)
			b = (void *) 0xA0000;
	}
	···
}

这段代码中,缓冲区结尾b每次循环都 -1024,即一页的值,缓冲头h每次都+1(一个buffer_head的大小),直到二者相撞,即扫描完整个缓冲区。其实就是一个双指针,一个从前往后,一个从后往前,h中的b_data指向了上面的缓冲块b。值得注意一下,这里面只用了b_prev_freeb_next_free ,这是因为目前还在初始化,全是未使用的。构建如下:

循环后的三行就是将这个双向链表变为了双向循环链表,由于创建了一个free_list指向链表头结点,h此时位于链表尾结点,通过将首尾相连实现。

c 复制代码
void buffer_init(long buffer_end)
{
	···
	free_list = start_buffer;
	free_list->b_prev_free = h;
	h->b_next_free = free_list;
	···
}

构建结果如下:

至此链表构建完成,free_list指向双向链表的第一个结构,从双相链表中遍历到任何一个缓冲头结构,而通过缓冲头可以找到对应的缓冲块。

📃hashmap查找体系

这里讲一个其他的事情,缓冲区是为了文件系统服务的,如果内核程序需要访问设备中的数据,需要经过缓冲区间接操作。也就是说读取块设备的数据(硬盘中的数据),需要先读到缓冲区中,如果缓冲区已有了,就不用从块设备读取了,直接取走。

那么问题来了,如何知道我要的数据是否在缓冲区呢?一个办法就是双向链表遍历,但是效率很低,因此会构建一个hashmap进行快速查找,代码中就是最后的一个for循环:

c 复制代码
void buffer_init(long buffer_end)
{
	···
	for (i=0;i<NR_HASH;i++)
		hash_table[i]=NULL;
}

至于如何使用,等用到的时候再说(文件系统),但是哈希+双相链表 ,这不就是LRU吗,所以这个可以为之后缓冲区的使用和弃用做铺垫。

🎯总结

本文主要讲解了在操作系统初始化阶段,主存与缓存的初始化,明确了二者的分界线以及内核代码区与缓存的分界线。

📖参考资料

[1] linux源码趣读

[2] 一个64位操作系统的设计与实现

[3] 操作系统缓冲区管理(单缓冲、双缓冲、循环缓冲以及缓冲池)

[4] 缓冲区(Buffer)的概念

相关推荐
云上艺旅2 小时前
K8S学习之基础十八:k8s的灰度发布和金丝雀部署
学习·云原生·容器·kubernetes
小羊在奋斗2 小时前
【Linux网络】NAT技术、DNS系统、五种IO模型
linux·网络·智能路由器
jiarg4 小时前
linux 内网下载 yum 依赖问题
linux·运维·服务器
yi个名字4 小时前
Linux第一课
linux·运维·服务器
Kurbaneli4 小时前
深入理解 C 语言函数的定义
linux·c语言·ubuntu
Archer1944 小时前
C语言——链表
c语言·开发语言·链表
夜晚中的人海4 小时前
【C语言】------ 实现扫雷游戏
android·c语言·游戏
菜鸟xy..5 小时前
linux 基本命令教程,巡查脚本,kali镜像
linux·运维·服务器·kali镜像·巡查脚本·nmtui
暴躁的小胡!!!5 小时前
Linux权限维持之协议后门(七)
linux·运维·服务器·网络·安全