리눅스 커널 코드를 읽으면 다음 매크로를 자주 볼 수 있습니다.
+ current_thread_info()
+ current

위 매크로를 활용해서 프로세스 태스크 디스크립터와 struct thread_info 구조체 필드 정보를 읽어 프로세스를 제어합니다. 

4.10.1 current_thread_info()
이전 절에서 프로세스 스택 최상단 주소에 struct thread_info 구조체에 프로세스 실행을 저장한다고 분석했습니다.

current_thread_info() 매크로 함수는 프로세스가 어떤 함수를 실행하더라도 struct thread_info 구조체가 있는 스택 최상단 주소를 전달합니다.

current_thread_info() 매크로 함수 구현부를 분석하기 전에 매크로 함수가 어느 코드에서 쓰는 지 알아봅시다.
1  static __always_inline int preempt_count(void)
2  {
return READ_ONCE(current_thread_info()->preempt_count);
4  }
5
6  static __always_inline volatile int *preempt_count_ptr(void)
7  {
8 return &current_thread_info()->preempt_count;
9  }
10
11 static __always_inline void preempt_count_set(int pc)
12 {
13 *preempt_count_ptr() = pc;
14 }

1번째 줄 코드에서 preempt_count() 함수는 current_thread_info() 함수를 써서 struct thread_info 구조체 멤버인 preempt_count 에 접근합니다.

6번째 줄 코드에 있는 preempt_count_ptr() 함수는 struct thread_info 구조체 멤버 preempt_count 주소를 반환합니다.

11~13번째 줄 코드는 preempt_count_ptr() 함수를 써서 struct thread_info 구조체 멤버 preempt_count에 pc란 인자를 저장합니다.

이번에는 다른 코드를 봅시다.
#define raw_smp_processor_id() (current_thread_info()->cpu)

struct thread_info 구조체 멤버인 cpu에 접근하는 매크로 함수입니다.
프로세스가 현재 실행 중이거나 이전에 실행했던 CPU 번호를 저장합니다.

위에서 다룬 리눅스 커널 전반에 아주 많이 쓰는 함수니 눈에 잘 익히도록 합시다.

이제 current_thread_info() 매크로 함수 코드를 분석하겠습니다.

1 register unsigned long current_stack_pointer asm ("sp");
2
3 static inline struct thread_info *current_thread_info(void)
4 {
5 return (struct thread_info *)
6 (current_stack_pointer & ~(THREAD_SIZE - 1));
7 }

참고로, THREAD_SIZE 이란 0x2000 값으로 ARM 아키텍처에서 실행하는 프로세스 스택 사이즈를 의미합니다.
#define THREAD_SIZE 0x2000

ARM64 비트 아키텍처에서는 커널 프로세스 스택 사이즈가 0x4000 바이트입니다.
이 책에서는 라즈베리파이가 탑재한 ARM32 아키텍처를 기준으로 코드를 분석합니다.

current_thread_info() 매크로 함수에서 current_stack_pointer 코드의 의미를 먼저 확인합시다.

이 코드는 현재 구동 중인 프로세스의 스택 주소를 current_stack_pointer 변수로 가져오는 명령어입니다.
1 register unsigned long current_stack_pointer asm ("sp");

asm ("sp")  어셈블리 명령어는 현재 실행 중인 프로세스 스택 주소를 알려줍니다. 

우리가 보는 모든 함수는 프로세스 스택 공간에서 실행합니다. 위 명령어와 조합해서 1번 코드와 같이 선언하면 current_stack_pointer 전역 변수에 스택 주소를 저장합니다.

분석한 내용을 적용해서 6~7번째 줄 코드를 다음과 같이 변경해봅시다.
(current_stack_pointer & ~((0x2000) - 1));
(current_stack_pointer & ~(0x1fff));

THREAD_SIZE가 0x2000이고 0x2000 에서 1을 빼면 0x1fff입니다.

위 코드에서 ~란 비트 연산자는 비트를 모두 역전시킵니다. 
0x1fff를 이진수로 바꿔서 ~연산을 수행하면 다음과 같은 결괏값을 얻을 수 있습니다.
0001|1111|1111|1111
~연산
-------------------
1110|0000|0000|0000

1110|0000|0000|0000 이진수를 16진수로 바꾸면 0xE000이 되는 것입니다.

현재 스택 주소에서 0xE000란 값을 AND 연산하는 코드입니다. 
(current_stack_pointer & ~((0x2000) - 1));
(current_stack_pointer & ~(0x1fff));
(current_stack_pointer & (0xe000));

그런데 현재 스택 주소에서 0xE000을 AND 연산하는 게 어떤 의미가 있을까요?
아래 볼드체로된 0--12비트는 모두 0이니 다른 주소와 AND 연산을 하면 0이 됩니다.
1110|0000|0000|0000(0xE000)

0xD000C248 & (0000|0000|0000) 연산을 하면 0xD000C000이 됩니다. 16진수 3자리 값을 모두 0으로 변환시킵니다. 
1100|0000|0000|0000|1011|0010|0100|1000 (0xD000C248)
                              0000|0000|0000
AND ------------------------------------------
1100|0000|0000|0000|1011|0000|0000|0000 (0xD000C000)


이번에 0xE000 값에서 0xE만 빼서 이진수로 어떤 동작을 하는지 확인할 차례입니다.
이제 0xE와 AND 비트 연산을 하는 의미를 알아보려는 것입니다.
1110|0000|0000|0000

1110비트 연산을 수행하면 현재 값이 짝수면 그 값을 그대로 유지하고 홀수면 -1만큼 뺍니다.
아래 연산 결과를 참고하시면 이해가 빠를 겁니다.
 
이제 정리를 합시다. 각각 스택 주소를 다음과 같다고 했을 때 0xE000와 AND 연산을 하면 결과값은 다음과 같습니다.
스택주소 
0xD0002248 & 0xE000 = 0xD0002000
0xD0003248 & 0xE000 = 0xD0002000
0xD0004248 & 0xE000 = 0xD0004000
0xD0005248 & 0xE000 = 0xD0004000

현재 스택 주소에서 위 규칙으로 비트를 연산하는 이유는 뭘까요? ARM32 비트 아키텍처에서 0x2000바이트 크기만큼 스택을 지정합니다. 커널이 프로세스 스택 주소를 할당할 때 0x2000바이트 기준 정렬을 맞춰서 할당하기 때문입니다. 프로세스 최상단 주소는 짝수 바이트입니다. 그래서 위와 같은 비트 연산으로 스택 최상단 주소를 계산한 것입니다.

4.10.2 current 매크로란
current란 매크로는 현재 구동 중인 프로세스의 태스크 디스크립터 주소에 접근합니다.

먼저 current 매크로를 쓰는 코드를 보겠습니다. 파일 디스크립터를 할당하는 get_unused_fd_flags() 함수입니다.
1 int get_unused_fd_flags(unsigned flags)
2 {
3 return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
4 }
5 EXPORT_SYMBOL(get_unused_fd_flags);

3번째 줄 코드를 보면 current 변수로 current->files 멤버에 접근해서 __alloc_fd 함수 첫 번째 파라미터로 전달합니다. 코드를 조금 눈여겨보면 뭔가 이상합니다. 어느 코드에도 current를 지역변수나 전역 변수로 선언한 흔적이 없습니다.

하지만 current 매크로는 struct task_struct 구조체 주소를 포인터 형태로 반환합니다.
그래서 current->files 코드로 struct task_struct 구조체 멤버 files(파일 디스크립터)에 접근하는 것입니다.

이번에는 __put_task_struct() 함수 코드를 봅시다.
1 void __put_task_struct(struct task_struct *tsk)
2 {
3 WARN_ON(!tsk->exit_state);
4 WARN_ON(atomic_read(&tsk->usage));
5 WARN_ON(tsk == current);

5번째 줄 코드를 보면 __put_task_struct() 함수로 전달된 struct task_struct 타입 tsk 파라미터와 current가 같은지 체크를 합니다. 이번도 마찬가지입니다. 함수 내 current란 변수를 선언한 흔적이 없습니다. 그런데 갑자기 current란 변수가 나타났습니다.

__put_task_struct() 함수 인자인 tsk는 struct task_struct 구조체 포인터 타입인데 current와 같은지 비교하는 코드입니다.

current 매크로는 리눅스 커널 코드에서 굉장히 많이 볼 수 있습니다. 이 코드의 의미를 정확히 이해해야 커널 코드를 읽는 데 문제가 없을 것입니다.

current 매크로에 대해서 알아봅시다. 
current는 현재 구동 중인 프로세스의 태스크 디스크립터의 주소를 알려주는 역할을 수행합니다. 구조체는 struct task_struct이고 포인터형 변수입니다. 그래서 current->comm, current->files 형태로 각 멤버에 접근합니다. 

해당 코드는 다음과 같습니다.
1 #define get_current() (current_thread_info()->task)
2 #define current get_current()

current 매크로는 get_current() 매크로 함수로 치환되는데 이전 소절에 다뤘던 current_thread_info() 매크로 함수를 써서 task 멤버 주소를 반환합니다.

실행 중인 프로세스 스택 주소를 이용해 최상단 주소에 접근해서 struct thread_info 구조체 task 멤버 주소를 반환하는 코드입니다.



프로세스 컨택스트 정보는 어떻게 저장할까? 


리눅스 커널에서는 프로세스 실행 흐름은 컨택스트란 용어로 표현합니다.

컨택스트 종류는 다음과 같습니다.
   1. 프로세스 컨택스트: 프로세스가 스케줄링 될 수 있는 상태
   2. 인터럽트 컨택스트: 인터럽트가 발생한 후 인터럽트 핸들링을 위한 동작 중
   3. Soft IRQ 컨택스트: Soft IRQ 서비스를 실행 중이며 Soft IRQ 서브루틴(하부 함수 흐름) 동작 중

컨택스트 정보는 struct thread_info 구조체 preempt_count 멤버에서 확인할 수 있습니다. 커널은 이 값을 읽어서 컨택스트 종류를 식별합니다.

커널에서는 어떻게 프로세스가 어떤 컨택스트에서 실행하는지 파악할까요?
in_interrupt()와 in_softirq() 매크로 함수를 통해 확인할 수 있습니다. 해당 코드는 현재 실행 중인 코드가 어떤 컨택스트인지 알려주는 목적으로 구현됐습니다. 

4.9.1 컨택스트 소개
이번에는 컨택스트의 의미에 대해 알아봅시다.
컨택스트는 프로세스 실행 그 자체를 의미합니다. 예를 들어 리눅스 커널에서 가장 많이 쓰이는 컨택스트 스위칭은 실행 흐름을 바꾼다고 해석할 수 있습니다.

컨택스트는 어떤 코드로 표현할 수 있을까요? struct cpu_context_save 구조체입니다.
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};

struct cpu_context_save 구조체 필드들은 ARM 프로세스 레지스터들을 의미합니다. 컨택스트 스위칭을 수행할 때 마지막에 실행 중인 레지스터 세트를 위 구조체 필드에 저장합니다. 

컨택스트 스위칭을 수행하는 핵심 함수는 switch_to() 이며 세부 동작은 __switch_to 레이블에 어셈블리 코드로 구현되어 있습니다.
#define switch_to(prev,next,last) \
do { \
__complete_pending_tlbi(); \
last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \
} while (0)

