Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

16192
888
89789


[리눅스커널] 시스템 콜: ARMv8 아키텍처에서 본 시스템 콜 테이블

ARMv8 아키텍처에서 본 시스템 콜 테이블은 다음과 같습니다.     
________________address||value_____________|symbol
   NSD:FFFFFF9D4D200798| 0xFFFFFF9D4BF3F240 \\vmlinux\aio\__arm64_sys_io_setup
   NSD:FFFFFF9D4D2007A0| 0xFFFFFF9D4BF3F680 \\vmlinux\aio\__arm64_sys_io_destroy
   NSD:FFFFFF9D4D2007A8| 0xFFFFFF9D4BF3F818 \\vmlinux\aio\__arm64_sys_io_submit
   NSD:FFFFFF9D4D2007B0| 0xFFFFFF9D4BF3FCB0 \\vmlinux\aio\__arm64_sys_io_cancel
   NSD:FFFFFF9D4D2007B8| 0xFFFFFF9D4BF3FE98 \\vmlinux\aio\__arm64_sys_io_getevents
   NSD:FFFFFF9D4D2007C0| 0xFFFFFF9D4BF0F7D0 \\vmlinux\fs/xattr\__arm64_sys_setxattr
   NSD:FFFFFF9D4D2007C8| 0xFFFFFF9D4BF0F808 \\vmlinux\fs/xattr\__arm64_sys_lsetxattr
   NSD:FFFFFF9D4D2007D0| 0xFFFFFF9D4BF0F840 \\vmlinux\fs/xattr\__arm64_sys_fsetxattr
   NSD:FFFFFF9D4D2007D8| 0xFFFFFF9D4BF0F910 \\vmlinux\fs/xattr\__arm64_sys_getxattr
   NSD:FFFFFF9D4D2007E0| 0xFFFFFF9D4BF0F9F8 \\vmlinux\fs/xattr\__arm64_sys_lgetxattr
   NSD:FFFFFF9D4D2007E8| 0xFFFFFF9D4BF0FAE0 \\vmlinux\fs/xattr\__arm64_sys_fgetxattr
   NSD:FFFFFF9D4D2007F0| 0xFFFFFF9D4BF0FB80 \\vmlinux\fs/xattr\__arm64_sys_listxattr
   NSD:FFFFFF9D4D2007F8| 0xFFFFFF9D4BF0FC58 \\vmlinux\fs/xattr\__arm64_sys_llistxattr
   NSD:FFFFFF9D4D200800| 0xFFFFFF9D4BF0FD30 \\vmlinux\fs/xattr\__arm64_sys_flistxattr
   NSD:FFFFFF9D4D200808| 0xFFFFFF9D4BF0FDC0 \\vmlinux\fs/xattr\__arm64_sys_removexattr
   NSD:FFFFFF9D4D200810| 0xFFFFFF9D4BF0FDF0 \\vmlinux\fs/xattr\__arm64_sys_lremovexattr
   NSD:FFFFFF9D4D200818| 0xFFFFFF9D4BF0FE20 \\vmlinux\fs/xattr\__arm64_sys_fremovexattr
   NSD:FFFFFF9D4D200820| 0xFFFFFF9D4BF21AD0 \\vmlinux\d_path\__arm64_sys_getcwd
   NSD:FFFFFF9D4D200828| 0xFFFFFF9D4BF898E0 \\vmlinux\dcookies\__arm64_sys_lookup_dcookie
   NSD:FFFFFF9D4D200830| 0xFFFFFF9D4BF3E850 \\vmlinux\eventfd\__arm64_sys_eventfd2
   NSD:FFFFFF9D4D200838| 0xFFFFFF9D4BF3A1F8 \\vmlinux\eventpoll\__arm64_sys_epoll_create1
   NSD:FFFFFF9D4D200840| 0xFFFFFF9D4BF3A268 \\vmlinux\eventpoll\__arm64_sys_epoll_ctl
   NSD:FFFFFF9D4D200848| 0xFFFFFF9D4BF3AF58 \\vmlinux\eventpoll\__arm64_sys_epoll_pwait
   NSD:FFFFFF9D4D200850| 0xFFFFFF9D4BF055A8 \\vmlinux\fs/file\__arm64_sys_dup
   NSD:FFFFFF9D4D200858| 0xFFFFFF9D4BF053C8 \\vmlinux\fs/file\__arm64_sys_dup3
   NSD:FFFFFF9D4D200860| 0xFFFFFF9D4BEF3FF8 \\vmlinux\fcntl\__arm64_sys_fcntl
   NSD:FFFFFF9D4D200868| 0xFFFFFF9D4BF39380 \\vmlinux\inotify_user\__arm64_sys_inotify_init1
   NSD:FFFFFF9D4D200870| 0xFFFFFF9D4BF39510 \\vmlinux\inotify_user\__arm64_sys_inotify_add_watch
   NSD:FFFFFF9D4D200878| 0xFFFFFF9D4BF39888 \\vmlinux\inotify_user\__arm64_sys_inotify_rm_watch
   NSD:FFFFFF9D4D200880| 0xFFFFFF9D4BEF67C8 \\vmlinux\fs/ioctl\__arm64_sys_ioctl
   NSD:FFFFFF9D4D200888| 0xFFFFFF9D4C1318C0 \\vmlinux\ioprio\__arm64_sys_ioprio_set
   NSD:FFFFFF9D4D200890| 0xFFFFFF9D4C131B68 \\vmlinux\ioprio\__arm64_sys_ioprio_get
   NSD:FFFFFF9D4D200898| 0xFFFFFF9D4BF4D3F0 \\vmlinux\locks\__arm64_sys_flock
   NSD:FFFFFF9D4D2008A0| 0xFFFFFF9D4BEEE7E8 \\vmlinux\fs/namei\__arm64_sys_mknodat
   NSD:FFFFFF9D4D2008A8| 0xFFFFFF9D4BEEEB68 \\vmlinux\fs/namei\__arm64_sys_mkdirat
   NSD:FFFFFF9D4D2008B0| 0xFFFFFF9D4BEEF580 \\vmlinux\fs/namei\__arm64_sys_unlinkat
   NSD:FFFFFF9D4D2008B8| 0xFFFFFF9D4BEEF960 \\vmlinux\fs/namei\__arm64_sys_symlinkat
   NSD:FFFFFF9D4D2008C0| 0xFFFFFF9D4BEF0000 \\vmlinux\fs/namei\__arm64_sys_linkat
   NSD:FFFFFF9D4D2008C8| 0xFFFFFF9D4BEF07D8 \\vmlinux\fs/namei\__arm64_sys_renameat
   NSD:FFFFFF9D4D2008D0| 0xFFFFFF9D4BF088E8 \\vmlinux\fs/namespace\__arm64_sys_umount
   NSD:FFFFFF9D4D2008D8| 0xFFFFFF9D4BF0B048 \\vmlinux\fs/namespace\__arm64_sys_mount
   NSD:FFFFFF9D4D2008E0| 0xFFFFFF9D4BF0B190 \\vmlinux\fs/namespace\__arm64_sys_pivot_root
   
ARMv7 아키텍처 기반 시스템 콜 테이블과는 다릅니다.

[리눅스커널] 메모리관리: 가상 주소를 물리 주소 변환하는 세부 원리 알아보기 14장. 메모리 관리

가상 주소를 물리 주소 변환하는 세부 원리 알아보기
이번에는 가상 주소를 물리 주소로 변환하는 과정을 살펴보겠습니다. 다음 그림을 같이 보겠습니다.
 
[그림 14.18] 가상 주소를 물리 주소로 변환하는 과정 전체 흐름도

가상 주소를 물리 주소로 변환하는 과정은 크게 2 단계로 나눌 수 있습니다.
 
[그림 14.19] 가상 주소를 물리 주소로 변환하는 단계

1단계
[그림 14.18] 가장 위쪽에 있는 주소는 TTBR(Translation Table Base Register)에 저장된 변환 테이블 시작 주소입니다. 페이지 테이블의 베이스 주소입니다. 변환 베이스 주소와 SBZ로 구분할 수 있는데 SBZ는 항상 0라는 의미입니다. 커널에서는 swapper_pg_dir 전역 변수가 이 주소를 저장합니다.

다음 ‘가상 주소’ 열에 보이는 주소는 그대로 가상 주소를 의미합니다. 가상 주소 [31:20] 비트는 페이지 테이블 인덱스는 저장합니다.

다음 ‘1st Level PTE 주소 열’은 레벨1 페이지 테이블 엔트리 주소를 계산하는 방식을 보여줍니다.
 
가상 주소 [31:20] 비트를 왼쪽으로 2비트 만큼 비트 쉬프트 해서
[13:2] 비트에 저장합니다. 이해하기 어려운 내용인데 쉽게 공식으로 풀면 다음과 같습니다.
(가상 주소 >> 20) << 2  
(가상 주소 >> 20) * 4 

어떤 값을 2비트만큼 왼쪽으로 쉬프트를 하는 연산은 2^2인 4를 곱하는 연산 결과와 같습니다.

만약 페이지 테이블 베이스 시작 주소가 0x8000_4000이고 가상 주소가 0x807A_0A83이면 가상 주소에 해당하는 페이지 테이블 엔트리는 다음과 같이 계산할 수 있습니다.
0x8000_4000 + ((0x807A_0A83 >> 20) << 2)
0x8000_4000 + ((0x807) << 2)
0x8000_4000 + (0x201C)
0x8000601C 

정리하면 0x8000601C 주소에 0x807A_0A83 가상 주소에 대한 페이지 테이블 엔트리가 있습니다.

2단계
다음은 페이지 테이블 엔트리 레코드를 분석하는 단계입니다. 하위 비트 [1:0]이 10 이니 섹션 엔트리임을 알 수 있습니다. 바로 물리 주소로 변환이 가능한 페이지 엔트리입니다.

이어서 페이지 테이블 엔트리 레코드를 분석하겠습니다. 

    마지막 하위 2개 [1:0] 비트를 확인합니다. 

[1:0] 비트가 10이면 섹션 엔트리이며 페이지 테이블 엔트리의 [31:20] 비트에 있는 값이 변환할 수 있는 물리 주소 정보입니다. 

이 주소를 [31:20]에 저장하고 기존 섹션 인덱스를 더하면 물리 주소가 됩니다.

이번에는 다른 그림으로 가상 주소를 물리 주소로 변환하는 과정을 살펴보겠습니다.
 
[그림 14.20] 가상 주소를 물리 주소로 변환하는 과정

가상 주소 베이스 부분으로 시작된 화살표는 해당 주소에 대한 페이지 테이블 엔트리 주소를 가르킵니다. ①으로 된 부분이 이전 그림에서 본 1단계입니다.

   먼저 페이지 테이블 엔트리 주소를 찾는 과정입니다.

다음 2 단계로 페이지 테이블 엔트리 타입을 식별합니다. 하위 [1:0] 비트가 10이니 섹션 엔트리입니다. 섹션 엔트리이니 바로 물리 주소로 변환이 가능합니다.

위 그림에서 페이지 테이블 베이스 주소 기준으로 4 인덱스에 회색으로 된 부분이 있습니다.
여기서 물리 주소 베이스 주소가 있는 것입니다. 

   이를 물리 베이스 주소로 하고 가상 주소의 오프셋 주소를 더해 물리 주소로 변환합니다.

이번 소절에서는 가상 주소를 물리 주소로 변환하는 원리를 알아봤습니다. 다음 소절에서는 이에서 실제 가상 주소를 물리 주소로 변환해 보겠습니다.


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



[리눅스커널] 메모리관리: 페이지 테이블에 대해 알아보기 14장. 메모리 관리

페이지 테이블로 가상 주소를 물리 주소로 변환하는 흐름을 살펴봤습니다. 이 과정에서 알아야 하는 주요 개념을 소개합니다.
 - 페이지 테이블 엔트리
 - 페이지 테이블 엔트리 주소

페이지 테이블 엔트리
페이지 테이블 엔트리(Page Table Entry, 줄여서 PTE)는 페이지 테이블의 정보이며 레코드라고도 부릅니다.

페이지 테이블 엔트리 주소
말 그대로 페이지 테이블 엔트리가 있는 주소를 의미합니다.

이해를 돕기 위해 다음 주소 테이블을 보겠습니다.
       주소       | 값       
1 NSD:80004000 | 0x0
2 NSD:80004004 | 0x0
3 NSD:80004008 | 0x0
4 NSD:8000400C | 0x0
...
5 NSD:80006018 | 0x0061941E
6 NSD:8000601C | 0x0071941E
7 NSD:80006020 | 0x0081941E

위 정보는 페이지 테이블 엔트리 주소와 페이지 테이블 엔트리입니다. 

1번째 줄 가장 왼쪽에 있는 0x80004000가 페이지 테이블 베이스 주소입니다. 5 번째 줄을 보면 0x80006018 주소에 0x0061941E가 있습니다.

0x0061941E 는 페이지 테이블 엔트리이며 80006018는 페이지 테이블 엔트리 주소입니다. 커널에서는 swapper_pg_dir 전역 변수가 페이지 테이블 시작 주소를 저장합니다.

페이지 테이블 종류 알아보기
이번에는 페이지 테이블 종류를 알아봅시다. 페이지 테이블은 레벨1 페이지 테이블과 레벨2 페이지 테이블로 분류할 수 있습니다.

레벨1 페이지 테이블은 마스터 페이지 테이블이라고 부르며 크게 3가지 유형의 엔트리가 있습니다. 다음 그림을 보면서 레벨1 페이지 테이블 엔트리 타입에 대해 살펴봅시다.
 
[그림 14.15] 레벨1 페이지 테이블 엔트리 타입

가장 위쪽은 Fault 타입으로 하위 비트 [1:0]가 00입니다. 유효하지 않은 페이지 테이블 엔트리이며 이 경우 페이지 폴트로 익셉션이 발생합니다.

그림 가장 아랫 부분에 보이는 것이 ‘섹션 엔트리’ 페이지 테이블입니다. 하위 비트 [1:0]가 10입니다. 섹션 엔트리의 [31:20] 비트에는 물리 주소 변환 주소가 있습니다. 이 페이지 테이블 엔트리 정보로 바로 물리 주소를 변환할 수 있습니다.

레벨2 페이지 테이블로 접근해 물리 주소를 변환하는 과정은 조금 복잡합니다. 이어서 다음 그림을 보면서 레벨2 페이지 테이블 타입에 대해 살펴봅시다.
 
[그림 14.16] 레벨2 페이지 테이블 엔트리 타입

위 그림 가장 윗 부분은 라지 페이지 테이블로 하위 비트 [1:0]가 01입니다. 메모리를 64KB 사이즈 블록으로 나눠 관리합니다. PA[31:16] 비트에 물리 주소로 매핑되는 변환 주소가 있습니다. 

다음으로 그림 아래 부분이 스몰 페이지 테이블이며 하위 비트 [1:0]가 1x입니다.
메모리를 4KB 사이즈 블록으로 나눠 관리합니다. PA[31:12] 비트에 물리 주소로 매핑되는 변환 주소가 있습니다.  

가운데는 라지 페이지 테이블로 하위 비트 [1:0]가 01입니다. 메모리를 64KB 사이즈 블록으로 나눠 관리합니다. PA[31:16] 비트는 물리 주소로 매핑되는 변환 주소가 있습니다.

각 페이지 테이블 엔트리에 따른 페이지 테이블 종류를 다음 테이블을 보면서 정리합시다.
 
[그림 14.17] 페이지 테이블 엔트리에 따른 페이지 테이블 종류

위 테이블에서 PA로 명시된 비트는 베이스 물리 주소를 의미합니다.

베이스 물리 주소로 명시한 이유는 변환된 물리 주소는 다음 형식이기 때문입니다.
 베이스 물리 주소 | 가상 주소 오프셋 주소 

예를 들어 스몰 페이지 엔트리의 경우 PA[31:12] 비트까지가 물리 주소이고 [11:0] 비트는
가상 주소 오프셋 주소 비트를 의미하기 때문입니다.

이 내용은 레벨 페이지 테이블 변환 과정을 통해 더 자세히 살펴보겠습니다.

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




[리눅스커널] 시스템 콜: strace와 ftrace로 시스템 콜 디버깅하기 11장. 시스템 콜

이전 시간에 strace로 유저 공간에서 시스템 세부 동작을 확인했습니다.이번에는 strace와 ftrace를 함께 보면서 시스템 콜 동작을 추적해보겠습니다.

먼저 설정 방법을 단계별로 알아봅시다.
ftrace 설정 
strace 실행 

위와 같이 설정을 한 다음 ftrace 로그 받으면 됩니다.

ftrace 이벤트를 설정하고 strace를 실행하기
시스템 콜 ftrace 이벤트를 설정하는 전체 명령어는 다음과 같습니다.
#!/bin/bash

echo  > /sys/kernel/debug/tracing/set_event
sleep 1

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

echo nop > /sys/kernel/debug/tracing/current_tracer
sleep 1"

echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/enable
echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/enable
sleep 1

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

syscall_ftrace.sh 란 셸 스크립트 파일로 저장해서 실행합니다.

다음으로 이전 소절에서 소개한 바와 같이 strace를 실행합니다. 
strace –fF ./simple_exit

ftrace와 strace 메시지를 함께 분석해보기
이번에는 strace 프로그램을 실행했을때 동작한 ftrace 로그를 함께 분석하겠습니다.

1 simple_exit-896 [003] 330.985283: sys_enter: NR 64 (0, 7ec6b5f8, 10, 1, 37e, 0)
2 simple_exit-896 [003] 330.985286: sys_exit: NR 64 = 894
3 simple_exit-896 [003] 330.985434: sys_enter: NR 20 (37e, 7ec6b5f8, 10, 1, 37e, 0)
4 simple_exit-896 [003] 330.985436: sys_exit: NR 20 = 896
5 simple_exit-896 [003] 330.985631: sys_enter: NR 4 (1, 32c008, 23, 0, 23, 32c008)
6 simple_exit-896 [003] 330.985644: sys_exit: NR 4 = 35
7 simple_exit-896 [003] 330.985816: sys_enter: NR 162 (7ec6b5f8, 7ec6b5f8, 10, 2, 0, 7ec6b5f8)
8 simple_exit-896  [003] 332.9859 60: sys_exit: NR 162 = 0
9 simple_exit-896 [003] 332.986414: sys_enter: NR 248 (0, 0, 0, ffffffff, 1, 0)
10 simple_exit-896 [003] 332.986418: do_exit+0x14/0xb60 <-do_group_exit+0x50/0xe4
11 simple_exit-896 [003] 332.986446: <stack trace>
12 simple_exit-896 [003] 332.986848: sched_process_exit: comm=simple_exit pid=896 prio=120

1번째 줄 로그를 보겠습니다.
1 simple_exit-896 [003] 330.985283: sys_enter: NR 64 (0, 7ec6b5f8, 10, 1, 37e, 0)
2 simple_exit-896 [003] 330.985286: sys_exit: NR 64 = 894

64번 시스템 콜이 실행됐음을 알 수 있습니다. 다음 경로에 있는 unistd.h 해더 파일로 64번 시스템 콜 핸들러 매크로는 __NR_getppid 임을 알 수 있습니다. 
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]
#define __NR_getppid (__NR_SYSCALL_BASE+ 64)

위 ftrace 로그는 다음 strace 로그에 대응합니다.
[pid   896] getppid()                   = 894

위 정보를 토대로 다음과 같은 사실을 알 수 있습니다.

   "getppid() 시스템 콜 함수를 호출해서 894란 값을 반환했다."

같은 방식으로 ftrace 로그와 strace 로그를 시스템 콜 종류 별로 함께 보겠습니다.

다음은 부모 PID를 읽는 시스템 콜 동작입니다.
[ftrace 로그]
3 simple_exit-896 [003] 330.985434: sys_enter: NR 20 (37e, 7ec6b5f8, 10, 1, 37e, 0)
4 simple_exit-896 [003] 330.985436: sys_exit: NR 20 = 896
...
[strace 로그]
 [pid   896] getpid()                    = 896

[시스템 콜 번호]
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]
#define __NR_getpid (__NR_SYSCALL_BASE+ 20)

다음은 write 시스템 콜 동작입니다. ftrace 와 strace 로그 같이 파일 스트링을 쓴 길이 35를 반환합니다.
[ftrace]
5 simple_exit-896 [003] 330.985631: sys_enter: NR 4 (1, 32c008, 23, 0, 23, 32c008)
6 simple_exit-896 [003] 330.985644: sys_exit: NR 4 = 35

[strace]
57 [pid   896] write(1, "raspbian tracing ppid:894 pid:89"..., 35raspbian tracing ppid:894 pid:896 
58 ) = 35

[시스템 콜 번호]
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]
#define __NR_write (__NR_SYSCALL_BASE+  4)

다음은 슬립에 진입하는 nanosleep 시스템 콜 동작입니다.
[ftrace]
7 simple_exit-896 [003] 330.985816: sys_enter: NR 162 (7ec6b5f8, 7ec6b5f8, 10, 2, 0, 7ec6b5f8)
8 simple_exit-896  [003] 332.985960: sys_exit: NR 162 = 0

[strace]
[pid   896] nanosleep({tv_sec=2, tv_nsec=0},  <unfinished ...>

[시스템 콜 번호]
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]
#define __NR_nanosleep (__NR_SYSCALL_BASE+162)

이번 소절에서 소개한 방식을 활용하면 유저 공간과 커널 공간에서 시스템 콜 동작을 하나의 그림으로 분석할 수 있습니다. 특히 다음과 같은 상황에서 유용하게 활용할 수 있습니다.
유저 공간에서 시스템 콜이 제대로 발생했는지 확인하고 싶을 때
유저 공간에서 시스템 콜을 발생했는데 커널 공간에서 시스템 콜 핸들러가 실행 여부를 확인하고 싶을 때 
시스템 콜 발생과 시스템 콜 핸들러 실행 시 오류 메시지를 확인하고 싶을 때

이렇게 리눅스에서 제공하는 ftrace와 strace를 활용하면 더 많은 것을 얻을 수 있습니다.

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





[리눅스커널] 메모리관리: 가상 주소를 물리 주소로 변환하는 단계 알아보기 14장. 메모리 관리

가상 주소를 물리 주소로 변환하는 단계 알아보기 
가상 주소를 물리 주소로 변환하는 과정은 다음 단계로 나눌 수 있습니다.

1 단계: 페이지 테이블 엔트리 주소를 검색
먼저 페이지 테이블이 있는 주소를 검색합니다. 그래야 페이지 테이블에 담긴 정보를 참고해서 주소를 변환합니다.

2 단계: 레벨 페이지 테이블 엔트리 분석  
 페이지 테이블 엔트리에 있는 값을 해석합니다. 페이지 테이블 엔트리 [1:0] 비트 패턴에 따라 페이지 테이블 유형이 나뉩니다.

 [1:0]이 10이면 
   섹션 엔트리 페이지 테이블이며 물리 주소 변환 정보를 포함하고 있습니다.
   바로 가상 주소를 물리 주소로 변환할 수 있습니다.
 [1:0]이 01이면 
   페이지 테이블 엔트리엔 라지 페이지와 스몰 페이지 테이블 주소가 저장돼 있습니다. 

