手写操作系统(五十四)-实现文件写入和读取

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.文件写入

接下来就是实现文件写入了

逻辑就是/fs/file.c的file_write函数

修改/fs/file.c,添加file_write函数

/* 把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字节
        printk("exceed max file_size 71680 bytes, write file failed\n");
        return -1;
    }
    uint8_t *io_buf = sys_malloc(BLOCK_SIZE);
    if (io_buf == NULL) {
        printk("file_write: sys_malloc for io_buf failed\n");
        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) {
            printk("file_write: block_bitmap_alloc failed\n");
            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) {
                    printk("file_write: block_bitmap_alloc for situation 1 failed\n");
                    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) {
                printk("file_write: block_bitmap_alloc for situation 2 failed\n");
                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);
        printk("file write at lba 0x%x\n", sec_lba);    //调试,完成后去掉

        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;
}

file_write函数接受3个参数,文件file、数据缓冲区buf、 字节数 count,功能是把 buf 中的 count 个字节写入 file,成功则返回写入的字节数,失败则返回-1。

我们的块大小就是扇区大小。我们的文件最大尺寸是140个块,为此在函数开头便判断新加的数据是否会使文件超过最大尺寸,如果超过了140个块的大小,即BLOCK_SIZE * 140,程序打印提示后,返回-1,其中宏BLOCK_SIZE等于SECTOR_SIZE,即值为512。

因为后面我们的磁盘操作都以 1 个扇区为单位,所以申请了 512 字节的缓冲区给 io_buf。

为写硬盘时方便获取块地址,我们打算把文件所有的块地址收集到all_blocks中统一获取,为此申请了BLOCK_SIZE+48大小的内容,即 128 个间接块+12个直接块的大小。

文件中的数据是个整体,因此是顺序连续写入块中的,并且是从最低块i_sector[0]向高块开始写,比如直接块0 写满后再写入直接块1,写完直接块后再写第0个间接块,直到第127个间接块,数据从前往后写,后面的块都是空白的,在文件中也不会出现空洞、跨块的情况,这和目录是不同的,目录中的目录项是单独的个体,它们可以分散在不同的块中。

文件第一次写入数据时要为其分配块地址,若未分配块地址的话,块地址则为0,这是之前调用inode_init 为其初始化时提前安排的。if (file->fd_inode->i_sectors[0] == 0)便判断文件是否是第一次写数据,如果是就通过block_bitmap_alloc分配扇区,地址写入文件的 i_sectors[0],然后将位图同步到硬盘。

下面判断文件是否要为这count个字节分配新块,也就是现有的扇区是否够用。变量file_has used_blocks是文件目前使用的块数,也就是未写入count个字节前文件占用的块数,变量file_willuse_blocks 是存储 count字节后该文件将占用的块数。变量add_blocks 是需要为count个字节数据分配的扇区数。

接下来把文件现在使用及未来使用的块地址收集到all_blocks中(不包括那些不参与写入操作的块地址),与数据写入无关。收集工作完成之后,all_block中包括原块地址及新数据占用的块地址,在这之后我们才进行数据写入工作。

if (add_blocks == 0)判断如果add_blocks为0,这说明count值小于等于原有扇区中的剩余空间,剩余空间便可容纳count个字节数据,无需再申请新的块。接着if (file_has_used_blocks <= 12)判断原有块地址是否为直接块,else代码下判断原有块地址是否是间接块,无论是直接块,还是间接块,文件的现有块地址都将收录到all_blocks中。

if (add_blocks == 0)的else语句就是处理需要分配新数据块的情况。

下面分三种情况讨论:

  • 若已经使用的扇区数在12块之内,新增了若干块后,文件大小还在12块之内,直接分配所需的块并把块地址写入 i_sectors 数组中即可。
  • 若已经使用的块数在12块之内,新增了若干块后,文件大小超过了12块,这种情况下所申请的块除了要写入i_sector 数组,还要创建一级间接块表并写入块地址。
  • 若已经使用的扇区数超过了12块,这种情况下要在一级间接块表中创建间接块项,间接块项就是在一级间接块表中记录间接块地址的条目。

以上三种情况所申请的块地址都要收录到all_blocks中。

if (file_will_use_blocks <= 12)的判断是处理第一种情况,新分配的块也属于直接块的情况。这分为两步,第1步是先将原有扇区中包含剩余空间的(可继续用的)块地址收录到all_blocks。第2步再将未来要用的扇区分配好并写入all_blocks.

else if (file_has_used_blocks <= 12 && file_will_use_blocks > 12)是处理第二种情况,旧数据在12个直接块内,新数据将使用间接块。这分为三步,先将原有扇区中包含剩余空间的块地址收录到all_blocks。然后在分配一个块作为一级间接块索引表。最后将未来要用的块分配好并收录到all_blocks中。接着在ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1)将所有的间接块地址同步到硬盘上的一级间接块索引表中。

else if (file_has_used_blocks > 12)判断处理第三种情况,新老数据都在间接块中。与前两种情况不同的是已使用的、包含剩余空间的间接块都在一级间接块索引表中,因此只要将此表读到all_blocks中便获取所有间接块地址,无需单独收录。while (block_idx < file_will_use_blocks)开始分配间接块并收录到all_blocks中。最后在ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1)将所有间接块地址,包括新分配的间接块地址一同写入硬盘上的一级间接块索引表中。

到目前为止,all_blocks中包含可继续使用的、含有剩余空间的块地址,以及新数据要占用的新的块地址。下面是开始接着在含有剩余空间的块写入新数据。

bool first_write_block = true变量 first_write_block 用于标识本次写入操作中第一个所写入的块,除第一次写入数据外,通常情况下该块中都有些数据,最新的数据要从接着该块的空闲空间接着写。因此在第一次写数据时,要将该块中的数据读出来,将新数据写入该块中空闲区域,之后再新老数据一同写入硬盘,这样就保护好了老数据,并且实现了数据追加的功能。

while (bytes_written < count)开始循环写入数据,变量bytes_written记录已写入的数据量大小,它已被初始为0,循环直到bytes_written等于count时结束,也就是写完为止。循环体中最先执行的是用memset清空io_buf,io_buf是要往文件数据块写入数据的缓冲区,必须要保证干净。

sec_idx = file->fd_inode->i_size / BLOCK_SIZE的sec_idx是根据文件大小计算出的块索引,也就是扇区索引值,在下一行将它代入all_blocks 获得扇区地址sec_Iba。 sec_off_bytes是数据在最后一个块中的偏移量, sec_left_bytes是块中的可用字节空间。

chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes的chunk_size是本次写入扇区的数据量,size_left表示剩下未写入硬盘的数据量,当size_left小于本次所写入块的剩余空间时,chunk_size就等于size_left,这属于剩余的数据不足一个块,往最后一个块中写剩余数据时的情况,如果size_left大于等于sec_left_bytes,那就把剩下的空闲空间写满,即chunk_size等于 sec_left_bytes。

if (first_write_block)判断,若马上要读写的块是本次操作中的第一个块,通常情况下该块中都已存在数据,因此在下一行先将该块中的数据读出来,然后使first_write_block置为false。

memcpy(io_buf + sec_off_bytes, src, chunk_size)将数据拷贝到io_buf中,拼好数据,在下一行将其写入硬盘。硬盘写入后,后面更新相应的数据,如使i_size加上chunk_size,bytes_written加上bytes_written,剩 余数据量减去bytes_written,将count个字节写完之后,在inode_sync(cur_part, file->fd_inode, io_buf)同步inode。最后释放all_blocks和io_buf,返回已写入的数据量bytes_written。

 

我们之前实现了write方法,但是只是模拟的实现,这里将他修改为真正调用sys_write

修改/fs/fs.c

添加sys_write函数

/* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1 */
int32_t sys_write(int32_t fd, const void* buf, uint32_t count) {
    if (fd < 0) {
        printk("sys_write: fd error\n");
        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;
    }
}

