写入文件
文件的数据都是记录在inode中的文件块中,在该文件系统的设计中,只用了12个直接块和一个间接块来存储文件,所以一个文件最大可以存放 140 * 512字节的数据。
写文件的过程对文件块和扇区的分配过程,根据当前要写入的数据量大小,来判断是否需要分配新的数据块。如果12个直接块不够存储该数据,就分配间接块来存储,当所需的数据块分配好了之后,就会逐块的往硬盘上写入数据,知道所有的数据被写入硬盘,最后返回写入的字节数。
/* 把buf中的count个字节写入file,成功则返回写入的字节数,失败则返回-1 */
int32_t file_write(struct file *file, const void *buf, uint32_t count)
{
if ((file->fd_inode->i_size + count) > (BLOCK_SIZE * 140))
{
// 文件目前最大只支持512*140=71680字节
return -1;
}
uint8_t *io_buf = sys_malloc(BLOCK_SIZE);
if (io_buf == NULL)
{
return -1;
}
uint32_t *all_blocks = (uint32_t *)sys_malloc(BLOCK_SIZE + 48); // 用来记录文件所有的块地址
if (all_blocks == NULL)
{
printk("file_write: sys_malloc for all_blocks failed\n");
return -1;
}
const uint8_t *src = buf; // 用src指向buf中待写入的数据
uint32_t bytes_written = 0; // 用来记录已写入数据大小
uint32_t size_left = count; // 用来记录未写入数据大小
int32_t block_lba = -1; // 块地址
uint32_t block_bitmap_idx = 0; // 用来记录block对应于block_bitmap中的索引,做为参数传给bitmap_sync
uint32_t sec_idx; // 用来索引扇区
uint32_t sec_lba; // 扇区地址
uint32_t sec_off_bytes; // 扇区内字节偏移量
uint32_t sec_left_bytes; // 扇区内剩余字节量
uint32_t chunk_size; // 每次写入硬盘的数据块大小
int32_t indirect_block_table; // 用来获取一级间接表地址
uint32_t block_idx; // 块索引
/* 判断文件是否是第一次写,如果是,先为其分配一个块 */
if (file->fd_inode->i_sectors[0] == 0)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
return -1;
}
file->fd_inode->i_sectors[0] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
ASSERT(block_bitmap_idx != 0);
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
/* 写入count个字节前,该文件已经占用的块数 */
uint32_t file_has_used_blocks = file->fd_inode->i_size / BLOCK_SIZE + 1;
/* 存储count字节后该文件将占用的块数 */
uint32_t file_will_use_blocks = (file->fd_inode->i_size + count) / BLOCK_SIZE + 1;
ASSERT(file_will_use_blocks <= 140);
/* 通过此增量判断是否需要分配扇区,如增量为0,表示原扇区够用 */
uint32_t add_blocks = file_will_use_blocks - file_has_used_blocks;
/* 开始将文件所有块地址收集到all_blocks,(系统中块大小等于扇区大小)
* 后面都统一在all_blocks中获取写入扇区地址 */
if (add_blocks == 0)
{
/* 在同一扇区内写入数据,不涉及到分配新扇区 */
if (file_has_used_blocks <= 12)
{ // 文件数据量将在12块之内
block_idx = file_has_used_blocks - 1; // 指向最后一个已有数据的扇区
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
}
else
{
/* 未写入新数据之前已经占用了间接块,需要将间接块地址读进来 */
ASSERT(file->fd_inode->i_sectors[12] != 0);
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
}
}
else
{
/* 若有增量,便涉及到分配新扇区及是否分配一级间接块表,下面要分三种情况处理 */
/* 第一种情况:12个直接块够用*/
if (file_will_use_blocks <= 12)
{
/* 先将有剩余空间的可继续用的扇区地址写入all_blocks */
block_idx = file_has_used_blocks - 1;
ASSERT(file->fd_inode->i_sectors[block_idx] != 0);
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
/* 再将未来要用的扇区分配好后写入all_blocks */
block_idx = file_has_used_blocks; // 指向第一个要分配的新扇区
while (block_idx < file_will_use_blocks)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
return -1;
}
/* 写文件时,不应该存在块未使用但已经分配扇区的情况,当文件删除时,就会把块地址清0 */
ASSERT(file->fd_inode->i_sectors[block_idx] == 0); // 确保尚未分配扇区地址
file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
block_idx++; // 下一个分配的新扇区
}
}
else if (file_has_used_blocks <= 12 && file_will_use_blocks > 12)
{
/* 第二种情况: 旧数据在12个直接块内,新数据将使用间接块*/
/* 先将有剩余空间的可继续用的扇区地址收集到all_blocks */
block_idx = file_has_used_blocks - 1; // 指向旧数据所在的最后一个扇区
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
/* 创建一级间接块表 */
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
return -1;
}
ASSERT(file->fd_inode->i_sectors[12] == 0); // 确保一级间接块表未分配
/* 分配一级间接块索引表 */
indirect_block_table = file->fd_inode->i_sectors[12] = block_lba;
block_idx = file_has_used_blocks; // 第一个未使用的块,即本文件最后一个已经使用的直接块的下一块
while (block_idx < file_will_use_blocks)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc for situation 2 failed\n");
return -1;
}
if (block_idx < 12)
{
// 新创建的0~11块直接存入all_blocks数组
ASSERT(file->fd_inode->i_sectors[block_idx] == 0); // 确保尚未分配扇区地址
file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
}
else
{
// 间接块只写入到all_block数组中,待全部分配完成后一次性同步到硬盘
all_blocks[block_idx] = block_lba;
}
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
block_idx++; // 下一个新扇区
}
ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 同步一级间接块表到硬盘
}
else if (file_has_used_blocks > 12)
{
/* 第三种情况:新数据占据间接块*/
ASSERT(file->fd_inode->i_sectors[12] != 0); // 已经具备了一级间接块表
indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址
/* 已使用的间接块也将被读入all_blocks,无须单独收录 */
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 获取所有间接块地址
block_idx = file_has_used_blocks; // 第一个未使用的间接块,即已经使用的间接块的下一块
while (block_idx < file_will_use_blocks)
{
block_lba = block_bitmap_alloc(cur_part);
if (block_lba == -1)
{
printk("file_write: block_bitmap_alloc for situation 3 failed\n");
return -1;
}
all_blocks[block_idx++] = block_lba;
/* 每分配一个块就将位图同步到硬盘 */
block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
}
ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 同步一级间接块表到硬盘
}
}
bool first_write_block = true; // 含有剩余空间的扇区标识
/* 块地址已经收集到all_blocks中,下面开始写数据 */
file->fd_pos = file->fd_inode->i_size - 1; // 置fd_pos为文件大小-1,下面在写数据时随时更新
while (bytes_written < count)
{
// 直到写完所有数据
memset(io_buf, 0, BLOCK_SIZE);
sec_idx = file->fd_inode->i_size / BLOCK_SIZE;
sec_lba = all_blocks[sec_idx];
sec_off_bytes = file->fd_inode->i_size % BLOCK_SIZE;
sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
/* 判断此次写入硬盘的数据大小 */
chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes;
if (first_write_block)
{
ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
first_write_block = false;
}
memcpy(io_buf + sec_off_bytes, src, chunk_size);
ide_write(cur_part->my_disk, sec_lba, io_buf, 1);
src += chunk_size; // 将指针推移到下个新数据
file->fd_inode->i_size += chunk_size; // 更新文件大小
file->fd_pos += chunk_size;
bytes_written += chunk_size;
size_left -= chunk_size;
}
inode_sync(cur_part, file->fd_inode, io_buf);
sys_free(all_blocks);
sys_free(io_buf);
return bytes_written;
}
最后将write添加到系统调用中, 根据其文件描述符来判断数据是写入磁盘还是标准输出中。
int32_t sys_write(int32_t fd, const void *buf, uint32_t count)
{
if (fd < 0)
{
return -1;
}
if (fd == stdout_no)
{
char tmp_buf[1024] = {0};
memcpy(tmp_buf, buf, count);
console_put_str(tmp_buf);
return count;
}
uint32_t _fd = fd_local2global(fd);
struct file *wr_file = &file_table[_fd];
if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR)
{
uint32_t bytes_written = file_write(wr_file, buf, count);
return bytes_written;
}
else
{
console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
return -1;
}
}
上图是向文件file1中写入数据hello,world之后,磁盘上数据的表现。
写入的位置是第0xa6c个扇区,将其*512转换成地址之后查看该地址的数,可以看到数据确实写入到磁盘上了。
读取文件
/* 从文件file中读取count个字节写入buf, 返回读出的字节数,若到文件尾则返回-1 */
int32_t file_read(struct file *file, void *buf, uint32_t count)
{
uint8_t *buf_dst = (uint8_t *)buf;
uint32_t size = count, size_left = size;
/* 若要读取的字节数超过了文件可读的剩余量, 就用剩余量做为待读取的字节数 */
if ((file->fd_pos + count) > file->fd_inode->i_size)
{
size = file->fd_inode->i_size - file->fd_pos;
size_left = size;
if (size == 0)
{
// 若到文件尾则返回-1
return -1;
}
}
uint8_t *io_buf = sys_malloc(BLOCK_SIZE);
if (io_buf == NULL)
{
return -1;
}
uint32_t *all_blocks = (uint32_t *)sys_malloc(BLOCK_SIZE + 48); // 用来记录文件所有的块地址
if (all_blocks == NULL)
{
return -1;
}
uint32_t block_read_start_idx = file->fd_pos / BLOCK_SIZE; // 数据所在块的起始地址
uint32_t block_read_end_idx = (file->fd_pos + size) / BLOCK_SIZE; // 数据所在块的终止地址
uint32_t read_blocks = block_read_start_idx - block_read_end_idx; // 如增量为0,表示数据在同一扇区
ASSERT(block_read_start_idx < 139 && block_read_end_idx < 139);
int32_t indirect_block_table; // 用来获取一级间接表地址
uint32_t block_idx; // 获取待读的块地址
/* 以下开始构建all_blocks块地址数组,专门存储用到的块地址(本程序中块大小同扇区大小) */
if (read_blocks == 0)
{
// 在同一扇区内读数据,不涉及到跨扇区读取
ASSERT(block_read_end_idx == block_read_start_idx);
if (block_read_end_idx < 12)
{
// 待读的数据在12个直接块之内
block_idx = block_read_end_idx;
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
}
else
{
// 若用到了一级间接块表,需要将表中间接块读进来
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
}
}
else
{
// 若要读多个块
/* 第一种情况: 起始块和终止块属于直接块*/
if (block_read_end_idx < 12)
{
// 数据结束所在的块属于直接块
block_idx = block_read_start_idx;
while (block_idx <= block_read_end_idx)
{
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
block_idx++;
}
}
else if (block_read_start_idx < 12 && block_read_end_idx >= 12)
{
/* 第二种情况: 待读入的数据跨越直接块和间接块两类*/
/* 先将直接块地址写入all_blocks */
block_idx = block_read_start_idx;
while (block_idx < 12)
{
all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
block_idx++;
}
ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表
/* 再将间接块地址写入all_blocks */
indirect_block_table = file->fd_inode->i_sectors[12];
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
}
else
{
/* 第三种情况: 数据在间接块中*/
ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表
indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址
ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
}
}
/* 用到的块地址已经收集到all_blocks中,下面开始读数据 */
uint32_t sec_idx, sec_lba, sec_off_bytes, sec_left_bytes, chunk_size;
uint32_t bytes_read = 0;
while (bytes_read < size)
{ // 直到读完为止
sec_idx = file->fd_pos / BLOCK_SIZE;
sec_lba = all_blocks[sec_idx];
sec_off_bytes = file->fd_pos % BLOCK_SIZE;
sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes; // 待读入的数据大小
memset(io_buf, 0, BLOCK_SIZE);
ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
memcpy(buf_dst, io_buf + sec_off_bytes, chunk_size);
buf_dst += chunk_size;
file->fd_pos += chunk_size;
bytes_read += chunk_size;
size_left -= chunk_size;
}
sys_free(all_blocks);
sys_free(io_buf);
return bytes_read;
}
设置文件的读写偏移量
想象一下这种情况,在文件读取到文件尾的时候,再想去读取文件前面的部分。在目前的实现下,只能将该文件关闭之后,再重新打开,才能读取到之前的数据,这样做的话显然是不合理的。所以需要实现文件读写定位的功能。也就是lseek的实现。
文件的读写偏移量的设置有三个基准数,文件头,文件当前位置,文件尾。
// 文件读写位置偏移量
enum whence
{
SEEK_SET = 1,
SEEK_CUR,
SEEK_END
};
/* 重置用于文件读写操作的偏移指针,成功时返回新的偏移量,出错时返回-1 */
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence)
{
if (fd < 0)
{
return -1;
}
ASSERT(whence > 0 && whence < 4);
uint32_t _fd = fd_local2global(fd);
struct file *pf = &file_table[_fd];
int32_t new_pos = 0; //新的偏移量必须位于文件大小之内
int32_t file_size = (int32_t)pf->fd_inode->i_size;
switch (whence)
{
/* SEEK_SET 新的读写位置是相对于文件开头再增加offset个位移量 */
case SEEK_SET:
new_pos = offset;
break;
/* SEEK_CUR 新的读写位置是相对于当前的位置增加offset个位移量 */
case SEEK_CUR: // offse可正可负
new_pos = (int32_t)pf->fd_pos + offset;
break;
/* SEEK_END 新的读写位置是相对于文件尺寸再增加offset个位移量 */
case SEEK_END: // 此情况下,offset应该为负值
new_pos = file_size + offset;
}
if (new_pos < 0 || new_pos > (file_size - 1))
{
return -1;
}
pf->fd_pos = new_pos;
return pf->fd_pos;
}
删除文件
删除文件主要是对资源的回收。主要有以下几部分的数据
- inode
- inode bitmap
- inode table
- inode中的12个直接块和一个间接块
- 存储间接块的扇区
- 目录项
- 该文件对应的目录项数据需要清0
- 该文件删除之后,目录中不存在目录项,需要回收目录项对应的块
- 目录inode中的size需要减去该文件目录项大小
- 将目录inode同步到硬盘
上面两部分资源的回收主要通过inode_release和delete_dir_entry这两个函数实现,这里只贴出删除文件的整体调用过程。
/* 删除文件(非目录),成功返回0,失败返回-1 */
int32_t sys_unlink(const char *pathname)
{
ASSERT(strlen(pathname) < MAX_PATH_LEN);
/* 先检查待删除的文件是否存在 */
struct path_search_record searched_record;
memset(&searched_record, 0, sizeof(struct path_search_record));
int inode_no = search_file(pathname, &searched_record);
ASSERT(inode_no != 0);
if (inode_no == -1)
{
dir_close(searched_record.parent_dir);
return -1;
}
if (searched_record.file_type == FT_DIRECTORY)
{
dir_close(searched_record.parent_dir);
return -1;
}
/* 检查是否在已打开文件列表(文件表)中 */
uint32_t file_idx = 0;
while (file_idx < MAX_FILE_OPEN)
{
if (file_table[file_idx].fd_inode != NULL && (uint32_t)inode_no == file_table[file_idx].fd_inode->i_no)
{
break;
}
file_idx++;
}
if (file_idx < MAX_FILE_OPEN)
{
dir_close(searched_record.parent_dir);
return -1;
}
ASSERT(file_idx == MAX_FILE_OPEN);
/* 为delete_dir_entry申请缓冲区 */
void *io_buf = sys_malloc(SECTOR_SIZE + SECTOR_SIZE);
if (io_buf == NULL)
{
dir_close(searched_record.parent_dir);
return -1;
}
struct dir *parent_dir = searched_record.parent_dir;
delete_dir_entry(cur_part, parent_dir, inode_no, io_buf);
inode_release(cur_part, inode_no);
sys_free(io_buf);
dir_close(searched_record.parent_dir);
return 0; // 成功删除文件
}