Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

15192
888
89788


[라즈베리파이]워크큐(Workqueue) - 워크 초기화는 어떻게 하나 8장. 워크큐

워크를 실행하기 위해서 먼저 워크를 초기화해야 합니다. 워크를 초기화하기 위해서 INIT_WORK() 혹은 DECLARE_WORK() 매크로를 써야 합니다. INIT_WORK() 매크로는 함수가 실행할 때 워크를 동적으로 초기화하고 DECLARE_WORK()은 컴파일이 될 때 전역 변수로 워크를 설정합니다.

두 매크로를 쓰면 워크를 어떻게 초기화하는지 드라이버 코드를 확인하겠습니다.

첫 번째로 INIT_WORK() 매크로로 워크를 초기화하는 방법입니다. 다음 4번째 줄 코드를 보겠습니다.
1 void tty_buffer_init(struct tty_port *port)
2 {
3 struct tty_bufhead *buf = &port->buf;
..
4 INIT_WORK(&buf->work, flush_to_ldisc);
5 buf->mem_limit = TTYB_DEFAULT_MEM_LIMIT;
6}

&buf->work 멤버 구조체는 struct work_struct 이고 flush_to_ldisc() 함수는 워크가 실행할 때 호출되는 핸들러입니다.

두 번째로 DECLARE_WORK() 매크로로 워크를 초기화하는 기법을 알아봅시다.
static DECLARE_WORK(console_work, console_callback);

위 코드를 보면 전역 변수 선언하듯 DECLARE_WORK() 매크로 함수를 씁니다.
여기서 console_work는 struct work_struct 구조체 전역 변수이고 console_callback() 함수는 워크 핸들러 함수입니다.

INIT_WORK()와 DECLEAR_WORK()로 워크를 초기화하는 코드를 알아봤으니 각각 코드 구현부를 알아볼 차례입니다. 우선 INIT_WORK() 매크로가 치환하는 코드를 펼쳐 봅시다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L236]
1 #define INIT_WORK(_work, _func) \
2 __INIT_WORK((_work), (_func), 0)
3
4 #define __INIT_WORK(_work, _func, _onstack) \
5 do { \
6 __init_work((_work), _onstack); \
7 (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
8 INIT_LIST_HEAD(&(_work)->entry); \
9 (_work)->func = (_func); \
10 } while (0)
11
12 #define WORK_DATA_INIT() ATOMIC_LONG_INIT(WORK_STRUCT_NO_POOL)

코드 분석을 시작하겠습니다.

6번 줄 코드에서 호출하는 __init_work() 함수는 CONFIG_DEBUG_OBJECTS 커널 컨피그가 켜져 있어야 실행합니다. 라즈비안이나 대부분 리눅스 시스템에선 CONFIG_DEBUG_OBJECTS 커널 컨피그가 꺼져 있습니다.
 
7번 줄 코드 분석으로 넘어가겠습니다.
struct work_struct->data에 WORK_DATA_INIT() 함수 리턴 값을 저장합니다.

WORK_DATA_INIT 매크로를 검색하면 다음 코드와 같이 WORK_STRUCT_NO_POOL 매크로로 치환됨을 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h]
#define WORK_DATA_INIT() ATOMIC_LONG_INIT(WORK_STRUCT_NO_POOL)

WORK_STRUCT_NO_POOL을 알아야 WORK_DATA_INIT를 알 수 있습니다.

WORK_STRUCT_NO_POOL를 다음 순서로 계산하니 0xFFFF_FFE0임을 알 수 있습니다. 
<a> -> <b> -> <c> 순서로 enum 값 연산을 어떻게 하는지 확인합시다.
<a> WORK_STRUCT_NO_POOL 계산
WORK_STRUCT_NO_POOL는 다음과 같이 선언돼 있으니 이를 바탕으로 계산하는 과정을 아래와 같습니다.
WORK_STRUCT_NO_POOL = (unsigned long)WORK_OFFQ_POOL_NONE << WORK_OFFQ_POOL_SHIFT,
0x7FFF_FFFF << 5 = WORK_OFFQ_POOL_NONE << WORK_OFFQ_POOL_SHIFT
0xFFFF_FFE0

WORK_OFFQ_POOL_NONE는 0x7FFF_FFFF이고 WORK_OFFQ_POOL_SHIFT는 5입니다. 따라서 0x7FFF_FFFF를 5만큼 왼쪽으로 비트 시프트한 결과는 0xFFFF_FFE0입니다.

