
서비스 · 프로세스 관리 12 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
11편에서 시그널이 무엇인지 봤다. 이번에는 시그널을 보내는 도구 다. kill, pkill, killall은 모두 시그널을 보내지만 대상을 고르는 방식이 다르고, 그 차이 때문에 실무 사고가 난다. "java 프로세스 하나를 끄려다 서버의 모든 java를 껐다", "스크립트 이름으로 pkill 했더니 SSH 세션까지 끊겼다" 같은 일이다.
이번 글에서는 세 도구의 매칭 규칙 을 비교하고, pgrep으로 먼저 확인하는 안전한 작업 순서를 만든다.
| 항목 | kill | pkill | killall |
|---|---|---|---|
| 대상 지정 | PID, %작업번호 | 패턴(정규식) | 프로세스 이름 |
| 기본 매칭 대상 | — | comm (15자 이름) | comm |
| 매칭 방식 | 정확 | 부분 일치 | 정확 일치 |
| 인자 전체 검색 | — | -f | — (일부 버전 -r 정규식) |
| 사용자 지정 | — | -u USER | -u USER |
| 패키지 | util-linux / 셸 빌트인 | procps-ng | psmisc |
| 사전 확인 도구 | ps -p | pgrep (같은 옵션) | killall -i(대화형) |
| 옵션 | 의미 |
|---|---|
-a | PID와 전체 명령 출력 (pgrep) |
-l | PID와 이름 출력 |
-f | 이름이 아닌 명령 인자 전체 에서 검색 |
-x | 정확히 일치 |
-u USER | 해당 사용자 프로세스만 |
-t pts/1 | 해당 터미널 |
-n / -o | 가장 최근 / 가장 오래된 것 하나 |
-P PPID | 특정 부모의 자식 |
-e | 종료한 PID 출력 (pkill) |
-c | 개수만 |
kill -0 PID는 시그널을 보내지 않고 프로세스 존재 여부와 권한만 확인 한다. 종료 코드 0이면 존재하고 시그널을 보낼 권한이 있다는 뜻이다. 스크립트에서 PID 파일이 가리키는 프로세스가 살아 있는지 확인할 때 쓴다.

