본문 바로가기
빌드노트 검색

빌드노트

flock 잠금에 실패했는데 출력 파일이 0바이트가 됐다

flock의 비차단 잠금에 실패해도 바깥 셸의 >는 이미 출력 파일을 비울 수 있다. 같은 잠금을 점유한 상태에서 종료 코드와 파일 바이트를 비교하고, 데이터 파일을 여는 리다이렉션을 잠금 성공 뒤로 옮긴다.

flock -n으로 중복 실행을 막았는데, 잠금을 얻지 못한 실행 때문에 출력 파일이 비어 버릴 수 있다. flock -n job.lock ./make-report >report.txt처럼 쓰고 있다면 바깥 셸이 report.txt를 여는 시점부터 확인한다. 셸은 flock을 실행하기 전에 >를 처리한다. 잠금 실패가 확인됐을 때는 이미 기존 파일을 0바이트로 줄인 뒤다.

수정할 것은 데이터를 여는 위치다. 출력 명령과 >report.txt를 함께 잠금 성공 뒤에 실행되도록 옮긴다. 예를 들어 flock -n -E 75 job.lock bash -c 'exec ./make-report >report.txt' 형태다. 여기서 ./make-report는 독자가 사용 중인 실행 파일 경로를 뜻한다. 아래 비교에서는 이 명령 대신 printf로 네 바이트만 출력한다.

잠금 실패와 파일 보존은 다른 결과였다

새 임시 폴더에서 한 프로세스가 별도의 job.lock을 점유하게 하고, 경쟁 명령 두 개를 실행했다. 각 실행 전 출력 파일을 OLD와 줄바꿈, 합계 4바이트로 되돌렸다. 실제 서비스의 장애 기록이 아니라 파일을 여는 시점을 비교하기 위한 격리 실험이다.

직접 비교하려면 Linux의 Bash, util-linux의 flock, 그리고 mktemp, sleep, wc, cat이 필요하다. 쓰기 가능한 로컬 파일시스템에서 진행한다. 두 터미널 모두 bash --noprofile --norc로 새 Bash를 열고, 아래 명령을 대화형으로 입력한다. 실패 즉시 종료하는 set -e와 기존 파일 덮어쓰기를 거부하는 noclobber는 켜지 않은 조건이다.

첫 터미널에서는 새 폴더의 잠금을 최대 60초 동안 점유한다. 출력된 폴더 경로를 두 번째 터미널에서 사용할 수 있다.

lab=$(mktemp -d) || exit 1
printf '%s\n' "$lab"
cd "$lab" || exit 1
flock job.lock bash -c 'printf "LOCKED\n"; exec sleep 60'

첫 창에 LOCKED가 나온 뒤, 두 번째 터미널에서 cd 뒤에 첫 창이 출력한 실제 절대경로를 붙여 같은 폴더로 이동한다. 그곳에서 다음 명령으로 바깥 리다이렉션과 잠금 내부 리다이렉션을 비교한다. rc=$?는 각 flock 바로 다음 줄에서 종료 코드를 저장한다.

printf 'OLD\n' >report.txt
flock -n -E 75 job.lock printf 'NEW\n' >report.txt
rc=$?
printf 'outside: rc=%s bytes=%s\n' "$rc" "$(wc -c <report.txt)"

printf 'OLD\n' >report.txt
flock -n -E 75 job.lock bash -c 'printf "NEW\n" >report.txt'
rc=$?
printf 'inside: rc=%s bytes=%s\n' "$rc" "$(wc -c <report.txt)"
cat report.txt

잠금이 유지된 상태에서 실제로 얻은 출력은 다음과 같다.

outside: rc=75 bytes=0
inside: rc=75 bytes=4
OLD

-n은 잠금을 바로 얻지 못하면 기다리지 않고 실패하게 한다. -E 75는 그 충돌의 종료 코드를 75로 정한 옵션이다. 이 옵션을 생략하면 충돌 코드는 기본값 1이다. util-linux 2.39.3의 flock 매뉴얼 (새 탭에서 열림)에 명시된 동작이다. 75를 파일 보존의 증거로 삼을 수는 없다. 위 두 실행의 종료 코드는 같고 남은 데이터는 달랐다.

