Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

175162
807
85243


[라즈베리파이] 커널동기화 - 임계영역(Critical Section)이란 9장. 커널 동기화 소개

리눅스 커널이나 운영체제에서 임계영역(Critical Section)이나 커널 동기화를 설명할 때 화장실을 예를 많이 듭니다.  

다음 그림을 보면서 임계영역에 대해 살펴봅시다.


하나밖에 없는 화장실 문에 있는 자물쇠를 잠그고 용무를 보는 상황입니다. 위 상황을 리눅스 커널 동기화 과정과 빗대서 생각해 봅시다.
 
첫 번째, 자물쇠는 화장실 문에 있는 잠금장치입니다. 누구나 화장실에 들어갈 때 화장실 문을 잠급니다. 화장실에 들어갈 때 먼저 화장실 문에 있는 자물쇠가 잠겨 있나 확인하는 습관입니다. 

화장실 잠금장치는 여러 가지 종류가 있듯 리눅스 커널에도 스핀락, 뮤텍스 기법에 따라 자물쇠 잠금 방법이 다릅니다.

두 번째, 화장실에서 용무는 딱 한 사람만 볼 수 있습니다. 화장실에서 두 사람이 동시에 들어갈 수 없습니다. 유일하게 한 사람이 일을 보는 순간입니다. 이를 운영체제에서 임계영역(Critical Section)이라고 합니다.

세 번째, 화장실에 다른 사람이 용무를 보고 있으면 밖에서 기다립니다. 화장실에서 기다리는 방식은 사람이 따라 다릅니다. 용무가 급한 사람은 다른 일을 하지 않고 밖에서 발을 동동 구르며 계속 기다립니다. 어떤 이는 화장실 밖에서 잡지를 보면서 유유히 기다립니다.

리눅스 커널에서 스핀락은 화장실 밖에서 다른 일을 하지 않고 계속 기다리는 방식입니다. 이를 Busy-wait라고 합니다. 대신 뮤텍스는 자신을 대기열에 추가하고 화장실 밖에서 잠듭니다.

위 그림에서 화살표로 표시된 순간이 임계영역입니다. 화장실에 단 한 사람이 들어가서 용무를 볼 수 있듯 하나의 프로세스가 실행해야 하는 코드 구간입니다. 임계 영역에 두 사람(프로세스)가 동시에 접근하면 예상치 않은 문제가 발생합니다. 

다음은 레이스(Race)에 대해 알아봅시다. 레이스(Race)는 임계 영역에 두 개 프로세스가 동시에 접근하는 상황을 말하며 이런 조건을 레이스 컨디션(Race Condition)이라 합니다.

다시 화장실을 예로 들겠습니다. A란 사람이 용무를 보기 전에 어떤 일인지 화장실문에 있는 자물쇠를 제대로 잠그지 않았습니다. 이후 B라는 사람은 화장실 문이 잠겨 있지 않으니 화장실에 사람이 없다고 판단 후 문을 엽니다. 결국 화장실에 두 사람이 같이 들어가게 됩니다. 이런 상황을 레이스가 발생했다고 말할 수 있습니다.

여기서 레이스 컨디션은 무엇일까요? A란 사람이 화장실 문에 들어간 다음 문을 제대로 잠그지 않았습니다. 그래서 B는 문이 잠겨 있지 않다고 판단하고 화장실에 들어갑니다. 이 조건을 레이스 컨디션이라고 말할 수 있습니다.

리눅스 커널 실행 도중 위와 같이 레이스가 발생하면 보통 커널 크래시로 시스템 동작을 멈춥니다.