3 단계: 레벨2 페이지 테이블 엔트리 분석
 라지와 스몰 페이지 테이블에 따라 물리 주소 변환 방식이 다릅니다. 물리 주소 정보가 포함된 비트를 참고해 가상 주소를 물리 주소로 변환합니다.


[리눅스커널] 프로세스: 유저 프로그램 실행 흐름 추적하기 4장. 프로세스 관리

리눅스 시스템에서는 빌트인으로 제공하는 프로그램이 있습니다. 이를 명령어 형태로 쓰고 있는데 이 프로그램은 실행할 때 프로세스 포멧으로 구동합니다.

ftrace 로그로 각 프로그램(프로세스)이 어떻게 생성, 실행 및 종료하는지 확인합시다.

ftrace 로그 설정하기
ftrace 로그 설정 코드는 다음과 같습니다.
1 "echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exec/enable"
2 "echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable"
3 "echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable"
4 "echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_free/enable"
5
6 "echo  _do_fork sys_clone do_exit search_binary_handler copy_process* > /sys/kernel/debug/tracing/set_ftrace_filter"

ftrace 이벤트가 sched_xxxx 으로 시작하면 보통 프로세스 스케줄링 동작을 출력할 것이라 예상합니다. 그런데 프로세스 생성, 실행, 종료 그리고 프로세스 정보 해제 동작을 추적할 수 있습니다.

각각 이벤트가 어떤 커널 동작을 추적하는지 살펴보겠습니다.
sched_process_fork: 프로세스 생성
sched_process_exec: 프로세스 실행
sched_process_exit: 프로세스 종료
sched_process_free: 프로세스 정보(메모리, 태스크 디스크립터) 해제

지정한 함수 콜스택을 확인하려면 다음과 같이 set_ftrace_filter 노드를 지정해야 합니다.
6 "echo  _do_fork sys_clone do_exit copy_process* > /sys/kernel/debug/tracing/set_ftrace_filter"

ftrace 로그를 설정하고, 셸 스크립트에서 다음 명령어를 입력합시다.
root@raspberrypi:/home/pi# cat /proc/interrupts 
root@raspberrypi:/home/pi# whoami 
root@raspberrypi:/home/pi# top

위 명령어 입력이 끝난 후 ftrace 로그를 받습니다.

ftrace 로그 분석하기
분석할 로그는 다음과 같습니다.
1  sh-10266 [003] ...1 29072.634528: copy_process.isra.65.part.66+0x40/0x15e4 <-_do_fork+0xe0/0x42c
2  sh-10266 [003] ...1 29072.634529: <stack trace>
3  => SyS_clone+0x44/0x50
4  => el0_svc_naked+0x24/0x28
5  sh-10266 [003] .... 29072.635890: sched_process_fork: comm=sh pid=10266 child_comm=sh child_pid=742
6  whoami-742   [003] ...1 29072.638211: search_binary_handler+0x20/0x200 <-do_execveat_common.isra.44+0x4d4/0x678
7  whoami-742   [003] ...1 29072.638215: <stack trace>
8 => SyS_execve+0x48/0x58
9 => el0_svc_naked+0x24/0x28
10 whoami-742   [003] .... 29072.639832: sched_process_exec: filename=/system/bin/whoami pid=742 old_pid=742
11 whoami-742   [006] ...1 29072.667553: do_exit+0x28/0xa70 <-do_group_exit+0x40/0xa0
12 whoami-742   [006] ...1 29072.667563: <stack trace>
13 => __wake_up_parent+0x0/0x3c
14 => el0_svc_naked+0x24/0x28
15 rcuop/1-23    [003] .... 29072.710872: sched_process_free: comm=whoami pid=742 prio=120
16 whoami-742   [006] .... 29072.668650: sched_process_exit: comm=whoami pid=742 prio=120

sh(pid: 10266) 프로세스가 유저 공간에서 fork() 함수를 호출해서 자식 프로세스를 생성하고 있습니다. 실행한 순서대로 보기 쉽게 정렬한 콜스택은 다음과 같습니다.
copy_process+0x40
_do_fork+0xe0/0x42c
sys_clone+0x44/0x50
el0_svc_naked+0x24/0x28

sh(pid: 10266) 프로세스가 whoami란 프로세스를 생성했으니 whoami 프로세스는 실행을 시작합니다. 이 동작은 6~9번째 로그에서 볼 수 있습니다.
6  whoami-742   [003] ...1 29072.638211: search_binary_handler+0x20/0x200 <-do_execveat_common.isra.44+0x4d4/0x678
7  whoami-742   [003] ...1 29072.638215: <stack trace>
8 => sys_execve+0x48/0x58
9 => el0_svc_naked+0x24/0x28
10 whoami-742   [003] .... 29072.639832: sched_process_exec: filename=/system/bin/whoami pid=742 old_pid=742

이어서 whoami 프로세스가 실행할 때 ftrace 로그를 보겠습니다.
10 whoami-742   [003] .... 29072.639832: sched_process_exec: filename=/system/bin/whoami pid=742 old_pid=742

이 로그는 어느 함수에서 출력하는 것일까요? SyS_execve() 이란 시스템 콜 핸들러 함수가 호출된 다음 함수 흐름으로 exec_binprm() 함수가 실행됩니다.
do_execveat_common() 
exec_binprm()

다음으로 exec_binprm() 함수를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/fs/exec.c]
1 static int exec_binprm(struct linux_binprm *bprm)
2 {
3 pid_t old_pid, old_vpid;
4 int ret;
5
6 /* Need to fetch pid before load_binary changes it */
7 old_pid = current->pid;
8 rcu_read_lock();
9 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
10 rcu_read_unlock();
11
12 ret = search_binary_handler(bprm);
13 if (ret >= 0) {
14 audit_bprm(bprm);
15 trace_sched_process_exec(current, old_pid, bprm);

위 15번째 줄 코드에서 whoami 프로세스가 출력하는 동작을 출력하는 것입니다.

파일 이름은 /system/bin/whoami 이고 pid는 742입니다.
/system/bin/whoami 리눅스 시스템에 있는 파일 형태로 있는 것이고 이 파일 자제가 프로세스는 아닙니다. 이 파일을 메모리에 적재해서 실행하는 것이 프로세스이며 이 프로세스가 실행되는 순간이 10번째 로그인 것입니다.

다음 11~14번째 줄은 whoami 프로세스가 종료하는 로그입니다. whoami 명령어를 입력하고 root 란 결과를 출력했으니 프로세스를 바로 종료하는 것입니다.
11 whoami-742   [006] ...1 29072.667553: do_exit+0x28/0xa70 <-do_group_exit+0x40/0xa0
12 whoami-742   [006] ...1 29072.667563: <stack trace>
13 => __wake_up_parent+0x0/0x3c
14 => el0_svc_naked+0x24/0x28

유저 공간에서 exit() 함수를 호출하면 실행하는 코드 흐름입니다.

위 콜스택에서 __wake_up_parent+0x0 이란 정보를 볼 수 있습니다. 정확한 디버깅 정보는 sys_exit_group+0xc 입니다.
11 whoami-742   [006] ...1 29072.667553: do_exit+0x28/0xa70 <-do_group_exit+0x40/0xa0
12 whoami-742   [006] ...1 29072.667563: <stack trace>
13 => sys_exit_group+0xc/0x3c
14 => el0_svc_naked+0x24/0x28

ARM에서 파이프라인을 적용해서 실제 실행된 코드 주소에서 +0x4만큼 주소를 프로그램 카운터로 저장합니다.


어셈블리 코드를 보면 실제로 sys_exit_group() 함수 가장 마지막 코드에서 do_group_exit() 함수를 호출합니다.
NSR:80121F0C|sys_exit_group:  cpy     r12,r13
NSR:80121F10|                 push    {r11-r12,r14,pc}
NSR:80121F14|                 sub     r11,r12,#0x4     ; r11,r12,#4
NSR:80121F18|                 str     r14,[r13,#-0x4]!
NSR:80121F1C|                 bl      0x8010E920       ; __gnu_mcount_nc
NSR:80121F20|                 lsl     r0,r0,#0x8       ; error_code,error_code,#8
NSR:80121F24|                 and     r0,r0,#0xFF00    ; r0,r0,#65280
NSR:80121F28|                 bl      0x80121E28       ; do_group_exit
NSR:80121F2C|__wake_up_parent:cpy     r12,r13
NSR:80121F30|                 push    {r11-r12,r14,pc}

ftrace 콜스택 정보에서 함수 이름 오른쪽에 실행 주소 오프셋이 0x0이면 함수 이름이 맞는 지 의심해봅시다.


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

[리눅스커널] 메모리관리: 가상 주소 변환 과정 전체 구조 파악하기 14장. 메모리 관리

이번 절에서는 가상 주소를 물리 주소로 바꾸는 전반적인 흐름과 세부 개념에 대해 상세히 알아봅니다. 가상 주소를 물리 주소로 변환하는 과정은 리눅스 커널에서 CPU 아키텍처에 의존적입니다.

다음은 라즈베리파이에서 ‘/proc/cpuinfo’ 파일로 확인한 CPU정보입니다. 
[https://www.raspberrypi.org/forums/viewtopic.php?t=155181]
pi@raspberrypi:~ $ less /proc/cpuinfo 
processor       : 0
model name      : ARMv7 Processor rev 4 (v7l)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU revision    : 4

위 출력 결과를 라즈베리파이의 CPU 정보는 ARMv7이며 32비트 커널로 구동됨을 알 수 있습니다. 

그런데 가상 주소를 물리 주소로 변환하는 과정은 리눅스 커널에서 CPU 아키텍처에 의존적입니다. 이번 절에서 가상 주소의 포멧과 주소 변환 과정은 ARMv7 아키텍처에 대한 세부 설명을 담은 다음 문서를 참고했습니다.   
  DDI0406C_C_arm_architecture_reference_manual.pdf 

위 문서는 다음 ARM Infocenter 홈페이지에 접근해 계정 등록을 하면 내려 받을 수 있습니다.
  http://infocenter.arm.com/help/index.jsp

가상 주소 변환 과정 전체 구조 파악하기
가상 주소를 물리주소로 바꾸는 전체 흐름을 먼저 살펴보겠습니다. 다음 그림과 함께 가상주소를 물리주소로 변환하는 과정을 배워볼까요? 
 
그림: 가상 주소 변환 과정 전체 흐름도

위 그림에서 ①로 표시된 부분은 CPU가 가상 주소를 실행하는 동작입니다.
CPU 입장에서는 가상 주소(논리 주소)만 보면서 실행을 합니다.
CPU가 가상 주소를 해석하고 실행하지만 배경으로 가상 주소를 물리 주소로 변환하는 동작을 수행합니다.

이번에 화살표와 함께 _②로 된 부분을 보겠습니다. 가상 주소의 베이스 주소 바탕으로 페이지 테이블에 접근하는 동작입니다. 페이지 테이블 베이스 주소에 먼저 접근해 주소 변환 정보가 있는 페이지 테이블 엔트리 주소를 찾습니다.

그림 ③ 부분은 페이지 테이블 엔트리 레코드를 읽어서 물리 주소로 변환하는 동작입니다. 물리 주소에서 보이는 베이스는 페이지 테이블 엔트리에 있는 주소 정보입니다.

가장 오른쪽에 ④ 로 표시된 부분은 이 정보를 참고해 물리 주소에 접근하는 동작입니다.

전체 시스템 관점에서 가상 주소를 물리 주소로 변환하는 과정을 살펴봤습니다. 그런데 사실 위 그림과 같이 시스템은 동작하지 않습니다.

가상 주소를 물리 주소로 변환하는 일을 하는 주인공은 MMU(Memory Management Unit)이며 하드웨어적으로 위 동작을 처리합니다. 

   그렇다면 MMU는 가상 주소를 물리 주소로 어떻게 바꿀까요?

MMU 내에는 TLB(Translation Lookaside Buffer)가 있는데 이 버퍼에는 페이지 테이블 레코드 정보를 담고 있습니다. 최근에 변환한 가상 주소에 대한 페이지 테이블 정보가 있습니다. 이 정보를 참고해 가상 주소를 물리 주소로 변환해줍니다.

이번에는 TLB 동작이 추가된 그림을 보겠습니다.
 
[그림] 가상 주소 변환 전체 흐름도에서 TLB의 역할

MMU가 가상 주소를 물리 주소로 변환 도중 TLB에 관련 페이지 테이블 엔트리 정보가 있는 경우 TLB Hit라고 부르며 바로 가상 주소를 물리 주소로 변환합니다.

[1] 동작은 TLB에 접근해 페이지 변환 정보를 점검하는데, 만약 TLB에 가상 주소를 변환할 수 있는 페이지 테이블 매핑 정보가 없으면 [3]번과 같이 페이지 테이블 베이스 주소에 접근해 페이지 테이블 매팅 정보를 업데이트합니다.


이렇게 가상 주소를 물리 주소로 변환하는 과정은 리눅스 커널보다 CPU 아키텍처 구조에 의존적입니다. 라즈베리파이는 ARMv7 아키텍처를 적용하므로 관련 내용을 숙지할 필요가 있습니다.


이번 소절에서는 전체 시스템 관점에서 가상 주소를 물리 주소로 변환하는 과정을 살펴봤습니다.
다음에는 시야를 가상 주소와 물리 주소로 좁혀서 세부 주소 변환 과정을 살펴보겠습니다.


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



#커널 메모리 목차 14장. 메모리 관리

#커널 메모리 목차
14.1.1 가상 메모리의 주요 개념 소개
14.1.2 가상 메모리와 가상 주소란
14.1.3 페이징에서 메모리 주소를 계산하는 방법 소개
14.1.4 페이지 프레임 번호와 페이지 디스크립터란
14.1.5 페이지 테이블이란
14.2.1 가상 주소 변환 과정 전체 구조 파악하기
14.2.2 가상 주소를 물리 주소로 변환하는 단계
14.2.3 페이지 테이블 관련 용어 소개
14.2.4 페이지 테이블 종류 알아보기
14.2.5 가상 주소를 물리 주소 변환 세부 원리 알아보기
14.2.6 가상 주소를 물리 주소로 변환해보기
14.3.1 메모리 존(Zone) 종류와 개념 소개
14.3.2 메모리 존 자료구조 struct zone 분석하기
14.4.1 동적 메모리와 정적 메모리 할당 소개
14.4.2 kmalloc() 함수를 쓰는 이유
14.4.3 kmalloc() 함수 소개
14.4.4 gfp 플래그 소개
14.4.5 kmalloc() 함수를 호출할 때 주의 사항
14.5.1 슬랩(Slab) 메모리의 주요 개념
14.5.2 kmalloc-size 슬랩 캐시 알아보기
14.5.3 슬럽 할당 세부 함수 분석하기
14.6 디버깅
14.6.1 ftrace로 메모리 할당 해제 확인하기
14.6.2 가상주소를 물리주소로 변환하는 과정 확인하기
14.6.3 kmalloc() 함수로 메모리 할당 후 슬럽 종류 확인하기
14.7 정리  

#가상 파일시스템 목차 13장. 가상 파일시스템

#가상 파일시스템 목차

13.1 가상 파일시스템 소개
   13.1.1 가상 파일 시스템이란
   13.1.2 가상 파일시스템 공통 모델
   13.1.3 함수 오퍼레이션
   13.1.4 유저 프로세스 입장에서 파일 처리
   13.1.5 파일시스템별 파일 함수 오퍼레이션 처리 과정
13.2 파일 객체
   13.2.1 struc file 구조체 분석 
   13.2.2 파일 객체 함수 오퍼레이션  
13.3 파일 객체 함수 오퍼레이션 동작
    13.3.1 파일을 오픈할 때 open 함수 오퍼레이션
   13.3.2 파일을 쓸 때 write 함수 오퍼레이션
   13.3.3 파일을 읽을 때 read 함수 오퍼레이션
   13.3.4 파일 포인터 위치를 갱신할 때 lseek 함수 오퍼레이션
   13.3.5 파일을 닫을 때 close 함수 오퍼레이션
13.4 프로세스는 파일객체 자료구조를 어떻게 관리할까?
   13.4.1 파일 객체 파일 디스크립터 테이블 등록
   13.4.2 파일 디스크립터로 파일 객체 로딩
   13.4.3 파일 디스크립터 해제
13.5 슈퍼블록 객체
   13.5.1 슈퍼 블록 객체 소개
   13.5.2 struct super_block 구조체 분석
   13.5.3 슈퍼 블록 함수 오퍼레이션
   13.5.4 슈퍼 블록 파일 오퍼레이션 관련 시스템 콜
   13.5.5 슈퍼블록 정보를 statfs 시스템 콜로 읽는 과정
13.6 아이노드 객체
   13.6.1 struct inode 구조체 분석
   13.6.2 아이노드 함수 오퍼레이션
   13.6.3 파일 속성을 읽는 stat 시스템 콜 처리 과정 분석하기
13.7 덴트리 객체
   13.7.1 덴트리 객체 소개
   13.7.2 struct dentry 구조체 분석
13.8 가상 파일시스템 디버깅
   13.8.1 파일 객체 함수 오퍼레이션 확인하기
   13.8.2 슈퍼블록 객체 함수 오퍼레이션 확인하기
   13.8.3 아이노드 객체 함수 오퍼레이션 확인하기
13.9 정리

시그널 목차 12장. 시그널

시그널 목차
#Referene 시그널
시그널이란
시그널 설정은 어떻게 할까
시그널 생성 과정 함수 분석
   유저 프로세스 tgkill()함수 실행
   커널은 언제 시그널 생성할까?
   __send_signal() 함수 분석
시그널 전달 진입점
   ret_fast_syscall 레이블 분석
   인터럽트 벡터
시그널 전달과 처리는 어떻게 할까?
   get_signal() 함수 분석
   handle_signal() 함수 분석
시그널 제어 함수 분석
   suspend() 함수
시그널 ftrace 디버깅
   ftrace 시그널 기본 동작 로그 분석
   ftrace 시그널 핸들러 동작 로그 분석

프로세스 스케줄링 목차 10장. 프로세스 스케줄링

(#Reference 프로세스 스케줄링)
스케줄링 소개
프로세스 상태 관리
   프로세스 상태 소개
   프로세스 상태 변화
   어떤 함수가 프로세스 상태를 변경할까? 
         TASK_RUNNING(실행 대기)
 TASK_RUNNING(CPU 실행)
 TASK_INTERRUPTIBLE 상태 변경
 TASK_UNINTERRUPTIBLE 상태 변경
   프로세스 상태 ftrace로 확인하기
스케줄링 클래스
   스케줄링 클래스 자료구조 소개
   5가지 스케줄러 클래스란 무엇일까?
   프로세스는 스케줄러 클래스를 어떻게 등록할까?
   프로세스는 스케줄링 클래스로 스케줄러 세부 함수를 어떻게 호출할까?
런큐
   런큐 자료구조 struct rq 소개
   runqueues 변수에 대해서
   런큐에 접근하는 함수 소개
   런큐 자료구조 확인하기
선점 스케줄링(Preemptive Scheduling)
프로세스는 어떻게 깨울까?
   프로세스를 깨운다는 것을 무엇을 의미할까?
   프로세스를 깨울 때 호출하는 함수
   프로세스를 런큐에 Enqueue하는 흐름
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
   컨택스트 스위칭이란 무엇인가?  
   컨택스트 스위칭 자료구조는 무엇일까?
스케줄링 디버깅
   ftrace: sched_switch와 sched_wakeup 이벤트 소개
   sched_switch/sched_wakeup 이벤트 출력 함수 코드 분석
   스케줄링과 프로세스를 깨울 때 콜스택 파악
   비선점 스케줄링 콜스택 파악
   스케줄링 프로파일링


10.1 스케줄링 소개
   10.1.1 스케줄링란 무엇일까?
   10.1.2 선점과 비선점 스케줄링이란
   10.1.3 컨택스트 스위칭이란
   10.1.4 스케줄링 정책이란
   10.1.5 스케줄러 클래스란 무엇일까?
   10.1.6 런큐란 무엇일까?
   10.1.7 우선순위(nice)란
10.2 프로세스 상태 관리
   10.2.1 프로세스 상태 소개  
   10.2.2. 프로세스 상태 변화 
   10.2.3 어떤 함수가 프로세스 상태를 바꿀까?
10.3 스케줄러 클래스
   10.3.1 스케줄링 클래스 자료구조 소개
   10.3.2 5가지 스케줄러 클래스란 무엇일까?
   10.3.3 프로세스는 스케줄러 클래스를 어떻게 등록할까?
   10.3.4 프로세스는 스케줄링 클래스로 스케줄러 세부 함수를 어떻게 호출할까?
10.4 런큐
   10.4.1 런큐 자료구조 struct rq 소개
   10.4.2 runqueues 변수에 대해서
   10.4.3 런큐에 접근하는 함수 소개
   10.4.4 런큐 자료구조 확인하기 
10.5 CFS 스케줄러
   10.5.1 CFS 스케줄러를 이루는 주요 개념 알아보기
   10.5.2 CFS 스케줄러 알고리즘
   10.5.3 CFS 관련 세부 함수 분석  
     10.5.3.1 타임 슬라이스 관리
     10.5.3.2 vruntime 관리 세부 함수 분석
   10.5.4 vruntime을 ftrace로 확인하는 실습 따라해보기
10.6 선점 스케줄링(Preemptive Scheduling)   
   10.6.1 선점 스케줄링(Preemptive Scheduling)이란 무엇일까?  
   10.6.2 선점 스케줄링 진입점은 어디인가?
   10.6.3 선점 스케줄링 발생 시점을 아는 것은 왜 중요할까?
   10.6.4 선점 스케줄링 진입점: 커널 모드 중 인터럽트 발생
   10.6.5 선점 스케줄링: 유저 프로세스 실행 중 인터럽트가 발생
   10.6.6 선점 스케줄링 진입점: 유저 프로세스가 시스템 콜 처리를 마무리한 후
   10.6.7 선점 스케줄링 지연 함수 preempt_enable()/preempt_disable() 소개
10.7 프로세스는 어떻게 깨울까?
   10.7.1. 프로세스를 깨운다는 것은 무엇을 의미할까?
   10.7.2 프로세스를 깨울 때 호출하는 함수 
   10.7.3 깨우는 프로세스를 런큐에 Enqueue 동작
10.8 스케줄링 핵심 schedule() 함수 분석
   10.8.1 스케줄링 핵심 schedule() 함수 분석하기
   10.8.2 schedule() 함수 동작 정리하기 
10.9 컨택스트 스위칭
   10.9.1 컨택스트 스위칭이란 무엇인가?
   10.9.2 컨택스트 스위칭 관련 자료 구조 알아보기
   10.9.3 컨택스트 스위칭 세부 코드 분석
   10.9.4 ftrace로 컨택스트 스위칭 동작 확인
   10.9.5 컨택스트 스위칭 디버깅하기
10.10 스케줄링 디버깅
   10.10.1 ftrace: sched_switch와 sched_wakeup 이벤트 소개 
   10.10.2 ftrace: 스케줄링과 프로세스를 깨울 때 콜스택 파악하기  
   10.10.3 스케줄링 프로파일링
     10.10.3.1 CPU에 부하를 주는 테스트  
     10.10.3.2 CPU에 부하를 주지 않는 테스트   
10.11. 정리


#라즈베리파이 설정 목차 2장. 라즈베리파이 설정

#라즈베리파이 설정 목차
라즈베리파이 소개
라즈베리파이 설정 방법
   라즈베리파이 실습을 위한 준비물 챙기기
   라즈베리파이 설치하기
   라즈베리파이 기본 세팅하기
라즈베리파이 커널 빌드하기
   라즈비안 커널 소스 코드 내려받기
   라즈비안 리눅스 커널 빌드하기
   라즈비안 리눅스 커널 설치하기
   전처리 코드 생성해보기
라즈베리파이에서 objdump 바이너리 유틸리티 써보기

리눅스 소개와 전망 목차 1장. 리눅스 소개

#리눅스 소개와 전망 목차 
리눅스의 역사
운영체제 점유율
리눅스가 인기가 있는 이유는 무엇인가
리눅스는 어디에 쓸까
리눅스 개발 단체
리눅스 커널은 왜 알아야 할까
리눅스 커널의 구조


[리눅스커널] 시스템 콜: 유저 공간에서 전달한 문자열 처리 방법 11장. 시스템 콜

유저 공간에서 시스템 콜 아규먼트로 지정한 파일 이름은 커널 공간에 그대로 전달됩니다.

한 가지 예를 들어볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/open.c]
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;

return do_sys_open(AT_FDCWD, filename, flags, mode);
}

위 sys_open() 함수 첫 번째 아규먼트로 filename이 전달됩니다.
이번에는 다른 예시를 들겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/open.c]
SYSCALL_DEFINE3(fchmodat, int, dfd, const char __user *, filename,
umode_t, mode)
{
return do_fchmodat(dfd, filename, mode);
}

int do_fchmodat(int dfd, const char __user *filename, umode_t mode)
{
struct path path;
int error;
unsigned int lookup_flags = LOOKUP_FOLLOW;

위 코드를 보면 유저 공간에서 전달된 filename 아규먼트가 보입니다.

그런데 filename의 정체는 무엇일까요?
이는 유저 공간에 위치한 문자열 메모리 공간을 의미합니다.

그렇다면 커널 공간에서 이 문자열을 처리하려면 어떻게 해야 할까요?

     "커널 공간에 버퍼를 잡고 문자열을 복사해야 합니다."
 
한 가지 예를 들어볼까요?
diff --git a/fs/open.c b/fs/open.c
index a003500..6e6e536 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -568,11 +568,17 @@ SYSCALL_DEFINE2(fchmod, unsigned int, fd, umode_t, mode)
        return ksys_fchmod(fd, mode);
 }

+#define USER_FILE_NAME "/proc/cmdline"
+
 int do_fchmodat(int dfd, const char __user *filename, umode_t mode)
 {
        struct path path;
        int error;
        unsigned int lookup_flags = LOOKUP_FOLLOW;
+
+       if(!strcmp(filename, USER_FILE_NAME)) {
+               printk("[+] filename: %s \n", filename);
+       }
 retry:
        error = user_path_at(dfd, filename, lookup_flags, &path);
        if (!error) {

위 패치 코드의 목적은 다음과 같습니다.
     
 "유저 공간에서 /proc/cmdline 퍼미션을 설정할 때 동작을 트레이싱하고 싶다."  

그런데 문제는 이렇게 코드를 작성하면 커널 패닉이 발생합니다. 

그렇다면 다음과 같이 코드를 작성하면 어떨까요?
diff --git a/fs/open.c b/fs/open.c
index 4dbbacc..55573c4 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -580,11 +580,29 @@ SYSCALL_DEFINE2(fchmod, unsigned int, fd, umode_t, mode)
        return ksys_fchmod(fd, mode);
 }

+static int debug_kernel_filename = 1;
+
+#define USER_FILE_NAME "/proc/cmdline"
 int do_fchmodat(int dfd, const char __user *filename, umode_t mode)
 {
        struct path path;
        int error;
        unsigned int lookup_flags = LOOKUP_FOLLOW;
+
+       if (debug_kernel_filename) {
+               char buffer[256] = {0,};
+               if(filename) {
+                       strncpy_from_user(&buffer[0], filename, sizeof(buffer) - 1);
+                       buffer[sizeof(buffer) - 1] = '\0';
+
+                       printk("[+][chmod] filename :%s \n", buffer);
+                       if (!strncmp(USER_FILE_NAME, buffer, strlen(USER_FILE_NAME)) ) {
+                               printk("[-] strncmp filename :%s \n", buffer);
+                       }
+               }
+       }
+
+
 retry:
        error = user_path_at(dfd, filename, lookup_flags, &path);
        if (!error) {

위 패치 코드의 원리는 간단합니다.
- strncpy_from_user() 함수로 유저 공간에 있는 문자열을 커널 스택 공간으로 복사
- strncmp() 함수를 써서 문자열 만큼 buffer와 비교 

위 코드를 반영하고 커널 로그를 받으니 제대로 동작하네요.
[   77.645675 / 01-01 00:01:38.189][6] Freeing unused kernel memory: 6784K
[   77.652541 / 01-01 00:01:38.199][7] Run /init as init process
[   77.875408 / 01-01 00:01:38.419][6] [+][chmod] filename :/proc/cmdline
[   77.882229 / 01-01 00:01:38.429][6] [-] strncmp filename :/proc/cmdline
...
[   78.635313 / 01-01 00:11:28.419][7] [+][chmod]] filename :/proc/self/fd/8
[   78.735755 / 01-01 00:11:28.519][6] [+][chmod]] filename :/proc/self/fd/13
[   78.743632 / 01-01 00:11:28.529][6] [+][chmod]] filename :/proc/self/fd/13
[   78.751334 / 01-01 00:11:28.539][6] [+][chmod]] filename :/proc/self/fd/13
[   78.758635 / 01-01 00:11:28.539][6] [+][chmod]] filename :/proc/self/fd/13


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




