【Create my OS】8 文件系统

December 29, 2024

Github代码仓库链接

本章将实现一个很基础的文件系统,并实现一些简单的内核文件操作,最终,我们将用从文件系统读入 ELF 文件的方式来创建进程。

8.1 SimpleFS

1、本节将对 Unix File System 的一个简单的实现——SimpleFileSystem 进行魔改,但是依旧沿用这个名字,因为这个文件系统足够 simple

alt text

2、超级块

typedef struct {
    uint32 magic;               // 魔数
    uint32 blocks;              // 总磁盘块数
    uint32 unusedBlocks;        // 未使用的磁盘块数
    uint32 freemapBlocks;       // freemap 块数
    uint8 info[32];             // 其他信息
} SuperBlock;
  • 第一个字段就是 magic number。这个字段恒为 0x4D534653,即 ASCII 码 “M”、“S”、“F”、“S”。在操作系统初始化文件系统时,会首先检查魔数以确定文件系统类型
  • blocks 字段表示这个文件系统一共占用多少个磁盘块
  • unusedBlocks 字段表示这个文件系统目前剩余的空闲磁盘块个数
  • freemapBlocks 表示 Freemap 块的总个数,这个字段的更重要用处在于推断出 Root Inode 所在的磁盘块号。
  • 最后就是 info 字段,这是一个字符串,记录了一些其他信息。
  • 超级块的实际内容很小,只有 48 字节,超级块的剩余空间由 0 填充。

3、Inode ,一个 Inode 块代表了一个文件或文件夹,结构如下:

typedef struct
{
    uint32 size;                // 文件大小,type 为文件夹时该字段为 0
    uint32 type;                // 文件类型
    uint8 filename[32];         // 文件名称
    uint32 blocks;              // 占据磁盘块个数
    uint32 direct[12];          // 直接磁盘块
    uint32 indirect;            // 间接磁盘块
} Inode;
  • type 字段表示这个 Inode 所代表的文件的类型,可用取值有 TYPE_FILE (普通文件) 或 TYPE_DIR (文件夹)。当 type 取 TYPE_DIR 时,size 字段为 0。并且直接磁盘块和间接磁盘块都是指向存储在该文件夹下的文件的 Inode。当 type 取 TYPE_FILE 时,磁盘块指向实际的数据块。
  • 注意,当 Inode 类型为文件夹时,direct[0] direct[1] 分别存储指向当前和指向上一级文件夹的 Inode。
  • 关于直接磁盘块和间接磁盘块,示意图如下:

alt text

  • 每个 Inode 块中有一个长度为 12 的 direct 数据,如果 blocks 字段小于等于 12,可以直接使用该数组存储磁盘块号,否则,由 indirect 字段指向一个 Indirect Block,在该磁盘块中可以存储更多的磁盘块号。
  • Inode 块的剩余空间也由 0 填充。