sys_write函数接受 3 个参数,文件描述符 fd、数据所在缓冲区 buf、写入的字节数 count。

函数中对标准输出做了特殊处理,若发现fd等于stdout_no,也就是往屏幕上打印信息时,就把buf中的count字节复制到临时缓冲区tmp_buf中,然后调用console_put_str(tmp_buf)输出,最后通过 return 返回 count。

在其他情况下,sys_write都是往文件中写数据,下面uint32_t _fd = fd_local2global(fd)通过fd_local2global获取文件描述符fd对应于文件表中的下标_fd,然后获得待写入文件的文件结构指针wr_file,在if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR)判断其flag,只有flag包含O_WRONLY或O_RDWR的文件才允许写入数据。 _CREAT只能创建文件,不能写文件,这里是参照Linux的做法实现的。 如果符合条件,则通过file_write完成数据写入,并返回写入的字节数,否则不允许写入数据,输出提示信息后返回-1。

对应fs.h加入

int32_t sys_write(int32_t fd, const void* buf, uint32_t count);

 

然后之前目模拟的函数定义在/userprog/syscall-init.c和声明在/userprog/syscall-init.h,记得删除,也就是删除sys_write函数的代码

#include "syscall-init.h"
#include "syscall.h"
#include "stdint.h"
#include "print.h"
#include "thread.h"
#include "console.h"
#include "string.h"
#include "memory.h"
#include "fs.h"

#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr];

/* 返回当前任务的pid */
uint32_t sys_getpid(void) {
    return running_thread()->pid;
}

