Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

11206
629
98793


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

리눅스의 전망과 소개


라즈베리파이 설정

2.1 라즈베리파이 소개
2.2 라즈베리파이 설정 방법
   2.2.1 라즈베리파이 실습을 위한 준비물 챙기기
   2.2.2 라즈베리파이 설치하기
   2.2.3 라즈베리파이 기본 세팅하기
2.3 라즈베리파이 커널 빌드하기
   2.3.1 라즈비안 커널 소스 코드 내려받기
   2.3.2 라즈비안 리눅스 커널 빌드하기
   2.3.3 라즈비안 리눅스 커널 설치하기
   2.3.4 전처리 코드 생성해보기
2.4 라즈베리파이에서 objdump 바이너리 유틸리티 써보기
2.5 정리

리눅스커널 디버깅


프로세스

프로세스 디버깅
   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 슬랩 캐시 분석   
커널 메모리 디버깅









[리눅스커널] 워크큐(workqueue): __queue_work() 함수 분석하기 8장. 워크큐

워크를 워크큐에 큐잉하는 핵심 동작은 __queue_work() 함수에서 수행합니다. 코드 분석을 통해 워크를 워크큐에 어떤 방식으로 큐잉하는지 살펴보겠습니다.

코드 분석에 앞서 __queue_work() 함수 선언부와 인자를 점검합시다.
static void __queue_work(int cpu, struct workqueue_struct *wq,
struct work_struct *work);

queue_work() 함수에서 첫 번째 인자로 WORK_CPU_UNBOUND, 두 번째 인자로 system_wq 를 전달했으니 cpu는 WORK_CPU_UNBOUND, wq는 system_wq 시스템 워크큐 전역 변수 주소입니다. 함수 인자 목록을 정리하면 다음과 같습니다.
int cpu: WORK_CPU_UNBOUND
struct workqueue_struct *wq: system_wq
struct work_struct *work: struct work_struct 구조체 주소

함수 인자를 살펴봤으니 __queue_work() 함수 처리 흐름을 단계별로 알아봅시다.
1 단계: 풀워크큐 가져오기
2 단계: 워커 구조체 가져오기
3 단계: ftrace 로그 출력하기
4 단계: 워커풀에 워크 연결 리스트 등록하고 워커 스레드 깨우기 

함수 인자와 처리 단계를 알아봤으니 소스 코드를 분석할 차례입니다.
__queue_work() 함수 코드는 다음과 같습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
01 static void __queue_work(int cpu, struct workqueue_struct *wq,
02 struct work_struct *work)
03 {
04 struct pool_workqueue *pwq;
05 struct worker_pool *last_pool;
06 struct list_head *worklist;
07 unsigned int work_flags;
08 unsigned int req_cpu = cpu;
...
09 retry:
10 if (req_cpu == WORK_CPU_UNBOUND)
11 cpu = wq_select_unbound_cpu(raw_smp_processor_id());
12
13 /* pwq which will be used unless @work is executing elsewhere */
14 if (!(wq->flags & WQ_UNBOUND))
15 pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
16 else
17 pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
18
19 last_pool = get_work_pool(work);
20 if (last_pool && last_pool != pwq->pool) {
21 struct worker *worker;
22
23 spin_lock(&last_pool->lock);
24
25 worker = find_worker_executing_work(last_pool, work);
26
27 if (worker && worker->current_pwq->wq == wq) {
28 pwq = worker->current_pwq;
30 } else {
31 /* meh... not running there, queue here */
32 spin_unlock(&last_pool->lock);
33 spin_lock(&pwq->pool->lock);
34 }
35 } else {
36 spin_lock(&pwq->pool->lock);
37 }
...
38 /* pwq determined, queue */
39 trace_workqueue_queue_work(req_cpu, pwq, work);
...
40 if (likely(pwq->nr_active < pwq->max_active)) {
41 trace_workqueue_activate_work(work);
42 pwq->nr_active++;
43 worklist = &pwq->pool->worklist;
44 if (list_empty(worklist))
45 pwq->pool->watchdog_ts = jiffies;
45 } else {
49 work_flags |= WORK_STRUCT_DELAYED;
50 worklist = &pwq->delayed_works;
51 }
52
53 insert_work(pwq, work, worklist, work_flags);

1 단계: 풀워크큐 가져오기

09번째 줄 코드부터 봅시다.
09 retry:
10 if (req_cpu == WORK_CPU_UNBOUND)
11 cpu = wq_select_unbound_cpu(raw_smp_processor_id());

req_cp 지역 변수는 이 함수에 전달되는 cpu를 그대로 저장했으니 WORK_CPU_UNBOUND입니다.
8 unsigned int req_cpu = cpu;

따라서 11번째 줄 코드를 바로 실행합니다. 

11번째 줄 코드는 raw_smp_processor_id() 함수로 현재 실행 중인 CPU 번호를 알아낸 후 wq_select_unbound_cpu() 함수에 인자를 전달합니다. wq_select_unbound_cpu() 함수는 다음와 같이 동작합니다. 

    이진수로 1111인 wq_unbound_cpumask 비트 마스크용 변수와 AND 비트 연산으로 CPU 
   번호를 얻어온다.

2 단계: 워커 구조체 가져오기

이번에는 14번째 줄 코드를 보겠습니다.
14 if (!(wq->flags & WQ_UNBOUND))
15 pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
16 else
17 pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));

대부분 워크큐는 해당 플래그인 wq->flags가 0이므로 일반적인 상황에서 15번째 코드를 실행합니다. struct workqueue_struct 구조체에서 per-cpu 타입 필드인 cpu_pwqs에 접근해서 CPU 주소를 읽어 옵니다.

다음 5번째 줄 struct workqueue_struct 구조체 cpu_pwqs 필드 선언부를 보면 __percpu 키워드를 볼 수 있습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
01 struct workqueue_struct {
02 struct list_head pwqs; /* WR: all pwqs of this wq */
03 struct list_head list; /* PR: list of all workqueues */
...
04 unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
05 struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */

per-cpu 타입 변수인 wq->cpu_pwqs 필드는 다음 코드가 실행할 때 접근합니다.
15 pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);

15번째 줄 코드 동작을 그림으로 표현하면 다음과 같습니다.
 
[그림 7.6 풀워크큐 per-cpu 자료구조]

위 그림과 같이 system_rq->cpu_pwqs 필드에 저장된 주소에 현재 실행 중인 CPU 번호 기준으로 __per_cpu_offset[cpu번호] 배열에 있는 주소를 더합니다. per-cpu 타입 변수는 CPU별로 주소 공간이 있는 것입니다. 라즈베리파이는 CPU 개수가 4개이니 4개 per-cpu 메모리 공간을 할당 받습니다.

이번에는 28줄 코드를 보겠습니다.
28 last_pool = get_work_pool(work);

struct work_struct 구조체인 work 변수로 get_work_pool() 함수를 호출합니다. 이후 struct worker_pool 구조체 주소를 last_pool 지역변수로 읽습니다. get_work_pool() 함수는 조금 후 분석할 예정입니다. 

자료구조를 반경하는 코드만 읽으면 바로 이해하기 쉽지 않습니다. 다음 그림을 같이 보면서 자료구조가 어떻게 바뀌는지 알아볼까요?
 
[그림 7.7] 워커풀에서 워커 검색 흐름 

워크를 실행한 적이 있으면 struct work_struct 구조체 data 필드는 풀워크 주소를 저장하고 있습니다. get_work_pool() 함수는 위 그림에서 [1],[2] 으로 표시된 부분과 같이 워커풀 주소를 가져옵니다.

이어서 29번째 줄 코드를 분석하겠습니다.
28 last_pool = get_work_pool(work);
29 if (last_pool && last_pool != pwq->pool) {
30 struct worker *worker;
31
32 spin_lock(&last_pool->lock);
33
34 worker = find_worker_executing_work(last_pool, work);
35
36 if (worker && worker->current_pwq->wq == wq) {
37 pwq = worker->current_pwq;
38 } else {
39 /* meh... not running there, queue here */
40 spin_unlock(&last_pool->lock);
41 spin_lock(&pwq->pool->lock);
42 }
43 } else {
44 spin_lock(&pwq->pool->lock);
45 }

29번째 줄부터 시작하는 조건문은 다음 조건을 체크합니다. 

     큐잉하는 워크를 이미 워커풀에서 실행한 적이 있는가?

워크를 처음 실행하면 struct work_struct 구조체 data 필드에 풀워크 주소를 저장합니다. data 필드에 액세스를 하면 워커풀 주소에 접근할 수 있습니다. 이미 워크를 처리한 풀워크가 있으면 이를 통해 워커풀을 재사용하려는 의도입니다.

다음 34번째 줄 코드를 보겠습니다.
34 worker = find_worker_executing_work(last_pool, work);
35
36 if (worker && worker->current_pwq->wq == wq) {
37 pwq = worker->current_pwq;

find_worker_executing_work() 함수를 호출해서 워크를 실행한 워커 스레드 구조체 주소를 worker 지역변수에 저장합니다. 워커 스레드 구조체 worker->current_pwq->wq에 저장된 워크큐 주소와 지금 큐잉하는 워크에 대응하는 워크큐와 같으면 struct pool_workqueue 구조체 주소를 가져옵니다. 그림 7.3에서 [3] 번호에 대응하는 동작입니다. 

워크를 여러 개의 워커 스레드에서 처리하지 못하게 제약 조건을 둔 것입니다. get_work_pool() 함수와 find_worker_executing_work() 함수는 다음 소절에서 분석할 예정입니다.

3 단계: ftrace 로그 출력하기

이제 3단계 처리 과정 코드를 분석하겠습니다.  
59 trace_workqueue_queue_work(req_cpu, pwq, work);
60
61 if (WARN_ON(!list_empty(&work->entry))) {
62 spin_unlock(&pwq->pool->lock);
63 return;
64 }

59번째 줄 코드는 ftrace로 workqueue_queue_work 워크큐 이벤트를 키면 실행해 다음과 같은 ftrace 로그를 출력합니다. 
make-13395 [000] d.s.   589.830896: workqueue_queue_work: work struct=b9c1aa30 function=mmc_rescan workqueue=b9c06700 req_cpu=4 cpu=0
make-13395 [000] d.s.   589.830897: workqueue_activate_work: work struct b9c1aa30

위 로그는 워크를 워크큐에 큐잉했다고 해석할 수 있습니다.  struct work_struct 구조체 주소는 b9c1aa30이고 워크 핸들러 함수인 mmc_rescan()입니다. 

schedule_work() 함수를 호출하면 워크를 보통 워크큐에 큐잉했다고 짐작합니다. 하지만 위 ftrace 로그에서 workqueue_queue_work와 workqueue_activate_work 메시지를 확인하기 전까지 제대로 동작할 것이라 확신하면 안됩니다. 워크를 워크큐에 큐잉하는 schedule_work() 함수를 호출해도 특정 상황에서 워크가 워크큐에 제대로 큐잉을 못할 수도 있기 때문입니다. 

그래서 새로운 워크를 선언한 다음 워크큐에 큐잉하는 코드를 작성했을 때 위와 같은 ftrace 메시지를 확인할 필요가 있습니다. 


실력있는 개발자가 되려면 자신이 구현한 드라이버 코드가 제대로 동작하는지 점검하는 방법도 알고 있어야 합니다. 


다음 68번째 줄 코드를 분석하겠습니다.
68 if (likely(pwq->nr_active < pwq->max_active)) {
69 trace_workqueue_activate_work(work);
70 pwq->nr_active++;
71 worklist = &pwq->pool->worklist;
72 if (list_empty(worklist))
73 pwq->pool->watchdog_ts = jiffies;
74 } else {
75 work_flags |= WORK_STRUCT_DELAYED;
76 worklist = &pwq->delayed_works;
77 }

먼저 if~else 문 조건을 살펴봅시다.

68번째 줄 코드는 if 조건문으로 현재 실행 중인 워커 개수를 점검합니다. if 문 결과에 따라 다음과 같이 처리합니다. 
풀 워크큐에서 현재 실행 중인 워커 개수가 255를 넘지 않으면 69~73번째 줄 코드를 실행 
반대의 경우 75~76번째 줄 코드를 실행 

여기서 struct pool_workqueue 구조체 필드 중 nr_active는 풀 워크큐에서 현재 실행 중인 워커 개수고 max_active는 풀 워크큐에서 실행 가능한 최대 워커 개수를 의미합니다.  pwq_adjust_max_active() 함수가 실행될 때 pwq->max_active는 255개로 설정됩니다.

74번째 줄 코드는 워크를 처리할 워커 개수가 255개를 넘어섰을 때 동작합니다. 이때 struct pool_workqueue 구조체 필드 중 delayed_works인 링크드 리스트를 worklist 지역 변수에 저장합니다. 여기서 보이는 delayed_works는 딜레이 워크(struct delayed_work)와 다른 개념입니다. 실행 중인 워커 개수가 많다는 것은 그만큼 처리해야 할 워크가 많다는 것을 의미합니다. 그래서 delayed_works 이란 연결 리스트(struct list_head) 필드에 워크를 등록하고 적절한 시점 이후 이 워크를 처리합니다.

딜레이 워크(struct delayed_work)는 특정 시각 후 워크를 실행합니다. 위 코드의 delayed_works와 딜레이 워크는 헷갈릴 수 있으니 주의하시길 바랍니다.

정리하면, 특정 상황에서 커널 서브 시스템이나 드라이버에서 워크를 아주 많이 생성해서 워크를 처리할 워커 개수가 255개를 넘어섰을 때를 제외하고 보통69~73번째 줄 코드를 실행합니다.


if~else 문 아래 78번째 줄 이후 코드는 69~73번 코드가 실행했다고 가정하고 분석하겠습니다.


if~else 문이 실행하는 조건을 점검했으니 69번째 줄 코드부터 분석을 시작합니다.

69번째 줄 코드는 ftrace에서 workqueue_activate_work 이벤트를 키면 다음과 같은 로그를 출력합니다. 코드와 ftrace 로그를 함께 보겠습니다.
69 trace_workqueue_activate_work(work);

make-13395 [000] d.s.   589.830896: workqueue_queue_work: work struct=b9c1aa30 function=mmc_rescan workqueue=b9c06700 req_cpu=4 cpu=0
make-13395 [000] d.s.   589.830897: workqueue_activate_work: work struct b9c1aa30

워크를 표현하는 struct work_struct 자료구조 주소를 출력하는데 보통 workqueue_queue_work ftrace 로그 이후에 출력합니다.

70번째 줄 코드는 현재 실행 중인 워커 개수를 1만큼 증감합니다.
70 pwq->nr_active++;

4 단계: 워커풀에 워크 연결 리스트 등록하고 워커 스레드 깨우기

__queue_work() 함수에서 가장 중요한 코드입니다.

71번과 76번째 줄 코드를 함께 보겠습니다.
71 worklist = &pwq->pool->worklist; 
...
76 worklist = &pwq->delayed_works;

먼저 71번째 줄 코드를 분석하겠습니다.
pwq 변수는 struct pool_workqueue 구조체 타입이고 pool은 struct worker_pool 구조체 타입입니다. &pwq->pool 코드로 struct worker_pool 구조체에 접근해서 worklist 필드를 worklist 지역 변수에 저장하는 코드입니다.

이번엔 76번째 줄 코드를 보겠습니다. struct pool_workqueue 구조체인 pwq 변수로 delayed_works란 필드 주소를 worklist 지역 변수로 저장합니다. 

코드만 보면 자료구조가 어떻게 바뀌는지 이해하기 어렵습니다. 다음 그림을 같이 보면서 워크를 큐잉할 때 자료구조가 어떻게 변경되는지 확인해볼까요? 
 
    [그림 7.8] 워크를 워커풀에 큐잉할 때 변경되는 자료구조  

[1] &pwq->pool->worklist는 71번째 줄 [2] &pwq->delayed_works는 76번째 줄 코드를 의미합니다. 워크큐 자료 구조를 변경하는 코드를 볼 때 이렇게 전체 자료 구조 흐름을 머릿속으로 그리면서 분석하면 이해가 더 빠릅니다.

이어서 79번째 줄 코드를 보겠습니다. 
79 insert_work(pwq, work, worklist, work_flags);

insert_work() 함수에 전달하는 주요 인자들을 살펴보고 insert_work() 함수 분석을 시작 하겠습니다.

pwq 는 per-cpu 타입인 struct pool_workqueue 구조체 주소를 담고 있고, work는 _queue_work() 함수로 워크큐에 큐잉하려는 워크입니다. worklist는 &pwq->pool->worklist 코드로 저장된 struct list_head 구조체인 링크드 리스트입니다. 

insert_work() 함수를 호출해서 &pwq->pool->worklist 연결 리스트에 다음과 같은 워크 연결리스트를 등록합니다. 
struct work_struct 구조체 entry 필드
다음 insert_work() 함수에서 wake_up_worker() 함수를 호출해서 워크를 실행할 워커 스레드를 깨웁니다.

여기까지 워크를 큐잉하는 흐름을 알아봤습니다. 다음 절에서는__queue_work_on() 함수에서 호출하는 워크큐 함수를 분석합니다.


[리눅스커널] 워크큐(workqueue): 워크를 워크큐에 큐잉하는 인터페이스 함수 분석하기 8장. 워크큐

커널은 디바이스 드라이버 레벨에서 워크큐를 큐잉할 수 있는 여러 가지 함수를 지원합니다.
이번 시간에는 워크를 워크큐에 큐잉할 때 사용하는 함수를 소개하고 코드를 분석합니다. 
schedule_work()
queue_work()
queue_work_on()

먼저 schedule_work() 함수를 분석해볼까요?

schedule_work() 함수 분석하기

schedule_work() 함수 구현부 코드는 다음과 같습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/workqueue.h]
1 static inline bool schedule_work(struct work_struct *work)
2 {
3 return queue_work(system_wq, work);
4}

schedule_work() 함수는 인라인 타입으로 함수 구현부가 간단합니다.

queue_work() 함수를 호출하는데 system_wq 전역 변수를 첫 번째 인자로 queue_work() 함수에 전달합니다. 이 코드 내용을 토대로 다음 사실을 알 수 있습니다. 

    schedule_work() 함수로 전달하는 워크는 시스템 워크큐에 큐잉된다.

시스템 워크큐는 system_wq 전역 변수로 관리하며 선언부는 다음과 같습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
struct workqueue_struct *system_wq __read_mostly;
EXPORT_SYMBOL(system_wq);

이어서 queue_work() 함수 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/workqueue.h]
01 static inline bool queue_work(struct workqueue_struct *wq,
02       struct work_struct *work)
03 {
04 return queue_work_on(WORK_CPU_UNBOUND, wq, work);
05 }

4번째 줄 코드를 보면 WORK_CPU_UNBOUND를 첫 번째 인자로 queue_work_on() 함수를 호출합니다.

정리하면 schedule_work() 함수로 전달하는 워크는 시스템 워크큐에 큐잉되며 다음 함수 흐름으로 queue_work_on() 함수를 호출한다는 사실을 알 수 있습니다. 
queue_work()
queue_work_on()

queue_work_on() 함수 분석하기

이어서 queue_work_on() 함수 코드를 분석해봅시다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
01 bool queue_work_on(int cpu, struct workqueue_struct *wq,
02    struct work_struct *work)
03 {
04 bool ret = false;
05 unsigned long flags;
06
07 local_irq_save(flags);
08
09 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
10 __queue_work(cpu, wq, work);
11 ret = true;
12 }
13
14 local_irq_restore(flags);
15 return ret;
16}

먼저 7번째 줄 코드를 보겠습니다.
07 local_irq_save(flags);

7~14번째 줄 코드 구간에서 워크를 큐잉하는 9~12번째 줄 코드 구간 실행 도중에 해당 CPU라인 인터럽트를 비활성화합니다. 이 코드의 목적은 다음과 같습니다. 

    해당 코드를 실행하는 도중 인터럽트가 발생하는 동기화 문제 발생을 방지하고 싶다. 

커널 코드는 언제든 인터럽트가 발생해서 실행 흐름이 멈출 수 있습니다.

이어서 9번째 줄 코드를 보겠습니다. 
09 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
10 __queue_work(cpu, wq, work);
11 ret = true;
12 }

work_data_bits(work) 매크로 함수는 struct work_struct 구조체 주소에서 data 필드를 읽습니다. 이 값이 WORK_STRUCT_PENDING_BIT(1)이면 9~12 번째 줄 코드를 실행하지 않고 바로 14 번째 줄 코드를 실행합니다.

test_and_set_bit() 함수는 리눅스 커널 자료구조 함수 중 하나입니다.
test_and_set_bit(A, B); 와 같이 호출하면 A와 B란 변수 비트를 AND 연산한 다음 결과가 1이면 1을 반환하고 반대로 0이면 0을 반환합니다. 연산 결과에 상관없이 B에 A비트를 설정합니다.

이해를 돕기 위해 9~12째 줄 코드를 다른 코드로 쉽게 표현하면 다음과 같습니다.
1 if (work->data == WORK_STRUCT_PENDING_BIT) {
3 } else
4 work->data =| WORK_STRUCT_PENDING_BIT;
5 __queue_work(cpu, wq, work);
6 ret = true;
7 }

work->data가 WORK_STRUCT_PENDING_BIT이면 if 문을 만족하니 2번째 줄 코드로 이동합니다. 그런데 2번째 줄에는 코드가 없으니 아무 동작을 안 하고 if 문을 빠져나옵니다. 대신 work->data가 WORK_STRUCT_PENDING_BIT 가 아니면 else문을 실행합니다. 

if 문 조건을 만족하면 아무 동작을 안 하는 코드는 보기 이상하니 if 문을 ! 조건으로 바꿔 코드를 작성하면 다음과 같습니다.
if ( !(work->data == WORK_STRUCT_PENDING_BIT)) {
work->data =| WORK_STRUCT_PENDING_BIT;
__queue_work(cpu, wq, work);
ret = true;
}

work->data 필드가 WORK_STRUCT_PENDING_BIT 플래그가 아니면 work->data에 WORK_STRUCT_PENDING_BIT를 저장하고 __queue_work() 함수를 호출하는 것입니다.

test_and_set_bit() 함수를 다른 코드로 바꿔서 설명을 드렸습니다. test_and_set_bit() 함수는 위에 바꾼 코드와 같이 struct work_struct구조체 data 필드가 WORK_STRUCT_PENDING_BIT가 아니면 struct work_struct 구조체 data 필드를 WORK_STRUCT_PENDING_BIT로 설정하고 0을 반환합니다.

반대로 struct work_struct 구조체 data 필드가 WORK_STRUCT_PENDING_BIT 플래그면 data 필드를 WORK_STRUCT_PENDING_BIT 플래그로 설정한 후 1을 반환합니다.

여기서 한 가지 의문이 생깁니다. 

    struct work_struct 구조체 data 필드가 WORK_STRUCT_PENDING_BIT인지 왜 
   점검하는 이유는 무엇일까?

struct work_struct 구조체 data 필드에 워크 실행 상태가 저장돼있기 때문입니다. queue_work_on() 함수 호출로 워크를 워크큐에 큐잉하기 직전에 이 필드는 WORK_STRUCT_PENDING_BIT 플래그로 바꿉니다. 

만약 워크를 워크큐에 큐잉하기 직전에 이 필드가 WORK_STRUCT_PENDING_BIT이라면 이를 어떻게 해석해야 할까요?  

    이미 워크를 워크에 큐잉한 상태로 볼 수 있습니다. 

이 조건에서는 워크를 워크큐에 큐잉하지 않습니다. 워크를 워크큐에 중복 큐잉할 때를 대비한 예외 처리 코드입니다.


커널은 디바이스 드라이버에서 중복 코드를 실행할 경우 시나리오를 생각해서 예외 처리를 수행합니다.



[리눅스커널] 워크큐(workqueue): 워크를 워크큐에 큐잉하는 전체 처리과정 8장. 워크큐

다음 워크큐 전체 실행 흐름도에서 워크를 워크큐에 큐잉하는 단계를 보겠습니다.
 
[그림 7.5] 워크 실행 흐름도 중 2단계

워크 실행은 위 그림과 같이 3단계로 나눌 수 있는데 이번 소절에서 2단계 부분을 살펴봅니다.
워크를 실행하려면 먼저 워크를 워크큐에 큐잉해야 하며 이를 위해 schedule_work() 함수를 호출해야 합니다.


[리눅스커널] 워크큐(workqueue): 워크를 워크큐에 큐잉하는 예제 코드 살펴보기 8장. 워크큐

워크를 워크큐에 큐잉하는 방법은 간단합니다.
struct work_struct 구조체 주소 인자와 함께 schedule_work() 함수를 호출하면 됩니다.

다음 워크를 워크큐에 큐잉하는 예제 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/tty/vt/vt.c]
01 static DECLARE_WORK(console_work, console_callback);
02
03 void schedule_console_callback(void)
04 {
05 schedule_work(&console_work);
06 }

05번째 줄 코드와 같이 struct work_struct 구조체 주소를 &console_work 변수로 schedule_work() 함수에 전달하면 됩니다.


[리눅스커널] 스케줄링/디버깅: 프로세스를 깨울 때 콜스택 분석하기 10장. 프로세스 스케줄링

이번에는 sched_wakeup ftrace 이벤트를 출력하는 ttwu_do_wake_up() 함수 콜스택을 확인하겠습니다. ftrace 메시지를 열어 보면 ttwu_do_wakeup() 함수가 다양한 경로로 호출된다는 사실을 알 수 있습니다.

이 중 평소 자주 볼 수 있는 함수 호출 흐름 중심으로 살펴봅시다.


ftrace를 설정하고 ftrace 로그를 받은 방법은 이전 10.11.2 절에서 소개한 내용을 참고하세요.


