当前位置: 首页 > news >正文

操作系统 4.5-文件使用磁盘的实现

通过文件进行磁盘操作入口

// 在fs/read_write.c中
int sys_write(int fd, const char* buf, int count)
{
    struct file *file = current->filp[fd];
    struct m_inode *inode = file->inode;
    if (S_ISREG(inode->i_mode))
        return file_write(inode, file, buf, count);
}

  1. 进程控制块(PCB)

    • current 是指向当前进程控制块(PCB)的指针。PCB 包含了进程的所有信息,包括打开的文件列表。

  2. 文件指针数组(filp)

    • current->filp[fd] 访问当前进程的文件指针数组,获取与文件描述符 fd 关联的 file 结构。

  3. 文件结构(file)

    • struct file 包含了打开文件的所有信息,包括指向 inode 的指针。

  4. inode 结构

    • struct m_inode 是文件的元数据结构,包含了文件的属性(如权限、大小、数据块位置等)。

    • inode->i_mode 包含了文件的类型和权限信息。

  5. 文件写入操作

    • file_write(inode, file, buf, count) 是实际执行文件写入操作的函数,它将 buf 中的 count 字节数据写入到由 inodefile 描述的文件中。

  6. 文件类型检查

    • S_ISREG(inode->i_mode) 检查 inode 表示的是否是一个普通文件。如果是,才执行写入操作。

  7. 数据流和控制流

    • 图中展示了从 write(fd) 调用开始,通过 PCB 和文件表,最终访问到 inode 和数据盘块的过程。

File_write

工作流程分析

  1. 确定要写入的字符段

    • 首先,需要确定要写入文件的字符段。这通常由文件的读写指针决定,该指针指示了文件中的当前位置。通过修改这个指针(例如使用 fseek),可以改变写入的起始位置,然后加上要写入的字符数量(count)来确定结束位置。

  2. 找到要写的盘块号

    • 确定了要写入的字符段后,下一步是找到这些字符对应的磁盘块号。这是通过文件的 inode 结构来实现的,inode 包含了文件的元数据,包括文件数据块的位置信息。

  3. 形成写入请求并执行写入

    • 使用找到的盘块号和要写入的数据(buf),形成一个写入请求(request),然后将这个请求放入磁盘调度算法(如电梯算法)中进行处理,以优化磁盘I/O操作。

函数参数解释:

  • inode

    • 这是一个指向文件的 inode 结构的指针。inode 包含了文件的元数据,如文件类型、权限、所有者、文件大小以及指向文件数据块的指针等。在写入操作中,inode 用于查找文件数据块的位置。

  • file

    • 这是一个指向文件结构的指针。文件结构包含了打开文件的所有信息,包括文件状态标志、文件的读写指针等。在写入操作中,file 结构用于获取文件的当前位置和状态。

  • buf

    • 这是一个指向要写入文件的数据缓冲区的指针。buf 包含了实际要写入文件的数据。

  • count

    • 这是一个整数,表示要写入文件的字节数。countbuf 参数一起,指定了要写入文件的数据量。

实现

以下是带有详细注解的 file_write 函数代码,解释了每一部分的功能和作用:

// file_write函数用于将数据从用户缓冲区写入到文件对应的磁盘块中
int file_write(struct m_inode *inode, struct file *filp, char *buf, int count)
{
    off_t pos; // 定义一个变量pos,用于记录写入文件的起始位置
​
    // 根据文件的读写标志(O_APPEND)和当前文件位置(filp->f_pos)确定写入的起始位置
    if (filp->f_flags & O_APPEND)
        pos = inode->i_size; // 如果设置了追加标志,则从文件末尾开始写入
    else
        pos = filp->f_pos; // 否则,从文件当前位置开始写入
​
    // 当还有字节需要写入时,继续循环
    while (i < count) {
        block = create_block(inode, pos / BLOCK_SIZE); // 创建或分配一个新的磁盘块
        bh = bread(inode->i_dev, block); // 读取指定的磁盘块到内存中,并将块地址存储在bh中
​
        int c = pos % BLOCK_SIZE; // 计算当前块内的偏移
        char *pc = c + bh->b_data; // 计算内存中块数据区域的起始地址
        bh->b_dirt = 1; // 标记块为脏块,表示该块已被修改,需要写回磁盘
        c = BLOCK_SIZE - c; // 计算剩余需要写入的字节数
        pos += c; // 更新文件位置
​
        // 将用户缓冲区buf中的数据逐字节写入到内存中的块数据区域bh->b_data
        while (c-- > 0)
            *(pc++) = get_fs_byte(buf++);
​
        brelse(bh); // 释放缓冲区,将其返回到缓冲区管理中
    }
​
    filp->f_pos = pos; // 更新文件的当前位置
}
  1. 确定写入位置

    • 通过检查文件的读写标志和当前文件位置,确定写入操作的起始位置。

  2. 循环写入数据

    • 使用 while 循环,根据要写入的字节数 count,逐块写入数据。

  3. 创建和读取磁盘块

    • create_block 函数用于创建或分配一个新的磁盘块。

    • bread 函数用于读取指定的磁盘块到内存中,并将块地址存储在 bh(缓冲头)中。

  4. 计算块内偏移和剩余字节数

    • 计算当前块内的偏移 c 和剩余需要写入的字节数。

  5. 写入数据

    • 将用户缓冲区 buf 中的数据逐字节写入到内存中的块数据区域 bh->b_data

  6. 标记块为脏块

    • bh->b_dirt 设置为 1,表示该块已被修改,需要写回磁盘。

  7. 释放缓冲区

    • 使用 brelse 函数释放缓冲区,将其返回到缓冲区管理中。

  8. 更新文件位置

    • 更新文件的当前位置 filp->f_pos,以便后续操作可以从正确的位置开始。

