4.7.1 프로세스를 식별하는 멤버
프로세스를 식별하는 멤버들을 살펴 보겠습니다.

char comm[TASK_COMM_LEN];

comm은 TASK_COMM_LEN 크기 배열이며 프로세스 이름을 저장합니다.

"ps -ely"란 명령어를 입력하면 다음과 같이 systemd, kthreadd 그리고 kworker/0:0H가 보입니다.
root@raspberrypi:/home/pi # ps -ely
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
2 S     0     1     0  0  80   0  5964  7007 SyS_ep ?        00:00:02 systemd
3 S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
4 I     0     4     2  0  60 -20     0     0 worker ?        00:00:00 kworker/0:0H

이 프로세스 이름들은 태스크 디스크립터의 comm이란 멤버에 접근해서 출력한 겁니다.
프로세스 이름은 set_task_comm() 이란 함수를 호출해서 지정할 수 있습니다.

현재 실행 중인 프로세스의 태스크 디스크립터 구조체인 struct task_struct에 접근하는 current 매크로와 프로세스 이름을 저장하는 comm 멤버를 조합하면 다양한 디버깅 용 코드를 작성할 수 있습니다.

한 가지 디버깅용 코드를 작성해볼까요? 이번에 kthreadd 프로세스만 깨울 때 함수 호출 흐름을 출력하는 디버깅 코드를 작성해 보겠습니다.
2 static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
3    struct rq_flags *rf)
4 {
5 check_preempt_curr(rq, p, wake_flags);
6 p->state = TASK_RUNNING;
7 trace_sched_wakeup(p);
8
9  + if (strcmp(p->comm, "kthreadd")) {
10 + printk("[+][%s] wakeup kthreadd process \n", current->comm);
11 + dump_stack();
12 + }
13
14 #ifdef CONFIG_SMP
15 if (p->sched_class->task_woken) {
16 /*

ttwu_do_wakeup() 함수 인자인 struct task_struct *p가 깨우려는 프로세스의 태스크 디스크립터입니다. 프로세스 이름은 struct task_struct 구조체 comm 필드에 저장된 프로세스 이름이 “kthreadd” 인 경우 dump_stack() 함수 호출로 함수 호출 흐름을 커널 로그로 출력합니다.

pid_t pid;

pid는 Process ID의 약자로 pid는 프로세스마다 부여하는 정수형 값입니다.
pid 상수는 프로세스를 생성할 때 마다 증감하므로 pid 값 크기로 프로세스가 언제 생성됐는지 확인할 수 있습니다.

pid_t tgid;

pid와 같은 타입의 멤버로 스레드 그룹 아이디를 표현하는 정수형 값입니다.
해당 프로스세가 스레드 리더인 경우는 tgid 와 pid 가 같고, child thread 인 경우는 tgid 와 pid 는 다릅니다.

4.7.2 프로세스 상태 저장
이번에는 프로세스 상태를 알 수 있는 멤버를 알아봅시다. 프로세스 실행 상태는 저장하는 state와 프로세스 세부 동작 상태와 속성을 알 수 있는 flags를 눈여겨봅시다.

volatile long state;

프로세스 상태를 저장하는 멤버로 다음 매크로 중 하나입니다.
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002

대부분 프로세스들은 TASK_INTERRUPTIBLE 상태입니다.
런큐에 큐잉되서 실행하거나 런큐에서 실행을 기다리는 프로세스들은 state가 TASK_RUNNING 이고 특정 조건에서 깨어나는 프로세스들은 state가 TASK_UNINTERRUPTIBLE 를 저장합니다.

리눅스 커널 시스템에서 TASK_RUNNING 혹은 TASK_UNINTERRUPTIBLE 상태인 프로세스가 비정상적으로 많으면 시스템에 문제(데드락, 특정 프로세스 스톨)가 있는 경우가 많습니다. 

프로세스 상태 별 세부 동작은 프로세스 스케줄러 시간에 상세히 다룹니다.

unsigned int flags;

프로세스 종류와 프로세스 실행 상태를 저장하는 멤버입니다.
flags 필드는 PF_* 로 시작하는 매크로 필드를 OR 비트 연산한 결과를 저장합니다.

PF_* 매크로들을 저장하는데 PF_* 매크로 중 눈여겨볼 만한 코드는 다음과 같습니다.
#define PF_IDLE 0x00000002 /* I am an IDLE thread */
#define PF_EXITING 0x00000004 /* Getting shut down */
#define PF_EXITPIDONE 0x00000008 /* PI exit done on shut down */
#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */
#define PF_KTHREAD 0x00200000 /* I am a kernel thread */

flags 멤버에 저장된 값으로 프로세스 세부 실행 상태를 알 수 있습니다. 커널은 flags에 저장된 프로세스 세부 동작 상태를 읽어서 예외 처리 코드로 프로세스를 제어합니다.

한 가지 예를 들어 보겠습니다. 
1 static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc)
2 {
3 struct task_struct *tsk;
4 struct task_struct *selected = NULL;
5 int rem = 0;
...
6 for_each_process(tsk) {
7 struct task_struct *p;
8 int oom_score_adj;
9
10 if (tsk->flags & PF_KTHREAD)
11 continue;
12
13 p = find_lock_task_mm(tsk);
14 if (!p)
15 continue;
...
16 lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
17      p->pid, p->comm, oom_score_adj, tasksize);
18 }

위 코드는 안드로이드에서 메모리 회수를 위해 특정 프로세스를 종료하는 lowmem_shrink() 함수 일부분 코드입니다.

위에 소개한 코드에서 10~11 번째 줄 코드를 보면 종료할 프로세스가 커널 스레드이면 continue 문을 실행해 다른 프로세스를 실행하는 코드입니다.
10 if (tsk->flags & PF_KTHREAD)
11 continue;

이 방식으로 프로세스 동작 상태를 읽어서 예외 처리를 수행합니다.

int exit_state;

프로세스가 종료 상태를 저장합니다. 다음 매크로 중 하나 값을 저장합니다.
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010
#define EXIT_ZOMBIE 0x0020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)

int exit_code;

프로세스를 종료 코드를 저장하는 멤버 변수입니다. do_exit() 함수 3번째 줄 코드에서 종료 코드를 저장합니다.
1 void __noreturn do_exit(long code)
2 {
...
3 tsk->exit_code = code; 

do_exit() 함수를 호출할 때 인자로 다음과 같은 시그널이나 프로세스를 종료하는 옵션을 지정할 수 있습니다.
1 static void
2 __do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
3   struct pt_regs *regs)
4 {
...
5 do_exit(SIGKILL);
6 }

5 번째 줄 코드를 보면 SIGKILL 인자와 함께 do_exit() 함수를 호출합니다. 

4.7.3 프로세스 간 관계
이전 시간에 유저 공간에서 생성한 모든 프로세스의 부모 프로세스는 init 이고 커널 공간에서 생성한 커널 스레드(프로세스)의 부모 프로세스는 kthreadd라고 했습니다. 태스크 디스크립터에서는 프로세스의 부모와 자식 관계를 상세히 알 수 있습니다.

struct task_struct  *real_parent;

프로세스를 생성한 부모 프로세스의 태스크 디스크립터 주소를 저장합니다.

struct task_struct *parent;

부모 프로세스를 의미합니다. real_parent 란 멤버는 해당 프로세스를 생성해준 프로세스를 의미합니다. 그런데 자식 프로세스 입장에서 부모 프로세스가 소멸된 경우 부모 프로세스를 다른 프로세스로 지정합니다. 프로세스 계층 구조에서 지정한 부모 프로세스가 없을 경우 init 프로세스를 부모 프로세스로 변경합니다.

이 동작은 다음 그림과 같은 코드 흐름에서 수행합니다.

 

예외 상황으로 부모 프로세스가 종료되면 do_exit() 함수에서 화살표 방향으로 함수를 호출합니다. 함수 이름과 같이 forget_original_parent() 함수와 find_new_reaper() 에서 새로운 부모 프로세스를 지정합니다.

커널 스레드의 경우 자신을 생성한 프로세스가 종료하지 않고 계속 실행 중이면 real_parent와 parent가 같습니다.

struct list_head children;

부모 프로세스가 자식 프로세스를 생성할 때 children이란 연결 리스트에 자식 프로세스를 등록합니다.

struct list_head sibling;

같은 부모 프로세스로 생성된 프로세스 연결 리스트입니다. 단어가 의미하는 바와 같이 형제 관계 프로세스입니다.

위 children와 sibling 멤버가 어떤 방식으로 연결됐는지 조금 더 구체적으로 알아봅시다. 이를 위해 라즈베리파이에서 다음 명령어로 부모와 자식 프로세스 관계를 확인해 봅시다.
root@raspberrypi:/home/pi/dev_raspberri# ps axjf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
    2     4     0     0 ?           -1 I<       0   0:00  \_ [kworker/0:0H]
    2     6     0     0 ?           -1 I<       0   0:00  \_ [mm_percpu_wq]
    2     7     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/0]

