여러분들은 리눅스 시스템이 탑재된 휴대폰이나 라즈베리파이를 쓰면 동시에 여러 프로그램을 실행할 수 있습니다. 휴대폰을 보면 다양한 프로그램이 동시에 실행하는 것을 확인할 수 있습니다. 예를 들면 브라우저를 실행하면서 음악을 듣거나 메신저를 하면서 어플리케이션을 다운로드 할 수 있습니다.

그래서 사람들은 여러 프로세스들이 동시에 CPU에서 실행한다고 느낄 수 있습니다. 하지만 CPU는 여러 개의 프로세스를 절대로 동시에 실행할 수는 없습니다. 리눅스 커널을 포함한 다양한 운영체제에서 스케줄링과 멀티 태스킹 기법이 생겨난 이유는 다음과 같습니다.
CPU는 한 순간에 한 개의 프로세스의 코드만을 실행할 수 있습니다.
 
여러 개의 프로세스들이 효율적으로 번갈아 CPU에서 실행할 수 있게 규칙을 부여하고 프로세스들을 관리하는 소프트웨어 모듈을 스케줄러라고 말합니다.

하나의 프로세스는 CPU에서 실행을 시작하면 계속 CPU에서 실행하는 것이 아니라 실행을 하다가 잠깐 멈추고 다시 실행하는 방식으로 동작합니다. 즉, 프로세스는 CPU를 점유하면서 실행 중인 상태와 실행 대기하는 상태로 계속 변경하는 것입니다.

메모리에 존재하는 여러 프로세스 중에서 실제 CPU에서 실행될 프로세스를 선택하는 일을 스케줄링이라고 말합니다. 이 때 어떤 프로세스를 어떤 방식으로 선택할지를 결정해야 합니다. 

스케줄링 동작은 다음 그림으로 표현할 수 있습니다.
 

CPU에서 실행하려고 대기 중인 Process A ~ Process D 프로세스 중 하나를 선택해서 CPU에서 실행시키는 동작입니다.

스케줄링 동작을 다른 각도에서 살펴보겠습니다. 다음 그림은 프로세스 상태 변화 다이어그램입니다.
 

커널은 프로세스에게 프로세스 상태를 부여합니다. 프로세스가 생성 및 실행된 후 종료할 때까지 위와 같은 상태 변화로 동작합니다.

프로세스가 CPU에서 실행하기 위해서는 실행 대기(TASK_RUNNING) 상태로 변경한 다음 커널 스케줄링에 의해 CPU 실행(TASK_RUNNING) 상태로 변경되어야 합니다.

대부분 보통 프로세스 실행 상태 변화 흐름을 프로세스 1인칭으로 바라볼 때가 많습니다.
이번에는 시스템 전체 관점으로 프로세스 상태 변화 다이어그램을 살펴봅시다.

 

커널에서 실행 중인 전체 프로세스가 각각 어떤 상태로 실행 중인지를 보여주는 그림입니다. 원형으로 표시된 A~N는 각각 프로세스를 의미합니다.

A~D 프로세스들은 실행 대기(TASK_RUNNING) 상태에 있습니다. CPU에서 실행하기 위해 대기 중인 프로세스입니다.

CPU 실행(TASK_RUNNING) 상태를 보면 E 프로세스가 CPU에서 실행 중입니다.

다른 관점으로 스케줄링 동작을 다음과 같이 설명할 수 있습니다.
실행 대기(TASK_RUNNING) 상태에 있는 프로세스 중 하나를 선택해서 CPU 실행(TASK_RUNNING) 상태로 바꿔주는 동작

커널 스케줄링은 프로세스 상태 기준으로 실행 대기 중에 있는 프로세스를 어떤 방식으로 실행할지를 결정합니다. 따라서 프로세스 상태 정보는 매우 중요합니다.

대부분 드라이버 코드를 작성할 때 프로세스 상태를 변경하는 코드를 작성할 필요는 없습니다. 하지만, 커널 스레드 핸들 함수를 구현할 때 프로세스 상태를 변경하는 코드를 입력할 때가 있습니다. 이 때 반드시 set_current_state() 함수를 써서 프로세스 상태를 변경해야 합니다.

그 이유는 다음과 같이 set_current_state() 함수 코드를 보면 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/sched.h]
1 #define set_current_state(state_value)
2 do {
3 WARN_ON_ONCE(is_special_task_state(state_value));
4 current->task_state_change = _THIS_IP_;
5 smp_store_mb(current->state, (state_value));
6 } while (0)

5 번째 줄을 보면 smp_store_mb() 함수를 호출해서 메모리 배리어를 실행합니다.
메모리 배리어 코드를 추가하면 GCC 컴파일러가 코드 최적화를 위해 코드 위치를 변경하는 것을 방지합니다.


리눅스 커널 코드를 읽으면 다음 매크로를 자주 볼 수 있습니다.
+ 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


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

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

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

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

_do_fork() 함수 소개

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

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

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


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


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

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

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

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

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

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

unsigned long clone_flags;

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

unsigned long stack_start;

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

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

int __user *parent_tidptr;
int __user *child_tidptr;

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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



#Reference

프로세스 소개

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

프로세스 확인하기

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

  | do_fork() 함수 소개

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

+ Recent posts