<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://limjaehwan-real.github.io/BlogLim/feed.xml" rel="self" type="application/atom+xml" /><link href="https://limjaehwan-real.github.io/BlogLim/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-08-01T15:08:44+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/feed.xml</id><title type="html">ForRest</title><subtitle>Auto Programmer</subtitle><entry><title type="html">[WIL] 13주차</title><link href="https://limjaehwan-real.github.io/BlogLim/wil-13%EC%A3%BC/" rel="alternate" type="text/html" title="[WIL] 13주차" /><published>2026-05-28T09:00:00+09:00</published><updated>2026-05-28T09:00:00+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/wil-13%EC%A3%BC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/wil-13%EC%A3%BC/"><![CDATA[<h2 id="13주차-주간-회고-프레임워크-없이-신경망-학습-과정을-직접-만난-한-주">13주차 주간 회고: 프레임워크 없이 신경망 학습 과정을 직접 만난 한 주</h2>

<p>13주차에는 MNIST 손글씨 숫자 분류기를 NumPy만으로 구현했다. 딥러닝 프레임워크를 사용하면 <code class="language-plaintext highlighter-rouge">model.fit()</code>이나 <code class="language-plaintext highlighter-rouge">loss.backward()</code> 뒤에 숨어 지나가는 과정들을 직접 코드로 풀어보는 것이 목표였다. 그래서 이번 프로젝트의 초점은 단순히 MNIST 정확도를 높이는 것이 아니라, 신경망 학습이 실제로 어떤 계산 흐름으로 이루어지는지 확인하는 데 있었다.</p>

<p>이번 주의 흐름은 <code class="language-plaintext highlighter-rouge">데이터 전처리 -&gt; layer 구현 -&gt; forward -&gt; loss -&gt; backward -&gt; optimizer update -&gt; 학습 결과 검증</code>으로 이어졌다. 최종적으로는 다층 퍼셉트론 기반 MNIST 분류기를 만들었고, 테스트 정확도 <code class="language-plaintext highlighter-rouge">98.52%</code>를 기록했다. 최소 목표였던 95%와 권장 목표였던 97%를 모두 넘겼다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-13-mnist-mlp.svg" alt="MNIST MLP 구조와 NumPy 기반 학습 흐름" />
  <figcaption>이번 주에는 784차원 입력에서 10개 클래스 출력까지 이어지는 MLP 학습 과정을 NumPy 코드로 직접 구현했다</figcaption>
</figure>

<h2 id="1-프로젝트-목표와-최종-결과">1. 프로젝트 목표와 최종 결과</h2>

<p>이번 프로젝트의 목표는 MNIST 손글씨 숫자 이미지를 0부터 9까지 분류하는 신경망을 직접 만드는 것이었다. 데이터셋 자체는 익숙한 예제지만, 중요한 조건이 있었다. PyTorch나 TensorFlow 같은 프레임워크 없이 NumPy만 사용해야 했다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>데이터셋</td>
      <td>MNIST 손글씨 숫자 데이터</td>
    </tr>
    <tr>
      <td>문제 유형</td>
      <td>0~9 숫자 이미지 분류</td>
    </tr>
    <tr>
      <td>구현 방식</td>
      <td>NumPy 기반 직접 구현</td>
    </tr>
    <tr>
      <td>최소 목표</td>
      <td>테스트 정확도 95% 이상</td>
    </tr>
    <tr>
      <td>권장 목표</td>
      <td>테스트 정확도 97% 이상</td>
    </tr>
    <tr>
      <td>최종 결과</td>
      <td>테스트 정확도 98.52%</td>
    </tr>
  </tbody>
</table>

<p>최종 결과만 보면 단순히 “정확도가 잘 나왔다”로 끝낼 수도 있다. 하지만 이번 프로젝트에서 더 의미 있었던 부분은 정확도보다 그 정확도가 만들어지는 내부 과정을 직접 따라갔다는 점이었다. 입력이 layer를 지나 확률이 되고, 확률과 정답의 차이가 gradient가 되고, 그 gradient가 다시 각 파라미터를 움직이는 흐름을 코드로 확인했다.</p>

<h2 id="2-모델-구조">2. 모델 구조</h2>

<p>MNIST 이미지는 28x28 픽셀이다. 입력 단계에서는 이를 784차원 벡터로 펼쳤고, 두 개의 은닉층을 거쳐 10개 클래스에 대한 확률을 출력하도록 구성했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Input(784)
-&gt; Affine(784 -&gt; 512)
-&gt; BatchNorm
-&gt; ReLU
-&gt; Dropout
-&gt; Affine(512 -&gt; 256)
-&gt; BatchNorm
-&gt; ReLU
-&gt; Dropout
-&gt; Affine(256 -&gt; 10)
-&gt; Softmax
</code></pre></div></div>

<p>구조를 표로 정리하면 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>구성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>입력층</td>
      <td>784차원 벡터</td>
    </tr>
    <tr>
      <td>은닉층 1</td>
      <td>Affine(784 -&gt; 512), BatchNorm, ReLU, Dropout</td>
    </tr>
    <tr>
      <td>은닉층 2</td>
      <td>Affine(512 -&gt; 256), BatchNorm, ReLU, Dropout</td>
    </tr>
    <tr>
      <td>출력층</td>
      <td>Affine(256 -&gt; 10), Softmax</td>
    </tr>
    <tr>
      <td>손실 함수</td>
      <td>Cross Entropy Loss</td>
    </tr>
    <tr>
      <td>출력 값</td>
      <td>숫자 0~9에 대한 클래스별 확률</td>
    </tr>
  </tbody>
</table>

<p>처음에는 단순한 Affine-ReLU 구조만으로도 충분하지 않을까 생각했지만, 최종 모델에는 BatchNorm과 Dropout까지 넣었다. BatchNorm은 학습을 안정화하고, Dropout은 특정 뉴런이나 경로에 과하게 의존하지 않도록 정규화 역할을 한다.</p>

<h2 id="3-직접-구현한-모듈들">3. 직접 구현한 모듈들</h2>

<p>이번 프로젝트에서는 신경망을 구성하는 주요 부품을 각각 나누어 구현했다.</p>

<table>
  <thead>
    <tr>
      <th>파일</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/data.py</code></td>
      <td>MNIST 데이터 로드, 784차원 flatten, 0~1 정규화</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/activations.py</code></td>
      <td>ReLU, Softmax 구현</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/layers.py</code></td>
      <td>Affine, BatchNorm, Dropout 구현</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/losses.py</code></td>
      <td>Cross Entropy Loss 구현</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/optimizers.py</code></td>
      <td>SGD, Adam Optimizer 구현</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/network.py</code></td>
      <td>전체 신경망 구조 조립, forward/backward/loss/predict</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/training.py</code></td>
      <td>학습 루프, 평가 함수, loss curve 시각화</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/util.py</code></td>
      <td>데이터와 라벨을 같은 순서로 섞는 유틸리티</td>
    </tr>
  </tbody>
</table>

<p>프레임워크를 사용할 때는 activation, layer, loss, optimizer가 이미 준비된 부품처럼 느껴진다. 하지만 직접 구현해보니 각 모듈은 서로 강하게 연결되어 있었다. 특히 layer는 forward에서 중간 값을 저장해두어야 backward에서 gradient를 계산할 수 있었다. 이 점이 가장 먼저 체감된 차이였다.</p>

<h2 id="4-forward-loss-backward-update">4. Forward, Loss, Backward, Update</h2>

<p>학습 루프의 핵심 흐름은 네 단계로 정리할 수 있었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Forward
-&gt; Loss
-&gt; Backward
-&gt; Optimizer Update
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">NeuralNetwork.forward()</code>는 <code class="language-plaintext highlighter-rouge">OrderedDict</code>에 저장된 layer를 순서대로 실행한다. BatchNorm과 Dropout은 학습 모드와 평가 모드에서 동작이 다르기 때문에 <code class="language-plaintext highlighter-rouge">train</code> 인자를 함께 넘겨야 했다. 학습 중에는 mini-batch 통계와 dropout mask를 사용하지만, 평가 중에는 running mean, running variance와 보정된 출력을 사용한다.</p>

<p>Loss는 Cross Entropy Loss를 사용했다. 예측 확률과 실제 정답 레이블을 비교해 손실을 계산하고, <code class="language-plaintext highlighter-rouge">np.clip</code>으로 <code class="language-plaintext highlighter-rouge">log(0)</code>이 발생하지 않도록 처리했다.</p>

<p>역전파에서는 Softmax와 Cross Entropy를 결합한 gradient를 직접 만들었다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dout</span> <span class="o">=</span> <span class="n">y_pred</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
<span class="n">dout</span><span class="p">[</span><span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">x_batch</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="n">y_batch</span><span class="p">]</span> <span class="o">-=</span> <span class="mi">1</span>
<span class="n">dout</span> <span class="o">=</span> <span class="n">dout</span> <span class="o">/</span> <span class="n">x_batch</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>이 코드는 짧지만 의미는 크다. 모델이 낸 확률 분포에서 정답 클래스 위치만 1을 빼면, Softmax와 Cross Entropy를 함께 사용했을 때의 gradient가 된다. 수식으로만 볼 때는 추상적이었는데, 코드로 쓰고 나니 출력층에서 역방향으로 어떤 신호가 흘러가는지 훨씬 선명해졌다.</p>

<p>마지막으로 optimizer가 <code class="language-plaintext highlighter-rouge">model.params</code>를 직접 갱신한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>optimizer.update(model.params, grads)
</code></pre></div></div>

<p>최종 학습에서는 Adam Optimizer를 사용했다. SGD도 구현했지만, Adam이 손실을 더 빠르게 낮추는 것을 확인할 수 있었다.</p>

<h2 id="5-학습-설정과-파라미터-수">5. 학습 설정과 파라미터 수</h2>

<p>최종 학습 설정은 다음과 같았다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Optimizer</td>
      <td>Adam</td>
    </tr>
    <tr>
      <td>Learning rate</td>
      <td>0.001</td>
    </tr>
    <tr>
      <td>Epochs</td>
      <td>20</td>
    </tr>
    <tr>
      <td>Batch size</td>
      <td>128</td>
    </tr>
    <tr>
      <td>Dropout ratio</td>
      <td>0.5</td>
    </tr>
    <tr>
      <td>BatchNorm momentum</td>
      <td>0.9</td>
    </tr>
    <tr>
      <td>Weight initialization</td>
      <td>He initialization</td>
    </tr>
    <tr>
      <td>Bias initialization</td>
      <td>0</td>
    </tr>
    <tr>
      <td>입력 정규화</td>
      <td>0~1 범위</td>
    </tr>
    <tr>
      <td>Seed</td>
      <td>42</td>
    </tr>
  </tbody>
</table>

<p>총 학습 가능 파라미터 수는 <code class="language-plaintext highlighter-rouge">537,354</code>개였다.</p>

<table>
  <thead>
    <tr>
      <th>파라미터</th>
      <th>Shape</th>
      <th>개수</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">W1</code></td>
      <td><code class="language-plaintext highlighter-rouge">(784, 512)</code></td>
      <td>401,408</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">b1</code></td>
      <td><code class="language-plaintext highlighter-rouge">(512,)</code></td>
      <td>512</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">W2</code></td>
      <td><code class="language-plaintext highlighter-rouge">(512, 256)</code></td>
      <td>131,072</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">b2</code></td>
      <td><code class="language-plaintext highlighter-rouge">(256,)</code></td>
      <td>256</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">W3</code></td>
      <td><code class="language-plaintext highlighter-rouge">(256, 10)</code></td>
      <td>2,560</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">b3</code></td>
      <td><code class="language-plaintext highlighter-rouge">(10,)</code></td>
      <td>10</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gamma1</code></td>
      <td><code class="language-plaintext highlighter-rouge">(512,)</code></td>
      <td>512</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">beta1</code></td>
      <td><code class="language-plaintext highlighter-rouge">(512,)</code></td>
      <td>512</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gamma2</code></td>
      <td><code class="language-plaintext highlighter-rouge">(256,)</code></td>
      <td>256</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">beta2</code></td>
      <td><code class="language-plaintext highlighter-rouge">(256,)</code></td>
      <td>256</td>
    </tr>
  </tbody>
</table>

<p>직접 숫자를 세어보니 “작은 MNIST 모델”이라고 해도 생각보다 많은 파라미터가 움직이고 있었다. 특히 첫 번째 Affine layer의 <code class="language-plaintext highlighter-rouge">W1</code>만 해도 40만 개가 넘는다. 입력 차원 784와 은닉층 512가 곱해지는 순간 파라미터 수가 빠르게 커진다는 점을 체감했다.</p>

<h2 id="6-테스트와-최종-학습-결과">6. 테스트와 최종 학습 결과</h2>

<p>단위 테스트는 전체 <code class="language-plaintext highlighter-rouge">21 passed</code>를 달성했다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>실행 명령</td>
      <td><code class="language-plaintext highlighter-rouge">pytest tests/ -q</code></td>
    </tr>
    <tr>
      <td>결과</td>
      <td><code class="language-plaintext highlighter-rouge">21 passed</code></td>
    </tr>
    <tr>
      <td>실행 시간</td>
      <td>약 0.85초</td>
    </tr>
  </tbody>
</table>

<p>최종 학습 결과는 다음과 같았다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>학습 데이터</td>
      <td>MNIST train set 60,000개</td>
    </tr>
    <tr>
      <td>테스트 데이터</td>
      <td>MNIST test set 10,000개</td>
    </tr>
    <tr>
      <td>Epochs</td>
      <td>20</td>
    </tr>
    <tr>
      <td>Batch size</td>
      <td>128</td>
    </tr>
    <tr>
      <td>Optimizer</td>
      <td>Adam</td>
    </tr>
    <tr>
      <td>Learning rate</td>
      <td>0.001</td>
    </tr>
    <tr>
      <td>초기 train loss</td>
      <td>0.4139</td>
    </tr>
    <tr>
      <td>최종 train loss</td>
      <td>0.0430</td>
    </tr>
    <tr>
      <td>Test accuracy</td>
      <td>98.52%</td>
    </tr>
    <tr>
      <td>총 파라미터 수</td>
      <td>537,354</td>
    </tr>
    <tr>
      <td>학습 시간</td>
      <td>약 392.89초, 약 6분 32.89초</td>
    </tr>
    <tr>
      <td>평가 시간</td>
      <td>약 0.22초</td>
    </tr>
  </tbody>
</table>

<p>1 epoch 연습 실행에서도 테스트 정확도 95.63%를 기록했다. 이 결과는 전체 학습 파이프라인이 정상적으로 연결되어 있다는 신호였다. 최종 20 epoch 학습에서는 train loss가 <code class="language-plaintext highlighter-rouge">0.4139</code>에서 <code class="language-plaintext highlighter-rouge">0.0430</code>까지 감소했고, 테스트 정확도는 <code class="language-plaintext highlighter-rouge">98.52%</code>까지 올라갔다.</p>

<h2 id="7-이번-주에-배운-것">7. 이번 주에 배운 것</h2>

<p>이번 주의 가장 큰 배움은 신경망 학습이 “마법 같은 자동 계산”이 아니라는 점이었다. Forward에서 저장한 중간 값이 Backward의 재료가 되고, Backward에서 만든 gradient가 Optimizer의 입력이 되고, Optimizer가 파라미터를 바꾸면 다음 Forward의 출력이 달라진다. 이 연결이 끊기면 학습은 진행되지 않는다.</p>

<p>두 번째 배움은 BatchNorm과 Dropout처럼 익숙한 이름의 layer도 학습 모드와 평가 모드에서 전혀 다르게 동작한다는 점이다. BatchNorm은 학습 중에는 mini-batch 통계를 사용하지만, 평가 중에는 running mean과 running variance를 사용한다. Dropout은 학습 중에는 일부 뉴런을 비활성화하고, 평가 중에는 출력 크기를 보정한다. 같은 layer라도 현재 모델이 train 상태인지 eval 상태인지에 따라 계산이 달라진다.</p>

<p>세 번째 배움은 optimizer의 역할이다. Optimizer는 단순히 learning rate를 곱해 빼는 함수가 아니라, gradient를 어떤 방식으로 해석하고 누적할지 결정하는 전략이다. SGD와 Adam을 둘 다 구현해보면서 같은 gradient라도 업데이트 방식에 따라 수렴 속도와 안정성이 달라질 수 있다는 점을 확인했다.</p>

<h2 id="8-아쉬운-점과-개선-방향">8. 아쉬운 점과 개선 방향</h2>

<p>아쉬운 점은 기본 설정만으로 목표 정확도를 달성했기 때문에 하이퍼파라미터 탐색을 깊게 하지는 못했다는 점이다. Learning rate, dropout ratio, epoch 수를 바꾸면서 정확도와 과적합 여부를 비교했다면 결과 해석이 더 풍부해졌을 것이다.</p>

<p>또 하나의 개선 방향은 CNN 구조를 직접 구현하는 것이다. 이번 모델은 이미지를 784차원 벡터로 펼쳐서 사용했다. 이 방식은 간단하지만, 이미지의 공간적 구조를 충분히 활용하지는 못한다. Convolution layer를 직접 구현하면 MNIST 이미지의 지역 패턴을 더 자연스럽게 잡아낼 수 있을 것이다.</p>

<p>SGD와 Adam의 수렴 속도도 같은 조건에서 더 체계적으로 비교해보고 싶다. 이번에는 최종 결과 중심으로 Adam을 사용했지만, optimizer별 loss curve를 나란히 비교하면 각 방식의 특징을 더 명확히 설명할 수 있을 것 같다.</p>

<h2 id="현재-아쉬웠던-점과-다음-액션">현재 아쉬웠던 점과 다음 액션</h2>

<p>현재 아쉬운 점은 역전파 수식을 완전히 내 언어로 설명하는 데 아직 시간이 더 필요하다는 것이다. 코드로는 구현했지만, Affine, BatchNorm, Softmax-Cross Entropy의 gradient가 왜 그렇게 나오는지 누군가에게 처음부터 설명하려면 더 정리해야 한다.</p>

<p>다음 액션은 세 가지다.</p>

<p>첫째, 각 layer의 backward 계산을 수식과 코드가 나란히 보이도록 다시 정리한다. 둘째, SGD와 Adam을 같은 조건에서 비교하는 실험을 추가한다. 셋째, 가능하다면 CNN의 Convolution, Pooling layer를 NumPy로 직접 구현해본다.</p>

<p>13주차는 프레임워크가 대신 해주던 일을 직접 손으로 따라가본 주였다. 덕분에 신경망을 “모델을 선언하고 학습시키는 도구”가 아니라, 데이터와 파라미터와 gradient가 계속 주고받는 계산 그래프로 다시 보게 되었다.</p>]]></content><author><name></name></author><category term="Jungle" /><category term="WIL(Weekly I Learned)" /><summary type="html"><![CDATA[13주차에는 PyTorch나 TensorFlow 없이 NumPy만으로 MNIST 손글씨 숫자 분류 신경망을 직접 구현하며 Forward, Loss, Backward, Optimizer Update가 어떻게 연결되는지 코드 수준에서 학습했다.]]></summary></entry><entry><title type="html">[WIL] 12주</title><link href="https://limjaehwan-real.github.io/BlogLim/wil-12%EC%A3%BC/" rel="alternate" type="text/html" title="[WIL] 12주" /><published>2026-05-21T09:00:00+09:00</published><updated>2026-05-21T09:00:00+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/wil-12%EC%A3%BC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/wil-12%EC%A3%BC/"><![CDATA[<h2 id="12주차-주간-회고-가상메모리와-주소-변환을-다시-보는-한-주">12주차 주간 회고: 가상메모리와 주소 변환을 다시 보는 한 주</h2>

