前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >读取一个文件的时候,操作系统发生了什么

读取一个文件的时候,操作系统发生了什么

作者头像
theanarkh
发布2023-10-30 15:51:14
2080
发布2023-10-30 15:51:14
举报
文章被收录于专栏:原创分享

今天分享一下读取文件的过程。linux万物皆文件,任意文件的操作,都是通过统一的函数开始,所以我们就从read函数,分析针对一般文件的读取过程。

代码语言:javascript
复制
int sys_read(unsigned int fd,char * buf,int count){
    struct file * file;
    struct m_inode * inode;
    // 通过fd拿到file和inode结构体
    if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
        return -EINVAL;
    inode = file->f_inode;
    ...
    /*
        f_pos表示当前的读取指针,i_size表示整个文件大小
        下面代码判断读的长度是否大于剩下的可读长度,是的话只取剩下的部分
    */
    if (count+file->f_pos > inode->i_size)
        count = inode->i_size - file->f_pos;
    // 到底了
    if (count<=0)
        return 0;
    return file_read(inode,file,buf,count);
}

下面是进程结构体和文件系统结构体的关系。

在这里插入图片描述 file_read函数是对一般文件进行读取的函数。

代码语言:javascript
复制
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    int left,chars,nr;
    struct buffer_head * bh;

    if ((left=count)<=0)
        return 0;
    while (left) {
        // bmap取得该文件偏移对应的硬盘块号,然后读进来
        if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
            if (!(bh=bread(inode->i_dev,nr)))
                break;
        } else
            bh = NULL;
        // 偏移
        nr = filp->f_pos % BLOCK_SIZE;
        // 读进来的数据中,可读的长度和还需要读的长度,取小的,如果还没读完继续把块从硬盘读进来
        chars = MIN( BLOCK_SIZE-nr , left );
        filp->f_pos += chars; // 更新偏移指针
        left -= chars; // 更新还需药读取的长度
        if (bh) {
            char * p = nr + bh->b_data;
            while (chars-->0)
                put_fs_byte(*(p++),buf++); //复制到buf里 
            brelse(bh);
        } else {
            // 没有数据则复制0
            while (chars-->0)
                put_fs_byte(0,buf++);
        }
    }
    // 更新访问时间
    inode->i_atime = CURRENT_TIME;
    // 返回读取的长度,如果一个都没读则返回错误
    return (count-left)?(count-left):-ERROR;
}

上面的函数代码看起来很多,但是逻辑其实比较清晰。他主要是根据当前的读指针位置,算出对应文件内容所在的硬盘块,接着把文件在硬盘中的数据块读进来内存,然后复制到用户空间。所以现在的问题有两个。 1 根据读指针计算文件内容在硬盘的位置。我们知道一个文件对应一个inode。inode里记录了文件内容的一些信息。如图。

在这里插入图片描述 我们看到inode里记录了文件每个数据块的逻辑块号在硬盘中对应的块号。所以我们根据读指针和硬盘逻辑块的大小算出逻辑块号。然后根据逻辑块号从inode的映射表中找到对应的硬盘块号。 2 根据硬盘块号,把数据读取出来。读取函数是bread(block read)。

代码语言:javascript
复制
struct buffer_head * bread(int dev,int block)
{
    struct buffer_head * bh;
    // 先从buffer链表中获取一个buffer
    if (!(bh=getblk(dev,block)))
        panic("bread: getblk returned NULL\n");
    // 之前已经读取过并且有效,则直接返回
    if (bh->b_uptodate)
        return bh;
    // 返回读取硬盘的数据
    ll_rw_block(READ,bh);
    //ll_rw_block会锁住bh,所以会先阻塞在这然后等待唤醒 
    wait_on_buffer(bh);
    // 底层读取数据成功后会更新该字段为1,否则就是读取出错了
    if (bh->b_uptodate)
        return bh;
    brelse(bh);
    return NULL;
}

我们分三部分分析bread函数。 1 根据设备号和块号从buffer链表中获取缓存的数据,操作系统在硬盘上面实现了一层缓存系统。对于文件的读写进行了缓存处理。比如我们读取了一个文件的某一部分内容,如果下次继续读取这部分内容,则不需要再从硬盘读取,直接从缓存中读取就行。这样就提高了读取的速度,因为我们知道硬盘的读取是非常慢的操作。当然操作系统会对数据的有效性进行维护(b_uptodate字段等于1说明有效)。 2 如果缓存失效,则调用ll_rw_block函数进行硬盘读取。 3 因为硬盘读取非常慢,所以这时候进程会阻塞。通过wait_on_buffer函数实现进程的阻塞。等到进程被唤醒的时候再次通过b_uptodate字段判断是否读取成功。b_uptodate字段会在数据读取成功的时候设置为1.

代码语言:javascript
复制
static inline void wait_on_buffer(struct buffer_head * bh)
{
    cli();
    while (bh->b_lock)
        sleep_on(&bh->b_wait);
    sti();
}

