프로세스에 대한 이해를 하려면 프로세스가 어떻게 생성되는 지 알면 좋습니다. 프로세스 생성 과정에서 프로세스를 관리하는 자료구조 관계를 알 수 있기 때문입니다. 

리눅스에서 구동되는 프로세스는 크게 유저 레벨에서 생성된 프로세스와 커널 레벨에서 생성된 프로세스가 있습니다. 

유저 레벨에서 생성된 프로세스는 유저 공간에서 프로세스를 생성하는 라이브러리(glibc) 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 커널 프로세스는 kthread_create() 함수를 호출해서 커널 내부에서 프로세스를 생성합니다. 커널 프로세스는 커널 스레드라고 부르며 커널 내부에서 스레드를 직접 관리합니다.

공통으로 리눅스에서 생성된 프로세스는 _do_fork() 함수를 호출합니다. 프로세스 생성하는 핵심함수는 _do_fork() 이니 이 함수를 중심으로 프로세스가 어떻게 생성되는지 알아봅시다.

_do_fork() 함수 소개

리눅스에서 구동 중인 모든 프로세스는 _do_fork() 함수가 실행할 때 생성됩니다. 프로세스는 누가 생성할까요? 리눅스 시스템에서 프로세스 생성을 전담하는 프로세스가 있습니다. 주인공은 init과 kthreadd 프로세스입니다.

유저 레벨 프로세스는 init 프로세스, 커널 레벨 프로세스(커널 스레드)는 kthreadd 프로세스가 생성하는 것입니다. 프로세스 생성 과정에 대해서 조금 더 정확히 말하면 프로세스는 생성이 아니라 복제된다고 설명할 수 있습니다.

프로세스를 생성할 때 여러 리소스(메모리 공간, 페이지 테이블, 가상 메모리 식별자)를 커널로부터 할당 받아야 합니다. 프로세스 동작에 필요한 리소스를 각각 할당 받으면 시간이 오래 걸리니 이미 생성된 프로세스에서 복제하는 것입니다.


리눅스 커널에서는 속도 개선을 위해 반복해서 실행하는 코드를 줄이려는 노력을 한 흔적을 볼 수 있습니다. 커널 메모리 할당자인 슬럽 메모리 할당자(Slub Memory Allocator)도 유사한 역할을 수행합니다. 드라이버에서 자주 메모리 할당 요청을 하는 쓰는 구조체를 정의해서 해당 구조체에 대한 메모리를 미리 확보해 놓습니다. 메모리 할당 요청 시 바로 이미 확보한 메모리를 할당하는 속도가 빠르기 때문입니다.


프로세스 생성 과정도 마찬가지입니다. 프로세스를 생성할 때 이미 생성된 프로세스에서 복제하는 것이 더 효율적입니다. 따라서 모든 프로세스는 부모와 자식 프로세스를 확인할 수 있습니다.

먼저 _do_fork() 함수 선언부를 보면서 이 함수에 전달되는 인자와 반환값을 확인합시다.

[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long);

long _do_fork(unsigned long clone_flags,
      unsigned long stack_start,
      unsigned long stack_size,
      int __user *parent_tidptr,
      int __user *child_tidptr,
      unsigned long tls);

먼저 반환값을 확인합시다.
함수 선언부와 같이 반환값 타입은 long이며 PID를 반환합니다. 프로세스 생성 시 에러가 발생하면 PTR_ERR() 매크로로 지정된 에러 값을 반환합니다.

_do_fork() 함수에 전달하는 인자값들을 점검합시다.

unsigned long clone_flags;

프로세스를 생성할 때 전달하는 매크로 옵션 정보를 저장합니다. 이 멤버에 다음 매크로를 저장합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/linux/sched.h]
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100  
#define CLONE_FS 0x00000200  
#define CLONE_FILES 0x00000400  
#define CLONE_SIGHAND 0x00000800  
#define CLONE_PTRACE 0x00002000 
#define CLONE_VFORK 0x00004000 
#define CLONE_PARENT 0x00008000 
#define CLONE_THREAD 0x00010000

