Linux와 Windows의 프로세스/스레드 ID 관리 방식
윈도우 기반의 개발자가 Linux로 이전하면서 혼란을 느끼는 것 중의 하나가 바로 프로세스와 스레드일 것입니다. ^^;
우선, 윈도우는 프로세스와 스레드의 관계가 주종 관계입니다.
EPROCESS_1
|- 스레드_1 (ETHREAD)
|- 스레드_2 (ETHREAD)
: ... (ETHREAD)
ㄴ 스레드_N (ETHREAD)
EPROCESS_2
|- 스레드_1 (ETHREAD)
|- 스레드_2 (ETHREAD)
: ... (ETHREAD)
ㄴ 스레드_N (ETHREAD)
따라서, 프로세스의 ID는 EPROCESS 구조체에 담긴 ID 필드 값이고, 스레드의 ID는 ETHREAD 구조체에 담긴 ID 필드 값으로 분리돼 있습니다.
lkd> dt _EPROCESS
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x438 ProcessLock : _EX_PUSH_LOCK
+0x440 UniqueProcessId : Ptr64 Void
+0x448 ActiveProcessLinks : _LIST_ENTRY
...[생략]...
+0x5e0 ThreadListHead : _LIST_ENTRY
...[생략]...
lkd> dt _ETHREAD
nt!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x430 CreateTime : _LARGE_INTEGER
+0x438 ExitTime : _LARGE_INTEGER
...[생략]...
+0x478 Cid : _CLIENT_ID
...[생략]...
lkd> dt _CLIENT_ID
nt!_CLIENT_ID
+0x000 UniqueProcess : Ptr64 Void
+0x008 UniqueThread : Ptr64 Void
하지만, 리눅스 세계에서는 프로세스와 스레드의 구분이 없고 모든 것이 "Task"입니다.
Task_1
|- Task_2
|- Task_3
: ...
ㄴ Task_N
Task_N+1
|- Task_N+2
|- Task_N+3
: ...
ㄴ Task_N+N
문제는, Task를 표현하는 task_struct 구조체 필드의 이름이 윈도우 세계의 개념과 혼동을 줄 수 있다는 점입니다.
$ grep -A 214 "struct task_struct {" vmlinux.h
struct task_struct {
...[생략]...
pid_t pid;
pid_t tgid;
...[생략]...
struct key *cached_requested_key;
char comm[16];
...[생략]...
};
// https://www.linkedin.com/pulse/brief-linux-process-amit-nadiger/
pid: the process ID of the process
tgid: the thread group ID of the process
pid를 "process ID"라고 설명하고 있는데요, 저 말을 윈도우 세계의 프로세스/스레드 개념으로 이해하면 안 됩니다. 즉, 리눅스에서 저 2개의 필드는 실은 다음과 같은 의미로 쓰입니다.
pid: task id (윈도우라면 스레드 ID와 유사)
tgid: task group id (윈도우라면 프로세스 ID와 유사)
실제로, 저 개념으로 보면 그나마 윈도우 운영체제의 프로세스/스레드 개념과 비슷하게 이해할 수 있습니다. 가령, task를 하나 생성하면 최초에 pid와 tgid는 동일한 값으로 나옵니다.
[신규 task 생성]
pid == 100
tgid == 100
그리고, 저 task에서 새로운 스레드(task)를 하나 생성하면 pid는 새로운 값으로 변경되지만, tgid는 부모 task의 tgid 값으로 결정됩니다.
[task에서 신규 스레드 생성]
pid == 100
tgid == 100
pid == 101
tgid == 100
반면, 스레드가 아닌 자식 프로세스의 개념(예를 들어, fork)으로 task를 생성하는 경우에는 pid는 언제나처럼 새로운 값으로 변경되고, tgid는 다시 그 pid의 값을 따르게 됩니다.
[task에서 신규 프로세스 생성]
pid == 100
tgid == 100
pid == 101
tgid == 101
그런데, 위의 필드들과 관련된 함수들을 보면 더욱더 혼란스러운 면이 있습니다.
pid_t getpid (void): task_struct의 tgid 반환 (윈도우라면 프로세스 ID와 유사)
pid_t getppid (void): task_struct의 real_parent의 tgid 반환 (윈도우라면 부모 프로세스 ID와 유사)
pid_t gettid (void): task_struct의 pid 반환 (윈도우라면 스레드 ID와 유사)
보는 바와 같이 getpid가 (pid가 아닌) tgid를 반환하고, gettid가 pid를 반환하는 식입니다. 즉, 함수 측면에서 보면 오히려 윈도우의 프로세스/스레드 개념과 유사하게 이해할 수 있습니다.
참고로, "pstree" 명령어를 사용하면 리눅스 시스템에서 프로세스/스레드의 관계를 트리 구조로 확인할 수 있습니다.
systemd─┬─ModemManager───2*[{ModemManager}]
├─NetworkManager───2*[{NetworkManager}]
├─accounts-daemon───2*[{accounts-daemon}]
...[생략]...
한 가지 유의해야 할 점은, 위에서 systemd와 ModemManager는 프로세스의 부모/자식 관계인 반면, ModemManager와 "2*[{ModemManager}]"는 같은 프로세스에서의 스레드 부모/자식 관계입니다. 이름으로 그렇게 구분하는 것은 아니고, "{", "}" 중괄호로 묶인 것이 스레드라는 의미이기 때문인데요, 스레드의 경우 앞서 표기된 "2"라는 숫자는 스레드의 개수를 의미합니다. (윈도우의 관점에서 해석하면, ModemManager 프로세스에는 Main 스레드 1개와 secondary 스레드 2개로 총 3개의 스레드가 존재하는 걸로 이해할 수 있습니다.)
마지막으로 리눅스와 윈도우 모두 스레드 ID는 시스템 내에서 유일한 값임을 보증할 수 있습니다. 단지, 리눅스의 경우에는 꾸준히 증가하는 식으로 할당이 되는 반면, 윈도우는 스레드가 종료되면 그 ID가 재사용될 수 있으므로 주의가 필요합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]