Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

5112
737
82115


[라즈베리파이] 라즈비안(Raspbian) 리눅스 커널(LinuxKernel) 전처리 파일 생성하기 2장. 라즈베리파이 설정

전처리 코드 생성해보기

이번 소절에서는 리눅스 커널 빌드 과정에서 전처리 코드 생성 방법을 소개합니다

리눅스 커널 소스 코드를 분석하다 보면 수 많은 매크로를 만납니다. 그런데 이 매크로가 소스 분석의 큰 걸림돌입니다. 리눅스 커널에서 캡슐화와 다형성과 객체지향 방식을 구현하다 보니 매크로로 구현된 코드가 많습니다.

전처리 코드는 이 매크로를 모두 풀어서 표현합니다. 따라서 훨씬 편하게 소스 코드를 분석할 수 있습니다. 리눅스 커널 코드를 분석할 때 전처리 코드를 함께 보시기를 희망합니다.

전처리 코드는 GCC 컴파일 오브젝트를 생성하는 과정에서 추출됩니다. 

먼저 리눅스 커널 전체 소스 코드를 전처리 파일로 추출하는 방법을 소개합니다.  
01 diff --git a/Makefile b/Makefile
02 index 3da5790..0414cb2 100644
03  --- a/Makefile
04 +++ b/Makefile
05 @@ -419,6 +419,7 @@ KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
06            -fno-strict-aliasing -fno-common -fshort-wchar \
07            -Werror-implicit-function-declaration \
08            -Wno-format-security \
09 +          -save-temps=obj \
10            -std=gnu89
11  KBUILD_CPPFLAGS := -D__KERNEL__
12  KBUILD_AFLAGS_KERNEL :=

패치 코드 입력 방식에 대한 이해를 돕기 위해 패치 코드를 반영하기 전 코드를 소개합니다.
[Makefile] 
408 # Use LINUXINCLUDE when you must reference the include/ directory.
409 # Needed to be compatible with the O= option
410 LINUXINCLUDE    := \
411         -I$(srctree)/arch/$(hdr-arch)/include \
412         -I$(objtree)/arch/$(hdr-arch)/include/generated \
413         $(if $(KBUILD_SRC), -I$(srctree)/include) \
414         -I$(objtree)/include \
415         $(USERINCLUDE)
416
417 KBUILD_AFLAGS   := -D__ASSEMBLY__
418 KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
419            -fno-strict-aliasing -fno-common -fshort-wchar \
420            -Werror-implicit-function-declaration \
421            -Wno-format-security \
422            -std=gnu89
423 KBUILD_CPPFLAGS := -D__KERNEL__
424 KBUILD_AFLAGS_KERNEL :=
425 KBUILD_CFLAGS_KERNEL :=
426 KBUILD_AFLAGS_MODULE  := -DMODULE

위 Makefile 파일 421 라인 다음에 아래 코드를 입력하면 됩니다.
-save-temps=obj \

"-save-temps=obj \" 코드를 입력한 후 Makefile은 다음과 같습니다. 
417 KBUILD_AFLAGS   := -D__ASSEMBLY__
418 KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
419            -fno-strict-aliasing -fno-common -fshort-wchar \
420            -Werror-implicit-function-declaration \
421            -Wno-format-security \
422         -save-temps=obj \
423            -std=gnu89
424 KBUILD_CPPFLAGS := -D__KERNEL__
425 KBUILD_AFLAGS_KERNEL :=
426 KBUILD_CFLAGS_KERNEL :=
427 KBUILD_AFLAGS_MODULE  := -DMODULE
 
Makefile 을 위 방식으로 수정한 다음 이전 절에 소개한 build_RPi_kernel.sh 커널 빌드 스크립트를 실행합시다. 전처리 코드는 out 폴더에 생성됩니다. 전처리 코드가 어떻게 생성됐는지 확인합시다.

커널 스케줄링 공통 코드가 있는 kernel/sched/core.c 파일에 대한 전처리 코드를 찾아 보겠습니다.
root@raspberrypi:/home/pi/RPi_kernel_src/out/kernel/sched# ls -al
total 33692
...
-rw-r--r--  1 root home  613852 Mar 19 09:35 .tmp_completion.i
-rw-r--r--  1 root home 20953 Mar 19 09:35 .tmp_completion.s
-rw-r--r--  1 root home 2800883 Mar 19 09:57 .tmp_core.i
-rw-r--r--  1 root home 368699 Mar 19 09:57 .tmp_core.s
-rw-r--r--  1 root home 1262723 Mar 19 09:35 .tmp_cpuacct.i
-rw-r--r--  1 root home 17772 Mar 19 09:35 .tmp_cpuacct.s

C 포멧 리눅스 커널 소스 파일은 다음 위치에서 전처리 파일로 생성됩니다. 
linux/kernel/sched/core.c
out/kernel/sched/.tmp_core.i

소스 파일 이름 앞에 ".tmp_"란 접두사와 가장 마지막에 i가 붙습니다.

그러면 다른 리눅스 커널 소스 코드는 어떻게 전처리 파일로 생성될까요?
linux/init 폴더에 있는 아래 파일을 예로 들겠습니다.
calibrate.c  do_mounts.c do_mounts_initrd.c

위 파일들은 전처리 과정으로 out/init 폴더에 다음 이름으로 생성됩니다.
.tmp_calibrate.i  .tmp_do_mounts.c  .tmp_do_mounts.i  .tmp_do_mounts_initrd.i

root@raspberrypi:/home/pi/RPi_kernel_src/out/init# ls -al
total 12912
-rw-r--r--  1 312538 Mar 19 09:34 .tmp_calibrate.i
-rw-r--r--  1 10928 Mar 19 09:34 .tmp_calibrate.s
-rw-r--r--  1 2830878 Mar 19 09:34 .tmp_do_mounts.i
-rw-r--r--  1 1616189 Mar 19 09:34 .tmp_do_mounts_initrd.i
-rw-r--r--  1 7311 Mar 19 09:34 .tmp_do_mounts_initrd.s
-rw-r--r--  1 1621237 Mar 19 09:34 .tmp_do_mounts_rd.i
-rw-r--r--  1 14425 Mar 19 09:34 .tmp_do_mounts_rd.s

위와 같이 커널 Makefile을 수정해 모든 리눅스 커널 소스 파일을 전처리 코드가 담긴 *.i 파일로 변환할 수 있습니다. 문제는 "*.i" 뿐만 아니라 "*.s" 파일로 생성되 용량이 5GB 까지 커진다는 점입니다. 

대부분 우리는 보고 싶은 커널 소스 파일에 대한 전처리 파일을 열어 보는 경우가 많습니다.

이번에 특정 소스 파일만 전처리 파일로 생성하는 방법을 소개합니다.

셸 스크립트는 다음과 같습니다.
[build_preprocess_RPi_kernel.sh]
01 #!/bin/sh
02
03 echo "configure build output path"
04 
05 KERNEL_TOP_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
06 OUTPUT="$KERNEL_TOP_PATH/out"
07 echo "$OUTPUT"
08 
09 KERNEL=kernel7
10 BUILD_LOG="$KERNEL_TOP_PATH/rpi_preproccess_build_log.txt"
11 
12 PREPROCESS_FILE=$1
13 echo "build preprocessed file: $PREPROCESS_FILE"
14
15 echo "move kernel source"
16 cd linux
17
18 echo "make defconfig"
19 make O=$OUTPUT bcm2709_defconfig
20
21 echo "kernel build"
22 make $PREPROCESS_FILE O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

위와 같은 코드를 입력한 후 build_preprocess_RPi_kernel.sh 이름으로 저장합시다.
파일을 저장한 후 다음 명령어로 실행 권한을 줍시다.
root@raspberrypi:/home/pi/RPi_kernel_src# chmod +x build_preprocess_RPi_kernel.sh

이전에 소개했던 build_RPi_kernel.sh 빌드 스크립트 코드에 3줄 정도 명령어를 추가했습니다.
12 PREPROCESS_FILE=$1
13 echo "build preprocessed file: $PREPROCESS_FILE"

12~13 번째 줄은 셸 스크립트를 실행할 때 전달하는 소스 코드 이름입니다.

22 번째 줄 코드를 보겠습니다.
22 make $PREPROCESS_FILE O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

커널 코드를 빌드할 때 "$PREPROCESS_FILE" 구문이 추가됐습니다.
지정한 파일만 전처리 파일로 추출하라는 의도입니다.

이번에 build_preprocess_RPi_kernel.sh 셸 스크립트를 실행하는 방법을 알아봅시다. build_preprocess_RPi_kernel.sh 셸 스크립트를 실행할 때 디렉토리를 포함한 파일 이름을 지정해야 합니다. 
build_preprocess_RPi_kernel.sh [파일이름.i]

예를 들어 linux/sched/core.c 파일을 전처리 코드로 추출하려면 다음 형식으로 셸 스크립트를 실행하면 됩니다.
build_preprocess_RPi_kernel.sh linux/sched/core.i

이번엔 라즈베리파이에서 다음 명령어로 실행해 봅시다.
root@raspberrypi:/home/pi/RPi_kernel_src# build_preprocess_RPi_kernel.sh kernel/sched/core.i
configure build output path
build preprocessed file: kernel/sched/core.i
make[1]: Entering directory ' root@raspberrypi:/home/pi/RPi_kernel_src/out '
  GEN     ./Makefile
#
# configuration written to .config
#
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'
make[1]: Entering directory '/home/pi/RPi_kernel_src/out'
  GEN     ./Makefile
scripts/kconfig/conf  --silentoldconfig Kconfig
  CHK     include/config/kernel.release
  GEN     ./Makefile
  CHK     include/generated/uapi/linux/version.h
  Using  /home/pi/RPi_kernel_src/linux as source for kernel
  CHK     include/generated/utsrelease.h
  CHK     scripts/mod/devicetable-offsets.h
  CHK     include/generated/timeconst.h
  CHK     include/generated/bounds.h
  CHK     include/generated/asm-offsets.h
  CALL    /home/pi/RPi_kernel_src/linux/scripts/checksyscalls.sh
  CHK     include/generated/compile.h
  CPP     kernel/sched/core.i
  GZIP    kernel/config_data.gz
  CHK     kernel/config_data.h
  Kernel: arch/arm/boot/Image is ready
  Building modules, stage 2.
  Kernel: arch/arm/boot/zImage is ready
  MODPOST 1506 modules
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'

제대로 전처리 파일을 생성하면 위와 같은 빌드 메시지를 볼 수 있습니다.

out/kernel/sched 디렉토리로 가면 core.i 파일만 전처리 코드로 생성된 것을 확인할 수 있습니다. 
root@raspberrypi:/home/pi/RPi_kernel_src/out/kernel/sched# ls
autogroup.o  clock.o       core.i  cpuacct.o      cpufreq.o            cpupri.o


소스 코드 디렉토리를 잘못 지정하면 다음 에러 메시지와 함께 빌드가 중단됩니다.
austin.kim@LGEARND20B15:~/src/book_RPi_kernel$ ./build_preprocess_RPi_kernel.sh  sched/core.i
configure build output path
build preprocessed file: sched/core.i
make[1]: Entering directory '/home001/austin.kim/src/book_RPi_kernel/out'
  GEN     ./Makefile
#
# configuration written to .config
#
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'
make[1]: Entering directory '/home/pi/RPi_kernel_src/out'
  GEN     ./Makefile
scripts/kconfig/conf  --silentoldconfig Kconfig
make[1]: *** No rule to make target 'sched/core.i'.  Stop.
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'
Makefile:146: recipe for target 'sub-make' failed
make: *** [sub-make] Error 2

이 셸 스크립트를 실행할 때는 디렉토리와 소스 파일을 이름을 정확히 지정해야 합니다.

[라즈베리파이] 라즈비안(Raspbian) 리눅스 커널(LinuxKernel) 빌드하기 2장. 라즈베리파이 설정

이번 절에서는 라즈베리파이에서 라즈비안 리눅스 커널 소스 코드를 내려받고 빌드하는 방법을 알아보겠습니다. 

필자는 라즈베리파이를 쓸 때 다음 명령어로 root로 변경해 커널 빌드를 합니다.
$ sudo su

명령어 입력 과정에서 불필요한 권한 설정을 피하기 위해서입니다.

라즈비안 커널 소스 코드 내려받기

리눅스 커널 소스 코드를 다운로드 받는 방법을 소개합니다.

다음 명령어를 입력하면 라즈비안 최신 커널 소스를 내려 받을 수 있습니다.
git clone --depth=3000 https://github.com/raspberrypi/linux

위 명령어를 라즈베리파이 터미널에서 입력하면 다음 화면을 볼 수 있습니다.
root@raspberrypi:/home/pi/RPi_kernel_src# git clone --depth=3000 https://github.com/raspberrypi/linux
Cloning into 'linux'...
remote: Enumerating objects: 85646, done.
remote: Counting objects: 100% (85646/85646), done.
remote: Compressing objects: 100% (65774/65774), done.
Receiving objects:  33% (28569/85646), 41.45 MiB | 1.22 MiB/s    

소스 코드를 내려 받는데 약 10분 정도가 걸립니다.

커널 소스를 다 내려받고 나서 브랜치를 확인하니 rpi-4.19.y입니다.
root@raspberrypi:/home/pi/RPi_kernel_src# git branch
* rpi-4.19.y

linux 폴더에 생성되는 .git/config 파일을 열어보면 기본으로 rpi-4.19.y 브랜치를 Fetch합니다.
root@raspberrypi:/home/pi/RPi_kernel_src/linux# vi .git/config
.git/config
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
[remote "origin"]
    url = https://github.com/raspberrypi/linux
    fetch = +refs/heads/rpi-4.19.y:refs/remotes/origin/rpi-4.19.y
[branch "rpi-4.19.y"]
    remote = origin
    merge = refs/heads/rpi-4.19.y

이렇게 git clone 명령어를 써서 라즈비안 리눅스 커널 소스 코드를 내려 받으면 최신 버전 브랜치를 선택합니다.

리눅스 커널은 git 유틸리티 프로그램을 써서 소스 코드를 관리합니다. 브랜치는 git에서 자주 쓰는 개념인데 프로젝트 소스 코드의 큰 줄기라고 볼 수 있습니다. 라즈비안은 리눅스 커널 버전에 따라 각기 다른 브랜치를 운영합니다.

이 블로그에서 리눅스 커널 4.19.30 버전 기준으로 빌드와 테스트를 했습니다. 따라서 이 블로그에서 다루는 리눅스 커널 버전에 맞춰서 커널 코드를 내려 받기를 권장합니다. 이를 위해 rpi-4.19.y 브랜치를 선택해야 합니다.

이번에는 브랜치를 rpi-4.19.y로 지정해 라즈비안 리눅스 커널 코드를 다운로드하는 방식을 소개합니다.
git clone --depth=1000 --branch rpi-4.19.y https://github.com/raspberrypi/linux

root@raspberrypi:/home/pi/RPi_kernel_src# git clone --depth=1000 --branch rpi-4.19.y https://github.com/raspberrypi/linux
Cloning into 'linux'...
remote: Enumerating objects: 122739, done.
remote: Counting objects: 100% (122739/122739), done.
remote: Compressing objects: 100% (74357/74357), done.
remote: Total 122739 (delta 54284), reused 72109 (delta 47307), pack-reused 0
Receiving objects: 100% (122739/122739), 185.97 MiB | 1.17 MiB/s, done.
Resolving deltas: 100% (54284/54284), done.
Checking connectivity... done.
Checking out files: 100% (61885/61885), done.

독자 분들은 가급적이면 본 도서에서 입력한 소스 코드 버전인 4.19에 맞춰서 라즈비안 리눅스 커널 소스 코드를 다운로드하길 희망합니다. 시간이 흘러 2021년이 되면 라즈비안 기본 브랜치는 아마 5.4 로 설정될 수 있습니다. 즉, git clone 명령어로 브랜치 이름을 설정하지 않으면 최신 브랜치 소스 코드를 다운로드하게 됩니다.

리눅스 커널 4.19는 LTS(Long Term Relelase) 버전입니다. 리눅스 커널 커뮤니티에서 업데이트된 버그 패치를 꾸준히 반영하면서 관리하는 것이 LTS입니다.

라즈비안 리눅스 커널 빌드하기

라즈비안 리눅스 커널 소스 코드를 내려 받았으니 이제 소스를 빌드하는 방법을 알아볼 차례입니다.

다음 라즈베리파이 홈페이지에 가면 커널 빌드 방법을 확인할 수 있습니다.
[https://www.raspberrypi.org/documentation/linux/kernel/building.md]
Raspberry Pi 2, Pi 3, Pi 3+, and Compute Module 3 default build configuration
cd linux
KERNEL=kernel7
make bcm2709_defconfig

Build and install the kernel, modules, and Device Tree blobs; this step takes a long time:
make -j4 zImage modules dtbs
sudo make modules_install
sudo cp arch/arm/boot/dts/*.dtb /boot/
sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/
sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/
sudo cp arch/arm/boot/zImage /boot/$KERNEL.img

위 방식으로 빌드 명령어를 터미널에서 입력할 수 있습니다.  
커널 빌드를 할 때마다 위와 같은 명령어를 입력하면 어떨까요? 시간이 오래 걸립니다.
그래서 위와 같은 커널 빌드 명령어를 모아 파일을 하나 생성하며 커널 빌드를 할 때 실행합니다. 이를 빌드 스크립트라고 부르며 대부분 임베디드 리눅스 개발에서 활용합니다.

다음은 라즈베리파이에서 커널 빌드를 할 수 있는 빌드 스크립트 코드입니다.
01 #!/bin/sh
02
03 echo "configure build output path"
04 
05 KERNEL_TOP_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
06 OUTPUT="$KERNEL_TOP_PATH/out"
07 echo "$OUTPUT"
08 
09 KERNEL=kernel7
10 BUILD_LOG="$KERNEL_TOP_PATH/rpi_build_log.txt"
11 
12 echo "move kernel source"
13 cd linux
14
15 echo "make defconfig"
16 make O=$OUTPUT bcm2709_defconfig
17
18 echo "kernel build"
19 make O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

위 코드를 입력한 다음 build_RPi_kernel.sh 파일로 저장합시다. 참고로 가장 왼쪽에 있는 번호는 입력하면 안됩니다.

파일을 저장한 다음 아래 "chmod +x" 명령어를 입력해 파일에 실행 권한을 부여합시다.
root@raspberrypi:/home/pi/RPi_kernel_src# chmod +x build_RPi_kernel.sh

빌드 스크립트 코드에 대해서 조금 더 살펴볼까요?
05 KERNEL_TOP_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
06 OUTPUT="$KERNEL_TOP_PATH/out"
07 echo "$OUTPUT"

05 번째 줄은 현재 실행 디렉토리를 KERNEL_TOP_PATH 에 저장합니다. 난해해 보이는 이 코드는 셸 스크립트 명령어입니다. 

06 번째 줄은 KERNEL_TOP_PATH 경로에 out 폴더 추가해 OUTPUT 매크로에 저장합니다. 
OUTPUT=root@raspberrypi:/home/pi/RPi_kernel_src

05~06 번째 줄 코드를 실행하면 OUTPUT 폴더를 위와 같이 지정할 수 있습니다.

OUTPUT은 16 번째와 19번째 줄과 같이 커널 컨피그와 커널 빌드 명령어에 "O=$OUTPUT" 형식으로 추가합니다. 
16 make O=$OUTPUT bcm2709_defconfig
19 make O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

커널을 빌드하면 추출되는 오브젝트 파일을 out 폴더에 생성하겠다는 의도입니다.

다음 커널 컨피그 파일을 생성하는 코드를 소개합니다.
16 make O=$OUTPUT bcm2709_defconfig

위 명령어는 다음 경로에 있는 bcm2709_defconfig 파일에 선언된 컨피그 파일을 참고해 .config 파일을 생성합니다.
root@raspberrypi:/home/pi/RPi_kernel_src/linux/arch/arm/configs/bcm2709_defconfig

make 옵션으로 "O=$OUTPUT" 을 추가하니 다음과 같이 out 폴더에 .config가 생성됩니다.
root@raspberrypi:/home/pi/RPi_kernel_src/out/.config

다음 19 번째 줄입니다.
19 make O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

리눅스 커널 소스 코드를 빌드하는 명령어입니다.

다음은 커널 빌드 로그를 저장하는 코드를 함께 보겠습니다.
10 BUILD_LOG="$KERNEL_TOP_PATH/rpi_build_log.txt"
19 make O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

10 번째 줄은 $KERNEL_TOP_PATH 커널 코드 디렉토리에 rpi_build_log.txt 파일을 지정해 BUILD_LOG에 저장합니다. 19 번째 줄 커널 빌드 명령어입니다. 가장 오른쪽에 "2>&1 | tee $BUILD_LOG" 코드를 추가됐습니다.

커널 빌드 시 출력하는 메시지를 $BUILD_LOG 파일에 저장하는 동작입니다.
커널 빌드 도중 컴파일 에러가 발생하면 rpi_build_log.txt 파일을 열어 어디서 문제가 생겼는지 확인합니다.

실전 개발에서 리눅스 커널 빌드 도중 문제가 발생하면 이 방식으로 커널 빌드 로그를 다른 개발자에게 전달합니다. 커널 로그에 커널 빌드 옵션 등을 볼 수 있기 때문입니다.

빌드 스크립트 코드를 설명드렸으니 이번에는 빌드 스크립트를 실행하는 방법을 소개합니다.

먼저 라즈비안 커널 소스 코드로 이동합니다. 소스 코드에 이동한 상태는 다음과 같습니다.
root@raspberrypi:/home/pi/RPi_kernel_src# ls
build_RPi_kernel.sh linux  

다음 "./build_RPi_kernel.sh" 명령어를 입력해 커널 빌드를 시작합시다. 
root@raspberrypi:/home/pi/RPi_kernel_src# ./build_RPi_kernel.sh
configure build output path
make[1]: Entering directory ' /home/pi/RPi_kernel_src/out'
  GEN     ./Makefile
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
  SHIPPED scripts/kconfig/zconf.tab.c
  SHIPPED scripts/kconfig/zconf.lex.c
  HOSTCC  scripts/kconfig/zconf.tab.o
  HOSTLD  scripts/kconfig/conf
...
  CC [M]  net/bridge/netfilter/ebt_nflog.o
  AR      net/bluetooth/bnep/built-in.o
  CC [M]  net/bluetooth/bnep/core.o
  CC [M]  fs/dlm/recover.o
  CC [M]  sound/soc/codecs/cs42xx8-i2c.o
  AR      drivers/i2c/busses/built-in.o
  CC [M]  drivers/i2c/busses/i2c-bcm2708.o
  CC [M]  fs/dlm/recoverd.o
...
  LD [M]  sound/soc/snd-soc-core.ko
  LD [M]  sound/usb/6fire/snd-usb-6fire.ko
  LD [M]  sound/usb/caiaq/snd-usb-caiaq.ko
  LD [M]  sound/usb/hiface/snd-usb-hiface.ko
  LD [M]  sound/usb/misc/snd-ua101.ko
  LD [M]  sound/usb/snd-usb-audio.ko
  LD [M]  sound/usb/snd-usbmidi-lib.ko
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'

만약 컴파일 에러가 발생하면 다음과 같은 에러 메시지와 함께 빌드가 중단됩니다.
root@raspberrypi:/home/pi/RPi_kernel_src# ./build_RPi_kernel.sh
configure build output path
make[1]: Entering directory '/home/pi/RPi_kernel_src/out'
  GEN     ./Makefile
#
# configuration written to .config
#
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'
make[1]: Entering directory '/home/pi/RPi_kernel_src/out'
  GEN     ./Makefile
scripts/kconfig/conf  --silentoldconfig Kconfig
...
  CHK     include/generated/compile.h
  CC      kernel/sched/core.o
/home/pi/RPi_kernel_src/linux/kernel/sched/core.c:3302:6: error: #error "invoke compile error inside __schdedule"
     #error "invoke compile error inside __schdedule"
      ^
/home/pi/RPi_kernel_src/linux/scripts/Makefile.build:328: recipe for target 'kernel/sched/core.o' failed
make[3]: *** [kernel/sched/core.o] Error 1
/home/pi/RPi_kernel_src/linux/scripts/Makefile.build:587: recipe for target 'kernel/sched' failed
make[2]: *** [kernel/sched] Error 2
/home/pi/RPi_kernel_src/linux/Makefile:1040: recipe for target 'kernel' failed
make[1]: *** [kernel] Error 2
make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory '/home/pi/RPi_kernel_src/out'
Makefile:146: recipe for target 'sub-make' failed
make: *** [sub-make] Error 2

사실 위 컴파일 에러는 다음 코드를 일부러 작성해 발생시킨 것입니다.
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 4e89ed8..5c46f29 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -3299,7 +3299,7 @@ static void __sched notrace __schedule(bool preempt)
        struct rq_flags rf;
        struct rq *rq;
        int cpu;
-
+    #error "invoke compile error inside __schdedule"
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
        prev = rq->curr;

#error는 gcc 컴파일러가 제공하는 매크로인데 코드를 컴파일 할 때 무조건 컴파일 에러를 유발합니다.

이렇게 컴파일 에러를 만나면 반드시 리눅스 커널 코드를 수정한 다음 다시 빌드를 해야 합니다. 만약 컴파일 에러를 제대로 수정하지 않고 커널 이미지를 설치하면 수정한 코드가 제대로 동작하지 않습니다.

라즈비안 리눅스 커널 설치하기

커널 코드를 빌드만 해서는 수정한 코드가 라즈베리파이에서 실행하지 않습니다.
컴파일해 생성된 이미지를 라즈베리파이에 설치해야 합니다.

리눅스 커널 코드를 빌드했으니 이제 빌드한 커널 이미지를 설치해봅시다.

다음 코드는 라즈비안 이미지를 라즈베리파이에 설치하는 셸 스크립트입니다.
[install_RPi_kernel_img.sh]
01 #!/bin/sh
02
03 KERNEL_TOP_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
04 OUTPUT="$KERNEL_TOP_PATH/out"
05 echo "$OUTPUT"
06
07 cd linux
08
09 make O=$OUTPUT modules_install
10 cp $OUTPUT/arch/arm/boot/dts/*.dtb /boot/
11 cp $OUTPUT/arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/
12 cp $OUTPUT/arch/arm/boot/dts/overlays/README /boot/overlays/
13 cp $OUTPUT/arch/arm/boot/zImage /boot/kernel7.img

위와 같은 코드를 입력 후 다음 명령어로 install_RPi_kernel_img.sh 셸 스크립트를 실행합시다.
root@raspberrypi:/home/pi/RPi_kernel_src# ./install_RPi_kernel_img.sh

install_RPi_kernel_img.sh 스크립트를 실행하기 전에 먼저 커널 빌드 에러 메시지를 반드시 수정해야 합니다. 이 과정을 빼 먹고 커널 이미지를 설치하면 수정한 커널 이미지가 제대로 설치되지 않습니다. 



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


3장. 리눅스커널 디버깅


4장. 프로세스 관리


5장 인터럽트 핸들링

6장. 인터럽트 후반부 처리
6.9 Soft IRQ 서비스는 누가 언제 처리하나?
6.13 Soft IRQ 디버깅
   6.13.1 ftrace Soft IRQ 이벤트 분석 방법
   6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인


7장. 커널 타이머관리

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

9장 커널 동기화 소개

10장 커널 동기화 기법

11장 시스템 콜

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


13장 프로세스 스케줄링
스케줄링 소개
프로세스 상태 관리
   프로세스 상태 소개
   프로세스 상태 변화
   어떤 함수가 프로세스 상태를 변경할까? 
         TASK_RUNNING(실행 대기)
 TASK_RUNNING(CPU 실행)
 TASK_INTERRUPTIBLE 상태 변경
 TASK_UNINTERRUPTIBLE 상태 변경
   프로세스 상태 ftrace로 확인하기 
스케줄링 클래스
   스케줄링 클래스 자료구조 소개
   5가지 스케줄러 클래스란 무엇일까?
   프로세스는 스케줄러 클래스를 어떻게 등록할까?
   프로세스는 스케줄링 클래스로 스케줄러 세부 함수를 어떻게 호출할까?
런큐
   런큐 자료구조 struct rq 소개
   runqueues 변수에 대해서
   런큐에 접근하는 함수 소개
   런큐 자료구조 확인하기
비동기적인 Preemption(선점)
   선점 스케줄링(Preemptive Scheduling)이란 무엇일까?
   선점 스케줄링 진입점은 어디인가?
   선점 스케줄링 발생 시점을 아는 것은 왜 중요할까?
   커널 모드 중 인터럽트 발생으로 선점 스케줄링 실행  
   유저 프로세스 실행 중 인터럽트 발생으로 선점 스케줄링
   시스템 콜 실행이 끝난 후 선점 스케줄링
   비동기적 선점을 지연하는 방법: preempt_disable()/preempt_enable() 
프로세스는 어떻게 깨울까?
   프로세스를 깨운다는 것을 무엇을 의미할까?
   프로세스를 깨울 때 호출하는 함수
   프로세스를 런큐에 Enqueue하는 흐름
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
   컨택스트 스위칭이란 무엇인가?  
   컨택스트 스위칭 자료구조는 무엇일까?
스케줄링 디버깅
   ftrace: sched_switch와 sched_wakeup 이벤트 소개
   sched_switch/sched_wakeup 이벤트 출력 함수 코드 분석
   스케줄링과 프로세스를 깨울 때 콜스택 파악
   비선점 스케줄링 콜스택 파악
   스케줄링 프로파일링

14장 가상 파일시스템

15장 메모리 관리



[리눅스커널] 비트 마스크를 어셈블리 코드로 빨리 읽는 방법 - HARDIRQ_MASK, SOFTIRQ_MASK, NMI_MASK Linux Kernel - Core Analysis

이번에는 비트 마스크를 C 코드가 아닌 어셈블리 코드로 읽는 방법을 소개합니다.

in_interrupt() 함수 소개

in_interrupt() 함수는 현재 프로세스가 인터럽트 컨택스트인지 알려주는 기능입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h]
#define in_interrupt() (irq_count())

in_interrupt() 함수 코드를 보면 irq_count() 함수로 치환됩니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h]
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
| NMI_MASK))

irq_count() 함수 코드를 보면 preempt_count() 결과값과 다음 플래그간 AND BIT 오퍼레이션을 실행합니다.
HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK

(HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK) 의 정체는 무엇일까요?

다시 소스 코드를 따라가 보니, 눈이 어지럽습니다. "이것을 어떻게 계산할까!!!" 란 생각이 드는군요.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h]
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 1

#define PREEMPT_SHIFT 0
#define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS)
#define HARDIRQ_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS)
#define NMI_SHIFT (HARDIRQ_SHIFT + HARDIRQ_BITS)

#define __IRQ_MASK(x) ((1UL << (x))-1)

#define PREEMPT_MASK (__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT)
#define SOFTIRQ_MASK (__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT)
#define HARDIRQ_MASK (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT)
#define NMI_MASK (__IRQ_MASK(NMI_BITS)     << NMI_SHIFT)

(HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK) 마스크를 어셈블리 코드로 알아보기

C 코드 형태로 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK) 의 정체를 알기는 좀 복잡합니다.
그러면 위 마스크값들의 정체를 알기 위해 in_interrupt() 함수를 어셈블리 코드를 한 번 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/mm/vmalloc.c]
static struct vm_struct *__get_vm_area_node(unsigned long size,
unsigned long align, unsigned long flags, unsigned long start,
unsigned long end, int node, gfp_t gfp_mask, const void *caller)
{
struct vmap_area *va;
struct vm_struct *area;

BUG_ON(in_interrupt());

__get_vm_area_node() 함수 가장 앞단에 in_interrupt() 함수를 호출합니다. 아주 좋은 예시입니다.
01 NSR:80265FEC|__get_vm_area_node:   cpy     r12,r13
02 NSR:80265FF0|                      push    {r4-r9,r11-r12,r14,pc}
03 NSR:80265FF4|                      sub     r11,r12,#0x4     ; r11,r12,#4
04 NSR:80265FF8|                      sub     r13,r13,#0x8     ; r13,r13,#8
05 NSR:80265FFC|                      str     r14,[r13,#-0x4]!
06 NSR:80266000|                      bl      0x8010FDBC       ; __gnu_mcount_nc
07 NSR:80266004|                      cpy     r14,r13
08 NSR:80266008|                      bic     r12,r14,#0x1FC0   ; r12,r14,#8128
09 NSR:8026600C|                      bic     r12,r12,#0x3F    ; r12,r12,#63
10 NSR:80266010|                      ldr     r14,[r12,#0x4]
11 NSR:80266014|                      ldr     r12,0x80266104
12 NSR:80266018|                      cpy     r8,r1            ; r8,align
13 NSR:8026601C|                      and     r12,r12,r14
14 NSR:80266020|                      cmp     r12,#0x0         ; r12,#0
15 NSR:80266024|                      cpy     r4,r2            ; r4,flags
16 NSR:80266028|                      cpy     r9,r3            ; r9,start

먼저 07~09번째 줄 코드를 보겠습니다.
07 NSR:80266004|                      cpy     r14,r13
08 NSR:80266008|                      bic     r12,r14,#0x1FC0   ; r12,r14,#8128
09 NSR:8026600C|                      bic     r12,r12,#0x3F    ; r12,r12,#63

스택 주소를 통해 프로세스 스택 최상단 주소에 접근하는 동작입니다. 
위 어셈블리 코드 연산 결과 r12는 스택 최상단 주소를 저장하게 됩니다.

다음 10번째 줄 코드입니다.
10 NSR:80266010|                      ldr     r14,[r12,#0x4]

스택 최상단 주소에 있는 struct thread_info 구조체 preempt_count 필드를 r14에 로딩합니다.
만약 r12가 0x800c0000이면 0x800C0004주소에 있는 preempt_count 필드의 0x00010002를 r14에 저장합니다. 
  (struct thread_info *) [-] (struct thread_info*)0x800c0000  
    (long unsigned int) [D:0x800C0000] flags = 0x0,
    (int) [D:0x800C0004] preempt_count = 0x00010002,
    (mm_segment_t) [D:0x800C0008] addr_limit = 0x0,

핵심 코드를 볼 차례입니다.
11 NSR:80266014|                      ldr     r12,0x80266104
12 NSR:80266018|                      cpy     r8,r1            ; r8,align
13 NSR:8026601C|                      and     r12,r12,r14
14 NSR:80266020|                      cmp     r12,#0x0         ; r12,#0
 
[11 행]: 0x80266104 주소에 있는 값을 r12에 저장합니다.

0x80266104 주소엔 0x001FFF00가 있습니다.  
_____address|________0________4________8________C 
NSD:80266100| E7F001F2>001FFF00 00693EE0 809E02AC  

(HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK) 의 정체가 0x001FFF00입니다.

[13 행]: r12와 r14 레지스터와 AND 비트 연산을 수행합니다.
이를 쉽게 표현하면 다음과 같습니다.
(struct thread_info 구조체 preempt_count 필드) & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)

어셈블리 코드 분석으로 다음 내용을 알게 됐습니다.
(HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK) = 0x001FFF00

HARDIRQ_MASK, SOFTIRQ_MASK, NMI_MASK 플래그를 어셈블리 코드로 계산해보기

어셈블리 코드 분석으로 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)의 정체가 0x001FFF00이란 사실을 알게 됐습니다.
이번엔 각각 플래그 값을 알아볼까요?

이번엔 tracing_generic_entry_update() 함수를 어셈블리 코드로 분석해 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/trace/trace.c]
1 void
2 tracing_generic_entry_update(struct trace_entry *entry, unsigned long flags,
3      int pc)
4 {
5 struct task_struct *tsk = current;
7 entry->preempt_count = pc & 0xff;
8 entry->pid = (tsk) ? tsk->pid : 0;
9 entry->flags =
10 #ifdef CONFIG_TRACE_IRQFLAGS_SUPPORT
11 (irqs_disabled_flags(flags) ? TRACE_FLAG_IRQS_OFF : 0) |
12 #else
13 TRACE_FLAG_IRQS_NOSUPPORT |
14 #endif
15 ((pc & NMI_MASK    ) ? TRACE_FLAG_NMI     : 0) |
16 ((pc & HARDIRQ_MASK) ? TRACE_FLAG_HARDIRQ : 0) |
17 ((pc & SOFTIRQ_OFFSET) ? TRACE_FLAG_SOFTIRQ : 0) |

위 코드 15~17번째 줄을 어셈블리 코드로 분석하는 것입니다.

tracing_generic_entry_update() 함수를 어셈블리 코드로 보면 다음과 같습니다. 
01 NSR:801E3B50|tracing_generic_entry_update:  cpy     r12,r13
02 NSR:801E3B54|                               push    {r4-r5,r11-r12,r14,pc}
03 NSR:801E3B58|                               sub     r11,r12,#0x4     ; r11,r12,#4
04 NSR:801E3B5C|                               cpy     r12,r13
05 NSR:801E3B60|                               bic     r3,r12,#0x1FC0   ; r3,r12,#8128
06 NSR:801E3B64|                               bic     r3,r3,#0x3F      ; r3,r3,#63
07 NSR:801E3B68|                               ldr     r3,[r3,#0x0C]
08 NSR:801E3B6C|                               bic     r12,r12,#0x1FC0   ; r12,r12,#8128
09 NSR:801E3B70|                               strb    r2,[r0,#0x3]     ; pc,[r0,#3]
10 NSR:801E3B74|                               cmp     r3,#0x0          ; tsk,#0
11 NSR:801E3B78|                               ldrne   r3,[r3,#0x3A8]   ; tsk,[r3,#936]
12 NSR:801E3B7C|                               bic     r12,r12,#0x3F    ; r12,r12,#63
13 NSR:801E3B80|                               tst     r2,#0x100000     ; pc,#1048576 /* <<-- NMI_MASK */
14 NSR:801E3B84|                               ldr     r12,[r12]
15 NSR:801E3B88|                               moveq   r5,#0x0          ; r5,#0
16 NSR:801E3B8C|                               movne   r5,#0x40         ; r5,#64
17 NSR:801E3B90|                               tst     r2,#0xF0000      ; pc,#983040 /* <<-- HARDIRQ_MASK */
18 NSR:801E3B94|                               moveq   r4,#0x0          ; r4,#0
19 NSR:801E3B98|                               movne   r4,#0x8          ; r4,#8
20 NSR:801E3B9C|                               tst     r2,#0x100        ; pc,#256  /* <<-- SOFTIRQ_MASK */
21 NSR:801E3BA0|                               cpy     r14,r13
22 NSR:801E3BA4|                               moveq   r14,#0x0         ; r14,#0

HARDIRQ_MASK, SOFTIRQ_MASK, NMI_MASK 플래그의 정체는 다음과 같습니다.
NMI_MASK : 0x100000
HARDIRQ_MASK :  0xF0000
SOFTIRQ_MASK : 0x100


[tegra4i] UI freeze due to RT class 프로세스 [Crash]Troubleshooting!!

커널 로그는 다음과 같습니다. UI freeze 상태라 강제 커널 크래시를 유발시켜 램덤프를 추출했습니다.

[ 7164.614352] tegra sound_card.27: Ply shutdown, devnum 7
[ 7165.077580] ------------[ cut here ]------------
[ 7165.077807] WARNING: at /root/rpi/kernel/workqueue.c:2904 __cancel
_work_timer+0x12c/0x178()
[ 7165.087114] Modules linked in: texfat(PO)
[ 7165.092596] CPU: 3 PID: 1373 Comm: mmcqd/0 Tainted: P        W  O 3.10.44+ #1
[ 7165.101579] [<c0017344>] (unwind_backtrace+0x0/0x144) from [<c0013874>] (show_stack+0x20/0x24)
[ 7165.111750] [<c0013874>] (show_stack+0x20/0x24) from [<c0936350>] (dump_stack+0x20/0x28)
[ 7165.121489] [<c0936350>] (dump_stack+0x20/0x28) from [<c0028848>] (warn_slowpath_common+0x5c/0x7c)
[ 7165.132133] [<c0028848>] (warn_slowpath_common+0x5c/0x7c) from [<c0028894>] (warn_slowpath_null+0x2c/0x34)
[ 7165.143418] [<c0028894>] (warn_slowpath_null+0x2c/0x34) from [<c0048600>] (__cancel_work_timer+0x12c/0x178)
[ 7165.154654] [<c0048600>] (__cancel_work_timer+0x12c/0x178) from [<c0048668>] (cancel_delayed_work_sync+0x1c/0x20)
[ 7165.168097] [<c0048668>] (cancel_delayed_work_sync+0x1c/0x20) from [<c007b588>] (pm_qos_update_request+0x34/0xac)
[ 7165.178711] [<c007b588>] (pm_qos_update_request+0x34/0xac) from [<c064348c>] (sdhci_disable+0x2c/0x48)
[ 7165.189268] [<c064348c>] (sdhci_disable+0x2c/0x48) from [<c062e68c>] (mmc_release_host+0xa8/0xc0)
[ 7165.199855] [<c062e68c>] (mmc_release_host+0xa8/0xc0) from [<c0640794>] (mmc_blk_issue_rq+0x104/0x664)
[ 7165.210600] [<c0640794>] (mmc_blk_issue_rq+0x104/0x664) from [<c0641514>] (mmc_queue_thread+0xb4/0x14c)
[ 7165.221673] [<c0641514>] (mmc_queue_thread+0xb4/0x14c) from [<c004ef1c>] (kthread+0xc0/0xd0)
[ 7165.231685] [<c004ef1c>] (kthread+0xc0/0xd0) from [<c000f788>] (ret_from_fork+0x14/0x20)
[ 7165.241314] ---[ end trace b64c1dfefe8a5e9e ]---


각 cpu별 current task들의 backtrace
0 번 CPU만 key에 의한 강제 crash를 발생 시키기 위한 work thread가 돌고 있고 나머지 cpu들은 idle task였습니다.

PID: 21555  TASK: c3e76400  CPU: 0   COMMAND: "kworker/0:1" ? 강제 crash를 발생한 work
#0 [<c03df0c4>] (crash_kexec_odin) from [<c0931038>]
#1 [<c0931038>] (panic) from [<c0013b4c>]
#2 [<c0013b4c>] (die) from [<c0013ba8>]
#3 [<c0013ba8>] (arm_notify_die) from [<c0008370>]
#4 [<c0008370>] (do_undefinstr) from [<c000f2f0>]
 
PID: 0      TASK: d909e400  CPU: 1   COMMAND: "swapper/1"
#0 [<c00166b0>] (machine_crash_nonpanic_core) from [<c0098774>]
#1 [<c0098774>] (generic_smp_call_function_single_interrupt) from [<c00158ec>]
#2 [<c00158ec>] (handle_IPI) from [<c00086f0>]
#3 [<c00086f0>] (gic_handle_irq) from [<c000f244>]
#4 [<c000f244>] (__irq_svc) from [<c008dce4>]
 
PID: 0      TASK: d909ee00  CPU: 2   COMMAND: "swapper/2"
#0 [<c00166b0>] (machine_crash_nonpanic_core) from [<c0098774>]
#1 [<c0098774>] (generic_smp_call_function_single_interrupt) from [<c00158ec>]
#2 [<c00158ec>] (handle_IPI) from [<c00086f0>]
#3 [<c00086f0>] (gic_handle_irq) from [<c000f244>]
#4 [<c000f244>] (__irq_svc) from [<c008dce4>]
#5 [<c007e63c>] (cpu_startup_entry) from [<c092dc10>]
 
PID: 0      TASK: d92a0000  CPU: 3   COMMAND: "swapper/3"
#0 [<c00166b0>] (machine_crash_nonpanic_core) from [<c0098774>]
#1 [<c0098774>] (generic_smp_call_function_single_interrupt) from [<c00158ec>]
#2 [<c00158ec>] (handle_IPI) from [<c00086f0>]
#3 [<c00086f0>] (gic_handle_irq) from [<c000f244>]
#4 [<c000f244>] (__irq_svc) from [<c008dce4>]


task state 확인
system_server가 UNINTERRUPTIBLE 상태였습니다. 따라서, UI freeze 발생한 것으로 보입니다.

2611      2   3  d79d4600  UN   0.0       0      0  [jbd2/mmcblk0p29]
   2627      2   2  d82dc600  UN   0.0       0      0  [jbd2/mmcblk0p10]
   2630      2   1  d82dee00  UN   0.0       0      0  [jbd2/mmcblk0p22]
   2977      1   3  d82c0a00  UN   0.0    1092    524  rctd
   2993      1   2  d6763c00  UN   0.0    3576    776  sdcard
   3053   2981   0  d84a6e00  UN   0.0     904    432  logcat
   3066   2980   2  d7c91e00  UN   0.0    1312    876  kernel_logger
   3070   2984   0  d7f2da00  UN   0.0     900    428  logcat
   3071   2985   3  d7c91400  UN   0.0     916    440  logcat
   3072   2982   0  d75a8a00  UN   0.0     900    428  logcat
   3077   2986   3  d776b200  UN   0.1    3560   1144  tcpdump
   3127   2758   1  d66ca800  UN   5.5 1075264 112420  system_server
   3304      2   2  d6f69e00  UN   0.0       0      0  [ts_ldisc_tx_loo]
   3305      1   3  d7cbe400  UN   0.1    5568   1248  nvm_server
   3332   2758   2  d675e400  UN   5.5 1075264 112420  InputDispatcher
   3546   2758   3  d7cba800  UN   5.5 1075264 112420  Binder_5
   3556   2758   1  d6f6a800  UN   5.5 1075264 112420  watchdog
   3598   2758   0  d79f8000  UN   3.5  986664  72460  com.lge.ime
   4674      1   2  c6562800  UN   0.0   11324    576  thermalmgr
   5103      1   0  c55f0000  UN   0.0    1040    576  dhcpcd


system_server Call Stack
system_server가 file open을 하는 도중, mutex_lock을 얻기 위해, UNINTERRUPTIBLE 상태로 들어간 상태에서 schedule된 상태입니다.

crash> bt 3127
PID: 3127   TASK: d66ca800  CPU: 1   COMMAND: "system_server"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c093e6e0>]
#2 [<c093e6e0>] (schedule_preempt_disabled) from [<c093cab4>]
#3 [<c093cab4>] (mutex_lock_nested) from [<c0145ed8>]
#4 [<c0145ed8>] (do_last) from [<c0146950>]
#5 [<c0146950>] (path_openat) from [<c014707c>]
#6 [<c014707c>] (do_filp_open) from [<c01372d0>]
#7 [<c01372d0>] (do_sys_open) from [<c0137398>]
#8 [<c0137398>] (sys_open) from [<c000f6c0>]
    pc : [<400a0918>]    lr : [<400ae009>]    psr: 200f0010
    sp : be807318  ip : 6d773a44  fp : be80736c
    r10: 417ed7a0  r9 : 6d773a38  r8 : be807354
    r7 : 00000005  r6 : 00000042  r5 : 00000180  r4 : 417ed178
    r3 : be807328  r2 : 00000180  r1 : 00020042  r0 : 88d01960
    Flags: nzCv  IRQs on  FIQs on  Mode USER_32  ISA ARM


