Kernel-provided User Helpers

 

커널 메모리의 코드 및 데이터 일부를 유저가 직접 호출 또는 접근할 수 있도록 유일하게 허용한 페이지이다.

  • Posix syscall을 사용하지 않고 직접 유저 공간에서 호출하므로 매우 빠른 속도가 요구되는 코드를 수행할 수 있다.
  • 하이 벡터를 사용하는 프로세서에서 CONFIG_KUSER_HELPERS 커널 옵션을 사용하여 제공한다.
    • 로우 벡터에서는 kuser helper 코드를 지원하지 않는다.
  • arm에서 가상 주소 0xffff_0000로 시작하는 페이지를 사용하는 하이 벡터 페이지의 사용하지 않는 윗 부분을 이용한다.
    • 현재 0xffff_0f60 ~ 0xffff0fff 까지 범위에서 사용하고 있다.

 

코드 위치

현재 커널은 하이벡터 페이지의 가장 윗 부분을 이용하여 다음 4개의 함수와 1개의 상수 값을 제공한다.

  • __kuser_cmpxchg64()
  • __kuser_memory_barrier()
  • __kuser_cmpxchg()
  • __kuser_get_tls()
  • __kuser_helper_version 상수

 

코드가 위치한 주소는 다음 그림과 같다.

 

Kernel provided User Helper 버전 확인

가상 주소: 0xffff_0ffc

 

사용 방법

#define __kuser_helper_version (*(int32_t *)0xffff0ffc)

void check_kuser_version(void)
{
        if (__kuser_helper_version < 2) {
                fprintf(stderr, "can't do atomic operations, kernel too old\n");
                abort();
        }
}

버전 값이 2보다 작은 경우 커널이 너무 오래되어서 atomic operation을 지원하지 않는 것을 알 수 있다.

 

__kuser_cmpxchg64() 함수

가상 주소: 0xffff_0f60

 

사용방법

typedef int (__kuser_cmpxchg64_t)(const int64_t *oldval,
                                  const int64_t *newval,
                                  volatile int64_t *ptr);
#define __kuser_cmpxchg64 (*(__kuser_cmpxchg64_t *)0xffff0f60)

int64_t atomic_add64(volatile int64_t *ptr, int64_t val)
{
        int64_t old, new;

        do {
                old = *ptr;
                new = old + val;
        } while(__kuser_cmpxchg64(&old, &new, ptr));

        return new;
}

64bit 값이 담긴 ptr 포인터 주소에 val 값을 atomic하게 더한다.

  • __kuser_cmpxchg64() 함수는 64bit 현재 값이 oldval과 같은 경우 newval을 atomic 하게 저장하고 성공리에 0을 반환한다. 그 외의 값은 oldval과 newval이 달라 저장하지 않은 경우이다.

 

 3가지 구현  방법

Kernel-provided User Helper code 중 kuser_cmpxchg64() 함수는 시스템에 따라 3 가지의 구현 중 하나를 사용한다.

  1. fastpath
    • ARMv6K를 포함하여 이후 버전의 ARM 아키텍처는 user space에서 직접 cmpxchg 및 cmpxchg64 atomic operation을 수행할 수 있다.
      • ARM SMP 시스템에서 사용하는 atomic operation
        • ARMv6: swp
        • ARMv7: ldrex/strex이 권장 사용되며, swp는 호환목적으로 s/w emulation 방법을 사용하여 구현되어 있다.(비권장)
  2. slowpath for SMP
    • fastpath를 지원하지 않는 SMP 아키텍처를 사용하는 경우 CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션을 사용하여 arm syscall을 사용하여 커널로 진입하여 64bit cmpxchg atomic operation을 수행하는 방법이다.
  3. slowpath for UP
    • UP 시스템에서 atomic을 포기하고 순서대로 진행하는 방법으로 인터럽트가 커널에서 호출되는 경우에 한하여 fixup을 수행하여 보강한다.

 

NEEDS_SYSCALL_FOR_CMPXCHG

  • arm 아키텍처로 집중해서 이 커널 옵션을 보면 ARMv6 이전 SMP 프로세서(아직은 이렇게 만든 SoC가 없는 것 같다. 하지만 미래에 어떤 회사가 만들지 모르는 법이므로…)는 직접 user space에서 atomic하게 cmpxchg를 수행할 수 없다. 따라서 느리더라도 POSIX call을 사용하여 커널에 진입한 후 처리하도록 이 방법을 사용하여야 한다.

 

__kuser_cmpxchg64:

user space에서 64 bit atomic operation을 처리하기 위해 이러한 기능에 대한 아키텍처의 지원 여부에 따라 구현 방법을 3가지로 달리 구현되었다.

1) 커널에 위탁 처리하는 방법

arch/arm/kernel/entry-armv.S

/*
 * Due to the length of some sequences, __kuser_cmpxchg64 spans 2 regular
 * kuser "slots", therefore 0xffff0f80 is not used as a valid entry point.
 */

__kuser_cmpxchg64:                              @ 0xffff0f60

#if defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

        /*
         * Poor you.  No fast solution possible...
         * The kernel itself must perform the operation.
         * A special ghost syscall is used for that (see traps.c).
         */
        stmfd   sp!, {r7, lr}
        ldr     r7, 1f                  @ it's 20 bits
        swi     __ARM_NR_cmpxchg64
        ldmfd   sp!, {r7, pc}
1:      .word   __ARM_NR_cmpxchg64

CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션은 주로 user space에서 atomic operation을 지원하지 못하는 SMP 아키텍처에서 사용한다. atomic operation이 필요한 경우 커널은 인터럽트되지 않도록 블럭한 후 처리할 수 있기 때문에 user space에서의 kuser_cmpxchg64 요청은 arm syscall을 사용하여 커널에 위탁하여 처리하고 그 결과를 내려보내주게 구현되었다.

  • 코드 라인 15에서 r7, lr 레지스터 사용해야 하므로 잠시 스택에 보관한다.
  • 코드 라인 16~17에서 r7에 __ARM_NR_cmpxchg64에 해당하는 arm syscall 넘버를 대입한 후 swi swi 명령을 사용하여 arm syscall 호출을 수행한다.
  • 코드 라인 18에서 스택으로 부터 r7을 복구하고, lr 주소로 jump 한다.

 

2) atomic이 지원되는 아키텍처에서 직접 수행하는 방법

#elif defined(CONFIG_CPU_32v6K)

        stmfd   sp!, {r4, r5, r6, r7}
        ldrd    r4, r5, [r0]                    @ load old val
        ldrd    r6, r7, [r1]                    @ load new val
        smp_dmb arm
1:      ldrexd  r0, r1, [r2]                    @ load current val
        eors    r3, r0, r4                      @ compare with oldval (1)
        eoreqs  r3, r1, r5                      @ compare with oldval (2)
        strexdeq r3, r6, r7, [r2]               @ store newval if eq
        teqeq   r3, #1                          @ success?
        beq     1b                              @ if no then retry
        smp_dmb arm
        rsbs    r0, r3, #0                      @ set returned val and C flag
        ldmfd   sp!, {r4, r5, r6, r7}
        usr_ret lr

