fcntl 锁在 Go 中通过 execve 之后不生效的问题

2018/05/10

Categories: OS Linux Tags: linux fcntl lock

背景

man 2 fcntl

Record locks are not inherited by a child created via fork(2), but are preserved across an execve(2).

看到 fcntl 的介绍,我们想当然地认为 fcntl 的记录锁在 execve 之后都是能够保留的。

在我们使用 Go 来实现的时候,很快就发现了问题,请看如下代码:

package main

import (
   "fmt"
   "log"
   "os"
   "runtime"
   "syscall"
   "time"
)

func main() {
   fmt.Println("Begin")
   fd, err := syscall.Open("lock", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
   if err != nil {
       log.Printf("%s", err)
       return
   }

   ft := &syscall.Flock_t{
       Type: syscall.F_WRLCK,
   }

   err = syscall.FcntlFlock(uintptr(fd), syscall.F_SETLK, ft)
   if err != nil {
       log.Printf("%s", err)
       return
   }

   fmt.Printf("Pid %d  fd %d \n", os.Getpid(), fd)
   time.Sleep(5 * time.Second)

   argv := []string{"./test.sh"}
   syscall.Exec(argv[0], argv, []string{})
}

这段代码在 Linux 上,是无法把 fcntl 的锁传递给 test.sh 进程的。然而在 macOS 上很正常。

另外通过 C/Python 来实现,也是没有问题的。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>  
#include <stdlib.h>

int main() {
    printf("begin %d\n", getpid());
    int fd = open("lock", O_WRONLY | O_CREAT | O_TRUNC , 0777);
    if (fd < 0 ) {
      printf("fail to open");
      return -1;
    }

    printf("fd %d\n", fd); 

    struct flock fl ;
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;
    
    int errno = fcntl(fd, F_SETLK, &fl);
    if( errno != 0) {
      printf("err %d", errno);
      return -1;
    }

    sleep(5);
    char *env[] = { NULL };
    char *argv[] = {"./test.sh", NULL};
    execve(argv[0], argv, env);
    return 0;
}

所以问题是出在 Go 语言本身吗?

原因

在同事手写了一个内核模块深入分析后,发现内核在 exceve 时关闭了锁。阅读 Linux Kernel 源码后,终于发现真相。

我们来看下 exceve 系统调用的部分实现。

do_execveat_common 中:

struct files_struct *displaced;
retval = unshare_files(&displaced);
...
if (displaced)
    put_files_struct(displaced);

我们需要关注的是 displaced 这个变量。

unshare_files 调用了 unshare_fd:

/*
 *    Helper to unshare the files of the current task.
 *    We don't want to expose copy_files internals to
 *    the exec layer of the kernel.
 */

int unshare_files(struct files_struct **displaced)
{
    struct task_struct *task = current;
    struct files_struct *copy = NULL;
    int error;

    error = unshare_fd(CLONE_FILES, &copy);
    if (error || !copy) {
        *displaced = NULL;
        return error;
    }
    *displaced = task->files;
    task_lock(task);
    task->files = copy;
    task_unlock(task);
    return 0;
}

unshare_fd:

/*
 * Unshare file descriptor table if it is being shared
 */
static int unshare_fd(unsigned long unshare_flags, struct files_struct **new_fdp)
{
    struct files_struct *fd = current->files;
    int error = 0;

    if ((unshare_flags & CLONE_FILES) &&
        (fd && atomic_read(&fd->count) > 1)) {
        *new_fdp = dup_fd(fd, &error);
        if (!*new_fdp)
            return error;
    }

    return 0;
}

这里,当发现 fd 的引用计数大于 1 时,调用 dup_fd 复制一个新的 fdt 表。

之后在 put_files_struct 函数中 displaced 文件进行关闭操作。

void put_files_struct(struct files_struct *files)
{
    if (atomic_dec_and_test(&files->count)) {
        struct fdtable *fdt = close_files(files);

        /* free the arrays if they are not embedded */
        if (fdt != &files->fdtab)
            __free_fdtable(fdt);
        kmem_cache_free(files_cachep, files);
    }
}

结论

所以在单线程环境中,fd 的引用计数等于 1,原来的 fdt 会被之后的进程继承。而在多线程环境下,旧的 fdt 会被替换并且关闭。

总之,多线程环境下,慎用 fcntl 记录锁。

当我们在 C 代码中加入线程后,就能发现 execve 之后 fcntl 锁没有保留。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <pthread.h>  
#include <stdlib.h>

void thread(){   
    for(int i=0;i<=5;i++)  
    {  
        printf("this is thread %d\n", i);  
        sleep(5);
    }  
}  

int main() {
    printf("begin %d\n", getpid());
    int fd = open("lock", O_WRONLY | O_CREAT | O_TRUNC , 0777);
    if (fd < 0 ) {
        printf("fail to open");
        return -1;
    }

    printf("fd %d\n", fd); 

    struct flock fl ;
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;
    
    int errno = fcntl(fd, F_SETLK, &fl);
    if( errno != 0) {
        printf("err %d", errno);
        return -1;
    }

    pthread_t id;  //线程的标识符,unsigned long int.  
    int ret,i=0;  
    ret = pthread_create(&id,NULL,(void *)thread,NULL);   
    if(ret!=0)    //  线程创建成功返回0  
    {  
        printf("To thread failed\n");  
        exit(0);  
    }  

    sleep(5);
    char *env[] = { NULL };
    char *argv[] = {"./test.sh", NULL};
    execve(argv[0], argv, env);
    return 0;
}