Featured image of post [Bash Shell] 25. kill, jobs - 시그널과 작업 제어

[Bash Shell] 25. kill, jobs - 시그널과 작업 제어

kill은 프로세스에 시그널을 보내 종료·제어하고, jobs·fg·bg는 현재 셸이 띄운 작업을 확인하며 포그라운드·백그라운드로 전환한다. SIGTERM·SIGKILL 차이와 작업 번호·PID 문법의 함정을 실전 예제로 다룬다.

이 장을 읽기 전에

직전 챕터인 24장: top에서는 화면이 주기적으로 갱신되는 도구로 CPU·메모리를 많이 쓰는 프로세스를 찾는 법을 다뤘다. “무엇이 문제인지"를 찾아냈다면, 이 장에서는 그 프로세스에 실제로 개입하는 두 가지 별개의 도구를 다룬다. 이 장이 전제하는 지식은 PID가 무엇인지, ps/top으로 프로세스 목록을 확인하는 법 정도다. 별도의 셸 스크립팅 지식은 필요 없다. 난이도는 입문–중급이며, 시그널의 정확한 의미와 좀비 프로세스가 처리되는 방식에서 약간의 시스템 지식이 필요하다.

다루지 않는 것: 터미널 세션 자체가 끊겨도 백그라운드 프로세스를 계속 살려두는 방법은 26장: nohup에서 다룬다. 스크립트 안에서 시그널을 가로채 정리 작업을 실행하는 trap은 33장(종료 코드와 set -e/-x, trap)에서 다룬다.

당신의 수준에 맞는 경로

수준읽을 부분핵심 목표
입문개요+정신 모델, 사용법·옵션의 기본 예시(1–4번)kill PID로 프로세스를 정상 종료하고, jobs로 현재 셸의 작업 목록을 확인할 수 있다
중급fg/bg 전환 예시, kill %N vs kill PID 구분Ctrl+Z로 중지한 작업을 bg/fg로 전환하고, 작업 번호와 PID를 혼동하지 않고 쓸 수 있다
심화주의사항·함정, 흔한 오개념SIGTERM을 먼저 시도해야 하는 이유, 좀비 프로세스가 kill로 해결되지 않는 이유를 설명할 수 있다

개요 + 정신 모델

프로세스 제어에는 서로 다른 두 축이 있다. 하나는 kill프로세스에 시그널을 보내는 것이다. 이름과 달리 kill은 “죽인다"는 동작 하나만을 의미하지 않는다. 커널에 등록된 임의의 프로세스에게 우편으로 봉투(시그널)를 배달하는 우체국에 가깝고, 봉투를 열어본 프로세스가 그 시그널에 어떻게 반응할지는 원칙적으로 프로세스 자신이 결정한다 — 기본 동작대로 종료할 수도, 무시할 수도, 직접 등록한 처리 함수(핸들러)를 실행할 수도 있다. 예외는 SIGKILL(9)과 SIGSTOP(19)뿐인데, 이 둘은 프로세스가 가로채거나 무시할 수 없도록 커널이 강제한다.

다른 하나는 jobs·fg·bg·Ctrl+Z현재 셸이 띄운 작업(Job)을 대화형으로 관리하는 것이다. 이 작업 목록은 kill이 다루는 시스템 전역 PID 테이블과는 완전히 별개의 장부로, 지금 로그인한 그 셸 프로세스 하나가 자신이 실행시킨 자식 프로세스를 추적하려고 내부적으로 유지하는 정보다. 그래서 작업 번호(%1, %2…)는 그 셸 세션 안에서만 의미가 있고, 다른 터미널을 열거나 그 셸이 종료되면 작업 목록도 함께 사라진다. kill은 시스템의 아무 프로세스에나(권한이 있다면) 시그널을 보낼 수 있는 저수준 도구이고, jobs/fg/bg는 그중에서도 지금 이 셸이 대화형으로 실행한 것만 다루는 고수준 편의 기능이라고 구분하면 둘의 관계가 명확해진다.

사용법 · 옵션

kill — 시그널 전송

1
2
kill [-시그널] PID...
kill -l [시그널번호]

-시그널은 시그널 번호(-9) 또는 이름(-TERM, -SIGTERM 모두 가능)으로 지정한다. 생략하면 기본값인 SIGTERM(15)이 전송된다.

