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



+ Recent posts