해당 mutex lock 조사

crash> struct mutex 0xD8F0B900
struct mutex {
  count = {
    counter = -1  // lock 걸려있음
  },
<생략>
  wait_list = {
    next = 0xd626fdb0,
    prev = 0xd626fdb0
  },
  owner = 0xc4815000,   // lock의 owner임
  name = 0x0,
  magic = 0xd8f0b900,
  dep_map = {
    key = 0xc0fa51e8 <ext4_fs_type+92>,
    class_cache = {0xc124114c <lock_classes+231200>, 0xc1241bec <lock_classes+233920>},
    name = 0xc0ce3190 "&type->i_mutex_dir_key"
  }
}


owner task의 backtrace

PID: 21441   TASK: c4815000  CPU: 4   COMMAND: "BatteryStats-Wr"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c01eb6a0>]
#2 [<c01eb6a0>] (start_this_handle) from [<c01ebb44>]
#3 [<c01ebb44>] (jbd2__journal_start) from [<c01d3810>]
#4 [<c01d3810>] (__ext4_journal_start_sb) from [<c01a66f8>]
#5 [<c01a66f8>] (__ext4_new_inode) from [<c01b5bd4>]
#6 [<c01b5bd4>] (ext4_create) from [<c0143dcc>]
#7 [<c0143dcc>] (vfs_create) from [<c01464a0>]
#8 [<c01464a0>] (do_last) from [<c0146950>]
#9 [<c0146950>] (path_openat) from [<c014707c>]
#10 [<c014707c>] (do_filp_open) from [<c01372d0>]
#11 [<c01372d0>] (do_sys_open) from [<c0137398>]
#12 [<c0137398>] (sys_open) from [<c000f6c0>]
    pc : [<400a0918>]    lr : [<400ae009>]    psr: 20070010
    sp : 7f639ad0  ip : 796fbef4  fp : 7f639b24
    r10: 87b27138  r9 : 796fbee8  r8 : 7f639b0c
    r7 : 00000005  r6 : 00000241  r5 : 00000180  r4 : 86d5cd08
    r3 : 7f639ae0  r2 : 00000180  r1 : 00020241  r0 : 80b05e48
    Flags: nzCv  IRQs on  FIQs on  Mode USER_32  ISA ARM


0xc4815000 task(PID ? 21441, BatteryStats-Wr ) 도 UNINTERRUPTIBLE 상태임

21441   2758   4  c4815000  UN   5.5 1075264 112420  BatteryStats-Wr


BatteryStats-Wr task가 mutex lock을 accquire한 상태에서, TASK_UNINTERRUPTIBLE로 들어갔고, 
system_server는 같은 mutex lock을 accquire하기 위해 대기중 인 상태라 UI Freeze가 발생한 것으로 보입니다.

static int start_this_handle(journal_t *journal, handle_t *handle) {
<생략>
              spin_lock(&transaction->t_handle_lock);
              needed = transaction->t_outstanding_credits + nblocks;
 
              if (needed > journal->j_max_transaction_buffers) {
                            DEFINE_WAIT(wait);
 
                            jbd_debug(2, "Handle %p starting new commit...\n", handle);
                            spin_unlock(&transaction->t_handle_lock);
                            prepare_to_wait(&journal->j_wait_transaction_locked, &wait, TASK_UNINTERRUPTIBLE); //--> 여기서 UN으로 들어감
                            __log_start_commit(journal, transaction->t_tid);
                            spin_unlock(&journal->j_state_lock);
                            schedule();
                            finish_wait(&journal->j_wait_transaction_locked, &wait);
                            goto repeat;
              }
}


TASK_UNINTERRUPTIBLE 상태인 다른 몇몇 task의 backtrace를 조사해 봤습니다.

PID: 2611   TASK: d79d4600  CPU: 3   COMMAND: "jbd2/mmcblk0p29"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c01ee80c>]
#2 [<c01ee80c>] (jbd2_journal_commit_transaction) from [<c01f4794>]
#3 [<c01f4794>] (kjournald2) from [<c004ef1c>]
#4 [<c004ef1c>] (kthread) from [<c000f788>]


아래의 코드에서 UNINTERRUPTIBLE로 상태로 변경 후 schedule됩니다.

void jbd2_journal_commit_transaction(journal_t *journal){
// snip
              while (atomic_read(&commit_transaction->t_updates)) {
                            DEFINE_WAIT(wait);
                            prepare_to_wait(&journal->j_wait_updates, &wait, TASK_UNINTERRUPTIBLE);
                            if (atomic_read(&commit_transaction->t_updates)) {
                                          spin_unlock(&commit_transaction->t_handle_lock);
                                          write_unlock(&journal->j_state_lock);
                                          schedule();
                                          write_lock(&journal->j_state_lock);
                                          spin_lock(&commit_transaction->t_handle_lock);
                            }
                            finish_wait(&journal->j_wait_updates, &wait);
              }  // snip


아래와 같이 commit_transaction->t_updates가 2입니다.

crash> struct transaction_s.t_updates 0xD6858D80
 t_updates = {
   counter = 2
 }


BatteryStats-Wr task가 mutex lock을 accquire한 상태에서, TASK_UNINTERRUPTIBLE로 들어갔고, system_server는 같은 mutex lock을 accquire하기 위해 대기중 인 상태라 UI Freeze가 발생한 것으로 보입니다.

static int start_this_handle(journal_t *journal, handle_t *handle) {
<생략>
              spin_lock(&transaction->t_handle_lock);
              needed = transaction->t_outstanding_credits + nblocks;
 
              if (needed > journal->j_max_transaction_buffers) {
                            DEFINE_WAIT(wait);
 
                            jbd_debug(2, "Handle %p starting new commit...\n", handle);
                            spin_unlock(&transaction->t_handle_lock);
                            prepare_to_wait(&journal->j_wait_transaction_locked, &wait, TASK_UNINTERRUPTIBLE); //<<-- 여기서 UN으로 들어감
                            __log_start_commit(journal, transaction->t_tid);
                            spin_unlock(&journal->j_state_lock);
                            schedule();
                            finish_wait(&journal->j_wait_transaction_locked, &wait);
                            goto repeat;
              }
}
.

jbd2/mmcblk0p10 task를 보면, 아래의 코드에서 UNINTERRUPTIBLE로 상태로 변경 후 schedule됐는데, io buffer가 lock상태여서, lock이 풀릴때까지 대기 상태에 있는 것입니다.

void jbd2_journal_commit_transaction(journal_t *journal) {
<생략>
                            jh = commit_transaction->t_iobuf_list->b_tprev;
                            bh = jh2bh(jh);
                            if (buffer_locked(bh)) {
                                          wait_on_buffer(bh);
                                          goto wait_for_iobuf;
                            }
<생략>
}
PID: 2627   TASK: d82dc600  CPU: 2   COMMAND: "jbd2/mmcblk0p10"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c093e3d4>]
#2 [<c093e3d4>] (io_schedule) from [<c0168084>]
#3 [<c0168084>] (sleep_on_buffer) from [<c093b784>]
#4 [<c093b784>] (__wait_on_bit) from [<c093b834>]
#5 [<c093b834>] (out_of_line_wait_on_bit) from [<c0168064>]
#6 [<c0168064>] (__wait_on_buffer) from [<c01ef218>]
#7 [<c01ef218>] (jbd2_journal_commit_transaction) from [<c01f4794>]
#8 [<c01f4794>] (kjournald2) from [<c004ef1c>]
#9 [<c004ef1c>] (kthread) from [<c000f788>]


TASK_UNINTERRUPTIBLE상태의 task중 아래와 같이 대부분의 task들이 file관련 동작중 TASK_UNINTERRUPTIBLE상태로 진입한 상태입니다.

PID: 2977   TASK: d82c0a00  CPU: 3   COMMAND: "rctd"
PID: 2993   TASK: d6763c00  CPU: 2   COMMAND: "sdcard"
PID: 3053   TASK: d84a6e00  CPU: 0   COMMAND: "logcat"
PID: 3070   TASK: d7f2da00  CPU: 0   COMMAND: "logcat"
PID: 3071   TASK: d7c91400  CPU: 3   COMMAND: "logcat"
PID: 3072   TASK: d75a8a00  CPU: 0   COMMAND: "logcat"
PID: 3077   TASK: d776b200  CPU: 3   COMMAND: "tcpdump"
PID: 3127   TASK: d66ca800  CPU: 1   COMMAND: "system_server"
PID: 3305   TASK: d7cbe400  CPU: 3   COMMAND: "nvm_server"
PID: 3332   TASK: d675e400  CPU: 2   COMMAND: "InputDispatcher"
PID: 3546   TASK: d7cba800  CPU: 3   COMMAND: "Binder_5"
PID: 3556   TASK: d6f6a800  CPU: 1   COMMAND: "watchdog"
PID: 5103   TASK: c55f0000  CPU: 0   COMMAND: "dhcpcd"
PID: 5165   TASK: c5008a00  CPU: 1   COMMAND: "GC"
PID: 5738   TASK: c59e3200  CPU: 2   COMMAND: "Thread-315"
PID: 8773   TASK: c5344600  CPU: 3   COMMAND: "kworker/u16:6"
PID: 21359  TASK: c4826e00  CPU: 2   COMMAND: "OMXCallbackDisp"
PID: 21413  TASK: d6e48000  CPU: 0   COMMAND: "Thread-220"
PID: 21416  TASK: c9ee0a00  CPU: 1   COMMAND: "referencesProto"
PID: 21424  TASK: d84a4600  CPU: 0   COMMAND: "Compiler"
PID: 21431  TASK: c62c1e00  CPU: 4   COMMAND: "UsageStatsServi"
PID: 21441  TASK: c4815000  CPU: 4   COMMAND: "BatteryStats-Wr"


다시 최초의 log를 리뷰하니 PID 1373, mmcqd/0 task에서 WARNING 메시지 출력합니다.

[ 7165.077807] WARNING: at /root/pi/kernel/workqueue.c:2904 __cancel_work_timer+0x12c/0x178()
[ 7165.087114] Modules linked in: texfat(PO)
[ 7165.092596] CPU: 3 PID: 1373 Comm: mmcqd/0 Tainted: P        W  O 3.10.44+ #1
[ 7165.101579] [<c0017344>] (unwind_backtrace+0x0/0x144) from [<c0013874>] (show_stack+0x20/0x24)
[ 7165.111750] [<c0013874>] (show_stack+0x20/0x24) from [<c0936350>] (dump_stack+0x20/0x28)
[ 7165.121489] [<c0936350>] (dump_stack+0x20/0x28) from [<c0028848>] (warn_slowpath_common+0x5c/0x7c)
[ 7165.132133] [<c0028848>] (warn_slowpath_common+0x5c/0x7c) from [<c0028894>] (warn_slowpath_null+0x2c/0x34)
[ 7165.143418] [<c0028894>] (warn_slowpath_null+0x2c/0x34) from [<c0048600>] (__cancel_work_timer+0x12c/0x178)
[ 7165.154654] [<c0048600>] (__cancel_work_timer+0x12c/0x178) from [<c0048668>] (cancel_delayed_work_sync+0x1c/0x20)
[ 7165.168097] [<c0048668>] (cancel_delayed_work_sync+0x1c/0x20) from [<c007b588>] (pm_qos_update_request+0x34/0xac)
[ 7165.178711] [<c007b588>] (pm_qos_update_request+0x34/0xac) from [<c064348c>] (sdhci_disable+0x2c/0x48)
[ 7165.189268] [<c064348c>] (sdhci_disable+0x2c/0x48) from [<c062e68c>] (mmc_release_host+0xa8/0xc0)
[ 7165.199855] [<c062e68c>] (mmc_release_host+0xa8/0xc0) from [<c0640794>] (mmc_blk_issue_rq+0x104/0x664)
[ 7165.210600] [<c0640794>] (mmc_blk_issue_rq+0x104/0x664) from [<c0641514>] (mmc_queue_thread+0xb4/0x14c)
[ 7165.221673] [<c0641514>] (mmc_queue_thread+0xb4/0x14c) from [<c004ef1c>] (kthread+0xc0/0xd0)
[ 7165.231685] [<c004ef1c>] (kthread+0xc0/0xd0) from [<c000f788>] (ret_from_fork+0x14/0x20)


 mmcqd관련 task의 backtrace 조사한 결과를 정리하면 다음과 같습니다.

 01 |  mmcqd/0 task의 backtrace를 보면, WARNING을 출력했던 backtrace와 동일한 backtrace를 가지고 있음을 알 수 있음.
 02 | mmcqd/0는 mmc 동작중 pm_qos를 통해 boosting을 했고, 완료 이후, pm_qos설정을 이전으로 돌리기 위한 작업을 진행하는 과정
 03 |   sdhci_disable () -> pm_qos_update_request () -> cancel_delayed_work_sync () -> __cancel_work_timer () -> schedule ()

PID: 1373    TASK: d7d29400  CPU: 7   COMMAND: "mmcqd/0"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c0048610>]
#2 [<c0048610>] (__cancel_work_timer) from [<c0048668>]
#3 [<c0048668>] (cancel_delayed_work_sync) from [<c007b588>]
#4 [<c007b588>] (pm_qos_update_request) from [<c064348c>]
#5 [<c064348c>] (sdhci_disable) from [<c062e68c>]
#6 [<c062e68c>] (mmc_release_host) from [<c0640794>]
#7 [<c0640794>] (mmc_blk_issue_rq) from [<c0641514>]
#8 [<c0641514>] (mmc_queue_thread) from [<c004ef1c>]
#9 [<c004ef1c>] (kthread) from [<c000f788>]
 
PID: 1374   TASK: d7d29e00  CPU: 0   COMMAND: "mmcqd/0boot0"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c0641580>]
#2 [<c0641580>] (mmc_queue_thread) from [<c004ef1c>]
#3 [<c004ef1c>] (kthread) from [<c000f788>]
 
PID: 1375   TASK: d79d6e00  CPU: 0   COMMAND: "mmcqd/0boot1"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c0641580>]
#2 [<c0641580>] (mmc_queue_thread) from [<c004ef1c>]
#3 [<c004ef1c>] (kthread) from [<c000f788>]
 
PID: 1376   TASK: d79d6400  CPU: 2   COMMAND: "mmcqd/0rpmb"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c0641580>]
#2 [<c0641580>] (mmc_queue_thread) from [<c004ef1c>]
#3 [<c004ef1c>] (kthread) from [<c000f788>]
 
PID: 1472   TASK: d8222800  CPU: 2   COMMAND: "mmcqd/1"
#0 [<c093de28>] (__schedule) from [<c093e324>]
#1 [<c093e324>] (schedule) from [<c0641580>]
#2 [<c0641580>] (mmc_queue_thread) from [<c004ef1c>]
#3 [<c004ef1c>] (kthread) from [<c000f788>]


__cancel_work_timer ()  함수에서 mmcqd  프로세스가 TASK_INERRUPTIBLE로 진입 후 schedule() 됩니다.

static bool __cancel_work_timer(struct work_struct *work, bool is_dwork) {
<생략>
              do {
                            ret = try_to_grab_pending(work, is_dwork, &flags);
                            if (unlikely(ret == -ENOENT)) {
                                          if (!flush_work(work)) {
                                                        if (work_is_canceling(work)) {
                                                                      set_current_state(TASK_INTERRUPTIBLE);
                                                                      schedule(); //<<--INTERRUPTIBLE로 진입후 스케쥴
                                                                      __set_current_state(TASK_RUNNING);
                                                        }
                                          }
                            }
              } while (unlikely(ret < 0));
<생략> }


최초 로그의 WARNING이 출력된 시간과 비슷(7165s) 합니다.

