十七. 文件系统三(文件的创建)

文件描述符

在之前介绍的概念中,inode是用来表示一个文件的,用于描述文件的存储信息,文件的权限等。但这个inode结构是操作系统为自己的文件系统准备的数据结构,仅供其内部使用,与用户的关系不大,接下来要介绍的文件描述符才是用户能够使用的结构。

在linux系统中,读写文件的本质是先通过文件的inode找到文件数据块的扇区地址,随后对这些扇区的数据进行读写,从而实现了文件的读写。

对用户进程来说,一个进程可以多次的打开同一个文件,一个文件也可以被多个进行同时打开。对于这个被多次打开的文件,每打开一次,都需要一个结构来记录该文件目前的状态。比如说A进行第一次打开1.txt的时候,读取到了第10行的位置,第二次打开1.txt的时候,读取到了20行的位置。这里的10行,20行就是打开的文件状态中的文件偏移量,它记录的是相对于文件首地址的一个偏移。即使一个文件被同时多次打开,各自操作的偏移量也互不影响。

也就是说,会有一个文件结构来描述文件打开后,文件读写的偏移量等信息。一个文件对应一个inode,一个文件可以被多次打开,即一个inode对应多个文件结构。

文件描述符的基本结构如下

文件描述符的结构

接下来通过linux的open函数来了解一下文件描述符

open的函数原型如下

1
int open(const char *pathname, inf flags);

成功调用该函数之后,它会返回文件pathname的文件描述符,该返回值是一个int的数值,肯定不是代表真正的文件描述符结构。它是作为进程pcb中文件描述符数组的下标索引。通过这个下标在进程pcb中找到某个数据。大家可能会认为这个数据就是真正的文件描述符了。其实不然,在进程pcb的文件描述符数组中,记录的任然不是真正的文件描述符结构,它是一个指针数组,数组中的数据指向文件表中某个文件结构,该文件结构就是真正记录被打开文件的信息所在。

该过程比较复杂,将其分解为下面四步

  1. 调用open函数,得到一个文件描述符
  2. 将第一步得到的文件描述符作为进程pcb中文件描述符数组的下标,取得该下标对应的数据
  3. 将第二步得到的数据,相当于是一个指针,从这个指针中取到对应的文件结构
  4. 从该文件结构中获取到记录的文件信息

通过一副图来描述一下这个过程

mark

看到这里获取会有一个疑问,为什么pcb中不直接记录文件的描述符信息,而是指向了其他的区域。

因为记录文件信息的描述符较大,每打开一次文件,就需要记录一次。打开的文件多了之后,进程pcb的结构就会变的很大,而pcb占用的内存通常就是几个页框,linux中的pcb也只是2页框的大小,所以这些信息不会被放入pcb中。

看完了上面的理论之后,接下来就来具体看一下文件描述符的代码实现

1
2
3
4
5
struct task_struct
{
// ...
int32_t fd_table[MAX_FILES_OPEN_PER_PROC];
}

这是在进程pcb中记录的文件描述符数组,为了简化实现,这里并不是一个指针数组。里面记录的数据就是对应文件表的下标值。通过该下标去文件表中找到对应的文件结构。