<p>12주차에는 Pintos의 다음 단계로 넘어가기 위해 가상메모리와 페이징을 다시 정리했다. 이전까지는 스레드, 시스템 콜, 파일 디스크립터, 프로세스 생명주기처럼 커널이 제공하는 기능을 중심으로 봤다면, 이번 주에는 프로세스가 바라보는 주소 공간과 실제 RAM 사이에 운영체제가 어떤 층을 끼워 넣는지를 집중해서 봤다.</p>

<p>핵심 흐름은 <code class="language-plaintext highlighter-rouge">가상메모리 → 페이징 → 페이지 테이블 → 페이지 폴트 → 세그멘테이션 → Copy-on-write</code>로 이어졌다. 처음에는 각각의 개념이 따로 떨어져 보였지만, 결국 모두 “프로세스에게는 독립적이고 연속적인 메모리처럼 보이게 하면서, 실제 물리 메모리는 운영체제가 유연하게 관리한다”는 하나의 목표로 묶여 있었다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-pintos-memory-map.png" alt="Pintos 가상 메모리 맵" />
  <figcaption>Pintos의 메모리 맵을 보며 유저 영역과 커널 영역, 가상 주소 공간의 경계를 다시 확인했다</figcaption>
</figure>

<h2 id="1-가상메모리의-큰-그림">1. 가상메모리의 큰 그림</h2>

<p>가상메모리는 물리적 메모리, 즉 RAM이 부족할 때 보조기억장치의 일부를 메모리처럼 사용할 수 있게 해주는 기술이다. 더 넓게 보면, 물리 메모리 크기와 배치에 상관없이 각 프로세스에게 독립적인 메모리 공간을 제공하는 운영체제의 핵심 추상화다.</p>

<p>예를 들어 프로세스 A와 B는 모두 자신이 <code class="language-plaintext highlighter-rouge">0x0000</code>부터 <code class="language-plaintext highlighter-rouge">0xFFFF</code>까지의 주소 공간 전체를 쓰고 있다고 생각할 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>프로세스 A -&gt; "나는 0x0000~0xFFFF 전체를 쓴다"
프로세스 B -&gt; "나도 0x0000~0xFFFF 전체를 쓴다"
실제 RAM    -&gt; OS가 필요한 부분만 쪼개서 각 프로세스에 매핑한다
</code></pre></div></div>

<p>프로세스 입장에서는 주소 공간이 자기 것처럼 보이지만, 실제로는 운영체제가 페이지 테이블을 통해 가상 주소를 물리 주소로 바꿔준다. 이 구조 덕분에 프로세스끼리 서로의 메모리를 함부로 침범하지 못하고, RAM이 부족할 때는 일부 데이터를 디스크로 내보내는 것도 가능해진다.</p>

<p>이번 주에 가장 먼저 잡아야 했던 감각은 “가상 주소는 진짜 물리 위치가 아니라, 운영체제가 해석해 주는 이름표”라는 점이었다.</p>

<h2 id="2-페이징과-외부-단편화">2. 페이징과 외부 단편화</h2>

<p>페이징은 메모리를 고정 크기의 작은 조각으로 나누어 관리하는 방식이다. 가상 메모리의 조각은 페이지, 물리 메모리의 조각은 프레임이라고 부른다. RAM이 부족하면 당장 쓰지 않는 페이지를 디스크의 스왑 영역으로 내보내고, 필요할 때 다시 불러올 수 있다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-paging.png" alt="페이징 구조" />
  <figcaption>페이징은 가상 주소 공간의 페이지를 물리 메모리의 프레임에 매핑한다</figcaption>
</figure>

<p>페이징의 중요한 장점은 외부 단편화를 해결할 수 있다는 것이다. 세그멘테이션처럼 연속된 큰 공간을 요구하는 방식에서는 전체 여유 공간이 충분해도, 그 공간이 여러 조각으로 흩어져 있으면 새 프로세스를 올리지 못할 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>처음 상태
[  A(100KB)  ][  B(200KB)  ][  C(150KB)  ]

B 해제 후
[  A(100KB)  ][   빈공간    ][  C(150KB)  ]
               (200KB)

새로운 프로세스 D(250KB) 등장
-&gt; 빈 공간이 200KB밖에 없으니 들어갈 수 없다
-&gt; 전체 여유 공간은 충분해도 연속된 공간이 부족하다
</code></pre></div></div>

<p>반면 페이징은 연속된 물리 공간을 요구하지 않는다. 프로세스 A가 12KB를 필요로 한다면 4KB 프레임 3개만 있으면 된다. 그 프레임들이 물리적으로 떨어져 있어도 페이지 테이블이 연결해 주기 때문에, 프로세스 입장에서는 연속된 메모리처럼 보인다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>프로세스 A가 12KB 필요
-&gt; 4KB 프레임 3개면 충분하다
-&gt; 3개가 물리적으로 떨어져 있어도 괜찮다

물리 메모리:
[프레임0: A의 1번째][프레임1: 비어있음][프레임2: A의 2번째][프레임3: A의 3번째]
</code></pre></div></div>

<p>결국 페이지 테이블이 떨어진 조각들을 연결해 주기 때문에, 프로세스는 실제 배치를 몰라도 된다. 이 부분이 페이징의 가장 큰 힘이었다.</p>

<h2 id="3-페이징의-주소-변환">3. 페이징의 주소 변환</h2>

<p>페이징에서 가상 주소는 보통 페이지 번호와 오프셋으로 나뉜다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>가상 주소 = [페이지 번호 | 오프셋]
</code></pre></div></div>

<p>주소 변환은 다음 순서로 일어난다.</p>

<ol>
  <li>CPU가 가상 주소를 만든다.</li>
  <li>페이지 번호로 페이지 테이블을 조회한다.</li>
  <li>페이지 테이블에서 프레임 번호를 얻는다.</li>
  <li>프레임 번호와 오프셋을 합쳐 물리 주소를 만든다.</li>
</ol>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>가상 주소 = [페이지 번호 | 오프셋]
페이지 테이블 조회 = 페이지 번호 -&gt; 프레임 번호
물리 주소 = 프레임 번호 + 오프셋
</code></pre></div></div>

<p>여기서 오프셋은 페이지 안에서의 위치다. 가상 페이지가 어떤 물리 프레임에 올라가든, 그 페이지 내부에서 몇 번째 바이트인지는 그대로 유지된다. 그래서 주소 변환은 “큰 조각의 번호는 바꾸고, 조각 내부 위치는 유지하는 작업”으로 이해할 수 있었다.</p>

<h2 id="4-페이지-테이블">4. 페이지 테이블</h2>

<p>페이지 테이블은 가상 메모리 주소와 물리 메모리 주소의 매핑 정보를 담는 테이블이다. 단순히 페이지 번호와 프레임 번호만 저장하는 것이 아니라, 그 페이지가 현재 메모리에 있는지, 접근된 적이 있는지, 수정되었는지, 어떤 권한을 갖는지도 함께 관리한다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-page-table.png" alt="페이지 테이블 구조" />
  <figcaption>페이지 테이블 엔트리는 프레임 번호뿐 아니라 접근, 변경, 유효, 권한 비트 같은 상태 정보도 함께 가진다</figcaption>
</figure>

<p>페이지 테이블에서 중요한 정보는 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>페이지 번호</td>
      <td>가상 주소에서 어떤 페이지인지 나타내는 값</td>
    </tr>
    <tr>
      <td>프레임 번호</td>
      <td>실제 물리 메모리의 어느 프레임에 올라가 있는지 나타내는 값</td>
    </tr>
    <tr>
      <td>접근 비트</td>
      <td>페이지가 메모리에 올라온 뒤 사용된 적이 있는지 알려주는 비트</td>
    </tr>
    <tr>
      <td>변경 비트</td>
      <td>페이지 내용이 수정되었는지 알려주는 비트</td>
    </tr>
    <tr>
      <td>유효 비트</td>
      <td>페이지가 현재 물리 메모리에 있는지 나타내는 비트</td>
    </tr>
    <tr>
      <td>읽기/쓰기/실행 비트</td>
      <td>페이지에 대한 접근 권한을 나타내는 비트</td>
    </tr>
  </tbody>
</table>

<p>이번 주에 정리하면서 페이지 테이블은 단순한 주소 변환표가 아니라, 운영체제가 메모리 정책을 수행하기 위한 판단 근거라는 점이 더 중요하게 느껴졌다. 어떤 페이지를 쫓아낼지, 디스크에 다시 써야 하는지, 접근을 허용할지 같은 판단이 이 비트들에 걸려 있다.</p>

<h2 id="5-페이지-폴트">5. 페이지 폴트</h2>

<p>페이지 폴트는 프로세스가 접근하려는 페이지가 현재 RAM에 없을 때 발생한다. 프로세스의 가상 주소 공간에는 존재하지만, 실제 물리 메모리에는 아직 올라와 있지 않은 데이터를 읽거나 쓰려고 할 때 생기는 문제다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-page-fault.png" alt="페이지 폴트 처리 흐름" />
  <figcaption>페이지 폴트는 실패가 아니라, 필요한 페이지를 뒤늦게 메모리에 올리는 계기가 될 수 있다</figcaption>
</figure>

<p>페이지 폴트가 발생하면 운영체제는 먼저 이것이 정상적으로 처리 가능한 접근인지 확인해야 한다. 예를 들어 스왑 영역에 내려가 있던 페이지를 다시 가져오면 되는 상황이라면, 빈 프레임을 확보하고 데이터를 읽어 온 뒤 페이지 테이블을 갱신하면 된다.</p>

<p>하지만 잘못된 주소에 접근했거나 권한이 없는 페이지에 접근했다면 이야기가 달라진다. 이 경우는 페이지를 가져오는 문제가 아니라 잘못된 메모리 접근이므로, 해당 프로세스를 종료해야 한다.</p>

<p>정리하면 페이지 폴트 처리는 다음 두 갈래로 나뉜다.</p>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>처리</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>유효한 접근이지만 페이지가 RAM에 없음</td>
      <td>디스크나 파일에서 페이지를 가져와 매핑</td>
    </tr>
    <tr>
      <td>잘못된 주소 또는 권한 위반</td>
      <td>프로세스 종료</td>
    </tr>
  </tbody>
</table>

<p>Pintos Project 3에서는 이 구분이 중요해질 것 같다. 모든 page fault를 똑같이 보면 안 되고, “복구 가능한 fault인가”와 “프로세스를 죽여야 하는 fault인가”를 나눠야 한다.</p>

<h2 id="6-세그멘테이션">6. 세그멘테이션</h2>

<p>세그멘테이션은 프로그램을 논리적으로 의미 있는 덩어리로 나누는 방식이다. 하나의 프로세스는 보통 코드, 데이터, 힙, 스택 같은 영역으로 나눠 볼 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>하나의 프로세스
├── 코드 세그먼트    (실행할 명령어들)
├── 데이터 세그먼트  (전역 변수)
├── 힙 세그먼트      (동적 할당 메모리)
└── 스택 세그먼트    (함수 호출, 지역 변수)
</code></pre></div></div>

<p>세그멘테이션의 주소 변환은 세그먼트 번호와 오프셋을 기준으로 이뤄진다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>가상 주소 = [세그먼트 번호 | 오프셋]
세그먼트 테이블 조회 = 세그먼트 번호 -&gt; 베이스 주소 + 한계값
물리 주소 = 베이스 주소 + 오프셋
</code></pre></div></div>

<p>이 방식은 논리적 구조를 표현하기 좋지만, 각 세그먼트가 서로 다른 크기를 갖기 때문에 외부 단편화가 생길 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>총 1000KB
[A 200KB][B 300KB][C 150KB][빈공간 350KB]

프로세스 B 종료
[A 200KB][빈공 300KB][C 150KB][빈공간 350KB]

프로세스 D(250KB) 할당
[A 200KB][D 250KB][빈공 50KB][C 150KB][빈공간 350KB]

프로세스 E(350KB) 요청
-&gt; 전체 빈 공간은 400KB지만 연속된 350KB 공간이 없으면 배치가 어렵다
</code></pre></div></div>

<p>세그멘테이션과 페이징을 비교하면서, “논리적으로 나누기 좋은 단위”와 “물리 메모리를 효율적으로 관리하기 좋은 단위”가 다를 수 있다는 점을 배웠다.</p>

<h2 id="7-copy-on-write">7. Copy-on-write</h2>

<p>Copy-on-write는 처음부터 모든 메모리를 복사하지 않고, 실제로 쓰기가 발생할 때 복사하는 전략이다. 특히 <code class="language-plaintext highlighter-rouge">fork()</code>처럼 부모 프로세스의 주소 공간을 자식에게 복제해야 하는 상황에서 중요하다.</p>

<p>일반적인 복사 방식이라면 <code class="language-plaintext highlighter-rouge">fork()</code> 시점에 부모의 모든 페이지를 자식에게 복사해야 한다. 하지만 실제로는 <code class="language-plaintext highlighter-rouge">fork()</code> 직후 <code class="language-plaintext highlighter-rouge">exec()</code>를 호출하는 경우도 많고, 부모와 자식이 모든 페이지를 전부 수정하지 않을 수도 있다. 이때 처음부터 전체를 복사하면 비용이 크다.</p>

<p>Copy-on-write는 다음 흐름으로 동작한다.</p>

<ol>
  <li>부모와 자식이 같은 물리 프레임을 공유한다.</li>
  <li>공유 중인 페이지는 읽기 전용으로 표시한다.</li>
  <li>둘 중 하나가 해당 페이지에 쓰려고 하면 page fault가 발생한다.</li>
  <li>운영체제가 새 프레임을 할당하고 기존 내용을 복사한다.</li>
  <li>쓰기를 시도한 프로세스의 페이지 테이블만 새 프레임을 가리키게 바꾼다.</li>
</ol>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-cow-1.png" alt="Copy-on-write 1단계" />
  <figcaption>처음에는 부모와 자식이 같은 프레임을 공유할 수 있다</figcaption>
</figure>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-cow-2.png" alt="Copy-on-write 2단계" />
  <figcaption>공유 페이지는 쓰기 시점까지 실제 복사를 미룬다</figcaption>
</figure>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-cow-3.png" alt="Copy-on-write 3단계" />
  <figcaption>쓰기 요청이 들어오면 page fault를 계기로 새 프레임을 준비한다</figcaption>
</figure>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-12-cow-4.png" alt="Copy-on-write 4단계" />
  <figcaption>수정하는 프로세스만 새 프레임을 바라보게 하여 불필요한 복사를 줄인다</figcaption>
</figure>

<p>이 구조를 보면서 page fault가 항상 “에러”만은 아니라는 점을 다시 느꼈다. 어떤 page fault는 운영체제가 lazy하게 일을 처리하기 위한 신호가 된다. Copy-on-write에서는 쓰기 권한 위반처럼 보이는 fault를 이용해, 그 순간에 진짜 복사를 수행한다.</p>

<h2 id="이번-주에-배운-점">이번 주에 배운 점</h2>

<p>이번 주의 가장 큰 배움은 가상메모리가 단순히 “RAM이 부족할 때 디스크를 쓰는 기술”만은 아니라는 점이다. 가상메모리는 프로세스에게 독립적인 주소 공간을 제공하고, 메모리 보호를 가능하게 하며, 실제 물리 메모리를 훨씬 유연하게 관리하게 해주는 운영체제의 핵심 추상화다.</p>

<p>두 번째 배움은 페이지 테이블을 주소 변환표로만 보면 부족하다는 점이다. 페이지 테이블 엔트리 안의 접근 비트, 변경 비트, 유효 비트, 권한 비트는 모두 운영체제가 메모리 정책을 수행하기 위한 정보다. 앞으로 Pintos에서 page fault handler나 frame table을 구현할 때 이 비트들이 실제 판단의 근거가 될 것이다.</p>

<p>세 번째는 page fault를 더 넓게 봐야 한다는 점이다. 잘못된 접근으로 프로세스를 죽여야 하는 fault도 있지만, 스왑된 페이지를 다시 가져오거나 Copy-on-write를 수행하기 위한 정상적인 fault도 있다. 결국 중요한 것은 fault가 발생했다는 사실 자체보다, 그 fault가 어떤 의미인지 해석하는 일이다.</p>

<h2 id="아쉬웠던-점과-다음-액션">아쉬웠던 점과 다음 액션</h2>

<p>아쉬운 점은 아직 가상 주소, 페이지 테이블, 프레임, 스왑, 파일 매핑이 실제 코드에서 어떤 구조체로 연결되는지 완전히 손에 잡히지는 않는다는 것이다. 개념은 정리했지만, Pintos Project 3의 구현으로 들어가면 supplemental page table, frame table, swap table 같은 구체적인 자료구조와 연결해야 한다.</p>

<p>다음 액션은 세 가지다.</p>

<p>첫째, Pintos의 <code class="language-plaintext highlighter-rouge">vm/</code> 디렉터리와 page fault handler 흐름을 읽으면서 가상 주소 하나가 어떤 자료구조들을 거쳐 물리 프레임에 도달하는지 추적한다. 둘째, 페이지 폴트가 발생했을 때 “유효한 lazy load인지, stack growth인지, 잘못된 접근인지”를 구분하는 기준을 표로 정리한다. 셋째, Copy-on-write가 실제로 구현된다면 어떤 비트와 reference count가 필요한지 손으로 그려본다.</p>

<p>12주차는 메모리를 “주소값”이 아니라 “운영체제가 관리하는 매핑과 정책”으로 다시 보기 시작한 주였다. 다음 주에는 이 개념들을 Pintos 코드 안에서 실제 자료구조와 함수 흐름으로 연결해 봐야겠다.</p>]]></content><author><name></name></author><category term="Jungle" /><category term="WIL(Weekly I Learned)" /><summary type="html"><![CDATA[12주차는 Pintos Project 3의 기반이 되는 가상메모리, 페이징, 페이지 테이블, 페이지 폴트, 세그멘테이션, Copy-on-write를 정리하며 프로세스가 보는 주소 공간과 실제 메모리가 어떻게 연결되는지 학습한 한 주였다.]]></summary></entry><entry><title type="html">[WIL] 10주</title><link href="https://limjaehwan-real.github.io/BlogLim/wil-10%EC%A3%BC/" rel="alternate" type="text/html" title="[WIL] 10주" /><published>2026-05-07T09:00:00+09:00</published><updated>2026-05-07T09:00:00+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/wil-10%EC%A3%BC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/wil-10%EC%A3%BC/"><![CDATA[<h2 id="10주차-주간-회고-유저-프로그램과-커널의-경계를-구현한-한-주">10주차 주간 회고: 유저 프로그램과 커널의 경계를 구현한 한 주</h2>

<p>10주차는 Pintos Project 2 User Programs에 본격적으로 들어간 주였다. 9주차에는 스레드, 스케줄링, 락, 세마포어처럼 커널 내부의 실행 흐름을 주로 봤다면, 이번 주부터는 유저 프로그램이 커널에게 무엇을 요청하고, 커널이 그 요청을 어디까지 믿을 수 있는지를 다뤘다.</p>

<p>이번 주에 정리한 글은 총 9개였다.</p>

