Linux 文件 io 的原子性与 O_APPEND 参数

news2024/11/27 0:33:26

转自:https://www.cnblogs.com/moonwalk/p/15642478.html

1. 文件 io

1.1 open() 系统调用

  1. 在进程/线程 struct task ---> struct files_struct 结构体中,添加一项新打开的文件描述符 fd,并指向文件表
  2. 创建一个新的 struct file 即文件表,里面存储了文件偏移量、指向 inode 的指针、open() 和 fcntl() 设置的文件状态标志等信息
  3. inode 即文件索引节点里面存储了实际文件的大小、文件数据 block(cluster) 位置、权限等信息。如果是新建一个文件,那么会从磁盘中载入一个新的 inode 并初始化

1.2 两个独立进程各自打开同一份文件

文件描述符和文件表项是独立的,但是指向同一个 inode 文件索引结构

1.3 父子进程

子进程会继承父进程的文件描述符,即子进程会复制父进程的文件描述符表,这样父子进程会指向相同的文件表

2. 写文件实验

针对如下常见问题:

  1. 多线程共享同一个文件描述符
  2. 父子进程指向同一个文件表
  3. 多进程/多线程打开同一份文件

在这 3 种情况下 write() 数据到文件中,是否会发生数据覆盖呢?下面几段代码简单验证下

2.1 多线程共享同一个文件描述符

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>

void* task01(void* fd) {
  int _fd = *((int*)fd);
  char buf[100];
  memset(buf, '+', 100);
  write(_fd, buf, 100);
  return NULL;
}

void* task02(void* fd) {
  int _fd = *((int*)fd);
  char buf[100];
  memset(buf, '-', 100);
  write(_fd, buf, 100);
  return NULL;
}

int main() {
  int fd = open("tmpfile", O_RDWR);
  if (!fd) {
    return 0;
  }
  pthread_t thread01;
  pthread_t thread02;
  int ret = pthread_create(&thread01, NULL, task01, &fd);
  ret = pthread_create(&thread02, NULL, task02, &fd);
  pthread_join(thread01, NULL);
  pthread_join(thread02, NULL);
  close(fd);

  return 0;
}

多次实验表明,多线程共享同一个文件描述符号不会发生写入覆盖,且文件长度符合预期

2.2 父子进程指向同一个文件表

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <wait.h>

int main() {
  int fd = open("tmpfile", O_RDWR);
  if (!fd) {
    return 0;
  }

  pid_t pid = fork();
  if (pid == 0) {
    // child process
    char buf[100];
    memset(buf, '+', 100);
    write(fd, buf, 100);
  }
  if (pid > 0) {
    // parent process
    char buf[100];
    memset(buf, '-', 100);
    write(fd, buf, 100);
    wait(NULL);
  }

  return 0;
}

多次实验表明,父子进程指向同一个文件表不会发生写入覆盖,且文件长度符合预期

2.3 多进程/多线程打开同一份文件

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>

void* task01(void* arg) {
  int fd = open("tmpfile", O_RDWR);
  if (!fd) {
    return NULL;
  }
  char buf[100];
  memset(buf, '+', 100);
  write(fd, buf, 100);
  close(fd);
  return NULL;
}

void* task02(void* arg) {
  int fd = open("tmpfile", O_RDWR);
  if (!fd) {
    return NULL;
  }
  char buf[100];
  memset(buf, '-', 100);
  write(fd, buf, 100);
  close(fd);
  return NULL;
}

int main() {
  pthread_t thread01;
  pthread_t thread02;
  int ret = pthread_create(&thread01, NULL, task01, NULL);
  ret = pthread_create(&thread02, NULL, task02, NULL);
  pthread_join(thread01, NULL);
  pthread_join(thread02, NULL);

  return 0;
}

多次实验表明,多进程/多线程打开同一份文件,不会发生内容交错,但是会发生内容覆盖,实际内容来自其中一个线程

3. 写文件实验结果分析

3.1 write() 三部曲

  1. 从文件表中获取偏移量
  2. 从偏移量处开始写,更新文件长度
  3. 更新文件表偏移量

3.2 不发生数据覆盖或交叉实验分析

对于 多线程共享同一个文件描述符父子进程指向同一个文件表 的情况,这3个步骤似乎不会被打断(没有出现数据覆盖或交叉的情况)
之所以会这样,原因在于文件表有一个读写锁,查看内核 write() 调用源码:

