简介

Donald E. Porter 教授的 CSE 506: Operating Systems 教材 VFS 部分中提到Linux文件操作使用的一个标识

CLOSE_ON_EXEC – a bit that prevents file inheritance
if a new binary is exec’ed (set by open or fcntl) 

这个标识位支持exec执行前关闭其从父进程继承来的文件描述符

使用

设置方法:

  • 通过fcntl 设置FD_CLOEXEC
int flags = fcntl(connfd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(connfd, F_SETFD, flags);
  • 通过O_CLOEXEC
open(path, O_CLOEXEC | flags)
socket(DOMAIN, SOCK_CLOEXEC | type, PROTOCOL)
accept4(int sockfd, struct sockaddr *addr, \
         socklen_t *addrlen, SOCK_CLOEXEC | flags);
fopen(path, "re")

可以在以下这种模式下使用, fork后还是dup了父进程的文件描述符,exec后将自动关闭.

pid_t pid;
pid = fork(); 
if (pid == 0) {
    exec(...)
};

验证

  • 代码

server.c

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>         
#include <netinet/in.h>
#include <fcntl.h>

void main()
{
int sockfd;
#ifdef __CLOEXEC
     sockfd = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, 0);
#else
     sockfd = socket(AF_INET, SOCK_STREAM, 0);
#endif

struct sockaddr_in servaddr;

     bzero(&servaddr, sizeof(servaddr));
     servaddr.sin_family = AF_INET;
     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
     servaddr.sin_port = htons(8888);

     int option = 1;
     setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof( option));

     bind(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr));

     listen(sockfd, 5);

struct sockaddr_in cliaddr;
int connfd;
int cliaddlen = sizeof(cliaddr);

#ifdef __CLOEXEC
     connfd = accept4(sockfd, (struct sockaddr *)&cliaddr,  &cliaddlen, SOCK_CLOEXEC);
#else
     connfd = accept(sockfd, (struct sockaddr *)&cliaddr,  &cliaddlen);
#endif

#ifdef __FCNTL
     int flags = fcntl(connfd, F_GETFD);
     flags |= FD_CLOEXEC;
     fcntl(connfd, F_SETFD, flags);
#endif

     char buf[100];
     snprintf(buf, sizeof(buf), "%d", connfd);

     pid_t pid = fork();
     if ( pid == 0) {
        char* argv[] = {"./serverson", buf, NULL };
        char* envp[] = { NULL };
        execve("./serverson", argv, envp) ;
        printf(" child start\n");
        exit(0);
     }

     printf(" parent start\n");
     while (0){
         //do nothing
     }

     printf("parent exit \n");
     return ;
}

serverson.c

#include <stdio.h>

void
main (int argc, char *argv[])
{
  printf("in child %s", argv[1]);
  int connfd = atol(argv[1]);
  printf ("in child running\n");
  while (1)
    {
      char buff[10];
      memset (buff, 0, sizeof (buff));
      read (connfd, buff, 1);
      printf ("%s", buff);
      if (buff[0] == '#')
          break;

    }
  return 0;
}
  • 运行
### 编译
$ gcc server.c -o server
$ gcc serverson.c -o serverson

### 服务端运行
$ ./server

### 客户端
$ telnet ip 8888

通过运行结果可以看到如果不设置close-on-exec, 服务端子进程会处理请求屏幕打印客户端数据的字符串。重新编译开启close-on-exec后

gcc -D__CLOEXEC server.c -o server
gcc -D__FCNTL server.c -o server

客户端返回, exec执行是会关闭继承过来的文件描述符。

Connection closed by foreign host.

kernel相关

  • O_CLOEXEC相关内核实现

与进程相关结构可以看到close_on_exec定义,有可能存在多个fd,可以看到是指针类型.

/include/linux/fdtable.h

struct fdtable {
    unsigned int max_fds;
    struct file __rcu **fd;     
     /* current fd array */
    unsigned long *close_on_exec;
    unsigned long *open_fds;
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};

分配文件描述符时的处理,根据O_CLOEXEC设置 fdtableclose_on_exec

fs/file.c

int __alloc_fd(...) {
    __set_open_fd(fd, fdt);
    if (flags & O_CLOEXEC)
        __set_close_on_exec(fd, fdt);
    else
        __clear_close_on_exec(fd, fdt);
}

static inline void __set_close_on_exec(...)
{
    __set_bit(fd, fdt->close_on_exec);
}

在载入二进制文件时,load_elf_binary->begin_new_exec->do_close_on_exec

/fs/binfmt_elf.c

static int load_elf_binary(...) {

...
    /* Flush all traces of the currently
     running executable */
    retval = begin_new_exec(bprm);
...

}

/fs/exec.c

int begin_new_exec(...)
{
    ...
    do_close_on_exec(me->files);
    ...
}

可以看到获取进程每个文件是否设置了close_on_exec来判断是否在调用exec后.需要关闭

/fs/file.c

void do_close_on_exec(struct files_struct *files)
{
    unsigned i;
    struct fdtable *fdt;

    /* exec unshares first */
    spin_lock(&files->file_lock);
    for (i = 0; ; i++) {
        unsigned long set;
        unsigned fd = i * BITS_PER_LONG;
        fdt = files_fdtable(files);
        if (fd >= fdt->max_fds)
            break;
        set = fdt->close_on_exec[i];
        if (!set)
            continue;
        fdt->close_on_exec[i] = 0;
        for ( ; set ; fd++, set >>= 1) {
            struct file *file;
            if (!(set & 1))
                continue;
            file = fdt->fd[fd];
            if (!file)
                continue;
            rcu_assign_pointer(fdt->fd[fd], NULL);
            __put_unused_fd(files, fd);
            spin_unlock(&files->file_lock);
            filp_close(file, files);
            cond_resched();
            spin_lock(&files->file_lock);
        }

    }
    spin_unlock(&files->file_lock);
}

参考

Excuse me son, but your code is leaking !!! Introduce O_CLOEXEC (take >2) close on exec