[리눅스커널] 스케줄링: CFS 스케줄러를 이루는 주요 개념 알아보기 10장. 프로세스 스케줄링

CFS(Completely Fair Scheduler)는 2.6.23 커널 버전 이후 적용된 리눅스의 기본 스케줄러입니다. CFS이란 용어를 그대로 풀면 ‘완벽하게 공정한 스케줄러’라고 해석할 수 있습니다. 즉, 런큐에서 실행 대기 상태로 기다리는 프로세스를 공정하게 실행하도록 기회를 부여하는 스케줄러입니다. 

이번 절에서는 CFS 알고리즘의 개념을 알아보고 코드 분석으로 세부 동작을 살펴보겠습니다.

CFS 스케줄러를 이루는 주요 개념 알아보기
CFS는 실행 대기 상태인 프로세스들을 우선 순위에 따라 최대한 공정하게 실행하는 스케줄러입니다. CFS 세부 동작과 알고리즘을 이해하려면 다음과 같은 개념을 파악할 필요가 있습니다. 
  - 타임 슬라이스
  - 우선순위
  - 가상 실행 시간(vruntime)

이번 소절에는 먼저 CFS를 이루는 3가지 주요 개념을 소개합니다.
 
타임 슬라이스란
타임 슬라이스는 무엇을 말하는 것일까요? 먼저 용어의 뜻부터 살펴봅시다.
타임 슬라이스는 사실 운영체제에서 쓰는 용어입니다. 

    '스케줄러가 프로세스에게 부여한 실행 시간' 

CFS는 프로세스마다 실행할 수 있는 단위 시간을 부여하는데 이를 타임 슬라이스라고 부릅니다. 프로세스는 주어진 타임 슬라이스를 다 소진하면 컨택스트 스위칭됩니다.

타임 슬라이스로 프로세스를 관리하는 가장 큰 이유는 모든 프로세스가 최대한 CPU에서 실행할 수 있는 기회를 부여하기 위해서입니다. 만약 타임 슬라이스가 없다면 프로세스 실행 단위를 관리하기 어려울 것입니다.

스케줄러 입장에서는 타임 슬라이스는 프로세스 실행 시간을 관리하기 위한 단위라고 볼 수 있습니다. 하지만 프로세스 입장에서 타임 슬라이스는 모래시계와 같습니다. 만약 프로세스에게 부여된 타임 슬라이스가 10ms이라고 가정해봅시다. 프로세스가 5ms 시간 동안 실행했다면 실행할 수 있는 시간이 5ms 남은 것입니다. 즉 5ms 만큼 타임 슬라이스를 소진한 것입니다.

CFS 스케줄러는 타이머 인터럽트가 발생했을 때 주기적으로 프로세스가 얼마나 타임 슬라이스를 소진하고 있는지 체크합니다. 

   "프로세스가 타임 슬라이스를 모두 소진하면 스케줄러는 어떤 동작을 할까?" 

스케줄러는 해당 프로세스가 선점 스케줄링 대상이라고 마킹합니다. 


이 동작을 선점 스케줄링 요청을 한다라고도 부릅니다. 해당 프로세스의 struct thread_info 필드 flags에 TIF_NEED_RESCHED 플래그를 설정합니다.


선점 스케줄링 대상으로 마킹된 프로세스는 컨택스트 스위칭됩니다.

우선 순위란
우선 순위는 CFS 스케줄러가 컨택스트 스위칭 시 다음 프로세스를 선택하는 기준 중 하나입니다.
CFS 스케줄러는 우선 순위가 높은 프로세스에 대해 다음과 같이 처리합니다.  

     "가장 먼저 CPU에서 실행시킨다." 
      "더 많은 타임 슬라이스를 할당해준다."

리눅스는 총 140단계의 우선순위 제공하며 다음과 같은 범위입니다.
  - 0~99: RT(실시간) 프로세스
  - 100~139: 일반 프로세스

CFS 스케줄러는 프로세스마다 설정된 우선 순위를 기준으로 가상 실행 시간을 설정합니다. 이를 위해 커널이 프로세스 우선 순위를 어떤 방식으로 처리하는지 파악할 필요가 있습니다.

가상 실행 시간이란
가상 실행시간(vruntime)은 가상이란 의미인 형용사인 "virtual"와 실행 시간을 뜻하는 "runtime"을 붙여 만든 용어입니다. 즉, vruntime을 가상 실행시간이라고 해석할 수 있습니다.

커널에서 vruntime는 무엇이라고 설명할 수 있을까요?

   "프로세스가 그 동안 실행한 시간을 정규화한 시간 정보이다." 

CFS가 프로세스를 선택할 수 있는 load weight와 같은 여러 지표가 고려된 실행 시간입니다.

vruntime은 다음과 같은 특징이 있습니다.
 - CFS는 런큐에 실행 대기 상태에 있는 프로세스 중 vruntime이 가장 적은 프로세스를 다음에 실행할 프로세스로 선택합니다.
  - 우선 순위가 높은 프로세스는 우선 순위가 낮은 프로세스에 비해 vruntime가 더 서서히 증가합니다. 즉, 우선 순위가 높은 프로세스가 낮은 프로세스에 비해 vruntime가 덜 증가한다는 것입니다.

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

   "vruntime은 어느 자료구조에서 확인할 수 있을까?"

프로세스는 자신의 태스크 디스크립터 &se.vruntime 필드에 vruntime 정보를 저장합니다. 
다음은 Trace32로 확인한 vruntime 정보입니다.
01  (struct task_struct *) (struct task_struct*)0xA36AF9180
...
02    (struct sched_class *) sched_class = 0xA9208BE0,
03    (struct sched_entity) se = (
04      (struct load_weight) load = ((long unsigned int) weight = 90891264,
05      (struct rb_node) run_node = ((long unsigned int) __rb_parent_color = 1,
06      (struct list_head) group_node 
07      (unsigned int) on_rq = 0 
08      (u64) exec_start = 13097506209 
09      (u64) sum_exec_runtime = 15165884 
10      (u64) vruntime = 3532405028 

10 번째 줄과 같이 vruntime은 3532405028임을 알 수 있습니다. 

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


[IT] 좋은 IT회사를 입사하기 전에 대략적이라도 판단할 수 있는 방법 임베디드 에세이

부서내 편차는 어느 탑 클래스 IT업체에서도 존재하며 화려한 스팩과 경력의 개발자도 '벌레처럼' 기어다닐 수도 있습니다.
신입 개발자가 좋은 부서로 배치 받는 것은 '운에 좌우된다.'라고 말할 수 밖에 없습니다.

또한 IT 업체마다 '개발 문화/개발 방식/평가 기준'이 다르기 때문에 '어느 업체는 어떻다.'라고 이야기하긴 어렵습니다.
만약 어떤 분이 코드 작성 속도가 느리나 사이드 없이 제대로 과제를 마무리한다고 가정하겠습니다.

저 같으면 이런 분을 아주 높게 평가하고 과제를 끝낼 때 까지 기다려 줄 것입니다. 하지만 관점이 다른 분은 이런 부류의 개발자를 낮게 바라볼 수 있는 것입니다.
또한 부서과 과제의 미션에 따라 달라질 수 있습니다. 이에 대한 평가는 제각각입니다. 

하지만 그래도 어떤 IT업체가 전망이 좋은지 IT 회사 밖에서 판단할 수 있는 여러가지 척도는 있습니다.

1. 그 업체가 그 분야를 선도하고 있는가

IT업체가 3rd Party(외주) 업체이건 큰 규모이건 업체가 개발하는 기술이 시장을 선도하고 있는지 확인하십시오.
시장을 선도하거나 선도하려고 하는 IT업체는 실력 좋은 개발자에게 어느 정도 대우할 수 밖에 없습니다.
실력 좋은 개발자가 있는 IT업체가 실력있는 회사라고 볼 수 있겠죠?

2. Senior 개발자를 어떻게 대우하는가

Senior 개발자를 업계 수준 이상(연봉)으로 대우하는지 확인할 필요가 있습니다. 
단순히 돈 많이 주는 회사를 가라고 하는 것은 아닙니다.

원천 소프트웨어 기술이 없거나 업무 전문성이 없는 IT 회사는 사실 'Senior 개발자'가 필요 없습니다.
이런 부류의 업체는 대부분 '개발자의 몸빵'으로 버티는 경우가 많습니다. Junior 급 개발자를 뽑고 굴리는 것이지요.
임베디드 혹은 BSP 리눅스 업체에서도 생각보다 개발자의 '시간을 갈아 넣는' 일은 의외로 많습니다.
- 실컷 테스트를 한 다음에 로그 말아 주기
- 리눅스 버전을 기계적으로 마이그레이션(git merge) 하기(소스 분석은 없음)
- 하루 종일 빌드 스크립트를 작성하기 

3. 개발 문화를 알아보세요.
최근 Flexible time제는 SW 회사의 기본으로 자리 잡았습니다.
개발 문화(Flexible 근무, 재택근무)가 어떤지도 그 회사가 어떤지 알아보는 방법 중 하나입니다.

그렇다면 IT 회사가 개발 문화에 신경을 쓰는 이유는 무엇일까요?
그것은 실력 좋은 개발자를 유치하기 위해서입니다.

'야근이 너무 많다, 이직이 심하다.' 이런 소문이 들리는 업체는 전문성이 떨어지는 소프트웨어를 개발할 가능성이 높습니다.

제 생각에 운이 좋지 않게 좋은 회사의 나쁜 부서로 배치받는게 나쁜 회사의 좋은 부서에 배치되는 것 보단 낫다고 봅니다. '좋은 회사의 나쁜 부서'에 배치 받아도 꾸준히 기량을 닦고 내공을 키우면 언젠가 기회는 오기 마련이거든요.

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




[리눅스커널] 커널 소스 읽기가 제일 쉬었어요(1) - /proc/cpuinfo Linux Kernel - Core Analysis

/proc/cpuinfo
/proc/cpuinfo 파일은 CPU 아키텍처 정보를 저장합니다.

root:/proc $ cat cpuinfo
Processor       : AArch64 Processor rev 0 (aarch64)
processor       : 0
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x51
CPU architecture: 8
CPU variant     : 0xd
CPU part        : 0x805
CPU revision    : 14

processor       : 1
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x51
CPU architecture: 8
CPU variant     : 0xd
CPU part        : 0x805
CPU revision    : 14

processor       : 2
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x51
CPU architecture: 8
CPU variant     : 0xd
CPU part        : 0x805
CPU revision    : 14

processor       : 3
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x51
CPU architecture: 8
CPU variant     : 0xd
CPU part        : 0x805
CPU revision    : 14

processor       : 4
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x41
CPU architecture: 8
CPU variant     : 0x0
CPU part        : 0xd0d
CPU revision    : 0

processor       : 5
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x41
CPU architecture: 8
CPU variant     : 0x0
CPU part        : 0xd0d
CPU revision    : 0

processor       : 6
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x41
CPU architecture: 8
CPU variant     : 0x0
CPU part        : 0xd0d
CPU revision    : 0

processor       : 7
BogoMIPS        : 38.40
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer : 0x41
CPU architecture: 8
CPU variant     : 0x0
CPU part        : 0xd0d
CPU revision    : 0

'proc/cpuinfo' 파일은 proc_cpuinfo_init() 함수에서 03번째 줄을 실행할 때 생성됩니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/proc/cpuinfo.c]
01 static int __init proc_cpuinfo_init(void)
02 {
03    proc_create("cpuinfo", 0, NULL, &proc_cpuinfo_operations);
04    return 0;
05 }
06 fs_initcall(proc_cpuinfo_init);

proc_cpuinfo_init() 함수 선언부 키워드에 __init이 있으니 부팅 도중 호출된다는 사실을 알 수 있습니다.


'proc/cpuinfo' 파일 연산
'proc/cpuinfo' 파일로 어떤 연산을 수행하는지 알려면 proc_cpuinfo_operations 전역 변수를 봐야 합니다.
static const struct file_operations proc_cpuinfo_operations = {
.open = cpuinfo_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release,
};

proc 파일 시스템에서 seq_file 인터페이스를 써서 파일 출력을 하므로 open 함수 연산을 수행하는 cpuinfo_open() 함수를 분석할 필요가 있습니다.

다음 cpuinfo_open() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/proc/cpuinfo.c]
01 static int cpuinfo_open(struct inode *inode, struct file *file)
02 {
03 arch_freq_prepare_all();
04 return seq_open(file, &cpuinfo_op);
05 }

05번째 줄에서 seq_open() 함수 2번째 인자로 cpuinfo_op seq_file 연산을 지정합니다.

struct seq_operations 구조체 타입인 cpuinfo_op 함수 연산은 아키텍처별로 위치가 다릅니다.

AArch64 ARMv8 아키텍처의 경우 cpuinfo_op 는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/cpuinfo.c]
const struct seq_operations cpuinfo_op = {
.start = c_start,
.next = c_next,
.stop = c_stop,
.show = c_show
};

