QUESTION
fcntl과 ioctl은 둘 다 fd를 받는데 무엇을 제어하는가?
fcntl은 fd slot flag, open file status flag, duplication, byte-range lock처럼 VFS가 공통으로 이해하는 제어를 제공한다. ioctl은 terminal, block device, network device 등 subsystem별 command ABI로 내려간다.
전통적인 POSIX record lock은 process-associated라 같은 process가 해당 inode의 어떤 fd 하나를 close해도 lock에 영향이 생길 수 있다. Linux open-file-description lock은 struct file과 연결돼 thread/process 간 ownership을 더 명확히 할 수 있다.
STRUCTURE
구조 그림
ioctl argument는 이 lock map과 달리 device UAPI 구조체이며 command별 size·direction·version을 따로 검증한다.
lock은 파일 전체가 아니라 [start, start+len) 범위다. 겹치지 않는 read lock은 공존할 수 있지만 write lock과 겹치면 충돌한다.
CALL PATH
호출 흐름
command 번호만 기록하지 말고 argument 구조체의 크기, 방향, ownership과 lock이 어느 객체에 붙는지 함께 기록한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| fs/fcntl.c | do_fcntl(), f_dupfd() | fcntl command 분기와 fd/open-file flag 구분 |
| fs/locks.c | fcntl_setlk(), locks_lock_inode_wait() | record lock 충돌, 대기, owner |
| fs/ioctl.c | sys_ioctl(), do_vfs_ioctl() | 공통 ioctl과 file operation dispatch |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 lock_range.c -o lock_range01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <stdio.h>
04#include <string.h>
05#include <unistd.h>
06
07int main(int argc, char **argv)
08{
09 if (argc != 2)
10 return 2;
11 int fd = open(argv[1], O_RDWR | O_CREAT | O_CLOEXEC, 0644);
12 if (fd < 0)
13 return 1;
14
15 struct flock lock;
16 memset(&lock, 0, sizeof(lock));
17 lock.l_type = F_WRLCK;
18 lock.l_whence = SEEK_SET;
19 lock.l_start = 0;
20 lock.l_len = 16;
21
22 if (fcntl(fd, F_OFD_SETLKW, &lock) < 0) {
23 perror("F_OFD_SETLKW");
24 return 1;
25 }
26 puts("locked bytes [0, 16); press Enter");
27 getchar();
28 lock.l_type = F_UNLCK;
29 int rc = fcntl(fd, F_OFD_SETLK, &lock);
30 close(fd);
31 return rc < 0;
32}
CODE NOTES
코드 조각별 설명
memset(&lock, 0uapi 구조체의 padding과 사용하지 않는 field까지 0으로 만들어 architecture별 쓰레기 값을 넘기지 않는다.
lock.l_whence = SEEK_SETbyte range 기준을 파일 시작으로 고정한다. SEEK_CUR이면 공유 file offset 변화가 lock range 계산에 영향을 줄 수 있다.
lock.l_len = 16[0,16) 구간만 advisory write lock을 건다. 0이면 시작 위치부터 EOF 이후 성장 구간까지를 뜻한다.
F_OFD_SETLKW충돌이 풀릴 때까지 대기하는 Linux OFD lock이다. ownership은 process PID가 아니라 open file description에 붙는다.
lock.l_type = F_UNLCK같은 owner와 range로 명시적 unlock한다. 마지막 struct file reference close에서도 OFD lock은 해제된다.
DETAILS
세부 동작
advisory lock은 I/O를 강제로 막지 않는다
협력하는 process가 lock protocol을 지킬 때만 보호된다. lock을 얻지 않고 read/write하는 process를 VFS가 일반적으로 차단하지 않는다.
database나 log writer는 lock 범위와 acquire order를 application protocol에 포함해야 한다.
ioctl request number도 ABI다
_IO, _IOR, _IOW, _IOWR macro는 type, number, direction, size를 encoding하지만 kernel이 pointer 유효성과 구조체 version을 자동으로 해결해 주는 것은 아니다. compat task에는 32/64-bit layout 변환이 필요할 수 있다.
driver private ioctl을 설계할 때 fixed-width type, padding, reserved field, size/version 규칙을 둔다.
fd 종류별 지원 여부를 먼저 확인한다
regular file, pipe, socket, tty가 같은 command를 이해하지 않는다. ENOTTY는 fd가 terminal이 아니라는 뜻뿐 아니라 해당 ioctl command를 구현하지 않는다는 일반 반환으로도 사용된다.
fstat와 subsystem query로 type을 확인하고 command 실패를 장치 부재와 ABI mismatch로 나눈다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
file_lock | fcntl lock 요청에서 만들어져 inode lock tree/list에 연결된다 | owner, range, type |
OFD owner | struct file 수명과 함께 유지되며 dup/fork 공유 특성이 있다 | fl_owner, last file ref |
ioctl argument | syscall 동안 user memory에 있고 driver가 copy_from/to_user한다 | size, alignment, reserved field |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| lock을 걸어도 다른 writer가 씀 | 상대가 advisory protocol 미준수 | 상대 fcntl 호출과 I/O 경로 |
| 예상보다 lock이 일찍 풀림 | POSIX lock과 close semantics 혼동 | F_SETLK/F_OFD_SETLK 구분 |
| ioctl ENOTTY | 잘못된 fd type 또는 command 미지원 | fstat, driver version, UAPI header |
LAB
직접 확인
- 두 terminal에서 예제를 실행해 두 번째 process가 첫 번째 unlock까지 block되는지 확인한다.
- F_OFD_SETLKW를 F_SETLKW로 바꾸고 dup fd/별도 open fd를 close할 때 lock 수명을 비교한다.
- 터미널 fd에 TIOCGWINSZ ioctl을 호출하고 redirected stdin에서 ENOTTY가 되는 조건을 확인한다.
./lock_range locked.datstrace -e trace=openat,fcntl,ioctl,close ./lock_range locked.datPRIMARY REFERENCES