먼저 ftrace 메시지를 보겠습니다.
1 lxterminal-840 [000] d... 8688.516171: ttwu_do_wakeup+0x10/0x1a4 <-ttwu_do_activate+0x80/0x84
2 lxterminal-840 [000] d... 8688.516204: <stack trace>
3  => wake_up_process+0x20/0x24
4 => insert_work+0x8c/0xd4
5 => __queue_work+0x1ac/0x51c
6 => queue_work_on+0xa0/0xa4
7 => tty_flip_buffer_push+0x3c/0x40
8 => pty_write+0x84/0x88
9 => n_tty_write+0x368/0x458
10 => tty_write+0x1c8/0x2e4
11 => __vfs_write+0x3c/0x13c
12 => vfs_write+0xd4/0x214
13 => sys_write+0x4c/0xa0
14 => __sys_trace_return+0x0/0x10
15 lxterminal-840   [000] d...  8688.516208: sched_wakeup: comm=kworker/u8:0 pid=1128 prio=120 target_cpu=000
16 lxterminal-840   [000] d...  8688.516497: sched_switch: prev_comm=lxterminal prev_pid=840 prev_prio=120 prev_state=D ==> next_comm=Xorg next_pid=552 next_prio=120
17 Xorg-552   [000] d...  8688.516757: sched_switch: prev_comm=Xorg prev_pid=552 prev_prio=120 prev_state=D ==> next_comm=lxterminal next_pid=840 next_prio=120
18 lxterminal-840   [000] d...  8688.516900: sched_switch: prev_comm=lxterminal prev_pid=840 prev_prio=120 prev_state=D ==> next_comm=kworker/u8:0 next_pid=1128 next_prio=120

워크를 워크큐에 큐잉하면 워커 스레드를 깨웁니다. 이때 함수 호출 흐름을 볼 수 있습니다.

다음 단계로 워커 스레드를 깨우면 스케줄러가 어떤 흐름으로 워커 스레드를 스케줄링 하는 과정을 볼 수 있습니다. 

    이 경우 wake_up_process() 함수를 호출합니다.

이제 함수 호출 순서 기준으로 ftrace 메시지를 분석하겠습니다. 1~14 번째 줄 메시지를 보겠습니다.
1 lxterminal-840 [000] d... 8688.516171: ttwu_do_wakeup+0x10/0x1a4 <-ttwu_do_activate+0x80/0x84
2 lxterminal-840 [000] d... 8688.516204: <stack trace>
3  => wake_up_process+0x20/0x24
4 => insert_work+0x8c/0xd4
5 => __queue_work+0x1ac/0x51c
6 => queue_work_on+0xa0/0xa4
7 => tty_flip_buffer_push+0x3c/0x40
8 => pty_write+0x84/0x88
9 => n_tty_write+0x368/0x458
10 => tty_write+0x1c8/0x2e4
11 => __vfs_write+0x3c/0x13c
12 => vfs_write+0xd4/0x214
13 => sys_write+0x4c/0xa0
14 => __sys_trace_return+0x0/0x10

함수 호출 흐름은 14 번째 줄 로그에서 1번 째 줄 로그 방향입니다. 이해를 쉽게 하기 위해 함수 호출 방향 기준으로 로그를 설명하겠습니다. 

14~8 번째 줄 메시지는 유저 공간에서 write() 시스템 콜 함수를 실행했다는 사실을 알 수 있습니다. 커널 공간에서 write() 시스템 콜 핸들러인 sys_write() 함수가 호출됩니다. 파일 객체에 등록된 tty 드라이버 write() 함수를 호출해서 tty 버퍼 처리를 수행하는 동작입니다  

다음 7~3 번째 줄 메시지를 보겠습니다.
3  => wake_up_process+0x20/0x24
4 => insert_work+0x8c/0xd4
5 => __queue_work+0x1ac/0x51c
6 => queue_work_on+0xa0/0xa4
7 => tty_flip_buffer_push+0x3c/0x40

워크를 워크큐에 큐잉하는 함수 흐름입니다. 
워크를 워크큐 연결 리스트에 등록한 다음 워크를 실행할 워커 스레드를 깨우는 동작입니다.


워커 스레드를 프로세스와 같은 개념으로 설명을 드리고 있는데 용어를 잠깐 정리합시다.

워커 스레드는 프로세스 종류 중 하나입니다.
워커 스레드를 프로세스 관점으로 설명을 하면, 워커 스레드는 워크를 관리하기 위해 커널 공간에서만 실행하는 프로세스입니다.

워크와 워크큐에 세부 동작은 워크큐 챕터를 참고하세요.


프로세스를 깨우는 함수는 wake_up_process() 함수이라는 사실을 기억합시다.

wake_up_process() 함수 코드를 잠깐 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c]
1 int wake_up_process(struct task_struct *p)
2 {
3 return try_to_wake_up(p, TASK_NORMAL, 0);
4 }

wake_up_process() 함수는 태스크 디스크립터인 p인자를 try_to_wake_up() 함수 1 번째 인자로 전달합니다. p 인자는 깨우려고 하는 프로세스 태스크 디스크립터 주소입니다. 

다음은 프로세스를 깨운 실제 언제 스케줄링이 되는지 살펴보겠습니다.

위에서 살펴본 로그로 kworker/u8:0(pid=1128) 프로세스를 깨웠습니다. 그럼 바로 스케줄러가 kworker/u8:0(pid=1128) 프로세스를 실행할까요? 그렇지는 않습니다. 

    스케줄러는 스케줄러 런큐에 큐잉된 프로세스를 점검합니다. 
    이 과정에서 프로세스 별 우선순위를 계산해서 가장 먼저 실행할 프로세스를 선택합니다.

다음 로그를 이어서 분석해보겠습니다.
prev_comm=lxterminal prev_pid=840 prev_prio=120 prev_state=D ==> next_comm=Xorg next_pid=552 next_prio=120
prev_comm=Xorg prev_pid=552 prev_prio=120 prev_state=D ==> next_comm=lxterminal next_pid=840 next_prio=120
prev_comm=lxterminal prev_pid=840 prev_prio=120 prev_state=D ==> next_comm=kworker/u8:0 next_pid=1128 next_prio=120

kworker/u8:0(pid=1128) 프로세스보다 Xorg(pid=552) 프로세스와 lxterminal(pid=840) 프로세스 우선순위가 더 높다고 판단한 것입니다. 그래서 스케줄러는 Xorg(pid=552) 프로세스와 lxterminal(pid=840) 프로세스를 먼저 실행하고 난 다음 kworker/u8:0(pid=1128) 프로세스를 실행한 것입니다.

프로세스를 깨운다는 용어의 의미는 해당 프로세스를 실행시켜 달라고 스케줄러에게 요청하는 동작입니다. 이때 스케줄러는 다음과 같은 순서로 처리를 합니다. 
런큐에 이미 큐잉된 다른 프로세스들과 우선순위를 비교한다.
스케줄러는 우선순위에 따라 다음에 실행할 프로세스를 결정한다. 

만약 실행 요청을 한 프로세스가 이미 큐잉된 다른 프로세스들 보다 우선순위가 높으면 스케줄러는 실행 요청한 프로세스를 바로 실행을 시킬 것입니다. 


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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링/디버깅: ftrace: 스케줄링 실행 시 콜스택 파악하기 10장. 프로세스 스케줄링

sched_switch와 sched_wakeup 이벤트는 각각 프로세스 스케줄링과 프로세스를 깨우는 동작을 트레이싱합니다. 이번에는 스케줄링이 실행할 때 콜스택을 점검해 보겠습니다.

커널에선 2가지 타입 스케줄링을 지원합니다.
 
[그림 10.48] 2가지 스케줄링 종류과 동작 방식

스케줄링 종류 별로 어떤 콜스택인지 점검합시다. 먼저 ftrace 설정을 위한 다음 코드를 소개합니다.
1 #!/bin/bash
3 echo 0 > /sys/kernel/debug/tracing/tracing_on
4 sleep 1
5 echo "tracing_off"
6
7 echo 0 > /sys/kernel/debug/tracing/events/enable
8 sleep 1
9 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
20 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
21
22 echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
23 echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
24
25 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/enable
26 sleep 1
27 echo "event enabled"
28
29 echo  schedule ttwu_do_wakeup > /sys/kernel/debug/tracing/set_ftrace_filter
30
31 sleep 1
32 echo "set_ftrace_filter enabled"
33
34 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
35 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
36 echo "function stack trace enabled"
37
38 echo 1 > /sys/kernel/debug/tracing/tracing_on
39 echo "tracing_on"

다음은 sched_switch와 sched_wakeup 이벤트를 설정하는 명령어입니다.
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
20 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable

이번에는 함수에 필터를 거는 명령어입니다.
29 echo  schedule ttwu_do_wakeup > /sys/kernel/debug/tracing/set_ftrace_filter

위 명령어로 schedule()와 ttwu_do_wakeup() 함수의 콜스택을 볼 수 있습니다.

다음 22~25번째 줄은 irq_handler_entry, irq_handler_exit 이벤트와 시스템 콜 이벤트를 키는 코드입니다.
22 echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
23 echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
24
25 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/enable

위와 같이 인터럽트 동작 관련 이벤트와 시스템 콜 이벤트를 설정한 이유는, 인터럽트 핸들링과 시스템 콜 처리가 끝난 다음에 선점 스케줄링이 실행하기 때문입니다.

위 셸 스크립트를 sched_irq_syscall.sh 파일로 저장한 다음에 실행합시다.

셸 스크립트를 실행한 다음 20초 정도 방치합시다. 이후 get_ftrace.sh란 셸 스크립트 파일을 실행해서 ftrace 로그를 추출합시다.

명시적 스케줄링 시 콜스택 분석

이번에는 명시적 스케줄링을 실행할 때 콜스택을 보겠습니다.  

    명시적 스케줄링이란 무엇일까?

프로세스가 스스로 schedule() 함수를 호출해 스케줄링 하는 동작입니다. 비선점 스케줄링을 실행하는 함수 목록은 다음과 같습니다.
schedule_hrtimeout_range_clock()
schedule_timeout()
worker_thread()
__mutex_lock_common()

schedule_hrtimeout_range_clock() 함수 콜스택 분석하기

schedule_hrtimeout_range_clock() 함수 콜스택을 보면서 명시적 스케줄링 실행 시 함수 흐름을 살펴보겠습니다.

먼저 분석할 ftrace 로그는 다음과 같습니다.
1 lxterminal-840   [001] ....  8632.128798: schedule+0x10/0xa8 <-schedule_hrtimeout_range_clock+0xd8/0x14c
2 lxterminal-840   [001] ....  8632.128816: <stack trace>
3 => poll_schedule_timeout+0x54/0x84
4 => do_sys_poll+0x3d8/0x500
5 => sys_poll+0x74/0x114
6 => __sys_trace_return+0x0/0x10
7 lxterminal-840   [001] d...  8632.128827: sched_switch: prev_comm=lxterminal prev_pid=840 prev_prio=120 prev_state=D ==> next_comm=sched_basic.sh next_pid=1153 next_prio=120

위 로그에서 먼저 1번째 줄을 보면 다음 정보를 확인할 수 있습니다. 

    schedule_hrtimeout_range_clock() 함수에서 schedule() 함수를 호출한다.

콜스택으로 프로세스 스스로 schedule() 함수를 호출한다는 사실을 확인했습니다. 이 정보로 다음 사실을 알 수 있습니다.  

    프로세스 스스로 schedule() 함수를 호출해 명시적 스케줄링을 실행한다.

여기서 6~3번째 줄 로그가 함수 호출 방향입니다.

콜스택 다음에 바로 7 번째 줄과 같이 sched_switch 이벤트 메시지를 볼 수 있습니다. 이 메세지로 다음 사실을 알 수 있습니다. 

    "lxterminal" 프로세스에서 "sched_basic.sh" 프로세스로 스케줄링된다.