unsigned long stack_start;

보통 유저 영역에서 스레드를 생성할 때 복사하려는 스택 주소입니다. 이 스택 주소는 유저 공간에서 실행 중인 프로세스 스택 주소입니다.

unsigned long stack_size;
보통 유저 영역 실행 중인 스택 크기입니다. 보통 유저 영역에서 스레드를 생성할 때 복사합니다.

int __user *parent_tidptr;
int __user *child_tidptr;

부모와 자식 스레드 그룹을 관리하는 핸들 정보입니다.

커널에서 _do_fork() 함수를 언제 호출할까요? 생성하려는 프로세스 유형에 따라 함수 호출 흐름이 나뉩니다.
 1. 유저 모드에서 생성한 프로세스: sys_clone() 시스템 콜 함수 
 2. 커널 모드에서 생성한 커널 스레드: kernel_thread() 함수
프로세스는 유저 모드에서 생성된 프로세스와 커널 모드에서 생성된 프로세스로 분류할 수 있습니다. 각각 유저 레벨 프로세스와 커널 레벨 프로세스라고 부릅니다.


유저 레벨 프로세스 생성 시 _do_fork() 함수 흐름 
먼저 유저 레벨 프로세스는 어떻게 생성할까요? 저수준 리눅스 어플리케이션 프로그램으로 fork() 함수를 호출하면 리눅스에서 제공하는 라이브러리 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 여기까지가 유저 모드에서 프로세스를 요청하는 단계입니다.

리눅스에서 제공하는 라이브러리는 시스템 콜을 발생하고 리눅스 커널에서는 fork() 함수에 대응하는 시스템 콜 핸들러인 sys_clone() 함수를 호출합니다.

먼저 sys_clone() 함수 코드를 봅시다.
 [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
1 #ifdef __ARCH_WANT_SYS_CLONE
2 #ifdef CONFIG_CLONE_BACKWARDS
3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
4  int __user *, parent_tidptr,
5  unsigned long, tls,
6  int __user *, child_tidptr)
...
7 #endif
8 {
9 return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
10 }
11 #endif

아래와 같이 시스템 콜 함수를 정의하면, sys_clone() 이란 시스템 콜 래퍼 함수를 생성합니다.
3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

sys_clone() 함수는 _do_fork() 함수를 그대로 호출합니다.

마찬가지로 sys_fork()와 sys_vfork() 이란 시스템 콜 함수를 확인해봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
1 SYSCALL_DEFINE0(fork)
2 {
3 #ifdef CONFIG_MMU
4 return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
5 #else
6 /* can not support in nommu mode */
7 return -EINVAL;
8 #endif
9 }
10
11 SYSCALL_DEFINE0(vfork)
12 {
13 return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
14 0, NULL, NULL, 0);
15 }


sys_fork()와 sys_vfork() 함수도 역시 _do_fork() 함수를 그대로 호출합니다. 

정리하면 유저 공간에서 생성한 프로세스는 sys_clone(), sys_fork() 그리고 sys_vfork() 시스템 콜 함수를 통해서 _do_fork() 함수를 호출합니다.

그런데 유저 공간(리눅스 시스템 프로그래밍)에서 fork() 이란 함수로 프로세스를 생성하면 시스템 콜 함수로 sys_clone() 를 호출합니다. 예전 리눅스 커널 버전에서는 fork()을 쓰면 sys_fork() 함수를 호출했으나 최근 리눅스 커널에서는 sys_clone() 함수를 실행합니다. vfork() 시스템 콜 함수도 fork() 시스템 콜을 개선하기 위해 이전 리눅스 커널 버전에서 썼던 레거시(과거) 코드입니다.

유저 레벨에서 생성한 프로세스와 스레드를 커널에서 동등하게 처리합니다. 그러니 sys_clone() 함수를 호출하는 것입니다.

정리하면 유저 공간에서 fork() 란 함수를 호출하면 시스템 콜이 실행해어 커널 공간에서 sys_clone() 함수를 호출합니다. 

커널 프로세스 생성 시 do_fork() 함수 흐름 

