운영체제 · Pintos · Project 2 · Week 10
WIL 10주차: Pintos Project 2에서 배운 것은 syscall이 아니라 경계였다
Pintos Project 2를 진행하며 배운 문제해결, 설계, 구현, 협업 흐름과 정글 핵심 10대 역량 달성도를 정리한다.
이번 주 Pintos Project 2는 syscall을 많이 외우는 주가 아니었다. 유저가 커널에게 무언가를 요구할 때, 그 요구를 어디까지 믿고 어디서 끊어야 하는지 계속 확인한 주였다.
이번 주를 한 줄로 정리하면
args를 보려면 `write` syscall도 살아야 했다.
유저 포인터는 검증 전까지 위험한 요청이었다.
복사보다 부모-자식 장부가 핵심이었다.
PASS 보존이 팀 코드의 품질 기준이 됐다.
1. 문제해결: 출력 하나를 살리려면 syscall까지 내려가야 했다
처음에는 Argument Passing을 하면 args 테스트가 바로 보일 줄 알았다. 그런데 유저 프로그램은 결과를 출력할 때 `printf()`를 쓰고, 그 `printf()`는 결국 `write` syscall을 호출한다. 그래서 args를 보기 위해서는 stack만 맞추는 것이 아니라 `SYS_WRITE`, `SYS_EXIT`의 최소 흐름도 같이 살아 있어야 했다.
이 장면에서 문제를 보는 방식이 바뀌었다. 테스트 하나가 실패했다고 해서 그 테스트 파일만 보면 안 됐다. `args-single`은 argument passing 테스트처럼 보이지만, 실제로는 process 실행, user stack, syscall write, exit 메시지가 이어진 결과였다.
2. 설계: 유저는 믿는 대상이 아니라 검증할 대상이었다
Project 2에서 가장 많이 바뀐 관점은 유저 프로그램을 대하는 태도였다. 유저가 넘긴 포인터는 정상 주소일 수도 있지만, NULL일 수도 있고, 커널 주소일 수도 있고, 페이지에 매핑되지 않은 주소일 수도 있다.
그래서 user memory validation은 부가 기능이 아니었다. syscall handler 앞에 세우는 검문소였다. 이 검문소가 없으면 잘못된 유저 요청 하나가 유저 프로세스만 죽이는 것이 아니라 커널 전체를 무너뜨릴 수 있다.
3. 구현: fork는 복사가 아니라 관계 등록이었다
내 담당은 fork 쪽이었다. 처음에는 부모 process를 하나 더 만들면 된다고 생각했다. 하지만 실제 구현은 단순 복사보다 복잡했다. 부모의 register 문맥을 넘기고, 자식의 `rax`만 0으로 바꾸고, pml4를 순회해 user page를 복제하고, fd table도 상속해야 했다.
여기서 끝나지 않는다. fork로 생긴 자식은 나중에 wait로 회수되어야 한다. 그래서 `child_info`가 필요했다. 이 구조는 자식이 누구인지, 죽었는지, exit status가 무엇인지, 부모가 이미 wait했는지를 남기는 장부였다. 이때부터 fork를 복사 함수가 아니라 부모와 자식의 관계를 등록하는 절차로 보게 됐다.
4. 협업: 각자 통과한 코드는 합치면 다시 흔들린다
이번 주 후반은 merge가 중요했다. 각자 브랜치에서는 통과하던 코드가 main에 합쳐지면 깨질 수 있었다. 특히 user memory, fd, process lifecycle, fork는 서로의 구조를 직접 사용한다.
그래서 단순히 내 브랜치에서 몇 개가 통과됐는지보다, merge 후 기존 PASS가 FAIL로 바뀌지 않았는지가 더 중요했다. 이 경험 때문에 테스트 결과를 말할 때 branch, build path, test scope를 같이 말해야 한다는 기준이 생겼다.
5. 정글 핵심 10대 역량 목표와 달성도
6. 이번 주 체크리스트
- 시스템 호출 흐름: Argument Passing에서 시작해 `write`, `exit`, pointer validation까지 연결했다.
- 자원 장부 흐름: fd table, child_info, exit status처럼 커널이 직접 관리해야 하는 장부를 구분했다.
- 프로세스 복제 흐름: fork에서 register, pml4, fd table이 각각 왜 복제되는지 정리했다.
- 협업 검증 흐름: merge 후 기존 PASS가 깨지지 않는지를 기준으로 최종 main을 확인했다.
| 상태 | 분야 | 구체 체크포인트 | 관련 기록 |
|---|---|---|---|
| 완료 | Argument Passing | `argc`, `argv`는 user stack 위의 호출 약속이다. 남은 의문은 stack 확장이 Project 3에서 어떻게 달라지는가이다. | 기록 |
| 완료 | System Call | `printf()`도 결국 `write` syscall을 지나야 한다. 남은 의문은 page fault 기반 검증 방식과 현재 선검증 방식의 차이다. | 기록 |
| 완료 | Fork | `fork`는 복사가 아니라 부모-자식 관계 등록이다. 남은 의문은 multi-oom에서 실패 경로를 얼마나 더 엄격히 회수해야 하는가이다. | 기록 |
| 진행 | Merge 검증 | 브랜치별 PASS를 main에서 보존해야 한다. 남은 의문은 rox와 filesys random 계열을 누가 어떤 순서로 잡을지이다. | 팀 main 결과 |
| 완료 | 발표 회고 | 어려운 구조는 내 언어로 바꿀 때 오래 남는다. 남은 의문은 비유가 기술 이해를 흐리지 않는 선을 어디에 둘지이다. | 제국 세계관 발표 |
7. 내 이야기로 남은 장면
가장 기억에 남는 장면은 fork를 설명하다가 `자식은 부모의 기억을 복사받지만 rax만 0으로 조작된다`는 말을 이해한 순간이다. 같은 fork 호출인데 부모는 자식 번호를 받고, 자식은 0을 받는다. 처음에는 이상한 예외처럼 보였지만, 이게 부모와 자식이 같은 코드 위치에서 서로 다른 삶을 시작하게 만드는 장치였다.
또 하나는 merge 과정이었다. 어떤 브랜치는 로컬에서 통과했고, 다른 환경에서는 깨졌다. 이때부터 통과 개수만 말하는 방식이 위험하다는 걸 알았다. `어느 브랜치`, `어느 build 폴더`, `어떤 make check`, `어떤 테스트 범위`인지 같이 말하지 않으면 결과가 사실상 반쪽짜리 정보였다.
발표 준비도 기억에 남는다. 그냥 `fork는 process를 복제합니다`라고 말하면 듣는 사람도, 말하는 나도 금방 잊는다. 그런데 Pintos를 제국으로 보고, fork를 생산 공장으로 보고, child_info를 노예증표이자 사망신고서로 보니 구조가 오래 남았다. 과한 비유였지만, 어려운 구조를 내 언어로 바꿨다는 점에서 이번 주의 중요한 장면이었다.
8. 남은 테스트와 아쉬운 점
최종 main은 95개 중 85개를 통과했다. 남은 실패는 그냥 숫자가 아니라 아직 닫지 못한 구멍이다. `rox-simple`, `rox-child`, `rox-multichild`는 실행 중인 파일에 대한 deny write가 부족하다는 신호이고, `exec-read`, `multi-child-fd`는 exec와 fd 상속/파일 위치 관리가 더 정확해야 한다는 신호다.
filesys의 `lg-random`, `sm-random`, `syn-remove`, `syn-write`는 seek/tell/remove 이후 열린 파일 처리와 동시성 흐름을 더 봐야 한다. `multi-oom`은 fork 실패와 자원 회수 경로가 아직 충분히 단단하지 않다는 뜻이다.
아쉬운 점은 처음부터 최종 구조를 보지 못했다는 것이다. args를 통과하기 위한 임시 발판과 최종 process lifecycle 구조를 더 빨리 구분했다면 덜 헤맸을 것이다. 그래도 그 시행착오 덕분에 임시 구현과 최종 구현을 구분하는 기준이 생겼다.
9. 다음 주로 가져갈 기준
- 테스트 결과를 말할 때는 branch, build path, test scope를 같이 말한다.
- 임시 발판 코드는 주석으로 표시하고, 최종 구조와 섞이지 않게 한다.
- 내 담당 코드가 다른 담당자의 어떤 구조와 맞물리는지 먼저 적고 시작한다.
- PASS 개수보다 regression 여부를 먼저 확인한다.
- 비유를 쓰더라도 기술 구조를 흐리지 않는 선을 지킨다.
마무리
이번 주가 끝나고 나니 대충 넘기기 어려워진 것이 있다. 테스트 하나는 하나의 함수만 보라는 신호가 아니고, fork는 복사라는 말 하나로 설명되지 않으며, merge 결과는 숫자만으로 말하면 위험하다.
이제 Pintos 코드를 볼 때 먼저 찾게 되는 것은 함수 이름이 아니라 경계다. 누가 누구를 믿는지, 누가 어떤 장부를 들고 있는지, 실패했을 때 누가 정리하는지. 이번 주의 변화는 그 질문들이 계속 거슬리기 시작했다는 데 있다.