本文主要是介绍MIT 6.828 (五) Lab 5: File system, Spawn and Shell,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
重要的前面基本上已经做了,现在就剩下lab5
了。对于这个实验本身要你写的代码不多,但是要自己去看的贼他妈的多。原谅我复制粘贴别人的。
Lab 5: File system, Spawn and Shell
在本实验中,我们将实现spawn
库调用用以加载和运行磁盘上的可执行文件。然后,JOS内核和库构成的操作系统将足以在控制台上运行shell
了。这些功能需要一个文件系统,本实验将介绍了一个简单的读/写文件系统
。
切换一下分支,会多出来几个文件,文件干啥的好好看看。
fs/fs.c
操作文件系统在磁盘上的结构,理解成文件储存结构fs/bc.c
基于用户级页错误处理机制的块缓存。fs/ide.c
最小化的基于PIO(非中断驱动的)的IDE磁盘驱动。磁盘驱动知道怎么调用就行fs/serv.c
文件系统服务端代码,客户端用户环境通过IPC与之交互,你就当做内核进行系统调用吧lib/fd.c
实现通常的UNIX风格的文件描述符接口。lib/file.c
磁盘文件类型的驱动,实现为文件系统IPC客户端。lib/console.c
控制台I/O文件类型的驱动lib/spawn.c
spawn库调用实现
这些文件后面都会要看一看的不用慌。
开场让我先试试水,还能不能运行lab4
的pingpong
,primes
,forktree
,要先把kern/init.c
中的ENV_CREATE(fs_fs)
和lib/exit.c
的close_all()
注释掉。我们去看看他做了啥。
我们先看看init.c
里面的初始化函数。
void
i386_init(void)
{// Initialize the console.// Can't call cprintf until after we do this!cons_init();cprintf("6828 decimal is %o octal!\n", 6828);// Lab 2 memory management initialization functionsmem_init();// Lab 3 user environment initialization functionsenv_init();trap_init();// Lab 4 multiprocessor initialization functionsmp_init();lapic_init();// Lab 4 multitasking initialization functionspic_init();// Acquire the big kernel lock before waking up APs// Your code here:lock_kernel();// Starting non-boot CPUsboot_aps();//上个实验都是一样的,后面才开始不同// Start fs.ENV_CREATE(fs_fs, ENV_TYPE_FS);//这肯定是运行了一个 程序是啥呢?看不出来,但是根据输出//最后推断出来是 fs/serv.c 里面的umain#if defined(TEST)// Don't touch -- used by grading script!ENV_CREATE(TEST, ENV_TYPE_USER);
#else// Touch all you want.ENV_CREATE(user_icode, ENV_TYPE_USER);//这个后面再说
#endif // TEST*// Should not be necessary - drains keyboard because interrupt has given up.kbd_intr();//不用管。// Schedule and run the first user environment!sched_yield();
}
看完之后,发现就是多了一个进程而已,其他都没怎么变。那个进程应该是运行了serv.c
,我们简单看一下main
函数,具体做了啥,后面再说。
void
umain(int argc, char **argv)
{static_assert(sizeof(struct File) == 256);//检查结构binaryname = "fs";cprintf("FS is running\n");// Check that we are able to do I/Ooutw(0x8A00, 0x8A00);cprintf("FS can do I/O\n");//检查一下FSserve_init();//服务初始化fs_init();//fs初始化fs_test();//测试serve();//运行服务
}
closeall()
int
close(int fdnum)
{struct Fd *fd;int r;if ((r = fd_lookup(fdnum, &fd)) < 0)return r;elsereturn fd_close(fd, 1);
}void
close_all(void)
{int i;for (i = 0; i < MAXFD; i++)close(i);
}
应该就是关闭所有文件的意思。把这两行注释掉,就可以正常运行lab4
的测试。
后面是一大堆理论慢慢看。
File system preliminaries
我们将要实现的文件系统虽然比真实的文件系统简单的多,但是也足以提供一些基本的特性:创建、读、写、删除组织在目录结构层次中的文件。
我们现在(目前为止)只开发单用户操作系统,它提供足够的保护来捕捉错误,但不能保护可疑的用户程序之间的干扰。因此,我们的文件系统不支持文件所有权或权限的UNIX概念。我们的文件系统目前还不支持像大多数UNIX文件系统那样的硬链接,符号链接,时间戳或特殊的设备文件。
On-Disk File System Structure
大多数UNIX文件系统将可用磁盘空间分为两种主要类型的区域:inode
区域和数据区域。 UNIX文件系统为文件系统中的每个文件分配一个inode
;文件的inode
保存关于文件的关键元数据,例如其stat属性和指向其数据块的指针。数据区域被划分成更大(通常为8KB
或更多)的数据块,文件系统在其中存储文件数据和目录元数据。目录条目包含文件名和指向inode
的指针;如果文件系统中的多个目录条目引用该文件的inode
,则文件被称为硬链接。由于我们的文件系统不支持硬链接,所以我们不需要这种级别的重定向,因此可以方便的简化:我们的文件系统根本不会使用inode
,而只是在(唯一)的目录条目中存储所有的文件(或子目录)的元数据。
文件和目录逻辑上都是由一系列数据块组成的,这些数据块可能散布在整个磁盘上,就像用户环境的虚拟地址空间的页面可以分散在整个物理内存中一样。文件系统环境隐藏数据块布局的细节,仅呈现在文件任意偏移量处读/写字节序列的接口。文件系统环境将对目录的所有修改作为文件创建和删除等操作内部处理的一部分。我们的文件系统允许用户环境直接读取目录元数据(例如,read
),这意味着用户环境可以自己执行目录扫描操作(例如,实现ls
程序),而不必依赖额外特殊的对文件系统的调用。对目录扫描方法的缺点,以及大多数现代UNIX
变体阻止它的原因在于它使应用程序依赖于目录元数据的格式,使得在不更改或至少重新编译应用程序的情况下难以更改文件系统的内部布局。
简单来讲,我们文件系统就只有一个数据结构保存文件,没有索引。
Sectors and Blocks
大多数磁盘不能以字节粒度执行读取和写入,而是以扇区为单位执行读取和写入操作。在JOS中,扇区为512
字节。文件系统实际上以块为单位分配和使用磁盘存储。请注意两个术语之间的区别:扇区大小是磁盘硬件的属性,而块大小是操作系统使用磁盘的一个方面。文件系统的块大小必须是底层磁盘扇区大小的倍数。
UNIX xv6文件系统使用512
字节的块大小,与底层磁盘的扇区大小相同。然而,大多数现代文件系统使用更大的块大小,因为存储空间已经变得更便宜,并且以更大的粒度来管理存储效率更高。我们的文件系统将使用4096
字节的块大小,方便地匹配处理器的页面大小。
简单来讲,磁盘默认512字节是一个扇区,我们系统4096
字节一个块,也就是8个扇区一个块。
Superblocks
文件系统通常将某些磁盘块保留在磁盘上的“易于查找”位置(例如起始或最后),以保存描述整个文件系统属性的元数据,例如块大小,磁盘大小,找到根目录所需的任何元数据,文件系统上次挂载的时间,文件系统上次检查错误的时间等等。这些特殊块称为超级块。
我们的文件系统将只有一个超级块,它将始终位于磁盘上的块1。它的布局由struct Super
在inc/fs.h
中定义。块0通常保留用于保存引导加载程序和分区表,因此文件系统通常不使用第一个磁盘块。许多“真正的”文件系统具有多个超级块,这几个副本在磁盘的几个广泛间隔的区域,以便如果其中一个被损坏或磁盘在该区域中产生媒体错误,则仍然可以找到其他超级块,并将其用于访问文件系统。
简单来讲,块0我们用了,在前面讲过,块1就是保存了一些磁盘布局,尤其是根目录。中间可能会有一些块用于磁盘恢复,还有位图。
struct Super {uint32_t s_magic; // Magic number: FS_MAGIC 啥编号uint32_t s_nblocks; // Total number of blocks on disk 总共块数struct File s_root; // Root directory node 根目录
};
File Meta-data
描述文件系统中的文件的元数据的布局由inc/fs.h
中的struct File
定义。该元数据包括文件的名称,大小,类型(常规文件或目录)以及指向包含该文件的块的指针。如上所述,我们没有inode
,所以元数据存储在磁盘上的目录条目中。与大多数“真实”文件系统不同,为简单起见,我们将使用这个struct File
来表示在磁盘和内存中出现的文件元数据。
struct File
中的f_direc
t数组包含存储文件前10个(NDIRECT
)块的块号的空间,这前10个块被称之为文件的直接块
。对于大小为10 * 4096 = 40KB
的小文件,这意味着所有文件块的块号将直接适用于struct File
本身。然而,对于较大的文件,我们需要一个地方来保存文件的其他块号。因此,对于大于40KB的任何文件,我们分配一个额外的磁盘块,称为文件的间接块
,最多容纳4096/4 = 1024
个附加块号。因此,我们的文件系统允许文件的大小可达1034
个块,或者刚刚超过四兆字节大小。为了支持更大的文件,“真实”文件系统通常也支持双重和三重间接块。
简单来讲,储存文件我们用 struct File,小于10个块我们直接储存,超过10个块开一个间接块标记那几个块
struct File {char f_name[MAXNAMELEN]; // filename 文件名off_t f_size; // file size in bytes 文件大小uint32_t f_type; // file type 文件类型// Block pointers.// A block is allocated iff its value is != 0.uint32_t f_direct[NDIRECT]; // direct blocks 直接块uint32_t f_indirect; // indirect block 间接块// Pad out to 256 bytes; must do arithmetic in case we're compiling// fsformat on a 64-bit machine. //填满256字节,能够 64位机上运行 sizeof(struct File)刚好256.uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
} __attribute__((packed)); // required only on some 64-bit machines
Directories versus Regular Files
我们的文件系统中的struct File
可以表示常规文件或目录;这两种类型的“文件”通过struct File
中的类型字段进行区分。文件系统以完全相同的方式管理常规文件和目录文件,除了它不解释与常规文件相关联的数据块的内容,而文件系统将目录文件的内容解释为一系列描述目录中的文件和子目录的struct File
。
// File types
#define FTYPE_REG 0 // Regular file 文件
#define FTYPE_DIR 1 // Directory 目录
我们的文件系统中的超级块包含一个struct File
(其实struct Super
中的根字段),它保存文件系统根目录的元数据。根目录文件的内容是描述位于文件系统根目录下的文件和目录的struct File
序列。根目录中的任何子目录可以依次包含表示子子目录的更多的struct File
,依此类推。
简单来讲,没啥可讲,够简单了
The File System
本lab
的目标不是实现整个文件系统,而是仅实现某些关键组件。特别是,需要实现将块读入块高速缓存
并将其刷新回磁盘
;分配磁盘块
;将文件偏移映射到磁盘块
;并在IPC接口中中实现读,写和打开
。因为你不会自己实现所有的文件系统,所以你需要熟悉提供的代码和各种文件系统接口。
看到那几个打出标记的了么,重点要考的。
Disk Access
我们操作系统中的文件系统环境需要能够访问磁盘,但是我们还没有在内核中实现任何磁盘访问功能。而不是采取传统的单内核操作系统的策略将IDE磁盘驱动器添加到内核中,允许文件系统以系统调用访问它,而是将IDE磁盘驱动器作为用户级文件系统的一部分。我们仍然需要稍微修改内核,以便设置文件系统环境具有实现磁盘访问所需的权限。
用人话来讲,我们是把磁盘访问放在用户环境,让用户能访问磁盘,但是还需要改一改内核
只要我们依靠轮询,基于“可编程I/O
”(programmed I/O
, PIO
)的磁盘访问并且不使用磁盘中断,就很容易在用户空间中实现磁盘访问。也可以在用户态下实现中断驱动的设备驱动(例如,L3和L4内核),但是由于内核必须field
设备中断并将其分配到正确的用户态环境,所以更为困难。
x86处理器使用EFLAGS
寄存器中的IOPL
位来确定是否允许保护模式代码执行特殊的设备I/O指令,如IN和OUT指令。由于我们需要访问的所有IDE磁盘寄存器位于x86的I/O空间中,而不是内存映射,因此为文件系统环境提供“I/O特权”是我们唯一需要做的,以便允许文件系统访问这些寄存器。实际上,EFLAGS
寄存器中的IOPL
位为内核提供了一种简单的“全或无”方法来控制用户态代码能否访问I/O空间。在我们的实现中,我们希望文件系统环境能够访问I/O空间,但是我们不希望任何其他环境能够访问I/O
空间。
用人话来讲,设置 EFLAGS 中的IOPL 可以让用户访问 I/O
练习1
让我们实现上述问题。我们只需要添加一句话。
void
env_create(uint8_t *binary, enum EnvType type)
{// LAB 3: Your code here.struct Env * e;int r=env_alloc(&e,0);if(r!=0){cprintf("%e\n",r);panic("env_create:error");}load_icode(e,binary);e->env_type=type;// If this is the file server (type == ENV_TYPE_FS) give it I/O privileges.// LAB 5: Your code here.if(type==ENV_TYPE_FS)e->env_tf.tf_eflags|=FL_IOPL_MASK; //添加这一句即可。
}
Question 1
要不要,考虑切换用户进程的权限问题。不需要,用户环境切换的时候,eflags
寄存器的状态有CPU压入内核栈,最后由env_pop_tf
的iret
指令恢复eflags
寄存器状态。
后面告诉你两个指令可以恢复初始化。
The Block Cache
在我们的文件系统中,我们将在处理器的虚拟内存系统的帮助下实现一个简单的“缓冲区缓存
”(实际上只是块缓存
)。块缓存
的代码在fs/bc.c
中。
我们的文件系统将限于处理大小为3GB
或更小的磁盘。我们保留一个大的,固定的3GB
区域的文件系统环境的地址空间,从0x10000000
(DISKMAP
)~0xD0000000
(DISKMAP
+ DISKMAX
),作为磁盘的“内存映射”版本。例如,磁盘块0映射为虚拟地址0x10000000
,磁盘块1映射到虚拟地址0x10001000
,依此类推。fs/bc.c
中的diskaddr
函数实现了从磁盘块号到虚拟地址的转换(以及一些理性检查)。
由于我们的文件系统环境具有独立于系统中所有其他环境的虚拟地址空间的虚拟地址空间,文件系统环境唯一需要做的就是实现文件访问,因此保留大部分文件系统环境的这个地址空间。由于现代磁盘大于3GB
,因此在32
位计算机上这样实现真正的文件系统将会很尴尬。这样的缓冲区高速缓存管理方法在具有64
位地址空间的机器上仍然是合理的。
当然,将整个磁盘读入内存需要很长时间,因此我们将实现一种按需分页的形式,其中我们只在磁盘映射区域中分配页面,并从磁盘读取相应的块,以响应这个区域的页面错误。这样,我们可以假装整个磁盘都在内存中。
简单来讲,我们保留了一个3GB 的内存用来做磁盘映射,因为读取整个磁盘要很长的时间,所以我们就用分页形式。
练习2
让我实现在fs/bc.c
里面的bc_pgfault
和flush_block
。
都在一个文件里面,我们直接看看文件。
在看这个文件之前我们先看看ide.c
ide.c
/** Minimal PIO-based (non-interrupt-driven) IDE driver code.* For information about what all this IDE/ATA magic means,* see the materials available on the class references page.*/
//最小化的基于PIO(非中断驱动的)的IDE磁盘驱动。
#include "fs.h"
#include <inc/x86.h>
//下面这没有注释也不知道干啥的
#define IDE_BSY 0x80
#define IDE_DRDY 0x40
#define IDE_DF 0x20
#define IDE_ERR 0x01static int diskno = 1;static int
ide_wait_ready(bool check_error)
{int r;while (((r = inb(0x1F7)) & (IDE_BSY|IDE_DRDY)) != IDE_DRDY)/* do nothing */;//这个应该是检查磁盘是不是准备好的if (check_error && (r & (IDE_DF|IDE_ERR)) != 0)//检查是否出错?return -1;return 0;
}bool
ide_probe_disk1(void)
{int r, x;// wait for Device 0 to be readyide_wait_ready(0);// switch to Device 1outb(0x1F6, 0xE0 | (1<<4));// check for Device 1 to be ready for a whilefor (x = 0;x < 1000 && ((r = inb(0x1F7)) & (IDE_BSY|IDE_DF|IDE_ERR)) != 0;x++)/* do nothing */;// switch back to Device 0outb(0x1F6, 0xE0 | (0<<4));cprintf("Device 1 presence: %d\n", (x < 1000));return (x < 1000);
}void
ide_set_disk(int d)
{if (d != 0 && d != 1)panic("bad disk number");diskno = d;
}
//前面也不知道是啥,不过不重要重要的是下面两个函数int
ide_read(uint32_t secno, void *dst, size_t nsecs) //参数三个,扇区号,虚拟地址,已经读取扇区个数
{int r;assert(nsecs <= 256);ide_wait_ready(0);//判断是不是等待
//后面看不懂了outb(0x1F2, nsecs);outb(0x1F3, secno & 0xFF);outb(0x1F4, (secno >> 8) & 0xFF);outb(0x1F5, (secno >> 16) & 0xFF);outb(0x1F6, 0xE0 | ((diskno&1)<<4) | ((secno>>24)&0x0F));outb(0x1F7, 0x20); // CMD 0x20 means read sectorfor (; nsecs > 0; nsecs--, dst += SECTSIZE) {if ((r = ide_wait_ready(1)) < 0)return r;insl(0x1F0, dst, SECTSIZE/4);}return 0;
}int
ide_write(uint32_t secno, const void *src, size_t nsecs)//写入扇区,写入内容,写入扇区个数
{int r;assert(nsecs <= 256);ide_wait_ready(0);outb(0x1F2, nsecs);outb(0x1F3, secno & 0xFF);outb(0x1F4, (secno >> 8) & 0xFF);outb(0x1F5, (secno >> 16) & 0xFF);outb(0x1F6, 0xE0 | ((diskno&1)<<4) | ((secno>>24)&0x0F));outb(0x1F7, 0x30); // CMD 0x30 means write sectorfor (; nsecs > 0; nsecs--, src += SECTSIZE) {if ((r = ide_wait_ready(1)) < 0)return r;outsl(0x1F0, src, SECTSIZE/4);}return 0;
}
看完这个文件,主要要记住的就是那两个函数后面用的上。
int ide_read(uint32_t secno, void *dst, size_t nsecs)
int ide_write(uint32_t secno, const void *src, size_t nsecs)
bc
#include "fs.h"
// 基于用户级页错误处理机制的块缓存。
// Return the virtual address of this disk block.
void*
diskaddr(uint32_t blockno) //看的出来是返回对应块号的虚拟地址
{if (blockno == 0 || (super && blockno >= super->s_nblocks))panic("bad block number %08x in diskaddr", blockno);return (char*) (DISKMAP + blockno * BLKSIZE);
}// Is this virtual address mapped?
bool
va_is_mapped(void *va)//这个应该是判断这个页有没有映射
{return (uvpd[PDX(va)] & PTE_P) && (uvpt[PGNUM(va)] & PTE_P);
}// Is this virtual address dirty?
bool
va_is_dirty(void *va)//这个虚拟地址是不是修改了
{return (uvpt[PGNUM(va)] & PTE_D) != 0;
}// Fault any disk block that is read in to memory by
// loading it from disk. 这个应该就是缺页处理了
static void
bc_pgfault(struct UTrapframe *utf)
{void *addr = (void *) utf->utf_fault_va;//获取缺页地址uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;//对应块号int r;// Check that the fault was within the block cache region判断地址合不合法if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))panic("page fault in FS: eip %08x, va %08x, err %04x",utf->utf_eip, addr, utf->utf_err);// Sanity check the block number. 检查块号合不合法if (super && blockno >= super->s_nblocks)panic("reading non-existent block %08x\n", blockno);// Allocate a page in the disk map region, read the contents// of the block from the disk into that page.// Hint: first round addr to page boundary. fs/ide.c has code to read// the disk. fs/ide.c 有代码去读取硬盘// // LAB 5: you code here://经过练习的提示,我们应该要页对齐然后再分配内存addr=ROUNDDOWN(addr,PGSIZE);if(sys_page_alloc(0,addr,PTE_SYSCALL)<0)panic("error page alloc"); //分配一个页//分配完页之后,我们需要把磁盘里面的内容读出来,练习提示了我们 函数是按扇区来的//而我们是按块来的,还有页,第blockno*8 个扇区,读8个扇区ide_read(blockno*8,addr,8);//把页磁盘里面的值读出来 // Clear the dirty bit for the disk block page since we just read the// block from disk 刚加入的还没有修改,所以还是干净的if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)panic("in bc_pgfault, sys_page_map: %e", r);// Check that the block we read was allocated. (exercise for// the reader: why do we do this *after* reading the block// in?) 检查这个是不是已经分配了if (bitmap && block_is_free(blockno))panic("reading free block %08x\n", blockno);
}// Flush the contents of the block containing VA out to disk if
// necessary, then clear the PTE_D bit using sys_page_map.
// If the block is not in the block cache or is not dirty, does
// nothing. 把VA地址的内容写到磁盘,如果没有修改就什么都不要做
// Hint: Use va_is_mapped, va_is_dirty, and ide_write. 这三个是可以用的
// Hint: Use the PTE_SYSCALL constant when calling sys_page_map. 用 sys_page_map清楚 修改位
// Hint: Don't forget to round addr down.不要忘记页对齐
void
flush_block(void *addr)
{uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;//获取块号if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))//检查地址panic("flush_block of bad va %08x", addr);// LAB 5: Your code here.addr=ROUNDDOWN(addr,PGSIZE);//页对齐if(!va_is_mapped(addr)||!va_is_dirty(addr))return ;//检查地址是不是已经映射了和修改 如果没有映射,或者是修改说明没有做任何改变,那么什么都不用做ide_write(blockno*8,addr,8); //修改了就要写入sys_page_map(0,addr,0,addr,PTE_SYSCALL);// 清楚修改标志//panic("flush_block not implemented");
}// Test that the block cache works, by smashing the superblock and
// reading it back. 测试缓存是不是工作。
static void
check_bc(void)
{struct Super backup;// back up super blockmemmove(&backup, diskaddr(1), sizeof backup);// smash itstrcpy(diskaddr(1), "OOPS!\n");flush_block(diskaddr(1));assert(va_is_mapped(diskaddr(1)));assert(!va_is_dirty(diskaddr(1)));// clear it outsys_page_unmap(0, diskaddr(1));assert(!va_is_mapped(diskaddr(1)));// read it back inassert(strcmp(diskaddr(1), "OOPS!\n") == 0);// fix itmemmove(diskaddr(1), &backup, sizeof backup);flush_block(diskaddr(1));// Now repeat the same experiment, but pass an unaligned address to// flush_block.// back up super blockmemmove(&backup, diskaddr(1), sizeof backup);// smash itstrcpy(diskaddr(1), "OOPS!\n");// Pass an unaligned address to flush_block.flush_block(diskaddr(1) + 20);assert(va_is_mapped(diskaddr(1)));// Skip the !va_is_dirty() check because it makes the bug somewhat// obscure and hence harder to debug.//assert(!va_is_dirty(diskaddr(1)));// clear it outsys_page_unmap(0, diskaddr(1));assert(!va_is_mapped(diskaddr(1)));// read it back inassert(strcmp(diskaddr(1), "OOPS!\n") == 0);// fix itmemmove(diskaddr(1), &backup, sizeof backup);flush_block(diskaddr(1));cprintf("block cache is good\n");
}void
bc_init(void)//初始化磁盘
{struct Super super;set_pgfault_handler(bc_pgfault);//设置缺页处理手段check_bc();//检查磁盘缓冲// cache the super block by reading it once 第一次加载,在这个时候把根目录取到内存了,在这之后我们就可以通过super 访问磁盘状态了memmove(&super, diskaddr(1), sizeof super)
}
我们现在就有缺页处理,和刷新缓存的功能了。
fs/fs.c
中的fs_init
函数是如何使用块缓存的主要示例。在初始化块缓存之后,它将指针存储在super
全局变量的磁盘映射区域中。此后,我们可以简单地从struct Super
中读取,就像它们在内存中一样,我们的页面错误处理程序将根据需要从磁盘读取它们。
我们看看fs_init()
// Initialize the file system
void
fs_init(void)
{static_assert(sizeof(struct File) == 256);//检查File 对不对// Find a JOS disk. Use the second IDE disk (number 1) if availableif (ide_probe_disk1()) //这个是找盘?看注释是看有没有1号盘,有就用1号没有0号?ide_set_disk(1);elseide_set_disk(0);bc_init();//这个讲过了// Set "super" to point to the super block.super = diskaddr(1);//在bc_init里面实际上已经有了 也不知道这两个有啥区别check_super();// Set "bitmap" to the beginning of the first bitmap block.bitmap = diskaddr(2);//位图 我们接下来就要讲了check_bitmap();
The Block Bitmap
在fs_init
设置位图指针之后,我们可以将位图视为一个打包数组的位,每一个位对应于磁盘上的每个块。参见例如block_is_free
,它在位图中检查给定的块是否被标记为空闲。
练习3
实现fs/fs.c
里面的alloc_block
。
我们简单看看相关函数
// --------------------------------------------------------------
// Super block
// --------------------------------------------------------------// Validate the file system super-block. 检查super是否正常
void
check_super(void)
{if (super->s_magic != FS_MAGIC)//不知道干啥的panic("bad file system magic number");if (super->s_nblocks > DISKSIZE/BLKSIZE)//检查文件系统是不是正常大小panic("file system is too large");cprintf("superblock is good\n");
}// --------------------------------------------------------------
// Free block bitmap
// --------------------------------------------------------------// Check to see if the block bitmap indicates that block 'blockno' is free.
// Return 1 if the block is free, 0 if not. 看的出来是检查一个块号是不是空闲额,1空闲0不空闲
bool
block_is_free(uint32_t blockno)
{if (super == 0 || blockno >= super->s_nblocks)return 0;if (bitmap[blockno / 32] & (1 << (blockno % 32)))return 1;return 0;
}// Mark a block free in the bitmap //把一个块号释放掉
void
free_block(uint32_t blockno)
{// Blockno zero is the null pointer of block numbers.if (blockno == 0) //记住0是不能用的panic("attempt to free zero block");//uint32_t *bitmap; bitmap 定义是 32位,每位代表一个块bitmap[blockno/32] |= 1<<(blockno%32);
}// Search the bitmap for a free block and allocate it. When you
// allocate a block, immediately flush the changed bitmap block
// to disk.
// 暴力搜索第一个块号然后分配。
// Return block number allocated on success,
// -E_NO_DISK if we are out of blocks.
//
// Hint: use free_block as an example for manipulating the bitmap. 使用free_block 作为一个列子
int
alloc_block(void)//看了上面那个这个实现就简单额。
{// The bitmap consists of one or more blocks. A single bitmap block// contains the in-use bits for BLKBITSIZE blocks. There are// super->s_nblocks blocks in the disk altogether.//告诉你总共有 super->s_nblocks 这么多个块// LAB 5: Your code here.for(int i=0;i<super->s_nblocks;i++){//直接暴力所有块if(block_is_free(i)){ //检查是不是空闲bitmap[i/32] ^= (1<<(i%32)); //不懂异或 的可以用 bitmap[i/32]&=~(1<<(i%32); 代替flush_block(diskaddr(i));//刷新缓存。return i;}}//panic("alloc_block not implemented");return -E_NO_DISK;
}// Validate the file system bitmap.
//
// Check that all reserved blocks -- 0, 1, and the bitmap blocks themselves --
// are all marked as in-use. 检查位图,没有我们需要操作的地方,看一下就行
void
check_bitmap(void)
{uint32_t i;// Make sure all bitmap blocks are marked in-use 检查位图块都被使用了for (i = 0; i * BLKBITSIZE < super->s_nblocks; i++)assert(!block_is_free(2+i));// Make sure the reserved and root blocks are marked in-use.assert(!block_is_free(0));assert(!block_is_free(1));cprintf("bitmap is good\n");
}
File Operations
我们在fs/fs.c
中提供了各种函数,以实现解释和管理struct File
,扫描和管理目录条目所需的基本功能,并从文件系统的根目录开始遍历以解析绝对路径名。阅读fs/fs.c
中的所有代码,并确保您了解每个函数执行的操作。
刚才我们已经看了一部分了,然后还是短短的一部分。
练习4
我们需要实现file_block_walk
和 file_get_block
,这两个功能十分重要。file_block_walk
这个是找到文件里面第filebno
块号地址,是指向块号的地址,并不是块的具体地址,file_get_block
功能是找到 filebno
对应的块号是多少,并返回块号地址,这个是具体地址
基本上所有文件操作都要通过上述两个函数。
file_block_walk和file_get_block
// Find the disk block number slot for the 'filebno'th block in file 'f'.
// Set '*ppdiskbno' to point to that slot.
// The slot will be one of the f->f_direct[] entries,
// or an entry in the indirect block.
// When 'alloc' is set, this function will allocate an indirect block
// if necessary.
//在f 文件里面找到第 filebno 块对应的地址,储存到*ppdiskbno,可能在 f->f_direct[] 里面
//或者 间接块里面,如果我们 分配位置设置了,就分配一个
// Returns:
// *ppdiskbno 储存的是块号
// 0 on success (but note thatppdiskbno might equal 0).分配冲个返回0
// -E_NOT_FOUND if the function needed to allocate an indirect block, but
// alloc was 0. 没有找到 且没有设置分配位
// -E_NO_DISK if there's no space on the disk for an indirect block.没空间了
// -E_INVAL if filebno is out of range (it's >= NDIRECT + NINDIRECT).
//超出范围
// Analogy: This is like pgdir_walk for files.和pgdir_walk很像
// Hint: Don't forget to clear any block you allocate.不要忘记清楚你分配的块
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)//f 文件 fileno文件中的第几块 ppdiskbno储存地址 alloc是否可以分配间接块
{// LAB 5: Your code here.//NDIRECT NINDIRECT 两个在fs.h 里面有定义if(filebno>=NDIRECT+NINDIRECT)return -E_INVAL;//文件块数超出了最大值if(filebno<NDIRECT){//如果在直接块里面 ,就直接返回地址if(ppdiskbno){*ppdiskbno=&f->f_direct[filebno];}return 0;}//如果没有在直接块里面,那么就只能在间接块里,然后我们怎么判断有没有间接块呢???//在这个文件里面搜索 `f_indirectf`,发现在 file_truncate_blocks函数的前面//有说明 f->f_indirect != 0代表没有 特么这个函数不告诉我,别问我经历了什么if((f->f_indirect)==0){//判断没有间接块if(alloc==0)return -E_NOT_FOUND;//没有设置分配位,就返回没有找到int r=alloc_block();//分配一个块if(r<=0)return -E_NO_DISK;f->f_indirect=r;//间接快块号memset(diskaddr(f->f_indirect), 0, BLKSIZE);//出事化为0flush_block(diskaddr(f->f_indirect));//刷新缓存}if (ppdiskbno)//只是返回地址,没有具体的值。*ppdiskbno = &((uint32_t *)diskaddr(f->f_indirect))[filebno-NDIRECT];//此时 *ppdiskbno 是块号return 0;//panic("file_block_walk not implemented");
}// Set *blk to the address in memory where the filebno'th
// block of file 'f' would be mapped.
//设置 *blk 为 在 f 文件连 的第 fileno 的地址。明显是要用到上一个函数。
// Returns 0 on success, < 0 on error. Errors are:返回0成功
// -E_NO_DISK if a block needed to be allocated but the disk is full. 没有空间了返回
// -E_INVAL if filebno is out of range. 超出范围
//
// Hint: Use file_block_walk and alloc_block. 使用这两个函数
int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{// LAB 5: Your code here.uint32_t *ppdiskbno,blockno;int r=0; //首先得知道对应磁盘中的块号是多少,//通过这个函数 ppdiskbno 就是指向对应磁盘块号的地址,也就是 *ppdiskbno 存的是块号。if ((r = file_block_walk(f, filebno, &ppdiskbno, true)) < 0)return r;if ((*ppdiskbno)==0) {//块号是 0 说明还没有分配块if ((r = alloc_block()) < 0)//分配一个块return r;blockno = r;*ppdiskbno = blockno;//指向那个块flush_block(diskaddr(*ppdiskbno));//刷新缓存}if (blk)*blk = (char *)diskaddr(*ppdiskbno);//块号在磁盘中的地址 是 *blk存的是虚拟地址指针return 0;//panic("file_get_block not implemented");
}
看懂上面两个函数,其他的就都简单了。我们来看看其他函数。
fs.c
// Try to find a file named "name" in dir. If so, set *file to it.
// 在目录里面找一个name 的文件
// Returns 0 and sets *file on success, < 0 on error. Errors are:
// -E_NOT_FOUND if the file is not found 没找到
static int
dir_lookup(struct File *dir, const char *name, struct File **file)
{int r;uint32_t i, j, nblock;char *blk;struct File *f;// Search dir for name.// We maintain the invariant that the size of a directory-file// is always a multiple of the file system's block size.assert((dir->f_size % BLKSIZE) == 0);nblock = dir->f_size / BLKSIZE;for (i = 0; i < nblock; i++) {if ((r = file_get_block(dir, i, &blk)) < 0)return r;f = (struct File*) blk;for (j = 0; j < BLKFILES; j++)//就是简单的一个个比较if (strcmp(f[j].f_name, name) == 0) {*file = &f[j];return 0;}}return -E_NOT_FOUND;
}// Set *file to point at a free File structure in dir. The caller is
// responsible for filling in the File fields. 在目录里面添加一个文件
static int
dir_alloc_file(struct File *dir, struct File **file)
{int r;uint32_t nblock, i, j;char *blk;struct File *f;assert((dir->f_size % BLKSIZE) == 0);nblock = dir->f_size / BLKSIZE;for (i = 0; i < nblock; i++) {if ((r = file_get_block(dir, i, &blk)) < 0)return r;f = (struct File*) blk;for (j = 0; j < BLKFILES; j++)if (f[j].f_name[0] == '\0') {*file = &f[j];//找到第一个能放 文件描述符的位置return 0;}}dir->f_size += BLKSIZE;//文件大小增加if ((r = file_get_block(dir, i, &blk)) < 0)//不知道为啥还要在找一次return r;f = (struct File*) blk;*file = &f[0];return 0;
}// Skip over slashes.
static const char* //跳过斜杠的第一个字符
skip_slash(const char *p)
{while (*p == '/')p++;return p;
}// Evaluate a path name, starting at the root. 一个路径 从根目录开始
// On success, set *pf to the file we found 成功设置文件指针pf 和 路径指针pdir
// and set *pdir to the directory the file is in.
// If we cannot find the file but find the directory
// it should be in, set *pdir and copy the final path
// element into lastelem.如果我们没有找到一个文件但是找到了路径,那么就把路径指过去,再复制最后一个元素
static int
walk_path(const char *path, struct File **pdir, struct File **pf, char *lastelem)
{const char *p;char name[MAXNAMELEN];struct File *dir, *f;int r;// if (*path != '/')// return -E_BAD_PATH;path = skip_slash(path);f = &super->s_root;dir = 0;name[0] = 0;if (pdir)*pdir = 0;*pf = 0;while (*path != '\0') {dir = f;p = path;while (*path != '/' && *path != '\0')path++;if (path - p >= MAXNAMELEN)return -E_BAD_PATH;memmove(name, p, path - p);name[path - p] = '\0';path = skip_slash(path);if (dir->f_type != FTYPE_DIR)return -E_NOT_FOUND;if ((r = dir_lookup(dir, name, &f)) < 0) {if (r == -E_NOT_FOUND && *path == '\0') {if (pdir)*pdir = dir;if (lastelem)strcpy(lastelem, name);*pf = 0;}return r;}}if (pdir)*pdir = dir;*pf = f;return 0;
}// --------------------------------------------------------------
// File operations
// --------------------------------------------------------------// Create "path". On success set *pf to point at the file and return 0.
// On error return < 0. 在path 创建文件,*pf指向文件????
int
file_create(const char *path, struct File **pf)
{char name[MAXNAMELEN];int r;struct File *dir, *f;if ((r = walk_path(path, &dir, &f, name)) == 0)return -E_FILE_EXISTS;if (r != -E_NOT_FOUND || dir == 0)return r;if ((r = dir_alloc_file(dir, &f)) < 0)return r;strcpy(f->f_name, name);*pf = f;file_flush(dir);return 0;
}// Open "path". On success set *pf to point at the file and return 0.
// On error return < 0. 打开文件,pf 指向文件
int
file_open(const char *path, struct File **pf)
{return walk_path(path, 0, pf, 0);
}// Read count bytes from f into buf, starting from seek position
// offset. This meant to mimic the standard pread function.
// Returns the number of bytes read, < 0 on error.
//这个函数要看懂,其实就是在 f 里面读 count 个文件到buf里面,offset 你可以理解成光标
ssize_t
file_read(struct File *f, void *buf, size_t count, off_t offset)
{int r, bn;off_t pos;char *blk;if (offset >= f->f_size)//判断光标是不是文件尾return 0;count = MIN(count, f->f_size - offset);//最多能写这么多个for (pos = offset; pos < offset + count; ) {if ((r = file_get_block(f, pos / BLKSIZE, &blk)) < 0)return r;bn = MIN(BLKSIZE - pos % BLKSIZE, offset + count - pos);memmove(buf, blk + pos % BLKSIZE, bn);//读取pos += bn;buf += bn;}return count;
}// Write count bytes from buf into f, starting at seek position
// offset. This is meant to mimic the standard pwrite function.
// Extends the file if necessary.
// Returns the number of bytes written, < 0 on error.
//在光标后面写 count 个文件
int
file_write(struct File *f, const void *buf, size_t count, off_t offset)
{int r, bn;off_t pos;char *blk;// Extend file if necessaryif (offset + count > f->f_size)//判断需不需要扩大文件if ((r = file_set_size(f, offset + count)) < 0)return r;for (pos = offset; pos < offset + count; ) {if ((r = file_get_block(f, pos / BLKSIZE, &blk)) < 0)return r;bn = MIN(BLKSIZE - pos % BLKSIZE, offset + count - pos);memmove(blk + pos % BLKSIZE, buf, bn);//复制 这个时候写完其实还是在内存里面,并没有写到磁盘pos += bn;buf += bn;}return count;
}// Remove a block from file f. If it's not there, just silently succeed.
// Returns 0 on success, < 0 on error. 从f 里面删除块 第filebno
static int
file_free_block(struct File *f, uint32_t filebno)
{int r;uint32_t *ptr;if ((r = file_block_walk(f, filebno, &ptr, 0)) < 0)return r;if (*ptr) {free_block(*ptr);*ptr = 0;}return 0;
}// Remove any blocks currently used by file 'f',
// but not necessary for a file of size 'newsize'.
// For both the old and new sizes, figure out the number of blocks required,
// and then clear the blocks from new_nblocks to old_nblocks.
// If the new_nblocks is no more than NDIRECT, and the indirect block has
// been allocated (f->f_indirect != 0), then free the indirect block too.
// (Remember to clear the f->f_indirect pointer so you'll know
// whether it's valid!)
// Do not change f->f_size.
//把新大小到 旧的大小之间的块全部删除。如果不需要间接块了,把他也删了,不用修改文件大小
static void
file_truncate_blocks(struct File *f, off_t newsize)
{int r;uint32_t bno, old_nblocks, new_nblocks;old_nblocks = (f->f_size + BLKSIZE - 1) / BLKSIZE;new_nblocks = (newsize + BLKSIZE - 1) / BLKSIZE;for (bno = new_nblocks; bno < old_nblocks; bno++)if ((r = file_free_block(f, bno)) < 0)cprintf("warning: file_free_block: %e", r);if (new_nblocks <= NDIRECT && f->f_indirect) {free_block(f->f_indirect);f->f_indirect = 0;}
}// Set the size of file f, truncating or extending as necessary. 设置文件大小
int
file_set_size(struct File *f, off_t newsize)
{if (f->f_size > newsize)file_truncate_blocks(f, newsize);f->f_size = newsize;flush_block(f);return 0;
}// Flush the contents and metadata of file f out to disk.
// Loop over all the blocks in file.
// Translate the file block number into a disk block number
// and then check whether that disk block is dirty. If so, write it out.
//刷新文件 写入到磁盘了
void
file_flush(struct File *f)
{int i;uint32_t *pdiskbno;for (i = 0; i < (f->f_size + BLKSIZE - 1) / BLKSIZE; i++) {if (file_block_walk(f, i, &pdiskbno, 0) < 0 ||pdiskbno == NULL || *pdiskbno == 0)continue;flush_block(diskaddr(*pdiskbno));}flush_block(f);if (f->f_indirect)flush_block(diskaddr(f->f_indirect));
}// Sync the entire file system. A big hammer. 刷新 整个文件系统
void
fs_sync(void)
{int i;for (i = 1; i < super->s_nblocks; i++)flush_block(diskaddr(i));
}
终于看完了。看完之后告诉你,实际上没啥卵用。
The file system interface
现在我们的文件系统环境已经具有必要的功能,我们必须让其他希望使用文件系统的用户环境可以访问文件系统。由于其他用户环境不能直接调用文件系统环境中的函数,因此我们将通过远程过程调用(RPC)即在JOS的IPC机制上封装构建的对文件系统环境的访问接口。如下图所示,对文件系统服务器的调用(比如说,read):
Regular env FS env+---------------+ +---------------+| read | | file_read || (lib/fd.c) | | (fs/fs.c) | ...|.......|.......|...|.......^.......|...............| v | | | | RPC mechanism| devfile_read | | serve_read || (lib/file.c) | | (fs/serv.c) || | | | ^ || v | | | || fsipc | | serve || (lib/file.c) | | (fs/serv.c) || | | | ^ || v | | | || ipc_send | | ipc_recv || | | | ^ |+-------|-------+ +-------|-------+| |+-------------------+ |
简单来讲,就是我要访问磁盘,先通过IPC发给另一个进程,然后另一个进程帮我访问,就是这样了
虚线以下的所有内容都只是从常规用户环境到文件系统环境的读取请求的机制。从最开始,read
适用于任何文件描述符,并且简单地分发到适当的设备读取函数,在这种情况下为devfile_read
我们可以有更多的设备类型,如管道。devfile_read
专门实现为读取磁盘文件。lib/file.c
中的devfile_read
和其他devfile_*
函数实现了客户端FS
操作,并且都以大致相同的方式工作,在request
结构体中封装参数,调用fsipc
发送IPC
请求,并解包和返回结果。 fsipc
函数简单地处理向服务器发送请求并接收回复的常见细节。
文件系统服务器代码可以在fs/serv.c
中找到。它在serve
函数中循环,无休止地通过IPC
接收请求,将该请求分发到适当的处理函数,并通过IPC发送结果。在read
示例中,serve
函数将请求分发到serve_read
,它将处理read
请求指定的IPC
细节,例如解包request
结构体,最后调用file_read
来实际执行文件读取。
回想一下,JOS
的IPC
机制允许用户环境发送一个32位数字,并且可选地共享一个页面。要从客户端发送请求到服务器,我们使用32位数字作为请求类型(文件系统服务器RPC
也是像系统调用编号那样编号),并将参数存储在通过IPC
共享页面的Fsipc
联合类型中的request
结构体中。在客户端,我们总是在fsipcbuf
共享页面;在服务器端,我们将传入请求页映射到fsreq
(0x0ffff000
)。
简单来说,32位作为请求类型,页放到fsreq,并且储存到request`结构体中
服务器还通过IPC
发回响应。我们使用32
位数字作为函数的返回码。对于大多数RPC
,这些是他们返回的全部内容。FSREQ_READ
和FSREQ_STAT
也返回数据,它们只是将数据写入客户端发送请求的页面。无需在响应IPC
中发送此页面,因为客户端首先与文件系统服务器共享。此外,在其回复中,FSREQ_OPEN
与客户端共享一个新的Fd page
。我们将很快返回到文件描述符页面。
练习5
和练习6
让我们实现serve_read
和serve_write
,devfile_write
,我们直接一起看了。
在做之前了解一下
// The file system server maintains three structures
// for each open file.
//服务器的三层 结构
// 1. The on-disk 'struct File' is mapped into the part of memory
// that maps the disk. This memory is kept private to the file
// server. //磁盘文件层 就是 struct File,前面我已经看过了
// 2. Each open file has a 'struct Fd' as well, which sort of
// corresponds to a Unix file descriptor. This 'struct Fd' is kept
// on *its own page* in memory, and it is shared with any
// environments that have the file open.
// 每个开放的文件都有他自己的 描述 struct Fd,供所有环境使用
// 3. 'struct OpenFile' links these other two structures, and is kept
// private to the file server. The server maintains an array of
// all open files, indexed by "file ID". (There can be at most
// MAXOPEN files open concurrently.) The client uses file IDs to
// communicate with the server. File IDs are a lot like
// environment IDs in the kernel. Use openfile_lookup to translate
// file IDs to struct OpenFile. struct OpenFile' 将两者联系起来,并保持私有
// 客服端通过id与 服务器通信,就像进程ID,使用 openfile_lookup 可以获得对应OpenFilestruct OpenFile {uint32_t o_fileid; // file idstruct File *o_file; // mapped descriptor for open fileint o_mode; // open modestruct Fd *o_fd; // Fd page
};
我们先看看read
是怎么执行的吧。
read
read
一开始在file.c
里面调用devfile_read
,我们看看怎么实现的。
// Read at most 'n' bytes from 'fd' at the current position into 'buf'.
//cong fd 读n 个字节到buf 里面。
// Returns:
// The number of bytes successfully read.
// < 0 on error.
static ssize_t
devfile_read(struct Fd *fd, void *buf, size_t n)
{ // fd 应该是打开的文件,buf,保存读出来的值,n是读的大小 返回了读了几个值// Make an FSREQ_READ request to the file system server after// filling fsipcbuf.read with the request arguments. The// bytes read will be written back to fsipcbuf by the file// system server.int r;fsipcbuf.read.req_fileid = fd->fd_file.id; //fsipcbuf 作为信息传递fsipcbuf.read.req_n = n;if ((r = fsipc(FSREQ_READ, NULL)) < 0)//fsipc 就是分配函数return r;assert(r <= n);assert(r <= PGSIZE);memmove(buf, fsipcbuf.readRet.ret_buf, r);//返回的值在ret_buf 里面return r;
}static int
fsipc(unsigned type, void *dstva) //这个时候dstva 应该是空
{static envid_t fsenv;if (fsenv == 0)fsenv = ipc_find_env(ENV_TYPE_FS);static_assert(sizeof(fsipcbuf) == PGSIZE);if (debug)cprintf("[%08x] fsipc %d %08x\n", thisenv->env_id, type, *(uint32_t *)&fsipcbuf);ipc_send(fsenv, type, &fsipcbuf, PTE_P | PTE_W | PTE_U);return ipc_recv(NULL, dstva, NULL);//目前来说 这个dstva 是个空值,所以实际上没啥返回值。 写回是通过共享页面实现的。共享了这个fsipcbuf
}
然后就发送到了我们原本说的serv.c
里面去了。serv.c
最终运行了serve
。
void
serve(void)
{uint32_t req, whom;int perm, r;void *pg;while (1) { //明显是个无限循环,所以这个进程值在这运行perm = 0;//在这等待接收,我们现在这种情况,就是接收到了 刚才发送的那个req = ipc_recv((int32_t *) &whom, fsreq, &perm); if (debug)cprintf("fs req %d from %08x [page %08x: %s]\n",req, whom, uvpt[PGNUM(fsreq)], fsreq);// All requests must contain an argument pageif (!(perm & PTE_P)) {cprintf("Invalid request from %08x: no argument page\n",whom);continue; // just leave it hanging...}pg = NULL;if (req == FSREQ_OPEN) {r = serve_open(whom, (struct Fsreq_open*)fsreq, &pg, &perm);} else if (req < ARRAY_SIZE(handlers) && handlers[req]) {//这个写法非常骚气,r = handlers[req](whom, fsreq);//这个时候根据req分配了函数。我们现在也就是serve_read} else {cprintf("Invalid request code %d from %08x\n", req, whom);r = -E_INVAL;}ipc_send(whom, r, pg, perm);sys_page_unmap(0, fsreq);}
}
// 所以我们就跑到了这个函数
// Read at most ipc->read.req_n bytes from the current seek position
// in ipc->read.req_fileid. Return the bytes read from the file to
// the caller in ipc->readRet, then update the seek position. Returns
// the number of bytes successfully read, or < 0 on error.
//Fsipc 里面存了我们所需要的东西。
int
serve_read(envid_t envid, union Fsipc *ipc)
{struct Fsreq_read *req = &ipc->read;//请求struct Fsret_read *ret = &ipc->readRet;//返回的结果if (debug)cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);// Lab 5: Your code here:int r=0;struct OpenFile *o;//openfile_lookup这个函数是查找 打开的文件if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)return r;r=file_read(o->o_file,ret,req->req_n,o->o_fd->fd_offset);if(r>=0)o->o_fd->fd_offset+=r;return r;
}
write
看完read
,write
也是一样的照搬就行了。
// Write at most 'n' bytes from 'buf' to 'fd' at the current seek position.
//
// Returns:
// The number of bytes successfully written.
// < 0 on error.
static ssize_t
devfile_write(struct Fd *fd, const void *buf, size_t n)
{// Make an FSREQ_WRITE request to the file system server. Be// careful: fsipcbuf.write.req_buf is only so large, but// remember that write is always allowed to write *fewer*// bytes than requested.// LAB 5: Your code hereif ( n > sizeof (fsipcbuf.write.req_buf)) n = sizeof (fsipcbuf.write.req_buf);fsipcbuf.write.req_fileid = fd->fd_file.id;fsipcbuf.write.req_n = n;memmove(fsipcbuf.write.req_buf, buf, n); //先把写的内容复制return fsipc(FSREQ_WRITE, NULL);//然后发送过去//panic("devfile_write not implemented");
}
// Write req->req_n bytes from req->req_buf to req_fileid, starting at
// the current seek position, and update the seek position
// accordingly. Extend the file if necessary. Returns the number of
// bytes written, or < 0 on error.
int
serve_write(envid_t envid, struct Fsreq_write *req)
{if (debug)cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n);// LAB 5: Your code here.int r=0;struct OpenFile *o;if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)return r;//前面肯定一样的r=file_write(o->o_file,req->req_buf,req->req_n,o->o_fd->fd_offset);//不同的就只有这,这调用的是file_writeif(r>=0)o->o_fd->fd_offset+=r;return r;//panic("serve_write not implemented");
}
其他函数的调用过程自行分析了。
Spawning Processes
我们已经给你提供了生成一个新的用户环境spawn
的代码(参见lib/spawn.c
),将文件系统中的程序镜像加载到用户环境中,然后启动运行此程序的子环境。然后,父进程继续独立于该子进程运行。spawn
函数就像UNIX
中fork
后面紧接着在子进程中执行exec
。
我们实现了spawn
而不是一个UNIX风格的exe
c,因为在exkernel
方式下,在用户空间不借助内核的特殊帮助中更容易实现spawn
。想想你在用户空间实现exec
时必须做什么,并确保你明白为什么更难。
练习7
只要我们实现sys_env_set_trapframe
。
我们先看看spawn
做了啥
// Spawn a child process from a program image loaded from the file system.
// 从文件加载了一个子进程
// prog: the pathname of the program to run. 文件路径
// argv: pointer to null-terminated array of pointers to strings, 参数
// which will be passed to the child as its command-line arguments.
// Returns child envid on success, < 0 on failure.
int
spawn(const char *prog, const char **argv)
{unsigned char elf_buf[512];struct Trapframe child_tf;envid_t child;int fd, i, r;struct Elf *elf;struct Proghdr *ph;int perm;// This code follows this procedure:// 后面虽然有一大堆,但是大部分没啥用// - Open the program file.//// - Read the ELF header, as you have before, and sanity check its// magic number. (Check out your load_icode!)// 读取ELF// - Use sys_exofork() to create a new environment.// 创建进程// - Set child_tf to an initial struct Trapframe for the child.// 设置tf// - Call the init_stack() function above to set up// the initial stack page for the child environment.// 初始化堆栈,有兴趣的自己看看看 后面都是写细节了// - Map all of the program's segments that are of p_type// ELF_PROG_LOAD into the new environment's address space.// Use the p_flags field in the Proghdr for each segment// to determine how to map the segment://// * If the ELF flags do not include ELF_PROG_FLAG_WRITE,// then the segment contains text and read-only data.// Use read_map() to read the contents of this segment,// and map the pages it returns directly into the child// so that multiple instances of the same program// will share the same copy of the program text.// Be sure to map the program text read-only in the child.// Read_map is like read but returns a pointer to the data in// *blk rather than copying the data into another buffer.//// * If the ELF segment flags DO include ELF_PROG_FLAG_WRITE,// then the segment contains read/write data and bss.// As with load_icode() in Lab 3, such an ELF segment// occupies p_memsz bytes in memory, but only the FIRST// p_filesz bytes of the segment are actually loaded// from the executable file - you must clear the rest to zero.// For each page to be mapped for a read/write segment,// allocate a page in the parent temporarily at UTEMP,// read() the appropriate portion of the file into that page// and/or use memset() to zero non-loaded portions.// (You can avoid calling memset(), if you like, if// page_alloc() returns zeroed pages already.)// Then insert the page mapping into the child.// Look at init_stack() for inspiration.// Be sure you understand why you can't use read_map() here.//// Note: None of the segment addresses or lengths above// are guaranteed to be page-aligned, so you must deal with// these non-page-aligned values appropriately.// The ELF linker does, however, guarantee that no two segments// will overlap on the same page; and it guarantees that// PGOFF(ph->p_offset) == PGOFF(ph->p_va).//// - Call sys_env_set_trapframe(child, &child_tf) to set up the// correct initial eip and esp values in the child.//// - Start the child process running with sys_env_set_status().if ((r = open(prog, O_RDONLY)) < 0)return r;fd = r;// Read elf headerelf = (struct Elf*) elf_buf;//readn 在fd.c里面 可以看看底层调用了 dev_read 应该就是前面实现的那个函数if (readn(fd, elf_buf, sizeof(elf_buf)) != sizeof(elf_buf)//读取elf 头部|| elf->e_magic != ELF_MAGIC) {close(fd);cprintf("elf magic %08x want %08x\n", elf->e_magic, ELF_MAGIC);return -E_NOT_EXEC;}// Create new child environmentif ((r = sys_exofork()) < 0)return r;child = r;//复制进程// Set up trap frame, including initial stack.初始化tfchild_tf = envs[ENVX(child)].env_tf;child_tf.tf_eip = elf->e_entry;if ((r = init_stack(child, argv, &child_tf.tf_esp)) < 0)return r;// Set up program segments as defined in ELF header. 这个很眼熟吧ph = (struct Proghdr*) (elf_buf + elf->e_phoff);for (i = 0; i < elf->e_phnum; i++, ph++) {if (ph->p_type != ELF_PROG_LOAD)continue;perm = PTE_P | PTE_U;if (ph->p_flags & ELF_PROG_FLAG_WRITE)perm |= PTE_W;if ((r = map_segment(child, ph->p_va, ph->p_memsz,fd, ph->p_filesz, ph->p_offset, perm)) < 0)goto error;}close(fd);fd = -1;// Copy shared library state.if ((r = copy_shared_pages(child)) < 0)panic("copy_shared_pages: %e", r);child_tf.tf_eflags |= FL_IOPL_3; // devious: see user/faultio.cif ((r = sys_env_set_trapframe(child, &child_tf)) < 0)//在这个地方调用了sys_env_set_trapframe 我们可以看到 child_tf 前面已经帮我们设置好了。panic("sys_env_set_trapframe: %e", r);if ((r = sys_env_set_status(child, ENV_RUNNABLE)) < 0)panic("sys_env_set_status: %e", r);return child;error:sys_env_destroy(child);close(fd);return r;
}
因为传进来的tf
已经设置好了,所以我们直接改就行了
// Set envid's trap frame to 'tf'.
// tf is modified to make sure that user environments always run at code
// protection level 3 (CPL 3), interrupts enabled, and IOPL of 0.
//
// Returns 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{// LAB 5: Your code here.// Remember to check whether the user has supplied us with a good// address!struct Env *child;if((envid2env(envid,&child,1))<0){return -E_BAD_ENV;}//判断进程id 是不是有效的child->env_tf=*tf;//直接指过去child->env_tf.tf_cs |= 0x3; //修改一下提示要求的值child->env_tf.tf_eflags &= (~FL_IOPL_MASK);child->env_tf.tf_eflags |= FL_IF;return 0;//panic("sys_env_set_trapframe not implemented");
}
设置完后记得添加syscall
.
case SYS_env_set_trapframe:return sys_env_set_trapframe((envid_t)a1,(struct Trapframe*)a2);
说一下我测试的时候发现,测试文件里面有一个单词和代码不一样。
在grade-lab5
里面有一个environments
单词和代码里面差了一个a
,我修改了一下,如果没错的话,当我没说。environments
和environmeants
搜了一下单词意思好像是一样的。
@test(10, "spawn via spawnhello")
def test_spawn():r.user_test("spawnhello")r.match('i am parent environment 00001001','hello, world','i am environment 00001002','No runnable environmeants in the system!')
Sharing library state across fork and spawn
UNIX文件描述符是一个普遍的概念,它包括管道,控制台I/O等。在JOS中,每一个这样的设备类型的都具有相应的struct Dev
,该结构体中有指向实现该设备类型读/写等操作的函数指针。lib/fd.c
在struct Dev
之上实现了一般的类UNIX文件描述符接口。每个struct Fd
表示其设备类型,并且lib/fd.c
中的大多数函数简单地将操作分发到合适的struct Dev
中的函数。
lib/fd.c
还在每个应用程序环境的地址空间虚拟地址FDTABLE
开始中处护文件描述符表区域。该区域为应用程序可以一次能最多打开MAXFD
(当前为32)个文件描述符的每个文件描述符保留一个页面(4KB)大小的地址空间。在任何时候,当且仅当相应的文件描述符被使用时,文件描述符表页会被映射。每个文件描述符还有一个在FILEDATA
开始的区域可选的“数据页”,如果选择了数据区域,设备就可以使用该区域。
我们想在fork
和spawn
之间共享文件描述符状态,但文件描述符状态保存在用户空间内存中。现在,fork
完成之后,内存将被标记为copy-on-write
,所以状态将被重复而不是共享。 (这意味着环境将无法在他们没有打开自己的文件中查找,并且管道将在fork
之后也不能运行。)spawn
完成之后,内存将被丢弃,完全不会被复制。(有效地,spawn
的用户环境在没有打开的文件描述符状态下开始运行。)
我们将更改fork
,让fork
时了解某些区域的内存是由“库操作系统”使用,应始终共享。而不是在某个地方硬编码一个区域列表,我们将在页表项中设置一个未使用的位(就像我们在fork
中使用PTE_COW
位一样)来确定共享区域。
我们已经在inc/lib.h
中定义了一个新的PTE_SHARE
位。该位是Intel和AMD手册中标记为“可用于软件使用”的三个PTE位之一。我们约定,如果一个页表项中该位置位,应该在fork
和spawn
中直接将该PTE从父对象复制到子环境。请注意,这不同于标记PTE为copy-on-write:如第一段所述,我们要确保共享页面更新。
简单来说,就是我们定义了一个PTE_SHARE位,表示分享这个页,分享的这个页不用写时复制。
练习8
就是修改了这些东西。添加一个if
就行了。
static int
duppage(envid_t envid, unsigned pn)
{int r;// LAB 4: Your code here.void* vaddr=(void*)(pn*PGSIZE);if(uvpt[pn]&PTE_SHARE){//多添加这个if就行了if((r=sys_page_map(0,vaddr,envid,vaddr,uvpt[pn]&PTE_SYSCALL))<0)return r;}else if((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)){if ((r = sys_page_map(0, vaddr, envid, vaddr, PTE_P | PTE_U | PTE_COW)) < 0)return r;if ((r = sys_page_map(0, vaddr, 0, vaddr, PTE_P | PTE_U | PTE_COW)) < 0)return r;}else if((r = sys_page_map(0, vaddr, envid, vaddr, PTE_P | PTE_U)) < 0) {return r;}//panic("duppage not implemented");return 0;
}
另外一个直接暴力找就行了没啥太大的区别。
// Copy the mappings for shared pages into the child address space.
static int
copy_shared_pages(envid_t child)
{// LAB 5: Your code here.int r=0,pn=0;for (pn=PGNUM(UTEXT); pn<PGNUM(USTACKTOP); pn++){if ((uvpd[pn >> 10] & PTE_P) &&uvpt[pn] & PTE_SHARE)if ( (r = sys_page_map(thisenv->env_id, (void *)(pn*PGSIZE), child, (void *)(pn*PGSIZE), uvpt[pn] & PTE_SYSCALL )) < 0)return r;}return 0;
}
The keyboard interface
倒着之后,实验让我们做的东西非常简单,但是又有好多东西还没搞清楚。
为了使shell
工作,我们需要一种方法在shell
中输入。 QEMU一直在显示我们在CGA显示器和串行端口的输出内容,但到目前为止,我们只能在内核监视器中输入。在QEMU中,在图形窗口中的输入显示为从键盘输入到JOS,从控制台的输入显示为串行端口上的字符。kern/console.c
包含键盘和串行驱动程序,从lab1
开始内核监视器就一直在使用,但现在你需要将它们附加到系统的其余部分。
练习9
让我们添加kbd_intr
处理IRQ_OFFSET+IRQ_KBD
和 serial_intr
处理IRQ_OFFSET+IRQ_SERIAL
。
直接在trap_dispatch
添加就行了。
case IRQ_OFFSET+IRQ_KBD:{lapic_eoi();kbd_intr();break;}case IRQ_OFFSET+IRQ_SERIAL:{lapic_eoi();serial_intr();break;}
我们在lib/console.c
中为你实现了控制台输入/输出文件类型。kbd_intr
和serial_intr
用最近读取的输入填充缓冲区,控制台文件类型清空缓冲区(默认情况下,控制台文件类型用于stdin/stdout
,除非用户重定向它们)。
The Shell
运行make run-icode
或make run-icode-nox
命令。这将运行您的内核并启动user/icode
。icode spawn init
,它将控制台设置为文件描述符0和1(标准输入和标准输出)。然后spawn sh
,shell
程序。你应该能够运行以下命令:
echo hello world | cat
cat lorem |cat
cat lorem |num
cat lorem |num |num |num |num |num
lsfd
请注意,用户库例程cprintf
直接打印到控制台,而不使用文件描述符代码。这适用于调试,但不适合与其他程序进行管道通信。要将输出打印到特定文件描述符(例如,1
,标准输出),请使用fprintf(1,“...”,...)
。 printf(“...”,...)
是打印到FD 1
的快捷方式。有关示例,请参阅user/lsfd.c
。
练习10
让我们实现user/sh.c
里面的<
重定向,里面就有>
的重定向,复制一下就没了。
// LAB 5: Your code here.if ((fd = open(t, O_RDONLY)) < 0) {cprintf("open %s for write: %e", t, fd);exit();}if (fd != 1) {dup(fd, 0);close(fd);}
到这里我们就做完了所有练习了。
然而我并不知道后面发生了什么。
所以我们来分析用户文件。
直接make qemu
,会输出下面这些东西。
/*在这之前的一大堆 都是 内核,和文件系统造成的*/
icode: read /motd
This is /motd, the message of the day.Welcome to the JOS kernel, now with a file system!icode: close /motd
icode: spawn /init
icode: exiting
init: running
init: data seems okay
init: bss seems okay
init: args: 'init' 'initarg1' 'initarg2'
init: running sh
init: starting sh
我们首先进入了user/icode.c
这个文件里面的umain
,也就是内核里面的那个环境创建运行的那个。
#include <inc/lib.h>void
umain(int argc, char **argv)
{int fd, n, r;char buf[512+1];binaryname = "icode";cprintf("icode startup\n");cprintf("icode: open /motd\n");//这几个值都看到了if ((fd = open("/motd", O_RDONLY)) < 0)//打开根目录下的/motdpanic("icode: open /motd: %e", fd);//我门可以找到find 找到motd 文件在 ./fs/ 目录下,所以fs 就是我们的根目录cprintf("icode: read /motd\n");while ((n = read(fd, buf, sizeof buf-1)) > 0)//读取了buf 个字符sys_cputs(buf, n);//输出了他cprintf("icode: close /motd\n");close(fd);cprintf("icode: spawn /init\n");if ((r = spawnl("/init", "init", "initarg1", "initarg2", (char*)0)) < 0)panic("icode: spawn /init: %e", r);//用spawnl创建了init ,所以接下来我们去了initcprintf("icode: exiting\n");
}
#include <inc/lib.h>struct {char msg1[5000];char msg2[1000];
} data = {"this is initialized data","so is this"
};char bss[6000];int
sum(const char *s, int n)
{int i, tot = 0;for (i = 0; i < n; i++)tot ^= i * s[i];return tot;
}void
umain(int argc, char **argv)
{int i, r, x, want;char args[256];//前面一大堆是初始化用的cprintf("init: running\n");want = 0xf989e;if ((x = sum((char*)&data, sizeof data)) != want)cprintf("init: data is not initialized: got sum %08x wanted %08x\n",x, want);elsecprintf("init: data seems okay\n");if ((x = sum(bss, sizeof bss)) != 0)cprintf("bss is not initialized: wanted sum 0 got %08x\n", x);elsecprintf("init: bss seems okay\n");// output in one syscall per line to avoid output interleaving strcat(args, "init: args:");for (i = 0; i < argc; i++) {strcat(args, " '");strcat(args, argv[i]);strcat(args, "'");}cprintf("%s\n", args);cprintf("init: running sh\n");// being run directly from kernel, so no file descriptors open yetclose(0);//因为根目录是在内二上,所以没有文件打开。if ((r = opencons()) < 0)//打开控制台??panic("opencons: %e", r);if (r != 0)panic("first opencons used fd %d", r);if ((r = dup(0, 1)) < 0)//把0复制到了 1panic("dup: %e", r);while (1) {cprintf("init: starting sh\n");r = spawnl("/sh", "sh", (char*)0);//这个时候又跑了shif (r < 0) {cprintf("init: spawn sh: %e\n", r);continue;}wait(r);//然后无线循环这个程序}
}
sh
里面就是我们的shell
了,告辞。
这篇关于MIT 6.828 (五) Lab 5: File system, Spawn and Shell的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!