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

1.2 两个独立进程各自打开同一份文件
文件描述符和文件表项是独立的,但是指向同一个 inode 文件索引结构

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

2. 写文件实验
针对如下常见问题:
- 多线程共享同一个文件描述符
- 父子进程指向同一个文件表
- 多进程/多线程打开同一份文件
在这 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() 三部曲
- 从文件表中获取偏移量
- 从偏移量处开始写,更新文件长度
- 更新文件表偏移量
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(),其行为也是一样的,只是写数据变为读数据













![[golang 微服务] 6. GRPC微服务集群+Consul集群+grpc-consul-resolver案例演示](https://img-blog.csdnimg.cn/img_convert/c089a3215074dfddca1d2ad2ad269a8f.png)





