Linux 0.12 内存管理

源码下载

Linux 源码下载路径位于 https://mirrors.edge.kernel.org/pub/linux/kernel/,这篇博客所需要的 0.12 版本源码通过点击链接 https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/old-versions/linux-0.12.tar.gz 可下载。

对比 Linux 0.11 版本 变化

Linux 从 0.12 版本开始,增加了内存交换的功能,也就是我们常说的 swap 分区。当物理内存使用紧张时,内存交换代码会将暂时不用的页面内容临时保存到磁盘中,这个页面内容是的才需要交换,不然直接释放到该页面,待到需要使用时再从执行文件映像中加载页面就可以了。这项技术相当于增大了物理内存的容量。对于 0.12 版本的内核来说,交换设备使用磁盘上的一个单独分区,分区不含文件系统。这部分代码在 mm/swap.c 中实现,这是相对于 0.11 版本一个新增的文件。下面先介绍内存交换,然后再对比 0.11 版本介绍 memory.c 文件。

内存交换

基本原理介绍

在编译内核时,如果我们定义了 SWAP_DEV,那么编译出来的内核就有内存交换的功能。在这里我们只需要知道 SWAP_DEV 是交换设备号就可以了,读写交换设备将会用到它,当然判断交换设备存不存在(即是交换内存的功能存不存在)也会用到它。内存交换是按页面来交换的,譬如将某物理内存页面交换到交换设备上,那就得知道对应交换设备上哪一页是空闲可用的,即还没有内存交换到那个位置上,这个管理内存交换是否空闲的方法和管理物理内存的方法一样,用一个数组来管理,不过内存交换使用的更加细腻,按比特位来使用数组,这也是因为它要管理尽可能多的页面。交换内存的数组在代码中名为 swap_bitmap

mm/swap.c

static char * swap_bitmap = NULL;

swap_bitmap 的大小是一页,也就是 4K,由于它以比特位来管理页面,所以它总共有多少个比特位就能最多管理多少页面,当然这还要看交换设备最多能放多少个页面了。

mm/memory.c