通过这些步骤,file_write 函数实现了将用户数据写入文件的完整过程,包括确定写入位置、创建和读取磁盘块、写入数据、标记块为脏块以及更新文件位置等。

Create_block

图中展示了 create_block 函数和 _bmap 函数的代码,这些函数用于在文件系统中创建和映射磁盘块。以下是提取的代码和注解:

提取的代码:

// create_block函数用于根据inode和块号分配或创建一个新的磁盘块
int create_block(struct m_inode *inode, int block)
{
    while (i < count) {
        block = create_block(inode, pos / BLOCK_SIZE);
        bh = bread(inode->i_dev, block);
    }
​
    int _bmap(m_inode *inode, int block, int create)
    {
        if (block < 7) {
            if (create && !inode->i_zone[block]) {
                inode->i_zone[block] = new_block(inode->i_dev);
                inode->i_ctime = CURRENT_TIME;
                inode->i_dirt = 1;
            }
            return inode->i_zone[block];
        }
        block -= 7;
        if (block < 512) {
            bh = bread(inode->i_dev, inode->i_zone[7]);
            return (bh->b_data)[block];
        }
    }
}

代码注解:

  1. create_block函数

    • 这个函数用于根据给定的 inode 和块号 block 分配或创建一个新的磁盘块。

    • 它通过调用 _bmap 函数来实现块的映射和创建。

  2. _bmap函数

    • 这是一个辅助函数,用于处理块号的映射。

    • 它首先检查块号是否小于7,这7个块号直接存储在 inodei_zone 数组中。

    • 如果块号小于7,并且需要创建新块(create 为真且当前块号未分配),则调用 new_block 函数分配新块,更新 inodei_ctimei_dirt 标志。

    • 如果块号大于等于7且小于512,表示这是一个一重间接块,需要读取间接块表并找到对应的块号。

  3. new_block函数

    • 这个函数用于分配一个新的磁盘块。具体实现未在图中展示,但通常涉及查找空闲块、分配块号等操作。

  4. bread函数

    • 这个函数用于读取指定的磁盘块到内存中。它将块地址存储在 bh(缓冲头)中,以便后续操作。

总结:

这段代码实现了文件系统中磁盘块的分配和映射机制。通过 create_block_bmap 函数,系统可以根据文件的 inode 和块号动态分配和映射磁盘块。这种机制支持文件的动态增长和高效的磁盘空间管理。

通过间接块表(如一重间接和二重间接块表),文件系统还可以支持非常大的文件,即使单个文件的数据量超过了直接块表所能表示的范围。

m_inode

图中展示了与设备文件的 inode 结构相关的代码,以及如何通过 sys_open 函数打开设备文件。以下是提取的代码和注解:

提取的代码:

// m_inode结构体定义,用于表示设备文件的inode
struct m_inode {
    unsigned short i_mode; // 文件的类型和属性
    unsigned short i_zone[9]; // 指向文件内容数据块
    struct task_struct *i_wait; // 多个进程共享的打开这个inode,有的进程等待...
    unsigned short i_count; // 
    unsigned char i_lock; // 
    unsigned char i_dirt; // 
};
​
// sys_open函数用于打开文件或设备
int sys_open(const char* filename, int flag)
{
    if (S_ISCHR(inode->i_mode)) { // 检查是否为字符设备文件
        if (MAJOR(inode->i_zone[0]) == 4) { // 检查设备文件类型
            current->tty = MINOR(inode->i_zone[0]); // 设置当前进程的tty
        }
    }
}
​
// 宏定义,用于从设备号中提取主设备号和次设备号
#define MAJOR(a) ((((unsigned)(a)) >> 8)) // 取高字节
#define MINOR(a) ((a) & 0xff) // 取低字节