시그널번호*기본 동작용도
SIGTERM15종료 (가로채기 가능)정상 종료 요청. kill의 기본값
SIGKILL9즉시 종료 (가로채기 불가)최후의 수단. 정리 작업 없이 강제 종료
SIGHUP1종료 (가로채기 가능)원래는 “터미널 연결 끊김”, 데몬에서는 관용적으로 “설정 재적재” 신호로도 씀
SIGINT2종료 (가로채기 가능)Ctrl+C와 동일한 인터럽트 요청
SIGQUIT3종료+코어 덤프Ctrl+\와 동일
SIGSTOP19일시 정지 (가로채기 불가)커널이 강제로 실행을 멈춤
SIGCONT18재개정지된 프로세스를 다시 실행

* 번호는 x86/ARM 리눅스 기준이며, POSIX는 시그널 이름만 표준화하고 번호는 구현에 따라 달라질 수 있는 값으로 남겨둔다. 이식성이 필요하면 번호 대신 이름(-TERM, -KILL)을 쓰는 편이 안전하다. 시스템에 정의된 전체 시그널 목록은 kill -l로 확인한다.

jobs · fg · bg — 작업 제어

1
2
3
jobs [-l]
fg [%작업]
bg [%작업]

jobs는 현재 셸의 백그라운드·정지된 작업 목록을 보여주고, -l을 붙이면 PID도 함께 표시한다. fg는 지정한 작업(생략 시 현재 작업)을 포그라운드로 가져오고, bgCtrl+Z로 정지된 작업을 백그라운드에서 이어 실행한다. 작업 지정자(%작업)는 다음 형식을 쓸 수 있다.

지정자의미
%N작업 번호 N
%%, %+현재(가장 최근) 작업
%-바로 이전 작업
%문자열명령행이 해당 문자열로 시작하는 작업

예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 1. PID를 지정해 기본 시그널(SIGTERM)로 정상 종료 요청
kill 12345

# 2. 번호 대신 이름으로 명시 (가독성 우선)
kill -TERM 12345

# 3. 정상 종료가 안 먹힐 때 최후 수단으로 강제 종료
kill -9 12345

# 4. 시스템에 정의된 시그널 전체 목록 확인
kill -l

# 5. 장시간 명령을 실행하다 Ctrl+Z로 잠시 멈추고, 셸로 제어권 돌려받기
sleep 300
# Ctrl+Z 입력 → [1]+  Stopped   sleep 300

# 6. 정지된 작업을 백그라운드에서 계속 실행
bg %1

# 7. 백그라운드 작업을 다시 포그라운드로 가져오기
fg %1

# 8. 작업 번호와 PID를 함께 확인
jobs -l

# 9. 이름으로 PID를 찾아 그 PID에 종료 시그널 전송 (jobs가 아니라 kill의 대상은 PID)
kill "$(pgrep -f my-script.sh)"

# 10. 여러 PID를 한 번에 종료
kill -TERM 1111 2222 3333

주의사항 · 함정

SIGKILL(kill -9)은 최후의 수단이어야 한다. SIGTERM(기본값, kill -15)을 받은 프로세스는 열려 있는 파일을 닫고 임시 파일을 지우고 자식 프로세스를 정리하는 등 자신이 등록한 종료 처리 로직을 실행할 기회를 얻는다. 반면 SIGKILL은 프로세스에 전달되지 않고 커널이 즉시 프로세스를 회수해버리므로, 정리 코드가 아예 실행되지 않는다. GNU coreutils와 POSIX의 kill 문서 모두 기본 시그널을 SIGTERM으로 규정하는 이유가 여기 있다 — 먼저 SIGTERM으로 정상 종료를 시도하고, 일정 시간이 지나도 응답이 없을 때만 SIGKILL로 넘어가는 것이 관례다.

