<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>xoey.log</title>
        <link>https://velog.io/</link>
        <description>[Roman 8:18] consider that our present sufferings are not worth comparing with the glory that will be revealed in us.</description>
        <lastBuildDate>Sun, 09 Mar 2025 08:27:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>xoey.log</title>
            <url>https://velog.velcdn.com/images/jayy_19/profile/7acc5bc3-9f4d-4e8f-9fad-a28dfd1279f3/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. xoey.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jayy_19" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Programmers Lv.2] 2개 이하로 다른 비트 (테케 10, 11 틀린 이유 분석)]]></title>
            <link>https://velog.io/@jayy_19/Programmers-Lv.2-2%EA%B0%9C-%EC%9D%B4%ED%95%98%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%EB%B9%84%ED%8A%B8-%ED%85%8C%EC%BC%80-10-11-%ED%8B%80%EB%A6%B0-%EC%9D%B4%EC%9C%A0-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@jayy_19/Programmers-Lv.2-2%EA%B0%9C-%EC%9D%B4%ED%95%98%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%EB%B9%84%ED%8A%B8-%ED%85%8C%EC%BC%80-10-11-%ED%8B%80%EB%A6%B0-%EC%9D%B4%EC%9C%A0-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Sun, 09 Mar 2025 08:27:52 GMT</pubDate>
            <description><![CDATA[<p>알고리즘을 열심히 풀고 있는 요즈음인데 정작 블로그 글은 안 쓴 지 천 만 년이 되어가 오랜만에 작성한다.</p>
<p>이번 글에서는 분명 시간 복잡도도 만족하고, 분석도 잘 한 것 같은데 특정 테케에서 아예 오답이 나와 왜일까 고민해본 과정을 적어 내려가보려고 한다.</p>
<h1 id="🔴-첫-번째-시도">🔴 첫 번째 시도</h1>
<pre><code class="language-javascript">function countDifferentBits(target, compare) {
  let t = target.toString(2);
  const c = compare.toString(2);

  t = &#39;0&#39;.repeat(c.length - t.length) + t;

  let count = 0;
  for (let i = 0; i &lt; t.length; i++) {
    if (t[i] !== c[i]) count++;
  }

  return count;
}

function solution(numbers) {
  return numbers.map((number) =&gt; {
    for (let i = number + 1; ; i++) {
      const differentCount = countDifferentBits(number, i);
      if (differentCount &lt;= 2) return i;
      else continue;
    }
  });
}</code></pre>
<p>브루트포스 방식으로 일일이 계산해주었다.
<code>number</code>에서 숫자를 하나씩 증가시키면서 비트가 다를 때마다 count해주고, 이 개수가 2개 이하이면 return하는 방식.
<code>numbers</code> 길이가 최대 100,000개이므로 당연히 시간 초과로 탈락!</p>
<h1 id="🔴-두-번째-시도">🔴 두 번째 시도</h1>
<p>시간 초과가 날 것을 알았지만 완탐 밖에 떠오르지 않아서 지피티에게 도움 요청.
<strong>Bit Masking + Greedy 알고리즘</strong>을 제시해주었다.</p>
<p>문제를 분석해보면 비트가 2개 이하로 다르면서 가장 작은 수를 찾는 것이기 때문에, 어떻게 하면 _<strong>비트를 2개 이하로 조작</strong>_할까 고민해보면 좋을 것 같다.</p>
<h2 id="1-짝수일-경우">1. 짝수일 경우</h2>
<ul>
<li><p>짝수는 항상 맨 뒤 비트가 0으로 끝난다. (<code>...0</code>)</p>
</li>
<li><p>따라서 +1을 할 경우, 비트가 1개만 변경되면서 가장 작은 값을 만족한다. <code>f(x) = x + 1</code></p>
<h2 id="2-홀수일-경우">2. 홀수일 경우</h2>
</li>
<li><p>홀수는 맨 뒤 비트가 1로 끝난다. (<code>...1</code>)</p>
</li>
<li><p>홀수의 경우 가장 작은 <code>0</code>을 찾아서 <code>1</code>로 바꾸어준다. </p>
<ul>
<li>예: <code>7 (0111)</code> -&gt; <code>15 (1111)</code></li>
</ul>
</li>
<li><p>이후, 바꾸어준 자리 바로 오른쪽의 <code>1</code>을 <code>0</code>으로 변경하면 최소 변화 조건을 만족하면서 가장 작은 수가 된다.</p>
<ul>
<li><p>예: <code>7 (0111)</code> -&gt; <code>15 (1111)</code> - &gt; <code>11 (1011)</code></p>
<h2 id="3-최종-코드">3. 최종 코드</h2>
<pre><code class="language-javascript">function solution(numbers) {
return numbers.map(x =&gt; {
  if (x % 2 === 0) return x + 1;

  let bit = 1;
  while ((x &amp; bit) !== 0) bit &lt;&lt;= 1;

  return x + bit - (bit &gt;&gt; 1);
});
}</code></pre>
<h3 id="31-while-x--bit--0-bit--1">3.1. <code>while ((x &amp; bit) !== 0) bit &lt;&lt;= 1;</code></h3>
</li>
</ul>
</li>
<li><p><code>x &amp; bit</code>: <code>x</code>의 특정 비트가 1인지 확인하는 연산</p>
</li>
<li><p><code>&lt;&lt;</code>, <code>&gt;&gt;</code>: 비트를 해당 방향(<code>&lt;</code>, <code>&gt;</code>)으로 이동시키는 연산</p>
</li>
<li><p><code>&lt;&lt;=</code>, <code>&gt;&gt;=</code>: 비트를 해당 방향(<code>&lt;</code>, <code>&gt;</code>)으로 이동시키고, 이를 비트에 <strong>할당</strong>한다.</p>
<pre><code class="language-javascript">bit = 1;   // 0001  (1의 자리)
x &amp; bit = 1  (0이 아님, 계속 이동)
</code></pre>
</li>
</ul>
<p>bit &lt;&lt;= 1  // 0010  (2의 자리)
x &amp; bit = 2  (0이 아님, 계속 이동)</p>
<p>bit &lt;&lt;= 1  // 0100  (3의 자리)
x &amp; bit = 4  (0이 아님, 계속 이동)</p>
<p>bit &lt;&lt;= 1  // 1000  (4의 자리)
x &amp; bit = 0  (4의 자리 비트가 0임. 종료)</p>
<pre><code>결과적으로 `while ((x &amp; bit) !== 0) bit &lt;&lt;= 1;`는 `bit`를 `0001` → `0010` → `0100` → `1000`으로 이동하면서 `0`을 찾게 된다.
### 3.2. `x + bit - (bit &gt;&gt; 1);`
- `x + bit`는 처음 `0`이 나온 자리를 `1`로 변경한다.
    - `x = 7 (0111)`
    - `bit = 8 (1000)`
    - `x + bit = 1111`
- `bit &gt;&gt; 1`는 처음 `0`이 나온 자리의 바로 오른쪽 비트를 `0`으로 변경한다.
    - `bit &gt;&gt; 1 = 0100`
    - `x + bit - (bit &gt;&gt; 1) = 1111 - 0100 = 1011`
## 4. 그런데..
![](https://velog.velcdn.com/images/jayy_19/post/e142f1bf-1fb9-48db-8d1e-a0958dad7327/image.png)

테케 10, 11에서 계속 오답이 뜨는 나..
시간 복잡도는 해결되었는데 특정 케이스에서 아예 오답이 뜬다고?

![](https://velog.velcdn.com/images/jayy_19/post/14d2e4dd-00e8-46f5-b44a-709d51b624c6/image.jpeg)

원인이 무엇인고.. 고민해보기로 했다.

# 🟢 세 번째 시도
프로그래머스 질문하기 페이지에서 나처럼 테케 10, 11이 틀린 사람들을 살펴봤다.


몇몇 답변으로는 N이 굉장히 큰 수일 때 어쩌고.. 라고 해서 [MDN document](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Bitwise_AND)를 살펴 보았는데,
![](https://velog.velcdn.com/images/jayy_19/post/af114f89-2ffe-4d7d-b222-c5d3de3c2d36/image.png)

자바스크립트에서는 비트 연산자가 32비트 정수로 자동 변환되기 때문에, `10^15` 같은 큰 숫자를 다룰 때 문제가 발생할 수 있다고 한다. 
즉, **비트 연산(`&amp;`, `|`, `^`, `&lt;&lt;`, `&gt;&gt;`)을 사용하면 32비트 초과 부분이 잘려버릴 수 있다는 것!**

```javascript
console.log(9126805503 | 0);  // 536884607 (잘림)
console.log(8796093022207 | 0);  // -2147483648 (음수가 됨)</code></pre><p>이를 해결하기 위해 <strong>BigInt</strong>를 사용하여 처리했다.</p>
<pre><code class="language-javascript">function solution(numbers) {
  return numbers
    .map((x) =&gt; {
      x = BigInt(x);

      if (x % 2n === 0n) return x + 1n;

      let bit = 1n;
      while ((x &amp; bit) !== 0n) bit &lt;&lt;= 1n;
      return x + bit - (bit &gt;&gt; 1n);
    })
    .map(Number);
}</code></pre>
<p>BigInt를 사용하면 32비트 제한을 벗어나 큰 숫자도 안전하게 비트 연산이 가능해진다.
연산 후 <code>map(Number)</code>를 활용해 일반 숫자로 변환해주면?</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/494fb667-eb92-4f1d-ad9d-d273ea0b3896/image.png" alt=""></p>
<p>👏🏻👏🏻👏🏻 정답입니다~</p>
<hr>
<p>사실 비트마스킹 방식 자체가 나에게 익숙하지 않아서, 이번에 비트 연산자를 공부하며 다시 개념을 정리했다.</p>
<p>이외에도 32비트를 초과하는 숫자에서 비트 연산자를 사용할 경우 값이 잘릴 수 있다는 점과, 이를 방지하기 위해 BigInt를 활용하는 방법을 MDN 문서를 통해 직접 확인하며 학습하는 등 여러 새로운 개념을 접하고 적용해 본 문제였기 때문에 이렇게 기록해본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] Blocking & Non-blocking IO]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-Blocking-Non-blocking-IO</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-Blocking-Non-blocking-IO</guid>
            <pubDate>Thu, 09 Jan 2025 00:33:07 GMT</pubDate>
            <description><![CDATA[<h1 id="1-io-작업이란">1. I/O 작업이란?</h1>
<p><strong>I/O(Input/Output)</strong>는 컴퓨터나 서버가 파일을 읽거나, 네트워크 통신을 하거나, 데이터베이스에서 데이터를 주고받는 작업을 의미한다. 예를 들어, 네트워크에서 한 컴퓨터가 데이터를 보내고(send), 다른 컴퓨터가 그 데이터를 받는(read) 것을 I/O 작업이라고 한다.</p>
<p>I/O는 User 레벨에서 직접 수행할 수 없고, Kernel에서 수행된다. 유저 프로세스는 커널에 요청만 할 수 있고, 결과과 반환될 때까지 기다리게 된다.</p>
<h1 id="2-blocking-io">2. Blocking I/O</h1>
<p>I/O 작업이 진행되는 동안, 유저 프로세스가 차단되어 기다리는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/c67ded8c-be2c-4d64-8a08-393e8452fe9b/image.png" alt=""></p>
<blockquote>
<h4 id="📌-동작-방식">📌 <strong>동작 방식</strong></h4>
</blockquote>
<ol>
<li>유저가 커널에 <code>read</code> 작업을 요청하면, 제어권을 커널에 넘겨준다.</li>
<li>데이터가 입력될 때까지 유저는 대기하며, 다른 작업을 할 수 없다.</li>
<li>데이터가 입력되고 나서야 커널이 유저에게 결과를 전달하며, 유저는 제어권을 다시 받아 작업을 재개할 수 있다.</li>
</ol>
<p>이 방식은 다른 작업을 진행하지 못하기 때문에 자원이 낭비된다는 단점이 있다.</p>
<h1 id="3-non-blocking-io">3. Non-blocking I/O</h1>
<p>I/O 작업이 진행되는 동안에도 유저 프로세스가 차단되지 않고 다른 작업을 계속 진행할 수 있는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3f4d878a-5cbf-4c58-8352-4088ee572698/image.png" alt=""></p>
<blockquote>
<h4 id="📌-동작-방식-1">📌 <strong>동작 방식</strong></h4>
</blockquote>
<ol>
<li>유저가 커널에 <code>read</code> 작업을 요청하면, 바로 결과가 반환된다.</li>
<li>입력된 데이터가 없으면 커널은 <code>EWOULDBLOCK</code> 메세지를 반환하여 데이터가 없다는 것을 알린다.</li>
<li>유저는 제어권을 유지하며 다른 작업을 계속 진행할 수 있다. 데이터가 준비될 때까지 1, 2번 과정을 반복한다.</li>
<li>데이터가 입력되면 커널은 그 데이터를 유저에게 전달해준다.</li>
</ol>
<p>이 방식은 프로그램이 멈추지 않고 계속 작업을 진행할 수 있다는 장점이 있다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://didu-story.tistory.com/307">[Network] Blocking I/O 와 Non-Blocking I/O 에 대해서 알아보자</a></li>
<li><a href="https://developer.ibm.com/articles/l-async/">Boost application performance using asynchronous I/O</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] Blocking/Non-blocking & Synchronous/Asynchronous]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-BlockingNon-blocking-SynchronousAsynchronous</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-BlockingNon-blocking-SynchronousAsynchronous</guid>
            <pubDate>Wed, 08 Jan 2025 00:23:47 GMT</pubDate>
            <description><![CDATA[<p>프로그래밍에서 자주 사용되는 개념인 <strong>Synchronous/Asynchronous</strong>와 <strong>Blocking/Non-blocking</strong>은 서로 다른 차원의 작업 수행 방식을 설명하는 두 가지 개념이다. 이 개념들은 종종 혼용되거나 비슷한 것으로 오해하는 경우가 있는데, 실제로는 매우 다른 의미를 가지고 있다.</p>
<p><strong>동기와 비동기</strong>는 작업 완료 여부를 신경 쓰는 방식과 관련이 있다. 즉, 어떤 작업이 시작되면 그 작업의 완료 여부를 기다리는지 또는 바로 다른 작업을 처리할 수 있는지에 따라 동기와 비동기로 나뉜다.</p>
<p><strong>블로킹과 논블로킹</strong>은 작업을 요청한 후 해당 작업이 시스템 자원을 차단(blocking)하는지 여부에 대한 관점이다. 즉, 현재 작업이 다른 작업의 진행을 막느냐에 따라 블로킹과 논블로킹으로 나뉜다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/e75fd949-688d-44e4-bd03-a04eaa21fdeb/image.png" alt=""></p>
<p>이처럼 두 개념에 대한 의미 차이는 명확하지만, 프로그래밍에서는 종종 혼용되어 사용되기도 한다. 대표적으로 자바스크립트의 <code>setTimeout()</code> 함수를 일반적으로 비동기 함수라고 부르지만 동시에 논블로킹 함수이기도 하다. 즉, 우리가 편의상 부르는 자바스크립트 비동기 함수는 사실 비동기+논블로킹 함수인 것이다. 따라서 이들을 정확하게 구분하고 이해하는 것이 컴퓨터 아키텍쳐를 이해하는 데 있어 중요하다.</p>
<h1 id="1-synchronousasynchronous">1. Synchronous/Asynchronous</h1>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/5f3fa01d-7f68-4775-b486-71a93038f0cb/image.png" alt=""></p>
<h2 id="11-synchronous">1.1. Synchronous</h2>
<p><strong>동기(Synchronous)</strong>라는 것은 작업을 하나하나 순서대로 처리하는 방식이다. 어떤 작업을 요청했을 때, 그 작업이 끝날 때까지 기다려야 다음 작업을 할 수 있다. 즉, 작업이 순차적으로 처리되는 것이다.</p>
<p>예를 들어, 친구한테 “커피 좀 사다줘”라고 말했을 때, 친구가 돌아올 때까지 기다리고, 그 다음에 다른 일을 할 수 있는 것이다. 완료 여부를 확인하고 나서야 다른 일을 시작하는 방식인 셈이다.</p>
<h2 id="12-asynchronous">1.2. Asynchronous</h2>
<p><strong>비동기(Asynchronous)</strong>는 그 반대다. 어떤 작업을 시키더라도 그 결과를 기다리지 않고 바로 다른 일을 할 수 있다. 기다릴 필요 없이 동시에 여러 작업을 처리하는 것처럼 느껴진다.</p>
<p>친구에게 “커피 좀 사다줘”라고 한 후, 커피가 올 때까지 기다리지 않고, 그동안 다른 일을 하면서 보낼 수 있는 것이다. 친구가 커피를 사오면 그때 커피를 받는다.</p>
<h3 id="121-비동기의-성능-이점">1.2.1. 비동기의 성능 이점</h3>
<p><em>비동기가 왜 성능이 더 좋을까?</em> 느린 작업을 기다리지 않고 다른 일을 바로 할 수 있기 때문이다.</p>
<p>예를 들어, 웹 애플리케이션이 데이터베이스에서 데이터를 가져와야 하는 상황을 가정해 보자. 동기 방식일 경우, 데이터베이스에서 데이터를 다 가져올 때까지 기다려야 다음 요청을 처리할 수 있다. 그러면 서버가 대기 상태에 빠지고, 많은 사용자가 한꺼번에 접속하면 서버는 느려질 것이다.</p>
<p>하지만 비동기 방식일 경우, 데이터를 가져오는 동안에도 다른 작업을 계속 처리할 수 있다. 데이터베이스 응답이 오면 그때 결과를 처리하고, 그동안은 다른 사용자의 요청도 처리할 수 있는 것이다. 때문에 비동기 방식은 대규모 트래픽에서도 효율적이고, 서버 성능을 더 최적화할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3abf7526-8f2c-4d44-8338-14aae8ef57ab/image.png" alt=""></p>
<h3 id="122-자바스크립트의-비동기">1.2.2. 자바스크립트의 비동기</h3>
<p>자바스크립트에서는 비동기 작업을 많이 사용하는데, 예를 들어 <code>setTimeout()</code>라는 함수가 있다. 이는 지정된 시간이 지난 후에 특정 작업을 수행하도록 하는 함수다.</p>
<p>중요한 점은, <strong>시간을 기다리는 동안 다른 코드도 계속 실행된다</strong>는 것이다. 자바스크립트는 싱글 스레드로 작동해서 한 번에 하나의 작업만 할 수 있는 것처럼 보이지만, 브라우저에서는 멀티 스레드로 돌아가는 <strong>Web API</strong>가 있어 비동기 작업을 백드라운드에서 처리하도록 한다.</p>
<p>이런 구조 덕분에 자바스크립트는 한꺼번에 여러 작업을 처리할 수 있는 것처럼 보이고, 서버 성능을 크게 향상시킬 수 있다.</p>
<h2 id="13-동기와-비동기의-작업-순서">1.3. 동기와 비동기의 작업 순서</h2>
<p>동기와 비동기의 가장 큰 차이 중 하나는 작업 순서다.</p>
<ul>
<li><p><strong>동기</strong>에서는 $A→B→C$와 같이 순서대로 작업이 진행된다. 하나가 끝나야 그 다음 작업을 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/dc7208fd-527f-49c2-928d-d950b56cc85e/image.png" alt=""></p>
</li>
<li><p><strong>비동기</strong>에서는 $A→C→B$와 같이 순서가 보장되지 않는다. 각 작업이 독립적으로 처리되기 때문에, 어떤 작업이 먼저 끝날지 알 수 없다. 때문에 결과가 무작위로 나올 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/204c862a-f348-418f-97db-9af608f3e8b2/image.png" alt=""></p>
</li>
</ul>
<h1 id="2-blockingnon-blocking">2. Blocking/Non-blocking</h1>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/11842119-620a-4c89-af74-e2ed55ffbf10/image.png" alt=""></p>
<h2 id="21-blocking">2.1. Blocking</h2>
<p><strong>블로킹</strong>은 말 그대로 작업이 끝날 때까지 기다리게 만드는 방식이다.</p>
<p>예를 들어, 친구에게 “커피 좀 사다줘”라고 했을 때, 친구가 돌아올 때까지 아무것도 못하고 문 앞에서 계속 기다리는 상황이다. 다른 일을 할 수가 없고, 친구가 커피를 사오는 작업이 끝날 때까지 대기해야 한다.</p>
<h2 id="22-non-blocking">2.2. Non-Blocking</h2>
<p>반대로 <strong>논블로킹</strong>은 작업이 끝나지 않았더라도 다른 일을 계속 할 수 있는 방식이다.</p>
<p>커피를 부탁해도 굳이 문 앞에서 기다리지 않고, 집 안에서 다른 일을 하면서 기다리는 것이다. 친구가 커피를 사오면 그때 커피를 받는다. 즉, 다른 작업이 멈추지 않고 동시에 진행되는 것이다.</p>
<blockquote>
<h4 id="비동기asynchronous와-논블로킹non-blocking의-차이"><strong>비동기(Asynchronous)와 논블로킹(Non-Blocking)의 차이</strong></h4>
</blockquote>
<ul>
<li><strong>비동기</strong>는 <strong>작업 완료 여부</strong>와 관련된 개념이다. 작업을 시켰을 때 결과가 바로 오지 않더라도 다른 작업을 먼저 할 수 있는지 여부를 말한다.</li>
<li><strong>논블로킹</strong>은 <strong>현재 작업이 다른 작업을 막는지 여부</strong>를 말한다. 작업이 진행 중이더라도, 다른 작업을 동시에 할 수 있으면 논블로킹이다.</li>
</ul>
<p>둘 다 결과적으로 동시에 여러 작업을 처리할 수 있다는 점에서 비슷해 보이지만, <strong>비동기</strong>는 <strong>‘작업이 끝났냐’</strong>를 신경쓰지 않기 때문에 계속 진행하는 것이고, <strong>논블로킹</strong>은 <strong>‘작업이 진행되더라도 다른 작업을 멈추지 않고 동시에 처리’</strong>하는 것이다.</p>
</aside>

<h1 id="3-synchronousasynchronous--blockingnon-blocking">3. Synchronous/Asynchronous &amp; Blocking/Non-blocking</h1>
<h2 id="31-제어권control과-blockingnon-blocking">3.1. 제어권(Control)과 Blocking/Non-Blocking</h2>
<p>프로그래밍에서 <strong>제어권</strong>은 함수나 코드 흐름을 제어할 수 있는 권리를 말한다. 제어권을 누가 가지고 있느냐에 따라 블로킹과 논블로킹을 구분할 수 있다.</p>
<h3 id="311-blocking">3.1.1. Blocking</h3>
<p>블로킹 방식은 호출된 함수(B 함수)가 제어권을 가지고, 호출한 함수(A 함수)는 멈춰 있어야 하는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/2a4ce2e5-a3a3-4a24-8fda-1a6ef6e64ff2/image.png" alt=""></p>
<ol>
<li>A 함수가 B 함수를 호출하면, B에게 제어권이 넘어간다.</li>
<li>B 함수가 실행되는 동안, A 함수는 잠시 멈춘다.(Block)</li>
<li>B 함수가 끝나면, A 함수에게 제어권을 다시 돌려준다.</li>
<li>A 함수는 그제서야 다시 다음 작업을 진행할 수 있다.</li>
</ol>
<p>쉽게 말하면, A 함수는 B 함수가 끝날 때까지 대기해야 하고, 제어권을 잠시 <strong>B 함수에게 양보</strong>하는 상태라고 볼 수 있다.</p>
<h3 id="312-non-blocking">3.1.2. Non-Blocking</h3>
<p>논블로킹 방식은 호출된 함수(B 함수)가 실행되지만, 제어권은 여전히 A 함수가 가지고 있는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/2b5c1da7-85af-4e23-a10b-5ef49575dfc1/image.png" alt=""></p>
<ol>
<li>A 함수가 B 함수를 호출하면, B 함수가 실행되지만 제어권은 A 함수에게 계속 남아있다.</li>
<li>A 함수는 제어권을 가지고 있기 때문에 B 함수가 끝나지 않더라도 자신의 작업을 계속할 수 있다.</li>
<li>B 함수는 백그라운드에서 실행되고, A 함수는 중단되지 않고 코드를 계속 처리한다.</li>
</ol>
<p>즉, A 함수는 B 함수가 끝나길 기다리지 않고 <strong>다른 작업을 바로 진행</strong>할 수 있다.</p>
<h2 id="32-sync-blocking동기블로킹">3.2. Sync Blocking(동기+블로킹)</h2>
<p>작업을 순서대로 처리하고, 하나의 작업이 끝날 때까지 기다리는 방식이다. 다른 작업이 진행되는 동안 멈추고, 결과가 나올 때까지 대기하는 형태이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/7629d138-7bf5-42ea-8b9e-a83649332f35/image.png" alt=""></p>
<h3 id="321-실생활-예시">3.2.1. 실생활 예시</h3>
<p>팀장이 사원1에게 A 업무를 시키고, 사원1이 A를 끝낼 때까지 기다린 후에 사원2에게 B 업무를 시키는 상황. </p>
<p>사원의 작업이 끝나기 전까지 팀장은 다른 사원에게 아무 일도 시키지 못한다.</p>
<h3 id="322-코드-예시">3.2.2. 코드 예시</h3>
<pre><code class="language-jsx">const data1 = fs.readFileSync(&#39;file1.txt&#39;, &#39;utf8&#39;); // A 작업
console.log(data1);

const data2 = fs.readFileSync(&#39;file2.txt&#39;, &#39;utf8&#39;); // B 작업
console.log(data2);</code></pre>
<p>이 방식은 코드가 순차적으로 실행된다. 작은 작업에서는 유용할 수 있으나, 시간이 오래 걸리는 작업에서는 비효율적이다.</p>
<h3 id="323-적용-프로그램-예시">3.2.3. 적용 프로그램 예시</h3>
<p>대표적으로 C나 Java의 코드 실행 후 커맨드에서 입력을 받는 경우가 이에 해당된다. 사용자로부터 입력을 받아야 그 입력값을 가지고 내부 처리를 하여 결과값을 콘솔에 출력해주기 때문에 순차적인 작업이 요구된다.</p>
<p>내부적으로 본다면 실행 코드가 콘솔창을 띄우고 <code>Please enter your name</code> 텍스트를 치고 난 다음 사용자의 리턴값이 필요하기 때문에 제어권을 시스템에서 사용자로 넘겨 사용자가 값을 입력할 때까지 기다리는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3dbfcb60-030a-41e3-9ee7-0eacfaabce4c/image.png" alt=""></p>
<h2 id="33-async-blocking비동기블로킹">3.3. Async Blocking(비동기+블로킹)</h2>
<p>작업이 시작되면 결과를 기다리지는 않지만, 다른 작업이 완료될 때까지는 막혀있다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/1f8acef4-5b31-44c8-bb88-3a09d203f124/image.png" alt=""></p>
<h3 id="331-async-blocking-vs-sync-blocking">3.3.1. Async Blocking vs Sync Blocking</h3>
<p>3.2와 비교했을 때, Async Blocking은 Sync Blocking 수행과 큰 차이가 없어 보인다. 실제로 개념적으로만 차이가 존재할 뿐이지, 성능적으로 큰 차이가 없을 뿐더러 실무에서 마주할 일이 없다.</p>
<p>보통 Async Blocking은 개발자가 비동기 논블로킹으로 처리하려다가 실수하는 경우에 발생하거나, 자기도 모르게 블로킹 작업을 실행하는 의도치 않은 경우에 사용된다. 그래서 이 방식을 안티 패턴(anti-pattern)이라고 치부하기도 한다.</p>
<h3 id="332-활용-예시-프로그램">3.3.2. 활용 예시 프로그램</h3>
<p>다만 Async Blocking이 실제로 적용된 실무 사례가 있 다. Node.js + MySQL의 조합이 대표적인데, Node.js에서 비동기 방식으로 데이터베이스에 접근하기 때문에 Async지만, MySQL 데이터베이스에 접근하기 위한 MySQL 드라이버가 블로킹 방식으로 작동되기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/84902f39-73d2-411c-be63-014ca4a372cf/image.png" alt=""></p>
<ol>
<li>JavaScript는 비동기 방식으로 MySQL에 쿼리를 보낸다. (Async)</li>
<li>MySQL은 쿼리를 처리하면서 JavaScript에게 제어권을 넘겨주지 않는다. (Blocking)</li>
<li>JavaScript는 다른 작업을 계속 수행할 수는 있지만, MySQL의 결과값을 필요로 하기 때문에 MySQL이 쿼리를 완료할 때까지 기다린다.</li>
<li>결국 Sync Blocking과 차이가 없게 된다.</li>
</ol>
<h2 id="34-sync-non-blocking동기논블로킹">3.4. Sync Non-Blocking(동기+논블로킹)</h2>
<p>결과가 나오기 전에 다른 작업을 할 수 있지만, 작업 순서는 보장돼야 한다. 즉, 순서대로 작업을 해야 하지만, 각 작업이 진행되는 동안 다른 작업을 방해하지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/41a3c80e-3cdb-461c-8396-569534ac8897/image.png" alt=""></p>
<h3 id="341-실생활-예시">3.4.1. 실생활 예시</h3>
<p>팀장에게 A, B, C 세 개의 업무가 있는데, A 작업을 끝내야 B 작업을, B 작업을 끝내야 C 작업을 시작할 수 있다. 팀장은 각각의 작업을 시작한 후, 결과가 나올 때까지 동시에 다른 부서와 회의를 하거나 이메일을 처리할 수 있다. </p>
<h3 id="342-코드-동작-예시">3.4.2. 코드 동작 예시</h3>
<pre><code class="language-java">Thread thread = new Thread(new MyTask());
thread.start(); // 논블로킹 실행

System.out.println(&quot;Main thread is running...&quot;);

while (thread.isAlive()) {
    System.out.println(&quot;Waiting for the thread to finish...&quot;);
}

// A가 끝나야 B를 실행
System.out.println(&quot;Thread finished!&quot;);

System.out.println(&quot;Run the next tasks&quot;);</code></pre>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/8bd130cc-293c-41df-9088-23fed1f4c4a8/image.png" alt=""></p>
<p>스레드를 이용하여 작업을 병렬적으로 처리하도록 지시했지만, <code>while</code> 문을 수행함으로써 요청한 작업의 완료 여부를 계속 확인하고, 결과적으로 순서대로 작업이 수행되는 것을 확인할 수 있다.</p>
<p>자바스크립트의 경우 동기+논블로킹 코드를 구현하기에는 지원하는 메서드에 한계가 있어 완벽히 표현할 수는 없다. 다만 이와 비슷하게 구현하려면 <code>Promise</code>와 <code>async/await</code>를 사용하면 된다.</p>
<pre><code class="language-jsx">const fs = require(&#39;fs&#39;); 
const { promisify } = require(&#39;util&#39;); 
const readFileAsync = promisify(fs.readFile); // fs.readFile 함수를 Promise 객체를 반환하는 함수로 변환

async function readFiles() {
  try {
    // Promise.all() 메소드를 사용하여 여러 개의 비동기 작업을 병렬로 처리한다. (비동기 논블로킹)
    const [data1, data2, data3] = await Promise.all([
      readFileAsync(&#39;file.txt&#39;, &#39;utf8&#39;), 
      readFileAsync(&#39;file2.txt&#39;, &#39;utf8&#39;), 
      readFileAsync(&#39;file3.txt&#39;, &#39;utf8&#39;) 
    ]);

    // 파일 읽기가 완료되면 data에 파일 내용이 들어온다.
    console.log(data1); 
    console.log(data2); 
    console.log(data3); 

    // 파일 비교 로직 실행...

  } catch (err) {
    throw err;
  }
}

readFiles(); // async 함수를 호출</code></pre>
<ul>
<li><strong>비동기 논블로킹</strong>: <code>Promise.all()</code>을 사용하여 여러 파일을 동시에 읽는 작업을 비동기적으로 실행할 수 있다. 이렇게 하면 작업이 병렬로 진행되어 논블로킹 방식이 된다.</li>
<li><strong>동기적 순서 유지</strong>: <code>await</code>를 사용하여 파일 읽기 작업이 모두 완료된 후 그 결과를 순차적으로 처리할 수 있다. 즉, 파일을 동시에 읽되, 결과를 동기적으로 처리할 수 있는 방법이다.</li>
</ul>
<blockquote>
</blockquote>
<p><code>async/await</code>은 <strong>비동기 작업을 동기적으로 표현</strong>해주는 기법처럼 보이지만, 그 이면에서는 <strong>비동기 논블로킹</strong>으로 작동한다는 점을 기억해야 한다. 자바스크립트의 <strong>메인 콜 스택</strong>이 모두 비워진 후에 <code>async</code> 함수가 실행되기 때문에, 여전히 비동기적으로 작동하면서 다른 작업을 막지 않는다는 것이다.</p>
<h3 id="343-활용-예시-프로그램">3.4.3. 활용 예시 프로그램</h3>
<p>게임에서 맵을 이동할 때를 생각해보자. 맵 다운로드는 시간이 걸리는 작업이다. 이 작업은 비동기적으로 백그라운드에서 처리된다. 즉, 게임은 맵 다운로드가 끝날 때까지 기다리지 않고 다른 작업을 계속할 수 있다. 여기서 다른 작업이란 맵 다운로드가 진행되는 동안 게임이 로딩 바를 업데이트하거나 UI를 계속해서 표시하는 것을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/4e8bc55b-2df7-4f33-aa02-beda9fb203b0/image.png" alt=""></p>
<blockquote>
<h4 id="⏱️-sync-blocking-vs-sync-non-blocking">⏱️ <strong>Sync Blocking vs Sync Non-Blocking</strong></h4>
</blockquote>
<p>블로킹이든 논블로킹이든, 메인 함수에서 결국 코드를 동기적으로 실행하면 이 둘은 차이가 없어 보인다. 그럼 두 방식은 도긴개긴인 것인가?</p>
<blockquote>
</blockquote>
<p>성능 차이는 상황에 따라 다르겠으나, 일반적으로 <strong>동기+논블로킹</strong>이 동기+블로킹보다 효율적일 수 있다. 왜냐하면 동기+논블로킹은 호출하는 함수가 제어권을 가지고 있어 다른 작업을 병렬적으로 수행할 수 있기 때문이다. 반면, 동기+블로킹은 호출하는 함수가 제어권을 잃어서 다른 작업을 수행할 수 없다.</p>
<h2 id="35-async-non-blocking비동기논블로킹">3.5. Async Non-Blocking(비동기+논블로킹)</h2>
<p>작업이 끝나기 전에 다른 작업을 처리할 수 있고, 작업 순서에 구애받지 않는다. Node.js같은 환경에서 많이 사용되는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/74ce9340-6e14-42c1-aab2-d86834937bcb/image.png" alt=""></p>
<h3 id="351-실생활-예시">3.5.1. 실생활 예시</h3>
<p>팀장이 사원1, 사원2, 사원3에게 동시에 업무를 시키고, 결과가 언제 나오든 나오는 대로 처리하는 방식이다. A, B, C라는 작업을 동시에 시킬 수 있고, 완료되는 순서와 상관없이 결과를 처리할 수 있다.</p>
<h3 id="352-코드-예시">3.5.2. 코드 예시</h3>
<pre><code class="language-jsx">fs.readFile(&#39;file.txt&#39;, &#39;utf8&#39;, (err, data) =&gt; {
  console.log(data);
});

fs.readFile(&#39;file2.txt&#39;, &#39;utf8&#39;, (err, data) =&gt; {
  console.log(data);
});

console.log(&#39;done&#39;); // 이게 먼저 출력됨</code></pre>
<p>여기서는 파일 읽기가 비동기적으로 처리되고, 순서에 상관없이 논블로킹으로 작업이 진행된다. 비동기 논블로킹 방식은 작업량이 많을 때, 다른 작업을 동시에 처리할 수 있어 성능을 최적화할 수 있다.</p>
<blockquote>
<p><code>fs.readFileSync</code>와 <code>fs.readFile</code> 함수는 둘 다 똑같이 파일을 읽는 함수이지만, 동기냐 비동기냐에 차이가 있다. 즉, <code>fs.readFileSync</code> 함수는 동기적으로 파일을 읽고, <code>fs.readFile</code> 함수는 비동기적으로 파일을 읽는다.</p>
</blockquote>
<h3 id="353-활용-예시-프로그램">3.5.3. 활용 예시 프로그램</h3>
<p>비동기 논블로킹을 활용하는 프로그램은 많다. 그중 예를 들자면, 웹 브라우저의 파일 다운로드가 비동기 논블로킹을 활용하는 예시라고 할 수 있다. 웹 브라우저는 웹 사이트에서 파일을 다운로드할 때, 파일의 전송이 완료될 때까지 다른 작업을 하지 않고 기다리는 것이 아니라, 다른 탭이나 창을 열거나 웹 서핑을 할 수 있다. 이는 웹 브라우저가 파일 다운로드를 비동기적으로 처리하고, 콜백 함수를 통해 다운로드가 완료되면 알려주는 방식으로 구현되어 있기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/092f7f44-f735-46bc-a556-e95fbe0298fc/image.png" alt=""></p>
<p>파일들이 거의 동시에 다운 받아지는 것을 Waterfall 그래프에서 시작적으로 확인할 수 있다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
<ul>
<li><a href="https://inpa.tistory.com/entry/%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB-%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%B8%94%EB%A1%9C%ED%82%B9%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC">완벽히 이해하는 동기/비동기 &amp; 블로킹/논블로킹</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] 로드밸런싱(Load Balancing)]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1Load-Balancing</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1Load-Balancing</guid>
            <pubDate>Tue, 07 Jan 2025 00:28:18 GMT</pubDate>
            <description><![CDATA[<h1 id="1-개요">1. 개요</h1>
<p><strong>로드 밸런싱</strong>이란 여러 대의 컴퓨터 자원(CPU, 저장장치 등)에 작업을 고르게 분배하여 트래픽을 효율적으로 관리하는 기술이다. 현대의 웹사이트들은 많은 사람들이 동시에 접속하는 경우가 많아, 단일 서버로 모든 트래픽을 처리하기엔 한계가 있다. 이를 해결하기 위한 두 가지 방법이 있다.</p>
<ul>
<li><strong>Scale-up</strong>: 하드웨어 성능을 향상시키는 방법</li>
<li><strong>Scale-out</strong>: 여러 대의 서버가 트래픽을 나눠서 처리하도록 구성하는 방법</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/45c260a7-336b-4094-ab63-dc331514a185/image.png" alt=""></p>
<p>Scale-out은 무중단 서비스 환경을 구성하기 용이하며, 여러 서버에 트래픽을 균등하게 분배하는 것이 바로 로드 밸런싱이다.</p>
<h1 id="2-원리">2. 원리</h1>
<p>로드 밸런싱은 <strong>여러 서버에 부하(Load)를 고르게 나누는 분산식 웹 서비스 구조</strong>이다. <strong>로드 밸런서</strong>는 클라이언트와 서버 사이에 위치하여 트래픽을 분산시켜 서버의 과부하를 방지한다. 서비스 규모에 따라 서버를 증설할 수 있으며, 로드 밸런서를 통해 이러한 서버들을 관리하여 성능을 최적화할 수 있다.</p>
<h1 id="3-로드-밸런서의-서버-선택-방식">3. 로드 밸런서의 서버 선택 방식</h1>
<p>로드 밸런서의 서버 선택 방식에는 다양한 알고리즘이 사용되며, 각 방식은 트래픽의 특성이나 서버의 상태에 따라 효율성을 달리할 수 있다. 대표적인 서버 선택 방식은 아래와 같다.</p>
<h2 id="31-라운드-로빈round-robin">3.1. 라운드 로빈(Round Robin)</h2>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/1091a53b-a9ea-4468-9ace-fa5ea5465cf1/image.png" alt=""></p>
<p><strong>순차적으로 서버를 선택하여 트래픽을 분산하는 방식</strong>이다. 서버 목록에서 첫 번째 서버부터 마지막 서버까지 차례대로 요청을 배분하고, 다시 처음으로 돌아가 반복된다. 이 방식은 각 서버의 처리 능력이나 현재 부하를 고려하지 않고 균등하게 분배하는 것이 특징이다.</p>
<ul>
<li>장점: 단순하고 구현이 쉽다. 서버의 부하 상태를 고려하지 않아도 고르게 분산된다.</li>
<li>단점: 각 서버의 성능이나 현재 연결된 세션 수를 고려하지 않기 때문에, 성능이 다르거나 세션이 긴 요청이 있는 환경에서는 비효율적일 수 있다.</li>
</ul>
<h2 id="32-least-connections">3.2. Least Connections</h2>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/fb3f0fe4-2708-401b-b749-c39e9c944bb8/image.png" alt=""></p>
<p><strong>현재 연결된 세션 수가 가장 적은 서버에 요청을 분배하는 방식</strong>이다. 이 방식은 각 서버의 부하 상태를 실시간으로 고려하며, 세션이 길어지는 트래픽이나 비대칭적인 요청 분포가 발생하는 환경에서 유용하다.</p>
<ul>
<li>장점: 서버 간의 부하를 실시간으로 반영하여 상대적으로 가벼운 서버에 우선적으로 요청을 분배함으로써 효율성을 높인다.</li>
<li>단점: 각 서버의 연결 상태를 지속적으로 모니터링해야 하므로 추가적인 오버헤드가 발생할 수 있다. 또한, 세션 수만을 기준으로 삼기 때문에 요청당 리소스가 큰 경우(e.g. 대형 파일 전송)에는 서버 부하를 완전히 반영하지 못할 수 있다.</li>
</ul>
<h2 id="33-sourceip-해싱">3.3. Source(IP 해싱)</h2>
<p><strong>클라이언트의 IP 주소를 해싱하여 특정 서버에 트래픽을 분배하는 방식</strong>이다. 해시 알고리즘을 사용해 사용자의 IP를 기반으로 항상 같은 서버에 연결되도록 보장한다. 이 방식은 사용자 세션 유지가 중요한 환경에서 주로 사용된다.</p>
<ul>
<li>장점: 특정 사용자가 동일한 서버에 항상 연결되므로, 세션 유지가 중요한 경우(e.g. 사용자 로그인 상태, 지속적인 데이터를 처리하는 웹 애플리케이션 등) 유리하다. 세션 유지에 별도의 처리를 할 필요가 없다.</li>
<li>단점: 특정 서버에 트래픽이 몰릴 수 있는 위험이 있다. IP 분포가 불균형할 경우 특정 서버에만 많은 부하가 걸릴 수 있으며, 로드 밸런싱의 효과가 떨어질 수 있다.</li>
</ul>
<h1 id="4-로드-밸런서의-종류">4. 로드 밸런서의 종류</h1>
<p>주로 L4와 L7 로드 밸런서가 많이 사용된다. OSI 7 계층 모델에서 L4는 전송 계층, L7은 애플리케이션 계층을 의미한다. 이 계층에 따라 로드 밸런싱의 정교함이 달라지며, L4부터는 포트 정보를 바탕으로 트래픽 분산이 가능하다.</p>
<h2 id="41-l4-로드-밸런서layer-4-load-balancer">4.1. L4 로드 밸런서(Layer 4 Load Balancer)</h2>
<p>L4 로드 밸런서는 <strong>네트워크 계층에서 작동</strong>한다. 주로 TCP나 UDP와 같은 전송 계층 프로토콜을 사용해서 클라이언트의 요청을 여러 서버로 분산한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/5d453c3a-1c6e-4e33-be11-88c3fcce6c64/image.png" alt=""></p>
<h3 id="411-특징">4.1.1. 특징</h3>
<ul>
<li><strong>작동 방식</strong>: 패킷의 소스 IP 주소, 소스 포트, 목적지 IP 주소, 목적지 포트를 기준으로 트래픽을 분산한다. 이 정보는 TCP/UDP 헤더에 포함되어 있으며, 이러한 데이터를 기준으로 요청을 처리한다.</li>
<li><strong>트래픽 분산</strong>: 트래픽을 직접 분석하지 않고 단순히 네트워크 연결을 기반으로 분배하기 때문에, 세션을 유지하거나 특정 요청의 내용에 따라 트래픽을 분산하는 데에 한계가 있다. 즉, <strong>요청의 데이터(e.g. HTTP 헤더나 콘텐츠)를 분석하지 않고 연결을 처리</strong>하는 것이 특징이다.</li>
<li><strong>성능</strong>: 트래픽 분산을 매우 효율적으로 처리할 수 있다. 애플리케이션 계층에서 일어나는 복잡한 처리 없이 패킷 레벨에서 트래픽을 전달하기 때문에, <strong>대규모 트래픽 처리에 적합</strong>하다.</li>
</ul>
<h3 id="412-장점">4.1.2. 장점</h3>
<ul>
<li><strong>빠르고 간단하다.</strong><ul>
<li>애플리케이션 계층까지 도달하지 않기 때문에, 데이터 패킷을 빠르게 분산할 수 있다.</li>
<li>고성능이 요구되는 대규모 네트워크 환경에서 유리하다.</li>
</ul>
</li>
<li><strong>프로토콜에 무관하다.</strong><ul>
<li>HTTP뿐만 아니라 TCP나 UDP를 사용하는 다양한 서비스에 사용할 수 있다.</li>
<li>e.g. 데이터베이스나 메일 서버 등 비HTTP 서비스에서 활용 가능</li>
</ul>
</li>
</ul>
<h3 id="413-단점">4.1.3. 단점</h3>
<ul>
<li><strong>세밀한 제어가 부족하다.</strong><ul>
<li>트래픽을 세부적으로 분석하지 않기 때문에 요청의 종류나 사용자의 상태에 따라 적절히 분배하지 못할 수 있다.</li>
<li>e.g. 특정 URL을 처리하는 서버가 필요한 경우 적합하지 않다.</li>
</ul>
</li>
<li><strong>세션 유지가 어렵다.</strong><ul>
<li>사용자의 세션이나 상태를 기반으로 분산하는 기능이 제한적이다.</li>
</ul>
</li>
</ul>
<h2 id="42-l7-로드-밸런서layer-7-load-balancer">4.2. L7 로드 밸런서(Layer 7 Load Balancer)</h2>
<p>L7 로드 밸런서는 애플리케이션 계층에서 작동한다. HTTP, HTTPS와 같은 애플리케이션 레벨의 프로토콜을 분석하고, 클라이언트의 요청을 보다 정교하게 처리한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/0943f8d8-be04-4e23-acb7-cb9ad79b0685/image.png" alt=""></p>
<h3 id="421-특징">4.2.1. 특징</h3>
<ul>
<li><strong>작동 방식</strong>: <strong>요청의 내용(e.g. URL, HTTP 헤더, 쿠키, 메소드 등)을 분석하여 트래픽을 분산</strong>한다. 사용자가 요청하는 리소스에 따라 서버를 다르게 지정하거나, 특정 콘텐츠를 처리할 수 있는 서버로 트래픽을 유도할 수 있다.</li>
<li><strong>트래픽 분산</strong>: HTTP 요청을 이해하고 처리할 수 있으므로 URL 기반, 쿠키 기반, 세션 기반 등 다양한 방식으로 트래픽을 분배할 수 있다. 예를 들어, 특정 URL 패턴에 대한 요청을 특정 서버로 전달하거나, 사용자 데이터를 분석하여 적절한 서버에 할당하는 방식으로 <strong>매우 세밀한 로드 밸런싱이 가능</strong>하다.</li>
<li><strong>성능</strong>: 트래픽을 분석하고 처리하는 데 더 많은 리소스를 사용하기 때문에 L4에 비해 성능이 약간 떨어질 수 있다. 하지만 고급 트래픽 제어가 가능하다는 점에서 웹 애플리케이션 환경에서 매우 유용하다.</li>
</ul>
<h3 id="422-장점">4.2.2. 장점</h3>
<ul>
<li><strong>세밀한 제어가 가능하다.</strong><ul>
<li>요청의 내용을 분석하여 더 정교한 트래픽 분배가 가능하다.</li>
<li>e.g. 특정 URL로 들어오는 요청만 특정 서버에 전달, 사용자 IP나 쿠키 정보를 바탕으로 분산 조정 가능</li>
</ul>
</li>
<li><strong>세션 유지가 용이하다.</strong><ul>
<li>특정 사용자가 항상 같은 서버에 연결되도록 보장하는 등, 사용자 상태 기반의 트래픽 관리가 가능하다.</li>
</ul>
</li>
<li><strong>SSL 종료</strong><ul>
<li>HTTPS 요청에 대해 SSL/TLS 인증을 처리하고, 암호화를 해제한 후 내부 서버로 전달할 수 있다.</li>
<li>이를 통해 서버는 암호화 부담 없이 요청을 처리할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="423-단점">4.2.3. 단점</h3>
<ul>
<li><strong>리소스 소모</strong><ul>
<li>애플리케이션 계층에서 요청을 분석하기 때문에 L4보다 더 많은 리소스를 소모하기 때문에, 대규모 트래픽 처리 시 성능 저하가 발생할 수 있다.</li>
</ul>
</li>
<li><strong>프로토콜 한정</strong><ul>
<li>주로 HTTP/HTTPS 기반의 트래픽에 활용되므로, TCP와 UDP같은 비HTTP 서비스에는 적합하지 않다.</li>
</ul>
</li>
</ul>
<h1 id="5-로드-밸런서의-장애-대비">5. 로드 밸런서의 장애 대비</h1>
<p>로드 밸런서가 여러 서버로 트래픽을 분산시키는 중요한 역할을 하지만, <strong><em>그 로드 밸런서 자체가 장애를 겪으면 어떻게 될까?</em></strong></p>
<p>이러한 문제를 예방하고 서비스의 무중단 가동을 보장하기 위해 <strong>로드 밸런서의 이중화</strong>가 필요하다.</p>
<h2 id="51-로드-밸런서의-이중화">5.1. 로드 밸런서의 이중화</h2>
<p>로드 밸런서 이중화는 동일한 네트워크 내에서 <strong>두 개 이상의 로드 밸런서가 함께 동작</strong> 하여, <strong>한쪽이 장애를 겪을 때 다른 쪽이 자동으로 그 역할을 대신하는 구조</strong>를 의미한다. 이중화된 로드 밸런서들은 서로의 상태를 지속적으로 모니터링하고, 문제가 발생하면 즉시 트래픽을 다른 로드 밸런서로 전환하여 서비스의 중단을 방지한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/2be5ebca-294b-43b7-a543-8b375fcde47f/image.png" alt=""></p>
<ul>
<li><strong>Active-Passive 방식</strong><ul>
<li><strong>Active 로드 밸런서</strong>: 평상시 모든 트래픽을 처리하는 <strong>주 로드 밸런서</strong></li>
<li><strong>Passive 로드 밸런서</strong>: 대기 상태에서 <strong>Active 로드 밸런서를 모니터링</strong>한다. Active 로드 밸런서가 장애를 겪으면, Passive 로드 밸런서가 즉시 활성화되어 트래픽을 처리한다.</li>
</ul>
</li>
<li><strong>Active-Active 방식</strong><ul>
<li>두 개 이상의 로드 밸런서가 <strong>동시에 활성된 상태에서 트래픽을 분산 처리</strong>한다. 각각의 로드 밸런서가 일부 트래픽을 처리하며, 한쪽이 장애를 겪으면 나머지가 자동으로 모든 트래픽을 처리한다.</li>
</ul>
</li>
</ul>
<h2 id="52-health-check">5.2. Health Check</h2>
<p>이중화된 로드 밸런서들이 서로의 상태를 <strong>실시간으로 모니터링</strong>하기 위해 사용하는 것이 <strong>Health Check</strong>이다. Health Check는 로드 밸런서가 주기적으로 서로의 상태를 점검하는 기능으로, 정상적인 통신이 이루어지고 있는지 확인한다.</p>
<blockquote>
<h4 id="📌-health-check의-동작-방식">📌 <strong>Health Check의 동작 방식</strong></h4>
</blockquote>
<ol>
<li>로드 밸런서들은 정기적인 요청을 통해 서로가 정상적으로 작동하고 있는지 확인한다.</li>
<li>만약 응답이 없거나, 이상이 감지되면 장애가 발생한 것으로 간주한다.</li>
<li>이를 통해 트래픽 처리를 맡고 있는 로드 밸런서에 문제가 발생하면, 즉시 다른 로드 밸런서가 그 역할을 대신하게 된다.</li>
</ol>
<h2 id="53-가상-ipvip와-트래픽-전환">5.3. 가상 IP(VIP)와 트래픽 전환</h2>
<p>VIP(Virtual IP)는 로드 밸런서의 장애 발생 시 트래픽을 자동으로 다른 로드 밸런서로 전환하는 중요한 역할을 한다. VIP는 실제 물리적 IP가 아니라, 클라이언트가 접속하는 <strong>논리적인 IP 주소</strong>이다. 즉, 물리적으로 특정 서버에 고정되어 있는 것이 아니라, <strong>여러 장비나 서버 사이에서 동적으로 할당</strong>될 수 있는 IP 주소다.</p>
<ul>
<li>주 로드 밸런서가 장애를 겪으면, VIP는 대기 중인 여분의 로드 밸런서로 전환된다.</li>
<li>클라이언트는 VIP에 접속하여 서비스에 접근하므로, 클라이언트는 장애 상황을 인식하지 못한 채 서비스를 지속해서 사용할 수 있다.</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://github.com/Songwonseok/CS-Study/blob/main/Network/%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1.md">CS-Study/Network/로드밸런싱.md</a></li>
<li><a href="https://www.geeksforgeeks.org/network-load-balancing-round-robin-vs-least-connections/">Network Load Balancing: Round Robin vs Least Connections</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] TLS/SSL HandShake]]></title>
            <link>https://velog.io/@jayy_19/TLSSSL-HandShake</link>
            <guid>https://velog.io/@jayy_19/TLSSSL-HandShake</guid>
            <pubDate>Mon, 06 Jan 2025 06:18:42 GMT</pubDate>
            <description><![CDATA[<p>고대 그리스에서는 타인에게 노출되지 않아야 하는 중요한 정보를 보낼 때, 전달하는 사람(사자)의 머리를 밀고 중요한 정보를 적은 뒤 머리가 자라 글이 보이지 않으면 상대방에게 사자를 보냈다고 한다. 그렇게 되면 해당 사실을 모르는 사람은 정보를 알 수 없고, 사실을 아는 수신자는 다시 머리를 깎은 후 정보를 볼 수 있을 것이다. 이 암호 기법을 <strong>스테가노그래피</strong>라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/4df194d7-52c7-483d-9be4-befcd05dcf86/image.png" alt=""></p>
<p>정보를 전달하기 위해서는 머리가 자라는 동안 기다려야 한다는 단점이 있지만, 중요한 정보를 확실히 감추는 데에는 이만한 방법이 없다. 옛날부터 중요한 정보를 안전하게 보내는 것은 매우 중요했다는 것을 알 수 있다.</p>
<p>HTTPS 통신 과정에서도 송신자와 수신자는 주고받을 데이터를 안전하게 보내는 것을 중요시 한다. 이때문에 데이터를 암호화할 대칭키(비밀키)를 타인에게 노출시키지 않고 클라이언트가 서버에게 전송하기 위해 협상을 벌인다. </p>
<p>오늘 다룰 <strong>TLS &amp; SSL Handshake</strong>는 송신자와 수신자가 암호화된 데이터를 교환하기 위해 벌이는 일련의 협상이다. 협상 과정에서는 TLS 인증서 전달, 대칭키(비밀키) 전달, 암호화 알고리즘 결정, TLS/SSL 프로토콜 결정 등이 포함된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/9b13f92f-e71a-45cc-b0a7-b047b5f47b07/image.png" alt=""></p>
<p>위의 파란색 칸은 TCP layer의 3-Way Handshake로 HTTPS가 TCP 기반의 프로토콜이기 때문에 암호화 협상(TLS Handshake)에 앞서 연결을 생성하기 위해 실시하는 과정이고, 아래 노란색 상자의 패킷들이 TLS Handshake이다.</p>
<blockquote>
<p>아래 두 가지를 기억하면서 글을 살펴보자.</p>
<ol>
<li><strong>암호화 알고리즘(<a href="https://aws-hyoh.tistory.com/47">Cipher Suite</a>) 결정</strong></li>
<li><strong>데이터를 암호화할 대칭키(비밀키) 전달</strong></li>
</ol>
</blockquote>
<h1 id="1-client-hello">1. Client Hello</h1>
<p>클라이언트가 서버에 연결을 시도하며 전송하는 패킷이다. 자신이 사용 가능한 Cipher Suite 목록, Session ID, SSL Protocol Version, Random byte 등을 전달한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/d7d413ad-fdcb-41e1-a711-d30c33582cd4/image.png" alt=""></p>
<p>🔼 Client Hello Packet</p>
<p><strong>Cipher Suite</strong>는 TLS 통신에서 보안 연결을 설정하는 데 사용되는 알고리즘의 집합이다. 이는 TLS의 프로토콜 버전, 인증서 검증 방식, 데이터 암호화 프로토콜, 해시 알고리즘 등의 정보를 포함한다. 클라이언트는 서버에 자신이 지원하는 Cipher Suite 목록을 제공하고, 서버는 그 중 하나를 선택해 통신에 사용할 암호화 알고리즘을 결정한다. 이렇게 선택된 Cipher Suite에 따라 이후의 데이터가 암호화된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/e4f60b26-a943-4d62-8a9e-681c8f139eeb/image.png" alt=""></p>
<p>🔼 Cipher Suite의 구성</p>
<h1 id="2-server-hello">2. Server Hello</h1>
<p>서버는 클라이언트가 보낸 <code>Client Hello</code> 패킷에서 제공된 여러 Cipher Suite 중 하나를 선택한다. 서버는 선택한 Cipher Suite와 함께 자신이 사용할 TLS 프로토콜 버전 등의 정보를 포함한 <code>Server Hello</code> 패킷을 클라이언트에 보낸다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/805ea774-fc90-4b03-aa19-7b4fd295dfd1/image.png" alt=""></p>
<p>🔼 Server Hello Packet</p>
<p>위 사진을 보면 Client Hello에서 17개였던 Cipher Suite와 달리, 아래에서는 서버가 선택한 한 줄(Cipher Suite: TLS_AES_256_GCM_SHA384)만이 존재하는 것을 알 수 있다. 클라이언트가 보낸 리스트에 있는 Cipher Suite 중 한 개를 선택한 것이다.</p>
<p>서버는 클라이언트가 보낸 Cipher Suite 리스트 중 자신이 지원하는 가장 높은 등급의 Suite를 선택하여 안전한 통신을 시작한다.</p>
<h1 id="3-certificate">3. Certificate</h1>
<p>서버가 자신의 TLS 인증서를 클라이언트에게 전달한다. 나의 경우 TLS1.3이기 때문에 key 교환 후 Application Data Protocol을 통해 <code>Certificate</code>이 암호화되어 전송되기 때문에 보이지 않는다. TLS1.2를 사용한 모 블로그의 사진으로 대체하겠다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/92230ae0-874f-4d0f-8dc7-932a6c105b7f/image.png" alt=""></p>
<p>🔼 Certificate Packet</p>
<p>이 인증서에는 서버가 발행한 공개키가 포함되어 있으며, 서버는 개인키를 소유한다. 클라이언트는 CA의 공개키를 이용해 인증서를 검증하고, 이 인증서가 유효한지 확인한다. </p>
<p>위 사진을 보면 인증서가 어떤 알고리즘(RSA)으로 암호화되었고, 어떤 Hash 알고리즘(SHA256)으로 서명되었는지 확인 가능하다.</p>
<h1 id="4-server-key-exchange--server-hello-done">4. Server Key Exchange / Server Hello Done</h1>
<p><code>Server Key Exchange</code>는 서버의 공개키가 TLS 인증서에 포함되지 않았을 때 서버가 직접 공개키를 전달하는 과정이다. 만약 인증서에 공개키가 포함되어 있다면, 클라이언트는 CA의 공개키로 인증서를 복호화해 서버의 공개키를 얻으므로 이 단계가 생략된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/23bfd178-e3f1-4899-a9a2-a98590d1a210/image.png" alt=""></p>
<p>이 경우 키 교환 알고리즘이 Diffie-Hellman이 사용되어, 서버는 키 교환을 위한 추가 정보를 제공해야 하므로 <code>Server Key Exchange</code>가 발생한다. 이 과정을 통해 최종 세션키가 생성된다.</p>
<h1 id="5-client-key-exchange">5. Client Key Exchange</h1>
<p>클라이언트는 대칭키를 생성한 후, 서버 인증서에서 추출한 서버의 공개키로 해당 대칭키를 암호화하여 서버로 전달한다. 이 대칭키는 이후 실제 데이터 암호화에 사용되며, TLS Handshake의 가장 중요한 목적 중 하나이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/8299a826-f7b9-4d82-9094-78274b507e33/image.png" alt=""></p>
<p>위의 사진처럼 Diffie-Hellman과 ECDHE 알고리즘을 사용하면 클라이언트는 대칭키를 직접 보내는 대신, 대칭키를 만들기 위한 재료(키 생성 파라미터)를 서버와 교환한다. 이를 통해 서버와 클라이언트는 각각 교환한 재료를 조합해 같은 대칭키를 생성하게 된다.</p>
<p>반면 RSA 알고이름을 사용할 경우, 클라이언트가 이미 생성한 대칭키를 서버의 공개키로 암호화하여 서버에 전달한다. 최종적으로 이 대칭키를 이용해 실제 데이터가 암호화된다.</p>
<h1 id="6-change-cipher-spec--finished">6. Change Cipher Spec / Finished</h1>
<p><code>Change Cipher Spec</code>은 클라이언트와 서버가 암호화를 시작하겠다는 신호로, 이 메세지를 주고받은 후 모든 통신이 암호화된다. 즉, 양측이 합의한 암호화 방식을 적용하는 단계이다.</p>
<p><code>Finished</code>는 Handshake가 끝나고, 성공적으로 암호화 키 교환과 인증이 완료되었음을 확인하는 메세지이다. 이 메세지에는 Handshake 과정 전반의 무결성을 확인하는 해시 값이 포함된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/c4399134-b56e-4384-a45e-7e6b7a246679/image.png" alt=""></p>
<blockquote>
<h4 id="📌-tls-handshake-요약">📌 <strong>TLS Handshake 요약</strong></h4>
</blockquote>
<p>Client Hello(암호화 알고리즘 나열 및 전달) → Server Hello(암호화 알고리즘 선택) → Server Certificate(인증서 전달) → Client Key Exchange(데이터를 암호화할 대칭키 전달) → Client / Server Hello done(정보 전달 완료) → Finished(TLS Handshake 종료)</p>
<p>위 과정을 통해 클라이언트와 서버는 데이터를 암호화할 동일한 대칭키를 갖게 되며, 서로에게 각자 갖고 있는 동일한 대칭키를 이용하여 데이터를 암호화해 전송하거나 복호화한다. 클라이언트와 서버는 이제 성공적으로 비밀 친구가 된 것이다.</p>
<hr>
<blockquote>
<h4 id="reference"><strong>Reference</strong></h4>
</blockquote>
<ul>
<li><a href="https://aws-hyoh.tistory.com/entry/HTTPS-%ED%86%B5%EC%8B%A0%EA%B3%BC%EC%A0%95-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-3SSL-Handshake">HTTPS 통신과정 쉽게 이해하기 #3(SSL Handshake, 협상)</a></li>
<li><a href="https://www.cloudflare.com/ko-kr/learning/ssl/what-happens-in-a-tls-handshake/">TLS 핸드셰이크의 원리는 무엇일까요?  | SSL 핸드셰이크</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] HTTP & HTTPS]]></title>
            <link>https://velog.io/@jayy_19/HTTP-HTTPS</link>
            <guid>https://velog.io/@jayy_19/HTTP-HTTPS</guid>
            <pubDate>Tue, 19 Nov 2024 01:56:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2014년, 구글에서는 HTTP를 HTTPS로 바꾸라고 권고한다. 이전까지는 전자상거래가 있는 웹 사이트에서만 다소 번거로운 HTTPS를 사용하고 있었다. HTTPS로의 전환을 장려하기 위해 구글에서는 HTTPS를 사용하는 웹 사이트에 대해 검색 순위 결과에 가산점을 주겠다고 발표했다. 이는 사실상 HTTP를 사용하는 웹 사이트에 벌점을 주는 것과 마찬가지였다.</p>
</blockquote>
<p><em>구글은 어떤 점 때문에 HTTPS로의 전환을 장려했을까? 각각에 대해 알아보자.</em></p>
<h1 id="1-http란">1. HTTP란?</h1>
<p><strong>HTTP(HyperText Transfer Protocol)</strong>란 전 세계 웹 브라우저와 서버 사이에서 데이터를 주고받기 위한 통신 규약이다. HTTP는 클라이언트-서버 모델을 기반으로 작동하는데, 웹 브라우저는 사용자가 URL을 입력하거나 링크를 클릭할 때마다 HTTP 요청을 보내고, 서버는 그 요청을 처리한 후 응답을 반환한다.</p>
<p>이 요청-응답의 반복을 통해 사용자와 서버 간에 데이터를 주고받는 통신이 이루어진다.</p>
<h2 id="11-구조">1.1. 구조</h2>
<p>HTTP는 애플리케이션 레벨의 프로토콜로, TCP/IP 위에서 작동한다. HTTP는 상태 정보를 유지하지 않는 <strong>Stateless</strong> 프로토콜이며, 클라이언트가 요청을 보낸 후 서버가 응답하는 형태로 이루어진다. </p>
<p>HTTP 메세지는 <strong>Method</strong>, <strong>Path</strong>, <strong>Version</strong>, <strong>Headers</strong>, <strong>Body</strong> 등의 구성 요소 이루어져 있다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/fe0575fe-0704-4175-bcb1-2bc356bb7356/image.png" alt=""></p>
<ul>
<li><strong>Method</strong>: 클라이언트가 서버에 요청하는 방식(GET, POST 등)</li>
<li><strong>Path</strong>: 요청할 리소스의 경로</li>
<li><strong>Version</strong>: HTTP 버전 정보</li>
<li><strong>Headers</strong>: 요청 또는 응답에 대한 부가 정보</li>
<li><strong>Body</strong>: 실제 데이터(필요한 경우)</li>
</ul>
<h2 id="12-작동-방식">1.2. 작동 방식</h2>
<h3 id="121-http-요청과-응답">1.2.1. HTTP 요청과 응답</h3>
<ul>
<li>클라이언트(보통 웹 브라우저)가 서버에 데이터를 요청하는 방식이다. 클라이언트가 URL을 입력하면 해당 요청이 서버로 전송되고, 서버는 그 요청에 맞는 응답을 클라이언트에 반환한다.</li>
<li>HTTP는 다양한 상태 코드로 응답 상태를 나타낸다. 예를 들어, 클라이언트가 요청한 작업이 성공했는지, 실패했는지, 잘못된 요청이었는지 숫자 코드로 나타낸다.</li>
</ul>
<h3 id="122-주요-상태-코드">1.2.2. 주요 상태 코드</h3>
<ul>
<li><code>200 - OK</code>: 요청이 성공적으로 처리되었음을 의미</li>
<li><code>400 - Bad Request</code>: 요청이 잘못되었거나 서버에서 이해할 수 없는 경우</li>
<li><code>404 - Resource Not Found</code>: 요청한 리소스를 찾을 수 없는 경우</li>
</ul>
<h2 id="13-문제점">1.3. 문제점</h2>
<p>초기의 HTTP는 암호화되지 않은 평문 데이터를 전송하는 방식이었다. 이로 인해 민감한 정보(비밀번호, 주민등록번호 등)를 HTTP로 주고받으면 제3자가 정보를 가로챌 수 있는 문제가 있었다. <em>이러한 보안 문제를 해결하기 위해 <strong>HTTPS</strong>가 등장했다.</em></p>
<pre><code>POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

username=johndoe&amp;password=12345</code></pre><h1 id="2-https란">2. HTTPS란?</h1>
<p><strong>HTTPS(HyperText Transfer Protocol Secure)</strong>란 HTTP에 보안 기능을 추가한 확장 버전이다. HTTPS는 <strong>SSL/TLS</strong> 프로토콜을 사용하여 클라이언트와 서버 간 통신을 암호화한다.</p>
<p>HTTPS의 핵심 기능은 데이터 전송 중에 제3자가 데이터를 가로채거나 볼 수 없도록 보호하는 것이다. 포트 번호는 HTTP와 다르게 443번 포트를 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/d5a2e4d9-6152-4b23-96f9-8e89242b99b7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/05eb995b-3853-4ba0-bd83-5b4ad886bdb2/image.png" alt=""></p>
<p>정리하자면, HTTPS는 HTTP에 SSL/TLS가 추가된 프로토콜이다.</p>
<h2 id="21-ssltls">2.1. SSL/TLS</h2>
<p><strong>SSL(Secure Socket Layer)/TLS(Transport Layer Security)</strong>란 웹 서버와 웹 브라우저 간의 암호화 통신을 위해 애플리케이션 계층에서 동작하는 프로토콜이다. 인증, 암호화, 무결성, 지원 프로토콜의 기능을 가진다.</p>
<p>SSL/TLS의 암호화는 세 가지 주요 요소로 구성된다.</p>
<ol>
<li><strong>비대칭키 암호화(Asymmetric Encryption)</strong><ul>
<li>SSL/TLS의 Handshake 과정에서 사용된다. (클라이언트-서버 세션 키(대칭키) 교환을 위한 초기 과정)</li>
<li>RSA, ECDHE(Elliptic Curve Diffie-Hellman Ephemeral) 알고리즘이 사용된다.</li>
</ul>
</li>
<li><strong>대칭키 암호화(Symmetric Encryption)</strong><ul>
<li>비대칭키 암호화 방식이 속도가 느리다는 점 때문에 실제 데이터 전송 과정에서는 대칭키 암호화가 사용된다.</li>
<li>이 대칭키는 Handshake 과정에서 안전하게 교환된 세션 키다.</li>
</ul>
</li>
<li><strong>해시 함수(Hash Function)</strong><ul>
<li>데이터의 무결성을 확인하기 위해 사용한다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/9a29aa58-7012-48f3-979e-a4d0af829ae4/image.png" alt=""></p>
<h2 id="22-연결-과정">2.2. 연결 과정</h2>
<ol>
<li><strong>클라이언트가 서버에 HTTPS 연결 요청</strong><ul>
<li>사용자가 브라우저에 HTTPS URL을 입력하면, 브라우저는 서버에 연결을 요청한다.</li>
<li>서버와 SSL/TLS는 연결을 설정하려고 시도한다.</li>
</ul>
</li>
<li><strong>서버가 클라이언트에 SSL/TLS 인증서 전송</strong><ul>
<li>서버의 공개키와 SSL 인증서의 유효성에 대한 정보를 포함한다.</li>
</ul>
</li>
<li><strong>클라이언트가 인증서를 검증</strong><ul>
<li>SSL 인증서의 서명 및 기만 만료 여부 등을 판단하여 유효성을 판단한다.</li>
<li>이 과정에서 문제가 발생하면, 브라우저는 보안 경고를 표시한다. (e.g. 인증서 만료, 유효하지 않음 등)</li>
</ul>
</li>
<li><strong>세션 키 생성 및 교환(Handshake)</strong><ul>
<li>RSA, ECDHE 방식 중 하나로 세션 키를 안전하게 교환한다.</li>
</ul>
</li>
<li><strong>대칭키 암호화로 데이터 전송</strong></li>
<li><strong>해시 함수를 사용해 데이터 무결성 검증</strong><ul>
<li>HMAC(Hash-based Message Authentication Code) 등의 해시 함수를 사용한다.</li>
</ul>
</li>
<li><strong>세션 종료 및 키 폐기</strong><ul>
<li>ECDHE 방식에서는 세션이 종료될 때 임시 키도 폐기되어 순방향 비밀성을 보장한다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/715641bb-d3ee-4e76-9d0c-82db62b34f68/image.png" alt=""></p>
<h2 id="23-https로의-전환의-필요성">2.3. HTTPS로의 전환의 필요성</h2>
<h3 id="231-보안">2.3.1. 보안</h3>
<p>HTTPS는 HTTP와 달리 <strong>SSL/TLS 암호화</strong>를 사용하여 클라이언트(사용자)와 서버 간에 주고받는 모든 데이터를 암호화한다. 이를 통해 데이터는 기밀성이 보장되고, 무결성이 인증되며, 서버의 신원이 인증된다.</p>
<p>민감한 데이터, 예를 들어 로그인 정보, 신용카드 세부 정보, 개인 식별 정보 등을 안전하게 보호하는 데 필수적이다.</p>
<h3 id="232-신뢰성과-사용자-경험">2.3.2. 신뢰성과 사용자 경험</h3>
<p>보안 외에도 신뢰성과 사용자 경험을 개선하는 데 중요한 역할을 한다. 특히, 브라우저가 HTTP 사이트와 HTTPS 사이트를 다르게 처리하기 때문에 사용자에게 큰 영향을 미친다.</p>
<ul>
<li><p><strong>브라우저의 신뢰도 표시</strong>: HTTPS가 활성화된 웹 사이트는 브라우저 주소 표시줄에 자물쇠 아이콘이 표시된다. 이 아이콘은 사용자 웹 사이트가 안전하다는 것을 즉각적으로 인식할 수 있게 도움을 준다. 반대로, HTTP 웹 사이트는 ‘안전하지 않음’이라는 경고가 표시되며, 이는 사용자에게 부정적인 경험을 줄 수 있다.</p>
<p>  <img src="https://velog.velcdn.com/images/jayy_19/post/30beb533-71f1-46bd-af3e-493a368fcfaf/image.png" alt=""></p>
</li>
<li><p><strong>브라우저 경고 메세지</strong>: 2021년 이후 대부분의 브라우저(크롬, 파이어폭스 등)는 HTTP로 연결된 웹 사이트에 대해 경고 메세지를 표시한다. 사용자에게 ‘이 웹 사이트는 안전하지 않습니다.’라는 메세지가 뜨면, 많은 사용자가 즉시 이탈하게 되어 웹 사이트 방문자 수가 급격히 줄어들 수 있다. 이러한 경고는 사용자에게 불안감을 주기 때문에, 신뢰도가 낮아지는 결과를 초래한다.</p>
<p>  <img src="https://velog.velcdn.com/images/jayy_19/post/b789bcbd-85a4-42f5-ad52-5e487f0d708b/image.png" alt=""></p>
</li>
</ul>
<h3 id="233-seo검색엔진최적화">2.3.3. SEO(검색엔진최적화)</h3>
<p>구글을 비롯한 주요 검색 엔진들은 웹 사이트의 HTTPS 여부를 순위 결정 신호로 사용하고 있다. 구글은 HTTPS를 웹 사이트의 보안성을 평가하는 중요한 요소로 간주하며, 이를 사용하지 않는 웹 사이트는 검색 결과에서 불리한 위치에 놓이게 된다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://brunch.co.kr/@hyoi0303/10">HTTP와 HTTPS 차이점</a></li>
<li><a href="https://velog.io/@dasssseul/CS-HTTP%EC%99%80-HTTPS%EC%9D%98-%EC%B0%A8%EC%9D%B4">[CS📖] HTTP와 HTTPS의 차이</a></li>
<li><a href="https://seo.tbwakorea.com/blog/https-http/">HTTPS란? – HTTP와의 차이점 및 전환의 중요성</a></li>
<li><a href="https://mangkyu.tistory.com/98">[Web] HTTP와 HTTPS의 개념 및 차이점</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] 대칭키 & 공개키]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%8C%80%EC%B9%AD%ED%82%A4-%EA%B3%B5%EA%B0%9C%ED%82%A4</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%8C%80%EC%B9%AD%ED%82%A4-%EA%B3%B5%EA%B0%9C%ED%82%A4</guid>
            <pubDate>Mon, 18 Nov 2024 01:11:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>암호화는 <strong>데이터를 안전하게 전송하고 보관</strong>하기 위해 중요한 역할을 한다. 이 과정에서 데이터를 암호화하는 방식에는 <strong>대칭키 암호화</strong>와 <strong>공개키 암호화</strong>라는 두 가지 주요 방법이 있다. 이를 이해하려면 암호화의 기본 목적과 각각의 방식이 어떻게 작동하는지 알아볼 필요가 있다.</p>
</blockquote>
<h1 id="1-배경">1. 배경</h1>
<p>암호화는 데이터 전송 중에 외부 공격자가 내용을 엿보거나 변조하지 못하게 하기 위해 필요하다. 데이터는 송신자와 수신자 사이에서 주고받는 중에 공격자에게 노출될 수 있기 때문에, 데이터를 보호하기 위해 암호화를 통해 전송 데이터를 암호문으로 바꾸고, 수신자는 다시 이를 복호화해 원본 데이터를 복원한다.</p>
<p>암호화의 목적은 크게 다음과 같다:</p>
<blockquote>
</blockquote>
<ol>
<li><strong>기밀성</strong>: 오직 허가된 사람만이 데이터를 읽을 수 있도록 함.</li>
<li><strong>무결성</strong>: 데이터가 변조되지 않았음을 보장함.</li>
<li><strong>인증</strong>: 송신자와 수신자가 서로의 신원을 확인함.</li>
<li><strong>부인 방지</strong>: 송신자가 나중에 자신이 보낸 데이터를 부인하지 못하도록 함.</li>
</ol>
<p>이러한 목적을 달성하기 위해 다양한 암호화 방식이 개발되었고, 크게 대칭키 암호화와 공개키 암호화로 나뉜다.</p>
<h1 id="2-대칭키-암호화symmetric-key-encryption">2. 대칭키 암호화(Symmetric Key Encryption)</h1>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/9c69d487-c8e6-4272-b9fe-5ba2ba18f90e/image.png" alt=""></p>
<p>대칭키 암호화는 가장 오래된 암호화 방식 중 하나이다. 이 방식에서는 <strong>송신자와 수신자가 동일한 키를 사용</strong>하여 데이터를 암호화하고 복호화한다. 데이터를 암호화할 때 사용한 키가 데이터 복호화에도 사용되기 때문에, 이를 대칭키라고 부른다.</p>
<p>대칭키 암호화는 매우 빠르고, 대용량 데이터를 처리하는 데 적합하지만 키 교환 문제가 있다. 송신자와 수신자가 서로 안전한 방법으로 이 키를 공유해야 하는데, 네트워크를 통해 키를 교환하는 과정에서 키가 탈취될 위험이 있다. 사용자가 많아질수록 각각의 사용자와 키를 안전하게 관리하기가 어려워지며, 이러한 단점 때문에 주로 소규모 환경이나 안전한 키 교환 방법을 사용할 수 있을 때 사용된다.</p>
<h2 id="21-장점">2.1. 장점</h2>
<p>대칭키 암호화는 암호화 및 복호화 속도가 매우 빠르며, 대용량 데이터를 처리하는 데 적합하다.</p>
<h3 id="211-대칭키-암호화의-기본-구조">2.1.1. 대칭키 암호화의 기본 구조</h3>
<p>대칭키 암호화는 단일 키를 사용하여 데이터를 암호화하고 복호화하는데, 이를 블록 단위로 처리하는 경우가 많다. 대칭키 알고리즘에서는 복잡한 수학 연산보다는 효율적인 비트 조작, 순환, 치환, XOR 연산 등이 많이 사용되기 때문에 빠른 속도를 유지할 수 있다.</p>
<h3 id="212-처리-방식">2.1.2. 처리 방식</h3>
<p>대칭키 암호화는 주로 블록 암호나 스트림 암호 방식을 사용한다.</p>
<ul>
<li><strong>블록 암호</strong>: 데이터를 일정한 크기의 블록(e.g. 64비트, 128비트)으로 나눈 뒤 각각의 블록에 동일한 연산을 적용한다. 이 방식은 연산 구조가 규칙적이기 때문에 대규모 병렬 처리가 가능해, 대용량 데이터를 빠르게 처리할 수 있다. AES와 DES가 대표적인 블록 암호 알고리즘이다.</li>
<li><strong>스트림 암호</strong>: 데이터를 연속된 비트 단위로 처리하며, 하나씩 처리하므로 빠르고, 실시간 처리에 적합하다.</li>
</ul>
<p>대칭키 알고리즘은 상대적으로 적은 연산 자원을 사용하므로, 소규모 장치나 네트워크 환경에서도 성능에 크게 부담을 주지 않는다. 이때문에 대칭키 암호화는 특히 대용량 처리에 적합하다.</p>
<h2 id="22-단점">2.2. 단점</h2>
<p>키 교환 과정이 복잡하며, 사용자가 증가할수록 키 관리가 어려워진다. 키 교환 과정이 복잡하다는 의미는, 이 키가 노출되면 모든 보안이 무너질 수 있다는 것을 의미한다.</p>
<h3 id="221-안전한-키-전달의-어려움">2.2.1. 안전한 키 전달의 어려움</h3>
<p>대칭키 암호화에서 송신자가 수신자와 동일한 키를 사용하여 데이터를 암호화하고 복호화하기 때문에, 이 키를 사전에 안전하게 교환해야 한다. 문제는 네트워크 상에서 이 키가 탈취되면, 공격자가 데이터를 쉽게 복호화할 수 있다는 것이다.</p>
<p>예를 들어 송신자가 수신자에게 키를 전달하는 과정에서 중간자 공격(Man-in-the-middle attack)이 발생할 수 있다. 공격자가 키 교환 과정을 가로채면, 전송되는 모든 데이터를 복호화할 수 있게 된다.</p>
<blockquote>
<h4 id="🔖-신뢰할-수-있는-경로의-필요성">🔖 <strong>신뢰할 수 있는 경로의 필요성</strong></h4>
<p>키를 전달하기 위해서는 신뢰할 수 있는 물리적 경로나 암호화된 통신 경로가 필요하다. 하지만 이 경로를 사전에 설정하기 어렵거나 불가능할 때 문제가 발생한다.</p>
</blockquote>
<h3 id="222-사용자-증가에-따른-관리-복잡성">2.2.2. 사용자 증가에 따른 관리 복잡성</h3>
<p>대칭키 암호화에서는 모든 사용자 간에 각각의 키를 관리해야 한다. 사용자가 2명일 때는 하나의 키만 관리하면 되지만, 사용자가 3명이면 서로 간에 3개의 키가 필요하고, 4명이면 6개의 키, 5명이면 10개의 키가 필요하다.</p>
<p>이를 일반화하면, 사용자 수가 $N$명일 때, $N(N-1)/2$개의 키가 필요하다. 사용자가 많아질수록 관리해야 할 키의 수가 기하급수적으로 증가하기 때문에, 이를 안전하게 관리하는 것이 매우 어려워진다.</p>
<h3 id="223-키-합의-프로토콜의-필요성">2.2.3. 키 합의 프로토콜의 필요성</h3>
<p>대칭키 암호화를 안전하게 사용하기 위해 <strong>안전하지 않은 네트워크에서도 안전하게 키를 교환할 수 있는 방법</strong>을 고안했고, 이를 키 합의 프로토콜이라고 한다. 대표적인 키 합의 프로토콜은 RSA와 Diffie-Hellman 기반의 키 교환이다.</p>
<h2 id="23-대표-알고리즘">2.3. 대표 알고리즘</h2>
<p>AES, DES, 3DES, ChaCha20, SEED, ARIA</p>
<h2 id="24-보장-기능">2.4. 보장 기능</h2>
<p>대칭키 암호화는 데이터를 외부로부터 보호하는 데 탁월하지만, <strong>무결성이나 인증 기능을 제공하지 않는다.</strong> 즉, 데이터가 변조되지 않았는지 확인하거나, 송신자의 신원을 보증하는 기능이 부족하다.</p>
<h1 id="3-공개키-암호화asymmetric-key-encryption">3. 공개키 암호화(Asymmetric Key Encryption)</h1>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3a062c73-23ae-43b9-8391-920fe3a6e5a9/image.png" alt=""></p>
<p>공개키 암호화는 대칭키의 키 관리 문제를 해결하기 위해 등장한 방식이다. 비대칭 키 암호화라고도 불리는 이 방식은 서로 다른 두 개의 키, 즉 <strong>공개키</strong>와 <strong>개인키</strong>를 사용한다. 공개키는 누구에게나 공개될 수 있지만, 개인키는 비밀로 유지되어야 한다.</p>
<p>이 방식의 핵심은 한 쪽의 키로 암호화하면, 반대쪽 키로만 복호화할 수 있다는 것이다. 예를 들어, 송신자는 수신자의 공개키로 데이터를 암호화하고, 수신자는 자신의 개인키로 이를 복호화한다. 이를 통해 키 교환 과정에서 발생할 수 있는 보안 문제를 해결할 수 있다.</p>
<p>공개키 암호화는 대칭키 암호화보다 느리지만, 키 관리가 매우 용이하여 다양한 보안 기능을 제공한다. 이를 통해 기밀성뿐만 아니라 인증, 부인 방지 등의 기능도 제공할 수 있다.</p>
<h2 id="31-장점">3.1. 장점</h2>
<h3 id="311-키-관리-용이성">3.1.1. 키 관리 용이성</h3>
<p>대칭키 암호화의 가장 큰 문제점은 키 교환이었다. 송신자와 수신자가 동일한 키를 사용해야 하기 때문에, 이 <strong>키를 서로 안전하게 전달</strong>해야 한다. 하지만 공개키 암호화 방식에서는 이런 문제를 크게 줄일 수 있다.</p>
<ul>
<li><strong>공개키</strong>: 공개키는 누구나 사용할 수 있기 때문에, 키를 미리 교환할 필요가 없다. 이렇게 키가 노출되더라도 개인키가 안전하게 보호되는 한, 보안에는 문제가 없다.</li>
<li><strong>개인키</strong>: 공개키는 누구에게나 배포할 수 있지만, 개인키는 송신자 혹은 수신자만이 소유하는 비밀 키다. 이 개인키는 절대 다른 사람과 공유되지 않기 때문에, 키 교환 과정에서 발생할 수 있는 공격을 방지할 수 있다.<blockquote>
<p> 즉, 공개키는 네트워크 상에 공개적으로 배포될 수 있고, 악의적인 사용자도 이를 가질 수 있지만, 그 자체로는 보안에 영향을 미치지 않으므로 대규모 환경에서도 키 관리가 쉬워진다. 대칭키 암호화에서는 사용자마다 각각의 키를 관리해야 하는 복잡성이 있지만, 공개키 암호화에서는 사용자별로 하나의 공개키와 개인키만 관리하면 된다. 사용자가 늘어나도 추가적인 키 교환이 필요 없기 때문에 <strong>확장성</strong>이 뛰어나다.</p>
</blockquote>
</li>
</ul>
<h3 id="312-다양한-보안-기능">3.1.2. 다양한 보안 기능</h3>
<p>공개키 암호화는 단순한 <strong>기밀성뿐만 아니라 인증, 부인 방지 등 다양한 보안 기능을 제공한다.</strong> 이들은 모두 암호화의 핵심 보안 요소들이다.</p>
<ul>
<li><strong>기밀성</strong>: 송신자는 수신자의 공개키로 데이터를 암호화하고, 수신자는 개인키로 이를 복호화한다. 중간에 공격자가 데이터를 가로채더라도 개인키가 없으면 복호화할 수 없다. 이는 송신된 정보가 비밀로 유지될 수 있음을 의미한다.</li>
<li><strong>인증</strong>: 공개키 암호화는 자신이 누구인지 증명하는 데 사용할 수 있다. 송신자가 자신의 개인키로 데이터를 암호화하면, 수신자는 그 데이터를 해당 송신자의 공개키로만 복호화할 수 있다. 공개키로 복호화가 가능다하면 그 데이터는 해당 개인키 소유자, 즉 송신자만이 암호화할 수 있다는 것이 보장되기 때문에 송신자가 개인키를 소유한 진짜 사용자임을 인증할 수 있다.</li>
<li><strong>부인 방지:</strong> 송신자가 데이터를 보낸 후에 이를 부인할 수 없게 만든다. 송신자가 개인키로 서명한 데이터는 해당 개인키에 대응하는 공개키로만 검증할 수 있기 때문에, 송신자가 나중에 “내가 보낸 데이터 아닌데?”라고 주장할 수 없다. 이는 법적 책임을 보장하는 중요한 기능이다.</li>
</ul>
<h2 id="32-단점">3.2. 단점</h2>
<p><strong>암호화 및 복호화 속도가 느리다.</strong> 따라서 대용량 데이터 암호화보다는 소량 데이터 암호화나 대칭키 교환에 사용되는 경우가 많다.</p>
<h3 id="321-단방향-연산의-복잡성">3.2.1. 단방향 연산의 복잡성</h3>
<p>공개키 암호화 알고리즘은 대부분 소수 분해, 이산 로그 문제, 타원 곡선 연산 등의 매우 복잡한 수학적 원리를 담은 단방향 함수를 사용한다. 이러한 연산은 계산량이 매우 크고, 컴퓨터에서 처리하는 데 시간이 많이 걸린다.</p>
<ul>
<li><strong>RSA 알고리즘</strong>:<ul>
<li>암호화(쉬운 연산): 두 개의 큰 소수를 곱하여 하나의 큰 수를 만든다.</li>
<li>복호화(어려운 연산): 이 큰 수에서 다시 두 소수로 분해하는 것은 까다롭다. 이 문제는 소인수 분해 문제로 불리며, 현재 컴퓨터 성능으로는 매우 오랜 시간이 걸릴 수 있다.</li>
</ul>
</li>
<li><strong>ECC(타원 곡선 암호화)</strong>:<ul>
<li>암호화(쉬운 연산): 타원 곡선의 한 점을 기반으로 연산을 수행하여 암호화한다.</li>
<li>복호화(어려운 연산): 타원 곡선의 한 점을 사용하여 다시 원래의 값을 추정하는 것은 이산 로그 문제로, 시간이 많이 소요된다. 이 문제는 현재 알고리즘으로는 효율적인 해법이 없기 때문에 높은 보안성을 제공한다.</li>
</ul>
</li>
</ul>
<h3 id="322-비트-크기">3.2.2. 비트 크기</h3>
<p>공개키 암호화에서는 높은 보안 수준을 유지하기 위해 긴 비트 크기의 키를 사용한다. 예를 들어, RSA 알고리즘에서는 안전성을 확보하기 위해 최소 2048비트 이상의 키를 사용하는 것이 권장된다. 이와 비교해 대칭키 암호화에서는 128비트, 256비트 정도의 키가 주로 사용된다.</p>
<h2 id="33-사용-사례">3.3. 사용 사례</h2>
<h3 id="331-소량-데이터-암호화">3.3.1. 소량 데이터 암호화</h3>
<p>공개키 암호화는 대용량 데이터를 처리하는 데는 부적합하지만, 소량의 중요한 정보를 처리하는 데에는 매우 유용하다.</p>
<p>예를 들어, 은행 계좌 정보, 비밀번호, 인증 정보 등 중요한 정보를 안전하게 암호화하는 데 사용된다. 이러한 소량의 데이터를 처리할 때는 공개키 암호화의 속도 저하가 크게 문제되지 않기 때문에, 안전성과 보안성을 우선할 수 있다.</p>
<h3 id="332-대칭키-교환">3.3.2. 대칭키 교환</h3>
<p>공개키 암호화는 대칭키 교환에 매우 자주 사용된다. 대칭키 암호화는 빠르지만 키 교환 과정에서 보안 문제가 발생할 수 있기 때문에, 공개키 암호화를 사용하여 대칭키(세션 키)를 안전하게 교환한 후, 대칭키로 실제 데이터를 암호화하는 방식이 일반적이다.</p>
<p>이 과정은 다음과 같다.</p>
<blockquote>
</blockquote>
<ol>
<li>송신자가 수신자의 공개키로 대칭키를 암호화하여 전송한다.</li>
<li>수신자는 자신의 개인키로 대칭키를 복호화한다.</li>
<li>이렇게 공유된 대칭키를 사용하여, 송신자와 수신자는 빠른 대칭키 암호화 방식을 사용해 대용량 데이터를 암호화하고 주고받는다.</li>
</ol>
<p>e.g. 웹에서 HTTPS 통신을 할 때 SSL/TLS 프로토콜은 공개키 암호화를 사용하여 대칭키를 안전하게 교환한 후, 그 대칭키로 실제 통신 데이터를 암호화한다. 이를 통해 보안성과 속도 모두를 확보할 수 있다.</p>
<h2 id="34-대표-알고리즘">3.4. 대표 알고리즘</h2>
<ul>
<li>RSA: 대표적인 공개키 암호화 방식으로 많이 사용된다.</li>
<li>Diffie-Hellman: 최초의 공개키 알고리즘이나, 위조에 취약하다.</li>
<li>ECC: 짧은 키로 높은 보안 강도를 제공하며, 모바일 기기 등에 적합하다.</li>
<li>DSA: 전자서명 표준 알고리즘</li>
</ul>
<h1 id="4-디지털-인증서와-공개키-인프라pki">4. 디지털 인증서와 공개키 인프라(PKI)</h1>
<p>공개키 암호화는 키 관리가 용이하지만, 한 가지 문제점이 있다. 송신자가 받은 공개키가 정말 수신자의 공개키인지 확신할 방법이 필요하다. 이를 위해 <strong>디지털 인증서</strong>와 <strong>공개키 인프라(PKI)</strong>가 사용된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/aeaf05e7-c764-46e9-896d-d597d3905e85/image.png" alt=""></p>
<p>디지털 인증서는 신뢰할 수 있는 제3자인 CA(Certificate Authority)가 발급한다. 공개키 교환 절차는 다음과 같다.</p>
<blockquote>
</blockquote>
<ol>
<li>B는 자신의 공개키를 CA에 제출하여, 그 키가 자신에게 속한다는 인증서를 발급받는다. </li>
<li>B의 인증서를 A에게 보낸다. 이 인증서에는 B의 공개키가 포함되어 있다.</li>
<li>A는 CA의 공개키로 B의 인증서를 검증하고, 서명이 유효하다면 인증서 안의 B의 공개키를 사용한다.</li>
<li>이제 A는 B의 공개키를 사용해 안전하게 통신을 시작할 수 있다.</li>
</ol>
<h1 id="5-기밀성-보장을-위한-노력">5. 기밀성 보장을 위한 노력</h1>
<p>앞서 설명한 바와 같이 대칭키를 안전하게 공유할 수 있는 방법에 대한 필요성으로 <strong>키 합의 프로토콜</strong>이 등장했다. 대표적인 키 합의 프로토콜에 대해 알아보자.</p>
<h2 id="51-rsa-키-합의-프로토콜">5.1. RSA 키 합의 프로토콜</h2>
<p>비대칭키 암호화인 RSA를 사용한 키 합의 프로토콜이다. </p>
<p>앨리스는 앞으로 통신에 사용할 세션키를 만들어서 이를 밥의 공개키로 암호화한 다음 밥에게 전송한다. 밥은 자신의 개인키로 암호화된 세션키를 복호화해 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/43386c99-08a3-4f19-bc24-25a3291c4c81/image.png" alt=""></p>
<p>문제점은 이 방식에서는 순방향 비밀성이 보장되지 않는다는 것이다.</p>
<p><strong>순방향 비밀성(Forward Secrecy)</strong>이란, 미래에 개인키가 유출되더라도, 과거의 세션 내용은 복호화할 수 없도록 보장하는 것이다. 하지만 RSA 방식에서는 만약 공격자가 이후에 밥의 개인키를 탈취하면, 이전 통신에서 사용한 세션키를 복호화하여 모든 과거의 통신 내용을 해독할 수 있게 된다.</p>
<h2 id="52-diffie-hellman-키-합의-프로토콜">5.2. Diffie-Hellman 키 합의 프로토콜</h2>
<p>디피-헬만 키 교환은 순방향 비밀성을 제공하는 대표적 키 합의 프로토콜이다. 이 프로토콜은 암호화되지 않은 공용 채널에서도 안전하게 대칭키를 생성하고 교환할 수 있도록 설계되었다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/60ed1072-f530-44f6-9446-4465a0dc9386/image.png" alt=""></p>
<blockquote>
</blockquote>
<ol>
<li><strong>기저키 선택</strong>: 앨리스와 밥은 서로 통신에 사용할 기저키(공용 값)를 공개적으로 선택한다. 이 기저키는 노출되어도 상관 없다.</li>
<li><strong>개인키 설정</strong>: 앨리스와 밥은 각각 자신만 아는 비밀키를 정한다.</li>
<li><strong>공유 정보 생성</strong>: 앨리스와 밥은 각자의 비밀키와 기저키를 조합하여 중간 값을 계산하고, 그 값을 서로에게 전송한다.</li>
<li><strong>최종 키 계산</strong>: 앨리스는 밥의 중간 값에 자신의 비밀키를 추가로 조합하여 최종 공유 키를 계산하고, 밥도 마찬가지로 앨리스의 중간 값에 자신의 비밀키를 추가하여 같은 최종 공유 키를 계산한다.</li>
<li><strong>비밀키 삭제</strong>: 세션이 끝나면 앨리스와 밥은 각자의 비밀키를 즉시 폐기한다.</li>
</ol>
<p>설사 나중에 공격자가 밥의 개인키를 탈취하더라도, 그로부터 얻을 수 있는 정보는 세션키를 만들기 위한 단서(힌트) 정도일 뿐, 실제 세션키나 과거의 통신키 내용을 복호화할 수는 없다.</p>
<h1 id="6-무결성-보장을-위한-노력">6. 무결성 보장을 위한 노력</h1>
<p>무결성을 보장하기 위해서는 데이터가 전송 중에 손상되거나 변경되었는지를 확인할 수 있어야 한다. 이를 위해 주로 <strong>해시 함수</strong>와 <strong>메세지 인증 코드(MAC)</strong>가 사용된다.</p>
<h2 id="61-해시-함수hash-function">6.1. 해시 함수(Hash Function)</h2>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/895f8c89-cb47-4a74-bfa6-72ebb20f1ac3/image.png" alt=""></p>
<p>해시 함수는 <strong>임의의 길이를 가진 데이터를 일정한 길이의 고정된 값으로 변환하는 함수</strong>이다. 이 고정된 값이 바로 해시 값 또는 해시라고 불린다.</p>
<h3 id="611-암호학적-해시-함수의-특징">6.1.1. 암호학적 해시 함수의 특징</h3>
<p>암호학적으로 안전한 해시 함수는 다음과 같은 특성을 가져야 한다.</p>
<ol>
<li><strong>단방향성</strong>: 해시 값만 보고 원래 입력 데이터를 알아내는 것이 불가능하거나 매우 어렵다.</li>
<li><strong>충돌 회피</strong>: 동일한 해시 값을 가지는 서로 다른 데이터(충돌)를 찾는 것이 매우 어렵다.</li>
<li><strong>민감도</strong>: 입력 데이터에서 단 1비트라도 변경되면 완전히 다른 해시 값이 출력된다.</li>
</ol>
<p>e.g. SHA-256: 매우 널리 사용되는 암호학적 해시 함수이다. 256비트(32바이트)의 해시 값을 생성하며, 블록 체인 기술에서도 많이 사용된다.</p>
<h3 id="612-해시-함수의-역할">6.1.2. 해시 함수의 역할</h3>
<p>데이터를 송신할 때, <strong>데이터의 해시 값을 함께 전송하여 수신자가 이를 검증</strong>할 수 있게 한다. 수신자가 데이터의 해시 값을 재계산하고 송신자가 보낸 해시 값과 비교하여 데이터가 변조되었는지 확인할 수 있다.</p>
<p>만약 두 해시 값이 다르면 데이터가 전송 중에 손상되었거나 변경된 것이다.</p>
<h3 id="613-문제점">6.1.3. 문제점</h3>
<p>해시 함수는 데이터가 변경되었는지 여부를 판단할 수는 있지만, <strong>누가 데이터를 변경했는지 확인할 수는 없다.</strong> 즉, 악의적인 공격자가 데이터를 변경한 후 새로운 해시 값을 생성하여 위조할 수 있다.</p>
<p>따라서 해시 함수만으로는 완전한 무결성을 보장하기 어렵다. 이를 보완하기 위해 사용하는 것이 키가 포함된 해시 함수, 즉 <strong>메세지 인증 코드(MAC)</strong>이다.</p>
<h2 id="62-메세지-인증-코드mac-message-authentication-code">6.2. 메세지 인증 코드(MAC: Message Authentication Code)</h2>
<p>메세지 인증 코드는 대칭키 암호화와 해시 함수를 결합한 방식으로, 메세지의 무결성과 인증을 동시에 제공한다. MAC은 송신자가 데이터를 변조하지 않도록 보호하고, 수신자는 해당 데이터가 변조되지 않았는지, 그리고 인증된 송신자로부터 온 것인지 확인할 수 있다.</p>
<p>MAC은 단순 해시 함수와 달리, <strong>송신자와 수신자가 미리 공유한 대칭키를 사용하여 생성</strong>한다. 대칭키는 송신자와 수신자만이 알고있기 때문에, 제3자가 이 키를 모르면 MAC 값을 생성하거나 변조할 수 없다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/f7980d11-17ac-41d4-aae0-44706dba4aba/image.png" alt=""></p>
<h3 id="621-인증-과정">6.2.1. 인증 과정</h3>
<blockquote>
</blockquote>
<ol>
<li><strong>키 공유</strong>: 송신자 앨리스와 수신자 밥은 미리 공유된 대칭키를 가지고 있다.</li>
<li><strong>MAC 값 생성</strong>: 송신자 앨리스는 메세지와 대칭키를 결합하여 MAC 값을 계산한다. 이를 해시 값과 비슷하게 생각할 수 있다.</li>
<li><strong>메세지 전송</strong>: 앨리스는 메세지와 함께 MAC 값을 수신자 밥에게 전송한다.</li>
<li><strong>MAC 검증</strong>: 밥은 메세지와 미리 공유된 대칭키를 사용하여 MAC 값을 재계산한 후, 앨리스가 보낸 MAC 값과 비교한다.</li>
<li><strong>무결성 및 인증 확인</strong>: 두 MAC 값이 같다면 데이터가 변조되지 않았고, 해당 데이터가 앨리스로부터 온 것임을 확인할 수 있다.</li>
</ol>
<h3 id="622-장점">6.2.2. 장점</h3>
<ul>
<li><strong>무결성 보장</strong>: 메세지가 전송 중 변경되었을 경우, 수신자가 이를 쉽게 알아낼 수 있다.</li>
<li><strong>인증 기능</strong>: 송신자와 수신자가 사전에 공유된 키를 사용하기 때문에, 수신자는 이 메세지가 진짜 송신자로부터 온 것임을 확인할 수 있다.</li>
<li><strong>안전성</strong>: MAC 값은 대칭키를 사용하여 계산되므로, 공격자가 대칭키를 알지 못하면 MAC 값을 위조할 수 없다.</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://velog.io/@gs0351/%EB%8C%80%EC%B9%AD%ED%82%A4-vs-%EA%B3%B5%EA%B0%9C%ED%82%A4%EB%B9%84%EB%8C%80%EC%B9%AD%ED%82%A4">대칭키 vs 공개키(비대칭키)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] UDP]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-UDP</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-UDP</guid>
            <pubDate>Fri, 15 Nov 2024 01:43:19 GMT</pubDate>
            <description><![CDATA[<h1 id="1-개요">1. 개요</h1>
<h2 id="11-ipinternet-protocol의-한계">1.1. IP(Internet Protocol)의 한계</h2>
<p>IP는 인터넷에서 Host to Host 간의 데이터를 전달하는 기본적인 프로토콜이다. 여기서 중요한 점은, IP는 데이터가 목적지에 도착할 때까지의 경로를 책임지는 역할은 하지만, 그 이후에 수많은 프로그램들이 어떻게 통신할지에 대한 정보는 제공하지 않는다는 것이다.</p>
<p>즉, IP는 장비 간의 통신만을 지원할 뿐, 그 장비 내부에서 어떤 애플리케이션이 데이터를 받을지에 대한 처리를 할 수 없다.</p>
<blockquote>
<h4 id="🚨-icmpinternet-control-message-protocol와-ip의-오류-처리">🚨 <strong>ICMP(Internet Control Message Protocol)와 IP의 오류 처리</strong></h4>
</blockquote>
<p>IP가 제공하는 통신 과정에서 오류가 발생할 때는 <strong>ICMP</strong>가 오류를 알려준다. 예를 들어, 목적지에 데이터가 도착하지 못했거나, 라우터에서 문제가 발생하면 ICMP는 송신 측에 오류 메세지를 보내 네트워크 문제를 감지하게 한다.</p>
<blockquote>
</blockquote>
<p>하지만, ICMP는 그저 오류를 알리는 역할만 할 뿐, 문제를 해결하거나 데이터를 재전송하는 등의 대처는 하지 못한다.</p>
<h2 id="12-포트-번호와-상위-프로토콜의-등장">1.2. 포트 번호와 상위 프로토콜의 등장</h2>
<h3 id="121-포트-번호port-number">1.2.1. 포트 번호(Port Number)</h3>
<p>여러 애플리케이션이 한 장비 안에서 동시에 통신하는 상황을 해결하기 위해 등장한 개념이 포트 번호다.</p>
<p>IP는 장치 간의 통신만 해결할 수 있기 때문에, 장비 내부에서 어떤 애플리케이션이 데이터를 받을지 식별하기 위해 포트 번호가 필요하다. 이를 통해 같은 장치 내에서 여러 프로그램이 동시에 네트워크 통신을 할 수 있게 되었다.</p>
<h3 id="122-tcp와-udp">1.2.2. TCP와 UDP</h3>
<p>TCP와 UDP는 IP 위에서 동작하는 전송 계층 프로토콜이다. 이 프로토콜들이 등장한 이유는 오류 처리와 프로그램 간의 데이터 통신을 좀 더 효율적으로 하기 위해서다.</p>
<ul>
<li><strong>TCP(Transmission Control Protocol)</strong>: <strong>신뢰성 있는 데이터 전송</strong>을 보장한다. 데이터 분실, 중복, 순서 뒤바뀜 등을 해결해 송신 측에서 수신 측으로 정확하게 데이터를 전달할 수 있도록 한다. 이를 위해 연결 설정(3-Way Handshake), 재전송, 흐름 제어, 혼잡 제어와 같은 복잡한 메커니즘을 사용한다.</li>
<li><strong>UDP(User Datagram Protocol)</strong>: 간단한 IP 상위 계층 프로토콜로, TCP와 달리 에러 처리나 재전송을 하지 않는다. 즉, UDP는 데이터 전송 시 오류가 발생할 수 있고, 순서가 뒤바뀔 수 있으며, 신뢰성이 보장되지 않지만 그만큼 <strong>빠르고 간단하게 데이터를 전송</strong>할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/024a9b86-1677-415d-ba31-fdb2fa7b1457/image.png" alt=""></p>
<h1 id="2-udpuser-datagram-protocol">2. UDP(User Datagram Protocol)</h1>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3582950b-e7da-418d-9b29-0760fce8668f/image.png" alt=""></p>
<p>TCP와 UDP의 등장 배경을 살펴봤으니, 오늘은 UDP에 대해 알아보자.</p>
<p>UDP는 TCP와 함께 OSI 모델 4계층인 전송 계층에 포함되는 프로토콜이다. IP를 사용하는 네트워크 내에서 컴퓨터 간 메시지들이 교환될 때 제한된 서비스만 제공하는 프로토콜이다.</p>
<p>UDP는 데이터를 전송할 때 IP 프로토콜을 통해 데이터그램이라는 단위로 데이터를 전송한다. TCP와 달리 메시지를 데이터그램으로 나누고, 반대편에서 재조립, 순서 조정 등의 서비스는 하지 않는다. </p>
<p>UDP는 IP 계층에서 제공하지 않는 서비스도 제공하는데, IP 내 사용자 구분을 위한 <strong>포트 번호 부여</strong>와 데이터 손상 여부 확인에 쓰이는 <strong>체크섬 서비스</strong>다.</p>
<h2 id="21-특징">2.1. 특징</h2>
<ul>
<li><strong>비연결형(Connectionless)</strong>: TCP처럼 연결을 설정하지 않고, 데이터를 보내기 전에 세션을 확립하지 않는다. 송신자는 데이터를 바로 전송하고, 수신자는 받는 즉시 처리한다.</li>
<li><strong>빠른 속도</strong>: 연결 설정이나 오류 제어 같은 추가 작업이 없기 때문에 데이터 전송 속도가 빠르다. 실시간 응답이 필요한 애플리케이션에서 많이 사용된다.</li>
<li><strong>신뢰성 없음</strong>: TCP와 달리 데이터 전송의 신뢰성을 보장하지 않는다. 즉, 패킷이 손실되거나, 순서가 바뀌거나, 중복되더라도 UDP는 이를 감지하지도, 처리하지도 않는다. 오류 처리는 애플리케이션이 직접 해야한다.</li>
<li><strong>작은 오버헤드</strong>: TCP보다 헤더가 간단해서 네트워크에 부하를 덜 주고, 자원 소모가 적다.</li>
</ul>
<h2 id="22-udp를-사용하는-이유">2.2. UDP를 사용하는 이유</h2>
<h3 id="221-데이터의-신속성">2.2.1. 데이터의 신속성</h3>
<p>UDP의 가장 큰 장점은 <strong>데이터 전송의 신속성</strong>이다. TCP는 연결 설정과 확인 응답, 흐름 제어 등의 과정을 거쳐야 하기 때문에 전송 속도가 느리지만, UDP는 이런 과정 없이 데이터를 바로 전송하므로 빠르게 데이터를 주고받을 수 있다.</p>
<p>이로 인해 실시간 통신이 중요한 상황(e.g. 실시간 스트리밍, 온라인 게임 등)에서 UDP가 많이 사용된다.</p>
<h3 id="222-dns에서의-udp">2.2.2. DNS에서의 UDP</h3>
<p>DNS(Domain Name System)는 도메인 이름을 IP 주소로 변환하는 작업을 하는데, 이때 UDP를 사용한다.</p>
<p>DNS 쿼리와 응답은 대부분 짧은 데이터이지만, 매우 많은 요청이 들어온다. UDP는 TCP와 달리 오버헤드가 적고, 데이터를 전송하기 위한 세션 확립 과정이 없기 때문에, 빠르게 요청과 응답을 처리하고 네트워크 리소스를 절약할 수 있다.</p>
<p>하지만, 512바이트를 넘는 데이터를 전송하거나 DNS 서버 간의 영역 전송(Zone Transfer) 같은 작업을 할 때는 TCP를 사용한다. TCP는 더 큰 데이터 전송이나 신뢰성이 중요한 작업에 적합하다.</p>
<h2 id="23-헤더-구조">2.3. 헤더 구조</h2>
<p>UDP는 간단한 프로토콜이기 때문에 헤더도 매우 단순하다. 헤더는 총 8바이트로, 아래와 같이 구성된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/d70a7ffb-29e7-423a-91e7-545f6d70b226/image.png" alt=""></p>
<ul>
<li><strong>Source Port</strong>(2바이트): 송신 측의 포트 번호</li>
<li><strong>Destination Port</strong>(2바이트): 수신 측의 포트 번호</li>
<li><strong>Length</strong>(2바이트): 전체 UDP 패킷의 길이(헤더와 데이터 포함)</li>
<li><strong>Checksum</strong>(2바이트): 오류 검출을 위한 값(필수는 아님)</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://brunch.co.kr/@swimjiy/35">그림으로 쉽게 보는 TCP</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] TCP/IP 흐름제어 & 혼잡제어]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCPIP-%ED%9D%90%EB%A6%84%EC%A0%9C%EC%96%B4-%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCPIP-%ED%9D%90%EB%A6%84%EC%A0%9C%EC%96%B4-%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Thu, 14 Nov 2024 01:10:48 GMT</pubDate>
            <description><![CDATA[<p><strong>TCP(Transmission Control Protocol)</strong>는 네트워크 통신의 근간을 이루는 중요한 프로토콜이다. TCP는 단순히 데이터를 보내고 받는 것 이상의 기능을 제공하는데, 주된 세 가지 기능은 <strong>흐름 제어</strong>, <strong>오류 제어</strong>, 그리고 <strong>혼잡 제어</strong>가 있다.</p>
<p>이 기능들 덕분에 네트워크에서 발생할 수 있는 문제를 TCP가 알아서 처리해주기 때문에 상위 레이어, 즉 애플리케이션 개발자는 복잡한 네트워크 상황을 신경 쓸 필요가 없다.</p>
<h1 id="1-흐름-제어flow-control">1. 흐름 제어(Flow Control)</h1>
<p>흐름 제어는 송신 측과 수신 측의 데이터 처리 속도를 맞추기 위한 장치이다. 송신 측이 데이터를 너무 빨리 보내면 수신 측의 버퍼가 꽉 차서 데이터를 제대로 처리하지 못할 수 있다. 이때 발생하는 문제가 <strong>오버플로우(Overflow)</strong>다. 이를 막기 위해 TCP는 <strong>윈도우 크기(Window Size)</strong>를 통해 송신 측이 보낼 수 있는 데이터 양을 조절한다.</p>
<p>윈도우 크기는 수신 측이 자신의 버퍼에서 처리할 수 있는 데이터의 양을 송신 측에 전달하는 정보다. 그래서 송신 측은 이 정보를 바탕으로 한 번에 얼마나 데이터를 보낼지 결정한다.</p>
<h2 id="11-stop-and-wait-방식">1.1. Stop and wait 방식</h2>
<p>가장 단순한 흐름 제어 방식은 Stop-and-Wait 방식이다. 송신 측이 데이터를 보낸 후, 수신 측의 응답(ACK)를 기다렸다가 다음 데이터를 전송하는 방식이다. </p>
<p>기본적인 <strong>ARQ(Automatic Repeat Request)</strong>를 구현한다고 생각해 보면, 수신 측의 윈도우 크기를 1 byte로 설정하고 <code>처리 가능 = 1</code>, <code>처리 불가능 = 0</code>과 같은 식으로 대충 구현해도 돌아가기는 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/594ee919-4ede-4177-8e15-10246cd8e3e9/image.png" alt=""></p>
<p>이 방식은 매우 간단한 만큼 비효율적이다. 송신 측이 자신의 데이터를 직접 보내봐야 이 데이터를 수신 측이 처리할 수 있는지 알 수 있기 때문이다.</p>
<p>이때문에 Stop and Wait 방식을 사용해 흐름 제어를 할 경우에는, 이런 비효율성을 커버하기 위해 이런 단순한 구현이 아닌 여러 가지 오류 제어 방식을 함께 도입해 사용한다.</p>
<h2 id="12-sliding-window-방식">1.2. Sliding Window 방식</h2>
<p>그렇기에 더 효율적인 방식인 Sliding Window가 등장한다. Sliding Window는 수신 측이 자신이 한 번에 받을 수 있는 데이터 양(윈도우 크기)을 송신 측에 알려주고, 송신 측은 그 범위 안에서 여러 개의 데이터를 한 번에 보낼 수 있다.</p>
<p>e.g. 수신 측이 한 번에 5개의 패킷을 처리할 수 있다면 송신 측은 응답을 기다리지 않고 5개의 패킷을 연달아 보낸다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/bf797196-d2c6-404e-8cc2-993c4cd05f49/image.png" alt=""></p>
<p>윈도우 안에 들어있는 프레임은 수신 측의 응답이 없어도 연속으로 보낼 수 있다.</p>
<p>송신 측의 윈도우 크기는 맨 처음 TCP의 연결을 생성하는 과정인 3-Way Handshake 때 결정된다. 이때 송신 측과 수신 측은 자신의 현재 버퍼 크기를 서로에게 알려주게 되고, 송신 측은 수신 측이 보내준 버퍼 크기를 사용해 자신의 윈도우 크기를 정하게 된다.</p>
<pre><code class="language-bash">localhost.initiator &gt; localhost.receiver: Flags [S], seq 1487079775, win 65535
localhost.receiver &gt; localhost.initiator: Flags [S.], seq 3886578796, ack 1487079776, win 65535
localhost.initiator &gt; localhost.receiver: Flags [.], ack 1, win 6379</code></pre>
<p><code>tcpdump</code>를 통해 3-Way Handshake를 관찰해 보자. 처음 <code>SYN</code>과 <code>SYN+ACK</code> 패킷에는 각자 자신의 버퍼를 알려준 후 마지막 <code>ACK</code> 패킷 때 송신 측이 자신이 정한 윈도우 사이즈를 수신 측에 통보한다.</p>
<p>이때 송신, 수신 측 모두 자신의 버퍼 크기를 <code>65535</code>라고 했지만 최종적으로 송신 측이 정한 윈도우 크기는 <code>6379</code>이다. 왜 송신 측은 수신 측 버퍼 크기의 10분의 1로 자신의 윈도우 크기를 정한 것일까?</p>
<p>송신 측의 윈도우 크기는 수신 측의 버퍼 크기로만 정하는 것이 아니라 다른 여러 가지 요인들을 함께 고려해 결정되기 때문이다. 네트워크 상태에 따라, 예를 들어 패킷 왕복 시간(Round Trop Time, RTT)이 길어지거나 네트워크가 혼잡하다고 판단되면, 송신 측은 윈도우 크기를 줄여서 네트워크에 부담을 덜 주도록 조절할 수 있다.</p>
<p>이때 정해진 윈도우 크기는 통신을 하는 과정 중간에도 계속해서 네트워크의 혼잡 환경과 수신 측이 보내주는 윈도우 크기를 통해 동적으로 변경될 수 있다.</p>
<blockquote>
<h4 id="🪟-이름이-왜-sliding-window인가">🪟 <strong>이름이 왜 Sliding Window인가?</strong></h4>
</blockquote>
<p>먼저, 송신 측이 0~6번의 시퀀스 번호를 가진 데이터를 상대방에게 전송하고 싶어하는 상황을 생각해 보자. 이때 송신 측의 버퍼에는 전송해야 할 데이터들이 아래와 같이 담겨져 있을 것이다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/8388d262-c8e2-4551-a720-fbc770cf51b6/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>이때 송신 측은 수신 측에게 받은 윈도우 크기와 현재 네트워크 상황을 고려하여 윈도우 크기를 3으로 잡았고, 윈도우 안에 있는 데이터를 순차적으로 전송한다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/26ea4f72-3103-418c-8045-9e34b44f7b27/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>송신 측이 데이터를 전송했을 때, 그 데이터가 수신 측에 도착했는지, 혹은 수신 측이 잘 처리했는지 여부는 아직 모르는 상태이다. 이 상황에서 송신 측의 윈도우에는 이미 전송한 데이터들이 남아있지만, 응답(ACK)을 받기 전까지는 여전히 확인되지 않은 상태인 것이다.</p>
<blockquote>
</blockquote>
<p>이처럼 윈도우 안에 있는 데이터들은 모두 전송이 끝났지만, 아직 수신 측이 응답을 보내지 않았기 때문에 “불확실한” 상태로 남아있다. 이 점이 슬라이딩 윈도우의 중요한 포인트 중 하나다. 즉, 송신 측은 항상 여러 개의 데이터를 보내면서도 그 데이터들이 실제로 전송이 완료되었는지 여부는 나중에 확인해야 한다.</p>
<blockquote>
</blockquote>
<p>이제 수신 측이 데이터를 처리한 후, 응답(ACK)을 보내면서 “나 윈도우 크기 1만큼 남았어”라는 메세지를 보낸다고 가정해 보자. 이때 송신 측은 그 메세지를 받고 나서, 윈도우를 한 칸 오른쪽으로 밀어서 새롭게 들어온 데이터를 보낼 준비를 한다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/c42aeb0a-d370-4064-8f5e-10c81d2b936d/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>이 과정이 슬라이딩 윈도우에서 중요한 “슬라이딩” 동작이다. 윈도우가 한 칸씩 옆으로 밀리면서 새로운 데이터를 전송한다. 예를 들어, 송신 측은 이제 3번 데이터를 윈도우에 넣고 전송할 수 있게 된다.</p>
<blockquote>
</blockquote>
<p>단, 이 경우 송신 측의 윈도우 크기가 3이기 때문에 수신 측이 4를 보냈다고 해서 4칸을 밀지는 않고, 자신의 윈도우 크기인 3만큼만 밀 수 있다. 그러나 이 경우에는 송신 측이 수신 측의 퍼포먼스가 더 좋아졌다는 것을 알았으니 자신의 윈도우 크기를 늘리는 방법으로 대처할 수 있을 것이다.</p>
<blockquote>
</blockquote>
<p>즉, 슬라이딩 윈도우 방식은 <code>보내고→응답 받고→윈도우 밀고</code>를 반복하면서, 현재 자신이 보낼 수 있는 데이터를 최대한 연속적으로 보내는 방법이라고 할 수 있다.</p>
<h1 id="2-오류-제어error-control">2. 오류 제어(Error Control)</h1>
<p>TCP는 데이터가 손상되거나 유실되는 것을 허용하지 않는다. 네트워크 통신에서 오류가 발생하면, 이를 감지하고 다시 데이터를 전송하는 방식으로 오류를 처리한다. 이를 재전송 기반 오류제어, <strong>ARQ(Automatic Repeat Request)</strong>라고 부른다. </p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/e402e330-e4bb-4961-9f76-7107092f23f8/image.png" alt=""></p>
<p>패킷 기반 전송을 하는 TCP의 특성 상 각 패킷의 도착 순서가 무조건 보장되는 것이 아니기에, 중복된 ACK를 보통은 3회 정도 받았을 때 에러라고 판별한다.</p>
<p>즉, <strong>데이터를 제대로 받지 못했으면 그 데이터를 다시 보내달라고 요청</strong>하는 방식이다. 이 방식이 TCP가 신뢰성을 제공하는 핵심 이유이다.</p>
<p>하지만 이 재전송이라는 작업 자체가 했던 일을 다시 해야하는 비효율적인 작업이기 때문에, 이 재전송 과정을 최대한 줄일 수 있는 여러 가지 방법을 사용하게 된다.</p>
<blockquote>
<h4 id="🚨-오류가-발생했다는-걸-확인하는-방법">🚨 <strong>오류가 발생했다는 걸 확인하는 방법</strong></h4>
</blockquote>
<p>TCP를 사용하는 송수신 측이 오류를 파악하는 방법은 크게 두 가지로 나누어진다.</p>
<blockquote>
</blockquote>
<p>수신 측이 송신 측에게 명시적으로 <code>NACK</code>(부정응답)을 보내는 방법, 그리고 송신 측에게 ACK(긍정응답)가 오지 않거나, 중복된 <code>ACK</code>가 계속해서 오면 오류가 발생했다고 추정하는 방법이다.</p>
<blockquote>
</blockquote>
<p>간단히 생각해 보면 NACK을 사용하는 게 훨씬 명확하고 간단할 것 같지만, NACK을 사용하게 되면 수신 측이 상대방에게 ACK을 보낼지 NACK을 보낼지 선택해야 하는 로직이 추가적으로 필요하기 때문에, 일반적으로는 ACK만을 사용해 오류를 추정한다.</p>
<h2 id="21-stop-and-wait">2.1. Stop and Wait</h2>
<p>가장 기본적인 오류 제어 방식이다. 송신 측은 데이터를 하나 보낸 뒤, ACK를 기다린다. 만약 일정 시간 안에 ACK가 오지 않으면, 송신 측은 해당 데이터를 다시 보낸다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/9f79a78b-75d3-4074-8fc9-be3aeecb85ff/image.png" alt=""></p>
<p>흐름 제어의 Stop and Wait 때 한 번 살펴본 방식인데, 이 방식만으로 흐름 제어와 오류 제어가 동시에 가능하다. 그러나 위에서 살펴본 슬라이딩 윈도우를 사용하여 흐름 제어를 하는 경우에는 윈도우 안에 있는 데이터를 연속적으로 보내야 하기 때문에, 오류 제어에 Stop and Wait를 사용해 버리면 슬라이딩 윈도우를 사용하는 이점을 잃어버린다.</p>
<p>그런 이유로 일반적으로 이런 단순한 방법보다 조금 더 효율적이도 똑똑한 ARQ를 사용한다.</p>
<h2 id="22-go-back-n">2.2. Go Back N</h2>
<p>더 발전된 방식이 Go Back N ARQ이다. 송신 측은 여러 데이터를 연속적으로 보낼 수 있는데, 만약 <strong>중간에 하나의 패킷이 손실되면 그 이후에 보낸 모든 데이터를 다시 보내는 방식</strong>이다.</p>
<p>e.g. 1, 2, 3, 4번 데이터를 보냈는데, 4번 데이터가 손실되었다면, 4번부터 그 이후 모든 데이터를 재전송한다. </p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/2556761a-4ec9-47c7-a98e-d03071b3c943/image.png" alt=""></p>
<p>이미 성공적으로 전송된 데이터까지 다시 보내기 때문에 조금 비효율적이긴 하다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/f73ace3b-a33c-4010-9aa1-8bd60481c1d0/image.png" alt=""></p>
<h2 id="23-selective-repeat">2.3. Selective Repeat</h2>
<p>그래서 나온 방식이 Selective Repeat ARQ이다. Go Back N과 달리 <strong>손실된 데이터만 다시 보내는 방식</strong>이다. </p>
<p>e.g. 1, 2, 3, 4번 중 3번 데이터만 손실되었다면, 3번 데이터만 다시 보내면 된다.</p>
<p>하지만 이 방식은 수신 측에서 데이터를 정렬할 별도의 버퍼가 필요하고, 데이터가 순차적으로 오지 않을 수 있기 때문에 약간의 복잡성이 추가된다.</p>
<h1 id="3-혼잡-제어congestion-control">3. 혼잡 제어(Congestion Control)</h1>
<p><strong>혼잡 제어</strong>는 <strong>네트워크에서 혼잡 상태를 파악하고 이를 해결하기 위해 데이터 전송을 조절하는 기법</strong>이다. 네트워크는 워낙 광대하고 블랙박스 같은 특성을 갖고 있어서, 어디에서 어떤 이유로 전송이 느려지는지 정확하게 파악하기 어렵다.</p>
<p>하지만 데이터 전송이 느려지는 현상 자체는 송신 측에서 쉽게 감지할 수 있다. 예를 들어, 데이터를 보냈는데 상대방으로부터 응답이 늦게 오거나 아예 안 오면 문제가 발생했다는 걸 알 수 있듯 말이다.</p>
<p>이때 흐름 제어나 오류 제어 기법만 사용하면 재전송이 계속 반복될 수밖에 없다. 한두 번의 재전송은 큰 문제가 아니겠지만, 네트워크가 여러 사람들이 함께 사용하는 공간이기 때문에, 이런 재전송이 여러 곳에서 발생하면 네트워크가 심각하게 혼잡해질 수 있다. 이것을 <strong>혼잡 붕괴</strong>라고 한다.</p>
<p>따라서 네트워크의 혼잡이 감지되면 최악의 상황을 피하기 위해 송신 측은 <strong>혼잡 윈도우(CWND)</strong>의 크기를 줄여 데이터를 덜 보내는 방식으로 대응한다. 이 과정이 바로 혼잡 제어다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/dc7f4ec8-c894-457b-ab40-f8d961fd184f/image.png" alt=""></p>
<blockquote>
<h4 id="🪟-혼잡-윈도우congestion-window-cwnd">🪟 <strong>혼잡 윈도우(Congestion Window, CWND)</strong></h4>
</blockquote>
<p>혼잡 제어에서 중요한 개념인 혼잡 윈도우는 <strong>송신 측이 네트워크의 혼잡 상태를 고려해 정하는 윈도우 크기</strong>를 의미한다. 송신 측은 데이터를 보낼 때 수신 측에서 보내준 수신 윈도우(RWND)와 자신이 결정한 혼잡 윈도우 중 더 작은 값을 사용한다. 즉, 송신 윈도우 크기가 이 두 값에 의해 결정된다.</p>
<blockquote>
</blockquote>
<p>혼잡 윈도우는 네트워크 상황을 반영해 계속 변화하는데, 여기서 조정하는 건 송신 윈도우가 아니라 송신 측의 혼잡 윈도우다.</p>
<h2 id="31-혼잡-회피-방법">3.1. 혼잡 회피 방법</h2>
<p>TCP 혼잡 제어 정책들은 혼잡 상태를 감지하는 방법과 혼잡 윈도우 크기를 조절하는 방식을 점차 발전시켜 왔다. 그 기본은 <strong>AIMD</strong>와 <strong>Slow Start</strong>라는 두 가지 기법을 상황에 맞게 조합해 사용하는 방식이다.</p>
<h3 id="311-aimdadditive-increasemultiplicative-decrease">3.1.1. AIMD(Additive Increase/Multiplicative Decrease)</h3>
<p>AIMD는 패킷을 처음 보낼 때 하나씩 보내고, <strong>문제가 발생하지 않으면 전송 속도를 서서히 증가시키는 방식</strong>이다. 전송 속도는 1씩 추가적으로 늘어나고(Additive Increase), 만약 패킷 전송에 실패하면 전송 속도를 절반으로 줄인다.(Multiplicative Decrease) 이렇게 하면 전송 속도가 네트워크 상태에 따라 서서히 안정되는 균형점에 도달한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/ae5aa671-ce28-449d-9711-b3461b965bcd/image.png" alt=""></p>
<p>이 방식은 아주 간단해 보이지만, 네트워크 자원을 공평하게 분배하는 데 효과적이다. </p>
<p>예를 들어, 여러 사용자가 이미 네트워크를 이용하고 있는 상황에서 새로 들어온 사용자가 있다면, 기존 사용자의 윈도우 크기가 더 클 것이다. 하지만 혼잡이 발생하면 윈도우 크기가 큰 사용자부터 데이터를 더 많이 보내게 되므로 손실 확률도 높아진다. </p>
<p>이런 상황이라면 네트워크에 일찍 참여해 이미 혼잡 윈도우가 큰 사람이 자신의 윈도우 크기를 줄여서 혼잡 상황을 해결하려 할 것이고, 이때 남은 대역폭을 활용하여 나중에 들어온 사람들이 자신의 혼잡 윈도우 크기를 키울 수 있는 것이다. 그런 이유로 시간이 흐르면 네트워크에 참여한 순서와 관계 없이 모든 호스트들의 윈도우 크기가 평행 상태로 수렴하게 된다.</p>
<p>그러나 AIMD의 문제점은 네트워크 대역이 펑펑 남아도는 상황에도 윈도우 크기를 너무 조금씩 늘리면서 접근한다는 것이다. 때문에 AIMD 방식은 <strong>네트워크의 모든 대역을 활용하여 제대로 된 속도로 통신하기까지 시간이 걸린다.</strong></p>
<h3 id="312-slow-start느린-시작">3.1.2. Slow Start(느린 시작)</h3>
<p>Slow Start는 AIMD의 단점을 보완한 방식이다. AIMD는 전송 속도를 1씩 천천히 올리는 반면, Slow Start는 처음에 패킷을 하나 보내고, ACK를 받을 때마다 <strong>윈도우 크기를 지수적으로(2배씩) 늘려나간다.</strong> 혼잡이 감지되면 윈도우 크기를 1로 다시 줄이고, 그 후에는 혼잡이 발생할 때까지 빠르게 늘리다가 일정 부분에서부터는 천천히 1씩 증가시킨다.</p>
<p>이 방식 덕분에 <strong>네트워크 용량에 대한 정보를 빠르게 파악</strong>할 수 있고, 어느 정도 네트워크 수용량을 예측한 후 혼잡이 발생하기 전까지는 전송 속도를 계속해서 빠르게 늘릴 수 있다.</p>
<h2 id="32-혼잡-제어-정책">3.2. 혼잡 제어 정책</h2>
<p>TCP에서 혼잡 제어는 네트워크의 혼잡 상태를 감지하고 그에 맞춰 데이터 전송을 조절하는 방법이다. 주요 기법으로는 <strong>Tahoe</strong>와 <strong>Reno</strong>가 있으며, 이 두 가지 기법은 기본적으로 네트워크가 혼잡하다고 느껴졌을 때 윈도우 크기를 줄이거나, 증가를 멈추고 혼잡을 회피하는 방식으로 동작한다.</p>
<blockquote>
<h4 id="🚨-혼잡-감지-기준">🚨 <strong>혼잡 감지 기준</strong></h4>
</blockquote>
<ul>
<li><strong>Timeout</strong>: 송신 측이 데이터를 보내고 나서 응답을 받지 못할 때 혼잡 상태라고 간주한다.</li>
<li><strong>3 <code>ACK</code> Duplicated</strong>: 수신 측이 정상적으로 데이터를 받지 못해 같은 승인 번호(ACK)를 세 번 이상 보낼 경우, 송신 측은 네트워크에 문제가 있다고 판단한다.<blockquote>
</blockquote>
이 두 가지 상황이 발생하면 송신 측은 혼잡이 발생했다고 보고 윈도우 크기를 줄인다.</li>
</ul>
<h3 id="321-tahoe">3.2.1. Tahoe</h3>
<p>Slow Start로 시작해 윈도우 크기를 지수적으로 빠르게 증가시킨다.</p>
<p>혼잡 상황이 발생하면, 윈도우 크기를 1로 줄이고 다시 Slow Start를 시작한다. 이때 ssthresh(Slow Start Threshold) 값을 윈도우 크기의 절반으로 설정하고, 이후 혼잡 제어는 AIMD 방식으로 진행된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/ee90846a-fdd9-41ea-a9da-240da986b4f8/image.png" alt=""></p>
<p>이는 혼잡이 발생했을 때 윈도우 크기를 1로 줄이는 과정이 비효율적일 수 있다.</p>
<h3 id="322-reno">3.2.2. Reno</h3>
<p>Reno는 Tahoe의 문제점을 개선한 방식으로, Slow Start로 시작하여 임계점(ssthresh)을 넘으면 AIMD 방식으로 전환된다. </p>
<ul>
<li><strong>3 <code>ACK</code> Duplicated 발생 시</strong>: Tahoe와 달리, 윈도우 크기를 1로 줄이지 않고 반으로만 줄이고 다시 합 증가 방식으로 윈도우 크기를 늘린다. 이를 통해 빠른 회복(Fast Recovery)가 가능하다.</li>
<li><strong>Timeout 발생 시</strong>: Tahoe처럼 윈도우 크기를 1로 줄이고 Slow Start로 다시 시작한다.</li>
</ul>
<p>즉, Reno는 <code>ACK</code> 중복과 타임아웃을 구분하여 더 유연하게 대처한다. <code>ACK</code> 중복은 비교적 작은 혼잡 상황으로 간주해 회복 과정을 빠르게 처리하고, 타임아웃은 더 심각한 문제로 보고 더 강하게 대처하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/8f19804f-9f2f-4221-996a-93dcd7cd17c7/image.png" alt=""></p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://evan-moon.github.io/2019/11/22/tcp-flow-control-error-control/">패킷의 흐름과 오류를 제어하는 TCP</a></li>
<li><a href="https://evan-moon.github.io/2019/11/26/tcp-congestion-control/">사이 좋게 네트워크를 나눠 쓰는 방법, TCP의 혼잡 제어</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] TCP 3 & 4 way handshake]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCP-3-4-way-handshake</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCP-3-4-way-handshake</guid>
            <pubDate>Wed, 13 Nov 2024 01:26:39 GMT</pubDate>
            <description><![CDATA[<h1 id="1-prerequisite">1. Prerequisite</h1>
<p>TCP의 3 &amp; 4 Way Handshake에 대해 알아보기 전에, 우선 아래의 내용을 살펴보자.</p>
<h2 id="11-transport-layer">1.1. <a href="https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7%EA%B3%84%EC%B8%B5">Transport Layer</a></h2>
<p>OSI 7계층에서 Transport Layer에는 양 끝단(End to End)의 사용자들이 신뢰성 있는 데이터를 주고 받을 수 있도록 해주어, 상위 계층들이 데이터 전달의 유효성이나 효율성을 생각하지 않도록 해준다.</p>
<p>전송 계층은 인터넷의 기반인 TCP/IP 모델과 일반적인 네트워크 모델인 개방형 시스템 간 상호 접속(Open Systems Interconnection, OSI) 모두 포함하고 있다.</p>
<p>전송 프로토콜 중 잘 알려진 것이 바로 TCP와 UDP이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a9a964b8-2e4e-4fee-b16d-aeb18c06830f/image.png" alt=""></p>
<ul>
<li>Transport Layer: Application 프로세스들 간의 논리적인 통신을 제공한다. 즉, 서로 다른 컴퓨터에서 실행되는 애플리케이션 프로세스들이 데이터를 주고받을 수 있도록 지원한다.</li>
<li>Network Layer: 호스트 간의 논리적인 통신을 제공한다. 네트워크 계층은 패킷을 목적지까지 전송하는 역할을 하며, 이를 위해 IP 주소를 사용해 데이터를 올바른 경로로 전달한다. 네트워크 계층은 각 호스트(컴퓨터 또는 장치) 간의 통신 경로를 결정하고, 패킷이 목적지까지 도달할 수 있도록 라우팅을 처리한다.</li>
</ul>
<h2 id="12-tcp--udp">1.2. TCP &amp; UDP</h2>
<p>TCP에서는 Transport Layer에서 주고 받는 데이터 단위를 <strong>세그먼트(Segment)</strong>라고 한다. 반면, UDP에서는 데이터를 <strong>데이터그램(Datagram)</strong>이라고 부른다. 데이터그램은 비연결형 서비스에서 사용되는 용어로, UDP가 데이터를 패킷으로 처리하는 방식을 설명하는 데 사용된다.</p>
<blockquote>
<h4 id="🙋🏻♀️-왜-굳이-두-단어를-구분해야-하죠">🙋🏻‍♀️ <strong>왜 굳이 두 단어를 구분해야 하죠?</strong></h4>
</blockquote>
<p>TCP는 연결 지향적이며, 데이터의 ‘정확한 전송’을 보장하기 위한 여러 제어 기능이 포함되어 있다. 이러한 특징을 반영하기 위해 각 세그먼트에는 데이터 외에도 제어 정보가 포함된다.</p>
<blockquote>
</blockquote>
<p>반면 데이터그램은 독립적이고 신뢰성 없이 전송되는 데이터 단위를 가리킨다. 이때문에 데이터그램에는 데이터 전송을 간단하고 빠르게 처리하기 위해 필수적인 최소한의 제어 정보만 포함시킨다.</p>
<blockquote>
</blockquote>
<p>즉, TCP와 UDP 모두 패킷에 정보가 담긴다는 점은 동일하지만, 헤더의 구조와 내용이 다르기 때문에 다른 단어를 사용하는 것이다.</p>
<h3 id="121-tcptransmission-control-protocol">1.2.1. TCP(Transmission Control Protocol)</h3>
<p><strong>TCP</strong>는 인터넷 상에서 데이터를 안전하게 전달하기 위해 IP와 함께 사용하는 프로토콜이다. TCP는 다음과 같은 특징을 가지고 있다.</p>
<ul>
<li><strong>연결지향적(Connected-Oriented)</strong>: 데이터를 보내기 전에 3-Way Handshake를 통해 연결을 설정하고, 이후 데이터를 주고받는다. 데이터를 주고받는 동안에도 지속적으로 연결 상태를 유지하며, 데이터 전송이 끝난 후에는 4-Way Handshake를 통해 연결을 종료한다.</li>
<li><strong>신뢰성 보장</strong>: TCP는 데이터를 신뢰성 있게 전송하는 것을 보장한다. 데이터가 손실되거나 손상되면 재전송하고, 데이터가 올바른 순서로 도착하도록 관리한다. 수신된 데이터의 확인(ACK) 응답을 통해 데이터 전송이 제대로 이루어졌는지 확인한다.</li>
<li><strong>흐름 제어(Flow Control)</strong>: 송신자와 수신자가 서로 다른 속도로 데이터를 처리할 수 있는 상황을 대비하여, 송신자가 데이터를 너무 빠르게 보내는 것을 방지한다. 이를 통해 수신자의 버퍼가 넘치지 않도록 조절한다.</li>
<li><strong>혼잡 제어(Congestion Control)</strong>: 네트워크의 혼잡 상태를 감지하고, 데이터 전송 속도를 동적으로 조절하여 네트워크 트래픽을 관리한다. 이를 통해 네트워크 혼잡 시 성능 저하를 방지한다.</li>
<li><strong>속도</strong>: TCP는 여러 보조 기능을 통해 신뢰성을 보장하기 때문에 UDP보다 속도가 느릴 수 있다. 하지만 이 속도 차이는 매우 미세하기 때문에 대부분의 사용자가 체감하기 어렵다.</li>
<li>파일 전송, 웹 브라우징, 이메일, 데이터베이스 통신 등 신뢰성이 중요한 상황에서 주로 사용된다.</li>
</ul>
<h3 id="122-udpuser-datagram-protocol">1.2.2. UDP(User Datagram Protocol)</h3>
<p><strong>UDP</strong>는 비연결형 프로토콜로, 데이터 전송 전에 연결을 설정하지 않고 빠르게 데이터를 전송하는 데 중점을 둔다. UDP는 다음과 같은 특징을 가지고 있다.</p>
<ul>
<li><strong>비연결형(Connectionless)</strong>: UDP는 데이터를 전송하기 전에 연결 설정을 하지 않으며, 데이터를 송신하면 수신자가 받는지 여부를 확인하지 않는다. 이는 TCP의 3-Way Handshake와 같은 과정이 없다는 것을 의미한다.</li>
<li><strong>독립적인 패킷 전송</strong>: 각 패킷은 독립적으로 전송되며, 서로 다른 경로로 전송될 수 있다. 따라서 UDP는 데이터의 순서 보장을 하지 않으며, 손실된 패킷에 대한 재전송도 이루어지지 않는다.</li>
<li><strong>빠른 전송 속도</strong>: UDP는 연결 설정 및 확인 과정이 없기 때문에 빠른 전송 속도를 제공한다. 또한, 흐름 제어와 혼잡 제어가 없어 네트워크 부하가 적다.</li>
<li>스트리밍, 실시간 비디오/음성 통화, 온라인 게임 등 연속성이 중요하고 약간의 데이터 손실이 큰 문제가 되지 않는 경우에 주로 사용된다.</li>
</ul>
<p>TCP와 UDP는 각각 별도의 포트 주소 공간을 관리하므로, 같은 포트 번호를 사용해도 서로 독립적인 포트로 동작한다.</p>
<p>또한 클라이언트 프로그램이 동시에 여러 서버와 연결을 맺을 때, 같은 프로토콜을 사용하는 경우라도 각 연결마다 다른 포트 번호를 사용하여 여러 연결을 처리한다.</p>
<table>
<thead>
<tr>
<th>프로토콜 종류</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody><tr>
<td>연결 방식</td>
<td>연결형 서비스</td>
<td>비연결형 서비스</td>
</tr>
<tr>
<td>패킷 교환 방식</td>
<td>가상 회선 방식</td>
<td>데이터그램 방식</td>
</tr>
<tr>
<td>전송 순서</td>
<td>전송 순서 보장</td>
<td>전송 순서가 바뀔 수 있음</td>
</tr>
<tr>
<td>수신 여부 확인</td>
<td>확인 o</td>
<td>확인 x</td>
</tr>
<tr>
<td>통신 방식</td>
<td>1:1 통신</td>
<td>1:1 or 1:N or N:N</td>
</tr>
<tr>
<td>신뢰성</td>
<td>높다</td>
<td>낮다</td>
</tr>
<tr>
<td>속도</td>
<td>느리다</td>
<td>빠르다</td>
</tr>
</tbody></table>
<h2 id="13-포트port-상태-정보">1.3. 포트(Port) 상태 정보</h2>
<ul>
<li><strong>CLOSED</strong>: 포트가 닫혀 있는 상태로, 어떤 연결 요청도 받지 않는다.</li>
<li><strong>LISTEN</strong>: 서버의 포트가 열린 상태로 연결 요청을 기다리는 상태이다. 이때 서버는 클라이언트로부터 들어오는 연결 요청을 받을 준비가 된 상태이다.</li>
<li><strong>SYN_RCV</strong>: 서버가 클라이언트의 SYN 요청을 받고, 이에 대한 응답으로 SYN-ACK 패킷을 보낸 상태이다. 즉, 서버는 클라이언트로부터 받은 연결 요청에 대해 응답을 보내고, 연결이 완료되기를 기다리고 있다.</li>
<li><strong>ESTABLISHED</strong>: 클라이언트와 서버 간에 연결이 성립된 상태. 이 상태에서는 데이터를 주고받을 수 있으며, 연결이 정상적으로 작동하고 있는 상태이다.</li>
</ul>
<h2 id="14-플래그-정보">1.4. 플래그 정보</h2>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a07788ee-111e-4fb8-b477-672cde2f29e9/image.png" alt=""></p>
<p>TCP 헤더에는 6개의 CONTROL BIT(플래그 비트)가 있으며, 각 비트는 특정 기능을 수행하는 패킷임을 나타낸다. 플래그 비트는 1bit의 정보를 가지며, 1이면 해당 기능을 수행하는 패킷이다.</p>
<blockquote>
<h4 id="📌-sequence-number">📌 <strong>Sequence Number</strong></h4>
</blockquote>
<p>TCP에서는 각 패킷이 전송될 때마다 Sequence Number(시퀀스 번호)가 부여된다. 이 번호는 패킷이 전송된 순서를 나타내고, 데이터가 손실 없이, 정확한 순서로 도착하도록 관리된다.</p>
<h3 id="141-synsynchronize-sequence-number--000010">1.4.1. SYN(Synchronize Sequence Number) / 000010</h3>
<ul>
<li>SYN 플래그는 연결 설정에 사용된다. 클라이언트가 서버에 연결 요청을 보낼 때 사용되며, 초기 Sequence Number를 설정하여 세션을 시작한다.</li>
<li>주로 3-Way Handshake의 첫 번째 단계에서 사용되며, 클라이언트와 서버 간에 초기 연결을 설정하는 역할을 한다.</li>
</ul>
<h3 id="142-ackacknowledgement--010000">1.4.2. ACK(Acknowledgement) / 010000</h3>
<ul>
<li>ACK 플래그는 응답 확인을 나타낸다. 클라이언트나 서버가 패킷을 정상적으로 수신했음을 확인하고, 이에 대한 응답을 보낼 때 사용된다.</li>
<li>ACK Number 필드는 수신자가 다음에 받고자 하는 데이터의 Sequence Number를 의미한다.<ul>
<li>e.g. 클라이언트 → 서버로 Sequence Number가 1000인 데이터 패킷을 보낸다.</li>
<li>서버가 이 패킷을 받으면, “Sequence Number 1000에서 시작하는 데이터를 잘 받았다.”는 의미로 ACK Number를 1001로 설정하여 클라이언트에게 응답한다.</li>
<li>ACK Number 1001은 “이제 1001번부터 시작하는 데이터를 보내달라”는 의미이다.</li>
<li>클라이언트는 ACK Number를 보고, 그다음 데이터를 1001번 시퀀스 번호부터 전송한다.</li>
</ul>
</li>
<li>연결이 설정된 후 모든 패킷에 이 ACK 플래그가 설정된다. 데이터 전송 과정에서 거의 모든 패킷에 사용된다고 볼 수 있다.</li>
</ul>
<h3 id="143-finfinish--000001">1.4.3. FIN(Finish) / 000001</h3>
<ul>
<li>FIN 플래그는 연결을 종료할 때 사용된다. 더 이상 전송할 데이터가 없다는 것을 알리기 위해 클라이언트 또는 서버가 FIN 패킷을 전송한다.</li>
<li>주로 4-Way Handshake 과정에서 연결 종료 요청을 나타내며, 양측 모두 이 플래그를 사용하여 연결을 종료한다.</li>
</ul>
<h1 id="2-3-way-handshake">2. 3-Way Handshake</h1>
<p><strong>3-Way Handshake</strong>는 TCP 연결을 설정하는 과정으로, 클라이언트와 서버가 데이터를 주고받기 전에 신뢰성 있는 연결을 설정하는 데 사용된다. 이 과정은 양방향 통신이 가능하도록 준비하기 위해 수행되며, 다음 세 단계로 이루어진다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/572db04a-cd1b-4ab4-a8b6-7095fcc9b99d/image.png" alt=""></p>
<h2 id="21-클라이언트-→-서버--syn">2.1. 클라이언트 → 서버 : SYN</h2>
<ul>
<li>클라이언트가 서버에 SYN 패킷을 보내 연결을 요청한다.</li>
<li>이때 SYN 플래그가 설정된 패킷이 전송되며, 이 패킷에는 랜덤한 초기 Sequence Number가 포함되어 있다.</li>
<li>클라이언트는 서버에게 “너랑 연결을 맺고 싶다”라는 의사를 전한다.</li>
<li>이 상태에서 서버는 <code>LISTEN</code> 상태에 있을 수 있다. 즉, 서버는 클라이언트와의 연결 요청에 귀기울이고 있는 상태이다.</li>
</ul>
<h2 id="22-서버-→-클라이언트--syn-ack">2.2. 서버 → 클라이언트 : SYN-ACK</h2>
<ul>
<li>서버는 클라이언트의 연결 요청을 수락하고, SYN-ACK 패킷을 클라이언트로 보낸다.</li>
<li>서버는 클라이언트가 보낸 SYN 패킷을 확인하고, 자신도 연결할 준비가 되었음을 나타내기 위해 SYN 플래그와 함께 ACK 플래그를 설정한 패킷을 보낸다.</li>
<li>ACK Number는 클라이언트가 보낸 Sequence Number + 1이다. 이는 클라이언트가 보낸 요청을 받았음을 확인하는 숫자이다.</li>
<li>이 시점에서 서버는 <code>SYN_RCV</code> 상태이다.</li>
</ul>
<h2 id="23-클라이언트-→-서버--ack">2.3. 클라이언트 → 서버 : ACK</h2>
<ul>
<li>클라이언트는 서버가 보낸 SYN-ACK 패킷을 받으면, 다시 서버에게 ACK 패킷을 보낸다.</li>
<li>이때 ACK 플래그만 설정된 패킷을 보내며, 이는 연결이 성립되었음을 확인하는 응답이다.</li>
<li>클라이언트와 서버가 모두 <code>ESTABLISHED</code> 상태로 전환되며, 이제 양측은 데이터를 주고받을 준비가 완료된 상태이다.</li>
</ul>
<p>이 3단계 과정을 통해 양방향에서 데이터를 전송할 준비가 완료된 Full-duplex 통신이 형성된다. <strong>Full-duplex 통신</strong>은 양방향으로  동시에 데이터를 전송할 수 있는 상태를 의미한다.</p>
<h1 id="3-4-way-handshake">3. 4-Way Handshake</h1>
<p><strong>4-Way Handshake</strong>는 TCP 연결을 종료하는 절차로, 클라이언트와 서버가 각각의 데이터 전송이 끝났음을 명확히 확인하는 과정을 거친다. 이때 양쪽 모두 FIN 플래그를 사용해 더 이상 보낼 데이터가 없음을 알리고, 마지막으로 ACK 패킷을 주고받아 연결을 종료한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/113de6c7-eb6d-4a70-be46-42119f931d5b/image.png" alt=""></p>
<h2 id="31-클라이언트-→-서버--finack">3.1. 클라이언트 → 서버 : FIN(+ACK)</h2>
<ul>
<li>클라이언트가 더 이상 보낼 데이터가 없을 때, 연결을 종료하려고 FIN 플래그가 설정된 패킷을 서버에 보낸다.</li>
<li>이때 클라이언트는 <code>FIN_WAIT_1</code> 상태로 들어가며, 서버로부터 ACK 응답을 기다린다.</li>
<li>여기서 중요한 점은, FIN 패킷은 데이터를 보내는 일방적인 요청이지만, 연결 자체를 완전히 종료하지는 않는다. 아직 서버가 데이터를 전송할 수도 있기 때문이다.</li>
</ul>
<h2 id="32-서버-→-클라이언트--ack">3.2. 서버 → 클라이언트 : ACK</h2>
<ul>
<li>서버는 클라이언트가 보낸 FIN 패킷을 받으면, 확인 응답(ACK 패킷)을 클라이언트에 보낸다.</li>
<li>서버는 <code>CLOSE_WAIT</code> 상태로 전환되며, 아직 처리 중인 데이터가 있다면 이를 처리하고 나서 연결을 완전히 종료할 준비를 한다.</li>
<li>클라이언트는 서버로부터 ACK 패킷을 받으면 <code>FIN_WAIT_2</code> 상태로 전환되며, 서버가 종료할 때까지 기다린다.</li>
</ul>
<h2 id="33-서버-→-클라이언트--fin">3.3. 서버 → 클라이언트 : FIN</h2>
<ul>
<li>서버가 남은 데이터를 모두 전송하고 나면, 연결을 종료하기 위해 FIN 패킷을 클라이언트에게 보낸다.</li>
<li>서버는 이때 <code>LAST_ACK</code> 상태로 들어가며, 클라이언트로부터 최종 ACK를 받기 전까지 기다린다.</li>
</ul>
<h2 id="34-클라이언트-→-서버--ack">3.4. 클라이언트 → 서버 : ACK</h2>
<ul>
<li>클라이언트는 서버로부터 FIN 패킷을 받은 후, 마지막 ACK 패킷을 서버에 보내 서버의 종료 요청을 확인한다.</li>
<li>클라이언트는 이때 <code>TIME_WAIT</code> 상태로 돌아가며, 네트워크 상에 남아있을지 모르는 패킷(e.g. 마지막 ACK 패킷이 손실된 경우)을 처리하기 위해 일정 시간 대기한다.</li>
<li>서버는 클라이언트로부터 ACK를 받으면 완전히 연결을 종료하고 <code>CLOSED</code> 상태로 전환된다.</li>
</ul>
<blockquote>
<h4 id="⏱️-time_wait-상태">⏱️ <strong>TIME_WAIT 상태</strong></h4>
</blockquote>
<ul>
<li>클라이언트는 <code>TIME_WAIT</code> 상태에서 남아있을 수 있는 잘못된 패킷을 처리하기 위해 잠시 대기한다. 네트워크에 혹여나 남아있을 수 있는 지연된 패킷을 처리하고, 만약 서버가 FIN 패킷을 다시 보내는 경우를 처리하기 위함이다.</li>
<li><code>TIME_WAIT</code> 시간이 끝나면 클라이언트도 <code>CLOSED</code> 상태로 전환되고, 연결이 완전히 종료된다.</li>
</ul>
<blockquote>
<h4 id="🔎-termination-종류">🔎 <strong>Termination 종류</strong></h4>
</blockquote>
<ul>
<li><strong>Graceful Connection Release(정상적인 연결 해제)</strong><ul>
<li>양쪽 모두가 연결을 안전하게 닫기 위해 서로 FIN과 ACK를 주고받으며 연결을 종료하는 방식이다. 4-Way Handshake가 바로 이 정상적인 연결 해제에 해당된다.</li>
<li>양쪽이 서로 연결 종료를 동의하며, 모든 데이터 전송이 끝날 때까지 연결을 유지한다.</li>
</ul>
</li>
<li><strong>Abrupt Connection Release(갑작스러운 연결 해제)</strong><ul>
<li>갑자기 한쪽이 연결을 강제로 종료하는 방식이다.</li>
<li>이때는 RST(Reset) 세그먼트를 사용해 연결을 즉시 끊는다.</li>
<li>e.g. 비정상적인 상황(리소스 부족, 비정상적인 패킷 수신 등)에서 한쪽이 연결을 강제로 닫고 싶을 때 RST 패킷을 보내 연결을 종료한다.</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="🔎-half-close-기법">🔎 <strong>Half-Close 기법</strong></h4>
</blockquote>
<p>Half-Close는 한쪽에서 먼저 연결을 닫았지만, 다른 쪽은 계속해서 데이터를 전송할 수 있는 상태를 말한다.</p>
<blockquote>
</blockquote>
<ul>
<li>예를 들어 클라이언트가 먼저 FIN 패킷을 보내면서 자신의 데이터를 모두 보냈음을 알리지만, 서버는 남은 데이터를 전송할 수 있는 상태를 유지한다.</li>
<li>FIN 패킷에 ACK가 포함되어 있는 이유는 <em>“일단 데이터를 보내는 건 끝났지만, 서버가 보낼 데이터는 계속 받을 준비가 되어있다.”</em>는 의미이다.</li>
<li>서버가 데이터를 모두 전송하고 나면, 다시 FIN 패킷을 보내 연결을 완전히 종료한다.</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://velog.io/@averycode/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCPUDP%EC%99%80-3-Way-Handshake4-Way-Handshake">[네트워크] TCP/UDP와 3 -Way Handshake &amp; 4 -Way Handshake</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] OSI 7계층]]></title>
            <link>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@jayy_19/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Tue, 12 Nov 2024 01:48:55 GMT</pubDate>
            <description><![CDATA[<h1 id="1-osi-7계층-모델">1. OSI 7계층 모델?</h1>
<h2 id="11-osiopen-systems-interconnection-7계층-모델">1.1. OSI(Open Systems Interconnection) 7계층 모델</h2>
<p><strong>OSI(Open Systems Interconnection) 7계층 모델</strong>은 네트워크 통신에서 발생하는 복잡한 작업을 일곱 개의 독립적인 계층으로 나누어 설명하는 개념적 모델이다. 이는 네트워크 통신을 계층화해서 쉽게 이해하고, 각 계층 간의 상호작용을 설명하는 데 중점을 둔다. 주로 이론적 교육용 모델로 사용된다.</p>
<blockquote>
</blockquote>
<p>그런데 잠깐, 우리가 실제로 배우는 네트워크 모델은 5개 계층 밖에 되지 않는데, <em>왜 7계층일까?</em></p>
<h2 id="12-tcpip-모델">1.2. TCP/IP 모델</h2>
<p>그 이유는 오늘날 네트워크 통신에서 OSI 7계층 모델보다 TCP/IP 모델이 더 널리 사용되기 때문이다. </p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/00c84c04-15da-4af9-9ae5-bb73de1d3385/image.png" alt=""></p>
<p><strong>TCP/IP 모델</strong>은 실제 구현된 인터넷 프로토콜을 바탕으로 한 네트워크 모델이다. OSI 7계층과 다르게 5개의 계층으로 구성되어 있고, 이는 실제로 인터넷과 대부분의 네트워크 시스템에서 사용되는 모델이다.</p>
<p>TCP/IP 모델은 OSI 모델의 세션(Session)과 표현(Presentation) 계층을 응용 계층(Application Layer)에 통합하여, 계층 수가 5개로 줄어들었다. 이는 실제로 네트워크 통신에서 세션 관리나 데이터 변환 등의 기능이 주로 응용 계층에서 처리되기 때문이다.</p>
<p>또한 TCP/IP 모델은 네트워크에서 실제로 구현되는 프로토콜을 기반으로 만들어졌기 때문에, 효율적이고 실용적인 구조로 설계되었다. 7계층 구조가 이론적으로는 유용하지만, 네트워크 프로토콜의 구현에서는 과도한 복잡성을 피하기 위해 5계층으로 단순화된 것이다.</p>
<blockquote>
</blockquote>
<p>정리하자면, OSI 모델은 이론적으로는 우수하나 실제로 적용하기에는 복잡하고 표준화 속도가 느렸던 반면, TCP/IP 모델은 실용적이고 빠르게 구현되었기 때문에 인터넷과 네트워크의 실제 표준으로 자리 잡게 된 것이다.</p>
<p>그럼 이제 TCP/IP 모델의 각 계층에 대해 자세히 살펴 보자.</p>
<h1 id="2-physical-layer">2. Physical Layer</h1>
<p>컴퓨터가 데이터를 주고 받기 위해서는 어떻게 해야할까? 모든 파일과 프로그램은 결국 0과 1로 이루어진 데이터의 나열이다. 그렇다면, 두 대의 컴퓨터가 서로 0과 1만 주고받을 수 있다면 모든 데이터를 주고 받을 수 있을 것이다.</p>
<h2 id="21-데이터-전송의-기초-0과-1의-전송">2.1. 데이터 전송의 기초: 0과 1의 전송</h2>
<p>두 대의 컴퓨터를 전선으로 직접 연결한다고 가정해보자.</p>
<ul>
<li>1을 보낼 때: +5V의 전기 신호를 보낸다.</li>
<li>0을 보낼 때: -5V의 전기 신호를 보낸다.</li>
</ul>
<p>이 방식을 이용하면 이진 데이터, 즉 0과 1의 나열을 주고 받을 수 있다. 두 컴퓨터가 데이터를 교환할 수 있는 기본적인 환경이 마련된 것처럼 보인다.</p>
<h2 id="22-현실적인-문제-신호-왜곡">2.2. 현실적인 문제: 신호 왜곡</h2>
<p>하지만 이 간단한 방식은 실제로는 잘 동작하지 않는다. 그 이유는 전기 신호가 이상적인 조건에서만 전송되지 않기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3c5ddf92-a3a0-485a-b5c8-24bf1ac37cfa/image.png" alt=""></p>
<p>파동은 왼쪽처럼 이상적인 sin파로만 전송되지 않는다. 전송되는 환경에 따라 오른쪽과 같이 왜곡되거나 변형된다.</p>
<p>예를 들어, 어떤 전선이 5-8Hz의 전자기파만 통과시킬 수 있다면, 1Hz에서 10Hz 사이의 다양한 주파수 성분을 가진 신호는 제대로 전달되지 못하고 일부 성분만 통과하게 된다. 결과적으로 도착하는 데이터는 엉뚱한 값이 될 가능성이 높아진다.</p>
<blockquote>
<p><strong><em>왜 이런 문제가 발생할까?</em></strong></p>
</blockquote>
<p>전자기파에는 다양한 주파수 성분이 포함되어 있다. 특히 수직선과 수평선으로 급격하게 변하는 신호(이진 데이터)의 경우 매우 넓은 주파수 대역(0~무한대)을 필요로 한다. 현실에서는 무한대의 주파수 범위를 전달할 수 있는 전선은 존재하지 않기 때문에, 이러한 신호를 그대로 보내는 것은 불가능하다.</p>
<h2 id="23-해결책-아날로그-신호로-변환">2.3. 해결책: 아날로그 신호로 변환</h2>
<p>따라서 실제 통신에서는 0과 1을 직접 전송하는 대신, 아날로그 신호로 변환하여 데이터를 전송한다. sin파나 cos파 같은 아날로그 신호는 전선에서 발생하는 주파수 제한 문제를 완화할 수 있다.</p>
<ul>
<li><strong>인코딩(Encoding)</strong>: 0과 1의 이진 데이터를 아날로그 신호로 변환하여 전송한다.</li>
<li><strong>디코딩(Decoding)</strong>: 전송된 아날로그 신호를 다시 0과 1로 변환하여 데이터를 복원한다.</li>
</ul>
<h2 id="24-physical-layer란">2.4. Physical Layer란?</h2>
<p>이러한 과정을 담당하는 것이 바로 네트워크 1계층, <strong>물리 계층(Physical Layer)</strong>이다. 물리 계층은 0과 1의 나열을 아날로그 신호로 변환하여 전선이나 기타 물리적인 매체를 통해 전달하고, 다시 아날로그 신호를 0과 1로 변환하여 데이터 통신을 가능하게 하는 모듈(module)이다.</p>
<p>물리 계층은 컴퓨터가 전기 신호를 통해 데이터를 전송하고 수신하는 기본적인 통신 방법을 제공하며, 네트워크의 가장 기초적인 부분을 담당한다.</p>
<p>물리 계층 기술은 <strong>PHY 칩</strong>이라고 불리는 하드웨어 장치에 의해 구현된다. 이 칩은 전기 신호의 인코딩과 디코딩을 처리하며, 주로 네트워크 장비나 컴퓨터의 네트워크 인터페이스 카드(NIC)에 내장되어 있다. 중요한 점은 물리 계층의 기능이 하드웨어적으로 구현된다는 것이다. 즉, 물리 계층은 우리가 프로그래밍 언어로 작성한 함수가 아니라 회로로 구성되어 동작한다.</p>
<h1 id="3-data-link-layer">3. Data-Link Layer</h1>
<p>컴퓨터가 서로 데이터를 주고 받기 위해서는 단순히 물리적으로 연결되는 것만으로는 충분치 않다. 여러 대의 컴퓨터가 동시에 하나의 전선을 통해 통신하기 위해서는 추가적인 기술이 필요하다.</p>
<h2 id="31-여러-대의-컴퓨터와-통신하기">3.1. 여러 대의 컴퓨터와 통신하기</h2>
<p>A와 B가 하나의 전선으로 통신하고 있다고 가정해 보자. 그런데 A가 C와도 통신하고 싶다면 어떻게 해야할까?</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/686d6c8c-5e56-4feb-b863-facd891de7fd/image.png" alt=""></p>
<p>방법은 간단하다. A와 C 사이에도 전선을 하나 더 연결하면 된다.</p>
<p>그러나 이 방식은 컴퓨터 수가 늘어날 때마다 전선을 계속 추가해야 하는 비효율적인 방식이다. 새로운 컴퓨터와 통신할 때마다 새로운 전선과 연결 포트가 필요하기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/dddb30f6-066c-489f-996c-7d2a9cfd389d/image.jpeg" alt=""></p>
<p>이러한 비효율성을 해결하기 위해 전선 하나를 여러 대의 컴퓨터가 공유하면서도, 동시에 충돌 없이 통신할 수 있는 방법이 필요하다. 위의 그림처럼 전선 하나에 여러 대의 컴퓨터를 연결한 상황을 생각해 보자. 가운데 전선을 구겨서 상자에 넣으면 오른쪽과 같은 그림이 되는데, 이를 <strong>스위치(Switch)</strong>라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/78e5db9d-c7ad-4025-a349-1ed731ea8971/image.jpeg" alt=""></p>
<p>위 그림과 같이 서로 다른 네트워크가 스위치와 스위치로 연결되어 서로 다른 네트워크에 속한 컴퓨터끼리 통신이 가능하게 해주는 장비를 <strong>라우터(Router)</strong>라고 한다. (엄밀히 해당 상자는 스위치+라우터로 구성된 L3스위치이지만, 뭉뚱그려 라우터라고 하자.)</p>
<p>이렇게 전 세계의 컴퓨터들이 계층구조로 연결되어 있는 것을 <strong>인터넷</strong>이라고 한다.</p>
<h2 id="32-framing">3.2. Framing</h2>
<p>여러 대의 컴퓨터가 어떻게 통신할 수 있는지는 알겠다. 아래 그림과 같은 상황을 생각해 보자.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/7f24d33f-093f-46ff-a8e1-2556cb33fcbc/image.jpeg" alt=""></p>
<p>A에게 B, C, D가 거의 동시에 각각의 정보를 보내면, A는 <code>010111110001</code>이라는 데이터를 받게 된다.</p>
<p>A는 송신된 데이터의 시작과 끝을 명확히 알지 않으면 데이터가 꼬이게 된다. 이를 위해 <strong>프레이밍(Framing)</strong>이라는 작업이 필요하다. 송신자는 데이터를 끊어서 전송하기 위해 특정한 비트열을 데이터 앞뒤에 붙인다. 이 비트열은 데이터를 주고받는 컴퓨터들이 데이터의 구분을 명확하게 할 수 있도록 도와준다.</p>
<h2 id="33-data-link-layer란">3.3. Data-Link Layer란?</h2>
<p>Data-Link Layer는 물리적으로 같은 네트워크에 연결된 여러 대의 컴퓨터들이 데이터를 원활하게 주고받을 수 있도록 도와준다. 이 계층에서는 데이터를 전송할 때 어디에서 시작되고 끝나는지 구분해야 하며, 데이터를 여러 부분으로 나눠서 전송하거나, 데이터 전송 중 오류가 발생하지 않도록 관리하는 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/e0f3fd04-3191-41f5-b32a-015fa8b23d5f/image.jpeg" alt=""></p>
<p>Data-Link Layer의 기술은 네트워크 인터페이스 카드(NIC), 흔히 <strong>랜카드</strong>라고 불리는 장치에 구현된다. 이는 1계층의 물리적인 신호 처리를 담당하는 부분과 함께 하드웨어적으로 구현되어 있다.</p>
<h2 id="34-data-link-layer와-physical-layer의-차이점">3.4. Data-Link Layer와 Physical Layer의 차이점</h2>
<p>1계층인 Physical Layer는 단순히 전기 신호를 매개체로 하여 데이터를 전송하는 기능을 담당한다. 이 계층에서는 데이터가 어떻게 전송되는지에 대한 규칙은 없고, 그저 신호를 주고 받기만 한다.</p>
<p>반면 Data-Link Layer는 단순히 신호를 주고 받는 것을 넘어서, 데이터를 프레임 단위로 나누고 이를 통제하여 충돌이나 오류 없이 전송할 수 있도록 관리한다. 즉, 물리적인 전송이 가능한 환경에서 안정적으로 데이터를 주고 받는 과정을 처리하는 것이다.</p>
<h1 id="4-network-layer">4. Network Layer</h1>
<p>컴퓨터들이 서로 통신을 하기 위해서는 IP 주소를 알아야 한다. IP 주소는 컴퓨터가 가지는 고유한 주소로, 네트워크 상에서 데이터를 주고 받기 위해서는 상대방 컴퓨터의 IP 주소가 필수적이다. </p>
<p>예를 들어, 우리가 웹 브라우저에 <code>www.naver.com</code>을 입력하면, 이 주소는 실제 IP 주소로 변환되어 사용된다. 결국 우리는 웹사이트에 접속할 때 이미 그 사이트의 IP 주소를 사용하고 있는 셈이다.</p>
<h2 id="41-패킷packet">4.1. 패킷(Packet)</h2>
<p>네트워크 상에서 데이터는 패킷이라는 단위로 전송된다. 패킷은 단순히 데이터를 담는 그릇이 아니라, 상대방의 IP 주소와 데이터를 함께 담은 것을 의미한다. 따라서 A가 B에게 데이터를 보내려면 B의 IP 주소를 알고 있어야 하며, 그 IP 주소에 데이터를 전송하는 것이 패킷을 보내는 과정이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/20b8a446-9596-472a-8bad-1c311610f865/image.jpeg" alt=""></p>
<p>위 그림에서 A에서 B로 패킷을 보내는 과정을 생각해 보자.</p>
<p>패킷은 여러 라우터를 거쳐 이동해야 한다. 라우터는 네트워크 상에서 중간중간 위치하며, 패킷이 최종 목적지로 갈 수 있도록 경로를 찾아주는 역할을 한다.</p>
<p>예를 들어 A가 B에게 패킷을 보내기 위해 <code>가</code> 라우터를 통해야 하고, 그 다음 <code>마</code> 라우터를 거친다고 가정해 보자.
인간인 우리는 딱 그림을 보면 <code>바</code> 라우터로 가야한다는 것을 알 수 있다. 하지만 라우터는 스스로 패킷이 어디로 가야 할지 알고 있는 것이 아니라, 경로를 찾아주는 과정을 거쳐야 한다. 이 과정이 바로 <strong>라우팅(Routing)</strong>이다. </p>
<ul>
<li><strong>라우팅(Routing)</strong>: 목적지 컴퓨터의 IP 주소를 바탕으로 경로를 찾는 과정. 라우터는 자신과 연결된 네트워크를 분석하고, 목적지까지 최적 경로를 계산하여 패킷을 어느 방향으로 보낼지 결정한다.</li>
<li><strong>포워딩(Forwarding)</strong>: 라우팅을 통해 결정된 경로를 바탕으로, 다음 라우터로 패킷을 실제로 전달하는 과정. 라우터는 패킷을 받아 다음 목적지로 넘기는 역할을 수행한다.</li>
</ul>
<h2 id="42-network-layer란">4.2. Network Layer란?</h2>
<p>Network Layer는 네트워크 상에서 IP 주소를 사용하여 경로를 찾고, 그 경로를 따라 데이터를 목적지까지 전달하는 역할을 담당한다. 이는 네트워크의 복잡성과 관계없이 패킷이 목적지까지 도달할 수 있도록 해주는 중요한 계층이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/dde94ee8-60b2-4a94-b15d-da0782fc75a2/image.jpeg" alt=""></p>
<p>네트워크 계층의 기능은 소프트웨어적으로 구현되어 있으며, 이는 운영체제의 <strong>커널</strong>에서 처리된다.</p>
<h1 id="5-transport-layer">5. Transport Layer</h1>
<p>이제 네트워크 레이어 덕분에 인터넷 상의 모든 컴퓨터가 서로 통신할 수 있게 되었다. 하지만 네트워크를 통해 데이터를 전송받는 컴퓨터는 여러 프로그램(프로세스)이 동시에 실행되고 있을 수 있다.</p>
<p>예를 들어, 한 컴퓨터에서 카카오톡과 크롬이 동시에 실행 중일 수 있다. 이때 컴퓨터는 <em>어떤 데이터를 어떤 프로그램에 보내야 할지 어떻게 알 수 있을까?</em></p>
<h2 id="51-포트-번호port-number">5.1. 포트 번호(Port Number)</h2>
<p>컴퓨터에서 실행 중인 여러 프로그램들이 네트워크 상에서 데이터를 주고 받기 위해서는, 각 프로그램이 고유한 포트 번호를 가져야 한다. <strong>포트 번호</strong>는 각 프로세스가 데이터를 구분하여 받을 수 있도록 컴퓨터 내에서 서로 구별되는 정수값이다. 이를 통해 컴퓨터는 어떤 데이터가 어떤 프로세스로 가야하는지 정확히 알 수 있다.</p>
<p>이를 통해 송신자는 데이터를 보낼 때 수신자의 IP 주소뿐만 아니라, 수신자 프로그램의 포트 번호도 함께 붙여서 보내야 한다는 것을 알 수 있다. 이렇게 하면 수신자 컴퓨터는 데이터를 받은 후, 이 데이터를 정확히 어느 프로그램에 전달해야 할지 알 수 있다.</p>
<p>우리가 웹사이트에 접속할 때도 포트 번호가 사용된다. 예를 들어, 웹 브라우저에서 <code>www.naver.com</code>을 입력할 때, 실제로는 포트 번호 80이 생략되어 있다. 이 포트 번호는 HTTP 프로토콜에서 기본적으로 사용되는 포트 번호이다. 즉, 우리는 인지하고 있지 못하고 있었지만 네이버의 포트 번호를 알고 있는 것이나 마찬가지인 셈이다.</p>
<h2 id="52--transport-layer란">5.2.  Transport Layer란?</h2>
<p>Transport Layer는 네트워크 상에서 데이터를 전송할 때 IP 주소와 포트 번호를 사용하여, 데이터를 정확한 프로세스(즉, 실행 중인 프로그램)에 전달하는 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/288f6aff-75e4-444c-8d56-e61312e9052f/image.jpeg" alt=""></p>
<p>이 계층에서는 두 가지 주요 프로토콜이 많이 사용된다.</p>
<ul>
<li><strong>TCP(Transmission Control Protocol)</strong>: 데이터 전송의 신뢰성과 순서를 보장하는 프로토콜. 데이터를 보내는 도중 손실되면 재전송하고, 순서가 뒤바뀌지 않도록 관리한다.</li>
<li><strong>UDP(User Datagram Protocol)</strong>: 상대적으로 빠르게 데이터를 전송하지만 신뢰성을 보장하지는 않는 프로토콜. 실시간 스트리밍이나 게임 등에서 많이 사용된다.</li>
</ul>
<p>전송 계층은 운영체제의 <strong>커널</strong>에서 소프트웨어적으로 구현되어 있다. 운영체제는 네트워크를 통해 들어온 데이터를 포트 번호를 기준으로 적절한 프로그램에 분배한다. 이를 통해 컴퓨터는 동시에 여러 프로그램이 실행 중이더라도, 각각의 프로그램이 자신에게 할당된 데이터를 문제없이 수신할 수 있게 된다.</p>
<h1 id="6-application-layer">6. Application Layer</h1>
<h2 id="61-application-layer란">6.1. Application Layer란?</h2>
<p>Application Layer는 네트워크 통신의 최종 도착지인 프로세스 간의 데이터 교환을 다룬다. 이 계층에서는 다양한 애플리케이션 프로토콜들이 사용된다. 가장 대표적인 것이 HTTP, FTP, SMTP와 같은 프로토콜이다.</p>
<p>애플리케이션 계층에서도 다른 계층과 마찬가지로 인코딩과 디코딩 과정이 이루어진다. 예를 들어, HTTP는 웹 브라우저가 웹 서버와 데이터를 주고받을 때 사용하는 프로토콜로, 인코딩과 디코딩을 통해 요청과 응답을 주고 받는다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/d572706d-dd61-4442-a394-bc5c6500b2b6/image.jpeg" alt=""></p>
<h2 id="62-tcpip-소켓-프로그래밍">6.2. TCP/IP 소켓 프로그래밍</h2>
<p><strong>TCP/IP 소켓 프로그래밍</strong>은 네트워크 프로그램을 작성할 때 Transport Layer에서 제공하는 API를 활용하여 프로그램 간 통신을 가능하게 한다. 이 소켓 프로그래밍을 통해 클라이언트와 서버 프로그램을 만들고, 데이터를 주고 받을 수 있다.</p>
<p>소켓 프로그래밍은 Transport Layer에서 네트워크 연결을 설정하고 데이터 전송을 담당하는 반면, Application Layer는 이 소켓을 이용해 실제로 주고 받을 데이터를 다루는 계층이다.</p>
<p>즉, 소켓 프로그래밍 자체는 Transport Layer에 속하지만, 이 소켓을 통해 주고 받는 데이터의 내용은 Application Layer에서 처리가 된다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://www.youtube.com/watch?v=1pfTxp25MA8">[10분 테코톡] 🔮 히히의 OSI 7 Layer</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 파일 시스템]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%8C%8C%EC%9D%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%8C%8C%EC%9D%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Mon, 11 Nov 2024 01:41:17 GMT</pubDate>
            <description><![CDATA[<p>컴퓨터 시스템에서 가장 중요한 자원은 CPU, 메인 메모리이고, 그 다음으로 중요한 것이 하드 디스크와 같은 보조 기억 장치이다.</p>
<p>하드 디스크에는 <strong>파일 시스템(File System)</strong>이 존재하며, 이 파일 시스템은 계층 구조로 이루어져 있다. 예를 들어, 윈도우의 루트 디렉토리 아래에 여러 폴더와 파일이 위치하는 것처럼 말이다.</p>
<p>파일이 하드 디스크에 어떻게 할당되는지 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/c943312c-8bac-4a5c-859e-625d42100afa/image.png" alt=""></p>
<p>하드 디스크는 다음과 같이 구성되어 있다.</p>
<ul>
<li><strong>플래터(Platter)</strong>: 하드 디스크의 데이터를 저장하는 원형의 디스크로, 데이터를 실제로 저장하는 물리적인 공간이다. 하드 디스크에는 여러 개의 플래터가 쌓여 있으며, 각 플래터는 양면에 데이터를 기록할 수 있다. 플래터는 자성 물질로 코딩되어 있어, 디스크 헤드가 자성을 읽거나 변경함으로써 데이터를 기록하거나 읽어온다.</li>
<li><strong>트랙(Track)</strong>: 플래터의 표면에 기록된 원형 경로로, 데이터를 저장하는 단위이다. 플래터는 수많은 동심원 모양의 트랙으로 나뉘어져 있다. 하드 디스크의 디스크 헤드가 특정 트랙 위에서 데이터를 읽거나 쓴다.</li>
<li><strong>실린더(Cylinder)</strong>: 여러 개의 플래터가 쌓여있을 때, 같은 위치에 있는 트랙들을 묶어 실린더라고 부른다. 즉, 상하로 쌓인 여러 플래터의 동일 위치에 있는 트랙들이 모여 하나의 실린더를 형성한다. 데이터를 읽어올 때, 디스크 헤드는 트랙의 위치에 맞춰 실린더 단위로 데이터를 읽어들인다.</li>
<li><strong>섹터(Sector)</strong>: 트랙은 여러 개의 작은 구획으로 나누어져 있는데, 이 각각의 구획을 섹터라고 한다. 섹터는 하드 디스크에서 데이터를 저장하고 읽어내는 최소 단위이다. 일반적으로 한 섹터는 512bytes 또는 4KB 크기의 데이터를 저장할 수 있다.</li>
</ul>
<p>섹터는 몇 개가 모여 <strong>블록(Block)</strong>을 이룬다. 하드 디스크는 블록 단위로 데이터를 읽고 쓰며, 이때문에 하드 디스크는 <strong>블록 디바이스(Block Device)</strong>라고도 불린다.</p>
<p>cf) 캐릭터 디바이스(Character Device): 글자 단위로 자료가 이동된다. (e.g. 키보드)</p>
<p>예시로 <code>test.txt</code>라는 파일에 ‘a’라는 글자를 저장한다고 가정해 보자. ‘a’는 1byte의 크기를 가지지만, 하드 디스크는 블록 단위로 데이터를 처리하기 때문에, 이 작은 파일을 저장할 때에도 최소 한 블록(일반적으로 4KB)이 할당된다. 결과적으로 1byte의 파일을 위해 4KB의 디스크 공간이 사용되며, 남은 공간은 비어있지만 사용할 수 없는 상태가 된다.</p>
<p>이 예시를 통해 디스크가 블록 단위로 데이터를 읽고 쓴다는 것을 알 수 있다. 블록 크기는 파일 시스템에 의해 결정되며, 작은 파일도 최소 블록 크기만큼의 공간을 차지한다.</p>
<p>디스크는 <strong>pool of free blocks</strong>로 구성되어 있으며, 이들은 각 파일에 할당된다. 각각의 파일에 대해 <em>free blocks를 어떻게 할당하고, 어떻게 관리할지</em> 알아보자.</p>
<h1 id="1-파일-할당">1. 파일 할당</h1>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/4902f1e2-4229-4d88-bfb7-ec05b75bacd0/image.png" alt=""></p>
<p>위 그림은 pool of free blocks를 논리적인 그림으로 나타낸 모습이다. 블록마다 인덱스 번호가 설정되어 있다. </p>
<p>파일에 블록을 할당하는 방식은 크게 연속 할당, 연결 할당, 색인 할당 세 가지로 나누어 볼 수 있다.</p>
<h2 id="11-연속-할당contiguous-allocation">1.1. 연속 할당(Contiguous Allocation)</h2>
<p>각 파일에 대해 디스크 상에 연속된 블록을 할당하는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/44dbcc87-a157-4df8-b7c5-ec6557e3f7ab/image.png" alt=""></p>
<p>예를 들어 <code>f1</code>이라는 파일이 5KB를 필요로 한다면, 디스크에서 5KB 크기의 연속된 블록을 할당해준다.</p>
<h3 id="111-특징">1.1.1. 특징</h3>
<p>연속 할당 방식은 파일을 읽을 때 디스크 헤드를 한 번만 움직이면 연속적으로 파일을 읽을 수 있어 속도가 빠르다. 옛날 60-70년대의 IBM에서 많이 사용됐던 방법이며, 동영상, 음악, VOD와 같이 연속적으로 재생하는 용량이 큰 파일에 적합하다.</p>
<p>또한 이 방식은 파일을 순서대로 읽는 <strong>순차 접근(Sequential Access)</strong>과 특정 부분을 바로 읽을 수 있는 <strong>직접 접근(Direct Access)</strong> 모두 가능하다.</p>
<blockquote>
<h4 id="🔎-디렉토리directory">🔎 <strong>디렉토리(Directory)</strong></h4>
</blockquote>
<p>디렉토리는 파일에 대한 메타데이터를 저장하는 파일 시스템의 테이블 역할을 한다. </p>
<blockquote>
</blockquote>
<p>디렉토리에는 파일의 이름, 크기, 생성 시간, 소유자와 같은 정보뿐만 아니라 시작 블록 번호와 파일의 크기도 저장된다. 연속 할당에서는 디렉토리에 파일의 시작 블록 번호와 연속된 블록의 크기만 있으면 해당 파일에 directly access할 수 있다.</p>
<blockquote>
</blockquote>
<pre><code>file name: f1
file size: 5 bytes
...
-----------------
block number: 0</code></pre><h3 id="112-단점">1.1.2. 단점</h3>
<p>이 방식은 현재 거의 사용되지 않는데, 외부 단편화가 큰 문제로 작용하기 때문이다. 파일이 삭제되면 그 공간은 hole로 남게 되고, 다양한 크기의 파일들이 생성되고 삭제되면서 빈 공간이 조각난다. 이로 인해 충분한 연속된 공간을 찾기 어려워지고, 파일을 저장하지 못하는 상황이 발생한다. 이를 해결하기 위해 <strong>compaction</strong>을 통해 빈 공간을 모으는 작업이 필요한데, 이 작업은 매우 비효율적이고 시간이 많이 소요된다. (80년대 MS-DOS에서는 이를 해결하기 위해 compaction 프로그램을 따로 돌렸었다.)</p>
<p>또한 파일 크기 예측의 어려움도 큰 단점이다. 파일이 생성될 때, 파일의 최종 크기는 미리 알 수 없다. 파일이 예상보다 커지면 기존에 할당된 공간을 넘어 확장해야 하는데, 연속된 공간이 부족하면 확장이 불가능해 파일을 다른 곳으로 옮겨야 하는 상황이 발생한다. 특히 로그 파일과 같이 크기가 계속 증가하는 파일의 경우, 초기 할당 크기를 예측하기 매우 어렵다.</p>
<p>연속 할당 방식은 이렇듯 빠른 데이터 접근을 제공하지만, 치명적인 단점들로 인해 현대의 파일 시스템에서는 거의 사용되지 않는다.</p>
<h2 id="12-연결-할당linked-allocation">1.2. 연결 할당(Linked Allocation)</h2>
<p>파일을 연속적으로 할당하지 않고, 임의의 블록에 할당하면서 각 블록에 다음 블록을 가리키는 포인터를 저장하는 방식이다. 이는 자료구조의 연결 리스트(Linked List)와 유사한 방식이다. (cf. 연속 할당은 배열처럼 파일을 저장한다.)</p>
<p>이러한 파일을 <strong>linked list of data blocks</strong>라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/452987f7-ab90-43ca-b629-2b59339abf24/image.png" alt=""></p>
<p>예를 들어, 파일의 첫 번째 블록을 6번 블록에 저장했다면, 6번 블록의 마지막에 다음 블록의 주소를 가리키는 포인터를 둔다. 디렉토리는 파일의 첫 번째 블록을 가리키는 주소를 저장한다.</p>
<pre><code>file name: f1
file size: 5 bytes
...
-----------------
block number: 6</code></pre><h3 id="121-특징">1.2.1. 특징</h3>
<p>외부 단편화가 발생하지 않는다. 파일이 연속적인 블록에 저장될 필요가 없기 때문에, 디스크의 빈 블록을 효율적으로 사용할 수 있다.</p>
<p>또한 파일이 커질 경우에도 쉽게 확장할 수 있다. 추가적인 블록을 할당하고 포인터를 연결해 주기만 하면 된다.</p>
<h3 id="122-단점">1.2.2. 단점</h3>
<p>가장 큰 단점은 직접 접근이 불가능하다는 점이다. 예를 들어, 파일 f1의 세 번째 블록에 바로 접근할 수 없다. 포인터를 통해 첫 번째 블록에서 순차적으로 접근해야만 세 번째 블록의 위치를 알 수 있다. 즉, 순차 접근만 가능하다는 뜻이다.</p>
<p>또한, 각 블록마다 포인터를 저장해야 하므로 추가적인 저장 공간 손실이 발생한다. 포인터 저장을 위해 약 4bytes 정도가 사용된다.</p>
<p>신뢰성 문제도 있는데, 만약 디스크의 특정 블록에 오류가 발생하면 해당 블록의 포인터를 읽지 못해 그 이후의 블록들도 모두 접근할 수 없게 된다. </p>
<p>이뿐만 아니라 파일이 여러 블록에 분산되어 있으므로 디스크 헤더가 자주 이동해야 하며, 이로 인해 성능 저하가 발생할 수 있다.</p>
<p>연결 할당은 연속 할당의 단점을 해결하지만, 나름의 문제점도 있다. 그럼에도 중요한 점은 외부 단편화가 발생하지 않아 디스크 공간 낭비가 없다는 것이다.</p>
<blockquote>
<h4 id="📁-fatfile-allocation-table-파일-시스템">📁 FAT(File Allocation Table) 파일 시스템</h4>
</blockquote>
<p>MS-DOS의 개발 당시, MS는 연결 할당의 장점을 살리면서도 효율적인 파일 시스템을 만들기 위해 FAT 파일 시스템을 고안했다. FAT는 연결 할당의 변종으로, 포인터들을 각 블록에 저장하는 대신, 이 포인터들을 테이블로 따로 모아 디스크 블록 중 하나에 저장하는 방식이다. 이 테이블을 <strong>FAT</strong>라고 부른다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/2152531d-a342-43c2-afa9-a9615d0a3e4d/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>FAT 시스템에서 각 블록은 FAT 테이블에 기록된 포인터를 통해 다음 블록을 가리킨다. 예를 들어, 6번 블록이 다음 블록으로 2번을 가리키고 있다고 할 때, 이 정보는 6번 블록에 있는 것이 아니라 FAT 테이블에 기록된다. 파일의 끝을 표시할 때는 -1을 사용한다.</p>
<blockquote>
</blockquote>
<p>이 방식의 장점은 직접 접근이 가능해진다는 것이다. 포인터를 일일이 따라갈 필요 없이 FAT 테이블을 읽어 원하는 데이터 블록으로 바로 이동할 수 있다. FAT 테이블은 부팅 시 메모리로 로드되기 때문에 테이블을 읽는 속도가 빠르다. 또한, 연결 할당의 단점 중 하나인 포인터가 깨지면 파일 전체를 읽을 수 없는 문제도 해결된다. 만약 테이블이 손상될 경우를 대비해 FAT 테이블의 복사본이 저장된다.</p>
<blockquote>
</blockquote>
<p>FAT에는 테이블을 위한 비트 할당 크기에 따라 여러 종류가 있다. FAT16, FAT32 같은 이름은 할당되는 비트 크기를 나타내며, 비트가 커질수록 저장 가능한 파일의 크기와 수가 늘어난다. </p>
<blockquote>
</blockquote>
<p>FAT는 주로 USB 메모리와 같은 이동식 디스크에서 많이 사용되며, 이 시스템은 MS-DOS, OS/2, Windows 등에서 널리 사용되고 있다.</p>
<h2 id="13-색인-할당index-allocation">1.3. 색인 할당(Index Allocation)</h2>
<p>파일의 각 블록을 가리키는 포인터들을 인덱스 테이블에 저장하는 방식이다. </p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/423fc6e3-651c-4b02-913b-39aff2490596/image.png" alt=""></p>
<p>디렉토리는 해당 파일의 인덱스 블록의 위치를 저장한다. 즉, <strong>인덱스 블록</strong>은 해당 파일의 데이터 블록들을 가리키는 포인터들의 집합이라고 볼 수 있다.</p>
<pre><code>file name: f1
file size: 5 bytes
...
-----------------
index block number: 11</code></pre><h3 id="131-특징">1.3.1. 특징</h3>
<p>이 방식은 순차 접근과 직접 접근이 모두 가능하다. </p>
<p>또한 파일이 연속적으로 저장될 필요가 없어 외부 단편화 문제도 해결된다.</p>
<h3 id="132-단점">1.3.2. 단점</h3>
<p>하지만 인덱스 테이블을 위한 별도의 블록이 필요하기 때문에 저장 공간의 낭비가 발생한다. 
예를 들어, 1byte의 작은 파일, 영어 한 글자를 저장하기 위해 데이터 블록 하나와 인덱스 블록 하나가 사용되며, 이는 상당한 비효율이다.</p>
<p>FAT 파일 시스템의 경우 앞의 몇 블록 정도만을 테이블로 사용하였는데, 색인 할당 방식은 하나의 파일 당 한 블럭이 사용되므로 조금 더 손실이 크다고 볼 수 있다.</p>
<p>파일이 커질 경우, 하나의 인덱스 블록만으로 충분하지 않을 수 있다. 예를 들어, 한 블록이 512bytes라고 가정하고, 4bytes 정수형 포인터를 사용하면 한 인덱스 블록에 $512/4=128$개의 포인터를 저장할 수 있다. 즉, 128개의 블록을 가리킬 수 있으며, 이는 $128*512=64KB$의 파일 크기에 해당한다. 파일 크기가 64KB를 초과할 경우, 하나의 인덱스 블록만으로는 모든 파일 블록을 가리킬 수 없다.</p>
<p>이 문제를 해결하기 위해 다양한 확장 방식이 있다.</p>
<ol>
<li><p><strong>Linked 인덱스 방식</strong>: 하나의 인덱스 블록이 부족할 경우, 다른 인덱스 블록과 연결하여 파일의 모든 블록을 가리킬 수 있다.</p>
<p> <img src="https://velog.velcdn.com/images/jayy_19/post/e5ebbd33-e142-4622-87c2-b88baa4e9487/image.png" alt=""></p>
</li>
<li><p><strong>다중 레벨 인덱스(Multilevel Index)</strong>: 여러 단계의 인덱스 테이블을 사용하여 대용량 파일을 처리할 수 있다. 예를 들어, 2단계 인덱스를 사용하면 첫 번째 인덱스 블록은 두 번째 인덱스 블록을 가리키고, 그 블록이 실제 데이터 블록을 가리킨다.</p>
<p> <img src="https://velog.velcdn.com/images/jayy_19/post/c76c007a-afcd-4ebf-bb5d-63185c3c4904/image.png" alt=""></p>
</li>
<li><p><strong>혼합 방식(Combined Scheme)</strong>: 첫 번째 인덱스 블록의 앞 부분은 실제 데이터 블록을 가리키고, 뒤쪽은 다른 인덱스 블록을 가리키는 방식이다. 이 방식은 대용량 파일을 처리하면서도 인덱스 테이블의 효율성을 높일 수 있다.</p>
</li>
</ol>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
<li><a href="https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-18.-%ED%8C%8C%EC%9D%BC-%ED%95%A0%EB%8B%B9">[운영체제(OS)] 18. 파일 할당</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 메모리]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC</guid>
            <pubDate>Fri, 08 Nov 2024 06:49:59 GMT</pubDate>
            <description><![CDATA[<h1 id="1-주기억장치-관리main-memory-management">1. 주기억장치 관리(Main Memory Management)</h1>
<p>보조기억장치(e.g. 하드 드라이브, SSD)에는 운영체제와 다양한 실행 파일들이 저장되어 있다. 컴퓨터의 전원을 켜면, 운영체제는 보조기억장치에서 메인 메모리(RAM)로 로드된다.</p>
<p>운영체제의 중요한 역할 중 하나는 프로세스를 관리하는 것이다. 즉, 운영체제는 CPU 자원을 효율적으로 관리하며, 여러 프로세스가 원활하게 실행될 수 있도록 조정합니다. 이를 <strong>프로세스 관리(Process Management)</strong>라고 한다.</p>
<p>또 다른 중요한 역할은 메인 메모리(RAM)를 효과적으로 관리하는 것이다. 운영체제는 메모리의 사용을 추적하고, 프로세스가 필요한 메모리를 할당하거나 해제하며, 메모리 자원을 최적화한다. 이를 <strong>메모리 관리(Memory Management)</strong>라고 한다.</p>
<p>메인 메모리 관리에 대해 자세히 알아보자.</p>
<h2 id="11-메모리의-역사">1.1. 메모리의 역사</h2>
<p>현재 우리는 메모리를 생각하면 반도체 집적 회로를 떠올린다. 하지만 과거에는 반도체 기술이 지금처럼 발달하지 않았다. 예전에는 반도체 안의 소자, 특히 트랜지스터가 손톱 크기 정도였고, 1비트를 저장하는 데에도 상당한 어려움이 있었다.</p>
<p><strong>트랜지스터 메모리</strong>는 1960-1970년대에 사용되었으며, 그 이전에는 <strong>진공관 메모리</strong>가 있었다. 진공관은 트랜지스터 보다 훨씬 크고 비효율적이었으며, 손가락 서너 개 크기의 진공관이 1비트를 저장할 수 있는 정도였다.</p>
<p>또 다른 초기 메모리 기술로 <strong>Core Memory</strong>가 있다. 이는 반지 모양의 철심에 자성 물질을 바르고 전기를 흘려 플레밍의 오른손 법칙에 따라 자장이 형성되도록 했다. 전기를 흘려 자석을 만드는 방식을 통해 메모리를 구성했다.</p>
<p>이런 과거의 기술적 한계를 고려할 때, 1비트를 저장하는 데에 굉장히 큰 비용이 들었을 것으로 예상할 수 있다.</p>
<h2 id="12-메모리-용량">1.2. 메모리 용량</h2>
<p>메모리 용량은 시간이 지나면서 급격히 증가했다.</p>
<ul>
<li>1970년대: 8-bit PC 64KB</li>
<li>1980년대: 16-bit IBM-PC 640KB → 1MB → 4MB</li>
<li>1990년대: 수 MB → 수십 MB</li>
<li>2000년~: 수백 MB → 수 GB</li>
</ul>
<p>우리가 배우는 운영체제는 언제 만들어졌을까? 운영체제의 기본 개념은 1960년대에 만들어졌다. (e.g. 리눅스는 1969년에 개발되었다.)</p>
<p>그 시절에는 메모리가 매우 부족했기 때문에, 프로세스 관리도 중요했지만 메모리 관리가 무엇보다도 중요했다. 사람들은 어떻게 하면 작은 메모리를 알차게 사용할 수 있을지 고민하며 다양한 기술을 개발했다.</p>
<p>하지만 지금은 메모리 용량이 매우 커졌다. 그럼 이제 메모리 관리는 중요치 않은 걸까?</p>
<p><em>그렇지 않다. 메모리는 언제나 부족하다.</em></p>
<h2 id="13-언제나-부족한-메모리">1.3. 언제나 부족한 메모리</h2>
<p>초등학생 때는 용돈 만 원으로 살고, 중학생 때는 용돈 5만원으로 살고, 고등학생 때는 용돈 10만 원으로도 충분히 살았는데 이제는 그 돈으로는 턱없이 부족하다. 생활 반경이 넓어지고 씀씀이가 커지면서 필요한 돈도 함께 늘어나기 때문이다.</p>
<p>메모리도 마찬가지다. 메모리 용량이 매우 커졌음에도 불구하고, 여전히 메모리는 늘 부족하다.</p>
<p>1950-1960년대에는 프로그램을 작성할 때 주로 기계어, 어셈블리어로 작성했다. 1970년대에는 C 언어와 같은 고급 언어로 작성되기 시작했다. 이후 자바와 같은 객체지향 언어가 등장하면서 프로그램의 규모는 더욱 커졌다. 예전에는 숫자나 문자를 처리하던 프로그램이 지금은 그림, 소리, 비디오 등 다양한 멀티미디어 자료를 처리하게 되면서 데이터의 크기가 기하급수적으로 증가했다. </p>
<p>때문에 <strong>현대에도 메모리는 여전히 부족하다.</strong> 메모리 관리에 대한 연구는 오랜 연구 분야지만, 그 필요성은 지금도 존재한다.</p>
<p>메모리 관리에서 우리가 배우고자 하는 것은, 이렇게 <em>늘 부족한 메모리를 어떻게 하면 낭비 없이 효율적으로 사용할 수 있을까</em> 하는 것이다. </p>
<h2 id="14-메모리에-프로그램-올리기">1.4. 메모리에 프로그램 올리기</h2>
<h3 id="141-메모리-구조">1.4.1. 메모리 구조</h3>
<p>메모리 구조는 기본적으로 <strong>주소(Address)</strong>와 <strong>데이터(Data)</strong>로 이루어져 있다. CPU가 특정 주소를 읽으려고 하면, 해당 주소를 메모리에 보내고 메모리는 그 주소에 해당하는 데이터를 CPU로 돌려준다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/6478a23a-69d0-4db2-af26-6d8547c607b2/image.png" alt=""></p>
<h3 id="142-프로그램-개발">1.4.2. 프로그램 개발</h3>
<p>프로그램을 빌드하면 컴파일 과정을 거치면서 원천 파일(Source file), 목적 파일(Object file), 실행 파일(Executable file)이 생성된다.</p>
<p><strong>원천 파일</strong>(e.g. <code>main.c</code>)은 고수준 언어나 어셈블리 언어로 작성된 코드가 포함되어 있다. 이 원천 파일을 컴파일하면 <strong>목적 파일</strong>(e.g. <code>main.o</code>)이 생성된다. 목적 파일은 기계어로 번역된 파일로, 컴파일 과정에서 생성되는 결과물이다. 이 목적 파일은 프로그램 실행에 필요한 코드의 일부만 포함하고 있으며, 나중에 다른 코드들과 연결(Link)되어 최종 <strong>실행 파일</strong>(e.g. <code>main.exe</code>)이 된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/f9a4b905-39a8-4ce3-b5dd-f9a7453c9cd6/image.png" alt=""></p>
<h3 id="143-프로그램-실행">1.4.3. 프로그램 실행</h3>
<p>실행 파일은 크게 세 가지 주요 세그먼트로 구성된다.</p>
<ul>
<li>코드(Code): 실제 기계어 코드가 저장된 부분</li>
<li>데이터(Data): 프로그램이 사용하는 변수들이 저장되는 부분</li>
<li>스택(Stack): 함수 호출 시 돌아오는 주소와 지역 변수가 저장되는 부분</li>
</ul>
<h3 id="144-메모리에-실행-파일-올리기">1.4.4. 메모리에 실행 파일 올리기</h3>
<p>이렇게 만들어진 실행 파일을 메모리에 올려보자. 이걸 메모리의 몇 번지에 올려야 할지 우리는 고민할 필요가 없다. 운영체제가 이 문제를 해결해 주기 때문이다. 운영체제의 메모리 관리 부서에서 Loader가 이 역할을 담당한다.</p>
<p>다중 프로그래밍 환경에서는 여러 프로세스가 메모리에 동시에 올라가 있다. 예를 들어, 오늘은 워드 프로세서를 메모리의 500번지에 올렸다. 내일 해당 주소에 다시 워드 프로세서를 올릴 수 있다는 보장이 있을까? 내일은 해당 주소가 다른 프로세스에 의해 사용될 수 있어 동일한 주소를 보장받을 수 없다.</p>
<p>이 문제를 해결하는 데 중요한 역할을 하는 것이 <strong>MMU(Memory Management Unit)</strong>이다.</p>
<h3 id="145-mmumemory-management-unit">1.4.5 MMU(Memory Management Unit)</h3>
<p>MMU는 CPU와 메모리 사이에 위치하며, base 레지스터와 limit 레지스터, 그리고 <strong>재배치 레지스터(Relocation Register)</strong>를 포함하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/ffdd9afd-bca6-4c27-ac2c-fdb1f9ab0ab5/image.png" alt=""></p>
<p>MMU 덕분에 우리는 실행 파일을 메인 메모리의 어느 곳에나 배치하여 실행할 수 있다.</p>
<p>예를 들어, CPU는 main.exe가 항상 메모리의 0번지에서 실행된다고 가정하지만, 실제로는 500번지에 위치할 수 있다. 이때 MMU가 500번지를 매핑해줌으로써 CPU는 0번지에서 실행된다고 생각하지만 실제로는 500번지에서 실행된다. 만약 다음 날 1000번지에서 실행된다면, MMU는 1000번지를 매핑해준다.</p>
<p>운영체제는 이 재배치 레지스터를 조정하여, 실제 메모리의 어느 위치에 프로그램이 있든지 상관없이 동일하게 실행되도록 한다. 따라서 프로그램이 메모리의 어느 번지에 위치하는지는 중요치 않게 된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a0c318c2-0803-4baf-8913-573e8e48f88d/image.png" alt=""></p>
<p>정리하자면, MMU는 <strong>주소 변환(Address Translation)</strong>을 수행하여 <strong>논리적 주소(CPU가 내는 주소)</strong>를 <strong>물리적 주소(실제 메모리 주소)</strong>로 변환한다. CPU는 항상 동일한 주소를 사용하고 있다고 생각하지만, 실제로 물리적 주소는 매번 다를 수 있다. 이렇게 해서 컴퓨터 내에는 두 가지 주소(논리적 주소, 물리적 주소)가 존재하게 된다.</p>
<h1 id="2-메모리-낭비-방지">2. 메모리 낭비 방지</h1>
<p>운영체제의 주된 목적은 컴퓨터를 쉽게 사용할 수 있도록 하고, 시스템의 효율성을 극대화하는 것이다. 이 과정에서 중요한 요소 중 하나는 <strong>메모리 낭비를 최소화하는 것</strong>이다. 메모리 낭비를 줄이기 위해 어떤 요소들이 문제로 작용하며, 이를 어떻게 해결할 수 있을지 알아보자.</p>
<h2 id="21-동적-적재dynamic-loading">2.1. 동적 적재(Dynamic Loading)</h2>
<p>예를 들어, 오류 처리를 위한 코드가 있다고 생각해보자. 이는 파일이 정상적으로 열리지 않는 경우에만 실행될 텐데, 실제로 대부분의 경우 파일은 정상적으로 열린다. 이처럼 거의 사용되지 않는 오류 처리 루틴을 메모리에 항상 올려놓는 것은 메모리 낭비다.</p>
<p><strong>동적 적재</strong>는 이러한 낭비를 방지하기 위한 방법이다. 동적 적재는 프로그램 실행 시 반드시 필요한 루틴이나 데이터만 메모리에 적재하고, 필요 시에만 추가로 적재한다. 모든 오류 처리, 모든 데이터(e.g. 배열)을 필요할 때에만 메모리에 올리는 것이 동적 적재의 예다.</p>
<p>현재 사용하는 윈도우, 리눅스는 동적 적재를 채택해 메모리 사용의 효율성을 높이고 있다.</p>
<p>cf) 정적 적재(Static Loading): 프로그램의 모든 루틴과 데이터를 미리 메모리에 올리는 방식</p>
<h2 id="22-동적-연결dynamic-linking">2.2. 동적 연결(Dynamic Linking)</h2>
<p>여러 프로그램이 공통적으로 사용하는 라이브러리 코드가 있다면, 이를 메모리에 중복해서 올리는 것은 큰 낭비이다. 예를 들어, <code>p1.exe</code>와 <code>p2.exe</code>가 <code>printf</code>와 같은 라이브러리 코드를 사용하는 경우, 이 라이브러리를 각각의 프로그램이 중복해서 메모리에 올린다면 메모리 자원의 낭비가 발생한다.</p>
<p><strong>동적 연결</strong>은 이러한 낭비를 방지하는 방법이다. 공통적으로 사용되는 라이브러리 코드를 하나만 메모리에 적재하고, 여러 프로그램이 이를 공유하는 방식이다. 동적 연결은 프로그램이 실행 파일을 우선 만든 뒤 필요에 따라 하드 디스크에서 이를 링크하는 방식으로, 실행 파일을 만들기 전에 링크가 이루어지는 <strong>정적 연결(Static Linking)</strong>과 대조된다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a1f7b9e2-6c0a-4d38-9269-ca965dd0085c/image.png" alt=""></p>
<p>리눅스에서는 이러한 공통 라이브러리를 <strong>공유 라이브러리(Shared Library)</strong>라고 하며, 윈도우에서는 <strong>동적 연결 라이브러리(DLL: Dynamic Linking Library)</strong>라고 한다.</p>
<h2 id="23-swapping">2.3. Swapping</h2>
<p>Swapping은 메모리에 적재되어 있지만 현재 사용되지 않는 프로세스를 메모리에서 하드 디스크로 이동시키는 방법이다.</p>
<p>hwp 프로그램을 사용하던 중에 전화가 와서 잠시 자리를 비운 상황을 생각해 보자. 내가 자리를 비우더라도 프로그램은 여전히 메모리에 올라가서 실행 중에 있고, 이는 명백한 메모리 자원의 낭비이다. 이때 운영체제는 메모리에서 이 프로그램을 하드 디스크로 이동시키고, 다른 프로세스를 위해 메모리를 확보한다.</p>
<p>하드 디스크에 저장된 <code>hwp.exe</code> 파일에는 기본적으로 code와 data가 포함되어 있다. 이 파일이 메모리에 올라가면 code, data, stack으로 구성된다. 하드 디스크에 있는 <code>hwp.exe</code>와 메모리에 적재된 <code>hwp.exe</code>는 동일한 코드 파일을 가질지 몰라도, data와 stack은 메모리에 로드된 후의 상태에 따라 다르게 관리된다.</p>
<p>따라서, 프로세스를 swap-out할 때 단순히 하드 디스크에 있는 원본 파일로 대체할 수는 없다. Swap-out은 하드 디스크의 특정 영역(Backing Store)에 현재까지 실행 중이던 프로세스의 상태(code, data, stack)를 저장하는 것을 의미한다. 이렇게 swap되어 확보된 메모리 공간은 다른 프로세스가 사용하게 되어 메모리 낭비를 방지한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/fa00128d-f007-41d3-bb8c-8b50d347c86c/image.png" alt=""></p>
<p>하드 디스크는 크게 <strong>파일 시스템(File System)</strong>과 <strong>Backing Store(=Swap Device)</strong>로 구분할 수 있다. Backing Store는 swap-out된 프로세스의 이미지를 저장하는 공간이다. 서버나 대형 컴퓨터의 경우, 파일 시스템과 Backing Store를 위한 별도의 하드 디스크를 두기도 한다. 그러나 일반적인 PC나 스마트폰은 하나의 하드 디스크를 나누어 이 기능을 수행한다.</p>
<p>Backing Store의 크기는 대개 메인 메모리 크기와 비슷하게 설정된다. 예를 들어, 메인 메모리가 1GB인 경우, 최대 1GB의 프로세스가 swap-out될 수 있으므로 Backing Store의 크기도 이와 비슷한 크기로 설정된다. 사실 메인 메모리에는 운영체제 영역도 있기 때문에 실제 필요한 크기는 더 작아도 된다. Backing Store의 크기는 시스템에 따라 기본적으로 설정되지만, 사용자가 직접 설정할 수도 있다.</p>
<p>전화를 마치고 돌아와 <code>hwp.exe</code>를 다시 실행하려고 할 때, <code>hwp.exe</code>가 사용 중이던 주소를 다른 프로세스가 사용하고 있다면 <code>hwp.exe</code>는 다른 메모리 영역으로 이동해야 한다. 이때 재배치 레지스터(Relocation Register) 값을 조정하면 문제없이 프로그램 실행이 가능하다. </p>
<p>Swapping 자체는 성능에 부담을 줄 수 있다. 프로세스의 상태를 Backing Store에 기록하는 작업은 하드 디스크의 속도에 의존하기 때문에 느릴 수 있다. 그럼에도 Swapping 덕분에 전체 메모리 활용도는 크게 향상된다. 일부 슈퍼 컴퓨터의 경우, Backing Store를 위해 속도가 느린 별도의 저장 장치를 사용하여 메모리 효율을 극대화하기도 한다.</p>
<h1 id="3-연속-메모리-할당">3. 연속 메모리 할당</h1>
<p>최초의 컴퓨터는 OS가 없었기 때문에 메모리에 프로세스 하나만 올라갔다. 
시간이 지나면서 메모리에는 운영체제와 프로세스 하나가 올라간다. (e.g. 도스)
이후 메모리에 단일 프로세스와 함께 여러 프로세스가 돌아가는, <strong>다중 프로그래밍 환경</strong>이 생성되었다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/aca007c7-567b-4353-9dfb-41bbbf9cd67d/image.png" alt=""></p>
<h2 id="31-외부-단편화external-fragmentation">3.1. 외부 단편화(External Fragmentation)</h2>
<p>부팅 직후의 메모리 상태는 OS와 big single hole로 구성되어 있다. 이후 프로세스가 생성되고 종료되는 것이 반복되며 scattered holes가 생성된다. 즉, 메모리가 쪼개져 있게 되는데, 이를 <strong>메모리 단편화(Memory Fragmentation)</strong>라고 한다.</p>
<p>Holes의 크기를 100KB, 50KB, 80KB라고 해보자. 새로운 130KB짜리 프로세스를 메인 메모리에 올리고 싶은데, holes가 불연속적으로 흩어져 있어 올릴 수 없다. 이를 <strong>외부 단편화</strong>라고 한다. 비어 있는 메모리 공간을 합치면 충분히 들어갈 수 있는데 떨어져 있기 때문에 이를 사용할 수 없는 상황을 말한다.</p>
<h2 id="32-연속-메모리-할당-방식">3.2. 연속 메모리 할당 방식</h2>
<p>외부 단편화를 어떻게 하면 최소화할 수 있을까? 우선 연속 메모리 할당 방식부터 살펴보자.</p>
<ul>
<li><strong>First-fit(최초 적합)</strong>: 메모리를 순차적으로 돌면서 해당 프로세스가 올라갈 수 있는 곳을 찾으면 즉시 올린다.</li>
<li><strong>Best-fit(최적 적합)</strong>: 프로세스의 크기와 가장 밀접한 메모리 공간에 올린다.<ul>
<li>e.g. 60KB의 프로세스는 100KB가 아니라 80KB에 올린다.</li>
</ul>
</li>
<li><strong>Worst-fit(최악 적합)</strong>: 프로세스와 가장 밀접하지 않은 메모리 공간에 올린다. 더 작은 메모리 공간에는 올릴 수 없다.</li>
</ul>
<p>메모리를 순차적으로 돌면서 알맞은 공간이면 바로 프로세스를 올린다는 점에서 속도 측면으로는 first-fit이 가장 우수하다. 메모리가 가장 효율적으로 활용되는 방식은 의외로 best-fit과 first-fit이 비슷하다. Best-fit 방식이 가장 우수할 것이라고 예상되지만, 여러 케이스를 돌려본 결과 두 방식이 유사하다고 한다. Worst-fit은 언제나 가장 비효율적이다. </p>
<p>그러나 어떤 방식을 사용하든 외부 단편화 문제를 완벽히 해결할 수는 없다. 실제로 이로 인한 메모리 낭비는 $1/3$ 수준에 달한다.</p>
<p>운영체제가 메모리 공간을 지켜보다가, 프로세스를 새로 돌리려고 할 때 실행 중인 프로세스들 또는 holes를 한 곳으로 모으는 것을 <strong>Compaction</strong>이라고 한다. 이는 외부 단편화를 해결할 수는 있겠으나 부담이 크고, 어떤 프로세스를 어디로 모으는 게 효율적일지 계산하는 최적 알고리즘이랄 게 없다.</p>
<h1 id="4-가상-메모리">4. 가상 메모리</h1>
<p>가상 메모리는 물리적 메모리 크기의 한계를 극복하기 위해 개발된 기술이다. 메인 메모리가 100MB밖에 없는데 200MB 크기의 프로그램을 실행하고 싶은 상황을 생각해 보자.</p>
<p>200MB의 프로그램을 실행할 때, 반드시 모든 프로세스 이미지를 메모리에 올려야 할까? 그렇지 않다. 우리는 동적 적재에서 보았듯, 오류 처리 코드와 같이 반드시 필요하지 않은 루틴은 실행 중에 올릴 필요가 없다. 예를 들어, 오류 처리 루틴이 50MB라면 이를 제외하고 150MB만 메모리에 올리면 된다.</p>
<p>그래도 메모리가 부족하다면 사용하지 않을 루틴을 더 제외할 수 있다. 예를 들어, 한글 작업을 하는 동안 정렬 또는 표 만들기 기능을 사용하지 않을 예정이라면, 이들을 메모리에 올리지 않고 당장 사용하는 부분만 올려 실행한다.</p>
<p>결국 가상 메모리의 핵심은, <strong>우리가 프로그램의 모든 코드를 메모리에 올려야 한다고 생각할 필요가 없다</strong>는 것이다. 필요한 부분만 메모리에 올려 프로그램을 실행할 수 있다는 개념이 가상 메모리의 본질이다.</p>
<h2 id="41-요구-페이징demand-paging">4.1. 요구 페이징(Demand Paging)</h2>
<p>요구 페이징은 프로세스를 페이지 단위로 나누어, 현재 필요한 페이지만 메모리에 올리는 방식이다. 필요한 페이지가 메모리에 없으면 하드 디스크에서 해당 페이지를 가져오고, 현재 요구되지 않는 페이지는 Backing Store에 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/3b65c1f0-0c0e-4ad4-b6c8-6fa1038716ee/image.png" alt=""></p>
<p>페이지 테이블에는 각 페이지 상태를 기록하는 <strong>유효 비트(Valid Bit)</strong>가 있다. 이 비트를 통해 메모리에 올라와 있는 페이지와 그렇지 않은 페이지(invalid)를 구분할 수 있다. CPU가 요청한 주소가 페이지 테이블에 없으면, MMU는 CPU에 인터럽트를 걸어 OS가 해당 페이지를 하드 디스크에서 메모리로 가져오도록 처리한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/57634c88-b765-4d46-a613-edad6f36ca16/image.png" alt=""></p>
<p>가상 메모리를 만드는 방법은 대표적으로 두 가지가 있으나, 요구 페이징이 가장 일반적으로 사용되어 두 용어를 동일하게 간주하는 경우가 많다.</p>
<h3 id="411-pure-demanding-paging">4.1.1. Pure Demanding Paging</h3>
<p>진짜 필요한 페이지만 메모리에 가져오는 방식이다. 프로그램이 처음 실행될 때 어떤 페이지가 필요한지 예측하지 않고, 처음에는 아무 페이지도 메모리에 올리지 않는다. </p>
<p>처음 프로그램이 실행될 때 페이지 부재가 많이 발생하여 속도가 느려질 수 있으나, 메모리 절약에는 도움이 된다.</p>
<h3 id="412-prepaging">4.1.2. Prepaging</h3>
<p>앞으로 필요할 것으로 예상되는 페이지를 미리 가져오는 방식이다.</p>
<p>페이지 부재를 줄이고 프로그램 실행 속도를 높일 수 있지만, 미리 가져온 페이지가 실제로 사용되지 않으면 메모리 낭비가 발생할 수 있다.</p>
<h3 id="413-swapping-vs-demanding-paging">4.1.3. Swapping vs Demanding Paging</h3>
<p>Swapping은 프로세스 전체를 메모리에서 하드 디스크로 내보내고, 필요할 때 전체를 다시 가져오는 방식이다. 반면, Demanding Paging은 필요한 페이지만을 메모리로 가져오는 방식이다.</p>
<p>둘 다 메모리와 Backing Store를 오가는 방식이지만, Swapping은 프로세스 단위로, Demanding Paging은 페이지 단위로 작업이 이루어진다.</p>
<h2 id="42-페이지-부재page-fault">4.2. 페이지 부재(Page Fault)</h2>
<p>CPU가 요청한 페이지가 메모리에 없을 때 발생하는 상황이다. 페이지 부재가 발생하면 CPU는 하던 일을 멈추고, OS가 해당 페이지를 하드 디스크에서 메모리로 가져오도록 처리한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/2a9b89d2-7583-43bb-bc45-f429cf19d1f3/image.png" alt=""></p>
<p>과정은 다음과 같다.</p>
<ol>
<li>CPU가 특정 주소를 요청한다.</li>
<li>해당 주소가 포함된 페이지가 메모리에 없는 경우, MMU가 페이지 부재를 감지하고 CPU에 신호를 보낸다.</li>
<li>OS가 하드 디스크를 돌면서 요구 페이지를 찾는다.</li>
<li>요구 페이지를 메인 메모리로 가져온다.</li>
<li>페이지 테이블을 갱신한다.</li>
<li>해당 페이지를 읽어 명령을 마저 수행한다.</li>
</ol>
<h3 id="421-유효-접근-시간effective-access-time">4.2.1. 유효 접근 시간(Effective Access Time)</h3>
<p>CPU가 메모리에 접근하는 시간은 페이지 부재 발생 여부에 따라 달라진다. 페이지 부재가 발생하지 않으면 메모리에서 데이터를 즉시 가져올 수 있지만, 페이지 부재가 발생하면 하드 디스크에서 데이터를 가져와야 하므로 시간이 더 오래 걸린다.</p>
<p><strong>유효 접근 시간</strong>이란 페이지 부재 확률($p$)와 페이지 부재 처리 시간($T_p$), 메모리 접근 시간($T_m$)을 조합한 평균 접근 시간을 의미한다. ($T_{eff}=(1-p)T_m+pT_p$)</p>
<p>아래 예제를 살펴 보자.</p>
<ul>
<li>$T_m=200ns$ (DRAM)</li>
<li>$T_p=8ms$</li>
<li>$T_{eff} = (1-p)200 + p 8,000,000 = 200 + 7,999,800p$</li>
</ul>
<p>여기서 페이지 부재 확률의 계수가 매우 커 $p$가 높아질수록 유효 접근 시간 $T_{eff}$가 크게 증가하는 것을 확인할 수 있다.</p>
<p>예를 들어 $p=1/1,000$일 경우, $T_{eff}=8.2μs$으로 $T_m$보다 40배는 느리다.
$p = 1/399,990$일 경우 $T_{eff}=220ns$으로 $T_m$보다 10% 느리다.</p>
<blockquote>
<h4 id="⏱️-페이지-부재-처리-시간-t_p은-seek-time-rotational-delay-transfer-time으로-구성되어-있다">⏱️ <strong>페이지 부재 처리 시간 $T_p$은 Seek Time, Rotational Delay, Transfer Time으로 구성되어 있다.</strong></h4>
</blockquote>
<p>하드 디스크의 구조는 원판(플래터)에 자성 물질이 코팅되어 있고, 그 위에 디스크 헤드가 위치하여 데이터를 읽고 쓴다. 디스크 헤드는 전기 코일로 이루어져 있으며, 이 코일에 전류가 흐르면 자성이 발생하고, 자성 물질에 기록된 데이터를 읽을 수 있다. 원판이 고속으로 회전하면서 디스크 헤드가 자성 물질의 변화에 따라 유도된 전기 신호를 감지하여 데이터를 읽어낸다.</p>
<blockquote>
</blockquote>
<p>하드 디스크에서 데이터를 읽어내는 과정은 다음과 같다.</p>
<blockquote>
</blockquote>
<ol>
<li>Seek Time: 디스크 헤드를 원하는 트랙(원판의 특정 위치)으로 이동시키는 데 걸리는 시간. 트랙은 원판의 원형 경로를 의미하며, 헤드가 물리적으로 해당 트랙으로 이동해야 한다. 이 과정은 전체 디스크 접근 시간 중 가장 오래 소요되는 부분이다.</li>
<li>Rotational Delay: 트랙에 도달한 후, 원판이 회전하여 디스크 헤드 아래로 원하는 데이터가 위치할 때까지의 대기 시간. 원판이 회전하는 속도에 따라 달라지며, 데이터가 헤드 아래에 도착할 때까지의 시간이다.</li>
<li>Transfer Time: 디스크 헤드가 데이터를 읽어내는 실제 시간. 원판이 회전하면서 자성 물질의 변화를 감지해 데이터를 읽어들이며, 전기가 유도되는 과정에서 정보가 전송된다.<blockquote>
</blockquote>
이 세 가지 시간을 모두 합한 것이 하드 디스크에서 데이터를 읽는 데 걸리는 총 시간이다. 일반적으로 이들 중 가장 오래 걸리는 시간은 Seek Time이다. Seek Time은 헤드가 물리적으로 움직여야 하기 때문이다.<blockquote>
</blockquote>
이를 거꾸로 생각해 보면, Backing Store를 하드 디스크로 쓰지 않고 SSD, 느린 저가 DRAM을 사용하는 방법을 활용해 유효 접근 시간을 낮출 수 있다.</li>
</ol>
<h3 id="422-지역성의-원리principal-of-locality">4.2.2. 지역성의 원리(Principal of Locality)</h3>
<p>CPU가 특정 주소를 참조할 때, 그 근처의 주소를 자주 참조하게 된다(Locality of Reference)는 특성을 말한다.</p>
<ul>
<li>시간적 지역성(Time Locality): 최근에 접근한 데이터를 다시 접근할 가능성이 높다.<ul>
<li>e.g. 보통 컴퓨터 프로그램은 반복문이 많아 방금 읽은 코드를 다시 읽을 가능성이 높다.</li>
</ul>
</li>
<li>공간적 지역성(Spatial Locality): 근처 주소를 연속적으로 접근할 가능성이 높다.<ul>
<li>e.g. 한 명령어 길이가 4byte면, 주소 1000번지에서 시작된 명령어를 읽고 곧바로 1004, 1008, … 등 그 인접 주소의 명령어들도 읽히게 된다.</li>
</ul>
</li>
</ul>
<p>이 원리 덕분에, 페이지 부재가 발생하면 그 인근의 데이터를 블록 단위로 미리 가져옴으로써 페이지 부재 확률을 줄일 수 있다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-12.-%EC%A3%BC%EA%B8%B0%EC%96%B5%EC%9E%A5%EC%B9%98%EA%B4%80%EB%A6%AC">[운영체제(OS)] 12. 주기억장치관리</a></li>
<li><a href="https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-15.-%EA%B0%80%EC%83%81%EB%A9%94%EB%AA%A8%EB%A6%AC">[운영체제(OS)] 15. 가상메모리</a></li>
<li><a href="https://gwpaeng.tistory.com/162">(56) Swapping</a></li>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 페이지 교체 알고리즘]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%90%EC%B2%B4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%90%EC%B2%B4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Thu, 07 Nov 2024 02:11:52 GMT</pubDate>
            <description><![CDATA[<h1 id="1-페이지-교체">1. 페이지 교체</h1>
<p>처음 부팅했을 때에는 메모리가 비워져 있다가, 프로그램 실행에 따라 요구되는 페이지들을 backing store에서 가져와 메모리에 올린다. 이를 반복하다 보면 언젠가는 메모리가 가득 차게 된다.</p>
<p>메모리가 가득 차면 추가로 페이지를 가져오기 위해 어떤 페이지는 backing store로 몰아내고(page-out) 빈 자리에 페이지를 가져온다.(page-in) </p>
<h2 id="11-victim-page">1.1. Victim Page</h2>
<p>이렇게 빈 자리를 위해 쫓아내는 페이지를 <strong>희생양 페이지(Victim Page)</strong>라고 한다.</p>
<p>Backing store로 페이지를 몰아낼 때, 즉 하드 디스크로 페이지를 보낼 때 해당 내용을 하드 디스크에 다시 써줘야 할까? 사실 페이지가 단순히 읽기 작업만 수행한 것이라면, 이미 하드 디스크에 있던 내용일 것이다. 그러나 만일 CPU에서 해당 페이지의 내용을 수정했다면 이를 하드 디스크에 다시 저장해야 한다. </p>
<p>하드 디스크는 일반적으로 속도가 느리기 때문에, 데이터를 쓰는 데 시간이 오래 걸린다. 따라서 <strong>가능한 한 수정되지 않은 페이지(victim)를 선택</strong>해 내보내는 것이 좋다. 수정 여부를 알기 위해 페이지 테이블에 Modified Bit(=Dirty Bit)를 추가하여 검사한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/bf60a4ae-1ea3-4ba6-a6a0-418863154f67/image.png" alt=""></p>
<p>위의 그림은 Modified Bit를 추가한 페이지 테이블이다. 그런데 수정되지 않은 페이지는 0, 2, 3으로 복수 개가 존재한다. 어떤 페이지를 victim으로 설정해야 할까?</p>
<p>Random으로 설정하여 가장 간단하지만 성능 또한 제멋대로인 방법도 있을 거고, FIFO를 사용하여 가장 먼저 메모리에 올라온 페이지를 쫓아내는 방법도 있을 것이다.</p>
<p>이처럼 victim page를 어떤 기준으로 선정할 것인지 판단하는 알고리즘을 <strong>페이지 교체 알고리즘(Page Replacement Algorithms)</strong>이라고 한다.</p>
<h1 id="2-페이지-교체-알고리즘">2. 페이지 교체 알고리즘</h1>
<p>페이징에서는 byte보다 페이지 단위가 중요하다. CPU가 낸 주소를 페이지 넘버와 $d$로 나눌 수 있는데, 이 페이지 넘버를 통해 내가 읽으려는 페이지가 메인 메모리의 어느 주소에 위치하는지 알 수 있기 때문이다.</p>
<pre><code>CPU 주소 = 100 101 102 432 612 103 104 611 612
Page No. = 1 1 1 4 6 1 1 6 6</code></pre><p>위의 예시에서 Page size = 100byte일 경우 페이지 번호는 1, 1, 1, 4, 6, 1, … 이 된다. (offset은 0, 1, 2, 32, 12, 3, …) </p>
<p>1번 페이지가 Page Fault가 나서 하드 디스크에서 이를 가져온다. 이 뒤에 바로 1번 페이지를 읽으면 절대 Page Fault가 나지 않는다. 이처럼 페이지 번호 중에서 바로 이어지는 번호는 스킵하고 읽는 것을 <strong>페이지 참조열(Page Reference String)</strong>이라고 한다.</p>
<pre><code>Page Reference String = 1 4 6 1 6</code></pre><h2 id="21-fifofirst-in-first-out">2.1. FIFO(First-In First-Out)</h2>
<p>FIFO(First-In, First-Out) 페이지 교체 알고리즘은 <strong>가장 먼저 메모리에 올라온 페이지를 가장 먼저 내보내는 방식</strong>으로 동작한다. 매우 간단한 방식이며, 페이지가 메모리에 올라온 순서를 그대로 유지하여 오래된 페이지를 우선적으로 교체한다.</p>
<p>해당 알고리즘의 아이디어는 <em>가장 처음 메모리에 올라온 페이지는 주로 프로그램 초기화와 관련된 코드나 데이터를 담고 있을 가능성이 높다</em>는 가정이었다.</p>
<p>e.g. 자바와 같은 언어에서는 객체가 생성될 때 생성자 메서드가 실행되며, 이는 한 번만 실행된다.</p>
<p>이처럼 초기화 코드는 대개 프로그램 초기에 한 번만 사용되고, 이후에는 재사용될 필요가 적다. 따라서 메모리에 가장 먼저 올라온 페이지는 초기화에 필요한 코드나 데이터를 담고 있을 가능성이 크다고 판단해 가장 처음 메모리에 올라온 페이지를 교체 대상으로 선택하는 것이 합리적이라 생각되어 고안된 것이다.</p>
<h3 id="211-예제">2.1.1. 예제</h3>
<pre><code>페이지 참조열 = 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 7 0 1
# of frames = 3</code></pre><table>
<thead>
<tr>
<th>Page-in</th>
<th>프레임</th>
<th>First Page</th>
<th>Page Fault</th>
</tr>
</thead>
<tbody><tr>
<td>7</td>
<td>{7}</td>
<td>7</td>
<td>1</td>
</tr>
<tr>
<td>0</td>
<td>{7, 0}</td>
<td>7</td>
<td>2</td>
</tr>
<tr>
<td>1</td>
<td>{7, 0, 1}</td>
<td>7</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>{2, 0, 1}</td>
<td>0</td>
<td>4</td>
</tr>
<tr>
<td>0</td>
<td>{2, 0, 1}</td>
<td>0</td>
<td></td>
</tr>
<tr>
<td>3</td>
<td>{2, 3, 1}</td>
<td>1</td>
<td>5</td>
</tr>
<tr>
<td>0</td>
<td>{2, 3, 0}</td>
<td>2</td>
<td>6</td>
</tr>
<tr>
<td>4</td>
<td>{4, 3, 0}</td>
<td>3</td>
<td>7</td>
</tr>
<tr>
<td>2</td>
<td>{4, 2, 0}</td>
<td>0</td>
<td>8</td>
</tr>
<tr>
<td>3</td>
<td>{4, 2, 3}</td>
<td>4</td>
<td>9</td>
</tr>
<tr>
<td>0</td>
<td>{0, 2, 3}</td>
<td>2</td>
<td>10</td>
</tr>
<tr>
<td>3</td>
<td>{0, 2, 3}</td>
<td>2</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td>{0, 2, 3}</td>
<td>2</td>
<td></td>
</tr>
<tr>
<td>1</td>
<td>{0, 1, 3}</td>
<td>3</td>
<td>11</td>
</tr>
<tr>
<td>2</td>
<td>{0, 1, 2}</td>
<td>0</td>
<td>12</td>
</tr>
<tr>
<td>0</td>
<td>{0, 1, 2}</td>
<td>0</td>
<td></td>
</tr>
<tr>
<td>7</td>
<td>{7, 1, 2}</td>
<td>1</td>
<td>13</td>
</tr>
<tr>
<td>0</td>
<td>{7, 0, 2}</td>
<td>2</td>
<td>14</td>
</tr>
<tr>
<td>1</td>
<td>{7, 0, 1}</td>
<td>7</td>
<td>15</td>
</tr>
</tbody></table>
<p>최종적으로 Page Fault는 15번 발생한다. </p>
<p>예제를 보면 알 수 있듯, 직전에 내보낸 페이지가 다시 필요한 경우가 있다. 단순히 처음 들어온 페이지를 Page-out하는 구조이기 때문에 이와 같은 경우 비효율적이라는 것을 알 수 있다. </p>
<h3 id="212-beladys-anomaly">2.1.2. Belady’s Anomaly</h3>
<p>일반적으로 메인 메모리의 용량이 작을수록 Page Fault가 더 자주 발생할 가능성이 크다. 메모리에 적재할 수 있는 페이지의 수가 제한적이기에 필요한 페이지를 자주 교체해야 하기 때문이다.</p>
<p>그러나 FIFO(First-In, First-Out)를 사용할 때, <strong>메모리의 프레임 수가 증가함에 따라 페이지 폴트 횟수가 오히려 증가하는 현상</strong>이 발생할 수 있다. 이 현상을 <strong>Belady&#39;s Anomaly</strong>라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/adba3466-535e-40bf-9bf5-8a01b8cb1477/image.png" alt=""></p>
<p>Belady&#39;s Anomaly는 FIFO 알고리즘의 특성상 발생하는데, 이는 페이지 참조열이 특정한 패턴을 가질 때 주로 나타난다. </p>
<p>e.g. 페이지 참조열이 순환적이거나 일정한 패턴을 반복하는 경우, FIFO는 초기의 오래된 페이지를 반복적으로 교체하며 더 많은 프레임을 할당받았음에도 불구하고 PF가 증가한다.</p>
<p>이러한 이유로 FIFO는 단순한 알고리즘이지만, 모든 상황에서 최적의 성능을 보장하지는 않는다.</p>
<h2 id="22-optimalopt">2.2. Optimal(OPT)</h2>
<p>위에서 언급한 FIFO의 문제점들을 해결하기 위해, <strong>앞으로 가장 오랫동안 사용되지 않을 페이지를 victim으로 선택하는 방법</strong>이 있다. 이 방식을 사용하면 동일한 페이지 참조열과 동일한 프레임 개수에서 Page Fault가 9번 발생하게 된다.</p>
<p>그러나 이 방식은 unrealistic하다. 실제 컴퓨터가 실행될 때, 앞으로 어떤 페이지가 사용되지 않을지 미래를 알 수 없기 때문이다. 따라서 알고리즘으로는 설명할 수 있으나 현실에서 적용하기는 어렵다. </p>
<p>cf) <a href="https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81">CPU 스케줄링의 Shortest Job First(SJF) 알고리즘</a></p>
<p>이를 통해 우리는 앞으로 사용되지 않을 페이지를 예측하는 것이 중요하다는 것을 알 수 있다. 해당 아이디어를 가져가면서 realistic한 방법은 없을까?</p>
<h2 id="23-least-recently-usedlru">2.3. Least-Recently-Used(LRU)</h2>
<p>미래를 정확히 예측할 수는 없지만 과거를 보면 대략 짐작할 수는 있다. 과거에 시험을 잘 본 사람은 앞으로의 시험을 잘 볼 확률이 높듯 말이다.</p>
<p>LRU는 <strong>최근에 가장 오랫동안 사용되지 않은 페이지를 victim으로 설정하는 방식</strong>이다. 아이디어는 최근에 사용되지 않았으면 앞으로도 사용될 일이 적을 것이라는 가정이다. 단, ‘최근’의 일이어야만 영향을 미친다고 본다. 유치원 때 공부를 잘했다고 성인이 되어서도 반드시 공부를 잘하는 것은 아닌 것과 비슷하다.</p>
<p>이 방식을 사용하면 동일한 페이지 참조열과 동일한 프레임 개수에서 Page Fault가 12번 발생하게 된다. OPT보다는 많고, FIFO 보다는 적다. OPT는 가장 이상적인 방법이나 적용이 어렵고, FIFO는 가장 간단하나 성능이 좋지 않고 메모리가 증가헀음에도 PF가 증가하는 기현상이 있어 대부분의 컴퓨터는 LRU를 채택한다.</p>
<h2 id="24-교체-방식">2.4. 교체 방식</h2>
<h3 id="241-global-replacement">2.4.1. Global replacement</h3>
<p>메모리 상의 <strong>모든 프로세스를 대상으로 하여 victim을 선택</strong>하는 방법이다. 모든 프로세스의 메모리 자원을 공유하는 방식으로, 특정 프로세스의 페이지 폴트가 발생할 때 다른 프로세스의 페이지를 희생양으로 선택할 수 있다.</p>
<h3 id="242-local-replacement">2.4.2. Local Replacement</h3>
<p><strong>특정 프로세스의 메모리 공간에서만 victim을 선택</strong>하는 방법이다. 각 프로세스가 할당받은 메모리 내에서만 페이지 교체가 이루어지도록 제한한다.</p>
<p>e.g. $P_1$의 5번 페이지에서 Page Fault가 발생했을 때, $P_1$의 페이지들에 한정하여 FIFO, LRU 등과 같은 알고리즘을 적용해 victim을 선택한다.</p>
<blockquote>
<h4 id="💡-global-vs-local-replacement">💡 Global vs Local Replacement</h4>
</blockquote>
<ul>
<li>Global replacement가 대개 더 효율적이다. 이는 모든 프로세스의 페이지를 대상으로 선택할 수 있기 때문에, 시스템 전체적으로 최적의 victim을 선택할 가능성이 높아지기 때문이다. <blockquote>
</blockquote>
</li>
<li>반면, Local replacement는 특정 프로세스의 페이지에만 제한되기 때문에, 선택의 폭이 좁아져 전체 시스템 성능이 저하될 수 있다.<blockquote>
</blockquote>
정리하자면, Global replacement는 시스템 자원의 전반적인 효율성을 높이는 반면, Local replacement는 각 프로세스의 독립성을 보장하는데 더 유리하다.</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
<li><a href="https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-15.-%EA%B0%80%EC%83%81%EB%A9%94%EB%AA%A8%EB%A6%AC">[운영체제(OS)] 15. 가상메모리</a></li>
<li><a href="https://www.naukri.com/code360/library/belady-s-anomaly-in-os">What is Belady&#39;s Anomaly in Operating System</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 페이징과 세그먼테이션]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%8E%98%EC%9D%B4%EC%A7%95%EA%B3%BC-%EC%84%B8%EA%B7%B8%EB%A8%BC%ED%85%8C%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%8E%98%EC%9D%B4%EC%A7%95%EA%B3%BC-%EC%84%B8%EA%B7%B8%EB%A8%BC%ED%85%8C%EC%9D%B4%EC%85%98</guid>
            <pubDate>Wed, 06 Nov 2024 01:38:03 GMT</pubDate>
            <description><![CDATA[<h1 id="1-페이징paging">1. 페이징(Paging)</h1>
<p>코끼리를 냉장고에 넣으려면 어떻게 해야할까? (잔인하지만) 코끼리를 잘라서 냉장고에 넣으면 된다.</p>
<p>우리는 이전까지 프로세스는 메모리 공간에 연속적으로 존재해야 한다고 생각했다. <strong>하지만 꼭 연속적으로 존재해야 할까?</strong> 페이징은 이런 발상의 전환으로부터 등장했다.</p>
<p>프로세스를 일정한 크기(=페이지)로 잘라서 메모리에 올리는 것을 <strong>페이징</strong>이라고 한다.</p>
<p>그런데 프로세스가 잘라져 있어도 실행될 수 있을까? CPU가 프로세스가 연속적으로 존재한다고 착각하게 만들면 된다. MMU의 재배치 레지스터를 여러 개 사용해 아래 그림과 같이 각 페이지의 실제 주소로 변경해준다. 이런 목적으로 사용되는 MMU는 재배치 레지스터가 테이블과 같은 형태로 배치되어 <strong>페이지 테이블(Page Table)</strong>이라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/1b49920a-06c9-401d-9c6b-f2ad7fab3604/image.png" alt=""></p>
<p>프로세스를 자른 단위를 페이지, 메모리를 자른 단위를 프레임이라고 한다.</p>
<p>위의 그림에서는 프로세스 $P_1$을 5개의 페이지로 나누고, 이를 메인 메모리 5곳에 나누어 할당하였다. CPU는 논리 주소를 사용해 연속적인 주소값으로 명령을 내리지만, 메모리에 접근하기 전에 각 페이지의 실제 물리 주소를 찾아야 한다. 이를 위해 페이지 테이블이 사용되며, 이는 논리 주소를 해당 페이지의 물리 주소로 변환하는 역할을 한다.</p>
<h2 id="11-주소-변환address-translation">1.1. 주소 변환(Address Translation)</h2>
<p>CPU가 내는 주소를 논리 주소(Logical Address)라고 한다. 이 논리 주소를 메모리로 가는 물리 주소(Physical Address)로 바꿔줘야 한다.</p>
<p>앞서 언급한 바와 같이, CPU가 메모리의 여러 곳에 흩어진 페이지에 접근하기 위해서는 페이지의 물리 주소가 필요하다. 이를 어떻게 변환하는지 알아보자.</p>
<h3 id="111-논리-주소logical-address">1.1.1. 논리 주소(Logical Address)</h3>
<p>논리 주소는 2진수로 표현된다.전체 비트를 $m$이라고 했을 때, 하위 $n$비트는 오프셋(offset) 또는 변위(displacement)라고 한다. 상위 $m-n$비트는 페이지 번호다.$(n=d, m-n=p)$</p>
<h3 id="112-물리-주소physical-address">1.1.2. 물리 주소(Physical Address)</h3>
<p>물리 주소는 프레임 번호($f$)와 변위($d$)로 구성된다. 페이지 번호($p$)는 페이지 테이블의 인덱스 값이고, 변위는 변하지 않는다.</p>
<h3 id="113-예제">1.1.3. 예제</h3>
<blockquote>
<p>Page size = 4bytes, Page Table: 5 6 1 2일 때, 논리 주소 13번지는 물리 주소 몇 번지인가?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/f4a6a062-64b2-42fc-9be1-35418ce98e5d/image.jpeg" alt=""></p>
<p>$13=1101_2$이고, $4=2^2$이므로 $d=2$이다. 따라서 뒤의 두 칸이 $d$를 가리키고, $p$는 $d$를 제외한 나머지 크기이다.</p>
<pre><code>13 = 1101
p = 11
d = 01</code></pre><p>$p=11_2=3$이므로 페이지 번호 3번을 가리킨다. 페이지 3번에 해당하는 프레임 번호는 2번이므로, 물리 주소를 구성하는 $f$값은 2가 된다.</p>
<pre><code>f = 10
d = 01
물리 주소 = 1001</code></pre><p>이처럼 프로세스를 흩어지게 함으로써 더이상 외부 단편화는 발생하지 않게 되나, 내부 단편화라는 문제가 생긴다.</p>
<h2 id="12-내부-단편화internal-fragment">1.2. 내부 단편화(Internal Fragment)</h2>
<p>페이지 사이즈가 4byte이고, 프로세스 크기는 15byte라고 하면 페이지는 4개가 필요하다. 마지막 페이지는 크기를 다 채우지 못하고 1byte가 남게 된다.</p>
<p>프로세스 크기가 페이지 크기의 배수가 아닐 때 마지막 페이지가 한 프레임을 다 채울 수 없는 것을 내부 단편화라고 한다. 공간이 남는다는 것은 곧 메모리 낭비를 의미한다.</p>
<p>하지만 내부 단편화로 낭비되는 메모리의 최대 크기는 <code>(page size - 1)byte</code>쯤 된다. 이는 매우 미미한 수준이어서 크게 문제가 되지 않는다.</p>
<h2 id="13-페이지-테이블-만들기">1.3. 페이지 테이블 만들기</h2>
<p>CPU와 메모리 사이에는 페이지 테이블이 있는데, 이를 만드는 방법은 여러 가지가 있다.</p>
<h3 id="131-cpu-레지스터">1.3.1. CPU 레지스터</h3>
<p>MMU를 CPU 안에 넣는 방법이다. 페이지 테이블은 프레임 넘버를 기억하기 때문에 일종의 기억장치이다. 따라서 CPU 안의 기억장치인 레지스터로 페이지 테이블을 만들 수 있다.</p>
<p>CPU 안에 있기 때문에 주소 변환이 매우 빠르다는 장점이 있으나, 많이 넣을 수는 없다는 단점이 있다.</p>
<h3 id="132-메모리">1.3.2. 메모리</h3>
<p>MMU를 메인 메모리 안에 넣는 방법이다. 메모리는 용량이 매우 크므로 페이지 테이블의 엔트리 개수가 아무리 많아도 넣기 용이하다. </p>
<p>하지만 주소 변환 속도가 느리다는 단점이 있다. CPU가 주소를 내면 그 주소는 OS 영역 안에서 페이지 테이블을 한 번 읽고, 읽고 나서 나온 프레임 넘버에 따라 물리 주소로 가야하기 때문이다.</p>
<h3 id="133-tlbtranslation-look-aside-buffer">1.3.3. TLB(Translation Look-aside Buffer)</h3>
<p>MMU를 캐시 메모리로 만드는 방법으로, 실제로 가장 많이 사용된다. 별도의 high speed SRAM 칩을 만들어 주소 변환만을 위해 이용하는 걸 TLB라고 부르며, 원리는 캐시 메모리와 동일하다.</p>
<p>이 방식은 CPU 레지스터로 만드는 방법이나 메모리로 만드는 방법과 테이블 엔트리 개수, 변환 속도 측면에서 성능을 비교할 수 있다.</p>
<p>e.g. TLB 사용 시 유효 메모리 접근 시간(Effective Memory Access Time) 구하기</p>
<p>메모리를 읽는 데 걸리는 시간 $T_m=100ns$, TLB를 읽는 시간 $T_b=20ns$, 어떤 주소가 페이지 테이블 엔트리에 있을 확률 hit ratio = 80%</p>
<p>$T_e=h(T_b+T_m)+$$(1-h)(T_b+T_m+T_m)$$=0.8<em>120+0.2</em>220=140ns$</p>
<p>이는 $T_m=100ns$보다 40% 느려진 시간이나, 실제로는 hit ratio가 95% 이상에 달하기 때문에 외부 단편화 해결을 위해 감수해야 하는 부분이라고 할 수 있다.</p>
<h2 id="14-보호와-공유">1.4. 보호와 공유</h2>
<h3 id="141-보호protection">1.4.1. 보호(Protection)</h3>
<p>운영체제가 해야하는 중요한 일 중 하나가 보호이다. CPU가 내는 모든 주소는 페이지 테이블을 경유하므로 해당 페이지에 대한 접근 제어가 가능하다.</p>
<p>페이지 테이블은 엔트리마다 r(read), w(write), x(execute) 비트를 두어 해당 비트의 값에 따라 수행 여부를 결정한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/82d99cde-27c7-4dcf-ab00-15f6c6f06213/image.png" alt=""></p>
<p>e.g. 2번 엔트리에 읽기 비트가 꺼져있는데 읽기 작업을 시도하면 CPU에 인터럽트가 발생하여 ISR에서 강제로 해당 프로세스를 종료시킨다.</p>
<h3 id="142-공유sharing">1.4.2. 공유(Sharing)</h3>
<p>하나의 프로그램이 실행될 때 code, stack, data가 필요하다. 같은 프로그램을 사용하는 복수 개의 프로세스 $P_A, P_B, P_C$가 있다고 해보자. 이들은 각각 독립적인 프로세스이기 때문에 data가 다르고, stack과 같이 실행 시점에 따라 달라지는 지역 변수 등도 다를 것이다.</p>
<p>하지만 같은 프로그램이기 때문에 code 영역은 동일하다. 동일한 값을 중복되게 메모리에 띄우는 것은 명백한 낭비이다. 따라서 같은 프로그램을 쓰는 프로세스가 있다면 code 영역을 공유하여 컨텍스트 스위치가 될 때마다 같은 곳을 가리키게 한다.</p>
<p>단, 코드가 실행되는 동안 스스로 내용을 바꾸지 않는다는 전제가 있어야 한다. 이는 non-self-modifying code = reentrant code(재진입가능 코드) = pure code라고 한다.</p>
<h1 id="2-세그멘테이션segmentation">2. 세그멘테이션(Segmentation)</h1>
<p>코끼리를 냉장고에 넣을 때 그냥 일정 크기로 자르는 방법도 있고, 부위별로 자르는 방법이 있다.</p>
<p>앞서 우리는 프로세스를 일정한 크기에 따라 잘랐다. 세그멘테이션은 프로세스를 논리적 내용, 세그먼트 단위로 잘라 메모리에 올리는 것을 말한다. 이때문에 세그먼트의 크기는 일정하지 않다.</p>
<h2 id="21-주소-변환address-translation">2.1. 주소 변환(Address Translation)</h2>
<p>세그멘테이션을 위한 테이블을 <strong>세그먼트 테이블</strong>이라고 한다. 세그먼트 테이블은 세그먼트 번호, base, limit으로 구성되어 있다. </p>
<p>세그먼트 번호($s$)는 세그먼트 테이블 인덱스 값을 의미한다. Base는 시작 번지, 즉 해당 세그먼트가 메모리 몇 번지에 있는지를 의미한다. Limit은 세그먼트 크기를 의미한다.</p>
<p>세그먼트의 주소 변환 역시 페이징 주소 변환과 유사하나, 세그먼트의 크기는 일정하지 않기 때문에 테이블에 limit 정보가 주어진다. CPU에서 해당 세그먼트의 크기를 넘어서는 주소가 들어오면 인터럽트가 발생해 해당 프로세스를 강제로 종료시킨다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a86aabf5-41bb-499c-abce-674c239af009/image.png" alt=""></p>
<p>위 그림은 세그먼트 테이블과 프로세스가 할당된 메모리의 모습이다. 페이징 주소 변환과 동일하게 $d$는 논리 주소와 물리 주소가 동일하다. 물리 주소는 $base[s]+d$로 계산된다.</p>
<p>e.g. </p>
<ul>
<li>논리 주소 (2, 100) ⇒ 물리 주소 4400번지</li>
<li>논리 주소 (1, 500) ⇒ 인터럽트로 인해 프로세스 강제 종료(잘못된 세그먼트 violation)</li>
</ul>
<h2 id="22-보호와-공유">2.2. 보호와 공유</h2>
<p>세그멘테이션은 보호와 공유 측면에서 페이징보다 더 나은 방법이다. 핵심은 세그멘테이션은 프로세스를 기능 단위로 자른다는 점이다.</p>
<h3 id="221-보호protection">2.2.1. 보호(Protection)</h3>
<p>모든 주소는 세그먼트 테이블을 경유하므로 세그먼트 테이블 엔트리마다 r, w, x 비트를 두어 해당 세그먼트에 대한 접근을 제어한다. </p>
<p>프로세스를 자를 때에 페이징은 그저 일정 크기로 자르기 때문에 예를 들어 어떤 페이지에는 code가 있고, 다음 페이지에는 data+code가 있고, 다음 페이지에는 code+stack이 있다.</p>
<p>반면 세그멘테이션은 기능별로 자르기 때문에 섞일 일이 없다. Data는 읽고 쓰되 실행은 하면 안 될 것이고, 어떤 것은 읽기만 하고 실행과 쓰기를 하면 안 된다. 그런데 페이징처럼 어중간하게 잘리면 r, w, x 비트를 어떻게 설정할지 곤란해진다.</p>
<p>따라서 보호의 측면에서 프로세스를 논리적으로 의미가 있는 구분으로 자르는 것이 더 나은 방식이다.</p>
<h3 id="222-공유sharing">2.2.2. 공유(Sharing)</h3>
<p>같은 프로그램을 돌리는 복수의 프로세스가 있을 때, 세그먼트 테이블의 code 영역이 같은 곳을 가리키게 하면 된다. 세그먼트는 확실하게 code 영역을 자르기 때문에, 페이징과 다르게 code 영역이 확실히 분리된다. 이때문에 공유의 측면에서도 세그멘테이션은 페이징보다 우월하다.</p>
<p>그럼에도 대부분의 운영체제는 페이징을 사용한다. 왜 그런 것일까?</p>
<h2 id="23-외부-단편화external-fragment">2.3. 외부 단편화(External Fragment)</h2>
<p>1.1에서 설명한 것과 같이, 다중 프로그래밍에서는 프로세스가 실행되고 종료됨에 따라 메모리에 hole이 생긴다. 이때문에 남는 메모리 공간 자체는 충분함에도 프로세스를 할당하지 못하는 외부 단편화 문제가 발생한다.</p>
<p>세그멘테이션에서도 똑같은 문제가 발생하는데, 이는 세그먼트가 논리적인 단위로 나누어져 크기가 상이하기 때문이다. 이로 인해 다양한 크기의 hole이 발생해 <strong>외부 단편화 문제가 또다시 나타난다.</strong></p>
<h1 id="3-세그멘테이션과-페이징">3. 세그멘테이션과 페이징</h1>
<p>이러한 문제 때문에 세그먼트를 페이징하는 방식이 나타났다.</p>
<p>프로세스를 자를 때 처음에는 세그먼트로 자른다. 그런데 이를 바로 메인 메모리에 올리면 외부 단편화 문제가 발생할 수 있기 때문에 세그먼트를 다시 일정 간격, 즉 페이지 단위로 자른다. 이를 Paged Segmentation이라고 한다.</p>
<p>이를 통해 우리는 두 가지 방식의 장점을 둘 다 가져갈 수 있게 되었다. 그런데 이는 주소 변환에 있어 시간적 부담이 늘어난다는 단점이 있다. 처음 주소가 세그먼트 테이블로 가고, 그 다음 페이징 테이블로 가서 단계가 하나 늘어났기 때문이다.</p>
<p>Paged Segmentation으로 보호와 공유 측면, 외부 단편화 문제에서도 더 뛰어난 성능을 가져갈 수 있게 되었으나, 이러한 이점에는 시간이 더 오래 걸린다는 trade-off가 따른다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
<li><a href="https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-13.-%ED%8E%98%EC%9D%B4%EC%A7%95">[운영체제(OS)] 13. 페이징</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 세마포어와 뮤텍스]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4%EC%99%80-%EB%AE%A4%ED%85%8D%EC%8A%A4</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4%EC%99%80-%EB%AE%A4%ED%85%8D%EC%8A%A4</guid>
            <pubDate>Tue, 05 Nov 2024 02:33:30 GMT</pubDate>
            <description><![CDATA[<p>메모리 안에 있는 프로세스들은 대개 독립적이지 않고 협조하는 관계다. 서로에게 영향을 미치고, 영향을 받는데 이는 <strong>공통된 자원</strong>을 서로 접근하기 때문이다. 메인 메모리에 여러 프로세스가 동시에 존재하기 때문에, <strong>프로세스 간 동기화 문제</strong>는 점점 더 중요해지고 있다.</p>
<h1 id="1-동기화-도구">1. 동기화 도구</h1>
<h2 id="11-세마포어semaphore">1.1. 세마포어(Semaphore)</h2>
<blockquote>
<p>n. (철도의) 까치발 신호기, 시그널; U (군대의) 수기 신호
OS에서의 세마포어란, 동기화 문제를 해결하기 위한 소프트웨어 툴로, 역사가 굉장히 오래되었다.</p>
</blockquote>
<p>세마포어는 <strong>정수형 변수</strong>와 <strong>두 개의 동작(P, V)</strong>으로 구성되어 있다. 
P는 Proberen(test), 정수 값을 테스트한다는 의미이고 <code>acquire()</code>로 사용한다. 
V는 Verhogen(increment), 정수 값을 증가시킨다는 의미이고 <code>release()</code>로 사용한다.</p>
<pre><code class="language-java">class Semaphore {
    int value; // number of permits

    Semaphore(int value) {
        ...
    }

    void acquire() {
        value--;
        if (value &lt; 0) {
            add this process/thread to list;
            block;
    }

    void release() {
        value++;
        if (value &lt;= 0) {
            remove a process P from list;
            wakeup P;
    }
}</code></pre>
<p>Stack에 <code>push</code>, <code>pop</code> 동작이 있듯, 세마포어에는 <code>acquire</code>, <code>release</code> 동작이 있다.</p>
<p><code>acquire</code>는 정수 값을 1만큼 감소시키고, 이 값이 0보다 작으면 이 <code>acquire</code>를 호출한 프로세스(혹은 스레드)를 세마포어의 큐에 집어 넣는다. 이 프로세스는 누가 꺼내주기 전까지 block 상태이다.</p>
<p><code>release</code>는 정수 값을 1만큼 증가시키고, 이 값이 0보다 작거나 크면 세마포어 큐 안에 어떤 프로세스가 갇혀 있는 것을 의미한다. 따라서 해당 프로세스를 wakeup하여 세마포어 큐에서 해방시킨다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/ea56a7af-b252-40a4-a03e-845719a65158/image.jpeg" alt=""></p>
<p>세마포어의 일반적 사용에 대한 예시를 보며 이해해 보자.</p>
<h3 id="111-mutual-exclusion">1.1.1. Mutual exclusion</h3>
<p>한 프로세스가 임계구역으로 들어가면 다른 프로세스는 들어가지 못한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/5a445cdf-d1cb-490b-8120-50dca32683d4/image.png" alt=""></p>
<p>세마포어의 초기값이 1이라고 해보자. 이 세마포어에 대해 <code>acquire</code>를 호출하면 value는 0이 되고, 해당 프로세스는 임계구역으로 진입한다.</p>
<p>이후 컨텍스트 스위치가 일어나 다른 프로세스가 <code>acquire</code>를 호출했다고 해보자. value는 0보다 작기 때문에 이 프로세스는 세마포어의 큐에 갇힌다. 즉, 임계구역으로 진입하지 못한다.</p>
<pre><code class="language-java">// Parent

sem.acquire();
/////////////////////////
int temp = balance + n;
System.out.print(&quot;+&quot;);
balance = temp;
/////////////////////////
sem.release();</code></pre>
<pre><code class="language-java">// Child

sem.acquire();
/////////////////////////
int temp = balance - n;
System.out.print(&quot;-&quot;);
balance = temp;
/////////////////////////
sem.release();</code></pre>
<p>여러 자원이 동시에 사용 가능한 경우는 어떨까? </p>
<p>세마포어 값은 1 이상이 될 수 있으며, 이때는 여러 프로세스가 동시에 자원에 접근할 수 있다. 이런 상황에서는 자원 간 경합이나 데이터 불일치 문제가 발생할 수 있기 때문에, <strong>Strict Mutual Exclusion(엄격한 상호 배제)</strong>이 요구되는 경우에는 세마포어만으로는 해결이 어렵다.</p>
<h3 id="112-ordering">1.1.2. Ordering</h3>
<p>세마포어의 초기값이 0이라고 해보자.</p>
<table>
<thead>
<tr>
<th>$P_1$</th>
<th>$P_2$</th>
</tr>
</thead>
<tbody><tr>
<td></td>
<td>sem.acquire()</td>
</tr>
<tr>
<td>$S_1$;</td>
<td>$S_2$;</td>
</tr>
<tr>
<td>sem.release();</td>
<td></td>
</tr>
</tbody></table>
<p>$P_1→P_2$의 순서로 프로세스를 실행시키고 싶다. $P_1$부터 실행시키면 우리가 원하는 대로 될 것이고, 공교롭게도 CPU가 $P_2$를 먼저 돌린다고 하더라도 <code>acquire</code>을 호출하여 $P_2$는 세마포어 큐에 갇히게 된다. 그렇게 되면 $S_2$로 넘어가지 못하고 컨텍스트 스위치가 일어나 $P_1$이 실행된다. 이후 $P_1$이 <code>release</code>를 호출해 value를 증가시키고 $P_2$가 깨어나게 된다.</p>
<table>
<thead>
<tr>
<th>$P_1$</th>
<th>$P_2$</th>
</tr>
</thead>
<tbody><tr>
<td></td>
<td>sem.acquire()</td>
</tr>
<tr>
<td>$S_1$;</td>
<td>$S_2$;</td>
</tr>
<tr>
<td>wsem.release();</td>
<td>dsem.release();</td>
</tr>
<tr>
<td>dsem.acquire();</td>
<td></td>
</tr>
</tbody></table>
<p>세마포어를 따로 두고 하나가 풀리면 하나를 가두는 것을 반복하면 두 프로세스를 교대로 실행할 수 있다.</p>
<h3 id="113-특징">1.1.3. 특징</h3>
<p>이처럼 세마포어는 여러 프로세스가 <strong>동시에 자원에 접근하는 것을 제어</strong>하기 위해 사용된다. 예를 들어, 동시에 n개의 자원을 사용할 수 있는 환경에서는 세마포어가 유용하다. 또한 세마포어는 <strong>프로세스의 실행 순서를 제어</strong>하거나, 교대 작업을 수행할 때 유용하게 사용된다.</p>
<p>하지만 세마포어는 여러 자원을 관리할 수 있다는 점에서 유연했지만, 앞서 언급했듯 <strong>Strict Mutual Exclusion을 완벽히 보장하지는 못하는 한계</strong>가 있었다. 즉, 특정 자원에 오직 하나의 프로세스만 접근해야 할 경우를 안전하게 처리하기에는 부족했다. 이로 인해 더 엄격한 동기화가 필요하다는 요구가 생겨났고, 그 결과 <strong>뮤텍스</strong>가 등장하게 된다.</p>
<h2 id="12-뮤텍스mutex">1.2. 뮤텍스(Mutex)</h2>
<p>뮤텍스는 <strong>이진 잠금 장치</strong>로, <strong>단 하나의 프로세스 또는 스레드만이 임계구역에 접근할 수 있도록 보장</strong>한다. 이 방식은 자원에 대해 엄격한 배타적 접근을 요구하는 경우에 적합하다.</p>
<h3 id="121-동작-과정">1.2.1. 동작 과정</h3>
<ul>
<li><strong>Lock</strong>: 임계구역에 들어가기 전에 잠금을 설정하여 다른 프로세스가 접근하지 못하도록 한다.</li>
<li><strong>Unlock</strong>: 임계구역을 벗어나면 잠금을 해제하여 다른 프로세스가 자원에 접근할 수 있도록 한다.</li>
</ul>
<pre><code class="language-java">class Mutex {
    boolean isLocked = false;

    void lock() {
        while (isLocked) {
            wait;  // 잠겨 있으면 대기
        }
        isLocked = true;  // 잠금 설정
    }

    void unlock() {
        isLocked = false;  // 잠금 해제
        notify();  // 기다리는 스레드를 깨움
    }
}</code></pre>
<p>뮤텍스는 자원을 소유한 프로세스만이 <code>unlock()</code>을 호출할 수 있다는 점에서, 세마포어와 구분된다. 세마포어는 누가 <code>release()</code>를 호출해도 세마포어 값을 증가시킬 수 있지만, 뮤텍스는 자원을 점유한 프로세스만이 잠금을 해제할 수 있는 <strong>소유권 개념</strong>이 있다.</p>
<h3 id="122-특징">1.2.2. 특징</h3>
<p>뮤텍스는 CPU 자원을 절약할 수 있는 장점이 있다. 자원이 사용 중일 때 스레드가 뮤텍스 큐에 들어가 대기 상태로 전환되기 때문에, <strong>CPU 사이클을 소모하지 않는다.</strong> 이는 특히 멀티 코어 시스템에서 스레드가 자주 대기해야 하는 상황에서 유리하다.</p>
<p>반면 스레드를 대기 상태로 전환하고 다시 실행 상태로 전환할 때, <strong>컨텍스트 스위치</strong>가 발생하며 이 과정에서 성능 <strong>오버헤드</strong>가 발생할 수 있다는 단점이 있다.</p>
<blockquote>
<h4 id="❓-뮤텍스-큐에서-임계구역이-해제되면-순서가-된-프로세스가-cpu-점유권을-즉시-갖게-될까"><strong>❓ 뮤텍스 큐에서 임계구역이 해제되면, 순서가 된 프로세스가 CPU 점유권을 즉시 갖게 될까?</strong></h4>
</blockquote>
<p>일반적으로 뮤텍스 큐에서 임계구역이 해제되면 그 큐에서 대기 중이던 프로세스 또는 스레드는 <strong>즉시 실행 상태(ready state)</strong>로 전환된다. 하지만 실행 상태로 전환된다고 해서 <strong>바로 CPU 점유권을 강제로 가져가지는 않는다.</strong> CPU를 실제로 점유하기 되는지 여부는 스케줄러에 의해 결정된다.</p>
<blockquote>
</blockquote>
<p><strong>선점형 스케줄링</strong>의 경우 프로세스가 높은 우선순위를 갖고 있다면, 현재 CPU에서 실행 중인 스레드를 선점하고 <strong>CPU를 획득</strong>한다. <strong>비선점형 스케줄링</strong>의 경우 임계구역이 해제되어도 즉시 CPU를 획득하지 못하고, 현재 CPU를 점유하고 있는 프로세스의 <strong>작업이 끝날 때까지 대기</strong>하게 된다.</p>
<blockquote>
</blockquote>
<p>만약 매번 뮤텍스 해제로 인해 강제로 CPU가 선점된다면, 컨텍스트 스위칭이 너무 자주 발생하여 성능 문제가 생길 수 있다. 이를 방지하기 위해 운영체제는 우선순위 기반 스케줄링, 타임 슬라이스 제어 등 다양한 기법을 사용해 효율적인 CPU 점유를 보장한다.</p>
<blockquote>
</blockquote>
<p>⇒ 정리하자면, 뮤텍스 해제 시 <strong>순서가 된 프로세스가 반드시 CPU를 강제로 점유하지는 않으며, 스케줄러가 적절한 시점에 프로세스를 실행</strong>하게 된다. 이로써 불필요한 컨텍스트 스위칭을 줄이면서도 효율적인 자원 할당이 이루어진다.</p>
<h3 id="123-스핀락-vs-뮤텍스">1.2.3. 스핀락 vs 뮤텍스</h3>
<p><strong>스핀락(Spinlock)</strong>은 뮤텍스와 유사하게 임계구역을 보호하기 위한 락이지만, 자원을 점유하고 있는 스레드가 해제될 때까지 <strong>계속해서 반복적으로 확인(스핀)</strong>한다. 즉, 자원이 사용 중일 때 자원을 기다리는 스레드는 대기 상태로 전환되지 않고, 그 자원이 풀릴 때까지 CPU 사이클을 소모하며 자원을 획득하려는 시도를 계속 반복한다. 이를 <strong>바쁜 대기(busy waiting)</strong>라고 부르기도 한다.</p>
<p>스핀락은 자원을 기다리는 동안 CPU 자원을 계속 소모하지만, 락을 짧은 시간 내에 해제할 수 있는 경우에는 뮤텍스보다 성능이 좋을 수 있다. 스핀락은 짧은 임계구역이나 컨텍스트 스위치가 비용이 큰 환경에서 효과적이다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>뮤텍스</th>
<th>스핀락</th>
</tr>
</thead>
<tbody><tr>
<td>대기 방식</td>
<td>대기 상태로 블록됨</td>
<td>계속 반복하여 자원 획득 시도(바쁜 대기)</td>
</tr>
<tr>
<td>CPU 자원 소모</td>
<td>대기 중에 CPU 자원을 소모하지 않음</td>
<td>자원이 사용 중일 때 CPU 자원을 계속 소모</td>
</tr>
<tr>
<td>컨텍스트 스위치</td>
<td>컨텍스트 스위치가 발생하여 성능 오버헤드 발생</td>
<td>컨텍스트 스위치 없음</td>
</tr>
<tr>
<td>자원 획득 시간</td>
<td>긴 대기 시간에 적합</td>
<td>짧은 대기 시간에 적합</td>
</tr>
<tr>
<td>적합한 환경</td>
<td>대기 시간이 길거나 자원이 자주 점유되는 상황</td>
<td>짧은 임계구역 또는 컨텍스트 스위치 비용이 큰 상황</td>
</tr>
</tbody></table>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 경쟁 상태(Race Condition)]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B2%BD%EC%9F%81-%EC%83%81%ED%83%9CRace-Condition</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B2%BD%EC%9F%81-%EC%83%81%ED%83%9CRace-Condition</guid>
            <pubDate>Mon, 04 Nov 2024 05:16:39 GMT</pubDate>
            <description><![CDATA[<h1 id="1-프로세스-동기화">1. 프로세스 동기화</h1>
<p>현대 운영체제에서는 보통 스위칭이 스레드 단위로 일어나기 때문에 정확히는 Thread synchronization이라고 할 수 있다.</p>
<p>프로세스는 일반적으로 두 부류로 나눌 수 있는데 하나는 <strong>Independent</strong> 프로세스이다. 프로세스 간에 아무런 관계가 없다면 independent하다고 볼 수 있다. 반면 어떤 형태로든 두 프로세스가 영향을 주고 받는다면 <strong>Cooperating</strong> 프로세스이다.</p>
<p>대부분의 프로세스는 사실 협력 관계에 해당되는데, 예를 들어 명절 기차표 예약, 수강신청을 생각해 보면 여러 사용자가 같은 데이터에 접근을 해 서로 간에 영향을 준다. </p>
<p>이처럼 공유된 데이터에 동시 접근이 발생할 때, 데이터에는 inconsistency가 야기될 수 있는데, 이를 <strong>경쟁 상태(Race Condition)</strong>라고 한다. 프로세스 동기화는 협력 관계에 놓인 프로세스에 순서를 정해 실행하여, 데이터 consistency를 유지하는 것이다.</p>
<h2 id="11-bankaccount-problem">1.1. BankAccount Problem</h2>
<p>부모님이 은행 계좌에 입금을 하고, 자녀는 출금하는 상황을 생각해 보자.
입금(deposit)과 출금(withdraw)은 독립적으로 일어난다.</p>
<pre><code class="language-java">class BankAccount {
    int balance;

    void deposit(int amount) {
        balance += amount;
    }

    void withdraw(int amount) {
        balance -= amount;
    }

    int getBalance() {
        return balance;
    }
}</code></pre>
<pre><code class="language-java">class Parent extends Thread {
    BankAccount b;

    Parent(BankAccount b) {
        this.b = b;
    }

    public void run() {
        for (int i = 0; i &lt; 100; i++) {
            b.deposit(1000);
        }
    }
}</code></pre>
<pre><code class="language-java">class Child extends Thread {
    BankAccount b;

    Child(BankAccount b) {
        this.b = b;
    }

    public void run() {
        for (int i = 0; i &lt; 100; i++) {
            b.withdraw(1000);
        }
    }
}</code></pre>
<pre><code class="language-java">class Test {
    public static void main(String[] args) 
        throws InterruptedException {
        BankAccount b = new BankAccount();
        Parent p = new Parent(b);
        Child c = new Child(b);
        p.start();
        c.start();
        p.join();
        c.join();
        System.out.println(&quot;Balance = &quot; + b.getBalance());
    }
}</code></pre>
<p>위의 파일을 실행해 보면 <code>balance</code>는 0원으로 출력이 된다.</p>
<pre><code>Balance = 0</code></pre><p>여기에 시간 지연을 주면 어떻게 될까?</p>
<pre><code class="language-java">class BankAccount {
    int balance;

    void deposit(int amount) {
        int temp = balance + amount;
        System.out.println(&quot;+&quot;);
        balance = temp;
    }

    void withdraw(int amount) {
        int temp = balance - amount;
        System.out.println(&quot;-&quot;);
        balance = amount;
    }

    int getBalance() {
        return balance;
    }
}</code></pre>
<p><code>deposit</code>, <code>withdraw</code> 함수에 <code>temp</code> 변수를 따로 선언하여 <code>balance</code> 값을 업데이트 하도록 하고, “+”, “-”를 출력하도록 하여 입출금 동작에 시간 지연을 추가했다.</p>
<pre><code>++--+++++++++++++++++++++++++-------------------------++++++++++-----++
-------+++++++-----------------++++++++--------------------------+-----
...
...Balance = 1000000</code></pre><p>실행 결과는 실행할 때마다 다르다. 0원보다 많게 나올 때도 있고, 0원보다 적게 나올 때도 있다. 상식적으로는 똑같이 1000원을 100번 입금하고, 100번 출금했는데 왜 0원이 나오지 않을까?</p>
<p>우리는 <code>balance</code>라는 공통 변수를 이용하는데 이를 동시에 업데이트 하려고 해서 문제가 생긴 것이다. High level language로는 한 줄이지만 low level language로는 여러 줄인데, 공통 변수가 업데이트되는 도중에 스위칭이 일어난 것이다.</p>
<pre><code>LDR r0, [balance]
LDR r1, [amount]
ADD r0, r0, r1
STR r0, [balance]</code></pre><p>이를 해결하기 위해서는 위 네 개의 명령이 atomic하게, 즉 분해되지 않고 한 번에 처리되어야 한다. 만약 3번째의 <code>ADD</code> 명령어를 수행하다가 문맥 전환이 일어나서 <code>balance</code>의 값이 원래 값과 달라지면 위에서 설명한 상황처럼 데이터의 일관성이 깨지게 된다.</p>
<p>이처럼 둘 이상의 스레드가 동시에 접근해서는 안 되는 공유 자원(예제에서는 balance)을 접근하는 코드의 일부를 <strong>임계구역</strong>이라고 한다.</p>
<h1 id="2-임계구역-문제the-critical-section-problem">2. 임계구역 문제(The Critical-Section Problem)</h1>
<p>의미 그대로는 <strong>이 구간에서 무언가 치명적인(critical) 오류가 일어날 수 있다</strong>는 의미이다.</p>
<p>여러 개의 스레드로 이루어진 시스템에서, 각각의 스레드는 어떤 코드의 영역을 가지고 있는데 이를 critical section이라고 한다. 이 안에서 여러 스레드들은 공통 변수, 테이블, 파일을 수정한다.
1.1의 예시에서는 <code>balance</code>라는 공통 변수를 업데이트하는 부분을 임계구역이라고 한다.</p>
<p>우리가 만들어야 하는 <strong>임계구역 문제의 해결은 아래 세 가지가 전부 만족되어야 한다.</strong></p>
<ul>
<li><strong>Mutual exclusion(상호배타)</strong>: 공통 변수에 대한 업데이트는 상호배타적이어야 한다.<ul>
<li>e.g. <code>Parent</code>가 critical section 안에 들어가면, 즉 <code>balance</code>라는 공통 변수를 업데이트하면 <code>Child</code>는 이를 업데이트해서는 안 된다. 그 반대도 마찬가지다.</li>
</ul>
</li>
<li><strong>Progress(진행)</strong>: 임계구역에 어떤 스레드가 먼저 들어갈 것인지 결정하는 것은 유한 시간 내에 결정되어야 한다.</li>
<li><strong>Bounded waiting(유한대기)</strong>: 어느 스레드라도 기다리고 다른 스레드가 기다리고 있다면 그 스레드는 유한 시간 내에 임계구역에 들어갈 수 있다.</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] 데드락(Deadlock)]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%8D%B0%EB%93%9C%EB%9D%BDDeadlock</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%8D%B0%EB%93%9C%EB%9D%BDDeadlock</guid>
            <pubDate>Fri, 01 Nov 2024 01:57:15 GMT</pubDate>
            <description><![CDATA[<p>운영체제가 하는 핵심적인 일은 프로세스 관리이고, 그 중에서도 CPU 스케줄링과 동기화(Synchronization)를 가장 중요하게 다루어야 한다. 동기화를 하다 보면 간혹 데드락에 빠지는 일이 있는데 데드락에 대해 알아보자.</p>
<h1 id="1-교착상태deadlock">1. 교착상태(Deadlock)</h1>
<blockquote>
<p><strong>프로세스는 실행을 위해 여러 자원을 필요로 한다.</strong> (e.g. CPU, 메모리, 파일, 프린터, …) 
그러나 프로세스가 필요로 하는 자원은 보통 하나이고, 프로세스들은 공통된 자원을 나누어 써야 하는 상황인 것이다. 이때 운영체제가 이 프로세스들에게 자원을 제대로 나누어 주지 못하면 <strong>교착상태(Deadlock)</strong>에 빠지게 된다. </p>
</blockquote>
<p>e.g. 어떤 자원은 갖고 있으나 다른 자원은 갖지 못할 때(e.g. 다른 프로세스가 사용 중) 대기해야 할 때 교착상태에 빠지게 된다.</p>
<p>교착상태에 빠진다는 것은 다음의 네 가지 필요 조건을 모두 만족해야 일어날 수 있다.</p>
<ul>
<li><strong>Mutual exclusion(상호배타)</strong>: 하나의 프로세스가 자원을 점유 중일 때, 다른 프로세스는 해당 자원을 점유하지 못하는 것</li>
<li><strong>Hold and wait(보유 및 대기)</strong>: 본인이 다른 자원을 보유하고 있으면서 다른 자원이 올 떄까지 대기하는 것</li>
<li><strong>No Preemption(비선점)</strong>: 다른 프로세스가 점유하고 있는 자원을 빼앗지 못하는 것</li>
<li><strong>Circular wait(환형대기)</strong>: 프로세스들이 자원을 기다리는 관계가 원형으로 연결된 상태<ul>
<li>프로세스 A → 프로세스 B가 점유한 자원 대기, 프로세스 B → 프로세스 C가 점유한 자원 대기, 프로세스 C → 프로세스 A가 점유한 자원 대기</li>
</ul>
</li>
</ul>
<blockquote>
<p>정리하자면 교착상태는 결국 하드웨어 자원이 한정적으로 존재하기 때문에 발생한다고 볼 수 있다.</p>
</blockquote>
<h1 id="2-자원resources">2. 자원(Resources)</h1>
<blockquote>
<p>프로세스가 자원을 사용하기 위해서는 OS에 <strong>요청(request)</strong>을 한다. OS는 요청이 올바를 경우 프로세스가 이를 <strong>사용(use)</strong>하게 한다. 자원을 다 쓰고 나면 OS에 해당 자원을 <strong>반납(release)</strong>한다.</p>
</blockquote>
<h2 id="21-자원-할당도resource-allocation-graph">2.1. 자원 할당도(Resource Allocation Graph)</h2>
<blockquote>
<p>자원 할당도는 컴퓨터에 어떤 자원이 있고 그 자원이 어떤 프로세스에 할당되었는지, 어떤 프로세스가 어떤 자원을 할당 받으려고 기다리고 있는지 시각적으로 표현하는 도구이다. 이를 통해 교착상태의 발생 여부를 판단할 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/418bd50a-0327-4303-8ffc-29dbe8554c44/image.png" alt=""></p>
<p>동일 자원이 여러 개 있을 수 있다. 각각을 instance라고 한다.</p>
<p>자원은 사각형, 프로세스는 원, 할당은 화살표로 표시한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/97822332-d5b2-4ea8-ab26-e7091a27d2d6/image.png" alt=""></p>
<ul>
<li>환형대기 x, 교착상태 x</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/d9494536-f4ea-472a-baf0-9e911b92ce76/image.png" alt=""></p>
<ul>
<li>식사하는 철학자 문제. 환형대기 o, 교착상태 가능성 o</li>
</ul>
<h1 id="3-교착상태-처리">3. 교착상태 처리</h1>
<p>교착상태는 위에서 언급한 필요 조건들을 전부 만족했을 때 일어날 ‘수’ 있다. 
즉, 컴퓨터에서 교착상태는 잘 일어나지 않는다. <em>하지만 그럼에도 발생하는 경우에는 어떻게 처리해야 할까?</em></p>
<h2 id="31-교착상태-방지deadlock-prevention">3.1. 교착상태 방지(Deadlock Prevention)</h2>
<p>교착상태는 필요 조건 4가지를 만족해야 일어날 가능성이 있다고 했다. 그렇다면 교착상태를 방지하기 위해서는 4가지 조건 중 한 가지 이상을 불만족하도록 만들면 된다.</p>
<h3 id="311-상호배타mutual-exclusion">3.1.1. 상호배타(Mutual exclusion)</h3>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/c14d7616-c96b-41d7-80ff-55608e50661a/image.jpeg" alt=""></p>
<p>위의 그림에서 자원은 2개의 instance를 가지고 있고, 각각 $P_1$, $P_2$에 할당되어 있다. 이때 $P_3$는 해당 자원을 가지지 못한다.</p>
<p>이를 해결하기 위해서는 해당 자원을 공유 가능하게 만드는 방법이 있다. 하지만 이는 원천적으로 불가능한 이야기이다. 식사하는 철학자 문제에서도 같은 오른쪽 젓가락을 두 명의 철학자가 동시에 사용하는 것이 불가능한 것을 생각해볼 수 있다.</p>
<h3 id="312-보유-및-대기hold--wait">3.1.2. 보유 및 대기(Hold &amp; Wait)</h3>
<p>보유 및 대기 조건을 불만족하기 위해서는 자원을 가지고 있으면서 다른 자원을 기다리지 않게 하면 된다. 
예를 들어 일부 자원만이 사용 가능할 경우에는 보유하는 자원을 모두 놓아준다. 즉, 자원을 보유하지 않은 상태에서 필요한 모든 자원을 대기한다.</p>
<p>이는 starvation을 야기하고 자원 활용률이 저하되나, 3.1.1과 비교했을 때 현실적으로 사용할 수 있는 방법이다.</p>
<h3 id="313-비선점no-preemption">3.1.3. 비선점(No preemption)</h3>
<p>다른 프로세스가 사용 중인 자원을 선점 가능하도록 하면 교착상태에 빠지지 않을 수 있다. 하지만 이 방법 또한 원천적으로 불가능하다. 예를 들어 다른 프로세스가 프린터를 사용 중인데 이를 강제로 빼앗아 사용하면 자료가 꼬일 수 있다. </p>
<p>CPU에서는 컨텍스트 스위칭을 강제로 하는 등 일부 가능은 하겠으나, 대부분의 경우 빼앗아 오는 것은 일반적으로 불가능하다.</p>
<h3 id="314-환형대기circular-wait">3.1.4. 환형대기(Circular wait)</h3>
<p>일반적으로 환형대기를 깨는 방법은 자원에 번호를 부여하고, 번호 오름차순으로 자원을 요청하는 것이다. </p>
<p>이는 대부분의 상황에 적용이 가능은 하겠으나, 실제 자원이 매우 많은 컴퓨터에 적용을 해보았을 때 자원 활용률이 굉장히 떨어지게 된다.</p>
<p>실제로 사용 가능한 방법은 3.1.2, 3.1.4 정도가 되겠다. 컴퓨터의 경우 교착상태가 발생하면 재부팅을 시도해볼 수 있지만, 항공선의 경우 재부팅 자체가 불가하다. 따라서 부득이하게 자원 활용률 및 기타 단점을 감수하고서라도 교착상태를 방지해야 하는 상황이 발생할 수 있다.</p>
<h2 id="32-교착상태-회피deadlock-avoidance">3.2. 교착상태 회피(Deadlock Avoidance)</h2>
<p>데드락 발생 이유를 자원 요청에 대한 잘못된 승인이라고 생각한다. 아래 예제를 살펴 보자.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Max needs</th>
<th>Current needs</th>
</tr>
</thead>
<tbody><tr>
<td>$P_0$</td>
<td>10</td>
<td>5</td>
</tr>
<tr>
<td>$P_1$</td>
<td>4</td>
<td>2</td>
</tr>
<tr>
<td>$P_2$</td>
<td>9</td>
<td>2</td>
</tr>
</tbody></table>
<p>12개의 magnetic tape와 3개의 프로세스가 있다.</p>
<p>Current needs에 따라 자원을 나누어 주면 $12-(5+2+2)=3$이 된다. 이때 $P_0$는 Max needs에 따라 $5$만큼을 더 필요로 하지만 자원이 없어 실행되지 못한다. $P_2$는 $2$만큼을 더 필요로 하여 자원을 가져간 뒤, 프로세스를 종료하여 $4$만큼을 반납한다. ($3-2+4=5$)</p>
<p>$P_0$이 필요한 자원 $5$를 만족하게 되어 $P_0$는 $5$만큼 가져다가 쓰고 프로세스를 종료하고 $10$을 반납한다. ($5-5+10=10$) $P_2$역시 $7$만큼 가져다가 쓰고 프로세스를 종료하고 $9$를 반납한다.($10-7+9=12$)</p>
<p>이처럼 자원이 적절히 배분되어 프로세스가 전체적으로 돌아갈 수 있도록 자원 요청을 승인하는 것을 안전한 할당(safe allocation)이라고 한다. 여기서 $P_2$의 current needs가 $2$에서 $3$으로만 바뀌어도 교착상태에 빠지게 되며, 이를 불안전한 할당(Unsafe allocation)이라고 한다.</p>
<p>불안전한 할당은 곧 교착상태로 이어지기 때문에, OS는 자원을 요청받을 때 데드락이 일어나지 않도록 승인을 잘 해주어야 한다. 이는 마치 대출 전문 은행과 유사하다고 하여, Banker’s Algorithm이라고 한다.</p>
<h2 id="33-교착상태-검출-및-복구deadlock-detection--recovery">3.3. 교착상태 검출 및 복구(Deadlock Detection &amp; Recovery)</h2>
<p>데드락이 일어나는 것을 허용은 하되, 이를 복구하도록 하는 방법이다.</p>
<p>운영체제가 처음부터 데드락이 일어나지 않도록 신경 쓰는 게 아니라, 프로세스가 필요한 대로 자원을 전부 나누어 준다. 이렇게 막 나누어 주다 보면 어쩌다 한 번 데드락이 일어날 수 있다.</p>
<p>이를 감지하기 위해 OS는 주기적으로 검사를 실행한다. 검사를 자주 실행해 혹시 모를 데드락을 대비하는 것도 좋겠으나, 검사를 위한 오버헤드가 크다는 문제가 있다.</p>
<p>데드락을 한 번 발견했다면, 복구를 위해 주기적으로 현재 상태를 기억해 두어야 한다. 데드락이 발생하기 이전 상태로 상황을 되돌려야 하기 때문이다. 이 방법이 불가능할 때에는 한 프로세스를 강제로 종료시키거나, 자원을 강제로 빼앗아(선점) 일부 프로세스에 할당한다.</p>
<h2 id="34-교착상태-무시dont-care">3.4. 교착상태 무시(Don’t Care)</h2>
<p>교착상태는 실제로 잘 일어나지 않는다. 앞서 언급한 바와 같이 4가지 필요 조건을 모두 만족하더라도 일어나지 않을 수 있다. 공연히 일어나지도 않을 일 때문에 detection하는 등의 방법은 오히려 컴퓨터 성능을 떨어뜨린다. 따라서 교착상태가 일어나도 아무런 조치를 하지 않는 것도 하나의 방법이라 할 수 있겠다.</p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
<li><a href="https://wansook0316.github.io/cs/os/2020/04/05/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%A0%95%EB%A6%AC-11-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%8F%99%EA%B8%B0%ED%99%94-4.html">11: 교착 상태: Deadlock</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] CPU 스케줄링]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</guid>
            <pubDate>Thu, 31 Oct 2024 07:26:55 GMT</pubDate>
            <description><![CDATA[<p>레디 큐, 메인 메모리에 여러 개의 프로세스가 기다리고 있을 때 현재 실행 중인 게 끝나면 어느 프로세스를 실행해 서비스를 할 것인가를 정해주는 알고리즘을 <strong>CPU Scheduling</strong> 알고리즘이라고 한다.</p>
<h1 id="1-preemptive-vs-non-preemptive">1. Preemptive vs Non-preemptive</h1>
<blockquote>
<p>병원에서 환자가 진료를 기다리고 있는 상황을 생각해 보자.</p>
<p>대기실(Ready Queue)에서 환자(Process)가 진료(CPU)를 기다린다.</p>
<p>앞의 환자가 진료가 끝날 때까지 환자들은 대기실에서 기다린다. 이는 Non-preemptive 스케줄링이라고 한다. </p>
<p>반면, 응급 환자가 나타나면 앞의 환자의 진료가 끝나지 않아도 진료를 시작한다. 이를 Preemptive 스케줄링이라고 한다.</p>
</blockquote>
<h2 id="11-preemptive">1.1. Preemptive</h2>
<p>프로세스가 CPU를 점유하고 있는 동안 I/O나 인터럽트가 발생한 것도 아니고 모든 작업을 끝내지도 않았는데, 다른 프로세스가 해당 CPU를 강제로 점유할 수 있다. 즉, 프로세스가 정상적으로 수행 중인 가운데 <strong>다른 프로세스가 CPU를 강제로 점유하여 실행</strong>할 수 있다.</p>
<h2 id="12-non-preemptive">1.2. Non-preemptive</h2>
<p>한 프로세스가 한 번 CPU를 점유했다면, I/O 또는 프로세스가 종료될 때까지 <strong>다른 프로세스가 CPU를 점유하지 못한다.</strong></p>
<h1 id="2-scheduling-criteria">2. Scheduling criteria</h1>
<blockquote>
<p>하나의 프로세스가 종료되면 큐에 올려져 있는 다음 프로세스를 실행하는 것이 가장 일반적이고 자연스러울 것이다. 하지만 필요에 따라 여러 방법을 이용할 수 있고, 이때 어떤 방법이 가장 적합할지 비교하기 위한 척도가 필요하다.</p>
</blockquote>
<ul>
<li><strong>CPU Utilization(CPU 이용률)</strong>: CPU가 얼마나 놀지 않고 부지런히 일하는가<ul>
<li>e.g. $P_1$→$P_2$→$P_3$로 실행하니 CPU가 100%로 사용되는데, $P_2$→$P_1$→$P_3$로 실행하니 CPU가 80%는 일하고 20%는 놀더라</li>
</ul>
</li>
<li><strong>Throughput(처리율)</strong>: 단위 시간당 몇 개의 작업을 처리하는가</li>
<li><strong>Turnaround time(반환시간)</strong>: 어떤 작업이 메인 메모리로 들어가 작업을 끝내고 나올 때까지 걸리는 시간이 얼마나 걸리는가</li>
<li><strong>Waiting time(대기시간)</strong>: CPU의 서비스를 받기 위해 Ready Queue에서 얼마나 기다리는가</li>
<li><strong>Response time(응답시간)</strong>: 명령을 내리고 첫 응답을 받을 때까지 시간이 얼마나 걸리는가, 주로 interactive system에서 중요한 척도로 사용.</li>
</ul>
<h1 id="3-cpu-scheduling-algorithms">3. CPU Scheduling Algorithms</h1>
<h2 id="31-first-come-first-servedfcfs">3.1. First-Come, First-Served(FCFS)</h2>
<p>먼저 온 것을 먼저 서비스 해준다. 가장 간단하고 공평한 방법이나, 반드시 좋은 성능을 나타내진 않는다. 아래 Gantt Chart의 <em>Average Waiting Time</em>을 구해보자.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Burst Time(ms)</th>
</tr>
</thead>
<tbody><tr>
<td>$P_1$</td>
<td>24</td>
</tr>
<tr>
<td>$P_2$</td>
<td>3</td>
</tr>
<tr>
<td>$P_3$</td>
<td>3</td>
</tr>
<tr>
<td>1. $P_1$→$P_2$→$P_3$</td>
<td></td>
</tr>
</tbody></table>
<pre><code>![](https://velog.velcdn.com/images/jayy_19/post/1f8e3648-f08c-4d3a-ac4f-5660e6b49a7d/image.png)

단순히 $P_1$이 먼저 도착했다고 해서 먼저 처리할 경우 AWT는 17ms가 된다.</code></pre><ol start="2">
<li><p>$P_3$→$P_2$→$P_1$</p>
<p> <img src="https://velog.velcdn.com/images/jayy_19/post/3100a935-9dae-4ef2-bd21-e1b3179d058e/image.png" alt=""></p>
<p> 반면 짧게 걸리는 $P_2$, $P_3$를 먼저 처리할 경우 AWT는 3ms가 된다.</p>
</li>
</ol>
<p>⇒ 평균 대기 시간 측면으로 보았을 때 FCFS가 반드시 좋은 방법은 아니란 것을 알 수 있다.</p>
<p>위의 예시처럼 CPU 점유 시간이 긴 프로세스($P_1$)를 CPU 점유 시간이 짧은 프로세스들($P_2$, $P_3$)이 뒤에서 오랫동안 기다리며 시중처럼 따라다니는 것 같은 모습을 <strong>Convoy Effect(호위효과)</strong>라고 한다.</p>
<blockquote>
<p>정리하자면 FCFS에는 호위효과라는 단점이 나타날 수 있고, 이 방식은 Non-preemptive 스케줄링이다.</p>
</blockquote>
<h2 id="32-shortest-job-firstsjf">3.2. Shortest-Job-First(SJF)</h2>
<p>가장 짧은 실행 시간이 걸리는 작업부터 먼저 처리한다. 이는 <em>Provably optimal</em>, 즉 수학적으로 증명은 생략하겠으나 가장 이상적인 방법이라고 할 수 있다. 아래 예시를 보자.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Burst Time(ms)</th>
</tr>
</thead>
<tbody><tr>
<td>$P_1$</td>
<td>6</td>
</tr>
<tr>
<td>$P_2$</td>
<td>8</td>
</tr>
<tr>
<td>$P_3$</td>
<td>7</td>
</tr>
<tr>
<td>$P_4$</td>
<td>3</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/16a2b5c0-08b9-4e3d-8bf1-171e20e0b559/image.png" alt=""></p>
<p>SJF 방식을 이용하여 AWT를 구한 결과 7ms가 나온다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/69710c06-7c96-4653-b997-166401fd1b25/image.png" alt=""></p>
<p>앞서 배운 FCFS 방식을 이용해 구한 AWT는 10.25ms로 역시나 SJF이 가장 효율적인 걸 알 수 있다.</p>
<blockquote>
<h4 id="그럼-항상-sjf를-사용하면-최고의-효율을-뽑을-수-있는-거-아닐까">그럼 항상 SJF를 사용하면 최고의 효율을 뽑을 수 있는 거 아닐까?</h4>
</blockquote>
<p>SJF 방식은 <em>Not realistic</em>하다. 실제 돌아가는 프로세스들은 CPU 점유 시간이 얼마나 걸릴지는 알 수 없기에 <strong>prediction</strong>이 필요하다. 하지만 프로세스의 예상 점유 시간을 구하기 위해 과거 실행 시간을 기억하고 계산하면 되려 오버헤드가 커진다. 따라서 현실적으로 적용하기 어려운 방법이라고 할 수 있다.</p>
<p>SJF는 <strong>Preemptive</strong>, <strong>Non-preemptive</strong> 두 가지 방식으로 만들 수 있다.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Arrival Time</th>
<th>Burst Time(ms)</th>
</tr>
</thead>
<tbody><tr>
<td>$P_1$</td>
<td>0</td>
<td>8</td>
</tr>
<tr>
<td>$P_2$</td>
<td>1</td>
<td>4</td>
</tr>
<tr>
<td>$P_3$</td>
<td>2</td>
<td>9</td>
</tr>
<tr>
<td>$P_4$</td>
<td>3</td>
<td>5</td>
</tr>
<tr>
<td>- Non-preemptive</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<pre><code>![](https://velog.velcdn.com/images/jayy_19/post/4811ff01-9cd8-4707-ad33-0013466182ac/image.png)

레디 큐에 올라와 있는 프로세스 중 가장 짧은 점유 시간을 가진 프로세스가 우선적으로 실행된다. 한 프로세스가 실행 중에 있을 때 점유 시간이 더 짧은 프로세스가 들어온다 하더라도 실행되는 프로세스가 바뀌지 않는다.</code></pre><ul>
<li><p>Preemptive</p>
<p>  <img src="https://velog.velcdn.com/images/jayy_19/post/27b4c92d-c640-431c-8293-05d00e573d4c/image.jpeg" alt=""></p>
<p>  한 프로세스가 실행 중에 있더라도, 남아 있는 시간이 짧은 순서대로 점유하는 프로세스가 달라진다. <em>Shortest-Remaining-Time-First</em>(최소잔여시간 우선)이라고도 한다.</p>
</li>
</ul>
<h2 id="33-priority-scheduling">3.3. Priority Scheduling</h2>
<p>우선순위가 더 높은 프로세스를 먼저 서비스해준다.</p>
<p>우선순위는 보통 컴퓨터에서 정수값으로 표기되고, 대부분의 운영체제(Unix/Linux)에서는 숫자가 작은 게 더 높은 우선순위를 나타낸다. 아래 예시를 보자.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Burst Time(ms)</th>
<th>Priority</th>
</tr>
</thead>
<tbody><tr>
<td>$P_1$</td>
<td>10</td>
<td>3</td>
</tr>
<tr>
<td>$P_2$</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>$P_3$</td>
<td>2</td>
<td>4</td>
</tr>
<tr>
<td>$P_4$</td>
<td>1</td>
<td>5</td>
</tr>
<tr>
<td>$P_5$</td>
<td>5</td>
<td>2</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/1036a13f-cf22-40f1-ac13-ece8c477e424/image.jpeg" alt=""></p>
<p>그럼 우선순위는 어떻게 정해질까? 내부적, 외부적 요인에 따라 결정된다.</p>
<ul>
<li>Internal: time limit, memory requirement, I/O to CPU burst, …</li>
<li>External: amount of funds being paid, political factors, …</li>
</ul>
<p>실제로 컴퓨터가 돌아갈 때 외부에서 계속해서 새로운 프로세스가 들어온다. 이때 우선순위가 낮은 프로세스의 경우, 아무리 오래 기다려도 해당 프로세스보다 우선순위가 높은 작업이 들어오면 계속해서 대기하게 된다. 이렇게 아무리 기다려도 CPU 시간을 차지하지 못하는 상황을 <strong>starvation</strong>이라고 한다.</p>
<p>이를 해결하기 위해 Ready Queue에서 오래 기다린 프로세스일수록 점진적으로 우선순위를 높여주는 방법을 <strong>aging</strong>이라고 한다.</p>
<h2 id="34-round-robin">3.4. Round-Robin</h2>
<p>뿅망치로 머리를 때리면 머리 위에 빙빙 도는 새처럼 빙빙 돌며 스케줄링하는 방식이며, <strong>Time-sharing system(시분할/시공유 시스템)</strong>에서 많이 사용되는 방법이다.</p>
<p>시간축을 길게 늘여뜨려 놓고 보았을 때, 일정 간격으로 시간축을 나눈다. 이를 <strong>Time Quantum</strong> 혹은 <strong>Time Slice</strong>라고 한다. 기호로는 델타(Δ)를 사용한다.</p>
<p>이 방식은 Time Quantum이 지나면 자동으로 다음 프로세스가 CPU를 점유하기 때문에 Preemptive 스케줄링에 해당된다. 아래 예제를 살펴보자.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Burst Time(ms)</th>
</tr>
</thead>
<tbody><tr>
<td>$P_1$</td>
<td>24</td>
</tr>
<tr>
<td>$P_2$</td>
<td>3</td>
</tr>
<tr>
<td>$P_3$</td>
<td>3</td>
</tr>
</tbody></table>
<p>Δ = 4ms일 때 AWT를 구하면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/8a5a6201-8dab-4ad3-a7a0-511d3dd9df6e/image.jpeg" alt=""></p>
<p>이 방식은 <strong>Time Quantum의 size에 의존적</strong>이다.</p>
<p>만약 위의 예제에서 $∆→∞$일 경우 점유 중인 프로세스가 실행이 끝날 때까지 switching이 일어나지 않는다. 즉, FCFS와 동일한 방법이 된다.
$∆→0$인 경우 switching이 빈번하게 일어나 여러 프로세스들이 거의 동시에 일어나는, Processor sharing과 동일하게 본다. 하지만 이 경우 context switching overhead가 커져 결코 좋은 방법으로 볼 수는 없다.</p>
<p>아래 예제에서 Average turnaround time을 구해보자.</p>
<table>
<thead>
<tr>
<th>Process</th>
<th>Burst Time(ms)</th>
</tr>
</thead>
<tbody><tr>
<td>$P_1$</td>
<td>6</td>
</tr>
<tr>
<td>$P_2$</td>
<td>3</td>
</tr>
<tr>
<td>$P_3$</td>
<td>1</td>
</tr>
<tr>
<td>$P_4$</td>
<td>7</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/286e1877-6198-4158-91a3-37c5a5efaac1/image.jpeg" alt=""></p>
<p>Δ = 1ms일 때</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a31d2abf-f482-4a15-9f7f-08b684e37fe8/image.jpeg" alt=""></p>
<p>Δ = 5ms일 때</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/a007c6db-c104-4c57-bfc5-414e34b40aa8/image.png" alt=""></p>
<p>이처럼 델타에 따라 turnaround time이 상이하게 나타나는 것을 알 수 있다. 즉, <strong>Δ를 얼마로 잡을 것이냐</strong>가 성능에 중요한 포인트가 된다.</p>
<h2 id="35-multilevel-queue-scheduling">3.5. Multilevel Queue Scheduling</h2>
<p>먼저 프로세스 그룹에 대해 살펴보자. 프로세스는 기준에 따라 몇 가지 그룹으로 나눌 수 있다.</p>
<ul>
<li>System processes: OS 커널 수준의 프로세스<ul>
<li>e.g. 가상 메모리 매핑, 파일 읽기, 통신 등</li>
</ul>
</li>
<li>Interactive processes: 유저 수준의 대화형 프로세스<ul>
<li>e.g. 게임</li>
</ul>
</li>
<li>Interactive editing processes<ul>
<li>e.g. 워드 프로세서</li>
</ul>
</li>
<li>Batch processes: 대화형이 아닌 프로세스, 일괄적으로 컴퓨터가 알아서 처리<ul>
<li>e.g. 컴파일러</li>
</ul>
</li>
<li>Student processes</li>
</ul>
<p>Interactive editing 프로세스는 컴퓨터 앞에 직접 앉아서 작업을 하는데 응답이 없으면 답답하다. 
반면 Batch 프로세스는 돌려놓고 집에 가도 되기 때문에 조금 느리게 처리되어도 크게 문제가 없다.</p>
<p>이렇게 다양한 성격의 프로세스를 동일한 Ready Queue에 두는 것은 비효율적이게 보인다. 즉, Queue를 여러 개 두고 서비스하는 것이 더 효율적이다. 이를 <strong>Multilevel Queue Scheduling</strong>이라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/e2686562-c44f-4ec7-a491-17996c2551b7/image.png" alt=""></p>
<p>이 방식의 경우 각각의 Queue에 절대적 우선순위가 존재하거나 CPU time을 각 Queue에 차등 배분한다. 또한 각 Queue는 독립된 scheduling 정책을 사용한다.</p>
<h2 id="36-multilevel-feedback-queue-scheduling">3.6. Multilevel Feedback Queue Scheduling</h2>
<p>3.5와 마찬가지로 <strong>복수 개의 Queue</strong>를 두고 프로세스는 하나의 입구로 진입한다.</p>
<p>해당 Queue의 스케줄링 방식대로 처리가 되다가, 일정 시간이 지나도 Queue가 줄지 않으면, 즉 너무 많은 CPU time을 사용하면 해당 스케줄링 방식이 맞지 않는다고 판단(feedback)하고 다른 Queue로 넘긴다.</p>
<p>또한 기아 상태 우려 시 우선순위가 높은 Queue로 프로세스를 옮긴다.</p>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/47956446-f493-44a9-a9ef-3eb5369ae5dc/image.png" alt=""></p>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=978503">KOCW 양희재 운영체제</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[운영체제] IPC(Inter Process Communication)]]></title>
            <link>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication</link>
            <guid>https://velog.io/@jayy_19/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication</guid>
            <pubDate>Wed, 30 Oct 2024 02:31:22 GMT</pubDate>
            <description><![CDATA[<h1 id="1-독립적-프로세스independent-process">1. 독립적 프로세스(Independent Process)</h1>
<ul>
<li>프로세스는 각자의 주소 공간을 가지고 수행된다.</li>
<li>원칙적으로 하나의 프로세스는 다른 프로세스 수행에 영향을 미치지 못한다.</li>
</ul>
<h1 id="2-협력-프로세스cooperating-process">2. 협력 프로세스(Cooperating Process)</h1>
<ul>
<li>프로세스 협력 메커니즘을 통해 하나의 프로세스가 다른 프로세스의 수행에 영향을 미칠 수 있다.</li>
</ul>
<h1 id="3-ipc-interprocess-communication">3. IPC: Interprocess Communication</h1>
<blockquote>
<p>프로세스 간에 정보를 주고 받을 수 있는 방법</p>
</blockquote>
<ul>
<li>메세지를 전달하는 방법<ul>
<li>Message Passing</li>
</ul>
</li>
<li>주소 공간을 공유하는 방법<ul>
<li>Shared Memory</li>
<li>Thread</li>
</ul>
</li>
</ul>
<h2 id="31-message-passing">3.1. Message Passing</h2>
<blockquote>
<p>공유 메모리와 대조적으로, <strong>공유 변수 없이 메세지를 통해 데이터를 전달</strong>한다는 점에서 차별화된다. 메세지 전달은 일반적으로 커널을 통해 이루어지며, 직접 또는 간접적으로 프로세스 간의 통신을 지원한다.</p>
</blockquote>
<h3 id="311-communication-방식">3.1.1. Communication 방식</h3>
<ul>
<li><p>Direct Communication: 통신하려는 프로세스의 이름을 명시적으로 표시</p>
<p>  <img src="https://velog.velcdn.com/images/jayy_19/post/fccc230f-4275-4870-969f-90ab96cc9281/image.png" alt=""></p>
<p>   인터페이스 측면에서 메세지를 전달하는 프로세스가 메세지를 전달 받을 프로세스를 명시</p>
</li>
<li><p>Indirect Communication: mailbox(또는 port)를 통해 메세지를 간접 전달</p>
<p> <img src="https://velog.velcdn.com/images/jayy_19/post/e75ae09f-ebdb-4d2f-a18d-74ba6c8aab4e/image.png" alt=""></p>
<p>  경우에 따라 메세지를 집어 넣으면서 문어발 식으로 아무나 꺼내가라 할 수도 있음.</p>
</li>
</ul>
<h3 id="312-동기화synchronization">3.1.2. 동기화(Synchronization)</h3>
<ul>
<li><strong>동기적 통신(Synchronous Communication)</strong><ul>
<li>메세지를 보내는 프로세스는 수신 프로세스가 메세지를 받을 때까지 기다린다. <strong>즉, 송신과 수신이 동시에 이루어져야 한다.</strong></li>
<li>통신의 신뢰성이 높으나, 송신 프로세스가 수신 프로세스의 응답을 기다려야 하므로 시스템 효율 저하를 일으킨다.</li>
</ul>
</li>
<li>비동기적 통신(Asynchronous Communication)<ul>
<li><strong>메세지를 보내는 프로세스는 메세지를 보낸 후 바로 다음 작업을 수행</strong>할 수 있으며, 수신 프로세스는 나중에 메세지를 수신한다.</li>
<li>수신 프로세스는 언제든지 메세지를 수신할 수 있으며, 송신 프로세스는 수신 여부에 관계없이 계속 작업 수행이 가능하다.</li>
<li>시스템의 효율성을 높일 수 있으나, 메세지 손실 가능성이 있다.</li>
</ul>
</li>
</ul>
<h2 id="32-pipe익명-파이프--named-파이프">3.2. Pipe(익명 파이프 &amp; Named 파이프)</h2>
<blockquote>
<p>프로세스 간에 데이터를 바이트 스트림 형태로 전달하며, 주로 부모-자식 프로세스 간의 통신에 사용된다. 파이프는 단방향 또는 양방향 통신을 지원하며, 익명 파이프와 Named 파이프 두 가지 주요 유형으로 나뉜다.</p>
</blockquote>
<ul>
<li>기본적으로 <strong>반이중 통신(단방향 통신)만 가능</strong>하기 때문에, 양방향 통신이 필요하면 두 개의 파이프를 만들어야 한다.</li>
<li>파이프는 단순한 데이터 흐름을 처리하는 데 적합하며, 두 프로세스가 연속적으로 데이터를 송수신하는 구조다.</li>
<li>유형<ul>
<li><strong>익명 파이프</strong>: 부모-자식 관계와 같이 밀접한 관계에 있는 프로세스 간에 사용된다.</li>
<li><strong>Named 파이프(FIFO)</strong>: 전혀 모르는 상태의 프로세스 사이에서도 사용될 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="33-shared-memory">3.3. Shared Memory</h2>
<blockquote>
<p>두 개 이상의 프로세스가 동일한 메모리 공간을 공유하여 데이터를 주고받을 수 있게 해주는 메커니즘이다. 빠른 데이터 교환을 가능하게 하지만, 동기화 문제를 적절히 처리하지 않으면 데이터 경합이나 일관성 문제가 발생할 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jayy_19/post/fb341eaf-9b59-4b4f-8237-8cfd26d70887/image.png" alt=""></p>
<p>Share Memory에서 프로세스 A, B는 따로 주소 공간을 가지고 있으나 물리적으로 매핑될 때 일부 공간이 공유된다.</p>
<ul>
<li>여러 프로세스가 동일한 메모리 영역을 동시에 접근할 수 있기 때문에, 데이터의 일관성을 유지하려면 <strong>동기화</strong>가 필요하다.</li>
<li><strong>세마포어</strong>, <strong>뮤텍스</strong> 같은 동기화 메커니즘을 사용하여 공유 메모리 접근을 제어해야 한다. 그렇지 않으면 여러 프로세스가 동시에 데이터를 수정할 때 충돌이나 데이터 손실이 발생할 수 있다.</li>
</ul>
<h2 id="34-thread">3.4. Thread</h2>
<ul>
<li>스레드는 사실상 하나의 프로세스이므로 프로세스 간 협력으로 보기에는 어렵다.</li>
<li>하지만 동일한 프로세스를 구성하는 스레드 간에는 주소 공간을 공유하므로 협력이 가능하다.</li>
</ul>
<hr>
<blockquote>
<h4 id="reference">Reference</h4>
</blockquote>
<ul>
<li><a href="http://www.kocw.net/home/search/kemView.do?kemId=1046323">이화여자대학교 운영체제-반효경 강의</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>