/* 每个字节 8 个比特位,所以左移 3 位相当于
 * 乘以 8,得到最多可以管理多少页面
 * /
#define SWAP_BITS (4096<<3)

swap_bitmap 中每一位对应着交换设备中的一页位置,位为 1 表示在交换设备上该页是空闲的,还没有页交换进来,可以用,位为 0 表示该页已经被占用了或者是无效的位置,譬如超出了交换设备分区的大小了。
swap_bitmap 位于交换设备的第一页,所以在 swap 初始化的时候会将 swap_bitmap 从交换设备读到物理内存页中。

小结

从上面的描述来看,假设在 Linux 运行时,系统块设备分区程序中(如 fdisk) 将交换设备分区大小设置为 swap_size 个交换页面,因为第一个页面已经被 swap_bitmap 占用,所以只有 swap_size - 1 个页面时真正可用的。且超出 swap_size 的位置在 swap_bitmap 中应设为 0,表示无效的位置。所以在初始时,swap_bitmap的示意图来源 《Linux 内核 0.12 完全注释》:
swap_bitmap.png

内存交换初始化

在进入到 init_swapping 函数之前,先介绍几个重要的辅助宏,因为 swap_bitmap 是比特位管理模式,C 语言又不支持直接操纵比特位,所以定义辅助宏会很方便:

mm/swap.c

#define bitop(name,op) \
static inline int name(char * addr,unsigned int nr) \
{ \
int __res; \
__asm__ __volatile__("bt" op " %1,%2; adcl $0,%0" \
:"=g" (__res) \
:"r" (nr),"m" (*(addr)),"0" (0)); \
return __res; \
}

bitop(bit,"")
bitop(setbit,"s")
bitop(clrbit,"r")

op 有三种可能输入,分别是 “”, “s”, “r”,这三个输入参数分别对应三种比特位测试指令:

  • bt %1,%2 ----- Bit Test,测试并用原值设置进位值
  • bts %1,%2 ----- Bit Test and Set,设置比特位(设为 1)并用原值设置进位值
  • btr %1,%2 ----- Bit Test and Reset,复位比特位(设为 0)并用原值设置进位值
    更加具体的内容请参阅我的另一篇博文
    下面介绍 init_swappng 函数。
mm/swap.c

void init_swapping(void)
{
	/* blk_size 这里不需要理解,涉及到文件系统,主次设备号,
	 * 只要知道 blk_size[MAJOR(SWAP_DEV)][MINOR(SWAP_DEV)]
	 * 可以得到交换设备分区数据块总数就可以,其中每一个数据块大小为 1K
	 * /
	extern int *blk_size[];
	int swap_size,i,j;
	/* 没有定义交换设备,直接返回 */
	if (!SWAP_DEV)
		return;
	/* 交换设备没有配置分区数据块大小信息,返回 */
	if (!blk_size[MAJOR(SWAP_DEV)]) {
		printk("Unable to get size of swap device\n\r");
		return;
	}
	/* 求得交换设备分区的数据块数 */
	swap_size = blk_size[MAJOR(SWAP_DEV)][MINOR(SWAP_DEV)];
	/* 块数为 0 返回 */
	if (!swap_size)
		return;
	/* 数据块过少,存不了几页,返回 */
	if (swap_size < 100) {
		printk("Swap device too small (%d blocks)\n\r",swap_size);
		return;
	}
	/* 一个数据块大小为 1K,一页为 4K,除以 4 即右移 2得到可交换页数 */
	swap_size >>= 2;
	/* 最多 SWAP_BITS 页,配置再大也没用 */
	if (swap_size > SWAP_BITS)
		swap_size = SWAP_BITS;
	/* 申请一页物理页,打算从交换分区把 swap_bitmap 读出来了,
	 * get_free_page 函数 0.12 版本放到了 mm/swap.c 中了
	 */
	swap_bitmap = (char *) get_free_page();
	/* 没内存了,伤心>_<,返回 */
	if (!swap_bitmap) {
		printk("Unable to start swapping: out of memory :-)\n\r");
		return;
	}
	/* 呀,有内存,从交换分区的第 0 页读出放到 swap_bitmap 中,至于 read_swap_page
	 * 这个函数怎么实现的,涉及到块设备,不管
	 * /
	read_swap_page(0,swap_bitmap);
	/* 交换分区的第 4086 字节起的 10 个字符一定得是 "SWAP-SPACE",
	 * 相当于 magic,不然交换分区无效
	 * /
	if (strncmp("SWAP-SPACE",swap_bitmap+4086,10)) {
		printk("Unable to find swap-space signature\n\r");
		free_page((long) swap_bitmap);
		swap_bitmap = NULL;
		return;
	}
	/* 因为 "SWAP-SPACE" 字符串中肯定含有比特 1 了,但是如果 swap_size 比 SWAP_BITS 小,
	 * 又 [swap_size, SWAP_BITS - 1] 之间是要置为无效的,也就是 0,那这 10 个字节肯定有
	 * 比特落在这个范围内啦,那它们之中有 1 那是不行的,视为配置不正确,所以干脆 menset 为 0 了,
	 * 这个纯属猜测~
	 * /
	memset(swap_bitmap+4086,0,10);
	/* 检验第 0 位和第 [swap_size, SWAP_BITS - 1] 位是否都为 0,为 1 那肯定是不行滴 */
	for (i = 0 ; i < SWAP_BITS ; i++) {
		if (i == 1)
			i = swap_size;
		if (bit(swap_bitmap,i)) {
			printk("Bad swap-space bit-map\n\r");
			free_page((long) swap_bitmap);
			swap_bitmap = NULL;
			return;
		}
	}
	/* 检验从第 [1, swap_size - 1] 有几位为 1,也就是可用页面有多少了,这个值由 j 记录 */
	j = 0;
	for (i = 1 ; i < swap_size ; i++)
		if (bit(swap_bitmap,i))
			j++;
	/* 如果 j 为 0,那就是都没有交换页,那就走不下了,返回 */
	if (!j) {
		free_page((long) swap_bitmap);
		swap_bitmap = NULL;
		return;
	}
	/* 开心通报下初始化成功消息^_^ */
	printk("Swap device ok: %d pages (%d bytes) swap-space\n\r",j,j*4096);
}