CONFIG_CPU_32v6K는 user space에서도 atomic operation을 사용할 수 있는 armv6 이상의 SMP 아키텍처에서 사용하는 커널 옵션이다. 이러한 경우 user space에서 직접 ldrex 및 strex를 사용하여 atomic operation을 수행할 수 있다.

  • 코드 라인 3에서 r4~r7까지 레지스터를 스택에 백업한다.
  • 코드 라인 4에서 r0(old)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r4와 r5 레지스터에 저장한다.
    • ldr r4, [r0]; ldr r5, [r0, #4]와 동일한 결과다.
  • 코드 라인 5에서 r1(new)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r6와 r7 레지스터에 저장한다.
  • 코드 라인 6에서 다음 ldrexd를 호출하기 전에 dmb를 사용하여 메모리에 대해 order 문제가 생기지 않도록 한다.
  • 코드 라인 7에서 r2(ptr)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r0와 r1 레지스터에  저장한다.
  • 코드 라인 8~10에서 읽은 ptr 값(r0, r1)과 old 값(r4, r5)가 비교를 하여 같은 경우 r2 주소에 new(r6, r7) 값을 저장하되 결과를 r3에 저장한다.
  • 코드 라인 11~12에서 저장 시 이 캐시 라인이 exclusive되지 않아 결과(r3)이 실패(#1)한 경우 다시 레이블 1로 이동하여 성공할 때까지 반복한다.
    • 다른 cpu에서 이 exclusive한 캐시라인에 접근하는 경우 open되어 strex 명령을 수행 시 실패를 얻게된다.
  • 코드 라인 13에서 다시 메모리 배리어를 사용하여 order 문제가 생기지 않도록 한다.
  • 코드 라인 14에서 0에서 r3(0=기록한 경우, 그 외=oldval과 newval이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 15~16에서 스택에 백업해둔 레지스터들을 복구하고 lr 주소로 복귀한다.

 

3) UP 시스템에서 atomic 구현

#elif !defined(CONFIG_SMP)

#ifdef CONFIG_MMU

        /*
         * The only thing that can break atomicity in this cmpxchg64
         * implementation is either an IRQ or a data abort exception
         * causing another process/thread to be scheduled in the middle of
         * the critical sequence.  The same strategy as for cmpxchg is used.
         */
        stmfd   sp!, {r4, r5, r6, lr}
        ldmia   r0, {r4, r5}                    @ load old val
        ldmia   r1, {r6, lr}                    @ load new val
1:      ldmia   r2, {r0, r1}                    @ load current val
        eors    r3, r0, r4                      @ compare with oldval (1)
        eoreqs  r3, r1, r5                      @ compare with oldval (2)
2:      stmeqia r2, {r6, lr}                    @ store newval if eq
        rsbs    r0, r3, #0                      @ set return val and C flag
        ldmfd   sp!, {r4, r5, r6, pc}

        .text
kuser_cmpxchg64_fixup:
        @ Called from kuser_cmpxchg_fixup.
        @ r4 = address of interrupted insn (must be preserved).
        @ sp = saved regs. r7 and r8 are clobbered.
        @ 1b = first critical insn, 2b = last critical insn.
        @ If r4 >= 1b and r4 <= 2b then saved pc_usr is set to 1b.
        mov     r7, #0xffff0fff
        sub     r7, r7, #(0xffff0fff - (0xffff0f60 + (1b - __kuser_cmpxchg64)))
        subs    r8, r4, r7
        rsbcss  r8, r8, #(2b - 1b)
        strcs   r7, [sp, #S_PC]
#if __LINUX_ARM_ARCH__ < 6
        bcc     kuser_cmpxchg32_fixup
#endif
        ret     lr
        .previous
#endif

        kuser_pad __kuser_cmpxchg64, 64

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg64 함수의 기능 구현은 atomic과 관련 없이 단순하게 구현되어 있다. 그 구현 루틴 아래에 위치한 kuser_cmpxchg64_fixup 루틴을 살펴보기 전에는 그 어떠한 atomic 관련한 부가 루틴도 찾아 볼 수가 없다. atomic에 대한 보장을 위해 자세한 것은 kuser_cmpxchg64_fixup 레이블에서 설명하기로 한다.

  • 코드 라인 11에서 r4, r5, r6, lr 레지스터를 스택에 백업해둔다.
  • 코드 라인 12에서 r0위치에 있는 64bit old 값을 r4와 r5 레지스터에 로드한다.
  • 코드 라인 13에서 r1 위치에 있는 64bit new 값을 r6와 lr 레지스터에 로드한다.
  • 코드 라인 14에서 r2 위치에 있는 64bit ptr 값을 r0와 r1 레지스터에 로드한다.
  • 코드 라인 15~17에서 old 값과 ptr 값을 비교하여 같은 경우 new 값을 ptr에 저장한다.
  • 코드 라인 18에서 0에서 r3(0=기록한 경우, 그 외=old 값과 new 값이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 19에서 스택에 백업해둔 레지스터들을 복구하고 lr 주소로 복귀한다.
kuser_cmpxchg64_fixup:

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg64 루틴을 수행 시 atomic하게 처리해야 하는 구간 즉, 레이블 1과 레이블 2 사이를 수행하는 도중에 irq, fiq, dabt 등을 만나게 되는 경우 해당 exception을 처리한 후 되돌아갈 주소를 atomic 구간의 가장 윗 부분으로 바꾸기 위해 스택의 pt_regs의 pc를 조작하는 방법을 사용한다.

  • 코드 라인 28~29에서 atomic operation이 시작되는 레이블 1:의 가상 주소 값을 알아온다.
    • r7 <- ffff0f60 + (1b – __kuser_cmpxchg64)를 대입하고 싶지만 operand에 사용하는 값의 크기 제한으로 인해 두 개의 명령을 사용하였다.
  • 코드 라인 30~32에서 인터럽트 되었을 때의 pc 주소가 담긴 r4 레지스터 값이 atomic 하게 처리할 구간 범위(레이블 1: ~ 레이블 2:)인 경우 스택에 저장해 둔 pt_regs 구조체 중 pc 위치에 레이블 1 주소를 저장하여 다시 인터럽트 복귀 시 atomic operation을 다시 시도하게 변경한다.
  • 코드 라인 33~35에서 arm 아키텍처가 버전 6보다 이전인 경우에는 위의 atomic opeation 수행 도중 인터럽트 된 것이 아니라면  kuser_cmpxchg32 명령 수행 도중에 발생한 일인지 확인하여 역시 같은 방식으로 복귀 주소를 변경하게 한다.

 

__kuser_memory_barrier:

arch/arm/kernel/entry-armv.S

__kuser_memory_barrier:                         @ 0xffff0fa0
        smp_dmb arm
        usr_ret lr

        kuser_pad __kuser_memory_barrier, 32

SMP 시스템에서 메모리 배리어를 사용하여 이전 로직의 메모리 액세스와 연관하여 order 문제가 생기지 않도록 막는다.

 

__kuser_cmpxchg:

user space에서 32 bit atomic operation을 처리하기 위해 이러한 기능에 대한 아키텍처의 지원 여부에 따라 구현 방법을 3가지로 달리 구현되었다.

1) 커널에 위탁 처리하는 방법

arch/arm/kernel/entry-armv.S

__kuser_cmpxchg:                                @ 0xffff0fc0

#if defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

        /*
         * Poor you.  No fast solution possible...
         * The kernel itself must perform the operation.
         * A special ghost syscall is used for that (see traps.c).
         */
        stmfd   sp!, {r7, lr}
        ldr     r7, 1f                  @ it's 20 bits
        swi     __ARM_NR_cmpxchg
        ldmfd   sp!, {r7, pc}
1:      .word   __ARM_NR_cmpxchg

CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션은 주로 user space에서 atomic operation을 지원하지 못하는 SMP 아키텍처에서 사용한다. atomic operation이 필요한 경우 커널은 인터럽트되지 않도록 블럭한 후 처리할 수 있기 때문에 user space에서의 kuser_cmpxchg 요청은 arm syscall을 사용하여 커널에 위탁하여 처리하고 그 결과를 내려보내주게 구현되었다.

  • 코드 라인 10에서 r7, lr 레지스터 사용해야 하므로 잠시 스택에 보관한다.
  • 코드 라인 11~12에서 r7에 __ARM_NR_cmpxchg64에 해당하는 arm syscall 넘버를 대입한 후 swi swi 명령을 사용하여 arm syscall 호출을 수행한다.
  • 코드 라인 13에서 스택으로 부터 r7을 복구하고, lr 주소로 jump 한다.

 

2) UP 시스템에서 atomic 구현

#elif __LINUX_ARM_ARCH__ < 6

#ifdef CONFIG_MMU

        /*
         * The only thing that can break atomicity in this cmpxchg
         * implementation is either an IRQ or a data abort exception
         * causing another process/thread to be scheduled in the middle
         * of the critical sequence.  To prevent this, code is added to
         * the IRQ and data abort exception handlers to set the pc back
         * to the beginning of the critical section if it is found to be
         * within that critical section (see kuser_cmpxchg_fixup).
         */
1:      ldr     r3, [r2]                        @ load current val
        subs    r3, r3, r0                      @ compare with oldval
2:      streq   r1, [r2]                        @ store newval if eq
        rsbs    r0, r3, #0                      @ set return val and C flag
        usr_ret lr

        .text
kuser_cmpxchg32_fixup:
        @ Called from kuser_cmpxchg_check macro.
        @ r4 = address of interrupted insn (must be preserved).
        @ sp = saved regs. r7 and r8 are clobbered.
        @ 1b = first critical insn, 2b = last critical insn.
        @ If r4 >= 1b and r4 <= 2b then saved pc_usr is set to 1b.
        mov     r7, #0xffff0fff
        sub     r7, r7, #(0xffff0fff - (0xffff0fc0 + (1b - __kuser_cmpxchg)))
        subs    r8, r4, r7
        rsbcss  r8, r8, #(2b - 1b)
        strcs   r7, [sp, #S_PC]
        ret     lr
        .previous

#else
#warning "NPTL on non MMU needs fixing"
        mov     r0, #-1
        adds    r0, r0, #0
        usr_ret lr
#endif

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg 함수의 기능 구현은 atomic과 관련 없이 단순하게 구현되어 있다. 그 구현 루틴 아래에 위치한 kuser_cmpxchg_fixup 루틴을 살펴보기 전에는 그 어떠한 atomic 관련한 부가 루틴도 찾아 볼 수가 없다. atomic에 대한 보장을 위해 자세한 것은 kuser_cmpxchg_fixup 레이블에서 설명하기로 한다.

  • 코드 라인 14에서 r2(ptr)위치에 있는 값을 r3 레지스터에 로드한다.
  • 코드 라인 15~16에서 r0(old) 레지스터 값과 r3 레지스터 값을 비교하여 같은 경우 r1(new) 레지스터 값을 r2(ptr) 레지스터가 가리키는 주소에 저장한다.
  • 코드 라인 17~18은 0에서 r3(0=기록한 경우, 그 외=old 값과 new 값이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담고 lr 주소로 복귀한다.
kuser_cmpxchg_fixup:

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg 루틴을 수행 시 atomic하게 처리해야 하는 구간 즉, 레이블 1과 레이블 2 사이를 수행하는 도중에 irq, fiq, dabt 등을 만나게 되는 경우 해당 exception을 처리한 후 되돌아갈 주소를 atomic 구간의 가장 윗 부분으로 바꾸기 위해 스택의 pt_regs의 pc를 조작하는 방법을 사용한다.

  • 코드 라인 27~28에서 atomic operation이 시작되는 레이블 1:의 가상 주소 값을 알아온다.
    • r7 <- ffff0fc0 + (1b – __kuser_cmpxchg)를 대입하고 싶지만 operand에 사용하는 값의 크기 제한으로 인해 두 개의 명령을 사용하였다.
  • 코드 라인 29~32에서 인터럽트 되었을 때의 pc 주소가 담긴 r4 레지스터 값이 atomic 하게 처리할 구간 범위(레이블 1: ~ 레이블 2:)인 경우 스택에 저장해 둔 pt_regs 구조체 중 pc 위치에 레이블 1 주소를 저장하여 다시 인터럽트 복귀 시 atomic operation을 다시 처음 부터 시도하게 변경하고 lr 주소로 복귀한다.

 

3) atomic이 지원되는 아키텍처에서 직접 수행하는 방법

#else

        smp_dmb arm
1:      ldrex   r3, [r2]
        subs    r3, r3, r0
        strexeq r3, r1, [r2]
        teqeq   r3, #1
        beq     1b
        rsbs    r0, r3, #0
        /* beware -- each __kuser slot must be 8 instructions max */
        ALT_SMP(b       __kuser_memory_barrier)
        ALT_UP(usr_ret  lr)

#endif

        kuser_pad __kuser_cmpxchg, 32

user space에서도 atomic operation을 사용할 수 있는 armv6 이상의 SMP 아키텍처에서 사용하는 커널 옵션이다. 이러한 경우 user space에서 직접 ldrex 및 strex를 사용하여 atomic operation을 수행할 수 있다.

  • 코드 라인 3에서 메모리 배리어를 사용하여 이전 로직의 메모리 액세스와 연관하여 order 문제가 생기지 않도록 막는다.
  • 코드 라인 4~6에서 r2(ptr)가 가리키는 주소에서 값을 읽은 값을 r3 레지스터에 저장한다. 이 값과 r0(old) 값을 비교하여 같은 경우 r1(new) 값을 r2(ptr)가 가리키는 주소에 저장한다. 저장 결과는 r3에 담는다. (성공=0, 실패=1)
  • 코드 라인 7~8에서 저장 시 이 캐시 라인이 exclusive되지 않아 결과(r3)이 실패(#1)한 경우 다시 레이블 1로 이동하여 성공할 때까지 반복한다.
    • 다른 cpu에서 이 exclusive한 캐시라인에 접근하는 경우 open되어 strex 명령을 수행 시 실패를 얻게된다.
  • 코드 라인 9는 0에서 r3(0=기록한 경우, 그 외=oldval과 newval이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 11~12에서 복귀를 하되 SMP 시스템인 경우 다시 메모리 배리어를 사용하여 order 문제가 생기지 않도록 한다.

 

__kuser_get_tls:

arch/arm/kernel/entry-armv.S

__kuser_get_tls:                        @ 0xffff0fe0
        ldr     r0, [pc, #(16 - 8)]     @ read TLS, set in kuser_get_tls_init
        usr_ret lr
        mrc     p15, 0, r0, c13, c0, 3  @ 0xffff0fe8 hardware TLS code
        kuser_pad __kuser_get_tls, 16
        .rep    3
        .word   0                       @ 0xffff0ff0 software TLS value, then
        .endr

TLS(Thread Local Storage) 값을 알아온다.  2가지의 구현을 사용한다.

  • S/W TLS
    • 0xffff_0ff0 주소에 TLS 값을 보관해두고 그 값을 읽어 사용한다.
  • H/W TLS
    • user 에서 읽기만 가능한 TPIDRURO 레지스터에서 값을 읽어 사용한다.

 

  • 코드 라인 2~3에서 0xfff_0ff0 위치에서 TLS 값을 가져와 r0 레지스터에 담고 lr 주소로 복귀한다.
  • 코드 라인 4에는 TPIDRURO 레지스터에서 값을 읽어 r0 레지스터에 담는 코드를 두었다.
    • H/W TLS 레지스터를 지원하는 경우 이 코드를 복사하여 코드 라인 2의 주소인 0xffff_0fe0에 복사하기 위해 사용된다.
      • setup_arch() -> paging_init() -> devicemaps_init() -> early_trap_init() -> kuser_init() 함수 내부에 다음 코드를 찾을 수 있다.
        • memcpy(vectors + 0xfe0, vectors + 0xfe8, 4);

 

매크로 함수

usr_ret 매크로

arch/arm/kernel/entry-armv.S

        .macro  usr_ret, reg
#ifdef CONFIG_ARM_THUMB
        bx      \reg
#else
        ret     \reg
#endif
        .endm

\reg 주소로 복귀한다.

 

kuser_pad 매크로

arch/arm/kernel/entry-armv.S

        .macro  kuser_pad, sym, size
        .if     (. - \sym) & 3
        .rept   4 - (. - \sym) & 3
        .byte   0
        .endr
        .endif
        .rept   (\size - (. - \sym)) / 4
        .word   0xe7fddef1
        .endr
        .endm

현재 주소 위치 – sym 주소가 4바이트 단위로 정렬되지 않은 경우 정렬을 위해 0으로 채운다.(0~3 바이트) 그 이후 sym 주소부터 size 만큼의 공간 중 현재 위치부터 0xe7fddef1 값으로 채운다.

  • 예) kuser_pad __kuser_memory_barrier, 32
    • __kuser_memory_barrier 함수 위치 부터 32 바이트 공간내에서 빈 자리를 0xe7fddef1 값으로 채운다.
      • 0xffff0fa0: 0xf57ff05b 0xe12fff1e 0xe7fddef1 0xe7fddef1
      • 0xffff0fb0: 0xe7fddef1 0xe7fddef1 0xe7fddef1 0xe7fddef1

 

참고

do_IPI()

 

do_IPI()

arch/arm/kernel/smp.c

/*
 * Main handler for inter-processor interrupts
 */
asmlinkage void __exception_irq_entry do_IPI(int ipinr, struct pt_regs *regs)
{
        handle_IPI(ipinr, regs);
}

ipinr 번호에 해당하는 IPI 소프트콜 서비스를 수행한다.

 

handle_IPI()

arch/arm/kernel/smp.c

void handle_IPI(int ipinr, struct pt_regs *regs)
{
        unsigned int cpu = smp_processor_id();
        struct pt_regs *old_regs = set_irq_regs(regs);

        if ((unsigned)ipinr < NR_IPI) {
                trace_ipi_entry(ipi_types[ipinr]);
                __inc_irq_stat(cpu, ipi_irqs[ipinr]);
        }

        switch (ipinr) {
        case IPI_WAKEUP:
                break;

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
        case IPI_TIMER:
                irq_enter();
                tick_receive_broadcast();
                irq_exit();
                break;
#endif

        case IPI_RESCHEDULE:
                scheduler_ipi();
                break;

        case IPI_CALL_FUNC:
                irq_enter();
                generic_smp_call_function_interrupt();
                irq_exit();
                break;

        case IPI_CALL_FUNC_SINGLE:
                irq_enter();
                generic_smp_call_function_single_interrupt();
                irq_exit();
                break;

        case IPI_CPU_STOP:
                irq_enter();
                ipi_cpu_stop(cpu);
                irq_exit();
                break;

#ifdef CONFIG_IRQ_WORK
        case IPI_IRQ_WORK:
                irq_enter();
                irq_work_run();
                irq_exit();
                break;
#endif

        case IPI_COMPLETION:
                irq_enter();
                ipi_complete(cpu);
                irq_exit();
                break;

        default:
                pr_crit("CPU%u: Unknown IPI message 0x%x\n",
                        cpu, ipinr);
                break;
        }

        if ((unsigned)ipinr < NR_IPI)
                trace_ipi_exit(ipi_types[ipinr]);
        set_irq_regs(old_regs);
}

ipinr 번호에 해당하는 IPI 소프트콜 서비스를 수행한다.

 

  • 코드 라인 3에서 현재 cpu 번호를 알아온다.
  • 코드 라인 4에서 기존 *pt_regs 포인터 주소를 가져오고, per-cpu *pt_regs 포인터 주소에 인수로 받은 새 regs를 대입한다.
  • 코드 라인 6~9에서 ipinr이 범위내에 있는 경우 trace 출력을 하고 해당 ipinr에 해당하는 ipi 카운터를 증가시킨다.
  • 코드 라인 11~13에서 IPI_WAKEUP(0) 요청을 받은 경우 아무것도 처리하지 않는다.
    • 이미 깨어나서 돌고 있고 아울러 트래킹을 위해 이미 해당 통계 카운터도 증가시켰다.
    • 호출 방법 1: arch_send_wakeup_ipi_mask(cpumask) 함수를 사용하여 cpumask에 해당하는 각 cpu들에 대해 WFI에 의해 잠들어 있는 경우 프로세서를 깨울 수 있다.
    • 호출 방법 2: 빅/리틀 hot plug cpu 시스템에서 gic_raise_softirq() 함수를 사용하여 WFI에 의해 대기하고 있는 cpu들을 깨운다. 이 때 인수로 1대신 0을 사용하여 호출하면 불필요한 printk 메시지(“CPU%u: Unknown IPI message 0x00000001”)를 출력하지 않고 트래킹을 위해 해당 카운터 수도 추적할 수 있다.
      • PM으로 전력 기능까지 콘트롤하는 GIC(Generic Interrupt Controller) #0을 사용하는 방법으로 지정한 cpu를 wakeup 시킨다.
    • 참고: ARM: 7536/1: smp: Formalize an IPI for wakeup
  • 코드 라인 15~21에서 IPI_TIMER(1) 요청을 받은 경우 tick 디바이스에 등록된 이벤트 디바이스의 핸들러 함수를 호출한다.
  • 코드 라인 23~25에서 IPI_RESCHEDULE 요청을 받은 경우 현재 프로세서에 대해 리스케쥴한다.
  • 코드 라인 27~31에서 IPI_CALL_FUNC 요청을 받은 경우 미리 등록된 함수들을 호출한다.
  • 코드 라인 33~37에서 IPI_CALL_FUNC_SINGLE 요청을 받은 경우 미리 등록된 함수를 호출한다.
  • 코드 라인 39~43에서 IPI_CPU_STOP 요청을 받은 경우 현재  cpu가 부팅 중 또는 동작 중인 경우 “CPU%u: stopping” 메시지 출력 및 스택 덤프를 하고 해당 cpu의 irq, fiq를 모두 정지 시키고 offline 상태로 바꾼 후 정지(spin)한다.
  • 코드 라인 45~51에서 IPI_IRQ_WORK 요청을 받은 경우 현재의 모든 irq 작업들을 즉시 수행하게 한다.
  • 코드 라인 53~57에서 IPI_COMPLETION 요청을 받은 경우 register_ipi_completion() 함수로 등록해 놓은 per-cpu cpu_completion 에서 wait_for_completion()등으로 대기하고 있는 스레드를 깨운다.
  • 코드 라인 65~66에서  ipinr이 범위내에 있는 경우 trace 출력을 한다.
  • 코드 라인 67에서 per-cpu *pt_regs 포인터 주소에 백업해 둔 old_regs를 대입한다.

 

tick_receive_broadcast()

kernel/time/tick-broadcast.c

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
int tick_receive_broadcast(void)
{
        struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
        struct clock_event_device *evt = td->evtdev;

        if (!evt)
                return -ENODEV;

        if (!evt->event_handler)
                return -EINVAL;

        evt->event_handler(evt);
        return 0;
}
#endif

tick 디바이스에 등록된 이벤트 디바이스의 핸들러 함수를 호출한다.

  • CONFIG_GENERIC_CLOCKEVENTS_BROADCAST 커널 옵션을 사용하는 경우에만 동작한다.
  • 호출 방법: tick_do_broadcast() 함수를 사용하여 해당 cpu들에 tick을 전달한다.
  • 참고: clockevents: Add generic timer broadcast receiver

 

  • 코드 라인 4~8에서 tick 디바이스에 등록된 clock 이벤트 디바이스를 알아온다. 없으면 -ENODEV 에러를 반환한다.
  • 코드 라인 10~11에서 clock 이벤트 디바이스에 이벤트 핸들러 함수가 등록되어 있지 않으면 -EINVAL 에러를 반환한다.
  • 코드 라인 13에서 등록된 이벤트 핸들러 함수를 호출한다.

 

scheduler_ipi()

kernel/sched/core.c

void scheduler_ipi(void)
{
        /*            
         * Fold TIF_NEED_RESCHED into the preempt_count; anybody setting
         * TIF_NEED_RESCHED remotely (for the first time) will also send
         * this IPI.
         */
        preempt_fold_need_resched();

        if (llist_empty(&this_rq()->wake_list) && !got_nohz_idle_kick())
                return;

        /*
         * Not all reschedule IPI handlers call irq_enter/irq_exit, since
         * traditionally all their work was done from the interrupt return
         * path. Now that we actually do some work, we need to make sure
         * we do call them.
         *
         * Some archs already do call them, luckily irq_enter/exit nest
         * properly.
         *
         * Arguably we should visit all archs and update all handlers,
         * however a fair share of IPIs are still resched only so this would
         * somewhat pessimize the simple resched case.
         */
        irq_enter();
        sched_ttwu_pending();

        /*
         * Check if someone kicked us for doing the nohz idle load balance.
         */
        if (unlikely(got_nohz_idle_kick())) {
                this_rq()->idle_balance = 1;
                raise_softirq_irqoff(SCHED_SOFTIRQ);
        }
        irq_exit();
}

현재 프로세서에 대해 리스케쥴한다.

  • 호출 방법: smp_send_reschedule() 명령을 사용하여 해당 cpu에서 리스케쥴링하도록 요청한다.

 

  • 코드 라인 8에서 현재 스레드에 리스케쥴 요청이 있는 경우 preempt count에 있는 리스쥴 요청중 비트를 제거한다.
  • 코드 라인 10~11애서 런큐의 wake_list가 비어 있으면서 현재 cpu의 런큐에 NOHZ_BALANCE_KICK 요청이 없거나, 현재 cpu가 idle 상태가 아니거나 리스케쥴 요청이 있는 경우 리스케쥴할 태스크가 없어서 함수를 빠져나간다.
  • 코드 라인 26에서 hard irq preecmption 카운터를 증가시켜 preemption을 막고 irq 소요 시간 및 latency를 측정할 수 있도록 한다.
  • 코드 라인 27에서 런큐의 wake_list에서 모두 꺼내서 다시 enqueue 하여 리스케쥴 한다.
  • 코드 라인 32~35에서 낮은 확률로 현재 cpu의 런큐에 NOHZ_BALANCE_KICK 요청이 있고 현재 cpu가 idle 상태이면서 리스케쥴 요청이 없는 경우 런큐의 idle_balance 를 1로 설정하고 SCHED_SOFTIRQ 를 정지시킨다.
  • 코드 라인 36에서  hard irq preecmption 카운터를 감소시켜 preemption을 다시 열어주고  irq 소요 시간 및 latency를 측정할 수 있도록 후처리 작업을 수행한다.

 

generic_smp_call_function_interrupt()

include/linux/smp.h

#define generic_smp_call_function_interrupt \
        generic_smp_call_function_single_interrupt

IPI에 의해 인터럽트 된 후 미리 등록된 함수를 호출한다.

  • 호출 방법: arch_send_call_function_ipi_mask() 명령을 사용하여 해당 cpu들에서 미리 등록된 함수들을 호출한다.

 

generic_smp_call_function_single_interrupt()

kernel/smp.c

/**
 * generic_smp_call_function_single_interrupt - Execute SMP IPI callbacks
 *
 * Invoked by arch to handle an IPI for call function single.
 * Must be called with interrupts disabled.
 */
void generic_smp_call_function_single_interrupt(void)
{
        flush_smp_call_function_queue(true);
}

IPI에 의해 인터럽트 된 후 미리 등록된 함수를 호출한다.

  • 호출 방법: arch_send_call_function_single_ipi() 명령을 사용하여 요청 cpu에서 미리 등록된 함수를 호출한다.

 

flush_smp_call_function_queue()

kernel/smp.c

/**
 * flush_smp_call_function_queue - Flush pending smp-call-function callbacks
 *
 * @warn_cpu_offline: If set to 'true', warn if callbacks were queued on an
 *                    offline CPU. Skip this check if set to 'false'.
 *
 * Flush any pending smp-call-function callbacks queued on this CPU. This is
 * invoked by the generic IPI handler, as well as by a CPU about to go offline,
 * to ensure that all pending IPI callbacks are run before it goes completely
 * offline.
 *
 * Loop through the call_single_queue and run all the queued callbacks.
 * Must be called with interrupts disabled.
 */
static void flush_smp_call_function_queue(bool warn_cpu_offline)
{
        struct llist_head *head;
        struct llist_node *entry;
        struct call_single_data *csd, *csd_next;
        static bool warned;

        WARN_ON(!irqs_disabled());

        head = this_cpu_ptr(&call_single_queue);
        entry = llist_del_all(head);
        entry = llist_reverse_order(entry);

        /* There shouldn't be any pending callbacks on an offline CPU. */
        if (unlikely(warn_cpu_offline && !cpu_online(smp_processor_id()) &&
                     !warned && !llist_empty(head))) {
                warned = true; 
                WARN(1, "IPI on offline CPU %d\n", smp_processor_id());

                /*
                 * We don't have to use the _safe() variant here
                 * because we are not invoking the IPI handlers yet.
                 */
                llist_for_each_entry(csd, entry, llist)
                        pr_warn("IPI callback %pS sent to offline CPU\n",
                                csd->func);
        }

        llist_for_each_entry_safe(csd, csd_next, entry, llist) {
                csd->func(csd->info);
                csd_unlock(csd);
        }

        /*
         * Handle irq works queued remotely by irq_work_queue_on().
         * Smp functions above are typically synchronous so they
         * better run first since some other CPUs may be busy waiting
         * for them.
         */
        irq_work_run();
}

call_single_queue에 있는 모든 call function들을 한꺼번에 처리하고 비운다. 또한 남은 irq work도 모두 처리하여 비운다.

 

  • 코드 라인 24~26에서 per-cpu call_single_queue에 등록된 엔트리들을 모두 제거하고 entry로 가져오는데 가장 처음에 추가한 call_single_data 엔트리가 앞으로 가도록 순서를 거꾸로 바꾼다.
  • 코드 라인 29~41에서 인수 warn_cpu_offline가 설정된 경우 현재 cpu가 offline된 cpu인 경우 한 번만 “IPI on offline CPU %d” 및 “IPI callback %pS sent to offline CPU”라는 경고 메시지를 출력하게 한다.
  • 코드 라인 43~46에서 등록되어 있는 함수들을 모두 호출하여 수행한다.
  • 코드 라인 54에서 현재의 모든 irq 작업들을 즉시 수행하게 한다.

 

ipi_cpu_stop()

arch/arm/kernel/smp.c

/*
 * ipi_cpu_stop - handle IPI from smp_send_stop()
 */
static void ipi_cpu_stop(unsigned int cpu)
{
        if (system_state == SYSTEM_BOOTING ||
            system_state == SYSTEM_RUNNING) {
                raw_spin_lock(&stop_lock);
                pr_crit("CPU%u: stopping\n", cpu);
                dump_stack();
                raw_spin_unlock(&stop_lock);
        }

        set_cpu_online(cpu, false);

        local_fiq_disable();
        local_irq_disable();

        while (1)
                cpu_relax();
}

현재  cpu가 부팅 중 또는 동작 중인 경우 “CPU%u: stopping” 메시지 출력 및 스택 덤프를 하고 해당 cpu의 irq, fiq를 모두 정지 시키고 offline 상태로 바꾼 후 정지(spin)한다.

  • 호출 방법: smp_send_stop() 함수를 사용하여 현재 cpu를 제외한 online cpu를 stop 시킨다.

 

irq_work_run()

kernel/irq_work.c

/*
 * hotplug calls this through:
 *  hotplug_cfd() -> flush_smp_call_function_queue()
 */
void irq_work_run(void)
{
        irq_work_run_list(this_cpu_ptr(&raised_list));
        irq_work_run_list(this_cpu_ptr(&lazy_list));
}       
EXPORT_SYMBOL_GPL(irq_work_run);

현재의 모든 irq 작업들을 즉시 수행하게 한다.

  • CONFIG_IRQ_WORK 커널 옵션을 사용하는 경우에만 동작한다.
  • 호출 방법: arch_irq_work_raise() 요청을 받은 경우 현재 자신의 cpu에서 모든 irq 작업들을 즉시 수행하게 한다.
  • 참고: ARM: 7872/1: Support arch_irq_work_raise() via self IPIs

 

  • 코드 라인 7에서 &raised_list에 있는 모든 irq 작업들을 수행하게 한다.
  • 코드 라인 8에서 &lazy_list에 있는 모든 irq 작업들을 수행하게 한다.

 

ipi_complete()

arch/arm/kernel/smp.c

static void ipi_complete(unsigned int cpu)
{
        complete(per_cpu(cpu_completion, cpu));
}

register_ipi_completion() 함수로 등록해 놓은 per-cpu cpu_completion 에서 wait_for_completion()등으로 대기하고 있는 스레드를 깨운다.

 

참고

 

ARM Exception Handler -1-

32bit ARM 프로세스에 exception이 발생하면 가상 벡터 주소(하이벡터=0xffff_0000, 로우벡터=0x0000_0000)의 8개의 각 exception을 담당하는 엔트리로 jump를 하게된다.

가장 첫 exception 엔트리가 reset 벡터가 있고 그 뒤로 나머지 7개의 exception 엔트리가 위치한다. 다음 8개의 exception 별로 호출되는 부분을 알아보자.

  • Reset
    • vector_rst 레이블로 jump 하고 swi 0을 호출하여 swi exception을 발생하게 한 후 이어서 vector_und: 레이블로 jump 한다.
  • Undefined
    • vector_und 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __und_usr, __und_svc 또는 __und_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc 위치로 복귀한다.
  • SWI
    • 벡터 테이블 바로 다음 페이지에 위치한 첫 엔트리에 vector_swi 레이블의 위치가 담겨있고 이 위치로 jump 한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.
  • Prefetch Abort
    • vector_pabt 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __pabt_usr, __pabt_svc 또는 __pabt_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.
  • Data Abort
    • vector_dabt 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __dabt_usr, __dabt_svc 또는 __dabt_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-8 위치로 복귀한다.
  • Address
    • vector_addrexcptn 레이블로 jump 한다. 이 주소는 현재 사용하지 않으므로 이 곳으로 진입하면 매우 위험하다.
  • IRQ
    • vector_irq 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __irq_usr, __irq_svc 또는 __irq_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.
  • FIQ
    • vector_fiq 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __fiq_usr, __fiq_svc 또는 __fiq_abt  레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.

 

다음 그림은 8개의 exception 벡터로 jump된 후 각각의 exception 핸들러를 호출하는 모습을 보여준다.

 

아래 그림은 exception 발생 시 모드의 변환과 주요 처리 내용을 보여준다.

  • swi에서 syscall 처리 후 전체 레지스터 중 r0는 제외합니다. (r0는 syscall에 대한 결과 값)

 

공통 핸들러

유저 모드에서 진입 시 레지스터 백업

usr_entry

arch/arm/kernel/entry-armv.S

/*
 * User mode handlers
 *
 * EABI note: sp_svc is always 64-bit aligned here, so should S_FRAME_SIZE
 */

#if defined(CONFIG_AEABI) && (__LINUX_ARM_ARCH__ >= 5) && (S_FRAME_SIZE & 7)
#error "sizeof(struct pt_regs) must be a multiple of 8"
#endif

        .macro  usr_entry, trace=1
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )       @ don't unwind the user space
        sub     sp, sp, #S_FRAME_SIZE
 ARM(   stmib   sp, {r1 - r12}  )
 THUMB( stmia   sp, {r0 - r12}  )

 ATRAP( mrc     p15, 0, r7, c1, c0, 0)
 ATRAP( ldr     r8, .LCcralign)

        ldmia   r0, {r3 - r5}
        add     r0, sp, #S_PC           @ here for interlock avoidance
        mov     r6, #-1                 @  ""  ""     ""        ""

        str     r3, [sp]                @ save the "real" r0 copied
                                        @ from the exception stack

 ATRAP( ldr     r8, [r8, #0])

        @
        @ We are now ready to fill in the remaining blanks on the stack:
        @
        @  r4 - lr_<exception>, already fixed up for correct return/restart
        @  r5 - spsr_<exception>
        @  r6 - orig_r0 (see pt_regs definition in ptrace.h)
        @
        @ Also, separately save sp_usr and lr_usr
        @
        stmia   r0, {r4 - r6}
 ARM(   stmdb   r0, {sp, lr}^                   )
 THUMB( store_user_sp_lr r0, r1, S_SP - S_PC    )

        @ Enable the alignment trap while in kernel mode
 ATRAP( teq     r8, r7)
 ATRAP( mcrne   p15, 0, r8, c1, c0, 0)

        @
        @ Clear FP to mark the first stack frame
        @
        zero_fp

        .if     \trace
#ifdef CONFIG_IRQSOFF_TRACER
        bl      trace_hardirqs_off
#endif
        ct_user_exit save = 0
        .endif
        .endm

유저 프로세스에 exception이 발생되어 해당 exception에 모드(fiq, irq, pabt, dabt, und)에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 다음과 같은 일들을 수행한다.

  • 스택에 18개의 레지스터를 보관할 수 있는 pt_regs 구조체 사이즈만큼 키운 후 레지스터들을 백업한다.
  • cr_alignment 값을 SCTLR에 저장한다.
  • 그 외 irq disable된 기간 및 컨텍스트  트래킹 등을 수행한다.

 

  • 코드 라인 14에서 스택을 pt_regs 구조체 사이즈만큼 키운다. (grows down)
    • S_FRAME_SIZE(72): 18개 레지스터를 담는 pt_regs 구조체 사이즈
      • S_R0(r0)~S_R10(r10), S_FP(r11), S_IP(r12), S_SP(r13), S_LR(r14), S_PC(r15), S_PSR(cpsr), S_OLD_R0(old r0) 순서
    • 커널 v.4.8.rc1에서 S_FRAME_SIZE -> PT_REGS_SIZE라는 이름으로 바뀐다.
  • 코드 라인 15에서 r1~r12 레지스터들의 정보를 pt_regs 구조체에 저장한다.
    • stmib (Store Memory Increment Before)를 사용하여 sp를 워드만큼 먼저 증가 시킨 후 레지스터들을 저장한다.
  • 코드 라인 18~19에서 SCTLR 값을 r7 레지스터에 읽어오고 cr_alignment 값을 가리키는 주소를 r8 레지스터에 대입한다.
    • ATRAP() 매크로는 CONFIG_ALIGNMENT_TRAP 커널 옵션이 사용되는 아키텍처에서 사용된다. (대부분의 arm에 적용됨)
    • 잠시 후에 cr_alignment 값을 읽어서 SCTLR에 저장하려 한다.
  • 코드 라인 21에서 mini 스택에서 백업한 값들을 r3~r5 레지스터에 로드한다.
  • 코드 라인 22에서 스택에 저장된 pc 주소 값를 r0에 대입한다.
  • 코드 라인 23에서 r6에 -1 값을 대입한다.
  • 코드 라인 25에서 r3(original r0) 레지스터 값을 pt_regs의 가장 첫 엔트리(r0)에 저장한다.
  • 코드 라인 28에서 cr_alignment 값을 읽어서 r8 레지스터에 저장한다.
  • 코드 라인 39에서 교정된 lr 값, psr 값, -1 값을 pt_regs 구조체의 pc, psr, old-rq에 순서대로 저장한다.
  • 코드 라인 40에서 sp와 lr 값을 pt_regs 구조체의 sp와 lr 위치에 그대로 저장한다.
  • 코드 라인 44~45에서 SCTLR 값을 읽은 r7 레지스터와 cr_alignment 값을 읽은 r8 레지스터 값을 비교해서 다른 경우에만 cr_alignment 값을 SCTLR에 저장한다.
    • SCTLR을 저장하는데 약 100 사이클 정도의 시간이 걸리므로 성능을 위해 변경 사항이 있는 경우에만 저장한다.
  • 코드 라인 50에서 fp 레지스터를 0으로 설정한다.
    • gcc 툴에서 tracing에 사용하는 fp 레지스터를 0으로 초기화한다.
    • exception 모드가 바뀌면 그 전 stack back trace 정보를 사용할 수 없으므로 이 시점에서 초기화한다.
  • 코드 라인 52~55에서 CONFIG_IRQSOFF_TRACER 커널 옵션이 사용되는 경우  얼마나 오랫동안 인터럽트가 disable 되었는지 그 주기를 알아보기 위한 트래킹 디버깅을 수행한다.
  • 코드 라인 56에서 컨텍스트 트래킹 디버깅이 enable된 경우에 수행된다.

 

다음 그림은 유저 모드에서 exception에 의해 해당 모드에 진입 시 레지스터들을 백업하는 usr_entry 매크로의 기능을 보여준다.

zero_fp 매크로

arch/arm/kernel/entry-header.S

        .macro  zero_fp
#ifdef CONFIG_FRAME_POINTER
        mov     fp, #0
#endif
        .endm

CONFIG_FRAME_POINTER 커널 옵션을 사용하는 경우 커널에서 문제가 발생했을 때 다양한 보고가 가능하도록 한다. 이 커널 옵션을 사용하지 않으면 보고되는 정보가 심각하게 제한된다.

 

trace_hardirqs_off()

kernel/trace/trace_irqsoff.c

void trace_hardirqs_off(void)
{
        if (!preempt_trace() && irq_trace())
                start_critical_timing(CALLER_ADDR0, CALLER_ADDR1);
}
EXPORT_SYMBOL(trace_hardirqs_off);

얼마나 오랫동안 인터럽트가 disable 되었는지 그 주기를 알아보기 위한 트래킹 디버깅을 수행한다.

 

ct_user_exit 매크로

arch/arm/kernel/entry-header.S

/*
 * Context tracking subsystem.  Used to instrument transitions
 * between user and kernel mode.
 */     
        .macro ct_user_exit, save = 1
#ifdef CONFIG_CONTEXT_TRACKING
        .if     \save
        stmdb   sp!, {r0-r3, ip, lr}
        bl      context_tracking_user_exit
        ldmia   sp!, {r0-r3, ip, lr}
        .else
        bl      context_tracking_user_exit
        .endif
#endif  
        .endm

CONFIG_CONTEXT_TRACKING 커널 옵션을 사용하면서 전역 static key 변수 context_tracking_enabled이 설정된 경우 컨텍스트 트래킹에 관련한  후처리 디버그 활동을 수행한다.

 

SVC 모드에서 진입 시 레지스터 백업

svc_entry

arch/arm/kernel/entry-armv.S

/*
 * SVC mode handlers
 */

#if defined(CONFIG_AEABI) && (__LINUX_ARM_ARCH__ >= 5)
#define SPFIX(code...) code
#else
#define SPFIX(code...)
#endif

        .macro  svc_entry, stack_hole=0, trace=1
 UNWIND(.fnstart                )
 UNWIND(.save {r0 - pc}         )
        sub     sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)
#ifdef CONFIG_THUMB2_KERNEL
 SPFIX( str     r0, [sp]        )       @ temporarily saved
 SPFIX( mov     r0, sp          )
 SPFIX( tst     r0, #4          )       @ test original stack alignment
 SPFIX( ldr     r0, [sp]        )       @ restored
#else
 SPFIX( tst     sp, #4          )
#endif
 SPFIX( subeq   sp, sp, #4      )
        stmia   sp, {r1 - r12}

        ldmia   r0, {r3 - r5}
        add     r7, sp, #S_SP - 4       @ here for interlock avoidance
        mov     r6, #-1                 @  ""  ""      ""       ""
        add     r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)
 SPFIX( addeq   r2, r2, #4      )
        str     r3, [sp, #-4]!          @ save the "real" r0 copied
                                        @ from the exception stack

        mov     r3, lr

        @
        @ We are now ready to fill in the remaining blanks on the stack:
        @
        @  r2 - sp_svc
        @  r3 - lr_svc
        @  r4 - lr_<exception>, already fixed up for correct return/restart
        @  r5 - spsr_<exception>
        @  r6 - orig_r0 (see pt_regs definition in ptrace.h)
        @
        stmia   r7, {r2 - r6}

        .if \trace
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        .endif
        .endm

커널 처리 중 exception이 발생되어 해당 exception 모드(fiq, irq, pabt, dabt, und)에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 다음과 같은 일들을 수행한다.

  • 스택에 18개의 레지스터를 보관할 수 있는 pt_regs 구조체 사이즈만큼 키운 후 레지스터들을 백업한다.
  • 그 외 irq disable된 기간 및 컨텍스트  트래킹 등을 수행한다.

 

  • 코드 라인 14에서 스택을 pt_regs 구조체 크기 + stack_hole – 4 만큼  확보한다.
    • stack_hole을 추가하는 이유
      • __und_svc() 레이블에서만 사용되는데 CONFIG_KPROBES 커널 옵션을 사용하고 kprobe를 이용하여 디버깅을 할 때 “stmdb sp!, {…}” 등의 문장에서 single step으로 디버깅을 하면 스택이 깨지는 문제가 발생하여 그러한 경우를 피하고자 64바이트(멀티 store 명령으로 최대 16개 레지스터를 저장할 수 있는 공간 크기)의 hole을 더 준비하였다.
      • 참고: ARM kprobes: don’t let a single-stepped stmdb corrupt the exception stack
    • -4를 하는 이유
      • r0를 제외한 스택 주소가 아래에서 설명하는 ARABI 규격으로 인해 정렬되어야 한다. 그 후 루틴의 마지막 즈음에서 정상적으로 스택을 4바이트 더 증가시킨다.
  • 코드 라인 21~23에서 AEABI(ARM Embedded Application Binary Interface) 규격에 맞게 스택을 사용 시 64비트(8 바이트) 정렬을 해야 한다. 따라서 기존 규격에서와 같이 32비트로만 정렬시켜 사용하는 경우 64비트 정렬을 하게 한다.
  • 코드 라인 24에서 r1에서 r12까지 레지스터를 모두 스택에 확보된 pt_regs에서 r1 위치부터 저장한다.
  • 코드 라인 26에서 기존 루틴에서 미니 스택에 저장해 놓은 old r0, 교정된 lr, spsr 값을 r3~r5 레지스터에 읽어 온다.
  • 코드 라인 27에서 r7 레지스터가 pt_regs의 sp 주소를 가리키게 한다.
  • 코드 라인 28에서 r6 레지스터에 -1을 대입한다.
  • 코드 라인 29~30에서 r2 레지스터가 stack_hole을 가리키게 한다. 만일 sp가 64비트 정렬을 한 경우라면 stack_hole 위치도 4 바이트만큼 위로 올린다. (stack_hole이 4바이트 커진다.)
  • 코드 라인 31에서 original r0를 읽어온 r3 레지스터의 내용을 pt_regs의 r0 주소에 저장한다. sp 주소는 4바이트 주소를 밑으로 이동시켜 정상적으로 sp가 pt_regs의 처음을 가리키게 한다.
  • 코드 라인 34~45에서 r3 레지스터에 lr_svc를 대입하고 sp_svc, lr_svc, lr_<exception>, spsr_<exception>, original r0 값이 담긴 r2~r6 레지스터 값을 pt_regs의 sp 주소부터 저장한다.
  • 코드 라인 47~50에서  CONFIG_TRACE_IRQFLAGS 커널 옵션이 사용되는 경우  얼마나 오랫동안 인터럽트가 disable 되었는지 그 주기를 알아보기 위한 트래킹 디버깅을 수행한다.

 

 

다음 그림과 같이 커널이 v4.9-rc1으로 버전업되면서 svc 모드에서 진입된 레지스터들을 백업하는데 기존 pt_regs를 사용하지 않고 svc_pt_regs를 사용한다.

레지스터 복구하고 서비스 모드로 복귀

svc_exit 매크로

arch/arm/kernel/entry-header.S

        .macro  svc_exit, rpsr, irq = 0
        .if     \irq != 0
        @ IRQs already off
#ifdef CONFIG_TRACE_IRQFLAGS
        @ The parent context IRQs must have been enabled to get here in
        @ the first place, so there's no point checking the PSR I bit.
        bl      trace_hardirqs_on
#endif
        .else   
        @ IRQs off again before pulling preserved data off the stack
        disable_irq_notrace
#ifdef CONFIG_TRACE_IRQFLAGS
        tst     \rpsr, #PSR_I_BIT
        bleq    trace_hardirqs_on
        tst     \rpsr, #PSR_I_BIT
        blne    trace_hardirqs_off
#endif
        .endif  
        msr     spsr_cxsf, \rpsr
#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)
        @ We must avoid clrex due to Cortex-A15 erratum #830321
        sub     r0, sp, #4                      @ uninhabited address
        strex   r1, r2, [r0]                    @ clear the exclusive monitor
#endif
        ldmia   sp, {r0 - pc}^                  @ load r0 - pc, cpsr
        .endm

exception 전의 svc 모드로 다시 복귀하기 위해 백업해두었던 레지스터들을 복구한다.

  • 코드 라인 2~8에서 irq=1로 설정되는 경우 fiq에 대한 latency 트래킹을 수행한다.
  • 코드 라인 9~18에서 irq=0으로 설정되는 경우 현재 cpu에 대해 irq를 mask하여 인터럽트가 진입하지 못하게 한다.
    • disable_irq_notrace 매크로에서 “cpsid   i”  명령을 수행한다.
  • 코드 라인 19에서 spsr에 \rpsr을 대입한다.
  • 코드 라인 20~24에서 clrex에 해당하는 코드를 수행한다. (erratum for Cortex-A15)
  • 코드 라인 25에서 스택으로부터 r0~pc까지 레지스터를 복구한다.
    • pc 위치에 이미 correction된 lr을 백업했었다.

 

허용하지 않은 모드에서 진입 시 레지스터 백업

inv_entry

arch/arm/kernel/entry-armv.S

/*
 * Invalid mode handlers
 */
        .macro  inv_entry, reason
        sub     sp, sp, #S_FRAME_SIZE
 ARM(   stmib   sp, {r1 - lr}           )
 THUMB( stmia   sp, {r0 - r12}          )
 THUMB( str     sp, [sp, #S_SP]         )
 THUMB( str     lr, [sp, #S_LR]         )
        mov     r1, #\reason
        .endm

스택에 레지스터들을 백업하기 위한 공간(struct pt_regs)을 확보하고  r1~r14(lr)까지 백업해둔다. r1 레지스터에는 reason 값을 대입한다.

  • pt_regs에 백업되지 않은 나머지 레지스터들은 common_invalid 레이블에서 백업을 계속 진행한다.

 

common_invalid

arch/arm/kernel/entry-armv.S

@
@ common_invalid - generic code for failed exception (re-entrant version of handlers)
@
common_invalid:
        zero_fp

        ldmia   r0, {r4 - r6}
        add     r0, sp, #S_PC           @ here for interlock avoidance
        mov     r7, #-1                 @  ""   ""    ""        ""
        str     r4, [sp]                @ save preserved r0
        stmia   r0, {r5 - r7}           @ lr_<exception>,
                                        @ cpsr_<exception>, "old_r0"

        mov     r0, sp
        b       bad_mode
ENDPROC(__und_invalid)

각 exception 핸들러들에서 허용하지 않은 모드에서 진입하여 실패처리를 위한 루틴이다.

  • 코드 라인 5에서 fp 레지스터에 0을 대입한다.
  • 코드 라인 7에서 스택에 백업해두었던 old r0, 교정된 lr, spsr 값을 r4~r6 레지스터로 읽어온다.
  • 코드 라인 8에서 스택에 백업해두었던 pt_regs 구조체 영역에서 pc 주소를 r0에 대입한다.
  • 코드 라인 9에서 r7에 -1을 대입한다.
  • 코드 라인 10에서 old r0 값을 읽어온 r4 레지스터 값을 pt_regs의 가장 첫 위치 r0에 저장한다.
  • 코드 라인 11에서 교정된 lr, spsr, -1 값을 담고 있는 r5~r7 레지스터 값을 스택의 pt_regs 위치 중 old r0, cpsr, r15(pc) 주소에 저장한다.
  • 코드 라인 14에서 스택 값을 r0에 대입한다.
  • 코드 라인 15에서 bad_mode 레이블로 이동하야 “Oops” 출력 및 panic() 처리한다.

 

bad_mode()

arch/arm/kernel/traps.c

/*
 * bad_mode handles the impossible case in the vectors.  If you see one of
 * these, then it's extremely serious, and could mean you have buggy hardware.
 * It never returns, and never tries to sync.  We hope that we can at least
 * dump out some state information...
 */
asmlinkage void bad_mode(struct pt_regs *regs, int reason)
{
        console_verbose();

        pr_crit("Bad mode in %s handler detected\n", handler[reason]);

        die("Oops - bad mode", regs, 0);
        local_irq_disable();
        panic("bad mode");
}

허용하지 않은 모드에서 진입 시 “Oops – bad mode”를 출력하고 panic 처리 한다.

 

kuser_cmpxchg_check

arch/arm/kernel/entry-armv.S

        .macro  kuser_cmpxchg_check
#if !defined(CONFIG_CPU_32v6K) && defined(CONFIG_KUSER_HELPERS) && \
    !defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)
#ifndef CONFIG_MMU
#warning "NPTL on non MMU needs fixing"
#else
        @ Make sure our user space atomic helper is restarted
        @ if it was interrupted in a critical region.  Here we
        @ perform a quick test inline since it should be false
        @ 99.9999% of the time.  The rest is done out of line.
        cmp     r4, #TASK_SIZE
        blhs    kuser_cmpxchg64_fixup
#endif
#endif
        .endm

Kernel-provided User Helper code 중 시스템이 POSIX syscall을 이용하여 cmpxchg를 이용하는 방식인 경우 커널 space에서 exception되어 이 루틴에 진입하게 되면 kuser_cmpchg64_fixup을 수행하고 온다.

  • r4에는 exception 처리 후 되돌아갈 주소(lr)이 담겨 있기 때문에 이 값을 TASK_SIZE와 비교하여 user space에서 진입하였는지 아니면 kernel space에서 진입하였는지 구분할 수 있다.
  • 참고: Kernel-provided User Helper | 문c

 

get_thread_info 매크로

include/asm/assembler.h

/*
 * Get current thread_info.
 */
        .macro  get_thread_info, rd
 ARM(   mov     \rd, sp, lsr #THREAD_SIZE_ORDER + PAGE_SHIFT    )
 THUMB( mov     \rd, sp                 )
 THUMB( lsr     \rd, \rd, #THREAD_SIZE_ORDER + PAGE_SHIFT       )
        mov     \rd, \rd, lsl #THREAD_SIZE_ORDER + PAGE_SHIFT
        .endm

현재 프로세스의 스택 하위에 위치한 thread_info 객체의 주소를 인수 rd에 반환한다.

 

IRQ 핸들러

arm 아키텍처는 nested 인터럽트를 처리할 수 있게 설계되어 있다. 그러나 현재 까지 arm 리눅스에서는 FIQ를 제외하고는 중첩하여 처리하지 못하게 하였다. 당연히 arm 리눅스가 아닌 펌웨어 레벨의 os에서는 사용자가 nested 인터럽트를 구현하여 사용할 수 있다.

 

  • CONFIG_MULTI_IRQ_HANDLER
    • Device Tree를 사용하는 경우 런타임에 해당 인터럽트 컨트롤러의 초기화 및 각 인트럽트에 대한 처리 방법 및 핸들러등을 준비해야 한다.
    • 전역 handle_arch_irq 변수는 set_handle_irq() 함수에 의해 핸들러 주소가 설정된다.
      • rpi2: Device Tree용 armctrl_of_init() 초기화 함수에서 두 개의 인터럽트 컨트롤러 초기화 함수를 호출하는데 그 중 부모 인터럽트 콘트롤러를 초기화할 때 핸들러로 bcm2836_arm_irqchip_handle_irq() 함수가 설정된다.

 

__irq_usr

arch/arm/kernel/entry-armv.S

        .align  5
__irq_usr:
        usr_entry
        kuser_cmpxchg_check
        irq_handler
        get_thread_info tsk
        mov     why, #0
        b       ret_to_user_from_irq
 UNWIND(.fnend          )
ENDPROC(__irq_usr)

유저 태스크 처리 중 인터럽트가 발생되어 irq exception 모드에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 관련 인터럽트 번호에 등록되어 있는 1개 이상의 ISR(Interrupt Service Routing)을 호출한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다.
  • 코드 라인 4에서 atomic 연산을 지원하지 못하는 아키텍처에서 atomic 하게 처리해야 하는 구간에서 인터럽트를 맞이하고 복귀할 때 그 atomic operation 구간의 시작부분으로 다시 돌아가도록 pt_regs의 pc를 조작한다.
  • 코드 라인 5에서 관련 인터럽트 번호에 등록되어 있는 1개 이상의 ISR(Interrupt Service Routing)을 호출한다
  • 코드 라인 6에서 tsk 레지스터에 thread_info 객체의 주소를 알아온다.
  • 코드 라인 7~8에서  스택에 백업해둔 레지스터들을 다시 불러 읽은 후 user 모드로 복귀한다.

 

__irq_svc

arch/arm/kernel/entry-armv.S

        .align  5
__irq_svc:
        svc_entry
        irq_handler

#ifdef CONFIG_PREEMPT
        get_thread_info tsk
        ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count
        ldr     r0, [tsk, #TI_FLAGS]            @ get flags
        teq     r8, #0                          @ if preempt count != 0
        movne   r0, #0                          @ force flags to 0
        tst     r0, #_TIF_NEED_RESCHED
        blne    svc_preempt
#endif

        svc_exit r5, irq = 1                    @ return from exception
 UNWIND(.fnend          )
ENDPROC(__irq_svc)

커널 처리 중 인터럽트가 발생되어 irq exception 모드에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 관련 인터럽트 번호에 등록되어 있는 1개 이상의 처리 핸들러를 호출한다.

  • 코드 라인 3에서 스택에 18개의 레지스터를 보관할 수 있는 pt_regs(svc_pt_regs) 구조체 사이즈만큼 키운 후 레지스터들을 백업한다.
  • 코드 라인 4에서 관련 인터럽트 번호에 등록되어 있는 1개 이상의 ISR(Interrupt Service Routing)을 호출한다
  • 코드 라인 6~14에서 preempt 커널에서 현재 프로세스 컨텍스트가 preempt 허용(preempt 카운트가 0) 상태인 경우에 한해 리스케쥴(_TIF_NEED_RESCHED) 요청이 있는 경우 리스케쥴을 위해 svc_preempt 레이블로 이동한다.
    • thread_info->preempt가 0인 경우 preempt 가능한 상태이다.
    • flags에 _TIF_NEED_RESCHED 설정된 경우 리스케쥴 요청이 들어온 경우이다.
  • 코드 라인 16에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 svc 모드로 복귀한다.

 

svc_preempt 레이블

arch/arm/kernel/entry-armv.S

#ifdef CONFIG_PREEMPT
svc_preempt:
        mov     r8, lr
1:      bl      preempt_schedule_irq            @ irq en/disable is done inside
        ldr     r0, [tsk, #TI_FLAGS]            @ get new tasks TI_FLAGS
        tst     r0, #_TIF_NEED_RESCHED
        reteq   r8                              @ go again
        b       1b
#endif

더 이상의 리스케쥴 요청이 없을 때까지 preemption 리스케쥴을 수행한다.

  • 코드 라인 3에서 r8 레지스터에 복귀할 주소를 담고 있는 lr 레지스터를 백업해둔다.
  • 코드 라인 4에서 현 태스크의 preemption을 포함한 리스케쥴을 수행한다.
    • 리스케쥴되어 현재 태스크보다 더 우선 순위 높은 태스크가 실행되는 경우 현재의 태스크는 잠든다. 그 후 깨어난 후 계속 진행한다.
  • 코드 라인 5~8에서 현재 프로세서 컨텍스트에 리스케쥴 요청이 있으면 다시 1: 레이블로 이동하여 계속 처리하고, 리스케쥴 요청이 없으면 루틴을 끝마치고 복귀한다.

 

irq_handler 매크로

arch/arm/kernel/entry-armv.S

/*
 * Interrupt handling.
 */
        .macro  irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
        ldr     r1, =handle_arch_irq
        mov     r0, sp
        adr     lr, BSYM(9997f)
        ldr     pc, [r1]
#else
        arch_irq_handler_default
#endif
9997:
        .endm

아키텍처에 따른 irq 핸들러를 호출한다.

  • CONFIG_MULTI_IRQ_HANDLER 커널 옵션을 사용 여부에 따라
    • 설정한 경우 커널이 여러 개의 머신에 해당하는 IRQ 핸들러 함수를 같이 컴파일하고 실제 커널 부팅 시 IRQ 관련하여 선택한 호출 함수를 handle_arch_irq에 저장하여야 한다.
    • 설정하지 않은 경우 현재 커널이 IRQ 처리 방식을 고정한 경우로 컴파일 시 결정된 arch_irq_handler_default 매크로를 호출한다.

 

arch_irq_handler_default 매크로(for RPI2)

mach-bcm2709/include/mach/entry-macro.S

/*
 * Interrupt handling.  Preserves r7, r8, r9
 */
        .macro  arch_irq_handler_default
1:      get_irqnr_and_base r0, r2, r6, lr
        .endm

 

get_irqnr_and_base 매크로(for RPI2)

 

IPI(Inter Process Interrupt) 처리

mach-bcm2709/include/mach/entry-macro.S

        .macro  get_irqnr_and_base, irqnr, irqstat, base, tmp

        /* get core number */
        mrc     p15, 0, \base, c0, c0, 5
        ubfx    \base, \base, #0, #2

        /* get core's local interrupt controller */
        ldr     \irqstat, = __io_address(ARM_LOCAL_IRQ_PENDING0)        @ local interrupt source
        add     \irqstat, \irqstat, \base, lsl #2
        ldr     \tmp, [\irqstat]
#ifdef CONFIG_SMP
        /* test for mailbox0 (IPI) interrupt */
        tst     \tmp, #0x10
        beq     1030f

        /* get core's mailbox interrupt control */
        ldr     \irqstat, = __io_address(ARM_LOCAL_MAILBOX0_CLR0)       @ mbox_clr
        add     \irqstat, \irqstat, \base, lsl #4
        ldr     \tmp, [\irqstat]
        clz     \tmp, \tmp
        rsb     \irqnr, \tmp, #31
        mov     \tmp, #1
        lsl     \tmp, \irqnr
        str     \tmp, [\irqstat]  @ clear interrupt source
        dsb
        mov     r1, sp
        adr     lr, BSYM(1b)
        b       do_IPI
#endif

 

pending 레지스터를 읽어 IPI 처리 요청이 있는 경우 mailbox0를 읽어 인터럽트 소스를 알아와서 모든 설정된 IPI 처리를 한다. 처리 순서는 높은 IPI 번호부터 처리한다. (irqnr=r0, irqstat=r2, base=r6, tmp=lr)

  • 코드 라인 4~10에서 cpu 번호를 구한 후 그 cpu에 해당하는 pending 레지스터 값을 읽어와서 \tmp에 대입한다.
    • 예) cpu #3에 대한 레지스터 가상 주소: ARM_LOCAL_IRQ_PENDING0(0xf400_0060) + cpu(3) * 4 = 0xf400_006c
  • 코드 라인 13~14에서 IPI(mailbox0) 인터럽트 요청이 아닌 경우 1030 레이블로 이동한다.
    • pending 레지스터의 0x10 비트: mailbox0
  • 코드 라인 17~19에서 cpu에 해당하는 mailbox0 값을 읽어와서 \tmp에 대입한다.
  • 코드 라인 20~21에서 가장 좌측에 설정된 인터럽트 비트를 \irqnr에 대입한다. (IPI는 역순으로 우선 처리된다.)
  • 코드 라인 22~24에서 클리어하고자 하는 IPI 인터럽트 비트만 설정하여 읽어왔던 mailbox0에 저장한다.
    • clz: 좌측부터 0으로 시작하는 비트 수
    • mailbox는 str 명령을 통하여 값이 저장되는 것이 아니라 값에 해당하는 인터럽트 소스들을 클리어 한다.
  • 코드 라인 26~28에서 두 번째 인수 r1에 스택에 있는 pt_regs를 대입하고, 복귀 주소로 arch_irq_handler_default 매크로 안에 있는 get_irqnr_and_base 호출 위치를 설정한 다음 IPI 처리를 위해 do_IPI() 함수를 호출한다.
    • IPI 처리가 다 완료될 때 까지 루프를 돈다.
    • 참고: do_IPI() | 문c

 

ARM(GPU) 및 ARM 인터럽트 처리
1030:
        /* check gpu interrupt */
        tst     \tmp, #0x100
        beq     1040f

        ldr     \base, =IO_ADDRESS(ARMCTRL_IC_BASE)
        /* get masked status */
        ldr     \irqstat, [\base, #(ARM_IRQ_PEND0 - ARMCTRL_IC_BASE)]
        mov     \irqnr, #(ARM_IRQ0_BASE + 31)
        and     \tmp, \irqstat, #0x300           @ save bits 8 and 9
        /* clear bits 8 and 9, and test */
        bics    \irqstat, \irqstat, #0x300
        bne     1010f

        tst     \tmp, #0x100
        ldrne   \irqstat, [\base, #(ARM_IRQ_PEND1 - ARMCTRL_IC_BASE)]
        movne   \irqnr, #(ARM_IRQ1_BASE + 31)
        @ Mask out the interrupts also present in PEND0 - see SW-5809
        bicne   \irqstat, #((1<<7) | (1<<9) | (1<<10))
        bicne   \irqstat, #((1<<18) | (1<<19))
        bne     1010f

        tst     \tmp, #0x200
        ldrne   \irqstat, [\base, #(ARM_IRQ_PEND2 - ARMCTRL_IC_BASE)]
        movne   \irqnr, #(ARM_IRQ2_BASE + 31)
        @ Mask out the interrupts also present in PEND0 - see SW-5809
        bicne   \irqstat, #((1<<21) | (1<<22) | (1<<23) | (1<<24) | (1<<25))
        bicne   \irqstat, #((1<<30))
        beq     1020f
1010:
        @ For non-zero x, LSB(x) = 31 - CLZ(x^(x-1))
        sub     \tmp, \irqstat, #1
        eor     \irqstat, \irqstat, \tmp
        clz     \tmp, \irqstat
        sub     \irqnr, \tmp
        b       1050f

 

pending #0 레지스터를 읽어 ARM(GPU) #1, ARM(GPU)#2, ARM #0 인터럽트 순서대로 발생한 인터럽트 소스의 ISR을 수행한다.

  • 예) ARM(GPU) #1에 배치된 arm dma #0 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x100, pending 1 레지스터 값=0x1_0000 (bit16), 최종 인터럽트 번호 \irqnr=16
  • 예) ARM(GPU) #2에 배치한 arm gpio 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x200, pending 2 레지스터 값=0x10_0000 (bit20), 최종 인터럽트 번호 \irqnr=52
  • 예) ARM #0에 배치한 arm uart 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x8_0000 (bit19), 최종 인터럽트 번호 \irqnr=83

 

  • 코드 라인 3~4에서 ARM(GPU) 인터럽트 처리 요청이 없으면 1040 레이블로 이동한다.
  • 코드 라인 6~8에서 pending #0 레지스터 값을 읽어 /irqstat에 저장한다.
    • ARM 인터럽트 컨트롤러 가상 주소를 구해 \base에 저장한다.
      • =IO_ADDRESS(0x3f00_0000(기본 주소) + 0xb000(peri offset) + 0x200(인터럽트 컨트롤러 offset) = 0xf300_b200
  • 코드 라인 9에서 ARM #0이 담당하는 IRQ 끝 번호 값(95)을 \irqnr에 대입한다.
  • 코드 라인 10~13에서 pending #0 레지스터에서 읽었던 /irqstat에서 ARM #1(GPU #1) 또는 ARM #2(GPU #2)에 해당하는 비트를 클리어한다. 클리어 전에도 설정된 적 없으면 ARM #0 인터럽트를 처리하기 위해 1010 레이블로 이동한다.
  • 코드 라인 15~21에서 ARM #1(GPU #1)에 해당하는 경우 pending #1 레지스터 값을 읽어 /irqstat에 저장하되 VideoCore에서 사용하는 인터럽트에 해당하는 7, 9, 10, 18, 19번 비트들을 제거한다. IRQ #1(GPU #1)의 인터럽트 끝 번호(31)를 \irqnr에 대입한 후 해당 인터럽트 수행을 위해 1010 레이블로 이동한다.
  • 코드 라인 23~29에서 ARM #2(GPU #2)에 해당하는 경우 pending #2 레지스터 값을 읽어 /irqstat에 저장하되 VideoCore에서 사용하는 인터럽트에 해당하는21~25, 30번 비트들을 제거한다. IRQ #2(GPU #2)의 인터럽트 끝 번호(63)를 \irqnr에 대입하고 해당 인터럽트 수행을 위해 계속 아래 1010 레이블로 진행한다.
  • 코드 라인 32~36에서 처리할 인터럽트 끝 번호(irq0=95, irq1=31, ir12=63, local irq=127)에서 \irqstat의 첫 인터럽트 비트에 해당하는 번호를 뺀 후 해당 인터럽트 수행을 위해 1050 레이블로 이동한다.

 

Local Interrupt 처리
1040:
        cmp     \tmp, #0
        beq     1020f

        /* handle local (e.g. timer) interrupts */
        @ For non-zero x, LSB(x) = 31 - CLZ(x^(x-1))
        mov     \irqnr, #(ARM_IRQ_LOCAL_BASE + 31)
        sub     \irqstat, \tmp, #1
        eor     \irqstat, \irqstat, \tmp
        clz     \tmp, \irqstat
        sub     \irqnr, \tmp
1050:
        mov     r1, sp
        @
        @ routine called with r0 = irq number, r1 = struct pt_regs *
        @
        adr     lr, BSYM(1b)
        b       asm_do_IRQ

1020:   @ EQ will be set if no irqs pending
        .endm

local ARM 인터럽트 처리요청이 있는 경우 발생한 인터럽트 소스의 ISR을 수행한다.

  • 예) Local ARM 인터럽트에 배치한 arm timer 인터럽트가 요청된 경우
    • local interrupt controller 레지스터 값=0x8, 최종 인터럽트 번호 \irqnr=99

 

  • 코드 라인 2~3에서 pending 레지스터 값이 0인 경우 더이상 처리할 인터럽트가 없으므로 종료한다.
  • 코드 라인 7~11에서 Local ARM이 처리하는 끝 번호(127) 값을 \irqnr에 대입한다.  끝 번호(127)에서 /tmp 인터럽트 비트들 중 가장 마지막 비트에 해당하는 번호를  뺀다
  • 코드 라인 13~18에서 r1 레지스터에 pt_regs 주소를 대입하고, lr에 이 매크로(get_irqnr_and_base)를 호출하는 곳을 담고 ISR을 호출한다.

 

 

ret_to_user_from_irq 및 no_work_pending 레이블

arch/arm/kernel/entry-common.S

ENTRY(ret_to_user_from_irq)
        ldr     r1, [tsk, #TI_FLAGS]
        tst     r1, #_TIF_WORK_MASK
        bne     work_pending
no_work_pending:
        asm_trace_hardirqs_on

        /* perform architecture specific actions before user return */
        arch_ret_to_user r1, lr
        ct_user_enter save = 0

        restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)

irq 처리를 마치고 복귀하기 전에 pending된 작업이 있으면 수행한 후 백업해 두었던 레지스터들을 다시 읽어 들인 후 복귀한다.

  • 코드 라인 2~4에서 thread_info->flags를 검사하여 _TIF_WORK_MASK에 속한 플래그가 있는 경우 work_pending 레이블로 이동한다.
    • _TIF_WORK_MASK
      • _TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME | _TIF_UPROBE
  • 코드 라인 9에서 user process로 다시 복귀 전에 처리할 아키텍처 specific한 일이 있는 경우 수행한다.
    • rpi2: 없음
  • 코드 라인 10에서 CONFIG_CONTEXT_TRACKING 커널 옵션을 사용한 context 트래킹 디버그가 필요한 경우 수행한다.
  • 코드 라인 12에서 백업해 두었던 레지스터들을 읽어들이며 다시 user space로 복귀한다.

 

지연 작업 처리

fast_work_pending 및 work_pending 레이블

arch/arm/kernel/entry-common.S

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
fast_work_pending:
        str     r0, [sp, #S_R0+S_OFF]!          @ returned r0
work_pending:
        mov     r0, sp                          @ 'regs'
        mov     r2, why                         @ 'syscall'
        bl      do_work_pending
        cmp     r0, #0
        beq     no_work_pending
        movlt   scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
        ldmia   sp, {r0 - r6}                   @ have to reload r0 - r6
        b       local_restart                   @ ... and off we go

지연된 작업이 있는 경우 완료 시킨 후 no_work_pending 레이블로 이동하는데 pending 시그널을 처리하다 restart 에러가 발생한 경우 local_restart 레이블로 이동한다.

  • 코드 라인 5에서 sp를 레지스터들을 백업해 두었던 pt_regs의 r0에 S_OFF(8)을 더한 위치로 변경하고 r0를 그 위치에 저장한다.
    • syscall 호출 시 5 번째 인수와 6 번째 인수를 스택에 저장한다.
  • 코드 라인 7~9에서 do_work_pending() 함수를 호출하여 지연된 작업들을 처리한다.
    • 첫 번째 인수에 pt_regs 위치를 대입한다.
    • 두 번째 인수는 플래그가 저장되어 있다.
    • 세 번째 인수는 syscall 테이블 index를 대입한다.
  • 코드 라인 10~11에서 지연된 작업이 없었던 경우라면 no_work_pending 레이블로 이동한다.
  • 코드 라인 12에서 결과가 음수인 경우 scno에 sys_restart_syscall() 함수를 처리하기 위한 syscall 인덱스 번호를 대입한다.
    • EABI(Embedded Application Binary Interface)를 사용하는 경우 0x0 – 0x0을 적용하여 0이된다.
    • OABI(Old Application Binary Interface)를 사용하는 경우 0x90000 – 0x90000을 적용하여 0이된다.
  • 코드 라인 13~14에서 스택으로부터 r0~r6 레지스터에 로드한 후 local_restart 레이블로 이동한다.

 

do_work_pending()

arch/arm/kernel/signal.c

asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
        do {
                if (likely(thread_flags & _TIF_NEED_RESCHED)) {
                        schedule();
                } else {
                        if (unlikely(!user_mode(regs)))
                                return 0;
                        local_irq_enable();     
                        if (thread_flags & _TIF_SIGPENDING) {
                                int restart = do_signal(regs, syscall);
                                if (unlikely(restart)) {
                                        /*
                                         * Restart without handlers.
                                         * Deal with it without leaving
                                         * the kernel space.
                                         */
                                        return restart;
                                }
                                syscall = 0;
                        } else if (thread_flags & _TIF_UPROBE) {
                                uprobe_notify_resume(regs);
                        } else {
                                clear_thread_flag(TIF_NOTIFY_RESUME);
                                tracehook_notify_resume(regs);
                        }
                }
                local_irq_disable();
                thread_flags = current_thread_info()->flags;
        } while (thread_flags & _TIF_WORK_MASK);
        return 0;
}

지연된 작업이 있는 경우 완료될 때까지 처리한다. 정상 처리되면 0을 반환하는데 만일 pending 시그널을 처리하다 에러가 발생한 경우 restart 값을 반환한다.

  • 코드 라인 5~6에서 현재 태스크의 플래그들 중 리스케쥴 요청(_TIF_NEED_RESCHED 플래그)이 있는 경우 리스케쥴한다.
    •  인터럽트 처리를 빠져나가기 전에 이 코드가 실제 preemption 요청을 처리하는 곳이다.
  • 코드 라인 8~9에서 유저 모드 진입이 아닌 경우 그냥 빠져나간다.
  • 코드 라인 11~21에서 pending 시그널(_TIF_SIGPENDING 플래그) 이 있는 경우 시그널을 처리한다. 만일 restart 응답을 받은 경우 restart 값으로 함수를 빠져나간다.
  • 코드 라인 22~23에서 uprobe break point가 hit(_TIF_UPROBE 플래그)되어 진입된 경우이었던 경우 resume 관련 처리를 수행한다.
  • 코드 라인 24~27에서 4개의 pending 관련 비트들 중 마지막 플래그 TIF_NOTIFY_RESUME를 클리어한다. 이 플래그는 user로 되돌아가기 전에 호출할 콜백함수들을 수행하게 한다.
    • task_work_add() 함수에서 인수 notify가 설정되어 요청한 경우 task->task_works 리스트에 추가된 콜백 함수들을 수행한다.
  • 코드 라인 30~31에서 현재 프로세서의 플래그에 pending 작업이 존재한다고 표시된 경우 계속 루프를 돈다.

 

restore_user_regs 매크로

arch/arm/kernel/entry-header.S

        .macro  restore_user_regs, fast = 0, offset = 0
        mov     r2, sp
        ldr     r1, [r2, #\offset + S_PSR]      @ get calling cpsr
        ldr     lr, [r2, #\offset + S_PC]!      @ get pc
        msr     spsr_cxsf, r1                   @ save in spsr_svc
#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)
        @ We must avoid clrex due to Cortex-A15 erratum #830321
        strex   r1, r2, [r2]                    @ clear the exclusive monitor
#endif
        .if     \fast
        ldmdb   r2, {r1 - lr}^                  @ get calling r1 - lr
        .else
        ldmdb   r2, {r0 - lr}^                  @ get calling r0 - lr
        .endif
        mov     r0, r0                          @ ARMv5T and earlier require a nop
                                                @ after ldm {}^
        add     sp, sp, #\offset + S_FRAME_SIZE
        movs    pc, lr                          @ return & move spsr_svc into cpsr
        .endm

백업해 두었던 레지스터들을 읽어들인 후 다시 user space로 복귀한다.

  • 코드 라인 2~5에서 스택에 백업해둔 pt_regs의 cpsr을 r1 레지스터를 통해 spsr 레지스터에 저장하고, pc 값을 lr 레지스터에 대입한다.
  • 코드 라인 6~9에서 Cortex-A15 아키텍처에서 strex를 사용하여 clrex를 대신하였다.
  • 코드 라인 10~14에서 fast 요청이 있는 경우 스택에 백업해 둔 pt_regs의 r1~lr 까지의 레지스터를 읽어오고 fast 요청이 아닌 경우 r0 레지스터를 포함해서 불러온다.
  • 코드 라인 15에서 ARMv5T 및 그 이전 arm 아키텍처에서 multiple load 명령을 사용 후 nop을 사용해야 한다.
  • 코드 라인 17~18에서 스택에서 pt_regs를 제외시킨 후 user space로 복귀한다.

 

preempt_schedule_irq()

kernel/sched/core.c

/*
 * this is the entry point to schedule() from kernel preemption
 * off of irq context.
 * Note, that this is called and return with irqs disabled. This will
 * protect us against recursive calling from irq.
 */
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
        enum ctx_state prev_state;

        /* Catch callers which need to be fixed */
        BUG_ON(preempt_count() || !irqs_disabled());

        prev_state = exception_enter();

        do {
                __preempt_count_add(PREEMPT_ACTIVE);
                local_irq_enable();
                __schedule(); 
                local_irq_disable();
                __preempt_count_sub(PREEMPT_ACTIVE);

                /* 
                 * Check again in case we missed a preemption opportunity
                 * between schedule and now.
                 */ 
                barrier();
        } while (need_resched());

        exception_exit(prev_state);
}

리스케쥴 요청이 있는 동안 인터럽트는 허용하되 preemption은 허용되지 않게 한 후 스케쥴한다.

  • 코드 라인 14에서 리스케쥴 전에 context 트래킹 디버깅을 위한 선 처리 작업을 한다.
  • 코드 라인 17~21에서 인터럽트는 허용하되 preemption은 허용되지 active 구간을 증가시킨다. 않게 한 후 스케쥴한다. 완료 후 다시 인터럽트를 막고 preemption이 허용되도록 active 구간을 감소시킨다.
  • 코드 라인 28에서 리스케쥴 요청이 없을 때까지 반복한다.
  • 코드 라인 30에서리스케쥴이 완료되었으므로 context 트래킹 디버깅을 위한 후 처리 작업을 한다.

 

CONFIG_MULTI_IRQ_HANDLER

rpi2가 device tree를 사용하는 경우 다음 함수에서 irq 핸들러가 설정된다.

  • “brcm,bcm2836-l1-intc” 인터럽트 컨트롤러 – bcm2836_arm_irqchip_l1_intc_of_init() 함수에서 irq 핸들러를 설정한다.

bcm2836_arm_irqchip_handle_irq()

drivers/irqchip/irq-bcm2836.c

static void
__exception_irq_entry bcm2836_arm_irqchip_handle_irq(struct pt_regs *regs)
{
        int cpu = smp_processor_id();
        u32 stat;

        stat = readl_relaxed(intc.base + LOCAL_IRQ_PENDING0 + 4 * cpu);
        if (stat & 0x10) {
#ifdef CONFIG_SMP
                void __iomem *mailbox0 = (intc.base +
                                          LOCAL_MAILBOX0_CLR0 + 16 * cpu);
                u32 mbox_val = readl(mailbox0);
                u32 ipi = ffs(mbox_val) - 1;

                writel(1 << ipi, mailbox0);
                dsb();
                handle_IPI(ipi, regs);
#endif
        } else if (stat) {
                u32 hwirq = ffs(stat) - 1;

                handle_IRQ(irq_linear_revmap(intc.domain, hwirq), regs);
        }
}

pending 레지스터들을 조사하여 handle_IPI() 또는 handle_IRQ() 함수를 호출한다.

  • 코드 라인 7~8에서 현재 cpu에 대한 local pending 레지스터를 읽어서 bit4를 통해 mailbox가 수신되었는지 확인한다. 만일 수신된 경우
  • 코드 라인 9~17에서 수신된 mailbox에서 가장 먼저(msb) 처리할 IPI 번호를 읽어 그 비트를 클리어하고 handle_IPI() 함수를 호출하여 IPI 처리를 수행하게 한다.
  • 코드 라인 18~22에서 hwirq  -> irq 번호로 reversemap을 사용하여 transalation하여 얻은 번호로 handle_irq()를 호출한다.

 

FIQ 핸들러

 

__fiq_usr

arch/arm/kernel/entry-armv.S

        .align  5
__fiq_usr:
        usr_entry trace=0
        kuser_cmpxchg_check
        mov     r0, sp                          @ struct pt_regs *regs
        bl      handle_fiq_as_nmi
        get_thread_info tsk
        restore_user_regs fast = 0, offset = 0
 UNWIND(.fnend          )
ENDPROC(__fiq_usr)

유저모드에서 fiq 인터럽트가 발생되어 fiq exception 모드에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 관련 fiq 용도로 핸들러를 호출한다.

user 모드에서 fiq exception을 만나 진입하게 되면 해당 ISR(Interrupt Service Routing)을 수행한다. (ARM에서는 기본적으로 등록되어 있지 않다)

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다. 속도를 중시하므로 trace를 제한한다.
  • 코드 라인 4에서 atomic 연산을 지원하지 못하는 아키텍처에서 atomic 하게 처리해야 하는 구간에서 인터럽트를 맞이하고 복귀할 때 그 atomic operation 구간의 시작부분으로 다시 돌아가도록 pt_regs의 pc를 조작한다.
  • 코드 라인 5~6에서 fiq 관련 등록된 ISR을 수행한다.
  • 코드 라인 7에서 tsk 레지스터에 thread_info 객체의 주소를 알아온다.
  • 코드 라인 8에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 user 모드로 복귀한다.

 

handle_fiq_as_nmi()

arch/arm/kernel/traps.c

/*
 * Handle FIQ similarly to NMI on x86 systems.
 *
 * The runtime environment for NMIs is extremely restrictive
 * (NMIs can pre-empt critical sections meaning almost all locking is
 * forbidden) meaning this default FIQ handling must only be used in
 * circumstances where non-maskability improves robustness, such as
 * watchdog or debug logic.
 *
 * This handler is not appropriate for general purpose use in drivers
 * platform code and can be overrideen using set_fiq_handler.
 */
asmlinkage void __exception_irq_entry handle_fiq_as_nmi(struct pt_regs *regs)
{
        struct pt_regs *old_regs = set_irq_regs(regs);

        nmi_enter();

        /* nop. FIQ handlers for special arch/arm features can be added here. */
 
        nmi_exit();

        set_irq_regs(old_regs);
}

rpi2에서는 hard irq 처리를 위한 루틴이 비어있다. 필요한 경우 임베디드 개발자가 추가하게 되어 있다.

 

nmi_enter()

include/linux/hardirq.h

#define nmi_enter()                                             \
        do {                                                    \
                lockdep_off();                                  \
                ftrace_nmi_enter();                             \
                BUG_ON(in_nmi());                               \
                preempt_count_add(NMI_OFFSET + HARDIRQ_OFFSET); \
                rcu_nmi_enter();                                \
                trace_hardirq_enter();                          \
        } while (0)

nmi 처리 시작 진입점에서 preempt count를 증가시켜 preemption되지 않도록 하고 nmi가 진행 중임을 rcu가 알 수 있도록 한다.

  • 코드 라인 6에서 nmi 및 hard irq에 대한 preempt 카운터를 증가시킨다. (nmi도 nest 가능하다)
  • 코드 라인 7에서 nmi 처리 중인 것을 RCU도 알아야하기 때문에 &rcu_dynticks->dynticks를 1로 설정한다.

 

nmi_exit()

include/linux/hardirq.h

#define nmi_exit()                                              \
        do {                                                    \
                trace_hardirq_exit();                           \
                rcu_nmi_exit();                                 \
                BUG_ON(!in_nmi());                              \
                preempt_count_sub(NMI_OFFSET + HARDIRQ_OFFSET); \
                ftrace_nmi_exit();                              \
                lockdep_on();                                   \
        } while (0)

nmi 처리 끝났음을 알리기 위해 rcu도 알 수 있도록 설정하고, preempt count를 감소시킨다.

  • 코드 라인 4에서 nmi 처리 완료된 것을 RCU도 알아야하기 때문에 &rcu_dynticks->dynticks를 0으로 클리어한다.
  • 코드 라인 6에서 nmi 및 hard irq에 대한 preempt 카운터를 감소시킨다. (nmi도 nest 가능하다)

 

set_irq_regs()

include/asm-generic/irq_regs.h

static inline struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{
        struct pt_regs *old_regs;

        old_regs = __this_cpu_read(__irq_regs);
        __this_cpu_write(__irq_regs, new_regs);
        return old_regs;
}

pt_regs 레지스터들을 백업하고 기존 레지스터들을 반환한다.

 

__fiq_svc

arch/arm/kernel/entry-armv.S

        .align  5
__fiq_svc:
        svc_entry trace=0
        mov     r0, sp                          @ struct pt_regs *regs
        bl      handle_fiq_as_nmi
        svc_exit_via_fiq
 UNWIND(.fnend          )
ENDPROC(__fiq_svc)

커널 처리 중 fiq 인터럽트가 발생되어 fiq exception 모드에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 해당 ISR(Interrupt Service Routing)을 수행한다. (ARM에서는 기본적으로 등록되어 있지 않다)

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다. 속도를 중시하므로 trace를 제한한다.
  • 코드 라인 4~5에서 fiq 관련 등록된 ISR을 수행한다. (ARM에서는 기본적으로 등록되어 있지 않다)
  • 코드 라인 6에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 해당 모드로 복귀한다.

 

svc_exit_via_fiq 매크로

arch/arm/kernel/entry-header.S

.       @
        @ svc_exit_via_fiq - like svc_exit but switches to FIQ mode before exit
        @
        @ This macro acts in a similar manner to svc_exit but switches to FIQ
        @ mode to restore the final part of the register state.
        @
        @ We cannot use the normal svc_exit procedure because that would
        @ clobber spsr_svc (FIQ could be delivered during the first few
        @ instructions of vector_swi meaning its contents have not been
        @ saved anywhere).
        @
        @ Note that, unlike svc_exit, this macro also does not allow a caller
        @ supplied rpsr. This is because the FIQ exceptions are not re-entrant
        @ and the handlers cannot call into the scheduler (meaning the value
        @ on the stack remains correct).
        @
        .macro  svc_exit_via_fiq
        mov     r0, sp
        ldmib   r0, {r1 - r14}  @ abort is deadly from here onward (it will
                                @ clobber state restored below)
        msr     cpsr_c, #FIQ_MODE | PSR_I_BIT | PSR_F_BIT
        add     r8, r0, #S_PC
        ldr     r9, [r0, #S_PSR]
        msr     spsr_cxsf, r9
        ldr     r0, [r0, #S_R0]
        ldmia   r8, {pc}^
        .endm

exception 전의 모드로 다시 복귀하기 위해 백업해두었던 레지스터들을 복구한다. (종료 전에 fiq 모드로 switch하는 것만 제외하고 svc_exit와 유사하다)

  • 코드 라인 18~19에서 스택으로 부터 r1 ~ r14 레지스터까지 복구한다.
  • 코드 라인 21에서 irq, fiq bit를 마스크한 상태로 fiq mode로 진입하다.
  • 코드 라인 22에서 r8에 스택에서 pt_regs의 pc 값을 읽어온다.
    • 가장 마지막에 복귀할 주소가 담긴다.
  • 코드 라인 23~24에서 스택에서 pt_regs의 psr 값을 읽어 spsr에 대입하여 기존 모드로 복귀한다.
  • 코드 라인 25에서 다시 스택에서 pt_regs의 r0 값을 읽어 r0 레지스터에 복구한다.
  • 코드 라인 26에서 복귀할 주소가 담긴 r8레지스터 값 주소로 jump 한다.

 

__fiq_abt

arch/arm/kernel/entry-armv.S (THUMB 코드 생략)

/*
 * Abort mode handlers
 */

@
@ Taking a FIQ in abort mode is similar to taking a FIQ in SVC mode
@ and reuses the same macros. However in abort mode we must also
@ save/restore lr_abt and spsr_abt to make nested aborts safe.
@
        .align 5
__fiq_abt:
        svc_entry trace=0

 ARM(   msr     cpsr_c, #ABT_MODE | PSR_I_BIT | PSR_F_BIT )
        mov     r1, lr          @ Save lr_abt
        mrs     r2, spsr        @ Save spsr_abt, abort is now safe
 ARM(   msr     cpsr_c, #SVC_MODE | PSR_I_BIT | PSR_F_BIT )
        stmfd   sp!, {r1 - r2}

        add     r0, sp, #8                      @ struct pt_regs *regs
        bl      handle_fiq_as_nmi

        ldmfd   sp!, {r1 - r2}
 ARM(   msr     cpsr_c, #ABT_MODE | PSR_I_BIT | PSR_F_BIT )
        mov     lr, r1          @ Restore lr_abt, abort is unsafe
        msr     spsr_cxsf, r2   @ Restore spsr_abt
 ARM(   msr     cpsr_c, #SVC_MODE | PSR_I_BIT | PSR_F_BIT )

        svc_exit_via_fiq
 UNWIND(.fnend          )
ENDPROC(__fiq_abt)

abt 모드에서 fiq exception이 발생되어 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 해당 ISR(Interrupt Service Routing)을 수행한다. (ARM에서는 기본적으로 등록되어 있지 않다)

  • 코드 라인 12에서 전체 레지스터를 스택에 백업한다.
  • 코드 라인 14~18에서 abt 모드의 lr, spsr을 스택에 백업한다.
    • irq 및 fiq를 disable한 상태로 다시 abt 모드로 진입하여 abt 모드에서의 lr 및 spsr을 r1, r2 레지스터에 잠시 저장하고 svc 모드로 바꾼 후 스택에 백업한다.
  • 코드 라인 20~21에서 fiq 관련 등록된 ISR을 수행한다. (ARM에서는 기본적으로 등록되어 있지 않다)
  • 코드 라인 23~27에서 irq와 fiq를 금지한 상태로 abt 모드에 진입해서 백업해 두었던 2개의 lr, spsr을 다시 복구한 후 svc 모드로 진입한다.

 

SWI 핸들러

arm에서 소프트 인터럽트 발생 시 호출되며 8개의 exception 벡터 주소가 있는 페이지(하이 벡터-0xffff_0000 또는 로우 벡터-0x0000_0000)의 바로 위 페이지 중 첫 엔트리에 arch/arm/kernel/entry-common.S – vector_swi 레이블의 주소가 담겨 있고, 이 주소를 호출한다.

 

Fast syscall vs Slow syscall

syscall이 처리된 후 다시 유저로 복귀하기 전에 처리할 스케쥴 워크가 남아 있는 경우 이를 처리한다.  즉 곧바로 유저로 복귀하지 않고 우선 순위가 높아 먼저 처리할 태스크를 위해 스케쥴이 다시 재배치되면서 다른 태스크를 수행하고 돌아오게 된다. 이렇게 syscall 처리 후 곧바로 돌아오는 경우 fast syscall이라 하고 곧바로 돌아오지 못하고 preemption 되었다가 돌아오는 경우를 slow syscall이라한다. slow syscall 상태가 되면 유저 입장에서 보면 blocked 함수를 호출한 것처럼 느리게 처리된다.

  • Fast syscall
    • syscall 처리 후 곧바로 유저로 복귀
  • Slow syscall
    • syscall 처리 후 유저 복귀 전에 먼저 처리할 태스크를 수행하도록 스케쥴을 바꾼다.

 

vector_swi:

arch/arm/kernel/entry-common.S

/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */

        .align  5
ENTRY(vector_swi)
        sub     sp, sp, #S_FRAME_SIZE
        stmia   sp, {r0 - r12}                  @ Calling r0 - r12
 ARM(   add     r8, sp, #S_PC           )
 ARM(   stmdb   r8, {sp, lr}^           )       @ Calling sp, lr
        mrs     r8, spsr                        @ called from non-FIQ mode, so ok.
        str     lr, [sp, #S_PC]                 @ Save calling PC
        str     r8, [sp, #S_PSR]                @ Save CPSR
        str     r0, [sp, #S_OLD_R0]             @ Save OLD_R0
        zero_fp
        alignment_trap r10, ip, __cr_alignment
        enable_irq
        ct_user_exit
        get_thread_info tsk

        /*
         * Get the system call number.
         */

#if defined(CONFIG_OABI_COMPAT)

        /*
         * If we have CONFIG_OABI_COMPAT then we need to look at the swi
         * value to determine if it is an EABI or an old ABI call.
         */
 USER(  ldr     r10, [lr, #-4]          )       @ get SWI instruction
 ARM_BE8(rev    r10, r10)                       @ little endian instruction

#elif defined(CONFIG_AEABI)

        /*
         * Pure EABI user space always put syscall number into scno (r7).
         */
#else
        /* Legacy ABI only. */
 USER(  ldr     scno, [lr, #-4]         )       @ get SWI instruction
#endif

        adr     tbl, sys_call_table             @ load syscall table pointer

#if defined(CONFIG_OABI_COMPAT)
        /*
         * If the swi argument is zero, this is an EABI call and we do nothing.
         *
         * If this is an old ABI call, get the syscall number into scno and
         * get the old ABI syscall table address.
         */
        bics    r10, r10, #0xff000000
        eorne   scno, r10, #__NR_OABI_SYSCALL_BASE
        ldrne   tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
        bic     scno, scno, #0xff000000         @ mask off SWI op-code
        eor     scno, scno, #__NR_SYSCALL_BASE  @ check OS number
#endif

소프트 인터럽트 핸들러로 user space에서 POSIX 시스템 콜 호출을 하는 경우 커널에서 “sys_”로 시작하는 syscall 함수 및 arm용 syscall 함수를 호출한다.  (thumb 소스는 생략하였다)

  • 리눅스는 3개의 ABI(Application Binary Interface) 관련 모드를 준비하였다.
    • CONFIG_OABI_COMPAT 커널 옵션은 Old ABI(legacy ABI) 및 AEABI 두  방식을 동시에 지원하기 위해 제공한다.
    • CONFIG_AEABI 커널 옵션은 AEABI 방식만 제공한다.
    • 위 두 커널 옵션을 사용하지 않는 경우 OABI를 지원한다.
  • 참고: ABI(Application Binary Interface) | 문c

 

  • 코드 라인 8에서 S_FRAME_SIZE 만큼 스택을 키운다. (grows down)
    • S_FRAME_SIZE: 18개 레지스터를 담는 pt_regs 구조체 사이즈
  • 코드 라인 9에서 r0~r12까지의 레지스터 값들을 스택에 저장한다.
  • 코드 라인 10~11에서 스택에 비워둔 레지스터 저장 장소 중 sp와 lr 레지스터 값을 해당 위치에 저장한다.
  • 코드 라인 12~15에 스택에 있는 pt_regs 구조체의 pc 위치에 돌아갈 주소가 담긴 lr을 대입하고, psr 위치에는 현재 모드의 spsr을 대입한다. 마지막으로 old_r0 위치에 r0 레지스터를 대입하다.
  • 코드 라인 16에서 fp 레지스터에 0을 대입한다
  • 코드 라인 17에서 CONFIG_ALIGNMENT_TRAP 커널 옵션을 사용하는 경우 alignment trap 기능을 적용한다.
  • 코드 라인 18에서 local irq를 enable 한다.
  • 코드 라인 19에서 컨텍스트 트래킹에 관련한  후처리 디버그 활동을 수행한다.
  • 코드 라인 20에서 tsk(r9)에 현재 cpu의 thread_info 주소를 알아온다.
  • 코드 라인 26~60에서 ABI 모드에 따라 swi 호출당시의 syscall 번호를 scno(r7)에 대입하고 tbl(r8)에 syscall 테이블 주소를  대입한다.
    • CONFIG_OABI_COMPAT: AEABI 및 OABI 두 모드를 동시에 지원한다.
      • swi 호출 당시의 숫자 부분만을 떼어 r10에 대입하고 그 값에 따라
        • 0인 경우 AEABI로 인식하여 아무것도 수행하지 않는다. (AEABI 규약에 의거 swi 0 호출 전에 r7에 syscall 번호가 담겨온다)
        • 0이 아닌 경우 OABI로 인식하여 r10에서 __NR_OABI_SYSCALL_BASE(0x900000)값을 더한 후 scno(r7)에 대입하고 tbl(r8)에 sys_oabi_call_table을 지정한다.
    • CONFIG_AEABI: AEABI 모드만 지원한다.
      • 아무것도 수행하지 않는다. (AEABI 규약에 의거 swi 0 호출 전에 r7에 syscall 번호가 담겨온다)
    • CONFIG_OABI
      • swi 호출 당시의 숫자 부분만을 떼어 scno(r7)에 대입하고 __NR_SYSCALL_BASE(0x900000)값을 더한다.
local_restart:
        ldr     r10, [tsk, #TI_FLAGS]           @ check for syscall tracing
        stmdb   sp!, {r4, r5}                   @ push fifth and sixth args

        tst     r10, #_TIF_SYSCALL_WORK         @ are we tracing syscalls?
        bne     __sys_trace

        cmp     scno, #NR_syscalls              @ check upper syscall limit
        adr     lr, BSYM(ret_fast_syscall)      @ return address
        ldrcc   pc, [tbl, scno, lsl #2]         @ call sys_* routine

        add     r1, sp, #S_OFF
2:      cmp     scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
        eor     r0, scno, #__NR_SYSCALL_BASE    @ put OS number back
        bcs     arm_syscall
        mov     why, #0                         @ no longer a real syscall
        b       sys_ni_syscall                  @ not private func

#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
        /*
         * We failed to handle a fault trying to access the page
         * containing the swi instruction, but we're not really in a
         * position to return -EFAULT. Instead, return back to the
         * instruction and re-enter the user fault handling path trying
         * to page it in. This will likely result in sending SEGV to the
         * current task.
         */
9001:
        sub     lr, lr, #4
        str     lr, [sp, #S_PC]
        b       ret_fast_syscall
#endif
ENDPROC(vector_swi)
local_restart 레이블
  • 코드 라인 2에서 현재 태스크의 플래그 값을 r10 레지스터에 대입한다.
  • 코드 라인 3에서 syscall 호출 시의 5번째 인수와 6번째 인수를 스택으로 부터 읽어와 r4와 r5에 대입한다.
  • 코드 라인 5~6에서 syscall tracing이 요청된 경우 트레이스 관련 함수를 호출하여 처리한다.
    • __sys_trace 레이블에서 slow_syscall 루틴이 동작한다. (코드 설명 생략)
  • 코드 라인 8~10에서 syscall 번호가 syscall 처리 범위 이내인 경우 syscall 테이블에서 해당 syscall 번호에 해당하는 “sys_”로 시작하는 함수로 jump 하고 수행이 완료된 후 ret_fast_syscall 레이블 주소로 이동하게 된다.
    • 커널 4.0 기준 0~387번 까지의 syscall 함수들이 등록되어 있다.
      • 0번은 커널 내부 사용목적의 syscall 함수이다. (sys_restart_syscall())
  • 코드 라인 12~17에서 syscall 번호가 ARM용 syscall 범위이내인 경우 arm_syscall() 함수를 호출한다. 만일 범위를 벗어난 경우 sys_ni_syscall() 함수를 호출하여 private한 syscall을 처리하게 한다.  만일 특별히 private syscall을 등록하지 않은 경우 -ENOSYS 에러로 함수를 빠져나온다.
    • 커널 4.0 기준 5개의 ARM syscall 함수들이 등록되어 있다.

 

alignment_trap 매크로

arch/arm/kernel/entry-header.S

        .macro  alignment_trap, rtmp1, rtmp2, label
#ifdef CONFIG_ALIGNMENT_TRAP
        mrc     p15, 0, \rtmp2, c1, c0, 0
        ldr     \rtmp1, \label
        ldr     \rtmp1, [\rtmp1]
        teq     \rtmp1, \rtmp2
        mcrne   p15, 0, \rtmp1, c1, c0, 0
#endif
        .endm

CONFIG_ALIGNMENT_TRAP 커널 옵션을 사용하는 경우 alignment trap 기능을 적용한다.

  • alignment trap을 사용하면 정렬되지 않은 주소 및 데이터에 접근 할 때 prefect abort 또는 data abort exception이 발생한다.
  • 기존에 저장해 두었던 cr_alignment 값과 SCTLR 값이 다른 경우에만 SCTLR에 cr_alignment 값을 저장하는 방식으로 성능 저하를 막는다.

 

ret_fast_syscall 레이블

arch/arm/kernel/entry-common.S

/*
 * This is the fast syscall return path.  We do as little as
 * possible here, and this includes saving r0 back into the SVC
 * stack.
 */
ret_fast_syscall:
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )
        disable_irq                             @ disable interrupts
        ldr     r1, [tsk, #TI_FLAGS]            @ re-check for syscall tracing
        tst     r1, #_TIF_SYSCALL_WORK
        bne     __sys_trace_return
        tst     r1, #_TIF_WORK_MASK
        bne     fast_work_pending
        asm_trace_hardirqs_on

        /* perform architecture specific actions before user return */
        arch_ret_to_user r1, lr
        ct_user_enter

        restore_user_regs fast = 1, offset = S_OFF
 UNWIND(.fnend

syscall 처리를 마치고 복귀하기 전에 pending된 작업이 있으면 수행한 후 백업해 두었던 레지스터들을 다시 읽어 들인 후 복귀한다.

 

sys_ni_syscall()

kernel/sys_ni.c

/*
 * Non-implemented system calls get redirected here.
 */                             
asmlinkage long sys_ni_syscall(void)
{
        return -ENOSYS; 
}

범위 밖의 syscall 요청이 수행되는 루틴이다. private syscall이 필요한 경우 이곳에 작성되는데 없으면 -ENOSYS 에러를 반환한다.

 

참고

ABI(Application Binary Interface)

 

ABI(Application Binary Interface) 표준

Application간 binary 데이터를 어떻게 교환해야 하는지 다음과 같은 규칙들을 정한다.

  • 데이터 타입과 정렬 방법
  • 함수 호출 시 인수 및 결과에 대해 레지스터 교환 방법
  • 시스템 콜 호출 방법
  • 프로그램 코드의 시작과 데이터에 대한 초기화 방법
  • 파일 교환 방법(ELF 등)

 

EABI 표준

EABI(Embeded ABI)는 임베디드 환경의 ABI를 다룬다. ARM 아키텍처에서 리눅스 버전에 따라 ABI를 사용하는 방식이 다음 두 가지로 나뉜다.

  • arm/OABI
    • 커널 v2.6.15 (mainline v2.6.16) 이전에 사용되던 ABI 방식(Old ABI 또는 legacy ABI라고도 불린다)
    • glibc 2.3.6 까지 사용
    • gcc: linux-arm-none-gnu
  • arm/EABI
    • 커널 v2.6.16 부터 사용되는 ARM EABI 방식
    • glibc v2.3.7 및 v2.4 부터 사용
    • gcc: linux-arm-none-gnueabi

arm/OABI 및 arm/EABI 차이점

소프트 인터럽트 호출방식

  • OABI
    • swi __NR_SYSCALL_BASE(==0x900000)+1
  • EABI
    • mov r7, #1    (시스템콜 인덱스)
    • swi 0

구조체 패키징

  • OABI
    • 구조체는 4 바이트 단위로 정렬
  • EABI
    • 구조체 사이즈대로 사용

 

스택에 인수 정렬

  • OABI
    • 스택에 저장할 때 4 바이트 단위로 저장
  • EABI
    • 스택에 저장할 때 8 바이트 단위로 저장

 

64bit 타입 인수 정렬

  • OABI
    • 4 바이트 단위로 정렬
  • EABI
    • 8 바이트 단위로 정렬

 

Enum 타입 사이즈

  • OABI
    • 4 바이트 단위
  • EABI
    • 가변으로 지정할 수 있음

인수 전달 시 레지스터 수

  • OABI
    • 4개 (r0~r3)
  • EABI
    • 7개(r0~r6)

차이점 예제

소프트 인터럽트 + 64bit 타입 인수 정렬

예) long sum64(unsigned int start, size_t size);   syscall no=100

  • arm/OABI
    • r0에 start 대입
    • r1과 r2에 size를 64비트로 대입
    • swi #(0x900000 + 100)
  • arm/EABI
    • r0에 start 대입
    • r2와 r3에 size를 64비트로 대입
    • r7에 100 대입
    • swi 0

툴체인(toolchain)

포맷: arch[-vendor][-os]-abi
  • arch
    • 사용 아키텍처
    • 예) arm, armeb, aarch64, aarch64_eb, mips, x86, i686…
      • eb = be = Endian Big
  • vendor
    • 툴체인 공급자
    • 예) gnu, apple, bcm2708, bcm2708hardfp, none(특정 벤더와 관련 없는)
  • os
    • 운영체제
    • 예) linux, none(bare metal, 운영체제와 관련 없는)
  • abi
    • 지원하는 ABI(Application Binary Interface)
    • 예) eabi, gnueabi, gbueabihf

 

Floating Point Unit

  • FPU(Floationg Point Unit) 사용
    • VFP (Coretex 시리즈 등)
    • 여러 아키텍처가 FPU를 사용하지 않거나 서로 다른 FP instruction set을 사용한다.
  • softfloat
    • FP instruction을 만들지 않고 GCC가 컴파일타임에 라이브러리에서 함수로 준비
  • hardfloat
    • FP instruction을 에뮬레이션한다.

 

ARM Tool Chain

  • arm-linux-gnueabi
    • GNU에서 softfloat을 사용하는 armel 아키텍처를 위해 사용한다.
    • FP(Floating Point)가 없는 arm 아키텍처에서 softfloat 방식을 사용한다.
  • arm-linux-gnueabihf
    • GNU에서 hard float을 사용하는 armhf 아키텍처를 위해 사용한다.
    • 참고로 응용 프로그램을 컴파일 하여 구동하는 경우 hardfloat을 사용한 라이브러리도 같이 준비되어 있어야 한다.
  • arm-bcm2708-linux-gnueabi
    • armv7 아키텍처용 softfloat 방식 사용
  • arm-bcm2708hardfp-linux-gnueabi
    • armv7 아키텍처용 hardfloat 방식 사용
  • 참고: linaro 툴체인을 사용하는 경우 gcc에 비해 조금 더 성능을 향상시키기 위한 변경을 가했다.

 

참고

User virtual maps (mmap2)

현재 유저 주소 공간의 vm(가상 메모리)에 file 또는 anon 매핑을 요청한다.

 

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

 

인수

  • addr
    • 매핑을 원하는 시작 가상 주소 값으로 커널이 이를 hint로 이용하여 적절한 주소를 찾으려고 한다. null이 입력되는 경우 커널이 빈 공간을 찾는다.
  • length
    • 매핑할 길이(bytes)
  • prot
    • 메모리 보호 속성
      • PROT_EXEC: 페이지는 실행 가능하다.
      • PROT_READ: 페이지는 읽기 가능하다.
      • PROT_WRITE: 페이지는 쓰기 가능하다.
      • PROT_NONE: 페이지는 접근할 할 수 없다.
  • flags
    • 플래그
  • fd
    • 파일 디스크립터
  • offset
    • 파일 offset
      • file 매핑하는 경우 skip 할 페이지 수를 사용한다.

 

요청 플래그

  • MAP_FIXED
    • 지정된 시작 가상 주소로 매핑한다. 지정된 주소를 사용할 수 없는 경우 mmap()은 실패한다.
    • 가상 시작 주소 addr는 페이지 사이즈의 배수여야 한다.
    • 요청 영역이 기존 매핑과 중복되는 경우 중복되는 영역의 기존 매핑은 제거된다.
  • MAP_ANONYMOUS
    • 어떠한 파일하고도 연결되지 않고 0으로 초기화된 영역.
    • fd는 무시되지만 어떤 경우에는 -1이 요구된다.
    • offset는 0으로 한다.
    • MAP_SHARED와 같이 사용될 수도 있다. (커널 v2.4)
  • MAP_FILE
    • 파일 매핑
  •  MAP_SHARED
    • 다른 프로세스와 영역에 대해 공유 매핑.
  • MAP_PRIVATE
    • private COW(Copy On Write) 매핑을 만든다. 이 영역은 다른 프로세스와 공유되지 않는다.
  •  MAP_DENYWRITE
    • 쓰기 금지된 매핑 영역
  •  MAP_EXECUTABLE
    • 실행 가능한 매핑 영역
  •  MAP_GROWSDOWN
    • 하향으로 자라는 스택에 사용된다.
  • MAP_HUGETLB (커널 v2.6.32)
    • huge page들을 사용하여 할당하게 한다.
  • MAP_HUGE_2MB, MAP_HUGE_1GB (커널 v3.8)
    • MAP_HUGETLB와 같이 사용되며 huge page를 선택할 수 있다.
    • “/sys/kernel/mm/hugepages” 디렉토리에 사용할 수 있는 huge page 종류를 볼 수 있다.
  •  MAP_LOCKED
    • mlock()과 동일하게 요청 영역은 물리메모리가 미리 할당되어 매핑된다.
    • mlock()과 다르게 물리메모리를 미리 할당하고 매핑할 때 실패하더라도 곧바로 -ENOMEM 에러를 발생시키지 않는다.
  •  MAP_NONBLOCK
    • 오직 MAP_POPULATE와 같이 사용될 때에만 의미가 있다.
  •  MAP_NORESERVE
    • 이 영역은 swap 공간을 준비하지 않는다.
  •  MAP_POPULATE
    • 매핑에 대한 페이지 테이블을 활성화(prefault)한다.
    • private 매핑에서만 사용된다. (커널 v2.6.23)
  • MAP_STACK (커널 v2.6.27)
    • 프로세스 또는 스레드 스택을 할당한다.
  • MAP_UNINITIALIZED (커널 v2.6.33)
    • anonymous 페이지들을 클리어하지 않는다.

 

sys_mmap2()

arch/arm/kernel/entry-common.S

/* 
 * Note: off_4k (r5) is always units of 4K.  If we can't do the requested
 * offset, we return EINVAL.
 */
sys_mmap2: 
#if PAGE_SHIFT > 12
                tst     r5, #PGOFF_MASK
                moveq   r5, r5, lsr #PAGE_SHIFT - 12
                streq   r5, [sp, #4]
                beq     sys_mmap_pgoff
                mov     r0, #-EINVAL
                ret     lr
#else
                str     r5, [sp, #4]
                b       sys_mmap_pgoff
#endif
ENDPROC(sys_mmap2)

유저 anon 및 file 캐시 매핑을 요청한다.

 

sys_mmap_pgoff()

mm/mmap.c

SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
                unsigned long, prot, unsigned long, flags,
                unsigned long, fd, unsigned long, pgoff)
{
        struct file *file = NULL;
        unsigned long retval = -EBADF;
                                      
        if (!(flags & MAP_ANONYMOUS)) {
                audit_mmap_fd(fd, flags);
                file = fget(fd);      
                if (!file)
                        goto out;
                if (is_file_hugepages(file))
                        len = ALIGN(len, huge_page_size(hstate_file(file)));
                retval = -EINVAL;
                if (unlikely(flags & MAP_HUGETLB && !is_file_hugepages(file)))
                        goto out_fput;
        } else if (flags & MAP_HUGETLB) {
                struct user_struct *user = NULL;
                struct hstate *hs;
                 
                hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & SHM_HUGE_MASK);
                if (!hs)              
                        return -EINVAL;

                len = ALIGN(len, huge_page_size(hs));
                /*
                 * VM_NORESERVE is used because the reservations will be
                 * taken when vm_ops->mmap() is called
                 * A dummy user value is used because we are not locking
                 * memory so no accounting is necessary
                 */
                file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
                                VM_NORESERVE,
                                &user, HUGETLB_ANONHUGE_INODE,
                                (flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
                if (IS_ERR(file))
                        return PTR_ERR(file);
        }

        flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);

        retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:
        if (file)
                fput(file);
out:
        return retval;
}

file 디스크립터에서 pgoff 페이지 부터 요청 길이만큼 요청한 가상 주소에  prot 속성으로 매핑한다.

  • 코드 라인 8~17에서 file 매핑인 경우 audit 설정이 필요 시 설정하고 huge 페이지를 사용하는 파일인 경우 길이를 huge 페이지에 맞춰 정렬한다.
    • 코드 라인 9에서 현재 태스크의 audit_context가 설정된 경우 audit_context에 요청 fd와 flags를 설정하고 타입으로 AUDIT_MMAP을 대입한다.
    • 코드 라인 10~12에서 file 디스크립터를 통해 file을 알아오는데 실패하는 경우 out 레이블을 통해 -EBADF 에러를 반환한다.
    • 코드 라인 13~14에서 hugetlbfs 또는 huge 페이지 매핑을 이용하는 공유 파일인 경우 길이를 huge 페이지 단위로 정렬한다.
    • 코드 라인 15~17에서 MAP_HUGETLB 플래그 요청을 하였지만 file이 huge 페이지 요청 타입이 아닌 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 18~39에서 MAP_HUGETLB 요청이 있는 경우 길이를 huge 페이지 단위로 정렬하여 HUGETLB_ANON_FILE 형태의 파일을 준비한다.
    • 코드 라인 22~24에서 플래그에 기록된 huge 페이지 정보가 발견되지 않으면 -EINVAL 에러를 반환한다.
      • 플래그의 bit26~bit31에 사이즈를 로그 단위로 변환하여 저장하였다.
        • x86 예) 21 -> 2M huge page, 30 -> 1G huge page
    • 코드 라인 26에서 길이를 huge 페이지 단위로 정렬한다.
    • 코드 라인 33~38에서 HUGETLB_ANON_FILE 형태의 파일을 준비한다. 실패하는 경우 에러를 반환한다.
  • 코드 라인 41~43에서 플래그에서 MAP_EXECUTABLE 및 MAP_DENYWRITE를 제외하고 매핑을 한다.

 

is_file_hugepages()

include/linux/hugetlb.h

static inline int is_file_hugepages(struct file *file)
{               
        if (file->f_op == &hugetlbfs_file_operations)
                return 1;
        if (is_file_shm_hugepages(file)) 
                return 1;
         
        return 0;       
}

file이 hugetlbfs에서 사용하는 파일이거나 huge 페이지를 사용하는 공유 메모리 파일인 경우 1을 반환한다. 그 외에는 0을 반환한다.

 

Audit 관련 함수

audit_mmap_fd()

include/linux/audit.h

static inline void audit_mmap_fd(int fd, int flags)
{
        if (unlikely(!audit_dummy_context()))
                __audit_mmap_fd(fd, flags);
}

작은 확률로 현재 태스크의 audit_context가 설정된 경우 audit_context에 요청 fd와 flags를 설정하고 타입으로 AUDIT_MMAP을 대입한다.

 

audit_dummy_context()

include/linux/audit.h

static inline int audit_dummy_context(void)
{
        void *p = current->audit_context;
        return !p || *(int *)p;
}

현재 태스크의 audit_context가 설정된 경우 0을 반환하고, 설정되지 않은 경우 0이 아닌 값을 반환한다.

 

__audit_mmap_fd()

kernel/auditsc.c

void __audit_mmap_fd(int fd, int flags) 
{
        struct audit_context *context = current->audit_context;
        context->mmap.fd = fd;
        context->mmap.flags = flags;
        context->type = AUDIT_MMAP;
}

현재 태스크의 audit_context에 요청 fd와 flags를 설정하고 타입으로 AUDIT_MMAP을 대입한다.

 

vm_mmap_pgoff()

mm/util.c

unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
        unsigned long len, unsigned long prot,
        unsigned long flag, unsigned long pgoff)
{
        unsigned long ret;
        struct mm_struct *mm = current->mm;
        unsigned long populate;

        ret = security_mmap_file(file, prot, flag);
        if (!ret) {
                down_write(&mm->mmap_sem);
                ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
                                    &populate);
                up_write(&mm->mmap_sem);
                if (populate)
                        mm_populate(ret, populate);
        }
        return ret;
}

file을 pgoff 페이지 부터 요청 길이만큼 가상 주소 addr에  prot 속성으로 매핑한다.

  • 코드 라인 9에서 LSM 및 LIM을 통해 파일 매핑의 허가 여부를 알아온다. 0=성공
    • LSM(Linux Security Module)과 LIM(Linux Integrity Module)의 hook api를 호출하여 한다.
  • 코드 라인 10~13에서 mmap_sem write 세마포어 락을 사용하여 vm 매핑을 요청한다.
  • 코드 라인 14~15에서 매핑 영역을 활성화(물리 RAM을 매핑)한다.

 

do_mmap_pgoff()

mm/mmap.c

/* 
 * The caller must hold down_write(&current->mm->mmap_sem).
 */

unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
                        unsigned long len, unsigned long prot,
                        unsigned long flags, unsigned long pgoff,
                        unsigned long *populate)
{
        struct mm_struct *mm = current->mm;
        vm_flags_t vm_flags;
                         
        *populate = 0;

        /*
         * Does the application expect PROT_READ to imply PROT_EXEC?
         *
         * (the exception is when the underlying filesystem is noexec
         *  mounted, in which case we dont add PROT_EXEC.)
         */
        if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
                if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
                        prot |= PROT_EXEC; 

        if (!len)
                return -EINVAL; 

        if (!(flags & MAP_FIXED))
                addr = round_hint_to_min(addr);

        /* Careful about overflows.. */
        len = PAGE_ALIGN(len);
        if (!len)
                return -ENOMEM;

        /* offset overflow? */
        if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
                return -EOVERFLOW;

        /* Too many mappings? */
        if (mm->map_count > sysctl_max_map_count)
                return -ENOMEM;

        /* Obtain the address to map to. we verify (or select) it and ensure
         * that it represents a valid section of the address space.
         */
        addr = get_unmapped_area(file, addr, len, pgoff, flags);
        if (addr & ~PAGE_MASK)
                return addr;

file을 pgoff 페이지 부터 요청 길이만큼 가상 주소 addr에  prot 속성으로 매핑한다. 실제 메모리가 매핑된 경우 출력 인수 populate에 길이를 대입한다.

  • 코드 라인 21~23에서 read 속성이 요청되는 경우 현재 태스크의 personality에 READ_IMPLIES_EXEC가 설정된 경우 exec 속성을 추가한다. 단 sysfs 및 proc 마운트 위의 file과 같이 마운트 플래그가  MNT_NOEXEC 설정이 있는경우는 제외한다.
  • 코드 라인 25~26에서 길이가 0인 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 28~29에서 고정 매핑을 요청한 경우가 아니면 페이지 정렬한 주소를 사용하되 mmap_min_addr 보다 작은 경우 mmap_min_addr 주소로 변경한다.
  • 코드 라인 32~34에서 길이를 페이지 단위로 정렬한다. 단 0인 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 37~38에서 pgoff 페이지+ len 페이지가 시스템 주소 범위를 초과하는 경우 -EOVERFLOW 에러를 반환한다.
  • 코드 라인 41~42에서 현재 태스크의 메모리 디스크립터에 매핑된 수가 최대치 허용을 초과한 경우 -ENOMEM 에러를 반환한다.
    • 기본 매핑 최대치: 65530 (“/proc/sys/vm/max_map_count”에서 설정)
  • 코드 라인 47~49에서 매핑되지 않은 영역을 찾아 시작 가상 주소를 알아온다. 만일 에러인 경우 에러 코드를 반환한다.
.
        /* Do simple checking here so the lower-level routines won't have
         * to. we assume access permissions have been handled by the open
         * of the memory object, so we don't do any here.
         */
        vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
                        mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

        if (flags & MAP_LOCKED)
                if (!can_do_mlock())
                        return -EPERM;

        if (mlock_future_check(mm, vm_flags, len))
                return -EAGAIN;

        if (file) {
                struct inode *inode = file_inode(file);

                switch (flags & MAP_TYPE) {
                case MAP_SHARED:
                        if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
                                return -EACCES;

                        /*
                         * Make sure we don't allow writing to an append-only
                         * file..
                         */
                        if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
                                return -EACCES;

                        /*
                         * Make sure there are no mandatory locks on the file.
                         */
                        if (locks_verify_locked(file))
                                return -EAGAIN;

                        vm_flags |= VM_SHARED | VM_MAYSHARE;
                        if (!(file->f_mode & FMODE_WRITE))
                                vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

                        /* fall through */
                case MAP_PRIVATE:
                        if (!(file->f_mode & FMODE_READ))
                                return -EACCES;
                        if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
                                if (vm_flags & VM_EXEC)
                                        return -EPERM;
                                vm_flags &= ~VM_MAYEXEC;
                        }
                        if (!file->f_op->mmap)
                                return -ENODEV;
                        if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
                                return -EINVAL;
                        break;

                default:
                        return -EINVAL;
                }
        } else {
                switch (flags & MAP_TYPE) {
                case MAP_SHARED:
                        if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
                                return -EINVAL;
                        /*
                         * Ignore pgoff.
                         */
                        pgoff = 0;
                        vm_flags |= VM_SHARED | VM_MAYSHARE;
                        break;
                case MAP_PRIVATE:
                        /*
                         * Set pgoff according to addr for anon_vma.
                         */
                        pgoff = addr >> PAGE_SHIFT;
                        break;
                default:
                        return -EINVAL;
                }
        }

        /*
         * Set 'VM_NORESERVE' if we should not account for the
         * memory use of this mapping.
         */
        if (flags & MAP_NORESERVE) {
                /* We honor MAP_NORESERVE if allowed to overcommit */
                if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
                        vm_flags |= VM_NORESERVE;

                /* hugetlb applies strict overcommit unless MAP_NORESERVE */
                if (file && is_file_hugepages(file))
                        vm_flags |= VM_NORESERVE;
        }

        addr = mmap_region(file, addr, len, vm_flags, pgoff);
        if (!IS_ERR_VALUE(addr) &&
            ((vm_flags & VM_LOCKED) ||
             (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
                *populate = len;
        return addr;
}
  • 코드 라인 6~7에서 vm_flags에 vm 플래그로 변환한 prot 플래그, vm 플래그로 변환한 flags, 메모리 디스크립터의 기본 플래그에 mayread, maywrite, mayexec를 추가한다.
  • 코드 라인 9~11에서 MAP_LOCKED 플래그 요청이 있는 경우 mlock 최대치 제한 등으로 인해 수행할 수 없으면 -EPERM 에러를 반환한다.
  • 코드 라인 13~14에서 VM_LOCKED 요청이 있고 기존 mlock 페이지에 요청 길이를 추가하여 mlock 페이지 최대치 제한에 걸리는 경우 -EAGAIN 에러를 반환한다.
  • 코드 라인 16~59에서 file 매핑인 경우 inode 값을 가져오고 다음 두 가지 요청을 수행한다.
    • 코드 라인 19~41에서 공유 파일 매핑(MAP_SHARED) 플래그 요청인 경우
      • 코드 라인 20~21에서 write 속성이 없는 파일에 write 요청을 한 경우 -EACCESS 에러를 반환한다.
      • 코드 라인 27~28에서 append only 파일인 경우 write 요청을 한 경우  -EACCESS 에러를 반환한다.
      • 코드 라인 33~34에서 mandatory 락이 걸려있는 파일인 경우 -EAGAIN 에러를 반환한다.
      • 코드 라인 36에서 vm_flags에 share 및 mayshare 속성을 추가한다.
      • 코드 라인 37~39에서 write 속성이 없는 파일인 경우 maywrite와 shared를 제거한다. 아래 private 요청을 계속 진행한다.
    • 코드 라인 42~55에서 private  파일 매핑(MAP_PRIVATE) 플래그 요청인 경우
      • 코드 라인 43~44에서 read 속성이 없는 파일인 경우 -EACCESS 에러를 반환한다.
      • 코드 라인 45~49에서 file이 마운트 곳의 마운트 플래그에 실행 금지가 설정된 경우 mayexec를 제거한다. 단 VM_EXEC 요청이 있는 경우에는 -EPERM 에러를 반환한다.
        • “/proc 및 /sys”가 마운트 되어 있는 곳은 실행 파일이 있을 수 없다.
      • 코드 라인 51~52에서 매핑이 없는 파일은 -ENODEV 에러를 반환한다.
      • 코드 라인 53~54에서 growsdown 및 growsup 요청된 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 60~80에서 anon 매핑인 경우 다음 두 가지 요청을 수행한다.
    • 코드 라인 61~70에서shared anon 매핑인 경우 pgoff=0, shared 및 maysahre 플래그를 추가한다. 단 growsdown 및 growsup 요청된 경우 -EINVAL 에러를 반환한다.
    • 코드 라인 71~76에서 private anon 매핑인 경우 pgoff에 가상 주소 페이지 번호를 대입한다.
  • 코드 라인 86~94에서 no reserve 요청인 경우 over commit 모드가 OVERCOMMIT_NEVER가 아닌 경우 또는 또한 huge page 파일인 경우  vm_noreserve 플래그를 추가한다.
  • 코드 라인 96에서 file을 pgoff 페이지 부터 가상 주소 addr에 길이 len 만큼 vm_flags 속성을 사용하여 vma를 구성하고 등록한다.
  • 코드 라인 97~100에서 실제 메모리가 매핑된 경우 출력 인수 populate에 길이를 대입한다.

 

round_hint_to_min()

mm/mmap.c

static inline unsigned long round_hint_to_min(unsigned long hint)
{
        hint &= PAGE_MASK;
        if (((void *)hint != NULL) &&
            (hint < mmap_min_addr))
                return PAGE_ALIGN(mmap_min_addr);
        return hint;
}

가상 주소 hint를 페이지 단위로 절삭하고 반환한다. 단 그 주소가 mmap_min_addr 보다 낮은 경우 페이지 단위로 정렬한 mmap_min_addr 주소를 반환한다.

 

calc_vm_prot_bits()

include/linux/mman.h

/*
 * Combine the mmap "prot" argument into "vm_flags" used internally.
 */
static inline unsigned long
calc_vm_prot_bits(unsigned long prot)
{
        return _calc_vm_trans(prot, PROT_READ,  VM_READ ) |
               _calc_vm_trans(prot, PROT_WRITE, VM_WRITE) |
               _calc_vm_trans(prot, PROT_EXEC,  VM_EXEC) |
               arch_calc_vm_prot_bits(prot);
}

prot 값에 PROT_READ, PROT_WRITE, PROT_EXEC 비트가 있는 경우 각각 VM_READ, VM_WRITE, VM_EXEC 플래그 속성으로 변환하여 반환한다.

  • 특정 아키텍처에 맞게 속성을 추가할 수 있다. (arm은 추가 없음)

 

_calc_vm_trans()

include/linux/mman.h

/*
 * Optimisation macro.  It is equivalent to:
 *      (x & bit1) ? bit2 : 0
 * but this version is faster. 
 * ("bit1" and "bit2" must be single bits)
 */
#define _calc_vm_trans(x, bit1, bit2) \
  ((bit1) <= (bit2) ? ((x) & (bit1)) * ((bit2) / (bit1)) \
   : ((x) & (bit1)) / ((bit1) / (bit2)))

x 플래그에서 bi1 속성이 있는 경우 bit 속성으로 변환하여 반환한다. 없는 경우 0을 반환한다.

 

calc_vm_flag_bits()

include/linux/mman.h

/*
 * Combine the mmap "flags" argument into "vm_flags" used internally.
 */ 
static inline unsigned long
calc_vm_flag_bits(unsigned long flags)
{
        return _calc_vm_trans(flags, MAP_GROWSDOWN,  VM_GROWSDOWN ) |
               _calc_vm_trans(flags, MAP_DENYWRITE,  VM_DENYWRITE ) |
               _calc_vm_trans(flags, MAP_LOCKED,     VM_LOCKED    );
}

flags 값에 MAP_GROWSDOWN, MAP_DENYWRITE, MAP_LOCKED 비트가 있는 경우 각각 VM_GROWSDOWN, VM_DENYWRITE, VM_LOCKED 플래그 속성으로 변환하여 반환한다.

 

unsigned long mmap_region(struct file *file, unsigned long addr,
                unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma, *prev;
        int error;
        struct rb_node **rb_link, *rb_parent;
        unsigned long charged = 0;

        /* Check against address space limit. */
        if (!may_expand_vm(mm, len >> PAGE_SHIFT)) {
                unsigned long nr_pages;

                /*
                 * MAP_FIXED may remove pages of mappings that intersects with
                 * requested mapping. Account for the pages it would unmap.
                 */
                if (!(vm_flags & MAP_FIXED))
                        return -ENOMEM;

                nr_pages = count_vma_pages_range(mm, addr, addr + len);

                if (!may_expand_vm(mm, (len >> PAGE_SHIFT) - nr_pages))
                        return -ENOMEM;
        }

        /* Clear old maps */
        error = -ENOMEM;
munmap_back:
        if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {
                if (do_munmap(mm, addr, len))
                        return -ENOMEM;
                goto munmap_back;
        }

        /*
         * Private writable mapping: check memory availability
         */
        if (accountable_mapping(file, vm_flags)) {
                charged = len >> PAGE_SHIFT;
                if (security_vm_enough_memory_mm(mm, charged))
                        return -ENOMEM;
                vm_flags |= VM_ACCOUNT;
        }

        /*
         * Can we just expand an old mapping?
         */
        vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL);
        if (vma)
                goto out;

file을 pgoff 페이지 부터 요청 길이만큼 가상 주소 addr에  prot 속성으로 vma를 구성하고 등록한다.

  • 코드 라인 11 ~19에서 len 페이지 만큼의 공간을 확장할 수 없는 경우 fix 매핑이 아닌 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 21~24에서 fix 매핑인 경우 기존 영역과 겹치지 않는 페이지 만큼이 확장 가능하지 않으면 -ENOMEM 에러를 반환환다.
    • fix 매핑인 경우 겹치는 영역만큼은 뺴고 매핑할 계획이다.
  • 코드 라인 29~34에서  munmap_back 레이블이다. 요청 영역과 겹치는 기존 영역을 언매핑하고 다시 이 레이블로 이동한다. 만일 언매핑이 실패한 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 39~44에서 vm 메모리 계량이 필요한 매핑인 경우 LSM을 통해 charged 페이지 만큼 vm 메모리 할당으로 commit하고 허용된 경우 VM_ACCOUNT 플래그를 추가한다.
    • accountable 매핑 확인(private writable 매핑)
      • huge 파일 매핑이 아니고 noreserve 및 shared가 없고 write 요청은 있는 경우이다.
    • LSM 모듈에서 SELinux 모듈을 사용 여부에 따라 selinux_vm_enough_memory() 함수를 먼저 호출하여 admin 권한만큼의 추가 영역을 확보한 후 __vm_enough_memory() 함수를 호출하여 commit 량에 대해 commit 옵션에 따라 vm 허용치 제한을 통해  할당 여부를 반환한다.
    • LSM 모듈에서 기본 Posix Capability 모듈만을 사용하는 경우 cap_vm_enough_memory() 함수를 먼저 호출하여 admin권한만큼의 추가 영역을 확보한 후 __vm_enough_memory() 함수를 호출하여 commit 량에 대해 commit 옵션에 따라 vm 허용치 제한을 통해  할당 여부를 반환한다.
  •  코드 라인 49~51에서 기존 영역과 병합할 수 있는 경우이면 병합한다. 병합에 성공한 경우 out 레이블로 이동한다.
.
        /*
         * Determine the object being mapped and call the appropriate
         * specific mapper. the address has already been validated, but
         * not unmapped, but the maps are removed from the list.
         */
        vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
        if (!vma) {
                error = -ENOMEM;
                goto unacct_error;
        }

        vma->vm_mm = mm;
        vma->vm_start = addr;
        vma->vm_end = addr + len;
        vma->vm_flags = vm_flags;
        vma->vm_page_prot = vm_get_page_prot(vm_flags);
        vma->vm_pgoff = pgoff;
        INIT_LIST_HEAD(&vma->anon_vma_chain);

        if (file) {
                if (vm_flags & VM_DENYWRITE) {
                        error = deny_write_access(file);
                        if (error)
                                goto free_vma;
                }
                if (vm_flags & VM_SHARED) {
                        error = mapping_map_writable(file->f_mapping);
                        if (error)
                                goto allow_write_and_free_vma;
                }

                /* ->mmap() can change vma->vm_file, but must guarantee that
                 * vma_link() below can deny write-access if VM_DENYWRITE is set
                 * and map writably if VM_SHARED is set. This usually means the
                 * new file must not have been exposed to user-space, yet.
                 */
                vma->vm_file = get_file(file);
                error = file->f_op->mmap(file, vma);
                if (error)
                        goto unmap_and_free_vma;

                /* Can addr have changed??
                 *
                 * Answer: Yes, several device drivers can do it in their
                 *         f_op->mmap method. -DaveM
                 * Bug: If addr is changed, prev, rb_link, rb_parent should
                 *      be updated for vma_link()
                 */
                WARN_ON_ONCE(addr != vma->vm_start);

                addr = vma->vm_start;
                vm_flags = vma->vm_flags;
        } else if (vm_flags & VM_SHARED) {
                error = shmem_zero_setup(vma);
                if (error)
                        goto free_vma;
        }

        vma_link(mm, vma, prev, rb_link, rb_parent);
        /* Once vma denies write, undo our temporary denial count */
        if (file) {
                if (vm_flags & VM_SHARED)
                        mapping_unmap_writable(file->f_mapping);
                if (vm_flags & VM_DENYWRITE)
                        allow_write_access(file);
        }
        file = vma->vm_file;
  • 코드 라인 7~19에서 vma(vm_area_struct 구조체)를 할당하고 구성한다. 할당이 실패하는 경우 unacct_error로 이동한다.
  • 코드 라인 13~19에서 vma를 구성한다.
  • 코드 라인 21~53에서 file 매핑인 경우
    • 코드 라인 22~26에서 denywrite 요청이 있는 경우 file을 denywrite 상태로 만든다. 실패하는 경우 free_vma 레이블로 이동한다.
    • 코드 라인 27~31에서  shared 요청이 있는 경우 매핑 영역에 대해 다음 writable 파일 매핑 요청이 거절되게 설정한다. 실패하는 경우 allow_write_and_free_vma 레이블로 이동한다.
    • 코드 라인 38에서 file의 사용 카운터 f_count를 증가시키고 file을 vma_vm_file에 대입한다.
    • 코드 라인 39~41에서 vma 정보로 file 매핑을 수행한다. 실패하는 경우 unmap_and_free_vma 레이블로 이동한다.
  • 코드 라인 54~58에서 shared anon 매핑을 준비한다. 실패하는 경우 free_vma 레이블로 이동한다.
    • “/dev/zero” 파일을 vma->vm_file에 지정하고 vma->vm_ops에 전역 shmem_vm_ops를 대입한다.
  • 코드 라인 60에서 vma 정보를 추가한다.
  • 코드 라인 62~67에서 shared file 매핑인 경우 매핑 영역에 대해 writable 매핑이 가능하게 바꾼다. 그리고 denywrite 요청을 가진 file 매핑인 경우 file에 대해 writable 매핑이 가능하도록 설정한다.

 

out:
        perf_event_mmap(vma);

        vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
        if (vm_flags & VM_LOCKED) {
                if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
                                        vma == get_gate_vma(current->mm)))
                        mm->locked_vm += (len >> PAGE_SHIFT);
                else
                        vma->vm_flags &= ~VM_LOCKED;
        }

        if (file)
                uprobe_mmap(vma);

        /*
         * New (or expanded) vma always get soft dirty status.
         * Otherwise user-space soft-dirty page tracker won't
         * be able to distinguish situation when vma area unmapped,
         * then new mapped in-place (which must be aimed as
         * a completely new data area).
         */
        vma->vm_flags |= VM_SOFTDIRTY;

        vma_set_page_prot(vma);

        return addr;

unmap_and_free_vma:
        vma->vm_file = NULL;
        fput(file);

        /* Undo any partial mapping done by a device driver. */
        unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
        charged = 0;
        if (vm_flags & VM_SHARED)
                mapping_unmap_writable(file->f_mapping);
allow_write_and_free_vma:
        if (vm_flags & VM_DENYWRITE)
                allow_write_access(file);
free_vma:
        kmem_cache_free(vm_area_cachep, vma);
unacct_error:
        if (charged)
                vm_unacct_memory(charged);
        return error;
}
  • 코드 라인 2에서 mmap에 대한 performance events 정보를 출력한다.
  • 코드 라인 4에서 메모리 디스크립터에 대해 가상 메모리에 대한 다음의 vm stat 카운터를 len 페이지만큼 추가한다.
    • mm->total_vm
    • file 매핑인 경우 mm->shared_vm
    • file 매핑이면서 읽기 또는 쓰기 속성이 있는 경우 mm->exec_vm
    • stack 매핑인 경우 mm->stack_mm
  • 코드 라인 5~11에서 mlock(VM_LOCKED) 요청인 경우 VM_SPECIAL, hugetlb 페이지 또는 gate vma 요청인 경우 VM_LOCKED 플래그를 삭제한다. 그렇지 않은 경우 mm->locked_vm에 len 페이지를 추가한다.
  • 코드 라인 13~14에서 file 매핑인 경우 uprobe가 설정되어 있고 uprobe filter 체인이 걸려있는 경우 는 요청 주소에 break point 명령 코드로 업데이트한다.
  • 코드 라인 23~27에서 softdirty 플래그를 추가하고 vma에 기록하고 정상적으로 가상 주소를 반환한다.
  • 코드 라인 29~37에서 unmap_and_free_vma 레이블은 매핑을 해제한다.
  • 코드 라인 38~40에서 allow_write_and_free_vma 레이블은 denywrite 요청이 있는 경우 inode의 i_writecount를 증가 시킨다.
  • 코드 라인 41~42에서 free_vma 레이블은 vma object를 할당 해제 한다.
  • 코드 라인 43~46에서 unacct_error 레이블은 vm 계량을 했었던(Private writable 매핑) 경우 commit 양을 되돌리기 위해 charged 만큼 다시 감소시킨다.

 

count_vma_pages_range()

mm/mmap.c

static unsigned long count_vma_pages_range(struct mm_struct *mm,
                unsigned long addr, unsigned long end)
{
        unsigned long nr_pages = 0;
        struct vm_area_struct *vma;

        /* Find first overlaping mapping */
        vma = find_vma_intersection(mm, addr, end);
        if (!vma)
                return 0;

        nr_pages = (min(end, vma->vm_end) -
                max(addr, vma->vm_start)) >> PAGE_SHIFT;

        /* Iterate over the rest of the overlaps */
        for (vma = vma->vm_next; vma; vma = vma->vm_next) {
                unsigned long overlap_len;

                if (vma->vm_start > end)
                        break;

                overlap_len = min(end, vma->vm_end) - vma->vm_start;
                nr_pages += overlap_len >> PAGE_SHIFT;
        }

        return nr_pages;
}

요청 영역과 기존 vma 영역과 겹치는 페이지 수를 반환한다. 겹치는 영역이 없으면 0을 반환한다.

  • 코드 라인 8~10에서 기존 vma 영역과 요청 영역이 겹치는 경우 관련 vma를 알아온다. 겹치는 영역이 없는 경우 0을 반환한다.
  • 코드 라인 12~13에서 현재 vma에서 겹치는 페이지 수를 산출한다.
  • 코드 라인 16~26에서 다음 vma 영역들과도 비교하여 겹치는 페이지 수를 산출한 후 반환한다.

 

accountable_mapping()

mm/mmap.c

/*
 * We account for memory if it's a private writeable mapping,
 * not hugepages and VM_NORESERVE wasn't set.
 */
static inline int accountable_mapping(struct file *file, vm_flags_t vm_flags)
{
        /*
         * hugetlb has its own accounting separate from the core VM
         * VM_HUGETLB may not be set yet so we cannot check for that flag.
         */
        if (file && is_file_hugepages(file))
                return 0;

        return (vm_flags & (VM_NORESERVE | VM_SHARED | VM_WRITE)) == VM_WRITE;
}

vm 메모리 계량이 필요한 매핑인지 확인한다.

  • accountable 매핑 확인(private writable 매핑)
    • huge 파일 매핑이 아니고 noreserve 및 shared가 없고 write 요청은 있는 경우이다.

 

writable 파일 매핑(1)

inode->i_writecount 값 상태

  • 0(writable)
    • write 권한이 허용되지 않은 상태로 새로운 writable 권한 요청이 가능한 상태
  • 1(write)
    • write 권한이 허용된 상태로 새로운 write 권한 요청은 금지된 상태
  • 음수(denywrite)
    • denywrite 요청에 의해 모든 write 권한 요청이 금지된 상태

 

get_write_access()

include/linux/fs.h

/*
 * get_write_access() gets write permission for a file.
 * put_write_access() releases this write permission.
 * This is used for regular files.
 * We cannot support write (and maybe mmap read-write shared) accesses and
 * MAP_DENYWRITE mmappings simultaneously. The i_writecount field of an inode
 * can have the following values:
 * 0: no writers, no VM_DENYWRITE mappings
 * < 0: (-i_writecount) vm_area_structs with VM_DENYWRITE set exist
 * > 0: (i_writecount) users are writing to the file.
 *
 * Normally we operate on that counter with atomic_{inc,dec} and it's safe
 * except for the cases where we don't hold i_writecount yet. Then we need to
 * use {get,deny}_write_access() - these functions check the sign and refuse
 * to do the change if sign is wrong.
 */
static inline int get_write_access(struct inode *inode)
{
        return atomic_inc_unless_negative(&inode->i_writecount) ? 0 : -ETXTBSY;
}

inode에 대해 write 권한을 요청한다. 성공하면 0을 반환하고, 실패하는 경우 -ETXTBSY 에러를 반환한다.

  • inode->i_writecount를 음수(-)가 아닌한 증가시킨다. 성공한 경우 0을 반환하고 , 이미 음수(-)여서 증가가 불가능한 경우 -ETXTBSY 에러를 반환한다.

 

put_write_access()

include/linux/fs.h

static inline void put_write_access(struct inode * inode)
{
        atomic_dec(&inode->i_writecount);
}

inode에 대해 write 권한을 제거한다.

  • inode->i_writecount를 감소시킨다.

 

deny_write_access()

include/linux/fs.h

static inline int deny_write_access(struct file *file)
{
        struct inode *inode = file_inode(file);
        return atomic_dec_unless_positive(&inode->i_writecount) ? 0 : -ETXTBSY;
}

요청 file을 denywrite 상태로 만들어 writable 매핑을 만들 수 없게 금지한다. 성공한 경우 0을 반환하고 그렇지 않은 경우  -ETXTBSY 에러를 반환한다.

  • 요청 file의 inode->i_writecount를 양수(+)가 아닌한 감소시킨다. 성공한 경우 0을 반환하고  이미 양수(+)여서 감소가 불가능한 경우 -ETXTBSY 에러를 반환한다.

 

allow_write_access()

include/linux/fs.h

static inline void allow_write_access(struct file *file)
{
        if (file)       
                atomic_inc(&file_inode(file)->i_writecount);
}

요청 파일에 대해 denywrite를 제거하여 새로운 writable 매핑을 허용할 수 있는 상태로 변경한다.

  • 요청 file의 inode->i_writecount를 증가시킨다.

 

writable 파일 매핑(2)

mapping->i_mapwritable 값 상태 (VM_SHARED 카운터)

  • 0(writable)
    • 공유 write 매핑되어 있지 않은 상태로 새로운 writable 매핑 요청이 가능한 상태
  • 양수(write)
    • 공유 write 매핑되어 있는 상태로 새로운 write 매핑 요청은 금지된 상태
  • 음수(denywrite)
    • 공유 denywrite 요청에 의해 모든 write 매핑이 금지된 상태이다.

 

 

mapping_map_writable()

include/linux/fs.h

static inline int mapping_map_writable(struct address_space *mapping)
{
        return atomic_inc_unless_negative(&mapping->i_mmap_writable) ?
                0 : -EPERM;
}

매핑 영역을 writable 공유 매핑 상태로 요청한다. 성공하면 0을 반환하고, 실패하는 경우 -EPERM을 반환한다.

  • mapping->i_mmapwritable을 음수(-)가 아닌한 증가시킨다. 성공한 경우 0을 반환하고 , 이미 음수(-)여서 증가가 불가능한 경우 -EPERM 에러를 반환한다.

 

mapping_unmap_writable()

include/linux/fs.h

static inline void mapping_unmap_writable(struct address_space *mapping)
{
        atomic_dec(&mapping->i_mmap_writable);
}

writable 공유 매핑 영역을 언맵 요청하여 writable 상태로 변경한다. 다시 새로운 writable 매핑 요청을 받을 수 있는 상태이다.

  • mapping->i_mmap_writable을 감소시킨다.

 

mapping_deny_writable()

include/linux/fs.h

static inline int mapping_deny_writable(struct address_space *mapping)
{
        return atomic_dec_unless_positive(&mapping->i_mmap_writable) ?
                0 : -EBUSY;
}

요청 매핑 영역에 대해 denywritable 상태로 변경하여 writable 매핑을 만들 수 없게 금지한다. 성공하면 0을 반환하고, 실패하는 경우 -EBUSY를 반환한다.

  • mapping->i_writecount를 양수(+)가 아닌한 감소시킨다. 성공한 경우 0을 반환하고 , 이미 양수(+)여서 감소가 불가능한 경우 -EPERM 에러를 반환한다.

 

mapping_allow_writable()

include/linux/fs.h

static inline void mapping_allow_writable(struct address_space *mapping)
{               
        atomic_inc(&mapping->i_mmap_writable);
}

요청 매핑 영역에 대해 denywrite를 제거하여 새로운 writable 매핑을 받을 수 있는 상태로 변경한다.

 

vm_stat_account()

mm/mmap.c

void vm_stat_account(struct mm_struct *mm, unsigned long flags,
                                                struct file *file, long pages)
{
        const unsigned long stack_flags
                = VM_STACK_FLAGS & (VM_GROWSUP|VM_GROWSDOWN);

        mm->total_vm += pages;

        if (file) {
                mm->shared_vm += pages;
                if ((flags & (VM_EXEC|VM_WRITE)) == VM_EXEC)
                        mm->exec_vm += pages;
        } else if (flags & stack_flags)
                mm->stack_vm += pages;
}

매핑 용도에 맞게 각각의 vm stat에 대해 pages 만큼 추가한다.

  • 코드 라인 4~5에서 스택영역의 할당 요청인 경우
  • 코드 라인 7에서 mm->total_vm 카운터에 페이지 수를 추가한다.
  • 코드 라인 9~12에서 file 매핑인 경우 mm->shared_vm 카운터에 페이지 수를 추가한다. 만일 실행 및 쓰기 요청이 있는 경우에는 mm->exec_vm 카운터에도 페이지 수를 추가한다.
  • 코드 라인 13~14에서 stack 매핑인 경우 mm->stack_vm 카운터에 페이지 수를 추가한다.

 

vma_set_page_prot()

mm/mmap.c

/* Update vma->vm_page_prot to reflect vma->vm_flags. */
void vma_set_page_prot(struct vm_area_struct *vma)
{
        unsigned long vm_flags = vma->vm_flags;

        vma->vm_page_prot = vm_pgprot_modify(vma->vm_page_prot, vm_flags);
        if (vma_wants_writenotify(vma)) {
                vm_flags &= ~VM_SHARED;
                vma->vm_page_prot = vm_pgprot_modify(vma->vm_page_prot,
                                                     vm_flags);
        }
}

vma->vm_flags에 맞는 메모리 속성 값으로 vma->vm_page_prot 속성 값을 업데이트한다.

  • 코드 라인 6에서 요청 vma 영역의 vm_page_prot 속성에 대해 아키텍처에 커스트마이즈된 캐시 변환이 필요한 경우 변환된 vm_flags 값을 저장한다. 매칭되지 않으면 그냥 vm_flags 속성을 저장한다.
  • 코드 라인 7~11에서 vma가 write notify를 사용하고자 하는 경우 shared 플래그를 제거한다.
    • vma 영역이 shared 매핑이고 페이지들이 read only 설정이된 경우 write 이벤트를 트래킹하고자 할 때 true를 반환한다.

 

vm_pgprot_modify()

mm/mmap.c

static pgprot_t vm_pgprot_modify(pgprot_t oldprot, unsigned long vm_flags)
{
        return pgprot_modify(oldprot, vm_get_page_prot(vm_flags));
}

oldprot에 해당하는 아키텍처별로 미리 정의된 프로토콜 변환이 있으면 변환하여 속성을 반환한다.

  • arm에서는 요청하는 oldprot 속성이 noncache, writecombine, device가 있는 경우 각각 noncache, buffer, noncache 형태로 변환한다. 매칭되지 않는 속성은 newprot 속성을 변환없이 그대로 반환한다.

 

 

pgprot_modify()

include/asm-generic/pgtable.h

static inline pgprot_t pgprot_modify(pgprot_t oldprot, pgprot_t newprot)
{
        if (pgprot_val(oldprot) == pgprot_val(pgprot_noncached(oldprot)))
                newprot = pgprot_noncached(newprot);
        if (pgprot_val(oldprot) == pgprot_val(pgprot_writecombine(oldprot)))
                newprot = pgprot_writecombine(newprot);
        if (pgprot_val(oldprot) == pgprot_val(pgprot_device(oldprot)))
                newprot = pgprot_device(newprot);
        return newprot;
}

아키텍처에서 매핑 시 old 마스크사용하는 odl 캐시 속성을 new 캐시 속성에 맞게 변환한다.

  • arm: no cache 또는 buffer

 

arch/arm/include/asm/pgtable.h

#define __pgprot_modify(prot,mask,bits)         \
        __pgprot((pgprot_val(prot) & ~(mask)) | (bits))
        
#define pgprot_noncached(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED)
          
#define pgprot_writecombine(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE)

#define pgprot_stronglyordered(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED)

#ifdef CONFIG_ARM_DMA_MEM_BUFFERABLE
#define pgprot_dmacoherent(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE | L_PTE_XN)
#else     
#define pgprot_dmacoherent(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED | L_PTE_XN)
#endif

arm 아키텍처에서 매핑 시 사용하는 캐시 속성을 선택한다. (uncached 또는 buffrable)

  • noncache -> no cache 속성 사용
  • writecombine -> buffer 속성 사용
  • stronglyordered -> no cache 속성 사용
  • dmacoherennt -> CONFIG_ARM_DMA_MEM_BUFFERABLE 커널 옵션에 따라 buffer 또는 no cache 사용
    • armv6 또는 armv7에서 dma가 buffer 사용 가능하다.

 

vma_wants_writenotify()

mm/mmap.c

/*
 * Some shared mappigns will want the pages marked read-only
 * to track write events. If so, we'll downgrade vm_page_prot
 * to the private version (using protection_map[] without the
 * VM_SHARED bit).
 */
int vma_wants_writenotify(struct vm_area_struct *vma)
{
        vm_flags_t vm_flags = vma->vm_flags;

        /* If it was private or non-writable, the write bit is already clear */
        if ((vm_flags & (VM_WRITE|VM_SHARED)) != ((VM_WRITE|VM_SHARED)))
                return 0;

        /* The backer wishes to know when pages are first written to? */
        if (vma->vm_ops && vma->vm_ops->page_mkwrite)
                return 1;

        /* The open routine did something to the protections that pgprot_modify
         * won't preserve? */
        if (pgprot_val(vma->vm_page_prot) !=
            pgprot_val(vm_pgprot_modify(vma->vm_page_prot, vm_flags)))
                return 0;

        /* Do we need to track softdirty? */
        if (IS_ENABLED(CONFIG_MEM_SOFT_DIRTY) && !(vm_flags & VM_SOFTDIRTY))
                return 1;

        /* Specialty mapping? */
        if (vm_flags & VM_PFNMAP)
                return 0;

        /* Can the mapping track the dirty pages? */
        return vma->vm_file && vma->vm_file->f_mapping &&
                mapping_cap_account_dirty(vma->vm_file->f_mapping);
}

vma 영역이 shared 매핑이고 페이지들이 read only 설정이된 경우 write 이벤트를 트래킹하고자 할 때 true를 반환한다.

  • 코드 라인 12~13에서 write 및 shared 요청이 없는 vma의 경우 false(0)를 반환한다.
  • 코드 라인 16~17에서 vm_ops->mkwrite 콜백 함수가 지정된 경우 true(1)를 반환한다.
  • 코드 라인 21~23에서 vm_page_prot 속성에 변화를 줄 필요가 없는 경우 false(0)를 반환한다.
  • 코드 라인 26~27에서 CONFIG_MEM_SOFT_DIRTY 커널 옵션을 사용하면서 soft dirty 기능을 요청하지 않은 경우  true(1)를 반환한다.
  • 코드 라인 30~31에서 pfnmap 매핑을 요청한 경우 false(0)을 반환한다.
  • 코드 라인 34~35에서 file 매핑이면서 bdi 에 dirty capable 설정된 경우  true(1)를 반환한다. 그렇지 않으면 flase(0)을 반환한다.

 

참고