crash> struct task_struct.se 0xd7d29400
 se = {
   load = {
     weight = 1024,
     inv_weight = 4194304
   },
   run_node = {
     __rb_parent_color = 1,
     rb_right = 0x0,
     rb_left = 0x0
   },
   group_node = {
     next = 0xd7d2944c,
     prev = 0xd7d2944c
   },
   on_rq = 0,
   exec_start = 7165247678131, //<<-- 최초 로그의 WARNING이 출력된 시간과 비슷함(7165s)


task_struct.se.exec_start는 아래의 코드에서 update가 되도록 되어있습니다.

static void update_curr(struct cfs_rq *cfs_rq){ <생략>
              curr->exec_start = now;
<생략>
}


emmc 관련 동작을 하는 mmcqd/0 가 7165s wakeup되어 running중, TASK_INTERRUPTIBLE 상태로 schedule이 되고, 다시 wakeup되지 않습니다.
 1.  file open/read/write 및 jonrnaling 등의 동작을 시도한 task들이 전부 TASK_UNINTERRUPTIBLE상태로 들어가게 됨.
 2.  따라서, UI Freeze로 현상이 나타남.



다음 패치를 적용한 후 UI Freeze 현상은 사라졌습니다.
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index e1cae9d..cb3a27b 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -2759,6 +2759,7 @@ static bool __cancel_work_timer(struct work_struct *work, bool is_dwork)
 * If someone else is canceling, wait for the same event it
                 * would be waiting for before retrying.
                 */
 
-               if (unlikely(ret == -ENOENT))
-                       flush_work(work);
+               if (unlikely(ret == -ENOENT)) {
+                       if (!flush_work(work) &&  current->policy == SCHED_FIFO && work_is_canceling(work)) {   
+                               struct sched_param old, new;
+                               new.sched_priority = 0;
+                               sched_setscheduler(current, SCHED_NORMAL, &new);
+                               schedule();
+                               sched_setscheduler(current, SCHED_FIFO, &old);
+                       }
+               }

[리눅스][커널] RT 프로세스가 CPU를 많이 점유할 때 개선 패치 13장. 프로세스 스케줄링

RT 프로세스는 자신이 일을 끝낼 때 까지 CPU를 점유합니다. 따라서, 다른 일반 프로세스들이 실행할 수 있는 기회를 놓치게 됩니다.

이를 방지할 수 있는 흥미로운 패치 코드를 소개합니다.
diff --git a/drivers/media/platform/rpi/tasklet_util.c b/drivers/media/platform/rpi/tasklet_util.c
index 2c08e4d..28cd15c 100644
--- a/drivers/media/platform/rpi/graphic/tasklet_util.c
+++ b/drivers/media/platform/rpi/graphic/tasklet_util.c
@@ -15,6 +15,7 @@
1  #include <linux/interrupt.h>
2  #include <linux/list.h>
3  #include <linux/ratelimit.h>
4 +#include <uapi/linux/sched/types.h>
5 #include "dev_tasklet_util.h"
6 #include "dev_irq_controller.h"
7 #include "dev_debug_util.h"
8 @@ -296,14 +297,41 @@ int devie_tasklet_start(void  *tasklet_info)
9        return 0;
10 }
11
12 +static bool is_rt_policy(unsigned int policy)
13 +{
14 +       return policy == SCHED_FIFO || policy == SCHED_RR;
15 +}
16 +
17 void devie_tasklet_stop(void  *tasklet_info)
18 {
19        struct devie_tasklet_info  *tasklet = tasklet_info;
20 +       unsigned int policy = current->policy;
21 +       unsigned int rt_priority = current->rt_priority;
22+
23+       /*
24+       * If the caller task calls the tasklet_kill when it has the rt policy,
25 +       * the task_kill function can cause an infinite wait state.
26 +       */
27 +       if (is_rt_policy(policy)) {
28 +               //If task has the rt policy, set the sched policy to normal
29 +               struct sched_param param = { .sched_priority = 0 };
30 +               sched_setscheduler_nocheck(current, SCHED_NORMAL, &param);
31 +               WARN("task policy was changed to normal.");
32 +       }
33
34        atomic_set(&tasklet->tasklet_active, 0);
35        tasklet_kill(&tasklet->tasklet);
36        tasklet_disable(&tasklet->tasklet);
37        devie_tasklet_flush(tasklet);
38 +
39 +       if (is_rt_policy(policy)) {
40 +               //If task had the rt policy, set the sched policy to rt
41 +               struct sched_param param = { .sched_priority = rt_priority };
42 +               sched_setscheduler_nocheck(current, policy, &param);
43 +               WARN( "task policy was changed to rt.");
44 +       }
45 +
46 }
47
48  /*

+ is_rt_policy() 함수를 호출해서 current 프로세스가 RT 프로세스인지 점검합니다.
  ; 만약 RT 프로세스이면 SCHED_NORMAL 으로 프로세스 정책을 설정해서 CFS 클래스 프로세스로 구동하게 합니다.
  ; 다른 프로세스들이 실행할 수 있는 기회를 주려는 것입니다.
27 +       if (is_rt_policy(policy)) {
28 +               //If task has the rt policy, set the sched policy to normal
29 +               struct sched_param param = { .sched_priority = 0 };
30 +               sched_setscheduler_nocheck(current, SCHED_NORMAL, &param);
31 +               WARN("task policy was changed to normal.");
32 +       }  
  
  
+ 디바이스를 태스크릿으로 처리하는데 태스크릿을 잠시 disable합니다.
34        atomic_set(&tasklet->tasklet_active, 0);
35        tasklet_kill(&tasklet->tasklet);
36        tasklet_disable(&tasklet->tasklet);
37        devie_tasklet_flush(tasklet);

+ 다시 is_rt_policy() 함수를 호출해서 current 프로세스가 RT 프로세스이면 이전에 설정했던 policy 값으로 스케줄링 정책을 설정합니다.  
39 +       if (is_rt_policy(policy)) {
40 +               //If task had the rt policy, set the sched policy to rt
41 +               struct sched_param param = { .sched_priority = rt_priority };
42 +               sched_setscheduler_nocheck(current, policy, &param);
43 +               WARN( "task policy was changed to rt.");
44 +       }

[리눅스커널][가상파일시스템] 수퍼블록: 슈퍼블록 정보를 statfs 시스템 콜로 읽는 과정 살펴보기 14장. 가상 파일시스템

슈퍼블록 각 멤버들은 파일시스템에 대한 메타 정보를 저장합니다.
유저 공간에서 파일시스템 정보를 알려면 어떤 함수를 호출해야 할까요?

유저 공간에서 statfs() 함수를 호출하면 커널 공간에서 해당 시스템 콜 핸들러 함수인 sys_statfs() 함수를 실행합니다.

예제 코드는 다음과 같습니다.
1 #define FILENAME_NAME "/home/pi"
2 #define BUFF_SIZE 256
3 int main() 
4 {
5 struct statfs file_sys_info;
6 char fname[BUFF_SIZE] = {0,};
7
8 strcpy(fname, FILENAME_NAME);
9
12 if(statfs(fname, &file_sys_info)) {
13 printf("Unable to statfs %s \n", fname);
14 exit(1);
15 }
16
17 return 0;
18}

12번째 줄과 같이 statfs() 함수 첫 번째 인자로 디렉토리 경로와 struct statfs 구조체인 file_sys_info 변수를 지정하면 파일시스템 속성 정보를 읽을 수 있는 것입니다.
12 if(statfs(fname, &file_sys_info)) {

유저 공간에서 statfs() 함수를 호출하면 커널 공간에서 해당 시스템 콜 핸들러 함수인 sys_statfs() 함수를 실행합니다.

이후 커널 공간에서 실행하는 함수 흐름은 다음 ftrace로 확인할 수 있습니다.
vfs_stat_fs-1990 [003] 12724.178684: sys_enter: NR 99 (7ecad4e8, 7ecad5e8, 7ecad5e8, 7ecad4e8, 10620, 0)
 vfs_stat_fs-1990 [003] 12724.178698: ext4_statfs+0x14/0x390 <-statfs_by_dentry+0x54/0x78
 vfs_stat_fs-1990  [003] 12724.178733: <stack trace>
 => user_statfs+0x54/0x90
 => SyS_statfs+0x24/0x40
 => __sys_trace_return+0x0/0x10
 vfs_stat_fs-1990  [003] 12724.178744: sys_exit: NR 99 = 0

99번째 시스템 콜 핸들러 함수는 다음 해더 파일에서 확인할 수 있습니다.
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]     
#define __NR_statfs (__NR_SYSCALL_BASE+ 99)


이번에는 sys_statfs() 함수 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
asmlinkage long sys_statfs(const char __user * path,
struct statfs __user *buf);

다음 각각 함수에 전달하는 인자를 확인합시다.
const char __user * path;

디렉토리나 파일 경로를 의미합니다. 
*path는 "/home/pi" 혹은 "/home/pi/sample_text.txt" 가 될 수 있는 것입니다.

struct statfs __user *buf;

유저 공간에 저장할 파일시스템 속성 정보 구조체가 있는 메모리 버퍼 주소입니다.

이번에는 sys_statfs() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 SYSCALL_DEFINE2(statfs, const char __user *, pathname, struct statfs __user *, buf)
2 {
3 struct kstatfs st;
4 int error = user_statfs(pathname, &st);
5 if (!error)
6 error = do_statfs_native(&st, buf);
7 return error;
8 }

sys_statfs() 함수 동작은 크게 2단계로 나눌 수 있습니다.

1단계: statfs 슈퍼블록 오퍼레이션 함수 호출
파일시스템에서 슈퍼블록 함수 오퍼레이션으로 statfs 멤버로 지정한 함수를 호출합니다.

2단계: 유저 공간에 파일시스템 정보 저장
do_statfs_native() 함수를 호출해서 슈퍼블록 객체에서 관리하는 속성 정보를 저장합니다.

여기서 기억할 포인트는 struct kstatfs 구조체는 커널 공간에서 파일시스템 메타 정보를 관리하는 구조체이고, struct statfs는 유저 공간에서 파일시스템 정보를 읽는 구조체이란 점입니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 int user_statfs(const char __user *pathname, struct kstatfs *st)
2 {
3 struct path path;
4 int error;
5 unsigned int lookup_flags = LOOKUP_FOLLOW|LOOKUP_AUTOMOUNT;
6 retry:
7 error = user_path_at(AT_FDCWD, pathname, lookup_flags, &path);
8 if (!error) {
9 error = vfs_statfs(&path, st);

6번째 줄 코드를 보겠습니다.
7 error = user_path_at(AT_FDCWD, pathname, lookup_flags, &path);

파일 경로에 대한 예외 처리를 수행한 후 파일 경로에 맞는 덴트리 정보가 포함된 정보를 path(struct path) 지역변수로 읽습니다. 이후 8번째 줄 코드와 같이 vfs_statfs() 함수를 호출합니다.

유저 공간에서 지정한 파일 경로가 유효한지 점검한 후, vfs_statfs() 함수를 호출하는 것입니다.

다음으로 vfs_statfs() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 int vfs_statfs(const struct path *path, struct kstatfs *buf)
2 {
3 int error;
4
5 error = statfs_by_dentry(path->dentry, buf);

vfs_statfs() 함수 5번째 줄에서 statfs_by_dentry() 함수를 호출합니다.

statfs_by_dentry() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 static int statfs_by_dentry(struct dentry *dentry, struct kstatfs *buf)
2 {
3 int retval;
4
5 if (!dentry->d_sb->s_op->statfs)
6 return -ENOSYS;
7
8 memset(buf, 0, sizeof(*buf));
9 retval = security_sb_statfs(dentry);
10 if (retval)
11 return retval;
12 retval = dentry->d_sb->s_op->statfs(dentry, buf);

5~6번째 줄 코드를 봅시다.
5 if (!dentry->d_sb->s_op->statfs)
6 return -ENOSYS;

덴트리 객체로 얻어온 슈퍼블락 객체 함수 오퍼레이션으로 statfs 멤버가 지정돼있지 않으면 -ENOSYS를 반환하고 함수 실행을 종료합니다.

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

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

ext4 파일시스템에 대한 정보를 채워주는 함수를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/ext4/super.c]
static int ext4_statfs(struct dentry *dentry, struct kstatfs *buf)
{
struct super_block *sb = dentry->d_sb;
struct ext4_sb_info *sbi = EXT4_SB(sb);
struct ext4_super_block *es = sbi->s_es;
ext4_fsblk_t overhead = 0, resv_blocks;
...
buf->f_bsize = sb->s_blocksize;
buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);
...
buf->f_bfree = EXT4_C2B(sbi, max_t(s64, bfree, 0));
buf->f_bavail = buf->f_bfree -
(ext4_r_blocks_count(es) + resv_blocks);

struct kstatfs 구조체인 buf이란 포인터에 파일시스템 메타 정보를 저장하는 것입니다.

ext4 파일시스템에 대한 세부 동작은 이 책의 범위를 넘어섭니다.
각 파일시스템별로 슈퍼블락 함수 오퍼레이션으로 statfs 멤버를 지정했으면 해당 함수가 호출된다는 사실을 기억합시다.

이번에는 sys_statfs() 함수 분석으로 되돌아가서 2단계 코드를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 SYSCALL_DEFINE2(statfs, const char __user *, pathname, struct statfs __user *, buf)
2 {
3 struct kstatfs st;
4 int error = user_statfs(pathname, &st);
5 if (!error)
6 error = do_statfs_native(&st, buf);
7 return error;
8 }

user_statfs() 함수를 호출하면 서부 루틴으로 statfs_by_dentry() 함수를 호출해서 각 파일시스템별로 지정한 슈퍼블락 함수 멤버로 statfs를 실행했습니다. 
     vfs_stat_fs-10633 [001] ...1  2950.414366: ext4_statfs+0x28/0x240 <-statfs_by_dentry+0x78/0x9c
     vfs_stat_fs-10633 [001] ...1  2950.414379: <stack trace>
 => vfs_statfs+0x28/0xbc
 => user_statfs+0x60/0xb0
 => SyS_statfs+0x38/0x70

do_statfs_native() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 static int do_statfs_native(struct kstatfs *st, struct statfs __user *p)
2 {
3 struct statfs buf;
...
4 buf.f_type = st->f_type;
5 buf.f_bsize = st->f_bsize;
6 buf.f_blocks = st->f_blocks;
7 buf.f_bfree = st->f_bfree;
8 buf.f_bavail = st->f_bavail;
9 buf.f_files = st->f_files;
10 buf.f_ffree = st->f_ffree;
11 buf.f_fsid = st->f_fsid;
12 buf.f_namelen = st->f_namelen;
13 buf.f_frsize = st->f_frsize;
14 buf.f_flags = st->f_flags;
15 memset(buf.f_spare, 0, sizeof(buf.f_spare));
16 }
17 if (copy_to_user(p, &buf, sizeof(buf)))
18 return -EFAULT;
19 return 0;
20 }

4~14번째 줄 코드를 보겠습니다.
struct kstatfs 구조체인 *st 포인터에 저장된 각각 멤버를 struct statfs 구조체인 *buf 인자 멤버에 복사합니다.

다음 17번째 줄 코드를 분석하겠습니다.
17 if (copy_to_user(p, &buf, sizeof(buf)))
18 return -EFAULT;

유저 공간에 struct statfs 구조체가 있는 메모리 버퍼 주소인 *p에 *buf 인자에 복사 합니다.

[리눅스커널][가상파일시스템] 수퍼블록: 슈퍼블록 함수 연산과 시스템 콜 연동 동작 알아보기 14장. 가상 파일시스템

이번 시간에는 슈퍼블록 함수 오퍼레이션이 시스템 콜과 어떻게 연동해서 실행하는지 살펴봅니다.

alloc_inode

파일시스템별로 inode 객체를 생성할 때 호출합니다. 파일시스템별 기능별로 파일을 관리하는 메타 정보도 업데이트합니다.

[선언부]
struct inode *(*alloc_inode)(struct super_block *sb);

파일시스템별로 inode 객체를 생성할 때 호출합니다. 파일시스템별 기능별로 파일을 관리하는 메타 정보도 업데이트합니다.

슈퍼 블락 함수 오퍼레이션으로 alloc_inode 함수 포인터는 다음 alloc_inode() 함수 6번째 줄 코드에서 실행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/inode.c]
1 static struct inode *alloc_inode(struct super_block *sb)
2 {
3 struct inode *inode;
4
5 if (sb->s_op->alloc_inode)
6 inode = sb->s_op->alloc_inode(sb);

슈퍼 블락 함수 오퍼레이션 멤버와 함수 이름이 같습니다.

alloc_inode() 함수는 언제 호출될까요?
보통 파일을 생성하는 과정에서 sys_open() 시스템 콜 핸들러 하부 루틴으로 alloc_inode() 함수가 실행됩니다.

destroy_inode

아이노드 객체와 각 파일시스템별로 지정한 세부 데이터를 삭제합니다.

[선언부]
void (*destroy_inode)(struct inode *);

슈퍼블락 함수 오퍼레이션 멤버인 destory_inode 함수 포인터는 언제 실행할까요? 다음 destory_inode() 함수 6번째 줄 코드에서 실행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/inode.c]
1 static void destroy_inode(struct inode *inode)
2 {
3 BUG_ON(!list_empty(&inode->i_lru));
4 __destroy_inode(inode);
5 if (inode->i_sb->s_op->destroy_inode)
6 inode->i_sb->s_op->destroy_inode(inode);

destroy_inode() 함수는 파일을 삭제할 때 호출됩니다.

dirty_inode

보통 파일 내용이 변경됐을 때 아이노드에 수정 표시를 합니다. 이 과정에서 호출됩니다. 주로 Ext4처럼 디스크에 저장된 저널을 업데이트하는 파일시스템에서 사용합니다. 

[선언부]
void (*dirty_inode) (struct inode *, int flags);

destroy_inode 슈퍼블락 오퍼레이션은 다음 __mark_inode_dirty() 함수에서 실행됩니다.
1 void __mark_inode_dirty(struct inode *inode, int flags)
2 {
3 #define I_DIRTY_INODE (I_DIRTY_SYNC | I_DIRTY_DATASYNC)
4 struct super_block *sb = inode->i_sb;
5 int dirtytime;
6
7 trace_writeback_mark_inode_dirty(inode, flags);
8
9 if (flags & (I_DIRTY_SYNC | I_DIRTY_DATASYNC | I_DIRTY_TIME)) {
10 trace_writeback_dirty_inode_start(inode, flags);
11
12 if (sb->s_op->dirty_inode)
13 sb->s_op->dirty_inode(inode, flags);

13번째 줄 코드를 보면 슈퍼블락 파일 오퍼레이션 정보인 s_op에서 dirty_inode 함수 포인터를 실행합니다.

write_inode

첫 번째 인자로 전달되는 아이노드 객체의 내용으로 세부 파일시스템 정보를 업데이트합니다.
여기에서 inode 객체의 i_ino 필드는 디스크에 있는 파일시스템 inode를 가리킵니다. flag 파라미터는 I/O 연산을 동기적으로 처리해야 하는지를 표시합니다. 
[선언부]
int (*write_inode) (struct inode *, struct writeback_control *wbc);

슈퍼 블락 write_inode 함수 오퍼레이션은 write_inode() 함수에서 실행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/fs-writeback.c]
1 static int write_inode(struct inode *inode, struct writeback_control *wbc)
2 {
3 int ret;
4
5 if (inode->i_sb->s_op->write_inode && !is_bad_inode(inode)) {
6 trace_writeback_write_inode_start(inode, wbc);
7 ret = inode->i_sb->s_op->write_inode(inode, wbc);
8 trace_writeback_write_inode(inode, wbc);
9 return ret;
10 }
11 return 0;
12}

write_inode() 함수 7번째 줄 코드를 보면 슈퍼 블락 함수 오퍼레이션 멤버인 s_op에서 write_inode() 함수 포인터를 실행합니다.

drop_inode

가상 파일시스템에서 해당 아이노드를 참조하지 않을 때 호출됩니다. 대부분 파일시스템에서 geteric_drop_inode() 함수를 호출해 아이노드를 삭제합니다. 

[선언부]
int (*drop_inode) (struct inode *);

put_super
파일시스템은 언마운트한 후 슈퍼 블록 객체를 제거할 때 호출됩니다.

[선언부]
void (*put_super) (struct super_block *);

슈퍼 블락 put_super 함수 오퍼레이션은 다음 코드와 같이 generic_shutdown_super() 함수에서 실행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/super.c]
1 void generic_shutdown_super(struct super_block *sb)
2 {
3 const struct super_operations *sop = sb->s_op;
4
5 if (sb->s_root) {
..
6 if (sop->put_super)
7 sop->put_super(sb);

6번째 줄 코드에서 슈퍼 블락 오퍼레이션 멤버 중 put_super에 함수 포인터가 지정됐는지 점검합니다. put_super에 함수 포인터가 지정됐으면 7번째 줄 코드와 같이 해당 함수를 호출합니다.

sync_fs

파일시스템 상세 정보인 메타 데이터를 디스크에 저장된 자료구조와 동기화를 맞출 때 실행합니다. 보통 ext4와 같은 저널링 파일시스템에서 사용됩니다.

[선언부]
int (*sync_fs)(struct super_block *sb, int wait);

슈퍼 블락 sync_fs 함수 오퍼레이션은 sync_fs_one_sb() 혹은 __sync_filesystem() 함수에서 실행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/sync.c]
1 static void sync_fs_one_sb(struct super_block *sb, void *arg)
2 {
3 if (!sb_rdonly(sb) && sb->s_op->sync_fs)
4 sb->s_op->sync_fs(sb, *(int *)arg);
5 }
6
7 static int __sync_filesystem(struct super_block *sb, int wait)
8 {
9 if (wait)
10 sync_inodes_sb(sb);
11 else
12 writeback_inodes_sb(sb, WB_REASON_SYNC);
13
14 if (sb->s_op->sync_fs)
15 sb->s_op->sync_fs(sb, wait);
16 return __sync_blockdev(sb->s_bdev, wait);
17}

sync_fs_one_sb() 함수와 __sync_filesystem() 함수 코드 각각 4번째와 15번째 줄 코드를 눈여겨봅시다. 둘다 sync_fs 멤버에 함수 포인터가 지정됐는지 점검 후 함수 포인터에 지정된 함수를 호출합니다.

statfs

파일시스템 통계 정보를 읽으려 할 때 호출됩니다. 지정한 파일시스템 통계 정보는 struct kstatfs 구조체에 저장됩니다.

[선언부]
int (*statfs) (struct dentry *, struct kstatfs *);

슈퍼 블락 sync_fs 함수 오퍼레이션은 유저 공간에서 statfs 함수를 호출하면 시스템 콜을 실행한 다음에 vfs_statfs() 함수 호출로 수행됩니다.

vfs_statfs() 함수와 statfs_by_dentry() 함수 코드를 분석합시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/statfs.c]
1 int vfs_statfs(const struct path *path, struct kstatfs *buf)
2 {
3 int error;
4
5 error = statfs_by_dentry(path->dentry, buf); 
...
6
7 static int statfs_by_dentry(struct dentry *dentry, struct kstatfs *buf)
8 {
...
9 memset(buf, 0, sizeof(*buf));
10 retval = security_sb_statfs(dentry);
11 if (retval)
12 return retval;
13 retval = dentry->d_sb->s_op->statfs(dentry, buf);  
14 if (retval == 0 && buf->f_frsize == 0)
15 buf->f_frsize = buf->f_bsize;
16 return retval;
17}

vfs_statfs() 함수는 5번째 줄 코드와 같이 statfs_by_dentry() 함수를 호출합니다. 
statfs_by_dentry() 함수 13번째 줄 코드에서 statfs 함수 포인터를 실행합니다.

remount_fs

가상 파일시스템이 파일시스템을 새로운 마운트 옵션으로 다시 마운트할 때 호출합니다.

[선언부]
int (*remount_fs) (struct super_block *, int *, char *);

해당 동작은 do_remount_sb() 함수 6번째 줄 코드에서 수행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/super.c]
1 int do_remount_sb(struct super_block *sb, int sb_flags, void *data, int force)
2 {
3 int retval;
4 int remount_ro;
..
5 if (sb->s_op->remount_fs) {
6 retval = sb->s_op->remount_fs(sb, &sb_flags, data);
..
7 }

umount_begin

가상 파일시스템이 마운트 동작을 중단할 때 호출됩니다.

[선언부]
void (*umount_begin) (struct super_block *);

관련 동작은 do_umount() 함수 7번째 줄 코드에서 수행합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/namespace.c]
1 static int do_umount(struct mount *mnt, int flags)
2 {
3 struct super_block *sb = mnt->mnt.mnt_sb;
4 int retval;
...
5
6 if (flags & MNT_FORCE && sb->s_op->umount_begin) {
7 sb->s_op->umount_begin(sb);
8 }

여기까지 슈퍼 블락 함수 오퍼레이션 멤버의 특징과 해당 함수 오퍼레이션이 수행하는 코드를 살펴봤습니다. 모두 각각 파일시스템별로 아이노드를 생성 및 제거하는 동작에서 파일시스템 재마운트 동작까지 슈퍼 블락 함수 오퍼레이션으로 지원합니다.

실제 슈퍼 블락 함수 오퍼레이션이 제대로 실행하는지 알려면 리눅스 커널에서 실제 함수 호출 흐름을 눈으로 확인할 필요가 있습니다. 하지만 슈퍼 블락 함수 오퍼레이션 동작은 대부분 그 종류만 알아도 될만한 내용들이 많습니다.

다음 소절에서는 슈퍼 블락 함수 오퍼레이션 중 시스템 콜과 연동되서 자주 실행되는 동작을 선택해서 살펴보겠습니다.

[IT] 사내 정치(Politics)을 깰 수 있는 오픈 소스 커뮤니티 에세이

오픈소스 프로젝트는 개발 지형과 인프라 그리고 무엇보다 개발자의 운명을 송두리째 뒤바꿔 놨다.
요즘 시대 오픈소스 프로젝트는 당연하게 생각하는 경우가 많다. 사실 이는 오픈소스의 선두 주자인 리눅스 커널 커뮤니티가 기폭제가 된 것 같다.

오픈 소스가 개발자의 운명을 어떻게 바꿔 놨는지 조금 되돌아 보자.

소스 코드의 권력화와 사내 폴리틱스 

나는 15년 미들웨어 플랫폼을 개발한 적이 있다. 지금 안드로이드 구조로 보면 프레임 워크로 surface flinger와 비슷한 것이었다. 물론 당시 오픈 소스는 아니었다. 사내에서 개발한 소스 코드로 개발했다.

재밌는 광경을 목격했다. 그것은 소스 코드의 권력화! 
조직에서 인정받는 몇몇의 개발자가 소스 코드를 독점 하는것이었다. 조금 구체적으로 말하면,
핵심 개발자만 코드를 수정할 수 있고 심지어 특정 루틴의 소스 코드는 그들만 볼 수 있었다. 

개발실에서 인정받는 몇몇의 개발자들이 코드를 책임진다는 명분이었다. 한 다섯 명 정도 코드를 볼 수 있었던 것 같다. 사실 다섯 명 개발자 중에 한 명은 정말 실력이 좋았다. 안타깝게도 나머지 2~3명 개발자는 다른 개발자와 실력이 비슷했다. 하지만 그들은 아가리 파이터였다. 
오랄 코딩, 오랑 디버깅의 황제들이었다. 회의실에서 그들은 리누스 토발즈보다 더 말을 잘했다.
돌대가리 관리자들은 아가리 파이터가 엄청난 실력의 소유자라고 믿었다.

이렇게 사내 권력을 이용해 소스를 독점적으로 관리하는 것이었다. 어떤 문제가 나오면 5명이 모여서 수근 수근 하다가 조금 후에 라이브러리 파일이 나왔다. 다른 개발자들이 라이브러리를 호출해 5명이 만든 함수를 쓰라는 것이었다. 

정말 긍정적으로 보면 핵심 개발자들만 소스 코드를 보고 수정할 수 있다는 것은 어찌보면 소스 코드에 대한 책임감을 더 진다고 볼 수 있다. 문제는 다른 개발자들이 소스 코드를 수정할 수 없다는 것이었다. 개발자들에게 소스코드 수정할 수 있는 자유를 뺐는 것은 많은 것을 의미한다. 단지 소스 코드는 분석을 위한 것이란 생각에 소스 코드를 보면서 개선 포인트를 찾지 않게 된다. 소스 코드를 깊게 볼 의지를 꺾는 게 아닌가 싶었다.

개발자 커뮤니티에 가면 여러 개발자가 사내 정치의 희생양이 된 슬픈 스토리를 볼 수 있다.
이를 유식하게 사내 폴리틱스(Politics)라고도 말한다. 고수 개발자들은 오타꾸 같은 기질이 있어 관리자들이 다루기 쉽지 않는 것은 사실이긴 하다. 

문제는 회사를 옮기려고 해도 이전 회사에서 일했던 결과물을 알리기도 어렵고 여러가지 방식으로 까다로운 실력 검증 절차를 거쳐야 한다. 또한 소스 코드가 오픈돼 있지 않은 상태라 각각 소프트웨어 회사마다 개발 툴과 아키텍처가 다르니 경험을 살리기도 어려운 상태였다.

오픈 소스는 사내 폴리틱스를 깰 수 있는 채널 

이런 악 조건에서 오픈 소스는 모든 사내 폴리틱스를 깨버릴 수 있는 채널이 되었다. 
개발자 스스로 자신은 개발 능력과 내공이 있는데 회사에서 자신을 벌레 취급하면 오픈 소스 활동을 하면 된다. 물론 개인 시간을 좀 내야 하지만 말이다.

웹킷이나 리눅스 커널 오픈 소스 커뮤니티에 자신의 코드가 반영되면 자연히 자신의 내공을 전 세계에 알릴 수 있다.
이 과정에서 다른 회사에서 스카우트 제의가 많이 오는 경우도 많다.
   
오픈소스 활동은 개발자의 성공과 이력 관리를 위해서도 필요하지만 굉장히 즐겁게 즐길 수 있는 취미가 될 수 있다.
유튜브나 아프리카 채널 보듯이 커뮤니티에 가서 테크니컬한 리뷰도 하는 것이다. 

이름만 대면 알만한 유명 네트워크 장비 업체 일했던 친구가 최근 회사에 그만둔다고 통보를 했다.
6개월 동안 리눅스 커널 오픈 소스 활동을 하고 다른 회사에 취업을 하려고 했다. 회사에 그만둔다고 했더니 그 회사에서월급(유급 휴가)을 줄 테니 3개월 동안 좀 쉬고 오라고 해서 다시 복귀를 했다. 참 대단한 친구다. 

새로운 채널이자 기회인 유튜브 

요즘엔 사실 오픈 소스 커뮤니티 보다 더 좋은 지식 채널이 생겼다. 이는 유튜브다. 유튜브에 자신이 알고 있는 노하우나 지식을 올릴 수 있지 않은가? 공부도 되고 굉장히 좋은 플랫폼이라 생각이 든다. 하지만 개발자들이 생각보다 유튜브를 잘 활용하지 않는 거 같다.

사내 폴리틱스에 휘둘려 빛을 보지 못한 개발자들이 오픈소스 활동으로 날개를 달기를 바라는 마음이다.


[리눅스커널][시그널] 시그널 전달과 처리는 어떻게 할까? 12장. 시그널

이번 소절에서 분석할 소스 코드를 보면 리눅스 커널이 배경 작업으로 시그널 처리를 위해 얼마나 정교하게 코드 구현이 됐는지 알 수 있습니다.

slow_work_pending 레이블에서 do_work_pending() 함수를 브랜치하는 코드를 이전 소절에서 알아봤습니다. 이번에는 do_work_pending() 함수부터 시그널을 처리하는 handle_signal() 함수까지 살펴봅니다.

get_signal() 함수 분석

get_signal() 함수를 분석하기 앞서 do_work_pending() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/signal.c]
1 asmlinkage int
2 do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
3 {
4 trace_hardirqs_off();
5 do {
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();
8 } else {
9 if (unlikely(!user_mode(regs)))
10 return 0;
11 local_irq_enable();
12 if (thread_flags & _TIF_SIGPENDING) {
13 int restart = do_signal(regs, syscall);

다시 한번 current_thread_info() 로 thread_flags 멤버를 읽어 _TIF_SIGPENDING이면 do_signal() 함수를 호출합니다. 프로세스가 시그널을 받을 때 가장 먼저 실행하는 함수는 do_signal() 입니다.

do_signal() 함수 코드를 읽기 전에 이 함수에 전달하는 인자를 점검합시다.
struct pt_regs *regs: 프로세스 최하단 스택 공간에 푸시한 유저 프로세스 레지스터 세트
int syscall: 시스템 콜 테이블 주소

만약 시스템 콜 핸들러를 실행한 후 ret_fast_syscall 레이블로 복귀해서 do_signal() 함수를 실행할 경우, syscall 시스템 콜 테이블 주소를 갖고 있습니다.

다음은 do_signal() 함수 구현부입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/signal.c]
1 static int do_signal(struct pt_regs *regs, int syscall)
2 {
3 unsigned int retval = 0, continue_addr = 0, restart_addr = 0;
4 struct ksignal ksig;
5 int restart = 0;
6
7 if (syscall) {
8 continue_addr = regs->ARM_pc;
9 restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
10 retval = regs->ARM_r0;
11
12 switch (retval) {
13 case -ERESTART_RESTARTBLOCK:
14 restart -= 2;
15 case -ERESTARTNOHAND:
16 case -ERESTARTSYS:
17 case -ERESTARTNOINTR:
18 restart++;
19 regs->ARM_r0 = regs->ARM_ORIG_r0;
20 regs->ARM_pc = restart_addr;
21 break;
22 }
23 }
24
25 if (get_signal(&ksig)) {
26 /* handler */
27 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
28 if (retval == -ERESTARTNOHAND ||
29     retval == -ERESTART_RESTARTBLOCK
30     || (retval == -ERESTARTSYS
31 && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
32 regs->ARM_r0 = -EINTR;
33 regs->ARM_pc = continue_addr;
34 }
35 }
36 handle_signal(&ksig, regs);
37 } else {
38 /* no handler */
39 restore_saved_sigmask();
40 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
41 regs->ARM_pc = continue_addr;
42 return restart;
43 }
44 }
45 return 0;
46}

먼저 7번째 줄 코드를 보겠습니다.
7 if (syscall) {
8 continue_addr = regs->ARM_pc;
9 restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
10 retval = regs->ARM_r0;
11
12 switch (retval) {
13 case -ERESTART_RESTARTBLOCK:
14 restart -= 2;
15 case -ERESTARTNOHAND:
16 case -ERESTARTSYS:
17 case -ERESTARTNOINTR:
18 restart++;
19 regs->ARM_r0 = regs->ARM_ORIG_r0;
20 regs->ARM_pc = restart_addr;
21 break;
22 }
23 }

유저 공간에서 실행 중인 레지스터 중 ARM_pc 레지스터를 로딩해서 continue_addr 변수에 저장한 후, restart_addr 지역 변수에 다시 저장합니다.

유저 공간에서 실행 중인 프로그램 카운터 주소 정보는 ARM_pc 멤버에 저장돼 있습니다.
이 주소에서 ARM 모드에 따라 2 혹은 4를 빼서 유저 공간에서 다시 실행할 프로그램 카운터 주소를 보정합니다.

ARM 프로세서는 파이프 라인을 적용한 아키텍처라 프로그램 카운터는 실제 실행 중인 함수 주소에서 +4만큼 큽니다. 그래서 다시 실행할 함수 주소에서 4를 빼서 보정하는 것입니다.

25~46번째 줄 코드를 보겠습니다. 이 코드는 크게 2단계로 나눌 수 있습니다.
25 if (get_signal(&ksig)) {
26 /* handler */
27 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
28 if (retval == -ERESTARTNOHAND ||
29     retval == -ERESTART_RESTARTBLOCK
30     || (retval == -ERESTARTSYS
31 && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
32 regs->ARM_r0 = -EINTR;
33 regs->ARM_pc = continue_addr;
34 }
35 }
36 handle_signal(&ksig, regs);
37 } else {
38 /* no handler */
39 restore_saved_sigmask();
40 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
41 regs->ARM_pc = continue_addr;
42 return restart;
43 }
44 }
45 return 0;
46}

25번째 줄 코드에서 get_signal() 함수를 호출한 후 반화하는 값에 따라 실행 흐름이 나뉘게 됩니다. 27~36번째 줄 코드는 시그널 핸들러를 지정했을때 실행하고, 37~44번째 줄 코드는 시그널 핸들러를 지정하지 않았을 때 실행합니다.

다음 get_signal() 함수를 보겠습니다. get_signal() 함수 앞부분과 중간에 수 많은 예외 처리 코드가 있는데, 시그널을 핵심 코드 조각을 모아 봤습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 int get_signal(struct ksignal *ksig)
2 {
3 struct sighand_struct *sighand = current->sighand;
4 struct signal_struct *signal = current->signal;
5 int signr;
...
6 for (;;) {
7 struct k_sigaction *ka;
...
8 signr = dequeue_signal(current, &current->blocked, &ksig->info);
9
10 if (!signr)
11 break; /* will return 0 */
...
12 ka = &sighand->action[signr-1];
13
14 /* Trace actually delivered signals. */
15 trace_signal_deliver(signr, &ksig->info, ka);
16
17 if (ka->sa.sa_handler == SIG_IGN) /* Do nothing.  */
18 continue;
19 if (ka->sa.sa_handler != SIG_DFL) {
20 /* Run the handler.  */
21 ksig->ka = *ka;
22
23 if (ka->sa.sa_flags & SA_ONESHOT)
24 ka->sa.sa_handler = SIG_DFL;
25
26 break; /* will return non-zero "signr" value */
27 }
28
29 if (sig_kernel_ignore(signr)) /* Default is nothing. */
30 continue;
...
31 current->flags |= PF_SIGNALED;
...
32 do_group_exit(ksig->info.si_signo);
33 /* NOTREACHED */
34 }
35 spin_unlock_irq(&sighand->siglock);
36
37 ksig->sig = signr;
38 return ksig->sig > 0;
39 }

8번째 줄 코드를 보겠습니다.
8 signr = dequeue_signal(current, &current->blocked, &ksig->info);
9
10 if (!signr)
11 break; /* will return 0 */

프로세스 태스크 디스크립터 구조체 struct task_struct 멤버 중 pending 혹은 signal->shared_pending로 펜딩된 시그널 정보를 &ksig->info로 저장하고 시그널 번호를 반환합니다.

만약 시그널 번호가 0이면 10~11번째 줄 코드와 같이 break 문을 실행해서 실행을 중단합니다.

12번째 줄 코드를 보겠습니다.
12 ka = &sighand->action[signr-1];

시그널 타입에 따라 시그널 핸들링 정보를 저장한 &sighand->action[] 배열에 접근해서 ka 지역 변수로 반환합니다.

다음은 ftrace 로그를 출력하는 코드입니다.
15 trace_signal_deliver(signr, &ksig->info, ka);

signal_deliver 이란 ftrace 이벤트를 켰을 때 실행하는 코드입니다.

signal_deliver 이벤트를 키면 다음과 같은 ftrace 로그를 확인할 수 있습니다.
signal_handle-12151 [001] d..1  6207.473891: signal_deliver: sig=2 errno=0 code=128 sa_handler=400398 sa_flags=10000000

시그널 핸들러 함수 주소와 전달하는 시그널 종류를 출력합니다.

19~26번째 줄 코드는 시그널 핸들러를 지정했으면 실행하는 코드입니다.
19 if (ka->sa.sa_handler != SIG_DFL) {
20 /* Run the handler.  */
21 ksig->ka = *ka;
22
23 if (ka->sa.sa_flags & SA_ONESHOT)
24 ka->sa.sa_handler = SIG_DFL;
25
26 break; /* will return non-zero "signr" value */
27 }
..
35 spin_unlock_irq(&sighand->siglock);
36
37 ksig->sig = signr;
38 return ksig->sig > 0;

21번째 줄 코드를 실행해서 시그널 정보를 저장한 다음 26번째 줄 코드를 실행해서 for(;;) 루프에서 빠져나와 37~38번째 줄 코드를 실행해서 1을 반환합니다. 보통 시그널 번호가 0보다 크니 1을 반환합니다.

유저 어플리케이션에서 시그널 별로 설정한 시그널 핸들러를 실행하기 위한 동작은 handle_signal() 함수에서 살펴보겠습니다.

다음 31번째 줄 코드를 보겠습니다.
31 current->flags |= PF_SIGNALED;
...
32 do_group_exit(ksig->info.si_signo);
33 /* NOTREACHED */

실행 중인 프로세스 태스크 디스크립터 flags 멤버에 PF_SIGNALED 매크로를 저장합니다. 
“|=” 연산자를 썼으니 이미 저장된 flags 멤버 값은 그대로 유지합니다.

32번째 줄 코드를 보면 do_group_exit() 함수를 호출합니다. 이 코드로 대부분 시그널 처리 결과는 프로세스 종료란 사실을 알 수 있습니다. do_group_exit() 함수를 호출해서 현재 실행 중인 프로세스와 프로세스가 속한 스레드 그룹 내 다른 프로세스들을 종료시킵니다.

리눅스 커널에서는 유저 프로세스 관점으로 생성된 스레드도 프로세스로 간주합니다. 그래서 커널 입장에서 위와 같이 표현한 것입니다. 유저 프로세스 관점으로는 해당 스레드와 스레그 그룹에 속한 스레드들을 종료한다라고 보면 됩니다.

33번째 줄 코드를 보면 “NOTREACHED” 이란 주석문을 볼 수 있습니다. do_group_exit() 함수는 해당 프로세스와 스레드 그룹에 소속한 다른 프로세스도 종료했으니 33번째 줄 코드는 다시 실행될 수 없기 때문입니다.

handle_signal() 함수 분석

