close-on-exec

简介

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

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注