get_swap_page

下面介绍下如何申请交换页面:

mm/swap.c
/* 申请成功返回页面号,也就是 swap_bitmap 的 index,不成功则返回 0 */
static int get_swap_page(void)
{
	int nr;
	/* 日常检查,正常操作 */
	if (!swap_bitmap)
		return 0;
	/* 这里有几点,1. 这个 32768 是个啥,它是 4096 * 8,也就是 SWAP_BITS,
	 * 这里为啥不用宏呢,可能忘了吧~特地查了下 0.95 版的代码,这里是用例 SWAP_BITS 的;
	 * 2. 这里用 clrbit 进行测试,如果是 0 的话设置为 0,返回 0,相当于如果无效或已占用则对原位没影响,
	 * 有效则设置为已占用~
	 * /
	for (nr = 1; nr < 32768 ; nr++)
		if (clrbit(swap_bitmap,nr))
			return nr;
	return 0;
}

swap_free

说完了申请,那就肯定要说说释放啦,想想申请用 clrbit,那释放应该就是用 setbit 吧~

mm/swap.c
/* 要释放哪个页面,就串下页面号咯 */
void swap_free(int swap_nr)
{
	/* 检测参数是否有效 */
	if (!swap_nr)
		return;
	/* 日常检查,然后用 setbit 释放,如果之前这个页面就是 0,那就通报 */
	if (swap_bitmap && swap_nr < SWAP_BITS)
		if (!setbit(swap_bitmap,swap_nr))
			return;
	printk("Swap-space bad (swap_free())\n\r");
	return;
}

swap in

swap in 就是从交换设备中将页面换到内存啦~

mm/swap.c
/* 传入页表项指针,想想缺页了,有两个问题,1. 交换设备中有没有该页存在,那就要查,怎么查呢,就是页目录项 P 为 1,
 * 而页表项 P 为 0;2. 交换设备中有这个页了,那这个页在哪嘞,第 1 点提到页表项 P 为 0,P 为 0 时,Linus 用高 31 位
 * 作为页号~
 * /
void swap_in(unsigned long *table_ptr)
{
	int swap_nr;
	unsigned long page;
	/* 日常检查 */
	if (!swap_bitmap) {
		printk("Trying to swap in without swap bit-map");
		return;
	}
	/* 检查页是不是在交换设备中 */
	if (1 & *table_ptr) {
		printk("trying to swap in present page\n\r");
		return;
	}
	/* 查页号 */
	swap_nr = *table_ptr >> 1;
	/* 页号为 0,不正常 */
	if (!swap_nr) {
		printk("No swap page in swap_in\n\r");
		return;
	}
	/* 要 swap in 了,申请页面 */
	if (!(page = get_free_page()))
		oom();
	/* 读进来 */
	read_swap_page(swap_nr, (char *) page);
	/* 设置交换页面为空闲的,如果本来就是空闲的,表示再次从交换设备中读入相同的页面,
	 * 警告一下
	 * /
	if (setbit(swap_bitmap,swap_nr))
		printk("swapping in multiply from same page\n\r");
	/* 换进来了,当然要更新页表项指针了,不用刷新 TLB,因为之前就无效的页表项是不会在 TLB 中的;
	 * 前面说交换设备里边的页面肯定是 DIRTY 的~
	 * /
	*table_ptr = page | (PAGE_DIRTY | 7);
}

swap out

有了 swap in,那肯定就有 swap out 啦~在正式出发阅读 swap_out 函数之前,先介绍几个宏:

mm/swap.c

/*
 * We never page the pages in task[0] - kernel memory.
 * We page all other pages.
 */
/* 这个注释说的是从不会将 task[0] 的页面交换出去,因为这是内核页面 */
/* FIRST_VM_PAGE 是第一个虚拟内存页面(这个和 swap_bitmap 中的 index 没啥关系),
 * 怎么算的呢?除去第一个 task,它的下一个字节就是 64MB,也就是 task[1] 的起始,
 * 也就是 TASK_SIZE,因为每个索引对应 4K 的页面,所以第一个线性地址页面就是 TASK_SIZE / 4K 了,也就是右移 12 位;
 * LAST_VM_PAGE 呢?最大线性地址空间可寻址 4G 范围,所以最后一页也就是 4G / 4K 了,这一页是 4G 之后的那一页
 * 所以 VM_PAGES 就可以直接相减得到了(这个才是最终目的,另外两个宏只是辅助求出这个)
 */
#define FIRST_VM_PAGE (TASK_SIZE>>12)
#define LAST_VM_PAGE (1024*1024)
#define VM_PAGES (LAST_VM_PAGE - FIRST_VM_PAGE)

相关宏介绍完毕了,接下来就真正进入主题啦~

mm/swap.c

/*
 * Ok, this has a rather intricate logic - the idea is to make good
 * and fast machine code. If we didn't worry about that, things would
 * be easier.
 */
 /* 嗯~从 Linus 的注释来看,下面这个函数的逻辑有点复杂,不过这也是为了产生
  * 更好更快的机器码,且看且理解吧
  * swap_out 函数的作用是搜索整个 4G 线性地址空间,找到一个可以交换到交换设备的页面,
  * 找到一个就返回 1,否则就返回 0。
  */
int swap_out(void)
{
	/* 这里面 FIRST_VM_PAGE 和其代表意义无关,只因其值是 TASK_SIZE >> 12,
	 * 再右移 10,那么就相当于右移 22,得到任务 1 的第一个页目录索引,
	 * 从这里开始搜索整个 4G 线性地址空间,注意这个 dir_entry 是静态变量,
	 * 每次搜索从 dir_entry 开始
	 */
	static int dir_entry = FIRST_VM_PAGE>>10;
	static int page_entry = -1;
	/* counter 的初始值为 VM_PAGES */
	int counter = VM_PAGES;
	int pg_table;
	/* 搜索找到一个页目录项,这个目录项有对应的页表,也就是其 P 位是 1,找到则退出循环 */
	while (counter>0) {
		/* 得到页目录项里面的内容:页表地址 | 属性 */
		pg_table = pg_dir[dir_entry];
		/* 找到退出循环 */
		if (pg_table & 1)
			break;
		/* 没找到,页面数减去 1024,因为一个页目录项对应 1024 个页面 */
		counter -= 1024;
		/* 没找到, 页目录项索引往前走 */
		dir_entry++;
		/* 一个目录项对应 1024 个页,那么总共有 4G / (1024 * 4K) = 1024 个页目录项,
		 * 因为 dir_entry 是静态变量,它不一定是从任务 1 的第一个页目录索引项开始遍历,
		 * 而每次遍历都走 VM_PAGES 次,所以其值有可能大于或等于 1024,因此当大于等于 1024 时,
		 * 就返回开始从任务 1 的第一个页目录索引开始遍历
		 */
		if (dir_entry >= 1024)
			dir_entry = FIRST_VM_PAGE>>10;
	}
	/* pg_table 来自上面 while 循环 pg_table = pg_dir[dir_entry];
	 * 所以它是页目录项里边的内容, & 0xfffff000 得到页表基地址
	 */
	pg_table &= 0xfffff000;
	/* 顺序循环搜索,counter 表示当前搜索到的可能交换点到 4G 的剩余页面
	 * 里面的循环是这样的:上面的循环不是找到了页目录表了么(虽然这个页目录项对应
	 * 的页可能都不在内存了,需要找下一个页目录项),那么现在的这个循环就是搜索
	 * 页目录项里面的页,看哪个页可以交换出去,判断是否可以交换并如果可以交换,
	 * 交换这个动作交给另外一个叫做 try_to_swap_out 的函数来做。如果这个页目录项
	 * 的页搜索完了,那么按照上一个循环的套路搜索下一个合格的页目录项
	 */
	while (counter-- > 0) {
		/* page_entry 是一个 static 变量,其初始值为 -1 */
		page_entry++;
		/* 这个页目录项搜完了,找下个合格的页目录项 */
		if (page_entry >= 1024) {
			page_entry = 0;
		repeat:
			dir_entry++;
			if (dir_entry >= 1024)
				dir_entry = FIRST_VM_PAGE>>10;
			pg_table = pg_dir[dir_entry];
			if (!(pg_table&1))
				if ((counter -= 1024) > 0)
					goto repeat;
				else
					/* 所以页都找遍了,没有合格的,退出 while 循环,返回 0 */
					break;
			pg_table &= 0xfffff000;
		}
		/* 尝试交换,这个函数下面介绍,传进去的参数是页表项指针 */
		if (try_to_swap_out(page_entry + (unsigned long *) pg_table))
			return 1;
	}
	/* 打印交换设备空间用完了(trey_to_swap_out 失败,因为一般来说都可以找到一页交换出去 */
	printk("Out of swap-memory\n\r");
	return 0;
}

接下来当然就是 try_to_swap_out 函数了,根据上面 swap_out 的介绍,try_to_swap_out 的参数是页表项指针:

mm/swap.c
/* 尝试交换成功返回 1,失败返回 0 */
int try_to_swap_out(unsigned long * table_ptr)
{
	unsigned long page;
	unsigned long swap_nr;
	/* 取出页表项内容 */
	page = *table_ptr;
	/* 如果该页本不在内存中,那当然交换不了,返回 0 */
	if (!(PAGE_PRESENT & page))
		return 0;
	/* 检查 page 的有效性,如果 page 大于最顶端物理内存,那不属于 Linux
	 * 管理范围(mem_map),返回 0
	 */
	if (page - LOW_MEM > PAGING_MEMORY)
		return 0;
	/* 页面是脏的才有可能交换出去,干净的直接释放就好 */
	if (PAGE_DIRTY & page) {
		page &= 0xfffff000;
		/* 该页面引用数大于 1,表示是共享页面,不宜交换,返回 0 */
		if (mem_map[MAP_NR(page)] != 1)
			return 0;
		/* 否则申请一个交换页面,准备交换出去 */
		if (!(swap_nr = get_swap_page()))
			return 0;
		/* 写入交换页号 */
		*table_ptr = swap_nr<<1;
		/* 之前有效的页表项被修改了,刷新 TLB */
		invalidate();
		/* 将页面内容写进交换设备对应页面,至于咋写的,这个就不管了 */
		write_swap_page(swap_nr, (char *) page);
		/* 页面交换出去了,释放该内存页面 */
		free_page(page);
		return 1;
	}
	/* 干净页面,直接释放 */
	*table_ptr = 0;
	invalidate();
	free_page(page);
	return 1;
}

swap out 的内容到此结束了。
mm/swap.c 的内容基本介绍完毕了,只剩下一个 get_free_page 的函数,这个函数原先是在 mm/memory.c 文件的,不过现在有改动,改动也没有很大,主要就是如果分配不到,那就试试 swap_out 之后会不会分配得到,这个函数看 Linux 0.01 版本的博文就可以了。
下面的内容是 mm/memory.c 文件中的函数,由于许多函数和 0.01 版本的对比没什么变化,所以这部分函数不讲,需要学习的前往 Linux 0.01 版本的博文,下面的内容是相对于 0.11 版本内核变化的部分,0.11 相对于 0.01 变化的部分在 0.11 版本内核 中已讲解。

memory.c

memory.c 函数的改变主要是因为两点:

  1. 虚拟内存交换功能:这一点主要体现在内存不足时尝试 swap out;页表项 P 位为 0 不代表页面不存在,可能存在于交换设备中;释放页面时,页面可能位于交换设备空间。
  2. task_struct 结构体增加了 library 成员,是程序执行时被加载的库文件的 inode 结构指针 :这一点主要体现在缺页管理,页面共享函数中,如果逻辑地址大于进程库在逻辑地址空间的起始地址 LIBRARY_OFFSET,则表明该地址位于进程使用的库文件中。
  3. 内核管理优化,一些变量函数搬到了别的文件,如 include/linux/mm.h
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值