커널 공간에서 시스템 리소스(메모리, 전원) 관리를 수행하는 프로세스를 커널 스레드라고 합니다. 커널 스레드는 어떻게 생성할까요? 다음과 같이 kthread_create() 함수에 적절한 인자를 전달하면 됩니다.

커널 스레드를 생성하는 코드를 같이 봅시다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/drivers/vhost/vhost.c#L334]
1 long vhost_dev_set_owner(struct vhost_dev *dev)
2 {
3 struct task_struct *worker;
4 int err;
...
5 /* No owner, become one */
6 dev->mm = get_task_mm(current);
7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

위 7번째 줄 코드에서 kthread_create() 함수를 실행하면 커널 스레드를 생성하는 것입니다. kthread_create() 이란 함수를 호출하면 커널 스레드 생성을 담당하는 kthreadd란 프로세스에게 커널 스레드 생성 요청을 합니다. 이후 kthreadd 스레드는 _do_fork() 함수를 실행해서 프로세스를 생성합니다. 

커널 스레드도 프로세스의 한 종류라 볼 수 있습니다.

대부분 커널 스레드는 시스템이 부팅할 때 생성하며 리눅스 커널이 커널 스레드가 필요할 때 동적으로 생성합니다. 예를 들어 리눅스 드라이버에서 많은 워크를 워크큐에 큐잉하면 커널 스레드의 종류인 워커 스레드를 더 생성해야 합니다. 시스템 메모리가 부족할 경우 메모리를 회수해서 가용 메모리를 확보하는 커널 스레드를 생성할 때도 있습니다. 보통 시스템이 더 많은 일을 해야 할 때 커널 스레드를 생성합니다.

프로세스 생성에 대한 소개를 했으니 다음에 라즈베리파이로 유저 공간에서 실행 중인 프로세스를 점검해봅시다.



#Reference

프로세스 소개

  | 프로세스란/ 태스크란/ 스레드란

프로세스 확인하기

프로세스는 어떻게 생성하나?

  | do_fork() 함수 소개

  | 유저 레벨 프로세스 생성 시 _do_fork() 함수 흐름

리눅스 시스템 개발자(디바이스 드라이버, 데브옵스)로 오래 동안 실력을 인정 받으려면 리눅스 커널을 잘 알면 좋습니다. 하지만 리눅스 커널은 그 내용이 방대하고 깊이가 있어 단기간에 익히기 어려운 기술 영역입니다. "프로세스란 무엇인가"란 질문으로 리눅스 커널을 시작합니다. 성경이나 불경같이 근엄한 단어를 많이 보입니다.  안타깝게도 20페이지 정도 읽다가 포기합니다. 너무 이론으로 프로세스를 설명하기 때문입니다.

프로세스에 익숙해지려면 리눅스 시스템에 익숙해져야 합니다. 이번 장에서는 라즈베리파이에서 명령어를 입력하고 ftrace 로그에서 출력되는 로그로 프로세스 동작을 확인합니다.

먼저 다음 리눅스 명령어로 시스템에서 프로세스 목록을 확인합시다. 이를 위해 라즈베리파이에서 x-terminal 프로그램을 실행해서 셸을 열어야 합니다. 
root@raspberrypi:/home/pi# ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  5956  6991 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
S  1000   867   517  0  80   0  7720 12887 poll_s ?        00:00:00 gvfsd-trash
S  1000   876   730  0  80   0 20084 12108 poll_s ?        00:00:07 lxterminal
S  1000   877   876  0  80   0  1324   590 unix_s ?        00:00:00 gnome-pty-helpe
S  1000   878   876  0  80   0  4028  1628 wait   pts/0    00:00:00 bash
S     0   886   878  0  80   0  3380  1908 poll_s pts/0    00:00:00 sudo
S     0   890   886  0  80   0  3076  1818 wait   pts/0    00:00:00 su
리눅스 시스템에서 프로세스 목록을 보기 위해서는 "ps"라는 명령어를 입력하면 됩니다.

x-terminal 셸을 실행한 상태에서 "info ps" 명령어를 입력하면 ps 명령어의 의미를 알 수 있습니다.

-------
PS(1)                                   User Commands                                   PS(1)

NAME
       ps - report a snapshot of the current processes.

SYNOPSIS
       ps [options]

리눅스 시스템에서 돌고 있는 프로세스를 출력하는 명령어입니다. 리눅스 시스템에서 디버깅을 할 때 많이 쓰는 명령어이니 자주 활용합시다.

리눅스 시스템에서 생성된 모든 프로세스(유저 레벨, 커널 스레드)는 init 프로세스를 표현하는 전역 변수 init_tasks.next 멤버에 연결 리스트로 등록돼 있습니다. ps 명령어를 입력하면 이 연결 리스트를 순회하면서 프로세스 정보(struct task_struct)를 얻어 프로세스 정보를 출력하는 겁니다.

이번에는 ps 명령어에 "-ejH" 이란 옵션을 주고 프로세스를 부모 자식 프로세스 관계로 출력합시다.
1 root@raspberrypi:/home/pi # ps -ejH
2   PID  PGID   SID TTY          TIME CMD
3    2     0     0 ?        00:00:00 kthreadd
4    4     0     0 ?        00:00:00   kworker/0:0H
5    6     0     0 ?        00:00:00   mm_percpu_wq
6    7     0     0 ?        00:00:00   ksoftirqd/0
...
7  17103     0     0 ?     00:00:00   kworker/1:1
8  17108     0     0 ?     00:00:00   kworker/u8:0
9     1     1     1 ?        00:00:02 systemd
10   94    94    94 ?        00:00:00   systemd-journal
11  127   127   127 ?        00:00:00   systemd-udevd
12  274   274   274 ?        00:00:00   systemd-timesyn

4~6번 줄에 보이는 "kworker/0:0H", "mm_percpu_wq" 그리고 "ksoftirqd/0" 이란 프로세스의 부모 프로세스는 3번 줄에 있는 "kthreadd" 입니다. 

pid가 2이 "kthreadd" 프로세스는 커널 공간에서 실행 중인 프로세스를 생성하는 역할을 수행합니다. 위 출력 결과에서 4~8번 줄에 있는 프로세스들은 같은 행으로 정렬돼 있습니다. 이 목록에서 보이는 프로세스를 커널 스레드, 커널 프로세스라고 합니다. 커널 공간에서만 실행합니다. 

리눅스 커널에서는 프로세스 마다 PID(Process id)라는 int 형 ID를 부여합니다.
swapper 프로세스는 PID가 0이고 init 프로세스는 PID가 1 그리고 커널 스레드를 생성하는 kthreadd 프로세스는 PID가 2입니다.
새로운 프로세스를 생성할 때 커널이 부여하는 PID 정수값은 증가합니다. PID로 프로세스가 언제 생성됐는지 추정할 수 있습니다.

PID는 최댓값은 32768로 정해져 있습니다.

이번에는 9번째 줄 로그를 봅시다. pid가 1인 systemd 프로세스가 보입니다.
9     1     1     1 ?        00:00:02 systemd

pid가 1인 프로세스를 임베디드 리눅스에서는 init 프로세스라고 하며 모든 유저 공간에서 생성된 프로세스의 부모 프로세스 역할을 수행합니다.

프로세스는 인간을 객체화해서 고안한 내용이 많습니다. 프로세스는 각자 부모 자식 프로세스들이 있고 자식 프로세스가 종료할 때 부모 프로세스에게 신호를 알립니다.

만약 조부모, 부모, 자식 프로세스가 있다고 가정합니다. 예외 상황으로 부모 프로세스가 종료되면 자식 프로세스 입장에서 부모 프로세스가 사라집니다. 이 때 조부모가 부모 프로세스가 됩니다. 이런 상황에서 init 프로세스가 조부모 역할(새로운 부모 프로세스)을 수행합니다.

다음에 리눅스 커널 소스 코드를 열어서 프로세스를 생성할 때 어떤 함수가 실행하는지 살펴봅시다.


프로세스는 추상적이고 다양한 의미를 담고 있어 다양한 관점으로 설명할 수 있습니다.

프로세스란 무엇일까요? 프로세스(Process)는 리눅스 시스템 메모리에서 실행 중인 프로그램을 말합니다. 스케줄링 대상인 태스크와 유사한 의미로 쓰입니다. 다수 프로세스를 실시간으로 사용하는 기법을 멀티프로세싱이라고 말하며 같은 시간에 멀티 프로그램을 실행하는 방식을 멀티태스킹이라고 합니다.

우리가 쓰고 있는 스마트폰 동작을 잠깐 생각해봅시다. 
전화를 하면서 메모를 남기고, 음악을 들으면서 브라우저를 볼 수 있습니다. 여러 어플리케이션이 동시에 실행하고 있습니다. 이것은 멀티태스킹을 수행해서 프로그램을 시분할 방식으로 처리하기 때문에 가능합니다.

이번에는 리눅스 개발자 입장에서 프로세스에 대해 생각해 봅시다. 프로세스는 리눅스 시스템 메모리에 적재되어 실행을 대기하거나 실행하는 실행 흐름을 의미합니다. 프로세스가 실행을 대기한다면 실행할 때 어떤 과정을 거칠까요? 프로세스는 어떤 구조체로 식별할까요? 다양한 의문이 생깁니다.

프로세스를 관리하는 자료구조에자 객체를 태스크 디스크립터라고 말하고 구조체는 struct task_struct 입니다. 이 구조체에 프로세스가 쓰는 메모리 리소스, 프로세스 이름, 실행 시각, 프로세스 아이디(PID), 프로세스 스택 최상단 주소가 저장돼 있습니다.

프로세스를 struct task_struct 이란 구조체로만 표현할 수 있을까요? 위에서 프로세스란 실행 흐름 그 자체라고 정의했습니다. 프로세스 실행 흐름은 어느 구조체에 저장할 수 있을까요?

프로세스는 실행할 때 리눅스 커널 함수를 호출합니다. 임베디드 리눅스 디버거의 전설인 Trace32 프로그램으로 콜 스택을 하나 봅시다.
1 -000|__schedule()
2 -001|schedule_timeout()
3 -002|do_sigtimedwait()
4 -003|sys_rt_sigtimedwait()
5 -004|ret_fast_syscall(asm)

위 함수 호출 방향은 5번째 줄에서 1번째 줄입니다. 콜 스택을 간단히 해석하면 유저 공간 프로그램에서 sigtimedwait() 이란 함수를 호출하면 이에 대응하는 시스템 콜 핸들러 함수인 sys_rt_sigtimedwait() 함수 실행 후 스케줄링되는 함수 흐름입니다.

프로세스는 함수를 호출하면서 실행을 합니다. 그런데 함수를 호출하고 실행할 때 어떤 리소스를 쓸까요? 프로세스 스택 메모리 공간입니다.

모든 프로세스들은 커널 공간에서 실행할 때 각자 스택 공간을 할당 받으며 스택 공간에서 함수를 실행합니다. 

위에서 본 프로세스가 스케줄러에 의해 다시 실행한다고 가정합시다. 그럼 어떻게 실행할까요?
1 -000|__schedule()
2 -001|schedule_timeout()
3 -002|do_sigtimedwait()
4 -003|sys_rt_sigtimedwait()
5 -004|ret_fast_syscall(asm)

1번 함수에서 5번 함수 방향으로 되돌아 올 겁니다. 이는 이 프로세스가 마지막에 실행했던 레지스터 세트와 실행 흐름이 프로세스 스택 공간에 저장돼 있었기 때문입니다.

프로세스를 실행 흐름을 표현하는 또 하나 중요한 공간은 프로세스 스택 공간이며 이 프로세스 스택 최상단 주소에 struct thread_info 란 구조체가 있습니다.

정리하면 프로세스는 추상적인 개념이지만 프로세스 정보와 프로세스 실행 흐름을 저장하는 구조체와 메모리 공간이 있습니다. 리눅스 커널에서 실시간으로 구동하는 프로세스에 대해 잘 알려면 이 자료구조를 잘 알 필요가 있습니다.

우리가 열심히 분석하는 리눅스 커널 소스 코드를 실행하는 주체가 프로세스이며 프로세스 스택 공간에서 실행하는 것입니다.

태스크란
태스크는 무엇일까요? 태스크는 리눅스 이외 다른 운영체제에서 예전부터 쓰던 용어입니다.
운영체제 이론을 다루는 예전 이론서는 대부분 태스크란 단어를 많이 볼 수 있습니다.

태스크는 운영체제에서 어떤 의미일까요? 말 그대로 실행(Execution)이라 할 수 있습니다.
운영체제 책들을 보면 첫 장에서 태스크에 대한 설명을 볼 수 있습니다. 최근 운영 체제에서는 대부분 기본으로 멀티 태스킹 환경에서 프로그램을 실행하나 예전에는 특정 코드나 프로그램 실행을 일괄 처리했습니다. 이 실행 및 작업 단위를 태스크라고 불렀습니다.


화면이 없는 간단한 시나리오의 임베디드 시스템에서는 태스크 2개로 서로 시그널을 주고 받으며 시스템 전체를 제어할 수 있습니다. 

하지만 태스크에 대한 개념은 현재 프로세스와 겹치는 부분이 많습니다. 태스크에 대한 의미가 프로세스와 스레드에 대한 개념이 도입하면서 발전했습니다. 태스크를 실행하는 단위인 실행(Execution)을 결정하는 기준이 스케줄링으로 바뀐 겁니다. 

예전에 쓰던 용어를 현재 소프트웨어에 그대로 쓰는 경우가 많습니다. 이를 레거시(Legacy)라고 말하고 과거 유물이란 뜻도 있습니다. 예전에 썼던 태스크란 용어를 리눅스 커널 용어나 소스 코드에서 그대로 쓰고 있습니다. 프로세스 속성을 표시하는 구조체 이름을 struct task_struct으로 쓰고 있습니다. 

프로세스 마다 속성을 표현하는 struct task_struct 구조체는 태스크 디스크립터라고 하며 프로세스 디스크립터라고도 말합니다.

리눅스 커널 함수 이름이나 변수 중에 task란 단어가 보이면 프로세스 관련 코드라 생각해도 좋습니다.

예를 들어 다음 함수는 모두 프로세스를 관리 및 제어하는 역할을 수행하며 함수 이름에 보이는 태스크는 프로세스로 바꿔도 무방합니다.
dump_task_regs
get_task_mm
get_task_pid
idle_task
task_tick_stop

리눅스 커널에서 태스크는 프로세스와 같은 개념으로 쓰는 용어입니다. 소스 코드나 프로세스에 대한 설명을 읽을 때 태스크란 단어를 보면 프로세스와 같은 개념으로 이해합시다.

스레드란
스레드는 무엇일까요? 간단히 말하면 유저 레벨에서 생성된 가벼운 프로세스라 말할 수 있습니다. 멀티 프로세스 실행 시 컨택스트 스위칭을 수행해야 하는데 이 때 비용(시간)이 많이 듭니다. 실행 중인 프로세스의 가상 메모리 정보를 저장하고 새롭게 실행을 시작하는 프로세스도 가상 메모리 정보를 로딩해야 합니다. 또한 스레드를 생성할 때는 프로세스를 생성할 때 보다 시간이 덜 걸립니다.

스레드는 자신이 속한 프로세스 내의 다른 스레드와 파일 디스크립터, 파일 및 시그널 정보에 대한 주소 공간을 공유합니다. 프로세스가 자신만의 주소 공간을 갖는 것과 달리 스레드는 스레드 그룹 안의 다른 스레드와 주소 공간을 공유합니다.

하지만 커널 입장에서는 스레드를 다른 프로세스와 동등하게 관리합니다. 대신 각 프로세스 식별자인 태스크 디스크립터(struct task_struct)에서 스레드 그룹 여부를 점검할 뿐입니다.


+ Recent posts