《ORANGE-s-一个操作系统实现》文件管理(7-2)

在硬盘上制作一个文件系统

文件系统通常有两个含义:

  • 用于存储和组织计算机文件数据的一套方法
  • 存在于某介质上的具备某种格式的数据
    我们所做的工作是第一种,所以我们需要考虑如何利用空间,如何对文件进行添加、删除以及修改,还要考虑不同类型的文件如何并存于一个文件系统。
    当我们说某个硬盘分区是“某某文件系统”时,是说的第二种,其格式表名这个分区是由某种策略和机制来管理。

    文件系统设计的数据结构

    主要有超级块、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这个宏来完成的。
    下面来执行一下,结构如图:
    mark

创建文件

对文件进行创建以及读写等操作,需要用到open()、write()、read()、close()等系统调用,而这些熊调用都用到了一个变量——fd,文件描述符(file descriptor)。如下是它的结构图:
mark
其中fd_mode用来记录这个fd是用来做什么操作的。fd_pos用来记录读写到了文件的什么位置。fd_inode便是指向inode的指针。
每当一个进程打开一个文件——无论是打开一个已经存在的还是创建一个新的,该进程的进程变filp数组就会分配一个文职——假设是k,用于存放打开文件的fd指针,而这个k就是返回给用户进程open()函数的返回值了。

open()

现在用户进程中创建一个文件:

1
2
3
4
5
6
7
void 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
15
PUBLIC 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
31
PUBLIC 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()两个系统调用,我们接着运行一下:
mark
可以看到进程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
    12
    PUBLIC 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
12
PUBLIC 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
39
void 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");
}

运行结果如图;
mark

文件系统调试

随着代码越来越多,我们需要其他调试手段,比如我们有了硬盘驱动和文件系统,我们可以直接开始写log。由于文件系统比较初级,所以Ilog可以直接通过硬盘驱动写入某个扇区。
下图是log。我们可以将log写出某种特定的格式,比如这个存成DOT源文件,然后用dd命令将磁盘映像中我们写入的log扇区抽取出来,存成本地文件,用bash脚本或线程的工具将log中的dot部分抽取出来,存成一个或多个文件,然后用graphviz包里面的工具将其装换成可视文件格式。
mark

删除文件

删除是添加的反过程,以下是我们要做的工作:

  • 释放inode-map中的相应位
  • 释放sector-map中的相应位。
  • 将inode_array中的i-node清零
  • 删除根目录中的目录项
    运行结果如下:
    mark
    我们创建了”foo”、”bar”、”baz”三个文件,然后删除,另外删除”/dev_tty0”,这时也被制止了。

    为文件系统添加系统调用的步骤

  1. 定义一种消息,比如MMM。
  2. 写一个函数来处理MMM消息。
  3. 修改task_fs(),增加对消息MMM的处理。
  4. 写一个用户接口函数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传送了多少字符。
您的支持将鼓励我继续创作!