文件描述符
在之前介绍的概念中,inode是用来表示一个文件的,用于描述文件的存储信息,文件的权限等。但这个inode结构是操作系统为自己的文件系统准备的数据结构,仅供其内部使用,与用户的关系不大,接下来要介绍的文件描述符才是用户能够使用的结构。
在linux系统中,读写文件的本质是先通过文件的inode找到文件数据块的扇区地址,随后对这些扇区的数据进行读写,从而实现了文件的读写。
对用户进程来说,一个进程可以多次的打开同一个文件,一个文件也可以被多个进行同时打开。对于这个被多次打开的文件,每打开一次,都需要一个结构来记录该文件目前的状态。比如说A进行第一次打开1.txt的时候,读取到了第10行的位置,第二次打开1.txt的时候,读取到了20行的位置。这里的10行,20行就是打开的文件状态中的文件偏移量,它记录的是相对于文件首地址的一个偏移。即使一个文件被同时多次打开,各自操作的偏移量也互不影响。
也就是说,会有一个文件结构来描述文件打开后,文件读写的偏移量等信息。一个文件对应一个inode,一个文件可以被多次打开,即一个inode对应多个文件结构。
文件描述符的基本结构如下
接下来通过linux的open函数来了解一下文件描述符
open的函数原型如下
int open(const char *pathname, inf flags);
成功调用该函数之后,它会返回文件pathname的文件描述符,该返回值是一个int的数值,肯定不是代表真正的文件描述符结构。它是作为进程pcb中文件描述符数组的下标索引。通过这个下标在进程pcb中找到某个数据。大家可能会认为这个数据就是真正的文件描述符了。其实不然,在进程pcb的文件描述符数组中,记录的任然不是真正的文件描述符结构,它是一个指针数组,数组中的数据指向文件表中某个文件结构,该文件结构就是真正记录被打开文件的信息所在。
该过程比较复杂,将其分解为下面四步
1. 调用open函数,得到一个文件描述符
2. 将第一步得到的文件描述符作为进程pcb中文件描述符数组的下标,取得该下标对应的数据
3. 将第二步得到的数据,相当于是一个指针,从这个指针中取到对应的文件结构
4. 从该文件结构中获取到记录的文件信息
通过一副图来描述一下这个过程
看到这里获取会有一个疑问,为什么pcb中不直接记录文件的描述符信息,而是指向了其他的区域。
因为记录文件信息的描述符较大,每打开一次文件,就需要记录一次。打开的文件多了之后,进程pcb的结构就会变的很大,而pcb占用的内存通常就是几个页框,linux中的pcb也只是2页框的大小,所以这些信息不会被放入pcb中。
看完了上面的理论之后,接下来就来具体看一下文件描述符的代码实现
struct task_struct
{
// ...
int32_t fd_table[MAX_FILES_OPEN_PER_PROC];
}
这是在进程pcb中记录的文件描述符数组,为了简化实现,这里并不是一个指针数组。里面记录的数据就是对应文件表的下标值。通过该下标去文件表中找到对应的文件结构。
文件描述符的初始化
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表示该文件描述符可被分配。
文件描述符结构和文件表
struct file
{
uint32_t fd_pos; // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
uint32_t fd_flag;// 文件打开的标志
struct inode *fd_inode;
};
// 文件表
struct file file_table[MAX_FILE_OPEN];
文件描述符只存储了打开一个需要记录的最基础的信息。文件表的本质就是一个文件描述符结构的数组。
创建文件
/* 创建文件,若成功则返回文件描述符,否则返回-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,并且要将新增的目录项写入目录对应的扇区中,如果原有的扇区已满,需要申请新扇区来存储目录项
5. 将上面过程中被改变的数据写入硬盘中。
系统调用open
open函数的功能相当强大,通过它的打开标志,不仅可以打开一个文件,同样可以创建一个文件。所以这里不打算单独实现文件的创建。
文件创建的标志
enum oflags
{
O_RDONLY, // 只读
O_WRONLY, // 只写
O_RDWR, // 读写
O_CREAT = 4 // 创建
};
/* 打开或创建文件成功后,返回文件描述符,否则返回-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字节也就是一扇区的数据内容,结果如下
在根目录下,目前有三个目录项 . ..和创建的文件file1,每个目录项包含三部分的内容,16字节的filename,4字节的inode号,4字节的文件类型。共24字节的内容
用红框标记的是 . 这个目录项的数据,2E代表它的文件名,它表示当前目录也就是根目录,其inode号为0,文件类型为2,代表一个目录
黄线标记的是 .. 它代表上一层目录,因为目前在根目录下,所以它还是表示根目录。
绿线标记的就是刚刚创建的文件,文件名为file1。inode号为1,文件类型是1,表示这是一个普通文件。
上面通过open函数创建了一个文件,接下来就完成其打开文件的功能。打开文件的功能主要是通过下面这个函数实现的。
/* 打开编号为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函数的功能实现完了之后就可以将其添加到系统调用中,以供用户的使用。
void syscall_init(void)
{
// ...
syscall_table[SYS_OPEN] = sys_open;
}
int open(const char *pathname, int flags)
{
return _syscall2(SYS_OPEN, pathname, flags);
}
系统调用close
/* 关闭文件描述符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);
}
添加系统调用
/* 初始化系统调用 */
void syscall_init()
{
//...
syscall_table[SYS_CLOSE] = sys_close;
}
int close(int fd)
{
return _syscall1(SYS_CLOSE, fd);
}