自制操作系统 - 文件系统相关系统调用(三)

文件系统相关系统调用(三)

Posted by 王富杰 on Saturday, March 9, 2024

一、获取文件状态

文件状态指的是文件的属性、大小、最近访问时间、最后修改时间、所属用户、 所属用户组等信息,包含两个函数,一个是通过文件名获取文件状态,一个是通过文件描述符获取文件状态。当ls命令加上 -l 参数时,就可以获取文件状态并打印。

typedef struct stat_t
{
    dev_t dev;    // 含有文件的设备号
    idx_t nr;     // 文件 i 节点号
    u16 mode;     // 文件类型和属性
    u8 nlinks;    // 指定文件的连接数
    u16 uid;      // 文件的用户(标识)号
    u8 gid;       // 文件的组号
    dev_t rdev;   // 设备号(如果文件是特殊的字符文件或块文件)
    size_t size;  // 文件大小(字节数)(如果文件是常规文件)
    time_t atime; // 上次(最后)访问时间
    time_t mtime; // 最后修改时间
    time_t ctime; // 最后节点修改时间
} stat_t;

int sys_stat(char *filename, stat_t *statbuf)
{
    inode_t *inode = namei(filename);   // 获取文件状态
    if (!inode)
    {
        return EOF;
    }
    copy_stat(inode, statbuf);  // 从内核中 拷贝inode的状态到用户态。
    iput(inode);
    return 0;
}

完成文件状态调用时,修改ls内建命令的实现,就可以使用-l参数来获取文件的详细信息了。通过文件描述符获取文件状态基本是一样的,通过文件描述符获取到文件结构,再获取到inode,最后拷贝文件状态到内核态。

二、创建设备文件

mknod 是用来创建设备文件的。在linux系统上,可以在/dev目录下看到许多的设备文件,这些都是通过mknod系统调用实现的。

int sys_mknod(char *filename, int mode, int dev)  // 传入文件名,权限和设备
{
    char *next = NULL;
    inode_t *dir = NULL;
    buffer_t *buf = NULL;
    inode_t *inode = NULL;
    int ret = EOF;
    dir = named(filename, &next);  // 获取父目录的inode
    // ...... 权限等信息的判断

    char *name = next;
    dentry_t *entry;
    buf = find_entry(&dir, name, &next, &entry);
    if (buf) // 目录项存在
        goto rollback;

    buf = add_entry(dir, name, &entry);  // 如果不存在添加目录项
    buf->dirty = true;
    entry->nr = ialloc(dir->dev);
    inode = new_inode(dir->dev, entry->nr);  // 申请一个inode
    inode->desc->mode = mode;
    if (ISBLK(mode) || ISCHR(mode))  // 如果是块设备或字符设备,就把设备直接写入inode的zone[0]
        inode->desc->zone[0] = dev;
    ret = 0;

rollback:
    brelse(buf);
    iput(inode);
    iput(dir);
    return ret;
}

在完成mknod系统调用后,就可以进行设备的初始化了。如下:

void dev_init()
{
    mkdir("/dev", 0755);  // 创建 /dev 目录
    device_t *device = NULL;
    device = device_find(DEV_CONSOLE, 0);   // 查找控制台和键盘设备,并创建文件
    mknod("/dev/console", IFCHR | 0200, device->dev);
    device = device_find(DEV_KEYBOARD, 0);
    mknod("/dev/keyboard", IFCHR | 0400, device->dev);

    char name[32];
    for (size_t i = 0; true; i++)  // 初始化磁盘设备
    {
        device = device_find(DEV_IDE_DISK, i);
        if (!device)
            break;
        sprintf(name, "/dev/%s", device->name);
        mknod(name, IFBLK | 0600, device->dev);
    }

    for (size_t i = 0; true; i++)
    {
        device = device_find(DEV_IDE_PART, i);  // 初始化分区设备
        if (!device)
        {
            break;
        }
        sprintf(name, "/dev/%s", device->name);
        mknod(name, IFBLK | 0600, device->dev);
    }
}

在完成设备初始化后,就会在/dev目录下看到设备文件。目前我们是直接把这些设备文件直接写到了磁盘,这些设备是无法使用,暂时先这样。

三、设备挂载与卸载

设备挂载与卸载使用到的系统调用时 mount 和 umount。

