Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


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

워크를 실행하려면 먼저 워크를 초기화해야 합니다. 

워크를 초기화하면 다음 2 가지 방식 중 하나를 적용해야 합니다.
INIT_WORK()
DECLARE_WORK()

INIT_WORK() 매크로는 커널이 INIT_WORK() 함수를 실행할 때 워크를 초기화합니다. 대신 DECLARE_WORK() 매크로는 커널 컴파일이 될 때 ‘워크 세부 정보가 포함된’ 전역 변수를 생성합니다. 

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

첫 번째로 INIT_WORK() 매크로로 워크를 초기화하는 방법입니다. 다음 4번째 줄 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/tty/tty_buffer.c]
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() 매크로 함수로 워크 초기화하는 방법 알아보기
우선 INIT_WORK() 매크로 구현부 코드를 펼쳐서 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/workqueue.h]
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://github.com/raspberrypi/linux/blob/rpi-4.19.y/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이니 struct work_struct->data에는 0xFFFF_FFE0을 저장합니다.  


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


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

8번째 줄 코드는 struct list_head 타입인 struct work_struct 구조체 entry 필드인 연결 리스트를 초기화합니다.

9번째 줄 코드는 struct work_struct 구조체 func 필드에 워크 핸들러 함수 주소를 지정합니다. 워커 스레드가 process_one_work() 함수에서 워크를 실행할 때 이 struct work_struct 구조체 func 필드에 접근해 워크 핸들러 함수를 호출합니다.

이번에는 INIT_WORK() 함수를 써서 워크를 초기화하는 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/tty/tty_buffer.c] 
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번째 줄 코드가 실행하고 나서 각 필드들이 어떤 값으로 바뀌는지 알아볼까요? 
다음은 struct work_struct 구조체 필드 정보입니다.
01  (struct work_struct *) (struct work_struct*)0xb62d3604 = 0xB62D3604
02    (atomic_long_t) data = ((int) counter = 0xFFFFFFE0), // WORK_STRUCT_NO_POOL
03    (struct list_head) entry = ((struct list_head *) 
04    (work_func_t) func = 0x804FDCA8 = flush_to_ldisc)

02번째 줄을 보면 struct work_struct 구조체 data 필드가 0xFFFF_FFE0입니다. 다음 04번째 줄에 있는 func 필드는 flush_to_ldisc() 함수 주소를 저장하고 있습니다.

      
이번에는 WORK_STRUCT_NO_POOL 플래그를 계산하는 과정을 소개합니다.
<1> -> <2> -> <3> 순서로 enum 연산을 해보겠습니다.

<1> 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는 어떻게 계산했는지 알아봅시다.

<2> 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 값을 어떻게 계산했는지 살펴 봅시다.

<3> 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 = 4 + 1 = WORK_OFFQ_FLAG_BASE + WORK_OFFQ_FLAG_BITS,


이번에 WORK_OFFQ_POOL_SHIFT 값을 어떻게 계산했는지 알아볼 차례입니다.

WORK_OFFQ_FLAG_BASE와 WORK_OFFQ_FLAG_BITS를 더하니 결과는 5입니다.


WORK_STRUCT_NO_POOL 플래그를 계산하는 과정에서 읽은 매크로 플래그는 다음 해더 파일에서 확인할 수 있습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/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,


DECLARE_WORK() 매크로 함수로 워크 초기화하는 방법 알아보기
이어서 DECLARE_WORK() 매크로 함수를 써서 워크를 초기화하는 과정을 살펴보겠습니다.

먼저 DECLARE_WORK() 매크로와 이 매크로 내부에서 치환하는 코드를 함께 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/workqueue.h]
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))

5번째 줄 코드를 보겠습니다. WORK_DATA_STATIC_INIT() 를 data 필드에 저장합니다.
WORK_DATA_STATIC_INIT() 매크로 함수의 정체는 무엇일까요?  
WORK_DATA_STATIC_INIT() =  (WORK_STRUCT_NO_POOL | WORK_STRUCT_STATIC)

WORK_STRUCT_NO_POOL 와 WORK_STRUCT_STATIC 플래그를 OR 비트 연산한  WORK_DATA_STATIC_INIT() 인 것입니다. 여기서 WORK_STRUCT_STATIC가 0x0이고 WORK_STRUCT_NO_POOL 가0xFFFF_FFE0니 WORK_DATA_STATIC_INIT()은 0xFFFF_FFE0가 됩니다. 

이번에는 DECLARE_WORK() 매크로를 사용해 워크를 초기화하는 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/tty/vt/vt.c]
static DECLARE_WORK(console_work, console_callback);

위와 같이 매크로를 써서 워크를 선언하면 다음 코드와 같이 워크는 초기화됩니다.
struct work_struct console_work {
.data = 0xFFFF_FFE0
.entry = 0x0
.func = console_callback,

이번 절에서는 워크를 초기화하는 코드 분석으로 다음 내용을 알게 됐습니다.
INIT_WORK() 혹은 DECLARE_WORK() 함수로 워크 초기화
struct work_struct 구조체 data 필드는 WORK_STRUCT_NO_POOL(0xFFFF_FFE0) 플래그를 저장하고 func 필드는 워크 핸들러 함수 저장 

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

    워크를 초기화할 때 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.

위 분석 내용을 간단히 정리하면 다음과 같습니다.   
process_one_work() 함수에서 워크 핸들러인 async_run_entry_fn() 함수를 실행을 시도했다.
struct work_struct 구조체 data 필드가 0xffffffe0이다.
커널 패닉이 발생했다. 

필자가 소개한 바와 같이 다음 워크 실행 단계에서 2단계를 건너뛰고 3 단계가 실행된 것입니다.
1. 워크 초기화
2. 워크를 워크큐에 큐잉
3. 워커 스레드에서 워크를 실행

이렇게 struct work_struct 구조체 data 저장되는 값을 보고 워크 실행 상태을 식별합니다.

이번 소절에서 워크 초기화 처리 과정을 알아봤습니다. 다음 절부터 워크를 워크큐에 큐잉하면 어떤 동작을 하는지 살펴봅니다. 

    핑백

    덧글

    댓글 입력 영역