FD(File Descriptor)란?

eunduk.log·어제

백엔드

목록 보기
4/4

FD(File Descriptor)

  • 프로세스가 커널 자원(파일/소켓/파이프 등)에 접근하기 위해 쓰는 작은 정수 번호이다.
    • 예: 0, 1, 2, 3, …
    • 어떤 자원에 접근하기 위한 키(key)라고 볼 수 있다. (객체 주소 값 → 이런 건 아님)
  • 커널 자원에 대한 ‘핸들(handle)’ 역할을 하는 정수 ID
  • 실제 물리적인 자원 그 자체를 가리키고 있는 것은 아니고, 그 자원을 관리하기 위한 객체(구조체)를 가리키고 있다.
    • 그 자원을 다루기 위한 상태(state) + 연결 정보 + 버퍼 + 규칙을 가진 커널 메모리 구조체
    • 참고로 자원을 위한 커널 객체는 시스템 콜로 인해 생성되고, 커널이 fd를 반환해주면 프로세스는 fd로 자원을 핸들링한다. (시스템콜 인자로)
      • (따라서 프로세스는 내부적으로 fd 테이블로 fd를 관리한다.)
  • 그러면 그 객체는 어떤 형태일까?

FD가 가리키는 ‘객체’들은 어떤 형태인가?

  • (참고) struct file 객체는 통일된 상위 핸들이지만, struct inode 객체는 파일을 위한 메타데이터 객체이다.
    • (struct file은 소켓이든 파일이든 공용, struct inode는 파일만을 위한 객체. 소켓이나 그외는 다른 객체를 사용)
    • 파일이 아닌 소켓이라면 struct socket 이라는 객체가 생성된다.
  • open(”a.txt”)라는 시스템 콜을 호출했다고 가정하고 이해해보자.
  • 시스템 콜을 통해 어떤 자원을 사용하고자 한다면(open등을 통해), 리눅스 커널은 2개의 객체를 생성한다.
    1. 자원 자체의 정보를 가지고, 자원을 제어할 수 있는 함수 정보를 가진 메타 데이터 객체(struct inode)
    2. 시스템 콜을 요청한 프로세스만을 위한 열린 상태 객체(struct file)

메타 데이터 객체(struct inode)

  • 커널은 inode 객체를 메모리에 캐시하여 들고있고, inode가 파일 데이터 자체를 들고 있지는 않지만
    • 이 객체를 통해 파일에 접근하거나 제어할 수 있음
  • FD가 바라보고 있는 객체는 아님
  • 리눅스 커널 코드 일부(struct inode)
    struct inode {
        umode_t      i_mode;   // 파일 타입 + 권한 bits (예: -rw-r--r--)
        kuid_t       i_uid;    // 소유자
        kgid_t       i_gid;    // 그룹
        loff_t       i_size;   // 파일 크기
        struct timespec64 i_atime, i_mtime, i_ctime; // 시간들
    
        // 파일시스템이 관리하는 데이터 위치(디스크 블록/extent 등)로 이어지는 포인터들
        void        *i_private;
    
        // 실제 파일 데이터 I/O를 수행할 때 사용할 함수 테이블(파일시스템 구현)
        const struct inode_operations *i_op;
        const struct file_operations  *i_fop;
    };

열린 상태 객체(struct file)

  • FD를 통해 연결되는 객체
  • 이 객체를 통해 실제 파일과 프로세스간 연결성을 보장함 (세션같은 존재)
    • 그래서 “열려 있다”라는 표현은 시스템 콜을 호출한 프로세스와 fd를 통해 이어진 상태라고 보면 된다.
    • 따라서 닫힌 상태로 바뀌면 프로세스는 fd를 잃고 서로가 끊기게 되며, struct file 객체도 참조를 잃어 메모리에서 정리된다. (참고로 열린 상태 객체는 프로세스 내부 또는 외부와 공유될 수 있다.)
      • 닫힌 상태가 되는 경우는 다음과 같다.
        • 프로세스에서 더이상 안 쓰겠다고 close()하는 경우
        • 프로세스 자체가 종료되는 경우 exit
  • 리눅스 커널 코드 일부(struct file)
    • 파일, 소켓 등 같이 사용하는 공통 핸들러인데, 파일을 위한 커서 위치나 inode등은 왜 존재하는가?
      - 리눅스는 역사적으로 ‘파일’이 기준 모델이라서 파일을 위한 필드가 존재하고, 다른 자원들은 필요없으면 사용하지 않는다.

      struct file {
          // 열린 파일의 현재 읽기/쓰기 위치(커서)
          loff_t      f_pos;
      
          // open 플래그(O_RDONLY/O_WRONLY/O_NONBLOCK/O_APPEND 등)
          unsigned int f_flags;
      
          // 이 열린 상태가 어떤 inode(=어떤 파일)를 가리키는지
          struct inode *f_inode;
      
          // 이 열린 상태로 read/write/poll/ioctl 등을 할 때 호출될 함수 테이블
          const struct file_operations *f_op;
      
          // 구현체(소켓/파이프/디바이스)가 "자기 객체"를 걸어두는 자리
          // 소켓이면 여기서 socket 객체로 이어짐
          void *private_data;
      };

