Cortex-M 문맥전환

프로세서는 Cortex-M을 가정한다. 실행모드는 특권모드와 일반모드 두가지로 각각 커널모드와 유저모드로 부른다. 그외에 인터럽트 발생시 핸들러 실행 상태가 있다. 따라서 문맥전환은 다음과 같이 일어날 수 있다:

  1. 유저 태스크 <-> 유저 태스크
  2. 커널 태스크 <-> 커널 태스크
  3. 유저 태크스 <-> 핸들러
  4. 커널 태스크 <-> 핸들러

스케줄러는 하나의 시스템 콜로서 우선순위가 가장 낮은 예외로 다른 핸들러를 선점하지 못하도록 한다. 따라서 3,4 번의 경우는 일어나지 않는다.

그리고 시스템(커널) 스택과 유저 스택, 두개의 스택이 사용된다. 스택은 다음과 같이 사용될 수 있다:

  1. 특권모드 - 커널스택 또는 유저스택
  2. 유저모드 - 유저스택
  3. 핸들러 - 커널스택

문맥을 각 태스크의 스택에 저장하므로 문맥전환 진입/탈출시에 현재 태스크의 모드와 사용 스택을 확인해야 한다. 진입시에 사용 스택을 확인해 해당 스택에 문맥을 저장한다. 탈출시에 새로 전환될 태스크의 모드를 확인하고 해당 모드와 스택으로 복귀한다.

Cortex-M 에는 MSP, PSP 두개의 스택 포인터가 있어서 예외 탈출시 어느 스택을 사용할지 결정해야 하는데, 이는 예외 진입시 LR 레지스터에 써둔 EXC_RETURN 값으로 결정된다:

0xfffffff1 - MSP를 사용해 핸들러 모드로 복귀
0xfffffff9 - MSP를 사용해 스레드 모드로 복귀
0xfffffffd - PSP를 사용해 스레드 모드로 복귀

3번째 비트(0x4)가 1이면 PSP, 0이면 MSP

Cortex-M에서 하나 참고할 만한 것이 비특권 모드에서 스택 접근은 sp로만 가능하다. 처음에 psp로 접근했는데 0으로만 읽히는 데다 쓰이지가 않아서 헤맸다.

예외 발생시 프로세서는 아래 레지스터 셋을 자동으로(하드웨어적으로) “현재” 스택에 저장하고, 탈출시에는 반대로 스택에서 꺼내 레지스터를 복구한다(프로그래밍 매뉴얼에 나온 순서와 달라 좀 고생).

 -----
| PSR |  |
| PC  |  |
| LR  |  | stack
| R12 |  |
| R3  |  v
| R2  |
| R1  |
| R0  |
 -----

스택에 저장되는 전체 레지스터 셋은 아래와 같다:

 __________
| psr      |  |
| pc       |  | stack
| lr       |  |
| r12      |  v
| r3       |
| r2       |
| r1       |
| r0       |
 ----------
| r4 - r11 | <- by software
 ----------

인터럽트가 서로 중첩되지(선점하지) 않도록 우선순위가 설정되어 있다(디폴트임). 적어도 스케줄러(스케쥴링 진입점인 pendSV의) 우선순위를 가장 낮게 설정해 다른 인터럽트를 선점하는 일이 없도록 한다. 문맥전환이 다른 인터럽트를 선점한 상태에서 이루어지면 EXC_RETURN을 비롯한 문맥의 불일치가 발생한다. 또는 핸들러 코드가 복잡해진다(오버헤드가 커진다).

실제 코드는 다음과 같다:

#define context_save(sp)                                \                                           
    	__asm__ __volatile__(                           \                                           
            	        "mrs    r12, psp        \n\t"   \                                           
                    	"tst    lr, #4          \n\t"   \                                           
                        "it     eq              \n\t"   \                                           
    	                "mrseq  r12, msp        \n\t"   \                                           
            	        "stmdb  r12!, {r4-r11}  \n\t"   \                                           
                    	"mov    %0, r12         \n\t"   \                                           
                        : "=&r"(sp) :: "memory")                                                    
                                                                                                
#define context_restore(sp)                             \                                           
        __asm__ __volatile__(                           \                                           
    	                "ldmia  %0!, {r4-r11}   \n\t"   \                                           
            	        "tst    %1, %2          \n\t"   \                                           
                        "bne    1f              \n\t"   \                                           
    	                "ldr    lr, =0xfffffffd \n\t"   \                                           
            	        "msr    psp, %0         \n\t"   \                                           
                    	"mov    r3, #1          \n\t"   \                                           
                        "msr    control, r3     \n\t"   \                                           
    	                "b      2f              \n\t"   \                                           
            	"1:"    "ldr    lr, =0xfffffff9 \n\t"   \                                           
                        "msr    msp, %0         \n\t"   \                                           
    	                "mov    r3, #0          \n\t"   \                                           
            	        "msr    control, r3     \n\t"   \                                           
                "2:"                                    \                                           
    	                :: "r"(sp),                     \                                           
            	           "r"(get_task_state(current)),\                                           
                    	   "I"(TASK_KERNEL)             \                                           
                        : "memory"                      \                                           
    	)

핸들러 진입시 사용중이던 스택에 나머지 문맥을 저장하고, 핸들러 탈출시 태스크 종류에 따라 특권모드 또는 일반모드로 복귀한다.

추가: 핸들러 모드에서만 커널스택 사용. 커널 태스크나 유저 태스크 모든 태스크에서는 유저스택 사용으로 변경.

각 태스크별로 하나의 커널스택과 유저스택을 갖는다. 커널 태스트, 유저 태스크 모두 유저스택(psp) 사용. 핸들러(인터럽트) 문맥에서만 커널스택(msp)을 사용한다. 시스템 콜과 문맥전환은 최하위 우선순위로 일반모드에서만 발생할 수 있다. 스케줄러가 최하위 우선순위를 취함으로서 진입시에 사용된 스택은 항상 유저스택임이 보장된다. 따라서 유저 태스크의 인터럽트 진입점인 시스템 콜과 문맥전환에서는 유저스택만 고려하면 된다. 인터럽트 진입 후 스택은 커널스택이 사용되고 스택 포인터는 인터럽트 중첩이 아니라면 항상 top을 가리킨다.