역으로 WORK_OFFQ_POOL_NONE와 WORK_OFFQ_POOL_SHIFT는 어떻게 계산했는지 알아봅시다.

<b> WORK_OFFQ_POOL_NONE 계산
다음 계산 과정으로 WORK_OFFQ_POOL_NONE은 0x7FFF_FFFF입니다.
WORK_OFFQ_POOL_NONE = (1LU << WORK_OFFQ_POOL_BITS) - 1,
(1 << 31) - 1
(0x8000_0000) - 1
0x7FFF_FFFF

WORK_OFFQ_POOL_BITS 값이 31이니 1을 왼쪽으로 31만큼 쉬프트한 결과는 0x8000_0000입니다. 이 값에 1을 빼니 결과는 0x7FFF_FFFF입니다.

여기서 WORK_OFFQ_POOL_BITS 값을 어떻게 계산했는지 살펴 봅시다.

<c> WORK_OFFQ_POOL_BITS 계산
다음 코드와 같이 삼중 연산자로 WORK_OFFQ_LEFT가 27이니 WORK_OFFQ_POOL_BITS는 31입니다.
WORK_OFFQ_POOL_BITS = WORK_OFFQ_LEFT <= 31 ? WORK_OFFQ_LEFT : 31,
31 = 27 <= 31 ? 27: 31
WORK_OFFQ_LEFT = BITS_PER_LONG - WORK_OFFQ_POOL_SHIFT,
27 = 32 - 5

여기서 BITS_PER_LONG는 32이고 WORK_OFFQ_POOL_SHIFT는 5입니다.
이번에 WORK_OFFQ_POOL_SHIFT 값을 어떻게 계산했는지 알아볼 차례입니다.

WORK_OFFQ_FLAG_BASE와 WORK_OFFQ_FLAG_BITS를 더하니 결과는 5입니다.
WORK_OFFQ_POOL_SHIFT = 4 + 1 = WORK_OFFQ_FLAG_BASE + WORK_OFFQ_FLAG_BITS,