/* 初始化系统调用 */
void syscall_init(void) {
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    put_str("syscall_init done\n");
}

 

修改/lib/user/syscall.c的调用

/* 把buf中count个字符写入文件描述符fd */
uint32_t write(int32_t fd, const void* buf, uint32_t count) {
    return _syscall3(SYS_WRITE, fd, buf, count);
}

对应/lib/user/syscall.h

uint32_t write(int32_t fd, const void* buf, uint32_t count);

 

接下来修改printf对write的调用

修改/lib/stdio.c

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
    va_list args;
    va_start(args, format);        // 使args指向format
    char buf[1024] = {0};          // 用于存储拼接后的字符串
    vsprintf(buf, format, args);
    va_end(args);
    return write(1, buf, strlen(buf));
}

 

改写main.c文件看看效果

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");

    uint32_t fd = sys_open("/file1", O_RDWR);
    printf("fd:%d\n", fd);
    sys_write(fd, "hello,world\n", 12);
    sys_close(fd);
    printf("%d closed now\n", fd);
    while(1);
    return 0;
}

 

执行结果如下:

 

2.读取文件

同样在/fs/file.c添加file_read函数

/* 从文件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) {
        printk("file_read: sys_malloc for io_buf failed\n");
    }
    uint32_t *all_blocks = (uint32_t *) sys_malloc(BLOCK_SIZE + 48);      // 用来记录文件所有的块地址
    if (all_blocks == NULL) {
        printk("file_read: sys_malloc for all_blocks failed\n");
        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;
}

file_read函数接受3个参数,读取的文件file、数据写入的缓冲区buf、读取的字节数count,功能是从文件file 中读取 count个字节写入 buf,返回读出的字节数,若到文件尾,则返回-1。

函数开头将buf_dst用buf赋值,后面我们将读到的数据存入此地址,不改变buf,代码if ((file->fd_pos + count) > file->fd_inode->i_size)判断文件是否已读到文件尾,如果是,就返回-1。

uint8_t *io_buf = sys_malloc(BLOCK_SIZE)的io_buf还是咱们硬盘操作的缓冲区,后面会把从硬盘中读出的数据存入到io_buf。uint32_t *all_blocks = (uint32_t *) sys_malloc(BLOCK_SIZE + 48)的all_blocks依然用来记录文件所有的块地址,我们后面的读硬盘操作将在此结构中获取地址。

变量block_read_start_idx表示当前指针fd_pod所指向的块索引,也就是数据读取的起始块索引, block_read_end_idx表示相对于当前位置fd_pos偏移count个字节所在的块索引,即数据读取的结束块索引。read_blocks表示要读取的块数。indirect_block_table是一级间接块索引表的地址,后面将为它赋值,block_idx 用于块索引。

下面开始把读操作中用到的块地址收录到all_blocks中。

当read_blocks为0时,这说明要读取的count个字节在一个块中,因此只需要读取1个块,下面分两种情况处理。if (block_read_end_idx < 12)判断若结束块属于直接块,就在i_sectors中获取块地址。否则就要获得间接块地址,因此indirect_block_table = file->fd_inode->i_sectors[12]获取一级间接块索引表地址,ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1)读取该表,获取所有的间接块地址。

若read_blocks不为 0,这说明 count个字节跨块,需要读取多个块。

下面分三种情况处理:

  • 若起始块和终止块都在12块之内,直接读入i_sectors数组中即可。
  • 若起始块在12块之内,结束块超过了12块,除了要读入i_sector数组,还要从一级间接块索引表中读取间接块地址。
  • 若起始块超过了 12 块,这种情况下要在一级间接块索引表中读取间接块。

if (block_read_end_idx < 12)处理第1种情况,循环将所需要的间接块地址录入all_blocks。

else if (block_read_start_idx < 12 && block_read_end_idx >= 12)处理第2种情况,先while (block_idx < 12) 通过while 循环收集直接块地址,然后在ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1)从硬盘上获取所有的间接块地址,录入到all_blocks中。

else语句就是是直接从硬盘上获取间接块地址到 all_blocks。

读硬盘操作中所涉及的块地址已经录入到all_blocks中。

while (bytes_read < size)是读取硬盘的过程,原理同写入数据类似,都是要选出合适的操作数大小,即chunk_size,每次都由ide_read(cur_part->my_disk, sec_lba, io_buf, 1)的ide_read函数读取1个扇区,然后在memcpy(buf_dst, io_buf + sec_off_bytes, chunk_size)往dst_buf中拷贝chunk_size个字节。 memset 清0操作不是必须的,理论上读入的数据会把io_buf 中旧数据覆盖。

最后分别释放 all_blocks 和 io_buf,通过 return 返回已读的字节数bytes_read.

对应file.h加入

int32_t file_read(struct file* file, void* buf, uint32_t count);

 

同样接下来封装到fs/fs.c

添加sys_read函数

/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
    if (fd < 0) {
        printk("sys_read: fd error\n");
        return -1;
    }
    ASSERT(buf != NULL);
    uint32_t _fd = fd_local2global(fd);
    return file_read(&file_table[_fd], buf, count);
}

对应fs.h添加函数声明

int32_t sys_read(int32_t fd, void* buf, uint32_t count);

 

修改main.c文件查看效果

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");

    uint32_t fd = sys_open("/file1", O_RDWR);
    printf("open /file1, fd:%d\n", fd);
    char buf[64] = {0};
    int read_bytes = sys_read(fd, buf, 18);
    printf("1_ read %d bytes:\n%s\n", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("2_ read %d bytes:\n%s", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("3_ read %d bytes:\n%s", read_bytes, buf);

    printf("________  close file1 and reopen  ________\n");
    sys_close(fd);
    fd = sys_open("/file1", O_RDWR);
    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 24);
    printf("4_ read %d bytes:\n%s", read_bytes, buf);

    sys_close(fd);
    while(1);
    return 0;
}

之前我们执行了两次写入文件,这里读取24字节

 

 

3.文件指针定位

这里我们要设置文件读写时的起始偏移量。也就是要实现sys_lseek。

linux原型是”off_t lseek(int fd, off_t offset, int whence)”, fd是文件描述符, offset是偏移量, whence 是offset的参照物,函数功能是设置文件读写指针fd_pos为参照物+偏移量的值,也就是说,文件指针具体的位置不仅取决于offset,还取决于whence。其中off_t是typedef自定义的类型,相当于signed int,有符号整型,因此offset是可正可负的值。

whence 取值有三种取值:

  • SEEK_SET,offset的参照物是文件开始处,也就是将读写位置指针设置为距文件开头偏移offset个字节处。
  • SEEK_CUR,offset的参照物是当前读写位置,也就是将读写位置指针设置为当前位置+offset
  • SEEK_END,offset 的参照物是文件尺寸大小,即文件最后一个字节的下一个字节,也就是将读写位置指针设置为文件尺寸+offset。文件的读写位置指针是fd_pos,fd_pos始终指向下一个可读写的位置,它是以0为起始的偏移量,因此文件末尾是指文件大小。

修改fs/fs.h

添加whence结构

/* 文件读写位置偏移量 */
enum whence {
    SEEK_SET = 1,
    SEEK_CUR,
    SEEK_END
};

 