代码注解:

  1. m_inode结构体

    • i_mode:表示文件的类型和属性,例如普通文件、目录、字符设备等。

    • i_zone:一个数组,用于存储指向文件内容数据块的指针或设备号等信息。

    • i_wait:指向等待该inode的进程的task_struct结构体。

    • i_count:表示当前有多少个进程打开了该inode。

    • i_lock:用于控制对inode的访问,防止并发访问导致的问题。

    • i_dirt:表示inode是否被修改,需要写回磁盘。

  2. sys_open函数

    • 该函数用于打开文件或设备。

    • S_ISCHR(inode->i_mode):检查文件是否为字符设备文件。

    • MAJOR(inode->i_zone[0])MINOR(inode->i_zone[0]):从设备号中提取主设备号和次设备号。

    • current->tty:设置当前进程的tty(终端设备)。

  3. 宏定义

    • MAJOR(a):从设备号中提取主设备号(高字节)。

    • MINOR(a):从设备号中提取次设备号(低字节)。

总结:

这段代码展示了如何使用 m_inode 结构体来表示设备文件的inode,并演示了如何通过 sys_open 函数打开设备文件。

文件视图的两条路

读写磁盘的过程:

  1. 用户发起文件操作

    • 用户通过 writeread 系统调用操作文件。

    • 传递参数:文件描述符(FD)、内存缓冲区(buff)、操作字节数(count)。

  2. 系统调用转换

    • write 调用内核中的 sys_writeread 调用 sys_read

    • 需要获取文件的 inode(索引节点)信息。

  3. 计算目标磁盘块

    • 通过文件结构 file 获取文件指针 f_pos,确定字符流中的位置。

    • 根据 inode 及索引结构,计算出对应的磁盘块号(block)。

    • 采用直接索引、一级索引、二级索引等方式解析出数据块。

  4. 磁盘操作

    • 读操作:调用 bread() 读取磁盘数据到缓存,再复制到用户缓冲区。

    • 写操作:调用 bwrite() 将用户缓冲区数据写入磁盘缓存,并同步到磁盘。

  5. 磁盘调度与完成

    • 磁盘请求加入电梯调度队列。

    • 设备驱动完成磁盘读写,系统中断通知完成。

    • 若是写操作,需更新 f_pos 以维护文件流的连续性。


输出到显示器的过程:

  1. 用户发起输出请求

    • 例如 printf()write(1, buff, count),其中 1 代表标准输出(stdout)。

  2. 系统调用转换

    • 进入内核,调用 sys_write 处理。

    • 识别文件描述符 1 代表标准输出(显示器)。

  3. 数据传输到终端设备

    • 由于 stdout 不是磁盘文件,而是字符设备,直接走 tty 终端驱动。

    • 终端驱动 tty_write 处理输出,将数据放入 tty 缓冲区。

  4. 驱动程序与硬件交互

    • tty 设备驱动调用 console_driver,执行 putc() 逐字符输出到屏幕。

    • 可能涉及 VGAframebufferUART 串口等设备。

  5. 显示完成

    • 终端驱动完成数据输出,可能触发屏幕刷新或回显。

    • 若是行缓冲模式,遇到 \n 或缓冲区满才实际输出。

对比总结

  • 读写磁盘涉及块设备(block device),数据按块存取,可能需要磁盘调度与缓冲管理。

  • 输出到显示器涉及字符设备(char device),数据按字符流传输,即时性更强。

  • 两者都通过系统调用进入内核,但处理方式不同,磁盘用 bread/bwrite,显示器用 tty_write

http://www.dtcms.com/a/113026.html

相关文章:

  • 【奇点时刻】GPT4o新图像生成模型底层原理深度洞察报告(篇2)
  • 数据结构(JAVA)栈
  • Nacos 服务发现的核心模型有哪些?Service, Instance, Cluster 之间的关系是什么?
  • 基于Transformer框架实现微调后Qwen/DeepSeek模型的流式推理并封装成接口
  • 获取inode的完整路径包含挂载的路径
  • 蓝桥杯 完全平方数 刷题笔记
  • 优化 Web 性能:管理第三方资源(Third-Party Summary)
  • 数字内容体验A/B测试优化实战
  • 本地命令行启动服务并连接MySQL8
  • NLP/大模型八股专栏结构解析
  • [特殊字符] Pandas 常用操作对比:Python 运算符 vs Pandas 函数
  • JuiceFS设计框架剖析
  • 【C/C++】编译与链接过程详解
  • 最小生成树理论
  • ROM/FLASH/RAM
  • LeetCode刷题常见的Java排序
  • 安卓开发工程师-布局设计
  • 【深度学习】嘿马深度学习目标检测教程第1篇:商品目标检测要求、目标,1.1 项目演示【附代码文档】
  • 前端判断值相等的方法和区别
  • I.MX6ULL 交叉编译环境配置与使用
  • 纯免费的零基础建站教程
  • Android使用OpenGL和MediaCodec录制
  • JDK8卸载与安装教程(超详细)
  • 122.买卖股票的最佳时机 II
  • Day2:前端项目uniapp壁纸实战
  • #SVA语法滴水穿石# (013)关于内建系统函数
  • Git三剑客:工作区、暂存区、版本库深度解析
  • 王者荣耀的游戏匹配机制
  • 《UNIX网络编程卷1:套接字联网API》第6章 IO复用:select和poll函数
  • 《算法笔记》9.8小节——图算法专题->哈夫曼树 问题 C: 哈夫曼树