리눅스 커널 패치를 소개하면서 레이스에 대해 조금 더 알아봅시다.
[https://lore.kernel.org/lkml/CAARE==e6obTMLBeo3t2oJuwwtv3zfei7sUhREJwDcqUEGFPdAg@mail.gmail.com/]
CPU0                                      CPU1
1 n_tty_ioctl_helper                      n_tty_ioctl_helper
2 __start_tty                             tty_send_xchar
3 tty_wakeup                              pty_write
4 n_hdlc_tty_wakeup                       tty_insert_flip_string
5 n_hdlc_send_frames                      tty_insert_flip_string_fixed_flag
6 pty_write
7 tty_insert_flip_string
8 tty_insert_flip_string_fixed_flag
 
위 그림은 CPU0와 CPU1에서 실행하는 함수 호출 흐름을 알기 쉽게 표현한 겁니다.
CPU0은 6번째 줄에서 pty_write()에 접근하고 CPU1은 3번째 줄에서도 pty_write() 함수에 동시에 접근합니다.

이후 tty_insert_flip_string()와 tty_insert_flip_string_fixed_flag() 함수에 동시에 실행합니다.

서로 다른 CPU에서 실행 중인 프로세스가 같은 코드나 함수에 접근하는 현상을 동시성(Concurrency)이라고 합니다.

그럼 위와 같은 레이스 컨디션이 발생한 결과 리눅스 시스템은 어떻게 동작했을까요? 커널 패닉으로 시스템 다운이 됐습니다.

커널 패닉이 발생했을 때 커널 로그는 다음과 같습니다.
1 BUG: KASAN: slab-out-of-bounds in
2 tty_insert_flip_string_fixed_flag+0xb5/0x130
3 drivers/tty/tty_buffer.c:316 at addr ffff880114fcc121
...
4 0000000000000000 ffff88011638f888 ffffffff81694cc3 ffff88007d802140
5 ffff880114fcb300 ffff880114fcc300 ffff880114fcb300 ffff88011638f8b0
6 ffffffff8130075c ffff88011638f940 ffff88007d802140 ffff880194fcc121
7 Call Trace:
8 [<ffffffff81694cc3>] __dump_stack lib/dump_stack.c:15 [inline]
9 [<ffffffff81694cc3>] dump_stack+0xb3/0x110 lib/dump_stack.c:51
10 [<ffffffff8130075c>] kasan_object_err+0x1c/0x70 mm/kasan/report.c:156
11 [<ffffffff813009f7>] print_address_description mm/kasan/report.c:194 [inline]
12 [<ffffffff813009f7>] kasan_report_error+0x1f7/0x4e0 mm/kasan/report.c:283
13 [<ffffffff81301076>] kasan_report+0x36/0x40 mm/kasan/report.c:303
14 [<ffffffff812ff9ce>] check_memory_region_inline mm/kasan/kasan.c:292 [inline]
15 [<ffffffff812ff9ce>] check_memory_region+0x13e/0x1a0 mm/kasan/kasan.c:299
16 [<ffffffff812ffea7>] memcpy+0x37/0x50 mm/kasan/kasan.c:335
17 [<ffffffff817f19f5>] tty_insert_flip_string_fixed_flag+0xb5/0x130
 
커널 패닉이 발생한 이유를 잠깐 알아보겠습니다.

16~17번째 줄 로그를 보면 tty_insert_flip_string_fixed_flag() 함수에서 memcpy() 함수를 호출한 흐름을 알 수 있습니다.

이후 10~15번째 줄 로그에서 메모리 영역에 Write/Read 할 때 메모리 상태를 점검하는 check_memory_region() 함수가 수행됐음을 알 수 있습니다. 이때 메모리 오염을 확인하고 리눅스 커널이 커널 패닉을 유발한 겁니다.

커널 패닉의 원인이 되는 코드를 잠깐 봅시다.
[https://elixir.bootlin.com/linux/v4.14.43/source/drivers/tty/tty_buffer.c#L305]
1 int tty_insert_flip_string_fixed_flag(struct tty_port *port,
2 const unsigned char *chars, char flag, size_t size)
3 {
4 int copied = 0;
5 do {
6 int goal = min_t(size_t, size - copied, TTY_BUFFER_PAGE);
7 int flags = (flag == TTY_NORMAL) ? TTYB_NORMAL : 0;
8 int space = __tty_buffer_request_room(port, goal, flags);
9 struct tty_buffer *tb = port->buf.tail;
10 if (unlikely(space == 0))
11 break;
12 memcpy(char_buf_ptr(tb, tb->used), chars, space);
 
위 함수는 tty 드라이버에서 버퍼에 어떤 값을 쓰는 루틴입니다.

커널 패닉을 유발한 코드는 12번째 줄 코드입니다.
12 memcpy(char_buf_ptr(tb, tb->used), chars, space);
 
chars 값을 tty 버퍼에 써주는 흐름에서 오동작한 겁니다. tty 드라이버에 두 개 프로세스가 동시에 접근하니 tb->used에 지정한 배열을 넘어서는 값이 저장돼 있어 out-of-bound로 커널 크래시가 발생한 겁니다. 
out-of-bound란 예상된 크기 이상으로 경계를 넘어 메모리 값을 복사하거나 읽는 과정을 의미합니다. 배열을 10만큼 잡았는데 12로 메모리를 복사하는 경우입니다.

CPU0과 CPU1가 동시에 tty_insert_flip_string_fixed_flag() 함수에 접근한 결과가 커널 패닉입니다. 

레이스 문제에 대해 많이 논의하는 이유는 뭘까요? Race가 발생하면 리눅스 시스템은 시스템이 아주 느려지거나 대부분 커널 패닉으로 시스템 다운이 되기 때문입니다. 리눅스 서버를 관리하는 데브옵스 개발자 입장에서 커널 패닉은 가장 시급히 해결해야 할 이슈입니다. 그래서 Race로 생기는 리눅스 커널 크래시를 해결하기 위해 지금도 수많은 리눅스 커널 개발자들은 머리를 싸매고 디버깅을 합니다.

전 세계 리눅스 커널 고수 개발자들은 리눅스 커널 메일링 리스트로 커널 버그와 패치에 대해 열띤 논의를 합니다. 이 메일링 리스트에서 가장 많이 논의하는 주제 중 하나가 커널 동기화 문제로 생기는 Race입니다.

이 레이스로 커널 패닉이 발생하는 문제를 해결하기 위한 패치 코드를 함께 봅시다.
--- a/drivers/tty/pty.c
+++ b/drivers/tty/pty.c
@@ -110,16 +110,19 @@ static void pty_unthrottle(struct tty_st
1 static int pty_write(struct tty_struct *tty, const unsigned char *buf, int c)
2 {
3        struct tty_struct *to = tty->link;
4 +       unsigned long flags;
5
6        if (tty->stopped)
7                return 0;
8
9        if (c > 0) {
10 +               spin_lock_irqsave(&to->port->lock, flags);
11                /* Stuff the data into the input queue of the other end */
12                c = tty_insert_flip_string(to->port, buf, c);
13                /* And shovel */
14                if (c)
15                        tty_flip_buffer_push(to->port);
16 +               spin_unlock_irqrestore(&to->port->lock, flags);
17        }
18        return c;
19 }

두 개의 CPU가 다음과 같이 12~15번째 줄 코드에 동시에 접근하니 코드 구간에 10번째와 16번째 코드와 같이 스핀락을 걸어준 겁니다.
12                c = tty_insert_flip_string(to->port, buf, c);
13                /* And shovel */
14                if (c)
15                        tty_flip_buffer_push(to->port);
 
위 코드에서 임계 영역은 어딜까요? 12~15번째 줄 코드가 임계영역이니 이 코드 구간에 한 개의 프로세스만 접근해야 합니다.

이번에는 다른 코드를 보면서 임계 영역을 알아보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/arch/arm/mach-tegra/pm.c]
1 void tegra_clear_cpu_in_lp2(void)
2 {
3 int phy_cpu_id = cpu_logical_map(smp_processor_id());
4 u32 *cpu_in_lp2 = tegra_cpu_lp2_mask;
5
6 spin_lock(&tegra_lp2_lock);
7
8 BUG_ON(!(*cpu_in_lp2 & BIT(phy_cpu_id)));
9 *cpu_in_lp2 &= ~BIT(phy_cpu_id);
10
11 spin_unlock(&tegra_lp2_lock);
12}

위 코드는 nVidia Tegra SoC에서 Power Management 관련 함수입니다.

6~11번째 줄 코드는 spin_lock()과 spin_unlock() 함수를 씁니다. 위 코드에서 임계 영역은 어느 코드 구간일까요?

8~9번째 줄 코드가 보호 영역입니다. 이 코드 구간에는 한 개의 프로세스만 접근할 수 있습니다. 만약 A란 프로세스가 8~9번째 줄 코드를 수행 중에 다른 프로세스가 이 코드에 접근할 때는 6번 코드에서 기다립니다.

리눅스 커널에서 임계 영역에 다음과 같이 스핀락이나 뮤텍스 함수를 호출해서 락을 걸고 풉니다.
void kernel_function() 
{
 spin_lock();
 // 임계 영역 코드 시작
 // ...
 // ...
 // 임계 영역 코드 마무리
spin_unlock();

임계 영역이 시작하는 코드 구간을 락 함수로 감쏴 주는 형식입니다.

다음에는 임계 영역을 두 개 프로세스가 동시에 접근할 때 상황인 Race에 대해 짚어 보겠습니다. 


#Reference 시스템 콜


Reference(워크큐)
워크큐(Workqueue) Overview


덧글

댓글 입력 영역