代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
接下来就可以来创建文件了
创建文件的步骤如下:
- 文件需要inode来描述大小、位置等属性,所以创建文件就要创建其inode,向inode_bitmap申请位图来获得inode号,因此inode_bitmap会被更新,inode_table数组中的某项也会由新的 inode 填充。
- inode_i_sectors是文件具体存储的扇区地址,这需要向block_bitmap申请可用位来获得可用的块(为简化处理,1块等于1扇区),因此block_bitmap会被更新,分区的数据区data_start_lba以后的某个扇区会被分配。
- 新增加的文件必然存在于某个目录,所以该目录的inode->i_size会增加个目录项的大小。此新增加的文件对应的目录项需要写入该目录的inode->i_sectors[]中的某个扇区,原有扇区可能已满,所以有可能要申请新扇区来存储目录项。
- 若其中某步操作失败,需要回滚之前已成功的操作。
- inode_bitmap、block_bitmap、新文件的inode及文件所在目录的inode,这些位于内存中已经被改变的数据要同步到硬盘。
修改/fs/file.c
/* 创建文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_create(struct dir *parent_dir, char *filename, uint8_t flag) {
/* 后续操作的公共缓冲区 */
void *io_buf = sys_malloc(1024);
if (io_buf == NULL) {
printk("in file_creat: sys_malloc for io_buf failed\n");
return -1;
}
uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态
/* 为新文件分配inode */
int32_t inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1) {
printk("in file_creat: allocate inode failed\n");
return -1;
}
/* 此inode要从堆中申请内存,不可生成局部变量(函数退出时会释放)
* 因为file_table数组中的文件描述符的inode指针要指向它.*/
struct inode *new_file_inode = (struct inode *) sys_malloc(sizeof(struct inode));
if (new_file_inode == NULL) {
printk("file_create: sys_malloc for inode failded\n");
rollback_step = 1;
goto rollback;
}
inode_init(inode_no, new_file_inode); // 初始化i结点
/* 返回的是file_table数组的下标 */
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1) {
printk("exceed max open files\n");
rollback_step = 2;
goto rollback;
}
file_table[fd_idx].fd_inode = new_file_inode;
file_table[fd_idx].fd_pos = 0;
file_table[fd_idx].fd_flag = flag;
file_table[fd_idx].fd_inode->write_deny = false;
struct dir_entry new_dir_entry;
memset(&new_dir_entry, 0, sizeof(struct dir_entry));
create_dir_entry(filename, inode_no, FT_REGULAR, &new_dir_entry); // create_dir_entry只是内存操作不出意外,不会返回失败
/* 同步内存数据到硬盘 */
/* a 在目录parent_dir下安装目录项new_dir_entry, 写入硬盘后返回true,否则false */
if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) {
printk("sync dir_entry to disk failed\n");
rollback_step = 3;
goto rollback;
}
memset(io_buf, 0, 1024);
/* b 将父目录i结点的内容同步到硬盘 */
inode_sync(cur_part, parent_dir->inode, io_buf);
memset(io_buf, 0, 1024);
/* c 将新创建文件的i结点内容同步到硬盘 */
inode_sync(cur_part, new_file_inode, io_buf);
/* d 将inode_bitmap位图同步到硬盘 */
bitmap_sync(cur_part, inode_no, INODE_BITMAP);
/* e 将创建的文件i结点添加到open_inodes链表 */
list_push(&cur_part->open_inodes, &new_file_inode->inode_tag);
new_file_inode->i_open_cnts = 1;
sys_free(io_buf);
return pcb_fd_install(fd_idx);
/*创建文件需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
rollback:
switch (rollback_step) {
case 3:
/* 失败时,将file_table中的相应位清空 */
memset(&file_table[fd_idx], 0, sizeof(struct file));
case 2:
sys_free(new_file_inode);
case 1:
/* 如果新文件的i结点创建失败,之前位图中分配的inode_no也要恢复 */
bitmap_set(&cur_part->inode_bitmap, inode_no, 0);
break;
}
sys_free(io_buf);
return -1;
}函数file_create接受3个参数,父目录partent_dir、文件名filename、创建标识flag,功能是在目录 parent_dir中以模式flag去创建普通文件filename,若成功则返回文件描述符,即pcb->fd_table中的下标,否则返回-1。
file_create是基于函数inode_sync和sync_dir_entry。 这两个是往硬盘上写数据的函数,其原理是需要先把原扇区的数据读到内存,在内存中将数据变更后再写入硬盘扇区,所以用于变更数据的内存缓冲区是不可少的。 往硬盘上同步数据的操作往往是诸多步骤中的最后一步,如果在这类函数内部申请内存作为缓冲区,万一内存不足,则往硬盘上同步数据就会失败,那之前所做的所有工作都会白费,所以在创建文件之初就应该把缓冲区准备好,如果申请内存失败了也不会多做无用功,也避免了最后无法同步到硬盘而造成数据不一致、回滚操作过多的情况发生。 一般情况下硬盘操作都是一次读写一个扇区,考虑到有数据会跨扇区的情况,故申请2个扇区大小的缓冲区,因此在函数开头就先申请了1024字节的缓冲区io_buf。
现在再说一下回滚,回滚就是资源变更后,将资源恢复到未修改前的状态。 文件系统是一套资源管理的方法,每变动一种数据就会涉及到多种资源的“联动”,这里所说的“联动”意指一个事物的状态改变后,周边事物也要一同跟着变动的连锁反应。 按理说相关资源的联动应该是一个事务,具有原子性,
创建文件包括多个修改资源的步骤,我们创建新文件的顺序是:创建文件i结点->文件描述符fd->目录项。 这种从后往前创建步骤的好处是每一步创建失败时回滚操作少。 不过随着步骤的递增,失败时回滚的步骤也将递增。 这里uint8_t rollback_step = 0定义了变量rollback_step用于记录回滚步骤。 真正用于回滚的代码是在标签rollback:处,那里有3部分的回滚操作,只列出了有可能会失败的操作,从上到下依次是case 3、 case2、 case1,各case之间没有break,它们是一种累加的回滚,因此case3执行的回滚操作最多,casel最少,需要回滚时,只要将rollback_step置为相应的case就好了。
int32_t inode_no = inode_bitmap_alloc(cur_part)调用inode_bitmap_alloc为新文件分配inode,struct inode *new_file_inode = (struct inode *) sys_malloc(sizeof(struct inode))为新文件的inode也就是new_file_inode申请内存,如果内存申请失败,rollback_step置为1,程序跳转到rollback处,根据rollback_step的值,会执行分支case 1,即执行bitmap_set(&cur_part->inode_bitmap, inode_no, 0);的bitmap_set回滚位图状态。如果内存分配成功的话,执行inode_init 初始化 new_file_inode。
接下来调用get_free_slot_in_global从file_talbe中获取空闲文件结构的下标,写入变量fd_idx中。如果 file_table中没有空闲位则返回-1,于是将rollback_step置为2,依然是通过goto语句跳转到rollback处,执行 case2处的代码,执行sys_free(new_file_inode)释放new_file_inode的内存,然后执行casel恢复inode位图。
file_table[fd_idx]初始化文件表中的文件结构, struct dir_entry new_dir_entry和memset(&new_dir_entry, 0, sizeof(struct dir_entry))为文件创建新目录项new_dir_entry,并将其清0,create_dir_entry(filename, inode_no, FT_REGULAR, &new_dir_entry)调用create_dir_entry用filename、inode_no和FT_REGULAR填充new_dir_entry。准备好目录项后,接着在if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf))通过函数sync_dir_entry (parent_dir, &new_dir_entry, io_buf)将其写入到父目录parent_dir 中,如果失败,rollback_step 置为3,执行回滚。
sync_dir_entry 会改变父目录 inode中的信息,因此在inode_sync(cur_part, parent_dir->inode, io_buf)调用函数 inode_sync将父目录 inode同步到硬盘。接着在第inode_sync(cur_part, new_file_inode, io_buf)和bitmap_sync(cur_part, inode_no, INODE_BITMAP)分别将新文件的inode同步到硬盘,将inode_bitmap位图同步到硬盘。list_push(&cur_part->open_inodes, &new_file_inode->inode_tag)将新文件的inode添加到inode列表,也就是cur_part->open_inodes,随后在其i_open_cnts置为1。以上几个同步操作一般不会出问题,因此这并未有相应的回滚。硬盘操作到这就结束了,在sys_free(io_buf)将io_buf释放,然后在return pcb_fd_install(fd_idx)调用peb_fd_install(fd_idx),在数组peb->fd_table中找个空闲位安装fd_idx,若成功则返回空闲位的下标,若失败则返回-1,用return将其返回值返回。
接着就是实现 sys_open,也就是创建文件的真正逻辑,是open函数的内核级实现。
修改/fs/fs.c
/* 打开或创建文件成功后,返回文件描述符,否则返回-1 */
int32_t sys_open(const char* pathname, uint8_t flags) {
/* 对目录要用dir_open,这里只有open文件 */
if (pathname[strlen(pathname) - 1] == '/') {
printk("can`t open a directory %s\n",pathname);
return -1;
}
ASSERT(flags <= 7);
int32_t fd = -1; // 默认为找不到
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
/* 记录目录深度.帮助判断中间某个目录不存在的情况 */
uint32_t pathname_depth = path_depth_cnt((char*)pathname);
/* 先检查文件是否存在 */
int inode_no = search_file(pathname, &searched_record);
bool found = inode_no != -1 ? true : false;
if (searched_record.file_type == FT_DIRECTORY) {
printk("can`t open a direcotry with open(), use opendir() to instead\n");
dir_close(searched_record.parent_dir);
return -1;
}
uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);
/* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
if (pathname_depth != path_searched_depth) { // 说明并没有访问到全部的路径,某个中间目录是不存在的
printk("cannot access %s: Not a directory, subpath %s is`t exist\n", \
pathname, searched_record.searched_path);
dir_close(searched_record.parent_dir);
return -1;
}
/* 若是在最后一个路径上没找到,并且并不是要创建文件,直接返回-1 */
if (!found && !(flags & O_CREAT)) {
printk("in path %s, file %s is`t exist\n", \
searched_record.searched_path, \
(strrchr(searched_record.searched_path, '/') + 1));
dir_close(searched_record.parent_dir);
return -1;
} else if (found && flags & O_CREAT) { // 若要创建的文件已存在
printk("%s has already exist!\n", pathname);
dir_close(searched_record.parent_dir);
return -1;
}
switch (flags & O_CREAT) {
case O_CREAT:
printk("creating file\n");
fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
dir_close(searched_record.parent_dir);
// 其余为打开文件
}
/* 此fd是指任务pcb->fd_table数组中的元素下标,
* 并不是指全局file_table中的下标 */
return fd;
}sys_open函数接受2个参数,pathname是待打开的文件,其为绝对路径,flags是打开标识,其值便 是之前在fs.h头文件中提前放入的enum oflags。函数功能是打开或创建文件成功后,返回文件描述符,即 pcb中fd_table中的下标,否则返回-1。
目录以字符”/”结尾,如”/a/”,a便是指目录,sys_open只支持文件打开,不支持目录打开,因此程序 开头判断pathname是否为目录,这里是对pathname的最后一个字符判断,即若pathname[strlen(pathname) -1]等于”/”,就表示pathname为目录,打印提示信息并返回-1。
ASSERT(flags <= 7)是限制flags的值在O_RDONLY|O_WRONLY|O_RDWR| O_CREAT 之内。int32_t fd = -1声明了变量fd,为其初始化为-1,即默认找不到文件。
struct path_search_record searched_record生成了路径搜索记录变量searched_record,path_search_record用来记录文件查找时所遍历过的目录,它会作为参数传给函数search_file,其值由函数search_file填充。当查找失败时,主调函数可以根据此结构了解在哪层子目录下失败了。若是创建文件,便可直接获得所创建文件的父目录,即在哪个目录下创建文件。path_search_record中一个很重要的成员是searched_path,它用来记录所处理过的路径,可以通过该路径的长度来判断查找是否成功。举个例子,若查找目标文件c,它的绝对路径是”/a/b/c”,查找时若 发现 b 目录不存在,存入 path_search_record.searched_path的内容便是”/a/b”,若按照此路径找到了c,path_search_record.searched_path 的值便是完整路径”/a/b/c”。
memset(&searched_record, 0, sizeof(struct path_search_record))将searched_record清0。由于searched_record位于栈中,栈中数据并不会自动清0,这非常容易出问题。尤其是在连续无间隔调用sys_open打开不同文件的时候,如果有的文件不存在,有的中间路径缺失,有的成功打开, search_file函数会在 searched_record中记录不同的状态,下一次调用sys_open,searched_recored还是指向栈中相同的位置,栈中数据不会自动清0,所以数据还是上一次调用中的结果,这影响sys_open中相关代码的判断结果,所以在使用前一定要先清0。
uint32_t pathname_depth = path_depth_cnt((char*)pathname)通过path_depth_ent计算pathname的深度,深度值写入变量pathname_depth,计算目录深度 的目的是帮助判断某个目录不存在的情况。无论是打开文件,还是创建文件,都要先判断文件是否已存在,在int inode_no = search_file(pathname, &searched_record)调用search_file 搜索文件 pathname,搜索结果存入 searched_record。bool found = inode_no != -1 ? true : false为 bool 变量 found 赋值。
if (searched_record.file_type == FT_DIRECTORY)判断 pathname 的判断,若为目录,在printk(“can`t open a direcotry with open(), use opendir() to instead\n”)打印提示信息,接着调用 dir_close 关闭目录 searched_record.parent_dir,并返回-1。search_file返回的path_search_record.parent_dir由 主调函数负责关闭,原因是主调函数有可能会用到此目录,也许会在该目录下创建文件。
uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path)调用 path_depth_cnt 计算 searched_record.searched_path 的路径深度,值写入变量 path_searched depth 中。接着在if (pathname_depth != path_searched_depth)用原路径 pathname的深度值 pathname_depth 与 path_searched_depth 对比,若不相等,说明查找文件的过程中并没有访问到全部的路径,某个中间目录或最后目标不存在,总之检索失败, 于是输出报错信息,告诉用户哪个子目录不存在,便于用户知道在哪里输错了。接着关闭目录, 返回-1。
当flags包含 O_CREAT时, open函数可以创建文件。在if (!found && !(flags & O_CREAT))判断,若目标文件未找到,并且 flags 不包含 O_CREAT,这说明想打开的文件不存在,并不是想创建文件,因此在下一行输出报错信息,然后关闭目录searched_record.parent_dir并返回-1。
else if (found && flags & O_CREAT)判断,若找到了文件并且flags包含O_CREAT,这说明想创建的文件名已存在,相同目录下不允许同名文件存在,因此输出报错并关闭目录,返回-1。
switch (flags & O_CREAT)的switch 结构根据flags中是否包括O_CREAT 的情况暂时只建立了。_CREAT分支,即目前只支持sys_open(“xxx”,O_CREAT|O_XXX)的用法。创建文件是用file_create实现的,因此在fd = file_create(searched_record.parent_dir, (strrchr(pathname, ‘/’) + 1), flags)调用file_create创建文件,返回文件描述符存入变量fd,完成后关闭目录searched_record.parent_dir,并返回fd,至此创建文件完成。
继续修改/fs/fs.c
#include "file.h"
更改filesys_init函数
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
uint8_t channel_no = 0, dev_no, part_idx = 0;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
if (sb_buf == NULL) {
PANIC("alloc memory failed!");
}
printk("searching filesystem......\n");
while (channel_no < channel_cnt) {
dev_no = 0;
while(dev_no < 2) {
if (dev_no == 0) { // 跨过裸盘hd60M.img
dev_no++;
continue;
}
struct disk* hd = &channels[channel_no].devices[dev_no];
struct partition* part = hd->prim_parts;
while(part_idx < 12) { // 4个主分区+8个逻辑
if (part_idx == 4) { // 开始处理逻辑分区
part = hd->logic_parts;
}
/* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
* partition又为disk的嵌套结构,因此partition中的成员默认也为0.
* 若partition未初始化,则partition中的成员仍为0.
* 下面处理存在的分区. */
if (part->sec_cnt != 0) { // 如果分区存在
memset(sb_buf, 0, SECTOR_SIZE);
/* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
ide_read(hd, part->start_lba + 1, sb_buf, 1);
/* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
if (sb_buf->magic == 0x19590318) {
printk("%s has filesystem\n", part->name);
} else { // 其它文件系统不支持,一律按无文件系统处理
printk("formatting %s`s partition %s......\n", hd->name, part->name);
partition_format(part);
}
}
part_idx++;
part++; // 下一分区
}
dev_no++; // 下一磁盘
}
channel_no++; // 下一通道
}
sys_free(sb_buf);
/* 确定默认操作的分区 */
char default_part[8] = "sdb1";
/* 挂载分区 */
list_traversal(&partition_list, mount_partition, (int)default_part);
/* 将当前分区的根目录打开 */
open_root_dir(cur_part);
/* 初始化文件表 */
uint32_t fd_idx = 0;
while (fd_idx < MAX_FILE_OPEN) {
file_table[fd_idx++].fd_inode = NULL;
}
}更改的地方就是在filesys_init函数最后添加几行代码
调用 open_root_dir打开根目录和初始化文件表,使文件结构中的 fd_inode 置为 NULL,表示该文件结构为空位,可分配。
修改/thread/thread.c的init_thread函数
将fd_table标准输入输出先空出来,其余全部置为-1。
/*线程初始化*/
/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
pthread->pid = allocate_pid();
strcpy(pthread->name, name);
if (pthread == main_thread) {
/* 由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
pthread->status = TASK_RUNNING;
} else {
pthread->status = TASK_READY;
}
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->priority = prio;
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL;
/* 标准输入输出先空出来 */
pthread->fd_table[0] = 0;
pthread->fd_table[1] = 1;
pthread->fd_table[2] = 2;
/* 其余的全置为-1 */
uint8_t fd_idx = 3;
while (fd_idx < MAX_FILES_OPEN_PER_PROC) {
pthread->fd_table[fd_idx] = -1;
fd_idx++;
}
pthread->stack_magic = 0x19870916; // 自定义的魔数
}
我们修改main.c创建文件测试一下:
添加sys_open(“/file1”, O_CREAT)
int main(void) {
put_str("I am kernel\n");
init_all();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
sys_open("/file1", O_CREAT);
while(1);
return 0;
}
执行效果
同个目录下不能有同名文件
第一次执行

第二次执行

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