출력 결과 kworker/0:0H, mm_percpu_wq 그리고 ksoftirqd/0 프로세스들의 부모 프로세스는 kthreadd 임을 알 수 있습니다. 각 프로세스들 PPID(Parent Process PID) 항목을 보면 모두 2입니다. 커널 스레드를 생성하는 kthreadd 프로세스의 pid는 2입니다.

리눅스를 탑재한 대부분 시스템(안드로이드, 라즈베리파이)에서 kthreadd 프로세스의 PID는 2입니다.
부모 프로세스인 kthreadd 입장에서 태스크 디스크립터는 다음 관계로 구성돼 있습니다.
 

태스크 디스크립터 관점으로 이 구조를 살펴봅시다.

“kthreadd” 프로세스 태스크 디스크립터 children 멤버는 연결 리스트입니다. 연결 리스트 해드에 등록된 자식 프로세스의 struct task_struct.sibling 멤버 주소를 저장합니다.

 “kthreadd” 프로세스의 자식 프로세스인 “kworker/0:0H” 입장에서 “mm_percpu_wq”와 “ksoftirqd/0” 프로세스는 sibling이란 연결 리스트로 연결돼 있습니다. 같은 부모 프로세스에서 생성된 프로세스이기 때문입니다.

조금 더 이해를 돕기 위해 Trace32로 위 자료구조를 분석해 봅시다.
다음 디버깅 정보는 kthreadadd 프로세스의 태스크 디스크립터입니다.
1  (struct task_struct *) (struct task_struct*)0xF1618740 = 0xF1618740 =  -> (
2   (long int) state = 1 = 0x1,
3    (void *) stack = 0xF1606000,
4    (atomic_t) usage = ((int) counter = 2 = 0x2),
...  
5    (struct task_struct *) parent = 0xC1A171B8
6    (struct list_head) children = (
7      (struct list_head *) next = 0xF161926C  
8        (struct list_head *) next = 0xF161A0EC  
9          (struct list_head *) next = 0xF161A82C 

7번째 줄 정보와 같이 children이란 연결 리스트 next 멤버는 0xF161926C 주소를 저장하고 있습니다.

0xF161926C 주소는 자식 프로세스의 struct task_struct 구조체에서 sibling 멤버 오프셋 만큼 떨어진 주소를 가르킵니다. struct task_struct 구조체에서 sibling 멤버 오프셋은 0x3EC 이므로 다음 계산으로 태스크 디스크립터 주소를 얻을 수 있습니다.
0xF1618E80 = 0xF161926C - 0x3EC

다음 명령어를 입력해서 태스크 디스크립터를 확인합시다.
v.v %l %t %h %s (struct task_struct*)(0xF161926C-0x3EC)
1  (struct task_struct *) [-] (struct task_struct*)(0xF161926C-0x3EC) = 0xF1618E80
2    (long int) [D:0xF1618E80] state = 0x1,
3    (void *) [D:0xF1618E84] stack = 0xB1630000,
4    (atomic_t) [D:0xF1618E88] usage = ((int) [D:0xB1618E88] counter = 0x3),
5    (unsigned int) [D:0xF1618E8C] flags = 0x04208040,
... 
6    (struct list_head) [D:0xF161926C] sibling = (
7      (struct list_head *) [D:0xF161926C] next = 0xF161A0EC,
8      (struct list_head *) [D:0xF1619270] prev = 0xF1618B24),
...
9    (char [16]) [D:0xF1619340] comm = "kworker/0:0H",

먼저 9번째 줄 디버깅 정보를 보면 프로세스 이름이 "kworker/0:0H"임을 알 수 있습니다.

2번째 줄 디버깅 정보를 보면 [D:0xF1618E80] 이란 주소가 보입니다.
이는 해당 멤버가 위치한 주소입니다.

struct task_struct 구조체 시작주소는 0xF1618E80이며, state 멤버는 0xF1618E80, stack은 0xF1618E84 주소에 위치합니다.

6번째 줄 디버깅 정보를 보면 sibling 이란 멤버가 위치한 주소인 0xF161926C 가 보입니다.
kthreadadd 프로세스의 태스크 디스크립터 children.next 멤버가 저장한 주소입니다.
6    (struct list_head) children = (
7      (struct list_head *) next = 0xF161926C =  -> (
8        (struct list_head *) next = 0xF161A0EC =  -> (

이번에는 "kworker/0:0H" 프로세스의 태스크 디스크립터 sibling 멤버가 가르키는 주소를 점검합시다.
1  (struct task_struct *)[-](struct task_struct*)(0xF161926C-0x3EC)= 0xF1618E80 -> (
2    (long int) [D:0xF1618E80] state = 0x1,
3    (void *) [D:0xF1618E84] stack = 0xB1630000,
4    (atomic_t) [D:0xF1618E88] usage = ((int) [D:0xB1618E88] counter = 0x3),
5    (unsigned int) [D:0xF1618E8C] flags = 0x04208040,
... 
6    (struct list_head) [D:0xF161926C] sibling = (
7      (struct list_head *) [D:0xF161926C] next = 0xF161A0EC,
8      (struct list_head *) [D:0xF1619270] prev = 0xF1618B24),

7번째 줄 디버깅 정보를 보면 연결 리스트인 sibling.next는 0xF161A0EC를 저장합니다.
이 주소는 형제 프로세스의 struct task_struct 구조체 sibling 멤버가 위치한 주소입니다.

0xF161A0EC 주소에서 struct task_struct 구조체 sibling 멤버 오프셋을 빼서 태스크 디스크립터를 봅시다.
v.v %l %t %h %s (struct task_struct*)(0xF161A0EC-0x3EC)
  (struct task_struct *)[-](struct task_struct*)(0xF161A0EC-0x3EC) = 0xF1619D00 -> (
    (long int) [D:0xF1619D00] state = 0x1,
    (void *) [D:0xF1619D04] stack = 0xF1634000,
    (atomic_t) [D:0xF1619D08] usage = ((int) [D:0xF1619D08] counter = 0x2),
    (unsigned int) [D:0xF1619D0C] flags = 0x04208060,
...
    (pid_t) [D:0xF161A0D0] pid = 0x5,
    (struct list_head) [D:0xF161A0EC] sibling = (
      (struct list_head *) [D:0xF161A0EC] next = 0xF161A82C,
      (struct list_head *) [D:0xF161A0F0] prev = 0xF161926C),
...
    (char [16]) [D:0xF161A1C0] comm = "kworker/0:0H",
...

태스크 디스크립터 정보를 볼 수 있습니다.
4.7.4 프로세스 연결 리스트
struct task_struct 멤버 중 tasks는 struct list_head 구조체로 연결 리스트입니다.

리눅스 커널에서 실행 중인 프로세스는 init_task.tasks 이란 연결 리스트로 등록되어 있습니다. 프로세스는 처음 생성될 때 init_task.tasks 이란 연결 리스트에 등록합니다.

이 동작은 copy_process() 함수에서 확인할 수 있습니다.
1 static __latent_entropy struct task_struct *copy_process(
2 unsigned long clone_flags,
3 unsigned long stack_start,
4 unsigned long stack_size,
5 int __user *child_tidptr,
6 struct pid *pid,
7 int trace,
8 unsigned long tls,
9 int node)
10 {
11 struct task_struct *p;
12 p = dup_task_struct(current, node);
...
13 list_add_tail_rcu(&p->tasks, &init_task.tasks);

12번째 줄 코드와 같이 태스크 디스크립터를 할당한 다음, 13번째 줄 코드에서 init_task.tasks 연결 리스트 마지막 노드에 현재 프로세스의 struct task_struct.tasks를 등록합니다.

임베디드 디버거의 전설인 Trace32 프로그램으로 태스크 디스크립터에서 프로세스 연결 리스트를 어떻게 관리하는지 점검합시다. 소스 코드 보다 코어 덤프를 통해 리눅스 커널 전역 변수를 직접 분석할 때 더 많은 것을 얻을 때가 있습니다.

다음은 init_task이란 전역 변수를 Trace32로 확인한 결과입니다.
v.v %l %t init_task
1  (static struct task_struct) [D:0xA1A171B8] init_task = (
2    (long int) [D:0xA1A171B8] state = 0,
3    (void *) [D:0xA1A171BC] stack = 0xA1A00000,
...
4    (struct sched_info) [D:0xA1A174A8] sched_info = ((long unsigned int) pcount = 0,
5    (struct list_head) [D: 0xA1A174C8] tasks = (
6      (struct list_head *) [D:0xA1A174E8] next = 0xA1618310 -> (
7        (struct list_head *) [D:0xA1618330] next = 0xA1618A70,
8        (struct list_head *) [D:0xA1618334] prev = 0xA1A174E8),
9      (struct list_head *) [D: 0xA1A174CC] prev = 0xA7778330),

init_task.tasks 멤버 구조체는 struct list_head와 연결 리스트이며 6~7번째 줄 디버깅 정보와 같이 next 멤버를 가르키고 있습니다. init_task 이란 전역 변수는 struct task_struct 구조체와 같이 태스크 디스크립터란 점을 기억합시다.

6번째 줄 로그에서 init_task.tasks.next는 0xA1618330 주소를 가르키고 있습니다. 이 주소는 어떤 의미일까요? init_task.tasks 이란 연결 리스트에 추가된 다음 프로세스의 struct task_struct.tasks 주소를 가르킵니다.

그림으로 표현하면 다음과 같습니다.

 

프로세스 실행 시각 정보
태스크 디스크립터에는 프로세스 실행 시각 정보를 알 수 있는 멤버들이 있습니다.

u64 utime;

유저 모드에서 프로세스 실행한 시각을 표현합니다.

이 멤버는 account_user_time() 함수 6번째 줄 코드에서 변경됩니다.
1 void account_user_time(struct task_struct *p, u64 cputime)
2 {
3 int index;
4
5 /* Add user time to process. */
6 p->utime += cputime;

u64 stime;

커널 모드에서 프로세스가 실행한 시각을 표현합니다.

이 멤버는 다음 코드와 같이 account_system_index_time() 함수에서 변경됩니다.
1 void account_system_index_time(struct task_struct *p,
2        u64 cputime, enum cpu_usage_stat index)
3{
4 /* Add system time to process. */
5 p->stime += cputime;
6

struct sched_info sched_info.last_arrival;

이 구조체는 프로세스 스케줄링 장에서 상세히 다룹니다.

sched_info 멤버는 프로세스 스케줄링 정보를 저장합니다. 이 중 last_arrival은 프로세스가 마지막에 CPU에서 실행된 시간을 저장합니다.

프로세스가 스케줄링으로 실행을 시작하기 직전 context_switch() 함수를 호출합니다.
context_switch() 함수에서 다음 함수 흐름으로 sched_info_switch() 함수를 호출해서 실행 시각을 sched_info.last_arrival 멤버에 저장합니다.
context_switch
   prepare_task_switch
      sched_info_switch
         __sched_info_switch
 sched_info_arrive

sched_info.last_arrival 멤버는 sched_info_arrive() 함수 9번째 줄 코드에서 업데이트 됩니다.
1 static void sched_info_arrive(struct rq *rq, struct task_struct *t)
2 {
3 unsigned long long now = rq_clock(rq), delta = 0;
4
5 if (t->sched_info.last_queued)
6 delta = now - t->sched_info.last_queued;
7 sched_info_reset_dequeued(t);
8 t->sched_info.run_delay += delta;
9 t->sched_info.last_arrival = now;

크래시 유틸리티 프로그램으로 ps -l 명령어를 입력하면 다음과 같이 프로세스가 실행된 시각은
나노초 단위로 표현합니다.
1 > crash> p jiffies
2 > jiffies = $9 = 5310085968
3 > crash>
4 > 
5 > crash> ps -l
6 > ..
7 > [4058835599089874] PID: 4136 TASK: ffff8801309ce640 CPU: 4 COMMAND: "kcapwdt" 

5번째 줄에서 ps란 명령어에 “-l” 옵션으로 추가해서 입력하니 7번째 줄과 같이 나노초 단위로 프로세스 실행 시각 정보를 표현합니다.

7번째 줄 정보는 태스크 디스크립터 struct task_struct 주소가 ffff8801309ce640인 "kcapwdt" 프로세스(PID: 4136) 가 4058835599089874 시각에 마지막 실행했다고 해석할 수 있습니다. 4058835599089874 시간 정보는 “kcapwdt” 프로세스의 태스크 디스크립터 struct task_struct 멤버인 sched_info.last_arrival 에 저장된 값입니다.


+ Recent posts