1. 구현해야할 것들
- argument passing
C에서 main 함수는 argc, argv 인자를 받을 수 있다. 실행명령어 + 파일명 + 인자들 이런식으로 프로그램을 실행시킬 때 파일명과 인자들을 OS 함수 내에서 파싱하여 해당 프로그램의 유저 프로세스를 생성 및 실행한다. - system call
여러가지 시스템콜 함수들을 구현한다. fork 함수가 가장 까다로웠다. fork는 다른 사람이 한 걸 참고하여 이해하는 방식으로 진행했다.
2. PintOS에서의 스레드와 프로세스
- 위 그림과 같이 PintOS에서의 thread는 kernel thread를 지칭한다. 메모리 시작부분에 struct thread 구조체가 할당되어 있고 interrupt frame이 들어있다. 커널 스택도 가지고 있다.
- load함수를 수행하면 유저영역 페이지를 할당한다. (유저 프로세스 생성)
- x86 아키텍처에는 Privilege level 이라는 개념이 있다. PL = 0 supervisor level 부터 PL = 3 user level 까지 있으며 유저영역에서 하드웨어에 접근하는 인스터럭션을 수행하지 못하도록 protection 하는 개념이다.
- 위 파란색글씨 userland, kernelland의 의미는 엄밀히 말하면 PL = 3 에서 실행중인 구간을 userland, PL = 0 에서 실행중인 구간을 kernelland라 한다.
- PintOS kernel은 64MB의 가상주소 공간을 공유한다.(물리주소와는 1:1로 매핑되어 있음) thread create를 실행하여 커널 스레드를 생성하면 3번그림 pagepool에서 4KB짜리 페이지를 하나 떼어다가 쓰는것이다. 실제로 fork 상황에서 부모와 자식 스레드의 주소를 찍어보면 2^12 byte (= 4096 byte) 만큼 차이난다. 유저영역은 3GB 물리공간을 사용한다.
3. SystemCall - Fork
(1) 시스템콜 호출전 tss 업데이트 작업
/* Sets up the CPU for running user code in the nest thread.
* This function is called on every context switch. */
void
process_activate (struct thread *next) {
/* Activate thread's page tables. */
pml4_activate (next->pml4);
/* Set thread's kernel stack for use in processing interrupts. */
tss_update (next);
}
/* Sets the ring 0 stack pointer in the TSS to point to the end
* of the thread stack. */
void
tss_update (struct thread *next) {
ASSERT (tss != NULL);
tss->rsp0 = (uint64_t) next + PGSIZE;
}
- thread create 실행뒤에 context switch가 되고 만들어진 자식스레드는 load를 통해 유저영역을 생성한다고 했다.
- 이 load 함수내부에서 process activate를 실행하여 tss에 들어있는 rsp0값을 업데이트 한다.
- next + PGSIZE(= 1<<12)를 할당하면 커널스레드의 맨 위를 가리키게 된다.
(2) 유저영역에서 테스트 케이스 인스트럭션 수행 및 시스템콜 호출
__attribute__((always_inline))
static __inline int64_t syscall (uint64_t num_, uint64_t a1_, uint64_t a2_,
uint64_t a3_, uint64_t a4_, uint64_t a5_, uint64_t a6_) {
int64_t ret;
register uint64_t *num asm ("rax") = (uint64_t *) num_;
register uint64_t *a1 asm ("rdi") = (uint64_t *) a1_;
register uint64_t *a2 asm ("rsi") = (uint64_t *) a2_;
register uint64_t *a3 asm ("rdx") = (uint64_t *) a3_;
register uint64_t *a4 asm ("r10") = (uint64_t *) a4_;
register uint64_t *a5 asm ("r8") = (uint64_t *) a5_;
register uint64_t *a6 asm ("r9") = (uint64_t *) a6_;
__asm __volatile(
"mov %1, %%rax\n"
"mov %2, %%rdi\n"
"mov %3, %%rsi\n"
"mov %4, %%rdx\n"
"mov %5, %%r10\n"
"mov %6, %%r8\n"
"mov %7, %%r9\n"
"syscall\n"
: "=a" (ret)
: "g" (num), "g" (a1), "g" (a2), "g" (a3), "g" (a4), "g" (a5), "g" (a6)
: "cc", "memory");
return ret;
}
- system call number를 rax에, 6개 인자를 레지스터 rdi, rsi, rdx, r10, r8, r9에 담는다.
- 위 과정을 끝내고 syscall entry 인스트럭션으로 넘어간다.
- 아래 docs convention에도 나와있지만 네번째 인자로 rcx가 아닌 r10을 사용한다. rcx는 rip를 저장할 때 사용하기 때문이다.
(3) rcx에 rip, r11에 eflag 저장. 어디에서 수행하는지는 못찾음
- 시스템콜을 호출할 때 어딘가에서(?) rcx에 현재 rip를 저장하고, r11에 eflag를 담는다.
- 위 내용은 docs에 있고 자세한 내용은 AMD64 linux kernel convention A.2.1에 calling convention에 표기되어 있다.
- 링크: https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf
(4) read ring 0, 커널 스택으로 jump
#include "threads/loader.h"
.text
.globl syscall_entry
.type syscall_entry, @function
syscall_entry:
movq %rbx, temp1(%rip)
movq %r12, temp2(%rip) /* callee saved registers */
movq %rsp, %rbx /* Store userland rsp */
movabs $tss, %r12
movq (%r12), %r12
movq 4(%r12), %rsp /* Read ring0 rsp from the tss */
/* Now we are in the kernel stack */
- rbx와 r12를 임시로 저장해두고 rbx에 userland rsp 저장
- task state segment 의 주소를 r12에 담고 r12가 가리키는 실제 테이블에 + 4 * 8byte(movq는 8바이트)의 값을 rsp가 가리키도록 한다. tss 구조체를 보면 정확히 32바이트 offset에 rsp변수가 담겨 있다.
(5) userland context push
push $(SEL_UDSEG) /* if->ss */
push %rbx /* if->rsp */
push %r11 /* if->eflags */
push $(SEL_UCSEG) /* if->cs */
push %rcx /* if->rip */
subq $16, %rsp /* skip error_code, vec_no */
push $(SEL_UDSEG) /* if->ds */
push $(SEL_UDSEG) /* if->es */
push %rax
movq temp1(%rip), %rbx
push %rbx
pushq $0
push %rdx
push %rbp
push %rdi
push %rsi
push %r8
push %r9
push %r10
pushq $0 /* skip r11 */
movq temp2(%rip), %r12
push %r12
push %r13
push %r14
push %r15
- 커널스레드에 올라와서 바로 userland context를 스택에 push한다. 이 push한 값들을 그대로 인자로 넘기기 위해서 interrupt frame 구조체와 동일한 크기와 변수 순서대로 push한다.
(6) syscall handler 호출
movq %rsp, %rdi
check_intr:
btsq $9, %r11 /* Check whether we recover the interrupt */
jnb no_sti
sti /* restore interrupt */
no_sti:
movabs $syscall_handler, %r12
call *%r12
- 유일한 인자인 rdi에 rsp를 복사한다. 이 rdi가 interrupt frame 구조체 포인터와 동일하다.
- 실제로 syscall handlerd에서 현재 스레드 구조체에 있는 interrupt frame을 printf해보면 유저 context가 아닌 kernel context가 찍힌다.
- 팀원들과 얘기해 봤을 때 여기서 두 가지 가능성을 떠올렸다.
1. 호출중에 다른스레드와 context switch가 돼서 interrupt frame에 들어 있던 user context 를 kernel context로 덮어 씌운 것이다.
2. 애초에 시스템콜을 호출할 때 interrupt frame에 user context를 담지 않는다. 그저 user context를 갖고 있는 레지스터의 정보를 커널스택에 push하고 나중에 pop하면 user 영역으로 다시되돌아 갈 수 있기 때문이다. 애초에 커널스택에 push 된 정보 자체가 interrupt frame 이다.
- 스레드를 생성할때 Interrupt frame을 초기화 한다. 그런데 시스템콜을 호출할때 현재 스레드의 Interrupt frame을 확인해보면 같은 값을 그대로 들고 있는 것을 확인할 수 있다.
- 1번 가능성을 증명하려면 현재 user context를 스레드 구조체 안 interrupt frame에 저장하는 부분이 있어야 하는데 찾을 수 없었다. 그리고 애초에 userland에서 커널 스레드 data인 interrupt frame에 접근하는 것 자체가 말이 안된다.
- 접근하려면 ring 0 상태가 되어야 하는데 이때는 이미 위의 (4) ~ (5) 상태이다. 여기에는 interrupt frame 구조체에 뭔가를 하는 부분이 없고 할 필요가 없다. 따라서 2번 가능성이 맞는 것 같다.
(7) pop해서 유저 context 레지스터 값 복구
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rsi
popq %rdi
popq %rbp
popq %rdx
popq %rcx
popq %rbx
popq %rax
addq $32, %rsp
popq %rcx /* if->rip */
addq $8, %rsp
popq %r11 /* if->eflags */
popq %rsp /* if->rsp */
sysretq
- push 했던 유저영역 context 레지스터를 복구한다.
- sysretq를 통해 유저영역으로 context를 복구하고 syscall을 종료한다.
(PintOS 구현 부분)
- 실제로 구현하는 부분은 위 (6) 단계에서 호출하는 syscall handler를 타고들어가면 나오는 시스템콜 함수들을 구현하는 것이다.
- Fork 에서 어려웠던 부분은 일단 자료가 없어서 어려웠고.. 핵심은 생성된 유저스레드의 유저영역으로 iret을통해 jump 하려면 부모 스레드의 유저 context interrupt frame을 넘겨줘야 하는데, interrupt frame을 찍었을 때 예상하는 값이 안나와서 어려웠다. 먼저 어셈블리단을 조금이나마 이해하니까 도움이 되었다.
'프로그래밍 > 정글' 카테고리의 다른 글
[WEEK 13] 정글끝까지 - PintOS 4. File System (0) | 2022.06.30 |
---|---|
[WEEK 11 - 12] 정글끝까지 - PintOS 3. Virtual Memory (0) | 2022.06.30 |
[WEEK 8] 정글끝까지 - PintOS 1. Threads (0) | 2022.05.26 |
[WEEK 7]탐험준비 - 웹서버 만들기 (0) | 2022.05.26 |
[WEEK6]탐험준비 - Malloc Lab (0) | 2022.05.26 |