Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

138199
1107
135866


리눅스 커널 레시피(4월 출간 예정) 전체 목차 -----Table of Contents-----

@ 리눅스 개발자분들께...

제 블로그에 오셔서 제 책이 언제 출간되는지 궁금해하시는 분이 계신 것 같은데요.
그래서 진행 과정을 공유드리려고 합니다.

이번 주 월요일(3/23/2020)에 최종 원고를 출판사에 전달했고 지금은 인덱스 작업 중에 있습니다.
4월 초에 출간이 될 예정이고 출간은 더 이상 연기되지는 않습니다.

참고로 책은 1부(700페이지)와 2부(900페이지)로 나눠져 있으며, 챕터들은 다음과 같이 구성돼 있습니다.

   * 1부: 리눅스 소개, 라즈베리 파이 설정, 커널 디버깅, 프로세스, 인터럽트, 인터럽트 후반부, 워크큐
   * 2부: 커널 타이머, 동기화, 스케줄링, 시스템 콜, 시그널, 가상 파일 시스템, 메모리 관리, 부록

혹시 궁금한 점이 있으면 댓글 주시면 답신드리겠습니다.

Thanks,
Austin Kim


리눅스의 전망과 소개


라즈베리 파이 설정 

라즈베리 파이 설치하기
라즈베리 파이 기본 설정하기 
라즈비안 리눅스 커널 빌드


리눅스커널 디버깅


프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  

인터럽트 처리



인터럽트 후반부 처리









6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인

워크큐

7.1 워크큐 소개
   7.1.1 워크큐 주요 개념 알아보기
   7.1.2 워크큐의 특징은 무엇인가
   7.1.3 워크큐를 다른 인터럽트 후반부 기법과 비교해보기
   7.1.4 워크큐를 잘 알아야 하는 이유
7.2 워크큐 종류 알아보기
   7.2.1 alloc_workqueue() 함수 분석하기  
   7.2.2 7가지 워크큐를 알아보기
7.3 워크란
   7.3.1 struct work_struct 구조체
   7.3.2 워크는 어떻게 초기화를 할까?
7.4 워크를 워크큐에 어떻게 큐잉할까?
   7.4.1 워크를 워크큐에 큐잉하는 예제 코드 살펴보기
   7.4.2 워크큐 전체 흐름도에서 워크를 워크큐에 큐잉하는 과정 소개
   7.4.3 워크를 워크큐에 큐잉하는 인터페이스 함수 분석하기
   7.4.4 __queue_work() 함수 분석하기
   7.4.5 __queue_work_on() 함수에서 호출하는 워크큐 내부 함수 분석하기
7.5 워크는 누가 언제 실행하나?
   7.5.1 워크 실행의 출발점인 worker_thread() 함수 분석 
   7.5.2 process_one_work() 함수 분석
7.6. 워커 스레드란
   7.6.1 워커와 워커 스레드란
   7.6.2 워커 자료구조인 struct worker 구조체 알아보기
   7.6.3 워커 스레드는 누가 언제 만들까
   7.6.4 워커 스레드를 만드는 create_worker() 함수 분석하기
   7.6.5 create_worker() 함수에서 호출한 워크큐 커널 함수 분석하기
   7.6.6 워커 스레드 핸들 worker_thread() 함수 분석하기 
7.7 워크큐 실습 및 디버깅
   7.7.1 ftrace 워크큐 이벤트 소개
   7.7.2 라즈베리파이에서 ftrace로 워크큐 동작 확인
   7.7.3 인터럽트 후반부로 워크큐 추가 실습 및 로그 분석
7.8 딜레이 워크 소개
   7.8.1 딜레이 워크란 무엇인가?
   7.8.2 딜레이 워크 전체 흐름도 소개
   7.8.3 딜레이 워크는 어떻게 초기화할까?
   7.8.4 딜레이 워크 실행의 시작점은 어디일까?
   7.8.5 딜레이 워크는 누가 언제 큐잉할까?
7.9 라즈베리파이 딜레이 워크 실습 및 로그 확인
   7.9.1 패치 코드 내용과 작성 방법 알아보기
   7.9.2 ftrace 로그 설정 방법 소개
   7.9.3 ftrace 로그 분석해보기
7.10 정리

커널 시간관리

커널 타이머 관리 주요 개념 소개
jiffies란
커널 타이머 제어
동적 타이머 초기화
동적 타이머 등록하기
동적 타이머는 누가 언제 실행하나?
라즈베리파이 커널 타이머 실습 및 로그 분석
   
커널 동기화

커널 동기화 기본 개념 소개
레이스 발생 동작 확인
커널 동기화 기법 소개
스핀락
뮤텍스란
커널 동기화 디버깅

프로세스 스케줄링

스케줄링 소개
프로세스 상태 관리
   어떤 함수가 프로세스 상태를 바꿀까?
스케줄러 클래스
런큐
CFS 스케줄러
   CFS 관련 세부 함수 분석  
선점 스케줄링(Preemptive Scheduling)   
프로세스는 어떻게 깨울까?
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
스케줄링 디버깅
   스케줄링 프로파일링
     CPU에 부하를 주는 테스트   
     CPU에 부하를 주지 않는 테스트 

시스템 콜

시스템 콜 주요 개념 소개
유저 공간에서 시스템 콜은 어떻게 발생할까
시스템 콜 핸들러는 어떤 동작을 할까? 
시스템 콜 실행 완료 후 무슨 일을 할까?
시스템 콜 관련 함수  
시스템 콜 디버깅  
   

시그널이란

시그널이란
시그널 설정은 어떻게 할까
시그널 생성 과정 함수 분석
프로세스는 언제 시그널을 받을까
시그널 전달과 처리는 어떻게 할까?
시그널 제어 suspend() 함수 분석 
시그널 ftrace 디버깅
 
  
가상 파일시스템 

가상 파일시스템 소개
파일 객체
파일 객체 함수 오퍼레이션 동작
프로세스는 파일객체 자료구조를 어떻게 관리할까?
슈퍼블록 객체
아이노드 객체
덴트리 객체
가상 파일시스템 디버깅

커널 메모리 

가상 주소를 물리 주소로 어떻게 변환할까?   
메모리 존(Zone)에 대해서   
커널 메모리 할당은 어떻게 할까   
슬랩 메모리 할당자와 kmalloc 슬랩 캐시 분석   
커널 메모리 디버깅


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)



[리눅스커널] 시그널: __send_signal()dequeue_signal() 함수 분석 - 시그널 생성/시그널 받기 12. 시그널

커널에서 시그널 서브시스템의 핵심 동작은 다음과 같다.

   * 시그널 전송: 시그널을 받은 프로세스에게 펜딩 시그널 정보를 써주고 시그널을 받을 프로세스를 깨운다.
   * 시그널 받기: 깨어난 프로세스는 펜딩 시그널을 받아 시그널에 대한 후속 처리를 한다.  

이번 시간에는 펜딩 시그널 정보를 써주고(시그널 생성), 펜딩된 시그널을 프로세스(시그널 받음)는 읽는 과정에 
초점을 맞춰 소스를 분석해보자.   

__send_signal() 함수 분석

먼저 __send_signal() 함수를 보자.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
enum pid_type type, int from_ancestor_ns)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
...
/* 태스크 디스크립터의 &t->signal->shared_pending 혹은 &t->pending를
    sigpending 구조체인 pending에 저장한다. 
    여기서 중요한 시사점이 있다. 전달되는 시그널의 유형에 따라 다른 필드에 펜딩 시그널을 저장한다는 점이다.
                        그룹에 전달되는 시그널이면:  &t->signal->shared_pending
         한 개 프로세스에 전달되는 시그널이면: &t->pending */

pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
...
/ * sigqueue 구조체인 q란 포인터형 지역 변수를 통해 동적 메모리를 할당 받는다. */
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
if (q) {
/ * &pending->list 연결 리스트에 &q->list를 추가한다.
list_add_tail(&q->list, &pending->list);

이 코드에서 중요한 포인트는 시그널의 유형에 따라 서로 다른 구조에 펜딩 시그널을 써준다는 점이다.

   * 그룹에 전달되는 시그널이면:  &t->signal->shared_pending
   *  한 개 프로세스에 전달되는 시그널이면: &t->pending */

이어서 시그널을 프로세스가 받는 과정에서 펜딩 시그널을 읽는 코드를 보자.

dequeue_signal() 함수 분석

dequeue_signal() 함수의 구현부의 코드를 보자.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
01 int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)
02 {
03 bool resched_timer = false;
04 int signr;
05
06 /* We only dequeue private signals from ourselves, we don't let
07 * signalfd steal them
08 */
09 signr = __dequeue_signal(&tsk->pending, mask, info, &resched_timer);
10 if (!signr) {
11 signr = __dequeue_signal(&tsk->signal->shared_pending,
12 mask, info, &resched_timer);

위 코드를 펜딩 시그널을 읽는 관점으로 분석하면;

   * 09번째 줄: '&tsk->pending'에 있는 연결 리스트(단일 프로세스에게 시그널이 전달됐을 경우)를 통해 펜딩 시그널 정보를 읽는다.
   * 11~12번째 줄: '&tsk->pending'에 있는 연결 리스트가 비어 있다면(그룹으로 시그널이 전달됐을 경우),
                          &tsk->signal->shared_pending에 있는 연결 리스트를 통해 펜딩 시그널 정보를 읽는다.

TRACE32 펜딩 시그널 디버깅 하기

이번에는 펜딩 시그널(그룹에게 전달된 시그널)을 받은 프로세스의 자료 구조를 디버깅을 통해 알아보자.

&t->pending->list
  (struct task_struct *) (struct task_struct*)0xebd99200 = 0xEBD99200 -> (
    (long int) state = 0x0,
    (void *) stack = 0xEBDA4000,
    (atomic_t) usage = ((int) counter = 0x6),
    (unsigned int) flags = 0x00404100,
...
    (sigset_t) blocked = ((long unsigned int [2]) sig = (0x0, 0x0)),
    (sigset_t) real_blocked = ((long unsigned int [2]) sig = (0x0, 0x0)),
    (sigset_t) saved_sigmask = ((long unsigned int [2]) sig = (0x0, 0x0)),
    (struct sigpending) pending = (
      (struct list_head) list = (
        (struct list_head *) next = 0xEBD9984C,
        (struct list_head *) prev = 0xEBD9984C),
      (sigset_t) signal = ((long unsigned int [2]) sig = (0x0, 0x0))),
 
위에서 보이듯 pending->list 의 next와 prev 필드가 값은 주소를 저장하고 있다. 
즉, 링크드 리스트가 비어 있다는 이야기이다.

이번에는 signal->shared_pending->list 를 확인해보자.

&t->signal->shared_pending->list

  (struct task_struct *) (struct task_struct*)0xebd99200 = 0xEBD99200 -> (
    (long int) state = 0x0,
    (void *) stack = 0xEBDA4000,
    (atomic_t) usage = ((int) counter = 0x6),
    (unsigned int) flags = 0x00404100,
...
    (struct signal_struct *) signal = 0xEBC5F440 -> (
      (atomic_t) sigcnt = ((int) counter = 0x10),
      (atomic_t) live = ((int) counter = 0x0E),
      (int) nr_threads = 0x0E,
      (struct list_head) thread_head = ((struct list_head *) next = 0xEBD99764, (struct list_head *)
      (wait_queue_head_t) wait_chldexit = ((spinlock_t) lock = ((struct raw_spinlock) rlock = ((arch
      (struct task_struct *) curr_target = 0xEBD99200,
      (struct sigpending) shared_pending = (
        (struct list_head) list = (
          (struct list_head *) next = 0xF1E4A510 -> (
            (struct list_head *) next = 0xEBC5F470 -> (
              (struct list_head *) next = 0xF1E4A510,
              (struct list_head *) prev = 0xF1E4A510),
            (struct list_head *) prev = 0xEBC5F470),
          (struct list_head *) prev = 0xF1E4A510),
        (sigset_t) signal = ((long unsigned int [2]) sig = (0x0100, 0x0))),
      (int) group_exit_code = 0x9,
      (int) notify_count = 0x0,

signal->shared_pending->list 필드를 보니 링크드 리스트에 뭔가 주소(0xF1E4A510, 0xEBC5F470)가 보인다.

&t->signal->shared_pending->list 주소를 통해 펜딩 시그널의 정체인 struct sigqueue를 확인해보자.
동작 원리는 아래 코드와 같다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
enum pid_type type, int from_ancestor_ns)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
...
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
if (q) {
/ * &pending->list 연결 리스트에 &q->list를 추가한다.
list_add_tail(&q->list, &pending->list);

v.v % container_of(0xF1E4A510,struct sigqueue,list)

  (struct sigqueue *) container_of(0xF1E4A510,struct sigqueue,list) = 0xF1E4A510 =  -> (
    (struct list_head) list = ((struct list_head *) next = 0xEBC5F470  
    (int) flags = 0 = 0x0 = '....',
    (siginfo_t) info = (
      (int) si_signo = 9 = 0x9 
      (int) si_errno = 0 = 0x0 
      (int) si_code = 0 = 0x0 
      (union) _sifields = ((int [29]) _pad = ([0] ...
    (struct user_struct *) user = 0xF1F35900  

출력 결과로 9번 SIG_KILL 시그널이 전달됐다는 사실을 알 수 있다.

이제껏 확인한 디버깅 정보의 주인공은 PID가 6269인 "mediaserver" 프로세스이다.
이번에는 프로세스 그룹에 속한 다른 프로세스의 목록을 보자.

crash> ps -g 6269
PID: 1218   TASK: ebd99200  CPU: 0   COMMAND: "mediaserver"
  PID: 1461   TASK: eac70900  CPU: 0   COMMAND: "Binder:1218_1"
  PID: 1483   TASK: eac95100  CPU: 1   COMMAND: "Binder:1218_2"
  PID: 1487   TASK: eb82bf00  CPU: 3   COMMAND: "HwBinder:1218_1"
  PID: 1489   TASK: ebc81200  CPU: 3   COMMAND: "Binder:1218_3"
  PID: 6269   TASK: eb4ee300  CPU: 1   COMMAND: "MediaClock"
  PID: 6270   TASK: d1a4a400  CPU: 3   COMMAND: "NuPlayerDriver "
  PID: 6271   TASK: d1a49b00  CPU: 2   COMMAND: "generic"
  PID: 6273   TASK: d1918000  CPU: 1   COMMAND: "NuPlayerRendere"
  PID: 7197   TASK: d23be300  CPU: 0   COMMAND: "MediaClock"
  PID: 7198   TASK: d23bda00  CPU: 1   COMMAND: "NuPlayerDriver "
  PID: 7199   TASK: d219a400  CPU: 3   COMMAND: "generic"
  PID: 7200   TASK: d219bf00  CPU: 0   COMMAND: "Binder:1218_4"
  PID: 7210   TASK: d21a8900  CPU: 3   COMMAND: "NuPlayerRendere"

위 프로세스 목록 중에서 태스크 디스크립터가 d1a4a400인 "NuPlayerDriver " 프로세스의 펜딩 시그널 정보를 확인해보자.

  (struct task_struct *) (struct task_struct*)0xd1a4a400 = 0xD1A4A400 -> (
    (long int) state = 0x0,
    (void *) stack = 0xE272C000,
    (atomic_t) usage = ((int) counter = 0x3),
    (unsigned int) flags = 0x40404040,
...
    (struct nsproxy *) nsproxy = 0xC1E1A468,
    (struct signal_struct *) signal = 0xEBC5F440 -> (
      (atomic_t) sigcnt = ((int) counter = 0x10),
      (atomic_t) live = ((int) counter = 0x0E),
      (int) nr_threads = 0x0E,
      (struct list_head) thread_head = ((struct list_head *) next = 0xEBD99764, (struct list_head *)
      (wait_queue_head_t) wait_chldexit = (
        (spinlock_t) lock = ((struct raw_spinlock) rlock = ((arch_spinlock_t) raw_lock = ((u32) sloc
        (struct list_head) task_list = ((struct list_head *) next = 0xEBC5F464, (struct list_head *)
      (struct task_struct *) curr_target = 0xEBD99200,
      (struct sigpending) shared_pending = (
        (struct list_head) list = (
          (struct list_head *) next = 0xF1E4A510 -> (
            (struct list_head *) next = 0xEBC5F470 -> (
              (struct list_head *) next = 0xF1E4A510,
              (struct list_head *) prev = 0xF1E4A510),
            (struct list_head *) prev = 0xEBC5F470),
          (struct list_head *) prev = 0xF1E4A510),
        (sigset_t) signal = ((long unsigned int [2]) sig = (0x0100, 0x0))),
      (int) group_exit_code = 0x9,
      (int) notify_count = 0x0,
 
역시 0xF1E4A510와 0xEBC5F470 주소로 보아 &t->signal->shared_pending->list 주소를 통해 펜딩 시그널이 전달된 것을 알 수 있다.

[리눅스커널] 워크큐: struct worker 구조체 파악하기 7. 워크큐(Workqueue)

워커를 관리하고 저장하는 자료구조는 worker 구조체입니다. 이번 절에서는 worker 구조체의 세부 필드를 분석하겠습니다. 

worker 구조체 분석

다음은 worker 구조체의 선언부입니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue_internal.h
1 struct worker {
2 union {
3 struct list_head entry; 
4 struct hlist_node hentry; 
5 };
6 struct work_struct *current_work;
7 work_func_t current_func;
8 struct pool_workqueue *current_pwq; 
9 bool desc_valid;
10 struct list_head scheduled;
11
12 struct task_struct *task;
13 struct worker_pool *pool;
14
15 struct list_head node;
16 unsigned long last_active;
17 unsigned int flags;
18 int id;
19
20 char desc[WORKER_DESC_LEN];
21
22 struct workqueue_struct *rescue_wq;
23};

각 구조체의 세부 필드를 살펴봅시다.

struct work_struct *current_work
work_struct 구조체로 현재 실행하려는 워크를 저장하는 필드입니다.

work_func_t current_func
실행하려는 워크 핸들러의 주소를 저장하는 필드입니다. 

그런데 워크 구조체와 워크 핸들러는 커널의 어느 코드에서 worker 구조체 필드에 저장될까요? 워크 구조체와 워크 핸들러는 process_one_work() 함수에서 앞의 current_work와 current_func 필드에 각각 저장됩니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c
static void process_one_work(struct worker *worker, struct work_struct *work)
{
...
worker->current_work = work;
worker->current_func = work->func;
worker->current_pwq = pwq;

이어서 다른 필드를 분석하겠습니다.

struct task_struct *task
워커 스레드의 태스크 디스크립터 주소입니다.

struct worker_pool *pool
워커를 관리하는 워커 풀 주소를 저장하는 필드입니다.

struct list_head node
워커풀에 등록된 연결 리스트입니다.

워커와 워커 스레드는 어떤 관계일까?

워커 스레드는 커널 스레드의 한 종류로서 워크를 실행하는 프로세스입니다. 워커 스레드의 스레드 핸들 함수는 worker_thread()입니다.


커널 스레드가 무슨 일을 하는지 알려면 해당 커널 스레드 핸들 함수를 분석해야 합니다. 처음 커널 스레드를 분석할 때는 먼저 스레드 핸들 함수를 분석해야 합니다. 이를 통해 스레드를 어떤 과정으로 실행 및 제어하는지 알 수 있습니다.


워커는 워커 스레드를 표현하는 자료구조이며, worker 구조체입니다. 워커 스레드와 워커는 비슷한 개념으로 볼 필요가 있습니다.

워커 스레드를 생성하기 위해서는 우선 워커를 생성해야 합니다. 워커는 워커 스레드를 담은 그릇(container)과 비슷한 역할을 수행합니다. 만약 “워커를 해제했다”라고 하면 “해당 워커 스레드를 해제했다”와 같은 뜻입니다.

워커 스레드 구조체인 worker 구조체의 주요 플래그를 살펴봤으니 워커 스레드를 누가 언제 생성하는지 살펴보겠습니다.


sample code palette

git clone --depth=1 https://github.com/raspberrypi/linux

>

#!/bin/bash
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo "ftrace off"
sleep 3
cp /sys/kernel/debug/tracing/trace .
mv trace ftrace_log.c


>

#!/bin/bash

echo 0 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo "tracing_off"

echo 0 > /sys/kernel/debug/tracing/events/enable
sleep 1
echo "events disabled"

echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter init"

echo function > /sys/kernel/debug/tracing/current_tracer
sleep 1
echo "function tracer enabled"

echo _raw_spin_unlock_irqrestore  > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter enabled"

echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
sleep 1
echo "event enabled"

echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
echo "function stack trace enabled"

echo 1 > /sys/kernel/debug/tracing/tracing_on
echo "tracing_on"


raspbian_fork: raspbian_test_fork.c
gcc -g -o raspbian_fork raspbian_test_fork.c


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#define PROC_TIMES  50
#define SLEEP_DURATION  2
#define FORK_MAX_TIMES  3

void raspbian_proc_process(void);

void raspbian_proc_process(void) 
{
int proc_times = 0;
 
for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
printf("raspbian tracing n");
sleep(SLEEP_DURATION);
}
}

int main() 
{
pid_t pid;
int fork_times = 0;

pid = fork();
if ( pid == 0 )  {
printf("start execution of child processn");
raspbian_proc_process();
}

else if ( pid > 0 ) {
printf("start execution of parent processn");
raspbian_proc_process();
}
}

[리눅스커널] 특정 CPU를 Isolation 시키고 싶은 경우 Linux Kernel - Core Analysis

프로젝트를 진행하다 보면 특정 CPU를 Isolation 시키고 싶을 때가 있습니다.
이 때 다음 패치(CPU2와 CPU3를 Isolation)를 적용하면 됩니다.

* 커널 4.19 버전
diff --git a/kernel/cpu.c b/kernel/cpu.c
index d9f855c..816bf4f 100644
--- a/kernel/cpu.c
+++ b/kernel/cpu.c
@@ -1126,6 +1126,10 @@ static int do_cpu_up(unsigned int cpu, enum cpuhp_state target)
 {
        int err = 0;

+       if(cpu == 2 | cpu ==3) {
+               return -EINVAL;
+       }
+
        if (!cpu_possible(cpu)) {
                pr_err("can't online cpu %d because it is not configured as may-hotadd at boot time\n",
                       cpu);

* 커널 3.18 버전
diff --git a/kernel/cpu.c b/kernel/cpu.c
index cd9c5c6..aeda1f8 100644
--- a/kernel/cpu.c
+++ b/kernel/cpu.c
@@ -518,6 +518,10 @@ int cpu_up(unsigned int cpu)
 {
    int err = 0;

+    if(cpu == 2 | cpu ==3) {
+       return -EINVAL;
+   }
+
    if (!cpu_possible(cpu)) {
        pr_err("can't online cpu %d because it is not configured as may-hotadd at boot time\n",
               cpu);


이처럼 논리적인 CPU를 끄면 디바이스 노드에도 CPU2와 CPU3이 보이지 않을 것입니다.
$ ls /sys/devices/system/cpu
cpu0   cpu1

[리눅스커널] GCC: notrace 옵션 - no_instrument_function Linux Kernel - Core Analysis

walk_stackframe() 함수의 구현부를 보면 notrace 키워드로 선언됐음을 알 수 있습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/stacktrace.c
void notrace walk_stackframe(struct stackframe *frame,
     int (*fn)(struct stackframe *, void *), void *data)
{
while (1) {
int ret;

if (fn(frame, data))
break;
ret = unwind_frame(frame);
if (ret < 0)
break;
}
}
EXPORT_SYMBOL(walk_stackframe);

이 함수를 전처리 코드에서 확인하면 구현부는 다음과 같습니다.

void __attribute__((no_instrument_function)) walk_stackframe(struct stackframe *frame,
       int (*fn)(struct stackframe *, void *), void *data)
{
 while (1) {
  int ret;

  if (fn(frame, data))
   break;
  ret = unwind_frame(frame);
  if (ret < 0)
   break;
 }
}

함수 구현부와 같이 notrace는 '__attribute__((no_instrument_function))' 구문으로 치환이 됩니다.

[리눅스커널] 인터럽트: ftrace와 커널 로그로 인터럽트 컨텍스트 확인해보기

이번 절에서는 ftrace 로그를 분석하면서 커널이 인터럽트를 어떻게 처리하는지 알아봅시다. 

리눅스 커널에서 커널 동작을 가장 정밀하게 담고 있는 로그는 뭘까요? 아마 많은 리눅스 전문가들은 ftrace라고 대답할 겁니다. ftrace는 리눅스 커널에서 제공하는 가장 강력한 디버그 로그입니다. 리눅스 커널의 공식 트레이서이기도 합니다. 여러분도 ftrace 로그를 자주 활용해서 리눅스 커널을 익히기를 바랍니다.

ftrace로 인터럽트를 처리하는 인터럽트 핸들러 함수에 필터를 걸고 콜 스택 로그를 받아 보겠습니다. 

인터럽트 동작을 확인하기 위한 ftrace 설정

ftrace로 인터럽트의 동작 방식을 분석하기 전에 ftrace를 설정하는 방법을 소개합니다. 먼저 다음 명령어를 입력해 봅시다.

#!/bin/bash

echo 0 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo "tracing_off"

echo 0 > /sys/kernel/debug/tracing/events/enable
sleep 1
echo "events disabled"

echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter init"

echo function > /sys/kernel/debug/tracing/current_tracer
sleep 1
echo "function tracer enabled"

echo dwc_otg_common_irq > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter enabled"

echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
echo "event enabled"

echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
echo "function stack trace enabled"

echo 1 > /sys/kernel/debug/tracing/tracing_on
echo "tracing_on"


이 같은 명령어를 입력한 후 irq_stack_trace.sh라는 이름으로 저장합니다. 그러고 나서 다음과 같은 명령어를 입력해 irq_stack_trace.sh 셸스크립트를 실행하면 ftrace를 빨리 설정할 수 있습니다.

root@raspberrypi:/home/pi # ./irq_stack_trace.sh

ftrace 설정 명령어 중 다음 코드를 함께 봅시다.

echo dwc_otg_common_irq > /sys/kernel/debug/tracing/set_ftrace_filter

이 명령어는 set_ftrace_filter에 다음 함수를 설정합니다.

dwc_otg_common_irq()

dwc_otg_common_irq() 함수가 호출될 때 함수 콜스택을 ftrace로 보기 위해 set_ftrace_filter 파일에 함수를 지정하는 것입니다.

이어서 ftrace를 받는 방법을 소개합니다.

#!/bin/bash

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

sleep 3

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

위 명령어를 입력해 get_ftrace.sh 셸 스크립트로 저장합니다. 그러고 나서 다음 명령어로 이 셸 스크립트를 실행하면 같은 폴더에 ftrace 로그를 저장한 ftrace_log.c 파일이 만들어집니다.

root@raspberrypi:/home/pi # ./get_ftrace.sh 

지금까지 설명한 실습 과정을 정리해 봅시다.

1. irq_stack_trace.sh 셸 스크립트를 실행해 ftrace를 설정한다.
2. get_ftrace.sh 셸 스크립트를 실행해 ftrace 로그를 받는다.

라즈베리 파이에서 받은 ftrace로 인터럽트 컨텍스트 확인

이제 ftrace 로그 분석을 시작하겠습니다. 먼저 ftrace 로그를 소개합니다.

1 kworker/0:0-27338 [000] d.h.  6028.897808: irq_handler_entry: irq=56 name=dwc_otg
2 kworker/0:0-27338 [000] 6028.897809: dwc_otg_common_irq <-__handle_irq_event_percpu
3 kworker/0:0-27338 [000] 6028.897847: <stack trace>
4  => handle_irq_event
5  => handle_level_irq
6  => generic_handle_irq
7  => bcm2836_chained_handle_irq
8  => generic_handle_irq
9  => __handle_domain_irq
10 => bcm2836_arm_irqchip_handle_irq
11 => __irq_svc
12 => _raw_spin_unlock_irqrestore
13 => _raw_spin_unlock_irqrestore
14 => schedule_timeout
15 => wait_for_common
16 => wait_for_completion_timeout
17 => usb_start_wait_urb
18 => usb_control_msg
19 => __usbnet_read_cmd
20 => usbnet_read_cmd
21 => __smsc95xx_read_reg
22 => __smsc95xx_phy_wait_not_busy
23 => __smsc95xx_mdio_read
24 => check_carrier
25 => process_one_work
26 => worker_thread
27 => kthread
28 => ret_from_fork


ftrace 로그를 보면 어느 로그부터 분석해야 할지 의문이 앞섭니다. 이때 염두에 둘 점은 아래에 있는 함수에서 위에 있는 함수 쪽으로 함수가 호출된다는 것입니다. 즉, ret_from_fork() 함수가 맨 먼저 실행된 후 다음과 같은 순서로 함수가 호출된 것입니다.

 kthread → worker_thread → process_one_work

이후 handle_level_irq() → handle_irq_event()→ __handle_irq_event_percpu()→ dwc_otg_common_irq() 순서로 함수가 호출됐습니다.  

다음 ftrace 로그는 조금 헷갈릴 수 있어 상세히 볼 필요가 있습니다.

2 kworker/0:0-27338 [000] 6028.897809: dwc_otg_common_irq <-__handle_irq_event_percpu
3 kworker/0:0-27338 [000] 6028.897847: <stack trace>
4  => handle_irq_event
5  => handle_level_irq

handle_irq_event() 함수까지 함수 호출이 수행된 듯합니다. 실제로는 다음 흐름으로 맨 마지막에 실행된 함수는 dwc_otg_common_irq()입니다. 함수 흐름은 다음과 같습니다.
handle_irq_event → __handle_irq_event_percpu → dwc_otg_common_irq


먼저 1번째 줄을 보겠습니다.

1 kworker/0:0-27338 [000] d.h.  6028.897808: irq_handler_entry: irq=56 name=dwc_otg

위 ftrace 메시지는 다음과 같은 사실을 말해줍니다.
pid가 27338인 kworker/0:0 프로세스 실행 중 인터럽트가 발생
인터럽트 번호는 56번이고 이름은 dwc_otg
인터럽트 핸들러가 실행을 시작한 시간은 6028.897808임

이번에는 콜 스택을 볼 차례입니다. 콜 스택에서는 맨 먼저 호출된 함수부터 봐야 하니 로그의 가장 아랫부분부터 봐야 합니다. 

12 => _raw_spin_unlock_irqrestore
13 => _raw_spin_unlock_irqrestore
14 => schedule_timeout
15 => wait_for_common
16 => wait_for_completion_timeout
17 => usb_start_wait_urb
18 => usb_control_msg
19 => __usbnet_read_cmd
20 => usbnet_read_cmd
21 => __smsc95xx_read_reg
22 => __smsc95xx_phy_wait_not_busy
23 => __smsc95xx_mdio_read
24 => check_carrier
25 => process_one_work
26 => worker_thread
27 => kthread
28 => ret_from_fork

위 ftrace 로그는 인터럽트가 발생하기 전의 함수 호출 흐름입니다. 콜스택을 보니 kworker/0:0 프로세스가 실행 중입니다. check_carrier() 워크 핸들러 함수가 호출된 후 USB 드라이버가 동작 중입니다. 

이어서 인터럽트가 발생하고 난 후의 로그를 보겠습니다. 

1 kworker/0:0-27338 [000] d.h.  6028.897808: irq_handler_entry: irq=56 name=dwc_otg
2 kworker/0:0-27338 [000] 6028.897809: dwc_otg_common_irq <-__handle_irq_event_percpu
3 kworker/0:0-27338 [000] 6028.897847: <stack trace>
4  => handle_irq_event
5  => handle_level_irq
6  => generic_handle_irq
7  => bcm2836_chained_handle_irq
8  => generic_handle_irq
9  => __handle_domain_irq
10 => bcm2836_arm_irqchip_handle_irq
11 => __irq_svc
12 => _raw_spin_unlock_irqrestore

여기서 어떤 함수가 실행되던 도중에 인터럽트가 발생한 것일까요? 이 질문을 받으면 다음과 같이 대답할 수 있습니다. 

_raw_spin_unlock_irqrestore() 함수 실행 중 "irq=56 name=dwc_otg" 인터럽트가 발생했다.
 
ARM 프로세스는 인터럽트가 발생하면 익셉션을 유발해 __irq_svc 인터럽트 벡터를 실행합니다. 이후 리눅스 커널 내부의 인터럽트를 처리하는 커널 내부의 함수가 다음 순서로 호출되는 것입니다.

handle_level_irq()
handle_irq_event()
__handle_irq_event_percpu()
dwc_otg_common_irq()

이후 “irq=56 name=dwc_otg” 인터럽트를 처리하는 인터럽트 핸들러인 dwc_otg_common_irq() 함수를 호출합니다.

다소 복잡해 보이는 ftrace 로그를 그림으로 정리하면 다음과 같습니다. 

 
[그림 6] 인터럽트 발생 시 프로세스 스택 공간에서 함수 호출 흐름  

위 그림은 56번 인터럽트가 발생한 후 함수 실행 흐름입니다. 위 그림에서 오른쪽 상단에 인터럽트 컨택스트라고 표시된 함수 구간이 있습니다. 이 함수가 실행될 때를 인터럽트 컨택스트라고 부릅니다.

ftrace 로그와 위 그림을 종합하면 실행 흐름은 다음과 같이 정리할 수 있겠습니다.
 - pid가 27338인 kworker/0:0 프로세스가 _raw_spin_unlock_irqrestore() 함수를 실행
 - "irq=56 name=dwc_otg" 인터럽트가 발생해 인터럽트 벡터인 __irq_svc() 함수로 브랜치
 - 인터럽트 핸들러인 dwc_otg_common_irq() 함수가 실행됨

커널 로그로 인터럽트 컨택스트 확인하기

이번엔 다른 리눅스 시스템에서 추출한 커널 로그를 보면서 인터럽트 컨택스트를 배워봅시다. 같이 분석할 커널 로그는 다음과 같습니다.
[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/commit/?id=bbe097f092b0d13e9736bd2794d0ab24547d0e5d]

WARNING: CPU: 0 PID: 0 at include/linux/usb/gadget.h:405
 ecm_do_notify+0x188/0x1a0
 Modules linked in:
 CPU: 0 PID: 0 Comm: swapper Not tainted 4.7.0+ #15
 Hardware name: Atmel SAMA5
 [<c010ccfc>] (unwind_backtrace) from [<c010a7ec>] (show_stack+0x10/0x14)
 [<c010a7ec>] (show_stack) from [<c0115c10>] (__warn+0xe4/0xfc)
 [<c0115c10>] (__warn) from [<c0115cd8>] (warn_slowpath_null+0x20/0x28)
 [<c0115cd8>] (warn_slowpath_null) from [<c04377ac>] (ecm_do_notify+0x188/0x1a0)
 [<c04377ac>] (ecm_do_notify) from [<c04379a4>] (ecm_set_alt+0x74/0x1ac)
 [<c04379a4>] (ecm_set_alt) from [<c042f74c>] (composite_setup+0xfc0/0x19f8)
 [<c042f74c>] (composite_setup) from [<c04356e8>] (usba_udc_irq+0x8f4/0xd9c)
 [<c04356e8>] (usba_udc_irq) from [<c013ec9c>] (handle_irq_event_percpu+0x9c/0x158)
 [<c013ec9c>] (handle_irq_event_percpu) from [<c013ed80>] (handle_irq_event+0x28/0x3c)
 [<c013ed80>] (handle_irq_event) from [<c01416d4>] (handle_fasteoi_irq+0xa0/0x168)
 [<c01416d4>] (handle_fasteoi_irq) from [<c013e3f8>] (generic_handle_irq+0x24/0x34)
 [<c013e3f8>] (generic_handle_irq) from [<c013e640>] (__handle_domain_irq+0x54/0xa8)
 [<c013e640>] (__handle_domain_irq) from [<c010b214>] (__irq_svc+0x54/0x70)
 [<c010b214>] (__irq_svc) from [<c0107eb0>] (arch_cpu_idle+0x38/0x3c)
 [<c0107eb0>] (arch_cpu_idle) from [<c0137300>] (cpu_startup_entry+0x9c/0xdc)
 [<c0137300>] (cpu_startup_entry) from [<c0900c40>] (start_kernel+0x354/0x360)
 [<c0900c40>] (start_kernel) from [<20008078>] (0x20008078)
 ---[ end trace e7cf9dcebf4815a6 ]---J6

위 커널 로그에서 __irq_svc(asm) ~ unwind_backtrace() 함수들은 인터텁트 컨택스트에서 수행되며, start_kernel() ~ arch_cpu_idle() 함수 구간은 프로세스 컨택스트라고 볼 수 있습니다. 

커널 로그에서 __irq_svc 레이블은 개발 도중 자주 보게 됩니다. 위와 같이 콜스택에서 인터럽트 벡터인 __irq_svc 레이블을 보면 다음과 같이 해석합시다. 

     “아, 인터럽트가 발생해서 인터럽트를 처리 중이구나” 
 
실전 임베디드 개발 중에 이런 패턴의 커널 로그를 자주 만나니 잘 기억해둡시다.

이번 소절에서는 인터럽트 컨택스트에 대해서 알아봤습니다. 중요한 내용이니 배운 내용을 정리해볼까요?
     첫째, 인터럽트 컨택스트는 무엇인가?
      인터럽트가 발생해 인터럽트를 핸들링하는 동작입니다.

    둘째, 인터럽트 컨택스트를 왜 정의할까?
     인터럽트를 핸들링하는 시점에서 더 빠르고 간결하게 코드를 실행하기 위해서입니다.

다음 소절에서는 인터럽트 컨택스트를 알려주는 in_interrupt() 함수를 소개합니다.
 
이번 절에서는 인터럽트 컨택스트에 대해서 알아봤습니다. 인터럽트 컨택스트란 현재 수행 중인 코드가 인터럽트를 처리하는 중이란 의미입니다. 다음 소절에서는 인터럽트 컨택스트를 알려주는 함수를 소개합니다.

프로세스는 높은 주소에서 낮은 주소 방향으로 스택을 씁니다. 사실 꼭 높은 주소에서 낮은 주소 방향으로 스택을 사용하도록 설정할 필요는 없습니다. 대신 반대로 낮은 주소에서 높은 주소 방향으로 스택을 쓰게 설정할 수 있습니다. 그런데 커널 고수들이 프로세스를 높은 주소에서 낮은 주소 방향으로 설정하니 업계의 사실상 표준이 된 것 같습니다. 

프로세스가 실행 중인 프로세스 스택 공간에 대해서 배워 볼까요? 프로세스가 생성될 때는 커널은 프로세스에게 0x2000 크기만큼 스택 공간을 할당합니다. 또한 프로세스 입장에서 스택 공간은 운동장으로 볼 수 있습니다. 프로세스는 스택 메모리 공간 내에서만 실행할 수 있습니다. 함수를 호출하거나 로컬 변수 사용할 때 자신에게 부여된 고유 스택 메모리를 사용합니다. 프로세스별로 할당된 스택 주소는 어떻게 확인할 수 있나요? 이 내용을 알기 전에 잠깐 태스크 디스크립터에 대해 배워야 합니다. 

여러분 혹시 TCB(Task Control Block)란 용어 들어본 적 있나요? 임베디드 시스템에서 태스크 혹은 프로세스 정보를 담고 있는 자료구조입니다. 리눅스 커널의 프로세스 정보를 담고 있는 자료 구조는 뭘까요? 여기서 임베디드 시스템에서 말하는 태스크와 리눅스 커널의 프로세스는 같은 개념으로 봐야 합니다. 정답은 struct task_struct입니다. 이 자료 구조에서 프로세스의 속성과 상태 정보를 확인할 수 있습니다.

프로세스별로 할당된 스택 주소는 태스크 디스크립터 struct task_struct 구조체 stack 필드에서 확인할 수 있습니다. 이 값은 스택 최상단 주소입니다. 프로세스에서 함수들을 실행할 때는 스택 최하단 주소에서 스택 최상단 주소 방향으로 스택을 씁니다. 즉, 높은 주소에서 낮은 주소로 스택을 사용합니다.

만약 프로세스가 스택 메모리 공간에서 실행 중 인터럽트가 발생하면 인터럽트 벡터와 인터럽트 서비스 루틴은 어느 공간에서 실행할까요? 정답은 프로세스 스택 공간에서 실행됩니다. 전세 살 듯이 잠시 프로세스 스택 공간을 활용하는 것입니다.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)


[리눅스커널] 인터럽트: 리눅스 커널에서의 인터럽트 처리 흐름 5. 인터럽트

인터럽트가 발생했을 때 커널이 이를 처리하는 과정은 다음과 같이 3단계로 나눌 수 있습니다.

1 단계: 인터럽트 발생
인터럽트가 발생하면 프로세스 실행을 중지하고 인터럽트 벡터로 이동합니다. 인터럽트 벡터에서 인터럽트 처리를 마무리한 후 다시 프로세스를 실행하기 위해 실행 중인 프로세스 레지스터 세트를 스택에 저장합니다. 이후 커널 내부 인터럽트 함수를 호출합니다. 

2단계: 인터럽트 핸들러 호출
커널 내부에서는 발생한 인터럽트에 대응하는 인터럽트 디스크립터를 읽어서 인터럽트 핸들러를 호출합니다. 

3단계: 인터럽트 핸들러 실행
인터럽트 핸들러에서 하드웨어를 직접 제어하고 유저 공간에 이 변화를 알립니다.

이해를 돕기 위해 한 가지 예를 들어보겠습니다. 안드로이드 휴대폰에서 화면을 손을 만지는 동작에서 여러분이 손으로 휴대폰 화면을 터치하면 내부 동작은 다음과 같은 단계로 나눌 수 있습니다.

1단계: 터치 인터럽트 발생
하드웨어적인 터치 모듈이 변화를 감지하고 터치 모듈에 대한 인터럽트를 발생시킵니다. 이때 인터럽트 벡터가 실행됩니다.

2단계: 터치 인터럽트 핸들러 호출
커널은 터치 인터럽트 번호로 해당 인터럽트 디스크립터를 읽습니다. 다음 인터럽트 디스크립터에 저장된 인터럽트 핸들러 주소를 찾아 인터럽트 핸들러를 호출합니다.

3단계: 터치 인터럽트 핸들러 실행
결국 터치 인터럽트 핸들러는 해당 터치 인터럽트를 받아 정해진 처리를 합니다. 화면을 업데이트하거나 하드웨어 터치 디바이스에 인터럽트를 잘 받았다는 사실을 알립니다. 

---
“인터럽트 디스크립터”, “인터럽트 벡터” 같은 낯선 용어로 설명했는데, 이러한 용어의 공학적 의미는 하나하나 각 장에서 다룰 예정입니다. 
---

인터럽트가 발생하면 이를 커널이 처리하는 과정을 터치 드라이버를 예로 들어 살펴봤습니다. 인터럽트 발생을 처리하는 단계를 함수 흐름과 실행 주체별로 분류하면 다음 그림과 같습니다.

그림 5.5 ARM 프로세서/리눅스 커널/디바이스 드라이버별 인터럽트 처리 흐름

전체 실행 흐름은 다음의 3단계로 분류할 수 있습니다.

1. ARM 프로세스
인터럽트가 발생하면 실행 중인 코드를 멈춘 후 인터럽트 벡터로 실행 흐름을 이동합니다. ARM 프로세스와 연관된 동작입니다. 

2. 리눅스 커널
인터럽트 벡터로 프로그램 카운터를 브랜치합니다. 커널 인터럽트 내부 함수에서 인터럽트를 관리하는 자료구조인 인터럽트 디스크립터를 읽습니다. 인터럽트 디스크립터에 저장된 인터럽트 핸들러를 호출합니다.

3. 디바이스 드라이버
각 디바이스 드라이버에서 등록한 인터럽트 핸들러를 실행해 인터럽트 발생에 대한 처리를 수행합니다.

정리하면 “인터럽트로 하드웨어적인 변화가 발생하면 리눅스 커널에서 어떻게 처리하는가"입니다. 이를 위해 이번 절에서는 인터럽트에 대해 소개했으니 이어지는 절에서 인터럽트 컨텍스트에 대해 살펴보겠습니다.


[리눅스커널] 인터럽트: 인터럽트를 잘 알아야 하는 이유 5. 인터럽트

커널이 인터럽트를 처리하는 과정과 자료구조를 왜 잘 알아야 할까요? 인터럽트를 처리하는 방식이 시스템 전반에 큰 영향을 끼치기 때문입니다. 또한 리눅스 커널 시스템 전반을 잘 이해하기 위해서도 커널이 인터럽트를 어떻게 처리하는지 잘 알고 있어야 합니다. 또 다른 이유는 다음과 같습니다. 

대부분의 리눅스 드라이버는 인터럽트를 통해 하드웨어 디바이스와 통신합니다. 그래서 디바이스 드라이버 코드를 처음 분석할 때 인터럽트를 처리하는 함수나 코드를 먼저 확인합니다. 인터럽트의 동작 방식을 잘 알고 있으면 디바이스 드라이버 코드를 빨리 이해할 수 있습니다.

인터럽트가 발생하면 프로세스는 이미 정해진 동작을 수행합니다. 인터럽트 처리 과정을 숙지하면 프로세스가 스택 메모리 공간에서 어떻게 실행되는지 알게 됩니다. 

CPU 아키텍처(x86, ARM)에 따라 인터럽트 벡터는 달리 동작합니다. 인터럽트 벡터가 어떻게 동작하는지 잘 알면 자연히 ARM 아키텍처에 대해 더 많이 알게 됩니다.

또한 리눅스 커널의 핵심 동작을 이해하기 위해서도 인터럽트의 세부 동작 방식을 알 필요가 있습니다. 그 이유는 다음과 같습니다.

스케줄링에서 선점(Preemptive) 스케줄링 진입 경로 중 하나가 인터럽트 처리를 끝낸 시점입니다.
유저 공간에서 등록한 시그널 핸들러는 인터럽트 핸들링을 한 다음 처리를 시작합니다.
레이스 컨디션이 발생하는 가장 큰 이유 중 하나가 비동기적으로 인터럽트가 발생해서 코드 실행을 멈추기 때문입니다.

무엇보다 리눅스 커널을 새로운 보드에 포팅하거나 시스템 전반을 설계하는 개발자는 커널이 인터럽트를 어떻게 처리하는지 잘 알아야 합니다. 커널 패닉이나 시스템이 느려지는 성능 문제가 인터럽트 동작과 연관된 경우가 많기 때문입니다.

[리눅스커널] 인터럽트: 리눅스 커널이 처리하는 인터럽트의 주요 개념 5. 인터럽트

앞에서 인터럽트에 대해 소개했으니 리눅스 커널에서 인터럽트를 처리하는 방식을 이해하기 위해 알아야 할 주요 개념을 소개합니다.

인터럽트 핸들러
인터럽트 벡터 
인터럽트 디스크립터 
인터럽트 컨텍스트

인터럽트 핸들러란?

인터럽트가 발생하면 이를 핸들링하기 위한 함수가 호출되는데 이를 인터럽트 핸들러라고 합니다. 예를 들어, 키보드를 타이핑해서 인터럽트가 발생하면 키보드 인터럽트를 처리하는 키보드 인터럽트 핸들러가 호출됩니다. 마찬가지로 휴대폰에서 화면을 손으로 만지면 터치 인터럽트가 발생하고 터치 인터럽트를 처리하는 터치 인터럽트 핸들러가 호출됩니다.

다음 그림을 보면서 각 디바이스별로 인터럽트 핸들러가 처리되는 과정을 알아보겠습니다.

그림 5.2 디바이스별로 실행되는 인터럽트 핸들러

그림 5.2에서 볼 수 있듯이 인터럽트 종류별로 인터럽트 핸들러가 있습니다. 인터럽트 핸들러는 함수 형태로 존재하며, 커널 내 인터럽트 함수에서 호출합니다. 이처럼 인터럽트가 발생해 지정한 인터럽트 핸들러가 동작하려면 어떻게 해야 할까요? request_irq() 함수를 적절한 인자와 함께 호출해서 미리 인터럽트 핸들러를 등록해야 합니다.

이해를 돕기 위해 컴퓨터에서 마우스를 움직였을 때 인터럽트를 처리하는 코드를 예로 들겠습니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/input/mouse/amimouse.c
01 static int amimouse_open(struct input_dev *dev)
02 {
03 unsigned short joy0dat;
...
04 error = request_irq(IRQ_AMIGA_VERTB, amimouse_interrupt, 0, "amimouse",
05     dev);

04 번째 줄을 보면 request_irq() 함수의 두 번째 인자로 인터럽트 핸들러 함수인 amimouse_interrupt()를 등록합니다.

이후 마우스 인터럽트가 발생하면 request_irq() 함수에서 지정한 amimouse_interrupt() 함수가 호출됩니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/input/mouse/amimouse.c
01 static irqreturn_t amimouse_interrupt(int irq, void *data)
02 {
03 struct input_dev *dev = data;
04 unsigned short joy0dat, potgor;
05 int nx, ny, dx, dy;
...
06 input_report_key(dev, BTN_LEFT,   ciaa.pra & 0x40);
07 input_report_key(dev, BTN_MIDDLE, potgor & 0x0100);
08 input_report_key(dev, BTN_RIGHT,  potgor & 0x0400);

인터럽트 핸들러에서는 마우스에서 입력한 데이터 정보를 참고해 유저 공간에 알리는 동작을 수행합니다.

코드는 복잡해 보이지만 다음 그림을 보면 인터럽트의 처리 과정을 쉽게 이해할 수 있습니다.


그림 5.3 마우스를 움직였을 때 마우스 인터럽트 핸들러를 호출하는 과정

마우스를 움직이면 마우스가 움직였다는 인터럽트가 발생해 인터럽트 벡터가 실행됩니다. 이후 커널 인터럽트 내부 함수에서 해당 인터럽트에 맞는 인터럽트 핸들러를 찾아 호출합니다. 많은 하드웨어 디바이스가 이 같은 방식으로 인터럽트를 통해 하드웨어의 변화를 알립니다.

인터럽트 컨텍스트는 언제 활성화될까?

인터럽트 컨텍스트는 현재 코드가 인터럽트를 처리 중이라는 뜻입니다. 인터럽트 컨텍스트에 대한 이해를 돕기 위해 먼저 소프트웨어 관점에서 인터럽트의 실행 흐름을 단계별로 보겠습니다.

1. 프로세스 실행 중
2. 인터럽트 벡터 실행
3. 커널 인터럽트 내부 함수 호출
4. 인터럽트 종류별로 인터럽트 핸들러 호출
  4.1 인터럽트 컨텍스트 시작
5. 인터럽트 핸들러의 서브루틴 실행 시작
6. 인터럽트 핸들러의 서브루틴 실행 마무리
  6.1 인터럽트 컨텍스트 마무리

복잡한 단계로 인터럽트가 처리되는 것 같아도 처리 과정을 요약하면 다음과 같습니다.

인터럽트가 발생하면 실행 중인 코드를 멈추고 인터럽트 벡터로 이동해 인터럽트에 대한 처리를 수행합니다.
인터럽트 종류별로 지정한 인터럽트 핸들러가 실행됩니다.

앞의 목록에서 4.1~6.1 사이에 호출된 함수는 인터럽트 컨텍스트에서 실행됐다고 할 수 있습니다. 여기서 한 가지 의문이 생깁니다. 현재 실행 중인 코드가 인터럽트 컨텍스트인지 어떻게 알 수 있을까요?

in_interrupt() 함수를 호출하면 현재 인터럽트 컨텍스트인지 알려줍니다. 이 함수가 true를 반환하면 현재 실행 중인 코드가 4.1~6.1 구간에 있다는 뜻입니다. 

인터럽트 디스크립터란?

인터럽트 종류별로 다음과 같은 인터럽트의 세부 속성을 관리하는 자료구조를 인터럽트 디스크립터라고 합니다.

인터럽트 핸들러
인터럽트 핸들러 매개변수 
논리적인 인터럽트 번호 
인터럽트 실행 횟수 

프로세스의 세부 속성을 표현하는 자료구조가 태스크 디스크립터이듯이 인터럽트에 대한 속성 정보를 저장하는 자료구조가 인터럽트 디스크립터인 것입니다. 커널 인터럽트의 세부 함수에서는 인터럽트 디스크립터에 접근해 인터럽트 종류별로 세부적인 처리를 수행합니다. 그림 5.4는 인터럽트가 발생했을 때 인터럽트 핸들러를 호출하는 흐름입니다.


 
그림 5.4 인터럽트 디스크립터로 인터럽트 핸들러를 호출하는 과정

커널 내부의 인터럽트 함수에서 인터럽트 종류별로 지정된 인터럽트 핸들러를 호출하려면 먼저 인터럽트 디스크립터에 접근해야 합니다. 인터럽트 디스크립터는 인터럽트 핸들러의 주소 정보를 갖고 있는데, 커널에서는 이를 읽어서 인터럽트 핸들러를 호출합니다.

인터럽트 디스크립터는 irq_desc 구조체이며 선언부는 다음과 같습니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/irqdesc.h
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;

참고로 이번 절에서 소개한 인터럽트의 주요 개념은 5.2절부터 상세히 살펴볼 예정입니다.


[리눅스커널] 프로세스: thread_info 구조체 초기화 코드 분석 4. 프로세스(Process) 관리

이전 절에서는 thread_info 구조체가 프로세스의 세부 동작 방식을 관리하는 내용을 살펴봤습니다. 이번에는 프로세스가 생성될 때 thread_info 구조체를 초기화하는 과정을 살펴보겠습니다. 이번 절에서는 다음 내용을 다룹니다.

프로세스가 생성될 때 스택 공간을 할당받는 동작 
태스크 디스크립터인 task_struct 구조체와의 관계

dup_task_struct() 함수 분석

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

dup_task_struct() 함수에서 호출하는 핵심 함수는 다음과 같습니다.

alloc_task_struct_node() 함수: 슬럽 할당자로 태스크 디스크립터 task_struct 구조체를 할당받음 
alloc_thread_stack_node() 함수: 프로세스 스택 공간을 할당받음

그런데 여기서 한 가지 의문이 생깁니다. thread_info 구조체를 초기화하는 과정을 알아보는 이유는 무엇일까요? 그 이유는 thread_info 구조체를 초기화하는 코드를 보면 thread_info 구조체와 해당 태스크 디스크립터가 어떻게 연결돼 있는지 알 수 있기 때문입니다.

dup_task_struct() 함수의 주요 코드를 보면서 세부 처리 방식을 살펴보겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
01 static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
02 {
03 struct task_struct *tsk;
04 unsigned long *stack;
05 struct vm_struct *stack_vm_area;
06 int err;
...
07 tsk = alloc_task_struct_node(node);
08 if (!tsk)
09 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);
...
16 return tsk;

먼저 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() 함수를 호출해서 스택 메모리 공간을 할당받습니다. 프로세스가 생성될 때 스택 공간을 할당받으며 그 크기는 정해져 있습니다. ARMv7 아키텍처 기반 리눅스 커널에서는 프로세스 스택의 크기가 0x2000입니다. 

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

다음으로 14번째 줄을 보겠습니다.

14 tsk->stack = stack;

task_struct 구조체의 task 필드에 할당받은 스택의 주소를 저장합니다. 할당받은 스택의 최상단 주소를 태스크 디스크립터에 설정하는 것입니다.

마지막으로 15번째 줄을 보겠습니다.

15 setup_thread_stack(tsk, orig);

setup_thread_stack() 함수를 호출해서 태스크 디스크립터 주소를 thread_info 구조체의 task 필드에 저장합니다. setup_thread_stack() 함수를 보면서 세부 동작 방식을 살펴봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched/task_stack.h
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 }

함수를 분석하기에 앞서 인자의 의미를 확인합시다.

struct task_struct *org: 부모 프로세스의 태스크 디스크립터
struct task_struct *p: 생성한 프로세스의 태스크 디스크립터

3 번째 줄에서는 부모 프로세스의 thread_info 구조체 필드를 자식 프로세스의 thread 구조체 공간에 복사합니다.

3 *task_thread_info(p) = *task_thread_info(org);

이로써 자식 프로세스는 부모 프로세스의 컨텍스트 정보를 공유 받습니다.

4번째 줄을 보겠습니다.

4 task_thread_info(p)->task = p;

thread_info 구조체의 task 필드에 태스크 디스크립터의 주소를 저장합니다. thread_info 구조체 선언부의 05 번째 줄을 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h
01 struct thread_info {
02 unsigned long flags; /* low level flags */
03 int preempt_count; /* 0 => preemptable, <0 => bug */
04 mm_segment_t addr_limit; /* address limit */
05 struct task_struct *task; /* main task structure */

thread_info 구조체 선언부의 05번째 줄을 보면 struct task_struct 타입인 포인터형 task 필드가 보입니다.

위 코드를 실행하면 프로세스의 태스크 디스크립터와 thread_info 구조체는 다음 그림과 같은 구조로 연결됩니다.

 
그림 4.22 thread_info 구조체와 task_struct 구조체의 관계

task_struct 구조체의 stack 필드는 프로세스 스택의 최상단 주소를 가리킵니다. 프로세스 스택의 최상단 주소에 있는 thread_info 구조체의 task 필드는 태스크 디스크립터의 주소를 가리킵니다. 그래서 프로세스 스택의 주소나 태스크 디스크립터의 주소만 알면 스택의 최상단 주소에 접근할 수 있습니다.

 
그림 4.23 스택 주소와 태스크 디스크립터 주소와의 관계

커널은 어셈블리 코드로 실행 중인 프로세스 스택 주소를 언제든지 알아낼 수 있습니다. 달리 보면 커널은 프로세스의 스택 최상단 주소와 태스크 디스크립터를 언제든 계산할 수 있습니다.

alloc_task_struct_node() 함수 분석 

이어서 프로세스를 생성하는 과정에서 태스크 디스크립터를 할당하는 alloc_task_struct_node() 함수를 살펴보겠습니다. 분석할 코드는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
01 static struct kmem_cache *task_struct_cachep;
02 
03 static inline struct task_struct *alloc_task_struct_node(int node)
04 {
05 return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
06 }

alloc_task_struct_node() 함수를 호출하면 task_struct_cachep라는 슬럽 캐시를 통해 task_struct 구조체를 할당합니다. 05번째 줄과 같이 kmem_cache_alloc_node() 함수를 호출해 지정한 슬럽 캐시에 대한 메모리를 할당받습니다.

task_struct_cachep라는 슬럽 캐시는 다음과 같은 기능을 수행합니다.

task_struct 구조체 크기만큼 메모리를 미리 확보해 놓고 대기
task_struct 구조체 할당 요청이 오면 이미 할당해 놓은 task_struct 구조체의 시작 주소를 반환


슬럽 캐시와 슬럽 오브젝트는 14.9절에서 상세히 분석합니다. 

alloc_thread_stack_node() 함수 분석

프로세스를 생성하는 과정에서 스택 메모리 공간을 할당하는 alloc_thread_stack_node() 함수를 살펴보겠습니다. 분석할 코드는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
01 static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node)
02 {
03 struct page *page = alloc_pages_node(node, THREADINFO_GFP,
04      THREAD_SIZE_ORDER);
05
06 return page ? page_address(page) : NULL;
07 }

03번째 줄은 페이지를 할당하는 동작입니다. THREAD_SIZE_ORDER 플래그가 1이니 2개의 페이지를 할당합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h
#define THREAD_SIZE_ORDER 1

---
리눅스에서 메모리를 관리하는 단위를 페이지(Page)라고 부르며, 커널의 '메모리 서브 시스템'에서 페이지를 관리합니다. 페이지의 크기는 일반적으로 4KB입니다.
---

다음은 06번째 줄입니다.

06 return page ? page_address(page) : NULL;

페이지를 할당한 다음에는 반드시 가상 주소로 변환해야 합니다. page_address() 함수를 호출해 페이지를 가상 주소로 바꾼 다음 이를 반환합니다. 페이지를 2개 할당했으니 0x2000 크기의 물리 메모리를 확보합니다.

따라서 32비트 ARMv7 아키텍처를 채용한 라즈비안에서는 프로세스의 스택 크기가 0x2000입니다.

이번 절에서 배운 내용을 정리하면 다음과 같습니다.

첫째, 태스크 디스크립터로 프로세스 스택의 최상단 주소는 어떻게 알 수 있을까?
task_struct 구조체의 stack 필드를 통해 프로세스 스택의 최상단 주소를 확인할 수 있습니다.

둘째, thread_info 구조체에서 태스크 디스크립터의 주소는 어떻게 알 수 있을까?
thread_info 구조체의 task 필드에 태스크 디스크립터 주소가 저장돼 있습니다.

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)





[리눅스커널] 프로세스: thread_info 구조체 - cpu 필드에 대한 상세 분석 4. 프로세스(Process) 관리

thread_info 구조체의 cpu 필드는 프로세스가 실행 중인 CPU 번호를 저장합니다. 그러면 현재 코드가 어떤 CPU에서 구동 중인지 알려면 어떤 함수를 써야 할까요? 커널에서 제공하는 smp_processor_id() 함수를 호출하면 됩니다. 

smp_processor_id() 함수 분석

smp_processor_id() 함수를 보면서 세부 동작 방식을 확인해보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/smp.h
1 # define smp_processor_id() raw_smp_processor_id()

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/smp.h
2 #define raw_smp_processor_id() (current_thread_info()->cpu)

선언부로 봐서 smp_processor_id() 함수는 매크로 타입의 함수임을 알 수 있습니다.

1~2번째 줄을 보면 smp_processor_id() 함수는 raw_smp_processor_id() 함수로 치환됩니다. 이어서 raw_smp_processor_id() 함수를 보면 current_thread_info()->cpu 코드로 치환된다는 사실을 알 수 있습니다.

위 선언부를 통해 알아본 smp_processor_id() 매크로 함수의 실체는 다음과 같습니다.

smp_processor_id() 
raw_smp_processor_id()
(current_thread_info()->cpu)

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

---
current_thread_info() 함수의 코드 분석은 4.10.2절에서 확인할 수 있습니다.
---

이번에는 smp_processor_id() 함수를 써서 현재 실행 중인 코드가 어떤 CPU에서 실행 중인지 식별하는 코드를 살펴보겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
01 void resched_curr(struct rq *rq)
02 {
03 struct task_struct *curr = rq->curr;
04 int cpu;
...
05 cpu = cpu_of(rq);
06 
07 if (cpu == smp_processor_id()) {
08 set_tsk_need_resched(curr);
09 set_preempt_need_resched();
10 return;
11 }

5번째 줄에서는 함수의 인자인 런큐 구조체에서 런큐 CPU 번호를 cpu 지역변수에 저장합니다. 7번째 줄에서는 smp_processor_id() 함수를 호출해서 현재 wake_up_idle_cpu() 함수가 몇 번 CPU에서 실행 중인지 확인합니다. 이때 현재 실행 중인 CPU 번호와 런큐 CPU 번호가 같으면 8~9번째 줄을 실행하고 함수 실행을 종료합니다.

참고로 67번째 줄의 매크로를 풀어서 실제 동작하는 코드로 바꾸면 아래 코드와 같습니다.
if (cpu == current_thread_info()->cpu) {

smp_processor_id() 함수의 전체 흐름 파악
커널의 다양한 코드에서 smp_processor_id() 함수를 호출해 현재 실행 중인 CPU 번호를 읽습니다. 이번에는 resched_curr() 함수에서 smp_processor_id() 함수를 호출했을 때의 실행 흐름을 확인해 봅시다.

 
그림 4.20 실행 중인 CPU 번호를 로딩하는 smp_processor() 함수의 실행 흐름


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


그림 4.20에서 볼 수 있듯이 resched_curr() 함수에서 smp_processor_id() 함수를 호출하면 실행 중인 프로세스 스택의 최상단 주소에 접근해서 cpu라는 필드를 읽어 옵니다.

set_task_cpu() 함수 분석

실행 중인CPU 번호를 저장하는 thread_info 구조체의 cpu 필드는 set_task_cpu() 함수가 호출될 때 변경됩니다. set_task_cpu() 함수를 보면서 세부 동작 방식을 분석해보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
void set_task_cpu(struct task_struct *p, unsigned int new_cpu)
{
...
__set_task_cpu(p, new_cpu);
}

set_task_cpu() 함수에서는 두 번째 인자인 new_cpu를 __set_task_cpu() 함수로 전달하며 호출합니다.

이어서 __set_task_cpu() 함수를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h
static inline void __set_task_cpu(struct task_struct *p, unsigned int cpu)
{
...
task_thread_info(p)->cpu = cpu;

task_thread_info() 함수는 태스크 디스크립터를 입력으로 받아 프로세스 스택의 최상단 주소를 반환합니다. thread_info 구조체의 cpu 필드에 매개변수로 전달된 cpu를 저장합니다.

정리하면 set_task_cpu() 함수를 실행하면 다음과 같은 동작을 수행합니다. 

프로세스 스택의 최상단 주소에 접근
thread_info 구조체의 cpu 필드에 실행 중인 cpu 인자의 값을 저장 

그림 4.21을 보면서 set_task_cpu() 함수의 실행 흐름을 확인해 보겠습니다.


 
그림 4.21 실행 중인 CPU 번호를 저장하는 set_task_cpu() 함수의 흐름

set_task_cpu() 함수를 호출하면 __set_task_cpu() 함수를 호출합니다. 이때 프로세스 스택의 최상단 주소에 접근해 thread_info 구조체의 cpu 필드에 cpu 번호를 저장합니다. 그림 4.21에서는 4를 CPU 번호로 지정합니다.

지금까지 thread_info 구조체의 세부 필드가 어떻게 바뀌는지 살펴봤습니다. 코드 분석을 토대로 태스크 디스크립터보다 thread_info 구조체가 프로세스 세부 동작을 저장하는 역할을 수행한다는 사실을 확인했습니다.

다음 절에서는 프로세스가 생성될 때 thread_info 구조체를 초기화하는 과정을 알아보겠습니다.

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)



[리눅스커널] thread_info 구조체의 preempt_count - 선점 스케줄링 여부 확인 4. 프로세스(Process) 관리

리눅스 커널의 핵심 동작은 스케줄링입니다. 프로세스는 schedule() 함수를 호출해서 명시적으로 스케줄링할 수도 있지만 인터럽트 처리 후 선점 스케줄링될 수 있습니다. 커널은 스케줄링 동작 중에 thread_info 구조체의 preempt_count 필드에 저장된 값을 보고 선점 스케줄링 실행 여부를 판단합니다.

그런데 여기서 한 가지 의문이 생깁니다. preempt_count 필드가 어떤 값일 때 선점 스케줄링을 시작할까요? preempt_count 필드가 0이면 프로세스가 선점될 수 있는 조건이라고 판단합니다. 그럼 선점 스케줄링 관련 코드를 보면서 세부 동작을 배워봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/entry-armv.S
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 코드로 표현하면 다음과 같습니다.

01    struct thread_info *current_thread_info = task_thread_info(current);
02
03   if (current_thread_info.preempt_count == 0 ) {
04    if (current_thread_info.flags & _TIF_NEED_RESCHED) {
04 svc_preempt();  // 스케줄링 실행
05       }
06   else {
06          current_thread_info.flags = 0;
06   }

변환된 C 코드의 1번째 줄에서는 프로세스 스택의 최상단 주소에 접근해 thread_info 구조체에 접근하며, 이를 current_thread_info라는 변수에 저장합니다.

03~04번째 줄에서는 다음 두 가지 조건을 검사합니다.

preempt_count 필드가 0인지 여부
flags 필드와 _TIF_NEED_RESCHED(0x2)와의 AND 연산을 수행해 연산 결과가 true인지 여부

두 조건을 모두 만족하면 svc_preempt() 함수를 호출해 선점 스케줄링을 실행합니다.

---
선점 스케줄링의 자세한 동작 방식은 10장 ‘프로세스 스케줄링’의 10.6절에서 상세히 설명합니다.
---

이번 절에서는 thread_info 구조체의 preempt_count 필드를 어떻게 처리하는 살펴봤습니다. 코드 분석으로 다음과 같은 내용을 알게 됐습니다.

인터럽트 컨텍스트 설정

시작: preempt_count += HARDIRQ_OFFSET;
종료: preempt_count -= HARDIRQ_OFFSET;

Soft IRQ 컨텍스트 설정

시작: preempt_count += SOFTIRQ_OFFSET;
종료: preempt_count -= SOFTIRQ_OFFSET;

프로세스 스케줄링 가능 여부

preempt_count가 0이면 선점 스케줄링되는 조건임
 
프로세스의 세부 실행 상태를 preempt_count 필드로 관리하므로 잘 알아둘 필요가 있습니다. 다음 절에서는 thread_info 구조체의 cpu 필드가 어떤 흐름으로 바뀌는지 살펴보겠습니다. 

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)




[리눅스커널] thread_info 구조체의 preempt_count - Soft IRQ 컨텍스트 실행 4. 프로세스(Process) 관리

Soft IRQ 컨텍스트의 시작 상태 저장

프로세스가 Soft IRQ 서비스를 처리 중이면 preempt_count 필드에 SOFTIRQ_OFFSET 매크로를 저장합니다. 다음 그림을 보면서 Soft IRQ 컨텍스트의 실행 시작을 설정하는 과정을 알아보겠습니다.

 
그림 4.18 Soft IRQ 컨텍스트 설정 시의 함수 흐름

그림 4.18에서 irq_exit() 함수가 보일 것입니다. 이 함수가 Soft IRQ 실행의 시작점입니다. 이처럼 Soft IRQ는 인터럽트 핸들링이 끝나면 실행을 시작합니다.

그렇다면 언제 Soft IRQ 컨텍스트를 설정할까요? 정답은 __do_softirq() 함수를 보면 알 수 있습니다. irq_exit() 함수에서 Soft IRQ 서비스 요청 여부를 체크한 다음 __do_softirq() 함수를 호출합니다. Soft IRQ 서비스를 실행하는 __do_softirq() 함수에서 프로세스 스택의 최상단 주소에 접근합니다. 

thread_info 구조체의 preempt_count 필드에 Soft IRQ 실행을 나타내는 SOFTIRQ_OFFSET(0x100) 매크로를 더합니다. __do_softirq() 함수의 코드를 보면서 이를 확인해 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c
01 asmlinkage __visible void __softirq_entry __do_softirq(void)
02 {
...
03 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
04 in_hardirq = lockdep_softirq_start();
05
06 restart:
07 /* Reset the pending bitmask before enabling irqs */
08 set_softirq_pending(0);
09
10 local_irq_enable();
11
12 h = softirq_vec;
13
14 while ((softirq_bit = ffs(pending))) {
15 unsigned int vec_nr;
16 int prev_count;
17
18 h += softirq_bit - 1;
19
20 vec_nr = h - softirq_vec;
21 prev_count = preempt_count();
22
23 kstat_incr_softirqs_this_cpu(vec_nr);
24
25 trace_softirq_entry(vec_nr);
26 h->action(h);
27 trace_softirq_exit(vec_nr);
...
28 }
...
29
30 lockdep_softirq_end(in_hardirq);
31 account_irq_exit_time(current);
32 __local_bh_enable(SOFTIRQ_OFFSET);

위 코드는 __do_softirq() 함수에서 중요한 부분을 요약한 것입니다. 먼저 03번째 줄을 봅시다. 

03 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);

thread_info 구조체의 preempt_count 필드에 접근해 다음과 같이 +SOFTIRQ_OFFSET 플래그만큼 증가합니다. 

preempt_count += SOFTIRQ_OFFSET(0x100)

06~28번째 줄은 Soft IRQ 서비스를 실행하는 동작입니다. 

마지막 32번째 줄 코드를 보겠습니다. 

32 __local_bh_enable(SOFTIRQ_OFFSET); 

thread_info 구조체의 preempt_count 필드에 접근해 -SOFTIRQ_OFFSET 플래그만큼 감소시킵니다. 

preempt_count -= SOFTIRQ_OFFSET(0x100)

이번에는 Soft IRQ 컨텍스트를 설정하는 __local_bh_disable_ip() 함수를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c
01 void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
02 {
...
03 __preempt_count_add(cnt);

03번째 줄에서 cnt 인자로 __preempt_count_add() 함수를 호출합니다. 

__preempt_count_add() 함수는 전달된 인자를 thread_info 구조체의 preempt_count 필드에 더합니다. 여기서 cnt는 SOFTIRQ_OFFSET 매크로를 저장하고 있으니 thread_info 구조체의 preempt_count는 다음 수식과 같이 바뀝니다. 참고로 thread_info 구조체는 실행 중인 프로세스 스택의 최상단 주소에 있다는 점을 기억하세요. 

preempt_count  += SOFTIRQ_OFFSET;

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

https://elixir.bootlin.com/linux/v4.19.30/source/mm/memcontrol.c
bool mem_cgroup_charge_skmem(struct mem_cgroup *memcg, unsigned int nr_pages)
{
01 gfp_t gfp_mask = GFP_KERNEL;
...
02 /* Don't block in the packet receive path */
03 if (in_softirq())
04 gfp_mask = GFP_NOWAIT;

03~04번째 줄을 보면 in_softirq() 함수가 true를 반환하면 04번째 줄 코드를 실행합니다. 이 코드는 Soft IRQ 컨텍스트에서 gfp_mask를 GFP_NOWAIT로 설정한다는 의미입니다.


in_softirq() 함수는 thread_info 구조체의 preempt_count 필드에 대해 다음과 같은 AND 연산을 수행합니다.

preempt_count &  0xFF00 (SOFTIRQ_MASK)

__do_softirq() 함수에서 Soft IRQ 서비스를 실행하기 전 __local_bh_disable_ip() 함수를 호출하면 thread_info 구조체의 preempt_count는 0x100이 됩니다.

Soft IRQ 서비스를 실행하는 중에 in_softirq() 함수를 실행하면 thread_info 구조체의 preempt_count 필드가 0x2100이므로 true를 반환합니다.

0x100 &  0xFF00 (SOFTIRQ_MASK) // true


Soft IRQ 컨텍스트의 종료 상태를 저장

이번에는 Soft IRQ 서비스 함수의 호출을 마친 다음에 어떤 동작을 하는지 알아보겠습니다. 그림 4.19는 처리 과정을 나타낸 것입니다.

 
 그림 4.19 Soft IRQ 컨텍스트 설정 해제 시의 함수 흐름

'Soft IRQ 컨텍스트'가 종료됐다는 정보는 __do_softirq() 함수에서 Soft IRQ 서비스 핸들러 함수를 호출한 후 설정합니다. 처리 과정은 다음과 같습니다.

1. 프로세스 스택 최상단 주소에 접근한다.
2. thread_info 구조체의 preempt_count 필드에서 SOFTIRQ_OFFSET(0x100) 비트를 뺀다.

'Soft IRQ 컨텍스트'가 종료됐다고 업데이트한 후 오른쪽의 ① 구간으로 표시된 함수에서 in_softirq() 함수를 호출하면 false를 반환합니다. 이는 __do_softirq() 함수에서 Soft IRQ 서비스 실행을 마무리하면 __local_bh_enable() 함수를 호출해 thread_info 구조체의 preempt_count 필드에서 SOFTIRQ_OFFSET(0x100)를 빼는 연산을 수행하기 때문입니다.

이어서 Soft IRQ 서비스 실행을 마치면 호출하는 __local_bh_enable() 함수를 분석해 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c
01 static void __local_bh_enable(unsigned int cnt)
02 {
...
03 __preempt_count_sub(cnt);
04 }

03번째 줄과 같이 __preempt_count_sub() 함수를 호출합니다. 이번에는 실행 중인 프로세스 스택의 최상단 주소에 접근한 후 thread_info 구조체의 preempt_count 필드에 -SOFTIRQ_OFFSET(0x100)만큼 감소시킵니다. 

preempt_count -= SOFTIRQ_OFFSET;


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)



[리눅스커널] thread_info 구조체의 preempt_count - 인터럽트 컨텍스트 실행 저장 4. 프로세스(Process) 관리

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

---
참고로 인터럽트는 외부 입출력 장치에 어떤 변화가 있을 때 발생하는 전기 신호 혹은 이를 CPU에게 알려서 처리하는 과정을 뜻합니다. 리눅스 커널은 인터럽트를 처리할 수 있는 함수를 지원하며, 자세한 내용은 5장의 5.1절에서 설명합니다.
---

thread_info 구조체의 preempt_count 필드에 인터럽트가 실행 중인 상태를 나타내는 비트를 설정합니다. 이 비트를 읽어 인터럽트 컨텍스트 유무를 식별합니다. 리눅스 커널에서는 현재 실행 중인 코드가 인터럽트 컨텍스트 내에 있는지 알려주는 in_interrupt() 함수를 제공합니다.

다음 그림을 보면서 인터럽트가 발생했을 때 어떤 흐름으로 thread_info 구조체의 preempt_count 필드가 바뀌는지 알아보겠습니다. 

 
그림 4.16 인터럽트 컨텍스트 설정 시의 함수 흐름

그림 4.16에서 인터럽트 컨텍스트를 설정하는 실행 흐름은 다음과 같습니다.

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 프로세스 스택 최상단 주소에 접근한 후 thread_info 구조체의 preempt_count 필드에 HARDIRQ_OFFSET(0x10000) 비트를 더함
5. 화살표 방향으로 함수를 계속 호출해서 인터럽트 핸들러 함수인 usb_hcd_irq() 함수를 호출
   5.1 서브루틴 함수를 실행

4.1단계 이후에 실행하는 모든 하위 함수에서 in_interrupt() 함수를 호출하면 true를 반환합니다. 예를 들면, 그림 4.16에서 irq_enter() 함수 이후로 호출되는 usb_hcd_irq() 함수에서 in_interrupt() 함수를 호출하면 true를 반환합니다.  

다음과 같이 if 문과 in_interrupt() 매크로를 함께 쓰면 실행 코드가 인터럽트 컨텍스트인지 확인할 수 있습니다.

if (in_interrupt()) {
// 인터럽트 컨텍스트에서 실행할  코드
}


in_interrupt() 함수는 thread_info 구조체의 preempt_count 필드에 대해 다음과 같은 AND 비트 연산을 수행합니다.

preempt_count &  0x001FFF00 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)

irq_enter() 함수를 실행하면 thread_info 구조체의 preempt_count 필드가 0x10000이니 true를 반환합니다.

0x10000 &  0x001FFF00 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK) // true


그럼 인터럽트 컨텍스트라는 정보가 업데이트되는 __irq_enter() 함수를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/hardirq.h
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번째 줄을 실행하면 thread_info 구조체의 preempt_count 필드에 HARDIRQ_OFFSET(0x10000) 비트를 더합니다.

preempt_count_add() 매크로 함수는 프로세스 스택의 최상단 주소에 접근해서 preempt_count 필드에 val인자를 더하는 동작을 수행합니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h
#define preempt_count_add(val) __preempt_count_add(val)

인터럽트 서비스 루틴의 실행을 시작할 때 thread_info 구조체의 preempt_count 필드에 HARDIRQ_OFFSET 비트를 더하게 됩니다.

인터럽트 컨텍스트의 종료 상태 저장

이번에는 인터럽트 서비스 루틴을 종료한 다음에 호출하는 irq_exit() 함수를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c
1 void irq_exit(void)
2 {
...
3 account_irq_exit_time(current);
4 preempt_count_sub(HARDIRQ_OFFSET);

4번째 줄을 실행해서 thread_info 구조체의 preempt_count 필드에 HARDIRQ_OFFSET 비트를 뺍니다.

인터럽트 핸들링을 마무리한 다음 irq_exit() 함수를 실행하는 코드의 흐름은 다음 그림과 같습니다.

 
그림 4.17 인터럽트 컨텍스트가 해제될 때의 함수 흐름

보다시피 인터럽트 핸들링을 끝냈으니 다시 검은색 화살 방향으로 함수가 호출됩니다. 이 과정에서 irq_exit() 함수를 실행해서 thread_info 구조체의 preempt_count 필드에서 HARDIRQ_OFFSET 비트를 뺍니다. 

이후 in_interrupt() 함수를 호출하면 false를 반환합니다. 그 이유는 irq_exit() 함수를 실행하면 thread_info 구조체의 preempt_count 필드가 0x0으로 바뀌기 때문입니다.

이를 연산하는 과정은 다음과 같습니다.

false = 0x0000 &  0x001FFF00 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)

irq_exit() 함수를 호출한 이후에는 리눅스 커널이 인터럽트 컨텍스트가 아니라고 판단하는 것입니다. 리눅스 커널에서 실행 중인 코드가 인터럽트 컨텍스트인지 점검하는 루틴은 자주 볼 수 있습니다. 

지금까지의 소스 분석으로 다음과 같은 내용을 알게 됐습니다.

인터럽트 컨텍스트의 실행 여부를 프로세스 스택의 최상단 주소에 있는 thread_info 구조체의 preempt_count 필드에 저장함


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)






[리눅스커널] 프로세스: thread_info 구조체란? 4. 프로세스(Process) 관리

지금까지 프로세스 속성을 관리하는 자료구조인 태스크 디스크립터를 배웠습니다. 커널에서는 태스크 디스크립터뿐만 아니라 thread_info 구조체로 프로세스 실행 동작을 관리합니다.

이번 절에서는 thread_info 구조체를 소개하고 각 필드의 의미를 살펴보겠습니다. 또한 thread_info 구조체 중 preempt_count 필드를 중심으로 프로세스의 실행 흐름을 분석하겠습니다.

thread_info 구조체란?

태스크 디스크립터는 프로세스의 공통 속성 정보를 저장하고 관리합니다. 추가로 커널에서는 프로세스의 세부 실행 정보를 저장하거나 로딩하는 자료구조가 필요한데 이를 thread_info 구조체에서 관리합니다.

thread_info 구조체는 다음과 같은 프로세스의 핵심 실행 정보를 저장합니다.

선점 스케줄링 실행 여부
시그널 전달 여부 
인터럽트 컨텍스트와 Soft IRQ 컨텍스트 상태
휴면 상태로 진입하기 직전 레지스터 세트를 로딩 및 백업

그럼 thread_info 구조체의 정체가 무엇인지 좀 더 자세히 알아보겠습니다.

먼저 thread_info 구조체는 어디에 있을까요? thread_info 구조체는 프로세스 스택에서 최상단 주소에 있습니다. 프로세스마다 자신만의 스택이 있으니 프로세스마다 1개의 thread_info 구조체가 있는 셈입니다.  

thread_info 구조체는 다음과 같은 프로세스의 세부 실행 정보를 저장합니다.

컨텍스트 정보
스케줄링 직전 실행했던 레지스터 세트
프로세스 세부 실행 정보 

그런데 위에서 언급한 동작은 CPU 아키텍처마다 구현 방식이 다릅니다. 즉, x86, ARMv7, ARMv8, PowerPC별로 구현 방식이 다릅니다. 그래서 CPU 아키텍처별로 프로세스의 세부 정보를 저장하는 자료구조인 thread_info 구조체가 필요한 것입니다.

다음 코드의 검색 결과를 보면서 아키텍처별 thread_info 구조체를 확인해 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/ident/thread_info
arch/alpha/include/asm/thread_info.h, line 15 (as a struct)
arch/arc/include/asm/thread_info.h, line 42 (as a struct)
arch/arm/include/asm/thread_info.h, line 49 (as a struct)
arch/arm64/include/asm/thread_info.h, line 39 (as a struct)
...
arch/x86/include/asm/thread_info.h, line 56 (as a struct)

다음은 x86 아키텍처의 thread_info 구조체 선언부입니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/x86/include/asm/thread_info.h
struct thread_info {
unsigned long flags; /* low level flags */
u32 status; /* thread synchronous flags */
};

thread_info 구조체의 필드가 2개밖에 없습니다.

다음은 ARMv7 아키텍처의 thread_info 구조체 선언부입니다.  

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h
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 */
...
};

라즈비안에서는 ARMv7 아키텍처에서 실행되므로 위 경로에 있는 thread_info 구조체로 프로세스의 세부 동작을 관리합니다.

이번에는 thread_info 구조체에 대해 정리해 봅시다. 먼저 태스크 디스크립터인 task_struct 구조체와 thread_info 구조체의 차이점은 무엇일까요?

태스크 디스크립터인 task_struct 구조체는 CPU 아키텍처에 독립적인 프로세스 관리용 속성을 저장합니다. 커널 버전이 같으면 x86이나 ARMv7에서 task_struct 구조체의 기본 필드는 같습니다. 하지만 thread_info 구조체는 CPU 아키텍처에 종속적인 프로세스의 세부 속성을 저장하므로 서로 다릅니다. 

thread_info 구조체에서 관리하는 커널의 핵심 동작은 무엇인가요? thread_info 구조체에서 관리하는 커널의 세부 동작은 다음과 같습니다.

현재 실행 중인 코드가 인터럽트 컨텍스트인지 여부 
현재 프로세스가 선점 가능한 조건인지 점검
프로세스가 시그널을 받았는지 여부 
컨텍스트 스케줄링 전후로 실행했던 레지스터 세트를 저장하거나 로딩

이번에는 thread_info 구조체를 소개했습니다. 이어서 thread_info 구조체의 세부 필드를 살펴보겠습니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)




[리눅스커널] 프로세스: 태스크 디스크립터(task_struct 구조체) - 프로세스 연결 리스트 4. 프로세스(Process) 관리

task_struct 구조체의 tasks 필드는 list_head 구조체로서 연결 리스트 타입입니다. 커널에서 구동 중인 모든 프로세스는 tasks 연결 리스트에 등록돼 있습니다. 그렇다면 프로세스의 태스크 디스크립터 tasks 연결 리스트 필드는 언제 init 프로세스의 태스크 디스크립터 tasks 연결 리스트에 등록될까요?

프로세스는 처음 생성될 때 init_task 전역변수 필드인 tasks 연결 리스트에 등록됩니다. 프로세스를 생성할 때 호출되는 copy_process() 함수를 보면서 처리 과정을 살펴보겠습니다.
 
https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
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 연결 리스트의 마지막 노드에 현재 프로세스의 task_struct 구조체의 tasks 주소를 등록한다.

자료구조를 변경하는 커널 코드를 읽으니 감이 잘 오지 않습니다. 이번에는 프로세스 태스크 디스크립터의 전체 구조에서 tasks 필드를 확인해 봅시다.

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 = 0xB1618A70,
8        (struct list_head *) [D:0xA1618334] prev = 0xA1A174E8),
9      (struct list_head *) [D: 0xA1A174CC] prev = 0xA7778330),

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

6번째 줄에서 init_task.tasks.next는 0xA1618330 주소를 가리키고 있습니다. 이 주소는 어떤 의미일까요? 바로 init_task.tasks 연결 리스트에 추가된 다음 프로세스의 task_struct 구조체의 tasks 필드 주소를 가리킵니다. 이 처리 흐름을 그림으로 표현하면 다음과 같습니다.


그림 4.14에서 [1]로 표시된 부분을 보면 태스크 디스크립터의 next 필드는 0xA1618310 주소를 가리킵니다. 여기서 중요한 질문을 던지겠습니다. 0xA1618310 주소의 정체는 무엇일까요? 바로 연결 리스트에 등록된 다음 프로세스 태스크 디스크립터의 next 필드 주소를 의미합니다. [1]에서 시작된 화살표 끝부분이 가리키는 박스를 보면 0xA1618310 주소가 보입니다. 이는 0xA1618000 주소에 있는 태스크 디스크립터의 tasks 필드의 주소인 것입니다.

이번에는 그림 4.14에서 [2]로 표시된 부분을 눈으로 따라가 보겠습니다. next 필드는 0xA16191B0 주소를 가리킵니다. 0xA16191B0 주소는 연결 리스트에 등록된 다음 프로세스 태스크 디스크립터의 next 필드 주소를 의미합니다. 모든 프로세스가 이렇게 연결 리스트로 등록돼 있습니다. 이 방식으로 커널에서 구동 중인 모든 프로세스의 태스크 디스크립터 주소를 알 수 있습니다. 

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)





[리눅스커널] 프로세스: 태스크 디스크립터(task_struct 구조체) - 프로세스 상태 4. 프로세스(Process) 관리

태스크 디스크립터에는 프로세스 상태를 관리하는 다음과 같은 두 가지 필드가 있습니다. 

state: 프로세스 실행 상태 
flags: 프로세스 세부 동작 상태와 속성 정보 

먼저 프로세스의 상태를 저장하는 state 필드를 소개합니다.

volatile long state;

프로세스 상태를 저장하는 필드로 다음 매크로 중 하나를 저장합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002

위 매크로에서 정의한 프로세스의 상태는 다음과 같습니다.

TASK_RUNNING: CPU에서 실행 중이거나 런큐에서 대기 상태에 있음
TASK_INTERRUPTIBLE: 휴면 상태 
TASK_UNINTERRUPTIBLE: 특정 조건에서 깨어나기 위해 휴면 상태로 진입한 상태

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

---
프로세스 상태별 세부 동작 방식은 10장 ‘프로세스 스케줄링’에서 상세히 다룹니다.
---

다음으로 flags 필드를 소개합니다.

unsigned int flags;

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

PF_* 매크로 가운데  눈여겨볼 만한 코드는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h
#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 */

각 매크로의 의미는 다음과 같습니다.

PF_IDLE: 아이들 프로세스
PF_EXITING: 프로세스 종료 중인 상태
PF_EXITPIDONE: 프로세스 종료를 마무리한 상태
PF_WQ_WORKER: 프로세스가 워커 스레드인 경우
PF_KTHREAD: 프로세스가 커널 스레드인 경우

flags 필드에 저장된 값으로 프로세스의 세부 실행 상태를 알 수 있습니다. 커널은 flags에 저장된 프로세스의 세부 동작 상태를 읽어서 프로세스를 제어합니다. 그래서 이번에는 flags 필드로 프로세스의 실행 흐름을 제어하는 한 가지 예를 들어 보겠습니다. 

https://elixir.bootlin.com/linux/v3.7/source/drivers/staging/android/lowmemorykiller.c
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 번째 줄을 봅시다.

10 if (tsk->flags & PF_KTHREAD)
11 continue;

10번째 줄에서는 태스크 디스크립터의 flags 필드와 PF_KTHREAD 매크로를 대상으로 AND 연산을 수행합니다. 이 조건을 만족하면 11번째 줄을 실행해 아래 부분의 코드를 실행하지 않습니다. 이 코드의 의도는 프로세스 타입이 커널 스레드이면 프로세스를 종료하지 않겠다는 것입니다. 이 같은 방식으로 프로세스 실행 정보를 읽어서 예외 처리를 수행합니다.

이어서 exit_state 필드를 확인해봅시다.

int exit_state;

프로세스 종료 상태를 저장합니다. 다음 매크로 중 하나 값을 저장합니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010
#define EXIT_ZOMBIE 0x0020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)

마지막으로 exit_code 필드를 확인해봅시다.

int exit_code;

프로세스의 종료 코드를 저장하는 필드입니다. do_exit() 함수의 3번째 줄에서 종료 코드를 저장합니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/exit.c
1 void __noreturn do_exit(long code)
2 {
...
3 tsk->exit_code = code; 

do_exit() 함수를 호출할 때 인자로 다음과 같은 시그널이나 프로세스를 종료하는 옵션을 지정할 수 있습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/mm/fault.c
01 static void
02 __do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
03   struct pt_regs *regs)
04 {
...
05 do_exit(SIGKILL);
06 }

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

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)




[리눅스커널] 프로세스: do_task_dead() 함수를 호출하고 난 후의 동작 4. 프로세스(Process) 관리

do_task_dead() 함수에서 __schedule() 함수를 호출하고 나면 커널은 어떻게 프로세스를 소멸시킬까요? 먼저 __schedule() 함수를 호출하면 어떤 흐름으로 finish_task_switch() 함수를 호출하는지 살펴보겠습니다. 다음은 이번 절에서 분석할 함수의 목록입니다.

__schedule() 함수
context_switch() 함수
finish_task_switch() 함수

위 함수들이 실행되면서 다음과 같은 과정으로 프로세스가 소멸됩니다.

종료할 프로세스는 do_exit() 함수에서 대부분 자신의 리소스를 커널에게 반납하고 자신의 상태를 TASK_DEAD로 바꾼다.
컨텍스트 스위칭을 한다.
컨텍스트 스위칭으로 다음에 실행하는 프로세스는 finish_task_switch() 함수에서 이전에 실행했던 프로세스 상태(종료할 프로세스)가 TASK_DEAD이면 프로세스 스택 공간을 해제한다.

조금 끔찍한 비유를 들면서 이 동작 방식을 설명하겠습니다. 전쟁 영화에서 '자살을 하기 두려운 병사가 다른 동료에게 자신을 죽여달라고 권총을 주는 장면'을 본 적이 있나요? 이와 조금 비슷한 상황인데, 프로세스가 스스로 자신의 스택 메모리 공간을 해제하지 못하니 컨텍스트 스위칭을 수행한 후 다음에 실행되는 프로세스에게 자신의 스택 메모리 공간을 해제하고 소멸시켜 달라고 부탁하는 것입니다. 

실행 흐름을 설명했으니 __schedule() 함수를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
01 static void __sched notrace __schedule(bool preempt)
02 {
...
03 if (likely(prev != next)) {
...
04 rq = context_switch(rq, prev, next, &rf);
05 } else {
06 rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
07 rq_unlock_irq(rq, &rf);
08 }
...
09 }

04번째 줄에서 context_switch() 함수를 호출해 컨텍스트 스위칭을 실행합니다.

다음으로 context_switch() 함수를 보겠습니다.

[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
01 static __always_inline struct rq *
02 context_switch(struct rq *rq, struct task_struct *prev,
03        struct task_struct *next, struct rq_flags *rf)
04 {
...
05 switch_to(prev, next, prev);
06 barrier();
07
08 return finish_task_switch(prev);
09 }

08 번째 줄에서는 finish_task_switch() 함수를 호출합니다. schedule() 함수를 호출하면 결국 finish_task_switch() 함수가 호출됩니다.

---
__schedule() 함수와 context_switch() 함수를 실행해 프로세스가 컨텍스트 스위칭이 이뤄지는 과정은 10장 '스케줄링'의 10.9절을 참고하세요.
---

__schedule() 함수에서 finish_task_switch() 함수까지 호출되는 흐름을 살펴봤으니 이번에는 finish_task_switch() 함수를 분석할 차례입니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
01 static struct rq *finish_task_switch(struct task_struct *prev)
02 __releases(rq->lock)
03 {
...
04 if (unlikely(prev_state == TASK_DEAD)) {
05 if (prev->sched_class->task_dead)
06 prev->sched_class->task_dead(prev);
07
08 kprobe_flush_task(prev);
09
10 put_task_stack(prev);
11
12 put_task_struct(prev);
13 }

04번째 줄은 이전에 실행했던 프로세스 상태가 TASK_DEAD일 때 05~12번째 줄을 실행하는 조건문입니다.

10번째 줄을 보겠습니다. put_task_stack() 함수를 호출해서 프로세스 스택 메모리 공간을 해제하고 커널 메모리 공간에 반환합니다.

이어서 12번째 줄에서는 put_task_struct() 함수를 실행해 프로세스를 표현하는 자료구조인 task_struct가 위치한 메모리를 해제합니다.

finish_task_switch() 함수에 ftrace 필터를 걸고 finish_task_switch() 함수가 호출되는 함수 흐름을 확인하면 다음과 같습니다.

01  TaskSchedulerSe-1803  [000] ....  2630.705218: do_exit+0x14/0xbe0 <-do_group_exit+0x50/0xe8
02  TaskSchedulerSe-1803  [000] ....  2630.705234: <stack trace>
03  => do_exit+0x18/0xbe0
04  => do_group_exit+0x50/0xe8
05  => get_signal+0x160/0x7dc
06  => do_signal+0x274/0x468
07  => do_work_pending+0xd4/0xec
08  => slow_work_pending+0xc/0x20
09  => 0x756df704
10 TaskSchedulerSe-1803  [000] dns.  2630.705438: sched_wakeup: comm=rcu_sched pid=10 prio=120 target_cpu=000
11 TaskSchedulerSe-1803  [000] dnh.  2630.705466: sched_wakeup: comm=jbd2/mmcblk0p2- pid=77 prio=120 target_cpu=000
12 TaskSchedulerSe-1803  [000] d...  2630.705479: sched_switch: prev_comm=TaskSchedulerSe prev_pid=1803 prev_prio=120 prev_state=R+ ==> next_comm=rcu_sched next_pid=10 next_prio=120
13       rcu_sched-10    [000] d...  2630.705483: finish_task_switch+0x14/0x230 <-__schedule+0x328/0x9b0
14       rcu_sched-10    [000] d...  2630.705504: <stack trace>
15 => finish_task_switch+0x18/0x230
16 => __schedule+0x328/0x9b0
17 => schedule+0x50/0xa8
18 => rcu_gp_kthread+0xdc/0x9fc
19 => kthread+0x140/0x170
20 => ret_from_fork+0x14/0x28
 
위 ftrace 메시지는 TaskSchedulerSe-1803 프로세스가 종료되는 과정을 담고 있습니다.

01~08번째 줄 메시지를 보겠습니다.

01  TaskSchedulerSe-1803  [000] ....  2630.705218: do_exit+0x14/0xbe0 <-do_group_exit+0x50/0xe8
02  TaskSchedulerSe-1803  [000] ....  2630.705234: <stack trace>
03  => do_exit+0x18/0xbe0
04  => do_group_exit+0x50/0xe8
05  => get_signal+0x160/0x7dc
06  => do_signal+0x274/0x468
07  => do_work_pending+0xd4/0xec
08  => slow_work_pending+0xc/0x20

TaskSchedulerSe-1803 프로세스가 종료 시그널을 받고 do_exit() 함수를 호출합니다.

다음 12번째 줄입니다.
12 TaskSchedulerSe-1803  [000] d...  2630.705479: sched_switch: prev_comm=TaskSchedulerSe prev_pid=1803 prev_prio=120 prev_state=R+ ==> next_comm=rcu_sched next_pid=10 next_prio=120

TaskSchedulerSe-1803 프로세스에서 pid가 10인 rcu_sched 프로세스로 스케줄링됩니다.

마지막으로 13~20번째 줄을 보겠습니다.

13       rcu_sched-10    [000] d...  2630.705483: finish_task_switch+0x14/0x230 <-__schedule+0x328/0x9b0
14       rcu_sched-10    [000] d...  2630.705504: <stack trace>
15 => finish_task_switch+0x18/0x230
16 => __schedule+0x328/0x9b0
17 => schedule+0x50/0xa8
18 => rcu_gp_kthread+0xdc/0x9fc
19 => kthread+0x140/0x170
20 => ret_from_fork+0x14/0x28

rcu_sched 프로세스는 finish_task_switch() 함수에서 TaskSchedulerSe-1803 프로세스의 마지막 리소스를 정리합니다. 

TaskSchedulerSe-1803 프로세스는 do_exit() 함수에서 태스크 디스크립터의 여러 필드를 해제했습니다. 그런데 do_exit() 함수를 TaskSchedulerSe-1803 프로세스의 스택 공간에서 실행 중이니 스스로 자신의 스택 공간을 해제할 수 없습니다. 따라서 스케줄링을 한 후 다음에 실행하는 프로세스인 rcu_sched가 종료되는 TaskSchedulerSe-1803 프로세스의 스택 메모리 공간을 해제하는 것입니다.

이번 절에서는 프로세스가 생성되고 종료되는 흐름을 살펴봤습니다. 이어서 프로세스를 표현하고 관리하는 프로세스 자료구조를 살펴보겠습니다.

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)








[리눅스커널] 프로세스: do_task_dead() 함수 분석 4. 프로세스(Process) 관리

이어서 do_task_dead() 함수를 분석하겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
01 void __noreturn do_task_dead(void)
02 {
03 set_special_state(TASK_DEAD);
04 current->flags |= PF_NOFREEZE;
05 
06 __schedule(false);
07 BUG();
08 for (;;)
09 cpu_relax();
10 }

03번째 줄에서 set_special_state() 함수를 호출해 프로세스 상태를 TASK_DEAD 플래그로 바꿉니다. 04번째 줄에서는 프로세스 태스크 디스크립터의 flags 필드에 PF_NOFREEZE 플래그를 OR 연산으로 적용합니다.

---
current는 현재 실행 중인 프로세스 태스크 디스크립터 자료구조로서 task_struct 구조체 타입입니다.
---

06번째 줄에서는 __schedule(false) 함수를 호출해 스케줄링을 요청합니다. __schedule() 함수에 false를 전달하는 이유는 선점 스케줄링을 실행하지 않겠다는 의미입니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)




[리눅스커널] 프로세스: 프로세스 종료 흐름 파악 4. 프로세스(Process) 관리

프로세스는 크게 두 가지 흐름으로 종료됩니다.

유저 애플리케이션에서 exit() 함수를 호출할 때
종료 시그널을 전달받을 때

먼저 프로세스가 종료되는 흐름을 알아보겠습니다.

커널에서 제공하는 do_exit() 함수를 실행하면 프로세스를 종료할 수 있습니다. do_exit() 함수에서 커널이 프로세스를 종료하는 코드를 분석하기에 앞서 do_exit() 함수가 어떤 흐름으로 호출되는지 살펴봅시다. 다음 그림은 프로세스가 종료되는 두 가지 흐름입니다.

 
그림 4.11 유저 프로세스가 종료될 때의 실행 흐름

exit() 시스템 콜 실행

그림 4.11의 왼쪽은 유저 공간에서 exit 시스템 콜을 발생시켰을 때 프로세스가 종료되는 흐름입니다. 보통 유저 프로세스가 정해진 시나리오에 따라 종료해야 할 때 exit() 함수를 호출합니다. 시스템 콜을 발생시킨 후 해당 시스템 콜 핸들러인 sys_group_exit() 함수를 호출합니다. 이후 do_exit() 함수를 호출합니다.

종료 시그널을 전달받았을 때

그림 4.11의 오른쪽은 POSIX kill 시그널을 받아 프로세스가 소멸하는 흐름입니다. 유저 프로세스뿐만 아니라 커널 프로세스도 커널 내부에서 종료 시그널을 받으면 소멸됩니다. 참고로 커널 함수로 send_signal() 함수를 호출하면 특정 프로세스 종료 시그널을 전달할 수 있습니다. 종료 시그널을 받은 프로세스는 do_exit() 함수를 실행해 종료됩니다.

do_exit() 함수로 커널이 프로세스를 종료시키는 세부 동작 못지 않게 프로세스가 종료되는 흐름을 파악하는 것이 중요합니다. 그 이유는 무엇일까요? 유저 애플리케이션 프로세스나 커널 프로세스가 예외 상황에서 의도하지 않게 종료해서 문제가 발생하는 경우가 있기 때문입니다. 

프로세스가 종료될 때 처리되는 흐름을 알면 이런 문제를 만났을 때 커널의 어느 함수부터 분석해야 할지 결정할 수 있습니다.

#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)








[리눅스커널] 프로세스: wake_up_new_task() 함수 분석 4. 프로세스(Process) 관리

프로세스 생성의 마지막 단계로 생성한 프로세스를 깨웁니다. 이 동작은 wake_up_new_task() 함수가 수행합니다. 

프로세스 상태를 TASK_RUNNING으로 변경
현재 실행 중인 CPU 번호를 thread_info 구조체의 cpu 필드에 저장
런큐에 프로세스를 큐잉

이번 절에서는 다음 wake_up_new_task() 함수를 분석하겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c
1 void wake_up_new_task(struct task_struct *p)
2 {
3 struct rq_flags rf;
4 struct rq *rq;
5
6 add_new_task_to_grp(p);
7 raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
8
9 p->state = TASK_RUNNING;
10 #ifdef CONFIG_SMP
11 __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0, 1));
12 #endif
13 rq = __task_rq_lock(p, &rf);
14 update_rq_clock(rq);
15 post_init_entity_util_avg(&p->se);
16
17 mark_task_starting(p);
18 activate_task(rq, p, ENQUEUE_NOCLOCK);


wake_up_new_task() 함수의 인자인 task_struct *p는 새롭게 생성된 프로세스의 태스크 디스크립터입니다.


9 번째 줄을 보겠습니다.

9 p->state = TASK_RUNNING;

프로세스 상태를 TASK_RUNNING으로 바꿉니다.

다음으로 11 번째 줄을 봅시다.

11 __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0, 1));

__set_task_cpu() 함수를 호출해 프로세스의 thread_info 구조체의 cpu 필드에 현재 실행 중인 CPU 번호를 저장합니다.

마지막으로 13~17 번째 줄을 봅시다.

13 rq = __task_rq_lock(p, &rf);
14 update_rq_clock(rq);
15 post_init_entity_util_avg(&p->se);
16
17 mark_task_starting(p);
18 activate_task(rq, p, ENQUEUE_NOCLOCK);

13번째 줄에서 런큐 주소를 읽은 다음, 18번째 줄에서 activate_task() 함수를 호출해 런큐에 새롭게 생성한 프로세스를 큐잉합니다. 그런데 여기서 생성한 프로세스를 바로 실행할 수 있을까요?

프로세스 스케줄러는 런큐에 대기 중인 프로세스와 우선순위를 계산해 새로 생성된 프로세스를 실행할지 결정합니다. 만약 실행을 기다리는 프로세스가 많거나 우선순위가 높은 프로세스가 이미 실행 중이면 새롭게 생성된 프로세스를 바로 실행할 수는 없습니다. 하지만 일반적인 상황에서 새로 생성된 프로세스는 바로 실행합니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)


[리눅스커널] 프로세스: copy_process() 함수 분석 4. 프로세스(Process) 관리

프로세스를 생성하는 핵심 동작은 copy_process() 함수에서 수행합니다. 대부분 부모 프로세스에 있는 리소스를 복사하는 동작입니다. 그럼 copy_process() 함수 코드를 본격적으로 분석해 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
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)
30 goto bad_fork_cleanup_fs;

먼저 14번째 줄을 봅시다.

14 p = dup_task_struct(current, node);
15 if (!p)
16 goto fork_out;

dup_task_struct() 함수는 생성할 프로세스의 태스크 디스크립터인 task_struct 구조체와 커널 프로세스 스택 공간을 할당합니다. 이후 task_struct 구조체 주소를 반환합니다. dup_task_struct() 함수를 호출해 태스크 디스크립터를 p에 저장합니다.

다음으로 18번째 줄을 보겠습니다.

18 retval = sched_fork(clone_flags, p);
19 if (retval)
20 goto bad_fork_cleanup_policy;

태스크 디스크립터를 나타내는 task_struct 구조체에서 스케줄링 관련 정보를 초기화합니다.

다음으로 22~27번째 줄을 분석하겠습니다.

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;

여기서는 프로세스의 파일 디스크립터 관련 내용(파일 디스크립터, 파일 디스크립터 테이블)을 초기화합니다. 부모 file_struct 구조체의 내용을 자식 프로세스에게 복사합니다. 만약 프로세스 생성 플래그 중 CLONE_FILES로 프로세스를 생성했을 경우 참조 카운트만 증가합니다.

다음으로 분석할 코드는 29번째 줄입니다.

28 retval = copy_sighand(clone_flags, p);
29 if (retval)
30 goto bad_fork_cleanup_fs;

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

이번 절까지 프로세스를 생성하는 함수를 살펴봤습니다. 이번 절에서는 copy_process() 함수를 실행해 부모 프로세스의 리소스를 새로 생성하는 프로세스의 task_struct 구조체에 복제하는 과정을 살펴봤습니다.

그런데 프로세스를 생성하는 과정은 여기서 끝일까요? 그렇지 않습니다. 생성한 프로세스를 바로 실행해야 합니다. 다음 절에서 이 내용을 다루겠습니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)




[리눅스커널] 프로세스: 커널 스레드는 어떻게 생성할까? 4. 프로세스(Process) 관리

이어서 커널 스레드를 생성하는 과정에서 호출되는 함수를 소개하고 세부 코드를 분석하겠습니다. 커널 스레드가 생성되는 과정은 크게 2단계로 나눌 수 있습니다.

1) 1단계: kthreadd 프로세스에게 커널 스레드 생성을 요청

kthread_create()
kthread_create_on_node()

2) 2단계: kthreadd 프로세스가 커널 스레드를 생성

kthreadd() 
create_kthread()

각 단계별로 실행되는 함수를 살펴보겠습니다.

1단계: kthreadd 프로세스에게 커널 스레드 생성 요청

유저 프로세스를 생성하려면 fork() 함수를 호출해야 하듯이, 커널 스레드를 생성하려면 kthread_create() 커널 함수를 호출해야 합니다. 먼저 kthreadd 프로세스에게 커널 스레드 생성 요청을 하는 함수 실행 흐름을 살펴보겠습니다(그림 4.6).

 
 그림 4.6 커널 스레드 생성 흐름도 1단계

이 그림에서 왼쪽 부분은 커널 스레드 생성을 kthreadd 프로세스에게 요청하는 동작입니다.

kthread_create() 함수 분석

커널 스레드를 생성하려면 먼저 kthread_create() 함수를 호출해야 합니다. 다음 코드는 kthread_create() 함수로서, 매크로 형태로 작성된 것을 알 수 있습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kthread.h
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    ...)

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

int (*threadfn)(void *data)
스레드 핸들 함수 주소를 저장하는 필드입니다. 커널 스레드의 세부 동작은 스레드 핸들 함수에 구현돼 있습니다.

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

int node
노드 정보입니다.

const char namefmt[]
커널 스레드 이름을 저장합니다.

kthread_create() 함수를 호출할 때 전달하는 인자를 알아봤습니다. 하지만 kthread_create() 함수 구현부만 봐서는 커널 스레드를 생성할 때 어떤 인자를 전달하는지 알기 어렵습니다. 이어서 커널 스레드를 생성하는 예제 코드를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/drivers/vhost/vhost.c
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() 함수의 첫 번째 인자로 vhost_worker 스레드 핸들 함수의 이름을 지정합니다.

7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

2번째 인자로 dev 변수를 지정하는데, 1번째 줄의 함수 인자를 보면 dev가 어떤 구조체인지 알 수 있습니다. vhost_dev 구조체의 주소를 2번째 인자로 전달하는 것입니다. 이 방식으로 전달하는 인자를 매개변수 혹은 디스크립터라고도 부릅니다. 

3번째 인자로 "vhost-%d"를 전달합니다. 이는 커널 스레드 이름을 나타냅니다.

---
이번에는 스레드 핸들 함수로 전달되는 매개변수를 점검해 봅시다. vhost_worker() 함수의 코드를 보겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/drivers/vhost/vhost.c
01 static int vhost_worker(void *data)
02 {
03 struct vhost_dev *dev = data;
04 struct vhost_work *work, *work_next;
05 struct llist_node *node;

vhost_dev_set_owner() 함수에서 kthread_create() 함수를 호출할 때 두 번째 인자로 vhost_dev 구조체인 dev를 지정했습니다. 이 주소가 스레드 핸들인 vhost_worker() 함수의 인자로 전달되는 것입니다. 

이처럼 vhost_worker() 스레드 핸들 함수의 매개변수로 인자를 전달합니다. 1번째 줄에서 보이는 void 타입의 data 포인터를 vhost_dev 구조체로 형변환(캐스팅)합니다.

커널에서는 이 같은 방식으로 스레드를 관리하는 구조체를 매개변수로 전달합니다.
---

kthread_create() 함수를 호출해 커널 스레드를 생성하는 예제를 봤으니 이어서 kthread_create() 함수의 구현부를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kthread.h
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

kthread_create() 함수는 선언부와 같이 매크로 타입입니다. 따라서 커널 컴파일 과정에서 전처리기는 kthread_create() 함수를 kthread_create_on_node() 함수로 바꿉니다. 즉, 커널이나 드라이버 코드에서 kthread_create() 함수를 호출하면 실제로 동작하는 코드는 kthread_create_on_node() 함수인 것입니다. 

kthread_create_on_node() 함수 분석

kthread_create() 매크로 함수의 실체인 kthread_create_on_node() 함수의 구현부를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
1 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
2    void *data, int node,
3    const char namefmt[],
4    ...)
5 {
6 struct task_struct *task;
7 va_list args;
8
9 va_start(args, namefmt);
10 task = __kthread_create_on_node(threadfn, data, node, namefmt, args);
11 va_end(args);
12
13 return task;
14}

함수 구현부를 봐도 특별한 일은 하지 않습니다. 다만 가변 인자를 통해 __kthread_create_on_node() 함수를 호출할 뿐입니다. 결국 __kthread_create_on_node() 함수에서 커널 스레드 생성 요청을 합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c  
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);

kmalloc() 함수를 호출해 kthread_create_info 구조체 크기만큼 동적 메모리를 할당받습니다.

다음 13~15번째 줄 코드를 봅시다.

13 create->threadfn = threadfn;
14 create->data = data;
15 create->node = node;

생성할 커널 스레드 핸들 함수와 매개변수 및 노드를 kthread_create_info 구조체의 필드에 저장합니다.

다음으로 19번째 줄 코드를 봅시다.

19 list_add_tail(&create->list, &kthread_create_list);
 
커널 스레드 생성 요청을 관리하는 kthread_create_list 연결 리스트에 &create->list를 추가합니다. kthreadadd 프로세스는 kthread_create_list 연결 리스트를 확인해 커널 스레드 생성 요청이 있었는지 확인합니다. 만약 kthread_create_list 연결 리스트가 비어 있지 않으면 누군가 커널 스레드 생성을 요청했으니 커널 스레드를 생성하는 일을 시작합니다.  

다음은 __kthread_create_on_node() 함수의 핵심 코드인 22번째 줄입니다.

22 wake_up_process(kthreadd_task);
 
kthreadd 프로세스의 태스크 디스크립터인 kthreadd_task를 인자로 삼아 wake_up_process() 함수를 호출해서 kthreadd 프로세스를 깨웁니다.

2단계: kthreadd 프로세스가 커널 스레드를 생성

kthreadd 프로세스를 깨우면 kthreadd 프로세스 스레드 핸들인 kthreadd() 함수가 실행을 시작합니다.

 
 그림 4.7 커널 스레드 생성 흐름: 2단계

먼저 이 그림에서 왼쪽 박스 부분을 눈으로 따라가 봅시다. 1단계에서 커널 스레드를 생성해 달라고 kthreadd 프로세스에게 요청했습니다. 그리고 kthreadd 프로세스를 깨웠습니다. 

다음으로 오른쪽 박스 부분을 보겠습니다. kthreadd 프로세스를 깨우면 kthreadd 프로세스 스레드 핸들인 kthreadd() 함수가 호출되어 프로세스를 생성합니다. 

kthreadd() 함수 분석

kthreadd 프로세스의 스레드 핸들인 kthreadd() 함수를 분석하겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
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 }

복잡해 보이는 kthreadd() 함수의 핵심 기능은 다음과 같습니다.

kthread_create_info 연결 리스트를 확인해 프로세스 생성 요청을 확인
create_kthread() 함수를 호출해 프로세스를 생성

kthreadd() 함수 분석을 시작하기 전에 한 가지 생각해 볼 점이 있습니다. kthreadd 프로세스를 깨우면 kthreadd() 함수가 실행한다고 했는데, 실제 kthreadd() 함수의 어느 코드가 실행할까요?

kthreadd 프로세스는 kthreadd() 함수 내에서 프로세스 생성 요청이 있었는지 kthread_create_list 연결 리스트를 지속적으로 점검합니다. 만약 kthreadd 프로세스가 요청한 프로세스 생성을 처리한 다음 더는 생성할 프로세스가 없을 때는 어느 코드를 실행할까요?

16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);

위와 같이 17번째 줄 코드를 실행합니다. schedule() 함수를 호출해 스스로 휴면 상태로 진입하는 동작입니다. 즉, 커널 스레드 생성 요청이 없으면 kthread_create_list 연결 리스트가 비게 되어 휴면하는 것입니다.

이후 커널 스레드 생성 요청을 받아 kthreadd 프로세스를 누군가 깨우면 18번째 줄 코드를 실행합니다. 그 이유는 schedule() 함수로 휴면 상태로 진입한 후 다음에 실행되는 코드이기 때문입니다.

kthreadd 프로세스가 깨어나면 실행되는 코드를 알아봤으니 이어서 함수 코드를 분석하겠습니다.

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 필드를 통해 kthread_create_info 구조체의 주소를 읽습니다.

연결 리스트 타입인 kthread_create_list 전역변수와 스레드 생성 정보를 나타내는 kthread_create_info 구조체의 관계는 다음과 같습니다.

 
그림 4.8 kthread_create_list 연결 리스트와 kthread_create_info 구조체의 관계

kthread_create_info 구조체를 보면서 위 그림을 설명하겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
01 struct kthread_create_info
02 {
03 int (*threadfn)(void *data);
04 void *data;
05 int node;
06
07 struct task_struct *result;
08 struct completion *done;
09
10 struct list_head list;
11 };

kthread_create_info 구조체의 마지막 필드는 list이며 struct list_head 타입입니다. 그런데 kthread_create_list 전역변수의 next 필드가 kthread_create_info 구조체의 list 필드 주소를 가리킵니다. 그림 4.8의 맨 왼쪽 부분에 있는 kthread_create_list의 next 전역변수는 화살표로 kthread_create_info list 구조체의 필드를 가리키는 것입니다.

kthread_create_info 구조체에서 list 필드의 오프셋을 계산해 kthread_create_info 구조체의 시작 주소를 알 수 있습니다.  

29번째 줄 코드를 봅시다.

29 create_kthread(create);

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

create_kthread() 함수 분석

이제 create_kthread() 함수를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
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 매크로를 OR 연산한 결과를 3번째 인자로 설정합니다.

kernel_thread() 함수 분석

이번에는 kernel_thread() 함수를 분석하겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
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() 함수를 호출해 프로세스를 생성하는 일을 시작합니다. 코드 분석을 통해 커널 스레드도 프로세스의 한 종류인 것을 알 수 있습니다.

지금까지 커널 스레드를 생성하는 과정을 코드 분석을 통해 알아봤습니다. 배운 내용을 정리해 봅시다.

첫째, 커널 스레드를 생성하려면 어떤 함수를 호출해야 할까?
kthread_create() 함수를 호출해야 합니다. 함수의 인자로 '스레드 핸들 함수', '매개변수', '커널 스레드 이름'을 지정해야 합니다.

둘째, 커널 스레드를 생성하는 단계는 무엇일까?
kthreadd 프로세스에게 커널 스레드 생성을 요청한 후 kthreadd 프로세스를 깨웁니다. kthreadd 프로세스는 깨어나 자신에게 커널 스레드 생성 요청이 있었는지 확인한 후 만약 생성 요청이 있다면 커널 스레드를 생성합니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)



[리눅스커널] 프로세스: 커널 스레드란? 4. 프로세스(Process) 관리

이전 절까지 유저 영역에서 실행한 프로세스가 어떤 과정으로 생성되고 종료되는지 배웠습니다. 이번 절에서는 커널 공간에서만 실행되는 프로세스인 커널 스레드에 대해 소개하고 생성 과정을 살펴보겠습니다. 

커널 스레드란?

커널 프로세스는 커널 공간에서만 실행되는 프로세스입니다. 대부분 커널 스레드 형태로 동작합니다. 커널 스레드는 리눅스 시스템 프로그래밍에서 데몬과 비슷한 일을 하는데, 데몬과 커널 스레드는 백그라운드 작업으로 실행되면서 시스템 메모리나 전원을 제어하는 동작을 수행합니다. 그런데 커널 스레드는 유저 영역과 시스템 콜을 받지 않고 동작합니다. 이 점이 데몬과 커널 스레드의 차이점입니다.

커널 스레드는 다음과 같은 세 가지 특징이 있습니다.

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

커널 스레드를 소개했으니 이번에는 커널 스레드의 종류를 알아봅시다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)













[리눅스커널] 프로세스: ftrace에서 프로세스 확인하기 4. 프로세스(Process) 관리

ftrace를 열어보면 다양한 이벤트 메시지를 볼 수 있습니다. 공통적으로 모든 ftrace 메시지의 맨 왼쪽 부분에서 프로세스 정보를 볼 수 있습니다.

먼저 다음 ftrace 로그를 보면서 프로세스의 정체를 확인해 봅시다.

chromium-browse-1436  [002] d...  9445.131875: sched_switch: prev_comm=chromium-browse prev_pid=1436 prev_prio=120 prev_state=S ==> next_comm=kworker/2:3 next_pid=1454 next_prio=120

위 ftrace는 sched_switch 이벤트의 포맷 로그입니다. 
 
ftrace 메시지에서 맨 왼쪽 부분에 ftrace를 출력하는 프로세스 정보를 볼 수 있습니다. 포맷은 “프로세스이름-[pid]” 형식입니다. 위에서 보이는 ftrace 메시지의 왼쪽 부분 정보는 pid가 1436인 chromium-browse 프로세스가 실행 중이라는 사실을 말해줍니다.

이번 절에서는 리눅스 터미널에서 ps 명령어를 입력해 프로세스가 무엇인지 설명했습니다. 또한 ftrace 메시지로 프로세스 정보를 확인했습니다. 이어지는 절에서는 프로세스를 생성하는 과정을 배워보겠습니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)









[리눅스커널] 프로세스: 스레드란? 4. 프로세스(Process) 관리

스레드(thread)란 무엇일까요? 스레드는 유저 레벨에서 생성된 가벼운 프로세스라 할 수 있습니다. 스레드는 일반 프로세스에 비해 컨텍스트 스위칭을 수행할 때 시간이 적게 걸립니다. 그 이유는 스레드는 자신이 속한 프로세스 내의 다른 스레드와 파일 디스크립터, 파일 및 시그널 정보에 대한 주소 공간을 공유하기 때문입니다. 프로세스가 자신만의 주소 공간을 갖는 것과 달리 스레드는 스레드 그룹 안의 다른 스레드와 주소 공간을 공유합니다.

하지만 커널 입장에서는 스레드를 다른 프로세스와 동등하게 관리합니다. 대신 각 프로세스 식별자인 태스크 디스크립터(task_struct)에서 스레드 그룹 여부를 점검할 뿐입니다.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

[리눅스커널] 프로세스: 태스크란 4. 프로세스(Process) 관리

제가 처음 리눅스 커널 코드를 볼 때 가장 궁금했던 점은 “리눅스 커널 함수의 이름에 ‘task’가 왜 보일까?”라는 것이었습니다. 사실 태스크는 리눅스 외의 다른 운영체제에서 예전부터 많이 쓰던 용어입니다. 운영체제 이론을 다루는 책에서는 태스크라는 단어를 많이 볼 수 있습니다.

태스크는 운영체제에서 어떤 의미일까요? 말 그대로 실행(Execution)이라 말할 수 있습니다.
운영체제 책을 보면 첫 장에서 태스크에 대한 설명을 볼 수 있습니다. 예전에는 특정 코드나 프로그램 실행을 일괄 처리했습니다. 이러한 실행 및 작업 단위를 태스크라고 불렀습니다.

임베디드 개발자는 태스크의 개수와 이름을 지정했고 시스템이 구동되는 동안 태스크는 바뀌지 않았습니다. 예를 들면 화면이 없는 간단한 시나리오로 구동하는 임베디드 시스템에서는 태스크 2개가 서로 시그널을 주고받으며 시스템 전체를 제어했습니다.

시간이 흘러 기존 임베디드 개발자들이 리눅스를 사용하는 임베디드 프로젝트로 유입됐습니다. 기존의 태스크라는 용어에 익숙한 임베디드 개발자들이 리눅스에서도 썼던 '태스크'라는 용어를 사용했습니다. '프로그램을 실행하는 단위'라고 생각했던 태스크의 개념은 프로세스와 겹치는 부분이 많기 때문입니다.

---
예전에 쓰던 용어를 현재 소프트웨어에 그대로 쓰는 경우가 많습니다. 이를 레거시(Legacy)라고 표현하는데, 여기에는 과거의 유물이라는 뜻도 있습니다.
---

그래서 예전에 썼던 태스크라는 용어를 리눅스 커널 소스코드에서 그대로 쓰고 있습니다. 대표적인 예로 프로세스 속성을 표시하는 구조체의 이름을 process_struct 대신 task_struct으로 쓰고 있습니다. 이처럼 프로세스마다 속성을 표현하는 task_struct 구조체를 태스크 디스크립터 혹은 프로세스 디스크립터라고도 합니다.

이처럼 리눅스 커널의 함수 이름이나 변수 중에 task란 단어가 보이면 프로세스 관련 코드라 생각해도 좋습니다. 예를 들어, 다음 함수는 모두 프로세스를 관리 및 제어하는 역할을 수행하며, 함수 이름에 보이는 task는 process로 바꿔도 무방합니다.

dump_task_regs()
get_task_mm()
get_task_pid()
idle_task()
task_tick_stop()

그러면 이번 절에서 배운 내용을 간단히 정리해 봅시다.

리눅스 커널에서 태스크는 프로세스와 같은 개념으로 쓰는 용어다. 
소스코드나 프로세스에 대한 설명을 읽을 때 태스크란 단어를 보면 프로세스와 같은 개념으로 이해하자.


#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)



[리눅스커널] 인터럽트: 'Unbalanced enable for IRQ' 메시지에 대해서 5. 인터럽트

프로젝트를 개발하다 보면 다음과 같은 WARN() 메시지를 커널 로그로 볼 수 있다.

Unbalanced enable for IRQ 23
------------[ cut here ]------------
WARNING: at kernel/irq/manage.c:437
:
NIP [c00000000016de8c] .__enable_irq+0x11c/0x140
LR [c00000000016de88] .__enable_irq+0x118/0x140
Call Trace:
[c000003ea1f23880] [c00000000016de88] .__enable_irq+0x118/0x140 (unreliable)
[c000003ea1f23910] [c00000000016df08] .enable_irq+0x58/0xa0
[c000003ea1f239a0] [c0000000000388b4] .eeh_enable_irq+0xc4/0xe0
[c000003ea1f23a30] [c000000000038a28] .eeh_report_reset+0x78/0x130
[c000003ea1f23ac0] [c000000000037508] .eeh_pe_dev_traverse+0x98/0x170
[c000003ea1f23b60] [c0000000000391ac] .eeh_handle_normal_event+0x2fc/0x3d0
[c000003ea1f23bf0] [c000000000039538] .eeh_handle_event+0x2b8/0x2c0
[c000003ea1f23c90] [c000000000039600] .eeh_event_handler+0xc0/0x170
[c000003ea1f23d30] [c0000000000da9a0] .kthread+0xf0/0x100
[c000003ea1f23e30] [c00000000000a1dc] .ret_from_kernel_thread+0x5c/0x80

"관련 이 로그를 왜 출력될까?"란 의문이 생깁니다. 이 의문을 풀어볼까요?

irq_desc 구조체의 depth 필드 확인

이 의문을 풀기 위해서 먼저 인터럽트 디스크립터의 구조체를 확인할 필요가 있습니다.
다음은 irq_desc 구조체의 구현부입니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqdesc.h
01 struct irq_desc {
02 struct irq_common_data irq_common_data;
03 struct irq_data irq_data;
04 unsigned int __percpu *kstat_irqs;
...
05 struct irqaction *action; /* IRQ action list */
06 unsigned int status_use_accessors;
07 unsigned int core_internal_state__do_not_mess_with_it;
08 unsigned int depth; /* nested irq disables */

08번째 줄을 보면 depth 필드가 있고 주석으로 '/* nested irq disables */' 메시지가 보인다.

depth 필드는 인터럽트의 상태를 다음과 같이 나타낸다.

   * 'depth = 0' : __enable_irq() 함수를 호출해 인터럽트를 활성화
   * 'depth > 0' 이면: __disable_irq() 함수를 호출해 인터럽트를 활성화

간단히 말하면 'depth가 0이면 인터럽트가 활성화, depth가 1이면 인터럽트가 비활성화'라고 요약할 수 있다.

TRACE32로 디버깅 하기

이번에는 인터럽트 디스크립터의 depth 필드를 TRACE32로 확인해 보자.

01  (struct irq_desc *) (struct irq_desc*)0xcd520940 = 0xCD520940 -> (
02    (struct irq_common_data) irq_common_data = ((unsigned int) state_use_accessors = 0x03402204, (vo
03    (struct irq_data) irq_data = ((u32) mask = 0x0, (unsigned int) irq = 0x2A, (long unsigned int) h
04    (unsigned int *) kstat_irqs = 0xDE36A7F8,
05    (irq_flow_handler_t) handle_irq = 0xC016E208,
06    (struct irqaction *) action = 0xCD15B680,
07    (unsigned int) status_use_accessors = 0x0104,
08    (unsigned int) core_internal_state__do_not_mess_with_it = 0x20,
09    (unsigned int) depth = 0x0,
 
09번째 줄을 보니 depth가 0이다.


__enable_irq() 함수 분석

이어서 __enable_irq() 함수의 구현부를 보자.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/manage.c
01 void __enable_irq(struct irq_desc *desc)
02 {
03 switch (desc->depth) {
04 case 0:
05 err_out:
06 WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n",
07      irq_desc_get_irq(desc));
08 break;
09 case 1: {
10 if (desc->istate & IRQS_SUSPENDED)
11 goto err_out;
12 /* Prevent probing on this irq: */
13 irq_settings_set_noprobe(desc);
14 irq_startup(desc, IRQ_RESEND, IRQ_START_FORCE);
15 break;
16 }
17 default:
18 desc->depth--;
19 }
20 }

03~07번째 줄을 보자.

03 switch (desc->depth) {
04 case 0:
05 err_out:
06 WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n",
07      irq_desc_get_irq(desc));
08 break;

desc->depth가 0이면 이미 인터럽트를 활성화한 조건으로 보고 06~07번째 줄과 같이 WARN() 매크로 함수를 실행한다.

해결 패치 

'Unbalanced enable for IRQ' 메시지가 출력되는 문제를 해결하기 위한 다양한 패치를 생각해볼 수 있다.
그 중 하나를 보자.

https://patchwork.ozlabs.org/patch/259995/
diff --git a/arch/powerpc/kernel/eeh_driver.c b/arch/powerpc/kernel/eeh_driver.c
index 3fee021..72c4a17 100644
--- a/arch/powerpc/kernel/eeh_driver.c
+++ b/arch/powerpc/kernel/eeh_driver.c
@@ -143,10 +143,14 @@  static void eeh_disable_irq(struct pci_dev *dev)
 static void eeh_enable_irq(struct pci_dev *dev)
 {
  struct eeh_dev *edev = pci_dev_to_eeh_dev(dev);
+ struct irq_desc *desc;
 
  if ((edev->mode) & EEH_DEV_IRQ_DISABLED) {
  edev->mode &= ~EEH_DEV_IRQ_DISABLED;
- enable_irq(dev->irq);
+
+ desc = irq_to_desc(dev->irq);
+ if (desc && desc->depth > 0)
+ enable_irq(dev->irq);
  }
 }

'desc->depth > 0' 구문을 보면 인터럽트 디스크립터의 depth 필드가 0보다 크면, 즉 인터럽트가 비활성화됐다면,
enable_irq() 함수를 호출해 인터럽트를 활성화하는 패치이다.

[리눅스커널] 딜레이 워크: 패치 코드 내용과 작성 방법 알아보기 7. 워크큐(Workqueue)

이번 절에서는 딜레이 워크를 만들어보는 실습을 진행합니다. 실습할 패치 코드 시나리오는 다음과 같습니다.

인터럽트 핸들러에서 워크를 큐잉
워크 핸들러 실행 
워크 핸들러에서 딜레이 워크를 실행

이번 장에서 배운 내용을 총복습하는 실습입니다. 먼저 패치 코드를 작성해 봅시다.

패치 코드의 내용과 작성 방법

먼저 패치 코드를 소개하겠습니다.

diff --git a/drivers/mailbox/bcm2835-mailbox.c b/drivers/mailbox/bcm2835-mailbox.c
--- a/drivers/mailbox/bcm2835-mailbox.c
+++ b/drivers/mailbox/bcm2835-mailbox.c
@@ -33,11 +33,23 @@
1  #include <linux/of_irq.h>
2  #include <linux/platform_device.h>
3  #include <linux/spinlock.h>
4 +#include <linux/workqueue.h>
5 +#include <linux/kernel_stat.h>
7  /* Mailboxes */
8  #define ARM_0_MAIL0 0x00
9  #define ARM_0_MAIL1 0x20
10 
11 +#define BCM_MAILBOX_IRQ_NUM 23
12 +#define BCM2835_DELAYED_WORK_TIME  msecs_to_jiffies(100)
13 +#define DELAYED_WORK_PERIOD 10
14 +
15 +struct work_struct bcm2835_mbox_work;
16 +struct delayed_work bcm2835_mbox_delayed_work;
17 +
18 +unsigned long work_execute_times = 0;
19 +
20 /*
21  * Mailbox registers. We basically only support mailbox 0 & 1. We
22  * deliver to the VC in mailbox 1, it delivers to us in mailbox 0. See
23 @@ -72,6 +84,51 @@ static struct bcm2835_mbox *bcm2835_link_mbox(struct mbox_chan 24 *link)
25 return container_of(link->mbox, struct bcm2835_mbox, controller);
26  }
27
28 +
29 +void bcm2835_mbox_delayed_work_callback(struct work_struct *ignored)
30 +{
31 + unsigned long flags = 0;
32 + int i;
33 +
34 + struct irq_desc *desc;
35 +
36 + desc = irq_to_desc(BCM_MAILBOX_IRQ_NUM);
37 + if (!desc)
38 + return;
39 +
40 + raw_spin_lock_irqsave(&desc->lock, flags);
41 +
42 + trace_printk("IRQ23 trigger times \n");
43 + for_each_online_cpu(i)
44 + trace_printk("%10u ", kstat_irqs_cpu(BCM_MAILBOX_IRQ_NUM, i));
45 +
46 + raw_spin_unlock_irqrestore(&desc->lock, flags);
47 +
48 + dump_stack();
49 +}
50 +
51 +void bcm2835_mbox_work_callback(struct work_struct *ignored)
52 +{
53 + void *stack;
54 + struct thread_info *current_thread;
55 +
56 + stack = current->stack;
57 + current_thread = (struct thread_info*)stack;
58 +
59 + trace_printk("current process: %s\n", current->comm);
60 + trace_printk("[+] in_interrupt:0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n",
61 + (unsigned int)in_interrupt(), (unsigned int)current_thread-62 >preempt_count, (long unsigned int)stack);
62 +
63 + dump_stack();
64 +
65 + work_execute_times ++;
66 + if (work_execute_times % DELAYED_WORK_PERIOD) {
67 + schedule_delayed_work(&bcm2835_mbox_delayed_work, BCM2835_DELAYED_WORK_TIME);
68 +
69 + }
70 +}
71 +
72 static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)
73 {
74 struct bcm2835_mbox *mbox = dev_id;
75 @@ -83,6 +140,11 @@ static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)
76 dev_dbg(dev, "Reply 0x%08X\n", msg);
77 mbox_chan_received_data(link, &msg);
78 }
79 +
80 + schedule_work(&bcm2835_mbox_work);
81 +
82 return IRQ_HANDLED;
83 }
84
85 @@ -153,6 +215,9 @@ static int bcm2835_mbox_probe(struct platform_device *pdev)
86 if (mbox == NULL)
87 return -ENOMEM;
88 spin_lock_init(&mbox->lock);
89 +
90 + INIT_WORK(&bcm2835_mbox_work, bcm2835_mbox_work_callback);
91 + INIT_DELAYED_WORK(&bcm2835_mbox_delayed_work,
bcm2835_mbox_delayed_work_callback);
92 +
93 ret = devm_request_irq(dev, platform_get_irq(pdev, 0),
94        bcm2835_mbox_irq, 0, dev_name(dev), mbox);

먼저 패치 코드를 입력하는 방법을 알아보고 패치 코드 분석에 들어가겠습니다.

우선 워크와 딜레이 워크를 초기화하는 코드를 입력하는 방법을 알아봅시다. 이해를 돕기 위해 원본 bcm2835_mbox_probe() 함수를 소개합니다.

1 static int bcm2835_mbox_probe(struct platform_device *pdev)
2 {
3 struct device *dev = &pdev->dev;
4 int ret = 0;
5 struct resource *iomem;
6 struct bcm2835_mbox *mbox;
7
8 mbox = devm_kzalloc(dev, sizeof(*mbox), GFP_KERNEL);
9 if (mbox == NULL)
10 return -ENOMEM;
11 spin_lock_init(&mbox->lock);
12
13 ret = devm_request_irq(dev, platform_get_irq(pdev, 0),
14        bcm2835_mbox_irq, 0, dev_name(dev), mbox);

여기서 12~13번째 줄 사이에 다음 코드를 작성합시다.
+
+ INIT_WORK(&bcm2835_mbox_work, bcm2835_mbox_work_callback);
+ INIT_DELAYED_WORK(&bcm2835_mbox_delayed_work, bcm2835_mbox_delayed_work_callback);
+

워크 핸들러는 bcm2835_mbox_work_callback() 함수이고, 딜레이 워크 핸들러는 bcm2835_mbox_delayed_work_callback() 함수입니다.


여기서 주의해야 할 점이 있습니다. 워크나 딜레이 워크의 초기화 코드는 인터럽트 핸들러를 설정하는 코드 앞에 작성하는 것이 좋습니다. 그 이유는 인터럽트 핸들러를 설정한 후 바로 인터럽트가 발생해서 인터럽트 핸들러에서 schedule_work() 함수를 호출하면 커널 크래시가 발생할 수 있기 때문입니다. 즉, 워크를 초기화하기 전에 인터럽트 핸들러에서 schedule_work() 함수가 실행되어 워크를 실행하기 때문입니다.


다음으로 워크 핸들러와 딜레이 워크 핸들러 함수를 입력하는 방법을 알아봅시다. 다음 코드를 bcm2835_mbox_irq() 함수의 윗부분에 입력합시다.

28 +
29 +void bcm2835_mbox_delayed_work_callback(struct work_struct *ignored)
30 +{
31 + unsigned long flags = 0;
32 + int i;
33 +
34 + struct irq_desc *desc;
35 +
36 + desc = irq_to_desc(BCM_MAILBOX_IRQ_NUM);
37 + if (!desc)
38 + return;
39 +
40 + raw_spin_lock_irqsave(&desc->lock, flags);
41 +
42 + trace_printk("IRQ23 trigger times \n");
43 + for_each_online_cpu(i)
44 + trace_printk("%10u ", kstat_irqs_cpu(BCM_MAILBOX_IRQ_NUM, i));
45 +
46 + raw_spin_unlock_irqrestore(&desc->lock, flags);
47 +
48 + dump_stack();
49 +}
50 +
51 +void bcm2835_mbox_work_callback(struct work_struct *ignored)
52 +{
53 + void *stack;
54 + struct thread_info *current_thread;
55 +
56 + stack = current->stack;
57 + current_thread = (struct thread_info*)stack;
58 +
59 + trace_printk("current process: %s\n", current->comm);
60 + trace_printk("[+] in_interrupt:0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n",
61 + (unsigned int)in_interrupt(), (unsigned int)current_thread-62 >preempt_count, (long unsigned int)stack);
62 +
63 +
64 +
65 + work_execute_times ++;
66 + if (work_execute_times % DELAYED_WORK_PERIOD) {
67 + schedule_delayed_work(&bcm2835_mbox_delayed_work, BCM2835_DELAYED_WORK_TIME);
68 +
69 + }
70 +}
71 +
72 static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)

이제 패치 코드의 내용을 알아보겠습니다.

bcm2835_mbox_work_callback() 함수의 65번째 줄을 보면 work_execute_times 전역변수의 값을 1만큼 증가시킵니다. 66번째 줄을 보면 work_execute_times 전역변수를 DELAYED_WORK_PERIOD(10)으로 나눠서 결괏값이 1이면 if 문 내의 67번째 줄을 실행합니다.

work_execute_times 전역변수가 10만큼 증가할 때 schedule_delayed_work() 함수를 호출합니다. 이것은 워크 핸들러가 10번 실행되면 딜레이 워크를 실행하기 위해서입니다.

이번에는 딜레이 워크 핸들러인 bcm2835_mbox_delayed_work_callback() 함수를 보겠습니다. 함수 구현부는 이전 절에서 구현한 코드와 같으니 넘어가겠습니다.

다음으로 42~44번째 줄에서는 17번 인터럽트 개수를 ftrace 로그로 출력하고 콜스택을 커널 로그로 출력합니다.

42 + trace_printk("IRQ23 trigger times \n");
43 + for_each_online_cpu(i)
44 + trace_printk("%10u ", kstat_irqs_cpu(BCM_MAILBOX_IRQ_NUM, i));

이어서 워크와 딜레이 워크의 변수를 설정하는 코드를 확인하겠습니다.

3  #include <linux/spinlock.h>
4 +#include <linux/workqueue.h>
5 +#include <linux/kernel_stat.h>
7  /* Mailboxes */
8  #define ARM_0_MAIL0 0x00
9  #define ARM_0_MAIL1 0x20
10 
11 +#define BCM_MAILBOX_IRQ_NUM 23
12 +#define BCM2835_DELAYED_WORK_TIME  msecs_to_jiffies(100)
13 +#define DELAYED_WORK_PERIOD 10
14 +
15 +struct work_struct bcm2835_mbox_work;
16 +struct delayed_work bcm2835_mbox_delayed_work;
17 +
18 +unsigned long work_execute_times = 0;
19 +

12번째 줄은 100밀리초를 jiffies로 변환하는 BCM2835_DELAYED_WORK_TIME 매크로 선언부입니다. msecs_to_jiffies() 함수는 밀리초 단위 시각을 jiffie(HZ) 단위로 변환합니다. BCM2835_DELAYED_WORK_TIME은 schedule_delayed_work() 함수의 두 번째 인자로 전달합니다.

15~16번째 줄은 bcm2835_mbox_work와 bcm2835_mbox_delayed_work 변수로 각각 워크와 딜레이 워크의 변수를 선언한 코드입니다. 

위와 같이 코드를 작성한 다음 커널을 빌드한 후 라즈베리 파이에 커널 이미지를 설치합니다.

1 2 3 4 5 6 7 8 9 10 다음