文件描述符的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
void init_thread(task_struct* pthread, char* name, int prio) 
{
// ...
pthread->fd_table[0] = 0;
pthread->fd_table[1] = 1;
pthread->fd_table[2] = 2;

uint8_t fd_idx = 3;

while(fd_idx < MAX_FILES_OPEN_PER_PROC)
{
pthread->fd_table[fd_idx++] = -1;
}

文件描述符的前三个文件结构将作为标准输入,标准输出,标准错误预留出来。置为-1表示该文件描述符可被分配。

文件描述符结构和文件表

1
2
3
4
5
6
7
8
9
struct file
{
uint32_t fd_pos; // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
uint32_t fd_flag;// 文件打开的标志
struct inode *fd_inode;
};

// 文件表
struct file file_table[MAX_FILE_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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/* 创建文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_create(struct dir *parent_dir, char *filename, uint8_t flag)
{
/* 后续操作的公共缓冲区 */
void *io_buf = sys_malloc(1024);
if (io_buf == NULL)
{
return -1;
}

uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态

/* 为新文件分配inode */
int32_t inode_no = inode_bitmap_alloc(cur_part);
if (inode_no == -1)
{
return -1;
}

/* 此inode要从堆中申请内存,不可生成局部变量(函数退出时会释放)
* 因为file_table数组中的文件描述符的inode指针要指向它.*/
struct inode *new_file_inode = (struct inode *)sys_malloc(sizeof(struct inode));
if (new_file_inode == NULL)
{
rollback_step = 1;
goto rollback;
}
inode_init(inode_no, new_file_inode); // 初始化inode

/* 返回的是file_table数组的下标 */
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1)
{
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);
// 同步内存数据到硬盘

/* a 在目录parent_dir下安装目录项new_dir_entry, 写入硬盘后返回true,否则false */
if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf))
{
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;
}

该函数的功能是在目录parent_dir下以创建模式flag去创建普通文件,如果函数执行成功的话返回文件描述符。

创建的过程如下:

  1. 首先为文件创建inode,该过程需要向inode的管理单元inode_bitmap申请inode号,并更新inode_bitmap
  2. 确定文件存储的扇区地址,这个需要在block_bitmap中申请可用的块,并更新block_bitmap
  3. 新增的文件必然位于某个目录中,所以该目录的目录项数量要加1,并且要将新增的目录项写入目录对应的扇区中,如果原有的扇区已满,需要申请新扇区来存储目录项
  4. 将上面过程中被改变的数据写入硬盘中。

系统调用open

open函数的功能相当强大,通过它的打开标志,不仅可以打开一个文件,同样可以创建一个文件。所以这里不打算单独实现文件的创建。

文件创建的标志

1
2
3
4
5
6
7
enum oflags
{
O_RDONLY, // 只读
O_WRONLY, // 只写
O_RDWR, // 读写
O_CREAT = 4 // 创建
};

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/* 打开或创建文件成功后,返回文件描述符,否则返回-1 */
int32_t sys_open(const char *pathname, uint8_t flags)
{
if (pathname[strlen(pathname) - 1] == '/')
{
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)
{
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)
{
// 说明并没有访问到全部的路径,某个中间目录是不存在的
dir_close(searched_record.parent_dir);
return -1;
}

/* 若是在最后一个路径上没找到,并且并不是要创建文件,直接返回-1 */
if (!found && !(flags & O_CREAT))
{
dir_close(searched_record.parent_dir);
return -1;
}
else if (found && flags & O_CREAT)
{
// 若要创建的文件已存在
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);
// 其余为打开文件
default:
fd = file_open(inode_no, flags);
}

/* 此fd是指任务pcb->fd_table数组中的元素下标,
* 并不是指全局file_table中的下标 */
return fd;
}

文件的创建过程中主要是对路径的解析,这里暂时还不支持相对路径,这里必须使用绝对路径来进行文件的创建。在路径没有问题且该文件不存在的前提下,就会调用之前的file_create函数创建文件。

接下来在模拟器上运行一下,看看创建文件的具体表现

主分区信息

上面的图片是主分区格式化时数据的区域,我用红框标记了数据的起始扇区位置,该位置应该与根目录所在的位置相同。

将数据起始扇区的位置*512后得到数据区的地址,在该地址处查看512字节也就是一扇区的数据内容,结果如下

mark

在根目录下,目前有三个目录项 . ..和创建的文件file1,每个目录项包含三部分的内容,16字节的filename,4字节的inode号,4字节的文件类型。共24字节的内容

用红框标记的是 . 这个目录项的数据,2E代表它的文件名,它表示当前目录也就是根目录,其inode号为0,文件类型为2,代表一个目录