workqueue.h이란 해더 파일에서 확인한 매크로 값들은 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h]
enum {
WORK_STRUCT_PENDING_BIT = 0,
...
WORK_STRUCT_COLOR_SHIFT = 4,
WORK_OFFQ_FLAG_BITS = 1,
WORK_OFFQ_FLAG_BASE = WORK_STRUCT_COLOR_SHIFT,
WORK_OFFQ_POOL_SHIFT = WORK_OFFQ_FLAG_BASE + WORK_OFFQ_FLAG_BITS,

다시 7번 줄 코드 분석으로 돌아가면, struct work_struct->data 에 0xFFFF_FFE0 값을 저장한다는 사실을 알 수 있습니다.

다음 8번줄 코드부터 분석하겠습니다.
8 INIT_LIST_HEAD(&(_work)->entry); \
9 (_work)->func = (_func); \

9번 줄 코드는 struct work_struct->func 멤버에 워크 핸들러 함수 주소를 지정합니다. 
워커 쓰레드가 process_one_work() 함수에서 워크를 실행할 때 이 struct work_struct->func 멤버에 접근해서 워크 핸들러 함수를 호출합니다.

INIT_WORK() 함수를 써서 워크를 초기화하는 코드를 다시 보겠습니다.
1 void tty_buffer_init(struct tty_port *port)
2 {
3 struct tty_bufhead *buf = &port->buf;
..
4 INIT_WORK(&buf->work, flush_to_ldisc);

위 함수에서 4번째 줄 코드가 실행하고 나서 각 멤버들이 어떤 값으로 변경되는지 알아봅시다. 다음은 Trace32 프로그램으로 확인한 결과입니다.
  (struct work_struct *) (struct work_struct*)0xb62d3604 = 0xB62D3604
    (atomic_long_t) data = ((int) counter = 0xFFFFFFE0), // WORK_STRUCT_NO_POOL
    (struct list_head) entry = ((struct list_head *) 
    (work_func_t) func = 0x804FDCA8 = flush_to_ldisc)

이번에는 다음 코드와 같이 DECLARE_WORK() 매크로로 워크를 초기화하면 어떤 자료 구조가 변경되는지 확인합시다.
static DECLARE_WORK(console_work, console_callback);

DECLARE_WORK() 매크로와 이 매크로가 치환하는 코드를 펼쳐 보면 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L91]
1 #define DECLARE_WORK(n, f) \
2 struct work_struct n = __WORK_INITIALIZER(n, f)
3
4 #define __WORK_INITIALIZER(n, f) { \
5 .data = WORK_DATA_STATIC_INIT(), \
6 .entry = { &(n).entry, &(n).entry }, \
7 .func = (f), \
8 __WORK_INIT_LOCKDEP_MAP(#n, &(n)) \
9 }
10
11 #define WORK_DATA_STATIC_INIT() \
ATOMIC_LONG_INIT((unsigned long)(WORK_STRUCT_NO_POOL | WORK_STRUCT_STATIC))


이 매크로를 실행하면 결국 다음 코드와 같이 워크를 초기화함을 알 수 있습니다.
struct work_struct console_work {
.data = 0xFFFF_FFE0
.entry = 0x0
.func = console_callback,

5번째 줄 코드에서 WORK_DATA_STATIC_INIT()는 (WORK_STRUCT_NO_POOL | WORK_STRUCT_STATIC) 연산한 결괏값입니다.
WORK_STRUCT_STATIC 값이 0x0이므로 WORK_DATA_STATIC_INIT()은 0xFFFF_FFE0(WORK_STRUCT_NO_POOL)입니다.

이번 절에서는 워크를 초기화하는 코드를 알아봤습니다. INIT_WORK() 혹은 DECLARE_WORK() 함수를 쓰면 되는데 struct work_struct.data 멤버에 0xFFFF_FFE0이라는 WORK_STRUCT_NO_POOL enum 값을 저장합니다.

여기서 한 가지 의문이 생깁니다. 워크를 초기화할 때 struct work_struct.data 멤버에 0xFFFF_FFE0를 왜 저장할까요? 이 값의 의미는 무엇일까요? 

워크큐는 struct work_struct.data 멤버에 저장된 값으로 예외 처리 및 워크의 라이프 사이클을 관리합니다. 0xFFFF_FFE0 값은 워크를 초기화하고 아직 워크를 워크큐를 실행하지 않은 상태라는 의미입니다.

이렇게 워크를 초기화하면 다음 단계로 워크가 실행을 합니다.
1. 워크 초기화
2. 워크를 워크큐에 큐잉
3. 워커 쓰레드에서 워크를 실행

그런데 1단계에서 2단계를 건너 뛰고 3단계로 건너 뛰어 워크를 실행할 때가 있습니다. 이런 경우 다음 메일링 리스트에서 언급된 내용과 같이 커널 크래시를 유발합니다.
[https://lists.gt.net/linux/kernel/2022989]
crash> struct work_struct ed7cf150 
struct work_struct { 
data = { 
counter = 0xffffffe0 
}, 
entry = { 
next = 0xed7cf154, 
prev = 0xed7cf154 
}, 
func = 0xc0140ac4 <async_run_entry_fn> 

The value of data is 0xffffffe0, which is basically the value after an 
INIT_WORK() or WORK_DATA_INIT(). 
This can happen if a driver calls INIT_WORK on same struct work again 
after queuing it.


struct work_struct.data 값 0xffffffe0인데 process_one_work() 함수가 호출되서 워크 핸들러가 async_run_entry_fn() 함수인 워크를 실행하다 발생한 커널 크래시인 겁니다.

이렇게 struct work_struct.data 저장되는 값은 워크 실행 상태와 워크가 오동작할 때 커널 크래시를 유발하는 효과가 있습니다.

워밍업을 하는 단계로 워크와 워크 초기화에 대해서 알아봤습니다. 다음에 워크를 워크큐에 큐잉하면 어떤 동작을 하는지 살펴봅니다. 우선 워크를 워크큐에 큐잉하면 어떤 동작을 하는지 알아봅시다.

#Reference 워크큐
워크큐 소개
워크큐 종류 알아보기
워크란  
워크를 워크큐에 어떻게 큐잉할까?
   워크를 큐잉할 때 호출하는 워크큐 커널 함수 분석   
워커 쓰레드란
워크큐 실습 및 디버깅
   ftrace로 워크큐 동작 확인   
   인터럽트 후반부로 워크큐 추가 실습 및 로그 분석 
   Trace32로 워크큐 자료 구조 디버깅하기 
딜레이 워크 소개  
   딜레이 워크는 누가 언제 호출할까?
라즈베리파이 딜레이 워크 실습 및 로그 확인  





    핑백

    덧글

    댓글 입력 영역