<table>
  <thead>
    <tr>
      <th>글</th>
      <th>핵심 내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pintos project1 발표 피드백</code></td>
      <td>Project 1 발표에서 보완해야 할 설명 방식과 용어 정리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">User Program 특강</code></td>
      <td>Project 2의 큰 방향, argument passing과 syscall 우선순위</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Project 2 5/2 질문</code></td>
      <td>user mode/kernel mode, <code class="language-plaintext highlighter-rouge">intr_frame</code>, PML4 같은 기반 개념</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Project 2 User Program</code></td>
      <td>ELF, argument passing, syscall 레지스터, user stack 구조</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Project 2 깃북 요약</code></td>
      <td>Project 2 전체 요구사항과 구현 순서 요약</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Pintos Project2 업무 분담 내용</code></td>
      <td>팀별 담당 영역과 구현 인터페이스 정리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">퀴즈</code></td>
      <td>커널 모드, MLFQS, fork/exec, 스택과 힙, <code class="language-plaintext highlighter-rouge">#ifdef</code> 복습</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">발표 피드백</code></td>
      <td>발표 후 받은 질문과 개선 포인트 정리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">project 2 argument passing 2분 발표자료</code></td>
      <td>argument passing 발표를 위한 스택 구조와 ABI 정리</td>
    </tr>
  </tbody>
</table>

<p>이 글들을 묶어 보면 이번 주의 흐름은 <code class="language-plaintext highlighter-rouge">Project 1 발표 피드백 → User Program 개념 이해 → Argument Passing → Syscall Dispatch → User Pointer 검증 → FD/File Syscall → fork/exec/wait → ROX와 통합 테스트 → 발표 개선</code>으로 이어졌다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-10-user-kernel-boundary.svg" alt="Pintos Project 2에서 유저 프로그램이 syscall로 커널에 진입하고, 커널이 포인터와 인자를 검증한 뒤 커널 서비스를 실행하는 흐름" />
  <figcaption>Project 2의 핵심은 유저 프로그램과 커널 사이의 신뢰 경계를 안전하게 통과시키는 것이다</figcaption>
</figure>

<h2 id="1-project-1-발표-피드백에서-출발한-점">1. Project 1 발표 피드백에서 출발한 점</h2>

<p>이번 주 초에는 Project 1 발표 피드백을 먼저 정리했다. 피드백의 핵심은 “코드와 일치하는 그림을 직접 그리고, 공통 용어를 정확히 사용하라”는 것이었다. <code class="language-plaintext highlighter-rouge">lock</code> 구조체에 <code class="language-plaintext highlighter-rouge">holder</code>가 왜 필요한지, 세마포어와 락과 조건변수의 차이가 무엇인지, 발표 자료의 그림이 실제 구현과 맞는지 같은 질문이 나왔다.</p>

<p>이 피드백은 Project 2를 준비하는 데도 그대로 이어졌다. Pintos는 단순히 테스트를 통과시키는 프로젝트가 아니라, 내가 구현한 구조를 다른 사람에게 설명할 수 있어야 하는 프로젝트다. 특히 운영체제 코드는 추상적인 용어가 많기 때문에 비유로만 설명하면 금방 한계가 온다. 이번 주에는 “이 함수가 왜 필요한가”, “이 필드는 어떤 생명주기를 갖는가”, “이 테스트는 어떤 실패를 잡는가”를 더 명확히 말하는 연습이 필요했다.</p>

<h2 id="2-user-program의-큰-그림">2. User Program의 큰 그림</h2>

<p>Project 2의 목표는 유저 프로그램이 커널과 안전하게 소통할 수 있는 통로를 만드는 것이다. 유저 프로그램은 파일을 직접 열거나, 프로세스를 직접 만들거나, 커널 메모리를 직접 건드릴 수 없다. 대신 <code class="language-plaintext highlighter-rouge">syscall</code>이라는 공식 창구를 통해 커널에 요청한다.</p>

<p>흐름을 단순화하면 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>유저 프로그램
-&gt; open(), read(), write(), fork(), exec(), wait() 같은 함수 호출
-&gt; syscall instruction 실행
-&gt; syscall_handler()로 커널 진입
-&gt; syscall 번호와 인자 확인
-&gt; 유저 주소 검증
-&gt; 커널 내부 함수 호출
-&gt; 결과 반환 또는 프로세스 종료
</code></pre></div></div>

<p>여기서 중요한 점은 커널이 유저 프로그램을 믿으면 안 된다는 것이다. 유저 프로그램은 <code class="language-plaintext highlighter-rouge">NULL</code> 포인터를 넘길 수도 있고, 커널 주소를 넘길 수도 있고, 매핑되지 않은 주소를 넘길 수도 있다. 이때 커널 전체가 죽으면 안 된다. 잘못된 유저 프로그램만 <code class="language-plaintext highlighter-rouge">exit(-1)</code>로 종료되어야 한다.</p>

<p>User Program 특강에서 특히 강조된 것도 이 부분이었다. Project 2는 처음에는 argument passing과 <code class="language-plaintext highlighter-rouge">write()</code> 정도를 구현해서 테스트 출력이 보이게 만드는 것이 중요하다. 그 다음 <code class="language-plaintext highlighter-rouge">process_wait()</code>이 임시 구현으로 남아 있으면 테스트가 timeout될 수 있으므로, 프로그램 생성과 종료 흐름도 함께 맞춰야 한다.</p>

<h2 id="3-user-mode-kernel-mode-intr_frame-pml4">3. User Mode, Kernel Mode, <code class="language-plaintext highlighter-rouge">intr_frame</code>, PML4</h2>

<p>이번 주에는 구현 전에 운영체제의 보호 구조를 다시 정리했다. 유저 모드와 커널 모드를 나누는 이유는 운영체제와 하드웨어를 유저 프로그램의 실수나 악의적인 동작으로부터 보호하기 위해서다.</p>

<p>유저 모드에서는 물리 메모리 전체 접근, 페이지 테이블 변경, 디스크나 키보드 같은 장치 제어, 다른 프로세스 생성과 종료 같은 작업을 직접 할 수 없다. 이런 작업은 반드시 커널 모드에서만 수행되어야 한다.</p>

<p><code class="language-plaintext highlighter-rouge">intr_frame</code>은 유저 프로그램에서 커널로 들어오거나 다시 유저 모드로 돌아갈 때 CPU 상태를 저장하는 구조체다. Project 2에서는 이 구조체를 자주 보게 된다.</p>

<table>
  <thead>
    <tr>
      <th>레지스터</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rip</code></td>
      <td>유저 프로그램이 다음에 실행할 명령어 주소</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rsp</code></td>
      <td>유저 스택의 현재 위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rax</code></td>
      <td>syscall 번호 또는 반환값</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rdi</code>, <code class="language-plaintext highlighter-rouge">rsi</code>, <code class="language-plaintext highlighter-rouge">rdx</code>, <code class="language-plaintext highlighter-rouge">r10</code>, <code class="language-plaintext highlighter-rouge">r8</code>, <code class="language-plaintext highlighter-rouge">r9</code></td>
      <td>syscall 인자 전달</td>
    </tr>
  </tbody>
</table>

<p>PML4도 중요한 기반 개념이었다. x86-64에서 PML4는 최상위 페이지 테이블이고, 각 프로세스가 독립적인 가상 주소 공간을 갖게 해준다. 새 유저 프로세스가 생성되면 <code class="language-plaintext highlighter-rouge">pml4_create()</code>로 페이지 테이블을 만들고, ELF segment와 유저 스택을 가상 주소에 매핑한다. 문맥 전환 시에는 <code class="language-plaintext highlighter-rouge">pml4_activate()</code>를 통해 CPU가 현재 프로세스의 주소 공간을 바라보게 된다.</p>

<p>이 개념들이 잡혀야 Project 2의 <code class="language-plaintext highlighter-rouge">load()</code>, <code class="language-plaintext highlighter-rouge">setup_stack()</code>, <code class="language-plaintext highlighter-rouge">process_exec()</code>, <code class="language-plaintext highlighter-rouge">process_fork()</code>가 단순한 함수 이름이 아니라 “유저 주소 공간을 만들고 바꾸는 흐름”으로 보인다.</p>

<h2 id="4-elf-load와-argument-passing">4. ELF Load와 Argument Passing</h2>

<p>Project 2에서 가장 먼저 붙잡아야 할 구현은 argument passing이다. 테스트는 아래처럼 실행될 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>args-multiple some arguments for you!
</code></pre></div></div>

<p>유저 프로그램의 <code class="language-plaintext highlighter-rouge">main()</code>은 다음과 같은 값을 받아야 한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>argc = 5
argv[0] = "args-multiple"
argv[1] = "some"
argv[2] = "arguments"
argv[3] = "for"
argv[4] = "you!"
argv[5] = NULL
</code></pre></div></div>

<p>이를 위해 커널은 커맨드라인 문자열을 토큰으로 나누고, 문자열과 포인터 배열을 유저 스택에 올려야 한다. 핵심 함수는 <code class="language-plaintext highlighter-rouge">process_exec()</code>, <code class="language-plaintext highlighter-rouge">load()</code>, <code class="language-plaintext highlighter-rouge">setup_stack()</code>이고, 문자열 처리를 위해 <code class="language-plaintext highlighter-rouge">strtok_r</code>, <code class="language-plaintext highlighter-rouge">strlen</code>, <code class="language-plaintext highlighter-rouge">memcpy</code>, <code class="language-plaintext highlighter-rouge">strlcpy</code> 같은 함수도 확인해야 한다.</p>

<p>스택은 높은 주소에서 낮은 주소 방향으로 자란다. 하지만 <code class="language-plaintext highlighter-rouge">argv</code> 배열은 시작 주소부터 증가 방향으로 읽힌다. 그래서 스택에 데이터를 쌓는 순서와 유저 프로그램이 읽는 순서를 동시에 고려해야 한다.</p>

<p>대략적인 흐름은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 커맨드라인 문자열을 실행 파일 이름과 인자로 분리한다.
2. 실행 파일 이름으로 ELF를 연다.
3. ELF header와 program header를 읽고 segment를 메모리에 적재한다.
4. 유저 스택 페이지를 만든다.
5. 인자 문자열들을 스택에 복사한다.
6. 각 문자열의 주소를 argv 배열 형태로 스택에 올린다.
7. argc는 rdi, argv 주소는 rsi에 넣는다.
8. rsp를 새 스택 위치로 맞춘 뒤 do_iret()으로 유저 모드에 진입한다.
</code></pre></div></div>

<p>여기서 16바이트 정렬도 신경 써야 한다. x86-64 ABI 규약에서는 함수 호출 시 스택 정렬이 중요하다. 정렬이 틀리면 당장 모든 테스트가 깨지지는 않더라도, ABI 관점에서 올바른 실행 환경을 만든 것이 아니다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-10-argument-stack.svg" alt="args-multiple some arguments for you 실행 문자열을 argc와 argv 형태로 유저 스택에 배치하는 argument passing 구조" />
  <figcaption>argument passing은 문자열을 복사하는 일과 argv 포인터 배열을 올바른 순서로 만드는 일을 함께 맞춰야 한다</figcaption>
</figure>

<h2 id="5-syscall-dispatch와-기본-시스템-콜">5. Syscall Dispatch와 기본 시스템 콜</h2>

<p>Argument passing 다음에는 syscall handler가 필요하다. 유저 프로그램은 <code class="language-plaintext highlighter-rouge">lib/user/syscall.c</code>의 wrapper를 통해 syscall instruction을 실행하고, 커널은 <code class="language-plaintext highlighter-rouge">userprog/syscall.c</code>의 <code class="language-plaintext highlighter-rouge">syscall_handler()</code>에서 요청을 받는다.</p>

<p>syscall 번호는 <code class="language-plaintext highlighter-rouge">rax</code>에 들어 있고, 인자는 레지스터에 순서대로 들어 있다.</p>

<table>
  <thead>
    <tr>
      <th>값</th>
      <th>레지스터</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>syscall 번호</td>
      <td><code class="language-plaintext highlighter-rouge">rax</code></td>
    </tr>
    <tr>
      <td>1번째 인자</td>
      <td><code class="language-plaintext highlighter-rouge">rdi</code></td>
    </tr>
    <tr>
      <td>2번째 인자</td>
      <td><code class="language-plaintext highlighter-rouge">rsi</code></td>
    </tr>
    <tr>
      <td>3번째 인자</td>
      <td><code class="language-plaintext highlighter-rouge">rdx</code></td>
    </tr>
    <tr>
      <td>4번째 인자</td>
      <td><code class="language-plaintext highlighter-rouge">r10</code></td>
    </tr>
    <tr>
      <td>5번째 인자</td>
      <td><code class="language-plaintext highlighter-rouge">r8</code></td>
    </tr>
    <tr>
      <td>6번째 인자</td>
      <td><code class="language-plaintext highlighter-rouge">r9</code></td>
    </tr>
  </tbody>
</table>

<p>가장 먼저 구현할 syscall은 <code class="language-plaintext highlighter-rouge">halt</code>, <code class="language-plaintext highlighter-rouge">exit</code>, <code class="language-plaintext highlighter-rouge">write</code>다. <code class="language-plaintext highlighter-rouge">halt()</code>는 Pintos 전체를 종료하는 syscall이고, <code class="language-plaintext highlighter-rouge">exit(status)</code>는 현재 유저 프로세스를 종료한다. <code class="language-plaintext highlighter-rouge">write(1, buffer, size)</code>는 stdout 출력이기 때문에, 테스트 메시지를 보기 위해서도 빨리 연결해야 한다.</p>

<p><code class="language-plaintext highlighter-rouge">exit</code>는 단순히 현재 thread를 종료하는 것으로 끝나지 않는다. 현재 프로세스의 exit status를 저장하고, 부모가 <code class="language-plaintext highlighter-rouge">wait()</code>에서 읽을 수 있게 해야 한다. 또한 테스트가 기대하는 종료 메시지도 맞춰야 한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>프로세스이름: exit(status)
</code></pre></div></div>

<p>이 출력은 유저 프로세스 종료에 대해서만 나와야 한다. 커널 thread나 <code class="language-plaintext highlighter-rouge">halt</code> 처리에서 불필요하게 출력하면 테스트가 실패할 수 있다.</p>

<h2 id="6-user-pointer-검증">6. User Pointer 검증</h2>

<p>Project 2에서 가장 위험한 경계는 유저 포인터다. 유저 프로그램이 커널에 넘기는 주소는 모두 의심해야 한다.</p>

<p>예를 들어 아래 요청들은 모두 방어해야 한다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">create</span><span class="p">(</span><span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">open</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span> <span class="mh">0x20101234</span><span class="p">);</span>
<span class="n">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">kernel_addr</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span>
<span class="n">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">bad_ptr</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span>
<span class="n">exec</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span> <span class="n">invalid_addr</span><span class="p">);</span>
</code></pre></div></div>

<p>검증 기준은 크게 세 가지다.</p>

<table>
  <thead>
    <tr>
      <th>확인할 것</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NULL</code>이 아닌가</td>
      <td>아무 메모리도 가리키지 않는 주소를 읽으면 안 된다</td>
    </tr>
    <tr>
      <td>유저 주소 영역인가</td>
      <td>커널 주소를 유저가 넘기면 보호 경계가 깨진다</td>
    </tr>
    <tr>
      <td>실제로 매핑된 주소인가</td>
      <td>주소값이 유저 영역처럼 보여도 page table에 없을 수 있다</td>
    </tr>
  </tbody>
</table>

<p>문자열은 <code class="language-plaintext highlighter-rouge">\0</code>을 만날 때까지 한 글자씩 확인해야 한다. <code class="language-plaintext highlighter-rouge">strlen()</code>을 먼저 호출하면, 검증 전에 유저 메모리를 읽어버리는 문제가 생긴다. 버퍼도 시작 주소만 보면 부족하다. <code class="language-plaintext highlighter-rouge">read-boundary</code>, <code class="language-plaintext highlighter-rouge">write-boundary</code>, <code class="language-plaintext highlighter-rouge">exec-boundary</code>, <code class="language-plaintext highlighter-rouge">create-bound</code> 같은 테스트는 문자열이나 버퍼가 페이지 경계에 걸쳐 있는 상황을 만든다.</p>

<p>그래서 helper는 다음과 같은 방향이 좋다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>check_user_ptr(uaddr)
-&gt; NULL 확인
-&gt; 유저 주소인지 확인
-&gt; 현재 pml4에 매핑되어 있는지 확인
-&gt; 실패하면 exit(-1)

check_user_buffer(buffer, size)
-&gt; size가 0이면 통과
-&gt; buffer부터 buffer + size - 1까지 범위 검증
-&gt; 페이지 경계를 고려해 필요한 주소를 모두 확인

copy_in_string(uaddr)
-&gt; 문자열 전체를 검증
-&gt; 커널 메모리로 복사
-&gt; 커널 내부 함수에는 복사본을 전달
</code></pre></div></div>

<p>이번 주에 가장 크게 느낀 점은, user pointer 검증은 “예외 케이스 처리”가 아니라 Project 2 전체의 안전 기반이라는 것이다. 커널은 유저 프로그램이 무슨 값을 넘기든 살아 있어야 한다.</p>

<h2 id="7-file-descriptor와-file-syscall">7. File Descriptor와 File Syscall</h2>

<p>파일 syscall은 유저 프로그램이 파일 시스템을 직접 만지지 못하게 하기 위한 인터페이스다. 유저 프로그램은 <code class="language-plaintext highlighter-rouge">struct file *</code>을 볼 수 없고, 대신 파일 디스크립터 번호만 받는다.</p>

<p>fd의 기본 규칙은 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>fd</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0</code></td>
      <td>stdin</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1</code></td>
      <td>stdout</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">2 이상</code></td>
      <td>열린 파일</td>
    </tr>
  </tbody>
</table>

<p>프로세스마다 fd table 또는 fd list가 필요하다. 같은 숫자 <code class="language-plaintext highlighter-rouge">2</code>라도 프로세스마다 다른 파일을 의미할 수 있기 때문이다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>프로세스 A: fd 2 -&gt; a.txt
프로세스 B: fd 2 -&gt; b.txt
</code></pre></div></div>

<p>그래서 <code class="language-plaintext highlighter-rouge">struct thread</code> 안에 <code class="language-plaintext highlighter-rouge">fd_list</code>, <code class="language-plaintext highlighter-rouge">next_fd</code> 같은 필드를 두는 방식이 자연스럽다. <code class="language-plaintext highlighter-rouge">open()</code>이 성공하면 새 fd 번호를 발급하고, <code class="language-plaintext highlighter-rouge">read</code>, <code class="language-plaintext highlighter-rouge">write</code>, <code class="language-plaintext highlighter-rouge">close</code>, <code class="language-plaintext highlighter-rouge">filesize</code>, <code class="language-plaintext highlighter-rouge">seek</code>, <code class="language-plaintext highlighter-rouge">tell</code>은 그 fd로 실제 <code class="language-plaintext highlighter-rouge">struct file *</code>을 찾아 작업한다.</p>

<p>파일 관련 syscall 흐름은 다음과 같이 정리할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>syscall</th>
      <th>핵심 흐름</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create(file, size)</code></td>
      <td>파일명 검증 후 <code class="language-plaintext highlighter-rouge">filesys_create()</code> 호출</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">remove(file)</code></td>
      <td>파일명 검증 후 <code class="language-plaintext highlighter-rouge">filesys_remove()</code> 호출</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">open(file)</code></td>
      <td>파일명 검증 후 <code class="language-plaintext highlighter-rouge">filesys_open()</code>, fd 발급</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filesize(fd)</code></td>
      <td>fd로 file을 찾고 <code class="language-plaintext highlighter-rouge">file_length()</code> 반환</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">read(fd, buffer, size)</code></td>
      <td>버퍼 검증, fd 0은 stdin, 일반 파일은 <code class="language-plaintext highlighter-rouge">file_read()</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">write(fd, buffer, size)</code></td>
      <td>버퍼 검증, fd 1은 stdout, 일반 파일은 <code class="language-plaintext highlighter-rouge">file_write()</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">seek(fd, position)</code></td>
      <td>fd로 file을 찾고 <code class="language-plaintext highlighter-rouge">file_seek()</code> 호출</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tell(fd)</code></td>
      <td>fd로 file을 찾고 <code class="language-plaintext highlighter-rouge">file_tell()</code> 반환</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">close(fd)</code></td>
      <td>fd entry 제거 후 <code class="language-plaintext highlighter-rouge">file_close()</code></td>
    </tr>
  </tbody>
