<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pingu52</title><description>개발 블로그</description><link>https://pingu52.vercel.app/</link><language>ko</language><item><title>Astro 7과 pnpm 11로 블로그 의존성 정리하기</title><link>https://pingu52.vercel.app/posts/system-engineering/server-infra/astro7-pnpm11-cleanup/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/system-engineering/server-infra/astro7-pnpm11-cleanup/</guid><description>FOSSA와 pnpm audit 이슈를 줄이기 위해 Astro 7, pnpm 11로 올리며 만난 문제들을 정리했습니다.</description><pubDate>Sat, 04 Jul 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;이번 작업의 시작은 FOSSA였다. 처음에는 단순히 취약한 패키지를 몇 개 올리면 끝날 줄 알았는데, 막상 들여다보니 직접 의존성보다 transitive dependency가 훨씬 많았다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pnpm audit&lt;/code&gt; 기준으로는 취약점을 모두 없앨 수 있었지만, FOSSA 화면에는 여전히 오래된 하위 의존성들이 남아 있었다. 특히 &lt;code&gt;ansi-regex&lt;/code&gt;, &lt;code&gt;chalk&lt;/code&gt;, &lt;code&gt;commander&lt;/code&gt;, &lt;code&gt;cssnano&lt;/code&gt;, &lt;code&gt;ejs&lt;/code&gt;, &lt;code&gt;entities&lt;/code&gt; 같은 패키지들이 계속 보였다.&lt;/p&gt;
&lt;p&gt;그래서 이번에는 단순 override가 아니라, 블로그의 기반 프레임워크인 Astro를 7로 올리고 pnpm도 11로 올리는 방식으로 의존성 트리를 한 번 정리했다.&lt;/p&gt;
&lt;h2&gt;목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Astro 6 → Astro 7 업그레이드&lt;/li&gt;
&lt;li&gt;pnpm 10 → pnpm 11 업그레이드&lt;/li&gt;
&lt;li&gt;pnpm audit 취약점 0개 유지&lt;/li&gt;
&lt;li&gt;FOSSA transitive dependency 이슈 감소&lt;/li&gt;
&lt;li&gt;기존 블로그 동작과 페이지 전환 효과 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다만 범위를 너무 넓히지는 않기로 했다. &lt;code&gt;@swup/astro&lt;/code&gt;를 제거하면 FOSSA에 남아 있는 오래된 transitive dependency가 많이 줄어들 수 있지만, 현재 블로그의 페이지 전환 애니메이션이 바뀔 수 있다. 그래서 이번 PR에서는 &lt;code&gt;@swup/astro&lt;/code&gt;는 유지하고, Astro 7과 pnpm 11 업그레이드까지만 처리했다.&lt;/p&gt;
&lt;p&gt;브랜치는 다음처럼 따로 만들었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout main
git pull
git checkout -b chore/upgrade-astro-7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Astro 7로 올리기&lt;/h2&gt;
&lt;p&gt;먼저 Astro와 관련 패키지들을 업데이트했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm up astro@latest \
  @astrojs/svelte@latest \
  @astrojs/rss@latest \
  @astrojs/sitemap@latest \
  @astrojs/check@latest \
  @astrojs/ts-plugin@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 코드 하이라이팅, 수식, 이미지 처리 쪽 패키지도 같이 올렸다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm up @expressive-code/core@latest \
  @expressive-code/plugin-collapsible-sections@latest \
  @expressive-code/plugin-line-numbers@latest \
  astro-expressive-code@latest \
  katex@latest \
  sharp@latest \
  @tailwindcss/typography@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript는 &lt;code&gt;6.x&lt;/code&gt;가 보였지만 이번 작업에서는 올리지 않았다. Astro 7과 pnpm 11만으로도 변경 범위가 충분히 컸고, TypeScript major update까지 같이 넣으면 문제 발생 지점을 분리하기 어려워진다.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; 명시 추가&lt;/h2&gt;
&lt;p&gt;Astro 7로 올리고 &lt;code&gt;pnpm check&lt;/code&gt;를 실행하니 Markdown 설정 관련 에러가 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` run on the `unified` processor from `@astrojs/markdown-remark`, which is no longer installed by default now that Starlight is the default Markdown processor.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존 &lt;code&gt;astro.config.mjs&lt;/code&gt;에서는 다음과 같이 Markdown pipeline을 직접 구성하고 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;markdown: {
  remarkPlugins: [...],
  rehypePlugins: [...],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;내 블로그는 &lt;code&gt;remark-math&lt;/code&gt;, &lt;code&gt;rehype-katex&lt;/code&gt;, &lt;code&gt;remark-directive&lt;/code&gt;, &lt;code&gt;rehype-slug&lt;/code&gt;, &lt;code&gt;rehype-autolink-headings&lt;/code&gt; 등을 사용한다. 이걸 Astro 7의 새로운 방식으로 한 번에 바꾸면 범위가 커진다. 그래서 이번에는 기존 Markdown pipeline을 유지하는 방향으로 갔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm add @astrojs/markdown-remark@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 기존 &lt;code&gt;remarkPlugins&lt;/code&gt;, &lt;code&gt;rehypePlugins&lt;/code&gt; 설정은 계속 동작한다. 다만 Astro는 이 설정 방식이 deprecated라고 경고를 띄운다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[astro] `markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` are deprecated.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번 PR에서는 이 경고를 해결하지 않았다. Markdown pipeline을 &lt;code&gt;unified({...})&lt;/code&gt; 기반으로 옮기는 작업은 별도 PR로 분리하는 편이 낫다.&lt;/p&gt;
&lt;h2&gt;Astro 7 빌드에서 터진 문법 오류&lt;/h2&gt;
&lt;p&gt;다음으로 &lt;code&gt;pnpm build&lt;/code&gt;를 실행했을 때 이런 에러가 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[CompilerError] Unterminated regular expression
Location:
src/pages/posts/[...slug].astro:97:9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 정규식 문제처럼 보였지만, 실제 원인은 Astro 컴포넌트의 조건부 렌더링 문법이었다.&lt;/p&gt;
&lt;p&gt;문제가 있던 코드는 이런 형태였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{licenseConfig.enable &amp;amp;&amp;amp; &amp;lt;License title={entry.data.title} slug={getPostSlug(entry)} pubDate={entry.data.published} class=&quot;mb-6 rounded-xl license-container onload-animation&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마지막에 &lt;code&gt;}&lt;/code&gt;가 빠져 있었다. Astro 7 compiler가 이 부분을 더 엄격하게 파싱하면서 뒤쪽의 &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt;를 이상하게 해석했고, 결과적으로 &lt;code&gt;Unterminated regular expression&lt;/code&gt; 에러가 난 것이다.&lt;/p&gt;
&lt;p&gt;수정 후 코드는 다음처럼 정리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  licenseConfig.enable &amp;amp;&amp;amp; (
    &amp;lt;License
      title={entry.data.title}
      slug={getPostSlug(entry)}
      pubDate={entry.data.published}
      class=&quot;mb-6 rounded-xl license-container onload-animation&quot;
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이미지 커버 조건부 렌더링도 같은 스타일로 정리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  entry.data.image &amp;amp;&amp;amp; (
    &amp;lt;ImageWrapper
      id=&quot;post-cover&quot;
      src={entry.data.image}
      basePath={path.join(&quot;content/posts/&quot;, getPostContentDir(entry))}
      class=&quot;mb-8 rounded-xl banner-container onload-animation&quot;
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 식으로 괄호를 명시해두면 Astro parser가 훨씬 안정적으로 읽는다.&lt;/p&gt;
&lt;h2&gt;pnpm 11로 올리기&lt;/h2&gt;
&lt;p&gt;Astro 7 업그레이드 이후 pnpm도 11로 올렸다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;corepack enable
corepack prepare pnpm@11.9.0 --activate
pnpm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;버전은 다음처럼 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;11.9.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 &lt;code&gt;package.json&lt;/code&gt;의 package manager도 변경했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm pkg set packageManager=pnpm@11.9.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pnpm 설정 위치 변경&lt;/h2&gt;
&lt;p&gt;pnpm 11로 올리자 다음 경고가 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WARN The &quot;pnpm&quot; field in package.json is no longer read by pnpm.
The following keys were ignored: &quot;pnpm.overrides&quot;, &quot;pnpm.onlyBuiltDependencies&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존에는 &lt;code&gt;package.json&lt;/code&gt; 안에 이런 식으로 pnpm 설정을 넣어두었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;pnpm&quot;: {
  &quot;overrides&quot;: {
    &quot;rollup-plugin-terser&amp;gt;serialize-javascript&quot;: &quot;7.0.7&quot;,
    &quot;astro&amp;gt;esbuild&quot;: &quot;0.28.1&quot;
  },
  &quot;onlyBuiltDependencies&quot;: [
    &quot;esbuild&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pnpm 11에서는 이 설정을 더 이상 읽지 않는다. 그래서 루트에 &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;을 만들고 설정을 옮겼다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;packages:
  - .

overrides:
  rollup-plugin-terser&amp;gt;serialize-javascript: 7.0.7

allowBuilds:
  esbuild: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 &lt;code&gt;onlyBuiltDependencies&lt;/code&gt;가 아니라 &lt;code&gt;allowBuilds&lt;/code&gt;를 써야 한다는 것이다.&lt;/p&gt;
&lt;h2&gt;minimum release age 정책에 걸린 경우&lt;/h2&gt;
&lt;p&gt;pnpm 11에서 &lt;code&gt;pnpm install&lt;/code&gt;을 하니 처음에는 supply-chain policy 에러가 났다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION]
11 lockfile entries failed verification
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@clack/*&lt;/code&gt;, &lt;code&gt;shiki&lt;/code&gt;, &lt;code&gt;@shikijs/*&lt;/code&gt;, &lt;code&gt;p-queue&lt;/code&gt; 같은 패키지가 너무 최근에 publish되어 minimum release age 정책에 걸린 것이다.&lt;/p&gt;
&lt;p&gt;이 경우에는 lockfile을 다시 정리하고 설치했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm clean --lockfile
pnpm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후에는 다음처럼 통과했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;✓ Lockfile passes supply-chain policies
Done in 2.6s using pnpm v11.9.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 정책은 귀찮아 보이지만, supply-chain 공격을 줄이기 위한 장치다. 그래서 바로 우회하기보다는 lockfile을 새로 풀거나 시간이 지난 뒤 다시 설치하는 쪽이 더 안전하다.&lt;/p&gt;
&lt;h2&gt;esbuild build script 승인&lt;/h2&gt;
&lt;p&gt;다음으로는 이런 에러가 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pnpm 11에서는 dependency의 build script 실행을 더 명시적으로 관리한다. &lt;code&gt;esbuild&lt;/code&gt;는 설치 후 바이너리 준비 과정이 필요하므로 허용해야 한다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;에 다음 설정을 추가했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;allowBuilds:
  esbuild: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 다시 설치하면 정상적으로 통과했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;취약 버전 확인&lt;/h2&gt;
&lt;p&gt;이번 작업에서 특히 확인한 패키지는 &lt;code&gt;serialize-javascript&lt;/code&gt;와 &lt;code&gt;esbuild&lt;/code&gt;였다. 이전에는 audit에서 이 둘이 문제로 남았었다.&lt;/p&gt;
&lt;p&gt;확인은 &lt;code&gt;pnpm why&lt;/code&gt;로 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm why serialize-javascript
pnpm why esbuild
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과는 다음과 같았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serialize-javascript@7.0.7
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;esbuild@0.28.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존 취약 범위는 다음과 같았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serialize-javascript &amp;lt;= 7.0.2
esbuild &amp;gt;=0.27.3 &amp;lt;0.28.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 현재 설치된 버전은 둘 다 patched version이다.&lt;/p&gt;
&lt;p&gt;최종적으로 audit도 통과했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm audit
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;No known vulnerabilities found
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;왜 &lt;code&gt;@swup/astro&lt;/code&gt;는 유지했나&lt;/h2&gt;
&lt;p&gt;FOSSA에서 보이는 오래된 transitive dependency를 따라가보면 상당수가 &lt;code&gt;@swup/astro&lt;/code&gt; 쪽에서 온다.&lt;/p&gt;
&lt;p&gt;대표적으로 이런 경로가 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@swup/astro
└─ @swup/parallel-plugin
   └─ @swup/plugin
      └─ microbundle
         ├─ rollup-plugin-postcss
         ├─ cssnano
         ├─ ejs
         ├─ chalk
         └─ rollup-plugin-terser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 &lt;code&gt;chalk&lt;/code&gt;, &lt;code&gt;cssnano&lt;/code&gt;, &lt;code&gt;ejs&lt;/code&gt;, &lt;code&gt;commander&lt;/code&gt;, &lt;code&gt;rollup-plugin-terser&lt;/code&gt; 같은 오래된 패키지가 &lt;code&gt;swup → microbundle&lt;/code&gt; 체인에서 들어온다.&lt;/p&gt;
&lt;p&gt;처음에는 &lt;code&gt;@swup/astro&lt;/code&gt;를 제거하고 Astro의 내장 View Transitions로 대체하는 것도 고려했다. 하지만 그렇게 하면 현재 블로그의 페이지 전환 애니메이션 느낌이 바뀔 수 있다.&lt;/p&gt;
&lt;p&gt;이번 PR의 목적은 Astro 7과 pnpm 11 업그레이드였기 때문에, UX에 영향을 줄 수 있는 swup 제거는 하지 않았다.&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;이번 PR에서 한 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Astro 7 업그레이드&lt;/li&gt;
&lt;li&gt;pnpm 11 업그레이드&lt;/li&gt;
&lt;li&gt;audit 취약점 0개 유지&lt;/li&gt;
&lt;li&gt;pnpm 설정 이전&lt;/li&gt;
&lt;li&gt;Astro 7 compiler 대응&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번 PR에서 하지 않은 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@swup/astro 제거&lt;/li&gt;
&lt;li&gt;Astro ClientRouter 전환&lt;/li&gt;
&lt;li&gt;FOSSA transitive dependency 전체 제거&lt;/li&gt;
&lt;li&gt;TypeScript 6 업그레이드&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;최종 검증&lt;/h2&gt;
&lt;p&gt;마지막으로 아래 명령들을 모두 실행했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm lint:ci
pnpm check
pnpm type-check
pnpm build
pnpm audit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 패키지 경로도 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm why rollup-plugin-terser
pnpm why serialize-javascript
pnpm why esbuild
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rollup-plugin-terser&lt;/code&gt;는 여전히 &lt;code&gt;@swup/astro&lt;/code&gt; 체인 아래에 남아 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rollup-plugin-terser@7.0.2
└─ microbundle@0.15.1
   └─ @swup/plugin@3.0.1
      └─ @swup/astro@1.8.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 &lt;code&gt;serialize-javascript&lt;/code&gt;는 patched version으로 올라갔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serialize-javascript@7.0.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;esbuild&lt;/code&gt;도 patched version이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;esbuild@0.28.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;이번 작업에서 가장 크게 느낀 점은 dependency cleanup은 단순히 &lt;code&gt;pnpm up&lt;/code&gt;만으로 끝나지 않는다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pnpm audit&lt;/code&gt;은 보안 취약점 중심으로 보고, FOSSA는 outdated transitive dependency까지 더 넓게 보여준다. 그래서 audit이 0이어도 FOSSA에는 이슈가 남을 수 있다.&lt;/p&gt;
&lt;p&gt;그리고 transitive dependency를 줄이려면 결국 부모 패키지를 봐야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm why chalk
pnpm why cssnano
pnpm why ejs
pnpm why commander
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 식으로 실제 경로를 따라가보면 문제가 어느 패키지 체인에서 나오는지 보인다. 이번에는 많은 경로가 &lt;code&gt;@swup/astro → microbundle&lt;/code&gt;로 이어졌다.&lt;/p&gt;
&lt;p&gt;다만 dependency cleanup보다 중요한 것은 변경 범위를 잘 자르는 것이다. &lt;code&gt;@swup/astro&lt;/code&gt;를 제거하면 dependency tree는 더 깨끗해질 수 있지만, 페이지 전환 UX가 바뀐다. 그래서 이번에는 Astro 7과 pnpm 11까지만 처리하고, swup 제거 여부는 나중에 별도 작업으로 남겨두었다.&lt;/p&gt;
&lt;p&gt;결과적으로 현재 상태는 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Astro 7 적용 완료&lt;/li&gt;
&lt;li&gt;pnpm 11 적용 완료&lt;/li&gt;
&lt;li&gt;pnpm audit 취약점 0개&lt;/li&gt;
&lt;li&gt;기존 페이지 전환 유지&lt;/li&gt;
&lt;li&gt;FOSSA에 남은 swup 계열 transitive dependency는 별도 검토 대상으로 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 정도면 이번 PR의 경계는 꽤 깔끔하게 잡힌 것 같다.&lt;/p&gt;
</content:encoded></item><item><title>FOSSA dependency audit 대응과 Astro 6 마이그레이션</title><link>https://pingu52.vercel.app/posts/system-engineering/server-infra/fossa-astro6-migration/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/system-engineering/server-infra/fossa-astro6-migration/</guid><description>FOSSA와 pnpm audit에서 나온 의존성 이슈를 줄이기 위해 Astro 6 마이그레이션, content collection 수정, slug/image path 처리, pnpm overrides 적용 과정을 정리합니다.</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;블로그에 FOSSA를 붙이고 나서 dependency 이슈가 꽤 많이 잡혔다.&lt;/p&gt;
&lt;p&gt;처음에는 단순히 패키지 버전만 올리면 끝날 줄 알았는데, 실제로는 Astro 6 마이그레이션, content collection 구조 변경, slug 처리 방식 변경, 이미지 경로 문제까지 같이 따라왔다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;code&gt;pingu52.github.io&lt;/code&gt; 블로그에서 FOSSA dependency 이슈를 줄이면서 겪은 과정을 정리한다.&lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;FOSSA에서 확인한 이슈는 크게 두 종류였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오래된 dependency&lt;/li&gt;
&lt;li&gt;vulnerability가 있는 transitive dependency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처음에는 FOSSA 화면에서 수십 개의 dependency issue가 보였고, 로컬에서도 &lt;code&gt;pnpm audit&lt;/code&gt;을 돌리면 여러 취약점이 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm audit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 중요한 점은 모든 항목이 직접 설치한 패키지에서 나온 것은 아니라는 것이다.
대부분은 다음처럼 여러 단계 아래에 있는 transitive dependency에서 발생했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@swup/astro
└─ @swup/parallel-plugin
   └─ @swup/plugin
      └─ microbundle
         └─ rollup-plugin-terser
            └─ serialize-javascript
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 단순히 &lt;code&gt;package.json&lt;/code&gt;에 보이는 dependency만 업데이트한다고 바로 해결되는 구조가 아니었다.&lt;/p&gt;
&lt;h2&gt;1차 목표&lt;/h2&gt;
&lt;p&gt;이번 작업의 목표는 모든 FOSSA 항목을 한 번에 정리하는 것이 아니었다.&lt;/p&gt;
&lt;p&gt;우선 main에 안전하게 들어갈 수 있는 1차 범위를 정했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pnpm audit 기준 취약점 제거&lt;/li&gt;
&lt;li&gt;Astro 6으로 dependency 업데이트&lt;/li&gt;
&lt;li&gt;Astro 6에서 깨지는 content API 수정&lt;/li&gt;
&lt;li&gt;build/check/type-check/lint 통과&lt;/li&gt;
&lt;li&gt;FOSSA에 남는 transitive/outdated 항목은 별도 PR로 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FOSSA UI에 남아 있는 transitive dependency 목록까지 한 번에 다 처리하려고 하면 PR 범위가 너무 커진다.
그래서 이번 PR에서는 로컬 audit과 빌드 안정화를 먼저 끝내고, 나머지 FOSSA 항목은 후속 브랜치에서 다루는 방향으로 잡았다.&lt;/p&gt;
&lt;h2&gt;dependency 업데이트&lt;/h2&gt;
&lt;p&gt;먼저 주요 dependency를 최신 범위로 올렸다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;dependencies&quot;: {
    &quot;astro&quot;: &quot;^6.4.8&quot;,
    &quot;@astrojs/check&quot;: &quot;^0.9.9&quot;,
    &quot;@astrojs/rss&quot;: &quot;^4.0.19&quot;,
    &quot;@astrojs/svelte&quot;: &quot;^7.2.5&quot;,
    &quot;@tailwindcss/vite&quot;: &quot;^4.3.2&quot;,
    &quot;markdown-it&quot;: &quot;^14.3.0&quot;,
    &quot;svelte&quot;: &quot;^5.56.4&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;업데이트 후에는 lockfile을 다시 생성했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 단계에서 바로 빌드가 되지는 않았다.
Astro 6에서 content collection API가 바뀌었기 때문이다.&lt;/p&gt;
&lt;h2&gt;Astro 6 content config 마이그레이션&lt;/h2&gt;
&lt;p&gt;기존에는 content collection 설정이 아래 위치에 있었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/content/config.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Astro 6에서는 프로젝트 루트의 &lt;code&gt;src/content.config.ts&lt;/code&gt; 형태를 사용해야 했다.
그래서 collection 설정을 새 파일로 옮기고, loader 기반으로 다시 정의했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineCollection } from &quot;astro:content&quot;;
import { glob } from &quot;astro/loaders&quot;;
import { z } from &quot;astro/zod&quot;;

const postsCollection = defineCollection({
  loader: glob({
    pattern: &quot;**/[^_]*.{md,mdx}&quot;,
    base: &quot;./src/content/posts&quot;,
  }),
  schema: z.object({
    title: z.string(),
    published: z.coerce.date(),
    updated: z.coerce.date().optional(),
    draft: z.boolean().optional().default(false),
    description: z.string().optional().default(&quot;&quot;),
    image: z.string().optional().default(&quot;&quot;),
    tags: z.array(z.string()).optional().default([]),
    category: z.string().optional().nullable().default(&quot;&quot;),
    categoryPath: z.array(z.string()).optional().default([]),
    lang: z.string().optional().default(&quot;&quot;),

    prevTitle: z.string().optional().default(&quot;&quot;),
    prevSlug: z.string().optional().default(&quot;&quot;),
    nextTitle: z.string().optional().default(&quot;&quot;),
    nextSlug: z.string().optional().default(&quot;&quot;),
  }),
});

const specCollection = defineCollection({
  loader: glob({
    pattern: &quot;**/[^_]*.{md,mdx}&quot;,
    base: &quot;./src/content/spec&quot;,
  }),
  schema: z.looseObject({}),
});

export const collections = {
  posts: postsCollection,
  spec: specCollection,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;spec&lt;/code&gt; collection도 중요했다.
&lt;code&gt;about.astro&lt;/code&gt;에서 &lt;code&gt;getEntry(&quot;spec&quot;, &quot;about&quot;)&lt;/code&gt;을 사용하고 있었기 때문에, &lt;code&gt;spec&lt;/code&gt; collection이 Markdown 파일을 읽지 못하면 &lt;code&gt;/about&lt;/code&gt; 페이지 빌드가 실패했다.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;entry.slug&lt;/code&gt; 제거 대응&lt;/h2&gt;
&lt;p&gt;Astro content entry에서 기존에 사용하던 &lt;code&gt;entry.slug&lt;/code&gt; 접근이 더 이상 그대로 맞지 않았다.
그래서 post의 slug를 직접 계산하는 helper를 만들었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { type CollectionEntry, getCollection } from &quot;astro:content&quot;;

export type PostEntry = CollectionEntry&amp;lt;&quot;posts&quot;&amp;gt;;

export function getPostSlug(post: Pick&amp;lt;PostEntry, &quot;id&quot;&amp;gt;): string {
  return post.id.replace(/\/index$/, &quot;&quot;).replace(/\.(md|mdx)$/, &quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 기존 코드의 &lt;code&gt;entry.slug&lt;/code&gt; 사용을 아래처럼 바꿨다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getPostSlug(entry)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들면 RSS 생성 코드에서는 다음처럼 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const posts = attachPrevNext(sortPostsByPublishedDesc(await getAllPosts()));

return rss({
  title: siteConfig.title,
  description: siteConfig.subtitle,
  site: context.site ?? siteConfig.site,
  items: posts.map((post) =&amp;gt; ({
    title: post.data.title,
    pubDate: post.data.published,
    description: post.data.description || &quot;&quot;,
    link: url(`/posts/${getPostSlug(post)}/`),
  })),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;entry.render()&lt;/code&gt; 변경&lt;/h2&gt;
&lt;p&gt;기존에는 content entry에서 직접 &lt;code&gt;entry.render()&lt;/code&gt;를 호출했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { Content, headings } = await entry.render();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Astro 6에서는 &lt;code&gt;render&lt;/code&gt;를 import해서 사용하도록 수정했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { render } from &quot;astro:content&quot;;

const { Content, headings } = await render(entry);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 변경은 post page뿐 아니라 card에서 frontmatter를 렌더링하던 부분에도 적용했다.&lt;/p&gt;
&lt;h2&gt;이미지 경로 문제&lt;/h2&gt;
&lt;p&gt;가장 헷갈렸던 부분은 이미지 경로였다.&lt;/p&gt;
&lt;p&gt;블로그 글 구조가 전부 같은 방식이 아니었기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/computer-science/os-architecture/ostep-45-file-integrity/index.md
src/content/posts/problem-solving/boj/10773.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;첫 번째는 폴더 기반 post이고, 두 번째는 flat markdown post이다.&lt;/p&gt;
&lt;p&gt;이때 단순히 &lt;code&gt;entry.id&lt;/code&gt; 기준으로 parent directory를 계산하면 folder-based post에서는 맞지만, flat post에서는 이미지 경로가 어긋날 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;filePath&lt;/code&gt;를 우선 사용해서 실제 markdown 파일의 위치를 기준으로 base path를 계산했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function getPostContentDir(
  post: Pick&amp;lt;PostEntry, &quot;id&quot; | &quot;filePath&quot;&amp;gt;,
): string {
  const normalizedFilePath = post.filePath?.replace(/\\/g, &quot;/&quot;);

  if (normalizedFilePath) {
    const marker = &quot;src/content/posts/&quot;;
    const relativePath = normalizedFilePath.includes(marker)
      ? normalizedFilePath.slice(
          normalizedFilePath.indexOf(marker) + marker.length,
        )
      : normalizedFilePath.replace(/^.*content\/posts\//, &quot;&quot;);

    const lastSlash = relativePath.lastIndexOf(&quot;/&quot;);
    return lastSlash &amp;gt;= 0 ? relativePath.slice(0, lastSlash + 1) : &quot;&quot;;
  }

  return `${post.id.replace(/\/index$/, &quot;&quot;).replace(/\.(md|mdx)$/, &quot;&quot;)}/`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 post page와 post card에서 이미지 wrapper에 넘기는 base path를 바꿨다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ImageWrapper
  src={entry.data.image}
  basePath={path.join(&quot;content/posts/&quot;, getPostContentDir(entry))}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하니 &lt;code&gt;index.md&lt;/code&gt; 기반 글과 &lt;code&gt;boj/*.md&lt;/code&gt; 기반 글 모두에서 이미지 경로를 안정적으로 찾을 수 있었다.&lt;/p&gt;
&lt;h2&gt;ImageWrapper 방어 코드&lt;/h2&gt;
&lt;p&gt;이미지 import가 실패하면 빌드가 바로 터지는 문제도 있었다.&lt;/p&gt;
&lt;p&gt;기존에는 dynamic import 대상이 없을 때도 그대로 호출하려고 해서 다음과 같은 에러가 났다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TypeError: file is not a function
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 이미지 파일을 찾지 못한 경우에는 빌드를 죽이지 않고 warning만 남기도록 바꿨다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const file = files[normalizedPath];

if (!file) {
  console.warn(
    `\n[WARN] Image file not found: ${normalizedPath.replace(&quot;../../&quot;, &quot;src/&quot;)}`,
  );
} else {
  img = await file();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이미지가 필수인 글이라면 warning을 보고 고치면 되고, 임시로 이미지가 비어 있는 글 때문에 전체 빌드가 막히지는 않게 된다.&lt;/p&gt;
&lt;h2&gt;남은 audit 2개 처리&lt;/h2&gt;
&lt;p&gt;업데이트 후 &lt;code&gt;pnpm audit&lt;/code&gt; 결과는 마지막에 두 개만 남았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;serialize-javascript &amp;lt;= 7.0.2
esbuild &amp;gt;= 0.27.3 &amp;lt; 0.28.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;둘 다 직접 dependency라기보다 하위 dependency 경로에서 들어온 항목이었다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;pnpm.overrides&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;pnpm&quot;: {
    &quot;overrides&quot;: {
      &quot;rollup-plugin-terser&amp;gt;serialize-javascript&quot;: &quot;7.0.3&quot;,
      &quot;astro&amp;gt;esbuild&quot;: &quot;0.28.1&quot;
    },
    &quot;onlyBuiltDependencies&quot;: [
      &quot;esbuild&quot;
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;수정 후 다시 설치하고 audit을 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
pnpm audit
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;type-check 이슈&lt;/h2&gt;
&lt;p&gt;중간에 &lt;code&gt;type-check&lt;/code&gt;도 한 번 막혔다.&lt;/p&gt;
&lt;p&gt;기존 script는 다음과 같았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;type-check&quot;: &quot;tsc --noEmit --isolatedDeclarations&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--isolatedDeclarations&lt;/code&gt;는 export되는 값의 타입을 매우 엄격하게 요구한다.
문제는 &lt;code&gt;src/content.config.ts&lt;/code&gt;가 Astro의 schema inference에 의존한다는 점이었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;postsCollection&lt;/code&gt;에 억지로 넓은 타입을 붙이면 &lt;code&gt;post.data&lt;/code&gt;가 전부 &lt;code&gt;unknown&lt;/code&gt;으로 무너졌다.&lt;/p&gt;
&lt;p&gt;그래서 이 프로젝트에서는 &lt;code&gt;--isolatedDeclarations&lt;/code&gt;를 제거하는 쪽으로 정리했다.
블로그는 library가 아니라 Astro app이고, 현재 script도 &lt;code&gt;--noEmit&lt;/code&gt;이라 선언 파일을 만들지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;type-check&quot;: &quot;tsc -p tsconfig.typecheck.json --noEmit&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 별도 tsconfig를 만들었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;extends&quot;: &quot;./tsconfig.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;isolatedDeclarations&quot;: false
  },
  &quot;exclude&quot;: [
    &quot;dist&quot;,
    &quot;node_modules&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 Astro content schema inference는 유지하면서 일반 TypeScript 검사는 계속 돌릴 수 있다.&lt;/p&gt;
&lt;h2&gt;테스트 stub 문제&lt;/h2&gt;
&lt;p&gt;마지막으로 &lt;code&gt;pnpm test&lt;/code&gt;에서 한 번 더 막혔다.&lt;/p&gt;
&lt;p&gt;실제 &lt;code&gt;src/utils/post-utils.ts&lt;/code&gt;에는 &lt;code&gt;getPostSlug&lt;/code&gt;를 export했는데, 테스트에서는 계속 이런 에러가 났다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The requested module &apos;@utils/post-utils&apos; does not provide an export named &apos;getPostSlug&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인해보니 테스트 loader가 실제 파일이 아니라 stub을 보고 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tests/ts-loader.mjs
└─ @utils/post-utils -&amp;gt; tests/stubs/post-utils.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 실제 코드가 아니라 &lt;code&gt;tests/stubs/post-utils.ts&lt;/code&gt;에 export를 추가해야 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function getPostSlug(post: { id: string }): string {
  return post.id.replace(/\/index$/, &quot;&quot;).replace(/\.(md|mdx)$/, &quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;필요하면 이미지 경로 helper도 stub에 같이 맞춰준다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function getPostContentDir(post: {
  id: string;
  filePath?: string;
}): string {
  const normalizedFilePath = post.filePath?.replace(/\\/g, &quot;/&quot;);

  if (normalizedFilePath) {
    const marker = &quot;src/content/posts/&quot;;
    const relativePath = normalizedFilePath.includes(marker)
      ? normalizedFilePath.slice(
          normalizedFilePath.indexOf(marker) + marker.length,
        )
      : normalizedFilePath.replace(/^.*content\/posts\//, &quot;&quot;);

    const lastSlash = relativePath.lastIndexOf(&quot;/&quot;);
    return lastSlash &amp;gt;= 0 ? relativePath.slice(0, lastSlash + 1) : &quot;&quot;;
  }

  return `${post.id.replace(/\/index$/, &quot;&quot;).replace(/\.(md|mdx)$/, &quot;&quot;)}/`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제는 테스트 자체가 틀렸다기보다, production code 변경을 stub이 따라오지 못한 경우였다.
그래서 테스트를 삭제하기보다는 stub을 수정하는 쪽이 맞았다.&lt;/p&gt;
&lt;h2&gt;최종 검증&lt;/h2&gt;
&lt;p&gt;마지막에는 아래 명령들을 순서대로 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm lint:ci
pnpm check
pnpm type-check
pnpm build
pnpm audit
pnpm test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번 작업에서 중요한 것은 &lt;code&gt;pnpm audit&lt;/code&gt;만 보는 것이 아니었다.
Dependency를 올리면서 프레임워크 API까지 같이 바뀌었기 때문에, build와 content rendering까지 같이 확인해야 했다.&lt;/p&gt;
&lt;h2&gt;PR 범위 분리&lt;/h2&gt;
&lt;p&gt;FOSSA 화면에는 여전히 transitive dependency 항목이 남을 수 있다.
특히 &lt;code&gt;ansi-regex&lt;/code&gt;, &lt;code&gt;brace-expansion&lt;/code&gt;, &lt;code&gt;commander&lt;/code&gt;, &lt;code&gt;cssnano&lt;/code&gt;처럼 여러 하위 패키지에서 끌려오는 항목은 단순 override로 밀어붙이면 lockfile이 너무 많이 흔들릴 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 이번 PR은 다음 범위로 제한했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Astro 6 migration&lt;/li&gt;
&lt;li&gt;local audit cleanup&lt;/li&gt;
&lt;li&gt;build/check/type-check/lint/test 안정화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 FOSSA UI에 남은 dependency 목록은 별도 브랜치에서 처리하기로 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout main
git pull
git checkout -b chore/fossa-transitive-cleanup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 나누면 1차 PR은 안전하게 리뷰할 수 있고, 2차 PR에서는 FOSSA 항목만 집중해서 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Astro 6 migration&lt;/li&gt;
&lt;li&gt;content collection 수정&lt;/li&gt;
&lt;li&gt;slug/image path 수정&lt;/li&gt;
&lt;li&gt;pnpm overrides&lt;/li&gt;
&lt;li&gt;type-check script 조정&lt;/li&gt;
&lt;li&gt;test stub 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처음에는 FOSSA 이슈를 줄이는 것이 목적이었지만, 실제로는 블로그 빌드 파이프라인 전체를 한 번 점검하는 작업이 되었다.&lt;/p&gt;
&lt;p&gt;특히 기억할 점은 세 가지다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;transitive vulnerability는 override 경로를 정확히 써야 한다.&lt;/li&gt;
&lt;li&gt;Astro content config는 schema inference를 해치면 post.data 타입이 unknown이 된다.&lt;/li&gt;
&lt;li&gt;테스트가 alias/stub을 쓰고 있다면 실제 코드 export만 고쳐서는 부족하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이번 PR을 main에 먼저 넣고, FOSSA에 남은 transitive/outdated 항목은 후속 PR에서 따로 정리할 예정이다.&lt;/p&gt;
</content:encoded></item><item><title>[C] 10773번: 제로</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/10773/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/10773/</guid><description>백준 10773번 제로 문제를 C로 풀이합니다.</description><pubDate>Wed, 11 Mar 2026 03:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/10773&quot;&gt;https://www.acmicpc.net/problem/10773&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;나코더 기장 재민이는 동아리 회식을 준비하기 위해서 장부를 관리하는 중이다.&lt;/p&gt;
&lt;p&gt;재현이는 재민이를 도와서 돈을 관리하는 중인데, 애석하게도 항상 정신없는 재현이는 돈을 실수로 잘못 부르는 사고를 치기 일쑤였다.&lt;/p&gt;
&lt;p&gt;재현이는 잘못된 수를 부를 때마다 0을 외쳐서, 가장 최근에 재민이가 쓴 수를 지우게 시킨다.&lt;/p&gt;
&lt;p&gt;재민이는 이렇게 모든 수를 받아 적은 후 그 수의 합을 알고 싶어 한다. 재민이를 도와주자!&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 번째 줄에 정수 K가 주어진다. (1 ≤ K ≤ 100,000)&lt;/p&gt;
&lt;p&gt;이후 K개의 줄에 정수가 1개씩 주어진다. 정수는 0에서 1,000,000 사이의 값을 가지며, 정수가 &quot;0&quot; 일 경우에는 가장 최근에 쓴 수를 지우고, 아닐 경우 해당 수를 쓴다.&lt;/p&gt;
&lt;p&gt;정수가 &quot;0&quot;일 경우에 지울 수 있는 수가 있음을 보장할 수 있다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;재민이가 최종적으로 적어 낸 수의 합을 출력한다. 최종적으로 적어낸 수의 합은 $2^{31}-1$보다 작거나 같은 정수이다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 가장 최근에 적은 수를 지우는 연산이 필요하므로 &lt;strong&gt;스택&lt;/strong&gt;으로 처리하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;숫자가 &lt;code&gt;0&lt;/code&gt;이 아니면 스택에 넣습니다.&lt;/li&gt;
&lt;li&gt;숫자가 &lt;code&gt;0&lt;/code&gt;이면 스택의 가장 위 값을 제거합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;모든 입력을 처리한 뒤, 스택에 남아 있는 값들의 합을 구하면 최종 결과가 됩니다.&lt;/p&gt;
&lt;p&gt;현재 코드에서는 입력값을 저장하는 배열 &lt;code&gt;a&lt;/code&gt;를 스택처럼 다시 사용하고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt;은 스택의 맨 위 위치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a[++top] = 값&lt;/code&gt;은 push&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top--&lt;/code&gt;는 pop&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;으로 볼 수 있습니다.&lt;/p&gt;
&lt;h3&gt;시간복잡도&lt;/h3&gt;
&lt;p&gt;입력을 한 번 처리하고, 마지막에 합을 한 번 구하므로 시간복잡도는&lt;/p&gt;
&lt;p&gt;$$
O(K)
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;h3&gt;공간복잡도&lt;/h3&gt;
&lt;p&gt;최대 &lt;code&gt;K&lt;/code&gt;개의 수를 저장하므로 공간복잡도는&lt;/p&gt;
&lt;p&gt;$$
O(K)
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int k;

    scanf(&quot;%d&quot;, &amp;amp;k);

    int a[k];

    for (int i = 0; i &amp;lt; k; i++) {
        scanf(&quot;%d&quot;, &amp;amp;a[i]);
    }

    int top = -1;

    for (int i = 0; i &amp;lt; k; i++) {
        if (a[i] == 0) {
            if (top &amp;gt;= 0) {
                top--;
            }
        } else {
            a[++top] = a[i];
        }
    }

    int sum = 0;

    for (int i = 0; i &amp;lt;= top; i++) {
        sum += a[i];
    }

    printf(&quot;%d\n&quot;, sum);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 11866번: 요세푸스 문제 0</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/11866/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/11866/</guid><description>백준 11866번 요세푸스 문제 0 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11866&quot;&gt;https://www.acmicpc.net/problem/11866&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;요세푸스 문제는 다음과 같다.&lt;/p&gt;
&lt;p&gt;1번부터 N번까지 N명의 사람이 원을 이루면서 앉아있고, 양의 정수 K(≤ N)가 주어진다. 이제 순서대로 K번째 사람을 제거한다. 한 사람이 제거되면 남은 사람들로 이루어진 원을 따라 이 과정을 계속해 나간다. 이 과정은 N명의 사람이 모두 제거될 때까지 계속된다. 원에서 사람들이 제거되는 순서를 (N, K)-요세푸스 순열이라고 한다. 예를 들어 (7, 3)-요세푸스 순열은 &amp;lt;3, 6, 2, 7, 5, 1, 4&amp;gt;이다.&lt;/p&gt;
&lt;p&gt;N과 K가 주어지면 (N, K)-요세푸스 순열을 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N과 K가 빈 칸을 사이에 두고 순서대로 주어진다. (1 ≤ K ≤ N ≤ 1,000)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;예제와 같이 요세푸스 순열을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 사람들을 원형으로 앉혀 놓고, &lt;code&gt;K&lt;/code&gt;번째 사람마다 제거하는 과정을 그대로 시뮬레이션하면 됩니다.&lt;/p&gt;
&lt;p&gt;큐를 사용해서 생각하면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;맨 앞 사람을 하나 꺼낸다&lt;/li&gt;
&lt;li&gt;현재 순서를 1 증가시킨다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;K&lt;/code&gt;번째가 아니면 다시 큐의 뒤에 넣는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;K&lt;/code&gt;번째이면 제거하고 출력한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 과정을 큐가 빌 때까지 반복하면 요세푸스 순열을 구할 수 있습니다.&lt;/p&gt;
&lt;p&gt;원형으로 계속 돌아야 하므로, 배열을 이용한 &lt;strong&gt;원형 큐&lt;/strong&gt; 형태로 구현하면 편리합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n, k;
    scanf(&quot;%d %d&quot;, &amp;amp;n, &amp;amp;k);

    int queue[1001];
    int front = 0, rear = 0;
    int size = 0;

    for (int i = 1; i &amp;lt;= n; i++) {
        queue[rear] = i;
        rear = (rear + 1) % 1001;
        size++;
    }

    int count = 0;

    printf(&quot;&amp;lt;&quot;);

    while (size &amp;gt; 0) {
        int x = queue[front];
        front = (front + 1) % 1001;
        size--;

        count++;

        if (count == k) {
            printf(&quot;%d&quot;, x);
            if (size &amp;gt; 0) {
                printf(&quot;, &quot;);
            }
            count = 0;
        } else {
            queue[rear] = x;
            rear = (rear + 1) % 1001;
            size++;
        }
    }

    printf(&quot;&amp;gt;\n&quot;);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 7568번: 덩치</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/7568/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/7568/</guid><description>백준 7568번 덩치 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7568&quot;&gt;https://www.acmicpc.net/problem/7568&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;우리는 사람의 덩치를 키와 몸무게, 이 두 개의 값으로 표현하여 그 등수를 매겨보려고 한다. 어떤 사람의 몸무게가 x kg이고 키가 y cm라면 이 사람의 덩치는 (x, y)로 표시된다. 두 사람 A 와 B의 덩치가 각각 (x, y), (p, q)라고 할 때 x &amp;gt; p 그리고 y &amp;gt; q 이라면 우리는 A의 덩치가 B의 덩치보다 &quot;더 크다&quot;고 말한다. 예를 들어 어떤 A, B 두 사람의 덩치가 각각 (56, 177), (45, 165) 라고 한다면 A의 덩치가 B보다 큰 셈이 된다. 그런데 서로 다른 덩치끼리 크기를 정할 수 없는 경우도 있다. 예를 들어 두 사람 C와 D의 덩치가 각각 (45, 181), (55, 173)이라면 몸무게는 D가 C보다 더 무겁고, 키는 C가 더 크므로, &quot;덩치&quot;로만 볼 때 C와 D는 누구도 상대방보다 더 크다고 말할 수 없다.&lt;/p&gt;
&lt;p&gt;N명의 집단에서 각 사람의 덩치 등수는 자신보다 더 &quot;큰 덩치&quot;의 사람의 수로 정해진다. 만일 자신보다 더 큰 덩치의 사람이 k명이라면 그 사람의 덩치 등수는 k+1이 된다. 이렇게 등수를 결정하면 같은 덩치 등수를 가진 사람은 여러 명도 가능하다. 아래는 5명으로 이루어진 집단에서 각 사람의 덩치와 그 등수가 표시된 표이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;(몸무게, 키)&lt;/th&gt;
&lt;th&gt;덩치 등수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;(55, 185)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;(58, 183)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;(88, 186)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;(60, 175)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;(46, 155)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;위 표에서 C보다 더 큰 덩치의 사람이 없으므로 C는 1등이 된다. 그리고 A, B, D 각각의 덩치보다 큰 사람은 C뿐이므로 이들은 모두 2등이 된다. 그리고 E보다 큰 덩치는 A, B, C, D 이렇게 4명이므로 E의 덩치는 5등이 된다. 위 경우에 3등과 4등은 존재하지 않는다. 여러분은 학생 N명의 몸무게와 키가 담긴 입력을 읽어서 각 사람의 덩치 등수를 계산하여 출력해야 한다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 줄에는 전체 사람의 수 N이 주어진다. 그리고 이어지는 N개의 줄에는 각 사람의 몸무게와 키를 나타내는 양의 정수 x와 y가 하나의 공백을 두고 각각 나타난다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;여러분은 입력에 나열된 사람의 덩치 등수를 구해서 그 순서대로 첫 줄에 출력해야 한다. 단, 각 덩치 등수는 공백문자로 분리되어야 한다.&lt;/p&gt;
&lt;h2&gt;제한&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2 ≤ N ≤ 50&lt;/li&gt;
&lt;li&gt;10 ≤ x, y ≤ 200&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

typedef struct {
    int weight;
    int height;
    int rank;
} Person;

int main() {
    int n;

    scanf(&quot;%d&quot;, &amp;amp;n);

    Person people[n];

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%d %d&quot;, &amp;amp;people[i].weight, &amp;amp;people[i].height);
        people[i].rank = 1; 
    }

    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; n; j++) {
            if (i != j) { 
                if (people[i].weight &amp;lt; people[j].weight &amp;amp;&amp;amp;
                    people[i].height &amp;lt; people[j].height) {
                    people[i].rank++; 
                }
            }
        }
    }

    for (int i = 0; i &amp;lt; n; i++) {
        printf(&quot;%d &quot;, people[i].rank);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 19945번: 새로운 언어 CC</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/19945/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/19945/</guid><description>백준 19945번 새로운 언어 CC를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 14:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/19945&quot;&gt;https://www.acmicpc.net/problem/19945&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;C언어는 int형 변수를 32개의 bit를 이용하여 2의 보수 방식을 따라서 이진수의 형태로 저장한다.&lt;/p&gt;
&lt;p&gt;즉, 정수 10은 0000 0000 0000 0000 0000 0000 0000 1010으로 저장된다.&lt;/p&gt;
&lt;p&gt;하지만 세상을 뒤흔들 새로운 언어 CC는 메모리를 줄이기 위해 int형 변수를 저장할 때 앞에서부터 연속된 0은 저장하지 않는다. 즉, 정수 10은 1010으로 저장된다. 그 외에 저장하는 방법은 C언어와 같다.&lt;/p&gt;
&lt;p&gt;정수 n을 CC에 저장하는데 필요한 bit의 개수를 출력하여라. 단, n이 0일 때는 1개의 bit를 사용하여 0을 저장한다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;언어 CC에 저장할 n이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;변수 n을 저장하는데 필요한 bit의 개수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 정수 &lt;code&gt;n&lt;/code&gt;을 2의 보수 형태로 저장할 때, 앞쪽의 연속된 &lt;code&gt;0&lt;/code&gt;만 제거한다고 생각하면 됩니다.&lt;/p&gt;
&lt;p&gt;경우를 나누면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n == 0&lt;/code&gt; 이면 문제에서 특별히 &lt;code&gt;0&lt;/code&gt;을 1비트로 저장하므로 답은 &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n &amp;lt; 0&lt;/code&gt; 이면 2의 보수 표현에서 가장 앞 비트가 &lt;code&gt;1&lt;/code&gt;이고, 앞의 &lt;code&gt;0&lt;/code&gt;이 존재하지 않으므로 32비트 전체가 필요&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n &amp;gt; 0&lt;/code&gt; 이면 이진수로 표현했을 때 필요한 비트 수를 세면 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;양수의 경우에는 &lt;code&gt;n&lt;/code&gt;을 오른쪽으로 한 비트씩 밀면서 몇 번 이동하는지 세면 필요한 비트 수를 구할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;제한&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;-2,147,483,648 ≤ n ≤ 2,147,483,647&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int count = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    if (n == 0) {
        printf(&quot;1\n&quot;);
        return 0;
    }

    if (n &amp;lt; 0) {
        printf(&quot;32\n&quot;);
        return 0;
    }

    while (n &amp;gt; 0) {
        n &amp;gt;&amp;gt;= 1;
        count++;
    }

    printf(&quot;%d\n&quot;, count);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2160번: 그림 비교</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2160/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2160/</guid><description>백준 2160번 그림 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 14:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2160&quot;&gt;https://www.acmicpc.net/problem/2160&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;N(2 ≤ N ≤ 50)개의 그림이 있다. 각각의 그림은 5×7의 크기이고, 두 가지 색으로 되어 있다. 이때 두 가지의 색을 각각 ‘X’와 ‘.’으로 표현하기로 하자. 이러한 그림들이 주어졌을 때, 가장 비슷한 두 개의 그림을 찾아내는 프로그램을 작성하시오. 두 개의 그림에서 다른 칸의 개수가 가장 적을 때, 두 개의 그림이 가장 비슷하다고 하자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2160_image.png&quot; alt=&quot;2160_image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 위와 같은 두 개의 그림이 주어졌을 때, 색칠한 부분이 서로 다르게 된다. 위의 그림은 5개의 칸이 서로 다르다. 이와 같이 서로 다른 칸의 개수가 가장 작은 경우를 찾는 것이다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N이 주어진다. 다음 5×N개의 줄에 7개의 문자로 각각의 그림이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 가장 비슷한 두 그림의 번호를 출력한다. 그림의 번호는 입력되는 순서대로 1, 2, …, N이다. 번호를 출력할 때에는 작은 것을 먼저 출력한다. 입력은 항상 답이 한 가지인 경우만 주어진다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 31776번: 예비 소집 결과 보고서</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/31776/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/31776/</guid><description>백준 31776번 예비 소집 결과 보고서 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 13:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/31776&quot;&gt;https://www.acmicpc.net/problem/31776&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PPC 본 대회 하루 전에는 참가 팀들의 환경 적응을 위한 예비 소집이 진행된다. 예비 소집에는 3문제가 사용되었으며, N팀이 참여하였다. 각 문제와 팀에는 1번부터 차례대로 번호가 붙어 있다. PPC 운영진들은 예비 소집이 얼마나 효과적이었는지 알아보기 위해 보고서를 작성해 보기로 했다.&lt;/p&gt;
&lt;p&gt;예비 소집의 효과를 알아내기 위해서는 성실하게 참여한 팀의 수를 구해야 한다. 예비 소집에 성실하게 참여한 팀이란 다음과 같은 조건을 만족하는 팀이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세 문제 중 최소 하나를 해결하였다.&lt;/li&gt;
&lt;li&gt;문제를 번호가 작은 것부터 해결하였다. 즉, 어떤 문제를 시간 $T$에 해결했을 때 그보다 번호가 작은 문제들은 모두 해결하였으며 해결 시간이 $T$ 이하이고, 그보다 번호가 큰 문제들은 해결하지 않았거나 해결 시간이 $T$ 이상이어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 팀이 각 문제를 해결한 시간이 주어질 때, 예비 소집에 성실하게 참여한 팀의 수를 구하여라.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 번째 줄에 예비 소집에 참여한 팀의 수 $N$이 주어진다. $(1\le N\le 100)$&lt;/p&gt;
&lt;p&gt;두 번째 줄부터 $N$개의 줄에 걸쳐 각각 $i$번 팀이 $1$번 문제, $2$번 문제, $3$번 문제를 해결한 시간을 의미하는 $3$개의 정수 $T_{i,1}, T_{i,2}, T_{i,3}$이 공백으로 구분되어 주어진다. 만약 $i$번 팀이 $j$번 문제를 해결하지 않았다면 $T_{i,j} = -1$이다. $(-1\le T_{i,j}\le 120)$&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;예비 소집에 성실하게 참여한 팀의 수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;성실하게 참여한 팀의 조건을 3문제 기준으로 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;최소 한 문제는 풀어야 하므로 &lt;code&gt;1번 문제&lt;/code&gt;는 반드시 풀어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2번 문제&lt;/code&gt;를 풀었다면 &lt;code&gt;1번 문제&lt;/code&gt;도 풀었고, 시간은 &lt;code&gt;1번 ≤ 2번&lt;/code&gt;이어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3번 문제&lt;/code&gt;를 풀었다면 &lt;code&gt;1번&lt;/code&gt;, &lt;code&gt;2번&lt;/code&gt;도 모두 풀었고, 시간은 &lt;code&gt;1번 ≤ 2번 ≤ 3번&lt;/code&gt;이어야 합니다.&lt;/li&gt;
&lt;li&gt;중간 문제를 건너뛰고 풀면 안 되므로, &lt;code&gt;2번을 안 풀었는데 3번만 푼 경우&lt;/code&gt;는 성실한 참여가 아닙니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;즉 가능한 경우는 다음 셋뿐입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1번만 풂&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1번, 2번을 순서대로 풂&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1번, 2번, 3번을 순서대로 풂&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 팀마다 위 조건을 검사해서 성실한 팀이면 개수를 1 증가시키면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int answer = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        int t1, t2, t3;
        scanf(&quot;%d %d %d&quot;, &amp;amp;t1, &amp;amp;t2, &amp;amp;t3);

        int good = 1;

        // 최소 한 문제 해결해야 하므로 1번은 반드시 풀어야 함
        if (t1 == -1) {
            good = 0;
        }

        // 2번을 풀었다면 1번보다 빠를 수 없음
        if (t2 != -1 &amp;amp;&amp;amp; t1 &amp;gt; t2) {
            good = 0;
        }

        // 2번을 안 풀었는데 3번을 푼 경우 불가능
        if (t2 == -1 &amp;amp;&amp;amp; t3 != -1) {
            good = 0;
        }

        // 3번을 풀었다면 2번보다 빠를 수 없음
        if (t3 != -1 &amp;amp;&amp;amp; t2 &amp;gt; t3) {
            good = 0;
        }

        if (good) {
            answer++;
        }
    }

    printf(&quot;%d\n&quot;, answer);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 22193번: Multiply</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/22193/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/22193/</guid><description>백준 22193번 Multiply 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 13:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/22193&quot;&gt;https://www.acmicpc.net/problem/22193&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;음이 아닌 두 정수 A와 B의 곱을 계산하는 프로그램을 작성하시오. 두 정수는 10진수로 표기되며, 각각 N자리와 M자리 수입니다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 두 수의 길이 N과 M이 공백으로 구분되어 주어집니다. 둘째 줄에는 A가, 셋째 줄에는 B가 주어집니다. 주어지는 수는 0으로 시작하지 않습니다 (불필요한 0이 앞에 오지 않음).&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;A와 B의 곱을 0으로 시작하지 않게 출력하시오.&lt;/p&gt;
&lt;h2&gt;제한&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 ≤ N, M ≤ 50 000&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;서브태스크&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;배점&lt;/th&gt;
&lt;th&gt;제한&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;N, M ≤ 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;N, M ≤ 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;N, M ≤ 5 000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;추가 제약조건 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;큰 정수를 문자열로 받아, 사람이 계산하듯 각 자리수를 곱해 누적한 뒤 자리올림을 처리하는 방식입니다.&lt;/p&gt;
&lt;p&gt;구체적으로는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A의 각 자리와 B의 각 자리를 곱해 &lt;code&gt;res[i + j + 1]&lt;/code&gt;에 누적&lt;/li&gt;
&lt;li&gt;곱셈이 끝난 뒤 뒤에서부터 자리올림 처리&lt;/li&gt;
&lt;li&gt;앞쪽의 불필요한 0을 건너뛰고 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하는 방식으로 구현할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

#define MAX 50001
#define MAX_RES 100001

char a[MAX], b[MAX];
int res[MAX_RES];

int main() {
    int n, m;
    scanf(&quot;%d %d&quot;, &amp;amp;n, &amp;amp;m);
    scanf(&quot;%s&quot;, a);
    scanf(&quot;%s&quot;, b);

    for (int i = n - 1; i &amp;gt;= 0; i--) {
        int da = a[i] - &apos;0&apos;;
        for (int j = m - 1; j &amp;gt;= 0; j--) {
            res[i + j + 1] += da * (b[j] - &apos;0&apos;);
        }
    }

    for (int i = n + m - 1; i &amp;gt; 0; i--) {
        res[i - 1] += res[i] / 10;
        res[i] %= 10;
    }

    int start = 0;
    while (start &amp;lt; n + m - 1 &amp;amp;&amp;amp; res[start] == 0) {
        start++;
    }

    for (int i = start; i &amp;lt; n + m; i++) {
        printf(&quot;%d&quot;, res[i]);
    }
    printf(&quot;\n&quot;);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 31868번: 수박 게임</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/31868/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/31868/</guid><description>백준 31868번 수박 게임 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 12:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/31868&quot;&gt;https://www.acmicpc.net/problem/31868&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;$1$단계 과일은 체리, $N$단계 과일은 수박이다.&lt;/p&gt;
&lt;p&gt;$i$단계 과일 $2$개를 소모하여 $i+1$단계 과일 $1$개를 만들 수 있다. $(1 \leq i \leq N-1)$&lt;/p&gt;
&lt;p&gt;$K$개의 체리로 최대 몇 개의 수박을 만들 수 있는지 구해보자!&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 번째 줄에 정수 $N$과 $K$가 공백을 사이에 두고 주어진다. $(2 \leq N \leq 30; 1 \leq K \leq 10^9)$&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫 번째 줄에 만들 수 있는 수박의 최대 개수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;N&lt;/code&gt;단계 과일 1개를 만들기 위해 필요한 체리 개수는&lt;/p&gt;
&lt;p&gt;$$
2^{N-1}
$$&lt;/p&gt;
&lt;p&gt;개입니다.&lt;/p&gt;
&lt;p&gt;따라서 만들 수 있는 수박의 최대 개수는&lt;/p&gt;
&lt;p&gt;$$
\left\lfloor \frac{K}{2^{N-1}} \right\rfloor
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n, k;
    int m = 1;
    scanf(&quot;%d %d&quot;, &amp;amp;n, &amp;amp;k);

    for (int i = 1; i &amp;lt; n; i++) {
        m *= 2;
    }

    printf(&quot;%d\n&quot;, k / m);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 10768번: 특별한 날</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/10768/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/10768/</guid><description>백준 10768번 특별한 날 문제를 C로 풀이합니다.</description><pubDate>Tue, 10 Mar 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/10768&quot;&gt;https://www.acmicpc.net/problem/10768&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2월 18일은 올해 CCC에 있어서 특별한 날이다.&lt;/p&gt;
&lt;p&gt;사용자로부터 정수인 월과 일을 입력받아 날짜가 2월 18일인지 전인지 후인지를 출력하는 프로그램이다.&lt;/p&gt;
&lt;p&gt;만약 날짜가 2월 18일 전이면, &quot;Before&quot;을 출력한다. 만약 날짜가 2월 18일 후면, &quot;After&quot;을 출력한다. 만약 2월 18일이라면 &quot;Special&quot; 을 출력한다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;입력은 두 개의 정수가 각 줄에 걸쳐 입력된다. 이 정수들은 2015년의 날짜에 포함된다.&lt;/p&gt;
&lt;p&gt;첫 번째 줄은 1에서 12사이의 월을 입력한다.&lt;/p&gt;
&lt;p&gt;두 번째 줄은 1에서 31사이의 그 달에 들어있는 날짜를 입력한다 .&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;마지막 줄에 &quot;Before&quot;, &quot;After&quot;나 &quot;Special&quot;을 출력한다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int a, b;
    scanf(&quot;%d&quot;, &amp;amp;a);
    scanf(&quot;%d&quot;, &amp;amp;b);

    if (a &amp;gt; 2 || (a == 2 &amp;amp;&amp;amp; b &amp;gt; 18)) {
        printf(&quot;After\n&quot;);
    } else if (a &amp;lt; 2 || (a == 2 &amp;amp;&amp;amp; b &amp;lt; 18)) {
        printf(&quot;Before\n&quot;);
    } else {
        printf(&quot;Special\n&quot;);
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 4949번: 균형잡힌 세상</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/4949/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/4949/</guid><description>백준 4949번 균형잡힌 세상 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 20:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/4949&quot;&gt;https://www.acmicpc.net/problem/4949&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;세계는 균형이 잘 잡혀있어야 한다. 양과 음, 빛과 어둠 그리고 왼쪽 괄호와 오른쪽 괄호처럼 말이다.&lt;/p&gt;
&lt;p&gt;정민이의 임무는 어떤 문자열이 주어졌을 때, 괄호들의 균형이 잘 맞춰져 있는지 판단하는 프로그램을 짜는 것이다.&lt;/p&gt;
&lt;p&gt;문자열에 포함되는 괄호는 소괄호(&quot;()&quot;) 와 대괄호(&quot;[]&quot;)로 2종류이고, 문자열이 균형을 이루는 조건은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 왼쪽 소괄호(&quot;(&quot;)는 오른쪽 소괄호(&quot;)&quot;)와만 짝을 이뤄야 한다.&lt;/li&gt;
&lt;li&gt;모든 왼쪽 대괄호(&quot;[&quot;)는 오른쪽 대괄호(&quot;]&quot;)와만 짝을 이뤄야 한다.&lt;/li&gt;
&lt;li&gt;모든 오른쪽 괄호들은 자신과 짝을 이룰 수 있는 왼쪽 괄호가 존재한다.&lt;/li&gt;
&lt;li&gt;모든 괄호들의 짝은 1:1 매칭만 가능하다. 즉, 괄호 하나가 둘 이상의 괄호와 짝지어지지 않는다.&lt;/li&gt;
&lt;li&gt;짝을 이루는 두 괄호가 있을 때, 그 사이에 있는 문자열도 균형이 잡혀야 한다.
정민이를 도와 문자열이 주어졌을 때 균형잡힌 문자열인지 아닌지를 판단해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;각 문자열은 마지막 글자를 제외하고 영문 알파벳, 공백, 소괄호(&quot;( )&quot;), 대괄호(&quot;[ ]&quot;)로 이루어져 있으며, 온점(&quot;.&quot;)으로 끝나고, 길이는 100글자보다 작거나 같다.&lt;/p&gt;
&lt;p&gt;입력의 종료조건으로 맨 마지막에 온점 하나(&quot;.&quot;)가 들어온다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;각 줄마다 해당 문자열이 균형을 이루고 있으면 &quot;yes&quot;를, 아니면 &quot;no&quot;를 출력한다.&lt;/p&gt;
&lt;h2&gt;힌트&lt;/h2&gt;
&lt;p&gt;7번째의 &quot; .&quot;와 같이 괄호가 하나도 없는 경우도 균형잡힌 문자열로 간주할 수 있다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;문자열을 왼쪽부터 한 글자씩 보면서 괄호만 스택에 처리하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여는 괄호 &lt;code&gt;(&lt;/code&gt; 또는 &lt;code&gt;[&lt;/code&gt; 가 나오면 스택에 넣습니다.&lt;/li&gt;
&lt;li&gt;닫는 괄호 &lt;code&gt;)&lt;/code&gt; 또는 &lt;code&gt;]&lt;/code&gt; 가 나오면 스택의 맨 위와 짝이 맞는지 확인합니다.&lt;/li&gt;
&lt;li&gt;짝이 맞지 않거나, 스택이 비어 있는데 닫는 괄호가 나오면 균형이 맞지 않는 문자열입니다.&lt;/li&gt;
&lt;li&gt;문자열을 끝까지 확인한 뒤 스택이 비어 있어야 균형이 맞습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;마지막 입력이 &lt;code&gt;.&lt;/code&gt; 한 글자만 있는 경우에는 종료합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

int main() {
    char str[102];

    while (1) {
        fgets(str, sizeof(str), stdin);

        if (strcmp(str, &quot;.\n&quot;) == 0 || strcmp(str, &quot;.&quot;) == 0) {
            break;
        }

        char stack[102];
        int top = -1;
        bool balanced = true;

        for (int i = 0; str[i] != &apos;\0&apos;; i++) {
            char ch = str[i];

            if (ch == &apos;(&apos; || ch == &apos;[&apos;) {
                stack[++top] = ch;
            } else if (ch == &apos;)&apos;) {
                if (top == -1 || stack[top] != &apos;(&apos;) {
                    balanced = false;
                    break;
                }
                top--;
            } else if (ch == &apos;]&apos;) {
                if (top == -1 || stack[top] != &apos;[&apos;) {
                    balanced = false;
                    break;
                }
                top--;
            }
        }

        if (balanced &amp;amp;&amp;amp; top == -1) {
            printf(&quot;yes\n&quot;);
        } else {
            printf(&quot;no\n&quot;);
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 4153번: 직각삼각형</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/4153/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/4153/</guid><description>백준 4153번 직각삼각형 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 20:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/4153&quot;&gt;https://www.acmicpc.net/problem/4153&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;과거 이집트인들은 각 변들의 길이가 3, 4, 5인 삼각형이 직각 삼각형인것을 알아냈다. 주어진 세변의 길이로 삼각형이 직각인지 아닌지 구분하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;입력은 여러개의 테스트케이스로 주어지며 마지막줄에는 0 0 0이 입력된다. 각 테스트케이스는 모두 30,000보다 작은 양의 정수로 주어지며, 각 입력은 변의 길이를 의미한다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;각 입력에 대해 직각 삼각형이 맞다면 &quot;right&quot;, 아니라면 &quot;wrong&quot;을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;직각삼각형인지 확인하려면 가장 긴 변을 빗변으로 두고 피타고라스 정리를 확인하면 됩니다.&lt;/p&gt;
&lt;p&gt;그래서 세 변을 오름차순으로 정렬한 뒤,&lt;/p&gt;
&lt;p&gt;$$
a^2 + b^2 = c^2
$$&lt;/p&gt;
&lt;p&gt;를 만족하는지 검사하면 됩니다.&lt;/p&gt;
&lt;p&gt;입력이 &lt;code&gt;0 0 0&lt;/code&gt;이면 종료합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int a, b, c;

    while (1) {
        scanf(&quot;%d %d %d&quot;, &amp;amp;a, &amp;amp;b, &amp;amp;c);
        if (a == 0 &amp;amp;&amp;amp; b == 0 &amp;amp;&amp;amp; c == 0) {
            break;
        }

        if (a &amp;gt; b) {
            int temp = a;
            a = b;
            b = temp;
        }
        if (b &amp;gt; c) {
            int temp = b;
            b = c;
            c = temp;
        }
        if (a &amp;gt; c) {
            int temp = a;
            a = c;
            c = temp;
        }

        if (a * a + b * b == c * c) {
            printf(&quot;right\n&quot;);
        } else {
            printf(&quot;wrong\n&quot;);
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2869번: 달팽이는 올라가고 싶다</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2869/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2869/</guid><description>백준 2869번 달팽이는 올라가고 싶다 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 19:40:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2869&quot;&gt;https://www.acmicpc.net/problem/2869&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;땅 위에 달팽이가 있다. 이 달팽이는 높이가 V미터인 나무 막대를 올라갈 것이다.&lt;/p&gt;
&lt;p&gt;달팽이는 낮에 A미터 올라갈 수 있다. 하지만, 밤에 잠을 자는 동안 B미터 미끄러진다. 또, 정상에 올라간 후에는 미끄러지지 않는다.&lt;/p&gt;
&lt;p&gt;달팽이가 나무 막대를 모두 올라가려면, 며칠이 걸리는지 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 세 정수 A, B, V가 공백으로 구분되어서 주어진다. (1 ≤ B &amp;lt; A ≤ V ≤ 1,000,000,000)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 달팽이가 나무 막대를 모두 올라가는데 며칠이 걸리는지 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제에서 가장 중요한 점은 &lt;strong&gt;정상에 도달한 날에는 밤에 미끄러지지 않는다&lt;/strong&gt;는 것입니다.&lt;/p&gt;
&lt;p&gt;처음에는 하루에 올라가는 양이 &lt;code&gt;A - B&lt;/code&gt; 이므로 단순히 &lt;code&gt;V / (A - B)&lt;/code&gt; 처럼 계산하고 싶어집니다.&lt;br /&gt;
하지만 마지막 날은 조금 다르게 생각해야 합니다.&lt;/p&gt;
&lt;p&gt;달팽이는 마지막 날 낮에 &lt;code&gt;A&lt;/code&gt;미터를 올라가서 정상에 도달하면 끝납니다.&lt;br /&gt;
즉, 마지막 날 이전까지는 전체 높이 &lt;code&gt;V&lt;/code&gt;가 아니라 &lt;strong&gt;&lt;code&gt;V - A&lt;/code&gt;까지만 도달해 있으면 충분합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;하루가 완전히 지나면 실제로 늘어나는 높이는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;$$
A - B
$$&lt;/p&gt;
&lt;p&gt;따라서 마지막 날 전까지 필요한 일수는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;$$
\left\lceil \frac{V - A}{A - B} \right\rceil
$$&lt;/p&gt;
&lt;p&gt;여기에 마지막 날 하루를 더하면 전체 날짜는&lt;/p&gt;
&lt;p&gt;$$
\left\lceil \frac{V - A}{A - B} \right\rceil + 1
$$&lt;/p&gt;
&lt;p&gt;이 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;정수 연산에서는 올림을 직접 사용할 수 없으므로, 다음 공식을 이용합니다.&lt;/p&gt;
&lt;p&gt;$$
\left\lceil \frac{x}{y} \right\rceil = \frac{x + y - 1}{y}
$$&lt;/p&gt;
&lt;p&gt;이를 그대로 대입하면&lt;/p&gt;
&lt;h1&gt;$$
\left\lceil \frac{V - A}{A - B} \right\rceil&lt;/h1&gt;
&lt;p&gt;\frac{(V - A) + (A - B) - 1}{A - B}
$$&lt;/p&gt;
&lt;p&gt;이므로 최종 식은 다음과 같습니다.&lt;/p&gt;
&lt;h1&gt;$$
\text{days}&lt;/h1&gt;
&lt;p&gt;\frac{(V - A) + (A - B) - 1}{A - B} + 1
$$&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;예를 들어 &lt;code&gt;A = 2&lt;/code&gt;, &lt;code&gt;B = 1&lt;/code&gt;, &lt;code&gt;V = 5&lt;/code&gt; 라고 해보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;마지막 날 전까지 필요한 높이: &lt;code&gt;V - A = 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;하루 순증가량: &lt;code&gt;A - B = 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 마지막 날 전까지 3일이 필요하고, 마지막 날 1일을 더해 총 4일이 됩니다.&lt;/p&gt;
&lt;p&gt;실제로 진행해보면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1일차 낮: 2, 밤: 1&lt;/li&gt;
&lt;li&gt;2일차 낮: 3, 밤: 2&lt;/li&gt;
&lt;li&gt;3일차 낮: 4, 밤: 3&lt;/li&gt;
&lt;li&gt;4일차 낮: 5 도달&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;로 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 문제는 반복문으로도 구현할 수 있지만, &lt;code&gt;V&lt;/code&gt;의 범위가 매우 크기 때문에 단순 시뮬레이션은 비효율적입니다.&lt;br /&gt;
따라서 수식을 이용해 한 번에 계산하는 방식이 적절합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int a, b, v;

    scanf(&quot;%d %d %d&quot;, &amp;amp;a, &amp;amp;b, &amp;amp;v);

    int days = (v - a + (a - b) - 1) / (a - b) + 1;

    printf(&quot;%d\n&quot;, days);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2798번: 블랙잭</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2798/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2798/</guid><description>백준 2798번 블랙잭 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 19:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2798&quot;&gt;https://www.acmicpc.net/problem/2798&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;카지노에서 제일 인기 있는 게임 블랙잭의 규칙은 상당히 쉽다. 카드의 합이 21을 넘지 않는 한도 내에서, 카드의 합을 최대한 크게 만드는 게임이다. 블랙잭은 카지노마다 다양한 규정이 있다.&lt;/p&gt;
&lt;p&gt;한국 최고의 블랙잭 고수 김정인은 새로운 블랙잭 규칙을 만들어 상근, 창영이와 게임하려고 한다.&lt;/p&gt;
&lt;p&gt;김정인 버전의 블랙잭에서 각 카드에는 양의 정수가 쓰여 있다. 그 다음, 딜러는 N장의 카드를 모두 숫자가 보이도록 바닥에 놓는다. 그런 후에 딜러는 숫자 M을 크게 외친다.&lt;/p&gt;
&lt;p&gt;이제 플레이어는 제한된 시간 안에 N장의 카드 중에서 3장의 카드를 골라야 한다. 블랙잭 변형 게임이기 때문에, 플레이어가 고른 카드의 합은 M을 넘지 않으면서 M과 최대한 가깝게 만들어야 한다.&lt;/p&gt;
&lt;p&gt;N장의 카드에 써져 있는 숫자가 주어졌을 때, M을 넘지 않으면서 M에 최대한 가까운 카드 3장의 합을 구해 출력하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 카드의 개수 N(3 ≤ N ≤ 100)과 M(10 ≤ M ≤ 300,000)이 주어진다. 둘째 줄에는 카드에 쓰여 있는 수가 주어지며, 이 값은 100,000을 넘지 않는 양의 정수이다.&lt;/p&gt;
&lt;p&gt;합이 M을 넘지 않는 카드 3장을 찾을 수 있는 경우만 입력으로 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 M을 넘지 않으면서 M에 최대한 가까운 카드 3장의 합을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;세 장의 카드를 고르는 방법은 3중 반복문으로 만들 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫 번째 카드 선택&lt;/li&gt;
&lt;li&gt;두 번째 카드 선택&lt;/li&gt;
&lt;li&gt;세 번째 카드 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 조합마다 세 카드의 합을 구한 뒤,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;합이 M을 넘지 않고&lt;/li&gt;
&lt;li&gt;지금까지 찾은 값보다 크면&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정답을 갱신하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n, m;
    int arr[100];
    int max = 0;

    scanf(&quot;%d %d&quot;, &amp;amp;n, &amp;amp;m);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%d&quot;, &amp;amp;arr[i]);
    }

    for (int i = 0; i &amp;lt; n - 2; i++) {
        for (int j = i + 1; j &amp;lt; n - 1; j++) {
            for (int k = j + 1; k &amp;lt; n; k++) {
                int sum = arr[i] + arr[j] + arr[k];

                if (sum &amp;lt;= m &amp;amp;&amp;amp; sum &amp;gt; max) {
                    max = sum;
                }
            }
        }
    }

    printf(&quot;%d\n&quot;, max);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2839번: 설탕 배달</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2839/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2839/</guid><description>백준 2839번 설탕 배달 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 19:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2839&quot;&gt;https://www.acmicpc.net/problem/2839&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;상근이는 요즘 설탕공장에서 설탕을 배달하고 있다. 상근이는 지금 사탕가게에 설탕을 정확하게 N킬로그램을 배달해야 한다. 설탕공장에서 만드는 설탕은 봉지에 담겨져 있다. 봉지는 3킬로그램 봉지와 5킬로그램 봉지가 있다.&lt;/p&gt;
&lt;p&gt;상근이는 귀찮기 때문에, 최대한 적은 봉지를 들고 가려고 한다. 예를 들어, 18킬로그램 설탕을 배달해야 할 때, 3킬로그램 봉지 6개를 가져가도 되지만, 5킬로그램 3개와 3킬로그램 1개를 배달하면, 더 적은 개수의 봉지를 배달할 수 있다.&lt;/p&gt;
&lt;p&gt;상근이가 설탕을 정확하게 N킬로그램 배달해야 할 때, 봉지 몇 개를 가져가면 되는지 그 수를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N이 주어진다. (3 ≤ N ≤ 5000)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;상근이가 배달하는 봉지의 최소 개수를 출력한다. 만약, 정확하게 N킬로그램을 만들 수 없다면 -1을 출력한다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int count = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    while (n &amp;gt;= 0) {
        if (n % 5 == 0) {
            count += n / 5;
            printf(&quot;%d\n&quot;, count);
            return 0;
        }
        n -= 3;
        count++;
    }

    printf(&quot;-1&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2775번: 부녀회장이 될테야</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2775/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2775/</guid><description>백준 2775번 부녀회장이 될테야 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 19:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2775&quot;&gt;https://www.acmicpc.net/problem/2775&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;평소 반상회에 참석하는 것을 좋아하는 주희는 이번 기회에 부녀회장이 되고 싶어 각 층의 사람들을 불러 모아 반상회를 주최하려고 한다.&lt;/p&gt;
&lt;p&gt;이 아파트에 거주를 하려면 조건이 있는데, “a층의 b호에 살려면 자신의 아래(a-1)층의 1호부터 b호까지 사람들의 수의 합만큼 사람들을 데려와 살아야 한다” 는 계약 조항을 꼭 지키고 들어와야 한다.&lt;/p&gt;
&lt;p&gt;아파트에 비어있는 집은 없고 모든 거주민들이 이 계약 조건을 지키고 왔다고 가정했을 때, 주어지는 양의 정수 k와 n에 대해 k층에 n호에는 몇 명이 살고 있는지 출력하라. 단, 아파트에는 0층부터 있고 각층에는 1호부터 있으며, 0층의 i호에는 i명이 산다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 번째 줄에 Test case의 수 T가 주어진다. 그리고 각각의 케이스마다 입력으로 첫 번째 줄에 정수 k, 두 번째 줄에 정수 n이 주어진다&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;각각의 Test case에 대해서 해당 집에 거주민 수를 출력하라.&lt;/p&gt;
&lt;h2&gt;제한&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 ≤ k, n ≤ 14&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;0층 i호 = i&lt;/code&gt;를 먼저 채운 뒤,&lt;/p&gt;
&lt;p&gt;$$
apt[k][n] = apt[k][n-1] + apt[k-1][n]
$$&lt;/p&gt;
&lt;p&gt;점화식으로 배열을 채우면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main(void) {
    int apt[15][15] = {0};
    int t;

    for (int i = 1; i &amp;lt;= 14; i++) {
        apt[0][i] = i;
    }

    for (int k = 1; k &amp;lt;= 14; k++) {
        for (int n = 1; n &amp;lt;= 14; n++) {
            apt[k][n] = apt[k][n - 1] + apt[k - 1][n];
        }
    }

    scanf(&quot;%d&quot;, &amp;amp;t);

    while (t--) {
        int k, n;
        scanf(&quot;%d&quot;, &amp;amp;k);
        scanf(&quot;%d&quot;, &amp;amp;n);

        printf(&quot;%d\n&quot;, apt[k][n]);
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2751번: 수 정렬하기 2</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2751/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2751/</guid><description>백준 2751번 수 정렬하기 2 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 19:10:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2751&quot;&gt;https://www.acmicpc.net/problem/2751&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;N개의 수가 주어졌을 때, 이를 오름차순으로 정렬하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄부터 N개의 줄에는 수가 주어진다. 이 수는 절댓값이 1,000,000보다 작거나 같은 정수이다. 수는 중복되지 않는다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄부터 N개의 줄에 오름차순으로 정렬한 결과를 한 줄에 하나씩 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 입력 크기가 최대 1,000,000이므로, 버블 정렬이나 선택 정렬처럼&lt;/p&gt;
&lt;p&gt;$$
O(N^2)
$$&lt;/p&gt;
&lt;p&gt;이 걸리는 정렬은 사용할 수 없습니다.&lt;/p&gt;
&lt;p&gt;보통은&lt;/p&gt;
&lt;p&gt;$$
O(N \log N)
$$&lt;/p&gt;
&lt;p&gt;정도의 정렬 알고리즘을 사용해야 합니다.&lt;/p&gt;
&lt;p&gt;이 문제는 두 가지 방식으로 풀 수 있습니다.&lt;/p&gt;
&lt;h3&gt;1. 병합 정렬로 푸는 방법&lt;/h3&gt;
&lt;p&gt;가장 일반적인 방법입니다.&lt;br /&gt;
표준 라이브러리 정렬 함수 없이도 직접 구현할 수 있고, 시간복잡도는&lt;/p&gt;
&lt;p&gt;$$
O(N \log N)
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;p&gt;입력 값의 범위와 상관없이 사용할 수 있어서, 일반적인 정렬 문제 풀이로 적합합니다.&lt;/p&gt;
&lt;h3&gt;2. 등장 여부 배열로 푸는 방법&lt;/h3&gt;
&lt;p&gt;이 문제는 입력되는 수의 범위가&lt;/p&gt;
&lt;p&gt;$$
-1{,}000{,}000 \sim 1{,}000{,}000
$$&lt;/p&gt;
&lt;p&gt;으로 제한되어 있고, 수가 &lt;strong&gt;중복되지 않는다&lt;/strong&gt;는 조건이 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 각 수가 등장했는지를 배열에 기록한 뒤, 작은 값부터 순서대로 출력하면 정렬 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 방법의 시간복잡도는&lt;/p&gt;
&lt;p&gt;$$
O(N + K)
$$&lt;/p&gt;
&lt;p&gt;이고, 여기서&lt;/p&gt;
&lt;p&gt;$$
K = 2{,}000{,}001
$$&lt;/p&gt;
&lt;p&gt;은 가능한 정수 범위의 개수입니다.&lt;/p&gt;
&lt;p&gt;즉, 이 문제에서는 병합 정렬보다도 더 단순하게 해결할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;h3&gt;코드 1. 병합 정렬&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

#define MAX 1000000

int arr[MAX];
int temp[MAX];

void merge_sort(int left, int right) {
    if (left &amp;gt;= right) return;

    int mid = (left + right) / 2;
    merge_sort(left, mid);
    merge_sort(mid + 1, right);

    int i = left;
    int j = mid + 1;
    int k = left;

    while (i &amp;lt;= mid &amp;amp;&amp;amp; j &amp;lt;= right) {
        if (arr[i] &amp;lt; arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }

    while (i &amp;lt;= mid) temp[k++] = arr[i++];
    while (j &amp;lt;= right) temp[k++] = arr[j++];

    for (int x = left; x &amp;lt;= right; x++) {
        arr[x] = temp[x];
    }
}

int main(void) {
    int n;
    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%d&quot;, &amp;amp;arr[i]);
    }

    merge_sort(0, n - 1);

    for (int i = 0; i &amp;lt; n; i++) {
        printf(&quot;%d\n&quot;, arr[i]);
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;코드 2. 등장 여부 배열&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

#define OFFSET 1000000
#define SIZE 2000001

int exist[SIZE];

int main(void) {
    int n, x;
    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%d&quot;, &amp;amp;x);
        exist[x + OFFSET] = 1;
    }

    for (int i = 0; i &amp;lt; SIZE; i++) {
        if (exist[i]) {
            printf(&quot;%d\n&quot;, i - OFFSET);
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2609번: 최대공약수와 최소공배수</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2609/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2609/</guid><description>백준 2609번 최대공약수와 최소공배수 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 18:50:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2609&quot;&gt;https://www.acmicpc.net/problem/2609&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;두 개의 자연수를 입력받아 최대 공약수와 최소 공배수를 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에는 두 개의 자연수가 주어진다. 이 둘은 10,000이하의 자연수이며 사이에 한 칸의 공백이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에는 입력으로 주어진 두 수의 최대공약수를, 둘째 줄에는 입력으로 주어진 두 수의 최소 공배수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;최대공약수는 &lt;strong&gt;유클리드 호제법&lt;/strong&gt;으로 구할 수 있습니다.&lt;/p&gt;
&lt;p&gt;두 수 &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;에 대해&lt;/p&gt;
&lt;p&gt;$$
\gcd(a, b) = \gcd(b, a \bmod b)
$$&lt;/p&gt;
&lt;p&gt;를 반복하면, 나머지가 0이 되는 순간의 값이 최대공약수입니다.&lt;/p&gt;
&lt;p&gt;최소공배수는 최대공약수를 이용해&lt;/p&gt;
&lt;p&gt;$$
\operatorname{lcm}(a, b) = \frac{a \cdot b}{\gcd(a, b)}
$$&lt;/p&gt;
&lt;p&gt;로 구할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int gcd(int a, int b) {
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

int main() {
    int a, b;
    scanf(&quot;%d %d&quot;, &amp;amp;a, &amp;amp;b);

    int g = gcd(a, b);
    int l = a * b / g;

    printf(&quot;%d\n&quot;, g);
    printf(&quot;%d\n&quot;, l);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2292번: 벌집</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2292/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2292/</guid><description>백준 2292번 벌집 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 18:45:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2292&quot;&gt;https://www.acmicpc.net/problem/2292&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2292_image.png&quot; alt=&quot;2292_image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위의 그림과 같이 육각형으로 이루어진 벌집이 있다. 그림에서 보는 바와 같이 중앙의 방 1부터 시작해서 이웃하는 방에 돌아가면서 1씩 증가하는 번호를 주소로 매길 수 있다. 숫자 N이 주어졌을 때, 벌집의 중앙 1에서 N번 방까지 최소 개수의 방을 지나서 갈 때 몇 개의 방을 지나가는지(시작과 끝을 포함하여)를 계산하는 프로그램을 작성하시오. 예를 들면, 13까지는 3개, 58까지는 5개를 지난다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N(1 ≤ N ≤ 1,000,000,000)이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;입력으로 주어진 방까지 최소 개수의 방을 지나서 갈 때 몇 개의 방을 지나는지 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;겹이 하나 늘어날 때마다 방의 개수는 &lt;code&gt;6&lt;/code&gt;, &lt;code&gt;12&lt;/code&gt;, &lt;code&gt;18&lt;/code&gt;, ... 처럼 &lt;code&gt;6&lt;/code&gt;의 배수만큼 증가합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1번째 겹: &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;2번째 겹: &lt;code&gt;2 ~ 7&lt;/code&gt; → 6개 증가&lt;/li&gt;
&lt;li&gt;3번째 겹: &lt;code&gt;8 ~ 19&lt;/code&gt; → 12개 증가&lt;/li&gt;
&lt;li&gt;4번째 겹: &lt;code&gt;20 ~ 37&lt;/code&gt; → 18개 증가...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현재 겹의 마지막 번호를 계속 늘려 가면서, 입력값 &lt;code&gt;N&lt;/code&gt;이 어느 겹에 포함되는지 찾으면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    scanf(&quot;%d&quot;, &amp;amp;n);

    int a = 1;
    int res = 1;

    while (n &amp;gt; a) {
        a += 6 * res;
        res++;
    }

    printf(&quot;%d\n&quot;, res);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2231번: 분해합</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2231/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2231/</guid><description>백준 2231번 분해합 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 18:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2231&quot;&gt;https://www.acmicpc.net/problem/2231&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;어떤 자연수 N이 있을 때, 그 자연수 N의 분해합은 N과 N을 이루는 각 자리수의 합을 의미한다. 어떤 자연수 M의 분해합이 N인 경우, M을 N의 생성자라 한다. 예를 들어, 245의 분해합은 256(=245+2+4+5)이 된다. 따라서 245는 256의 생성자가 된다. 물론, 어떤 자연수의 경우에는 생성자가 없을 수도 있다. 반대로, 생성자가 여러 개인 자연수도 있을 수 있다.&lt;/p&gt;
&lt;p&gt;자연수 N이 주어졌을 때, N의 가장 작은 생성자를 구해내는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 자연수 N(1 ≤ N ≤ 1,000,000)이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 답을 출력한다. 생성자가 없는 경우에는 0을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;1부터 N-1까지 각 수의 분해합을 구해 보면서, 처음으로 N이 되는 수를 찾으면 됩니다.&lt;br /&gt;
가장 먼저 찾은 수가 가장 작은 생성자입니다.&lt;/p&gt;
&lt;p&gt;어떤 수 &lt;code&gt;i&lt;/code&gt;의 분해합은&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자기 자신 &lt;code&gt;i&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;각 자리수의 합&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;을 더한 값입니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int result = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 1; i &amp;lt; n; i++) {
        int num = i;
        int sum = 0;

        while (num != 0) {
            sum += num % 10;
            num /= 10;
        }

        if (i + sum == n) {
            result = i;
            break;
        }
    }

    printf(&quot;%d\n&quot;, result);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2164번: 카드2</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2164/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2164/</guid><description>백준 2164번 카드2 문제를 C로 풀이합니다.</description><pubDate>Mon, 09 Mar 2026 18:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2164&quot;&gt;https://www.acmicpc.net/problem/2164&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;N장의 카드가 있다. 각각의 카드는 차례로 1부터 N까지의 번호가 붙어 있으며, 1번 카드가 제일 위에, N번 카드가 제일 아래인 상태로 순서대로 카드가 놓여 있다.&lt;/p&gt;
&lt;p&gt;이제 다음과 같은 동작을 카드가 한 장 남을 때까지 반복하게 된다. 우선, 제일 위에 있는 카드를 바닥에 버린다. 그 다음, 제일 위에 있는 카드를 제일 아래에 있는 카드 밑으로 옮긴다.&lt;/p&gt;
&lt;p&gt;예를 들어 N=4인 경우를 생각해 보자. 카드는 제일 위에서부터 1234 의 순서로 놓여있다. 1을 버리면 234가 남는다. 여기서 2를 제일 아래로 옮기면 342가 된다. 3을 버리면 42가 되고, 4를 밑으로 옮기면 24가 된다. 마지막으로 2를 버리고 나면, 남는 카드는 4가 된다.&lt;/p&gt;
&lt;p&gt;N이 주어졌을 때, 제일 마지막에 남게 되는 카드를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 정수 N(1 ≤ N ≤ 500,000)이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 남게 되는 카드의 번호를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;큐처럼 시뮬레이션하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;맨 앞 카드를 하나 제거&lt;/li&gt;
&lt;li&gt;다음 맨 앞 카드를 꺼내서 맨 뒤에 다시 넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C에서는 원형 큐처럼 배열과 &lt;code&gt;front&lt;/code&gt;, &lt;code&gt;rear&lt;/code&gt;를 사용해 구현할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int queue[1000001];
    int front = 0, rear = 0;
    int count;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 1; i &amp;lt;= n; i++) {
        queue[rear++] = i;
    }

    count = n;

    while (count &amp;gt; 1) {
        front++;
        count--;

        queue[rear++] = queue[front++];
    }

    printf(&quot;%d\n&quot;, queue[front]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2108번: 통계학</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2108/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2108/</guid><description>백준 2108번 통계학 문제를 C로 풀이합니다.</description><pubDate>Sun, 08 Mar 2026 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2108&quot;&gt;https://www.acmicpc.net/problem/2108&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;수를 처리하는 것은 통계학에서 상당히 중요한 일이다. 통계학에서 N개의 수를 대표하는 기본 통계값에는 다음과 같은 것들이 있다. 단, N은 홀수라고 가정하자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;산술평균 : N개의 수들의 합을 N으로 나눈 값&lt;/li&gt;
&lt;li&gt;중앙값 : N개의 수들을 증가하는 순서로 나열했을 경우 그 중앙에 위치하는 값&lt;/li&gt;
&lt;li&gt;최빈값 : N개의 수들 중 가장 많이 나타나는 값&lt;/li&gt;
&lt;li&gt;범위 : N개의 수들 중 최댓값과 최솟값의 차이
N개의 수가 주어졌을 때, 네 가지 기본 통계값을 구하는 프로그램을 작성하시오.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 수의 개수 N(1 ≤ N ≤ 500,000)이 주어진다. 단, N은 홀수이다. 그 다음 N개의 줄에는 정수들이 주어진다. 입력되는 정수의 절댓값은 4,000을 넘지 않는다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에는 산술평균을 출력한다. 소수점 이하 첫째 자리에서 반올림한 값을 출력한다.&lt;/p&gt;
&lt;p&gt;둘째 줄에는 중앙값을 출력한다.&lt;/p&gt;
&lt;p&gt;셋째 줄에는 최빈값을 출력한다. 여러 개 있을 때에는 최빈값 중 두 번째로 작은 값을 출력한다.&lt;/p&gt;
&lt;p&gt;넷째 줄에는 범위를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 &lt;strong&gt;정렬로도 풀 수 있고&lt;/strong&gt;, &lt;strong&gt;빈도 배열로도 풀 수 있습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;먼저 정렬을 사용하는 방법을 생각해 보면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;중앙값은 정렬된 배열의 가운데 값&lt;/li&gt;
&lt;li&gt;범위는 최댓값 - 최솟값&lt;/li&gt;
&lt;li&gt;최빈값은 정렬된 배열을 순회하며 같은 값이 몇 번 나오는지 세어서 구할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 구현 자체는 직관적입니다.&lt;br /&gt;
하지만 입력 크기가 최대 500,000이므로 정렬을 사용하면 시간복잡도는&lt;/p&gt;
&lt;p&gt;$$
O(N \log N)
$$&lt;/p&gt;
&lt;p&gt;이 됩니다.&lt;/p&gt;
&lt;p&gt;그런데 이 문제는 중요한 특징이 하나 있습니다.&lt;br /&gt;
입력되는 정수의 범위가 &lt;code&gt;-4000 ~ 4000&lt;/code&gt;으로 매우 작습니다.&lt;/p&gt;
&lt;p&gt;즉, 가능한 값은 총&lt;/p&gt;
&lt;p&gt;$$
4000 - (-4000) + 1 = 8001
$$&lt;/p&gt;
&lt;p&gt;개뿐입니다.&lt;/p&gt;
&lt;p&gt;이 경우 정렬 대신 &lt;strong&gt;빈도 배열&lt;/strong&gt;을 사용하는 것이 더 효율적입니다.&lt;/p&gt;
&lt;p&gt;배열 &lt;code&gt;freq[8001]&lt;/code&gt;을 만들고, 값 &lt;code&gt;x&lt;/code&gt;를 &lt;code&gt;x + 4000&lt;/code&gt; 위치에 저장하면 각 숫자가 몇 번 등장했는지 바로 셀 수 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-4000&lt;/code&gt; → &lt;code&gt;freq[0]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; → &lt;code&gt;freq[4000]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4000&lt;/code&gt; → &lt;code&gt;freq[8000]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;처럼 대응됩니다.&lt;/p&gt;
&lt;p&gt;이 방법을 사용하면 다음과 같이 구할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;1. 산술평균&lt;/h3&gt;
&lt;p&gt;입력받으면서 합을 구한 뒤, 마지막에 &lt;code&gt;N&lt;/code&gt;으로 나누고 반올림하면 됩니다.&lt;/p&gt;
&lt;h3&gt;2. 중앙값&lt;/h3&gt;
&lt;p&gt;빈도 배열을 앞에서부터 누적하면서 개수를 더해 갑니다.&lt;br /&gt;
누적 개수가 처음으로 &lt;code&gt;N / 2&lt;/code&gt;를 넘는 위치가 중앙값입니다.&lt;/p&gt;
&lt;h3&gt;3. 최빈값&lt;/h3&gt;
&lt;p&gt;먼저 가장 큰 빈도 &lt;code&gt;max_freq&lt;/code&gt;를 찾습니다.&lt;br /&gt;
그다음 다시 앞에서부터 탐색하면서 &lt;code&gt;max_freq&lt;/code&gt;와 같은 값을 찾습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫 번째 값은 일단 저장&lt;/li&gt;
&lt;li&gt;두 번째 값이 나오면 그 값을 정답으로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 하면 최빈값이 여러 개일 때 &lt;strong&gt;두 번째로 작은 값&lt;/strong&gt;을 구할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4. 범위&lt;/h3&gt;
&lt;p&gt;입력받는 동안 최솟값과 최댓값을 갱신해 두고, 마지막에 &lt;code&gt;max - min&lt;/code&gt;을 출력하면 됩니다.&lt;/p&gt;
&lt;p&gt;정리하면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;정렬 풀이: &lt;code&gt;O(N \log N)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;빈도 배열 풀이: &lt;code&gt;O(N + 8001)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이고, &lt;code&gt;8001&lt;/code&gt;은 고정된 상수이므로 전체 시간복잡도는&lt;/p&gt;
&lt;p&gt;$$
O(N)
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;p&gt;따라서 이 문제는 &lt;strong&gt;정렬보다 빈도 배열을 사용하는 풀이가 더 적합합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;math.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    scanf(&quot;%d&quot;, &amp;amp;n);

    int freq[8001] = {0};
    int sum = 0;
    int min = 4001;
    int max = -4001;

    for (int i = 0; i &amp;lt; n; i++) {
        int x;
        scanf(&quot;%d&quot;, &amp;amp;x);

        freq[x + 4000]++;
        sum += x;

        if (x &amp;lt; min)
            min = x;
        if (x &amp;gt; max)
            max = x;
    }

    // 1. 산술평균
    int avg = (int)round((double)sum / n);

    // 2. 중앙값
    int count = 0;
    int mid = 0;
    for (int i = 0; i &amp;lt; 8001; i++) {
        count += freq[i];
        if (count &amp;gt; n / 2) {
            mid = i - 4000;
            break;
        }
    }

    // 3. 최빈값
    int max_freq = 0;
    for (int i = 0; i &amp;lt; 8001; i++) {
        if (freq[i] &amp;gt; max_freq) {
            max_freq = freq[i];
        }
    }

    int mod = 0;
    int found = 0;
    for (int i = 0; i &amp;lt; 8001; i++) {
        if (freq[i] == max_freq) {
            mod = i - 4000;
            found++;
            if (found == 2) {
                break;
            }
        }
    }

    // 4. 범위
    int range = max - min;

    printf(&quot;%d\n%d\n%d\n%d\n&quot;, avg, mid, mod, range);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1966번: 프린터 큐</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1966/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1966/</guid><description>백준 1966번 프린터 큐 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 22:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1966&quot;&gt;https://www.acmicpc.net/problem/1966&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;여러분도 알다시피 여러분의 프린터 기기는 여러분이 인쇄하고자 하는 문서를 인쇄 명령을 받은 ‘순서대로’, 즉 먼저 요청된 것을 먼저 인쇄한다. 여러 개의 문서가 쌓인다면 Queue 자료구조에 쌓여서 FIFO - First In First Out - 에 따라 인쇄가 되게 된다. 하지만 상근이는 새로운 프린터기 내부 소프트웨어를 개발하였는데, 이 프린터기는 다음과 같은 조건에 따라 인쇄를 하게 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;현재 Queue의 가장 앞에 있는 문서의 ‘중요도’를 확인한다.&lt;/li&gt;
&lt;li&gt;나머지 문서들 중 현재 문서보다 중요도가 높은 문서가 하나라도 있다면, 이 문서를 인쇄하지 않고 Queue의 가장 뒤에 재배치 한다. 그렇지 않다면 바로 인쇄를 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예를 들어 Queue에 4개의 문서(A B C D)가 있고, 중요도가 2 1 4 3 라면 C를 인쇄하고, 다음으로 D를 인쇄하고 A, B를 인쇄하게 된다.&lt;/p&gt;
&lt;p&gt;여러분이 할 일은, 현재 Queue에 있는 문서의 수와 중요도가 주어졌을 때, 어떤 한 문서가 몇 번째로 인쇄되는지 알아내는 것이다. 예를 들어 위의 예에서 C문서는 1번째로, A문서는 3번째로 인쇄되게 된다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 줄에 테스트케이스의 수가 주어진다. 각 테스트케이스는 두 줄로 이루어져 있다.&lt;/p&gt;
&lt;p&gt;테스트케이스의 첫 번째 줄에는 문서의 개수 N(1 ≤ N ≤ 100)과, 몇 번째로 인쇄되었는지 궁금한 문서가 현재 Queue에서 몇 번째에 놓여 있는지를 나타내는 정수 M(0 ≤ M &amp;lt; N)이 주어진다. 이때 맨 왼쪽은 0번째라고 하자. 두 번째 줄에는 N개 문서의 중요도가 차례대로 주어진다. 중요도는 1 이상 9 이하의 정수이고, 중요도가 같은 문서가 여러 개 있을 수도 있다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;각 테스트 케이스에 대해 문서가 몇 번째로 인쇄되는지 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 프린터의 동작을 그대로 큐로 시뮬레이션하면 됩니다.&lt;/p&gt;
&lt;p&gt;각 문서에 대해&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;중요도 &lt;code&gt;priority&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;내가 찾는 문서인지 여부 &lt;code&gt;target&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;를 함께 저장합니다.&lt;/p&gt;
&lt;p&gt;큐의 맨 앞 문서를 하나 꺼낸 뒤, 나머지 문서들 중 현재 문서보다 중요도가 높은 문서가 있는지 확인합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;더 높은 중요도가 있으면 현재 문서를 큐의 뒤로 보냅니다.&lt;/li&gt;
&lt;li&gt;더 높은 중요도가 없으면 현재 문서를 인쇄합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;인쇄할 때마다 순서를 1 증가시키고, 그 문서가 내가 찾던 문서라면 그 순서를 출력하면 됩니다.&lt;/p&gt;
&lt;p&gt;문서 개수 N이 최대 100이므로, 매번 큐 전체를 확인하는 방식으로도 충분히 빠릅니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

#define MAX 100

typedef struct {
    int priority;
    bool target;
} Task;

int main() {
    int t;
    scanf(&quot;%d&quot;, &amp;amp;t);

    while (t--) {
        int n, m;
        Task queue[MAX];
        int front = 0, rear = 0;
        int count = 0;
        int order = 0;

        scanf(&quot;%d %d&quot;, &amp;amp;n, &amp;amp;m);

        for (int i = 0; i &amp;lt; n; i++) {
            int priority;
            scanf(&quot;%d&quot;, &amp;amp;priority);

            queue[rear].priority = priority;
            queue[rear].target = (i == m);
            rear = (rear + 1) % MAX;
            count++;
        }

        while (count &amp;gt; 0) {
            Task current = queue[front];
            front = (front + 1) % MAX;
            count--;

            bool has_higher = false;
            int idx = front;

            for (int i = 0; i &amp;lt; count; i++) {
                if (queue[idx].priority &amp;gt; current.priority) {
                    has_higher = true;
                    break;
                }
                idx = (idx + 1) % MAX;
            }

            if (has_higher) {
                queue[rear] = current;
                rear = (rear + 1) % MAX;
                count++;
            } else {
                order++;
                if (current.target) {
                    printf(&quot;%d\n&quot;, order);
                    break;
                }
            }
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1676번: 팩토리얼 0의 개수</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1676/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1676/</guid><description>백준 1676번 팩토리얼 0의 개수 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 21:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1676&quot;&gt;https://www.acmicpc.net/problem/1676&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;N!에서 뒤에서부터 처음 0이 아닌 숫자가 나올 때까지 0의 개수를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N이 주어진다. (0 ≤ N ≤ 500)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 구한 0의 개수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;팩토리얼의 뒤에 붙는 0은 &lt;code&gt;10 = 2 × 5&lt;/code&gt; 가 만들어질 때마다 하나씩 생깁니다.&lt;/p&gt;
&lt;p&gt;팩토리얼에서는 2의 개수는 충분히 많고, 5의 개수가 부족하므로 &lt;strong&gt;5가 몇 개 들어 있는지&lt;/strong&gt;만 세면 됩니다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;5&lt;/code&gt;는 5가 1개&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10&lt;/code&gt;도 $2 * 5$ 5가 1개&lt;/li&gt;
&lt;li&gt;&lt;code&gt;15&lt;/code&gt;도 $3 * 5$ 5가 1개&lt;/li&gt;
&lt;li&gt;&lt;code&gt;25&lt;/code&gt;는 $5 * 5$ 이므로 5를 2개 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 답은&lt;/p&gt;
&lt;p&gt;$$
\left\lfloor \frac{N}{5} \right\rfloor
+
\left\lfloor \frac{N}{25} \right\rfloor
+
\left\lfloor \frac{N}{125} \right\rfloor
+\cdots
$$&lt;/p&gt;
&lt;p&gt;를 모두 더한 값입니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int count = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    while (n &amp;gt;= 5) {
        count += n / 5;
        n /= 5;
    }

    printf(&quot;%d\n&quot;, count);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2475번: 검증수</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2475/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2475/</guid><description>백준 2475번 검증수 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 21:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2475&quot;&gt;https://www.acmicpc.net/problem/2475&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;컴퓨터를 제조하는 회사인 KOI 전자에서는 제조하는 컴퓨터마다 6자리의 고유번호를 매긴다. 고유번호의 처음 5자리에는 00000부터 99999까지의 수 중 하나가 주어지며 6번째 자리에는 검증수가 들어간다. 검증수는 고유번호의 처음 5자리에 들어가는 5개의 숫자를 각각 제곱한 수의 합을 10으로 나눈 나머지이다.&lt;/p&gt;
&lt;p&gt;예를 들어 고유번호의 처음 5자리의 숫자들이 04256이면, 각 숫자를 제곱한 수들의 합 0+16+4+25+36 = 81 을 10으로 나눈 나머지인 1이 검증수이다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 고유번호의 처음 5자리의 숫자들이 빈칸을 사이에 두고 하나씩 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 검증수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;입력받은 5개의 수를 각각 제곱한 뒤 모두 더하고, 마지막에 10으로 나눈 나머지를 구하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int a, b, c, d, e, key;
    scanf(&quot;%d %d %d %d %d&quot;, &amp;amp;a, &amp;amp;b, &amp;amp;c, &amp;amp;d, &amp;amp;e);

    key = ((a * a) + (b * b) + (c * c) + (d * d) + (e * e)) % 10;

    printf(&quot;%d\n&quot;, key);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2439번: 별 찍기 - 2</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2439/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2439/</guid><description>백준 2439번 별 찍기 - 2 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 21:10:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2439&quot;&gt;https://www.acmicpc.net/problem/2439&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;첫째 줄에는 별 1개, 둘째 줄에는 별 2개, N번째 줄에는 별 N개를 찍는 문제&lt;/p&gt;
&lt;p&gt;하지만, 오른쪽을 기준으로 정렬한 별(예제 참고)을 출력하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N(1 ≤ N ≤ 100)이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄부터 N번째 줄까지 차례대로 별을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이중 반복문으로 해결할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;먼저 공백을 &lt;code&gt;N - i&lt;/code&gt;개 출력하고&lt;/li&gt;
&lt;li&gt;그다음 별을 &lt;code&gt;i&lt;/code&gt;개 출력하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 1; i &amp;lt;= n; i++) {
        for (int j = 1; j &amp;lt;= n - i; j++) {
            printf(&quot; &quot;);
        }
        for (int k = 1; k &amp;lt;= i; k++) {
            printf(&quot;*&quot;);
        }
        printf(&quot;\n&quot;);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 2438번: 별 찍기 - 1</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/2438/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/2438/</guid><description>백준 2438번 별 찍기 - 1 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 21:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2438&quot;&gt;https://www.acmicpc.net/problem/2438&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;첫째 줄에는 별 1개, 둘째 줄에는 별 2개, N번째 줄에는 별 N개를 찍는 문제&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N(1 ≤ N ≤ 100)이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄부터 N번째 줄까지 차례대로 별을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;이중 반복문으로 해결할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 1; i &amp;lt;= n; i++) {
        for (int j = 1; j &amp;lt;= i; j++) {
            printf(&quot;*&quot;);
        }
        printf(&quot;\n&quot;);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1978번: 소수 찾기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1978/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1978/</guid><description>백준 1978번 소수 찾기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:50:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1978&quot;&gt;https://www.acmicpc.net/problem/1978&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;주어진 수 N개 중에서 소수가 몇 개인지 찾아서 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 줄에 수의 개수 N이 주어진다. N은 100이하이다. 다음으로 N개의 수가 주어지는데 수는 1,000 이하의 자연수이다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;주어진 수들 중 소수의 개수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;입력받은 수들을 하나씩 확인하면서 소수인지 판별하면 됩니다.&lt;/p&gt;
&lt;p&gt;소수는 1보다 크고, 1과 자기 자신만을 약수로 가지는 수입니다.&lt;br /&gt;
따라서 어떤 수 &lt;code&gt;num&lt;/code&gt;에 대해 &lt;code&gt;2&lt;/code&gt;부터 &lt;code&gt;num - 1&lt;/code&gt;까지 나누어 보면서 나누어떨어지는 값이 하나라도 있으면 소수가 아닙니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;은 소수가 아님&lt;/li&gt;
&lt;li&gt;소수이면 개수를 1 증가&lt;/li&gt;
&lt;li&gt;모든 수를 확인한 뒤 소수의 개수를 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int is_prime(int num) {
    if (num == 1) {
        return 0;
    }

    for (int i = 2; i &amp;lt; num; i++) {
        if (num % i == 0) {
            return 0;
        }
    }

    return 1;
}

int main() {
    int n;
    int count = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        int num;
        scanf(&quot;%d&quot;, &amp;amp;num);

        if (is_prime(num)) {
            count++;
        }
    }

    printf(&quot;%d\n&quot;, count);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1929번: 소수 구하기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1929/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1929/</guid><description>백준 1929번 소수 구하기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:45:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1929&quot;&gt;https://www.acmicpc.net/problem/1929&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;M이상 N이하의 소수를 모두 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 자연수 M과 N이 빈 칸을 사이에 두고 주어진다. (1 ≤ M ≤ N ≤ 1,000,000) M이상 N이하의 소수가 하나 이상 있는 입력만 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;한 줄에 하나씩, 증가하는 순서대로 소수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;에라토스테네스의 체&lt;/strong&gt;를 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;is_not_prime[i] == 1&lt;/code&gt; 이면 소수가 아님&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_not_prime[i] == 0&lt;/code&gt; 이면 소수&lt;/li&gt;
&lt;li&gt;2부터 시작해 각 수의 배수를 지워 나갑니다&lt;/li&gt;
&lt;li&gt;마지막에 M부터 N까지 확인하며 소수만 출력하면 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이미 더 작은 수의 배수로 지워진 값은 다시 처리할 필요가 없으므로, 현재 값이 아직 소수일 때만 배수를 지우면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int M, N;
    static int is_not_prime[1000001] = {0};

    scanf(&quot;%d %d&quot;, &amp;amp;M, &amp;amp;N);

    is_not_prime[0] = 1;
    is_not_prime[1] = 1;

    for (int i = 2; i * i &amp;lt;= N; i++) {
        if (!is_not_prime[i]) {
            for (int j = i * i; j &amp;lt;= N; j += i) {
                is_not_prime[j] = 1;
            }
        }
    }

    for (int i = M; i &amp;lt;= N; i++) {
        if (!is_not_prime[i]) {
            printf(&quot;%d\n&quot;, i);
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1920번: 수 찾기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1920/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1920/</guid><description>백준 1920번 수 찾기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:40:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1920&quot;&gt;https://www.acmicpc.net/problem/1920&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;N개의 정수 A[1], A[2], …, A[N]이 주어져 있을 때, 이 안에 X라는 정수가 존재하는지 알아내는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 자연수 N(1 ≤ N ≤ 100,000)이 주어진다. 다음 줄에는 N개의 정수 A[1], A[2], …, A[N]이 주어진다. 다음 줄에는 M(1 ≤ M ≤ 100,000)이 주어진다. 다음 줄에는 M개의 수들이 주어지는데, 이 수들이 A안에 존재하는지 알아내면 된다. 모든 정수의 범위는 $-2^{31}$ 보다 크거나 같고 $2^{31}$ 보다 작다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;M개의 줄에 답을 출력한다. 존재하면 1을, 존재하지 않으면 0을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;배열에서 어떤 수가 존재하는지 빠르게 확인하려면 먼저 정렬해야 합니다.&lt;/p&gt;
&lt;p&gt;그다음 각 질의값마다 이분 탐색을 수행하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배열을 정렬한다&lt;/li&gt;
&lt;li&gt;찾고 싶은 값을 이분 탐색한다&lt;/li&gt;
&lt;li&gt;찾으면 &lt;code&gt;1&lt;/code&gt;, 없으면 &lt;code&gt;0&lt;/code&gt; 출력한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;입력 크기가 크기 때문에, 단순 선형 탐색으로 매번 찾으면 시간이 오래 걸립니다.&lt;br /&gt;
정렬 후 이분 탐색을 사용하면 각 질의마다 빠르게 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int a[100000];

int compare(const void *x, const void *y) {
    int n1 = *(const int *)x;
    int n2 = *(const int *)y;

    if (n1 &amp;lt; n2) return -1;
    if (n1 &amp;gt; n2) return 1;
    return 0;
}

int binary_search(int key, int size) {
    int low = 0;
    int high = size - 1;

    while (low &amp;lt;= high) {
        int mid = (low + high) / 2;

        if (a[mid] == key) {
            return 1;
        } else if (key &amp;lt; a[mid]) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    return 0;
}

int main() {
    int n, m;

    scanf(&quot;%d&quot;, &amp;amp;n);
    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%d&quot;, &amp;amp;a[i]);
    }

    qsort(a, n, sizeof(int), compare);

    scanf(&quot;%d&quot;, &amp;amp;m);
    for (int i = 0; i &amp;lt; m; i++) {
        int x;
        scanf(&quot;%d&quot;, &amp;amp;x);
        printf(&quot;%d\n&quot;, binary_search(x, n));
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1874번: 스택 수열</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1874/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1874/</guid><description>백준 1874번 스택 수열 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:35:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1874&quot;&gt;https://www.acmicpc.net/problem/1874&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;스택 (stack)은 기본적인 자료구조 중 하나로, 컴퓨터 프로그램을 작성할 때 자주 이용되는 개념이다. 스택은 자료를 넣는 (push) 입구와 자료를 뽑는 (pop) 입구가 같아 제일 나중에 들어간 자료가 제일 먼저 나오는 (LIFO, Last in First out) 특성을 가지고 있다.&lt;/p&gt;
&lt;p&gt;1부터 n까지의 수를 스택에 넣었다가 뽑아 늘어놓음으로써, 하나의 수열을 만들 수 있다. 이때, 스택에 push하는 순서는 반드시 오름차순을 지키도록 한다고 하자. 임의의 수열이 주어졌을 때 스택을 이용해 그 수열을 만들 수 있는지 없는지, 있다면 어떤 순서로 push와 pop 연산을 수행해야 하는지를 알아낼 수 있다. 이를 계산하는 프로그램을 작성하라.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 줄에 n (1 ≤ n ≤ 100,000)이 주어진다. 둘째 줄부터 n개의 줄에는 수열을 이루는 1이상 n이하의 정수가 하나씩 순서대로 주어진다. 물론 같은 정수가 두 번 나오는 일은 없다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;입력된 수열을 만들기 위해 필요한 연산을 한 줄에 한 개씩 출력한다. push연산은 +로, pop 연산은 -로 표현하도록 한다. 불가능한 경우 NO를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;스택에 &lt;code&gt;1&lt;/code&gt;부터 차례대로 넣으면서 목표 수열을 만들 수 있는지 확인하면 됩니다.&lt;/p&gt;
&lt;p&gt;현재 만들어야 하는 값이 &lt;code&gt;value&lt;/code&gt;일 때&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt;가 지금까지 넣은 수보다 크면, &lt;code&gt;value&lt;/code&gt;까지 push 합니다.&lt;/li&gt;
&lt;li&gt;그 뒤 스택 top이 &lt;code&gt;value&lt;/code&gt;이면 pop 합니다.&lt;/li&gt;
&lt;li&gt;그런데 스택 top이 &lt;code&gt;value&lt;/code&gt;가 아니면, 원하는 수열을 만들 수 없으므로 &lt;code&gt;NO&lt;/code&gt;를 출력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 숫자는 오름차순으로만 push 할 수 있고, pop 순서가 목표 수열과 맞아야 합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int stack[100001];
    int top = -1;
    int start = 0;
    char result[400001];
    int idx = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        int value;
        scanf(&quot;%d&quot;, &amp;amp;value);

        if (value &amp;gt; start) {
            for (int j = start + 1; j &amp;lt;= value; j++) {
                stack[++top] = j;
                result[idx++] = &apos;+&apos;;
                result[idx++] = &apos;\n&apos;;
            }
            start = value;
        } else if (top == -1 || stack[top] != value) {
            printf(&quot;NO\n&quot;);
            return 0;
        }

        top--;
        result[idx++] = &apos;-&apos;;
        result[idx++] = &apos;\n&apos;;
    }

    result[idx] = &apos;\0&apos;;
    printf(&quot;%s&quot;, result);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1654번: 랜선 자르기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1654/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1654/</guid><description>백준 1654번 랜선 자르기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1654&quot;&gt;https://www.acmicpc.net/problem/1654&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;집에서 시간을 보내던 오영식은 박성원의 부름을 받고 급히 달려왔다. 박성원이 캠프 때 쓸 N개의 랜선을 만들어야 하는데 너무 바빠서 영식이에게 도움을 청했다.&lt;/p&gt;
&lt;p&gt;이미 오영식은 자체적으로 K개의 랜선을 가지고 있다. 그러나 K개의 랜선은 길이가 제각각이다. 박성원은 랜선을 모두 N개의 같은 길이의 랜선으로 만들고 싶었기 때문에 K개의 랜선을 잘라서 만들어야 한다. 예를 들어 300cm 짜리 랜선에서 140cm 짜리 랜선을 두 개 잘라내면 20cm는 버려야 한다. (이미 자른 랜선은 붙일 수 없다.)&lt;/p&gt;
&lt;p&gt;편의를 위해 랜선을 자르거나 만들 때 손실되는 길이는 없다고 가정하며, 기존의 K개의 랜선으로 N개의 랜선을 만들 수 없는 경우는 없다고 가정하자. 그리고 자를 때는 항상 센티미터 단위로 정수길이만큼 자른다고 가정하자. N개보다 많이 만드는 것도 N개를 만드는 것에 포함된다. 이때 만들 수 있는 최대 랜선의 길이를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에는 오영식이 이미 가지고 있는 랜선의 개수 K, 그리고 필요한 랜선의 개수 N이 입력된다. K는 1이상 10,000이하의 정수이고, N은 1이상 1,000,000이하의 정수이다. 그리고 항상 K ≦ N 이다. 그 후 K줄에 걸쳐 이미 가지고 있는 각 랜선의 길이가 센티미터 단위의 정수로 입력된다. 랜선의 길이는 $2^{31}-1$ 보다 작거나 같은 자연수이다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N개를 만들 수 있는 랜선의 최대 길이를 센티미터 단위의 정수로 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;가능한 랜선 길이의 범위에서 &lt;strong&gt;이분 탐색&lt;/strong&gt;을 하면 됩니다.&lt;/p&gt;
&lt;p&gt;길이를 &lt;code&gt;mid&lt;/code&gt;라고 정했을 때, 각 랜선을 &lt;code&gt;mid&lt;/code&gt; 길이로 몇 개 만들 수 있는지 모두 더하면 만들 수 있는 랜선 개수를 구할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count &amp;lt; n&lt;/code&gt; 이면 길이가 너무 김&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count &amp;gt;= n&lt;/code&gt; 이면 가능한 길이이므로 더 길게 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 과정을 반복하면 만들 수 있는 최대 길이를 찾을 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int k, n;
    long long arr[10000];
    long long max = 0, min = 0, mid;
    
    scanf(&quot;%d %d&quot;, &amp;amp;k, &amp;amp;n);

    for (int i = 0; i &amp;lt; k; i++) {
        scanf(&quot;%lld&quot;, &amp;amp;arr[i]);
        if (arr[i] &amp;gt; max) {
            max = arr[i];
        }
    }

    max++;

    while (min &amp;lt; max) {
        mid = (min + max) / 2;
        long long count = 0;

        for (int i = 0; i &amp;lt; k; i++) {
            count += arr[i] / mid;
        }

        if (count &amp;lt; n) {
            max = mid;
        } else {
            min = mid + 1;
        }
    }

    printf(&quot;%lld\n&quot;, min - 1);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1546번: 평균</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1546/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1546/</guid><description>백준 1546번 평균 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:25:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1546&quot;&gt;https://www.acmicpc.net/problem/1546&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;세준이는 기말고사를 망쳤다. 세준이는 점수를 조작해서 집에 가져가기로 했다. 일단 세준이는 자기 점수 중에 최댓값을 골랐다. 이 값을 M이라고 한다. 그리고 나서 모든 점수를 점수/M*100으로 고쳤다.&lt;/p&gt;
&lt;p&gt;예를 들어, 세준이의 최고점이 70이고, 수학점수가 50이었으면 수학점수는 50/70*100이 되어 71.43점이 된다.&lt;/p&gt;
&lt;p&gt;세준이의 성적을 위의 방법대로 새로 계산했을 때, 새로운 평균을 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 시험 본 과목의 개수 N이 주어진다. 이 값은 1000보다 작거나 같다. 둘째 줄에 세준이의 현재 성적이 주어진다. 이 값은 100보다 작거나 같은 음이 아닌 정수이고, 적어도 하나의 값은 0보다 크다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 새로운 평균을 출력한다. 실제 정답과 출력값의 절대오차 또는 상대오차가 $10^{-2}$ 이하이면 정답이다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;점수들을 모두 입력받으면서 최댓값 &lt;code&gt;M&lt;/code&gt;을 찾습니다.&lt;/p&gt;
&lt;p&gt;그다음 각 점수를&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;score / M * 100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;형태로 변환한 뒤 모두 더하고, 마지막에 과목 수 &lt;code&gt;N&lt;/code&gt;으로 나누면 새로운 평균을 구할 수 있습니다.&lt;/p&gt;
&lt;p&gt;나눗셈에서 정수 나눗셈이 되지 않도록 &lt;code&gt;double&lt;/code&gt; 형변환을 해주는 것이 중요합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int n;
    int score[1000];
    int max = 0;
    double sum = 0.0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%d&quot;, &amp;amp;score[i]);
        if (score[i] &amp;gt; max) {
            max = score[i];
        }
    }

    for (int i = 0; i &amp;lt; n; i++) {
        sum += (score[i] / (double)max) * 100.0;
    }

    printf(&quot;%f\n&quot;, sum / n);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1436번: 영화감독 숌</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1436/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1436/</guid><description>백준 1436번 영화감독 숌 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1436&quot;&gt;https://www.acmicpc.net/problem/1436&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;666은 종말을 나타내는 수라고 한다. 따라서, 많은 블록버스터 영화에서는 666이 들어간 제목을 많이 사용한다. 영화감독 숌은 세상의 종말 이라는 시리즈 영화의 감독이다. 조지 루카스는 스타워즈를 만들 때, 스타워즈 1, 스타워즈 2, 스타워즈 3, 스타워즈 4, 스타워즈 5, 스타워즈 6과 같이 이름을 지었고, 피터 잭슨은 반지의 제왕을 만들 때, 반지의 제왕 1, 반지의 제왕 2, 반지의 제왕 3과 같이 영화 제목을 지었다. 하지만 숌은 자신이 조지 루카스와 피터 잭슨을 뛰어넘는다는 것을 보여주기 위해서 영화 제목을 좀 다르게 만들기로 했다.&lt;/p&gt;
&lt;p&gt;종말의 수란 어떤 수에 6이 적어도 3개 이상 연속으로 들어가는 수를 말한다. 제일 작은 종말의 수는 666이고, 그 다음으로 큰 수는 1666, 2666, 3666, .... 이다. 따라서, 숌은 첫 번째 영화의 제목은 &quot;세상의 종말 666&quot;, 두 번째 영화의 제목은 &quot;세상의 종말 1666&quot;와 같이 이름을 지을 것이다. 일반화해서 생각하면, N번째 영화의 제목은 세상의 종말 (N번째로 작은 종말의 수) 와 같다.&lt;/p&gt;
&lt;p&gt;숌이 만든 N번째 영화의 제목에 들어간 수를 출력하는 프로그램을 작성하시오. 숌은 이 시리즈를 항상 차례대로 만들고, 다른 영화는 만들지 않는다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N이 주어진다. N은 10,000보다 작거나 같은 자연수이다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N번째 영화의 제목에 들어간 수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;가장 작은 종말의 수는 &lt;code&gt;666&lt;/code&gt;입니다.&lt;br /&gt;
이후 숫자를 1씩 증가시키면서, 해당 숫자에 문자열 &lt;code&gt;666&lt;/code&gt;이 포함되어 있는지 확인하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재 숫자를 1 증가&lt;/li&gt;
&lt;li&gt;문자열로 바꿔서 &lt;code&gt;666&lt;/code&gt;이 들어 있는지 검사&lt;/li&gt;
&lt;li&gt;포함되어 있으면 개수를 1 증가&lt;/li&gt;
&lt;li&gt;N번째가 되면 그 숫자를 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;브루트포스 방식이지만, 입력 범위가 크지 않아서 충분히 빠르게 해결할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main() {
    int n;
    int num = 666;
    int count = 1;
    char str[20];

    scanf(&quot;%d&quot;, &amp;amp;n);

    while (count != n) {
        num++;
        sprintf(str, &quot;%d&quot;, num);

        if (strstr(str, &quot;666&quot;) != NULL) {
            count++;
        }
    }

    printf(&quot;%d\n&quot;, num);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1330번: 두 수 비교하기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1330/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1330/</guid><description>백준 1330번 두 수 비교하기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:15:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1330&quot;&gt;https://www.acmicpc.net/problem/1330&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;두 정수 A와 B가 주어졌을 때, A와 B를 비교하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A와 B가 주어진다. A와 B는 공백 한 칸으로 구분되어져 있다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 다음 세 가지 중 하나를 출력한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A가 B보다 큰 경우에는 &apos;&amp;gt;&apos;를 출력한다.&lt;/li&gt;
&lt;li&gt;A가 B보다 작은 경우에는 &apos;&amp;lt;&apos;를 출력한다.&lt;/li&gt;
&lt;li&gt;A와 B가 같은 경우에는 &apos;==&apos;를 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;제한&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;-10,000 ≤ A, B ≤ 10,000&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;두 정수를 입력받아 대소 관계를 비교한 뒤, 조건에 맞는 기호를 출력하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int a, b;

    scanf(&quot;%d %d&quot;, &amp;amp;a, &amp;amp;b);

    if (a &amp;gt; b) {
        printf(&quot;&amp;gt;\n&quot;);
    } else if (a &amp;lt; b) {
        printf(&quot;&amp;lt;\n&quot;);
    } else {
        printf(&quot;==\n&quot;);
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1316번: 그룹 단어 체커</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1316/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1316/</guid><description>백준 1316번 그룹 단어 체커 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:10:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1316&quot;&gt;https://www.acmicpc.net/problem/1316&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;그룹 단어란 단어에 존재하는 모든 문자에 대해서, 각 문자가 연속해서 나타나는 경우만을 말한다. 예를 들면, ccazzzzbb는 c, a, z, b가 모두 연속해서 나타나고, kin도 k, i, n이 연속해서 나타나기 때문에 그룹 단어이지만, aabbbccb는 b가 떨어져서 나타나기 때문에 그룹 단어가 아니다.&lt;/p&gt;
&lt;p&gt;단어 N개를 입력으로 받아 그룹 단어의 개수를 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 단어의 개수 N이 들어온다. N은 100보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에 단어가 들어온다. 단어는 알파벳 소문자로만 되어있고 중복되지 않으며, 길이는 최대 100이다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 그룹 단어의 개수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;문자를 앞에서부터 확인하면서, 현재 문자가 이전 문자와 다를 때만 앞부분을 검사하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현재 문자와 이전 문자가 같으면 연속된 문자이므로 통과&lt;/li&gt;
&lt;li&gt;현재 문자와 이전 문자가 다른데, 앞에서 같은 문자가 이미 나온 적이 있으면 그룹 단어가 아님&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 과정을 모든 단어에 대해 반복하고, 조건을 만족하는 단어 개수를 세면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main() {
    int n;
    char word[101];
    int count = 0;

    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%s&quot;, word);

        int len = strlen(word);
        int is_group = 1;

        for (int j = 1; j &amp;lt; len; j++) {
            if (word[j] != word[j - 1]) {
                for (int k = 0; k &amp;lt; j; k++) {
                    if (word[j] == word[k]) {
                        is_group = 0;
                        break;
                    }
                }
            }

            if (!is_group) {
                break;
            }
        }

        if (is_group) {
            count++;
        }
    }

    printf(&quot;%d\n&quot;, count);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1271번: 엄청난 부자2</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1271/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1271/</guid><description>백준 1271번 엄청난 부자2 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:05:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1271&quot;&gt;https://www.acmicpc.net/problem/1271&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;갑부 최백준 조교는 동전을 최소로 바꾸는데 성공했으나 김재홍 조교가 그 돈을 발견해서 최백준 조교에게 그 돈을 나누자고 따진다.&lt;/p&gt;
&lt;p&gt;그 사실이 전 우주로 알려지자 우주에 있던 많은 생명체들이 자신들에게 돈을 분배해 달라고 당장 달려오기 시작했다.&lt;/p&gt;
&lt;p&gt;프로토스 중앙 우주 정부의 정책인, ‘모든 지적 생명체는 동등하다’라는 규칙에 입각해서 돈을 똑같이 분배하고자 한다.&lt;/p&gt;
&lt;p&gt;한 생명체에게 얼마씩 돈을 줄 수 있는가?&lt;/p&gt;
&lt;p&gt;또, 생명체들에게 동일하게 분배한 후 남는 돈은 얼마인가?&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에는 최백준 조교가 가진 돈 n과 돈을 받으러 온 생명체의 수 m이 주어진다. (1 ≤ m ≤ n ≤ 101000, m과 n은 10진수 정수)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 생명체 하나에게 돌아가는 돈의 양을 출력한다. 그리고 두 번째 줄에는 1원씩 분배할 수 없는 남는 돈을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;입력 범위가 매우 커서 일반 정수형으로 처리할 수 없습니다.&lt;/p&gt;
&lt;p&gt;C에는 자바의 &lt;code&gt;BigInteger&lt;/code&gt; 같은 기능이 없으므로, 문자열로 큰 수를 입력받아 긴 나눗셈을 직접 구현해야 합니다.&lt;/p&gt;
&lt;p&gt;왼쪽부터 한 자리씩 내려오면서 현재 값을 만들고, 그 값에서 divisor를 뺄 수 있을 만큼 빼서 몫의 각 자릿수를 구하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

#define MAX 1001

void normalize(const char *src, char *dst) {
    int i = 0;
    while (src[i] == &apos;0&apos; &amp;amp;&amp;amp; src[i + 1] != &apos;\0&apos;) {
        i++;
    }
    strcpy(dst, src + i);
}

int compare_num(const char *a, const char *b) {
    int len_a = strlen(a);
    int len_b = strlen(b);

    if (len_a &amp;gt; len_b) return 1;
    if (len_a &amp;lt; len_b) return -1;

    int cmp = strcmp(a, b);
    if (cmp &amp;gt; 0) return 1;
    if (cmp &amp;lt; 0) return -1;
    return 0;
}

void subtract_num(const char *a, const char *b, char *result) {
    char temp[MAX];
    int len_a = strlen(a);
    int len_b = strlen(b);
    int i = len_a - 1;
    int j = len_b - 1;
    int k = 0;
    int borrow = 0;

    while (i &amp;gt;= 0 || j &amp;gt;= 0) {
        int da = (i &amp;gt;= 0 ? a[i] - &apos;0&apos; : 0) - borrow;
        int db = (j &amp;gt;= 0 ? b[j] - &apos;0&apos; : 0);

        if (da &amp;lt; db) {
            da += 10;
            borrow = 1;
        } else {
            borrow = 0;
        }

        temp[k++] = (da - db) + &apos;0&apos;;
        i--;
        j--;
    }

    while (k &amp;gt; 1 &amp;amp;&amp;amp; temp[k - 1] == &apos;0&apos;) {
        k--;
    }

    for (int t = 0; t &amp;lt; k; t++) {
        result[t] = temp[k - 1 - t];
    }
    result[k] = &apos;\0&apos;;
}

void divide_bigint(const char *dividend, const char *divisor, char *quotient, char *remainder) {
    char rem[MAX] = &quot;0&quot;;
    char temp[MAX];
    char q[MAX];
    int qlen = 0;

    for (int i = 0; dividend[i] != &apos;\0&apos;; i++) {
        int len_rem = strlen(rem);

        if (strcmp(rem, &quot;0&quot;) == 0) {
            rem[0] = dividend[i];
            rem[1] = &apos;\0&apos;;
        } else {
            rem[len_rem] = dividend[i];
            rem[len_rem + 1] = &apos;\0&apos;;
        }

        normalize(rem, temp);
        strcpy(rem, temp);

        int digit = 0;
        while (compare_num(rem, divisor) &amp;gt;= 0) {
            subtract_num(rem, divisor, temp);
            strcpy(rem, temp);
            digit++;
        }

        q[qlen++] = digit + &apos;0&apos;;
    }

    q[qlen] = &apos;\0&apos;;
    normalize(q, quotient);
    strcpy(remainder, rem);
}

int main() {
    char n[MAX], m[MAX];
    char quotient[MAX], remainder[MAX];

    scanf(&quot;%s %s&quot;, n, m);

    divide_bigint(n, m, quotient, remainder);

    printf(&quot;%s\n&quot;, quotient);
    printf(&quot;%s\n&quot;, remainder);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1264번: 모음의 개수</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1264/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1264/</guid><description>백준 1264번 모음의 개수 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 20:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1264&quot;&gt;https://www.acmicpc.net/problem/1264&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;영문 문장을 입력받아 모음의 개수를 세는 프로그램을 작성하시오. 모음은 &apos;a&apos;, &apos;e&apos;, &apos;i&apos;, &apos;o&apos;, &apos;u&apos;이며 대문자 또는 소문자이다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;입력은 여러 개의 테스트 케이스로 이루어져 있으며, 각 줄마다 영어 대소문자, &apos;,&apos;, &apos;.&apos;, &apos;!&apos;, &apos;?&apos;, 공백으로 이루어진 문장이 주어진다. 각 줄은 최대 255글자로 이루어져 있다.&lt;/p&gt;
&lt;p&gt;입력의 끝에는 한 줄에 &apos;#&apos; 한 글자만이 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;각 줄마다 모음의 개수를 세서 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;한 줄씩 문자열로 입력받은 뒤, 각 문자를 확인하면서 모음이면 개수를 증가시키면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소문자 모음: &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;, &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt;, &lt;code&gt;u&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;대문자 모음: &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;E&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt;, &lt;code&gt;O&lt;/code&gt;, &lt;code&gt;U&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;입력이 &lt;code&gt;#&lt;/code&gt; 한 글자이면 종료합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main() {
    char s[257];

    while (1) {
        fgets(s, sizeof(s), stdin);

        if (strcmp(s, &quot;#\n&quot;) == 0 || strcmp(s, &quot;#&quot;) == 0) {
            break;
        }

        int count = 0;

        for (int i = 0; s[i] != &apos;\0&apos;; i++) {
            if (s[i] == &apos;a&apos; || s[i] == &apos;e&apos; || s[i] == &apos;i&apos; ||
                s[i] == &apos;o&apos; || s[i] == &apos;u&apos; || s[i] == &apos;A&apos; ||
                s[i] == &apos;E&apos; || s[i] == &apos;I&apos; || s[i] == &apos;O&apos; ||
                s[i] == &apos;U&apos;) {
                count++;
            }
        }

        printf(&quot;%d\n&quot;, count);
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1259번: 팰린드롬수</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1259/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1259/</guid><description>백준 1259번 팰린드롬수 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:50:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1259&quot;&gt;https://www.acmicpc.net/problem/1259&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;어떤 단어를 뒤에서부터 읽어도 똑같다면 그 단어를 팰린드롬이라고 한다. &apos;radar&apos;, &apos;sees&apos;는 팰린드롬이다.&lt;/p&gt;
&lt;p&gt;수도 팰린드롬으로 취급할 수 있다. 수의 숫자들을 뒤에서부터 읽어도 같다면 그 수는 팰린드롬수다. 121, 12421 등은 팰린드롬수다. 123, 1231은 뒤에서부터 읽으면 다르므로 팰린드롬수가 아니다. 또한 10도 팰린드롬수가 아닌데, 앞에 무의미한 0이 올 수 있다면 010이 되어 팰린드롬수로 취급할 수도 있지만, 특별히 이번 문제에서는 무의미한 0이 앞에 올 수 없다고 하자.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;입력은 여러 개의 테스트 케이스로 이루어져 있으며, 각 줄마다 1 이상 99999 이하의 정수가 주어진다. 입력의 마지막 줄에는 0이 주어지며, 이 줄은 문제에 포함되지 않는다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;각 줄마다 주어진 수가 팰린드롬수면 &apos;yes&apos;, 아니면 &apos;no&apos;를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;입력을 문자열로 받은 뒤, 양쪽 끝 문자를 서로 비교하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;맨 앞 문자와 맨 뒤 문자 비교&lt;/li&gt;
&lt;li&gt;그다음 앞 문자와 뒤 문자 비교&lt;/li&gt;
&lt;li&gt;하나라도 다르면 팰린드롬이 아님&lt;/li&gt;
&lt;li&gt;끝까지 모두 같으면 팰린드롬&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;입력이 &lt;code&gt;0&lt;/code&gt;이면 종료합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main() {
    char s[6];

    while (1) {
        scanf(&quot;%s&quot;, s);

        if (strcmp(s, &quot;0&quot;) == 0) {
            break;
        }

        int left = 0;
        int right = strlen(s) - 1;
        int is_palindrome = 1;

        while (left &amp;lt; right) {
            if (s[left] != s[right]) {
                is_palindrome = 0;
                break;
            }
            left++;
            right--;
        }

        if (is_palindrome) {
            printf(&quot;yes\n&quot;);
        } else {
            printf(&quot;no\n&quot;);
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1193번: 분수찾기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1193/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1193/</guid><description>백준 1193번 분수찾기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:45:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1193&quot;&gt;https://www.acmicpc.net/problem/1193&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;무한히 큰 배열에 다음과 같이 분수들이 적혀있다.&lt;/p&gt;
&lt;p&gt;&amp;lt;table border=&quot;1&quot;&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;1/1&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;1/2&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;1/3&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;1/4&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;1/5&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;2/1&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2/2&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2/3&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2/4&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;3/1&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;3/2&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;3/3&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;4/1&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;4/2&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;5/1&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;...&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;p&gt;이와 같이 나열된 분수들을 1/1 → 1/2 → 2/1 → 3/1 → 2/2 → … 과 같은 지그재그 순서로 차례대로 1번, 2번, 3번, 4번, 5번, … 분수라고 하자.&lt;/p&gt;
&lt;p&gt;X가 주어졌을 때, X번째 분수를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 X(1 ≤ X ≤ 10,000,000)가 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 분수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;분수들은 대각선 단위로 묶어서 생각할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1번째 대각선: 1개&lt;/li&gt;
&lt;li&gt;2번째 대각선: 2개&lt;/li&gt;
&lt;li&gt;3번째 대각선: 3개&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 현재 번호가 어느 대각선에 속하는지 찾기 위해 &lt;code&gt;1, 2, 3, ...&lt;/code&gt;을 차례대로 빼면 됩니다.&lt;/p&gt;
&lt;p&gt;예를 들어 X가 어떤 대각선에 들어왔을 때,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그 대각선 번호가 홀수이면 위에서 아래 방향&lt;/li&gt;
&lt;li&gt;짝수이면 아래에서 위 방향&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;으로 분자가 결정됩니다.&lt;/p&gt;
&lt;p&gt;따라서 대각선 번호의 홀짝에 따라 분자와 분모를 다르게 출력하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int x;
    int line = 1;

    scanf(&quot;%d&quot;, &amp;amp;x);

    while (x &amp;gt; line) {
        x -= line;
        line++;
    }

    if (line % 2 == 1) {
        printf(&quot;%d/%d\n&quot;, line + 1 - x, x);
    } else {
        printf(&quot;%d/%d\n&quot;, x, line + 1 - x);
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1181번: 단어 정렬</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1181/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1181/</guid><description>백준 1181번 단어 정렬 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:40:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1181&quot;&gt;https://www.acmicpc.net/problem/1181&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;알파벳 소문자로 이루어진 N개의 단어가 들어오면 아래와 같은 조건에 따라 정렬하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;길이가 짧은 것부터&lt;/li&gt;
&lt;li&gt;길이가 같으면 사전 순으로&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;단, 중복된 단어는 하나만 남기고 제거해야 한다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 단어의 개수 N이 주어진다. (1 ≤ N ≤ 20,000)&lt;br /&gt;
둘째 줄부터 N개의 줄에 걸쳐 알파벳 소문자로 이루어진 단어가 한 줄에 하나씩 주어진다.&lt;br /&gt;
주어지는 문자열의 길이는 50을 넘지 않는다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;조건에 따라 정렬하여 단어들을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;단어들을 모두 배열에 저장한 뒤 정렬하면 됩니다.&lt;/p&gt;
&lt;p&gt;정렬 기준은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;길이가 짧은 단어가 먼저&lt;/li&gt;
&lt;li&gt;길이가 같으면 사전 순으로 앞서는 단어가 먼저&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정렬이 끝난 뒤에는 같은 단어가 연속해서 나타나므로, 바로 앞 단어와 비교해서 다른 경우에만 출력하면 중복을 제거할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

char words[20000][51];

int compare(const void *a, const void *b) {
    const char *s1 = (const char *)a;
    const char *s2 = (const char *)b;

    int len1 = strlen(s1);
    int len2 = strlen(s2);

    if (len1 == len2) {
        return strcmp(s1, s2);
    }
    return len1 - len2;
}

int main() {
    int n;
    scanf(&quot;%d&quot;, &amp;amp;n);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%s&quot;, words[i]);
    }

    qsort(words, n, sizeof(words[0]), compare);

    printf(&quot;%s\n&quot;, words[0]);
    for (int i = 1; i &amp;lt; n; i++) {
        if (strcmp(words[i], words[i - 1]) != 0) {
            printf(&quot;%s\n&quot;, words[i]);
        }
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1157번: 단어 공부</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1157/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1157/</guid><description>백준 1157번 단어 공부 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:35:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1157&quot;&gt;https://www.acmicpc.net/problem/1157&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;알파벳 대소문자로 된 단어가 주어지면, 이 단어에서 가장 많이 사용된 알파벳이 무엇인지 알아내는 프로그램을 작성하시오. 단, 대문자와 소문자를 구분하지 않는다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 알파벳 대소문자로 이루어진 단어가 주어진다. 주어지는 단어의 길이는 1,000,000을 넘지 않는다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 이 단어에서 가장 많이 사용된 알파벳을 대문자로 출력한다. 단, 가장 많이 사용된 알파벳이 여러 개 존재하는 경우에는 ?를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;알파벳 26개의 개수를 배열에 저장한 뒤, 가장 큰 값을 찾으면 됩니다.&lt;br /&gt;
대소문자를 구분하지 않으므로 대문자와 소문자를 같은 인덱스에 누적합니다.&lt;br /&gt;
최댓값이 여러 개면 &lt;code&gt;?&lt;/code&gt;를 출력합니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    static char str[1000001];
    int count[26] = {0};
    int max = 0;
    char answer = &apos;?&apos;;

    scanf(&quot;%s&quot;, str);

    for (int i = 0; str[i] != &apos;\0&apos;; i++) {
        if (&apos;A&apos; &amp;lt;= str[i] &amp;amp;&amp;amp; str[i] &amp;lt;= &apos;Z&apos;) {
            count[str[i] - &apos;A&apos;]++;
        } else {
            count[str[i] - &apos;a&apos;]++;
        }
    }

    for (int i = 0; i &amp;lt; 26; i++) {
        if (count[i] &amp;gt; max) {
            max = count[i];
            answer = (char)(i + &apos;A&apos;);
        } else if (count[i] == max) {
            answer = &apos;?&apos;;
        }
    }

    printf(&quot;%c\n&quot;, answer);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1152번: 단어의 개수</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1152/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1152/</guid><description>백준 1152번 단어의 개수 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1152&quot;&gt;https://www.acmicpc.net/problem/1152&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;영어 대소문자와 공백으로 이루어진 문자열이 주어진다. 이 문자열에는 몇 개의 단어가 있을까? 이를 구하는 프로그램을 작성하시오. 단, 한 단어가 여러 번 등장하면 등장한 횟수만큼 모두 세어야 한다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫 줄에 영어 대소문자와 공백으로 이루어진 문자열이 주어진다. 이 문자열의 길이는 1,000,000을 넘지 않는다. 단어는 공백 한 개로 구분되며, 공백이 연속해서 나오는 경우는 없다. 또한 문자열은 공백으로 시작하거나 끝날 수 있다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 단어의 개수를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;문자열을 처음부터 끝까지 확인하면서, &lt;strong&gt;단어가 시작되는 위치&lt;/strong&gt;를 세면 됩니다.&lt;/p&gt;
&lt;p&gt;현재 문자가 공백이 아니고, 문자열의 처음이거나 바로 앞 문자가 공백이면 새로운 단어가 시작된 것입니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    static char str[1000002];
    int count = 0;

    fgets(str, sizeof(str), stdin);

    for (int i = 0; str[i] != &apos;\0&apos;; i++) {
        if (str[i] != &apos; &apos; &amp;amp;&amp;amp; str[i] != &apos;\n&apos; &amp;amp;&amp;amp;
            (i == 0 || str[i - 1] == &apos; &apos;)) {
            count++;
        }
    }

    printf(&quot;%d\n&quot;, count);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1085번: 직사각형에서 탈출</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1085/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1085/</guid><description>백준 1085번 직사각형에서 탈출 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:25:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1085&quot;&gt;https://www.acmicpc.net/problem/1085&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;한수는 지금 (x, y)에 있다. 직사각형은 각 변이 좌표축에 평행하고, 왼쪽 아래 꼭짓점은 (0, 0), 오른쪽 위 꼭짓점은 (w, h)에 있다. 직사각형의 경계선까지 가는 거리의 최솟값을 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 x, y, w, h가 주어진다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 문제의 정답을 출력한다.&lt;/p&gt;
&lt;h2&gt;제한&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 ≤ w, h ≤ 1,000&lt;/li&gt;
&lt;li&gt;1 ≤ x ≤ w-1&lt;/li&gt;
&lt;li&gt;1 ≤ y ≤ h-1&lt;/li&gt;
&lt;li&gt;x, y, w, h는 정수&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;현재 위치 &lt;code&gt;(x, y)&lt;/code&gt; 에서 직사각형의 네 변까지 거리를 각각 구하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;왼쪽 변까지 거리: &lt;code&gt;x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;아래 변까지 거리: &lt;code&gt;y&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;오른쪽 변까지 거리: &lt;code&gt;w - x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;위쪽 변까지 거리: &lt;code&gt;h - y&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 네 거리 중 가장 작은 값이 직사각형 경계선까지 가는 최솟값입니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int min(int a, int b) {
    return a &amp;lt; b ? a : b;
}

int main(void) {
    int x, y, w, h;
    int answer;

    scanf(&quot;%d %d %d %d&quot;, &amp;amp;x, &amp;amp;y, &amp;amp;w, &amp;amp;h);

    answer = min(min(x, y), min(w - x, h - y));

    printf(&quot;%d\n&quot;, answer);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1018번: 체스판 다시 칠하기</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1018/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1018/</guid><description>백준 1018번 체스판 다시 칠하기 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1018&quot;&gt;https://www.acmicpc.net/problem/1018&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;지민이는 자신의 저택에서 MN개의 단위 정사각형으로 나누어져 있는 M×N 크기의 보드를 찾았다. 어떤 정사각형은 검은색으로 칠해져 있고, 나머지는 흰색으로 칠해져 있다. 지민이는 이 보드를 잘라서 8×8 크기의 체스판으로 만들려고 한다.&lt;/p&gt;
&lt;p&gt;체스판은 검은색과 흰색이 번갈아서 칠해져 있어야 한다. 구체적으로, 각 칸이 검은색과 흰색 중 하나로 색칠되어 있고, 변을 공유하는 두 개의 사각형은 다른 색으로 칠해져 있어야 한다. 따라서 이 정의를 따르면 체스판을 색칠하는 경우는 두 가지뿐이다. 하나는 맨 왼쪽 위 칸이 흰색인 경우, 하나는 검은색인 경우이다.&lt;/p&gt;
&lt;p&gt;보드가 체스판처럼 칠해져 있다는 보장이 없어서, 지민이는 8×8 크기의 체스판으로 잘라낸 후에 몇 개의 정사각형을 다시 칠해야겠다고 생각했다. 당연히 8*8 크기는 아무데서나 골라도 된다. 지민이가 다시 칠해야 하는 정사각형의 최소 개수를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 N과 M이 주어진다. N과 M은 8보다 크거나 같고, 50보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에는 보드의 각 행의 상태가 주어진다. B는 검은색이며, W는 흰색이다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 지민이가 다시 칠해야 하는 정사각형 개수의 최솟값을 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;보드에서 가능한 모든 8×8 영역을 확인하면 됩니다.&lt;/p&gt;
&lt;p&gt;체스판 패턴은 두 가지뿐입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;왼쪽 위가 &lt;code&gt;W&lt;/code&gt; 인 경우&lt;/li&gt;
&lt;li&gt;왼쪽 위가 &lt;code&gt;B&lt;/code&gt; 인 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 8×8 영역마다 두 경우를 모두 검사해서, 다시 칠해야 하는 칸 수를 셉니다.&lt;br /&gt;
그중 더 작은 값을 선택하고, 전체 영역 중 최솟값을 구하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

char board[50][51];

int min(int a, int b) {
    return a &amp;lt; b ? a : b;
}

int count_repaint(int x, int y) {
    int repaint_w = 0;
    int repaint_b = 0;

    for (int i = x; i &amp;lt; x + 8; i++) {
        for (int j = y; j &amp;lt; y + 8; j++) {
            int parity = (i + j) % 2;

            if (parity == 0) {
                if (board[i][j] != &apos;W&apos;) repaint_w++;
                if (board[i][j] != &apos;B&apos;) repaint_b++;
            } else {
                if (board[i][j] != &apos;B&apos;) repaint_w++;
                if (board[i][j] != &apos;W&apos;) repaint_b++;
            }
        }
    }

    return min(repaint_w, repaint_b);
}

int main() {
    int n, m;
    int answer = 64;

    scanf(&quot;%d %d&quot;, &amp;amp;n, &amp;amp;m);

    for (int i = 0; i &amp;lt; n; i++) {
        scanf(&quot;%s&quot;, board[i]);
    }

    for (int i = 0; i &amp;lt;= n - 8; i++) {
        for (int j = 0; j &amp;lt;= m - 8; j++) {
            int repaint = count_repaint(i, j);
            if (repaint &amp;lt; answer) {
                answer = repaint;
            }
        }
    }

    printf(&quot;%d\n&quot;, answer);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1008번: A / B</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1008/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1008/</guid><description>백준 1008번 A / B 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:15:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/10081&quot;&gt;https://www.acmicpc.net/problem/10081&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;두 정수 A와 B를 입력받은 다음, A/B를 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A와 B가 주어진다. (0 &amp;lt; A, B &amp;lt; 10)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A/B를 출력한다. 실제 정답과 출력값의 절대오차 또는 상대오차가  $10^{-9}$ 이하이면 정답이다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;두 정수를 입력받아 나눈 뒤 출력하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
  int a,b;
  scanf(&quot;%d %d&quot;,&amp;amp;a, &amp;amp;b);
  printf(&quot;%.9f&quot;, (double)a/b);

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1001번: A - B</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1001/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1001/</guid><description>백준 1001번 A - B 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:10:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1001&quot;&gt;https://www.acmicpc.net/problem/1001&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;두 정수 A와 B를 입력받은 다음, A-B를 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A와 B가 주어진다. (0 &amp;lt; A, B &amp;lt; 10)&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A-B를 출력한다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;두 정수를 입력받아 뺀 뒤 출력하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
  int a, b;
  scanf(&quot;%d %d&quot;, &amp;amp;a, &amp;amp;b);
  printf(&quot;%d&quot;, a - b);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[C] 1000번: A + B</title><link>https://pingu52.vercel.app/posts/problem-solving/boj/1000/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/problem-solving/boj/1000/</guid><description>백준 1000번 A + B 문제를 C로 풀이합니다.</description><pubDate>Sat, 07 Mar 2026 19:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1000&quot;&gt;https://www.acmicpc.net/problem/1000&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;두 정수 A와 B를 입력받은 뒤, A+B를 출력하는 문제입니다.&lt;/p&gt;
&lt;h2&gt;입력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A와 B가 주어집니다.&lt;/p&gt;
&lt;h2&gt;출력&lt;/h2&gt;
&lt;p&gt;첫째 줄에 A+B를 출력합니다.&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;두 정수를 입력받아 더한 뒤 출력하면 됩니다.&lt;/p&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
  int a, b;
  scanf(&quot;%d %d&quot;, &amp;amp;a, &amp;amp;b);
  printf(&quot;%d&quot;, a + b);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[OSTEP] 45. Data Integrity and Protection</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-45-file-integrity/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-45-file-integrity/</guid><description>디스크의 잠재적 오류(LSE)와 데이터 손상(corruption)을 구분하고, 이를 탐지하기 위한 체크섬(checksum)의 원리와 종류(XOR, addition, Fletcher, CRC), 그리고 파일 시스템 내 배치 및 운용 전략(scrubbing, physical ID, lost write 대응)을 정리합니다.</description><pubDate>Wed, 11 Feb 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 시간까지 우리는 SSD와 FTL을 다뤘습니다. 이번 장은 스토리지 신뢰성의 마지막 퍼즐인 &lt;strong&gt;데이터 무결성(data integrity)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;디스크 고장은 단순한 fail-stop만 있는 것이 아닙니다. 현대 스토리지는 부분 실패(fail-partial)를 자주 보입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;잠재적 섹터 오류(Latent Sector Error, LSE)&lt;/strong&gt;: 해당 블록을 읽으려 하면 디스크가 에러를 반환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 손상(block corruption)&lt;/strong&gt;: 에러는 없는데 데이터 내용이 달라져서 조용히 틀린 값을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/45_1.png&quot; alt=&quot;Figure 45.1: Frequency Of LSEs And Block Corruption&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. LSE와 corruption은 해결 방식이 다르다&lt;/h2&gt;
&lt;h3&gt;2.1 LSE는 탐지가 쉽고, 복구는 redundancy의 문제다&lt;/h3&gt;
&lt;p&gt;LSE는 디스크가 에러를 반환하므로 탐지가 쉽습니다. 따라서 저장 시스템은 미러나 패리티 같은 &lt;strong&gt;중복성(redundancy)&lt;/strong&gt; 으로 복구하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;미러 RAID라면 다른 복사본에서 읽기&lt;/li&gt;
&lt;li&gt;RAID-4/5라면 패리티 그룹으로 재구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, LSE 자체는 체크섬보다도 복구 경로가 준비되어 있는지가 더 중요합니다.&lt;/p&gt;
&lt;h3&gt;2.2 corruption은 탐지가 핵심이고, 체크섬이 필요하다&lt;/h3&gt;
&lt;p&gt;corruption은 디스크가 정상처럼 행동하는데 내용이 다릅니다. 따라서 시스템이 데이터가 틀렸다는 사실을 알아차릴 수 있도록 &lt;strong&gt;탐지 메커니즘&lt;/strong&gt;이 필요합니다. 이때 가장 널리 쓰는 것이 체크섬입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 체크섬의 기본 개념&lt;/h2&gt;
&lt;p&gt;체크섬은 데이터 청크의 내용을 요약한 작은 값입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터 블록을 $D$&lt;/li&gt;
&lt;li&gt;체크섬 함수(알고리즘)를 $C$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;라고 하면 저장 시점에 $C(D)$를 계산해 함께 보관하고, 읽기 시점에 다시 계산한 $C(D&apos;)$와 비교합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$C_s(D)$: 저장된 체크섬(stored)&lt;/li&gt;
&lt;li&gt;$C_c(D)$: 읽은 데이터로 계산한 체크섬(computed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
C_s(D) = C_c(D) \Rightarrow \text{정상일 가능성이 높음}
$$&lt;/p&gt;
&lt;p&gt;$$
C_s(D) \ne C_c(D) \Rightarrow \text{corruption 탐지}
$$&lt;/p&gt;
&lt;p&gt;중요한 전제는 하나입니다.
체크섬은 탐지 장치이고, 복구를 하려면 별도의 redundancy가 있어야 합니다. redundancy가 없다면 결국 에러를 반환하는 것 외에 할 수 있는 일이 없습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 체크섬 알고리즘의 종류&lt;/h2&gt;
&lt;p&gt;체크섬은 강도(strength)와 속도(speed)의 트레이드오프입니다. 강해질수록 보통 비용이 커집니다.&lt;/p&gt;
&lt;p&gt;또한 요약값이 작기 때문에, 서로 다른 데이터가 같은 체크섬을 가질 수 있는 &lt;strong&gt;충돌(collision)&lt;/strong&gt; 가능성은 원리적으로 존재합니다.&lt;/p&gt;
&lt;h3&gt;4.1 XOR&lt;/h3&gt;
&lt;p&gt;바이트(또는 워드) 단위로 모두 XOR합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점: 매우 빠름&lt;/li&gt;
&lt;li&gt;단점: 같은 위치에서 짝수 번 비트가 뒤집히면 탐지 실패 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 Addition&lt;/h3&gt;
&lt;p&gt;2의 보수 덧셈을 누적하고 overflow는 버립니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점: 빠름&lt;/li&gt;
&lt;li&gt;단점: 데이터가 shift되는 형태의 변화에 약함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 Fletcher checksum&lt;/h3&gt;
&lt;p&gt;두 개의 누적 합 $s_1, s_2$를 유지해 위치 정보를 일부 반영합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$s_1 = (s_1 + D[i]) \bmod 255$&lt;/li&gt;
&lt;li&gt;$s_2 = (s_2 + s_1) \bmod 255$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;XOR나 단순 addition보다 강력하면서도 충분히 빠른 편입니다.&lt;/p&gt;
&lt;h3&gt;4.4 CRC&lt;/h3&gt;
&lt;p&gt;데이터를 큰 이진수로 보고 약속된 다항식으로 나눈 나머지를 사용합니다.
버스트 에러에 강하고, 하드웨어 지원이 있어 빠르게 계산 가능한 경우도 많습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 체크섬을 어디에 저장할까&lt;/h2&gt;
&lt;p&gt;체크섬은 계산보다 레이아웃이 더 골치 아픈 경우가 많습니다.&lt;/p&gt;
&lt;h3&gt;5.1 섹터 내 여유 공간(520-byte 섹터)&lt;/h3&gt;
&lt;p&gt;일부 디스크는 512B 대신 520B 섹터로 포맷해 추가 8B를 체크섬에 사용합니다.
구현은 깔끔하지만 하드웨어 지원이 필요합니다.&lt;/p&gt;
&lt;h3&gt;5.2 체크섬을 모아서 저장(checksum sector)&lt;/h3&gt;
&lt;p&gt;일반 디스크에서는 체크섬을 별도 섹터에 몰아 넣고, 그 뒤에 데이터 블록들을 배치할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점: 어떤 디스크에서도 동작&lt;/li&gt;
&lt;li&gt;단점: overwrite 시 체크섬 섹터 갱신 때문에 추가 I/O가 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 파트는 PDF의 레이아웃 그림을 그대로 캡처해 붙이면 이해가 매우 빨라집니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 새로운 문제 1: misdirected write&lt;/h2&gt;
&lt;p&gt;misdirected write는 디스크나 RAID 컨트롤러가 데이터를 제대로 쓰긴 했는데, 주소를 잘못 써서 엉뚱한 곳에 기록한 경우입니다.&lt;/p&gt;
&lt;p&gt;이 문제는 단순히 C(D)만 저장하면 탐지가 어려울 수 있습니다.
따라서 체크섬에 &lt;strong&gt;physical ID&lt;/strong&gt; 를 함께 저장하는 방식이 유용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;체크섬 옆에 disk 번호, block 오프셋을 함께 저장&lt;/li&gt;
&lt;li&gt;읽을 때 내가 기대한 (disk, block)과 저장된 physical ID가 다르면 misdirected write를 탐지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/45_2.png&quot; alt=&quot;Misdirected Write Detection With Physical ID&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 새로운 문제 2: lost write&lt;/h2&gt;
&lt;p&gt;lost write는 더 까다롭습니다.
디바이스가 write 완료를 알렸는데 실제로는 기록되지 않아, 디스크에는 예전 내용이 남아 있는 경우입니다.&lt;/p&gt;
&lt;p&gt;이 경우 예전 데이터 블록은 자기 체크섬과 일치할 가능성이 높고, physical ID도 맞기 때문에 탐지가 어렵습니다.&lt;/p&gt;
&lt;p&gt;대표적인 대응은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;read-after-write&lt;/strong&gt;: 즉시 다시 읽어 검증, 하지만 I/O가 2배가 되어 느림&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상위 메타데이터에 체크섬 저장&lt;/strong&gt;: inode/indirect 블록에 데이터 블록의 체크섬을 저장해 두면,
데이터 블록이 갱신되지 않은 경우 불일치로 탐지 가능
단, 메타데이터와 데이터의 write가 모두 lost면 실패할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 지점에서 ZFS의 체크섬 포인터 접근이 의미를 가집니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 스크러빙(scrubbing)&lt;/h2&gt;
&lt;p&gt;읽을 때만 검사하면, 거의 접근하지 않는 cold data는 부패가 진행돼도 모를 수 있습니다.
따라서 많은 시스템은 주기적으로 전체 블록을 스캔하며 체크섬을 확인하는 &lt;strong&gt;스크러빙&lt;/strong&gt;을 수행합니다.&lt;/p&gt;
&lt;p&gt;보통 nightly 또는 weekly 같은 주기 스케줄이 많이 언급됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 오버헤드&lt;/h2&gt;
&lt;p&gt;체크섬은 공짜가 아닙니다. 공간과 시간 오버헤드가 있습니다.&lt;/p&gt;
&lt;h3&gt;9.1 공간&lt;/h3&gt;
&lt;p&gt;예를 들어 4KiB 블록당 8B 체크섬이면&lt;/p&gt;
&lt;p&gt;$$
\frac{8}{4096} \approx 0.00195 = 0.195%
$$&lt;/p&gt;
&lt;p&gt;즉 대략 0.19% 수준입니다.
4B 체크섬이라면 대략 그 절반입니다.&lt;/p&gt;
&lt;h3&gt;9.2 시간&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CPU: 저장 시, 읽기 시 모두 체크섬 계산 비용이 듭니다&lt;/li&gt;
&lt;li&gt;최적화: 데이터 copy와 체크섬 계산을 결합하면 CPU 오버헤드를 줄일 수 있습니다&lt;/li&gt;
&lt;li&gt;I/O: 체크섬을 데이터와 떨어뜨려 두면 추가 I/O가 생길 수 있고, 스크러빙 역시 백그라운드 I/O를 유발합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;디스크는 fail-stop만 있는 것이 아니라 fail-partial 실패 모드도 가진다&lt;/li&gt;
&lt;li&gt;LSE는 탐지가 쉽고, 복구는 redundancy로 해결한다&lt;/li&gt;
&lt;li&gt;corruption은 탐지가 핵심이며, 체크섬이 표준 해법이다&lt;/li&gt;
&lt;li&gt;misdirected write는 checksum에 physical ID를 포함해 탐지할 수 있다&lt;/li&gt;
&lt;li&gt;lost write는 더 어렵고, read-after-write 또는 상위 메타데이터 체크섬 같은 추가 장치가 필요하다&lt;/li&gt;
&lt;li&gt;scrubbing은 잘 읽지 않는 데이터의 부패를 조기에 발견하는 데 유용하다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Data Integrity&lt;/code&gt;: 저장 장치가 오류 없이 동일한 데이터를 반환한다는 성질&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fail-stop&lt;/code&gt;: 장치가 고장 시 동작을 멈추거나 에러를 명확히 반환하는 실패 모델&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fail-partial&lt;/code&gt;: 일부 요청은 정상처럼 보이지만 내부적으로 데이터가 손상되는 실패 모델&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LSE (Latent Sector Error)&lt;/code&gt;: 읽기 시점에 드러나는 섹터/블록 단위의 잠재적 오류&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Block Corruption&lt;/code&gt;: 에러 없이 잘못된 데이터를 반환하는 현상&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Silent Corruption&lt;/code&gt;: corruption이 시스템에 감지되지 않고 조용히 지나가는 상황&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Checksum&lt;/code&gt;: 데이터 블록을 요약한 값으로, 읽기 시 재계산하여 불일치를 탐지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Collision&lt;/code&gt;: 서로 다른 데이터가 동일한 체크섬 값을 갖는 경우&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XOR Checksum&lt;/code&gt;: XOR 누적 기반의 매우 단순한 체크섬&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Addition Checksum&lt;/code&gt;: 덧셈 누적 기반의 간단한 체크섬&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fletcher Checksum&lt;/code&gt;: 두 개의 누적 합으로 위치 정보를 일부 반영하는 체크섬&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CRC&lt;/code&gt;: 다항식 나눗셈 기반의 체크섬으로, 버스트 에러 탐지에 강함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Physical ID&lt;/code&gt;: (disk, block)처럼 물리 위치를 식별하는 정보로, misdirected write 탐지에 활용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Misdirected Write&lt;/code&gt;: 데이터가 의도한 물리 위치가 아닌 다른 위치에 기록되는 오류&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Lost Write&lt;/code&gt;: write 완료가 보고되었으나 실제로는 기록되지 않아 이전 데이터가 남는 오류&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Read-after-write&lt;/code&gt;: write 직후 즉시 read로 검증하는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scrubbing&lt;/code&gt;: 백그라운드에서 전체 블록을 읽고 체크섬을 확인하는 주기적 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-integrity.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 45: Data Integrity and Protection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 44. Flash-based SSDs</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-44-file-ssd/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-44-file-ssd/</guid><description>기계적 구동 장치가 없는 플래시 메모리 기반 SSD의 물리적 특성(erase-before-program)과 이를 디스크처럼 보이게 만드는 FTL(Flash Translation Layer)의 설계(로그 구조, GC, 매핑 테이블, wear leveling)를 정리합니다.</description><pubDate>Tue, 10 Feb 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 장들에서 우리는 회전 플래터와 암을 가진 HDD를 기준으로 스토리지와 파일 시스템을 배웠습니다. 하지만 오늘날 성능 중심 스토리지의 핵심은 &lt;strong&gt;SSD(Solid-State Drive)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;SSD는 기계적인 구동부가 없어서 seek나 회전 지연이 없습니다. 대신 트랜지스터 기반의 &lt;strong&gt;NAND 플래시&lt;/strong&gt;를 사용하며, 이 플래시는 HDD에는 없던 제약을 가집니다. 이번 글에서는 그 제약을 숨기고 OS에게는 여전히 블록 디바이스처럼 보이게 만드는 &lt;strong&gt;FTL(Flash Translation Layer)&lt;/strong&gt; 의 내부 메커니즘을 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 플래시 메모리의 물리적 특성&lt;/h2&gt;
&lt;h3&gt;1.1 용어 주의: 블록과 페이지&lt;/h3&gt;
&lt;p&gt;플래시에서도 block, page라는 단어를 쓰지만, 우리가 디스크/VM에서 쓰던 의미와 다릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;페이지(page)&lt;/strong&gt;: 읽기(read)와 쓰기(program)의 단위, 보통 수 KB(예: 4 KiB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;블록(block, erase block)&lt;/strong&gt;: 지우기(erase)의 단위, 보통 수백 KB 이상(예: 128 KiB, 256 KiB)&lt;/li&gt;
&lt;li&gt;플래시 칩은 많은 &lt;strong&gt;bank/plane&lt;/strong&gt;으로 구성되고, 각 bank/plane은 많은 block, 각 block은 많은 page로 구성됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/44_1.png&quot; alt=&quot;Figure 44.1: Pages Within Blocks&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.2 3가지 기본 연산: read / erase / program&lt;/h3&gt;
&lt;p&gt;플래시에는 3개의 기본 연산이 있고, 여기서 모든 난제가 시작됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;read(페이지)&lt;/strong&gt;&lt;br /&gt;
임의 위치를 빠르게 읽습니다. 대략 수십 $\mu\mathrm{s}$ 수준입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;erase(블록)&lt;/strong&gt;&lt;br /&gt;
쓰기 전에 반드시 필요합니다. 블록 단위로만 지울 수 있고, 비용이 가장 큽니다. 대략 수 $\mathrm{ms}$ 수준입니다.&lt;br /&gt;
erase는 블록 내 모든 비트를 1로 만드는 동작으로 볼 수 있고, 기존 내용은 파괴됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;program(페이지)&lt;/strong&gt;&lt;br /&gt;
erase 이후에만 가능하며, 페이지를 한 번 program하면 같은 페이지를 다시 program할 수 없습니다.&lt;br /&gt;
또한 program은 보통 1을 0으로만 바꾸는 방향으로 동작합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 제약을 상태 전이로 보면 직관적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;초기: INVALID&lt;/li&gt;
&lt;li&gt;erase 후: ERASED&lt;/li&gt;
&lt;li&gt;program 후: VALID&lt;/li&gt;
&lt;li&gt;VALID 페이지를 다시 program: error&lt;/li&gt;
&lt;li&gt;내용을 바꾸려면: 해당 페이지가 속한 &lt;strong&gt;블록 전체 erase&lt;/strong&gt; 가 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 성능과 신뢰성&lt;/h3&gt;
&lt;p&gt;플래시는 read는 빠르지만, program과 erase가 훨씬 비쌉니다. 또한 신뢰성 이슈도 존재합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;wear out&lt;/strong&gt;: 블록은 program/erase(P/E) 사이클을 반복하면 열화됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;disturbance&lt;/strong&gt;: 인접 페이지 비트가 뒤집히는 read/program disturb가 발생할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/44_2.png&quot; alt=&quot;Figure 44.2: Raw Flash Performance&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. FTL: 플래시를 블록 디바이스처럼 보이게 하기&lt;/h2&gt;
&lt;p&gt;OS는 저장 장치를 여전히 LBA 기반 블록 배열로 취급합니다. 하지만 플래시는 overwrite가 불가능하고 erase 단위가 큽니다. 이 간극을 메워주는 계층이 &lt;strong&gt;FTL&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;FTL의 목표는 3가지로 요약됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;주소 변환&lt;/strong&gt;: 논리 주소(LBA) $\to$ 물리 페이지 주소(PPA)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;write amplification 최소화&lt;/strong&gt;: 내부 복사/GC로 인한 추가 쓰기 감소&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wear leveling&lt;/strong&gt;: 특정 블록만 닳지 않도록 P/E 사이클을 분산&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SSD 내부는 플래시 칩들 + volatile memory(SRAM/DRAM) + 컨트롤 로직으로 구성되고, FTL은 컨트롤 로직의 핵심 기능입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/44_3.png&quot; alt=&quot;Figure 44.3: SSD Logical Diagram&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 나쁜 접근: direct-mapped FTL&lt;/h2&gt;
&lt;p&gt;PDF에서 말하는 &lt;strong&gt;direct-mapped&lt;/strong&gt; 는 페이지 단위 매핑이 아니라, 훨씬 단순하고 위험한 고정 매핑입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;논리 페이지 $N$은 물리 페이지 $N$에 고정&lt;/li&gt;
&lt;li&gt;write(논리 페이지 $N$)를 처리하려면
&lt;ul&gt;
&lt;li&gt;해당 페이지가 속한 &lt;strong&gt;블록 전체를 읽고&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;블록을 &lt;strong&gt;erase&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;원래 데이터들과 변경된 페이지를 &lt;strong&gt;program&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과적으로 write는 블록 크기에 비례하는 write amplification을 만들고,
hot data overwrite가 특정 블록을 빨리 죽게 만들어 신뢰성도 최악입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 방식은 성능/수명 관점에서 실질적으로 사용할 수 없는 접근입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 좋은 접근: log-structured FTL&lt;/h2&gt;
&lt;p&gt;대부분의 현대 FTL은 &lt;strong&gt;log-structured&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;overwrite를 하지 않고&lt;/li&gt;
&lt;li&gt;다음 free page에 append 방식으로 program&lt;/li&gt;
&lt;li&gt;그리고 &lt;strong&gt;mapping table&lt;/strong&gt; 로 논리 주소가 현재 어디에 있는지 추적합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 방식은 작은 랜덤 쓰기를 내부적으로 큰 순차 쓰기 형태로 바꿔, erase 빈도를 낮추고 성능을 끌어올립니다.&lt;/p&gt;
&lt;h3&gt;4.1 매핑 테이블은 왜 필요한가&lt;/h3&gt;
&lt;p&gt;논리 블록 $L$을 쓸 때마다 물리 위치가 바뀌므로,
read(L)은 현재 물리 위치를 알아야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mapping table: $L \mapsto P$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 매핑 정보는 전원 꺼지면 어떻게 되나&lt;/h3&gt;
&lt;p&gt;매핑 테이블은 volatile memory에 있으므로 전원이 나가면 사라집니다. 따라서 SSD는 매핑 정보를 복구할 수 있어야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가장 단순한 방식: 각 페이지의 OOB(out-of-band)에 논리 주소 정보를 기록해두고, 부팅 시 전체를 스캔하여 매핑 테이블을 재구성&lt;/li&gt;
&lt;li&gt;단점: 대용량 SSD에서 스캔 비용이 큼&lt;/li&gt;
&lt;li&gt;고급 SSD는 logging/checkpointing으로 복구 시간을 줄이기도 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 가비지 컬렉션(GC)과 write amplification&lt;/h2&gt;
&lt;p&gt;log-structured의 대가가 있습니다. overwrite가 쌓이면 구버전 데이터가 &lt;strong&gt;garbage&lt;/strong&gt; 로 남습니다.&lt;/p&gt;
&lt;p&gt;FTL은 주기적으로 GC를 수행합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;garbage가 포함된 블록을 선택&lt;/li&gt;
&lt;li&gt;그 블록의 &lt;strong&gt;live page&lt;/strong&gt; 만 읽어서 새로운 위치로 이동&lt;/li&gt;
&lt;li&gt;블록 erase로 free 공간 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GC가 만드는 핵심 부작용이 &lt;strong&gt;write amplification&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;$$
\mathrm{WA} = \frac{\text{FTL이 플래시에 실제로 쓴 바이트}}{\text{호스트가 요청한 바이트}}
$$&lt;/p&gt;
&lt;h3&gt;5.1 TRIM이 왜 필요한가&lt;/h3&gt;
&lt;p&gt;호스트가 파일을 삭제해도 SSD는 그 LBA가 더 이상 필요 없다는 사실을 모르면, live로 착각하고 불필요한 복사를 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TRIM&lt;/strong&gt; 은 호스트가 더 이상 필요 없는 LBA 범위를 SSD에 알려 GC 효율을 높입니다.&lt;/p&gt;
&lt;p&gt;추가로 많은 SSD는 &lt;strong&gt;overprovisioning&lt;/strong&gt; 으로 여유 공간을 두어 GC를 더 늦추고, 백그라운드에서 처리하기 쉽게 만듭니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 매핑 테이블 크기 문제와 3가지 설계&lt;/h2&gt;
&lt;p&gt;log-structured는 보통 페이지 단위로 유연하게 쓰기 때문에, 이상적으로는 논리 페이지마다 매핑 엔트리가 필요합니다. 문제는 그 크기입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSD 용량: $C$&lt;/li&gt;
&lt;li&gt;페이지 크기: $B_{\mathrm{page}}$&lt;/li&gt;
&lt;li&gt;포인터 크기: $B_{\mathrm{ptr}}$ (예: 4 B)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
N_{\mathrm{page}} = \frac{C}{B_{\mathrm{page}}}, \quad
M_{\mathrm{map}} = N_{\mathrm{page}} \cdot B_{\mathrm{ptr}}
$$&lt;/p&gt;
&lt;p&gt;예를 들어 1 TiB SSD, 4 KiB 페이지, 4 B 포인터면 매핑 테이블만 대략 1 GiB 수준이 됩니다.&lt;/p&gt;
&lt;p&gt;이 때문에 다양한 절충이 등장합니다.&lt;/p&gt;
&lt;h3&gt;6.1 블록 단위 매핑(block-based mapping)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;장점: 엔트리 수가 블록 단위로 줄어 테이블이 작아짐&lt;/li&gt;
&lt;li&gt;단점: small write에서 기존 블록의 live 데이터를 대량 복사해야 하므로 write amplification이 커짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 하이브리드 매핑(hybrid mapping)&lt;/h3&gt;
&lt;p&gt;PDF의 대표 절충입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;대부분은 &lt;strong&gt;data table(블록 매핑)&lt;/strong&gt; 으로 관리&lt;/li&gt;
&lt;li&gt;소수의 &lt;strong&gt;log block&lt;/strong&gt; 에 대해서만 &lt;strong&gt;log table(페이지 매핑)&lt;/strong&gt; 을 유지&lt;/li&gt;
&lt;li&gt;쓰기는 log block에 append&lt;/li&gt;
&lt;li&gt;log block이 찼거나 정책상 필요해지면 merge로 data block을 재구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;merge는 3가지 케이스가 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;switch merge&lt;/strong&gt;: 가장 이상적, log block이 순서대로 꽉 차서 그대로 data block으로 전환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;partial merge&lt;/strong&gt;: 일부만 덮였으면 나머지 live page를 읽어 합쳐서 재구성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;full merge&lt;/strong&gt;: 흩어진 overwrite로 여러 블록을 모아 재조립, 가장 비싸고 피해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 페이지 매핑 + 캐싱(page mapping plus caching)&lt;/h3&gt;
&lt;p&gt;페이지 매핑을 유지하되,
working set의 translation만 메모리에 캐싱하는 방식도 연구됩니다.
working set이 작으면 좋지만, working set이 크면 translation miss 때문에 추가 read/write가 발생할 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. wear leveling&lt;/h2&gt;
&lt;p&gt;log-structured와 GC는 기본적으로 write load를 퍼뜨리지만, 여전히 문제가 남습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 블록이 &lt;strong&gt;long-lived data&lt;/strong&gt; 로 가득 차면 overwrite가 없어서 GC로는 회수되지 않음&lt;/li&gt;
&lt;li&gt;그러면 그 블록은 P/E 사이클을 거의 받지 않아 wear leveling 관점에서 불균형이 생김&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 FTL은 때때로 이런 블록의 live data를 다른 곳으로 옮겨,
해당 블록도 쓰기 대상이 되게 만들어 P/E 사이클을 평준화합니다.
이 과정은 write amplification을 늘리지만, SSD 수명을 위해 필수입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. SSD 성능과 비용 감각&lt;/h2&gt;
&lt;p&gt;SSD는 랜덤 I/O에서 HDD를 압도하지만, 순차 I/O에서는 격차가 상대적으로 줄어듭니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/44_4.png&quot; alt=&quot;Figure 44.4: SSD vs HDD Performance&quot; /&gt;&lt;/p&gt;
&lt;p&gt;또한 cost per GB는 여전히 HDD가 유리하여, 데이터센터에서는 hot 데이터는 SSD, cold 데이터는 HDD 같은 혼합 구성도 흔합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;플래시는 &lt;strong&gt;erase-before-program&lt;/strong&gt; 제약 때문에 overwrite가 불가능하고 erase가 비쌉니다&lt;/li&gt;
&lt;li&gt;FTL은 플래시 위에 블록 디바이스 인터페이스를 구현하며, 성능/수명 목표를 동시에 만족해야 합니다&lt;/li&gt;
&lt;li&gt;direct-mapped 같은 단순 고정 매핑은 write amplification과 wear out 때문에 실용적이지 않습니다&lt;/li&gt;
&lt;li&gt;대부분의 SSD는 log-structured FTL을 사용하고, GC로 인해 write amplification이 발생합니다&lt;/li&gt;
&lt;li&gt;page-level mapping은 유연하지만 테이블이 커지고, 이를 줄이기 위해 block/hybrid/caching 절충이 등장합니다&lt;/li&gt;
&lt;li&gt;wear leveling은 수명 확보를 위해 필수이며, 역시 추가 write amplification을 동반합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NAND Flash&lt;/code&gt;: SSD의 주 저장 매체인 비휘발성 플래시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Block (Erase Block)&lt;/code&gt;: erase 단위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page&lt;/code&gt;: read/program 단위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FTL&lt;/code&gt;: 논리 블록 요청을 물리 flash 연산(read/erase/program)으로 변환하는 계층&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Log-structured FTL&lt;/code&gt;: overwrite 대신 append로 쓰기를 처리하는 FTL 설계&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Garbage Collection&lt;/code&gt;: live page를 옮기고 블록을 erase하여 공간을 회수하는 과정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Write Amplification&lt;/code&gt;: 호스트 쓰기 대비 내부 쓰기 총량의 증가 비율&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Wear Leveling&lt;/code&gt;: P/E 사이클을 블록 전체에 고르게 분산시키는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TRIM&lt;/code&gt;: 호스트가 더 이상 필요 없는 LBA 범위를 SSD에 알려 GC 효율을 높이는 명령&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-ssd.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 44: Flash-based SSDs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 43. Log-structured File Systems</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-43-file-lfs/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-43-file-lfs/</guid><description>디스크 쓰기 성능을 극대화하기 위해 모든 업데이트를 로그 형태로 순차 기록하는 LFS의 설계 철학, 아이노드 맵(imap), 그리고 가비지 컬렉션(Cleaner) 메커니즘을 분석합니다.</description><pubDate>Tue, 10 Feb 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장에서 다룬 FFS(Fast File System)는 디스크의 물리적 구조를 고려해 지역성(Locality)을 살려 성능을 높였습니다. 하지만 90년대 초 버클리 연구진은 하드웨어 트렌드가 바뀌고 있다는 점에 주목했고, 완전히 다른 관점의 파일 시스템을 제안합니다.&lt;/p&gt;
&lt;p&gt;디스크를 거대한 로그처럼 사용하여, 모든 쓰기를 큰 순차 쓰기로 바꾸는 &lt;strong&gt;LFS(Log-structured File System)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 LFS가 왜 등장했는지, 그리고 덮어쓰지 않는 설계에서 파일 위치가 계속 바뀌는데도 어떻게 데이터를 찾고, 쓰레기를 어떻게 회수하는지까지 연결해서 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 등장 배경: 기술 트렌드의 변화&lt;/h2&gt;
&lt;p&gt;LFS는 다음 관찰에서 출발합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;메모리가 커지고 있다&lt;/strong&gt;
메모리가 커질수록 더 많은 데이터가 캐시에 올라가고, 디스크 트래픽은 점점 쓰기 중심으로 바뀝니다. 따라서 파일 시스템 성능은 결국 쓰기 성능이 좌우하기 쉽습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;순차 I/O와 랜덤 I/O의 격차&lt;/strong&gt;
디스크 전송 대역폭은 증가했지만, seek와 회전 지연은 크게 좋아지지 않습니다. 따라서 랜덤 쓰기보다 순차 쓰기를 만드는 것이 훨씬 유리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;기존 파일 시스템의 common workload 성능 한계&lt;/strong&gt;
FFS는 block group으로 가까이 모으더라도, 작은 파일 생성 같은 작업에서 inode, 비트맵, 디렉터리 블록 등 다수의 메타데이터를 갱신해야 합니다. 결국 많은 짧은 seek와 회전 지연이 발생해 peak sequential bandwidth에 한참 못 미칩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RAID-aware하지 않음&lt;/strong&gt;
RAID-4/5 같은 parity 기반 RAID는 작은 쓰기에서 read-modify-write 문제가 발생합니다. 기존 파일 시스템은 이 최악 패턴을 회피하도록 설계되지 않았습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;요약하면 목표는 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;쓰기를 가능한 한 &lt;strong&gt;크고 연속적인 순차 쓰기&lt;/strong&gt;로 바꿔서, positioning 비용을 상각한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 순차 쓰기: 덮어쓰지 않고 append-only로 기록한다&lt;/h2&gt;
&lt;p&gt;LFS는 기존 블록을 덮어쓰지 않습니다. 변경이 생기면 데이터 블록과 관련 메타데이터(inode 등)를 &lt;strong&gt;디스크의 빈 공간 끝에 새로 기록&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 중요한 디테일이 하나 있습니다.&lt;/p&gt;
&lt;h3&gt;2.1 순차 주소로 쓰는 것만으로는 부족하다&lt;/h3&gt;
&lt;p&gt;연속된 주소에 블록을 쓰더라도, 두 번의 작은 write 사이에 시간이 벌어지면 디스크가 회전해버립니다. 그 결과 다음 블록은 한 바퀴 가까이 기다린 뒤에야 기록될 수 있습니다.&lt;/p&gt;
&lt;p&gt;즉, 성능을 얻으려면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;순차 주소&lt;/li&gt;
&lt;li&gt;그리고 &lt;strong&gt;충분히 큰 contiguous write&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 조건이 동시에 필요합니다.&lt;/p&gt;
&lt;h3&gt;2.2 세그먼트(segment)와 write buffering&lt;/h3&gt;
&lt;p&gt;LFS는 이를 위해 write buffering을 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작은 업데이트들을 메모리에 모아 둔다&lt;/li&gt;
&lt;li&gt;충분히 쌓이면 큰 덩어리로 한 번에 디스크에 쓴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 큰 덩어리를 &lt;strong&gt;세그먼트(segment)&lt;/strong&gt; 라고 부릅니다. 실제 세그먼트는 보통 몇 MB 단위입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 얼마나 버퍼링해야 하는가&lt;/h2&gt;
&lt;p&gt;세그먼트가 크면 클수록 positioning 오버헤드를 잘 상각하고 peak bandwidth에 가까워질 수 있습니다. 이를 간단한 모델로 유도할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;positioning 비용: $T_{\mathrm{position}}$&lt;/li&gt;
&lt;li&gt;peak 전송률: $R_{\mathrm{peak}}$&lt;/li&gt;
&lt;li&gt;한 번에 쓰는 데이터 크기: $D$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
T_{\mathrm{write}} = T_{\mathrm{position}} + \frac{D}{R_{\mathrm{peak}}}
\tag{1}
$$&lt;/p&gt;
&lt;p&gt;$$
R_{\mathrm{eff}} = \frac{D}{T_{\mathrm{position}} + \frac{D}{R_{\mathrm{peak}}}}
\tag{2}
$$&lt;/p&gt;
&lt;p&gt;peak 대비 $F$ 만큼의 효율을 얻고 싶다면 $R_{\mathrm{eff}} = F\cdot R_{\mathrm{peak}}$를 만족하도록 $D$를 잡으면 되고, 정리하면 다음이 됩니다.&lt;/p&gt;
&lt;p&gt;$$
D = \frac{F}{1-F} \cdot R_{\mathrm{peak}} \cdot T_{\mathrm{position}}
\tag{6}
$$&lt;/p&gt;
&lt;p&gt;이 식이 말하는 직관은 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원하는 효율 $F$가 1에 가까워질수록, 필요한 $D$는 급격히 커집니다&lt;/li&gt;
&lt;li&gt;그래서 LFS는 KB 단위가 아니라 &lt;strong&gt;MB 단위&lt;/strong&gt;로 버퍼링하는 설계를 택합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 문제: inode를 어떻게 찾는가&lt;/h2&gt;
&lt;p&gt;FFS에서는 inode table이 고정 위치에 있어서 i-number만 알면 inode 위치를 계산할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 LFS는 덮어쓰지 않습니다.
inode를 갱신할 때마다 inode는 로그 끝으로 이동하고, 최신 inode는 디스크 여기저기에 흩어집니다.&lt;/p&gt;
&lt;p&gt;즉, i-number만으로는 inode를 찾을 수 없습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 해결: indirection 계층, inode map(imap)&lt;/h2&gt;
&lt;p&gt;LFS는 i-number와 inode 사이에 &lt;strong&gt;indirection&lt;/strong&gt; 계층을 하나 둡니다. 이것이 &lt;strong&gt;inode map(imap)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;입력: inode 번호&lt;/li&gt;
&lt;li&gt;출력: 최신 inode의 디스크 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;imap은 보통 배열처럼 구현할 수 있고, 엔트리 하나는 디스크 포인터 4바이트 정도로 생각할 수 있습니다. inode가 새 위치에 기록될 때마다 해당 inode 번호의 imap 엔트리도 함께 갱신됩니다.&lt;/p&gt;
&lt;p&gt;여기서 또 중요한 디테일이 있습니다.&lt;/p&gt;
&lt;h3&gt;5.1 imap도 고정 위치에 두지 않는다&lt;/h3&gt;
&lt;p&gt;imap을 디스크의 고정된 위치에 두면, inode를 갱신할 때마다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;로그 끝에 쓰기&lt;/li&gt;
&lt;li&gt;다시 imap 고정 위치에 쓰기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가 되어 seek가 생기고 성능이 무너집니다.&lt;/p&gt;
&lt;p&gt;그래서 LFS는 imap도 로그에 함께 씁니다.
구체적으로는 imap 전체가 아니라 &lt;strong&gt;갱신된 조각(chunk)&lt;/strong&gt; 을 데이터 블록, inode와 함께 같은 세그먼트에 기록합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 마지막 퍼즐: checkpoint region(CR)&lt;/h2&gt;
&lt;p&gt;imap 조각도 디스크 여기저기에 흩어지는데, 그럼 최신 imap 조각을 어디서 찾을까요?&lt;/p&gt;
&lt;p&gt;파일 시스템은 반드시 시작점이 하나 필요합니다.
LFS는 이를 위해 디스크의 알려진 고정 위치에 **checkpoint region(CR)**을 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CR은 최신 imap 조각들의 주소들을 저장합니다&lt;/li&gt;
&lt;li&gt;CR은 자주 쓰면 seek를 유발하므로, 보통 수십 초 단위로 주기적으로만 갱신합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 파일 접근 경로는 다음처럼 정리됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CR 읽기&lt;/li&gt;
&lt;li&gt;imap 조각 위치 확인 및 imap 확보&lt;/li&gt;
&lt;li&gt;inode 위치 확인&lt;/li&gt;
&lt;li&gt;inode 읽기&lt;/li&gt;
&lt;li&gt;데이터 블록 읽기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.1 읽기 성능은 어떤가&lt;/h3&gt;
&lt;p&gt;처음에는 CR과 imap을 읽어야 하지만, imap은 메모리에 캐시될 수 있습니다.
imap이 캐시된 뒤에는 inode 번호로 inode 주소를 찾는 것은 메모리 lookup이 되고, 그 이후 데이터 읽기는 일반 UNIX 파일 시스템과 유사하게 진행됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 디렉터리와 recursive update problem&lt;/h2&gt;
&lt;p&gt;디렉터리는 전통적인 UNIX와 동일하게 (이름, inode 번호) 매핑의 집합입니다.&lt;/p&gt;
&lt;p&gt;덮어쓰지 않는 파일 시스템에서는 inode 위치가 바뀔 때 디렉터리도 갱신해야 할 것처럼 보입니다.
그런데 디렉터리를 갱신하면 그 디렉터리의 inode도 바뀌고, 그러면 상위 디렉터리도 바뀌는 식으로 트리 전체로 파급되는 문제가 생깁니다.
이를 &lt;strong&gt;recursive update problem&lt;/strong&gt;이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;LFS는 imap으로 이를 피합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리는 이름을 inode 번호에만 매핑한다&lt;/li&gt;
&lt;li&gt;inode의 실제 위치 변화는 imap에서만 관리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 디렉터리를 바꿀 필요 없이 imap만 갱신하면 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 새로운 문제: garbage collection, Cleaner&lt;/h2&gt;
&lt;p&gt;LFS는 덮어쓰지 않기 때문에, 갱신이 반복되면 구버전 데이터와 구버전 inode가 디스크에 남습니다.
이들은 더 이상 참조되지 않는 &lt;strong&gt;garbage&lt;/strong&gt;가 됩니다.&lt;/p&gt;
&lt;p&gt;garbage를 회수하지 않으면 디스크는 결국 가득 차므로, LFS는 백그라운드에서 &lt;strong&gt;Cleaner&lt;/strong&gt;가 공간을 회수합니다.&lt;/p&gt;
&lt;p&gt;중요한 점은 Cleaner가 블록 단위로 땜빵하듯 free를 만들면 안 된다는 것입니다.
그렇게 하면 디스크에 작은 구멍만 남고, LFS는 더 이상 큰 연속 공간에 세그먼트를 쓰기 어렵게 되어 성능이 급락합니다.&lt;/p&gt;
&lt;p&gt;따라서 Cleaner는 &lt;strong&gt;segment 단위&lt;/strong&gt;로 동작합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오래된 세그먼트 M개를 읽고&lt;/li&gt;
&lt;li&gt;live block만 골라&lt;/li&gt;
&lt;li&gt;N개의 새 세그먼트로 압축해 다시 쓰고 (N &amp;lt; M)&lt;/li&gt;
&lt;li&gt;기존 M 세그먼트를 통째로 free로 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 메커니즘: 블록이 live인지 어떻게 판단하는가&lt;/h2&gt;
&lt;p&gt;LFS는 각 세그먼트의 앞부분에 &lt;strong&gt;segment summary block&lt;/strong&gt;을 둡니다.
여기에 각 블록이&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어느 inode 번호에 속하는지&lt;/li&gt;
&lt;li&gt;파일의 몇 번째 블록 오프셋인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;를 기록합니다.&lt;/p&gt;
&lt;p&gt;블록 D가 디스크 주소 A에 있을 때, liveness 판정은 다음 아이디어입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;summary에서 inode 번호 N과 오프셋 T를 얻는다&lt;/li&gt;
&lt;li&gt;imap으로 N의 최신 inode 위치를 찾고 inode를 읽는다&lt;/li&gt;
&lt;li&gt;inode가 가리키는 T번째 블록 주소가 A와 같으면 live&lt;/li&gt;
&lt;li&gt;다르면 이미 더 최신 버전이 있으므로 garbage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;추가로 LFS는 truncation이나 delete 같은 경우를 빠르게 처리하기 위해 inode 버전 번호를 활용해 비교만으로 단축하는 최적화도 사용합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 정책: 언제 무엇을 청소할 것인가&lt;/h2&gt;
&lt;p&gt;Cleaner의 또 다른 축은 정책입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;언제 청소할지&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;주기적으로&lt;/li&gt;
&lt;li&gt;유휴 시간에&lt;/li&gt;
&lt;li&gt;또는 디스크가 거의 찼을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;어떤 세그먼트를 선택할지&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원 논문은 hot/cold 세그먼트 분리를 제안합니다&lt;/li&gt;
&lt;li&gt;hot은 계속 덮어써져서 시간이 지나면 더 많은 블록이 dead가 되므로, 너무 빨리 청소하면 손해가 큽니다&lt;/li&gt;
&lt;li&gt;cold는 상대적으로 안정적이어서, dead가 생기면 일찍 청소하는 편이 효율적일 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 크래시 복구&lt;/h2&gt;
&lt;p&gt;LFS에서도 쓰기 도중 크래시는 발생합니다.
크래시는 크게 두 경우에 문제가 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세그먼트를 쓰는 도중 크래시&lt;/li&gt;
&lt;li&gt;CR을 갱신하는 도중 크래시&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;11.1 CR 원자성 확보: CR을 2개 둔다&lt;/h3&gt;
&lt;p&gt;CR 갱신이 중간에 깨지면 시작점이 망가질 수 있습니다.
그래서 LFS는 CR을 디스크 양 끝에 2개 두고 번갈아 갱신합니다.
또한 timestamp를 이용한 헤더와 꼬리 블록으로 일관성을 검사해, 가장 최신이면서 일관된 CR을 선택합니다.&lt;/p&gt;
&lt;h3&gt;11.2 roll-forward: 마지막 checkpoint 이후를 복구한다&lt;/h3&gt;
&lt;p&gt;CR은 수십 초마다만 갱신되므로, 단순히 마지막 CR로 돌아가면 그 사이의 업데이트가 통째로 사라집니다.&lt;/p&gt;
&lt;p&gt;이를 줄이기 위해 LFS는 roll-forward를 수행합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;마지막 일관된 CR로부터 로그 끝을 찾고&lt;/li&gt;
&lt;li&gt;그 이후 세그먼트들을 순차적으로 훑으며&lt;/li&gt;
&lt;li&gt;유효한 업데이트를 재구성해 반영합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;12. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;핵심 철학&lt;/strong&gt;: 모든 업데이트를 append-only로 기록해 쓰기를 큰 순차 쓰기로 만든다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;segment와 write buffering&lt;/strong&gt;: 큰 contiguous write를 만들기 위한 메커니즘&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;imap&lt;/strong&gt;: 움직이는 inode를 추적하기 위한 indirection 계층&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CR&lt;/strong&gt;: 최신 imap 조각을 찾기 위한 고정 시작점&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner&lt;/strong&gt;: segment 단위로 live block만 재배치해 큰 연속 free 공간을 만든다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;liveness 판정&lt;/strong&gt;: segment summary block과 imap, inode를 이용해 주소 일치로 판단한다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;crash recovery&lt;/strong&gt;: 2개의 CR과 roll-forward로 일관성과 최신성 사이를 절충한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LFS의 아이디어는 copy-on-write 계열 파일 시스템과 스토리지 계층 전반에 영향을 남겼고, 오늘날에도 큰 쓰기와 공간 회수라는 문제는 여전히 유효합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;13. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Log-structured&lt;/code&gt;: 덮어쓰기를 피하고 로그 append로 상태를 갱신하는 설계&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Segment&lt;/code&gt;: 큰 순차 쓰기를 만들기 위한 기록 단위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Imap (Inode Map)&lt;/code&gt;: inode 번호에서 최신 inode 디스크 주소로의 매핑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Checkpoint Region (CR)&lt;/code&gt;: 최신 imap 조각 주소들을 저장하는 고정 시작점&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cleaner&lt;/code&gt;: live block만 재배치하고 segment 단위로 공간을 회수하는 백그라운드 작업&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Segment Summary Block&lt;/code&gt;: 각 블록의 inode 번호와 오프셋을 기록해 liveness 판단을 돕는 메타데이터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Roll-forward&lt;/code&gt;: 마지막 checkpoint 이후 로그를 스캔해 유효한 업데이트를 복구하는 기법&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-lfs.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 43: Log-structured File Systems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 42. Crash Consistency: FSCK and Journaling</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-42-file-journaling/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-42-file-journaling/</guid><description>시스템 크래시나 전원 차단 시 파일 시스템의 일관성을 유지하는 문제(Crash Consistency)와 이를 해결하기 위한 FSCK 및 저널링(Journaling, WAL) 기법을 상세히 알아봅니다.</description><pubDate>Tue, 10 Feb 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;우리는 지금까지 파일을 어떻게 디스크에 효율적으로 배치할지(FFS 등)를 배웠습니다. 하지만 파일 시스템 개발자를 가장 골치 아프게 하는 문제는 따로 있습니다. &lt;strong&gt;쓰기 도중에 크래시가 나면 무엇이 남는가&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;디스크는 업데이트를 블록 단위로 수행하지만, 파일 시스템 연산은 보통 여러 블록의 갱신을 필요로 합니다. 이때 각 블록 쓰기는 원자적일 수 있어도, 여러 블록에 걸친 업데이트 전체는 원자성이 보장되지 않습니다. 이 불일치가 바로 &lt;strong&gt;크래시 일관성(Crash Consistency)&lt;/strong&gt; 문제를 만듭니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 이 문제를 해결하는 대표적인 두 접근, &lt;strong&gt;fsck(사후 복구)&lt;/strong&gt; 와 &lt;strong&gt;저널링(사전 예방)&lt;/strong&gt; 을 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 문제 상황: append는 왜 위험한가&lt;/h2&gt;
&lt;p&gt;파일에 데이터 블록 하나를 추가하는 append를 생각해 봅시다. 논리적으로는 간단하지만, 디스크 수준에서는 최소 다음 블록 갱신이 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;데이터 비트맵(Data Bitmap)&lt;/strong&gt;: 새 블록을 사용 중으로 표시 $B$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;inode&lt;/strong&gt;: 파일 크기 증가 및 데이터 포인터 추가 $I$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 블록(Data Block)&lt;/strong&gt;: 실제 데이터 기록 $D$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 그림은 append 전의 단순화된 on-disk 상태를 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/on_disk_start.png&quot; alt=&quot;Append 예시: 초기 on-disk 상태&quot; /&gt;&lt;/p&gt;
&lt;p&gt;즉, 최소 3개의 블록 쓰기가 필요합니다. 문제는 이 중간 어딘가에서 크래시가 나면 디스크 위 상태가 어중간해질 수 있다는 점입니다.&lt;/p&gt;
&lt;h3&gt;1.1 크래시 시나리오와 결과&lt;/h3&gt;
&lt;p&gt;아래는 대표적인 조합을 직관적으로 정리한 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$D$만 쓰고 크래시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터는 디스크에 존재하지만 어떤 메타데이터도 그 블록을 가리키지 않음&lt;/li&gt;
&lt;li&gt;결과적으로 공간 누수(leak) 또는 고아 데이터가 됨&lt;/li&gt;
&lt;li&gt;메타데이터 관점에서는 상대적으로 덜 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$B$만 쓰고 크래시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비트맵은 사용 중이라고 표시하지만 어떤 inode도 그 블록을 참조하지 않음&lt;/li&gt;
&lt;li&gt;역시 공간 누수(leak)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$I$만 쓰고 크래시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode는 어떤 데이터 블록을 가리키지만, 그 블록에는 새 데이터가 아직 기록되지 않았을 수 있음&lt;/li&gt;
&lt;li&gt;결과적으로 쓰레기 데이터 노출 또는 이전 내용(stale data) 노출 가능&lt;/li&gt;
&lt;li&gt;사용자가 기대하지 않은 바이트가 파일 내용으로 보일 수 있어 심각&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$I$와 $B$만 쓰고 크래시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode가 특정 블록을 가리키고, 비트맵도 사용 중으로 표시됨&lt;/li&gt;
&lt;li&gt;겉으로는 일관적으로 보이지만, $D$가 안 써졌다면 내용은 여전히 쓰레기일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$I$는 됐는데 $B$가 안 된 경우도 위험&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode는 블록을 사용한다고 생각하지만, 비트맵은 free라고 남을 수 있음&lt;/li&gt;
&lt;li&gt;이후 다른 파일이 같은 블록을 재할당받아 데이터 오염이 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리가 원하는 것은 결국 하나입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세 업데이트 $B, I, D$는 &lt;strong&gt;모두 실행되거나&lt;/strong&gt;, 아니면 &lt;strong&gt;아예 실행되지 않아야&lt;/strong&gt; 한다&lt;br /&gt;
즉, all or nothing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 그림은 append가 정상 완료되었을 때의 목표 on-disk 상태를 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/on_disk_end.png&quot; alt=&quot;Append 예시: 목표 on-disk 상태&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 해결책 1: fsck(File System Checker)&lt;/h2&gt;
&lt;p&gt;옛 유닉스 시스템은 단순한 전략을 택했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;평소에는 빠르게 진행한다&lt;/li&gt;
&lt;li&gt;크래시가 나면 부팅 시점에 전체를 검사하고 고친다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 역할을 하는 도구가 &lt;strong&gt;fsck(file system checker)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;fsck는 디스크 전체를 스캔하면서 메타데이터 불일치를 찾아 복구합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;슈퍼블록 검사&lt;/strong&gt;: 파일 시스템 기본 정보 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;inode 스캔&lt;/strong&gt;: 각 inode가 가리키는 데이터 블록 수집&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비트맵 재구성/검사&lt;/strong&gt;: inode가 참조하는 블록이 비트맵에서도 사용 중인지 확인하고 불일치 수정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도달 가능성 검사&lt;/strong&gt;: 디렉터리 트리를 따라가며 도달 불가능한 inode를 찾아 &lt;code&gt;lost+found&lt;/code&gt;로 이동&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;링크 카운트 검사&lt;/strong&gt;: 실제 디렉터리 엔트리 수와 inode의 link count 일치 여부 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.1 fsck의 한계&lt;/h3&gt;
&lt;p&gt;fsck의 결정적 단점은 &lt;strong&gt;너무 느리다&lt;/strong&gt;는 점입니다.&lt;/p&gt;
&lt;p&gt;fsck는 본질적으로 디스크를 광범위하게 읽는 작업이고, 그 시간은 대체로 디스크 크기에 비례해 증가합니다. 즉, 스캔 시간은 보통 **선형(O(N))**에 가깝습니다. 문제는 디스크 용량이 커지는 속도가 매우 빨라, 현실적으로는 수 TB급에서 검사 시간이 길어져 운영 환경에서 부담이 커진다는 점입니다.&lt;/p&gt;
&lt;p&gt;그래서 현대 파일 시스템은 크래시 복구를 전체 스캔에 의존하지 않는 방향으로 발전합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 해결책 2: 저널링(Journaling), WAL&lt;/h2&gt;
&lt;p&gt;저널링은 DBMS에서 널리 쓰던 &lt;strong&gt;Write-Ahead Logging(WAL)&lt;/strong&gt; 개념을 파일 시스템에 적용한 것입니다.&lt;/p&gt;
&lt;p&gt;핵심 아이디어는 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실제 위치에 반영하기 전에, 변경 의도와 내용을 로그에 먼저 기록한다&lt;/li&gt;
&lt;li&gt;크래시가 나면 로그를 보고 복구한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 로그는 보통 파일 시스템 내부의 &lt;strong&gt;journal 영역&lt;/strong&gt;에 존재합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3.1 물리적 저널링(Physical Journaling)&lt;/h2&gt;
&lt;p&gt;물리적 저널링은 데이터와 메타데이터를 모두 저널에 기록합니다. 트랜잭션 단위로 보면 아래처럼 진행됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Journal Write&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TxB(트랜잭션 시작 표시) 기록&lt;/li&gt;
&lt;li&gt;갱신될 블록들 기록 (예: $B, I, D$)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Journal Commit&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TxE(트랜잭션 완료 표시) 기록&lt;/li&gt;
&lt;li&gt;TxE가 디스크에 안전하게 기록되면, 트랜잭션은 커밋된 것으로 간주&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Checkpoint&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;저널에 있던 블록들을 실제 위치(home location)로 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;해당 로그 구간을 재사용 가능하게 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;아래 그림은 위 과정을 타임라인으로 요약합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/42_1.png&quot; alt=&quot;Figure 42.1: Data Journaling Timeline&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.1.1 크래시가 나면 무엇을 하는가&lt;/h3&gt;
&lt;p&gt;복구는 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TxE가 없는 트랜잭션은 무시한다 (커밋 전이므로 버림)&lt;/li&gt;
&lt;li&gt;TxE가 있는 트랜잭션은 replay 한다 (저널 내용을 실제 위치에 반영)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 커밋 여부를 TxE로 판단하고, 커밋된 것만 재실행하면 all or nothing을 만족합니다.&lt;/p&gt;
&lt;h3&gt;3.1.2 단점&lt;/h3&gt;
&lt;p&gt;물리적 저널링은 $D$까지 로그에 쓰므로, 데이터는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;저널에 한 번&lt;/li&gt;
&lt;li&gt;실제 위치에 한 번&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;총 두 번 기록됩니다. 따라서 쓰기 비용이 커집니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 성능 개선: 메타데이터 저널링과 ordered mode&lt;/h2&gt;
&lt;p&gt;대부분의 상용 파일 시스템은 성능 때문에 데이터를 저널에 남기지 않고, &lt;strong&gt;메타데이터만 저널링&lt;/strong&gt;하는 방식을 사용합니다. 리눅스 계열에서는 이를 ordered mode로 설명하는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;핵심은 아래 한 줄입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터는 홈 위치에 쓰되, 메타데이터 커밋 전에 데이터가 먼저 디스크에 도달해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1 ordered mode의 절차&lt;/h3&gt;
&lt;p&gt;append 예시를 기준으로 순서를 정리하면 아래처럼 생각할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Write&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$D$를 실제 위치에 먼저 기록한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Journal Write&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메타데이터만 저널에 기록한다 (예: $B, I$)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Journal Commit&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TxE를 기록한다&lt;/li&gt;
&lt;li&gt;커밋 시점 이후에는 복구가 가능해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Checkpoint&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메타데이터를 실제 위치로 반영한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4.2 왜 순서가 중요한가&lt;/h3&gt;
&lt;p&gt;ordered mode가 막고 싶은 최악의 상황은 이것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메타데이터가 커밋되어 inode가 새 블록을 가리키게 됐는데&lt;/li&gt;
&lt;li&gt;데이터 $D$가 아직 디스크에 없고&lt;/li&gt;
&lt;li&gt;그 상태로 크래시가 나면&lt;/li&gt;
&lt;li&gt;복구 후 inode가 쓰레기 또는 stale data를 가리킬 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 ordered mode는 반드시 다음 제약을 만족해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$D$가 디스크에 안전하게 기록된 이후에만 $B, I$를 커밋한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 그림은 메타데이터 저널링의 타임라인과 순서 제약을 한 번에 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/42_2.png&quot; alt=&quot;Figure 42.2: Metadata Journaling Timeline&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.3 writeback mode는 무엇이 다른가&lt;/h3&gt;
&lt;p&gt;일부 모드는 데이터 $D$의 디스크 도달을 커밋과 강하게 묶지 않습니다. 이 경우 메타데이터 일관성은 유지할 수 있지만, 크래시 후 파일 내용이 쓰레기 또는 stale로 보일 가능성이 커집니다. 그래서 ordered mode는 안전성과 성능의 균형점으로 자주 선택됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 구현 관점의 디테일: 순환 로그와 배치&lt;/h2&gt;
&lt;h3&gt;5.1 순환 로그(Circular Log)&lt;/h3&gt;
&lt;p&gt;저널 공간은 무한하지 않습니다. 따라서 저널은 보통 원형 버퍼처럼 운영됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;커밋 및 체크포인트가 끝난 구간은 재사용&lt;/li&gt;
&lt;li&gt;로그 헤드와 테일을 관리하며 순환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 배치(Batching)&lt;/h3&gt;
&lt;p&gt;매번 작은 연산마다 트랜잭션을 커밋하면 TxE 쓰기, 장치 flush 등 오버헤드가 과도해집니다. 그래서 실제 구현은 여러 파일 시스템 연산을 하나의 트랜잭션으로 묶어 커밋합니다.&lt;/p&gt;
&lt;p&gt;예를 들어 파일 생성이 연속으로 발생하면 이를 묶어 커밋해, 커밋 오버헤드를 상각할 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;크래시 일관성&lt;/strong&gt;: 여러 블록 업데이트는 원자적이지 않아서, 크래시 시 파일 시스템이 망가질 수 있다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fsck&lt;/strong&gt;: 전체 스캔 기반 사후 복구. 용량이 커질수록 시간이 오래 걸려 운영 부담이 커진다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;저널링(WAL)&lt;/strong&gt;: 변경을 로그에 먼저 기록하고, 크래시 후 로그를 replay 하여 빠르게 복구한다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ordered mode&lt;/strong&gt;: 데이터는 홈 위치에 쓰고 메타데이터만 로깅하되, 데이터가 먼저 디스크에 도달한 뒤에만 커밋한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저널링 덕분에 우리는 더 이상 부팅 중 disk checking 화면을 몇 시간씩 바라보는 시스템에 머물 필요가 없어졌습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Crash Consistency&lt;/code&gt;: 크래시 후에도 파일 시스템이 모순 없는 상태로 복구되는 성질&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fsck&lt;/code&gt;: 파일 시스템 전역 스캔으로 불일치를 찾아 복구하는 도구&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Journaling (WAL)&lt;/code&gt;: 변경을 홈 위치에 반영하기 전에 로그에 먼저 기록하는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Transaction&lt;/code&gt;: all or nothing으로 처리되어야 하는 업데이트 묶음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Commit (TxE)&lt;/code&gt;: 트랜잭션이 완료되었음을 나타내는 로그 레코드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Checkpoint&lt;/code&gt;: 저널 내용을 실제 위치에 반영하는 과정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ordered mode&lt;/code&gt;: 데이터 선기록 후 메타데이터 커밋을 강제하는 저널링 모드&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-journaling.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 42: Crash Consistency: FSCK and Journaling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 41. 지역성과 FFS (Locality and The Fast File System)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-41-file-ffs/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-41-file-ffs/</guid><description>기존 유닉스 파일 시스템의 성능 문제를 해결하기 위해 디스크 구조를 인식하고 지역성(Locality)을 활용한 FFS(Fast File System)의 설계 원칙과 구현(실린더 그룹, 할당 정책)을 정리합니다.</description><pubDate>Tue, 10 Feb 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장에서 우리는 아주 단순한 파일 시스템인 &lt;code&gt;VSFS&lt;/code&gt;를 통해 파일 시스템의 기본 구조를 배웠습니다. 초기 유닉스 파일 시스템(Old UNIX)도 큰 틀에서는 비슷했습니다. 하지만 치명적인 문제가 있었습니다. &lt;strong&gt;너무 느렸다&lt;/strong&gt;는 점입니다.&lt;/p&gt;
&lt;p&gt;당시 측정 결과는 디스크 대역폭의 &lt;strong&gt;약 2%&lt;/strong&gt; 만 활용하는 수준이었습니다. 원인은 단순합니다. 파일 시스템이 디스크를 메모리처럼 취급하면서 데이터와 메타데이터를 디스크 전체에 흩뿌려 두었고, 그 결과 &lt;strong&gt;지역성(Locality)&lt;/strong&gt; 이 무너졌습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 이를 해결하기 위해 &lt;strong&gt;디스크의 물리적 비용(Seek, Rotation)을 의식&lt;/strong&gt;하고 설계한 &lt;strong&gt;FFS(Fast File System)&lt;/strong&gt; 를 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 문제의 본질: 디스크는 메모리가 아니다&lt;/h2&gt;
&lt;p&gt;디스크 I/O 비용은 보통 다음 세 항으로 쪼개 생각합니다.&lt;/p&gt;
&lt;p&gt;$$
T_{\mathrm{io}} = T_{\mathrm{seek}} + T_{\mathrm{rot}} + T_{\mathrm{xfer}}
\tag{1}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T_{\mathrm{seek}}$: 헤드 이동 시간&lt;/li&gt;
&lt;li&gt;$T_{\mathrm{rot}}$: 원하는 섹터가 헤드 아래로 올 때까지의 회전 지연&lt;/li&gt;
&lt;li&gt;$T_{\mathrm{xfer}}$: 실제 전송 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;초기 유닉스 파일 시스템이 느렸던 이유는, &lt;strong&gt;작은 전송량을 위해서도 $T_{\mathrm{seek}} + T_{\mathrm{rot}}$를 반복해서 지불&lt;/strong&gt;하도록 데이터가 배치되었기 때문입니다.&lt;/p&gt;
&lt;h3&gt;1.1 옛날 유닉스 파일 시스템의 구조적 문제&lt;/h3&gt;
&lt;p&gt;초기 파일 시스템은 대략 다음처럼 단순 배치였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;superblock&lt;/li&gt;
&lt;li&gt;inode 영역&lt;/li&gt;
&lt;li&gt;data 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 설계가 만들어낸 문제는 대표적으로 아래 세 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;긴 탐색 거리&lt;/strong&gt;
inode와 데이터가 물리적으로 멀리 떨어지기 쉬워, 파일을 읽을 때 메타데이터 접근과 데이터 접근 사이에 seek가 자주 발생합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;단편화(Fragmentation)&lt;/strong&gt;
free list 기반의 거친 관리로 연속 할당이 깨지기 쉽고, 순차 읽기조차 중간중간 seek를 유발합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;작은 블록 크기&lt;/strong&gt;
512B 같은 작은 블록은 전송 효율이 낮고, 같은 파일을 읽어도 더 많은 I/O를 발생시켜 $T_{\mathrm{seek}} + T_{\mathrm{rot}}$ 비중을 키웁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이제 목표는 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;관련 있는 것들을 물리적으로 가깝게 두어&lt;/strong&gt; $T_{\mathrm{seek}}$와 $T_{\mathrm{rot}}$를 줄인다&lt;/li&gt;
&lt;li&gt;전송 단위를 키워 $T_{\mathrm{xfer}}$가 지배하도록 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 해결책: 실린더 그룹(Cylinder Group)&lt;/h2&gt;
&lt;p&gt;FFS의 핵심 아이디어는 디스크를 &lt;strong&gt;실린더 그룹(Cylinder Group)&lt;/strong&gt; 으로 나누는 것입니다. 현대 파일 시스템의 &lt;strong&gt;block group&lt;/strong&gt;과 유사한 개념입니다.&lt;/p&gt;
&lt;p&gt;각 실린더 그룹은 작은 파일 시스템처럼 필요한 메타데이터와 데이터 영역을 함께 포함합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;superblock의 복사본(또는 요약 정보)&lt;/li&gt;
&lt;li&gt;inode bitmap, data bitmap&lt;/li&gt;
&lt;li&gt;inode table&lt;/li&gt;
&lt;li&gt;data blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;핵심 요약&lt;/strong&gt;&lt;br /&gt;
FFS는 파일을 구성하는 메타데이터(inode)와 데이터 블록을 &lt;strong&gt;가능한 한 같은 그룹 안에&lt;/strong&gt; 배치해, 긴 seek를 줄이고 지역성을 살립니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 성능을 결정하는 핵심: 배치 정책(Allocation Policy)&lt;/h2&gt;
&lt;p&gt;실린더 그룹만 나눠놓고 아무렇게나 할당하면 의미가 없습니다. FFS의 성능은 어떤 그룹에 무엇을 둘지 결정하는 &lt;strong&gt;정책&lt;/strong&gt;에서 나옵니다.&lt;/p&gt;
&lt;h3&gt;3.1 디렉터리 배치: 분산을 통해 균형을 만든다&lt;/h3&gt;
&lt;p&gt;디렉터리는 파일들의 부모입니다. 디렉터리가 한 그룹에 몰리면, 그 그룹은 곧 inode와 데이터 블록이 부족해져서 이후 파일들이 다른 그룹으로 밀려나고 지역성이 깨집니다.&lt;/p&gt;
&lt;p&gt;따라서 FFS는 디렉터리를 &lt;strong&gt;분산&lt;/strong&gt;시키는 정책을 택합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 수가 상대적으로 적고&lt;/li&gt;
&lt;li&gt;free inode가 많은 그룹을 선호&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이것은 단순히 디렉터리를 흩뿌리는 것이 아니라, 미래의 파일 생성까지 고려한 &lt;strong&gt;load balancing&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;3.2 파일 배치: 부모 디렉터리와 가깝게 둔다&lt;/h3&gt;
&lt;p&gt;파일은 보통 다음 패턴으로 접근됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 탐색으로 inode를 찾고&lt;/li&gt;
&lt;li&gt;inode를 읽은 뒤&lt;/li&gt;
&lt;li&gt;그 inode가 가리키는 데이터 블록을 읽는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 메타데이터와 데이터는 함께 움직입니다. 그래서 FFS는 다음을 기본 규칙으로 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 inode는 &lt;strong&gt;부모 디렉터리와 같은 그룹&lt;/strong&gt;에 둔다&lt;/li&gt;
&lt;li&gt;파일 데이터 블록은 &lt;strong&gt;그 inode와 같은 그룹&lt;/strong&gt;에 두고, 가능하면 연속적으로 배치한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 정책은 디렉터리 워크로드(&lt;code&gt;ls&lt;/code&gt;, 디렉터리 아래 파일들 연속 접근)와 파일 열기 워크로드(open 후 read)를 동시에 개선합니다. 이것이 FFS가 말하는 &lt;strong&gt;공간 지역성(Spatial Locality)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3.3 워크로드 시나리오로 보는 정책 효과&lt;/h2&gt;
&lt;p&gt;정책의 가치는 워크로드에서 드러납니다. 아래 두 시나리오를 보면 왜 FFS가 디렉터리와 파일을 다르게 다루는지 감각이 잡힙니다.&lt;/p&gt;
&lt;h3&gt;3.3.1 시나리오 A: ls -la 같은 메타데이터 중심 워크로드&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ls -la&lt;/code&gt;는 단순히 디렉터리 엔트리 목록만 읽는 것이 아니라, 각 파일의 권한, 소유자, 시간 등을 보여주기 위해 inode를 대량으로 읽습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 데이터 블록을 읽고&lt;/li&gt;
&lt;li&gt;각 엔트리의 inode를 따라가며 메타데이터를 읽는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 inode들이 디렉터리와 멀리 흩어져 있으면, inode마다 seek가 유발되어 $T_{\mathrm{seek}} + T_{\mathrm{rot}}$가 반복됩니다.&lt;/p&gt;
&lt;p&gt;FFS의 기본 규칙은 여기서 효과가 큽니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부모 디렉터리와 같은 그룹에 파일 inode를 배치&lt;/li&gt;
&lt;li&gt;결과적으로 한 디렉터리의 inode들이 같은 그룹 근처에 모임&lt;/li&gt;
&lt;li&gt;inode read가 더 연속적으로 진행되어 seek가 줄어듦&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, ls -la는 파일 데이터가 아니라 &lt;strong&gt;메타데이터 지역성&lt;/strong&gt;에서 이득을 봅니다.&lt;/p&gt;
&lt;h3&gt;3.3.2 시나리오 B: open 후 read 같은 데이터 중심 워크로드&lt;/h3&gt;
&lt;p&gt;일반적인 파일 열기 패턴은 다음 순서를 가집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 탐색으로 inode를 찾고&lt;/li&gt;
&lt;li&gt;inode를 읽고&lt;/li&gt;
&lt;li&gt;데이터 블록을 읽는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;inode와 데이터가 서로 멀면, open 직후 read에서 바로 큰 seek가 발생합니다. FFS는 다음을 보장하려 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode와 데이터 블록을 같은 그룹에 배치&lt;/li&gt;
&lt;li&gt;가능하면 데이터 블록을 연속 배치&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 작은 파일이 많은 워크로드에서, inode 접근과 데이터 접근이 물리적으로 가까워져 open-read 경로가 빨라집니다.&lt;/p&gt;
&lt;h3&gt;3.3.3 시나리오 C: create 폭탄, 예를 들어 untar, 빌드 트리 생성&lt;/h3&gt;
&lt;p&gt;압축 해제나 빌드처럼 다음 작업이 반복되는 워크로드가 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 생성&lt;/li&gt;
&lt;li&gt;작은 파일 다량 생성&lt;/li&gt;
&lt;li&gt;같은 디렉터리 아래에 파일이 쏟아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;만약 디렉터리를 한 그룹에 몰아두면, 그 그룹은 빠르게 고갈되고 다음 문제가 생깁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리와 파일 inode, 데이터가 서로 다른 그룹으로 튐&lt;/li&gt;
&lt;li&gt;같은 디렉터리 아래 파일인데도 locality가 깨짐&lt;/li&gt;
&lt;li&gt;결과적으로 나중에 그 디렉터리를 스캔할 때 seek가 폭증&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 FFS는 디렉터리를 분산해 그룹별로 균형을 만들고, 그 위에 파일을 디렉터리 근처로 붙이는 전략을 택합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 예외 처리: 대용량 파일(Large Files)&lt;/h2&gt;
&lt;p&gt;위 규칙을 그대로 적용하면, 거대한 파일 하나가 어떤 실린더 그룹의 data blocks를 대부분 소비해버릴 수 있습니다. 그러면 그 그룹에는 이후 파일을 위한 공간이 부족해져, 다른 파일들이 먼 그룹으로 밀려나고 지역성이 무너집니다.&lt;/p&gt;
&lt;p&gt;그래서 FFS는 &lt;strong&gt;대용량 파일 예외&lt;/strong&gt;를 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작은 파일은 한 그룹 안에 최대한 묶어 locality를 얻고&lt;/li&gt;
&lt;li&gt;충분히 큰 파일은 어느 시점부터 &lt;strong&gt;다른 그룹으로 분산&lt;/strong&gt;한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 중요한 것은 분산의 단위입니다. 너무 자주 그룹을 바꾸면 $T_{\mathrm{seek}}$가 폭증합니다. 따라서 FFS는 일정 크기 이상의 &lt;strong&gt;청크(chunk)&lt;/strong&gt; 단위로 옮겨가며, 아래 조건이 만족되도록 설계합니다.&lt;/p&gt;
&lt;p&gt;$$
T_{\mathrm{xfer}}(\text{chunk})
\gg
T_{\mathrm{seek}} + T_{\mathrm{rot}}
\tag{2}
$$&lt;/p&gt;
&lt;p&gt;즉, 한 번 이동했으면 충분히 오래 순차 전송을 해서, 이동 비용을 상각(amortize)해야 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4.1 상각 관점에서의 청크 크기&lt;/h2&gt;
&lt;p&gt;FFS 문맥에서 청크 크기를 직관적으로 잡아보는 데 도움이 되는 그림이 PDF의 Figure 41.2입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/41_2.png&quot; alt=&quot;Figure 41.2: Amortization: How Big Do Chunks Have To Be?&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그림이 말하는 직관은 다음입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원하는 대역폭이 peak에 가까울수록 필요한 청크 크기는 로그 스케일로 커진다&lt;/li&gt;
&lt;li&gt;단지 약간 더 높은 효율을 얻기 위해 청크 크기가 급격히 커질 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 기타 개선 사항&lt;/h2&gt;
&lt;h3&gt;5.1 큰 블록과 작은 파일: fragments&lt;/h3&gt;
&lt;p&gt;블록을 크게 잡으면 순차 전송 효율은 좋아지지만, 작은 파일에서 내부 단편화가 커집니다. 예를 들어 4KiB 블록에 1KiB 파일을 저장하면 3KiB가 낭비됩니다.&lt;/p&gt;
&lt;p&gt;FFS는 이를 완화하기 위해 &lt;strong&gt;fragments&lt;/strong&gt;를 도입합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;큰 단위로는 4KiB 블록을 사용해 전송 효율을 얻고&lt;/li&gt;
&lt;li&gt;작은 파일은 더 작은 fragment 단위(예: 512B)로 저장해 공간 낭비를 줄인다&lt;/li&gt;
&lt;li&gt;파일이 커지면 fragment들을 모아 full block으로 승격시키는 방식으로 관리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이로써 성능과 공간 효율 사이의 트레이드오프를 완화합니다.&lt;/p&gt;
&lt;h3&gt;5.2 파라미터화(Parameterization)와 디스크 인식&lt;/h3&gt;
&lt;p&gt;FFS는 디스크의 물리적 특성을 반영하기 위해 다양한 파라미터를 활용했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랙, 실린더 배치&lt;/li&gt;
&lt;li&gt;회전 지연을 고려한 배치&lt;/li&gt;
&lt;li&gt;연속 블록 접근에서 섹터를 놓치지 않기 위한 스큐(skew)와 유사한 기법&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 디스크 인식 최적화는 현대 디스크, 컨트롤러(큰 캐시, 내부 재배치, NCQ 등)에서는 의미가 약해진 부분도 있지만, 중요한 메시지는 남습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 시스템은 저장장치의 비용 구조를 이해하고 설계되어야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 요약&lt;/h2&gt;
&lt;p&gt;FFS는 파일 시스템 설계의 교과서적인 사례입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;문제 인식&lt;/strong&gt;: 디스크 I/O는 $T_{\mathrm{seek}} + T_{\mathrm{rot}}$가 지배할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;구조 변경&lt;/strong&gt;: 실린더 그룹으로 나눠, 메타데이터와 데이터를 가까이 두는 단위를 만든다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정책 적용&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리는 분산 배치해 균형을 유지한다&lt;/li&gt;
&lt;li&gt;파일 inode와 데이터는 부모 디렉터리와 가깝게 두어 지역성을 얻는다&lt;/li&gt;
&lt;li&gt;대용량 파일은 청크 단위로 분산해 한 그룹 고갈을 방지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;추가 개선&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;fragments로 작은 파일의 공간 낭비를 줄인다&lt;/li&gt;
&lt;li&gt;디스크 특성을 반영하는 파라미터화로 최적화한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장에서는 성능만큼 중요한 주제인 &lt;strong&gt;크래시 일관성(Consistency)&lt;/strong&gt; 과 이를 위한 &lt;strong&gt;저널링(Journaling)&lt;/strong&gt; 을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Locality&lt;/code&gt;: 접근되는 데이터가 시간적, 공간적으로 뭉치는 성질&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cylinder Group&lt;/code&gt;: 관련 메타데이터와 데이터를 함께 배치하기 위한 디스크 분할 단위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fragmentation&lt;/code&gt;: 연속성이 깨져 추가 seek를 유발하는 외부 단편화, 블록 내부 낭비인 내부 단편화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Amortization&lt;/code&gt;: 큰 오버헤드를 큰 작업량으로 나눠 단위 비용을 작게 만드는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bitmap&lt;/code&gt;: 할당 여부를 비트로 관리하는 자료구조&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fragment&lt;/code&gt;: 작은 파일의 공간 효율을 위해 도입한 서브 블록 단위&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-ffs.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 41: Locality and The Fast File System&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 40. File System Implementation</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-40-file-implementation/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-40-file-implementation/</guid><description>VSFS(Very Simple File System)라는 학습용 파일 시스템 구현을 통해, on-disk 자료구조(superblock, bitmap, inode table)와 open/read/write가 유발하는 실제 I/O 흐름을 구현 관점에서 추적합니다.</description><pubDate>Mon, 09 Feb 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 장에서 우리는 &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;fsync&lt;/code&gt; 같은 파일 시스템 API를 사용자 관점에서 정리했습니다. 이번 장에서는 한 단계 내려가서, 운영체제가 그 API들을 &lt;strong&gt;디스크 위의 어떤 자료구조&lt;/strong&gt;로 구현하고, 각 시스템콜이 &lt;strong&gt;어떤 블록을 어떤 순서로&lt;/strong&gt; 읽고 쓰는지 추적합니다.&lt;/p&gt;
&lt;p&gt;OSTEP는 이를 위해 &lt;strong&gt;VSFS(Very Simple File System)&lt;/strong&gt; 라는 학습용 파일 시스템을 사용합니다. VSFS는 단순하지만, 아래 감각을 한 번에 잡게 해줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메타데이터가 어디에 놓이고, 왜 그 배치가 중요한가&lt;/li&gt;
&lt;li&gt;free space를 왜 별도 자료구조로 관리하는가&lt;/li&gt;
&lt;li&gt;i-number 하나로 inode 위치를 계산하는 방식&lt;/li&gt;
&lt;li&gt;경로 탐색과 메타데이터 갱신 때문에 I/O가 왜 폭증하는가&lt;/li&gt;
&lt;li&gt;이 비용을 줄이기 위해 캐싱과 버퍼링이 왜 필수인가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 파일 시스템을 보는 두 축&lt;/h2&gt;
&lt;p&gt;파일 시스템을 이해하는 가장 좋은 정신 모델은 두 축입니다.&lt;/p&gt;
&lt;h3&gt;1.1 on-disk 자료구조&lt;/h3&gt;
&lt;p&gt;디스크를 블록 배열로 보고, 그 위에 &lt;strong&gt;데이터와 메타데이터&lt;/strong&gt;를 어떤 구조로 올릴지 정합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 데이터는 어디에 두는가&lt;/li&gt;
&lt;li&gt;inode 같은 메타데이터는 어디에 두는가&lt;/li&gt;
&lt;li&gt;어떤 블록이 free인지 누가 추적하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 접근 경로&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;open()&lt;/code&gt;, &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt; 요청이 들어오면 &lt;strong&gt;어떤 블록들을 어떤 순서로&lt;/strong&gt; 접근하는지 따져봅니다.&lt;/p&gt;
&lt;p&gt;이 장의 핵심은 1.2입니다. 사용자 데이터보다 &lt;strong&gt;메타데이터 I/O가 먼저 터지는 구조&lt;/strong&gt;가 흔하고, 그게 파일 시스템 성능을 지배합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. VSFS의 전체 레이아웃&lt;/h2&gt;
&lt;p&gt;VSFS는 디스크를 블록 단위로 쪼갠 뒤, 블록 배열 위에 파일 시스템 자료구조를 배치합니다. 예시에서 블록 크기는 $$B_{\mathrm{blk}} = 4,\mathrm{KiB}$$ 입니다.&lt;/p&gt;
&lt;h3&gt;2.1 장난감 파티션: 64 blocks&lt;/h3&gt;
&lt;p&gt;파티션 크기를 &lt;strong&gt;64 블록&lt;/strong&gt;으로 두면, 블록 주소는 0부터 63까지입니다. VSFS는 마지막 56블록을 data region으로 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;data region: 블록 8–63 (총 56블록)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;앞부분은 메타데이터용으로 고정 배치합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구역&lt;/th&gt;
&lt;th&gt;블록 범위&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;superblock&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;전체 메타정보, magic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inode bitmap&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;inode 할당 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;data bitmap&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;데이터 블록 할당 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inode table&lt;/td&gt;
&lt;td&gt;3–7&lt;/td&gt;
&lt;td&gt;inode 배열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;data region&lt;/td&gt;
&lt;td&gt;8–63&lt;/td&gt;
&lt;td&gt;파일, 디렉터리 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 표가 이후 타임라인(그림 40.3, 40.4)을 이해하는 좌표계입니다.&lt;/p&gt;
&lt;h3&gt;2.2 inode table&lt;/h3&gt;
&lt;p&gt;파일마다 inode가 하나씩 필요합니다. VSFS는 inode들을 디스크에 &lt;strong&gt;배열&lt;/strong&gt;로 저장하며, 이 영역을 inode table이라 부릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode 크기 $$B_{\mathrm{inode}} = 256,\mathrm{B}$$&lt;/li&gt;
&lt;li&gt;한 블록(4KiB)에 들어가는 inode 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
N_{\mathrm{ipb}} = \frac{B_{\mathrm{blk}}}{B_{\mathrm{inode}}}
= \frac{4096}{256} = 16
\tag{1}
$$&lt;/p&gt;
&lt;p&gt;inode table을 5블록(블록 3–7)으로 잡으면 총 inode 수는 $$5 \times 16 = 80$$ 입니다. 즉, 이 파티션에서 만들 수 있는 파일 수의 상한도 대략 80개입니다.&lt;/p&gt;
&lt;h3&gt;2.3 free space 추적: bitmap 2개&lt;/h3&gt;
&lt;p&gt;파일을 만들고 블록을 할당하려면, 어떤 inode나 데이터 블록이 free인지 알아야 합니다. VSFS는 bitmap을 씁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode bitmap: 블록 1&lt;/li&gt;
&lt;li&gt;data bitmap: 블록 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 비트가 0이면 free, 1이면 in-use입니다. 예시 규모에서는 bitmap이 과하게 크지만, 단순화를 위해 bitmap을 블록 하나로 통째로 둡니다.&lt;/p&gt;
&lt;h3&gt;2.4 superblock&lt;/h3&gt;
&lt;p&gt;남은 블록 0은 superblock입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode 수, data 블록 수&lt;/li&gt;
&lt;li&gt;inode table 시작 블록 번호&lt;/li&gt;
&lt;li&gt;data region 시작 블록 번호&lt;/li&gt;
&lt;li&gt;파일 시스템 식별용 magic number 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;마운트 시 운영체제는 superblock을 먼저 읽고, 이후 필요한 on-disk 구조를 정확히 찾아갈 수 있게 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. inode: 파일의 핵심 메타데이터&lt;/h2&gt;
&lt;p&gt;inode는 파일 이름을 제외한 대부분의 메타데이터를 담는 구조입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;권한, 소유자, 시간 정보&lt;/li&gt;
&lt;li&gt;크기&lt;/li&gt;
&lt;li&gt;데이터 블록 위치(포인터)&lt;/li&gt;
&lt;li&gt;링크 수 등&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1 i-number로 inode 위치 계산하기&lt;/h3&gt;
&lt;p&gt;inode는 디스크의 inode table에 배열로 저장됩니다. 따라서 i-number $i$ 만 알면 inode 위치를 계산할 수 있습니다.&lt;/p&gt;
&lt;p&gt;inode table 시작 블록을 $b_{\mathrm{it}}$, 블록당 inode 수를 $N_{\mathrm{ipb}}$ 라 두면, i-number $i$ 의 inode가 들어있는 블록과 블록 내 오프셋은&lt;/p&gt;
&lt;p&gt;$$
b(i) = b_{\mathrm{it}} + \left\lfloor \frac{i}{N_{\mathrm{ipb}}} \right\rfloor,\qquad
o(i) = i \bmod N_{\mathrm{ipb}}
\tag{2}
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;p&gt;이 식이 의미하는 바는 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pathname 탐색으로 i-number만 알아내면&lt;/li&gt;
&lt;li&gt;inode table에서 그 inode를 읽어오는 위치는 계산으로 결정된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, i-number는 파일 시스템 내부에서 inode를 가리키는 안정적인 핸들입니다.&lt;/p&gt;
&lt;h3&gt;3.2 multi-level index: direct + indirect&lt;/h3&gt;
&lt;p&gt;inode 설계의 핵심 결정 중 하나는 데이터 블록을 어떻게 가리키는가입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;direct pointer: 데이터 블록을 직접 가리킴&lt;/li&gt;
&lt;li&gt;indirect pointer: 포인터 배열 블록을 가리킴&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;VSFS 예시는 아래 구조를 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;direct pointer 12개&lt;/li&gt;
&lt;li&gt;single indirect pointer 1개&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;블록 크기 $B_{\mathrm{blk}} = 4,\mathrm{KiB}$, 포인터 크기 $B_{\mathrm{ptr}} = 4,\mathrm{B}$ 이면, 간접 블록 하나가 담는 포인터 수는&lt;/p&gt;
&lt;p&gt;$$
N_{\mathrm{ptr}} = \frac{B_{\mathrm{blk}}}{B_{\mathrm{ptr}}}
= \frac{4096}{4} = 1024
\tag{3}
$$&lt;/p&gt;
&lt;p&gt;따라서 single indirect까지 포함한 최대 파일 크기는&lt;/p&gt;
&lt;p&gt;$$
B_{\max}
= (12 + 1024)\cdot 4,\mathrm{KiB}
= 4144,\mathrm{KiB}
\tag{4}
$$&lt;/p&gt;
&lt;p&gt;입니다.&lt;/p&gt;
&lt;p&gt;double, triple indirect는 더 큰 파일을 지원하기 위한 일반화지만, 이 장의 VSFS 예시는 single indirect만으로도 포인터 계층의 핵심을 충분히 보여줍니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 디렉터리도 파일이다&lt;/h2&gt;
&lt;p&gt;VSFS에서 디렉터리는 특별한 타입의 파일입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 inode는 inode table에 있음&lt;/li&gt;
&lt;li&gt;디렉터리 데이터 블록은 data region에 있음&lt;/li&gt;
&lt;li&gt;데이터 블록 내용은 name → i-number 매핑 리스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;VSFS의 디렉터리 엔트리는 &lt;code&gt;inum&lt;/code&gt;, &lt;code&gt;reclen&lt;/code&gt;, &lt;code&gt;strlen&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; 같은 필드로 표현되며, 각 디렉터리는 항상 &lt;code&gt;.&lt;/code&gt; 과 &lt;code&gt;..&lt;/code&gt; 엔트리를 포함합니다.&lt;/p&gt;
&lt;p&gt;VSFS 같은 단순 설계에서는 디렉터리 엔트리를 선형 검색합니다. 따라서 디렉터리가 커질수록 경로 탐색 비용이 증가합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Access Path: open, read, write가 만드는 I/O&lt;/h2&gt;
&lt;p&gt;이제 진짜 핵심입니다. 아래 표들은 &lt;strong&gt;캐시가 비어있는 최악 조건&lt;/strong&gt;을 가정한 I/O 카운트입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;page cache, inode cache가 비어있음&lt;/li&gt;
&lt;li&gt;필요한 메타데이터는 디스크에서 직접 읽어야 함&lt;/li&gt;
&lt;li&gt;atime 갱신이 켜져 있어 &lt;code&gt;read()&lt;/code&gt;가 inode write를 유발할 수 있음&lt;/li&gt;
&lt;li&gt;디렉터리 데이터가 예시처럼 1블록에 들어간다고 가정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현실 시스템에서는 캐시 hit, atime 정책, read-ahead 등에 의해 I/O는 크게 줄어들 수 있지만, 접근 경로 자체는 그대로입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;5.1 읽기: &lt;code&gt;/foo/bar&lt;/code&gt;를 open하고 12 KiB를 read&lt;/h3&gt;
&lt;p&gt;Figure 40.3은 &lt;code&gt;/foo/bar&lt;/code&gt;를 &lt;code&gt;open(O_RDONLY)&lt;/code&gt; 후 3블록(12 KiB)을 읽는 타임라인입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/40_3.png&quot; alt=&quot;Figure 40.3: File Read Timeline&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5.1.1 open에서 경로 탐색이 만든 read 누적&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;open&lt;/code&gt;은 pathname을 따라 내려가며 inode와 디렉터리 데이터를 읽습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 traversal은 루트 디렉터리에서 시작&lt;/li&gt;
&lt;li&gt;루트는 부모가 없으므로 루트 inode 번호는 파일 시스템이 마운트 시점에 알고 있어야 함&lt;/li&gt;
&lt;li&gt;많은 UNIX 계열에서 루트 inode 번호는 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 &lt;code&gt;/foo/bar&lt;/code&gt;를 열기 위해, 대략 아래 블록 read가 누적됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;루트 inode read&lt;/li&gt;
&lt;li&gt;루트 디렉터리 데이터 read&lt;/li&gt;
&lt;li&gt;&lt;code&gt;foo&lt;/code&gt; inode read&lt;/li&gt;
&lt;li&gt;&lt;code&gt;foo&lt;/code&gt; 디렉터리 데이터 read&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bar&lt;/code&gt; inode read&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pathname 레벨이 하나 늘 때마다 최소 inode 1회 + 디렉터리 데이터 1회 read가 추가된다고 보면 됩니다.&lt;/p&gt;
&lt;h4&gt;5.1.2 read는 bitmap을 보지 않는다&lt;/h4&gt;
&lt;p&gt;단순 읽기는 allocation 구조(bitmap)를 보지 않습니다. bitmap은 &lt;strong&gt;할당이 필요할 때만&lt;/strong&gt; 접근됩니다.&lt;/p&gt;
&lt;h4&gt;5.1.3 I/O 카운트 요약표&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시스템콜&lt;/th&gt;
&lt;th&gt;디스크 I/O 구성&lt;/th&gt;
&lt;th&gt;읽기 횟수&lt;/th&gt;
&lt;th&gt;쓰기 횟수&lt;/th&gt;
&lt;th&gt;합계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;open(/foo/bar)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;루트 inode + 루트 dir data + foo inode + foo dir data + bar inode&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read()&lt;/code&gt; 1회 (블록 1개)&lt;/td&gt;
&lt;td&gt;bar inode read + data block read + bar inode write (atime)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read()&lt;/code&gt; 3회 (총 12 KiB)&lt;/td&gt;
&lt;td&gt;위 패턴 반복&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;전체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;open + read(3 blocks)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;핵심은 하나입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터 3블록을 읽는 작업에서, 데이터 I/O(3 reads)보다 메타데이터 I/O가 더 많아질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;5.2 생성과 쓰기: create + 4 KiB write 3회&lt;/h3&gt;
&lt;p&gt;Figure 40.4는 &lt;code&gt;/foo/bar&lt;/code&gt;를 생성하고 4 KiB write를 3번 수행하는 타임라인입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/40_4.png&quot; alt=&quot;Figure 40.4: File Creation Timeline&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5.2.1 create 자체가 이미 무겁다&lt;/h4&gt;
&lt;p&gt;파일 생성은 아래를 동시에 해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새 inode 할당&lt;/li&gt;
&lt;li&gt;부모 디렉터리에 엔트리 추가&lt;/li&gt;
&lt;li&gt;부모 디렉터리 inode 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;디렉터리가 커져서 새 데이터 블록이 필요해지면, data bitmap 갱신까지 추가됩니다.&lt;/p&gt;
&lt;h4&gt;5.2.2 Figure 40.4 create 10 I/O를 표로 쪼개기&lt;/h4&gt;
&lt;p&gt;그림 40.4에서 &lt;code&gt;create(/foo/bar)&lt;/code&gt;는 총 10 I/O를 유발합니다. 타임라인을 그대로 분해하면 다음처럼 정리됩니다.&lt;/p&gt;
&lt;h5&gt;A. 경로 탐색: &lt;code&gt;/foo&lt;/code&gt;까지 내려가기&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순서&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;I/O&lt;/th&gt;
&lt;th&gt;읽기&lt;/th&gt;
&lt;th&gt;쓰기&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;루트 inode&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;루트 dir data&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo&lt;/code&gt; inode&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo&lt;/code&gt; dir data&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소계&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;B. create 메타데이터 갱신&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순서&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;I/O&lt;/th&gt;
&lt;th&gt;읽기&lt;/th&gt;
&lt;th&gt;쓰기&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;inode bitmap&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;inode bitmap&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo&lt;/code&gt; dir data&lt;/td&gt;
&lt;td&gt;write (엔트리 추가)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo&lt;/code&gt; inode&lt;/td&gt;
&lt;td&gt;write (디렉터리 inode 갱신)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bar&lt;/code&gt; inode block&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bar&lt;/code&gt; inode block&lt;/td&gt;
&lt;td&gt;write (RMW로 새 inode 초기화)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소계&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;따라서 create 전체는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;읽기 6회&lt;/li&gt;
&lt;li&gt;쓰기 4회&lt;/li&gt;
&lt;li&gt;합계 10회&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;로 정리됩니다.&lt;/p&gt;
&lt;p&gt;여기서 자주 놀라는 부분이 하나 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bar&lt;/code&gt; inode는 아직 초기화도 안 됐는데 read가 왜 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이는 inode가 작고(예: 256B), inode들이 더 큰 블록(예: 4KiB) 안에 같이 들어있기 때문입니다. 새 inode만 바꾸려면 블록 단위로 read-modify-write가 필요하고, 그 read가 타임라인에 나타납니다.&lt;/p&gt;
&lt;h4&gt;5.2.3 allocating write 1회는 5 I/O&lt;/h4&gt;
&lt;p&gt;이제 파일에 새 블록을 할당하며 쓰는 allocating write는 논리적으로 &lt;strong&gt;5 I/O&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순서&lt;/th&gt;
&lt;th&gt;갱신 대상&lt;/th&gt;
&lt;th&gt;I/O&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;data bitmap&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;data bitmap&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;파일 inode&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;파일 inode&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;데이터 블록&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Figure 40.4의 &lt;code&gt;write()&lt;/code&gt; 3회는 이 패턴이 그대로 3번 반복됩니다.&lt;/p&gt;
&lt;h4&gt;5.2.4 전체 합산&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업&lt;/th&gt;
&lt;th&gt;합계 I/O&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;create&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allocating write 3회&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;전체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이 수치가 말하는 바는 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자 데이터 12 KiB를 쓰는 작업이 메타데이터 갱신 때문에 &lt;strong&gt;25회의 디스크 I/O&lt;/strong&gt;를 만들 수 있음&lt;/li&gt;
&lt;li&gt;이것이 파일 시스템에서 흔히 말하는 &lt;strong&gt;write traffic 증가&lt;/strong&gt;, &lt;strong&gt;write amplification&lt;/strong&gt;의 출발점&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 느린 디스크를 버티는 방법: caching과 buffering&lt;/h2&gt;
&lt;p&gt;디스크 I/O는 느립니다. 그래서 파일 시스템은 DRAM을 적극 활용합니다.&lt;/p&gt;
&lt;h3&gt;6.1 읽기 캐싱: unified page cache&lt;/h3&gt;
&lt;p&gt;캐싱이 없다면, pathname 레벨이 하나 늘 때마다 open에 최소 2 reads가 더 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리 inode read&lt;/li&gt;
&lt;li&gt;디렉터리 data read&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;과거에는 파일 시스템 캐시를 메모리의 일정 비율(예: 대략 10퍼센트)로 고정해 두는 방식도 있었지만, 이는 유휴 캐시 페이지를 다른 용도로 재사용하기 어렵습니다.&lt;/p&gt;
&lt;p&gt;현대 OS는 VM 페이지와 파일 시스템 페이지를 통합한 &lt;strong&gt;unified page cache&lt;/strong&gt;로 동적으로 분배합니다. 그 결과, 자주 접근되는 inode와 디렉터리 데이터가 메모리에 남아, 5장의 메타데이터 read 폭탄을 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;h3&gt;6.2 쓰기 버퍼링: 지연 쓰기&lt;/h3&gt;
&lt;p&gt;read는 캐시로 완전히 필터링될 수 있지만, write는 영속성을 위해 결국 디스크로 가야 합니다. 다만 파일 시스템은 쓰기를 즉시 내리지 않고 버퍼링합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;batching: 작은 업데이트를 모아 I/O 감소&lt;/li&gt;
&lt;li&gt;scheduling: 더 좋은 순서로 디스크 I/O 배치&lt;/li&gt;
&lt;li&gt;avoidance: 곧바로 생성됐다가 삭제되는 데이터는 디스크 기록 자체를 피할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대신 크래시 시 유실 가능성이 있으므로, 영속성이 필요하면 &lt;code&gt;fsync()&lt;/code&gt;가 필요합니다. 많은 시스템이 수 초에서 수십 초 단위로 쓰기를 지연시키는 이유도 이 트레이드오프 때문입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;파일 시스템은 &lt;strong&gt;on-disk 자료구조&lt;/strong&gt;와 &lt;strong&gt;접근 경로&lt;/strong&gt;로 보면 가장 빠르게 이해됩니다.&lt;/li&gt;
&lt;li&gt;VSFS는 superblock, inode bitmap, data bitmap, inode table, data region으로 디스크를 구성합니다.&lt;/li&gt;
&lt;li&gt;inode는 i-number로 위치를 계산할 수 있고, direct 및 indirect 포인터로 큰 파일을 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open&lt;/code&gt;은 pathname traversal 때문에 메타데이터 read를 누적시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read&lt;/code&gt;는 데이터 블록 I/O 외에 정책에 따라 inode write(atime)를 동반할 수 있습니다.&lt;/li&gt;
&lt;li&gt;create는 경로 탐색 + inode 할당 + 디렉터리 갱신이 겹쳐 매우 비쌉니다.&lt;/li&gt;
&lt;li&gt;allocating write 1회는 data bitmap과 inode 갱신 때문에 &lt;strong&gt;5 I/O&lt;/strong&gt;를 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;이를 줄이기 위해 unified page cache와 write buffering이 사실상 필수입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VSFS&lt;/code&gt;: Very Simple File System. 학습용으로 단순화한 파일 시스템 모델.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Inode&lt;/code&gt;: 파일의 메타데이터와 데이터 블록 위치를 저장하는 자료구조.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Superblock&lt;/code&gt;: 파일 시스템의 전체 크기, 레이아웃 정보 등을 담은 블록.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bitmap&lt;/code&gt;: 블록의 할당 여부를 0과 1로 관리하는 자료구조.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Indirect Pointer&lt;/code&gt;: 데이터 블록이 아닌, 주소들을 담은 블록을 가리키는 포인터.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Direct Pointer&lt;/code&gt;: 실제 데이터 블록을 바로 가리키는 포인터.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-implementation.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 40: File System Implementation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 39. Files and Directories</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-39-file-intro/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-39-file-intro/</guid><description>영속성을 위한 운영체제의 핵심 추상화인 파일과 디렉터리의 개념, 그리고 이를 다루기 위한 시스템 콜(open, read, write, lseek, fork/dup 공유, 링크 등)을 정리합니다.</description><pubDate>Mon, 09 Feb 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지금까지 우리는 프로세스(CPU 가상화)와 주소 공간(메모리 가상화)을 다뤘습니다. 둘 다 전원이 꺼지면 사라지는 휘발성 자원입니다. 이제 데이터를 오래 보관하기 위한 운영체제의 세 번째 축인 &lt;strong&gt;영속성(Persistence)&lt;/strong&gt; 으로 들어갑니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 파일 시스템 구현(Implementation)으로 들어가기 전에, 사용자 관점에서 파일 시스템을 어떻게 사용하는지, 즉 &lt;strong&gt;인터페이스(API)&lt;/strong&gt; 를 먼저 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 파일과 디렉터리&lt;/h2&gt;
&lt;p&gt;파일 시스템은 저장 장치를 다루기 위해 두 가지 핵심 추상화를 제공합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;파일(File)&lt;/strong&gt;&lt;br /&gt;
읽고 쓸 수 있는 &lt;strong&gt;바이트의 선형 배열&lt;/strong&gt;입니다. 각 파일은 저수준 이름인 &lt;strong&gt;inode 번호(i-number)&lt;/strong&gt; 를 갖습니다. 운영체제는 파일이 텍스트인지 이미지인지 같은 의미를 해석하지 않고, 단지 바이트를 저장하고 다시 돌려주는 역할을 수행합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;디렉터리(Directory)&lt;/strong&gt;&lt;br /&gt;
사용자 친화적 이름을 inode 번호에 매핑하는 &lt;strong&gt;(이름, inode 번호)&lt;/strong&gt; 목록을 담는 특수한 파일입니다. 디렉터리도 inode 번호를 가지며, 디렉터리 안의 엔트리는 파일 또는 다른 디렉터리를 가리킵니다. 이 엔트리들이 연결되어 전체 디렉터리 트리(계층)를 이룹니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./images/39_1.png&quot; alt=&quot;Figure 39.1: An Example Directory Tree&quot; /&gt;&lt;/p&gt;
&lt;p&gt;확장자 &lt;code&gt;.c&lt;/code&gt;, &lt;code&gt;.jpg&lt;/code&gt; 같은 표기는 관례에 가깝고, 파일 시스템이 강제하지 않는 경우가 일반적입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 파일 생성과 입출력&lt;/h2&gt;
&lt;h3&gt;2.1 open과 file descriptor&lt;/h3&gt;
&lt;p&gt;파일을 다루려면 보통 &lt;code&gt;open()&lt;/code&gt;으로 시작합니다. &lt;code&gt;O_CREAT&lt;/code&gt;를 주면 파일이 없을 때 새로 만듭니다. 이때만 세 번째 인자 &lt;code&gt;mode&lt;/code&gt;가 의미를 갖습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int fd = open(&quot;foo&quot;,
              O_CREAT | O_WRONLY | O_TRUNC,
              S_IRUSR | S_IWUSR);
if (fd &amp;lt; 0) {
    /* error handling */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;open()&lt;/code&gt;이 성공하면 &lt;strong&gt;파일 디스크립터(file descriptor, fd)&lt;/strong&gt; 라는 정수를 돌려줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fd는 &lt;strong&gt;프로세스마다 독립&lt;/strong&gt;인 핸들입니다.&lt;/li&gt;
&lt;li&gt;내부적으로는 프로세스가 가진 열린 파일 테이블(보통 fd 테이블)의 인덱스처럼 관리됩니다.&lt;/li&gt;
&lt;li&gt;이후 &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt;, &lt;code&gt;fsync()&lt;/code&gt;, &lt;code&gt;close()&lt;/code&gt; 같은 호출의 대상으로 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;많은 시스템에서 프로세스는 시작 시점에 이미 3개의 fd를 열고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;: 표준 입력(stdin)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;: 표준 출력(stdout)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&lt;/code&gt;: 표준 에러(stderr)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 새로 &lt;code&gt;open()&lt;/code&gt;한 fd가 &lt;code&gt;3&lt;/code&gt;부터 시작하는 경우가 흔합니다.&lt;/p&gt;
&lt;p&gt;:::tip
strace로 시스템 콜 확인&lt;/p&gt;
&lt;p&gt;리눅스에서는 &lt;code&gt;strace&lt;/code&gt;로 프로그램이 어떤 시스템 콜을 쓰는지 확인할 수 있습니다.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strace -o trace.txt cat foo
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;2.2 read, write, close&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;read(fd, buf, size)&lt;/code&gt;는 파일에서 읽어 버퍼에 채우고, 실제로 읽은 바이트 수를 리턴합니다. EOF에서는 &lt;code&gt;0&lt;/code&gt;을 리턴합니다.&lt;br /&gt;
&lt;code&gt;write(fd, buf, size)&lt;/code&gt;는 버퍼 내용을 파일에 기록하고, 실제로 쓴 바이트 수를 리턴합니다.&lt;/p&gt;
&lt;p&gt;현실적으로는 다음을 항상 염두에 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read()&lt;/code&gt;/&lt;code&gt;write()&lt;/code&gt;는 요청한 바이트 수보다 &lt;strong&gt;적게&lt;/strong&gt; 처리하고 돌아올 수 있습니다.&lt;/li&gt;
&lt;li&gt;실패하면 &lt;code&gt;-1&lt;/code&gt;을 반환하고 원인은 &lt;code&gt;errno&lt;/code&gt;로 전달됩니다.&lt;/li&gt;
&lt;li&gt;따라서 일반 코드는 반환값을 확인하며 루프를 돕니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2.3 오프셋과 lseek&lt;/h3&gt;
&lt;p&gt;각 열린 파일은 커널이 관리하는 &lt;strong&gt;현재 오프셋(current offset)&lt;/strong&gt; 을 갖습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read()&lt;/code&gt; 또는 &lt;code&gt;write()&lt;/code&gt;로 &lt;code&gt;N&lt;/code&gt;바이트를 처리하면 오프셋이 &lt;code&gt;N&lt;/code&gt;만큼 증가합니다.&lt;/li&gt;
&lt;li&gt;임의 접근이 필요하면 &lt;code&gt;lseek()&lt;/code&gt;으로 오프셋을 바꿀 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

off_t off = lseek(fd, 0, SEEK_END);
if (off == (off_t) -1) {
    /* error handling */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중요한 포인트가 하나 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lseek()&lt;/code&gt;은 &lt;strong&gt;디스크 헤드를 직접 움직이지 않습니다.&lt;/strong&gt;&lt;br /&gt;
단지 커널 메모리에 있는 오프셋 값을 바꿉니다.&lt;br /&gt;
다만 이후 &lt;code&gt;read()&lt;/code&gt;/&lt;code&gt;write()&lt;/code&gt;가 파일의 먼 위치를 건드리면, 결과적으로 디스크 수준에서는 임의 접근이 늘어 탐색과 회전 비용이 커질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2.4 오픈 파일 테이블(Open File Table)&lt;/h3&gt;
&lt;p&gt;fd는 프로세스 내부의 fd 테이블 엔트리이고, 그 엔트리가 커널의 &lt;strong&gt;오픈 파일 테이블 엔트리(open file description)&lt;/strong&gt; 를 가리키는 형태로 구현되는 경우가 일반적입니다. 이 엔트리가 다음을 추적합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 inode를 가리키는지&lt;/li&gt;
&lt;li&gt;현재 오프셋&lt;/li&gt;
&lt;li&gt;접근 모드(읽기/쓰기)&lt;/li&gt;
&lt;li&gt;참조 카운트(refcount)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구조 덕분에 같은 파일을 두 번 &lt;code&gt;open()&lt;/code&gt;하면 각각 다른 오픈 파일 테이블 엔트리를 갖고, 오프셋도 독립적으로 움직입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 열린 파일 엔트리 공유: fork와 dup&lt;/h2&gt;
&lt;p&gt;오픈 파일 테이블 엔트리는 상황에 따라 &lt;strong&gt;공유&lt;/strong&gt;될 수 있습니다. 이때 핵심은 오프셋도 함께 공유된다는 점입니다.&lt;/p&gt;
&lt;h3&gt;3.1 fork는 오프셋을 공유한다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt;를 하면 자식은 부모의 fd 테이블을 복사하지만, 각 fd가 가리키는 &lt;strong&gt;오픈 파일 테이블 엔트리 자체는 공유&lt;/strong&gt;됩니다. 따라서 오프셋이 공유됩니다.&lt;/p&gt;
&lt;p&gt;아래 코드는 그 현상을 가장 단순하게 보여줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;assert.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main(void) {
    int fd = open(&quot;shared.txt&quot;, O_RDONLY);
    assert(fd &amp;gt;= 0);

    pid_t pid = fork();
    assert(pid &amp;gt;= 0);

    if (pid == 0) {
        /* child: advance the shared offset */
        off_t off = lseek(fd, 10, SEEK_SET);
        assert(off == 10);
        _exit(0);
    } else {
        wait(NULL);

        /* parent: offset was changed by the child */
        off_t off = lseek(fd, 0, SEEK_CUR);
        printf(&quot;offset=%lld\n&quot;, (long long) off);
        close(fd);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/39_3.png&quot; alt=&quot;Figure 39.3: Processes Sharing An Open File Table Entry&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이 성질은 협력적으로 같은 파일에 순차 기록을 이어붙이는 패턴 등에서 의도적으로 활용되기도 합니다. 반대로 의도하지 않은 공유라면, 오프셋 경쟁으로 인해 결과가 뒤섞일 수 있으니 주의가 필요합니다.&lt;/p&gt;
&lt;h3&gt;3.2 dup, dup2도 오프셋을 공유한다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dup(oldfd)&lt;/code&gt;는 기존 fd와 같은 오픈 파일 테이블 엔트리를 가리키는 새 fd를 만듭니다. 즉, 오프셋과 상태를 공유합니다. &lt;code&gt;dup2(oldfd, newfd)&lt;/code&gt;는 새 fd 번호를 지정할 수 있다는 점이 다릅니다. (PDF의 Figure 39.4가 이 포인트를 보여줍니다)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;assert.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main(void) {
    int fd1 = open(&quot;shared.txt&quot;, O_RDONLY);
    assert(fd1 &amp;gt;= 0);

    int fd2 = dup(fd1);
    assert(fd2 &amp;gt;= 0);

    /* advance offset via fd1 */
    off_t a = lseek(fd1, 5, SEEK_SET);
    assert(a == 5);

    /* observe the same offset via fd2 */
    off_t b = lseek(fd2, 0, SEEK_CUR);
    printf(&quot;offset(fd2)=%lld\n&quot;, (long long) b);

    close(fd2);
    close(fd1);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;셸의 리다이렉션이 &lt;code&gt;dup2()&lt;/code&gt; 계열을 많이 사용하는 이유도 여기에 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 영속성 보장: fsync&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;write()&lt;/code&gt;가 즉시 저장 장치에 기록되는 것은 아닙니다. 일반적으로 파일 시스템은 성능을 위해 쓰기를 메모리에 버퍼링한 뒤 나중에 내려씁니다.&lt;/p&gt;
&lt;p&gt;즉각적인 영속성이 필요하면 &lt;code&gt;fsync(fd)&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int fd = open(&quot;foo&quot;,
              O_CREAT | O_WRONLY | O_TRUNC,
              S_IRUSR | S_IWUSR);

write(fd, buffer, size);
fsync(fd);
close(fd);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 자주 빠뜨리는 디테일이 하나 더 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새 파일 생성, 이름 변경 등으로 &lt;strong&gt;디렉터리 엔트리&lt;/strong&gt;가 바뀌면, 파일 내용만 &lt;code&gt;fsync(fd)&lt;/code&gt; 해서는 부족할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이름이 들어 있는 &lt;strong&gt;디렉터리 자체도 fsync&lt;/strong&gt;해야, 그 이름 변경이 영속적으로 반영되는 경우가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 데이터와 메타데이터를 함께 영속화하려면 파일과 디렉터리까지 포함해 순서를 설계해야 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 이름 변경과 원자성: rename&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mv&lt;/code&gt;는 내부적으로 &lt;code&gt;rename(old, new)&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int rc = rename(&quot;old_name&quot;, &quot;new_name&quot;);
if (rc != 0) {
    /* error handling */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rename()&lt;/code&gt;은 보통 크래시에 대해 원자적으로 구현됩니다. 크래시가 나더라도 이름은 old 또는 new 중 하나로 남고, 중간 상태가 나타나지 않는다는 성질을 기대할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 성질을 이용한 고전적 패턴이 있습니다. 파일을 안전하게 갱신할 때는 다음처럼 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int fd = open(&quot;foo.txt.tmp&quot;,
              O_WRONLY | O_CREAT | O_TRUNC,
              S_IRUSR | S_IWUSR);

write(fd, buffer, size);
fsync(fd);
close(fd);

/* atomic name swap */
rename(&quot;foo.txt.tmp&quot;, &quot;foo.txt&quot;);

/* for full durability, fsync the directory that contains foo.txt */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추가로 기억할 점도 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rename()&lt;/code&gt;은 같은 파일 시스템 내부에서의 원자적 변경을 전제로 합니다.&lt;/li&gt;
&lt;li&gt;서로 다른 파일 시스템 간 이동은 복사 후 삭제로 구현되는 경우가 많고, 동일한 원자성을 기대하기 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 메타데이터 읽기: stat&lt;/h2&gt;
&lt;p&gt;파일의 내용 외에 크기, 권한, 소유자, 링크 수, 시간 정보 같은 것은 메타데이터입니다. 유닉스 계열에서는 &lt;code&gt;stat()&lt;/code&gt;/&lt;code&gt;fstat()&lt;/code&gt;로 조회합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;

int main(void) {
    struct stat sb;
    if (stat(&quot;foo.txt&quot;, &amp;amp;sb) != 0) {
        return 1;
    }

    printf(&quot;size=%lld\n&quot;, (long long) sb.st_size);
    printf(&quot;inode=%lld\n&quot;, (long long) sb.st_ino);
    printf(&quot;nlink=%lld\n&quot;, (long long) sb.st_nlink);
    printf(&quot;mode=%o\n&quot;, (unsigned) sb.st_mode);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Figure 39.5는 &lt;code&gt;struct stat&lt;/code&gt;의 대표 필드들을 보여줍니다. 플랫폼마다 필드 구성이 조금씩 다를 수 있지만, 핵심은 비슷합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* simplified view of struct stat (fields vary by platform) */
struct stat {
    dev_t     st_dev;     /* ID of device containing file */
    ino_t     st_ino;     /* inode number */
    mode_t    st_mode;    /* file type + permission bits */
    nlink_t   st_nlink;   /* hard link count */
    uid_t     st_uid;     /* user ID of owner */
    gid_t     st_gid;     /* group ID of owner */
    off_t     st_size;    /* total size, bytes */
    time_t    st_atime;   /* last access time */
    time_t    st_mtime;   /* last modification time */
    time_t    st_ctime;   /* last status change time */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 파일 삭제가 unlink인 이유&lt;/h2&gt;
&lt;p&gt;유닉스에서 파일 삭제는 &lt;code&gt;delete()&lt;/code&gt;가 아니라 &lt;code&gt;unlink(path)&lt;/code&gt;입니다. 이유는 파일이 실제로는 두 가지로 구성되기 때문입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inode: 파일 메타데이터와 데이터 블록을 가리키는 저수준 구조&lt;/li&gt;
&lt;li&gt;디렉터리 엔트리: 사람이 읽는 이름을 inode 번호에 연결하는 링크&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 삭제는 이름과 inode의 연결을 끊는 작업이며, 그래서 unlink입니다.&lt;/p&gt;
&lt;h3&gt;7.1 하드 링크와 링크 카운트&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;link()&lt;/code&gt; 또는 &lt;code&gt;ln&lt;/code&gt;은 동일 inode를 가리키는 이름을 하나 더 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln file file2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 &lt;code&gt;file&lt;/code&gt;과 &lt;code&gt;file2&lt;/code&gt;는 inode 번호가 같습니다. 파일 시스템은 inode 안에 **링크 카운트(nlink)**를 유지합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unlink()&lt;/code&gt;는 디렉터리 엔트리를 제거하고 링크 카운트를 1 줄입니다.&lt;/li&gt;
&lt;li&gt;링크 카운트가 0이 되는 시점에야 inode와 데이터 블록이 해제됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;추가로, 어떤 프로세스가 그 파일을 이미 열어두고 있다면 링크 카운트가 0이 되어도 마지막 fd가 닫힐 때까지 실제 데이터는 남아 있을 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 디렉터리 API&lt;/h2&gt;
&lt;h3&gt;8.1 mkdir, 그리고 . 과 ..&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mkdir()&lt;/code&gt;로 디렉터리를 만듭니다. 빈 디렉터리에도 최소한 두 엔트리가 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt;: 자기 자신&lt;/li&gt;
&lt;li&gt;&lt;code&gt;..&lt;/code&gt;: 부모 디렉터리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.2 디렉터리는 직접 write 할 수 없음&lt;/h3&gt;
&lt;p&gt;디렉터리도 파일이지만, 일반 파일처럼 &lt;code&gt;write()&lt;/code&gt;로 내용을 직접 바꿀 수는 없습니다. 디렉터리 형식은 파일 시스템 메타데이터이며, 무결성을 위해 파일 시스템이 직접 관리해야 하기 때문입니다. 파일을 만들거나 지우는 식으로 간접적으로만 갱신됩니다.&lt;/p&gt;
&lt;h3&gt;8.3 읽기: opendir, readdir, closedir&lt;/h3&gt;
&lt;p&gt;디렉터리 내용을 읽을 때는 다음 API를 씁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;dirent.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

int main(void) {
    DIR *dp = opendir(&quot;.&quot;);
    if (!dp) return 1;

    struct dirent *d;
    while ((d = readdir(dp)) != NULL) {
        printf(&quot;%lu %s\n&quot;,
               (unsigned long) d-&amp;gt;d_ino,
               d-&amp;gt;d_name);
    }
    closedir(dp);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;디렉터리 엔트리는 이름과 inode 번호를 포함하고, 더 많은 정보가 필요하면 각 엔트리에 대해 &lt;code&gt;stat()&lt;/code&gt;을 호출하는 방식이 흔합니다.&lt;/p&gt;
&lt;h3&gt;8.4 삭제: rmdir&lt;/h3&gt;
&lt;p&gt;디렉터리 삭제는 &lt;code&gt;rmdir()&lt;/code&gt;이며, 안전을 위해 &lt;strong&gt;비어 있는 디렉터리만&lt;/strong&gt; 지울 수 있습니다. 비어 있다는 것은 엔트리가 &lt;code&gt;.&lt;/code&gt;와 &lt;code&gt;..&lt;/code&gt;만 남아 있다는 뜻입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 심볼릭 링크&lt;/h2&gt;
&lt;p&gt;심볼릭 링크는 하드 링크의 제약을 풀기 위해 도입된 별도 파일 타입입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;심볼릭 링크 자체가 inode를 가지는 파일입니다.&lt;/li&gt;
&lt;li&gt;링크 파일의 내용으로 대상 경로 문자열을 저장합니다.&lt;/li&gt;
&lt;li&gt;대상이 삭제되면 링크는 깨진 경로를 가리키는 dangling 상태가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하드 링크는 일반적으로 다음 제약이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디렉터리에 하드 링크를 만들 수 없음(디렉터리 트리에 사이클이 생길 위험)&lt;/li&gt;
&lt;li&gt;다른 파일 시스템 파티션을 가로질러 하드 링크를 만들 수 없음(inode 번호는 파일 시스템 내부에서만 유일)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;심볼릭 링크는 이런 제약을 피해갈 수 있지만, 대신 dangling 문제를 갖습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 권한 비트와 ACL&lt;/h2&gt;
&lt;p&gt;유닉스는 파일을 공유하는 시스템이므로 접근 제어가 필요합니다.&lt;/p&gt;
&lt;h3&gt;10.1 권한 비트&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ls -l&lt;/code&gt;로 보면 &lt;code&gt;-rw-r--r--&lt;/code&gt; 같은 문자열이 나옵니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫 글자: 파일 타입(&lt;code&gt;-&lt;/code&gt; 일반 파일, &lt;code&gt;d&lt;/code&gt; 디렉터리, &lt;code&gt;l&lt;/code&gt; 심볼릭 링크 등)&lt;/li&gt;
&lt;li&gt;다음 9글자: 소유자, 그룹, 기타에 대한 읽기/쓰기/실행 권한&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;chmod 600 foo.txt&lt;/code&gt;는 소유자만 읽기/쓰기를 허용합니다.&lt;/p&gt;
&lt;p&gt;디렉터리에서 실행 비트는 의미가 다릅니다. 디렉터리에 대한 실행 권한은 그 디렉터리로 &lt;code&gt;cd&lt;/code&gt;하거나, 경로 탐색을 할 수 있는 권한과 밀접합니다.&lt;/p&gt;
&lt;h3&gt;10.2 superuser와 ACL&lt;/h3&gt;
&lt;p&gt;로컬 파일 시스템에서는 root 같은 superuser가 모든 파일에 접근할 수 있는 경우가 많습니다. 더 정교한 제어가 필요한 시스템은 디렉터리 단위의 &lt;strong&gt;ACL(Access Control List)&lt;/strong&gt; 같은 메커니즘을 제공하기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 파일 시스템 만들기와 마운트&lt;/h2&gt;
&lt;p&gt;디스크 파티션을 파일 시스템으로 초기화하려면 &lt;code&gt;mkfs&lt;/code&gt; 계열 도구를 사용합니다. 그리고 그 파일 시스템을 현재 디렉터리 트리의 한 지점에 붙이는 작업이 &lt;code&gt;mount&lt;/code&gt;입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount -t ext3 /dev/sda1 /home/users
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마운트 이후에는 &lt;code&gt;/home/users&lt;/code&gt;가 새 파일 시스템의 루트가 됩니다. 이 방식 덕분에 여러 종류의 파일 시스템(ext 계열, proc, sysfs, tmpfs, 네트워크 FS 등)을 하나의 트리로 통합해 일관된 이름 체계를 제공합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;12. 보안 관점 덤: TOCTTOU&lt;/h2&gt;
&lt;p&gt;파일을 검사한 뒤 사용하기까지 시간 간격이 있으면, 그 사이 공격자가 대상을 바꿔치기할 수 있습니다. 이를 TOCTTOU(time of check to time of use) 문제라고 부릅니다.&lt;/p&gt;
&lt;p&gt;파일을 열 때 플래그를 적절히 사용하고, 가능하면 경로 기반 검사보다 fd 기반 검사를 선호하는 방식으로 위험을 줄일 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;13. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;파일&lt;/strong&gt;은 바이트 배열이며, 저수준 이름으로 &lt;strong&gt;inode 번호&lt;/strong&gt;를 갖습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디렉터리&lt;/strong&gt;는 (이름, inode 번호) 매핑 목록이며 트리를 이룹니다.&lt;/li&gt;
&lt;li&gt;fd는 프로세스별 핸들이고, 커널의 &lt;strong&gt;오픈 파일 테이블 엔트리&lt;/strong&gt;를 가리킵니다. 오프셋은 그 엔트리에 저장됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt;와 &lt;code&gt;dup()&lt;/code&gt;는 오픈 파일 테이블 엔트리를 공유하여 오프셋도 공유합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fsync()&lt;/code&gt;는 영속성을 강제하지만, 디렉터리까지 포함해 올바르게 쓰는 것이 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rename()&lt;/code&gt;은 원자적 교체 패턴의 핵심 빌딩 블록입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unlink()&lt;/code&gt;는 이름과 inode의 연결을 끊는 호출이며, 링크 카운트가 0이 될 때 실제 해제가 발생합니다.&lt;/li&gt;
&lt;li&gt;권한 비트와 ACL은 공유 환경에서 접근 제어를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkfs&lt;/code&gt;와 &lt;code&gt;mount&lt;/code&gt;는 여러 파일 시스템을 하나의 트리로 합칩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장부터는 이런 추상화가 디스크 위에 어떻게 구현되는지(VSFS 등)를 살펴봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;14. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inode (i-number)&lt;/code&gt;: 파일의 메타데이터와 데이터 블록 위치를 담는 영속 구조&lt;/li&gt;
&lt;li&gt;&lt;code&gt;file descriptor (fd)&lt;/code&gt;: 열린 파일을 가리키는 프로세스별 정수 핸들&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open file table&lt;/code&gt;: 열린 파일의 상태(오프셋, 권한, 참조 카운트 등)를 추적하는 커널 자료구조&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offset&lt;/code&gt;: 다음 &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;가 시작될 파일 내 위치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lseek&lt;/code&gt;: 오프셋을 재설정하는 시스템 콜(자체로 디스크 I/O를 수행하지 않음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unlink&lt;/code&gt;: 디렉터리 엔트리를 제거해 이름과 inode의 연결을 끊는 시스템 콜&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hard link&lt;/code&gt;: 동일 inode를 가리키는 또 다른 이름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;symbolic link&lt;/code&gt;: 경로 문자열을 담는 별도 파일 타입의 링크&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fsync&lt;/code&gt;: 더티 데이터를 저장 장치로 강제 플러시하는 시스템 콜&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rename&lt;/code&gt;: 크래시에 대해 원자적으로 이름을 교체하는 시스템 콜&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkfs&lt;/code&gt;, &lt;code&gt;mount&lt;/code&gt;: 파일 시스템 생성과 트리 결합&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-intro.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 39: Interlude: Files and Directories&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 38. RAID (Redundant Arrays of Inexpensive Disks)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-38-file-raid/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-38-file-raid/</guid><description>여러 디스크를 묶어 성능, 용량, 신뢰성을 함께 끌어올리는 RAID의 핵심 개념과 RAID 0, 1, 4, 5의 동작 원리 및 트레이드오프를 정리합니다.</description><pubDate>Sat, 07 Feb 2026 19:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;디스크 하나는 느립니다. 더 빠른 I/O, 더 큰 용량, 그리고 디스크 고장에도 데이터가 사라지지 않는 신뢰성을 원하면 단일 디스크만으로는 한계가 빠르게 드러납니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하는 대표적인 접근이 RAID입니다. 여러 개의 디스크를 묶어 &lt;strong&gt;하나의 큰 디스크처럼 보이게&lt;/strong&gt; 만들고, 설계에 따라 &lt;strong&gt;성능&lt;/strong&gt;, &lt;strong&gt;용량&lt;/strong&gt;, &lt;strong&gt;신뢰성&lt;/strong&gt;을 서로 다른 비율로 끌어올립니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. RAID를 바라보는 관점&lt;/h2&gt;
&lt;p&gt;RAID는 위쪽(파일 시스템)에서 보면 단일 디스크와 같습니다. 파일 시스템은 논리 블록 주소(LBA)에 대해 읽기/쓰기를 요청하고, RAID가 내부에서 이를 여러 개의 물리 I/O로 변환합니다.&lt;/p&gt;
&lt;p&gt;RAID를 비교할 때는 다음 세 축을 먼저 고정하는 편이 깔끔합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용량(Capacity)&lt;/strong&gt;: 디스크가 $N$개이고 각 디스크가 $B$ 블록이라면, 유효 용량은 몇 블록인가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성(Reliability)&lt;/strong&gt;: 디스크 몇 개의 fail-stop 고장을 견딜 수 있는가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능(Performance)&lt;/strong&gt;: 단일 요청 지연과 steady-state 처리량이 어떤가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1 성능을 두 가지로 나누기&lt;/h3&gt;
&lt;p&gt;성능은 보통 다음 둘로 쪼개어 봅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;: 단일 논리 I/O 한 번이 끝나기까지 걸리는 시간&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt;: 다수 요청이 지속적으로 들어올 때의 총 처리량(대역폭)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;워크로드도 단순화해서 두 축으로 나누면 비교가 쉬워집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sequential&lt;/strong&gt;: 큰 연속 범위를 길게 읽고 쓰는 패턴&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random&lt;/strong&gt;: 작은 요청이 디스크의 여러 위치로 흩어지는 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래에서는 OSTEP의 단순화 모델을 그대로 따릅니다. 단일 디스크가 sequential에서 $S,[\mathrm{MB/s}]$, random에서 $R,[\mathrm{MB/s}]$의 처리량을 낸다고 두고($S \gg R$), RAID 레벨별 steady-state 처리량을 $S$와 $R$의 선형 결합으로 계산합니다. 또한 처리량 식은 &lt;strong&gt;충분한 큐 깊이&lt;/strong&gt;가 있어 디스크들을 병렬로 포화시킬 수 있다는 가정을 포함합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. RAID 0: Striping&lt;/h2&gt;
&lt;p&gt;RAID 0은 &lt;strong&gt;중복이 없는 스트라이핑&lt;/strong&gt;입니다. 신뢰성을 올리지 않고 오직 성능과 용량만 최대화합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_1.png&quot; alt=&quot;Figure 38.1: RAID-0 Simple Striping&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용량&lt;/strong&gt;: $N \cdot B$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성&lt;/strong&gt;: 없음(디스크 1개 고장이 곧 데이터 손실)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능(처리량)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Sequential read/write: $N \cdot S$&lt;/li&gt;
&lt;li&gt;Random read/write: $N \cdot R$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;: 작은 단일 요청은 단일 디스크와 거의 동일(요청은 한 디스크로만 감)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.1 Chunk size가 바뀌면 무엇이 달라지나&lt;/h3&gt;
&lt;p&gt;스트라이핑은 블록을 디스크에 round-robin으로 뿌리지만, 실제 시스템은 보통 &lt;strong&gt;chunk size&lt;/strong&gt;를 1블록보다 크게 잡습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_2.png&quot; alt=&quot;Figure 38.2: Striping With A Bigger Chunk Size&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;chunk가 작으면 한 파일이 여러 디스크로 퍼져 &lt;strong&gt;단일 파일 병렬성&lt;/strong&gt;이 좋아질 수 있음&lt;/li&gt;
&lt;li&gt;chunk가 크면 단일 파일이 한 디스크에 머무는 구간이 늘어 &lt;strong&gt;positioning 오버헤드&lt;/strong&gt;를 줄일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 chunk size는 워크로드(요청 크기, 동시성, 파일 크기 분포)에 강하게 의존하는 성능 파라미터입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. RAID 1: Mirroring&lt;/h2&gt;
&lt;p&gt;RAID 1은 &lt;strong&gt;미러링&lt;/strong&gt;입니다. 각 블록의 복사본을 다른 디스크에 저장해 디스크 고장에 대비합니다. 흔한 배치는 미러 쌍을 만들고 그 위에 스트라이핑을 하는 RAID 10 형태입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_3.png&quot; alt=&quot;Figure 38.3: Simple RAID-1 Mirroring&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용량&lt;/strong&gt;: $\frac{N}{2} \cdot B$  (2-way 미러 기준)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성&lt;/strong&gt;: 최악의 경우 1개 고장을 확실히 허용, 고장 위치가 운 좋게 분산되면 최대 $\frac{N}{2}$개까지도 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능(처리량)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Sequential read/write: $\frac{N}{2}\cdot S$&lt;/li&gt;
&lt;li&gt;Random read: $N \cdot R$&lt;/li&gt;
&lt;li&gt;Random write: $\frac{N}{2}\cdot R$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;읽기는 두 복사본 중 아무거나 선택할 수 있어 random read에서 특히 유리합니다. 반면 쓰기는 두 복사본을 모두 갱신해야 합니다. 다만 두 디스크에 &lt;strong&gt;병렬로&lt;/strong&gt; 쓸 수 있으므로 단일 쓰기 지연은 보통 단일 디스크보다 큰 폭으로 늘지 않고, 대략 느린 쪽에 맞춰지는 정도로 이해하면 됩니다.&lt;/p&gt;
&lt;h3&gt;3.1 Consistent-update problem&lt;/h3&gt;
&lt;p&gt;RAID 1(그리고 다중 디스크를 함께 갱신하는 RAID)은 한 번의 논리 쓰기가 &lt;strong&gt;여러 디스크 쓰기&lt;/strong&gt;로 분해됩니다. 이때 크래시 타이밍이 나쁘면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 디스크에는 새 데이터가 기록되고&lt;/li&gt;
&lt;li&gt;다른 디스크에는 옛 데이터가 남아&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;복사본 간 내용이 불일치해질 수 있습니다.&lt;/p&gt;
&lt;p&gt;일반적인 해결은 RAID 내부에 &lt;strong&gt;write-ahead log&lt;/strong&gt;를 두고, 다중 디스크 갱신을 트랜잭션처럼 처리하는 것입니다. 하드웨어 RAID는 이를 위해 배터리 백업이 있는 캐시 같은 &lt;strong&gt;비휘발성 버퍼&lt;/strong&gt;를 함께 쓰기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. RAID 4: Dedicated Parity&lt;/h2&gt;
&lt;p&gt;미러링은 용량 비용이 큽니다. RAID 4는 이를 줄이기 위해 &lt;strong&gt;패리티(Parity)&lt;/strong&gt; 를 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_4.png&quot; alt=&quot;Figure 38.4: RAID-4 With Parity&quot; /&gt;&lt;/p&gt;
&lt;p&gt;한 stripe에 데이터 블록들이 있고, 별도의 &lt;strong&gt;패리티 디스크&lt;/strong&gt;에 패리티 블록을 둡니다. 디스크 1개가 고장 나면 남은 블록들과 패리티로 복구할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용량&lt;/strong&gt;: $(N-1)\cdot B$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성&lt;/strong&gt;: 디스크 1개 고장까지 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1 XOR 패리티의 불변식&lt;/h3&gt;
&lt;p&gt;stripe의 데이터 블록을 $D_0,\dots,D_{N-2}$, 패리티를 $P$라고 하면&lt;/p&gt;
&lt;p&gt;$$
P = D_0 \oplus D_1 \oplus \cdots \oplus D_{N-2}
\tag{1}
$$&lt;/p&gt;
&lt;p&gt;한 블록이 사라지면 같은 연산으로 복원됩니다.&lt;/p&gt;
&lt;h3&gt;4.2 Full-stripe write와 small write&lt;/h3&gt;
&lt;p&gt;큰 연속 쓰기에서는 stripe를 통째로 채우는 &lt;strong&gt;full-stripe write&lt;/strong&gt;가 가능합니다. 이때는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새 데이터들로 새 패리티를 계산하고&lt;/li&gt;
&lt;li&gt;데이터 + 패리티를 병렬로 한 번에 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하면 되므로 효율이 좋습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_5.png&quot; alt=&quot;Figure 38.5: Full-stripe Writes In RAID-4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;문제는 작은 랜덤 쓰기입니다. stripe의 일부만 덮어쓰면 패리티를 갱신해야 하며, 대표적인 구현이 read-modify-write입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;old data 읽기&lt;/li&gt;
&lt;li&gt;old parity 읽기&lt;/li&gt;
&lt;li&gt;new parity 계산&lt;/li&gt;
&lt;li&gt;new data 쓰기&lt;/li&gt;
&lt;li&gt;new parity 쓰기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;패리티 갱신은 다음 형태로 정리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;$$
P_{\mathrm{new}} =
\left(D_{\mathrm{old}} \oplus D_{\mathrm{new}}\right) \oplus P_{\mathrm{old}}
\tag{2}
$$&lt;/p&gt;
&lt;p&gt;참고로 small write는 read-modify-write 말고, 같은 stripe의 다른 데이터 블록들을 모두 읽어 새 패리티를 재구성하는 방식(reconstruct-write)도 가능합니다. 어느 쪽이 유리한지는 디스크 수와 워크로드에 따라 갈립니다.&lt;/p&gt;
&lt;h3&gt;4.3 RAID 4 성능 요약&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sequential read&lt;/strong&gt;: $(N-1)\cdot S$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sequential write&lt;/strong&gt;: $(N-1)\cdot S$  (full-stripe write 가정)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random read&lt;/strong&gt;: $(N-1)\cdot R$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small random write&lt;/strong&gt;: 패리티 디스크가 병목&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 small random write는 패리티 디스크가 논리 쓰기마다 &lt;strong&gt;패리티 블록 read + write&lt;/strong&gt;를 수행해야 해서, 전체 처리량이 패리티 디스크 처리량에 의해 제한됩니다. 단순화 모델에서는&lt;/p&gt;
&lt;p&gt;$$
\mathrm{Throughput}_{\mathrm{RAID4,,rand,write}}
\approx
\frac{R}{2}
\tag{3}
$$&lt;/p&gt;
&lt;p&gt;로 요약합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_6.png&quot; alt=&quot;Figure 38.6: Writes Contend On Parity Disk&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. RAID 5: Rotating Parity&lt;/h2&gt;
&lt;p&gt;RAID 4의 가장 큰 문제는 패리티 디스크 단일 병목입니다. RAID 5는 패리티 블록을 디스크들에 &lt;strong&gt;회전 배치&lt;/strong&gt;하여 이를 제거합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_7.png&quot; alt=&quot;Figure 38.7: RAID-5 With Rotated Parity&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용량&lt;/strong&gt;: $(N-1)\cdot B$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성&lt;/strong&gt;: 디스크 1개 고장까지 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;성능은 RAID 4와 대부분 같지만, small random write에서 병목이 크게 완화됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sequential read/write&lt;/strong&gt;: $(N-1)\cdot S$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random read&lt;/strong&gt;: $N\cdot R$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small random write&lt;/strong&gt;: 4 I/O 비용은 남지만, 디스크 전체에 분산되어 병렬성이 생김&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단순화 모델에서는 RAID 5 small random write 처리량을&lt;/p&gt;
&lt;p&gt;$$
\mathrm{Throughput}_{\mathrm{RAID5,,rand,write}}
\approx
\frac{N}{4},R
\tag{4}
$$&lt;/p&gt;
&lt;p&gt;로 요약합니다. 손실 계수 4는 각 논리 쓰기가 2 reads + 2 writes의 물리 I/O로 분해되는 비용을 반영합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. RAID 레벨 비교&lt;/h2&gt;
&lt;p&gt;아래 표는 $N$개 디스크, 디스크당 용량 $B$, 단일 디스크 처리량 $S$ 및 $R$, 단일 디스크 요청 지연 $T$라는 단순화 모델에서의 비교입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/38_8.png&quot; alt=&quot;Figure 38.8: RAID Capacity, Reliability, and Performance&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;RAID 0&lt;/th&gt;
&lt;th&gt;RAID 1&lt;/th&gt;
&lt;th&gt;RAID 4&lt;/th&gt;
&lt;th&gt;RAID 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;유효 용량&lt;/td&gt;
&lt;td&gt;$N\cdot B$&lt;/td&gt;
&lt;td&gt;$\frac{N}{2}\cdot B$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot B$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot B$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;허용 고장 수&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1 (최악)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seq read&lt;/td&gt;
&lt;td&gt;$N\cdot S$&lt;/td&gt;
&lt;td&gt;$\frac{N}{2}\cdot S$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot S$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot S$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seq write&lt;/td&gt;
&lt;td&gt;$N\cdot S$&lt;/td&gt;
&lt;td&gt;$\frac{N}{2}\cdot S$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot S$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot S$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rand read&lt;/td&gt;
&lt;td&gt;$N\cdot R$&lt;/td&gt;
&lt;td&gt;$N\cdot R$&lt;/td&gt;
&lt;td&gt;$(N-1)\cdot R$&lt;/td&gt;
&lt;td&gt;$N\cdot R$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rand write&lt;/td&gt;
&lt;td&gt;$N\cdot R$&lt;/td&gt;
&lt;td&gt;$\frac{N}{2}\cdot R$&lt;/td&gt;
&lt;td&gt;$\frac{R}{2}$&lt;/td&gt;
&lt;td&gt;$\frac{N}{4}\cdot R$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency read&lt;/td&gt;
&lt;td&gt;$T$&lt;/td&gt;
&lt;td&gt;$T$&lt;/td&gt;
&lt;td&gt;$T$&lt;/td&gt;
&lt;td&gt;$T$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency write&lt;/td&gt;
&lt;td&gt;$T$&lt;/td&gt;
&lt;td&gt;$\approx T$&lt;/td&gt;
&lt;td&gt;$\approx 2T$&lt;/td&gt;
&lt;td&gt;$\approx 2T$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;$\approx 2T$는 small write의 두 단계(읽기 단계 + 쓰기 단계)를 가정한 값입니다. full-stripe write에서는 더 낮아질 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RAID는 여러 디스크를 묶어 성능, 용량, 신뢰성을 서로 다른 비율로 최적화하는 기술입니다.&lt;/li&gt;
&lt;li&gt;RAID 0은 최고 성능과 용량을 주지만 신뢰성이 없습니다.&lt;/li&gt;
&lt;li&gt;RAID 1은 용량을 희생하고 신뢰성과 random read 성능을 얻습니다. 대신 다중 디스크 갱신에 따른 consistent-update 문제가 존재합니다.&lt;/li&gt;
&lt;li&gt;RAID 4/5는 패리티로 용량 효율을 높이지만 small write에서 read-modify-write 계열 비용을 치릅니다.&lt;/li&gt;
&lt;li&gt;RAID 4는 패리티 디스크 단일 병목이 치명적이고, RAID 5는 패리티 회전으로 병목을 완화해 실무에서 더 널리 쓰입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RAID&lt;/code&gt;: 여러 디스크를 하나처럼 묶어 성능, 용량, 신뢰성을 개선하는 저장장치 구성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Striping&lt;/code&gt;: 블록을 디스크들에 분산 배치해 병렬성을 얻는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chunk&lt;/code&gt;: 스트라이핑에서 한 디스크에 연속 배치되는 단위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Stripe&lt;/code&gt;: 각 디스크의 한 chunk가 모여 이루는 묶음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mirroring&lt;/code&gt;: 동일 데이터 복사본을 다른 디스크에 저장하는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Parity&lt;/code&gt;: XOR 기반 복구 정보를 저장하는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Full-stripe write&lt;/code&gt;: 한 stripe를 통째로 덮어쓰는 쓰기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Read-modify-write&lt;/code&gt;: 작은 쓰기에서 old data/old parity를 읽고 패리티를 갱신하는 방식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Consistent-update problem&lt;/code&gt;: 다중 디스크 갱신 도중 크래시로 복사본 또는 패리티가 불일치해지는 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-raid.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 38: Redundant Arrays of Inexpensive Disks (RAIDs)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 37. Hard Disk Drives</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-37-file-disks/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-37-file-disks/</guid><description>하드 디스크의 물리적 구조(Platter, Head, Track)와 성능 모델(Seek, Rotation, Transfer), 그리고 디스크 스케줄링 기법(SSTF, SCAN, SPTF)을 정리합니다.</description><pubDate>Sat, 07 Feb 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 장에서 운영체제가 I/O 장치와 상호작용하는 일반적인 방식(폴링, 인터럽트, DMA)을 봤습니다. 이번 장에서는 오랫동안 영속성의 핵심 저장장치였던 &lt;strong&gt;하드 디스크 드라이브(HDD)&lt;/strong&gt; 를 더 깊게 들여다봅니다.&lt;/p&gt;
&lt;p&gt;HDD는 &lt;strong&gt;기계적 장치&lt;/strong&gt;입니다. 즉, 탐색과 회전 같은 물리적 동작이 성능을 지배합니다. 이 특성을 이해해야 이후 파일 시스템이 왜 연속 배치, 근접 배치, 순차 I/O를 중요하게 다루는지 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 디스크가 제공하는 인터페이스&lt;/h2&gt;
&lt;p&gt;현대 HDD의 외부 인터페이스는 단순하게 보입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디스크는 $0$부터 $n-1$까지 번호가 붙은 &lt;strong&gt;섹터(sector)&lt;/strong&gt; 의 배열로 표현됩니다.&lt;/li&gt;
&lt;li&gt;OS는 섹터 번호(논리 블록 주소, LBA)를 지정해 읽기/쓰기를 요청합니다.&lt;/li&gt;
&lt;li&gt;파일 시스템은 보통 더 큰 블록(예: $4,\mathrm{KiB}$) 단위로 I/O를 수행하지만, 디스크가 제공하는 최소 단위는 섹터입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 한 가지를 보수적으로 기억해 두는 편이 안전합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;쓰기 원자성은 보통 &lt;strong&gt;하나의 섹터 단위&lt;/strong&gt;만 보장된다고 가정합니다.&lt;/li&gt;
&lt;li&gt;섹터보다 큰 단위의 쓰기 도중 전원이 나가면 일부만 기록되는 &lt;strong&gt;torn write&lt;/strong&gt;가 생길 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또 하나의 중요한 사실은 명시적 인터페이스에 적히지 않습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디스크 주소 공간에서 &lt;strong&gt;가까운 블록&lt;/strong&gt; 접근이 &lt;strong&gt;먼 블록&lt;/strong&gt; 접근보다 보통 빠릅니다.&lt;/li&gt;
&lt;li&gt;큰 덩어리의 &lt;strong&gt;연속 접근(sequential)&lt;/strong&gt; 이 &lt;strong&gt;임의 접근(random)&lt;/strong&gt; 보다 압도적으로 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이것이 파일 시스템이 배치를 고민하는 근본 이유입니다.&lt;/p&gt;
&lt;p&gt;추가로, OS가 보는 LBA는 디스크 내부에서 물리 위치로 매핑됩니다. 따라서 물리적 결함 섹터 재매핑 같은 이유로, LBA가 단순히 기하학적 위치와 1:1로 대응하지 않을 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 물리적 구조&lt;/h2&gt;
&lt;p&gt;디스크 성능을 이해하려면 기본 기하(geometry)를 알아야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Platter&lt;/strong&gt;: 데이터를 저장하는 원판(자성 코팅)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spindle&lt;/strong&gt;: 플래터를 일정 속도로 회전시키는 축과 모터(RPM)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track&lt;/strong&gt;: 표면의 동심원 기록 단위&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cylinder&lt;/strong&gt;: 여러 플래터에서 같은 반지름의 트랙 집합&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Head / Arm&lt;/strong&gt;: 트랙 위로 이동하며 읽고 쓰는 헤드와 이를 움직이는 암&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/37_2.png&quot; alt=&quot;Figure 37.2: A Single Track Plus A Head&quot; /&gt;&lt;/p&gt;
&lt;p&gt;여기서 track, cylinder 같은 용어는 설명에 매우 유용하지만, 현대 디스크는 존(zones), 재매핑, 내부 최적화로 인해 이 기하학을 OS에 그대로 노출하지 않는 경우가 많습니다. 그럼에도 기계적 이동과 회전이 비용을 만든다는 본질은 같습니다.&lt;/p&gt;
&lt;h3&gt;2.1 탐색과 트랙 이동&lt;/h3&gt;
&lt;p&gt;헤드는 트랙 사이를 이동합니다. 이 이동이 &lt;strong&gt;탐색(seek)&lt;/strong&gt; 이고, 디스크가 느린 가장 큰 이유 중 하나입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/37_3.png&quot; alt=&quot;Figure 37.3: Three Tracks Plus A Head&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.2 트랙 스큐와 멀티존&lt;/h3&gt;
&lt;p&gt;연속 읽기가 트랙 경계를 넘을 때도 빠르게 이어지려면, 트랙 이동 시간 동안 회전해버린 각도를 보정해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Track skew&lt;/strong&gt;: 다음 트랙의 시작 섹터 위치를 의도적으로 밀어, 트랙 변경 직후에도 연속 섹터를 곧바로 읽게 만드는 기법&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/37_4.png&quot; alt=&quot;Figure 37.4: Track Skew&quot; /&gt;&lt;/p&gt;
&lt;p&gt;또한 바깥쪽 트랙은 둘레가 더 길어 더 많은 섹터를 담을 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-zoned recording&lt;/strong&gt;: 바깥쪽 존에 섹터를 더 많이 배치해 용량과 대역폭을 끌어올리는 방식(존별 섹터 수가 다름)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 특성 때문에, 같은 RPM이라도 바깥쪽 트랙의 &lt;strong&gt;순차 전송 대역폭&lt;/strong&gt;이 안쪽보다 더 높게 나오는 경향이 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 디스크 I/O 시간 모델&lt;/h2&gt;
&lt;p&gt;디스크 요청 하나의 서비스 시간은 세 요소의 합으로 모델링합니다.&lt;/p&gt;
&lt;p&gt;$$
T_{\mathrm{I/O}} = T_{\mathrm{seek}} + T_{\mathrm{rot}} + T_{\mathrm{xfer}}
\tag{1}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T_{\mathrm{seek}}$: 헤드를 목표 트랙으로 이동시키는 시간(가속, 이동, 감속, 안정화)&lt;/li&gt;
&lt;li&gt;$T_{\mathrm{rot}}$: 목표 섹터가 헤드 아래로 올 때까지 기다리는 회전 지연&lt;/li&gt;
&lt;li&gt;$T_{\mathrm{xfer}}$: 실제 데이터 전송 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1 평균 회전 지연&lt;/h3&gt;
&lt;p&gt;RPM이 주어지면 한 바퀴 시간은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;$$
T_{\mathrm{rev}} = \frac{60,\mathrm{s}}{\mathrm{RPM}}
= \frac{60\times 10^3}{\mathrm{RPM}},\mathrm{ms}
\tag{2}
$$&lt;/p&gt;
&lt;p&gt;평균적으로 반 바퀴를 기다리므로,&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}!\left[T_{\mathrm{rot}}\right]
= \frac{1}{2}T_{\mathrm{rev}}
= \frac{30\times 10^3}{\mathrm{RPM}},\mathrm{ms}
\tag{3}
$$&lt;/p&gt;
&lt;p&gt;예를 들어 $\mathrm{RPM}=10{,}000$이면 $T_{\mathrm{rev}}=6,\mathrm{ms}$, 평균 회전 지연은 $3,\mathrm{ms}$입니다.&lt;/p&gt;
&lt;h3&gt;3.2 왜 순차 I/O가 압도적으로 빠른가&lt;/h3&gt;
&lt;p&gt;임의 접근(random)은 매 요청마다 식 (1)의 $T_{\mathrm{seek}}$와 $T_{\mathrm{rot}}$를 반복해서 지불합니다. 반면 순차 접근(sequential)은 한 번의 위치 결정 이후 큰 전송으로 길게 이어지므로, 요청당 오버헤드가 희석되고 대부분 시간이 $T_{\mathrm{xfer}}$에 가까워집니다.&lt;/p&gt;
&lt;p&gt;처리율(대역폭)을 시간으로부터 계산할 때는 다음을 사용하면 됩니다.&lt;/p&gt;
&lt;p&gt;$$
R_{\mathrm{I/O}} = \frac{\mathrm{Size}}{T_{\mathrm{I/O}}}
\tag{4}
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/37_6.png&quot; alt=&quot;Figure 37.6: Disk Drive Performance&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 디스크 캐시와 쓰기 정책&lt;/h2&gt;
&lt;p&gt;현대 디스크는 내부에 작은 메모리 캐시를 둡니다(전통적으로 track buffer라고도 부름).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;읽기에서는 같은 트랙 근처 데이터를 더 읽어 캐시에 담아두면 이후 요청을 빠르게 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;쓰기에서는 두 전략이 있습니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;write-through&lt;/strong&gt;: 실제 플래터에 기록된 뒤 완료로 보고&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;write-back&lt;/strong&gt;: 디스크 캐시에 적재되면 완료로 보고, 실제 기록은 나중에 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;write-back은 체감 성능을 올리지만, 전원 장애 상황에서는 상위 계층이 기대하는 쓰기 완료 의미와 어긋날 수 있어 주의가 필요합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 디스크 스케줄링&lt;/h2&gt;
&lt;p&gt;디스크는 느린 장치이므로, 운영체제는 보통 여러 I/O 요청을 모아 큐를 만들고 &lt;strong&gt;요청 순서를 재배열&lt;/strong&gt;해 평균 비용을 줄입니다.&lt;/p&gt;
&lt;p&gt;현실적으로 OS가 알고 있는 정보는 제한적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OS는 보통 LBA 배열로만 디스크를 보고, 트랙, 헤드의 정확한 위치나 존 구조를 직접 알기 어렵습니다.&lt;/li&gt;
&lt;li&gt;그래서 트랙 기준 SSTF를 구현하기보다는, LBA 기준으로 가까운 요청을 고르는 근사(예: nearest-block-first)가 자주 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.1 FIFO&lt;/h3&gt;
&lt;p&gt;요청이 들어온 순서대로 처리합니다. 단순하지만, 멀리 떨어진 요청이 번갈아 오면 탐색 비용이 커져 성능이 나빠질 수 있습니다.&lt;/p&gt;
&lt;h3&gt;5.2 SSTF&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가장 짧은 탐색(seek)&lt;/strong&gt; 이 예상되는 요청을 우선 처리합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점: 평균 탐색을 줄여 처리량을 올림&lt;/li&gt;
&lt;li&gt;단점: 특정 영역 요청이 계속 들어오면 다른 영역 요청이 오래 밀리는 &lt;strong&gt;기아(starvation)&lt;/strong&gt; 가 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/37_7.png&quot; alt=&quot;Figure 37.7: SSTF Example&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.3 SCAN과 C-SCAN&lt;/h3&gt;
&lt;p&gt;기아를 피하려면 엘리베이터처럼 한 방향으로 쓸고(sweep) 나가면서 처리합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SCAN&lt;/strong&gt;: 한 방향으로 처리하다 끝에 닿으면 방향을 바꿔 다시 처리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-SCAN&lt;/strong&gt;: 한 방향으로만 처리하고, 끝에 닿으면 반대편으로 빠르게 복귀하여 다시 같은 방향으로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실무에서는 끝까지 가지 않고 마지막 요청까지만 훑는 LOOK, C-LOOK 같은 변형도 자주 함께 언급됩니다.&lt;/p&gt;
&lt;h3&gt;5.4 SPTF(SATF)&lt;/h3&gt;
&lt;p&gt;SSTF와 SCAN은 주로 탐색만 보지만, 실제로는 회전 지연도 무시할 수 없습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SPTF(Shortest Positioning Time First)&lt;/strong&gt; 는 $T_{\mathrm{seek}} + T_{\mathrm{rot}}$을 함께 고려해 다음 요청을 고릅니다.&lt;/li&gt;
&lt;li&gt;어떤 경우에는 탐색이 조금 더 길더라도, 회전이 거의 필요 없는 요청이 더 빨리 끝날 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/37_8.png&quot; alt=&quot;Figure 37.8: SSTF Sometimes Not Good Enough&quot; /&gt;&lt;/p&gt;
&lt;p&gt;다만 SPTF는 회전 위치(phase) 같은 세부 정보가 필요합니다. 이 정보는 디스크 내부가 더 정확히 알고 있으므로, 실제 시스템에서는 디스크가 내부적으로 더 정교한 재정렬을 수행하는 경우가 많습니다.&lt;/p&gt;
&lt;h3&gt;5.5 현대 시스템의 스케줄링 위치와 병합&lt;/h3&gt;
&lt;p&gt;현대 디스크는 내부적으로 여러 outstanding 요청을 받을 수 있고, 디스크 내부 스케줄러가 실제 헤드 위치를 알고 더 정교하게 재정렬하기도 합니다. 그래서 OS는 보통:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;적당한 수의 후보 요청(window)을 디스크로 보내고&lt;/li&gt;
&lt;li&gt;디스크가 내부에서 더 좋은 순서로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하는 식으로 협력합니다.&lt;/p&gt;
&lt;p&gt;또한 OS 스케줄러가 자주 수행하는 최적화가 &lt;strong&gt;I/O 병합(merging)&lt;/strong&gt; 입니다. 인접한 블록 요청을 합쳐 더 큰 연속 요청으로 만들면, 요청 개수 자체를 줄여 오버헤드를 낮출 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HDD는 기계적 동작 때문에 $T_{\mathrm{seek}}$와 $T_{\mathrm{rot}}$이 지배적이며, 임의 접근이 매우 비쌉니다.&lt;/li&gt;
&lt;li&gt;연속 접근은 한 번의 위치 결정 이후 큰 전송으로 이어져 대역폭이 크게 올라갑니다.&lt;/li&gt;
&lt;li&gt;디스크 스케줄링은 평균 비용을 줄이지만, 기아를 막기 위한 공정성 고려가 필요합니다.&lt;/li&gt;
&lt;li&gt;회전까지 고려하는 SPTF(SATF)는 더 좋은 선택이지만, 필요한 정보가 디스크 내부에 더 잘 존재하므로 현대 디스크는 내부 스케줄링을 함께 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Sector&lt;/code&gt;: 디스크의 기본 블록 단위(전통적으로 512B, 환경에 따라 4,\mathrm{KiB}도 사용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Seek Time&lt;/code&gt;: 헤드가 목표 트랙으로 이동하는 시간&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Rotational Delay&lt;/code&gt;: 목표 섹터가 헤드 아래로 올 때까지 기다리는 시간&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Transfer Time&lt;/code&gt;: 실제 데이터가 전송되는 시간&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Torn Write&lt;/code&gt;: 큰 쓰기 중 전원 장애 등으로 일부만 기록되는 현상&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Track Skew&lt;/code&gt;: 트랙 이동 시간을 고려해 다음 트랙의 섹터 시작 위치를 밀어 두는 기법&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSTF&lt;/code&gt;: 탐색이 가장 짧은 요청을 우선하는 정책&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCAN / C-SCAN&lt;/code&gt;: 한 방향 sweep을 기반으로 기아를 줄이는 엘리베이터 계열 정책&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SPTF / SATF&lt;/code&gt;: 탐색과 회전을 함께 고려해 위치 결정 시간이 최소인 요청을 고르는 정책&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Merging&lt;/code&gt;: 인접 요청을 합쳐 더 큰 연속 요청으로 만드는 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-disks.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 37: Hard Disk Drives&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 36. I/O Devices</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-36-file-devices/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-36-file-devices/</guid><description>운영체제가 다양한 입출력 장치와 효율적으로 상호작용하는 방법(Polling, Interrupt, DMA)과 이를 추상화하는 디바이스 드라이버의 구조를 정리합니다.</description><pubDate>Sat, 07 Feb 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;병행성(Concurrency) 파트를 마치고, 이제 운영체제의 세 번째 큰 주제인 &lt;strong&gt;영속성(Persistence)&lt;/strong&gt; 으로 넘어갑니다. 영속성을 이해하려면 먼저 시스템이 데이터를 입력받고 출력하는 &lt;strong&gt;I/O 장치&lt;/strong&gt;와 OS가 어떻게 소통하는지부터 정리해야 합니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 OSTEP 36장 내용을 따라, OS가 I/O 장치를 시스템에 통합하고 효율적으로 제어하는 핵심 메커니즘들인 &lt;strong&gt;Polling&lt;/strong&gt;, &lt;strong&gt;Interrupt&lt;/strong&gt;, &lt;strong&gt;DMA&lt;/strong&gt;, 그리고 이를 묶어주는 &lt;strong&gt;디바이스 드라이버&lt;/strong&gt;까지 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 시스템 아키텍처 (System Architecture)&lt;/h2&gt;
&lt;p&gt;CPU와 메인 메모리는 보통 매우 빠른 &lt;strong&gt;메모리 버스(memory bus)&lt;/strong&gt; 로 직접 연결됩니다. 그 아래에는 여러 장치를 연결하기 위한 버스가 계층적으로 붙습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory bus&lt;/strong&gt;: CPU와 DRAM 사이의 가장 빠른 경로&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;General I/O bus&lt;/strong&gt;: PCIe 같은 고성능 I/O 버스(그래픽, 고속 네트워크 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peripheral bus&lt;/strong&gt;: SATA, SCSI, USB 같은 상대적으로 느린 주변장치 버스(디스크, 키보드 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1 왜 계층 구조인가&lt;/h3&gt;
&lt;p&gt;핵심 이유는 &lt;strong&gt;물리적 제약과 비용&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;빠른 버스일수록 신호 무결성을 위해 길이가 짧아야 하고, 많은 장치를 멀리까지 달기 어렵습니다.&lt;/li&gt;
&lt;li&gt;빠르고 복잡한 버스는 구현 비용이 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 고성능 장치는 CPU 가까운 고속 버스에, 나머지 장치는 한 단계 아래의 버스에 연결하는 계층 구조가 일반적입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/36_1.png&quot; alt=&quot;Figure 36.1 System Architecture&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 표준 장치와 인터페이스 (Canonical Device)&lt;/h2&gt;
&lt;p&gt;장치는 크게 두 부분으로 나눌 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interface&lt;/strong&gt;: OS가 장치를 제어하기 위해 접근하는 레지스터 집합&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internals&lt;/strong&gt;: 장치 내부의 실제 동작(컨트롤러, 마이크로컨트롤러, 펌웨어, 장치 내부 메모리 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.1 장치 인터페이스의 전형&lt;/h3&gt;
&lt;p&gt;대부분의 장치는 다음과 같은 레지스터를 제공합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Status register&lt;/strong&gt;: 장치 상태(준비됨, 바쁨, 오류 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Command register&lt;/strong&gt;: 수행할 동작 지정(읽기, 쓰기, 리셋 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data register&lt;/strong&gt;: 데이터 송수신 통로&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OS는 이 레지스터를 읽고 쓰는 것만으로 장치를 제어할 수 있고, 장치 내부의 복잡함은 장치가 알아서 처리합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/36_2.png&quot; alt=&quot;Figure 36.2 A Canonical Device&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 표준 프로토콜과 Polling (Canonical Protocol)&lt;/h2&gt;
&lt;p&gt;가장 단순한 제어 방식은 &lt;strong&gt;폴링(polling)&lt;/strong&gt; 입니다. OS가 상태 레지스터를 반복해서 확인하면서 장치가 준비될 때까지 기다립니다.&lt;/p&gt;
&lt;h3&gt;3.1 폴링 기반 프로토콜 흐름&lt;/h3&gt;
&lt;p&gt;아래는 전형적인 흐름입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1) 장치가 준비될 때까지 대기
while (STATUS == BUSY) { /* spin */ }

// 2) 데이터 레지스터에 필요한 데이터 채우기
write_data_to_DATA_register();

// 3) 커맨드 레지스터에 명령 기록
write_command_to_COMMAND_register();

// 4) 완료될 때까지 다시 대기
while (STATUS == BUSY) { /* spin */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 폴링의 장단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;장점: 구현이 단순하고 예측 가능함&lt;/li&gt;
&lt;li&gt;단점: 장치가 느릴수록 CPU가 바쁜 루프만 돌며 낭비됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;폴링의 CPU 낭비를 아주 단순화해 보면, 장치 작업 시간이 $T_{dev}$ 일 때 CPU가 아무 일도 못 하는 시간이 거의 $T_{dev}$에 가깝습니다. 즉 CPU 유휴 시간을 활용할 수 없습니다. 정확히는 CPU가 idle이 아니라 busy-wait로 시간을 소비합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 인터럽트로 CPU 오버헤드 줄이기 (Interrupts)&lt;/h2&gt;
&lt;p&gt;폴링의 핵심 문제는 CPU가 기다림을 직접 수행한다는 점입니다. 이를 해결하기 위해 등장한 것이 &lt;strong&gt;인터럽트(interrupt)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;h3&gt;4.1 인터럽트 기반 흐름&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;OS가 장치에 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;OS는 해당 프로세스를 &lt;strong&gt;block&lt;/strong&gt; 시키고 다른 일을 수행합니다.&lt;/li&gt;
&lt;li&gt;장치가 작업을 끝내면 &lt;strong&gt;인터럽트&lt;/strong&gt;를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;CPU는 커널의 &lt;strong&gt;인터럽트 핸들러&lt;/strong&gt;로 들어가 마무리 작업을 합니다.&lt;/li&gt;
&lt;li&gt;대기하던 프로세스를 &lt;strong&gt;wakeup&lt;/strong&gt; 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 방식의 핵심 가치는 &lt;strong&gt;연산과 I/O의 중첩(overlap)&lt;/strong&gt; 입니다. 폴링은 기다림 동안 CPU가 놀지만, 인터럽트는 그 시간을 다른 프로세스 실행에 사용합니다.&lt;/p&gt;
&lt;h3&gt;4.2 인터럽트가 항상 이득은 아니다&lt;/h3&gt;
&lt;p&gt;인터럽트도 비용이 있습니다. 인터럽트 처리 과정은 보통 다음 오버헤드를 동반합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랩 및 컨텍스트 전환 비용&lt;/li&gt;
&lt;li&gt;핸들러 실행 비용&lt;/li&gt;
&lt;li&gt;캐시/파이프라인 교란 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 장치가 매우 빠른 경우에는&lt;/p&gt;
&lt;p&gt;$$
T_{dev} \ll T_{int}
$$&lt;/p&gt;
&lt;p&gt;일 때, 인터럽트 처리 비용 $T_{int}$ 가 지배적이 되어 오히려 폴링이 더 나을 수 있습니다. 그래서 현실의 시스템은 보통 &lt;strong&gt;짧게 폴링하다가, 오래 걸리면 인터럽트로 전환하는 하이브리드&lt;/strong&gt;를 사용합니다.&lt;/p&gt;
&lt;h3&gt;4.3 인터럽트 폭주와 coalescing&lt;/h3&gt;
&lt;p&gt;네트워크 패킷 폭주처럼 인터럽트가 너무 자주 발생하면, OS가 인터럽트 처리만 하느라 정상 작업을 거의 못 하는 &lt;strong&gt;livelock&lt;/strong&gt;에 빠질 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 완화하기 위한 대표 기법이 &lt;strong&gt;interrupt coalescing&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일정 시간 또는 일정 개수의 이벤트를 모아&lt;/li&gt;
&lt;li&gt;인터럽트를 한 번만 발생시켜&lt;/li&gt;
&lt;li&gt;인터럽트 빈도를 낮춥니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;장점은 CPU 오버헤드 감소, 단점은 이벤트 처리 지연(latency) 증가입니다. 결국 이 역시 &lt;strong&gt;오버헤드 vs 지연&lt;/strong&gt;의 트레이드오프입니다. 네트워크 스택에서는 인터럽트 완화와 함께 일정 구간 폴링으로 전환하는 방식도 흔히 결합됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 데이터 이동을 더 효율적으로 (DMA)&lt;/h2&gt;
&lt;p&gt;장치와 메모리 사이 데이터 이동을 CPU가 직접 수행하면, CPU가 단순 복사 작업에 묶입니다. 이를 &lt;strong&gt;PIO(programmed I/O)&lt;/strong&gt; 라고 부르기도 합니다.&lt;/p&gt;
&lt;h3&gt;5.1 DMA의 핵심 아이디어&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;DMA(Direct Memory Access)&lt;/strong&gt; 는 데이터 복사를 전담하는 하드웨어 엔진입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;OS가 DMA 엔진에 다음을 설정합니다.
&lt;ul&gt;
&lt;li&gt;소스/목적지 주소&lt;/li&gt;
&lt;li&gt;전송 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DMA 엔진이 메모리와 장치 사이 데이터를 복사합니다.&lt;/li&gt;
&lt;li&gt;완료되면 DMA가 인터럽트로 OS에 알립니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 하면 CPU는 전송 동안 다른 일을 할 수 있어, 대용량 I/O에서 특히 큰 이득이 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 장치 레지스터에 접근하는 방법 (Device Interaction)&lt;/h2&gt;
&lt;p&gt;OS가 장치 인터페이스 레지스터에 접근하는 방식은 크게 두 가지입니다.&lt;/p&gt;
&lt;h3&gt;6.1 Explicit I/O instructions&lt;/h3&gt;
&lt;p&gt;일부 아키텍처는 장치 접근을 위한 별도의 명령어를 제공합니다. 예를 들어 x86의 &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt; 같은 방식입니다.&lt;/p&gt;
&lt;h3&gt;6.2 Memory-mapped I/O&lt;/h3&gt;
&lt;p&gt;장치 레지스터를 물리 주소 공간의 일부로 매핑해, 일반 메모리 접근처럼 &lt;code&gt;load/store&lt;/code&gt; 로 접근하는 방식입니다. 현대 시스템에서 매우 흔합니다. 다만 MMIO는 일반 메모리와 달리 재주문이나 캐싱의 영향을 받을 수 있어, 드라이버에서는 &lt;code&gt;volatile&lt;/code&gt; 접근이나 메모리 배리어 같은 제어가 함께 쓰이기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 디바이스 드라이버와 추상화 (Device Driver)&lt;/h2&gt;
&lt;p&gt;OS는 장치 종류가 달라도 공통 인터페이스로 다루고 싶어 합니다. 이 역할을 하는 것이 &lt;strong&gt;디바이스 드라이버&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;7.1 스택으로 보는 위치&lt;/h3&gt;
&lt;p&gt;예를 들어 파일 시스템은 아래와 같은 계층을 통해 디스크에 접근합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 시스템: 읽기/쓰기 같은 논리 요청 생성&lt;/li&gt;
&lt;li&gt;범용 블록 계층: 요청을 일반화하고 스케줄링&lt;/li&gt;
&lt;li&gt;장치 드라이버: 실제 장치 프로토콜(레지스터 조작) 수행&lt;/li&gt;
&lt;li&gt;하드웨어 장치&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/36_3.png&quot; alt=&quot;Figure 36.3 A Storage Stack&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;7.2 드라이버의 현실적인 어려움&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;장치별로 드라이버가 다르고, 하드웨어 조합이 방대합니다.&lt;/li&gt;
&lt;li&gt;OS 커널 코드 중 상당 부분이 드라이버일 정도로 규모가 큽니다.&lt;/li&gt;
&lt;li&gt;하드웨어 세부에 강하게 결합된 코드가 많아 버그가 생기기 쉽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 추상화는 필수지만, 유지보수 비용도 함께 커집니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 사례 연구: IDE 디스크 드라이버 (xv6)&lt;/h2&gt;
&lt;p&gt;이 절은 디바이스 드라이버가 실제로 어떤 흐름을 갖는지 보여주는 예시입니다. xv6의 IDE 드라이버는 다음의 구성 요소로 동작합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청 큐(queue)&lt;/li&gt;
&lt;li&gt;요청 시작 루틴&lt;/li&gt;
&lt;li&gt;인터럽트 핸들러&lt;/li&gt;
&lt;li&gt;sleep/wakeup 동기화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.1 IDE 인터페이스 개요&lt;/h3&gt;
&lt;p&gt;IDE는 비교적 단순한 레지스터 기반 인터페이스를 갖습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/36_4.png&quot; alt=&quot;Figure 36.4 The IDE Interface&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8.2 요청 처리 흐름&lt;/h3&gt;
&lt;p&gt;전체 흐름은 아래처럼 이해하면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;요청이 들어오면 큐에 넣습니다.&lt;/li&gt;
&lt;li&gt;디스크가 idle이면 즉시 요청을 시작합니다.&lt;/li&gt;
&lt;li&gt;요청을 시작할 때 레지스터를 채우고 read 또는 write 명령을 내립니다.&lt;/li&gt;
&lt;li&gt;프로세스는 I/O 완료까지 sleep 합니다.&lt;/li&gt;
&lt;li&gt;디스크가 완료하면 인터럽트를 발생시키고, 핸들러가 필요한 데이터를 복사하거나 상태를 갱신한 뒤 잠든 프로세스를 깨웁니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 중요한 포인트는 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청 시작은 보통 &lt;strong&gt;인터럽트가 아닌 정상 실행 흐름&lt;/strong&gt;에서 수행됩니다.&lt;/li&gt;
&lt;li&gt;완료 처리는 &lt;strong&gt;인터럽트 핸들러&lt;/strong&gt;에서 수행되고, 그 결과로 다음 요청을 이어서 시작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 개념 코드: xv6 IDE 드라이버의 핵심 흐름을 단순화한 형태
// buf: 블록 I/O 요청 구조체(READ/WRITE 플래그, 섹터 번호, 데이터 버퍼 등)

static struct spinlock ide_lock;
static struct buf *idequeue;

static void ide_start(struct buf *b) {
    // 1) 장치 ready까지 짧게 폴링(status 확인)
    // 2) 섹터 번호/개수 및 데이터 레지스터 설정
    // 3) READ/WRITE 커맨드를 기록해 요청 시작
}

void ide_rw(struct buf *b) {
    acquire(&amp;amp;ide_lock);

    // 요청을 큐에 연결
    b-&amp;gt;qnext = NULL;
    if (idequeue == NULL) idequeue = b;
    else tail(idequeue)-&amp;gt;qnext = b;

    // 디스크가 idle이면 즉시 시작
    if (idequeue == b)
        ide_start(b);

    // 완료까지 sleep: 인터럽트 핸들러가 flags 갱신 후 wakeup
    while ((b-&amp;gt;flags &amp;amp; (B_VALID | B_DIRTY)) != B_VALID)
        sleep(b, &amp;amp;ide_lock);

    release(&amp;amp;ide_lock);
}

void ide_intr(void) {
    acquire(&amp;amp;ide_lock);

    struct buf *b = idequeue;
    if (b == NULL) {
        release(&amp;amp;ide_lock);
        return;
    }
    idequeue = b-&amp;gt;qnext;

    if ((b-&amp;gt;flags &amp;amp; B_DIRTY) == 0) {
        // READ 완료: 장치 -&amp;gt; 메모리 전송(예: insl(DATA, b-&amp;gt;data, BSIZE/4))
    } else {
        // WRITE 완료: 이미 메모리 -&amp;gt; 장치 전송(outsl)을 끝낸 상태
        b-&amp;gt;flags &amp;amp;= ~B_DIRTY;
    }

    b-&amp;gt;flags |= B_VALID;
    wakeup(b);

    // 다음 요청이 있으면 이어서 시작
    if (idequeue != NULL)
        ide_start(idequeue);

    release(&amp;amp;ide_lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;완료 조건은 다음 불변식으로 요약할 수 있습니다.&lt;/p&gt;
&lt;p&gt;$$
(b \texttt{-&amp;gt;} \mathit{flags} \mathbin{&amp;amp;} (\texttt{B_VALID} \mid \texttt{B_DIRTY})) = \texttt{B_VALID}
$$&lt;/p&gt;
&lt;p&gt;즉 I/O가 끝나면 버퍼는 유효하고(&lt;code&gt;B_VALID&lt;/code&gt;), 더 이상 dirty가 아니어야 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;시스템은 계층적 버스 구조로 다양한 I/O 장치를 연결합니다.&lt;/li&gt;
&lt;li&gt;장치는 status, command, data 레지스터 같은 인터페이스를 제공하고, OS는 이를 통해 제어합니다.&lt;/li&gt;
&lt;li&gt;폴링은 단순하지만 CPU를 낭비할 수 있습니다.&lt;/li&gt;
&lt;li&gt;인터럽트는 연산과 I/O를 중첩해 효율을 높이지만, 빠른 장치나 인터럽트 폭주 상황에서는 오버헤드 문제가 생길 수 있습니다.&lt;/li&gt;
&lt;li&gt;DMA는 대용량 데이터 이동에서 CPU 부담을 크게 줄입니다.&lt;/li&gt;
&lt;li&gt;디바이스 드라이버는 하드웨어의 구체성을 캡슐화해 OS 전체에 표준 인터페이스를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장부터는 디스크 같은 저장장치의 물리적 특성과 스케줄링으로 넘어갑니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Polling&lt;/code&gt;: 상태 레지스터를 반복적으로 읽어 완료 여부를 확인하는 방식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Interrupt&lt;/code&gt;: 장치가 작업 완료 시 CPU에 신호를 보내 커널 핸들러를 실행시키는 메커니즘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PIO&lt;/code&gt;: CPU가 직접 데이터 이동을 수행하는 방식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DMA&lt;/code&gt;: CPU 개입 없이 메모리와 장치 사이 데이터를 전송하는 하드웨어 엔진&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Memory-mapped I/O&lt;/code&gt;: 장치 레지스터를 메모리 주소에 매핑해 &lt;code&gt;load/store&lt;/code&gt; 로 접근하는 방식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Device Driver&lt;/code&gt;: 장치별 제어 방식(프로토콜)을 OS가 쓰기 쉬운 형태로 캡슐화한 커널 코드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Interrupt Coalescing&lt;/code&gt;: 여러 이벤트를 모아 인터럽트를 덜 자주 발생시키는 기법&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/file-devices.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 36: I/O Devices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 33. Event-based Concurrency</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-33-threads-events/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-33-threads-events/</guid><description>스레드 없이 병행성을 달성하는 이벤트 기반 프로그래밍의 원리(Event Loop, select/poll)와 장점, 그리고 비동기 I/O(AIO)와 수동 스택 관리의 어려움에 대해 다룹니다.</description><pubDate>Thu, 05 Feb 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지금까지는 병행성을 다루기 위해 &lt;strong&gt;스레드(Thread)&lt;/strong&gt; 를 사용하는 방법을 중심으로 정리했습니다. 하지만 스레드는 경쟁 상태, 락 누락, 교착 상태 같은 문제를 동반하고, 무엇이 언제 실행될지에 대한 제어권도 대부분 OS 스케줄러에 있습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 스레드를 전혀 사용하지 않고 병행 서버를 구축하는 &lt;strong&gt;이벤트 기반 병행성(Event-based Concurrency)&lt;/strong&gt; 을 다룹니다. Node.js 같은 현대적 프레임워크의 근간이 되는 C/UNIX 스타일 이벤트 루프를 기준으로 원리와 한계를 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 기본 아이디어: 이벤트 루프 (The Event Loop)&lt;/h2&gt;
&lt;p&gt;이벤트 기반 접근은 한 문장으로 요약됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이벤트가 발생할 때까지 기다린다&lt;/li&gt;
&lt;li&gt;발생한 이벤트를 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가장 전형적인 이벤트 루프는 다음처럼 생깁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (1) {
    events = getEvents();
    for (e in events)
        processEvent(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;processEvent()&lt;/code&gt;가 처리하는 코드를 **이벤트 핸들러(event handler)**라고 부릅니다. 중요한 성질은 단일 이벤트 루프가 단일 스레드로 돌아가는 한, 동시에 실행되는 핸들러의 수는&lt;/p&gt;
&lt;p&gt;$$H = 1$$&lt;/p&gt;
&lt;p&gt;이라는 점입니다. 즉, 다음에 어떤 이벤트를 처리할지 결정하는 행위가 곧 &lt;strong&gt;스케줄링&lt;/strong&gt;입니다. 스레드 기반 서버에서는 OS가 스케줄링을 결정하지만, 이벤트 기반 서버에서는 애플리케이션이 이벤트 처리 순서를 직접 제어합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 이벤트 받기: select() / poll() API&lt;/h2&gt;
&lt;p&gt;서버는 어떻게 이벤트의 존재를 알까요. UNIX 계열 시스템은 대표적으로 &lt;code&gt;select()&lt;/code&gt; 혹은 &lt;code&gt;poll()&lt;/code&gt;을 제공합니다. 공통 목적은 다음입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;감시 중인 fd 집합에서&lt;/li&gt;
&lt;li&gt;읽기 가능, 쓰기 가능, 예외 상태가 된 대상을 찾아&lt;/li&gt;
&lt;li&gt;준비된 fd만 골라서 알려준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;select()&lt;/code&gt; 시그니처는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int select(int nfds,
           fd_set *readfds,
           fd_set *writefds,
           fd_set *errorfds,
           struct timeval *timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심 포인트는 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;readfds / writefds&lt;/strong&gt;를 함께 다룰 수 있어, 입력 처리뿐 아니라 응답 전송 가능 여부까지 제어할 수 있습니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;timeout&lt;/strong&gt;으로 동작이 달라집니다
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NULL&lt;/code&gt;: 준비된 fd가 생길 때까지 무기한 대기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;: 즉시 리턴하여 폴링 형태로 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단순한 사용 예시는 다음 흐름입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fd_set readFDs;
FD_ZERO(&amp;amp;readFDs);
FD_SET(socket_fd, &amp;amp;readFDs);

int rc = select(maxFD + 1, &amp;amp;readFDs, NULL, NULL, NULL);
if (rc &amp;gt; 0 &amp;amp;&amp;amp; FD_ISSET(socket_fd, &amp;amp;readFDs)) {
    processFD(socket_fd);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 주의할 점은, 이벤트 기반 서버가 항상 논블로킹이라는 의미는 아닙니다. 보통 이벤트 루프는 &lt;code&gt;select()&lt;/code&gt; 자체로는 대기할 수 있습니다. 대신 핸들러 내부에서 디스크 I/O 같은 &lt;strong&gt;예측 불가능한 블로킹 호출&lt;/strong&gt;을 하지 않도록 설계하는 것이 핵심입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 왜 단순해 보일까: 락이 필요 없는 구간&lt;/h2&gt;
&lt;p&gt;단일 CPU에서 단일 이벤트 루프로 이벤트를 처리한다면, 다음이 성립합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 번에 하나의 핸들러만 실행된다&lt;/li&gt;
&lt;li&gt;핸들러 실행 중 다른 스레드가 끼어들지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 조건에서는 공유 자료구조를 건드릴 때 락을 잡을 필요가 없습니다. 따라서 스레드 기반에서 흔한 동기화 버그들이 기본 형태에서는 사라집니다.&lt;/p&gt;
&lt;p&gt;다만 이 단순함은 단일 루프, 단일 스레드라는 전제 위에 있습니다. 멀티 코어를 활용하려고 이벤트 루프를 여러 개로 늘리는 순간, 다시 임계 영역과 동기화 문제가 돌아옵니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 문제점 1: 블로킹 시스템 콜 (Blocking System Calls)&lt;/h2&gt;
&lt;p&gt;이벤트 기반 서버의 가장 중요한 규칙은 다음입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;이벤트 핸들러에서 블로킹 호출을 하지 말 것&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예를 들어 클라이언트 요청에 따라 디스크 파일을 읽어 응답해야 한다고 합시다. 핸들러가 &lt;code&gt;open()&lt;/code&gt;이나 &lt;code&gt;read()&lt;/code&gt;에서 디스크 I/O를 기다리며 블로킹되면, 이벤트 루프 전체가 멈춥니다. 그 동안 다른 요청은 들어와도 처리되지 않습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4.1 해결책: 비동기 I/O (Asynchronous I/O)&lt;/h2&gt;
&lt;p&gt;이 문제를 해결하려면, I/O 요청을 발행하고 즉시 리턴하는 **비동기 I/O(AIO)**가 필요합니다. POSIX AIO 계열에서는 보통 &lt;code&gt;aiocb&lt;/code&gt; 같은 제어 블록을 구성해 요청합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct aiocb {
    int aio_fildes;      // file descriptor
    off_t aio_offset;    // file offset
    volatile void *aio_buf;
    size_t aio_nbytes;
};

int aio_read(struct aiocb *aiocbp);
int aio_error(const struct aiocb *aiocbp);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;동작 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;aio_read()&lt;/code&gt;로 비동기 읽기 요청을 발행하고 즉시 리턴&lt;/li&gt;
&lt;li&gt;나중에 &lt;code&gt;aio_error()&lt;/code&gt;로 완료 여부를 확인
&lt;ul&gt;
&lt;li&gt;완료: 0&lt;/li&gt;
&lt;li&gt;진행 중: &lt;code&gt;EINPROGRESS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;하지만 여기서 또 다른 현실 문제가 생깁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;outstanding I/O가 수십, 수백 개면 각 요청을 계속 폴링하기가 괴롭다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 일부 시스템은 시그널 같은 메커니즘으로 완료를 통지해 폴링 부담을 줄이기도 합니다. 또한 어떤 환경에서는 비동기 디스크 I/O가 충분히 제공되지 않아, 네트워크는 이벤트로 처리하되 디스크 I/O는 스레드 풀로 처리하는 하이브리드 접근을 택하기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 문제점 2: 상태 관리 (State Management)&lt;/h2&gt;
&lt;p&gt;이벤트 기반 프로그래밍이 스레드보다 코딩하기 어려운 두 번째 이유는 &lt;strong&gt;상태(state) 관리&lt;/strong&gt;입니다. 스레드 기반 코드에서는 호출 스택이 자연스럽게 문맥을 저장합니다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음 코드는 스레드 기반에서는 너무 자연스럽습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int rc = read(fd, buffer, size);
rc = write(sd, buffer, size);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;read()&lt;/code&gt;가 끝나면 곧바로 &lt;code&gt;write()&lt;/code&gt;를 호출하면 됩니다. 이때 어떤 소켓 &lt;code&gt;sd&lt;/code&gt;로 쓸지, 어떤 버퍼를 쓸지는 스레드 스택에 그대로 남아 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 이벤트 기반 시스템에서는 &lt;code&gt;read()&lt;/code&gt;를 비동기로 요청하고 핸들러가 리턴해버립니다. 나중에 I/O 완료 이벤트가 왔을 때, 무엇을 이어서 해야 할지 정보를 잃지 않으려면 상태를 별도 구조에 저장해야 합니다. 이를 흔히 &lt;strong&gt;수동 스택 관리(manual stack management)&lt;/strong&gt; 라고 부릅니다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 자주 등장하는 개념이 &lt;strong&gt;continuation&lt;/strong&gt;입니다. 핵심은 단순합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다음에 이어서 해야 할 작업에 필요한 정보를 기록해 둔다&lt;/li&gt;
&lt;li&gt;이벤트가 발생했을 때 그 정보를 찾아서 마무리 작업을 수행한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예를 들어 fd 기준으로 continuation을 찾아 소켓을 알아내는 식으로 구성할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 예시: fd -&amp;gt; continuation(=sd 등 필요한 상태) 매핑
// 완료 이벤트에서 fd로 조회한 뒤 write 수행
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 패턴이 늘어나면 핸들러가 조각나고, 제어 흐름이 복잡해져 유지보수 난이도가 올라갑니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 기타 어려움들&lt;/h2&gt;
&lt;p&gt;이벤트 기반 접근은 만능이 아닙니다. 책에서는 대표적으로 다음을 지적합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;멀티 코어 확장성&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 루프는 단일 스레드&lt;/li&gt;
&lt;li&gt;여러 CPU를 쓰려면 여러 핸들러를 병렬로 돌려야 하고, 다시 락 기반 동기화가 필요해집니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;암시적 블로킹(implicit blocking)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;페이지 폴트 같은 사건은 프로그래머가 직접 제어하기 어렵습니다&lt;/li&gt;
&lt;li&gt;핸들러가 페이지 폴트를 내면 프로세스가 블로킹되고 서버 진행이 멈출 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;API 의미 변화에 취약&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 라이브러리 루틴이 논블로킹에서 블로킹으로 바뀌면, 해당 핸들러 구조를 다시 쪼개야 합니다&lt;/li&gt;
&lt;li&gt;이벤트 기반에서는 블로킹이 치명적이므로 이런 변화에 계속 민감해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;비동기 네트워크 I/O와 비동기 디스크 I/O의 비대칭&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;네트워크는 &lt;code&gt;select()&lt;/code&gt;류로 다루기 쉬운 편이지만&lt;/li&gt;
&lt;li&gt;디스크는 AIO 호출을 별도로 조합해야 하는 경우가 많아 인터페이스가 깔끔하게 통일되지 않습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약&lt;/h2&gt;
&lt;p&gt;이벤트 기반 병행성은 스레드의 대안으로 다음을 제공합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스케줄링 제어권을 애플리케이션이 더 많이 가진다&lt;/li&gt;
&lt;li&gt;단일 루프 구조에서는 락이 필요 없어 동기화 버그가 줄어든다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대신 다음 비용을 치릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;블로킹 호출을 피해야 하므로 비동기 I/O가 사실상 필수&lt;/li&gt;
&lt;li&gt;continuation 기반 상태 관리로 코드가 복잡해지고 유지보수가 어려워질 수 있다&lt;/li&gt;
&lt;li&gt;멀티 코어 확장, 페이징 같은 시스템 요소와의 상호작용이 까다롭다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 스레드와 이벤트는 같은 병행성 문제를 푸는 서로 다른 접근이며, 워크로드와 구현 복잡도, 운영 환경을 함께 보고 선택해야 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Event Loop (이벤트 루프)&lt;/code&gt;: 이벤트를 기다리고, 준비된 이벤트를 하나씩 처리하는 루프.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Event Handler (이벤트 핸들러)&lt;/code&gt;: 개별 이벤트를 처리하는 코드 조각. 단일 루프에서는 한 번에 하나만 실행됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select() / poll()&lt;/code&gt;: 다중 fd를 감시해 읽기/쓰기 준비 상태를 알려주는 시스템 콜.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Blocking / Non-blocking&lt;/code&gt;: 호출이 완료될 때까지 대기하면 블로킹, 바로 리턴하면 논블로킹.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Asynchronous I/O (AIO)&lt;/code&gt;: I/O 요청을 발행하고 완료를 기다리지 않고 즉시 리턴하는 I/O 방식.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Continuation&lt;/code&gt;: 나중에 계산을 재개하기 위해 필요한 상태 정보의 묶음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Manual Stack Management&lt;/code&gt;: 스레드 스택 대신 힙/테이블 등에 상태를 저장하고 이벤트 완료 시 복원하는 방식.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-events.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 33: Event-based Concurrency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/advanced-programming-in/0321637739/&quot;&gt;Advanced Programming in the UNIX Environment (Stevens, Rago)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cs.berkeley.edu/~brewer/papers/seda-sosp01.pdf&quot;&gt;SEDA: An Architecture for Well-Conditioned, Scalable Internet Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cs.princeton.edu/courses/archive/fall03/cos518/papers/flash-usenix99.pdf&quot;&gt;Flash: An Efficient and Portable Web Server&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 32. Common Concurrency Problems</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-32-threads-bugs/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-32-threads-bugs/</guid><description>실제 애플리케이션에서 발생하는 병행성 버그의 유형(원자성 위반, 순서 위반)과 교착 상태(Deadlock)의 원인 및 4가지 예방 전략을 상세히 분석합니다.</description><pubDate>Thu, 05 Feb 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;병행 프로그래밍이 어렵다는 것은 모두가 아는 사실입니다. 하지만 실제 현장에서 어떤 형태의 버그가 자주 터지는지에 대한 체계적인 분석은 생각보다 많지 않습니다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 Lu 등이 수행한 오픈소스 애플리케이션(MySQL, Apache, Mozilla, OpenOffice) 버그 분석 연구를 바탕으로, 병행성 버그를 크게 두 부류로 정리합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비교착(Non-deadlock) 버그&lt;/strong&gt;: 교착 상태가 아닌 대부분의 병행성 버그&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;교착(Deadlock) 버그&lt;/strong&gt;: 락/자원 대기 사이클로 인해 영원히 진행이 멈추는 버그&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;흥미로운 결론은, 전체 병행성 버그 중 &lt;strong&gt;비교착 버그가 더 큰 비중&lt;/strong&gt;을 차지했다는 점입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 비교착 버그 (Non-deadlock Bugs)&lt;/h2&gt;
&lt;p&gt;Lu의 연구에서 비교착 버그는 대표적으로 &lt;strong&gt;원자성 위반(Atomicity Violation)&lt;/strong&gt;, &lt;strong&gt;순서 위반(Order Violation)&lt;/strong&gt; 두 패턴으로 많이 나타났습니다.&lt;/p&gt;
&lt;h3&gt;1.1 원자성 위반 (Atomicity Violation)&lt;/h3&gt;
&lt;p&gt;원자성 위반은 &lt;strong&gt;원자적으로 실행되어야 할 코드 구간이 중간 끼어들기(interleaving)로 깨지는 현상&lt;/strong&gt;입니다. 전형적인 형태는 체크 후 사용(check-then-use) 패턴입니다.&lt;/p&gt;
&lt;p&gt;문제 코드(MySQL 예시):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Thread 1
if (thd-&amp;gt;proc_info) {
    // 이 사이에 다른 스레드가 끼어들 수 있음
    fputs(thd-&amp;gt;proc_info, ...);
}

// Thread 2
thd-&amp;gt;proc_info = NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thread 1이 &lt;code&gt;proc_info != NULL&lt;/code&gt;을 확인한 직후 Thread 2가 &lt;code&gt;proc_info = NULL&lt;/code&gt;로 바꾸면, Thread 1은 &lt;code&gt;fputs()&lt;/code&gt;에서 NULL 포인터 역참조로 크래시가 날 수 있습니다.&lt;/p&gt;
&lt;p&gt;해결책은 간단한 편입니다. &lt;strong&gt;검사와 사용을 하나의 임계 영역으로 묶어&lt;/strong&gt; 직렬화(serialize)하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_t proc_info_lock = PTHREAD_MUTEX_INITIALIZER;

// Thread 1
pthread_mutex_lock(&amp;amp;proc_info_lock);
if (thd-&amp;gt;proc_info) {
    fputs(thd-&amp;gt;proc_info, ...);
}
pthread_mutex_unlock(&amp;amp;proc_info_lock);

// Thread 2
pthread_mutex_lock(&amp;amp;proc_info_lock);
thd-&amp;gt;proc_info = NULL;
pthread_mutex_unlock(&amp;amp;proc_info_lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;원자성 위반을 보는 실전 관점은 이렇습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 변수라도 &lt;strong&gt;검사 → 사용&lt;/strong&gt;이 분리되어 있으면, 그 사이가 취약 지점이 됩니다.&lt;/li&gt;
&lt;li&gt;해결은 보통 &lt;strong&gt;락 추가&lt;/strong&gt;이지만, 해당 변수를 접근하는 모든 경로가 같은 락 규약을 따라야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 순서 위반 (Order Violation)&lt;/h3&gt;
&lt;p&gt;순서 위반은 &lt;strong&gt;두 스레드 사이에 기대한 실행 순서가 강제되지 않아&lt;/strong&gt; 발생합니다. 예를 들어 Thread 2가 Thread 1의 초기화를 전제하는데, 스케줄링에 따라 그 전제가 깨질 수 있습니다.&lt;/p&gt;
&lt;p&gt;문제 코드:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Thread 1
void init() {
    mThread = PR_CreateThread(mMain, ...);
}

// Thread 2
void mMain(...) {
    mState = mThread-&amp;gt;State; // mThread가 아직 NULL이면 크래시
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해결책은 &lt;strong&gt;순서(초기화 완료)를 동기화로 강제&lt;/strong&gt;하는 것입니다. 조건 변수(또는 세마포어)가 정석입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_t mtLock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  mtCond = PTHREAD_COND_INITIALIZER;
int mtInit = 0;

// Thread 1
void init() {
    mThread = PR_CreateThread(mMain, ...);

    pthread_mutex_lock(&amp;amp;mtLock);
    mtInit = 1;
    pthread_cond_signal(&amp;amp;mtCond);
    pthread_mutex_unlock(&amp;amp;mtLock);
}

// Thread 2
void mMain(...) {
    pthread_mutex_lock(&amp;amp;mtLock);
    while (mtInit == 0)
        pthread_cond_wait(&amp;amp;mtCond, &amp;amp;mtLock);
    pthread_mutex_unlock(&amp;amp;mtLock);

    mState = mThread-&amp;gt;State;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서도 핵심은 패턴입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;순서 위반은 결국 &lt;strong&gt;상태 변수(state) + wait/signal&lt;/strong&gt;로 해결합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;while&lt;/code&gt;로 조건을 재확인해야 합니다(Mesa semantics + spurious wakeup).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 교착 상태 (Deadlock)&lt;/h2&gt;
&lt;p&gt;교착 상태는 스레드들이 서로가 가진 자원을 기다리며 &lt;strong&gt;영원히 진행하지 못하는 상태&lt;/strong&gt;입니다. 가장 단순한 형태는 두 락을 서로 반대 순서로 잡는 패턴입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/32_6.png&quot; alt=&quot;Figure 32.6 - Simple deadlock example (deadlock.c)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 32.6: 두 스레드가 &lt;code&gt;L1&lt;/code&gt;, &lt;code&gt;L2&lt;/code&gt;를 교차 순서로 획득하면 교착 상태가 발생할 수 있습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Figure 32.6의 내용을 코드로 적으면 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Thread 1
pthread_mutex_lock(L1);
pthread_mutex_lock(L2);

// Thread 2
pthread_mutex_lock(L2);
pthread_mutex_lock(L1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어떤 실행에서는 우연히 지나가기도 하지만, 특정 인터리빙에서는 둘 다 두 번째 락에서 멈추며 교착이 됩니다. 이 현상은 의존성 그래프로 보면 더 명확합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/32_7.png&quot; alt=&quot;Figure 32.7 - Deadlock dependency graph&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 32.7: 의존성 그래프에서 락/자원 대기 관계가 사이클을 만들면 교착 상태가 발생합니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;의존성 그래프를 $G=(V,E)$ 로 두고, 간선 $a \rightarrow b$ 를 a가 b가 가진 락이나 자원을 기다리는 관계로 정의하면, 교착 상태는 $G$ 에 &lt;strong&gt;사이클(cycle)&lt;/strong&gt; 이 존재할 때 발생한다고 볼 수 있습니다.&lt;/p&gt;
&lt;h3&gt;2.1 교착 상태의 필요조건 (Coffman Conditions)&lt;/h3&gt;
&lt;p&gt;교착 상태는 아래 네 조건이 동시에 만족될 때 발생합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;상호 배제(Mutual Exclusion)&lt;/strong&gt;: 자원을 배타적으로 점유합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;점유 및 대기(Hold-and-Wait)&lt;/strong&gt;: 자원을 가진 채로 다른 자원을 기다립니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비선점(No Preemption)&lt;/strong&gt;: 자원을 강제로 빼앗을 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;순환 대기(Circular Wait)&lt;/strong&gt;: 대기 관계가 사이클을 이룹니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이를 논리식으로 쓰면 다음처럼 요약할 수 있습니다.&lt;/p&gt;
&lt;p&gt;$$
Deadlock \iff ME \land HW \land NP \land CW
$$&lt;/p&gt;
&lt;p&gt;즉, 네 조건 중 &lt;strong&gt;하나라도 깨면&lt;/strong&gt; 교착 상태를 예방할 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 교착 상태 예방 (Deadlock Prevention)&lt;/h2&gt;
&lt;p&gt;예방 전략은 결국 Coffman 조건 중 하나를 의도적으로 깨는 방식입니다.&lt;/p&gt;
&lt;h3&gt;3.1 순환 대기 예방: 락 순서 정하기 (Lock Ordering)&lt;/h3&gt;
&lt;p&gt;가장 실용적이면서 자주 쓰는 방법이 &lt;strong&gt;락 획득 순서를 강제&lt;/strong&gt;하는 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;항상 &lt;code&gt;L1 → L2&lt;/code&gt; 순서로만 잡게 만들면, 사이클이 생길 수 없습니다.&lt;/li&gt;
&lt;li&gt;복잡한 시스템에서는 전체 순서(total ordering) 대신 부분 순서(partial ordering)를 설계하기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다중 락을 인자로 받는 함수가 문제를 만들 수 있는데, 이때는 &lt;strong&gt;락 주소값으로 순서를 강제&lt;/strong&gt;하는 테크닉이 실용적입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void do_something(pthread_mutex_t *m1, pthread_mutex_t *m2) {
    if (m1 == m2) return; // 동일 락이면 특별 처리

    if (m1 &amp;gt; m2) { // 주소 큰 것부터(또는 반대로) 일관되게
        pthread_mutex_lock(m1);
        pthread_mutex_lock(m2);
    } else {
        pthread_mutex_lock(m2);
        pthread_mutex_lock(m1);
    }

    // ... critical section ...

    pthread_mutex_unlock(m1);
    pthread_mutex_unlock(m2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;락 순서는 결국 규약입니다. 규약이 깨지는 순간 교착은 다시 가능해지므로, 코드베이스 규모가 커질수록 문서화와 리뷰 규율이 중요해집니다.&lt;/p&gt;
&lt;h3&gt;3.2 점유 및 대기 예방: 한 번에 모두 획득하기&lt;/h3&gt;
&lt;p&gt;점유 및 대기 조건을 깨려면 &lt;strong&gt;필요한 락을 한 번에 원자적으로 획득&lt;/strong&gt;하면 됩니다. 전역 prevention 락으로 락 획득 구간을 감싸는 방식이 대표적입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_lock(&amp;amp;prevention);
pthread_mutex_lock(L1);
pthread_mutex_lock(L2);
// ...
pthread_mutex_unlock(&amp;amp;prevention);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;단점이 명확합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;병행성이 크게 줄어듭니다.&lt;/li&gt;
&lt;li&gt;필요한 락을 미리 알아야 하므로 캡슐화/모듈성이 발목을 잡습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 비선점 예방: trylock으로 후퇴(back off)하기&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pthread_mutex_trylock()&lt;/code&gt;을 이용하면, 두 번째 락 획득 실패 시 &lt;strong&gt;이미 잡은 락을 풀고 재시도&lt;/strong&gt;할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top:
pthread_mutex_lock(L1);
if (pthread_mutex_trylock(L2) != 0) {
    pthread_mutex_unlock(L1);
    // livelock 완화를 위해 랜덤/지수 backoff를 두는 것이 일반적
    goto top;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 교착은 피하지만, 서로가 계속 양보만 하는 &lt;strong&gt;라이브락(livelock)&lt;/strong&gt; 이 발생할 수 있습니다. 실무에서는 랜덤 딜레이, exponential backoff 같은 완화책을 함께 둡니다.&lt;/p&gt;
&lt;h3&gt;3.4 상호 배제 예방: 락 없는(lock-free) 접근&lt;/h3&gt;
&lt;p&gt;가장 강한 형태는 락 자체를 없애 &lt;strong&gt;상호 배제 조건을 성립하지 않게&lt;/strong&gt; 만드는 것입니다. 대표가 CAS 기반의 lock-free 설계입니다.&lt;/p&gt;
&lt;p&gt;CAS 정의:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int CompareAndSwap(int *addr, int expected, int newv) {
    if (*addr == expected) {
        *addr = newv;
        return 1; // success
    }
    return 0; // failure
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CAS로 구현한 원자적 증가:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AtomicIncrement(int *value, int amount) {
    int old, newv;
    do {
        old  = *value;
        newv = old + amount;
    } while (CompareAndSwap(value, old, newv) == 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;락이 없으니 교착은 원천적으로 사라지지만, 구현이 어렵고, 다른 형태의 진행 실패(예: livelock/기아) 가능성도 남습니다. 또한 유용한 자료구조를 lock-free로 완성하려면 훨씬 더 많은 고려가 필요합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 회피와 탐지 (Avoidance and Detection)&lt;/h2&gt;
&lt;h3&gt;4.1 회피 (Avoidance)&lt;/h3&gt;
&lt;p&gt;스레드가 어떤 락을 필요로 하는지 &lt;strong&gt;사전에 알고&lt;/strong&gt; 있다면, 스케줄러가 교착이 나지 않도록 실행을 배치할 수 있습니다. 은행원 알고리즘류가 여기에 속합니다.&lt;/p&gt;
&lt;p&gt;하지만 범용 OS/일반 애플리케이션에서는 전역적인 요구(lock demand)를 정확히 아는 것이 어려워 실용성이 제한됩니다. 회피는 종종 병행성을 희생하며, 결과적으로 성능 손해로 이어지기도 합니다.&lt;/p&gt;
&lt;h3&gt;4.2 탐지와 복구 (Detect and Recover)&lt;/h3&gt;
&lt;p&gt;교착을 완전히 막지 않고, &lt;strong&gt;발생을 허용한 뒤 주기적으로 탐지&lt;/strong&gt;하는 전략도 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자원(락) 대기 그래프를 만들고 사이클을 검사합니다.&lt;/li&gt;
&lt;li&gt;사이클 발견 시 스레드/트랜잭션을 중단하거나, 재시작으로 복구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;데이터베이스 시스템이 대표적이며, 교착이 드물고 복구 비용이 감당 가능할 때 현실적인 접근이 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비교착 버그는 실무에서 흔하며, 특히 &lt;strong&gt;원자성 위반&lt;/strong&gt;과 &lt;strong&gt;순서 위반&lt;/strong&gt; 패턴이 큰 비중을 차지합니다.
&lt;ul&gt;
&lt;li&gt;원자성 위반: check-then-use 같은 구간을 락으로 묶어 직렬화&lt;/li&gt;
&lt;li&gt;순서 위반: 상태 변수 + 조건 변수/세마포어로 순서를 강제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;교착은 네 조건이 동시에 만족될 때 발생하며,
$$
Deadlock \iff ME \land HW \land NP \land CW
$$
네 조건 중 하나를 깨는 방식으로 예방할 수 있습니다.&lt;/li&gt;
&lt;li&gt;현실에서 가장 실용적인 방어는 보통 &lt;strong&gt;락 순서 규약(lock ordering)&lt;/strong&gt; 을 세우고 지키는 것입니다.&lt;/li&gt;
&lt;li&gt;lock-free/새 병행 모델은 가능성이 크지만, 일반적으로는 복잡도가 높아 널리 적용되기 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;병행 프로그램을 완벽하게 만드는 것은 어렵습니다. 따라서 처음에는 단순한 동기화로 정확성을 확보하고, 패턴 기반으로 위험 구간(원자성/순서)을 점검하며, 락 순서 같은 규약을 통해 교착 가능성을 줄이는 접근이 가장 현실적입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Non-deadlock bug (비교착 버그)&lt;/code&gt;: 교착 상태가 아닌 병행성 버그. 원자성 위반, 순서 위반 등이 대표적.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Atomicity Violation (원자성 위반)&lt;/code&gt;: 원자적으로 실행되어야 할 코드 구간이 인터리빙으로 깨지는 버그.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Order Violation (순서 위반)&lt;/code&gt;: 스레드 간 필요한 실행 순서가 강제되지 않아 발생하는 버그.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Deadlock (교착 상태)&lt;/code&gt;: 둘 이상의 스레드가 서로 상대의 자원을 기다리며 무한 대기하는 상태.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Coffman Conditions&lt;/code&gt;: 교착이 발생하기 위한 네 가지 필요조건(상호 배제, 점유 및 대기, 비선점, 순환 대기).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Livelock (라이브락)&lt;/code&gt;: 스레드들이 실행은 계속하지만 충돌 회피만 반복하여 실제 진전이 없는 상태.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Lock-free / Wait-free&lt;/code&gt;: 락 없이(또는 모든 연산이 유한 단계 내 종료하도록) 동기화를 달성하는 설계 철학.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-bugs.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 32: Common Concurrency Problems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Lu et al., Learning from Mistakes: A Comprehensive Study on Real World Concurrency Bug Characteristics, ASPLOS 2008&lt;/li&gt;
&lt;li&gt;Coffman et al., System Deadlocks, ACM Computing Surveys, 1971&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 31. Semaphores</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-31-threads-sema/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-31-threads-sema/</guid><description>락과 조건 변수를 대체할 수 있는 강력한 동기화 도구인 세마포어의 정의와 사용법을 다룹니다. 생산자/소비자, 읽기/쓰기 락, 식사하는 철학자 문제 등 다양한 병행성 문제의 해결책을 제시합니다.</description><pubDate>Thu, 05 Feb 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장들에서 우리는 락(Lock)과 조건 변수(Condition Variable)로 병행성 문제를 해결했습니다. 하지만 Edsger Dijkstra는 이 둘을 아우르는 단일 동기화 원시(primitive)인 &lt;strong&gt;세마포어(Semaphore)&lt;/strong&gt; 를 제안했습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 세마포어의 정의를 정리하고, 이를 이용해 &lt;strong&gt;상호 배제&lt;/strong&gt;, &lt;strong&gt;순서 제어&lt;/strong&gt;, 그리고 고전적인 동기화 문제들을 어떻게 풀어내는지 살펴봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 세마포어의 정의&lt;/h2&gt;
&lt;p&gt;세마포어는 정수 값을 가진 객체이며, 오직 두 연산을 통해서만 조작됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sem_wait()&lt;/code&gt;  (P 연산)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sem_post()&lt;/code&gt;  (V 연산)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;POSIX 세마포어의 기본 사용 형태는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;semaphore.h&amp;gt;

sem_t s;
sem_init(&amp;amp;s, 0, 1);  // 3번째 인자가 초기 값
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 두 번째 인자 &lt;code&gt;pshared&lt;/code&gt;는 &lt;code&gt;0&lt;/code&gt;이면 같은 프로세스의 스레드 간 공유, &lt;code&gt;0&lt;/code&gt;이 아니면 프로세스 간 공유를 의미합니다.&lt;/p&gt;
&lt;p&gt;세마포어의 초기 값은 동작을 사실상 결정합니다. 앞으로의 예제에서 초기 값이 왜 중요한지 계속 확인하게 됩니다.&lt;/p&gt;
&lt;h3&gt;1.1 두 가지 핵심 연산&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sem_wait()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세마포어 값을 1 감소시킵니다.&lt;/li&gt;
&lt;li&gt;감소 결과가 0 미만이면, 호출 스레드는 더 진행할 수 없으므로 대기(sleep)합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sem_post()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세마포어 값을 1 증가시킵니다.&lt;/li&gt;
&lt;li&gt;대기 중인 스레드가 있다면 그중 하나를 깨웁니다(wake).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 장에서 자주 쓰는 직관적인 모델은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;세마포어의 내부 값 &lt;code&gt;S&lt;/code&gt;가 0 이상이면 자원이 남아 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S &amp;lt; 0&lt;/code&gt;이면 대기 중인 스레드가 존재하며, 그 수는 대략 &lt;code&gt;$|S|$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$S &amp;lt; 0 \Rightarrow \text{waiting threads} \approx |S|$$&lt;/p&gt;
&lt;p&gt;실제 구현은 음수 값을 외부에 드러내지 않을 수 있지만, &lt;strong&gt;의미를 이해하기 위한 모델&lt;/strong&gt;로는 유용합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 이진 세마포어&lt;/h2&gt;
&lt;p&gt;세마포어를 락처럼 쓰려면 초기 값을 1로 두면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sem_t m;
sem_init(&amp;amp;m, 0, 1);  // 초기값 1

sem_wait(&amp;amp;m);        // 락 획득
// 임계 영역 (Critical Section)
sem_post(&amp;amp;m);        // 락 해제
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 0과 1 중심으로 동작하는 세마포어를 &lt;strong&gt;이진 세마포어(binary semaphore)&lt;/strong&gt; 라고 부릅니다.&lt;/p&gt;
&lt;p&gt;동작 흐름은 다음처럼 이해하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드 A가 &lt;code&gt;wait&lt;/code&gt;를 호출하면 &lt;code&gt;1 -&amp;gt; 0&lt;/code&gt;이 되고 바로 진입합니다.&lt;/li&gt;
&lt;li&gt;스레드 B가 뒤이어 &lt;code&gt;wait&lt;/code&gt;를 호출하면 더 줄어들며 대기합니다.&lt;/li&gt;
&lt;li&gt;스레드 A가 &lt;code&gt;post&lt;/code&gt;를 호출하면 값이 증가하고, 대기 중인 스레드 하나가 깨어납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 순서 제어용 세마포어&lt;/h2&gt;
&lt;p&gt;조건 변수에서 했던 join 패턴을 세마포어로 구현할 수 있습니다. 핵심은 초기 값을 0으로 두는 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sem_t s;
sem_init(&amp;amp;s, 0, 0);  // 초기값 0

void *child(void *arg) {
    printf(&quot;child\n&quot;);
    sem_post(&amp;amp;s);    // 완료 신호
    return NULL;
}

int main() {
    pthread_t c;
    Pthread_create(&amp;amp;c, NULL, child, NULL);

    sem_wait(&amp;amp;s);    // 자식이 끝날 때까지 대기
    printf(&quot;parent: end\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 보장하고 싶은 것은 이 불변식입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부모가 출력하는 parent: end는 항상 child 출력 이후에 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 실행 순서를 다음처럼 고정합니다.&lt;/p&gt;
&lt;p&gt;$$child \prec parent$$&lt;/p&gt;
&lt;p&gt;부모가 먼저 &lt;code&gt;wait&lt;/code&gt;를 호출해도, 자식이 먼저 &lt;code&gt;post&lt;/code&gt;를 호출해도 결과는 동일합니다. 초기값을 0으로 두면, 세마포어 값이 &lt;strong&gt;완료 사실을 기록&lt;/strong&gt;해주기 때문입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 생산자/소비자 문제&lt;/h2&gt;
&lt;p&gt;유한 버퍼 문제(Bounded Buffer Problem)를 세마포어로 풀어봅니다. 보통 3개의 세마포어를 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;empty&lt;/code&gt;: 빈 슬롯 수, 초기값 &lt;code&gt;MAX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;full&lt;/code&gt;: 채워진 슬롯 수, 초기값 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt;: 버퍼 접근 상호 배제, 초기값 &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 버퍼 상태는 보통 다음 불변식을 만족합니다.&lt;/p&gt;
&lt;p&gt;$$0 \le full \le MAX$$
$$0 \le empty \le MAX$$
$$full + empty = MAX$$&lt;/p&gt;
&lt;h3&gt;4.1 구현 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void *producer(void *arg) {
    for (int i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;empty);   // 빈 공간 확보
        sem_wait(&amp;amp;mutex);   // 버퍼 상호 배제

        put(i);

        sem_post(&amp;amp;mutex);
        sem_post(&amp;amp;full);    // 채워짐 알림
    }
    return NULL;
}

void *consumer(void *arg) {
    for (int i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;full);    // 데이터 대기
        sem_wait(&amp;amp;mutex);   // 버퍼 상호 배제

        int tmp = get();

        sem_post(&amp;amp;mutex);
        sem_post(&amp;amp;empty);   // 빈 공간 알림

        printf(&quot;%d\n&quot;, tmp);
    }
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 주의점: 교착 상태를 피하는 획득 순서&lt;/h3&gt;
&lt;p&gt;중요한 규칙은 락(&lt;code&gt;mutex&lt;/code&gt;)을 먼저 잡고 조건(&lt;code&gt;empty/full&lt;/code&gt;)을 기다리지 않는 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;생산자가 &lt;code&gt;mutex&lt;/code&gt;를 잡은 채로 &lt;code&gt;empty&lt;/code&gt;를 기다리면, 소비자는 &lt;code&gt;mutex&lt;/code&gt;를 얻지 못해 &lt;code&gt;get()&lt;/code&gt;을 못 합니다.&lt;/li&gt;
&lt;li&gt;결국 둘 다 진행 불가능해져 &lt;strong&gt;교착 상태(deadlock)&lt;/strong&gt; 로 이어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 위 예제처럼 항상 다음 순서를 유지합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;생산자: &lt;code&gt;empty -&amp;gt; mutex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;소비자: &lt;code&gt;full -&amp;gt; mutex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 읽기/쓰기 락&lt;/h2&gt;
&lt;p&gt;읽기/쓰기 락은 여러 스레드의 동시 읽기를 허용하지만, 쓰기는 배타적으로 수행하게 합니다.&lt;/p&gt;
&lt;p&gt;핵심 아이디어는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;readers&lt;/code&gt;라는 공유 카운터로 현재 리더 수를 추적&lt;/li&gt;
&lt;li&gt;첫 번째 리더가 들어올 때 &lt;code&gt;writelock&lt;/code&gt;을 잠가 writer 진입을 막고&lt;/li&gt;
&lt;li&gt;마지막 리더가 나갈 때 &lt;code&gt;writelock&lt;/code&gt;을 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;typedef struct _rwlock_t {
    sem_t lock;       // readers 변수 보호
    sem_t writelock;  // writer 제어
    int  readers;     // 현재 리더 수
} rwlock_t;

void rwlock_acquire_readlock(rwlock_t *rw) {
    sem_wait(&amp;amp;rw-&amp;gt;lock);
    rw-&amp;gt;readers++;
    if (rw-&amp;gt;readers == 1)         // 첫 리더가 writer를 막음
        sem_wait(&amp;amp;rw-&amp;gt;writelock);
    sem_post(&amp;amp;rw-&amp;gt;lock);
}

void rwlock_release_readlock(rwlock_t *rw) {
    sem_wait(&amp;amp;rw-&amp;gt;lock);
    rw-&amp;gt;readers--;
    if (rw-&amp;gt;readers == 0)         // 마지막 리더가 writer를 풀어줌
        sem_post(&amp;amp;rw-&amp;gt;writelock);
    sem_post(&amp;amp;rw-&amp;gt;lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 읽기 비중이 높을 때 성능을 높여주지만, 리더가 계속 유입되면 writer가 영원히 기다릴 수 있는 &lt;strong&gt;기아(starvation)&lt;/strong&gt; 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 식사하는 철학자 문제&lt;/h2&gt;
&lt;p&gt;5명의 철학자가 원탁에 앉아 있고, 양옆의 포크 2개를 모두 집어야 식사할 수 있다는 고전 문제입니다.&lt;/p&gt;
&lt;p&gt;교착 상태는 다음 상황에서 발생합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 철학자가 동시에 왼쪽 포크를 집음&lt;/li&gt;
&lt;li&gt;각자 오른쪽 포크가 풀리길 기다림&lt;/li&gt;
&lt;li&gt;순환 대기(circular wait)가 성립하여 진행이 멈춤&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.1 해결책: 순환 대기 깨기&lt;/h3&gt;
&lt;p&gt;한 명만 포크를 집는 순서를 바꾸면 순환 대기를 깰 수 있습니다. 예를 들어 마지막 철학자만 오른쪽 포크를 먼저 집게 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void get_forks(int p) {
    if (p == 4) {
        sem_wait(&amp;amp;forks[right(p)]);
        sem_wait(&amp;amp;forks[left(p)]);
    } else {
        sem_wait(&amp;amp;forks[left(p)]);
        sem_wait(&amp;amp;forks[right(p)]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의존성 그래프에서 한 방향만 뒤집어 cycle을 끊는 전형적인 접근입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 스레드 조절&lt;/h2&gt;
&lt;p&gt;세마포어의 초기 값을 &lt;code&gt;N&lt;/code&gt;으로 설정하면, 특정 구역에 동시에 들어갈 수 있는 스레드 수를 &lt;code&gt;N&lt;/code&gt;개로 제한할 수 있습니다. 이를 &lt;strong&gt;throttling&lt;/strong&gt;이라고 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메모리 집약적 작업이 동시에 과도하게 실행되는 것을 방지&lt;/li&gt;
&lt;li&gt;외부 자원(소켓, 디스크 요청 등) 폭주를 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sem_t throttle;
sem_init(&amp;amp;throttle, 0, N);

sem_wait(&amp;amp;throttle);
// 제한하고 싶은 구역
sem_post(&amp;amp;throttle);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 요약&lt;/h2&gt;
&lt;p&gt;세마포어는 락과 조건 변수의 기능을 모두 수행할 수 있는 강력하고 유연한 동기화 도구입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;핵심은 &lt;strong&gt;초기 값 설정&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;락: &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;순서 제어: &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;스레드 조절: &lt;code&gt;N&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;생산자/소비자, 읽기/쓰기 락, 식사하는 철학자 등 고전 문제를 비교적 간결하게 풀어낼 수 있습니다.&lt;/li&gt;
&lt;li&gt;단, 잘못된 획득 순서나 과도한 깨움은 쉽게 교착 상태나 성능 문제로 이어질 수 있으므로, 항상 &lt;strong&gt;불변식과 획득 순서&lt;/strong&gt;를 먼저 세워두는 습관이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Semaphore&lt;/code&gt;: 정수 값을 가지며 &lt;code&gt;wait&lt;/code&gt;와 &lt;code&gt;post&lt;/code&gt; 연산으로 조작되는 동기화 객체.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Binary Semaphore&lt;/code&gt;: 값이 0 또는 1 중심으로 동작하며 락과 동일한 역할을 하는 세마포어.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Counting Semaphore&lt;/code&gt;: 임의의 정수 값을 가지며 자원의 개수를 세거나 동시 진입 수를 제한할 때 사용됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Producer/Consumer Problem&lt;/code&gt;: 유한 버퍼를 두고 생산자와 소비자가 데이터를 주고받는 문제.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reader-Writer Lock&lt;/code&gt;: 여러 읽기 작업을 동시에 허용하고 쓰기 작업은 배타적으로 수행하는 락.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dining Philosophers Problem&lt;/code&gt;: 한정된 자원을 두고 경쟁할 때 발생 가능한 교착 상태를 다루는 고전 문제.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Throttling&lt;/code&gt;: 동시에 실행되는 스레드의 수를 제한하여 시스템 자원을 보호하는 기법.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-sema.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 31: Semaphores&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 30. Condition Variables</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-30-threads-cv/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-30-threads-cv/</guid><description>락만으로는 해결하기 어려운 대기 문제를 풀기 위한 조건 변수의 개념과 올바른 사용법(wait, signal, broadcast)을 정리합니다</description><pubDate>Wed, 04 Feb 2026 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지금까지는 병행 프로그래밍의 핵심 도구인 락(Lock)을 중심으로 봤습니다. 락은 &lt;strong&gt;상호 배제(Mutual Exclusion)&lt;/strong&gt; 를 제공하지만, 스레드가 특정 &lt;strong&gt;조건(predicate)&lt;/strong&gt; 이 만족될 때까지 기다려야 하는 상황에서는 락만으로는 충분하지 않습니다. 이때 필요한 것이 스레드를 &lt;strong&gt;재우고(sleep)&lt;/strong&gt;, 조건이 만족되면 &lt;strong&gt;깨우는(wake)&lt;/strong&gt; 메커니즘입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 스레드 간 대기(wait)와 신호(signal)를 위한 &lt;strong&gt;조건 변수(Condition Variable)&lt;/strong&gt; 를 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 조건 변수의 개념&lt;/h2&gt;
&lt;p&gt;스레드는 실행을 계속하기 전에 어떤 조건이 참인지 확인해야 할 때가 많습니다. 예를 들어 부모 스레드가 자식 스레드의 작업 완료를 기다리는 join 같은 경우입니다. 단순히 공유 변수를 계속 확인하며 도는 스핀(spin)은 CPU 시간을 낭비하고, 설계가 잘못되면 영원히 기다리는 버그로 이어질 수 있습니다.&lt;/p&gt;
&lt;p&gt;조건 변수는 이런 상황에서 다음을 가능하게 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;wait()&lt;/strong&gt;: 조건이 만족되지 않으면 스레드가 스스로 잠들어(condition wait queue로) 대기합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;signal() / broadcast()&lt;/strong&gt;: 다른 스레드가 상태를 바꾼 뒤, 대기 중인 스레드 하나(signal) 또는 모두(broadcast)를 깨웁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;중요한 관점은 이것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;조건 변수는 조건 그 자체가 아니라, &lt;strong&gt;조건을 기다리는 스레드들이 모이는 큐&lt;/strong&gt; 입니다.&lt;/li&gt;
&lt;li&gt;진짜 조건은 &lt;code&gt;done == 1&lt;/code&gt;, &lt;code&gt;count &amp;gt; 0&lt;/code&gt;, &lt;code&gt;bytesLeft &amp;gt;= size&lt;/code&gt; 같은 &lt;strong&gt;공유 상태 변수 + 불리언 표현식&lt;/strong&gt; 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1 주요 인터페이스&lt;/h3&gt;
&lt;p&gt;POSIX 스레드(Pthreads)에서 자주 쓰는 인터페이스는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
int pthread_cond_signal(pthread_cond_t *c);
int pthread_cond_broadcast(pthread_cond_t *c);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 핵심은 &lt;code&gt;pthread_cond_wait()&lt;/code&gt;가 &lt;strong&gt;mutex를 함께 받는다는 점&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;는 mutex가 &lt;strong&gt;잠긴 상태&lt;/strong&gt;에서 호출되어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;는 내부에서 &lt;strong&gt;mutex 해제 + 대기열 진입 + 잠들기&lt;/strong&gt;를 한 덩어리로 처리합니다.&lt;/li&gt;
&lt;li&gt;깨어난 뒤에는 &lt;strong&gt;mutex를 다시 획득한 상태&lt;/strong&gt;로 리턴합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 동작 덕분에, 잠들기 직전에 신호를 놓치는 종류의 경쟁 조건을 피할 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 부모-자식 스레드 대기 (Join 문제)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pthread_join()&lt;/code&gt;은 이미 존재하지만, 이 절은 join을 조건 변수로 직접 구현하며 올바른 사용 패턴을 익히는 데 목적이 있습니다.&lt;/p&gt;
&lt;h3&gt;2.1 올바른 구현 패턴&lt;/h3&gt;
&lt;p&gt;조건 변수를 사용할 때는 항상 다음 3개를 함께 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;상태 변수(state variable)&lt;/strong&gt;: 예: &lt;code&gt;done&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mutex&lt;/strong&gt;: 상태 변수 접근을 보호&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;condition variable&lt;/strong&gt;: 조건이 만족될 때까지 잠들기/깨우기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;부모 스레드(대기):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pthread_mutex_lock(&amp;amp;m);
while (done == 0)
    Pthread_cond_wait(&amp;amp;c, &amp;amp;m);
Pthread_mutex_unlock(&amp;amp;m);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자식 스레드(상태 변경 + 신호):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pthread_mutex_lock(&amp;amp;m);
done = 1;
Pthread_cond_signal(&amp;amp;c);
Pthread_mutex_unlock(&amp;amp;m);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 핵심 규칙&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;상태 변수는 필수&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;signal()&lt;/code&gt;만으로는 충분하지 않습니다.&lt;/li&gt;
&lt;li&gt;자식이 먼저 실행되어 &lt;code&gt;signal()&lt;/code&gt;을 했는데 그때 아무도 wait 중이 아니라면, 이후 부모는 &lt;code&gt;wait()&lt;/code&gt;에서 영원히 잠들 수 있습니다. 상태 변수는 그 사실(done)을 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;락은 필수&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부모가 &lt;code&gt;done == 0&lt;/code&gt;을 확인한 직후 &lt;code&gt;wait()&lt;/code&gt;로 들어가기 전에 자식이 끼어들어 &lt;code&gt;done = 1; signal()&lt;/code&gt;을 수행하면, 부모는 신호를 놓치고 영원히 잠드는 레이스가 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;의 동작(락 해제와 잠들기의 결합)과 락으로 보호된 상태 변수 접근이 이 문제를 막습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;조건 확인은 while&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;깨어났다고 해서 조건이 참이라는 보장은 없습니다.&lt;/li&gt;
&lt;li&gt;따라서 &lt;code&gt;if&lt;/code&gt;가 아니라 &lt;strong&gt;while로 조건을 재확인&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;추가로 실전 팁을 하나만 덧붙이면, &lt;code&gt;signal()&lt;/code&gt;도 가능하면 락을 잡은 상태에서 호출하는 습관이 안전합니다. 상태 변수를 바꾼 뒤 같은 락을 잡은 채로 &lt;code&gt;signal()&lt;/code&gt; 하면, 신호와 상태 변화의 관계를 읽기 쉬워지고 실수를 줄일 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 생산자/소비자 문제 (Producer/Consumer, Bounded Buffer)&lt;/h2&gt;
&lt;p&gt;여러 생산자가 버퍼에 넣고, 여러 소비자가 버퍼에서 꺼내는 대표 문제입니다. 버퍼가 비어 있거나 가득 찼을 때는 적절히 잠들어야 하므로 락만으로는 해결할 수 없습니다.&lt;/p&gt;
&lt;h3&gt;3.1 Mesa semantics와 while 루프&lt;/h3&gt;
&lt;p&gt;대부분의 시스템에서 &lt;code&gt;signal()&lt;/code&gt;은 단지 &lt;strong&gt;상태가 바뀌었을 수 있다는 힌트&lt;/strong&gt;입니다. 깨어난 스레드가 곧바로 실행된다는 보장도 없고, 실행될 때 조건이 여전히 참이라는 보장도 없습니다(Mesa semantics).&lt;/p&gt;
&lt;p&gt;따라서 다음이 항상 안전한 규칙입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (count == 0)                 // if가 아니라 while
    Pthread_cond_wait(&amp;amp;fill, &amp;amp;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또한 구현과 스케줄링 세부에 따라 신호 없이도 깨어나는 &lt;strong&gt;spurious wakeup&lt;/strong&gt;이 있을 수 있으므로, while 재확인은 방어적으로도 필요합니다.&lt;/p&gt;
&lt;h3&gt;3.2 단일 조건 변수로는 부족한 이유&lt;/h3&gt;
&lt;p&gt;버퍼가 비었음과 가득 참은 서로 다른 조건입니다. 그런데 단일 조건 변수 하나로 관리하면, 소비자가 &lt;code&gt;signal()&lt;/code&gt; 했을 때 &lt;strong&gt;다른 소비자&lt;/strong&gt;를 깨우는 식의 잘못된 깨움이 가능해집니다. 깨어난 스레드는 조건을 만족하지 못해 다시 잠들고, 운이 나쁘면 모두가 잠드는 상태로 빠질 수 있습니다.&lt;/p&gt;
&lt;h3&gt;3.3 해결책: 두 개의 조건 변수&lt;/h3&gt;
&lt;p&gt;따라서 보통 다음처럼 조건 변수를 2개 둡니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;empty&lt;/code&gt;: 생산자가 기다리는 조건(버퍼에 빈 칸이 생김)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fill&lt;/code&gt;: 소비자가 기다리는 조건(버퍼에 데이터가 생김)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단일 슬롯(1-entry) 버퍼의 올바른 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Producer
Pthread_mutex_lock(&amp;amp;mutex);
while (count == 1)                 // 가득 찼으면 대기
    Pthread_cond_wait(&amp;amp;empty, &amp;amp;mutex);

put(i);                            // count, buffer 상태 변경
Pthread_cond_signal(&amp;amp;fill);        // 소비자 깨우기
Pthread_mutex_unlock(&amp;amp;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Consumer
Pthread_mutex_lock(&amp;amp;mutex);
while (count == 0)                 // 비었으면 대기
    Pthread_cond_wait(&amp;amp;fill, &amp;amp;mutex);

tmp = get();                       // count, buffer 상태 변경
Pthread_cond_signal(&amp;amp;empty);       // 생산자 깨우기
Pthread_mutex_unlock(&amp;amp;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 슬롯(MAX) 버퍼로 일반화하면 조건만 &lt;code&gt;count == MAX&lt;/code&gt; / &lt;code&gt;count == 0&lt;/code&gt;으로 바뀝니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 포함 조건 (Covering Conditions)&lt;/h2&gt;
&lt;p&gt;누구를 깨워야 할지 정확히 결정하기 어려운 상황이 있습니다. 예를 들어 메모리 할당기에서:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A: &lt;code&gt;allocate(100)&lt;/code&gt; 대기&lt;/li&gt;
&lt;li&gt;B: &lt;code&gt;allocate(10)&lt;/code&gt; 대기&lt;/li&gt;
&lt;li&gt;C: &lt;code&gt;free(50)&lt;/code&gt; 호출 후 &lt;code&gt;signal()&lt;/code&gt; 한 번&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 &lt;code&gt;signal()&lt;/code&gt;이 A를 깨우면 A는 다시 잠들고, 실제로 진행 가능한 B는 계속 잠들 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4.1 해결책: broadcast&lt;/h3&gt;
&lt;p&gt;이 경우는 &lt;code&gt;signal()&lt;/code&gt; 대신 &lt;code&gt;pthread_cond_broadcast()&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;broadcast&lt;/code&gt;는 대기 중인 모든 스레드를 깨웁니다.&lt;/li&gt;
&lt;li&gt;각 스레드는 깨어난 뒤 락을 획득하고 조건을 재확인합니다.&lt;/li&gt;
&lt;li&gt;실행 가능한 스레드만 진행하고, 나머지는 다시 잠듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 패턴을 포함 조건(covering condition)이라고 부릅니다. 불필요한 깨움이 생길 수 있지만, 어떤 스레드를 깨워야 하는지 정보가 부족한 상황에서는 가장 보수적이면서 올바른 해결책이 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약&lt;/h2&gt;
&lt;p&gt;조건 변수는 락과 함께 병행 프로그래밍의 필수 도구입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;조건 변수는 조건을 기다리는 스레드를 재우고 깨우기 위한 큐입니다. 조건은 상태 변수와 그에 대한 불리언 표현식입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;와 &lt;code&gt;signal()&lt;/code&gt;은 상태 변수를 보호하는 동일한 mutex로 묶어 다루는 것이 안전합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;는 반드시 &lt;strong&gt;while&lt;/strong&gt;로 감싸 조건을 재확인해야 합니다(Mesa semantics + spurious wakeup).&lt;/li&gt;
&lt;li&gt;생산자/소비자 문제에서는 보통 &lt;code&gt;empty&lt;/code&gt;/&lt;code&gt;fill&lt;/code&gt;처럼 &lt;strong&gt;서로 다른 조건을 서로 다른 CV로 분리&lt;/strong&gt;해야 안전합니다.&lt;/li&gt;
&lt;li&gt;누구를 깨워야 할지 모호한 포함 조건 상황에서는 &lt;strong&gt;broadcast&lt;/strong&gt;가 가장 단순하고 안전한 선택입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Condition Variable (조건 변수)&lt;/code&gt;: 스레드가 특정 조건이 참이 될 때까지 대기(sleep)할 수 있게 해주는 동기화 객체.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Wait&lt;/code&gt;: 락을 해제하고 스레드를 잠재우는 연산. 깨어날 때 다시 락을 획득함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Signal&lt;/code&gt;: 대기 중인 스레드 중 하나를 깨우는 연산.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Broadcast&lt;/code&gt;: 대기 중인 모든 스레드를 깨우는 연산.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mesa Semantics&lt;/code&gt;: Signal은 힌트에 가깝고, 깨어난 스레드가 실행될 때 조건이 여전히 참이라는 보장은 없다는 의미.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spurious Wakeup (허위 기상)&lt;/code&gt;: 신호가 없는데도 스레드가 깨어나는 현상. &lt;code&gt;while&lt;/code&gt; 루프로 방어해야 함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Covering Condition (포함 조건)&lt;/code&gt;: 어떤 스레드를 깨워야 할지 모를 때, 모든 스레드를 깨워 스스로 조건을 확인하게 하는 기법.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 30: Condition Variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 29. Lock-based Concurrent Data Structures</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-29-threads-locks-usage/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-29-threads-locks-usage/</guid><description>락을 사용하여 스레드 안전(Thread-safe)하면서도 고성능을 내는 자료구조를 만드는 방법을 다룹니다. 근사 카운터, 동시성 큐, 해시 테이블의 구현 전략을 살펴봅니다.</description><pubDate>Wed, 04 Feb 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 장에서 락(Lock)의 기본 개념과 구현을 배웠습니다. 이번 장에서는 그 락을 사용해 &lt;strong&gt;스레드 안전(Thread-safe)&lt;/strong&gt; 하면서도 &lt;strong&gt;고성능&lt;/strong&gt;을 내는 자료구조를 어떻게 설계할지 살펴봅니다.&lt;/p&gt;
&lt;p&gt;핵심 질문은 이것입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;락을 추가하면서도 병렬 성능을 어떻게 유지할 수 있을까?&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 병행 카운터 (Concurrent Counters)&lt;/h2&gt;
&lt;p&gt;가장 단순한 자료구조인 카운터부터 시작합니다.&lt;/p&gt;
&lt;h3&gt;1.1 단순한 방식: 거대한 락 하나&lt;/h3&gt;
&lt;p&gt;가장 쉬운 방법은 카운터 전체를 하나의 락으로 보호하는 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void increment(counter_t *c) {
    Pthread_mutex_lock(&amp;amp;c-&amp;gt;lock);
    c-&amp;gt;value++;
    Pthread_mutex_unlock(&amp;amp;c-&amp;gt;lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정확성은 보장되지만, &lt;strong&gt;확장성(Scalability)&lt;/strong&gt; 은 매우 나쁩니다. 코어 수가 늘어도 성능이 좋아지기 어렵고, 오히려 락 경쟁 때문에 급격히 느려질 수 있습니다.&lt;/p&gt;
&lt;h3&gt;1.2 해결책: 근사 카운터 (Approximate Counter, Sloppy Counter)&lt;/h3&gt;
&lt;p&gt;확장성 문제를 완화하기 위해 &lt;strong&gt;근사 카운터(approximate counter)&lt;/strong&gt; 를 소개합니다. 핵심은 하나의 논리적 카운터를 다음처럼 분해하는 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;로컬 카운터(Local Counters)&lt;/strong&gt;: CPU별로 따로 두는 카운터&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;글로벌 카운터(Global Counter)&lt;/strong&gt;: 전체 합을 대표하는 공유 카운터&lt;/li&gt;
&lt;li&gt;락도 두 종류가 필요합니다
&lt;ul&gt;
&lt;li&gt;CPU별 로컬 락&lt;/li&gt;
&lt;li&gt;글로벌 락&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;작동 원리는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;스레드는 &lt;strong&gt;현재 실행 중인 CPU&lt;/strong&gt;의 로컬 카운터를 증가시킵니다.&lt;br /&gt;
같은 CPU에서 여러 스레드가 실행될 수 있으므로 로컬 카운터도 락으로 보호합니다.&lt;/li&gt;
&lt;li&gt;로컬 값이 임계값 &lt;code&gt;S&lt;/code&gt;에 도달하면 글로벌 락을 잡고
&lt;ul&gt;
&lt;li&gt;글로벌 카운터에 로컬 값을 더한 뒤&lt;/li&gt;
&lt;li&gt;로컬 값을 0으로 되돌립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이때 생기는 트레이드오프가 핵심입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt;가 작을수록 글로벌 값이 더 정확하지만, 글로벌 락을 자주 잡아 성능이 떨어집니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt;가 클수록 로컬 업데이트가 대부분이라 성능은 좋아지지만, 글로벌 값이 실제 값보다 늦게 따라갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;글로벌 카운터의 지연은 최대 대략 &lt;strong&gt;CPU 개수 × S&lt;/strong&gt; 정도까지 날 수 있습니다. 또한 모든 로컬 락과 글로벌 락을 특정 순서로 잡으면 정확한 값을 얻을 수는 있지만, 그 방식 자체가 확장성을 해칩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 병행 연결 리스트 (Concurrent Linked Lists)&lt;/h2&gt;
&lt;p&gt;다음은 연결 리스트입니다. 여기서는 삭제 같은 연산은 생략하고, &lt;strong&gt;삽입(insert)&lt;/strong&gt; 과 &lt;strong&gt;조회(lookup)&lt;/strong&gt; 를 중심으로 정리합니다.&lt;/p&gt;
&lt;h3&gt;2.1 기본 접근: 단일 락&lt;/h3&gt;
&lt;p&gt;가장 표준적인 방식은 리스트 전체를 하나의 락으로 감싸는 것입니다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 포인트는 제어 흐름입니다. 단순 구현은 &lt;code&gt;malloc()&lt;/code&gt; 실패 같은 희귀 경로에서 &lt;strong&gt;반드시 unlock을 해줘야&lt;/strong&gt; 하며, 이런 예외 경로는 버그가 숨어들기 쉽습니다. 따라서 락 기반 코드에서는 반환 경로를 단순하게 유지하는 습관이 중요합니다.&lt;/p&gt;
&lt;h3&gt;2.2 개선: 락 범위 최소화 + 단일 반환 경로&lt;/h3&gt;
&lt;p&gt;실전에서 바로 적용하기 좋은 개선은 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;insert에서 &lt;code&gt;malloc()&lt;/code&gt;은 스레드 안전하다고 가정하고 &lt;strong&gt;락 밖에서 수행&lt;/strong&gt;해서 임계 영역을 줄입니다.&lt;/li&gt;
&lt;li&gt;lookup은 성공과 실패가 같은 경로로 빠져나가도록 만들어 &lt;strong&gt;락 해제 누락 위험을 줄입니다&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Insert: malloc은 락 밖에서, 공유 리스트 갱신만 락으로 보호
int List_Insert(list_t *L, int key) {
    node_t *new = malloc(sizeof(node_t));
    if (new == NULL) return -1;
    new-&amp;gt;key = key;

    Pthread_mutex_lock(&amp;amp;L-&amp;gt;lock);
    new-&amp;gt;next = L-&amp;gt;head;
    L-&amp;gt;head = new;
    Pthread_mutex_unlock(&amp;amp;L-&amp;gt;lock);
    return 0;
}

// Lookup: 단일 반환 경로로 정리
int List_Lookup(list_t *L, int key) {
    int rv = -1;
    Pthread_mutex_lock(&amp;amp;L-&amp;gt;lock);
    node_t *curr = L-&amp;gt;head;
    while (curr) {
        if (curr-&amp;gt;key == key) { rv = 0; break; }
        curr = curr-&amp;gt;next;
    }
    Pthread_mutex_unlock(&amp;amp;L-&amp;gt;lock);
    return rv;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 확장성 시도: Hand-over-hand locking&lt;/h3&gt;
&lt;p&gt;리스트의 병렬성을 높이기 위한 대표 기법이 &lt;strong&gt;hand-over-hand locking(lock coupling)&lt;/strong&gt; 입니다. 노드마다 락을 두고, 순회할 때 다음 노드 락을 잡은 뒤 현재 노드 락을 푸는 방식입니다.&lt;/p&gt;
&lt;p&gt;이론적으로는 동시 순회가 가능해 보이지만, 실제로는 노드마다 락을 잡고 푸는 오버헤드가 커서 단일 락 방식보다 느린 경우가 많습니다. 필요하다면 일정 노드마다 락을 잡는 하이브리드도 아이디어로 생각해볼 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 병행 큐 (Concurrent Queues)&lt;/h2&gt;
&lt;p&gt;큐는 큰 락 하나로도 만들 수 있지만, 락 분할로 동시성을 높인 설계가 널리 알려져 있습니다.&lt;/p&gt;
&lt;h3&gt;3.1 마이클-스콧 큐의 핵심 아이디어&lt;/h3&gt;
&lt;p&gt;Michael and Scott이 제안한 큐는 다음을 사용합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;두 개의 락&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Enqueue&lt;/code&gt;는 &lt;code&gt;Tail&lt;/code&gt; 락만 잡습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dequeue&lt;/code&gt;는 &lt;code&gt;Head&lt;/code&gt; 락만 잡습니다.&lt;/li&gt;
&lt;li&gt;따라서 삽입과 삭제가 서로 다른 락을 사용해 동시 수행될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;더미 노드(Dummy Node)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;초기화 시 빈 노드를 하나 만들어 &lt;code&gt;head&lt;/code&gt;와 &lt;code&gt;tail&lt;/code&gt;이 같은 노드를 가리키게 둡니다.&lt;/li&gt;
&lt;li&gt;이 더미 노드가 경계 조건을 단순화해서 head와 tail 작업을 분리할 수 있게 해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void Queue_Enqueue(queue_t *q, int value) {
    node_t *tmp = malloc(sizeof(node_t));
    tmp-&amp;gt;value = value;
    tmp-&amp;gt;next = NULL;

    Pthread_mutex_lock(&amp;amp;q-&amp;gt;tail_lock);
    q-&amp;gt;tail-&amp;gt;next = tmp;
    q-&amp;gt;tail = tmp;
    Pthread_mutex_unlock(&amp;amp;q-&amp;gt;tail_lock);
}

int Queue_Dequeue(queue_t *q, int *value) {
    Pthread_mutex_lock(&amp;amp;q-&amp;gt;head_lock);
    node_t *tmp = q-&amp;gt;head;
    node_t *new_head = tmp-&amp;gt;next;
    if (new_head == NULL) {
        Pthread_mutex_unlock(&amp;amp;q-&amp;gt;head_lock);
        return -1; // empty
    }
    *value = new_head-&amp;gt;value;
    q-&amp;gt;head = new_head;
    Pthread_mutex_unlock(&amp;amp;q-&amp;gt;head_lock);
    free(tmp); // 이전 더미 또는 소비된 헤드 정리
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 큐는 실무에서도 자주 쓰이지만, 큐가 비었을 때 기다리거나 큐가 꽉 찼을 때 막는 동작까지 포함하려면 락만으로는 부족합니다. 그 연결고리가 다음 장의 조건 변수입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 병행 해시 테이블 (Concurrent Hash Table)&lt;/h2&gt;
&lt;p&gt;해시 테이블은 구조적으로 락 분할이 쉬워 병행성을 끌어올리기 좋은 자료구조입니다.&lt;/p&gt;
&lt;h3&gt;4.1 버킷별 락으로 분할&lt;/h3&gt;
&lt;p&gt;전체 테이블에 락 하나를 거는 대신, &lt;strong&gt;버킷(각 연결 리스트)마다 락을 둡니다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int Hash_Insert(hash_t *H, int key) {
    return List_Insert(&amp;amp;H-&amp;gt;lists[key % BUCKETS], key);
}

int Hash_Lookup(hash_t *H, int key) {
    return List_Lookup(&amp;amp;H-&amp;gt;lists[key % BUCKETS], key);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서로 다른 버킷으로 해시되는 작업들은 락을 공유하지 않기 때문에, 동시 업데이트에서도 좋은 확장성을 보입니다.&lt;/p&gt;
&lt;p&gt;단, 여기서의 해시 테이블은 &lt;strong&gt;리사이즈를 하지 않는 단순 형태&lt;/strong&gt;입니다. 리사이즈까지 지원하려면 재해싱과 버킷 교체, 그리고 그 과정에서의 동기화 설계가 추가로 필요합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이 장의 메시지는 단순히 락을 더 많이 쪼개자는 이야기가 아닙니다. 특히 다음 교훈이 실전에서 중요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;락과 제어 흐름을 조심하라&lt;/strong&gt;&lt;br /&gt;
return, error path, 예외 경로에서 unlock 누락이 매우 흔한 버그가 됩니다.&lt;br /&gt;
가능한 한 락 획득과 해제 지점을 줄이고, 단일 반환 경로 같은 구조화를 고려합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;동시성이 많다고 항상 더 빠르지 않다&lt;/strong&gt;&lt;br /&gt;
hand-over-hand locking처럼 동시성은 늘었지만 락 오버헤드가 더 크면 성능이 나빠질 수 있습니다.&lt;br /&gt;
결국 두 설계를 구현해 측정하는 것이 확실합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;조기 최적화를 피하라&lt;/strong&gt;&lt;br /&gt;
처음에는 큰 락 하나로 올바르게 만들고, 성능 문제가 실제로 드러났을 때만 세분화합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자료구조별 패턴은 다음처럼 정리할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;카운터&lt;/strong&gt;: 로컬 카운터 + 글로벌 카운터, 임계값 S로 정확성과 성능을 조절&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리스트&lt;/strong&gt;: 단일 락이 기본, 임계 영역 최소화와 제어 흐름 단순화가 실전에서 중요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;큐&lt;/strong&gt;: head/tail 락 분리 + 더미 노드로 경계 조건 단순화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;해시 테이블&lt;/strong&gt;: 버킷별 락 분할로 높은 확장성 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장에서는 큐나 버퍼가 비었을 때 스레드를 재우고 깨우는 &lt;strong&gt;조건 변수(Condition Variable)&lt;/strong&gt; 를 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Thread-safe&lt;/code&gt;: 멀티 스레드 환경에서 안전하게 동작하는 성질.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Approximate Counter, Sloppy Counter&lt;/code&gt;: 로컬 카운터를 두어 락 경쟁을 줄이고, 주기적으로 글로벌 카운터에 반영하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Threshold S&lt;/code&gt;: 로컬에서 글로벌로 옮기는 기준. 정확성과 성능의 트레이드오프를 만든다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Hand-over-hand locking, lock coupling&lt;/code&gt;: 리스트 순회 시 다음 노드 락을 잡고 현재 노드 락을 푸는 방식.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Coarse-grained Lock&lt;/code&gt;: 자료구조 전체를 큰 락 하나로 보호하는 방식. 구현이 쉽지만 병렬성이 낮다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fine-grained Lock&lt;/code&gt;: 자료구조를 잘게 쪼개 여러 락으로 보호하는 방식. 병렬성은 높지만 오버헤드와 복잡성이 증가할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dummy Node&lt;/code&gt;: 큐에서 경계 조건을 단순화하기 위해 사용하는 초기 노드.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-locks-usage.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 29: Lock-based Concurrent Data Structures&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 28. 락(Locks): 병행성 제어의 기초</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-28-threads-locks/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-28-threads-locks/</guid><description>락의 기본 개념과 구현 방법을 다룹니다.</description><pubDate>Tue, 03 Feb 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장에서 병행 프로그램이 가진 근본적인 문제, 즉 원자적으로 실행되어야 할 명령어들이 중간에 중단되면서 발생하는 경쟁 상태(Race Condition)를 보았습니다. 이번 장에서는 이 문제를 해결하기 위한 가장 직접적인 도구인 &lt;strong&gt;락(Lock)&lt;/strong&gt; 을 다룹니다.&lt;/p&gt;
&lt;p&gt;프로그래머는 임계 영역(Critical Section) 주위에 락을 배치하여, 해당 구간이 마치 하나의 원자적 동작처럼 실행되도록 보장합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 락이란 무엇인가 (The Basic Idea)&lt;/h2&gt;
&lt;p&gt;락은 임계 영역에 오직 하나의 스레드만 진입하도록 강제하는 도구입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lock_t mutex;

lock(&amp;amp;mutex);
balance = balance + 1; // 임계 영역
unlock(&amp;amp;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;락 변수(&lt;code&gt;mutex&lt;/code&gt;)는 현재 락이 &lt;strong&gt;사용 가능(available, unlocked)&lt;/strong&gt; 한지, 아니면 누군가 &lt;strong&gt;획득(acquired, held)&lt;/strong&gt; 했는지를 나타내는 상태를 가집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lock()&lt;/code&gt;: 락 획득을 시도합니다. 누군가 락을 가지고 있다면 락이 해제될 때까지 반환하지 않고 대기합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unlock()&lt;/code&gt;: 락을 반환합니다. 대기 중인 스레드가 있다면 그 중 하나가 락을 얻을 수 있도록 진행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 단순한 규칙만으로도 프로그래머는 임계 영역의 동시 진입을 막고, 결과가 타이밍에 따라 바뀌는 문제를 통제할 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 락의 평가 기준 (Evaluating Locks)&lt;/h2&gt;
&lt;p&gt;우리가 만든 락이 좋은지 나쁜지는 어떻게 판단할까요. 보통 다음 세 가지 기준을 봅니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;상호 배제 (Mutual Exclusion)&lt;/strong&gt;&lt;br /&gt;
임계 영역에 다수의 스레드가 동시에 들어가는 것을 확실히 막아주는가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;공정성 (Fairness)&lt;/strong&gt;&lt;br /&gt;
락을 기다리는 스레드들이 굶주리지(Starvation) 않고 언젠가는 락을 얻을 수 있는가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;성능 (Performance)&lt;/strong&gt;&lt;br /&gt;
락 사용으로 발생하는 오버헤드는 얼마인가&lt;br /&gt;
경쟁이 없을 때와 경쟁이 심할 때를 나누어 고려해야 합니다&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 장의 흐름은 결국 다음 질문으로 이어집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상호 배제는 어떻게 보장할까&lt;/li&gt;
&lt;li&gt;공정성은 어떻게 확보할까&lt;/li&gt;
&lt;li&gt;경쟁이 심할 때 CPU 낭비를 어떻게 줄일까&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 초기 시도들: 하드웨어의 도움 없이?&lt;/h2&gt;
&lt;h3&gt;3.1 인터럽트 제어 (Controlling Interrupts)&lt;/h3&gt;
&lt;p&gt;초기 단일 프로세서 시스템에서는 임계 영역 진입 전에 &lt;strong&gt;인터럽트를 끄는&lt;/strong&gt; 방식이 사용되기도 했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void lock() {
    DisableInterrupts();
}

void unlock() {
    EnableInterrupts();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 단순하고 확실하게 상호 배제를 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;멀티프로세서에서는 동작하지 않습니다. 한 CPU에서 인터럽트를 꺼도 다른 CPU는 계속 실행됩니다.&lt;/li&gt;
&lt;li&gt;사용자 프로그램에 인터럽트 제어 권한을 주는 것은 매우 위험합니다. 무한 루프에 빠지면 시스템이 복구 불가능해질 수 있습니다.&lt;/li&gt;
&lt;li&gt;중요한 인터럽트(예: 디스크 I/O 완료)를 지연시키거나 놓치면 시스템 전체 지연이 커집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정리하면, 인터럽트 제어는 특정 상황에서만 제한적으로 의미가 있고 일반적인 사용자 레벨 락의 해법으로는 부적절합니다.&lt;/p&gt;
&lt;h3&gt;3.2 소프트웨어만 사용하기 (A Failed Attempt)&lt;/h3&gt;
&lt;p&gt;단순한 플래그 변수(&lt;code&gt;flag&lt;/code&gt;)로 락을 구현하려는 시도는 실패합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (flag == 1) {
    ; // spin
}
flag = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문제는 &lt;code&gt;flag&lt;/code&gt;를 확인하고 값을 설정하는 과정이 원자적이지 않다는 점입니다. 두 스레드가 동시에 &lt;code&gt;flag&lt;/code&gt;를 0으로 읽고 동시에 &lt;code&gt;flag = 1&lt;/code&gt;을 수행하면, 결국 둘 다 임계 영역에 진입할 수 있습니다. 즉, 상호 배제를 보장하지 못합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 하드웨어의 도움: 원자적 명령어&lt;/h2&gt;
&lt;p&gt;제대로 된 락을 만들기 위해 하드웨어는 &lt;strong&gt;원자적 명령어(Atomic Instruction)&lt;/strong&gt; 를 지원하기 시작했습니다. 핵심은 간단합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;락 획득 시점의 경쟁은 결국 메모리의 특정 값 하나를 두고 발생합니다.&lt;/li&gt;
&lt;li&gt;이 값을 읽고 갱신하는 과정을 원자적으로 묶어야 경쟁을 올바르게 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1 Test-And-Set (Atomic Exchange)&lt;/h3&gt;
&lt;p&gt;Test-And-Set은 메모리의 이전 값을 반환하면서 동시에 새로운 값을 씁니다. 이 과정 전체가 원자적으로 수행됩니다.&lt;/p&gt;
&lt;p&gt;이를 이용해 가장 단순한 &lt;strong&gt;스핀 락(Spin Lock)&lt;/strong&gt; 을 만들 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void lock(lock_t *lock) {
    while (TestAndSet(&amp;amp;lock-&amp;gt;flag, 1) == 1) {
        ; // spin-wait
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 락은 상호 배제를 보장하지만, 다음 한계가 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;공정성 부족&lt;/strong&gt;: 운이 나쁜 스레드는 계속 밀려 기아 상태가 생길 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 낭비&lt;/strong&gt;: 락을 기다리는 동안 계속 루프를 돌며 CPU 시간을 소모합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단일 CPU에서 특히 비효율적&lt;/strong&gt;: 스핀이 타임 슬라이스를 태우면, 정작 락 보유자가 실행될 기회가 줄어 더 느려질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 Compare-And-Swap (CAS)&lt;/h3&gt;
&lt;p&gt;Compare-And-Swap은 기대값(&lt;code&gt;expected&lt;/code&gt;)과 메모리 값이 일치할 때만 새로운 값으로 갱신합니다. 비교와 갱신이 원자적으로 수행됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test-And-Set과 마찬가지로 락 구현에 사용할 수 있고&lt;/li&gt;
&lt;li&gt;더 일반적으로는 락 프리(lock-free) 자료구조 등에도 활용됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 Load-Linked / Store-Conditional (LL/SC)&lt;/h3&gt;
&lt;p&gt;MIPS, ARM 등에서 사용하는 방식입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Load-Linked&lt;/code&gt;로 값을 읽고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Store-Conditional&lt;/code&gt;로 값을 쓰되&lt;/li&gt;
&lt;li&gt;그 사이 해당 주소에 다른 쓰기(write)가 없었을 때만 저장이 성공합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 목적은 동일합니다. 경쟁 구간을 원자적으로 다루기 위한 하드웨어 지원입니다.&lt;/p&gt;
&lt;h3&gt;4.4 Fetch-And-Add (Ticket Lock)&lt;/h3&gt;
&lt;p&gt;Fetch-And-Add는 값을 원자적으로 1 증가시키고 이전 값을 반환합니다. 이를 이용하면 &lt;strong&gt;티켓 락(Ticket Lock)&lt;/strong&gt; 을 만들 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int myturn = FetchAndAdd(&amp;amp;lock-&amp;gt;ticket);
while (lock-&amp;gt;turn != myturn) {
    ; // spin
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;티켓 락은 먼저 온 스레드가 먼저 락을 얻는 선입선출(FIFO)을 보장하여 &lt;strong&gt;공정성&lt;/strong&gt; 문제를 크게 완화합니다.&lt;/p&gt;
&lt;p&gt;다만 여전히 스핀 방식이므로 경쟁이 심하면 CPU 낭비와 메모리 트래픽 문제가 남습니다. 특히 많은 스레드가 &lt;code&gt;turn&lt;/code&gt; 값을 반복해서 읽으면서 캐시 라인이 흔들릴 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 스핀의 문제와 해결: 큐와 대기&lt;/h2&gt;
&lt;p&gt;스핀 락은 락을 기다리는 동안 CPU를 계속 소모합니다. 임계 영역이 짧고 경쟁이 약하면 괜찮지만, 경쟁이 커지거나 임계 영역이 길어지면 낭비가 급격히 커집니다. 이를 줄이기 위해 OS의 지원을 받는 방향으로 발전합니다.&lt;/p&gt;
&lt;h3&gt;5.1 양보 (Yield)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;yield()&lt;/code&gt; 같은 시스템 콜로 CPU를 다른 스레드에게 양보할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스핀 대신 양보하면 CPU 낭비는 줄어듭니다.&lt;/li&gt;
&lt;li&gt;하지만 스레드 수가 많으면 문맥 교환(Context Switch) 비용이 커지고, 실행 큐가 혼잡해집니다.&lt;/li&gt;
&lt;li&gt;또한 특정 스레드가 계속 밀리는 기아 상태를 완전히 없애기 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 큐 사용: 잠들기 (Sleeping)&lt;/h3&gt;
&lt;p&gt;더 근본적인 접근은 대기 스레드를 &lt;strong&gt;재우는 것&lt;/strong&gt;입니다. OSTEP에서는 Solaris의 &lt;code&gt;park()&lt;/code&gt;와 &lt;code&gt;unpark()&lt;/code&gt; 같은 인터페이스를 예로 들며, 대기 스레드를 큐로 관리하는 방식으로 확장합니다.&lt;/p&gt;
&lt;p&gt;개념적으로는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;락이 비어 있으면 즉시 획득&lt;/li&gt;
&lt;li&gt;이미 누군가 보유 중이면 대기 큐에 등록하고 잠듦&lt;/li&gt;
&lt;li&gt;락이 풀릴 때 큐에서 하나를 깨움&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (m-&amp;gt;flag == 0) {
    m-&amp;gt;flag = 1; // 락 획득
} else {
    queue_add(m-&amp;gt;q, gettid());
    park(); // 잠들기
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 스핀을 최소화하고 큐를 통해 공정성도 개선할 수 있습니다. 다만 여기에는 중요한 레이스가 숨어 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드가 &lt;code&gt;park()&lt;/code&gt; 직전에 락이 해제되고 &lt;code&gt;unpark()&lt;/code&gt;가 먼저 발생하면&lt;/li&gt;
&lt;li&gt;이후 해당 스레드는 깨울 이벤트를 놓친 채 영원히 잠들 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 막기 위해 &lt;code&gt;setpark()&lt;/code&gt; 같은 추가 프로토콜이 필요합니다. 즉, sleep 기반 락은 성능을 얻는 대신 잠듦과 깨움의 순서를 안전하게 설계해야 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Linux의 Futex&lt;/h2&gt;
&lt;p&gt;Linux는 &lt;code&gt;futex&lt;/code&gt;(fast userspace mutex)라는 메커니즘을 제공합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;경쟁이 없을 때&lt;/strong&gt;: 커널 진입 없이 사용자 공간의 원자적 연산으로 빠르게 락을 획득하고 해제합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경쟁이 있을 때&lt;/strong&gt;: 커널에 들어가 &lt;code&gt;futex&lt;/code&gt;를 통해 잠들고, 해제 시 깨우는 경로를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 대부분의 경우는 빠른 경로를 유지하고, 필요한 순간에만 커널의 도움을 받아 대기와 깨움을 처리하는 방식입니다. 현대적인 락 구현이 목표로 하는 균형점이기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약 (Summary)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;락(Lock)&lt;/strong&gt; 은 임계 영역의 상호 배제를 보장하는 도구입니다.&lt;/li&gt;
&lt;li&gt;하드웨어는 락 구현을 위해 &lt;code&gt;Test-And-Set&lt;/code&gt;, &lt;code&gt;Compare-And-Swap&lt;/code&gt;, &lt;code&gt;Fetch-And-Add&lt;/code&gt; 같은 &lt;strong&gt;원자적 명령어&lt;/strong&gt;를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스핀 락&lt;/strong&gt;은 구현이 단순하지만 공정성과 CPU 낭비 문제가 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;티켓 락&lt;/strong&gt;은 FIFO로 공정성을 개선하지만, 여전히 스핀 방식의 비용이 남습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;큐 기반 락&lt;/strong&gt;은 OS 지원을 받아 스핀을 줄이고 공정성과 성능을 함께 노립니다.&lt;/li&gt;
&lt;li&gt;Linux의 &lt;strong&gt;futex&lt;/strong&gt;는 사용자 공간 fast path와 커널 기반 대기 경로를 결합한 대표적인 예입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Critical Section (임계 영역)&lt;/code&gt;: 공유 자원에 접근하는 코드 구간으로, 한 번에 하나의 스레드만 실행해야 함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mutual Exclusion (상호 배제)&lt;/code&gt;: 락의 가장 기본 기능으로, 동시 진입을 막는 것.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spin Lock (스핀 락)&lt;/code&gt;: 락을 얻을 때까지 루프를 돌며 기다리는 방식.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ticket Lock (티켓 락)&lt;/code&gt;: &lt;code&gt;Fetch-And-Add&lt;/code&gt;로 번호표를 배부하고 순서대로 락을 주는 방식. 공정성을 보장함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Futex&lt;/code&gt;: Linux에서 제공하는 고성능 락킹 프리미티브. 사용자 공간의 빠른 경로와 커널의 대기 경로를 혼합함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Priority Inversion (우선순위 역전)&lt;/code&gt;: 낮은 우선순위 스레드가 락을 보유해 높은 우선순위 스레드가 실행되지 못하는 현상.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-locks.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 28: Locks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 27. Interlude: Thread API</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-27-threads-api/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-27-threads-api/</guid><description>POSIX 스레드(Pthreads) API의 핵심인 생성, 종료, 락, 조건 변수의 사용법과 주의사항을 상세히 정리합니다.</description><pubDate>Tue, 03 Feb 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장에서 스레드의 개념을 익혔다면, 이번 장에서는 운영체제가 제공하는 실제 인터페이스를 배울 차례입니다. 표준인 &lt;strong&gt;POSIX Threads (Pthreads)&lt;/strong&gt; 라이브러리를 중심으로 스레드를 생성하고 제어하는 방법을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 스레드 생성 (Thread Creation)&lt;/h2&gt;
&lt;p&gt;스레드 생성은 &lt;code&gt;pthread_create&lt;/code&gt; 함수를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *),
                   void *arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 함수는 4개의 인자를 받습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;thread&lt;/code&gt;: 생성된 스레드를 식별하는 &lt;code&gt;pthread_t&lt;/code&gt;의 포인터입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attr&lt;/code&gt;: 스레드 속성(우선순위, 스택 크기 등)을 지정합니다. 보통은 &lt;code&gt;NULL&lt;/code&gt;을 써서 기본값을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start_routine&lt;/code&gt;: 스레드가 실행을 시작할 함수입니다. 이 함수는 &lt;code&gt;void *&lt;/code&gt; 인자를 받고 &lt;code&gt;void *&lt;/code&gt;를 반환해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arg&lt;/code&gt;: 실행할 함수에 전달할 인자입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;왜 &lt;code&gt;void *&lt;/code&gt;를 쓸까요?&lt;/strong&gt;&lt;br /&gt;
C에서 &lt;code&gt;void *&lt;/code&gt;는 임의의 포인터 타입을 담을 수 있습니다. 따라서 다양한 타입의 인자를 전달하고, 결과도 포인터 형태로 유연하게 전달할 수 있도록 설계되어 있습니다. 다만 실제로는 호출자와 피호출자가 동일한 타입 규약을 공유해야 안전합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 스레드 종료 대기 (Thread Completion)&lt;/h2&gt;
&lt;p&gt;스레드가 작업을 마칠 때까지 기다리려면 &lt;code&gt;pthread_join&lt;/code&gt;을 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int pthread_join(pthread_t thread, void **value_ptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;thread&lt;/code&gt;: 기다릴 대상 스레드입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value_ptr&lt;/code&gt;: 스레드의 반환 값을 받아올 포인터의 포인터입니다. 반환 값이 필요 없으면 &lt;code&gt;NULL&lt;/code&gt;을 전달해도 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;[주의] 스택 변수 반환 금지&lt;/strong&gt;&lt;br /&gt;
스레드 함수 내에서 지역 변수(스택에 할당된 변수)의 주소를 반환하면 안 됩니다. 스레드가 종료되면 그 스택 프레임은 더 이상 유효하지 않으므로, 반환된 포인터는 잘못된 메모리를 가리키게 되어 치명적인 버그로 이어집니다. 값을 반환하려면 힙에 할당(&lt;code&gt;malloc&lt;/code&gt;)하거나, 호출자가 소유한 메모리를 인자로 넘겨 그 공간에 결과를 기록하는 방식이 안전합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 락 (Locks)&lt;/h2&gt;
&lt;p&gt;임계 영역을 보호하기 위해 &lt;strong&gt;상호 배제(Mutual Exclusion)&lt;/strong&gt; 기능을 제공하는 락을 사용합니다. Pthreads에서는 이를 &lt;code&gt;Mutex&lt;/code&gt;라고 부릅니다.&lt;/p&gt;
&lt;h3&gt;초기화와 사용&lt;/h3&gt;
&lt;p&gt;락은 사용 전에 반드시 초기화해야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 정적 초기화
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

// 동적 초기화 (실패 시 반환값 체크 필수)
#include &amp;lt;assert.h&amp;gt;

int rc = pthread_mutex_init(&amp;amp;lock, NULL);
assert(rc == 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용법은 직관적입니다. 임계 영역 앞에서 락을 걸고(&lt;code&gt;lock&lt;/code&gt;), 뒤에서 풉니다(&lt;code&gt;unlock&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_lock(&amp;amp;lock);
x = x + 1; // 임계 영역
pthread_mutex_unlock(&amp;amp;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 다른 스레드가 이미 락을 가지고 있다면, &lt;code&gt;pthread_mutex_lock&lt;/code&gt;을 호출한 스레드는 락을 얻을 때까지 대기(Block)합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 조건 변수 (Condition Variables)&lt;/h2&gt;
&lt;p&gt;스레드 간에 신호(Signal)를 주고받아야 할 때, 즉 특정 조건이 만족될 때까지 기다려야 하는 경우에 조건 변수를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;올바른 사용 패턴&lt;/h3&gt;
&lt;p&gt;조건 변수는 반드시 &lt;strong&gt;락(Mutex)과 함께&lt;/strong&gt; 사용해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대기하는 스레드 (Waiting Thread)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_lock(&amp;amp;lock);
while (ready == 0) { // 반드시 while 루프 사용
    pthread_cond_wait(&amp;amp;cond, &amp;amp;lock);
}
pthread_mutex_unlock(&amp;amp;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;신호를 보내는 스레드 (Signaling Thread)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_lock(&amp;amp;lock);
ready = 1;
pthread_cond_signal(&amp;amp;cond);
pthread_mutex_unlock(&amp;amp;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심 포인트&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_wait()&lt;/code&gt;는 호출 시점에 보유 중이던 &lt;strong&gt;락을 원자적으로 해제&lt;/strong&gt;하고 잠듭니다. 깨어날 때는 다시 &lt;strong&gt;락을 획득한 상태로&lt;/strong&gt; 반환합니다.&lt;/li&gt;
&lt;li&gt;대기 조건(&lt;code&gt;ready == 0&lt;/code&gt;) 검사는 &lt;code&gt;if&lt;/code&gt;가 아닌 &lt;strong&gt;&lt;code&gt;while&lt;/code&gt;&lt;/strong&gt; 로 해야 합니다. 깨어난 뒤 조건이 여전히 만족되지 않았을 수도 있고, 명시적인 신호 없이 깨어나는 spurious wakeup이 발생할 수도 있기 때문입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 컴파일 방법&lt;/h2&gt;
&lt;p&gt;Pthreads 라이브러리를 사용하는 코드를 컴파일할 때는 &lt;code&gt;-pthread&lt;/code&gt; 플래그를 추가합니다. 이 플래그는 링크 옵션뿐 아니라 스레드 관련 컴파일 옵션도 함께 설정해 주는 방식이라 보통 권장됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -o main main.c -Wall -pthread
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. API 사용 가이드라인&lt;/h2&gt;
&lt;p&gt;책에서 제시하는 멀티 스레드 프로그래밍의 조언입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;단순하게 유지하라 (Keep it simple)&lt;/strong&gt;: 락과 조건 변수의 상호작용이 복잡해질수록 버그가 급격히 늘어납니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;반환 값을 확인하라&lt;/strong&gt;: API 호출 실패는 언제든 일어날 수 있으니, 실패 시 경로를 명확히 처리해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스택 변수 주의&lt;/strong&gt;: 스레드 간에 로컬 변수(스택) 포인터를 오래 보관하거나 반환하지 마세요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;조건 변수 사용&lt;/strong&gt;: 단순 플래그를 바쁜 대기(spin)로 폴링하기보다는, 조건 변수를 사용해 효율적이고 안전하게 대기/깨움을 구현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장은 운영체제가 제공하는 스레드 인터페이스의 실체를 확인했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;생성 및 종료 대기&lt;/strong&gt;: &lt;code&gt;pthread_create&lt;/code&gt;, &lt;code&gt;pthread_join&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;락(Lock)&lt;/strong&gt;: &lt;code&gt;pthread_mutex_lock&lt;/code&gt;, &lt;code&gt;pthread_mutex_unlock&lt;/code&gt;을 통한 상호 배제&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;조건 변수&lt;/strong&gt;: &lt;code&gt;pthread_cond_wait&lt;/code&gt;, &lt;code&gt;pthread_cond_signal&lt;/code&gt;을 통한 순서 제어 및 대기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 API들은 간단해 보이지만, while 루프로 조건을 재확인한다거나, 스택 포인터를 반환하지 않는다 같은 디테일을 놓치면 치명적인 오류로 이어집니다. 다음 장부터는 이 도구들을 이용해 실제로 락과 조건 변수를 어떻게 구현하고 활용하는지 더 깊이 있게 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POSIX Threads (Pthreads)&lt;/code&gt;: UNIX 계열 시스템의 표준 스레드 API 라이브러리.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mutex (Mutual Exclusion)&lt;/code&gt;: 임계 영역에 오직 하나의 스레드만 접근하도록 통제하는 락 객체.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Condition Variable&lt;/code&gt;: 특정 조건이 만족될 때까지 스레드를 대기시키고(Wait), 조건이 만족되면 깨우는(Signal) 동기화 객체.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spurious Wakeup&lt;/code&gt;: 조건 변수 대기 중 명시적인 신호가 없었음에도 스레드가 깨어나는 현상. while 루프로 조건을 재확인해 방어해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Thread-safe&lt;/code&gt;: 멀티 스레드 환경에서 동시에 호출되어도 올바르게 동작하는 코드나 라이브러리의 성질.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-api.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 27: Thread API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 26. 병행성(Concurrency): 개요 - 스레드와 경쟁 상태</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-26-threads-intro/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-26-threads-intro/</guid><description>스레드의 기본 개념과 스케줄링 비결정성, 공유 데이터로 인한 경쟁 상태의 원인을 정리합니다. 임계 영역과 상호 배제, 원자성 필요성까지 연결합니다.</description><pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전까지 다뤘던 프로세스는 보통 하나의 실행 흐름만 가진다고 가정했습니다.이번 장에서는 하나의 주소 공간 안에 여러 실행 흐름이 공존하는 &lt;strong&gt;멀티 스레드 프로그램&lt;/strong&gt;을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 스레드란 무엇인가&lt;/h2&gt;
&lt;p&gt;스레드는 &lt;strong&gt;하나의 프로세스 안에서 실행되는 또 하나의 실행 흐름&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;각 스레드는 서로 독립적인 실행 상태를 가집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Program Counter (PC)&lt;/code&gt;: 다음에 가져올 명령어 위치를 추적합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;레지스터 상태&lt;/code&gt;: 연산을 위한 개인 상태(컨텍스트)입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;스레드별 스택&lt;/code&gt;: 함수 호출과 로컬 변수를 저장하기 위한 공간입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면, 스레드들은 &lt;strong&gt;주소 공간을 공유&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;코드(Code) 영역&lt;/li&gt;
&lt;li&gt;전역 변수 등 데이터 영역&lt;/li&gt;
&lt;li&gt;힙(Heap)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/26_1.png&quot; alt=&quot;Figure 26.1 Single-Threaded And Multi-Threaded Address Spaces&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;스택의 변화 (Figure 26.1)&lt;/strong&gt;
단일 스레드 프로세스는 스택이 하나만 존재하지만, 멀티 스레드 프로세스는 &lt;strong&gt;스레드마다 별도의 스택&lt;/strong&gt;이 주소 공간 내 여러 위치에 배치됩니다.이를 통해 각 스레드는 독립적으로 함수를 호출할 수 있습니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 왜 스레드를 사용하는가&lt;/h2&gt;
&lt;p&gt;스레드를 사용하는 대표적인 이유는 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;병렬성 (Parallelism)&lt;/strong&gt;: 멀티코어 환경에서 작업을 쪼개 동시에 실행하여 속도를 높입니다.(예: 큰 배열 연산 분산 처리)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 대기 시간 활용&lt;/strong&gt;: 한 스레드가 I/O 작업으로 막혀도(Block), 다른 스레드가 CPU를 사용해 유용한 작업을 계속할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 스레드 생성과 스케줄링의 비결정성&lt;/h2&gt;
&lt;p&gt;스레드는 &lt;code&gt;pthread_create()&lt;/code&gt;로 생성하고 &lt;code&gt;pthread_join()&lt;/code&gt;으로 종료를 기다립니다.&lt;/p&gt;
&lt;p&gt;다음 코드는 메인 스레드가 A와 B를 출력하는 두 스레드를 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;

void *mythread(void *arg) {
printf(&quot;%s\n&quot;, (char *)arg);
return NULL;
}

int main(int argc, char *argv[]) {
pthread_t p1, p2;
// 스레드 생성: mythread(&quot;A&quot;), mythread(&quot;B&quot;) 실행
pthread_create(&amp;amp;p1, NULL, mythread, &quot;A&quot;);
pthread_create(&amp;amp;p2, NULL, mythread, &quot;B&quot;);

// 대기
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심은 실행 순서를 알 수 없다는 점입니다.&lt;/strong&gt;
OS 스케줄러의 선택에 따라 A가 먼저 나올 수도, B가 먼저 나올 수도 있습니다.이처럼 같은 입력에도 실행 순서와 결과가 달라질 수 있는 성질을 &lt;code&gt;Indeterminate&lt;/code&gt;(비결정적)이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/26_3.png&quot; alt=&quot;Figure 26.3 Thread Trace Example&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 공유 데이터가 들어오면 문제가 시작된다&lt;/h2&gt;
&lt;p&gt;단순 출력만으로는 큰 문제가 드러나지 않지만, &lt;strong&gt;공유 데이터&lt;/strong&gt;를 건드리기 시작하면 심각한 문제가 발생합니다.
다음은 두 스레드가 하나의 &lt;code&gt;counter&lt;/code&gt; 변수를 공유하며 각각 10,000,000번씩 1을 더하는 예제입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static volatile int counter = 0;

// 각 스레드가 이 루프를 10,000,000번 반복합니다.
for (i = 0; i &amp;lt; 1e7; i++) {
counter = counter + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기대하는 결과는 &lt;code&gt;20,000,000&lt;/code&gt;이지만, 실제 실행해보면 &lt;code&gt;19,345,221&lt;/code&gt; 같은 예상과 다른 값이 나옵니다.심지어 실행할 때마다 값이 바뀌기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 왜 이런 일이 생기는가: 경쟁 상태 (Race Condition)&lt;/h2&gt;
&lt;p&gt;문제의 원인은 &lt;code&gt;counter = counter + 1&lt;/code&gt; 코드가 &lt;strong&gt;원자적(Atomic)이지 않기 때문&lt;/strong&gt;입니다.
이 C언어 코드는 어셈블리어(x86) 관점에서 보면 3단계로 쪼개집니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;mov 0x8049a1c, %eax&lt;/code&gt;: 메모리에서 값을 레지스터로 가져옵니다 (&lt;strong&gt;Load&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add $0x1, %eax&lt;/code&gt;: 레지스터 값을 1 증가시킵니다 (&lt;strong&gt;Add&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mov %eax, 0x8049a1c&lt;/code&gt;: 레지스터 값을 다시 메모리에 저장합니다 (&lt;strong&gt;Store&lt;/strong&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;만약 타이머 인터럽트로 컨텍스트 스위치가 이 명령어들 사이에 끼어들면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/26_7.png&quot; alt=&quot;Figure 26.7 The Problem Up Close&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Thread 1&lt;/strong&gt;이 &lt;code&gt;counter&lt;/code&gt;(50)를 로드하고 1을 더해 &lt;code&gt;51&lt;/code&gt;을 만듭니다 (아직 저장하지 않음).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(인터럽트 발생)&lt;/em&gt; &lt;strong&gt;Thread 2&lt;/strong&gt;로 전환됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread 2&lt;/strong&gt;도 &lt;code&gt;counter&lt;/code&gt;를 로드합니다.메모리에는 아직 &lt;code&gt;50&lt;/code&gt;이 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread 2&lt;/strong&gt;가 1을 더해 &lt;code&gt;51&lt;/code&gt;을 만들고 메모리에 저장합니다.(&lt;code&gt;counter = 51&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(다시 Thread 1 복귀)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread 1&lt;/strong&gt;은 아까 들고 있던 &lt;code&gt;51&lt;/code&gt;을 메모리에 저장합니다.(&lt;code&gt;counter = 51&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결국 두 번 더했는데 결과는 1만 증가했습니다.이처럼 실행 타이밍에 따라 결과가 달라지는 상황을 &lt;code&gt;Race Condition&lt;/code&gt;(경쟁 상태)이라고 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 원자성(Atomicity)의 필요&lt;/h2&gt;
&lt;p&gt;우리가 원하는 것은 임계 영역의 명령어들이 마치 &lt;strong&gt;하나의 슈퍼 명령어&lt;/strong&gt;처럼 실행되는 것입니다.&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;All or Nothing&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;중간에 끊기지 않고 모두 실행되거나, 아예 실행되지 않아야 합니다.이를 &lt;code&gt;Atomicity&lt;/code&gt;(원자성)이라고 합니다. 하드웨어는 몇 가지 기본적인 동기화 명령어(Synchronization Primitives)를 제공하고, 운영체제는 이를 이용해 &lt;strong&gt;락(Lock)&lt;/strong&gt; 같은 도구를 만들어 제공합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 또 다른 문제: 순서 대기 (Waiting)&lt;/h2&gt;
&lt;p&gt;공유 변수 문제 외에도, 한 스레드가 다른 스레드의 작업이 끝날 때까지 기다려야 하는 상황(예: I/O 완료 대기)이 있습니다. 이를 위해 &lt;code&gt;Condition Variable&lt;/code&gt;(조건 변수) 같은 메커니즘이 필요하며, 이후 챕터에서 다룰 예정입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장에서는 병행성(Concurrency)의 기초를 다뤘습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;스레드&lt;/strong&gt;: 주소 공간을 공유하지만 실행 흐름(PC, 레지스터, 스택)은 분리된 실행 단위입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비결정성&lt;/strong&gt;: 스케줄링에 따라 실행 순서가 달라질 수 있어 결과 예측이 어렵습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경쟁 상태&lt;/strong&gt;: 공유 데이터 접근 시 원자성이 보장되지 않으면 데이터가 손상될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;해결 방향&lt;/strong&gt;: 하드웨어와 OS의 지원을 받아 상호 배제(Lock)와 순서 제어(Condition Variable)를 구현해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Thread&lt;/code&gt;: 프로세스 내에서 실행되는 독립적인 실행 흐름.PC, 레지스터, 스택을 독립적으로 가짐.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Critical Section&lt;/code&gt;: 공유 자원에 접근하는 코드의 일부분으로, 동시에 실행되면 안 되는 구역.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Race Condition&lt;/code&gt;: 여러 스레드가 동시에 임계 영역에 진입하여 실행 타이밍에 따라 결과가 달라지는 상황.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Indeterminate&lt;/code&gt;: 프로그램의 실행 결과가 실행할 때마다 달라지는 성질.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mutual Exclusion&lt;/code&gt;: 하나의 스레드만 임계 영역에 진입하도록 보장하는 것.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Atomicity&lt;/code&gt;: 작업이 중단되지 않고 한 번에 실행되는 성질. All or Nothing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Context Switch&lt;/code&gt;: CPU 제어권을 한 스레드(또는 프로세스)에서 다른 스레드로 넘기는 작업.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Program Counter&lt;/code&gt;: 현재 실행 중인 명령어의 주소를 저장하는 레지스터.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/threads-intro.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 26: Concurrency: An Introduction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 23. Complete Virtual Memory Systems (VAX/VMS &amp; Linux)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-23-vm-complete/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-23-vm-complete/</guid><description>가상 메모리 이론이 실제 OS에서 어떻게 구현되는지 살펴봅니다. VAX/VMS의 설계와 Linux의 VM을 비교하며 핵심 아이디어를 정리합니다.</description><pubDate>Thu, 29 Jan 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지금까지 가상 메모리의 메커니즘과 정책을 배웠습니다. 이번 장에서는 이 조각들이 실제 운영체제에서 어떻게 결합되는지 확인합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VAX/VMS&lt;/strong&gt;: 하드웨어 제약을 소프트웨어로 흡수하며 표준 기법을 정립한 시스템&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux&lt;/strong&gt;: 대용량 메모리와 현대 보안 요구에 맞춰 진화한 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. VAX/VMS 가상 메모리 시스템&lt;/h2&gt;
&lt;p&gt;VAX는 32비트 주소 공간을 사용하지만 페이지 크기가 &lt;strong&gt;512바이트&lt;/strong&gt;로 매우 작습니다. 페이지가 작으면 단편화는 줄지만 페이지 테이블이 커지고 디스크 I/O 효율도 나빠지기 쉽습니다. VMS는 이런 제약을 시스템 설계로 흡수했습니다.&lt;/p&gt;
&lt;h3&gt;1.1 하드웨어 관점에서의 Hybrid 구조&lt;/h3&gt;
&lt;p&gt;VAX는 페이징과 세그멘테이션을 결합한 하이브리드 형태입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가상 주소는 VPN과 Offset으로 나뉘고, VPN의 상위 비트가 어떤 세그먼트에 속하는지 구분합니다.&lt;/li&gt;
&lt;li&gt;프로세스 공간은 &lt;strong&gt;P0, P1&lt;/strong&gt;로 나뉘며 각 세그먼트는 별도 페이지 테이블을 가집니다.&lt;/li&gt;
&lt;li&gt;세그먼트별 페이지 테이블은 Base/Bounds 레지스터로 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 주소 공간 구조와 Figure 23.1&lt;/h3&gt;
&lt;p&gt;VMS는 주소 공간을 크게 세 영역으로 나눕니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P0 (User Code/Heap)&lt;/strong&gt;: 사용자 코드와 힙이 위치하며, 힙은 아래 방향으로 성장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P1 (User Stack)&lt;/strong&gt;: 사용자 스택이 위치하며, 스택은 위 방향으로 성장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S (System)&lt;/strong&gt;: 커널 코드와 데이터가 위치하며, 모든 프로세스에서 공유됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;VMS는 &lt;strong&gt;0번 페이지를 접근 불가(Invalid)&lt;/strong&gt; 로 두어 Null 포인터 역참조를 빠르게 드러나게 합니다. 또한 실제 주소 공간에는 사용자 영역 외에도 Trap Tables, Kernel Heap 같은 요소가 포함됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/23_1.png&quot; alt=&quot;Figure 23.1 VAX VMS address space&quot; /&gt;
&lt;em&gt;Figure 23.1: VAX/VMS 주소 공간 예시. 0번 페이지 Invalid, User 영역, Trap Tables, Kernel 영역이 함께 구성된다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;1.3 페이지 테이블 자체의 가상화&lt;/h3&gt;
&lt;p&gt;VMS의 중요한 설계는 &lt;strong&gt;사용자 페이지 테이블 자체를 커널 가상 메모리(S 영역)&lt;/strong&gt; 에 둔다는 점입니다 .&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로세스별로 P0, P1 페이지 테이블이 존재합니다.&lt;/li&gt;
&lt;li&gt;페이지 테이블은 커널 가상 메모리에서 할당되므로, 심한 메모리 압박에서는 &lt;strong&gt;페이지 테이블 페이지도 스왑 아웃&lt;/strong&gt; 대상이 됩니다.&lt;/li&gt;
&lt;li&gt;결과적으로 페이지 테이블이 물리 메모리를 잠식하는 문제를 VM 시스템 안으로 끌고 들어와 완화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 선택은 주소 변환 자체를 더 복잡하게 만들지만, 하드웨어 관리 TLB가 대부분의 비용을 숨겨줍니다.&lt;/p&gt;
&lt;h3&gt;1.4 교체 정책과 I/O 최적화&lt;/h3&gt;
&lt;p&gt;VAX PTE에는 Valid, Protection, Dirty, OS Reserved, PFN 등이 들어갑니다. 중요한 제약은 &lt;strong&gt;Reference Bit이 없다&lt;/strong&gt;는 점입니다. VMS는 이를 소프트웨어로 보완합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reference Bit 에뮬레이션&lt;/strong&gt;: 페이지를 접근 불가(Invalid)로 설정해 접근 시 트랩을 유발하고, OS가 OS Reserved 필드에 참조 여부를 기록한 뒤 원래 보호 비트를 복구합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스별 Resident Set 관리&lt;/strong&gt;: 메모리 독점을 막기 위해 프로세스별로 상주 페이지 수를 제한합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Segmented FIFO 계열&lt;/strong&gt;: Evict된 페이지를 곧바로 재사용하지 않고 Global List(Clean/Dirty)에 두어 재참조되면 회수할 기회를 주고, 처리 비용을 줄입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clustering&lt;/strong&gt;: 작은 페이지 단위의 I/O 비효율을 줄이기 위해 여러 페이지를 묶어 디스크에 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.5 지연 최적화 (Lazy Optimization)&lt;/h3&gt;
&lt;p&gt;VMS는 현대 OS에서 표준이 된 지연 최적화를 적극 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Demand Zeroing&lt;/strong&gt;: 힙 확장 시 즉시 프레임을 할당해 0으로 채우지 않고, 실제 접근 시점에 페이지 폴트로 할당과 초기화를 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copy-On-Write (COW)&lt;/strong&gt;: &lt;code&gt;fork&lt;/code&gt; 시 페이지를 복사하지 않고 읽기 전용으로 공유하다가, 쓰기 시점에 폴트로 새 페이지를 할당해 복사합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Linux 가상 메모리 시스템&lt;/h2&gt;
&lt;p&gt;Linux는 VMS에서 발전한 아이디어를 계승하면서도 대용량 메모리와 현대 워크로드에 맞춘 기능을 확장했습니다.&lt;/p&gt;
&lt;h3&gt;2.1 주소 공간 분할과 Figure 23.2&lt;/h3&gt;
&lt;p&gt;32비트 Linux는 사용자 영역과 커널 영역을 분할합니다. 전형적인 Split은 &lt;code&gt;0xC0000000&lt;/code&gt; 기준입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x00000000&lt;/code&gt; ~ &lt;code&gt;0xBFFFFFFF&lt;/code&gt;: 사용자 가상 주소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xC0000000&lt;/code&gt; ~ &lt;code&gt;0xFFFFFFFF&lt;/code&gt;: 커널 가상 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;커널 가상 주소는 두 종류로 구분됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kernel Logical Address&lt;/strong&gt;: &lt;code&gt;kmalloc&lt;/code&gt;으로 확보하며 물리 메모리 일부와 &lt;strong&gt;Direct Mapping&lt;/strong&gt; 관계를 가집니다. 스왑이 불가능하며 DMA에 필수적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel Virtual Address&lt;/strong&gt;: &lt;code&gt;vmalloc&lt;/code&gt;으로 확보하며 가상으로 연속이지만 물리적으로는 불연속일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/23_2.png&quot; alt=&quot;Figure 23.2 Linux address space&quot; /&gt;
&lt;em&gt;Figure 23.2: Linux 주소 공간 예시. User 영역과 Kernel 영역이 분할되고, Kernel 내부에 Logical과 Virtual이 공존한다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;2.2 Page Cache와 Active/Inactive 리스트&lt;/h3&gt;
&lt;p&gt;Linux는 파일 I/O 성능을 위해 &lt;strong&gt;Page Cache&lt;/strong&gt;를 적극 활용하고, 교체 정책은 LRU를 근사하는 방향으로 설계됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inactive List&lt;/strong&gt;: 재참조가 확인되지 않은 페이지가 머무는 리스트로, 교체 후보가 되기 쉽습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active List&lt;/strong&gt;: 재참조가 확인된 페이지가 승격되어 머무는 리스트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구조는 큰 파일을 한 번만 읽는 작업(Sequential Scan)이 메모리를 오염시키는 문제를 완화하는 데 도움이 됩니다.&lt;/p&gt;
&lt;h3&gt;2.3 Huge Pages&lt;/h3&gt;
&lt;p&gt;메모리가 커질수록 TLB 엔트리 수는 상대적으로 부족해집니다. Linux는 &lt;strong&gt;Huge Page (2MB, 1GB)&lt;/strong&gt; 를 지원해 TLB 미스를 줄입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;큰 페이지는 TLB 커버리지를 키워 성능을 높일 수 있습니다.&lt;/li&gt;
&lt;li&gt;대신 내부 단편화와 관리 비용이 커질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 메모리 매핑과 Demand Paging&lt;/h3&gt;
&lt;p&gt;Linux는 실행 파일과 공유 라이브러리를 포함해 많은 데이터를 &lt;strong&gt;Memory Mapped File&lt;/strong&gt; 형태로 주소 공간에 배치합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;매핑된 구간은 실제 접근 시점에 페이지 폴트로 필요한 부분만 메모리에 올립니다.&lt;/li&gt;
&lt;li&gt;결과적으로 주소 공간 구성과 지연 적재가 자연스럽게 결합됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 보안 기능: NX, ASLR, KPTI&lt;/h3&gt;
&lt;p&gt;현대 VM 시스템에서 보안은 핵심 요구사항입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NX Bit&lt;/strong&gt;: 스택/힙 영역에서 코드 실행을 막아 버퍼 오버플로우 공격을 완화합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ASLR&lt;/strong&gt;: 코드, 힙, 스택 배치를 무작위화해 공격자가 주소를 예측하기 어렵게 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KPTI&lt;/strong&gt;: 멜트다운(Meltdown) 같은 CPU 취약점에 대응해 커널 페이지 테이블을 사용자 공간과 격리시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VAX/VMS&lt;/strong&gt;: 작은 페이지 크기와 Reference Bit 부재 같은 하드웨어 제약을 설계와 소프트웨어 기법으로 흡수했습니다. 0번 페이지 Invalid, 커널 매핑, 페이지 테이블 가상화, 지연 최적화, Clustering 같은 기법이 핵심입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux&lt;/strong&gt;: 주소 공간 분할과 커널 매핑 위에서 Page Cache와 LRU 근사 교체 정책을 운용하고, Huge Pages로 대용량 메모리에 대응합니다. 또한 NX, ASLR, KPTI 같은 보안 기법이 VM과 강하게 결합되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Demand Zeroing&lt;/code&gt;: 요청 시 즉시 초기화하지 않고, 접근 시점에 폴트로 할당과 초기화를 수행하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Copy-On-Write (COW)&lt;/code&gt;: 공유 후 쓰기 시점에 복사하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Resident Set&lt;/code&gt;: 프로세스가 물리 메모리에 상주시키는 페이지 집합.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Clustering&lt;/code&gt;: 디스크 I/O 효율을 위해 여러 페이지를 묶어 처리하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page Cache&lt;/code&gt;: 파일 데이터를 메모리에 캐시해 재사용하는 컴포넌트.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kernel Logical Address&lt;/code&gt;: Direct Mapping 기반의 연속적인 커널 주소 영역.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kernel Virtual Address&lt;/code&gt;: &lt;code&gt;vmalloc&lt;/code&gt; 기반의 가상 연속 커널 주소 영역.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NX Bit&lt;/code&gt;: 실행 금지 비트.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ASLR&lt;/code&gt;: 주소 공간 배치 무작위화.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KPTI&lt;/code&gt;: 커널 페이지 테이블 격리.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-complete.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 23: Complete Virtual Memory Systems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 22. Beyond Physical Memory: Policies</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-22-vm-beyondphys-policy/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-22-vm-beyondphys-policy/</guid><description>AMAT 관점에서 OPT, FIFO, Random, LRU, Clock 정책을 비교하고 Dirty Page 처리와 Thrashing 대응까지 정리합니다.</description><pubDate>Thu, 29 Jan 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 21장에서는 스왑 공간과 페이지 폴트를 통해 물리 메모리의 한계를 넘는 &lt;strong&gt;메커니즘&lt;/strong&gt;을 살펴보았습니다. 이제 남는 질문은 &lt;strong&gt;정책(Policy)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;메모리가 부족해져 교체(Eviction)가 필요할 때, 과연 &lt;strong&gt;어떤 페이지를 내보낼지&lt;/strong&gt;가 시스템 성능의 핵심입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 캐시 관리의 목표: AMAT 최소화&lt;/h2&gt;
&lt;p&gt;메인 메모리는 시스템의 모든 페이지 중 일부를 담는 &lt;strong&gt;캐시(Cache)&lt;/strong&gt; 로 볼 수 있습니다. 우리의 목표는 캐시 미스를 최소화하여 &lt;strong&gt;평균 메모리 접근 시간(AMAT)&lt;/strong&gt; 을 낮추는 것입니다.&lt;/p&gt;
&lt;p&gt;$$AMAT = T_M + (P_{miss} \times T_D)$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T_M$: 메모리 접근 비용 (약 100ns)&lt;/li&gt;
&lt;li&gt;$T_D$: 디스크 접근 비용 (약 10ms)&lt;/li&gt;
&lt;li&gt;$P_{miss}$: 캐시 미스 확률&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$T_D$가 매우 크기 때문에 아주 작은 미스율의 증가도 전체 성능에 치명적입니다. 예를 들어 $T_M$이 100ns이고 $T_D$가 10ms라면, 미스율($P_{miss}$)이 0.1%만 되어도 AMAT는 약 10µs 수준으로 치솟습니다. 이는 메모리만 쓸 때보다 약 100배 느려지는 결과입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 기준점: OPT 또는 MIN&lt;/h2&gt;
&lt;p&gt;Belady가 제시한 최적 정책(MIN 또는 OPT)은 &lt;strong&gt;&quot;가장 먼 미래에 다시 접근될 페이지를 교체하는 것&quot;&lt;/strong&gt; 입니다 .&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 미스 횟수를 최소화하는 이상적인 기준입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한계&lt;/strong&gt;: OS는 미래를 알 수 없으므로 실제 구현은 불가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;: 다른 현실적인 정책들이 얼마나 좋은지 평가하는 &lt;strong&gt;비교 기준(Baseline)&lt;/strong&gt; 으로 사용됩니다 .&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 단순한 정책: FIFO와 Random&lt;/h2&gt;
&lt;h3&gt;3.1 FIFO (First-In, First-Out)&lt;/h3&gt;
&lt;p&gt;시스템에 가장 먼저 들어온 페이지를 먼저 내보냅니다 .&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 구현이 매우 단순합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 페이지의 중요도를 전혀 고려하지 않습니다. 자주 쓰이는 페이지라도 먼저 들어왔다는 이유로 쫓겨날 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: &lt;strong&gt;벨라디의 역설(Belady&apos;s Anomaly)&lt;/strong&gt; 이 발생할 수 있습니다. 즉, 캐시 크기를 늘렸음에도 불구하고 오히려 히트율이 떨어지는 기이한 현상이 나타날 수 있습니다 .&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/22_2.png&quot; alt=&quot;Figure 22.2 FIFO example&quot; /&gt;
&lt;em&gt;Figure 22.2: FIFO 정책의 동작 예시. 자주 참조되는 페이지(0번)도 먼저 들어왔다는 이유로 교체될 수 있습니다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;3.2 Random&lt;/h3&gt;
&lt;p&gt;메모리 압박 시 무작위로 페이지를 선택해 교체합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 구현이 쉽고, 특정 &quot;꼬인 케이스(Corner-case)&quot;를 피하는 성질이 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 성능이 운에 좌우되며, 지능적인 선택을 하지는 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Random은 일부 패턴(Looping)에서 의외로 잘 버티기도 하고, 어떤 경우에는 최적에 크게 못 미치기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 과거를 이용하기: LRU와 지역성&lt;/h2&gt;
&lt;p&gt;미래를 알 수 없다면 &lt;strong&gt;과거(History)&lt;/strong&gt; 를 이용합니다. 대부분의 프로그램은 &lt;strong&gt;지역성(Locality)&lt;/strong&gt; 을 보입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간 지역성 (Temporal Locality)&lt;/strong&gt;: 최근에 접근한 데이터는 곧 다시 접근될 가능성이 큽니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공간 지역성 (Spatial Locality)&lt;/strong&gt;: 어떤 데이터 주소 근처의 데이터도 함께 접근될 가능성이 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LRU (Least Recently Used)&lt;/strong&gt; 는 가장 오랫동안 참조되지 않은 페이지를 내보내는 정책으로, 시간 지역성을 잘 활용하여 일반적으로 우수한 성능을 냅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Workload로 보는 정책의 성격&lt;/h2&gt;
&lt;p&gt;작은 트레이스만으로는 정책의 특성이 잘 드러나지 않으므로, 대표적인 워크로드(Workload)를 통해 정책들을 비교해 봅시다.&lt;/p&gt;
&lt;h3&gt;5.1 80-20 Workload&lt;/h3&gt;
&lt;p&gt;전체 참조의 80%가 특정 20%의 &lt;strong&gt;Hot Page&lt;/strong&gt;에 집중되는 형태입니다. LRU는 자주 쓰이는 Hot Page를 메모리에 오래 유지하는 경향이 있어, FIFO나 Random보다 훨씬 유리합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/22_7.png&quot; alt=&quot;Figure 22.7 80-20 Workload&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.2 Looping Sequential Workload&lt;/h3&gt;
&lt;p&gt;0부터 49까지의 페이지를 순차적으로 반복 접근하는 형태입니다.&lt;/p&gt;
&lt;p&gt;이 경우 LRU와 FIFO는 최악의 성능을 보입니다. 캐시 크기가 49라도(페이지가 50개인데), 가장 오래된 페이지(방금 접근했던 페이지)를 내보내게 되어 &lt;strong&gt;히트율이 0%&lt;/strong&gt; 가 될 수 있습니다. 반면 Random은 특정 패턴에 갇히지 않으므로 0보다는 나은 성능을 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/22_8.png&quot; alt=&quot;Figure 22.8 Looping Sequential Workload&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 완전한 LRU는 비싸다&lt;/h2&gt;
&lt;p&gt;LRU가 좋긴 하지만, 이를 완벽하게 구현하려면 &lt;strong&gt;모든 메모리 참조마다&lt;/strong&gt; 해당 페이지를 리스트의 가장 앞으로 옮기는 등의 작업이 필요합니다. 이는 하드웨어적으로나 성능적으로 오버헤드가 너무 큽니다.&lt;/p&gt;
&lt;p&gt;그래서 현대 시스템은 LRU를 완벽하게 구현하는 대신 &lt;strong&gt;근사(Approximation)&lt;/strong&gt; 하는 방법을 사용합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 시계 알고리즘 (Clock Algorithm): LRU 근사&lt;/h2&gt;
&lt;p&gt;근사의 핵심 도구는 &lt;strong&gt;Use Bit (Reference Bit)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;페이지가 참조될 때 하드웨어가 &lt;strong&gt;Use Bit를 1로 설정&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;OS는 페이지들을 원형 리스트(시계)로 관리하며 &lt;strong&gt;시계 바늘&lt;/strong&gt;을 돌립니다 .&lt;/li&gt;
&lt;li&gt;교체가 필요하면 바늘이 가리키는 페이지의 Use Bit를 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1이면&lt;/strong&gt;: 최근에 사용되었으므로 기회를 줍니다. &lt;strong&gt;0으로 클리어&lt;/strong&gt;하고 다음 페이지로 이동합니다 .&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0이면&lt;/strong&gt;: 최근에 사용되지 않았으므로 &lt;strong&gt;교체 대상(Victim)&lt;/strong&gt; 으로 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 방식은 주기적으로 Use Bit를 청소하며 오래된 페이지를 찾아내므로, LRU와 유사하게 동작하면서도 오버헤드가 적습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Dirty Page와 Thrashing&lt;/h2&gt;
&lt;h3&gt;8.1 Dirty Page 고려&lt;/h3&gt;
&lt;p&gt;내용이 수정된 &lt;strong&gt;Dirty Page&lt;/strong&gt;를 내보내려면 디스크에 다시 써야(Write-back) 하므로 비용이 비쌉니다. 반면 수정되지 않은 &lt;strong&gt;Clean Page&lt;/strong&gt;는 그냥 버리면 됩니다.&lt;/p&gt;
&lt;p&gt;따라서 OS는 &lt;strong&gt;Modified Bit (Dirty Bit)&lt;/strong&gt; 를 함께 확인하여, 기왕이면 &lt;strong&gt;Unused &amp;amp; Clean&lt;/strong&gt; 페이지를 우선적으로 교체하려 합니다.&lt;/p&gt;
&lt;h3&gt;8.2 Thrashing (스레싱)&lt;/h3&gt;
&lt;p&gt;실행 중인 프로세스들의 메모리 요구량(Working Set의 합)이 물리 메모리를 초과하면, 시스템은 끊임없이 페이지 교체만 하느라 실제 작업은 거의 못 하게 됩니다. 이를 &lt;strong&gt;스레싱&lt;/strong&gt;이라 합니다.&lt;/p&gt;
&lt;p&gt;대응 방법은 크게 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;입장 제어 (Admission Control)&lt;/strong&gt;: 일부 프로세스를 잠시 재우거나 실행을 늦춰서, 나머지 프로세스들이 충분한 메모리를 확보하게 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OOM Killer&lt;/strong&gt;: 최악의 경우, 메모리를 많이 쓰는 프로세스를 강제로 종료하여 시스템을 살려냅니다 .&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 교체 외 정책도 있다&lt;/h2&gt;
&lt;p&gt;페이지 교체 외에도 VM 시스템은 다양한 정책을 가집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page Selection Policy&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Demand Paging&lt;/strong&gt;: 페이지가 실제로 접근될 때 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefetching&lt;/strong&gt;: 곧 사용될 페이지를 예측해 미리 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write Policy&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clustering&lt;/strong&gt;: 여러 번의 쓰기 작업을 모아서 한 번에 디스크에 씁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 요약: 효율적인 페이지 교체를 위한 설계 원칙&lt;/h2&gt;
&lt;p&gt;이번 장을 통해 우리는 단순히 누구를 내보낼까를 넘어, 시스템 전체 성능을 결정짓는 핵심 설계 원칙들을 배웠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;성능의 척도는 AMAT이다&lt;/strong&gt;: 디스크 I/O는 메모리보다 압도적으로 느리기 때문에, 아주 미세한 미스율($P_{miss}$)의 차이가 시스템 전체 속도를 결정짓습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;미래는 알 수 없으므로 과거를 빌려온다&lt;/strong&gt;: 이상적인 OPT(미래 기준)는 구현이 불가능하므로, 우리는 &apos;지역성(Locality)&apos;의 원리를 믿고 &apos;최근 기록(History)&apos;인 LRU를 대안으로 삼습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;완벽함보다 효율성이 우선이다&lt;/strong&gt;: 완벽한 LRU는 관리가 너무 비싸기 때문에, 현대 OS는 하드웨어의 Use Bit를 활용한 &apos;Clock 알고리즘&apos;으로 성능과 오버헤드 사이에서 현실적인 타협점을 찾습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 비용의 비대칭성을 활용한다&lt;/strong&gt;: 수정된 페이지(Dirty)를 내보내는 것은 추가적인 쓰기 작업을 유발하므로, Clean 페이지를 우선적으로 교체해 디스크 부담을 줄이는 것이 영리한 설계입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시스템의 한계를 인정해야 한다&lt;/strong&gt;: 아무리 좋은 알고리즘도 물리 메모리 부족(Thrashing) 앞에서는 무력합니다. 이때는 프로세스 수를 줄이는 &apos;입장 제어(Admission Control)&apos;나 &apos;OOM Killer&apos; 같은 시스템 차원의 결단이 필요합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AMAT&lt;/code&gt;: 평균 메모리 접근 시간 (Average Memory Access Time).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPT / MIN&lt;/code&gt;: 미래를 보고 가장 늦게 다시 쓰일 페이지를 교체하는 이상적 정책.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIFO&lt;/code&gt;: 먼저 들어온 페이지를 먼저 교체하는 정책.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Random&lt;/code&gt;: 무작위로 교체 대상을 선택하는 정책.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LRU&lt;/code&gt;: 가장 오랫동안 사용되지 않은(Least Recently Used) 페이지를 교체하는 정책.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;시간/공간 지역성&lt;/code&gt;: 최근 접근한 데이터나 그 주변 데이터가 다시 접근될 가능성이 높다는 성질.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Use Bit (Reference Bit)&lt;/code&gt;: 페이지가 참조되었는지를 나타내는 비트.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Clock Algorithm&lt;/code&gt;: Use Bit를 활용해 LRU를 근사하는 알고리즘.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dirty Bit (Modified Bit)&lt;/code&gt;: 페이지 내용이 수정되었는지를 나타내는 비트.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Thrashing&lt;/code&gt;: 메모리 부족으로 인해 페이지 교체만 반복되며 성능이 급격히 저하되는 현상.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Working Set&lt;/code&gt;: 프로세스가 원활히 실행되기 위해 필요한 활성 페이지들의 집합.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Admission Control&lt;/code&gt;: 시스템 부하 조절을 위해 작업의 진입을 제한하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OOM Killer&lt;/code&gt;: 메모리 부족 시 프로세스를 강제 종료하여 시스템을 복구하는 리눅스 커널 기능.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys-policy.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 22: Beyond Physical Memory: Policies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 21. Beyond Physical Memory: Mechanisms</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-21-vm-beyondphys/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-21-vm-beyondphys/</guid><description>물리 메모리보다 큰 가상 주소 공간을 지원하기 위한 핵심 메커니즘인 Swap Space와 Page Fault 처리 과정을 다룹니다. Present Bit의 역할, OS의 Page Fault Handler 동작 방식, 그리고 Swap Daemon의 역할을 정리합니다.</description><pubDate>Wed, 28 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지금까지는 활성 페이지가 모두 물리 메모리에 있다고 가정했습니다. 하지만 현대 시스템은 물리 메모리보다 훨씬 큰 프로그램을 실행하거나, 여러 프로그램을 동시에 실행해야 합니다.&lt;/p&gt;
&lt;p&gt;이를 위해 OS는 메모리 계층 구조에서 한 단계 아래에 있는 저장장치(HDD 또는 SSD)를 활용합니다. 당장 필요하지 않은 페이지는 디스크로 내리고, 다시 필요해지면 메모리로 올리는 방식으로 큰 가상 주소 공간을 제공합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 스왑 공간 (Swap Space)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./images/21_1.png&quot; alt=&quot;Figure 21.1 Physical Memory and Swap Space&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OS는 페이지를 임시로 보관하기 위해 디스크에 &lt;strong&gt;스왑 공간(Swap Space)&lt;/strong&gt; 이라는 영역을 예약합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;역할&lt;/strong&gt;: 메모리에서 쫓겨난 페이지를 보관합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단위&lt;/strong&gt;: OS는 스왑 공간을 페이지 크기 단위로 읽고 씁니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;요구사항&lt;/strong&gt;: 스왑 공간에서 각 페이지가 저장된 위치(Disk Address)를 OS가 추적해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구조 덕분에 프로세스는 물리 메모리 크기에 제한받지 않고 더 큰 주소 공간을 가진 것처럼 실행될 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 핵심 메커니즘: Valid Bit와 Present Bit&lt;/h2&gt;
&lt;p&gt;하드웨어는 주소 변환 시 해당 페이지가 접근 가능한 매핑인지, 그리고 실제로 메모리에 올라와 있는지 확인해야 합니다. 이를 위해 PTE에는 보통 두 종류의 상태 비트가 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Valid Bit&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;1: 해당 가상 페이지는 프로세스 주소 공간에 속함 (접근 가능).&lt;/li&gt;
&lt;li&gt;0: 주소 공간에 속하지 않음, 접근하면 즉시 예외 처리.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Present Bit&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;1: 페이지가 물리 메모리에 존재함.&lt;/li&gt;
&lt;li&gt;0: 주소 공간에는 속하지만 현재 메모리에 없고 스왑(디스크)에 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;중요한 구분은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Valid 0&lt;/strong&gt;: 잘못된 접근(Invalid Access), 스왑 인 대상이 아님.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid 1 &amp;amp; Present 0&lt;/strong&gt;: 정상 주소이지만 메모리에 없어 OS가 디스크에서 가져와야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번 장에서 집중하는 &lt;strong&gt;페이지 폴트(Page Fault)&lt;/strong&gt; 는 두 번째 경우입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 페이지 폴트 (Page Fault) 처리 과정&lt;/h2&gt;
&lt;p&gt;프로그램이 Valid 1인 페이지에 접근했지만 Present 0이면, 하드웨어는 주소 변환을 완료할 수 없어 예외를 발생시킵니다. 이 예외가 바로 &lt;strong&gt;Page Fault&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;Page Fault는 디스크 I/O가 필요하므로 처리는 반드시 OS의 &lt;strong&gt;Page Fault Handler&lt;/strong&gt;가 맡습니다.&lt;/p&gt;
&lt;h3&gt;처리 순서 (Control Flow)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;예외 발생&lt;/strong&gt;: 하드웨어가 PTE를 확인하다 Present 0을 발견하고 Page Fault Exception을 발생시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS 개입 및 위치 확인&lt;/strong&gt;: OS는 해당 페이지가 스왑 공간의 어디에 저장되어 있는지 확인합니다. 일반적으로 Present 0인 동안 PTE의 PFN 필드는 실제 PFN이 아니라 &lt;strong&gt;디스크 주소(Disk Address)&lt;/strong&gt; 정보를 담도록 재해석해 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;빈 프레임 확보&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Free Frame이 있으면 바로 사용합니다.&lt;/li&gt;
&lt;li&gt;없으면 &lt;strong&gt;Victim Page&lt;/strong&gt;를 골라 내보내기(Eviction)를 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디스크 I/O 요청&lt;/strong&gt;: OS가 디스크에 페이지 읽기 요청을 보내고, 지정한 물리 프레임에 적재하도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스 차단 (Block)&lt;/strong&gt;: 디스크 I/O는 매우 느리므로, OS는 Fault를 낸 프로세스를 &lt;strong&gt;Blocked&lt;/strong&gt; 상태로 전환하고 그동안 다른 Ready 프로세스를 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 완료 처리&lt;/strong&gt;: 디스크 작업이 끝나면 인터럽트가 발생하고, OS가 완료 처리를 수행합니다. PTE를 갱신합니다.
&lt;ul&gt;
&lt;li&gt;Present Bit를 1로 변경.&lt;/li&gt;
&lt;li&gt;PFN 필드에 실제 물리 프레임 번호 기록.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;재실행 (Retry)&lt;/strong&gt;: OS는 Fault를 일으켰던 명령어를 재시도(Retry)합니다.
&lt;ul&gt;
&lt;li&gt;재실행 시 &lt;strong&gt;TLB Miss&lt;/strong&gt;가 다시 발생할 수 있으며, 이때는 페이지가 메모리에 존재하므로(Present 1) 정상적으로 TLB가 업데이트되고 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 메모리가 가득 찼다면 (Replacement)&lt;/h2&gt;
&lt;p&gt;Free Frame이 없으면 OS는 어떤 페이지를 내보낼지 결정해야 합니다. 이를 &lt;strong&gt;페이지 교체(Page Replacement)&lt;/strong&gt; 라고 합니다.&lt;/p&gt;
&lt;p&gt;잘못된 페이지를 반복적으로 내보내면 성능이 급격히 떨어질 수 있습니다. 어떤 페이지를 내보낼지는 &lt;strong&gt;정책(Policy)&lt;/strong&gt; 의 영역이며, 다음 장에서 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 스왑 데몬 (The Swap Daemon)&lt;/h2&gt;
&lt;p&gt;OS는 메모리가 완전히 바닥난 다음에야 대응하는 방식보다, 일정량의 여유 프레임을 유지하는 쪽이 안정적입니다. 이를 위해 &lt;strong&gt;High Watermark (HW)&lt;/strong&gt; 와 &lt;strong&gt;Low Watermark (LW)&lt;/strong&gt; 를 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여유 메모리가 &lt;strong&gt;LW&lt;/strong&gt; 아래로 떨어지면 백그라운드 스레드가 동작합니다.&lt;/li&gt;
&lt;li&gt;여유 메모리를 &lt;strong&gt;HW&lt;/strong&gt; 수준까지 회복시키면 다시 대기(Sleep)합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 백그라운드 스레드는 &lt;strong&gt;Swap Daemon&lt;/strong&gt; 또는 &lt;strong&gt;Page Daemon&lt;/strong&gt;으로 불리며, 핵심 역할은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;미리 페이지를 비워(Free) 여유 공간을 확보합니다.&lt;/li&gt;
&lt;li&gt;Page Fault Handler가 매번 교체 작업을 길게 수행하지 않도록 부담을 줄입니다.&lt;/li&gt;
&lt;li&gt;여러 페이지를 묶어 한 번에 쓰는 &lt;strong&gt;Clustering&lt;/strong&gt; 같은 방식으로 디스크 효율을 높일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장에서는 물리 메모리의 한계를 넘어서는 메커니즘을 살펴보았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Swap Space&lt;/strong&gt;: 디스크를 메모리의 확장 공간으로 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Present Bit&lt;/strong&gt;: 페이지가 메모리에 있는지 디스크에 있는지 구분합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page Fault&lt;/strong&gt;: Present 0인 페이지 접근 시 발생하며, OS가 디스크 I/O로 해결합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swap Daemon&lt;/strong&gt;: 백그라운드에서 여유 메모리를 유지해 시스템 효율을 높입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 스왑을 수행하는 방법을 봤고, 다음 장에서는 어떤 페이지를 내보낼지 결정하는 교체 정책을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Swap Space&lt;/code&gt;: 메모리에서 쫓겨난 페이지들을 보관하기 위해 디스크에 미리 예약된 공간.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Present Bit&lt;/code&gt;: 해당 페이지가 물리 메모리에 존재하는지(1), 아니면 스왑 공간에 있는지(0)를 나타내는 PTE의 비트.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page Fault&lt;/code&gt;: 프로그램이 접근하려는 유효한 페이지가 현재 물리 메모리에 없을 때 하드웨어가 발생시키는 예외(Exception).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page Fault Handler&lt;/code&gt;: 페이지 폴트 발생 시 실행되는 OS 코드로, 디스크에서 페이지를 읽어와 메모리에 적재하는 역할을 수행함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Swap Daemon (Page Daemon)&lt;/code&gt;: 여유 공간이 부족할 때(Low Watermark 도달) 백그라운드에서 실행되어 페이지를 스왑 아웃시키는 커널 스레드.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;High/Low Watermark&lt;/code&gt;: 스왑 데몬이 동작을 시작(Low)하고 멈추는(High) 기준이 되는 여유 메모리 임계값.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 21: Beyond Physical Memory: Mechanisms&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 20. Paging: Smaller Tables</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-20-vm-smalltables/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-20-vm-smalltables/</guid><description>Linear Page Table의 과도한 메모리 사용 문제를 줄이기 위한 기법들: Bigger Pages, Hybrid(Paging+Segmentation), Multi-level Page Table, Inverted Page Table, 그리고 Page Table Swapping을 정리합니다.</description><pubDate>Mon, 26 Jan 2026 19:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장에서 Paging의 동작과 TLB 기반 가속을 봤다면, 이번 장의 핵심은 &lt;strong&gt;Page Table의 공간(Space) 비용&lt;/strong&gt;입니다.
TLB로 시간(Time)은 어느 정도 잡았는데, Page Table은 프로세스 수만큼 존재하니 메모리 소모가 무시하기 어렵습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;32-bit 주소 공간&lt;/strong&gt;, &lt;strong&gt;4KB Page&lt;/strong&gt;, &lt;strong&gt;4-byte PTE&lt;/strong&gt;를 가정하면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가상 페이지 수: $2^{32} / 2^{12} = 2^{20}$ (약 100만 개)&lt;/li&gt;
&lt;li&gt;Page Table 크기: $2^{20} \times 4\text{B} = 4\text{MB}$&lt;/li&gt;
&lt;li&gt;프로세스 100개면: &lt;strong&gt;$400\text{MB}$&lt;/strong&gt; 가 Page Table로만 빠질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 간단한 해결책: Bigger Pages&lt;/h2&gt;
&lt;p&gt;가장 직관적인 방법은 &lt;strong&gt;Page Size를 키워서&lt;/strong&gt; 페이지 수(=PTE 수)를 줄이는 것입니다.&lt;/p&gt;
&lt;p&gt;예를 들어 32-bit 주소 공간에서 Page Size를 $4\text{KB} \rightarrow 16\text{KB}$로 올리면 다음과 같이 변화합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offset: $12\text{bit} \rightarrow 14\text{bit}$&lt;/li&gt;
&lt;li&gt;VPN: $20\text{bit} \rightarrow 18\text{bit}$&lt;/li&gt;
&lt;li&gt;PTE 개수: $2^{20} \rightarrow 2^{18}$&lt;/li&gt;
&lt;li&gt;Page Table: $4\text{MB} \rightarrow 1\text{MB}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, &lt;strong&gt;Page Size를 4배 키우면 Page Table도 정확히 4배 줄어듭니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;단점: Internal Fragmentation&lt;/h3&gt;
&lt;p&gt;하지만 페이지가 커질수록 프로세스가 실제로 쓰지 않는 공간이 페이지 내부에 남아 &lt;strong&gt;내부 단편화(Internal Fragmentation)&lt;/strong&gt; 가 커집니다.
그래서 대부분의 시스템은 기본 페이지를 작게 유지하고(예: $4\text{KB}$, $8\text{KB}$), 필요할 때만 큰 페이지를 쓰는 식의 타협을 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Hybrid 접근법: Paging + Segmentation&lt;/h2&gt;
&lt;p&gt;Linear Page Table이 큰 이유는 &lt;strong&gt;사용하지 않는 가상 주소 영역까지 PTE를 전부 만들어&lt;/strong&gt; 들고 있기 때문입니다.
일반적인 프로세스 주소 공간은 Code, Heap, Stack만 듬성듬성 쓰고, 중간에 큰 Hole(빈 공간)이 남는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/20_1.png&quot; alt=&quot;Figure 20.1 16KB Address Space with 1KB Pages&quot; /&gt;
&lt;em&gt;Figure 20.1: 대부분이 비어있는(Sparse) 주소 공간의 예시. 이 빈 공간을 위해 Page Table을 유지하는 것은 낭비입니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;그래서 나온 아이디어가 &lt;strong&gt;“세그먼트별로 Page Table을 따로 두자”&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code segment용 Page Table&lt;/li&gt;
&lt;li&gt;Heap segment용 Page Table&lt;/li&gt;
&lt;li&gt;Stack segment용 Page Table&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 Segmentation의 Base/Bounds 개념을 재활용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Base&lt;/strong&gt;: “세그먼트 자체의 시작”이 아니라 &lt;strong&gt;해당 세그먼트 Page Table의 물리 주소&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounds&lt;/strong&gt;: 해당 Page Table의 끝(=유효한 페이지 개수)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hole 구간을 위한 PTE를 만들지 않으므로, &lt;strong&gt;희소(Sparse) 주소 공간에서 낭비를 크게 줄일 수 있음&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점: External Fragmentation(다시 등장)&lt;/h3&gt;
&lt;p&gt;이 방식은 Page Table이 “페이지 단위”가 아니라 &lt;strong&gt;PTE 개수 단위로(가변 크기)&lt;/strong&gt; 메모리를 차지하게 됩니다.
즉, 물리 메모리에서 &lt;strong&gt;딱 맞는 빈 공간을 찾아 넣는 문제&lt;/strong&gt;가 생기며, 결과적으로 &lt;strong&gt;External Fragmentation 성격의 복잡성&lt;/strong&gt;을 다시 불러옵니다.&lt;/p&gt;
&lt;p&gt;또한 문맥 교환 시 세그먼트별 Base/Bounds도 함께 교체되어야 하므로 구현/관리 부담이 늘어납니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Multi-level Page Tables&lt;/h2&gt;
&lt;p&gt;Hybrid가 Segmentation을 끌어온 타협이라면, Multi-level은 &lt;strong&gt;Page Table 자체를 Paging하자&lt;/strong&gt; 라는 접근입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/20_2.png&quot; alt=&quot;Figure 20.2 A Page Table for 16KB Address Space&quot; /&gt;
&lt;em&gt;Figure 20.2: (문제점) 대부분이 Invalid인 Linear Page Table. 낭비되는 공간이 많습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;핵심 아이디어는 간단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Linear Page Table을 &lt;strong&gt;페이지 크기 단위로 조각&lt;/strong&gt;냅니다.&lt;/li&gt;
&lt;li&gt;어떤 조각(Page Table Page) 안에 &lt;strong&gt;유효한 PTE가 하나도 없으면&lt;/strong&gt;, 그 조각은 &lt;strong&gt;아예 할당하지 않습니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;대신, 어떤 조각이 존재하는지 추적하는 상위 구조가 필요합니다. → &lt;strong&gt;Page Directory&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.1 시각화: Linear vs Multi-level&lt;/h3&gt;
&lt;p&gt;Multi-level이 하는 일을 가장 직관적으로 보여주는 그림이 &lt;strong&gt;Figure 20.3&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linear&lt;/strong&gt;: 주소 공간 중간이 비어 있어도, 중간에 해당하는 Page Table Page까지 전부 메모리에 존재해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-level&lt;/strong&gt;: Page Directory가 필요한 조각만 Valid로 표시하고, 나머지 조각은 &lt;strong&gt;Not Allocated&lt;/strong&gt; 상태로 둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/20_3.png&quot; alt=&quot;Figure 20.3 Linear vs Multi-level&quot; /&gt;
&lt;em&gt;Figure 20.3: Linear(좌) vs Multi-level(우). 중간 Page Table Page를 사라지게 만들고, Page Directory로 추적합니다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;3.2 Page Directory / PDE의 의미&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Page Directory는 “Page Table의 각 조각(Page Table Page)”마다 엔트리를 하나씩 가집니다.&lt;/li&gt;
&lt;li&gt;엔트리 = &lt;strong&gt;PDE(Page Directory Entry)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PDE는 최소한 다음을 포함합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Valid Bit&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PFN&lt;/strong&gt; (해당 Page Table Page가 올라간 물리 프레임 번호)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;중요한 포인트 하나: &lt;strong&gt;PDE의 Valid 의미는 PTE의 Valid와 조금 다릅니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PDE Valid&lt;/strong&gt;: 이 PDE가 가리키는 Page Table Page 안에 &lt;strong&gt;Valid PTE가 최소 1개라도 존재&lt;/strong&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDE Invalid&lt;/strong&gt;: 그 조각은 아예 존재하지 않으며, PFN 등 나머지 필드는 정의되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 장단점 (Time–Space Trade-off)&lt;/h3&gt;
&lt;h4&gt;장점(Space)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;사용한 주소 공간의 양에 비례해서만 Page Table 공간을 할당 → &lt;strong&gt;Sparse Address Space에 강함&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;조각이 “페이지 단위”로 관리되므로, OS가 Page Table을 늘릴 때도 &lt;strong&gt;그냥 빈 페이지 하나 잡아 붙이면 됨&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;(그 결과) &lt;strong&gt;연속된 큰 물리 메모리 덩어리&lt;/strong&gt;가 꼭 필요하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;단점(Time)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;(2-level 기준) TLB Miss가 나면 Page Walk 과정에서 &lt;strong&gt;추가 메모리 접근 2회&lt;/strong&gt;가 필요합니다.
&lt;ol&gt;
&lt;li&gt;Page Directory 접근&lt;/li&gt;
&lt;li&gt;Page Table 접근&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Multi-level을 손으로 따라가기: Detailed Example&lt;/h2&gt;
&lt;p&gt;OSTEP는 Multi-level이 어떻게 인덱싱되는지를 작은 예제로 풀어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;가정:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;주소 공간: $16\text{KB}$&lt;/li&gt;
&lt;li&gt;페이지 크기: $64\text{B}$&lt;/li&gt;
&lt;li&gt;가상 주소: $14\text{bit}$ (VPN $8\text{bit}$ + Offset $6\text{bit}$)&lt;/li&gt;
&lt;li&gt;PTE 크기: $4\text{B}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linear Page Table은 $2^8 = 256$ 엔트리 → $256 \times 4\text{B} = 1\text{KB}$가 됩니다.
그런데 페이지가 $64\text{B}$이므로, $1\text{KB}$ Page Table은 $1024 / 64 = 16$개의 Page Table Page로 나눌 수 있고, 각 Page Table Page는 $64 / 4 = 16$개의 PTE를 담습니다.&lt;/p&gt;
&lt;h3&gt;4.1 주소공간 예시 (Figure 20.4)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/20_4.png&quot; alt=&quot;Figure 20.4 Address Space Example&quot; /&gt;
&lt;em&gt;Figure 20.4: 16KB 주소공간 + 64B 페이지 예시.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;4.2 Page Directory와 조각난 Page Table (Figure 20.5)&lt;/h3&gt;
&lt;p&gt;Figure 20.5는 Page Directory가 &lt;strong&gt;어떤 Page Table Page(PFN 100, 101 등)를 가리키는지&lt;/strong&gt;, 그리고 그 조각 안에 어떤 PTE가 들어있는지를 “3단(Directory / PT page / PT page)” 형태로 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/20_5.png&quot; alt=&quot;Figure 20.5 Page Directory and Pieces of Page Table&quot; /&gt;
&lt;em&gt;Figure 20.5: Page Directory(좌)와 Page Table 조각(중/우). Linear의 16페이지 대신 3페이지만 할당하는 예시를 보여줍니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심 결론:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linear였다면 Page Table Page를 16개 다 잡아야 하지만,&lt;/li&gt;
&lt;li&gt;Multi-level에서는:
&lt;ul&gt;
&lt;li&gt;Page Directory 1페이지&lt;/li&gt;
&lt;li&gt;실제로 쓰는 PT 조각 2페이지&lt;/li&gt;
&lt;li&gt;총 &lt;strong&gt;3페이지만&lt;/strong&gt; 쓰는 식으로 절약이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 주소 변환 예시 (계산)&lt;/h3&gt;
&lt;p&gt;OSTEP는 예시 주소 &lt;code&gt;0x3F80&lt;/code&gt;(VPN 254의 0th byte)을 변환합니다.&lt;/p&gt;
&lt;p&gt;이 예제에서는 VPN $8\text{bit}$를 상위 $4\text{bit}$/하위 $4\text{bit}$로 나눠서 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상위 $4\text{bit}$: &lt;strong&gt;Page Directory Index&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;하위 $4\text{bit}$: &lt;strong&gt;Page Table Index&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;변환 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PD Index 추출&lt;/strong&gt;: &lt;code&gt;1111&lt;/code&gt; ($15$) → Directory의 마지막 엔트리 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PT Index 추출&lt;/strong&gt;: &lt;code&gt;1110&lt;/code&gt; ($14$) → 해당 PT Page의 14번째 엔트리 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;물리 주소 계산&lt;/strong&gt;: 결과 PTE가 $\text{PFN} = 55$라면:
&lt;ul&gt;
&lt;li&gt;$\text{PhysAddr} = (55 \ll 6) + \text{Offset}$&lt;/li&gt;
&lt;li&gt;Offset이 0이면 최종 주소는 &lt;code&gt;0x0DC0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;제어 흐름 (Control Flow)&lt;/h4&gt;
&lt;p&gt;주소 변환 과정을 코드로 표현하면 다음과 같습니다. Linear 방식보다 메모리 접근이 한 번 더 발생(Directory → Table)하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 가상 주소 분해
VPN = (VirtualAddress &amp;amp; VPN_MASK) &amp;gt;&amp;gt; SHIFT
(PDIndex, PTIndex) = Split(VPN)

// 2. Page Directory 접근 (첫 번째 메모리 접근)
PDEAddr = PageDirBase + (PDIndex * sizeof(PDE))
PDE = AccessMemory(PDEAddr)

if (PDE.Valid == False) {
    RaiseException(SEGMENTATION_FAULT) // 존재하지 않는 조각
} else {
    // 3. Page Table 접근 (두 번째 메모리 접근)
    // PDE.PFN은 해당 Page Table Page의 물리 주소를 가리킴
    PTEAddr = (PDE.PFN &amp;lt;&amp;lt; SHIFT) + (PTIndex * sizeof(PTE))
    PTE = AccessMemory(PTEAddr)

    if (PTE.Valid == False) {
        RaiseException(SEGMENTATION_FAULT) // 유효하지 않은 페이지
    } else if (CanAccess(PTE.ProtectBits) == False) {
        RaiseException(PROTECTION_FAULT)
    } else {
        // 4. TLB 업데이트 및 재실행
        TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits)
        RetryInstruction()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 더 깊은 계층: More Than Two Levels&lt;/h2&gt;
&lt;p&gt;2-level로 끝나지 않는 이유는 간단합니다.
&lt;strong&gt;&quot;각 레벨의 조각이 한 페이지에 딱 들어가게 만들자&quot;&lt;/strong&gt; 가 목표인데, &lt;strong&gt;Page Directory 자체가 너무 커질 수 있기 때문&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OSTEP 예제:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VA = $30\text{bit}$&lt;/li&gt;
&lt;li&gt;Page = $512\text{B}$ → Offset $9\text{bit}$&lt;/li&gt;
&lt;li&gt;VPN = $21\text{bit}$&lt;/li&gt;
&lt;li&gt;PTE $4\text{B}$ → 한 페이지($512\text{B}$)에 PTE 128개 → 인덱스에 $7\text{bit}$ 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, $21\text{bit}$ VPN 중 하위 $7\text{bit}$는 PT Index로 쓰고 나면, 상위에 $14\text{bit}$가 남습니다.
그런데 Directory가 $2^{14}$ 엔트리이고 PDE가 $4\text{B}$라면, Directory 크기는 $2^{14} \times 4\text{B} = 64\text{KB}$로 &lt;strong&gt;128페이지&lt;/strong&gt;가 됩니다.
이러면 Directory도 한 페이지에 들어가게 하자는 목표가 깨지므로, Directory를 다시 Paging하고 상위 Directory를 추가해서 트리를 더 깊게 만듭니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Inverted Page Tables&lt;/h2&gt;
&lt;p&gt;Multi-level과는 다른 방향으로, 공간을 더 줄이는 접근이 &lt;strong&gt;Inverted Page Table&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“프로세스마다 Page Table”을 두는 대신,&lt;/li&gt;
&lt;li&gt;시스템에 &lt;strong&gt;단 하나의 Page Table&lt;/strong&gt;만 두고,&lt;/li&gt;
&lt;li&gt;엔트리는 “가상 페이지”가 아니라 &lt;strong&gt;물리 페이지(Physical Page)마다 하나씩&lt;/strong&gt; 둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 각 엔트리는 대략 다음 정보를 담습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이 물리 페이지를 누가(어떤 Process) 쓰는지&lt;/li&gt;
&lt;li&gt;그 프로세스의 어떤 VPN이 매핑되는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문제는 &lt;strong&gt;Lookup&lt;/strong&gt;입니다.
가상주소(VPN) → PFN을 찾으려면 테이블을 검색해야 하므로, 선형 탐색은 비싸고 보통 &lt;strong&gt;해시(Hash)&lt;/strong&gt; 를 사용하여 속도를 높입니다 (예: PowerPC).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Swapping the Page Tables to Disk&lt;/h2&gt;
&lt;p&gt;지금까지는 Page Table이 &lt;strong&gt;항상 물리 메모리(커널 소유)&lt;/strong&gt; 에 있다고 가정했습니다.
하지만 트릭을 다 써도 Page Table이 너무 커질 수 있고, 이때 일부 시스템은 다음과 같이 동작합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Page Table을 &lt;strong&gt;Kernel Virtual Memory&lt;/strong&gt;에 두고,&lt;/li&gt;
&lt;li&gt;메모리 압박이 크면 Page Table의 일부를 &lt;strong&gt;디스크로 Swap Out&lt;/strong&gt; 하기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 요약 (Summary)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bigger Pages&lt;/strong&gt;: Page Table은 줄지만 Internal Fragmentation이 커짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid (Paging + Segmentation)&lt;/strong&gt;: Hole을 피할 수 있으나, Page Table이 가변 크기로 흩어지며 외부 단편화 성격의 문제와 복잡성이 다시 등장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-level Page Table&lt;/strong&gt;: Page Table을 Page-sized 조각으로 나누고, 필요한 조각만 할당.
&lt;ul&gt;
&lt;li&gt;Sparse Address Space에 강함.&lt;/li&gt;
&lt;li&gt;TLB Miss 시 Page Walk 비용(추가 접근)이 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More Than Two Levels&lt;/strong&gt;: Directory도 Page-sized Unit으로 유지하려면 트리를 더 깊게 해야 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inverted Page Table&lt;/strong&gt;: “물리 페이지당 1엔트리”로 공간을 줄이는 대신 Lookup을 해시 등으로 해결.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swapping PT to Disk&lt;/strong&gt;: Page Table조차 메모리에 다 못 두면 일부를 디스크로 내릴 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Linear Page Table&lt;/code&gt;: VPN을 인덱스로 바로 접근하는 배열형 페이지 테이블&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Internal Fragmentation&lt;/code&gt;: 할당 단위(페이지) 내부에서 남는 낭비&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Hybrid (Paging + Segmentation)&lt;/code&gt;: 세그먼트별로 별도 Page Table을 두는 접근&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page Directory&lt;/code&gt;: Page Table 조각들의 존재/위치를 추적하는 상위 테이블&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PDE (Page Directory Entry)&lt;/code&gt;: Page Directory의 엔트리 (조각 존재 여부 + PFN 등)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Multi-level Page Table&lt;/code&gt;: Page Directory를 루트로 하는 트리형 페이지 테이블&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page Walk&lt;/code&gt;: TLB Miss 시 Page Table을 따라가며 변환을 찾는 과정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Inverted Page Table&lt;/code&gt;: 물리 페이지당 하나의 엔트리를 갖는 단일 페이지 테이블 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-smalltables.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 20: Paging: Smaller Tables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 19. 변환 색인 버퍼 (TLB: Translation Lookaside Buffers)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-19-vm-tlbs/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-19-vm-tlbs/</guid><description>페이징의 성능 문제인 메모리 접근 오버헤드를 해결하기 위한 하드웨어 캐시, TLB(Translation Lookaside Buffer)를 상세히 다룹니다. 기본 알고리즘, 지역성(Locality)의 원리, 미스 처리 방식, 그리고 문맥 교환 시 발생하는 문제와 해결책(ASID)을 정리합니다.</description><pubDate>Sun, 25 Jan 2026 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 18장에서 우리는 페이징(Paging)이 외부 단편화 문제를 해결하고 유연한 메모리 관리를 가능하게 한다는 것을 배웠습니다. 하지만 페이징에는 치명적인 비용이 따릅니다.&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;주소 변환을 위해 메모리에 있는 페이지 테이블을 매번 읽어야 한다.&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;즉, 명령어 인출(Fetch)이나 데이터 로드(Load)를 할 때마다 메모리 접근이 최소 2배로 늘어나게 되어 시스템 성능이 심각하게 저하됩니다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 이 문제를 해결하기 위해 도입된 하드웨어 &lt;strong&gt;변환 색인 버퍼(TLB: Translation Lookaside Buffer)&lt;/strong&gt; 에 대해 알아봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. TLB의 기본 개념 (Basic Algorithm)&lt;/h2&gt;
&lt;p&gt;TLB는 MMU(Memory Management Unit)의 일부로서, &lt;strong&gt;가상 주소에서 물리 주소로의 변환 정보를 저장하는 하드웨어 캐시(Cache)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;주소 변환이 필요할 때 하드웨어는 페이지 테이블(메모리)을 보기 전에 먼저 TLB(캐시)를 확인합니다. 하드웨어의 동작 로직을 의사 코드(Pseudo-code)로 표현하면 다음과 같습니다.&lt;/p&gt;
&lt;h3&gt;하드웨어 제어 흐름 (TLB Control Flow)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 0. 가상 주소에서 VPN 추출
VPN = (VirtualAddress &amp;amp; VPN_MASK) &amp;gt;&amp;gt; SHIFT

// 1. TLB 검색
(Success, TlbEntry) = TLB_Lookup(VPN)

if (Success == True) { // TLB Hit (빠름)
    if (CanAccess(TlbEntry.ProtectBits) == True) {
        Offset = VirtualAddress &amp;amp; OFFSET_MASK
        PhysAddr = (TlbEntry.PFN &amp;lt;&amp;lt; SHIFT) | Offset
        AccessMemory(PhysAddr)
    } else {
        RaiseException(PROTECTION_FAULT)
    }
}
else { // TLB Miss (느림)
    // 2. 페이지 테이블 조회 (메모리 접근 발생)
    PTEAddr = PTBR + (VPN * sizeof(PTE))
    PTE = AccessMemory(PTEAddr)

    if (PTE.Valid == False) {
        RaiseException(SEGMENTATION_FAULT)
    } else if (CanAccess(PTE.ProtectBits) == False) {
        RaiseException(PROTECTION_FAULT)
    } else {
        // 3. TLB 업데이트 및 명령어 재실행
        TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits)
        RetryInstruction()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심 과정&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;TLB 히트 (Hit)&lt;/strong&gt;: 변환 정보가 TLB에 있다면, 즉시 물리 주소를 계산하여 메모리에 접근합니다. (페이지 테이블 접근 없음)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB 미스 (Miss)&lt;/strong&gt;: 없다면, 하드웨어가 페이지 테이블(메모리)에 접근하여 정보를 가져옵니다. 그 후 TLB를 업데이트하고 명령어를 &lt;strong&gt;재실행(Retry)&lt;/strong&gt; 합니다. 재실행 시에는 히트가 발생합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 지역성 (Locality): TLB가 작동하는 이유&lt;/h2&gt;
&lt;p&gt;TLB는 크기가 매우 작습니다(보통 32~128 항목). 이렇게 작은 캐시가 어떻게 시스템 전체의 성능을 획기적으로 높일 수 있을까요? 그 비밀은 &lt;strong&gt;지역성(Locality)&lt;/strong&gt; 에 있습니다.&lt;/p&gt;
&lt;p&gt;다음은 16바이트 페이지 크기를 가진 시스템에서 정수 배열 &lt;code&gt;a[0]&lt;/code&gt;부터 &lt;code&gt;a[9]&lt;/code&gt;까지 순차적으로 접근하는 상황입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/19_2.png&quot; alt=&quot;Figure 19.2 Array Access Example&quot; /&gt;
&lt;em&gt;Figure 19.2: 배열이 메모리에 배치된 모습. a[0]~a[2]는 같은 페이지(06)에, a[3]~a[6]은 다음 페이지(07)에 위치합니다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;공간 지역성 (Spatial Locality)&lt;/h3&gt;
&lt;p&gt;위 그림을 보면 &lt;code&gt;a[0]&lt;/code&gt;, &lt;code&gt;a[1]&lt;/code&gt;, &lt;code&gt;a[2]&lt;/code&gt;는 모두 &lt;strong&gt;VPN 06&lt;/strong&gt; 페이지에 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;a[0]&lt;/code&gt; 접근: &lt;strong&gt;TLB Miss&lt;/strong&gt;. (VPN 06 정보를 TLB에 로드)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a[1]&lt;/code&gt; 접근: &lt;strong&gt;TLB Hit&lt;/strong&gt;. (이미 06번 정보가 있음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a[2]&lt;/code&gt; 접근: &lt;strong&gt;TLB Hit&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이처럼 한 번 미스가 발생하면, 같은 페이지 내의 인접한 데이터들은 모두 히트가 발생합니다.&lt;/p&gt;
&lt;h3&gt;시간 지역성 (Temporal Locality)&lt;/h3&gt;
&lt;p&gt;프로그램이 루프(Loop)를 돌며 이 배열에 반복해서 접근한다고 가정해 봅시다. 두 번째 반복부터는 필요한 모든 페이지 정보가 이미 TLB에 들어있으므로 &lt;strong&gt;100% 히트&lt;/strong&gt;가 발생합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. TLB 미스 처리: 누가 담당하는가?&lt;/h2&gt;
&lt;p&gt;TLB 미스가 발생했을 때 페이지 테이블을 뒤져서 새로운 정보를 가져오는 작업은 누가 할까요? 이는 아키텍처(CISC vs RISC)마다 다릅니다.&lt;/p&gt;
&lt;h3&gt;3.1 하드웨어 관리 (CISC, 예: x86)&lt;/h3&gt;
&lt;p&gt;하드웨어가 페이지 테이블의 위치(CR3 레지스터 등)와 구조를 정확히 알고 있습니다. 1번 섹션의 코드처럼, 미스가 발생하면 하드웨어가 직접 페이지 테이블을 탐색(Walk)하고 TLB를 업데이트합니다.&lt;/p&gt;
&lt;h3&gt;3.2 소프트웨어 관리 (RISC, 예: MIPS)&lt;/h3&gt;
&lt;p&gt;현대의 많은 RISC 아키텍처는 하드웨어를 단순하게 유지하기 위해 &lt;strong&gt;소프트웨어 관리 방식&lt;/strong&gt;을 사용합니다. 하드웨어의 동작 로직이 훨씬 간단해집니다.&lt;/p&gt;
&lt;h3&gt;하드웨어 제어 흐름 (Software-Managed TLB)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;VPN = (VirtualAddress &amp;amp; VPN_MASK) &amp;gt;&amp;gt; SHIFT
(Success, TlbEntry) = TLB_Lookup(VPN)

if (Success == True) { // TLB Hit
    if (CanAccess(TlbEntry.ProtectBits) == True) {
        Offset = VirtualAddress &amp;amp; OFFSET_MASK
        PhysAddr = (TlbEntry.PFN &amp;lt;&amp;lt; SHIFT) | Offset
        AccessMemory(PhysAddr)
    } else {
        RaiseException(PROTECTION_FAULT)
    }
}
else { // TLB Miss
    RaiseException(TLB_MISS) // 하드웨어는 예외만 발생시키고 끝!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;동작 과정&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;미스 발생 시&lt;/strong&gt;: 하드웨어는 단순히 &lt;code&gt;TLB_MISS&lt;/code&gt; &lt;strong&gt;예외(Exception)&lt;/strong&gt; 를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;트랩 핸들러 실행&lt;/strong&gt;: CPU는 커널 모드로 전환되고, OS의 트랩 핸들러 코드가 실행됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;소프트웨어 처리&lt;/strong&gt;: OS가 페이지 테이블을 조회하고, 특권 명령어를 사용해 TLB를 업데이트합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;재실행&lt;/strong&gt;: 트랩에서 리턴(Return-from-trap)하여 명령어를 재실행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 방식은 하드웨어 설계를 단순화하고, OS가 페이지 테이블 구조(역방향 페이지 테이블, 해시 테이블 등)를 자유롭게 선택할 수 있는 유연성을 제공합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. TLB 항목의 구성 (TLB Contents)&lt;/h2&gt;
&lt;p&gt;TLB는 단순한 VPN-PFN 매핑 외에도 몇 가지 중요한 비트들을 포함합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VPN&lt;/th&gt;
&lt;th&gt;PFN&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;th&gt;Prot&lt;/th&gt;
&lt;th&gt;Dirty&lt;/th&gt;
&lt;th&gt;ASID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Valid Bit&lt;/strong&gt;: 이 항목이 유효한 변환 정보를 담고 있는지 표시합니다. (페이지 테이블의 Valid 비트와는 다릅니다. 부팅 직후에는 모두 0입니다.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protection Bits&lt;/strong&gt;: 읽기/쓰기/실행 권한을 표시합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dirty Bit&lt;/strong&gt;: 해당 페이지가 수정되었는지 여부를 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ASID (Address Space ID)&lt;/strong&gt;: 문맥 교환 문제를 해결하기 위한 프로세스 식별자입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 문맥 교환 문제 (Context Switch Issues)&lt;/h2&gt;
&lt;p&gt;TLB는 프로세스별 가상 주소를 물리 주소로 변환합니다. 만약 프로세스 A에서 프로세스 B로 &lt;strong&gt;문맥 교환(Context Switch)&lt;/strong&gt; 이 일어나면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;프로세스 A의 VPN 10번과 프로세스 B의 VPN 10번은 서로 다른 물리 주소를 가리켜야 합니다. 하지만 TLB에 VPN 10번 정보가 남아있다면 하드웨어는 이를 구분할 수 없습니다.&lt;/p&gt;
&lt;h3&gt;해결책 1: 플러시 (Flush)&lt;/h3&gt;
&lt;p&gt;문맥 교환이 일어날 때마다 TLB의 모든 내용을 비웁니다(Valid 비트를 0으로 설정).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 프로세스가 실행될 때마다 초기 미스(Cold-start miss)가 대량으로 발생하여 성능 비용이 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;해결책 2: ASID (Address Space ID) 활용&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/19_4.png&quot; alt=&quot;Figure 19.4 Context Switch Problem&quot; /&gt;
&lt;em&gt;Figure 19.4: ASID 필드를 추가하여 두 프로세스(ASID 1, 2)가 동일한 VPN(10)을 가지고 있어도 구분할 수 있게 한 모습.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;TLB 항목에 &lt;strong&gt;ASID&lt;/strong&gt; 필드를 추가하여, 어떤 프로세스의 매핑 정보인지 구분합니다. 이렇게 하면 문맥 교환 시에도 TLB를 비울 필요가 없어 성능 저하를 막을 수 있습니다. (PID와 유사하지만 비트 수가 더 적습니다.)&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 교체 정책 (Replacement Policy)&lt;/h2&gt;
&lt;p&gt;TLB가 가득 찼을 때 새로운 항목을 넣으려면 기존 항목 중 하나를 버려야 합니다. 어떤 것을 버리는 게 좋을까요?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LRU (Least Recently Used)&lt;/strong&gt;: 가장 오랫동안 사용하지 않은 항목을 교체합니다. 지역성의 원리에 따라 일반적으로 효율적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random&lt;/strong&gt;: 무작위로 교체합니다. 구현이 매우 간단하고, LRU가 취약한 특정 패턴(TLB 크기보다 약간 큰 데이터를 반복 순회하는 경우)에서 오히려 더 좋은 성능을 보이기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장에서는 페이징의 성능 문제를 해결하는 핵심 장치인 &lt;strong&gt;TLB&lt;/strong&gt;를 다뤘습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TLB는 하드웨어 캐시&lt;/strong&gt;로서 주소 변환 속도를 획기적으로 높여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공간/시간 지역성&lt;/strong&gt; 덕분에 작은 크기로도 높은 히트율을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB 미스&lt;/strong&gt;는 하드웨어 또는 OS(소프트웨어)가 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문맥 교환 시&lt;/strong&gt; 발생하는 문제는 &lt;strong&gt;ASID&lt;/strong&gt;를 통해 효율적으로 해결합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장에서는 페이지 테이블 자체가 너무 커지는 &lt;strong&gt;공간 문제&lt;/strong&gt;를 해결하기 위한 &lt;strong&gt;멀티 레벨 페이지 테이블&lt;/strong&gt;에 대해 알아보겠습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TLB(Translation Lookaside Buffer)&lt;/code&gt;: 가상-물리 주소 변환 정보를 저장하는 하드웨어 캐시.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TLB 히트(Hit)&lt;/code&gt;: 원하는 변환 정보가 TLB에 있는 경우.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TLB 미스(Miss)&lt;/code&gt;: 변환 정보가 TLB에 없어 페이지 테이블을 조회해야 하는 경우.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;공간 지역성(Spatial Locality)&lt;/code&gt;: 참조된 주소와 인접한 주소가 곧 참조될 가능성이 높은 특성.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;시간 지역성(Temporal Locality)&lt;/code&gt;: 최근에 참조된 주소가 다시 참조될 가능성이 높은 특성.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ASID(Address Space ID)&lt;/code&gt;: TLB 내에서 프로세스를 구별하기 위해 사용하는 식별자.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;플러시(Flush)&lt;/code&gt;: TLB의 모든 항목을 무효화(비움)하는 작업.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-tlbs.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 19: Translation Lookaside Buffers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 18. 페이징: 개요 (Paging: Introduction)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-18-vm-paging/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-18-vm-paging/</guid><description>외부 단편화를 해결하기 위한 고정 크기 메모리 관리 기법인 페이징(Paging)을 다룹니다. 가상 주소의 구조(VPN, Offset), 페이지 테이블의 역할과 PTE의 구성 요소, 그리고 페이징이 가져오는 속도와 공간의 문제점을 정리합니다.</description><pubDate>Sun, 25 Jan 2026 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장들에서 우리는 메모리 가상화를 위해 두 가지 접근법을 배웠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;베이스/바운드&lt;/strong&gt;: 주소 공간 전체를 연속적으로 배치 (내부 단편화 발생)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;세그멘테이션(Segmentation)&lt;/strong&gt;: 논리적 단위로 쪼개서 배치 (외부 단편화 발생)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;세그멘테이션(Segmentation)의 가변 크기 할당은 필연적으로 메모리를 조각내어 &lt;strong&gt;외부 단편화&lt;/strong&gt;를 유발합니다. 이를 해결하기 위해 운영체제는 가변 크기 대신 &lt;strong&gt;고정 크기(Fixed-size)&lt;/strong&gt; 단위로 메모리를 관리하는 &lt;strong&gt;페이징(Paging)&lt;/strong&gt; 기법을 도입했습니다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 페이징의 기본 개념과 주소 변환 메커니즘, 그리고 이에 따른 비용을 분석합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 개념: 페이지와 페이지 프레임&lt;/h2&gt;
&lt;p&gt;페이징의 핵심 아이디어는 메모리 공간을 일정하고 고정된 크기로 나누는 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;페이지(Page)&lt;/strong&gt;: 가상 메모리(주소 공간)를 고정 크기로 나눈 조각.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;페이지 프레임(Page Frame)&lt;/strong&gt;: 물리 메모리를 동일한 고정 크기로 나눈 조각(슬롯).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/18_1.png&quot; alt=&quot;Figure 18.1 Simple Address Space&quot; /&gt;
&lt;em&gt;Figure 18.1: 64바이트 가상 주소 공간이 4개의 16바이트 페이지(Page 0~3)로 나뉜 모습.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/18_2.png&quot; alt=&quot;Figure 18.2 Physical Memory with Pages&quot; /&gt;
&lt;em&gt;Figure 18.2: 128바이트 물리 메모리. 가상 페이지들이 순서 없이 물리 프레임(Frame)에 흩어져 배치된 것을 볼 수 있습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;운영체제는 가상 주소 공간의 각 &lt;strong&gt;페이지&lt;/strong&gt;를 물리 메모리의 빈 &lt;strong&gt;페이지 프레임&lt;/strong&gt; 중 아무 곳에나 배치할 수 있습니다. 순서대로 배치할 필요가 없으며, 물리 메모리가 흩어져 있어도 상관없습니다. 모든 프레임의 크기가 같으므로 &lt;strong&gt;외부 단편화가 완벽하게 해결&lt;/strong&gt;됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 주소 변환 (Address Translation)&lt;/h2&gt;
&lt;p&gt;페이징 시스템에서 가상 주소는 두 부분으로 나뉩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;가상 페이지 번호 (VPN: Virtual Page Number)&lt;/strong&gt;: 해당 주소가 몇 번째 페이지에 있는지 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오프셋 (Offset)&lt;/strong&gt;: 페이지 내부에서 데이터가 어디에 위치하는지 나타냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.1 예시: 64바이트 주소 공간, 16바이트 페이지&lt;/h3&gt;
&lt;p&gt;주소 공간이 64바이트($2^6$)이므로 가상 주소는 6비트입니다.
페이지 크기가 16바이트($2^4$)이므로 하위 4비트는 &lt;strong&gt;오프셋&lt;/strong&gt;이고, 나머지 상위 2비트는 &lt;strong&gt;VPN&lt;/strong&gt;이 됩니다.&lt;/p&gt;
&lt;p&gt;예를 들어 가상 주소 &lt;code&gt;21&lt;/code&gt; (이진수 &lt;code&gt;010101&lt;/code&gt;)을 변환해 봅시다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VPN&lt;/strong&gt;: 상위 2비트 &lt;code&gt;01&lt;/code&gt; -&amp;gt; &lt;strong&gt;1번 페이지&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offset&lt;/strong&gt;: 하위 4비트 &lt;code&gt;0101&lt;/code&gt; -&amp;gt; &lt;strong&gt;5번째 바이트&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 가상 페이지 1번의 5번째 바이트를 의미합니다.&lt;/p&gt;
&lt;h3&gt;2.2 물리 주소 계산&lt;/h3&gt;
&lt;p&gt;이제 하드웨어는 VPN(1)을 물리 프레임 번호(&lt;strong&gt;PFN&lt;/strong&gt;: Physical Frame Number)로 바꿔야 합니다. 만약 가상 페이지 1번이 물리 프레임 7번에 있다고 가정하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PFN&lt;/strong&gt;: &lt;code&gt;111&lt;/code&gt; (7번 프레임)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offset&lt;/strong&gt;: &lt;code&gt;0101&lt;/code&gt; (그대로 유지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;물리 주소&lt;/strong&gt;: &lt;code&gt;1110101&lt;/code&gt; (117번지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 변환 정보를 저장하는 곳이 바로 &lt;strong&gt;페이지 테이블(Page Table)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/18_3.png&quot; alt=&quot;Figure 18.3 Address Translation Process&quot; /&gt;
&lt;em&gt;Figure 18.3: 주소 변환 로직. 가상 주소의 VPN(01)을 이용해 PFN(111)을 찾고, 오프셋(0101)과 합쳐 물리 주소를 만드는 과정을 보여줍니다.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 페이지 테이블 (Page Table)&lt;/h2&gt;
&lt;p&gt;페이지 테이블은 &lt;strong&gt;가상 페이지(VPN)를 물리 프레임(PFN)으로 매핑&lt;/strong&gt;하는 자료구조입니다.&lt;/p&gt;
&lt;h3&gt;3.1 어디에 저장되는가?&lt;/h3&gt;
&lt;p&gt;이전의 베이스/바운드 레지스터나 세그먼트 레지스터는 개수가 적어 CPU 내부의 레지스터(MMU)에 저장할 수 있었습니다.
하지만 페이징에서는 페이지 개수가 매우 많기 때문에 레지스터에 담을 수 없습니다. 따라서 페이지 테이블은 &lt;strong&gt;물리 메모리(RAM)&lt;/strong&gt; 에 저장됩니다.&lt;/p&gt;
&lt;h3&gt;3.2 페이지 테이블 항목 (PTE: Page Table Entry)&lt;/h3&gt;
&lt;p&gt;페이지 테이블의 각 항목(PTE)은 PFN뿐만 아니라 시스템 관리를 위한 중요한 비트들을 포함합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Valid Bit&lt;/strong&gt;: 해당 변환이 유효한지(사용 중인 주소 공간인지) 표시. (세그먼트의 바운드 체크와 유사)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protection Bits&lt;/strong&gt;: 읽기/쓰기/실행 권한 표시.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Present Bit&lt;/strong&gt;: 해당 페이지가 물리 메모리에 있는지, 디스크(스왑)에 있는지 표시.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dirty Bit&lt;/strong&gt;: 페이지가 메모리에 올라온 후 수정되었는지 표시.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reference Bit (Accessed Bit)&lt;/strong&gt;: 최근에 접근되었는지 표시 (페이지 교체 알고리즘에서 사용).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 페이징의 문제점: 속도와 공간&lt;/h2&gt;
&lt;p&gt;페이징은 외부 단편화를 해결했지만, 두 가지 심각한 새로운 문제를 야기합니다.&lt;/p&gt;
&lt;h3&gt;4.1 속도 문제 (Too Slow)&lt;/h3&gt;
&lt;p&gt;가상 주소를 물리 주소로 바꾸려면 페이지 테이블을 읽어야 합니다. 그런데 페이지 테이블은 &lt;strong&gt;메모리&lt;/strong&gt;에 있습니다.
즉, 명령어 하나를 실행(&lt;code&gt;Fetch&lt;/code&gt;)하거나 데이터를 로드(&lt;code&gt;Load&lt;/code&gt;)할 때마다, &lt;strong&gt;페이지 테이블을 읽기 위해 추가적인 메모리 접근&lt;/strong&gt;이 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;명령어 인출&lt;/strong&gt;: 메모리 접근 1회 (페이지 테이블) + 메모리 접근 1회 (실제 명령어) = 2회 접근&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 로드&lt;/strong&gt;: 메모리 접근 1회 (페이지 테이블) + 메모리 접근 1회 (실제 데이터) = 2회 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;메모리 접근은 CPU 연산보다 훨씬 느리므로, 시스템 성능이 절반 이하로 떨어질 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4.2 공간 문제 (Too Big)&lt;/h3&gt;
&lt;p&gt;페이지 테이블의 크기가 너무 큽니다.
예를 들어 32비트 주소 공간에서 4KB 페이지를 사용한다고 가정해 봅시다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPN 크기: 20비트 (약 100만 개 페이지)&lt;/li&gt;
&lt;li&gt;PTE 크기: 4바이트&lt;/li&gt;
&lt;li&gt;프로세스당 페이지 테이블 크기: 100만 x 4바이트 = &lt;strong&gt;4MB&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;프로세스가 100개 실행 중이라면 페이지 테이블만으로 &lt;strong&gt;400MB&lt;/strong&gt;의 메모리가 낭비됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;18장에서는 고정 크기 할당 방식인 &lt;strong&gt;페이징(Paging)&lt;/strong&gt; 의 기초를 다루었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: &lt;strong&gt;외부 단편화가 없음&lt;/strong&gt;. 물리 메모리 관리의 유연성 확보.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메커니즘&lt;/strong&gt;: 가상 주소를 &lt;strong&gt;VPN&lt;/strong&gt;과 &lt;strong&gt;Offset&lt;/strong&gt;으로 나누고, &lt;strong&gt;페이지 테이블&lt;/strong&gt;을 통해 VPN을 PFN으로 변환.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;속도 저하&lt;/strong&gt;: 매 메모리 접근마다 페이지 테이블 조회를 위한 추가 메모리 접근 발생.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메모리 낭비&lt;/strong&gt;: 프로세스마다 거대한 페이지 테이블을 유지해야 함.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장부터는 이 두 가지 문제(속도와 공간)를 해결하기 위한 기법인 &lt;strong&gt;TLB(Translation Lookaside Buffer)&lt;/strong&gt; 와 &lt;strong&gt;멀티 레벨 페이지 테이블&lt;/strong&gt;을 차례로 알아볼 것입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;페이지(Page)&lt;/code&gt;: 가상 주소 공간을 고정 크기로 나눈 블록.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;페이지 프레임(Page Frame)&lt;/code&gt;: 물리 메모리를 페이지와 동일한 크기로 나눈 블록.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VPN(Virtual Page Number)&lt;/code&gt;: 가상 주소의 상위 비트로, 페이지 테이블의 인덱스로 사용됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PFN(Physical Frame Number)&lt;/code&gt;: 물리 메모리 상의 프레임 번호.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PTE(Page Table Entry)&lt;/code&gt;: 페이지 테이블의 각 항목으로, PFN과 상태 비트(Valid, Dirty 등)를 포함함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;페이지 테이블(Page Table)&lt;/code&gt;: VPN을 PFN으로 매핑 정보를 저장하는 자료구조.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-paging.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 18: Paging: Introduction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 17. 빈 공간 관리 (Free-Space Management)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-17-vm-freespace/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-17-vm-freespace/</guid><description>외부 단편화 문제를 해결하기 위한 빈 공간 관리 기법을 상세히 다룹니다. 분할(Splitting)과 병합(Coalescing), 헤더(Header)를 통한 크기 추적, 프리 리스트(Free List) 임베딩, 그리고 버디 할당(Buddy Allocation) 등 다양한 전략을 정리합니다.</description><pubDate>Sat, 24 Jan 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 16장에서 세그멘테이션이 가변 크기의 메모리를 할당하면서 &lt;strong&gt;외부 단편화(External Fragmentation)&lt;/strong&gt; 문제를 일으킨다는 점을 확인했습니다. 전체 빈 공간은 충분하지만 연속된 공간이 없어 할당에 실패하는 이 문제는 메모리 관리 시스템의 핵심 과제입니다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 &lt;code&gt;malloc&lt;/code&gt; 라이브러리나 운영체제 커널이 **빈 공간(Free Space)**을 효율적으로 관리하기 위해 사용하는 구체적인 기법과 정책들을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 가정 및 문제 정의&lt;/h2&gt;
&lt;p&gt;우리가 관리해야 할 힙(Heap) 메모리 환경은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인터페이스&lt;/strong&gt;: &lt;code&gt;malloc(size_t size)&lt;/code&gt;로 할당하고 &lt;code&gt;free(void *ptr)&lt;/code&gt;로 해제합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가변 크기&lt;/strong&gt;: 1바이트부터 수 킬로바이트까지 다양한 크기의 요청이 들어옵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;목표&lt;/strong&gt;: 외부 단편화를 최소화하고, 할당/해제 속도를 최적화해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 저수준 메커니즘 (Low-level Mechanisms)&lt;/h2&gt;
&lt;p&gt;대부분의 할당자(Allocator)는 빈 공간 관리를 위해 &lt;strong&gt;분할(Splitting)&lt;/strong&gt; 과 &lt;strong&gt;병합(Coalescing)&lt;/strong&gt; 이라는 기본 메커니즘을 사용합니다.&lt;/p&gt;
&lt;h3&gt;2.1 분할 (Splitting)&lt;/h3&gt;
&lt;p&gt;사용자가 요청한 크기보다 더 큰 빈 덩어리(Chunk)만 존재할 때 사용합니다.
예를 들어 10바이트 빈 공간이 있는데 1바이트 요청이 들어온 경우:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;10바이트 덩어리를 찾습니다.&lt;/li&gt;
&lt;li&gt;이를 1바이트(할당)와 9바이트(빈 공간)로 &lt;strong&gt;나눕니다(Split)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;1바이트는 사용자에게 반환하고, 나머지 9바이트는 빈 공간 리스트(Free List)에 유지합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 병합 (Coalescing)&lt;/h3&gt;
&lt;p&gt;메모리가 해제되었을 때, 단순히 리스트에 추가하기만 하면 작은 조각들이 흩어지게 됩니다. 이를 방지하기 위해 병합을 수행합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;메모리가 해제될 때(&lt;code&gt;free&lt;/code&gt;), 해제되는 영역 바로 옆에 빈 공간이 있는지 확인합니다.&lt;/li&gt;
&lt;li&gt;인접한 빈 공간이 있다면 하나의 큰 덩어리로 &lt;strong&gt;합칩니다(Coalesce)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 과정이 없다면 힙은 시간이 지날수록 작은 조각들로 파편화되어 큰 요청을 처리할 수 없게 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 할당된 크기 추적 (Tracking The Size)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;free(void *ptr)&lt;/code&gt; 인터페이스는 포인터만 받을 뿐, &lt;strong&gt;해제할 크기&lt;/strong&gt;를 인자로 받지 않습니다. 메모리 할당 라이브러리는 어떻게 해제해야 할 크기를 알 수 있을까요?&lt;/p&gt;
&lt;p&gt;대부분의 할당자는 &lt;strong&gt;헤더(Header)&lt;/strong&gt; 라는 메타데이터를 할당된 메모리 바로 앞부분에 저장합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/17_1.png&quot; alt=&quot;Figure 17.1 Allocated Region with Header&quot; /&gt;
&lt;em&gt;Figure 17.1: 할당된 영역의 구조. ptr은 사용자가 사용하는 공간을 가리키며, 그 바로 앞(hptr)에 크기 정보를 담은 헤더가 위치합니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;헤더 구조체의 예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
 int size;
 int magic;
} header_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용자가 &lt;code&gt;malloc(N)&lt;/code&gt;을 호출하면, 라이브러리는 &lt;code&gt;N + sizeof(header_t)&lt;/code&gt; 크기의 공간을 찾습니다. 사용자는 &lt;code&gt;ptr&lt;/code&gt;을 받지만, 라이브러리는 &lt;code&gt;free(ptr)&lt;/code&gt; 호출 시 포인터 연산을 통해 헤더의 위치를 찾아내고 크기(&lt;code&gt;size&lt;/code&gt;)를 확인하여 안전하게 해제합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 프리 리스트 임베딩 (Embedding A Free List)&lt;/h2&gt;
&lt;p&gt;빈 공간을 관리하기 위한 리스트(Linked List) 노드는 어디에 저장할까요? 메모리 관리를 위한 추가 메모리를 또 &lt;code&gt;malloc&lt;/code&gt;으로 할당할 수는 없습니다.&lt;/p&gt;
&lt;p&gt;따라서 할당자는 &lt;strong&gt;빈 공간 그 자체&lt;/strong&gt;에 리스트 노드를 저장합니다. 이를 &lt;strong&gt;임베딩(Embedding)&lt;/strong&gt; 이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/17_3.png&quot; alt=&quot;Figure 17.3 Heap with Free List&quot; /&gt;
&lt;em&gt;Figure 17.3: 빈 공간 내부에 프리 리스트가 임베딩된 모습. 4KB 힙 안에 하나의 거대한 빈 덩어리가 있고, 그 안에 size와 next 포인터가 저장되어 있습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;빈 공간의 시작 부분에 &lt;code&gt;next&lt;/code&gt; 포인터와 &lt;code&gt;size&lt;/code&gt; 정보를 기록하여, 빈 덩어리들을 연결 리스트로 관리합니다. 메모리가 할당되면 이 공간은 사용자 데이터로 덮어쓰여지고, 해제되면 다시 리스트 노드로 활용됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 기본 할당 전략 (Basic Strategies)&lt;/h2&gt;
&lt;p&gt;빈 공간 리스트에서 적절한 덩어리를 찾는 정책(Policy)은 다양합니다.&lt;/p&gt;
&lt;h3&gt;5.1 Best Fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;리스트 전체를 검색하여 요청 크기보다 크거나 같은 것 중 &lt;strong&gt;가장 작은&lt;/strong&gt; 덩어리를 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 공간 낭비를 최소화합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 전체를 탐색해야 하므로 성능 오버헤드가 큽니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 Worst Fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;리스트 전체를 검색하여 &lt;strong&gt;가장 큰&lt;/strong&gt; 덩어리를 선택하고 분할합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의도&lt;/strong&gt;: 남은 조각도 충분히 크도록 하여 활용도를 높이려는 의도입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;현실&lt;/strong&gt;: 전체 탐색 비용이 들고, 실험적으로 단편화를 오히려 심화시키는 경향이 있어 잘 사용되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 First Fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;리스트를 처음부터 검색하여 &lt;strong&gt;가장 먼저 발견된&lt;/strong&gt; 충분한 크기의 덩어리를 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 탐색 속도가 빠릅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 리스트의 앞부분에 작은 조각(Splinters)들이 몰리는 현상이 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.4 Next Fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;First Fit과 유사하지만, &lt;strong&gt;마지막으로 할당한 위치&lt;/strong&gt;에서부터 검색을 시작합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 리스트 전체에 빈 공간 분산을 유도하여 First Fit의 앞부분 쏠림 현상을 완화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 고급 접근법 (Other Approaches)&lt;/h2&gt;
&lt;h3&gt;6.1 분리 가용 리스트 (Segregated Lists)&lt;/h3&gt;
&lt;p&gt;자주 할당되는 특정 크기(예: 32바이트, 64바이트)의 객체들을 위한 전용 리스트를 별도로 유지합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 크기가 고정되어 있어 단편화가 발생하지 않으며 할당 속도가 매우 빠릅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예시&lt;/strong&gt;: 솔라리스의 &lt;strong&gt;Slab Allocator&lt;/strong&gt;가 대표적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 버디 할당 (Buddy Allocation)&lt;/h3&gt;
&lt;p&gt;메모리를 $2^N$ 크기로 관리하며, 할당 시 큰 덩어리를 절반씩($2^N \rightarrow 2^{N-1}$) 계속 **이분(Binary Splitting)**하여 할당합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/17_8.png&quot; alt=&quot;Figure 17.8 Buddy Managed Heap&quot; /&gt;
&lt;em&gt;Figure 17.8: 64KB 공간에서 7KB 요청을 처리하기 위해 8KB 블록까지 분할하는 과정. 해제 시에는 인접한 &apos;버디&apos;가 비어있다면 즉시 상위 블록으로 병합됩니다.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 해제 시 주소 연산만으로 인접한 &lt;strong&gt;버디(Buddy)&lt;/strong&gt; 블록을 찾아 쉽게 &lt;strong&gt;병합&lt;/strong&gt;할 수 있습니다. 속도가 빠릅니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: $2^N$ 단위로만 할당하므로 내부 단편화가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;17장에서는 메모리 할당자가 빈 공간을 관리하는 구체적인 기법을 다루었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;핵심 기술&lt;/strong&gt;: 분할과 병합, 헤더를 이용한 정보 추적, 프리 리스트 임베딩.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;할당 정책&lt;/strong&gt;: Best Fit, First Fit 등 상황에 맞는 전략 선택이 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고급 기법&lt;/strong&gt;: 성능과 효율성을 위해 Slab Allocator나 Buddy System 같은 고도화된 알고리즘이 실제 OS와 라이브러리에서 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장에서는 가상 메모리 시스템의 완성형이라 할 수 있는 &lt;strong&gt;페이징(Paging)&lt;/strong&gt; 기법에 대해 본격적으로 알아봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;외부 단편화(External Fragmentation)&lt;/code&gt;: 빈 공간의 총합은 충분하나 연속되지 않아 할당할 수 없는 상태.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;분할(Splitting)&lt;/code&gt;: 큰 빈 공간을 요청된 크기만큼 자르고 나머지를 빈 공간으로 유지하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;병합(Coalescing)&lt;/code&gt;: 인접한 빈 공간들을 합쳐 더 큰 빈 공간을 만드는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;헤더(Header)&lt;/code&gt;: 할당된 메모리 블록 앞에 메타데이터(크기 등)를 저장하는 영역.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;임베딩(Embedding)&lt;/code&gt;: 메모리 관리를 위한 자료구조(리스트)를 빈 공간 내부에 저장하는 방식.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;버디 할당(Buddy Allocation)&lt;/code&gt;: 메모리를 2의 거듭제곱 단위로 분할 및 병합하여 관리하는 기법.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-freespace.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 17: Free-Space Management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 16. 세그멘테이션 (Segmentation)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-16-vm-segmentation/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-16-vm-segmentation/</guid><description>내부 단편화를 해결하기 위한 세그멘테이션 기법을 상세히 정리합니다. 주소 변환 하드웨어의 동작 원리, 스택의 음수 방향 성장 처리, 그리고 새로운 문제점인 외부 단편화(External Fragmentation)와 그 해결책을 다룹니다.</description><pubDate>Sat, 24 Jan 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 장에서 다룬 &lt;strong&gt;베이스와 바운드(Base and Bounds)&lt;/strong&gt; 방식은 물리 메모리의 연속된 공간에 프로세스의 전체 주소 공간을 배치하는 방식이었습니다. 하지만 이 방식은 치명적인 비효율성을 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;일반적인 프로세스 주소 공간을 보면, &lt;strong&gt;스택(Stack)&lt;/strong&gt; 과 &lt;strong&gt;힙(Heap)&lt;/strong&gt; 사이에는 사용되지 않는 빈 공간(Free Space)이 존재합니다. 베이스/바운드 방식은 이 빈 공간까지 포함하여 물리 메모리를 할당해야 하므로, 메모리 낭비가 심각합니다. 이를 &lt;strong&gt;내부 단편화(Internal Fragmentation)&lt;/strong&gt; 라고 합니다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 이 문제를 해결하기 위해 주소 공간을 논리적인 단위로 쪼개서 관리하는 &lt;strong&gt;세그멘테이션(Segmentation)&lt;/strong&gt; 기법을 알아봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 세그멘테이션: 베이스/바운드의 일반화&lt;/h2&gt;
&lt;p&gt;세그멘테이션의 핵심 아이디어는 &lt;strong&gt;베이스/바운드 쌍을 논리적 세그먼트(Segment)마다 따로 두는 것&lt;/strong&gt;입니다. 이를 통해 주소 공간 전체가 아닌, 실제로 사용 중인 부분만 물리 메모리에 매핑할 수 있습니다.&lt;/p&gt;
&lt;p&gt;일반적인 주소 공간은 다음 3개의 논리적 세그먼트로 나뉩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;코드(Code)&lt;/strong&gt;: 명령어 집합 (읽기 전용)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;힙(Heap)&lt;/strong&gt;: 동적 데이터 (아래로 성장)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스택(Stack)&lt;/strong&gt;: 지역 변수 및 함수 컨텍스트 (위로 성장)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;운영체제는 이 세그먼트들을 물리 메모리의 각기 다른 위치에 독립적으로 배치합니다. 결과적으로 스택과 힙 사이의 빈 공간은 물리 메모리를 차지하지 않게 되어 메모리 효율이 획기적으로 개선됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/16_2.png&quot; alt=&quot;Figure 16.2 Segments in Physical Memory&quot; /&gt;
&lt;em&gt;Figure 16.2: 물리 메모리에 배치된 세그먼트들. 논리적으로 연속된 주소 공간이 물리 메모리에서는 불연속적으로, 필요한 크기만큼만 할당되어 있다.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 주소 변환 메커니즘 (Address Translation)&lt;/h2&gt;
&lt;p&gt;세그멘테이션 환경에서 하드웨어는 가상 주소를 어떻게 물리 주소로 변환할까요? 이제 가상 주소는 &lt;strong&gt;세그먼트 식별자&lt;/strong&gt;와 &lt;strong&gt;오프셋&lt;/strong&gt;으로 해석됩니다.&lt;/p&gt;
&lt;h3&gt;2.1 세그먼트 식별 (Explicit Approach)&lt;/h3&gt;
&lt;p&gt;가장 일반적인 방법은 가상 주소의 &lt;strong&gt;최상위 비트(Top bits)&lt;/strong&gt; 를 세그먼트 번호로 사용하는 것입니다.
예를 들어 14비트 가상 주소 공간에서 상위 2비트를 식별자로 사용한다면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;00&lt;/code&gt;: 코드 세그먼트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;01&lt;/code&gt;: 힙 세그먼트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10&lt;/code&gt;: (미사용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;11&lt;/code&gt;: 스택 세그먼트&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;나머지 하위 12비트는 해당 세그먼트 내부에서의 &lt;strong&gt;오프셋(Offset)&lt;/strong&gt; 이 됩니다.&lt;/p&gt;
&lt;h3&gt;2.2 주소 변환 과정&lt;/h3&gt;
&lt;p&gt;하드웨어는 다음과 같은 로직으로 물리 주소를 계산합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;세그먼트 추출&lt;/strong&gt;: 가상 주소의 상위 비트를 읽어 세그먼트 번호를 얻습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오프셋 추출&lt;/strong&gt;: 나머지 하위 비트를 오프셋으로 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;범위 검사 (Protection)&lt;/strong&gt;: 오프셋이 해당 세그먼트의 &lt;code&gt;Bounds&lt;/code&gt;보다 작은지 확인합니다. 범위를 벗어나면 &lt;code&gt;Segmentation Fault&lt;/code&gt;를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;물리 주소 계산&lt;/strong&gt;: &lt;code&gt;Base[Segment] + Offset&lt;/code&gt;을 통해 최종 물리 주소를 얻습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 스택(Stack)의 처리: 음수 방향 성장&lt;/h2&gt;
&lt;p&gt;스택은 다른 세그먼트와 달리 &lt;strong&gt;높은 주소에서 낮은 주소로(Backwards)&lt;/strong&gt; 자라납니다. 이를 지원하기 위해 하드웨어는 추가적인 정보가 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;성장 방향 비트 (Grows Positive?)&lt;/strong&gt;: 1이면 양의 방향(힙, 코드), 0이면 음의 방향(스택)으로 성장함을 의미합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스택의 경우 물리 주소 변환 방식이 조금 다릅니다. 스택은 세그먼트의 끝에서부터 거꾸로 자라기 때문에, 오프셋을 &lt;strong&gt;음수(Negative Offset)&lt;/strong&gt; 로 계산해야 합니다. 이때 스택의 &lt;strong&gt;베이스 레지스터&lt;/strong&gt;는 세그먼트의 시작이 아닌 &lt;strong&gt;물리적 끝(높은 주소)&lt;/strong&gt; 을 가리키게 됩니다.&lt;/p&gt;
&lt;p&gt;$$\text{Offset} = \text{VirtualAddress\ Offset} - \text{Max\ Segment\ Size}$$
$$\text{PhysicalAddress} = \text{Base} + \text{Offset}$$&lt;/p&gt;
&lt;p&gt;예를 들어 4KB 크기의 스택 세그먼트에서 3KB 지점에 접근한다면, 오프셋은 $3KB - 4KB = -1KB$가 되며, 이를 베이스 주소(높은 주소)에서 뺍니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 공유와 보호 (Sharing &amp;amp; Protection)&lt;/h2&gt;
&lt;p&gt;세그멘테이션은 메모리 효율성뿐만 아니라 &lt;strong&gt;공유(Sharing)&lt;/strong&gt; 의 이점도 제공합니다.
특히 &lt;strong&gt;코드 세그먼트&lt;/strong&gt;는 읽기 전용(Read-Only)이므로, 여러 프로세스가 동일한 물리 메모리상의 코드를 공유하여 메모리를 절약할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 위해 하드웨어는 세그먼트 레지스터에 &lt;strong&gt;보호 비트(Protection Bits)&lt;/strong&gt; 를 추가합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-Execute&lt;/strong&gt;: 코드 세그먼트 (공유 가능)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read-Write&lt;/strong&gt;: 힙, 스택 세그먼트 (프로세스별 전용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;만약 &lt;strong&gt;읽기 전용&lt;/strong&gt;인 코드 세그먼트에 쓰기(Store)를 시도하면, 하드웨어는 보호 오류를 발생시켜 OS에게 알립니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 세분화(Fine-grained) vs 광역(Coarse-grained)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;광역 세그멘테이션 (Coarse-grained)&lt;/strong&gt;: 우리가 지금까지 살펴본 것처럼 주소 공간을 코드, 힙, 스택 등 큼직한 몇 개의 단위로 나누는 방식입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;세분화 세그멘테이션 (Fine-grained)&lt;/strong&gt;: 초기 시스템(예: Burroughs B5000)처럼 주소 공간을 매우 잘게 쪼개어 수많은 세그먼트로 관리하는 방식입니다. 이 경우 세그먼트 정보를 레지스터가 아닌 메모리상의 &lt;strong&gt;세그먼트 테이블&lt;/strong&gt;에 저장해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 운영체제의 역할과 새로운 문제&lt;/h2&gt;
&lt;p&gt;세그멘테이션은 내부 단편화 문제를 해결했지만, &lt;strong&gt;외부 단편화(External Fragmentation)&lt;/strong&gt; 라는 새로운 난제를 가져왔습니다.&lt;/p&gt;
&lt;h3&gt;6.1 외부 단편화 (External Fragmentation)&lt;/h3&gt;
&lt;p&gt;세그먼트들의 크기는 고정되어 있지 않고 가변적(Variable-sized)입니다. 프로세스가 생성되고 종료됨에 따라 물리 메모리에는 서로 다른 크기의 세그먼트들이 할당되고 해제됩니다. 이 과정에서 &lt;strong&gt;작은 빈 공간(Hole)&lt;/strong&gt; 들이 메모리 곳곳에 흩어지게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/16_6.png&quot; alt=&quot;Figure 16.6 Non-compacted and Compacted Memory&quot; /&gt;
&lt;em&gt;Figure 16.6: (좌) 외부 단편화 발생. 총 빈 공간은 24KB지만 연속되지 않아 20KB를 할당할 수 없음. (우) 압축(Compaction) 후 할당 가능한 상태.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;전체 빈 메모리의 합은 충분하더라도, 연속된 공간이 부족하여 배치를 못 하는 현상이 발생합니다.&lt;/p&gt;
&lt;h3&gt;6.2 해결책&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;압축 (Compaction)&lt;/strong&gt;: 흩어진 세그먼트들을 한쪽으로 몰아 큰 빈 공간을 확보합니다. 하지만 메모리 복사 비용이 매우 비싸 시스템 성능에 부담을 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;빈 공간 관리 알고리즘&lt;/strong&gt;: &lt;code&gt;Best-fit&lt;/code&gt;, &lt;code&gt;First-fit&lt;/code&gt;, &lt;code&gt;Buddy Algorithm&lt;/code&gt; 등의 기법을 사용하여 리스트를 효율적으로 관리해 단편화를 최소화합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;세그멘테이션은 &lt;strong&gt;드문드문한(Sparse) 주소 공간&lt;/strong&gt;을 효율적으로 지원하기 위해 도입되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 내부 단편화 해결, 코드 공유 용이, 동적 스택/힙 성장 지원.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 가변 크기 할당으로 인한 &lt;strong&gt;외부 단편화&lt;/strong&gt; 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 가변 크기의 세그먼트를 관리하는 것은 근본적으로 복잡합니다. 그래서 현대 OS는 메모리를 고정 크기 단위로 관리하는 &lt;strong&gt;페이징(Paging)&lt;/strong&gt; 기법을 사용하게 됩니다. 다음 장에서 그 내용을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;세그멘테이션(Segmentation)&lt;/code&gt;: 주소 공간을 논리적 단위(코드, 힙, 스택)로 나누어 물리 메모리에 비연속적으로 할당하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;내부 단편화(Internal Fragmentation)&lt;/code&gt;: 할당된 공간 내부에서 사용되지 않고 낭비되는 메모리.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;외부 단편화(External Fragmentation)&lt;/code&gt;: 전체 빈 공간은 충분하지만, 연속되지 않아서 할당할 수 없는 상태.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;보호 비트(Protection Bits)&lt;/code&gt;: 세그먼트별로 읽기/쓰기/실행 권한을 설정하여 불법적인 접근을 막는 비트.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;압축(Compaction)&lt;/code&gt;: 메모리 단편화를 해결하기 위해 사용 중인 메모리 블록들을 이동시켜 빈 공간을 합치는 작업.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-segmentation.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 16: Segmentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[STM32MP25] BSP I/O 데모: 버튼 → BME280 → HTTP (Yocto/systemd)</title><link>https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/stm32-bsp-io-demo/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/stm32-bsp-io-demo/</guid><description>STM32MP25(OpenSTLinux)에서 gpio-keys 버튼 이벤트로 BME280(IIO) 값을 읽어 HTTP로 전송하는 userspace 데모를 만들고, Yocto(meta-myboard) 레이어로 패키징해 systemd 서비스로 자동 실행까지 구성합니다.</description><pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;임베디드 I/O 기초: GPIO/I2C/SPI/UART 실습의 형태로, &lt;strong&gt;보드에서 이벤트가 발생하면 센서값을 읽어 서버로 전송하는 최소 경로(DoD)&lt;/strong&gt; 를 구현해 봤습니다.&lt;/p&gt;
&lt;p&gt;저는 아직 외부 LED/버튼 부품이 없어서, &lt;strong&gt;보드에 기본으로 탑재된 버튼(gpio-keys → /dev/input/eventX)&lt;/strong&gt; 과 &lt;strong&gt;기본 LED(/sys/class/leds)&lt;/strong&gt; 를 활용했습니다. I2C 센서는 앞서 bring-up 한 &lt;strong&gt;BME280(IIO sysfs)&lt;/strong&gt; 를 그대로 재사용합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;구성 개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;입력(GPIO): 보드 기본 버튼 → Linux input subsystem(&lt;code&gt;/dev/input/event0&lt;/code&gt;) 이벤트 감지&lt;/li&gt;
&lt;li&gt;센서(I2C): BME280 → IIO sysfs(&lt;code&gt;in_temp_input&lt;/code&gt;, &lt;code&gt;in_humidityrelative_input&lt;/code&gt;, &lt;code&gt;in_pressure_input&lt;/code&gt;) 값 읽기&lt;/li&gt;
&lt;li&gt;네트워크: userspace에서 HTTP POST(JSON)로 서버 전송&lt;/li&gt;
&lt;li&gt;서비스: systemd로 부팅 시 자동 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;구성 흐름은 아래 한 줄로 요약됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;button(gpio-keys) → bme280(IIO) → HTTP POST(JSON)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;전제 조건&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenSTLinux 부팅 완료, root 쉘 접근 가능&lt;/li&gt;
&lt;li&gt;BME280 bring-up 완료(IIO sysfs에서 값 읽기 가능)&lt;/li&gt;
&lt;li&gt;보드가 서버(PC)와 같은 L2/L3 망에 있고, HTTP 포트가 열려 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 버튼(gpio-keys) / LED 리소스 확인&lt;/h2&gt;
&lt;h3&gt;1.1 gpio-keys가 어떤 input event로 잡혔는지 확인&lt;/h3&gt;
&lt;p&gt;보드에 기본으로 올라간 버튼은 보통 &lt;code&gt;gpio-keys&lt;/code&gt;로 구성되어 있고, 결과적으로 Linux에서는 &lt;strong&gt;키보드 이벤트처럼&lt;/strong&gt; &lt;code&gt;/dev/input/eventX&lt;/code&gt;로 노출됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /proc/bus/input/devices | sed -n &apos;/gpio-keys/,+25p&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래처럼 &lt;code&gt;Name=&quot;gpio-keys&quot;&lt;/code&gt;, &lt;code&gt;Handlers=kbd event0&lt;/code&gt;가 보이면, 이 보드에서는 버튼 이벤트가 &lt;code&gt;/dev/input/event0&lt;/code&gt;으로 들어온다는 의미입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/02-gpio-keys.png&quot; alt=&quot;gpio-keys input device&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::TIP
&lt;code&gt;evtest /dev/input/event0&lt;/code&gt;(패키지 설치 필요)로도 쉽게 키 코드가 확인됩니다.
:::&lt;/p&gt;
&lt;h3&gt;1.2 LED class 디바이스 확인 및 수동 제어&lt;/h3&gt;
&lt;p&gt;보드 기본 LED는 &lt;code&gt;/sys/class/leds/&lt;/code&gt; 아래에 노출됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -1 /sys/class/leds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 &lt;code&gt;blue:heartbeat&lt;/code&gt; 같은 이름이 보이면, 아래처럼 &lt;strong&gt;트리거를 끄고(=none) 밝기를 직접 제어&lt;/strong&gt;할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LED=blue:heartbeat

# 현재 trigger 확인
cat /sys/class/leds/${LED}/trigger

# 트리거를 none으로 바꾸면 brightness로 직접 제어 가능
echo none &amp;gt; /sys/class/leds/${LED}/trigger

# ON/OFF
echo 1 &amp;gt; /sys/class/leds/${LED}/brightness
sleep 1
echo 0 &amp;gt; /sys/class/leds/${LED}/brightness
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 서버(PC) 준비: 최소 수신기(recv.py)&lt;/h2&gt;
&lt;p&gt;저는 PC에서 간단히 Python HTTP 서버로 &lt;code&gt;/ingest&lt;/code&gt;를 받아 로그로 찍는 방식으로 시작했습니다.&lt;/p&gt;
&lt;h3&gt;2.1 ping이 안 되는데 HTTP가 되는 경우&lt;/h3&gt;
&lt;p&gt;처음에 서버 IP로 ping이 안 간다에서 멈출 수 있는데, Windows 방화벽 정책 등에 따라 &lt;strong&gt;ICMP Echo(ping)만 막고 TCP는 허용&lt;/strong&gt;되는 구성이 흔합니다.&lt;/p&gt;
&lt;p&gt;실제로 보드에서 라우팅은 정상으로 잡혀 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/03-ip-route-get.png&quot; alt=&quot;ip route get&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그리고 TCP 레벨에서는 연결까지는 되었지만, 서버가 예외로 죽어서 &lt;code&gt;Empty reply from server&lt;/code&gt;가 발생하는 상태였습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/04-curl-empty-reply.png&quot; alt=&quot;curl empty reply&quot; /&gt;&lt;/p&gt;
&lt;p&gt;따라서 이 단계에서는 ping 유무보다 &lt;strong&gt;HTTP 요청이 정상 응답(예: 204/200)을 받는지&lt;/strong&gt;를 먼저 확인하는 편이 작업이 빠릅니다.&lt;/p&gt;
&lt;h3&gt;2.2 수신 서버 코드(예외 수정 포함)&lt;/h3&gt;
&lt;p&gt;아래는 제가 사용한 최소 수신 서버입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# recv.py

from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        n = int(self.headers.get(&apos;Content-Length&apos;, &apos;0&apos;))
        body = self.rfile.read(n).decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)

        ts = datetime.now().isoformat(timespec=&apos;microseconds&apos;)
        print(f&quot;{ts} {self.client_address[0]} {self.path} {body}&quot;)

        # 204 No Content
        self.send_response(204)
        self.end_headers()

    def log_message(self, fmt, *args):
        # 기본 access log를 끄고 싶으면 pass
        return

if __name__ == &apos;__main__&apos;:
    addr = (&apos;0.0.0.0&apos;, 18000)
    print(f&quot;listening on http://{addr[0]}:{addr[1]}/ingest&quot;)
    HTTPServer(addr, Handler).serve_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python recv.py
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. userspace 앱(bsp-io-demo): 버튼 이벤트로 BME280 읽고 HTTP POST&lt;/h2&gt;
&lt;h3&gt;3.1 동작 요구사항(DoD)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;버튼 입력 발생 → BME280 센서값 1회 읽기&lt;/li&gt;
&lt;li&gt;JSON payload 구성 → 서버 &lt;code&gt;/ingest&lt;/code&gt;로 HTTP POST&lt;/li&gt;
&lt;li&gt;성공/실패를 journal 로그로 남기기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 참고 구현(단일 C 파일)&lt;/h3&gt;
&lt;p&gt;아래는 최소 동작을 목표로 한 단일 파일 구현 예시입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;입력: &lt;code&gt;/dev/input/event0&lt;/code&gt;에서 &lt;code&gt;struct input_event&lt;/code&gt; 읽기&lt;/li&gt;
&lt;li&gt;센서: IIO sysfs 파일을 직접 읽기(값 없으면 error payload)&lt;/li&gt;
&lt;li&gt;전송: libcurl로 HTTP POST&lt;/li&gt;
&lt;li&gt;설정: &lt;code&gt;SERVER_URL&lt;/code&gt;, &lt;code&gt;INPUT_EVENT&lt;/code&gt;, &lt;code&gt;LED_NAME&lt;/code&gt; 환경변수로 런타임 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// bsp-io-demo.c

#define _GNU_SOURCE
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;linux/input.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdarg.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;time.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#include &amp;lt;curl/curl.h&amp;gt;

static volatile int g_stop = 0;

static void on_sigint(int sig) {
    (void)sig;
    g_stop = 1;
}

static void logi(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    printf(&quot;[INFO] &quot;);
    vprintf(fmt, ap);
    printf(&quot;\n&quot;);
    fflush(stdout);
    va_end(ap);
}

static void logw(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    printf(&quot;[WARN] &quot;);
    vprintf(fmt, ap);
    printf(&quot;\n&quot;);
    fflush(stdout);
    va_end(ap);
}

static int read_file_to_buf(const char *path, char *buf, size_t buflen) {
    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd &amp;lt; 0) return -1;
    ssize_t n = read(fd, buf, buflen - 1);
    close(fd);
    if (n &amp;lt;= 0) return -1;
    buf[n] = &apos;\0&apos;;
    // trim newline
    char *p = strchr(buf, &apos;\n&apos;);
    if (p) *p = &apos;\0&apos;;
    return 0;
}

static int write_str(const char *path, const char *s) {
    int fd = open(path, O_WRONLY | O_CLOEXEC);
    if (fd &amp;lt; 0) return -1;
    ssize_t n = write(fd, s, strlen(s));
    close(fd);
    return (n == (ssize_t)strlen(s)) ? 0 : -1;
}

static void led_pulse_once(const char *led_name) {
    if (!led_name || !*led_name) return;

    char trig[256];
    char bri[256];
    snprintf(trig, sizeof(trig), &quot;/sys/class/leds/%s/trigger&quot;, led_name);
    snprintf(bri, sizeof(bri), &quot;/sys/class/leds/%s/brightness&quot;, led_name);

    // best-effort: disable trigger and pulse
    (void)write_str(trig, &quot;none&quot;);
    (void)write_str(bri, &quot;1&quot;);
    usleep(80 * 1000);
    (void)write_str(bri, &quot;0&quot;);
}

static int find_bme280_iio(char *out_dir, size_t out_len) {
    // 아주 단순하게 iio:device0~9를 훑어보고 name==bme280인지 확인
    for (int i = 0; i &amp;lt; 10; i++) {
        char namep[128];
        char name[64];
        snprintf(namep, sizeof(namep), &quot;/sys/bus/iio/devices/iio:device%d/name&quot;, i);
        if (read_file_to_buf(namep, name, sizeof(name)) == 0) {
            if (strcmp(name, &quot;bme280&quot;) == 0 || strcmp(name, &quot;bmp280&quot;) == 0) {
                snprintf(out_dir, out_len, &quot;/sys/bus/iio/devices/iio:device%d&quot;, i);
                return 0;
            }
        }
    }
    return -1;
}

static int read_bme280_raw(long *temp_raw, long *hum_raw, double *press_kpa) {
    char base[128];
    if (find_bme280_iio(base, sizeof(base)) &amp;lt; 0) {
        return -1;
    }

    char p_temp[256], p_hum[256], p_press[256];
    char b[64];

    snprintf(p_temp, sizeof(p_temp), &quot;%s/in_temp_input&quot;, base);
    snprintf(p_hum, sizeof(p_hum), &quot;%s/in_humidityrelative_input&quot;, base);
    snprintf(p_press, sizeof(p_press), &quot;%s/in_pressure_input&quot;, base);

    if (read_file_to_buf(p_temp, b, sizeof(b)) &amp;lt; 0) return -1;
    *temp_raw = strtol(b, NULL, 10);

    if (read_file_to_buf(p_hum, b, sizeof(b)) &amp;lt; 0) return -1;
    *hum_raw = strtol(b, NULL, 10);

    if (read_file_to_buf(p_press, b, sizeof(b)) &amp;lt; 0) return -1;
    long press_pa = strtol(b, NULL, 10);

    // Pa -&amp;gt; kPa (102739 Pa -&amp;gt; 102.739 kPa)
    *press_kpa = ((double)press_pa) / 1000.0;
    return 0;
}

static size_t discard_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
    (void)ptr; (void)userdata;
    return size * nmemb;
}

static int http_post_json(const char *url, const char *json) {
    CURL *curl = curl_easy_init();
    if (!curl) return -1;

    struct curl_slist *hdrs = NULL;
    hdrs = curl_slist_append(hdrs, &quot;Content-Type: application/json&quot;);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard_cb);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L);

    CURLcode rc = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &amp;amp;http_code);

    curl_slist_free_all(hdrs);
    curl_easy_cleanup(curl);

    if (rc != CURLE_OK) {
        return -1;
    }
    if (http_code &amp;lt; 200 || http_code &amp;gt;= 300) {
        return -1;
    }
    return 0;
}

int main(void) {
    signal(SIGINT, on_sigint);
    signal(SIGTERM, on_sigint);

    const char *server_url = getenv(&quot;SERVER_URL&quot;);
    const char *input_event = getenv(&quot;INPUT_EVENT&quot;);
    const char *led_name = getenv(&quot;LED_NAME&quot;);

    if (!server_url) server_url = &quot;http://192.168.45.231:18000/ingest&quot;;
    if (!input_event) input_event = &quot;/dev/input/event0&quot;;
    if (!led_name) led_name = &quot;blue:heartbeat&quot;;

    logi(&quot;SERVER_URL=%s&quot;, server_url);
    logi(&quot;LED_NAME=%s&quot;, led_name);
    logi(&quot;INPUT_EVENT=%s&quot;, input_event);

    if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) {
        logw(&quot;curl_global_init failed&quot;);
        return 1;
    }

    int fd = open(input_event, O_RDONLY | O_CLOEXEC);
    if (fd &amp;lt; 0) {
        logw(&quot;open(%s) failed: %s&quot;, input_event, strerror(errno));
        curl_global_cleanup();
        return 1;
    }

    while (!g_stop) {
        struct input_event ev;
        ssize_t n = read(fd, &amp;amp;ev, sizeof(ev));
        if (n &amp;lt; 0) {
            if (errno == EINTR) continue;
            logw(&quot;read input_event failed: %s&quot;, strerror(errno));
            break;
        }
        if (n != (ssize_t)sizeof(ev)) continue;

        if (ev.type == EV_KEY &amp;amp;&amp;amp; ev.value == 1) { // key press
            long t = 0, h = 0;
            double p = 0.0;

            char payload[256];
            if (read_bme280_raw(&amp;amp;t, &amp;amp;h, &amp;amp;p) &amp;lt; 0) {
                logw(&quot;sensor read failed; sending empty payload&quot;);
                snprintf(payload, sizeof(payload),
                         &quot;{\&quot;sensor\&quot;:\&quot;bme280\&quot;,\&quot;error\&quot;:\&quot;read_failed\&quot;}&quot;);
            } else {
                snprintf(payload, sizeof(payload),
                         &quot;{\&quot;sensor\&quot;:\&quot;bme280\&quot;,\&quot;temp_raw\&quot;:\&quot;%ld\&quot;,\&quot;hum_raw\&quot;:\&quot;%ld\&quot;,\&quot;press_raw\&quot;:\&quot;%.9f\&quot;}&quot;,
                         t, h, p);
            }

            logi(&quot;key_press code=%u -&amp;gt; POST %s payload=%s&quot;, ev.code, server_url, payload);
            led_pulse_once(led_name);

            int ok = http_post_json(server_url, payload);
            logi(&quot;post_result=%s&quot;, ok == 0 ? &quot;OK&quot; : &quot;FAIL&quot;);
        }
    }

    close(fd);
    curl_global_cleanup();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Yocto(meta-myboard)로 패키징: 이미지 포함 + systemd 자동 실행&lt;/h2&gt;
&lt;p&gt;이 글에서는 커널/DT를 건드리지 않고, &lt;strong&gt;userspace 앱을 레이어에서 패키징해서 이미지에 포함&lt;/strong&gt;시키는 쪽에 집중합니다.&lt;/p&gt;
&lt;h3&gt;4.1 레이어 파일 배치(예시)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;meta-myboard&lt;/code&gt;에 아래처럼 배치하는 구성을 기준으로 합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.io/pingu52/stm32mp257f-bsp-lab&quot;&gt;github stm32mp257f-bsp-lab&lt;/a&gt;를 기반으로 작성하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;meta-myboard/
  recipes-apps/
    bsp-io-demo/
      files/
        bsp-io-demo.c
        bsp-io-demo.service
        bsp-io-demo.conf
      bsp-io-demo_1.0.bb
  recipes-st/images/
    st-image-weston.bbappend
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 레시피(bsp-io-demo_1.0.bb)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LIC_FILES_CHKSUM&lt;/code&gt;은 빌드 로그에서 나온 새로운 md5 값으로 맞춥니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# bsp-io-demo_1.0.bb

SUMMARY = &quot;BSP IO demo (button -&amp;gt; bme280 -&amp;gt; http)&quot;
LICENSE = &quot;MIT&quot;
LIC_FILES_CHKSUM = &quot;file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302&quot;

SRC_URI = &quot;\
  file://bsp-io-demo.c \
  file://bsp-io-demo.service \
  file://bsp-io-demo.conf \
&quot;

S = &quot;${WORKDIR}&quot;

inherit systemd pkgconfig

DEPENDS += &quot;curl&quot;

SYSTEMD_SERVICE:${PN} = &quot;bsp-io-demo.service&quot;
SYSTEMD_AUTO_ENABLE:${PN} = &quot;enable&quot;

do_compile() {
    ${CC} ${CFLAGS} ${LDFLAGS} \
        -o bsp-io-demo bsp-io-demo.c \
        $(${PKG_CONFIG} --cflags --libs libcurl)
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 bsp-io-demo ${D}${bindir}/bsp-io-demo

    install -d ${D}${systemd_system_unitdir}
    install -m 0644 ${WORKDIR}/bsp-io-demo.service ${D}${systemd_system_unitdir}/

    install -d ${D}${sysconfdir}
    install -m 0644 ${WORKDIR}/bsp-io-demo.conf ${D}${sysconfdir}/bsp-io-demo
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::TIP
위에서 중요한 포인트는 &lt;code&gt;--libs&lt;/code&gt;가 쉘에서 &lt;strong&gt;명령으로 오해되지 않도록&lt;/strong&gt; &lt;code&gt;$(${PKG_CONFIG} ...)&lt;/code&gt; 형태로 감싸는 것입니다.
:::&lt;/p&gt;
&lt;h3&gt;4.3 systemd 유닛(bsp-io-demo.service)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=BSP IO DEMO (button -&amp;gt; bme280 -&amp;gt; http)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=-/etc/bsp-io-demo
ExecStart=/usr/bin/bsp-io-demo
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.4 환경 설정 파일(bsp-io-demo.conf)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# bsp-io-demo

SERVER_URL=http://&amp;lt;본인 서버 IP&amp;gt;:18000/ingest
INPUT_EVENT=/dev/input/event0
LED_NAME=blue:heartbeat
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.5 이미지에 포함(st-image-weston.bbappend)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# st-image-weston.bbappend

IMAGE_INSTALL:append = &quot; bsp-io-demo&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 런타임 검증(보드)&lt;/h2&gt;
&lt;h3&gt;5.1 서비스 기동 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl status bsp-io-demo --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서비스가 &lt;code&gt;active (running)&lt;/code&gt;이고, 환경변수가 로그에 찍히면 1차 OK입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/01-systemctl-status.png&quot; alt=&quot;systemctl status&quot; /&gt;&lt;/p&gt;
&lt;p&gt;서비스 시작 직후 journal에 &lt;code&gt;SERVER_URL/LED_NAME/INPUT_EVENT&lt;/code&gt;가 출력되면, systemd 환경변수 주입까지 정상입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/08-journal-serverurl.png&quot; alt=&quot;journal start log&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::TIP
unit 파일을 수정했는데 &quot;changed on disk&quot; 경고가 뜨면 &lt;code&gt;systemctl daemon-reload&lt;/code&gt; 후 서비스를 재시작합니다.
:::&lt;/p&gt;
&lt;h3&gt;5.2 HTTP 전송 성공 확인(journal + 서버 로그)&lt;/h3&gt;
&lt;p&gt;버튼을 눌렀을 때 보드에서 아래처럼 &lt;code&gt;post_result=OK&lt;/code&gt;가 찍히면 전송 성공입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/09-journal-post-ok.png&quot; alt=&quot;journal post OK&quot; /&gt;&lt;/p&gt;
&lt;p&gt;서버 측에서는 요청이 들어오면서 204 응답이 찍힙니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/10-server-204.png&quot; alt=&quot;server 204&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 트러블슈팅 포인트&lt;/h2&gt;
&lt;h3&gt;6.1 네트워크가 의심되면: 라우팅/주소부터 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ip -4 addr
ip -4 route
ip route get &amp;lt;본인 서버 IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/05-ip-addr-route.png&quot; alt=&quot;ip addr/route&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.2 &lt;code&gt;Empty reply from server&lt;/code&gt;는 서버가 예외로 연결을 끊는 경우가 많음&lt;/h3&gt;
&lt;p&gt;보드에서 &lt;code&gt;curl -v&lt;/code&gt;로 확인했을 때 연결은 되는데 응답이 비어 있으면(=서버가 제대로 응답 못함) 서버 콘솔/로그에서 예외를 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/04-curl-empty-reply.png&quot; alt=&quot;curl empty reply&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.3 &lt;code&gt;sensor read failed&lt;/code&gt;가 뜨는 경우&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;BME280 IIO 디바이스 번호가 바뀌었을 수 있음(&lt;code&gt;/sys/bus/iio/devices/iio:deviceX/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;필요한 sysfs 파일명이 환경에 따라 다를 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for d in /sys/bus/iio/devices/iio:device*; do
  echo &quot;== $d ==&quot;;
  cat &quot;$d/name&quot; 2&amp;gt;/dev/null || true;
  ls -1 &quot;$d&quot; | grep -E &apos;temp|press|humid&apos; || true;
  echo;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4 systemd 유닛 변경 경고(&quot;changed on disk&quot;)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;systemctl status&lt;/code&gt;에 아래 경고가 보이면, 유닛 파일이 변경되었는데 systemd가 아직 반영하지 않은 상태입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/06-systemctl-status-initial.png&quot; alt=&quot;systemctl changed on disk&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart bsp-io-demo
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ST wiki: How to control a GPIO in userspace (libgpiod, userspace GPIO)
&lt;a href=&quot;https://wiki.st.com/stm32mpu/wiki/How_to_control_a_GPIO_in_userspace&quot;&gt;https://wiki.st.com/stm32mpu/wiki/How_to_control_a_GPIO_in_userspace&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ST wiki: GPIOLib overview
&lt;a href=&quot;https://wiki.st.com/stm32mpu/wiki/GPIOLib_overview&quot;&gt;https://wiki.st.com/stm32mpu/wiki/GPIOLib_overview&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux kernel Device Tree binding: gpio-keys
&lt;a href=&quot;https://www.kernel.org/doc/Documentation/devicetree/bindings/input/gpio-keys.txt&quot;&gt;https://www.kernel.org/doc/Documentation/devicetree/bindings/input/gpio-keys.txt&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux kernel ABI (IIO sysfs: in_temp_input, in_pressure_input, in_humidityrelative_input 등)
&lt;a href=&quot;https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-iio&quot;&gt;https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-iio&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Yocto Project Dev Manual: Working With Licenses (LIC_FILES_CHKSUM)
&lt;a href=&quot;https://docs.yoctoproject.org/dev-manual/licenses.html&quot;&gt;https://docs.yoctoproject.org/dev-manual/licenses.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(참고) Yocto 메일링리스트: PKG_CONFIG가 비어 있는 케이스/맥락
&lt;a href=&quot;https://docs.yoctoproject.org/pipermail/yocto/2019-November/047314.html&quot;&gt;https://docs.yoctoproject.org/pipermail/yocto/2019-November/047314.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;systemd.exec(5): EnvironmentFile= (서비스 환경파일 로딩)
&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/systemd.exec.html&quot;&gt;https://www.freedesktop.org/software/systemd/man/systemd.exec.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 15. 주소 변환의 원리 (Address Translation)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-15-vm-mechanism/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-15-vm-mechanism/</guid><description>메모리 가상화를 구현하는 핵심 메커니즘인 주소 변환(Address Translation)을 다룹니다. 하드웨어 기반의 동적 재배치(Base and Bounds) 기법의 원리와 이를 지원하기 위한 OS와 하드웨어의 역할을 정리합니다.</description><pubDate>Thu, 22 Jan 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 장에서는 메모리 가상화를 실제로 구현하는 &lt;strong&gt;메커니즘(Mechanism)&lt;/strong&gt; 을 다룹니다.&lt;/p&gt;
&lt;p&gt;CPU 가상화에서 &lt;strong&gt;LDE(Limited Direct Execution, 제한적 직접 실행)&lt;/strong&gt; 기법을 사용했던 것 기억하시나요? 메모리 가상화에서도 비슷한 전략을 사용합니다. 프로그램이 메모리에 직접 접근하게 두되, &lt;strong&gt;중요한 순간에 OS와 하드웨어가 개입&lt;/strong&gt;하여 효율성과 제어를 동시에 잡는 것이 목표입니다.&lt;/p&gt;
&lt;p&gt;그 첫 번째 단계로, 가장 단순하지만 강력한 기법인 &lt;strong&gt;하드웨어 기반 주소 변환(Hardware-based Address Translation)&lt;/strong&gt;, 일명 &lt;strong&gt;동적 재배치(Dynamic Relocation)&lt;/strong&gt; 를 알아봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 주소 변환 (Address Translation)&lt;/h2&gt;
&lt;p&gt;주소 변환의 핵심 아이디어는 간단합니다.&lt;/p&gt;
&lt;p&gt;:::note
하드웨어가 모든 메모리 참조를 가로채서(Interpose), 가상 주소를 실제 물리 주소로 바꾼다.
:::&lt;/p&gt;
&lt;p&gt;프로그램은 자신이 &lt;code&gt;0&lt;/code&gt;번지부터 시작하는 완벽한 전용 메모리를 가진다고 생각하지만, 실제로는 물리 메모리의 어딘가(예: &lt;code&gt;32KB&lt;/code&gt;)에 배치되어 있습니다. CPU는 명령어를 실행하거나 데이터를 읽고 쓸 때마다 이 변환을 수행합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 동적 재배치: 베이스와 바운드 (Base and Bounds)&lt;/h2&gt;
&lt;p&gt;이 변환을 이해하기 위해, 먼저 프로세스가 바라보는 메모리(환상)와 실제 메모리(현실)를 비교해 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/15_1.png&quot; alt=&quot;Figure 15.1 Process Address Space&quot; /&gt;
&lt;em&gt;Figure 15.1: 프로세스의 관점. 코드는 0부터, 스택은 16KB에서 시작한다고 믿습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;하지만 실제 물리 메모리 상에서의 모습은 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/15_2.png&quot; alt=&quot;Figure 15.2 Physical Memory with Relocated Process&quot; /&gt;
&lt;em&gt;Figure 15.2: 실제 물리 메모리. OS는 이 프로세스를 32KB 지점에 재배치했습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이러한 &lt;strong&gt;재배치(Relocation)&lt;/strong&gt; 를 효율적으로 수행하기 위해 CPU에는 두 개의 특수 레지스터가 필요합니다.&lt;/p&gt;
&lt;h3&gt;2.1 베이스 레지스터 (Base Register)&lt;/h3&gt;
&lt;p&gt;프로그램이 물리 메모리의 &lt;strong&gt;어디에 로드되었는지(시작 위치)&lt;/strong&gt; 를 저장합니다.
주소 변환 공식은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;$\text{Physical Address} = \text{Virtual Address} + \text{Base}$&lt;/p&gt;
&lt;p&gt;예를 들어, 베이스가 &lt;code&gt;32KB&lt;/code&gt;이고 프로그램이 가상 주소 &lt;code&gt;128&lt;/code&gt;을 요청하면:
&lt;code&gt;32768 + 128 = 32896&lt;/code&gt; (물리 주소)&lt;/p&gt;
&lt;h3&gt;2.2 바운드 레지스터 (Bounds Register)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;보호(Protection)&lt;/strong&gt; 를 위한 레지스터입니다. 주소 공간의 &lt;strong&gt;크기(Limit)&lt;/strong&gt; 를 저장합니다.
프로그램이 자신의 크기를 벗어난 가상 주소(예: 범위를 넘거나 음수)를 요청하면, 하드웨어는 변환을 중단하고 &lt;strong&gt;예외(Exception)&lt;/strong&gt; 를 발생시킵니다.&lt;/p&gt;
&lt;h3&gt;2.3 동작 예시&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;가상 주소 공간 크기: 4KB&lt;/li&gt;
&lt;li&gt;물리 메모리 로드 위치: 16KB (Base)&lt;/li&gt;
&lt;li&gt;가상 주소 &lt;code&gt;0&lt;/code&gt; 접근 -&amp;gt; &lt;code&gt;16KB&lt;/code&gt; (유효)&lt;/li&gt;
&lt;li&gt;가상 주소 &lt;code&gt;3000&lt;/code&gt; 접근 -&amp;gt; &lt;code&gt;19384&lt;/code&gt; (16KB + 3000, 유효)&lt;/li&gt;
&lt;li&gt;가상 주소 &lt;code&gt;4400&lt;/code&gt; 접근 -&amp;gt; &lt;strong&gt;Fault (Out of Bounds)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 기법을 &lt;strong&gt;동적 재배치(Dynamic Relocation)&lt;/strong&gt; 라고 부르는 이유는, 프로그램 실행 중에도 베이스 레지스터만 바꾸면 주소 공간을 쉽게 다른 곳으로 옮길(Move) 수 있기 때문입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 하드웨어의 지원 (Hardware Support)&lt;/h2&gt;
&lt;p&gt;이 메커니즘이 동작하려면 하드웨어(CPU/MMU)가 다음 기능들을 지원해야 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;특권 모드(Privileged Mode)&lt;/strong&gt;: 베이스/바운드 레지스터는 오직 커널 모드에서만 수정 가능해야 합니다. 일반 프로그램이 이를 수정하면 메모리 보호가 무너집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예외 발생(Exception Raising)&lt;/strong&gt;: 범위 밖 메모리 접근 시 CPU는 실행을 멈추고 OS의 예외 핸들러를 호출해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주소 변환 회로&lt;/strong&gt;: 매 명령어 인출, 로드, 스토어마다 덧셈과 비교 연산을 매우 빠르게 수행해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 운영체제의 역할 (OS Issues)&lt;/h2&gt;
&lt;p&gt;하드웨어가 변환을 담당하더라도, OS가 처리해야 할 중요한 이슈들이 있습니다.&lt;/p&gt;
&lt;h3&gt;4.1 프로세스 생성 시 (Creation)&lt;/h3&gt;
&lt;p&gt;새 프로세스를 위한 메모리 공간을 찾아야 합니다. 이를 위해 &lt;strong&gt;프리 리스트(Free List)&lt;/strong&gt; 같은 자료구조로 비어 있는 물리 메모리 슬롯을 관리합니다.&lt;/p&gt;
&lt;h3&gt;4.2 프로세스 종료 시 (Termination)&lt;/h3&gt;
&lt;p&gt;프로세스가 끝나면 사용하던 메모리를 회수하여 다시 프리 리스트에 반환해야 합니다.&lt;/p&gt;
&lt;h3&gt;4.3 문맥 교환 시 (Context Switch)&lt;/h3&gt;
&lt;p&gt;베이스와 바운드 레지스터는 CPU에 하나뿐입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 프로세스 실행 중단 -&amp;gt; A의 베이스/바운드 값을 &lt;strong&gt;PCB(Process Control Block)&lt;/strong&gt; 에 저장.&lt;/li&gt;
&lt;li&gt;B 프로세스 실행 재개 -&amp;gt; PCB에 있던 B의 베이스/바운드 값을 CPU 레지스터에 복원.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 예외 처리 (Exception Handling)&lt;/h3&gt;
&lt;p&gt;바운드 범위를 벗어난 접근이 발생했을 때 실행할 **핸들러(Handler)**를 부팅 시에 등록해야 합니다. 보통 이런 경우 해당 프로세스를 강제 종료(Kill)시킵니다.&lt;/p&gt;
&lt;h3&gt;4.5 실행 흐름 상세 분석 (Timeline)&lt;/h3&gt;
&lt;p&gt;프로그램이 실행될 때(런타임), OS와 하드웨어 그리고 프로그램이 어떻게 상호작용하는지 표로 정리해 봅시다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OS (커널 모드)&lt;/th&gt;
&lt;th&gt;하드웨어&lt;/th&gt;
&lt;th&gt;프로그램 (유저 모드)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;프로세스 A 시작&lt;/strong&gt;&amp;lt;br&amp;gt;1. 프로세스 테이블 항목 할당&amp;lt;br&amp;gt;2. 메모리 할당&amp;lt;br&amp;gt;3. 베이스/바운드 레지스터 설정&amp;lt;br&amp;gt;4. 트랩 리턴 (return-from-trap)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1. A의 레지스터 복원&amp;lt;br&amp;gt;2. 유저 모드로 전환&amp;lt;br&amp;gt;3. A의 시작(PC)으로 점프&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;주소 변환 &amp;amp; 실행&lt;/strong&gt;&amp;lt;br&amp;gt;1. 가상 주소 변환&amp;lt;br&amp;gt;2. 명령어 인출(Fetch)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;프로세스 A 실행&lt;/strong&gt;&amp;lt;br&amp;gt;명령어 실행...&amp;lt;br&amp;gt;(A 실행 중)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;타이머 인터럽트 발생&lt;/strong&gt;&amp;lt;br&amp;gt;1. 커널 모드로 전환&amp;lt;br&amp;gt;2. 인터럽트 핸들러로 점프&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;컨텍스트 스위치 (A → B)&lt;/strong&gt;&amp;lt;br&amp;gt;1. A 중단, B 실행 결정&amp;lt;br&amp;gt;2. &lt;code&gt;switch()&lt;/code&gt; 루틴 호출&amp;lt;br&amp;gt;3. A의 레지스터(베이스/바운드 포함) 저장&amp;lt;br&amp;gt;4. B의 레지스터(베이스/바운드 포함) 복원&amp;lt;br&amp;gt;5. 트랩 리턴&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1. B의 레지스터 복원&amp;lt;br&amp;gt;2. 유저 모드로 전환&amp;lt;br&amp;gt;3. B의 PC로 점프&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;명령어 인출 및 실행&lt;/strong&gt;&amp;lt;br&amp;gt;1. 가상 주소 변환&amp;lt;br&amp;gt;2. 주소 범위 체크 (유효)&amp;lt;br&amp;gt;3. 로드/스토어 수행&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;프로세스 B 실행&lt;/strong&gt;&amp;lt;br&amp;gt;잘못된 로드(Bad Load) 시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;예외 발생 (Out-of-Bounds)&lt;/strong&gt;&amp;lt;br&amp;gt;1. 범위 초과 감지&amp;lt;br&amp;gt;2. 커널 모드로 전환&amp;lt;br&amp;gt;3. 트랩 핸들러로 점프&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;예외 처리&lt;/strong&gt;&amp;lt;br&amp;gt;1. 프로세스 B 강제 종료 (Kill)&amp;lt;br&amp;gt;2. B의 메모리 반환&amp;lt;br&amp;gt;3. 프로세스 테이블에서 제거&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장에서는 메모리 가상화의 기본 메커니즘인 &lt;strong&gt;동적 재배치&lt;/strong&gt;를 살펴봤습니다.
베이스와 바운드 레지스터라는 간단한 하드웨어 추가만으로 우리는 &lt;strong&gt;효율성(빠른 변환)&lt;/strong&gt; 과 &lt;strong&gt;보호(범위 체크)&lt;/strong&gt; 를 달성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 이 방식에는 치명적인 단점이 하나 있습니다. 바로 &lt;strong&gt;내부 단편화(Internal Fragmentation)&lt;/strong&gt; 입니다. 스택과 힙 사이의 사용하지 않는 공간(Free Space)도 물리 메모리를 차지해야 하기 때문에 메모리 낭비가 심합니다. 이를 해결하기 위해 다음 장에서는 &lt;strong&gt;세그멘테이션(Segmentation)&lt;/strong&gt; 을 배울 것입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;주소 변환(Address Translation)&lt;/code&gt;: 가상 주소를 물리 주소로 변환하는 하드웨어 메커니즘.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;동적 재배치(Dynamic Relocation)&lt;/code&gt;: 프로그램 실행 중에 베이스 레지스터를 이용해 주소 공간의 물리적 위치를 지정하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;베이스 레지스터(Base Register)&lt;/code&gt;: 주소 변환을 위해 가상 주소에 더해지는 시작 물리 주소 값.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;바운드 레지스터(Bounds Register)&lt;/code&gt;: 메모리 보호를 위해 주소 공간의 크기(상한선)를 저장하는 레지스터.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MMU(Memory Management Unit)&lt;/code&gt;: 주소 변환과 접근 제어를 담당하는 CPU 내부의 하드웨어 장치.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;프리 리스트(Free List)&lt;/code&gt;: 사용 가능한 물리 메모리 영역들을 관리하는 OS의 자료구조.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;내부 단편화(Internal Fragmentation)&lt;/code&gt;: 할당된 메모리 영역 내부에서 사용되지 않고 낭비되는 공간.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-mechanism.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 15: Mechanism: Address Translation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 14. 메모리 API (Memory API)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-14-vm-api/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-14-vm-api/</guid><description>UNIX/C 환경에서의 메모리 할당 및 관리 방법을 정리합니다. 스택과 힙의 차이, malloc/free 사용법, 그리고 흔히 발생하는 메모리 오류(Memory Leak, Buffer Overflow 등)와 운영체제의 지원 메커니즘을 다룹니다.</description><pubDate>Thu, 22 Jan 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 13장에서는 메모리 가상화의 개념인 주소 공간을 다루었습니다. 이번 14장은 실제 &lt;strong&gt;UNIX/C 프로그래밍&lt;/strong&gt; 환경에서 메모리를 할당하고 관리하는 API와 주의사항을 다룹니다.&lt;/p&gt;
&lt;p&gt;C 언어의 &lt;code&gt;malloc&lt;/code&gt;과 &lt;code&gt;free&lt;/code&gt; 함수는 메모리 관리의 핵심이지만, 올바르게 사용하지 않을 경우 치명적인 오류를 발생시킵니다. 이번 글에서는 이들의 정확한 사용법과 흔한 오류 사례, 그리고 운영체제 차원의 지원 기능을 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 메모리의 종류: 스택(Stack) vs 힙(Heap)&lt;/h2&gt;
&lt;p&gt;C 프로그램이 실행될 때 할당되는 메모리는 관리 방식에 따라 크게 두 가지로 나뉩니다.&lt;/p&gt;
&lt;h3&gt;1.1 스택 메모리 (Stack Memory)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;관리 주체&lt;/strong&gt;: 컴파일러 (암묵적 할당 및 해제)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 함수 호출 시 지역 변수 등을 저장하기 위해 자동으로 할당되며, 함수가 반환되면 자동으로 해제됩니다. 이러한 특성 때문에 **자동 메모리(Automatic Memory)**라고도 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;선언 예시&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func() {
    int x; // 스택에 정수형 변수 선언
    // 함수 종료 시 x는 자동 해제됨
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 힙 메모리 (Heap Memory)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;관리 주체&lt;/strong&gt;: 프로그래머 (명시적 할당 및 해제)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 프로그램 실행 중 동적으로 크기가 결정되거나, 함수 반환 이후에도 데이터가 유지되어야 할 때 사용합니다. 프로그래머가 명시적으로 할당하고 해제해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;선언 예시&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void func() {
    int *x = (int *) malloc(sizeof(int)); // 힙에 메모리 할당 요청
    // ...
    free(x); // 사용 후 반드시 해제
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. malloc() 함수&lt;/h2&gt;
&lt;p&gt;힙 메모리를 할당할 때 사용하는 표준 라이브러리 함수입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdlib.h&amp;gt;
void *malloc(size_t size);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인자&lt;/strong&gt;: 할당받을 메모리의 크기(바이트 단위).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;반환값&lt;/strong&gt;: 성공 시 할당된 메모리의 시작 주소(&lt;code&gt;void *&lt;/code&gt;), 실패 시 &lt;code&gt;NULL&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;사용 시 주의사항&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sizeof()&lt;/code&gt; 연산자 사용&lt;/strong&gt;: &lt;code&gt;malloc(sizeof(int))&lt;/code&gt;와 같이 &lt;code&gt;sizeof()&lt;/code&gt; 연산자를 사용하여 타입의 크기를 정확하게 전달해야 합니다. 숫자를 직접 입력하는 것은 이식성을 떨어뜨립니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문자열 할당&lt;/strong&gt;: 문자열을 위한 공간을 할당할 때는 반드시 종료 문자(&lt;code&gt;\0&lt;/code&gt;)를 고려하여 &lt;code&gt;malloc(strlen(s) + 1)&lt;/code&gt;과 같이 1바이트를 더 할당해야 합니다. &lt;code&gt;sizeof()&lt;/code&gt;는 포인터 자체의 크기만 반환하므로 문자열 길이를 구할 때는 적합하지 않습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. free() 함수&lt;/h2&gt;
&lt;p&gt;더 이상 사용하지 않는 힙 메모리를 운영체제에 반환하는 함수입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void free(void *ptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인자&lt;/strong&gt;: &lt;code&gt;malloc()&lt;/code&gt;이 반환했던 포인터.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 해제할 메모리의 크기를 인자로 전달하지 않습니다. 메모리 할당 라이브러리가 내부적으로 해당 포인터가 가리키는 메모리 블록의 크기를 추적하고 있기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 흔한 오류 (Common Errors)&lt;/h2&gt;
&lt;p&gt;메모리 관리는 오류가 발생하기 쉽습니다. 다음은 컴파일은 성공하지만 실행 시 문제를 일으키는 대표적인 오류 유형입니다.&lt;/p&gt;
&lt;h3&gt;4.1 메모리 할당 누락 (Forgetting To Allocate Memory)&lt;/h3&gt;
&lt;p&gt;포인터를 선언한 후 메모리를 할당하지 않고 데이터를 복사하려는 경우입니다. 이 경우 &lt;strong&gt;Segmentation Fault&lt;/strong&gt;가 발생합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *src = &quot;hello&quot;;
char *dst; // 메모리 할당되지 않음
strcpy(dst, src); // Segfault 발생
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 메모리 공간 부족 (Not Allocating Enough Memory)&lt;/h3&gt;
&lt;p&gt;필요한 크기보다 적게 메모리를 할당하는 경우로, &lt;strong&gt;버퍼 오버플로우(Buffer Overflow)&lt;/strong&gt; 라고 합니다. 주로 문자열 종료 문자 공간을 계산하지 않았을 때 발생합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *src = &quot;hello&quot;;
char *dst = (char *) malloc(strlen(src)); // 종료 문자 공간 부족
strcpy(dst, src); // 힙 영역 침범 발생
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 초기화 누락 (Forgetting to Initialize)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;malloc()&lt;/code&gt;은 메모리 공간만 확보할 뿐 데이터를 초기화하지 않습니다. 초기화하지 않은 메모리를 읽을 경우 예측할 수 없는 값(Garbage Value)을 읽게 됩니다(Uninitialized Read).&lt;/p&gt;
&lt;h3&gt;4.4 메모리 해제 누락 (Memory Leak)&lt;/h3&gt;
&lt;p&gt;할당한 메모리를 &lt;code&gt;free()&lt;/code&gt;로 해제하지 않는 경우 **메모리 누수(Memory Leak)**가 발생합니다. 장시간 실행되는 서버 프로그램 등에서 메모리 누수가 누적되면 시스템 메모리가 고갈될 수 있습니다.&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;참고&lt;/strong&gt;: 프로세스가 종료되면 운영체제가 해당 프로세스의 모든 메모리를 회수합니다. 따라서 단기 실행 프로그램에서는 치명적이지 않을 수 있으나, 항상 메모리를 해제하는 습관을 갖는 것이 권장됩니다.
:::&lt;/p&gt;
&lt;h3&gt;4.5 해제된 메모리 사용 (Dangling Pointer)&lt;/h3&gt;
&lt;p&gt;이미 &lt;code&gt;free()&lt;/code&gt;를 통해 반환된 메모리 주소에 접근하는 오류입니다. 프로그램이 충돌하거나, 해당 주소에 다른 데이터가 할당되었을 경우 데이터 무결성이 훼손될 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4.6 중복 해제 (Double Free)&lt;/h3&gt;
&lt;p&gt;동일한 포인터에 대해 &lt;code&gt;free()&lt;/code&gt;를 두 번 이상 호출하는 오류입니다. 메모리 할당 라이브러리의 관리 구조가 손상되어 크래시가 발생할 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 운영체제의 지원 (Underlying OS Support)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;malloc&lt;/code&gt;과 &lt;code&gt;free&lt;/code&gt;는 시스템 콜이 아닌 C 라이브러리 함수입니다. 이 라이브러리는 운영체제의 시스템 콜을 사용하여 메모리를 관리합니다.&lt;/p&gt;
&lt;h3&gt;5.1 brk / sbrk&lt;/h3&gt;
&lt;p&gt;프로그램의 힙 영역 끝(Program Break) 위치를 변경하는 시스템 콜입니다. &lt;code&gt;sbrk&lt;/code&gt;를 호출하여 힙의 크기를 늘리거나 줄일 수 있습니다. 현대 프로그래밍에서는 이를 직접 호출하지 않고 &lt;code&gt;malloc&lt;/code&gt;을 사용합니다.&lt;/p&gt;
&lt;h3&gt;5.2 mmap&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mmap()&lt;/code&gt; 시스템 콜을 사용하여 운영체제로부터 익명 메모리 페이지(Anonymous Memory Region)를 직접 할당받을 수 있습니다. &lt;code&gt;malloc&lt;/code&gt; 구현체는 대용량 메모리를 할당할 때 &lt;code&gt;brk&lt;/code&gt; 대신 &lt;code&gt;mmap&lt;/code&gt;을 사용하기도 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 기타 함수&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;calloc()&lt;/code&gt;&lt;/strong&gt;: 메모리를 할당함과 동시에 모든 비트를 &lt;strong&gt;0으로 초기화&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;realloc()&lt;/code&gt;&lt;/strong&gt;: 이미 할당된 메모리 블록의 &lt;strong&gt;크기를 변경&lt;/strong&gt;합니다. 기존 데이터를 유지하며 더 큰 공간으로 확장하거나 축소할 때 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장에서는 메모리 API와 관리 기법을 정리했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;스택&lt;/strong&gt;은 컴파일러가 관리하는 자동 메모리이며, &lt;strong&gt;힙&lt;/strong&gt;은 프로그래머가 관리하는 수동 메모리입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt;으로 메모리를 할당하고 &lt;code&gt;free&lt;/code&gt;로 해제합니다.&lt;/li&gt;
&lt;li&gt;메모리 누수, 버퍼 오버플로우, 초기화 누락 등의 오류는 프로그램의 안정성을 저해합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gdb&lt;/code&gt;나 &lt;strong&gt;&lt;code&gt;valgrind&lt;/code&gt;&lt;/strong&gt; 같은 디버깅 및 분석 도구를 활용하여 메모리 관련 오류를 탐지할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장부터는 운영체제가 이 가상 주소 공간을 물리 메모리에 어떻게 매핑하는지, 구체적인 주소 변환 메커니즘을 다룹니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;스택 메모리(Stack Memory)&lt;/code&gt;: 함수 호출 시 자동 할당/해제되는 메모리 영역.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;힙 메모리(Heap Memory)&lt;/code&gt;: &lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt;를 통해 명시적으로 관리되는 동적 메모리 영역.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Segmentation Fault&lt;/code&gt;: 프로그램이 접근 권한이 없는 메모리 영역에 접근 시 발생하는 오류.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Buffer Overflow&lt;/code&gt;: 할당된 메모리 범위를 벗어나 데이터를 기록하는 오류.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Memory Leak&lt;/code&gt;: 할당된 메모리를 해제하지 않아 사용할 수 없는 메모리가 누적되는 현상.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dangling Pointer&lt;/code&gt;: 해제된 메모리를 가리키는 포인터.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;brk/sbrk&lt;/code&gt;: 프로세스의 데이터 세그먼트 크기를 조정하는 시스템 콜.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-api.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 14: Interlude: Memory API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 13. 주소 공간의 개념 (The Abstraction: Address Spaces)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-13-vm-intro/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-13-vm-intro/</guid><description>메모리 가상화의 기초. 초기 시스템부터 멀티프로그래밍을 거쳐 &apos;주소 공간(Address Space)&apos;이라는 핵심 추상화가 도입된 배경과 구조, 그리고 OS가 메모리를 가상화할 때 달성해야 하는 3가지 목표를 정리합니다.</description><pubDate>Wed, 21 Jan 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 포스팅까지 CPU 가상화(스케줄링)를 다루었고, 이번 글부터는 OS의 또 다른 핵심 기능인 &lt;strong&gt;메모리 가상화(Memory Virtualization)&lt;/strong&gt; 를 정리합니다.&lt;/p&gt;
&lt;p&gt;『Operating Systems: Three Easy Pieces(OSTEP)』 13장은 메모리 가상화의 가장 기초가 되는 개념인 &lt;strong&gt;주소 공간(Address Space)&lt;/strong&gt; 의 정의와 필요성, 그리고 이를 구현하기 위한 목표를 설명합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 초기 시스템과 멀티프로그래밍&lt;/h2&gt;
&lt;h3&gt;1.1 초기 시스템 (Early Systems)&lt;/h3&gt;
&lt;p&gt;초기의 컴퓨터 시스템은 메모리 관리가 단순했습니다.
물리 메모리의 앞부분(예: 0~64KB)에는 운영체제(라이브러리 세트)가 상주하고, 나머지 공간에는 &lt;strong&gt;단 하나의 실행 중인 프로그램&lt;/strong&gt;이 로드되어 물리 메모리를 직접 사용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/13_1.png&quot; alt=&quot;Figure 13.1 Early Systems&quot; /&gt;
&lt;em&gt;Figure 13.1: 초기 시스템의 메모리 구조. OS와 하나의 프로그램만 존재한다.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;물리 메모리를 그대로 사용했습니다.&lt;/li&gt;
&lt;li&gt;가상화? 추상화? 그런 건 없었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 멀티프로그래밍과 시분할 (Multiprogramming &amp;amp; Time Sharing)&lt;/h3&gt;
&lt;p&gt;이후 컴퓨터 자원의 효율성을 높이기 위해 여러 프로세스를 동시에 메모리에 올리는 &lt;strong&gt;멀티프로그래밍(Multiprogramming)&lt;/strong&gt; 이 도입되었습니다. 이어 사용자와의 상호작용이 중요한 &lt;strong&gt;시분할 시스템(Time Sharing)&lt;/strong&gt; 시대로 넘어오면서, 여러 프로세스가 빠른 응답 속도로 전환되며 실행되어야 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/13_2.png&quot; alt=&quot;Figure 13.2 Three Processes Sharing Memory&quot; /&gt;
&lt;em&gt;Figure 13.2: 시분할 시스템. 물리 메모리에 여러 프로세스가 나누어 적재되어 있다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;여기서 문제가 생깁니다.
여러 프로세스가 메모리에 동시에 올라와 있어야 하는데, &lt;strong&gt;서로의 영역을 침범하지 않게 어떻게 보호할 것인가?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 과정에서 &lt;strong&gt;보호(Protection)&lt;/strong&gt; 문제가 대두되었습니다.
하나의 메모리 공간에 여러 프로세스가 공존하게 되면서, 어떤 프로세스가 다른 프로세스의 메모리나 OS 영역을 침범(읽기/쓰기)하지 못하도록 막아야 할 필요성이 생긴 것입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 핵심 추상화: 주소 공간 (Address Space)&lt;/h2&gt;
&lt;p&gt;이 문제를 해결하기 위해 OS는 &lt;strong&gt;주소 공간(Address Space)&lt;/strong&gt; 이라는 개념을 도입합니다.&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;주소 공간&lt;/strong&gt;: 실행 중인 프로그램이 보기에 자신이 독점하고 있는 메모리의 모습
:::&lt;/p&gt;
&lt;p&gt;실제 물리 메모리는 수많은 프로세스가 공유하고 있지만, OS는 각 프로세스에게 &lt;strong&gt;&quot;네가 이 메모리를 다 쓰고 있어&quot;&lt;/strong&gt; 라는 환상을 심어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/13_3.png&quot; alt=&quot;Figure 13.3 An Example Address Space&quot; /&gt;
&lt;em&gt;Figure 13.3: 일반적인 주소 공간의 구조 (Code, Heap, Stack)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;일반적인 주소 공간은 다음 세 가지 요소로 구성됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;코드(Code)&lt;/strong&gt;: 명령어(Instruction)가 저장되는 영역입니다. 크기가 고정(Static)되어 있으므로 주소 공간의 상단에 위치합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;힙(Heap)&lt;/strong&gt;: &lt;code&gt;malloc()&lt;/code&gt; (C) 또는 &lt;code&gt;new&lt;/code&gt; (Java/C++) 등을 통해 동적으로 할당되는 메모리입니다. 상단(코드 직후)에서 시작하여 아래로 확장됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스택(Stack)&lt;/strong&gt;: 함수 호출 체인, 지역 변수, 리턴 주소 등이 저장되는 영역입니다. 주소 공간의 하단에서 시작하여 위로 확장됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 메모리 가상화 (Virtualizing Memory)&lt;/h2&gt;
&lt;p&gt;메모리 가상화의 핵심 원리는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;사용자 프로그램이 보는 모든 주소는 가상 주소(Virtual Address)이다.&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;C 프로그램에서 포인터 주소를 출력했을 때 나오는 값은 실제 물리 메모리의 주소(Physical Address)가 아닙니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;프로세스가 가상 주소(예: 0번지)에 접근을 시도합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;와 &lt;strong&gt;하드웨어&lt;/strong&gt;가 개입하여 이 가상 주소를 실제 데이터가 위치한 &lt;strong&gt;물리 주소&lt;/strong&gt;로 변환합니다.&lt;/li&gt;
&lt;li&gt;이를 통해 프로세스는 자신이 메모리를 독점하는 것처럼 느끼지만, 실제로는 OS가 물리 메모리를 여러 프로세스에 나누어 관리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 메모리 가상화의 목표 (Goals)&lt;/h2&gt;
&lt;p&gt;OS가 메모리를 가상화할 때 달성해야 하는 세 가지 주요 목표는 다음과 같습니다.&lt;/p&gt;
&lt;h3&gt;4.1 투명성 (Transparency)&lt;/h3&gt;
&lt;p&gt;가상화는 실행 중인 프로그램이 인지할 수 없어야 합니다.
프로그램은 자신이 가상 메모리를 사용한다는 사실을 모른 채, 전용 물리 메모리를 가진 것처럼 동작해야 합니다.&lt;/p&gt;
&lt;h3&gt;4.2 효율성 (Efficiency)&lt;/h3&gt;
&lt;p&gt;가상화로 인한 오버헤드를 최소화해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간(Time)&lt;/strong&gt;: 주소 변환 과정이 프로그램 실행 속도를 크게 저하시키지 않아야 합니다. (하드웨어 TLB 등의 지원 필요)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공간(Space)&lt;/strong&gt;: 가상화를 지원하기 위한 데이터 구조(페이지 테이블 등)가 메모리를 너무 많이 차지하지 않아야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 보호 (Protection)&lt;/h3&gt;
&lt;p&gt;프로세스 간의 &lt;strong&gt;격리(Isolation)&lt;/strong&gt; 를 보장해야 합니다.
한 프로세스가 다른 프로세스의 메모리나 OS 자체의 메모리에 접근하거나 영향을 주어서는 안 됩니다. 이를 통해 프로그램이 오류를 일으켜도 다른 프로세스나 시스템 전체는 안전하게 보호됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;13장에서는 메모리 가상화의 기본 개념을 다루었습니다.
OS는 &lt;strong&gt;주소 공간&lt;/strong&gt;이라는 추상화를 통해 프로그램에게 사용하기 쉬운 메모리 뷰를 제공하고, 내부적으로는 &lt;strong&gt;주소 변환(Address Translation)&lt;/strong&gt; 메커니즘을 통해 물리 메모리를 효율적이고 안전하게 관리합니다.&lt;/p&gt;
&lt;p&gt;다음 장부터는 이 가상화를 실제로 구현하기 위한 기법들(베이스/바운드 레지스터, 세그멘테이션, 페이징 등)을 순차적으로 살펴볼 예정입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;주소 공간(Address Space)&lt;/code&gt;: 실행 중인 프로그램이 인식하는 메모리의 추상화된 범위.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;가상 주소(Virtual Address)&lt;/code&gt;: 프로그램이 참조하는 논리적인 주소. OS에 의해 물리 주소로 변환됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;물리 주소(Physical Address)&lt;/code&gt;: 실제 하드웨어 메모리 상의 주소.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;투명성(Transparency)&lt;/code&gt;: 가상화 시스템이 사용자에게 드러나지 않고 투명하게 동작하는 성질.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;격리(Isolation)&lt;/code&gt;: 프로세스가 서로의 메모리에 간섭하지 못하도록 분리하여 시스템 신뢰성을 높이는 원칙.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/vm-intro.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 13: The Abstraction: Address Spaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/dialogue-vm.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Dialogue on Memory Virtualization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 10. 멀티프로세서 스케줄링 (Multiprocessor Scheduling)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-10-cpu-sched-multi/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-10-cpu-sched-multi/</guid><description>싱글 코어에서 멀티 코어로 넘어오면서 생기는 문제(캐시 일관성, 동기화)와 이를 해결하기 위한 스케줄링 기법(SQMS vs MQMS)을 정리합니다. 리눅스 스케줄러(O(1), CFS)의 기반이 되는 아이디어를 다룹니다.</description><pubDate>Wed, 21 Jan 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 『Operating Systems: Three Easy Pieces(OSTEP)』의 &lt;strong&gt;Multiprocessor Scheduling (Advanced)&lt;/strong&gt; 파트를 읽고 정리한 노트입니다.&lt;/p&gt;
&lt;p&gt;지난 시간까지 우리는 CPU가 하나만 있는 상황을 가정하고 스케줄링(SJF, RR, MLFQ, Lottery 등)을 고민했습니다. 하지만 요즘 세상에 싱글 코어 CPU를 쓰는 사람은 거의 없죠.&lt;/p&gt;
&lt;p&gt;CPU가 여러 개(Multicore)가 되면 기존의 스케줄링 이론들은 어떤 도전에 직면할까요? 단순히 &quot;CPU가 많으니까 일을 나눠서 하면 된다&quot;라고 하기엔, 하드웨어적인 제약과 성능 문제가 꽤 복잡합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 배경: 멀티프로세서 환경의 하드웨어 이슈&lt;/h2&gt;
&lt;p&gt;멀티프로세서 스케줄링을 이해하려면, 먼저 하드웨어 아키텍처가 어떻게 변했는지 이해해야 합니다. 핵심은 &lt;strong&gt;캐시(Cache)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;h3&gt;1.1 캐시 일관성 (Cache Coherence)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/10_2.png&quot; alt=&quot;Figure 10.2 Two CPUs With Caches&quot; /&gt;
&lt;em&gt;Figure 10.2: 두 개의 CPU가 메모리를 공유하지만 캐시는 따로 가진 구조&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;각 CPU는 자신만의 L1, L2 캐시를 가지고 있습니다.
만약 CPU 1이 메모리 주소 A의 데이터를 읽어와서 수정했다면, CPU 1의 캐시에는 최신 데이터가 있지만 메인 메모리와 CPU 2의 캐시에는 &lt;strong&gt;구버전 데이터(Stale Data)&lt;/strong&gt; 가 남아있게 됩니다.&lt;/p&gt;
&lt;p&gt;이 상태에서 CPU 2가 A를 읽으려고 하면 문제가 생기겠죠? 이를 해결하기 위해 하드웨어는 &lt;strong&gt;버스 스누핑(Bus Snooping)&lt;/strong&gt; 등의 기술로 모든 캐시의 데이터를 일치시키는 &lt;strong&gt;캐시 일관성&lt;/strong&gt;을 보장합니다.&lt;/p&gt;
&lt;h3&gt;1.2 동기화 (Synchronization)&lt;/h3&gt;
&lt;p&gt;OS 커널이나 애플리케이션이 공유 데이터(예: 작업 큐)에 접근할 때, 여러 CPU가 동시에 접근하면 데이터가 깨질 수 있습니다.
이를 막기 위해 &lt;strong&gt;락(Lock)&lt;/strong&gt; 을 사용해야 하는데, CPU가 많아질수록 락을 얻기 위한 경쟁(Contention)이 심해져 성능 저하가 발생합니다.&lt;/p&gt;
&lt;h3&gt;1.3 캐시 친화성 (Cache Affinity)&lt;/h3&gt;
&lt;p&gt;프로세스 A가 CPU 1에서 실행되었다면, CPU 1의 캐시에는 A가 쓰던 데이터가 잔뜩 남아있을 겁니다(Warm Cache).
다음에 A를 실행할 때 CPU 2로 옮겨버리면, CPU 2는 맨땅에서 데이터를 다시 로드해야 하므로 느려집니다. 따라서 스케줄러는 &lt;strong&gt;가능하면 같은 프로세스를 같은 CPU에서 실행&lt;/strong&gt;하려고 노력해야 하는데, 이를 &lt;strong&gt;캐시 친화성&lt;/strong&gt;이라고 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 정책 1: SQMS (Single Queue Multiprocessor Scheduling)&lt;/h2&gt;
&lt;p&gt;가장 단순한 접근법은 &lt;strong&gt;&quot;기존 스케줄러를 재활용하자&quot;&lt;/strong&gt; 입니다.
모든 작업을 &lt;strong&gt;하나의 글로벌 큐(Single Queue)&lt;/strong&gt; 에 넣고, 여러 CPU가 이 큐에서 작업을 꺼내가는 방식입니다.&lt;/p&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;단순함&lt;/strong&gt;: 기존 싱글 프로세서용 스케줄러(예: MLFQ)를 거의 그대로 쓸 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로드 밸런싱(Load Balancing)&lt;/strong&gt;: 모든 CPU가 하나의 큐를 공유하므로, 특정 CPU가 놀고 있는 일이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;확장성(Scalability) 부족&lt;/strong&gt;: 모든 CPU가 큐 하나에 접근하려다 보니 &lt;strong&gt;락 경쟁(Lock Contention)&lt;/strong&gt; 이 심각해집니다. CPU를 늘려도 성능이 그만큼 오르지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시 친화성(Affinity) 나쁨&lt;/strong&gt;: 작업이 이 CPU 저 CPU를 왔다 갔다 하게 됩니다(Ping-pong Effect). 캐시 효과를 전혀 못 봅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./images/sqms_trace.png&quot; alt=&quot;SQMS Cache Affinity Issue&quot; /&gt;
&lt;em&gt;SQMS의 문제점: 작업(A~E)들이 CPU를 고정하지 못하고 계속 옮겨 다닌다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;물론 Affinity Mask 같은 기법으로 일부 해결할 수는 있지만, 근본적인 락 경쟁 문제는 남습니다.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 정책 2: MQMS (Multi-Queue Multiprocessor Scheduling)&lt;/h2&gt;
&lt;p&gt;확장성 문제를 해결하기 위해 등장한 것이 &lt;strong&gt;MQMS&lt;/strong&gt;입니다.
&lt;strong&gt;CPU마다 각자의 큐(Queue)를 갖는 방식&lt;/strong&gt;입니다. 작업이 시스템에 들어오면 특정 CPU의 큐에 배치됩니다.&lt;/p&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;확장성(Scalability) 좋음&lt;/strong&gt;: 각 CPU는 자기 큐만 봅니다. 글로벌 락 경쟁이 사라집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시 친화성(Affinity) 좋음&lt;/strong&gt;: 작업이 한 번 큐에 들어가면 계속 같은 CPU에서 실행되므로 캐시 효율이 극대화됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점: 로드 불균형 (Load Imbalance)&lt;/h3&gt;
&lt;p&gt;치명적인 단점이 있습니다. 운 나쁘게 어떤 CPU의 큐는 텅 비고, 어떤 CPU의 큐는 작업이 쌓일 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/mqms_imbalance.png&quot; alt=&quot;MQMS Load Imbalance&quot; /&gt;
&lt;em&gt;MQMS의 문제점: CPU 1은 바쁜데(B, D), CPU 0은 할 일이 없어 놀고 있다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이런 상황을 해결하기 위해 &lt;strong&gt;워크 스틸링(Work Stealing)&lt;/strong&gt; 기술이 필요합니다.&lt;/p&gt;
&lt;h4&gt;해결책: 워크 스틸링 (Work Stealing)&lt;/h4&gt;
&lt;p&gt;놀고 있는 CPU가 바쁜 CPU의 큐를 훔쳐(peek) 보고, 작업이 많으면 일부를 가져오는 기법입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;너무 자주 훔쳐보면? -&amp;gt; 오버헤드 증가&lt;/li&gt;
&lt;li&gt;너무 안 훔쳐보면? -&amp;gt; 로드 불균형 지속&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 &quot;얼마나 자주 훔쳐볼 것인가&quot;가 튜닝의 핵심이 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 리눅스의 접근 (Linux Schedulers)&lt;/h2&gt;
&lt;p&gt;현대 리눅스 스케줄러들은 대부분 &lt;strong&gt;MQMS&lt;/strong&gt; 방식을 기반으로 발전했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;O(1) Scheduler&lt;/strong&gt;: 우선순위 배열을 CPU마다 두어 확장성을 잡았고, 인터랙티브 성능에 집중했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CFS (Completely Fair Scheduler)&lt;/strong&gt;: 현재 리눅스의 기본 스케줄러로, 역시 멀티 큐 구조를 사용하며 결정론적인 공정성(vruntime)을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFS&lt;/strong&gt;: 특이하게 다시 단일 큐(SQMS) 구조를 채택하여, 데스크탑 환경에서의 복잡성을 줄이려 시도하기도 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;멀티프로세서 스케줄링은 &lt;strong&gt;확장성&lt;/strong&gt;과 &lt;strong&gt;캐시 효율&lt;/strong&gt; 사이의 줄타기입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;정책&lt;/th&gt;
&lt;th&gt;구조&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;단일 큐 공유&lt;/td&gt;
&lt;td&gt;구현 단순, 자동 로드 밸런싱&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;락 경쟁(확장성 X)&lt;/strong&gt;, 캐시 친화성 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MQMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPU별 개별 큐&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;확장성 우수&lt;/strong&gt;, 캐시 친화성 높음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;로드 불균형&lt;/strong&gt;, 워크 스틸링 구현 복잡&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;결국 &quot;완벽한 스케줄러는 없다&quot;는 명제는 여기서도 유효합니다. 시스템의 목적(서버용 vs 데스크탑용)에 따라 적절한 트레이드오프를 선택해야 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;캐시 일관성(Cache Coherence)&lt;/code&gt;: 여러 CPU 캐시에 저장된 동일한 데이터가 항상 같은 값을 유지하도록 보장하는 하드웨어 메커니즘.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;버스 스누핑(Bus Snooping)&lt;/code&gt;: 캐시들이 버스를 감시하며 메모리 변경 사항을 감지하는 기술.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;캐시 친화성(Cache Affinity)&lt;/code&gt;: 프로세스를 이전에 실행했던 CPU에서 계속 실행하여 캐시 적중률(Hit Rate)을 높이려는 성질.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SQMS (Single Queue Multiprocessor Scheduling)&lt;/code&gt;: 모든 작업을 하나의 글로벌 큐에서 관리하는 방식.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MQMS (Multi-Queue Multiprocessor Scheduling)&lt;/code&gt;: CPU마다 별도의 큐를 두어 관리하는 방식.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;워크 스틸링(Work Stealing)&lt;/code&gt;: MQMS에서 로드 불균형을 해소하기 위해, 한가한 CPU가 바쁜 CPU의 작업을 가져오는 기법.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched-multi.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 10: Multiprocessor Scheduling (Advanced)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 09. 비례 배분 스케줄링(Proportional Share): Lottery와 CFS</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-09-cpu-sched-lottery/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-09-cpu-sched-lottery/</guid><description>공정성(Fairness)을 목표로 하는 비례 배분(Proportional Share) 스케줄러를 다룹니다. 추첨 스케줄링(Lottery Scheduling), 보폭 스케줄링(Stride Scheduling), CFS(Completely Fair Scheduler)의 수식적 원리를 정리합니다.</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 『Operating Systems: Three Easy Pieces(OSTEP)』의 &lt;strong&gt;Scheduling: Proportional Share&lt;/strong&gt; 파트를 읽고 정리한 노트입니다.&lt;/p&gt;
&lt;p&gt;지난 장(MLFQ)까지는 반환 시간(Turnaround Time)이나 응답 시간(Response Time) 같은 &lt;strong&gt;성능 지표를 최적화&lt;/strong&gt;하는 데 집중했습니다. 이번 장에서는 관점을 조금 바꿔, &lt;strong&gt;각 작업에게 CPU를 얼마나 공정하게(Fair) 나누어 줄 것인가?&lt;/strong&gt; 를 고민하는 &lt;strong&gt;비례 배분(Proportional Share)&lt;/strong&gt; 스케줄러에 대해 알아봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 추첨 스케줄링 (Lottery Scheduling)&lt;/h2&gt;
&lt;p&gt;비례 배분을 구현하는 가장 직관적이고 오래된 아이디어 중 하나는 &lt;strong&gt;추첨(Lottery)&lt;/strong&gt; 입니다. (Waldspurger &amp;amp; Weihl, 1994)&lt;/p&gt;
&lt;h3&gt;1.1 기본 개념: 티켓(Tickets)&lt;/h3&gt;
&lt;p&gt;핵심은 &lt;strong&gt;티켓(Ticket)&lt;/strong&gt; 입니다. 티켓은 해당 작업이 받아야 할 &lt;strong&gt;자원의 몫(Share)&lt;/strong&gt; 을 나타냅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전체 티켓이 100장일 때, 프로세스 A가 75장, B가 25장을 가지고 있다면?&lt;/li&gt;
&lt;li&gt;A는 75%의 확률로, B는 25%의 확률로 CPU를 얻게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스케줄러는 매 타임 슬라이스마다 복권 추첨을 하듯 무작위로 숫자를 뽑고, 당첨된 티켓을 가진 프로세스를 실행합니다.&lt;/p&gt;
&lt;h3&gt;1.2 한계: 짧은 기간의 불공정성&lt;/h3&gt;
&lt;p&gt;추첨의 가장 큰 문제는 &lt;strong&gt;확률&lt;/strong&gt;에 의존한다는 점입니다.
작업이 길다면 대수의 법칙에 의해 결국 목표 비율(75:25)에 수렴하겠지만, &lt;strong&gt;실행 시간이 짧다면&lt;/strong&gt; 운에 따라 심각한 불균형(Unfairness)이 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/9_2.png&quot; alt=&quot;Figure 9.2 Lottery Fairness Study&quot; /&gt;
&lt;em&gt;Figure 9.2: 작업 길이가 짧을수록(좌측) 공정성이 낮고, 길수록(우측) 1에 수렴한다.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 보폭 스케줄링 (Stride Scheduling)&lt;/h2&gt;
&lt;p&gt;&quot;왜 굳이 랜덤을 써서 불확실성을 안고 가야 해?&quot;라는 의문에서 나온 결정론적(Deterministic) 방식입니다.&lt;/p&gt;
&lt;h3&gt;2.1 보폭(Stride)과 패스(Pass)&lt;/h3&gt;
&lt;p&gt;각 프로세스는 티켓 수에 반비례하는 &lt;strong&gt;보폭(Stride)&lt;/strong&gt; 값을 가집니다. 티켓이 많을수록 보폭은 작아집니다.&lt;/p&gt;
&lt;p&gt;$$Stride = \frac{\text{Large Constant}}{\text{Tickets}}$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pass&lt;/strong&gt;: 현재까지 프로세스가 이동한 거리 (CPU 사용량)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;알고리즘&lt;/strong&gt;: 현재 &lt;strong&gt;Pass 값이 가장 작은 프로세스&lt;/strong&gt;를 선택해 실행하고, 실행 후에는 그 프로세스의 Pass에 Stride를 더합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/9_3.png&quot; alt=&quot;Figure 9.3 Stride Scheduling Trace&quot; /&gt;
&lt;em&gt;Figure 9.3: 보폭 스케줄링의 정확한 실행 순서&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;2.2 장단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 언제나 정확한 비율로 CPU를 배분합니다. 짧은 기간에도 공정성이 보장됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: &lt;strong&gt;상태 관리(Global State)&lt;/strong&gt; 가 필요합니다. 새로운 프로세스가 중간에 들어오면 Pass 값을 얼마로 설정해야 할지(0으로 하면 독점하게 됨) 등 복잡한 문제가 생깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 리눅스 CFS (Completely Fair Scheduler)&lt;/h2&gt;
&lt;p&gt;현대 리눅스의 기본 스케줄러인 &lt;strong&gt;CFS&lt;/strong&gt;는 비례 배분 방식을 매우 효율적으로 구현한 사례입니다. 티켓 대신 &lt;strong&gt;vruntime(가상 실행 시간)&lt;/strong&gt; 이라는 개념을 사용합니다.&lt;/p&gt;
&lt;h3&gt;3.1 기본 동작: vruntime&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;모든 프로세스는 &lt;code&gt;vruntime&lt;/code&gt;을 가집니다.&lt;/li&gt;
&lt;li&gt;프로세스가 실행되는 동안 &lt;code&gt;vruntime&lt;/code&gt;은 실제 실행 시간만큼 증가합니다.&lt;/li&gt;
&lt;li&gt;CFS는 &lt;strong&gt;가장 작은 &lt;code&gt;vruntime&lt;/code&gt;을 가진 프로세스&lt;/strong&gt;를 다음에 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, &quot;가장 덜 실행된 녀석&quot;을 골라 실행시켜 균형(Fairness)을 맞추는 것입니다.&lt;/p&gt;
&lt;h3&gt;3.2 가중치와 타임 슬라이스 (Weight &amp;amp; Time Slice)&lt;/h3&gt;
&lt;p&gt;CFS는 우선순위(Unix &lt;code&gt;nice&lt;/code&gt; 값)를 &lt;strong&gt;가중치(Weight)&lt;/strong&gt; 로 변환하여 처리합니다.
우선순위가 높은 프로세스는 더 많은 타임 슬라이스를 받아야 합니다. CFS는 &lt;strong&gt;목표 응답 주기(&lt;code&gt;sched_latency&lt;/code&gt;)&lt;/strong&gt; 를 각 프로세스의 가중치 비율에 따라 나눕니다.&lt;/p&gt;
&lt;p&gt;$$time_slice_k = \frac{weight_k}{\sum_{i=0}^{n-1} weight_i} \cdot sched_latency$$&lt;/p&gt;
&lt;p&gt;예를 들어 A의 가중치가 3, B의 가중치가 1이고 &lt;code&gt;sched_latency&lt;/code&gt;가 48ms라면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A는 $$\frac{3}{4} \times 48 = 36ms$$&lt;/li&gt;
&lt;li&gt;B는 $$\frac{1}{4} \times 48 = 12ms$$
를 할당받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 vruntime의 증가 속도&lt;/h3&gt;
&lt;p&gt;또한 CFS는 가중치가 높은 프로세스의 &lt;code&gt;vruntime&lt;/code&gt;이 &lt;strong&gt;천천히 증가&lt;/strong&gt;하게 만듭니다. 그래야 스케줄러가 &quot;아직 덜 실행됐네?&quot;라고 판단하고 더 자주 뽑아주기 때문입니다.&lt;/p&gt;
&lt;p&gt;$$vruntime_i = vruntime_i + \frac{weight_0}{weight_i} \cdot runtime_i$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;weight_0&lt;/code&gt;: 기본 가중치 (nice 0일 때의 값, 보통 1024)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runtime_i&lt;/code&gt;: 실제 CPU를 사용한 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가중치(&lt;code&gt;weight_i&lt;/code&gt;)가 클수록 더해지는 값이 작아지므로, &lt;code&gt;vruntime&lt;/code&gt;이 느리게 자랍니다.&lt;/p&gt;
&lt;h3&gt;3.4 효율적인 자료구조: Red-Black Tree&lt;/h3&gt;
&lt;p&gt;수많은 프로세스 중에서 &lt;code&gt;vruntime&lt;/code&gt;이 가장 작은 녀석을 빠르게 찾기 위해, CFS는 리스트 대신 &lt;strong&gt;레드-블랙 트리(Red-Black Tree)&lt;/strong&gt; 를 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/9_5.png&quot; alt=&quot;Figure 9.5 CFS Red-Black Tree&quot; /&gt;
&lt;em&gt;Figure 9.5: vruntime을 키(Key)로 정렬된 레드-블랙 트리 구조&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;가장 왼쪽 노드(Min vruntime)를 선택하는 비용이 $$O(1)$$, 다시 삽입하는 비용이 $$O(\log N)$$으로 매우 효율적입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;이번 장에서는 성능 최적화가 아닌 &lt;strong&gt;비례 배분(Fair Share)&lt;/strong&gt; 을 위한 스케줄러들을 살펴봤습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Lottery Scheduling&lt;/strong&gt;: &lt;strong&gt;무작위성(Randomness)&lt;/strong&gt; 과 &lt;strong&gt;티켓&lt;/strong&gt;을 이용해 단순하고 유연하게 몫을 배분하지만, 단기적으로는 불공정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stride Scheduling&lt;/strong&gt;: 결정론적 알고리즘으로 정확한 배분을 보장하지만, 신규 작업 처리가 까다롭습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux CFS&lt;/strong&gt;: &lt;strong&gt;vruntime&lt;/strong&gt;과 &lt;strong&gt;RB-Tree&lt;/strong&gt;를 이용해 공정성과 효율성, 확장성을 모두 잡은 현대적인 스케줄러입니다. 특히 &lt;strong&gt;가중치 공식&lt;/strong&gt;을 통해 정교한 타임 슬라이스 분배를 구현했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;비례 배분(Proportional Share)&lt;/code&gt;: 반환/응답 시간 최적화보다 각 작업에 일정 비율의 CPU를 보장하는 것을 목표로 하는 스케줄링.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;티켓(Ticket)&lt;/code&gt;: 해당 프로세스가 받아야 할 자원의 몫을 나타내는 추상적인 단위.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;보폭(Stride)&lt;/code&gt;: 티켓 수의 역수. 보폭 스케줄링에서 프로세스가 한 번 실행될 때마다 증가하는 Pass 값의 크기.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vruntime(Virtual Runtime)&lt;/code&gt;: CFS에서 사용하는 가상 실행 시간. 우선순위(Weight)가 높을수록 실제 시간보다 느리게 증가함.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sched_latency&lt;/code&gt;: CFS가 모든 프로세스를 한 번씩 실행시키는 데 걸리는 목표 주기.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Red-Black Tree&lt;/code&gt;: CFS가 실행 대기 중인 프로세스들을 vruntime 순으로 정렬하여 관리하는 균형 이진 탐색 트리 자료구조.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched-lottery.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 9: Scheduling: Proportional Share&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 08. 스케줄링: 멀티 레벨 피드백 큐(Multi-Level Feedback Queue(MLFQ))</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-08-cpu-sched-mlfq/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-08-cpu-sched-mlfq/</guid><description>MLFQ(Multi-Level Feedback Queue)를 정리합니다. 작업의 길이를 미리 알지 못해도 반환 시간과 응답 시간을 동시에 잡는 방법과, 기아(Starvation) 및 게이밍(Gaming) 문제를 해결하는 과정을 다룹니다.</description><pubDate>Mon, 19 Jan 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 『Operating Systems: Three Easy Pieces(OSTEP)』의 &lt;strong&gt;Scheduling: The Multi-Level Feedback Queue(MLFQ)&lt;/strong&gt; 파트를 읽고 정리한 노트입니다.&lt;/p&gt;
&lt;p&gt;지난 7장에서 우리는 SJF/STCF(반환 시간 최적)와 RR(응답 시간 최적)이라는 두 가지 갈래를 봤습니다. 하지만 이들은 치명적인 전제가 필요했습니다. 바로 &lt;strong&gt;작업의 실행 시간을 미리 알고 있다&lt;/strong&gt; 는 것이죠.&lt;/p&gt;
&lt;p&gt;현실의 OS는 미래를 볼 수 없습니다. 이번 장에서는 &lt;strong&gt;정보가 없는 상태(without perfect knowledge)&lt;/strong&gt; 에서, 어떻게 &lt;strong&gt;반환 시간과 응답 시간&lt;/strong&gt;을 모두 잡을 수 있는지 알아봅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 핵심 질문: 정보 없이 스케줄링하기&lt;/h2&gt;
&lt;p&gt;우리가 원하는 이상적인 스케줄러의 조건은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;짧은 작업(Interactive Job)&lt;/strong&gt; 은 빨리 실행시켜 &lt;strong&gt;응답 시간(Response Time)&lt;/strong&gt; 을 줄이고 싶다. (RR처럼)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;긴 작업(CPU-bound Job)&lt;/strong&gt; 은 완료 시간을 줄여 &lt;strong&gt;반환 시간(Turnaround Time)&lt;/strong&gt; 을 최적화하고 싶다. (SJF처럼)&lt;/li&gt;
&lt;li&gt;단, &lt;strong&gt;OS는 작업이 짧은지 긴지 미리 알 수 없다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 문제를 해결하기 위해 &lt;strong&gt;MLFQ(Multi-Level Feedback Queue)&lt;/strong&gt; 는 &lt;strong&gt;과거의 행동(History)&lt;/strong&gt; 을 보고 미래를 예측하는 전략을 사용합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. MLFQ의 기본 구조와 규칙&lt;/h2&gt;
&lt;p&gt;MLFQ는 이름 그대로 &lt;strong&gt;여러 개의 큐(Multi-Level Queue)&lt;/strong&gt; 를 사용합니다. 각 큐는 &lt;strong&gt;우선순위(Priority)&lt;/strong&gt; 가 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/8_1.png&quot; alt=&quot;Figure 8.1 MLFQ Example&quot; /&gt;
&lt;em&gt;Figure 8.1: MLFQ의 기본 구조&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;기본 규칙 (Basic Rules)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rule 1&lt;/strong&gt;: 우선순위가 높은(A) 작업이 낮은(B) 작업보다 먼저 실행된다. (&lt;code&gt;Pri(A) &amp;gt; Pri(B)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 2&lt;/strong&gt;: 우선순위가 같으면, &lt;strong&gt;RR(Round Robin)&lt;/strong&gt; 방식으로 돌아가며 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 높은 우선순위 큐에 있는 작업들이 다 끝나야 낮은 큐의 작업이 실행됩니다. 그렇다면 우선순위는 어떻게 정할까요?&lt;/p&gt;
&lt;h3&gt;우선순위 조정 (Priority Adjustment)&lt;/h3&gt;
&lt;p&gt;MLFQ는 작업이 처음 들어오면 일단 &lt;strong&gt;&quot;짧은 작업일 것이다&quot;&lt;/strong&gt; 라고 가정하고 최고 우선순위를 부여합니다. 그리고 실행하면서 &lt;strong&gt;CPU를 많이 쓰면 우선순위를 깎습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/8_2.png&quot; alt=&quot;Figure 8.2 Long-running Job Over Time&quot; /&gt;
&lt;em&gt;Figure 8.2: 긴 작업은 시간이 지남에 따라 아래 큐로 떨어진다&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rule 3&lt;/strong&gt;: 작업이 시스템에 진입하면, &lt;strong&gt;가장 높은 우선순위(Top Queue)&lt;/strong&gt; 에 둔다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 4 (초기 버전)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;작업이 할당된 타임 슬라이스(Time Slice)를 다 쓰면, 우선순위를 낮춘다(강등).&lt;/li&gt;
&lt;li&gt;작업이 타임 슬라이스를 다 쓰기 전에 CPU를 양보(I/O 등)하면, 우선순위를 &lt;strong&gt;유지&lt;/strong&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/8_3.png&quot; alt=&quot;Figure 8.3 Along Came An Interactive Job&quot; /&gt;
&lt;em&gt;Figure 8.3: 긴 작업(검은색) 실행 중 짧은 작업(회색)이 들어오면 높은 우선순위에서 빨리 처리된다 (SJF 근사)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이 방식대로면 짧은 작업은 빨리 끝나서 나가고, 긴 작업은 천천히 아래 큐로 내려가게 됩니다. &lt;strong&gt;SJF를 흉내&lt;/strong&gt; 내는 것이죠.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 문제점과 해결책&lt;/h2&gt;
&lt;p&gt;하지만 위의 기본 규칙만으로는 심각한 문제들이 발생합니다.&lt;/p&gt;
&lt;h3&gt;3.1 문제 1: 기아 상태 (Starvation)&lt;/h3&gt;
&lt;p&gt;시스템에 짧은 대화형 작업(Interactive Job)이 끊임없이 들어오면 어떻게 될까요?
Rule 1에 의해 높은 우선순위 큐만 계속 실행되므로, 낮은 우선순위에 있는 긴 작업들은 CPU를 전혀 받지 못하는 &lt;strong&gt;기아(Starvation)&lt;/strong&gt; 상태에 빠집니다.&lt;/p&gt;
&lt;h4&gt;해결책: 주기적인 우선순위 상향 (Priority Boost)&lt;/h4&gt;
&lt;p&gt;주기적으로 모든 작업을 최상위 큐로 올려버리면 해결됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/8_4.png&quot; alt=&quot;Figure 8.4 Priority Boost&quot; /&gt;
&lt;em&gt;Figure 8.4: (좌) 부스트 없음 - 긴 작업(검은색) 기아 발생 / (우) 부스트 있음 - 주기적으로 실행됨&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rule 5 (Priority Boost)&lt;/strong&gt;: 일정 시간 &lt;strong&gt;S&lt;/strong&gt;가 지나면, 시스템의 &lt;strong&gt;모든 작업을 최상위 큐로 이동&lt;/strong&gt;시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 하면 두 가지가 해결됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;긴 작업도 주기적으로 실행 기회를 보장받습니다 (기아 해결).&lt;/li&gt;
&lt;li&gt;CPU 위주 작업이었다가 대화형으로 바뀐 작업도 다시 높은 우선순위를 얻을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 문제 2: 스케줄러 속이기 (Gaming the Scheduler)&lt;/h3&gt;
&lt;p&gt;초기 Rule 4에는 허점이 있습니다.
&lt;em&gt;&quot;타임 슬라이스를 다 쓰기 전에 양보하면 우선순위 유지&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;만약 어떤 악의적인 프로그램이 타임 슬라이스가 10ms일 때, &lt;strong&gt;9.9ms만 쓰고 0.1ms 동안 I/O를 하는 행위를 반복&lt;/strong&gt;한다면?
이 작업은 CPU를 거의 독점하면서도 계속 최상위 큐에 머물게 됩니다. 이를 &lt;strong&gt;게이밍(Gaming)&lt;/strong&gt; 이라고 합니다.&lt;/p&gt;
&lt;h4&gt;해결책: 더 나은 측정 (Better Accounting)&lt;/h4&gt;
&lt;p&gt;각 단계에서 &lt;strong&gt;총 사용 시간&lt;/strong&gt;을 누적해서 기록해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/8_5.png&quot; alt=&quot;Figure 8.5 Gaming Tolerance&quot; /&gt;
&lt;em&gt;Figure 8.5: (좌) 게이밍 허용 - 꼼수 쓰는 작업(회색)이 CPU 독점 / (우) 게이밍 방지 - 총 사용량을 채우면 강등됨&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rule 4 (개정판)&lt;/strong&gt;: 해당 단계에서 사용한 &lt;strong&gt;CPU 시간의 총합(Allotment)&lt;/strong&gt; 을 다 쓰면, I/O를 했든 안 했든 우선순위를 낮춘다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 꼼수를 부려도 결국 할당량을 채우면 가차 없이 강등됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 튜닝과 파라미터 (Tuning and Parameters)&lt;/h2&gt;
&lt;p&gt;MLFQ를 실제로 구현하려면 수많은 상수(Parameter)를 정해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;큐의 개수&lt;/strong&gt;: 몇 단계로 나눌 것인가?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;타임 슬라이스 크기&lt;/strong&gt;: 상위 큐는 응답성이 중요하니 짧게(10ms), 하위 큐는 처리량이 중요하니 길게(100ms) 주는 것이 일반적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부스트 주기 (S)&lt;/strong&gt;: 너무 길면 기아 상태가 생기고, 너무 짧으면 대화형 작업의 응답성이 떨어집니다. 이를 &quot;Voo-doo Constants(주술적 상수)&quot;라고 부르기도 합니다. (정답이 없고 튜닝이 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/8_6.png&quot; alt=&quot;Figure 8.6 Lower Priority Longer Quanta&quot; /&gt;
&lt;em&gt;Figure 8.6: 하위 큐로 갈수록 타임 슬라이스를 길게 설정한 예&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Solaris 같은 실제 OS는 테이블 형태로 이 값들을 관리하여 관리자가 튜닝할 수 있게 합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 요약 (Summary)&lt;/h2&gt;
&lt;p&gt;우리는 작업의 정보를 모르는 상태에서 스케줄링을 해야 했습니다. &lt;strong&gt;MLFQ&lt;/strong&gt;는 &lt;strong&gt;&quot;과거는 미래를 보여준다(Learn from History)&quot;&lt;/strong&gt; 는 철학으로 이 문제를 해결했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;최종 MLFQ 규칙:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rule 1&lt;/strong&gt;: &lt;code&gt;Priority(A) &amp;gt; Priority(B)&lt;/code&gt;이면 A 실행.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 2&lt;/strong&gt;: &lt;code&gt;Priority(A) == Priority(B)&lt;/code&gt;이면 RR로 실행.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 3&lt;/strong&gt;: 새 작업은 최상위 큐에 배치.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 4&lt;/strong&gt;: 한 단계에서 할당 시간(Allotment)을 다 쓰면 강등 (게이밍 방지).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 5&lt;/strong&gt;: 주기(S)마다 모든 작업을 최상위로 리셋 (부스트).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이로써 MLFQ는 짧은 작업에는 &lt;strong&gt;빠른 응답성&lt;/strong&gt;을, 긴 작업에는 &lt;strong&gt;공정한 진행&lt;/strong&gt;을 보장하며 현대 OS 스케줄러의 근간이 되었습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MLFQ (Multi-Level Feedback Queue)&lt;/code&gt;: 여러 개의 우선순위 큐를 두고, 작업의 실행 패턴(CPU 사용량)에 따라 큐를 이동시키며 스케줄링하는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;피드백 (Feedback)&lt;/code&gt;: 작업의 과거 실행 이력(짧게 쓰고 반환했는지, 오래 썼는지)을 바탕으로 우선순위를 조정하는 메커니즘.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;기아 상태 (Starvation)&lt;/code&gt;: 우선순위가 낮은 작업이 높은 우선순위 작업들에 밀려 오랫동안 CPU를 할당받지 못하는 현상.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;우선순위 상향 (Priority Boost)&lt;/code&gt;: 기아 상태를 방지하기 위해 주기적으로 모든 작업의 우선순위를 최상위로 올리는 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;게이밍 (Gaming)&lt;/code&gt;: 스케줄러의 허점(예: 타임 슬라이스 직전에 CPU 반환)을 악용하여 부당하게 많은 CPU 시간을 차지하려는 행위.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;할당량 (Allotment)&lt;/code&gt;: 해당 우선순위 단계에서 머무를 수 있는 CPU 사용 시간의 총량. 이를 소진하면 아래 단계로 강등됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Voo-doo Constants&lt;/code&gt;: 최적의 값을 찾기 어렵고 상황에 따라 달라지는 튜닝 파라미터들(부스트 주기, 타임 슬라이스 길이 등)을 일컫는 말.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched-mlfq.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 8: Scheduling: The Multi-Level Feedback Queue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 07. 스케줄링: 개요(Scheduling: Introduction)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-07-cpu-sched/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-07-cpu-sched/</guid><description>CPU 스케줄링 정책의 기초. FIFO, SJF, STCF, RR 알고리즘의 발전 과정과 Turnaround Time vs Response Time의 트레이드오프를 정리합니다.</description><pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이전 장(LDE)에서는 &lt;strong&gt;어떻게(Mechanism)&lt;/strong&gt; CPU를 가상화하는지 다뤘다면, 이번 장부터는 &lt;strong&gt;무엇을(Policy)&lt;/strong&gt; 실행할지 결정하는 스케줄링 정책에 대해 다룹니다.&lt;/p&gt;
&lt;p&gt;OSTEP 7장에서는 가장 단순한 알고리즘(FIFO)에서 시작해 현실적인 문제들을 하나씩 해결해가며 라운드 로빈(RR)까지 발전하는 과정을 설명합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 워크로드 가정 (Workload Assumptions)&lt;/h2&gt;
&lt;p&gt;가장 이상적인(그리고 비현실적인) 상황을 가정하고 하나씩 제약을 풀어봅시다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;모든 작업은 실행 시간이 같다.&lt;/li&gt;
&lt;li&gt;모든 작업은 동시에 도착한다.&lt;/li&gt;
&lt;li&gt;작업은 끝날 때까지 멈추지 않는다 (비선점, Non-preemptive).&lt;/li&gt;
&lt;li&gt;모든 작업은 CPU만 쓴다 (I/O 없음).&lt;/li&gt;
&lt;li&gt;각 작업의 실행 시간을 미리 알고 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 평가 기준 (Scheduling Metrics)&lt;/h2&gt;
&lt;p&gt;스케줄러의 좋고 나쁨을 무엇으로 판단할까요? 여기서는 두 가지 상반된 지표를 사용합니다.&lt;/p&gt;
&lt;h3&gt;2.1 반환 시간 (Turnaround Time)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;작업이 시스템에 도착해서 끝날 때까지 걸린 시간입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능(Performance)&lt;/strong&gt; 위주의 지표입니다.&lt;/li&gt;
&lt;li&gt;$$T_{turnaround} = T_{completion} - T_{arrival}$$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 응답 시간 (Response Time)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;작업이 도착해서 &lt;strong&gt;처음으로 스케줄&lt;/strong&gt; 될 때까지 걸린 시간입니다.&lt;/li&gt;
&lt;li&gt;대화형 시스템(터미널, UI)에서의 &lt;strong&gt;반응성(Interactivity)&lt;/strong&gt; 지표입니다.&lt;/li&gt;
&lt;li&gt;$$T_{response} = T_{firstrun} - T_{arrival}$$&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 알고리즘의 진화&lt;/h2&gt;
&lt;h3&gt;3.1 FIFO (First In, First Out)&lt;/h3&gt;
&lt;p&gt;가장 단순한 선착순 방식입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 단순하고 구현이 쉽습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문제점&lt;/strong&gt;: &lt;strong&gt;Convoy Effect (호위 효과)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;만약 100초 걸리는 작업 A가 먼저 오고, 10초 걸리는 B, C가 바로 뒤에 왔다면?&lt;/li&gt;
&lt;li&gt;B와 C는 A가 끝날 때까지 하염없이 기다려야 합니다. 평균 반환 시간이 급격히 나빠집니다.&lt;/li&gt;
&lt;li&gt;마치 마트 계산대에서 카트 가득 채운 사람 뒤에, 우유 하나 든 사람이 서 있는 꼴입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 SJF (Shortest Job First)&lt;/h3&gt;
&lt;p&gt;&quot;가장 짧은 작업을 먼저 실행하자&quot;는 아이디어입니다. (가정 1 완화: 실행 시간이 다름)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;해결&lt;/strong&gt;: A(100초), B(10초), C(10초)가 동시에 도착하면, SJF는 B -&amp;gt; C -&amp;gt; A 순서로 실행합니다. Convoy Effect를 해결하고 평균 반환 시간을 최적화합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문제점&lt;/strong&gt;: &lt;strong&gt;늦은 도착 (Late Arrival)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;A(100초)가 $t=0$에 도착해서 이미 실행 중인데, $t=10$에 B(10초)가 도착한다면?&lt;/li&gt;
&lt;li&gt;SJF는 비선점(Non-preemptive) 방식이므로 A가 끝날 때까지 B는 기다려야 합니다. 다시 Convoy Effect가 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 STCF (Shortest Time-to-Completion First)&lt;/h3&gt;
&lt;p&gt;SJF에 &lt;strong&gt;선점(Preemption)&lt;/strong&gt; 기능을 추가합니다. (가정 3 완화: 중간에 멈출 수 있음)
새로운 작업이 도착하면, &quot;현재 실행 중인 작업의 남은 시간&quot;과 &quot;새 작업의 시간&quot;을 비교해서 더 짧은 것을 선택합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;해결&lt;/strong&gt;: A가 실행 중이어도 B가 도착하면, B가 더 짧으므로 A를 잠시 멈추고(Context Switch) B를 먼저 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성과&lt;/strong&gt;: 반환 시간(Turnaround Time) 측면에서는 거의 최적(Optimal)에 가깝습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;새로운 문제점&lt;/strong&gt;: &lt;strong&gt;응답 시간(Response Time)이 최악&lt;/strong&gt;입니다.
&lt;ul&gt;
&lt;li&gt;작업 3개가 동시에 도착했다면, 세 번째 작업은 앞의 두 개가 다 끝날 때까지 한 번도 실행되지 못합니다. 타자기를 치는데 글자가 10초 뒤에 뜬다면 아무도 그 컴퓨터를 쓰지 않을 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 Round Robin (RR)&lt;/h3&gt;
&lt;p&gt;응답 시간을 해결하기 위해 &lt;strong&gt;타임 슬라이스(Time Slice)&lt;/strong&gt; 개념을 도입합니다. 작업을 끝까지 실행하는 게 아니라, 일정 시간(Quantum)만큼만 실행하고 다음 작업으로 넘어갑니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 타임 슬라이스가 10ms라면, A(10ms) -&amp;gt; B(10ms) -&amp;gt; C(10ms) -&amp;gt; A... 순으로 돕니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 모든 작업이 짧은 시간 안에 한 번씩은 CPU를 맛볼 수 있어 &lt;strong&gt;응답 시간이 획기적으로 개선&lt;/strong&gt;됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: &lt;strong&gt;반환 시간은 최악&lt;/strong&gt;이 됩니다.
&lt;ul&gt;
&lt;li&gt;A, B, C가 각각 5초씩 걸린다면, RR은 이들을 조금씩 번갈아 실행하므로 셋 다 거의 15초가 되어서야 동시에 끝납니다. (차라리 하나씩 끝내는 게 반환 시간엔 유리합니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade-off&lt;/strong&gt;: 타임 슬라이스가 너무 짧으면 문맥 교환(Context Switch) 오버헤드가 커지고, 너무 길면 응답성이 떨어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 현실적인 고려: I/O 작업 (Incorporating I/O)&lt;/h2&gt;
&lt;p&gt;지금까지는 CPU만 쓴다고 가정(가정 4)했지만, 실제 프로그램은 디스크 I/O를 빈번하게 합니다.
프로세스 A가 CPU를 10ms 쓰고 10초 동안 디스크를 읽는다면, 그 10초 동안 CPU를 놀게 두는 것은 낭비입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;해결&lt;/strong&gt;: A가 I/O를 요청하면 A를 &lt;strong&gt;Blocked&lt;/strong&gt; 상태로 만들고, 그동안 B를 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 완료 시&lt;/strong&gt;: 인터럽트가 발생하면 A를 다시 &lt;strong&gt;Ready&lt;/strong&gt; 상태로 옮깁니다.&lt;/li&gt;
&lt;li&gt;이를 통해 자원을 중첩(Overlap)하여 효율을 극대화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;요약 (Summary)&lt;/h2&gt;
&lt;p&gt;우리는 두 가지 상반된 목표를 가진 스케줄러들을 살펴봤습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;알고리즘&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FIFO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;선착순&lt;/td&gt;
&lt;td&gt;단순함&lt;/td&gt;
&lt;td&gt;Convoy Effect (긴 작업 뒤에 짧은 작업)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SJF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;짧은 작업 먼저&lt;/td&gt;
&lt;td&gt;반환 시간 좋음&lt;/td&gt;
&lt;td&gt;늦게 도착하면 소용 없음 (비선점)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;STCF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;남은 시간 짧은 것 먼저 (선점)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;반환 시간 최적&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;응답 시간 나쁨&lt;/strong&gt; (기다림)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;타임 슬라이싱&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;응답 시간 최적&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;반환 시간 나쁨&lt;/strong&gt; (다 같이 늦게 끝남)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;반환 시간(성능)을 잡으려니 응답성이 떨어지고, 응답성을 잡으려니 성능이 떨어진다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아직 해결하지 못한 마지막 가정(가정 5)이 있습니다. &lt;strong&gt;&quot;OS는 작업의 길이를 미리 알 수 없다.&quot;&lt;/strong&gt;
미래를 모르는 상태에서 어떻게 STCF처럼 반환 시간을 줄이면서 RR처럼 응답성도 챙길 수 있을까요?
다음 장에서 다룰 &lt;strong&gt;MLFQ(Multi-Level Feedback Queue)&lt;/strong&gt; 가 그 해답이 될 것입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched.pdf&quot;&gt;Operating Systems: Three Easy Pieces - Chapter 7: Scheduling: Introduction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 06. 제한적 직접 실행 (Limited Direct Execution (LDE))</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-06-cpu-mechanisms/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-06-cpu-mechanisms/</guid><description>OSTEP 6장: CPU 가상화를 구현하면서 성능과 제어권을 동시에 잡는 기법인 제한적 직접 실행(LDE)을 정리합니다. User/Kernel Mode, System Call, Context Switch의 동작 원리를 다룹니다.</description><pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 『Operating Systems: Three Easy Pieces(OSTEP)』의 &lt;strong&gt;Mechanism: Limited Direct Execution(제한적 직접 실행)&lt;/strong&gt; 파트를 읽고 정리한 노트입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;실습 준비: 로컬에서 바로 확인할 것들&lt;/h3&gt;
&lt;p&gt;이 글의 실습은 별도의 커널 소스나 복잡한 설정 없이, 일반적인 리눅스 환경에서 &lt;code&gt;gcc&lt;/code&gt;만 있으면 바로 테스트해 볼 수 있도록 구성했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컴파일러&lt;/strong&gt;: &lt;code&gt;gcc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시간 측정&lt;/strong&gt;: &lt;code&gt;clock_gettime(CLOCK_MONOTONIC)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(선택) CPU 고정&lt;/strong&gt;: &lt;code&gt;taskset&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 이 장의 핵심 질문: 성능과 제어를 동시에&lt;/h2&gt;
&lt;p&gt;CPU 가상화의 기본 아이디어는 단순합니다. 하나의 CPU를 여러 프로세스가 번갈아 가며 사용하게 하여(Time Sharing), 마치 동시에 실행되는 것처럼 보이게 하는 것입니다.&lt;/p&gt;
&lt;p&gt;하지만 이를 구현하려면 두 가지 상충하는 목표를 해결해야 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;성능(Performance)&lt;/strong&gt;: 가상화로 인해 시스템이 느려지면 안 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제어(Control)&lt;/strong&gt;: OS가 CPU에 대한 통제권을 잃어서는 안 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;프로그램을 CPU에서 바로 실행하는 &lt;strong&gt;직접 실행(Direct Execution)&lt;/strong&gt; 방식은 빠르지만, 제한을 두지 않으면 OS가 시스템을 제어할 수 없게 됩니다. 따라서 OSTEP은 다음 두 가지 문제를 제기합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;(문제 1) 제한된 연산&lt;/strong&gt;: 사용자 프로그램이 디스크 I/O 같은 특권 명령을 수행해야 할 때는 어떻게 하는가?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(문제 2) 프로세스 전환&lt;/strong&gt;: 사용자 프로그램이 실행 중일 때(OS가 멈춰 있을 때) 어떻게 OS가 다시 제어권을 가져와 프로세스를 바꿀 것인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 장에서는 이 두 마리 토끼를 잡기 위한 기법인 &lt;strong&gt;제한적 직접 실행(Limited Direct Execution, LDE)&lt;/strong&gt; 을 설명합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 기본 원리: 제한적 직접 실행(Limited Direct Execution)&lt;/h2&gt;
&lt;p&gt;LDE의 핵심 메시지는 다음과 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&quot;사용자 코드는 CPU에서 직접 실행하여 성능을 얻되, 위험한 순간에는 하드웨어 메커니즘을 통해 커널이 개입한다.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&quot;Limited(제한적)&quot;라는 말은 OS가 &lt;strong&gt;커널 진입 지점&lt;/strong&gt;과 &lt;strong&gt;복귀 방식&lt;/strong&gt;을 엄격하게 통제한다는 뜻입니다.&lt;/p&gt;
&lt;h3&gt;프로그램 시작 프로토콜&lt;/h3&gt;
&lt;p&gt;OS가 프로그램을 실행하는 과정은 대략 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;프로세스 목록에 항목 생성 및 메모리 할당&lt;/li&gt;
&lt;li&gt;디스크에서 프로그램 코드/데이터 로드&lt;/li&gt;
&lt;li&gt;스택(&lt;code&gt;argc&lt;/code&gt;, &lt;code&gt;argv&lt;/code&gt;) 초기화&lt;/li&gt;
&lt;li&gt;레지스터 정리 후 &lt;code&gt;main()&lt;/code&gt;으로 점프 (이후 제어권은 하드웨어/프로세스로 넘어감)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 문제점 1: 제한된 연산(Restricted Operations)&lt;/h2&gt;
&lt;p&gt;프로그램이 파일을 읽거나, 다른 프로세스를 생성하는 작업은 시스템 전체에 영향을 줍니다. 사용자 프로그램이 이를 마음대로 하게 두면 시스템 보호가 불가능해집니다.&lt;/p&gt;
&lt;h3&gt;3.1 User Mode vs Kernel Mode&lt;/h3&gt;
&lt;p&gt;이를 해결하기 위해 하드웨어는 실행 모드를 구분합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User Mode (사용자 모드)&lt;/strong&gt;: 애플리케이션이 동작하는 모드입니다. 하드웨어 리소스에 대한 접근이 제한됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel Mode (커널 모드)&lt;/strong&gt;: OS가 동작하는 모드입니다. 모든 특권 명령(Privileged Instruction)을 실행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;사용자 모드에서 특권 명령을 시도하면 하드웨어는 &lt;strong&gt;예외(Exception)&lt;/strong&gt; 를 발생시키고, OS는 해당 프로세스를 종료시킵니다.&lt;/p&gt;
&lt;h3&gt;3.2 시스템 콜(System Call)과 Trap&lt;/h3&gt;
&lt;p&gt;사용자 프로그램이 커널의 기능을 사용하고 싶을 때(예: &lt;code&gt;read()&lt;/code&gt;), &lt;strong&gt;시스템 콜&lt;/strong&gt;을 사용합니다. 동작 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;프로그램이 &lt;strong&gt;Trap&lt;/strong&gt;이라는 특수 명령어를 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trap&lt;/strong&gt;: 권한이 커널 모드로 격상되고, 미리 정의된 &lt;strong&gt;Trap Handler&lt;/strong&gt;로 점프합니다.&lt;/li&gt;
&lt;li&gt;OS가 커널 모드에서 요청을 처리합니다.&lt;/li&gt;
&lt;li&gt;작업 완료 후 &lt;strong&gt;Return-from-trap&lt;/strong&gt; 명령어를 실행하여 사용자 모드로 복귀합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::note
&lt;strong&gt;왜 시스템 콜은 함수 호출처럼 보일까?&lt;/strong&gt;
C 코드의 &lt;code&gt;open()&lt;/code&gt;은 일반 함수처럼 보이지만, 실제로는 C 라이브러리가 그 안에서 어셈블리 명령(&lt;code&gt;trap&lt;/code&gt; 등)을 실행해 커널 경계를 넘는 작업을 대신 처리해주기 때문입니다.
:::&lt;/p&gt;
&lt;h3&gt;3.3 트랩 테이블(Trap Table)&lt;/h3&gt;
&lt;p&gt;보안상 가장 중요한 것은 &lt;strong&gt;사용자 코드가 커널 내부의 어디로 점프할지 정할 수 없다&lt;/strong&gt;는 점입니다. 만약 임의의 주소로 점프할 수 있다면 악성 코드가 커널을 장악할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 OS는 부팅 시점에 **트랩 테이블(Trap Table)**을 초기화하여 하드웨어에게 &quot;시스템 콜이나 예외가 발생하면 실행할 코드의 주소&quot;를 미리 알려줍니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 문제점 2: 프로세스 간 전환(Switching Between Processes)&lt;/h2&gt;
&lt;p&gt;프로세스 A가 실행 중이라면 OS는 실행 중이 아닙니다. OS가 실행되지 않으면 프로세스를 바꿀(스케줄링) 수도 없습니다. OS는 어떻게 다시 CPU 제어권을 가져올까요?&lt;/p&gt;
&lt;h3&gt;4.1 협조적(Cooperative) 방식&lt;/h3&gt;
&lt;p&gt;과거에는 프로세스가 주기적으로 &lt;code&gt;yield()&lt;/code&gt; 시스템 콜을 호출하여 제어권을 넘겨줄 것이라 믿었습니다. 하지만 프로세스가 무한 루프에 빠지면 재부팅밖에 답이 없는 치명적 단점이 있습니다.&lt;/p&gt;
&lt;h3&gt;4.2 선점형(Preemptive) 방식: 타이머 인터럽트&lt;/h3&gt;
&lt;p&gt;OS가 확실하게 주도권을 쥐기 위해 &lt;strong&gt;타이머 인터럽트(Timer Interrupt)&lt;/strong&gt; 가 도입되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부팅 시 OS가 타이머를 설정합니다.&lt;/li&gt;
&lt;li&gt;수 밀리초마다 하드웨어가 인터럽트를 발생시켜 현재 프로세스를 중단합니다.&lt;/li&gt;
&lt;li&gt;미리 등록된 OS의 &lt;strong&gt;Interrupt Handler&lt;/strong&gt;가 실행됩니다.&lt;/li&gt;
&lt;li&gt;이로써 OS는 주기적으로 CPU 제어권을 되찾을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 컨텍스트 스위치(Context Switch)&lt;/h3&gt;
&lt;p&gt;OS가 제어권을 얻었을 때, 스케줄러가 현재 프로세스(A)에서 다른 프로세스(B)로 전환하기로 결정하면 &lt;strong&gt;컨텍스트 스위치&lt;/strong&gt;가 일어납니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;현재(A) 상태 저장&lt;/strong&gt;: A의 범용 레지스터, PC 등을 A의 커널 스택(혹은 구조체)에 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다음(B) 상태 복원&lt;/strong&gt;: B의 커널 스택에서 레지스터 값을 복원합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스택 전환(Switch Stack)&lt;/strong&gt;: 커널 스택 포인터를 A에서 B로 변경합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return-from-trap&lt;/code&gt;을 실행하면 B의 문맥으로 돌아가 B가 실행됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 한 장으로 흐름 잡기: LDE 타임라인&lt;/h2&gt;
&lt;p&gt;전체 흐름을 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;부팅 (Kernel Mode)&lt;/strong&gt;: 트랩 테이블 초기화, 타이머 시작.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스 A 실행 (User Mode)&lt;/strong&gt;: &lt;code&gt;return-from-trap&lt;/code&gt;으로 시작.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이벤트 발생 (Timer Interrupt)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;하드웨어&lt;/strong&gt;: A의 레지스터를 A의 커널 스택에 저장 → 커널 모드 진입 → Trap Handler로 점프.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS (Trap Handler)&lt;/strong&gt;: 스케줄러가 B로 전환 결정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS (Switch)&lt;/strong&gt;: &lt;code&gt;switch()&lt;/code&gt; 루틴이 A의 상태를 PCB에 저장하고, B의 상태를 복원.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스 B 실행 (User Mode)&lt;/strong&gt;: &lt;code&gt;return-from-trap&lt;/code&gt;으로 B 실행 재개.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 실습: 비용 측정하기 (C Code)&lt;/h2&gt;
&lt;p&gt;이론적인 &quot;비용(Cost)&quot;을 실제 코드로 체감해 봅시다.&lt;/p&gt;
&lt;h3&gt;6.1 시스템 콜 비용 측정 (&lt;code&gt;getpid&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;getpid()&lt;/code&gt;는 &lt;code&gt;trap&lt;/code&gt; → &lt;code&gt;kernel&lt;/code&gt; → &lt;code&gt;return-from-trap&lt;/code&gt;을 거치는 가장 가벼운 시스템 콜 중 하나입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# syscall_cost.c

#define _GNU_SOURCE
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;time.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

static inline uint64_t nsec_now(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &amp;amp;ts);
    // 초와 나노초를 합쳐서 반환
    return (uint64_t)ts.tv_sec * 1000000000ull + 
           (uint64_t)ts.tv_nsec;
}

int main(int argc, char **argv) {
    // 인자로 반복 횟수를 받음 (기본값: 10,000,000)
    const long iters = (argc &amp;gt; 1) ? atol(argv[1]) : 10000000L;
    volatile pid_t sink = 0; // 컴파일러 최적화 방지

    uint64_t t0 = nsec_now();
    for (long i = 0; i &amp;lt; iters; i++) {
        sink ^= getpid(); // 시스템 콜 반복 호출
    }
    uint64_t t1 = nsec_now();

    double avg = (double)(t1 - t0) / (double)iters;
    printf(&quot;iters=%ld, avg cost=%.2f ns\n&quot;, iters, avg);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/01_syscall_cost_run.png&quot; alt=&quot;01. syscall_cost 실행&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.2 컨텍스트 스위치 비용 측정 (Pipe Ping-Pong)&lt;/h3&gt;
&lt;p&gt;두 프로세스가 파이프를 통해 데이터를 주고받으며 강제로 &lt;code&gt;block&lt;/code&gt;과 &lt;code&gt;unblock&lt;/code&gt;을 반복하게 하여 전환 비용을 측정합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ctxswitch_pingpong.c

#define _GNU_SOURCE
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;time.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;

// 시간 측정 함수 (syscall_cost.c와 동일)
static inline uint64_t nsec_now(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &amp;amp;ts);
    return (uint64_t)ts.tv_sec * 1000000000ull + 
           (uint64_t)ts.tv_nsec;
}

int main(int argc, char **argv) {
    const long rounds = (argc &amp;gt; 1) ? atol(argv[1]) : 100000L;

    int p2c[2], c2p[2]; // 부모-&amp;gt;자식, 자식-&amp;gt;부모 파이프
    if (pipe(p2c) != 0 || pipe(c2p) != 0) {
        perror(&quot;pipe&quot;);
        return 1;
    }

    pid_t pid = fork();
    if (pid &amp;lt; 0) {
        perror(&quot;fork&quot;);
        return 1;
    }

    if (pid == 0) {
        // Child Process
        close(p2c[1]); close(c2p[0]);
        char b;
        for (long i = 0; i &amp;lt; rounds; i++) {
            // 부모가 쓸 때까지 대기 (Context Switch 발생 지점)
            if (read(p2c[0], &amp;amp;b, 1) != 1) _exit(2);
            if (write(c2p[1], &amp;amp;b, 1) != 1) _exit(3);
        }
        _exit(0);
    }

    // Parent Process
    close(p2c[0]); close(c2p[1]);

    char b = &apos;x&apos;;
    uint64_t t0 = nsec_now();
    for (long i = 0; i &amp;lt; rounds; i++) {
        if (write(p2c[1], &amp;amp;b, 1) != 1) return 4;
        // 자식이 쓸 때까지 대기 (Context Switch 발생 지점)
        if (read(c2p[0], &amp;amp;b, 1) != 1) return 5;
    }
    uint64_t t1 = nsec_now();

    int st;
    waitpid(pid, &amp;amp;st, 0);

    // 한 라운드에 (A-&amp;gt;B, B-&amp;gt;A) 두 번의 전환이 발생함에 유의
    double avg = (double)(t1 - t0) / (double)(rounds * 2);
    printf(&quot;rounds=%ld, avg switch cost=%.2f ns\n&quot;, rounds, avg);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/02_ctxswitch_run.png&quot; alt=&quot;02. ctxswitch 실행&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
정확한 측정을 위해 &lt;code&gt;taskset -c 0 ./ctxswitch_pingpong&lt;/code&gt; 처럼 특정 코어에 바인딩하여 실행하는 것이 좋습니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 마무리&lt;/h2&gt;
&lt;p&gt;이 장은 철저히 &lt;strong&gt;메커니즘(Mechanism)&lt;/strong&gt; 을 다뤘습니다. 어떻게 CPU를 뺏고, 어떻게 돌려줄 것인가? 에 대한 답은 &lt;strong&gt;Trap, Timer Interrupt, Context Switch&lt;/strong&gt;였습니다.&lt;/p&gt;
&lt;p&gt;이제 그렇다면 누구를 실행할 것인가? 라는 &lt;strong&gt;정책(Policy)&lt;/strong&gt; 의 영역이 남았습니다. 다음 글부터는 스케줄링 알고리즘에 대해 다룹니다.&lt;/p&gt;
&lt;h2&gt;용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Limited Direct Execution (LDE)&lt;/code&gt;: 직접 실행으로 성능을 얻고, OS 개입으로 제어를 얻는 가상화 기법.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Trap&lt;/code&gt; / &lt;code&gt;Return-from-trap&lt;/code&gt;: 유저 모드와 커널 모드를 오가는 핵심 명령어.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Trap Table&lt;/code&gt;: 부팅 시 OS가 설정하는 핸들러 주소 목록 (보안의 핵심).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Context Switch&lt;/code&gt;: 레지스터와 스택을 교체하여 실행 중인 프로세스를 변경하는 작업.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 05. 프로세스 API (Process API)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-05-process-api/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-05-process-api/</guid><description>OSTEP ch.05 Interlude: Process API를 읽고 fork/exec/wait 조합의 의도, 셸 리다이렉션/파이프, 시그널 기반 프로세스 제어를 정리합니다.</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 『Operating Systems: Three Easy Pieces(OSTEP)』의 Interlude: Process API를 읽고 정리한 노트입니다.&lt;br /&gt;
영문 PDF와 한국어 번역 PDF를 함께 보면서, 같은 내용이 어떤 표현과 흐름으로 설명되는지도 같이 체크했습니다.&lt;/p&gt;
&lt;p&gt;이 파트의 핵심은 다음으로 정리됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNIX는 프로세스 생성, 실행, 동기화를 &lt;code&gt;fork()&lt;/code&gt; + &lt;code&gt;exec()&lt;/code&gt; + &lt;code&gt;wait()&lt;/code&gt; 같은 작은 조각들의 조합으로 풀었다&lt;/li&gt;
&lt;li&gt;그 분리 덕분에 셸이 리다이렉션과 파이프 같은 기능을 프로그램 수정 없이 제공할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;실습 준비: ostep-code (cpu-api)&lt;/h3&gt;
&lt;p&gt;이 장의 예제(p1.c~p4.c)는 &lt;code&gt;ostep-code&lt;/code&gt; 저장소의 &lt;code&gt;cpu-api/&lt;/code&gt; 디렉터리에 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/remzi-arpacidusseau/ostep-code.git
cd ostep-code/cpu-api

# 한 번에 빌드 (p1~p4 생성)
make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/01_make.png&quot; alt=&quot;01. cpu-api 빌드&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 이 Interlude가 던지는 질문(CRUX)&lt;/h2&gt;
&lt;p&gt;OSTEP는 이 막간(interlude)에서 다음 질문을 던집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로세스를 생성하고 제어하려면 OS가 어떤 인터페이스를 제공해야 하는가?&lt;/li&gt;
&lt;li&gt;유용하면서도 편하게 쓰이려면, 그 인터페이스는 어떻게 설계되어야 하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UNIX는 이 질문에 대해 거대한 단일 API 대신 작은 API들을 조합하는 답을 내놓습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt; : 새 프로세스를 복제(copy)로 만든다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exec()&lt;/code&gt; : 현재 프로세스에 다른 프로그램을 갈아끼운다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;/&lt;code&gt;waitpid()&lt;/code&gt; : 부모가 자식 종료를 기다리고 수거(reap)한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. fork(): 복제로 자식 프로세스를 만든다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt;는 새 프로세스를 하나 생성하지만, 그 방식이 특이합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자식 프로세스는 &lt;code&gt;main()&lt;/code&gt;부터 시작하지 않는다&lt;/li&gt;
&lt;li&gt;부모와 자식 모두 &lt;code&gt;fork()&lt;/code&gt; 다음 줄부터 실행을 이어간다&lt;/li&gt;
&lt;li&gt;거의 동일한 프로세스 복제본이 2개가 된 것처럼 보인다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.1 예제: p1.c (fork 호출)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# /p1.c

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int
main(int argc, char *argv[])
{
    printf(&quot;hello world (pid:%d)\n&quot;, (int) getpid());
    int rc = fork();
    if (rc &amp;lt; 0) {
        // fork failed; exit
        fprintf(stderr, &quot;fork failed\n&quot;);
        exit(1);
    } else if (rc == 0) {
        // child (new process)
	for (int i = 0; i &amp;lt; 1000; i++)
            printf(&quot;hello, I am child (pid:%d)\n&quot;, (int) getpid());
    } else {
        // parent goes down this path (original process)
	for (int i = 0; i &amp;lt; 1000; i++)
            printf(&quot;hello, I am parent of %d (pid:%d)\n&quot;,
	       rc, (int) getpid());
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;make p1
./p1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/02_p1.png&quot; alt=&quot;02. p1 실행 결과&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt;의 반환값이 부모와 자식에서 다릅니다.
&lt;ul&gt;
&lt;li&gt;부모: 자식의 PID(양수)&lt;/li&gt;
&lt;li&gt;자식: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;출력 순서는 실행마다 달라질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
출력 순서가 달라지는 이유는 &lt;code&gt;fork()&lt;/code&gt; 이후 runnable한 실행 흐름이 2개가 되고,
단일 CPU에서도 스케줄러가 누굴 먼저 돌릴지 보장하지 않기 때문입니다(비결정성).
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. wait(): 부모가 자식이 끝나길 기다린다&lt;/h2&gt;
&lt;p&gt;부모가 자식이 끝날 때까지 기다리려면 &lt;code&gt;wait()&lt;/code&gt; 또는 &lt;code&gt;waitpid()&lt;/code&gt;를 씁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부모는 &lt;code&gt;wait()&lt;/code&gt;에서 block됨(대기 상태로 진입)&lt;/li&gt;
&lt;li&gt;자식이 종료하면 부모가 깨어나고 &lt;code&gt;wait()&lt;/code&gt;가 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;는 자식의 종료 상태를 회수해 좀비 프로세스가 남지 않도록 정리하는 의미도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1 예제: p2.c (fork + wait)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# p2.c

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;

int
main(int argc, char *argv[])
{
    printf(&quot;hello world (pid:%d)\n&quot;, (int) getpid());
    int rc = fork();
    if (rc &amp;lt; 0) {
        // fork failed; exit
        fprintf(stderr, &quot;fork failed\n&quot;);
        exit(1);
    } else if (rc == 0) {
        // child (new process)
        printf(&quot;hello, I am child (pid:%d)\n&quot;, (int) getpid());
	sleep(1);
    } else {
        // parent goes down this path (original process)
        int wc = wait(NULL);
        printf(&quot;hello, I am parent of %d (wc:%d) (pid:%d)\n&quot;,
	       rc, wc, (int) getpid());
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;make p2
./p2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/03_p2_wait.png&quot; alt=&quot;03. p2 실행 결과&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;를 추가하면 출력 순서가 안정됩니다.&lt;/li&gt;
&lt;li&gt;부모가 먼저 스케줄되더라도 곧바로 &lt;code&gt;wait()&lt;/code&gt;에 들어가므로, 결과적으로 자식이 먼저 출력하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. exec(): 현재 프로세스를 다른 프로그램으로 교체한다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;exec()&lt;/code&gt; 계열은 새 프로세스를 만드는 함수가 아닙니다.&lt;br /&gt;
현재 프로세스의 프로그램 이미지(코드/정적 데이터)를 다른 실행 파일로 교체하고, 그 프로그램을 실행합니다.&lt;/p&gt;
&lt;p&gt;실습에서 자주 보는 특징:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exec()&lt;/code&gt;가 성공하면, 보통 그 아래 코드는 실행되지 않습니다(되돌아오지 않음).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반적으로 열린 파일 디스크립터는 &lt;code&gt;exec()&lt;/code&gt; 이후에도 유지되며,
필요하면 &lt;code&gt;FD_CLOEXEC&lt;/code&gt;(close-on-exec)로 exec 시점에 닫히도록 설정할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4.1 예제: p3.c (fork + exec + wait)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;p3.c&lt;/code&gt;는 자식에서 &lt;code&gt;execvp()&lt;/code&gt;로 &lt;code&gt;wc&lt;/code&gt;를 실행해, &lt;code&gt;p3.c&lt;/code&gt; 파일의 line/word/byte를 출력합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;

int
main(int argc, char *argv[])
{
    printf(&quot;hello world (pid:%d)\n&quot;, (int) getpid());
    int rc = fork();
    if (rc &amp;lt; 0) {
        // fork failed; exit
        fprintf(stderr, &quot;fork failed\n&quot;);
        exit(1);
    } else if (rc == 0) {
        // child (new process)
        printf(&quot;hello, I am child (pid:%d)\n&quot;, (int) getpid());
        char *myargs[3];
        myargs[0] = strdup(&quot;wc&quot;);   // program: &quot;wc&quot; (word count)
        myargs[1] = strdup(&quot;p3.c&quot;); // argument: file to count
        myargs[2] = NULL;           // marks end of array
        execvp(myargs[0], myargs);  // runs word count
        printf(&quot;this shouldn&apos;t print out&quot;);
    } else {
        // parent goes down this path (original process)
        int wc = wait(NULL);
        printf(&quot;hello, I am parent of %d (wc:%d) (pid:%d)\n&quot;,
	       rc, wc, (int) getpid());
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;make p3
./p3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/04_p3_exec.png&quot; alt=&quot;04. p3 실행 결과&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;자식 프로세스가 &lt;code&gt;wc&lt;/code&gt;로 변신한 것처럼 보이는 게 핵심입니다.&lt;/li&gt;
&lt;li&gt;코드에 있는 &lt;code&gt;printf(...)&lt;/code&gt; 같은 문장은 &lt;code&gt;exec()&lt;/code&gt;가 성공하면 보이지 않는 게 정상입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 exec 변형들(왜 이렇게 많나?)&lt;/h3&gt;
&lt;p&gt;리눅스에는 &lt;code&gt;execl()&lt;/code&gt;, &lt;code&gt;execlp()&lt;/code&gt;, &lt;code&gt;execle()&lt;/code&gt;, &lt;code&gt;execv()&lt;/code&gt;, &lt;code&gt;execvp()&lt;/code&gt;, &lt;code&gt;execve()&lt;/code&gt; 등 변형이 존재합니다.&lt;br /&gt;
인자 전달 방식, PATH 검색 여부, 환경 변수 전달 같은 차이를 제공합니다. 필요할 때 &lt;code&gt;man exec&lt;/code&gt;로 확인하는 게 정석입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 왜 fork()와 exec()를 굳이 분리했을까? (셸이 핵심)&lt;/h2&gt;
&lt;p&gt;셸이 외부 명령을 실행하는 기본 뼈대는 다음을 반복하는 것입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt;로 자식 생성&lt;/li&gt;
&lt;li&gt;자식에서 실행 준비(FD 재배선 등)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exec()&lt;/code&gt;로 명령 실행&lt;/li&gt;
&lt;li&gt;부모는 &lt;code&gt;wait()&lt;/code&gt;로 종료를 기다리고 프롬프트로 복귀&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;핵심은 2번이 &lt;code&gt;fork()&lt;/code&gt;와 &lt;code&gt;exec()&lt;/code&gt; 사이에 존재한다는 점입니다.&lt;br /&gt;
그 짧은 구간 때문에 셸은 프로그램 수정 없이 실행 환경을 바꿀 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 리다이렉션(&amp;gt;): 표준출력을 파일로 보내는 원리&lt;/h2&gt;
&lt;p&gt;예를 들어 아래 명령은 &lt;code&gt;wc&lt;/code&gt;가 파일 출력을 지원해서 가능한 게 아닙니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wc p3.c &amp;gt; newfile.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;셸이 자식에서 &lt;code&gt;exec()&lt;/code&gt;를 호출하기 전에 표준출력(stdout)을 파일로 연결하기 때문에 가능한 것입니다.&lt;/p&gt;
&lt;h3&gt;6.1 핵심 아이디어: close(stdout) → open(file)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;자식에서 &lt;code&gt;close(STDOUT_FILENO)&lt;/code&gt;로 표준출력을 닫고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open(&quot;newfile.txt&quot;, ...)&lt;/code&gt;로 파일을 엽니다&lt;/li&gt;
&lt;li&gt;그 다음 &lt;code&gt;exec()&lt;/code&gt;로 &lt;code&gt;wc&lt;/code&gt;를 실행합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 중요한 OS 가정이 하나 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNIX는 보통 가장 낮은 번호의 미사용 FD부터 할당합니다.&lt;/li&gt;
&lt;li&gt;stdout(1)을 닫아 비워두면, 다음 &lt;code&gt;open()&lt;/code&gt;이 그 자리(1)를 재사용할 가능성이 큽니다.&lt;/li&gt;
&lt;li&gt;결과적으로 &lt;code&gt;printf()&lt;/code&gt; 같은 출력이 화면 대신 파일로 자연스럽게 흘러갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또 하나의 포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;열린 파일 디스크립터는 &lt;code&gt;exec()&lt;/code&gt;를 건너서 유지됩니다. 그래서 리다이렉션이 먹힙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반화된 구현은 &lt;code&gt;dup2(newfd, STDOUT_FILENO)&lt;/code&gt;처럼 원하는 FD로 명시적으로 재배선합니다.
예를 들어 stderr까지 리다이렉션할 때는 &lt;code&gt;dup2(newfd, STDERR_FILENO)&lt;/code&gt;가 필요합니다.&lt;/p&gt;
&lt;h3&gt;6.2 예제: p4.c (stdout → file)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;assert.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;

int
main(int argc, char *argv[])
{
    int rc = fork();
    if (rc &amp;lt; 0) {
        // fork failed; exit
        fprintf(stderr, &quot;fork failed\n&quot;);
        exit(1);
    } else if (rc == 0) {
	// child: redirect standard output to a file
	close(STDOUT_FILENO); 
	open(&quot;./p4.output&quot;, O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

	// now exec &quot;wc&quot;...
        char *myargs[3];
        myargs[0] = strdup(&quot;wc&quot;);   // program: &quot;wc&quot; (word count)
        myargs[1] = strdup(&quot;p4.c&quot;); // argument: file to count
        myargs[2] = NULL;           // marks end of array
        execvp(myargs[0], myargs);  // runs word count
    } else {
        // parent goes down this path (original process)
        int wc = wait(NULL);
	assert(wc &amp;gt;= 0);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;make p4
./p4

# 실행 직후에는 아무것도 안 찍힌 것처럼 보일 수 있음(출력이 파일로 갔기 때문)
cat p4.output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/05_p4_redirect.png&quot; alt=&quot;05. p4 실행 + 결과 파일&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;./p4&lt;/code&gt; 실행 시 터미널에 출력이 없는 것처럼 보입니다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;code&gt;p4.output&lt;/code&gt;을 보면 &lt;code&gt;wc&lt;/code&gt; 출력이 그대로 들어 있습니다.&lt;/li&gt;
&lt;li&gt;셸이 하는 리다이렉션을 코드로 그대로 재현한 예제입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 파이프(|): 한 프로세스의 stdout을 다른 프로세스의 stdin으로&lt;/h2&gt;
&lt;p&gt;파이프도 본질은 같습니다. FD 연결입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한쪽 프로세스의 stdout → 파이프 write-end&lt;/li&gt;
&lt;li&gt;다른쪽 프로세스의 stdin → 파이프 read-end&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 다음이 가능해집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -o foo file | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OSTEP는 파이프가 리다이렉션과 유사한 방식으로 구현되지만, &lt;code&gt;pipe()&lt;/code&gt; 시스템 콜로 두 프로세스를 같은 파이프에 연결한다는 점을 강조합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 프로세스 제어: signals, Ctrl+C / Ctrl+Z, kill()&lt;/h2&gt;
&lt;p&gt;fork/exec/wait 말고도, UNIX에는 프로세스를 제어하기 위한 인터페이스가 많습니다. 대표가 시그널(signals)입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kill()&lt;/code&gt; 시스템 콜: 프로세스에 시그널 전송&lt;/li&gt;
&lt;li&gt;셸 단축키:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+C&lt;/code&gt; → 보통 &lt;code&gt;SIGINT&lt;/code&gt;(인터럽트, 보통 종료)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+Z&lt;/code&gt; → 보통 &lt;code&gt;SIGTSTP&lt;/code&gt;(stop, 일시정지)&lt;/li&gt;
&lt;li&gt;일시정지한 프로세스는 &lt;code&gt;fg&lt;/code&gt;로 다시 포그라운드 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.1 간단 실습: job control 흐름&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sleep 100
# Ctrl+Z
jobs
fg %1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/06_job_control.png&quot; alt=&quot;06. job control&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. man, ps/top, 그리고 Homework&lt;/h2&gt;
&lt;h3&gt;9.1 RTFM: man 페이지를 읽는 습관&lt;/h3&gt;
&lt;p&gt;번역본에는 RTFM에 대한 여담이 실려 있습니다.&lt;br /&gt;
fork/exec/wait 같은 API는 반환값, 에러 조건을 제대로 모르면 사고가 나기 쉬우므로 &lt;code&gt;man fork&lt;/code&gt;, &lt;code&gt;man exec&lt;/code&gt;, &lt;code&gt;man wait&lt;/code&gt;로 확인하는 습관이 중요합니다.&lt;/p&gt;
&lt;h3&gt;9.2 프로세스 관찰 도구&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ps aux | head
top
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.3 숙제(코드)&lt;/h3&gt;
&lt;p&gt;이 장은 API를 손에 익히는 성격이 강합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fork 이후 변수 값을 부모/자식에서 각각 바꿔보고, 서로 영향이 없는지 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open()&lt;/code&gt; 후 &lt;code&gt;fork()&lt;/code&gt; 해서 부모/자식이 같은 FD를 공유할 때 동작(오프셋/출력 섞임)을 관찰&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt; 없이 자식이 항상 먼저 출력되게 할 수 있는지 고민해보기(왜 정석은 wait인지)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exec*()&lt;/code&gt; 변형들을 하나씩 써보고 차이를 정리하기(PATH 검색 여부 등)&lt;/li&gt;
&lt;li&gt;stdout을 닫고 &lt;code&gt;printf()&lt;/code&gt; 하면 어떻게 되는지(오류/출력 누락) 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipe()&lt;/code&gt;로 두 자식 프로세스를 연결해 &lt;code&gt;cmd1 | cmd2&lt;/code&gt;를 코드로 재현해보기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;영문 PDF에는 &lt;code&gt;fork.py&lt;/code&gt; 시뮬레이터 기반 숙제(Homework: Simulation)도 별도로 있습니다.&lt;br /&gt;
프로세스 트리(부모/자식 관계)가 어떻게 변하는지 눈으로 보는 데 도움이 됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 용어정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;프로세스(Process)&lt;/code&gt;: 실행 중인 프로그램 인스턴스(주소공간/레지스터/FD 등 상태 포함)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PID(Process ID)&lt;/code&gt;: 프로세스 식별자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt;: 현재 프로세스를 복제해 자식 프로세스를 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exec()&lt;/code&gt;: 현재 프로세스 이미지를 다른 실행 파일로 교체(성공 시 반환하지 않는 것이 일반적)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()/waitpid()&lt;/code&gt;: 부모가 자식 종료를 기다리고 상태를 수거(reap)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;파일 디스크립터(File Descriptor)&lt;/code&gt;: 프로세스가 파일/파이프/소켓 등을 다루는 핸들(0/1/2는 stdin/stdout/stderr)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;리다이렉션(Redirection)&lt;/code&gt;: FD를 재배선해 표준입출력 흐름을 파일/파이프 등으로 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;파이프(Pipe)&lt;/code&gt;: 한 프로세스의 출력과 다른 프로세스의 입력을 커널 버퍼로 연결&lt;/li&gt;
&lt;li&gt;&lt;code&gt;시그널(Signal)&lt;/code&gt;: 프로세스에 전달되는 비동기 이벤트/제어 메시지(SIGINT, SIGTSTP 등)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;job control&lt;/code&gt;: 셸이 포그라운드/백그라운드 작업을 관리하는 기능(&lt;code&gt;jobs&lt;/code&gt;, &lt;code&gt;fg&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt; 등)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[OSTEP] 04. Process and CPU Virtualization</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-04-processes/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-04-processes/</guid><description>OSTEP Chapter 3 Dialogue on Virtualization와 Chapter 4 The Abstraction The Process 정리 노트입니다. Process와 CPU virtualization의 큰 그림을 잡고, process-run.py로 state 전이를 직접 확인합니다.</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이 글은 OSTEP Part I Virtualization의 출발점입니다.&lt;br /&gt;
Chapter 3 Dialogue on Virtualization은 비유로 방향을 잡아주고, Chapter 4 The Abstraction The Process에서 Process라는 핵심 abstraction을 정의합니다.&lt;/p&gt;
&lt;p&gt;이번 글의 목표는 딱 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Process가 무엇인지, OS가 왜 Process라는 abstraction을 제공하는지 이해합니다&lt;/li&gt;
&lt;li&gt;CPU virtualization이 어떻게 가능해지는지, time sharing 관점에서 감을 잡습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 실습 준비&lt;/h2&gt;
&lt;p&gt;아직 이전 글을 안 보셨다면 아래 링크에서 환경 준비와 실행 감각을 먼저 잡는 편이 좋습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pingu52.github.io/posts/computer-science/os-architecture/ostep-02-os-introduction/&quot;&gt;https://pingu52.github.io/posts/computer-science/os-architecture/ostep-02-os-introduction/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이번 글의 실습은 process-run.py로 Process states 전이를 관찰하는 데 집중합니다.&lt;br /&gt;
ostep-homework 저장소가 이미 있다면 그대로 사용하고, 없다면 이전 글의 방식과 동일하게 한 번만 clone해서 준비합니다.&lt;/p&gt;
&lt;h2&gt;1. Dialogue on Virtualization&lt;/h2&gt;
&lt;p&gt;Virtualization은 한 개의 physical resource를 여러 개의 virtual resource처럼 보이게 만드는 것입니다.&lt;br /&gt;
중요한 포인트는 사용자가 진짜로 자원이 여러 개 있다고 믿게 만드는 illusion입니다.&lt;/p&gt;
&lt;p&gt;CPU로 옮기면 이렇게 들립니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;물리 CPU는 보통 소수입니다&lt;/li&gt;
&lt;li&gt;그런데 우리는 동시에 많은 프로그램을 실행합니다&lt;/li&gt;
&lt;li&gt;OS가 CPU virtualization을 제공하기 때문에, 각 프로그램은 자기 전용 CPU를 가진 듯한 느낌을 받습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 기본 기법이 time sharing입니다. time sharing은 용어 그대로 시간을 나눠서 번갈아 사용하는 방식입니다. 이 용어는 이후 챕터에서도 계속 그대로 쓰이므로, 이번 글에서 익숙해지는 것이 좋습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Chapter 4의 질문, 왜 Process인가요&lt;/h2&gt;
&lt;p&gt;Chapter 4의 질문은 매우 직접적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;physical CPUs가 few인데도, OS는 어떻게 many CPUs가 있는 것처럼 보이게 할까&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 OS가 내놓는 답이 Process입니다.&lt;/p&gt;
&lt;p&gt;Process는 running program입니다.&lt;br /&gt;
여기서 program은 디스크에 저장된 파일 형태의 코드와 데이터 덩어리이고, 아직 실행 중이 아닙니다.&lt;br /&gt;
OS가 program을 메모리에 올리고 CPU가 실행하도록 준비시킨 순간, 그 실행 단위를 Process로 부릅니다.&lt;/p&gt;
&lt;p&gt;아래처럼 이해하는 편이 좋습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;program은 저장된 설계도입니다&lt;/li&gt;
&lt;li&gt;Process는 실제로 움직이는 작업 단위입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Process가 있어야 CPU virtualization이 쉬워집니다&lt;/h2&gt;
&lt;p&gt;CPU virtualization의 핵심은 동시에 실행되는 것처럼 보이는 효과입니다.&lt;br /&gt;
실제로 CPU가 한 개라면 동시에 실행은 불가능합니다. 그래서 OS는 다음을 반복합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Process A를 잠깐 실행합니다&lt;/li&gt;
&lt;li&gt;멈춥니다&lt;/li&gt;
&lt;li&gt;Process B를 잠깐 실행합니다&lt;/li&gt;
&lt;li&gt;다시 바꿉니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 사용자는 동시에 실행되는 것처럼 느낍니다. 하지만 실제로는 매우 빠르게 번갈아 실행되는 것입니다.&lt;br /&gt;
이 방식은 concurrency를 만들어내지만, 성능 비용도 있습니다. CPU를 공유하면 각 Process는 단독 실행보다 느려질 수 있습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Time sharing과 Space sharing&lt;/h2&gt;
&lt;h3&gt;4.1 Time sharing&lt;/h3&gt;
&lt;p&gt;time sharing은 자원을 시간 단위로 쪼개서 돌아가며 사용하는 방식입니다. CPU virtualization의 핵심 도구입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU를 1초씩 나눠 A, B, C에게 번갈아 배정하는 식으로 이해하면 됩니다&lt;/li&gt;
&lt;li&gt;실제 OS는 1초보다 훨씬 짧은 단위로 바꾸며, 사람이 보기에는 동시에 실행되는 것처럼 보입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 Space sharing&lt;/h3&gt;
&lt;p&gt;space sharing은 자원을 공간 단위로 나눠 고정 할당하는 방식입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디스크는 공간을 나눠서 파일에게 블록을 배정합니다&lt;/li&gt;
&lt;li&gt;어떤 파일이 특정 블록을 쓰고 있으면, 그 블록이 해제되기 전까지 다른 파일이 쓰지 않습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이처럼 CPU는 time sharing이 자연스럽고, 디스크는 space sharing이 자연스럽다는 대비가 이번 챕터의 요지 중 하나입니다.&lt;/p&gt;
&lt;h3&gt;4.3 Timeline example without I/O&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/fig03-time-sharing-no-io.png&quot; alt=&quot;Figure 3. Time sharing timeline without I/O&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Figure 3은 입출력이 없는 단순한 경우를 보여줍니다. Process0이 끝날 때까지 계속 실행되고, 그 다음 Process1이 실행됩니다.&lt;br /&gt;
이 경우에는 switch가 적고 단순하지만, interactive workload가 많으면 response time 측면에서 불리할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4.4 Timeline example with I/O overlap&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/fig04-time-sharing-with-io.png&quot; alt=&quot;Figure 4. Time sharing timeline with I/O overlap&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Figure 4는 Process0이 입출력을 시작한 뒤 대기 상태로 내려가고, 그 사이 OS가 Process1을 실행시키는 흐름입니다.&lt;br /&gt;
OS 입장에서는 CPU를 놀리지 않는 것이 중요합니다. 그래서 입출력이 끝날 때까지 기다리는 Process0 대신, 준비된 Process1을 실행합니다.&lt;/p&gt;
&lt;p&gt;이 패턴이 multiprogramming의 동기입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;입출력 장치가 느려서 기다리는 시간이 생깁니다&lt;/li&gt;
&lt;li&gt;그 시간 동안 다른 Process를 실행해서 CPU utilization을 높입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Mechanism and Policy를 분리해서 보기&lt;/h2&gt;
&lt;p&gt;OS 설명은 자주 Mechanism과 Policy로 나뉩니다. 초보자 입장에서는 이렇게 잡으면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mechanism 메커니즘은 가능하게 만드는 장치입니다. 어떻게 할 수 있는가에 대한 부분입니다&lt;/li&gt;
&lt;li&gt;Policy 정책은 선택 기준입니다. 무엇을 먼저 할 것인가에 대한 부분입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CPU virtualization에 그대로 적용하면 다음입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mechanism은 context switch로 Process A를 멈추고 Process B를 이어서 실행할 수 있게 합니다&lt;/li&gt;
&lt;li&gt;Policy는 scheduling policy로 지금 CPU를 누구에게 줄지 결정합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Policy는 목표에 따라 달라집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interactive performance, 즉 응답성을 우선할 수도 있습니다&lt;/li&gt;
&lt;li&gt;throughput, 즉 처리량을 우선할 수도 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구분을 머리에 두면, 이후 스케줄링 챕터에서 어떤 내용이 Mechanism이고 어떤 내용이 Policy인지 정리가 쉬워집니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Process를 구성하는 machine state&lt;/h2&gt;
&lt;p&gt;Process는 단순히 코드 파일이 아닙니다. 실행 중인 상태 전체가 Process입니다.&lt;br /&gt;
그래서 OS는 Process를 다룰 때 machine state를 관리해야 합니다.&lt;/p&gt;
&lt;h3&gt;6.1 Address space&lt;/h3&gt;
&lt;p&gt;Address space는 Process가 접근할 수 있는 메모리 공간입니다. 보통 다음으로 구성됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;code instructions&lt;/li&gt;
&lt;li&gt;static data&lt;/li&gt;
&lt;li&gt;heap&lt;/li&gt;
&lt;li&gt;stack&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/fig01-process-address-space.png&quot; alt=&quot;Figure 1. Program loading and process address space&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Figure 1은 디스크에 있던 program이 메모리로 load되고, 그 결과 Process의 Address space가 구성되는 큰 흐름을 보여줍니다.&lt;br /&gt;
heap과 stack만큼은 확실히 기억하는 편이 좋습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;heap은 실행 중에 필요한 만큼 늘어나는 동적 메모리입니다&lt;/li&gt;
&lt;li&gt;stack은 함수 호출과 지역 변수 같은 실행 흐름을 담는 메모리입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 Registers&lt;/h3&gt;
&lt;p&gt;Registers는 CPU 내부의 매우 빠른 저장소입니다. 실행의 현재 위치와 중간 결과가 여기에 있습니다.&lt;/p&gt;
&lt;p&gt;특히 아래 항목이 자주 등장합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;program counter PC 또는 instruction pointer IP, 다음에 실행할 instruction의 위치&lt;/li&gt;
&lt;li&gt;stack pointer, frame pointer, stack의 현재 위치와 함수 프레임 관리&lt;/li&gt;
&lt;li&gt;general purpose registers, 계산 결과나 임시 값을 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;context switch가 중요한 이유는 명확합니다.&lt;br /&gt;
Process를 바꿔 실행하려면, 이전 Process의 Registers 값을 저장하고 다음 Process의 Registers 값을 복원해야 합니다.&lt;/p&gt;
&lt;h3&gt;6.3 I/O information&lt;/h3&gt;
&lt;p&gt;Process는 디스크 파일을 열고 읽고 쓰는 일을 합니다. 그래서 I/O information도 Process의 상태에 포함됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;열린 파일 목록&lt;/li&gt;
&lt;li&gt;파일 위치 같은 메타데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;리눅스와 유닉스 계열에서는 기본적으로 STDIN, STDOUT, STDERR 같은 표준 입출력 경로가 준비된다는 점도 함께 기억해두면 좋습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Process API sketch&lt;/h2&gt;
&lt;p&gt;Chapter 4는 실제 system call 상세에 들어가기 전에, Process API가 무엇을 제공해야 하는지 윤곽을 줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create, 새로운 Process 생성&lt;/li&gt;
&lt;li&gt;Destroy, 문제가 있는 Process 강제 종료&lt;/li&gt;
&lt;li&gt;Wait, Process 종료까지 대기&lt;/li&gt;
&lt;li&gt;Misc control, suspend와 resume 같은 제어&lt;/li&gt;
&lt;li&gt;Status, runtime과 current state 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 목록은 fork, exec, wait으로 구체화됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Process states and transitions&lt;/h2&gt;
&lt;p&gt;OS는 Process를 상태로 나눠서 관리합니다. simplified view에서 3-state model은 아래입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Running, CPU에서 실행 중&lt;/li&gt;
&lt;li&gt;Ready, 실행 준비 완료지만 CPU를 아직 못 받음&lt;/li&gt;
&lt;li&gt;Blocked, 이벤트를 기다림. 대표 원인은 입출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/fig02-process-states.png&quot; alt=&quot;Figure 2. Process states and transitions&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Figure 2는 전이를 한 눈에 보여줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ready에서 Running으로 올라가는 순간이 scheduled&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Running에서 Ready로 내려오는 순간이 descheduled&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Running에서 입출력을 시작하면 Blocked로 내려가고, 입출력이 끝나면 Ready로 복귀합니다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Blocked는 CPU가 느린 것이 아니라 입출력이 느려서 기다리는 상태입니다&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;9. Process list and PCB&lt;/h2&gt;
&lt;p&gt;OS는 여러 Process를 동시에 관리해야 합니다. 그래서 Process list 또는 task list 같은 자료구조를 갖습니다.&lt;br /&gt;
각 Process의 정보를 담은 레코드를 PCB Process Control Block이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;PCB에는 대략 아래 정보가 들어갑니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Process state&lt;/li&gt;
&lt;li&gt;register context&lt;/li&gt;
&lt;li&gt;Address space 관련 정보&lt;/li&gt;
&lt;li&gt;I/O information&lt;/li&gt;
&lt;li&gt;accounting 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;유닉스 계열에서는 종료는 했지만 정리가 끝나지 않은 상태를 zombie state로 부른다는 점도 함께 등장합니다. 이 내용은 이후 Process API에서 wait를 배울 때 더 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 실습, process-run.py로 상태 전이 따라가기&lt;/h2&gt;
&lt;p&gt;이번 글의 실습 목표는 Process state 전이를 눈으로 확인하는 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ostep-homework/cpu-intro
python3 process-run.py -l 5:100,5:100 -c -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/01_process_state.png&quot; alt=&quot;01. process state&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;관찰 포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Ready, Running, Blocked가 언제 바뀌는지 확인합니다&lt;/li&gt;
&lt;li&gt;입출력이 섞이면 CPU utilization이 왜 좋아지는지 확인합니다&lt;/li&gt;
&lt;li&gt;switching behavior를 바꾸는 것이 policy 실험이라는 점을 연결합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;python3 process-run.py -l 1:0,4:200 -c -S SWITCH_ON_END
python3 process-run.py -l 1:0,4:100 -c -S SWITCH_ON_IO
python3 process-run.py -l 3:0,5:100,5:100,5:100 -S SWITCH_ON_IO -I IO_RUN_LATER -c -p
python3 process-run.py -l 3:0,5:100,5:100,5:100 -S SWITCH_ON_IO -I IO_RUN_IMMEDIATE -c -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 옵션은 다음처럼 이해하면 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SWITCH_ON_END는 Process가 끝날 때만 바꿉니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/02_process_state_switch_on_end.png&quot; alt=&quot;02. process state switch on end&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SWITCH_ON_IO는 입출력로 Blocked가 되면 바로 다른 Process로 바꿉니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/03_process_state_switch_on_io.png&quot; alt=&quot;03. process state switch on io&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IO_RUN_LATER와 IO_RUN_IMMEDIATE는 입출력 처리 타이밍을 바꿔서 전이를 비교하게 해줍니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/04_process_state_io_run_later.png&quot; alt=&quot;04. process state io run later&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/05_process_state_io_run_immediate.png&quot; alt=&quot;05. process state io run immediate&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 용어 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;추상화 (Abstraction)&lt;/code&gt;: 복잡한 세부를 감추고, 프로그램이 쓰기 쉬운 형태로 일관된 interface를 제공합니다. Process, file, address space 같은 개념이 대표적인 abstraction입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;가상화 (Virtualization)&lt;/code&gt;: 물리 자원 physical resource을 프로그램 관점에서 가상 자원 virtual resource처럼 보이게 만듭니다. CPU virtualization은 물리 CPU가 여러 개처럼 보이게 하는 효과를 말합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;프로세스 (Process)&lt;/code&gt;: running program입니다. 디스크에 저장된 program을 OS가 메모리에 올리고 실행 가능한 상태로 만든 실행 단위입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;시분할 (time sharing)&lt;/code&gt;: 자원을 시간 단위로 쪼개 여러 주체가 번갈아 쓰는 방식입니다. CPU virtualization의 핵심 기법입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;공간 분할 (space sharing)&lt;/code&gt;: 자원을 공간 단위로 나눠 고정적으로 배정하는 방식입니다. 디스크 블록을 파일에 할당하는 방식이 대표적입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;주소 공간 (Address space)&lt;/code&gt;: Process가 접근 가능한 메모리 범위입니다. 보통 code, static data, heap, stack으로 구성됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;레지스터 (Registers)&lt;/code&gt;: CPU 내부의 빠른 저장소로, 실행 위치와 중간 결과 같은 machine state를 담습니다. context switch는 registers를 save and restore하는 과정입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;프로그램 카운터 (program counter)&lt;/code&gt;, 인스트럭션 포인터 instruction pointer: 다음에 실행할 instruction의 위치를 가리키는 register입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;문맥교환 (context switch)&lt;/code&gt;: 실행 중인 Process를 바꾸기 위해 기존 Process의 state를 저장하고, 다음 Process의 state를 복원하는 mechanism입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;스케줄링 (scheduling)&lt;/code&gt;: Ready 상태의 Process 중에서 누구를 Running으로 올릴지 결정하는 과정입니다. scheduling policy는 그 선택 기준입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;정책 (policy)&lt;/code&gt;: 무엇을 선택할지에 대한 기준입니다. CPU를 누구에게 먼저 줄지, 언제 바꿀지 같은 선택을 포함합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;메커니즘 (mechanism)&lt;/code&gt;: 선택을 실제로 가능하게 만드는 구현 방법입니다. context switch 같은 저수준 동작이 여기에 해당합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;입출력 (I/O)&lt;/code&gt;: 디스크, 네트워크, 터미널 같은 장치와 데이터를 주고받는 작업입니다. I/O가 느리면 Process는 Blocked로 내려가 이벤트를 기다립니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;실행 (Running)&lt;/code&gt;: CPU에서 instructions를 실제로 실행 중인 상태입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;준비 (Ready)&lt;/code&gt;: 실행 준비는 되었지만 CPU를 아직 받지 못한 상태입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;대기 (Blocked)&lt;/code&gt;: 이벤트를 기다리는 상태입니다. 대표 이유는 I/O 대기입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;프로세스 제어 블록 (PCB, Process Control Block)&lt;/code&gt;: 각 Process의 핵심 정보를 담는 자료구조 entry입니다. state, register context, address space 정보, I/O 정보 등이 들어갑니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;멀티프로그래밍 (multiprogramming)&lt;/code&gt;: I/O로 기다리는 시간이 생길 때 다른 Process를 실행해 CPU utilization을 높이려는 접근입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CPU 활용률 (CPU utilization)&lt;/code&gt;: CPU가 유용한 일을 하고 있는 비율입니다. I/O 대기 시간을 다른 Process 실행으로 메우면 utilization이 올라갑니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;좀비 상태 (zombie state)&lt;/code&gt;: Process가 exit했지만, 부모 Process가 wait로 종료 코드를 회수하지 않아 정리가 끝나지 않은 상태입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;다음 글에서는 Process API를 system call 관점으로 확장합니다. fork, exec, wait가 어떻게 Process creation과 연결되는지 이어서 정리하겠습니다.&lt;/p&gt;
</content:encoded></item><item><title>[OSTEP] 02. 운영체제 개요 (Introduction)</title><link>https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-02-os-introduction/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/computer-science/os-architecture/ostep-02-os-introduction/</guid><description>OSTEP 2장 정리 노트. CPU/메모리 가상화(Virtualization), 동시성(Concurrency), 영속성(Persistence) 프레임과 System Call/User-Kernel Mode, 설계 목표를 정리합니다.</description><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 『Operating Systems: Three Easy Pieces(OSTEP)』 &lt;strong&gt;2장(Introduction to Operating Systems)&lt;/strong&gt; 을 읽고 정리한 노트입니다.&lt;br /&gt;
1장 Dialogue는 별도 글로 만들지 않고, 이 장의 흐름(세 가지 축 + Mechanism/Policy) 안에 자연스럽게 섞어서 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;실습 준비: ostep-code&lt;/h3&gt;
&lt;p&gt;이 장의 예제(cpu.c, mem.c, threads.c 등)는 &lt;code&gt;ostep-code&lt;/code&gt; 저장소의 &lt;code&gt;intro/&lt;/code&gt; 디렉터리에 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/remzi-arpacidusseau/ostep-code.git
cd ostep-code/intro
# (선택) 한 번에 빌드하고 싶으면
# make
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 이 장에서 잡아야 할 관점&lt;/h2&gt;
&lt;p&gt;운영체제(Operating System)는 물리 자원(CPU/Memory/Disk)을 직접 드러내지 않고, 프로그램이 쓰기 쉬운 추상화(Abstraction)로 제공합니다.&lt;br /&gt;
그리고 운영체제는 아래 세 가지 큰 문제를 반복해서 다룹니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가상화(Virtualization): CPU/메모리 같은 자원을 &lt;strong&gt;마치 분리되어 있는 것처럼&lt;/strong&gt; 보이게 만듭니다.&lt;/li&gt;
&lt;li&gt;동시성(Concurrency): 여러 실행 흐름이 겹칠 때 생기는 오류를 다룹니다.&lt;/li&gt;
&lt;li&gt;영속성(Persistence): 전원이 꺼져도 데이터가 남도록 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Virtualization 1: CPU 가상화 (Virtualizing the CPU)&lt;/h2&gt;
&lt;p&gt;단일 CPU에서도 여러 프로그램이 동시에 실행되는 것처럼 보입니다.&lt;br /&gt;
이 &lt;strong&gt;동시 실행처럼 보이는 효과&lt;/strong&gt;는 운영체제가 하드웨어의 도움을 받아 만든 &lt;strong&gt;환상(illusion)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;h3&gt;2.1 예제: cpu.c&lt;/h3&gt;
&lt;p&gt;책의 예제는 1초 정도 바쁜 대기(Spin) 후 문자열을 출력하는 단순 루프입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# build
gcc -o cpu cpu.c -Wall

# run (foreground)
./cpu A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/01_cpu_A.png&quot; alt=&quot;01. CPU 한개만 실행&quot; /&gt;&lt;/p&gt;
&lt;p&gt;여기서 핵심은 &lt;strong&gt;한 번에 하나만 실행&lt;/strong&gt;이 아니라 &lt;strong&gt;여러 개를 동시에 실행&lt;/strong&gt;하는 상황입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# zsh 기준 예시: 백그라운드로 여러 프로세스 실행
./cpu A &amp;amp; ./cpu B &amp;amp; ./cpu C &amp;amp; ./cpu D &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/02_cpu_multi.png&quot; alt=&quot;02. CPU 여러개 실행&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;CPU는 하나인데 출력이 섞입니다.&lt;/li&gt;
&lt;li&gt;운영체제는 타임-셰어링(Time-sharing) 방식으로 CPU 시간을 쪼개서, 마치 가상 CPU(virtual CPU)가 여러 개인 것처럼 보이게 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;누가 먼저 CPU를 쓰는가?&lt;/strong&gt; 는 정책(Policy) 문제로 이어집니다(스케줄링 Scheduling).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;실행 중지 팁&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;포그라운드 실행은 &lt;code&gt;Ctrl+C&lt;/code&gt;로 종료합니다.&lt;/li&gt;
&lt;li&gt;백그라운드 실행은 &lt;code&gt;jobs&lt;/code&gt;로 확인 후 &lt;code&gt;kill %1&lt;/code&gt;처럼 종료합니다.&lt;/li&gt;
&lt;li&gt;간단히 정리하려면 &lt;code&gt;pkill cpu&lt;/code&gt;도 사용할 수 있습니다(동명이 프로그램이 있으면 주의합니다).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Virtualization 2: 메모리 가상화 (Virtualizing Memory)&lt;/h2&gt;
&lt;p&gt;물리 메모리(physical memory)는 바이트 배열이고, 주소를 통해 읽고/씁니다.&lt;br /&gt;
그런데 각 프로세스는 자기만의 주소 공간(Address Space, Virtual Address Space)을 가진 것처럼 보입니다.&lt;/p&gt;
&lt;h3&gt;3.1 예제: mem.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# build
gcc -o mem mem.c -Wall

# run
./mem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/03_memory_single.png&quot; alt=&quot;03. 메모리 예제 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;여러 인스턴스를 동시에 실행하면, 각 프로세스가 같은 가상 주소(예: 0x200000)를 출력하면서도 서로 영향을 주지 않는 것처럼 보입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./mem &amp;amp; ./mem &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/04_memory_multi.png&quot; alt=&quot;04. 메모리 예제 2&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;같은 주소인데 왜 충돌이 없나?&lt;/strong&gt; → 여기서 주소는 가상 주소(virtual address)이고, 운영체제가 이를 물리 메모리(physical memory)로 매핑(mapping)합니다.&lt;/li&gt;
&lt;li&gt;결과적으로 프로세스마다 사적인(private) 메모리를 가진 것처럼 보이지만, 실제 물리 메모리는 공유 자원(shared resource)이며 OS가 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
이 예제는 주소 공간 랜덤화(ASLR, Address Space Layout Randomization)가 켜져 있으면 책의 출력과 다르게 보일 수 있습니다. &lt;strong&gt;같은 주소가 반복된다&lt;/strong&gt;는 현상을 재현하려면 ASLR을 끄는 방식이 필요합니다.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 끄기(재현용) — 실습 후 원복을 권장합니다
sudo sysctl -w kernel.randomize_va_space=0
# 원복(기본값)
sudo sysctl -w kernel.randomize_va_space=2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Concurrency: 동시성(Concurrency) 문제를 코드로 확인하기&lt;/h2&gt;
&lt;p&gt;동시성은 여러 작업이 겹쳐 실행될 때 생기는 문제를 통칭합니다.&lt;br /&gt;
운영체제는 가상화를 위해 여러 프로세스를 번갈아 실행하므로(스케줄링) 동시성 이슈를 구조적으로 안고 있습니다. 또한 현대 소프트웨어는 애플리케이션 자체도 멀티스레드(Multi-threaded)로 동작하므로, 동일한 문제가 사용자 프로그램에서도 그대로 나타납니다.&lt;/p&gt;
&lt;h3&gt;4.1 예제: threads.c (Race Condition)&lt;/h3&gt;
&lt;p&gt;두 개의 스레드(Thread)가 같은 변수(counter)를 증가시킵니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# build
gcc -o threads threads.c -Wall -pthread

# run
./threads 1000
./threads 1000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/05_thread.png&quot; alt=&quot;05. 스레드 예제&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;관찰 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;loops=1000일 때는 기대값(2N)이 잘 나올 수 있습니다.&lt;/li&gt;
&lt;li&gt;loops를 크게 하면 결과가 틀리거나 실행마다 달라질 수 있습니다 → 경쟁 조건(Race Condition)의 전형적인 증상입니다.&lt;/li&gt;
&lt;li&gt;원인은 &lt;code&gt;counter++&lt;/code&gt;가 원자적(atomic) 연산이 아니기 때문입니다. 일반적으로 &lt;code&gt;load → increment → store&lt;/code&gt; 같은 여러 단계로 분해되어 실행되며, 그 사이에 스케줄링이 끼어들면 결과가 깨집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 파트는 이후 챕터에서 동기화(Synchronization), 상호배제(Mutual Exclusion), 락(Lock) 같은 기본 도구로 확장됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Persistence: 영속성(Persistence)과 파일 시스템(File System)&lt;/h2&gt;
&lt;p&gt;DRAM 같은 메모리는 휘발성(volatile)이라 전원이 꺼지거나 크래시가 나면 데이터가 사라집니다.&lt;br /&gt;
그래서 시스템은 저장장치(I/O device: HDD/SSD 등)에 데이터를 &lt;strong&gt;영속적으로 저장&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;하드웨어: 디스크/SSD 같은 저장 장치&lt;/li&gt;
&lt;li&gt;소프트웨어(운영체제): 파일 시스템(File System)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CPU/메모리와 달리, 디스크는 &lt;strong&gt;프로세스마다 사적인 가상 디스크&lt;/strong&gt;를 제공하기보다 &lt;strong&gt;공유(share)&lt;/strong&gt; 를 전제로 합니다.&lt;br /&gt;
에디터로 파일을 만들고, 컴파일러로 빌드하고, 실행 파일을 돌리는 일련의 흐름이 바로 그 공유를 활용합니다.&lt;/p&gt;
&lt;p&gt;또한 OS는 저장장치에 접근하는 복잡한 세부를 감추고, 시스템 콜(System Call) 형태의 단순한 인터페이스로 I/O를 제공합니다(예: open/write/close).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Mechanism vs Policy: 구현과 선택을 분리해서 보기&lt;/h2&gt;
&lt;p&gt;OSTEP는 &lt;strong&gt;가능하게 하는 방법(How)&lt;/strong&gt; 과 &lt;strong&gt;무엇을 선택할지(Which)&lt;/strong&gt; 를 분리해 사고하는 습관을 강조합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메커니즘(Mechanism): 기능을 가능하게 하는 구현 수단입니다.
&lt;ul&gt;
&lt;li&gt;예: 타임-셰어링(Time-sharing), 트랩(trap) 기반 시스템 콜 진입&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정책(Policy): 여러 선택지 중 무엇을 택할지에 대한 기준입니다.
&lt;ul&gt;
&lt;li&gt;예: 스케줄링(Scheduling)에서 &lt;strong&gt;어느 프로세스에게 CPU를 먼저 줄지&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;예: 파일 시스템에서 캐싱/쓰기 지연(write-back) 같은 정책 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 운영체제 설계 목표(Goals)&lt;/h2&gt;
&lt;p&gt;이 장에서는 &lt;strong&gt;OS를 만든다면 어떤 목표를 먼저 세워야 하는가&lt;/strong&gt;도 함께 언급합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;편의성(Convenience): 추상화(Abstraction)로 사용을 쉽게 만듭니다.&lt;/li&gt;
&lt;li&gt;성능(Performance): OS 오버헤드(overhead)를 과도하게 키우지 않습니다.&lt;/li&gt;
&lt;li&gt;보호/격리(Protection/Isolation): 한 프로그램이 다른 프로그램/OS를 망가뜨리지 못하게 합니다.&lt;/li&gt;
&lt;li&gt;신뢰성(Reliability): OS가 실패하면 전체가 멈추므로, 높은 신뢰성이 필요합니다.&lt;/li&gt;
&lt;li&gt;기타: 에너지 효율(Energy-efficiency), 보안(Security), 이동성(Mobility) 등은 환경에 따라 중요도가 달라집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 시스템 콜(System Call)과 실행 모드(User/Kernel Mode)&lt;/h2&gt;
&lt;p&gt;운영체제가 &lt;strong&gt;그저 라이브러리&lt;/strong&gt;를 넘어서려면, OS 코드와 앱 코드를 구분해야 합니다.&lt;br /&gt;
대표적인 장치가 시스템 콜(System Call)입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프로시저 호출(Procedure Call): 같은 권한 레벨에서 함수로 점프합니다.&lt;/li&gt;
&lt;li&gt;시스템 콜(System Call): OS로 점프하면서 권한 레벨(privilege level)을 올립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반 앱은 사용자 모드(User Mode)에서 실행되어 I/O 요청이나 물리 메모리 접근 같은 위험한 작업이 제한됩니다.&lt;br /&gt;
시스템 콜은 트랩(trap) 같은 특별한 하드웨어 명령을 통해 커널 모드(Kernel Mode)로 진입하고, OS가 요청을 처리한 뒤 다시 사용자 모드로 돌아옵니다.&lt;/p&gt;
&lt;p&gt;이 구조가 곧 격리(Isolation)와 보호(Protection)의 기반입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 짧은 역사: 라이브러리에서 멀티프로그래밍까지&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;초기 OS는 공통 루틴을 모아둔 라이브러리 수준에서 출발했습니다.&lt;/li&gt;
&lt;li&gt;I/O가 느린 환경에서 CPU 활용률을 높이려면, 한 작업이 I/O를 기다릴 때 다른 작업으로 전환해야 했습니다.
&lt;ul&gt;
&lt;li&gt;그 결과 멀티프로그래밍(Multiprogramming)과 인터럽트(Interrupt) 기반 설계가 확산됐습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 과정에서 메모리 보호, 동시성 문제(특히 인터럽트로 인한 경쟁)가 핵심 과제로 부상했습니다.&lt;/li&gt;
&lt;li&gt;UNIX는 다양한 아이디어를 비교적 단순한 형태로 정리해 대중화에 큰 영향을 줬습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 용어정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;추상화(Abstraction)&lt;/code&gt;: 복잡한 세부를 감추고 일관된 인터페이스를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;가상화(Virtualization)&lt;/code&gt;: 물리 자원을 프로그램 관점에서 가상 자원으로 보이게 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;주소 공간(Address Space, Virtual Address Space)&lt;/code&gt;: 프로세스가 바라보는 가상 메모리 범위입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;동시성(Concurrency)&lt;/code&gt;: 실행 흐름이 겹치며 발생하는 문제 영역입니다(스레드/인터럽트 등).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;경쟁 조건(Race Condition)&lt;/code&gt;: 타이밍에 따라 결과가 달라지는 버그입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;영속성(Persistence)&lt;/code&gt;: 크래시/전원 차단에도 데이터가 유지되어야 하는 성질입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;파일 시스템(File System)&lt;/code&gt;: 저장장치 위에서 파일/디렉터리 추상화를 제공하는 OS 구성요소입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;시스템 콜(System Call)&lt;/code&gt;: 사용자 모드 → 커널 모드 전환을 동반하는 OS 호출입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;사용자 모드(User Mode)&lt;/code&gt; / &lt;code&gt;커널 모드(Kernel Mode)&lt;/code&gt;: 권한 레벨 분리 메커니즘입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;다음 글은 4장 가상화(Virtualization) 파트로 들어갑니다.&lt;br /&gt;
3장 Dialogue(가상화에 관한 대화)는 별도 글로 만들지 않고, 4장(프로세스 Process) 서두에서 필요한 프레임만 짧게 연결하겠습니다.&lt;/p&gt;
</content:encoded></item><item><title>[STM32MP25] BME280 Bring-up: DTS 패치 + Yocto 커널 모듈 추가</title><link>https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/stm32mp25_bme280_bringup/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/stm32mp25_bme280_bringup/</guid><description>STM32MP257F-DK(STM32MP25-DISCO)에서 BME280을 Device Tree로 등록하고, Yocto(OpenSTLinux)에서 bmp280 드라이버를 모듈로 빌드한 뒤 SD 카드로 부팅해 IIO(sysfs)로 값 읽기까지 정리합니다.</description><pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글은 &lt;strong&gt;STM32MP257F-DK(빌드 머신: &lt;code&gt;stm32mp25-disco&lt;/code&gt;)&lt;/strong&gt; 환경에서 &lt;strong&gt;BME280 센서(I2C0, 주소 &lt;code&gt;0x76&lt;/code&gt;)&lt;/strong&gt; 를 붙이고,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Device Tree(DTS)에 노드 추가&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yocto에서 커널 드라이버(bmp280)를 모듈로 활성화&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이미지 빌드 → SD 카드 플래시 → 부팅&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IIO(&lt;code&gt;/sys/bus/iio&lt;/code&gt;)에서 온도/기압/습도 값 읽기&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;까지 따라하면 그대로 되는 방식으로 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;구성 개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;커널 소스에 직접 커밋하지 않고&lt;/strong&gt;: &lt;code&gt;meta-myboard&lt;/code&gt; 레이어에서 &lt;code&gt;bbappend + patch&lt;/code&gt;로 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;드라이버는 모듈로&lt;/strong&gt;: &lt;code&gt;bmp280_i2c.ko&lt;/code&gt;를 &lt;code&gt;modprobe&lt;/code&gt;로 로드&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;런타임 검증은 IIO sysfs&lt;/strong&gt;: &lt;code&gt;in_temp_input&lt;/code&gt;, &lt;code&gt;in_pressure_input&lt;/code&gt;, &lt;code&gt;in_humidityrelative_input&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;전제 조건&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ST &lt;strong&gt;Distribution-Package(OpenSTLinux)&lt;/strong&gt; 를 기준으로 설명합니다.&lt;/li&gt;
&lt;li&gt;빌드 환경이 아래처럼 잡혀있다고 가정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 예시
DISTRO=openstlinux-weston
MACHINE=stm32mp25-disco
source layers/meta-st/scripts/envsetup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;커스텀 레이어(예: &lt;code&gt;~/work/stm32mp257/stm32mp257f-bsp-lab/yocto/meta-myboard&lt;/code&gt;)를 운영 중이며,
&lt;strong&gt;bblayers.conf에 추가&lt;/strong&gt;되어 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;bitbake-layers show-layers | grep -E &quot;meta-myboard|meta-st&quot; || true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;1. meta-myboard 레이어 준비&lt;/h2&gt;
&lt;h3&gt;1.1 파일 배치 위치&lt;/h3&gt;
&lt;p&gt;이번 글에서 사용하는 파일은 아래 두 개입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;linux-stm32mp_%.bbappend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0001-...bme280...patch&lt;/code&gt; (DTS 수정 패치)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bmp280.cfg&lt;/code&gt; (커널 config fragment)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/01_meta_myboard_tree.png&quot; alt=&quot;meta-myboard 레이어 구조&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;저는 &lt;code&gt;openstlinux-weston&lt;/code&gt; 환경에만 적용되도록 &lt;code&gt;linux-stm32mp/openstlinux-weston/&lt;/code&gt; 아래에 파일을 두었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Device Tree 수정 패치 만들기&lt;/h2&gt;
&lt;p&gt;BME280를 커널이 자동으로 인식하게 하려면, 보드 DTS에 I2C 디바이스 노드를 추가해야 합니다.&lt;br /&gt;
여기서는 &lt;strong&gt;Yocto devshell 없이&lt;/strong&gt;, 현재 빌드 환경(Distribution-Package) 안에서 커널 소스 트리를 “확정적으로” 찾고, 그 변경을 &lt;strong&gt;패치 파일(0001-*.patch)&lt;/strong&gt; 로 만들어 &lt;code&gt;meta-myboard&lt;/code&gt;에 보관합니다.&lt;/p&gt;
&lt;h3&gt;2.1 커널 소스 트리 경로를 확정(S 변수)&lt;/h3&gt;
&lt;p&gt;Yocto에서 커널 소스 디렉토리는 레시피 변수 &lt;code&gt;S&lt;/code&gt;로 확정할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# (이미 envsetup.sh로 Yocto bash 환경을 올린 상태에서)
KSRC=$(bitbake -e virtual/kernel | sed -n &apos;s/^S=&quot;\([^&quot;]*\)&quot;.*/\1/p&apos;)
echo &quot;KSRC=$KSRC&quot;
ls -la &quot;$KSRC&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 &lt;code&gt;$KSRC&lt;/code&gt;가 비어있거나, 디렉토리가 아직 준비되지 않았다면 아래를 한 번 실행해 소스를 준비합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake -c fetch  virtual/kernel
bitbake -c unpack virtual/kernel
bitbake -c patch  virtual/kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 DTS 원본 백업&lt;/h3&gt;
&lt;p&gt;패치를 만들 때 &lt;strong&gt;/tmp 같은 외부 경로로 diff를 뜨면&lt;/strong&gt;, patch 헤더가 &lt;code&gt;/tmp/...&lt;/code&gt;로 들어가서 &lt;code&gt;do_patch&lt;/code&gt; 단계에서 “파일을 못 찾는” 문제가 자주 발생합니다.&lt;br /&gt;
따라서 원본 백업도 &lt;strong&gt;커널 트리 내부&lt;/strong&gt;에 둔 뒤 diff를 생성합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd &quot;$KSRC&quot;
DTS=arch/arm64/boot/dts/st/stm32mp257f-dk.dts

# 원본 백업(같은 디렉토리 안에 둠)
cp -v &quot;$DTS&quot; &quot;${DTS}.orig&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 DTS에 BME280 노드 추가&lt;/h3&gt;
&lt;p&gt;I2C 디바이스 노드 이름은 보통 &lt;code&gt;&amp;lt;device&amp;gt;@&amp;lt;addr&amp;gt;&lt;/code&gt; 형태를 씁니다.&lt;br /&gt;
예를 들어 0x76이면 &lt;code&gt;bme280@76&lt;/code&gt;처럼 표기하는 것이 일반적이며, 이 “노드 이름”은 &lt;strong&gt;사람이 읽기 좋은 방식&lt;/strong&gt;이고 실제 드라이버 매칭은 &lt;code&gt;compatible&lt;/code&gt; / &lt;code&gt;reg&lt;/code&gt;로 결정됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/02_dts_bme280_node.png&quot; alt=&quot;stm32mp257f-dk.dts&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;amp;i2c2&lt;/strong&gt;를 찾아 내부에 bme280 노드를 추가합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&amp;amp;i2c2 {
    status = &quot;okay&quot;;

    bme280@76 {
        compatible = &quot;bosch,bme280&quot;;
        reg = &amp;lt;0x76&amp;gt;;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
주소가 0x77인 모듈도 있으니, 하드웨어 SDO 설정/모듈 스펙에 맞춰 &lt;code&gt;reg&lt;/code&gt;를 결정합니다.
:::&lt;/p&gt;
&lt;h3&gt;2.4 커널 소스 트리에서 &lt;code&gt;git commit&lt;/code&gt; 후 &lt;code&gt;git format-patch -1&lt;/code&gt;로 패치 만들기&lt;/h3&gt;
&lt;p&gt;아래 &lt;code&gt;KSRC&lt;/code&gt;는 &lt;strong&gt;반드시 &lt;code&gt;git status&lt;/code&gt;가 동작하는 경로&lt;/strong&gt;로 잡습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# 커널 소스 트리 경로(예: 현재 사용 중인 kernel-source)
KSRC=$(bitbake -e virtual/kernel | sed -n &apos;s/^S=&quot;\([^&quot;]*\)&quot;.*/\1/p&apos;)

cd &quot;$KSRC&quot;
git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아까 DTS에 노드를 추가하였습니다&lt;a href=&quot;###-2.3-DTS%EC%97%90-BME280-%EB%85%B8%EB%93%9C-%EC%B6%94%EA%B0%80&quot;&gt;(2.3)&lt;/a&gt;. 이제 아래처럼 &lt;strong&gt;정확히 3줄&lt;/strong&gt;로 패치를 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd &quot;$KSRC&quot;

git add arch/arm64/boot/dts/st/stm32mp257f-dk.dts
git commit -m &quot;arm64: dts: stm32mp257f-dk: add bme280 on i2c2@0x76&quot;
git format-patch -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성된 패치 파일명은 커밋 subject에 따라 달라질 수 있으니, &lt;strong&gt;레이어에서 재사용하기 쉽게 파일명을 고정&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 방금 생성된 patch 파일을 찾아 파일명 고정
PATCH_OUT=$(ls -t 0001-*.patch | head -n 1)
mv -v &quot;$PATCH_OUT&quot; 0001-stm32mp257f-dk-add-bme280-i2c2-0x76.patch
PATCH_OUT=&quot;$KSRC/0001-stm32mp257f-dk-add-bme280-i2c2-0x76.patch&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
(선택) 커널 소스 트리에 “로컬 커밋을 남기고 싶지 않다면”, 패치 파일을 레이어로 복사한 뒤 아래로 되돌릴 수 있습니다.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# 주의: 로컬 커밋/변경이 없을 때만
git reset --hard HEAD~1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 meta-myboard로 패치 이관&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;DST_DIR=~/work/stm32mp257/stm32mp257f-bsp-lab/yocto/meta-myboard/recipes-kernel/linux/linux-stm32mp/openstlinux-weston
install -d &quot;$DST_DIR&quot;
cp -v &quot;$PATCH_OUT&quot; &quot;$DST_DIR/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
DTS에서는 &lt;code&gt;&amp;amp;i2c2&lt;/code&gt;에 붙였더라도, 런타임에서는 &lt;code&gt;i2c-0&lt;/code&gt;처럼 “버스 번호”가 다르게 매겨질 수 있습니다.&lt;br /&gt;
이때는 &lt;code&gt;0-0076&lt;/code&gt;처럼 보이는 sysfs 경로가 정상이며, Device Tree의 &lt;code&gt;compatible/reg&lt;/code&gt;와 드라이버 매칭이 핵심입니다.
:::&lt;/p&gt;
&lt;h2&gt;3. meta-myboard에서 패치 적용(bbappend)&lt;/h2&gt;
&lt;h3&gt;3.1 linux-stm32mp_%.bbappend 작성&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;meta-myboard&lt;/code&gt; 안에 &lt;code&gt;linux-stm32mp_%.bbappend&lt;/code&gt;를 만들고, 2장에서 만든 patch를 &lt;code&gt;SRC_URI&lt;/code&gt;에 추가합니다.&lt;/p&gt;
&lt;p&gt;파일: &lt;code&gt;yocto/meta-myboard/recipes-kernel/linux/linux-stm32mp_%.bbappend&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
FILESEXTRAPATHS:prepend := &quot;${THISDIR}/${PN}:&quot;

SRC_URI:append = &quot; \
    file://0001-stm32mp257f-dk-add-bme280-i2c2-0x76.patch \
&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::note
&lt;code&gt;local.conf&lt;/code&gt;에 우회로로 넣으면 당장은 편해 보이지만, &lt;strong&gt;빌드 디렉토리 로컬 설정에 종속&lt;/strong&gt;되어 재현·공유가 어렵습니다.
DTS/커널 변경은 &lt;code&gt;bbappend + patch&lt;/code&gt;로 레이어에서 관리하는 편이 BSP 실습/협업/블로그 기록에 훨씬 유리합니다.
:::&lt;/p&gt;
&lt;h3&gt;3.2 패치 파일 배치 확인&lt;/h3&gt;
&lt;p&gt;patch 파일은 &lt;code&gt;FILESEXTRAPATHS&lt;/code&gt;가 가리키는 디렉토리(여기서는 &lt;code&gt;${THISDIR}/${PN}&lt;/code&gt; 계열) 아래에 있어야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l ~/work/stm32mp257/stm32mp257f-bsp-lab/yocto/meta-myboard/recipes-kernel/linux/linux-stm32mp/openstlinux-weston/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 bbappend가 실제로 먹는지 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bitbake-layers show-appends | grep -i &quot;linux-stm32mp&quot; || true

# SRC_URI에 patch가 실제로 포함됐는지 확인
bitbake -e virtual/kernel | egrep &apos;^(FILESEXTRAPATHS=|SRC_URI=)&apos; | sed -n &apos;1,160p&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(옵션) &lt;code&gt;do_patch&lt;/code&gt; 단계만 강제로 돌려서 빠르게 검증할 수도 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake -c patch virtual/kernel -f
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 커널 설정: BMP280 드라이버를 모듈로 활성화&lt;/h2&gt;
&lt;p&gt;리눅스에서는 BME280가 “전용 드라이버 이름”으로 보이기보다, &lt;code&gt;bmp280&lt;/code&gt; 드라이버가 BME280(습도 채널 포함)까지 함께 지원하는 구성이 일반적입니다.&lt;br /&gt;
따라서 커널 설정에서는 보통 &lt;strong&gt;BMP280 + I2C transport&lt;/strong&gt;를 모듈로 켜는 것이 핵심입니다.&lt;/p&gt;
&lt;h3&gt;4.1 menuconfig 실행(virtual/kernel)&lt;/h3&gt;
&lt;p&gt;아래 명령은 &lt;strong&gt;현재 빌드 환경&lt;/strong&gt;(envsetup.sh 적용 상태)에서 실행합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake -c menuconfig virtual/kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 활성화할 Kconfig 포인트(모듈 기준)&lt;/h3&gt;
&lt;p&gt;menuconfig에서 아래 항목을 찾아 &lt;strong&gt;M(모듈)&lt;/strong&gt; 로 설정합니다(메뉴 경로는 커널 버전에 따라 달라질 수 있습니다).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Device Drivers ---&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt; * &amp;gt; Industrial I/O support ---&amp;gt;&lt;/li&gt;
&lt;li&gt;Pressure sensors ---&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt; M &amp;gt; Bosch ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/09_linux_kernel_config.png&quot; alt=&quot;kernel config&quot; /&gt;
&lt;img src=&quot;./images/10._bosch_config.png&quot; alt=&quot;bosch config&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.3 diffconfig로 fragment.cfg 추출&lt;/h3&gt;
&lt;p&gt;menuconfig로 바꾼 설정을 config로 뽑아내기 위해 diffconfig를 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake -c diffconfig virtual/kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.4 fragment.cfg 위치 찾기&lt;/h3&gt;
&lt;p&gt;diffconfig 결과물은 보통 커널 WORKDIR 아래에 생성됩니다. 경로를 “확정적으로” 찾는 방식은 아래처럼 고정합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KWORKDIR=$(bitbake -e virtual/kernel | sed -n &apos;s/^WORKDIR=&quot;\([^&quot;]*\)&quot;.*/\1/p&apos;)
FRAG=$(find &quot;$KWORKDIR&quot; -maxdepth 5 -name &quot;fragment.cfg&quot; | head -n 1)
echo &quot;FRAG=$FRAG&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.5 fragment를 레이어에 고정(bmp280.cfg)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CFG_DST=~/work/stm32mp257/stm32mp257f-bsp-lab/yocto/meta-myboard/recipes-kernel/linux/linux-stm32mp/openstlinux-weston/bmp280.cfg
install -d &quot;$(dirname &quot;$CFG_DST&quot;)&quot;
cp -v &quot;$FRAG&quot; &quot;$CFG_DST&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(옵션) fragment.cfg를 직접 만들고 싶다면, 최소한 아래 3줄만으로도 동작합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# bme280.cfg

CONFIG_IIO=y
CONFIG_BMP280=m
CONFIG_BMP280_I2C=m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.6 bbappend로 cfg를 실제 빌드에 반영&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;KERNEL_CONFIG_FRAGMENTS&lt;/code&gt;는 보통 &lt;code&gt;${WORKDIR}&lt;/code&gt;에서 cfg를 찾습니다.&lt;br /&gt;
따라서 &lt;strong&gt;cfg를 반드시 &lt;code&gt;SRC_URI&lt;/code&gt;에도 추가&lt;/strong&gt;해야 &lt;code&gt;do_configure&lt;/code&gt; 단계에서 “파일을 못 찾는” 문제를 피할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;meta-myboard/recipes-kernel/linux/linux-stm32mp_%.bbappend&lt;/code&gt; 예시:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
FILESEXTRAPATHS:prepend := &quot;${THISDIR}/${PN}:&quot;

SRC_URI:append = &quot; \
    file://bmp280.cfg \
&quot;

KERNEL_CONFIG_FRAGMENTS:append = &quot; \
    file://bmp280.cfg \
&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/03_bbappend_src_uri_and_cfg.png&quot; alt=&quot;bbappend_src_uri_cfg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;반영 여부는 &lt;code&gt;tmp-glibc/work/.../linux-stm32mp/.../temp/log.do_configure.*&lt;/code&gt;에서&lt;br /&gt;
&lt;code&gt;NOTE: file = file://bmp280.cfg&lt;/code&gt; 이후에 &lt;strong&gt;“Could not find …”가 없어야&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;h3&gt;4.7 사용 중인 커널 설정 변수 확인&lt;/h3&gt;
&lt;p&gt;레시피/클래스에 따라 cfg 병합에 사용되는 변수가 다를 수 있습니다. 빌드 트리에서 실제로 무엇을 쓰는지 아래 출력으로 먼저 확인해두면, bbappend를 망가뜨릴 가능성이 크게 줄어듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake -e virtual/kernel | egrep &apos;^(PN=|PREFERRED_PROVIDER_virtual/kernel=|S=|KERNEL_CONFIG_FRAGMENTS=|KERNEL_FEATURES=|KCONFIG_MODE=|KBUILD_DEFCONFIG=)&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KERNEL_CONFIG_FRAGMENTS&lt;/code&gt;가 존재하면, 4.6처럼 cfg를 &lt;code&gt;SRC_URI&lt;/code&gt;로 가져온 뒤 fragments에 추가하는 방식이 보통 정답입니다.&lt;/li&gt;
&lt;li&gt;만약 다른 메커니즘을 사용한다면(예: &lt;code&gt;KERNEL_FEATURES&lt;/code&gt;), 위 출력에서 힌트를 얻은 뒤 그 변수에 맞춰 반영 방식을 조정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 커널/이미지 빌드 &amp;amp; deploy 산출물 확인&lt;/h2&gt;
&lt;h3&gt;5.1 커널만 재빌드&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bitbake linux-stm32mp -c cleansstate
bitbake linux-stm32mp -c compile
bitbake linux-stm32mp -c deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이미지까지 한 번에 만들려면 아래로 가면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake st-image-weston
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 deploy 결과 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ls -al tmp-glibc/deploy/images/stm32mp25-disco | head
ls -al tmp-glibc/deploy/images/stm32mp25-disco/*.dtb 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. SD 카드 플래시&lt;/h2&gt;
&lt;h3&gt;6.1 FlashLayout TSV 선택&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;deploy/images/&amp;lt;MACHINE&amp;gt;&lt;/code&gt; 아래에 flashlayout TSV가 생성됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd tmp-glibc/deploy/images/stm32mp25-disco
find flashlayout_st-image-weston -maxdepth 2 -type f -name &quot;FlashLayout_sdcard*.tsv&quot; -print
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;OpenSTLinux Weston + OP-TEE 흐름이라면 보통 &lt;code&gt;optee/FlashLayout_sdcard_...-optee.tsv&lt;/code&gt;를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 raw 이미지 생성&lt;/h3&gt;
&lt;p&gt;ST가 제공하는 스크립트로 TSV → raw 이미지를 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd tmp-glibc/deploy/images/stm32mp25-disco

# 예시: optee FlashLayout
TSV=flashlayout_st-image-weston/optee/FlashLayout_sdcard_stm32mp257f-dk-optee.tsv

./scripts/create_sdcard_from_flashlayout.sh &quot;$TSV&quot;
ls -al *.raw 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.3 dd로 SD 카드에 기록&lt;/h3&gt;
&lt;p&gt;먼저 장치가 무엇인지 확실히 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsblk -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그 다음, 마운트된 파티션이 있으면 해제하고 raw를 씁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SD=/dev/sdX   # 반드시 본인 SD 카드 디바이스로 변경
sudo umount ${SD}?* 2&amp;gt;/dev/null || true

RAW=$(ls -t *.raw | head -n 1)
sudo dd if=&quot;$RAW&quot; of=&quot;$SD&quot; bs=8M conv=fsync status=progress
sync
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4 파티션 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo partprobe &quot;$SD&quot; || true
lsblk &quot;$SD&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 부팅 후 BME280(IIO) 검증&lt;/h2&gt;
&lt;h3&gt;7.1 I2C 디바이스 노드 확인&lt;/h3&gt;
&lt;p&gt;BME280 노드가 올라오면 보통 아래처럼 &lt;code&gt;0-0076&lt;/code&gt;이 생깁니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l /sys/bus/i2c/devices/0-0076
cat /sys/bus/i2c/devices/0-0076/modalias
cat /sys/bus/i2c/devices/0-0076/of_node/compatible 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/05_runtime_model_and_i2c_devices.png&quot; alt=&quot;런타임 i2c 디바이스 확인&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;7.2 모듈 존재 확인 + 로드&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;find /lib/modules/$(uname -r) -name &quot;*bmp280*&quot; | head
modprobe bmp280_i2c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/06_module_and_iio_device.png&quot; alt=&quot;bmp280 모듈 로드 후 IIO 디바이스 생성&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;7.3 드라이버 바인딩 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ls -l /sys/bus/i2c/devices/0-0076/driver 2&amp;gt;/dev/null || echo &quot;NOT BOUND&quot;
ls /sys/bus/i2c/drivers | grep -E &quot;bmp280|bme280&quot; || true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/07_driver_bound_and_iio_list.png&quot; alt=&quot;드라이버 바인딩 + IIO 목록&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;7.4 IIO 값 읽기 (온도/기압/습도)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/sys/bus/iio/devices/iio:device*/name&lt;/code&gt; 중 &lt;code&gt;bme280&lt;/code&gt;를 찾아서 읽습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEV=$(for d in /sys/bus/iio/devices/iio:device*; do
  [ &quot;$(cat &quot;$d/name&quot;)&quot; = &quot;bme280&quot; ] &amp;amp;&amp;amp; echo &quot;$d&quot; &amp;amp;&amp;amp; break
done)

echo &quot;DEV=$DEV&quot;

cat &quot;$DEV/in_temp_input&quot;
cat &quot;$DEV/in_pressure_input&quot;
cat &quot;$DEV/in_humidityrelative_input&quot; 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/08_read_values_script.png&quot; alt=&quot;IIO 값 읽기&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>[STM32MP25] OpenSTLinux Wi‑Fi 설정: wpa_supplicant + systemd-networkd</title><link>https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/stm32mpu-wifi-connect/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/stm32mpu-wifi-connect/</guid><description>STM32MP25(OpenSTLinux)에서 wlan0를 wpa_supplicant로 연결하고 systemd-networkd로 DHCP 주소를 받는 방법을 정리합니다.</description><pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;br /&gt;
이번 글에서는 &lt;strong&gt;STM32MP25-DISCO(OpenSTLinux)&lt;/strong&gt; 에서 &lt;strong&gt;Wi‑Fi(WLAN)&lt;/strong&gt; 를 설정해 &lt;code&gt;wlan0&lt;/code&gt;가 &lt;strong&gt;부팅 시 자동으로 연결&lt;/strong&gt;되도록 구성하는 방법을 정리합니다.&lt;/p&gt;
&lt;p&gt;저는 ST Wiki의 가이드를 기준으로 진행했고, 실제 보드에서 확인한 로그/스크린샷을 함께 첨부했습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;구성 개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;wpa_supplicant&lt;/strong&gt;: SSID/PSK 기반 무선 인증 및 연결&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;systemd-networkd&lt;/strong&gt;: 링크 관리 및 &lt;strong&gt;DHCP(IPv4)&lt;/strong&gt; 로 IP 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;전제 조건&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenSTLinux 부팅 완료, root 쉘 접근 가능&lt;/li&gt;
&lt;li&gt;무선 인터페이스가 &lt;code&gt;wlan0&lt;/code&gt;로 잡혀 있음(환경에 따라 &lt;code&gt;wlan1&lt;/code&gt;일 수도 있으니 실제 이름 확인)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. wlan0 상태 확인 및 UP&lt;/h2&gt;
&lt;p&gt;먼저 인터페이스가 보이는지 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig -a
# 또는
ip link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ifconfig wlan0&lt;/code&gt;에서 &lt;code&gt;UP&lt;/code&gt; 표시가 없다면 아직 내려가 있는 상태입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/03-ifconfig-wlan0-down.png&quot; alt=&quot;ifconfig wlan0 출력 예시 (UP 없음)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;올려줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig wlan0 up
# 또는
ip link set wlan0 up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/04-ifconfig-wlan0-up.png&quot; alt=&quot;ifconfig wlan0 출력 예시 (UP 상태)&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. SSID 스캔으로 무선 동작 확인&lt;/h2&gt;
&lt;p&gt;주변 AP가 보이면 드라이버/펌웨어/안테나 쪽은 1차로 정상 가능성이 높습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iw dev wlan0 scan | grep SSID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/05-iw-scan-ssid.png&quot; alt=&quot;iw scan으로 SSID 목록 확인&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. systemd-networkd: wlan0 DHCP(IPv4) 설정&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;systemd-networkd&lt;/code&gt;가 &lt;code&gt;wlan0&lt;/code&gt;에 DHCP를 적용하도록 &lt;code&gt;.network&lt;/code&gt; 파일을 둡니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /lib/systemd/network/51-wireless.network
[Match]
Name=wlan0

[Network]
DHCP=ipv4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /lib/systemd/network/51-wireless.network
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/02-networkd-51-wireless-network.png&quot; alt=&quot;51-wireless.network에 DHCP=ipv4 설정&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. wpa_supplicant: SSID/PSK 설정&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/etc/wpa_supplicant/wpa_supplicant-wlan0.conf&lt;/code&gt; 를 준비합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/wpa_supplicant/wpa_supplicant-wlan0.conf

ctrl_interface=/var/run/wpa_supplicant
eapol_version=1
ap_scan=1
fast_reauth=1

network={
    ssid=&quot;&amp;lt;YOUR_SSID&amp;gt;&quot;
    psk=&quot;&amp;lt;YOUR_PSK&amp;gt;&quot;
    key_mgmt=WPA-PSK
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/07-wpa_supplicant-wlan0-conf.png&quot; alt=&quot;wpa_supplicant-wlan0.conf 편집 화면&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
실무에서는 &lt;code&gt;wpa_passphrase &quot;&amp;lt;SSID&amp;gt;&quot; &quot;&amp;lt;PASS&amp;gt;&quot;&lt;/code&gt;로 &lt;code&gt;psk=&lt;/code&gt;를 생성해 붙여 넣는 편이 안전합니다(따옴표/이스케이프 실수 방지).
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 자동 연결: 서비스 enable + 재시작&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable wpa_supplicant@wlan0.service

systemctl restart systemd-networkd.service
systemctl restart wpa_supplicant@wlan0.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/01-systemctl-enable-wpa_supplicant.png&quot; alt=&quot;wpa_supplicant@wlan0 enable 및 재시작&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 연결 상태 확인&lt;/h2&gt;
&lt;h3&gt;networkctl로 한 번에 보기&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;networkctl --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;연결이 아직 안 된 상황에서는 아래처럼 &lt;code&gt;wlan0&lt;/code&gt;가 &lt;code&gt;no-carrier / unmanaged&lt;/code&gt;로 보일 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/06-networkctl-unmanaged.png&quot; alt=&quot;networkctl 예시: wlan0 no-carrier/unmanaged&quot; /&gt;&lt;/p&gt;
&lt;p&gt;정상 연결 후에는 &lt;code&gt;wlan0&lt;/code&gt;가 &lt;code&gt;routable / configured&lt;/code&gt;로 바뀌는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/08-networkctl-routable.png&quot; alt=&quot;networkctl 예시: wlan0 routable/configured&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;추가 확인(필요 시)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;iw wlan0 link
ip addr show wlan0
ip route
ping -c 3 8.8.8.8
ping -c 3 google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 자주 보는 문제와 점검 포인트&lt;/h2&gt;
&lt;h3&gt;7.1 IP가 안 잡힐 때&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wpa_supplicant&lt;/code&gt;가 실제로 붙었는지 로그 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl status wpa_supplicant@wlan0 --no-pager
journalctl -u wpa_supplicant@wlan0 -b --no-pager | tail -n 200
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd-networkd&lt;/code&gt;가 DHCP를 수행하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl status systemd-networkd --no-pager
journalctl -u systemd-networkd -b --no-pager | tail -n 200
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2 SSID 스캔이 안 될 때&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;인터페이스가 UP인지 재확인&lt;/li&gt;
&lt;li&gt;(있는 경우) rfkill 차단 여부 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;rfkill list
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;드라이버/펌웨어 로그 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dmesg | grep -iE &quot;wlan|wifi|firmware|cfg80211&quot; | tail -n 200
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ST Wiki: How to setup a WLAN connection&lt;br /&gt;
&lt;a href=&quot;https://wiki.st.com/stm32mpu/index.php?title=How_to_setup_a_WLAN_connection&amp;amp;sfr=stm32mpu&quot;&gt;https://wiki.st.com/stm32mpu/index.php?title=How_to_setup_a_WLAN_connection&amp;amp;sfr=stm32mpu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[Yocto] OpenSTLinux 빌드 실패 해결: custom layer로 관리 (numactl/avahi 등)</title><link>https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/openstlinux-failures-meta-myboard/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/openstlinux-failures-meta-myboard/</guid><description>벤더 레이어를 직접 수정하지 않고, 커스텀 레이어(meta-myboard)에서 bbappend로 빌드 실패를 안정적으로 해결하는 패턴을 정리합니다.</description><pubDate>Sun, 04 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;Yocto/OpenEmbedded 빌드는 패키지 수가 많고, 호스트 환경·네트워크·병렬성·QA 규칙 등 변수가 많아서 &lt;strong&gt;레시피 단위로 간헐/고질적인 빌드 실패&lt;/strong&gt;가 발생할 수 있습니다.&lt;br /&gt;
이 글에서는 ST OpenSTLinux 환경에서 실제로 만난 실패 유형을 예시로 들며, &lt;strong&gt;벤더 레이어를 건드리지 않고&lt;/strong&gt; &lt;code&gt;meta-myboard&lt;/code&gt; 같은 커스텀 레이어에서 &lt;code&gt;.bbappend&lt;/code&gt;로 해결하는 방식(유지보수 가능한 패턴)을 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 문제 상황&lt;/h2&gt;
&lt;h3&gt;1.1 numactl: pkg-config QA(sanity) 실패&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/numactl-qa-error.png&quot; alt=&quot;numactl do_populate_sysroot QA 실패 예시&quot; /&gt;&lt;/p&gt;
&lt;p&gt;요약하면 &lt;code&gt;.pc&lt;/code&gt; 파일에 TMPDIR/WORKDIR 같은 “빌드 경로”가 남아 QA 검사에서 실패하는 케이스입니다.&lt;/p&gt;
&lt;h3&gt;1.2 avahi: gobject-introspection(g-ir-scanner) 관련 do_install 실패&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/avahi-do-install-error.png&quot; alt=&quot;avahi do_install 실패 예시&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;g-ir-scanner&lt;/code&gt;가 임시 바이너리를 실행할 때 런타임 로더가 빌드 트리의 &lt;code&gt;libavahi*.so&lt;/code&gt;를 찾지 못해 실패하는 유형입니다.&lt;/p&gt;
&lt;h3&gt;1.3 네트워크 sanity check 실패(Connectivity check)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/connectivity-check-error.png&quot; alt=&quot;connectivity check 실패 예시&quot; /&gt;&lt;/p&gt;
&lt;p&gt;빌드 자체가 아니라 “환경 점검” 단계에서 외부 접속 확인이 실패해 중단되는 케이스입니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 해결: 커스텀 레이어(meta-myboard)에서 bbappend로 오버레이&lt;/h2&gt;
&lt;p&gt;벤더 레이어(예: &lt;code&gt;meta-st-*&lt;/code&gt;, &lt;code&gt;openembedded-core&lt;/code&gt;)를 직접 수정하면 업스트림 업데이트/리베이스 때 충돌이 쉽게 생깁니다.&lt;br /&gt;
가장 유지보수하기 좋은 패턴은&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;내 레이어(&lt;code&gt;meta-myboard&lt;/code&gt;)를 만들고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bitbake-layers add-layer&lt;/code&gt;로 빌드에 추가한 뒤&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bbappend&lt;/code&gt;로 필요한 부분만 오버레이&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.1 meta-myboard 구성(layers.conf/layer.conf)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/layer-conf.png&quot; alt=&quot;meta-myboard layer.conf 작성 예시&quot; /&gt;&lt;/p&gt;
&lt;p&gt;레이어 등록 후에는 아래 명령으로 정상 인식 여부를 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake-layers show-layers | grep -E &quot;myboard|meta-myboard&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 오버레이(appends) 적용 여부 확인&lt;/h3&gt;
&lt;p&gt;어떤 레시피가 &lt;code&gt;.bbappend&lt;/code&gt;로 오버레이되는지는 다음으로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake-layers show-appends | grep -i &amp;lt;recipe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/add-layer-and-avahi-bbappend.png&quot; alt=&quot;show-appends 확인 예시&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. sanity(Connectivity check) 우회/수정: CONNECTIVITY_CHECK_URIS&lt;/h2&gt;
&lt;p&gt;이 경우 &lt;code&gt;local.conf&lt;/code&gt;에서 체크용 URI를 신뢰할 수 있는 엔드포인트로 바꾸는 방식이 현실적입니다.&lt;/p&gt;
&lt;h3&gt;3.1 local.conf 튜닝 예시&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/local-conf-tuning.png&quot; alt=&quot;local.conf 튜닝 예시&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# conf/local.conf
CONNECTIVITY_CHECK_URIS = &quot;https://www.google.com/ https://www.st.com/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 병렬 설치/경쟁 조건(race) 완화: PARALLEL_MAKEINST(필요 시)&lt;/h2&gt;
&lt;p&gt;간헐적으로 “있어야 할 파일이 없다” 유형의 실패가 난다면, Makefile 의존성이 완전하지 않아서 병렬 설치 시 순서가 꼬이는 경우가 있습니다.&lt;br /&gt;
이때는 문제 패키지에 한해 설치 단계의 병렬성을 끄는 것이 우회책이 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;Yocto 문서에서도 **병렬 빌드 실패(parallel build failures)**가 간헐적으로 나타날 수 있으며, 워크어라운드로 &lt;code&gt;PARALLEL_MAKE&lt;/code&gt;를 비우는 방법 등을 언급합니다.&lt;/p&gt;
&lt;h3&gt;4.1 문서 예시&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/yocto-parallel-failures.png&quot; alt=&quot;Yocto 문서의 parallel build failures 설명&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.2 특정 패키지에 한정해 병렬성 조정 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# conf/local.conf
PARALLEL_MAKEINST:pn-binutils-cross-aarch64 = &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;패키지별 override는 실제 실패 패키지 이름(&lt;code&gt;pn-...&lt;/code&gt;)에 맞춰 적용합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;5. avahi do_install 실패 해결: GIR_EXTRA_LIBS_PATH 보강&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;g-ir-scanner&lt;/code&gt;는 임시 바이너리를 실행하면서 심볼을 분석합니다. 이때 동적 로더가 빌드 트리의 &lt;code&gt;.libs&lt;/code&gt;에 있는 &lt;code&gt;libavahi*.so&lt;/code&gt;를 찾지 못하면 실패합니다.&lt;br /&gt;
해결은 &lt;code&gt;avahi_0.8.bbappend&lt;/code&gt;에서 gobject-introspection이 참조하는 라이브러리 경로를 추가해주는 방식이 깔끔합니다.&lt;/p&gt;
&lt;h3&gt;5.1 bbappend 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# meta-myboard/recipes-connectivity/avahi/avahi_0.8.bbappend
# g-ir-scanner executes a temporary binary that must find libavahi-*.so from build tree (.libs)

GIR_EXTRA_LIBS_PATH += &quot;:${B}/avahi-common/.libs:${B}/avahi-client/.libs:${B}/avahi-glib/.libs:${B}/avahi-gobject/.libs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/add-layer-and-avahi-bbappend.png&quot; alt=&quot;avahi bbappend 추가 예시&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.2 적용 후 재빌드&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bitbake -c cleansstate avahi
bitbake avahi
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. numactl pkg-config QA 실패 해결: numa.pc의 prefix 정리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;numa.pc&lt;/code&gt;가 빌드/스테이징 경로를 그대로 품고 있으면 QA에서 “tmpdir sanity test”로 실패합니다.&lt;br /&gt;
Yocto에서 &lt;code&gt;pkg-config&lt;/code&gt;는 래퍼를 통해 sysroot 처리를 하기 때문에 &lt;code&gt;.pc&lt;/code&gt;의 &lt;code&gt;prefix&lt;/code&gt;는 보통 &lt;code&gt;/usr&lt;/code&gt; 형태로 두는 편이 안전합니다.&lt;/p&gt;
&lt;h3&gt;6.1 bbappend 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# meta-myboard/recipes-support/numactl/numactl_%.bbappend

do_install:append() {
    pc=&quot;${D}${libdir}/pkgconfig/numa.pc&quot;
    if [ -f &quot;$pc&quot; ]; then
        # Fix pkg-config file: must not contain TMPDIR/WORKDIR paths
        # Expected: prefix=/usr (sysrooting is handled by pkg-config wrapper)
        sed -i -e &apos;s|^prefix=.*|prefix=/usr|&apos; &quot;$pc&quot;
    fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/numactl-bbappend.png&quot; alt=&quot;numactl bbappend 작성 예시&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.2 적용 후 재빌드&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bitbake -c cleansstate numactl
bitbake numactl
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 빌드 산출물/상태 확인&lt;/h2&gt;
&lt;p&gt;이미지 빌드가 완료되면 &lt;code&gt;tmp-glibc/deploy/images/&amp;lt;machine&amp;gt;/&lt;/code&gt;에 결과물이 생성됩니다.&lt;br /&gt;
또한 “생성된 파일이 없다는 경고”가 나오면, 실제 deploy에 생성되었는지 &lt;code&gt;find&lt;/code&gt;나 &lt;code&gt;ls&lt;/code&gt;로 확인하는 것이 빠릅니다.&lt;/p&gt;
&lt;h3&gt;7.1 deploy 산출물 확인 예시&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./images/deploy-artifacts.png&quot; alt=&quot;deploy 산출물 확인 예시&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Yocto Project Documentation: &lt;a href=&quot;https://docs.yoctoproject.org/&quot;&gt;https://docs.yoctoproject.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yocto Project Reference Manual (Variables 포함): &lt;a href=&quot;https://docs.yoctoproject.org/ref-manual/&quot;&gt;https://docs.yoctoproject.org/ref-manual/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;BitBake User Manual: &lt;a href=&quot;https://docs.yoctoproject.org/bitbake/&quot;&gt;https://docs.yoctoproject.org/bitbake/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenEmbedded-Core(OE-Core) 소스 트리: &lt;a href=&quot;https://git.openembedded.org/openembedded-core/&quot;&gt;https://git.openembedded.org/openembedded-core/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;sanity.bbclass (CONNECTIVITY_CHECK_URIS 등 확인용): &lt;a href=&quot;https://git.openembedded.org/openembedded-core/tree/meta/classes-global/sanity.bbclass&quot;&gt;https://git.openembedded.org/openembedded-core/tree/meta/classes-global/sanity.bbclass&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;gobject-introspection.bbclass (GIR_* 변수 확인용): &lt;a href=&quot;https://git.openembedded.org/openembedded-core/tree/meta/classes-recipe/gobject-introspection.bbclass&quot;&gt;https://git.openembedded.org/openembedded-core/tree/meta/classes-recipe/gobject-introspection.bbclass&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[Yocto] STM32MP2 OpenSTLinux 빌드 가이드</title><link>https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/openstlinux-stm32mp25-build/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/openstlinux-stm32mp25-build/</guid><description>ST OpenSTLinux Distribution Package를 repo sync 단계부터 시작해, WSL2에서 stm32mp25-disco 타깃의 이미지(st-image-weston)를 빌드합니다.</description><pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;strong&gt;STM32MP25(= STM32MP2 시리즈) + OpenSTLinux&lt;/strong&gt; 기준으로, &lt;strong&gt;DP(Distribution Package) 확보 → 환경 초기화 → 이미지(&lt;code&gt;st-image-weston&lt;/code&gt;) 빌드 → 산출물 확인&lt;/strong&gt; 순서로 정리합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;전제: 보드는 &lt;code&gt;stm32mp25-disco(stm32mp257f-dk)&lt;/code&gt;, DISTRO는 &lt;code&gt;openstlinux-weston&lt;/code&gt;을 기준으로 설명합니다.&lt;br /&gt;
(다른 보드도 흐름은 같고, &lt;code&gt;MACHINE/DISTRO&lt;/code&gt; 값만 바뀝니다.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 먼저 짚고 가기: DP vs Starter Package&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Starter Package&lt;/strong&gt;: “보드가 부팅되는 기준 이미지”를 제공(초기 보드 점검/플래시 파티션 구성 확인에 중요)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribution Package(DP)&lt;/strong&gt;: Yocto/OpenEmbedded 기반 &lt;strong&gt;소스 트리&lt;/strong&gt;(레이어/레시피/빌드 스크립트 포함). 여기서 직접 빌드해서 이미지를 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ST도 “Starter Package로 보드가 정상 부팅되는 것”을 먼저 확인한 뒤 DP로 넘어가길 권장합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 작업 디렉토리/디스크/WSL2 주의사항&lt;/h2&gt;
&lt;h3&gt;1.1 경로 길이&lt;/h3&gt;
&lt;p&gt;Yocto는 &lt;strong&gt;절대 경로 길이&lt;/strong&gt; 때문에 파일명이 길어지면 빌드가 실패할 수 있습니다.&lt;br /&gt;
따라서 DP는 너무 깊은 경로(예: &lt;code&gt;/home/&amp;lt;user&amp;gt;/very/long/...&lt;/code&gt;)에 두지 말고, 가능하면 짧게 잡으세요.&lt;/p&gt;
&lt;p&gt;예) &lt;code&gt;~/work/stm32mp25/Distribution-Package&lt;/code&gt; 처럼&lt;/p&gt;
&lt;h3&gt;1.2 디스크 여유&lt;/h3&gt;
&lt;p&gt;DP 자체는 크지 않지만, &lt;strong&gt;빌드 산출물 포함하면 수십 GB&lt;/strong&gt;가 필요합니다(최소 30GB 이상을 잡는 것이 안전).&lt;/p&gt;
&lt;h3&gt;1.3 WSL2 사용 시&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;가능하면 DP/빌드 디렉토리는 &lt;strong&gt;WSL 리눅스 파일시스템(/home)&lt;/strong&gt; 쪽에 두세요. (&lt;code&gt;/mnt/c&lt;/code&gt; 등 Windows 파일시스템은 체감 성능이 크게 떨어질 수 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 호스트(WSL2/Ubuntu) 패키지 설치&lt;/h2&gt;
&lt;p&gt;아래는 ST가 OpenEmbedded/Yocto 빌드를 위해 예시로 제시하는 Ubuntu 패키지 목록(필수/권장 포함)입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ubuntu 24.04 기준&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update

# Yocto/OpenEmbedded 빌드 기본 패키지
sudo apt-get install gawk wget git git-lfs diffstat unzip texinfo gcc-multilib chrpath socat cpio python3 python3-pip python3-pexpect xz-utils debianutils iputils-ping python3-git python3-jinja2 libsdl1.2-dev pylint xterm bsdmainutils libusb-1.0-0 bison flex

# 추가로 자주 필요한 패키지들
sudo apt-get install libssl-dev libgmp-dev libmpc-dev lz4 zstd

# (Ubuntu 22.04 only) ST 문서에 언급된 추가 패키지
sudo apt-get install libegl1-mesa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;repo&lt;/code&gt;를 설치하려면 Python 기본을 &lt;code&gt;python3&lt;/code&gt;로 맞추는 과정이 필요할 수 있어, ST 문서에서는 다음도 안내합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install python-is-python3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/host-deps-warning.png&quot; alt=&quot;호스트 패키지 누락 경고 예시&quot; /&gt;
&lt;em&gt;ST 스크립트가 “필수/권장 패키지 누락”을 감지하면 이런 안내가 뜹니다.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. repo(Repo tool) 설치/검증&lt;/h2&gt;
&lt;p&gt;ST의 DP는 “manifest + repo” 방식으로 내려받는 것이 기본 흐름입니다.&lt;br /&gt;
Ubuntu에서는 우선 apt로 설치를 시도하고, 안 되면 수동 설치(검증 포함)로 진행하면 됩니다.&lt;/p&gt;
&lt;h3&gt;3.1 apt로 설치&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install repo
repo version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 수동 설치(apt가 안 될 때)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export REPO=$(mktemp /tmp/repo.XXXXXXXXX)
curl -o ${REPO} https://storage.googleapis.com/git-repo-downloads/repo
gpg --recv-keys 8BB9AD793E8E6153AF0F9A4416530D5E920F5C65
curl -s https://storage.googleapis.com/git-repo-downloads/repo.asc | gpg --verify - ${REPO} &amp;amp;&amp;amp; install -m 755 ${REPO} ~/bin/repo

# PATH에 ~/bin이 없다면 추가
echo &apos;export PATH=&quot;$HOME/bin:$PATH&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc

repo version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. DP(Distribution Package) 받기: repo init + repo sync&lt;/h2&gt;
&lt;p&gt;이제 실제로 “Distribution-Package” 소스 트리를 받습니다.&lt;/p&gt;
&lt;h3&gt;4.1 작업 디렉토리 만들기&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/work/stm32mp25/Distribution-Package
cd ~/work/stm32mp25/Distribution-Package
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 repo init (ST manifest / tag)&lt;/h3&gt;
&lt;p&gt;ST 문서 기준으로 OpenSTLinux 배포는 **manifest repo + tag(revision)**로 제공됩니다.&lt;br /&gt;
아래에서는 ST 위키/문서에 안내된 &lt;strong&gt;release revision(tag)&lt;/strong&gt; 를 그대로 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repo init -u https://github.com/STMicroelectronics/oe-manifest.git \
  -b refs/tags/openstlinux-6.6-yocto-scarthgap-mpu-v25.08.27
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;참고&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;repo init&lt;/code&gt; 중간에 “ERROR 404”가 보일 수 있으나, ST 문서에서는 프로세스에 영향이 없을 수 있다고 언급합니다.
위 tag가 “항상 최신”은 아닐 수 있습니다. (ST 위키의 minor release 안내를 우선으로 따라가세요.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.3 repo sync&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;repo sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;네트워크/디스크/CPU에 따라 시간이 꽤 걸립니다.&lt;br /&gt;
완료되면 대략 아래와 같은 트리(&lt;code&gt;layers/meta-st/scripts/envsetup.sh&lt;/code&gt; 포함)가 구성됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Distribution-Package/
├─ layers/
│  ├─ meta-openembedded/
│  ├─ meta-st/
│  │  ├─ meta-st-openstlinux/
│  │  ├─ meta-st-stm32mp/
│  │  └─ scripts/envsetup.sh
│  └─ openembedded-core/
└─ ...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 빌드 환경 초기화: envsetup&lt;/h2&gt;
&lt;p&gt;DP 루트에서 아래 명령을 실행합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DISTRO=openstlinux-weston MACHINE=stm32mp25-disco source layers/meta-st/scripts/envsetup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/envsetup-bitbake.png&quot; alt=&quot;envsetup 적용 + 첫 빌드 명령 예시&quot; /&gt;
&lt;em&gt;보통 envsetup 후 같은 셸에서 &lt;code&gt;bitbake &amp;lt;image&amp;gt;&lt;/code&gt;를 이어서 실행합니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;envsetup 스크립트는 보통 다음을 자동으로 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;build-&amp;lt;distro&amp;gt;-&amp;lt;machine&amp;gt;&lt;/code&gt; 형식의 빌드 디렉토리를 생성/이동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conf/local.conf&lt;/code&gt;, &lt;code&gt;conf/bblayers.conf&lt;/code&gt; 기본 구성 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예: &lt;code&gt;build-openstlinux-weston-stm32mp25-disco/&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;반드시 &lt;strong&gt;bash&lt;/strong&gt;에서 실행하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;6. local.conf에서 MACHINE/DISTRO 확인&lt;/h2&gt;
&lt;p&gt;envsetup 이후 현재 디렉토리는 빌드 디렉토리이며, 설정 파일은 &lt;code&gt;conf/&lt;/code&gt;에 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwd
ls conf
grep -nE &apos;^(MACHINE|DISTRO)\s*=&apos; conf/local.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예상:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MACHINE = &quot;stm32mp25-disco&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DISTRO = &quot;openstlinux-weston&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 이미지 빌드: st-image-weston&lt;/h2&gt;
&lt;p&gt;이제 이미지 빌드를 시작합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bitbake st-image-weston
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;첫 빌드는 툴체인/기본 패키지까지 전부 빌드하므로 시간이 많이 걸립니다.&lt;br /&gt;
실패하면 “어떤 레시피/태스크에서 실패했는지”부터 잡는 게 정석입니다.&lt;/p&gt;
&lt;h3&gt;7.1 실패 로그 확인&lt;/h3&gt;
&lt;p&gt;에러 메시지에는 보통 이런 형태가 포함됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ERROR: &amp;lt;recipe&amp;gt; ... do_fetch/do_compile/do_install/do_rootfs ...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;상세 로그는 대개 다음에 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tmp-glibc/work/&amp;lt;...&amp;gt;/temp/log.do_&amp;lt;task&amp;gt;.*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/bitbake-task-failed-example.png&quot; alt=&quot;bitbake 실패 예시(레시피/태스크/로그 경로)&quot; /&gt;
&lt;em&gt;에러 하단에 실패한 레시피/태스크와 &lt;code&gt;log.do_&amp;lt;task&amp;gt;&lt;/code&gt; 경로가 같이 출력됩니다.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 산출물 확인 위치&lt;/h2&gt;
&lt;p&gt;빌드가 끝나면 산출물은 일반적으로 다음에 모입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지/커널/DT/부트로더 등:&lt;br /&gt;
&lt;code&gt;tmp-glibc/deploy/images/stm32mp25-disco/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 &lt;code&gt;.wic&lt;/code&gt;, &lt;code&gt;.ext4&lt;/code&gt;, &lt;code&gt;FlashLayout_*.tsv&lt;/code&gt; 등 “보드에 올릴 때 필요한 것”까지 함께 나오는 경우가 많습니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ST Wiki: STM32MPU Distribution Package (OpenSTLinux 설치/빌드 개요): &lt;a href=&quot;https://wiki.st.com/stm32mpu/wiki/Main_Page&quot;&gt;https://wiki.st.com/stm32mpu/wiki/Main_Page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ST Wiki: PC prerequisites (호스트 패키지/WSL2/AppArmor/Locale 등): &lt;a href=&quot;https://wiki.st.com/stm32mpu/wiki/Main_Page&quot;&gt;https://wiki.st.com/stm32mpu/wiki/Main_Page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yocto Project Documentation: &lt;a href=&quot;https://docs.yoctoproject.org/&quot;&gt;https://docs.yoctoproject.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[Yocto] Yocto 프로젝트 이해하기</title><link>https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/yocto-understanding/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/embedded-system/bsp-build-system/yocto-understanding/</guid><description>Yocto의 구성요소(Poky/OE-Core/BitBake)와 레이어·레시피·빌드 흐름을 STM32MP2(OpenSTLinux) 맥락에서 정리합니다.</description><pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;Yocto를 “리눅스 배포판을 만드는 빌드 프레임워크”라고만 이해하면, 실제로 무엇이 어디에서 결정되고(메타데이터), 무엇이 실행되는지(BitBake) 감이 잘 오지 않습니다.
이 글에서는 &lt;strong&gt;Yocto 프로젝트의 구성요소&lt;/strong&gt;와 &lt;strong&gt;빌드가 진행되는 방식&lt;/strong&gt;을 “레이어/레시피/태스크” 관점에서 정리하고, STM32MP2(OpenSTLinux) 같은 BSP 환경에서 어떻게 적용되는지까지 연결해 설명합니다.&lt;/p&gt;
&lt;h2&gt;Yocto가 하는 일: 소스 → 이미지 산출까지의 “규칙(메타데이터)” 집합&lt;/h2&gt;
&lt;p&gt;Yocto(정확히는 Yocto Project)는 단일 프로그램이라기보다는, 다음을 가능하게 하는 &lt;strong&gt;툴·메타데이터·가이드의 묶음&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 패키지를 어떤 옵션으로 빌드할지&lt;/li&gt;
&lt;li&gt;어떤 루트 파일시스템 구성을 만들지&lt;/li&gt;
&lt;li&gt;부트로더/커널/디바이스 트리까지 포함해 어떤 “이미지”를 만들지&lt;/li&gt;
&lt;li&gt;반복 가능한 빌드(캐시/재현성/QA)로 관리할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심은 “코드”라기보다 &lt;strong&gt;빌드 규칙과 의존 관계를 정의한 메타데이터&lt;/strong&gt;가 빌드를 결정한다는 점입니다.&lt;/p&gt;
&lt;h2&gt;구성요소 빠르게 구분하기: Poky / OE-Core / BitBake&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BitBake&lt;/strong&gt;: 메타데이터(.bb/.bbappend/.conf 등)를 해석해 &lt;strong&gt;태스크(task)를 실행&lt;/strong&gt;하는 빌드 엔진.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OE-Core(OpenEmbedded-Core)&lt;/strong&gt;: 기본 클래스(bbclass), 기본 레시피, QA 규칙 등 “표준 기반”의 핵심 메타데이터.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poky&lt;/strong&gt;: BitBake + OE-Core + 레퍼런스 레이어를 한데 묶어 제공하는 “참조 배포” 성격의 저장소.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, “Yocto를 쓴다”는 말은 보통 &lt;strong&gt;BitBake로 OE 메타데이터를 실행&lt;/strong&gt;해 이미지를 만든다는 뜻에 가깝습니다.&lt;/p&gt;
&lt;h2&gt;레이어(Layer)란: 메타데이터를 관리하는 단위&lt;/h2&gt;
&lt;p&gt;Yocto는 여러 벤더/프로젝트가 동시에 참여하는 구조라서, 변경을 “한 덩어리”로 관리할 수 있어야 합니다.&lt;br /&gt;
그 단위가 &lt;strong&gt;레이어(layer)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;레이어는 보통 &lt;code&gt;conf/layer.conf&lt;/code&gt;를 가지며&lt;/li&gt;
&lt;li&gt;레시피(.bb), 레시피 수정(.bbappend), 설정(.conf) 등을 포함합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bblayers.conf&lt;/code&gt;에 레이어를 추가하면 BitBake가 그 레이어의 메타데이터를 탐색합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;레시피(.bb) / 레시피 수정(.bbappend)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.bb&lt;/code&gt;: “패키지/컴포넌트”를 어떻게 가져오고(fetch), 어떻게 빌드하고(compile), 어떻게 설치하고(install), 어떻게 패키징할지(package) 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bbappend&lt;/code&gt;: 기존 레시피에 &lt;strong&gt;패치/변수/태스크&lt;/strong&gt;를 덧붙이는 방식으로 오버레이(overlay)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;벤더 레이어(예: ST의 OpenSTLinux 레이어)를 그대로 두고, 내가 만든 레이어에서 &lt;code&gt;.bbappend&lt;/code&gt;로 수정하는 패턴이 가장 유지보수에 안전합니다.&lt;/p&gt;
&lt;h2&gt;빌드의 축: MACHINE / DISTRO / IMAGE(레시피)&lt;/h2&gt;
&lt;p&gt;Yocto 빌드에서 “무엇을 만들지”는 크게 세 축으로 잡힙니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MACHINE&lt;/strong&gt;: 타깃 보드/SoC에 대한 설정 (커널 구성, DT, 부트로더, 툴체인 튜닝 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DISTRO&lt;/strong&gt;: 배포판 정책/기본 패키지/기본 설정 (systemd 여부, 보안 정책 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IMAGE 레시피&lt;/strong&gt;: 최종 이미지에 어떤 패키지를 포함할지 (예: &lt;code&gt;core-image-minimal&lt;/code&gt;, &lt;code&gt;st-image-weston&lt;/code&gt; 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실제로는 &lt;code&gt;local.conf&lt;/code&gt;에서 MACHINE/DISTRO를 선택하고, &lt;code&gt;bitbake &amp;lt;image&amp;gt;&lt;/code&gt;로 이미지를 빌드하는 흐름이 일반적입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/bitbake-envsetup.png&quot; alt=&quot;OpenSTLinux envsetup + bitbake 실행 예시&quot; /&gt;
&lt;em&gt;환경 설정 스크립트 적용 후 &lt;code&gt;bitbake&lt;/code&gt;로 이미지 레시피를 빌드하는 전형적인 흐름.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;BitBake 태스크 흐름: Fetch → Configure → Compile → Install → Package → Image&lt;/h2&gt;
&lt;p&gt;BitBake는 레시피에 정의된 태스크를 “그래프(의존 관계)”로 계산한 뒤 실행합니다. 대표적인 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;do_fetch&lt;/code&gt;: 소스 가져오기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_unpack&lt;/code&gt;: 압축 해제/체크아웃&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_configure&lt;/code&gt;: configure/CMake 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_compile&lt;/code&gt;: 컴파일&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_install&lt;/code&gt;: &lt;code&gt;${D}&lt;/code&gt;(destdir)에 설치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_package&lt;/code&gt;: 패키지로 분리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_rootfs&lt;/code&gt;: 루트FS 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do_image&lt;/code&gt;: 이미지 산출&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 실제 파일이 쌓이는 곳이 &lt;code&gt;tmp-glibc/work/...&lt;/code&gt; 같은 작업 디렉토리이고, 결과물이 모이는 곳이 &lt;code&gt;tmp-glibc/deploy/...&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;h2&gt;캐시 관점에서 이해하기: sstate(Shared State)&lt;/h2&gt;
&lt;p&gt;Yocto는 빌드 시간이 길기 때문에, 동일 입력에 대해 “태스크 결과”를 재사용할 수 있도록 &lt;strong&gt;sstate 캐시&lt;/strong&gt;를 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sstate&lt;/code&gt;가 히트하면 해당 태스크는 재실행되지 않고 결과가 재사용됩니다.&lt;/li&gt;
&lt;li&gt;CI나 여러 PC 환경에서 성능을 끌어올릴 때 가장 중요한 최적화 포인트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;실전 팁: 원인 찾을 때 유용한 명령들&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;레이어 확인: &lt;code&gt;bitbake-layers show-layers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;레시피 오버레이 확인: &lt;code&gt;bitbake-layers show-appends | grep -i &amp;lt;recipe&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;변수 확인: &lt;code&gt;bitbake -e &amp;lt;recipe&amp;gt; | less&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;특정 태스크만: &lt;code&gt;bitbake -c &amp;lt;task&amp;gt; &amp;lt;recipe&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;캐시 제거: &lt;code&gt;bitbake -c cleansstate &amp;lt;recipe&amp;gt;&lt;/code&gt; (필요할 때만)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.yoctoproject.org/&quot;&gt;Yocto Project Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.yoctoproject.org/ref-manual/&quot;&gt;Yocto Project Reference Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.yoctoproject.org/bitbake/&quot;&gt;BitBake User Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.openembedded.org/openembedded-core/&quot;&gt;OpenEmbedded-Core(OE-Core) 소스 트리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.openembedded.org/wiki/Main_Page&quot;&gt;OpenEmbedded Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.st.com/stm32mpu/wiki/Main_Page&quot;&gt;ST STM32MPU Wiki(메인)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.st.com/stm32mpu/wiki/Category:OpenSTLinux_distribution&quot;&gt;(ST) OpenSTLinux 배포/SDK 관련 문서(위 Wiki 내)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[WSL2] WSL2로 USB 디바이스 연결하기 (usbipd-win 가이드)</title><link>https://pingu52.vercel.app/posts/system-engineering/virtualization/wsl2-usbipd-setup/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/system-engineering/virtualization/wsl2-usbipd-setup/</guid><description>WSL2에서 USB 장치가 안 보일 때, usbipd-win(USB/IP)으로 USB-UART/디버그 프로브(ST-Link 등)를 WSL2(Ubuntu)로 연결하는 방법을 정리합니다.</description><pubDate>Mon, 29 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;지난 글에서는 Windows 11에서 &lt;strong&gt;WSL2(Ubuntu) 빌드 환경&lt;/strong&gt;을 세팅했습니다.&lt;br /&gt;
(&lt;a href=&quot;/posts/system-engineering/virtualization/wsl2-install&quot;&gt;지난 글: WSL2 세팅&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;이번 글은 그 다음 단계입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows에 USB 디바이스는 정상 인식되는데 WSL2에서는 인식이 안되는 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해결은 &lt;strong&gt;usbipd-win(USB/IP)&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;:::note
“Windows에 꽂힌 USB를 WSL2로 패스스루해서 Linux에서 네이티브처럼 쓰기”에 초점을 둡니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;usbipd-win이란?&lt;/h2&gt;
&lt;p&gt;Windows에서 USB 디바이스를 &lt;strong&gt;USB/IP 방식으로 공유&lt;/strong&gt;하고, WSL2가 그 장치를 네트워크로 “붙여서” 쓰게 해주는 도구입니다.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;dorssel/usbipd-win&quot;}&lt;/p&gt;
&lt;p&gt;(WSL 관련 프로젝트도 함께 참고하면 좋습니다.)&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;microsoft/WSL&quot;}&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows 11 + WSL2 환경에서 &lt;strong&gt;USB 디바이스를 WSL2로 패스스루&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usbipd list → (관리자) bind → attach → (WSL에서 확인) → detach&lt;/code&gt; 흐름으로 끝내기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;준비&lt;/h2&gt;
&lt;h3&gt;0. Windows / WSL 요구사항&lt;/h3&gt;
&lt;p&gt;:::important
Microsoft 공식 가이드 기준으로 &lt;strong&gt;Windows 11 Build 22000 이상&lt;/strong&gt;, WSL 커널 버전 조건이 있습니다.&lt;br /&gt;
가장 안전한 접근은 &lt;code&gt;wsl --update&lt;/code&gt;로 WSL을 최신으로 올리는 것입니다.
:::&lt;/p&gt;
&lt;p&gt;Windows에서 한 번에 정리:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# (선택) WSL이 꼬였을 때는 먼저 완전 종료
wsl --shutdown

# 커널/WSL 업데이트
wsl --update
wsl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WSL(Ubuntu)에서 커널 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;혹은 PowerShell에서 커널 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;1. usbipd-win 설치 (Windows)&lt;/h2&gt;
&lt;p&gt;두 가지 중 편한 방법으로 설치합니다. (1번 권장)&lt;/p&gt;
&lt;h3&gt;1.1 &lt;code&gt;.msi&lt;/code&gt; 설치&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dorssel/usbipd-win/releases&quot;&gt;usbipd-win Github Release Page&lt;/a&gt;에서 최신 버전 다운로드 후 설치&lt;/p&gt;
&lt;h3&gt;1.2 &lt;code&gt;winget&lt;/code&gt; 설치&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;winget install --interactive --exact dorssel.usbipd-win
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 후 터미널을 다시 열어 PATH 반영을 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd --version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 연결할 USB 디바이스 확인 (Windows)&lt;/h2&gt;
&lt;p&gt;PowerShell에서 현재 연결된 장치를 나열합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예시 출력:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BUSID  VID:PID    DEVICE                                    STATE
2-1    0483:3748  ST-LINK/V2-1                              Not shared
2-2    10c4:ea60  Silicon Labs CP210x USB to UART Bridge    Not shared
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;strong&gt;BUSID&lt;/strong&gt;(예: &lt;code&gt;2-2&lt;/code&gt;)를 기억합니다.&lt;/p&gt;
&lt;p&gt;:::tip
예전 글/예전 버전에서는 &lt;code&gt;usbipd wsl list&lt;/code&gt; 같은 하위 명령이 보일 수 있는데, 최근 가이드는 &lt;code&gt;usbipd list&lt;/code&gt;를 기본으로 사용합니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. (중요) 디바이스 공유하기: bind (Windows 관리자)&lt;/h2&gt;
&lt;p&gt;WSL에 붙이기 전에 먼저 &lt;strong&gt;공유(share)&lt;/strong&gt; 해야 합니다.&lt;br /&gt;
이 단계는 &lt;strong&gt;관리자 권한이 필요&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd bind --busid 2-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다시 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;STATE&lt;/code&gt;가 &lt;code&gt;Shared&lt;/code&gt;로 바뀌면 정상입니다.&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;bind&lt;/code&gt;는 &lt;strong&gt;영구(persistent)&lt;/strong&gt; 입니다. 재부팅해도 유지되며, 보통 디바이스당 &lt;strong&gt;한 번만&lt;/strong&gt; 해두면 됩니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. WSL2로 붙이기: attach&lt;/h2&gt;
&lt;p&gt;:::important
attach 전에 &lt;strong&gt;WSL 터미널을 미리 열어두세요.&lt;/strong&gt;&lt;br /&gt;
WSL2이 살아 있어야 attach가 안정적으로 됩니다.
:::&lt;/p&gt;
&lt;p&gt;이제 attach:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd attach --wsl --busid 2-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;STATE&lt;/code&gt;가 &lt;code&gt;Attached&lt;/code&gt;로 나오면 성공입니다.&lt;/p&gt;
&lt;p&gt;:::warning
디바이스가 WSL에 붙어 있는 동안 &lt;strong&gt;Windows에서는 해당 USB를 사용할 수 없습니다.&lt;/strong&gt;&lt;br /&gt;
(Windows 쪽 앱/드라이버가 잡고 있으면 attach가 실패할 수 있습니다.)
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. WSL(Ubuntu)에서 인식 확인&lt;/h2&gt;
&lt;p&gt;WSL로 돌아가서 확인합니다.&lt;/p&gt;
&lt;h3&gt;5.1 &lt;code&gt;lsusb&lt;/code&gt; 확인&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lsusb&lt;/code&gt;가 없다면 설치:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install -y usbutils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsusb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;연결한 장치(예: CP210x, ST-Link)가 보이면 1차 성공입니다.&lt;/p&gt;
&lt;h3&gt;5.2 시리얼(UART)이라면 &lt;code&gt;/dev/tty*&lt;/code&gt; 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ls -l /dev/ttyUSB* /dev/ttyACM* 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
CH340/CP210x 같은 USB-UART는 장치에 따라 커널 모듈/권한 이슈로 &lt;code&gt;/dev/ttyUSB0&lt;/code&gt;가 바로 안 생길 수 있습니다.&lt;br /&gt;
우선 &lt;code&gt;dmesg -w&lt;/code&gt;로 연결 이벤트를 확인하고, 드라이버/권한 문제를 분리해서 보세요.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 연결 해제: detach (Windows)&lt;/h2&gt;
&lt;p&gt;작업이 끝났으면 detach로 Windows에 돌려줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd detach --busid 2-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;code&gt;attach&lt;/code&gt;는 &lt;strong&gt;비영구(non-persistent)&lt;/strong&gt; 입니다.&lt;br /&gt;
재부팅/WSL 재시작/USB 재연결/보드 리셋 시에는 다시 attach가 필요할 수 있습니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. (선택) 공유 해제: unbind (Windows 관리자)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bind&lt;/code&gt;는 “공유 상태를 유지”하므로, 공유 자체를 끄고 싶다면 unbind를 사용합니다.&lt;br /&gt;
이 단계도 &lt;strong&gt;관리자 권한이 필요&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usbipd unbind --busid 2-2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 자주 겪는 문제 (Troubleshooting)&lt;/h2&gt;
&lt;h3&gt;8.1 &lt;code&gt;Device is not shared&lt;/code&gt; / &lt;code&gt;Not shared&lt;/code&gt; 상태에서 attach 시도&lt;/h3&gt;
&lt;p&gt;먼저 &lt;code&gt;bind&lt;/code&gt;를 해야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 관리자 PowerShell
usbipd bind --busid &amp;lt;BUSID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2 &lt;code&gt;The device appears to be used by Windows&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Windows에서 해당 장치를 잡고 있는 프로그램을 종료하세요.&lt;br /&gt;
(예: 시리얼 모니터, IDE, 드라이버 유틸 등)&lt;/p&gt;
&lt;h3&gt;8.3 보드 리셋/부트로더 진입으로 BUSID가 바뀌는 경우&lt;/h3&gt;
&lt;p&gt;펌웨어 업로드/부트로더 진입에서 장치가 재인식되면 BUSID가 바뀌며 attach가 끊길 수 있습니다.&lt;br /&gt;
이때는 &lt;code&gt;--auto-attach&lt;/code&gt; + &lt;code&gt;--hardware-id (VID:PID)&lt;/code&gt; 조합이 도움이 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 예시: VID:PID 기반 자동 재-attach (실행 중인 동안 루프로 동작)
usbipd attach --wsl --auto-attach --hardware-id 10c4:ea60
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;code&gt;--auto-attach&lt;/code&gt;는 서비스처럼 상시 백그라운드에 붙는 개념이 아니라, &lt;strong&gt;해당 명령이 실행 중인 동안&lt;/strong&gt; 재연결을 시도하는 형태입니다.
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows에서: &lt;code&gt;usbipd list → (관리자) usbipd bind → usbipd attach&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WSL에서: &lt;code&gt;lsusb&lt;/code&gt; / &lt;code&gt;/dev/ttyUSB*&lt;/code&gt; 확인 후 사용&lt;/li&gt;
&lt;li&gt;끝나면: &lt;code&gt;usbipd detach&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;공유까지 끄고 싶으면: &lt;code&gt;(관리자) usbipd unbind&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft 공식 가이드: Connect USB devices (WSL)&lt;br /&gt;
&lt;a href=&quot;https://learn.microsoft.com/windows/wsl/connect-usb&quot;&gt;https://learn.microsoft.com/windows/wsl/connect-usb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;usbipd-win WSL 지원 위키&lt;br /&gt;
&lt;a href=&quot;https://github.com/dorssel/usbipd-win/wiki/WSL-support&quot;&gt;https://github.com/dorssel/usbipd-win/wiki/WSL-support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;usbipd-win GitHub 저장소&lt;br /&gt;
&lt;a href=&quot;https://github.com/dorssel/usbipd-win&quot;&gt;https://github.com/dorssel/usbipd-win&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>WSL2 세팅</title><link>https://pingu52.vercel.app/posts/system-engineering/virtualization/wsl2-install/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/system-engineering/virtualization/wsl2-install/</guid><description>Windows 11에서 WSL2로 Linux 빌드 환경을 안정적으로 세팅하는 방법을 정리합니다.</description><pubDate>Sat, 27 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, pingu52입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 Windows 11 기준으로 &lt;strong&gt;WSL2(Windows Subsystem for Linux)&lt;/strong&gt; 를 이용해&lt;br /&gt;
&lt;strong&gt;안정적인 Linux 빌드 환경(=임베디드/Yocto/크로스 컴파일용)&lt;/strong&gt; 을 만드는 방법을 정리합니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows 11에서 &lt;strong&gt;WSL2 + Ubuntu&lt;/strong&gt; 설치&lt;/li&gt;
&lt;li&gt;기본 개발툴(컴파일러/디버깅/네트워크) 세팅&lt;/li&gt;
&lt;li&gt;임베디드 빌드(특히 Yocto)에서 &lt;strong&gt;성능/경로/디스크&lt;/strong&gt; 이슈 피하기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;준비&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows 11 최신 업데이트 권장&lt;/li&gt;
&lt;li&gt;디스크 여유(중요):
&lt;ul&gt;
&lt;li&gt;일반 임베디드 빌드: 수십 GB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yocto&lt;/strong&gt;: 프로젝트에 따라 &lt;strong&gt;100GB~300GB+&lt;/strong&gt; 도 흔함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Windows Terminal(있으면 편함)&lt;/li&gt;
&lt;li&gt;관리자 권한 PowerShell/터미널&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. WSL2 설치&lt;/h2&gt;
&lt;p&gt;관리자 PowerShell을 열고 아래 실행:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기본 배포판(보통 Ubuntu)이 설치됩니다.&lt;br /&gt;
이미 설치되어 있다면 WSL 자체 업데이트만 해도 좋습니다.&lt;/p&gt;
&lt;p&gt;WSL 버전/커널을 최신으로 업데이트:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --update
wsl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 및 업데이트 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(선택) 설치 할 수 있는 리눅스 배포판 확인&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --list --online
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Ubuntu 실행 및 초기 설정&lt;/h2&gt;
&lt;p&gt;설치 후 Ubuntu를 실행하면 사용자 계정을 만들라고 나옵니다.&lt;/p&gt;
&lt;p&gt;Ubuntu 들어오면 가장 먼저 업데이트:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. (중요) 빌드 작업은 /mnt/c에서 하지 말기&lt;/h2&gt;
&lt;p&gt;WSL에서 Windows 드라이브는 &lt;code&gt;/mnt/c&lt;/code&gt;, &lt;code&gt;/mnt/d&lt;/code&gt;처럼 마운트됩니다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;strong&gt;대규모 빌드(특히 Yocto)&lt;/strong&gt; 는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일 I/O가 엄청 많고&lt;/li&gt;
&lt;li&gt;작은 파일을 수만~수십만 개 만지기 때문에&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;/mnt/c&lt;/code&gt; 같은 Windows 파일시스템에서 돌리면 속도도 느리고, 권한/대소문자/링크 문제로 터질 확률이 올라갑니다.&lt;/p&gt;
&lt;h3&gt;권장 구조&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;소스/빌드 디렉터리는 &lt;strong&gt;WSL의 Linux 파일시스템(예: &lt;code&gt;~/repo&lt;/code&gt;)&lt;/strong&gt; 에 둔다.&lt;/li&gt;
&lt;li&gt;Windows에서 공유가 필요하면 나중에 &lt;code&gt;\\wsl$&lt;/code&gt;로 접근한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예시:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/repo
cd ~/repo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 탐색기에서 접근하려면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;탐색기 주소창에 &lt;code&gt;\\wsl$\Ubuntu\home\&amp;lt;username&amp;gt;\repo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 기본 개발 툴 설치&lt;/h2&gt;
&lt;p&gt;임베디드/빌드 환경이면 아래는 거의 고정으로 설치합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y \
  build-essential \
  gcc g++ make cmake ninja-build \
  git curl wget unzip zip tar \
  pkg-config \
  python3 python3-pip python3-venv \
  ca-certificates gnupg lsb-release \
  openssh-client \
  net-tools iputils-ping dnsutils \
  vim nano
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추가로 편의 툴:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y htop tree jq ripgrep fd-find
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. VS Code 연동 (Remote - WSL)&lt;/h2&gt;
&lt;p&gt;Windows에 VS Code 설치 후 확장:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Remote - WSL&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ubuntu에서 프로젝트 폴더로 이동 후:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 실행 시 WSL 서버가 설치되면서 자동으로 연결됩니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Yocto 빌드 준비&lt;/h2&gt;
&lt;p&gt;Yocto는 의존성이 많아서 최소 패키지 세트를 같이 정리해둡니다.&lt;br /&gt;
(배포판/Yocto 버전에 따라 조금 다를 수 있음)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y \
  gawk wget git diffstat unzip texinfo gcc build-essential chrpath socat \
  cpio python3 python3-pip python3-pexpect xz-utils debianutils iputils-ping \
  python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev pylint3 xterm \
  python3-subunit mesa-common-dev zstd lz4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yocto는 디스크/CPU/메모리를 정말 많이 씁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가능하면 &lt;strong&gt;WSL용 디스크 여유&lt;/strong&gt; 크게 확보&lt;/li&gt;
&lt;li&gt;빌드 폴더를 Linux 파일시스템에 두기(중요)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;설치는 &lt;code&gt;wsl --install&lt;/code&gt;로 끝내고, Ubuntu에서는 &lt;code&gt;apt update/upgrade&lt;/code&gt;부터 진행한다.&lt;/li&gt;
&lt;li&gt;대규모 빌드(특히 Yocto)는 &lt;strong&gt;절대 &lt;code&gt;/mnt/c&lt;/code&gt;에서 돌리지 말고&lt;/strong&gt;, WSL의 Linux 파일시스템(&lt;code&gt;~/work&lt;/code&gt;)에 둔다.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>핑구 == 주인장.</title><link>https://pingu52.vercel.app/posts/log-life/daily/pingu52/</link><guid isPermaLink="true">https://pingu52.vercel.app/posts/log-life/daily/pingu52/</guid><description>나는 핑구다.</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;핑구 = 블로그 주인장&lt;/h1&gt;
&lt;p&gt;나는 핑구다.&lt;br /&gt;
정확히는, &lt;strong&gt;핑구처럼 살고 싶은 사람&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;말은 줄이고, 기록은 남기고,&lt;br /&gt;
필요할 땐 문을 쾅 닫고(마음속으로),&lt;br /&gt;
다시 돌아와서 정리한다.&lt;br /&gt;
대부분의 문제는… 일단 한 번 “노트노트” 하고 나면 생각보다 풀린다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;이 블로그는 뭐 하는 곳?&lt;/h2&gt;
&lt;p&gt;대단한 포부보다는 &lt;strong&gt;쌓이는 기록&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;삽질하다가 건진 것들&lt;/li&gt;
&lt;li&gt;헷갈려서 다시 보려고 적어둔 것들&lt;/li&gt;
&lt;li&gt;나중의 내가 고마워할 메모들&lt;/li&gt;
&lt;li&gt;가끔은 그냥 생각 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;깔끔한 글도 있고, 투박한 글도 있다.&lt;br /&gt;
공통점은 하나: &lt;strong&gt;내가 진짜로 겪은 것&lt;/strong&gt;만 적는다.&lt;/p&gt;
</content:encoded></item></channel></rss>