pkill -f 'sleep 1001'
↓ /proc/*/cmdline 을 모두 읽어 정규식 'sleep 1001' 검색
↓ 일치한 PID 목록 (자기 자신은 제외)
↓ 각 PID 에 kill(pid, SIGTERM)
↓ 권한 없는 PID → "Operation not permitted", 종료 코드 1
comm(이름)과 args(인자)는 다를 수 있다. exec -a 이름이나 프로그램 내부의 argv[0] 변경으로 인자 쪽 이름만 바꿀 수 있기 때문이다. 이름 기반 검색과 -f 검색의 결과가 다른 이유다.
# 1) 테스트 프로세스: 하나는 argv[0] 을 backup-job 으로 위장
sleep 1001 & sleep 1002 & (exec -a backup-job sleep 1003) &
pgrep -a sleep # 이름(comm) 기준
pgrep -a -f 'sleep 100[12]' # 인자 기준 정규식
pgrep -l backup; pgrep -a -f backup # 이름 vs 인자
pkill -f 'sleep 1001'; pgrep -a sleep
kill -0 $(pgrep -f 'sleep 1002') && echo "1002 살아 있음"
# 2) (root) 사용자 지정과 매칭 방식 차이
su - analyst -c 'sleep 2001 & sleep 2002 &' ; sleep 2003 &
pkill -u analyst -x sleep # analyst 의 sleep 만
killall -v sleep # 이름 정확 일치
sleep 2004 &
killall -v slee # 정확 일치 → 없음
pkill -e slee # 부분 일치 → sleep 종료!
# 3) (Ubuntu, analyst) 다른 사용자 프로세스
pkill -f 3001
pkill -x cron; echo "종료 코드=$?"



텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ sleep 1001 & sleep 1002 & (exec -a backup-job sleep 1003) &
[analyst@rocky9-lab ~]$ sleep 0.3; pgrep -a sleep
1156 sleep 1001
1157 sleep 1002
1158 backup-job 1003
[analyst@rocky9-lab ~]$ pgrep -a -f 'sleep 100[12]'
1156 sleep 1001
1157 sleep 1002
[analyst@rocky9-lab ~]$ pgrep -l backup; pgrep -a -f backup
1158 backup-job 1003
[analyst@rocky9-lab ~]$ pkill -f 'sleep 1001'; sleep 0.2; pgrep -a sleep
1157 sleep 1002
1158 backup-job 1003
[analyst@rocky9-lab ~]$ kill -0 $(pgrep -f 'sleep 1002') && echo "1002 살아 있음"
1002 살아 있음
[root@rocky9-lab ~]# su - analyst -c 'sleep 2001 & sleep 2002 &' ; sleep 2003 &
[root@rocky9-lab ~]# sleep 0.3; ps -o pid,user,cmd -C sleep
PID USER CMD
1242 analyst sleep 2001
1243 analyst sleep 2002
1244 root sleep 2003
[root@rocky9-lab ~]# pkill -u analyst -x sleep; sleep 0.2; ps -o pid,user,cmd -C sleep
PID USER CMD
1244 root sleep 2003
[root@rocky9-lab ~]# killall -v sleep; sleep 0.2
Killed sleep(1244) with signal 15
[root@rocky9-lab ~]# sleep 2004 & sleep 0.2
[root@rocky9-lab ~]# killall -v slee
slee: no process found
[root@rocky9-lab ~]# pkill -e slee
sleep killed (pid 1252)
analyst@ubuntu-lab:~$ sleep 3001 &
analyst@ubuntu-lab:~$ pgrep -a -f 3001
656 sleep 3001
analyst@ubuntu-lab:~$ pkill -f 3001 ; echo "pkill 종료 코드=$?"
pkill 종료 코드=0
analyst@ubuntu-lab:~$ pgrep -a -f 3001 || echo "3001 없음"
3001 없음
analyst@ubuntu-lab:~$ pkill -x cron; echo "종료 코드=$? (다른 사용자 프로세스: 권한 없음)"
pkill: killing pid 80 failed: Operation not permitted
종료 코드=1 (다른 사용자 프로세스: 권한 없음)
| 관찰 | 의미 |
|---|---|
pgrep -a sleep 결과에 1158 backup-job 1003 포함 | pgrep은 comm(sleep) 으로 찾았고, 출력(-a)은 args(backup-job 1003) 를 보여 줬다. 이름과 인자가 다른 프로세스다 |
pgrep -l backup → 결과 없음 | comm은 여전히 sleep이라 이름 검색에서는 찾을 수 없다 |
pgrep -a -f backup → 1158 | 인자 검색(-f)에서만 찾았다 |
pkill -f 'sleep 1001' 후 1002·1003만 남음 | 인자 정규식에 정확히 맞는 하나만 종료되었다 |
kill -0 → "1002 살아 있음" | 시그널 없이 존재·권한만 확인했다 |
pkill -u analyst -x sleep → root의 2003만 남음 | 사용자 조건과 정확 일치를 함께 걸어 범위를 좁혔다 |
killall -v sleep → Killed sleep(1244) with signal 15 | killall 기본 시그널도 TERM(15)이다 |
killall -v slee → no process found | killall은 이름이 정확히 같아야 한다 |
pkill -e slee → sleep killed (pid 1252) | pkill은 부분 일치 라 slee로 sleep이 종료되었다. -x 없는 pkill의 위험이다 |
Ubuntu pkill -x cron → Operation not permitted, 종료 코드 1 | analyst는 root 소유 cron에 시그널을 보낼 수 없다 |
| 주제 | 내용 |
|---|---|
| 인자 위장 | 악성 프로세스는 exec -a나 argv 덮어쓰기로 [kworker/0:1], sshd: root@pts/0 같은 이름을 흉내 낸다. comm과 args, /proc/PID/exe를 서로 비교 하면 불일치가 드러난다 (43편) |
| 권한 경계 | 일반 사용자는 다른 UID의 프로세스를 종료할 수 없다. 서비스 계정이 탈취되어도 root 서비스는 보호된다 |
| 오탐 조치 | 관제 대응 중 pkill -f 패턴은 정상 프로세스까지 종료 할 수 있다. 서비스 중단도 보안 사고다 |
| 경쟁 조건 | PID를 확인한 뒤 kill하기까지 사이에 프로세스가 끝나고 PID가 재사용될 수 있다. 확인 즉시 조치하고, 필요하면 kill -STOP으로 먼저 멈춘다 |
[조치 요청] 악성 프로세스 "kdevtmpfsi" 종료 요청
↓
[① 목록 확인] pgrep -a -x kdevtmpfsi
pgrep -a -f kdevtmpfsi ← 인자에만 있는 것도 확인
↓
[② 증거 수집] PID 별 /proc/PID/exe, cmdline, 네트워크 (05·16편)
↓
[③ 정지] kill -STOP <PID들> ← 재생성·통신 차단, 증거 보존
↓
[④ 부모 확인] ps -o ppid= -p <PID> → 부모(워치독)부터 처리
↓
[⑤ 종료] kill -KILL <PID들> ← 악성은 TERM 핸들러로 버틸 수 있다
↓
[⑥ 확인] pgrep -a -f kdevtmpfsi || echo 제거 완료
| 원칙 | 이유 |
|---|---|
| 확인 명령과 조치 명령의 조건을 똑같이 | pgrep 결과가 곧 pkill 대상이다 |
| 가능하면 PID 직접 지정 | 범위가 명확하고 보고서에 기록하기 쉽다 |
| 조치 명령을 그대로 기록 | 무엇을 종료했는지 재현·설명할 수 있어야 한다 |
| 실수 | 결과 | 예방 |
|---|---|---|
pkill java | 서버의 모든 java 서비스 종료 | pgrep -a java로 확인, PID 지정 |
pkill -f backup.sh를 SSH 세션에서 실행 | 인자에 backup.sh가 들어간 내 셸·sudo·편집기 까지 매칭될 수 있다 | -x, -u, -t로 범위 제한 |
killall을 다른 유닉스와 같다고 착각 | Solaris 등의 killall은 모든 프로세스를 종료 한다 | Linux 외 시스템에서는 사용하지 않는다 |
이름이 15자를 넘는 프로세스를 -x로 검색 | comm은 15자로 잘려 불일치 | pgrep -f 또는 잘린 이름 사용 |
| 종료 코드 확인 없이 다음 단계 진행 | 권한 오류로 실제로는 안 죽었을 수 있다 | $?와 재확인 |
[ ] pgrep 의 이름 검색과 -f 인자 검색 결과 차이를 확인했다
[ ] exec -a 로 바뀐 args 와 comm 이 다른 것을 확인했다
[ ] kill -0 으로 존재 여부를 확인했다
[ ] pkill -u, -x 로 대상을 좁혔다
[ ] killall 정확 일치와 pkill 부분 일치 차이를 확인했다
[ ] 다른 사용자 프로세스에 대한 Operation not permitted 를 확인했다
-f는 인자 전체다. 둘은 다를 수 있다.pgrep -a로 먼저 확인 한다.다음 글 「13. SIGTERM과 SIGKILL — 안전한 종료」 에서는 왜 kill -9를 먼저 쓰면 안 되는지를 락 파일이 남는 실험 으로 확인하고, systemctl stop이 내부적으로 TERM → 타임아웃 → KILL 순서로 동작하는 과정을 로그로 추적한다.