uCore-Tutorial-Guide-2023S/source/chapter7/3disk-based-loader

222 lines
8.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

基于磁盘的 OS
================================================
本节导读
------------------------------------------------
在有了对磁盘的控制能力后,我们的 os 也随之发生了一系列的改变,包括:
- 从磁盘加载文件(之前一直通过奇怪的操作打包进内存)
- 更加完善的文件管理
- 支持命令行参数
- 支持 IO 重定向
这一节我们介绍前两点,下一节介绍后两点的实现。
从磁盘加载文件
------------------------------------------------
之前,我们都通过 pack.py 和 kernelld.py 将用户软件的镜像打包进内存。但内存并不是持久化的存储介质,一旦掉电就会失效,因此把用户程序存储在磁盘上,有需要的时候从磁盘加载才是科学的方式,我们来看看要办到这一点需要那些修改。
首先,我们不再需要 pack.py kernelld.py 等脚本,仅仅保留了一个为了测试方便的 initproc.py这个脚本的唯一作用就是插入 INIT_PROC 这个 symbol。
接下来os 主要的修改就主要集中在 bin_loader 这个函数了,:
.. code-block:: c
// 首先,不在需要传入 start, end 只需要传入对应的 inode 指针
// 所有的用户程序镜像已经打包到了磁盘里,这部分之后介绍
int bin_loader(struct inode *ip, struct proc *p)
{
ivalid(ip);
void *page;
// 不需要 pa 相关的东西,其实 start = 文件开头length = 文件大小
uint64 length = ip->size;
// va 相关设定不变
uint64 va_start = BASE_ADDRESS;
uint64 va_end = PGROUNDUP(BASE_ADDRESS + length);
for (uint64 va = va_start, off = 0; va < va_end; va += PGSIZE, off += PAGE_SIZE) {
page = kalloc();
// 之前的 memmove 变为了 readi也就是从磁盘读取
readi(ip, 0, (uint64)page, off, PAGE_SIZE);
// 由于 kalloc 会写入垃圾数据,不对其的部分还是需要手动清空
if (off + PAGE_SIZE > length) {
memset(page + (length - off), 0, PAGE_SIZE - (length - off));
}
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W | PTE_X);
}
// 其余部分不变
// map stack
// set trapframe
}
那么,用户程序是如何被打包成一个遵循上一节所说设定的磁盘镜像的呢?这就是 ``nfs/fs.c`` 的功劳了。我们可以使用 ``make -C nfs`` 来得到一个 fs.img 的镜像,对应的 ``nfs/Makefile`` 如下:
.. code-block:: Makefile
FS_FUSE := fs
$(FS_FUSE): fs.c fs.h types.h
fs.img: $(FS_FUSE)
./$(FS_FUSE) $@ $(wildcard $(U)/$(USER_BIN_DIR)/*)
可以看到 fs.c 会被编译为可执行程序 fs, 使用格式如下:
.. code-block:: console
fs 输出镜像名称 输入文件1 输入文件2 输入文件3 ...
Makfile 中,我们把 user 产生的所有 bin 文件作为输入文件。接下来我们简要解析 fs.c 的具体逻辑,同学们不需要完整的理解这段程序的逻辑,但想要正确的完成 lab7 的实验,必须对 fs.c 进行对应的修改。在 exercise 章节会有一定提示。
.. code-block:: c
// 一些全局常量定义
int NINODES = 200; // 文件系统设定的 inode 数量
int nbitmap = FSSIZE / (BSIZE * 8) + 1; // bitmap block 的数量
int ninodeblocks = NINODES / IPB + 1; // inode block 的数量
int nmeta; // meta 数据块的数量(含义下方解释)
int nblocks;
// 一些全局变量定义
int fsfd; // 输出镜像文件的 fd
struct superblock sb; // 超级块
char zeroes[BSIZE]; // BSIZE 大小的空 buf用来清空某一个磁盘块
uint freeinode = 1; // 表示还空闲的 inode每使用一个 inode 需要 +1
uint freeblock; // 表示还空闲的 block每使用一个 block 需要 +1
int main(int argc, char *argv[])
{
int i, cc, fd;
uint rootino, inum, off;
struct dirent de;
char buf[BSIZE];
struct dinode din;
// 至少需要输入镜像的名称
if (argc < 2) {
fprintf(stderr, "Usage: mkfs fs.img files...\n");
exit(1);
}
// 创建输出文件
fsfd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
// [ boot block | sb block | inode blocks | free bit map | data blocks ]
// meta data 包括: boot block, superblock, inode block, free bitmap
nmeta = 2 + ninodeblocks + nbitmap;
// nblocks 表示存放数据剩余的块数
nblocks = FSSIZE - nmeta;
// superblock 初始化
// xint() 的功能仅仅是转换字节顺序,大家可以忽略这个细节
sb.magic = FSMAGIC;
sb.size = xint(FSSIZE);
sb.nblocks = xint(nblocks);
sb.ninodes = xint(NINODES);
sb.inodestart = xint(2);
sb.bmapstart = xint(2 + ninodeblocks);
// 目前还空闲的的块,目前我们只占用了 meta data 的那些块
// 之后没使用一块freeblock 需要 +1。
freeblock = nmeta;
// wsect 会写输出镜像,把第 i 个块写为 buf这里首先清空一遍
for (i = 0; i < FSSIZE; i++)
wsect(i, zeroes);
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
// 0 号块不处理
// 1 号快写为之前设定好的 superblock
wsect(1, buf);
// ialloc() 会分配一个空的 inode并初始化 type返回 inode id
rootino = ialloc(T_DIR);
// 从第二个参数开始都是需要打包的用户程序
for (i = 2; i < argc; i++) {
// 获得 basename这是为了去掉前缀 "../user/target/bin/"
char *shortname = basename(argv[i]);
assert(index(shortname, '/') == 0);
// 打开对应的用户文件
if ((fd = open(argv[i], 0)) < 0) {
perror(argv[i]);
exit(1);
}
// 为每一个用户程序分配一个 inode
inum = ialloc(T_FILE);
// 为每一个用户程序分配一个根目录中的目录项
bzero(&de, sizeof(de));
de.inum = xshort(inum);
strncpy(de.name, shortname, DIRSIZ);
// 把该目录项写入根目录的数据块
// iappend 会像某一个 inode 对应的数据块后 append 数据
iappend(rootino, &de, sizeof(de));
// 读取该程序的数据并写入对应 inode 的数据块
while ((cc = read(fd, buf, sizeof(buf))) > 0)
iappend(inum, buf, cc);
close(fd);
}
// 更新 rootdir inode 对应的 size 数据,按数据快大小取整
rinode(rootino, &din);
off = xint(din.size);
off = ((off / BSIZE) + 1) * BSIZE;
din.size = xint(off);
winode(rootino, &din);
// balloc 完成 bitmap 的填写,把 [0, freeblock) 的块标记为已经使用
balloc(freeblock);
return 0;
}
更加完善的文件管理
----------------------------------------
ch7 规范化了对于 fd 的管理,比如 0, 1, 2 不再是保留给 stdio 文件的 fd 参数,而是会在进程创建的时候给进程 3 个默认文件。为什么要这么做?这使得不需要的时候我们可以关闭 stdio 文件:
.. code-block:: c
// allocproc() 之后必须执行 init_stdio_files 来初始化 files
int init_stdio_files(struct proc *p)
{
for (int i = 0; i < 3; i++) {
p->files[i] = stdio_init(i);
}
return 0;
}
struct file *stdio_init(int fd)
{
struct file *f = filealloc();
f->type = FD_STDIO;
f->ref = 1;
f->readable = (fd == STDIN || fd == STDERR);
f->writable = (fd == STDOUT || fd == STDERR);
return f;
}
同时 close 0,1,2 号文件不会失败并返回 -1文件读写也不会直接根据 fd 判断,例如 sys_write:
.. code-block:: c
uint64 sys_write(int fd, uint64 va, uint64 len)
{
if (fd < 0 || fd > FD_BUFFER_SIZE)
return -1;
struct proc *p = curr_proc();
struct file *f = p->files[fd];
switch (f->type) {
case FD_STDIO:
return console_write(va, len);
case FD_PIPE:
return pipewrite(f->pipe, va, len);
case FD_INODE:
return inodewrite(f, va, len);
default:
panic("unknown file type %d\n", f->type);
}
}
之后我们会看到,这使得文件重定向成为了可能, // TODO