FD 번호 할당 및 반환

  • 새 fd를 만들려면 open_fds 비트가 0인 자리를 찾아서
    • 그 자리 번호를 fd로 결정하고, 1로 채운다.
  • 리눅스 커널 코드 일부(files_struct)
    struct files_struct {
    	atomic_t count;
    	bool resize_in_progress;
    	wait_queue_head_t resize_wait;
    	struct fdtable __rcu *fdt;
    	struct fdtable fdtab;
    	
    	spinlock_t file_lock ____cacheline_aligned_in_smp;
    	unsigned int next_fd; // 다음 빈 자리 찾을 위치
    	unsigned long close_on_exec_init[1];
    	unsigned long open_fds_init[1]; // 현재 열려있는 fd인지
    	unsigned long full_fds_bits_init[1];
    	struct file __rcu * fd_array[NR_OPEN_DEFAULT];
    };
    
    // 의사코드 (이렇게 fd가 생성될 것이다.)
    int get_unused_fd(struct files_struct *files)
    {
        spin_lock(&files->file_lock);
    
        struct fdtable *fdt = files_fdtable(files);
    
        int fd = find_next_zero_bit(fdt->open_fds, fdt->max_fds, files->next_fd);
        if (fd >= fdt->max_fds) {
            // fdtable 키워야 함 (max_fds 확장)
            // resize_in_progress / resize_wait 같은 필드가 이때 관여
            fd = expand_files(files, fd);  // 개념
        }
    
        __set_bit(fd, fdt->open_fds);
        if (close_on_exec_needed)
            __set_bit(fd, fdt->close_on_exec);
    
        files->next_fd = fd + 1;
    
        spin_unlock(&files->file_lock);
        return fd;
    }
    
  • fd 슬롯에 file을 “설치”한다.
    • fdt → fd[fd] = file
  • (추가 내용)
    • files_struct는 프로세스가 가지고, 이 자료구조 내부에 fdtable이 존재하는 것이다.
      • 참고로 프로세스와 스레드들은 이 files_struct를 공유한다. (프로세스 1개 기준)
    • 그래서 fd슬롯에 file을 설치한다는 말은, fdtable에 fd에 맞는 file 객체를 연결하는 것이다.
      • (fd → file주소)

커널이 FD로 열린 상태 객체(struct file)를 찾는 과정

  • 시스템 콜이 들어오면 커널이 제일 먼저 하는 일은 fd를 통해 struct file 객체를 찾는 것이다.
    • 프로세스는 시스템 콜에 정수 fd만 넘긴다.
  • 커널은 프로세스의 fd table에서 fd에 대응하는 열린 객체를 찾기 위한 여정을 떠난다.

struct file을 얻는 과정 (열린 상태 객체에 접근)

  1. files_struct 내부의 fdtable에 접근
  2. fdtable에서 fd[fd]를 통해 struct file* 을 획득
  • 리눅스 커널 코드 일부(struct files_struct, struct fdtable)
    struct fdtable {
        unsigned int max_fds;
        struct file __rcu **fd;   // fd 배열: fd 번호 -> struct file*
        unsigned long *close_on_exec;
        unsigned long *open_fds;
        ...
    };
    
    struct files_struct {
        ...
        struct fdtable __rcu *fdt;   // 현재 사용중인 fdtable
        struct fdtable fdtab;        // 기본 내장 fdtable
        ...
        struct file __rcu * fd_array[NR_OPEN_DEFAULT];
    };

열린 상태 객체를 찾은 이후에는?

  • 커널이 열린 상태 객체를 찾았다면 이제 시스템 콜에 맞는 행동을 수행하게 된다.
    • 이 때 “열린 상태”(struct file)을 중심으로 진행된다.
  • read를 하든 write를 하든 열린 객체의 상태를 중심으로 진행하고, 참조하고 있는 inode와(또는 sock객체 등)과 함께 작업을 완료한다.

0개의 댓글