ftrace 로그를 봤으니 커널 코드를 보면서 실제 위와 같이 함수 호출이 이루어지는지 확인해볼까요?
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/time/hrtimer.c]
1 int __sched
2 schedule_hrtimeout_range_clock(ktime_t *expires, u64 delta,
3        const enum hrtimer_mode mode, int clock)
4 {
...
5 if (!expires) {
6 schedule();
7 return -EINTR;
8 }

위 schedule_hrtimeout_range_clock() 함수를 스케줄링 관점으로 해석하면 
지정한 시각이 지나면 타임아웃으로 명시적 스케줄링을 실행하는 동작입니다. 6번째 줄을 보면 schedule() 함수를 호출합니다.

schedule_timeout() 함수 콜스택 분석하기

이어서 schedule_timeout() 함수 콜스택을 분석하겠습니다. 
01 VCHIQ completio-1630  [002] ....  9447.646780: schedule+0x10/0xa8 <-schedule_timeout+0x1e0/0x418
02 VCHIQ completio-1630  [002] ....  9447.646829: <stack trace>
03 => down_interruptible+0x5c/0x68
04 => vchiq_ioctl+0x9d4/0x1950
05 => do_vfs_ioctl+0xb0/0x7d0
06 => sys_ioctl+0x44/0x6c
07 => __sys_trace_return+0x0/0x10
08 VCHIQ completio-1630  [002] d...  9447.646934: sched_switch: prev_comm=VCHIQ completio prev_pid=1630 prev_prio=120 prev_state=D ==> next_comm=swapper/2 next_pid=0 next_prio=120

위 로그에서 먼저 1번째 줄을 보면 다음 정보를 확인할 수 있습니다. 

    schedule_timeout() 함수에서 schedule() 함수를 호출한다.

이번에도 프로세스 스스로 schedule() 함수를 호출하는 정보를 확인했습니다. 이 정보를 토대로 우리는 사실을 알 수 있습니다.  

    프로세스 스스로 schedule() 함수를 호출해 명시적 스케줄링을 실행한다.

여기서 07~03번째 줄 로그가 함수 호출 방향입니다.

콜스택 다음에 바로 08 번째 줄과 같이 sched_switch 이벤트 메시지를 볼 수 있습니다. 이 메세지를 해석하면 다음과 같습니다.  

    "VCHIQ completio" 프로세스에서 "swapper/2" 프로세스로 스케줄링된다.

이번에는 ftrace에서 본 콜스택을 커널 함수에서 확인해보겠습니다. ftrace 로그 분석에서 그치지 말고 커널 코드를 열어 보면 더 많은 것을 얻을 수 있습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/locking/semaphore.c]
01 static inline int __sched __down_common(struct semaphore *sem, long state,
02 long timeout)
03 {
...
04
05 for (;;) {
...
06 __set_current_state(state);
07 raw_spin_unlock_irq(&sem->lock);
08 timeout = schedule_timeout(timeout);

08번째 줄 코드와 같이 세마포어를 획득하는  __down_common() 함수에서 schedule_timeout() 함수를 호출하는 합니다. 이후 schedule_timeout() 함수를 호출하면 schedule() 함수를 호출하게 됩니다.

__mutex_lock_slowpath() 함수 콜스택 분석하기

이번에는 뮤텍스를 할당하는 과정에서 비선점 스케줄링을 수행하는 콜스택을 보겠습니다.
1  kworker/u16:19-23715 [003] ...1 21857.480423: schedule+0x10/0x9c <-schedule_preempt_disabled+0x18/0x2c
2  kworker/u16:19-23715 [003] ...1 21857.480427: <stack trace>
3  => __mutex_lock_slowpath+0x15c/0x39c
4  => mutex_lock+0x34/0x48
5  => clk_prepare_lock+0x48/0xd4
6  => clk_get_rate+0x24/0x90
7  => dev_get_cur_freq+0x28/0x70
8  => update_devfreq+0xa0/0x1d4
9  => devfreq_monitor+0x34/0x94
10 => process_one_work+0x184/0x480
11 => worker_thread+0x140/0x4b4
12 => kthread+0xf4/0x108
13 => ret_from_fork+0x10/0x50
14 <...>-23715 [003] d..2 21857.480468: sched_switch: prev_comm=kworker/u16:19 prev_pid=23715 prev_prio=120 prev_state=D ==> next_comm=swapper/3 next_pid=0 next_prio=120 

먼저 1~3번째 줄 로그를 보겠습니다.
1  kworker/u16:19-23715 [003] ...1 21857.480423: schedule+0x10/0x9c <-schedule_preempt_disabled+0x18/0x2c
2  kworker/u16:19-23715 [003] ...1 21857.480427: <stack trace>
3  => __mutex_lock_slowpath+0x15c/0x39c

1~3번째 줄에 보이는 함수는 다음과 같이 호출됩니다.
__mutex_lock_slowpath()
schedule_preempt_disabled()
schedule()

__mutex_lock_slowpath() 함수에서 schedule_preempt_disabled() 함수를 호출해 명시적 스케줄링을 실행하는 것입니다. 결국 schedule_preempt_disabled() 함수에서 schedule() 함수를 호출하기 때문입니다. 이번에도 프로세스 스스로 schedule() 함수를 호출해 명시적 스케줄링을 수행합니다.

다음 14번째 줄 메시지를 볼까요?
14 <...>-23715 [003] d..2 21857.480468: sched_switch: prev_comm=kworker/u16:19 prev_pid=23715 prev_prio=120 prev_state=D ==> next_comm=swapper/3 next_pid=0 next_prio=120 

kworker/u16:19 프로세스에서 swapper/3 프로세스로 스케줄링되는 동작입니다.

이렇게 뮤텍스를 획득하지 못한 프로세스는 자신을 TASK_UNINTERRUPTIBLE 상태로 바꾼 후 휴면에 진입합니다.

커널 스레드에서 schedule() 함수를 호출하는 패턴을 확인합시다.
1 kworker/u8:0-1128 [001] 8632.128947: schedule+0x10/0xa8 <-worker_thread+0x104/0x5f0
2 kworker/u8:0-1128 [001] 8632.128961: <stack trace>
3 => ret_from_fork+0x14/0x28
4 kworker/u8:0-1128  [001] d...  8632.128968: sched_switch: prev_comm=kworker/u8:0 prev_pid=1128 prev_prio=120 prev_state=R+ ==> next_comm=lxterminal next_pid=840 next_prio=120

ftrace 1 번째 메시지를 보면 worker_thread() 함수에서 schedule() 함수를 호출하는 콜스택을 확인할 수 있습니다.

워크를 처리하는 워커 스레드 핸들인 worker_thread() 함수 어느 코드에서 schedule() 함수를 호출할까요? 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
1 static int worker_thread(void *__worker)
2 {
...
3 process_one_work(worker, work);
...
4 sleep:
5 worker_enter_idle(worker);
6 __set_current_state(TASK_IDLE);
7 spin_unlock_irq(&pool->lock);
8 schedule();
9 goto woke_up;
10 }

워크를 모두 처리한 다음에 8번째 줄 코드와 같이 워커 스레드는 schedule() 함수를 호출해 휴면에 진입합니다.

선점 스케줄링 시 콜스택 분석

프로세스가 자신의 의지에 상관없이 스케줄러에 의해 CPU를 비우는 스케줄링 방식을 선점 스케줄링이라고 합니다. 이번에는 선점 스케줄링 동작 시 콜스택을 ftrace로 확인하겠습니다.

분석할 ftrace 로그는 다음과 같습니다.
1  chromium-browse-1436  [000] d.h.  9448.149965: irq_handler_entry: irq=92 name=mmc1
2  chromium-browse-1436  [000] d.h.  9448.149972: irq_handler_exit: irq=92 ret=handled
3  chromium-browse-1436  [000] d.h.  9448.149982: ttwu_do_wakeup+0x10/0x1a4 <-ttwu_do_activate+0x80/0x84
4  chromium-browse-1436  [000] d.h.  9448.150003: <stack trace>
5  => wake_up_process+0x20/0x24
6  => __irq_wake_thread+0x70/0x74
7  => __handle_irq_event_percpu+0x84/0x224
8  => handle_irq_event_percpu+0x2c/0x68
9  => handle_irq_event+0x54/0x78
10 => handle_level_irq+0xb4/0x160
11 => generic_handle_irq+0x34/0x44
12 => bcm2836_chained_handle_irq+0x38/0x50
13 => generic_handle_irq+0x34/0x44
14 => __handle_domain_irq+0x6c/0xc4
15 => bcm2836_arm_irqchip_handle_irq+0xac/0xb0
16 => __irq_usr+0x4c/0x60
17 => 0x4228e4c
18 chromium-browse-1436  [000] dnh.  9448.150005: sched_wakeup: comm=irq/92-mmc1 pid=65 prio=49 target_cpu=000
19 chromium-browse-1436  [000] dn..  9448.150010: schedule+0x10/0xa8 <-do_work_pending+0x38/0xcc
20 chromium-browse-1436  [000] dn..  9448.150016: <stack trace>
21 chromium-browse-1436  [000] d...  9448.150028: sched_switch: prev_comm=chromium-browse prev_pid=1436 prev_prio=120 prev_state=S ==> next_comm=irq/92-mmc1 next_pid=65 next_prio=49

조금 복잡해 보이는 로그의 실행 흐름은 다음과 같이 분류할 수 있습니다. 

1단계: 인터럽트 발생(01~02번째 줄)
92번 mmc1 인터럽트가 발생한 후 인터럽트 핸들러가 실행됐습니다.

2단계: IRQ 스레드를 깨움(03~18번째 줄)
인터럽트 핸들러에서 IRQ_WAKE_THREAD 을 반환해 등록한 IRQ 스레드를 깨웁니다.

3 단계 선점 스케줄링 실행(19~21번째 줄)
인터럽트 핸들링을 마무리 한 다음에 선점 스케줄링을 실행합니다.

4단계 IRQ 스레드 실행(19~21번째 줄)
바로 IRQ 스레드가 실행을 시작합니다.

1단계 인터럽트 발생 로그를 보겠습니다.
1  chromium-browse-1436  [000] d.h.  9448.149965: irq_handler_entry: irq=92 name=mmc1
2  chromium-browse-1436  [000] d.h.  9448.149972: irq_handler_exit: irq=92 ret=handled

1~2 번째 줄 로그를 보면 92번 mmc1 인터럽트가 발생한 동작을 확인할 수 있습니다. 

다음 2단계로 IRQ 스레드를 깨우는 로그를 보겠습니다.
3  chromium-browse-1436  [000] d.h.  9448.149982: ttwu_do_wakeup+0x10/0x1a4 <-ttwu_do_activate+0x80/0x84
4  chromium-browse-1436  [000] d.h.  9448.150003: <stack trace>
5  => wake_up_process+0x20/0x24
6  => __irq_wake_thread+0x70/0x74
7  => __handle_irq_event_percpu+0x84/0x224
8  => handle_irq_event_percpu+0x2c/0x68
9  => handle_irq_event+0x54/0x78
10 => handle_level_irq+0xb4/0x160
11 => generic_handle_irq+0x34/0x44
12 => bcm2836_chained_handle_irq+0x38/0x50
13 => generic_handle_irq+0x34/0x44
14 => __handle_domain_irq+0x6c/0xc4
15 => bcm2836_arm_irqchip_handle_irq+0xac/0xb0
16 => __irq_usr+0x4c/0x60
17 => 0x4228e4c

7 번째 줄 로그를 보면 __handle_irq_event_percpu() 함수에서 __irq_wake_thread() 함수를 호출해서 IRQ 스레드를 깨웁니다. 

함수 호출 흐름 다음에 보이는 sched_wakeup 이벤트 로그로 irq/92-mmc1 IRQ 스레드를 깨우는 동작을 확인할 수 있습니다.
18 chromium-browse-1436  [000] dnh.  9448.150005: sched_wakeup: comm=irq/92-mmc1 pid=65 prio=49 target_cpu=000

18 번째 줄 로그에서 놓치지 말아야 할 메시지는 ‘9448.150005’ 타임 스탬프 왼쪽에 있는 "dnh" 텍스트입니다. 이는 다음 사실을 알려줍니다. 

    이 중 h란 텍스트는 인터럽트 컨텍스트를 의미합니다.


필자가 92번 mmc1 인터럽트 핸들러가 IRQ_WAKE_THREAD 매크로를 반환해서 IRQ 스레드를 깨웠다고 해석했습니다. 

그 근거는 무엇일까요? 이해를 돕기 위해 소스 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/irq/handle.c]
1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
2 {
...
3 for_each_action_of_desc(desc, action) {
4 irqreturn_t res;
5
6 trace_irq_handler_entry(irq, action);
7 res = action->handler(irq, action->dev_id);
8 trace_irq_handler_exit(irq, action, res);
9
...
10 switch (res) {
11 case IRQ_WAKE_THREAD:
...
12 __irq_wake_thread(desc, action);

7 번째 줄 코드를 보겠습니다.
인터럽트 핸들러를 실행한 후 반환값을 res에 저장합니다.

다음 10 번째 줄 코드를 보면 switch/case 문을 실행하는데 res가 IRQ_WAKE_THREAD를 저장하기 때문에 __irq_wake_thread() 함수를 호출하는 것입니다.

물론 ftrace 로그에서 92번 인터럽트 핸들러가 실행했고 IRQ_WAKE_THREAD를 반환했다는 직접적인 메시지를 확인할 수는 없습니다. 하지만 ftrace 로그를 출력하는 코드 흐름을 이해하면 로그에서 출력하지 않는 동작도 머리로 떠올릴 수 있습니다.


다음 분석할 ftrace 로그는 3 단계인 선점 스케줄링 동작입니다. 
19 chromium-browse-1436  [000] dn..  9448.150010: schedule+0x10/0xa8 <-do_work_pending+0x38/0xcc
20 chromium-browse-1436  [000] dn..  9448.150016: <stack trace>
21 chromium-browse-1436  [000] d...  9448.150028: sched_switch: prev_comm=chromium-browse prev_pid=1436 prev_prio=120 prev_state=S ==> next_comm=irq/92-mmc1 next_pid=65 next_prio=49

19~20 번째 줄 로그를 보면 난데 없이 do_work_pending 함수에서 schedule() 함수를 호출했습니다. 조금 더 세밀하게 레이블 동작 흐름을 나열하면 다음과 같습니다.
__irq_usr()
ret_to_user_from_irq()
slow_work_pending()
do_work_pending()
schedule()

__irq_usr 이란 인터럽트 벡터에서 인터럽트 핸들링을 마무리한 다음 선점 스케줄링 조건을 점검합니다. 이전 소절에 다룬 내용을 떠올려 이 로그를 해석해볼까요? 

    현재 실행 중인 chromium-browse(pid:1436) 프로세스 struct thread_info 구조체 
    flags 값을 보고 선점 스케줄링할 조건인지 확인한다. 

프로세스 struct thread_info 구조체 preempt_count필드가 0이고 flags가 TIF_NEED_RESCHED(2) 이면 slow_work_pending(), do_work_pendig() 함수 그리고 schedule() 함수를 실행해서 선점 스케줄링을 실행 하는 것입니다.

다음 21 번째 줄 로그를 보면 바로 선점 스케줄링 동작을 확인할 수 있습니다.
21 chromium-browse-1436  [000] d...  9448.150028: sched_switch: prev_comm=chromium-browse prev_pid=1436 prev_prio=120 prev_state=S ==> next_comm=irq/92-mmc1 next_pid=65 next_prio=49

chromium-browse(pid:1436) 프로세스에서 irq/92-mmc1 프로세스로 컨텍스트 스위칭되는 것입니다.

21 번째 줄 로그를 유심히 보면 irq/92-mmc1 프로세스의 우선순위는 49임을 알 수 있습니다. 0~99 사이 우선순위 프로세스는 RT 클래스 스케줄러에서 실행한다고 유추할 수 있습니다.


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

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

Reference(프로세스 스케줄링)

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




[리눅스커널] 스케줄링: 스케줄링/디버깅: ftrace: sched_switch와 sched_wakeup 이벤트 소개 10장. 프로세스 스케줄링

리눅스 커널 ftrace 에서 sched_switch와 sched_wakeup 이벤트를 지원합니다. 각각 이벤트에 대해 소개하고 메시지를 분석하는 방법을 살펴보겠습니다.


리눅스 커널의 프로세스 동작을 처음 접하는 분들이 겪는 어려움이 있습니다. 실제 리눅스 시스템에서 얼마나 자주 프로세스가 스케줄링 되는지 확인할 수 없다는 것입니다. 그 이유는 간단합니다. 

    코드를 분석한 내용을 실제 리눅스 시스템에서 확인하지 않기 때문입니다. 

그래서 임베디드 리눅스를 개발할 때 모듈이나 드라이버 코드가 실행할 때 어떤 프로세스가 어떻게 스케줄링 되는지 확인하기도 어렵습니다.

리눅스 커널에서는 이런 의문을 해소시킬 수 있는 디버깅 기능을 지원합니다. 바로 ftrace입니다. ftrace 에는 프로세스 스케줄링 관련 동작을 트레이싱 할 수 있는 이벤트들이 준비돼 있습니다.
sched_switch
sched_wakeup


sched_switch, sched_wakeup 이벤트를 중심으로 리눅스 시스템에서 얼마나 자주 프로세스를 깨우고 스케줄링 하는지 점검합시다.
 
[그림 10.44] ftrace: sched_switch와 sched_wakeup 이벤트 소개 

위 그림에서 보이듯 sched_switch와 sched_wakeup 이벤트는 커널의 다음 동작을 트레이싱합니다.
sched_switch: 프로세스 스케줄링
sched_wakeup: 프로세스를 깨우기 동작

sched_switch와 sched_wakeup 이벤트를 키는 방법 알아보기

ftrace 메시지를 분석하는 방법을 소개하기 전에 ftrace sched_switch와 sched_wakeup 이벤트를 키는 방법부터 소개하겠습니다.

먼저 다음 명령어를 같이 볼까요? 
1 #!/bin/bash
3 echo 0 > /sys/kernel/debug/tracing/tracing_on
4 sleep 1
5 echo "tracing_off"
6
7 echo 0 > /sys/kernel/debug/tracing/events/enable
8 sleep 1
9 echo "events disabled"
10
11 echo nop > /sys/kernel/debug/tracing/current_tracer
12 sleep 1
13 echo "nop tracer enabled"
14
15 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
16 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
17 sleep 1
18 echo "event enabled"
19
20 echo 1 > /sys/kernel/debug/tracing/tracing_on
21 echo "tracing_on"

위 명령어 중 sched_switch와 sched_wakeup 이벤트를 키는 부분은 다음과 같습니다.
15 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
16 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable

위 셸 스크립트 코드를 sched_basic.sh 이란 이름으로 저장합니다. 이 후 다음 명령어를 입력해서 sched_basic.sh 스크립트를 실행하면 효율적으로 ftrace를 설정할 수 있습니다. 
root@raspberrypi:/home/pi# ./sched_basic.sh

셸 스크립트를 실행한 다음 10초 동안 기다립니다. 이때 라즈베리파이에서 특별히 브라우저나 다른 프로그램을 실행할 필요는 없습니다. 

10초가 지난 후 ftrace 로그를 추출합시다.
이를 위해 다음과 같은 스크립트를 작성한 후 get_ftrace.sh 이름으로 저장합시다.
#!/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

위 셸 스크립트를 실행하면 ftrace_log.c 파일이 생성됩니다. 이 파일이 ftrace 로그를 저장하고 있는 것입니다.

sched_switch와 sched_wakeup 이벤트 메시지 분석하기

먼저 ftrace 로그에서 sched_switch 이벤트 메시지를 소개합니다.
kworker/2:1-1106  [002] d...  7831.602384: sched_switch: prev_comm=kworker/2:1 prev_pid=1106 prev_prio=120 prev_state=T ==> next_comm=ksoftirqd/2 next_pid=19 next_prio=120

sched_switch 이벤트 로그는 대부분 위와 같은 패턴으로 프로세스 스케줄링 동작을 표현합니다.

이제 ftrace 로그의 의미를 차근차근 분석해볼까요? 
 
[그림 10.45] ftrace 로그 prev와 next 메시지의 의미

sched_switch 메시지 세부 내용은 다음과 같습니다.
 
[그림 10.46] ftrace: sched_switch 이벤트 메시지 세부 내용
 


여기서 다음과 같은 의문이 생깁니다.

    ftrace sched_switch 이벤트를 메시지 포멧을 어디서 확인할 수 있을까?

sched_switch 이벤트를 출력하는 포멧은 다음 해더 파일에서 확인할 수 있습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/trace/events/sched.h]
01 TRACE_EVENT(sched_switch,
02
03 TP_PROTO(bool preempt,
04 struct task_struct *prev,
05 struct task_struct *next),
...
06 TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d",
07 __entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
08
09 (__entry->prev_state & (TASK_REPORT_MAX - 1)) ?
10   __print_flags(__entry->prev_state & (TASK_REPORT_MAX - 1), "|",
11 { 0x01, "S" }, { 0x02, "D" }, { 0x04, "T" },
12 { 0x08, "t" }, { 0x10, "X" }, { 0x20, "Z" },
13 { 0x40, "P" }, { 0x80, "I" }) :
14   "R",
15
16 __entry->prev_state & TASK_REPORT_MAX ? "+" : "",
17 __entry->next_comm, __entry->next_pid, __entry->next_prio)
18 );

prev 프로세스와 next 프로세스의 태스트 디스크립터를 읽어서 각각 필드를 출력하는 것입니다.

이 중에 프로세스 상태를 출력하는 포멧은 다음 코드입니다.
09 (__entry->prev_state & (TASK_REPORT_MAX - 1)) ?
10   __print_flags(__entry->prev_state & (TASK_REPORT_MAX - 1), "|",
11 { 0x01, "S" }, { 0x02, "D" }, { 0x04, "T" },
12 { 0x08, "t" }, { 0x10, "X" }, { 0x20, "Z" },
13 { 0x40, "P" }, { 0x80, "I" }) :

프로세스 태스크 디스크립터 state 필드 값을 읽어서 알파벳으로 출력하는 것입니다.
각 알파벳은 다음 상태 정보에 매핑할 수 있습니다.
 
[그림 10.47] ftrace: 프로세스 상태 메시지와 프로세스 상태 코드 관계


먼저 가장 왼쪽 부분에 있는 "kworker/2:1-1106" 메시지를 보겠습니다.
kworker/2:1-1106  [002] d...  7831.602384

ftrace 로그를 실행하는 프로세스 정보입니다.  

    프로세스 이름은 "kworker/2:1" 이고 PID는 1106입니다. 

프로세스 이름으로 보아 워크를 처리하는 워커 스레드임을 알 수 있습니다. 또한 실행 중인 CPU 번호는 2이고 타임스탬프는 7831.602384입니다.

다음 메시지는 스케줄링 되기 전 실행했던 프로세스 정보를 출력합니다.
prev_comm=kworker/2:1 prev_pid=1106 prev_prio=120 prev_state=T

"prev_state=T" 메시지로 보아 프로세스 상태가 __TASK_STOPPED 임을 알 수 있습니다.

분석 내용을 요약하면 스케줄링으로 CPU를 비울 prev 프로세스 정보는 다음과 같습니다. 

    프로세스 이름은 "kworker/2:1" 이고 PID는 1106입니다. 또한 프로세스 우선순위가 
      120이고 프로세스 상태는 T이다.

다음은 스케줄링 동작으로 다음에 실행될 프로세스 정보입니다.
next_comm=ksoftirqd/2 next_pid=19 next_prio=120

프로세스 이름은 "ksoftirqd/2" 이고 PID는 19입니다. 또한 프로세스 우선순위가 120입니다.

정리하면 "kworker/2:1" 프로세스에서 "ksoftirqd/2" 프로세스로 스케줄링하는 동작을 출력하는 메시지입니다.

다음 sched_wakeup 이벤트 로그를 소개합니다. 
1 lxpanel-718 [002] d... 7831.739767: sched_wakeup: comm=Xorg pid=552 prio=120 target_cpu=002
2 lxpanel-718   [002] d...  7831.739824: sched_switch: prev_comm=lxpanel prev_pid=718 prev_prio=120 prev_state=D ==> next_comm=Xorg next_pid=552 next_prio=120


1 번째 줄 메시지가 sched_wakeup 이벤트인데 2 번째 줄에 sched_switch 이벤트를 추가했습니다. sched_wakeup 이벤트는 sched_switch 이벤트와 같이 분석하는 경우가 많기 때문입니다.


먼저 1 번째 줄 메시지를 봅시다.
1 lxpanel-718 [002] d... 7831.739767: sched_wakeup: comm=Xorg pid=552 prio=120 target_cpu=002

위 메시지는 다음 사실을 말해줍니다. 

    PID가 552 이고 우선순위가 120인 "Xorg" 프로세스를 깨우는 동작입니다.

여기서 "Xorg" 프로세스를 누가 깨울까요? ftrace 로그에서 가장 왼쪽에 있는 부분이 현재 코드를 실행하는 프로세스 정보입니다.  

    "Xorg" 프로세스는 lxpanel(pid: 718) 프로세스가 깨웁니다.

이 메시지로 프로세스는 자신을 깨울 수 없다는 사실을 파악할 수 있습니다.

ftrace로 sched_switch/sched_wakeup 이벤트 로그에 대해 알아봤으니 이번에 sched_switch/sched_wakeup 이벤트 로그를 출력하는 리눅스 커널 코드를 살펴봅시다.

sched_switch와 sched_wakeup 이벤트 출력 함수 분석하기

ftrace 메시지를 보면 그 내용을 해석하면서 해당 ftrace 이벤트를 메시지를 커널 어느 코드에서 출력하는지 확인하면 더 많은 것을 배울 수 있습니다. sched_switch 이벤트 로그를 출력하는 코드를 확인해볼까요? 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c]
1 static void __sched notrace __schedule(bool preempt)
2 {
3 struct task_struct *prev, *next;
...
4
5 if (likely(prev != next)) {
6 rq->nr_switches++;
7 rq->curr = next;
8
9 ++*switch_count;
10
11 trace_sched_switch(preempt, prev, next);
12
13 rq = context_switch(rq, prev, next, &rf);
14 } else {
15 rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
16 rq_unlock_irq(rq, &rf);
17 }
18
19 balance_callback(rq);
20 }

__schedule() 함수 13 번째 줄을 보면 context_switch() 함수를 호출해서 컨텍스트 스위칭을 실행합니다. 13 번째 줄 바로 위 11 번째 줄에서 trace_sched_switch() 함수를 실행할 때 sched_switch 이벤트 로그를 출력합니다.


여기서 한 가지 의문이 생깁니다. 

    ftrace 이벤트를 출력하는 함수 이름에 어떤 패턴이 있지 않을까?

ftrace 이벤트를 보면 리눅스 커널 소스 코드 어디서 해당 메시지를 출력하는지 파악할 수 있습니다.

ftrace 이벤트를 출력하는 함수 이름은 다음과 같은 형식입니다. 따라서 sched_switch 이벤트를 출력하는 함수는 trace_sched_switch() 함수입니다.
trace_ + ftrace 이벤트 이름

마찬가지로 sched_wakeup 이벤트를 출력하는 함수는 trace_sched_wakeup() 란 사실을 유추할 수 있습니다.


이어서 sched_wakeup 이벤트를 출력하는 코드는 다음과 같습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c]
1 static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
2    struct rq_flags *rf)
3 {
4 check_preempt_curr(rq, p, wake_flags);
5 p->state = TASK_RUNNING;
6 trace_sched_wakeup(p);

ttwu_do_wakeup() 함수 6 번째 줄 코드에서 trace_sched_wakeup() 함수를 호출할 때 sched_wakeup 이벤트를 출력하는 것입니다.

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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: 선점 스케줄링(Preemptive Scheduling)이란 무엇일까? 10장. 프로세스 스케줄링

선점 스케줄링은 다음과 같이 정의내릴 수 있습니다.  

    CPU에서 실행 중인 프로세스를 비우고 새로운 프로세스를 CPU에서 실행시킴
 
선점 스케줄링에 대한 이해를 돕기 위해 야구에서 투수 교체를 하는 과정을 예를 들겠습니다.

마운드에 A란 투수가 있다고 가정합시다. A란 투수는 열심히 공을 던지고 있습니다. 감독은 투수가 마운드에서 제대로 공을 던지고 있는지 계속 관찰합니다. 제구는 좋은지 구속은 제대로 나오고 있는지 점검합니다.

그런데 시간이 흘러 B, C와 D란 투수가 자신이 공을 던지고 싶다는 의사와 함께 불펜에서 몸을 풀기 시작했습니다. B, C와 D란 투수도 A와 같은 기량의 투수입니다.

감독은 A와 B, C, D 투수를 비교합니다. 만약 B, C, D 투수 중에 A보다 잘 던진다고(우선순위가 높으면) 감독은 어떤 결정은 내릴까요? A 투수를 강판시키고 B란 투수를 마운드에 올립니다.

여기서 A와 B 투수를 프로세스, 감독을 스케줄러 그리고 마운드를 CPU로 바꿔서 생각해 봅시다.

현재 A란 프로세스가 CPU를 점유하면서 실행 중이라고 가정하겠습니다.
그런데 런큐에 Enqueue한 후 실행을 기다리는 B, C, D란 프로세스가 있습니다.   

스케줄러는 A와 B, C, D 프로세스와 우선순위를 비교합니다. 만약 A 프로세스보다 B란 프로세스가 우선순위가 높다고 판단하면 A란 프로세스를 CPU에서 비우고 B란 프로세스를 CPU에 실행 시킵니다.

이를 선점 스케줄링이라고 말합니다.


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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: 런큐에 등록된 프로세스 자료구조 확인하기 10장. 프로세스 스케줄링

이번 소절에서는 런큐 자료구조를 소개합니다. 

런큐에 Enqueue한 프로세스 리스트 확인하기

일반 프로세스가 런큐에 Enqueue를 하면 런큐 구조체 struct rq 필드 중 연결리스트인 cfs_tasks에 자신의 태스크 디스크립터 주소(&se->group_node)를 등록합니다.

프로세스 태스크 디스크립터인 struct task_struct 구조체 &se->group_node 필드 주소를 struct rq 런큐 구조체 cfs_tasks 연결 리스트 필드에 저장하는 것입니다.

이 관계를 다음 그림을 보면서 살펴보겠습니다.
 
 [그림 10.25] 런큐에 Enqueue한 프로세스 연결 리스트 구조

여기서 한 가지 의문이 생깁니다. 

    어디서 프로세스 태스크 디스크립터를 런큐 cfs_tasks 필드에 등록할까?

정답은 언제나 소스 코드에 있습니다. 
다음 account_entity_enqueue() 함수를 같이 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/fair.c]
1 static void
2 account_entity_enqueue(struct cfs_rq *cfs_rq, struct sched_entity *se)
3 {
4 update_load_add(&cfs_rq->load, se->load.weight);
5 if (!parent_entity(se))
6  update_load_add(&rq_of(cfs_rq)->load, se->load.weight);
7 #ifdef CONFIG_SMP
8 if (entity_is_task(se)) {
9  struct rq *rq = rq_of(cfs_rq);
10
11  account_numa_enqueue(rq, task_of(se));
12  list_add(&se->group_node, &rq->cfs_tasks);
13 }
#endif
14 cfs_rq->nr_running++;
15 }

위 코드에서 눈여겨볼 부분은 9~12 번째 줄입니다.

9 번째 줄은 cfs_rq란 입력으로 런큐 주소를 읽는 동작입니다.
9  struct rq *rq = rq_of(cfs_rq);

다음 12번째 줄 코드를 보겠습니다.
12  list_add(&se->group_node, &rq->cfs_tasks);

12번째 줄 코드를 실행하면, struct task_struct->se->group_node 주소를 &rq->cfs_tasks 주소에 등록합니다.


여기서 'struct task_struct->se->group_node' 자료구조에서 struct task_struct->se의 정체는 무엇일까요?

다음 코드와 같이 account_entity_enqueue() 함수 2 번째 인자인 struct sched_entity *se 는 런큐에 Enqueue하려는 프로세스 태스크 디스크립터 se 필드 주소를 담고 있습니다.
1 static void
2 account_entity_enqueue(struct cfs_rq *cfs_rq, struct sched_entity *se)

여기서 한 가지 의문이 생깁니다. 

    struct sched_entity *se 인자는 어디서 확인할 수 있을까?