</table>

<p>파일 시스템은 내부 동기화가 없기 때문에 여러 프로세스가 동시에 접근하면 꼬일 수 있다. 그래서 file syscall 주변에 전역 <code class="language-plaintext highlighter-rouge">filesys_lock</code>을 두는 것도 중요한 설계 포인트다.</p>

<h2 id="8-wait-fork-exec의-생명주기">8. wait, fork, exec의 생명주기</h2>

<p>Project 2에서 가장 얽히는 부분은 부모-자식 관계다. <code class="language-plaintext highlighter-rouge">wait</code>, <code class="language-plaintext highlighter-rouge">fork</code>, <code class="language-plaintext highlighter-rouge">exec</code>는 각각 따로 보면 이해할 수 있지만, 실제 테스트에서는 서로 섞여 나온다.</p>

<p><code class="language-plaintext highlighter-rouge">wait(pid)</code>는 부모가 자식의 종료 상태를 회수하는 syscall이다. 여기서 중요한 조건은 다음과 같다.</p>

<ol>
  <li>기다리는 pid가 내 자식이어야 한다.</li>
  <li>같은 자식은 한 번만 wait할 수 있다.</li>
  <li>자식이 아직 살아 있으면 부모는 기다려야 한다.</li>
  <li>자식이 먼저 종료되었더라도 exit status는 남아 있어야 한다.</li>
  <li>커널에 의해 죽은 자식의 status는 <code class="language-plaintext highlighter-rouge">-1</code>로 전달되어야 한다.</li>
</ol>

<p>이 조건을 만족하려면 자식 thread 자체만 바라보면 위험하다. 자식이 종료되면 thread 구조체가 사라질 수 있기 때문이다. 그래서 부모와 자식 관계를 나타내는 별도 구조체가 필요하다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">child_info</span> <span class="p">{</span>
  <span class="n">tid_t</span> <span class="n">tid</span><span class="p">;</span>
  <span class="kt">int</span> <span class="n">exit_status</span><span class="p">;</span>
  <span class="n">bool</span> <span class="n">waited</span><span class="p">;</span>
  <span class="n">bool</span> <span class="n">exited</span><span class="p">;</span>
  <span class="k">struct</span> <span class="n">semaphore</span> <span class="n">wait_sema</span><span class="p">;</span>
  <span class="k">struct</span> <span class="n">list_elem</span> <span class="n">elem</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fork()</code>는 현재 프로세스를 복제한다. 부모에게는 자식 pid가 반환되고, 자식에게는 <code class="language-plaintext highlighter-rouge">0</code>이 반환되어야 한다. 이를 위해 부모의 <code class="language-plaintext highlighter-rouge">intr_frame</code>, 주소 공간, fd table, 부모-자식 metadata를 복제해야 한다. 특히 fd table은 같은 <code class="language-plaintext highlighter-rouge">struct file *</code>을 그대로 공유하기보다 <code class="language-plaintext highlighter-rouge">file_duplicate()</code>로 복제하는 쪽이 안전하다.</p>

<p><code class="language-plaintext highlighter-rouge">exec(cmd_line)</code>은 현재 프로세스를 새 프로그램으로 교체한다. <code class="language-plaintext highlighter-rouge">fork</code>가 복제라면 <code class="language-plaintext highlighter-rouge">exec</code>는 교체다. 성공하면 원래 코드로 돌아오면 안 되고, 실패하면 기존 프로세스에서 <code class="language-plaintext highlighter-rouge">-1</code>을 반환해야 한다.</p>

<p>흐름을 비교하면 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>syscall</th>
      <th>의미</th>
      <th>반환</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fork(name)</code></td>
      <td>현재 프로세스를 복제해 자식 생성</td>
      <td>부모는 자식 pid, 자식은 <code class="language-plaintext highlighter-rouge">0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exec(cmd_line)</code></td>
      <td>현재 프로세스의 실행 이미지를 새 프로그램으로 교체</td>
      <td>성공 시 반환 없음, 실패 시 <code class="language-plaintext highlighter-rouge">-1</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wait(pid)</code></td>
      <td>자식 종료를 기다리고 status 회수</td>
      <td>자식 exit status 또는 <code class="language-plaintext highlighter-rouge">-1</code></td>
    </tr>
  </tbody>
</table>

<p>이번 주에는 이 세 syscall이 단순 기능이 아니라 “프로세스 생명주기 관리”라는 하나의 문제로 묶여 있다는 점을 많이 느꼈다.</p>

<h2 id="9-실행-파일-write-deny와-통합-테스트">9. 실행 파일 Write Deny와 통합 테스트</h2>

<p>Project 2 후반에는 실행 중인 파일을 쓰지 못하게 막아야 한다. <code class="language-plaintext highlighter-rouge">rox-simple</code>, <code class="language-plaintext highlighter-rouge">rox-child</code>, <code class="language-plaintext highlighter-rouge">rox-multichild</code> 테스트가 이 부분을 확인한다.</p>

<p>핵심은 실행 파일을 load할 때 파일을 열고 <code class="language-plaintext highlighter-rouge">file_deny_write()</code>를 호출한 뒤, 프로세스가 살아 있는 동안 그 file 객체를 유지하는 것이다. load가 끝났다고 바로 close하면 실행 중인 파일에 대한 write deny가 풀릴 수 있다.</p>

<p>정리하면 다음 흐름이다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>load(executable)
-&gt; filesys_open()
-&gt; file_deny_write()
-&gt; thread_current()-&gt;exec_file에 저장
-&gt; 프로세스 실행 동안 유지
-&gt; process_exit()에서 file_close()
</code></pre></div></div>

<p>이 기능은 독립적으로 보이지만 실제로는 <code class="language-plaintext highlighter-rouge">open</code>, <code class="language-plaintext highlighter-rouge">write</code>, <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">wait</code>, <code class="language-plaintext highlighter-rouge">process_exit()</code>과 모두 연결된다. 그래서 단품 테스트를 통과한 뒤에도 <code class="language-plaintext highlighter-rouge">multi-recurse</code>, <code class="language-plaintext highlighter-rouge">multi-child-fd</code>, <code class="language-plaintext highlighter-rouge">rox-*</code>에서 다시 문제가 드러날 수 있다.</p>

<p>이번 주에 정리한 테스트 의존성도 이 사실을 잘 보여준다.</p>

<table>
  <thead>
    <tr>
      <th>순서</th>
      <th>기능</th>
      <th>대표 테스트</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Argument passing</td>
      <td><code class="language-plaintext highlighter-rouge">args-none</code>, <code class="language-plaintext highlighter-rouge">args-single</code>, <code class="language-plaintext highlighter-rouge">args-multiple</code>, <code class="language-plaintext highlighter-rouge">args-many</code>, <code class="language-plaintext highlighter-rouge">args-dbl-space</code></td>
    </tr>
    <tr>
      <td>2</td>
      <td>Basic syscall</td>
      <td><code class="language-plaintext highlighter-rouge">halt</code>, <code class="language-plaintext highlighter-rouge">exit</code></td>
    </tr>
    <tr>
      <td>3</td>
      <td>User pointer 검증</td>
      <td><code class="language-plaintext highlighter-rouge">*-null</code>, <code class="language-plaintext highlighter-rouge">*-bad-ptr</code>, <code class="language-plaintext highlighter-rouge">*-boundary</code>, <code class="language-plaintext highlighter-rouge">bad-*</code></td>
    </tr>
    <tr>
      <td>4</td>
      <td>FD table과 file syscall</td>
      <td><code class="language-plaintext highlighter-rouge">create-*</code>, <code class="language-plaintext highlighter-rouge">open-*</code>, <code class="language-plaintext highlighter-rouge">read-*</code>, <code class="language-plaintext highlighter-rouge">write-*</code>, <code class="language-plaintext highlighter-rouge">close-*</code></td>
    </tr>
    <tr>
      <td>5</td>
      <td>wait와 child status</td>
      <td><code class="language-plaintext highlighter-rouge">wait-simple</code>, <code class="language-plaintext highlighter-rouge">wait-twice</code>, <code class="language-plaintext highlighter-rouge">wait-killed</code>, <code class="language-plaintext highlighter-rouge">wait-bad-pid</code></td>
    </tr>
    <tr>
      <td>6</td>
      <td>fork</td>
      <td><code class="language-plaintext highlighter-rouge">fork-once</code>, <code class="language-plaintext highlighter-rouge">fork-multiple</code>, <code class="language-plaintext highlighter-rouge">fork-recursive</code>, <code class="language-plaintext highlighter-rouge">fork-read</code>, <code class="language-plaintext highlighter-rouge">fork-close</code></td>
    </tr>
    <tr>
      <td>7</td>
      <td>exec</td>
      <td><code class="language-plaintext highlighter-rouge">exec-once</code>, <code class="language-plaintext highlighter-rouge">exec-arg</code>, <code class="language-plaintext highlighter-rouge">exec-missing</code>, <code class="language-plaintext highlighter-rouge">exec-boundary</code>, <code class="language-plaintext highlighter-rouge">exec-read</code></td>
    </tr>
    <tr>
      <td>8</td>
      <td>executable write deny</td>
      <td><code class="language-plaintext highlighter-rouge">rox-simple</code>, <code class="language-plaintext highlighter-rouge">rox-child</code>, <code class="language-plaintext highlighter-rouge">rox-multichild</code></td>
    </tr>
    <tr>
      <td>9</td>
      <td>통합/스트레스</td>
      <td><code class="language-plaintext highlighter-rouge">multi-recurse</code>, <code class="language-plaintext highlighter-rouge">multi-child-fd</code></td>
    </tr>
  </tbody>
</table>

<p>테스트는 파일명 순서로 푸는 것보다 의존성 순서로 보는 것이 훨씬 좋다. <code class="language-plaintext highlighter-rouge">args-*</code>가 깨지면 <code class="language-plaintext highlighter-rouge">exec-arg</code>와 <code class="language-plaintext highlighter-rouge">multi-recurse</code>도 흔들리고, fd table이 불안정하면 <code class="language-plaintext highlighter-rouge">fork-read</code>, <code class="language-plaintext highlighter-rouge">fork-close</code>, <code class="language-plaintext highlighter-rouge">multi-child-fd</code>가 같이 흔들린다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-10-test-dependency.svg" alt="Pintos Project 2 테스트를 argument passing, basic syscall, pointer 검증, file syscall, wait, fork, exec, rox, stress 순서로 읽는 의존성 다이어그램" />
  <figcaption>Project 2 테스트는 기능이 쌓이는 순서로 읽어야 실패한 층을 빠르게 좁힐 수 있다</figcaption>
</figure>

<h2 id="10-팀-작업-분담과-인터페이스-합의">10. 팀 작업 분담과 인터페이스 합의</h2>

<p>이번 주에는 Project 2를 팀 단위로 어떻게 나눌지도 정리했다. 중요한 것은 담당을 나누더라도 <code class="language-plaintext highlighter-rouge">thread.h</code>, <code class="language-plaintext highlighter-rouge">process.c</code>, <code class="language-plaintext highlighter-rouge">syscall.c</code> 같은 병목 파일의 인터페이스를 먼저 맞추는 것이다.</p>

<p>역할을 크게 나누면 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>담당</th>
      <th>주 책임</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Thread 담당</td>
      <td>부모-자식 관계, exit status, wait, fork 동기화, cleanup</td>
    </tr>
    <tr>
      <td>FD 담당</td>
      <td>fd table/list, open/read/write/close helper, fd 복제</td>
    </tr>
    <tr>
      <td>Syscall Handler 담당 1</td>
      <td>syscall dispatch, halt/exit, pointer 검증, fork/exec/wait 연결</td>
    </tr>
    <tr>
      <td>Syscall Handler 담당 2</td>
      <td>파일 syscall 연결, stdin/stdout 처리, filesys lock</td>
    </tr>
  </tbody>
</table>

<p>초반에 반드시 합의해야 할 것은 <code class="language-plaintext highlighter-rouge">struct thread</code> 필드, <code class="language-plaintext highlighter-rouge">child_info</code> 구조체, <code class="language-plaintext highlighter-rouge">fd_entry</code> 구조체, user pointer 검증 정책, filesystem lock 위치다. 이 합의 없이 각자 구현하면 나중에 merge할 때 함수 이름과 상태 저장 위치가 어긋난다.</p>

<p>이번 주에 정리한 기준은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>잘못된 유저 메모리 주소
-&gt; exit(-1)

파일 없음, 잘못된 fd, 자식 아님, 이미 wait함 같은 논리 실패
-&gt; syscall별 실패값 반환
</code></pre></div></div>

<p>이 정책을 정해두면 <code class="language-plaintext highlighter-rouge">open(NULL)</code>과 <code class="language-plaintext highlighter-rouge">open("없는파일")</code>을 다르게 처리할 수 있다. 전자는 잘못된 포인터이므로 유저 프로세스 종료이고, 후자는 정상적인 요청의 실패이므로 <code class="language-plaintext highlighter-rouge">-1</code> 반환이다.</p>

<h2 id="11-발표-피드백과-다음-발표에서-보완할-점">11. 발표 피드백과 다음 발표에서 보완할 점</h2>

<p>발표 피드백에서는 몇 가지 질문이 남았다. 유저 모드와 커널 모드 전환이 구체적으로 어떻게 일어나는지, <code class="language-plaintext highlighter-rouge">do_iret()</code> 이후 유저 모드로 내려갔다가 syscall로 다시 커널 모드에 어떻게 복귀하는지, pointer validation을 비효율적으로 구현하지 않으려면 어떻게 해야 하는지 같은 질문이었다.</p>

<p>이 질문들은 모두 Project 2의 핵심과 맞닿아 있다. argument passing을 설명할 때는 단순히 “스택에 넣는다”가 아니라, <code class="language-plaintext highlighter-rouge">rsp</code>, <code class="language-plaintext highlighter-rouge">rdi</code>, <code class="language-plaintext highlighter-rouge">rsi</code>, ABI, <code class="language-plaintext highlighter-rouge">do_iret()</code>까지 연결해야 한다. syscall을 설명할 때도 “handler에서 switch문을 돈다”가 아니라, 유저 wrapper가 레지스터에 값을 넣고 syscall instruction으로 커널에 들어온다는 흐름을 말해야 한다.</p>

<p>또 OOM 테스트, fork/wait/exec 테스트를 더 챙기라는 피드백도 있었다. 이는 단순 기능 구현보다 실패 경로와 자원 회수가 중요하다는 뜻이다. 프로세스 생성이 실패했을 때 child metadata를 정리하는지, fd 복제 중 실패하면 이미 복제한 fd를 닫는지, 부모가 먼저 죽었을 때 자식의 <code class="language-plaintext highlighter-rouge">child_info</code>가 dangling pointer가 되지 않는지 같은 부분이 결국 안정성을 결정한다.</p>

<p>발표 방식도 개선할 점이 있었다. 발표 스크립트를 그대로 읽기보다, 핵심 내용을 이해하고 말해야 한다. 발표 시간도 지켜야 하고, 컴퓨터 조작과 시연 흐름도 미리 익혀야 한다. 결국 기술 발표는 내용을 아는 것과 전달하는 것이 둘 다 필요하다.</p>

<h2 id="이번-주에-배운-점">이번 주에 배운 점</h2>

<p>이번 주의 가장 큰 배움은 “커널은 경계를 관리하는 프로그램”이라는 점이었다. Project 1에서는 스레드 사이의 실행 순서와 공유 자원을 관리했다면, Project 2에서는 유저 프로그램과 커널 사이의 신뢰 경계를 관리한다. 유저 포인터를 검증하고, syscall 번호와 인자를 해석하고, fd라는 간접 참조를 통해 파일을 다루게 하는 모든 장치가 그 경계를 지키기 위한 구조였다.</p>

<p>두 번째 배움은 process의 생명주기를 thread 하나로만 보면 안 된다는 점이다. Pintos에서는 유저 프로세스 하나가 thread 하나로 표현되지만, 부모가 자식의 종료 상태를 나중에 읽어야 하므로 thread가 사라진 뒤에도 남아 있어야 하는 정보가 있다. <code class="language-plaintext highlighter-rouge">child_info</code> 같은 별도 구조체가 필요한 이유를 여기서 이해했다.</p>

<p>세 번째는 테스트 의존성을 먼저 읽는 습관이다. Project 2는 구현 범위가 넓어서 무작정 파일 순서대로 읽으면 길을 잃기 쉽다. <code class="language-plaintext highlighter-rouge">args-*</code>, <code class="language-plaintext highlighter-rouge">halt/exit</code>, pointer, fd, wait, fork, exec, rox, multi 순서로 의존성을 잡아두면 지금 실패한 테스트가 어느 층의 문제인지 더 빨리 좁힐 수 있다.</p>

<h2 id="아쉬웠던-점과-다음-액션">아쉬웠던 점과 다음 액션</h2>

<p>아쉬운 점은 아직 <code class="language-plaintext highlighter-rouge">fork</code>, <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">wait</code>의 실패 경로를 완전히 내 언어로 설명할 만큼 익숙하지 않다는 것이다. 정상 흐름은 따라갈 수 있지만, 부모가 먼저 종료되는 경우, 자식이 먼저 종료되는 경우, fork 도중 fd 복제가 실패하는 경우, exec load가 실패하는 경우를 모두 안정적으로 설명하려면 더 정리해야 한다.</p>

<p>다음 액션은 세 가지다.</p>

<p>첫째, <code class="language-plaintext highlighter-rouge">process_exec()</code>, <code class="language-plaintext highlighter-rouge">load()</code>, <code class="language-plaintext highlighter-rouge">setup_stack()</code>을 실제 코드 기준으로 다시 읽고 argument passing의 스택 배치를 손으로 그려본다. 둘째, <code class="language-plaintext highlighter-rouge">syscall_handler()</code>에서 각 syscall이 어떤 검증 helper를 거쳐야 하는지 표로 정리한다. 셋째, <code class="language-plaintext highlighter-rouge">wait</code>, <code class="language-plaintext highlighter-rouge">fork</code>, <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">process_exit()</code>의 정상/실패/cleanup 흐름을 상태도처럼 정리한다.</p>

<p>10주차는 유저 프로그램과 커널이 만나는 경계를 처음으로 제대로 만진 주였다. 다음 주에는 이 경계를 더 단단하게 만들고, 단품 테스트를 넘어 <code class="language-plaintext highlighter-rouge">multi-*</code>, <code class="language-plaintext highlighter-rouge">rox-*</code>, OOM 계열까지 버틸 수 있는 구조로 정리해 나가야겠다.</p>]]></content><author><name></name></author><category term="Jungle" /><category term="WIL(Weekly I Learned)" /><summary type="html"><![CDATA[10주차는 Pintos Project 2 User Programs를 중심으로 argument passing, syscall, user pointer 검증, file descriptor, fork/exec/wait, 실행 파일 write deny까지 정리하며 유저 프로그램과 커널이 만나는 경계를 학습한 한 주였다.]]></summary></entry><entry><title type="html">[WIL] 9주</title><link href="https://limjaehwan-real.github.io/BlogLim/wil-9%EC%A3%BC/" rel="alternate" type="text/html" title="[WIL] 9주" /><published>2026-04-30T09:00:00+09:00</published><updated>2026-04-30T09:00:00+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/wil-9%EC%A3%BC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/wil-9%EC%A3%BC/"><![CDATA[<h2 id="9주차-주간-회고-pintos로-운영체제를-코드-안에서-만난-한-주">9주차 주간 회고: Pintos로 운영체제를 코드 안에서 만난 한 주</h2>