我们继续分析ll_rw_block函数,看看操作系统是如何对硬盘的数据进行读取的。

代码语言:javascript
复制
void ll_rw_block(int rw, struct buffer_head * bh)
{
    unsigned int major;

    if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||
    !(blk_dev[major].request_fn)) {
        printk("Trying to read nonexistent block-device\n\r");
        return;
    }
    // 新建一个读写硬盘数据的请求
    make_request(major,rw,bh);
}

ll_rw_block函数的逻辑非常简单,直接调用make_request。分析这个函数之前我们先了解一下struct request结构体和一些硬盘读取的内容。硬盘对应上层的读写操作,维护了一个结构体struct blk_dev_struct。

在这里插入图片描述 该结构体记录了请求硬盘操作的任务队列和处理函数。struct request结构体则记录了请求硬盘任务的一些上下文。比如操作的类型(读或写),读取的扇区、扇区数、保存读写数据的指针。接下来我们继续分析make_request函数。

代码语言:javascript
复制
static void make_request(int major,int rw, struct buffer_head * bh)
{
    struct request * req;
    int rw_ahead;
    ...
    // 请求队列1/3用于读,2/3用于写
repeat:
    if (rw == READ)
        req = request+NR_REQUEST;
    else
        req = request+((NR_REQUEST*2)/3);
    /* find an empty request */
    while (--req >= request)
        // 小于0说明该结构没有被使用
        if (req->dev<0)
            break;
    // 没有找到可用的请求结构
    if (req < request) {
        // 预读写则直接返回
        if (rw_ahead) {
            unlock_buffer(bh);
            return;
        }
        // 阻塞等待可用的请求结构
        sleep_on(&wait_for_request);
        // 被唤醒后重新查找
        goto repeat;
    }

    req->dev = bh->b_dev;
    req->cmd = rw;
    req->errors=0;
    req->sector = bh->b_blocknr<<1; // 一块等于两个扇区所以乘以2,即左移1位,比如要读地10块,则读取第二十个扇区
    req->nr_sectors = 2;// 一块等于两个扇区,即读取的扇区是2
    req->buffer = bh->b_data;
    req->waiting = NULL;
    req->bh = bh;
    req->next = NULL;
    // 插入请求队列
    add_request(major+blk_dev,req);
}

该函数就是生成一个struct request节点插入到请求硬盘操作的队列中。继续看add_request

代码语言:javascript
复制
static void add_request(struct blk_dev_struct * dev, struct request * req)
{
    struct request * tmp;

    req->next = NULL;
    cli();
    if (req->bh)
        req->bh->b_dirt = 0;
    // 当前没有请求项,插入队列,开始处理请求
    if (!(tmp = dev->current_request)) {
        dev->current_request = req;
        sti();
        (dev->request_fn)();
        return;
    }
    // 如果已经在处理队列中的请求,那么使用电梯算法插入相应的位置,等待处理。
    for ( ; tmp->next ; tmp=tmp->next)
        if ((IN_ORDER(tmp,req) ||
            !IN_ORDER(tmp,tmp->next)) &&
            IN_ORDER(req,tmp->next))
            break;
    req->next=tmp->next;
    tmp->next=req;
    sti();
}

不管是第一个任务节点还是后续的任务节点。都由request_fn对应的函数逐个进行处理。硬盘操作对应的处理函数是do_hd_request。do_hd_request函数根据request结构体中的上下文,对硬盘控制器发送操作命令,比如需要读取的操作类型、读取的扇区等。并且设置回调函数read_intr(因为我们分析的是读取操作)。这时候进程就阻塞了。等到硬盘控制器从硬盘中读取数据成功后,会触发中断。在中断处理函数中会执行刚才我们设置的回调read_intr。read_intr函数从硬盘控制器的数据寄存器中把数据读取进来。如果还没读取完毕,则继续等待后续硬盘中断。如果全部读取成功则唤醒进程。

代码语言:javascript
复制
    // 读写数据成功,数据有效位置1
    CURRENT->bh->b_uptodate = uptodate;
    unlock_buffer(CURRENT->bh);

看一下unlock_buffer做了什么。

代码语言:javascript
复制
inline void unlock_buffer(struct buffer_head * bh)
{
    if (!bh->b_lock)
        printk(DEVICE_NAME ": free buffer being unlocked\n");
    bh->b_lock=0;
    // 唤醒等待的进程
    wake_up(&bh->b_wait);
}

至此,文件的读取整个过程就分析完了。最后顺便说一下文件写入的过程,其实和读取的过程很类似。如果是修改文件之前的内容,则先把这块内容读取到内存,然后修改内存的数据,最后回写硬盘。如果是追加性写入,则先在硬盘申请一个新的数据块,并且修改位图、inode信息。然后把新块读取到内存,接着修改内存数据,最后回写到硬盘。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档