4、相关数据

  • 一个 Freemap 块可以表示 32K 个磁盘块的空闲状况(一个磁盘块为 4K 大小,共 32K 个bit位)
  • 一个文件夹下,除去 “.”“..” 以外,最多可以存储 1034 个文件或文件夹(12+1024)
  • 单个普通文件大小最大为 4.04 MB(1034*4K=4.04M

8.2 打包镜像

1、多用户程序编译

  • 我们将 user 文件夹下的文件分成两类,一类是依赖,一类是用户编写的应用程序
UPROSBASE =                    \
    $U/entry.o                \
    $U/malloc.o                \
    $U/io.o                    

UPROS =                        \
    hello                    \
    hello2
  • 这里定义了两个应用程序,hello2 仅仅是把 hello 复制了一遍,并修改了输出。
  • 接着我们就需要遍历 UPROS,来将每一个用户程序都编译成一个可执行文件,而不是把它们全都链接成一个文件。
User: $(subst .c,.o,$(wildcard $U/*.c))
    mkdir -p rootfs/bin
    for file in $(UPROS); do                                            \
        $(LD) $(LDFLAGS) -o rootfs/bin/$$file $(UPROSBASE) $U/$$file.o;    \
    done
  • User 任务首先会创建文件夹 rootfs/bin,所有编译出的可执行文件都会被放置在这个文件夹下。之后就是将每个应用程序都和其依赖链接起来,链接成一个可执行文件。

2、经过以上过程后,所有的用户程序都会被编译成可执行文件放在 rootfs/bin 文件夹下。现在我们要以 rootfs 作为文件系统的根目录,将其打包成一个 SimpleFS 格式的镜像。

  • 首先要定义文件系统的一些结构:
// mkfs/simplefs.h

#ifndef _SIMPLEFS_H
#define _SIMPLEFS_H

#include "types.h"

#define BLOCK_SIZE  4096
#define MAGIC_NUM   0x4D534653U // MSFS

typedef struct {
    uint32 magic;               // 魔数
    uint32 blocks;              // 总磁盘块数
    uint32 unusedBlocks;        // 未使用的磁盘块数
    uint32 freemapBlocks;       // freemap 块数
    uint8 info[32];             // 其他信息
} SuperBlock;

#define TYPE_FILE   0
#define TYPE_DIR    1

typedef struct
{
    uint32 size;                // 文件大小,type 为文件夹时该字段为0
    uint32 type;                // 文件类型
    uint8 filename[32];         // 文件名称
    uint32 blocks;              // 占据磁盘块个数
    uint32 direct[12];          // 直接磁盘块
    uint32 indirect;            // 间接磁盘块
} Inode;

#endif
  • 接着就是按部就班地填充各个块了。我们定义的一个 Image 字节数组,它的大小和文件系统一致,所有的块都首先写入 Image 中,最后再将 Image 保存成文件。
// mkfs/mksfs.c

/*
 * mksfs.c 用于将一个文件夹作为根目录打包成一个 SimpleFS 镜像文件
 * 
 * SimpleFS 镜像组成如下:
 * +-------+------+     +------+------+------+     +------+
 * | Super | Free | ... | Free | Root |Other | ... |Other |
 * | Block | Map  |     | Map  |Inode |Inode |     |Inode |
 * +-------+------+     +------+------+------+     +------+
 */

#include "types.h"
#include "simplefs.h"
// 标准库
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>

// 总块数 256 块,大小为 1M
#define BLOCK_NUM       256
// Freemap 块的个数
#define FREEMAP_NUM     1

// 定义 Image 字节数组,它的大小和文件系统一致
// 所有的块都首先写入 Image 中,最后再将 Image 保存成文件
// 最终的镜像数据
char Image[BLOCK_SIZE * BLOCK_NUM];

// 临时的 freemap,最后需要写入 Image,此时一个 char 代表一块
char freemap[BLOCK_NUM];
uint32 freenum = BLOCK_NUM;

// 被打包的文件夹名称
char *rootdir = "rootfs";

void walk(char *dirName, Inode *nowInode, uint32 nowInodeNum);
uint64 getBlockAddr(int blockNum);
int getFreeBlock();
void copyInodeToBlock(int blockNum, Inode *in);

// 初始化各个块,并写入 Image 中
void
main()
{
    // 最开始的几块分别是 超级块,freemap 块 和 root 文件夹所在的 inode
    freemap[0] = 1;
    int i;
    // 设置 freemap 块为已占用
    for(i = 0; i < FREEMAP_NUM; i ++) freemap[1+i] = 1;
    freemap[FREEMAP_NUM + 1] = 1;   // 设置根目录块为已占用
    freenum -= (FREEMAP_NUM + 2);   // 更新空闲块数量
    
    // 填充 superblock 信息
    SuperBlock spBlock;
    spBlock.magic = MAGIC_NUM;              // 魔数
    spBlock.blocks = BLOCK_NUM;             // 文件系统总块数
    spBlock.freemapBlocks = FREEMAP_NUM;    // 空闲块数
    char *info = "SimpleFS By JokerDebug";  // 文件系统信息
    for(i = 0; i < strlen(info); i ++) {
        spBlock.info[i] = info[i];
    }
    spBlock.info[i] = '\0';
    
    // 设置根 inode
    Inode rootInode;
    rootInode.size = 0;         // 目录文件大小为0
    rootInode.type = TYPE_DIR;  // 文件类型为目录
    rootInode.filename[0] = '/'; rootInode.filename[1] = '\0';  // 根目录文件名
    // 递归遍历根文件夹,并设置和填充数据

    // 递归遍历文件夹,为每个文件和文件夹创建 Inode 并填充信息
    walk(rootdir, &rootInode, FREEMAP_NUM+1);

    spBlock.unusedBlocks = freenum; // 设置超级块中未使用的块数

    // 将超级块写入 Image
    char *ptr = (char *)getBlockAddr(0), *src = (char *)&spBlock;
    for(i = 0; i < sizeof(spBlock); i ++) {
        ptr[i] = src[i];
    }

    // 将 freemap 写入 Image
    ptr = (char *)getBlockAddr(1);
    for(i = 0; i < BLOCK_NUM/8; i ++) {
        char c = 0;
        int j;
        for(j = 0; j < 8; j ++) {
            if(freemap[i*8+j]) {
                c |= (1 << j);
            }
        }
        *ptr = c;
        ptr ++;
    }

    // 将 rootInode 写入 Image
    copyInodeToBlock(FREEMAP_NUM+1, &rootInode);

    // 将 Image 写到磁盘上
    FILE *img = fopen("fs.img", "w+b");
    fwrite(Image, sizeof(Image), 1, img);
    fflush(img); fclose(img);
}
  • walk() 函数用于递归遍历文件夹,为每个文件和文件夹创建 Inode 并填充信息
// mkfs/mksfs.c

/* 根据块号获取 Image 中的块的起始地址 */
uint64
getBlockAddr(int blockNum) {
    void *addr = (void *)Image;         // Image起始地址
    addr += (blockNum * BLOCK_SIZE);    // 加上偏移
    return (uint64)addr;
}

// 递归遍历文件夹,为每个文件和文件夹创建 Inode 并填充信息,将文件的 inode 写入磁盘块
// dirName 当前文件夹名,nowInode 为当前文件夹的 Inode,nowInodeNum 为其 Inode 号
void
walk(char *dirName, Inode *nowInode, uint32 nowInodeNum)
{
    // 打开当前文件夹
    DIR *dp = opendir(dirName);
    struct dirent *dirp;

    // 文件夹下第一个文件为其自己
    nowInode->direct[0] = nowInodeNum;
    if(!strcmp(dirName, rootdir)) {     // 判断是否为根目录
        // 若在根目录,则无上一级,上一级文件夹也为其自己
        nowInode->direct[1] = nowInodeNum;
    }
    // 下一个文件的序号
    int emptyIndex = 2;     // 初始化空闲位置索引,从第 2 个位置开始

    // 遍历当前文件夹下所有文件,创建 Inode 并填充信息
    while((dirp = readdir(dp))) {
        if(!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, "..")) {
            // 跳过 . 和 ..
            continue;
        }
        int blockNum;
        if(dirp->d_type == DT_DIR) {
            // 文件夹处理,递归遍历
            Inode dinode;
            dinode.size = 0;            // 设置文件夹 inode 大小为 0
            dinode.type = TYPE_DIR;     // 设置文件夹类型
            int i;
            for(i = 0; i < strlen(dirp->d_name); i ++) {
                dinode.filename[i] = dirp->d_name[i];   // 将文件夹名称复制到 inode 的 filename
            }
            dinode.filename[i] = '\0';
            blockNum = getFreeBlock();          // 获取一个空闲的块来存储文件夹
            // 文件夹的前两个文件分别为 . 和 ..
            dinode.direct[0] = blockNum;        // 当前文件夹
            dinode.direct[1] = nowInodeNum;     // 父文件夹
            char *tmp = (char *)malloc(strlen(dirName) + strlen(dirp->d_name) + 1);
            sprintf(tmp, "%s/%s", dirName, dirp->d_name);   // 拼接文件夹的完整路径
            walk(tmp, &dinode, blockNum);                   // 递归处理子文件夹

            copyInodeToBlock(blockNum, &dinode);            // 将文件夹的 inode 写入磁盘块
        } else if(dirp->d_type == DT_REG) {
            // 普通文件处理
            Inode finode;
            finode.type = TYPE_FILE;
            int i;
            for(i = 0; i < strlen(dirp->d_name); i ++) {
                finode.filename[i] = dirp->d_name[i];       // 将文件名称复制到 inode 的 filename
            }
            finode.filename[i] = '\0';
            char *tmp = (char *)malloc(strlen(dirName) + strlen(dirp->d_name) + 1);
            sprintf(tmp, "%s/%s", dirName, dirp->d_name);   // 拼接文件的完整路径
            // 获取文件信息
            struct stat buf;
            stat(tmp, &buf);            // 获取文件的状态信息
            finode.size = buf.st_size;  // 设置文件大小
            finode.blocks = (finode.size - 1) / BLOCK_SIZE + 1; // 计算文件所占的块数
            
            blockNum = getFreeBlock();  // 获取一个空闲的块来存储文件数据

            // 将文件数据复制到对应的块
            uint32 l = finode.size;         // 剩余未拷贝的大小
            int blockIndex = 0;
            FILE *fp = fopen(tmp, "rb");    // 以二进制读取文件
            while(l) {
                int ffb = getFreeBlock();                   // 获取空闲块
                char *buffer = (char *)getBlockAddr(ffb);   // 获取块的地址
                size_t size;
                if(l > BLOCK_SIZE) size = BLOCK_SIZE;       // 如果剩余数据大于块大小,则取块大小
                else size = l;                              // 否则剩余的大小为实际大小
                fread(buffer, size, 1, fp);                 // 将文件内容读取到 buffer
                l -= size;                                  // 减少剩余大小
                if(blockIndex < 12) {
                    finode.direct[blockIndex] = ffb;        // 前 12 块直接存储数据
                } else {
                    // 12 个间接块均已使用
                    if(finode.indirect == 0) {
                        finode.indirect = getFreeBlock();   // 如果间接块未分配,分配一个空闲块
                    }
                    uint32 *inaddr = (uint32 *)getBlockAddr(finode.indirect);   // 获取间接块的地址
                    inaddr[blockIndex - 12] = ffb;          // 将块号保存到间接块中(共可存储1024个块,减去12的直接块偏移)
                }
                blockIndex ++;                              // 增加块索引
            }
            fclose(fp);                                     // 关闭文件
            copyInodeToBlock(blockNum, &finode);            // 将文件的 inode 写入磁盘块
        } else {
            continue;   // 跳过其他类型的文件
        }
        
        // 更新当前文件夹 nowInode 的信息
        if(emptyIndex < 12) {
            // 如果直接块未满,直接将块号存储到 nowInode 的 direct 数组中
            nowInode->direct[emptyIndex] = blockNum;
        } else {
            if(nowInode->indirect == 0) {
                nowInode->indirect = getFreeBlock();    // 如果间接块未分配,分配一个空闲块
            }
            uint32 *inaddr = (uint32 *)getBlockAddr(nowInode->indirect);    // 获取间接块的地址
            inaddr[emptyIndex - 12] = blockNum;         // 将块号存储到间接块中
        }
        emptyIndex ++;              // 增加空闲位置索引
    }
    closedir(dp);                   // 关闭当前文件夹
    nowInode->blocks = emptyIndex;  // 更新当前文件夹所占的块数
}
  • getFreeBlock() 函数用于从 freemap 中找到一个空闲的块,将其标记为占用并返回其块号
// mkfs/mksfs.c

// 从 freemap 中找到一个空闲的块,将其标记为占用并返回其块号
int getFreeBlock() {
    int i;
    // 遍历freemap标志位
    for(i = 0; i < BLOCK_NUM; i ++) {
        if(!freemap[i]) {
            freemap[i] = 1;     // 标记为占用
            freenum --;         // 更新空闲块数
            return i;
        }
    }
    printf("get free block failed!\n");
    exit(1);
}

3、在 Makefile 中新建一个 target,用来编译这个打包工具。此时就不需要使用 RISCV 工具链了(riscv64-linux-gnu-objdump),因为我们需要在当前机器上完成打包过程,所以直接使用 gcc 即可。编译完成后,在 User 运行的最后执行这个打包工具。

mksfs:
    gcc mkfs/mksfs.c -o mksfs

User: mksfs $(subst .c,.o,$(wildcard $U/*.c))
    mkdir -p rootfs/bin
    for file in $(UPROS); do                                            \
        $(LD) $(LDFLAGS) -o rootfs/bin/$$file $(UPROSBASE) $U/$$file.o;    \
    done
    ./mksfs
  • 打包后的结果为 fs.img 文件,大小为 1M,和我们在代码中定义的大小相同。

4、合并内核

  • 这里为了简化过程,依旧是将文件系统的内容直接链接到 .data 段,这样做可以更加注重文件系统相关的操作,而不是去适配 QEMU 的驱动。
  • 只需要修改之前用来链接 ELF 文件的汇编即可。
// kernel/linkFS.asm

# 将文件系统链接到 .data 段

.section .data
    .global _fs_img_start
    .global _fs_img_end
_fs_img_start:
    .incbin "fs.img"
_fs_img_end:

8.3 内核文件驱动

1、目前的需求是,可以根据一个路径,从文件系统中找到这个文件的 Inode,并且读取它的所有数据到一个字节数组中。基于这个需求,我们先定义一些函数,创建文件等功能可以后续再支持。

  • 文件系统初始化:找到 root inode 即可。
// kernel/fs.c

Inode *ROOT_INODE;      // 声明一个指针 ROOT_INODE,指向根目录的 Inode 结构
char *FREEMAP;          // 声明一个指针 FREEMAP,指向文件系统的 freemap

// 初始化文件系统(只需要找到 root inode 即可)
void
initFs()
{
    // 获取 freemap 块地址
    FREEMAP = (char *)getBlockAddr(1);      
    // 获取超级块的起始地址,_fs_img_start 是文件系统镜像的起始位置(在 linkUser 中定义)
    SuperBlock* spBlock = (SuperBlock *)_fs_img_start;  
    // 根目录的 Inode 紧接在 freemap 块之后
    ROOT_INODE = (Inode *)getBlockAddr(spBlock->freemapBlocks + 1);
}
  • 因为线程初始化要从文件系统中读取 ELF 文件,所以文件系统的初始化需要在线程之前
// kernel/main.c

void main()
{
    extern void initInterrupt();    initInterrupt();    // 设置中断处理程序入口 和 模式
    extern void initTimer();        initTimer();        // 时钟中断初始化
    extern void initMemory();       initMemory();       // 初始化 页分配 和 动态内存分配
    extern void mapKernel();        mapKernel();        // 内核重映射,三级页表机制
    extern void initFs();           initFs();           // 初始化文件系统
    extern void initThread();       initThread();       // 初始化线程管理
    extern void runCPU();           runCPU();           // 切换到 idle 调度线程,表示正式由 CPU 进行线程管理和调度

    while(1) {}
}
  • lookup() 函数将传入的 Inode 作为当前目录,从当前目录下根据路径查找文件。例如,若当前目录为 /usr,传入 ./hello 和 hello 甚至 ../usr/hello 都可以找到可执行文件 hello。如果传入的路径以 / 开头,函数会忽略当前路径而从根目录下开始查找。如果传入的 Inode 为 null,函数也会从根目录开始查找。
  • 函数根据路径分割出需要在当前目录下查找的文件或文件夹名称,并递归调用 lookup 函数进一步搜索。
// kernel/fs.c

/*
 * 传入的 Inode 作为当前目录,从当前目录下根据路径查找文件
 * 若当前目录为 /usr,传入 ./hello 和 hello 甚至 ../usr/hello 都可以找到可执行文件 hello
 * 如果传入的路径以 / 开头,函数会忽略当前路径而从根目录下开始查找
 * 如果传入的 Inode 为 null,函数也会从根目录开始查找 
*/
Inode *
lookup(Inode *node, char *filename)
{
    // 如果文件名以 '/' 开头,表示从根目录开始查找
    if(filename[0] == '/') {
        node = ROOT_INODE;      // 将当前节点指向根目录的 Inode
        filename ++;            // 跳过 '/' 字符
    }
    // 如果当前节点为 NULL,则设为根目录 Inode
    if(node == 0) node = ROOT_INODE;
    // 如果文件名为空(表示已找到目标 Inode),返回当前节点
    if(*filename == '\0') return node;
    // 如果当前节点不是目录类型,则返回 NULL
    if(node->type != TYPE_DIR) return 0;
    // 创建一个字符串变量用来存储目标文件名
    char cTarget[strlen(filename) + 1];
    int i = 0;
    // 从文件名中提取出一个部分(直到遇到 '/' 或字符串结束)
    while (*filename != '/' && *filename != '\0') {
        cTarget[i] = *filename;
        filename ++;
        i ++;
    }
    cTarget[i] = '\0';
    // 如果文件名后面还有 '/',则跳过它
    if(*filename == '/') filename ++;
    // 如果目标文件名是 ".",表示当前目录,递归调用 lookup 查找下一个部分
    if(!strcmp(".", cTarget)) {
        return lookup(node, filename);
    }
    // 如果目标文件名是 "..",表示父目录,递归查找父目录
    if(!strcmp("..", cTarget)) {
        Inode *upLevel = (Inode *)getBlockAddr(node->direct[1]);    // 获取父目录的 Inode
        return lookup(upLevel, filename);       // 递归查找父目录
    }
    // 当前节点的块数
    int blockNum = node->blocks;
    // 如果块数小于等于 12,则直接在 direct 数组中查找文件
    if(blockNum <= 12) {
        for(i = 2; i < blockNum; i ++) {
            Inode *candidate = (Inode *)getBlockAddr(node->direct[i]);  // 获取当前块的 Inode
            // 如果文件名匹配,则递归查找该文件
            if(!strcmp((char *)candidate->filename, cTarget)) {
                return lookup(candidate, filename);
            }
        }
        return 0;   // 如果没有找到匹配的文件,则返回 NULL
    } else {
        // 如果块数大于 12,先在 direct 数组中查找
        for(i = 2; i < 12; i ++) {
            Inode *candidate = (Inode *)getBlockAddr(node->direct[i]);  // 获取当前块的 Inode
            // 如果文件名匹配,则递归查找该文件
            if(!strcmp((char *)candidate->filename, cTarget)) {
                return lookup(candidate, filename);
            }
        }
        // 如果文件仍然没有找到,则在间接块(indirect)中查找
        uint32 *indirect = (uint32 *)getBlockAddr(node->indirect);      // 获取间接块的地址
        for(i = 12; i < blockNum; i ++) {
            Inode *candidate = (Inode *)getBlockAddr(indirect[i-12]);   // 获取间接块中的 Inode
            // 如果文件名匹配,则递归查找该文件
            if(!strcmp((char *)candidate->filename, cTarget)) {
                return lookup(candidate, filename);
            }
        }
        return 0;   // 如果没有找到匹配的文件,则返回 NULL
    }
}
  • readall() 函数传入一个 Inode 和一个字节数组,Inode 应当为文件类型的 Inode。这个函数将该 Inode 所代表的文件数据全部拷贝到字节数组中
/* 读取一个表示文件的 Inode 的所有字节到 buf 中 */
void
readall(Inode *node, char *buf) {
    // 检查 Inode 类型是否为文件,如果不是文件则触发 panic
    if(node->type != TYPE_FILE) {
        panic("Cannot read a directory!\n");
    }
    // 获取文件的大小和文件占用的块数
    int l = node->size, b = node->blocks;
    // 如果文件占用的块数小于或等于 12,则直接在 direct 数组中读取数据
    if(b <= 12) {
       int i;
       for(i = 0; i < b; i ++) {
           // 获取当前块的地址
           char *src = (char *)getBlockAddr(node->direct[i]);
           // 拷贝大小判断,大于一页取4096
           int copySize = l >= 4096 ? 4096 : l;
           // 将数据从块中复制到 buf 中
           copyByteToBuf(src, buf, copySize);
           // 更新 buf 指针,指向下一个写入的位置
           buf += copySize;
           // 更新剩余大小
           l -= copySize;
       }
    } else {
        // 如果文件占用的块数大于 12,先处理前 12 块
        int i;
        // 同上
        for(i = 0; i < 12; i ++) {
            char *src = (char *)getBlockAddr(node->direct[i]);
            int copySize = l >= 4096 ? 4096 : l;
            copyByteToBuf(src, buf, copySize);
            buf += copySize;
            l -= copySize;
        }
        // 获取间接块地址
        uint32 *indirect = (uint32 *)getBlockAddr(node->indirect);
        // 处理所有间接块数据
        for(i = 0; i < b-12; i ++) {
            char *src = (char *)getBlockAddr(indirect[i]);
            int copySize = l >= 4096 ? 4096 : l;
            copyByteToBuf(src, buf, copySize);
            buf += copySize;
            l -= copySize;
        }
    }
}

8.4 文件系统测试

1、我们完成了文件系统的简单驱动,现在就可以从文件系统中读取 ELF 文件了!

// kernel/thread.c

// 初始化线程
void initThread()
{
    // 1.创建调度函数实现
    Scheduler s = {
        schedulerInit,
        schedulerPush,
        schedulerPop,
        schedulerTick,
        schedulerExit};
    s.init(); // 初始化调度器
    // 2.创建线程池
    ThreadPool pool = newThreadPool(s);
    // 3.构建idle调度线程
    Thread idle = newKernelThread((usize)idleMain);
    // 4.初始化CPU调度器
    initCPU(idle, pool);
    // 5.构造线程并添加到CPU中
    usize i;
    for (i = 0; i < 5; i++)
    {
        Thread t = newKernelThread((usize)helloThread); // 构造新内核线程
        usize args[8];
        args[0] = i;
        appendArguments(&t, args); // 为线程传入初始化参数
        // 6.启动
        addToCPU(t); // 将线程添加到调度队列中
    }
    // 从文件系统中读取 elf 文件
    Inode *helloInode = lookup(0, "/bin/hello");    // 查找文件inode
    char *buf = kalloc(helloInode->size);           // 分配内存
    readall(helloInode, buf);                       // 读取文件所有数据到buf
    Thread t = newUserThread(buf);
    kfree(buf);
    // 创建一个用户线程并添加到 CPU
    // extern void _user_img_start(); // linkUser.asm中定义用户程序位置
    // Thread t = newUserThread((char *)_user_img_start);
    addToCPU(t);
    printf("***** init thread *****\n");
}
  • 首先获取到 ELF 文件的 Inode,根据其大小分配一个 buffer,再将文件的内容全部读取到 buffer 里。再次运行,运行结果和上一章一致。

友情提示,在kernel文件夹下记得新增如下文件并引用:

// kernel/fs.h

#ifndef _SIMPLEFS_H
#define _SIMPLEFS_H

#include "types.h"

#define BLOCK_SIZE  4096
#define MAGIC_NUM   0x4D534653U // MSFS

typedef struct {
    uint32 magic;               // 魔数
    uint32 blocks;              // 总磁盘块数
    uint32 unusedBlocks;        // 未使用的磁盘块数
    uint32 freemapBlocks;       // freemap 块数
    uint8 info[32];             // 其他信息
} SuperBlock;

#define TYPE_FILE   0
#define TYPE_DIR    1

typedef struct
{
    uint32 size;                // 文件大小,type 为文件夹时该字段为0
    uint32 type;                // 文件类型
    uint8 filename[32];         // 文件名称
    uint32 blocks;              // 占据磁盘块个数
    uint32 direct[12];          // 直接磁盘块
    uint32 indirect;            // 间接磁盘块
} Inode;

Inode *lookup(Inode *node, char *filename);
void readall(Inode *node, char *buf);
// void ls(Inode *node);
// char *getInodePath(Inode *inode, char path[256]);

#endif
  • 同时也记得新增string.c的字符串操作
// kernel/string.c

// 字符串长度
int
strlen(char *str)
{
    int num = 0;
    while (*str != '\0') {
        num ++;
        str ++;
    }
    return num;
}

// 字符串对比
int
strcmp(char *str1, char *str2)
{

    if(strlen(str1) != strlen(str2)) return 1;
    int i, len = strlen(str1);
    for(i = 0; i < len; i ++) {
        if(str1[i] != str2[i]) return 1;
    }
    return 0;
}

Profile picture

Written by JokerDebug who works at Southeast University, Nanjing, China You can follow me on Github