---> /fs/read_write.c::ksys_write()

ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos, *ppos = file_ppos(f.file);
        if (ppos) {
            pos = *ppos;
            ppos = &pos;
        }
        ret = vfs_write(f.file, buf, count, ppos);
        if (ret >= 0 && ppos)
            f.file->f_pos = pos;
        fdput_pos(f);
    }

    return ret;
}

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    return ksys_write(fd, buf, count);
}

---> /include/linux/file.h::fdget_pos()
---> fs/file.c::__fdget_pos()

unsigned long __fdget_pos(unsigned int fd)
{
    unsigned long v = __fdget(fd);
    struct file *file = (struct file *)(v & ~3);

    if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
        if (file_count(file) > 1) {
            v |= FDPUT_POS_UNLOCK;
            mutex_lock(&file->f_pos_lock);
        }
    }
    return v;
}

mutex_lock(&file->f_pos_lock) 会锁住文件表的 f_pos_lock 锁,其它线程试图调用 write() 写入数据时,只要 fd 指向的是同一个文件表,那么就必须等待锁释放

3.3 发生数据覆盖实验分析

对于 多进程/多线程打开同一份文件 的情况,这3个步骤中,步骤1和2之间可能被打断,步骤2和3之间也可能被打断,但是步骤2单独不会被打断(没有出现数据交叉的情况)
之所以会这样,原因在于 inode 索引节点也有一个读写锁,查看内核 write() 调用源码:

---> fs/read_write.c::ksys_write()
---> fs/vfs_write()::vfs_write()
---> fs/ext4/file.c::ext4_file_write_iter()
---> fd/ext4/file.c::ext4_buffered_write_iter()

static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
                    struct iov_iter *from)
{
    ssize_t ret;
    struct inode *inode = file_inode(iocb->ki_filp);

    if (iocb->ki_flags & IOCB_NOWAIT)
        return -EOPNOTSUPP;

    ext4_fc_start_update(inode);
    inode_lock(inode);
    ret = ext4_write_checks(iocb, from);
    if (ret <= 0)
        goto out;

    current->backing_dev_info = inode_to_bdi(inode);
    ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos);
    current->backing_dev_info = NULL;

out:
    inode_unlock(inode);
    ext4_fc_stop_update(inode);
    if (likely(ret > 0)) {
        iocb->ki_pos += ret;
        ret = generic_write_sync(iocb, ret);
    }

    return ret;
}

inode_lock(inode) 会锁住 inode,其它线程试图调用 write() 写入数据时,只要 fd 指向的是同一个 inode,那么就必须等待锁释放
但是因为不同进程/线程从文件表获取偏移量是独立并行执行的,向同一个偏移量开始写入数据,所以写入会发生覆盖

4. O_APPEND 参数是如何发挥作用的

在 open() 的时候,加上 O_APPEND 就能解决 3.3 数据覆盖的问题。原因是,在第二步锁住 inode 后,强制更新偏移量为当前文件的实际大小,第一步获取的偏移量将无效:

---> fs/read_write.c::ksys_write()
---> fs/vfs_write()::vfs_write()
---> fs/ext4/file.c::ext4_file_write_iter()
---> fs/ext4/file.c::ext4_buffered_write_iter()
---> fs/read_write.c::generic_write_checks()

ssize_t generic_write_checks(struct kiocb *iocb, struct iov_iter *from)
{
    struct file *file = iocb->ki_filp;
    struct inode *inode = file->f_mapping->host;
    loff_t count;
    int ret;

    if (IS_SWAPFILE(inode))
        return -ETXTBSY;

    if (!iov_iter_count(from))
        return 0;

    /* FIXME: this is for backwards compatibility with 2.4 */
    if (iocb->ki_flags & IOCB_APPEND)
        iocb->ki_pos = i_size_read(inode);

    if ((iocb->ki_flags & IOCB_NOWAIT) && !(iocb->ki_flags & IOCB_DIRECT))
        return -EINVAL;

    count = iov_iter_count(from);
    ret = generic_write_check_limits(file, iocb->ki_pos, &count);
    if (ret)
        return ret;

    iov_iter_truncate(from, count);
    return iov_iter_count(from);
}
EXPORT_SYMBOL(generic_write_checks);

