源码下载
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 完全注释》:
内存交换初始化
在进入到 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
函数的改变主要是因为两点:
- 虚拟内存交换功能:这一点主要体现在内存不足时尝试 swap out;页表项 P 位为 0 不代表页面不存在,可能存在于交换设备中;释放页面时,页面可能位于交换设备空间。
task_struct
结构体增加了library
成员,是程序执行时被加载的库文件的 inode 结构指针 :这一点主要体现在缺页管理,页面共享函数中,如果逻辑地址大于进程库在逻辑地址空间的起始地址LIBRARY_OFFSET
,则表明该地址位于进程使用的库文件中。- 内核管理优化,一些变量函数搬到了别的文件,如
include/linux/mm.h
。