这里已经到了《ORANGE'S:一个操作系统的实现》这本书的最后一章了,最后一章的内容不像之前的章节有个主题,而是做一些之前没有做过的东西。包括:让mkfs()只执行一次、从硬盘引导、将OS安装到真实的计算机。这篇文章记录尾声的第一个内容:让mkfs()只执行一次。
让mkfs()只执行一次
目前我们的系统每次启动都是"全新"的,因为每一次init_fs()都会调用mkfs()刷新硬盘,一定程度上这比较有利于我们调试------每次启动时可保证文件系统是一样的,但它也存在明显的坏处,那就是上次建立的文件到下一次启动时就不见了。我们下面就来改变这一现状,代码如下所示。
代码 fs/main.c,让mkfs()只执行一次。
cpp
PRIVATE void init_fs()
{
...
/* open the device: hard disk */
MESSAGE driver_msg;
driver_msg.type = DEV_OPEN;
driver_msg.DEVICE = MINOR(ROOT_DEV);
assert(dd_map[MAJOR(ROOT_DEV)].driver_nr != INVALID_DRIVER);
send_recv(BOTH, dd_map[MAJOR(ROOT_DEV)].driver_nr, &driver_msg);
/* read the super block of ROOT DEVICE */
RD_SECT(ROOT_DEV, 1);
sb = (struct super_block *)fsbuf;
if (sb->magic != MAGIC_V1) {
printl("{FS} mkfs\n");
mkfs(); /* make FS */
}
...
}
很简单,只需要每次先读取超级块,如果发现了魔数(Magic Number),则认为分区已经"装上Orange's了",否则调用mkfs()。
接下来你会发现,系统启动时不会每次都mkfs()了,但每次还是会执行一次解开cmd.tar的操作,无论是不是上次启动时解开过。我们这就来改一下untar(),让它解包之后就在cmd.tar这个文件中留个记号,下次看到记号,就不再傻傻得解包了。代码如下所示。
代码 kernel/main.c,修改untar()。
cpp
void untar(const char * filename)
{
printf("[extract '%s'\n", filename);
int fd = open(filename, O_RDWR);
assert(fd != -1);
char buf[SECTOR_SIZE * 16];
int chunk = sizeof(buf);
int i = 0;
int bytes = 0;
while (1) {
bytes = read(fd, buf, SECTOR_SIZE);
assert(bytes == SECTOR_SIZE); /* size of a TAR file must multiple of 512 */
if (buf[0] == 0) {
if (i == 0) {
printf(" read not unpack the file.\n");
}
break;
}
i++;
struct posix_tar_header * phdr = (struct posix_tar_header *)buf;
/* calculate the file size */
char * p = phdr->size;
int f_len = 0;
while (*p) {
f_len = (f_len * 8) + (*p++ - '0'); /* octal */
}
int bytes_left = f_len;
int fdout = open(phdr->name, O_CREAT | O_RDWR | O_TRUNC);
if (fdout == -1) {
printf(" failed to extract file: %s\n", phdr->name);
printf(" aborted]\n");
close(fd);
return;
}
printf(" %s\n", phdr->name);
while (bytes_left) {
int iobytes = min(chunk, bytes_left);
read(fd, buf, ((iobytes - 1) / SECTOR_SIZE + 1) * SECTOR_SIZE);
bytes = write(fdout, buf, iobytes);
assert(bytes == iobytes);
bytes_left -= iobytes;
}
close(fdout);
}
if (i) {
lseek(fd, 0, SEEK_SET);
buf[0] = 0;
bytes = write(fd, buf, 1);
assert(bytes == 1);
}
close(fd);
printf(" done, %d files extracted]\n", i);
}
这里增加了一个变量i,表示从cmd.tar中总共解出来多少文件。每次功能的解包操作之后(这时i必大于0),我们将cmd.tar的第一个字节置为0,这样下一次untar()执行到第15行时会发现第一个字节为零,于是退出。
如果我们增加或者改写了应用程序,通常会使用dd重新将cmd.tar写入磁盘。由于TAR文件的开始处是包含其中的文件的文件名,所以第一个字节必不为零,于是再次启动时,untar()发现第一个字节非零,从而进入解包的步骤。
值得注意的一点是再次解包时,很可能包内包含的文件已经在磁盘上存在了,所以我们需要将原来的文件内容清除,然后写入新内容,这需要引入O_TRUNC,加入到open()的参数中,见代码第33行。
引入O_TRUNC后,我们还要修改文件系统中的do_open(),代码如下所示。
代码 fs/open.c,修改do_open()。
cpp
/**
* Open a file and return the file descriptor.
*
* @return File descriptor if successful, otherwise a negative error code.
*/
PUBLIC int do_open()
{
...
int inode_nr = search_file(pathname);
struct inode * pin = 0;
if (inode_nr == INVALID_INODE) { /* file not exists */
if (flags & O_CREAT) {
pin = create_file(pathname, flags);
} else {
printl("{FS} file not exists: %s\n", pathname);
return -1;
}
} else if (flags & O_RDWR) { /* file exists */
if ((flags & O_CREAT) && (!(flags & O_TRUNC))) {
assert(flags == (O_RDWR | O_CREAT));
printl("{FS} file exists: %s\n", pathname);
return -1;
}
assert((flags == O_RDWR ) ||
(flags == (O_RDWR | O_TRUNC )) ||
(flags == (O_RDWR | O_TRUNC | O_CREAT)));
char filename[MAX_PATH];
struct inode * dir_inode;
if (strip_path(filename, pathname, &dir_inode) != 0) {
return -1;
}
pin = get_inode(dir_inode->i_dev, inode_nr);
} else { /* file exists, no O_RDWR flag */
printl("{FS} file exists: %s\n", pathname);
return -1;
}
if (flags & O_TRUNC) {
assert(pin);
pin->i_size = 0;
sync_inode(pin);
}
if (pin) {
...
} else {
return -1;
}
return fd;
}
引入O_TRUNC之后do_open()的逻辑复杂了若干,大致可以描述为:
- 如果文件不存在,则只要有O_CREAT就成功,没有就失败。
- 如果文件存在,则判断是否有O_RDWR,若没有则失败,若有的话,则分以下情况:
- 仅有O_RDWR,成功
- 有O_CREAT但无O_TRUNC,失败
- 有O_TRUNC(无论有没有O_CREAT),成功
代码中还使用了一个新的函数lseek(),它的作用是重新定位读或写的文件偏移量。它的代码如下所示,首先查看接用户口文件代码。
代码 lib/lseek.c,这是新建的文件。
cpp
/**
* Reposition r/w file offset.
*
* @param fd File descriptor.
* @param offset The offset according to `whence'.
* @param whence SEEK_SET, SEEK_CUR or SEEK_END.
*
* @return The resulting offset location as measured in bytes from the beginning of the file.
*/
PUBLIC int lseek(int fd, int offset, int whence)
{
MESSAGE msg;
msg.type = LSEEK;
msg.FD = fd;
msg.OFFSET = offset;
msg.WHENCE = whence;
send_recv(BOTH, TASK_FS, &msg);
return msg.OFFSET;
}
lseek()函数会向task_fs发送一个类型为LSEEK的消息,然后task_fs会在接收到这个消息后调用do_lseek()函数进行处理。
代码 fs/open.c,do_lseek()。
cpp
/**
* Handle the message LSEEK.
*
* @return The new offset in bytes from the beginning of the file if successful,
* otherwise a negative number.
*/
PUBLIC int do_lseek()
{
int fd = fs_msg.FD;
int off = fs_msg.OFFSET;
int whence = fs_msg.WHENCE;
int pos = pcaller->filp[fd]->fd_pos;
int f_size = pcaller->filp[fd]->fd_inode->i_size;
switch (whence) {
case SEEK_SET:
pos = off;
break;
case SEEK_CUR:
pos += off;
break;
case SEEK_END:
pos = f_size + off;
break;
default:
return -1;
break;
}
if ((pos > f_size) || (pos < 0)) {
return -1;
}
pcaller->filp[fd]->fd_pos = pos;
return pos;
}
在task_fs中添加处理LSEEK消息代码。
代码 fs/main.c,处理LSEEK消息。
cpp
/**
* <Ring 1> The main loop of TASK FS.
*/
PUBLIC void task_fs()
{
...
case LSEEK:
fs_msg.OFFSET = do_lseek();
break;
...
}
好了,我们现在可以make并运行程序了,还是提醒一下,不要忘记更改Makefile。运行结果如下图所示。
再次运行时,mkfs()和解开cmd.tar的操作就省略了。
欢迎关注我的公众号
公众号中对应文章附有当前文章代码下载说明。