다음은 'cat /proc/cpuinfo' 명령어를 입력했을 때 CPU 아키텍처를 출력하는 c_show() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/cpuinfo.c]
01 static int c_show(struct seq_file *m, void *v)
02 {
03 int i, j;
04 bool compat = personality(current->personality) == PER_LINUX32;
05 
06 for_each_online_cpu(i) {
07 struct cpuinfo_arm64 *cpuinfo = &per_cpu(cpu_data, i);
08 u32 midr = cpuinfo->reg_midr;
09
10 seq_printf(m, "processor\t: %d\n", i);
11 if (compat)
12 seq_printf(m, "model name\t: ARMv8 Processor rev %d (%s)\n",
13    MIDR_REVISION(midr), COMPAT_ELF_PLATFORM);
14
15 seq_printf(m, "BogoMIPS\t: %lu.%02lu\n",
16    loops_per_jiffy / (500000UL/HZ),
17    loops_per_jiffy / (5000UL/HZ) % 100);

위 함수 코드와 같이 percpu 타입인 cpu_data 변수로 CPU 정보를 출력한다는 사실을 알 수 있습니다.

cpu_data percpu 타입 전역 변수 디버깅해보기 
cpu_data를 크래시 유틸리티(Crash-Utility) 프로그램으로 확인하겠습니다.
crash64_kaslr> p cpu_data
PER-CPU DATA TYPE:
  struct cpuinfo_arm64 cpu_data;
PER-CPU ADDRESSES:
  [0]: ffffffff3f530290
  [1]: ffffffff3f549290
  [2]: ffffffff3f562290
  [3]: ffffffff3f57b290
  [4]: ffffffff3f594290
  [5]: ffffffff3f5ad290
  [6]: ffffffff3f5c6290
  [7]: ffffffff3f5df290

출력 결과가 보이듯 percpu 타입 변수란 사실을 알 수 있습니다.
어떤 전역 변수가 percpu 타입 변수면 다음과 같이 떠올릴 필요가 있습니다. 

   CPU 갯수만큼 주소 공간이 있다.  
  
cpu_data 변수에서 per-cpu0 정보를 확인하면 다음과 같습니다.
crash64_kaslr> struct cpuinfo_arm64 ffffffff3f530290
struct cpuinfo_arm64 {
  cpu = {
    node_id = 0x0,
    hotpluggable = 0x1,
    dev = {
      parent = 0x0,
      p = 0xffffffff364d3380,
      kobj = {
        name = 0xffffffff36573780 "cpu0",
        entry = {
          next = 0xffffffff3f5492b0,
          prev = 0xffffffff364def68
        },
        parent = 0xfffffffebfec1890,
        kset = 0xfffffffebfeb7280,
        ktype = 0xffffff9689a84170,
        sd = 0xffffffff3654cae8,
        kref = {
          refcount = {
            refs = {
              counter = 0x8
            }
          }
        },
        state_initialized = 0x1,
        state_in_sysfs = 0x1,
        state_add_uevent_sent = 0x1,
        state_remove_uevent_sent = 0x0,
        uevent_suppress = 0x0
      },
      init_name = 0x0,
      type = 0x0,
      mutex = {
        owner = {
          counter = 0x0
        },
        wait_lock = {
          {
            rlock = {
              raw_lock = {
                {
                  val = {
                    counter = 0x0
                  },
                  {
                    locked = 0x0,
                    pending = 0x0
                  },
                  {
                    locked_pending = 0x0,
                    tail = 0x0
                  }
                }
              }
            }
          }
        },
        osq = {
          tail = {
            counter = 0x0
          }
        },
        wait_list = {
          next = 0xffffffff3f530308,
          prev = 0xffffffff3f530308
        }
      },
      bus = 0xffffff9689a84900,
      driver = 0x0,
      platform_data = 0x0,
      driver_data = 0x0,
      links = {
        suppliers = {
          next = 0xffffffff3f530338,
          prev = 0xffffffff3f530338
        },
        consumers = {
          next = 0xffffffff3f530348,
          prev = 0xffffffff3f530348
        },
        status = DL_DEV_NO_DRIVER
      },
      power = {
        power_state = {
          event = 0x0
        },
        can_wakeup = 0x0,
        async_suspend = 0x0,
        in_dpm_list = 0x1,
        is_prepared = 0x0,
        is_suspended = 0x0,
        is_noirq_suspended = 0x0,
        is_late_suspended = 0x0,
        early_init = 0x1,
        direct_complete = 0x0,
        driver_flags = 0x0,
        lock = {
          {
            rlock = {
              raw_lock = {
                {
                  val = {
                    counter = 0x0
                  },
                  {
                    locked = 0x0,
                    pending = 0x0
                  },
                  {
                    locked_pending = 0x0,
                    tail = 0x0
                  }
                }
              }
            }
          }
        },
        entry = {
          next = 0xffffffff3f549370,
          prev = 0xffffffff364df028
        },
        completion = {
          done = 0xffffffff,
          wait = {
            lock = {
              {
                rlock = {
                  raw_lock = {
                    {
                      val = {
                        counter = 0x0
                      },
                      {
                        locked = 0x0,
                        pending = 0x0
                      },
                      {
                        locked_pending = 0x0,
                        tail = 0x0
                      }
                    }
                  }
                }
              }
            },
            head = {
              next = 0xffffffff3f530390,
              prev = 0xffffffff3f530390
            }
          }
        },
        wakeup = 0x0,
        wakeup_path = 0x0,
        syscore = 0x0,
        no_pm_callbacks = 0x1,
        must_resume = 0x0,
        may_skip_resume = 0x0,
        suspend_timer = {
          entry = {
            next = 0x0,
            pprev = 0x0
          },
          expires = 0x0,
          function = 0xffffff9687fe2bd8,
          flags = 0x7
        },
        timer_expires = 0x0,
        work = {
          data = {
            counter = 0xfffffffe0
          },
          entry = {
            next = 0xffffffff3f5303e8,
            prev = 0xffffffff3f5303e8
          },
          func = 0xffffff9687fe2b30
        },
        wait_queue = {
          lock = {
            {
              rlock = {
                raw_lock = {
                  {
                    val = {
                      counter = 0x0
                    },
                    {
                      locked = 0x0,
                      pending = 0x0
                    },
                    {
                      locked_pending = 0x0,
                      tail = 0x0
                    }
                  }
                }
              }
            }
          },
          head = {
            next = 0xffffffff3f530408,
            prev = 0xffffffff3f530408
          }
        },
        wakeirq = 0x0,
        usage_count = {
          counter = 0x0
        },
        child_count = {
          counter = 0x0
        },
        disable_depth = 0x1,
        idle_notification = 0x0,
        request_pending = 0x0,
        deferred_resume = 0x0,
        runtime_auto = 0x1,
        ignore_children = 0x0,
        no_callbacks = 0x0,
        irq_safe = 0x0,
        use_autosuspend = 0x0,
        timer_autosuspends = 0x0,
        memalloc_noio = 0x0,
        links_count = 0x0,
        request = RPM_REQ_NONE,
        runtime_status = RPM_SUSPENDED,
        runtime_error = 0x0,
        autosuspend_delay = 0x0,
        last_busy = 0x0,
        active_jiffies = 0x0,
        suspended_jiffies = 0x0,
        accounting_timestamp = 0xffff8bba,
        subsys_data = 0x0,
        set_latency_tolerance = 0x0,
        qos = 0xffffffff364d2480
      },
      pm_domain = 0x0,
      msi_domain = 0x0,
      pins = 0x0,
      msi_list = {
        next = 0xffffffff3f530490,
        prev = 0xffffffff3f530490
      },
      dma_ops = 0x0,
      dma_mask = 0x0,
      coherent_dma_mask = 0x0,
      bus_dma_mask = 0x0,
      dma_pfn_offset = 0x0,
      dma_parms = 0x0,
      dma_pools = {
        next = 0xffffffff3f5304d0,
        prev = 0xffffffff3f5304d0
      },
      dma_mem = 0x0,
      cma_area = 0x0,
      removed_mem = 0x0,
      archdata = {
        iommu = 0x0,
        dma_coherent = 0x0,
        mapping = 0x0
      },
      of_node = 0xffffffff3f604ea8,
      fwnode = 0x0,
      devt = 0x0,
      id = 0x0,
      devres_lock = {
        {
          rlock = {
            raw_lock = {
              {
                val = {
                  counter = 0x0
                },
                {
                  locked = 0x0,
                  pending = 0x0
                },
                {
                  locked_pending = 0x0,
                  tail = 0x0
                }
              }
            }
          }
        }
      },
      devres_head = {
        next = 0xffffffff3f530530,
        prev = 0xffffffff3f530530
      },
      knode_class = {
        n_klist = 0x0,
        n_node = {
          next = 0x0,
          prev = 0x0
        },
        n_ref = {
          refcount = {
            refs = {
              counter = 0x0
            }
          }
        }
      },
      class = 0x0,
      groups = 0xffffff9689a849c0,
      release = 0xffffff9687fd6778,
      iommu_group = 0x0,
      iommu_fwspec = 0x0,
      offline_disabled = 0x0,
      offline = 0x0,
      of_node_reused = 0x0
    }
  },
  kobj = {
    name = 0xffffff96890b9755 "regs",
    entry = {
      next = 0xffffffff3f530598,
      prev = 0xffffffff3f530598
    },
    parent = 0xffffffff3f5302a8,
    kset = 0x0,
    ktype = 0xffffff96899cdfa8,
    sd = 0xffffffff2e1a9b38,
    kref = {
      refcount = {
        refs = {
          counter = 0x1
        }
      }
    },
    state_initialized = 0x1,
    state_in_sysfs = 0x1,
    state_add_uevent_sent = 0x0,
    state_remove_uevent_sent = 0x0,
    uevent_suppress = 0x0
  },
  reg_ctr = 0x84448004,
  reg_cntfrq = 0x124f800,
  reg_dczid = 0x4,
  reg_midr = 0x51df805e,
  reg_revidr = 0xf,
  reg_id_aa64dfr0 = 0x10305408,
  reg_id_aa64dfr1 = 0x0,
  reg_id_aa64isar0 = 0x100010211120,
  reg_id_aa64isar1 = 0x100001,
  reg_id_aa64mmfr0 = 0x101122,
  reg_id_aa64mmfr1 = 0x10212122,
  reg_id_aa64mmfr2 = 0x1011,
  reg_id_aa64pfr0 = 0x11112222,
  reg_id_aa64pfr1 = 0x0,
  reg_id_aa64zfr0 = 0x0,
  reg_id_dfr0 = 0x4010088,
  reg_id_isar0 = 0x2101110,
  reg_id_isar1 = 0x13112111,
  reg_id_isar2 = 0x21232042,
  reg_id_isar3 = 0x1112131,
  reg_id_isar4 = 0x11142,
  reg_id_isar5 = 0x1011121,
  reg_id_mmfr0 = 0x10201105,
  reg_id_mmfr1 = 0x40000000,
  reg_id_mmfr2 = 0x1260000,
  reg_id_mmfr3 = 0x2122211,
  reg_id_pfr0 = 0x10000131,
  reg_id_pfr1 = 0x10011011,
  reg_mvfr0 = 0x10110222,
  reg_mvfr1 = 0x13211111,
  reg_mvfr2 = 0x43,
  reg_zcr = 0x0
}

struct cpuinfo_arm64 구조체 필드 출력 결과를 모두 표시했습니다. 나중에 디버깅을 할 때 필요할지 모르기 때문입니다.

눈치가 빠른 분은 위 출력 결과로 Aarch64 아키텍처이란 사실을 유추할 수 있을 것입니다. 

   "주소 정보를 16바이트 단위로 출력한다." 
 
이번에 레퍼런스를 위해 ARMv7 아키텍처에서 cpu_data percpu 변수를 확인하겠습니다.

crash> p cpu_data
PER-CPU DATA TYPE:
  struct cpuinfo_arm cpu_data;
PER-CPU ADDRESSES:
  [0]: e6b16048
  [1]: e6b29048
  [2]: e6b3c048
  [3]: e6b4f048
  [4]: e6b62048
  [5]: e6b75048
  [6]: e6b88048
  [7]: e6b9b048

crash> struct cpuinfo_arm  e6b16048
struct cpuinfo_arm {
  cpu = {
    node_id = 0x0,
    hotpluggable = 0x0,
    dev = {
      parent = 0x0,
      p = 0xe57a6200,
      kobj = {
        name = 0xe57a5680 "cpu0",
        entry = {
          next = 0xe6b2905c,
          prev = 0xe57a720c
        },
        parent = 0xe5e02e08,
        kset = 0xe5df0c80,
        ktype = 0xc17522ec <device_ktype>,
        sd = 0xe57a83c0,
        kref = {
          refcount = {
            counter = 0x3
          }
        },
        state_initialized = 0x1,
        state_in_sysfs = 0x1,
        state_add_uevent_sent = 0x1,
        state_remove_uevent_sent = 0x0,
        uevent_suppress = 0x0
      },
      init_name = 0x0,
      type = 0x0,
      mutex = {
        count = {
          counter = 0x1
        },
        wait_lock = {
          {
            rlock = {
              raw_lock = {
                {
                  slock = 0x20002,
                  tickets = {
                    owner = 0x2,
                    next = 0x2
                  }
                }
              },
              magic = 0xdead4ead,
              owner_cpu = 0xffffffff,
              owner = 0xffffffff
            }
          }
        },
        wait_list = {
          next = 0xe6b16098,
          prev = 0xe6b16098
        },
        owner = 0x0,
        magic = 0xe6b16084
      },
      bus = 0xc17526fc <cpu_subsys>,
      driver = 0x0,
      platform_data = 0x0,
      driver_data = 0x0,
      power = {
        power_state = {
          event = 0x0
        },
        can_wakeup = 0x0,
        async_suspend = 0x0,
        is_prepared = 0x0,
        is_suspended = 0x0,
        is_noirq_suspended = 0x0,
        is_late_suspended = 0x0,
        early_init = 0x1,
        direct_complete = 0x0,
        lock = {
          {
            rlock = {
              raw_lock = {
                {
                  slock = 0x20002,
                  tickets = {
                    owner = 0x2,
                    next = 0x2
                  }
                }
              },
              magic = 0xdead4ead,
              owner_cpu = 0xffffffff,
              owner = 0xffffffff
            }
          }
        },
        entry = {
          next = 0xe6b290d0,
          prev = 0xe57a7280
        },
        completion = {
          done = 0x7fffffff,
          wait = {
            lock = {
              {
                rlock = {
                  raw_lock = {
                    {
                      slock = 0x10001,
                      tickets = {
                        owner = 0x1,
                        next = 0x1
                      }
                    }
                  },
                  magic = 0xdead4ead,
                  owner_cpu = 0xffffffff,
                  owner = 0xffffffff
                }
              }
            },
            task_list = {
              next = 0xe6b160ec,
              prev = 0xe6b160ec
            }
          }
        },
        wakeup = 0x0,
        wakeup_path = 0x0,
        syscore = 0x0,
        no_pm_callbacks = 0x1,
        suspend_timer = {
          entry = {
            next = 0x0,
            pprev = 0x0
          },
          expires = 0x0,
          function = 0xc055b8a0 <pm_suspend_timer_fn>,
          data = 0xe6b16050,
          flags = 0x4
        },
        timer_expires = 0x0,
        work = {
          data = {
            counter = 0xffffffe0
          },
          entry = {
            next = 0xe6b1611c,
            prev = 0xe6b1611c
          },
          func = 0xc055b914 <pm_runtime_work>
        },
        wait_queue = {
          lock = {
            {
              rlock = {
                raw_lock = {
                  {
                    slock = 0x0,
                    tickets = {
                      owner = 0x0,
                      next = 0x0
                    }
                  }
                },
                magic = 0xdead4ead,
                owner_cpu = 0xffffffff,
                owner = 0xffffffff
              }
            }
          },
          task_list = {
            next = 0xe6b16138,
            prev = 0xe6b16138
          }
        },
        wakeirq = 0x0,
        usage_count = {
          counter = 0x0
        },
        child_count = {
          counter = 0x0
        },
        disable_depth = 0x1,
        idle_notification = 0x0,
        request_pending = 0x0,
        deferred_resume = 0x0,
        run_wake = 0x0,
        runtime_auto = 0x1,
        ignore_children = 0x0,
        no_callbacks = 0x0,
        irq_safe = 0x0,
        use_autosuspend = 0x0,
        timer_autosuspends = 0x0,
        memalloc_noio = 0x0,
        request = RPM_REQ_NONE,
        runtime_status = RPM_SUSPENDED,
        runtime_error = 0x0,
        autosuspend_delay = 0x0,
        last_busy = 0x0,
        active_jiffies = 0x0,
        suspended_jiffies = 0x0,
        accounting_timestamp = 0xfffea0e4,
        subsys_data = 0x0,
        set_latency_tolerance = 0x0,
        qos = 0x0
      },
      pm_domain = 0x0,
      pins = 0x0,
      dma_mask = 0x0,
      coherent_dma_mask = 0x0,
      dma_pfn_offset = 0x0,
      dma_parms = 0x0,
      dma_pools = {
        next = 0xe6b16198,
        prev = 0xe6b16198
      },
      dma_mem = 0x0,
      archdata = {
        dma_ops = 0x0,
        dma_coherent = 0x0
      },
      of_node = 0xe6bb34e4,
      fwnode = 0x0,
      devt = 0x0,
      id = 0x0,
      devres_lock = {
        {
          rlock = {
            raw_lock = {
              {
                slock = 0x0,
                tickets = {
                  owner = 0x0,
                  next = 0x0
                }
              }
            },
            magic = 0xdead4ead,
            owner_cpu = 0xffffffff,
            owner = 0xffffffff
          }
        }
      },
      devres_head = {
        next = 0xe6b161cc,
        prev = 0xe6b161cc
      },
      knode_class = {
        n_klist = 0x0,
        n_node = {
          next = 0x0,
          prev = 0x0
        },
        n_ref = {
          refcount = {
            counter = 0x0
          }
        }
      },
      class = 0x0,
      groups = 0xc1f0b0e8 <common_cpu_attr_groups>,
      release = 0xc0552c08 <cpu_device_release>,
      iommu_group = 0x0,
      iommu_fwspec = 0x0,
      offline_disabled = 0x1,
      offline = 0x0
    }
  },
  cpuid = 0x410fd034,
  loops_per_jiffy = 0xd177
}

c_show() 함수 콜스택 확인하기 
함수 분석이 끝나면 콜스택을 확인할 필요가 있습니다. 함수 구현부만 보면 시야가 좁아 질 수 있기 때문입니다.

c_show() 함수 콭스택은 다음과 같습니다.
<...>-16076 [002] ...1  4396.616120: seq_printf+0x44/0xf0 <-c_show+0x54/0x418
<...>-16076 [002] ...1  4396.616123: <stack trace>
 => ftrace_graph_call+0x0/0xc
 => seq_printf+0x48/0xf0
 => c_show+0x54/0x418
 => seq_read+0x198/0x478
 => proc_reg_read+0x8c/0xe8
 => __vfs_read+0x54/0x158
 => vfs_read+0xa4/0x140
 => __arm64_sys_read+0x5c/0xc0
 => el0_svc_common+0xa0/0x110
 => el0_svc_handler+0x7c/0x98
 => el0_svc+0x8/0xc

유저 공간에서 read 시스템 콜을 실행해 proc_reg_read() 함수를 통해 c_show() 함수가 호출됩니다. 

정리
이번 시간에 코드를 분석한 내용을 정리해보겠습니다.

   " 첫째, /proc/cpuinfo 파일은 어느 함수에서 생성할까?"

'proc/cpuinfo' 파일은 proc_cpuinfo_init() 함수에서 03번째 줄을 실행할 때 생성됩니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/proc/cpuinfo.c]
01 static int __init proc_cpuinfo_init(void)
02 {
03    proc_create("cpuinfo", 0, NULL, &proc_cpuinfo_operations);
04    return 0;
05 }
06 fs_initcall(proc_cpuinfo_init);

   " 둘째, /proc/cpuinfo 파일을 읽을 때 어느 함수가 호출될까?"

arch/arm64/kernel/cpuinfo.c 파일 c_show() 함수가 호출됩니다. c_show() 함수 위치는 CPU 아키텍처마다 다릅니다.

   " 셋째, /proc/cpuinfo 파일에서 출력하는 자료구조는 무엇인가?"

percpu 타입 변수 cpu_data입니다. 


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."


[리눅스커널] 워크큐: 워크큐 주요 개념 알아보기 8장. 워크큐

이번 소절에서는 워크큐를 이루는 주요 개념을 소개합니다.
 -   워크
 -   워커스레드
 -   워커풀
 -   풀워크큐

먼저 워크큐의 기본 실행 단위인 워크를 배워볼까요?

워크란
워크는 워크큐를 실행하는 단위입니다. 워크는 누가 실행할까요? 워크는 실행하는 주인공은 워커 스레드입니다. 인터럽트 후반부 처리나 지연해야 할 작업을 워크에서 실행하는 것입니다. 


리눅스 커널에서는 워크를 work라고도 부릅니다. 이 책에서는 편의상 워크라고 명시하겠습니다. 

다음 그림을 보면서 워크에 대해 배워볼까요?

[그림 1] 워크 실행 흐름도

워크의 처리 흐름은 위 그림과 같이 3단계로 분류할 수 있습니다.

1단계부터 알아볼까요? 그림에서 ①으로 표시된 부분입니다. 
워크를 실행하려면 먼저 워크를 워크큐에 큐잉해야 합니다. 이를 위해 schedule_work() 함수를 호출해야 합니다.

이어서 그림에서 ②로 표시된 부분을 눈으로 따라가 볼까요? 2단계 동작입니다. wake_up_worker() 함수를 호출해 워크를 실행할 워커 스레드를 깨웁니다.  

마지막 3단계 동작을 알아볼까요? 그림 가장 오른쪽 부분입니다. 워커 스레드에서 워크를 실행합니다. 이렇게 워크 후반부 처리는 워크에서 지정한 워크 핸들러가 담당합니다.

이번엔 다른 그림을 보면서 워크에 대해 알아봅시다.

[그림 2] 인터럽트 후반부를 처리하기 위해 워크를 실행하는 흐름도 

위 그림은 인터럽트 후반부를 워크에서 처리하는 흐름도입니다.

그림에서 ①으로 표시된 부분을 보면 인터럽트 핸들러인 bcm2835_mbox_irq() 함수에서 워크를 워크큐에 큐잉합니다. 인터럽트를 핸들링 할 때 빨리 처리해야 할 코드는 bcm2835_mbox_irq() 함수에서 처리하고 인터럽트 후반부는 워크가 실행하는 것입니다. 

다음 ②번으로 표시된 부분을 볼까요?
워크를 큐잉하고 난 다음 wake_up_worker() 함수를 호출해 워커 스레드를 깨웁니다.
스케줄러는 이미 런큐에 큐잉된 다른 프로세스와 우선 순위를 체크한 다음 워커 스레드를 실행시킵니다.

마지막 ③ 단계입니다. 워커 스레드가 깨어나면 스레드 핸들인 worker_thread() 함수가 일을 시작합니다. 화살표 방향으로 함수가 실행되며 process_one_work() 함수에서 워크 핸들러인 bcm2835_mbox_work_callback() 함수를 호출합니다.

인터럽트 핸들링 관점으로 인터럽트 후반부를 처리하는 코드를 2단계로 나눌 수 있습니다. 빠르게 실행해야 할 코드는 인터럽트 핸들러에서 처리하고 인터럽트 후반부 처리 코드는 워크 핸들러에서 실행하는 것입니다. 즉, 인터럽트 핸들러에서 처리하지 못한 동작을 bcm2835_mbox_work_callback() 함수에서 실행하는 것입니다.

이렇게 워크는 워커 스레드를 프로세스 컨택스트에서 실행하므로 워크 핸들러 실행 도중 휴면에 진입할 수 있습니다. 그래서 커널에서 지원하는 모든 함수를 다 쓸 수 있습니다.

우선 워크를 다음과 같이 정리해볼까요?

     “커널 후반부를 처리하는 단위이며 워크 핸들러 실행 도중 휴면에 진입할 수 있다.”
     “워크는 워커 스레드가 실행한다.”

워크에 대해 소개했으니 이어서 워크를 실행하는 워커 스레드를 알아볼까요?

워커 스레드란
워커 스레드는 워크를 실행하고 워크큐 관련 자료구조를 업데이트하는 작업을 수행하는 커널 스레드입니다. 즉, 워크큐 전용 프로세스입니다. 워커 스레드 세부 동작을 보려면 어떤 함수를 봐야 할까요? 스레드 핸들인 worker_thread() 함수를 봐야 합니다. 참고로 커널 스레드의 세부 동작은 스레드 핸들 함수에 구현돼 있습니다. 

그러면 라즈베리파이에서 워커 스레드를 확인해볼까요? 터미널을 열고 "ps -ely | grep kworker" 명령어를 입력해봅시다. 
root@raspberrypi:/# ps -ely | grep kworker
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
I     0     4     2  0  60 -20     0     0 worker ?        00:00:00 kworker/0:0H
I     0    16     2  0  60 -20     0     0 worker ?        00:00:00 kworker/1:0H
I     0    21     2  0  60 -20     0     0 worker ?        00:00:00 kworker/2:0H
I     0    26     2  0  60 -20     0     0 worker ?        00:00:00 kworker/3:0H
I     0    29     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:1
I     0    30     2  0  80   0     0     0 worker ?        00:00:00 kworker/1:1

워커 스레드 이름은 “kworker/” 으로 시작하며 뒷 쪽에 숫자가 붙습니다. 예를 들어 위에서 소개한 가장 마지막 워커 스레드 이름은 “kworker/1:1”입니다. 뒤에 1:1 이란 숫자 보입니다.

워커 스레드 이름은 워커 스레드를 생성하는 create_worker() 함수를 실행할 때 정해집니다.

그러면 워커 스레드에 대해서 다음 내용으로 소개를 마쳐 보겠습니다. 

     “워커 스레드는 워크를 실행하는 임무를 수행하는 커널 스레드다.”
     “워커 스레드 핸들 함수는 worker_thread() 함수이다.”

워커풀이란
워크를 실행하려면 먼저 워크큐에 큐잉을 해야 하고 워크를 워커 스레드가 실행한다고 설명을 드렸습니다. 워커풀은 워크와 워커 스레드를 관리하는 역할을 수행하며 워크큐의 핵심 자료 구조 중 하나입니다.

워커풀 자료구조는 struct worker_pool 구조체이며 선언부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]
01 struct worker_pool { 
02 spinlock_t lock; 
03 int cpu; 
...
04 struct list_head worklist; 
05 int nr_workers;
...
06 struct list_head workers;

04번째 줄에 보이는 worklist 필드로 큐잉한 워크 리스트를 관리합니다. 다음 06번째 줄 workers 필드로 워커 스레드를 관리합니다.

다음 워크큐 전체 자료구조 그림을 보면서 워커풀에 대해 알아볼까요? 
[그림 3] 워크큐 전체 자료구조에서 워커풀의 위치

위 그림 그림 가장 왼쪽 윗 부분에 표시한 struct workqueue_struct 구조체는 워크큐 전체를 제어하는 자료구조입니다. struct workqueue_struct 구조체 필드 가장 아랫 부분을 보면 cpu_pwqs 필드가 보입니다. 필드 왼쪽에 __percpu 키워드로 선언했으니 percpu 타입 변수란 사실을 알 수 있습니다. CPU 갯수만큼 struct pool_workqueue 구조체 공간이 있는 것입니다. 

①에 표시된 per_cpu_ptr() 함수는 percpu 오프셋을 알려줍니다. 이 오프셋으로 CPU 별로 할당된 메모리 주소를 찾을 수 있습니다. ① per_cpu_ptr() 아랫 부분을 보면 4개 struct pool_workqueue 구조체를 볼 수 있습니다. percpu 타입 변수이니 CPU 갯수만큼 메모리 공간이 있는 것입니다.

4개 struct pool_workqueue 구조체 박스 가장 아랫 부분을 보면 struct worker_pool 타입인 pool필드가 보입니다. ② 으로 표시된 부분을 보면 struct pool_workqueue 구조체 필드를 볼 수 있습니다.

③ 으로 표시된 worklist는 워크큐에 큐잉한 워크 리스트를 관리하는 필드이며 ④로 표시된 workers 필드는 워커 스레드를 관리하는 필드입니다.

정리하면 워커풀은 워크와 워커 스레드를 총괄하는 중요한 역할을 수행합니다.

풀워크큐란 
[그림 3] 가장 왼쪽 윗부분에서 본 struct workqueue_struct 구조체 cpu_pwqs 필드를 풀워크큐라고 부릅니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */
...
struct rcu_head rcu;

/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; 
struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
struct pool_workqueue __rcu *numa_pwq_tbl[]; 
};