__switch_to 레이블 구현부 코드를 잠깐 봅시다.
ENTRY(__switch_to)
.fnstart
2 .cantunwind
3 add ip, r1, #TI_CPU_SAVE
4 stmia ip!, {r4 - r11} @ Store most regs on stack
5 str sp, [ip], #4
6 str lr, [ip], #4
7 mov r5, r0
8 add r4, r2, #TI_CPU_SAVE
9 ldr r0, =thread_notify_head
10 mov r1, #THREAD_NOTIFY_SWITCH
11 bl atomic_notifier_call_chain
12 mov ip, r4
13 mov r0, r5
14 ldmia ip!, {r4 - r11} @ Load all regs saved previously
15 ldr sp, [ip]
16 ldr pc, [ip, #4]!
17 .fnend
ENDPROC(__switch_to)

위 코드 4번째 줄 코드에서 실행 중인 레지스터 세트를 struct thread_info.cpu_context를 저장합니다.

스케줄링 실행으로 다시 실행할 프로세스는 자신의 스택 최상단 주소에 접근해서 struct thread_info.cpu_context에 저장된 레지스터 값을 ARM 레지스터로 로딩합니다. 이 동작은 14번째 줄 코드에서 동작합니다.

이렇게 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 멤버 cpu_context에 실행 중인 ARM 레지스터 세트를 저장하는 것입니다. 컨택스트 스위칭할 때 프로세스 실행 정보를 담고 있는 레지스터 세트를 저장하는 세부 동작은 스케줄링 장에서 상세히 다룹니다.

4.9.2 인터럽트 컨택스트 정보 확인하기
인터럽트 컨택스트란 현재 실행 중인 코드가 인터럽트가 발생한 후 호출되는 서브 함수 중 하나란 의미입니다. 즉, 인터럽트에 대한 핸들링을 진행 중인 상태라 볼 수 있습니다.

리눅스 시스템에서 인터럽트는 언제든 발생할 수 있습니다. 인터럽트가 발생하면 프로세스 실행을 멈추고 인터럽트에 해당하는 인터럽트 핸들러를 실행합니다. 리눅스 커널에서는 인터럽트가 발생해서 인터럽트 서비스 루틴을 실행하는 동작을 인터럽트 컨택스트라고 말합니다. 

컨텍스트란 실행 그 자체란 의미라고 설명을 드렸습니다. 이 용어 앞에 인터럽트가 추가됐으니 인터럽트를 핸들링 중인 상태를 의미합니다.

프로세스가 인터럽트 컨택스트인지 어떻게 확인할 수 있을까요? 프로세스 최상단 주소에 있는 struct thread_info 구조체 preempt_count 필드를 읽어 확인합니다.

인터럽트 컨택스트 시작 정보는 어느 함수에서 설정할까?
커널은 인터럽트가 발생 한 후 인터럽트를 핸들링을 시작할 때 struct thread_info 구조체 preempt_count 필드에 인터럽트가 실행 중인 상태를 나타내는 비트를 설정합니다. 커널은 이 비트를 읽어서 인터럽트 컨택스트 유무를 식별합니다. 리눅스 커널에서는 현재 실행 중인 코드가 인터럽트 컨택스트인지 알 수 있는 in_interrupt() 매크로 함수를 제공합니다.

다음 그림을 보면서 인터럽트가 발생했을 때 어떤 흐름으로 struct thread_info 구조체 preempt_count 멤버가 업데이트되는지 확인합시다.

 

위 그림에서 함수 실행 흐름은 다음과 같습니다.
1. __wake_up_common_lock() 함수 실행 도중에 인터럽트 발생
2. 인터럽트 벡터인 __irq_svc 레이블 실행
3. 다음과 같은 인터럽트 제어 함수 호출
 : bcm2836_arm_irqchip_handle_irq()/__handle_domain_irq() 
4. __handle_domain_irq() 함수에서 irq_enter() 매크로 함수를 호출
4.1 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 
  멤버에 HARDIRQ_OFFSET(0x10000) 비트 설정
5. 화살표 방향으로 함수를 계속 호출해서 인터럽트 핸들러 함수인 usb_hcd_irq() 함수 호출
  5.1 서브 루틴 함수 실행

위에서 소개한 함수 실행 흐름에서 4.1 단계 이후 실행하는 모든 서브 함수에서 in_interrupt() 함수를 호출하면 0x10000을 반환합니다. 따라서 다음과 같이 if 문과 in_interrupt() 매크로를 함께 쓰면 실행 코드가 인터럽트 컨택스트인지 확인할 수 있습니다.
if (in_interrupt()) {
// 인터럽트 컨택스트 실행 시 제어 코드
}

인터럽트 서비스 루틴 실행을 시작할 때 struct thread_info 구조체 preempt_count 멤버에 HARDIRQ_OFFSET 비트를 더하는 코드는 다음과 같습니다.
1 #define __irq_enter() \
2 do { \
3 account_irq_enter_time(current); \
4 preempt_count_add(HARDIRQ_OFFSET); \
5 trace_hardirq_enter(); \
6 } while (0)

4번째 줄 코드를 실행해서 struct thread_info 구조체 preempt_count 멤버에 HARDIRQ_OFFSET(0x10000) 비트를 더합니다..

preempt_count_add(val) 매크로 함수는 프로세스 스택 최상단 주소에 접근해서 preempt_count 멤버에 val인자를 더하는 동작입니다.
#define preempt_count_add(val) __preempt_count_add(val)

인터럽트 컨택스트 종료는 어느 함수에서 설정할까?
이번에는 인터럽트 컨택스트가 끝나는 시점을 어느 코드에서 설정하는지 확인하겠습니다.
이를 위해 인터럽트 서비스 루틴을 종료한 다음에 호출하는 irq_exit() 함수를 보겠습니다.
1 void irq_exit(void)
2 {
...
3 account_irq_exit_time(current);
4 preempt_count_sub(HARDIRQ_OFFSET);

4번째 줄 코드를 실행하면 struct thread_info 구조체 preempt_count 필드에 HARDIRQ_OFFSET 비트를 뺍니다.

다음 그림에서 인터럽트 핸들링을 마무리한 다음 irq_exit() 함수를 실행하는 코드 흐름을 확인할 수 있습니다.

 

인터럽트 핸들링을 끝냈으니 다시 검은색 화살 방향으로 함수 호출이 수행됩니다.

irq_exit() 매크로 함수를 실행해서 struct thread_info.preempt_count 필드에서 HARDIRQ_OFFSET 비트를 뺍니다. 그러니 in_interrupt() 매크로는 FALSE를 반환합니다.
irq_exit() 함수 호출 이후에는 리눅스 커널은 인터럽트 컨택스트가 아니라 판단하는 것입니다.
인터럽트 컨택스트에 대한 세부 동작은 5장 인터럽트에서 확인할 수 있습니다.
 
리눅스 커널에서 실행 중인 코드가 인터럽트 컨택스트인지 점검하는 루틴은 매우 많습니다. 

커널은 인터럽트 컨택스트 실행 여부를 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 preempt_count 멤버에 저장합니다.

현재 실행 중인 코드가 인터럽트 컨택스트인지 식별하는 것을 매우 중요합니다. 인터럽트 컨택스트에서 호출할 수 있는 커널 함수가 제한돼 있기 때문입니다. 예를 들어 스케줄링을 할 수 있는 뮤덱스, 세마포어아 같은 함수를 쓸 수 없습니다. 그 이유는 인터럽트가 발생하면 빨리 코드를 실행해야 하기 때문입니다.

다음 소절에서는 프로세스 struct thread_info 구조체 preempt_count 필드에 Soft IRQ 컨택스트를 어떻게 저장하고 읽는 지 살펴봅니다.

4.9.3 Soft IRQ 컨택스트 정보 확인하기
“어떤 프로세스가 Soft IRQ 컨택스트이다.”란 문장은 현재 실행 중인 코드가 Soft IRQ 서비스를 실행 중이라는 의미입니다.

Soft IRQ 컨택스트 정보 설정는 어디서 설정할까?
인터럽트 컨택스트와 마찬가지로 프로세스가 Soft IRQ를 처리 중인 상태면 프로세스 struct thread_info 구조체 preempt_count 필드에 SOFTIRQ_OFFSET 매크로를 저장합니다.

Soft IRQ가 실행하는 과정을 다음 그림에서 같이 봅시다.

 

Soft IRQ는 인터럽트 핸들링이 끝나면 실행을 시작합니다. Soft IRQ 서비스를 실행하는 __do_softirq() 함수에서 프로세스 스택 최상단 주소에 접근해서 struct thread_info preempt_count 필드에 SOFTIRQ_OFFSET(0x100) 매크로를 설정합니다. SOFTIRQ_OFFSET(0x100)는 Soft IRQ 실행 중 혹은 Soft IRQ 컨택스트임을 알려줍니다.

Soft IRQ 컨택스트 시작을 프로세스 struct thread_info preempt_count 필드에 저장하는 코드를 분석하겠습니다. 
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2 {
...
3 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
4           // Soft IRQ 서비스 실행
5 ...
6 void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
7 {
...
8 __preempt_count_add(cnt);

3번째 줄 코드와 같이 __do_softirq() 함수에서 __local_bh_disable_ip() 함수, 
8번째 줄 코드에서 __local_bh_disable_ip() 함수는 __preempt_count_add() 함수를 호출합니다.

__preempt_count_add() 함수는 전달된 인자를 실행 중인 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 필드에 SOFTIRQ_OFFSET 매크로 비트를 더합니다.

Soft IRQ 서비스를 실행하는 서브 함수에서 in_softirq() 매크로를 if 문과 함께 쓰면 현재 실행 중인 코드가 Soft IRQ 컨택스트인지 알 수 있습니다.

Soft IRQ 컨택스트 종료 상태는 어디서 저장할까?
이번에는 Soft IRQ 서비스 함수 호출을 마친 다음에 어떤 동작을 하는지 알아보겠습니다.
해당 코드 흐름은 다음 그림과 같습니다.

 

Soft IRQ 서비스 함수 실행을 완료하면 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 멤버에서 SOFTIRQ_OFFSET(0x100) 비트를 뺍니다.

이 동작은 irq_exit() 함수에서 실행하면 이후 검은색 화살표 방향으로 호출되는 함수에서 in_softirq() 함수는 FALSE를 반환합니다.

 __do_softirq() 함수 마지막 부분 코드를 봅시다. Soft IRQ 실행이 끝났다는 정보를 저장하는 동작입니다.
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2 {
3
4 // Soft IRQ 서비스 루틴
...
5 lockdep_softirq_end(in_hardirq);
6 account_irq_exit_time(current);
7 __local_bh_enable(SOFTIRQ_OFFSET);
8
9 static void __local_bh_enable(unsigned int cnt)
10 {
...
11 preempt_count_sub(cnt);
}

7번째 줄 코드와 같이 __local_bh_enable() 함수를 호출하고 preempt_count_sub() 함수를 실행합니다.

이번에는 실행 중인 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 멤버에 SOFTIRQ_OFFSET 매크로 비트를 뺍니다.

4.9.4 선점 스케줄링 여부 정보 저장
프로세스는 실행 도중 언제든 선점 스케줄링이 될 수 있습니다. 즉, 언제든 실행을 멈추고 다른 프로세스로 스케즐링 될 수 있습니다. 

물론 프로세스는 스스로 schedule() 함수를 호출해서 명시적으로 스케줄링할 수 있지만 많은 경우 선점 스케줄링됩니다. 선점 스케줄링의 진입점은 다음과 같습니다.
 + 인터럽트 핸들링 후
 + 시스템 콜 핸들링 후

선점 스케줄링 여부를 점검할 때 프로세스 스택 최상단에 접근 후 struct thread_info 구조체 preempt_count 필드 정보를 읽습니다. 이 값을 기준으로 선점 스케줄링을 실행할 지 결정합니다. 

관련 코드를 함께 봅시다.
1 __irq_svc:
2 svc_entry
3 irq_handler
4
5 #ifdef CONFIG_PREEMPT
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0
10 tst r0, #_TIF_NEED_RESCHED
11 blne svc_preempt
12 #endif

낯선 어셈블리 코드이지만 차근차근 코드 의미를 분석해겠습니다.

1~3번째 줄 코드를 보겠습니다.
1 __irq_svc:
2 svc_entry
3 irq_handler

__irq_svc 레이블은 인터럽트가 발생했을 때 실행하는 인터럽트 벡터를 의미하며 2~3번째 줄 코드는 인터럽트에 대한 핸들링을 수행합니다. 인터럽트가 발생하면 인터럽트에 대응하면 인터럽트 핸들러 함수를 실행하는 역할을 수행합니다.

6~11번째 줄 코드가 스케줄링을 실행할 지 결정하는 어셈블리 코드입니다.
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0
10 tst r0, #_TIF_NEED_RESCHED
11 blne svc_preempt

어셈블리 코드를 이해하기 쉽게 C 코드로 표현하면 다음과 같습니다.
a    struct thread_info *current_thread_info = task_thread_info(current);
b
c   if (current_thread_info.preempt_count == 0 ) {
d if (current_thread_info.flags & _TIF_NEED_RESCHED) {
e  svc_preempt();  // 스케줄링 실행
f            }
g    else {
h current_thread_info.flags = 0;
i    }

a 코드는 프로세스 스택 최상단 주소에 접근해 struct thread_info 구조체에 접근하며 이 값을 current_thread_info 란 변수에 저장합니다.

preempt_count 멤버가 0이면 flags 멤버와 _TIF_NEED_RESCHED(0x2) 와 AND 비트 연산을 수행해 연산 결과가 true이면 svc_preempt() 함수를 호출해서 스케줄링을 실행하는 것입니다.

struct thread_info 구조체 preempt_count와 flags 멤버는 스케줄링 실행을 결정하는 중요한 정보를 저장하고 있습니다.

이번 소절에서 다룬 내용을 정리하면 preempt_count 멤버는 프로세스 컨택스트와 스케쥴링 가능 정보를 저장합니다.
1. 인터럽트 컨택스트
2. Soft IRQ 컨택스트
3. 프로세스 스케줄링 가능 여부



스레드 정보(struct thread_info)
프로세스 상세 정보를 저장하는 자료구조는 태스크 디스크립터입니다. 이와 더불러 프로세스 세부 실행 정보를 저장하는 struct thread_info 자료구조가 있습니다.

커널이 프로세스를 생성할 때 프로세스마다 고유한 스택 공간을 부여합니다. 스택 최상단 주소에 struct thread_info 구조체 필드가 저장돼 있습니다. 이 구조체 필드는 커널이 프로세스를 제어할 때 필요한 중요 정보를 담고 있습니다.
+ 프로세스 컨택스트 정보
+ 선점 스케줄링 여부
+ 컨택스트 스위칭 시 저장한 레지스터 세트

이번 절에서는 프로세스 동작을 표현하는 struct thread_info 구조체에 대해서 알아봅시다.

struct thread_info 구조체 각 멤버에 대해 알아보기 전에 이 구조체가 프로세스 어느 주소에 위치했는지 먼저 점검하겠습니다. 

4.8.1 프로세스는 스택을 어떻게 쓸까?
struct thread_info 구조체 필드는 프로세스 스택 최상단 주소에 저장됩니다. 
다음 그림을 함께 보겠습니다.

 

위 그림에서 프로세스 스택 최상단 주소는 0x80C00000 그리고 스택 최하단 주소는 0x80C02000입니다. 

스택 최하단 주소가 0x80C02000인 이유는 뭘까요? ARM 아키텍처에서 프로세스가 실행하는 스택 사이즈는 0x2000 바이트로 고정돼 있기 때문입니다.

프로세스가 스택을 어떻게 쓰는지 간단히 알아봅시다. 

리눅스 커널에서 프로세스 스택은 높은 주소에서 낮은 주소 방향으로 자랍니다.
그림에서 화살표 방향을 눈여겨봅시다. 함수 호출은 프로세스 스택 최하단 주소(0x80C02000)에서 최상단 주소(0x80c0000) 방향입니다.

먼저 프로세스가 실행할 때는 커널 함수를 호출합니다. sys_write() 함수에서 vfs_write() 함수 방향으로 함수를 호출하는 것입니다.

가장 먼저 호출된 함수는 프로세스 스택 최하단 주소에 위치해있고 프로세스 스택 최상단 주소 방향으로 스택을 사용합니다.

또한 함수를 실행하면서 로컬 변수나 복귀 레지스터(링크드 레지스터: 자신을 호출한 함수 주소)도 프로세스 스택에 저장(푸시) 합니다. 이 때 필요한 스택 메모리를 프로세스 스택 공간을 최하단 주소에서 최상단 주소 방향으로 사용합니다.

쉽게 설명하면, 프로세스 스택은 최하단 주소에서 최상단 주소로 자란다고 볼 수 있습니다.

__wake_up_common_lock() 함수가 실행을 마치면 pipe_write() 함수로 복귀하고,
pipe_write() 함수 실행이 끝나면 __vfs_write() 함수로 복귀합니다. 자료구조 시간에 배웠던 스택(푸시, 팝)과 같은 동작입니다.

이번에는 스택 용어로 프로세스가 스택 공간에서 어떻게 동작하는지 조금 더 알아봅시다.
프로세스가 새로운 함수를 호출하면 프로세스 스택 공간에 스택에 이전에 호출했던 복귀 레지스터를 스택에 푸시합니다. 함수 실행이 끝나고 자신을 호출한 함수 주소를 알아야 이전 함수로 복귀할 수 있기 때문입니다.

프로세스가 함수 실행을 마치고 이전에 자신을 호출했던 함수로 돌아가려고 할 때 복귀 레지스터를 스택에 팝(Pop)합니다. 함수 실행이 끝나고 자신을 호출한 함수 주소를 알아야 이전 함수로 복귀할 수 있기 때문입니다.

여기서 한 가지 의문이 생깁니다.
프로세스가 새로운 함수를 실행할 때 프로세스 스택 푸시와 팝하는 리눅스 커널 코드를 어디서 볼 수 있을까?

C 코드에서는 스택 푸시와 팝하는 코드를 볼 수는 없습니다. 대신 어셈블리 코드에서 관련 코드를 확인할 수 있습니다. 프로세스 스택 공간에 푸시 팝 동작은 CPU 아키텍처(ARM, x86, ARM64) 마다 다르기 때문입니다.

__wake_up_common_lock() 함수 코드를 봅시다.
1 NSR:8015F23C|E1A0C00D  __wake_up_common_lock:  cpy     r12,r13
2 NSR:8015F240|E92DDFF0                               push    {r4-r12,r14,pc}
...
3 NSR:8015F2E8|E89DAFF0                               ldm     r13,{r4-r11,r13,pc}

1번째 줄 어셈블리 코드를 실행하면 현재 스택(r13) 주소를 r12 레지스터에 저장합니다.
조금 더 알아보기: cpy 명령어에 대해서

r1 레지스터가 0x1이고 r2 레지스터가 0xFFFF 라고 가정합시다.
다음 명령어를 실행하면 r1가 0xFFFF로 변경됩니다.
cpy r1 r2

cpy 어셈블리 명령어는 오른쪽에 있는 레지스터가 저장한 값을 왼쪽 레지스터에 저장하는 명령어입니다.
2번째 줄 코드에서 push란 명령어를 써서 프로세스 스택 공간에 r4, r5, r6, r7, r8, r9, r10, r11, r12, r14 그리고 pc 레지스터를 푸시합니다.

3번째 줄 코드는 __wake_up_common_lock() 함수 마지막 어셈블리 코드입니다.
ldm이란 명령어를 입력해서 스택에 푸시한 r4, r5, r6, r7, r8, r9, r10, r11, r12, r14 그리고 pc 레지스터를 팝하는 동작을 수행합니다. 스택에 저장한 레지스터 값들을 ARM 프로세스 레지스터로 다시 복사하는 과정입니다.

스택 메모리에 저장된 주소를 ARM 프로세스 PC(Program Counter)에 저장하면 PC에 저장된 주소에 있는 어셈블리 코드 Fetch해서 실행합니다.

4.8.2 struct thread_info 구조체 소개
struct thread_info 구조체에는 프로세스 실행 흐름에 대한 중요한 정보를 저장합니다. 프로세스 스케줄링 실행 시 이전에 실행했던 레지스터 세트와 프로세스 컨택스트 정보를 확인할 수 있습니다.

struct thread_info 구조체 코드를 살펴봅시다.
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
};


구조체 파일 위치가 arch/arm/include 이므로 CPU 아키텍처에 의존적인 코드임을 짐작할 수 있습니다.

unsigned long flags;

아키텍처에 의존적인 프로세스 동작을 저장하는 멤버입니다.
#define TIF_SIGPENDING 0
#define TIF_NEED_RESCHED 1
#define TIF_NOTIFY_RESUME 2 /* callback before returning to user */
#define TIF_UPROBE 7
#define TIF_SYSCALL_TRACE 8

int preempt_count;

프로세스 컨택스트(인터럽트 컨택스트, Soft IRQ 컨택스트) 실행 정보와 프로세스가 스케줄링(Preemption)될 수 있는 조건을 저장합니다. struct thread_info 구조체에서 가장 중요한 멤버 중 하나입니다.

다음 소절에서 이 멤버를 리눅스 커널 코드에서 어떻게 접근하고 활용하는지 점검합니다.

__u32 cpu;
프로세스가 실행 중인 CPU 번호를 저장하는 멤버입니다.

struct task_struct *task;

실행 중인 프로세스의 태스크 디스크립터 주소를 저장합니다.

struct cpu_context_save cpu_context;

컨택스트 스위칭 되기 전 실행했던 레지스터 세트를 저장하는 멤버입니다. 프로세스가 스케줄링되고 다시 실행을 시작할 때 cpu_context란 멤버에 저장된 레지스터를 로딩합니다.

struct cpu_context_save 구조체 선언부에 있는 각각 필드는 레지스터를 의미합니다.
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};

프로세스가 컨택스트 스위칭할 때 struct cpu_context_save 구조체에 정의된 멤버에 실행 중인 레지스터를 저장하는 것입니다.

위에서 다룬 struct thread_info 구조체가 어떤 값을 저장하는지 Trace32로 확인해 보겠습니다.
v.v %t %h %d %i  (struct thread_info*)0xCE4F8000 
1   (struct thread_info *) (struct thread_info*)0xCE4F8000 = 0xCE4F8000 -> (
2   (long unsigned int) flags = 2 = 0x2,
3    (int) preempt_count = 2097155 = 0x00100003,
4    (mm_segment_t) addr_limit = 3204448256 = 0xBF000000,
5    (struct task_struct *) task = 0xD2FB2B80,
6    (struct exec_domain *) exec_domain = 0xC1A1AFDC,
7    (__u32) cpu = 5 = 0x5,
8    (__u32) cpu_domain = 0 = 0x0,
9    (struct cpu_context_save) cpu_context = (
10      (__u32) r4 = 4056868736 = 0xF1CEE780,
11      (__u32) r5 = 3539676032 = 0xD2FB2B80,
12      (__u32) r6 = 3664491200 = 0xDA6BB2C0,
13      (__u32) r7 = 3688475136 = 0xDBD9AA00,
14      (__u32) r8 = 3248547128 = 0xC1A0E538,
15      (__u32) r9 = 3496897024 = 0xD06E6A00,
16      (__u32) sl = 3539677168 = 0xD2FB2FF0,
17     (__u32) fp = 3461324268 = 0xCE4F9DEC,
18      (__u32) sp = 3461324200 = 0xCE4F9DA8,
19      (__u32) pc = 3237955160 = 0xC0FF4658,
20      (__u32 [2]) extra = ([0] = 0 = 0x0, [1] = 0 = 0x0)),

2번째 줄을 봅시다.
2   (long unsigned int) flags = 2 = 0x2,

flags가 2이니 TIF_NOTIFY_RESUME입니다. 해당 프로세스가 시스템 콜을 실행한 다음 커널에서 실행 중이란 의미입니다.
#define TIF_NOTIFY_RESUME 2 /* callback before returning to user */

유저 레벨 프로세스는 시스템 콜 실행 후 유저 모드로 돌아갑니다.

3번째 줄을 봅시다.
3    (int) preempt_count = 2097155 = 0x00100003,

preempt_count 멤버에 0x00100000 비트가 포함됐으니 인터럽트 컨택스트이고 3이란 값은 현재 프로세스가 스케줄링 가능한 조건임을 의미합니다.

5번 줄을 볼 차례입니다.
5    (struct task_struct *) task = 0xD2FB2B80,

태스크 디스크립터 주소를 저장하는 task 멤버입니다.

7번째 줄 정보는 현재 프로세스가 실행 중인 CPU 번호입니다.
7    (__u32) cpu = 5 = 0x5,

마지막으로 9~20번째 줄 정보를 보겠습니다.
9    (struct cpu_context_save) cpu_context = (
10      (__u32) r4 = 4056868736 = 0xF1CEE780,
11      (__u32) r5 = 3539676032 = 0xD2FB2B80,
12      (__u32) r6 = 3664491200 = 0xDA6BB2C0,
13      (__u32) r7 = 3688475136 = 0xDBD9AA00,
14      (__u32) r8 = 3248547128 = 0xC1A0E538,
15      (__u32) r9 = 3496897024 = 0xD06E6A00,
16      (__u32) sl = 3539677168 = 0xD2FB2FF0,
17     (__u32) fp = 3461324268 = 0xCE4F9DEC,
18      (__u32) sp = 3461324200 = 0xCE4F9DA8,
19      (__u32) pc = 3237955160 = 0xC0FF4658,
20      (__u32 [2]) extra = ([0] = 0 = 0x0, [1] = 0 = 0x0)),

cpu_context 멤버가 컨택스트 스위칭 전 실행했던 레지스터 세트 정보를 저장합니다.

이번 절에는 struct thread_info 구조체를 소개했습니다. 다음 절에서는 struct thread_info 멤버가 어떤 코드에서 변경되며 커널 입장에서 세부 동작을 살펴보겠습니다.

4.8.3 실행 중인 CPU 번호 확인하기
struct thread_info 필드 중 cpu는 현재 프로세스가 실행 중인 cpu 번호를 의미합니다.

현재 코드가 어떤 CPU에서 구동 중인지 알려면 어떤 함수를 써야 할까요? 리눅스 커널에서는 smp_processor_id() 이라는 매크로를 제공합니다.

이 매크로의 선언부를 보겠습니다.
1 # define smp_processor_id() raw_smp_processor_id()

2 #define raw_smp_processor_id() (current_thread_info()->cpu)

1~2번째 줄 코드를 보면 smp_processor_id() 함수는 raw_smp_processor_id() 함수로 치환되는데, raw_smp_processor_id() 함수를 보면 current_thread_info()->cpu 멤버를 반환합니다.

current_thread_info() 란 함수는 실행 중인 함수 스택 주소를 읽어서 프로세스 스택 최상단 주소를 얻어옵니다.

커널 곳곳에 smp_process_id() 란 함수를 써서 실행 코드가 어떤 CPU에서 실행 중인지 점검합니다. 예제 코드를 보겠습니다.
1void resched_curr(struct rq *rq)
2 {
3 int cpu;
...
4 cpu = cpu_of(rq);
5
6 if (cpu == smp_processor_id()) {
7 set_tsk_need_resched(curr);
8 set_preempt_need_resched();
9 return;
10 }

4번째 줄 코드를 보면 함수 인자인 런큐 구조체에서 런큐 CPU 번호를 cpu란 지역 변수에 저장합니다. 

6번째 줄 코드를 보면 smp_processor_id() 이란 함수를 호출해서 현재 wake_up_idle_cpu() 함수가 어떤 CPU에서 실행 중인지 확인합니다. 현재 실행 중인 CPU 번호와 런큐 CPU 번호가 같으면 7~8번째 줄 코드를 실행하고 함수 실행을 끝냅니다.

6번째 줄 코드 매크로를 풀어서 실제 동작하는 코드로 바꾸면 주석문 코드와 같습니다.
if (cpu == current_thread_info()->cpu) {

이 동작을 그림으로 표현하면 다음과 같습니다.

 

위 함수 흐름은 라즈베리파이 SDIO 드라이버에서 워크 핸들러인 brcmf_sdio_dataworker() 함수를 실행합니다. 이 후 __wake_up() 함수를 호출해서 프로세스 스케줄링 요청을 수행하는 resched_curr() 함수를 실행합니다.

resched_curr() 함수에서 smp_processor_id() 함수를 호출하면 실행 중인 프로세스 스택 최상단 주소에 접근해서 cpu 란 멤버를 읽어 오는 겁니다.

이번에는 언제 struct thread_info.cpu 멤버에 현재 실행 중인 CPU 번호를 저장하는지 점검합시다.
void set_task_cpu(struct task_struct *p, unsigned int new_cpu)
{
...
__set_task_cpu(p, new_cpu);
}

static inline void __set_task_cpu(struct task_struct *p, unsigned int cpu)
{
...
task_thread_info(p)->cpu = cpu;

위 코드와 같이 set_task_cpu() 함수를 실행하면 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 cpu 멤버에 실행 중인 cpu 인자 정수값을 저장합니다.

다음 그림은 set_task_cpu() 함수가 동작하는 흐름입니다.
 
set_task_cpu() 함수를 호출하면 __set_task_cpu() 함수를 호출해서 프로세스 스택 최상단 주소 접근 후 struct thread_info 구조체 cpu 멤버에 cpu 번호를 저장합니다.

4.8.4 struct thread_info 초기화 코드 분석
이번 소절에서는 struct thread_info를 초기화하는 코드를 분석하겠습니다. 

이전 절에서 프로세스를 처음 생성할 때 copy_process() 함수를 호출한다는 사실을 확인했습니다. 이 copy_process() 함수에서 dup_task_struct() 함수를 호출해서 태스크 디스크립터와 프로세스가 실행할 스택 공간을 새로 만듭니다.

dup_task_struct() 함수 주요 코드는 다음과 같습니다.
1 static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
2 {
3 struct task_struct *tsk;
4 unsigned long *stack;
5 struct vm_struct *stack_vm_area;
6 int err;
...
7 tsk = alloc_task_struct_node(node);
8 if (!tsk)
9 return NULL;
10
11 stack = alloc_thread_stack_node(tsk, node);
12 if (!stack)
13 goto free_tsk;
...
14 tsk->stack = stack;
...
15 setup_thread_stack(tsk, orig);

7번째 줄 코드입니다.
7 tsk = alloc_task_struct_node(node);

alloc_task_struct_node() 함수를 호출해서 태스크 디스크립터를 할당합니다.

11번째 줄 코드를 볼 차례입니다.
11 stack = alloc_thread_stack_node(tsk, node);

alloc_thread_stack_node() 함수를 호출해서 스택 메모리 공간을 할당 받습니다.
프로세스가 생성할 때 스택 공간을 할당 받으며 그 크기는 정해져 있습니다.

ARM32 아키텍처는 프로세스 스택 크기가 0x2000입니다. 

라즈베리파이는 ARM32 아키텍처를 적용했으니 프로세스 스택 크기는 0x2000입니다.
참고로, ARM64비트 아키텍처를 적용한 시스템에서는 프로세스 스택 크기는 0x4000입니다.

14번째 줄 코드를 볼 차례입니다.
14 tsk->stack = stack;

struct task_struct.task 이란 멤버에 할당 받은 스택 포인터를 저장합니다.
이제 할당 받은 스택 최상단 주소를 태스크 디스크립터에 설정하는 겁니다.

마지막으로 15번째 줄 코드를 보겠습니다.
15 setup_thread_stack(tsk, orig);

setup_thread_stack() 함수를 호출해서 태스트 디스크립터 주소를 current_thread_info() 구조체에 저장합니다.

setup_thread_stack() 함수를 보면서 어떤 설정을 하는지 조금 더 짚어 봅시다.
1 static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
2 {
3 *task_thread_info(p) = *task_thread_info(org);
4 task_thread_info(p)->task = p;
5 }

4번째 줄 코드를 보겠습니다. task_thread_info() 함수를 호출해서 프로세스 스택 최상단 주소로 struct thread_info 구조체를 읽어 옵니다.

4.8.5 struct thread_info 디버깅
Trace32로 struct thread_info 구조체를 확인하면 다음과 같습니다.
1   (struct thread_info *) (struct thread_info*)0xAE4F8000 = 0xAE4F8000 -> (
2    (long unsigned int) flags = 0x2,
3    (int) preempt_count = 0x00200003,
4    (mm_segment_t) addr_limit = 0xBF000000,
5    (struct task_struct *) task = 0xA2FB2B80,
6    (__u32) cpu = 0x0,
7    (__u32) cpu_domain = 0x0,

5번째 줄 정보를 보면 task이란 멤버가 0xA2FB2B80 주소를 저장하고 있습니다.
이 주소가 태스크 디스크립터 구조체 위치입니다.

이번에는 태스크 디스크립터 주소인 0xA2FB2B80에 접근해서 스택 주소를 점검합시다.
1    (struct task_struct *) task = 0xA2FB2B80 -> (
2      (long int) state = 0x0,
3      (void *) stack = 0xAE4F8000,
4      (atomic_t) usage = ((int) counter = 0x0),

3 번째 줄 정보를 보면 stack이란 필드에 스택 최상단 주소인 0xAE4F8000 가 저장돼 있습니다.



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 에 저장된 값입니다.


태스크 디스크립터(struct task_struct) 

프로세스 속성 정보를 표현하는 가장 중요한 자료구조는 무엇일까요? 정답은 태스크 디스크립터이며 자료구조는 struct task_struct입니다. 리눅스 커널에서 가장 중요한 자료구조이며 리눅스 디바이스 드라이버와 커널 코드를 보는 분들은 모두 잘 알아야 할 내용입니다.

TCB(Task Control Block)란 용어 들어본 적 있나요? 임베디드 시스템에서 태스크 혹은 프로세스 정보를 표현하는 자료구조입니다. 
리눅스 커널에서 프로세스 정보를 표현하는 자료 구조는 뭘까요? 여기서 임베디드 시스템에서 말하는 태스크와 리눅스 커널의 프로세스는 같은 개념으로 봐야 합니다. 

프로세스마다 자신의 속성을 저장하는 태스크 디스크립터가 있는 것입니다.
정답은 struct task_struct입니다. 이 구조체를 태스크 디스크립터라고 말하며 프로세스의 속성과 상태 정보를 확인할 수 있습니다.

struct task_struct 구조체에 접근해서 프로세스 정보를 출력하는 한 가지 예를 들어보겠습니다. 라즈베리파이에서 다음와 같이 “ps –ely”란 명령어를 입력하면 프로세스 목록을 볼 수 있습니다.
root@raspberrypi:/home/pi/dev_raspberri # ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  5964  7007 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
I     0     4     2  0  60 -20     0     0 worker ?        00:00:00 kworker/0:0H
I     0     6     2  0  60 -20     0     0 rescue ?        00:00:00 mm_percpu_wq
S     0     7     2  0  80   0     0     0 smpboo ?        00:00:00 ksoftirqd/0
I     0     8     2  0  80   0     0     0 rcu_gp ?        00:00:00 rcu_sched
I     0     9     2  0  80   0     0     0 rcu_gp ?        00:00:00 rcu_bh

가장 오른쪽에 systemd, kthreadd 순서로 프로세스 이름을 볼 수 있고 PID는 1, 2, 4번으로 확인할 수 있습니다. 이 프로세스 정보는 태스크 디스크립터 필드에 저장된 값을 읽어서 이를 표현하는 것입니다.

리눅스 시스템에서 구동 중인 프로세스 목록은 어떻게 출력할까요? 이 목록은 프로세스를 관리하는 태스크 디스크립터의 연결 리스트(init_task.tasks에 접근해서 등록된 프로세스를 출력합니다.

처음 리눅스 커널 소스 코드를 분석을 시작할 때 품은 의문이 있습니다. “어떤 코드부터 분석할까?”란 고민을 하는 경우 많습니다.
소스 코드 분석을 시작할 때 태스크 디스크립터 각각 멤버가 리눅스 커널 어느 코드에서 변경되는지 점검하는 것도 좋습니다. 리눅스 커널은 프로세스 중심으로 중요한 데이터를 저장하고 로딩하기 때문입니다.
데브웁스나 리눅스 커널 개발자에게 태스크 디스크립터 구조체인 struct task_struct는 반드시 알아야 할 자료구조입니다.

태스크 디스크립터를 각 항목별로 점검해봅시다. 
처음 태스크 디스크립터 멤버들을 모두 무리해서 외우지 말고 각 멤버들이 무슨 의미인지 코드를 이해하면서 자연히 체득하는 것이 좋습니다.


do_exit() 함수 분석
do_exit() 함수로 커널이 프로세스를 종료 시키는 세부 동작 못지 않게 프로세스가 종료되는 흐름을 파악하는 것이 중요합니다. 그 이유는 무엇일까요? 

유저 어플리케이션 프로세스나 커널 프로세스가 예외 상황에서 의도하지 않게 종료해서 문제가 발생하는 경우가 있습니다. 이런 문제를 만났을 때 커널 어느 함수부터 분석을 해야할 지 결정할 수 있습니다.

이번에는 do_exit() 함수가 어떻게 실행되는지 알아봅시다.

- exit() 시스템 콜 실행
유저 어플리케이션 프로세스가 자신을 종료하려면 리눅스 저수준 함수로 exit() 함수를 호출합니다. 이 때 시스템 콜을 발생시킨 다음 sys_group_exit() 시스템콜 핸들러를 실행합니다.

- 다른 프로세스가 프로세스 종료 시그널을 전달했을 경우
프로세스 종료 시그널을 다른 프로세스가 전달했을 경우에도 do_exit() 함수가 호출될 수 있습니다. 또한 커널 함수로 send_signal() 함수를 호출하면 특정 프로세스 종료 시그널을 전달할 수 있습니다. 프로세스 종료 시그널을 받은 프로세스는 do_exit() 함수를 실행해서 자신을 종료합니다.

이렇게 프로세스를 종료할 때 do_exit() 함수를 호출합니다. 이 함수는 프로세스가 쓰는 리소스를 해제하는 역할을 수행합니다.

먼저 do_exit() 함수에 전달되는 인자와 반환값을 확인합시다.
void __noreturn do_exit(long code);

do_exit() 함수는 프로세스에 대한 리소스를 정리한 후 do_task_dead() 함수를 호출한 후 schedule() 함수를 실행합니다. 따라서 do_exit() 함수를 끝까지 실행하지 않습니다. 그래서 함수 선언부에 __noreturn 이란 매크로로 지정한 것입니다.

do_exit() 함수에서 do_task_dead() 함수를 호출해서 schedule() 함수 실행으로 함수 흐름을 마무리하는 이유는 무엇일까요?

그 동안 프로세스 실행 흐름에 대해 살펴봤듯이 프로세스는 자신의 스택 공간에서 함수를 실행합니다. do_exit() 함수는 프로세스 스택 공간에서 실행을 시작하는 것입니다.

그런데 프로세스 리소스를 해제하는 함수를 호출하면서 태스크 디스크립터 대부분 멤버와 스택 공간까지 해제합니다. do_exit() 함수 실행을 끝낸 다음에 복귀를 못하는 것입니다. 따라서 do_task_dead()/schedule() 함수를 호출해서 do_exit() 함수 실행을 마무리합니다.

이번에는 이 함수에 전달하는 code란 인자를 점검합시다. code라는 인자는 프로세스 종료 코드를 의미합니다.

만약 kill -9 라는 명령어로 프로세스를 종료하면 code 인자로 9가 전달됩니다.

다음은 Trace32로 do_exit() 함수를 호출했을 때 브레이크 포인트를 걸고 본 디버깅 정보입니다.
-000|do_exit(?)
-001|do_group_exit(exit_code = 9)
-002|get_signal(?)
-003|do_notify_resume(regs = 0A4233EC0, thread_flags = 9)
-004|work_pending(asm)

"kill -9 [PID]" 명령어로 프로세스를 종료하니 exit_code 인자에 9가 전달된 것입니다.  

do_exit() 함수는 다음 순서로 프로세스를 종료합니다.
- 이미 프로세스가 종료 중 인데 다시 do_exit() 함수를 호출하는지 점검합니다.
- 프로세스 리소스(파일 디스크립터, 가상 메모리, 시그널) 등등을 해제합니다.
- 부모 프로세스에게 자신이 종료되고 있다고 알립니다.
- 프로세스 실행 상태를 struct task_struct state 멤버에 TASK_DEAD 로 설정합니다.
- do_task_dead() 함수를 호출해서 schedule() 함수를 실행하여 스케줄링 됩니다..

분석할 do_exit() 함수 코드는 다음과 같습니다.
1 void __noreturn do_exit(long code)
2 {
3 struct task_struct *tsk = current;
4 int group_dead;
...
5 if (unlikely(in_interrupt()))
6 panic("Aiee, killing interrupt handler!");
7 if (unlikely(!tsk->pid))
8 panic("Attempted to kill the idle task!");
...
9 if (unlikely(tsk->flags & PF_EXITING)) {
10 pr_alert("Fixing recursive fault but reboot is needed!\n");
11
12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();
15 }
16
17 exit_signals(tsk);  /* sets PF_EXITING */
...
18 exit_mm();
...
19 exit_files(tsk);
20 exit_fs(tsk);
...
21 do_task_dead();
...
22}

먼저 다음 코드를 보겠습니다.
5 if (unlikely(in_interrupt()))
6 panic("Aiee, killing interrupt handler!");
7 if (unlikely(!tsk->pid))
8 panic("Attempted to kill the idle task!");

특이한 상황에 부팅 도중 init 프로세스가 중료하는 경우가 있습니다. 이 때 다음와 같은 커널 로그와 함께 커널 패닉이 발생합니다.
[  837.981513 / 10-11 11:11:00.958][4] Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b
[  837.981513 / 10-11 11:11:00.958][4] 
[  837.981547 / 10-11 11:11:00.958][6] CPU6: stopping
[  837.981571 / 10-11 11:11:00.958][6] CPU: 6 PID: 339 Comm: mmc-cmdqd/0 Tainted: P        W  O   3.18.31-perf #1

init 프로세스는 유저 레벨 프로세스를 생성하고 관장하는 역할을 수행합니다. 그래서 init 프로세스가 불의의 상황으로 종료하면 강제로 커널 패닉을 유발합니다.
보통 리눅스 커널 버전을 업그레이드 후 root 파일 시스템이나 다바이스 노드를 생성 못했을 때 init 프로세스가 종료됩니다.
 
다음 코드를 보겠습니다.
9 if (unlikely(tsk->flags & PF_EXITING)) {
10 pr_alert("Fixing recursive fault but reboot is needed!\n");
11
12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();
15 }

프로세스가 종료 중이면 do_exit() 함수에서 호출되는 exit_signals() 함수에서  태스크 디스크립터 struct task_struct flags를 PF_EXITING로 설정합니다.
void exit_signals(struct task_struct *tsk)
{
...
tsk->flags |= PF_EXITING;

프로세스가 종료 중인데 다시 do_exit() 함수를 실행해서 중복 종류를 시도하는 경우 예외 처리 코드입니다.

프로세스 태스크 디스크립터 멤버인 state에 TASK_UNINTERRUPTIBLE로 상태를 지정하고 schedule() 함수를 호출해서 다른 프로세스가 동작하도록 합니다.

다음 코드를 보겠습니다.
17 exit_signals(tsk);  /* sets PF_EXITING */

프로세스 struct task_strtuct.flags를 PF_EXITING 으로 변경합니다.
종료할 프로세스가 처리할 시그널이 있으면 retarget_shared_pending() 함수를 실행해서 시그널을 대신 처리할 프로세스를 선정합니다.

다음 코드를 보겠습니다.
18 exit_mm();

프로세스의 메모리 디스크립터(struct mm_struct) 리소스를 해제하고 메모리 디스크립터 사용 카운트를 1만큼 감소합니다.

다음 코드를 봅시다.
19 exit_files(tsk);
20 exit_fs(tsk);

프로세스에 쓰고 있는 파일 디스크립터 정보를 해제합니다.

schedule() 함수를 호출하면, finish_task_switch() 함수에서 do_exit() 함수로 종료하는 프로세스의 태스크 디스크립터와 스택 메모리를 해제합니다.
static struct rq *finish_task_switch(struct task_struct *prev)
__releases(rq->lock)
{
...
if (unlikely(prev_state == TASK_DEAD)) {
if (prev->sched_class->task_dead)
prev->sched_class->task_dead(prev);

kprobe_flush_task(prev);

/* Task is done with its stack. */
put_task_stack(prev);

put_task_struct(prev);
}

put_task_stack() 함수를 호출해서 프로세스 스택 메모리 공간을 해제하여 커널 메모리 공간에 반환합니다. 바로 put_task_struct() 함수를 실행해서 프로세스를 표현하는 자료구조인 struct task_struct 가 위치한 메모리를 해제합니다.

참고로, finish_task_switch() 함수에 ftrace 필터를 걸고 이 함수가 호출되는 함수 흐름을 확인하면 다음과 같습니다.
->transport-8537  [002] d..3 714.329199: finish_task_switch+0x28/0x20c <-__schedule+0x2bc/0x84c
     ->transport-8537  [002] d..3   714.329206: <stack trace>
 => schedule+0x3c/0x9c
 => schedule_timeout+0x288/0x378
 => unix_stream_read_generic+0x588/0x754
 => unix_stream_recvmsg+0x70/0x94
 => sock_read_iter+0xd4/0x100
 => new_sync_read+0xd8/0x11c
 => vfs_read+0x118/0x178
 => SyS_read+0x60/0xc0
 => ret_fast_syscall_+0x4/0x28

프로세스 스케줄링 동작 때 호출되는 함수입니다.


이전 시간까지 유저 프로세스와 커널 프로세스가 어떤 흐름으로 생성되는지 살펴봤습니다.
둘 다 _do_fork() 함수를 호출한다는 사실을 알 수 있습니다.

프로세스 생성 시 공통으로 실행하는 _do_fork() 함수 코드를 분석하면서 커널이 어떻게 프로세스를 생성하는지 살펴봅니다.

4.6.1 _do_fork() 함수 분석
_do_fork() 함수을 분석하기 앞서 이 함수 동작을 분류해봅시다.

1단계: 프로세스 생성
copy_process() 함수를 호출해서 프로세스를 생성합니다. 프로세스를 생성하는 세부 동작을 파악하려면 copy_process() 함수를 분석할 필요가 있습니다.

2단계: 생성한 프로세스 실행 요청
copy_process() 함수를 호출해서 프로세스 생성을 마쳤으면 wake_up_new_task() 함수를 호출해서 프로세스를 바로 깨웁니다. 프로세스를 깨운다는 의미는 스케줄러에게 프로세스 실행 요청을 하는 것입니다.

_do_fork() 함수 전체 흐름에 대해 알아봤으니 이번에는 _do_fork() 함수에 전달되는 인자와 반환값을 살펴봅시다.
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)

_do_fork() 함수는 프로세스를 생성한 다음 생성한 프로세스의 PID를 반환합니다.

_do_fork() 함수 인자와 반환값을 알아봤으니 이제 소스 리뷰를 할 차례입니다.
1  long _do_fork(unsigned long clone_flags,
2       unsigned long stack_start,
3       unsigned long stack_size,
4       int __user *parent_tidptr,
5       int __user *child_tidptr,
6       unsigned long tls)
7 {
8 struct task_struct *p;
9 int trace = 0;
10 long nr;
...
11
12 p = copy_process(clone_flags, stack_start, stack_size,
13  child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
14 add_latent_entropy();
...
15
16 if (!IS_ERR(p)) {
17 struct completion vfork;
18 struct pid *pid;
19
20 trace_sched_process_fork(current, p);
21
22 pid = get_task_pid(p, PIDTYPE_PID);
23 nr = pid_vnr(pid);
24
...
25 wake_up_new_task(p);
...
26 }
27 return nr;
28}

12번째 줄 코드를 봅시다.
12 p = copy_process(clone_flags, stack_start, stack_size,
13  child_tidptr, NULL, trace, tls, NUMA_NO_NODE);

copy_process() 란 함수를 호출해서 부모 프로세스의 메모리 및 시스템 정보를 생성하려는 자식 프로세스에게 복사합니다. _do_fork() 함수에 전달된 clone_flags와 stack_stack 그리고 stack_size 인자를 copy_process() 함수에 전달합니다.

다음 16번째 줄 조건문 코드를 보겠습니다.
16 if (!IS_ERR(p)) {

copy_process() 함수를 실행하면 이 함수는 프로세스를 식별하는 태스크 디스크립터를 p이란 포인터 변수를 반환합니다. 이 태스크 디스크립터에 오류가 있는지 점검하는 코드가 16번째 줄 코드입니다. 17~25번째 줄 코드 구간은 태스크 디스크립터에 오류가 없으면 실행합니다.

다음 20~22번째 줄 코드를 봅시다.
20 trace_sched_process_fork(current, p);
21
22 pid = get_task_pid(p, PIDTYPE_PID);
23 nr = pid_vnr(pid);

20번째 줄 코드는 sched_process_fork 이란 ftrace 이벤트를 켰을 때 실행하며 다음과 같은 ftrace 로그를 확인할 수 있습니다.
kthreadd-2 [003] ....  3495.071290: copy_process0x14/0x17d8 <-_do_fork+0xb0/0x3ec
kthreadd-2 [003] ....  3495.071304: <stack trace>
 => kthreadd+0x1dc/0x268
 => ret_from_fork+0x14/0x28
kthreadd-2     [003] ....  3495.071381: sched_process_fork: comm=kthreadd pid=2 child_comm=kthreadd child_pid=17193
kworker/u8:0-17193 [002] d...  3495.071431: sched_switch: prev_comm=kthreadd prev_pid=17193 prev_prio=120 prev_state=D ==> next_comm=swapper/2 next_pid=0

다음은 22~23번째 줄 코드입니다.
22 pid = get_task_pid(p, PIDTYPE_PID);
23 nr = pid_vnr(pid);

pid를 계산해서 nr이란 지역 변수에 저장합니다.

_do_fork() 함수는 copy_process() 함수를 호출해서 프로세스를 생성하고 프로세스 PID를 커널로부터 할당 받아 반환합니다. 프로세스 생성 핵심 함수는 copy_process() 인데 다음에 분석이 이어집니다.

4.6.2 copy_process() 함수 분석
프로세스를 생성하는 핵심 동작은 copy_process() 함수에서 수행합니다. 대부분 부모 프로세스에 있는 리소스를 복사하는 흐름입니다.

분석할 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 int retval;
12 struct task_struct *p;
...
13 retval = -ENOMEM;
14 p = dup_task_struct(current, node);
15 if (!p)
16 goto fork_out;
...
17 /* Perform scheduler related setup. Assign this task to a CPU. */
18 retval = sched_fork(clone_flags, p);
19 if (retval)
20 goto bad_fork_cleanup_policy;
21
...
22 retval = copy_files(clone_flags, p);
23 if (retval)
24 goto bad_fork_cleanup_semundo;
25 retval = copy_fs(clone_flags, p);
26 if (retval)
27 goto bad_fork_cleanup_files;
28 retval = copy_sighand(clone_flags, p);
29 if (retval)
20 goto bad_fork_cleanup_fs;

먼저 14번째 줄 코드를 봅시다.
14 p = dup_task_struct(current, node);
15 if (!p)
16 goto fork_out;

생성한 프로세스의 태스크 디스크립터인 struct task_struct 구조체와 커널 프로세스 스택 공간을 할당합니다. 

18번째 줄 코드입니다.
18 retval = sched_fork(clone_flags, p);
19 if (retval)
20 goto bad_fork_cleanup_policy;

태스크 디스크립터 struct task_struct 구조체에서 스케줄링 관련 초기화를 진행합니다.

다음 22~24번째 줄 코드를 분석하겠습니다.
22 retval = copy_files(clone_flags, p);
23 if (retval)
24 goto bad_fork_cleanup_semundo;

프로세스의 파일 디스크립터 관련 내용(파일 디스크립터, 파일 디스크립터 테이블)을 초기화하는 동작입니다. 부모 struct file_struct 구조체 내용을 자식 프로세스에게 복사합니다.

만약 프로세스 생성 플래그 중 CLONE_FILES 로 프로세스를 생성했을 경우 참조 카운트만 증가합니다.

다음 25번째 줄 코드를 확인하겠습니다.
25 retval = copy_fs(clone_flags, p);
26 if (retval)
27 goto bad_fork_cleanup_files;

프로세스의 파일 디스크립터 관련 내용(파일 디스크립터, 파일 디스크립터 테이블)을 초기화하는 동작입니다. 부모 struct file_struct 구조체 내용을 자식 프로세스에게 복사합니다.

다음 분석할 코드는 29번째 줄입니다.
28 retval = copy_sighand(clone_flags, p);
29 if (retval)
20 goto bad_fork_cleanup_fs;

프로세스가 등록한 시그널 핸들러 정보인 struct sighand_struct 구조체를 생성해서 복사합니다.

이번 절 까지 프로세스를 생성하는 함수를 살펴 봤습니다. 다음 절에서는 프로세스를 종료할 때 실행하는 do_exit() 함수를 분석하겠습니다.


4.5 커널 스레드
이전절까지 유저 영역에서 실행한 프로세스가 어떻게 실행됐는지 점검했습니다.
이번에는 커널 공간에서만 실행하는 커널 프로세스가 어떻게 생성하는지 알아봅시다.

4.5.1 커널 스레드란
커널 프로세스는 커널 공간에서만 실행하는 프로세스를 의미하며 대부분 커널 스레드 형태로 구동합니다. 커널 스레드는 리눅스 시스템 프로그래밍에서 데몬과 비슷한 일을 합니다. 데몬과 커널 스레드는 백그라운드 작업으로 실행하면서 시스템 메모리나 전원을 제어하는 역할을 수행합니다. 

커널 스레드는 커널 내부에서 백그라운드로 구동하면서 커널 시스템에 도움을 주는 역할을 수행합니다. 데몬과 커널 스레드의 차이점은 보통 유저 영역와 시스템 콜을 받지 않고 동작한다는 점입니다. 리눅스 커널 개발자나 드라이버 개발자가 구현하는 경우가 많습니다.

정리하면 커널 스레드는 다음과 같은 3가지 특징이 있습니다.
1. 커널 스레드는 커널 공간에서만 실행하며 유저 공간과 상호작용을 하지 않습니다.
2. 커널 스레드는 실행, 휴면 등 모든 동작을 커널에서 직접 제어 관리합니다.
3. 대부분 커널 스레드는 시스템이 부팅할 때 생성되고 시스템이 종료할 때까지 백그라운드로 실행합니다.

4.5.2 커널 스레드 종류
리눅스 커널에서 구동중인 대표적인 커널 스레드를 알아보기 위해 라즈베리파이에서 다음 명령어를 입력합시다.
root@raspberrypi:/home/pi# ps -ejH
1 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
2     0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
3     2     4     0     0 ?           -1 I<       0   0:00  \_ [kworker/0:0H]
4     2     7     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/0]
5     2    10     0     0 ?           -1 S        0   0:00  \_ [migration/0]
6     2    11     0     0 ?           -1 S        0   0:00  \_ [cpuhp/0]
7     2    12     0     0 ?           -1 S        0   0:00  \_ [cpuhp/1]
8     2    13     0     0 ?           -1 S        0   0:00  \_ [migration/1]
9     2    14     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/1]
..
 10   2    66     0     0 ?           -1 S        0   0:00  \_ [irq/92-mmc1]

“ps –ejH” 명령어를 입력하면 출력하는 정보가 커널 스레드 목록입니다. 각 정보에서 보이는 커널 스레드를 소개하겠습니다.

kthreadd 프로세스
    0     2     0     0 ?           -1 S        0   0:00 [kthreadd]

모든 커널 스레드의 부모 프로세스입니다.
스레드 핸들 함수는 kthreadd() 이며 커널 스레드를 생성하는 역할을 주로 수행합니다.

워커 스레드
    2     4     0     0 ?           -1 I<       0   0:00  \_ [kworker/0:0H]

워크큐에서 요청된 워크를 실행하는 프로세스입니다.
스레드 핸들 함수는 worker_thread() 이며 process_one_work() 함수를 호출해서 워크를 실행하는 역할을 수행합니다.

ksoftirqd 프로세스
    2     7     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/0]

ksoftirqd 스레드는 smp_boot 스레드이며 프로세스 이름 가장 오른쪽에 실행 중인 CPU 번호를 볼 수 있습니다.

ksoftirqd 스레드 핸들 함수는 run_ksoftirqd() 인데 주로 Soft IRQ 서비스 요청을 _do_softirq() 함수에서 처리하지 못했을 때 실행합니다.

irq/92-mmc1 스레드
    2    66     0     0 ?           -1 S        0   0:00  \_ [irq/92-mmc1]

IRQ 스레드라고 하며 인터럽트 후반부 처리용 프로세스입니다. 대표적인 커널 스레드를 소개했으니 다음 절에서 커널 스레드를 어떻게 생성하는지 알아봅시다.

4.5.3 커널 스레드는 어떻게 생성하나?
이번에는 커널 스레드 생성 요청과 커널 스레드를 생성하는 코드 흐름을 살펴봅시다. 커널 스레드를 생성하려면 다음과 같이 kthread_create() 함수를 호출해야 합니다.
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
3
4 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
5    void *data, int node,
6    const char namefmt[],
7    ...)

먼저 이 함수에 전달하는 인자부터 살펴봅시다.

threadfn
스레드 핸들 함수입니다. 커널 스레드 동작을 살펴보기 위해서 스레드 핸들 함수를 분석해야 합니다.

data
스레드 핸들 함수로 전달하는 매개 변수입니다. 주로 주소를 전달하며 스레드를 식별하는 구조체 주소를 전달합니다.

namefmt
커널 스레드 이름을 전달하는 역할을 수행합니다.

위와 같이 커널 스레드 생성 시 전달하는 인자에 대한 이해를 돕기 위해 다음 코드를 같이 봅시다.
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);
...
8 static int vhost_worker(void *data)
9 {
10 struct vhost_dev *dev = data;
11 struct vhost_work *work, *work_next;
12 struct llist_node *node;
13
14 for (;;) {
15 set_current_state(TASK_INTERRUPTIBLE);
16
...
17 llist_for_each_entry_safe(work, work_next, node, node) {
18 clear_bit(VHOST_WORK_QUEUED, &work->flags);
19 __set_current_state(TASK_RUNNING);
20 work->fn(work);
21 if (need_resched())
22 schedule();
23 }
24 }

7번 째 줄 코드를 보면 kthread_create() 함수 첫 번째 인자로 vhost_worker 스레드 핸들 함수이름을 지정합니다.
7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

2번째 인자로 dev 변수를 지정하는데 1번째 줄 코드를 보면 dev가 어떤 구조체 인지 알 수 있습니다. struct vhost_dev 란 구조체 주소를 2번째 인자로 전달하는 겁니다.

이런 매개 변수와 같은 역할을 보통 디스크립터라고도 부릅니다. 3번째 인자로 "vhost-%d"를 전달하는데 이는 커널 스레드 이름을 지정하는 겁니다.

이번에는 스레드 핸들 함수로 전달되는 매개 변수를 점검합니다.

8번째 줄 코드를 보면 vhost_worker() 함수로 void 타입 포인터인 data를 전달합니다.
8 static int vhost_worker(void *data)
9 {
10 struct vhost_dev *dev = data;

커널 스레드를 생성할 때 두 번째 인자로 struct vhost_dev 구조체인 dev를 지정했습니다.

vhost_worker() 스레드 핸들 함수 인자로 매개 변수 인자를 전달하며 10번째 줄 코드와 같이 void 타입 data 포인터를 struct vhost_dev 구조체로 형 변환(캐스팅)합니다.

리눅스 커널에서는 이런 방식으로 스레드를 관리하는 유일한 구조체를 매개 변수로 전달합니다.

커널 스레드 생성은 2단계로 나눠서 분류할 수 있습니다.
1. 커널 스레드를 kthreadadd란 프로세스에 요청
2. kthreadadd 프로세스는 요청된 프로세스를 생성

먼저 커널 스레드 생성 요청 코드를 봅시다.
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
3
4 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
5    void *data, int node,
6    const char namefmt[],
7    ...)
8 {
9 struct task_struct *task;
10 va_list args;
11
12 va_start(args, namefmt);
13 task = __kthread_create_on_node(threadfn, data, node, namefmt, args);
14 va_end(args);
15
16 return task;
17}

kthread_create() 함수에 커널 스레드를 식별하는 인자와 함께 호출하면 kthread_create_on_node() 함수를 호출합니다. kthread_create_on_node() 함수는 가변 인자를 아규먼트로 받고 __kthread_create_on_node() 함수를 호출합니다.

커널 스레드 생성 요청은 __kthread_create_on_node() 함수에서 이루어집니다. 코드를 봅시다.
1 struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
2     void *data, int node,
3     const char namefmt[],
4     va_list args)
5 {
6 DECLARE_COMPLETION_ONSTACK(done);
7 struct task_struct *task;
8 struct kthread_create_info *create = kmalloc(sizeof(*create),
9      GFP_KERNEL);
10
11 if (!create)
12 return ERR_PTR(-ENOMEM);
13 create->threadfn = threadfn;
14 create->data = data;
15 create->node = node;
16 create->done = &done;
17
18 spin_lock(&kthread_create_lock);
19 list_add_tail(&create->list, &kthread_create_list);
20 spin_unlock(&kthread_create_lock);
21
22 wake_up_process(kthreadd_task);

먼저 8~9번째 줄 코드를 봅시다.
8 struct kthread_create_info *create = kmalloc(sizeof(*create),
9      GFP_KERNEL);

struct kthread_create_info 구조체 메모리를 struct kthread_create_info 크기만큼 할당 받습니다.

다음 13~15번째 줄 코드를 봅시다.
13 create->threadfn = threadfn;
14 create->data = data;
15 create->node = node;

커널 스레드 핸들 함수와 매개 변수 및 노드를 struct kthread_create_info 멤버에 저장합니다.

다음 19번째 줄 코드를 봅시다.
19 list_add_tail(&create->list, &kthread_create_list);

커널 스레드 생성 요청을 관리하는 kthread_create_list이란 링크드 리스트에 &create->list를 추가합니다. kthreadadd란 프로세스는 커널 스레드 생성으로 깨어나면 kthread_create_list 링크드 리스트가 비어 있는지 확인하고 커널 스레드를 생성합니다.

__kthread_create_on_node() 함수 핵심 코드인 22번째 줄입니다.
22 wake_up_process(kthreadd_task);
 
kthreadd 프로세스의 태스크 디스크립터인 kthreadd_task를 인자로 wake_up_process() 함수를 호출해서 kthreadd 프로세스를 깨웁니다.

kthreadd 프로세스의 스레드 핸들러인 kthreadd() 함수 코드를 분석하겠습니다.
1 int kthreadd(void *unused)
2 {
3 struct task_struct *tsk = current;
4
5 /* Setup a clean context for our children to inherit. */
6 set_task_comm(tsk, "kthreadd");
7 ignore_signals(tsk);
8 set_cpus_allowed_ptr(tsk, cpu_all_mask);
9 set_mems_allowed(node_states[N_MEMORY]);
10
11 current->flags |= PF_NOFREEZE;
12 cgroup_init_kthreadd();
13
14 for (;;) {
15 set_current_state(TASK_INTERRUPTIBLE);
16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);
19
20 spin_lock(&kthread_create_lock);
21 while (!list_empty(&kthread_create_list)) {
22 struct kthread_create_info *create;
23
24 create = list_entry(kthread_create_list.next,
25     struct kthread_create_info, list);
26 list_del_init(&create->list);
27 spin_unlock(&kthread_create_lock);
28
29 create_kthread(create);
30
31 spin_lock(&kthread_create_lock);
32 }
33 spin_unlock(&kthread_create_lock);
34 }

wake_up_process(kthreadd_task); 함수를 호출해서 kthreadd 프로세스를 깨우면 실행하는 코드입니다.
16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);

커널 스레드 생성 요청이 없으면 kthread_create_list 이란 링크드 리스트가 비게 되고 휴면에 진입하다가 커널 스레드 생성 요청이 오면 18번째 줄 코드를 실행합니다.

21~32번째 줄 코드를 보기 전에 while 문 조건인 21번째 줄 코드를 봅시다.
kthread_create_list 이란 링크드 리스트가 비어있지 않으면 21~32번째 줄 코드를 실행해서 커널 스레드를 생성합니다.
21 while (!list_empty(&kthread_create_list)) {
22 struct kthread_create_info *create;
23
24 create = list_entry(kthread_create_list.next,
25     struct kthread_create_info, list);
26 list_del_init(&create->list);
27 spin_unlock(&kthread_create_lock);
28
29 create_kthread(create);
30
31 spin_lock(&kthread_create_lock);
32 }

24~25번째 줄 코드를 봅시다.
kthread_create_list.next 멤버를 통해 struct kthread_create_info 구조체 주소를 읽습니다.

29번째 줄 코드를 분석합시다.
29 create_kthread(create);

create_kthread() 함수를 호출해서 커널 스레드를 생성합니다.

이제 create_kthread() 함수를 봅시다.
1 static void create_kthread(struct kthread_create_info *create)
2 {
3 int pid;
4
5 #ifdef CONFIG_NUMA
6 current->pref_node_fork = create->node;
7 #endif
8 /* We want our own signal handler (we take no signals by default). */
9 pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);

9번째 줄 코드와 같이 kernel_thread() 함수를 호출하는데 CLONE_FS, CLONE_FILES, SIGCHLD 매크로를 아규먼트로 설정합니다.

kernel_thread() 함수를 분석하겠습니다.
1 pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
2 {
3 return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
4 (unsigned long)arg, NULL, NULL, 0);
5 }


3번째 줄 코드와 같이 _do_fork() 함수를 호출합니다.
_do_fork() 함수는 프로세스를 생성할 때 실행하는 함수로 알고 있습니다.

이로 커널 스레드도 프로세스의 한 종류인 것을 알 수 있습니다.

Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


#Reference 시스템 콜

Reference(워크큐)
워크큐(Workqueue) Overview


부모 자식 프로세스 생성 실습 및 ftrace 로그 분석 


이번에 리눅스 시스템 프로그래밍으로 프로세스를 생성해 봅시다. 소스 코드는 다음과 같으니 같이 입력해 봅시다.
1  #include <stdio.h>
2  #include <unistd.h>
3  #include <sys/types.h>
4
5  #define PROC_TIMES  7
6  #define SLEEP_DURATION  2
7  #define FORK_MAX_TIMES  3
8
9  void raspbian_proc_process(void);
10
11 void raspbian_proc_process(void) 
12 {
13 int proc_times = 0;
14
15 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
16 printf("raspbian tracing ppid:%d pid:%d \n", getppid(), getpid());
17 sleep(SLEEP_DURATION);
18 }
19
20  exit(EXIT_SUCCESS);
21 }

22 int main() 
23 {
24 pid_t pid;
25 int fork_times = 0;
26
27 printf("About to fork process \n");
28
29 pid = fork();
30
31 if ( pid == 0 )  {
32 printf("start execution of child process\n");
33 raspbian_proc_process();
34 }
35
36 else if ( pid > 0 ) {
37 printf("start execution of parent process\n");
38 raspbian_proc_process();
39 }
40
41 return 0;
42 }

이전 절에 작성한 코드에서 다음 동작이 추가됐습니다. 29번째 줄 코드부터 분석을 시작합니다.
29 pid = fork();
30
31 if ( pid == 0 )  {
32 printf("start execution of child process\n");
33 raspbian_proc_process();
34 }
35
36 else if ( pid > 0 ) {
37 printf("start execution of parent process\n");
38 raspbian_proc_process();
39 }

fork() 함수를 호출해서 자식 프로세스를 생성합니다. fork() 함수는 실행 후 pid란 지역변수로 반환값을 저장합니다. pid가 0이면 자식 프로세스 혹은 pid가 0보다 크면 부모 프로세스가 생성된 겁니다. 만약 fork() 함수 실행 도중 오류가 발생하면 자식 프로세스가 생성되지 않고 -1를 반환합니다.

위 코드를 입력한 다음 raspbian_test_fork.c 란 이름으로 저장합시다.
raspbian_fork: raspbian_test_fork.c
gcc -o raspbian_fork raspbian_test_fork.c

이전 시간에 작성한 ftrace 로그 설정 셸 스크립트인 ./clone_process_debug.sh를 실행합니다.

다음 명령어로 raspbian_fork 프로세스를 실행합시다.
root@raspberrypi:/home/pi # ./raspbian_fork 
About to fork process 
start execution of parent process
raspbian tracing ppid:895 pid:17120 
start execution of child process
raspbian tracing ppid:17120 pid:17121 
raspbian tracing ppid:895 pid:17120 
raspbian tracing ppid:17120 pid:17121

화면에 출력되는 메시지는 raspbian_test_fork.c 파일에서 작성한 코드가 실행하면서 출력합니다. pid가 17120/17121인 프로세스가 실행하는 것으로 보입니다.

다음 명령어로 실행 중인 프로세스를 확인합시다.
root@raspberrypi:/home/pi # ps -ely | grep raspbian_fork
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000 17120   895  0  80   0   320   453 hrtime pts/0    00:00:00 raspbian_fork
S  1000 17121 17120  0  80   0    96   453 hrtime pts/0    00:00:00 raspbian_fork

위 메시지로 보아 pid가 17121인 raspbian_fork 프로세스의 부모 프로세스의 pid는 17120임을 알 수 있습니다. 마찬가지로 pid가 17120인 raspbian_fork 프로세스 부모 프로세스 pid는 895입니다. 

이렇게 raspbian_fork 프로세스를 실행하면 14초 정도 구동하다가 프로세스가 종료합니다. 그 이유는 14초 정도 raspbian_proc_process() 함수 실행 후 다음 20번째 줄 코드와 같이 exit() 함수를 실행해서 프로세스를 종료하기 때문입니다.
11 void raspbian_proc_process(void) 
12 {
13 int proc_times = 0;
14
15 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
16 printf("raspbian tracing ppid:%d pid:%d \n", getppid(), getpid());
17 sleep(SLEEP_DURATION);
18 }
19
20  exit(EXIT_SUCCESS);
21 }

이 후 이전에 썼던 방식으로 다음 스크립트를 실행해서 ftrace 로그를 라즈베리파이에서 추출합시다.
root@raspberrypi:/home/pi#./get_ftrace.sh

프로세스 생성과 종료 과정 메시지가 포함된 ftrace 전체 로그는 다음과 같습니다. 각 단계 별로 ftrace 로그를 분석하겠습니다.
1 raspbian_fork-17120 [003] ....1318.513909: copy_process+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
2  raspbian_fork-17120 [003] ....1318.513921: <stack trace>
3 => _do_fork+0xb0
4 => SyS_clone+0x30
5 => ret_fast_syscall+0x0
...
6 raspbian_fork-17120 [003] d...1318.514446: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
7 raspbian_fork-17121 [002] d...1318.514869: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
8 raspbian_fork-17120 [003] d...1320.514615: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
9 raspbian_fork-17121 [002] d...1320.515011: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
...
10 raspbian_fork-17120 [003] d...1322.514829: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
11 raspbian_fork-17121 [002] d...1322.515192: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
...
12 raspbian_fork-17121 [002] ....  1343.333582: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
13 raspbian_fork-17120 [003] ....  1343.333583: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
14 raspbian_fork-17121 [002] ....  1343.333621: <stack trace>
15 => SyS_exit_group+0x24/SyS_exit_group+0x24
16 => ret_fast_syscall+0x0/0x28
17   raspbian_fork-17120 [003] ....  1343.333621: <stack trace>
18 => SyS_exit_group+0x24/SyS_exit_group+0x24
19 => ret_fast_syscall+0x0/0x28
20 lxterminal-876   [000] d...  1343.333844: sched_switch: prev_comm=lxterminal prev_pid=876 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
21 Xorg-454   [000] dn..  1343.333946: sched_wakeup: comm=lxterminal pid=876 prio=120 target_cpu=000
22 Xorg-454   [000] d...  1343.333957: sched_switch: prev_comm=Xorg prev_pid=454 prev_prio=120 prev_state=R ==> next_comm=lxterminal next_pid=876 next_prio=120
23 raspbian_fork-17120 [003] ....  1343.333959: sched_process_exit: comm=raspbian_fork pid=17120 prio=120
24 raspbian_fork-17121 [002] ....  1343.333980: sched_process_exit: comm=raspbian_fork pid=17121 prio=120
25 raspbian_fork-17120 [003] d...  1343.334028: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0
26 lxterminal-876   [000] dnh.  1343.334048: sched_wakeup: comm=bash pid=895 prio=120 target_cpu=000
27 raspbian_fork-17121 [002] d...  1343.334049: signal_generate: sig=17 errno=0 code=2 comm=systemd pid=1 grp=1 res=0

1번째 로그부터 분석을 시작합니다.
1 raspbian_fork-17120 [003] ....1318.513909: copy_process+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
2 raspbian_fork-17120 [003] ....1318.513921: <stack trace>
3 => _do_fork+0xb0
4 => SyS_clone+0x30
5 => ret_fast_syscall+0x0

pid가 17120인 raspbian_fork 프로세스가 pid가 17121인 raspbian_fork 프로세스를 생성하는 동작입니다.
raspbian_test_fork.c 파일에서 유저 공간에서 프로세스를 생성하는 코드는 29번째 줄과 같았습니다. 
22 int main() 
23 {
24 pid_t pid;
25 int fork_times = 0;
26
27 printf("About to fork process \n");
28
29 pid = fork();

유저 공간에서 fork() 함수를 호출하면 시스템 콜이 실행해서 커널 공간에서 SyS_clone() 이란 함수를 호출하는 겁니다.

다음 6~11번째 줄 로그를 분석해봅시다.
6 raspbian_fork-17120 [003] d...1318.514446: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
7 raspbian_fork-17121 [002] d...1318.514869: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
8 raspbian_fork-17120 [003] d...1320.514615: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
9 raspbian_fork-17121 [002] d...1320.515011: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
...
10 raspbian_fork-17120 [003] d...1322.514829: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
11 raspbian_fork-17121 [002] d...1322.515192: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120

raspbian_fork-17120와 raspbian_fork-17121 프로세스가 2초 간격으로 스케줄링되어 실행합니다. 
raspbian_test_fork.c 파일에서 다음과 같이 두 개 프로세스가 2초 주기로 휴면되도록 구현한 코드가 실행하는 겁니다.
11 void raspbian_proc_process(void) 
12 {
13 int proc_times = 0;
14
15 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
16 printf("raspbian tracing ppid:%d pid:%d \n", getppid(), getpid());
17 sleep(SLEEP_DURATION);
18 }
19
20  exit(EXIT_SUCCESS);
21 }

다음은 유저 레벨 프로세스에서 위에서 보이는 20번째 줄 exit(EXIT_SUCCESS); 함수를 실행하면 커널에서 어떤 동작을 하는지 살펴볼 차례입니다. 

exit() 리눅스 저수준 표준 함수 중 하나이며 리눅스 시스템 프로그램에서 많이 쓰는 함수입니다. 
다음 리눅스 메뉴얼 페이지에서 exit() 함수에 대한 내용을 읽을 수 있습니다.
EXIT(3)                   Linux Programmer's Manual                  EXIT(3)
NAME         top
       exit - cause normal process termination
SYNOPSIS         top
       #include <stdlib.h>

       void exit(int status);
DESCRIPTION         top
       The exit() function causes normal process termination and the value
       of status & 0377 is returned to the parent (see wait(2)).

명시적으로 프로세스를 종료시키는 동작입니다.
로그 분석으로 돌아가겠습니다.
12 raspbian_fork-17121 [002] ....  1343.333582: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
13 raspbian_fork-17120 [003] ....  1343.333583: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
14 raspbian_fork-17121 [002] ....  1343.333621: <stack trace>
15 => SyS_exit_group+0x24/SyS_exit_group+0x24
16 => ret_fast_syscall+0x0/0x28
17   raspbian_fork-17120 [003] ....  1343.333621: <stack trace>
18 => SyS_exit_group+0x24/SyS_exit_group+0x24
19 => ret_fast_syscall+0x0/0x28

raspbian_fork-17120와 raspbian_fork-17121 프로세스가 종료하는 동작입니다.
이 로그에서 프로세스가 종료하는 흐름은 다음 그림과 같습니다.

리눅스 저수준 exit() 함수를 유저 프로세스에서 실행하니 해당 함수에 대한 시스템 콜 핸들러인 sys_exit_group() 함수에서 do_group_exit() -> do_exit() 순서로 함수를 호출해서 두 프로세스를 종료합니다.

마지막으로 27~31번째 줄 로그를 봅시다.
27 raspbian_fork-17120 [003] ....  1343.333959: sched_process_exit: comm=raspbian_fork pid=17120 prio=120
28 raspbian_fork-17121 [002] ....  1343.333980: sched_process_exit: comm=raspbian_fork pid=17121 prio=120
29 raspbian_fork-17120 [003] d...  1343.334028: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0
30 lxterminal-876   [000] dnh.  1343.334048: sched_wakeup: comm=bash pid=895 prio=120 target_cpu=000
31 raspbian_fork-17121 [002] d...  1343.334049: signal_generate: sig=17 errno=0 code=2 comm=systemd pid=1 grp=1 res=0

sched_process_exit 이란 ftrace 이벤트로 pid 17120, 17121 인 raspbian_fork 프로세스가 종료하고, 각각 부모 프로세스에 시그널을 전달합니다.

raspbian_fork-17120 프로세스는 bash란 부모 프로세스에 시그널을 전달하고, raspbian_fork-17121 프로세스는 부모 프로세스인 raspbian_fork-17120가 종료됐으니 pid가 1인 systemd 프로세스에 시그널을 전달합니다.
29 raspbian_fork-17120 [003] d...  1343.334028: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0
30 lxterminal-876   [000] dnh.  1343.334048: sched_wakeup: comm=bash pid=895 prio=120 target_cpu=000
31 raspbian_fork-17121 [002] d...  1343.334049: signal_generate: sig=17 errno=0 code=2 comm=systemd pid=1 grp=1 res=0

유저 공간에서 fork() 이란 함수를 호출하면 리눅스 커널에서 어떤 과정으로 프로세스를 생성하는지 확인했습니다. 이번에 다룬 내용을 정리합시다.
1. 유저 공간에서 fork() 함수를 호출하면 시스템 콜이 실행되어 커널 공간에 있는 SyS_clone()이란 함수를 호출하고 _do_fork() 이란 프로세스를 생성하는 함수를 호출합니다.

2. 유저 레벨 프로세스는 스스로 프로세스를 생성하지 못합니다. 시스템 라이브러리 도움을 받아서 커널 공간에 프로세스 생성 요청을 합니다.

3. 프로세스를 종료할 때 do_exit() 함수를 호출합니다.

4. 프로세스가 종료할 때 부모 프로세스에게 자신이 종료됐다는 사실을 시그널로 알립니다.

유저 공간에서 어떤 함수를 호출하면 리눅스 커널에서 어떤 함수 흐름으로 코드가 실행되는지 전체 그림을 그리면서 파악하는 것이 중요합니다.

Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 



기본 유저 레벨 프로세스 실행 실습 및 ftrace 로그 분석

라즈베리파이에서 X-terminal 프로그램을 실행해서 셸을 엽시다. 

root@raspberrypi:/boot# ps -ely | grep bash
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   541   443  0  80   0  4024  1645 poll_s tty1     00:00:00 bash
S  1000   880   878  0  80   0  4008  1628 wait   pts/0    00:00:00 bash
S     0   977   972  0  80   0  3284  1416 wait   pts/0    00:00:00 bash
S  1000   993   989  0  80   0  3960  1628 poll_s pts/1    00:00:00 bash

grep bash 명령어로 현재 실행 중인 프로세스 중에 bash 프로세스를 출력합니다. 출력 결과 4개 bash 프로세스 목록을 볼 수 있습니다.

이 상태에서 X-terminal 셸을 하나 더 실행하고 다음 명령어를 입력해서 bash 프로세스 목록을 확인합시다.
root@raspberrypi:/boot# ps -ely | grep bash
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   541   443  0  80   0  4024  1645 poll_s tty1     00:00:00 bash
S  1000   880   878  0  80   0  4008  1628 wait   pts/0    00:00:00 bash
S     0   977   972  0  80   0  3284  1416 wait   pts/0    00:00:00 bash
S  1000   993   989  0  80   0  3960  1628 poll_s pts/1    00:00:00 bash
S  1000  1027   878  3  80   0  4036  1628 poll_s pts/2    00:00:00 bash

이전에 출력한 결과와 비교해봅시다. 맨 마지막 줄 로그를 보면 pid가 1027인 bash 프로세스가 보입니다. 셸을 하나 더 열고 “ps –ely” 명령어를 입력하니 bash(pid:1027)과 같이 새로 생성된 프로세스를 볼 수 있습니다. 이렇게 새로운 프로그램을 실행하면 이에 해당하는 프로세스가 생성됩니다.

라즈베리파이 X-Terminal 셸 화면을 마우스로 더블 클릭하는 순간 라즈베리파이 배경 화면을 처리하는 프로세스가 이벤트를 받아서 bash라는 프로세스를 생성합니다. 이 때 리눅스 저수준 함수인 fork()를 호출합니다. 이렇게 유저 레벨 프로세스는 셸이나 다른 프로세스를 통해서 실행을 시작합니다. 유저 레벨 프로세스는 혼자서 실행할 수 없습니다.

이번에는 라즈베리파이에서 소스 에디터로 많이 쓰는 Geany란 프로그램을 열겠습니다.
root@raspberrypi:/boot# ps -ely | grep geany
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   989   671  1  80   0 28276 25827 poll_s ?        00:00:06 geany

Geany 프로그램을 하나 더 열고 다음 명령어를 입력합시다.
root@raspberrypi:/boot# ps -ely | grep geany
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   989   671  1  80   0 28276 25827 poll_s ?        00:00:06 geany
S  1000  1297   671 38  80   0 25204 13533 poll_s ?        00:00:01 geany

PID가 1297인 geany 프로세스가 생성됐습니다.

프로세스를 어렵게 생각할 필요가 없습니다. 셸이나 geany이란 프로그램을 실행하면 메모리에서 실행하는 것이 프로세스입니다. 유저 레벨에서 실행하는 프로세스는 이렇게 유저 동작으로 생성됩니다.

이번에 리눅스 시스템 프로그래밍으로 프로세스를 생성해 봅시다. 소스 코드는 다음과 같으니 같이 입력해 봅시다.
1 #include <stdio.h>
2 #include <unistd.h>
3
4 #define PROC_TIMES 500
5 #define SLEEP_DURATION 3  // second unit
6
7 int main() 
8 {
9 int proc_times = 0;
10
11 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
12 printf("raspbian tracing \n");
13 sleep(SLEEP_DURATION);
14 }
15
16 return 0;
17 }

위와 같은 프로그램을 리눅스 시스템 프로그램이라고 합니다. 리눅스 시스템을 관리하는 sleep()이나 fork() 함수를 직접 호출하기 때문에 응용 프로그램 입장에서 저수준 프로그래밍이라고도 합니다. 위 함수를 리눅스 시스템 저수준 함수(API)라고 부르겠습니다.

위 코드는 다음 코드 이외에 다른 동작을 하지 않습니다.
11 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
12 printf("raspbian tracing \n");
13 sleep(SLEEP_DURATION);
14 }

소스 코드를 잠깐 봅시다.

12번째 줄 코드와 같이 “raspbian tracing”이란 메시지를 셸로 출력하고 13번째 줄 코드와 같이 3초 동안 휴면에 들어갈 뿐입니다.

위 코드를 입력한 다음 raspbian_test.c 란 이름으로 저장합시다. 컴파일을 쉽게 하기 위해 다음과 같이 코드를 작성하고 파일 이름을 Makefile으로 저장합시다.
raspbian_proc: raspbian_test.c
gcc -o raspbian_proc raspbian_test.c

“make” 명령어로 위와 같은 메이크 파일을 실행하면 raspbian_proc이란 실행 파일이 생성됩니다.

메이크 파일은 여러 모듈을 일일이 컴파일 명령어를 입력하기 어려우니 컴파일 설정 속도를 빠르게 하기 위해 고안된 겁니다. 실전 프로젝트에서 메이크 파일은 자주 쓰니 잘 알아둡시다.

make란 명령어를 입력해서 raspbian_test.c 파일을 컴파일하면 raspbian_proc이란 프로그램을 생성할 수 있습니다.

raspbian_proc 이란 프로세스가 어떻게 생성되고 실행되는지 파악하려면 다음과 같이 ftrace 로그를 설정할 필요가 있습니다. 코드를 봅시다.
1  #!/bin/sh
3  echo 0 > /sys/kernel/debug/tracing/tracing_on
4  sleep 1
5  echo "tracing_off"
6
7 echo 0 > /sys/kernel/debug/tracing/events/enable
8 sleep 1
9 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14 
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo SyS_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter
20 echo _do_fork copy_process* >> /sys/kernel/debug/tracing/set_ftrace_filter
21
22 sleep 1
23 echo "set_ftrace_filter enabled"
24
25 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
26 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
27 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable
28 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable
29
30 echo 1 > /sys/kernel/debug/tracing/events/signal/enable
31
32 sleep 1
33 echo "event enabled"
34
35 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
36 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
37 echo "function stack trace enabled"
38
39 echo 1 > /sys/kernel/debug/tracing/tracing_on
40 echo "tracing_on"
조금 더 알아보기
ftrace 에서 시스템 콜 핸들러 함수 심볼(이름)을 Alias 심볼로 씁니다.

예를 들어 sys_write() 함수에 대한 alias 심볼은 SyS_write와 같습니다.
다음 전처리 코드는 fs/read_write.c 파일에 위치한 write 시스템 콜 핸들러 함수 선언부입니다.
[out/fs/.tmp_read_write.i]
1 long sys_write(unsigned int fd, const char * buf, size_t count) __attribute__((alias("SyS_write")));
2
3 [https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
4 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
5 size_t, count)
6 {
7 struct fd f = fdget_pos(fd);
8 ssize_t ret = -EBADF;

1번째 줄 코드를 보면 함수 인자 오른쪽에 다음과 같은 코드를 볼 수 있습니다.
__attribute__((alias("SyS_write")));

GCC 컴파일러가 함수 컴파일시 alias 심볼을 적용한 것인데 sys_write() 함수에 대한 alias 심볼이 SyS_write입니다.

ftrace 로그에서 SyS_xxx 로 어떤 함수를 표현하면 실제 함수 이름은 sys_xxx() 이라고 생각해도 좋습니다.

따라서 ftrace 로그 설정 시 set_ftrace_filter로 SyS_clone 함수로 지정한 겁니다.
19 echo SyS_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter

이렇게 지정하면 ftrace는 실제 리눅스 커널 코드에서 sys_clone 함수를 추적(트레이싱)합니다.

위와 같이 코드를 작성한 후 clone_process_debug.sh 와 같은 이름을 저장한 후 다음과 같이 이 셸 스크립트를 실행합시다.
./clone_process_debug.sh

위 셸 스크립트를 실행하면 5~6초 내 ftrace 로그 설정이 끝납니다. 이후 raspbian_test.c 파일을 컴파일하면 생성되는 raspbian_proc이란 프로그램을 다음 명령어로 실행합시다. 
root@raspberrypi:/home/pi# ./raspbian_proc 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing

raspbian_proc 이란 프로그램을 실행하니 3초 간격으로 “raspbian tracing”이란 메시지를 출력합니다. 소스 코드에서 구현한 대로 실행합니다.

raspbian_proc 프로그램을 실행했으니 이에 해당하는 프로세스가 생성됐을 것이라 예상할 수 있습니다. 이번에는 “ps -ely” 명령어를 입력해서 프로세스 목록을 확인합시다.
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  5956  6991 SyS_ep ?        00:00:02 systemd
3 S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
4
5 S     0   895   890  0  80   0  3420  1448 wait   pts/0    00:00:00 bash
6 S  1000   991   685  0  80   0  7500  7842 poll_s ?        00:00:00 ibus-engine-han
...
7  S     0  1078  1073  0  80   0  3244  1416 wait   pts/2    00:00:00 bash
8  I     0  1079     2  0  80   0     0     0 worker ?        00:00:00 kworker/3:2
9  I     0  2302     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:1
10 S     0 17082   895  0  80   0   344   453 hrtime pts/0    00:00:00 raspbian_proc
11 I     0 17084     2  0  80   0     0     0 worker ?        00:00:00 kworker/u8:1
12 I     0 17085     2  0  80   0     0     0 worker ?        00:00:00 kworker/1:0
13 R     0 17086  1078  0  80   0  1156  1918 -      pts/2    00:00:00 ps

프로세스 목록 10번째 항목을 보면 pid가 17082인 raspbian_proc 프로세스가 보입니다. 리눅스 시스템에서 raspbian_proc 프로세스가 READY 상태이란 의미입니다. 
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
5 S     0   895   890  0  80   0  3420  1448 wait   pts/0    00:00:00 bash
...
10 S     0 17082   895  0  80   0   344   453 hrtime pts/0    00:00:00 raspbian_proc

1번째 줄 로그에서 PPID가 보입니다. 이 정보는 부모 프로세스의 pid를 의미합니다. raspbian_proc 프로세스의 부모 프로세스는 pid가 895입니다. pid가 895인 프로세스를 확인하니 프로세스 목록 5번째 항목과 같이 bash 프로세스입니다. raspbian_proc 프로세스의 부모 프로세스는 bash임을 알 수 있습니다.

raspbian_proc 프로세스의 부모 프로세스는 왜 bash(pid:895) 일까요? raspbian_proc 프로세스를 실행할 때 X-Terminal bash 셸에서 다음 명령어로 실행했기 때문입니다.
root@raspberrypi:/home/pi# ./raspbian_proc 

이렇게 유저 레벨 프로세스는 셸이나 다른 프로세스를 통해서 실행을 시작합니다. 만약 라즈베리파이 바탕 화면에 있는 아이콘을 클릭해서 프로그램을 시작해서 유저 레벨 프로세스를 실행했다고 가정합시다. 이 경우 바탕화면을 제어하는 프로세스가 부모 프로세스가 됩니다.

raspbian_proc 프로세스를 이렇게 15초 동안 실행시킨 다음에 다른 x-terminal 셸을 실행을 실행해서 다음과 같이 raspbian_proc 프로세스를 강제 종료해봅시다.
root@raspberrypi:/home/pi# kill -9  17082

kill 명령어로 pid를 지정하면 강제로 지정한 프로세스를 종료합니다. -9는 강제로 프로세스를 종료시키는 옵션입니다.

다음 명령어를 입력해서 kill이란 명령어가 어떤 의미인지 확인합시다.
root@raspberrypi:/home/pi# info kill

24.1 ‘kill’: Send a signal to processes
=======================================

The ‘kill’ command sends a signal to processes, causing them to
terminate or otherwise act upon receiving the signal in some way.
Alternatively, it lists information about signals.  Synopses:

     kill [-s SIGNAL | --signal SIGNAL | -SIGNAL] PID...
     kill [-l | --list | -t | --table] [SIGNAL]...

   Due to shell aliases and built-in ‘kill’ functions, using an
unadorned ‘kill’ interactively or in a script may get you different
functionality than that described here.  Invoke it via ‘env’ (i.e., ‘env
kill ...’) to avoid interference from the shell.

매뉴얼 내용과 같이 kill 명령어는 프로세스를 종료하는 역할을 수행합니다.

이번에는 다음과 같은 셸 스크립트를 실행해서 ftrace 로그를 추출합시다.
#!/bin/sh

echo 0 > /sys/kernel/debug/tracing/tracing_on
echo "ftrace off"

sleep 3

cp /sys/kernel/debug/tracing/trace . 
mv trace ftrace_log.c

위 코드를 get_ftrace.sh 이름으로 저장해놓고 ftrace 로그를 받을 때 다음 명령어를 실행합니다.
root@raspberrypi:/home/pi#./get_ftrace.sh

그러면 같은 폴더에 ftrace.c이란 파일이 생성됐음을 확인할 수 있습니다.

이제까지 프로세스 생성과 종료 과정을 저장한 ftrace 로그를 추출하기 위해 진행한 과정을 정리하면 다음과 같습니다.
1. 다음 명령어로 프로세스 실행
root@raspberrypi:/home/pi# ./raspbian_proc 

2. ftrace 로그 설정 및 시작
3. ps 명령어로 프로세스 동작 확인

4. raspbian_proc 프로세스 종료
root@raspberrypi:/home/pi# kill -9  17082

5. ftrace 로그 추출

커널 공간에서 raspbian_proc 이란 프로세스가 어떤 코드 흐름으로 생성하고 종료했는지 ftrace 로그로 알아봅시다.

분석할 전체 ftrace 로그는 다음과 같습니다.
1 bash-895 [003] .... 909.253260: SyS_clone+0x14/0x38 <-ret_fast_syscall+0x0/0x28
2 bash-895 [003] .... 909.253295: <stack trace>
3 bash-895 [003] .... 909.253298: _do_fork+0x14/0x3ec <-SyS_clone+0x30/0x38
4 bash-895 [003] .... 909.253310: <stack trace>
5 lxpanel-730 [002] d.h.   909.253310: sched_wakeup: comm=ibus-x11 pid=717 prio=120 target_cpu=002
6 bash-895 [003] .... 909.253312: copy_process.part.5+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
7 bash-895 [003] .... 909.253324: <stack trace>
8 => ret_fast_syscall+0x0/0x28
...
9 bash-895 [003] .... 909.253776: sched_process_fork: comm=bash pid=895 child_comm=bash child_pid=17082
10 <idle>-0 [002] d... 909.253809: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=bash next_pid=17082 next_prio=120
11 bash-895 [003] d... 909.254159: sched_switch: prev_comm=bash prev_pid=895 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=1047 next_prio=120
12 <idle>-0 [000] dnh. 909.254206: sched_wakeup: comm=lxterminal pid=876 prio=120 target_cpu=000
13 <idle>-0 [000] d... 909.254215: sched_switch: prev_comm=swapper/0 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=lxterminal next_pid=876 next_prio=120
14 kworker/u8:0-1047 [003] d... 909.254221: sched_switch: prev_comm=kworker/u8:0 prev_pid=1047 prev_prio=120 prev_state=t ==> next_comm=swapper/3 next_pid=0 next_prio=120
15 raspbian_proc-17082 [002] d.s. 909.254409: sched_wakeup: comm=rcu_sched pid=8 prio=120 target_cpu=002
16 raspbian_proc-17082 [002] d... 909.257817: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
17 <idle>-0 [002] dnh. 912.257874: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002
18 raspbian_proc-17082 [002] d... 912.257957: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
19 <idle>-0  [002] dnh. 915.258028: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002          
20 <idle>-0 [002] d... 915.258044: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=raspbian_proc next_pid=17082 next_prio=120          
21 <idle>-0     [003] dnh.   915.258098: sched_wakeup: comm=kworker/u8:1 pid=17084 prio=120 target_cpu=003   
22 raspbian_proc-17082 [002] d...   915.258110: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
23 raspbian_proc-17082 [002] d...   933.741224: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
24 kworker/u8:2-137 [001] d...   933.741230: sched_switch: prev_comm=kworker/u8:2 prev_pid=137 prev_prio=120 prev_state=t ==> next_comm=swapper/1 next_pid=0 next_prio=120
25 raspbian_proc-17082 [002] .... 933.741230: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
26 raspbian_proc-17082 [002] .... 933.741270: <stack trace>
27 => do_signal+0x300/0x3d4
28 => do_work_pending+0xb4/0xcc
29 => slow_work_pending+0xc/0x20
30 lxpanel-730 [000] d... 933.741295: sched_switch: prev_comm=lxpanel prev_pid=730 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
31 Xorg-454 [000] dn.. 933.741333: sched_wakeup: comm=openbox pid=723 prio=120 target_cpu=000
32 Xorg-454 [000] d... 933.741346: sched_switch: prev_comm=Xorg prev_pid=454 prev_prio=120 prev_state=R ==> next_comm=openbox next_pid=723 next_prio=120
33 raspbian_proc-17082 [002] .... 933.741609: sched_process_exit: comm=raspbian_proc pid=17082 prio=120
34 ibus-x11-717 [001] d... 933.741639: sched_switch: prev_comm=ibus-x11 prev_pid=717 prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120
35 openbox-723 [000] d... 933.741673: sched_switch: prev_comm=openbox prev_pid=723 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
36 raspbian_proc-17082 [002] d... 933.741693: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0

먼저 유저 공간에서 프로세스가 생성하면 다음 그림과 같은 흐름으로 _do_fork() 함수가 실행합니다.
                    
리눅스에서 실행 공간은 메모리 접근과 실행 권한 기준으로 유저 공간과 커널 공간으로 구분할 수 있습니다. 유저 공간은 유저 모드, 커널 공간은 커널 모드에서 각각 에서 실행하는 메모리 공간입니다. 유저 공간과 유저 모드는 거의 비슷한 의미로 씁니다.

리눅스 시스템 저수준 함수로 fork() 함수를 호출하면 유저 공간에서 코드 공간으로 실행 흐름 공간을 이동하는 시스템 콜이 발생하고 커널 모드로 실행 흐름이 변경됩니다. 이후 커널 모드에서 시스템 콜 번호에 해당하는 핸들러 함수가 호출됩니다. 이 함수가 sys_clone()입니다.

위 그림이 실제 ftrace 로그로 어떻게 출력하는지 확인합시다.

먼저 1~8번째 줄 로그를 분석하겠습니다.
1 bash-895 [003] .... 909.253260: SyS_clone+0x14/0x38 <-ret_fast_syscall+0x0/0x28
2 bash-895 [003] .... 909.253295: <stack trace>
3 bash-895 [003] .... 909.253298: _do_fork+0x14/0x3ec <-SyS_clone+0x30/0x38
4 bash-895 [003] .... 909.253310: <stack trace>
5 lxpanel-730 [002] d.h.   909.253310: sched_wakeup: comm=ibus-x11 pid=717 prio=120 target_cpu=002
6 bash-895 [003] .... 909.253312: copy_process.part.5+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
7 bash-895 [003] .... 909.253324: <stack trace>
8 => ret_fast_syscall+0x0/0x28
조금 더 알아보기: ftrace 로그 한줄 한줄을 실행하는 주체는 프로세스입니다. 모든 ftrace 가장 왼쪽에서 프로세스 이름과 pid를 볼 수 있습니다.

위 ftrace 로그에서는 가장 왼쪽에 “bash-895” 메시지가 보이는데 이 정보를 어떻게 해석해야 할까요? 이 정보는 pid가 895인 bash 프로세스를 의미합니다. 5번째 줄 로그 가장 왼쪽에서도 “lxpanel-730”이란 메시지를 볼 수 있습니다. 역시 pid가 730인 lxpanel이란 프로세스를 의미합니다.

로그들이 어지럽게 섞여 있는데 위 로그는 “bash-895”이란 프로세스가 다음 함수 흐름(콜스택)으로 실행 중인 상태입니다.
1 => copy_process.part.5+0x14/0x17d8
2 => _do_fork+0x14/0x3ec
3 => SyS_clone+0x14/0x38
4 => ret_fast_syscall+0x0/0x28

함수 호출 방향은 4에서 1번 로그 방향입니다. 시스템 콜 함수인 SyS_clone() 함수에서 _do_fork() 함수를 호출합니다.

다음 9번째 줄 로그를 봅시다.
9 bash-895 [003] .... 909.253776: sched_process_fork: comm=bash pid=895 child_comm=bash child_pid=17082

sched_process_fork이란 ftrace 이벤트 메시지와 함께 디버깅 정보를 출력합니다. pid가 895인 bash 프로세스가 pid가 17082인 자식 프로세스를 생성한다는 의미입니다. 부모 프로세스가 자식 프로세스를 생성할 때는 프로세스 이름은 그대로 가져갑니다. 이후 프로세스 이름을 자식 프로세스 이름으로 변경합니다.

task_rename 이란 ftrace 이벤트를 키면 다음과 같은 정보를 볼 수 있습니다.
raspbian_proc-17083  [003] .... 909.253836 task_rename: pid=17082 oldcomm=bash newcomm=raspbian_proc oom_score_adj=0

15~23번째 줄 로그는 raspbian_proc(pid:17083) 프로세스가 실행하는 정보입니다. 
15 raspbian_proc-17082 [002] d.s. 909.254409: sched_wakeup: comm=rcu_sched pid=8 prio=120 target_cpu=002
16 raspbian_proc-17082 [002] d... 909.257817: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
17 <idle>-0 [002] dnh. 912.257874: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002
18 raspbian_proc-17082 [002] d... 912.257957: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
19 <idle>-0 [002] dnh. 918.258177: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002
20 <idle>-0 [002] d... 918.258193: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=raspbian_proc next_pid=17082 next_prio=120
21 raspbian_proc-17082 [002] d... 918.258250: sched_wakeup: comm=kworker/u8:1 pid=17084 prio=120 target_cpu=002
22 raspbian_proc-17082 [002] d... 918.258277: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:1 next_pid=17084 next_prio=120

15번째 줄 로그를 보면 909.254409초에 raspbian_proc(pid: 17082) 프로세스가 "rcu_sched" 프로세스를 깨웁니다. 이후 909.257817초에 16번째 줄 로그와 같이 "swapper/2" 프로세스로 스케줄링됩니다.

17번째 로그를 보면, 약 3초 후인 912.257874 초에 idle-0이란 프로세스가 raspbian_proc(pid: 17082) 프로세스를 깨우고, 18번째 로그와 같이 912.257957 초에  "swapper/2" 프로세스로 스케줄링됩니다.

sched_wakeup 이란 ftrace 이벤트는 프로세스를 깨우는 동작을 표현합니다. 정확히 설명을 드리면 스케줄러에게 해당 프로세스 실행 요청을 하는 것입니다.

같은 패턴으로 19~22번째 줄 로그를 보면 915.258028 초에 raspbian_proc(pid: 17082) 프로세스가 깨어나 실행합니다.

raspbian_proc_test.c 파일에서 3초 간격으로 sleep() 함수를 호출하고 raspbian tracing 이란 메시지를 출력하는 코드가 동작하는 것입니다. 구현된 코드와 같이 다음 시간 간격으로 3초 주기로 raspbian_proc 프로세스가 실행합니다.
909.254409 -> 912.257874  -> 915.258028

프로세스가 생성하는 1단계부터 raspbian_proc 프로세스가 3초 간격으로 실행하는 로그를 분석했습니다.

정리하면 유저 모드에서 fork() 이란 리눅스 시스템 저수준 함수를 호출하면 커널 공간에서 fork() 에 대응하는 시스템 콜 핸들러인 sys_clone() 함수를 호출하는 것입니다. 이 후 sys_clone() 함수에서 _do_fork() 함수 호출로 프로세스를 생성(복제)합니다. 

프로세스는 생성 후 바로 실행을 시작하는데 3초 주기로 실행합니다.

이번에는 raspbian_proc 프로세스가 종료하는 2단계 로그를 분석할 차례입니다. 이 과정은 다음 그림에서 확인할 수 있습니다.


우리는 다음 명령어로 raspbian_proc(pid:17082) 프로세스를 강제 종료시켰습니다.
root@raspberrypi:/home/pi# kill -9  17082

유저 레벨 프로세스가 유저 모드에서 명시적으로 exit() 함수를 호출해 종료하지 않고 kill 당한 것입니다.

다음과 같은 형식으로 kill 명령어를 입력하면 유저 레벨 프로세스를 강제 종료시킬 수 있습니다. 여기서 PID는 프로세스 아이디로 정수형 숫자를 의미합니다.
kill -9 [PID]
조금 더 알아보기

위 명령어를 입력하면 리눅스 커널에서는 다음과 같은 동작을 수행합니다.
1. 유저 모드에서 kill() 함수를 호출해서 커널 모드에서 sys_kill() 함수를 실행합니다.
2. “kill -9 [PID]” 명령어에서 PID에 해당하는 프로세스를 스케줄러에게 깨워 달라고 요청합니다.
3. 스케줄러가 PID에 해당하는 프로세스를 컨택스트 스위칭하면 해당 프로세스는 유저 공간으로 복귀 직전 slow_work_pending 이란 레이블을 실행합니다.
4. 위 그림 화살표 방향에서 보이는 함수 흐름으로 do_exit() 함수를 실행해 프로세스를 종료합니다.

kill 명령어는 프로세스를 종료시키는 시그널을 전달하는 명령어입니다.

kill이란 명령어는 유저 레벨에서 시그널로 전달하고 있고 커널에서 kill 명령어 실행에 할 수 있습니다. 조금 어렵게 설명을 드리면 커널에서 kill 명령어에 대응하는 함수 조합으로 kill 명령어 실행과 같은 동작을 할 수 있다는 것입니다.

유저 프로세스가 종료하려면 exit() 이란 함수를 실행하면 됩니다. 이 함수를 호출하면 시스템 콜이 실행되어 커널 공간에서 do_exit() 이란 함수가 실행합니다. 이번 소절에서는 kill 이란 명령어를 입력해서 프로세스를 강제 종료했습니다. exit() 저수준 함수를 호출해서 프로세스를 종료할 때와 실행 흐름이 다릅니다. 

다음은 raspbian_proc-17082 프로세스가 종료할 때 로그입니다.
25 raspbian_proc-17082 [002] .... 933.741230: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
26 raspbian_proc-17082 [002] .... 933.741270: <stack trace>
27 => do_signal+0x300/0x3d4
28 => do_work_pending+0xb4/0xcc
29 => slow_work_pending+0xc/0x20
30 lxpanel-730 [000] d... 933.741295: sched_switch: prev_comm=lxpanel prev_pid=730 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
31 Xorg-454 [000] dn.. 933.741333: sched_wakeup: comm=openbox pid=723 prio=120 target_cpu=000
32 Xorg-454 [000] d... 933.741346: sched_switch: prev_comm=Xorg prev_pid=454 prev_prio=120 prev_state=R ==> next_comm=openbox next_pid=723 next_prio=120
33 raspbian_proc-17082 [002] .... 933.741609: sched_process_exit: comm=raspbian_proc pid=17082 prio=120
34 ibus-x11-717 [001] d... 933.741639: sched_switch: prev_comm=ibus-x11 prev_pid=717 prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120
35 openbox-723 [000] d... 933.741673: sched_switch: prev_comm=openbox prev_pid=723 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
36 raspbian_proc-17082 [002] d... 933.741693: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0

위 로그는 다음 명령어로 강제로 raspbian_proc(pid: 17082) 프로세스를 종료했을 때 출력합니다.
root@raspberrypi:/home/pi# kill -9  17082

25~29번째 줄 로그를 보면 raspbian_proc(pid: 17082)가 어떤 콜스택으로 종료하는지 알 수 있습니다.
25 raspbian_proc-17082 [002] .... 933.741230: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
26 raspbian_proc-17082 [002] .... 933.741270: <stack trace>
27 => do_signal+0x300/0x3d4
28 => do_work_pending+0xb4/0xcc
29 => slow_work_pending+0xc/0x20

raspbian_proc(pid:17082) 프로세스가 do_signal() 함수에서 do_exit() 함수를 호출해서 raspbian_proc(pid: 17082) 프로세스를 종료하는 동작입니다.

이번에 36번째 줄 로그를 보겠습니다.
36 raspbian_proc-17082 [002] d... 933.741693: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0

raspbian_proc(pid: 17082)가 자신이 종료한다는 정보를 시그널로 전달합니다. 
프로세스는 종료 과정에서 자신의 부모 프로세스에게 자신이 종료하고 있다는 시그널을 전달합니다.

프로세스가 생성할 때 SyS_clone() 이란 커널 함수가 호출되고 종료될 때 do_exit() 커널 함수가 호출된다는 점을 확인했습니다.

다음에 다른 리눅스 시스템 프로그램을 작성해서 유저 공간에서 생성된 프로세스가 어떤 과정으로 생성되고 소멸되는지 확인합시다.



+ Recent posts