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



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

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


+ Recent posts