<p>9주차부터는 4주 동안 진행되는 Pintos 프로젝트가 시작됐다. 이번 주의 범위는 Project 1 Threads였다. 앞으로 <code class="language-plaintext highlighter-rouge">threads</code>, <code class="language-plaintext highlighter-rouge">user programs</code>, <code class="language-plaintext highlighter-rouge">virtual memory</code>까지 이어지기 때문에 첫 주에 방향을 잘 잡는 것이 중요했다. 발제에서도 “진도를 다 빼는 것보다 팀원 전체가 이해하는 것이 우선”이라는 말이 강조됐다.</p>

<p>이번 주의 큰 흐름은 <code class="language-plaintext highlighter-rouge">Pintos 구조 파악 → Alarm Clock → Priority Scheduling → Synchronization Waiters → Priority Donation → MLFQS → 발표 정리</code>로 이어졌다. 이전 주차까지는 네트워크와 서버를 주로 봤다면, 이번 주부터는 운영체제가 CPU를 누구에게 줄지, 스레드를 언제 재울지, 공유 자원 앞에서 어떤 순서를 보장할지를 직접 코드로 다루게 됐다.</p>

<h2 id="1-pintos-project-1의-전체-지도">1. Pintos Project 1의 전체 지도</h2>

<p>Pintos는 작은 교육용 OS지만, 막상 코드를 열어보면 실제 운영체제의 핵심 고민이 거의 다 들어 있다. 이번 주에는 특히 <code class="language-plaintext highlighter-rouge">threads/</code> 아래의 <code class="language-plaintext highlighter-rouge">timer.c</code>, <code class="language-plaintext highlighter-rouge">thread.c</code>, <code class="language-plaintext highlighter-rouge">synch.c</code>, <code class="language-plaintext highlighter-rouge">thread.h</code>, <code class="language-plaintext highlighter-rouge">synch.h</code>를 중심으로 봤다.</p>

<p>Project 1에서 구현해야 할 내용은 크게 네 단계로 나눌 수 있었다.</p>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>핵심 목표</th>
      <th>대표 테스트</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alarm Clock</td>
      <td>busy waiting 없이 스레드를 재웠다가 깨우기</td>
      <td><code class="language-plaintext highlighter-rouge">alarm-single</code>, <code class="language-plaintext highlighter-rouge">alarm-multiple</code>, <code class="language-plaintext highlighter-rouge">alarm-simultaneous</code></td>
    </tr>
    <tr>
      <td>Priority Scheduling</td>
      <td>높은 priority 스레드가 먼저 실행되게 하기</td>
      <td><code class="language-plaintext highlighter-rouge">priority-change</code>, <code class="language-plaintext highlighter-rouge">priority-preempt</code>, <code class="language-plaintext highlighter-rouge">priority-fifo</code></td>
    </tr>
    <tr>
      <td>Sync Waiters</td>
      <td>semaphore/condition variable에서도 priority 순서 유지</td>
      <td><code class="language-plaintext highlighter-rouge">priority-sema</code>, <code class="language-plaintext highlighter-rouge">priority-condvar</code>, <code class="language-plaintext highlighter-rouge">alarm-priority</code></td>
    </tr>
    <tr>
      <td>Priority Donation</td>
      <td>lock 대기 중 발생하는 priority inversion 해결</td>
      <td><code class="language-plaintext highlighter-rouge">priority-donate-one</code>, <code class="language-plaintext highlighter-rouge">priority-donate-nest</code>, <code class="language-plaintext highlighter-rouge">priority-donate-chain</code></td>
    </tr>
    <tr>
      <td>MLFQS</td>
      <td><code class="language-plaintext highlighter-rouge">nice</code>, <code class="language-plaintext highlighter-rouge">recent_cpu</code>, <code class="language-plaintext highlighter-rouge">load_avg</code> 기반 고급 스케줄러 구현</td>
      <td><code class="language-plaintext highlighter-rouge">mlfqs-load-avg</code>, <code class="language-plaintext highlighter-rouge">mlfqs-recent-1</code>, <code class="language-plaintext highlighter-rouge">mlfqs-fair-*</code></td>
    </tr>
  </tbody>
</table>

<p>팀 일정도 이 순서에 맞춰 잡았다. 4/24에는 구조 이해와 협업 룰을 정리하고, 4/25에는 Alarm Clock, 4/27에는 Priority 기본과 Sync Waiters, 4/28에는 Donation, 4/29에는 MLFQS와 발표 초안을 목표로 삼았다. 작업을 카드 단위로 나누고, 테스트 이름을 기준으로 실패 원인을 좁히는 방식이 중요했다.</p>

<h2 id="2-alarm-clock과-busy-waiting-제거">2. Alarm Clock과 Busy Waiting 제거</h2>

<p>처음 마주한 문제는 <code class="language-plaintext highlighter-rouge">timer_sleep()</code>이었다. 기존 방식은 아래처럼 시간이 지났는지 계속 확인하면서 CPU를 양보하는 구조였다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span> <span class="p">(</span><span class="n">timer_elapsed</span><span class="p">(</span><span class="n">start</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">ticks</span><span class="p">)</span>
    <span class="n">thread_yield</span><span class="p">();</span>
</code></pre></div></div>

<p>이 코드는 겉으로는 “잠자는” 것처럼 보이지만 실제로는 계속 깨어나서 조건을 확인한다. 그래서 CPU를 불필요하게 사용한다. 이번 주에 정리한 핵심은 sleeping list를 두고, 스레드를 진짜로 blocked 상태로 만든 뒤 timer interrupt에서 깨우는 구조였다.</p>

<p>흐름은 다음과 같이 볼 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>timer_sleep()
-&gt; 현재 thread에 wakeup_tick 기록
-&gt; sleeping_list에 삽입
-&gt; thread_block()
-&gt; timer_interrupt()에서 깨어날 시간이 된 thread를 thread_unblock()
</code></pre></div></div>

<p>여기서 특히 조심해야 했던 부분은 interrupt on/off 구간이었다. <code class="language-plaintext highlighter-rouge">thread_block()</code>은 현재 스레드의 상태를 <code class="language-plaintext highlighter-rouge">THREAD_BLOCKED</code>로 바꾸고 <code class="language-plaintext highlighter-rouge">schedule()</code>을 호출한다. 그런데 그 사이에 timer interrupt가 끼어들면 커널은 이미 blocked로 표시된 스레드를 여전히 running 중인 것처럼 다루게 될 수 있다. 그래서 리스트 조작, block/unblock, schedule 주변에서는 interrupt를 끄고 상태 전환을 원자적으로 다뤄야 한다는 점이 중요했다.</p>

<h2 id="3-priority-scheduling과-sync-waiters">3. Priority Scheduling과 Sync Waiters</h2>

<p>Alarm Clock 다음은 ready list와 동기화 객체의 waiters를 priority 기준으로 관리하는 일이었다. 단순히 <code class="language-plaintext highlighter-rouge">ready_list</code>만 정렬하면 끝나는 문제가 아니었다. 높은 priority 스레드가 새로 ready 상태가 되었을 때 현재 스레드가 CPU를 양보해야 하고, semaphore나 condition variable에서 깨어나는 순서도 priority를 따라야 했다.</p>

<p>이번 주에 자주 확인한 지점은 다음 함수들이었다.</p>

<table>
  <thead>
    <tr>
      <th>함수/구조</th>
      <th>확인한 이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">thread_unblock()</code></td>
      <td>blocked 스레드를 ready list에 넣을 때 priority 순서가 유지되는지 확인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">thread_yield()</code></td>
      <td>현재 스레드가 CPU를 양보할 때 ready list 순서가 깨지지 않는지 확인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sema_up()</code></td>
      <td>semaphore waiters 중 높은 priority 스레드를 먼저 깨우는지 확인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cond_signal()</code></td>
      <td>condition variable waiters에서도 priority 순서가 반영되는지 확인</td>
    </tr>
  </tbody>
</table>

<p>조건변수를 따로 정리하면서도 <code class="language-plaintext highlighter-rouge">if</code>가 아니라 <code class="language-plaintext highlighter-rouge">while</code>로 조건을 다시 확인해야 한다는 점을 배웠다. 스레드가 깨어났다고 해서 조건이 여전히 참이라는 보장은 없다. 다른 스레드가 먼저 자원을 가져갔을 수도 있고, 가짜 깨어남처럼 보이는 상황도 생길 수 있다. 그래서 조건변수는 항상 mutex와 함께 쓰고, 깨어난 뒤에도 조건을 다시 확인하는 패턴이 필요하다.</p>

<h2 id="4-priority-inversion과-donation">4. Priority Inversion과 Donation</h2>

<p>이번 주에 가장 설명하기 어려웠고, 동시에 가장 운영체제답게 느껴졌던 주제는 priority inversion이었다. 낮은 priority 스레드가 lock을 잡고 있고, 높은 priority 스레드가 그 lock을 기다리는 상황에서 중간 priority 스레드가 CPU를 계속 사용하면 높은 priority 스레드가 오히려 오래 기다리게 된다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-9-priority-inversion.png" alt="우선순위 역전과 priority inheritance 해결 과정" />
  <figcaption>낮은 priority 스레드가 가진 lock 때문에 높은 priority 스레드가 지연되는 priority inversion 상황</figcaption>
</figure>

<p>예를 들어 <code class="language-plaintext highlighter-rouge">L</code>, <code class="language-plaintext highlighter-rouge">M</code>, <code class="language-plaintext highlighter-rouge">H</code> 세 스레드가 있다고 하자. <code class="language-plaintext highlighter-rouge">L</code>은 낮은 priority지만 lock을 가지고 있고, <code class="language-plaintext highlighter-rouge">H</code>는 높은 priority지만 그 lock이 필요해서 blocked 된다. 이때 lock이 필요 없는 <code class="language-plaintext highlighter-rouge">M</code>이 계속 CPU를 차지하면 <code class="language-plaintext highlighter-rouge">L</code>이 lock을 풀 기회를 얻지 못하고, 결과적으로 <code class="language-plaintext highlighter-rouge">H</code>도 계속 기다린다. 문제의 원인은 <code class="language-plaintext highlighter-rouge">H</code>가 약해서가 아니라, <code class="language-plaintext highlighter-rouge">L</code>이 CPU를 받지 못해 lock을 풀 수 없다는 점이다.</p>

<p>해결 방법은 priority donation이다. <code class="language-plaintext highlighter-rouge">H</code>가 <code class="language-plaintext highlighter-rouge">L</code>이 가진 lock을 기다릴 때, <code class="language-plaintext highlighter-rouge">H</code>의 priority를 잠시 <code class="language-plaintext highlighter-rouge">L</code>에게 빌려준다. 그러면 scheduler는 <code class="language-plaintext highlighter-rouge">L</code>을 더 높은 priority로 보고 CPU를 먼저 주고, <code class="language-plaintext highlighter-rouge">L</code>은 작업을 끝내 lock을 해제한다. lock이 해제되면 <code class="language-plaintext highlighter-rouge">H</code>가 바로 실행될 수 있고, <code class="language-plaintext highlighter-rouge">L</code>은 원래 priority로 돌아간다.</p>

<p>구현 관점에서는 <code class="language-plaintext highlighter-rouge">base_priority</code>와 <code class="language-plaintext highlighter-rouge">effective_priority</code>를 나눠 생각하는 것이 중요했다.</p>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">base_priority</code></td>
      <td>스레드가 원래 가지고 있던 priority</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">effective_priority</code></td>
      <td>donation을 반영해 실제 스케줄링에 사용하는 priority</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">waiting_lock</code></td>
      <td>현재 스레드가 기다리고 있는 lock</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lock-&gt;holder</code></td>
      <td>해당 lock을 들고 있는 스레드</td>
    </tr>
  </tbody>
</table>

<p>Donation은 단순히 한 번 값을 올리는 것으로 끝나지 않는다. <code class="language-plaintext highlighter-rouge">A</code>가 <code class="language-plaintext highlighter-rouge">B</code>를 기다리고, <code class="language-plaintext highlighter-rouge">B</code>가 <code class="language-plaintext highlighter-rouge">C</code>를 기다리는 nested donation이나 chain donation도 생긴다. 그래서 <code class="language-plaintext highlighter-rouge">waiting_lock</code>을 따라 priority를 전파하고, <code class="language-plaintext highlighter-rouge">lock_release()</code> 시점에는 해당 lock과 관련된 donation만 제거한 뒤 priority를 다시 계산해야 한다.</p>

<h2 id="5-mlfqs와-테스트를-읽는-법">5. MLFQS와 테스트를 읽는 법</h2>

<p>MLFQS는 수식이 많아서 처음에는 구현보다 개념을 잡는 데 시간이 걸렸다. <code class="language-plaintext highlighter-rouge">load_avg</code>, <code class="language-plaintext highlighter-rouge">recent_cpu</code>, <code class="language-plaintext highlighter-rouge">nice</code> 값을 fixed-point arithmetic으로 계산하고, 일정 tick마다 priority를 다시 계산해야 한다. 여기서는 정확한 수식 구현도 중요하지만, 어떤 테스트가 무엇을 보는지 먼저 아는 것이 훨씬 도움이 됐다.</p>

<p>Pintos 테스트는 <code class="language-plaintext highlighter-rouge">.c</code>, <code class="language-plaintext highlighter-rouge">.ck</code>, <code class="language-plaintext highlighter-rouge">Make.tests</code>를 함께 봐야 한다. <code class="language-plaintext highlighter-rouge">.c</code>는 실제 테스트 프로그램이고, <code class="language-plaintext highlighter-rouge">.ck</code>는 출력 순서와 내용을 검사한다. 기능이 얼추 맞아 보여도 출력 순서가 다르면 실패할 수 있기 때문에 scheduling 테스트에서는 테스트가 요구하는 순서를 이해하는 것이 핵심이었다.</p>

<p>이번 주에 정리한 테스트 읽기 순서는 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>프로젝트 단계</th>
      <th>먼저 볼 테스트</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alarm Clock</td>
      <td><code class="language-plaintext highlighter-rouge">alarm-single</code> → <code class="language-plaintext highlighter-rouge">alarm-multiple</code> → <code class="language-plaintext highlighter-rouge">alarm-simultaneous</code></td>
    </tr>
    <tr>
      <td>Priority 기본</td>
      <td><code class="language-plaintext highlighter-rouge">priority-change</code> → <code class="language-plaintext highlighter-rouge">priority-preempt</code> → <code class="language-plaintext highlighter-rouge">priority-fifo</code></td>
    </tr>
    <tr>
      <td>Sync Waiters</td>
      <td><code class="language-plaintext highlighter-rouge">priority-sema</code>, <code class="language-plaintext highlighter-rouge">priority-condvar</code>, <code class="language-plaintext highlighter-rouge">alarm-priority</code></td>
    </tr>
    <tr>
      <td>Donation</td>
      <td><code class="language-plaintext highlighter-rouge">priority-donate-one</code> → <code class="language-plaintext highlighter-rouge">priority-donate-multiple</code> → <code class="language-plaintext highlighter-rouge">priority-donate-nest</code> → <code class="language-plaintext highlighter-rouge">priority-donate-chain</code></td>
    </tr>
    <tr>
      <td>MLFQS</td>
      <td><code class="language-plaintext highlighter-rouge">mlfqs-load-*</code> → <code class="language-plaintext highlighter-rouge">mlfqs-recent-1</code> → <code class="language-plaintext highlighter-rouge">mlfqs-fair-*</code> → <code class="language-plaintext highlighter-rouge">mlfqs-nice-*</code></td>
    </tr>
  </tbody>
</table>

<p>실패한 테스트 이름은 꽤 많은 정보를 준다. 예를 들어 <code class="language-plaintext highlighter-rouge">alarm-simultaneous</code>가 실패하면 같은 tick에 여러 스레드를 모두 깨우는지 먼저 보면 되고, <code class="language-plaintext highlighter-rouge">priority-sema</code>가 실패하면 ready list보다 semaphore waiters의 정렬을 먼저 의심할 수 있다.</p>

<h2 id="6-운영체제-강의와-pintos가-연결된-지점">6. 운영체제 강의와 Pintos가 연결된 지점</h2>

<p>운영체제 강의에서는 OS를 application과 hardware 사이에 있는 계층으로 봤다. OS의 핵심 역할은 추상화, 보호, 공유다. Pintos를 만지면서 이 말이 훨씬 구체적으로 느껴졌다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-9-os-abstraction.png" alt="운영체제의 추상화와 보호, 파일 시스템, 멀티코어 난제, 하드웨어 기반 격리 개념 정리" />
  <figcaption>운영체제는 하드웨어의 복잡성을 추상화하고, 프로세스와 자원을 보호하며, 제한된 자원을 공유하게 만든다</figcaption>
</figure>

<p>CPU는 스레드와 scheduler로 추상화되고, 메모리는 가상 주소 공간과 page table로 추상화된다. 저장소는 파일, 경로, offset으로 추상화된다. 동시에 OS는 user mode와 kernel mode를 나눠 애플리케이션이 커널 영역을 침범하지 못하게 막고, timer interrupt를 통해 무한 루프를 도는 프로그램에서도 제어권을 다시 가져온다.</p>

<p>Pintos는 싱글코어 환경이지만, 그래도 interrupt, lock, semaphore, condition variable이 얽히는 순간 충분히 어렵다. 실제 하드웨어의 복잡성을 모두 다루지는 않더라도, OS가 왜 이런 장치를 제공해야 하는지 이해하기에는 아주 좋은 실습이었다.</p>

<h2 id="이번-주에-배운-점">이번 주에 배운 점</h2>

<p>이번 주의 핵심은 “스레드는 함수처럼 실행되는 코드 조각이 아니라, 상태와 우선순위와 대기 이유를 가진 스케줄링 대상”이라는 점이었다. <code class="language-plaintext highlighter-rouge">THREAD_READY</code>, <code class="language-plaintext highlighter-rouge">THREAD_RUNNING</code>, <code class="language-plaintext highlighter-rouge">THREAD_BLOCKED</code> 같은 상태가 단순 enum이 아니라, 커널이 CPU를 누구에게 줄지 판단하는 기준이라는 것을 체감했다.</p>

<p>또 하나는 테스트 중심으로 코드를 읽는 습관이다. Pintos는 구현해야 할 기능이 많지만, 테스트 이름과 checker를 보면 지금 무엇이 깨졌는지 방향을 잡을 수 있다. <code class="language-plaintext highlighter-rouge">make check</code> 결과만 보는 것이 아니라, 어떤 테스트가 어떤 상황을 만들고 어떤 출력 순서를 기대하는지 읽어야 디버깅이 빨라진다.</p>

<p>마지막으로 팀 프로젝트에서 중요한 것은 혼자 빨리 가는 것이 아니라, 같은 지도를 보고 움직이는 것이라는 점을 배웠다. 아침 sprint 회의, 오후 PR, 저녁 merge 흐름을 잡고, 실패한 테스트와 의심되는 함수를 기록하는 방식이 다음 주차까지 이어질 기반이 될 것 같다.</p>

<h2 id="아쉬웠던-점과-다음-액션">아쉬웠던 점과 다음 액션</h2>

<p>아쉬운 점은 Pintos의 전체 구조를 아직 완전히 내 언어로 설명할 정도로 익숙해지지는 못했다는 것이다. 특히 donation과 MLFQS는 구현 흐름은 따라갈 수 있어도, 발표에서 질문이 들어왔을 때 <code class="language-plaintext highlighter-rouge">lock-&gt;holder</code>가 왜 필요한지, semaphore와 lock과 condition variable이 어떻게 다른지 더 정확한 용어로 답해야 한다.</p>

<p>다음 액션은 세 가지다. 첫째, Project 1에서 통과한 테스트를 기능별로 다시 묶어 설명할 수 있게 정리한다. 둘째, 발표 자료의 그림은 실제 코드 구조와 용어에 맞춰 직접 그린다. 셋째, 다음 주 user program으로 넘어가기 전에 <code class="language-plaintext highlighter-rouge">thread.c</code>, <code class="language-plaintext highlighter-rouge">synch.c</code>, <code class="language-plaintext highlighter-rouge">timer.c</code>의 핵심 함수 흐름을 다시 읽고, interrupt를 꺼야 하는 구간과 priority를 다시 계산해야 하는 시점을 명확히 정리한다.</p>

<p>9주차는 운영체제를 교재의 개념이 아니라, 실패하는 테스트와 꼬이는 스레드 상태로 만난 주였다. 어렵지만 재미있는 쪽으로 제대로 들어온 느낌이 든다.</p>]]></content><author><name></name></author><category term="Jungle" /><category term="WIL(Weekly I Learned)" /><summary type="html"><![CDATA[9주차는 Pintos Project 1 Threads를 시작하며 Alarm Clock, priority scheduling, priority donation, MLFQS를 팀 단위로 학습하고, 운영체제의 추상화와 보호 개념을 실제 커널 코드와 테스트로 연결해 본 한 주였다.]]></summary></entry><entry><title type="html">[WIL] 8주</title><link href="https://limjaehwan-real.github.io/BlogLim/wil-8%EC%A3%BC/" rel="alternate" type="text/html" title="[WIL] 8주" /><published>2026-04-23T09:00:00+09:00</published><updated>2026-04-23T09:00:00+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/wil-8%EC%A3%BC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/wil-8%EC%A3%BC/"><![CDATA[<h2 id="8주차-주간-회고-네트워크와-웹-서버의-흐름을-코드로-따라간-한-주">8주차 주간 회고: 네트워크와 웹 서버의 흐름을 코드로 따라간 한 주</h2>

