关于内存映射的思考

文章讨论了内存映射在文件读取中的作用,如何通过内存映射提高读取效率,以及在删除文件块时,为何需要先读取再写回文件系统以保持数据一致性。内存映射技术将文件内容映射到进程地址空间,减少磁盘I/O,而删除块时需要更新前一个节点的指针并写回,确保文件系统的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 先看一段读取文件的函数

int MMapFileOperation::pread_file(char *buf, int32_t size, const int64_t offset)
{
    // 情况1,内存已经映射
    // 此时一次读不完
    if (is_mapped_ && ((offset + size) > map_file_->get_size()))
    {
        if (debug)
        {
            ////////////////出错,原因:__PRI64_PREFIX写错////////////////////////
            fprintf(stdout, "MMapFileOperation::pread,size=%d,offset=%"__PRI64_PREFIX
                            "d,map file size:%d.need remap\n",
                    size, offset, map_file_->get_size());
        }
        map_file_->remap_file(); // 扩容
    }
    if (is_mapped_ && (offset + size) <= map_file_->get_size())
    {
        memcpy(buf, (char *)map_file_->get_data() + offset, size);
        return TFS_SUCCESS;
    }

    // 情况2,内存没有映射or要读取的数据映射不全
    return FileOperation::pread_file(buf, size, offset);
}

这段代码是一个文件读取函数的实现,通过判断文件是否被内存映射来决定使用内存映射方式还是普通文件读取方式来读取文件数据。具体来说,函数中的逻辑如下:

  1. 如果文件已经被内存映射,并且读取数据的范围超出了当前映射的文件大小,那么需要重新映射文件以扩容。此时会调用 map_file_->remap_file() 函数进行重新映射。
  2. 如果文件已经被内存映射,并且读取数据的范围在当前映射的文件大小范围内,那么直接从内存中复制数据到缓冲区中,并返回读取成功的结果。
  3. 如果文件没有被内存映射或者要读取的数据范围超出了当前映射的文件大小,那么使用普通的文件读取方式来读取文件数据。

这段代码的作用是尽可能地利用内存映射的方式来提高文件读取的效率,因为内存映射可以将文件映射到内存中,从而避免了频繁的磁盘 IO 操作,提高了文件读取的速度。同时,如果要读取的数据范围超出了当前映射的文件大小,需要扩容再重新映射,以保证能够读取到所有的数据。

2. 具体讲讲什么内存映射?从哪映射到哪?

内存映射是一种将文件映射到进程地址空间中的技术,它允许进程像访问内存一样来读写文件,从而避免了频繁的磁盘 I/O 操作,提高了文件读写的效率

具体来说,内存映射是通过操作系统提供的系统调用(如 mmap())来实现的。当一个文件被内存映射后,操作系统会将文件中的内容映射到进程的虚拟地址空间中的一块区域,这块区域就称为文件的内存映射区。进程可以像访问内存一样来访问这个区域,读写操作都会直接在内存中进行,而不需要进行磁盘 I/O 操作。

内存映射通常分为两种方式:匿名内存映射和文件内存映射。其中,文件内存映射就是将一个文件映射到内存中,这也是本题中所涉及的情况。具体来说,内存映射的过程如下:

  1. 打开文件:首先需要打开文件,以便后续映射操作能够访问到文件的内容。
  2. 创建内存映射区:使用系统调用 mmap() 创建一个内存映射区,将文件的内容映射到该区域中。在创建映射区时需要指定映射的起始地址、映射的大小、映射区的访问权限等参数。
  3. 访问映射区:将映射区视为内存来访问,读写操作都会直接在内存中进行。
  4. 解除映射区:在不再需要访问映射区时,需要使用 munmap() 系统调用将映射区解除映射,释放内存。

在本题中,文件被映射到了 map_file_->get_data() 中,该函数返回的指针就是文件内存映射区的起始地址。在函数中,如果文件已经被内存映射,并且要读取的数据范围在当前映射的文件大小范围内,就可以直接从内存中复制数据到缓冲区中,而不需要进行磁盘 I/O 操作,从而提高了文件读取的效率

3.磁盘 I/O 操作是指读写文件吗?(比如:在test.txt中读写)

是的,磁盘 I/O 操作指的就是对磁盘上的文件进行读写操作。在计算机中,磁盘是一种常用的存储介质,用于存储大量的数据和文件。对于进程来说,如果需要读写文件,就需要通过磁盘 I/O 操作来进行文件的读写

在文件读写过程中,通常会涉及到两种类型的 I/O 操作:磁盘 I/O 和内存 I/O磁盘 I/O 操作指的是读写磁盘上的文件,而内存 I/O 操作指的是读写内存中的数据。对于大型文件或者频繁的文件读写操作,磁盘 I/O 操作的效率往往会比内存 I/O 操作低,因为磁盘需要进行旋转和寻道等操作,而这些操作会增加访问文件的时间。