다음 struct task_struct 구조체 선언부와 같이 프로세스 태스크 디스크립터 필드 중에 se가 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h]
01 struct task_struct {
...
02 const struct sched_class *sched_class;
03 struct sched_entity  se;

03번째 줄과 같이 struct sched_entity 타입입니다.


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

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

Reference(프로세스 스케줄링)

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




[리눅스커널] 스케줄링: 런큐에 접근하는 함수 - cpu_rq/this_rq 10장. 프로세스 스케줄링

커널에서는 런큐에 접근할 수 있는 인터페이스 함수를 제공합니다.
cpu_rq()
this_rq() 

cpu_rq() 함수 분석하기

cpu_rq() 함수 코드부터 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 #define cpu_rq(cpu)  (&per_cpu(runqueues, (cpu)))

cpu_rq() 함수 구현부를 보면 per-cpu 타입 runqueues 변수에서 CPU 오프셋을 적용한 주소에 접근하는 코드를 볼 수 있습니다.

커널 스케줄러 common 코드를 보면 cpu_rq() 함수를 써서 런큐 주소를 얻어오는 패턴을 자주 볼 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/fair.c]
1 static unsigned long scale_rt_capacity(int cpu)
2 {
3 struct rq *rq = cpu_rq(cpu);

위 코드에서 cpu 번호에 해당하는 런큐 주소를 rq 포인터 주소에 저장합니다.

this_rq() 함수 분석하기

이번엔 this_rq() 함수를 소개합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 #define this_rq()  this_cpu_ptr(&runqueues)

코드 구현부와 같이 per-cpu 타입 runqueues 런큐 변수를 cpu 오프셋을 적용해서 주소를 얻어옵니다. this_rq() 함수는 cpu 번호를 지정 안해도 현재 실행 중인 CPU 번호의 런큐 주소를 반환합니다.

다음은 this_rq() 함수를 쓰는 예제 코드를 소개합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 static int migration_cpu_stop(void *data)
2 {
3 struct migration_arg *arg = data;
4 struct task_struct *p = arg->task;
5 struct rq *rq = this_rq();

5 번째 줄 코드와 같이 this_rq() 함수를 써서 런큐 주소를 읽어서 rq이란 포인터에 저장합니다.


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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: 런큐 runqueues 변수에 대해서 10장. 프로세스 스케줄링

런큐는 per-cpu 타입 전역 변수인 runqueues로 각각 CPU 갯수 별로 프로세스 스케줄링 정보를 저장합니다.

runqueues 변수 선언부 확인하기

먼저 per-cpu 타입 런큐 전역 변수를 소개합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

런큐는 runqueues 이란 per-cpu 타입 전역 변수로 관리합니다. 따라서 per-cpu 별 런큐 주소를 얻기 위해서 다음과 같은 cpu_rq() 함수와 this_irq() 함수를 호출해야 합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
#define cpu_rq(cpu)  (&per_cpu(runqueues, (cpu)))
#define this_rq()  this_cpu_ptr(&runqueues)

runqueues 변수 percpu 구조 확인하기

라즈베리파이에서 확인한 런큐 디버깅 정보와 함께 percpu 타입 변수인 runqueues 변수에 대해서 조금 더 짚어 보겠습니다.
 
  [그림 10.24] 런큐 runqueues 변수 구조

먼저 runqueues 란 전역 변수 주소는 0x80B8CD40입니다.
  (struct rq *) &runqueues = 0x80B8CD40 

다음 계산식으로 per-cpu 별 런큐 위치를 확인할 수 있습니다.
CPU0: runqueues
0xB0E69D40 = 0x80B8CD40 + 0x302DD000 = &runqueues + __per_cpu_offset[0]

CPU1: runqueues
0xB0E75D40 = 0x80B8CD40 + 0x302E9000 = &runqueues + __per_cpu_offset[1]

CPU2: runqueues
0xB0E81D40 = 0x80B8CD40 + 0x302F5000 = &runqueues + __per_cpu_offset[2]

CPU3: runqueues
0xB0E8DD40 = 0x80B8CD40 + 0x30301000 = &runqueues + __per_cpu_offset[3]

per-cpu 타입 변수인 runqueues 전역 변수 주소에 __per_cpu_offset[4] 에 저장된 percpu 오프셋을 더하면 각 CPU별로 관리하는 런큐 주소를 알 수 있습니다.

__per_cpu_offset 배열 값은 CPU 갯수 만큼 설정하며 각 배열에 저장된 값은 다음과 같습니다.
  (static long unsigned int [4]) __per_cpu_offset = (
    [0x0] = 0x302DD000,
    [0x1] = 0x302E9000,
    [0x2] = 0x302F5000,
    [0x3] = 0x30301000,


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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: 런큐 자료구조 struct rq 소개 10장. 프로세스 스케줄링

런큐는 실행 대기와 CPU에서 실행 중인 프로세스를 관리하는 스케줄링 핵심 자료구조입니다.
프로세스가 CPU에서 실행을 하려면 먼저 런큐에 Enqueue를 해야 합니다. 스케줄러는 런큐에 Enqueue한 프로세스 중에 우선순위를 계산해서 다음 프로세스를 선택하기 때문입니다. 런큐의 특징은 다음과 같습니다. 
percpu 타입 전역 변수
RT, CFS, Deadline 서브 런큐를 관리
실행 요청을 한 프로세스가 Enqueue됨
CPU를 점유하면서 실행 중인 current 프로세스 관리

런큐에 대해서 알아보기 전에 런큐 단어의 뜻부터 생각해보겠습니다.
runqueue는 run+queue 합성어로 실행큐라는 의미입니다. 달리 보면 프로세스가 실행하기 위한 큐라고 볼 수 있습니다.

스케줄러는 런큐와 자주 대화를 나눕니다. 

    CPU를 점유하고 있는 프로세스는 누구니?
    현재 런큐에서 대기 중인 프로세스는 갯수를 알려줘

이렇게 스케줄러는 런큐 자료구조에 자주 접근해서 실행 대기 상태 프로세스 목록을 확인하고 스케줄링 프로파일 정보를 확인합니다. 따라서 리눅스 시스템에서 프로세스 실행 흐름에 대한 전반적인 정보를 런큐가 저장합니다.

태스크 디스크립터만큼 중요한 자료구조가 런큐입니다. 런큐 자료구조를 보면 실행 대기 상태 프로세스와 CPU를 점유하면서 실행 중인 프로세스 목록까지 확인할 수 있기 때문입니다.

먼저 런큐 자료구조부터 살펴볼까요?

런큐 자료구조 struct rq 소개

런큐 구조체는 struct rq 이며 이 구조체 필드에 프로세스 스케줄링에 관련된 데이터를 저장합니다.

struct rq 구조체 선언부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
struct rq {
raw_spinlock_t lock;
unsigned int nr_running;
...
struct load_weight load;
unsigned long nr_load_updates;
u64 nr_switches;

struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
...
unsigned long nr_uninterruptible;

struct task_struct *curr, *idle, *stop;
...
};

다음 테이블에서 struct rq 구조체 필드 중 중요한 내용을 확인할 수 있습니다.
타입 필드 런큐 자료구조를 바꿀 때 경쟁 조건을 피하기 위한 락
raw_spinlock_t lock 런큐 자료구조를 변경할 때 경쟁 조건을 피하기 위한 락
unsigned int nr_running 런큐에 Enqueue된 모든 프로세스 갯수
u64 nr_switches 컨텍스트 스위칭을 수행한 개수
struct cfs_rq cfs CFS 런큐
struct rt_rq rt RT(실시간) 런큐
unsigned long nr_uninterruptible 런큐에 있는 태스크 중 TASK_UNINTERRUPTIBLE 상태 프로세스 갯수 
struct task_struct * curr 해당 런큐에서 CPU를 점유하면서 실행 중인 프로세스의 태스트 디스크립터
struct task_struct * idle 런큐 idle 프로세스의 태스크 디스크립터
struct list_head cfs_tasks CFS 런큐에 Enqueue된 모든 일반 프로세스의 연결 리스트

런큐 자료구조인 struct rq 필드를 소개했습니다. 다음 소절에서는 라즈베리파이에서 확보한 코어 덤프로 런큐 자료구조를 직접 확인해 보는 시간을 갖겠습니다.

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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: 프로세스는 스케줄러 클래스로 스케줄러 세부 함수를 어떻게 호출할까? 10장. 프로세스 스케줄링

스케줄러 클래스 자료구조는 스케줄러 세부 함수 동작을 모듈화한 함수 포인터를 저장하는 필드로 구성돼 있습니다. 이번 소절에는 스케줄러 클래스 메소드들 중 enqueue_task()와 dequeue_task() 함수를 호출하면 어떤 방식으로 스케줄러 클래스 세부 함수를 호출하는지 분석합니다.

enqueue_task() 함수 분석하기

먼저 살펴볼 함수는 enqueue_task() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
2 {
3 if (!(flags & ENQUEUE_NOCLOCK))
4 update_rq_clock(rq);
5
6 if (!(flags & ENQUEUE_RESTORE))
7 sched_info_queued(rq, p);
8
9 p->sched_class->enqueue_task(rq, p, flags);
10 }

위 코드는 스케줄러 클래스를 배워야 하는 이유를 알려주는 좋은 예시입니다. 어떤 분이 여러분에게 묻습니다. 

    9번째 줄 코드에서 어느 함수를 호출할까?

스케줄러 클래스를 모른다면 대답하기 어려운 질문입니다. 여러분은 스케줄러 클래스를 배웠으니 다음과 같이 대답할 수 있는 역량이 쌓였습니다. 

    프로세스 태스크 디스크립터에 등록된 enqueue 스케줄러 클래스 메소드를 호출한다.

enqueue_task() 함수는 프로세스를 런큐에 큐잉하는 동작을 수행합니다.

먼저 함수에 전달하는 인자부터 점검합시다. 
첫 번째 인자: struct rq *rq는 런큐 주소
두 번째 인자: struct task_struct *p는 런큐에 큐잉하려는 프로세스 태스크 디스크립터 주소 

다음 9 번째 줄 코드를 보겠습니다.
9 p->sched_class->enqueue_task(rq, p, flags);

두 번째 인자인 p(struct task_struct) 필드인 sched_class에 접근해 enqueue_task 필드가 저장한 함수를 호출합니다. 여기서 어떤 함수를 호출할까요?

만약 이 코드를 실행하는 프로세스의 스케줄러 클래스가 CFS 스케줄러로 지정돼 있으면 CFS 스케줄러 클래스로 지정된 enqueue_task_fair() 함수를 호출합니다. RT 클래스 스케줄러를 등록했을 경우 enqueue_task_rt() 함수를 호출합니다.

dequeue_task() 함수 분석하기

프로세스를 런큐에 Enqueue하는 동작을 알아봤으니 이번에는 프로세스를 런큐에서 Dequeue하는 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 static inline void dequeue_task(struct rq *rq, struct task_struct *p, int flags)
2 {
3 if (!(flags & DEQUEUE_NOCLOCK))
4 update_rq_clock(rq);
5
6 if (!(flags & DEQUEUE_SAVE))
7 sched_info_dequeued(rq, p);
8
9 p->sched_class->dequeue_task(rq, p, flags);
10 }

dequeue_task() 함수 9 번째 줄 코드를 보면 프로세스의 스케줄러 클래스 필드에서 dequeue_task() 메소드를 호출합니다.  

    여기서 어떤 함수를 호출할까요?

만약 이 코드를 실행하는 프로세스의 스케줄러 클래스가 CFS 스케줄러로 지정돼 있으면 CFS 스케줄러 클래스로 지정된 dequeue_task_fair() 함수를 호출합니다. RT 클래스 스케줄러인 경우 dequeue_task_rt() 함수를 호출합니다.

여기까지 배운 내용을 정리하면 다음과 같습니다.
프로세스는 생성되면서 스케줄러 클래스를 부모 프로세스로부터 물려 받거나 우선순위에 따라 스케줄러 클래스를 바꿉니다. 모든 프로세스는 스케줄러 클래스에 등록한 상태로 실행합니다.


필자가 진행한 세미나 시간에 받았던 질문을 공개합니다. 

    리눅스 시스템에서 전체 프로세스 중에 RT 클래스 프로세스와 CFS 클래스 프로세스 
   비율은 어떻게 될까?

수 많은 리눅스 코어 덤프를 관찰한 결과 대부분 99% 정도 프로세스들은 CFS 스케줄러 클래스로 등록되서 실행하고 나머지 1% 프로세스들이 RT 스케줄러 클래스로 등록해 실행합니다. 리눅스 커널 기본 스케줄러 정책은 일반 프로세스는 CFS 스케줄러 클래스로 실행하고 우선순위가 높은 프로세스들은 RT 클래스 스케줄러로 빠른 시간 내 실행하는 것입니다.

대부분 일반 프로세스들이 CFS 스케줄러 클래스로 실행하므로 입출력 중심으로 처리하는 프로세스들의 실행 속도가 빠릅니다.

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

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

Reference(프로세스 스케줄링)

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




[리눅스커널] 스케줄링: 프로세스는 스케줄러 클래스를 어떻게 등록할까? 10장. 프로세스 스케줄링

이전 소절에서는 스케줄러 클래스와 5가지 스케줄러 클래스에 대해서 살펴봤습니다. 이제부터 프로세스 입장에서 스케줄러 클래스를 통해 세부 스케줄러 함수를 호출하는 과정에 대해 알아보겠습니다. 

    스케줄러 클래스를 통해 세부 스케줄러 함수를 실행하는 주인공은 누구일까? 

정답은 프로세스입니다. 프로세스는 스케줄러 클래스를 통해 스케줄러 세부 함수를 실행할 수 있습니다. 이를 위해 프로세스에 스케줄러 클래스를 등록해야 합니다. 

프로세스 입장에선 스케줄러 클래스 설정은 크게 2가지 단계로 분류할 수 있습니다.

1단계: 스케줄러 클래스 설정

프로세스는 생성될 때 부모 프로세스로부터 스케줄러 클래스를 함께 물려 받습니다. 이해를 돕기 위해 프로세스가 생성되는 과정에서 호출되는 sched_fork() 함수를 소개합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
01 int sched_fork(unsigned long clone_flags, struct task_struct *p)
02 {
03 unsigned long flags;
...
04 if (dl_prio(p->prio))
05 return -EAGAIN;
06 else if (rt_prio(p->prio))
07 p->sched_class = &rt_sched_class;
08 else
09 p->sched_class = &fair_sched_class; 

이 함수의 핵심 동작은 다음과 같습니다. 
태스크 디스크립터 prio 필드에 저장된 우선순위에 따라 스케줄러 클래스를 지정한다.

04~09번째 줄 코드를 보겠습니다.
dl_prio() 함수와 rt_prio() 함수는 p->prio 필드에 저장된 우선순위를 보고 데드라인 혹은 RT 클래스 스케줄러 우선순위인지 식별합니다.

rt_prio() 함수 코드를 잠깐 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched/rt.h]
1 static inline int rt_prio(int prio)
2 {
3 if (unlikely(prio < MAX_RT_PRIO))
4 return 1;
5 return 0;
6 }

MAX_RT_PRIO는 100으로 선언돼 있습니다.
우선순위가 100보다 작을 때는 1, 나머지 경우 0을 반환합니다.

5~12 번째 줄 코드를 보면 p->sched_class란 필드에 프로세스 우선순위에 따라 각각 스케줄러 클래스를 등록합니다.

2단계: 스케줄러 클래스 변경

프로세스가 실행 도중 우선순위 높혀서 실행해야 할 때가 있습니다. 이때 스케줄러 클래스를 바꿔야할 필요가 있습니다.

이 경우 __setscheduler() 함수를 호출해서 스케줄러 클래스를 바꿉니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 static void __setscheduler(struct rq *rq, struct task_struct *p,
2    const struct sched_attr *attr, bool keep_boost)
3 {
4 __setscheduler_params(p, attr);
5
6 p->prio = normal_prio(p);
7 if (keep_boost)
8 p->prio = rt_effective_prio(p, p->prio);
9
10 if (dl_prio(p->prio))
11 p->sched_class = &dl_sched_class;
12 else if (rt_prio(p->prio))
13 p->sched_class = &rt_sched_class;
14 else
15 p->sched_class = &fair_sched_class;
16 }

이 함수는 태스크 디스크립터 prio 필드에 저장된 우선순위에 따라 스케줄러 클래스를 지정하는 역할을 수행합니다.

10~15 번째 줄 코드는 프로세스 우선순위에 따라 p->sched_class 필드에 각각 스케줄러 클래스를 등록합니다.

정리하면 모든 프로세스의 태스크 디스크립터에는 스케줄러 클래스 정보가 있고 프로세스 우선순위에 따라 스케줄러 클래스를 동적으로 바꿀 수 있는 것입니다.

스케줄러 클래스 자료 구조를 태스크 디스크립터에서 확인하기
마지막으로 태스크 디스크립터에서 스케줄러 클래스를 확인하는 디버깅 시간을 갖겠습니다.

다음 태스크 디스크립터의 주인공은 라즈베리파이에서 92번 인터럽트 후반부 처리를 담당하는 IRQ 스레드인 "irq/92-mmc1" 프로세스입니다.
1  (struct task_struct *) (struct task_struct*)0xB01E8740 
2    (long int) state = 1  
3    (void *) stack = 0xB0260000,
...
4    (int) prio = 49  
5    (int) static_prio = 120  
6    (int) normal_prio = 49  
7    (unsigned int) rt_priority = 50  
...
8    (struct sched_class *) sched_class = 0x80802698 = rt_sched_class -> (
9       (struct sched_class *) next = 0x80802608 = fair_sched_class,
10      (void (*)()) enqueue_task = 0x80159258 = enqueue_task_rt,
11      (void (*)()) dequeue_task = 0x80158CFC = dequeue_task_rt,
12      (void (*)()) yield_task = 0x80156D94 = yield_task_rt,
...
13      (void (*)()) switched_to = 0x80157E18 = switched_to_rt,
14      (void (*)()) prio_changed = 0x80157D74 = prio_changed_rt,
15      (unsigned int (*)()) get_rr_interval = 0x80156DB8 = get_rr_interval_rt,
16      (void (*)()) update_curr = 0x80158964 = update_curr_rt,

8 번째 줄 필드를 보면 struct task_struct 필드 중 하나인 sched_class는 rt_sched_class 전역 변수 주소를 가리키고 있습니다. 이는 "irq/92-mmc1" 프로세스가 RT 클래스 스케줄러를 등록했다는 의미입니다.

다음에 살펴볼 태스크 디스크립터는 “kworker/0:0H” 프로세스입니다. 프로세스 이름으로 워커 스레드임을 알 수 있습니다.
1 (struct task_struct *) (struct task_struct*)0xB1619D00 
2   (long int) state = 1 = 0x1,
3   (void *) stack = 0xB1634000 = ,
4   (atomic_t) usage = ((int) counter = 2 = 0x2),
5   (unsigned int) flags = 0x04208060,
...
6   (int) on_rq = 0 
7   (int) prio = 100 
8   (int) static_prio = 100 
9   (int) normal_prio = 100 
10  (unsigned int) rt_priority = 0 = 0x0,
11  (struct sched_class *) sched_class = 0x80802608 = fair_sched_class -> (
12     (struct sched_class *) next = 0x80802528 = idle_sched_class,
13     (void (*)()) enqueue_task = 0x80150B0C = enqueue_task_fair,
14     (void (*)()) dequeue_task = 0x8014F310 = dequeue_task_fair,
15     (void (*)()) yield_task = 0x8014F248 = yield_task_fair,
...
16     (void (*)()) switched_to = 0x80151C88 = switched_to_fair,
17     (void (*)()) prio_changed = 0x80151C3C = prio_changed_fair,
18     (unsigned int (*)()) get_rr_interval = 0x8014C5D4 = get_rr_interval_fair,
19     (void (*)()) update_curr = 0x8014DB48 = update_curr_fair,

11 번째 줄을 보면 struct task_struct 필드 중 하나인 sched_class는 fair_sched_class 전역 변수 주소를 가리키고 있습니다. 이는 “kworker/0:0H” 프로세스 프로세스가 스케줄러 클래스로 CFS 클래스 스케줄러를 등록했다는 의미입니다.

다음 소절에서는 스케줄러 클래스를 이용해서 프로세스가 스케줄러 클래스 함수를 호출하는 과정을 소개합니다.


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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: 5가지 스케줄러 클래스란 무엇일까? 10장. 프로세스 스케줄링

다음 코드와 같이 struct sched_class 구조체로 스케줄러 클래스를 정의할 수 있습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 extern const struct sched_class stop_sched_class;
2 extern const struct sched_class dl_sched_class;
3 extern const struct sched_class rt_sched_class;
4 extern const struct sched_class fair_sched_class;
5 extern const struct sched_class idle_sched_class;

1~5 번째 줄에서 볼 수 있는 5가지 전역 변수를 스케줄러 클래스라고 합니다. 각각 전역 변수를 열어보면 struct sched_class 구조체에 스케줄러 별로 실행하는 함수를 볼 수 있습니다. 예를 들어 RT Class 스케줄러 클래스 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/rt.c]
1 const struct sched_class rt_sched_class = {
2 .next = &fair_sched_class,
3 .enqueue_task = enqueue_task_rt,
4 .dequeue_task = dequeue_task_rt,
5 .yield_task = yield_task_rt,
6
7 .check_preempt_curr = check_preempt_curr_rt,
8
9 .pick_next_task = pick_next_task_rt,
10 .put_prev_task = put_prev_task_rt,
...
11 .get_rr_interval = get_rr_interval_rt,
12
13 .prio_changed = prio_changed_rt,
14 .switched_to = switched_to_rt,
15
16 .update_curr = update_curr_rt,
17 };

위 코드를 보면 struct sched_class 구조체 필드에 RT 스케줄러 세부 함수를 선언했습니다. 이렇게 스케줄러 동작을 모듈화한 struct sched_class 구조체 필드에 스케줄러별 세부 함수를 등록한 것이 스케줄러 클래스라고 볼 수 있습니다.

다음 그림에서 5가지 스케줄러 클래스와 세부 함수를 확인할 수 있습니다.
 
[그림 10.21] 스케줄러 클래스 별 세부 연산 함수 

정리하면 스케줄러 클래스는 스케줄러 세부 동작을 표현한 인터페이스용 객체라고 볼 수 있습니다.

스케줄러 클래스 우선순위에 대해서
스케줄러 클래스 사이에 우선순위가 있습니다.
 
[그림 10.22] 스케줄러 클래스 별 우선순위 

우선순위가 가장 큰 스케줄러 클래스는 stop_sched_class인데 오른쪽 화살표 방향 순서 우선순위가 줄어듭니다. RT 스케줄러 클래스(rt_sched_class)는 CFS 스케줄러 클래스(fair_sched_class)보다 우선순위가 높습니다. 

    스케줄러에서 스케줄러 클래스 우선순위에 따라 스케줄러 클래스를 처리하는 코드는 
   무엇일까? 

정답은 for_each_class() 함수입니다. 스케줄러 클래스를 순회할 때 for_each_class() 함수를 호출합니다.

for_each_class() 함수 구현부를 보면 stop_sched_class 전역변수로 시작해서 class->next에 지정된 다음 우선순위 스케줄러 클래스 전역 변수에 접근합니다. 우선순위 방향으로 연결된 스케줄러 클래스를 순차적으로 호출하는 동작입니다.

스케줄러 클래스 별 struct sched_class 구조체 확인하기

이해를 돕기 위해 stop_sched_class 부터 idle_sched_class 전역 변수 선언부를 보겠습니다.
1 const struct sched_class stop_sched_class = {
2 .next = &dl_sched_class,
...
3 const struct sched_class dl_sched_class = {
4 .next = &rt_sched_class,
...
5 const struct sched_class rt_sched_class = {
6 .next = &fair_sched_class,
...
7 const struct sched_class fair_sched_class = {
8 .next = &idle_sched_class,
...
9 const struct sched_class idle_sched_class = {
10 /* .next is NULL */

스케줄러 클래스 구조체인 struct sched_class 의 첫 번째 필드는 struct sched_class next입니다. next 필드에 다음 우선순위 스케줄러 클래스를 지정하는 것입니다.

다음 코드와 같이 struct sched_class 구조체 첫 번째 필드는 const struct sched_class *next입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 struct sched_class {
2 const struct sched_class *next;
3
4 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);

이번에는 커널 스케줄링 공통 코드에서 for_each_class() 함수를 어떻게 쓰는지 알아봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
2 {
3 const struct sched_class *class;
4
5 if (p->sched_class == rq->curr->sched_class) {
6 rq->curr->sched_class->check_preempt_curr(rq, p, flags);
7 } else {
8 for_each_class(class) {
9 if (class == rq->curr->sched_class)
10 break;
11 if (class == p->sched_class) {
12 resched_curr(rq);
13 break;
14 }
15 }
16 }

check_preempt_curr() 함수 8 번째 줄 코드에서 for_each_class(class) 를 볼 수 있습니다. for_each_class() 코드는 stop_sched_class 전역변수로 시작해서 다음 전역 변수를 순회하는 동작입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
#define for_each_class(class) \
   for (class = sched_class_highest; class; class = class->next)

#define sched_class_highest (&stop_sched_class)

for_each_class() 함수 구현부를 보면 stop_sched_class 전역변수로 시작해서 class->next에 지정된 다음 우선순위 스케줄러 클래스 전역 변수에 접근합니다. 이는 우선순위 방향으로 연결된 스케줄러 클래스를 순차적으로 호출하는 것입니다.

배운 내용을 참고해서 다음 코드는 어떻게 해석할 수 있을까요?
8 for_each_class(class) {
9 if (class == rq->curr->sched_class)
10 break;
11 if (class == p->sched_class) {
12 resched_curr(rq);
13 break;
14 }
15 }

먼저 8~15 번째 사이 코드 실행 조건을 봅시다. 이 구간 코드는 stop_sched_class 전역 변수부터 idle_sched_class 전역 변수까지 순회합니다.

다음 9 번째 줄 코드를 보면 현재 CPU를 점유하면서 실행 중인 프로세스의 스케줄러 클래스와 같은지 점검합니다. 

만약 현재 CPU를 점유하면서 실행 중인 프로세스가 RT 스케줄러 클래스에 등록돼 있으면
stop_sched_class, dl_sched_class 변수를 순회한 다음 rt_sched_class 전역 변수와 비교한 후 10 번째 줄 break 문에서 for_each_class 문을 빠져 나올 것입니다. RT 스케줄러 클래스로 등록된 프로세스는 current->sched_class = &rt_sched_class로 지정돼 있기 때문입니다. 


다음 그림은 라즈베리파이에서 확인한 스케줄러 클래스 실제 자료구조입니다.
 
[그림 10.23] Trace32로 본 스케줄러 클래스 자료구조


이번 소절까지 스케줄러 동작을 모듈화하는 스케줄러 클래스와 이 자료 구조를 이용한 5 가지 스케줄러 클래스를 알아봤습니다. 다음 소절에서는 5가지 스케줄러 클래스를 프로세스가 어떻게 등록하는지 살펴보겠습니다.

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

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

Reference(프로세스 스케줄링)

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




[리눅스커널] 스케줄링: 스케줄러 클래스 소개 10장. 프로세스 스케줄링

스케줄러 클래스는 다양한 스케줄러가 공존하면서 유연하게 실행하게 스케줄링 동작을 모듈화한 자료구조입니다. 또한 스케줄러 클래스는 스케줄러 별 세부 함수 연산을 할 수 있는 함수 포인터 형식의 메소드를 제공합니다. 즉, 스케줄러 클래스 메소드를 통해 리눅스 커널에서 제공하는 5가지 스케줄러 세부 함수에 접근합니다.
 
[그림 10.19] 스케줄러 클래스 종류 

위 그림을 보면서 스케줄러 클래스 개념에 대해서 조금 더 알아봅시다.

프로세스는 생성됨과 동시에 스케줄러 클래스를 등록합니다. 이후 리눅스 커널에서 지원하는 5개 스케줄러 세부 함수를 호출하기 위해서는 current->sched_class에 등록된 함수 메소드를 호출합니다.


리눅스 커널에서는 기본으로 5개의 스케줄러를 지원합니다. 5개 스케줄러에 대한 소개는 다음 테이블에서 확인할 수 있습니다.
 
[그림 10.20] 스케줄러 클래스 별 소스 위치 

이번 절에서는 자주 사용하는 RT 스케줄러, CFS 스케줄러, Idle 스케줄러에 초점을 맞추겠습니다. 


각각 5개의 스케줄러 동작을 모듈화해서 struct sched_class 이란 구조체로 선언하고 프로세스 태스크 디스크립터 sched_class 필드에 등록하는 것입니다.

스케줄러 클래스를 도입한 이유는 무엇일까?
커널에서 스케줄러 클래스를 도입한 이유는 무엇일까요? 

    프로세스가 스케줄러 클래스를 통해 유연하게 스케줄러를 바꿀 수 있다. 

이해를 돕기 위해 한 가지 예를 들겠습니다. 
대부분 일반 프로세스는 CFS 스케줄러에 의해 실행 흐름이 관리됩니다. 그런데 프로세스 실행 중 중요한 데이터나 코드가 선점되지 않고 CPU를 점유하면서 지속적으로 처리해야 하는 상황이 생길 수 있습니다. 이때 CFS 스케줄러보다 RT 스케줄러를 쓰면 계속 CPU를 점유하면서 실행할 수 있습니다. 이 상황에서 다음과 같이 처리하면 됩니다. 

    리눅스 커널에서 지원하는 5가지 스케줄러 클래스 중 RT 클래스를 스케줄러 클래스로 
      등록하면 된다.

스케줄러 클래스는 왜 알아야 할까?
이번에 커널에서 스케줄러 클래스는 왜 알아야 하는지 생각해 봅니다. 

    스케줄러 클래스가 어떤 자료구조로 구현돼 있는지 모르면 스케줄러 관련 코드를 읽을 수
    없다. 

스케줄러 세부 함수들은 모두 스케줄러 클래스를 통해 접근하는 구조로 설계 됐습니다. 따라서 스케줄링 코드 전체 흐름을 이해하기 위해 스케줄러 클래스를 이해해야 합니다. 커널 스케줄러 세부 알고리즘만큼 코드 전체 흐름도를 파악하는 것이 중요합니다.
스케줄러 클래스 자료구조 소개

스케줄러 클래스는 리눅스 커널이 기본으로 지원하는 5가지 스케줄러 세부 동작을 모듈화한 자료 구조입니다.

struct sched_class 구조체 알아보기
먼저 struct sched_class 구조체를 소개하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/sched.h]
1 struct sched_class {
2 const struct sched_class *next;
3
4 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
5 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
6 void (*yield_task) (struct rq *rq);
7 bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
8
9 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
10
11 struct task_struct * (*pick_next_task) (struct rq *rq,
12 struct task_struct *prev,
13 struct rq_flags *rf);
14 void (*put_prev_task) (struct rq *rq, struct task_struct *p);
15 int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
16 void (*migrate_task_rq)(struct task_struct *p);
17 void (*task_woken) (struct rq *this_rq, struct task_struct *task);
18 void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);

2 번째 줄 *next 필드를 제외하고 모든 필드가 포인터 형식 메소드입니다.
다음 테이블에서 스케줄러 클래스 필드 중 중요한 항목을 확인할 수 있습니다.
필드 설명
enqueue_task 프로세스가 실행 가능한 상태로 진입
dequeue_task 프로세스가 더 이상 실행 가능한 상태가 아닐때
yield_task 프로세스가 스스로 yield() 시스템콜을 실행했을 때
check_preempt_curr 현재 실행 중인 프로세스를 선점(preempt)할 수 있는지 검사
pick_next_task 실행할 다음 프로세스를 선택
put_prev_task 실행중인 태스크를 다시 내부 자료구조에 큐잉
load_balance 코어 스케줄러가 태스크 부하를 분산하고자 할때
set_curr_task 태스크의 스케줄러 클래스나 태스크 그룹을 바꿀때
task_tick 타이머 틱 함수가 호출

struct sched_class 자료구조에 대해 소개를 했으니 이어서 5가지 스케줄러 클래스에 대해서 살펴보겠습니다.


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

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

Reference(프로세스 스케줄링)

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


[리눅스커널] 스케줄링: 프로세스 상태를 ftrace로 확인하기 10장. 프로세스 스케줄링

필자가 처음 리눅스 커널을 공부할 때 '프로세스 상태 변화도'를 읽고 다음과 같은 궁금증이 생겼습니다. 

    커널이 실행하는 동안 커널 소스에서 프로세스 상태를 어떻게 확인할 수 있을까?

며칠 동안 고민을 했으나 끝내 알아내지 못했습니다. 결국 리눅스 커널 공부를 포기하고 말았습니다. 이번 소절에서는 리눅스 시스템에서 프로세스 상태를 직접 확인하는 방법을 공개하려 합니다. 프로세스 상태 변화에 대해 알아봤으니 이번에는 리눅스 시스템에서 실제 프로세스 상태가 어떻게 바뀌는지 알아보는 것입니다.

패치 코드 소개
먼저 컨텍스트 스위칭을 수행하는 다음 코드에 ftrace 로그를 하나 추가합시다.
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index aedd9bf..4fbd5e5 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -3457,7 +3457,7 @@ static void __sched notrace __schedule(bool preempt)
                ++*switch_count;

                trace_sched_switch(preempt, prev, next);
-
+               trace_printk("[+] prev->state:%d, next->state: %d \n", prev->state, next->state);
                /* Also unlocks the rq: */
                rq = context_switch(rq, prev, next, &rf);
        } else {

패치 코드 입력하는 방법에 대한 이해를 돕기 위해 다음 코드를 소개합니다. 
원래 __schedule() 함수 코드에서 박스로 된 부분 코드를 입력하면 됩니다.
 

prev와 next는 프로세스 태스크 디스크립터를 의미합니다.
prev 컨텍스트 스위칭될 프로세스이고 next는 새롭게 실행을 시작하는 프로세스인 것입니다.

위에서 소개한 패치 코드를 입력한 후 커널 빌드를 한 후 커널 이미지를 설치합시다.

ftrace 로그로 프로세스 상태 확인하기
이후 ftrace 를 키고 sched_switch 이벤트를 킨 다음 ftrace 로그를 받습니다. 추출한 ftrace 로그를 열어 보면 다음과 같은 메시지를 확인할 수 있습니다.
1 <idle>-0     [002] d..2   148.079678: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=rcu_preempt next_pid=8 next_prio=120
2 <idle>-0     [002] d..2   148.079686: __schedule+0x468/0x938: [+] prev->state:0, next->state: 0 


ftrace 실행 방법은 3.4 ftrace 절에서 상세히 다루고 있습니다.


2 번째 줄 로그에서 prev->state 는 0입니다. 이 정보는 prev 프로세스의 태스크 디스크립터인 struct task_struct 구조체 state 필드에 저장된 값입니다. state 필드가 0이면 프로세스 상태는 무엇일까요?

다음 해더 파일을 열어보면 정답을 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h]
#define TASK_RUNNING 0x0000

위 코드로 다음 사실을 알 수 있습니다.

    0x0000은 TASK_RUNNING 상태를 의미합니다.

prev 프로세스의 상태 값은 0인데, ftrace 메시지에서는 R(prev_state=R)로 표시합니다.
1 <idle>-0     [002] d..2   148.079678: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=rcu_preempt next_pid=8 next_prio=120

이번에는 다른 ftrace 로그를 보겠습니다.
1 watchdog/1-15    [001] d..2   148.095672: sched_switch: prev_comm=watchdog/1 prev_pid=15 prev_prio=0 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120
2 watchdog/1-15    [001] d..2   148.095677: __schedule+0x468/0x938: [+] prev->state:1, next->state: 0 

2 번째 줄 로그를 보면 prev->state 는 1입니다. 이번에는 state 필드가 1이면 어떤 프로세스 상태를 의미할까요? 다음 해더 파일을 보면 정답을 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h]
#define TASK_INTERRUPTIBLE 0x0001 

sched_switch 스케줄러 이벤트 메시지로 프로세스 상태를 알 수 있습니다. 이 메시지를 자주 활용해서 프로세스 상태 변화를 체크합시다. 

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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: TASK_INTERRUPTIBLE 상태로 바뀔 때 호출되는 함수 10장. 프로세스 스케줄링

TASK_UNINTERRUPTIBLE 상태로 바뀔 때 호출하는 함수 분석
다음 함수가 호출될 때 프로세스 상태를 TASK_UNINTERRUPTIBLE로 바꿉니다.
io_wait_event()
mutex_lock() 
usleep_range() 
msleep()
wait_for_completion()

io_wait_event()
io_wait_event() 함수를 호출할 때도 프로세스는 TASK_UNINTERRUPTIBLE 상태로 바뀝니다.

io_wait_event() 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/wait.h]
1 #define io_wait_event(wq_head, condition) \
2 do { \
3 might_sleep(); \
4 if (condition) \
5 break; \
6 __io_wait_event(wq_head, condition); \
7 } while (0)
8
9 #define __io_wait_event(wq_head, condition) \
10 (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
11     io_schedule())

io_wait_event() 함수에서 __io_wait_event() 함수를 호출하는데,
___wait_event() 함수 3 번째 인자로 TASK_UNINTERRUPTIBLE 를 전달해서 프로세스 상태를 바꿉니다.

__mutex_lock_common()
뮤텍스는 리눅스 커널에서 커널 동기화 기법 중 대표 선수 중 하나입니다.
임계 영역 구간 코드를 실행 중에 특정 프로세스만 뮤텍스를 획득해서 다른 프로세스가 임계 영역 접근을 막는 기법입니다.


뮤텍스 동작은 크게 fastpath와 slowpath로 나눌 수 있습니다.

fastpath는 뮤텍스를 다른 프로세스가 획득하지 않았을 때 실행하는 흐름이고,
slowpath는 뮤텍스를 다른 프로세스가 획득했거나 해제하는 흐름일 때 실행하는 동작입니다.


프로세스가 뮤텍스를 획득하는 과정을 대화로 풀어서 설명을 드려볼까요?
A 프로세스: 뮤텍스를 획득하고 싶습니다.

커널: 이미 B란 프로세스가 뮤텍스를 획득한 상태다.

A 프로세스: 그러면 뮤텍스를 계속 기다려야 할까요?

커널: 네가 뮤텍스를 획득하기 위해 계속 기다리면 다른 프로세스가 CPU에서 일을 못한다. 너를 TASK_UNINTERRUPTIBLE 상태로 바꾸고 휴면에 진입 시켜주마. B란 프로세스가 뮤텍스를 해제하면 널 깨워줄께.

이렇게 slowpath 뮤텍스 동작 흐름에서 프로세스를 TASK_UNINTERRUPTIBLE 상태로 바꿉니다.
조금 더 구체적으로 이미 프로세스가 뮤텍스를 획득했을 때 다른 프로세스가 뮤텍스를 획득하려고 하면, 자신을 뮤텍스 wait list에 등록하고 TASK_UNINTERRUPTIBLE 상태로 바꾼 후 휴면에 진입합니다.

뮤텍스 동작에 대해 간단히 알아봤으니 이제 관련 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 void __sched mutex_lock(struct mutex *lock)
2 {
3 might_sleep();
4
5 if (!__mutex_trylock_fast(lock))
6 __mutex_lock_slowpath(lock);
7}

6번째 줄 코드를 보면 __mutex_lock_slowpath() 함수를 호출합니다.

__mutex_lock_slowpath() 함수 코드를 보면 TASK_UNINTERRUPTIBLE 를 2 번째 인자로 __mutex_lock() 함수를 호출합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 static noinline void __sched
2 __mutex_lock_slowpath(struct mutex *lock)
3 {
4 __mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_);
5 }

이어서 __mutex_lock() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 static int __sched
2 __mutex_lock(struct mutex *lock, long state, unsigned int subclass,
3      struct lockdep_map *nest_lock, unsigned long ip)
4 {
5 return __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false);
6 }

__mutex_lock() 함수 2번째 인자가 long state입니다. 
__mutex_lock_slowpath() 함수에서 long state 인자로 TASK_UNINTERRUPTIBLE 플래그를 전달합니다.

다음은 프로세스 상태를 TASK_UNINTERRUPTIBLE로 바꾸는 __mutex_lock_common() 함수 구현부입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 static __always_inline int __sched
2 __mutex_lock_common(struct mutex *lock, long state, unsigned int subclass,
3     struct lockdep_map *nest_lock, unsigned long ip,
4     struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx)
5 {
...
6 set_current_state(state);
7 for (;;) {
...
7 schedule_preempt_disabled();

6 번째 줄 코드와 같이 set_current_state() 함수를 호출해 프로세스 상태를 바꿉니다. 함수 인자로 전달되는 state가 TASK_UNINTERRUPTIBLE 이니 TASK_UNINTERRUPTIBLE 상태로 바뀌는 것입니다.

이렇게 다른 프로세스가 뮤텍스를 잡고 있으면 15번째 줄 코드와 같이 자신은 슬립에 들어갑니다. 나중에 뮤텍스를 획득한 프로세스가 뮤텍스를 해제할 때까지 휴면 상태에 있는 것입니다.

usleep_range()
max와 min으로 지정한 시각만큼 슬립에 들어가는 usleep_range() 함수를 실행해도 TASK_UNINTERRUPTIBLE 상태로 바꿉니다.

usleep_range() 함수 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c]
1 void __sched usleep_range(unsigned long min, unsigned long max)
2 {
3 ktime_t exp = ktime_add_us(ktime_get(), min);
4 u64 delta = (u64)(max - min) * NSEC_PER_USEC;
5
6 for (;;) {
7 __set_current_state(TASK_UNINTERRUPTIBLE);
8 /* Do not return before the requested sleep time has elapsed */
9 if (!schedule_hrtimeout_range(&exp, delta, HRTIMER_MODE_ABS))
10 break;
11 }
12 }

7번째 줄 코드를 보면 프로세스 상태를 TASK_UNINTERRUPTIBLE 로 바꿉니다.

msleep() 
msleep() 함수는 밀리 초 단위로 딜레이를 줄 때 호출합니다. 주로 리눅스 드라이버에서 많이 활용합니다.

msleep() 함수를 쓰는 예제 코드를 잠깐 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/i2c/busses/i2c-tegra.c]
01 static int tegra_i2c_flush_fifos(struct tegra_i2c_dev *i2c_dev)
02 {
...
03 while (i2c_readl(i2c_dev, offset) & mask) {
04 if (time_after(jiffies, timeout)) {
05 dev_warn(i2c_dev->dev, "timeout waiting for fifo flush\n");
06 return -ETIMEDOUT;
07 }
08 msleep(1);
09 }
10 return 0;
11 }

3~10번째 줄 코드는 while 문입니다.
3번째 줄 코드에서 i2c_readl() 함수를 호출해서 i2c 버스에서 약속된 값을 읽습니다.

그런데 9 번째 줄 코드에서 msleep(1) 함수를 호출해서 1밀리초 만큼 딜레이를 줍니다. 위 코드는 디바이스 드라이버 관점으로 다음과 같이 동작합니다.

    하드웨어적으로 i2c 버스에서 어떤 값을 읽을 때 딜레이를 줘서 i2c 버스가 실행할 
   마진을 주자.

예제 코드를 봤으니 msleep() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c]
1 void msleep(unsigned int msecs)
2 {
3 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
4
5 while (timeout)
6 timeout = schedule_timeout_uninterruptible(timeout);
7 }
8
9 signed long __sched schedule_timeout_uninterruptible(signed long timeout)
10 {
11 __set_current_state(TASK_UNINTERRUPTIBLE);
12 return schedule_timeout(timeout);
13 }

msleep() 함수에서 6 번째 줄 코드와 같이 schedule_timeout_uninterruptible() 함수를 호출합니다. schedule_timeout_uninterruptible() 함수 코드를 보면 11 번째 줄과 같이 프로세스를 TASK_UNINTERRUPTIBLE 상태로 바꿉니다.

이 후 12 번째 줄 코드를 실행해서 schedule_timeout() 함수를 실행합니다.

wait_for_completion() 
wait_for_completion() 함수를 호출하면 프로세스 상태는 TASK_UNINTERRUPTIBlE로 바뀝니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/completion.c]
1 void __sched wait_for_completion(struct completion *x)
2 {
3 wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
4 }

3번째 줄 코드에서 wait_for_common() 함수를 호출할 때 3 번째 인자로 TASK_UNINTERRUPTIBlE 상태를 지정합니다.

wait_for_common() 함수를 호출하면 다음 흐름으로 함수 호출이 수행됩니다.
 
 
그러면 어느 함수에서 프로세스 상태를 UNINTERRUPTIBlE로 바꿀까요?

do_wait_for_common() 코드를 보면 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/completion.c]
1 static inline long __sched
2 do_wait_for_common(struct completion *x,
3    long (*action)(long), long timeout, int state)
4 {
5 if (!x->done) {
6 DECLARE_WAITQUEUE(wait, current);
7
8 __add_wait_queue_entry_tail_exclusive(&x->wait, &wait);
9 do {
10 if (signal_pending_state(state, current)) {
11 timeout = -ERESTARTSYS;
12 break;
13 }
14 __set_current_state(state);
15 spin_unlock_irq(&x->wait.lock);
16 timeout = action(timeout);
17 spin_lock_irq(&x->wait.lock);
18 } while (!x->done && timeout);
19 __remove_wait_queue(&x->wait, &wait);
20 if (!x->done)
21 return timeout;
22 }
23 if (x->done != UINT_MAX)
24 x->done--;
25 return timeout ?: 1;
26 }

14번째 줄 코드를 보면 __set_current_state() 함수를 호출해서 프로세스 상태를 TASK_UNINTERRUPTIBLE로 바꿉니다.
14 __set_current_state(state);

다음으로 16 번째 줄 코드를 실행해서 action으로 등록한 콜백 함수를 호출합니다.
16 timeout = action(timeout);

wait_for_completion() 함수 실행 시 콜 스택을 참고하면 action 포인터를 실행하면 schedule_timeout() 함수를 호출함을 알 수 있습니다.


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

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

Reference(프로세스 스케줄링)

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


[리눅스커널] 스케줄링: TASK_INTERRUPTIBLE 상태로 바뀔 때 호출되는 함수 10장. 프로세스 스케줄링

보통 프로세스가 휴면에 진입할 때 커널은 프로세스 상태를 TASK_INTERRUPTIBLE 로 바꿔 줍니다. 그렇다면 커널 어느 코드에서 프로세스 상태를 TASK_INTERRUPTIBLE 상태로 바꿀까요?

프로세스 상태를 TASK_INTERRUPTIBLE 상태로 바꾸는 다양한 커널이나 드라이버 코드를 볼 수 있습니다. 그 중 대표적인 함수를 요약하면 다음과 같습니다.
 
[그림 10.18] 프로세스 상태가 TASK_INTERRUPTIBLE로 바뀔 때 호출하는 함수

wait_event_interruptible
wait_event_interruptible() 함수를 호출하면 휴면상태(TASK_INTERRUTIBLE)로 프로세스 상태를 바꿉니다. wait_event_interruptible() 함수를 호출하면 웨이트 큐에서 웨이트 큐 이벤트가 실행될 때까지 기다립니다.

이제 커널이 wait_event_interruptible() 함수를 호출하면 프로세스 상태를 어떻게 바꾸는지 살펴보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/wait.h]
1 #define wait_event_interruptible(wq_head, condition) \
2 ({ \
3 int __ret = 0; \
4 might_sleep(); \
5 if (!(condition)) \
6 __ret = __wait_event_interruptible(wq_head, condition); \
7 __ret; \
8 })
9
10 #define __wait_event_interruptible(wq_head, condition) \
11 ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
12       schedule())

