linux c fcntl 函数 close_on_exec标志位的作用

当一个程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数加载执行另一个新程序。
此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。
描述进程的结构体中有一个close_on_exec,它是一个进程所有文件描述符(文件句柄)的位图标志,
每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄(参见include/fcntl.h)。
若一个文件描述符在close_on_exec中的对应比特位被设置,那么在执行execve()时该描述符将被关闭,否则该描述符将始终处于打开状态,可以继续在execve 打开的进程中继续使用。
当打开一个文件时,默认情况下文件句柄在子进程中也处于打开状态。

测试代码 : app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <time.h>

int main()  
{  
    printf("--------------\n");
    pid_t pid; 
    int fd; 
    int ret ;
    time_t current_time;
    char *p ;
    fd = open("test.txt",O_RDWR|O_APPEND);  

    printf("fd = %d\n",fd);
    current_time = time(NULL);
    printf("time = %s\n",ctime(&current_time));  

    fcntl(fd, F_SETFD, 1);                      
    char *s="123456789\n";  
    pid = fork();  
    if(pid == 0){  
    	execl("ass", "./ass", &fd, NULL);
    }

    wait(NULL); 
    printf("wait return from child process\n"); 
    p = ctime(&current_time);
    ret = write(fd, p, strlen(p));
    printf("retA = %d \n",ret );
    ret = write(fd, s, strlen(s));

    printf("retB = %d\n",ret);

    close(fd);  
    return 0;  
}

测试代码: ass.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc, char *argv[])  
{  
    int fd;
    int len;  
    printf("argc = %d ",argc);

    fd = *argv[1];  
    printf("fd = %d\n",fd);  

    char *s = "abcdefghijklmn\n";  
    len = write(fd, (void *)s, strlen(s));
    if(-1 == len)
    {
        printf("write fail\n");
    }
 
    close(fd);  
    return 0;  
}

执行效果

首先手动创建一个 空的 test.txt 文件。

执行log

--------------
fd = 3
time = Fri Jul  2 10:28:28 2021

argc = 2 fd = 3
write fail
wait return from child process
retA = 25
retB = 10

查看test.txt文件的内容

Fri Jul  2 10:28:28 2021
123456789

修改测试代码 app2.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <time.h>

int main()  
{  
    printf("--------------\n");
    pid_t pid; 
    int fd; 
    int ret ;
    time_t current_time;
    char *p ;
    fd = open("test2.txt",O_RDWR|O_APPEND);  

    printf("fd = %d\n",fd);
    current_time = time(NULL);
    printf("time = %s\n",ctime(&current_time));  

    fcntl(fd, F_SETFD, 0);                      
    char *s="123456789\n";  
    pid = fork();  
    if(pid == 0){  
    	execl("ass", "./ass", &fd, NULL);
    }

    wait(NULL); 
    printf("wait return from child process\n"); 
    p = ctime(&current_time);
    ret = write(fd, p, strlen(p));
    printf("retA = %d \n",ret );
    ret = write(fd, s, strlen(s));

    printf("retB = %d\n",ret);

    close(fd);  
    return 0;  
}

执行log
--------------
fd = 3
time = Fri Jul  2 10:30:26 2021

argc = 2 fd = 3
wait return from child process
retA = 25
retB = 10

cat test2.txt

abcdefghijklmn
Fri Jul  2 10:30:26 2021
123456789

fcntl(fd, F_SETFD, 1) 此句将fd的close-on-exec 标志设置为1,开启此标志。那么当子进程调用execl函数时,execl执行ass,ass是不能向fd内写入的,因为在调用execl函数之前系统已经讲子进程的中复制的这个文件描述符关闭了。(attention:这里是子进程!)
但是如果将 fcntl(fd, F_SETFD, 1)改为fcntl(fd, F_SETFD, 0),或者直接将此句注释掉,那么,ass便可以向这个文件描述符中任意添写东西了~~

PS:如果将fcntl设置为开启,即设置为1,那么,此文件描述符依然是可以被主进程操作的。

fcntl(fd, F_SETFD, 1); 只是表示 主进程打开的 文件描述符,在子进程中被复制,子进程可以读写这个文件。
如果子进程执行execl函数,被子进程复制的这个文件描述符被关闭。
但是主进程中打开的文件描述符是不受影响的。
在子进程中关闭一个文件描述符,不会影响主进程中打开的文件描述符。

FD_CLOEXEC

/usr/include/x86_64-linux-gnu/bits/fcntl-linux.h

include/x86_64-linux-gnu/bits/fcntl-linux.h:# define F_DUPFD_CLOEXEC 1030       /* Duplicate file descriptor with
include/x86_64-linux-gnu/bits/fcntl-linux.h:#define FD_CLOEXEC  1       /* Actually anything with low bit set goes */

11

转载请注明出处:帘卷西风的专栏(http://blog.csdn.net/ljxfblog)

前几天写了一篇博客,讲述了端口占用情况的查看和解决。

关于linux系统端口查看和占用的解决方案

大部分这种问题都能够解决,在文章的最后,提到了一种特殊情况,就是父子进程中的端口占用情况。父进程监听一个端口后,fork出一个子进程,然后kill掉父进程,再重启父进程,这个时候提示端口占用,用netstat查看,子进程占用了父进程监听的端口。

原理其实很简单,子进程在fork出来的时候,使用了写时复制(COW,Copy-On-Write)方式获得父进程的数据空间、 堆和栈副本,这其中也包括文件描述符。刚刚fork成功时,父子进程中相同的文件描述符指向系统文件表中的同一项(这也意味着他们共享同一文件偏移量)。这其中当然也包含父进程创建的socket。

接着,一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。

但是在复杂系统中,有时我们fork子进程时已经不知道打开了多少个文件描述符(包括socket句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在fork子进程前打开某个文件句柄时就指定好:“这个句柄我在fork子进程后执行exec时就关闭”。其实时有这样的方法的:即所谓 的 close-on-exec。

回到我们的应用场景中来,只要我们在创建socket的时候加上SOCK_CLOEXEC标志,就能够达到我们要求的效果,在fork子进程中执行exec的时候,会清理掉父进程创建的socket。

#ifdef WIN32
	SOCKET ss = ::socket(PF_INET, SOCK_STREAM, 0);
#else
	SOCKET ss = ::socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
#endif

当然,其他的文件描述符也有类似的功能,例如文件,可以在打开的时候使用O_CLOEXEC标识(linux 2.6.23才开始支持此标记),达到和上面一样的效果。或者使用系统的fcntl函数设置FD_CLOEXEC即可。

//方案A
int fd = open(“foo.txt”,O_RDONLY);
int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
//方案B,linux 2.6.23后支持
int fd = open(“foo.txt”,O_RDONLY | O_CLOEXEC);

好了,现在我们终于可以完美的解决端口占用这个令人烦恼的问题了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值