운영체제 기본/Linux/파일 시스템 콜/시스템 콜과 파일 디스크립터
응용 프로그램은 파일을 열어 파일 디스크립터(File Descriptor, fd)를 얻고, 같은 fd로 읽기·쓰기·위치 변경·닫기를 요청한다. Linux v6.12에서는 이 요청들이 파일 디스크립터 테이블과 열린 파일 객체를 공유한다. 시스템 콜마다 별도의 파일 체계를 만드는 대신, 공통 객체에서 필요한 연산을 선택한다.
파일 접근의 수명
openat(AT_FDCWD, "/home/ldmsys/a.txt", O_RDWR)
-> 경로 탐색과 열기 -> fd = 3
|
+-------------+--------------+
| | |
read(3, ...) write(3, ...) lseek(3, ...)
| | |
+------ 같은 struct file ----+
|
close(3)
|
fd 테이블의 연결 해제
openat()은 이름으로 대상을 찾아 열린 파일 객체를 만든다. 그 뒤의 read(), write(), lseek()는 fd로 이미 열린 객체를 찾으므로 매번 /home/ldmsys/a.txt를 다시 탐색하지 않는다. close()는 fd 테이블에서 그 연결을 끊는다. 열린 파일 객체를 참조하는 다른 fd나 메모리 매핑이 있으면 객체의 수명은 계속된다.
아키텍처별 시스템 콜 진입
C 함수의 인자를 커널에 전달하는 규약은 아키텍처마다 다르다. 아래는 Linux의 일반적인 64비트 사용자 ABI다. 라이브러리 래퍼가 시스템 콜 번호와 인자를 준비하며, open() 같은 라이브러리 함수가 내부적으로 openat 시스템 콜을 사용할 수도 있다.[1]
| 항목 | x86-64 | RISC-V RV64 |
|---|---|---|
| 진입 명령 | syscall |
ecall
|
| 시스템 콜 번호 | rax |
a7
|
| 인자 1~6 | rdi, rsi, rdx, r10, r8, r9 |
a0~a5
|
| 반환값 | rax |
a0
|
x86-64에서는 entry_SYSCALL_64가 커널 스택과 레지스터 상태를 준비하고 do_syscall_64()의 디스패치로 이어진다. RV64에서는 사용자 모드의 ecall 트랩이 handle_exception을 거쳐 do_trap_ecall_u()로 전달된다. 이 함수는 저장된 a7에서 번호를 읽고 다음 명령으로 돌아갈 수 있도록 저장된 PC를 진행시킨다. 아키텍처별 래퍼가 인자를 꺼낸 뒤에는 같은 fs/open.c, fs/read_write.c의 공통 구현으로 연결된다.[2][3][4][5]
시스템 콜 진입은 현재 스레드가 커널 코드를 실행하기 위한 권한 전환이다. 항상 다른 스레드로 컨텍스트 스위치하는 것은 아니다. 데이터가 준비되기를 기다려야 하면 현재 스레드가 잠들고 다른 스레드가 실행될 수 있다. 권한 전환과 스케줄링의 관계는 보호와 시스템 콜, 프로세스와 스레드에서 이어진다.
파일 디스크립터와 열린 파일 객체
current
-> files (struct files_struct)
-> fdtable
fd[3] ----> struct file
f_pos 현재 파일 위치
f_flags 열린 파일의 상태 플래그
f_path 경로와 마운트
f_inode 파일의 inode
f_op ----> struct file_operations
read / read_iter
write / write_iter
llseek / fsync / release
current는 현재 태스크이고, current->files는 그 태스크가 사용하는 파일 디스크립터 테이블로 이어진다. fd는 이 테이블의 슬롯 번호다. 다른 테이블의 fd 3은 다른 파일을 가리킬 수 있다. struct file은 파일을 한 번 연 상태를 나타내며, 파일의 메타데이터를 나타내는 inode와 구별된다. 같은 파일을 두 번 열면 대개 같은 inode를 가리키는 서로 다른 struct file을 얻는다.[6][7]
fd 조회에서는 단순히 포인터 배열을 읽는 것 외에도 다른 스레드의 닫기와 객체 수명을 고려해야 한다. fdget()은 이런 조회와 참조 처리를 제공하고, fdget_pos()는 공유된 파일 위치를 사용하는 연산에 필요한 f_pos_lock도 잡는다. 사용을 마치면 각각 fdput(), fdput_pos()로 대응한다.[8]
공통 VFS와 대상별 연산
가상 파일시스템(Virtual File System, VFS)은 열린 파일과 연산의 공통 틀을 제공한다. read()는 vfs_read()를 거쳐 f_op의 읽기 함수를 고르고, write()도 vfs_write()에서 쓰기 함수를 고른다. 이때 실제 구현은 ext4, procfs, 장치 드라이버 등에 따라 달라진다. inode·dentry·마운트의 관계는 VFS와 파일 객체에서 자세히 다룬다.
openat()은 아직 fd가 없으므로 경로 탐색에서 시작하고, close()는 fd 테이블의 연결을 제거한다. 모든 시스템 콜이 똑같은 함수열을 통과하는 것은 아니지만, 이들이 만들고 사용하는 struct file이 파일 접근의 수명을 연결한다.
참고 문헌
- ↑ Linux man-pages, open(2).
- ↑ Linux v6.12 arch/x86/entry/entry_64.S, entry_SYSCALL_64.
- ↑ Linux v6.12 arch/x86/entry/common.c, do_syscall_64, do_syscall_x64.
- ↑ Linux v6.12 arch/riscv/kernel/entry.S, handle_exception.
- ↑ Linux v6.12 arch/riscv/kernel/traps.c, do_trap_ecall_u.
- ↑ Linux v6.12 include/linux/fdtable.h, files_struct, fdtable.
- ↑ Linux v6.12 include/linux/fs.h, file, file_operations.
- ↑ Linux v6.12 fs/file.c, __fget_light, fdget_pos, file_needs_f_pos_lock.