<p>8주차는 CS:APP 11장을 중심으로 네트워크 프로그래밍과 웹 서버 구현을 집중적으로 학습한 주였다. 8주차 발제에서 잡은 방향도 명확했다. 단순히 네트워크 이론을 넓게 훑는 것이 아니라, 서버를 단계적으로 구현하면서 웹 서버가 실제로 어떻게 요청을 받고 응답하는지 이해하는 것이 핵심이었다.</p>

<p>이번 주의 큰 흐름은 <code class="language-plaintext highlighter-rouge">클라이언트-서버 모델 → 소켓 인터페이스 → HTTP → Tiny Web Server → TCP 연결 과정 → 동시성 서버</code>로 이어졌다. 여기에 수요코딩에서 다룬 B+Tree 인덱스와 피드백까지 더해지면서, 단순 구현보다 “내가 만든 구조를 어떻게 설명하고 개선할 것인가”를 많이 고민하게 됐다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-8-web-request-flow.svg" alt="브라우저 URL 입력부터 DNS, TCP 연결, HTTP 요청, 서버 응답까지 이어지는 웹 요청 실제 흐름" />
  <figcaption>브라우저에서 URL을 입력한 뒤 화면이 보이기까지의 전체 흐름</figcaption>
</figure>

<h2 id="1-csapp-11장과-네트워크-프로그래밍">1. CS:APP 11장과 네트워크 프로그래밍</h2>

<p>가장 먼저 잡은 개념은 클라이언트-서버 모델이었다. 클라이언트는 서비스를 요청하고, 서버는 정해진 포트에서 기다리다가 요청을 처리한다. 이 구조 자체는 단순해 보이지만, 실제 코드로 들어가면 <code class="language-plaintext highlighter-rouge">socket</code>, <code class="language-plaintext highlighter-rouge">bind</code>, <code class="language-plaintext highlighter-rouge">listen</code>, <code class="language-plaintext highlighter-rouge">accept</code>, <code class="language-plaintext highlighter-rouge">connect</code> 같은 시스템 호출들이 각각 분명한 역할을 가진다.</p>

<p>특히 소켓을 파일 디스크립터처럼 다룬다는 점이 중요했다. 네트워크 통신도 결국 읽고 쓰는 인터페이스로 추상화되기 때문에, 서버 입장에서는 클라이언트와 연결된 <code class="language-plaintext highlighter-rouge">connfd</code>를 통해 요청을 읽고 응답을 쓰게 된다. 이 관점이 잡히니 echo server, Tiny server, proxy server가 전혀 다른 프로그램이 아니라 같은 뼈대 위에 있다는 점이 보였다.</p>

<p>Network Programming Part 1, Part 2를 보면서는 DNS, IP 주소, 포트 번호, <code class="language-plaintext highlighter-rouge">getaddrinfo()</code>의 역할도 정리했다. 사람이 입력한 도메인 이름이 IP 주소로 바뀌고, 그 주소와 포트를 이용해 연결이 만들어지는 과정이 웹 요청의 출발점이라는 것을 다시 확인했다.</p>

<h2 id="2-http와-tiny-web-server">2. HTTP와 Tiny Web Server</h2>

<p>HTTP는 이번 주 학습에서 가장 실용적으로 와닿은 부분이었다. 브라우저가 서버에게 요청 라인과 헤더를 보내고, 서버는 상태 라인, 헤더, 본문으로 응답한다. 텍스트 기반 프로토콜이라 직접 보면 단순하지만, 서버 구현에서는 이 형식을 정확히 지켜야 한다.</p>

<p>Tiny Web Server를 정리하면서 서버의 흐름을 함수 단위로 나눠서 이해했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">main()</code>은 listen socket을 열고 클라이언트 연결을 받는다.</li>
  <li><code class="language-plaintext highlighter-rouge">doit()</code>는 요청 한 건의 전체 흐름을 담당한다.</li>
  <li><code class="language-plaintext highlighter-rouge">read_requesthdrs()</code>는 헤더를 끝까지 읽어 요청 스트림을 정리한다.</li>
  <li><code class="language-plaintext highlighter-rouge">parse_uri()</code>는 URI를 분석해 정적 컨텐츠와 동적 컨텐츠를 구분한다.</li>
  <li><code class="language-plaintext highlighter-rouge">serve_static()</code>은 파일을 읽어 HTTP 응답으로 보낸다.</li>
  <li><code class="language-plaintext highlighter-rouge">serve_dynamic()</code>은 CGI 프로그램을 실행해 동적 응답을 만든다.</li>
</ul>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-8-tiny-server-flow.svg" alt="Tiny Web Server에서 main, accept, doit, read_requesthdrs, parse_uri, serve_static, serve_dynamic으로 이어지는 요청 처리 흐름" />
  <figcaption>Tiny Web Server에서 요청 한 건이 처리되는 함수 흐름</figcaption>
</figure>

<p>Tiny가 작은 예제임에도 웹 서버의 핵심 구조를 거의 다 담고 있다는 점이 인상적이었다. 정적 컨텐츠는 서버가 직접 파일을 읽어 보내고, 동적 컨텐츠는 <code class="language-plaintext highlighter-rouge">fork</code>, <code class="language-plaintext highlighter-rouge">execve</code>, <code class="language-plaintext highlighter-rouge">dup2</code>, 환경변수를 이용해 CGI 프로그램의 실행 결과를 클라이언트에게 전달한다. 이 흐름을 이해하니 FastCGI, Flask, Tomcat 같은 구조가 왜 등장했는지도 조금 더 자연스럽게 연결됐다.</p>

<h2 id="3-tcp-연결-과정과-웹-요청의-실제-흐름">3. TCP 연결 과정과 웹 요청의 실제 흐름</h2>

<p>TCP 3-way handshake는 “연결이 됐다”는 말을 더 구체적으로 이해하게 해준 주제였다. 클라이언트가 <code class="language-plaintext highlighter-rouge">SYN</code>을 보내고, 서버가 <code class="language-plaintext highlighter-rouge">SYN + ACK</code>로 응답하고, 클라이언트가 다시 <code class="language-plaintext highlighter-rouge">ACK</code>를 보내야 비로소 양방향 통신이 가능한 상태가 된다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-8-tcp-flow.svg" alt="TCP 3-way handshake, 데이터 전송, FIN ACK 기반 연결 종료 흐름" />
  <figcaption>TCP 연결은 handshake, data transfer, teardown 단계로 이어진다</figcaption>
</figure>

<p>이 과정의 목적은 단순히 인사를 나누는 것이 아니었다. 양쪽이 서로 데이터를 보낼 수 있는지 확인하고, 시퀀스 번호를 동기화하며, 과거에 지연된 패킷 때문에 잘못된 연결이 생기는 것을 막는다. 이후 데이터 전송과 연결 종료까지 생각하면, 웹 브라우저에서 주소를 입력하는 단순한 행동 뒤에 생각보다 많은 시스템 단계가 숨어 있다는 것을 알 수 있었다.</p>

<p>4/21 퀴즈에서 나온 DNS, TCP 연결, HTTP 요청/응답, 파일 디스크립터 문제도 이번 주 학습 내용을 점검하는 데 도움이 됐다. 특히 브라우저 주소창에 URL을 입력한 뒤 화면이 보이기까지의 과정을 단계별로 설명하는 문제는, 이번 주 배운 내용을 하나의 이야기로 묶어보는 좋은 연습이었다.</p>

<h2 id="4-동시성-프로그래밍과-서버-설계-고민">4. 동시성 프로그래밍과 서버 설계 고민</h2>

<p>서버는 한 명의 클라이언트만 상대하지 않는다. 그래서 동시성 프로그래밍도 이번 주 중요한 주제였다. 프로세스, 스레드, I/O 멀티플렉싱은 모두 여러 연결을 처리하기 위한 방법이지만, 자원 사용 방식과 복잡도가 다르다.</p>

<p>I/O 멀티플렉싱은 하나의 프로세스나 스레드가 여러 파일 디스크립터를 감시하다가 이벤트가 발생한 것만 처리하는 방식이다. <code class="language-plaintext highlighter-rouge">select</code>, <code class="language-plaintext highlighter-rouge">poll</code>, <code class="language-plaintext highlighter-rouge">epoll</code> 같은 방식이 있고, 적은 자원으로 많은 연결을 처리할 수 있다는 장점이 있다. Redis, Nginx, Node.js 같은 시스템이 이런 구조와 연결된다는 점도 흥미로웠다.</p>

<p>반대로 스레드 기반 서버를 생각하면 공유 자원과 락 문제가 따라온다. 수요코딩 피드백에서도 스레드풀, bounded queue, 락을 거는 공유 자원, 로그와 메트릭, 에러 처리, 응답 지연 시간 같은 주제가 나왔다. 단순히 “동시에 처리한다”가 아니라, 어떤 자원을 공유하는지, 큐가 가득 찼을 때 어떻게 대응할지, 서버 상태를 어떻게 관찰할지까지 설계해야 한다는 점을 배웠다.</p>

<h2 id="5-btree-index와-설명-가능한-구현">5. B+Tree Index와 설명 가능한 구현</h2>

<p>수요코딩에서는 B+Tree 인덱스를 SQL Processor에 어떻게 연결했는지 정리했다. 실제 데이터는 배열에 저장하고, B+Tree는 <code class="language-plaintext highlighter-rouge">id -&gt; Record*</code>를 연결하는 보조 인덱스 역할을 맡았다.</p>

<p>이 구조를 통해 <code class="language-plaintext highlighter-rouge">WHERE id = ?</code> 같은 단건 조회와 <code class="language-plaintext highlighter-rouge">WHERE id &gt;= ?</code> 같은 범위 조회를 더 빠르게 처리할 수 있었다. 특히 범위 조회에서는 B+Tree로 시작 지점을 찾고 leaf link를 따라가면 되기 때문에, 배열 전체를 처음부터 끝까지 훑는 방식보다 효율적이다.</p>

<p>이번 주 피드백을 통해 느낀 점은 구현 자체보다 구현을 설명하는 방식도 중요하다는 것이다. 어려운 용어를 쓸 수밖에 없다면 그림이나 다이어그램으로 보완해야 하고, 어떤 공유 자원에 락을 걸었는지, 어떤 데이터 포맷으로 스레드와 메인 흐름이 소통하는지까지 명확히 보여줘야 한다.</p>

<h2 id="6-mini-dbms-sql-api-server-thread-pool-queue-lock">6. Mini DBMS SQL API Server: Thread Pool, Queue, Lock</h2>

<p>이번 주에 정리한 또 하나의 구현은 C 기반 in-memory SQL 엔진과 B+Tree를 HTTP API 서버로 감싼 Mini DBMS SQL API Server였다. 핵심은 SQL 엔진 자체를 멀티스레드용으로 크게 뜯어고친 것이 아니라, 서버 경계인 <code class="language-plaintext highlighter-rouge">src/server/</code>에서 요청 분산, 큐잉, 락, timeout, metrics를 관리하도록 만든 점이다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-8-sql-api-architecture.png" alt="Thread pool 기반 SQL API 서버 아키텍처와 요청 흐름" />
  <figcaption>Thread pool 기반 SQL API 서버 아키텍처와 요청 흐름</figcaption>
</figure>

<p>구조를 나누면 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>영역</th>
      <th>역할</th>
      <th>대표 파일</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Core DB Engine</td>
      <td>SQL 파싱/실행, <code class="language-plaintext highlighter-rouge">users</code> 테이블, B+Tree index 처리</td>
      <td><code class="language-plaintext highlighter-rouge">src/core/sql.c</code>, <code class="language-plaintext highlighter-rouge">src/core/table.c</code>, <code class="language-plaintext highlighter-rouge">src/core/bptree.c</code></td>
    </tr>
    <tr>
      <td>HTTP Server</td>
      <td>socket accept, bounded queue, worker thread pool, HTTP 응답</td>
      <td><code class="language-plaintext highlighter-rouge">src/server/http_server.c</code></td>
    </tr>
    <tr>
      <td>DB Server Boundary</td>
      <td>공유 <code class="language-plaintext highlighter-rouge">Table *</code> 보관, SQL별 lock 선택, metrics 기록</td>
      <td><code class="language-plaintext highlighter-rouge">src/server/db_server.c</code>, <code class="language-plaintext highlighter-rouge">src/server/db_server.h</code></td>
    </tr>
    <tr>
      <td>Platform Wrapper</td>
      <td>pthread mutex, cond, rwlock, thread API 감싸기</td>
      <td><code class="language-plaintext highlighter-rouge">src/server/platform.c</code>, <code class="language-plaintext highlighter-rouge">src/server/platform.h</code></td>
    </tr>
    <tr>
      <td>API Layer</td>
      <td>HTTP request 파싱, JSON response 생성</td>
      <td><code class="language-plaintext highlighter-rouge">src/server/api.c</code>, <code class="language-plaintext highlighter-rouge">src/server/api.h</code></td>
    </tr>
  </tbody>
</table>

<p>서버는 요청이 올 때마다 thread를 새로 만들지 않는다. 시작할 때 정해진 수의 worker thread를 만들고, main accept thread는 client socket을 queue에 넣기만 한다. 기본 옵션은 <code class="language-plaintext highlighter-rouge">--workers 4</code>, <code class="language-plaintext highlighter-rouge">--queue 16</code>, <code class="language-plaintext highlighter-rouge">--lock-timeout-ms 1000</code>으로 잡았다. 그래서 동시에 처리 가능한 요청 수와 대기 가능한 요청 수를 서버 설정으로 제어할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">HTTPRequestQueue</code>는 bounded 원형 큐로 동작한다. main thread는 <code class="language-plaintext highlighter-rouge">http_request_queue_push()</code>로 새 socket을 넣고, worker thread는 <code class="language-plaintext highlighter-rouge">http_request_queue_pop()</code>으로 socket을 꺼낸다. queue가 비어 있으면 worker는 condition variable에서 대기하고, 새 요청이 들어오면 깨어난다. 반대로 queue가 가득 차면 요청을 무한히 쌓지 않고 즉시 <code class="language-plaintext highlighter-rouge">503 queue_full</code>을 반환한다. 이것이 이 서버에서의 backpressure이다.</p>

<p>공유 자원은 크게 두 가지로 나눴다.</p>

<table>
  <thead>
    <tr>
      <th>공유 자원</th>
      <th>보호 방식</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">server-&gt;table</code></td>
      <td><code class="language-plaintext highlighter-rouge">PlatformRWLock db_lock</code></td>
      <td>SQL 실행 중 Table/B+Tree 구조가 깨지지 않도록 보호</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">server-&gt;metrics</code></td>
      <td><code class="language-plaintext highlighter-rouge">PlatformMutex metrics_mutex</code></td>
      <td>여러 thread가 counter를 동시에 증가시키는 race condition 방지</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">src/core/</code>의 SQL 엔진, Table, B+Tree는 자체적으로 lock을 알지 못한다. 따라서 여러 worker가 같은 <code class="language-plaintext highlighter-rouge">Table *</code>에 직접 접근하면 <code class="language-plaintext highlighter-rouge">INSERT</code>와 <code class="language-plaintext highlighter-rouge">SELECT</code>가 충돌하거나, B+Tree split 중 잘못된 포인터를 읽는 문제가 생길 수 있다. 이 프로젝트에서는 그 위험을 <code class="language-plaintext highlighter-rouge">db_server_execute()</code>에서 한 번에 통제한다.</p>

<p>SQL 문자열은 먼저 읽기와 쓰기로 분류한다.</p>

<table>
  <thead>
    <tr>
      <th>SQL 종류</th>
      <th>내부 분류</th>
      <th>Lock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SELECT ...</code></td>
      <td><code class="language-plaintext highlighter-rouge">DB_SERVER_QUERY_KIND_READ</code></td>
      <td>read lock</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">INSERT ...</code></td>
      <td><code class="language-plaintext highlighter-rouge">DB_SERVER_QUERY_KIND_WRITE</code></td>
      <td>write lock</td>
    </tr>
    <tr>
      <td>그 외</td>
      <td><code class="language-plaintext highlighter-rouge">DB_SERVER_QUERY_KIND_NONE</code></td>
      <td>DB lock 없이 core가 오류 판정</td>
    </tr>
  </tbody>
</table>

<p>정책은 단순하다. <code class="language-plaintext highlighter-rouge">SELECT</code>끼리는 read lock을 공유하므로 동시에 실행할 수 있고, <code class="language-plaintext highlighter-rouge">INSERT</code>는 Table row 배열, <code class="language-plaintext highlighter-rouge">next_id</code>, B+Tree index를 바꾸기 때문에 write lock으로 독점 실행한다. mutex 하나만 써도 안전성은 얻을 수 있지만, 그렇게 하면 읽기 요청끼리도 모두 순차 실행된다. read/write lock을 쓴 이유는 읽기 중심 workload에서 SELECT 병렬성을 살리기 위해서였다.</p>

<figure>
  <img src="/BlogLim/assets/images/posts/wil-8-request-processing-sequence.svg" alt="SELECT와 INSERT 요청 처리 시퀀스" />
  <figcaption>SELECT와 INSERT 요청은 같은 API 경로로 들어오지만 lock 단계에서 다르게 처리된다</figcaption>
</figure>

<p>lock을 기다릴 때도 무한 대기하지 않는다. 현재 구현은 blocking lock을 한 번 호출하는 대신 <code class="language-plaintext highlighter-rouge">try lock → 실패하면 1ms sleep → 다시 try lock</code>을 반복한다. <code class="language-plaintext highlighter-rouge">--lock-timeout-ms</code>를 넘으면 SQL을 실행하지 않고 <code class="language-plaintext highlighter-rouge">503 lock_timeout</code>을 반환하며, metrics에는 <code class="language-plaintext highlighter-rouge">total_lock_timeouts</code>를 증가시킨다. 이 덕분에 worker thread가 DB lock 앞에서 무한히 묶이는 상황을 줄이고, 클라이언트도 서버가 멈춘 것처럼 기다리는 대신 명확한 실패 응답을 받는다.</p>