handle_signal() 함수 분석에 앞서 어떤 조건으로 handle_signal() 함수가 호출하는지 코드로 살펴보겠습니다.
1 static int do_signal(struct pt_regs *regs, int syscall)
2 {
3 unsigned int retval = 0, continue_addr = 0, restart_addr = 0;
4 struct ksignal ksig;
...
5 if (get_signal(&ksig)) {
6 /* handler */
7 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
8 if (retval == -ERESTARTNOHAND ||
9     retval == -ERESTART_RESTARTBLOCK
10     || (retval == -ERESTARTSYS
11 && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
12 regs->ARM_r0 = -EINTR;
13 regs->ARM_pc = continue_addr;
14 }
15 }
16 handle_signal(&ksig, regs);

get_signal() 함수에서 시그널 정보에서 시그널 핸들러가 등록됐다는 정보를 확인 후 1을 반환합니다. 이후 16번째 줄 코드를 실행해서 handle_signal() 함수를 실행합니다.

handle_signal() 함수를 시작으로 유저 프로세스가 등록한 시그널 핸들러를 실행하는 함수 흐름은 다음과 같습니다.
 

[1] 단계
시그널을 처리하기 전에 해당 프로세스는 sys_pause() 함수에서 schedule() 함수를 호출해서 실행 준비 상태 였습니다. 시그널을 생성하면 시그널을 받을 프로세스를 깨우니 schedule() 함수에서 ret_fast_syscall() 함수로 실행합니다.

[2] 단계
프로세스는 시그널이 자신에게 전달됐다는 사실을 알아채고 시그널에 대한 처리를 수행합니다. get_signal() 함수에서 시그널 핸들러가 등록됐다는 사실을 파악한 후 handle_signal() 함수를 호출합니다.

setup_return() 함수에서는 시그널 정보에서 시그널 핸들러 함수 주소를 읽어서 유저 공간에서 복귀하면 바로 시그널 핸들러를 실행할 수 있게 ARM PC 레지스터에 시그널 핸들러 함수 주소를 써줍니다.

또한 시그널 핸들러 함수 실행 이후 다시 커널 공간으로 복귀해야 하니 R14 레지스터에 시스템 콜을 바로 실행하는 코드를 지정합니다.

[3] 단계
유저 모드로 복귀한 다음 시그널 핸들러를 실행합니다.

[4] 단계
시그널 핸들러를 실행하고 난 후 R14에 저장된 주소 코드를 실행합니다. 시스템 콜 번호 117를 r7 레지스터에 저장하고 시스템 콜을 발생해서 다시 커널 공간으로 이동합니다.

[5] 단계
커널 공간으로 다시 진입 한 후 sys_sigreturn() 함수를 실행해서 커널 공간 스택 프레임 정보를 복원합니다.

[6] 단계
시그널이 생성하기 전 상태 콜스택으로 되돌아가 다시 시그널을 기다립니다.

우리는 유저 어플리케이션에서 시그널 핸들러를 등록하면 당연히 시그널 핸들러가 제대로 실행된다고 가정하고 프로그램을 작성합니다. 커널이 유저 어플리케이션에서 등록한 시그널 핸들러를 실행하기 위해 배경 작업으로 많은 동작을 합니다.

무엇보다 이 과정에서 커널 스택 공간에 저장된 프레임 정보를 유저 공간에 백업한 후 다시 커널 공간으로 이동 후 복사하는 과정을 거칩니다. 그래서 ARM 아키텍처 관련 코드가 상당히 많아 이 동작을 이해하기 어렵습니다.

코드를 자세히 분석하기 전 전체 흐름을 먼저 파악할 필요가 있습니다.  

전체 흐름을 눈에 그리면서 소스 코드를 봅시다. 다음은 handle_signal() 함수 코드입니다.
1 static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
2 {
3 sigset_t *oldset = sigmask_to_save();
4 int ret;
5
6 /*
7  * Set up the stack frame
8  */
9 if (ksig->ka.sa.sa_flags & SA_SIGINFO)
10 ret = setup_rt_frame(ksig, oldset, regs);
11 else
12 ret = setup_frame(ksig, oldset, regs);
13
14 /*
15  * Check that the resulting registers are actually sane.
16  */
17 ret |= !valid_user_regs(regs);
18
19 signal_setup_done(ret, ksig, 0);
20 }

9~12번째 줄 코드를 보면 시그널 설정 상태에 따라 두 개 함수로 호출 흐름이 나뉩니다.
9 if (ksig->ka.sa.sa_flags & SA_SIGINFO)
10 ret = setup_rt_frame(ksig, oldset, regs);
11 else
12 ret = setup_frame(ksig, oldset, regs);

시그널 설정 플래그가 SA_SIGINFO이면 setup_rt_frame() 함수 이외에는 setup_frame() 함수를 호출합니다. 일반적으로 setup_frame() 함수를 호출하므로 setup_frame() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/signal.c]
1 static int
2 setup_frame(struct ksignal *ksig, sigset_t *set, struct pt_regs *regs)
3 {
4 struct sigframe __user *frame = get_sigframe(ksig, regs, sizeof(*frame));
5 int err = 0;
6
7 if (!frame)
8 return 1;
9
10 __put_user_error(0x5ac3c35a, &frame->uc.uc_flags, err);
11
12 err |= setup_sigframe(frame, regs, set);
13 if (err == 0)
14 err = setup_return(regs, ksig, frame->retcode, frame);
15
16 return err;
17 }

먼저 4~12번째 줄 코드를 분석하겠습니다.
4 struct sigframe __user *frame = get_sigframe(ksig, regs, sizeof(*frame));
5 int err = 0;
6
7 if (!frame)
8 return 1;
9
10 __put_user_error(0x5ac3c35a, &frame->uc.uc_flags, err);
11
12 err |= setup_sigframe(frame, regs, set);

4번째 줄 코드는 유저 공간 스택에 접근해서 프레임 정보를 읽습니다.

이후 12번째 줄 코드와 같이 setup_sigframe() 함수를 호출해서,
프로세스 최하단 스택 근처에 저장된 유저 모드 레지스터 세트(struct pt_regs *regs)를 유저모드 스택에 추가 공간을 할당합니다.

이는 기존 유저모드 실행 흐름으로 복귀하는 대신에 유저모드로 복귀하면 시그널 핸들러를 호출하기 위해서 입니다. 보통 시스템 콜 핸들러 처리 이 후 유저 모드로 복귀할 때 프로세스 최하단 스택에 푸시된 유저 모드에서 실행했던 레지스터 세트를 로딩해서 실행합니다.

이 동작을 마무리 한 후 14번째 줄 코드와 같이 setup_return() 함수를 호출해서 시그널 핸들러 함수를 설정합니다.

다음으로 볼 함수는 setup_return() 입니다.
1 static int
2 setup_return(struct pt_regs *regs, struct ksignal *ksig,
3      unsigned long __user *rc, void __user *frame)
4 {
...
5 regs->ARM_r0 = ksig->sig;
6 regs->ARM_sp = (unsigned long)frame;
7 regs->ARM_lr = retcode;
8 regs->ARM_pc = handler;
9 regs->ARM_cpsr = cpsr;
10
11 return 0;
12 }

유저 공간에 복귀하면 바로 실행할 레지스터 정보를 업데이트 합니다.
이는 유저 모드 레지스터 세트로 원래 실행 흐름으로 복귀하지 않고 시그널 핸들러를 실행하기 위함입니다.

ARM_lr 멤버에는 시그널 핸들러를 처리한 후 실행할 코드를 저장합니다. sigreturn 시스템 콜을 실행하게 준비된 코드를 가리키게 합니다. 시그널 핸들러로부터 복귀할 때 다음 어셈블리 코드가 실행하는 순간 시스템 콜을 유발해서 해당 프로세스가 다시 커널 공간으로 진입하도록 합니다.
mov pc, lr

시그널 핸들러 실행 후 유저 모드로 복귀할 코드는 다음과 같은데, sigreturn_codes를 배열로 관리해서 ARM 프로세스 모드에 따라 시스템 콜을 실행합니다.
extern const unsigned long sigreturn_codes[7];

1  80800414 <sigreturn_codes>:
2  80800414: e3a07077  mov r7, #119 ; 0x77
3  80800418: ef900077  svc 0x00900077
4  8080041c: 2777       movs r7, #119 ; 0x77
5  8080041e: df00       svc 0
6  80800420: e3a070ad  mov r7, #173 ; 0xad
7  80800424: ef9000ad  svc 0x009000ad
8  80800428: 27ad       movs r7, #173 ; 0xad
9  8080042a: df00       svc 0
10 8080042c: 00000000  andeq r0, r0, r0

일반적인 상황에서는 4~5번째 줄 코드를 실행해서 r7 레지스터에 시스템 콜 번호를 저장하고 "svc 0x0" 명령어를 실행해서 커널 모드로 진입합니다.

119번에 해당하는 시스템 콜은 sigreturn() 함수이며 커널 공간에서 sys_sigreturn() 함수를 실행합니다.
1 asmlinkage int sys_sigreturn(struct pt_regs *regs)
2 {
3 struct sigframe __user *frame;
...
4
5 frame = (struct sigframe __user *)regs->ARM_sp;
6
7 if (!access_ok(VERIFY_READ, frame, sizeof (*frame)))
8 goto badframe;
9
10 if (restore_sigframe(regs, frame))

10번째 줄 코드와 같이 restore_sigframe() 함수에서 실행합니다.

이전에 유저 모드 스택에 저장해놓은 (원래 프로세스 스택 최하단 주소에 저장했던) 유저 모드 레지스터 세트를 다시 프로세스 스택 최하단 주소에 다시 복구합니다. 이 동작은 restore_sigframe() 함수에서 실행합니다.

[IT][임베디드] 열심히 일을 할 수록 바보가 되는 SW 개발자 에세이

개발자의 개발 역량을 끌어 올릴 수 없는 잔업무에 시간을 쏟아 부으면 점점 바보가 되는 것 같다.
- 반복 테스트 실행
- 기계적인 커널 버전 git merge 마이그레이션 
- 아무 생각 없이 하는 기계적인 디버깅
  ; 코드를 넣다 뺏다.
  ; 모듈을 넣다 뺏다.
  ; 이전 버전으로 코드를 돌리고 테스트 
- 비생산적인 페이퍼 워크(Paper Work)
- cheery-pick으로 코드 가져오기

이런 일은 열심히 하면 할수록 좀비 바보 개발자가 된다.

그 이유는 이런 패턴의 일을 반복하면 머리가 단순해 지기 때문이다.

자기 개발서에선 별거 아닌 일도 최선을 다하라고 말한다. 그래서 이런 일들을 최선을 다해 열심히 하는 개발자를 종종 본다. 이런 일도 열심히 하고 코드를 입력하거나 디버깅하는 개발도 하면 좋지 않은가?라고 되물을 수도 있다. 문제는 이런 일은 열심히 하면 할 수록 바보가 된다는 것이다.

물론 어쩔 수 없이 돌대가리 관리자나 쓰레기 정치 조직에 파 뭍혀 이런 일을 하는 상황도 있긴 하다.
이럴 땐 개인 사이드 프로젝트로 이런 늪에서 돌파해야 한다.
 
예를 들어 리눅스 개발 역량을 꾸준히 키우려면 다음과 같은 일에 더 많은 시간을 투자해야 한다.
- 꾸준한 리눅스 커뮤니티 활동
- 리눅스 커널 Contribution
- 드라이버 코드 구현 및 개선
- 디버깅 Feature 구현 
- 디버깅 방법 연구 및 개발

일을 무작정 열심히 하는 것보다 어떤 일을 열심히 하면 바보가 되는지 생각해 보는 것도 중요하다.

[IT] 언제 SW 개발자들의 목소리가 쎄질까? 에세이

루비콘 강을 건너다란 말이 있다. 2천년 전 로마 시대 시저가 갈리아 원정을 성공하고 로마에 복귀할 때 했던 말이다.
원래 루비콘 강을 건너기 전에 시저는 군대를 해산해야 했다. 그게 로마 원로원에서 만든 법이었다.

로마 원로원들은 지금의 국회 의원 노릇을 하는 관료들로 구성돼 있었다. 그들은 교묘히 왕정 시대 도래를 견제하고 군인을 물 먹이는 존재들이었다.
해외 원정을 다녀온 장군이 사병을 그대로 거느리고 있으면 원로원의 힘이 약해질 것을 두려워 한 것이었다.

그런데 시저는 군대를 해산하지 않고 사병을 이끌고 그대로 로마로 진격했다.
현재 시점으로 시저는 쿠데타를 저지른 것이다. 군대를 이끌고 루비콘 강을 건너 시저는 폼페이우스와 내전을 겪는 시기로 돌아섰다.

그런데 이렇게 전쟁이 많이 일어나는 시대에서 대부분 군인들의 힘이 관료보다 쎈 경우가 많다. 
대부분 전쟁통에서는 군인들이 대통령이나 왕이 된 경우가 많다. 

하지만 전쟁이 끝나고 평화가 정착되면 군인들은 서서히 힘을 잃게 된다. 관료들의 힘이 쎄지는 것이다.
군인들은 안전 유지나 경찰 노릇만 하며 정치에 참여도 못한다.

그런데 이런 IT SW 동네도 비슷하다. 어느 SW 회사이던 개발자와 관리자나 스태프 부서가 있기 마련이다. 
기술 변화가 심한 시점에서 SW 개발자가 조금 더 목소리를 낼 수 있다. 대신 기술 변화가 없으면 관리자들의 힘이 쎄진다. 
군인 : 관료 = 개발자 : 관리자(스태프)

이런 공식으로 봐야 할까?

[IT] 소프트웨어 개발자에 있어 성공이란 무엇일까 에세이

우리는 신자본주의 시대에 살고 있다. 모든 직업도 연봉을 잣대로 평가하는 시대다.
사회 풍토가 그런 것 같다. 요즘 초등학생이 선망하는 직업이 내가 좋아하는 유튜버나 아프리카 BJ이니 말이다.

소프트웨어 개발자들도 마찬가지다. 연봉이나 인센티브를 얼마나 받는지에 대해 관심을 갖는다.
사실 직장인들이 회사에 나가는 이유는 간단하다. 돈을 벌기 위해서다. 

그러면 소프트웨어 개발자 혹은 SW 엔지니어에게 있어 성공이란 무엇일까?
연봉을 많이 받는 것일까? 연봉을 많이 받으면 얼마나 받아야 성공이라고 봐야 할까?

아니면 고수 개발자가 되는 것이 성공일까? 사실 정답이 없는 질문이긴 하다.

내 생각엔 나의 개발 능력으로 다른 사람들에게 좋은 영향을 끼칠 수 있는 것이 성공이라고 본다.
쉽게 말하면 다른 개발자들에게 도움을 줄 수 있는 개발자말이다. 
시야를 조금 넓혀서 개발자 뿐만 아니라 학생들에게도 지속적으로 도움을 주고 좋은 영향을 끼치는 것이다. 

어떤 회사에 임원이 있다고 가정하자. 그 임원은 연봉을 많이 받아 노후 걱정이 없는 상태다.
정년이 되기 2년 전에 엄청난 돈을 받고 은퇴를 한 후 세계 여행을 하고 있다고 치자. 물론 일은 안한다.
그럼 이걸 성공이라고 봐야 할까?

글쎄, 내 기준엔 아닌 것 같다. 
뭐 연봉 많이 받는 것도 나쁘진 않지만 "나의 영향력을 끼칠 수 없는 사회에 살고 있다."면 그리 행복하지는 않을 것 같다.

내 기준으로 미국 대통령들이 내가 생각하는 성공의 기준에 좀 맞아 떨어지는 듯 하다. 미국 전 대통령들은 퇴임 후 백악관에서 겪었던 경험을 살려 수 많은 강연과 세미나에서 발표를 한다고 한다. 얼마나 좋은가? 
(한국 전임 대통령들은 좀 측은하다. 거의 숨만 쉬면서 외부 활동을 안하거나 감옥에 있으니 말이다.)

[리눅스커널][SoftIRQ] 디버깅: /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인 6장. 인터럽트 후반부 처리

6.12.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인

proc 파일 시스템에서 /proc/softirqs 파일을 통해 Soft IRQ 종류 별 처리 횟수를 출력합니다. 이번 소절에서는 /proc/softirqs 파일 출력 결과와 이를 어떤 코드에서 실행하는지 살펴보겠습니다.

/proc/softirqs 출력 메시지 확인하기  

먼저 다음 명령어로 /proc/softirqs 파일 내용을 확인해봅시다.
root@raspberrypi:/home/pi# cat /proc/softirqs             
01                    CPU0       CPU1       CPU2       CPU3        
02          HI:          0          0          0          0             
03     TIMER:     133935     128454     132499     133430      
04   NET_TX:          0          0          0        361       
05   NET_RX:          0          0          0          0          
06    BLOCK:      13072      25079          0      11138       
07 IRQ_POLL:          0          0          0          0          
08  TASKLET:       2777     241748        763     230328          
09    SCHED:     109806     109155     111689     113882      
10 HRTIMER:          0          0          0          0           
11       RCU:      90942      90547      93124      95240         

Soft IRQ 서비스 종류별 실행 횟수를 확인할 수 있습니다.
TIMER Soft IRQ 서비스는 CPU0~CPU3에서 133935, 128454, 132499, 133430 번 실행했습니다.

Soft IRQ 서비스 처리 횟수를 리눅스 커널 어떤 코드에서 출력하는지 알아 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/proc/softirqs.c]
01 static int show_softirqs(struct seq_file *p, void *v)
02 {
03 int i, j;
04
05 seq_puts(p, "                    ");
06 for_each_possible_cpu(i)
07 seq_printf(p, "CPU%-8d", i);
08 seq_putc(p, '\n');
09
10 for (i = 0; i < NR_SOFTIRQS; i++) {
11 seq_printf(p, "%12s:", softirq_to_name[i]);
12 for_each_possible_cpu(j)
13 seq_printf(p, " %10u", kstat_softirqs_cpu(i, j));
14 seq_putc(p, '\n');
15 }
16 return 0;
17 }

06~07 번째 줄을 보겠습니다.
06 for_each_possible_cpu(i)
07 seq_printf(p, "CPU%-8d", i);

콘솔 창에 출력할 CPU 번호를 처리합니다.

10~15 번째 줄은 for 루프인데 Soft IRQ 서비스 갯수인 NR_SOFTIRQS 플래그 갯수만큼 실행합니다.

11 번째 줄은 Soft IRQ 서비스 이름을 저장하는 softirq_to_name 배열에 접근해 Soft IRQ 서비스 이름을 출력합니다.
11 seq_printf(p, "%12s:", softirq_to_name[i]);

12~13 번째 줄 코드를 보겠습니다.
12 for_each_possible_cpu(j)
13 seq_printf(p, " %10u", kstat_softirqs_cpu(i, j));

kstat_softirqs_cpu() 함수를 호출해 CPU 갯수 만큼 Soft IRQ 서비스 실행 횟수를 출력합니다.  

kstat_softirqs_cpu() 함수 구현부는 다음과 같습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kernel_stat.h]
01 static inline unsigned int kstat_softirqs_cpu(unsigned int irq, int cpu)
02 {
03       return kstat_cpu(cpu).softirqs[irq];
04 }

커널 통계 정보를 기록하는 percpu 타입 kstat_cpu 변수 softirqs 필드에 저장된 값을 출력합니다.

그러면 kstat_cpu() 함수 구현부를 조금 더 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kernel_stat.h]
#define kstat_cpu(cpu) per_cpu(kstat, cpu)

코드를 보니 kstat_cpu 함수는  per-cpu 타입 변수인 kstat를 통해 Soft IRQ 처리 횟수를 읽어 옵니다.

kstat 변수를 살펴 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kernel_stat.h]
01 struct kernel_stat {
02 unsigned long irqs_sum;
03 unsigned int softirqs[NR_SOFTIRQS];
04 };
05
06 DECLARE_PER_CPU(struct kernel_stat, kstat);

06 번째 줄을 보면 struct kernel_stat 구조체 타입인 kstat을 percpu 타입으로 선언했습니다.

per-cpu 타입 kstat 변수에 Soft IRQ 서비스 처리 횟수를 저장하는 함수는 kstat_incr_softirqs_this_cpu() 입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kernel_stat.h]
static inline void kstat_incr_softirqs_this_cpu(unsigned int irq)
{
__this_cpu_inc(kstat.softirqs[irq]);
}

irq이란 입력 값을 받으면 per-cpu에 해당하는 kstat 변수에 접근해서 softirqs[irq] 값을 +1만큼 증감합니다. 

Soft IRQ 서비스 실행 횟수는 어느 함수에서 저장할까?  

Soft IRQ 서비스를 실행한 횟수를 출력하는 코드를 분석했는데 이번에는 Soft IRQ 서비스 횟수를 저장하는 코드를 볼 차례입니다.

Soft IRQ 서비스를 실행하는 함수는 __do_softirq() 함수니 이 함수 내에서 Soft IRQ 서비스 실행 횟수를 저장할 것입니다.

Soft IRQ 서비스별 실행 횟수는 다음 __do_softirq() 함수 12 번째 줄 코드를 실행할 때 저장합니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 asmlinkage __visible void __softirq_entry __do_softirq(void)
02 {
...
03 while ((softirq_bit = ffs(pending))) {
04 unsigned int vec_nr;
05 int prev_count;
06
07 h += softirq_bit - 1;
08
09 vec_nr = h - softirq_vec;
10 prev_count = preempt_count();
11
12 kstat_incr_softirqs_this_cpu(vec_nr);
13
14 trace_softirq_entry(vec_nr);
15 h->action(h);
16 trace_softirq_exit(vec_nr);
...
17

kstat_incr_softirqs_this_cpu() 함수 구현부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kernel_stat.h]
01 static inline void kstat_incr_softirqs_this_cpu(unsigned int irq)
02 {
03 __this_cpu_inc(kstat.softirqs[irq]);
04 } 
 
 03 번째 줄 코드와 같이 kstat.softirqs 변수에 접근해 Soft IRQ 서비스별 실행 횟수를 +1만큼 증감합니다.

[리눅스커널][SoftIRQ] 디버깅: ftrace Soft IRQ 이벤트 소개 6장. 인터럽트 후반부 처리

6.12.1 ftrace Soft IRQ 이벤트 소개


이번 시간에 Soft IRQ 실행 흐름을 추적하는 ftrace event를 소개합니다.

ftrace는 커널의 주요 동작을 추적하며 이를 이벤트로 정의합니다. Soft IRQ 도 커널의 중요 기능이니 ftrace에서 다음 이벤트를 제공합니다.
- softirq_raise: Soft IRQ 서비스를 요청
- softirq_entry: Soft IRQ 서비스 실행 시작
- softirq_exit: Soft IRQ 서비스 실행 마무리

먼저 ftrace 이벤트를 활성화하는 방법을 알아보고 ftrace에서 각 로그를 분석해보겠습니다.

Soft IRQ ftrace 이벤트 켜기

Soft IRQ 이벤트는 다음 명령어로 활성화할 수 있습니다.
"echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_raise/enable"
"echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_entry/enable"
"echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_exit/enable"

Soft IRQ 서비스 실행은 인터럽트 핸들링이 마무리된 후 시작하므로 irq_handler_entry, irq_handler_exit ftrace 이벤트와 함께 분석하는 경우가 많습니다.

Soft IRQ ftrace 이벤트 로그 패턴와 실행 코드 확인하기
Soft IRQ ftrace 이벤트 메시지는 다음 형식으로 출력합니다.
softirq_raise: vec= [1--10] [action=Soft IRQ 서비스 타입]
softirq_entry: vec= [1--10] [action=Soft IRQ 서비스 타입]
softirq_exit: vec= [1--10] [action=Soft IRQ 서비스 타입]

vec= [1--10] 로그에서 1-10은 1부터 10까지 Soft IRQ 벡터 번호를 출력하고 Soft IRQ 서비스 타입은 다음 softirq_to_name 배열에 저장된 이름입니다.
const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"

각각 메시지의 의미와 메시지를 출력하는 함수 이름은 다음 테이블과 같습니다.
이벤트 종류 역할 실행 함수
softirq_raise Soft IRQ 서비스 요청 __raise_softirq_irqoff()
softirq_entry Soft IRQ 서비스 실행 시작 __do_softirq()
softirq_exit   Soft IRQ 서비스 실행 마무리 __do_softirq()

 
이번에는 각각 이벤트가 실행하는 커널 코드를 소개합니다. 

softirq_raise 이벤트를 출력하는 코드입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 void __raise_softirq_irqoff(unsigned int nr)
2 {
3 trace_softirq_raise(nr);
4 or_softirq_pending(1UL << nr);
5 }

3 번째 줄 코드에서 trace_softirq_raise() 함수를 실행할 때 softirq_raise 이벤트 메시지를 출력합니다.

다음 softirq_entry와 softirq_exit 이벤트를 출력하는 코드를 소개합니다.

Soft IRQ 서비스를 실행하는 __do_softirq() 함수 코드입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2 {
...
3 restart:
4 /* Reset the pending bitmask before enabling irqs */
5 set_softirq_pending(0);
6
7 local_irq_enable();
8
9 h = softirq_vec;
10
11 while ((softirq_bit = ffs(pending))) {
12 unsigned int vec_nr;
13 int prev_count;
14
15 h += softirq_bit - 1;
16
17 vec_nr = h - softirq_vec;
18 prev_count = preempt_count();
19
20 kstat_incr_softirqs_this_cpu(vec_nr);
21
22 trace_softirq_entry(vec_nr);
23 h->action(h);
24 trace_softirq_exit(vec_nr);
...
33 }

23 번째 줄 코드를 실행할 때 Soft IRQ 서비스 핸들러를 호출합니다. 이 코드 전 후로 softirq_entry와 softirq_exit 메시지를 출력합니다. 22 번째 줄 코드가 실행하면 softirq_entry 메시지를 출력하고 24 번째 줄은 softirq_exit 메시지를 출력합니다.

Soft IRQ ftrace 이벤트 로그 분석하기

이번에는 ftrace에서 Soft IRQ 이벤트를 분석하는 방법을 소개합니다.

분석할 로그를 소개합니다.
01 <idle>-0 [000] dnh1   239.691099: irq_handler_entry: irq=17 name=arch_timer
02 <idle>-0 [000] dnh1   239.691125: softirq_raise: vec=1 [action=TIMER]
03 <idle>-0 [000] dnh1   239.691128: softirq_raise: vec=9 [action=RCU]
04 <idle>-0 [000] dnh1   239.691134: softirq_raise: vec=7 [action=SCHED]
05 <idle>-0 [000] dnh1   239.691141: irq_handler_exit: irq=17 ret=handled
06 <idle>-0 [000] .ns1   239.691153: softirq_entry: vec=1 [action=TIMER]
07 <idle>-0 [000] .ns1   239.691204: softirq_exit: vec=1 [action=TIMER]
08 <idle>-0 [000] .ns1   239.691205: softirq_entry: vec=7 [action=SCHED]
09 <idle>-0 [000] .ns1   239.691217: softirq_exit: vec=7 [action=SCHED]
10 <idle>-0 [000] .ns1   239.691218: softirq_entry: vec=9 [action=RCU]
11 <idle>-0 [000] .ns1   239.691236: softirq_exit: vec=9 [action=RCU]

Soft IRQ 서비스는 인터럽트를 핸들링하고 나서 실행하므로 인터럽트 동작을 추적하는 irq_handler_entry와 irq_handler_exit 이벤트를 같이 분석합니다.

01~05 번째 줄 로그를 보겠습니다.
01 <idle>-0 [000] dnh1 239.691099: irq_handler_entry: irq=17 name=arch_timer
02 <idle>-0 [000] dnh1 239.691125: softirq_raise: vec=1 [action=TIMER]
03 <idle>-0 [000] dnh1 239.691128: softirq_raise: vec=9 [action=RCU]
04 <idle>-0 [000] dnh1 239.691134: softirq_raise: vec=7 [action=SCHED]
05 <idle>-0 [000] dnh1 239.691141: irq_handler_exit: irq=17 ret=handled

다섯 줄 메시지 공통으로 볼드체와 같이 dnh1이란 프로세스 컨택스트 정보가 보입니다. 알파벳 중에 h는 인터럽트 컨택스트란 의미입니다.

03 번째 줄은 TIMER Soft IRQ 서비스, 03번째 줄은 RCU Soft IRQ 서비스 마지막 04 번째는 SCHED 서비스를 요청하는 동작입니다. 인터럽트 컨택스트에서 Soft IRQ 서비스 실행을 요청하는 동작입니다.

06~11 번째 줄 로그는 Soft IRQ 서비스를 실행하는 동작입니다.
06 <idle>-0 [000] .ns1   239.691153: softirq_entry: vec=1 [action=TIMER]
07 <idle>-0 [000] .ns1   239.691204: softirq_exit: vec=1 [action=TIMER]
08 <idle>-0 [000] .ns1   239.691205: softirq_entry: vec=7 [action=SCHED]
09 <idle>-0 [000] .ns1   239.691217: softirq_exit: vec=7 [action=SCHED]
10 <idle>-0 [000] .ns1   239.691218: softirq_entry: vec=9 [action=RCU]
11 <idle>-0 [000] .ns1   239.691236: softirq_exit: vec=9 [action=RCU]

06~07 번째 줄은 TIMER Soft IRQ 서비스를 실행하는 동작입니다. 
08~09 번째 줄은 SCHED Soft IRQ 서비스 그리고 10~11 번째 줄은 RCU 서비스를 실행합니다.

Soft IRQ 서비스를 실행한다는 것은 각 Soft IRQ 서비스별로 등록한 핸들러 함수를 실행한다는 의미입니다.
06 <idle>-0 [000] .ns1   239.691153: softirq_entry: vec=1 [action=TIMER]  // run_timer_softirq() 함수 실행 시작
07 <idle>-0 [000] .ns1   239.691204: softirq_exit: vec=1 [action=TIMER]  // run_timer_softirq() 함수 실행 마무리
08 <idle>-0 [000] .ns1   239.691205: softirq_entry: vec=7 [action=SCHED] // run_rebalance_domains() 함수 실행 시작
09 <idle>-0 [000] .ns1   239.691217: softirq_exit: vec=7 [action=SCHED] // run_rebalance_domains() 함수 실행 마무리
10 <idle>-0 [000] .ns1   239.691218: softirq_entry: vec=9 [action=RCU] // rcu_process_callbacks() 함수 실행 시작
11 <idle>-0 [000] .ns1   239.691236: softirq_exit: vec=9 [action=RCU] // rcu_process_callbacks() 함수 실행 마무리

위 메시지로 각각 함수 실행 시각을 예측할 수 있습니다. 

인터럽트 후반부 처리 목차 6장. 인터럽트 후반부 처리

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인




[리눅스커널][태스크릿] 태스크릿은 언제 실행할까? tasklet_action_common() 분석 6장. 인터럽트 후반부 처리

태스크릿은 언제 실행할까?

태스크릿은 Soft IRQ 서비스 중 하나입니다. 따라서 Soft IRQ 서비스를 실행하는 __do_softirq() 함수에서 태스크릿 서비스 핸들러 함수를 호출합니다.

Soft IRQ 전체 흐름도에서 태스크릿 서비스를 실행하는 그림을 보겠습니다.
 
[태스크릿 전체 흐름도에서 태스크릿 실행 단계]

[1] 단계에서 인터럽트가 발생한 후 인터럽트 핸들러에서 태스크릿 스케줄링을 실행했습니다. 이번엔 [2] 단계에서 태스크릿 서비스 핸들러가 호출하는 과정을 살펴봅니다.

전체 Soft IRQ 구조에서 __do_softirq() 함수가 호출되는 흐름은 6.8 절을 참고하세요. 

태스크릿 서비스 핸들러인 tasklet_action() 함수를 실행하는 출발점은 __do_softirq() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 asmlinkage __visible void __softirq_entry __do_softirq(void)
02 {
...
03 while ((softirq_bit = ffs(pending))) {
04 unsigned int vec_nr;
05 int prev_count;
06
07 h += softirq_bit - 1;
08
09 vec_nr = h - softirq_vec;
10 prev_count = preempt_count();
11
12 kstat_incr_softirqs_this_cpu(vec_nr);
13
14 trace_softirq_entry(vec_nr);
15 h->action(h);
16 trace_softirq_exit(vec_nr);
...
17

위 함수 15 번째 줄 코드에서 tasklet_action() 함수를 호출합니다.

__do_softirq() 함수에서 호출하는 tasklet_action() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 static __latent_entropy void tasklet_action(struct softirq_action *a)
2 {
3 tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
4 }

tasklet_action() 함수가 특별히 하는 일은 없습니다. 다음 인자로 tasklet_action_common() 함수를 호출합니다.
 - a: struct softirq_action 타입 
 - this_cpu_ptr(&tasklet_vec): tasklet_vec 전역 변수에서 percpu 오프셋을 적용 주소
 - TASKLET_SOFTIRQ: 태그크릿 플래그

this_cpu_ptr() 함수는 현재 실행 중인 CPU 번호에 맞게 percpu 오프셋을 적용한 주소로 변환합니다.
 
다음 tasklet_action_common() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 static void tasklet_action_common(struct softirq_action *a,
02    struct tasklet_head *tl_head,
03    unsigned int softirq_nr)
04 {
05  struct tasklet_struct *list;
06 
07  local_irq_disable();
08  list = tl_head->head;
09 tl_head->head = NULL;
10 tl_head->tail = &tl_head->head;
11 local_irq_enable();
12
13 while (list) {
14 struct tasklet_struct *t = list;
15
16 list = list->next;
17
18 if (tasklet_trylock(t)) {
19 if (!atomic_read(&t->count)) {
20 if (!test_and_clear_bit(TASKLET_STATE_SCHED,
21 &t->state))
22 BUG();
23 t->func(t->data);
24 tasklet_unlock(t);
25 continue;
26 }
27 tasklet_unlock(t);
28 }
...
29 }
30 } 

먼저 태스크릿 실행 조건을 확인하는 구문부터 보겠습니다.
19 if (!atomic_read(&t->count)) {
20 if (!test_and_clear_bit(TASKLET_STATE_SCHED,
21 &t->state))
22 BUG();

19 번째 줄에서는 struct tasklet_struct 타입 &t->count가 0인지를 체크합니다.
만약 0이 아니면 20~25 번째 줄 코드를 실행하지 않습니다.

다음은 struct tasklet_struct 타입 &t->state가 TASKLET_STATE_SCHED 플래그가 아니면 22 번째 줄 코드인 BUG() 함수를 실행해 커널 패닉을 유발합니다.

리눅스 커널에서 BUG() 함수는 시스템을 정상적으로 구동할 수 없는 치명적인 오류가 있을 때 호출합니다.

23 번째 줄은 태스크릿 핸들러 함수를 호출합니다.
23 t->func(t->data);
 
태스크릿 핸들러 함수가 실행하는 흐름입니다.

예를 들면 keyboard_tasklet 태스크릿은 다음과 같이 func 필드에는 kbd_bh() 함수 주소가 있습니다.
  (static struct tasklet_struct) keyboard_tasklet = (
    (struct tasklet_struct *) next = 0x0 = ,
    (long unsigned int) state = 0x0,
    (atomic_t) count = ((int) counter = 0x0),
    (void (*)()) func = 0x8051F144 = kbd_bh,
    (long unsigned int) data = 0x0)

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인


[리눅스커널][태스크릿] 태스크릿(struct tasklet_struct)은 어떻게 등록할까? 6장. 인터럽트 후반부 처리

먼저 태스크릿을 등록하는 2가지 방법을 소개합니다.
1. 태스크릿 전역 변수 선언
  : DECLARE_TASKLET() 혹은 DECLARE_TASKLET_DISABLED() 함수 호출
2. 태스크릿 초기화 함수 호출
  : tasklet_init() 함수 

DECLARE_TASKLET() 혹은 DECLARE_TASKLET_DISABLED() 함수로 태스크릿 등록하기

DECLARE_TASKLET() 혹은 DECLARE_TASKLET_DISABLED() 매크로를 써서 태스크릿을 초기화하는 방법입니다. 태스크릿 전역 변수는 컴파일 타임에 자료구조가 정해집니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
1 #define DECLARE_TASKLET(name, func, data) \
2 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
3
4 #define DECLARE_TASKLET_DISABLED(name, func, data) \
5 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

각 함수별로 전달하는 인자 타입은 다음과 같습니다.
name: struct tasklet_struct 타입
func: 태스크릿 핸들러 함수
data: 태스크릿 핸들러 함수 매개인자

1 번째 줄 코드를 보면 첫 번째 인자인 name으로 struct tasklet_struct 타입 전역 변수를 선언합니다. 선언된 name 전역 변수 각 필드에 초기화 값을 저장합니다.

DECLARE_TASKLET() 함수로 태스크릿을 초기화하면 struct tasklet_struct 필드가 바뀝니다.
1 #define DECLARE_TASKLET(name, func, data) \
2 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
struct tasklet_struct
{
struct tasklet_struct *next = NULL;
unsigned long state = 0;
atomic_t count = ATOMIC_INIT(0);
void (*func)(unsigned long) = func;
unsigned long data = data;
};

ATOMIC_INIT(0) 은 0을 의미합니다.

struct tasklet_struct 구조체에서 볼드체로 된 부분을 눈으로 따라가 보기실 바랍니다.

DECLARE_TASKLET_DISABLED() 함수가 DECLARE_TASKLET() 함수와 다른 점은 atomic_t count 필드를 ATOMIC_INIT(1)로 설정한다는 점입니다. 태스크릿에서 atomic_t count 필드가 0이면 태스크릿이 활성화된 상태입니다. atomic_t count 필드를 1로 설정해 태스크릿은 기본으로 비활성화합니다.

이번에는 DECLARE_TASKLET_DISABLED() 함수를 써서 태스크릿을 초기화하는 코드를 소개합니다.
[https://elixir.bootlin.com/linux/v4.20/source/drivers/tty/vt/keyboard.c]
DECLARE_TASKLET_DISABLED(keyboard_tasklet, kbd_bh, 0);

위와 같인 선언하면 keyboard_tasklet 전역 변수 필드는 다음과 같습니다. 
  (static struct tasklet_struct) keyboard_tasklet = (
    (struct tasklet_struct *) next = 0x0 = ,
    (long unsigned int) state = 0,
    (atomic_t) count = ((int) counter = 1),
    (void (*)()) func = 0x8050636C = kbd_bh,
    (long unsigned int) data = 0)

기본으로 태스크릿을 비활성화해 초기화한 후 tasklet_enable() 함수를 호출하면 태스크릿을 활성화할 수 있습니다. tasklet_enable() 함수 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
01 static inline void tasklet_enable(struct tasklet_struct *t)
02{
03 smp_mb__before_atomic();
04 atomic_dec(&t->count);
05 }

tasklet_enable() 함수 코드를 보면 05 번째 줄과 같이 struct tasklet_struct 구조체 count 필드를 -1만큼 감소시킵니다.

tasklet_enable() 함수를 호출해 태스크릿을 활성하는 예제는 다음 코드 3번째 줄입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/tty/vt/keyboard.c]
01 int __init kbd_init(void)
02 {
...
03 tasklet_enable(&keyboard_tasklet);
04 tasklet_schedule(&keyboard_tasklet);
05
06 return 0;
07 }

tasklet_init() 함수로 태스크릿 초기화하기

태스크릿을 초기화하려면 다음 함수 선언부에서 보이는 인자에 적절한 값을 저장한 다음
tasklet_init() 함수를 호출하면 됩니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
extern void tasklet_init(struct tasklet_struct *t,
 void (*func)(unsigned long), unsigned long data);

함수에 전달하는 인자의 의미를 소개합니다.

struct tasklet_struct *t;
드라이버에서 태스크릿을 생성하면 태스크릿을 식별하는 구조체입니다.

void (*func)(unsigned long);
태스크릿 핸들러 함수입니다. 태스크릿 서비스를 등록하면 실행한 콜백 함수 주소를 저장합니다.

unsigned long data;
태스크릿 콜백 함수로 전달하는 매개인자입니다.
디바이스 드라이버 핸들과 같이 태스크릿 서비스가 실행할 때 제어하려는 인자입니다.
인터럽트 핸들러를 호출할 때 전달하는 매개인자와 같은 개념입니다.

tasklet_init() 함수 구현부 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 void tasklet_init(struct tasklet_struct *t,
02   void (*func)(unsigned long), unsigned long data)
03 {
04 t->next = NULL;
05 t->state = 0;
06 atomic_set(&t->count, 0);
07 t->func = func;
08 t->data = data;
09 }

tasklet_init() 함수를 호출하면 1 번째 인자로 전달되는 태스크릿을 핸들하는 struct tasklet_struct 구조체 t 인자에 기본 설정을 합니다.

04~06 번째 줄 코드를 보겠습니다.
04 t->next = NULL;
05 t->state = 0;
06 atomic_set(&t->count, 0);

다음 태스크릿 핸들을 저장하는 next 필드에 NULL을 지정하고 state 필드를 0으로 바꿉니다.
이어서 count 필드도 0으로 바뀝니다.

07~08 번째 줄은 tasklet_init() 함수로 전달된 인자를 태스크릿 핸들에 설정하는 코드입니다.
07 t->func = func;
08 t->data = data;

07 번째 줄은 태스크릿 핸들러 함수를 설정하고 08 번째 줄은 태스크릿 핸들러에 전달하는 매개인자를 저장합니다.

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



[리눅스커널][태스크릿] 태스크릿(struct tasklet_struct) 이란 무엇인가 6장. 인터럽트 후반부 처리

태스크릿은 프로세스와 전혀 상관 없는 용어입니다. 태스크릿이란 이름 때문에 많은 분이 프로세스와 관련됐다고 오해할 수 있습니다.

태스크릿은 Soft IRQ 서비스 중 하나로 동적으로 Soft IRQ 서비스를 쓸 수 있는 인터페이스입니다. 드라이버 레벨에서 태스크릿은 자주 쓰고 활용합니다.

태스크릿은 무엇일까?

우리는 인터럽트 후반부 기법으로 Soft IRQ에 대해 다음과 같은 내용을 배웠습니다.  
인터럽트 핸들링 후 바로 Soft IRQ 서비스 실행 
인터럽트 발생 후 바로 후반부를 처리할 때 용이 

다른 인터럽트 후반부 기법에 비해 Soft IRQ는 인터럽트 후반부를 빨리 처리해야 할 때 적용합니다.

그러면 이런 장점이 있는 Soft IRQ 서비스를 드라이버 레벨에서 쓰면 좋을 때가 있습니다.
태스크릿은 드라이버 레벨에서 Soft IRQ 서비스를 등록해 쓸 수 있는 인터페이스입니다.

다른 관점으로는 태스크릿은 동적인 Soft IRQ 서비스라고 볼 수 있습니다.
태스크릿을 Soft IRQ 태스크릿 서비스에 등록하면 태스크릿 서브 서비스로 동작합니다.

리눅스 커널에서는 다음과 같은 Soft IRQ 서비스가 있으며 각각 종류는 softirq_to_name에서 확인할 수 있습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
};

softirq_to_name 전역 변수에 지정된 Soft IRQ 서비스 중 "TASKLET"이 태스크릿 인터페이스입니다.

또한 리눅스 커널에서는 다음과 같은 Soft IRQ 서비스가 있으며 각각 서비스 핸들러 함수는 softirq_vec 배열에서 확인할 수 있습니다. 
01  (static struct softirq_action [10]) [D:0x80C02080] softirq_vec = (
02    [0] = ((void (*)()) [D:0x80C02080] action = 0x80122888 = tasklet_hi_action),
03    [1] = ((void (*)()) [D:0x80C02084] action = 0x80181270 = run_timer_softirq),
04    [2] = ((void (*)()) [D:0x80C02088] action = 0x80614684 = net_tx_action),
05    [3] = ((void (*)()) [D:0x80C0208C] action = 0x80615AB0 = net_rx_action),
06    [4] = ((void (*)()) [D:0x80C02090] action = 0x804279B0 = blk_done_softirq),
07    [5] = ((void (*)()) [D:0x80C02094] action = 0x0 = ),
08    [6] = ((void (*)()) [D:0x80C02098] action = 0x8012299C = tasklet_action),
09    [7] = ((void (*)()) [D:0x80C0209C] action = 0x801588EC = run_rebalance_domains),
10    [8] = ((void (*)()) [D:0x80C020A0] action = 0x0 = ),
11    [9] = ((void (*)()) [D:0x80C020A4] action = 0x8017ABC4 = rcu_process_callbacks))

08 번째 줄 6 번째 배열 인덱스에 있는 tasklet_action() 함수가 태스크릿 서비스 핸들러 함수입니다.

다음 그림은 태스크릿 전체 흐름도입니다.
 
[태스크릿 전체 실행 흐름도]


태스크릿은 Soft IRQ 서비스 중 하나이므로 이전 절에 배웠던 Soft IRQ 서비스 실행 흐름도와 거의 비슷합니다. 

[1] 단계: Soft IRQ 서비스 요청
태스크릿 서비스를 요청합니다. tasklet_schedule() 함수에서 raise_softirq_irqoff() 함수를 호출합니다.

[2] 단계: Soft IRQ 서비스 실행
Soft IRQ 서비스 중 하나인 태스크릿 서비스 핸들러 tasklet_action() 함수를 실행합니다. 여기서 태스크릿에 등록한 태스크릿 핸들러 함수가 호출됩니다.

[3] 단계: ksoftirqd 스레드 깨움
__do_softirq() 함수에서 실행이 오래 걸려 태스크릿 서비스 핸들러를 실행 못하면 ksoftirqd 스레드를 깨웁니다.

[4] 단계: kosoftirqd 스레드 실행
ksoftirqd 스레드에서 Soft IRQ 서비스를 실행합니다.

다음 그림을 보면 태스크릿이 무엇인지 더 쉽게 이해할 수 있습니다.
 
[Soft IRQ 서비스 중 태스크릿 세부 구조]

Soft IRQ 서비스 이름을 softirq_to_name[NR_SOFTIRQS] 배열을 참고해 나열했습니다.
이 중 TASKLET 이란 Soft IRQ 서비스를 볼 수 있습니다. 이는 전체 Soft IRQ 서비스 중 하나입니다.

TASKLET Soft IRQ 아랫 부분을 보면 태스크릿A, 태스크릿B 그리고 태스크릿C가 있습니다.
이번 절에서 다루는 내용은 태스크릿A~태스크릿C 을 TASKLET Soft IRQ 서비스에 등록해 각 태스크릿 핸들러 함수가 호출되는 과정입니다.

태스크릿 자료구조 알아보기

태스크릿을 표현하는 자료구조는 struct tasklet_struct이며 선언부는 다음과 같습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
01 struct tasklet_struct
02 {
03 struct tasklet_struct *next;
04 unsigned long state;
05 atomic_t count;
06 void (*func)(unsigned long);
07 unsigned long data;
08 };

struct tasklet_struct *next;
   태스크릿으로의 포인터입니다.
   
unsigned long state;
태스크릿 상태를 담고 있으며 태스크릿 세부 제어를 위해 씁니다.

state 필드는 다음에 선언된 플래그 중 하나를 저장합니다.   
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]   
enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};

TASKLET_STATE_SCHED는 태스크릿이 실행을 위해 스케줄링, TASKLET_STATE_RUN는 태스크릿이 실행 중인 상태입니다.

atomic_t count; 
태스크릿 레퍼런스 카운터이며 태스크릿을 초기화하는 tasklet_init() 함수에서 0으로 설정됩니다. count는 태스크릿 실행 여부를 판단하는 중요한 필드입니다. 반드시 0 이어야만 태스크릿을 실행합니다.

void (*func)(unsigned long);
태스크릿 핸들러 함수입니다.

tasklet_init() 함수를 호출할 때 2 번째 인자로 핸들러 함수를 등록합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
extern void tasklet_init(struct tasklet_struct *t,
 void (*func)(unsigned long), unsigned long data);

unsigned long data;
태스크릿 핸들러 함수에 전달되는 매개인자입니다.


# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



[리눅스커널][SoftIRQ] Soft IRQ 컨택스트에 대해서 - in_softirq() 6장. 인터럽트 후반부 처리

6.10 Soft IRQ 컨택스트에 대해서

인터럽트가 발생한 후 인터럽트 핸들러가 실행 중인 상태를 인터럽트 컨택스트라고 합니다. 이와 마찬가지로 Soft IRQ 서비스를 실행 중인 상태를 Soft IRQ 컨택스트라고 부릅니다. 이번 절에서는 Soft IRQ 컨택스트 시작 시점과 관련 자료구조에 대해서 살펴보겠습니다.

6.10.1 Soft IRQ 컨택스트 시작점은 어디인가?

Soft IRQ 컨택스트는 Soft IRQ 서비스를 실행할 때 활성화됩니다. 
Soft IRQ 서비스는 __do_softirq() 함수에서 실행하니 다음 코드를 보면서 Soft IRQ 컨택스트 활성화 시점을 알아봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 asmlinkage __visible void __softirq_entry __do_softirq(void)
02 {
...
03 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
04 in_hardirq = lockdep_softirq_start();
05
06 restart:
...
07
08 while ((softirq_bit = ffs(pending))) {
...
09 trace_softirq_entry(vec_nr);
10 h->action(h);
11 trace_softirq_exit(vec_nr);
...
12 pending >>= softirq_bit;
13 }
...
14 lockdep_softirq_end(in_hardirq);
15 account_irq_exit_time(current);
16 __local_bh_enable(SOFTIRQ_OFFSET);
...
17 }

위 코드는 __do_softirq() 함수에서 가장 중요한 부분만 가져온 것입니다.

03~16 번째 코드 구간은 Soft IRQ 서비스를 실행하는 블록입니다. 

3 번째 줄 코드에서 Soft IRQ 컨택스트를 활성화하고 16 번째 줄에서는 Soft IRQ 컨택스트를 비활성화합니다. 
03 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
... Soft IRQ 서비스 실행 루틴 ...
16 __local_bh_enable(SOFTIRQ_OFFSET);

Soft IRQ 컨택스트가 활성화되는 시점은 __do_softirq() 함수에서 Soft IRQ 서비스를 실행할 때입니다.

6.10.2 Soft IRQ 컨택스트는 언제 시작할까?

Soft IRQ 컨택스트를 활성화할 때 변경하는 자료구조는 무엇일까요?
각각 프로세스의 스택 최상단 주소에 있는 struct thread_info 구조체 preempt_count 필드입니다.

Soft IRQ 컨택스트일 때는 struct thread_info 구조체 preempt_count 필드가 0x100 으로 바꾸고 Soft IRQ 컨택스트가 아닐 때는 0x100을 Clear합니다. 이 필드가 바뀌는 전체 흐름도는 다음과 같습니다.

Soft IRQ 인터럽트를 활성화 및 비활성화는 다음 단계로 동작합니다.
1. Soft IRQ 컨택스트 시작: __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
2. ... Soft IRQ 서비스 실행 루틴 ...
3. Soft IRQ 컨택스트 끝: __local_bh_enable(SOFTIRQ_OFFSET);

먼저 __local_bh_disable_ip() 함수와 __local_bh_enable() 함수에 전달하는 SOFTIRQ_OFFSET 플래그에 대해 알아봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h]
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT)