첫 창의 60초가 지났거나 잠금을 다른 폴더에 잡았다면 경쟁 명령이 종료 0으로 성공할 수 있다. 이 경우에는 잠금 실패를 비교한 결과가 아니다. 첫 창의 점유 상태와 두 창의 작업 경로를 먼저 확인한다. 파일 열기 오류가 나거나 종료 코드가 75와 다르다면 stderr도 읽어 권한·경로·명령 실행 오류를 구분한다.

0바이트로 만드는 쪽은 바깥 셸이었다

Debian이 배포하는 GNU Bash 5.2 매뉴얼의 REDIRECTION 절 (새 탭에서 열림)은 명령 실행 전에 리다이렉션을 처리하며, 일반적인 >는 기존 파일을 크기 0으로 줄인다고 설명한다. noclobber를 켠 경우에는 기존 일반 파일에 대한 >가 실패하므로 여기의 실행 조건과 구분해야 한다.

바깥에 >report.txt가 있는 명령의 순서는 이렇다. 셸이 파일을 쓰기용으로 열어 내용을 비운다. 이어 실행된 flock이 job.lock의 잠금을 시도하고 실패한다. 그 뒤의 출력 명령은 실행되지 않는다. 별도 표식 파일을 쓰는 대조 실험에서도 출력 명령의 미실행을 확인했지만, report.txt는 이미 비어 있었다.

반대로 bash -c 'printf "NEW\n" >report.txt'는 잠금에 성공했을 때 flock이 실행할 명령이다. 작은따옴표 안의 >는 바깥 Bash가 지금 처리할 리다이렉션이 아니라 안쪽 Bash에 전달할 명령 문자열이다. 충돌하면 안쪽 Bash 자체를 실행하지 않으므로 데이터 파일도 열지 않는다. flock(1)의 설명과 예제 (새 탭에서 열림)는 명령 실행을 잠금 안에 두는 이 사용법을 설명한다.

첫 터미널에서 Ctrl+C로 점유 명령을 끝내거나 60초 종료를 기다린 뒤, 두 번째 창에서 수정한 명령을 다시 실행하면 성공 경로도 확인할 수 있다.

flock -n -E 75 job.lock bash -c 'printf "NEW\n" >report.txt'
rc=$?
printf 'released: rc=%s bytes=%s\n' "$rc" "$(wc -c <report.txt)"
cat report.txt

잠금을 해제한 뒤의 실제 출력은 released: rc=0 bytes=4와 다음 줄의 NEW였다. 두 실패 실행이 남긴 바이트도 따로 확인했다. 바깥 리다이렉션은 빈 파일, 안쪽 리다이렉션은 4f 4c 44 0a, 해제 후 성공은 4e 45 57 0a였다. 각각 빈 내용, OLD\n, NEW\n에 해당한다.

기존 스크립트에서는 데이터를 여는 모든 >가 잠금 성공 뒤에 있는지 읽는다. 내부를 고쳤어도 호출하는 쪽에 >report.txt가 남아 있으면 그 셸이 먼저 파일을 연다. 잠금용 job.lock과 결과용 report.txt는 별개로 유지하고, 경쟁 실행도 같은 잠금 파일을 사용해야 한다. >를 >>로 바꾸면 결과가 누적되는 다른 동작이 되므로, 결과를 교체하려는 명령의 수정으로 대신하지 않는다.

관찰 환경은 WSL2의 Ubuntu 24.04.4, Bash 5.2.21, util-linux 2.39.3이며 실험 파일은 Linux 쪽 ext4에 두었다. 확인한 범위는 같은 잠금 파일을 사용하는 프로세스 사이에서 잠금 실패 전후의 파일 내용 변화다. 잠금 파일의 inode 교체와 네트워크 파일시스템은 시험하지 않았다. 잠금 성공 후 출력 프로그램이 중간에 실패했을 때 기존 완성본을 보존하는 문제도 이 수정만으로 해결됐다고 볼 수 없다.

← 전체 글 보기