크래프톤 정글/TIl _ WILL

WIL 10주차: Pintos Project 2에서 배운 것은

cedis 2026. 5. 7. 14:29

운영체제 · Pintos · Project 2 · Week 10

WIL 10주차: Pintos Project 2에서 배운 것은 syscall이 아니라 경계였다

Pintos Project 2를 진행하며 배운 문제해결, 설계, 구현, 협업 흐름과 정글 핵심 10대 역량 달성도를 정리한다.

이번 주 Pintos Project 2는 syscall을 많이 외우는 주가 아니었다. 유저가 커널에게 무언가를 요구할 때, 그 요구를 어디까지 믿고 어디서 끊어야 하는지 계속 확인한 주였다.

이번 주를 한 줄로 정리하면

Week 10
커널과 유저, 부모와 자식, 파일 원본과 fd 번호 사이의 경계를 코드로 그어 본 주였다.
이번 주 학습 흐름
1. 출력이 안 보임
args를 보려면 `write` syscall도 살아야 했다.
2. 주소를 못 믿음
유저 포인터는 검증 전까지 위험한 요청이었다.
3. fork가 흔들림
복사보다 부모-자식 장부가 핵심이었다.
4. merge가 시험함
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대 역량 목표와 달성도

문제해결
정의: 실패한 현상을 작은 원인 후보로 쪼개고, 실제 실행 흐름으로 검증하는 능력
이번 주 목표: args 실패를 argument passing 하나로만 보지 않고 syscall 출력 흐름까지 추적하기
구체 회고: `args-single`이 안 보일 때 `printf()`가 `write` syscall을 지나야 한다는 사실을 확인했다. 이후 테스트 실패를 한 파일 문제가 아니라 연결된 실행 경로 문제로 보기 시작했다.
평가: 단일 원인 추측에서 벗어나 흐름 단위로 문제를 보는 쪽으로 발전했다.
이번 주에 가져가는 것: 테스트 이름은 입구일 뿐이고, 실제 원인은 더 아래 계층에 있을 수 있다.
설계
정의: 구조체와 함수가 나중에 어떤 관계를 맺을지 먼저 정리하는 능력
이번 주 목표: fork, wait, exit가 함께 쓸 장부 구조를 이해하기
구체 회고: `child_info`를 단순 보조 필드가 아니라 부모와 자식이 공유하는 종료 정보 장부로 이해했다. 이 장부가 없으면 wait가 직접 자식인지 확인할 수도, exit status를 회수할 수도 없다는 점을 정리했다.
평가: 기능 단위가 아니라 관계 단위로 설계를 보는 감각이 생겼다.
이번 주에 가져가는 것: process 구현은 함수 목록이 아니라 장부 설계다.
구현
정의: 이해한 구조를 실제 코드의 위치와 책임으로 옮기는 능력
이번 주 목표: `process_fork()`, `__do_fork()`, `duplicate_pte()` 흐름을 실제 코드 기준으로 설명하기
구체 회고: 부모의 intr_frame을 `fork_args`에 담고, 자식에서 `rax=0`으로 바꾸며, pml4와 fd table을 복제하는 흐름을 내 담당 파트로 정리했다.
평가: 코드를 외우는 단계에서 벗어나 각 줄이 fork 규칙의 어떤 요구를 만족하는지 설명할 수 있게 됐다.
이번 주에 가져가는 것: 구현은 복붙이 아니라 규칙을 코드 위치에 배치하는 일이다.
품질
정의: 새 기능이 기존 통과 테스트를 깨지 않는지 확인하는 능력
이번 주 목표: merge 후 regression을 확인하는 습관 만들기
구체 회고: 각 브랜치에서 통과하던 테스트가 main에서 실패로 바뀌면 안 된다는 원칙을 세웠다. 최종 main 기준으로 95개 중 85개 통과, userprog 64개 중 58개 통과 상태를 기준으로 남은 실패를 분류했다.
평가: 통과 개수보다 통과 상태의 보존을 더 중요하게 보게 됐다.
이번 주에 가져가는 것: 새 PASS보다 기존 PASS 보존이 먼저다.
유지보수
정의: 임시 발판과 최종 구조를 구분해 나중에 걷어낼 수 있게 만드는 능력
이번 주 목표: 임시 구현을 최종 구조처럼 착각하지 않기
구체 회고: args를 통과시키기 위한 임시 wait와 `child_info` 기반 정식 wait가 목적이 다르다는 점을 뒤늦게 분리했다. 이후 임시 발판은 주석으로 표시해야 한다는 기준을 세웠다.
평가: 코드를 만드는 것보다 나중에 교체할 수 있게 남기는 것이 중요하다는 감각이 생겼다.
이번 주에 가져가는 것: 임시 코드는 통과보다 제거 가능성이 더 중요하다.
협업
정의: 내 파트가 다른 사람의 파트와 만나는 지점을 먼저 밝히는 능력
이번 주 목표: fork 담당으로 fd, wait, user memory 담당과의 접점을 명확히 하기
구체 회고: fork는 혼자 완성되지 않았다. fd 상속은 fd table이 필요했고, wait는 child_info가 필요했고, thread_name 검증에는 user memory 방어막이 필요했다. 그래서 담당을 나눠도 접점은 계속 공유해야 했다.
평가: 분업은 고립이 아니라 연결 지점 관리라는 것을 배웠다.
이번 주에 가져가는 것: 각자 작업보다 접점 합의가 merge를 살린다.
태도
정의: 모르는 개념을 넘기지 않고 멈춰서 확인하는 태도
이번 주 목표: 막히는 단어를 감으로 넘기지 않기
구체 회고: `f->R.rax`, `sentinel`, `pml4`, `fd`, `sema` 같은 단어에서 자주 멈췄다. 진행이 느려졌지만, 그 덕분에 fork와 syscall 흐름을 남에게 설명할 수 있는 수준으로 바꿀 수 있었다.
평가: 빠르게 지나가는 것보다 정확히 멈추는 것이 이번 주에는 더 효과적이었다.
이번 주에 가져가는 것: 이해하지 못한 단어는 나중에 반드시 비용으로 돌아온다.
비즈니스 이해
정의: 기술 구현을 사용자와 시스템의 계약 관점으로 해석하는 능력
이번 주 목표: syscall을 단순 함수가 아니라 OS가 제공하는 서비스 계약으로 보기
구체 회고: 유저 프로그램은 파일을 열고, 읽고, 쓰고, 종료하고 싶어 한다. 커널은 그 요구를 무조건 들어주는 것이 아니라 fd, pointer validation, exit status 같은 형식으로 제한한다. 이 제한이 곧 OS의 서비스 경계라는 것을 이해했다.
평가: 기술 요구사항을 시스템이 제공하는 인터페이스의 관점으로 읽기 시작했다.
이번 주에 가져가는 것: syscall은 기능이 아니라 계약이다.
AI 활용
정의: AI 답변을 그대로 적용하지 않고, 테스트와 코드 기준으로 검증하는 능력
이번 주 목표: 코덱스와 함께 구현하되 직접 질문하고 검증하기
구체 회고: 처음에는 답을 따라가다가 중간중간 질문이 생기면 멈췄다. 또 복사본과 로컬 결과가 다르게 나올 수 있다는 점을 겪으면서, AI가 제시한 방향도 실제 branch와 make check로 검증해야 한다는 기준을 세웠다.
평가: AI를 정답 생성기가 아니라 검증 가능한 가설 생성기로 쓰는 쪽에 가까워졌다.
이번 주에 가져가는 것: AI 답변의 최종 근거는 말이 아니라 테스트 결과다.
학습 민첩성
정의: 오해가 드러났을 때 학습 순서를 바꾸는 능력
이번 주 목표: 바로 구현보다 개념과 흐름을 먼저 잡기
구체 회고: 처음에는 바로 코드를 넣으려 했지만, 중간에 공부모드가 필요하다고 판단했다. Argument Passing, syscall, user memory, fd, fork를 순서대로 쪼개고, 모르는 개념을 질문하면서 학습 순서를 계속 조정했다.
평가: 계획을 고정하지 않고 이해 상태에 맞게 조정했다.
이번 주에 가져가는 것: 모르면 속도를 줄이는 것도 학습 전략이다.

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 코드를 볼 때 먼저 찾게 되는 것은 함수 이름이 아니라 경계다. 누가 누구를 믿는지, 누가 어떤 장부를 들고 있는지, 실패했을 때 누가 정리하는지. 이번 주의 변화는 그 질문들이 계속 거슬리기 시작했다는 데 있다.