SOFTIRQ_SHIFT 플래그가 8이니, 1 << 8 연산의 결과인 0x100(256)가 SOFTIRQ_OFFSET입니다.

Soft IRQ 컨택스트 시작: __local_bh_disable_ip() 함수 분석

__local_bh_disable_ip() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
02 {
03 unsigned long flags;
04
05 WARN_ON_ONCE(in_irq());
06
07 raw_local_irq_save(flags);
08 __preempt_count_add(cnt);

08 번째 줄 코드를 보면__local_bh_disable_ip() 함수 두 번째 인자 cnt를 그대로 __preempt_count_add() 함수에 전달합니다. int cnt 는 SOFTIRQ_OFFSET 플래그입니다.

__preempt_count_add() 함수 구현부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/asm-generic/preempt.h]
01 static __always_inline void __preempt_count_add(int val)
02 {
03 *preempt_count_ptr() += val;
04 }

*preempt_count_ptr() 에 함수 인자인 val을 더하는 연산을 수행합니다.

preempt_count_ptr() 함수 구현부는 다음과 같습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/asm-generic/preempt.h]
static __always_inline volatile int *preempt_count_ptr(void)
{
return &current_thread_info()->preempt_count;
}
current_thread_info() 함수를 통해 프로세스 스택 최상단 주소 struct thread_info 구조체 preempt_count 필드에 접근합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h]
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}

Soft IRQ 컨택스트 끝: __local_bh_enable() 함수 분석

Soft IRQ 컨택스트를 비활성화할 때 __local_bh_enable() 함수를 __do_softirq() 함수에서 호출하며 구현부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 static void __local_bh_enable(unsigned int cnt)
02 {
03 lockdep_assert_irqs_disabled();
...
04 __preempt_count_sub(cnt);
05 }

__preempt_count_sub() 함수 구현부는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/asm-generic/preempt.h]
static __always_inline void __preempt_count_sub(int val)
{
*preempt_count_ptr() -= val;
}

preempt_count_ptr() 함수에 접근해 프로세스 struct thread_info 구조체 preempt_count 필드에 -val 만큼 증감합니다.
__local_bh_enable() 함수를 호출 시 SOFTIRQ_OFFSET 플래그를 전달하니 struct thread_info 구조체 preempt_count 필드에서 0x100을 빼는 연산입니다.

  (struct thread_info *) (struct thread_info*)0x9B1EC000 = 0x8100C000 -> (
    (long unsigned int) flags = 0x2,
    (int) preempt_count = 0x00000102,
    (mm_segment_t) addr_limit = 0,
6.10.3 Soft IRQ 컨택스트 확인방법 

현재 실행 중인 코드가 Soft IRQ 컨택스트인지를 알려면 in_softirq() 함수를 쓰면 됩니다.

in_softirq() 함수를 호출해 true를 반환하면 현재 실행 중인 코드가 Soft IRQ 컨택스트인 것입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/preempt.h]
#define in_softirq() (softirq_count())

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



[리눅스커널][SoftIRQ] ksoftirqd 스레드란 6장. 인터럽트 후반부 처리

6.9 ksoftirqd 스레드

이번 절에서는 Soft IRQ 처리용으로 생성된 ksoftirqd 스레드에 대해 살펴봅니다. ksoftirqd 스레드 생성과정과 동작 원리를 배워봅시다.

6.9.1 ksoftirqd 스레드란

ksoftirqd이란 per-cpu 타입 프로세스입니다. 즉 CPU 개수만큼 생성해서 정해진 CPU 내에서만 실행합니다. ksoftirqd 프로세스는 커널 쓰레드로 Soft IRQ 서비스를 쓰레드 레벨에서 처리합니다. 

리눅스 커널을 탑재한 어떤 시스템에서도 볼 수 있는 친근한 프로세스입니다. 먼저 ksoftirqd 쓰레드를 같이 확인하겠습니다.

다음 사이트를 방문하면 다른 리눅스 시스템에서 ksoftirqd 스레드를 확인할 수 있습니다.
[출처: https://zetawiki.com/wiki/Ksoftirqd]
[root@zetawiki ~]# ps -ef | grep ksoftirqd | grep -v grep
root         3     2  0 Jan08 ?        00:00:07 [ksoftirqd/0]
root        13     2  0 Jan08 ?        00:00:10 [ksoftirqd/1]
root        18     2  0 Jan08 ?        00:00:08 [ksoftirqd/2]
root        23     2  0 Jan08 ?        00:00:07 [ksoftirqd/3]
root        28     2  0 Jan08 ?        00:00:07 [ksoftirqd/4]
root        33     2  0 Jan08 ?        00:00:06 [ksoftirqd/5]
root        38     2  0 Jan08 ?        00:00:06 [ksoftirqd/6]
root        43     2  0 Jan08 ?        00:00:07 [ksoftirqd/7]

“ps –ef” 명령어와 grep 명령어를 조합해서 프로세스 목록 중 ksoftirqd 쓰레드만 출력했습니다. ksoftirqd란 쓰레드 이름 뒤에 숫자가 보입니다. 이는 각각 ksoftirqd 쓰레드가 실행 중인 CPU번호입니다.

이번에는 라즈베리파이에서 ksoftirqd 쓰레드를 확인해 보겠습니다.
root@raspberrypi:/home/pi# ps axl | grep ksoftirq
1     0     7     2  20   0      0     0 smpboo S    ?          0:00 [ksoftirqd/0]
1     0    14     2  20   0      0     0 smpboo S    ?          0:00 [ksoftirqd/1]
1     0    19     2  20   0      0     0 smpboo S    ?          0:00 [ksoftirqd/2]
1     0    24     2  20   0      0     0 smpboo S    ?          0:00 [ksoftirqd/3]

라즈비안은 CPU4개인 쿼드코어 시스템이므로 CPU는 0~3번까지 보입니다.

ksoftirqd 는 시스템 CPU 개수만큼 생성되는데 커널은 다음 규칙으로 ksoftirqd프로세스 이름을 짓습니다.
"ksoftirqd/[CPU 번호]"

ksoftirqd/0 쓰레드는 CPU0에서만 실행하고 ksoftirqd/1, ksoftirqd/2 그리고 ksoftirqd/3 쓰레드도 해당 CPU(CPU1/CPU2/CPU3)에서 수행합니다. 이런 프로세스를 per-cpu 쓰레드라고 부릅니다. 라즈비안에서는 4개 ksoftirqd 프로세스가 보이니 CPU가 4개인 쿼드코어 시스템이라고 유추할 수 있습니다.

 ksoftirqd 프로세스는 언제 생성할까요? 다음과 같이 spawn_ksoftirqd() 함수에서 생성합니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
static __init int spawn_ksoftirqd(void)
{
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
  takeover_tasklets);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

return 0;
}

spawn_ksoftirqd() 함수 앞에 __init란 매크로가 보이니 커널이 부팅할 때 1번 실행합니다.

ksoftirqd 프로세스 선언부를 보면 ksoftirqd 프로세스가 실행하면 run_ksoftirqd 함수가 수행한다는 사실을 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};

ksoftirqd와 같은 per-cpu 타입 쓰레드는 smp 핫플러그 쓰레드로 등록해서 실행합니다. 

간단히 설명을 드리면, 리눅스 커널에선 시스템 부하가 떨어졌을 때는 여러 개의 CPU가 실행할 필요가 없습니다. 예를 들어 라즈베리파이에서 음악이나 동영상 재생을 안 하고 아무 프로그램도 실행을 안 한 상태로 두면 1개 CPU만 실행합니다. 시스템 부하에 따라 CPU를 끄고 키는 동작을 하는데 이때 smp_boot 쓰레드가 동작합니다.

ksoftirqd 쓰레드는 각 CPU마다 생성된 프로세스입니다. 예를 들면 "ksoftirqd/2" 쓰레드는 CPU2에서만 일을 합니다.  그런데 CPU2가 꺼져 있으면 안 됩니다. CPU2가 꺼지는 동작을 유식하게 CPU Hot-plugout이라고 합니다. 

만약 smp_boot는 "ksoftirqd/2"에서 더 처리해야 할 Soft IRQ 서비스가 있는데 CPU2가 Hotplug-out될 상황이면 이 Soft IRQ 서비스를 "ksoftirqd/3”와 같이 깨어 있는 다른 ksoftirqd 쓰레드가 실행하게 처리합니다.