<p>metrics counter도 별도 mutex로 보호한다. <code class="language-plaintext highlighter-rouge">counter++</code>는 C에서 atomic 연산이 아니기 때문에 여러 worker가 동시에 증가시키면 값이 유실될 수 있다. 그래서 DB 자체를 보호하는 <code class="language-plaintext highlighter-rouge">db_lock</code>과 관측 지표를 보호하는 <code class="language-plaintext highlighter-rouge">metrics_mutex</code>를 분리했다. 이 분리 덕분에 metrics 갱신 때문에 DB read/write lock을 오래 잡지 않아도 된다.</p>

<p>이 설계를 발표에서 설명한다면 다음 한 문장이 핵심이다.</p>

<blockquote>
  <p>이 프로젝트는 기존 SQL 엔진을 thread-safe하게 직접 수정하지 않고, 서버 경계에서 thread pool, bounded queue, read/write lock을 적용해 외부 HTTP 요청을 안전하게 병렬 처리하도록 만든 구조입니다.</p>
</blockquote>

<p>정리하면 이 프로젝트의 동시성 설계는 세 단계로 나눌 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>해결하는 문제</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Thread pool</td>
      <td>요청마다 thread를 만들지 않고 제한된 worker로 병렬 처리</td>
    </tr>
    <tr>
      <td>Bounded queue</td>
      <td>서버가 감당 못 할 요청을 무한히 쌓지 않고 <code class="language-plaintext highlighter-rouge">503 queue_full</code>로 제어</td>
    </tr>
    <tr>
      <td>Read/write lock</td>
      <td>공유 DB는 안전하게 보호하면서 SELECT끼리는 병렬 실행</td>
    </tr>
  </tbody>
</table>

<p>주의할 점도 있다. README 일부 설명에는 <code class="language-plaintext highlighter-rouge">phase gate</code>라는 표현이 있지만, 현재 코드 기준으로는 별도의 writer 우선 phase gate 구조체가 구현되어 있지는 않다. 실제 구현은 <code class="language-plaintext highlighter-rouge">pthread_rwlock_tryrdlock()</code>, <code class="language-plaintext highlighter-rouge">pthread_rwlock_trywrlock()</code>을 wrapper로 감싼 뒤 timeout loop를 도는 방식이다. 따라서 “별도 phase gate로 writer starvation을 직접 방지한다”는 표현보다는 “read/write lock과 try-lock timeout으로 공유 Table 접근을 제어한다”는 표현이 더 정확하다.</p>

<p>면접이나 발표에서 예상되는 질문도 몇 가지로 정리할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>질문</th>
      <th>답변 요지</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>왜 thread pool을 사용했나요?</td>
      <td>요청마다 thread를 만들면 생성 비용이 크고 요청 폭주 시 thread 수가 제한 없이 늘어날 수 있다. worker 수를 고정하면 동시 처리량과 자원 사용량을 제어할 수 있다.</td>
    </tr>
    <tr>
      <td>queue가 왜 필요한가요?</td>
      <td>accept한 socket을 worker에게 전달하는 중간 버퍼가 필요하다. bounded queue를 쓰면 처리 한계를 넘는 요청에 <code class="language-plaintext highlighter-rouge">503 queue_full</code>로 backpressure를 줄 수 있다.</td>
    </tr>
    <tr>
      <td>mutex 하나로 처리하면 안 되나요?</td>
      <td>가능하지만 모든 SQL이 순차 실행된다. SELECT끼리는 데이터를 변경하지 않으므로 read/write lock으로 SELECT 병렬성과 INSERT 안전성을 함께 얻었다.</td>
    </tr>
    <tr>
      <td>INSERT는 왜 write lock인가요?</td>
      <td>INSERT는 <code class="language-plaintext highlighter-rouge">Table.rows</code>, <code class="language-plaintext highlighter-rouge">next_id</code>, B+Tree index를 변경한다. 동시에 실행되면 id 증가나 index 갱신이 충돌할 수 있어 독점 실행해야 한다.</td>
    </tr>
    <tr>
      <td>lock timeout은 왜 필요한가요?</td>
      <td>lock을 무한 대기하면 worker가 계속 묶여 서버가 멈춘 것처럼 보일 수 있다. timeout을 두면 일정 시간 이후 <code class="language-plaintext highlighter-rouge">503 lock_timeout</code>으로 실패하고 metrics로 관찰할 수 있다.</td>
    </tr>
    <tr>
      <td>core SQL 엔진은 thread-safe한가요?</td>
      <td>core 자체가 thread-safe하게 설계된 것은 아니다. 대신 <code class="language-plaintext highlighter-rouge">DBServer</code> 경계에서 같은 <code class="language-plaintext highlighter-rouge">Table *</code>에 접근하기 전에 lock을 잡도록 만들어 thread-safe하게 사용한다.</td>
    </tr>
  </tbody>
</table>

<h2 id="이번-주에-배운-점">이번 주에 배운 점</h2>

<p>이번 주의 핵심은 웹 서버를 “마법처럼 동작하는 프로그램”이 아니라, 파일 디스크립터와 문자열 프로토콜, 프로세스, 스레드, 큐, 락이 조합된 시스템으로 보는 관점을 얻은 것이다.</p>

<p>CS:APP 11장을 따라가면서 네트워크 요청이 코드에서 어떤 함수 호출로 표현되는지 이해했고, Tiny Server를 통해 HTTP 요청이 실제 서버 함수들 사이를 어떻게 흐르는지 정리했다. TCP handshake와 DNS까지 연결하니 브라우저에서 주소를 입력하는 행동도 더 이상 추상적인 일이 아니었다.</p>

<p>동시에, 구현을 잘하는 것과 설명을 잘하는 것은 다르다는 점도 느꼈다. B+Tree 인덱스나 thread pool 기반 SQL API 서버처럼 구조가 있는 구현은 성능 수치만 보여주는 것보다, 어떤 문제를 해결하려고 도입했고 어떤 흐름으로 동작하는지 보여주는 것이 더 중요했다.</p>

<h2 id="아쉬웠던-점과-다음-액션">아쉬웠던 점과 다음 액션</h2>

<p>아쉬운 점은 Tiny Server와 동시성 서버를 아직 완전히 내 코드처럼 설명할 정도로 반복하지는 못했다는 것이다. 함수 흐름은 정리했지만, 직접 코드를 다시 작성해보거나 오류 상황을 넣어가며 테스트하는 과정은 더 필요하다.</p>

<p>다음 주에는 네트워크 프로그래밍을 단순 학습에서 구현으로 더 끌고 가고 싶다. 특히 proxy server를 구현하면서 요청 헤더를 어떻게 재구성할지, 여러 클라이언트를 어떻게 처리할지, 로그와 에러 처리를 어떻게 남길지까지 같이 고민해볼 계획이다.</p>

<p>이번 8주차는 웹 서버를 이해하기 위한 기반을 다진 주였다. 이제부터는 이 기반 위에서 실제로 더 안정적인 서버를 구현하고, 내가 만든 구조를 다른 사람에게 설명할 수 있는 수준으로 끌어올리는 것이 목표다.</p>]]></content><author><name></name></author><category term="Jungle" /><category term="WIL(Weekly I Learned)" /><summary type="html"><![CDATA[8주차는 CS:APP 11장을 중심으로 네트워크 프로그래밍, HTTP, Tiny Web Server, TCP 연결 과정, 동시성 서버 구조를 학습하고, B+Tree 인덱스와 thread pool 기반 Mini DBMS SQL API Server를 설명 가능한 형태로 정리한 한 주였다.]]></summary></entry><entry><title type="html">[WIL] 7주</title><link href="https://limjaehwan-real.github.io/BlogLim/wil-7%EC%A3%BC/" rel="alternate" type="text/html" title="[WIL] 7주" /><published>2026-04-16T20:43:03+09:00</published><updated>2026-04-16T20:43:03+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/wil-7%EC%A3%BC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/wil-7%EC%A3%BC/"><![CDATA[<h2>[주간 회고] 7주차: 가상 메모리의 심장, Malloc Lab 구현과 OS의 이해</h2>
<p>이번 7주차는 Malloc Lab 프로젝트를 통해 C언어로 직접 동적 메모리 할당기를 구현하며 시스템 프로그래밍의 깊은 곳을 탐험한 시간이었습니다. 단순히 코드를 짜는 것을 넘어, 메모리를 어떻게 하면 더 효율적으로 관리할 것인가에 대해 치열하게 고민했던 기록들을 정리합니다.</p>
<h3>1. 힙(Heap)의 구조와 경계 처리</h3>
<p>메모리 할당기를 구현하기 위해 가장 먼저 힙의 물리적 및 논리적 구조를 설계했습니다.</p>
<ul>
<li><b>정렬(Alignment)과 패딩:</b> 데이터 접근 성능 향상을 위해 8바이트 혹은 16바이트 경계를 맞추는 패딩(Padding)의 중요성을 배웠습니다.</li>
<li><b>프롤로그 및 에필로그:</b> 힙의 시작과 끝을 알리는 파수꾼(Sentinel) 블록을 두어, 메모리 순회 시 발생할 수 있는 경계 오류를 방지했습니다.</li>
</ul>
<figure>
  <img src="/BlogLim/assets/images/posts/journey-starts-here-cover.png" alt="[WIL] 7주 대표 이미지" />
</figure>
<h3>2. 가용 리스트 관리 전략: Implicit vs Explicit</h3>
<p>가용 블록(Free Block)을 어떻게 찾아낼 것인가에 따라 할당기의 성능이 극명하게 갈리는 것을 확인했습니다.</p>
<ul>
<li><b>Implicit Free List (묵시적 가용 리스트):</b> 구현은 단순하지만 힙이 커질수록 탐색 비용이 블록 수에 비례하여 증가하는 비확장적 자료구조임을 체감했습니다.</li>
<li><b>Explicit Free List (명시적 가용 리스트):</b> 가용 블록 내에 next, prev 포인터를 두어 탐색 속도를 높였습니다.
<ul>
<li><b>LIFO (Last-In-First-Out):</b> 삽입 속도가 매우 빠르지만 단편화에 취약할 수 있습니다.</li>
<li><b>Address-Ordered:</b> 주소 순으로 정렬하여 병합(Coalescing) 효율을 극대화하고 단편화를 방지했습니다.</li>
</ul>
</li>
</ul>

<figure>
  <img src="https://blog.kakaocdn.net/dna/5F8Wt/dJMcaflYVqV/AAAAAAAAAAAAAAAAAAAAAAWqbw64xwQThbSxYI6kgJhmsunaAixTzx2UL4gPPlJ6/img.png" alt="implicit first fit/ explicit first fit/ explicit best fit 비교" />
  <figcaption>implicit first fit/ explicit first fit/ explicit best fit 비교</figcaption>
</figure>
<h3>3. 더 나은 성능을 위한 고도화 기법</h3>
<p>기본적인 할당기를 넘어 실제 시스템에서 사용되는 고도화된 전략들을 학습했습니다.</p>
<ul>
<li><b>분리 가용 리스트(Segregated Free List):</b> 가용 블록을 크기 구간별로 여러 리스트로 나누어 관리하여 탐색 시간을 단축했습니다.</li>
<li><b>버디 시스템(Buddy System):</b> 블록 크기를 2의 거듭제곱 단위로 관리하여 빠른 병합과 분할을 가능하게 하는 구조를 이해했습니다.</li>
</ul>
<h3>4. 운영체제 핵심 개념 정리</h3>
<p>메모리 관리의 배경이 되는 커널의 동작 원리도 함께 정리했습니다.</p>
<ul>
<li><b>System Call:</b> sbrk나 mmap을 통해 사용자 프로세스가 커널에 메모리 자원을 요청하는 인터페이스를 학습했습니다.</li>
<li><b>가상 메모리(Virtual Memory):</b> 각 프로세스가 독립적인 메모리 공간을 가진 것처럼 추상화해주는 원리와 보안 및 안정성을 위한 격리의 중요성을 배웠습니다.</li>
</ul>
<hr />]]></content><author><name></name></author><category term="Jungle" /><category term="WIL(Weekly I Learned)" /><summary type="html"><![CDATA[[주간 회고] 7주차: 가상 메모리의 심장, Malloc Lab 구현과 OS의 이해 이번 7주차는 Malloc Lab 프로젝트를 통해 C언어로 직접 동적 메모리 할당기를 구현하며 시스템 프로그래밍의 깊은 곳을 탐험한 시간이었습니다. 단순히 코드를 짜는]]></summary></entry><entry><title type="html">Explicit Free List</title><link href="https://limjaehwan-real.github.io/BlogLim/explicit-free-list/" rel="alternate" type="text/html" title="Explicit Free List" /><published>2026-04-16T19:42:22+09:00</published><updated>2026-04-16T19:42:22+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/explicit-free-list</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/explicit-free-list/"><![CDATA[<h2>Explicit Free List: Unordered vs. Address-Ordered</h2>
<ul>
<li>Explicit Free List(명시적 가용 리스트)는 가용 블록(Free Block) 내에 next와 prev 포인터를 포함시켜, 가용 블록들끼리만 연결 리스트 형태로 관리하는 방식입니다. 이때 리스트를 유지하는 두 가지 주요 전략을 비교해 드립니다.</li>
</ul>
<div><hr /></div>
<h3>1. Unordered (LIFO - Last-In-First-Out)</h3>
<p>가용 블록을 리스트에 삽입할 때 특별한 순서 없이, 보통 리스트의 맨 앞(Root)에 가장 최근에 반환된 블록을 넣는 방식입니다.</p>
<ul>
<li><b>동작:</b> free()가 호출되면 해당 블록을 리스트의 시작 부분에 즉시 연결합니다.</li>
</ul>
<ul>
<li><b>장점 (O(1)):</b> 삽입 속도가 매우 빠릅니다. 단순히 포인터 몇 개만 바꿔주면 끝납니다.</li>
</ul>
<ul>
<li><b>단점:</b> 메모리 주소와 상관없이 리스트가 구성되므로, 인접한 가용 블록들을 합치는 <b>Coalescing(병합)</b> 효율이 떨어질 수 있으며, Address-ordered 방식에 비해 메모리 단편화(Fragmentation)가 더 많이 발생할 수 있습니다.</li>
</ul>
<div><hr /></div>
<h3>2. Address-Ordered (주소 순서 정렬)</h3>
<p>가용 리스트의 블록들을 항상 <b>메모리 주소 순서대로</b> 정렬된 상태로 유지하는 방식입니다. (예: Addr(A) &lt; Addr(B) &lt; Addr(C))</p>
<ul>
<li><b>동작:</b> free()가 호출되면 리스트를 처음부터 순회하며, 해당 블록의 주소가 들어갈 적절한 위치를 찾아 삽입합니다.</li>
</ul>
<ul>
<li><b>장점:</b> First-fit 검색 시 메모리 낮은 주소부터 효율적으로 채울 수 있고, 인접한 블록이 리스트 상에서도 앞뒤에 위치할 가능성이 높아 <b>병합(Coalescing) 성능이 우수</b>합니다. 결과적으로 단편화가 적습니다.</li>
</ul>
<ul>
<li><b>단점 (O(n)):</b> 삽입할 때마다 리스트를 탐색해야 하므로 반환(free) 작업 시 시간이 더 소요됩니다.</li>
</ul>
<div><hr /></div>
<h3>💡 한눈에 비교하기</h3>
<table border="1">
<tbody>
<tr>
<td><b>구분</b></td>
<td><b>Unordered (LIFO)</b></td>
<td><b>Address-Ordered</b></td>
</tr>
<tr>
<td><b>삽입 속도</b></td>
<td><b>매우 빠름 (O(1))</b></td>
<td>상대적으로 느림 (O(n))</td>
</tr>
<tr>
<td><b>단편화 방지</b></td>
<td>보통 수준</td>
<td><b>우수함</b></td>
</tr>
<tr>
<td><b>병합 효율</b></td>
<td>보통</td>
<td>매우 높음</td>
</tr>
<tr>
<td><b>구현 난이도</b></td>
<td>쉬움</td>
<td>리스트 순회 로직 필요</td>
</tr>
</tbody>
</table>

<h1>Implicit VS Explicit 비교</h1>
<figure>
  <img src="https://blog.kakaocdn.net/dna/cuLyWU/dJMcahRBQCr/AAAAAAAAAAAAAAAAAAAAAPbxpwNEUQMtn6RHjSc9oOF2PyY-O0co6sDuwBYBtldu/img.png" alt="이미지" />