接下来就是修改fs/fs.c

添加sys_lseek函数

/* 重置用于文件读写操作的偏移指针,成功时返回新的偏移量,出错时返回-1 */
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence) {
    if (fd < 0) {
        printk("sys_lseek: fd error\n");
        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;
}

sys_lseek接受3个参数,文件描述符fd、偏移量offset、参数位置whence,功能是重置用于文件读写操作的偏移指针,成功时返回新的偏移量,出错时返回-1。文件的读写位置是由文件结构中的fd_pos决 定的,因此sys_lseek的原理是将whence+offset转换为fd_pos。

函数开头先将文件描述符fd通过fd_local2global转换为文件表中的下标,然后在struct file* pf = &file_table[_fd]用pf指向对应的文件结构,后面都用指针pf来操作啦。接着声明变量new_pos作为新的fd_pos值,后面会根据不同的情况计算该值。file_size 是文件的大小,下面通过 switch 结构判断参照物 whence 的情况。位置指针以 0 为起始偏移量,无论 whence 是 SEEK_SET、SEEK_CUR,还是 SEEK_END,新位置指针 new_pos 肯定要在文件大小范围内,不能小于 0,也不能超过文件尺寸-1,即不能大于等于文件尺寸。

switch (whence)分别计算了三种情况下的new_pos。

if (new_pos < 0 || new_pos > (file_size – 1))判断新的位置new_pos是否在文件大小范围之外,如果是就返回-1,否则就在pf->fd_pos = new_pos用new_pos 修改文件的 fd_pos,然后返回新的位置。

 

同样在main.c文件测试

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");

    uint32_t fd = sys_open("/file1", O_RDWR);
    printf("open /file1, fd:%d\n", fd);
    char buf[64] = {0};
    int read_bytes = sys_read(fd, buf, 18);
    printf("1_ read %d bytes:\n%s\n", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("2_ read %d bytes:\n%s", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("3_ read %d bytes:\n%s", read_bytes, buf);

    printf("________  SEEK_SET 0  ________\n");
    sys_lseek(fd, 0, SEEK_SET);
    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 24);
    printf("4_ read %d bytes:\n%s", read_bytes, buf);

    sys_close(fd);
    while(1);
    return 0;
}

使用sys_lseek就不用先close再open了,功能是把读写位置指针置为文件开头偏移为 0 的地址。

 

 

4.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