在硬盘上制作一个文件系统
文件系统通常有两个含义:
- 用于存储和组织计算机文件数据的一套方法
- 存在于某介质上的具备某种格式的数据
我们所做的工作是第一种,所以我们需要考虑如何利用空间,如何对文件进行添加、删除以及修改,还要考虑不同类型的文件如何并存于一个文件系统。
当我们说某个硬盘分区是“某某文件系统”时,是说的第二种,其格式表名这个分区是由某种策略和机制来管理。文件系统设计的数据结构
主要有超级块、i-node和目录项。
超级块主要关注以下内容: - 文件系统的标识。
- 文件系统最多允许有多少个i-node。
- inode_array占用多少扇区。
- 文件系统总共扇区数是多少
- inode-map占用多少扇区
- sector-map占用多少扇区
- 第一个数据扇区的扇区号是多少
- 根目录区的i-node号是多少
编码建立文件系统
建立文件系统的函数mkfs()分为如下几部分:
- 向硬盘驱动程序索取ROOT_DEV的起始扇区和大小
- 建立超级块
- 建立inode-map
- 建立sector-map
- 写入inode-array
- 建立根目录文件
在函数mkfs()中,所有写入磁盘的内容都是先放进fsbuf这个缓冲区的,与通常的做法不同,没有定义一个数组,而是定义了一个指针,让它指向0x600000.
也就是说,指定内存地址6MB~7MB为系统文件的缓冲区。在建立FS的过程中,写扇区的函数都是WR_SECT这个宏来完成的。
下面来执行一下,结构如图:
创建文件
对文件进行创建以及读写等操作,需要用到open()、write()、read()、close()等系统调用,而这些熊调用都用到了一个变量——fd,文件描述符(file descriptor)。如下是它的结构图:
其中fd_mode用来记录这个fd是用来做什么操作的。fd_pos用来记录读写到了文件的什么位置。fd_inode便是指向inode的指针。
每当一个进程打开一个文件——无论是打开一个已经存在的还是创建一个新的,该进程的进程变filp数组就会分配一个文职——假设是k,用于存放打开文件的fd指针,而这个k就是返回给用户进程open()函数的返回值了。
open()
现在用户进程中创建一个文件:1
2
3
4
5
6
7void TestA()
{
int fd = open("/blah",O_CREAT);
printf("fd:%d\n",fd);
close(fd);
spin("TESTA");
}
下面是open()的系统调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15PUBLIC int open(const char *pathname, int flags)
{
MESSAGE msg;
msg.type = OPEN;
msg.PATHNAME = (void*)pathname;
msg.FLAGS = flags;
msg.NAME_LEN = strlen(pathname);
send_recv(BOTH, TASK_FS, &msg);
assert(msg.type == SYSCALL_RET);
return msg.FD;
}
发了一个OPEN消息给文件系统,所以文件系统需要处理它:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31PUBLIC void task_fs()
{
printl("Task FS begins.\n");
init_fs();
while (1) {
send_recv(RECEIVE, ANY, &fs_msg);
int src = fs_msg.source;
pcaller = &proc_table[src];
switch (fs_msg.type) {
case OPEN:
fs_msg.FD = do_open();
break;
case CLOSE:
fs_msg.RETVAL = do_close();
break;
.....
default:
dump_msg("FS::unknown message:", &fs_msg);
assert(0);
break;
}
/* reply */
fs_msg.type = SYSCALL_RET;
send_recv(SEND, src, &fs_msg);
}
}
其中用do_open来专门处理OPEn消息,首先是从消息内对出各项参数,其中需要格外注意的是文件名的读取。
创建文件所设计的其他函数
strip_path()
该函数用于把路径分为文件名和文件夹两个部分,定位直接包含文件的文件夹,并得到给定文件在此文件夹中的名称。
search_file()
用来得到文件所在目录的i-node,通过这个i-node来得到目录所在的扇区,然后读取这些扇区,查看里面是否有我们要找的文件,如果找到就返回文件的i-node,如果没有就返回0.
do_clode()
关闭文件显得十分简单,CLOSE消息是由do_close()来处理。
完成了open()和close()两个系统调用,我们接着运行一下:
可以看到进程TestA打印出了新创建的文件的fd:0。
打开文件
打开文件其实就是根据文件名找到i-node,并且建立进程表、f_desc_table[]和inode_table[]之间的关联。对于普通文件而言,打开操作有以下情况:
- 文件存在。这时我们获得文件的i-node号,读出i-node,建立前面所述三表的关联,并返回fd。
- 文件不存在。直接返回-1。
- 文件不存在。创建文件,建立前面所述的关联,并返回fd。
读写文件
因为采取一次分配的原则,所以读写变得比较简单,但是这个方式并不好。在读写的过程中,我们任然是把它扔给相应的驱动程序——虽然驱动程序并未准备还处理,但发送一个消息只是举手之劳。下面是read()函数1
2
3
4
5
6
7
8
9
10
11
12PUBLIC int read(int fd, void *buf, int count)
{
MESSAGE msg;
msg.type = READ;
msg.FD = fd;
msg.BUF = buf;
msg.CNT = count;
send_recv(BOTH, TASK_FS, &msg);
return msg.CNT;
}
下面是write():1
2
3
4
5
6
7
8
9
10
11
12PUBLIC int write(int fd, const void *buf, int count)
{
MESSAGE msg;
msg.type = WRITE;
msg.FD = fd;
msg.BUF = (void*)buf;
msg.CNT = count;
send_recv(BOTH, TASK_FS, &msg);
return msg.CNT;
}
测试文件读写
修改TestA代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void TestA()
{
int fd;
int n;
const char filename[] = "blah";
const char bufw[] = "abcde";
const int rd_bytes = 3;
char bufr[rd_bytes];
assert(rd_bytes <= strlen(bufw));
/* create */
fd = open(filename, O_CREAT | O_RDWR);
assert(fd != -1);
printf("File created. fd: %d\n", fd);
/* write */
n = write(fd, bufw, strlen(bufw));
assert(n == strlen(bufw));
/* close */
close(fd);
/* open */
fd = open(filename, O_RDWR);
assert(fd != -1);
printf("File opened. fd: %d\n", fd);
/* read */
n = read(fd, bufr, rd_bytes);
assert(n == rd_bytes);
bufr[n] = 0;
printf("%d bytes read: %s\n", n, bufr);
/* close */
close(fd);
spin("TestA");
}
运行结果如图;
文件系统调试
随着代码越来越多,我们需要其他调试手段,比如我们有了硬盘驱动和文件系统,我们可以直接开始写log。由于文件系统比较初级,所以Ilog可以直接通过硬盘驱动写入某个扇区。
下图是log。我们可以将log写出某种特定的格式,比如这个存成DOT源文件,然后用dd命令将磁盘映像中我们写入的log扇区抽取出来,存成本地文件,用bash脚本或线程的工具将log中的dot部分抽取出来,存成一个或多个文件,然后用graphviz包里面的工具将其装换成可视文件格式。
删除文件
删除是添加的反过程,以下是我们要做的工作:
- 释放inode-map中的相应位
- 释放sector-map中的相应位。
- 将inode_array中的i-node清零
- 删除根目录中的目录项
运行结果如下:
我们创建了”foo”、”bar”、”baz”三个文件,然后删除,另外删除”/dev_tty0”,这时也被制止了。为文件系统添加系统调用的步骤
- 定义一种消息,比如MMM。
- 写一个函数来处理MMM消息。
- 修改task_fs(),增加对消息MMM的处理。
- 写一个用户接口函数XXX()
将TTY纳入文件系统
假设进程P要求读取TTY,它会发送消息给文件系统,文件系统将消息传递给TTY,TTY记下发出请求的进程号等信息之后立即返回,而文件系统这时并不对P接触阻塞,因为结果还没有准备好,在接下来的过程中,文件系统像往常一样等待来自任何进程的请求。而TTY则会将键盘输入复制进P传入的内存地址,一直遇到回车,TTY就告诉文件系统,P的请求已被满足,文件系统会接触对P的阻塞,于是整个读取工作结束。
在写TTY时,P发消息给文件系统,文件 系统传递给TTY,TTY收到消息后立即将字符写入显存,完成后发消息给文件系统,文件系统再发消息给P,整个过程结束。
作为驱动程序,TTY接受并处理DEV_OPEN、DEV_READ、DEV_WRITE消息。
DEV_READ、DEV_WRITE分别有对应的函数tty_do_read()、tty_do_write()来处理。
tty_caller用来保存想TTY发送消息的进程的进程号。tty_procnr用来保存请求数据的进程的进程号。tty_req_buf保存进程P用来存放读入字符的缓冲区的线性地址。tty_left_cnt保存P想读入的字符数。tty_trans_cnt保存TTY已经向P传送了多少字符。