O_APPEND 会对应 IOCB_APPEND,如果设置了,则更新 iocb->ki_pos,即会覆盖之前获取的偏移量,使用文件实际的大小。注意,这一步是在锁住 inode 节点后做的操作

5. 总结

文件 io 的原子性,是用锁来保证的,文件表有一把,保护偏移量,inode 有一把,保护读写数据。实际上,只有加上了 O_APPEND 参数,才能对同一个文件的写入操作实现真正的原子性
这里没有讨论 read(),但是类比 write(),其行为也是一样的,只是写数据变为读数据

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/647220.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Debezium系列之:为Debezium集群JMX页面增加监控,JMX页面出现异常时发送飞书告警,确保任务能够获取debezium集群指标

Debezium系列之:为Debezium集群JMX页面增加监控,JMX页面出现异常时发送飞书告警,确保任务能够获取debezium集群指标 一、需求背景二、相关技术博客三、监控JMX页面状态四、发送飞书告警五、定时执行脚本六、告警效果展示七、总结和延展一、需求背景 下游任务需要使用Debeziu…

【正项级数】敛散性判别

Hi&#xff01;&#x1f60a;&#x1f970;大家好呀&#xff01;欢迎阅读本篇文章正项级数敛散性判别。由于最近时间比较紧张&#xff0c;所以几乎没有使用公式编辑器&#xff0c;更多的内容多以图片形式呈现&#xff0c;希望本篇内容对你有帮助呀&#xff01; 可能对你有帮助的…

JavaSE进阶--网络编程

文章目录 前言一、网络编程二、通信1、两个重要的要素2、通信协议 三 、Socket四、基于TCP的网络编程1、单向通信1.1 服务端1.2 客户端 2、双向通信2.1 服务端2.2 客户端 3、传输对象3.1 服务端3.2 客户端 4、保持通信4.1 服务端4.2 客户端 五、基于UDP的网络编程1、单向通信1.…

深入理解深度学习——Transformer:解码器(Decoder)的多头注意力层(Multi-headAttention)

分类目录&#xff1a;《深入理解深度学习》总目录 相关文章&#xff1a; 注意力机制&#xff08;Attention Mechanism&#xff09;&#xff1a;基础知识 注意力机制&#xff08;Attention Mechanism&#xff09;&#xff1a;注意力汇聚与Nadaraya-Watson核回归 注意力机制&…

AI 绘画(1):生成一个图片的标准流程

文章目录 文章回顾感谢人员生成一个图片的标准流程前期准备&#xff0c;以文生图为例去C站下载你需要的绘画模型导入参数导入生成结果&#xff1f;可能是BUG事后处理 图生图如何高度贴合原图火柴人转角色 涂鸦局部重绘 Ai绘画公约 文章回顾 AI 绘画&#xff08;0&#xff09;&…

Fluent基于profile定义变量

1 概述 Profile中文可称呼为数据表&#xff0c;是Fluent中一种定义边界条件和体积域条件的方式。数据表主要用于将实验、第三方软件等其他数据源的物理场分布数据传递给Fluent。 Profile文件为CSV或PROF格式的文本文件&#xff0c;记录了物理场分布规律。 profile文件示意&…

智警杯初赛复现

eee考核的时候搭建环境出了问题。。虽然有点久远&#xff0c;但还能看看 1.克隆centos 先查看第一台的ip ifconfig 编辑另外两台 进入根目录 cd/ 编辑 vim /etc/sysconfig/network-scripts/ifcfg-ens33 更改项 IPADDR192.168.181.4 # 设置为想要的固定IP地址重启 2.…

K8S 基本概念

功能 1. 自动装箱 基于容器对应用运行环境的资源配置要求自动部署应用容器 2. 自我修复(自愈能力) 当容器失败时&#xff0c;会对容器进行重启。 当所部署的 Node 节点有问题时&#xff0c;会对容器进行重新部署和重新调度 当容器未通过监控检查时&#xff0c;会关闭此容器直到…

从零开始理解Linux中断架构(2)-朴素的中断管理设计理念

既然是从零开始,我们先从最为简单的中断逻辑处理架构开始,这个逻辑结构跟CPU架构没有关系,纯逻辑上的。纯逻辑是跨越系统和应用的,不管对于应用程序员还是系统程序员,逻辑推导是基本的工具,设计原型是基本的出发点。 中断发起的时候,PC指针被设置为中断向量表中相对应的…

