시스템 콜이 들어왔을 때 💭
t1 쓰레드가 CPU running 중이다. read()라는 system call을 만나 `kernel mode`로 진입한다.
t1은 실행중이던 cpu 상태를 저장한 후 디스크에서 파일을 읽을 준비를 한다. I/O 요청은 시간이 오래걸리기에 t1을 대기 리스트(waiting list)에 보내고 ready list에 있던 t2를 실행 시킨다.
t2가 실행중이다가 t1이 디스크에 요청했던 read가 끝나면 I/O 컨트롤러에서 인터럽트를 발생시켜 kernel 모드로 다시들어온다. 실행중이던 t2 상태를 저장 후 t1이 요청했던 read()가 준비가 다 되었기에 ready list에 올린다.
system handler까지 실행 순서
- 시스템 콜 호출: 사용자 프로그램이 read()와 같은 시스템 콜을 호출함.
- 시스템 콜 명령어 실행: int 0x80 또는 syscall 명령어가 실행되어 CPU에 인터럽트를 발생시킴.
- 인터럽트 벡터 확인: CPU는 인터럽트 벡터 테이블을 확인하고, 시스템 콜을 처리할 핸들러(syscall_handler)를 찾아냄.
- 커널 모드 전환: CPU는 사용자 모드에서 커널 모드로 전환되어 시스템 콜을 안전하게 처리할 준비를 함.
- 핸들러 실행: syscall_handler가 실행되어, 시스템 콜을 처리하기 위한 작업을 시작함.
Address Validation(주소 검증)
void check_address(void *addr) {
struct thread *t = thread_current(); // 현재 실행 중인 스레드를 가져옵니다.
if (!is_user_vaddr(addr) || addr == NULL ||
pml4_get_page(t->pml4, addr) == NULL) {
// 만약 위 조건 중 하나라도 충족하지 않는 경우(즉, 주소가 유효하지 않을 때)
exit(-1); // 프로세스를 강제로 종료하며, 종료 상태 코드는 -1입니다.
}
}
- 주소가 사용자 영역의 가상 주소인지 확인
- `is_user_vaddr(addr) : 주소가 user 영역 주소인지, os 영역 주소인지 확인
- 주소가 NULL인지 확인
- 주소가 페이지 테이블에 실제로 매핑 되어 있는지 확인
- pml4_get_page(t->pml4, addr): 현재 pml4가 실제 물리 메모리에 매핑되었는지 확인,. 이를 통해 유저 주소 영역 내에서도 실제 할당이 이루어졌는지를 검증
pml4
https://jinsang-2.tistory.com/98
get_argument 함수는 32bit 에서만 !
// 64bit에서는 필요없음
void get_argument(void *esp, int *arg , int count)
{
/* 유저 스택에 저장된 인자값들을 커널로 저장 */
/* 인자가 저장된 위치가 유저영역인지 확인 */
}
시스템 호출 방식이 32비트와 64비트에서 다르다.
- 32비트 환경에서는 시스템 호출의 인자를 스택을 통해 전달하는데 프로그램이 시스템 호출 시 인자들이 스택에 쌓이고 커널은 이 스택에서 인자들을 꺼내 처리함
- 64비트 환경에서는 레지스터를 사용하여 시스템 호출의 인자를 전달한다.
- 시스템 호출 번호는 rax, 각 인자는 rdi, rsi, rdx, r10, r8, r9
결론 : 64비트 환경에서는 시스템 호출의 인자가 스택이 아니라 레지스터를 통해 전달되므로 스택에서 인자를 추출하는 함수인 `get_argument()`가 필요하지 않다.
exit()
- 종료 상태 설정
- 인자로 전달된 상태값을 기반으로 프로세스가 정상 종료했는지, 아니면 비정상 종료했는지를 기록합니다.
- 프로세스 자원 정리
- 종료된 프로세스가 사용 중인 자원이나 저장 공간을 해제해야 합니다.
- 프로세스의 종료를 상위 프로세스에게 알리기
- 부모 프로세스(이 프로세스를 생성한)가 자식 프로세스의 종료 상태를 확인할 수 있도록 해당 상태를 저장합니다.
- 프로세스 종료
- 마지막으로 실제로 프로세스를 제거하고 CPU에서 더 이상 실행되지 않도록 합니다.
실제 시나리오
- 현재 프로세스가 "user_program"인 프로세스가 "exec("echo hello")" 호출을 통해 자신을 "echo hello"라는 프로그램으로 변경하려 시도
- 주소가 올바른지 확인 후, 새 메모리 페이지를 할당하고 "echo hello" 문자열을 복사하여 새로운 프로그램을 실행하려 함
- process_exec()를 통해 프로그램이 성공적으로 로드되면, 이는 새로운 프로그램(echo hello)로 전환되어 실행이 계속됩니다.
- 만약 실행이 실패하면, 현재 프로세스는 역할을 수행하지 못하므로 상태 -1로 종료됩니다.
thread 구조체에 exit_status 추가
struct thread {
...
// system call 구현
int exit_status; /* 프로세스가 종료되었을 때 상태 저장*/
- exit(int status) 수정
void exit(int status) {
struct thread *cur = thread_current(); // 현재 실행 중인 스레드 (프로세스에 대응)
// 1. 종료 시 출력 메시지
printf("%s: exit(%d)\n", cur->name, status); // 프로세스 이름과 종료 상태 출력
// 2. 종료 상태를 스레드/프로세스 상태에 저장
cur->exit_status = status;
// 추가: 필요한 자원 혹은 파일 정리 등을 이곳에서 수행합니다.
// 예를 들어 열려 있는 파일 닫기, 메모리 해제 등 작업이 포함될 수 있습니다.
// 3. 스레드를 종료시킴
thread_exit(); // 현재 스레드의 실행을 종료하고 자원 해제
}
- 3. 스레드를 종료시킴
- thread_exit() -> process_exit() 함수 수정해야 한다!
void
process_exit (void) {
struct thread *curr = thread_current ();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
process_cleanup ();
}
fork()
1. fork 시스템 콜의 기본 원리
- fork는 부모 프로세스를 복제해서 자식 프로세스를 만드는 시스템 콜이다.
- 부모는 자식 프로세스의 PID를 받고, 자식은 0을 반환한다.
- 둘 다 같은 프로그램 코드와 데이터, 같은 파일 디스크립터와 메모리를 갖지만, 자식은 새로 만들어진 독립된 프로세스로서 실행된다.
2. 레지스터 복제
fork가 실행되면, 부모 프로세스의 레지스터 값이 자식 프로세스로 그대로 복사돼야 한다. 여기서 중요한 점은 어떤 레지스터들을 복제할지가 중요하다.
- Callee-saved registers
- rbx, rsp, rbp, r12, r13, r14, r15 레지스터는 함수 호출 시에 보존되기에 복제해야 한다.
- General-purpose registers(범용 레지스터)
- 범용 레지스터들 중에서 called-saved registers가 아닌 것들은 fork에서 따로 관리할 필요가 없다.
- rax, rcx, rdx 같은 caller-saved registers는 특별히 복제하지 않아도 된다.
- Special-purpose registers(특별 레지스터)
- %rip, %ds, %cs, %ss 같은 특수 레지스터들은 복제해야 한다.
- %rip 는 현재 실행 중인 명령어의 주소를 저장하는 레지스터로, 자식 프로세스가 어느 위치부터 실행할지 결정
- %cs, %ss는 코드 세그먼트와 스택 세그먼트 레지스터로, 자식 프로세스가 동일한 코드/스택 세그먼트를 사용할수 있게 한다.
즉, 레지스터를 복제할 때는 일반 레지스터 중 특정 부분만 복제하지 않으면 되고, 그 외에 특수한 역할을 하는 레지스터들은 반드시 복제해줘야 한다.
3. 자식 프로세스의 반환 값 처리
fork가 성공적으로 호출되면 부모와 자식은 다르게 반환돼야 한다.
- 부모는 자식의 PID를 반환해야 하고,
- 자식은 무조건 0을 반환해야 해.
fork가 호출된 뒤 자식 프로세스에서는 내부적으로 반환 값을 0으로 설정하고, 부모는 자식의 PID를 반환한다. 이렇게 반환 값을 다르게 설정해서, 부모와 자식이 서로 다른 동작을 하게 해서 자식은 자식의 프로그램을 실행하고 부모는 자식을 관리하거나 부모 프로그램을 각각 실행 시킬 수 있다.
4. 리소스 복제
자식 프로세스는 부모의 파일 디스크립터와 가상 메모리 같은 중요한 리소스를 복제해야 한다.
파일 디스크립터 복제
- 부모가 열어둔 파일 디스크립터는 자식에게도 그대로 복제한다. 자식은 부모와 동일한 파일 디스크립터로 파일을 열 수 있다.
- 하지만 중요한 차이점이 있는데, 자식은 부모와 파일 오프셋을 공유하지 않아서, 파일을 읽거나 쓸 때 따로따로 오프셋이 관리되어 즉, 부모가 파일을 읽는 동안 자식도 그 파일을 읽거나 쓸 수 있지만, 서로 간섭하지 않는다.
- 파일 내에서 읽고 쓰는 위치(오프셋)**가 서로 독립적으로 관리된다는 뜻
- 부모가 파일의 중간(예: 100바이트 지점)을 읽고 있어도, 자식은 처음(0바이트)부터 파일을 읽을 수 있다. 서로 읽고 쓰는 위치가 다르니까, 부모가 읽는 위치와 자식이 읽는 위치가 겹치지 않아서 간섭하지 않음.
가상 메모리 복제
- 자식은 부모가 사용하고 있는 가상 메모리 공간도 복제해야 한다. 이때 페이지 테이블을 복사하는 과정이 중요한데, 이를 위해 템플릿에 나와 있는 `pml4_for_each()` 함수가 사용.
- 페이지 테이블(Page Table)은 프로세스의 가상 주소를 물리 주소로 변환하는 매핑 테이블이다. `pml4_for_each()` 함수는 부모 프로세스의 페이지 테이블을 순회하면서 자식 프로세스에 동일한 가상 메모리 공간을 설정한다. 이 과정에서 자식이 부모와 같은 메모리 레이아웃을 가질 수 있게 복제
5. 부모는 자식이 복제되는 동안 기다려야 한다.
fork() 호출이 끝나기 전까지 부모 프로세스는 자식 프로세스가 복제되는 과정을 기다려야 한다. 자식이 복제에 성공했는지 실패했는지 확인한 뒤에야 부모는 fork()에서 빠져나온다.
만약 자식이 리소스를 제대로 복제하지 못하면, 부모는 오류 코드(TID_ERROR)를 반환해야 해. 이 과정에서 자식 프로세스의 상태를 모니터링하는 코드를 넣어, 자식이 복제에 실패했을 때 부모에게 알려줄 수 있게 구현해야 한다.
6. pml4_for_each()와 pte_for_each_func
- pml4_for_each()는 부모 프로세스의 페이지 테이블을 순회하면서 자식의 페이지 테이블로 복사해주는 역할을 해.
- 이 함수에서 각 페이지 테이블 엔트리를 순회하며, 이를 처리하는 함수가 바로 pte_for_each_func야. 이 부분을 완성해야 부모의 메모리 페이지를 자식에게 복제할 수 있어.
- pte_for_each_func는 **페이지 테이블 엔트리(Page Table Entry, PTE)**를 처리하는 콜백 함수인데, 여기서 각 메모리 페이지를 어떻게 복사할지 정의해줘야 해.
예를 들어, 부모의 페이지가 읽기 전용인지, 쓰기 가능한지 등을 확인한 후 자식에게 동일한 권한으로 페이지를 복사해주면 돼. 이 부분이 제대로 동작해야 부모와 자식이 동일한 가상 메모리 구조를 가지게 되는 거야.
요약
- fork는 부모 프로세스를 복제해서 자식 프로세스를 생성하는 시스템 콜이야.
- 레지스터 복제: callee-saved 레지스터는 복제해야 하고, %rip, %ds 같은 특수 레지스터도 복제해줘야 해.
- 리턴 값: 부모는 자식의 PID를, 자식은 0을 반환하도록 처리.
- 리소스 복제: 파일 디스크립터와 가상 메모리를 복제하고, 페이지 테이블을 순회하며 자식에게 부모의 메모리 공간을 복사.
- 부모는 자식이 성공적으로 복제되기 전까지 기다리고, 실패 시 오류를 반환.
- pml4_for_each()와 pte_for_each_func는 페이지 테이블을 복사해서 자식이 동일한 메모리 맵을 갖도록 함.
이 정도로 설명하면 좀 더 구체적인 느낌일 것 같아. fork는 시스템 전반을 다루는 만큼 세부적인 부분에 신경 써야 하고, 특히 메모리와 리소스 복제가 잘 이루어지도록 해야 해.
'SW 사관학교 정글(Jungle) > 운영체제-PintOS' 카테고리의 다른 글
[PintOS Project 2 - User Program] 여러 자료 정리..(kaist 자료 등) (2) | 2024.10.07 |
---|---|
pml4(page map level 4) 페이지 테이블 최상위 레벨 (0) | 2024.10.07 |
[PintOS Project 1 - Threads] 2번 Priority Scheduling (0) | 2024.10.01 |
cpu와 스레드 (0) | 2024.10.01 |
[PintOS Project 1 - Threads] 1번 Alarm Clock (0) | 2024.09.28 |