← System Programming DUJINLABS.COM

Signal / Event / IPC · Linux userspace / kernel ABI

inotify와 directory 변경 추적

watch descriptor와 pathname 수명을 구분하고 rename cookie, queue overflow, recursive watch 누락을 복구 가능한 protocol로 다룹니다.

Series
26 / 37
Build
cc -std=c17 -Wall -Wextra -O2 watch_dir.c -o watch_dir
Run
./watch_dir .
Kernel
Linux 6.18.37 LTS

inotify event만 모으면 directory의 현재 상태를 항상 복원할 수 있는가?

inotify는 filesystem object 변화의 event stream을 제공하지만 완전한 transaction log나 recursive snapshot은 아니다. queue가 넘치면 IN_Q_OVERFLOW 한 건만 남고 어떤 변화가 빠졌는지 알 수 없어 전체 rescan이 필요하다.

directory watch는 현재 directory 자체와 바로 아래 entry event를 보고할 뿐 새로 생긴 하위 directory에 자동 watch를 추가하지 않는다. event 처리와 watch 추가 사이의 변경을 다루는 재검사 절차가 필요하다.

구조 그림

그림 1. watch tree와 하나의 inotify event queue
inotify instance fdgroup queue · max_queued_events · overflow flag

wd=1 /project

  • IN_CREATE
  • IN_MOVED_*
  • child name record

wd=2 /project/src

  • 별도 watch mark
  • inode event
  • rename cookie

새 /project/build

  • CREATE|ISDIR
  • scan 필요
  • add_watch 필요

queue

  • wd/mask/cookie/name
  • 가변 길이 record
  • IN_Q_OVERFLOW → rescan

watch는 recursive하지 않다. 새 subdirectory에는 별도 mark가 필요하고 queue overflow 뒤에는 전체 tree를 다시 scan해야 한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
inotify_add_watch path를 wd에 연결
fsnotify mark inode/mount에 등록
filesystem change event 생성
read records 가변 길이 batch
reconcile overflow/rename 복구

event를 최종 상태로 쓰지 말고 rescan을 줄이는 invalidation hint로 사용한다. event sequence와 실제 directory scan 사이 일관성 요구를 따로 정의한다.

그림 3. 커널 내부에서 지나가는 주요 지점
fsnotify hook inode operation
inotify_handle_inode_event mask/name 구성
group queue event merge/limit
inotify_read user buffer copy
wd lookup application state 결합

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
fs/notify/inotify/inotify_user.c inotify_add_watch(), inotify_read() wd 생성과 가변 길이 event 반환
fs/notify/inotify/inotify_fsnotify.c inotify_handle_inode_event() fsnotify event를 inotify 형식으로 변환
fs/notify/notification.c fsnotify_add_event() group queue limit와 overflow 처리

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 watch_dir.c -o watch_dir
01#define _GNU_SOURCE
02#include <errno.h>
03#include <stdio.h>
04#include <sys/inotify.h>
05#include <unistd.h>
06
07int main(int argc, char **argv)
08{
09    const char *path = argc > 1 ? argv[1] : ".";
10    int fd = inotify_init1(IN_CLOEXEC);
11    if (fd < 0)
12        return 1;
13    int wd = inotify_add_watch(fd, path,
14        IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_Q_OVERFLOW);
15    if (wd < 0)
16        return 1;
17
18    _Alignas(struct inotify_event) char buffer[8192];
19    for (;;) {
20        ssize_t count = read(fd, buffer, sizeof(buffer));
21        if (count < 0 && errno == EINTR)
22            continue;
23        if (count <= 0)
24            break;
25        for (char *p = buffer; p < buffer + count; ) {
26            struct inotify_event *event = (struct inotify_event *)p;
27            printf("wd=%d mask=0x%x cookie=%u name=%s\n",
28                   event->wd, event->mask, event->cookie,
29                   event->len ? event->name : "-");
30            p += sizeof(*event) + event->len;
31        }
32    }
33    close(fd);
34    return 0;
35}

코드 조각별 설명

실제 코드 10행inotify_init1(IN_CLOEXEC)

event queue를 fd로 만들고 exec 상속을 막는다. epoll에 합칠 경우 IN_NONBLOCK도 함께 사용한다.

실제 코드 14행IN_MOVED_FROM | IN_MOVED_TO

같은 inotify instance 안의 rename 양쪽 event는 cookie로 짝지을 수 있다. 다른 filesystem 이동은 create/delete처럼 보일 수 있다.

실제 코드 18행_Alignas(struct inotify_event)

char buffer가 inotify_event를 읽기에 필요한 정렬을 갖도록 한다.

실제 코드 25행p < buffer + count

한 read에 여러 가변 길이 record가 들어오므로 반환 byte 범위 안에서 직접 순회한다.

실제 코드 30행sizeof(*event) + event->len

event->len에는 name과 padding이 포함된다. strlen(name)만 더하면 다음 record 정렬을 잃는다.

세부 동작

01

wd는 pathname이 아니다

watch descriptor는 inotify instance 안의 정수 key다. watched object가 rename돼도 inode watch는 이어질 수 있고, IN_IGNORED 뒤 같은 wd 숫자가 재사용될 수 있다.

application map에는 wd와 generation, 현재 추정 path를 함께 두고 stale event를 구분한다.

02

rename 짝은 timeout이 필요하다

IN_MOVED_FROM을 받았지만 corresponding IN_MOVED_TO가 같은 queue에 오지 않을 수 있다. watched tree 밖으로 이동하거나 overflow가 생길 수 있기 때문이다.

cookie map entry를 무한히 보관하지 말고 짧은 timeout 뒤 delete/out-of-tree 이동으로 확정한다.

03

overflow는 전체 rescan 경계다

IN_Q_OVERFLOW 뒤에는 어떤 entry가 바뀌었는지 추론할 수 없다. queue를 비우고 authoritative directory scan으로 상태를 다시 만들고 watch set도 검증한다.

event 처리 속도, max_queued_events, 폭발적인 build output을 계측하되 limit 증가만으로 정확성 protocol을 대신하지 않는다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
inotify groupinotify_init1에서 생기고 fd close에서 queue와 mark가 해제된다queue length, overflow state
watch mark/wdadd_watch에서 inode에 붙고 rm_watch/object delete/close에서 제거된다mask, inode, generation
inotify_event recordkernel queue에서 read buffer로 복사된 뒤 application이 소비한다mask, cookie, len, name

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
변경이 누락됨IN_Q_OVERFLOW 또는 watch 추가 전 변화overflow 처리와 full rescan
rename 짝이 없음watched tree 밖 이동/queue 경계cookie timeout policy
하위 directory 변화 없음recursive watch를 자동으로 기대새 directory scan + add_watch

직접 확인

  1. 감시 directory에서 mv로 이름을 바꾸고 FROM/TO cookie가 같은지 확인한다.
  2. 짧은 시간에 대량 파일을 만들어 queue overflow를 유도하고 rescan 경로를 테스트한다.
  3. 새 하위 directory 생성 event를 받자마자 내부 scan과 watch 추가를 수행하는 recursive tracker를 구현한다.
실행./watch_dir .
추적strace -e trace=inotify_init1,inotify_add_watch,read,close ./watch_dir .

원문