wait_event_interruptible() 함수는 매크로 포멧으로 구성돼 있습니다.
wait_event_interruptible() 함수를 호출하면 3~7번째 줄 코드로 치환되는데 __wait_event_interruptible() 호출합니다.

이어서 __wait_event_interruptible() 함수 구현부를 보면 11 번째 줄과 같이 ___wait_event() 함수를 호출합니다.

다음으로 ___wait_event() 함수 구현부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/wait.h]
1 #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
2 ({ \
3 __label__ __out; \
4 struct wait_queue_entry __wq_entry; \
5 long __ret = ret; /* explicit shadow */ \
6 \
7 init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
8 for (;;) { \
9 long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
... \
10 } \
11 finish_wait(&wq_head, &__wq_entry); \
12 __out: __ret; \
13 })

7 번째 줄에서 웨이트 큐를 초기화한 다음 9 번째 줄 코드에서 prepare_to_wait_event() 함수를 호출해서 웨이트 큐 전처리 과정을 수행합니다.

prepare_to_wait_event() 함수 코드를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/wait.c]
1 long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int 2 state)
3 {
4 unsigned long flags;
5 long ret = 0;
6
7 spin_lock_irqsave(&wq_head->lock, flags);
8 if (unlikely(signal_pending_state(state, current))) {
...
9 } else {
10 if (list_empty(&wq_entry->entry)) {
11 if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
12 __add_wait_queue_entry_tail(wq_head, wq_entry);
13 else
14 __add_wait_queue(wq_head, wq_entry);
15 }
16 set_current_state(state);
17 }
...
18 }

11~14 번째 줄 코드에서 웨이트 큐를 Enqueue 시킨 다음, 16 번째 줄 코드에서 set_current_state() 함수를 호출해서 프로세스 상태를 TASK_INTERRUPTIBLE(휴면상태)로 바꿉니다.


set_current_state() 함수 구현부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched.h]
1 #define set_current_state(state_value) \
2 do { \
3 WARN_ON_ONCE(is_special_task_state(state_value));\
4 current->task_state_change = _THIS_IP_; \
5 smp_store_mb(current->state, (state_value)); \
6 } while (0)

5 번째 줄 코드가 핵심인데 current->state 필드에 state_value 인자를 저장합니다.
이 과정에 smp_store_mb() 함수를 써서 GCC 컴파일러가 코드 최적화를 위해 코드 위치를 바꾸는 것을 방지합니다.


sys_pause() 함수
이번에 볼 코드는 시그널을 기다릴 때 호출하는 sys_pause() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 SYSCALL_DEFINE0(pause)
2 {
3 while (!signal_pending(current)) {
4 __set_current_state(TASK_INTERRUPTIBLE);
5 schedule();
6 }
7 return -ERESTARTNOHAND;
8 }

sys_pause() 함수는 다음 동작을 수행합니다
팬딩된 시그널(자신에게 전달된 시그널) 없는지 점검
자신을 TASK_INTERRUPTIBLE(휴면상태)로 변경
schedule() 함수 호출로 휴면에 진입

위 함수 4 번째 줄 코드를 보면 프로세스 상태를 TASK_INTERRUPTIBLE(실행대기)로 바꿉니다.


시그널에 대한 내용은 12장을 참고하세요.


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

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

Reference(프로세스 스케줄링)

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


[리눅스커널] 스케줄링: TASK_RUNNING(CPU 실행)로 바뀔 때 호출되는 함수 10장. 프로세스 스케줄링

TASK_RUNNING(CPU실행)로 바뀔 때 호출하는 함수 분석
프로세스 상태를 CPU실행(TASK_RUNNING)으로 변경하는 함수는 1개 밖에 없습니다. __schedule() 함수를 실행할 때 프로세스는 CPU를 점유하면서 실행하는 상태로 바뀝니다.