int sys_mount(char *devname, char *dirname, int flags)  // 设备挂载
{
    LOGK("mount %s to %s\n", devname, dirname);   
    inode_t *devinode = NULL;
    inode_t *dirinode = NULL;
    super_block_t *sb = NULL;
    devinode = namei(devname);   // 查找设备文件,如果不存在或者不是块设备就结束
    if (!devinode)
        goto rollback;
    if (!ISBLK(devinode->desc->mode))
        goto rollback;

    dev_t dev = devinode->desc->zone[0];  // 找到设备号
    dirinode = namei(dirname);   // 查找被挂载点, 如果不存在或者不是目录就结束
    if (!dirinode)
        goto rollback;
    if (!ISDIR(dirinode->desc->mode))
        goto rollback;
    if (dirinode->count != 1 || dirinode->mount)  // 如果目录的引用不是1 或者 该目录已经被挂载了就结束。
        goto rollback;

    sb = read_super(dev);  // 读取设备的超级块
    if (sb->imount)
        goto rollback;
    sb->iroot = iget(dev, 1);   // 将根目录设置为这个设备的第一个inode
    sb->imount = dirinode;      // 这个设备的imount设置为被挂载点的inode
    dirinode->mount = dev;      // 目录的mount属性设置为设备号
    iput(devinode);             // 释放设备的inode
    return 0;
rollback:
    put_super(sb);
    iput(devinode);
    iput(dirinode);
    return EOF;
}

卸载和挂载是相反的操作,这里不再提供卸载的代码展示。这里还有两个特别的点,一个是如果一个目录被挂载了,那在进行iget时这个目录inode就需要变成被挂载设备的根目录。

static inode_t *fit_inode(inode_t *inode)
{
    if (!inode->mount)
        return inode;

    super_block_t *sb = get_super(inode->mount);
    assert(sb);
    iput(inode);
    inode = sb->iroot;
    inode->count++;
    return inode;
}

如上,在iget中如果找到了inode,就需要调用下fit_node判断inode是不是被挂载的,如果是被挂载,那就是设置这个目录的inode为被挂载设备的根目录。

第二个点是通过 .. 访问父目录。这里修改的是find_entry函数,通过父目录的nr是否为1进行判断。当在文件的根目录需要解析 .. 文件夹时,执行如下代码:

static buffer_t *find_entry(inode_t **dir, const char *name, char **next, dentry_t **result)
{
    // 保证 dir 是目录
    assert(ISDIR((*dir)->desc->mode));

    if (match_name(name, "..", next) && (*dir)->nr == 1)
    {
        super_block_t *sb = get_super((*dir)->dev);  // 获取设备的超级块
        inode_t *inode = *dir;      // 保存旧的
        (*dir) = sb->imount;        // 把当前目录切换到挂载点目录
        (*dir)->count++;            // 新 inode 引用计数+1
        iput(inode);                // 释放旧 inode
    }
    ......
}

所以当访问 .. 时:不再返回子分区的根目录本身,而是返回到主分区中的挂载点目录 /mnt。举个例子,当位于 /mnt/etc 时,如果执行了 cd .. 。 那就应该回到挂载点 /mnt。这段逻辑就是为了解决「在挂载的子文件系统根目录访问 .. 时,要正确跳回挂载点目录」的问题。

实现了mount的系统调用后,就可以给从盘进行挂载操作了。需要提醒一点的是,主盘的挂载前边我们在代码mount_root中写死了, mount_root设置了主盘的超级块的iroot为主盘的第一个inode, 设置了mount为主盘的设备号。 这里注意系统初始化是mount_root是第一个被挂载的盘,因此它使用的inode就是申请的系统表的第0个元素的位置。在创建进程时设置了进程的iroot和ipwd为系统inode表的第0个元素, 并且设置了名字为 / 。

通过mount系统调用可以进行从盘的挂载,通过挂载将从盘超级块的inode设置为1号inode, 然后把超级块的imount设置为挂载点的inode。设置目录的属性为被挂载的设备号。然后释放设备文件的inode。这样当我们访问被挂载点目录查找被挂载点的inode时,就会判断这个inode是一个挂载点,将这个inode进行释放,并将设备的跟目录inode返回。这样就是在该设备中进行读写了。注意这里被挂载点的inode被释放只是在内存中被释放了,暂时无法访问,但是仍然存在于它的原磁盘上,当解除挂载时,这个目录就恢复正常访问状态了。

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付