smpboot를 관리하는 함수는 smpboot_thread_fn() 이며 다음 코드와 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/smpboot.c]
1 static int smpboot_thread_fn(void *data)
2 {
3 struct smpboot_thread_data *td = data;
....
4 if (!ht->thread_should_run(td->cpu)) {
5 preempt_enable_no_resched();
6 schedule();
7 } else {
8 __set_current_state(TASK_RUNNING);
9 preempt_enable();
10 ht->thread_fn(td->cpu);
11 }

smpboot 에 핫플러그인으로 등록된 ksoftirqd 쓰레드의 thread_fn 멤버로 등록한 run_ksoftirqd() 함수는 smpboot_thread_fn() 함수의 다음 10번 줄 코드에서 실행합니다.

smp_boot는 이 책의 범위를 벗어나니 더 자세한 내용은 다음 저자 블로그에 포스팅된 글을 참고하세요. 
[http://rousalome.egloos.com/9994595]


6.9.2 ksoftirqd 스레드는 언제 깨울까?

ksoftirqd 스레드는 깨우려면 wakeup_softirqd() 함수를 호출해야 합니다. 이 함수는 다음 조건에서 호출합니다.
 - __do_softirq() 함수에서 Soft IRQ 서비스 실행 중 MAX_SOFTIRQ_TIME 지피 시간만큼 경과했을 때
 - 인터럽트 컨택스트가 아닌 상황에서 Soft IRQ 서비스를 요청할 때

ksoftirqd 스레드를 깨우는 wakeup_softirqd() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 static void wakeup_softirqd(void)
02 {
03 /* Interrupts are disabled: no need to stop preemption */
04 struct task_struct *tsk = __this_cpu_read(ksoftirqd);
05
06 if (tsk && tsk->state != TASK_RUNNING)
07 wake_up_process(tsk);
08 }

먼저 04 번째 줄 코드를 보겠습니다.
04 struct task_struct *tsk = __this_cpu_read(ksoftirqd);

percpu 타입 변수인 ksoftirqd으로 실행 중인 CPU에 해당하는 struct task_struct 구조체를 *tsk 변수로 읽습니다.

리눅스 커널 디버깅 툴인 크래시 유틸리티로 본 ksoftirqd 변수는 다음과 같습니다. 
crash> p ksoftirqd
PER-CPU DATA TYPE:
  struct task_struct *ksoftirqd;
PER-CPU ADDRESSES:
  [0]: c17cd608
  [1]: c17d6608
  [2]: c17df608
  [3]: c17e8608

CPU 갯수 별로 struct task_struct 구조체가 있습니다.

06~07 번째 줄 코드를 보겠습니다.
06 if (tsk && tsk->state != TASK_RUNNING)
07 wake_up_process(tsk);

06 번째 줄 코드는 07 번째 줄 코드 실행 조건문입니다. 
ksoftirqd 스레드의 태스트 디스크립터 struct task_struct 구조체 state 필드가 TASK_RUNNING이 아닌 경우 07 번째 줄 코드를 실행합니다. ksoftirqd 스레드가 실행 중이지 않을 때 07 번째 줄 코드인 wake_up_process() 함수를 실행합니다.

프로세스 중복 실행 요청을 방지하기 위한 예외 처리 코드입니다.

wake_up_process() 함수를 호출하면 스케줄러에게 해당 프로세스 실행 요청을 합니다.

__do_softirq() 함수에서 Soft IRQ 서비스 실행 시간이 MAX_SOFTIRQ_TIME을 넘었을 때 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 asmlinkage __visible void __softirq_entry __do_softirq(void)
02 {
03 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
...
04 restart:
...
05 while ((softirq_bit = ffs(pending))) {
...
06 trace_softirq_entry(vec_nr);
07 h->action(h);
08 trace_softirq_exit(vec_nr);
...
09 h++;
10 pending >>= softirq_bit;
11 }
...
12 pending = local_softirq_pending();
13 if (pending) {
14 if (time_before(jiffies, end) && !need_resched() &&
15     --max_restart)
16 goto restart;
17
18 wakeup_softirqd();
19 }

05~11 번째 줄 코드에서 Soft IRQ 서비스 실행합니다.

14~15 번째 줄 코드는 Soft IRQ 서비스 실행 시간이 MAX_SOFTIRQ_TIME을 넘었는지 체크합니다.

__do_softirq() 함수 실행 시간이 길어지면 프로세스 실행에 악영향을 끼칠 수 있습니다. 
18 번째 줄 코드와 같이 wakeup_softirqd() 함수를 호출해 ksoftirqd 프로세스를 깨우고 __do_softirq() 함수 실행을 종료합니다.

Soft IRQ 서비스를 요청할 때

Soft IRQ 서비스를 요청하기 위해서는 raise_softirq_irqoff() 함수를 호출해야 합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
01 inline void raise_softirq_irqoff(unsigned int nr)
02 {
03 __raise_softirq_irqoff(nr);
04
05 if (!in_interrupt())
06 wakeup_softirqd();
07 }

그런데 05 번째 줄 코드와 같이 현재 실행 중인 코드가 인터럽트 컨택스트 아닌 경우 wakeup_softirqd() 함수를 호출해 ksoftirqd 프로세스를 깨웁니다.

Soft IRQ 서비스 실행은 보통 인터럽트 컨택스트에서 요청합니다. 그런데 이렇게 인터럽트 컨택스트가 아닌 경우에도 Soft IRQ 서비스 실행을 요청할 수 있습니다. Soft IRQ 와 연동해 동작하는 드라이버에서 인터럽트 컨택스트가 아닌 경우에도 Soft IRQ 서비스 실행 요청을 하도록 인터페이스를 제공합니다.  

6.9.3 ksoftirqd 핸들 run_softirqd() 함수 분석

다음에 볼 코드는 ksoftirqd 프로세스가 실행될 때 호출되는 run_ksoftirqd() 함수입니다. 
1 static void run_ksoftirqd(unsigned int cpu)
2 {
3 local_irq_disable();
4 if (local_softirq_pending()) {
5 __do_softirq();
6 local_irq_enable(); 
7 cond_resched_rcu_qs();
8 return;
9 }
10 local_irq_enable();
11 }

4 번째 줄 코드를 보면 local_softirq_pending 함수를 호출해서 요청한 Soft IRQ 서비스가 있는지 점검합니다. 만약에 Soft IRQ 서비스 요청이 있으면 __do_softirq() 함수를 호출해서 Soft IRQ 서비스 핸들러를 실행합니다.

이번에는 ksoftirqd 쓰레드를 제어하는 ksoftirqd_should_run() 함수를 살펴보겠습니다.
static int ksoftirqd_should_run(unsigned int cpu)
{
return local_softirq_pending();
}

함수 이름과 같이 ksoftirqd 쓰레드를 실행 여부를 알려주는 임무를 수행합니다.
이번에도 local_softirq_pending() 함수를 호출해서 Soft IRQ 서비스 요청이 있으면 ksoftirqd 쓰레드를 실행 여부를 알려줍니다.

전체 Soft IRQ 구조에서 ksoftirqd 쓰레드는 어떤 역할을 할까요? 다음 그림에서 검은색으로 된 블록을 보시면 됩니다.
 
[그림 6.15] ksoftirqd 스레드 실행 흐름도

인터럽트가 발생한 후 irq_exit() 함수로 시작해서 __do_softirq() 함수에서 Soft IRQ 서비스 핸들러를 실행합니다. __do_softirq() 함수에서 Soft IRQ 서비스 핸들러 실행 시간이 2ms 초를 넘어서면 하던 일을 멈추고 ksoftirqd 프로세스를 깨웁니다. Soft IRQ 서비스는 프로세스 실행을 멈춘 상태에서 동작하므로 실행 시간이 길면 시스템 응답 속도가 느려져 오동작할 수 있기 때문입니다.

여기까지 ksoftirqd 쓰레드가 어떻게 생성되고 Soft IRQ 구조에서 어떤 역할을 수행하는지 알아봤습니다. ksoftirqd 쓰레드는 IRQ Thread와 비슷한 역할을 합니다. ksoftirqd 쓰레드는 프로세스 레벨에서 Soft IRQ 서비스를 처리하는 임무를 수행합니다. 다른 관점으로 Soft IRQ가 인터럽트 후반부 처리를 할 때의 주인공이 ksoftirqd 쓰레드인 것입니다.

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인




[리눅스커널]Soft IRQ 서비스는 누가 언제 처리하나?(2/2): __do_softirq() 분석 6장. 인터럽트 후반부 처리

6.8.3 Soft IRQ 서비스 실행

이어서 Soft IRQ 서비스를 실행하는 세부 코드를 살펴보겠습니다.

__do_softirq() 함수 분석하기

이제 Soft IRQ 의 핵심 코드인 __do_softirq() 함수를 분석할 차례입니다. 
분석할 전체 코드는 다음과 같습니다.
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2{
3 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
4 unsigned long old_flags = current->flags;
5 int max_restart = MAX_SOFTIRQ_RESTART;
6 struct softirq_action *h;
7 bool in_hardirq;
8 __u32 pending;
9 int softirq_bit;
10
...
11
12 pending = local_softirq_pending();
13 account_irq_enter_time(current);
14
15 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
16 in_hardirq = lockdep_softirq_start();
17
18 restart:
19 /* Reset the pending bitmask before enabling irqs */
20 set_softirq_pending(0);
21
22 local_irq_enable();
23
24 h = softirq_vec;
25
26 while ((softirq_bit = ffs(pending))) {
27 unsigned int vec_nr;
28 int prev_count;
29
30 h += softirq_bit - 1;
31
32 vec_nr = h - softirq_vec;
33 prev_count = preempt_count();
34
35 kstat_incr_softirqs_this_cpu(vec_nr);
36
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);
...
40 h++;
41 pending >>= softirq_bit;
42 }
50
43 rcu_bh_qs();
44 local_irq_disable();
45
46 pending = local_softirq_pending();
47 if (pending) {
48 if (time_before(jiffies, end) && !need_resched() &&
49     --max_restart)
50 goto restart;
51
52 wakeup_softirqd();
53 }

__do_softirq() 함수에 선언된 지역 변수부터 살펴봅니다.
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2{
3 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
4 unsigned long old_flags = current->flags;
5 int max_restart = MAX_SOFTIRQ_RESTART;

#define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10

먼저 3 번째 줄 코드를 분석합시다.
3 unsigned long end = jiffies + MAX_SOFTIRQ_TIME;

MAX_SOFTIRQ_TIME 매크로는 msecs_to_jiffies(2) 코드로 치환됩니다. msecs_to_jiffies 함수는 현재 시각 기준으로 2밀리 초를 jiffies 기준값으로 변환합니다. 
이 값을 현재 시각을 표현하는 jiffies란 변수에 더해서 end란 지역변수에 저장합니다. 

현재 시각에서 2밀리 초를 end에 저장하는 것입니다. end란 로컬 변수는 이 __do_softirq() 함수 실행 시간을 제한하려는 용도로 씁니다. __do_softirq() 함수에서 Soft IRQ 서비스 핸들러를 실행하는 도중 실행 시각이 end를 초과하면 함수 실행을 종료합니다. 이 코드는 조금 후에 더 분석할 예정입니다.

다음은 MAX_SOFTIRQ_RESTART 을 max_restart 지역 변수에 설정합니다. 이 값은 10입니다.
__do_softirq() 함수에서 max_restart 지역 변수를 10으로 설정해 놓고 이 함수 내 restart 레이블을 실행할 때마다 --max_restart; 연산으로 1만큼 감소시킵니다. 만약 10번 restart 레이블을 실행해서 max_restart 변수가 0이 되면 restart 레이블은 실행을 못하고 __do_softriq() 함수는 실행을 종료합니다.

end, max_restart 이 로컬 변수는 __do_softirq() 함수 실행에 대한 중요한 제약 조건이니 잘 기억합시다.

다음은 Soft IRQ 서비스 요청을 점검하는 코드입니다.
12 pending = local_softirq_pending();
13 account_irq_enter_time(current);
14
15 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
16 in_hardirq = lockdep_softirq_start();
17

12번째 줄 코드를 전처리 코드로 보겠습니다. 
(irq_stat[(current_thread_info()->cpu)].__softirq_pending);

irq_stat 전역 변수에서 __softirq_pending 값을 pending 지역 변수로 읽어 옵니다. pending 지역 변수는 로컬 변수로 선언했으므로 __do_softirq() 함수를 실행하는 스택 메모리 공간에 있습니다. 

전역 변수를 지역 변수에 복사하는 이유는 무엇일까요? __do_softirq() 함수가 실행하는 동안 irq_stat이란 전역 변수가 업데이트될 수 있기 때문입니다. 가급적 전역 변수는 지역 변수에 저장한 후 연산해야 동기화 문제를 피할 수 있습니다.

이제는 restart 레이블 코드를 볼 차례입니다.
18 restart:
19 /* Reset the pending bitmask before enabling irqs */
20 set_softirq_pending(0);
21
22 local_irq_enable();
23
24 h = softirq_vec;

이번에도 전처리 코드로 분석하겠습니다. set_softirq_pending() 함수는 다음과 같이 치환됩니다. 
((irq_stat[(current_thread_info()->cpu)].__softirq_pending) = (0));

해당 CPU의 irq_stat 배열 멤버인 __softirq_pending 변수를 0으로 초기화하는 것입니다.

24번째 줄 코드에서는 Soft IRQ 서비스 핸들러 정보가 담긴 softirq_vec 전역 변수를 h란 지역 변수에 저장합니다.

이제 Soft IRQ 서비스 핸들러를 호출하는 코드를 분석합시다.
26 while ((softirq_bit = ffs(pending))) {
27 unsigned int vec_nr;
28 int prev_count;
29
30 h += softirq_bit - 1;
31
32 vec_nr = h - softirq_vec;
33 prev_count = preempt_count();
34
35 kstat_incr_softirqs_this_cpu(vec_nr);
36
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);
...
40 h++;
41 pending >>= softirq_bit;
42 }

26번째 줄 코드는 while 문을 구동하는 조건이므로 가장 유심히 점검해야 합니다.
26 while ((softirq_bit = ffs(pending))) {
27 unsigned int vec_nr;
28 int prev_count;

 리눅스 커널에서 제공하는 ffs란 라이브러리 함수를 써서 pending 이란 변수에서 Soft IRQ 서비스를 설정한 비트를 찾습니다. 만약 pending 지역변수가 20이면 2진수로는 10100입니다.
 
[그림 6.13]  ffs 라이브러리 실행 결과

ffs(20)을 입력하면 가장 먼저 1이 세팅된 비트 위치인 2를 알려줍니다.
30번째 코드는 softirqbit에서 1만큼 뺍니다. 이 이유는 뭘까요?
30 h += softirq_bit - 1;

irq_stat[cpu].__softirq_pending 변수에 Soft IRQ 서비스 아이디별로 설정된 비트 값이 Soft IRQ 서비스 핸들러 함수를 저장한 softirq_vec 배열 위치보다 1만큼 크기 때문입니다. 1 << 0(HI_SOFTIRQ) 연산 결과가 1 입니다. 이 관계를 그림으로 정리하면 다음과 같습니다.
 
  [그림 6.14] __softirq_pending 변수와 softirq_vec 배열과의 관계 

각 Soft IRQ 서비스 아이디를 설정하는 __softirq_pending 변수의 비트 필드와 이를 실행하는 Soft IRQ 서비스 핸들러를 저장하는 softirq_vec 배열은 아주 밀접한 관계입니다.

38번째 코드는 Soft IRQ 서비스 핸들러를 호출합니다. 위 그림에서 softirq_vec에 표시된 함수를 호출하는 것입니다.
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);

라즈비안에서 softirq_vec Soft IRQ 벡터에 등록된 함수들은 다음과 같습니다. 아래 함수 중 하나를 호출하는 것입니다.
(static struct softirq_action [10]) [D:0x80C02080] softirq_vec = (
    [0] = ((void (*)()) [D:0x80C02080] action = 0x80122888 = tasklet_hi_action),
    [1] = ((void (*)()) [D:0x80C02084] action = 0x80181270 = run_timer_softirq),
    [2] = ((void (*)()) [D:0x80C02088] action = 0x80614684 = net_tx_action),
    [3] = ((void (*)()) [D:0x80C0208C] action = 0x80615AB0 = net_rx_action),
    [4] = ((void (*)()) [D:0x80C02090] action = 0x804279B0 = blk_done_softirq),
    [5] = ((void (*)()) [D:0x80C02094] action = 0x0 = ),
    [6] = ((void (*)()) [D:0x80C02098] action = 0x8012299C = tasklet_action),
    [7] = ((void (*)()) [D:0x80C0209C] action = 0x801588EC = run_rebalance_domains),
    [8] = ((void (*)()) [D:0x80C020A0] action = 0x0 = ),
    [9] = ((void (*)()) [D:0x80C020A4] action = 0x8017ABC4 = rcu_process_callbacks))

37번째와 39번째 코드는 ftrace 로그를 출력하는 코드입니다. 
37 trace_softirq_entry(vec_nr);
38 h->action(h);
39 trace_softirq_exit(vec_nr);

ftrace 는 Soft IRQ 동작을 트레이싱할 수 있는 이벤트를 제공합니다.

다음 코드는 pending에서 1로 설정된 비트를 없애는 코드입니다. 그래야 다음에 1로 설정된 비트를 읽을 수 있습니다.
40 h++;
41 pending >>= softirq_bit;

이렇게 while loop을 실행해서 pending 된 Soft IRQ 서비스 핸들러 호출을 마치면 다음 코드를 실행합니다. Soft IRQ 에서 가장 중요한 코드입니다.
46 pending = local_softirq_pending();
47 if (pending) {
48 if (time_before(jiffies, end) && !need_resched() &&
49     --max_restart)
50 goto restart;
51
52 wakeup_softirqd();
53 }

46번째 줄 코드를 보면 다시 Soft IRQ 서비스 요청이 있었는지 확인합니다. 

__do_softirq() 함수 첫 부분에서 Soft IRQ 서비스 요청이 있는지 확인했는데 왜 다시 확인할까요? 그 이유는 이 함수가 irq_exit() 함수에서 호출될 수도 있지만 ksoftirqd 스레드가 실행하는 run_ksoftirqd() 함수에서도 __do_softirq() 함수를 호출할 수 있기 때문입니다. ksoftirqd 스레드 입장에서 __do_softirq 함수 실행 도중 인터럽트가 다시 발생하면 누군가 Soft IRQ 서비스를 요청했을 수 있습니다. 그래서 이 조건을 다시 검사하는 것입니다.

만약 Soft IRQ 서비스 요청이 있으면 48번째 코드를 실행합니다. 여기서 다음 2가지 조건을 점검합니다. 
1. restart 레이블을 10번 이상을 실행했는지
2. time_before() 함수를 써서 __do_softirq() 함수 실행 시간이 2ms을 넘어서지는 않았는지 

time_before() 함수에 전달하는 jiffies 값과 end의 의미는 뭘까요? jiffies는 48번째 코드를 실행할 때 시각 정보를 담고 있습니다. end란 지역 변수는 __do_softirq() 함수가 처음 실행할 때 시각 정보를 담고 있는 jiffies에 2밀리 초를 jiffies로 환산한 값을 더한 값을 저장합니다. 

jiffies와 end인자로 time_before() 타이머 함수를 호출해서 __do_softirq() 함수를 실행한 시간이 2밀리 초를 넘지 않았으면 1, 2밀리 초를 넘으면 0을 반환합니다.

max_restart 지역 변수는 처음에 10으로 설정했고 restart 레이블을 실행할 때마다 1만큼 뺍니다. 그런데 이 값이 0이면 뭘 의미일까요? 10번 restart 레이블을 실행했다는 정보입니다. __do_softirq() 함수 실행 시간이 오래 걸리는 것을 방지하기 위한 코드입니다.

이번 절에서는 Soft IRQ 서비스를 언제 실행하는지 살펴봤습니다.
  1. 인터럽트 핸들링 후 Soft IRQ 서비스 진입
  2. Soft IRQ 서비스 요청 점검
  3. __do_softirq() 함수에서 Soft IRQ 서비스 핸들러 실행
  - 만약 __do_softirq() 실행 시각이 2밀리초를 넘으면 ksoftirq 스레드를 깨움

이어서 __do_softirq() 함수에서 ksoftirq 스레드를 깨우는 과정을 살펴보겠습니다.
 
6.8.4 ksoftirqd 스레드 깨우기

Soft IRQ 서비스 핸들러를 호출하는 __do_softirq() 함수 실행 시간이 길면 ksoftirqd 스레드를 깨웁니다. 

ksoftirqd 스레드를 깨우는 wakeup_softirqd() 함수 분석하기

이어서 wakeup_softirqd() 함수를 분석하겠습니다.
1 static void wakeup_softirqd(void)
2 {
3 /* Interrupts are disabled: no need to stop preemption */
4 struct task_struct *tsk = __this_cpu_read(ksoftirqd);
5
6 if (tsk && tsk->state != TASK_RUNNING)
7 wake_up_process(tsk);
8 }

4번째 줄 코드를 보면 CPU 별로 생성된 ksoftirqd 스레드의 태스크 디스크립터를 가져옵니다. 

다음에 볼 6 번째 줄 코드는 예외 처리를 위한 조건문입니다. 
 - tsk 이란 지역 변수가 태스크 디스크립터 주소를 저장
 - ksoftirqd 스레드가 TASK_RUNNING 상태가 아닌지 점검

이미 ksoftirqd 프로세스 실행 요청을 한 상태면 ksoftirqd 프로세스를 깨우지 않겠다는 의도입니다.

이 조건을 만족하면 wake_up_process() 함수를 호출해서 ksoftirqd 프로세스를 깨웁니다.

이렇게 __do_softirq() 함수가 처리 도중 wakeup_softirqd() 함수를 호출하면 ksoftirqd 스레드를 깨웁니다. wakeup_softirqd() 함수를 실행하면 ksoftirqd 스레드를 깨우는 동작을 수행합니다. 나중에 프로세스 레벨에서 스케줄링 되어 실행됩니다. 

ksoftirqd 스레드 핸들러인 run_ksoftirqd() 함수 분석하기
이번에 ksoftirqd 스레드를 어떻게 실행하는지 분석합니다. 다음에 볼 함수는 ksoftirqd 스레드 핸들러인 run_ksoftirqd() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 static void run_ksoftirqd(unsigned int cpu)
2 {
3 local_irq_disable();
4 if (local_softirq_pending()) {
5 __do_softirq();
6 local_irq_enable();
7 cond_resched_rcu_qs();
8 return;
9 }
10 local_irq_enable();
11}

4번째 줄 코드를 보면 역시 local_softirq_pending 함수를 호출해서 Soft IRQ 서비스 요청이 있었는지 점검 후 __do_softirq() 함수를 호출합니다. 참고로 smp 핫플러그인 스레드로 실행하는 ksoftirqd 쓰레드의 속성은 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};

여기까지 Soft IRQ 서비스를 어떻게 처리하는지 살펴봤습니다. 여러 코드를 넘나들면서 분석을 했습니다. 이렇게 Soft IRQ는 IRQ 스레드 기법에 비해 인터럽트 후반부 처리를 더 세밀하게 처리합니다. 하지만 인터럽트 후반부 관점으로 Soft IRQ 동작을 다음과 같이 정리할 수 있습니다.
 1. 인터럽트가 발생하면 인터럽트 핸들러에서 Soft IRQ 서비스 요청 
 2. 요청한 Soft IRQ 서비스는 인터럽트 핸들러 수행이 끝나면 다음 함수 흐름으로 Soft IRQ 서비스 실행 
; irq_exit -> invoke_softirq -> __do_softirq 
 3. __do_softirq 함수 실행 시간이 2ms이상 이거나 10번 이상 Soft IRQ 서비스 핸들러를 호출하면 ksoftirqd 스레드를 깨우고 __do_softirq 함수는 바로 실행을 마무리 
 4. ksoftirqd 스레드가 깨어나면 이전 __do_softirq 함수가 실행 제한 조건에 걸려 처리 못한 Soft IRQ 서비스를 실행합니다.

다음 소절에서는 Soft IRQ 에서 Bottom Half 임무를 수행하는 ksoftirqd 프로세스에 대해 알아보겠습니다.

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인




[리눅스커널][SoftIRQ] Soft IRQ 서비스는 누가 언제 처리하나?(1/2) 6장. 인터럽트 후반부 처리

6.8 Soft IRQ 서비스는 누가 언제 처리하나?

이제 Soft IRQ 기법의 핵심인 Soft IRQ 서비스를 처리하는 흐름을 살펴볼 차례입니다. 

Soft IRQ 서비스는 언제 처리할까요? 아래 그림과 같이 인터럽트 핸들러를 처리하는 인터럽트 서비스 루틴이 끝나는 시점에 Soft IRQ 서비스 처리를 시작합니다. 
 
[그림 6.12]  Soft IRQ 서비스 요청 실행 흐름도

Soft IRQ 서비스 처리를 시작하는 코드를 점검하려면 인터럽트 서비스 루틴이 끝나는 코드부터 확인해야 합니다.

이번 절에서는 Soft IRQ 전체 흐름도에서 볼드체로 된 부분을 점검할 예정이니 눈여겨봅시다.

6.8.1 Soft IRQ 서비스 실행 진입점는 어디일까?

Soft IRQ 서비스 실행 진입점은 인터럽트를 핸들링을 마무리 한 후입니다.
__handle_domain_irq() 함수를 보면서 이 과정을 파악해봅시다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/irqdesc.c]
1 int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
2 bool lookup, struct pt_regs *regs)
3 {
4 struct pt_regs *old_regs = set_irq_regs(regs);
5 unsigned int irq = hwirq;
6 int ret = 0;
7
8 irq_enter();
9
10 #ifdef CONFIG_IRQ_DOMAIN
11 if (lookup)
12 irq = irq_find_mapping(domain, hwirq);
13 #endif
14
15 /*
16 * Some hardware gives randomly wrong interrupts.  Rather
17 * than crashing, do something sensible.
18 */
19 if (unlikely(!irq || irq >= nr_irqs)) {
20 ack_bad_irq(irq);
21 ret = -EINVAL;
22 } else {
23 generic_handle_irq(irq);
24 }
25
26 irq_exit(); 
다음 23번째 줄 코드에서 호출하는generic_handle_irq() 함수 서브 루틴에서 인터럽트 핸들러를 호출합니다.
19 if (unlikely(!irq || irq >= nr_irqs)) {
20 ack_bad_irq(irq);
21 ret = -EINVAL;
22 } else {
23 generic_handle_irq(irq);
24 }
25
26 irq_exit();

23 번째 줄 코드 실행을 마치면 26 번재 줄 코드를 실행합니다. 즉, 인터럽트 핸들러 처리를 마치고 irq_exit() 함수를 호출합니다. irq_exit() 함수가 Soft IRQ 서비스를 처리하는 시작점입니다. 

5.2.2 소절에서 봤던 ftrace 로그를 보면 아래 generic_handle_irq() 함수가 보입니다. 이 함수가 호출된 후 62번 인터럽트 핸들러인 dwc_otg_common_irq() 함수가 호출됐습니다.
kworker/0:0-27338 [000] 6028.897809: dwc_otg_common_irq <-__handle_irq_event_percpu
kworker/0:0-27338 [000] 6028.897847: <stack trace>
 => handle_irq_event
 => handle_level_irq
 => generic_handle_irq
 => bcm2836_chained_handle_irq
 => generic_handle_irq
 => __handle_domain_irq
 => bcm2836_arm_irqchip_handle_irq
 => __irq_svc

Soft IRQ 서비스 실행 시작점은 인터럽트 핸들러 실행을 마친 시점입니다. 이어서 irq_exit() 함수를 분석하겠습니다.

6.8.2 Soft IRQ 서비스 요청 점검

이전 소절에 분석한 바와 같이 인터럽트 핸들러 처리를 마무리 한 후 irq_exit() 함수를 호출합니다. irq_exit() 함수에서 Soft IRQ 서비스 요청을 점검하는 부분부터 분석하겠습니다.

Soft IRQ 서비스 시작점인 irq_exit() 함수 분석하기
Soft IRQ 서비스 실행의 출발점인 irq_exit() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 void irq_exit(void)
2 {
3 account_irq_exit_time(current);
4 preempt_count_sub(HARDIRQ_OFFSET);
5 if (!in_interrupt() && local_softirq_pending())
6 invoke_softirq();
7
8 tick_irq_exit();
9 rcu_irq_exit();
10 trace_hardirq_exit(); /* must be last! */
11}

4번째 줄 코드를 보면 preempt_count_sub() 함수를 호출합니다. struct thread_info->preeempt_count 변수에서 HARDIRQ_OFFSET를 뺍니다. 이제는 인터럽트 컨택스트가 아니라고 설정하는 것입니다. 
4 preempt_count_sub(HARDIRQ_OFFSET);
5 if (!in_interrupt() && local_softirq_pending())
6 invoke_softirq();

그다음 5번째 줄 코드에서는 두 가지 조건을 점검합니다.

in_interrupt() 매크로를 호출해서 인터럽트 컨택스트가 아닌지 확인합니다. 4번째 줄 코드인 preempt_count_sub() 함수가 처리된 직후 다른 인터럽트가 발생할 수 있기 때문입니다. 이 때 다음 코드와 같이 preempt_count_add(HARDIRQ_OFFSET)를 실행해서 in_interrupt() 함수가 true를 반환하게 활성화할 수 있습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/hardirq.h]
#define __irq_enter()     \
  do {      \
   account_irq_enter_time(current); \
   preempt_count_add(HARDIRQ_OFFSET); \   
   trace_hardirq_enter();   \
  } while (0)

인터럽트는 언제 어디든 발생할 수 있으니 예외 처리 조건을 추가한 것입니다.

다음은 local_softirq_pending() 함수를 호출해서 Soft IRQ 서비스를 요청했는지 점검합니다. 전처리 코드로 irq_exit() 함수를 확인하면 local_softirq_pending() 함수는 (irq_stat[(current_thread_info()->cpu)].__softirq_pending) 코드로 치환됩니다. 
void irq_exit(void)
{
...
__preempt_count_sub((1UL << ((0 + 8) + 8)));
if (!((preempt_count() & ((((1UL << (4))-1) << ((0 + 8) + 8)) | (((1UL << (8))-1) << (0 + 8)) | (((1UL << (1))-1) << (((0 + 8) + 8) + 4))))) && (irq_stat[(current_thread_info()->cpu)].__softirq_pending))
invoke_softirq();

Soft IRQ 서비스를 실행하는 invoke_softirq() 함수 분석하기

이어서 Soft IRQ 서비스를 실행하는 invoke_softirq() 함수를 점검해 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 static inline void invoke_softirq(void)
2 {
3
4 if (!force_irqthreads) {
5 #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
6 __do_softirq();
7  #else
8 do_softirq_own_stack();
9 #endif
10 } else {
11 wakeup_softirqd();
12 }
13 }

위 코드에서 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 컨피그에 따라 8번째 줄 혹은 10번째 줄 코드가 컴파일됩니다. 라즈비안에서 리눅스를 빌드할 때 생성되는 .config 파일을 열어서 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 가 있는지 확인해야 할까요? 그럴 필요 없이 다음과 같이 전처리 코드를 열어보면 실제 컴파일하는 코드를 보여줍니다.
1 static inline void invoke_softirq(void)
2 {
3 if (!force_irqthreads) {
 # 361 "/home/pi/dev_raspberrian/src/rasp_kernel/linux/kernel/softirq.c"
4   do_softirq_own_stack();
5  } else {
6   wakeup_softirqd();
7  }

3번째 줄 코드부터 볼까요? force_irqthreads 변수를 1로 설정했으면 wakeup_softirqd 함수를 호출해서 ksofrirqd 스레드를 깨웁니다. 이 변수는 리눅스 커널을 부팅하기 전 부트로더에서 threadirqs를 커맨드라인으로 전달하면 설정됩니다. 라즈비안에서 기본으로 0으로 설정돼 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/manage.c]
__read_mostly bool force_irqthreads;
static int __init setup_forced_irqthreads(char *arg)
{
force_irqthreads = true;
return 0;
}
early_param("threadirqs", setup_forced_irqthreads);

따라서 do_softirq_own_stack() 함수를 호출합니다.

커맨드 라인이란 부트로더에서 리눅스 커널을 부팅시킬 때 전달하는 아규먼트와 같은 역할을 합니다. 리눅스 커널이 부팅할 때 이 커맨드 라인을 읽어서 커널 시스템 설정을 합니다. 

이어서 do_softirq_own_stack() 함수를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
static void do_softirq_own_stack(void)
{
__do_softirq();
}

do_softirq_own_stack() 함수는 바로 __do_softirq() 함수를 호출합니다.

정리하면 invoke_softirq() 함수는 특별한 동작을 수행하지 않습니다. 바로 __do_softirq() 함수를 호출합니다.


# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



[리눅스커널][SoftIRQ] Soft IRQ 서비스는 언제 요청하나? 6장. 인터럽트 후반부 처리

6.7 Soft IRQ 서비스는 언제 요청하나?

이번 시간에는 Soft IRQ 서비스를 어떻게 요청하는지 배워 보겠습니다. 우리는 이전 절에서 Soft IRQ를 등록하는 과정을 배웠습니다. 다음 코드와 같이 open_softirq() 함수를 써서 TIMER_SOFTIRQ 타입의 Soft IRQ 서비스를 등록했습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c]
1 void __init init_timers(void)
2 {
3 init_timer_cpus();
4 open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
5 }

 TIMER_SOFTIRQ 타입의 Soft IRQ 서비스를 등록만 하면 핸들러 함수인 run_timer_softirq() 함수가 호출될까요? 그렇지 않습니다. 따로 Soft IRQ 서비스를 요청해야 합니다. 인터럽트 핸들러를 등록하는 과정보다 약간 복잡합니다.

6.7.1 Soft IRQ 서비스 요청 전체 흐름도

전체 Soft IRQ 구조에서 Soft IRQ 서비스를 요청하는 흐름도는 다음과 같습니다.
  
[그림 6.11] Soft IRQ 서비스 요청 시 자료구조

위 그림과 같이 __softirq_pending 변수의 비트 필드 순서는 Soft IRQ 벡터에 대응합니다. 즉, __softirq_pending 변수의 두 번째 비트 필드가 1이면 run_timer_softirq 함수가 호출되는 것입니다.

6.7.2 raise_softirq() 함수 분석

Soft IRQ 서비스를 요청하려면 raise_softirq() 함수 혹은 __raise_softirq_irqoff() 함수를 호출해야 합니다. raise_softirq() 함수 분석으로 Soft IRQ 서비스 요청 세부 동작을 알아봅시다.

raise_softirq() 함수 코드를 함께 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 void raise_softirq(unsigned int nr)
2 {
3 unsigned long flags;
4
5 local_irq_save(flags);
6 raise_softirq_irqoff(nr);
7 local_irq_restore(flags);
8 }
9
10 inline void raise_softirq_irqoff(unsigned int nr)
11{
12 __raise_softirq_irqoff(nr);
13
14 if (!in_interrupt())
15 wakeup_softirqd();
16 }

raise_softirq() 함수 6 번째 줄 코드를 보면 raise_softirq_irqoff() 함수를 호출합니다. 이어서 raise_softirq_irqoff() 함수에서 __raise_softirq_irqoff() 함수를 호출합니다.

Soft IRQ 서비스를 요청하려면 raise_softirq() 혹은 __raise_softirq_irqoff() 함수를 호출해야 하는데 두 함수의 차이점은 뭘까요?

raise_softirq() 함수는 local_irq_save()와 local_irq_store() 함수를 써서 raise_softirq_irqoff() 함수 실행 도중 인터럽트 동기화를 수행합니다. 그래서 대부분 Soft IRQ 서비스를 요청할 때 raise_softirq() 함수를 씁니다. 다만 이미 인터럽트 발생에 필요없다면 __raise_softirq_irqoff() 함수를 써도 됩니다.


raise_softirq_irqoff() 함수 14~16 번째 줄 코드로 흥미로운 사실을 유추할 수 있습니다.
14 if (!in_interrupt())
15 wakeup_softirqd();
16 }

14 번째 줄은 현재 실행 코드가 인터럽트 컨택스트 아닌지 점검합니다. 인터럽트 컨택스트가 아니면 15 번째 줄 코드와 같이 wakeup_softirq() 함수를 호출해 ksoftirqd 스레드를 깨웁니다. 

위 코드 분석으로 다음과 같은 사실을 알 수 있습니다.
인터럽트 컨택스트가 아닐 때도 Soft IRQ 서비스 요청을 할 수 있습니다.

이번에는 __raise_softirq_irqoff() 함수 코드를 분석할 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 void __raise_softirq_irqoff(unsigned int nr)
2 {
3 trace_softirq_raise(nr);
4 or_softirq_pending(1UL << nr);
5 }

3 번째 줄 코드는 softirq_raise이란 ftrace 이벤트 로그를 출력합니다. softirq_raise 이벤트가 켜져 있을 때만 실행합니다.

다음 4 번째 줄 코드를 봅시다.
입력 인자인 nr을 왼쪽으로 쉬프트한 값을 or_softirq_pending() 함수에 전달합니다.

다음 소절에선 or_softirq_pending() 함수 분석을 진행합니다.

6.7.3 irq_stat 전역 변수 분석

or_softirq_pending() 함수를 호출하면 irq_stat 전역 변수에 비트 마스크를 설정합니다.
Soft IRQ 서비스를 요청하면 이 정보를 irq_stat란 전역 변수에 저장합니다.

이어서 or_softirq_pending() 함수 코드를 봅시다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
1 #define or_softirq_pending(x)  (local_softirq_pending() |= (x))
2
3 #define local_softirq_pending() \
4 __IRQ_STAT(smp_processor_id(), __softirq_pending)
5
6 extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */
7 #define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

or_softirq_pending() 함수는 연달아 다른 함수 코드로 치환됩니다. 이해하기 편하게 코드를 펼치면 다음과 같습니다.
or_softirq_pending(x)
  (local_softirq_pending() |= (x))
   __IRQ_STAT(smp_processor_id(), __softirq_pending)
  (irq_stat[cpu].member)

or_softirq_pending 매크로 함수를 쓰면 다음 코드가 실행된다는 점을 알 수 있습니다.
(irq_stat[cpu].__softirq_pending) |= (1UL << x)

분석한 코드 내용을 종합하면 위 코드 연산 결과는 다음과 같이 알기 쉽게 표현할 수 있습니다.
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

irq_stat[cpu]__softirq_pending |=  (1UL << nr)
irq_stat[cpu]__softirq_pending =  irq_stat[cpu]__softirq_pending + (1UL << nr)

원래 __softirq_pending 변수가 저장한 값과 “|=” 연산을 하니 __softirq_pending 필드가 저장한 값은 그대로 두는 것입니다. 

매크로 함수 중에 smp_processor_id() 함수는 현재 실행 중인 CPU 번호를 알려 줍니다. 코드를 잠깐 보면 smp_processor_id() 함수는 current_thread_info()->cpu로 치환됩니다. 5장에서 배운 in_interrupt() 함수와 비슷한 원리입니다.

이번에 irq_stat 전역 변수를 소개합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irq_cpustat.h]
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */

irq_stat 는 CPU 개수만큼 배열입니다.