__schedule() 함수 코드를 살펴봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 static void __sched notrace __schedule(bool preempt)
2 {
3 struct rq *rq;
4 int cpu;
5
6 cpu = smp_processor_id();
7 rq = cpu_rq(cpu);
...
8 if (likely(prev != next)) {
9 rq->nr_switches++;
10 rq->curr = next;
...
11 /* Also unlocks the rq: */
12 rq = context_switch(rq, prev, next, &rf);
13 } else {
...

먼저 6~7 번째 줄 코드를 보겠습니다.
6 cpu = smp_processor_id();
7 rq = cpu_rq(cpu);

6 번째 줄은 현재 실행 중인 CPU 번호를 cpu 지역 변수에 저장합니다. 이어서 7 번째 줄은 현재 실행 중인 CPU 번호에 해당하는 런큐 구조를 rq 지역변수로 읽습니다.

컨텍스트 스위칭 동작 이전 코드인 8~10 번째 줄을 보겠습니다.
8 if (likely(prev != next)) {
9 rq->nr_switches++;
10 rq->curr = next;

스케줄러가 컨텍스트 스위칭으로 다음에 실행할 프로세스 태스크 디스크립터 주소는 next 지역 변수가 담고 있습니다. 10 번째 줄 코드를 보면 런큐 구조체 curr 필드에 next를 저장합니다.

런큐 구조체 curr 필드는 현재 CPU를 점유하면서 실행 중인 프로세스 태스크 디스크립터 주소를 저장합니다. 이 코드가 실행하면서 current 프로세스 혹은 TASK_RUNNING(CPU실행) 상태로 바꾸는 것입니다.

누군가 ’커널에서 CPU를 점유하면서 실행 중인 프로세스는 어디서 확인할까’라고 질문을 던진다면 어떻게 답할 수 있을까요? 다음과 같이 대답할 수 있습니다. 

    런큐 struct rq 구조체 curr 필드에서 확인할 수 있다.


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

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

Reference(프로세스 스케줄링)

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



[리눅스커널] 스케줄링: TASK_RUNNING(실행 대기)로 바뀔 때 호출되는 함수 10장. 프로세스 스케줄링

TASK_RUNNING(실행 대기)로 바뀔 때 호출하는 함수 분석
프로세스가 다음과 같은 동작을 수행할 때 실행대기(TASK_RUNNING) 상태로 바꿉니다.
프로세스를 깨울 때
프로세스를 처음 생성하고 실행 요청을 할 때
프로세스 관련 정보를 업데이트 할 때

보통 휴면 중에 있는 프로세스를 깨우면 프로세스는 실행대기(TASK_RUNNING)상태로 바꿉니다.

프로세스 상태가 실행대기(TASK_RUNNING)로 바뀔 때 실행하는 함수 목록은 다음과 같습니다.

 
[그림 10.15] 프로세스 상태가 실행 대기로 바뀔 때 호출하는 함수 목록

위에서 소개한 함수를 분석하면서 프로세스 상태를 실행대기(TASK_RUNNING) 바꾸는 과정을 살펴봅시다.

wake_up_new_task()
프로세스가 생성된 직후 커널은 생성한 프로세스 상태를 실행대기(TASK_RUNNING) 상태로 바꿉니다.
 
[그림 10.16] 프로세스 생성 직후 실행 대기 상태로 바뀌는 함수 흐름도

프로세스를 생성할 때 _do_fork() 함수를 호출합니다. _do_fork() 함수에서 copy_process() 함수 호출로 부모 프로세스 리소스를 자식 프로세스에게 복사합니다. 
이 과정을 마무리하면 부모 프로세스는 바로 wake_up_new_task() 함수를 호출해서 생성한 자식 프로세스를 깨웁니다. 즉, 프로세스를 생성한 다음 바로 실행 요청을 하는 것입니다.

이번에 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 raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
7 p->state = TASK_RUNNING;

7번째 줄 코드에서 TASK_RUNNING 로 프로세스 상태를 바꿉니다. 여기서 다음과 같은 의문이 생깁니다. 

    wake_up_new_task() 함수는 어떤 흐름으로 호출될까?

다음 코드를 보면 알수 있듯이 프로세스를 생성하는 _do_fork() 함수에서 wake_up_new_task() 함수를 호출해 생성한 프로세스를 깨웁니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c]
01 long _do_fork(unsigned long clone_flags,
02       unsigned long stack_start,
03       unsigned long stack_size,
04       int __user *parent_tidptr,
05       int __user *child_tidptr,
06       unsigned long tls)
07 {
08 struct completion vfork;
...
09 wake_up_new_task(p);

wake_up_process() 함수
휴면 중인 프로세스를 깨울 때 가장 많이 쓰는 함수입니다. wake_up_process() 함수를 호출하면 프로세스는 TASK_RUNNING(실행 대기) 상태로 바꿉니다. 
                   
[그림 10.17] 프로세스를 깨울 때 함수 실행 흐름도

wake_up_process() 함수를 호출하면 위 그림과 같은 함수 흐름으로 ttwu_do_wakeup() 함수를 호출합니다.

프로세스 상태를 TASK_RUNNING(실행 대기)로 바꾸는 ttwu_do_wakeup() 함수 코드를 같이 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
2    struct rq_flags *rf)
3 {
4 check_preempt_curr(rq, p, wake_flags);
5 p->state = TASK_RUNNING;
6 trace_sched_wakeup(p);

5번째 줄 코드를 보면 태스크 디스크립터 state 필드에 TASK_RUNNING를 저장합니다. 프로세스 상태를 TASK_RUNNING로 바꾸는 코드입니다.


프로세스를 깨운다는 의미를 잠깐 더 생각해 봅시다.

“프로세스를 깨운다”란 문장은 프로세스 실행을 스케줄러에게 요청한다는 의미입니다. 이때 런큐에서 실행을 기다리는 프로세스들과 우선순위를 참고해서 실제 실행은 스케줄러가 수행합니다.

프로세스를 깨운다는 의미는 프로세스 실행을 스케줄러에게 요청한다는 것입니다.


yield() 함수
프로세스 실행을 잠시 양보할 때 호출하는 yield() 함수에서도 프로세스 상태를 실행 대기(TASK_RUNNING)로 바꿉니다.

yield() 함수 코드를 소개합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 void __sched yield(void)
2 {
3 set_current_state(TASK_RUNNING);
4 sys_sched_yield();
5 }

3번째 줄 코드를 보면 set_current_state() 함수 호출로 프로세스 상태를 TASK_RUNNING으로 바꿉니다.

4 번째 줄 코드에서 호출하는 sys_sched_yield() 함수는 스케줄러 클래스 yield_task 메소드를 호출한 다음 스스로 휴면에 진입합니다.

do_nanosleep() 함수
이번에는 특정 시각 동안 휴면에 진입하는 do_nanosleep() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/hrtimer.c]
1 static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
2 {
3 struct restart_block *restart;
4
...
5 __set_current_state(TASK_RUNNING);

5번째 줄 코드와 같이 프로세스 상태를 TASK_RUNNING 으로 바꿉니다.

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

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

Reference(프로세스 스케줄링)

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




[리눅스커널] 가상파일시스템: 파일시스템별 파일 함수 오퍼레이션 처리 과정 13장. 가상 파일시스템

지금까지 ext4 파일 시스템에서 파일을 오픈할 때 함수 실행 흐름을 살펴봤습니다.
이번에는 proc과 sysfs 파일 시스템에서 관리하는 파일을 오픈할 때 어떤 함수가 호출되는지 살펴보겠습니다.  

proc 파일시스템 함수 오퍼레이션 동작 확인하기
먼저 리눅스 시스템 상태 정보를 출력해주는 proc 파일시스템 함수 오퍼레이션 동작을 알아봅시다.

그러면 proc 파일시스템 함수 오퍼레이션 동작을 확인하려면 어떻게 해야 할까요? 방법은 간단합니다. 다음 파일을 열면 됩니다.
root@raspberrypi:/proc # cat kmsg 

위 파일을 읽을 때 가상 파일시스템 관점으로 어떻게 동작할까요?
 
  [그림 13.4] proc 파일 시스템에서 open 함수 오퍼레이션 실행 흐름도

do_dentry_open() 함수에서 이번에는 proc_reg_open() 함수를 호출합니다.

이전 소절에서 배운 바와 같이  /home/pi/sample_text.text 파일을 열 때는 do_dentry_open() 함수에서 ext4_file_open() 함수를 호출했습니다. 이는 가상 파일시스템에서 sample_text.text 파일은 ext4 파일시스템에서 관리한다는 사실을 알고 있기 때문입니다.

do_dentry_open() 함수 코드를 보면서 처리 과정을 살펴봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/open.c]
1 static int do_dentry_open(struct file *f,
2   struct inode *inode,
3   int (*open)(struct inode *, struct file *),
4   const struct cred *cred)
5 {
...
6 if (!open)
7 open = f->f_op->open;
8 if (open) {
9 error = open(inode, f);

9번째 줄 코드에서는 proc_reg_open() 함수를 호출합니다. 이 이유는 무엇일까요?

가상 파일시스템에서 파일 위치에 따라 파일시스템 종류를 식별해서 파일시스템 별 함수 테이블을 로딩하기 때문입니다. proc 파일 시스템의 경우 파일을 열 때 proc_reg_open() 함수를 실행해서 파일 오픈을 실행합니다.

이번에는 /proc/kmsg 파일을 열고 읽을 때 동작을 확인하겠습니다. 
 
[그림 13.5] proc 파일시스템 파일 읽기와 쓰기 함수 오퍼레이션 흐름도

이렇게 /proc/kmsg 파일은 proc 파일시스템에서 관리하는 특수 파일입니다. 가상 파일시스템에서 /proc/kmsg 파일이 proc 파일시스템에서 관리한다는 사실을 파악한 후 proc 파일시스템에서 등록한 파일 오퍼레이션 함수 테이블에 따라 함수를 분기합니다.

sysfs 파일시스템 함수 오퍼레이션 동작 파악하기
이어서 리눅스 시스템에 탑재된 하드웨어 장치 정보를 출력해주는 sysfs 파일시스템 함수 오퍼레이션 동작을 알아봅시다.

다음과 같이 리눅스 커널에서 sysfs 파일시스템에서 관리하는 파일을 하나 열어보겠습니다.
root@raspberrypi:/sys/devices/system/cpu/cpu0/cpufreq# cat cpuinfo_cur_freq 
1200000

위 파일은 CPU 주파수 정보를 출력합니다.


sysfs는 리눅스 커널이 지원하는 장치 드라이버에 대한 정보를 출력하는 가상 파일시스템 중 하나입니다. 디바이스와 하드웨어 장치를 디렉토리 계층 구조로 알려줍니다.


위 파일을 읽을 때 가상 파일시스템 관점으로 어떻게 동작할까요?
 
[그림 13.6] sysfs 파일 시스템 함수 오픈 오퍼레이션 흐름도 

sysfs 파일시스템에서 관리하는 파일을 열 때는 kernfs_fop_open() 함수를 호출해서 파일 오픈을 수행합니다.  

다음 그림은 sysfs 파일시스템에서 파일을 읽고 쓸 때 처리 과정입니다.
 
  [그림 13.7] sysfs 파일시스템 read/write 함수 오퍼레이션 흐름도

파일을 읽을 때는 kernfs_fop_read() 함수, 파일을 쓸 때는 kernfs_fop_write() 함수를 호출합니다.

여기까지 각 파일시스템별로 파일을 오픈하고 읽고 쓸 때 흐름을 살펴봤습니다. 

파일시스템별 파일 오픈 함수 오퍼레이션 실행 흐름 파악하기
이제 가상 파일시스템 관점으로 이제까지 다룬 내용을 정리합시다. 파일을 오픈해서 열고 쓰고 닫을 때 가상 파일시스템 계층에서 각 파일 시스템 별 함수 테이블로 분기를 시킵니다. 이런 동작이 가능한 이유는 가상 파일시스템에서 파일 객체란 struct file 자료 구조가 파일 시스템 별 파일 오픈 및 쓰고 읽기 동작에 대한 관리를 해주기 때문입니다.

다음 그림에서 파일시스템 별로 파일을 오픈할 때 함수 흐름을 볼 수 있습니다.
 
[그림 13.8] 각 파일시스템별 함수 오픈 오퍼레이션 동작 흐름도

어떤 파일을 읽을 때 가상 파일시스템 계층에서는 다음과 같은 동작을 수행합니다.
1. 덴트리 객체로 현재 실행 중인 디렉토리를 관리하는 파일 시스템 점검
2. 파일 종류별 파일 오퍼레이션 로딩
3. 파일을 읽고 쓰는 동작을 수행할 때 파일 시스템 함수 테이블로 분기

소프트웨어 계층 구조 관점으로 가상 파일시스템은 파일시스템 상위에서 파일시스템을 분기하고 인터페이싱 하는 역할을 수행합니다. 이 구조로 소프트웨어 구조를 설계하면 다양한 파일 시스템을 공존해서 실행시킬 수 있습니다.

라즈베리파이에서는 위에서 언급한 ext4, proc, sysfs 파일 시스템 이외에 다른 파일 시스템도 지원합니다. 이 파일시스템에 접근하기 위해서 가상 파일시스템 계층을 거쳐야 하는 것입니다.


라즈베리파이에서 지원하는 파일시스템 종류는 어떻게 파악할 수 있을까요? 다음 경로에 있는 파일을 열면 확인할 수 있습니다.
root@raspberrypi:/proc # cat filesystems 
nodev sysfs
nodev rootfs
nodev ramfs
nodev bdev
nodev proc
nodev cpuset
nodev cgroup
nodev cgroup2
nodev tmpfs
nodev devtmpfs
nodev configfs
nodev debugfs
nodev tracefs
nodev sockfs
nodev pipefs
nodev rpc_pipefs
nodev devpts
ext3
ext2
ext4
vfat
msdos
nodev nfs

nodev nfs4
nodev autofs
f2fs
nodev mqueue
fuseblk
nodev fuse
nodev fusectl


가상 파일시스템은 이렇게 파일시스템별 함수 테이블을 로딩해서 동적으로 파일시스템이 관리하는 함수를 호출하는 역할을 수행합니다. 그래서 가상 파일시스템에서는 함수 포인터로 각 파일시스템별로 등록된 함수를 호출하는 패턴이 많습니다. 위에서 알아봤던 open(), write() 그리고 read() 함수는 함수 포인터로 각 파일 열고 닫기 동작을 수행합니다. 

다음 소절에서는 파일 객체를 실행할 때 호출하는 open(), read()와 write() 함수에 대해서 알아보겠습니다.


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

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


Reference(가상 파일시스템)

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



[리눅스커널] 가상파일시스템/디버깅: 아이노드 객체 함수 오퍼레이션 확인 13장. 가상 파일시스템

이전 소절에서 실습했듯이 슈퍼블록 statfs 함수 오퍼레이션으로 파일시스템의 속성 정보를 읽거나 저장합니다. 이와 마찬가지로 파일의 속성 정보를 읽거나 저장하는 과정에서 아이노드 객체 함수 오퍼레이션이 실행됩니다.

이번 소절에서 유저 어플리케이션에서 stat 함수를 호출하면 아이노드 객체 함수 오퍼레이션 함수 흐름을 확인하겠습니다.

실습 패치 코드 작성해보기 

이어서 stat() 함수를 호출해 파일속성을 읽는 코드를 작성해보겠습니다. 다음 소스 코드를 같이 입력해볼까요?
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <unistd.h>
04 #include <sys/types.h>
05 #include <sys/stat.h>
06 #include <signal.h>
07 #include <string.h>
08 #include <fcntl.h>
09
10 #define GENERAL_FILE "/home/pi/sample_text.text"
11 #define PROC_FILE "/proc/cmdline"
12
13 #define BUFF_SIZE 128
14
15 int main() 
16 {
17 struct stat fileinfo;
18 char fname[BUFF_SIZE] = {0,};
19
20 strcpy(fname, GENERAL_FILE);
21
22 printf("fname[%s] \n", fname);
23 if(stat(fname, &fileinfo)) {
24 printf("Unable to stat %s \n", fname);
25 exit(1);
26 }
27
28 strcpy(fname, PROC_FILE);
29
30 printf("fname[%s] \n", fname);
31 if(stat(fname, &fileinfo)) {
32 printf("Unable to stat %s \n", fname);
33 exit(1);
34 }
35
36 return 0;
37 }

위 코드의 핵심 내용은 다음 파일의 세부 속성을 stat() 함수로 읽는 것입니다.
"/home/pi/sample_text.text"
"/proc/cmdline"

소스 코드를 설명드리기 전에 stat() 함수가 어떤 기능인지 알아볼까요?
이를 위해 라즈베리파이에서 다음 'info stat' 명령어를 입력할 필요가 있습니다.
root@raspberrypi:/home/pi# info stat
Next: sync invocation,  Prev: du invocation,  Up: Disk usage

14.3 ‘stat’: Report file or file system status
==============================================

‘stat’ displays information about the specified file(s).  Synopsis:

위 출력 결과는 다음 내용을 말해줍니다.

    ‘stat’은 지정한 파일의 속성 정보를 알려준다.

이어서 위에서 소개한 소스 코드를 분석해볼까요?

먼저 20~26번째 줄 코드를 보겠습니다.
20 strcpy(fname, GENERAL_FILE);
21
22 printf("fname[%s] \n", fname);
23 if(stat(fname, &fileinfo)) {
24 printf("Unable to stat %s \n", fname);
25 exit(1);
26 }

20~22번째 줄 코드는 fname 변수에 GENERAL_FILE 매크로에서 지정한 "/home/pi/sample_text.text" 스트링을 복사하고 터미널에 출력하는 동작입니다.

23번째 줄 코드는 stat() 함수를 호출해 파일 속성을 읽습니다. 24~25번째 줄은 stat() 함수가 파일속성을 읽지 못해 true를 반환했을 때 실행하는 예외 처리 코드입니다. 

이어서 28~34번째 줄 코드를 보겠습니다.
28 strcpy(fname, PROC_FILE);
29
30 printf("fname[%s] \n", fname);
31 if(stat(fname, &fileinfo)) {
32 printf("Unable to stat %s \n", fname);
33 exit(1);
34 }

28~34번째 줄 코드는 다음 동작을 제외하고 20~26번째 줄 코드와 기능이 같습니다.

    stat() 함수로 "/proc/cmdline" 파일 속성을 읽자.

코드 내용에 대해 알아봤으니 소스 코드를 컴파일하는 방법을 알아볼까요?

여기서 한 가지 의문이 생깁니다.
     
    컴파일을 할때 어느 프로그램을 써야 할까?

프로그램 이름은 터미널입니다. 기존에 소개한 방식과 같인 라즈베리파이에서 터미널을 열어서 컴파일을 하는 것입니다. 

먼저 위에서 소개한 코드를 rpi_vfs_stat_operation.c 파일로 저장합니다.
컴파일을 쉽게 하기 위해 다음과 같이 코드를 작성하고 파일 이름을 Makefile으로 저장합시다.
stat_file_proc: rpi_vfs_stat_operation.c
gcc -o stat_file_proc rpi_vfs_stat_operation.c
이후 다음과 같이 make 명령어를 입력해 rpi_vfs_stat_operation.c 소스 파일을 컴파일합시다.
root@raspberrypi:/home/pi# make
gcc -o stat_file_proc rpi_vfs_stat_operation.c
오타없이 코드를 입력하면 위와 같은 메시지가 출력되면서 stat_file_proc 파일이 생성될 것입니다.
rpi_vfs_stat_operation.c 소스 파일의 실행 파일 이름은 stat_file_proc입니다.

ftrace 설정해보기 

이어서 ftrace 설정하는 방법을 알아볼까요? 명령어는 다음과 같습니다.
01 #!/bin/bash
02
03 echo 0 > /sys/kernel/debug/tracing/tracing_on
04 sleep 1
05 echo "tracing_off" 
06
07 echo 0 > /sys/kernel/debug/tracing/events/enable
08 sleep 1
09 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
20 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/enable
21 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/enable
22 sleep 1
23 echo "event enabled"
24
echo ext4_file_getattr generic_fillattr > /sys/kernel/debug/tracing/set_ftrace_filter
28 sleep 1
29 echo "set_ftrace_filter enabled"
30
31 sleep 1
32 echo "set_ftrace_filter enabled"
33
34 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
35 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
36 echo "function stack trace enabled"
37
38 echo 1 > /sys/kernel/debug/tracing/tracing_on
39 echo "tracing_on"

위에서 소개한 ftrace 설정 명령어를 입력한 후 rpi_vfs_stat.sh 이름으로 저장합시다.

    이전 소절에서 봤던 ftrace 설정 명령어와 거의 비슷한 것 같다.

맞습니다. 그래서 이전 장에서 소개한 ftrace 설정 명령어와 차이점 부분 위주로 살펴보겠습니다.
echo ext4_file_getattr generic_fillattr > /sys/kernel/debug/tracing/set_ftrace_filter
underline

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

위에서 보이는 함수 콜스택을 ftrace로 보기 위해 set_ftrace_filter 파일에 함수를 지정하는 것입니다.

함수 이름이 조금 친숙해 보이지 않아요? 모두 13.6.2 절에서 분석한 아이노드 함수 오퍼레이션 함수입니다.

ftrace 로그 추출하는 방법

실습 코드 실행 파일 stat_file_proc이 준비됐고 ftrace 설정 방법도 알게 됐습니다.
이어서 ftrace를 설정하고 stat_file_proc 파일을 실행할 차례입니다.

먼저 rpi_vfs_stat.sh 셸 스크립트를 실행해 ftrace를 설정합시다.
root@raspberrypi:/home/pi # ./rpi_vfs_stat.sh

다음 "./vfs_file_proc" 명령어를 입력해 vfs_file_proc 파일을 실행합니다.
root@raspberrypi:/home/pi # ./stat_file_proc
fname[/home/pi/sample_text.text] 
fname[/proc/cmdline] 

이어서 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 

여기까지 소개드린 실습 과정을 정리해볼까요?
      첫째, 유저 어플리케이션 rpi_vfs_stat_operation.c 코드를 입력한 다음
       컴파일한다. 실행 파일은 vfs_file_proc 이다.
      둘째, rpi_vfs_stat.sh 셸 스크립트를 실행해 ftace를 설정한다.
      셋째, vfs_file_proc 파일을 실행한다.
      넷째, get_ftrace.sh 셸 스크립트를 실행해 ftrace 로그를 받는다.

이제 이번 소절의 하이라이트인 ftrace 로그를 분석을 시작하겠습니다.

ftrace 로그 분석해보기 

다음은 이번 소절에 분석할 ftrace 로그입니다.
01  stat_file_proc-2848  [002] ....  9731.927338: sys_enter: NR 197 (3, 7ed91450, 7ed91450, 76f19c90, 3, 7ed91534)
02  stat_file_proc-2848  [002] ....  9731.927346: ext4_file_getattr+0x10/0xbc <-vfs_getattr_nosec+0x68/0x7c
03  stat_file_proc-2848  [002] ....  9731.927382: <stack trace>
04 => ext4_file_getattr+0x14/0xbc
05 => vfs_getattr_nosec+0x68/0x7c
06 => vfs_statx_fd+0x4c/0x78
07 => sys_fstat64+0x3c/0x6c
08 => __sys_trace_return+0x0/0x10
09 => 0x7ed91444
10  stat_file_proc-2848  [002] ....  9731.927385: generic_fillattr+0x10/0x108 <-ext4_getattr+0xc8/0xd0
11  stat_file_proc-2848  [002] ....  9731.927402: <stack trace>
12 => generic_fillattr+0x14/0x108
13 => ext4_getattr+0xc8/0xd0
14 => ext4_file_getattr+0x24/0xbc
15 => vfs_getattr_nosec+0x68/0x7c
16 => vfs_statx_fd+0x4c/0x78
17 => sys_fstat64+0x3c/0x6c
18 => __sys_trace_return+0x0/0x10
19 => 0x7ed91444
20  stat_file_proc-2848 [002] .... 9731.927409: sys_exit: NR 197 = 0
...
21  stat_file_proc-2848  [002] ....  9731.929170: sys_enter: NR 197 (1, 7ed90f18, 7ed90f18, 76de5f18, 76eb9d50, 76eb7bec)
22  stat_file_proc-2848  [002] ....  9731.929174: generic_fillattr+0x10/0x108 <-vfs_getattr_nosec+0x74/0x7c
23  stat_file_proc-2848  [002] ....  9731.929190: <stack trace>
24 => generic_fillattr+0x14/0x108
25 => vfs_getattr_nosec+0x74/0x7c
26 => vfs_statx_fd+0x4c/0x78
27 => sys_fstat64+0x3c/0x6c
28 => __sys_trace_return+0x0/0x10
29 => 0x7ed90f10
30  stat_file_proc-2848 [002] .... 9731.929195: sys_exit: NR 197 = 0

이번 실습은 유저 공간에서 stat() 함수로 파일 속성을 읽을 때 커널 공간 아이노드 stat 함수 오퍼레이션 함수 실행 흐름을 알아보기 위한 것입니다. 복잡해보이는 ftrace 로그 실행 흐름은 2단계로 분류할 수 있습니다.
1단계: "/home/pi/sample_text.text" 파일 속성을 읽을 때 커널 공간 아이노드 stat 함수 오퍼레이션 함수 실행 흐름
2단계: "/proc/cmdline" 파일 속성을 읽을 때 커널 공간 아이노드 stat 함수        오퍼레이션 함수 실행 흐름 

1단계: "/home/pi/sample_text.text" 파일 속성을 읽을 때 함수 실행 흐름
"/home/pi/sample_text.text" 파일 속성을 읽을 때 함수 실행 흐름을 분석해볼까요?
01  stat_file_proc-2848  [002] ....  9731.927338: sys_enter: NR 197 (3, 7ed91450, 7ed91450, 76f19c90, 3, 7ed91534)
02  stat_file_proc-2848  [002] ....  9731.927346: ext4_file_getattr+0x10/0xbc <-vfs_getattr_nosec+0x68/0x7c
03  stat_file_proc-2848  [002] ....  9731.927382: <stack trace>
04 => ext4_file_getattr+0x14/0xbc
05 => vfs_getattr_nosec+0x68/0x7c
06 => vfs_statx_fd+0x4c/0x78
07 => sys_fstat64+0x3c/0x6c
08 => __sys_trace_return+0x0/0x10
09 => 0x7ed91444

먼저 01번째 줄 코드를 보겠습니다.
01  stat_file_proc-2848  [002] ....  9731.927338: sys_enter: NR 197 (3, 7ed91450, 7ed91450, 76f19c90, 3, 7ed91534)

01번째 줄 메시지 'sys_enter: NR 197'는 다음 사실을 말해줍니다.

    197번 시스템 콜 실행을 시작한다.

그러면 197번 시스템 콜의 정체는 무엇일까요? 다음 코드를 보면 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_fstat64 (__NR_SYSCALL_BASE+197)


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

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


Reference(가상 파일시스템)

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


[리눅스커널] 가상파일시스템/디버깅: 슈퍼블록 객체 함수 오퍼레이션 확인 13장. 가상 파일시스템

이번 소절에서 유저 어플리케이션에서 statfs() 함수를 호출했을때 커널 내부 슈퍼블록 객체 함수 오퍼레이션 동작을 살펴보겠습니다. 

먼저 실습 패치 코드를 작성해볼까요?

실습 패치 코드 작성해보기

소스 코드는 다음과 같으니 같이 입력해 봅시다.
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <unistd.h>
04 #include <sys/types.h>
05 #include <sys/vfs.h>
06 #include <string.h>
07 #include <fcntl.h>
08 
09 #define GENERAL_DIR "/home/pi"
10 #define PROC_DIR "/proc"
11 #define BUFF_SIZE 128
12 
13 int main() 
14 {
15 struct statfs file_sys_info;
16 char fname[BUFF_SIZE] = {0,};
17 
18 strcpy(fname, GENERAL_DIR);
19
20 printf("statfs under %s \n", GENERAL_DIR);
21 if(statfs(fname, &file_sys_info)) {
22 printf("Unable to statfs %s \n", fname);
23 exit(1);
24 }
25
26 strcpy(fname, PROC_DIR);
27 printf("statfs under %s \n", PROC_DIR);
28 if(statfs(fname, &file_sys_info)) {
29 printf("Unable to statfs %s \n", fname);
30 exit(1);
31 }
32
33 return 0;
34 }

코드 내용은 어렵지 않으니 간단히 리뷰하는 수준으로 설명을 드리겠습니다.

18~24번째 줄 코드를 보겠습니다.
18 strcpy(fname, GENERAL_DIR);
19
20 printf("statfs under %s \n", GENERAL_DIR);
21 if(statfs(fname, &file_sys_info)) {
22 printf("Unable to statfs %s \n", fname);
23 exit(1);
24 }

fname 버퍼에 GENERAL_DIR("/home/pi") 경로를 복사한 다음 statfs() 함수를 호출합니다.
statfs() 함수를 호출하면 지정된 디렉토리를 관리하는 파일시스템 속성을 알 수 있습니다.

이어서 26~31번째 줄 코드를 보겠습니다.
26 strcpy(fname, PROC_DIR);
27 printf("statfs under %s \n", PROC_DIR);
28 if(statfs(fname, &file_sys_info)) {
29 printf("Unable to statfs %s \n", fname);
30 exit(1);
31 }

fname 버퍼에 PROC_DIR("/proc") 경로를 복사한 다음 statfs() 함수를 호출합니다.
statfs() 함수를 호출하면 "/proc" 디렉토리를 관리하는 파일시스템 속성을 알 수 있습니다.
 
리눅스 시스템 프로그램을 한번이라도 해본 분이면 이해할 수 있는 수준의 코드입니다.

위에서 소개한 코드를 rpi_vfs_statfs_operation.c 파일로 저장합니다.
코드를 입력했으니 컴파일을 해볼 차례입니다. 컴파일을 쉽게 하기 위해 다음과 같이 코드를 작성하고 파일 이름을 Makefile으로 저장합시다.
statfs_proc: rpi_vfs_statfs_operation.c
gcc -o statfs_proc rpi_vfs_statfs_operation.c
이후 다음과 같이 make 명령어를 입력해 rpi_vfs_statfs_operation.c 소스 파일을 컴파일합시다.
root@raspberrypi:/home/pi# make
gcc -o statfs_proc rpi_vfs_statfs_operation.c
오타없이 코드를 입력하면 위와 같은 메시지가 출력되면서 statfs_proc 파일이 생성될 것입니다. rpi_vfs_statfs_operation.c 소스 파일의 실행 파일 이름은 statfs_proc입니다.

ftrace 설정해보기  

이번에는 ftrace 설정 방법을 소개합니다.
01 #!/bin/bash
02
03 echo 0 > /sys/kernel/debug/tracing/tracing_on
04 sleep 1
05 echo "tracing_off" 
06
07 echo 0 > /sys/kernel/debug/tracing/events/enable
08 sleep 1
09 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
20 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/enable
21 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/enable
22 sleep 1
23 echo "event enabled"
24
25 echo ext4_statfs simple_statfs > /sys/kernel/debug/tracing/set_ftrace_filter
26 sleep 1
27 echo "set_ftrace_filter enabled"
28
29 sleep 1
30 echo "set_ftrace_filter enabled"
31
32 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
33 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
34 echo "function stack trace enabled"
35
36 echo 1 > /sys/kernel/debug/tracing/tracing_on
37 echo "tracing_on"
이전 장에서 소개한 ftrace 설정 명령어와 차이점 부분 위주로 살펴보겠습니다.

다음 명령어를 소개합니다.
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
20 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/enable
21 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/enable

시스템 콜 동작을 확인하기 위해 프로세스 스케줄링과 시스템 콜 이벤트를 키는 명령어입니다.

이어서 함수 필터를 설정하는    째 줄 명령어를 보겠습니다.
25 echo ext4_statfs simple_statfs > /sys/kernel/debug/tracing/set_ftrace_filter

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

함수 이름이 조금 친숙해 보이지 않아요? 모두 13.3 절에서 분석한 슈퍼블록 함수 오퍼레이션 함수입니다. 위에서 소개한 ftrace 설정 명령어를 입력한 후 rpi_vfs_statfs.sh 이름으로 저장합시다.

ftrace 로그 추출하는 방법 알아보기 

실습 코드 실행 파일(statfs_proc)이 준비됐고 ftrace 설정 방법을 확인했습니다.
이어서 ftrace 를 설정하고 statfs_proc 파일을 실행할 차례입니다.

먼저 rpi_vfs_statfs.sh 셸 스크립트를 실행해 ftrace를 설정합시다.
root@raspberrypi:/home/pi # ./rpi_vfs_statfs.sh

다음 "./vfs_file_proc" 명령어를 입력해 vfs_file_proc 파일을 실행합니다.
root@raspberrypi:/home/pi # ./vfs_file_proc
statfs under /home/pi 
statfs under /proc 

다음 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 

ftrace 로그 분석해보기 

01 statfs_proc-2571 [002] .... 8294.737296: sys_enter: NR 99 (7e8ac508, 7e8ac588, 7e8ac588, 7e8ac508, 105c8, 0)
02 statfs_proc-2571 [002] .... 8294.737310: ext4_statfs+0x14/0x398 <-statfs_by_dentry+0x58/0x7c
03 statfs_proc-2571  [002] .... 8294.737345: <stack trace>
04  => ext4_statfs+0x18/0x398
05  => statfs_by_dentry+0x58/0x7c
06  => vfs_statfs+0x24/0x94
07  => user_statfs+0x64/0xac
08  => sys_statfs+0x34/0x64
09  => __sys_trace_return+0x0/0x10
10  => 0x7e8ac504
11 statfs_proc-2571 [002] .... 8294.737358: sys_exit: NR 99 = 0
...
12 statfs_proc-2571 [002] .... 8294.737395: simple_statfs+0x10/0x38 <-statfs_by_dentry+0x58/0x7c
13 statfs_proc-2571  [002] .... 8294.737412: <stack trace>
14  => simple_statfs+0x14/0x38
15  => statfs_by_dentry+0x58/0x7c
16  => vfs_statfs+0x24/0x94
17  => user_statfs+0x64/0xac
18  => sys_statfs+0x34/0x64
19  => __sys_trace_return+0x0/0x10
20 => 0x7e8ac504
21 statfs_proc-2571  [002] ....  8294.737417: sys_exit: NR 99 = 0 

ftrace 로그에서 함수 실행 흐름은 다음과 같이 2단계로 나눌 수 있습니다.
 
[그림 13.25] ftrace: 슈퍼블록 statfs 함수 오퍼레이션 실행 흐름도

유저 공간에서 statfs() 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_statfs() 함수가 호출되는 실행 흐름입니다. 이후 statfs_by_dentry() 함수가 호출되어 슈퍼블록 객체 함수 오퍼레이션을 수행합니다.

ext4로 표시된 부분은 유저 공간에서 다음 함수가 실행하면 호출됩니다.
18 strcpy(fname, GENERAL_DIR);
19
20 printf("statfs under %s \n", GENERAL_DIR);
21 if(statfs(fname, &file_sys_info)) {
22 printf("Unable to statfs %s \n", fname);
23 exit(1);
24 }

가상 파일 시스템 내부에서 "/home/pi" 디렉토리를 관리하는 슈퍼블록 객체에 접근해 해당 폴더를 관리하는 ext4 파일시스템 정보를 읽습니다.

proc로 표시된 부분은 유저 공간에서 다음 함수가 실행하면 호출됩니다.
26 strcpy(fname, PROC_DIR);
27 printf("statfs under %s \n", PROC_DIR);
28 if(statfs(fname, &file_sys_info)) {
29 printf("Unable to statfs %s \n", fname);
30 exit(1);
31 }

가상 파일 시스템 내부에서 "/proc" 디렉토리를 관리하는 슈퍼블록 객체에 접근해 해당 폴더를 관리하는 proc 파일시스템 정보를 읽습니다.

먼저 01번째 줄 로그를 보겠습니다.
01 statfs_proc-2571 [002] .... 8294.737296: sys_enter: NR 99 (7e8ac508, 7e8ac588, 7e8ac588, 7e8ac508, 105c8, 0)

99번 statfs 시스템 콜이 발생했다는 정보입니다. 99번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_statfs (__NR_SYSCALL_BASE+ 99)

02~10번째 줄 로그에서 statfs 시스템 콜이 발생한 다음 ext4_statfs() 함수 콜스택을 볼 수 있습니다.

이어서 ext4_statfs() 함수를 호출하는 statfs_by_dentry() 함수를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/statfs.c]
1 static int statfs_by_dentry(struct dentry *dentry, struct kstatfs *buf)
2 {
3 int retval;
4
5 if (!dentry->d_sb->s_op->statfs)
6 return -ENOSYS;
7
8 memset(buf, 0, sizeof(*buf));
9 retval = security_sb_statfs(dentry);
10 if (retval)
11 return retval;
12 retval = dentry->d_sb->s_op->statfs(dentry, buf);

다음 12번째 줄 코드를 분석하겠습니다. 
12 retval = dentry->d_sb->s_op->statfs(dentry, buf);

슈퍼 블록 함수 오퍼레이션으로 지정한 statfs 함수를 호출합니다.

ext4 파일시스템 슈퍼블록 함수 오퍼레이션에서 statfs 함수 포인터는 ext4_statfs() 함수 주소를 저장하고 있습니다. 

    따라서 04~05번째 줄 로그와 같이 ext4_statfs() 함수를 호출합니다.

proc 파일시스템의 경우 statfs 함수 포인터는 simple_statfs() 함수 주소를 저장하므로 14~15번째 줄 로그와 같이 simple_statfs() 함수를 호출합니다.


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

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


Reference(가상 파일시스템)

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


[리눅스커널] 가상파일시스템/디버깅: 파일 객체 함수 오퍼레이션 확인하기(2) 13장. 가상 파일시스템

ftrace 로그 분석해보기

분석할 ftrace 로그는 다음과 같습니다.
#open
01 vfs_file_proc-2102 [001] .... 3993.231746: sys_enter: NR 5 (76fe7ed8, 80000, 3, 76ffac90, 76fe7ed8, 7edfa524)
02 vfs_file_proc-2102 [001] .... 3993.231764: ext4_file_open+0x14/0x1dc <-do_dentry_open+0x240/0x3c4
03 vfs_file_proc-2102 [001] .... 3993.231798: <stack trace>
04 => ext4_file_open+0x18/0x1dc
05 => do_dentry_open+0x240/0x3c4
06 => vfs_open+0x3c/0x40
07 => path_openat+0x3a8/0x1000
08 => do_filp_open+0x84/0xf0
09 => do_sys_open+0x144/0x1f4
10 => sys_open+0x28/0x2c
11 => __sys_trace_return+0x0/0x10
12 => 0x7edfa434
13 vfs_file_proc-2102  [001] ....  3993.231809: sys_exit: NR 5 = 3
...
#read 
14 vfs_file_proc-2102 [001] .... 3993.231946: sys_enter: NR 3 (3, 7edfa080, 200, 0, 76ff7000, 0)
15 vfs_file_proc-2102 [001] .... 3993.231952: ext4_file_read_iter+0x10/0x54 <-__vfs_read+0x108/0x168
16 vfs_file_proc-2102 [001] .... 3993.231969: <stack trace>
17 => ext4_file_read_iter+0x14/0x54
18 => __vfs_read+0x108/0x168
19 => vfs_read+0x9c/0x164
20 => ksys_read+0x5c/0xbc
21 => sys_read+0x18/0x1c
22 => __sys_trace_return+0x0/0x10
23 => 0x7edf9fdc
24 vfs_file_proc-2102  [001] ....  3993.231977: sys_exit: NR 3 = 512
...
#write(x1)
25 vfs_file_proc-2102 [001] .... 3993.234630: sys_enter: NR 4 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)
26 vfs_file_proc-2102 [001] .... 3993.234636: ext4_file_write_iter+0x14/0x4c0 <-__vfs_write+0x10c/0x170
27 vfs_file_proc-2102 [001] .... 3993.234654: <stack trace>
28 => ext4_file_write_iter+0x18/0x4c0
29 => __vfs_write+0x10c/0x170
30 => vfs_write+0xb4/0x1c0
31 => ksys_write+0x5c/0xbc
32 => sys_write+0x18/0x1c
33 => __sys_trace_return+0x0/0x10
34 => 0x7edfa4a4
35 vfs_file_proc-2102 [001] .... 3993.234796: sys_exit: NR 4 = 16
...
#lseek
36 vfs_file_proc-2102 [001] .... 3993.234803: sys_enter: NR 19 (3, 0, 0, 7edfa4a8, 1077c, 0)
37 vfs_file_proc-2102 [001] .... 3993.234807: ext4_llseek+0x14/0x15c <-ksys_lseek+0xa8/0xd8
38 vfs_file_proc-2102 [001] .... 3993.234821: <stack trace>
39 => ext4_llseek+0x18/0x15c
40 => ksys_lseek+0xa8/0xd8
41 => sys_lseek+0x18/0x1c
42 => __sys_trace_return+0x0/0x10
43 => 0x7edfa4a4
44 vfs_file_proc-2102  [001] .... 3993.234825: sys_exit: NR 19 = 0
...
#write(x2)
45 vfs_file_proc-2102 [001] .... 3993.235673: sys_enter: NR 4 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)
46 vfs_file_proc-2102 [001] .... 3993.235677: ext4_file_write_iter+0x14/0x4c0 <-__vfs_write+0x10c/0x170
47 vfs_file_proc-2102 [001] .... 3993.235702: <stack trace>
48 => ext4_file_write_iter+0x18/0x4c0
49 => __vfs_write+0x10c/0x170
50 => vfs_write+0xb4/0x1c0
51 => ksys_write+0x5c/0xbc
52 => sys_write+0x18/0x1c
53 => __sys_trace_return+0x0/0x10
54 => 0x7edfa4a4
55 vfs_file_proc-2102 [001] .... 3993.235757: sys_exit: NR 4 = 16
...
#fsync 
56 vfs_file_proc-2102 [001] .... 3993.235767: sys_enter: NR 118 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)
57 vfs_file_proc-2102 [001] .... 3993.235770: ext4_sync_file+0x14/0x464 <-vfs_fsync_range+0x4c/0x8c
58 vfs_file_proc-2102 [001] .... 3993.235796: <stack trace>
59 => ext4_sync_file+0x18/0x464
60 => vfs_fsync_range+0x4c/0x8c
61 => do_fsync+0x4c/0x74
62 => sys_fsync+0x1c/0x20
63 => __sys_trace_return+0x0/0x10
64 => 0x7edfa4a4
... 
65 vfs_file_proc-2102 [001] .... 3993.247132: sys_exit: NR 118 = 0 
...
#close
66 vfs_file_proc-2102 [001] .... 3993.247156: sys_enter: NR 6 (3, 7edfa4a8, 10, 0, 1077c, 0)
67 vfs_file_proc-2102 [001] .... 3993.247162: __close_fd+0x10/0xa0 <-sys_close+0x30/0x58
68 vfs_file_proc-2102 [001] .... 3993.247197: <stack trace>
69 => __close_fd+0x14/0xa0
70 => sys_close+0x30/0x58
71 => __sys_trace_return+0x0/0x10
72 => 0x7edfa4a4
73 vfs_file_proc-2102  [001] ....  3993.247203: sys_exit: NR 6 = 0

위에서 소개한 ftrace 실행 흐름은 다음 그림과 같이 분류할 수 있습니다.
 
[그림 13.18] ftrace에서 시스템 콜 실행 순서

먼저 그림 왼쪽 부분을 눈으로 따라가봅시다. 유저 공간 어플리케이션에서 실행한 함수 이름입니다. 이번 소절에서 소개한 어플리케이션 예제 코드에서 호출된 함수입니다. 다음 가운데 부분을 보면 시스템콜이란 화살표가 보입니다. 유저 공간에서 커널 공간으로 진입하려면 이렇게 시스템 콜을 발생해야 합니다. 

이번에 오른쪽 화면 오른쪽 함수 목록을 보면 첫 번째 함수는 시스템 콜 핸들러 함수입니다. 다음 그 아래에 있는 함수는 파일시스템별 함수 오퍼레이션을 수행합니다. 예를 들어 유저 공간에서 open 시스템 콜을 발생하면 시스템 콜 핸들러는 sys_open() 함수이고 open() 함수에 대한 파일 객체 함수 오퍼레이션은 do_dentry_open() 함수에서 실행합니다.  

이번 소절에서 읽고 쓰기를 하는 파일은 /home/pi/sample_text.text입니다. 라즈비안에서 저장매체에 저장하는 파일은 ext4 파일 시스템에서 관리합니다. 따라서 ext4_xxx와 ext4 시작되는 함수들이 호출됩니다

만약 proc 파일시스템에서 관리하는 파일을 읽기 쓰기 연산을 하면 proc_xxx으로 시작하는 함수들이 호출될 것입니다.

open 파일 오퍼레이션 동작 함수 흐름 확인하기  

파일을 읽기 쓰기 위해서 먼저 파일을 먼저 열어야 합니다. 이를 위해 유저 공간에서 오픈이란 함수를 호출 해야 합니다. 유저 공간에서 open() 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_open() 함수가 호출됩니다. 이점을 염두하면서 다음 ftrace 로그를 분석합시다.

분석할 ftrace 메시지는 다음과 같습니다.
#open
01 vfs_file_proc-2102 [001] .... 3993.231746: sys_enter: NR 5 (76fe7ed8, 80000, 3, 76ffac90, 76fe7ed8, 7edfa524)
02 vfs_file_proc-2102 [001] .... 3993.231764: ext4_file_open+0x14/0x1dc <-do_dentry_open+0x240/0x3c4
03 vfs_file_proc-2102 [001] .... 3993.231798: <stack trace>
04 => ext4_file_open+0x18/0x1dc
05 => do_dentry_open+0x240/0x3c4
06 => vfs_open+0x3c/0x40
07 => path_openat+0x3a8/0x1000
08 => do_filp_open+0x84/0xf0
09 => do_sys_open+0x144/0x1f4
10 => sys_open+0x28/0x2c
11 => __sys_trace_return+0x0/0x10
12 => 0x7edfa434
13 vfs_file_proc-2102  [001] ....  3993.231809: sys_exit: NR 5 = 3

복잡해 보이는 ftrace 로그에서 함수 실행 흐름을 정리하면 다음 그림과 같습니다.
 
[그림 13.19] ftrace: 파일 오픈 시 open 함수 오퍼레이션 실행 흐름도

유저 공간에서 open() 오픈 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_open() 함수가 호출되는 실행 흐름입니다. 이후 do_dentry_open() 함수가 호출되어 파일 객체 함수 오퍼레이션을 수행합니다.

01번째 줄 로그를 보겠습니다.
01 vfs_file_proc-2102 [001] .... 3993.231746: sys_enter: NR 5 (76fe7ed8, 80000, 3, 76ffac90, 76fe7ed8, 7edfa524)

5번 open 시스템 콜이 발생했다는 정보입니다. 5번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_open (__NR_SYSCALL_BASE+  5)

2~11번째 줄 로그에서 open 시스템 콜이 발생한 다음 ext4_file_open() 함수 콜스택을 볼 수 있습니다.

이어서 ext4_file_open() 함수를 호출하는 코드를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/open.c]
01 static int do_dentry_open(struct file *f,
02   struct inode *inode,
03   int (*open)(struct inode *, struct file *))
04 {
05 static const struct file_operations empty_fops = {};
06 int error;
...
07 f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
08 if (!open)
09 open = f->f_op->open;
10 if (open) {
11 error = open(inode, f);
12 if (error)
13 goto cleanup_all;
14 }
...

do_dentry_open() 함수 09번째 줄에서 파일 오퍼레이션 중 open 함수 포인터를 open에 저장합니다. 이후 11번째 줄에서 ext4_file_open() 함수를 호출합니다.

read 파일 오퍼레이션 동작 함수 흐름 확인하기  

이어서 유저 공간에서 read() 함수를 호출했을 때 커널 내부 함수 실행 흐름을 살펴보겠습니다.

분석할 ftrace 메시지는 다음과 같습니다.
#read 
14 vfs_file_proc-2102 [001] .... 3993.231946: sys_enter: NR 3 (3, 7edfa080, 200, 0, 76ff7000, 0)
15 vfs_file_proc-2102 [001] .... 3993.231952: ext4_file_read_iter+0x10/0x54 <-__vfs_read+0x108/0x168
16 vfs_file_proc-2102 [001] .... 3993.231969: <stack trace>
17 => ext4_file_read_iter+0x14/0x54
18 => __vfs_read+0x108/0x168
19 => vfs_read+0x9c/0x164
20 => ksys_read+0x5c/0xbc
21 => sys_read+0x18/0x1c
22 => __sys_trace_return+0x0/0x10
23 => 0x7edf9fdc
24 vfs_file_proc-2102  [001] ....  3993.231977: sys_exit: NR 3 = 512

ftrace 로그에서 함수 실행 흐름은 다음 그림과 같습니다.
 
[그림 13.20] ftrace: 파일을 읽을 때 read 함수 오퍼레이션 실행 흐름도

유저 공간에서 read() 오픈 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_read() 함수가 호출되는 실행 흐름입니다. 이후 __vfs_read() 함수가 호출되어 파일 객체 read 함수 오퍼레이션을 수행합니다.

14번째 줄 로그를 보겠습니다.
14 vfs_file_proc-2102 [001] .... 3993.231946: sys_enter: NR 3 (3, 7edfa080, 200, 0, 76ff7000, 0)

3번 read 시스템 콜이 발생했다는 정보입니다. 3번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_read (__NR_SYSCALL_BASE+  3)

15~23번째 줄 로그에서 read 시스템 콜이 발생한 다음 ext4_file_read_iter() 함수 콜스택을 볼 수 있습니다.

이어서 ext4_file_read_iter() 함수를 호출하는 코드를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/open.c]
01 static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
02 {
03 struct iovec iov = { .iov_base = buf, .iov_len = len };
04 struct kiocb kiocb;
05 struct iov_iter iter;
06 ssize_t ret;
07
08 init_sync_kiocb(&kiocb, filp);
09 kiocb.ki_pos = *ppos;
10 iov_iter_init(&iter, READ, &iov, 1, len);
11
12 ret = call_read_iter(filp, &kiocb, &iter);
...

new_sync_read() 함수 11번째 줄과 같이 call_read_iter() 함수에서 ext4_file_open() 함수를 호출합니다.

write 파일 오퍼레이션 동작 함수 흐름 확인하기  

이어서 유저 공간에서 write() 함수를 호출했을 때 커널 내부 함수 실행 흐름을 살펴보겠습니다.

분석할 ftrace 메시지는 다음과 같습니다.
#write(x1)
25 vfs_file_proc-2102 [001] .... 3993.234630: sys_enter: NR 4 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)
26 vfs_file_proc-2102 [001] .... 3993.234636: ext4_file_write_iter+0x14/0x4c0 <-__vfs_write+0x10c/0x170
27 vfs_file_proc-2102 [001] .... 3993.234654: <stack trace>
28 => ext4_file_write_iter+0x18/0x4c0
29 => __vfs_write+0x10c/0x170
30 => vfs_write+0xb4/0x1c0
31 => ksys_write+0x5c/0xbc
32 => sys_write+0x18/0x1c
33 => __sys_trace_return+0x0/0x10
34 => 0x7edfa4a4
35 vfs_file_proc-2102 [001] .... 3993.234796: sys_exit: NR 4 = 16