黄线标记的是 .. 它代表上一层目录,因为目前在根目录下,所以它还是表示根目录。

绿线标记的就是刚刚创建的文件,文件名为file1。inode号为1,文件类型是1,表示这是一个普通文件。

上面通过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
32
33
34
/* 打开编号为inode_no的inode对应的文件,若成功则返回文件描述符,否则返回-1 */
int32_t file_open(uint32_t inode_no, uint8_t flag)
{
int fd_idx = get_free_slot_in_global();
if (fd_idx == -1)
{
printk("exceed max open files\n");
return -1;
}
file_table[fd_idx].fd_inode = inode_open(cur_part, inode_no);
file_table[fd_idx].fd_pos = 0; // 每次打开文件,要将fd_pos还原为0,即让文件内的指针指向开头
file_table[fd_idx].fd_flag = flag;
bool *write_deny = &file_table[fd_idx].fd_inode->write_deny;

if (flag & O_WRONLY || flag & O_RDWR)
{ // 只要是关于写文件,判断是否有其它进程正写此文件
// 若是读文件,不考虑write_deny
/* 以下进入临界区前先关中断 */
enum intr_status old_status = intr_disable();
if (!(*write_deny))
{
// 若当前没有其它进程写该文件,将其占用.
*write_deny = true; // 置为true,避免多个进程同时写此文件
intr_set_status(old_status); // 恢复中断
}
else
{ // 直接失败返回
intr_set_status(old_status);
printk("file can`t be write now, try again later\n");
return -1;
}
} // 若是读文件或创建文件,不用理会write_deny,保持默认
return pcb_fd_install(fd_idx);
}

可以看到,打开一个文件的本质就是在进程的pcb中安装了该文件对应的文件描述符。

open函数的功能实现完了之后就可以将其添加到系统调用中,以供用户的使用。

1
2
3
4
5
6
7
8
9
10
void syscall_init(void)
{
// ...
syscall_table[SYS_OPEN] = sys_open;
}

int open(const char *pathname, int flags)
{
return _syscall2(SYS_OPEN, pathname, flags);
}

系统调用close

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
int32_t sys_close(int32_t fd)
{
int32_t ret = -1;
if (fd > 2)
{
uint32_t _fd = fd_local2global(fd);
ret = file_close(&file_table[_fd]);
running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
}
return ret;
}

/* 将文件描述符转化为文件表的下标 */
static uint32_t fd_local2global(uint32_t local_fd)
{
task_struct *cur = running_thread();
int32_t global_fd = cur->fd_table[local_fd];
ASSERT(global_fd >= 0 && global_fd < MAX_FILE_OPEN);
return (uint32_t)global_fd;
}

/* 关闭文件 */
int32_t file_close(struct file *file)
{
if (file == NULL)
{
return -1;
}
file->fd_inode->write_deny = false;
inode_close(file->fd_inode);
file->fd_inode = NULL; // 使文件结构可用
return 0;
}

/* 关闭inode或减少inode的打开数 */
void inode_close(struct inode *inode)
{
/* 若没有进程再打开此文件,将此inode去掉并释放空间 */
enum intr_status old_status = intr_disable();
if (--inode->i_open_cnts == 0)
{
list_remove(&inode->inode_tag); // 将I结点从part->open_inodes中去掉

/* inode_open时为实现inode被所有进程共享,
* 已经在sys_malloc为inode分配了内核空间,
* 释放inode时也要确保释放的是内核内存池 */
task_struct *cur = running_thread();
uint32_t *cur_pagedir_bak = cur->pgdir;
cur->pgdir = NULL;
sys_free(inode);
cur->pgdir = cur_pagedir_bak;
}
intr_set_status(old_status);
}

添加系统调用

1
2
3
4
5
6
7
8
9
10
11
/* 初始化系统调用 */
void syscall_init()
{
//...
syscall_table[SYS_CLOSE] = sys_close;
}

int close(int fd)
{
return _syscall1(SYS_CLOSE, fd);
}