per-cpu 타입 변수라 CPU 갯수만큼 struct pool_workqueue 구조체가 있습니다.


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."

[리눅스커널] 시간관리: 커널 타이머 관리 주요 개념 소개 7장. 타이머관리

커널 타이머는 리눅스 커널 핵심 기능 중 하나입니다. 커널 내부에서 배경 작업으로 다음 동작을 수행합니다. 
  - 커널 스케줄링 함수들은 커널 타이머를 써서 프로세스를 제어합니다.
  - 시스템 시간은 타이머 인터럽트를 받아 주기적으로 갱신됩니다.
  - Soft IRQ 타이머 서비스를 주기적으로 실행해 동적 타이머를 관리합니다.

또한 리눅스 커널 세부 알고리즘과 디바이스 드라이버는 실행 시간을 기준으로 세부 제어를 합니다. 예시를 들면 다음과 같습니다.
  - 어떤 함수를 현재 시각 기준으로 200ms 초 후에 실행
  - 어떤 함수가 500ms 내에 실행 안 하면 예외 처리
  - 특정 함수를 1초 주기로 실행해서 시스템 상태를 점검

리눅스 커널이나 드라이버 코드를 분석하면 커널 타이머와 시간 흐름을 제어하는 코드를 만날 확률이 높습니다.

이번 절에는 ‘커널 시간 관리’를 이루는 주요 개념을 소개합니다.
  - HZ와 jiffies
  - Soft IRQ 서비스
  - 커널 타이머와 동적 타이머

먼저 HZ 에 대해 배워볼까요?

HZ란 무엇인가
HZ는 진동수라고 부르며 다음과 같이 정의 내릴 수 있습니다. 

   " 1초에 지피스(jiffies)가 업데이트되는 횟수이다."

만약 HZ가 100이면 지피스는 1초에 100번 증감하고, HZ가 300이면 지피스는 1초에 300번 증감합니다.

HZ에 대한 이해를 돕기 위해 시계 초침을 예로 들겠습니다. 벽걸이 시계를 보면 1분에 60번 초침이 움직입니다. 그러면 1초 동안 100번 움직이는 초침이 있다고 상상해볼까요? 1초에 100번 움직이니 우리 눈으로 100번 초침이 바뀌는 것을 인지하지는 못할 것입니다. 리눅스 커널 관점으로는 HZ가 100이면 위에서 든 1초 동안 움직이는 100번 초침 횟수와 같다고 보면 됩니다. 

리눅스 커널 관점으로는 HZ는 1초 동안 움직이는 초침과 비슷한 개념입니다.  

Soft IRQ TIMER_SOFTIRQ 서비스
커널 타이머는 Soft IRQ 서비스로 등록되어 실행합니다. 따라서 커널 타이머 전체 실행 흐름을 파악하려면 Soft IRQ 구조를 알아야 합니다. Soft IRQ 타이머 서비스 실행 단계를 알아볼까요?  
1. 타이머 인터럽트가 발생하면 TIMER_SOFTIRQ이란 Soft IRQ 서비스를 요청합니다.
2. Soft IRQ 서비스 루틴에서 TIMER_SOFTIRQ 서비스 아이디 핸들러인 run_timer_softirq() 함수를 호출합니다. 
3. run_timer_softirq() 함수에서 time_bases이란 전역 변수에 등록된 동적 타이머를 처리합니다.

Soft IRQ 전체 흐름을 알아야 커널 타이머 동작원리를 이해할 수 있습니다. Soft IRQ란 용어가 익숙하지 않은 분은 Soft IRQ 장을 먼저 읽고 오시기 바랍니다.

커널 타이머와 동적 타이머란 무엇인가 
우리가 일상 생활에서 쓰는 알람과 커널 타이머는 개념 상 유사한 점이 많습니다. 다음과 같이 우문현답으로 커널 타이머와 동적 타이머가 무엇인지 풀어서 설명을 드리겠습니다.

누군가 여러분에게 다음과 같이 질문을 합니다.

   " 휴대폰으로 2시간 후 알람을 맞춰 놓으면 2시간 후에 어떤 동작을 할까요?"

이 질문에 어떻게 대답하시겠습니까? 아마 다음과 같이 대답을 할 것입니다.

   "  2시간 후에 알람이 울릴 것이다."

이제 필자가 여러분에게 질문을 드리겠습니다.

   " 2시간 후 실행하도록 동적 타이머를 등록하면 2시간 후에 어떤 동작을 할까요?"

조금 어렵게 들리는 질문이지만 필자라면 다음과 같이 대답을 할 것입니다.

   " 2시간 후에 동적 타이머가 실행될 것이다."

여기서 의문이 생깁니다. 과연 동적 타이머가 실행된다는 것은 어떤 의미일까?

   " 동적 타이머 핸들러가 호출된다."

이렇게 동적 타이머는 디바이스 드라이버 레벨에서 등록한 타이머를 의미합니다. 알람을 2시간 후에 맞춰 놓으면 알람이 울리듯 동적 타이머를 2시간 후로 지정해 등록하면 2시간 후에 동적 타이머가 실행하는 것입니다.

여러분이 알람을 맞춰 놓으면 내부에서 다음과 같은 동작을 할 것입니다.
   " 1초 간격으로 시간 흐름을 체크"
   " 1초 간격으로 알람이 등록됐는지 확인 후 알람이 확인되면 알람을 울림" 

마찬가지로 커널 타이머도 다음과 같은 일을 합니다.
  - jiffies 간격(1초에 100번)으로 시간 흐름을 체크"
  - jiffies 간격으로 동적 타이머가 만료됐는지 확인 후 동적 타이머를 실행"

커널 타이머는 배경으로 실행하면서 동적 타이머를 관리하고 실행하며 다음 동작을 수행합니다.
  - 커널 타이머는 전체 시스템 관점에서 타이머를 처리하는 함수 흐름과 자료 구조입니다. 
  - 커널 타이머는 Soft IRQ 서비스를 이용해서 커널 타이머 자료 구조를 관리하고 타이머를 호출한다. 

이번 챕터에서는 먼저 커널이 시간을 어떤 방식으로 관리하는지 먼저 살펴본 후 커널 타이머와 동적 타이머를 다룹니다. 

커널이 실행 시각을 관리하는 방식을 왜 잘 알아야 할까?
커널이 실행 시각을 관리하는 방식을 왜 잘 알아야 하는지 생각해봅시다.  

첫 번째, 코드를 읽는 능력을 더 키울 수 있습니다. 
리눅스 커널 세부 함수나 드라이버 코드에서 코드 실행 시간을 기준으로 흐름을 제어하는 경우가 많습니다. 커널 코드를 읽으면 타이머 관련 함수를 써서 실행 시간을 제어하는 루틴을 만날 가능성이 높습니다. 드라이버 코드를 읽다가 타이머 관련 함수를 만나면 바로 이해해야 드라이버 구조를 이해할 수 있습니다. 
두 번째, 커널 타이머 구조를 알면 안정적인 타이머 코드를 입력해 시간 흐름을 제어할 수 있습니다. 다른 드라이버 코드를 참고해 동적 타이머 코드는 누구나 할 수 있습니다. 하지만 커널 타이머 전체 실행 흐름을 아는 분은 커널 타이머가 Soft IRQ 서비스로 실행한다는 사실을 알기에 동적 타이머 핸들러를 간결하게 작성할 것입니다. 

세 번째, 문제 해결 능력을 키울 수 있습니다.  
타이머를 써서 실행 시간을 제어하는 코드를 작성한 후 문제가 생기면 ftrace로 커널 타이머 이벤트를 써서 타이머 세부 동작을 추적할 수 있습니다. ftrace로 타이머가 제대로 등록됐는지 동적 타이머 핸들러가 호출될 시점인지까지 알 수 있습니다. 커널 타이머에 대한 디버깅 스킬 향상으로 문제 해결 능력을 키울 수 있습니다.


리눅스 커널의 시간을 처리하는 기법은 매우 다양하고 난이도가 높습니다. SoC(System On Chip)에서 제공하는 틱 디바이스와 timekeeping 그리고 HRTimer(High Resolution Timer)에 대한 기법도 있습니다. 이 주제는 리눅스 커널 입문자가 익히기는 어렵기 때문에 커널 타이머 관리 방법만 다룹니다. 커널 타이머 관리하는 기법을 제대로 익히면 다른 타이머 기법도 쉽게 분석할 수 있습니다.

다음 절에서 jiffies 에 대해서 살펴보겠습니다.

"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."


[Crash-Utility][리눅스커널][디버깅] Radix Tree(라덱스-트리) 디버깅 하기(크래시 유틸리티) Linux Kernel - Core Analysis

리눅스 커널에서는 라덱스 트리로 핵심 자료구조를 관리합니다.
이번 시간에는 크래시 유틸리티로 라덱스 트리를 디버깅하는 방법을 소개합니다.

라덱스 트리를 보기 위한 명령어

크래시 유틸리티로 라덱스 트리 노드를 보기 위한 명령어 포멧은 다음과 같습니다.
tree -t radix -N (struct radix_tree_node *) 구조체 주소

struct radix_tree_node 구조체 시작 주소만 알면 됩니다.

struct radix_tree_node 구조체 주소가 0xFFFFFFFF3A806E79 인 경우 출력 결과는 다음과 같습니다.
crash64_kaslr> tree -t radix -N 0xFFFFFFFF3A806E79
ffffffff3f53c180
ffffffff3f53c4c0
ffffffff3f555180
ffffffff3f5554c0
ffffffff3f56e180
ffffffff3f56e4c0
ffffffff3f587180
ffffffff3f5874c0
ffffffff3f5a0180
ffffffff3f5a04c0
ffffffff3f5b9180
ffffffff3f5b94c0
ffffffff3f5d2180
ffffffff3f5d24c0
ffffffff3f5eb180
ffffffff3f5eb4c0
fffffffe4683d480
ffffffff3657a480

Case Study: worker_pool_idr 전역변수로 라덱스 트리 디버깅해보기 
워커풀은 라덱스 트리로 구성돼 있습니다.

#define for_each_pool(pool, pi)                     \
    idr_for_each_entry(&worker_pool_idr, pool, pi)          \
        if (({ assert_rcu_or_pool_mutex(); false; })) { }   \
        else


worker_pool_idr 전역변수 타입은 struct idr worker_pool_idr입니다.

crash64_kaslr> whatis worker_pool_idr
struct idr worker_pool_idr;
crash64_kaslr>

그런데 struct idr 구조체 idr_rt 필드 타입은 struct radix_tree_root입니다.
crash64_kaslr> struct idr
struct idr {
    struct radix_tree_root idr_rt;
    unsigned int idr_base;
    unsigned int idr_next;
}

다음 명령어로 worker_pool_idr.idr_rt 필드의 라덱스 트리 노드는 0xffffffff3a806e79임을 알 수 있습니다.

crash64_kaslr> p worker_pool_idr.idr_rt.rnode
$2 = (struct radix_tree_node *) 0xffffffff3a806e79


다음 명령어를 입력하면 라덱스 트리 모든 노드를 볼 수 있습니다.
crash64_kaslr> tree -t radix -N 0xFFFFFFFF3A806E79
ffffffff3f53c180
ffffffff3f53c4c0
ffffffff3f555180
ffffffff3f5554c0
ffffffff3f56e180
ffffffff3f56e4c0
ffffffff3f587180
ffffffff3f5874c0
ffffffff3f5a0180
ffffffff3f5a04c0
ffffffff3f5b9180
ffffffff3f5b94c0
ffffffff3f5d2180
ffffffff3f5d24c0
ffffffff3f5eb180
ffffffff3f5eb4c0
fffffffe4683d480
ffffffff3657a480


위  주소 리스트는 struct worker_pool 구조체이며 다음과 같이 확인할 수 있습니다.