ftrace 로그에서 함수 실행 흐름은 다음 그림과 같습니다.
 
[그림 13.21] ftrace: 파일을 쓸 때 write 함수 오퍼레이션 실행 흐름도

유저 공간에서 write() 오픈 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_write() 함수가 호출되는 실행 흐름입니다. 이후 __vfs_write() 함수가 호출되어 파일 객체 write 함수 오퍼레이션을 수행합니다.

25번째 줄 로그를 보겠습니다.
25 vfs_file_proc-2102 [001] .... 3993.234630: sys_enter: NR 4 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)

4번 write 시스템 콜이 발생했다는 정보입니다. 4번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_write (__NR_SYSCALL_BASE+  4)

26~33번째 줄 로그에서 write 시스템 콜이 발생한 다음에 ext4_file_write_iter() 함수 콜스택을 볼 수 있습니다.

이어서 ext4_file_write_iter() 함수를 호출하는 코드를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/read_write.c]
01 static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
02 {
03 struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
04 struct kiocb kiocb;
05 struct iov_iter iter;
06 ssize_t ret;
07
08 init_sync_kiocb(&kiocb, filp);
09 kiocb.ki_pos = *ppos;
10 iov_iter_init(&iter, WRITE, &iov, 1, len);
11
12 ret = call_write_iter(filp, &kiocb, &iter);
13 BUG_ON(ret == -EIOCBQUEUED);
14 if (ret > 0)
15 *ppos = kiocb.ki_pos;
16 return ret;
17 }

new_sync_write() 함수 12번째 줄과 같이 call_read_iter() 함수에서 ext4_file_write_iter() 함수를 호출합니다.

lseek 파일 오퍼레이션 동작 함수 흐름 확인하기 

유저 공간에서 파일을 읽고 쓰기 할 때 파일 포인터 위치를 바꿔야 할 때가 있습니다. 이럴 때 lseek() 함수를 호출하면 함수에 지정된 옵션에 따라 파일 포인터 위치를 반환합니다.

이번엔 파일 포인터 위치를 설정하기 위해 유저 공간에서 lseek() 함수를 호출할 때 커널 공간에서 함수 실행 흐름을 확인하겠습니다.

분석할 ftrace 메시지는 다음과 같습니다.
#lseek
36 vfs_file_proc-2102 [001] .... 3993.234803: sys_enter: NR 19 (3, 0, 0, 7edfa4a8, 1077c, 0)
37 vfs_file_proc-2102 [001] .... 3993.234807: ext4_llseek+0x14/0x15c <-ksys_lseek+0xa8/0xd8
38 vfs_file_proc-2102 [001] .... 3993.234821: <stack trace>
39 => ext4_llseek+0x18/0x15c
40 => ksys_lseek+0xa8/0xd8
41 => sys_lseek+0x18/0x1c
42 => __sys_trace_return+0x0/0x10
43 => 0x7edfa4a4
44 vfs_file_proc-2102  [001] .... 3993.234825: sys_exit: NR 19 = 0

ftrace 로그에서 함수 실행 흐름은 다음 그림과 같습니다.
 
 [그림 13.22] ftrace: 파일 포인터 위치를 설정할 때 lseek 함수 오퍼레이션 실행 흐름도

유저 공간에서 lseek() 오픈 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_lseek() 함수가 호출되는 실행 흐름입니다. 이후 vfs_llseek() 함수가 호출되어 파일 객체 lseek 함수 오퍼레이션을 수행합니다.

36번째 줄 로그를 보겠습니다.
36 vfs_file_proc-2102 [001] .... 3993.234803: sys_enter: NR 19 (3, 0, 0, 7edfa4a8, 1077c, 0)

19번 write 시스템 콜이 발생했다는 정보입니다. 19번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_lseek (__NR_SYSCALL_BASE+ 19)

37~44번째 줄 로그에서 lseek 시스템 콜이 발생한 다음 ext4_llseek() 함수 콜스택을 볼 수 있습니다.

이어서 ext4_llseek() 함수를 호출하는 코드를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/read_write.c]
01 loff_t vfs_llseek(struct file *file, loff_t offset, int whence)
02 {
03 loff_t (*fn)(struct file *, loff_t, int);
04
05 fn = no_llseek;
06 if (file->f_mode & FMODE_LSEEK) {
07 if (file->f_op->llseek)
08 fn = file->f_op->llseek;
09 }
10 return fn(file, offset, whence);
11 }
...

vfs_llseek() 함수 08번째 줄 코드에서 ext4_llseek() 함수를 호출합니다.

fsync 파일 오퍼레이션 동작 함수 흐름 확인하기  

어플리케이션에서 read()/write() 함수를 써서 파일을 변경하거나 저장합니다. 이 과정에서 커널 내부에서 버퍼를 잡아 변경된 파일을 처리합니다. 즉, 파일을 변경하면 그 내용이 바로 저장 매체에 저장되는 것은 아닙니다. 그래서 읽기 쓰기 동작 후 변경한 내용을 저장매체에 저장하려면 fsync() 함수를 호출해야 합니다. 이 점을 기억하고 다음 ftrace 로그를 분석합시다.

#fsync 
56 vfs_file_proc-2102 [001] .... 3993.235767: sys_enter: NR 118 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)
57 vfs_file_proc-2102 [001] .... 3993.235770: ext4_sync_file+0x14/0x464 <-vfs_fsync_range+0x4c/0x8c
58 vfs_file_proc-2102 [001] .... 3993.235796: <stack trace>
59 => ext4_sync_file+0x18/0x464
60 => vfs_fsync_range+0x4c/0x8c
61 => do_fsync+0x4c/0x74
62 => sys_fsync+0x1c/0x20
63 => __sys_trace_return+0x0/0x10
64 => 0x7edfa4a4
... 
65 vfs_file_proc-2102 [001] .... 3993.247132: sys_exit: NR 118 = 0

ftrace 로그에서 함수 실행 흐름은 다음 그림과 같습니다.
 
  [그림 13.23] ftrace: 파일 fsync 함수 오퍼레이션 실행 흐름도

유저 공간에서 fsync() 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_fsync() 함수가 호출되는 실행 흐름입니다. 이후 vfs_fsync_range() 함수가 호출되어 파일 객체 fsync 함수 오퍼레이션을 수행합니다.

56번째 줄 로그를 보겠습니다.
56 vfs_file_proc-2102 [001] .... 3993.235767: sys_enter: NR 118 (3, 7edfa4a8, 10, 7edfa4a8, 1077c, 0)

118번 fsync 시스템 콜이 발생했다는 정보입니다. 118번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_fsync (__NR_SYSCALL_BASE+118)

57~64번째 줄 로그에서 fsync 시스템 콜이 발생한 다음 ext4_sync_file() 함수 콜스택을 볼 수 있습니다.

이어서 ext4_sync_file() 함수를 호출하는 코드를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/sync.c]
01 int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
02 {
03 struct inode *inode = file->f_mapping->host;
04
05 if (!file->f_op->fsync)
06 return -EINVAL;
07 if (!datasync && (inode->i_state & I_DIRTY_TIME))
08 mark_inode_dirty_sync(inode);
09 return file->f_op->fsync(file, start, end, datasync);
10 }

vfs_fsync_range() 함수 09번째 줄 코드에서 ext4_sync_file() 함수를 호출합니다.

close 파일 오퍼레이션 동작 함수 흐름 확인하기 

마지막으로 유저 공간에서 파일을 닫을 때 close() 함수를 호출하면 커널 공간에서 어떤 함수가 실행되는지 살펴보겠습니다.
#close
66 vfs_file_proc-2102 [001] .... 3993.247156: sys_enter: NR 6 (3, 7edfa4a8, 10, 0, 1077c, 0)
67 vfs_file_proc-2102 [001] .... 3993.247162: __close_fd+0x10/0xa0 <-sys_close+0x30/0x58
68 vfs_file_proc-2102 [001] .... 3993.247197: <stack trace>
69 => __close_fd+0x14/0xa0
70 => sys_close+0x30/0x58
71 => __sys_trace_return+0x0/0x10
72 => 0x7edfa4a4
73 vfs_file_proc-2102  [001] ....  3993.247203: sys_exit: NR 6 = 0

ftrace 로그에서 함수 실행 흐름은 다음 그림과 같습니다.
 
[그림 13.24] ftrace: 파일을 닫을 때 close 함수 오퍼레이션 실행 흐름도

유저 공간에서 close() 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_close() 함수가 호출되는 실행 흐름입니다. sys_close() 함수가 호출되면 커널 내부에서 파일시스템별 함수 오퍼레이션을 실행하지 않습니다. 단지 프로세스 태스크 디스크립터 내 파일 디스크립터 테이블에 저장된 파일 디스크립터를 지웁니다.

66번째 줄 로그를 보겠습니다.
66 vfs_file_proc-2102 [001] .... 3993.247156: sys_enter: NR 6 (3, 7edfa4a8, 10, 0, 1077c, 0)

6번 close 시스템 콜이 발생했다는 정보입니다. 6번 시스템 콜 번호는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.4.180/source/arch/arm/include/uapi/asm/unistd.h]
#define __NR_close (__NR_SYSCALL_BASE+  6)

67~72번째 줄 로그에서 close 시스템 콜이 발생한 다음 __close_fd() 함수 콜스택을 볼 수 있습니다.

