안녕하세요.
이전 내용에서 인터럽트와 인터럽트 컨텍스트에 대해서 확인했습니다.
이번에는 인터럽트 컨텍스트 구간인지 어떻게 알 수 있는지 확인합니다.
1. in_interrupt() 함수란?
결론적으로 말하면, 인터럽트 컨텍스트 구간이면 in_interrupt() 함수는 true를 반대면 false를 반환하는 함수입니다.
이 함수를 알고 있어야 하는 이유는 커널 코드 내에서는 수많은 함수를 호출되므로 함수 호출 흐름을 파악하기가 어렵습니다. 그렇기 때문에 커널 혹은 드라이버 코드에서 인터럽트 컨텍스트에서 실행 중인지 알기가 어렵습니다.
다시 인터럽트 컨텍스트에서 실행 중인지 확인이 필요한지 의문이 생깁니다. 그것은 인터럽트 서비스 루틴은 실행 중인 프로세스를 멈추고 동작하기 때문에, 커널 혹은 드라이버 코드에서 인터럽트 컨텍스트 조건에서 빠르게 동작하는 코드를 추가하는 것입니다.
1) in_interrupt() 함수를 써서 만든 패치 코드 확인
최근 패치 내용이지만, 직접 수정해서 작업해 봅니다.
사용자 공간에서 MMC 블록 장치에 대한 IOCTL 명령 데이터를 커널 공간으로 안전하게 복사하는 함수인 mmc_blk_ioctl_copy_from_user 내에 한 줄
block.c 파일 상의 364번 줄에 in_interrupt()와 ?(삼항연산자)를 사용해 코드를 수정했습니다.
in_interrupt() 함수가 true를 반환하면 인터럽트 컨텍스트로 인식하여 gfp_flag를 GFP_ATOMIC으로 설정하고, 반대 경우 GFP_KERNEL 옵션으로 할당합니다.
옵션의 차이를 간단하게 보자면, GFP_KERNEL은 여유 있게 메모리를 요청하는 상황에서 쓰고, GFP_ATOMIC은 절대 잠들 수 없는 상황에서 최소한의 메모리를 즉시 확보해야 할 때 쓰는 긴급 플래그입니다. 일반적 상황에서는 GFP_KERNEL 옵션을 많이 사용합니다.
2. in_interrupt() 함수 코드 분석해 보기
in_interrupt() 함수의 선언부를 확인해 봅니다. 그런데 함수 선언부를 보면 irq_count() 함수로 치환된다는 것을 알 수 있습니다.
linux# vim include/linux/preempt.h
irq_count() 함수를 확인해 보면 preempt_count() 반환값과 HARDIRQ_MAS | SOFTIRQ_MASK | NMI_MASK 비드 마스크에 대해 OR 비트 연산을 수행합니다.
플래그 값은 주석에서 알 수 있습니다. 결국, preempt_count() & 0x1fff00 이 됩니다.
preempt_count() 함수를 따라가 보면,
linux# vim include/asm-generic/preempt.h
preempt_count 값을 반환합니다. 그런데 어디서 본 듯한 thread_info가 있습니다.
current_thread_info() 함수를 보면, 스택 주소를 current_stack_pointer 변수로 읽는 동작을 합니다. 여기서 register 타입으로 선언했는데, ARM 레지스터를 써서 처리하겠다는 의미입니다. 즉 더 빠른 연산 속도를 기대할 수 있습니다.
읽어 오는 정정보는 현재 실행 중인 프로세스의 최상단 주소를 계산합니다. 이 주소에 프로세스의 실행 정보가 담긴 thread_info 구조체가 있습니다. 이전 글에서 thread를 구조체에 대해서 알아보았습니다.
결국 preempt_count()는 구조체 내의 preempt_count 필드 값을 얻어오는 값입니다.
linux# vim arch/arm/include/asm/thread_info.h
아래 그림과 같이 이전 "도서 공부하기 16 - 스레드 정보(thread_info)" 글에서 한 번 본 적이 있습니다.
0x0001 0002
0x001F FF00
---------------- AND 연산
0x 0001 0000 (true)
0x0001 0000 비트는 __irq_enter() 함수에서 HARDIRQ_OFFSET 비트를 프로세스의 thread_info 구조체의 preempt_count에 저장합니다.
__handle_domain_irq() 함수에서 인터럽트 핸들러를 호출하기 전에 irq_enter() 함수를 호출합니다. 이것이 인터럽트 처리 시작을 나타내는 표시입니다.
linux# vim kernel/irq/irqdesc.c
preempt_count_add() 함수를 호출하면 current_thread_info()->preempt_count 필드에 HARDIRQ_OFFSET 비트를 더합니다. 이 동작은 지금 인터럽트 처리 중이라는 의미입니다.
__irq_exit() 함수는 __irq_enter과 반대로 빼는 동작을 하며 인터럽트 처리 중인 상태가 아니라는 것을 말해줍니다.
3. 라즈베리 파이에서 in_interrupt() 함수 동작 방식 확인
라즈베리 파이에서 in_interrupt() 함수가 어떤 값을 반환하는지 확인해 보겠습니다. 그러기 위해서는 리눅스 커널의 소스를 수정하고 빌드해야 합니다.
<책에서 패치하는 부분 - raspberry pi4 버전에서 출력을 볼 수 없음>
bcm2835-sdhost.c 파일에서 패치를 했지만, 로그가 나오지 않았습니다.
linux# vim drivers/mmc/host/bcm2835-sdhost.c
<수정 파일을 bcm2835-mmc.c로 변경함>
유사하다고 생각하는 bcm2835_mmc_irq 함수에 패치 내용을 적용했습니다. 이 함수는 호스트 컨트롤러에서 발생하는 하드웨어 인터럽트를 처리하는 함수입니다.
linux# vim drivers/mmc/host/bcm2835-mmc.c
패치할 부분은 998줄~1006줄까지입니다. 간략하게 코드를 설명한다면
현재 실행 중인 프로세스의 스택 최상단 주소를 읽어 stack에 저장합니다. current는 프로세스 시간에 보았습니다. 프로세스의 태스크 디스크립트를 얻는 매크로입니다.
stack = current->stack;
스택 최상단 주소를 thread_info 구조체로 캐스팅해 current_thread 지역변수에 저장합니다.
current_thread = (struct thread_info*)stack;
in_interrupt() 함수 반환값, thread_info 구조체의 preempt_count 필드 값을 커널 로그로 출력합니다.
printk("[+] in_interrupt : 0x%08x,preempt_count = 0x%08x, statck=0x%08lx \n",
(unsigned int)in_interrupt(), (unsigned int)current_thread->preempt_count,
(long unsigned int)stack);
저장을 했다면, 이전에 빌드와 설치하는 스크립트를 실행하고 재부팅합니다. 그리고 로그를 통해 출력 결과를 확인합니다.
빌드 시 error로 make mrproper 하라고 나올 수 있습니다.
커널 빌드를 시작하기 전에 깨끗한 빌드 환경을 통해 잠재적 문제를 예방하기 위한 빌드를 전 작업이라고 생각하면 됩니다.
linux# sh ../build_rpi_kernel.sh
linux# sh ../install_rpi_kernel_img.sh
재부팅 후 dmesg를 통해 로그를 확인합니다.
linux# dmesg | grep in_interrupt
preempt_count 값은 0x0001 0000이고 in_interrupt 값은 0x0001 0000입니다. preempt_count는 프로세스 스택의 최상단 주소에 있는 thread_info 구조체의 필드에 저장된 값입니다.
출력 결과를 보면, in_interrupt를 통해 현재 인터럽트 처리 중이고 현재 코드가 인터럽트 컨텍스트라는 것을 알려줍니다.
메시지가 많이 생성되므로 확인 후 추가한 소스코드를 삭제 혹은 주석 처리하고 다시 빌드해서 사용하는 것을 추천드립니다.
감사합니다.
<참고 자료>
1. [도서] 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 p302 ~ 318, wikibook