</figure>]]></content><author><name></name></author><category term="프로그래밍 언어" /><category term="C언어" /><summary type="html"><![CDATA[Explicit Free List: Unordered vs. Address-Ordered Explicit Free List(명시적 가용 리스트)는 가용 블록(Free Block) 내에 next와 prev 포인터를 포함시켜, 가용 블록들끼리만 연결 리스]]></summary></entry><entry><title type="html">7주차 키워드 정리</title><link href="https://limjaehwan-real.github.io/BlogLim/7%EC%A3%BC%EC%B0%A8-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%A0%95%EB%A6%AC/" rel="alternate" type="text/html" title="7주차 키워드 정리" /><published>2026-04-16T19:32:44+09:00</published><updated>2026-04-16T19:32:44+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/7%EC%A3%BC%EC%B0%A8-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%A0%95%EB%A6%AC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/7%EC%A3%BC%EC%B0%A8-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%A0%95%EB%A6%AC/"><![CDATA[<h1>🖥️ 운영체제 및 메모리 관리 핵심 정리</h1>
<h2>1. 시스템 콜 (System Call)</h2>
<p><b>개념:</b> 사용자 프로세스가 운영체제(OS) 커널이 제공하는 서비스를 이용하기 위해 커널 함수를 호출하는 인터페이스입니다.</p>
<ul>
<li><b>필요성:</b> 응용 프로그램은 하드웨어(CPU, 메모리, 디스크 등)에 직접 접근할 수 없습니다. 보안과 안정성을 위해 OS가 자원을 관리하며, 응용 프로그램은 &lsquo;요청&rsquo;을 통해서만 자원을 사용할 수 있습니다.</li>
<li><b>작동 원리:</b>
<ol>
<li>사용자 모드(User Mode)에서 실행 중인 프로그램이 시스템 콜을 호출합니다.</li>
<li>트랩(Trap)이 발생해 커널 모드(Kernel Mode)로 전환됩니다.</li>
<li>커널이 요청을 처리한 뒤 사용자 모드로 복귀합니다.</li>
</ol>
</li>
<li><b>주요 예시:</b> fork()(프로세스 생성), read()/write()(파일 I/O), exit()(종료).</li>
</ul>
<h2>2. 데이터 세그먼트 (Data Segment)</h2>
<p><b>개념:</b> 프로세스의 가상 메모리 공간 중 전역 변수와 정적 변수(static)가 저장되는 영역입니다.</p>
<ul>
<li><b>메모리 구조 내 위치:</b> 보통 코드(Text) 세그먼트 위에 있고, 힙(Heap) 세그먼트 아래에 위치합니다.</li>
<li><b>세부 분류:</b>
<ul>
<li><b>Data 세그먼트 (Initialized):</b> 초기화된 전역 변수 및 정적 변수가 저장됩니다. (예: int a = 10;)</li>
<li><b>BSS 세그먼트 (Uninitialized):</b> 초기화되지 않은 전역 변수 및 정적 변수가 저장됩니다. 프로그램 시작 시 0으로 초기화됩니다. (예: int b;)</li>
</ul>
</li>
<li><b>특징:</b> 프로그램 시작 시 할당되며 종료 시 해제됩니다.</li>
</ul>
<h2>3. 메모리 단편화 (Memory Fragmentation)</h2>
<p><b>개념:</b> 메모리가 할당&middot;해제되는 과정에서 총 메모리 공간은 충분하지만, 실제로는 할당할 수 없는 작은 조각들이 생기는 현상입니다.</p>
<h3>① 내부 단편화 (Internal Fragmentation)</h3>
<ul>
<li><b>상황:</b> 할당된 메모리 블록이 요청한 크기보다 클 때 발생합니다.</li>
<li><b>결과:</b> 할당된 블록 내부에 남는 공간이 생겨 낭비됩니다.</li>
</ul>
<h3>② 외부 단편화 (External Fragmentation)</h3>
<ul>
<li><b>상황:</b> 메모리 사이사이에 남은 작은 공간들의 합은 충분하지만, 연속된 공간이 아니라서 큰 프로세스를 할당하지 못하는 상태입니다.</li>
<li><b>해결책:</b> 페이징(Paging), 압축(Compaction), 세그멘테이션(Segmentation).</li>
</ul>
<h2>4. sbrk와 mmap (힙 확장 방식)</h2>
<p>메모리 할당자(malloc 등)가 커널로부터 더 많은 메모리를 확보하기 위해 사용하는 시스템 콜입니다.</p>
<h3>① sbrk (Set Program Break)</h3>
<ul>
<li><b>방식:</b> 데이터 세그먼트의 끝을 나타내는 &lsquo;program break&rsquo; 주소를 위로 올리거나 아래로 내려 힙 공간을 확장&middot;축소합니다.</li>
<li><b>특징:</b> 메모리가 연속적으로 할당되며, 주로 작은 크기의 메모리 할당에 사용됩니다.</li>
</ul>
<h3>② mmap (Memory Map)</h3>
<ul>
<li><b>방식:</b> 특정 파일이나 익명 메모리 공간(Anonymous Memory)을 프로세스의 가상 메모리 주소 공간에 직접 매핑합니다.</li>
<li><b>특징:</b>
<ul>
<li>힙 영역이 아닌 별도의 메모리 매핑 영역에 할당됩니다.</li>
<li>주로 큰 크기의 메모리 할당(보통 128KB 이상)에 사용됩니다.</li>
<li>메모리를 해제하면 즉시 커널에 반환할 수 있어 단편화 방지에 유리합니다.</li>
</ul>
</li>
</ul>
<h3>💡 요약 비교표</h3>
<p>구분 sbrk mmap</p>
<table border="1">
<tbody>
<tr>
<td><b>주요 용도</b></td>
<td>작은 메모리 할당 (연속적)</td>
<td>큰 메모리 할당 (비연속적/독립적)</td>
</tr>
<tr>
<td><b>작동 방식</b></td>
<td>heap의 끝 포인터를 이동</td>
<td>가상 메모리에 새 매핑 영역 생성</td>
</tr>
<tr>
<td><b>장점</b></td>
<td>오버헤드가 적음</td>
<td>메모리 반환이 유연함, 공유 메모리 가능</td>
</tr>
</tbody>
</table>]]></content><author><name></name></author><category term="운영체제" /><summary type="html"><![CDATA[🖥️ 운영체제 및 메모리 관리 핵심 정리 1. 시스템 콜 (System Call) 개념: 사용자 프로세스가 운영체제(OS) 커널이 제공하는 서비스를 이용하기 위해 커널 함수를 호출하는 인터페이스입니다. 필요성: 응용 프로그램은 하드웨어(CPU, 메모]]></summary></entry><entry><title type="html">동적 메모리 할당 basic 영상 요약</title><link href="https://limjaehwan-real.github.io/BlogLim/%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9-basic-%EC%98%81%EC%83%81-%EC%9A%94%EC%95%BD/" rel="alternate" type="text/html" title="동적 메모리 할당 basic 영상 요약" /><published>2026-04-16T17:59:18+09:00</published><updated>2026-04-16T17:59:18+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9-basic-%EC%98%81%EC%83%81-%EC%9A%94%EC%95%BD</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9-basic-%EC%98%81%EC%83%81-%EC%9A%94%EC%95%BD/"><![CDATA[<p><a href="https://www.youtube.com/watch?v=1eRDwxtLrxI&amp;list=PLtv4PQFH2yMas_SCMTEaE4eUIWWWiwISj&amp;index=22">유튜브 영상 링크</a></p>
<h1>🧠 동적 메모리 할당 (Dynamic Memory Allocation) 총정리</h1>
<h2>1. 개요 및 기본 개념</h2>
<h3>1.1. 동적 메모리 할당이란?</h3>
<p>프로그램 실행 중(Runtime) 가상 메모리를 할당받는 방식입니다. 컴파일 타임에 크기를 알 수 없는 가변적인 데이터 구조를 다룰 때 필수적입니다.</p>
<ul>
<li><b>메모리 영역 (Heap):</b> 동적 할당은 <b>힙(Heap)</b> 영역에서 이루어집니다.
<ul>
<li>가상 주소 공간 내에 존재하며, <b>스택(Stack)과 반대 방향</b>으로 성장합니다.</li>
<li>시스템 포인터(BRK)에 의해 경계가 정해지며, 매우 큰 공간을 가질 수 있습니다.</li>
</ul>
</li>
<li><b>힙 내의 블록:</b> 할당자는 힙을 가변 크기의 블록으로 관리하며, 각 블록은 <b>할당(Allocated)</b> 또는 <b>자유(Free)</b> 상태 중 하나입니다.</li>
</ul>
<hr />

<h3>1.2. 할당자의 분류</h3>
<p>구분 특징 대표 언어</p>
<table border="1">
<tbody>
<tr>
<td><b>명시적 할당자</b></td>
<td>개발자가 직접 malloc으로 할당하고 free로 해제</td>
<td>C, C++</td>
</tr>
<tr>
<td><b>암시적 할당자</b></td>
<td>가비지 컬렉터(GC)가 미사용 메모리를 자동 해제</td>
<td>Java, Python</td>
</tr>
</tbody>
</table>
<hr />

<h3>1.3. C 언어 주요 함수</h3>
<p>함수 역할 비고</p>
<table border="1">
<tbody>
<tr>
<td><b>malloc(size)</b></td>
<td>요청 크기 이상의 블록 포인터 반환</td>
<td>16-바이트 정렬 보장, 실패 시 NULL</td>
</tr>
<tr>
<td><b>free(ptr)</b></td>
<td>할당된 메모리 블록 해제</td>
<td>malloc/realloc으로 할당된 주소여야 함</td>
</tr>
<tr>
<td><b>realloc(ptr, size)</b></td>
<td>이미 할당된 영역의 크기 변경</td>
<td>기존 데이터 보존 시도</td>
</tr>
<tr>
<td><b>calloc(n, size)</b></td>
<td>메모리 할당 및 <b>0으로 초기화</b></td>
<td>요소 개수와 크기를 인자로 받음</td>
</tr>
<tr>
<td><b>sbrk(incr)</b></td>
<td><b>BRK 포인터</b>를 이동시켜 힙 크기 조절</td>
<td>시스템 내부에서 주로 사용</td>
</tr>
</tbody>
</table>
<hr />

<h3>1.4. 설계 가상 및 예시</h3>
<ul>
<li><b>주소 지정:</b> 단어(Word) 단위 주소 지정 가능.</li>
<li><b>정렬(Alignment):</b> 2단어(Double Word) 단위 정렬 (예시에서는 2-바이트 경계).</li>
<li><b>시각화 규칙:</b>
<ul>
<li>🟩 <b>녹색:</b> 할당된 블록 (Allocated)</li>
<li>⬜ <b>무색:</b> 자유 블록 (Free)</li>
</ul>
</li>
<li><b>공간 낭비 예시:</b> 정렬 경계 요구사항 때문에 할당 요청 사이에 사용하지 못하는 틈새 공간이 발생할 수 있습니다.</li>
</ul>
<hr />

<h2>2. 제약 조건 및 성능 목표</h2>
<h3>2.1. 할당자의 제약 조건</h3>
<blockquote>
<p>할당자는 프로그램의 요청에 <b>즉각 응답</b>해야 하며, 메모리를 임의로 옮길 수 없습니다.</p>
</blockquote>
<ol>
<li><b>요청 제어 불가:</b> 프로그램이 어떤 순서로 할당/해제를 요청할지 알 수 없음.</li>
<li><b>즉각 응답:</b> 요청을 재정렬하거나 기다릴 수 없음.</li>
<li><b>정렬 준수:</b> 아키텍처별 정렬 요구사항을 반드시 만족해야 함.</li>
<li><b>이동 불가(No Moving):</b> 할당된 블록을 다른 위치로 옮길 수 없음 (free 시 위치를 알 수 없게 되기 때문).</li>
</ol>
<h3>2.2. 성능 목표 (Trade-off 관계)</h3>
<ul>
<li><b>처리량 (Throughput):</b> 단위 시간당 완료된 요청 수</li>
</ul>
<p>$$ 10,000\ operations / 10\ sec = 1,000\ ops/sec $$</p>
<ul>
<li><b>최대 메모리 활용률 (Utilization):</b> 힙을 얼마나 알뜰하게 사용하는가?
<ul>
<li><b>낭비 요인:</b> 패딩(Padding), 관리용 오버헤드(Header), 단편화.</li>
</ul>
</li>
<li>$$ \frac{\max(\text{Payload 합계})}{\text{Current Heap Size}} $$</li>
</ul>
<hr />

<h2>3. 메모리 단편화 (Fragmentation)</h2>
<h3>3.1. 내부 단편화 (Internal Fragmentation)</h3>
<ul>
<li><b>원인:</b> 요청한 크기보다 더 큰 블록이 할당될 때 발생 (정렬 패딩, 탐색 정책의 한계).</li>
<li><b>특징:</b> 할당된 블록 <b>내부</b>에 존재하므로 측정이 명확함.</li>
</ul>
<h3>3.2. 외부 단편화 (External Fragmentation)</h3>
<ul>
<li><b>원인:</b> 자유 공간의 총합은 충분하지만, <b>연속적이지 않고 조각나 있어</b> 할당 요청을 수용하지 못하는 상태.</li>
<li><b>특징:</b> 미래의 요청 패턴에 따라 달라지므로 예측이 어려움.</li>
</ul>
<hr />

<h2>4. 할당자 구현 시 고려 사항</h2>
<h3>4.1. 정보 저장 (Header)</h3>
<ul>
<li>free(ptr) 시 크기를 알기 위해 블록 시작점에 길이 필드(Length Field)를 저장합니다.</li>
<li>이 필드에는 (페이로드 + 헤더) 크기가 포함되며, 하위 비트(LSB)를 사용하여 할당 여부를 표시합니다 (0: Free, 1: Allocated).</li>
</ul>
<h3>4.2. 자유 블록 관리 (Free List)</h3>
<ol>
<li><b>암시적 리스트 (Implicit List):</b> 모든 블록(할당/자유)을 헤더 정보를 따라 순차 탐색. 구현이 간단하나 탐색이 느림(O(N)).</li>
<li><b>명시적 리스트 (Explicit List):</b> 자유 블록들만 포인터로 연결. 할당된 블록을 건너뛰어 탐색이 빠름.</li>
<li><b>분리 자유 리스트 (Segregated List):</b> 크기별로 리스트를 따로 관리하여 속도 향상.</li>
<li><b>균형 트리 (Balanced Tree):</b> 크기순으로 정렬하여 최적의 핏(Best-fit)을 빠르게 탐색.</li>
</ol>
<h3>4.3. 블록 분할 (Splitting)</h3>
<p>자유 블록이 요청보다 클 경우, 필요한 만큼만 할당하고 남은 공간을 <b>새로운 자유 블록</b>으로 쪼개어 단편화를 방지합니다.</p>
<h3>4.4. 블록 병합 (Coalescing)</h3>
<p>해제된 블록 주위에 다른 자유 블록이 있다면 하나로 합칩니다.</p>
<ul>
<li><b>경계 태그 (Boundary Tags):</b> 각 블록의 끝에 푸터(Footer)를 두어 이전 블록의 정보를 즉시 확인합니다 (O(1)병합 가능).</li>
<li><b>4가지 경우:</b> (앞/뒤 상태에 따라) ①둘 다 할당됨 ②뒤만 자유 ③앞만 자유 ④둘 다 자유.</li>
<li><b>시점:</b> free 시 즉시 수행하거나(Immediate), 나중에 필요할 때 한꺼번에 수행(Deferred).</li>
</ul>
<hr />

<h2>5. 할당 정책 요약</h2>
<ul>
<li><b>배치 정책 (Placement):</b>
<ul>
<li><b>First Fit:</b> 첫 번째로 맞는 블록 선택.</li>
<li><b>Next Fit:</b> 이전 탐색 종료 지점부터 시작.</li>
<li><b>Best Fit:</b> 크기가 가장 딱 맞는 블록 선택 (메모리 효율 최상, 탐색 속도 최저).</li>
</ul>
</li>
<li><b>암시적 리스트의 한계:</b> 구현은 쉬우나 할당 속도가 전체 블록 수에 비례하므로 실제 시스템에서는 다른 구조와 병행하여 사용됩니다.</li>
</ul>
<hr />]]></content><author><name></name></author><category term="프로그래밍 언어" /><category term="C언어" /><summary type="html"><![CDATA[유튜브 영상 링크 🧠 동적 메모리 할당 (Dynamic Memory Allocation) 총정리 1. 개요 및 기본 개념 1.1. 동적 메모리 할당이란? 프로그램 실행 중(Runtime) 가상 메모리를 할당받는 방식입니다. 컴파일 타임에 크기를 알]]></summary></entry><entry><title type="html">CSAPP 9.9장 모르는 부분 정리</title><link href="https://limjaehwan-real.github.io/BlogLim/csapp-9-9%EC%9E%A5-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%A0%95%EB%A6%AC/" rel="alternate" type="text/html" title="CSAPP 9.9장 모르는 부분 정리" /><published>2026-04-10T23:19:58+09:00</published><updated>2026-04-10T23:19:58+09:00</updated><id>https://limjaehwan-real.github.io/BlogLim/csapp-9-9%EC%9E%A5-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%A0%95%EB%A6%AC</id><content type="html" xml:base="https://limjaehwan-real.github.io/BlogLim/csapp-9-9%EC%9E%A5-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%A0%95%EB%A6%AC/"><![CDATA[<h1>📌 1. 힙 확장과 sbrk 동작 구조</h1>
<figure>
  <img src="https://blog.kakaocdn.net/dna/wzqRz/dJMcajaJpW3/AAAAAAAAAAAAAAAAAAAAAMf5F8K_JNVPJihgiOquVv1gp5CGvdqqDFxCzAQFee7d/img.jpg" alt="이미지" />
</figure>
<h2>핵심 개념</h2>
<ul>
<li>sbrk(incr) = 힙의 끝(brk)을 늘림</li>
<li>반환값 = <b>이전 brk (새 메모리 시작 주소)</b></li>
</ul>
<h2>핵심 포인트</h2>
<ul>
<li>allocator는 &ldquo;새 공간 시작 주소&rdquo;가 필요</li>
<li>그래서 <b>old brk 반환</b></li>
</ul>
<hr />

<h1>📌 2. 메모리 정렬과 패딩</h1>
<figure>
  <img src="https://blog.kakaocdn.net/dna/EMJQ8/dJMcagykehc/AAAAAAAAAAAAAAAAAAAAAE34MB89CljRGkuOTVQmZd_cH4m59GNKyWOoIXqL9l1W/img.jpg" alt="이미지" />
</figure>
<figure>
  <img src="https://blog.kakaocdn.net/dna/AwJHR/dJMcajaJpW5/AAAAAAAAAAAAAAAAAAAAAFH1E-d7iqC-0Sq7N5--qzwee3DJlN6XlxbEfX50wZoY/img.jpg" alt="이미지" />
</figure>
<h2>핵심</h2>
<p>👉 <b>정렬(alignment)</b> 때문에 필요</p>
<ul>
<li>요청: 13 bytes &rarr; 실제: 16 bytes</li>
<li>남는 공간 = padding</li>
</ul>
<h2>효과</h2>
<ul>
<li>주소 계산 단순화</li>
<li>성능 향상</li>
</ul>
<hr />

<h1>📌 3. 힙 구조 (Prologue / Epilogue)</h1>
<figure>
  <img src="https://blog.kakaocdn.net/dna/bALg5E/dJMcajaJpW4/AAAAAAAAAAAAAAAAAAAAAFRcYv7e2NdxtMz9B7Aecdb0hRVeczDEf8YKnRZZFHkY/img.jpg" alt="이미지" />
</figure>
<h2>구조</h2>
<pre><code>[padding][prologue][block...][epilogue]
</code></pre>
<h2>핵심</h2>
<ul>
<li>epilogue = 힙 끝 표시</li>
<li>coalesce 오류 방지</li>
</ul>
<hr />

<h1>📌 4. 가상 메모리</h1>
<h2>정의</h2>
<p>👉 <b>프로세스마다 독립된 주소공간처럼 보이게 하는 기술</b></p>
<h2>핵심</h2>
<ul>
<li>실제: 물리 메모리 분산</li>
<li>보이는 것: 연속 공간</li>
</ul>
<hr />

<h1>📌 5. 비확장성 구조 (Implicit Free List)</h1>
<figure>
  <img src="https://blog.kakaocdn.net/dna/bPJpOA/dJMcajaJpW0/AAAAAAAAAAAAAAAAAAAAAIHu8D-HmDUhm2qgmrV0gHPTxC2s85RgZER8HRyV8wee/img.jpg" alt="이미지" />
</figure>
<h2>문제</h2>
<ul>
<li>모든 블록 순회</li>
<li>O(n)</li>
</ul>
<h2>결론</h2>
<p>👉 <b>힙 커질수록 성능 붕괴</b></p>
<hr />

<h1>📌 6. 고급 할당 전략</h1>
<hr />

<h2>🔹 Segregated Free List</h2>
<ul>
<li>크기별 리스트 분리</li>
<li>탐색 속도 &uarr;</li>
</ul>
<hr />

<h2>🔹 Buddy System</h2>
<figure>
  <img src="https://blog.kakaocdn.net/dna/blcaSw/dJMcagykeha/AAAAAAAAAAAAAAAAAAAAAFfOvzCK2G3iynY0XVdieliQA2C1LxEwV-Kbf_pOi8tq/img.jpg" alt="이미지" />
</figure>
<ul>
<li>2의 거듭제곱 기반</li>
<li>merge 빠름</li>
</ul>
<hr />

<h1>📌 7. extern</h1>
<h2>핵심</h2>
<p>👉 다른 파일 변수 참조</p>
<pre><code>extern int count;
</code></pre>

<ul>
<li>메모리 생성 X</li>
<li>참조만 O</li>
</ul>
<hr />

<h1>📌 전체 흐름</h1>
<pre><code>가상메모리
   &darr;
힙
   &darr;
sbrk 확장
   &darr;
정렬 + 패딩
   &darr;
prologue / epilogue
   &darr;
implicit list (느림)
   &darr;
segregated / buddy (개선)
</code></pre>
<p><br /><b>prologue와 epliogue 왜 필요할까????</b></p>]]></content><author><name></name></author><category term="프로그래밍 언어" /><category term="C언어" /><summary type="html"><![CDATA[📌 1. 힙 확장과 sbrk 동작 구조 핵심 개념 sbrk(incr) = 힙의 끝(brk)을 늘림 반환값 = 이전 brk (새 메모리 시작 주소) 핵심 포인트 allocator는 &ldquo;새 공간 시작 주소&rdquo;가 필요 그래서 old br]]></summary></entry></feed>