fcntl 锁在 Go 中通过 execve 之后不生效的问题
背景
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 系统调用的部分实现。
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, ©);
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;
}