解决socket在execl之后被自动继承的问题

本文同步自:https://zohead.com/archives/close-socket-after-execl/

Linux下的进程在创建子进程(fork)后,子进程会自动继承父进程的文件描述符,这种机制在没有 execl 运行时一般都没有问题,而且也比较方便在子进程中直接使用父进程中打开的文件描述符,但如果子进程通过 execl 类函数运行新的程序时,这些继承的文件描述符却没有被关闭,导致 execl 之后的程序中也有父进程中文件描述符的拷贝,有些情况就会出现错误。

由于 socket 也在这些文件描述符范围内,本文简单说下解决 socket 被自动继承的几种方法。

首先是测试程序,头文件就没列出来了。

# ... 包含的头文件... #

int main(int argc, char ** argv)
{
	int ret = 0, sock = 0;
	struct sockaddr_in in_svr = {0};
	pid_t p_ret = 0;

	signal(SIGCHLD, SIG_IGN);

	sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (sock < 0) {
		perror("create socket failed");
		return 1;
	}

	in_svr.sin_family = AF_INET;
	in_svr.sin_addr.s_addr = htonl(INADDR_ANY);
	in_svr.sin_port = htons(5555);

	ret = bind(sock, (struct sockaddr *)&in_svr, sizeof(in_svr));
	if (ret != 0) {
		perror("bind failed");
		close(sock);
		return 2;
	}

	p_ret = fork();

	if (p_ret < 0) {
		perror("fork failed");
		close(sock);
		return 3;
	} else if (p_ret == 0) {
		execl("/bin/sleep", "sleep", "10", NULL);
		exit(0);
	}

	sleep(10);
	close(sock);

	return 0;
}

父进程和子进程分别 sleep 10秒钟,方便外部查看打开的文件描述符,编译运行程序,使用 lsof 命令就可以看到子进程在 execl 之后的 sleep 进程中也打开了父进程的 socket 描述符(UDP连接),这不是我们想要的效果滴:

[root@localhost ~]# lsof -c sleep
COMMAND     PID    USER   FD        TYPE  DEVICE SIZE/OFF   NODE      NAME
sleep               5750  root       cwd      DIR     8,1           4096           8145         /root
sleep               5750  root      rtd         DIR     8,1           4096           2                /
sleep        
      5750  root      txt         REG    8,1           27880         456104    /bin/sleep
sleep        
       5750  root     mem    REG    8,1           155696       301801    /lib64/ld-2.12.so
sleep      
         5750  root     mem    REG    8,1           1912928    301802    /lib64/libc-2.12.so
sleep      
         5750  root     mem    REG    8,1           99158752  391655   /usr/lib/locale/locale-archive
sleep       
        5750  root     0u        CHR    136,1      0t0                4              /dev/pts/1
sleep     
          5750  root     1u        CHR    136,1      0t0               4               /dev/pts/1
sleep   
            5750  root     2u        CHR    136,1      0t0               4               /dev/pts/1
sleep               5750  root     3u        IPv4    15022      0t0             UDP         *:5555

解决办法有如下几种:

1、比较死板的方法,在子进程 fork 之后关闭所有的 socket 描述符,在 execl 之前加上:

    int fd = 0, fdtablesize = 0;

    for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) {
        // close all socket handle inherited from parent process
        if (isfdtype(fd, S_IFSOCK)) {
            close(fd);
        }
    }

    这种处理比较彻底,即使子进程没有 execl 也会关闭继承的 socket 句柄,如果要关闭从父进程继承的所有句柄,去掉 isfdtype 判断即可。

2、使用 fcntl 为父进程 socket 设置 O_CLOEXEC 标志,表示此 socket 在 execl 之前自动关闭(注意并非在 fork 之后),在 socket 创建完成之后加上:

    ret = fcntl(sock, F_GETFD);
    ret = fcntl(sock, F_SETFD, ret | FD_CLOEXEC);

3、Linux 2.6.27 之后创建 socket 时开始支持 SOCK_CLOEXEC 参数,与上面的效果相似,只要改创建 socket 的地方:

    sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP);

    另外 Linux 2.6.27 之后创建 socket 另外支持 SOCK_NONBLOCK 参数,如果设置此参数,socket 自动创建为非阻塞式。

以上三种办法均在 RHEL 6.1 64位系统上编译测试通过,有任何问题欢迎交流,玩的开心 ^_^





*