linux文件系统思考

Posted by Jason on Wednesday, October 30, 2019

TOC

直面问题

从一段代码讲起,可以先自己🤔思考一下,下面代码存在什么问题?

// 上传本地文件到某个服务器
func (c *server) Upload(context.Context, file string) error {
    src, err := os.Open(file)
    if err != nil {
        return err
    }
    defer closer.Close(src)
    ........
    return nil
}

// 从某个数据储存中下载文件到本地
func (c *server) Download(ctx context.Context, file string) error {
   
    .......

    // lock to allow only one job access isDownloading map
    c.lock.Lock()
    r, ok := c.isDownloading[file]
    if ok {
        // unlock if another job is downloading
        c.lock.Unlock()
        c.waitDownloading(file)
        <-r.finished
        return r.err
    }

    r = &result{
        finished: make(chan struct{}),
    }
    c.isDownloading[file] = r
    // unlock to process other jobs
    c.lock.Unlock()

    defer func() {
        c.lock.Lock()
        delete(c.isDownloading, file)
        close(r.finished)
        c.lock.Unlock()
    }()

    // Download source file into local file
    _, err = c.client.Download(file /* source */, file /* target */)
    
    .......

    klog.Infof("download file %v successfully", file)
    return nil
}

上面实现功能是文件上传和下载。在下载代码里加了文件锁,所以,多个任务同时下载一个文件时,只会有一个协程真正从数据中心进行下载。乍一看不会存在什么问题,但是仔细一想,如果上传和下载同时进行,上传的文件就会被损坏。

所以,上面虽然避免了同时写同一个文件的问题,但是,针对上传和下载同时进行的情况并没有避免。一种简单的方法是,在上传的代码中同样加一个文件锁,这样会造成系统的吞吐降低。还有另一种解决方案,先下载到一个随机命名的文件,然后rename成目的文件。修改Download 函数下载部分,如下:

......
    tmpFile, err := ioutilTempFile("", "tmp-"+file+".*")
    if err != nil {
        r.err = fmt.Errorf("failed creating tmp file for app pkg: %v", err)
        return r.err
    }

    // Download file into temp file
    _, err = c.client.Download(file, tmpFile.Name())
    if nil != err {
        if err := os.Remove(tmpFile.Name()); err != nil && !os.IsNotExist(err) {
            klog.Warningf("can't remove damaged local file %v", file)
        }

        r.err = fmt.Errorf("failed to download %v: %v", file, err)
        return r.err
    }

    err = os.Rename(tmpFile.Name(), file)
    if err != nil {
        r.err = fmt.Errorf("failed to rename app pkg from %s to %s: \n%v", tmpFile.Name(), file, err)
        return r.err
    }

这样修改,并发上传、下载都不会出现问题了。可是另一个问题来了,rename 为什么就不会影响正在上传的文件了呢?

linux 文件系统

回答上面问题,先要从Linux的文件系统讲起。在 Linux 文件系统(以ext4为例),文件分为 superblock、inode、block 这三个模块。我们在系统中创建文件,并写入文件时,文件系统的组成结构:

引用鸟哥的图

  • superblock:记录此 filesystem 的整体信息,包括inode/block的总量、使用量、剩余量, 以及文件系统的格式与相关信息等;

  • inode:记录文件的属性,文件类型、文件执行权限(rwx)、uid、gid、创建时间等。一个文件占用一个inode,同时记录此文件的数据所在的 block 号码;

  • block:实际记录文件的内容,若文件太大时,会占用多个 block。ext2 文件系统中,block 的大小可以为 1KB、2KB、4KB等。

  • inode bitmap/data bitmap 分别标示哪个该位置是否被使用。

文件映射简图

以系统中文件 /etc/passwd 为例,读取文件内容,需要先从根(/)目录的 inode 依次定位到文件(/etc/passwd)的inode,然后根据需要将数据块加载到内存中。

// 移动前
dev@go-dev-54d4f7c7cd-z4ptc:/go$ ls -l /proc/1358/fd
total 0
lrwx------. 1 dev dev 64 Oct 30 11:35 0 -> /dev/pts/1
lrwx------. 1 dev dev 64 Oct 30 11:35 1 -> /dev/pts/1
lrwx------. 1 dev dev 64 Oct 30 11:35 2 -> /dev/pts/1
lr-x------. 1 dev dev 64 Oct 30 11:35 3 -> /tmp/test.file
lrwx------. 1 dev dev 64 Oct 30 11:35 4 -> anon_inode:[eventpoll]
// rename 
dev@go-dev-54d4f7c7cd-z4ptc:/go$ mv src/111 /tmp/test.file
// 移动后
dev@go-dev-54d4f7c7cd-z4ptc:/go$ ls -l /proc/1404/fd
total 0
lrwx------. 1 dev dev 64 Oct 30 11:36 0 -> /dev/pts/1
lrwx------. 1 dev dev 64 Oct 30 11:36 1 -> /dev/pts/1
lrwx------. 1 dev dev 64 Oct 30 11:36 2 -> /dev/pts/1
lr-x------. 1 dev dev 64 Oct 30 11:36 3 -> /go/src/test.file (deleted)
lrwx------. 1 dev dev 64 Oct 30 11:36 4 -> anon_inode:[eventpoll]

当进程读取文件 /tmp/test.file 时,Linux会在目录 /proc/{进程pid}/fd目录下创建一个文件 /tmp/test.file 的符号链接。如果这个时候执行 mv /go/111 /tmp/test.file 会看到符号链接链接源标记为删除状态了。可以看到符号链接 3 已经被标记为 deleted 状态。

程序输出结果仍然是原来的文件内容。因为在相同分区内,mv 操作首先删除 /tmp/test.file(如果文件被占用,则会标记inode 为删除,等占用结束后,系统自动清除 block 内容标记),然后修改文件名 (/tmp/test.file) 关联的 inode 为 src/111 的 inode。

总结

所以,对文件先下载到随机文件,再执行 rename(mv 最终调用的rename)没有造成文件不一致,究其原因是:程序读取前已经拿到原始文件的fd (即上面3),读取过程使用通过 inode 索引 datablock,而 rename 过程中并不会修改 data block 的内容。直接 download 下载的时候,修改的是 data block 的内容,所以,会造成文件的不一致。

另外,有两个疑问:

  1. 为什么移动后,进程 fd 目录下描述符3 由 3 -> /tmp/test.file 变成了 3 -> /go/src/test.file,即:目录变成了被移动文件所在的目录。

  2. 写入文件时,如何知道哪个区域可以写入?

参考

  1. 认识Linux文件系统
  2. 删除正在使用的文件——釜底抽薪?

「真诚赞赏,手留余香」

Jason Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付


comments powered by Disqus