轻量云服务器远程连接不了怎么办?

​  轻量云服务器为轻量级云计算服务&#xff0c;其可满足低需求、轻体验的个人和企业用户。但是&#xff0c;有时候我们会遇到轻量云服务器远程连接不了的问题&#xff0c;这对于需要远程管理服务器的用户来说是非常困扰的。本文展示了轻量云服务器无法正常远程连接的一些排…

测试老鸟总结,性能测试监控的关键指标(详全)你要的都有...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 稳定性测试的要点…

测试外包公司的妹子每天干15小时,被开发怼了几句,直接提桶跑路了。。。

最近我们公司的测试任务比较重&#xff0c;特别是我们组&#xff0c;每天都要加班两三个小时。我们组还有一个来公司才两三个月的妹子&#xff0c;工作挺认真的&#xff0c;每天加班两三个小时也没有抱怨什么&#xff0c;前几天看她每天太累了&#xff0c;要她放松一下别加那么…

编程示例: 计算CRC校验码

编程示例&#xff1a; 计算CRC校验码 循环冗余检查&#xff08;CRC&#xff09;是一种数据传输检错功能&#xff0c;对数据进行 多项式计算&#xff0c;并将得到的结果附在帧的后面&#xff0c;接收设备也执行 类似的算法&#xff0c;进而可以保证在软件层次上数据传输的正确性…

[golang 微服务] 6. GRPC微服务集群+Consul集群+grpc-consul-resolver案例演示

一. GRPC微服务集群概念 上一节讲解了consul集群: [golang 微服务] 5. 微服务服务发现介绍,安装以及consul的使用,Consul集群,这样的话,当一台server挂掉之后,集群就会从另一台server中获取服务,这就保证了客户端访问consul集群的负载均衡性. 这里还有一个问题: 就是当终端的对…

opencv初学记录

准备工作&#xff1a; 1.找一张图片 2.准备python运行环境&#xff0c;并导入库&#xff0c;pip install opencv-python 读取文件&#xff0c;并打印维度 import cv2 #为什么是cv2不是cv呢&#xff0c;这个2指的是c的api&#xff0c;是为了兼容老板&#xff0c;cv指的就是c&am…

设计模式(十七):行为型之状态模式

设计模式系列文章 设计模式(一)&#xff1a;创建型之单例模式 设计模式(二、三)&#xff1a;创建型之工厂方法和抽象工厂模式 设计模式(四)&#xff1a;创建型之原型模式 设计模式(五)&#xff1a;创建型之建造者模式 设计模式(六)&#xff1a;结构型之代理模式 设计模式…

强化学习笔记-12 Eligibility Traces

前篇讨论了TD算法将MC同Bootstrap相结合&#xff0c;拥有很好的特性。本节所介绍的Eligibility Traces&#xff0c;其思想是多个TD(n)所计算预估累积收益按权重进行加权平均&#xff0c;从而得到更好的累积收益预估值。 价值预估模型的参数更新式子可以调整为&#xff1a; 1. O…

Vue CLI $nextTick 过渡与动画

3.12.$nextTick 这是一个生命周期钩子 **this.$nextTick(回调函数)**在下一次DOM更新结束后执行其指定的回调 什么时候用&#xff1a;当改变数据后&#xff0c;要基于更新后的新DOM进行某些操作时&#xff0c;要在nextTick所指定的回调函数中执行 使用 $nextTick 优化 Todo…

【UE 从零开始制作坦克】2-控制坦克移动(简单的移动效果)

效果 步骤 1. 新建蓝图类&#xff0c;父类选择“VehicleWheel&#xff08;载具车轮&#xff09;” 这里就命名为“TankWheel” 双击打开“TankWheel”&#xff0c;设置形状半径为40 2. 打开 “BP_West_Tank_M1A1Abrams” 选中“网格体&#xff08;VehicleMesh&#xff09;&…

DeepSurvk部署教程

DeepSurvk部署教程 作者:千树、Totoro github项目地址 https://github.com/arturomoncadatorres/deepsurvk Pypi项目地址 https://pypi.org/project/deepsurvk/ 一、DeepSurvk简介 项目作者原话(翻译) DeepSurv 是一种 Cox 比例风险深度神经网络&#xff0c;用于模拟患者协变…