Linux写文件场景下的几个问题

Posted by Keal on March 11, 2021

多进程并发写日志是否会乱序

“All system calls are executed atomically. By this, we mean that the kernel guarantees that all of the steps in a system call are completed as a single operation, without being interrupted by another process of thread.”

–The Linux Programming Interface

首先明确一点:即linux下所有的系统调用本身都是原子性的.也就是说你如果调用了多个write()去写文件,能保证每个write写的内容的连续的,不会写到一半被其他线程插入的情况.

现在你需要知道写文件的时候实际调用了内核的哪些操作.

通常来讲,写日志的场景会是在文件的尾部添加.因此需要两个步骤:

  1. 调用lseek(fd,0,SEEK_END); // 定位到文件尾部
  2. 调用write(fd,”log message”,len); // 执行写操作

考虑到多个系统调用并不具备原子性,所以这样并不能保证并发下写文件的正确性. 假设一个场景,A和B两个线程都要写日志.A和B都通过lseek定位到了目前的尾部,但是A和B分开写的时候,因为在同一个位置开始写,会导致有一个线程的内容被覆盖,造成内容丢失.

如何解决?

O_APPEND标志.

Unix系统提供了一种机制是在打开文件的时候指定O_APPEND标志,此标志的作用在于每次写之前,内核都将文件的偏移量定位到文件末尾.这样在写的时候就不要使用lseek,而只需要用write保证了写文件的原子性.

写文件时将文件移走或删除

这里需要明确两个概念,在linux中

文件的元信息存在称为inode的结构中.

文件会有两个link,一个是i_count表示正在引用这个文件的数量,一个是i_nlink,表示硬链接到这个文件的数量.只有当两个都为0时,才算真正的从系统中删除了这个文件.

使用mv移走文件

mv的两种场景.

  1. 如果mv的目标地址有重名文件,则会删除旧的重名文件并用源文件替换.因此对源文件的写入依然能够进行.
  2. 如果mv的目标地址没有重名文件,则直接创建一个相同的文件(inode是不变的)

因此,对源文件的写入依然能够进行,因为inode没有发生改变..

使用rm删除文件

如果文件的硬链接数目在删除后不为0,那么写入的内容仍然写入到了文件中.你可以找到此文件的另外一个硬链接查看.

如果文件的硬链接数目在删除为为0, 但是因为文件实际上还有被引用(i_count不为0),因此实际上操作系统并没有真正彻底删除文件.所以实际上依然能够写入到文件中.只是用户未能察觉.等到所有引用的进程退出后,操作系统会删除掉这个文件.

这里面补充一些知识方便理解:

当一个进程运行时,会在系统的 /proc 下建立一个名字为进程 id 的子目录,其中的 fd 目录中含有所有被进程打开的文件描述符. 这个文件描述符实际上是个软链接,与目的文件的inode是一样的.因此你可以认为当你对一个正在写入的文件进行删除时,文件实际上都不会被删除(因为至少还有1个i_count引用). 直到最后所以进程退出后,才有可能真正删除文件.

Reference:

The Unix Filesystem

删除正在使用的文件——釜底抽薪?