kill %1kill 1234는 완전히 다른 문법이다. %1은 “작업 번호 1"이고 1234는 “PID 1234"다. 겉보기엔 비슷해 보이지만 %가 있고 없고에 따라 kill이 참조하는 장부 자체가 바뀐다 — %가 붙으면 현재 셸의 작업 목록에서, 없으면 시스템 전역 PID 테이블에서 대상을 찾는다. 습관적으로 %를 빼먹고 kill 1처럼 쓰면, 의도한 작업 1번이 아니라 우연히 그 번호를 가진(또는 존재하지 않는) 전혀 다른 PID에 시그널을 보내게 된다. 스크립트에서 작업 번호를 다룰 때는 항상 %를 명시적으로 확인하는 습관이 안전하다.

좀비 프로세스는 kill로 해결되지 않는다. 좀비(Z 상태)는 이미 실행이 끝나 커널이 회수할 자원이 남아 있지 않은 프로세스다. 남아 있는 것은 종료 상태 코드 같은 최소한의 기록뿐이며, 이 기록은 부모 프로세스가 wait() 계열 시스템 콜을 호출해 자식의 종료를 “확인"해야 비로소 프로세스 테이블에서 지워진다. 이미 죽은 프로세스에는 보낼 시그널이 없으므로 좀비를 향한 kill -9는 아무 효과가 없다 — 근본 원인은 부모가 wait()를 호출하지 않는 버그이며, 해결하려면 부모 프로세스를 고치거나(직접 고칠 수 없다면 재시작해) 부모가 종료되게 만들어야 한다(부모가 사라지면 좀비는 init/systemd에게 입양되어 정리된다).

작업 목록은 그 셸 세션에만 존재한다. jobs가 보여주는 목록은 지금 이 셸 프로세스가 기억하는 자식 목록일 뿐이므로, 다른 터미널 탭이나 SSH 세션에서는 같은 작업이 보이지 않는다. 셸을 종료하면 백그라운드 작업에도 기본적으로 SIGHUP이 전달되어 함께 종료된다(터미널 연결이 끊겼다는 신호이므로) — 이 동작을 피하는 방법은 다음 장인 nohup에서 다룬다.

흔한 오개념

"kill은 무조건 프로세스를 죽인다"는 오해다. 이름 때문에 생기는 가장 흔한 오해로, kill은 시그널을 “전달"할 뿐 그 시그널을 어떻게 처리할지는 수신 프로세스에 달려 있다. SIGSTOP은 실행을 멈출 뿐 종료시키지 않고, SIGCONT는 오히려 프로세스를 재개시키며, SIGHUP은 nginx나 각종 데몬에서 “설정 파일을 다시 읽어라"는 신호로 재해석되어 쓰인다. kill -HUP <nginx PID>가 nginx를 재시작이 아니라 무중단 설정 재적재로 동작하는 것이 대표적인 예다.

"jobs/kill이 같은 목록을 본다"는 오해다. jobs가 보여주는 작업 번호(%1)는 셸이 자체적으로 매기는 번호일 뿐, 커널이 관리하는 PID와는 다른 번호 체계다. 그래서 bash에서 killjobs/bin/kill 같은 외부 실행 파일이 아니라 셸 내장 명령(builtin)으로 구현되어 있다 — type kill을 실행해보면 kill is a shell builtin이라는 결과를 확인할 수 있다. 내장 kill만이 %1 같은 작업 지정자 문법을 이해하며, /bin/kill처럼 PATH에서 찾아 실행되는 외부 kill 바이너리는 PID만 받아들이고 %1을 이해하지 못한다.

다음 장에서는

26장: nohup에서는 터미널 세션 자체가 끊기거나 SIGHUP이 전달되어도 백그라운드 작업이 계속 실행되게 만드는 법을 다룬다.

평가 기준

  • kill이 “프로세스 종료"만이 아니라 임의의 시그널을 전달하는 도구라는 것을 설명할 수 있다.
  • SIGTERM을 먼저 시도하고 SIGKILL을 최후 수단으로 남겨야 하는 이유를 프로세스의 정리 작업 관점에서 설명할 수 있다.
  • kill %1(작업 번호)과 kill 1234(PID) 문법의 차이를 구분하고, %를 빠뜨렸을 때의 위험을 설명할 수 있다.
  • jobs·fg·bg로 현재 셸의 작업을 확인하고 포그라운드·백그라운드 사이를 전환할 수 있다.
  • 좀비 프로세스가 kill로 해결되지 않는 이유와, 부모 프로세스의 wait() 호출이 왜 필요한지 설명할 수 있다.

참고