빌드노트
timeout 종료 코드 124, 재실행 전에 확인할 것
GNU timeout의 124와 자식 오류·실행 실패를 실제 종료 상태와 남은 파일로 구별한다. preserve-status의 143·0, TERM 무시 후 kill-after의 137을 비교하고 재실행 전에 완료 범위와 중복 가능성을 확인한다.
GNU timeout으로 감싼 명령이 끝났는데 종료 코드가 124라면, 먼저 시간 제한에 도달했는지 확인한다. 기본 동작에서 124는 제한 도달을 알리기 위해 timeout이 반환하는 상태다. 원래 명령이 제한 전에 7로 끝났다면 7이 전달된다. 두 경우를 같은 오류로 묶어 재실행하면, 중단 전에 이미 처리한 부분을 다시 실행할 수 있다.
실행 당시의 옵션, 표준 오류, 작업 결과를 함께 봐야 한다. 자식 명령이 스스로 124를 반환할 수도 있고, --preserve-status를 쓰면 제한에 도달해도 124가 나오지 않을 수 있다. 종료 상태의 기준은 GNU Coreutils 9.4 공식 매뉴얼의 timeout 절 (새 탭에서 열림)에서 확인했다.
0·7·124와 남은 기록을 비교했다
Linux 또는 WSL의 Bash에서 GNU timeout이 필요하다. timeout --version으로 구현과 버전을 확인한다. Windows의 timeout.exe는 이 글에서 사용하는 명령과 다르다. 직접 비교한 환경은 WSL2의 Ubuntu 24.04.4, GNU coreutils 9.4, Bash 5.2.21이었다.
아래 입력은 빠른 성공, 자식의 고정 오류, 제한 초과를 비교하고 중단 전에 쓴 파일이 남는지 확인한다. 쓰기 가능한 실습 폴더에서 bash --noprofile --norc로 새 Bash를 연 뒤 실행한다. 새 하위 폴더에 작은 기록 파일 하나만 만든다. set +e는 비교 중 오류가 나도 다음 줄에서 상태를 읽을 수 있게 한다.
set +e
export LC_ALL=C
work=$(mktemp -d ./timeout-demo.XXXXXX) || exit
cd "$work" || exit
timeout 2s bash -c 'exit 0'
printf 'success=%s\n' "$?"
timeout 2s bash -c 'exit 7'
printf 'child-error=%s\n' "$?"
timeout --verbose --kill-after=0.2s 0.3s bash -c '
printf "started\n" > stage.log
sleep 3
printf "finished\n" >> stage.log
'
rc=$?
printf 'deadline=%s\n' "$rc"
cat stage.log
표준 출력은 다음과 같았다. success·child-error·deadline은 각 명령 바로 뒤에서 읽은 종료 상태이며, 마지막 started는 stage.log를 읽은 내용이다.
success=0
child-error=7
deadline=124
started
표준 오류에는 timeout: sending signal TERM to command 'bash'가 남았다. --verbose가 보낸 신호를 알려 준 것이다. 이 조건에서는 0.3초 제한에 도달해 TERM을 보냈고, sleep 뒤의 finished 기록까지 진행하지 못했다. 이미 쓴 started는 그대로 남았다. 프로세스를 중단해도 앞선 쓰기가 자동으로 취소되지는 않는다는 작은 관찰이다.
rc=$?는 timeout 바로 다음에 저장해야 한다. 중간에 cat이나 printf를 실행한 뒤 읽으면 그 명령의 상태를 보게 된다. 자신의 결과가 다르면 먼저 GNU 구현인지, 같은 옵션인지, 상태를 즉시 읽었는지 확인한다. 두 표식이 모두 보이면 이 실험처럼 중간에 멈춘 조건이 아니므로 신호 기록과 실행한 입력을 다시 대조한다. 위 예제는 합성 작업이며 실제 작업을 재시도한 결과가 아니다.
실행 실패와 시간 제한을 나누어 읽는다
별도 대조군에서는 다음 결과도 확인했다. 숫자와 함께 timeout이 남긴 표준 오류를 읽으면 조사할 위치를 좁힐 수 있다.
- 125: 시간 값으로
not-a-duration을 주자invalid time interval과 함께 반환됐다. 이 경우에는timeout호출 형식부터 고친다. - 126: 새 디렉터리를 명령 경로로 지정하자
Permission denied와 함께 반환됐다. 대상은 존재하지만 실행할 수 없는 경우다. - 127: 없는
./missing-command를 지정하자No such file or directory와 함께 반환됐다. 지정한 명령 경로부터 확인한다.
126 대조군을 처음에는 실행 비트가 없는 파일로 만들려 했지만, 이 WSL의 Windows 드라이브에서는 chmod 644 뒤에도 모드가 777로 관찰돼 파일이 실행됐다. 따라서 일반 파일의 실행 권한 부족은 재현했다고 하지 않는다. 여기서 확인한 126은 디렉터리를 실행하려 한 조건이다.
자식 자체가 곧바로 124를 반환하는 대조군도 만들었다. 제한을 보내지 않았는데 결과는 똑같이 124였고, --verbose의 신호 메시지는 없었다. 이처럼 상태 번호만으로 원인을 확정할 수는 없다. 기존 실행에 --verbose가 없었다면 메시지가 없다는 사실도 구별 근거가 되지 않는다. 원래 명령의 종료 코드 계약과 기존 실행 기록을 함께 확인한다.
상태를 보존하면 124가 사라질 수 있다
--preserve-status는 제한에 도달했을 때도 관리 대상 명령의 상태를 돌려준다. 앞의 새 Bash에서 다음 입력으로 TERM에 종료되는 sleep의 상태를 확인할 수 있다.
timeout --preserve-status --verbose --kill-after=0.2s 0.3s sleep 3
printf 'preserved=%s\n' "$?"
실제 출력은 preserved=143이었다. 표준 오류에는 sleep에 TERM을 보냈다는 메시지가 있었다. 이 Linux에서 143은 TERM 번호 15에 128을 더한 표현이다. 같은 제한을 상태 보존 없이 작은 Python 자식에 적용했을 때는 124, 상태 보존을 켰을 때는 143이었다.
차이는 143에만 그치지 않았다. TERM을 받으면 표식을 남기고 0으로 종료하도록 만든 다른 합성 자식은 기본 옵션에서 124, 상태 보존 시 0을 반환했다. 두 실행 모두 시작·신호 수신 표식만 있었고 정상 처리 끝의 완료 표식은 없었다. 이 경우의 0은 자식이 선택한 종료 상태이며, 예정한 작업 전체가 완료됐다는 증거로 쓸 수 없다.
배치에서 124로 제한 도달을 분기하고 있다면 --preserve-status를 추가할 때 그 판별도 바뀐다. 자식 상태를 그대로 전달할 목적이 있는지 먼저 정하고, 제한 도달 여부는 신호 기록 등으로 따로 남긴다.
TERM 뒤에도 살아 있으면 추가 종료 제한이 필요하다
앞 예제의 --kill-after=0.2s는 첫 종료 신호를 보낸 뒤에도 대상이 남아 있으면 추가로 KILL을 보내는 옵션이다. 0.3초는 첫 TERM까지의 제한, 0.2초는 그 신호 이후의 대기다. 실제 신호 시점은 시스템 상황에 영향을 받으므로 두 값을 더한 시간을 정확한 실행 시간으로 보장하지는 않는다.
TERM을 무시하고 3초 대기하는 합성 자식에 같은 설정을 주자 표준 오류에 TERM과 KILL이 차례로 남았다. Bash에서 읽은 종료 상태는 137이었다. Python으로 만든 표식 실험에서도 시작 파일만 남았고 완료 파일은 없었으며, 해당 자식이 종료된 것을 확인했다. --kill-after가 동작한 이 관찰을 124 처리에만 넣으면 강제 종료 경로를 놓친다.
매뉴얼은 대상 명령이나 timeout 자신이 KILL을 받아도 137이 될 수 있다고 설명한다. 따라서 137만으로 어느 쪽이 종료됐는지 구별하거나 관리 대상이 모두 사라졌다고 단정하지 않는다. 자신의 실행 관리 도구에서 해당 작업의 생존 여부도 확인해야 한다.
재실행 전에는 다음 세 가지를 기존 기록과 결과에서 확인한다.
- 원래 명령과
timeout중 어느 쪽의 상태를 읽었는지, 상태 보존·추가 종료 옵션과 신호 기록이 무엇인지 확인한다. - 작업 ID에 대응하는 결과 파일, 임시 파일, 반영 기록과 완료 지점을 조회한다. 완료 로그가 없다는 사실만으로 앞선 변경까지 없다고 판단하지 않는다.
- 같은 입력을 다시 처리해도 중복이 생기지 않는지, 이어서 처리할 지점이나 복구 절차가 있는지 확인한 뒤 재실행 여부를 정한다.
직접 실행한 범위는 네트워크 연결 없는 작은 자식과 같은 실습에서 만든 파일뿐이다. 분리된 세션, 원격 작업, 실제 DB 변경의 취소·중복 방지, 운영 작업 재시도는 검증하지 않았다. 종료 코드는 중단 이유를 좁히는 단서이고, 다시 실행해도 되는지는 남은 작업 상태로 판단한다.