fcntl 锁在 Go 中通过 execve 之后不生效的问题
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;
}

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Java并发编程-volatile关键字介绍
前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸。最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍。 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文。 什么是可见性? 一图胜千言上图已经把JAVA内存模型(JMM)展示得很详细了,简单概括一下 每个Thread有一个属于自己的工作内存(可以理解为每个厨师有一个属于自己的铁锅) 所有Thread共用一个主内存(餐厅所有的厨师共用同一个冰箱) 每个Thread操作数据之前都会去主内存中获取数据(厨师炒菜之前都要去冰箱里拿食材) Thread:厨师 工作内存:铁锅 store&load:放熟食,取食材 主内存:冰箱 读者可思考以下情景:餐厅来了一位顾客点了一份红烧肉,此时有两位大厨(假设大厨之间互不通信),由于互不通信,所以两位大厨都打开冰箱取出食材开始炒菜。最后炒出了两份红烧肉,顾客只要一份。为什么会造成这种结果? 由于大厨之间没有...
-
下一篇
序列化,数据库存多个字段数据
$old['xxx'] = (int) $_POST['ooo']; $res = serialize($old); 取出: var_dump(unserialize($res));die; 手册:http://php.net/manual/zh/function.serialize.php 序列化表单(Ajax中):https://blog.csdn.net/xinghuo0007/article/details/72654817
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- MySQL8.0.19开启GTID主从同步CentOS8