NAME

close_range - 지정한 범위의 파일 디스크립터 모두 닫기

SYNOPSIS

#include <linux/close_range.h>

int close_range(unsigned int first, unsigned int last,
                unsigned int flags);

주의: 이 시스템 호출에 대한 glibc 래퍼가 없다. NOTES 참고.

DESCRIPTION

close_range() 시스템 호출은 first에서 last까지 (경계 포함) 열린 파일 디스크립터들을 모두 닫는다.

현재는 파일 디스크립터를 닫는 중의 오류를 무시한다.

flags는 다음을 0개 이상 담은 비트 마스크다.

CLOSE_RANGE_CLOEXEC (리눅스 5.11부터)
지정한 파일 디스크립터들을 즉시 닫는 게 아니라 exec에서 닫기 플래그를 설정한다.
CLOSE_RANGE_UNSHARE
지정한 파일 디스크립터들을 닫기 전에 다른 프로세스와 공유 해제해서 파일 디스크립터 테이블을 공유하는 다른 스레드와의 경쟁을 피한다.

RETURN VALUE

성공 시 close_range()는 0을 반환한다. 오류 시 -1을 반환하며 오류를 나타내도록 errno를 설정한다.

ERRORS

EINVAL
flags가 유효하지 않거나, firstlast보다 크다.

CLOSE_RANGE_UNSHARE에서 (새 디스크립터 테이블을 구성하면서) 다음 오류가 발생할 수 있다.

EMFILE
열린 파일 디스크립터 수가 /proc/sys/fs/nr_open에 지정된 제한을 초과한다. (proc(5) 참고.) CLOSE_RANGE_UNSHARE 플래그를 지정한 close_range() 호출에 앞서 그 제한을 낮춘 경우에 이 오류가 발생할 수 있다.
ENOMEM
사용 가능한 커널 메모리가 충분하지 않다.

VERSIONS

리눅스 5.9에서 close_range()가 처음 등장했다.

CONFORMING TO

close_range()는 비표준 함수이며 FreeBSD에도 있다.

NOTES

glibc에서 이 시스템 호출의 래퍼를 제공하지 않는다. syscall(2)을 이용해 호출해야 한다.

모든 파일 디스크립터 닫기

가능한 파일 디스크립터 범위의 파일 디스크립터들을 무턱대고 닫는 방식을 피하기 위해 (리눅스에서) /proc/self/fd/의 열린 파일 디스크립터들을 나열해서 각각에 close(2)를 호출하는 경우가 있다. close_range()는 이를 /proc 필요 없이 한 번의 시스템 호출에서 대신 해 주며, 그래서 상당한 성능 이득을 준다.

exec 전에 파일 디스크립터 닫기

다음처럼 해서 파일 디스크립터들을 안전하게 닫을 수 있다.

/* stderr 다음은 다 필요 없는 경우 */
close(range(3, ~0U, CLOSE_RANGE_UNSHARE);
execve(....);

CLOSE_RANGE_UNSHARE는 개념적으로 다음과 동등하되 더 효율적이다.

unshare(CLONE_FILES);
close_range(first, last, 0);

공유 해제 범위가 호출자의 파일 디스크립터 테이블에 현재 할당된 파일 디스크립터 최대 번호를 넘어가는 경우에 (흔히 하듯 last가 ~0U이면 그렇게 된다.) 커널에선 first까지만 호출자의 새 파일 디스크립터 테이블을 공유 해제해서 파일 디스크립터를 가급적 적게 복사한다. 그러면 이어지는 close(2) 호출이 아예 필요치 않게 된다. 즉, 테이블을 공유 해제하고 나면 전체 동작이 완료된다.

exec 시 파일 닫기

exec 전의 여러 구성 단계들이 서로 충돌할 위험이 있는 경우에 특히 유용하다. 예를 들어 seccomp(2) 프로파일 설정이 close_range() 호출과 충돌할 수 있다. 만약 seccomp(2) 프로파일을 설정하기 전에 파일 디스크립터들을 닫는다면 프로파일 설정 단계에서 그 디스크립터들을 이용할 수도, 그리고 닫는 걸 통제할 수도 없다. 만약 설정 후에 파일 디스크립터들을 닫는다면 seccomp 프로파일에서 close_range() 호출 내지 대체 호출을 차단할 수 없다. CLOSE_RANGE_CLOEXEC를 쓰면 이 상황을 피할 수 있다. 디스크립터에 표시를 한 다음 seccomp(2) 프로파일을 설정하면 프로파일에서 호출 프로세스에 영향을 끼치지 않고 close_range() 접근을 통제할 수 있다.

EXAMPLES

아래 프로그램은 명령행 인자가 가리키는 파일들을 열고, (/proc/PID/fd의 항목들을 순회해서) 연 파일들의 목록을 표시하고, close_range()로 3 이상의 파일 디스크립터를 모두 닫고, 다시 프로세스의 열린 파일 목록을 표시한다. 다음 예가 프로그램 사용 방식을 보여 준다.

$ touch /tmp/a /tmp/b /tmp/c
$ ./a.out /tmp/a /tmp/b /tmp/c
/tmp/a opened as FD 3
/tmp/b opened as FD 4
/tmp/c opened as FD 5
/proc/self/fd/0 ==> /dev/pts/1
/proc/self/fd/1 ==> /dev/pts/1
/proc/self/fd/2 ==> /dev/pts/1
/proc/self/fd/3 ==> /tmp/a
/proc/self/fd/4 ==> /tmp/b
/proc/self/fd/5 ==> /tmp/b
/proc/self/fd/6 ==> /proc/9005/fd
========= About to call close_range() =======
/proc/self/fd/0 ==> /dev/pts/1
/proc/self/fd/1 ==> /dev/pts/1
/proc/self/fd/2 ==> /dev/pts/1
/proc/self/fd/3 ==> /proc/9005/fd

참고로 경로명 /proc/9005/fd가 나오는 행은 opendir(3) 호출로 인한 것이다.

프로그램 소스

#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/close_range.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>

/* /proc/self/fd의 심볼릭 링크들 내용 보여 주기 */

static void
show_fds(void)
{
    DIR *dirp = opendir("/proc/self/fd");
    if (dirp  == NULL) {
        perror("opendir");
        exit(EXIT_FAILURE);
    }

    for (;;) {
        struct dirent *dp = readdir(dirp);
        if (dp == NULL)
            break;

        if (dp->d_type == DT_LNK) {
            char path[PATH_MAX], target[PATH_MAX];
            snprintf(path, sizeof(path), "/proc/self/fd/%s",
                     dp->d_name);

            ssize_t len = readlink(path, target, sizeof(target));
            printf("%s ==> %.*s\n", path, (int) len, target);
        }
    }

    closedir(dirp);
}

int
main(int argc, char *argv[])
{
    for (int j = 1; j < argc; j++) {
        int fd = open(argv[j], O_RDONLY);
        if (fd == -1) {
            perror(argv[j]);
            exit(EXIT_FAILURE);
        }
        printf("%s opened as FD %d\n", argv[j], fd);
    }

    show_fds();

    printf("========= About to call close_range() =======\n");

    if (syscall(__NR_close_range, 3, ~0U, 0) == -1) {
        perror("close_range");
        exit(EXIT_FAILURE);
    }

    show_fds();
    exit(EXIT_FAILURE);
}

SEE ALSO

close(2)


2021-03-22