지금 부터는 구현 했던 syscall 함수 중에서 가장 어렵고 이해가 힘들었던 fork 관련 내용을 정리하려고 한다 .
pid_t fork (const char *thread_name);
먼저 fork 함수의 기능에 대해 설명하자면 현재 프로세스 이름이 thread_name인 프로세스의 복제본을 만드는 새 프로세스를 생성해야 합니다. 호출자 보존 레지스터인 %RBX, %RSP, %RBP, and %R12 ~ %R15 의 값 이외에는 레지스터 값을 복제할 필요가 없습니다. 함수는 자식 프로세스의 pid를 반환해야 합니다. 그렇지 않으면 유효한 pid가 아니어야 합니다. 자식 프로세스에는 반환 값이 0이어야 합니다. 자식 프로세스는 파일 디스크립터와 가상 메모리 공간을 포함하여 복제된 리소스를 가져야 합니다. 부모 프로세스는 자식 프로세스가 성공적으로 복제되기 전에 fork() 호출에서 반환 되면 안됩니다. 즉, 자식 프로세스가 리소스를 복제하지 못하면 부모의 fork()호출은 TID_ERROR입니다.
템플릿은 threads/mmu.c에서 pml4_for_each() 를 사용하여 해당 페이지 테이블 구조를 포함한 전체 사용자 메모리 공간을 복사, 전달된 pte_for_each_func 의 누락된 부분을 채워야 한다.
typedef bool pte_for_each_func (uint64_t *pte, void *va, void *aux);
bool pml4_for_each (uint64_t *pml4, pte_for_each_func *func, void *aux);
PML4 하위의 각 유효한 엔트리에 대해, 보조 값 AUX를 사용하여 FUNC를 적용합니다. VA는 엔트리의 가상 주소를 나타냅니다. 만약 pte_for_each_func가 false를 반환하면, 반복을 중단하고 false를 반환합니다.
static bool
stat_page (uint64_t *pte, void *va, void *aux) {
if (is_user_vaddr (va))
printf ("user page: %llx\\n", va);
if (is_writable (va))
printf ("writable page: %llx\\n", va);
return true;
}
/* Clones the current process as `name`. Returns the new process's thread id, or
* TID_ERROR if the thread cannot be created. */
tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
/* Clone current thread to new thread.*/
return thread_create (name,
PRI_DEFAULT, __do_fork, thread_current ());
}
process_fork()는 인자로 프로세스의 이름과 인터럽트 프레임 구조체를 받는다. 인터럽트 프레임은 인터럽트가 호출되었을때, 이전에 레지스터에 작업하던 context 정보를 스택에 담는 구조체이다. 부모 프로세스가 갖고 있던 레지스터 정보를 담아 그대로 복사해야 하기 때문이다.
return 값으로 thread_create()를 실행하는데, 그 안에 인자로서 _do__fork() 함수를 실행한다.
_do__fork() 는 부모 프로세스의 내용을 자식 프로세스로 복사하는 함수라고 할 수 있다.
/* A thread function that copies parent's execution context.
* Hint) parent->tf does not hold the userland context of the process.
* That is, you are required to pass second argument of process_fork to
* this function. */
/* 부모 프로세스의 실행 context를 복사하는 thread 함수다 힌트: parent->tf(부모 프로세스 구조체 내 인터럽트 프레임 멤버
는 프로세스의 userland context 정보를 들고 있지 않다.
즉, 당신은 process_fork()의 두번째 인자를 이 함수에 넘겨줘야만 한다*/
static void
__do_fork(void *aux)
{
}
위 글에서 parent->tf는 userland context를 들고 있지 않다고 했다. 그럼 얘는 어디로 간 걸까?
관련 QnA 정리
Q 1. fork시 7개 레지스터만 복사하면 되는 것 아닌가요?
제가 이해한 바로는 process_fork의 두번째 인자인 *if 는 fork()를 호출하는 시점의 부모 프로세스의 inter_frame을 가지고 있는 것으로 알고 있습니다. 그리고 해당 인자를 __do_fork() 함수로 전달해 주기 위해서 pf라는 새로운 field값을 thread 구조체에 추가하고 memcpy 를 통해 저장 시켜 주었습니다.
이후 do_fork() 함수에서 parent_if라는 포인터 변수에 위에서 저장한 부모 프로세스의 inter_frame을 대입연산자를 통해 넣어주었습니다.
제가 처음으로 한 생각은 gitbook에 적힌 것처럼 총 7개의 레스터 값들을 부모 프로세스의 inter_frame으로 부터 자식 프로세스로 clone해 줘야 한다고 생각했습니다. 하지만 정상적으로 작동하지 않았고 memcpy(&if_, &parent->pf, sizeof(struct intr_fram)) 을 통해 intr_frame자체를 전부 복사를 하니 fork()가 작동하였습니다.
A 1. gitbook에 적힌 말은 general purpose register 중에 callee-saverd register 제외하고는 백업안해도 된다는 의미인 것 같습니다. %rip, %ds, %ss, %es, %cs 등 특수한 역할을 하는 레지스터들도 복사해줘야 fork()를 통해 새로 생긴 프로세스가 제대로 된 위치에서 실행을 이어 갈 수 있습니다.(%rip는 다음에 실행할 명령어의 위치가 담겨 있고, %ds, %ss, %es, %cs는 어느 세그먼트를 참조할지 가리키는 레지스터로 알고있습니다.)
Q 2. syscall-entry.S 라는 어셈블리어 파일의 경우 기존의 User 메모리에 있던 부모 프로세스의 inter_fram 을 커널 스택 영역으로 복사하는 과정이고 이렇게 복사한 inter_fram 을 userprog/syscall.c 내의 syscall_handler(struct intr_frame *f UNUSED) 넘겨준다고 이해했는데 맞는 걸까요?
A 2. user memory에 있던 inter_frame을 커널 스택으로 복사하는 것이 아니라, 커널 스택에서 intr_frame 구조를 바로 만드는 것입니다. inter_fram은 syscall을 호출하기 직전의 프로세스 정보(여러 register 값들)를 백업한 구조이므로 user memory에 미리 존재했다고 말하긴 힘들 것 같습니다.
Q 3. thread가 가지는 intr_frame 구조체가 실제로 저장되는 메모리 영역은 커널 영역이고 만약 USER_PROGRAM이 실행되면 해당 thread의 pml4를 유저 영역의 메모리로 할당시켜주고, rsp가 가르키는 주소를 유저영역 메모리로 변경함으로써 유저영역으로 제어가 넘어간다고 이해했는데 맞을까요?
A 3.
- intr_frame이 실제로 저장되는 메모리 영역은 커널 영역: O
- pml4를 유저 영역의 메모리로 할당: pml4 table 자체는 커널 영역에 있습니다. 하지만 pml4 table은 user virtual address를 physical address로 mapping하는 자료를 갖고 있습니다.
- rsp가 가르키는 주소를 유저영역에 메모리로 변경 : O
- 그런데, rsp를 유저 영역 메모리로 변경함으로써 제어가 넘어가는 것이 아니라, 제어를 넘기기 전에 rsp를 변경시켜 주는 것입니다. 제어 자체는 %rip를 유저 영역 메모리의 올바른 위치로 넘기고 여러 권한을 재설정함으로써 유저영역으로 넘어갑니다.
Q 4. fork를 구현할 때 두 가지 인터럽트 프레임 구조체가 있습니다. 하나는 thread_parent가 가지고 있는 인터럽트 프레임 구조체 “tf”가 있고 다른 하나는 시스템콜 핸들러가 process_fork에게 인자로 넘기는 인터럽트 프레임 구조체 “f”가 있습니다. 둘 다 인터럽트 프레임 구조체 같지 않은데 둘의 역할이 무엇인가요??
Q 4_1. 포크가 부모를 복사하는 건데, “부모의 인터럽트”를 넘겨주면 안되는 이유
Q 4_2. 결론적으로 시스템 콜 핸들러가 넘겨준 인터럽트 프레임을 자식이 복사하고 이를 활용해 do_iret해야 테스트를 통과하는데 시스템 콜 핸들러가 넘겨준 인터럽트 프레임은 누구의 정보 인가?
A 4.
- “부모의 인터럽트 프레임을 넘겨야지” 하는 생각 자체는 맞습니다. 다만 더 정확하게 말하자면 “부모의 user-level 정보가 담긴 인터럽트 프레임”을 넘겨줘야 하고, 이건 f에 저장 되어 있습니다. 인터럽트 프레임에 무슨 정보가 담겨 있는지 질문을 주셨는데, 여기에는 실행할 때 사용하는 정보(주로 레지스터의 값)가 백업되어 있습니다.
- tf는 부모 프로세스가 fork()를 수행하던 도중 context switch가 일어나서 다른 스레드가 실행이 될 경우, tf에는 fork()를 수행하던 커널이 어디까지 작업했는지 정보가 저장 됩니다. 하지만 f는 user-level에서의 부모 프로세스가 실행되던 정보가 항상 담겨있습니다. fork()해서 만든 자식 프로세스는 부모 프로세스의 user-level 정보를 이어받아 user-level에서 실행을 마저 해야하므로, f를 복사하는것이 맞습니다.
Q 5. process_fork 함수 내 tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, parent);
를 분기로 한 개만 있던 스레드가 2개(fork-once인 경우)가 되려고 하는데, 이 때 부모 스레드가 자식은 만들어지지도 않았는데 먼저 cpu의 선택을 받아 실행되어 return pid하고 끝나버리는 경우를 막아야 한다고 알고있습니다. do_fork에서 f를 복사하지 않고 tf를 복사하면 이때의 tf는 return pid 하지 않기 위해 부모 스레드가 잠들기 직전 정보를 자식에게 넘긴 꼴이 되는건가요? 아님 do_fork를 하는 순간인 tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, parent); 까지 부모 스레드가 실행하고 있던 정보를 자식에게 넘긴 것이 되는건가요??
A 5. tf를 복사하면 말씀하신 대로 return pid하지 않기 위해 부모 스레드가 잠들기 직전 정보를 자식에게 넘긴 꼴이 될 수 도 있고, 그 전에 scheduler에 의해 context switch가 일어나면 그 전의 정보가 자식에게 넘어 갈 수도 있습니다. thread_create의 구현에 따라 다를 수 있지만, 부모 thread는 반드시 그 순간에 잠들어 있을것이고, 그 직전에 실행되던 정보가 저장 되어 있습니다.
위의 관련 QnA 에서 보면 thread_parentrk 가지고 있는 인터럽트 프레임 "tf", 시스템콜 핸들러가 process_fork에게 인자로 넘기는 인터럽트 프레임 구조체 "f"가 있다고 나온다. 질문의 답에서 보면 tf를 넘겨주는게 아니라 'f"를 넘겨 줘야 한다는 답에 도달하는데 이 개념이 굉장히 어렵고 이해가 안되었다. 그래서 그림을 참조해서 이해를 하려고 했다.
위의 그림들을 참조하면 thread_parent가 fork 시 thread_child에게 넘겨줘서 thread_child가 fork() 이후 작업을 하기 위해서는 thread_parent의 user_stack 정보이다.
하지만 유저 스택 부분의 fork() -> syscall 과정에서 syscall_init() -> syscall-entry.S로 넘어가서 어셈블리 코드가 실행된다.
코드 흐름을 보면,
1. rbx 레지스터에 현재 rsp 레지스터에 담겨있던 유저 스택 포인터 주소값을 임시로 저장한다
2. rsp 레지스터에 tss에 저장되어 있던 커널 스택 포인터(아래 그림에서 커널 스택을 가리키고 있는 tss->rsp0값) 주소값을 가져와 붙여넣기한다. 즉, 이때부터 rsp 레지스터에는 커널 스택 포인터 값이 들어있다.
3. 이 tf는 커널 스택 안에 있는 스레드 구조체의 멤버로 있으며, switching 시 맥락을 담는 인터럽트 프레임 구조체이다. 이 인터럽트 프레임 내 rsp 값은 이제 커널 스택 포인터를 가리킨다.
이 과정에서 thread_parent의 "tf"는 유저 스택 이라 커널 스택 포인터를 가리키게 된다. 그래서 처음의 user_stack 정보를 저장하기 위해 thread 구조체에 parent_tf 따로 추가해주고 저장해준다.
'SW Jungle > Week 09' 카테고리의 다른 글
project 02 User program(System_call) (0) | 2023.05.09 |
---|---|
project 02 User program(Argument passing) (0) | 2023.05.09 |
project 02 User program(background) (0) | 2023.05.01 |