1、 需要配合其他代码一起看,才能有深刻的理解。
[432页]
9-5 ramdisk.c程序
9-5-1 功能描述
本文件是内存虚拟盘(Ram Disk)驱动程序,由Theodore Ts’o编制。虚拟盘设备是一种利用物理内存来模拟实际磁盘存储数据的方式。其目的主要是为了提高对"磁盘"数据的读写操作速度。除了需要占用一些宝贵的内存资源外,其主要缺点是一旦系统崩溃或关闭,虚拟盘中的所有数据将全部消失。因此虚拟盘中通常存放一些系统命令等常用工具程序或临时数据,而非重要的输入文档。
当在linux/Makefile文件中定义了常量RAMDISK,内核初始化程序就会在内存中划出一块该常量值指定大小的内存区域用于存放虚拟盘数据。虚拟盘在物理内存中所处的具体位置是在内核初始化阶段确定的(init/main.c,123行),它位于内核高速缓冲区和主内存区之间。若运行的机器含有16MB的物理内存,那么虚拟盘区域会被设置在内存4MB开始处,虚拟盘容量即等于RAMDISK的值(KB)。若RAMDISK=512,则此时内存情况如图9-6所示。
图9-6
对虚拟盘设备的读写访问操作原则上完全按照对普通磁盘的操作进行,也需要按照块设备的访问方式对其进行读写操作。由于在实现上不牵涉与外部控制器或设备进行同步操作,因此其实现方式比较简单。对于数据在系统与设备之间的传送只需执行内存数据块复制操作即可。
本程序包含3个函数。
rd_init()会在系统初始化时被init/main.c程序调用,用于确定虚拟盘在
物理内存中的具体位置和打;
do_rd_request()是虚拟盘设备的请求项操作函数,对当前请求
项实现虚拟盘数据的访问操作;
rd_load()是虚拟盘根文件加载函数。在系统初始化阶段,该函数被用于尝试从启动
引导盘上指定的磁盘块位置开始处把一个根文件系统加载到虚拟盘中。
在函数中,这个起始磁盘块位置被定为256.当然你也可以根据自己的具体要求修改这个值,只要保证这个值所规定的磁盘容量能容纳内核映射文件即可。这样一个由内核引导映像文件(Bootimage)加上根文件系统映像(Rootimage)组合而成的"二合一"磁盘,就可以想启动DOS系统盘那样来启动Linux系统。关于这种组合盘(集成盘)的制作方式可参见第14章中相关内容。
在进行正常的根文件系统加载之前,系统会首先执行rd_load()函数,试图从磁盘的第257块中读取根文件系统超级块。若成功,就把该根文件映像文件读到内存虚拟盘中,并把根文件系统设备标志ROOT_DEV设置为虚拟盘设备(0x0101),否则退出rd_load(),系统继续从别的设备上执行根文件加载操作。
操作流程如图9-7所示
如果在编译Linux0.12内核源码代码时,在其linux/Makefile配置文件中定义了RAMDISK的大小,则内核代码在引导并初始化RAMDISK区域后就会首先尝试检测启动盘上的第256磁盘块(每个磁盘块为1KB,即2个扇区)开始处是否存在一个根文件系统。检测方法是判断第257磁盘块中是否存在一个有效的文件系统超级块信息。如果有,则将该文件系统加载到RAMDISK区域中,并将其作为根文件系统使用。从而我们就可以使用一张集成了根文件系统的启动盘来引导系统到shell命令提示符状态。若启动盘上指定磁盘块位置(第256磁盘块)上没有存放一个有效的根文件系统,那么内核就会提示插入根文件系统盘。在用户按下回车键确认后,内核就把处于独立盘上的根文件系统整个地读入到内存的虚拟盘区域中去执行。
在一张1.44MB的内核引导启动盘上把一个基本的根文件系统放在盘的第256个磁盘块开始的地方就可以组合成一张集成盘,其结构如图9-8所示。
9-5-2 代码注释
/*
* linux/kernel/blk_drv/ramdisk.c
*
* Written by Theodore Ts'o, 12/2/91
*/
/* 由Theodore Ts'o编制,12/2/91
*/
// Theodore Ts'o (Ted Ts'o)是Linux社区中的著名人物。Linux在世界范围内的流行也有他很
// 大的功劳。早在Linux操作系统刚问世时,他就怀着极大的热情为Linux的发展提供了电子邮
// 件列表服务maillist,并在北美地区最早设立了Linux的ftp服务器站点(tsx-11.mit.edu),
// 而且至今仍为广大Linux用户提供服务。他对Linux作出的最大贡献之一是提出并实现了ext2
// 文件系统。该文件系统已成为Linux世界中事实上的文件系统标准。最近他又推出了ext3文件
// 系统,大大提高了文件系统的稳定性、可恢复性和访问效率。作为对他的推崇,第97期(2002
// 年5月)的LinuxJournal期刊将他作为了封面人物,并对他进行了采访。目前他为IBM Linux
// 技术中心工作,并从事着有关LSB (Linux Standard Base)等方面的工作。(他的个人主页是:
// http://thunk.org/tytso/)
#include <string.h> // 字符串头文件。主要定义了一些有关字符串操作的嵌入函数。
#include <linux/config.h> // 内核配置头文件。定义键盘语言和硬盘类型(HD_TYPE)可选项。
#include <linux/sched.h> // 调度程序头文件,定义了任务结构task_struct、任务0的数据,
// 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。
#include <linux/fs.h> // 文件系统头文件。定义文件表结构(file、m_inode)等。
#include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原型定义。
#include <asm/system.h> // 系统头文件。定义了设置或修改描述符/中断门等嵌入式汇编宏。
#include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。
#include <asm/memory.h> // 内存拷贝头文件。含有memcpy()嵌入式汇编宏函数。
// 定义RAM盘主设备号符号常数。在驱动程序中主设备号必须在包含blk.h文件之前被定义。
// 因为blk.h文件中要用到这个符号常数值来确定一些列的其他常数符号和宏。
#define MAJOR_NR 1
#include "blk.h"
// 虚拟盘在内存中的起始位置。该位置会在第52行上初始化函数rd_init()中确定。参见内核
// 初始化程序init/main.c,第124行。'rd'是'ramdisk'的缩写。
char *rd_start; // 虚拟盘在内存中的开始地址。
int rd_length = 0; // 虚拟盘所占内存大小(字节)。
// 虚拟盘当前请求项操作函数。
// 该函数的程序结构与硬盘的do_hd_request()函数类似,参见hd.c,294行。在低级块设备
// 接口函数ll_rw_block()建立起虚拟盘(rd)的请求项并添加到 rd的链表中之后,就会调
// 用该函数对 rd当前请求项进行处理。该函数首先计算当前请求项中指定的起始扇区对应虚
// 拟盘所处内存的起始位置addr 和要求的扇区数对应的字节长度值 len,然后根据请求项中
// 的命令进行操作。若是写命令WRITE,就把请求项所指缓冲区中的数据直接复制到内存位置
// addr处。若是读操作则反之。数据复制完成后即可直接调用 end_request() 对本次请求项
// 作结束处理。然后跳转到函数开始处再去处理下一个请求项。若已没有请求项则退出。
void do_rd_request(void)
{
int len;
char *addr;
// 首先检测请求项的合法性,若已没有请求项则退出(参见blk.h,第127行)。然后计算请
// 求项处理的虚拟盘中起始扇区在物理内存中对应的地址addr和占用的内存字节长度值len。
// 下句用于取得请求项中的起始扇区对应的内存起始位置和内存长度。其中sector << 9表示
// sector * 512,换算成字节值。CURRENT被定义为 (blk_dev[MAJOR_NR].current_request)。
INIT_REQUEST;
addr = rd_start + (CURRENT->sector << 9);
len = CURRENT->nr_sectors << 9;
// 如果当前请求项中子设备号不为1或者对应内存起始位置大于虚拟盘末尾,则结束该请求项,
// 并跳转到 repeat 处去处理下一个虚拟盘请求项。标号 repeat 定义在宏 INIT_REQUEST 内,
// 位于宏的开始处,参见blk.h文件第127行。
if ((MINOR(CURRENT->dev) != 1) || (addr+len > rd_start+rd_length)) {
end_request(0);
goto repeat;
}
// 然后进行实际的读写操作。如果是写命令(WRITE),则将请求项中缓冲区的内容复制到地址
// addr处,长度为len字节。如果是读命令(READ),则将addr开始的内存内容复制到请求项
// 缓冲区中,长度为len字节。否则显示命令不存在,死机。
if (CURRENT-> cmd == WRITE) {
(void ) memcpy(addr,
CURRENT->buffer,
len);
} else if (CURRENT->cmd == READ) {
(void) memcpy(CURRENT->buffer,
addr,
len);
} else
panic("unknown ramdisk-command");
// 然后在请求项成功后处理,置更新标志。并继续处理本设备的下一请求项。
end_request(1);
goto repeat;
}
/* 返回内存虚拟盘ramdisk所需的内存量 */
// 虚拟盘初始化函数。
// 该函数首先设置虚拟盘设备的请求项处理函数指针指向do_rd_request(),然后确定虚拟盘
// 在物理内存中的起始地址、占用字节长度值。并对整个虚拟盘区清零。最后返回盘区长度。
// 当linux/Makefile文件中设置过RAMDISK值不为零时,表示系统中会创建RAM虚拟盘设备。
// 在这种情况下的内核初始化过程中,本函数就会被调用(init/main.c,L151行)。该函数
// 的第2个参数length会被赋值成RAMDISK * 1024,单位为字节。
long rd_init(long mem_start, int length)
{
int i;
char *cp;
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // do_rd_request()。
rd_start = (char *) mem_start; // 对于16MB系统该值为4MB。
rd_length = length; // 虚拟盘区域长度值。
cp = rd_start;
for (i=0; i < length; i++) // 盘区清零。
*cp++ = '\0';
return(length);
}
/*
* 如果根文件系统设备(root device)是ramdisk的话,则尝试加载它。
* root device原先是指向软盘的,我们将它改成指向ramdisk。
*/
尝试把根文件系统加载到虚拟盘中。
// 该函数将在内核设置函数setup()(hd.c,156行)中被调用。另外,1磁盘块 = 1024字节。
// 第75行上的变量block=256表示根文件系统映像文件被存储于boot盘第256磁盘块开始处。
void rd_load(void)
{
struct buffer_head *bh; // 高速缓冲块头指针。
struct super_block s; // 文件超级块结构。
int block = 256; /* 开始于256盘块 */
int i = 1;
int nblocks; // 文件系统盘块总数。
char *cp; /* Move pointer */
// 首先检查虚拟盘的有效性和完整性。如果ramdisk的长度为零,则退出。否则显示ramdisk
// 的大小以及内存起始位置。如果此时根文件设备不是软盘设备,则也退出。
if (!rd_length)
return;
printk("Ram disk: %d bytes, starting at 0x%x\n", rd_length,
(int) rd_start);
if (MAJOR(ROOT_DEV) != 2)
return;
// 然后读根文件系统的基本参数。即读软盘块256+1、256和256+2。这里block+1是指磁盘上
// 的超级块。breada() 用于读取指定的数据块,并标出还需要读的块,然后返回含有数据块的
// 缓冲区指针。如果返回NULL,则表示数据块不可读(fs/buffer.c,322)。然后把缓冲区中
// 的磁盘超级块(d_super_block是磁盘超级块结构)复制到s变量中,并释放缓冲区。 接着
// 我们开始对超级块的有效性进行判断。 如果超级块中文件系统魔数不对,则说明加载的数据
// 块不是MINIX文件系统,于是退出。有关MINIX超级块的结构请参见文件系统一章内容。
bh = breada(ROOT_DEV,block+1,block,block+2,-1);
if (!bh) {
printk("Disk error while looking for ramdisk!\n");
return;
}
*((struct d_super_block *) &s) = *((struct d_super_block *) bh->b_data);
brelse(bh);
if (s.s_magic != SUPER_MAGIC)
/* 磁盘中没有ramdisk映像文件,退出去执行通常的软盘引导 */
return;
// 然后我们试图把整个根文件系统读入到内存虚拟盘区中。对于一个文件系统来说,其超级块
// 结构的 s_nzones 字段中保存着总逻辑块数(或称为区段数)。一个逻辑块中含有的数据块
// 数则由字段s_log_zone_size指定。因此文件系统中的数据块总数nblocks 就等于 (逻辑块
// 数 * 2^(每区段块数的次方)),即nblocks = (s_nzones * 2^s_log_zone_size)。如果遇到
// 文件系统中数据块总数大于内存虚拟盘所能容纳的块数的情况,则不能执行加载操作,而只
// 能显示出错信息并返回。
nblocks = s.s_nzones << s.s_log_zone_size;
if (nblocks > (rd_length >> BLOCK_SIZE_BITS)) {
printk("Ram disk image too big! (%d blocks, %d avail)\n",
nblocks, rd_length >> BLOCK_SIZE_BITS);
return;
}
printk("Loading %d bytes into ram disk... 0000k",
nblocks << BLOCK_SIZE_BITS);
cp = rd_start;
while (nblocks) {
if (nblocks > 2) // 若读取块数多于2块则采用超前预读。
bh = breada(ROOT_DEV, block, block+1, block+2, -1);
else // 否则就单块读取。
bh = bread(ROOT_DEV, block);
if (!bh) {
printk("I/O error on block %d, aborting load\n",
block);
return;
}
(void) memcpy(cp, bh->b_data, BLOCK_SIZE); // 复制到cp处。
brelse(bh);
printk("\010\010\010\010\010%4dk",i); // 打印加载块计数值。
cp += BLOCK_SIZE; // 虚拟盘指针前移。
block++;
nblocks--;
i++;
}
// 当boot盘中从256盘块开始的整个根文件系统加载完毕后,我们显示“done”,并把目前
// 根文件设备号修改成虚拟盘的设备号0x0101,最后返回。
printk("\010\010\010\010\010done \n");
ROOT_DEV=0x0101;
}