1: ffffffff3f53c4c0 struct worker_pool 디버깅 
[0]: watchdog_ts = 4295299595 // 워커 터치 시간 
[1]: 해당 워커풀에 큐잉한 워크 
[2]: 실행 중인 워커: 0xFFFFFFFEE11F5B80

  (struct worker_pool *) (struct worker_pool*)0xffffffff3f53c180 = 0xFFFFFFFF3F53C180 = end+0x68B4978180 -> (
    (spinlock_t) lock = ((struct raw_spinlock) rlock = ((arch_spinlock_t) raw_lock = ((atomic_t) val = ((int) counter = 0), (u8) locked = 0, (u8)
    (int) cpu = 0,
    (int) node = 0,
    (int) id = 0,
    (unsigned int) flags = 0,
    (long unsigned int) watchdog_ts = 4295299595, "//<<--[0]"
    (struct list_head) worklist = (
      (struct list_head *) next = 0xFFFFFF9689B2D098 = binder_deferred_work.entry -> (  "//<<--[1]"
        (struct list_head *) next = 0xFFFFFF9689A052C8 = delayed_fput_work.work.entry, "//<<--[1]"
        (struct list_head *) prev = 0xFFFFFFFF3F53C1A0 = end+0x68B49781A0),
      (struct list_head *) prev = 0xFFFFFF9689B65710 = nd_tbl.gc_work.work.entry), "//<<--[1]"
    (int) nr_workers = 3,
    (int) nr_idle = 2,
    (struct list_head) idle_list = ((struct list_head *) next = 0xFFFFFFFEE4BF0680 = end+0x685A02C680, (struct list_head *) prev = 0xFFFFFFFEE07B
    (struct timer_list) idle_timer = ((struct hlist_node) entry = ((struct hlist_node *) next = 0xDEAD000000000200 
    (struct timer_list) mayday_timer = ((struct hlist_node) entry = ((struct hlist_node *) next = 0xDEAD000000000200 
    (struct hlist_head [64]) busy_hash = (
      [0] = ((struct hlist_node *) first = 0x0 = ),
...
      [45] = ((struct hlist_node *) first = 0x0 = ),
      [46] = ((struct hlist_node *) first = 0xFFFFFFFEE11F5B80 = end+0x6856631B80), "//<<--[2]"
      [47] = ((struct hlist_node *) first = 0x0 = ),
 ...
      [63] = ((struct hlist_node *) first = 0x0 = )),
    (struct worker *) manager = 0x0 = ,
    (struct list_head) workers = ((struct list_head *) next = 0xFFFFFFFEE07B9BC8 = end+0x6855BF5BC8, (struct list_head *) prev = 0xFFFFFFFEE11F5B
    (struct completion *) detach_completion = 0x0 = ,
    (struct ida) worker_ida = ((struct radix_tree_root) ida_rt = ((spinlock_t) xa_lock = ((struct raw_spinlock) rlock = ((arch_spinlock_t) raw_lo
    (struct workqueue_attrs *) attrs = 0xFFFFFFFE46881C00 = end+0x67BBCBDC00,
    (struct hlist_node) hash_node = ((struct hlist_node *) next = 0x0 = , (struct hlist_node * *) pprev = 0x0 = ),
    (int) refcnt = 1,
    (atomic_t) nr_running = ((int) counter = 1),
    (struct callback_head) rcu = ((struct callback_head *) next = 0x0 = , (void (*)()) func = 0x0 = ))


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."





[리눅스커널] 커널동기화: 레이스 컨디션 커널 패치 소개 9장. 커널 동기화 소개

이번에는 리눅스 커널 커뮤니티에서 논의된 레이스 컨디션 관련 커널 패치를 소개합니다. 

패치 코드를 분석하면서 다음과 같은 내용을 알아보겠습니다.
  - 레이스 컨디션 발생 원인
  - 임계영역 구간
  - 패치 코드와 관련 소스 코드 분석

임베디드 리눅스 입문자분들은 정상급 ‘리눅스 커널 개발자’들이 어떻게 리눅스 커널을 개발하는지 궁금해합니다. 이 궁금증을 해소할 수 있는 지름길은 ‘리눅스 커널 메일링 리스트’를 보는 것입니다. ‘리눅스 커널 메일링 리스트’를 읽으면 커널 개발 도중 생긴 문제와 해결 방법에 대한 심도있는 분석을 확인할 수 있습니다.

먼저 패치 코드에서 소개된 다음 함수 흐름을 같이 볼까요? 
[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() 함수를 거의 동시에 실행합니다. CPU0과 CPU1 함수 실행 흐름 마지막 부분을 보면 다음과 같은 사실을 알 수 있습니다.

     CPU0과 CPU1이 tty_insert_flip_string() 함수와 
         tty_insert_flip_string_fixed_flag() 함수를 동시에 실행한다.

위 내용을 일반화해서 다음과 같이 설명을 드릴 수 있습니다.

   "서로 다른 CPU에서 실행 중인 프로세스가 같은 코드나 함수에 접근한다."

그러면 위와 같은 레이스 컨디션으로 리눅스 시스템은 어떻게 동작했을까요? 

   "커널 패닉으로 시스템 다운이 됐습니다."

커널 패닉이란 "리눅스 시스템에 심각한 오류가 생겨 시스템을 다운시키는 동작"을 의미합니다. 

여러분은 대부분 컴퓨터나 노트북을 윈도 운영체제로 쓰실 것입니다. 그런데 혹시 컴퓨터를 오래 쓰다가 블루 스크린을 본 적이 있나요? 밝은 파란색 배경 화면에 흰색으로 이상한 경고 문구가 떠 있습니다. 이 때 컴퓨터는 아무런 동작을 안합니다. 마찬가지로 리눅스 운영체제에서는 검은색 배경에 흰색 문구를 띄웁니다. 이를 보통 커널 패닉 혹은 커널 크래시라고도 부릅니다.

커널 패닉이 발생했을 때 커널 로그를 보겠습니다.
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

     
위 로그의 출처는 다음과 같습니다
[https://lore.kernel.org/lkml/CAARE==e6obTMLBeo3t2oJuwwtv3zfei7sUhREJwDcqUEGFPdAg@mail.gmail.com/]

     
로그 분석으로 커널 패닉은 레이스 컨디션으로 발생했다는 사실을 알 수 있습니다. 

   "CPU0와 CPU1가 동시에 memcpy() 함수를 실행했다."

커널 패닉이 발생한 이유를 알기 위해 커널 로그를 차례대로 분석해보겠습니다. 

16~17번째 줄 로그를 볼까요?
16 [<ffffffff812ffea7>] memcpy+0x37/0x50 mm/kasan/kasan.c:335
17 [<ffffffff817f19f5>] tty_insert_flip_string_fixed_flag+0xb5/0x130

먼저 함수 호출 방향은 17번째 줄에서 16번째 줄 방향이라는 사실을 기억합시다.

 tty_insert_flip_string_fixed_flag() 함수에서 memcpy() 함수를 호출한 흐름이 보입니다. 

다음 10~15번째 줄 로그를 보겠습니다. 
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

위 함수 흐름으로 다음과 같은 사실을 알 수 있습니다.

   "메모리 영역에 읽거나 쓸 때 메모리 상태를 점검하는 check_memory_region() 함수가 
     호출됐다. "

check_memory_region() 함수 내부에서 메모리 오염을 확인해서 커널 패닉을 유발한 것입니다.

이어서 커널 패닉의 원인이 되는 코드를 분석해볼까요?
[https://elixir.bootlin.com/linux/v4.14.43/source/drivers/tty/tty_buffer.c]
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() 함수에 접근했다."

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

그렇다면 레이스 문제에 대해 많이 논의하는 이유는 뭘까요? 그럴만한 이유가 있습니다.

   "레이스가 발생하면 리눅스 시스템은 시스템이 아주 느려지거나 대부분 커널 패닉으로 
     시스템 다운이 된다." 

여러분이 휴대폰이나 컴퓨터를 쓰고 있는데 갑자기 리부팅이 되면 기분이 어떨까요? 짜증이 날 것입니다. 제품 안정성 관점에서 심각한 문제로 볼 수 있습니다. 그래서 리눅스 커널이나 디바이스 드라이버 개발자 입장에서 커널 패닉은 빨리 해결해야 할 문제입니다. 그래서 레이스로 생기는 리눅스 커널 크래시를 해결하기 위해 지금도 수많은 리눅스 커널 개발자들은 머리를 싸매고 디버깅을 합니다.


이어서 레이스 컨디션으로 발생하는 커널 패닉을 해결하기 위한 패치 코드를 함께 봅시다.
--- 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 }

패치 코드를 보니 12~15번째 줄 코드 구간을 스핀락을 써서 보호합니다. 그것은 다음과 같은 이유 때문입니다.
2개의 CPU에서 실행 중인 프로세스가 tty_insert_flip_string() 함수에 동시에 접근한다.
12~15번째 줄 코드 구간이 임계영역이다.

12번째 줄 코드에서 스핀락을 걸어주면 CPU0에서 실행 중인 프로세스가 12~15번째 줄 코드를 실행할 수 있습니다. 이 때 CPU1에서 다른 프로세스가 12~15번째 줄 코드를 실행하려고 시도하면 어떻게 동작할까요?

    "CPU0에서 실행 중인 프로세스가 16번째 줄 코드를 실행할 때까지 10번째 줄에서 
     스핀락을 기다린다."

정리하면 두 개의 CPU가 다음과 같이 12~15번째 줄 코드에 동시에 접근하므로 10번째와 16번째 코드에 스핀락(spin_lock)을 걸어준 것입니다.
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번째 줄 코드가 임계영역입니다. 이 코드 구간은 한 개의 프로세스만 접근해 실행해야 합니다. 

"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."


[리눅스커널] 커널동기화: 레이스 컨디션은 왜 발생할까? 9장. 커널 동기화 소개

이전 소절에서 레이스 컨디션과 임계 영역의 개념에 대해서 소개했습니다. 이 내용을 읽으니 자연스럽게 다음과 같은 의문이 생깁니다.

   " 레이스 컨디션과 동시성은 왜 발생할까?"

이번 시간에는 그 이유에 대해 살펴보겠습니다.

SMP(symmetric multiprocessing)
레이스 컨디션이 발생하는 첫 번째 이유는 리눅스 시스템에서 SMP(symmetric multiprocessing)를 적용하기 때문입니다. 여기서 SMP란 무엇일까요?

   "하나의 시스템에 다수의 CPU가 한 개의 메모리를 쓰는 컴퓨터 시스템 아키텍처이다."

이렇게 SMP에 대한 정의를 읽고 나면 SMP를 적용한 후 레이스 컨디션이 왜 발생하는지 잘 이해가 가지 않습니다. 소프트웨어적 보면 SMP 시스템에서는 다음과 같이 실행을 한다고 볼 수 있습니다.

   "다수의 CPU에서 병렬로 프로세스가 실행을 한다.

만약 리눅스 시스템에 CPU 4개를 탑재한 SMP 시스템에서는 4개의 CPU에서 병렬로 서로 다른 프로세스들이 실행할 수 있는 것입니다. 이는 다음과 같은 상황을 초래합니다.

   "서로 다른 CPU에서 실행 중인 프로세스가 같은 코드나 함수에 접근할 수 있다."

이런 현상을 동시성(Concurrency)이라고 말합니다. 달리 보면 SMP 시스템은 동시성이 발생할 수 밖에 없는 조건입니다.

현재 대부분 리눅스 시스템은 SMP 환경에서 구동합니다. 여러분이 쓰고 있는 휴대폰을 포함해 대부분 리눅스는 SMP 시스템에서 실행합니다. 라즈베리파이도 쿼드코어 CPU(4개 CPU)를 사용합니다. 

선점 스케줄링 
레이스가 발생하는 두 번째 요인은 리눅스 커널이 선점 스케줄링을 지원하기 때문입니다. 현재 대부분 리눅스 커널은 선점형 스케줄링 환경에서 실행되며 이를 다음과 같이 말할 수 있습니다.

   "우리가 입력한 코드 블락이 실행되는 도중 선점 스케줄링될 수 있다. "

이 내용을 읽고 나면 자연히 다음과 같은 의문이 생깁니다.

   "선점 스케줄링으로 레이스 컨디션이 발생할까? " 

선점 스케줄링으로 다양한 레이스가 발생할 수 있습니다. 이해를 위해 한 가지 예를 들어보겠습니다. 여러분이 작성한 코드가 rpi_set_synchronize() 함수에서 실행 중이라고 가정하겠습니다. A 프로세스가 rpi_set_synchronize() 함수 실행 도중 선점 스케줄링이 된 후 B 프로세스가 다시 rpi_set_synchronize() 함수를 호출하면 어떻게 될까요? 중요한 자료 구조를 처리하는 코드 구간을 B 프로세스가 다시 실행한다면 레이스가 발생할 수 있습니다.

선점 스케줄링이 발생해 다른 프로세스가 rpi_set_synchronize() 함수를 호출하지 않아도 다른 문제를 겪을 수 있습니다. 만약 rpi_set_synchronize() 함수 내에서 알고리즘 연산이나 정확한 딜레이를 주는 코드를 실행 중이라고 가정하겠습니다. 이 때 선점 스케줄링으로 실행하는 코드를 멈추고 다른 프로세스로 스케줄링되면 오동작을 유발할 수 있습니다.

이렇게 동시성이 발생하는 원인 중 하나가 선점 스케줄링입니다. 

인터럽트 발생 
어떤 CPU에서도 인터럽트는 언제든 발생할 수 있습니다. 여러분이 작성한 코드가 실행 도중 인터럽트가 발생하면 실행을 멈추고 인터럽트 핸들러를 호출할 수 있습니다. 만약 rpi_set_synchronize() 함수 실행 도중 인터럽트가 발생 해 다시 rpi_set_synchronize() 함수에 진입하면 어떻게 될까요? 이 상황도 체크해야 할 것입니다.

유저 프로세스에서 스레드 핸들링 
유저 어플리케이션에서 스레드를 생성해 한 개의 파일을 다수의 스레드가 접근할 수 있습니다.
이 과정에서 동시성 혹은 레이스 컨디션이 발생할 수 있습니다.

동시성 발생 사례 소개
임베디드 리눅스를 처음 접하는 분들은 동시성 문제가 왜 발생하는지 감이 잘 안옵니다. 이해를 돕기 위해 리눅스 커널 커뮤니티에서 SMP 환경에서 동시성 문제가 발생하는 사례를 소개하겠습니다.

먼저 첫 번째 사례를 소개합니다.
[https://lore.kernel.org/patchwork/patch/901442/]
01         CPU0                    CPU1
02         ----                    ----
03    lock(&mm->mmap_sem);
04                                 lock(bpf_event_mutex);
05                                 lock(&mm->mmap_sem);
06    lock(bpf_event_mutex);
07
08   *** DEADLOCK ***

03번째 줄을 보면 CPU0에서 &mm->mmap_sem으로 락을 걸고 있습니다.
락을 거는 구간에서 05번째 줄과 같이 CPU1이 &mm->mmap_sem으로 락을 다시 걸고 있습니다. 

이번에는 다른 사례입니다.
[https://lore.kernel.org/patchwork/patch/880120/]
01 CPU0                                    CPU1
02 mmap syscall                           ioctl syscall
03 -> mmap_sem (acquired)                 -> ashmem_ioctl
04 -> ashmem_mmap                            -> ashmem_mutex (acquired)
05    -> ashmem_mutex (try to acquire)       -> copy_from_user
06                                              -> mmap_sem (try to acquire)

03번째 줄에서 CPU0이 mmap_sem() 함수에 접근해 락을 획득했습니다.
그런데 거의 동시에 CPU1에서 06번째 줄과 같이 mmap_sem() 함수에 접근하고 있습니다.

두 가지 사례의 공통점은 다음과 같습니다. 

   "2개 이상 CPU에서 실행 중인 프로세스가 동시에 같은 코드 블락을 실행한다. "

여기까지 2개 CPU에서 실행 중인 프로세스가 같은 코드 블록에 동시에 접근하는 사례를 살펴봤습니다. 그런데 이 부분을 읽은 독자분들은 다음과 같이 불만을 토로할 수 있습니다.

   "문제의 원인과 해결 방법에 대해 조금 더 자세히 설명할 수는 없나?"

이번에는 2개 CPU가 같은 코드 블락을 실행할 수 있다는 사실에 초점을 맞춰 설명을 드렸습니다. 이어서 다음 소절에서 리눅스 커뮤니티에서 논의된 레이스 관련 패치를 분석하면서 레이스에 대해 더 알아보겠습니다.


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."




[리눅스커널] 시그널: 유저 공간에서 pause() 함수 호출 시 커널 실행 흐름 파악하기 12장. 시그널

유저 공간에서 pause() 함수를 호출하면 커널 공간에서 어떤 함수가 실행할까요? 다음 시그널 설정 흐름도에서 가장 하단에 표시된 박스를 확인합시다.
 
[그림 ] 유저 공간에서 pause() 함수 호출 시 실행 흐름도

pause() 함수는 시그널을 기다릴 때 호출합니다.

라즈베리파이에서 다음 명령어를 입력해 pause 명령어에 대한 매뉴얼을 확인합시다. 
root@raspberrypi:/home/pi# info pause
PAUSE(2)                  Linux Programmer's Manual                 PAUSE(2)
NAME         top
       pause - wait for signal

매뉴얼에서 출력하는 결과와 같이 시그널을 기다리는 역할을 수행합니다.

시스템 콜 발생으로 sys_pause() 함수 호출 과정
이번엔 pause() 함수를 유저 공간에서 실행했을 때 커널에서 실행 흐름을 다른 각도로 살펴봅시다.
 
[그림 ] 유저 공간에서 pause() 함수 호출 시 시스템 콜 발생 흐름도

위 그림과 같이 유저 공간에서 pause() 함수를 호출하면 해당 시스템 콜 핸들러인 sys_pause() 함수가 실행합니다. 유저 공간과 커널 공간별 처리 과정은 다음과 같습니다.

유저 공간
1. r7 레지스터에 sys_pause() 함수에 해당하는 시스템 콜 번호인 29를 지정
2. “svc 0” ARM 어셈블리 명령어로 시스템 콜 처리를 위해 소프트웨어 인터럽트를 발생해서 커널 공간 스위칭

커널 공간 
3. vector_swi 레이블 실행
4. 유저 공간에서 저장한 시스템 콜 번호인 29를 r7 레지스터에서 읽음
5. 시스템 콜 테이블인 sys_call_table 심볼에 접근해서 시스템 콜 핸들러인 sys_pause 시스템 콜 핸들러로 분기

각 단계별로 처리 과정을 살펴봤으니 이어서 소스 코드를 분석하겠습니다.

sys_pause() 함수 분석하기
pause() 함수에 대한 시스템 콜 핸들러는 sys_pause() 함수입니다. 

먼저 sys_pause() 함수 구현부를 살펴볼까요? 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/syscalls.h]
asmlinkage long sys_pause(void);

sys_pause() 함수 선언부를 확인하니 void형으로 인자를 받지 않는다는 사실을 알 수 있습니다.

이어서 sys_pause() 함수 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/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 }

3번째 줄 코드에서 while 문 조건을 먼저 점검합시다. signal_pending() 함수를 호출해서 false를 반환하면 4~5번째 줄 코드를 실행합니다. 그러면 signal_pending() 함수는 어떤 기능일까요?

   " 프로세스에 시그널이 전달됐으면 true, 아니면 false를 반환한다."

signal_pending() 함수의 의미를 알았으니 3~6번째 줄 코드는 다음과 같이 동작합니다.

   " 프로세스에 전달된 시그널이 없으면 프로세스를 TASK_INTERRUPTIBLE 상태로 바꾸고 
     schedule() 함수를 호출해서 휴면에 진입한다."

팬딩 시그널이란 프로세스에게 시그널이 전달되어 처리해야 할 시그널이 있는 상태를 의미합니다.

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

   "그러면 프로세스가 시그널을 받아서 다시 깨어나면 어떤 코드를 실행할까?" 

3번째 줄 코드를 실행합니다. 프로세스에게 시그널이 전달되면 signal_pending() 함수는 true를 반환하니 while 문을 종료한 후 7번 코드를 실행해서 –ERESTARTNOHAND를 반환합니다. 바로 sys_pause() 함수 실행을 종료한 후 ret_fast_syscall 레이블로 이동해 시그널을 받아 처리를 합니다.

이번 절에서 유저 공간에서 2개 함수 분석으로 다음 내용을 알게 됐습니다.
1. 유저 공간 sigaction() 함수 호출
  - 커널 공간에서 sys_rt_sigaction() 함수 호출
  - do_sigaction() 함수를 실행 해 시그널 설정 정보 저장
         ; 프로세스의 태스크 디스크립터 struct task_struct sighand->action[] 배열에  
        접근한 후 시그널을 설정 

2. 유저 공간 pause() 함수 호출
  - 커널 공간에서 sys_pause() 함수를 호출해 시그널을 받을 때까지 휴면에 진입

유저 공간에서 시그널 함수를 호출하면 리눅스 커널은 위와 같은 동작을 한다는 사실을 확인할 수 있습니다.


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."



고참 임베디드 개발자의 분노와 분통(영어 스트레스?) 임베디드 에세이

실제 내가 어떤 고참 임베디드 개발자와 나눈 대화다.

   "전 오랫동안 개발을 하고 싶은데 계속 관리를 하라고 합니다."
   "개발 업무를 더 하겠다고 해도 말이 안 통하더군요." 

   모르시는 분들이 없을 정도의 프로젝트 몇건 뛰어 봤지만 이쑤시개, 설거지였어요."

혹시 다른 회사를 알아보셨나요?

   개발직으로 PM 또는 책임 연구원으로 구인한다고해서 가보면 설거지 또는 울트라 슈퍼맨으로 개발을 원하면서 
   임금은 깍으려고 하고... 정말 힘드네요."

한국 SW 회사에서 고참 개발자에게 관리를 강요하고 월급을 깍는다고요?  그렇다면 외국계 SW 회사를 알아보세요.
외국계 SW 회사에서는 한국 회사보다 개발자로 더 오랫동안 일할 수 있습니다.

외국계 SW 회사를 알아보세요. 영어만 조금 준비하면 되요.

   아 제가 영어가 안돼서... " 

개발자가 쓰는 영어 표현은 어느 정도 한정돼 있습니다. '중학교' 수준 영어 표현만 익히면 외국계 회사에서 개발하는데 별 지장이 없습니다.
 
   아 제가 영어가 안돼서... " 

저도 영어가 꽤 딸립니다만, 한국에서 외국회사일을 원격근무로 하고 있습니다. 공부할 때 영문자료 보시는 정도에 조금만 더하시면 가능하지 싶습니다.

   아 제가 영어가 안돼서... 나이가 있어서요."

아예 외국으로 나오시는 것는 어떨까요? 독일에서는 65세까지 개발자로 일할 수 있답니다. 
직업에 대한 스트레스는 받지 않고 평생 개발자 하실 수 있습니다.

   아 제가 영어가 안돼서... 나이가 있어서요."

미국이면 경력 많으시면 꽤 좋을것 같은데 아쉽습니다. 
미국 SW회사에서 일하는 분께 물어보니 개발자가 어느 정도 연차가 되면 회사가 물어보더군요. '계속 TECH TREE로 갈건지 MANAGER 쪽으로 갈건지' 개발 하고 싶으시면 TECH으로 가시면 됩니다. 미국 취업을 생각해보세요.

   아 제가 영어가 안돼서... 나이가 있어서요."


그런데 이 분의 대답은 내 귀엔 다르게 들리네?

"아 제가 영어가 안돼서... 나이가 있어서요."
"아, 제가 '새로운 것을 배우는 것은' 안돼서요... 나이가 있어서요."



"이 포스팅을 읽고 혹시 다른 의견이 있으면 댓글 달아 주세요. 올려주시면 저도 댓글로 제 '생각'을 업데이트하려 합니다."




[리눅스커널] 워크큐: 워크 실행의 출발점인 worker_thread() 함수 분석 8장. 워크큐

워커 스레드가 깨어나면 스레드 핸들러인 worker_thread() 함수가 실행됩니다.
worker_thread() 함수는 워커 스레드를 종료하거나 생성하는 기능을 수행하지만 핵심 동작은 워크를 실행하는 것입니다.

이번 소절에서는 worker_thread() 함수에서 워크를 실행하는 동작에 초점을 맞춰 분석하겠습니다.

worker_thread() 함수 세부 동작은 다음 워커 스레드 절에서 상세히 다룹니다.

다음은 worker_thread() 함수에서 워크 실행에 관련된 코드 조각입니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
01 static int worker_thread(void *__worker)
02 {
03 struct worker *worker = __worker;
04 struct worker_pool *pool = worker->pool;
...
05 do {
06 struct work_struct *work =
07 list_first_entry(&pool->worklist,
08 struct work_struct, entry);
09
10 pool->watchdog_ts = jiffies;
12
13 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
14 /* optimization path, not strictly necessary */
15 process_one_work(worker, work);
16 if (unlikely(!list_empty(&worker->scheduled)))
17 process_scheduled_works(worker);
18 } else {
19 move_linked_works(work, &worker->scheduled, NULL);
20 process_scheduled_works(worker);
21 }
22 } while (keep_working(pool));

먼저 03 번째 줄 코드를 보겠습니다.
03 struct worker *worker = __worker;

워커 스래드 핸들러 함수 매개 인자인 __worker를 struct worker 구조체로 캐스팅해 worker에 저장합니다.

다음 04번째 줄 코드를 보겠습니다.
04 struct worker_pool *pool = worker->pool;

워커 자료 구조인 struct worker 구조체 pool 필드를 지역 변수인 pool에 저장합니다.
구조체 필드 이름과 지역 변수 이름이 같습니다. 코드를 읽다가 헷갈릴수 있으니 주의합시다. 

다음 05~22번째 줄 코드는 do~while 문입니다. 소스 코드 분석 전 do~while의 조건을 살펴보겠습니다. 22번째 줄 코드와 같이 keep_working() 함수가 true를 반환하면 do~while문을 계속 실행합니다.

keep_working() 함수를 보면 등록된 워크가 있으면 true 아니면 false를 반환합니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/workqueue.c]
static bool keep_working(struct worker_pool *pool)
{
return !list_empty(&pool->worklist) &&
atomic_read(&pool->nr_running) <= 1;
}

list_empty() 함수는 리눅스 커널에서 연결 리스트가 비어있는지 체크하는 기능입니다. 만약 연결 리스트가 비어 있으면 true을 반환합니다. 위 코드에서 list_empty() 함수 앞에 ! 기호가 있으니 연결 리스트가 비어 있지 않으면 true를 반환합니다.

위 코드를 워크큐 관점에서 해석하면 다음과 같습니다.

   "워커풀에 워크가 큐잉됐으면 do_while 문 내 소스 코드를 실행한다."

do~while 문 실행 조건을 봤으니 do~while 문 내 소스 코드를 보겠습니다.
06 struct work_struct *work =
07 list_first_entry(&pool->worklist,
08 struct work_struct, entry);

06~08번째 줄 코드는 워커풀 연결 리스트에서 워크 자료 구조인 struct work_struct 주소를 읽는 동작입니다. 워커풀 연결 리스트 필드인 worklist에 접근해 struct work_struct 구조체 주소를 work에 저장합니다.

이해를 위해 워크큐 관점에서 위 코드는 다음과 같이 해석할 수 있습니다. 

   "워커풀에 큐잉된 워크 연결 리스트를 가져와 워크 구조체를 알아낸다."

10 번째 줄 코드를 보겠습니다. 
10 pool->watchdog_ts = jiffies;

워커풀 시간 정보를 watchdog_ts 필드에 저장합니다. 

watchdog_ts 필드의 용도는 워크가 제대로 실행됐는지 점검하는 것입니다. CONFIG_WQ_WATCHDOG 컨피그가 켜져 있으면 watchdog_ts 필드에 워크를 큐잉한 마지막 시간 정보를 저장합니다. 만약 워크를 워크큐에 큐잉하고 난 후 30초 동안 워크를 실행하지 않으면 시스템에 문제가 있다고 보고 에러 로그를 출력합니다.

드디어 워크를 실행하는 process_one_work() 함수를 호출하는 단계까지 왔습니다.
13 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
14 /* optimization path, not strictly necessary */
15 process_one_work(worker, work);

13 번째 줄 조건문은 struct work_struct 구조체 data 플래그가 WORK_STRUCT_LINKED가 아닌지 점검합니다. 일반적인 상황에서 data 플래그가 WORK_STRUCT_LINKED가 아니므로 15 번째 줄 코드를 실행합니다.

워크 실행 관점으로 worker_thread() 함수 코드를 분석한 내용을 정리하면 다음과 같습니다.
- 워커풀에 워크가 큐잉됐는지 체크한다.
- 워커풀에 큐잉된 워크 연결 리스트를 가져와 워크 구조체를 알아낸다.
- process_one_work() 함수해 워크를 실행한다.

worker_thread() 함수는 워커풀에 등록된 워크를 모두 로딩해 process_one_work() 함수를 호출합니다. 이어서 워크 핸들러를 실행하는 process_one_work() 함수를 분석하겠습니다.

"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."



[리눅스] 유닉스와 리눅스역사 알아보기(Linux History) 1장. 리눅스 소개

이번 절에서는 먼저 리눅스의 역사에 대해 소개합니다. 이어서 리눅스가 우리 생활을 이루는 운영체제 중 하나로 발전하는 과정을 살펴보겠습니다.

리눅스의 역사를 알기 위해서는 유닉스가 탄생했는지 알 필요가 있습니다. 리눅스의 모태는 유닉스이기 때문입니다.

1.2.1 유닉스가 태어난 과정
컴퓨터가 발명된 후 한 동안 한 가지 프로그램만 실행했었습니다. 지금은 상상하기 어려운 이야기입니다. ‘CD 플레이어에서 CD를 플레이하는 프로그램만 실행’하는 경우를 예로 들 수 있습니다. 

시간이 흘러 컴퓨터는 1개 이상 프로그램을 돌릴 수 있는 수준으로 컴퓨터 계산 속도가 빨라지게 됩니다. 이 조건에서 컴퓨터를 연구하는 분들이 모여 뭔가 새로운 것을 만들어 보려는 시도를 하게 됩니다. 이 프로젝트 이름은 그 유명한 ‘멀틱스’ 운영체제입니다. 유닉스는 멀틱스라는 프로젝트로 시작됐습니다.

멀틱스 운영체제
1965년 MIT, AT&T 벨 연구소, General Electric 소속 개발자들이 모여 멀틱스(Multics)라는 운영체제 개발을 시작했습니다. 이 중에 켄톰슨과 데니스 리치와 같은 컴퓨터 역사를 바꾼 전설이 포함돼 있었습니다. 이들이 구현하고자 하는 운영체제의 핵심 기능은 다음과 같았습니다.

   " 멀티 태스킹을 지원하는 운영체제를 구현하자!"
지금 4차 산업혁명 시대를 맞이하는 시점에서 보면 멀티 태스킹은 운영체제의 기본 기능입니다. 하지만 1960년대 중반에는 놀라운 기능이었습니다. 당시 소프트웨어 기술 수준으로 한 개의 제품에 한 개의 소프트웨어만 돌리는 수준이었기 때문입니다. 

여러 가지 노력에도 불구하고 멀틱스 운영체제는 목표를 이루지 못하고 좌초되고 맙니다. 하지만 멀틱스 프로젝트에 투입됐던 켄 톰슨과 개발자들은 다음과 같은 목표를 이루기 위해 지속적으로 운영체제 연구에 몰두하게 됩니다.

   "멀티태스킹과 멀티 유저를 지원하는 운영체제를 개발하자."

이 과정에서 탄생한 것이 유닉스(UNIX)입니다. UNIX 운영체제는 어셈블리어를 사용해 프로그램을 작성했는데 PDP-7란 소형 컴퓨터에서만 돌아갔습니다. 호환성이 없다고 볼 수 있겠습니다. 다른 기종 CPU에서 유닉스를 쓰려면 해당 기종에 맞는 어셈블리어로 코드를 작성해 유닉스 운영체제를 구현해야 했습니다. 예를 들면 다음과 같은 상황을 겪게 됐습니다. 

     "드디어 유닉스 커널을 완성했다. 자 이제 구현된 어셈블리 코드를 다른 
        CPU 어셈블리 코드로 바꿔서 유닉스 커널을 개발해 볼까?" 

그 당시 개발자들은 다음과 같은 불평을 했을 것이라 생각합니다.

     "전체 유닉스 커널 어셈블리 코드를 다른 CPU에서 구동하는 어셈블리 명령어로 
       바꾸라고? 장난해?"

이렇게 유닉스를 완성한 시점에서 모든 유닉스 어셈블리 코드를 새로운 CPU에 맞게 모두 바꿔야 했습니다. 개발 속도는 느릴 수 밖에 없었습니다.

현재 시점으로 x86 어셈블리 명령어로 구현된 리눅스 전체 소스 코드를 ARMv8 명령어로 ‘모두’ 바꿔서 개발해야 한다고 가정해봅시다. 과연 이 프로젝트가 잘 진행될까요? 개발 속도가 느릴 것입니다.

1973년: 최초의 유닉스 완성 
유닉스의 호환성을 개선하고자 데니스 리치(Dennis Ritchie)는 아예 새로운 프로그램 언어를 개발했습니다. 이 프로그램 언어의 이름은 C언어입니다. 결과 모두 어셈블리 명령어로 구현됐던 유닉스를 C언어로 작성해 새로운 버전의 유닉스를 개발하게 됩니다. 유닉스 공통 기능은 C언어로 입력하고 해당 CPU에 의존적인 동작은 어셈블리어로 작성한 것입니다.

유닉스는 여러 기기에서 실행할 수 있는 호환성과 이식성을 갖추게 됐습니다. C 언어의 등장으로 유닉스 개발 속도가 붙게 되었고 이에 발 맞춰 대학과 연구기관에 소속된 개발자들이 유닉스 기능을 구현하게 됩니다. 시간이 흘러 유닉스는 상업 시장에 적용되는 운영체제로 발전하게 됐습니다.
여기까지 유닉스의 중반기 시절로 볼 수 있습니다. 그러면 유닉스가 빠른 속도로 완성도가 높아진 이유는 무엇일까요?

   "첫째, 유닉스는 오픈 소스로 소스 코드를 무료로 대학기관이나 연구소에 배포됐다."

처음 대학 기관에서 유닉스는 운영체제의 원리를 학습하고 연구하려는 목적으로 소스 코드를 분석했습니다. 하지만 소스 코드 분석에서 한 걸음 나아가 유닉스 코드 개선으로 유닉스 프로젝트에 기여하게 됐습니다.

   "둘째, C 언어 도입으로 이식성과 호환성을 높히게 됐다."
  
초창기 유닉스는 모든 소스를 어셈블리 명령어로 작성했습니다. 그래서 유닉스가 특정한 CPU에서만 작동할 수 있었습니다. 그런데 유닉스를 데니스 리치가 구현한 ‘C언어 코드’로 작성하면서 어셈블리 명령어에 비해 이식성과 호환성을 높일 수 있게 됐습니다.

유닉스는 발전을 거듭해 다양한 Berkeley Unix(BSD), SYSV 계열로 분화가 됐습니다. 

1984년: 유닉스 유로화에 대한 반발로 GNU 단체 설립
여러분에게 소개했다 싶히 유닉스는 AT&T 회사의 벨 연구소에서 개발했습니다. 그런데 AT&T란 회사가 전화기와 전자와 컴퓨터 사업까지 영역을 넓히다 보니 큰 규모의 기업이 됐습니다.

AT&T란 회사가 거대 기업이 되다 보니 모든 미국 졸업생은 AT&T로 몰려들게 되고 미국도 AT&T에 의존하게 됐습니다. 이런 상황에서 AT&T는 ‘반독점 소송’을 당했는데 패소하게 됩니다. 1984년에 이르러 미국 법원은 다음과 같은 결정을 내리게 됩니다. 

   "AT&T 란 거대 기업을 7개 회사로 나누자." 

그 7개 회사 중에 지금도 미국에서 유명한 버라이즌, 루슨트 등등이 있습니다. 이 과정에서 미국 법원에서 AT&T에게 다음과 같은 판결을 하게 됩니다.

   "AT&T는 컴퓨터 사업에 손 대지 말아라!"

미국 법원에서 이런 판결을 내리자 AT&T는 수 년 동안 유닉스를 개발하고도 이를 컴퓨터 제품으로 만들 수 있는 환경을 구축할 수 없게 됩니다. 그래서 유닉스를 연구할 필요가 없게 됐습니다.

그래서 AT&T는 유닉스를 돈을 받고 팔게 됩니다. 그런데 유닉스를 여러 회사에 팔다 보니 유닉스에 변종들이 생겨나게 됩니다. 그래서 ‘유닉스에 대한 표준을 맞춰 보자’라고 해서 POSIX라는 규격이 나오게 됩니다. 이후 여러 유닉스 벤더들은 POSIX 규약에 맞춰 호환성을 유지한 유닉스 제품을 개발하게 됩니다.

그런데 유닉스 개발에 참여했던 대학이나 연구기관은 더 이상 무료로 유닉스 소스 코드를 볼 수 없게 됐습니다. 오로지 유닉스를 AT&T에서 구매한 컴퓨터 업체 개발자들만 유닉스를 개발할 수 있었습니다. 그래서 기존 유닉스 개발에 참여했던 연구원들은 불만을 갖기 시작합니다.

이런 유닉스의 유료화에 반기를 들고 새로운 무료버전 유닉스를 만들어보자는 움직임이 일어납니다. 돈을 내고 유닉스를 만드는 것이 아니라 무료 버전 유닉스를 개발하자는 목표로 다음 단체를 설립합니다.

   "GNU(GNU is not UNIT): GNU는 유닉스가 아니다."

GNU라는 단체가 FSF(Free Software Foundation) 재단를 설립하면서 무료 유닉스 코드를 다시 작성하게 됩니다. GNU 단체는 리처드 스톨먼(Richard Stallman)을 중심으로 설립됐는데 시간이 흘러 수백만명이 참여하는 프로젝트로 커지게 됩니다.

그래서 유닉스 유틸리티와 같은 유닉스 기능을 다시 구현하게 됐는데 기존의 유닉스보다 더 완성도가 높은 코드를 작성하게 됩니다. 이렇게 유닉스 유틸리티를 하나 하나씩 구현하면서 유닉스를 이루는 소프트웨어를 만들기 시작합니다.

그런데 문제가 생겼습니다. 유닉스를 프로그램을 구조적 관점으로 보면 유닉스를 구동하는 핵심 동작인 유닉스 커널이 있습니다. 유닉스 커널 위에서 실행되는 유닉스 유틸리티가 있습니다. 유닉스 유틸리티는 여러 개발자들이 모여 어느 정도 완성도를 높혀서 개발을 했습니다.

   "유닉스 커널을 제대로 만들기 어렵다."

그래서 한 동안 유닉스 커널 개발이 정체되게 됩니다.  
  
1992년: AT&T BSD 유닉스 소송
그런데 GNU 에서만 무료 유닉스를 개발을 시도한 것은 아니었습니다. 기존 유닉스 개발 초반 유닉스 코드가 대학 연구소에 배포가 됐었습니다. 그 중 UC 버클리에 있는 BSD 개발자들이 기존 유닉스 코드를 재해석해서 1989년 후반 유닉스 커널을 포함한 무료 유닉스를 완성하게 됩니다.
  
그래서 BSD개발자는 기존 AT&T가 배포한 유닉스 코드를 재작성해서 'Net/1'란 이름으로 무료로 소스 코드를 배포하게 됩니다. 이후 1991년에는 코드 완성도를 높혀 'Net/2'를 배포합니다.

하지만 1992년 AT&T는 UC 버클리에 소송을 걸게 됩니다. 
(출처:https://en.wikipedia.org/wiki/UNIX_System_Laboratories,_Inc._v._Berkeley_Software_Design,_Inc.)

   "AT&T 내부 자료를 이용해 배포한 유닉스는 AT&T의 저작권을 침해한 것이다. 'Net/2'를 폐기해라!" 

1992년부터 시작된 AT&T와 BSD간 소송은 ‘Unix War’라고 부르기고 합니다. 소송은  1994년까지 진행됐는데 AT&T USL이 노벨에 팔린 후에 소송을 취하게 됩니다.

이 소송이 진행되는 동안 US 버클리에서 BSD유닉스 개발은 정체되고 맙니다. 소송에 걸려 있는데 BSD에서 유닉스를 개발할 수 없었겠지요.

1991년: 리누스 토발즈의 등장 
비슷한 시기에 무료 유닉스를 만들고자 하는 움직임이 있었습니다. 그 주인공은 리누스 토발즈입니다. 핀란드의 헬싱키라는 곳에서 리누스 토발즈라는 대학생이 GNU시스템에 적합한 커널을 개발했던 것입니다. 리눅스 토발스가 1991년 8월 25일 GNU 커널을 어느 정도 완성시킨 다음에 comp.os.minix에 포스팅을 합니다.
[https://www.cs.cmu.edu/~awb/linux.history.html]
From: torvalds@klaava.Helsinki.FI (Linus Benedict Torvalds)
  Newsgroups: comp.os.minix
  Subject: What would you like to see most in minix?
  Summary: small poll for my new operating system
  Message-ID:
  Date: 25 Aug 91 20:57:08 GMT
  Organization: University of Helsinki

  Hello everybody out there using minix -

  I'm doing a (free) operating system (just a hobby, won't be big and
  professional like gnu) for 386(486) AT clones.  This has been brewing
  since april, and is starting to get ready.  I'd like any feedback on
  things people like/dislike in minix, as my OS resembles it somewhat
  (same physical layout of the file-system (due to practical reasons)
  among other things).

  I've currently ported bash(1.08) and gcc(1.40), and things seem to work.
  This implies that I'll get something practical within a few months, and
  I'd like to know what features most people would want.  Any suggestions
  are welcome, but I won't promise I'll implement them :-)

메시지의 핵심은 다음과 같습니다.

   "자신은 GNU 무료 운영체제를 개발 중인데, 인텔 칩 i386에서 구동이 된다."

그런데 초기 버전 0.01 코드는 기본적인 커널 기능을 지원했으며 사실 실행이 되지 않은 단계였습니다. 얼마 후 리눅스 공식 버전인 0.02가 발표되었는데, bash(GNU Bourne Again Shell)와 gcc(GNU C 컴파일러)정도가 실행될 수 있는 수준이었습니다.

3월 리눅스는 0.95로 버전이 업그레이드 됐는데 다음 기능을 지원하게 됐습니다.
   " 인텔 x86칩에서 그래픽 사용자 인터페이스가 추가"
 
그 당시 스톨만과 FSF 단체는 GNU 커널로 Hurd를 개발 중이었습니다. 하지만 Hurd 개발이 진철되지 않은 상태에서 리누스 토발즈가 올린 유닉스 커널을 GNU 커널로 채택하기로 결정합니다. 리눅스는 강력한 GNU C 컴파일러인 gcc로 컴파일된  많은 응용프로그램들을 가지게 되었고, 둘의 결합으로 GNU시스템은 완전한 구조를 갖추게 되었습니다. 리눅스의 커널 부분은 리누스 주도 하에 계속 개발되었는데, 리누스는 최대한 확장 가능한, 즉 사용자에게 제어권이 있으며, 어떠한 인터페이스에도 종속되지 않도록 개발을 이끌고자 하는 의지가 있었습니다. 


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."



[리눅스커널] 시스템 콜: 시스템 콜의 특징

이번 소절에서는 시스템 콜의 특징을 알아보겠습니다.

앞서 알아봤듯이 시스템 콜은 유저 모드에서 커널 모드로 진입하는 관문입니다. 소프트웨어 구조 관점으로 보면 시스템 콜은 유저 공간과 커널 공간 사이 가상 계층이자 인터페이스라고 볼 수도 있습니다. 이 계층은 다음과 같은 특징이 있습니다.

1. 시스템 콜 계층으로 시스템 안정성과 보안을 지킬 수 있습니다. 
유저모드에서 어플리케이션이 커널 공간에 아무런 제약 없이 접근한다고 가정합시다. 실수로 어플리케이션이 커널 코드 영역 메모리를 오염을 시키면 시스템은 오동작할 가능성이 높습니다. 유저 모드에서 시스템 콜로만 커널 모드에 진입해서 제한된 메모리 공간에 접근하는 것입니다.

2. 유저 어플리케이션에서 추상화된 하드웨어 인터페이스를 제공합니다. 
유저 모드에서 구동 중인 어플리케이션 입장에서 하나의 파일 시스템 위에서 구동 중인 것으로 착각하게 합니다.

3. 시스템 콜 구현으로 유저 어플리케이션의 호환성과 이식성을 보장할 수 있습니다. 
리눅스 시스템은 시스템 콜 인터페이스는 POSIX(Portable Operating System Interface) 이란 유닉스 표준 규약에 맞게 구현되어 있기 때문입니다. 유저 어플리케이션 코드를 라즈베리파이, 안드로이드 등 리눅스 계열의 시스템과 유닉스 운영체제에서도 구동할 수 있습니다.

4. 유저 공간에서 실행하는 어플리케이션에서 커널 공간으로 진입하는 인터페이스를 두고 커널과 독립적으로 구동합니다. 유저 어플리케이션 입장에서 파일 시스템과 프로세스 생성과 같은 내부 동작에 신경 쓸 필요가 없습니다.

리눅스 디바이스 드라이버와 가상 파일 시스템 함수도 시스템 콜 핸들러를 통해 관련 코드를 실행합니다.  

또한 시스템 콜은 ARM 아키텍처와 연관이 깊은 동작입니다. ARM 프로세서는 시스템 콜을 익셉션의 한 종류인 소프트웨어 인터럽트로 실행하기 때문입니다. ARM 프로세스 관점으로 시스템 콜을 어떻게 구현했는지 같이 배워볼까요? 


"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."

[ARM] CPSR(Current Program Status Register) Register [Linux][ARM] Core Analysis

501 ARM Mode와 PSR..너희들은 누구냐?

뭘 알아야 이해를 하지

아래의 그림을 보면서 이야기 해 보죠. 아래는 하드웨어 디버거로 ARM9TDMI의 core 레지스터를 보여 주고 있답니다. 모두 32bit를 가지고 있어요. 막상 이 그림을 보면 레지스터 개수가 상당히 많이 있는 것으로 보이지만 실제로는 그렇지 않답니다. 같이 개수를 세어 보죠.   


 
ARM core는 6개의 모드로 나누어 진답니다.

USR, FIQ, SVC, IRQ, UND, ABT 모드가 있으며, SVC, IRQ, UND, ABT는 3개의 레지스터가 있답니다. FIQ는 8개가 있구요. USR는 R8~R14까지만 있는 것으로 보이겠지만 좀 더 자세히 살펴보시면 R0~R14까지 해서 15개가 있지요. 안 보이시는 분은 눈을 크게 뜨고 자세히 살펴보세요. ㅎㅎ 맞죠?? 보이죠 ㅋㅋ 그리고 마지막으로 PC, CPSR 있답니다. ② 에는 현재 6개의 모드 중에 어떤 모드가 사용 중이고, ①은 해당 모드에서 사용중인 레지스터 값을 보여 주는 거랍니다. R13, R14는 6개 모두 존재하고 있어요. 그 중에서 ②가 어떤 모드인지를 가리키게 되면, 각 모드에서 R13, R14 사용된답니다. 따라서 현재 ②에 SVC 모드라고 보여 주기 때문에 SVC 모드에 있는 R13_svc, R14_svc 레지스터 사용되었고, 레지스터의 값이 ①과 같게 보이는 거죠. 조금 헷갈릴 수도 있으니 정리 해보죠. 
  
 

 
 
이렇게 보니깐 쉽죠잉~~!! 그럼 개수를 세어 보았으니, 각 레지스터가 하는 역할에 대해 알아 보죠. 제일 먼저 PSR(Program Status Register)에 대해 살펴보죠. CPSR(Current PSR)과 SPSR(Saved PSR) 두 가지가 있답니다. CPSR은 condition flag, reserved, extension, control 구간으로 나뉘어요. 32bit로 구성된 레지스터는 각각의 역할들이 있답니다. 
  
 
 

Condition Flags

N

 연산 결과가 마이너스이면 '1'로 셋팅

Z

 연산 결과가 '0' 이면 '1'로 셋팅

C

 연산 결과가 32bit를 넘으면 '1'로 셋팅

V

 연산 결과가 32bit를 넘어 sign bit가 상실되면 '1'로 셋팅

 
N[31] 비트는 ALU 연산 결과에서 마이너스(-) 값이 나왔을 때 ALU bus로부터 PSR로 전달되어 N flag를 '1' 바뀐답니다. 



 

 
실제 소스 코드와 ARM core를 비교해 보죠. cmp 명령어가 있습니다. 이 명령어는 CPSR flags := Rn - Op2 실행을 하는 명령어랍니다. 즉, 마이너스 연산 후 CPSR에 flag bit를 반영한답니다. cmp 명령어가 실행되면 ALU에서 빼기 명령을 실행하고, CPSR에 Flag 값을 반영해요. 현재 결과가 마이너스라서 "N" flag 값은 '1' 되네요. 


 

R2가 0x5라면 어떻게 될까요? 연산 결과는 '0'이 라서 때문에 Z flag가 반영되고, 32bit 값을 넘겨서 C flag가 '1'로 셋팅 됐어요.



 

그럼 Overflow는 언제 발생할까요? cmp 명령어가 실행되면서 sign bit가 변경이 됐네요. sign bit가 변경이 되면 Overflow가 발생한답니다. 그리고 32bit 값을 넘겨서 C flag가 '1'로 셋팅 됐어요. 

 

 

Control

 인터럽트 Enable/Disable, Thumb 모드 그리고 CPU 수행 모드를 설정한답니다. 인터럽트는 IRQ와 FIQ 인터럽트를 Enable/Disable을 설정할 수 있어요. 예를 들면, 휴대폰으로 게임을 하고 있을 때 전화가 온다면 받게 할 것인가 말 것인가를 이 두 개의 bit를 가지고 결정하죠. IRQ, FIQ가 '1'로 되어 있으면 인터럽트가 발생해도 허용 안 한답니다. RTOS에서 A 태스크가 크리티컬 섹션 진입 전에 인터럽트를 disable을 하고, 크리티컬 섹션 코드 수행이 끝나면 다시 인터럽트를 enable을 할 때 사용되죠. CPSR에 있는 control bit를 조절하는 명령어는 MRS와 MSR 있어요. 소스 코드와 레지스터 값을 비교해 보아요.



0xC0값이 CPSR에 들어가면서 control bit 값이 바뀌게 되고, I와 F bit가 변경된답니다.

다음으로 "T" 비트에 대해 알아보죠. 32bit 명령어(ARM) 동작 중에 16bit 명령어(THUMB)로 전환이 필요하거나 16bit 명령어에서 32bit 명령어로 전환이 필요한 경우 사용되는 bit랍니다. bx 명령어를 사용하며, 명령어 뒤에는 어떤 레지스터를 사용 할 것인가를 지정해요. 이 레지스터 값에 들어 있는 데이타가 홀수이면 THUMB로 전환 하면서 T bit를 '1'로 셋팅하고, 짝수이면 ARM로 전환하면서 T bit를 '0'으로 셋팅한답니다.




bx r14에는 0x1169라는 홀수의 값이 있네요. 그럼 Thumb로 state 변환하면서 T비트를 '1'로 셋팅을 합니다. 그리고 0x1169라는 주소로 점프를 하는데 실제로는 1169-(1) 주소로 간답니다. 

마지막으로 MODE bit랍니다. MODE는 5bit로 구성되어 있네요. ARM core를 만들 때 5 bit의 값들을 이렇게 고정시켜 놓았답니다.
 

 

CPSR의 모드 bit 값이 0x10이면 USR, 0x11이면 FIQ, 0x12이면 IRQ, 0x13이라면 svc 모드이며, svc에 있는 ① R13, R14, SPSR 레지스터들이 동작하지요.

지금까지 CPSR를 살펴보았다면, 이제는 SPSR을 보아요. CPSR은 현재 Mode가 무엇인지 가리킨다면 SPSR는 Mode가 바뀔 때 이전모드를 기억하는 temp 와 같은 역할을 한답니다. 현재 어플리케이션이 Usr mode에서 동작하다가 인터럽트가 들어오면 Irq mdoe로 바꾸는 과정에서SPSR은 CPSR이 가지고 있던 usr mode를 백업①하고, CPSR은 usr에서 Irq로 바꾼②답니다. 그리고 Irq mode에서 인터럽트를 처리를 끝낸 후 다시 Usr mode로 복원하기 위해 CPSR은 SPSR에 있는 usr mode를 복원③해서 다시 어플리케이션을 실행하게 한답니다.


이제 ARM11의 CPSR에 대해 알아보죠. ARM9에 비해 좀 더 추가가 됐다는 것을 알 수가 있어요.


새롭게 추가가 된 bit는 Q[27], GE[19:16], E[9], A[8] 랍니다.



여기서 ③에 대해 먼저 잠시 알아보죠. ARM1176 프로세서 시리즈에 있는 이 기능은 TrustZone아라고 하고, OpenOS에서 동작하는 시스템에서 보안 기능을 강화시키는 목적으로 chip에서 보안 기능을 보장해준답니다. 보안 요소들이 Core에서 구현되어 있어서 모든 핵심 기술의 고유 기능을 보안하고 유지시킬 수가 있게 해 주죠. 이 기술을 적용하면 중요한 데이타와 DRM에 대한 보안을 제공해 준답니다. TrustZone은 두 개의 격리된 영역인 Secure area와 Non-secure area 가 있어요. 


 

ARM core에서는 MON mode가 동작한답니다. 그럼 Secure 모드를 진입하려면 어떻게 하는지 알아보죠. 우선 Co-processor 15번인 SCR 레지스터에 NS bit값이 '0' 이면 secure area로 된답니다. 




Co-processor 명령어를 이용하여 SCR 레지스터 값을 직접 변경 가능하답니다.

 MCR p15, 0, Rd, c1, c0, 0

 

Q[27] Flag

  연산결과가 '+'의 최대값(0x7FFFFFFF)과 '-'의 최소값(0x80000000)이 넘지 않게 하기 위해서 사용되고, 주로 분수 연산을 위해 새로운 명령어가 만들어졌답니다.

 QADD

 Add and Saturating

 QSUB

 Subtract and Saturating

 QDADD

 Add and Double Saturating

 QDSUB

 Subtract and Double Saturating

이러한 명령어가 사용되면 Q flag 값이 '1'로 된답니다. 일반적인 산술 연산의 예를 들어보죠.

 덧셈

 뺄셈

  mov r0, #0x7FFFFFFF 
  mov r1, #0x1
  add r3, r0, r1
  mov r0, #0x80000000
  mov r1, #1 
  sub r3, r0, r1

  r3 = 0x80000000, -1 

  (+ 최대값 넘어감)

  r3 = 0x7FFFFFFF, +1

  (-최소값 넘어감)

r3의 값이 r0의 값으로 그대로 유지하면서 연산을 하려면 QDADD와 같은 명령어를 사용하고 
0x7FFFFFFF +1은 0x7FFFFFFF 유지하고, Q bit가 '1'로 된답니다. 
0x80000000-1은 0x80000000 유지하고, Q bit가 '1'로 된답니다.

GE[19:16] bit

 CPSR의 [19:16] bit가 ② 그림의 GE 비트를 나타내죠. GE[3], GE[2], GE[1], GE[0]번까지 해서 총 4개가 있답니다. 하나의 명령어로 여러 개의 데이타를 동일한 연산 수행을 할 수 있는 명령어가 추가가 됐답니다. SADD, SSUB, QADD, QSUB, SHADD, SHSUB, SXT, SXTA, SSAT, SEL, PKH 명령어들이고, 일반적으로 SIMD(Single Instruction Multiple Data) instructions 이라고 부른답니다. 이 명령어는 다시 16bit냐 8bit냐에 따라 달라지며 또한 Signed과 Unsigned로 구분된답니다.

 Signed

 Unsigned

 SADD16
 SSUB16
 SADD8
 SSUB8
 UADD16
 USUB16
 UADD8
 USUB8

연산결과 R0 값을 16 bit씩 나누어 각각 (+ ) or (-) 이냐에 따라 GE bit가 '1'로 된답니다. [31:16] 까지는 GE2,3번 반영되며, [15:0]까지는 GE0,1번에 반영되죠.

 

E[9] bit

메모리에 있는 데이타를 읽어올 때, Big과 Little endian 두 가지 방식을 모두 지원해 준답니다. 개발자는 직접 endian을 바꿔가면서 프로그램을 할 수 있도록 해 주는데, SETEND 명령어 사용한답니다. 아래 그림의 ①은 Little endian으로 읽어 왔을 때 R0 값이고, ②는 Big endian으로 읽어 왔을 때 R0 값이며, ③와 같이 E bit가 '1'로 된답니다.

A[8] bit Imprecise Data Abort mask bit라고 불린답니다. A bit가 '1'이면 imprecise Data Abort는 Data Abort를 발생시키지 않고, '0'이면 발생시킨답니다.


출처

http://recipes.egloos.com/5618965


[리눅스커널][시그널] 시그널 생성: __send_signal() 커널 함수 분석 12장. 시그널

누군가 ‘시그널을 생성하는 핵심 함수가 무엇인가?’ 라고 질문을 한다면 __send_signal() 함수라고 대답할 수 있습니다. 그렇습니다. 시그널을 생성하는 핵심 함수는 __send_signal() 입니다. 

이제부터 __send_signal() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
int group, int from_ancestor_ns)
3 {
4 struct sigpending *pending;
5 struct sigqueue *q;
6 int override_rlimit;
7 int ret = 0, result;
8
9 assert_spin_locked(&t->sighand->siglock);
10
11 result = TRACE_SIGNAL_IGNORED;
12 if (!prepare_signal(sig, t,
13 from_ancestor_ns || (info == SEND_SIG_FORCED)))
14 goto ret;
15
16 pending = group ? &t->signal->shared_pending : &t->pending;
17
18 result = TRACE_SIGNAL_ALREADY_PENDING;
19 if (legacy_queue(pending, sig))
20 goto ret;
...
21
22 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
23 if (q) {
24 list_add_tail(&q->list, &pending->list);
25 switch ((unsigned long) info) {
26 case (unsigned long) SEND_SIG_NOINFO:
27    q->info.si_signo = sig;
28    q->info.si_errno = 0;
29    q->info.si_code = SI_USER;
30    q->info.si_pid = task_tgid_nr_ns(current,
31 task_active_pid_ns(t));
32    q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
33    break;
34 case (unsigned long) SEND_SIG_PRIV:
35    q->info.si_signo = sig;
36    q->info.si_errno = 0;
37    q->info.si_code = SI_KERNEL;
38    q->info.si_pid = 0;
39    q->info.si_uid = 0;
40 break;
41 default:
42    copy_siginfo(&q->info, info);
43    if (from_ancestor_ns)
44 q->info.si_pid = 0;
45 break;
46 }
47
48 userns_fixup_signal_uid(&q->info, t);
49
50 } else if (!is_si_special(info)) {
51 if (sig >= SIGRTMIN && info->si_code != SI_USER) {
52 result = TRACE_SIGNAL_OVERFLOW_FAIL;
53 ret = -EAGAIN;
54 goto ret;
55 } else {
56 result = TRACE_SIGNAL_LOSE_INFO;
57 }
58 }
59
60 out_set:
61 signalfd_notify(t, sig);
62 sigaddset(&pending->signal, sig);
63 complete_signal(sig, t, group);
64 ret:
65 trace_signal_generate(sig, info, t, group, result);
66 return ret;
67 }

먼저 __send_signal() 함수 분석에 앞서 이 함수가 어떤 일을 하는지 알아볼까요?
__send_signal() 함수 세부 처리 과정은 5단계로 분류할 수 있습니다.
1 단계:  시그널 예외 처리
2 단계: 시그널 펜딩 리스트에 시그널 정보를 저장한 후 시그널을 받을 프로세스 태스크 디스크립터에 써줌
3 단계: 시그널을 받을 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 flags에 _TIF_SIGPENDING 플래그를 써줌
4. 시그널을 받을 프로세스를 깨움
5. 시그널 ftrace 로그 출력

이어서 각 단계별로 세부 소스 코드를 분석해보겠습니다.

1 단계:  시그널 예외 처리 코드
12번째 줄 코드를 보겠습니다.
12 if (!prepare_signal(sig, t,
13 from_ancestor_ns || (info == SEND_SIG_FORCED)))
14 goto ret;

prepare_signal() 함수는 시그널을 생성하기 전 전처리 동작을 수행합니다. 시그널을 생성하기 위한 조건을 점검하는 것입니다. 시그널 생성 조건을 만족하지 않으면 ret 레이블을 실행해서 send_signal() 함수를 종료합니다.

prepare_signal() 함수는 시그널 종류에 따라 시그널 큐를 갱신하고 시그널을 처리 중인지 점검합니다. 또한, sig_ignored() 함수를 호출해서 시그널을 생성할 조건인지 확인합니다.

2단계: 시그널 펜딩 리스트를 태스크 디스크립터 시그널 필드에 써줌
16번째와 32번째 줄 코드를 보겠습니다.
16 pending = group ? &t->signal->shared_pending : &t->pending;
...
32 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
33 if (q) {
34 list_add_tail(&q->list, &pending->list);
35 switch ((unsigned long) info) {

16번째 줄 코드를 먼저 보겠습니다. 
시그널을 받을 프로세스가 스레드 그룹에 속해 있으면 &t->signal->shared_pending 필드를 pending 지역 변수에 저장합니다. 이외 조건에서 &t->pending 필드를 pending 변수에 저장합니다.

시그널을 전달 받은 프로세스 유형에 따라 펜딩 시그널을 서로 다른 필드에 저장하는 것입니다. 
  - 유저 프로세스
pending = &t->signal->shared_pending;

  - 커널 프로세스
pending = &t-> pending;

유저 레벨에서 생성된 프로세스는 &t->signal->shared_pending 필드 혹은 커널 스레드의 경우 &t->pending 필드에 펜딩 시그널에 저장돼 있습니다.

다음 34번째 줄 코드를 보겠습니다. struct sigqueue 구조체를 동적 할당 받은 다음 &pending->list 구조체에 추가합니다.

유저 프로세스가 시그널을 생성했을 때 시그널 정보를 저장하는 루틴입니다. struct sigqueue 구조체 info 필드에 시그널 정보를 저장합니다.
36 case (unsigned long) SEND_SIG_NOINFO:
37    q->info.si_signo = sig;
38    q->info.si_errno = 0;
39    q->info.si_code = SI_USER;
40    q->info.si_pid = task_tgid_nr_ns(current,
41 task_active_pid_ns(t));
42    q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
43    break;

만약 유저 어플리케이션에서 kill() 함수를 호출했거나 리눅스 터미널에서 “kill -9 [pid]” 포멧으로 명령어를 입력했을 경우 36~43번째 줄 코드가 실행합니다.

sig는 시그널 번호를 의미하는 정수값, SI_USER는 시그널 발생 소스가 유저 레벨 프로세스란 의미입니다. 

다음은 커널에서 시그널을 설정할 때 실행하는 루틴입니다.
44 case (unsigned long) SEND_SIG_PRIV:
45    q->info.si_signo = sig;
46    q->info.si_errno = 0;
47    q->info.si_code = SI_KERNEL;
48    q->info.si_pid = 0;
49    q->info.si_uid = 0;
50 break;

역시 sig는 시그널 정수 값을 의미하고 si_code 필드에 SI_KERNEL 플래그를 저장합니다. 커널 프로세스가 시그널을 생성했다는 정보입니다.

다음 70~73번째 줄 코드를 보겠습니다.
70 out_set:
71 signalfd_notify(t, sig);
72 sigaddset(&pending->signal, sig);
73 complete_signal(sig, t, group);

complete_signal() 함수를 호출해서 시그널을 받을 프로세스를 깨웁니다.

3단계: ftrace로 시그널 생성 완료를 출력
이어서 75번째 줄 코드를 보겠습니다.
74 ret:
75 trace_signal_generate(sig, info, t, group, result);
76 return ret;

75번째 줄 코드를 실행하면 ftrace 로그 출력합니다.
signal_generate 이벤트를 켰을 때 signal_generate ftrace 메시지를 출력합니다.


signal_generate ftrace 이벤트는 다음 명령어로 킬 수 있습니다.
"echo 1 > /sys/kernel/debug/tracing/events/signal/signal_generate/enable"

다음 로그는 signal_generate 이벤트를 출력할 때 메시지입니다.
kworker/u8:2-1208  [003] d...  3558.051261: signal_generate: sig=2 errno=0 code=128 comm=RPi_signal pid=1218 grp=1 res=0

시그널 번호가 2이니 SIGIN 시그널을 생성하며 시그널을 받을 프로세스가 RPi_signal 이라는 정보입니다.

이번 소절에서 소스 코드 분석으로 다음과 같은 내용을 알게 됐습니다.
1. 시그널을 받을 프로세스에게 시그널 정보를 써줌
     시그널을 받을 프로세스 struct task_struct 구조체 pending 필드에 펜딩 시그널 
     정보를 써줌
2. 시그널을 받을 프로세스를 깨움

이 동작이 시그널을 만드는 핵심입니다. 이렇게 시그널을 받을 프로세스의 태스크 디스크립터에 펜딩 시그널 정보를 써 준 다음 시그널을 받을 프로세스를 깨워줘야 합니다. 

complete_signal() 함수 분석
다음 시그널 생성 3 단계인 시그널을 받을 프로세스를 깨우는 함수 흐름을 살펴보겠습니다. 
먼저 complete_signal() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 static void complete_signal(int sig, struct task_struct *p, int group)
2 {
3 struct signal_struct *signal = p->signal;
...
4 if (sig_fatal(p, sig) &&
5     !(signal->flags & SIGNAL_GROUP_EXIT) &&
6     !sigismember(&t->real_blocked, sig) &&
7     (sig == SIGKILL || !p->ptrace)) {
8
9 if (!sig_kernel_coredump(sig)) {
10 signal->flags = SIGNAL_GROUP_EXIT;
11 signal->group_exit_code = sig;
12 signal->group_stop_count = 0;
13 t = p;
14 do {
15 task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
16 sigaddset(&t->pending.signal, SIGKILL);
17 signal_wake_up(t, 1);
18 } while_each_thread(p, t);
19 return;
20 }
21 }
22
23 signal_wake_up(t, sig == SIGKILL);
24 return;
25 }

complete_signal() 함수 주요 동작은 2단계로 분류할 수 있습니다.
  - 시그널을 받을 프로세스의 struct thread_info 구조체 flags 필드에 _TIF_SIGPENDING 플래그를 써주기
  - 시그널을 받을 프로세스를 깨우기

위 함수는 프로세스가 스레드 그룹에 속했을 경우와 단일 프로세스일 때 다른 조건으로 실행합니다.

4~21번째 줄 코드는 프로세스가 스레드 일 경우 스레드 그룹에 속한 모든 프로세스를 깨웁니다.
이외 조건에서 커널 스레드일 경우 23번째 줄 코드와 같이 signal_wake_up() 함수를 호출합니다.

시그널을 받을 프로세스의 struct thread_info 구조체 flags 필드에 _TIF_SIGPENDING 플래그를 써주기
이어서 signal_wake_up() 함수를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched/signal.h]
1 static inline void signal_wake_up(struct task_struct *t, bool resume)
2 {
3 signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0);
4 }

두 번째 인자에 따라 다른 인자로 signal_wake_up_state() 함수를 호출합니다.

다음 signal_wake_up_state() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 void signal_wake_up_state(struct task_struct *t, unsigned int state)
2 {
3 set_tsk_thread_flag(t, TIF_SIGPENDING);
4
5 if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
6 kick_process(t);
7 }

3번째 줄 코드에서는 set_tsk_thread_flag() 함수를 호출합니다. 

set_tsk_thread_flag() 함수를 볼까요?
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/sched.h]
static inline void set_tsk_thread_flag(struct task_struct *tsk, int flag)
{
set_ti_thread_flag(task_thread_info(tsk), flag);
}

프로세스 스택 최상단 주소에 있는 struct thread_info flags 필드를 _ TIF_SIGPENDING 플래그로 설정합니다. 시그널을 받을 프로세스에게 시그널이 전달됐다는 정보를 써주는 것입니다.

다음 5번째 줄 코드에서는 wake_up_state() 함수를 호출합니다.

시그널을 받을 프로세스를 깨우기
마지막으로 wake_up_state() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 int wake_up_state(struct task_struct *p, unsigned int state)
2 {
3 return try_to_wake_up(p, state, 0);
4 }

try_to_wake_up() 함수를 호출해서 시그널을 받을 프로세스를 깨웁니다.

이번 소절에서는 시그널을 생성하는 역할을 수행하는 __send_signal() 함수 코드를 분석했습니다. 코드 분석으로 알게 된 내용을 정리해볼까요?

1단계: 시그널을 받을 프로세스에게 시그널 정보를 써줌
  - struct task_struct: signal 혹은 signal->shared_pending 필드에 시그널 
정보를 저장
 - struct thread_info: _TIF_SIGPENDING 플래그 저장

2단계: 시그널을 받을 프로세스를 깨워줌 
  - signal_wake_up() 함수를 호출해 시그널을 받을 프로세스를 깨움 
여기까지 시그널을 생성하는 세부 동작을 알아봤습니다.

이어서 다음 절에서는 시그널을 프로세스가 받아 처리하는 과정을 살펴보겠습니다.

"혹시 궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."

[리눅스커널][시그널] T32: 시그널 생성 과정 디버깅해보기 12장. 시그널

이전 포스팅에서 커널이 '시그널'을 어떤 방식으로 처리하는지 살펴봤습니다.
이전에 배웠던 내용을 리뷰하는 차원으로 '시그널' 처리 과정을 정리해볼까요?

1. 시그널 생성
   시그널을 받은 프로세스의 태스크 디스크립터에 시그널 정보를 써주고 해당 프로세스를 깨운다. 
2. 시그널 전달 
   시그널을 받을 프로세스는 깨어나 시그널을 처리한다. 

시그널 처리 과정에 대해 소스 코드를 분석했는데 이번에는 __send_signal() 함수에서 시그널을 생성할 때 바뀌는 시그널 자료구조에 대해서 디버깅해보는 시간을 갖겠습니다. 사실 함수 실행 흐름과 자료구조는 '이와 잇몸'의 관계와 같습니다. 조금 더 구체적으로 말씀드리면 다음과 같겠네요.

     "함수는 자료구조를 읽고 바꾸기 위해 실행한다."
     "자료구조에 저장된 값으로 함수 흐름을 제어한다."

이제 본론으로 들어가서 디버깅하려는 __send_signal() 함수 소스 코드를 같이 볼까요?
01 static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
02 enum pid_type type, int from_ancestor_ns)
03 {
04 struct sigpending *pending;
05 struct sigqueue *q;
06 int override_rlimit;
07 int ret = 0, result;
...
08 pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
...
09 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
10 if (q) {
11 list_add_tail(&q->list, &pending->list);
12 switch ((unsigned long) info) {
13 case (unsigned long) SEND_SIG_NOINFO:
14 clear_siginfo(&q->info);
15 q->info.si_signo = sig;
16 q->info.si_errno = 0;
17 q->info.si_code = SI_USER;
18 q->info.si_pid = task_tgid_nr_ns(current,
19 task_active_pid_ns(t));
20 q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
21 break;
22 case (unsigned long) SEND_SIG_PRIV:
23 clear_siginfo(&q->info);
24 q->info.si_signo = sig;
25 q->info.si_errno = 0;
26 q->info.si_code = SI_KERNEL;
27 q->info.si_pid = 0;
28 q->info.si_uid = 0;
29 break;
30 default:
31 copy_siginfo(&q->info, info);
32 if (from_ancestor_ns)
33 q->info.si_pid = 0;
34 break;
35 }
...
36 out_set:
37 signalfd_notify(t, sig);
38 sigaddset(&pending->signal, sig);
...
39 complete_signal(sig, t, type);
40 ret:
41 trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result);
42 return ret;
43}

먼저 시그널을 받을 프로세스 태스크 디스크립터에 시그널 정보를 써주는 코드를 볼까요?
04 struct sigpending *pending;
05 struct sigqueue *q;
...
08 pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
...
09 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
10 if (q) {
11 list_add_tail(&q->list, &pending->list);

08번째 줄: 시그널을 받을 프로세스 태스크 디스크립터의 &t->signal->shared_pending 혹은 &t->pending 필드를 읽는다.
09~11번째: 시그널 정보가 담긴 q변수 연결 리스트를 &pending->list에 추가한다. 

이번에는 시그널을 받을 프로세스에게 시그널이 전달됐다는 정보를 써줄 때 함수 흐름을 소개합니다.
complete_signal()
signal_wake_up()
signal_wake_up_state()  
 
위 함수가 실행하면 다음 동작을 수행합니다.

     "시그널을 받을 프로세스 struct thread_info 구조체 flags 필드에 _TIF_PENDINGSIG 정보를 써준다." 
 
간단히 코드를 리뷰했으니 이제 시그널을 생성했을 때 바뀌는 자료구조를 분석해보겠습니다.
먼저 디버깅을 하기 전 전제 조건을 보겠습니다.
 - 시그널을 받을 프로세스 이름: "bash"
 - 시그널을 받을 프로세스 주소: 0xFFFFFFEFC7EECC00 
 - 전달하려는 시그널: SIGCHLD(17)
 
struct sigpending * 정보를 보겠습니다.
  (struct task_struct *) [-] (struct task_struct *)0xFFFFFFEFC7EECC00 = 0xFFFFFFEFC7EECC00 -> (
    (struct thread_info) [D:0xFFFFFFEFC7EECC00] thread_info = ((long unsigned int) [D:0xFFFFFFEFC7EE
    (long int) [D:0xFFFFFFEFC7EECC50] state = 0,
    (void *) [D:0xFFFFFFEFC7EECC58] stack = 0xFFFFFF80108E8000,
    (atomic_t) [D:0xFFFFFFEFC7EECC60] usage = ((int) [D:0xFFFFFFEFC7EECC60] counter = 2),
...
    (struct nsproxy *) [D:0xFFFFFFEFC7EED3D8] nsproxy = 0xFFFFFFAF890B30D0,
    (struct signal_struct *) [D:0xFFFFFFEFC7EED3E0] signal = 0xFFFFFFEFC4CF7300 -> (
      (atomic_t) [D:0xFFFFFFEFC4CF7300] sigcnt = ((int) [D:0xFFFFFFEFC4CF7300] counter = 1),
      (atomic_t) [D:0xFFFFFFEFC4CF7304] live = ((int) [D:0xFFFFFFEFC4CF7304] counter = 1),
      (int) [D:0xFFFFFFEFC4CF7308] nr_threads = 1,
      (struct list_head) [D:0xFFFFFFEFC4CF7310] thread_head = ((struct list_head *) [D:0xFFFFFFEFC4C
      (wait_queue_head_t) [D:0xFFFFFFEFC4CF7320] wait_chldexit = ((spinlock_t) [D:0xFFFFFFEFC4CF7320
      (struct task_struct *) [D:0xFFFFFFEFC4CF7368] curr_target = 0xFFFFFFEFC7EECC00,
      (struct sigpending) [D:0xFFFFFFEFC4CF7370] shared_pending = (
        (struct list_head) [D:0xFFFFFFEFC4CF7370] list = (
          (struct list_head *) [D:0xFFFFFFEFC4CF7370] next = 0xFFFFFFEFDF691178 -> (  //<<--
            (struct list_head *) [D:0xFFFFFFEFDF691178] next = 0xFFFFFFEFC4CF7370 -> (

위 태스크 디스크립터에서 "//<<--" 라고 표시된 부분이 다음 코드 '&t->signal->shared_pending' 자료구조입니다.
shared_pending->list->next 필드가 0xFFFFFFEFDF691178를 가리키고 있습니다.

04 struct sigpending *pending;
05 struct sigqueue *q;
...
08 pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;

shared_pending->list->next 필드가 0xFFFFFFEFDF691178이니 다음 명령어를 써서 struct sigqueue *q 구조체를 볼 수 있겠습니다.
v.v %t %l container_of(0xFFFFFFEFDF691178,struct sigqueue,list)
  (struct sigqueue *) [-] container_of(0xFFFFFFEFDF691178,struct sigqueue,list) = 0xFFFFFFEFDF691178
    (struct list_head) [D:0xFFFFFFEFDF691178] list = ((struct list_head *) [D:0xFFFFFFEFDF691178] ne
    (int) [D:0xFFFFFFEFDF691188] flags = 0,
    (siginfo_t) [D:0xFFFFFFEFDF691190] info = (
      (int) [D:0xFFFFFFEFDF691190] si_signo = 17, "//<<--[1]"
      (int) [D:0xFFFFFFEFDF691194] si_errno = 0,
      (int) [D:0xFFFFFFEFDF691198] si_code = 1,
      (union) [D:0xFFFFFFEFDF6911A0] _sifields = ((int [28]) [D:0xFFFFFFEFDF6911A0] _pad = (6653, 20
    (struct user_struct *) [D:0xFFFFFFEFDF691210] user = 0xFFFFFFEFDC6CAD40)

"//<<--[1]"에 표시된 것과 같이 시그널 번호가 17입니다.

이번에는 시그널을 받을 프로세스의 struct thread_info 구조체 flags 필드를 보겠습니다.
  (struct task_struct *) [-] (struct task_struct *)0xFFFFFFEFC7EECC00 = 0xFFFFFFEFC7EECC00 -> (
    (struct thread_info) [D:0xFFFFFFEFC7EECC00] thread_info = (
      (long unsigned int) [D:0xFFFFFFEFC7EECC00] flags = 0x00100001,
      (long unsigned int [7]) [D:0xFFFFFFEFC7EECC08] padding = (0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0),
 
위에서 보이듯 struct thread_info 구조체 flags 필드가 0x00100001이니 _TIF_SIGPENDING 정보가 써져 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/include/asm/thread_info.h]
#define _TIF_SIGPENDING (1 << TIF_SIGPENDING)

이제 디버깅 정보를 통해 배운 내용을 정리해보겠습니다.

v.v %t %l container_of(0xFFFFFFEFDF691178,struct sigqueue,list)
  (struct sigqueue *) [-] container_of(0xFFFFFFEFDF691178,struct sigqueue,list) = 0xFFFFFFEFDF691178
    (struct list_head) [D:0xFFFFFFEFDF691178] list = ((struct list_head *) [D:0xFFFFFFEFDF691178] ne
    (int) [D:0xFFFFFFEFDF691188] flags = 0,
    (siginfo_t) [D:0xFFFFFFEFDF691190] info = (
      (int) [D:0xFFFFFFEFDF691190] si_signo = 17, "//<<--[1]"
      (int) [D:0xFFFFFFEFDF691194] si_errno = 0,
      (int) [D:0xFFFFFFEFDF691198] si_code = 1,
      (union) [D:0xFFFFFFEFDF6911A0] _sifields = ((int [28]) [D:0xFFFFFFEFDF6911A0] _pad = (6653, 20
    (struct user_struct *) [D:0xFFFFFFEFDF691210] user = 0xFFFFFFEFDC6CAD40)

     "시그널을 받으려는 프로세스의 태스크 디스크립터 signal->shared_pending->list에 
     struct sigqueue 구조체 list 플래그를 주소를 등록한다."

 
  (struct task_struct *) [-] (struct task_struct *)0xFFFFFFEFC7EECC00 = 0xFFFFFFEFC7EECC00 -> (
    (struct thread_info) [D:0xFFFFFFEFC7EECC00] thread_info = (
      (long unsigned int) [D:0xFFFFFFEFC7EECC00] flags = 0x00100001,
      (long unsigned int [7]) [D:0xFFFFFFEFC7EECC08] padding = (0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0),

    "시그널을 받을 프로세스의 struct thread_info 구조체 flags는 _TIF_SIGPENDING로 바뀐다." 

이 정도로 정리하면 시그널 생성 과정에 대해서 쉽게 잊어 먹지는 않을 것 같네요.


"혹시라도 궁금한점이 있으시다면 답글로 남겨주세요. 아는 한 성실하게 답변해 드리겠습니다."



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