在本题中,如果文件已经被内存映射,并且要读取的数据范围在当前映射的文件大小范围内,就可以直接从内存中复制数据到缓冲区中,而不需要进行磁盘 I/O 操作。这样可以避免频繁的磁盘 I/O 操作,提高文件读取的效率。

4.再来看一段删除块的函数

// 根据文件id删除meta_info
int IndexHandle::delete_segment_meta(const uint64_t key)
{
    int32_t current_offset = 0, previous_offset = 0;

    // 人家不在,你删个毛线
    int32_t ret = hash_find(key, current_offset, previous_offset);
    if (ret != TFS_SUCCESS)
    {
        return ret;
    }

    // 它的上一个节点指向它的下一个节点,就把他删掉了
    // 上一个节点已经知道了previous_offset,现在我们要找下一个节点
    MetaInfo meta_info;
    // 读取当前节点
    ret = file_op_->pread_file(reinterpret_cast<char *>(&meta_info),
                               sizeof(MetaInfo),
                               current_offset);
    if (TFS_SUCCESS != ret)
    {
        return ret;
    }

    // 通过meta_info找到当前节点的下一个节点
    int32_t next_pos = meta_info.get_next_meta_offset();

    if (previous_offset == 0) // 前面没有节点了.它就是首节点,我们要删除首节点
    {
        int32_t slot = static_cast<uint32_t>(key) % bucket_size();
        bucket_slot()[slot] = next_pos; // 找到槽,让槽指向它的下一个节点
    }
    else
    {
        MetaInfo pre_meta_info;
        // 读取当前节点的上一个节点,pre_meta_info是传入传出参数
        ret = file_op_->pread_file(reinterpret_cast<char *>(&pre_meta_info),
                                   sizeof(MetaInfo),
                                   previous_offset);
        if (TFS_SUCCESS != ret)
        {
            return ret;
        }
        // 上一个节点链上下一个节点
        pre_meta_info.set_next_meta_offset(next_pos);
        // 在更新前一个节点的指针之前,它是存储在文件系统中的。
        // 当需要删除当前节点时,需要先读取当前节点和前一个节点的数据块,
        // 然后将前一个节点的指针更新,最后将更新后的前一个节点数据块重新写回文件系统中。
        // 在这个"过程"中,前一个节点的数据块被"读取到内存"中,并在"内存中进行了修改",
        // 但是"修改后的数据并没有被写回到文件系统中"。
        // 也就是说,你读出来指的是将其从文件系统读到内存中,你的操作是在内存中的进行的,
        // 你的文件系统没有改变,所以你还要"将你在内存中的操作后的结果"重新写入到文件系统中!
        ret = file_op_->pwrite_file(reinterpret_cast<char *>(&pre_meta_info),
                                    sizeof(MetaInfo),
                                    previous_offset);
        if (TFS_SUCCESS != ret) // 写失败,说明删除失败
        {
            return ret;
        }
    }

    // 删除并非真的删除,只是标记了该节点不可用了,然后再将该节点找个机会重用(当然,如果可重用节点过多,多到一定比例时,淘宝分布式文件系统会在人流量少时,将这些可重用节点进行删除[不过该功能,我并没实现])
    //把删除节点加入可重用节点链表
    meta_info.set_next_meta_offset(free_head_offset()); // index_header()->free_head_offset_;
    ret = file_op_->pwrite_file(reinterpret_cast<char *>(&meta_info), sizeof(MetaInfo), current_offset);
    if (TFS_SUCCESS != ret)
    {
        return ret;
    }

    index_header()->free_head_offset = current_offset;
    if (debug)
        printf("delete_segment_meta - reuse metainfo, current_offset: %d\n", current_offset);

    update_block_info(C_OPER_DELETE, meta_info.get_size());
    

    return TFS_SUCCESS;
}

上面1,2,3解释了,为什么删除该节点后,还需要再写回去?
原因:你先将它从文件中读出来(通过mmap内存映射),再进行的操作是在内存中的操作,文件根本不知道你操作了什么,所以你在删除掉该节点后,还需要再写回上一个节点的相关信息

// 上一个节点链上下一个节点
pre_meta_info.set_next_meta_offset(next_pos);
// 在更新前一个节点的指针之前,它是存储在文件系统中的。
// 当需要删除当前节点时,需要先读取当前节点和前一个节点的数据块,
// 然后将前一个节点的指针更新,最后将更新后的前一个节点数据块重新写回文件系统中。
// 在这个"过程"中,前一个节点的数据块被"读取到内存"中,并在"内存中进行了修改",
// 但是"修改后的数据并没有被写回到文件系统中"。
// 也就是说,你读出来指的是将其从文件系统读到内存中,你的操作是在内存中的进行的,
// 你的文件系统没有改变,所以你还要"将你在内存中的操作后的结果"重新写入到文件系统中!
ret = file_op_->pwrite_file(reinterpret_cast<char *>(&pre_meta_info),
                            sizeof(MetaInfo),
                            previous_offset);
if (TFS_SUCCESS != ret) // 写失败,说明删除失败
{
    return ret;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

踏过山河,踏过海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值