🐮 process-tree 정리의 rhapsody
발단
-
상황배경:
-
…그런데…
- 갑자기 Control-C(
SIGINT)을 눌러 내 프로그램을 종료했다. - 😕 yt-dlp 프로세스와 그 하위 aria2c 프로세스 10여개는 종료되었을까?
- 갑자기 Control-C(
… yt-dlp, aria2c에 기대하는 behavior은: 그렇다-이다.
그런데 그렇지 않을 경우도 있을테고(순전히 각 애플리케이션의 처리에 따를테니까, 플랫폼(예: linux / freebsd)에 따라 그렇게 되지 않는 경우도 있다. 😑
그렇다면, 이런 상황을 제대로 처리하는 "내 프로그램"을 만들 수 있을까? 🤔
[접근법] 하위프로세스를 검색하여 다 정리
너무 당연해 보이고, 심지어 자연스러운 발상에 기반한 접근법.
모든 프로세스-는 자신의 PID (Process ID)-와 부모프로세스의 PPID
(Parent-)을 갖고 있으니, 내 프로그램의 하위프로세스 PID와 그 PID을
부모로 하는 모든 프로세스를 찾아서 종료하면 되겠지.
그런데… 그 "모든 프로세스", 혹은 "자식 프로세스"를 enumerate하는 일이 플랫폼마다 다르다…🌋 baam! 히밤쾅.
-
Python도 "배터리" 표준라이브러리에 없고, psutil 사용
- 어째서, 별거 아닌거 같은 라이브러리가 gh-stars⭐이 1.1만개나 받았을까…하고 내가 하려는 짓을 의심해 보아야 한다.
-
"Elegantly get list of descendant processes" 스택오버플로우
- "우아하게" 하위프로세스 목록을 구하는 방법에 대해 물었더니,
ps,pstree,pgrep등의 유닉스 도구를 실행해서 그 출력을 파싱하라는 "우아한가?" 싶은 답변만 달려 있다.
- "우아하게" 하위프로세스 목록을 구하는 방법에 대해 물었더니,
실은, 정답은 아마 이럴 것 같다:
-
리눅스라면,
- procfs
/proc-을 디렉토리/파일 순회를 하면서 알아서 PID/PPID 정보를 모아서 사용해라. - https://docs.kernel.org/filesystems/proc.html :
/proc/${PID}/status파일에Pid:,PPid:필드가 친절하게 있다.🤣
- procfs
-
FreeBSD 쪽이라면,
-
리눅스와 같이 procfs을 지원한다!
- 하지만, 이걸 설정해놓는건 선택적 사항이므로… 🚷 https://man.freebsd.org/cgi/man.cgi?procfs
kvm_getnprocs(3)함수를 사용한다.
-
- Windows이라면:
EnumerateProcesses함수를 사용한다. -
Mac OS X이라면,
-
NSWorkspace/NSRunningApplication클래스를 사용한다. (AppKit) - BSD
sysctl-을 쓰는 방법도 있는거 같지만, 이식성과 편의성 면에서는 …AppKit 쓰는거보다 딱히 나을게 없는거 같아서 …넘어가자.
-
그렇다. psutil 같은 별도 패키지가 아예 따로 있고, "Cross-platform!"이라고 마빡에 써붙인데에는 다 이유가 있던 것이다.
개인적으로 선호하는 방식은(빈정거리기는 했었어도), 특별한 시스템 호출이 필요 없는 procfs 방식으로 노출하는게 맞다고 생각한다. https://en.wikipedia.org/wiki/Procfs …어쨌든 userland 구현 라이브러리에서 호환성 생각 않고 접근하기도 좋으니까.
내가 좋아하는 것과 상관없이, 현실에선 모든 플랫폼에 적용하기 어렵다.
그런데, 이런 "하위프로세스의 정리"-같은건 일반적인 상황이라 남들도 충분히 고민을 해왔을 것 같은데, 정말 겨우 이런 수준까지만 가능한걸까?
[접근법] kill -- -${PGID}
현대의 유닉스는 이런 문제를 위해 (PID 이외에) "Process Group"
개념을 지원한다. 👉위키백과 ..이 글에선 "Session ID" 대신 "Process
Group"-만 소개하겠음. (이유는 이후 "차이점" 섹션 참고)
하옇튼, 이를 위해 kill(1) manpage에선 다음과 같이 설명:
kill -SIGTERM -123Send the signal SIGTERM to process group 123.
The signal name or number is required if specifying process groups with a negative PID.
혹은 다음과 같이 실행해도 된다: kill -- -123
"-${PID}" 같이 표기하면, "123"부분이 SIGNO인줄 알고 제대로 동작 않으니
---으로 구분. 보내는 시그널은아마 기본인 SIGTERM.
💲 PGID을 PID처럼(대신 "-"을 앞에 붙여서), 지정하면 해당 process-group의 프로세스를 종료해준다.
Session ID와의 차이점?
구글 검색하니 AI이 다음과 같이 정리해준다:
setpgrp()-은 현재 세션-에서 프로그램그룹만을 분리.setsid()-은 완전히 새로운 세션-을 시작하고, 현재 터미널(pty등)에서 프로세스를 분리(detached)한다.setid-이 더 격리된 환경을 만들지만, 프로세스 daemonization등에 더 적합.- 반면
setpgrp-은 프로세스제어, 즉 내가 하려는 하위프로세스의 정리에 더 적합. (혹은 딱 그 정도에 부합)
[실험] process-group 지정해보기
하위프로세스으로 예시로 쓰기 위해, 무한루프 + 1초마다 시각을 찍는 간단한 프로그램을 만들었다:
(naïve) system() == 👌
-
위와 같이 짜서
clock.pl실행하고,- 다른 터미널에서
ps j-을 실행한 결과를 붙여 넣었다: (L-5 이하)
- 다른 터미널에서
- PGID=2444으로 동일하게 설정되고, 원 호출PID와 같다. (자동으로 설정됨.)
IPC::Open3 == 👌
|
|
- 이번에는 조금 더 복잡한 경우를 대비해서,
IPC::Open3패키지를 사용해봄. - 역시 잘 동작함을 확인.
fork + exec == 👌
|
|
- 이렇게도 잘 동작. ㅎㅎ
직접 ⚙️제어하기: setpgrp == 👌
다음처럼 setpgrp 직접 호출해서, 원하는 시점/프로세스에서 pgrp을
새로 시작해도 됨:
|
|
Windows에서는?
setpgrp-은 아마 제대로 동작하지 않을 것 같다.
-
CreateProcessAPI:- https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
CREATE_NEW_PROCESS_GROUP(0x00000200)
The new process is the root process of a new process group.
The process group includes all processes that are descendants of this root process.
The process identifier of the new process group is the same as the process identifier, which is returned in the
lpProcessInformationparameter.Process groups are used by the
GenerateConsoleCtrlEventfunction to enable sending a CTRL+BREAK signal to a group of console processes. If this flag is specified, CTRL+C signals will be disabled for all processes within the new process group. This flag is ignored if specified withCREATE_NEW_CONSOLE.
…그리고 흔히 사용하는 TerminateProcess API은 이렇게 동작 않을거
같다. 차라리 taskkill /F /PID ${PID} /T-와 같이 커맨드라인
유틸리티를 사용하라고 하는거 같다. (…그리고 이건 아마도 "프로세스
나열하기"-방식으로 하위프로세스를 찾아서 종료하겠지 싶다. ㅎㅎ
procmon으로 확인해봐도 재밌을거 같다.)
안습한 점은, node.js의 "kill-process-group" 패키지를 봐도, 윈도에선 그냥 taskkill 유틸리티를 호출한다. (윈도 지원이라고 해서 설레였었다…)
결국:
-
윈도에서도 process-group을 흉내내려고 하려면,
-
https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects 을 쓰는게 가장 맞는 방법인 것 같다.
- (내가 프로세스를 시작한다면)
-
- 그게 아니라면, 그냥 enumerate => terminate 방식으로 구현하는게 맞을 것 같다.
[Addendum] cgroups – 갑자기 docker & kubernetes
정말 별거 아닌 일을 하기 위해서 process-group으로 분리하고 하는 일들을 보였는데, 의외로 이런 방식들이 발전을 계속하여 현재의 경량가상화 / 혹은 컨테이너화를 위한 단계까지 온 것 같다.
pgrp만이 아니라 다른 chroot 같은 "오래된" 유닉스개념들이 더해지고 발전하여 현대의 docker / kubernetes 같은 경량컨테이너화 기술의 기반이 되었다.
👉 LWN Control groups, part 1: On the history of process grouping