이어서 __close_fd() 함수를 같이 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/file.c]
01 int __close_fd(struct files_struct *files, unsigned fd)
02 {
03 struct file *file;
04 struct fdtable *fdt;
05
06 spin_lock(&files->file_lock);
07 fdt = files_fdtable(files);
08 if (fd >= fdt->max_fds)
09 goto out_unlock;
10 file = fdt->fd[fd];
11 if (!file)
12 goto out_unlock;
13 rcu_assign_pointer(fdt->fd[fd], NULL);

__close_fd() 함수 13번째 줄 코드와 같이 파일 디스크립터 테이블에서 해당 파일 디스크립터를 초기화합니다.

여기까지 실습을 통해 커널 내부 함수 흐름을 ftrace로 살펴봤습니다.
실습으로 다음 내용을 알게 됐습니다.
각 시스템콜 별과 연계해 가상 파일시스템에서 관리하는 파일 객체 함수 오퍼레이션을 수행함 
함수 오퍼레이션은 함수 포인터를 써서 수행함 

다른 관점으로 보면 리눅스 디바이스 드라이버에서 지정한 함수 오퍼레이션(연산)도 이번 장에서 배운 파일 객체 함수 오퍼레이션 구조에서 실행됩니다. 이번 소절에서 실습한 내용을 응용해서 여러분이 작성한 디바이스 드라이버 함수 오퍼레이션 동작도 ftrace로 확인해 보시길 바랍니다.


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

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


Reference(가상 파일시스템)

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




[리눅스커널] 가상파일시스템/디버깅: 파일 객체 함수 오퍼레이션 확인하기(1) 13장. 가상 파일시스템

이번 소절에서 유저 어플리케이션에서 다음 함수를 호출하면 커널 가상 파일시스템에서 어떤 함수를 호출하는지 알아봅니다.
open
write
read
lseek
fsync
close 

실습 패치 코드 작성해보기 

소스 코드는 다음과 같으니 같이 입력해 봅시다.
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <unistd.h>
04 #include <sys/types.h>
05 #include <signal.h>
06 #include <string.h>
07 #include <fcntl.h>
08
09 #define FILENAME_NAME "/home/pi/sample_text.text"
10
11 int main() 
12 {
13    int fd = 0;
14    ssize_t read_buf_size;
15    off_t new_file_pos;
16
17    char buf[256];
18    char string[] = "Raspbian Linux!\n";
19
20   memset(buf, 0x0, sizeof(buf));
21    
22    fd = open(FILENAME_NAME, O_RDWR);
23   
24    read_buf_size = read(fd, buf, 256);
25    printf("%s", buf);
26
27    write(fd, string, strlen(string));
28    
29    new_file_pos = lseek(fd, (off_t)0, SEEK_SET);
30    
31    read_buf_size = read(fd, buf, 256);
32    printf("read again \n");
33    printf("[+]read buffer: %s \n", buf);
34
35   write(fd, string, strlen(string));
36   if ( -1 == fsync(fd)) {
37 printf( "fsync() fails"); 
38 exit(0);
39   }
40
41   close(fd);
42
43    return 0;
44 }


위 프로그램은 /home/pi/sample_text.text 파일을 읽고 쓰는 동작입니다.
따라서 다음과 같이 /home/pi 디렉토리로 이동해 "touch sample_text.text" 명령어를 입력해 파일을 먼저 만들어야 합니다.
root@raspberrypi:/home/pi# touch sample_text.text


코드 내용은 어렵지 않으니 간단히 리뷰하는 수준으로 설명을 드리겠습니다.

22~25번째 줄 코드를 보겠습니다.
22    fd = open(FILENAME_NAME, O_RDWR);
23   
24    read_buf_size = read(fd, buf, 256);
25    printf("%s", buf);

22번째 줄 코드에서 "/home/pi/sample_text.text" 파일을 오픈한 후 파일 디스크립터를 fd로 얻어옵니다. 이후 24번째 코드를 실행해 버퍼인 buf 변수를 통해 "/home/pi/sample_text.text" 내용을 읽어 터미널에 출력하는 동작입니다.

다음 27~29번째 줄 코드입니다.
27    write(fd, string, strlen(string));
28    
29    new_file_pos = lseek(fd, (off_t)0, SEEK_SET);

write() 함수를 호출해 "Raspbian Linux!\n" 스트링을 sample_text.text 파일 쓰기 버퍼에 써줍니다. 이후 lseek() 함수를 호출해 파일 포인터 위치를 가장 앞부분에 설정합니다.

이어서 31~33번째 줄 코드를 보겠습니다.
31    read_buf_size = read(fd, buf, 256);
32    printf("read again \n");
33    printf("[+]read buffer: %s \n", buf);

read() 함수를 호출해 sample_text.text 파일 버퍼를 읽어서 터미널에 출력합니다. 

다음 35~39번째 줄 코드를 보겠습니다.
35   write(fd, string, strlen(string));
36   if ( -1 == fsync(fd)) {
37 printf( "fsync() fails"); 
38 exit(0);
39   }

write() 함수를 호출해 "Raspbian Linux!\n" 스트링을 sample_text.text 파일 쓰기 버퍼에 써줍니다. 다음 36번째 줄 코드와 같이 fsync() 함수를 호출해 저장매체와 동기화를 수행합니다.

마지막 41번째 줄 코드를 보겠습니다.
41   close(fd);

close() 함수를 호출해 파일 디스크립터를 닫는 동작입니다.

리눅스 시스템 프로그램을 한번이라도 해본 분이면 이해할 수 있는 수준의 코드입니다.

위에서 소개한 코드를 rpi_vfs_file_operation.c 파일로 저장합니다.
코드를 입력했으니 컴파일을 해볼 차례입니다. 컴파일을 쉽게 하기 위해 다음과 같이 코드를 작성하고 파일 이름을 Makefile으로 저장합시다.
vfs_file_proc: rpi_vfs_file_operation.c
gcc -o vfs_file_proc rpi_vfs_file_operation.c

이후 다음과 같이 make 명령어를 입력해 rpi_vfs_file_operation.c 소스 파일을 컴파일합시다.
root@raspberrypi:/home/pi# make
gcc -o vfs_file_proc rpi_vfs_file_operation.c
오타없이 코드를 입력하면 위와 같은 메시지가 출력되면서 vfs_file_proc 파일이 생성될 것입니다. rpi_vfs_file_operation.c 소스 파일의 실행 파일 이름은 vfs_file_proc입니다.

ftrace 설정해보기 

이번에는 ftrace 설정 방법을 소개합니다.
01 #!/bin/bash
02
03 echo 0 > /sys/kernel/debug/tracing/tracing_on
04 sleep 1
05 echo "tracing_off" 
06
07 echo 0 > /sys/kernel/debug/tracing/events/enable
08 sleep 1
09 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
20 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/enable
21 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/enable
22 sleep 1
23 echo "event enabled"
24
25 echo ext4_file_open ext4_file_write_iter > /sys/kernel/debug/tracing/set_ftrace_filter
26 echo ext4_file_read_iter ext4_llseek >> /sys/kernel/debug/tracing/set_ftrace_filter
27 echo ext4_sync_file __close_fd >> /sys/kernel/debug/tracing/set_ftrace_filter
28 sleep 1
29 echo "set_ftrace_filter enabled"
30
31 sleep 1
32 echo "set_ftrace_filter enabled"
33
34 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
35 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
36 echo "function stack trace enabled"
37
38 echo 1 > /sys/kernel/debug/tracing/tracing_on
39 echo "tracing_on"
이전 장에서 소개한 ftrace 설정 명령어와 다른 부분 위주로 살펴보겠습니다.

먼저 다음 명령어를 봅시다.
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
20 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/enable
21 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/enable

open, read, write, lseek, fsync, close 시스템 콜 동작을 확인하기 위해 프로세스 스케줄링과 시스템 콜 ftrace 이벤트를 키는 명령어입니다.

이어서 함수 필터를 설정하는 25~27번째 줄 명령어를 보겠습니다.
25 echo ext4_file_open ext4_file_write_iter > /sys/kernel/debug/tracing/set_ftrace_filter
26 echo ext4_file_read_iter ext4_llseek >> /sys/kernel/debug/tracing/set_ftrace_filter
27 echo ext4_sync_file __close_fd >> /sys/kernel/debug/tracing/set_ftrace_filter


위 명령어에서 26~27번째 줄 볼드체로 된 부분과 같이 ">>" 기호를 입력합시다. 만약 ">>" 대신 ">" 기호를 입력하면 윗 줄에 있는 명령어로 설정한 함수 필터 정보가 지워집니다.


위 명령어는 set_ftrace_filter에 다음 함수를 설정합니다.
ext4_file_open()
ext4_file_write_iter()
ext4_file_read_iter()
ext4_llseek()
ext4_sync_file()
__close_fd()

함수 이름이 조금 친숙해 보이지 않아요? 모두 13.3 절에서 분석한 ext4 파일시스템 파일 오퍼레이션 함수입니다.

위에서 소개한 ftrace 설정 명령어를 rpi_vfs_file_trace.sh 이름으로 저장합시다.

ftrace 로그 추출하는 방법 알아보기 

실습 코드 실행 파일(vfs_file_proc)이 준비됐고 ftrace 설정 방법을 확인했습니다. 이어서 ftrace 를 설정하고 vfs_file_proc 파일을 실행할 차례입니다.

먼저 rpi_vfs_file_trace.sh 셸 스크립트를 실행해 ftrace를 설정합시다.
root@raspberrypi:/home/pi # ./rpi_vfs_file_trace.sh

다음 "./vfs_file_proc" 명령어를 입력해 vfs_file_proc 파일을 실행합니다.
root@raspberrypi:/home/pi # ./vfs_file_proc
read again 
[+]read buffer: Raspbian Linux!

다음 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 


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

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


Reference(가상 파일시스템)

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



[리눅스커널] 가상파일시스템: struct dentry 구조체 분석 13장. 가상 파일시스템

덴트리에 대한 세부 속성은 struct dentry 구조체에서 확인할 수 있습니다.
먼저 struct dentry 구조체 선언부를 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/source/include/linux/dcache.h]
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */

/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */

union {
struct list_head d_lru; /* LRU list */
wait_queue_head_t *d_wait; /* in-lookup ones only */
};
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
/*
* d_alias and d_rcu can share memory
*/
union {
struct hlist_node d_alias; /* inode alias list */
struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */
struct rcu_head d_rcu;
} d_u;
} __randomize_layout;

struct dentry 구조체 필드 중에 중요한 속성을 알아 보겠습니다. 다음 테이블 목록에서 struct dentry 구조체 중 주요 필드를 볼 수 있습니다.
타입 필드 설명
unsigned int d_flags 덴트리 상태 플래그
struct dentry *d_parent 부모 디렉터리의 dentry 객체
struct qstr d_name 덴트리 이름
struct inode *d_inode 덴트리에 해당하는 아이노드
unsigned char d_iname[36] 덴트리 약칭 이름
const struct dentry_operations *d_op 덴트리 오퍼레이션 함수 테이블
struct super_block *d_sb 파일의 수퍼 블록 객체
void *d_fsdata 파일시스템에 따른 포인터 매개 변수
struct list_head d_child 디렉터리에 대해서, 동일한 부모 디렉터리 내 
디렉터리 dentry 리스트를 가리키는 포인터
struct list_head d_subdirs 디렉터리에 대해서, 하위 디렉터리 dentry들의 리스트 헤드

덴트리도 다른 객체와 마찬가지로 const struct dentry_operations 타입인 d_op 필드로 덴트리 함수 오퍼레이션을 지원합니다. 덴트리 함수 오퍼레이션은 세부 동작은 난이도가 너무 높아 이 책에서 다루지는 않습니다. 

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

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


Reference(가상 파일시스템)

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



[리눅스커널] 가상파일시스템: 덴트리 객체 소개 13장. 가상 파일시스템

유저 공간에서 다양한 디렉터리 패스 정보가 포함된 파일 정보를 인자로 시스템 콜로 호출합니다. 만약 유저 공간에서 다음 06번째 줄 코드와 같이 파일 오픈 요청을 한다고 가정합시다.
01 int main() 
02 {
03    int fd = 0;
04    ssize_t read_buf_size;
05   
06    fd = open("/home/pi/sample_text.text", O_RDWR);
...
07 }

물론 유저 공간에서 open() 함수를 호출하면 커널 공간에서 시스템 콜 핸들러인 sys_open() 함수를 호출할 것입니다. 이 과정에서 덴트리 객체는 다음과 같은 동작을 수행합니다. 
"/home" 디렉토리에서 pi 디렉토리를 검색
pi 디렉토리가 유효하고 접근 가능한지 점검
pi 디렉토리 내 sample.txt 파일이 있는지 체크
디렉토리의 상관 관계도 정확히 점검해 덴트리 구조를 생성: "/home" 하위 디렉토리에 pi가 있고 pi 하부 디렉토리에 sample_text.txt 

이렇게 디렉토리 경로를 해석하고 디렉토리 간 관계를 점검하는 동작을 덴트리 객체를 이용해서 수행합니다.

덴트리에 대해 소개를 했으니 다음 소절에서 덴트리 자료 구조를 알아보겠습니다.


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

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


Reference(가상 파일시스템)

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

[리눅스커널] 가상파일시스템: 파일 속성을 읽는 stat 시스템 콜 처리 과정 분석하기 13장. 가상 파일시스템

유저 공간에서 세부 파일 속성을 파악하려면 어떤 함수를 호출해야 할까요? stat() 함수를 호출하면 가상 파일시스템에서 아이노드 객체에 접근해 상세 파일 속성 정보를 읽습니다.
 
이번 소절에서는 stat() 함수를 호출하면 가상 파일시스템에서 어떤 흐름으로 아이노드 객체에 접근하는지 살펴보겠습니다. 

stat 시스템 콜 처리 과정 확인하기

먼저 stat() 함수를 호출하는 유저 어플리케이션 코드를 소개합니다.  
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <unistd.h>
04 #include <sys/types.h>
05 #include <sys/stat.h>
06 #include <signal.h>
07 #include <string.h>
08 #include <fcntl.h>
09 int main() 
10 {
11 struct stat fileinfo;
12 char fname[BUFF_SIZE] = {0,};
13
14 strcpy(fname, "/home/pi/sample_text.text");
15
16 printf("fname[%s] \n", fname);
17
18 if(stat(fname, &fileinfo)) {
19 printf("Unable to stat %s \n", fname);
20 exit(1);
21 }
22
23 return 0;
24 }

먼저 18번째 줄 코드를 보면 stat() 함수를 호출합니다.
첫 번째 인자로 파일 경로가 포함된 파일 이름인 "/home/pi/sample_text.text"으로 지정하고 두 번째 인자로 파일 속성 정보를 표현하는 struct stat 구조체 변수인 fileinfo를 전달합니다. stat() 함수가 실행되면 fileinfo에 파일 속성 정보가 업데이트됩니다.

sys_fstat64() 함수 세부 코드 분석하기

유저 공간에서 stat() 함수를 호출하면 실행하는 시스템 콜 핸들러 함수는 무엇일까요?
다음 코드와 같이 sys_fstat64() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 SYSCALL_DEFINE2(fstat64, unsigned long, fd, struct stat64 __user *, statbuf)
02 {
03 struct kstat stat;
04 int error = vfs_fstat(fd, &stat);
05
06 if (!error)
07 error = cp_new_stat64(&stat, statbuf);
08
09 return error;
10 }

sys_fstat64() 함수는 크게 2개 동작으로 분류할 수 있습니다.

1단계: 아이노드 속성 정보 읽기

vfs_fstat() 함수를 호출해서 아이노드에 저장된 파일 속성 정보를 읽습니다. 이 과정에서 다음 함수를 호출합니다.
vfs_fstat()
vfs_statx_fd()
vfs_getattr() 
vfs_getattr_nosec() 
generic_fillattr() 

2단계: 유저 공간에 파일 정보 복사

cp_new_stat() 함수를 실행해서 파일 속성 정보를 유저 공간(struct stat 구조체)에 써줍니다. 

이어서 각 단계별로 실행하는 함수 소스 코드를 분석해볼까요?

1단계: 아이노드 속성 정보를 읽는 과정 분석하기

먼저 vfs_fstat() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/include/linux/fs.h]
static inline int vfs_fstat(int fd, struct kstat *stat)
{
return vfs_statx_fd(fd, stat, STATX_BASIC_STATS, 0);
}

vfs_fstat() 함수는 인라인 함수인데 단지 인자를 추가해서 vfs_statx_fd() 함수를 호출합니다. 3번째 인자로 STATX_BASIC_STATS 플래그를 지정합니다.

다음 vfs_statx_fd() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 int vfs_statx_fd(unsigned int fd, struct kstat *stat,
02 u32 request_mask, unsigned int query_flags)
03 {
04 struct fd f;
05 int error = -EBADF;
06
07 if (query_flags & ~KSTAT_QUERY_FLAGS)
08 return -EINVAL;
09
10 f = fdget_raw(fd);
11 if (f.file) {
12 error = vfs_getattr(&f.file->f_path, stat,
13     request_mask, query_flags);
14 fdput(f);
15 }
16 return error;
17 }
EXPORT_SYMBOL(vfs_statx_fd);

10번째 줄 코드를 보겠습니다.
10 f = fdget_raw(fd);

정수형인 파일스크립터인 fd 인자로 파일 객체를 읽는 동작입니다.

다음 11~13번째 줄 코드를 분석합니다.
11 if (f.file) {
12 error = vfs_getattr(&f.file->f_path, stat,
13     request_mask, query_flags);

11번째 줄 코드에서 파일 객체를 제대로 읽었는지 확인합니다.
파일 객체 주소가 유효한 경우 12번째 줄 코드와 같이 vfs_getattr() 함수를 호출합니다. 

이어서 vfs_getattr() 함수를 분석하겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 int vfs_getattr(const struct path *path, struct kstat *stat,
02 u32 request_mask, unsigned int query_flags)
03 {
04 int retval;
05
06 retval = security_inode_getattr(path);
07 if (retval)
08 return retval;
09 return vfs_getattr_nosec(path, stat, request_mask, query_flags);
10 }

09번째 줄 코드와 같이 vfs_getattr_nosec() 함수를 호출합니다. 

다음 vfs_getattr_nosec() 함수 코드를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 int vfs_getattr_nosec(const struct path *path, struct kstat *stat,
02       u32 request_mask, unsigned int query_flags)
03 {
04 struct inode *inode = d_backing_inode(path->dentry);
05
06 memset(stat, 0, sizeof(*stat));
07 stat->result_mask |= STATX_BASIC_STATS;
08 request_mask &= STATX_ALL;
09 query_flags &= KSTAT_QUERY_FLAGS;
10 if (inode->i_op->getattr)
11 return inode->i_op->getattr(path, stat, request_mask,
12     query_flags);
13
14 generic_fillattr(inode, stat);
15 return 0;
16 }

4번째 줄 코드에서 덴트리 객체에 저장된 아이노드 객체 정보를 로딩합니다.
4 struct inode *inode = d_backing_inode(path->dentry);

이후 6번째 줄과 같이 stat 포인터를 0으로 초기화합니다.

10번째 줄에서는 아이노드 함수 오퍼레이션으로 getattr 필드에 함수가 지정돼 있으면 지정된 함수를 호출합니다.
10 if (inode->i_op->getattr)
11 return inode->i_op->getattr(path, stat, request_mask,
12     query_flags);

이번 소절에서 stat 함수를 호출할 때 지정한 파일 이름은  "/home/pi/sample_text.text" 파일입니다. 이 파일은 저장매체에 저장되므로 ext4 파일시스템에서 관리합니다. 이 기준으로 보면 11번째 줄 코드에서는 ext4_file_getattr() 함수를 호출합니다.

참고로 라즈비안에서 ext4와 proc 파일시스템에서 파일을 관리하는 아이노드 함수 오퍼레이션은 다음과 같습니다.
  (static struct inode_operations) ext4_file_inode_operations = (
    (struct dentry * (*)()) lookup = 0x0 = ,
    (char * (*)()) get_link = 0x0 = ,
    (int (*)()) permission = 0x0 = ,
    (struct posix_acl * (*)()) get_acl = 0x8036B9E4 = ext4_get_acl,
    (int (*)()) readlink = 0x0 = ,
    (int (*)()) create = 0x0 = ,
    (int (*)()) link = 0x0 = ,
    (int (*)()) unlink = 0x0 = ,
    (int (*)()) symlink = 0x0 = ,
    (int (*)()) mkdir = 0x0 = ,
    (int (*)()) rmdir = 0x0 = ,
    (int (*)()) mknod = 0x0 = ,
    (int (*)()) rename = 0x0 = ,
    (int (*)()) setattr = 0x80334BB4 = ext4_setattr,
    (int (*)()) getattr = 0x8032F9C8 = ext4_file_getattr,
    (ssize_t (*)()) listxattr = 0x80368D54 = ext4_listxattr,
    (int (*)()) fiemap = 0x8031A6E0 = ext4_fiemap,
    (int (*)()) update_time = 0x0 = ,
    (int (*)()) atomic_open = 0x0 = ,
    (int (*)()) tmpfile = 0x0 = ,
    (int (*)()) set_acl = 0x8036BC44 = ext4_set_acl)

다음 proc 파일시스템 기준 아이노드 함수 오퍼레이션 선언부입니다.
  (static struct inode_operations) proc_file_inode_operations = (
    (struct dentry * (*)()) lookup = 0x0 = ,
    (char * (*)()) get_link = 0x0 = ,
    (int (*)()) permission = 0x0 = ,
    (struct posix_acl * (*)()) get_acl = 0x0 = ,
    (int (*)()) readlink = 0x0 = ,
    (int (*)()) create = 0x0 = ,
    (int (*)()) link = 0x0 = ,
    (int (*)()) unlink = 0x0 = ,
    (int (*)()) symlink = 0x0 = ,
    (int (*)()) mkdir = 0x0 = ,
    (int (*)()) rmdir = 0x0 = ,
    (int (*)()) mknod = 0x0 = ,
    (int (*)()) rename = 0x0 = ,
    (int (*)()) setattr = 0x802F3CA0 = proc_notify_change,
    (int (*)()) getattr = 0x0 = ,
    (ssize_t (*)()) listxattr = 0x0 = ,
    (int (*)()) fiemap = 0x0 = ,
    (int (*)()) update_time = 0x0 = ,
    (int (*)()) atomic_open = 0x0 = ,
    (int (*)()) tmpfile = 0x0 = ,
    (int (*)()) set_acl = 0x0 = )

ext4 파일시스템 내 아이노드 함수 오퍼레이션인 경우 getattr 필드에 지정된 ext4_file_getattr() 함수 호출로 파일 속성 정보를 추가로 읽습니다. 대신 proc 파일시스템 아이노드 오퍼레이션으로 getattr 필드는 0x0으로 설정돼 있습니다. 이 경우 10~13번째 줄 코드는 실행하지 않습니다.

이후 ext4_file_getattr() 함수에서 ext4_getattr() 함수를 호출해 파일 속성 정보를 읽습니다. 이후 ext4_getattr() 함수에서 generic_fillattr() 함수를 호출합니다.

다음 generic_fillattr() 함수 코드를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
1 void generic_fillattr(struct inode *inode, struct kstat *stat)
2 {
3 stat->dev = inode->i_sb->s_dev;
4 stat->ino = inode->i_ino;
5 stat->mode = inode->i_mode;
6 stat->nlink = inode->i_nlink;
7 stat->uid = inode->i_uid;
8 stat->gid = inode->i_gid;
9 stat->rdev = inode->i_rdev;
10 stat->size = i_size_read(inode);
11 stat->atime = inode->i_atime;
12 stat->mtime = inode->i_mtime;
13 stat->ctime = inode->i_ctime;
14 stat->blksize = i_blocksize(inode);
15 stat->blocks = inode->i_blocks;
16
17 if (IS_NOATIME(inode))
18 stat->result_mask &= ~STATX_ATIME;
19 if (IS_AUTOMOUNT(inode))
20 stat->attributes |= STATX_ATTR_AUTOMOUNT;
21 }
EXPORT_SYMBOL(generic_fillattr);

파일 속성 정보를 채우는 핵심 루틴입니다. 각각 아이노드 필드들을 stat 이란 구조체 필드에 저장합니다. 

2단계: 유저 공간에 파일 정보 복사하는 과정 분석하기

파일 속성 정보를 유저 공간에 전달하는 단계 소스 코드를 분석할 차례입니다.
이번에는 sys_fstat64() 함수 분석으로 되돌아가서 2단계 코드를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/stat.c]
01 SYSCALL_DEFINE2(fstat64, unsigned long, fd, struct stat64 __user *, statbuf)
02 {
03 struct kstat stat;
04 int error = vfs_fstat(fd, &stat);
05
06 if (!error)
07 error = cp_new_stat64(&stat, statbuf);
08
09 return error;
10 }

04번째 줄 코드와 같이 vfs_fstat() 함수를 호출해 파일속성 정보를 읽고 난 다음 07번째 줄 코드와 같이 cp_new_stat64() 함수를 호출합니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 static long cp_new_stat64(struct kstat *stat, struct stat64 __user *statbuf)
02 {
03 struct stat64 tmp;
...
04 tmp.st_atime = stat->atime.tv_sec;
05 tmp.st_atime_nsec = stat->atime.tv_nsec;
06 tmp.st_mtime = stat->mtime.tv_sec;
07 tmp.st_mtime_nsec = stat->mtime.tv_nsec;
08 tmp.st_ctime = stat->ctime.tv_sec;
09 tmp.st_ctime_nsec = stat->ctime.tv_nsec;
10 tmp.st_size = stat->size;
11 tmp.st_blocks = stat->blocks;
12 tmp.st_blksize = stat->blksize;
13 return copy_to_user(statbuf,&tmp,sizeof(tmp)) ? -EFAULT : 0;
14 }

cp_new_stat64() 함수 분석에 앞서 함수 인자를 살펴보겠습니다.
struct kstat *stat 구조체는 가상 파일시스템을 통해 업데이트된 파일 속성 정보가 저장돼 있습니다. struct stat64 구조체는 유저 공간에서 파일 속성을 표현합니다. 

이어서 코드를 분석해볼까요?
04~12번째 줄 코드는 struct kstat 구조체에 저장된 파일 속성 정보를 struct stat 구조체로 복사합니다. 

다음 13번째 줄 코드와 같이 copy_to_user() 함수를 호출해서 &tmp 구조체에 저장된 데이터를 유저 공간 메모리 주소인 statbuf에 복사합니다.

여기까지 유저 공간에서 stat() 함수를 호출하면 커널 가상 파일시스템 계층을 통해 파일 속성 정보를 읽는 과정을 살펴봤습니다. 코드 분석으로 다음 내용을 알게 됐습니다.

    유저 공간에서 파일 속성 정보를 읽는 stat 함수를 호출하면 아이노드 객체에 있는 
    필드를 읽어 저장한다.
  
이어서 다음 절에는 덴트리 객체를 살펴보겠습니다.


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

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


Reference(가상 파일시스템)

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



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