/usr/src/linux-2.4.20-8/kernel/sched.c
/*
* context_switch – switch to the new MM and the new
* thread’s register state.
*/
static inline task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
/*
* rq(runqueue_t)
* prev(task_t): 현재 실행되고 있는 process
* next(task_t): scheduling받아 이제 실행 될 process
*/
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if(unlikely(!mm)) //이전 process(prev)와 주소공간 공유
{
next->active_mm = oldmm;
//이전 process(prev)의 active_mm을 갖는다
atomic_inc(&oldmm->mm_count);
//oldmm의 reference count 증가
enter_lazy_tlb(oldmm, next, smp_processor_id()); //TLB설정
}
else //다른 주소공간을 가진 process일 경우
switch_mm(oldmm, mm, next);
//switch_mm을 호출하여 주소공간switch
if(unlikely(!prev->mm)) //현재process(prev)의 mm포인터가 null이라면
{
prev->active_mm = NULL;
//prev의 active_mm을 null로 만들어준다.
rq->prev_mm = oldmm;
}
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev); // CPU에 dependent한 매크로
return prev;
}
이 함수는 schedule() 함수에서 호출된다. scheduler가 다음 수행 될 process를 고르고 나서 호출하는 함수가 바로 이 context_switch함수이다. 현재 수행되고 있는 process의 정보와 다음 수행 될 process의 정보를 argument로 넘겨받는 context_switch함수는 가장먼저 process를 메모리 상에 올리는 일을 수행 한다. scheduling에 의해 선택되어 다음 번에 실행 될 process의 memory management(task_struct안의 mm_struct)가 NULL일 경우에는 수행되던 process의 주소공간을 공유하는 것이므로 이전의 process가 가진 mm_struct를 가지게 되고 mm_strcut의 구조체 포인터를 하나 증가시켜준다. NULL이 아닐 경우에는 수행 되던 process와 주소공간이 다른 것이므로 이 process가 가지고 있던 switch_mm이라는 함수를 호출한다. 여기서 switch_mm이 하는 일은 process의 주소 공간을 교체하는 것이다. 만약 수행중인 process(prev)의 mm이 NULL이라면 다른 누군가와 주소공간을 공유하고 있다는 뜻이다. 주소공간을 공유하더라도 이제 실행되지 않으면 active_mm을 NULL값을 주어 실행되지 않는 process임을 나타낸다. 그리고 runqueue의 prev_mm에 수행되던 process의 active_mm을 저장해 두었던 oldmm값을 넣는다. 이러한 주소공간 switching이 모두 일어난 후에 switch_to라는 매크로를 호출하여 레지스터의 저장과 저장되어있던 레지스터를 불러오는 일을 한다.
/usr/src/linux-2.4.20-8/include/asm-i386/system.h
#define switch_to(prev, next, last) do{
asm volatile(“pushl %%esi\n\t”
“pushl %%edi\n\t”
“pushl %%ebp\n\t” //레지스터 값 저장
“movl %%esp,%0\n\t” /* save ESP */
“movl %2,%%esp\n\t” /* restore ESP */
“movl $1f,%1\n\t” /* save EIP */
“pushl %3\n\t” /* restore EIP */
“jmp __switch_to\n” //__switch_to로 점프
“1:\t”
“popl %%ebp\n\t”
“popl %%edi\n\t”
“popl %%esi\n\t” //레지스터 값 로딩
:”=m” (prev->thread.esp), “=m” (prev->thread.eip)//출력
:”m” (next->thread.exp),”m”(next->thread.eip), “a”(prev), “d” (next)); //입력
}while(0)
이 부분은 CPU에 dependent한 부분이므로 CPU에 따라 달라질 수 있다. 이 매크로가 하는 일은 현재 실행중인 프로세서의 레지스터들을 저장하고 스케줄링 되어 실행 될 프로세서의 레지스터들을 가지고 오는 것이다. 먼저 실행중인 프로세서의 레지스터들을 stack에 저장하는데 그것이 esi, ebp, esp이다. thread_struct라는 struct의 0번째는 esp0가 있는데 이곳에 esp를 저장하고 2번째에 있는 esp값을 esp에 가져다 놓는다. 그리고 1번째에는 eip가 있는데 이 곳에 1f를 저장하여 다음번에 1f라는 곳에서부터 수행할 것을 알려준다. 3번째 값은 fs인데 이곳에 eip를 저장한다. 그리고 나서 __switch_to로 점프뛰는데 이 함수가 실행되고 난 후에는 바뀐 프로세서가 수행될 것이다. context switch가 완전히 다 일어나고 난 후에는 1: 이라고 되있는 leble부터 스케줄링 된 프로세스가 수행된다. 그래서 ebp, edi, esi가 차례대로 복구된다. m은 메모리를 의미하는데 “=”이 출력을, a 또는 d는 eax, edx레지스터를 뜻한다. 그래서 밑의 3줄 소스를 보면 prev(현재 실행중인 프로세스)는 eax레지스터에 저장되고 next(스케줄링 받은 프로세스)는 edx에 저장이 된다.
/usr/src/linux-2.4.20-8/arch/i386/kernel/process.c
void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
int cpu = smp_processor_id();
struct tss_struct *tss = init_tss + cpu;
//현재 실행중인 프로세스 prev의 FPU상태를 저장
/* never put a printk in __switch_to … printk() calls wake_up*() indire ctly*/
unlazy_fpu(prev_p);
/* Reload esp0, LDT and the page table pointer: */
tss->esp0 = next0->esp0;
//스케줄링 받은 프로세스next의 esp0(커널 스택 주소)를 저장
/* Load the per-thread Thread-Local Storage descriptor. */
load_TLS(next, cpu);
/* Save away %fs and %gs. No need to save %es and %ds, as
those are always kernel segments while inside the kernel. */
asm volatile(“movl %%fs, %0”: “=m” (*(int *)&prev->fs));
asm volatile(“movl %%gs, %0”: “=m” (*(int *)&prev->gs));
//prev 프로세스의 fs와 gs레지스터 저장
/* Restore %fs and %gs if needed. */
if(unlikely(prev->fs | prev->gs | next->fs | next->gs)){
loadsegment(fs, next->fs);
loadsegment(gs, next->gs);
} //next 프로세스의 fs와 gs 레지스터 저장
/* Now maybe reload the debug registers */
if(unlikely(next->debugreg[7])) {
loaddebug(next, 0);
loaddebug(next, 1);
loaddebug(next, 2);
loaddebug(next, 3);
/* no 4 and 5 */
loaddebug(next, 6);
loaddebug(next, 7);
} //디버그 레지스터 로딩
if (unlikely(prev->ioperm || next->ioperm))
//IOperm시스템 콜일 경우에만 해당.
{
if(next->ioperm) {
memcpy(tss->io_bitmap, next->io_bitmap,
IO_BITMAP_SIZE*sizeof(unsigned long));
tss->bitmap = IO_BITMAP_OFFSET;
} else
tss->bitmap = INVALID_IO_BITMAP_OFFSET;
}
}
이 함수가 처음 하는 일은 현재 실행중인 프로세스의 상태를 저장하는 것이다. 그리고 스케줄링 받은 프로세서의 커널 스택 주소를 저장하는데 이 주소는 tss_struct라는 구조체의 esp0필드에 저장되어야 한다. 이 곳에서 CPU가 사용하는 커널 스택 주소가 결정되기 때문이다. 그 후에는 현재 수행중인 프로세스의 fs, gs 레지스터를 저장한다. fs와 gs는 데이터 이동시 사용자 세그먼트와 커널 세그먼트를 전환하는데 사용한다. 현재 수행중인 프로세스의 레지스터를 저장한 후에 할일은 당연히 다음 수행 될 프로세스의 레지스터를 로딩하는 것이다. 그리하여 fs, gs 레지스터를 로딩시키고 마지막으로 디버그 레지스터들을 로딩시킨다. 만약 프로세스가 IOperm시스템콜을 호출하였다면 그 프로세스가 I/O bitmap을 가지고 있는지 아닌지를 검사한다. 다음 수행될 프로세스가 I/O bitmap을 가지고 있다면 tss로 unsigned long크기만큼의 byte를 복사하고 그렇지 않다면 port 제어 명령어를 사용할 수 없다는 플래그를 설정한다.