라즈베리파이는 4개의 CPU를 쓰므로 irq_stat 변수를 Trace32 프로그램으로 보면 다음과 같습니다.
v.v %t %d %h %i irq_stat
(static irq_cpustat_t [4]) irq_stat = (
    [0] = (
      (unsigned int) __softirq_pending = 0 = 0x1,
    [1] = (
      (unsigned int) __softirq_pending = 0 = 0x3,
    [2] = (
      (unsigned int) __softirq_pending = 0 = 0x0,
    [3] = (
      (unsigned int) __softirq_pending = 0 = 0x6,

커널은 CPU 별로 지정한 __softirq_pending 변수에 (1 << Soft IRQ 서비스 아이디) 비트 연산 결과를 저장합니다. Soft IRQ 서비스 아이디를 왼쪽 시프트 한 결괏값입니다.

각 CPU 별로 저장한 __softirq_pending 변수를 해석하면 다음과 같습니다. 
CPU 번호 10진수 2진수 비트 연산자 Soft IRQ 서비스 아이디
0 1 001 (1 << 0) HI_SOFTIRQ
1 2 010 (1 << 1) TIMER_SOFTIRQ
2 0 000 0 N/A
3 6 110 (1 << 2) | (1 << 1) NET_TX_SOFTIRQ | TIMER_SOFTIRQ

각 Soft IRQ 서비스 아이디 값은 다음과 같이 선언돼있습니다. HI_SOFTIRQ 아이디가 0이고, RCU_SOFTIRQ가 9입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h] 
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, 
RCU_SOFTIRQ,  
NR_SOFTIRQS
};

Trace32 프로그램으로 확인한 irq_stat 값과 위에서 연산한 결과를 주석문으로 같이 확인해보겠습니다.    
(static irq_cpustat_t [4]) irq_stat = (
    [0] = (
      (unsigned int) __softirq_pending = 0 = 0x1, // HI_SOFTIRQ
    [1] = (
      (unsigned int) __softirq_pending = 0 = 0x2, // HI_SOFTIRQ | TIMER_SOFTIRQ
    [2] = (
      (unsigned int) __softirq_pending = 0 = 0x0,
    [3] = (
      (unsigned int) __softirq_pending = 0 = 0x6, // NET_TX_SOFTIRQ | TIMER_SOFTIRQ

CPU0에는 HI_SOFTIRQ이란 Soft IRQ 서비스를 요청했고, CPU1는 HI_SOFTIRQ와 TIMER_SOFTIRQ Soft IRQ 서비스를 요청한 상태입니다. CPU3에서 요청된 Soft IRQ 서비스는 NET_TX_SOFTIRQ와 TIMER_SOFTIRQ입니다.


이렇게 or_softirq_pending란 함수를 3단계로 매크로가 매크로를 호출하니 소스를 따라가기 어렵습니다. 이럴 때는 3장에서 소개했듯이 전처리 코드를 보면 효율적으로 커널 코드를 읽을 수 있습니다. 전처리 코드는 매크로를 모두 푼 정보를 담고 있기 때문입니다.

전처리 코드를 보겠습니다. 
1 void __raise_softirq_irqoff(unsigned int nr)
2 {
3  trace_softirq_raise(nr);
4  ((irq_stat[(current_thread_info()->cpu)].__softirq_pending) |= (1UL << nr));
5 }

전처리 코드를 보니 or_softirq_pending(1UL << nr); 코드는 위와 같은 4번째 줄 코드로 치환됩니다. irq_stat[cpu갯수].__softirq_pending 멤버에 Soft IRQ enum 값을 왼쪽으로 쉬프트한 값을 OR 연산으로 저장합니다.


6.7.4 Soft IRQ 서비스를 요청했는지는 누가 어떻게 점검할까?

Soft IRQ 서비스를 요청하는 코드를 살펴봤습니다. Soft IRQ 서비스를 요청했으면 어디선가 이 서비스 요청 알아보고 점검할 것입니다. Soft IRQ 서비스를 요청했는지는 누가 어떻게 점검할까요? 

Soft IRQ 서비스 요청 여부는 인터럽트 핸들러 수행이 끝나면 호출하는 irq_exit()과 ksoftirqd 스레드 핸들 함수인 run_ksoftirqd() 함수에서 점검합니다. 이 때 커널에서 제공하는 local_softirq_pending() 함수를 호출합니다.

실제 소스 코드를 함께 살펴보겠습니다. 다음 코드 irq_exit() 함수에서 local_softirq_pending() 함수를 호출합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 void irq_exit(void)
2 {
...
3 if (!in_interrupt() && local_softirq_pending())
4 invoke_softirq();

irq_exit() 함수에서 2가지 조건을 점검한 후 Soft IRQ 서비스를 실행할지 결정합니다. 첫 번째는 현재 실행 중인 코드가 인터럽트 컨택스트 상태인지와 두 번째로 Soft IRQ 서비스 요청이 있었는지를 점검합니다.

irq_exit 함수를 전처리 코드에서 보면 local_softirq_pending() 함수는 irq_stat[(current_thread_info()->cpu)].__softirq_pending)로 치환됨을 알 수 있습니다.
1 void irq_exit(void)
2 {
...
3 if (!((preempt_count() & ((((1UL << (4))-1) << ((0 + 8) + 8)) | (((1UL << (8))-1) << (0 + 8)) | (((1UL << (1))-1) << (((0 + 8) + 8) + 4))))) && (irq_stat[(current_thread_info()->cpu)].__softirq_pending))

ksoftirqd 스레드도 마찬가지입니다. 다음 4번째 줄 코드에서 local_softirq_pending 함수를 호출해서 Soft IRQ 서비스 요청이 있었는지 점검합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
1 static void run_ksoftirqd(unsigned int cpu)
2 {
3 local_irq_disable();
4 if (local_softirq_pending()) {
5 __do_softirq();

run_ksoftirqd() 함수를 전처리 코드로 열어봐도 (irq_stat[(current_thread_info()->cpu)].__softirq_pending) 코드가 실행함을 알 수 있습니다.
[./kernel/.tmp_softirq.i]
static void run_ksoftirqd(unsigned int cpu)
{
  do { arch_local_irq_disable(); trace_hardirqs_off(); } while (0);
  if ((irq_stat[(current_thread_info()->cpu)].__softirq_pending)) {
   __do_softirq();

이렇게 __raise_softirq_irqoff 함수를 써서 irq_stat[(current_thread_info()->cpu)].__softirq_pending)에 값을 써주면 나중에 이 변수를 읽어서 확인하는 것입니다.

이번 절에 배운 내용을 Q/A 로 간단히 정리해 보겠습니다.

Q: Soft IRQ 서비스는 어떻게 요청할까?

A: 인터럽트 핸들러에서 or_softirq_pending()이란 함수를 호출해서 Soft IRQ 서비스를 요청합니다. 

Q: Soft IRQ 서비스 요청은 어느 코드에서 점검할까?

A: 인터럽트 핸들러 실행을 마친 후 호출되는 irq_exit() 함수가 ksoftirqd 스레드 핸들인 run_softirqd() 함수에서 local_softirq_pending() 함수를 호출해 Soft IRQ 요청을 확인합니다. 

또한 Soft IRQ 서비스 핸들러를 호출하는  __do_softirq() 함수에서 각 CPU 별로 irq_stat[cpu].__softirq_pending 변수에 접근해서 비트 필드를 점검합니다. 비트 필드가 활성화됐으면 해당 softirq_vec에 접근해서 Soft IRQ 서비스 핸들러 함수를 호출합니다.

다음 절에는 Soft IRQ 서비스 요청을 어디서 처리하는지 살펴봅니다. 

# Reference 인터럽트 후반부 처리








6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인


    [리눅스커널] Soft IRQ 서비스에 대해서 6장. 인터럽트 후반부 처리

    6.6 Soft IRQ 서비스

    이번 절에서는 Soft IRQ 서비스의 의미와 서비스 등록 과정에 대해 살펴봅니다. 

    6.6.1 Soft IRQ 서비스 알아보기 

    Soft IRQ를 설명하면서 낯선 용어를 설명했는데, 이제부터 그 의미를 하나하나씩 살펴 보겠습니다. Soft IRQ 서비스란 용어를 소개합니다.

    Soft IRQ 서비스를 빨리 이해하려면 코드를 먼저 봐야 합니다. Soft IRQ 서비스는 아래 enum으로 정의한 코드와 같습니다.
    [https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/interrupt.h]
    enum
    {
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, 
    RCU_SOFTIRQ,  
    NR_SOFTIRQS
    };

    const char * const softirq_to_name[NR_SOFTIRQS] = {
    "HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
    "TASKLET", "SCHED", "HRTIMER", "RCU"
    };

    리눅스 커널에서는 Soft IRQ 서비스 종류를 지정해 놨습니다. 위 코드를 보면 10개나 됩니다. enum 타입으로 정의된 이름으로 타이머, 네트워크 그리고 블록 디바이스별로 Soft IRQ 서비스가 있습니다.

    Soft IRQ 서비스는 어떻게 실행할까요? 각 Soft IRQ 서비스별로 등록된 서비스 핸들러 함수가 실행할 때 Soft IRQ 서비스를 실행합니다. Soft IRQ 서비스 핸들러는 어디서 확인할 수 있을까요? softirq_vec 변수가 Soft IRQ 서비스 핸들러 정보를 저장합니다.

    다음에 라즈비안에서 Soft IRQ 서비스는 어떤 코드에서 등록하는지 알아봅시다.

    6.6.2 Soft IRQ 서비스 핸들러는 언제 등록할까?

    Soft IRQ 서비스 핸들러 등록과 Soft IRQ 서비스 등록은 같은 의미입니다. 왜냐면 Soft IRQ 서비스 등록을 할 때 Soft IRQ 핸들러 함수를 등록해야 하기 때문입니다.

    Soft IRQ 서비스 핸들러를 등록하려면 다음 규칙에 따라 open_softirq() 함수를 호출해야 합니다. 
    open_softirq(Soft IRQ 서비스 아이디, Soft IRQ 서비스 핸들러);

    우선 TIMER_SOFTIRQ이란 Soft IRQ 서비스 아이디로 Soft IRQ 서비스 핸들러를 등록하는 코드를 살펴 보겠습니다.
    [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c]
    1 void __init init_timers(void)
    2 {
    3 init_timer_cpus();
    4 open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
    5 }

    위 코드를 보면 init_timer() 함수에서 open_softirq() 함수를 호출합니다. 첫 번째 인자는 Soft IRQ 서비스 아이디, 두 번째로 run_timer_softirq() 함수를 지정합니다. 

    참고로, init_timers() 함수 선언부를 보면 __init 매크로가 붙어 있으니 부팅 도중 한번 실행됩니다.

    open_softirq() 함수 코드를 보면 예상한 대로 Soft IRQ 서비스 핸들러 정보를 저장하는 softirq_vec에 함수 주소를 지정합니다.
    [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
    void open_softirq(int nr, void (*action)(struct softirq_action *))
    {
    softirq_vec[nr].action = action;
    }

    커널에서는 softirq_vec 전역 변수는 Soft IRQ 벡터라고 부릅니다. 이 전역 변수는 Soft IRQ 서비스 핸들러 함수 포인터를 저장합니다. softirq_vec은 NR_SOFTIRQS 크기만큼 배열입니다. 
    [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
    static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

    이렇게 각 Soft IRQ 서비스 아이디 별로 Soft IRQ 서비스를 등록하면 softirq_vec[nr].action 필드에 Soft IRQ 서비스 핸들러 함수를 볼 수 있습니다.

    조금 후 살펴볼 예정이지만, Soft IRQ 서비스 핸들러는 다음 코드와 같이 __do_softirq() 함수에서 호출합니다. TIMER_SOFTIRQ Soft IRQ 서비스 아이디인 경우 __do_softirq() 함수에서 run_timer_softirq() 함수를 호출합니다.
    [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c]
    1 asmlinkage __visible void __softirq_entry __do_softirq(void) {
    2 struct softirq_action *h;
    3 h = softirq_vec;
    4 h->action(h);

    위 코드는 __do_softirq() 함수에서 Soft IRQ 벡터인 softirq_vec 변수를 읽어서 Soft IRQ 핸들러 함수를 호출하는 부분만 가져온 것입니다. 위 4번째 줄 코드에서 Soft IRQ 핸들러 함수를 호출합니다.

    여기까지 Soft IRQ 서비스를 등록 과정을 살펴봤습니다. 세부 코드는 간단합니다. Soft IRQ 서비스 아이디와 Soft IRQ 서비스 핸들러를 지정해서 open_softirq() 함수를 호출하는 것입니다.

    라즈비안에서는 Soft IRQ 서비스를 어떻게 등록하는지 알아볼까요? 코드만 봐서 Soft IRQ 서비스 전체 개수를 파악하기는 어렵습니다. 직접 라즈베리파이에서 Soft IRQ 서비스를 어떻게 등록하는지 알아 보겠습니다.

    # Reference 인터럽트 후반부 처리








    6.9 Soft IRQ 서비스는 누가 언제 처리하나?




    6.13 Soft IRQ 디버깅
    6.13.1 ftrace Soft IRQ 이벤트 분석 방법
    6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



    [리눅스커널] Soft IRQ 소개 6장. 인터럽트 후반부 처리

    6.5 Soft IRQ 소개

    Soft IRQ는 리눅스 커널을 이루는 핵심 기능 중 하나입니다. Soft IRQ 서비스 형태로 커널의 타이머, 스케줄링은 물론 네트워크 시스템에서 사용합니다. Soft IRQ 은 인터럽트 후반부 기법으로 씁니다. 이번 절에서는 Soft IRQ를 인터럽트 후반부 처리 중심으로 살펴봅니다. Soft IRQ 전체 구조를 익힌 다음 Soft IRQ에서 프로세스, 타이머 혹은 네트워크 시스템을 어떻게 처리하는지 알아보는 것이 좋습니다.  

    6.5.1 Soft IRQ는 왜 알아야 할까?

    Soft IRQ에 대해 알아보기 전 먼저 Soft IRQ를 왜 알아야 하는지 생각해 봅시다.

    1. 리눅스 커널 입문자를 벗어나 중급 수준 개발자가 되려면 Soft IRQ가 뭔지 알아야 합니다. 반응 속도에 민감한 네트워크 패킷 처리나 고속 그래픽 처리 및 스토리지(UFS) 드라이버들은 Soft IRQ 핸들러를 이용해서 구현됐기 때문입니다.

    2. 우리는 인터럽트가 발생하면 일하던 프로세스를 멈추고 인터럽트 핸들러를 실행한다고 알고 있습니다. 그런데 Soft IRQ는 인터럽트 핸들러가 수행하면 일하던 프로세스로 돌아가지 않고 바로 Soft IRQ 실행을 시작합니다. Soft IRQ 에서 실행 속도가 늦으면 시스템 반응 속도가 늦어질 수 있습니다. 시스템 성능 문제가 생겼을 때 체크해야 할 점검 포인트 중 하나입니다. 그래서 시스템 전반을 책임지는 개발자는 Soft IRQ를 잘 알아야 합니다.

    3. 커널 타이머를 제대로 이해하려면 Soft IRQ 구조를 알아야 합니다. 드라이버에서 요청한 로컬 타이머들은 타이머 인터럽트가 발생한 다음 Soft IRQ 서비스로 실행하기 때문입니다.

    4. 태스크릿은 Soft IRQ 서비스 형태로 사용합니다. 태스크릿을 제대로 이해하려면 Soft IRQ를 알아야 합니다.

    6.5.2 Soft IRQ 서비스란

    Soft IRQ 를 실행한다는 것은 Soft IRQ 서비스 요청을 받아 이를 처리하는 과정입니다. Soft IRQ 전체 흐름을 이해하려면 먼저 Soft IRQ 서비스에 대해 알아야 합니다.

    Soft IRQ 서비스 소개

    우선 Soft IRQ 서비스란 용어를 알아봅시다. 리눅스 커널에서는 다음과 같이 10가지 Soft IRQ 서비스를 지원합니다. 
    [include/linux/interrupt.h]
    const char * const softirq_to_name[NR_SOFTIRQS] = {
    "HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
    "TASKLET", "SCHED", "HRTIMER", "RCU"
    };

    “HI” 부터 “RCU” 까지가 Soft IRQ 서비스 이름입니다.
    Soft IRQ 서비스는 부팅할 때 open_softirq() 란 함수를 써서 등록합니다.

    각 Soft IRQ 서비스 종류는 다음 테이블과 같습니다.
    우선순위 Soft IRQ 서비스 설명
    0 HI_SOFTIRQ 가장 우선 순위가 높으며 TASKLET_HI로 적용 
    1 TIMER_SOFTIRQ 동적 타이머로 사용
    2 NET_TX_SOFTIRQ 네트워크 패킷 송신용으로 사용
    3 NET_RX_SOFTIRQ 네트워크 패킷 수신용 사용
    4 BLOCK_SOFTIRQ 블록 디바이스에서 사용
    5 IRQ_POLL_SOFTIRQ IRQ_POLL 연관 동작
    6 TASKLET_SOFTIRQ 일반 태스크릿으로 사용
    7 SCHED_SOFTIRQ 스케줄러에서 주로 사용
    8 HRTIMER_SOFTIRQ 현재 사용 안하나 하위 호완성을 위해 남겨둠
    9 RCU_SOFTIRQ RCU 처리용으로 사용

    이어서 Soft IRQ 서비스 실행 흐름을 단계별로 소개합니다.

    Soft IRQ 서비스 라이프 사이클

    Soft IRQ 서비스의 실행 흐름은 크게 3 단계로 나눌 수 있습니다. 다음 그림을 Soft IRQ 서비스 라이프 사이클에 대해서 알아봅시다.

     
      [그림 6.9] Soft IRQ 서비스 단계 별 실행 흐름

    부팅 과정에서는 Soft IRQ 서비스를 1번 등록하고 [2]과 [3] 단계는 매우 자주 반복 실행합니다.

    [1] 단계: 부팅 과정 
    부팅 과정에서 open_softirq() 함수를 호출해 Soft IRQ 서비스를 등록합니다.

    [2] 단계: 인터럽트 처리 
    인터럽트 핸들러(인터럽트 컨택스트)나 인터럽트 서브 함수에서 raise_softirq() 함수를 호출해 Soft IRQ 서비스를 요청합니다. raise_softirq() 함수 이름 그대로 "Soft IRQ를 올린다"라고 부르는 경우도 있습니다.

    이번 Soft IRQ 절에서는 이 동작을 "Soft IRQ 서비스를 요청한다"고 명시합니다.

    [3] 단계: Soft IRQ 컨택스트 
    __do_softirq() 함수에서 이미 요청한 Soft IRQ 서비스를 실행합니다.

    인터럽트 핸들링이 끝나고 Soft IRQ 서비스를 바로 실행하는 동작을 Soft IRQ 컨택스트라고 합니다.
     
    Soft IRQ 서비스 핸들러

     Soft IRQ 서비스 핸들러는 Soft IRQ 서비스를 실행할 때 호출하는 함수입니다. 부팅 과정에서 open_softirq() 함수로 Soft IRQ 서비스를 등록할 때 softirq_vec이란 전역 변수에 등록합니다. 다음 softirq_vec 전역 변수는 Trace32로 라즈베리안에 등록된 Soft IRQ 서비스 핸들러 정보입니다. 
    (static struct softirq_action [10]) [D:0x80C02080] softirq_vec = (
        [0] = ((void (*)()) [D:0x80C02080] action = 0x80122888 = tasklet_hi_action),
        [1] = ((void (*)()) [D:0x80C02084] action = 0x80181270 = run_timer_softirq),
        [2] = ((void (*)()) [D:0x80C02088] action = 0x80614684 = net_tx_action),
        [3] = ((void (*)()) [D:0x80C0208C] action = 0x80615AB0 = net_rx_action),
        [4] = ((void (*)()) [D:0x80C02090] action = 0x804279B0 = blk_done_softirq),
        [5] = ((void (*)()) [D:0x80C02094] action = 0x0 = ),
        [6] = ((void (*)()) [D:0x80C02098] action = 0x8012299C = tasklet_action),
        [7] = ((void (*)()) [D:0x80C0209C] action = 0x801588EC = run_rebalance_domains),
        [8] = ((void (*)()) [D:0x80C020A0] action = 0x0 = ),
        [9] = ((void (*)()) [D:0x80C020A4] action = 0x8017ABC4 = rcu_process_callbacks))

    이 함수들은 __do_softirq() 함수에서 호출됩니다.

    Soft IRQ 서비스를 요청할 때는 raise_softirq() 함수나 raise_softirq_irqoff() 함수를 호출해야 합니다. 이 때 요청할 Soft IRQ 서비스 아이디를 지정해야 합니다.

    6.5.3 Soft IRQ 전체 흐름도 소개

    Soft IRQ 서비스를 소개했으니 이어서 Soft IRQ 전체 구조를 살펴봅시다.
     
    [그림 6.10]  Soft IRQ 전체 실행 흐름도

    위 그림과 같이 Soft IRQ 전체 동작 4단계로 분류할 수 있습니다. 단계별로 세부 동작을  알아봅시다. 

    [1] 단계
    인터럽트가 발생하면 해당 인터럽트 핸들러에서 Soft IRQ 서비스를 요청합니다. 이를 위해 raise_softirq_irqoff() 함수를 호출해야 합니다. 이 동작은 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하는 것과 유사합니다. 

    [2] 단계
    인터럽트 서비스 루틴 동작이 끝나면 irq_exit() 함수를 호출합니다. 여기서 Soft IRQ 서비스 요청 여부를 점검하고 요청한 Soft IRQ 서비스가 있으면 __do_softirq() 함수를 호출해서 해당 Soft IRQ 서비스 핸들러를 실행합니다. 만약 Soft IRQ 서비스 요청이 없으면 irq_exit() 함수는 바로 종료합니다. 

    [3] 단계
    __do_softirq() 함수에서 Soft IRQ 서비스 핸들러를 호출하다가 2ms 이상 시간이 걸리거나 10번 이상 Soft IRQ 서비스 핸들러를 호출하면 다음 동작을 수행합니다.
     - wakeup_softirqd() 함수를 호출해서 ksoftirqd 프로세스를 깨움
     - __do_softirq() 함수 종료

    __do_softirq() 함수 실행 시간에 제약을 건 이유는 __do_softirq() 함수를 호출하는 irq_exit() 함수가 프로세스가 일을 멈춘 상태에서 실행하기 때문입니다.

    [4] 단계
    ksoftirqd 프로세스가 깨어나 [3] 단계에서 마무리 못 한 Soft IRQ 서비스 핸들러를 실행합니다.

    Soft IRQ가 인터럽트 후반부를 처리하는 흐름을 알아보니 IRQ Thread 구조보다 과정이 더 복잡합니다. IRQ Thread는 인터럽트 핸들러에서 못한 일을 프로세스 레벨에서 실행합니다. 그런데 Soft IRQ 서비스는 인터럽트 핸들러가 수행된 후 바로 일을 시작합니다. 그러니 Soft IRQ 서비스 핸들러는 빨리 실행해야 합니다.

    다음 소절에서는 Soft IRQ를 다른 인터럽트 후반부 기법과 비교해 보겠습니다.

    6.5.4 후반부 기법으로 Soft IRQ는 언제 쓸까? 

    Soft IRQ는 IRQ 스레드 기법보다 더 복잡한 구조인데 IRQ 스레드에 비해 훨씬 반응 속도에 민감한 리눅스 커널 시스템에서 사용합니다. 10개 Soft IRQ 서비스를 인터럽트 핸들러에서 요청하면 프로세스 레벨에서 Soft IRQ 서비스에서만 실행하는 것이 아닙니다. 

    인터럽트 핸들러 처리를 종료한 후 바로 Soft IRQ 서비스는 실행합니다. 따라서 인터럽트가 발생 빈도가 매우 높거나 인터럽트 발생 후 바로 응답해야 하는 경우 Soft IRQ를 적용합니다.

    # Reference 인터럽트 후반부 처리








    6.9 Soft IRQ 서비스는 누가 언제 처리하나?




    6.13 Soft IRQ 디버깅
    6.13.1 ftrace Soft IRQ 이벤트 분석 방법
    6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



      [리눅스커널][인터럽트후반부] ftrace로 IRQ 스레드 핸들러 실행 시각 측정하기 6장. 인터럽트 후반부 처리

      6.5.3 IRQ 스레드 핸들러 실행 시각 측정하기

      이전 절에서는 ftrace 로 콜스택과 함께 인터럽트가 언제 발생하고 언제 IRQ 스레드를 깨우는지 알아봤습니다. 이번에는 실제 인터럽트 핸들러와 IRQ 스레드 핸들러 함수 실행 시간을 측정하는 방법을 소개합니다. 각 함수의 실행 시간을 측정한 결과 인터럽트 핸들러 실행 시간이 길면 인터럽트 핸들러 함수 코드를 점검할 필요가 있습니다. 나중에 여러분이 인터럽트 핸들러를 작성한 후 이 방법을 써서 실행 시간을 측정해 보시길 바랍니다.

      ftrace에서 지원하는 function_graph 트레이서를 쓸려면 다음 config가 설정돼 있어야 합니다.
      CONFIG_DYNAMIC_FTRACE=y
      CONFIG_DYNAMIC_FTRACE_WITH_REGS=y
      CONFIG_FUNCTION_PROFILER=y
      CONFIG_FTRACE_MCOUNT_RECORD=y
      CONFIG_NOP_TRACER=y
      CONFIG_HAVE_FUNCTION_TRACER=y
      CONFIG_HAVE_FUNCTION_GRAPH_TRACER=y
      CONFIG_HAVE_DYNAMIC_FTRACE=y
      CONFIG_HAVE_DYNAMIC_FTRACE_WITH_REGS=y
      CONFIG_HAVE_FTRACE_MCOUNT_RECORD=y

      라즈비안을 빌드하면 생성하는 .config 파일을 확인하면 위 config들이 기본으로 설정돼 있습니다. 라즈비안에서는 기본으로 컨피그가 설정돼 있으니 이 function_graph 트레이서를 쓰면 됩니다. 라즈비안 이외 다른 리눅스 보드로 function_graph 트레이서를 쓰려면 위에서 설명해 드린 커널 컨피그가 설정됐는지 확인해야 합니다.

      인터럽트 핸들러 실행 시각 측정을 위한 function 설정 방법 소개

      function_graph 트레이서 설정을 하려면 다음 명령어를 입력해야 합니다. 다음 코드를 입력한 후 timer_trace.sh란 셸 스크립트로 저장합니다.
      #!/bin/sh

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

      echo 0 > /sys/kernel/debug/tracing/events/enable
      sleep 1
      echo "events disabled"

      echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
      sleep 1
      echo "set_ftrace_filter init"

      echo function_graph > /sys/kernel/debug/tracing/current_tracer
      sleep 1
      echo "function_graph tracer enabled"

      echo bcm2835_mmc_irq  bcm2835_mmc_thread_irq > /sys/kernel/debug/tracing/set_ftrace_filter
      echo bcm2835_mbox_irq  bcm2835_mbox_threaded_irq >> /sys/kernel/debug/tracing/set_ftrace_filter
      echo bcm2835_sdhost_irq >> /sys/kernel/debug/tracing/set_ftrace_filter
      sleep 1
      echo "set_ftrace_filter enabled"

      echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
      echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
      sleep 1

      echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
      echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
      echo "event enabled"

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

      fuction_gragh 트레이서를 설정하고 set_ftrace_filter에 함수를 지정하면 함수 실행 시간을 측정할 수 있습니다. 위 셸 스크립트에서 다음 함수 실행 시각을 측정합니다.
      - bcm2835_mmc_irq()
      - bcm2835_mmc_thread_irq()
      - bcm2835_mbox_irq() 
      - bcm2835_mbox_threaded_irq() 
      - bcm2835_sdhost_irq()

      이 내용을 참고하면서 “source timer_trace.sh” 명령어로 위 셸 스크립트를 실행한 후 20초 후 ftrace 로그를 받습니다.

      ftrace로 인터럽트 핸들러 실행 시간 측정하기

      로그를 열어보니 새로운 형태의 ftrace 로그를 볼 수 있습니다. 같이 로그를 살펴 봅시다.
      먼저 인터럽트 핸들러가 실행한 시간을 확인하겠습니다.
      1 0)               |  /* irq_handler_entry: irq=92 name=mmc1 */
      2 0)   6.823 us    |  bcm2835_mmc_irq();
      3 0)               |  /* irq_handler_exit: irq=92 ret=handled */
      4 0)               |  /* irq_handler_entry: irq=92 name=mmc1 */
      5 0)   2.344 us    |  bcm2835_mmc_irq();
      5 0)               |  /* irq_handler_exit: irq=92 ret=handled */

      92번 mmc1 인터럽트 핸들러인 bcm2835_mmc_irq() 함수 실행 시간이 6.823 us, 2.344 us입니다.

      다음은 86번 인터럽트 핸들러인 bcm2835_sdhost_irq() 함수 실행 시간을 확인한 로그입니다.
      0)               |  /* irq_handler_entry: irq=86 name=mmc0 */
      0)   4.479 us    |  bcm2835_sdhost_irq();
      0)               |  /* irq_handler_exit: irq=86 ret=handled */
      ...
      0)               |  /* irq_handler_entry: irq=86 name=mmc0 */
      0)   6.562 us    |  bcm2835_sdhost_irq();
      0)               |  /* irq_handler_exit: irq=86 ret=handled */

      위 로그에서 볼드체로 된 부분을 보면 실행 시간이 4.479 us, 6.562 us 입니다.

      인터럽트 핸들러 실행 시간이 10나노초 미만입니다. 이 정도면 빠른 시간에 인터럽트 핸들러가 실행됐습니다.

      이번에는 IRQ Thread 핸들러 실행 시간을 확인해 보겠습니다. 관련 함수들 실행 시간과 ftrace 이벤트 로그를 함께 살펴 보겠습니다.

      분석하려는 전체 로그는 다음과 같습니다.
      1 0)               |  /* irq_handler_entry: irq=23 name=3f00b880.mailbox */
      2 0)   7.084 us    |  bcm2835_mbox_irq();
      3 0)               |  /* irq_handler_exit: irq=23 ret=handled */
      4 ------------------------------------------
      5 3)   kworker-32   =>  chromiu-919  
      6 ------------------------------------------
      7
      8 3)               |  /* sched_wakeup: comm=kworker/3:1 pid=32 prio=120 target_cpu=003 */
      9 3)               |  /* sched_switch: prev_comm=chromium-browse prev_pid=919 prev_prio=120 prev_state=R ==> next_comm=kworker/3:1 next_pid=32 next_prio=120 */
      10  ------------------------------------------
      11 3)  chromiu-919   =>   kworker-32  
      12 ------------------------------------------
      13
      14 3)               |  /* sched_wakeup: comm=irq/23-3f00b880 pid=33 prio=49 target_cpu=003 */
      15 3)               |  /* sched_switch: prev_comm=kworker/3:1 prev_pid=32 prev_prio=120 prev_state=R ==> next_comm=irq/23-3f00b880 next_pid=33 next_prio=49 */
      16  ------------------------------------------
      17 3)   kworker-32   =>   irq/23--33  
      18  ------------------------------------------
      19 
      20 3)               |  bcm2835_mbox_threaded_irq() {
      21 3)               |  /* irq=23, process: irq/23-3f00b880  */
      22 3)               |  /* [+] in_interrupt: 0x00000000,preempt_count = 0x00000000, stack=0xb9714000  */
      23 3) ! 153.437 us  |  }

      다음 로그를 보면 23번 인터럽트가 발생 후 bcm2835_mbox_irq() 함수가 7.084us 동안 실행됐습니다.
      1 0)               |  /* irq_handler_entry: irq=23 name=3f00b880.mailbox */
      2 0)   7.084 us    |  bcm2835_mbox_irq();
      3 0)               |  /* irq_handler_exit: irq=23 ret=handled */

      다음 10번 줄 로그를 보겠습니다. “irq/23-3f00b880” IRQ 스레드가 깨어난 후 스케줄링되는 동작입니다.
      10  ------------------------------------------
      11 3)  chromiu-919   =>   kworker-32  
      12 ------------------------------------------
      13
      14 3)               |  /* sched_wakeup: comm=irq/23-3f00b880 pid=33 prio=49 target_cpu=003 */
      15 3)               |  /* sched_switch: prev_comm=kworker/3:1 prev_pid=32 prev_prio=120 prev_state=R ==> next_comm=irq/23-3f00b880 next_pid=33 next_prio=49 */

      11번째 줄 로그를 보면 chromiu-919 프로세스에서 kworker-32 프로세스로 스케줄링이 된 상태입니다. 14, 15번째 줄 로그에 보이는 (3)은 kworker-32  프로세스입니다. kworker-32  프로세스가 pid가 33인 “irq/23-3f00b880” 프로세스를 깨우고 스케줄링을 수행합니다.

      다음 16번 줄부터 “irq/23-3f00b880” IRQ 스레드가 실행하는 로그입니다.
      16  ------------------------------------------
      17 3)   kworker-32   =>   irq/23--33  
      18  ------------------------------------------
      19 
      20 3)               |  bcm2835_mbox_threaded_irq() {
      21 3)               |  /* irq=23, process: irq/23-3f00b880  */
      22 3)               |  /* [+] in_interrupt: 0x00000000,preempt_count = 0x00000000, stack=0xb9714000  */
      23 3) ! 153.437 us  |  }

      무려 153.437 us나 걸렸습니다. bcm2835_mbox_threaded_irq() 함수에서 호출하는 dump_stack() 함수는 다른 인터럽트 핸들러 함수에 비해 실행 속도가 오래 걸립니다. 그래서 인터럽트 핸들러에서 쓰면 안된다고 언급한 것입니다. 

      여러분의 동료가 혹시 인터럽트 핸들러나 IRQ 스레드 핸들러를 작성했으면 조용히 이 방법으로 실행 시간을 측정한 다음에 인터럽트 핸들러 함수 실행 시간이 짧으면 칭찬해주고 오래 걸리면 조용히 알려줍니다. 

      만약 다른 팀 개발자가 여러분이 작성한 인터럽트 핸들러나 IRQ 스레드 핸들러 실행이 오래 걸린다고 공격하면 이 방법으로 방어합시다.

      # Reference 인터럽트 후반부 처리








      6.9 Soft IRQ 서비스는 누가 언제 처리하나?




      6.13 Soft IRQ 디버깅
      6.13.1 ftrace Soft IRQ 이벤트 분석 방법
      6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인 



      [리눅스커널][인터럽트후반부] IRQ 스레드를 만든 후 ftrace로 분석하기 6장. 인터럽트 후반부 처리

      6.5.2 IRQ 스레드 생성 실습하기

      이번에는 IRQ 스레드를 직접 생성하는 실습을 하겠습니다. 직접 손으로 코드를 작성하고 로그로 동작을 확인하면 배운 내용을 더 오랫동안 기억할 수 있습니다.

      IRQ 스레드를 생성할 인터럽트 선택하기

      다른 리눅스 시스템보다 라즈베리파이는 IRQ 스레드 개수가 많지 않습니다. 아래와 같이 “ps –ely” 을 입력하면 92번 mmc1 인터럽트를 처리하는 IRQ 스레드가 1개만 보입니다.
      root@raspberrypi:/home/pi/dev_raspberrian# ps -ely
      S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
      S     0     1     0  0  80   0  6012  6750 SyS_ep ?        00:00:02 systemd
      S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
      ...
      S     0    64     2  0  70 -10     0     0 down_i ?        00:00:00 SMIO
      S     0    65     2  0   9   -     0     0 irq_th ?        00:00:00 irq/92-mmc1
      I     0    66     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:3

      라즈비안에서 어떤 인터럽트를 IRQ 스레드로 생성해 볼까요? 인터럽트 상태를 점검하기 위해 다음 “cat /proc/interrupts” 명령어로 인터럽트의 개수와 상태를 확인합시다. 
      root@raspberrypi:/home/pi/dev_raspberrian# cat /proc/interrupts
                 CPU0       CPU1       CPU2       CPU3        
      16:          0          0          0          0  bcm2836-timer   0 Edge      arch_timer 
      17:     129951     132772     128401     128648  bcm2836-timer   1 Edge      arch_timer 
      21:          0          0          0          0  bcm2836-pmu     9 Edge      arm-pmu 
      23:       1264          0          0          0  ARMCTRL-level   1 Edge      3f00b880.mailbox 
      24:       1710          0          0          0  ARMCTRL-level   2 Edge      VCHIQ doorbell 
      46:        137          0          0          0  ARMCTRL-level  48 Edge      bcm2708_fb dma 
      48:          0          0          0          0  ARMCTRL-level  50 Edge      DMA IRQ 
      50:          0          0          0          0  ARMCTRL-level  52 Edge      DMA IRQ 
      51:       2375          0          0          0  ARMCTRL-level  53 Edge      DMA IRQ 
      54:      32325          0          0          0  ARMCTRL-level  56 Edge      DMA IRQ 
      59:          0          0          0          0  ARMCTRL-level  61 Edge      bcm2835-auxirq 
      62:    1355025          0          0          0  ARMCTRL-level  64 Edge      dwc_otg, dwc_otg_pcd, dwc_otg_hcd:usb1 
      86:       8025          0          0          0  ARMCTRL-level  88 Edge      mmc0 
      87:       5121          0          0          0  ARMCTRL-level  89 Edge      uart-pl011 
      92:      52579          0          0          0  ARMCTRL-level  94 Edge      mmc1


      62번 인터럽트는 너무 자주 발생하고 51번 인터럽트는 발생 빈도가 너무 낮으니 23번 “3f00b880.mailbox” 인터럽트가 적당합니다.

      이제 23번 인터럽트를 처리하는 IRQ 스레드를 생성하는 코드를 작성을 시작합니다. 이후 인터럽트 핸들러가 수행한 후 IRQ 스레드가 어떤 흐름으로 호출되는지도 점검하겠습니다.

      23번 인터럽트 IRQ 스레드 생성 실습 패치 코드 입력해보기 

      우선 전체 패치 코드를 소개합니다.
      diff --git a/drivers/mailbox/bcm2835-mailbox.c b/drivers/mailbox/bcm2835-mailbox.c
      index d9c6c217c..fe6e52996 100644
      --- a/drivers/mailbox/bcm2835-mailbox.c
      +++ b/drivers/mailbox/bcm2835-mailbox.c
      @@ -72,6 +72,23 @@ static struct bcm2835_mbox *bcm2835_link_mbox(struct mbox_chan *link)
        return container_of(link->mbox, struct bcm2835_mbox, controller);
       }
       
      +static irqreturn_t bcm2835_mbox_threaded_irq(int irq, void *dev_id)
      +{
      + void *stack;
      + struct thread_info *current_thread;
      +
      + stack = current->stack;
      + current_thread = (struct thread_info*)stack;
      +
      + trace_printk("irq=%d, process: %s \n", irq, current->comm);
      + trace_printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n",
      + (unsigned int)in_interrupt(), (unsigned int)current_thread->preempt_count, (long unsigned int)stack);
      +
      +    dump_stack();
      +
      + return IRQ_HANDLED;
      +}
      +
       static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)
       {
        struct bcm2835_mbox *mbox = dev_id;
      @@ -83,13 +100,13 @@ static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)
        dev_dbg(dev, "Reply 0x%08X\n", msg);
        mbox_chan_received_data(link, &msg);
        }
      - return IRQ_HANDLED;
      + return IRQ_WAKE_THREAD;
       }
       
       static int bcm2835_send_data(struct mbox_chan *link, void *data)
       {
        struct bcm2835_mbox *mbox = bcm2835_link_mbox(link);
      - u32 msg = *(u32 *)data;
      + u32 msg = *(u32 *)data;
       
        spin_lock(&mbox->lock);
        writel(msg, mbox->regs + MAIL1_WRT);
      @@ -154,8 +171,12 @@ static int bcm2835_mbox_probe(struct platform_device *pdev)
        return -ENOMEM;
        spin_lock_init(&mbox->lock);
       
      - ret = devm_request_irq(dev, platform_get_irq(pdev, 0),
      -        bcm2835_mbox_irq, 0, dev_name(dev), mbox);
      +        
      + ret = devm_request_threaded_irq(dev, platform_get_irq(pdev, 0),
      +        bcm2835_mbox_irq, bcm2835_mbox_threaded_irq, 0, dev_name(dev), mbox);
      +        
        if (ret) {
        dev_err(dev, "Failed to register a mailbox IRQ handler: %d\n",
        ret);

      이제 패치 코드를 입력하는 방법을 설명해 드리겠습니다.

      우선 bcm2835_send_data() 함수에 있는 devm_request_irq() 함수 코드 대신 devm_request_threaded_irq() 함수로 바꿉니다. 아래 – 표시가 된 코드가 원래 코드이고 +로 표시된 코드가 새롭게 입력해야 할 코드입니다.
      - ret = devm_request_irq(dev, platform_get_irq(pdev, 0),
      -        bcm2835_mbox_irq, 0, dev_name(dev), mbox);
      +        
      + ret = devm_request_threaded_irq(dev, platform_get_irq(pdev, 0),
      +        bcm2835_mbox_irq, bcm2835_mbox_threaded_irq, 0, dev_name(dev),

      devm_request_irq() 함수는 인터럽트 핸들러만 등록하는 함수인데 devm_request_threaded_irq() 함수로 인터럽트 핸들러와 IRQ 스레드 핸들러를 등록할 수 있습니다. 여기서 IRQ 스레드 핸들러 함수는 bcm2835_mbox_threaded_irq()로 지정합니다. 

      인터럽트 컨택스트에서 빨리해야 할 일은 bcm2835_mbox_irq() 함수, 조금 있다가 프로세스 레벨에서 처리할 일은 bcm2835_mbox_threaded_irq() 함수에서 처리하는 것입니다. 

      다음엔 bcm2835_mbox_irq() 함수를 어떻게 수정했는지 알아볼 차례입니다. 
      01 static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)
      02 {
      03 struct bcm2835_mbox *mbox = dev_id;
      @@ -83,13 +100,13 @@ static irqreturn_t bcm2835_mbox_irq(int irq, void *dev_id)
      04 dev_dbg(dev, "Reply 0x%08X\n", msg);
      05 mbox_chan_received_data(link, &msg);
      06 }
      07 - return IRQ_HANDLED;
      08 + return IRQ_WAKE_THREAD;
      09 }

      원래 이 함수는 07 번째 줄과 같이 IRQ_HANDLED을 반환했습니다. 그런데 IRQ 스레드를 실행시키려면 위 코드와 같이 IRQ_HANDLED 대신 IRQ_WAKE_THREAD를 입력해야 합니다. 

      이렇게 코드를 수정하는 이유는 뭘까요? 이전 절에 배운 바와 같이 대부분 인터럽트 핸들러는 __handle_irq_event_percpu() 함수에서 호출합니다. 그런데 __handle_irq_event_percpu() 함수가 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환해야 __irq_wake_thread() 함수를 호출해서 IRQ 스레드를 깨우기 때문입니다.

      이번에는 bcm2835_mbox_threaded_irq() 구현부 코드를 보겠습니다.
      이 코드는 bcm2835_mbox_irq() 함수 윗부분에 입력하시면 됩니다. 혹시 숫자와 + 기호까지 코드로 입력하면 안됩니다. + 기호는 리눅스 패치 코드에서 새롭게 추가되는 코드를 표현하는 것일 뿐입니다.
      1 +static irqreturn_t bcm2835_mbox_threaded_irq(int irq, void *dev_id)
      2 +{
      3 + void *stack;
      4 + struct thread_info *current_thread;
      5 +
      6 + stack = current->stack;
      7 + current_thread = (struct thread_info*)stack;
      8 +
      9 + trace_printk("irq=%d, process: %s \n", irq, current->comm);
      10+ trace_printk("[+] in_interrupt: 0x%08x,preempt_count = 0x%08x, stack=0x%08lx \n",
      11+ (unsigned int)in_interrupt(), (unsigned int)current_thread->preempt_count, (long unsigned int)stack);
      12+
      13+    dump_stack();
      14+
      15+ return IRQ_HANDLED;
      16+}
      +

      이번에 bcm2835_mbox_threaded_irq() 함수 구현부 코드를 소개합니다.

      9번째부터 11번째 줄 코드는 프로세스 이름과 인터럽트 번호 그리고 in_interrupt() 함수 반환값을 출력합니다. 이후 13번째 줄 코드에서 dump_stack() 함수를 호출해서 콜스택을 출력합니다.

      dump_stack() 함수는 생각보다 많은 일을 합니다. 현재 구동 중인 프로세스의 스택 주소를 얻어서 스택에 푸시된 R14와 Frame Pointer(이전 스택 주소) 레지스터를 읽어 콜스택을 커널 로그로 출력하기 때문입니다. 그래서 인터럽트 핸들러나 인터럽트 컨택스트에서 이 함수를 호출하면 시스템이 느려지거나 오동작을 하니 주의해야 합니다.

      이렇게 코드를 입력한 후 컴파일 후 라즈비안에 설치를 합니다. 라즈베리파이를 재부팅시키고  6.1.2 절에 소개한 irq_thread_trace.sh 셸 스크립트에서 아래부분만 수정한 다음에 스크립트를 실행해서 ftrace 로그를 설정합니다.
      echo bcm2835_mbox_threaded_irq bcm2835_mbox_irq   > /sys/kernel/debug/tracing/set_ftrace_filter
      sleep 1
      echo "set_ftrace_filter enabled"

      bcm2835_mbox_threaded_irq() 함수와 bcm2835_mbox_irq() 함수를 set_ftrace_filter에 지정하는 코드입니다.

      이 셸 스크립트를 실행한 후 20초 후에 ftrace 로그를 받으면 됩니다. 그 사이 23번 인터럽트가 여러 번 발생할 것입니다.

      실습으로 만든 IRQ 스레드를 ftrace로 분석해보기

      우리가 같이 분석할 로그는 다음과 같습니다.
      1 kworker/0:1-31    [000] d.h.   592.790968: irq_handler_entry: irq=23 name=3f00b880.mailbox
      2 kworker/0:1-31    [000] d.h.   592.790970: bcm2835_mbox_irq <-__handle_irq_event_percpu
      3 kworker/0:1-31    [000] d.h.   592.791014: <stack trace>
      4   => handle_irq_event
      5   => handle_level_irq
      6   => generic_handle_irq
      7   => bcm2836_chained_handle_irq
      8   => generic_handle_irq
      9   => __handle_domain_irq
      10 => bcm2836_arm_irqchip_handle_irq
      11 => __irq_svc
      12 => schedule_timeout
      13 => schedule_timeout
      14 => wait_for_completion_timeout
      15 => mbox_send_message
      16 => rpi_firmware_transaction
      17 => rpi_firmware_property_list
      18 => rpi_firmware_property
      19 => rpi_firmware_get_throttled
      20 => get_throttled_poll
      21 => process_one_work
      22 => worker_thread
      23 => kthread
      24 => ret_from_fork
      25 kworker/0:1-31    [000] d.h.   592.791016: irq_handler_exit: irq=23 ret=handled 
      26 <idle>-0 [003] dnh.   592.791033: sched_wakeup: comm=irq/23-3f00b880 pid=33 prio=49 target_cpu=003
      27 <idle>-0   [003] d...   592.791048: sched_switch: prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=irq/23-3f00b880 next_pid=33 next_prio=49
      28 irq/23-3f00b880-33    [003] ....   592.791052: bcm2835_mbox_threaded_irq <-irq_thread_fn
      29 irq/23-3f00b880-33    [003] ....   592.791064: <stack trace>
      30 => kthread
      31 => ret_from_fork
      32     kworker/0:1-31    [000] ....   592.791065: workqueue_execute_end: work struct b97a0d44
      33 irq/23-3f00b880-33    [003] ....   592.791073: bcm2835_mbox_threaded_irq: irq=23, process: irq/23-3f00b880 
      34 irq/23-3f00b880-33    [003] ....   592.791076: bcm2835_mbox_threaded_irq: [+] in_interrupt: 0x00000000,preempt_count = 0x00000000, stack=0xb9714000

      우선 첫 번째 줄 로그는 3f00b880.mailbox 인터럽트가 발생했다고 말해줍니다. 
      1 kworker/0:1-31    [000] d.h.   592.790968: irq_handler_entry: irq=23 name=3f00b880.mailbox

      그 다음 두 번째 줄부터 25번 줄 로그까지 여러 함수들이 늘어서 있습니다.
      2 kworker/0:1-31    [000] d.h.   592.790970: bcm2835_mbox_irq <-__handle_irq_event_percpu
      3 kworker/0:1-31    [000] d.h.   592.791014: <stack trace>
      4  => handle_irq_event
      5  => handle_level_irq
      6  => generic_handle_irq
      7  => bcm2836_chained_handle_irq
      8  => generic_handle_irq
      9  => __handle_domain_irq
      10 => bcm2836_arm_irqchip_handle_irq
      11 => __irq_svc
      12 => schedule_timeout
      13 => schedule_timeout
      14 => wait_for_completion_timeout
      15 => mbox_send_message
      16 => rpi_firmware_transaction
      17 => rpi_firmware_property_list
      18 => rpi_firmware_property
      19 => rpi_firmware_get_throttled
      20 => get_throttled_poll
      21 => process_one_work
      22 => worker_thread
      23 => kthread
      24 => ret_from_fork
      25 kworker/0:1-31    [000] d.h.   592.791016: irq_handler_exit: irq=23 ret=handled 

      함수 호출 흐름을 살펴보면 pid가 31인 kworker/0:1워커 스레드가 schedule_timeout() 함수에서 실행 중이었습니다. 이때 인터럽트가 발생해서 인터럽트 벡터인 __irq_svc 레이블이 실행 한 후 인터럽트 핸들러인 bcm2835_mbox_irq() 함수가 호출됐습니다.

      다음 26 번째 줄 로그입니다. 
      26 <idle>-0     [003] dnh.   592.791033: sched_wakeup: comm=irq/23-3f00b880 pid=33 prio=49 target_cpu=003

      “irq/23-3f00b880” 프로세스를 깨우는 동작입니다. “irq/23-3f00b880”는 3f00b880.mailbox 인터럽트의 IRQ 스레드 이름입니다. 이번에 작성한 패치로 실제 “irq/23-3f00b880” IRQ 스레드가 생성되서 실행하고 있습니다.

      이어서 27 번째 줄을 보겠습니다.
      27 <idle>-0     [003] d...   592.791048: sched_switch: prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=irq/23-3f00b880  next_pid=33 next_prio=49
      28 irq/23-3f00b880-33    [003] ....   592.791052: bcm2835_mbox_threaded_irq <-irq_thread_fn
      29 irq/23-3f00b880-33    [003] ....   592.791064: <stack trace>
      30 => kthread
      31 => ret_from_fork

      “irq/23-3f00b880”이란 IRQ 스레드로 스케줄링 된 후 “irq/23-3f00b880” IRQ 스레드 핸들러인 bcm2835_mbox_threaded_irq() 함수가 호출됩니다.

      마지막 33~34 번째 줄입니다.
      33 irq/23-3f00b880-33    [003] ....   592.791073: bcm2835_mbox_threaded_irq: irq=23, process: irq/23-3f00b880 
      34 irq/23-3f00b880-33    [003] ....   592.791076: bcm2835_mbox_threaded_irq: [+] in_interrupt: 0x00000000,preempt_count = 0x00000000, stack=0xb9714000

       bcm2835_mbox_threaded_irq() 함수에 추가한 디버깅 로그 정보입니다.

      in_interrupt() 함수와 프로세스 struct thread_info 필드 preempt_count가 0x0입니다. 이 값이 0x10000(HARDIRQ_OFFSET)이면 인터럽트 컨택스트인데 0이니 인터럽트 컨택스트가 아닙니다.

      패치 코드를 떠 올리면 bcm2835_mbox_threaded_irq() 함수 13번째 줄 코드에 dump_stack 함수를 입력했습니다. 커널 로그로 콜스택을 출력하는 디버깅 코드입니다. 커널 로그를 열어보면 같은 디버깅 정보를 확인할 수 있습니다.
      [4.151743] CPU: 3 PID: 33 Comm: irq/23-3f00b880 Tainted: G         C      4.19.30-v7+ #15
      [4.151756] Hardware name: BCM2835
      [4.151796] [<8010ffe0>] (unwind_backtrace) from [<8010c21c>] (show_stack+0x20/0x24)
      [4.151815] [<8010c21c>] (show_stack) from [<8078703c>] (dump_stack+0xc8/0x10c)
      [4.151841] [<8078703c>] (dump_stack) from [<8066ab14>] (bcm2835_mbox_threaded_irq+0x80/0x94)
      [4.151862] [<8066ab14>] (bcm2835_mbox_threaded_irq) from [<80176c50>] (irq_thread_fn+0x2c/0x64)
      [4.151880] [<80176c50>] (irq_thread_fn) from [<80176f78>] (irq_thread+0x148/0x20c)
      [4.151895] [<80176f78>] (irq_thread) from [<8013daac>] (kthread+0x144/0x174)
      [4.151915] [<8013daac>] (kthread) from [<801080cc>] (ret_from_fork+0x14/0x28)

      커널 로그로 bcm2835_mbox_threaded_irq() 함수는 irq/23-3f00b880 IRQ 스레드에서 실행됐다고 알 수 있습니다. ftrace 로그에서 봤던 정보와 같습니다.

      생성한 IRQ 스레드 실행 전체 흐름도 파악해보기

      우리는 23번 인터럽트 후반부를 처리하는 "irq/23-3f00b880" IRQ 스레드를 만들어 ftrace로 실행 과정을 확인했습니다. 이번 소절에 실습한 "irq/23-3f00b880" IRQ 스레드 실행 전체 흐름도는 다음 그림과 같습니다.
       
      [그림 6.8] "irq/23-3f00b880" IRQ 스레드 전체 실행 흐름도

      각 단계별 동작을 리뷰하면서 이번 실습 과정을 마무리합시다.

      [1] 단계: 23 번 인터럽트가 발생해 인터럽트 벡터인 __irq_svc 레이블이 실행합니다. 커널 인터럽트 공통 함수가 호출돼 인터럽트 핸들러를 실행합니다.

      [2] 단계: 23 번 인터럽트 핸들러인 bcm2835_mbox_irq() 함수가 호출됩니다. 이 함수에 우리가 실습 코드를 입력했습니다. IRQ_WAKE_THREAD 플래그를 반환해 "irq/23-3f00b880" IRQ 스레드를 깨웁니다.

      [3] 단계: "irq/23-3f00b880" IRQ 스레드 핸들러인 bcm2835_mbox_threaded_irq() 함수가 호출됩니다. bcm2835_mbox_threaded_irq() 는 이번 실습 과정에서 입력한 함수입니다. 

      이번 소절에 소개된 패치 원리를 이해하면 여러분이 갖고 있는 다른 리눅스 보드에도 IRQ 스레드를 만들 수 있습니다. 이번에 소개한 패치 코드를 이해하고 실습한 다음 다른 코드에도 적용해보면 IRQ 스레드가 더 오랫동안 머리 속에 남을 것입니다.

      그리고 이번 시간에는 ftrace를 활용해 함수 콜스택을 분석했습니다. 분석하고 싶은 함수에 필터를 걸고 콜스택을 확인한 것입니다. 그런데 ftrace 에서는 함수 실행 시간을 알려주는 더 정교한 기능을 제공합니다. 이어서 함수 실행 시간을 측정할 수 있는 IRQ 스레드 디버깅 방법을 소개합니다. 

      # Reference 인터럽트 후반부 처리








      6.9 Soft IRQ 서비스는 누가 언제 처리하나?




      6.13 Soft IRQ 디버깅
      6.13.1 ftrace Soft IRQ 이벤트 분석 방법
      6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인



      [리눅스커널][인터럽트후반부] ftrace로 IRQ 스레드 동작 확인하기 6장. 인터럽트 후반부 처리

      6.5 IRQ 스레드 디버깅
      이번 절에서는 IRQ 스레드 디버깅 방법을 소개합니다.
       - ftrace로 IRQ Thread 동작 확인하기
       - IRQ 스레드를 생성하는 실습 
       - ftrace function_gragh 트레이서로 IRQ 스레드 핸들러 실행 시각 측정하기

      언제나 실습이 중요한 것 같습니다. 코드를 분석한 시간을 다지기 위해 꼭 필요한 과정입니다.

      6.5.1 ftrace로 IRQ 스레드 동작 확인하기

      이제 실제 라즈베리안에서 IRQ 스레드가 어떻게 수행되는지 ftrace 로그로 분석할 시간입니다.

      IRQ 스레드를 확인도록 ftrace 설정하기

      IRQ 스레드를 확인할 수 있는 ftrace 설정 명령어를 소개합니다.  
      #!/bin/sh

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

      echo 0 > /sys/kernel/debug/tracing/events/enable
      sleep 1
      echo "events disabled"

      echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
      sleep 1
      echo "set_ftrace_filter init"

      echo function > /sys/kernel/debug/tracing/current_tracer
      sleep 1
      echo "function tracer enabled"

      echo bcm2835_mbox_threaded_irq bcm2835_mbox_irq   > /sys/kernel/debug/tracing/set_ftrace_filter
      sleep 1
      echo "set_ftrace_filter enabled"

      echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
      echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable

      echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
      echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

      sleep 1
      echo "event enabled"

      echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
      echo "function stack trace enabled"

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

      이전 5장 5.2.2 절에서 설정한 ftrace log 대비 달라진 점은 두 가지입니다. 
      우선bcm2835_mbox_threaded_irq()와 bcm2835_mbox_irq() 함수를 다음과 같이 set_ftrace_filter에 저장합니다. 이 함수 콜스택을 보기 위해서입니다. 
      echo bcm2835_mbox_threaded_irq bcm2835_mbox_irq   > /sys/kernel/debug/tracing/set_ftrace_filter

      그다음으로 sched_switch, sched_wakeup event를 킵니다. IRQ 스레드를 누가 깨우고 IRQ 스레드가 언제 스케쥴링 되는지 점검하기 위해서입니다
      echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
      echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

      위 셸 스크립트를 irq_thread_trace.sh 파일 이름으로 저장하고 “source irq_thread_trace.sh” 명령어로 실행하면 ftrace 를 빨리 설정할 수 있습니다.

      이렇게 ftrace를 설정 후 10초 후에 ftrace 로그(/sys/kernel/debug/tracing/trace)을 열어봅니다. 그러면 10초 동안 커널이 동작한 ftrace 로그를 볼 수 있습니다. 이 로그에서 92번 인터럽트가 발생하고 나서 “irq/92-mmc1” IRQ 스레드가 실행하는 흐름까지 살펴보겠습니다.

      ftrace 메시지 분석으로 IRQ 스레드 동작 확인하기

      분석하려는 전체 ftrace 로그는 다음과 같습니다.
      1 <idle>-0     [000] d.h.  2207.201353: irq_handler_entry: irq=92 name=mmc1
      2 <idle>-0     [000] d.h.  2207.201359: bcm2835_mmc_irq <-__handle_irq_event_percpu
      3 <idle>-0     [000] d.h.  2207.201404: <stack trace>
      4 => handle_irq_event
      5 => handle_level_irq
      6 => generic_handle_irq
      7 => bcm2836_chained_handle_irq
      8 => generic_handle_irq
      9 => __handle_domain_irq
      10 => bcm2836_arm_irqchip_handle_irq
      11 => __irq_svc
      12 => arch_cpu_idle
      13 => arch_cpu_idle
      14 => default_idle_call
      15 => do_idle
      16 => cpu_startup_entry
      17 => rest_init
      18 => start_kernel
      19 <idle>-0     [000] d.h.  2207.201414: irq_handler_exit: irq=92 ret=handled
      20 <idle>-0     [000] dnh.  2207.201425: sched_wakeup: comm=irq/92-mmc1 pid=65 prio=49 target_cpu=000
      21 <idle>-0     [000] d...  2207.201436: sched_switch: prev_comm=swapper/0 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=irq/92-mmc1 next_pid=65 22 next_prio=49
      23 irq/92-mmc1-65    [000] ....  2207.201441: bcm2835_mmc_thread_irq <-irq_thread_fn
      24 irq/92-mmc1-65    [000] ....  2207.201459: <stack trace>
      25 => kthread
      26 => ret_from_fork

      이제부터 위 ftrace 로그를 실행 순서별로 차근차근 분석해 보겠습니다.

      우선, 아래 로그는 mmc1 인터럽트가 발생했다고 말해줍니다. pid가 0인 idle 프로세스의 동작을 멈추고 인터럽트가 발생했습니다. 이제 인터럽트 핸들러가 호출될 것입니다.
      1 <idle>-0     [000] d.h.  2207.201353: irq_handler_entry: irq=92 name=mmc1

      다음 로그로 인터럽트 핸들러인 bcm2835_mmc_irq() 함수가 콜스택을 알 수 있습니다.
      2 <idle>-0     [000] d.h.  2207.201359: bcm2835_mmc_irq <-__handle_irq_event_percpu
      3 <idle>-0     [000] d.h.  2207.201404: <stack trace>
      4 => handle_irq_event
      5 => handle_level_irq
      6 => generic_handle_irq
      7 => bcm2836_chained_handle_irq
      8 => generic_handle_irq
      9 => __handle_domain_irq
      10 => bcm2836_arm_irqchip_handle_irq
      11 => __irq_svc
      12 => arch_cpu_idle
      13 => arch_cpu_idle
      14 => default_idle_call
      15 => do_idle
      16 => cpu_startup_entry
      17 => rest_init
      18 => start_kernel

      idle 프로세스가 arch_cpu_idle() 함수에서 일하고 있는 도중에 92번 “mmc1” 인터럽트가 발생한 것입니다. 11번째 줄 로그에서  __irq_svc가 보입니다. 인터럽트 벡터 함수입니다.

      다음은 소절에서 장에서 가장 중요한 로그이니 조금 집중해서 볼 필요가 있습니다. 
      20 <idle>-0     [000] dnh.  2207.201425: sched_wakeup: comm=irq/92-mmc1 pid=65 prio=49 target_cpu=000
      21 <idle>-0     [000] d...  2207.201436: sched_switch: prev_comm=swapper/0 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=irq/92-mmc1 next_pid=65 22 next_prio=49

      20번째 줄 로그에서 sched_wakeup이란 메시지가 보입니다. 이는 프로세스를 깨우는 동작을 표현하는 ftrace 이벤트 로그입니다. 오른쪽 메시지인 “comm=irq/92-mmc1” 을 보면 깨우려는 프로세스 정보를 알 수 있습니다. 즉, pid가 65이고 프로세스 이름이 irq/92-mmc1인 IRQ 스레드를 깨우는 동작입니다. 

      인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하니 __irq_wake_thread() 함수가 호출되어 wake_up_process() 함수를 호출한 것입니다.

      21번 줄 로그를 보면 swapper/0 프로세스에서 irq/92-mmc1 프로세스로 스케줄링합니다. 이제 irq/92-mmc1 프로세스를 실행할 것입니다.

      다음은 IRQ 스레드인 irq/92-mmc1 프로세스가 실행을 시작합니다.
      23 irq/92-mmc1-65    [000] ....  2207.201441: bcm2835_mmc_thread_irq <-irq_thread_fn
      24 irq/92-mmc1-65    [000] ....  2207.201459: <stack trace>
      25 => kthread
      26 => ret_from_fork

      위 콜스택을 조금 더 쉽게 보이게 다시 정렬해보면 다음 번호 순서 흐름으로 irq/92-mmc1 IRQ 스레드 핸들인 bcm2835_mmc_thread_irq() 함수를 호출합니다. 
      4 => bcm2835_mmc_thread_irq
      3 => irq_thread_fn
      2 => kthread
      1 => ret_from_fork

      여러분이 갖고 있는 리눅스 보드가 라즈베리파이가 아니더라도 이 방법으로 IRQ 스레드가 어떤 흐름으로 실행하는지 점검하길 바랍니다. 리눅스 커널 코드를 분석할 때 보다 훨씬 더 많은 정보를 얻을 수 있습니다.


      # Reference 인터럽트 후반부 처리








      6.9 Soft IRQ 서비스는 누가 언제 처리하나?




      6.13 Soft IRQ 디버깅
      6.13.1 ftrace Soft IRQ 이벤트 분석 방법
      6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인




      [리눅스커널] 인터럽트 후반부 처리: IRQ 스레드를 깨우는 코드 분석 6장. 인터럽트 후반부 처리

      6.4 IRQ 스레드는 누가 언제 실행할까?

      이번 절에서는 IRQ 스레드가 어떤 과정으로 실행하는지 살펴보겠습니다. 
      IRQ 스레드는 크게 다음 단계로 실행합니다.
       1. 인터럽트 핸들러에서 IRQ_WAKE_THREAD 반환
       2. IRQ 스레드 깨움
       3. IRQ 스레드 핸들러 실행

      IRQ 스레드를 실행하는 출발점은 인터럽트 핸들러가 IRQ_WAKE_THREAD 를 반환하는 시점입니다. 이 부분부터 IRQ 스레드를 어떤 과정으로 깨우는지 세부 동작을 점검하겠습니다.

      6.4.1 IRQ 스레드를 깨우는 코드 분석

      인터럽트가 발생하면 인터럽트 핸들러가 실행됩니다. 이 인터럽트 핸들러 실행이 IRQ 스레드 시작의 출발점입니다. 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환하면 해당 IRQ 스레드를 깨웁니다.

      예를 들어 92번 “mmc1” 인터럽트 핸들러인 bcm2835_mmc_irq() 함수에서 IRQ_WAKE_THREAD를 반환하면 irq/92-mmc1이란 IRQ 스레드를 깨웁니다. IRQ 스레드를 깨우면 스케줄러는 우선 순위를 고려한 후 IRQ 스레드를 실행합니다. 프로세스 컨택스트에서 IRQ 스레드가 실행하는 것입니다. 

      IRQ 스레드 전체 흐름도 파악해보기

      IRQ 스레드 전체 흐름도는 다음과 같습니다.
       
      [그림 6.5] IRQ 스레드 전체 실행 흐름도

      이 흐름을 더 구체적으로 알기 위해 라즈비안 인터럽트 핸들러 코드와 함께 커널 코드를 분석하겠습니다. 

      이전에 irq/92-mmc1 IRQ 스레드가 생성되는 과정을 살펴봤으니 이번에는 라즈베이파이에서 92번 인터럽트가 발생하고 나서 irq/92-mmc1 IRQ 스레드가 실행하는 과정까지 살펴보겠습니다.

      IRQ 스레드 실행의 출발점인 __handle_irq_event_percpu() 함수 분석하기

      먼저 인터럽트 핸들러를 호출하는 코드를 열어 봅시다.  
      [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c]
      1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
      2 {
      3 irqreturn_t retval = IRQ_NONE;
      4 unsigned int irq = desc->irq_data.irq;
      5 struct irqaction *action;
      6
      7 for_each_action_of_desc(desc, action) {
      8 irqreturn_t res;
      9
      10 trace_irq_handler_entry(irq, action);
      11 res = action->handler(irq, action->dev_id);   
      12 trace_irq_handler_exit(irq, action, res);
      13
      14 if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
      15       irq, action->handler))
      16 local_irq_disable();
      17
      18 switch (res) {
      19 case IRQ_WAKE_THREAD:
      20
      21 if (unlikely(!action->thread_fn)) {
      22 warn_no_thread(irq, action);
      23 break;
      24 }
      25
      26 __irq_wake_thread(desc, action);   

      우리는 __handle_irq_event_percpu() 함수의 11번째 줄 코드에서 인터럽트 핸들러를 호출한다고 배웠습니다.  __handle_irq_event_percpu() 함수 첫 번째 인자는 struct irq_desc 구조체인 desc입니다. struct irq_desc는 구조체는 인터럽트 디스크립터입니다.

      7번째 코드를 보면 인터럽트 디스크립터 필드 중 struct irqaction 구조체인 action이란 필드가 저장한 주소를 *action이란 지역변수에 전달합니다.
      1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
      2 {
      3 irqreturn_t retval = IRQ_NONE;
      4 unsigned int irq = desc->irq_data.irq;
      5 struct irqaction *action;
      6
      7 for_each_action_of_desc(desc, action) {
      8 irqreturn_t res;


      for_each_action_of_desc 매크로 코드를 소개합니다.
      1 #define for_each_action_of_desc(desc, act) \
      2 for (act = desc->action; act; act = act->next)

      2 번째 줄 코드를 보면 desc->action 멤버를 act에 저장합니다.

      7번째 줄 코드를 전처리 코드에서 보면 다음과 같습니다. 전처리 코드를 보면 매크로를 풀어 실제 어떤 코드를 실행하는지 알려주니 빨리 코드를 읽을 수 있어 좋습니다.
      [/kernel/irq/.tmp_handle.i]
       for (action = desc->action; action; action = action->next) {
      irqreturn_t res;

      다음 11~26번째 줄 코드를 보겠습니다.
      11 res = action->handler(irq, action->dev_id);
      ...
      18 switch (res) {
      19 case IRQ_WAKE_THREAD:
      20
      21 if (unlikely(!action->thread_fn)) {
      22 warn_no_thread(irq, action);
      23 break;
      24 }
      25
      26 __irq_wake_thread(desc, action);

      11번째 줄 코드를 보면 action->handler 함수 포인터로 인터럽트 핸들러를 호출합니다. 그리고 res 지역 변수로 인터럽트 핸들러 반환값을 저장합니다. 18번째 줄 코드에서 res 값을 기준으로 switch~case 문을 처리합니다.

      res가IRQ_WAKE_THREAD인 경우 __irq_wake_thread() 함수를 호출해 IRQ 스레드를 깨웁니다. 

      21번째 줄은 인터럽트 디스크립터로 action->thread_fn 함수 포인터가 NULL인지를 체크하는 조건문입니다. IRQ 스레드를 등록할 때 IRQ 스레드 핸들 함수를 지정했으면 당연히 action->thread_fn 필드는 NULL이 아닐 것입니다. 대신 IRQ 스레드 핸들 함수 주소를 저장할 것입니다. 

      그런데 왜 이런 조건문을 추가했을까요? 이는 IRQ 스레드를 등록하지 않았는데 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 반환할 경우를 위한 예외 처리 코드입니다. 또는 IRQ 스레드를 생성할 때 IRQ 스레드 핸들러 함수를 제대로 등록했는지 확인하는 것입니다. 만약 IRQ 스레드 핸들 함수가 NULL이면 다음과 같이 warn_no_thread() 함수를 호출해서 친절하게 커널 로그로 경고 메시지를 출력합니다. 이 후 break 문을 실행해서 switch~case 문을 종료합니다.

      warn_no_thread() 함수 코드를 보면 다음 6~7 번째 줄 메시지와 같이 인터럽트 이름과 번호를 커널 로그로 출력합니다. 
      [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c]
      1 static void warn_no_thread(unsigned int irq, struct irqaction *action)
      2 {
      3 if (test_and_set_bit(IRQTF_WARNED, &action->thread_flags))
      4 return;
      5
      6 printk(KERN_WARNING "IRQ %d device %s returned IRQ_WAKE_THREAD "
      7        "but no thread function available.", irq, action->name);
      8 }

      7 번째 줄 "but no thread function available." 메시지는 IRQ 스레드 핸들 함수를 지정하지 않았다는 의미입니다.

      디바이스 드라이버 개발자가 실수로 request_threaded_irq() 함수를 호출할 때 IRQ 스레드 핸들 함수를 지정하지 않을 수 있습니다. 그러면 인터럽트 핸들러에서 IRQ_WAKE_THREAD를 하면 생성하지도 않은 IRQ 스레드를 깨우려고 시도할 것입니다. 이전 소절에서 배웠다시피 IRQ 스레드 핸들러 함수를 등록해야 IRQ Thread를 생성하기 때문입니다. 이런 실수를 막기 위한 예외 처리 코드입니다.

      이렇게 리눅스 커널 코드에서 예외 처리 루틴을 보면 지나치치 말고 주의 깊게 읽을 필요가 있습니다. 이렇게 유익한 정보를 얻을 수 있습니다.

      IRQ_WAKE_THREAD 를 반환하는 인터럽트 핸들러 분석하기

      이번엔 92번 인터럽트 핸들러인 bcm2835_mmc_irq() 함수 어디서 IRQ_WAKE_THREAD를 반환하는지 살펴보겠습니다.
      [https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/mmc/host/bcm2835-mmc.c]
      1 static irqreturn_t bcm2835_mmc_irq(int irq, void *dev_id)
      2 {
      3 irqreturn_t result = IRQ_NONE;
      4 struct bcm2835_host *host = dev_id;
      5 u32 intmask, mask, unexpected = 0;
      6 int max_loops = 16;
      7
      8 spin_lock(&host->lock);
      9
      10 intmask = bcm2835_mmc_readl(host, SDHCI_INT_STATUS);
      11
      ...
      12 do {
      13 /* Clear selected interrupts. */
      14 mask = intmask & (SDHCI_INT_CMD_MASK | SDHCI_INT_DATA_MASK |
      15   SDHCI_INT_BUS_POWER);
      16 bcm2835_mmc_writel(host, mask, SDHCI_INT_STATUS, 8);
      ...
      17 if (intmask & SDHCI_INT_CARD_INT) {
      18 bcm2835_mmc_enable_sdio_irq_nolock(host, false);
      19 host->thread_isr |= SDHCI_INT_CARD_INT;
      20 result = IRQ_WAKE_THREAD;
      21 }
      ...
      22 out:
      ...
      23 return result;
      24 }

      20번째 코드를 보면 result에 IRQ_WAKE_THREAD 을 저장합니다. 이후 함수 마지막 부분에 IRQ_WAKE_THREAD를 반환합니다. 

      bcm2835_mmc_irq() 함수 가장 마지막 줄인 23번째 코드를 보면 이 result 변수를 반환합니다.

      이렇게 인터럽트 핸들러인 bcm2835_mmc_irq() 함수가 IRQ_WAKE_THREAD를 반환하면 __handle_irq_event_percpu() 함수에서 __irq_wake_thread() 함수를 호출합니다. 

      __handle_irq_event_percpu() 함수에서 이 동작을 수행하는 코드만 모아서 보겠습니다.
      [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c]
      1 irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
      ...
      11 res = action->handler(irq, action->dev_id); /* IRQ_WAKE_THREAD 반환 */
      12 trace_irq_handler_exit(irq, action, res);
      13
      ...
      17
      18 switch (res) {
      19 case IRQ_WAKE_THREAD:
      20
      ...
      26 __irq_wake_thread(desc, action);

      11 번 째 줄 코드에서 인터럽트 핸들러가 IRQ_WAKE_THREAD를 반환하면 26번째 줄 코드를 실행합니다.

      보통 인터럽트 핸들러만 수행하면 후속 동작이 없을 것이라 예상하고 코드를 읽습니다. 하지만 인터럽트 핸들러에 IRQ_WAKE_THREAD를 반환하는 코드를 보면 인터럽트는 IRQ 스레드 핸들러를 등록했다는 사실을 떠올립시다. 이렇게 코드를 예측하면서 분석하면 전체 구조를 더 빨리 이해할 수 있습니다.

      여기서 한 가지 생각해 볼 점이 있습니다. 
      인터럽트가 핸들러가 IRQ 스레드를 깨우고 싶지 않을 때도 있습니다. 특정 상황에서 인터럽트 핸들러에서 정해진 일을 마무리한 상황입니다. 이 때 인터럽트 핸들러에서 IRQ_HANDLED을 반환하면 됩니다. IRQ 스레드를 깨우지 않고 인터럽트 핸들링을 마무리합니다.

      이 방식으로 인터럽트를 제어하면 조금 더 유연하게 인터럽트 후반부를 처리하는 드라이버 코드를 작성할 수 있습니다.

      IRQ 스레드를 깨우는 irq_wake_thread() 함수 분석하기 

      이어서 인터럽트 핸들러가 IRQ_WAKE_THREAD를 반환하면 호출하는 irq_wake_thread() 함수를 살펴 보겠습니다. 
      [https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c]
      1 void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
      2 {
      ...
      3 atomic_inc(&desc->threads_active);
      4 wake_up_process(action->thread);
      5 }

      4 번째 코드를 보겠습니다.
      wake_up_process() 함수를 호출해서 IRQ 스레드를 깨운다는 사실을 알 수 있습니다. 

      여기서 wake_up_process() 함수에 전달하는 action->thread인자는 다음과 같이IRQ 스레드의 태스크 디스크립터 주소입니다.
      (struct irq_desc *) (struct irq_desc*)0xB008B300 
      ...
        (struct irqaction *) action = 0xBB4E6E40  
            (irq_handler_t) handler = 0x8061EC00 = bcm2835_mmc_irq,
      ...
            (irq_handler_t) thread_fn = 0x8061DCC4 = bcm2835_mmc_thread_irq,
            (struct task_struct *) thread = 0xBB516CC0

      wake_up_process() 함수 선언부를 확인해봐도 태스크 디스크립터를 인자로 받는다는 사실을 알 수 있습니다.
      extern int wake_up_process(struct task_struct *tsk);

      이 함수까지 인터럽트 컨텍스트 도중 처리하는 코드입니다. wake_up_process() 함수를 호출해서 IRQ 스레드를 깨우면 스케줄링을 실행하면 프로세스 레벨에서 IRQ 스레드가 실행합니다. 

      다음은 IRQ 스레드 전체 흐름도입니다.
       
         [그림 6.6] IRQ 스레드 전체 실행 흐름도에서 IRQ 스레드를 깨우는 부분

      위 흐름도에서 검은색으로된 부분이 이번 소절에서 살펴본 IRQ 스레드를 깨우는 과정입니다.
      다음 소절에서는 IRQ 스레드를 깨우면 어떤 흐름으로 IRQ 스레드가 실행하는지 살펴보겠습니다.
       

      # Reference 인터럽트 후반부 처리








      6.9 Soft IRQ 서비스는 누가 언제 처리하나?




      6.13 Soft IRQ 디버깅
      6.13.1 ftrace Soft IRQ 이벤트 분석 방법
      6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인




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