当一个程序使用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(¤t_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(¤t_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(¤t_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(¤t_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)
前几天写了一篇博客,讲述了端口占用情况的查看和解决。
大部分这种问题都能够解决,在文章的最后,提到了一种特殊情况,就是父子进程中的端口占用情况。父进程监听一个端口后,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);
好了,现在我们终于可以完美的解决端口占用这个令人烦恼的问题了。