ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Scheduler - process switching
    Linux/kernel 2023. 11. 21. 15:53

    글의 참고

    - http://www.wowotech.net/process_management/context-switch-arch.html

    - https://www.kernel.org/doc/gorman/html/understand/understand006.html 


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Overview

    " 대학생 시절, 운영 체제 시간에 context 라는 단어가 감이 잡히질 않았다(사실, 지금도 모름). 그런데, 거기다가 context switch 라는 말은 더 이해가 안갔고, 한국어로 번역하면 `문맥 교환` 인데, 정말 최악이라 생각했다. 개인적으로 이걸 알려면, 이 용어를 만들어논 놈들의 코드를 뜯어보면 알 수 있을 것 같다. 그래서, 이 글은 arm64 에서 context switch 가 어떤식으로 발생하는지에 대해 알아볼 것이다.

     

     

     

    - Context swtich

    " 이 글에서 설명할 context switch 는 2 가지다.

    1. process switching
    2. process address space switching
    // kernel/sched/core.c - v4.4.6
    /*
     * context_switch - switch to the new MM and the new thread's register state.
     */
    static inline struct rq *
    context_switch(struct rq *rq, struct task_struct *prev,
    	       struct task_struct *next) // --- 1
    {
    	struct mm_struct *mm, *oldmm;
    
    	prepare_task_switch(rq, prev, next);
    
    	mm = next->mm;
    	oldmm = prev->active_mm; // --- 2
    	/*
    	 * For paravirt, this is coupled with an exit in switch_to to
    	 * combine the page table reload and the switch backend into
    	 * one hypercall.
    	 */
    	arch_start_context_switch(prev); 
    
    	if (!mm) { // --- 3
    		next->active_mm = oldmm;
    		atomic_inc(&oldmm->mm_count); 
    		enter_lazy_tlb(oldmm, next); // --- 4
    	} else
    		switch_mm(oldmm, mm, next); // --- 5
    
    	if (!prev->mm) { // --- 6
    		prev->active_mm = NULL;
    		rq->prev_mm = oldmm;
    	}
    	/*
    	 * Since the runqueue lock will be released by the next
    	 * task (which is an invalid locking op but in the case
    	 * of the scheduler it's an obvious special-case), so we
    	 * do an early lockdep release here:
    	 */
    	lockdep_unpin_lock(&rq->lock);
    	spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
    
    	/* Here we just switch the register state and the stack. */
    	switch_to(prev, next, prev); // --- 7
    	barrier();
    
    	return finish_task_switch(prev);
    }
    1. scheduler 를 통해 prev task 와 next task 를 결정되면, 실제 process switching 을 수행하기 위해서 context_swithc() 함수가 실행된다. 이 때, 전달되는 인자는 총 3개가 있다.
    1. rq : 멀티 프로세서 아키텍처에서는 기본적으로, 각 코어별로 process switching 이 독립적으로 발생한다. 이게 가능하려면, 각 코어별로 프로세스를 관리하는 `런큐(rq)` 를 가지고 있어야 한다.

    2. prev : 현재 실행중이지미나, 곧 선점당할 프로세스

    3. next : prev 를 선점해서 이제 곧 실행될 프로세스 

     

    2. `struct task_struct.mm` 필드는 process 가 할당받은 address space space 를 나타낸다. `struct task_struct.active_mm` 필드는 process 가 현재 사용중인 address space space 를 나타낸다. normal process 같은 경우는 mm 과 active_mm 이 동일하다. 즉, 2개 모두 process address space 를 가리킨다. 그러나, kernel thread 같은 경우는, mm 필드가 NULL 이다. 왜냐면, kernel thread 에는 process address space 라는 존재하지 않기 때문이다. 그렇기 때문에, 만약 kernel thread 가 scheduling 될 경우, active_mm 은 prev process 에게 빌린 process address space 를 가리키게된다.

    3. mm 이 NULL 이라는 것은 next process 가 kernel thread 임을 나타낸다. 그렇다면, address space 를 빌려야 한다. 누구한테 빌릴까? 현재 실행중인(prev process) process 에게 빌리는 것이다. 그런데, 궁금한 점이 있다. address space 를 빌릴 때, prev->mm 에서 빌릴 수는 없을까? prev process 도 kernel thread 일 수 있기 때문에, `prev->mm` 을 사용할 수는 없다.

    4. lazy TLB mode 는 무슨뜻일까? 만약, 다음에 실행 될 process 가 kernel thread 라면, 당분간은 TLB flush 를 할 필요가 없다. 왜냐면, kernel thread 는 userspace 에 액세스할 수 없기 때문에, 남아있는 user space TLB entries 는 kernel thread 에 영향을 미치지 않을 것이다. 그러므로, 굳이 TLB flush 를 진행할 필요가 없다.

    " 이런 경우에 performance 를 향상 시키기 위해 `lazy TLB mode` 로 진입한다. 즉, 이 kernel thread 가 실행되는 동안에는 lazy TLB mode 로 동작한다는 것이다. 자세한 내용은 이 글을 참고하자.

    5. next process 가 kernel thread 가 아니면, 일반적인 user process 임을 의미한다. user process 는 자신만의 user space 를 갖기 때문에 `switch_mm` 함수를 호출해서 process address space switching(page table 교체) 을 할 필요가 있다.

    " 그런데, 이 코드는 주의가 필요하다. 왜냐면, 만약 process A 에서 process B 로 전환한다고 가정할 때, switch_to() 함수를 호출하기전에 switch_mm() 함수를 호출하고 있다. 즉, 실제 process 가 바뀌지도 않았는데, address space 부터 먼저 바꾸고 있다. 이거 문제 없을까? 생각해보자. 지금 코드가 실행되는 위치는 kernel address space 다. 모든 processes 들의 kernel address space 는 동일하다고 했다. 즉, process address space 가 switched 되더라도, kernel address spac e 는 변하지 않는다. 

    6. kernel thread A -> process B 로 switching 되는 상황을 가정하자. kernel thread 의 mm(prev->mm) 은 default 로 NULL 이기 때문에, if 문안으로 진입하게 된다. kernel thread 의 active_mm 은 자신 때문에 switched-out 된 process 의 kernel address space 를 의미한다. 그러나, 이제는 더 이상 필요가 없으므로, 해제를 준비한다(prev->active_mm = NULL). 이후에 내용은 뒤에서 다시 설명하도록 한다.

    7. process switch 는 3 단계를 나뉜다. 예를 들어, process A -> process B -> ... 시간이 흘러 ... -> process X -> process A 순으로 context switch 가 발생한다고 가정하자. 이 때, 우리의 기준은 `process A` 다. 즉, A 가 suspend 되는 시점과 resume 되는 시점에 이미 2번 의 process switch 가 발생한다. 그런데, 하나는 또 뭘까? 바로 `process B -> ... -> process X` 다. 직접적으로 A 와 관련이 없지만, 이 과정이 없다면 A 는 resume 될 수 없다. 이 과정은 뒤에서 자세히 알아보도록 한다.

     

     

    " 그런데, 왜 swtich_to() 함수에는 3개의 인자나 필요한 것일까? 현재 프로세스와 다음에 실행될 프로세스만 있으면 되는거 아니었나?

    // include/asm-generic/switch_to.h - v4.4.6
    /*
     * Context switching is now performed out-of-line in switch_to.S
     */
    extern struct task_struct *__switch_to(struct task_struct *,
    				       struct task_struct *);
    
    #define switch_to(prev, next, last)					\
    	do {								\
    		((last) = __switch_to((prev), (next)));			\
    	} while (0)

     

     

    " switch_to() 함수는 두 개의 파트로 나눠볼 수 있다. 바로, switch_to() 함수를 호출하기 `전` 과 `후` 로 나눠볼 수 있다.

     

     

    " 그리고, switch_to() 함수를 호출할 경우, 실제로는 1개의 프로세스당 context switch 는 2번 발생하게 된다.

    1. P1 이 switch out 될 때 : 위 그림에서 파란색
    2. P1 이 switch into 될 때 : 위 그림에서 붉은색

     

     

    " 예를 하나 들어보자. 싱글 코어 시스템에 3개의 프로세스가 있다고 가정하자(1, 2, ?). process 1 가 time slice 를 모두 소진하여, 스케줄러에 의해서 선점당할 시점이 됬다. 이 때, switch_to() 함수를 통해 process 1 에서 process 2 로 실행 흐름이 전환되었다. 그러다가, 시간이 흐르고, process 1 가 다시 스케줄러에 의해서 resume 이 되었다. 그렇다면, process 1 를 실행시킨 process 누구일까? 물론, process 2 일 수 있다. 그러나, 일반적으로 그럴 확률은 적다. 그렇기 때문에, process 1 과 process 2 가 아닌 모든 process 를 process ? 라고 부르자. 그렇다면, 확률적으로 process 1 가 resume 되었을 때, 이전 process 는 process ? 일 확률이 높다. 바로, 이 process ? 가 __switch_to() 매크로 함수의 last 변수를 의미한다.

     

     

    " 그런데, switch_to() 함수에 prev 가 2개가 들어간다. 매크로 전처리 과정을 끝내면, prev 를 overwrite 하는 것을 볼 수 있다.

     

    " 그렇다면, last process 는 왜 필요할까? 사용이 끝난 process(last) 의 address space 는 제거해야 할 필요가 있다. 다음 섹션에서 자세히 다루도록 한다.

     

     

     

     

    - process address space switching

     

    " 위에서 언급했다시피, kernel thread 로 switching 될 경우, 실제적으로 process address space 는 switching 되지 않는다. kernel thread 는 이전 process 의 address space(active_mm) 에 기생해서 살아간다. kernel space 로 내려오게 되면 메모리 관리가 상당히 중요하기 때문에, kernel space 에 존재하는 일부 객체들은 `reference count` 라는 필드를 가지고 있다. 만약, 객체에 대한 reference 가 없을 경우(reference count == 0), 해당 객체를 release 시킬 수 있다. memory descriptor 에서 이와 비슷한 개념이 사용된다. 우리는 `context_switch` 함수에서 아래와 같은 내용을 봤었다.

    // kernel/sched/core.c - v4.4.6
    /*
     * context_switch - switch to the new MM and the new thread's register state.
     */
    static inline struct rq *
    context_switch(struct rq *rq, struct task_struct *prev,
    	       struct task_struct *next)
    {
    	....
    	if (!mm) {
    		next->active_mm = oldmm;
    		atomic_inc(&oldmm->mm_count);
    		enter_lazy_tlb(oldmm, next);
    	} 
        	....
    }

     

    " kernel thread 가 다른 process 의 memory 에 기생해서 동작한다는 것은 결국, 해당 memory region 은 reference 한다는 뜻이 된다. 그러므로, 이전 process address space 의 reference count 를 증가시키는 것(`atomic_inc`)은 reasonable 한 것으로 보인다.

     

    " 그런데, 이렇게 kernel thread 가 process 의 address space 를 빌려쓰는 것에 고민해볼 필요가 있다. 예를 들어, kernel thread 는 process address space 를 빌려쓰기전에 미리 reference count 를 증가시킨다. 당연하다. 왜냐면, 이제 곧 사용할 것이기 때문에, 미리 증가시키고 memory 를 사용하는 것이다. 그렇다면, memory 를 모두 사용한 후에는 어떻게 해야할까? 당연히, reference count 를 감소시켜야 한다. 그런데, 잘 보면 kernel thread 가 memory 를 사용하고 있는 도중에는 reference count 를 증가 및 감소시킬 수 가 없다. 즉, kernel thread 스스로 정해진 시간안에 일을 마무리해서 메모리를 반납한 뒤 reference count 를 감소시키면 모를까, 일이 다 끝나지도 않았는데, 다른 scheduler 에게 선점될 가능성도 매우 높아서 메모리를 반납할 타이밍을 잡을 수 가 없는 것이다. 결국, 외부에서 kernel thread 의 메모리를 반납시켜줄 알고리즘이 필요하다.

     

    " 시나리오를 하나 만들어보자. 다음와 같은 경우, X 의 address space 는 X 가 context_switch() 함수를 호출하는 시점에 reference count 가 증가한다. 왜냐면 kernel thread Y 가 X 의 address space 를 사용할 것이기 때문이다. 그렇다면, X address space 의 reference count 는 언제 감소시켜야 할까? 이 코드 또한  context_switch() 함수에 있다.

    X -> Y (kernel thread) -> Z

     

     

    " kernel thread Y 가 X 의 memory 를 빌려서 열심히 작업을 수행중이다. 이 때, 자발적이든 강제적으로든 scheduling 이 발생하게 된다. scheduling 이 일어나기 위해서는 반드시 context_switch() 함수가 호출되야 하고, 이 시점에 `prev` 에는 kernel thread Y, `next` 에는 process Z 가 들어오게 될 것이다(참고로, kernel thread 의 mm(prev->mm) 은 default 로 NULL 이다). 아래 `prev->active_mm = NULL` 은 kernel thread Y 가 더 이상 X 의 memory 를 사용하지 않는다는 것을 의미하고, `rq->prev_mm = oldmm` 은 다음에 실행 될 프로세스(Z)가 X 의 memory 를 정리할 수 있도록 해당 reference 를 저장해놓는 것을 의미한다. rq 는 CPU 당 한개만 존재하고, kernel thread Y 와 process Z 는 동일한 CPU 에서 실행되기 때문에, rq 를 통해 데이터를 공유할 수 있다. X 의 memory 를 해제할 준비는 끝났다. 그렇다면, 도대체 언제 release 하는 것이냐?

    // kernel/sched/core.c - v4.4.6
    /*
     * context_switch - switch to the new MM and the new thread's register state.
     */
    static inline struct rq *
    context_switch(struct rq *rq, struct task_struct *prev,
    	       struct task_struct *next)
    {
    	....
    	if (!prev->mm) {
    		prev->active_mm = NULL;
    		rq->prev_mm = oldmm;
    	}
        	....
    }

     

     

    " 바로 `finish_task_switch` 함수에서 X 의 memory 를 free 한다. 이 함수는 process Z 에 의해서 호출된다.

    // kernel/sched/core.c - v4.4.6
    /**
     * finish_task_switch - clean up after a task-switch
     * @prev: the thread we just switched away from.
     *
     * finish_task_switch must be called after the context switch, paired
     * with a prepare_task_switch call before the context switch.
     * finish_task_switch will reconcile locking set up by prepare_task_switch,
     * and do any other architecture-specific cleanup actions.
     *
     * Note that we may have delayed dropping an mm in context_switch(). If
     * so, we finish that here outside of the runqueue lock. (Doing it
     * with the lock held can cause deadlocks; see schedule() for
     * details.)
     *
     * The context switch have flipped the stack from under us and restored the
     * local variables which were saved when this task called schedule() in the
     * past. prev == current is still correct but we need to recalculate this_rq
     * because prev may have moved to another CPU.
     */
    static struct rq *finish_task_switch(struct task_struct *prev)
    	__releases(rq->lock)
    {
    	struct rq *rq = this_rq();
    	struct mm_struct *mm = rq->prev_mm; // --- 1
    	....
    
    	rq->prev_mm = NULL;
    	....
    	
    	if (mm)
    		mmdrop(mm); // --- 2
    	....
        
    	return rq;
    }
    1. kernel thread Y 가 context_switch() 함수를 호출할 때, X 에게 빌린 address space 를 현재 CPU 의 run-queue 에 저장했다. 동일 CPU 에서 process Z 로의 switching 이후, run-queue 에는 kernel thread Y 의 address space 가 들어있게 된다. 이제 process Z 가 이걸 release 해줄 일만 남았다.

    2. process Z 로의 switching 이 완료되면, kernel thread Y 가 빌린 X 의 address space 는 release 해야 한다. 왜냐면, 이제 더 이상  X 의 address space 를 사용할 필요가 없기 때문이다.

     

     

     

     

    (2) process address switching in arm64

     

    " arm64 아키텍처에서는 현재 CPU 코어에서 실행중 인 process 의 address space 를 나타내기 위해 CPU 코어별로 이와 관련된 2 개의 레지스터를 가지고 있다. 참고로, 모든 processes 들은 kernel address space 를 공유하기 때문에, 흔히 `process address space switching` 이라고 부르는 것은 TTR0_EL1 를 switching 하는 것을 의미한다.

    1. TTBR0_EL1 - user address space
    2. TTBR1_EL1 - kernel address space

     

    " 대학교 운영체제 시간에 우리는 paging 이라는 메모리 관리 기법에 대해 배운다. 거기서 하나의 page frame 은 4KB address space 를 차지하며, 이게 1024 개가 모여, 4MB 가 된다. 즉, page table(VA->PA) 한 개는 4MB address space 를 표현할 수 있다는 뜻이다. 그리고, address space 는 결국 page table 과 표현된다고 볼 수 있다. process 마다 자신의 user space virtual address 를 translating 해줄 수 있는 다수의 page table`s 을 가지고 있다. 그리고, 이러한 page table`s 들의 주소는 page directory 에 저장되고, page directory 의 physical address 는 `struct mm_struct` 구조체의 `pgd(physical global directory)` 필드에 저장된다[참고1]. 정리하면, process 마다 별도의 pgd 를 가지고 있고, process address space switching 의 실체는 TTBR0_EL1 레지스터에 pgd 를 write 함으로써, new process 의 memory 가 active 된다고 볼 수 있다.

     

    // arch/arm64/include/asm/mmu_context.h - v4.4.6
    /*
     * This is the actual mm switch as far as the scheduler
     * is concerned.  No registers are touched.  We avoid
     * calling the CPU specific function when the mm hasn't
     * actually changed.
     */
    static inline void
    switch_mm(struct mm_struct *prev, struct mm_struct *next,
    	  struct task_struct *tsk) // --- 1
    {
    	unsigned int cpu = smp_processor_id();
    
    	if (prev == next) // --- 2
    		return;
    
    	/*
    	 * init_mm.pgd does not contain any user mappings and it is always
    	 * active for kernel addresses in TTBR1. Just set the reserved TTBR0.
    	 */
    	if (next == &init_mm) { // --- 3
    		cpu_set_reserved_ttbr0();
    		return;
    	}
    
    	check_and_switch_context(next, cpu); // --- 4
    }
    1. `prev` 는 swithed out 될 address space 를 의미하고, `next` 는 switched into 될 address space 를 의미한다. 그리고, `tsk` 는 이제 scheduling 될 process 를 의미하낟(`next` address space 를 소유한 process). 

    2. swithed out 될 address space 와 switched into 될 address space 가 같다면, memory 를 switch 할 필요가 없으니, return 한다.

    3. arm64 에서 address space switching 은 TTBR0_EL1 을 switching 하는 것이라고 언급했었다. 리눅스에서 `swapper process` 는 user space 에 대한 어떠한 mapping 관계도 갖고 있지 않다. 그런데, 만약 switch_mm() 함수에 swapper process 의 address space(`next`) 가 전달될 경우(`next == init_mm`), 즉, swapper process 가 실행되어야 할 경우, TTBR0_EL1 에 empty_zero_page 를 mapping 한다(커널 주석에 설명이 잘 되어있다).
    // arch/arm64/mm/mmu.c - v4.4.6
    /*
     * Empty_zero_page is a special page that is used for zero-initialized data
     * and COW.
     */
    struct page *empty_zero_page;
    // arch/arm64/include/asm/mmu_context.h - v4.4.6
    /*
     * Set TTBR0 to empty_zero_page. No translations will be possible via TTBR0.
     */
    static inline void cpu_set_reserved_ttbr0(void)
    {
    	unsigned long ttbr = page_to_phys(empty_zero_page);
    
    	asm(
    	"	msr	ttbr0_el1, %0			// set TTBR0\n"
    	"	isb"
    	:
    	: "r" (ttbr));
    }​


    4. check_and_context_switch() 함수는 TLB 와 ASID 관련 정보들을 설정하는 함수다(이 함수는 이 글에서 자세히 다룬다). 이 함수는 최종적으로는 `cpu_swtich_mm() -> cpu_do_switch_mm()` 함수를 호출해서, level 0 translation table 의 물리적 주소(`struct mm_struct mm.pgd`)를 TTBR0_EL1 레지스터에 write 한다.

     

     

     

     

    - process switching in arm64

    " 내가 context switch 를 공부할 때, 가장 궁금했던 점이 하나있다.

     

    뭘 저장해야 하지?

     

    " context switch 시에 저장해야 하는건 뭐가 있을까? 사실, 정답이 없다. 대개, 아키텍처에 상당히 의존적이고, 운영 체제마다도 상당히 다르다. 그래서 CPU 아키텍처 문서를 찾아봤다. x86 문서에서는 본적이 없는데, arm64 에는(권고 수준에서) 어느 정도 구체적인 context switch 정보들을 언급해준다.

    Exactly what has to be saved and restored varies between different operating systems, but typically a process context switch includes saving or restoring some or all of the following elements:

    • general-purpose registers X0-X30.
    • Advanced SIMD and Floating-point registers V0 - V31.
    • Some status registers. • TTBR0_EL1 and TTBR0.
    • Thread Process ID (TPIDxxx) Registers.
    • Address Space ID (ASID).

    - 참고 : ARM Cortex-A Series Programmer’s Guide for ARMv8-A

     

     

    " 이제 진짜 코드를 분석해보자.

    // arch/arm64/kernel/process.c - v4.4.6
    /*
     * Thread switching.
     */
    struct task_struct *__switch_to(struct task_struct *prev,
    				struct task_struct *next)
    {
    	struct task_struct *last;
    
    	fpsimd_thread_switch(next); // --- 1
    	tls_thread_switch(next); // --- 2
    	hw_breakpoint_thread_switch(next);
    	contextidr_thread_switch(next); // --- 3
    
    	/*
    	 * Complete any pending TLB or cache maintenance on this CPU in case
    	 * the thread migrates to a different CPU.
    	 */
    	dsb(ish);
    
    	/* the actual thread switch */
    	last = cpu_switch_to(prev, next); // --- 4
    
    	return last;
    }
    1. 현재 process 의 floating-point(소수점) 와 멀티 미디어 및 signal processing 과 관련있는 SIMD(Single Instruction Multiple Data) 의 상태(struct task_struct -> struct thread_struct -> struct fpsimd_state)를 스택에 저장한다. (fpsimd_thread_switch -> fpsimd_save_state -> fpsimd_save)

    2. tls 는 `thread local storage` 를 의미한다. 결국, 이 정보도 저장을 해야한다는 것인데, 이와 관련된 레지스터로는 TPIDR_EL0 과 TPIDRRO_EL0 가 있고, 관련된 데이터는 tp_value 가 있다(struct task_struct -> struct thread_struct -> unsigned long tp_value). tls 와 관련된 내용은 thread library 를 찾아봐야 한다. 별도의 글에서 다루도록 한다.

    3. CONTEXTIDR_EL1(context identifier) 레지스터에 다음에 실행될 process 의 process number 를 설정한다[참고1]. 

     

     

    (4) 이제 진짜 process switching 준비가 끝났다.

    // arch/arm64/kernel/entry.S - v4.4.6
    /*
     * Register switch for AArch64. The callee-saved registers need to be saved
     * and restored. On entry:
     *   x0 = previous task_struct (must be preserved across the switch)
     *   x1 = next task_struct
     * Previous and next are guaranteed not to be the same.
     *
     */
    ENTRY(cpu_switch_to)
    	mov	x10, #THREAD_CPU_CONTEXT // --- 1
    	add	x8, x0, x10 // --- 2
    	mov	x9, sp
    	stp	x19, x20, [x8], #16		// store callee-saved registers --- 3
    	stp	x21, x22, [x8], #16
    	stp	x23, x24, [x8], #16
    	stp	x25, x26, [x8], #16
    	stp	x27, x28, [x8], #16
    	stp	x29, x9, [x8], #16
    	str	lr, [x8]
    	add	x8, x1, x10 // --- 4
    	ldp	x19, x20, [x8], #16		// restore callee-saved registers --- 5
    	ldp	x21, x22, [x8], #16 
    	ldp	x23, x24, [x8], #16
    	ldp	x25, x26, [x8], #16
    	ldp	x27, x28, [x8], #16
    	ldp	x29, x9, [x8], #16
    	ldr	lr, [x8]
    	mov	sp, x9
    	ret // --- 6
    ENDPROC(cpu_switch_to)
    1. x0 은 prev task 를 의미하고, x1 은 next task 를 의미한다. context switch 는 주로 general purpose register 를 저장하는 작업이 많다. 여기서 주의할 점은 AAPCS 문서에 따르면, callee-saved registers 와 caller-saved register 로 나뉜다는 것이다. 즉, 함수를 호출을 당한 측에서 저장해야 할 레지스터와 함수를 호출한 측에서 저장해야 할 레지스터가 나눠져 있다. arm 같은 경우는 x19 ~ x28 레지스터까지는 callee-saved registers 에 속한다. callee or caller 관계없이 함수 호출전과 과정중에 레지스터를 저장하는 이유는 이전 컨택스트를 잃지 않게 하기 위해서다.

    2. 아래에서 볼 수 있다시피, THREAD_CPU_CONTEXT 는 sizeof(thread.cpu_conext) 로 되어있고, cpu_context 구조체의 사이즈는 `8 * 13` 으로 104 가 된다.
    // arch/arm64/kernel/asm-offsets.c - v4.4.6
    DEFINE(THREAD_CPU_CONTEXT,	offsetof(struct task_struct, thread.cpu_context));​
    // arch/arm64/include/asm/processor.h - v4.4.6
    struct cpu_context {
    	unsigned long x19;
    	unsigned long x20;
    	unsigned long x21;
    	unsigned long x22;
    	unsigned long x23;
    	unsigned long x24;
    	unsigned long x25;
    	unsigned long x26;
    	unsigned long x27;
    	unsigned long x28;
    	unsigned long fp;
    	unsigned long sp;
    	unsigned long pc;
    };​



    " 그렇다면, x8 레지스터는 어떤 주소를 가리키고 있을까? 결국, `x8 = struct task_struct + THREAD_CPU_CONTEXT` 다. 그렇다면, struct task_struct.stack 어딘가를 가리키고 있다는 뜻이된다. 뒤에서 다시 알아본다.
    // include/linux/sched.h - v4.4.6
    struct task_struct {
    	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    	void *stack;
            ....
    };


    3. stp 명령어는 `sp` 가 가리키는 곳에 데이터를 저장하는게 아닌, 세 번째 인자가(x8) 이 가리키고 있는 주소에 데이터를 저장하는 것을 의미한다. 네 번째 인자는 16 바이트를 의미한다. sp 는 x9 에, 이전 주소(pc) 는 lr 에, 그렇다면, fp 는 어디에 있을까? fp 는 arm64 에서 x29 레지스터를 의미한다. 이 과정이 끝나면, prev process 의 스택은 다음과 같다.

    4. prev process 의 context 를 저장할 때와 완전 동일하다. prev process 에서는 x8 이 prev process 의 cpu_context 주소를 가리켰다면, 현재는 next process 의 cpu_contex 주소를 가리킨다.

    5. (3) 단계와 동일하다. 그러나, 여기는 save 가 아닌, restore 다. 즉, next process context 의 restore 작업을 하는 단계다.

    6. `ret` 명령어를 실행하면, lr 레지스터에 cpu_switch_to() 함수를 호출하기 전 주소(lr 레지스터에 저장되어 있음)가 PC 포인터에 로드된다. 그리고, 이 시점에는 next process 의 컨택스트가 완전히 복구되서 실행 가능한 상태를 의미한다.
Designed by Tistory.