<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>fishing_bear99.log</title>
        <link>https://velog.io/</link>
        <description>취업 준비생 낚곰입니다!! 반갑습니다!!</description>
        <lastBuildDate>Tue, 18 Nov 2025 14:30:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. fishing_bear99.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/fishing_bear99" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[운영체제 스터디 11주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-11%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-11%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 18 Nov 2025 14:30:39 GMT</pubDate>
            <description><![CDATA[<h2 id="참고">참고</h2>
<p><a href="https://os2024.jeju.ai/week12/threads-cv.html">https://os2024.jeju.ai/week12/threads-cv.html</a></p>
<h1 id="컨디션-변수">컨디션 변수</h1>
<p><em>‘락’</em> 하나만 가지고는 제대로 병행 프로그램을 작성할 수 없습니다. 쓰레드가 계속 진행하기 전에 특정 조건이 만족되었는지 검사가 필요한 경우가 있습니다. 예를 들어 자식 스레드가 작업을 끝냈는 지 확인할 필요가 있습니다. </p>
<pre><code class="language-c">volatile int done = 0;

void *child(void *arg) {
    printf(&quot;child\n&quot;);
    done = 1;
    return NULL;
}

int main(int argc, char *argv[]) {
    printf(&quot;parent: begin\n&quot;);
    pthread_t c;
    Pthread_create(&amp;c, NULL, child, NULL);
    while (done == 0)
    ; // spin
    printf(&quot;parent: end\n&quot;);
    return 0;
}</code></pre>
<p>이렇게 공유 변수를 사용해서 구현할 수 있습니다. 하지만 부모 스레드가 spin을 돌면서 자원을 낭비하고 있는 문제가 발생합니다. </p>
<blockquote>
<p>부모 스레드의 자원 낭비를 막기 위해서는 특정 조건이 될 때까지 재워두는 것입니다.</p>
</blockquote>
<h2 id="컨디션-변수에서-스레드를-재우고-깨우는-방법이-무엇인가">컨디션 변수에서 스레드를 재우고 깨우는 방법이 무엇인가?</h2>
<p>조건이 참이 될 때까지 기다리기 위해 <strong>컨디션 변수(Condition Variable)</strong>를 활용할 수 있습니다. 컨디션 변수는 일종의 큐 자료 구조로서, 어떤 실행의 상태 (또는 어떤 조건)가 원하는 것과 다를 때 참이 되기를 기다리며 스레드가 대기할 수 있는 큐입니다.</p>
<p>컨디션 변수에는 두 가지 연산이 존재합니다.</p>
<ul>
<li><code>wait()</code> : 쓰레드가 스스로를 잠재우기 위해 호출하는 함수</li>
<li><code>signal()</code> : 쓰레드가 무엇인가를 변경했기 때문에 조건이 참이 되기를 기다리며 잠자고 있던 쓰레드를 깨울 때 호출하는 함수</li>
</ul>
<h2 id="wait함수에서-주의할-점">wait()함수에서 주의할 점</h2>
<p><code>wait()</code>에서 주의할 점은 <code>mutex</code>를 매개변수로 사용한다는 것입니다. <code>wait()</code>가 호출될 때 <code>mutex</code>는 잠겨있었다고 가정합니다. <code>wait()</code>는 락을 해제하고 호출한 쓰레드를 재운 후, 어떤 다른 쓰레드가 시그널을 보내 쓰레드가 깨어나면 <code>wait()</code>에서 리턴하기 전에 락을 다시 획득해야 합니다.</p>
<p>즉, 조건이 만족되어 잠에서 깨어났더라도 락을 획득하지 못하면 다시 잠에 드는 것입니다. 이렇게 복잡한 이유는 쓰레드가 스스로 잠들려고 할 때 경쟁 조건(Race Condition)의 발생을 방지하기 위해서입니다.</p>
<h3 id="어떤-다른-쓰레드가-시그널을-보내-쓰레드가-깨어나면-wait에서-리턴하기-전에-락을-다시-획득해야-되는-이유">어떤 다른 쓰레드가 시그널을 보내 쓰레드가 깨어나면 <code>wait()</code>에서 리턴하기 전에 락을 다시 획득해야 되는 이유?</h3>
<pre><code class="language-c">int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;

void thr_exit() {
    Pthread_mutex_lock(&amp;m);
    done = 1;
    Pthread_cond_signal(&amp;c);
    Pthread_mutex_unlock(&amp;m);
}

void *child(void *arg) {
    printf(&quot;child\n&quot;);
    thr_exit();
    return NULL;
}

void thr_join() {
    Pthread_mutex_lock(&amp;m);
    while (done == 0)
        Pthread_cond_wait(&amp;c, &amp;m);
    Pthread_mutex_unlock(&amp;m);
}

int main(int argc, char *argv[]) {
    printf(&quot;parent: begin\n&quot;);
    pthread_t p;
    Pthread_create(&amp;p, NULL, child, NULL);
    thr_join();
    printf(&quot;parent: end\n&quot;);
    return 0;
}</code></pre>
<p>부모 스레드가 자식 스레드를 생성하고 thr_join()를 호출하여 자식 스레드가 끝나기를 기다림 → 부모 쓰레드는 락을 획득하고 자식이 끝났는지 검사(<code>while (done == 0)</code>)한 후, 자식이 아직 끝나지 않았으므로 <code>wait()</code>을 호출하여 락을 해제하고 스스로를 잠재움 → 나중에 자식 쓰레드가 실행되어 <code>thr_exit()</code>을 호출하면 부모 쓰레드가 깨어나고, <code>wait()</code>에서 락을 다시 획득한 채로 리턴하여 부모 쓰레드가 계속 실행되고 락을 해제한 후 종료</p>
<p>문제가 되는 부분은 여기…</p>
<p>자식 스레드가 생성되자마자 즉시 실행되어 done을 1로 만들고, 자고 있는 스레드를 깨우기 위해서 시그널을 보냄 → 하지만 아직은 자고 있는 스레드가 없기 때문에 아무런 효과가 없음 → 이후 부모 스레드가 실행되어 thr_join() 호출 → 하지만 이미 done이 1이므로 while을 건너뛰고 바로 락을 해제하고 return하는 문제가 발생</p>
<blockquote>
<p>문제가 자식 스레드가 작업을 끝낼 때까지 부모 스레드가 대기하고 있어야 하는데 자식 스레드의 작업이 끝나기도 전에 return 해버려서 그런건가?</p>
</blockquote>
<h1 id="생산자--소비자-문제-해결하기">생산자 / 소비자 문제 해결하기</h1>
<h2 id="문제">문제</h2>
<ul>
<li>여러 개의 생산자(Producer) 쓰레드와 소비자(Consumer) 쓰레드가 있습니다.</li>
<li>생산자는 데이터를 만들어 버퍼에 집어넣습니다.</li>
<li>소비자는 버퍼에서 데이터를 꺼내 사용합니다.</li>
<li>버퍼는 유한한 크기를 가집니다.</li>
<li>버퍼가 가득 차면 생산자는 기다려야 하고, 비어있으면 소비자가 기다려야 합니다.</li>
</ul>
<h2 id="여러-쓰레드가-버퍼라는-공유-자원에-동시에-접근하므로-경쟁-조건을-피하기-위해-동기화가-필요하다-어떻게-해결할-수-있을까">여러 쓰레드가 버퍼라는 공유 자원에 동시에 접근하므로, 경쟁 조건을 피하기 위해 동기화가 필요하다. 어떻게 해결할 수 있을까?</h2>
<pre><code class="language-c">cond_t cond;
mutex_t mutex;

void *producer(void *arg) {
    int i;
    for (i = 0; i &lt; loops; i++) {
        Pthread_mutex_lock(&amp;mutex); // p1
        if (count == 1) // p2
            Pthread_cond_wait(&amp;cond, &amp;mutex); // p3
        put(i); // p4
        Pthread_cond_signal(&amp;cond); // p5
        Pthread_mutex_unlock(&amp;mutex); // p6
    }
}

void *consumer(void *arg) {
    int i;
    for (i = 0; i &lt; loops; i++) {
        Pthread_mutex_lock(&amp;mutex); // c1
        if (count == 0) // c2
            Pthread_cond_wait(&amp;cond, &amp;mutex); // c3
        int tmp = get(); // c4
        Pthread_cond_signal(&amp;cond); // c5
        Pthread_mutex_unlock(&amp;mutex); // c6
        printf(&quot;%d\n&quot;, tmp);
    }
}</code></pre>
<h3 id="put과-get을-이용한-해결법">put()과 get()을 이용한 해결법</h3>
<pre><code class="language-c">int buffer;
int count = 0;

void put(int value) {
    assert(count == 0);
    count = 1;
    buffer = value;
}

int get() {
    assert(count == 1);
    count = 0;
    return buffer;
}</code></pre>
<ul>
<li>생산자는 버퍼가 빌 때까지(<code>count == 1</code>) 기다립니다.</li>
<li>소비자는 버퍼에 데이터가 있을 때까지(<code>count == 0</code>) 기다립니다.</li>
</ul>
<p>생산자와 소비자가 각각 1명씩만 있을 때는 문제가 없지만 2 이상일 때는 문제가 생긴다.</p>
<blockquote>
<p>여기서 문제가 생긴다는게 생산자 1과 2가 각각 생산품을 1개씩 생산을 해도 소비자 1이 생산품을 가져간 후에 생산품이 생산되기도 전에 소비자 1과 2과 동시에 접근하게 되면 문제가 생길 수 있다는건가?</p>
</blockquote>
<h3 id="put과-get을-이용한-해결법의-문제점">put()과 get()을 이용한 해결법의 문제점</h3>
<p>대기 루프의 <code>if</code>문과 관련이 있습니다.</p>
<p>소비자 쓰레드가 2개(Tc1, Tc2) 있고 생산자 쓰레드가 1개(Tp) 있다고 가정해 보겠습니다.</p>
<ol>
<li>먼저 소비자 Tc1이 실행됩니다. 락을 획득하고(c1) 버퍼를 소비할 수 있는지 검사합니다(c2). 현재 버퍼가 비어있으므로 Tc1은 <code>wait()</code>을 호출하여(c3) 락을 해제하고 sleep합니다.</li>
<li>이제 생산자 Tp가 실행됩니다. 락을 획득하고(p1) 버퍼가 비었음을 확인합니다(p2). 데이터를 버퍼에 넣고(p4) 소비자에게 시그널을 보냅니다(p5). 이때 대기 중이던 Tc1이 깨어나 실행 가능한 상태가 됩니다.</li>
<li>하지만 Tc1이 바로 실행되는 것이 아니라 단지 실행 가능한 상태일 뿐입니다. 그 사이에 생산자 Tp는 계속 실행 중이며 다시 버퍼 상태를 검사합니다. 버퍼가 차 있으므로 이번엔 자신이 <code>wait()</code>을 호출하고(p3) 잠듭니다.</li>
<li>이제 문제가 발생합니다. Tc1 대신 다른 소비자 Tc2가 먼저 실행될 수 있습니다. Tc2는 버퍼에서 데이터를 꺼내 가버립니다(c1-c6).</li>
<li>이제 Tc1이 비로소 실행됩니다. 그러나 <code>get()</code>을 호출했을 때(c4) 버퍼는 이미 비어있습니다! Tc2가 먼저 데이터를 가져갔기 때문입니다.</li>
</ol>
<blockquote>
<p>여기서 핵심은 <em>Tc1이 시그널에 의해 깨어났을 때 조건이 여전히 만족된다는 보장이 없다</em>는 것입니다. 시그널은 쓰레드를 깨우기만 할 뿐, 깨어난 쓰레드가 즉시 실행된다는 보장은 없습니다.</p>
</blockquote>
<blockquote>
<p>이해가 안되는데 Tc1이 버퍼에서 값을 못 가져갈 경우가 왜 문제가 되는거지?</p>
</blockquote>
<h3 id="해결책">해결책</h3>
<p>이 문제는 대기 루프의 <code>if</code>문을 <code>while</code>문으로 바꾸면 해결됩니다.</p>
<pre><code class="language-c">cond_t cond;
mutex_t mutex;

void *producer(void *arg) {
    int i;
    for (i = 0; i &lt; loops; i++) {
        Pthread_mutex_lock(&amp;mutex); // p1
        while (count == 1) // p2
            Pthread_cond_wait(&amp;cond, &amp;mutex); // p3
        put(i); // p4
        Pthread_cond_signal(&amp;cond); // p5
        Pthread_mutex_unlock(&amp;mutex); // p6
    }
}

void *consumer(void *arg) {
    int i;
    for (i = 0; i &lt; loops; i++) {
        Pthread_mutex_lock(&amp;mutex); // c1
        while (count == 0) // c2
            Pthread_cond_wait(&amp;cond, &amp;mutex); // c3
        int tmp = get(); // c4
        Pthread_cond_signal(&amp;cond); // c5
        Pthread_mutex_unlock(&amp;mutex); // c6
        printf(&quot;%d\n&quot;, tmp);
    }
}</code></pre>
<p>이제 Tc1이 시그널에 의해 깨어나면(lock을 획득한 상태로) 즉시 조건을 다시 검사합니다(<code>while (count == 0)</code>). 만약 그 사이에 Tc2가 데이터를 가져갔다면 Tc1은 다시 <code>wait()</code>을 호출하여 잠듭니다.</p>
<blockquote>
<p>조건을 한 번만 검사하는 것이 아니라 여러 번 검사하게 함으로써 위의 문제를 방지한 듯</p>
</blockquote>
<h1 id="세마포어">세마포어</h1>
<h2 id="목적">목적</h2>
<ol>
<li>세마포어란?</li>
<li>락과 조건 변수 대신에 세마포어를 사용하는 방법은 무엇인가?</li>
<li>세마포어를 사용하는 방법?</li>
<li>락과 조건 변수를 사용하여 세마포어를 구현하는 것이 가능한가? 그 반대로 세마포어를 사용하여 락과 조건 변수를 구현하는 것이 가능한가?</li>
</ol>
<h2 id="세마포어란">세마포어란?</h2>
<p>세마포어는 음이 아닌 정수 값을 갖는 객체로서 두 가지 연산(<code>wait</code>와 <code>post</code>)을 통해 조작할 수 있습니다. 세마포어는 초기값에 따라 동작이 결정되기 때문에 사용 전에 반드시 초기화를 해야 합니다.</p>
<pre><code class="language-c">int sem_wait(sem_t *s) {
    // 세마포어 s의 값을 1 감소
    // 세마포어 s의 값이 음수가 되면 해당 쓰레드는 블록됨
}

int sem_post(sem_t *s) {
    // 세마포어 s의 값을 1 증가
    // 블록된 쓰레드가 있다면 그 중 하나를 깨움
}</code></pre>
<p><code>sem_wait()</code> 함수는 세마포어의 값이 양수일 때 즉시 리턴하고, 그렇지 않으면 세마포어의 값이 양수가 될 때까지 호출한 쓰레드를 블록시킵니다. </p>
<p><code>sem_post()</code> 함수는 세마포어의 값을 증가시키고 블록된 쓰레드가 있다면 그 중 하나를 깨웁니다. </p>
<blockquote>
<p>그러니까 세마포어가 양수일 때만 접근할 수 있다는거군.</p>
</blockquote>
<h1 id="질문-3개">질문 3개</h1>
<ol>
<li>왜 락만으로는 제대로 된 병행 프로그램을 작성할 수 가 없을까요?
답변 : 락은 &quot;한 번에 한 쓰레드만 실행&quot;을 보장하지만, &quot;특정 조건이 만족될 때까지 효율적으로 대기&quot;하는 메커니즘은 제공하지 않습니다. 컨디션 변수는 쓰레드를 <strong>sleep 상태로 전환</strong>하여 CPU를 낭비하지 않고 조건이 만족될 때까지 기다릴 수 있게 해줍니다.</li>
<li>조건 검사에 while문 사용을 권장하는 이유가 무엇인가?
답변 : 시그널로 깨어났다고 해서 조건이 반드시 만족된다는 보장이 없기 때문입니다. 따라서 깨어난 후 <strong>반드시 조건을 재검사해야 합니다.</strong></li>
<li>컨디션 변수는 어떤 자료구조로 구현되나요? 그리고 왜 이런 자료구조를 사용할까요?
답변 : 큐 자료구조를 사용합니다. 큐 자료구조를 사용하는 이유는 조건이 충족될 때까지 CPU 시간을 낭비하는 <strong>스핀 대기(Spin-waiting)</strong> 대신, 스레드를 효율적으로 재울 수 있습니다. 조건이 충족되면 <code>signal</code>을 통해 <strong>특정 스레드만 선택적</strong>으로 깨울 수 있습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 10주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-10%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-10%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 11 Nov 2025 08:20:12 GMT</pubDate>
            <description><![CDATA[<h1 id="참고">참고</h1>
<p><a href="https://os2024.jeju.ai/week11/threads-locks.html">https://os2024.jeju.ai/week11/threads-locks.html</a></p>
<p><a href="https://os2024.jeju.ai/week11/threads-locks-usage.html">https://os2024.jeju.ai/week11/threads-locks-usage.html</a></p>
<h1 id="락">락</h1>
<p>락(Lock)은 코드의 특정 영역을 감싸서 한 순간에 오로지 한 스레드만 이 영역에 접근할 수 있도록 해주는 동기화 메커니즘입니다. 즉, 락은 여러 스레드가 공유 자원에 동시에 접근하는 것을 제어하여 상호 배제(Mutual Exclusion)를 보장합니다.</p>
<h1 id="락의-개념">락의 개념</h1>
<p>예를 들어, 다음과 같은 공유 자원에 대한 연산이 있다고 가정해봅시다.</p>
<pre><code class="language-c">balance = balance + 1;</code></pre>
<p>이 코드를 임계 영역(Critical Section)이라고 하며, 락을 사용하여 다음과 같이 보호할 수 있습니다.</p>
<pre><code class="language-c">lock_t mutex; // 전역 변수로 선언된 락
...
lock(&amp;mutex);
balance = balance + 1;
unlock(&amp;mutex);</code></pre>
<p>락은 하나의 변수로 표현되며, 사용하기 전에 먼저 선언해야 합니다. 이 락 변수는 두 가지 상태를 가질 수 있습니다.</p>
<ol>
<li>사용 가능 상태: 어느 스레드도 락을 가지고 있지 않은 상태</li>
<li>사용 중 상태: 임계 영역에서 정확히 하나의 스레드가 락을 획득한 상태</li>
</ol>
<p><code>lock()</code> 함수는 락을 획득하려고 시도하고, <code>unlock()</code> 함수는 획득한 락을 해제합니다. 어떤 스레드가 <code>lock()</code> 함수를 호출하여 락을 획득하면, 해당 스레드를 락의 소유자(Owner)라고 부릅니다. 락이 이미 다른 스레드에 의해 사용 중인 경우, <code>lock()</code> 함수는 해당 락이 해제될 때까지 대기합니다.</p>
<p>락의 소유자가 <code>unlock()</code> 함수를 호출하면 락은 다시 사용 가능한 상태가 됩니다. 만약 어떤 스레드도 해당 락을 기다리고 있지 않다면, 락은 사용 가능한 상태로 유지됩니다.</p>
<p>락은 프로그래머에게 스케줄링에 대한 제어권을 제공하여 특정 코드 영역 내에서 한 번에 하나의 스레드만 실행되도록 보장합니다. 이를 통해 스레드 간의 잘못된 실행 순서로 인한 문제를 예방할 수 있습니다.</p>
<h1 id="락-기반-병행-자료-구조">락 기반 병행 자료 구조</h1>
<p>병행 자료 구조(Concurrent Data Structures)란 다수의 스레드가 동시에 접근할 수 있는 자료 구조를 말합니다. 이러한 자료 구조는 병행성(Concurrency) 문제를 해결하기 위해 락(Lock)을 사용하여 데이터의 일관성(Consistency)을 유지합니다.</p>
<h1 id="병행-카운터">병행 카운터</h1>
<blockquote>
<p>여러 스레드가 동시에 카운터 값을 변경할 때도 그 값이 정확하게 유지되어야 한다.</p>
</blockquote>
<pre><code class="language-c">pthread_mutex_t lock;
int counter = 0;

void increment() {
    pthread_mutex_lock(&amp;lock);  // 락 획득
    counter++;                  // 카운터 증가
    pthread_mutex_unlock(&amp;lock);  // 락 해제
}

void decrement() {
    pthread_mutex_lock(&amp;lock);  // 락 획득
    counter--;                  // 카운터 감소
    pthread_mutex_unlock(&amp;lock);  // 락 해제
}</code></pre>
<p>increment()와 <code>decrement()</code> 함수를 사용하여 락을 획득한 후 카운터 값을 변경하고, 마지막에 락을 해제하는 방법을 통해 카운터 값의 일관성을 유지한다. 간단하게 구현할 수 있지만 단점으로는 모든 연산에 락을 사용하므로 성능 저하가 발생할 수 있다.</p>
<p>병행 카운터는 웹 서버에서 활성 사용자 수를 추적하거나, 데이터베이스에서 특정 이벤트 발생 횟수를 기록하는 등 다양한 응용 프로그램에서 사용됩니다.</p>
<h1 id="병행-연결-리스트">병행 연결 리스트</h1>
<pre><code class="language-c">typedef struct Node {
    int data;
    struct Node* next;
    pthread_mutex_t lock;
} Node;

Node* head = NULL;
pthread_mutex_t list_lock = PTHREAD_MUTEX_INITIALIZER;

void add_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    new_node-&gt;data = value;
    pthread_mutex_init(&amp;new_node-&gt;lock, NULL);

    pthread_mutex_lock(&amp;list_lock);  // 리스트 락 획득
    new_node-&gt;next = head;
    head = new_node;
    pthread_mutex_unlock(&amp;list_lock);  // 리스트 락 해제
}

void remove_node(int value) {
    pthread_mutex_lock(&amp;list_lock);  // 리스트 락 획득
    Node* current = head;
    Node* prev = NULL;

    while (current != NULL) {
        if (current-&gt;data == value) {
            if (prev == NULL) {
                head = current-&gt;next;
            } else {
                prev-&gt;next = current-&gt;next;
            }
            pthread_mutex_destroy(&amp;current-&gt;lock);
            free(current);
            break;
        }
        prev = current;
        current = current-&gt;next;
    }
    pthread_mutex_unlock(&amp;list_lock);  // 리스트 락 해제
}</code></pre>
<ul>
<li>연결 리스트 전체에 대한 하나의 락(<code>list_lock</code>)을 사용하여 노드 추가와 삭제 연산을 제어.</li>
<li>노드 추가 시에는 새로운 노드를 생성하고 락을 초기화한 후, 리스트 락을 획득하여 새 노드를 리스트의 head에 추가.</li>
<li>노드 삭제 시에는 리스트 락을 획득한 후 리스트를 탐색하여 해당 노드를 찾아 제거.</li>
</ul>
<h3 id="병행-연결-리스트란">병행 연결 리스트란?</h3>
<p>다수의 스레드가 동시에 노드를 추가하거나 삭제할 수 있는 연결 리스트입니다. 이때 락을 사용하여 스레드 간의 노드 접근을 안전하게 제어한다. </p>
<p>병행 연결 리스트는 노드별로 락을 사용하거나, 전체 리스트에 대한 하나의 락을 사용할 수 있습니다.</p>
<blockquote>
<p>스레드가 노드에 접근하려는 이유가 뭐지?</p>
</blockquote>
<blockquote>
<p>병행 연결 리스트에서 전체 리스트 락(one-lock) 방식과 노드별 락(per-node lock) 방식의 차이는 무엇인가?</p>
</blockquote>
<h3 id="병행-리스트-vs-병행-카운터">병행 리스트 vs 병행 카운터</h3>
<blockquote>
<p>병행 리스트의 단점으로 카운트 값을 변경할 때마다 락을 걸어야 하는 문제로 인해 성능 저하가 발생한다고 했는데 병행 리스트는 병행 카운터보다 얼마만큼의 성능 향상이 발생하는 걸까?
예를 들어, 병행 카운터는 카운터 값을 변경할 때마다 락을 걸어줘야 해서 성능 저하가 발생할 수 있지만 병행 리스트는 전체 리스트에 대한 하나의 락을 사용할 수 있으니까 병행 카운터보다는 더 좋은 퍼포먼스를 낼 수 있지 않을까?</p>
</blockquote>
<h3 id="병행-연결-리스트를-사용하는-이유가-뭘까">병행 연결 리스트를 사용하는 이유가 뭘까?</h3>
<p>운영체제의 스케줄러, 메모리 할당기, 네트워크 프로토콜 스택 등 다양한 시스템 소프트웨어에서 사용됩니다. 예를 들어, 스케줄러는 실행 중인 프로세스나 스레드의 목록을 관리하기 위해 병행 연결 리스트를 활용할 수 있습니다.</p>
<h1 id="병행-큐">병행 큐</h1>
<pre><code class="language-c">typedef struct QueueNode {
    int data;
    struct QueueNode* next;
} QueueNode;

typedef struct {
    QueueNode* front;
    QueueNode* rear;
    pthread_mutex_t lock;
    pthread_cond_t cond;
} Queue;

void enqueue(Queue* q, int value) {
    QueueNode* new_node = (QueueNode*)malloc(sizeof(QueueNode));
    new_node-&gt;data = value;
    new_node-&gt;next = NULL;

    pthread_mutex_lock(&amp;q-&gt;lock);  // 큐 락 획득
    if (q-&gt;rear == NULL) {
        q-&gt;front = new_node;
        q-&gt;rear = new_node;
    } else {
        q-&gt;rear-&gt;next = new_node;
        q-&gt;rear = new_node;
    }
    pthread_cond_signal(&amp;q-&gt;cond);  // 대기 중인 스레드에 시그널 전송
    pthread_mutex_unlock(&amp;q-&gt;lock);  // 큐 락 해제
}

int dequeue(Queue* q) {
    pthread_mutex_lock(&amp;q-&gt;lock);  // 큐 락 획득
    while (q-&gt;front == NULL) {
        pthread_cond_wait(&amp;q-&gt;cond, &amp;q-&gt;lock);  // 조건 변수를 사용하여 대기
    }
    QueueNode* temp = q-&gt;front;
    int value = temp-&gt;data;
    q-&gt;front = q-&gt;front-&gt;next;
    if (q-&gt;front == NULL) {
        q-&gt;rear = NULL;
    }
    free(temp);
    pthread_mutex_unlock(&amp;q-&gt;lock);  // 큐 락 해제
    return value;
}</code></pre>
<blockquote>
<p>대기 중인 스레드에 시그널을 전송한다?</p>
</blockquote>
<blockquote>
<p>dequeue 부분에서 조건 변수를 사용하여 대기할 때 큐가 비어있다면 새로운 노드가 생성될 때까지 스레드를 대기시키는 건가?</p>
</blockquote>
<p>큐 전체에 대한 하나의 락(<code>lock</code>)과 조건 변수(<code>cond</code>)를 사용합니다. <code>enqueue()</code> 함수에서는 큐 락을 획득한 후 새로운 노드를 큐의 rear에 추가하고, 대기 중인 스레드에 시그널을 보냅니다. <code>dequeue()</code> 함수에서는 큐 락을 획득하고, 큐가 비어 있는 경우 조건 변수를 사용하여 대기합니다. 큐에 요소가 있으면 front 노드를 제거하고 해당 값을 반환합니다.</p>
<h3 id="병행-큐란">병행 큐란?</h3>
<p>다수의 스레드가 동시에 요소를 삽입하거나 제거할 수 있는 큐.</p>
<h3 id="병행-큐를-사용하는-이유가-뭘까">병행 큐를 사용하는 이유가 뭘까?</h3>
<p>병행 큐는 삽입 연산과 제거 연산에 대해 별도의 락을 사용할 수 있습니다. 이를 통해 삽입과 제거가 동시에 이루어질 수 있어 성능이 향상.</p>
<p>로그 시스템, 작업 스케줄러, 메시지 전달 시스템 등 생산자-소비자 패턴이 필요한 다양한 응용 프로그램에서 사용될 수 있음.</p>
<blockquote>
<p>삽입과 제거 연산에 별도의 락을 사용할 수 있다는 것은 병행 카운터처럼 모든 연산에 락을 사용하는 것이고, 결국 성능 저하가 발생한다는 말이 아닌가?</p>
</blockquote>
<blockquote>
<p>삽입과 제거가 동시에 이루어진다는 게 무슨 말이지?</p>
</blockquote>
<h3 id="병행-리스트-vs-병행-큐">병행 리스트 vs 병행 큐</h3>
<blockquote>
<p>병행 리스트는 병행 큐처럼 동시에 삽입 삭제가 불가능할까?</p>
</blockquote>
<h1 id="병행-해시-테이블">병행 해시 테이블</h1>
<pre><code class="language-c">#define HASH_SIZE 101

typedef struct HashNode {
    int key;
    int value;
    struct HashNode* next;
    pthread_mutex_t lock;
} HashNode;

typedef struct {
    HashNode* buckets[HASH_SIZE];
    pthread_mutex_t table_lock;
} HashTable;

unsigned int hash(int key) {
    return key % HASH_SIZE;
}

void insert(HashTable* table, int key, int value) {
    unsigned int index = hash(key);
    pthread_mutex_lock(&amp;table-&gt;table_lock);  // 테이블 락 획득

    HashNode* new_node = (HashNode*)malloc(sizeof(HashNode));
    new_node-&gt;key = key;
    new_node-&gt;value = value;
    pthread_mutex_init(&amp;new_node-&gt;lock, NULL);

    new_node-&gt;next = table-&gt;buckets[index];
    table-&gt;buckets[index] = new_node;

    pthread_mutex_unlock(&amp;table-&gt;table_lock);  // 테이블 락 해제
}

int lookup(HashTable* table, int key) {
    unsigned int index = hash(key);
    pthread_mutex_lock(&amp;table-&gt;table_lock);  // 테이블 락 획득

    HashNode* current = table-&gt;buckets[index];
    while (current != NULL) {
        if (current-&gt;key == key) {
            pthread_mutex_unlock(&amp;table-&gt;table_lock);  // 테이블 락 해제
            return current-&gt;value;
        }
        current = current-&gt;next;
    }

    pthread_mutex_unlock(&amp;table-&gt;table_lock);  // 테이블 락 해제
    return -1; // 키를 찾지 못한 경우
}</code></pre>
<h3 id="병행-해시-테이블이란">병행 해시 테이블이란?</h3>
<p>병행 해시 테이블은 다수의 스레드가 동시에 키-값 쌍을 삽입, 삭제 또는 조회할 수 있는 해시 테이블입니다. 병행 해시 테이블은 성능 향상을 위해 버킷(Bucket) 단위로 락을 사용할 수 있습니다.</p>
<p>각 버킷에 대해 개별적인 락을 사용하면 서로 다른 버킷에 대한 연산이 동시에 이루어질 수 있어 성능이 크게 향상.</p>
<blockquote>
<p>버킷 단위로 락을 사용할 수 있다는 게 무슨 말일까? 그리고 이게 성능 향상과 연관되어 있는 이유는 뭘까?</p>
</blockquote>
<h3 id="병행-해시-테이블은-어디에-사용될까">병행 해시 테이블은 어디에 사용될까?</h3>
<p>웹 서버의 세션 관리 시스템에서는 세션 정보를 병행 해시 테이블에 저장하여 여러 스레드가 동시에 세션 정보를 효율적으로 조회하고 갱신할 수 있어서 데이터베이스 시스템, 캐시 시스템, 네트워크 라우팅 테이블 등 다양한 응용 프로그램에서 사용됩니다.</p>
<blockquote>
<p>여러 스레드가 동시에 세션 정보에 접근하는 경우가 어떤 게 있을까?</p>
</blockquote>
<h3 id="병행-해시-테이블은-병행-리스트-병행-큐와-어떤-다른-점은-뭐지">병행 해시 테이블은 병행 리스트, 병행 큐와 어떤 다른 점은 뭐지?</h3>
<h1 id="락-기반-병행-자료-구조의-장단점"><strong>락 기반 병행 자료 구조의 장단점</strong></h1>
<p>락 기반의 병행 자료 구조는 다음과 같은 장단점을 가지고 있습니다.</p>
<p>장점:</p>
<ul>
<li>구현이 비교적 간단하고 직관적입니다.</li>
<li>데이터의 일관성을 보장할 수 있습니다.</li>
<li>다양한 자료 구조에 적용할 수 있습니다.</li>
</ul>
<p>단점:</p>
<ul>
<li>락 사용으로 인한 성능 저하가 발생할 수 있습니다.</li>
<li>락 경합이 발생하면 효율이 크게 떨어질 수 있습니다.</li>
<li>데드락이나 라이브락 등의 문제가 발생할 수 있습니다.</li>
<li>락의 개수가 많아질수록 메모리 오버헤드가 증가합니다.</li>
</ul>
<h1 id="질문">질문</h1>
<ol>
<li>병행 자료 구조(Concurrent Data Structure)란 무엇이며, 왜 락(Lock)을 사용하는가?</li>
<li>병행 카운터(Concurrent Counter)를 락 기반으로 구현했을 때의 장점과 단점은 무엇인가?</li>
<li>락의 granularity(범위) 조절이 왜 중요한가?</li>
<li>락 기반 병행 자료 구조의 장점은 무엇이며, 반대로 어떤 단점이 존재하는가?</li>
<li>병행 연결 리스트에서 전체 리스트 락(one-lock) 방식과 노드별 락(per-node lock) 방식의 차이는 무엇인가?</li>
<li>노드별 락을 사용할 때 생길 수 있는 데드락(Deadlock) 또는 라이브락(Livelock)의 시나리오를 예로 들어 설명해 보라.</li>
<li>병행 해시 테이블(Concurrent Hash Table)을 버킷 단위 락(bucket-level lock)로 구현하는 것과 테이블 전체 락(table-level lock) 방식의 차이점은 무엇인가?</li>
<li>왜 “락 없는(Lock-Free)” 병행 자료 구조가 대안으로 제시되었으며, 그 방식이 락 기반 방식에 비해 가지는 장점과 단점은 무엇인가?</li>
<li>실제 시스템(예: 웹 서버, 데이터베이스, 네트워크 라우팅 테이블)에서 병행 자료 구조가 어떤 용도로 사용되는지 예를 들어 설명하라.</li>
<li>데드락, 라이브락, 락 경합(Lock Contention) 등의 문제가 병행 자료 구조에서 어떻게 발생할 수 있는가?</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 9주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Thu, 06 Nov 2025 07:28:20 GMT</pubDate>
            <description><![CDATA[<h1 id="참고">참고</h1>
<p><a href="https://os2024.jeju.ai/week10/dialogue.html">https://os2024.jeju.ai/week10/dialogue.html</a></p>
<p><a href="https://os2024.jeju.ai/week10/threads-intro.html#">https://os2024.jeju.ai/week10/threads-intro.html#</a></p>
<p><a href="https://os2024.jeju.ai/week10/threads-api.html">https://os2024.jeju.ai/week10/threads-api.html</a></p>
<h1 id="목적">목적</h1>
<p>쓰레드들이 메모리에 접근하는 것을 조정하지 않으면 프로그램이 예상처럼 동작하지 않을 수도 있고, 운영체제는 그 자체로 최초의 동시 프로그램이기 때문이다.</p>
<h1 id="병행성이란">병행성이란?</h1>
<p>여러 사람이 복숭아를 먹고 싶어 한다고 해보자꾸나.</p>
<p>다른 사람이 보고 있는 복숭아를 나도 같이 보고 있는 경우가 존재. 내가 집으려고 할 때 상대가 나보다 먼저 집게 되면 어떻게 되는가?</p>
<p>줄을 세운 후에 자기 차례가 되면 복숭아를 집어 먹도록 하는 방법을 사용하면?</p>
<p>문제는 뭔지 알고 있나? 줄을 직접 다 세워줘야 한다. 하지만 한 번에 한 명씩 복숭아를 집기 때문에 느리지만 정확하다.</p>
<p>여기서 복숭아 == 쓰레드 이다.</p>
<p>쓰레드들이 메모리를 접근하는 것을 조정하지 않으면 프로그램이 예상처럼 동작하지 않을 수도 있다.</p>
<p>이번 시간의 목표는 락(lock)과 컨디션 변수(conditional variables)와 같은 기본 동작으로 멀티쓰레드 프로그램을 지원하는 방법을 공부한다.</p>
<p>이유는 운영체제는 그 자체로 최초의 동시 프로그램이기 때문이야.</p>
<h1 id="쓰레드-생성예제">쓰레드 생성(예제)</h1>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;assert.h&gt;
#include &lt;pthread.h&gt;

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

int main(int argc, char *argv[]) {
    pthread_t p1, p2;
    int rc;

    printf(&quot;main: begin\n&quot;);

    rc = pthread_create(&amp;p1, NULL, mythread, &quot;A&quot;);
    assert(rc == 0);

    rc = pthread_create(&amp;p2, NULL, mythread, &quot;B&quot;);
    assert(rc == 0);

    // 쓰레드가 종료될 때까지 대기하기 위해 join 사용
    rc = pthread_join(p1, NULL); assert(rc == 0);
    rc = pthread_join(p2, NULL); assert(rc == 0);

    printf(&quot;main: end\n&quot;);
    return 0;
}</code></pre>
<p>위에서 쓰레드 A가 먼저 생성될 수도 있고, B가 먼저 생성될 수도 있다.</p>
<pre><code class="language-c">main: begin
A
B
main: end</code></pre>
<pre><code class="language-c">main: begin
B
A
main: end</code></pre>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/87cd8c0a-ce9a-4dd2-99e0-1ebd8a963e9a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/2cb1c645-9a44-4e1c-a4ad-7b6fdd2bb5e1/image.png" alt=""></p>
<p>위의 실행 순서는 쓰레드 실행의 다양한 가능성 중 일부일 뿐, 유일한 실행 순서는 아닙니다. 스케줄러가 특정 시점에 어떤 쓰레드를 실행하느냐에 따라 다양한 순서가 나올 수 있습니다.</p>
<p>쓰레드 1이 쓰레드 2보다 먼저 생성되었더라도 스케줄러가 쓰레드 2를 먼저 실행한다면 “B”가 “A”보다 먼저 출력될 수 있다.</p>
<blockquote>
<p>쓰레드의 동작을 예측하는 방법은 없을까?</p>
</blockquote>
<p>쓰레드의 생성은 함수 호출과 유사해 보이지만, 함수 호출에서는 함수가 실행을 마치면 호출자(caller)에게 리턴하는 반면, 쓰레드 생성에서는 새로운 쓰레드가 생성되어 호출자와는 독립적으로 실행됩니다.</p>
<p>따라서 쓰레드는 생성 함수가 리턴되기 전에 실행될 수 있다.</p>
<blockquote>
<p>쓰레드는 프로그램의 실행 흐름을 복잡하게 만든다. 어떤 쓰레드가 언제 실행될지 정확히 예측이 불가능하기 때문이다.</p>
</blockquote>
<h1 id="데이터-공유">데이터 공유</h1>
<p>전역 공유 변수를 갱신하는 두 개의 쓰레드를 사용한 간단한 예제</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;pthread.h&gt;
#include &quot;mythreads.h&quot;

static volatile int counter = 0;

// mythread()
// 인자로 전달된 문자열을 출력하고
// 10000000번 반복하며 counter에 1을 더하는 함수
// 끝나면 다시 인자 문자열을 출력
void *mythread(void *arg) {
    printf(&quot;%s: begin\n&quot;, (char *) arg);

    for (int i = 0; i &lt; 1e7; i++) {
        counter = counter + 1;
    }

    printf(&quot;%s: done\n&quot;, (char *) arg);
    return NULL;
}

// main()
// 두 개의 쓰레드를 생성하고 (pthread_create)
// 기다린다 (pthread_join)
int main(int argc, char *argv[]) {
    pthread_t p1, p2;

    printf(&quot;main: begin (counter = %d)\n&quot;, counter);

    Pthread_create(&amp;p1, NULL, mythread, &quot;A&quot;);
    Pthread_create(&amp;p2, NULL, mythread, &quot;B&quot;);

    // 쓰레드가 종료될 때까지 기다리기 위해 join 사용
    Pthread_join(p1, NULL);
    Pthread_join(p2, NULL);

    printf(&quot;main: done with both (counter = %d)\n&quot;, counter);
    return 0;
}</code></pre>
<p>쓰레드 간의 상호작용, 특히 공유 데이터에 대한 접근을 다뤘음.</p>
<p>따라서 최종 결과는 20,000,000이 되어야 한다. 각 작업자 쓰레드는 공유 변수 <code>counter</code>에 1,000만 번(1e7) 1을 더했음.</p>
<pre><code class="language-c">main: begin (counter = 0)
A: begin
B: begin
A: done
B: done
main: done with both (counter = 20000000)</code></pre>
<pre><code class="language-c">main: begin (counter = 0)
A: begin
B: begin
A: done
B: done
main: done with both (counter = 19345221)</code></pre>
<pre><code class="language-c">main: begin (counter = 0)
A: begin
B: begin
A: done
B: done
main: done with both (counter = 19221041)</code></pre>
<p>여기서 잠깐….왜 결과값이 바뀌는거지?</p>
<p>일단 mythread 함수를 살펴보면 20000000이 당연히 나와야할 것처럼 보인다…</p>
<p>힌트가 될만한 말이 없을까?</p>
<blockquote>
<p>쓰레드 간의 상호작용, 특히 공유 데이터에 대한 접근</p>
</blockquote>
<p>공유 데이터… 어디서 많이 들어본 말이다…</p>
<blockquote>
<p>만약 스레드 A와 B가 동시에 공유 데이터에 접근해서 1을 더한다면?</p>
</blockquote>
<blockquote>
<p>즉, A가 접근해서 counter를 증가시키기 전에 B가 접근하게 되면 A의 연산과정이 무시되면서 잘못된 결과가 나온 것 같다.</p>
</blockquote>
<h1 id="스케줄링">스케줄링</h1>
<p>목적 : 왜 결과값이 ‘20000000’이 아니라 다른 값이 나올 수 가 있는가?</p>
<p>핵심은 <code>counter</code> 갱신을 위해 컴파일러가 생성한 코드의 실행 순서이다.</p>
<pre><code class="language-c">mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c</code></pre>
<p>이 예제에서 <code>counter</code> 변수의 주소는 <code>0x8049a1c</code>라고 가정합니다. </p>
<ol>
<li>x86의 <code>mov</code> 명령어가 지정된 메모리 주소의 값을 읽어 <code>eax</code> 레지스터에 저장합니다. </li>
<li><code>eax</code> 레지스터의 값에 1(<code>0x1</code>)을 더합니다. </li>
<li><code>eax</code>에 저장된 값을 메모리의 원래 주소에 다시 저장합니다.</li>
</ol>
<p>타이머 인터럽트가 발생하여 운영체제가 실행 중인 쓰레드의 PC 값과 <code>eax</code>를 포함한 레지스터들의 현재 상태를 쓰레드의 TCB(Thread Control Block)에 저장합니다. </p>
<p>쓰레드 2가 선택되고 <code>counter</code> 값을 증가시키는 동일한 코드 영역에 진입합니다. 첫 번째 명령어를 실행하여 <code>counter</code> 값을 읽어 <code>eax</code>에 저장합니다. </p>
<p>각 쓰레드는 개별적인 쓰레드 전용 레지스터를 가지고 있습니다. 사용 중이던 레지스터들을 저장하고 복구하는 문맥 교환 코드에 의해 이 레지스터들은 가상화됩니다.</p>
<p> <code>counter</code> 값은 아직 50이므로 쓰레드 2의 <code>eax</code> 값은 50입니다. 쓰레드 2가 다음 두 문장을 실행하면 <code>eax</code> 값을 1 증가시켜 <code>eax</code>는 51이 되고, <code>eax</code> 값을 <code>counter</code>(주소 <code>0x8049a1c</code>)에 저장합니다. 전역 변수 <code>counter</code>는 이제 51이 됩니다.</p>
<p>문맥 교환이 발생하면 쓰레드 1이 다시 실행됩니다. 이전 상황을 떠올려 보면 쓰레드 1은 <code>mov</code>와 <code>add</code> 동작을 실행했고 이제 마지막 <code>mov</code> 명령어를 수행하려는 중입니다. 그리고 <code>eax</code>는 51입니다. </p>
<p>따라서 <code>mov</code> 명령어가 실행되면 레지스터의 값을 메모리에 저장하여 <code>counter</code>의 값은 다시 51이 됩니다. 따라서 <code>counter</code>의 값을 증가시키는 코드가 두 번 수행 → 50에서 시작한 <code>counter</code>의 값은 1만 증가하여 51이 되는 문제가 발생됨.</p>
<h1 id="예제-코드">예제 코드</h1>
<blockquote>
<p>운영체제가 쓰레드를 생성하고 제어하는 데 어떤 인터페이스를 제공해야 할까? 어떻게 이 인터페이스를 설계해야 쉽고 유용하게 사용할 수 있을까?</p>
</blockquote>
<h2 id="쓰레드-생성">쓰레드 생성</h2>
<pre><code class="language-c">#include &lt;pthread.h&gt;
int pthread_create(      
pthread_t * thread,
const pthread_attr_t * attr,
void * (*start_routine)(void*),
void * arg
);</code></pre>
<pre><code class="language-c">#include &lt;pthread.h&gt;

typedef struct __myarg_t {
    int a;
    int b;
} myarg_t;

void *mythread(void *arg) {
    myarg_t *m = (myarg_t *) arg;
    printf(“%d %d\n ”, m−&gt;a, m−&gt;b);
    return NULL;
}

int
main(int argc, char *argv[]) {
    pthread_t p;
    int rc;

    myarg_t args;
    args.a = 10;
    args.b = 20;
    rc = pthread_create(&amp;p, NULL, mythread, &amp;args);
    ...
}</code></pre>
<p>쓰레드를 생성하고 나면, 실행 중인 모든 쓰레드와 같은 주소 공간에서 실행되는 또 하나의 실행 개체를 보유하게 된다.</p>
<h2 id="쓰레드-종료">쓰레드 종료</h2>
<pre><code class="language-c">int pthread_join(pthread_t thread, void **value_ptr);</code></pre>
<ol>
<li><code>thread</code>: 기다릴 쓰레드의 ID입니다. 이 값은 <code>pthread_create()</code> 호출 시 초기화됩니다.</li>
<li><code>value_ptr</code>: 쓰레드의 반환값을 받을 포인터의 포인터입니다. 쓰레드 함수가 <code>void *</code>를 반환하므로, 이를 받기 위해 이중 포인터가 사용됩니다.</li>
</ol>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;pthread.h&gt;
#include &lt;stdlib.h&gt;

typedef struct {
  int a;
  int b;
} myarg_t;

typedef struct {
  int x;
  int y;
} myret_t;

void *mythread(void *arg) {
  myarg_t *m = (myarg_t *) arg;
  printf(&quot;%d %d\n&quot;, m-&gt;a, m-&gt;b);

  myret_t *r = malloc(sizeof(myret_t));
  r-&gt;x = 1;
  r-&gt;y = 2;
  return (void *) r;
}

int main() {
  pthread_t p;
  myarg_t args = { 10, 20 };
  myret_t *retval;

  pthread_create(&amp;p, NULL, mythread, &amp;args);
  pthread_join(p, (void **) &amp;retval);

  printf(&quot;returned %d %d\n&quot;, retval-&gt;x, retval-&gt;y);
  free(retval);

  return 0;
}</code></pre>
<p>이 예제에서는 <code>myarg_t</code>를 통해 쓰레드 함수에 인자를 전달하고, <code>myret_t</code>를 통해 반환값을 받습니다. main 함수에서는 <code>pthread_join()</code>을 호출하여 쓰레드가 종료될 때까지 기다립니다. 쓰레드가 종료되면 반환값을 <code>retval</code>을 통해 받고, 이를 출력합니다.</p>
<h2 id="락">락</h2>
<pre><code class="language-c">int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);</code></pre>
<p>이 함수들은 사용하기 쉽습니다. 공유 자원에 접근하기 전에 <code>pthread_mutex_lock()</code>을 호출하여 락을 획득하고, 접근이 끝나면 <code>pthread_mutex_unlock()</code>을 호출하여 락을 해제합니다.</p>
<pre><code class="language-c">pthread_mutex_t lock;

pthread_mutex_lock(&amp;lock);
// 공유 자원 접근
x = x + 1;
pthread_mutex_unlock(&amp;lock);</code></pre>
<p><code>pthread_mutex_lock()</code>이 호출되었을 때, 다른 쓰레드가 이미 락을 획득한 상태라면 해당 쓰레드는 락이 해제될 때까지 대기합니다. 락을 획득한 쓰레드만이 <code>pthread_mutex_unlock()</code>을 호출할 수 있습니다.</p>
<h1 id="질문-3가지">질문 3가지</h1>
<ol>
<li>데이터 공유 파트에서 총 결과값이 20000000이 나와야 하는데 그렇지 않은 결과값이 존재했다. 원인은 무엇이였고, 어떻게 해결할 수 있을까?</li>
<li>예제에서 스레드 생성 순서와 결과 출력 순서가 달랐다. 이유가 무엇이였는가?</li>
<li>스레드의 문맥 교환과 프로세스의 문맥 교환에는 차이점이 있다. 어떤 차이가 있는가?</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 8주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-8%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-8%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Thu, 30 Oct 2025 12:06:27 GMT</pubDate>
            <description><![CDATA[<h1 id="참고">참고</h1>
<p><a href="https://os2024.jeju.ai/week09/vm-paging.html">https://os2024.jeju.ai/week09/vm-paging.html</a></p>
<p><a href="https://os2024.jeju.ai/week09/vm-tlbs.html">https://os2024.jeju.ai/week09/vm-tlbs.html</a></p>
<p><a href="https://os2024.jeju.ai/week09/vm-swap.html">https://os2024.jeju.ai/week09/vm-swap.html</a></p>
<p><a href="https://os2024.jeju.ai/week09/summary.html">https://os2024.jeju.ai/week09/summary.html</a></p>
<h1 id="목적이-뭐지">목적이 뭐지?</h1>
<p>주소 공간을 효율적으로 사용하는 방법?</p>
<h1 id="페이징">페이징</h1>
<p>페이징을 알아야 되는 이유가 뭘까?</p>
<blockquote>
<ul>
<li>세상에 나갔을 때 시스템들이 실제로 어떻게 동작하는지 알 수 있다.</li>
</ul>
</blockquote>
<ul>
<li>가상 메모리가 어떻게 동작하는지에 대한 모델을 잘 이해하고 있으면 여러 종류의 흥미로운 성능 관련 문제들을 진단하는 데 도움이 된다.</li>
<li>어떻게 동작하는지 개념적 모델을 만들 수 있다. 개념적 모델을 만들면 시스템이 예상과 다르게 동작하더라도 놀라지 않을 수 있고, 시스템이 어떻게 동작할지를 생각만으로도 예상을 할 수 있게 된다.</li>
</ul>
<p>페이징에서는 프로세스의 주소 공간을 가변 크기의 논리 세그멘트로 나누는 대신, 고정 크기의 단위인 페이지(Page)로 나눕니다. 이에 상응하여 물리 메모리도 페이지 프레임(Page Frame)이라고 불리는 고정 크기의 슬롯 배열로 간주합니다. 각 페이지 프레임은 하나의 가상 메모리 페이지를 저장할 수 있습니다.</p>
<blockquote>
<p>핵심 질문: 페이징을 사용하여 어떻게 메모리를 가상화할 수 있을까요? 세그멘테이션의 문제점을 해결하기 위해 페이징을 어떻게 활용할 수 있을까요? 기본 기법은 무엇이며, 공간과 시간 오버헤드를 최소화하면서 그 기법을 잘 동작하게 만들기 위한 방법은 무엇일까요?</p>
</blockquote>
<p>아래 그림은 총 64바이트이며 4개의 16바이트 페이지로 구성된 작은 주소 공간의 예를 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/ccb9c662-a408-470e-8890-fbc7ff45dc8d/image.png" alt=""></p>
<p>물리 메모리는 고정 크기의 슬롯들로 구성되며, 이 예에서는 8개의 페이지 프레임으로 이루어진 총 128바이트의 비현실적으로 작은 물리 메모리를 가정합니다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/7bae92ab-a9ce-4357-a1d9-09124b26fdfe/image.png" alt=""></p>
<p>가상 주소 공간(Address Space, AS)의 페이지들이 페이지 프레임에 분산 배치되어 있음을 알 수 있습니다. 또한 운영체제가 자기 자신을 위해 물리 메모리의 일부를 사용하는 것도 볼 수 있습니다.</p>
<p>또 다른 장점은 단순함입니다. 운영체제가 64바이트의 가상 주소 공간을 8페이지 물리 메모리에 배치하려면, 단순히 비어 있는 4개의 페이지만 찾으면 됩니다. 이를 위해 운영체제는 빈 페이지 리스트를 유지하고, 리스트의 첫 네 개 페이지를 선택할 수 있습니다.</p>
<p>위의 예시에서는 가상 페이지(VP) 0은 물리 페이지 프레임(PF) 3에, VP 1은 PF 7에, VP 2는 PF 5에, VP 3은 PF 2에 매핑되어 있습니다. 페이지 테이블은 프로세스마다 존재한다는 점을 기억해야 합니다. 다른 프로세스를 실행한다면, 그 프로세스를 위한 별도의 페이지 테이블이 필요할 것입니다.</p>
<h1 id="페이징이-차지하는-용량">페이징이 차지하는 용량</h1>
<p>페이지 테이블은 매우 커질 수 있습니다. 예를 들어, 4KB 크기의 페이지를 가지는 전형적인 32비트 주소 공간을 생각해 보겠습니다. 이 가상 주소는 20비트 VPN과 12비트 Offset으로 구성됩니다(2^12 = 4KB, 나머지는 VPN).</p>
<p>20비트의 VPN은 운영체제가 각 프로세스를 위해 관리해야 하는 변환의 개수가 2^20개라는 것을 의미합니다. 물리 주소로의 변환 정보와 기타 필요한 정보를 저장하기 위해 페이지 테이블 항목(Page Table Entry, PTE)마다 4바이트가 필요하다고 가정하면, 각 페이지 테이블을 저장하기 위해 4MB(4바이트 * 2^20)의 메모리가 필요합니다.</p>
<p>만약 100개의 프로세스가 실행 중이라면 400MB의 메모리가 필요하다는 것을 의미합니다. 현재와 같이 GB 단위의 메모리를 갖고 있는 상황에서도 주소 변환을 위해 이렇게 많은 메모리가 필요하다는 것은 다소 비정상적입니다.</p>
<blockquote>
<p>프로그램을 하나 실행할 때마다 4byte를 먹는 비정상적인 상황</p>
</blockquote>
<h1 id="페이징이-느려">페이징이 느려?</h1>
<p>페이징을 사용할 때 페이지 테이블의 크기가 메모리 상에서 매우 크게 증가할 가능성이 있고, 이로 인해 처리 속도가 저하될 수 있습니다.</p>
<pre><code class="language-jsx">movl 21, %eax</code></pre>
<p>이 명령어는 21번 메모리 주소에서 데이터를 가져와 eax 레지스터에 저장합니다. 여기서는 명령어 자체의 반입은 고려하지 않고, 데이터 21에 대한 참조만 살펴보겠습니다.</p>
<p>데이터를 가져오기 위해서는 먼저 가상 주소 21을 물리 주소 117로 변환해야 합니다. 주소 117에서 데이터를 가져오기 전에 시스템은 다음과 같은 단계를 거쳐야 합니다:</p>
<ol>
<li>프로세스의 페이지 테이블에서 해당 가상 주소에 대한 페이지 테이블 항목(Page Table Entry, PTE)을 읽어옵니다.</li>
<li>PTE를 사용하여 가상 주소를 물리 주소로 변환합니다.</li>
<li>변환된 물리 주소를 통해 실제 데이터를 메모리에서 읽어옵니다.</li>
</ol>
<p>이를 위해 하드웨어는 현재 실행 중인 프로세스의 페이지 테이블 위치를 알고 있어야 합니다.</p>
<p>가장 큰 문제는 모든 메모리 참조마다 페이지 테이블에서 주소 변환 정보를 읽어와야 한다는 점입니다. 이로 인해 메모리 접근이 최소 한 번 더 발생하게 되는데, 메모리 접근은 비용이 높은 연산이므로 프로그램의 실행 속도가 크게 저하될 수 있습니다.</p>
<h1 id="더-빠른-변환tlb">더 빠른 변환(TLB)</h1>
<p>주소 변환 과정에서 추가적인 메모리 접근이 발생한다. 프로세스가 메모리에 접근할 때마다 페이지 테이블을 읽어야 하므로, 매 메모리 접근마다 두 번의 메모리 연산(페이지 테이블 읽기 + 실제 데이터 읽기/쓰기)이 필요하게 됩니다. 이는 심각한 성능 저하를 초래할 수 있습니다.</p>
<p><strong>핵심 질문:</strong></p>
<ol>
<li>주소 변환 속도를 어떻게 향상시킬 수 있을까?</li>
<li>페이징에서 발생하는 추가 메모리 참조를 어떻게 피할 수 있을까?</li>
<li>이를 위해 어떤 하드웨어 지원이 필요할까?</li>
<li>운영체제는 어떤 식으로 관여해야 할까?</li>
</ol>
<p><strong>TLB : Translation Lookaside Buffer</strong></p>
<p> TLB의 목적은 최근에 사용된 가상 주소와 물리 주소 간의 매핑 정보를 저장하여, 주소 변환 속도를 향상시키는 것입니다.</p>
<p>프로세스가 가상 주소를 사용하여 메모리에 접근하면, 하드웨어는 먼저 TLB에서 해당 가상 주소의 변환 정보를 찾아봅니다. 만약 TLB에 정보가 있다면 (TLB Hit), 곧바로 물리 주소를 얻어 메모리에 접근할 수 있습니다. 이 경우, 페이지 테이블을 읽는 추가 작업 없이 주소 변환을 완료할 수 있으므로 매우 빠릅니다.</p>
<p>반면에, 원하는 정보가 TLB에 없는 경우 (TLB Miss)에는 기존대로 페이지 테이블을 참조하여 주소 변환을 진행해야 합니다. 이 과정에서 얻은 변환 정보는 향후 재사용을 위해 TLB에 저장됩니다.</p>
<blockquote>
<p>아직도 잘 이해가 안되는데 가상 주소 → 물리 주소로 변환하는 과정에서 PTE를 생성해서 변환하는 것과 TLB를 이용해서 변환하는 것의 차이를 잘 모르겠어… PTE를 만들어서 변환하든 TLB(작은 하드웨어 캐시)를 이용하든 똑같은 과정 아닌가?</p>
</blockquote>
<blockquote>
<ul>
<li>페이지 테이블은 크고 느린 메모리에 존재하기 때문에 메모리 접근 비용이 높은 연산이다(PTE - RAM, TLB - CPU 내부의 고속 메모리)</li>
</ul>
</blockquote>
<ul>
<li>페이지 테이블의 크기가 커질 수 있고, 모든 메모리 참조마다 페이지 테이블을 통한 주소 변환이 필요하다.</li>
<li>자주 사용되는 일부 PTE 정보만 저장하므로 <strong>크기가 작고 빠르다</strong>.</li>
</ul>
<h1 id="tlb의-기본-알고리즘">TLB의 기본 알고리즘</h1>
<pre><code class="language-jsx">int main() {
    int array[100];
    array[50] = 10;  // 배열의 특정 위치에 데이터 쓰기
    return 0;</code></pre>
<p>이 코드에서 array[50]에 값을 할당하려면 다음과 같은 과정이 필요합니다.</p>
<ol>
<li>가상 주소 0x1400 (50 * 4) 생성</li>
<li>TLB 검색: 0x1400에 대한 엔트리가 존재하는지 확인<ul>
<li>TLB 히트: 엔트리가 존재하면 엔트리에 포함된 물리 페이지 번호를 사용하여 물리 주소를 생성하고 메모리에 액세스</li>
<li>TLB 미스: 엔트리가 존재하지 않으면 페이지 테이블 워크를 통해 변환 정보를 검색</li>
</ul>
</li>
<li>페이지 테이블 워크: 페이지 테이블에서 가상 페이지 번호 0x3C에 대한 엔트리를 찾아 물리 페이지 번호를 얻음</li>
<li>TLB에 엔트리 추가: 0x1400 (VPN)과 0x0A00 (PPN)으로 구성된 엔트리를 TLB에 추가</li>
<li>물리 주소 생성: 물리 페이지 번호 0x0A00과 오프셋 0x00을 사용하여 물리 주소 0xA000 생성</li>
<li>메모리 액세스: 0xA000 주소에 값 10 저장</li>
</ol>
<h1 id="페이지-폴트">페이지 폴트</h1>
<p>프로세스가 메모리에 존재하지 않는 페이지에 접근하려는 경우 발생하는 예외 상황</p>
<p><strong>처리 과정</strong></p>
<ol>
<li>운영체제는 해당 페이지를 스왑 공간에서 메모리로 로드</li>
<li>페이지 테이블을 업데이트하여 Present bit를 1로 설정</li>
<li>프로세스가 다시 해당 페이지에 접근하도록 허용</li>
</ol>
<p><strong>영향</strong>: 페이지 폴트는 메모리 접근 지연을 야기하여 성능 저하를 초래한다.</p>
<h2 id="present-bit"><strong>Present Bit</strong></h2>
<p><strong>개념</strong>: 페이지 테이블에 존재하는 각 엔트리에 설정되는 플래그 비트</p>
<p><strong>기능</strong>: 해당 페이지가 현재 물리 메모리에 존재하는지 여부를 나타냄</p>
<p><strong>활용</strong></p>
<ul>
<li>Present bit가 1이면 페이지가 메모리에 존재하므로 바로 접근 가능</li>
<li>Present bit가 0이면 페이지가 스왑 공간에 존재하므로 페이지 폴트 발생</li>
</ul>
<h1 id="스왑-공간">스왑 공간</h1>
<p>물리 메모리가 부족하면 사용하지 않는 페이지를 하드 디스크의 특정 영역(스왑 공간)에 백업하고, 필요하면 다시 메모리로 불러오는 기술</p>
<p>가상 메모리 공간이 부족하면 사용하지 않는 페이지를 스왑 공간으로 백업하고, 필요하면 다시 메모리로 불러온다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/9b1e0ad8-b58c-4c16-99f2-a1d53656f680/image.png" alt=""></p>
<p>단점으로 하드 디스크는 메모리보다 훨씬 느려 페이지 폴트 발생 시 성능 저하</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 7주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-7%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-7%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 21 Oct 2025 09:10:03 GMT</pubDate>
            <description><![CDATA[<h2 id="참고">참고</h2>
<p><a href="https://os2024.jeju.ai/week07/vm-freespace.html">https://os2024.jeju.ai/week07/vm-freespace.html</a></p>
<h2 id="이번-회차-목표">이번 회차 목표</h2>
<p>페이지 단편화를 알고 메모리 공간을 효율적으로 사용하는 방법</p>
<ul>
<li>외부 단편화(External Fragmentation): 메모리에 할당되지 않은 작은 빈 공간들이 여러 곳에 산재해 있는 현상을 말합니다. 이러한 작은 빈 공간들은 새로운 메모리 할당 요청을 만족시키기에는 충분히 크지 않아서 메모리 낭비를 초래합니다.</li>
<li>내부 단편화(Internal Fragmentation): 할당된 메모리 블록 내부에서 발생하는 낭비를 말합니다. 할당된 블록의 크기가 요청된 크기보다 클 경우, 사용되지 않는 공간이 블록 내부에 존재하게 되는데 이를 내부 단편화라고 합니다.</li>
</ul>
<h2 id="버디-할당">버디 할당</h2>
<p>빈 공간의 합병을 간단히 하는 방법을 버디 할당이라고 합니다.</p>
<h3 id="이진-버디-할당기">이진 버디 할당기</h3>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/7b4d8f12-9bc1-4810-adb6-374102e3b67b/image.png" alt=""></p>
<p>빈 메모리는 처음에 개념적으로 2ⁿ인 하나의 큰 공간으로 생각하면, 메모리 요청이 발생해 그 요청을 충족시키기에 충분한 공간이 발견될 때까지 분할하고, 더 분할하면 공간이 너무 작아져서 요청을 만족시킬 수 없을 때까지 빈 공간을 2개로 계속 분할합니다.</p>
<p>예시로 7KB 크기의 요청이 들어오면 가장 왼쪽의 8KB 블록이 될 때까지 분할하고, 사용자에게 반환됩니다.</p>
<p>블록 해제 시에는 해제한 블록 옆의 ‘buddy’블록을 확인합니다. 만약 비어있다면 두 블록을 병합하고, 이 재귀 합병 과정은 트리를 따라 전체 빈 공간이 복원되거나 버디가 사용 중이라는 것이 밝혀질 때까지 계속 올라갑니다.</p>
<p>• 버디(Buddy): 버디 할당에서 인접한 두 개의 블록을 버디라고 합니다. 버디는 항상 동일한 크기를 가지며, 주소가 인접해 있습니다. 버디 블록은 병합과 분할 과정에서 함께 처리됩니다.</p>
<h2 id="슬랩-할당기">슬랩 할당기</h2>
<p>위의 문제는 특수 목적 할당기인 슬랩 할당기가 더 나은 방법으로 해결했습니다. 커널이 부팅될 때 커널 객체를 위한 여러 객체 캐시(object cache)를 할당합니다.</p>
<p>여기서 커널 객체란 락, 파일 시스템 아이노드 등 자주 요청되는 자료 구조들을 일컫습니다. 객체 캐시는 지정된 크기의 객체들로 구성된 빈 공간 리스트이고, 메모리 할당 및 해제 요청을 빠르게 하기 위해 사용됩니다.</p>
<p>기존에 할당된 캐시 공간이 부족하면 상위 메모리 할당기에게 추가 슬랩을 요청합니다. 슬랩 내 객체들에 대한 참조 횟수가 0이 되면 상위 메모리 할당기는 이 슬랩을 회수할 수 있습니다.</p>
<p>슬랩 할당 방식은 빈 객체들을 사전에 초기화된 상태로 유지한다는 점에서 개별 리스트 방식보다 우수합니다. 반납된 객체들을 초기화된 상태로 리스트에 유지하여 슬랩 할당기는 객체당 잦은 초기화와 반납의 작업을 피할 수 있어서 오버헤드를 현저히 감소시킵니다.</p>
<ul>
<li>슬랩(Slab): 커널 객체를 저장하기 위해 할당된 연속적인 메모리 블록입니다. 슬랩은 동일한 크기의 객체들로 구성되며, 각 객체는 미리 초기화된 상태로 유지됩니다.</li>
<li>객체 캐시(Object Cache): 동일한 크기와 타입의 객체들을 관리하는 캐시입니다. 각 객체 캐시는 해당 객체의 할당과 해제 요청을 처리합니다. 객체 캐시는 여러 개의 슬랩으로 구성될 수 있습니다.</li>
</ul>
<p>이거 뭔 말임?</p>
<h2 id="개별-리스트segregated-list">개별 리스트(Segregated List)</h2>
<p>특정 응용 프로그램이 자주 요청하는 크기의 객체를 관리하기 위해 별도의 리스트를 유지하는 것을 개별 리스트라고 합니다.</p>
<p>이 방법의 장점은 특정 크기의 요청을 위한 메모리 청크를 유지하기 때문에 단편화 가능성을 상당히 줄일 수 있다는 것입니다. 요청된 크기의 청크만이 존재하기 때문에 복잡한 리스트 검색이 필요하지 않으므로 할당과 해제 요청을 신속히 처리할 수 있습니다.</p>
<p>잘 모르겠다 ㅎㅎ…</p>
<h2 id="best-fit최적-적합">Best Fit(최적 적합)</h2>
<p><strong>가장 작은 남는 공간에 할당합니다.</strong></p>
<p>이거 빈 공간 리스트를 만들어야 한다. 그리고 리스트를 조회해서 요청한 크기와 같거나 더 큰 빈 메모리 청크를 찾는다. 그 후, 후보자 그룹 중에서 가장 작은 크기의 청크를 반환합니다. Best fit은 빈 공간 리스트를 한 번만 순회하면 반환할 블록을 찾을 수 있지만 요청이 들어올 때마다 리스트를 순회해야 되는 비용이 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/65bdda5e-992a-46fe-8925-202bd04f7a26/image.png" alt=""></p>
<h2 id="worst-fit최악-적합">Worst Fit(최악 적합)</h2>
<p>가장 큰 남는 공간에 할당합니다.</p>
<p>최적 적합의 반대 방식입니다. 가장 큰 빈 청크를 찾아 요청된 크기만큼만 반환하고 남는 부분은 빈 공간 리스트에 계속 유지됩니다. 최적 적합 방식에서 발생할 수 있는 수많은 작은 청크 대신에 커다란 빈 청크를 남기려고 시도합니다. 그러나 다시 한번 항상 빈 공간 전체를 탐색해야 하기 때문에 역시 높은 비용을 지불해야 합니다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/5122465f-c6f0-4280-8d11-4bad8748166d/image.png" alt=""></p>
<h2 id="first-fit최초-적합">First Fit(최초 적합)</h2>
<p><strong>첫 번째로 충분히 큰 공간에 할당합니다.</strong></p>
<p>간단하게 요청보다 큰 첫 번째 블록을 찾아서 요청만큼 반환합니다. 남은 빈 공간은 후속 요청을 위해 계속 유지됩니다. 속도가 빠르다는 것이 장점입니다. 원하는 블록을 찾기 위해 항상 빈 공간 리스트 전체를 탐색할 필요가 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/c7b23d55-3b5b-4945-a212-ea7c464e8c64/image.png" alt=""></p>
<h2 id="next-fit다음-적합">Next Fit(다음 적합)</h2>
<p><strong>마지막으로 할당된 공간의 다음부터 충분히 큰 공간을 찾아 할당합니다.</strong></p>
<p>항상 리스트의 처음부터 탐색하는 대신 다음 적합 알고리즘은 마지막으로 찾았던 원소를 가리키는 추가의 포인터를 유지합니다. 아이디어는 빈 공간 탐색을 리스트 전체에 더 균등하게 분산시키는 것입니다. 리스트의 첫 부분에만 단편화가 집중적으로 발생하는 것을 방지합니다. 전체 탐색을 하지 않기 때문에 최초 적합의 성능과 비슷합니다.</p>
<p>이거 구현해봤는데 현재 할당 할당된 공간을 가리키는 포인터를 두고, 다음 할당할 공간을 찾을 때는 현재 위치부터 탐색을 시작했던 것 같다. 만약 탐색이 되지 않는다면 first fit처럼 처음부터 탐색을 했던 것 같은데 내 정확하지는 않다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/e1ad9eff-4a23-4950-ad2e-b164aafac9d2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 6주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 14 Oct 2025 11:28:49 GMT</pubDate>
            <description><![CDATA[<h1 id="베이스-레지스터와-바운드-레지스터">베이스 레지스터와 바운드 레지스터</h1>
<blockquote>
<p>“프로세스마다 메모리 공간이 분리되어 있는 이유가 뭘까?”</p>
<p>바로 CPU가 <strong>베이스 레지스터(Base Register)</strong> 와 <strong>바운드 레지스터(Bound Register)</strong> 라는 하드웨어를 통해
각 프로세스의 메모리 접근을 감시하고, 보호하기 때문이다.</p>
</blockquote>
<hr>
<h2 id="1️⃣-배경--모든-프로그램이-같은-메모리를-쓸-수-있었던-시절">1️⃣ 배경 — 모든 프로그램이 같은 메모리를 쓸 수 있었던 시절</h2>
<p>초기의 컴퓨터에서는
모든 프로그램이 물리 메모리를 그대로 사용했다.
즉, 운영체제와 사용자 프로그램이 <strong>한 주소 공간을 공유</strong>했기 때문에,
하나의 버그가 시스템 전체를 날려버릴 수 있었다.</p>
<pre><code class="language-text">[물리 메모리]
|---------------------------|
| OS | 프로그램 A | 프로그램 B |
|---------------------------|</code></pre>
<ul>
<li>프로그램 A가 실수로 B의 주소를 참조하면 B의 데이터가 덮여버림</li>
<li>운영체제 코드 영역을 침범하면 시스템 전체가 다운됨</li>
</ul>
<p>이 문제를 해결하기 위해 등장한 것이
<strong>“주소 변환(Address Translation)”</strong>, 그리고
그 핵심 하드웨어가 바로 <strong>Base / Bound 레지스터</strong>다.</p>
<hr>
<h2 id="2️⃣-개념--가상-주소를-물리-주소로-바꾸는-기준점">2️⃣ 개념 — “가상 주소를 물리 주소로 바꾸는 기준점”</h2>
<p>프로세스가 메모리를 접근할 때,
CPU는 실제 물리 주소로 바로 접근하지 않는다.</p>
<p>그 대신 프로세스 입장에서는
항상 <strong>0번지부터 시작하는 독립된 공간</strong>을 본다.</p>
<p>이 “가상 주소”를 실제 “물리 주소”로 바꿔주는 역할을
CPU의 <strong>베이스 레지스터</strong>와 <strong>바운드 레지스터</strong>가 맡는다.</p>
<hr>
<h2 id="3️⃣-베이스-레지스터-base-register">3️⃣ 베이스 레지스터 (Base Register)</h2>
<blockquote>
<p>“이 프로세스의 메모리는 물리 메모리 어디서부터 시작하나요?”</p>
</blockquote>
<ul>
<li><strong>정의:</strong> 프로세스의 주소 공간이 실제 메모리상에서 시작되는 물리 주소</li>
<li><strong>기능:</strong>
가상 주소(<code>virtual_addr</code>) + 베이스 값 = 물리 주소(<code>physical_addr</code>)</li>
</ul>
<p>즉, 프로세스가 “0번지에 쓰기”를 시도해도
CPU는 <code>Base + 0</code> 주소에 접근하도록 자동 변환한다.</p>
<h3 id="예시">예시</h3>
<pre><code class="language-text">프로세스 A: Base = 1000
프로세스 B: Base = 5000</code></pre>
<p>프로세스 A가 <code>0x0100</code>에 접근 → 실제는 <code>1000 + 0x0100 = 1100</code>
프로세스 B가 <code>0x0100</code>에 접근 → 실제는 <code>5000 + 0x0100 = 5100</code></p>
<p>두 프로세스 모두 “0x0100”을 썼지만,
물리 메모리상에서는 전혀 다른 영역에 접근한다.</p>
<hr>
<h2 id="4️⃣-바운드-레지스터-bound-register">4️⃣ 바운드 레지스터 (Bound Register)</h2>
<blockquote>
<p>“이 프로세스가 접근할 수 있는 최대 범위는 어디까지인가?”</p>
</blockquote>
<ul>
<li><strong>정의:</strong> 프로세스의 주소 공간 크기(또는 끝 주소)를 저장</li>
<li><strong>기능:</strong>
접근하려는 가상 주소가 이 범위 안에 있는지 검사</li>
<li><strong>역할:</strong>
잘못된 메모리 접근을 <strong>하드웨어 수준에서 차단</strong></li>
</ul>
<h3 id="예시-1">예시</h3>
<pre><code class="language-text">Base = 1000
Bound = 4000 (즉, 0x0 ~ 0x0FFF까지만 유효)</code></pre>
<ul>
<li>접근: <code>virtual_addr = 0x0500</code> → OK ✅</li>
<li>접근: <code>virtual_addr = 0x2000</code> → ❌ 바운드 초과 → 예외(Exception) 발생 → 세그폴트(SIGSEGV)</li>
</ul>
<p>이 과정은 <strong>운영체제가 아닌 CPU(MMU)</strong> 가 즉시 처리한다.
즉, 성능에 전혀 영향을 주지 않고 하드웨어가 자동으로 보호 기능을 수행한다.</p>
<hr>
<h2 id="5️⃣-전체-동작-흐름">5️⃣ 전체 동작 흐름</h2>
<pre><code class="language-text">[CPU 실행 흐름]

가상 주소 요청
   ↓
가상주소 + Base → 물리주소 계산
   ↓
Bound 범위 검사
   ↓
┌───────────────┬───────────────┐
│ 유효 주소     │ 범위 초과     │
│ → 접근 허용   │ → 예외 발생   │
└───────────────┴───────────────┘</code></pre>
<p>즉, <strong>하드웨어가 “주소 변환 + 보호”를 한 번에 처리</strong>하는 구조야.
운영체제는 이 값(Base, Bound)을 세팅해주기만 하면 된다.</p>
<hr>
<h2 id="6️⃣-예시-코드-시뮬레이션">6️⃣ 예시 코드 (시뮬레이션)</h2>
<p>아래는 이 원리를 단순히 시뮬레이션한 C 코드야.</p>
<pre><code class="language-c">#define BASE 1000
#define BOUND 4000

int translate_address(int virtual_addr) {
    if (virtual_addr &lt; 0 || virtual_addr &gt;= BOUND) {
        printf(&quot;Segmentation Fault! 접근 불가 주소: %d\n&quot;, virtual_addr);
        return -1;
    }
    return BASE + virtual_addr;
}

int main() {
    int vaddr = 1200;
    int paddr = translate_address(vaddr);
    if (paddr != -1)
        printf(&quot;가상주소 %d → 물리주소 %d\n&quot;, vaddr, paddr);
}</code></pre>
<p>출력:</p>
<pre><code>가상주소 1200 → 물리주소 2200</code></pre><p>만약 <code>vaddr = 5000;</code> 이면:</p>
<pre><code>Segmentation Fault! 접근 불가 주소: 5000</code></pre><hr>
<h2 id="7️⃣-운영체제가-이걸-어떻게-관리하나">7️⃣ 운영체제가 이걸 어떻게 관리하나</h2>
<p>운영체제는 프로세스마다 “메모리 맵핑 정보”를 저장해야 한다.
이를 <strong>PCB (Process Control Block)</strong> 에 기록한다.</p>
<table>
<thead>
<tr>
<th>프로세스</th>
<th>Base</th>
<th>Bound</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>1000</td>
<td>4000</td>
</tr>
<tr>
<td>B</td>
<td>5000</td>
<td>2000</td>
</tr>
</tbody></table>
<p>프로세스 전환(Context Switch) 시,
운영체제는 <strong>CPU의 Base/Bound 레지스터를 PCB에 맞게 교체</strong>한다.</p>
<p>이로써 동시에 여러 프로세스가 돌아도
서로의 메모리를 침범할 수 없다.</p>
<hr>
<h2 id="8️⃣-현대-시스템에서의-진화">8️⃣ 현대 시스템에서의 진화</h2>
<table>
<thead>
<tr>
<th>과거</th>
<th>현대</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Base/Bound</td>
<td>Page Table</td>
<td>더 유연한 메모리 매핑 (비연속적 영역 가능)</td>
</tr>
<tr>
<td>단일 주소 범위</td>
<td>다수의 페이지</td>
<td>세그멘테이션 + 페이징 결합</td>
</tr>
<tr>
<td>단순 보호</td>
<td>권한 비트(R/W/X)</td>
<td>세밀한 접근 제어</td>
</tr>
<tr>
<td>CPU 검사</td>
<td>MMU + TLB</td>
<td>빠른 주소 변환 캐시</td>
</tr>
</tbody></table>
<p>즉, Base/Bound는 <strong>현대 가상 메모리의 조상</strong>이자,
“프로세스 격리(isolation)”의 가장 단순한 형태다.</p>
<hr>
<h2 id="9️⃣-백엔드-개발자-관점에서의-비유">9️⃣ 백엔드 개발자 관점에서의 비유</h2>
<table>
<thead>
<tr>
<th>OS 메커니즘</th>
<th>백엔드 대응 개념</th>
</tr>
</thead>
<tbody><tr>
<td>Base Register</td>
<td>컨테이너 루트 디렉토리 (<code>chroot</code>)</td>
</tr>
<tr>
<td>Bound Register</td>
<td>cgroup 메모리 제한</td>
</tr>
<tr>
<td>예외 발생</td>
<td>API 403/401 Forbidden</td>
</tr>
<tr>
<td>PCB + Context Switch</td>
<td>스레드 풀의 컨텍스트 교체</td>
</tr>
</tbody></table>
<p>즉, <strong>운영체제의 메모리 격리 원리 = 서버의 자원 격리 원리</strong>야.
Base/Bound는 우리가 매일 쓰는 Docker, VM, 컨테이너 격리의 기초 설계 철학이야.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p><strong>Base Register</strong>는 “어디서부터 시작할지”,
<strong>Bound Register</strong>는 “어디까지 허용할지”를 정의한다.
이 두 값 덕분에 운영체제는</p>
<ul>
<li>각 프로세스의 메모리를 독립적으로 보호하고,</li>
<li>버그나 공격으로부터 시스템 전체를 방어할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 5주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 30 Sep 2025 08:28:17 GMT</pubDate>
            <description><![CDATA[<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://os2024.jeju.ai/week05/vm-intro.html">https://os2024.jeju.ai/week05/vm-intro.html</a></p>
<p><a href="https://os2024.jeju.ai/week05/vm-api.html">https://os2024.jeju.ai/week05/vm-api.html</a></p>
<p><a href="https://os2024.jeju.ai/week05/lab.html">https://os2024.jeju.ai/week05/lab.html</a></p>
<hr>
<h1 id="주소-공간의-개념">주소 공간의 개념</h1>
<h2 id="📌-핵심-내용-요약">📌 핵심 내용 요약</h2>
<ol>
<li><strong>가상 메모리의 필요성</strong><ul>
<li>여러 프로그램이 동시에 실행될 때, 각 프로그램이 물리 주소를 직접 쓰면 충돌 위험이 있음.</li>
<li>→ 따라서 <strong>각 프로세스마다 독립적인 주소 공간</strong>을 제공해야 함.</li>
</ul>
</li>
<li><strong>주소 변환 (Address Translation)</strong><ul>
<li>프로그램은 <strong>가상 주소(virtual address)</strong>를 사용.</li>
<li>MMU가 이를 <strong>물리 주소(physical address)</strong>로 변환.</li>
<li>덕분에 같은 가상 주소라도, 프로세스마다 다른 물리 주소에 매핑될 수 있음.</li>
</ul>
</li>
<li><strong>보호 (Protection)</strong><ul>
<li>프로세스가 다른 프로세스의 메모리에 접근하지 못하게 차단.</li>
<li>커널 영역과 사용자 영역도 분리해서 안전 보장.</li>
</ul>
</li>
<li><strong>편리함 (Convenience)</strong><ul>
<li>프로그래머는 “0번지부터 쭉 이어진 메모리”처럼 단순하게 생각하면 됨.</li>
<li>실제 물리 메모리가 흩어져 있어도 운영체제가 알아서 매핑.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="💡-쉽게-비유">💡 쉽게 비유</h2>
<ul>
<li><strong>가상 주소</strong> = 호텔 방 열쇠 번호</li>
<li><strong>물리 주소</strong> = 실제 호텔 방 위치</li>
<li>투숙객(프로세스)은 방 열쇠(가상 주소)만 보면 되고, 그게 실제 어디 방(물리 주소)에 연결되는지는 호텔 직원(운영체제 + MMU)이 알아서 처리하는 구조.</li>
</ul>
<hr>
<h1 id="메모리-관리-api">메모리 관리 API</h1>
<h2 id="핵심-내용-요약">핵심 내용 요약</h2>
<ol>
<li><strong>메모리 공간의 종류</strong><ul>
<li><strong>스택 (stack)</strong>: 자동으로 할당/해제됨 (지역 변수).</li>
<li><strong>힙 (heap)</strong>: 프로그래머가 직접 <code>malloc</code>, <code>free</code> 같은 API로 관리해야 함.</li>
</ul>
</li>
<li><strong>메모리 할당 API</strong><ul>
<li><code>malloc(size)</code>: 원하는 크기만큼 메모리를 요청.</li>
<li><code>free(ptr)</code>: 사용이 끝난 메모리를 반환.</li>
<li><code>calloc(n, size)</code>: 초기화된 배열 메모리 요청.</li>
<li><code>realloc(ptr, new_size)</code>: 기존 메모리 크기 조정.</li>
</ul>
</li>
<li><strong>실수하기 쉬운 점</strong><ul>
<li><code>free</code>를 깜빡하거나, 같은 포인터를 두 번 <code>free</code>하면 오류 발생.</li>
<li><code>malloc</code>한 메모리를 쓰지 않고 잊어버리면 <strong>메모리 누수(memory leak)</strong>.</li>
</ul>
</li>
<li><strong>현대적 의미</strong><ul>
<li>실제 물리 메모리가 아니라, <strong>가상 메모리 주소 공간</strong>에서의 요청/해제.</li>
<li>운영체제가 가상 주소를 실제 물리 메모리에 매핑해줌.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="가상-메모리의-필요성">가상 메모리의 필요성</h3>
<p>초기에 시분할 시스템을 이용해서 여러 사용자가 동시에 컴퓨터를 사용할 수 있도록 했지만 문제가 발생했다. 시분할을 구현하는 방법 중 하나인 프로세스를 아주 짧은 시간 동안만 실행시키되, 그 순간에는 메모리 전체에 대한 접근 권한을 부여하는 것이었습니다. 그 후 프로세스를 중단하고 그때의 모든 상태를 디스크 같은 곳에 저장한 뒤, 다른 프로세스의 정보를 불러와 또 잠깐 실행하는 식이었죠.</p>
<p>이 방식의 문제점은 레지스터 값을 저장하고 복원하는 건 빨랐지만 메모리 전체를 디스크에 옮기는 건 너무 느렸다고 한다. 이 문제를 해결하기 위해 메모리 가상화가 탄생했다.</p>
<h3 id="가상-주소와-물리-주소는-어떤-관계를-가질까">가상 주소와 물리 주소는 어떤 관계를 가질까?</h3>
<p>가상 주소는 프로세스마다 가지는 독립된 논리 주소 공간을 말하며, 물리 주소는 실제 메모리 하드웨어의 주소를 말한다. 가상 주소는 물리 주소로 변환되어 메모리에 실제로 저장된다.</p>
<h3 id="프로세스마다-주소-공간을-독립적으로-두는-이유는-무엇인가">프로세스마다 주소 공간을 독립적으로 두는 이유는 무엇인가?</h3>
<p>이전의 저장된 데이터를 다른 프로세스가 덮어쓰지 말라고.</p>
<h3 id="컨텍스트-스위칭-시-가상-메모리가-어떤-역할을-하는가">컨텍스트 스위칭 시 가상 메모리가 어떤 역할을 하는가?</h3>
<p>기존의 데이터는 저장된 채로 프로세스만 전환할 수 있다.</p>
<h3 id="가상-메모리를-사용하지-않는다면-어떤-문제가-생길-수-있는가">가상 메모리를 사용하지 않는다면 어떤 문제가 생길 수 있는가?</h3>
<h3 id="우리가-malloc을-호출할-때-실제로는-운영체제의-어떤-동작이-일어날까">우리가 <code>malloc</code>을 호출할 때 실제로는 운영체제의 어떤 동작이 일어날까?</h3>
<p>malloc을 쓴다면 가상 메모리 주소를 생성하고 MMU가 실제 메모리 주소랑 매핑해준다.</p>
<h3 id="운영체제가-malloc-요청을-받으면-곧바로-물리-메모리-페이지를-다-붙여줄까-아니면-필요할-때까지-미뤄두는-방식지연-할당도-가능할까">운영체제가 <code>malloc</code> 요청을 받으면 곧바로 물리 메모리 페이지를 다 붙여줄까, 아니면 <strong>필요할 때까지 미뤄두는 방식(지연 할당)</strong>도 가능할까?</h3>
<p>지연 할당 방식도 가능하다. 요청할 때 물리 메모리 일부를 다른 프로세스가 못 쓰게 막는다면 메모리 낭비로 이어질 것.</p>
<blockquote>
<p><code>malloc(1GB)</code> 같은 큰 요청을 해도 그 순간 1GB 물리 메모리를 다 주는 게 아니라, <strong>가상 주소 공간만 예약해두고</strong> 실제 물리 메모리는 접근할 때(페이지 폴트가 날 때) 채워줘.</p>
</blockquote>
<h3 id="만약-프로그래머가-malloc만-하고-free를-안-한다면-지연-할당-방식에서는-어떤-문제가-생길까">만약 프로그래머가 <code>malloc</code>만 하고 <code>free</code>를 안 한다면, <strong>지연 할당 방식</strong>에서는 어떤 문제가 생길까?</h3>
<p>일단 malloc을 했을 경우 가상 메모리 공간을 일부 예약해 놓기 때문에 실제 메모리 공간이 사용이 안되고 있더라고 메모리 부족 현상이 발생할 수 있다.</p>
<blockquote>
<p><code>malloc</code>만 하고 <code>free</code>하지 않으면,</p>
<p>그 <strong>가상 주소 공간이 계속 잡혀 있는 상태</strong>가 돼.</p>
<ul>
<li><p>처음엔 지연 할당 덕분에 실제 물리 메모리 사용은 적을 수 있지만,</p>
<p>  프로그램이 그 영역을 실제로 접근하기 시작하면 <strong>점점 물리 메모리까지 소비</strong>하게 돼.</p>
</li>
<li><p>그럼 결국 운영체제 입장에서는 &quot;다른 프로그램에게 줄 수 있는 여유 메모리&quot;가 줄어들어서, 시스템이 느려지거나 심하면 OOM(Out Of Memory) 에러까지 날 수 있어.</p>
</li>
</ul>
</blockquote>
<h3 id="내가-네-이해도를-확인하고-싶은데-혹시-malloc으로-할당한-후-free하지-않고-프로그램이-끝까지-실행을-마치고-종료한다면-그-메모리는-어떻게-될까-운영체제가-계속-잡고-있을까-아니면-자동으로-회수할까">내가 네 이해도를 확인하고 싶은데, 혹시 <code>malloc</code>으로 할당한 후 <code>free</code>하지 않고 프로그램이 <strong>끝까지 실행을 마치고 종료</strong>한다면, 그 메모리는 어떻게 될까? 운영체제가 계속 잡고 있을까, 아니면 자동으로 회수할까?</h3>
<p>C는 garbage 컬렉션이 없기 때문에 회수하지 않고, 여전히 메모리를 할당한 채로 남아있을 것 같아…</p>
<blockquote>
<p><strong>프로세스 실행 중</strong>에는 <code>free</code>하지 않으면 계속 잡혀 있어서 메모리 부족 문제가 생김.</p>
<ul>
<li><strong>프로세스가 종료될 때</strong>는 운영체제가 &quot;이제 이 프로세스 필요 없네&quot; 하고 통째로 정리해버림.</li>
</ul>
</blockquote>
<h3 id="이런-동작을-생각했을-때-왜-굳이-우리가-free를-해줘야-할까-어차피-끝나면-os가-회수해주는데라는-생각도-들-수-있잖아">이런 동작을 생각했을 때, 왜 굳이 우리가 <code>free</code>를 해줘야 할까? “어차피 끝나면 OS가 회수해주는데…”라는 생각도 들 수 있잖아?</h3>
<p>여유 메모리가 줄어서 시스템이 느려질 수 있기 때문에</p>
<blockquote>
<ul>
<li><strong>메모리 부족 → 디스크 스와핑 증가 → 시스템 전체가 느려짐</strong></li>
<li>장시간 실행되는 서버 프로그램 같은 경우, 누수가 쌓이면 결국 다운될 수도 있음</li>
</ul>
</blockquote>
<h2 id="정리">정리</h2>
<ul>
<li><code>malloc</code> → 가상 주소 공간에 예약</li>
<li>실제 물리 메모리 할당은 지연(lazy)</li>
<li><code>free</code> 안 하면 실행 중에 메모리 낭비 발생</li>
<li>프로세스 종료 시 운영체제가 통째로 회수</li>
</ul>
<h3 id="지금까지-malloc을-써온-상황을-떠올려-보면-calloc이나-realloc-중-어떤-게-있으면-더-편했을-것-같다는-생각이-들어">지금까지 <code>malloc</code>을 써온 상황을 떠올려 보면, <code>calloc</code>이나 <code>realloc</code> 중 <strong>어떤 게 있으면 더 편했을 것 같다</strong>는 생각이 들어?</h3>
<p>realloc이야. 왜냐하면 malloc으로 메모리 공간을 할당했어도 추가적으로 공간이 필요할 경우가 생기기 때문이야. python 같은 경우 array를 생성해도 추가적으로 데이터를 append할 수 있으니까 realloc을 쓸 것 같아</p>
<blockquote>
<ul>
<li>내부적으로는 새 메모리를 할당하고, 기존 데이터를 복사하고, 옛 공간을 해제하는 식으로 동작할 수도 있어.</li>
<li>그래서 <code>realloc</code> 쓰면 Python 리스트의 자동 확장과 비슷한 효과를 흉내낼 수 있는 거지.</li>
</ul>
</blockquote>
<h3 id="만약-네가-malloc으로-int-5개짜리-배열을-만들었는데-나중에-10개로-늘리고-싶어졌어-이때-realloc을-안-쓰고-직접-하려면-어떤-과정을-밟아야-할까">만약 네가 <code>malloc</code>으로 <code>int</code> 5개짜리 배열을 만들었는데, 나중에 10개로 늘리고 싶어졌어. 이때 <code>realloc</code>을 안 쓰고 직접 하려면 어떤 과정을 밟아야 할까?</h3>
<p>기존에 만들었던 5개짜리 배열을 삭제하고 다시 10개로 만들어야 하지 않아?</p>
<blockquote>
<ul>
<li><code>malloc(5 * sizeof(int))</code> 으로 5칸짜리 배열을 만든다.</li>
<li>새로운 배열을 위해 <code>malloc(10 * sizeof(int))</code> 으로 10칸짜리를 만든다.</li>
<li><code>memcpy</code> 같은 함수로 기존 5칸짜리의 데이터를 새 배열로 복사한다.</li>
<li>기존 5칸짜리 배열을 <code>free</code>한다.</li>
</ul>
</blockquote>
<h1 id="정리-가상-메모리-api-학습">정리: 가상 메모리 API 학습</h1>
<h3 id="📌-기본-개념">📌 기본 개념</h3>
<ul>
<li><strong>malloc</strong>: 가상 주소 공간을 예약하는 함수. 실제 물리 메모리는 접근할 때 할당된다(지연 할당, lazy allocation).</li>
<li><strong>free</strong>: 예약한 메모리를 운영체제에 반환하는 함수.</li>
<li><strong>calloc</strong>: <code>malloc</code>과 유사하지만, 할당된 메모리를 0으로 초기화해준다.</li>
<li><strong>realloc</strong>: 기존 메모리 블록의 크기를 조정하는 함수. 내부적으로 새 블록을 만들고 복사 후 기존 블록을 해제할 수도 있음.</li>
</ul>
<hr>
<h3 id="⚙️-동작-원리">⚙️ 동작 원리</h3>
<ul>
<li><code>malloc</code> → <strong>가상 주소 공간 예약</strong></li>
<li>실제 물리 메모리는 <strong>페이지 폴트 발생 시 할당</strong></li>
<li><code>free</code>하지 않으면 실행 중 메모리 누수 발생 → 시스템 전체 성능 저하</li>
<li>프로그램이 종료되면 운영체제가 해당 프로세스의 모든 메모리를 자동으로 회수</li>
</ul>
<hr>
<h3 id="🚨-주의할-점">🚨 주의할 점</h3>
<ul>
<li>C는 garbage collection이 없다 → 반드시 직접 <code>free</code> 해줘야 함</li>
<li><code>realloc</code> 사용 시 기존 포인터 대신 <strong>반환된 새 포인터</strong>를 사용해야 함 (이전 포인터는 무효화될 수 있음)</li>
</ul>
<hr>
<h3 id="💡-비유">💡 비유</h3>
<ul>
<li><code>malloc</code>: 창고에 빈 칸 예약</li>
<li><code>free</code>: 예약 취소하고 다른 사람도 쓰게 해줌</li>
<li><code>calloc</code>: 창고 빈 칸을 예약하고 <strong>모두 청소까지 완료</strong></li>
<li><code>realloc</code>: 더 큰 창고로 이사 가면서 기존 물건을 옮겨주는 과정</li>
<li>Python 리스트의 <code>append</code> = 내부적으로 <code>realloc</code>과 유사 (여유분까지 크게 늘려서 효율적으로 관리)</li>
</ul>
<hr>
<h1 id="가상-메모리-실습">가상 메모리 실습</h1>
<h2 id="📌-핵심-내용-요약-1">📌 핵심 내용 요약</h2>
<ol>
<li><strong>malloc/free 실습</strong><ul>
<li>작은 크기, 큰 크기 메모리를 할당해 보고 시스템이 어떻게 반응하는지 확인.</li>
<li><code>malloc</code>은 가상 주소 공간만 예약하므로, 실제 물리 메모리는 접근해야 할당됨을 확인.</li>
</ul>
</li>
<li><strong>메모리 접근 실험</strong><ul>
<li>할당만 하고 접근하지 않을 때와, 실제로 데이터를 채울 때 차이를 비교.</li>
<li>페이지 폴트가 발생하는 시점을 관찰.</li>
</ul>
</li>
<li><strong>메모리 해제 실험</strong><ul>
<li><code>free</code>를 했을 때 OS의 메모리 사용량이 바로 줄어드는지 확인.</li>
<li>운영체제가 내부적으로 free된 메모리를 어떻게 처리하는지 살펴봄.</li>
</ul>
</li>
<li><strong>도구 활용</strong><ul>
<li><code>top</code>, <code>ps</code>, <code>/proc/[pid]/maps</code> 등을 이용해 실제 프로세스 메모리 사용량 관찰.</li>
<li>&quot;눈에 보이는 메모리 사용량&quot;과 &quot;실제로 할당된 가상 공간&quot;의 차이를 느끼는 게 목적.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="🧩-학습-포인트">🧩 학습 포인트</h2>
<ul>
<li><code>malloc</code>과 실제 물리 메모리 사용이 다르다는 걸 실험으로 체득.</li>
<li><code>free</code>를 안 하면 어떻게 되는지 직접 관찰.</li>
<li>운영체제가 <strong>lazy allocation</strong>을 쓰는 이유를 이해.</li>
</ul>
<hr>
<h2 id="코드">코드</h2>
<p>va.c</p>
<pre><code class="language-jsx">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

int main(int argc, char *argv[])
{
    printf(&quot;location of code : %p\n&quot;, main);
    printf(&quot;location of heap : %p\n&quot;, malloc(100e6));
    int x = 3;
    printf(&quot;location of stack: %p\n&quot;, &amp;x);
    return 0;
}</code></pre>
<p>makefile</p>
<pre><code class="language-jsx">.DEFAULT_GOAL := help

.PHONY: help
help: ## Display this help
    @awk &#39;BEGIN {FS = &quot;:.*##&quot;; printf &quot;\nUsage:\n make \033[36m&lt;target&gt;\033[0m\n&quot;} /^[a-zA-Z_-]+:.*?##/ { printf &quot; \033[36m%-25s\033[0m %s\n&quot;, $$1, $$2 } /^##@/ { printf &quot;\n\033[1m%s\033[0m\n&quot;, substr($$0, 5) } &#39; $(MAKEFILE_LIST)

##@ Build

all: va ## Build all targets

va: ## Build target va
    gcc -o va va.c -Wall

##@ Run

run: ## Run the lottery program
    ./va

##@ Clean-up

clean: ## Remove all build artifacts
    rm -f va</code></pre>
<h2 id="결과">결과</h2>
<pre><code class="language-jsx">location of code : 0x5d338ef01189
location of heap : 0x7d275b4a1010
location of stack: 0x7fff9eded3d4</code></pre>
<h3 id="스택-주소가-힙보다-더-높게-있는-이유는-뭐라고-생각해"><strong>스택 주소가 힙보다 더 높게 있는 이유</strong>는 뭐라고 생각해?</h3>
<p>스택은 데이터 공간이 많이 필요할 것이다. 프로그램이 실행될 때 데이터를 스택에 저장해 놓을테니까. 그리고 힙은 실행되면서 동적으로 데이터의 크기가 커질 경우에 사용함으로 스택보다 작아도 된다.</p>
<blockquote>
<h3 id="📌-왜-스택이-더-높은-주소에-있을까">📌 왜 스택이 더 높은 주소에 있을까?</h3>
<ol>
<li><strong>스택은 위에서 아래로 성장</strong><ul>
<li>함수 호출할 때마다 새로운 stack frame이 위 주소에서 아래로 내려옴.</li>
<li>그래서 운영체제는 스택을 <strong>가장 높은 주소에 배치</strong>해 두고, 필요할 때 아래로 확장시키는 구조를 씀.</li>
</ul>
</li>
<li><strong>힙은 아래에서 위로 성장</strong><ul>
<li><code>malloc</code> 같은 호출을 할 때마다 점점 더 높은 주소 쪽으로 커짐.</li>
<li>따라서 낮은 주소대(코드/데이터 영역)와 높은 주소대(스택) 사이의 “중간 지대”를 차지.</li>
</ul>
</li>
<li><strong>서로 충돌 방지</strong><ul>
<li>스택은 위→아래, 힙은 아래→위로 커지니까, 둘 사이에 <strong>넓은 빈 공간을 두고 시작</strong>해.</li>
<li>만약 충돌할 정도로 커지면 → <code>stack overflow</code>나 <code>out of memory</code> 같은 에러 발생.</li>
</ul>
</li>
</ol>
</blockquote>
<h3 id="만약-어떤-프로그램에서-재귀-호출을-엄청-깊게-해서-스택이-엄청-커진다면-힙과-부딪힐-가능성이-생기겠지-이-경우-운영체제는-어떤-식으로-문제를-처리할까">만약 어떤 프로그램에서 <strong>재귀 호출을 엄청 깊게</strong> 해서 스택이 엄청 커진다면, 힙과 부딪힐 가능성이 생기겠지? 이 경우 운영체제는 어떤 식으로 문제를 처리할까?</h3>
<p>일단 힙은 스택보다 낮은 주소에 있고 스택은 낮은 주소에서 높은 주소로 이동을 하는데 어떻게 힙이랑 부딪히는 지 모르겠어.… → 스택은 높은 주소에서 낮은 주소로 성장하기 때문에 만날 수 있음</p>
<blockquote>
<p>운영체제가 <strong>segmentation fault</strong>나 <strong>stack overflow</strong> 같은 에러를 발생시켜 프로그램을 종료해버려.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 - 4주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 23 Sep 2025 10:53:05 GMT</pubDate>
            <description><![CDATA[<h2 id="참고자료">참고자료</h2>
<p><a href="https://os2024.jeju.ai/week04/mlfq.html">https://os2024.jeju.ai/week04/mlfq.html</a></p>
<p><a href="https://os2024.jeju.ai/week04/lottery.html">https://os2024.jeju.ai/week04/lottery.html</a></p>
<p><a href="https://os2024.jeju.ai/week04/lab-sched.html">https://os2024.jeju.ai/week04/lab-sched.html</a></p>
<p><a href="https://os2024.jeju.ai/week04/lab-lottery.html">https://os2024.jeju.ai/week04/lab-lottery.html</a></p>
<h2 id="멀티-레벨-피드백-큐">멀티 레벨 피드백 큐</h2>
<p>MLFQ에 대한 이야기가 나온다.</p>
<blockquote>
<p>MLFQ(다단계 피드백 큐) 스케줄링 알고리즘은 프로세스의 실행 특성에 따라 동적으로 우선순위를 조정합니다.</p>
</blockquote>
<p>프로세스마다 우선순위가 부여되는 것 같고, 아무리 먼저 온 프로세스라도 우선순위가 뒤에 들어온 프로세스보다 낮으면 순위가 조정되는 것 같다.</p>
<hr>
<blockquote>
<p>타임 슬라이스는 각 프로세스에 할당되는 CPU 사용 시간 단위로, 이를 모두 사용하면 선점되고 자발적으로 양보하면 우선순위가 유지됩니다. 이러한 규칙을 통해 I/O를 많이 수행하는 프로세스는 높은 우선순위를 유지하고, CPU를 오래 사용하는 프로세스는 점차 낮은 우선순위로 이동하게 됩니다.</p>
</blockquote>
<h3 id="자신의-우선순위를-유지한다는게-프로세스마다-타임-슬라이스와-우선순위를-갖고-있다는-말인-지-궁금했다-그렇다면-자신이-가지고-있는-타임-슬라이스를-전부-사용했을-경우-큐의-맨-끝에서부터-기다려야하고-타임-슬라이스를-남겨두고-자동으로-자원을-반납한-프로세스는-자신의-우선-순위에-맞게-배정된다는-걸까">자신의 우선순위를 유지한다는게 프로세스마다 타임 슬라이스와 우선순위를 갖고 있다는 말인 지 궁금했다. 그렇다면 자신이 가지고 있는 타임 슬라이스를 전부 사용했을 경우 큐의 맨 끝에서부터 기다려야하고, 타임 슬라이스를 남겨두고 자동으로 자원을 반납한 프로세스는 자신의 우선 순위에 맞게 배정된다는 걸까?</h3>
<p>답변:</p>
<ul>
<li>프로세스는 타임 슬라이스와 우선순위 두 가지 속성을 가짐</li>
<li>자신의 타임 슬라이스를 다 쓰기 전에 cpu를 자발적으로 양보하면 현재 우선순위를 유지한다. 하지만 타임 슬라이스를 끝까지 다 사용하면 낮은 우선순위로 보내진다.</li>
<li>타임 슬라이스를 다 쓰면 우선순위가 한 단계 떨어지고 떨어진 레벨의 큐 맨 끝에서 기다린다.</li>
<li>타임 슬라이스를 남기고 양보하면 우선순위를 그대로 유지하고, 자신의 레벨에서 다음 기회를 얻는다.</li>
</ul>
<p>결론 :</p>
<p>cpu 사용을 오래하는 프로세스는 후순위로 밀리게 되면서 cpu 사용 시간이 감소하고, 적게 사용하는 프로세스는 cpu 사용 시간이 증가한다.</p>
<hr>
<h3 id="타임-슬라이스를-끝까지-다-사용하면-낮은-우선순위로-보내진다고-했는데-우선순위를-어떻게-낮춘다는-말일까-혹시-각-레벨이-존재하고-각-레벨마다-우선순위-큐가-있는걸까-그러면-처음에-1레벨에서-돌다가-타임-슬라이스를-전부-소진하면-2레벨로-떨어지는거고-자동으로-자원을-반납하면-1레벨이-올라가나">타임 슬라이스를 끝까지 다 사용하면 낮은 우선순위로 보내진다고 했는데 우선순위를 어떻게 낮춘다는 말일까? 혹시 각 레벨이 존재하고, 각 레벨마다 우선순위 큐가 있는걸까? 그러면 처음에 1레벨에서 돌다가 타임 슬라이스를 전부 소진하면 2레벨로 떨어지는거고? 자동으로 자원을 반납하면 1레벨이 올라가나?</h3>
<p>답변 :</p>
<ul>
<li>레벨이 올라가는 경우는 없다.</li>
<li>스케줄러는 여러 개의 큐를 준비해 두고, 각 레벨마다 타임 슬라이스 길이도 다르게 정할 수 있다.</li>
</ul>
<hr>
<blockquote>
<ul>
<li>개별 프로세스에 대한 사전 정보가 전혀 없는 상황에서 이런 스케줄러를 어떻게 만들 수 있을까요?</li>
<li>프로세스가 실행되는 동안 그 특성을 파악하고, 이를 토대로 더 나은 스케줄링 결정을 내리려면 어떤 방법을 사용할 수 있을까요?</li>
</ul>
</blockquote>
<p>여기까지만 읽었을 때 실제로 프로세스가 실행되는 시간을 모르는데 어떻게 효율적인 스케줄링을 하지? 라는 의문이 떠오름. 답은 MLFQ이지만 MLFQ는 프로세스의 실행시간을 모르는 상태에서도 타임 슬라이스라는 개념을 두고, cpu를 효과적으로 사용할 수 있도록 관리한다.</p>
<hr>
<blockquote>
<p>MLFQ의 핵심은 바로 이 우선순위를 결정하는 방식에 있습니다. MLFQ는 각 프로세스에 고정된 우선순위를 부여하지 않고, 프로세스의 실행 특성에 따라 동적으로 우선순위를 조정합니다.</p>
</blockquote>
<h3 id="mlfq는-각-프로세스에-고정된-우선순위를-부여하지-않고-프로세스의-실행-특성에-따라-동적으로-우선순위를-조정한다">MLFQ는 각 프로세스에 고정된 우선순위를 부여하지 않고, 프로세스의 실행 특성에 따라 동적으로 우선순위를 조정한다?</h3>
<p>답변 :</p>
<p>고정된 우선순위 값은 존재하지 않음. 오직 큐 레벨이 바뀌는 것임</p>
<p>만약 타임 슬라이스를 모두 사용해서 2레벨로 내려간 뒤에 cpu를 자동 반납했어도 현재 레벨은 유지되지만 큐의 맨 뒤에 삽입된다고 한다.</p>
<hr>
<h3 id="이렇게-스케줄링을-함으로써-얻는-이득이-뭐지-cpu를-독점하는-프로세스의-우선순위를-낮춰서-cpu-점유를-뒤로-늦춘다면-io를-통해서-자주-양보하고-자주-사용하는-프로세스의-반응속도를-높이기-위해서인가">이렇게 스케줄링을 함으로써 얻는 이득이 뭐지? cpu를 독점하는 프로세스의 우선순위를 낮춰서 cpu 점유를 뒤로 늦춘다면 i/o를 통해서 자주 양보하고 자주 사용하는 프로세스의 반응속도를 높이기 위해서인가??</h3>
<p>답변 : </p>
<p>대화형 프로세스의 우선순위를 높게 함으로써 cpu를 요청했을 때 바로 실행될 수 있게 하기 위함.</p>
<hr>
<h2 id="비례-배분">비례 배분</h2>
<blockquote>
<p>비례 배분 스케줄링은 각 작업(job)이나 프로세스에게 CPU 시간의 일정 비율을 보장하는 것을 목표로 합니다.</p>
</blockquote>
<blockquote>
<p>특정한 비율로 CPU 시간을 분배하도록 스케줄러를 어떻게 설계할 수 있을까요? 이를 위해 필요한 핵심 기술은 무엇이고, 그 기술은 얼마나 효과적일까요?</p>
</blockquote>
<h3 id="기존-스케줄링-기법은-우선순위를-부여해서-cpu를-점유할-프로세스를-선정하는-것이라면-비례-배분-스케줄링-기법은-cpu를-점유할-확률을-준다-어떻게-특정-프로세스가-cpu를-점유할-확률을-높이거나-낮출-수-있을까">기존 스케줄링 기법은 우선순위를 부여해서 cpu를 점유할 프로세스를 선정하는 것이라면 비례 배분 스케줄링 기법은 cpu를 점유할 확률을 준다. 어떻게 특정 프로세스가 cpu를 점유할 확률을 높이거나 낮출 수 있을까?</h3>
<p>내가 생각한 바는 프로세스마다 고유 count를 부여하고 랜덤으로 프로세스를 선정한다. 만약 특정 프로세스가 지급 받은 count를 다 사용할 경우 다른 프로세스에게 점유권이 넘어감으로 비율을 유지할 수 있을 거라고 생각했음...</p>
<hr>
<blockquote>
<p>예를 들어 각 프로세스에 우선순위 값을 부여하고, 그 값에 비례하는 시간 동안 CPU를 할당하는 방식 등이 있겠죠.</p>
</blockquote>
<h3 id="위는-비례-배분-스케줄링에-대한-설명이다-우선순위를-부여한다는-것-자체가-특정-프로세스의-cpu-점유-순서를-높이는-거-아닌가-만약-b60가-a70보다-앞에-있어도-a가-먼저-실행될-수-있도록-말이야-근데-비례-배분은-특정-확률에-비례하여-cpu를-점유할-수-있도록-하는-거잖아-약간-다르다고-생각하는데">위는 비례 배분 스케줄링에 대한 설명이다. 우선순위를 부여한다는 것 자체가 특정 프로세스의 cpu 점유 순서를 높이는 거 아닌가? 만약 B(60)가 A(70)보다 앞에 있어도 A가 먼저 실행될 수 있도록 말이야. 근데 비례 배분은 특정 확률에 비례하여 cpu를 점유할 수 있도록 하는 거잖아. 약간 다르다고 생각하는데?</h3>
<p>답변 :</p>
<p>고정 우선순위는 순서를 보장해주는 경우. 하지만 비례 배분은 시간 점유율을 보장한다. 여러 번 반복 실행했을 경우 cpu 사용량이 비율에 맞게 조정함.</p>
<p>만약 cpu 시간이 100ms라면 A에게 70ms를 B에게 30ms를 할당함으로써 여러 번 실행할 경우 비율이 맞도록 설정</p>
<hr>
<blockquote>
<p><strong>추첨권 분배</strong>: 시스템은 각 프로세스의 중요도에 비례하여 추첨권을 나눠줍니다. 중요할수록 더 많은 추첨권을 받게 되고, 따라서 CPU 시간을 할당받을 확률도 높아집니다.</p>
</blockquote>
<h3 id="프로세스의-중요도는-어떻게-평가하지">프로세스의 중요도는 어떻게 평가하지?</h3>
<p>운영체제 설계자나 시스템 관리자가 정한다고 한다.</p>
<hr>
<blockquote>
<ol>
<li><strong>추첨</strong>: 스케줄러는 1부터 10까지의 번호 중 하나를 무작위로 선택합니다.<ul>
<li>1~6이 나오면 프로세스 A가 선택됩니다.</li>
<li>7~9가 나오면 프로세스 B가 선택됩니다.</li>
<li>10이 나오면 프로세스 C가 선택됩니다.</li>
</ul>
</li>
</ol>
</blockquote>
<p>처음에 1 ~ 10까지의 번호 중 무작위 번호가 뽑히면 해당 숫자가 list에서 빠지는 줄 알았는데 아니였다… 티켓 부여는 프로세스의 우선순위에 따라 부여된 것이고, 각 프로세스는 티켓의 수만큼 cpu를 점유할 수 있는 기회를 얻는다. 전체적으로 60%, 30%, 10% 비율로 cpu를 사용할 수 있게된 것이다.</p>
<hr>
<h3 id="보폭-스케줄링">보폭 스케줄링</h3>
<blockquote>
<p>보폭 스케줄링에서는 각 프로세스마다 고유한 ‘보폭(stride)’ 값을 할당합니다. 프로세스가 CPU를 사용할 때마다 ‘pass’ 변수를 보폭만큼 증가시켜 CPU 사용량을 추적합니다. 스케줄러는 이 pass 값과 보폭을 기준으로 다음에 실행할 프로세스를 결정하게 됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/b0d1158d-2aa1-4b7a-ba2d-79e8f6d59757/image.png" alt=""></p>
<blockquote>
<ol>
<li>초기화: 각 프로세스에 초기 보폭을 할당합니다. 보폭은 해당 프로세스의 우선순위나 중요도를 나타내는 값이 됩니다.</li>
<li>프로세스 선택: 실행 가능한 프로세스 중 가장 작은 pass 값을 가진 프로세스를 선택하여 CPU를 할당합니다.</li>
<li>Pass 값 업데이트: 선택된 프로세스는 CPU를 사용하는 동안 자신의 pass 값을 보폭만큼 증가시킵니다. 이를 통해 다른 프로세스들도 공정하게 CPU를 사용할 수 있게 됩니다.</li>
<li>다른 프로세스 대기: CPU를 사용 중인 프로세스 외의 다른 프로세스들은 자신의 pass 값을 유지하거나 감소시킵니다. Pass 값을 감소시키면 더 빨리 CPU를 받을 수 있고, 유지하면 기존의 순서를 유지하게 됩니다.</li>
<li>반복: CPU 사용이 끝나면 위 과정을 반복하여 다음 프로세스를 선택합니다.</li>
</ol>
</blockquote>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/28903190-a34e-4e43-bf0d-eb826a1b08d6/image.png" alt=""></p>
<h2 id="스케줄링-알고리즘">스케줄링 알고리즘</h2>
<blockquote>
<p>각 스케줄링 알고리즘에 따른 작업의 응답 시간, 반환 시간, 대기 시간을 계산하는 방법을 배웁니다.</p>
</blockquote>
<h3 id="이걸-왜-알아야-되는-거지">이걸 왜 알아야 되는 거지?</h3>
<p>어떤 스케줄링 기법이 어떤 상황에선 효율적이고, 어떤 상황에서는 비효율적인지 배울 수 있지 않을까?</p>
<h3 id="대기-시간은-알겠는데-응답-시간-반환-시간은-대체-뭘-의미하는-거지">대기 시간은 알겠는데 응답 시간, 반환 시간은 대체 뭘 의미하는 거지?</h3>
<h4 id="반환-시간-">반환 시간 :</h4>
<p>시스템에 들어와서 끝날 때까지 걸린 총 시간</p>
<p>사용자 관점에서 보면 총 처리 지연 시간이라고 보면 될 것 같다. </p>
<p>반환 시간은 도착했음에도 불구하고 먼저 실행되고 있는 프로세스가 있다면 대기해야 되는 시간 + 실행 시간이 얼마나 걸리는 지 확인할 수 있다.</p>
<p>계산식 → 반환 시간 = 완료 시각 - 도착 시각</p>
<h4 id="대기-시간">대기 시간</h4>
<p>계산식 → 대기 시간 = 반환 시간 - 실행 시간</p>
<h4 id="응답-시간-">응답 시간 :</h4>
<p>응답 시간은 대기 시간이랑 비슷하다고 생각이 든다.</p>
<p>사용자가 어떤 작업을 요청했을 때 첫 반응이 오기까지의 시간을 말한다고 한다.</p>
<p>계산식 → 응답 시간 = 첫 실행 시작 시각 - 도착 시각</p>
<hr>
<h3 id="fifo-스케줄링-기법으로-실행-시간이-258인-프로세스를-실행시킨-결과">fifo 스케줄링 기법으로 실행 시간이 2,5,8인 프로세스를 실행시킨 결과</h3>
<pre><code class="language-jsx">ARG 정책 FIFO
ARG 작업 수 3
ARG 최대 길이 10
ARG 시드 100

각 작업의 실행 시간을 포함한 작업 목록입니다:
  작업 0 ( 길이 = 2 )
  작업 1 ( 길이 = 5 )
  작업 2 ( 길이 = 8 )

** 해결책 **

실행 흔적:
  [ 시간   0 ] 작업 0 실행 2.00 초 ( 완료 시간 2.00 )
  [ 시간   2 ] 작업 1 실행 5.00 초 ( 완료 시간 7.00 )
  [ 시간   7 ] 작업 2 실행 8.00 초 ( 완료 시간 15.00 )

통계:
평균 대기 시간: 3.00 초
평균 회전 시간: 8.00 초
평균 응답 시간: 3.00 초</code></pre>
<p>이 작업을 fifo 스케줄링 기법을 사용했을 때 효율적으로 작업을 하는가? 에 대해 묻는다면 잘 모르겠다고 답할 것 같다…. 그래서 똑같은 조건에서 SJF 스케줄링 기법으로 다시 돌려보았다.</p>
<pre><code class="language-jsx">ARG 정책 SJF
ARG 작업 수 3
ARG 최대 길이 10
ARG 시드 100

각 작업의 실행 시간을 포함한 작업 목록입니다:
  작업 0 ( 길이 = 2 )
  작업 1 ( 길이 = 5 )
  작업 2 ( 길이 = 8 )

** 해결책 **

실행 흔적:
  [ 시간   0 ] 작업 0 실행 2.00 초 ( 완료 시간 2.00 )
  [ 시간   2 ] 작업 1 실행 5.00 초 ( 완료 시간 7.00 )
  [ 시간   7 ] 작업 2 실행 8.00 초 ( 완료 시간 15.00 )

통계:
평균 대기 시간: 3.00 초
평균 회전 시간: 8.00 초
평균 응답 시간: 3.00 초</code></pre>
<p>SJF스케줄링 기법은 실행 시간이 짧은 프로세스를 먼저 실행하기 때문에 똑같은 결과가 나왔다.</p>
<p>마지막으로 RR 스케줄링 기법으로 변경해서 돌려보았다.</p>
<pre><code class="language-jsx">ARG 정책 RR
ARG 작업 수 3
ARG 최대 길이 10
ARG 시드 100

각 작업의 실행 시간을 포함한 작업 목록입니다:
  작업 0 ( 길이 = 2 )
  작업 1 ( 길이 = 5 )
  작업 2 ( 길이 = 8 )

** 해결책 **

실행 흔적:
  [ 시간   0 ] 작업 0 실행 1.00 초 ( 남은 시간 1.00 )
  [ 시간   1 ] 작업 1 실행 1.00 초 ( 남은 시간 4.00 )
  [ 시간   2 ] 작업 2 실행 1.00 초 ( 남은 시간 7.00 )
  [ 시간   3 ] 작업 0 실행 1.00 초 ( 남은 시간 0.00 )
  [ 시간   4 ] 작업 1 실행 1.00 초 ( 남은 시간 3.00 )
  [ 시간   5 ] 작업 2 실행 1.00 초 ( 남은 시간 6.00 )
  [ 시간   6 ] 작업 1 실행 1.00 초 ( 남은 시간 2.00 )
  [ 시간   7 ] 작업 2 실행 1.00 초 ( 남은 시간 5.00 )
  [ 시간   8 ] 작업 1 실행 1.00 초 ( 남은 시간 1.00 )
  [ 시간   9 ] 작업 2 실행 1.00 초 ( 남은 시간 4.00 )
  [ 시간  10 ] 작업 1 실행 1.00 초 ( 남은 시간 0.00 )
  [ 시간  11 ] 작업 2 실행 1.00 초 ( 남은 시간 3.00 )
  [ 시간  12 ] 작업 2 실행 1.00 초 ( 남은 시간 2.00 )
  [ 시간  13 ] 작업 2 실행 1.00 초 ( 남은 시간 1.00 )
  [ 시간  14 ] 작업 2 실행 1.00 초 ( 남은 시간 0.00 )

통계:
  작업 0 회전 시간: 4 초
  작업 1 회전 시간: 11 초
  작업 2 회전 시간: 15 초
평균 회전 시간: 10.00 초</code></pre>
<p>결론 :
실행 시간이 짧은 작업 순으로 도착했을 경우 fifo 스케줄링 기법은 효율적이다.</p>
<hr>
<h3 id="위와-같은-결과를-봤을-때-응답시간-이외에-대기-시간-반환-시간이-다른-스케줄링에-비해-느린-rr-방식은-어떤-상황에서-장점이-있는-걸까">위와 같은 결과를 봤을 때 응답시간 이외에 대기 시간, 반환 시간이 다른 스케줄링에 비해 느린 RR 방식은 어떤 상황에서 장점이 있는 걸까?</h3>
<p>내 생각인데 특정한 한 개의 프로세스가 다른 프로세스들보다 작업 시간이 훨씬 길다면 RR 방식이 더 효율적이지 않을까?</p>
<pre><code class="language-jsx">ARG 정책 RR
ARG 작업 목록 20,5,3

각 작업의 실행 시간을 포함한 작업 목록입니다:
  작업 0 ( 길이 = 20.0 )
  작업 1 ( 길이 = 5.0 )
  작업 2 ( 길이 = 3.0 )

** 해결책 **

실행 흔적:
  [ 시간 0.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 19.00 )
  [ 시간 1.000000 ] 작업 1 실행 1.00 초 ( 남은 시간 4.00 ) 
  [ 시간 2.000000 ] 작업 2 실행 1.00 초 ( 남은 시간 2.00 ) 
  [ 시간 3.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 18.00 )
  [ 시간 4.000000 ] 작업 1 실행 1.00 초 ( 남은 시간 3.00 ) 
  [ 시간 5.000000 ] 작업 2 실행 1.00 초 ( 남은 시간 1.00 ) 
  [ 시간 6.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 17.00 )
  [ 시간 7.000000 ] 작업 1 실행 1.00 초 ( 남은 시간 2.00 )
  [ 시간 8.000000 ] 작업 2 실행 1.00 초 ( 남은 시간 0.00 )
  [ 시간 9.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 16.00 )
  [ 시간 10.000000 ] 작업 1 실행 1.00 초 ( 남은 시간 1.00 )
  [ 시간 11.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 15.00 )
  [ 시간 12.000000 ] 작업 1 실행 1.00 초 ( 남은 시간 0.00 )
  [ 시간 13.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 14.00 )
  [ 시간 14.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 13.00 )
  [ 시간 15.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 12.00 )
  [ 시간 16.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 11.00 )
  [ 시간 17.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 10.00 )
  [ 시간 18.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 9.00 )
  [ 시간 19.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 8.00 )
  [ 시간 20.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 7.00 )
  [ 시간 21.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 6.00 )
  [ 시간 22.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 5.00 )
  [ 시간 23.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 4.00 )
  [ 시간 24.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 3.00 )
  [ 시간 25.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 2.00 )
  [ 시간 26.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 1.00 )
  [ 시간 27.000000 ] 작업 0 실행 1.00 초 ( 남은 시간 0.00 )

통계:
  작업 0 회전 시간: 28.0 초
  작업 1 회전 시간: 13.0 초
  작업 2 회전 시간: 9.0 초
평균 회전 시간: 16.67 초</code></pre>
<p>응답 시간 : 1.0</p>
<p>대기 시간 : 7.33</p>
<pre><code class="language-jsx">ARG 정책 FIFO
ARG 작업 목록 20,5,3

각 작업의 실행 시간을 포함한 작업 목록입니다:
  작업 0 ( 길이 = 20.0 )
  작업 1 ( 길이 = 5.0 )
  작업 2 ( 길이 = 3.0 )

** 해결책 **

실행 흔적:
  [ 시간   0 ] 작업 0 실행 20.00 초 ( 완료 시간 20.00 )
  [ 시간  20 ] 작업 1 실행 5.00 초 ( 완료 시간 25.00 )
  [ 시간  25 ] 작업 2 실행 3.00 초 ( 완료 시간 28.00 )

통계:
평균 대기 시간: 15.00 초
평균 회전 시간: 24.33 초
평균 응답 시간: 15.00 초</code></pre>
<p>fifo는 먼저 도착한 프로세스의 실행시간이 길다면 성능이 좋지 않다.</p>
<pre><code class="language-jsx">ARG 정책 SJF
ARG 작업 목록 20,5,3

각 작업의 실행 시간을 포함한 작업 목록입니다:
  작업 0 ( 길이 = 20.0 )
  작업 1 ( 길이 = 5.0 )
  작업 2 ( 길이 = 3.0 )

** 해결책 **

실행 흔적:
  [ 시간   0 ] 작업 2 실행 3.00 초 ( 완료 시간 3.00 )
  [ 시간   3 ] 작업 1 실행 5.00 초 ( 완료 시간 8.00 )
  [ 시간   8 ] 작업 0 실행 20.00 초 ( 완료 시간 28.00 )

통계:
평균 대기 시간: 3.67 초
평균 회전 시간: 13.00 초
평균 응답 시간: 3.67 초</code></pre>
<p>아무래도 SJF 스케줄링 특성상 실행시간이 짧은 프로세스를 우선적으로 실행하다보니 RR 방식보다 성능이 좋을 수 밖에 없다.</p>
<p>하지만 현실에서 SJF 방식은 프로세스의 실행 시간을 정확히 알 수 있는 방법이 없고, 실행 시간이 긴 프로세스는 계속 후순위로 밀리면서 기아 현상이 발생한다. 반면 RR 방식은 모든 프로세스를 공평하게 실행하므로 기아 현상이 발생하지 않고, 응답 시간이 짧다.</p>
<h2 id="lottery-스케줄링-알고리즘">Lottery 스케줄링 알고리즘</h2>
<p>일단 실행해봤다.</p>
<pre><code class="language-jsx">List: [25] [100] [50] 
List: [25] [100] [50]
winner: 8 25

List: [25] [100] [50]
winner: 11 25

List: [25] [100] [50]
winner: 2 25

List: [25] [100] [50]
winner: 40 100

List: [25] [100] [50]
winner: 43 100

List: [25] [100] [50] 
winner: 10 25

List: [25] [100] [50]
winner: 136 50

List: [25] [100] [50]
winner: 142 50

List: [25] [100] [50]
winner: 99 100

List: [25] [100] [50]
winner: 171 50

List: [25] [100] [50]
winner: 37 100

List: [25] [100] [50]
winner: 152 50

List: [25] [100] [50]
winner: 90 100

List: [25] [100] [50]
winner: 109 100

List: [25] [100] [50]
winner: 13 25

List: [25] [100] [50]
winner: 126 50

List: [25] [100] [50]
winner: 115 100

List: [25] [100] [50]
winner: 1 25

List: [25] [100] [50]
winner: 72 100

List: [25] [100] [50]
winner: 86 100

List: [25] [100] [50]
winner: 136 50

List: [25] [100] [50]
winner: 168 50

List: [25] [100] [50]
winner: 117 100

List: [25] [100] [50]
winner: 79 100

List: [25] [100] [50]
winner: 107 100

List: [25] [100] [50]
winner: 5 25

List: [25] [100] [50]
winner: 12 25

List: [25] [100] [50]
winner: 48 100

List: [25] [100] [50]
winner: 92 100

List: [25] [100] [50]
winner: 110 100

List: [25] [100] [50]
winner: 54 100

List: [25] [100] [50]
winner: 77 100

List: [25] [100] [50]
winner: 122 100

List: [25] [100] [50]
winner: 33 100

List: [25] [100] [50]
winner: 94 100

List: [25] [100] [50]
winner: 142 50

List: [25] [100] [50]
winner: 43 100

List: [25] [100] [50]
winner: 56 100

List: [25] [100] [50]
winner: 86 100

List: [25] [100] [50]
winner: 142 50

List: [25] [100] [50]
winner: 29 100

List: [25] [100] [50]
winner: 123 100

List: [25] [100] [50]
winner: 96 100

List: [25] [100] [50]
winner: 119 100

List: [25] [100] [50]
winner: 34 100

List: [25] [100] [50]
winner: 87 100

List: [25] [100] [50]
winner: 48 100

List: [25] [100] [50]
winner: 149 50

List: [25] [100] [50]
winner: 65 100

List: [25] [100] [50]
winner: 120 100

List: [25] [100] [50]
winner: 38 100

List: [25] [100] [50]
winner: 26 100

List: [25] [100] [50]
winner: 91 100

List: [25] [100] [50]
winner: 155 50

List: [25] [100] [50]
winner: 106 100

List: [25] [100] [50]
winner: 23 25

List: [25] [100] [50]
winner: 137 50

List: [25] [100] [50]
winner: 95 100

List: [25] [100] [50]
winner: 71 100

List: [25] [100] [50]
winner: 31 100

List: [25] [100] [50]
winner: 30 100

List: [25] [100] [50]
winner: 125 50

List: [25] [100] [50]
winner: 109 100

List: [25] [100] [50]
winner: 152 50

List: [25] [100] [50]
winner: 136 50

List: [25] [100] [50]
winner: 5 25

List: [25] [100] [50]
winner: 96 100

List: [25] [100] [50]
winner: 4 25

List: [25] [100] [50]
winner: 38 100

List: [25] [100] [50]
winner: 7 25

List: [25] [100] [50]
winner: 124 100

List: [25] [100] [50]
winner: 45 100

List: [25] [100] [50]
winner: 107 100

List: [25] [100] [50]
winner: 45 100

List: [25] [100] [50]
winner: 164 50

List: [25] [100] [50]
winner: 142 50

List: [25] [100] [50]
winner: 109 100

List: [25] [100] [50]
winner: 14 25

List: [25] [100] [50]
winner: 93 100

List: [25] [100] [50]
winner: 0 25

List: [25] [100] [50]
winner: 112 100

List: [25] [100] [50]
winner: 108 100

List: [25] [100] [50]
winner: 26 100

List: [25] [100] [50]
winner: 28 100

List: [25] [100] [50]
winner: 88 100

List: [25] [100] [50]
winner: 109 100

List: [25] [100] [50]
winner: 28 100

List: [25] [100] [50]
winner: 51 100

List: [25] [100] [50]
winner: 29 100

List: [25] [100] [50]
winner: 99 100

List: [25] [100] [50]
winner: 82 100

List: [25] [100] [50]
winner: 60 100

List: [25] [100] [50]
winner: 26 100

List: [25] [100] [50]
winner: 168 50

List: [25] [100] [50]
winner: 14 25

List: [25] [100] [50]
winner: 162 50

List: [25] [100] [50]
winner: 151 50

List: [25] [100] [50]
winner: 111 100

List: [25] [100] [50]
winner: 144 50

List: [25] [100] [50]
winner: 14 25</code></pre>
<p>결과만보면 3개의 프로세스가 있고, 각 프로세스의 실행 시간을 list에 저장했다. 랜덤으로 티켓을 뽑으면서 뽑힌 티켓에 해당하는 프로세스를 실행하고 있다.</p>
<p>0 ~ 24 → 프로세스 1</p>
<p>25 ~ 124 → 프로세스 2</p>
<p>125 ~ 174 → 프로세스 3</p>
<p>이 된다.</p>
<p>특정 프로세스가 당첨될 확률 = (그 프로세스가 가진 티켓 수) ÷ (총 티켓 수)</p>
<p>결과를 따라 확률을 계산해봤을 때</p>
<p>프로세스 1 → 14%</p>
<p>프로세스 2 → 57%</p>
<p>프로세스 3 → 29%</p>
<p>100회 추첨을 진행했을 때</p>
<p>프로세스 1 → 16%</p>
<p>프로세스 2 → 63%</p>
<p>프로세스 3 → 21%</p>
<p>1000회도 돌려봤는데 터미널에 결과가 다 안나와서 패스…. 아무튼 1만회, 10만회 이렇게 추첨 횟수가 많아질 수록 예상했던 확률과 점차 가까워지게 된다.</p>
<h3 id="간단하게-코드-분석">간단하게 코드 분석</h3>
<pre><code class="language-jsx">void insert(int tickets)
{
    // 새로운 노드를 생성하고 메모리 할당
    struct node_t *tmp = malloc(sizeof(struct node_t));
    assert(tmp != NULL);

    // 새로운 노드의 티켓 수를 설정하고 리스트의 헤드에 삽입
    tmp-&gt;tickets = tickets;
    tmp-&gt;next = head;
    head = tmp;

    // 전역 티켓 카운트 업데이트
    gtickets += tickets;
}</code></pre>
<ul>
<li>노드를 하나 생성하고, tmp에 ticket 부여</li>
<li>tmp의 다음 노드로 head를 이동시키고, tmp가 head를 가리키도록 함.</li>
<li>티켓 하나 생성했으면 ticket 카운트를 1 up</li>
</ul>
<pre><code class="language-jsx">void print_list()
{
    struct node_t *curr = head;
    printf(&quot;List: &quot;);

    // 리스트를 순회하며 각 노드의 티켓 수를 출력
    while (curr)
    {
        printf(&quot;[%d] &quot;, curr-&gt;tickets);
        curr = curr-&gt;next;
    }
    printf(&quot;\n&quot;);
}</code></pre>
<p>주석에 적혀있는 게 다인 것 같다….</p>
<pre><code class="language-jsx">int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        fprintf(stderr, &quot;usage: lottery &lt;seed&gt; &lt;loops&gt;\n&quot;);
        exit(1);
    }

    // 명령행 인자에서 시드와 루프 횟수를 가져옴
    int seed = atoi(argv[1]);
    int loops = atoi(argv[2]);

    // 랜덤 시드 설정
    srandom(seed);

    // 리스트에 작업들을 추가하고 각 작업에 티켓 수를 할당
    insert(50);
    insert(100);
    insert(25);

    // 리스트 출력
    print_list();

    int i;
    for (i = 0; i &lt; loops; i++)
    {
        int counter = 0;
        // 전체 티켓 수 범위 내에서 랜덤한 당첨 번호 선택
        int winner = random() % gtickets;
        struct node_t *current = head;

        // 티켓 수의 합이 당첨 번호보다 클 때까지 리스트를 순회
        while (current)
        {
            counter = counter + current-&gt;tickets;
            if (counter &gt; winner)
                break; // 당첨자 찾음
            current = current-&gt;next;
        }

        // 당첨자 출력 및 스케줄링
        print_list();
        printf(&quot;winner: %d %d\n\n&quot;, winner, current-&gt;tickets);
    }

    return 0;
}</code></pre>
<blockquote>
<ol>
<li><code>main</code> 함수에서는 명령행 인자로 시드와 루프 횟수를 받아옵니다.</li>
<li>랜덤 시드를 설정하고, 리스트에 작업들을 추가합니다.</li>
<li>지정된 루프 횟수만큼 반복하면서 다음을 수행합니다:<ul>
<li>전체 티켓 수 범위 내에서 랜덤한 당첨 번호를 선택합니다.</li>
<li>리스트를 순회하면서 티켓 수의 합이 당첨 번호보다 클 때까지 탐색합니다.</li>
<li>당첨된 작업을 출력하고 스케줄링합니다.</li>
</ul>
</li>
<li>프로그램을 종료합니다.</li>
</ol>
<p>이 코드는 lottery 스케줄링의 기본</p>
</blockquote>
<p>친절하게 강의서에 설명을 해놨다…</p>
<pre><code class="language-jsx">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;assert.h&gt;

// 전역 티켓 카운트
int gtickets = 0;

struct node_t
{
    int tickets;
    struct node_t *next;
};

struct node_t *head = NULL;

void insert(int tickets)
{
    // 새로운 노드를 생성하고 메모리 할당
    struct node_t *tmp = malloc(sizeof(struct node_t));
    assert(tmp != NULL);

    // 새로운 노드의 티켓 수를 설정하고 리스트의 헤드에 삽입
    tmp-&gt;tickets = tickets;
    tmp-&gt;next = head;
    head = tmp;

    // 전역 티켓 카운트 업데이트
    gtickets += tickets;
}

void print_list()
{
    struct node_t *curr = head;
    printf(&quot;List: &quot;);

    // 리스트를 순회하며 각 노드의 티켓 수를 출력
    while (curr)
    {
        printf(&quot;[%d] &quot;, curr-&gt;tickets);
        curr = curr-&gt;next;
    }
    printf(&quot;\n&quot;);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        fprintf(stderr, &quot;usage: lottery &lt;seed&gt; &lt;loops&gt;\n&quot;);
        exit(1);
    }

    // 명령행 인자에서 시드와 루프 횟수를 가져옴
    int seed = atoi(argv[1]);
    int loops = atoi(argv[2]);

    // 랜덤 시드 설정
    srandom(seed);

    // 리스트에 작업들을 추가하고 각 작업에 티켓 수를 할당
    insert(50);
    insert(100);
    insert(25);

    // 리스트 출력
    print_list();

    int i;
    for (i = 0; i &lt; loops; i++)
    {
        int counter = 0;
        // 전체 티켓 수 범위 내에서 랜덤한 당첨 번호 선택
        int winner = random() % gtickets;
        struct node_t *current = head;

        // 티켓 수의 합이 당첨 번호보다 클 때까지 리스트를 순회
        while (current)
        {
            counter = counter + current-&gt;tickets;
            if (counter &gt; winner)
                break; // 당첨자 찾음
            current = current-&gt;next;
        }

        // 당첨자 출력 및 스케줄링
        print_list();
        printf(&quot;winner: %d %d\n\n&quot;, winner, current-&gt;tickets);
    }

    return 0;
}</code></pre>
<p>전체 코드는 위와 같은데</p>
<p>구현 부분에 아래와 같이 설명해놨다..</p>
<blockquote>
<ul>
<li>연결 리스트를 사용하여 작업 큐를 구현합니다.</li>
<li>각 작업에는 티켓 수가 할당되며, 전체 티켓 수는 <code>gtickets</code> 변수에 저장됩니다.</li>
<li><code>insert</code> 함수를 사용하여 새로운 작업을 큐에 추가하고 티켓 수를 할당합니다.</li>
<li><code>print_list</code> 함수를 사용하여 현재 작업 큐의 상태를 출력합니다.</li>
<li><code>main</code> 함수에서는 명령행 인자로 시드와 루프 횟수를 받아와 Lottery 스케줄링을 수행합니다.<ul>
<li>랜덤 번호를 생성하여 당첨된 티켓을 선택합니다.</li>
<li>선택된 티켓에 해당하는 작업을 찾아 스케줄링합니다.</li>
<li>지정된 루프 횟수만큼 스케줄링을 반복합니다.</li>
</ul>
</li>
</ul>
</blockquote>
<p>원래는 이것만 보고 구현해보라고 작성해놓은 것 같은데… 솔직히 나는 못한다… 나는 가짜 중에서도 가짜인 듯…</p>
<h2 id="과제">과제</h2>
<blockquote>
<p>3개의 작업과 난수 시드 1, 2, 3으로 시뮬레이션한 솔루션을 계산하세요.</p>
</blockquote>
<p>난수 시드 3 :</p>
<pre><code class="language-jsx">ARG jlist 
ARG jobs 3
ARG maxlen 10
ARG maxticket 100
ARG quantum 1
ARG seed 3

Here is the job list, with the run time of each job: 
  Job 0 ( length = 2, tickets = 54 )
  Job 1 ( length = 3, tickets = 60 )
  Job 2 ( length = 6, tickets = 6 )

Here is the set of random numbers you will need (at most):
Random 13168
Random 837469
Random 259354
Random 234331
Random 995645
Random 470263
Random 836462
Random 476353
Random 639068
Random 150616
Random 634861</code></pre>
<p>일단 결과가 어떤 의미를 가지고 있는 지 알아야 한다…</p>
<p>3개의 프로세스가 작업을 하고 있고, 실행 시간과 부여 받은 티켓 목록을 표시하고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 - 2주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 09 Sep 2025 11:33:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고 자료 
<a href="https://os2024.jeju.ai/week02/cpu-intro.html">프로세스의 개념</a></p>
</blockquote>
<p>가상화란?
한 개의 자원을 여러 개의 가상 자원으로 만들어 낸 것</p>
<h2 id="프로세스의-개념">프로세스의 개념</h2>
<p>프로그램은 디스크에 저장된 명령어와 데이터의 집합이다.</p>
<p>운영체제는 시분할 방식으로 cpu를 나눠쓴다. 따라서 일정 시간이 지나면 실행 중인 프로세스를 멈추고, 제어권을 실행해야 할 프로세스로 넘겨준다.</p>
<p>프로세스 전환 시에 실행하는 것이 문맥 교환이다. 문맥 교환을 하기 전에 운영체제는 현재 프로세스의 레지스터 값, 프로그램 카운터, 스택 포인터 등을 저장하게 된다. 그리고 새로운 프로세스의 실행 상태를 가져와 cpu 레지스터에 복원한다.</p>
<p>메커니즘 → 운영체제가 필요한 기능을 구현하는 방법
정책 → 운영체제가 어던 결정을 내리기 위한 규칙 및 알고리즘
프로세스 → 실행 중에 접근하거나 영향을 받은 자원의 목록
프로그램 카운터 → 실행 중인 명령어를 알려준다
스택 포인터 &amp; 프레임 포인터 → 함수의 변수와 리턴 주소를 저장하는 스택을 관리한다.</p>
<h3 id="프로세스와-하드웨어-상태는-무슨-연관이-있는거지">프로세스와 하드웨어 상태는 무슨 연관이 있는거지?</h3>
<p>프로세스를 실행하기 위해서는 하드웨어가 무조껀 필요하다. 메모리는 명령어를 읽거나 데이터를 저장하며 레지스터는 명령어가 데이터를 직접 읽거다 변경하기 때문이다</p>
<h3 id="프로세스-api">프로세스 API</h3>
<blockquote>
<p>생성 → 간단한 코드를 작성해서 실행시키면 운영체제가 새로운 프로세스를 생성한다
제거 → 스스로 종료하지 않는 프로세스를 강제로 종료시킨다.
대기 → 특정 프로세스의 작업이 끝날 때까지 기다려야 함
각종 제어 → 프로세스 일시 정지 및 다시 시작 등의 제어 기능들을 제공.
상태 -&gt; 프로세스의 현재 상태 정보를 제공</p>
</blockquote>
<blockquote>
<p>로딩 → 프로그램의 코드와 정적 데이터를 메모리, 즉 프로세스의 주소 공간으로 불러오는 행위.</p>
</blockquote>
<p>그러니까 프로그램을 실행시키기 위해서는 우전 디스크에 있는 프로그램 파일을 읽어와 메모리에 올려놔야 한다는 말이다.</p>
<h4 id="프로그램을-실행시키기-위해서-코드와-정적-데이터를-메모리에-로드한-뒤에-추가로-해야-되는-작업이-있다">프로그램을 실행시키기 위해서 코드와 정적 데이터를 메모리에 로드한 뒤에 추가로 해야 되는 작업이 있다.</h4>
<blockquote>
<p>스택 메모리 할당 → C 프로그램은 지역 변수, 함수 인자, 리턴 주소 등을 저장하기 위해 스택을 사용한다.
힙 메모리 할당 → 동적으로 할당되는 메모리 공간. 가변 크기의 자료구조를 위해 쓰인다.
입출력 초기화 →  프로그램의 입출력을 위한 초기화 작업.</p>
</blockquote>
<h3 id="프로세스-상태">프로세스 상태</h3>
<blockquote>
<p>실행 → 말그대로 프로세스가 실행 중인 상태
준비 → 프로세스가 실행할 준비는 되어있지만 운영체제가 다른 프로세스를 실행시키고 있는 중이므로 대기 상태이다.
대기 → 프로세스의 수행을 중단시키는 연산.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/0e2148b1-0f15-4879-9718-071a2f1a5f19/image.png" alt=""></p>
<blockquote>
<p>‘실행’ 상태에서 ‘준비’ 상태로의 전이는 프로세스가 나중에 다시 스케줄 될 수 있는 상태가 되었다는 것을 의미한다. 프로세스가 입출력 요청 등의 이유로 대기 상태가 되면 요청 완료 등의 이벤트가 발생할 때까지 대기 상태로 유지된다. 이벤트가 발생하면 프로세스는 다시 준비 상태로 전이되고 운영체제의 결정에 따라 바로 다시 실행될 수 있다.</p>
</blockquote>
<p>운영체제는 입출력 요청 등의 작업이 발생하면 해당 프로세스를 대기 상태로 보내버린다. 그리고 요청한 작업이 끝났을 경우 준비 상태로 전이시키고, 스케줄러에 따라 실행 상태로 전이시키는 것을 결정하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/25fcd420-06d5-4716-82e3-f2b58dd3f772/image.png" alt=""></p>
<blockquote>
<p>2개의 프로세스가 있다고 가정을 한다.</p>
<ol>
<li>Process0은 입출력을 요청하고 요청한 작업이 완료되기를 기다린다</li>
<li>프로세스는 디스크를 읽거나 네트워크로부터 패킷을 기다릴 때 대기 상태로 전이한다.</li>
<li>운영체제는 Process0이 CPU를 사용하지 않는다는 것을 감지하고, Process1을 실행시킨다.</li>
<li>Process1이 실행되는 동안 입출력이 완료되고 Process0은 준비 상태로 다시 전이된다.</li>
<li>Process1은 종료되고, Process0이 실행되어 종료된다.</li>
</ol>
</blockquote>
<p>위 내용을 보면 처음에는 프로세스0이 cpu를 점유했지만 입출력 작업을 요청하고 요청된 작업이 완료될 때까지 프로세스0이 cpu를 점유한다면 cpu는 놀게 될 수 밖에 없다. 이것은 자원 이용률을 낮추게 됨으로 운영체제는 프로세스0이 아무 작업을 하지 않고 놀고 있을 때 프로세스0을 대기 상태로 전이시키고, 프로세스1을 실행시킨다.</p>
<p>프로세스1은 요청된 작업이 완료가 되더라도 바로 실행되지 않고, 준비 상태에 있다가 프로세스1이 종료되면 그때서야 cpu를 사용할 수 있게 된다.</p>
<p>위 내용을 봤을 때 운영체제가 어떻게 프로세스0이 놀고 있는 지 아는거지? 라는 궁금증이 생길 수 있다. 프로세스는 입출력 작업이 필요할 때 시스템 콜을 통해서 운영체제에게 현재 입출력 작업이 필요하다는 것을 알려준다고 한다. 이를 통해 운영체제는 해당 프로세스가 cpu를 쓸 일이 없다는 것을 알게 된다. 그리고 입출력 작업이 끝나면 해당 장치에서는 인터럽트를 발생시키고, 운영체제에서 인터럽트를 처리하면서 프로세스를 다시 준비 상태로 옮겨둔다.</p>
<h3 id="자료구조">자료구조</h3>
<blockquote>
<p>운영체제 역시 일종의 프로그램이기에, 다른 프로그램들처럼 여러 정보를 저장하고 관리하기 위한 자료구조를 갖고 있습니다.</p>
</blockquote>
<h3 id="운영체제가-프로그램이라는-것을-처음-알았어-프로그램처럼-생기지-않았는데-운영체제는-어떤-프로그램이지-프로그램이라면-누군가-메모리에-올려서-실행을-해줘야-한다는-얘기는데-이-역할은-누가-해주는데">운영체제가 프로그램이라는 것을 처음 알았어… 프로그램처럼 생기지 않았는데 운영체제는 어떤 프로그램이지? 프로그램이라면 누군가 메모리에 올려서 실행을 해줘야 한다는 얘기는데 이 역할은 누가 해주는데??</h3>
<p>운영체제는 cpu, 메모리, 디스크, 입출력 장치 등을 관리하기 위해 작성되었으며 커널 공간에서 돌아가는 특수한 프로그램이다.</p>
<p>운영체제도 프로그램이라면 실행되기 위해서는 메모리에 올라와야 한다.</p>
<ol>
<li>컴퓨터의 전원이 켜지면 메모리는 비어있고, OS는 디스크에 일반 파일처럼 들어있음.</li>
<li>부트로더를 실행시킨다. 얘기 하는 일이 운영체제 커널 파일을 디스크에서 메모리로 적재하는 것임.</li>
<li>커널이 메모리에 올라오면 제일 먼저 프로세스 테이블, 메모리 맵 등 자신의 자료구조를 초기화한다.</li>
</ol>
<blockquote>
<p>아울러 운영체제는 입출력 작업 등으로 인해 대기(blocked) 상태에 있는 프로세스도 추적해야 합니다. 해당 입출력이 완료되면, 운영체제는 이 정보를 토대로 대기 중이던 프로세스를 깨워 실행 가능한 상태(ready)로 만들어 줄 수 있어야 하죠.</p>
</blockquote>
<p>대기 상태에 있는 프로세스도 운영체제가 깨워줘야 하기 때문에 계속 추적하고 있다고 한다.</p>
<h2 id="프로세스-api-1">프로세스 API</h2>
<blockquote>
<p>프로세스 API는 운영체제(OS)가 애플리케이션에 제공하는 인터페이스로, 사용자 프로그램이 운영체제의 다양한 기능을 사용할 수 있도록 해주는 시스템 호출이다. 이는 프로세스의 생성, 종료, 정지, 재개와 같은 기본적인 관리 작업뿐만 아니라, 프로세스 상태 정보 제공, 메모리 할당, 파일 접근 등 가상 머신 관련 기능을 요청하는데 필수적이다.</p>
</blockquote>
<p>라고 하는데 지금까지 프로젝트 진행할 때 운영체제에 접근해서 프로세스의 생성, 종료를 직접 컨트롤 할 일이 없었던 것 같은데…</p>
<h3 id="fork-시스템-콜">fork() 시스템 콜</h3>
<blockquote>
<p>fork() 시스템 콜은 현재 실행 중인 프로세스(부모 프로세스)와 똑같은 복사본인 새로운 프로세스(자식 프로세스)를 생성하는 기능을 합니다.</p>
</blockquote>
<p>한마디로 부모랑 똑같은 자식 프로세스를 만든다는 것. 이렇게 생성된 자식 프로세스는 부모 프로세스의 메모리 공간을 복사하여 가지게 된다.</p>
<p>자식 프로세스를 만드는 이유는 멀티 코어 환경에서 병렬 처리가 가능하기 때문이다. 이 덕분에 작업 처리 속도가 크게 향상된다. 또한 자신과 똑같은 자식을 생성하고 자신의 작업을 자식에게 위임함으로써 자식은 실행되다가 죽더라도 부모는 자식의 결과를 가지고 다음 작업을 이어나갈 수 있다.</p>
<blockquote>
<p>자식 프로세스는 부모 프로세스의 메모리 공간을 복사 </p>
</blockquote>
<p>자식 프로세스와 부모 프로세스가 동일한 메모리 공간을 가진다고 하는데 이러면 자식과 부모가 동시에 똑같은 변수에 접근한다면 자칫 변수의 값이 잘못될 수도 있다. 따라서 운영체제는 독립된 가상 메모리 공간을 제공해 이러한 문제를 해결한다.</p>
<pre><code>#include &lt;stdio.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;

int main() {
    pid_t pid;
    pid = fork();
    if (pid == 0) { // 자식 프로세스
        printf(&quot;이것은 자식 프로세스입니다.\n&quot;);
    } else if (pid &gt; 0) { // 부모 프로세스
        printf(&quot;이것은 부모 프로세스입니다.\n&quot;);
    } else { // fork 실패
        printf(&quot;fork() 실패\n&quot;);
    }
    return 0;
}</code></pre><p>결과 : </p>
<pre><code class="language-c">이것은 부모 프로세스입니다.
이것은 자식 프로세스입니다.</code></pre>
<h3 id="만약에-독립된-프로세스가-실행이-된다면-부모-프로세스보다-먼저-자식-프로세스가-실행될-수-도-있는거-아니야-꼭-부모-프로세스가-먼저-실행되어야-하는거야">만약에 독립된 프로세스가 실행이 된다면 부모 프로세스보다 먼저 자식 프로세스가 실행될 수 도 있는거 아니야? 꼭 부모 프로세스가 먼저 실행되어야 하는거야?</h3>
<pre><code class="language-c">이것은 자식 프로세스입니다.
이것은 부모 프로세스입니다.</code></pre>
<p>결과를 보면 알 수 있듯이 부모 프로세스라고 해서 먼저 실행이 된다는 보장이 없다. 자식 프로세스가 먼저 실행될 수도 있다는 얘기이다. 왜 그런걸까? 운영체제는 fork()가 호출됨과 동시에 부모와 자식을 생성하고 준비 상태로 이전시킨다. 그리고 어떤 프로세스를 실행시킬지는 cpu 스케줄러의 결정에 달려있다.</p>
<h3 id="pid--fork-이-코드-이해가-안되거든-fork가-종료되면서--1을-감소시키는-거야-그러면-처음에-왜-pid는-0보다-큰거지">pid = fork() 이 코드 이해가 안되거든? fork가 종료되면서 -1을 감소시키는 거야? 그러면 처음에 왜 pid는 0보다 큰거지??</h3>
<p>-1을 감소시킨다는 개념이 아니였다… 부모 프로세스가 실행될 때 pid의 값은 자식 프로세스의 PID값을 리턴하기 때문에 양수가 된다. 그리고 자식 없는 프로세스는 0을 반환 받는다.</p>
<h3 id="wait-시스템-콜">wait() 시스템 콜</h3>
<p>부모 프로세스가 자식 프로세스가 종료될 때까지 기다린다. 기다리면서 자식 프로세스의 종료 상태 값을 얻는다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;
#include &lt;unistd.h&gt;

int main() {
    pid_t pid;
    int status;

    pid = fork();
    if (pid == 0) { // 자식 프로세스
        printf(&quot;자식 프로세스 실행\n&quot;);
    } else if (pid &gt; 0) { // 부모 프로세스
        wait(&amp;status); // 자식 프로세스의 종료를 대기
        printf(&quot;부모 프로세스 재개\n&quot;);
    } else { // fork 실패
        printf(&quot;fork() 실패\n&quot;);
    }

    return 0;
}</code></pre>
<p>결과:</p>
<pre><code class="language-c">자식 프로세스 실행
부모 프로세스 재개
// 자식 종료 상태(raw status) = 0</code></pre>
<p>wait()으로 인해 부모 프로세스는 자식 프로세스가 종료될 때까지 기다려야 한다.</p>
<h3 id="exec-시스템-콜">exec() 시스템 콜</h3>
<blockquote>
<p>프로세스가 새로운 프로그램을 실행하게 해준다.
exec() 호출은 현재 프로세스의 이미지를 새로운 프로그램의 이미지로 교체한다.</p>
</blockquote>
<h3 id="내가-알고-있는-이미지의-개념이랑-다른-것-같은데-프로세스에서의-이미지란-무엇일까">내가 알고 있는 이미지의 개념이랑 다른 것 같은데 프로세스에서의 이미지란 무엇일까?</h3>
<p>프로세스 이미지 → 프로세스 실행에 필요한 메모리 안의 전체 상태 즉, 프로세스를 구성하는 코드와 데이터가 메모리에 올라와 있는 모습을 말한다.</p>
<p>ex)</p>
<ul>
<li><strong>코드 영역 (text segment)</strong> : 실행할 기계어 명령어</li>
<li><strong>데이터 영역 (data segment, bss segment)</strong> : 전역/정적 변수</li>
<li><strong>힙(heap)</strong> : 동적 메모리 영역</li>
<li><strong>스택(stack)</strong> : 함수 호출, 지역 변수, 리턴 주소 등</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;

int main() {
    char *args[] = {&quot;echo&quot;, &quot;Hello, exec()!&quot;, NULL};
    execvp(&quot;echo&quot;, args);

    // execvp 호출 후 이 코드는 실행되지 않습니다.
    printf(&quot;이 문장은 실행되지 않습니다.\n&quot;);

    return 0;
}</code></pre>
<p>결과 :</p>
<pre><code class="language-c">Hello, exec()!</code></pre>
<h3 id="printf이-문장은-실행되지-않습니다n-←-이-코드가-실행되지-않는-이유">printf(&quot;이 문장은 실행되지 않습니다.\n&quot;); ← 이 코드가 실행되지 않는 이유</h3>
<p>execvp()이 실행되면 현재 프로세스 이미지를 새로운 프로세스 이미지로 교체하기 때문이다. 따라서 교체된 이후의 코드는 존재하지 않는 코드가 되기 때문에 출력되지 않는다.</p>
<h2 id="제한적-직접-실행-원리">제한적 직접 실행 원리</h2>
<blockquote>
<p>운영체제는 CPU 가상화를 위해 제한적 직접 실행이라는 기법을 사용합니다. 이 기법의 기본 아이디어는 프로그램을 CPU에서 직접 실행시키되, 운영체제가 CPU 제어권을 잃지 않도록 프로세스의 행동에 제한을 두는 것입니다.</p>
</blockquote>
<p>왜 프로세스의 행동에 제한을 두는 것일까?</p>
<h3 id="기본-원리--제한적-직접-실행">기본 원리 : 제한적 직접 실행</h3>
<blockquote>
<ol>
<li>프로세스를 위한 메모리를 할당하고 프로그램을 메모리에 적재합니다.</li>
<li>CPU를 사용자 모드로 전환하고 프로그램의 main() 함수로 이동합니다.</li>
<li>프로그램이 실행되면서 시스템 콜이 호출되면 커널 모드로 전환되고 운영체제가 해당 요청을 처리합니다.</li>
<li>요청 처리가 완료되면 다시 사용자 모드로 돌아가 프로그램 실행을 계속합니다.</li>
</ol>
</blockquote>
<h3 id="직접-실행-방식을-쓰지-않는-이유">직접 실행 방식을 쓰지 않는 이유</h3>
<ol>
<li>운영체제가 원하지 않는 동작을 프로그램이 수행하지 못하도록 막아야 한다.</li>
<li>운영체제가 cpu 사용을 적절히 분배하기 위해서는 프로그램 실행을 중단하고 다른 프로세스로 전환할 수 있어야 한다.</li>
</ol>
<h3 id="문제점-1-제한된-연산">문제점 1: 제한된 연산</h3>
<p>사용자 모드에서 실행되는 코드의 특정 연산을 제한하는 이유가 뭘까? 
프로세스가 모든 연산을 수행하도록 내버려둔다면 안정성과 보안에 문제가 생기기 때문이다. </p>
<p>그렇다면 제한된 연산을 실행하기 위해서는 어떻게 해야할까? </p>
<blockquote>
<p>하드웨어는 두 가지 실행 모드를 제공하여 운영체제를 지원합니다 :</p>
<ol>
<li>사용자 모드(user mode): 응용 프로그램이 실행되는 모드로, 하드웨어 자원에 대한 접근이 제한됩니다.</li>
<li>커널 모드(kernel mode): 운영체제가 실행되는 모드로, 모든 하드웨어 자원에 접근할 수 있는 권한을 가집니다.</li>
</ol>
</blockquote>
<p>사용자 모드에서 제한된 연산을 수행하려면 커널 모드로 전환해야 한다. 보안상 민감한 명령은 커널 모드에서만 수행할 수 있도록 제한함.</p>
<blockquote>
<ul>
<li>trap: 사용자 모드에서 커널 모드로 전환하는 명령어</li>
<li>return-from-trap: 커널 모드에서 사용자 모드로 돌아가는 명령어</li>
</ul>
<p>프로세스는 제한된 연산이 필요할 때 시스템 콜을 호출하여 trap을 발생시키고, 운영체제는 요청받은 연산을 대신 수행한 뒤 return-from-trap을 통해 다시 프로세스에게 제어를 넘겨줍니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 스터디 - 1주차]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@fishing_bear99/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Tue, 02 Sep 2025 05:00:29 GMT</pubDate>
            <description><![CDATA[<p><a href="https://os2024.jeju.ai/week01/dialogue.html">운영체제 - 아주 쉬운 3가지 이야기</a>
책에서 말하는 3가지 이야기란 가상화, 병행성, 영속성을 말한다.</p>
<p>책 읽기에 앞서 왜 운영체제를 공부해야 되는 지 의문이 들었다. 이전에도 조금 배웠었지만 배운 내용들을 개발할 때 사용해본 적이 없다. 하드웨어 위에서 소프트웨어가 돌아간다고 해도 동시성 처리나 스케줄링 같은 것들은 운영체제가 알아서 해주기 때문에 몰라도 상관없지 않을까?
하지만 앞으로도 이러한 지식이 필요하지 않을거라는 보장이 없으므로 책을 공부해야 되는 동기는 모르겠지만 목표를 하나 정해두고 시작하려고 한다. 목표는 &#39;지금 배운 지식을 가지고 실제 나의 문제를 1가지라도 해결하기&#39;이다. 이론만 머릿속에 넣어서는 아무 의미가 없다고 생각하기 때문에 실제로 나의 문제를 해결하고, 필요성에 대해서 깨닫고 싶다.</p>
<h3 id="cpu-가상화">CPU 가상화</h3>
<p>가상화라는 개념(?)이 왜 탄생했을까? 뭐 때매 탄생했을까? 궁금했다.
처음에 생각한 바는 멀티 프로세스와 관련이 있나 싶었다. cpu는 하나의 프로세스만 점유할 수 있기 때문에 둘 이상의 프로세스를 동작시키기 위해서는 둘 이상의 cpu가 존재해야 한다. 하지만 이렇게 cpu를 늘리는 방식으로 돌려야 하는 많은 프로세스를 감당할 수 있을까?</p>
<pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;sys/time.h&gt;
#include &lt;assert.h&gt;
#include &quot;common.h&quot;

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, &quot;사용법: cpu &lt;문자열&gt;\n&quot;);
        exit(1);
    }
    char *str = argv[1];
    while (1) {
        Spin(1);
        printf(&quot;%s\n&quot;, str);
    }
    return 0;
}</code></pre><p>위 코드를 딱 봤을 때 처음 든 생각은 가상화와 무슨 상관이 있지? 라는 생각이였다. 딱 봐도 while()을 돌면서 1초마다 문자열을 출력하는 코드이기 때문이다.
그런데 cpu는 위 프로그램을 동작시키면서 다른 작업을 할 수 있다. 이게 가능한 이유는 타이머 인터럽트 때문이라고 한다. 타이머 인터럽트는 하나의 프로세스가 cpu를 독점하지 못하도록 일정 주기로 cpu 제어권을 다른 프로세스에게 넘겨준다. 이 덕분에 하나의 프로세스가 cpu를 독점하고 있는 것처럼 보이지만 그렇지 않고, 실행시키는 와중에도 영상이나 다른 작업을 할 수 있다.</p>
<blockquote>
<p>하나의 cpu로 마치 여러 개의 프로세스가 동시에 실행되는 것처럼 보여지는 cpu 가상화, 여러 프로세스가 순서에 맞게 작동하도록 스케줄링 기법.</p>
</blockquote>
<h3 id="메모리-가상화">메모리 가상화</h3>
<pre><code>#include &lt;unistd.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;common.h&quot;

int main(int argc, char *argv[]) {
    int *p = malloc(sizeof(int)); // 정수형 포인터 p에 메모리 할당
    assert(p != NULL); // p가 NULL이 아닌지 확인
    printf(&quot;(%d) p의 메모리 주소: %08x\n&quot;, getpid(), (unsigned) p); // 프로세스 ID와 p의 메모리 주소 출력
    *p = 0; // p가 가리키는 값을 0으로 초기화
    while (1) {
        Spin(1); // 1초 대기
        *p = *p + 1; // p가 가리키는 값을 1 증가
        printf(&quot;(%d) p: %d\n&quot;, getpid(), *p); // 프로세스 ID와 p가 가리키는 값 출력
    }
    return 0;
}</code></pre><p>위 코드의 핵심은 malloc으로 메모리 공간을 할당하고, 넘겨준 주소는 가상 주소라는 것. 위 프로그램은 하나의 프로세스에서 고유한 가상 주소를 갖게 되지만, 말 그대로 가상 주소이기 때문에 또다른 터미널에서 위 프로그램을 실행 하더라도 똑같은 가상 주소가 나올 수 있음.</p>
<p>결론은 운영체제는 가상 메모리 공간에 값을 저장함으로써 다른 프로세스에서 위 주소로 접근하더라도 실제 값은 변경되지 않는다.</p>
<h3 id="병행성">병행성</h3>
<blockquote>
<p>예를 들어, 메인 프로그램이 Pthread_create() 함수를 사용하여 두 개의 쓰레드를 만들고, 각 쓰레드가 worker() 함수를 실행하여 카운터 값을 증가시키는 상황을 생각해 볼 수 있습니다. 만약 각 쓰레드가 카운터를 1000번 증가시키도록 설정된다면, 최종 카운터 값은 2000이 될 것으로 예상할 수 있습니다.</p>
</blockquote>
<p>위 상황에서 2000이 증가될 것 같지만 그렇지 않은 경우가 발생한다. 만약 스레드A가 접근한 값이 0이라고 했을 때 그와 동시에 스레드B가 접근한다면 둘다 0이라는 값을 1 증가시키는 상황이 발생할 수 도 있기 때문이다.
위와 같은 문제는 팀 프로젝트를 진행하면서도 발생했던 문제다. 댓글 하위의 좋아요 버튼이 있는데 나와 다른 유저가 동시에 좋아요 버튼을 누른다면 어떻게 처리해줘야 할까?</p>
<h3 id="영속성">영속성</h3>
<p>일반적으로 DRAM에 저장된 데이터는 휘발된다는 것을 알고 있을 것이다.
그래서 휘발되면 안되는 정보는 디스크에 저장하고, 파일 시스템은 디스크에 저장된 정보에 접근해서 데이터를 읽어올 수 있도록 도와준다.</p>
<pre><code>#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;assert.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;stdlib.h&gt;

int main(int argc, char *argv[]) {
    int fd = open(&quot;/tmp/file&quot;, O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
    if (fd == -1) {
        perror(&quot;open&quot;);
        exit(1);
    }
    assert(fd &gt; -1); // 파일 열기를 확인
    int rc = write(fd, &quot;hello world\n&quot;, 13); // &quot;hello world\n&quot; 문자열 쓰기
    assert(rc == 13); // 쓰기 작업을 확인
    printf(&quot;파일 쓰기 완료&quot;);
    close(fd); // 파일 닫기

    fd = open(&quot;/tmp/file&quot;, O_RDONLY); // 읽기용으로 다시 열기
    if (fd == -1) {
        perror(&quot;open for read&quot;);
        exit(1);
    }
    char buf[100];
    int n = read(fd, buf, sizeof(buf) - 1);
    buf[n] = &#39;\0&#39;; // 문자열 종료
    printf(&quot;읽은 내용: %s\n&quot;, buf);
    close(fd);
    return 0;
}</code></pre><p>디스크에 데이터를 쓰는 건 굉장히 복잡한 일이지만 운영체제는 시스템 콜이라는 표준화된 기법을 통해 우리가 손쉽게 디스크에 데이터를 저장할 수 있도록 도와준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오늘도 해가 지고 들어가는구나...]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%98%A4%EB%8A%98%EB%8F%84-%ED%95%B4%EA%B0%80-%EC%A7%80%EA%B3%A0-%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94%EA%B5%AC%EB%82%98</link>
            <guid>https://velog.io/@fishing_bear99/%EC%98%A4%EB%8A%98%EB%8F%84-%ED%95%B4%EA%B0%80-%EC%A7%80%EA%B3%A0-%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94%EA%B5%AC%EB%82%98</guid>
            <pubDate>Sun, 29 Jun 2025 14:34:35 GMT</pubDate>
            <description><![CDATA[<h1 id="project-시작-전-전체-폴더-구조-설계">project 시작 전 전체 폴더 구조 설계</h1>
<p>아래 코드들은 폴더 구조가 필요한 이유를 좀 더 구체적으로 설명하기 위한 예시 코드입니다.</p>
<h3 id="우선순위">우선순위</h3>
<ul>
<li>utils folder를 따로 생성하는 이유가 뭐임? 해당 folder에 어떤 file을 담을건데?</li>
<li>hooks 자주 쓰는 동작을 커스텀 해야 되는 이유가 뭐임?</li>
<li>auth 폴더를 생성하면 되는데 인증관련 로직을 clerk 라이브러리를 사용해서 인증할꺼거든. 이러면 auth 폴더를 따로 관리할 필요가 있을까?</li>
<li>app하위의 folder 구조를 어떻게 설계해야 할 지 고민</li>
</ul>
<h3 id="fetch-api로-데이터-전송-어떻게-하지">fetch api로 데이터 전송 어떻게 하지?</h3>
<pre><code class="language-c">src/
├── components/
│   └── chatTextArea/
│       └── tiptap.tsx   ← UI 컴포넌트
├── api/
│   └── message.ts       ← fetch wrapper (백엔드 호출)</code></pre>
<p>이라고 가정을 했을 때 클라이언트에서 fetch로 api쪽에 데이터를 보내기 위해서 src/api/message.ts에서</p>
<pre><code class="language-c">// src/api/message.ts

export async function sendMessage({ content, workspaceId, channelId }: {
  content: string;
  workspaceId: string;
  channelId: string;
}) {
  const res = await fetch(`https://your-backend.com/workspaces/${workspaceId}/channels/${channelId}/messages`, {
    method: &#39;POST&#39;,
    headers: {
      &#39;Content-Type&#39;: &#39;application/json&#39;,
    },
    body: JSON.stringify({ content }),
  });

  if (!res.ok) throw new Error(&quot;메시지 전송 실패&quot;);
  return await res.json();
}</code></pre>
<p>위와 같이 보내주는 것임.</p>
<p>아래를 보면</p>
<pre><code class="language-c">// src/components/chatTextArea/tiptap.tsx

import { sendMessage } from &#39;@/api/message&#39;;
import { useParams } from &#39;next/navigation&#39;;
import { useEditor, EditorContent } from &#39;@tiptap/react&#39;;
import StarterKit from &#39;@tiptap/starter-kit&#39;;
import { useState } from &#39;react&#39;;

export default function TipTap() {
  const [content, setContent] = useState(&#39;&#39;);
  const [isSending, setIsSending] = useState(false);
  const params = useParams();

  const editor = useEditor({
    extensions: [StarterKit],
    content: &#39;&#39;,
    onUpdate: ({ editor }) =&gt; {
      setContent(editor.getText());
    },
  });

  const handleSubmit = async () =&gt; {
    if (!content.trim()) return;

    try {
      setIsSending(true);
      await sendMessage({
        content: content.trim(),
        workspaceId: params.workspaceId as string,
        channelId: params.channelId as string,
      });
      editor?.commands.clearContent();
    } catch (err) {
      console.error(&#39;메시지 전송 실패:&#39;, err);
    } finally {
      setIsSending(false);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent) =&gt; {
    if (e.key === &#39;Enter&#39; &amp;&amp; !e.shiftKey) {
      e.preventDefault();
      handleSubmit();
    }
  };

  return (
    &lt;div&gt;
      &lt;EditorContent editor={editor} onKeyDown={handleKeyDown} /&gt;
      &lt;button onClick={handleSubmit} disabled={isSending}&gt;Send&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>먼저 api/message.ts를 import 해온다.</p>
<p>만약 전송 button을 click을 하면 setContent()를 가져와서 content를 sendMessage()의 props로 전달함. sendMessage()에서 전달받은 content를 백으로 전송하는 구조.</p>
<p>위에서 말하는 const params = useParams();는 동적 라우팅 폴더를 만들었을 때 해당 사용자가 접근하는 주소에 따라서( 만약 /post/123이라면 ) 123을 id: 123이라는 객체로 반환한다.</p>
<pre><code class="language-c">const [feeds, setFeeds] = useState([]);

useEffect(() =&gt; {
  fetch(&quot;http://localhost:4000/feed&quot;)
    .then((res) =&gt; res.json())
    .then((data) =&gt; setFeeds(data));
}, []);</code></pre>
<p>이렇게 기능 구현한 file에서 fetch로 데이터를 보낸다면 재사용이 불가능하고 api도 하드코딩해서 박아놨기 때문에 유지보수하는 데 좋지 않은 구조라고 한다.</p>
<h3 id="nextjs-action">Next.js action</h3>
<p>서버(user server)에서 직접 실행된다. </p>
<p>form을 서버 함수에서 바로 제출하는 용도로 사용됨.</p>
<p>단점으로는 코드가 간단한 거에 비해 제한되는 부분이 많다.</p>
<p>채팅처럼 <strong>즉각적인 전송/반응</strong>이 중요한 UI는 TipTap + zustand + fetch 조합이 유리하다.</p>
<p>app/api/message/route.ts는 서버에서 메시지를 받아서 DB에 저장해주는 백엔드의 역할을 하는 것이다.</p>
<h3 id="constants-폴더를-생성해야-되는-이유">constants 폴더를 생성해야 되는 이유</h3>
<pre><code class="language-c">export const API_ENDPOINTS = {
  MESSAGES: &#39;/api/messages&#39;,
  CHANNELS: &#39;/api/channels&#39;,
  USERS: &#39;/api/users&#39;,
  WORKSPACES: &#39;/api/workspaces&#39;,
} as const;</code></pre>
<p>api endpoint를 관리할 수 있음. </p>
<pre><code class="language-c">import { API_ENDPOINTS } from &#39;@/utils/endpoints&#39;;

export async function sendMessage(data: { content: string }) {
  const res = await fetch(API_ENDPOINTS.MESSAGES, {
    method: &#39;POST&#39;,
    body: JSON.stringify(data),
    headers: {
      &#39;Content-Type&#39;: &#39;application/json&#39;,
    },
  });

  return res.json();
}</code></pre>
<p>이런 식으로 import해서 사용하면 messages의 api주소라는 것을 한 눈에 알 수 있고, 수정하기도 쉽다.</p>
<pre><code class="language-c">// constants/websocket.ts
export const WEBSOCKET_URL = process.env.NODE_ENV === &#39;production&#39;
  ? &#39;wss://api.your-domain.com/ws&#39;
  : &#39;ws://localhost:8000/ws&#39;;

export const WEBSOCKET_EVENTS = {
  // 메시지 관련
  MESSAGE_SENT: &#39;message_sent&#39;,
  MESSAGE_RECEIVED: &#39;message_received&#39;,
  MESSAGE_UPDATED: &#39;message_updated&#39;,
  MESSAGE_DELETED: &#39;message_deleted&#39;,

  // 사용자 관련
  USER_JOINED: &#39;user_joined&#39;,
  USER_LEFT: &#39;user_left&#39;,
  USER_TYPING: &#39;user_typing&#39;,
  USER_STOPPED_TYPING: &#39;user_stopped_typing&#39;,

  // 채널 관련
  CHANNEL_CREATED: &#39;channel_created&#39;,
  CHANNEL_UPDATED: &#39;channel_updated&#39;,
  CHANNEL_DELETED: &#39;channel_deleted&#39;,

  // 연결 관련
  CONNECT: &#39;connect&#39;,
  DISCONNECT: &#39;disconnect&#39;,
  RECONNECT: &#39;reconnect&#39;,
  ERROR: &#39;error&#39;,
} as const;

export const WEBSOCKET_RECONNECT = {
  MAX_RETRIES: 5,
  INITIAL_DELAY: 1000,     // 1초
  MAX_DELAY: 30000,        // 30초
  MULTIPLIER: 2,           // 지수 백오프
} as const;</code></pre>
<p>위 코드는 정확히 모른다. 하지만 우리는 websocket을 사용해야 한다. 왜냐하면 실시간 통신을 할꺼니까. 채팅을 사용자가 친다면 실시간으로 화면에 띄워줘야 하니까 websocket을 사용해야 한다. 그러면 위 file은 왜 만드는 걸까?</p>
<p>일단 이렇게 해서 WebSocket을 사용한다면 위와 같이 상수값을 따로 빼서 관리할 경우 재사용</p>
<h3 id="utils-폴더를-생성해야-되는-이유">utils 폴더를 생성해야 되는 이유</h3>
<pre><code class="language-c">// utils/message.ts
export interface Message {
  id: string;
  content: string;
  userId: string;
  channelId: string;
  createdAt: string;
  updatedAt?: string;
  type: &#39;text&#39; | &#39;image&#39; | &#39;file&#39;;
}

export function groupMessagesByDate(messages: Message[]): Record&lt;string, Message[]&gt; {
  return messages.reduce((groups, message) =&gt; {
    const date = new Date(message.createdAt).toDateString();
    if (!groups[date]) {
      groups[date] = [];
    }
    groups[date].push(message);
    return groups;
  }, {} as Record&lt;string, Message[]&gt;);
}

export function isConsecutiveMessage(
  current: Message,
  previous: Message | undefined,
  timeThreshold = 5 * 60 * 1000 // 5분
): boolean {
  if (!previous) return false;

  const isSameUser = current.userId === previous.userId;
  const timeDiff = new Date(current.createdAt).getTime() - new Date(previous.createdAt).getTime();

  return isSameUser &amp;&amp; timeDiff &lt; timeThreshold;
}</code></pre>
<p>message관리. 위는 어디까지나 예시 코드입니다.</p>
<p>message를 보낼 때 보내는 사람의 sendId와 createAt 같은 정보가 필요하다.</p>
<p>위 정보는 백엔드에서 만들어진 메시지를 클라이언트가 가져와서 화면에 던지기 위해서 사용함.</p>
<p>만약 시간을 띄운다거나 날짜 같은 걸 메시지 창에 띄우지 않는다면 utils 구조는 딱히 필요가 없어 보임.</p>
<pre><code class="language-c">// utils/url.ts
export function buildChannelUrl(workspaceId: string, channelId: string): string {
  return `/client/${workspaceId}/channel/${channelId}`;
}

export function buildDMUrl(workspaceId: string, userId: string): string {
  return `/client/${workspaceId}/dm/${userId}`;
}

export function buildProfileUrl(workspaceId: string, userId: string): string {
  return `/client/${workspaceId}/profile/${userId}`;
}

export function parseChannelUrl(url: string): { workspaceId?: string; channelId?: string } {
  const match = url.match(/\/client\/([^\/]+)\/channel\/([^\/]+)/);
  if (match) {
    return { workspaceId: match[1], channelId: match[2] };
  }
  return {};
}</code></pre>
<p>client는 수정할 것임. Slack-LMS처럼 채널, DM, 프로필 등 다양한 URL 패턴이 있는 경우 URL 빌더 함수가 필요하다.</p>
<pre><code class="language-c">// 하드코딩 ❌
const url = `/workspace/${workspaceId}/channel/${channelId}?messageId=${messageId}`;

// url builder 사용 ✅
const url = buildChannelUrl(workspaceId, channelId, { messageId });</code></pre>
<p>사용은 url을 하드코딩하지 않고 채널 url을 해당 file을 import해서 사용할 수 있음. </p>
<p>url builder를 사용한다고 해서 url이 폴더 구조인 next에서 url을 변경할 수 있는 것이 아니라 폴더 구조의 변경이 필요할 때 url을 하드코딩 했다면 이것을 수동으로 일일이 변경해줘야 하는 문제가 생긴다.</p>
<h3 id="auth-폴더를-생성해야-되는-이유">auth 폴더를 생성해야 되는 이유</h3>
<pre><code class="language-c">// auth/storage.ts
import { AuthTokens, User } from &#39;./types&#39;;

export const tokenStorage = {
  // 토큰 저장
  setTokens: (tokens: AuthTokens): void =&gt; {
    localStorage.setItem(&#39;accessToken&#39;, tokens.accessToken);
    localStorage.setItem(&#39;refreshToken&#39;, tokens.refreshToken);
    localStorage.setItem(&#39;tokenExpiresAt&#39;, 
      (Date.now() + tokens.expiresIn * 1000).toString()
    );
  },

  // 액세스 토큰 조회
  getAccessToken: (): string | null =&gt; {
    return localStorage.getItem(&#39;accessToken&#39;);
  },

  // 리프레시 토큰 조회
  getRefreshToken: (): string | null =&gt; {
    return localStorage.getItem(&#39;refreshToken&#39;);
  },

  // 토큰 만료 확인
  isTokenExpired: (): boolean =&gt; {
    const expiresAt = localStorage.getItem(&#39;tokenExpiresAt&#39;);
    if (!expiresAt) return true;

    return Date.now() &gt; parseInt(expiresAt);
  },

  // 토큰 삭제
  clearTokens: (): void =&gt; {
    localStorage.removeItem(&#39;accessToken&#39;);
    localStorage.removeItem(&#39;refreshToken&#39;);
    localStorage.removeItem(&#39;tokenExpiresAt&#39;);
  },

  // 사용자 정보 저장
  setUser: (user: User): void =&gt; {
    localStorage.setItem(&#39;user&#39;, JSON.stringify(user));
  },

  // 사용자 정보 조회
  getUser: (): User | null =&gt; {
    const userStr = localStorage.getItem(&#39;user&#39;);
    if (!userStr) return null;

    try {
      return JSON.parse(userStr);
    } catch {
      return null;
    }
  },

  // 사용자 정보 삭제
  clearUser: (): void =&gt; {
    localStorage.removeItem(&#39;user&#39;);
  },

  // 모든 인증 데이터 삭제
  clearAll: (): void =&gt; {
    tokenStorage.clearTokens();
    tokenStorage.clearUser();
  }
};

export const sessionStorage = {
  // 임시 데이터 저장 (비밀번호 재설정 등)
  setTemporaryData: &lt;T&gt;(key: string, data: T): void =&gt; {
    sessionStorage.setItem(key, JSON.stringify(data));
  },

  getTemporaryData: &lt;T&gt;(key: string): T | null =&gt; {
    const item = sessionStorage.getItem(key);
    if (!item) return null;

    try {
      return JSON.parse(item);
    } catch {
      return null;
    }
  },

  removeTemporaryData: (key: string): void =&gt; {
    sessionStorage.removeItem(key);
  }
};</code></pre>
<p>storage.ts에서 토큰을 관리한다고 함</p>
<pre><code class="language-c">// auth/providers.ts (OAuth)
export const oauthProviders = {
  // 구글 OAuth
  google: {
    getAuthUrl: (): string =&gt; {
      const params = new URLSearchParams({
        client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
        redirect_uri: `${window.location.origin}/auth/callback/google`,
        response_type: &#39;code&#39;,
        scope: &#39;openid email profile&#39;,
        access_type: &#39;offline&#39;,
      });

      return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
    },

    handleCallback: async (code: string): Promise&lt;AuthResponse&gt; =&gt; {
      const response = await fetch(&#39;/api/auth/google/callback&#39;, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
        body: JSON.stringify({ code })
      });

      return response.json();
    }
  },

  // 깃허브 OAuth
  github: {
    getAuthUrl: (): string =&gt; {
      const params = new URLSearchParams({
        client_id: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID!,
        redirect_uri: `${window.location.origin}/auth/callback/github`,
        scope: &#39;user:email&#39;,
      });

      return `https://github.com/login/oauth/authorize?${params}`;
    },

    handleCallback: async (code: string): Promise&lt;AuthResponse&gt; =&gt; {
      const response = await fetch(&#39;/api/auth/github/callback&#39;, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
        body: JSON.stringify({ code })
      });

      return response.json();
    }
  }
};</code></pre>
<p>구글 소셜 로그인을 사용한다면</p>
<pre><code class="language-c">// auth/guards.ts
import { redirect } from &#39;next/navigation&#39;;
import { tokenStorage } from &#39;./storage&#39;;
import { authApi } from &#39;./api&#39;;

// 서버 컴포넌트용 인증 가드
export async function requireAuth(): Promise&lt;User&gt; {
  const token = tokenStorage.getAccessToken();

  if (!token || tokenStorage.isTokenExpired()) {
    redirect(&#39;/auth/login&#39;);
  }

  try {
    const user = await authApi.getCurrentUser();
    return user;
  } catch (error) {
    tokenStorage.clearAll();
    redirect(&#39;/auth/login&#39;);
  }
}

// 워크스페이스 접근 권한 확인
export async function requireWorkspaceAccess(workspaceId: string): Promise&lt;User&gt; {
  const user = await requireAuth();

  if (!user.workspaces.includes(workspaceId)) {
    redirect(&#39;/workspaces&#39;);
  }

  return user;
}

**// 관리자 권한 확인
export async function requireAdmin(): Promise&lt;User&gt; {
  const user = await requireAuth();

  if (user.role !== &#39;admin&#39;) {
    redirect(&#39;/unauthorized&#39;);
  }

  return user;
}**</code></pre>
<p>접근 권한 확인</p>
<h3 id="hooks-폴더를-생성해야-되는-이유">hooks 폴더를 생성해야 되는 이유</h3>
<pre><code class="language-c"># 백엔드 (FastAPI) - 이미 구현되어 있음
@router.websocket(&quot;/{client_id}&quot;)
async def websocket_endpoint(websocket: WebSocket, channel_id: str, client_id: int):
    await connection.connect(channel_id, websocket)  # 연결 관리
    try:
        while True:
            data = await websocket.receive_text()  # 메시지 받기
            await connection.broadcast(channel_id, f&quot;Client #{client_id}: {data}&quot;)  # 브로드캐스트
    except WebSocketDisconnect:
        connection.disconnect(channel_id, websocket)  # 연결 해제</code></pre>
<pre><code class="language-c">// 프론트엔드 - 이건 누가 처리해야 할까?
const ws = new WebSocket(&#39;ws://localhost:8000/ws/123?channel_id=general&#39;);

ws.onopen = () =&gt; console.log(&#39;연결됨&#39;);
ws.onmessage = (event) =&gt; {
  const message = event.data;
  // 받은 메시지를 화면에 어떻게 표시할까?
  // 어떤 컴포넌트에서 처리할까?
};
ws.onclose = () =&gt; console.log(&#39;연결 종료&#39;);
ws.onerror = () =&gt; console.log(&#39;에러 발생&#39;);

// 메시지 보내기
ws.send(&#39;Hello World&#39;);</code></pre>
<p>백에서 전달받은 데이터를 화면에다 실시간으로 뿌려주기 위해 </p>
<pre><code class="language-c">// ❌ 매번 컴포넌트마다 WebSocket 코드 반복
// components/ChatRoom.tsx
function ChatRoom() {
  const [messages, setMessages] = useState([]);
  const [ws, setWs] = useState(null);

  useEffect(() =&gt; {
    const websocket = new WebSocket(&#39;ws://localhost:8000/ws/123?channel_id=general&#39;);

    websocket.onopen = () =&gt; console.log(&#39;연결됨&#39;);
    websocket.onmessage = (event) =&gt; {
      setMessages(prev =&gt; [...prev, event.data]); // 메시지 추가
    };
    websocket.onclose = () =&gt; console.log(&#39;연결 종료&#39;);
    websocket.onerror = () =&gt; console.log(&#39;에러&#39;);

    setWs(websocket);

    return () =&gt; websocket.close();
  }, []);

  const sendMessage = (message) =&gt; {
    if (ws) ws.send(message);
  };

  return &lt;div&gt;채팅 UI&lt;/div&gt;;
}

// components/UserList.tsx  
function UserList() {
  const [users, setUsers] = useState([]);
  const [ws, setWs] = useState(null);

  useEffect(() =&gt; {
    // 똑같은 WebSocket 코드 또 반복! 😱
    const websocket = new WebSocket(&#39;ws://localhost:8000/ws/123?channel_id=general&#39;);

    websocket.onopen = () =&gt; console.log(&#39;연결됨&#39;);
    websocket.onmessage = (event) =&gt; {
      // 사용자 목록 업데이트 로직
    };
    // ... 반복
  }, []);
}</code></pre>
<p>실시간 통신 같은 경우 재사용성을 높이기 위해서 hook으로 만들어서 사용하는 것이 좋아보인다.</p>
<h3 id="대규모-프로젝트도-아니고-tailwind를-사용하기-때문에-styles는-필요없음">대규모 프로젝트도 아니고 tailwind를 사용하기 때문에 styles는 필요없음</h3>
<h1 id="최종-폴더-구조">최종 폴더 구조</h1>
<p>app</p>
<ul>
<li>page.tsx</li>
<li>layout.tsx (현재 로그인 기능이 해당 file에 있는데 page에서 관리해야 한다.)</li>
<li>global.css</li>
<li>workspaceId<ul>
<li>layout.tsx(최상단바, 사이드바, 탭, 프로필)</li>
<li>@sidebar<ul>
<li>page.tsx</li>
</ul>
</li>
<li>@profile<ul>
<li>page.tsx</li>
<li>[tabId]<ul>
<li>layout.tsx(tab 헤더, message, chatting text)</li>
<li>@tabHeader<ul>
<li>page.tsx</li>
</ul>
</li>
<li>@messageArea<ul>
<li>page.tsx</li>
</ul>
</li>
<li>@chatArea<ul>
<li>page.tsx</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>admin<ul>
<li>회원<ul>
<li>page.tsx</li>
</ul>
</li>
<li>그룹<ul>
<li>page.tsx</li>
</ul>
</li>
<li>역할관리<ul>
<li>page.tsx</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>hooks</p>
<ul>
<li>useWebSocket.ts</li>
<li>useMobile.ts</li>
</ul>
<p>auth</p>
<p>(실제 회원가입 &amp; 로그인 기능을 추가한다면)</p>
<ul>
<li>login.ts</li>
<li>logout.ts</li>
<li>vlidation.ts(email, password check)</li>
</ul>
<p>(구글 소셜 로그인을 사용한다면)</p>
<ul>
<li>providers.ts(구글 소셜 로그인을 사용한다면)</li>
</ul>
<p>(공통)</p>
<ul>
<li>storage.ts(로그인에 필요한 토큰을 여기서 관리한다고 함)</li>
<li>guards.ts(실제로 워크스페이스 접근 권한을 확인한다고 함)</li>
</ul>
<p>component</p>
<ul>
<li>chat-text-area<ul>
<li>tiptap.tsx</li>
<li>toolbar.tsx</li>
<li>styles.scss</li>
</ul>
</li>
<li>tiptap-icons<ul>
<li>icons~</li>
</ul>
</li>
<li>ui<ul>
<li>shardcn ui~</li>
</ul>
</li>
</ul>
<p>utils</p>
<ul>
<li>websocket.ts</li>
<li>message.ts</li>
</ul>
<p>public</p>
<ul>
<li>각종 image~</li>
</ul>
<p>store</p>
<ul>
<li>channelStore.ts</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[note에 적다보니 적을 게 없어지는 게 문제]]></title>
            <link>https://velog.io/@fishing_bear99/note%EC%97%90-%EC%A0%81%EB%8B%A4%EB%B3%B4%EB%8B%88-%EC%A0%81%EC%9D%84-%EA%B2%8C-%EC%97%86%EC%96%B4%EC%A7%80%EB%8A%94-%EA%B2%8C-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@fishing_bear99/note%EC%97%90-%EC%A0%81%EB%8B%A4%EB%B3%B4%EB%8B%88-%EC%A0%81%EC%9D%84-%EA%B2%8C-%EC%97%86%EC%96%B4%EC%A7%80%EB%8A%94-%EA%B2%8C-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 27 Jun 2025 15:21:27 GMT</pubDate>
            <description><![CDATA[<h3 id="toolbar">ToolBar</h3>
<p>진짜 만들기 싫었지만 기존 코드에서는 img 삽입 방식을 커스텀하기가 어려워서 한땀한땀 다 가져다 붙였다.</p>
<pre><code>const ToolBar = ({ editor, setLink, addImage }: { editor: Editor; setLink: () =&gt; void; addImage: () =&gt; void }) =&gt; {
  return (
    &lt;div className=&quot;toolbar&quot;&gt;
      &lt;div className=&quot;itemBox&quot;&gt;
        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleBold().run()}
          className={`toolbarBtn ${editor.isActive(&quot;bold&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;BoldIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleItalic().run()}
          className={`toolbarBtn ${editor.isActive(&quot;italic&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;ItalicIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleStrike().run()}
          className={`toolbarBtn ${editor.isActive(&quot;strike&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;StrikeIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button onClick={setLink} className={`toolbarBtn ${editor.isActive(&quot;link&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}&gt;
          &lt;LinkIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleOrderedList().run()}
          className={`toolbarBtn ${editor.isActive(&quot;orderedList&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;ListOrderedIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleBulletList().run()}
          className={`toolbarBtn ${editor.isActive(&quot;bulletList&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;ListIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleBlockquote().run()}
          className={`toolbarBtn ${editor.isActive(&quot;blockquote&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;BlockQuoteIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleCode().run()}
          className={`toolbarBtn ${editor.isActive(&quot;code&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;CodeIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button
          onClick={() =&gt; editor.chain().focus().toggleCodeBlock().run()}
          className={`toolbarBtn ${editor.isActive(&quot;codeBlock&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}
        &gt;
          &lt;CodeBlockIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;

        &lt;button onClick={addImage} className={`toolbarBtn ${editor.isActive(&quot;image&quot;) ? &quot;is-active&quot; : &quot;&quot;}`}&gt;
          &lt;HardDriveUploadIcon className=&quot;w-4 h-4&quot; /&gt;
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};</code></pre><h3 id="tooltip">ToolTip</h3>
<p>img와 link를 넣다보니 코드가 굉장히 길어졌다.
이 부분의 핵심은 file 삽입과 삽입된 file의 미리보기 기능이다.</p>
<pre><code>export default () =&gt; {
  const [text, setText] = useState(&quot;helloWorld&quot;);
  const fileInputRef = useRef&lt;HTMLInputElement&gt;(null);
  const editor = useEditor({
    editable: true,
    extensions: [
      Document,
      Paragraph,
      Text,
      Blockquote,
      Bold,
      Italic,
      Strike,
      Code,
      ListItem,
      OrderedList,
      BulletList,
      CodeBlock,
      Dropcursor,
      Image,
      Link.configure({
        openOnClick: false,
        autolink: true,
        defaultProtocol: &quot;https&quot;,
        protocols: [&quot;http&quot;, &quot;https&quot;],
        isAllowedUri: (url, ctx) =&gt; {
          try {
            // construct URL
            const parsedUrl = url.includes(&quot;:&quot;) ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`);

            // use default validation
            if (!ctx.defaultValidate(parsedUrl.href)) {
              return false;
            }

            // disallowed protocols
            const disallowedProtocols = [&quot;ftp&quot;, &quot;file&quot;, &quot;mailto&quot;];
            const protocol = parsedUrl.protocol.replace(&quot;:&quot;, &quot;&quot;);

            if (disallowedProtocols.includes(protocol)) {
              return false;
            }

            // only allow protocols specified in ctx.protocols
            const allowedProtocols = ctx.protocols.map((p) =&gt; (typeof p === &quot;string&quot; ? p : p.scheme));

            if (!allowedProtocols.includes(protocol)) {
              return false;
            }

            // disallowed domains
            const disallowedDomains = [&quot;example-phishing.com&quot;, &quot;malicious-site.net&quot;];
            const domain = parsedUrl.hostname;

            if (disallowedDomains.includes(domain)) {
              return false;
            }

            // all checks have passed
            return true;
          } catch {
            return false;
          }
        },
        shouldAutoLink: (url) =&gt; {
          try {
            // construct URL
            const parsedUrl = url.includes(&quot;:&quot;) ? new URL(url) : new URL(`https://${url}`);

            // only auto-link if the domain is not in the disallowed list
            const disallowedDomains = [&quot;example-no-autolink.com&quot;, &quot;another-no-autolink.com&quot;];
            const domain = parsedUrl.hostname;

            return !disallowedDomains.includes(domain);
          } catch {
            return false;
          }
        },
      }),
    ],
    content: text,
  });

  const setLink = useCallback(() =&gt; {
    const previousUrl = editor.getAttributes(&quot;link&quot;).href;
    const url = window.prompt(&quot;URL&quot;, previousUrl);

    // cancelled
    if (url === null) {
      return;
    }

    // empty
    if (url === &quot;&quot;) {
      editor.chain().focus().extendMarkRange(&quot;link&quot;).unsetLink().run();

      return;
    }

    // update link
    try {
      editor.chain().focus().extendMarkRange(&quot;link&quot;).setLink({ href: url }).run();
    } catch (e) {
      alert(e.message);
    }
  }, [editor]);

  const convertFileToBase64 = (file: File): Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
      const reader = new FileReader();
      reader.onload = () =&gt; resolve(reader.result as string);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  };

  const handleFileSelect = useCallback(
    async (event: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
      const file = event.target.files?.[0];
      if (file &amp;&amp; editor) {
        // 파일 유효성 검사
        // if (!file.type.startsWith(&quot;image/&quot;)) {
        //   alert(&quot;이미지 파일만 선택할 수 있습니다.&quot;);
        //   return;
        // }

        // 파일 크기 제한 (5MB)
        if (file.size &gt; 5 * 1024 * 1024) {
          alert(&quot;파일 크기는 5MB 이하여야 합니다.&quot;);
          return;
        }

        // 파일을 base64로 변환
        const base64 = await convertFileToBase64(file);

        // 에디터에 이미지 삽입
        if (file.type.startsWith(&quot;image/&quot;)) {
          editor.chain().focus().setImage({ src: base64 }).run();
        } else {
          const ext = file.name.split(&quot;.&quot;).pop()?.toLowerCase() || &quot;&quot;;

          let defaultImg = &quot;/upload_default.png&quot;; // 기본값

          if (ext === &quot;pdf&quot;) defaultImg = &quot;/upload_default.png&quot;;
          else if ([&quot;doc&quot;, &quot;docx&quot;].includes(ext)) defaultImg = &quot;/upload_default.png&quot;;
          else if ([&quot;xls&quot;, &quot;xlsx&quot;].includes(ext)) defaultImg = &quot;/upload_default.png&quot;;
          else if ([&quot;ppt&quot;, &quot;pptx&quot;].includes(ext)) defaultImg = &quot;/upload_default.png&quot;;

          editor.chain().focus().setImage({ src: defaultImg }).run();
        }

        // 파일 입력 초기화
        if (fileInputRef.current) {
          fileInputRef.current.value = &quot;&quot;;
        }
      }
    },
    [editor],
  );

  const addImage = useCallback(() =&gt; {
    fileInputRef.current?.click(); // 숨겨진 input 클릭
  }, []);

  if (!editor) {
    return null;
  }

  return (
    &lt;div className=&quot;chat-text-area&quot;&gt;
      &lt;input
        ref={fileInputRef}
        type=&quot;file&quot;
        accept=&quot;image/*, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx&quot;
        onChange={handleFileSelect}
        style={{ display: &quot;none&quot; }}
      /&gt;
      &lt;div className=&quot;toolbar-container&quot;&gt;
        &lt;ToolBar editor={editor} setLink={setLink} addImage={addImage} /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;editor-container&quot;&gt;
        &lt;EditorContent editor={editor} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};</code></pre><h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/082d36fd-9126-45e3-b66a-d4e858e1c0d5/image.png" alt=""></p>
<p>요즘들어 ai 의존도가 다시 높아졌다. 이 부분이 염려된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오랜만에 쓰는 글]]></title>
            <link>https://velog.io/@fishing_bear99/%EC%98%A4%EB%9E%9C%EB%A7%8C%EC%97%90-%EC%93%B0%EB%8A%94-%EA%B8%80</link>
            <guid>https://velog.io/@fishing_bear99/%EC%98%A4%EB%9E%9C%EB%A7%8C%EC%97%90-%EC%93%B0%EB%8A%94-%EA%B8%80</guid>
            <pubDate>Thu, 26 Jun 2025 14:45:23 GMT</pubDate>
            <description><![CDATA[<p>프레임워크 공부하면서 한참동안 블로그를 작성하지 못했다..
굳이 연습하는 코드를 블로그에 적으면 뭐하나 싶기도 했고..</p>
<h1 id="chat-textarea">chat Textarea</h1>
<h3 id="우선순위">우선순위</h3>
<ol>
<li>chat 서식에 img 첨부 가능하도록 기능 구현</li>
<li>prettier extention 설치해서 은채가 올려준 guide보고 따라하기</li>
<li>팝업창 구현 어떻게 할 지 설계해보기</li>
</ol>
<h3 id="추가-구현해야-할-부분">추가 구현해야 할 부분</h3>
<ul>
<li>window size에 따라 button 부분이 동적으로 동작되도록 구성해야 함.</li>
<li>back으로 보낼 api 추가</li>
<li>chat창에 서식 적용된 text를 어떻게 보여줄 지 고민해봐야 함.</li>
</ul>
<h2 id="수정해야-할-부분">수정해야 할 부분</h2>
<h3 id="우선순위-1">우선순위</h3>
<ul>
<li>미리보기 img 삭제 버튼</li>
<li>pdf file이 삽입되지 않는 문제 수정</li>
<li>chatting이 마우스 drag 시 drag된 부분이 보이지 않는 문제</li>
</ul>
<h2 id="file-upload">file upload</h2>
<h3 id="우선순위-2">우선순위</h3>
<ul>
<li>img 미리보기를 작은 size로 띄우기</li>
<li>file type과 file name 띄우기</li>
</ul>
<h2 id="chatarea-지금까지-구현한-부분">chatArea 지금까지 구현한 부분</h2>
<h3 id="myeditor">MyEditor()</h3>
<pre><code>export default function MyEditor() {
  // 텍스트 영역
  const [text, setText] = useState&lt;string&gt;(&quot;HelloWorld&quot;);
  const [files, setFiles] = useState&lt;File[]&gt;([]);

  // 파일 추가 시 확장되는 높이
  const [isOpen, setIsOpen] = useState&lt;boolean&gt;(false);
  const editorRef = useRef&lt;HTMLDivElement&gt;(null);

  const editor = useEditor({
    immediatelyRender: false,
    extensions: [
      StarterKit,
      Link.configure({
        openOnClick: false,
      }),
    ],
    content: text,
    onUpdate: ({ editor }) =&gt; {
      setText(editor.getText());
    },
  });

  return (
    &lt;div className=&quot;rounded-t-lg rounded-b-lg border-input border&quot;&gt;
      &lt;EditorContext.Provider value={{ editor }}&gt;
        &lt;div className=&quot;p-2 bg-muted/50&quot;&gt;
          &lt;div className=&quot;tiptap-button-group&quot; data-orientation=&quot;horizontal&quot;&gt;
            &lt;MarkButton type=&quot;bold&quot; /&gt;
            &lt;MarkButton type=&quot;italic&quot; /&gt;
            &lt;MarkButton type=&quot;strike&quot; /&gt;

            &lt;LinkPopover /&gt;
            &lt;ListButton type=&quot;orderedList&quot; /&gt;
            &lt;ListButton type=&quot;bulletList&quot; /&gt;

            &lt;BlockquoteButton /&gt;
            &lt;MarkButton type=&quot;code&quot; /&gt;
            &lt;CodeBlockButton /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className={`transition-all duration-300 ease-in`} ref={editorRef} style={{}}&gt;
          &lt;EditorContent
            editor={editor}
            role=&quot;presentation&quot;
            className=&quot;bg-transparent rounded-b-md px-3 py-2 text-sm min-h-16 w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-none&quot;
          /&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;CustomButton
            files={files}
            setFiles={setFiles}
            isOpen={isOpen}
            setIsOpen={setIsOpen}
          /&gt;
        &lt;/div&gt;
      &lt;/EditorContext.Provider&gt;
    &lt;/div&gt;
  );
}</code></pre><h3 id="imagepreview">ImagePreview</h3>
<pre><code>export default function ImagePreview({ files }: ImagePreviewProps) {
  const [previews, setPreviews] = useState&lt;string[]&gt;([]);

  // 파일이 변경될 때마다 미리보기 생성
  useEffect(() =&gt; {
    const generatePreviews = async () =&gt; {
      const newPreviews = await Promise.all(
        files.map((file) =&gt; {
          return new Promise&lt;string&gt;((resolve) =&gt; {
            const reader = new FileReader();
            reader.onload = (e) =&gt; resolve(e.target?.result as string);
            reader.readAsDataURL(file);
          });
        }),
      );
      setPreviews(newPreviews);
    };

    generatePreviews();
  }, [files]);

  return (
    &lt;div className=&quot;p-4&quot;&gt;
      &lt;div className=&quot;grid grid-cols-8 gap-4&quot;&gt;
        {files.map((file, index) =&gt; (
          &lt;div key={index} className=&quot;border rounded-lg&quot;&gt;
            &lt;img
              src={previews[index]}
              alt={file.name}
              className=&quot;w-20 h-20 object-cover rounded&quot;
            /&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre><h3 id="custombutton">CustomButton</h3>
<pre><code>export default function CustomButton({
  files,
  setFiles,
  setIsOpen,
  isOpen,
}: {
  files: File[];
  setFiles: (files: File[]) =&gt; void;
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) =&gt; void;
}) {
  const fileInputRef = useRef&lt;HTMLInputElement&gt;(null);

  const handleFileChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const selected = Array.from(e.target.files || []);
    setFiles([...files, ...selected]);
    setIsOpen(true);
  };

  return (
    &lt;div className=&quot;ml-5 mt-3&quot;&gt;
      {/* 숨겨진 파일 입력 */}
      &lt;input
        ref={fileInputRef}
        type=&quot;file&quot;
        multiple
        accept=&quot;image/*&quot;
        onChange={handleFileChange}
        className=&quot;hidden&quot;
      /&gt;

      {/* 동그란 회색 버튼 */}
      &lt;div&gt;
        &lt;button
          onClick={() =&gt; fileInputRef.current?.click()}
          className=&quot;w-[25px] h-[25px] rounded-full bg-gray-300 hover:bg-gray-500 active:bg-gray-700 transition-colors duration-200 shadow-md&quot;
          title=&quot;파일 선택&quot;
        &gt;
          &lt;Plus className=&quot;h-6 w-6 text-white&quot; /&gt;
        &lt;/button&gt;
      &lt;/div&gt;

      {/* 이미지 미리보기 */}
      {files.length &gt; 0 &amp;&amp; &lt;ImagePreview files={files} /&gt;}
    &lt;/div&gt;
  );
}</code></pre><h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/f0d2642f-c54c-497a-b72f-e282d1ca327d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)오늘은 간단하게...]]></title>
            <link>https://velog.io/@fishing_bear99/TIL%EC%98%A4%EB%8A%98%EC%9D%80-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C</link>
            <guid>https://velog.io/@fishing_bear99/TIL%EC%98%A4%EB%8A%98%EC%9D%80-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C</guid>
            <pubDate>Mon, 16 Jun 2025 16:22:34 GMT</pubDate>
            <description><![CDATA[<h2 id="test">test</h2>
<h3 id="getall">getAll()</h3>
<p>목표 : movies의 list를 check해야 함</p>
<ol>
<li>movies가 비어있는 지 check</li>
<li>getAll()의 type이 list type인지 check</li>
</ol>
<h3 id="getone">getOne()</h3>
<p>목표 : id를 받아서 movies list에 있는 id를 find()해서 가져옴</p>
<ol>
<li>id값이 number type인 지 check(typeScript에서 에러를 내주는 건지 모르겠음)</li>
<li>반환값이 movies 요소인 지 check</li>
<li>id가 존재하지 않으면 에러가 출력되는 지 check</li>
</ol>
<h3 id="deleteone">deleteOne()</h3>
<p>목표 : id를 받아서 movies list에 있는 id를 getOne()해서 해당 id만 filter()한 다음 list에서 pop()해준다.</p>
<p>솔직히 어떻게 뭘 test해야할 지 몰랐다.</p>
<ol>
<li>삭제하기 전 movies의 length를 확인해서 삭제한 후의 length가 -1이 감소했는 지 check</li>
</ol>
<h3 id="create">Create()</h3>
<p>목표 : 하나의 dict를 받아서 list에 push()해줌.</p>
<ol>
<li>추가하기 전과 후의 Length를 비교</li>
<li>나는 각 멤버가 맞게 생성이 됐는 지까지 확인을 했다.</li>
</ol>
<h3 id="update">upDate()</h3>
<p>목표 : movies list에 있는 특정 요소의 멤버 값을 수정한다</p>
<ol>
<li>멤버 값이 수정이 됐는 지 확인</li>
<li>잘못된 인자가 들어오면 예외 처리가 되는 지 확인</li>
</ol>
<h2 id="구현">구현</h2>
<pre><code class="language-c">describe(&#39;MoviesService&#39;, () =&gt; {
  let service: MoviesService;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      providers: [MoviesService],
    }).compile();

    service = module.get&lt;MoviesService&gt;(MoviesService);
  });

  it(&#39;should be defined&#39;, () =&gt; {
    expect(service).toBeDefined();
  });

  describe(&quot;getAll&quot;, () =&gt; {
     it(&quot;sholud return an array&quot;, () =&gt; {
      const result = service.getAll();

      expect(result).toBeInstanceOf(Array);
     })
  })

  describe(&quot;getOne&quot;, () =&gt; {
    it(&#39;moive를 정상적으로 반환합니다&#39;, () =&gt; {
      service.create({
        title: &#39;Test Movie&#39;,
        year: 2000,
        genres: [&#39;test&#39;],
      })

      const moive = service.getOne(1);
      expect(moive).toBeDefined();
      expect(moive.id).toEqual(1);
    })

    it(&#39;id가 존재하지 않으면 error를 출력하는 지 확인합니다&#39;, () =&gt; {
      expect(() =&gt; {
        service.getOne(2);
      }).toThrow(NotFoundException);
    })
  })

  describe(&quot;deleteOne&quot;, () =&gt; {
    it(&quot;제대로 delete 됐는 지 확인합니다.&quot;, () =&gt; {
      service.create({
        title: &#39;Test Movie&#39;,
        year: 2000,
        genres: [&#39;test&#39;],
      })

      expect(() =&gt; {
        const allMovies = service.getAll();
        service.deleteOne(1);
        const afterDelete = service.getAll();

        expect(afterDelete.length).toEqual(allMovies.length - 1);
      })
    })

    it(&#39;404를 뱉는 지 확인합니다.&#39;, () =&gt; {
      try{
        service.deleteOne(999);
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
      }
    })
  })

  describe(&#39;create&#39;, ()=&gt; {
    it(&#39;모든 멤버가 정확히 생성되는 지 check&#39;, () =&gt; {
      const beforeCreate = service.getAll().length;
      service.create({
        title: &#39;Test Movie&#39;,
        year: 2000,
        genres: [&#39;test&#39;],
      })

      const afterCreate = service.getAll().length;
      expect(afterCreate).toBe(beforeCreate + 1);

      const movie = service.getOne(1);
      expect(movie.title).toEqual(&#39;Test Movie&#39;);
      expect(movie.year).toEqual(2000);
      expect(movie.genres).toEqual([&#39;test&#39;]);
    })
  })

  describe(&#39;update&#39;, () =&gt; {
    it(&#39;movie 멤버가 update 됐는 지 확인&#39;, () =&gt; {
      service.create({
        title: &#39;Test Movie&#39;,
        year: 2000,
        genres: [&#39;test&#39;],
      })

      const updateData = {
        title: &#39;update Movie&#39;,
        year: 1000,
      }

      service.update(1, updateData);
      const upDateMovie = service.getOne(1);

      expect(upDateMovie.title).toEqual(&#39;update Movie&#39;);
      expect(upDateMovie.year).toEqual(1000);
    })

    it(&#39;404를 뱉는 지 확인합니다.&#39;, () =&gt; {
      try{
        service.update(999, {});
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
      }
    })
  })
});
</code></pre>
<hr>
<h3 id="db">DB</h3>
<h3 id="memo">memo</h3>
<ul>
<li>id(그냥 auto로 생성하면 될 것 같은데?)</li>
<li>title</li>
<li>descript</li>
<li>create date</li>
</ul>
<h3 id="user">User</h3>
<ul>
<li>id</li>
<li>first name</li>
<li>last name</li>
<li>password</li>
</ul>
<h3 id="comment댓글">Comment(댓글)</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)풀스택을 하기 위해 nest.js를 선택했더니...]]></title>
            <link>https://velog.io/@fishing_bear99/TIL%ED%92%80%EC%8A%A4%ED%83%9D%EC%9D%84-%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%B4-nest.js%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%96%88%EB%8D%94%EB%8B%88</link>
            <guid>https://velog.io/@fishing_bear99/TIL%ED%92%80%EC%8A%A4%ED%83%9D%EC%9D%84-%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%B4-nest.js%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%96%88%EB%8D%94%EB%8B%88</guid>
            <pubDate>Sun, 15 Jun 2025 14:28:39 GMT</pubDate>
            <description><![CDATA[<h3 id="nextjs에-대해서-알아보자">next.js에 대해서 알아보자</h3>
<ul>
<li>폴더 기반 라우팅 서비스를 사용한다?</li>
<li>MVC 모델을 사용한다.</li>
<li>typeScript 기반이다.</li>
</ul>
<h3 id="부족한-부분">부족한 부분</h3>
<ul>
<li>JS에서의 함수 ‘⇒’ 이거 사용하면 어떻게 동작되는 지</li>
<li>typeScript 강의 듣고 와야되나? 간단한 filter()함수 썼는데 이해가 안되네?&#39;</li>
</ul>
<hr>
<h2 id="구현">구현</h2>
<h3 id="문제-1">문제 1</h3>
<pre><code class="language-c">@Get(&quot;:id&quot;)
    getOne(@Param(&#39;id&#39;) id:number):Movie{
        console.log(typeof id);
        return this.movieService.getOne(id);
    }</code></pre>
<p>위 코드에서 log를 찍었을 시 string type으로 찍힘. 이것 때문에 url : <code>http://localhost:3000/movies/1</code>로 id를 입력했는데 get으로 찾을 수 없다길래 어떤 문제인 지 몰랐음. 결론적으로 id값을 int로 type 변환을 해주고서야 성공이 됐는데 분명 type을 int로 줬는데 왜 string으로 type이 들어오는 거야?</p>
<h3 id="문제-2">문제 2</h3>
<pre><code class="language-c">async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));
  await app.listen(process.env.PORT ?? 3000);
}</code></pre>
<p>여기서 crate() → validation check → listen() 이 흐름이 이해가 안됨</p>
<h3 id="문제-3">문제 3</h3>
<p>지금 계속해서 </p>
<p>Cotroller ↔ Service</p>
<p>Service ↔ entity 이 사이에서 dto로 validation check를 해주는 것 같다</p>
<p>아무튼 MVC모델을 사용하는 것 같은데 사실 이부분을 공부하다가 말아서 위 구조에 너무 취약한 상태인 것 같다.</p>
<h3 id="문제-4">문제 4</h3>
<pre><code class="language-c">app.useGlobalPipes(new ValidationPipe)</code></pre>
<p>큰일 났다. validationPipe의 목적은 data type check하는 건 알겠는데 이걸 언제 왜 어디서 써야하는 지 흐름을 모르겠다..</p>
<p>살짝 OOP에 대해서 공부를 해야할 것 같은데 1주일만에 react.js랑 nest.js를 둘 다 배워야한다는게…</p>
<h2 id="crud-실습">CRUD 실습</h2>
<h3 id="controller">controller</h3>
<pre><code>@Controller(&#39;movies&#39;)
export class MoviesController {
    constructor(private readonly movieService: MoviesService){

    }

    @Get()
    getAll():Movie[]{
        return this.movieService.getAll();
    }

    @Get(&quot;search&quot;)
    search(@Query(&#39;name&#39;) searchingName:string){
        return `search for a movie ${searchingName}`;
    }

    @Get(&quot;:id&quot;)
    getOne(@Param(&#39;id&#39;) id:number):Movie{
        console.log(typeof id);
        return this.movieService.getOne(id);
    }

    @Post()
    create(@Body() movieData: CreateMovieDto){
        return this.movieService.create(movieData);
    }

    @Delete(&#39;:id&#39;)
    delete(@Param(&#39;id&#39;) id:number){
        return this.movieService.deleteOne(id);
    }

    @Patch(&#39;:id&#39;)
    patch(@Param(&#39;id&#39;) id:number, @Body() upData:UpdateMovieDto){
        return this.movieService.update(id, upData)
    }
}</code></pre><h3 id="service">Service</h3>
<pre><code>@Injectable() 
export class MoviesService {
    private movies:Movie[] = [];

    getAll():Movie[]{
        return this.movies;
    }

    getOne(id:number):Movie{
        const movie = this.movies.find(movie =&gt; movie.id === +id);
        if(!movie){
            throw new NotFoundException(`Movie ID 존재하지 않음 : ${id}`);
        }
        return movie
    }

    deleteOne(id:number):boolean{
        this.getOne(id);
        this.movies.filter(movie =&gt; movie.id ! == +id);
        return true;
    }

    create(movieData:CreateMovieDto):boolean{
        this.movies.push({
            id: this.movies.length + 1,
            ...movieData,
        });
        return true;
    }

    update(id:number, movieData:UpdateMovieDto):boolean{
        const movie = this.getOne(id)
        this.deleteOne(id);
        this.movies.push({...movie, ...this.update});
        return true
    }
}</code></pre><h3 id="entity">entity</h3>
<pre><code>export class Movie{
    id: number;
    title: string;
    year: number;
    genres: string[];
}</code></pre><h3 id="create-dto">create dto</h3>
<pre><code>export class CreateMovieDto{
    @IsString()
    readonly title: string

    @IsNumber()
    readonly year: number

    @IsOptional()
    @IsString({each: true})
    readonly genres: string[]
}</code></pre><h3 id="update-dto">update dto</h3>
<pre><code>export class UpdateMovieDto extends PartialType(CreateMovieDto){

}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)swap마저 진행하기]]></title>
            <link>https://velog.io/@fishing_bear99/TILswap%EB%A7%88%EC%A0%80-%EC%A7%84%ED%96%89%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fishing_bear99/TILswap%EB%A7%88%EC%A0%80-%EC%A7%84%ED%96%89%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 11 Jun 2025 14:30:31 GMT</pubDate>
            <description><![CDATA[<h2 id="vm_anon_init-수정">vm_anon_init 수정</h2>
<h3 id="flow">flow</h3>
<p>→ disk를 초기화 한다.</p>
<p>→ disk의 정보를 가져와서</p>
<p>→ disk size를 계산한다.</p>
<p>→ swap table을 생성한다.
<img src="https://velog.velcdn.com/images/fishing_bear99/post/0c6689e5-3ee7-4f5d-8bd5-df6085f4a72f/image.png" alt=""></p>
<h3 id="구현">구현</h3>
<pre><code class="language-c">// assertion `intr_handlers[vec_no] == NULL&#39; failed.
/* 익명 페이지를 위한 데이터를 초기화합니다 */
void vm_anon_init(void)
{
    // TODO: swap 구현 시 아래 내용을 추가해야 함. 사유는 그때 가서 이해하기.
    /* swap_disk를 설정하세요. */
    swap_disk = disk_get(1, 1); // NOTE: disk_get 인자값 적절성 검토 완료.
    if(swap_disk == NULL){
        PANIC(&quot;[vm_anon_init] swap dist를 가져오는 데 실패함\n&quot;);
    }
    size_t sector_count = disk_size(swap_disk); // SECTORS_PER_PAGE;
    size_t slot_count = sector_count / 8;
    swap_table = bitmap_create(slot_count); // disk size만큼 slot을 생성한다. slot은 bitmap으로 관리하며 slot의 사용여부는 0과 1로 판단한다.
}</code></pre>
<hr>
<h3 id="static-bool-anon_swap_in-struct-page-page-void-kva">static bool anon_swap_in (struct page *page, void *kva);</h3>
<h3 id="구상">구상</h3>
<ul>
<li>read(slot idx)<ul>
<li>swap disk → anon page로 데이터를 읽어와야 한다.</li>
</ul>
</li>
<li>bitmap reset(slot idx)<ul>
<li>slot을 다시 사용할 수 있게 데이터를 reset한다.</li>
</ul>
</li>
<li>slot idx 초기화</li>
<li><del>anon page initializer()</del></li>
<li>swap in()이 성공했으면 return true</li>
</ul>
<h3 id="구현-1">구현</h3>
<pre><code class="language-c">/* 스왑 디스크에서 내용을 읽어 페이지를 스왑인합니다 */
static bool
anon_swap_in(struct page *page, void *kva)
{
    // 스왑 디스크 데이터 내용을 읽어서 익명 페이지를(디스크에서 메모리로)  swap in합니다. 
    // 스왑 아웃 될 때 페이지 구조체는 스왑 디스크에 저장되어 있어야 합니다. 
    // 스왑 테이블을 업데이트해야 합니다(스왑 테이블 관리 참조).
    struct anon_page *anon_page = &amp;page-&gt;anon;
    for(int i=0; i&lt;8; i++){
        disk_read(swap_disk, anon_page-&gt;swap_slot_idx * DISK_SECTOR_COUNT + i, kva + DISK_SECTOR_SIZE * i);
    }
    bitmap_reset(swap_table, anon_page-&gt;swap_slot_idx);
    // lock??
    anon_page-&gt;swap_slot_idx = BITMAP_ERROR; // 스왑 슬롯을 NULL로 초기화 해준거나 같음.

    return true;
}</code></pre>
<hr>
<h3 id="static-bool-anon_swap_out-struct-page-page"><code>static bool anon_swap_out (struct page *page);</code></h3>
<h3 id="구상-1">구상</h3>
<ul>
<li>bitmap_scan_and_flip()를 사용해서 slot bit가 0인 slot을 찾는다.</li>
<li><del>memcpy(anon, swap slot)</del>인줄 알았지만 swap disk에 메모리를 복사하는 게 아니다… disk write()를 이용해서 anon page의 데이터를 swap disk의 slot공간에다가 write()해줘야 한다…</li>
<li>swap slot idx를 anon page struct에 멤버 변수로 만들어주고,</li>
<li>swap slot idx를 초기화 해준다.</li>
<li>swap out()이 성공했다면 return true</li>
</ul>
<h3 id="구현하면서-알게-됐던-점">구현하면서 알게 됐던 점</h3>
<ul>
<li><p>1page = 1slot이고, 1slot = 8sector이고, 1sector = 512byte이다.</p>
</li>
<li><p>swap disk에 메모리를 복사하는 게 아니다… disk write()를 이용해서 anon page의 데이터를 swap disk의 slot공간에다가 write()해줘야 한다…</p>
<p>  → swap disk의 swap slot에 anon page의 데이터를 저장해야 한다.</p>
<p>  → disk write()로 swap disk에 write()할 수 있는 최대 size는 512byte이다. 따라서 1page 데이터를 옮기려면 512 * 8(=4096)을 disk write()해줘야 함.</p>
<p>  → page→frame→kva의 주소가 slot의 top이라고 생각하면 1번 loop을 돌 때마다 +512byte를 해줘야 함.</p>
<ul>
<li>→ disk는 연속된 sector로 이루어져 있고, 원하는 sector에 write()하려면 slot idx에다 sector count를 곱해줘야 함. 그냥 slot idx를 사용할 경우 원하는 sector idx가 아닌 엉뚱한 sector에 값을 덮어쓸 수 있음</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/fishing_bear99/post/28778348-23fc-4b88-8ec3-d146d1423142/image.png" alt=""></p>
<h3 id="구현-2">구현</h3>
<pre><code class="language-c">/* 스왑 디스크에 내용을 써서 페이지를 스왑아웃합니다 */
static bool
anon_swap_out(struct page *page)
{
    // TODO: 메모리에서 디스크로 내용을 복사하여 익명 페이지를 스왑 디스크로 교체합니다. 

    // 먼저 스왑 테이블을 사용하여 디스크에서 사용 가능한 스왑 슬롯을 찾은 다음 데이터 페이지를 슬롯에 복사합니다. 
    // 데이터의 위치는 페이지 구조체에 저장되어야 합니다. 디스크에 사용 가능한 슬롯이 더 이상 없으면 커널 패닉이 발생할 수 있습니다.
    struct anon_page *anon_page = &amp;page-&gt;anon;
    size_t swap_slot_idx = bitmap_scan_and_flip(swap_table, 0, 1, false);
    if(swap_slot_idx == BITMAP_ERROR){
        PANIC(&quot;[anon_swap_out] 비어있는 swap slot을 찾는데 실패했습니다\n&quot;);
    }

    for(int i=0; i&lt;8; i++){
        disk_write(swap_disk, swap_slot_idx * DISK_SECTOR_COUNT + i, page-&gt;frame-&gt;kva + DISK_SECTOR_SIZE * i);
    }

    anon_page-&gt;swap_slot_idx = swap_slot_idx;

    return true;
}</code></pre>
<hr>
<h2 id="debug">debug</h2>
<h3 id="try-1">try 1</h3>
<pre><code class="language-c">Kernel PANIC at ../../threads/interrupt.c:213 in register_handler(): assertion `intr_handlers[vec_no] == NULL&#39; failed.</code></pre>
<pre><code class="language-c">// assertion `intr_handlers[vec_no] == NULL&#39; failed.
/* 익명 페이지를 위한 데이터를 초기화합니다 */
void vm_anon_init(void)
{
    // TODO: swap 구현 시 아래 내용을 추가해야 함. 사유는 그때 가서 이해하기.
    /* swap_disk를 설정하세요. */
    // disk_init();
    swap_disk = disk_get(1, 1); // NOTE: disk_get 인자값 적절성 검토 완료.
    if(swap_disk == NULL){
        PANIC(&quot;[vm_anon_init] swap dist를 가져오는 데 실패함\n&quot;);
    }
    size_t sector_count = disk_size(swap_disk); // SECTORS_PER_PAGE;
    size_t slot_count = sector_count / 8;
    swap_table = bitmap_create(slot_count); // disk size만큼 slot을 생성한다. slot은 bitmap으로 관리하며 slot의 사용여부는 0과 1로 판단한다.
}</code></pre>
<p>vm anon init()에서 disk init()를 호출해서 문제가 발생한 거였음. disk init()은 시스템이 부팅될 때 호출이 되기 때문에 중복 호출이 발생함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)swap에 대해서]]></title>
            <link>https://velog.io/@fishing_bear99/TILswap%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@fishing_bear99/TILswap%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Tue, 10 Jun 2025 15:41:53 GMT</pubDate>
            <description><![CDATA[<h2 id="swap-inout"><code>Swap In/Out</code></h2>
<h3 id="git-book-내용-정리">git book 내용 정리</h3>
<ul>
<li>swap slot? → disk 공간에 있는 page size 정도의 공간을 말함.<ul>
<li>page size에 따라 정렬해야 한다.</li>
</ul>
</li>
<li>같은 프레임을 참조하는 여러 page들(aliases)를 조심해라.<ul>
<li>유저 가상 주소로 접근한 것인지 check</li>
</ul>
</li>
<li>swap table을 관리해야 한다<ul>
<li>process exit()될 경우 swap slot을 free() 해줘야 한다.</li>
<li>스왑 슬롯은 필요할 때마다 할당이 이루어져야 한다. page하나를 저장하기 위해 미리 메모리에 할당하는 것은 매우 비효율적이다.</li>
</ul>
</li>
</ul>
<h3 id="구상">구상</h3>
<ul>
<li>퇴거시킬 page선택한다() - evict frame<ul>
<li>vm type이 VM anon인지 check</li>
<li>page table entry에 있는 비트쌍인 accessed bit와 dirty bit를 이용해서 page 재배치 알고리즘을 구현해야 한다.</li>
<li>page를 read 시에는 accessed bit를 1로 설정, write 시에는 dirty bit를 1로 설정해줘야 한다.</li>
<li>사용 가능 여부를 확인하기 위해서는 accessed bit가 1인지 확인한다. 만약 1이라면 다음 page를 탐색하고, 0이라면 퇴거시킨다.</li>
<li>퇴거시킬 때 dirty bit를 check해야하는데 dirty bit가 1이면 swap out()을 진행하고, 0이면 그냥 버려도 된다.</li>
</ul>
</li>
<li>swap out()<ul>
<li>page→swap slot idx를 -1로 초기화한다. - swap in할 때 idx로 page를 가져옴</li>
<li>page→frame을 메모리에서 Disk로 복사해야 한다.</li>
<li>이때 block write()에서 sector 단위가 512byte이기 때문에 pgsize(4096)짜리 데이터를 읽기 위해서는 8개 섹터(4096/512)를 거쳐야 한다.</li>
<li>page→frame 연결 해제</li>
<li>pml4에서 해당 entry를 제거해서 가상 주소와의 매핑을 해제해야 함.</li>
<li>frame→page = NULL로 초기화함으로써 해당 frame을 다른 page에서도 사용할 수 있게 해줘야 함?</li>
<li>swap table이나 disk 접근은 swap lock으로 보호해야 한다.</li>
<li>만약 slot이 없거나 block write(), read()의 실패처리는 panic()으로 처리한다.</li>
</ul>
</li>
<li>swap in()<ul>
<li>page→frame을 재할당하고, swap 데이터 복원?</li>
<li>이때 block read()에서 sector 단위가 512byte이기 때문에 pgsize(4096)짜리 데이터를 읽기 위해서는 8개 섹터(4096/512)를 거쳐야 한다.</li>
<li>만약 할당 실패했다면 page→frame에 palloc get page(pal user)을 설정해줌으로써 다른 frame을 evict해준다.</li>
<li>page→frame을 할당했다면 pml4_set_page()를 사용해서 가상주소와 매핑을 진행시켜줘야 한다.</li>
<li>swap table이나 disk 접근은 swap lock으로 보호해야 한다.</li>
<li>만약 slot이 없거나 block write(), read()의 실패처리는 panic()으로 처리한다.</li>
</ul>
</li>
</ul>
<h3 id="함수의-역할">함수의 역할</h3>
<p>→ vm_anon_init()</p>
<ul>
<li>swap Disk init?</li>
<li>swap disk get()</li>
<li>swap slot 수 계산?? -&gt; 이건 왜 필요한거지?</li>
<li>swap table create() -&gt; 이해 못 함</li>
</ul>
<p>→ anon_initializer()</p>
<ul>
<li>anon page → swap slot idx 초기화??</li>
<li>page→frame == NULL check?</li>
</ul>
<p>→ anon swap out()</p>
<ul>
<li>anon page를 swap disk로 교체?<ul>
<li>사용 가능한 swap slot을 bitmap에서 찾는다?</li>
<li>memcpy()로 anon page를 swap slot에 복사?</li>
<li>swap slot idx를 필드에 저장?</li>
<li>swap 실패 시 frame 연결 해제 및 fasle 반환</li>
<li>dirty bit check?</li>
</ul>
</li>
</ul>
<p>→ anon swap in()</p>
<ul>
<li>swap table에 할당된 swap slot idx를 가져와서</li>
<li>read at()으로 anon page를 읽는다.</li>
<li>memcpy()로 해당 데이터를 frame→kva로 복사.</li>
<li>swap table의 해당 slot을 bitmap reset()을 사용하여 재사용 가능하게 설정?</li>
<li>swap slot idx 초기화</li>
<li>anon page가 물리 frame과 연결되어있음을 명시한다.</li>
</ul>
<h2 id="static-struct-frame-vm_get_victimvoid">static struct frame *vm_get_victim(void)</h2>
<h3 id="목표">목표</h3>
<p>page table entry에 있는 비트쌍인 accessed bit와 dirty bit를 이용해서 page 재배치 알고리즘을 구현하고, 내쫓을 frame을 반환한다.</p>
<h3 id="flow">flow</h3>
<ul>
<li><p>frame table을 만든다</p>
</li>
<li><p>frame struct에 frame elem 멤버를 추가한다.</p>
</li>
<li><p>frame table을 list end()까지 순회하면서</p>
</li>
<li><p>frame이 NULL인지 check. 만약 frame이 NULL이라면 frame table이 존재하지 않는다는 말이되기 때문이다.</p>
</li>
<li><p>accessed bit가 1인지 check한다. 이미 접근한 page는 사용중일 확률이 높으므로 접근하지 않은 page를 희생시킨다.</p>
</li>
<li><p>page가 anon page인지 확인한다. file backed page일 경우에는 파일에 대한 데이터 일관성 유지로 인해서 eviction 대상에서 제외한다는 데 이 부분은 그림을 그려봐야 이해가 될 것 같다.</p>
</li>
<li><p>위 조건을 만족하지 않을 경우 희생 page로 설정한다.</p>
<ul>
<li>frame table에서 삭제하고, 해당 frame을 반환한다.</li>
</ul>
</li>
<li><p>만약 frame table에 희생시킬 page가 존재하지 않는다면 panic()을 발생시킨다.</p>
<h3 id="구현">구현</h3>
<pre><code>static struct frame *
vm_get_victim(void)
{
  /* TODO: 페이지 교체 정책은 여러분이 결정할 수 있습니다. */
  struct thread *curr = thread_current();
  struct list_elem *e = list_begin(&amp;frame_table);

  while(e != list_end(&amp;frame_table)){
      struct frame *f = list_entry(e, struct frame, frame_elem);
      struct page *page = f-&gt;page;

      // 만약 frame이 존재하지 않는다면...
      if(f == NULL){
          PANIC(&quot;[vm_get_victim] frame이 존재하지 않습니다...\n&quot;);
      }

      if (VM_ANON != page-&gt;operations-&gt;type){
          e = list_next(e);
          continue;
      }

      if(pml4_is_accessed(curr-&gt;pml4, page-&gt;va)){ 
          /* accessed bit를 reset()을 사용해서 0으로 만들고 넘어간다.
          * 내 생각으로는 이미 한 번 접근한 page는 접근 bit를 1로 만들지만 다시 접근했을 때 0으로 초기화 해주는 이유는
          * 기존의 page가 메모리 공간을 차지함으로써 새로운 page가 로드되는 걸 막기 때문이지 않을까?? */
          pml4_set_accessed(curr-&gt;pml4, page-&gt;va, false);
          e = list_next(e);
          continue;
      }

      list_remove(e);
      return f;
  }
  PANIC(&quot;[vm_get_victim] frame victim이 존재하지 않아요 ㅠㅠ\n&quot;);
}</code></pre></li>
</ul>
<h3 id="static-struct-frame-vm_get_framevoid">static struct frame *vm_get_frame(void)</h3>
<pre><code>static struct frame *
vm_get_frame(void)
{
    struct frame *frame = malloc(sizeof(struct frame));

    if (frame == NULL)
    {
        PANIC(&quot;TODO&quot;);
    }

    // NOTE: PAL_USER인 이유는 주석에 user space pages를 본 함수로 할당받아야 한다고 명시되어 있어서 이렇게 함. 악성 프로그램이 고의로 커널풀 메모리 고갈시키는 거 막기 위한 분리.
    frame-&gt;page = NULL;
    ASSERT(frame-&gt;page == NULL);

    frame-&gt;kva = palloc_get_page(PAL_USER);
    if (frame-&gt;kva == NULL)
    {
        free(frame);              // frame 메타 데이터 자료구조 해제
        frame = vm_evict_frame(); // TODO: evict frame 함수가 아직 구현되지 않음.
    }
    ASSERT(frame-&gt;kva != NULL);

    // frame lock 걸어줘야 함.
    list_push_back(&amp;frame_table, &amp;frame-&gt;frame_elem);

    return frame;
}</code></pre><p>frame은 vm_get_frame()에서 push해준다.</p>
<hr>
<h2 id="static-struct-frame-vm_evict_framevoid">static struct frame *vm_evict_frame(void)</h2>
<h3 id="구상-1">구상</h3>
<ul>
<li>frame을 change해줘야 한다.<ul>
<li>현재 입력 받은 victim frame의 dirty bit을 check</li>
<li>만약 dirty bit가 1이면 swap out()</li>
<li>0이면 그냥 버린다.</li>
</ul>
</li>
<li>page→frame을 NULL로 초기화한다.</li>
<li>page를 NULL로 초기화한다.</li>
<li>재사용할 frame을 반환한다.</li>
</ul>
<h3 id="구현-1">구현</h3>
<pre><code class="language-c">/* 하나의 페이지를 교체하고 해당 프레임을 반환합니다.
 * 실패 시 NULL을 반환합니다. */
static struct frame *
vm_evict_frame(void)
{
    /* TODO: victim을 스왑 아웃하고 교체된 프레임을 반환하세요. */
    struct frame *victim UNUSED = vm_get_victim();
    struct thread *curr = thread_current();
    struct page *page = victim-&gt;page;
    struct frame *change_f;
    // 퇴거시킬 때 dirty bit를 check해야하는데 dirty bit가 1이면 swap out()을 진행하고, 0이면 그냥 버려도 된다.
    if(pml4_is_dirty(curr-&gt;pml4, page-&gt;va)){
        if(!swap_out(page)){
            PANIC(&quot;[vm_evict_frame]swap out fail!!!\n&quot;);
        }
    }

    page-&gt;frame = NULL;
    victim-&gt;page = NULL;

    return victim;
}</code></pre>
<h2 id="vm_anon_init"><code>vm_anon_init()</code></h2>
<h3 id="구상-2">구상</h3>
<ul>
<li>스왑 disk가 뭐지??</li>
<li>사용 가능한 영역과 사용된 영역을 어떻게 구분해??<ul>
<li>swap size을 계산해서 top부터 size까지는 사용된 영역, 그 밑으로는 사용 가능한 영역으로 나누면 어떨까?</li>
</ul>
</li>
</ul>
<h3 id="flow-1">flow</h3>
<p>→ disk를 초기화 한다.</p>
<p>→ disk의 정보를 가져와서</p>
<p>→ disk size를 계산한다.</p>
<h3 id="구현-2">구현</h3>
<pre><code class="language-c">/* 익명 페이지를 위한 데이터를 초기화합니다 */
void vm_anon_init(void)
{

    // TODO: swap 구현 시 아래 내용을 추가해야 함. 사유는 그때 가서 이해하기.
    /* swap_disk를 설정하세요. */
    disk_init();
    swap_disk = disk_get(1, 1); // NOTE: disk_get 인자값 적절성 검토 완료. 
    size_t swap_size = disk_size(swap_disk); // SECTORS_PER_PAGE;
    // swap_table = bitmap_create(swap_size);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)요즘 하루종일 디버깅만 하다 끝나는 것 같다~]]></title>
            <link>https://velog.io/@fishing_bear99/TIL%EC%9A%94%EC%A6%98-%ED%95%98%EB%A3%A8%EC%A2%85%EC%9D%BC-%EB%94%94%EB%B2%84%EA%B9%85%EB%A7%8C-%ED%95%98%EB%8B%A4-%EB%81%9D%EB%82%98%EB%8A%94-%EA%B2%83-%EA%B0%99%EB%8B%A4</link>
            <guid>https://velog.io/@fishing_bear99/TIL%EC%9A%94%EC%A6%98-%ED%95%98%EB%A3%A8%EC%A2%85%EC%9D%BC-%EB%94%94%EB%B2%84%EA%B9%85%EB%A7%8C-%ED%95%98%EB%8B%A4-%EB%81%9D%EB%82%98%EB%8A%94-%EA%B2%83-%EA%B0%99%EB%8B%A4</guid>
            <pubDate>Mon, 09 Jun 2025 15:08:47 GMT</pubDate>
            <description><![CDATA[<h1 id="mmap-read-test">mmap-read test</h1>
<pre><code class="language-c">void
test_main (void)
{
  char *actual = (char *) 0x10000000;
  int handle;
  void *map;
  size_t i;

  CHECK ((handle = open (&quot;sample.txt&quot;)) &gt; 1, &quot;open \&quot;sample.txt\&quot;&quot;);
  CHECK ((map = mmap (actual, 4096, 0, handle, 0)) != MAP_FAILED, &quot;mmap \&quot;sample.txt\&quot;&quot;);

  /* Check that data is correct. */
  if (memcmp (actual, sample, strlen (sample)))
    fail (&quot;read of mmap&#39;d file reported bad data&quot;);

  /* Verify that data is followed by zeros. */
  for (i = strlen (sample); i &lt; 4096; i++)
    if (actual[i] != 0)
      fail (&quot;byte %zu of mmap&#39;d region has value %02hhx (should be 0)&quot;,
            i, actual[i]);

  munmap (map);
  close (handle);
}</code></pre>
<pre><code class="language-c">✅ 1. 파일 내용이 메모리에 제대로 매핑되었는가?

if (memcmp (actual, sample, strlen (sample)))
  fail (&quot;read of mmap&#39;d file reported bad data&quot;);

    •    sample.txt의 앞부분이 메모리 주소 0x10000000에 정확히 복사되었는지 확인합니다.
    •    memcmp()을 통해 매핑된 메모리 영역(actual)과 사전에 정의된 참조 데이터 sample을 비교합니다.
    •    파일에서 읽은 내용이 기대한 문자열과 다르면 실패 처리합니다.

⸻

✅ 2. 매핑된 페이지의 나머지 영역이 0으로 초기화되었는가?

for (i = strlen (sample); i &lt; 4096; i++)
  if (actual[i] != 0)
    fail (&quot;byte %zu of mmap&#39;d region has value %02hhx (should be 0)&quot;, i, actual[i]);

    •    strlen(sample) 이후의 메모리 공간이 전부 0으로 채워져 있어야 합니다.
    •    이는 파일의 크기가 4096바이트보다 작을 경우, 남은 부분을 0으로 채워야 한다는 mmap의 요구사항을 테스트하는 것입니다.
    •    예를 들어, 파일이 100바이트라면, 나머지 3996바이트는 0이 되어야 합니다.</code></pre>
<h3 id="디버깅">디버깅</h3>
<pre><code class="language-c">nterrupt 0x0d (#GP General Protection Exception) at rip=8004221fe0
 cr2=0000000010000000 error=               0
rax ccccccccccccccb4 rbx 00008004247f1800 rcx 0000000000000011 rdx 0000008004244000
rsp 0000008004244e90 rbp 0000008004244ee0 rsi 0000000000000251 rdi 0000008004244070
rip 0000008004221fe0 r8 0000008004244d18  r9 000000800421b557 r10 0000000000000000
r11 0000000000000216 r12 000000800421d777 r13 0000010424400000 r14 0000800424400000
r15 0000800422bdd800 rflags 00000286
es: 001b ds: 001b cs: 0008 ss: 0010</code></pre>
<p>rax의 값을 보면 이상한 쓰레기 값을 반환하고 있다. 문제 발생 근원지를 찾기 위해 로그를 찍어봤더니</p>
<pre><code class="language-c">// spt remove page start
                spt_remove_page(spt, page);
                dprintfg(&quot;[do_munmap] spt remove() 실행 후 다음 line으로 넘어가나요??\n&quot;);
                unmap_addr += PGSIZE;
                length -= PGSIZE; </code></pre>
<p>do_munmap()의 spt page를 해제해주는 부분에서 문제가 발생했다.</p>
<pre><code class="language-c">struct hash_elem *
hash_delete (struct hash *h, struct hash_elem *e) {
    dprintfg(&quot;[hash_delete] hash func addr: %p, less func addr: %p\n&quot;, h-&gt;hash, h-&gt;less);
    struct hash_elem *found = find_elem (h, find_bucket (h, e), e);</code></pre>
<p>find_elem에서 hash elem의 주소가 Null값이 반환되었기 때문이라고 예상하고, page→hash_elem 주소가 깨질 수 있는 경우를 생각해봤다.</p>
<pre><code class="language-c">if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_file_backed, aux))</code></pre>
<p>do_mmap()에서 file backed page를 lazy load방식으로 할당해주는 부분이 있는데 흐름을 타고 들어가면</p>
<pre><code class="language-c">initializer = file_backed_initializer;</code></pre>
<p>file_backed initializer()를 호출하는 걸 볼 수 있다. 왜 file backed 구조체 멤버 값들을 초기화 해줘야한다는 생각을 하지 못했을까? 그런데 아직도 do_mmap()에서 lazy load file backed()함수를 호출하면서 lazy load 구조체를 초기화 해주기 때문에  file_backed_initializer()에서 초기화 해주는 것이 의미가 있나 싶다.</p>
<p>여전히 문제가 해결되지 않았고, 원인도 모르겠어서 gpt의 도움을 받았다. </p>
<pre><code class="language-c">/* Do the munmap */
void do_munmap(void *addr)
{
    // 프로세스가 종료되면 매핑 자동해제. munmap할 필요는 없음.
    // 매핑 해제 시 수정된 페이지는 파일에 반영
    // 수정되지 않은 페이지는 반영할 필요 없음
    // munmap 하고 spt제거?
    // 파일 close, remove는 매핑에 반영되지 않음( 프레임은 가마니)
    // 한 파일을 여러번 mmap하는 경우에는 file_reopen을 통해 독립된 참조. -&gt; 하나의 file이 여러번 mmap 되어 있는 걸 어떻게 알지?
    struct thread *curr = thread_current();
    struct supplemental_page_table *spt = &amp;curr-&gt;spt; // 현재 스레드의 spt 정보 참조

    for(struct list_elem *e = list_begin(&amp;curr-&gt;mmap_list); e != list_end(&amp;curr-&gt;mmap_list); e = list_next(e)){
        struct mmap_file *mmap_file = list_entry(e, struct mmap_file, elem);
        void *unmap_addr = mmap_file-&gt;start_addr;
        size_t length = mmap_file-&gt;start_length;

        if(mmap_file-&gt;start_addr == addr){
            dprintfg(&quot;[do_mmap] start_addr : %p, addr : %p\n&quot;, mmap_file-&gt;start_addr, addr);

            while(length &gt; 0){
                dprintfg(&quot;[do_mmap] while() 안의 length : %d\n&quot;, length);
                struct page *page = spt_find_page(spt, unmap_addr); // spt정보를 가져온다.
                dprintfg(&quot;[do_mmap] spt find page : %p\n&quot;, page);

                // spt remove page debug log
                if (page == NULL) {
                    dprintfg(&quot;[do_munmap] spt_find_page() 결과 NULL! 잘못된 주소일 가능성\n&quot;);
                    break;
                }
                dprintfg(&quot;[debug] page: %p, &amp;page-&gt;hash_elem: %p, page-&gt;va: %p\n&quot;, page, &amp;page-&gt;hash_elem, page-&gt;va);

                dprintfg(&quot;[debug] hash_elem addr: %p, 예상 page addr: %p\n&quot;, &amp;page-&gt;hash_elem, hash_entry(&amp;page-&gt;hash_elem, struct page, hash_elem));

                // spt remove page start
                spt_remove_page(spt, page);
                dprintfg(&quot;[do_munmap] spt remove() 실행 후 다음 line으로 넘어가나요??\n&quot;);
                unmap_addr += PGSIZE;
                length -= PGSIZE; 
            }

            list_remove(&amp;mmap_file-&gt;elem);
            free(mmap_file);
        }
    }
}</code></pre>
<p>위 코드의 for()문에서 문제가 발생했는데 문제는 </p>
<pre><code class="language-c">free(mmap_file);</code></pre>
<p>메모리 누수를 막기 위해서 mmap list의 mmap_file을 free()해주고 나서 다시 loop()을 도는데 </p>
<pre><code class="language-c">e != list_end(&amp;curr-&gt;mmap_list); e = list_next(e)</code></pre>
<p>mmap list의 end인지 check하고 아니라면 list next()로 넘어가는 흐름인데 next()로 포인터가 이동하기도 전에 mmap_file을 free()해준 것이다. 따라서 이미 free()된 주소를 다시 참조하면서 문제가 발생한 것이다.</p>
<pre><code class="language-c">/* Do the munmap */
void do_munmap(void *addr)
{
    // 프로세스가 종료되면 매핑 자동해제. munmap할 필요는 없음.
    // 매핑 해제 시 수정된 페이지는 파일에 반영
    // 수정되지 않은 페이지는 반영할 필요 없음
    // munmap 하고 spt제거?
    // 파일 close, remove는 매핑에 반영되지 않음( 프레임은 가마니)
    // 한 파일을 여러번 mmap하는 경우에는 file_reopen을 통해 독립된 참조. -&gt; 하나의 file이 여러번 mmap 되어 있는 걸 어떻게 알지?
    struct thread *curr = thread_current();
    struct supplemental_page_table *spt = &amp;curr-&gt;spt; // 현재 스레드의 spt 정보 참조

    struct list_elem *e = list_begin(&amp;curr-&gt;mmap_list);
    while (e != list_end(&amp;curr-&gt;mmap_list)) {
        struct list_elem *next = list_next(e);
        struct mmap_file *mmap_file = list_entry(e, struct mmap_file, elem);
        void *unmap_addr = mmap_file-&gt;start_addr;
        size_t length = mmap_file-&gt;start_length;

        if(mmap_file-&gt;start_addr == addr){
            dprintfg(&quot;[do_munmap] start_addr : %p, addr : %p\n&quot;, mmap_file-&gt;start_addr, addr);

            while(length &gt; 0){
                dprintfg(&quot;[do_mmap] while() 안의 length : %d\n&quot;, length);
                struct page *page = spt_find_page(spt, unmap_addr); // spt정보를 가져온다.
                dprintfg(&quot;[do_munmap] spt find page : %p\n&quot;, page);

                // spt remove page debug log
                if (page == NULL) {
                    dprintfg(&quot;[do_munmap] spt_find_page() 결과 NULL! 잘못된 주소일 가능성\n&quot;);
                    break;
                }
                dprintfg(&quot;[do_munmap] page: %p, &amp;page-&gt;hash_elem: %p, page-&gt;va: %p\n&quot;, page, &amp;page-&gt;hash_elem, page-&gt;va);

                dprintfg(&quot;[do_munmap] hash_elem addr: %p, 예상 page addr: %p\n&quot;, &amp;page-&gt;hash_elem, hash_entry(&amp;page-&gt;hash_elem, struct page, hash_elem));

                // spt remove page start
                spt_remove_page(spt, page);
                dprintfg(&quot;[do_munmap] spt remove() 실행 후 다음 line으로 넘어가나요??\n&quot;);
                unmap_addr += PGSIZE;
                length -= PGSIZE; 
            }

            list_remove(&amp;mmap_file-&gt;elem);
            dprintfg(&quot;[do_munmap] list remove(mmap file -&gt; elem) success\n&quot;);
            free(mmap_file);
            dprintfg(&quot;[do_munmap] free(mmap file) success\n&quot;);
        }
        e = next;
    }
}</code></pre>
<p>위와 같이 수정하니 null값을 참조하는 문제는 해결되었다. 위 코드는 next() 주소를 미리 저장해놨다가 e != list_end(&amp;curr-&gt;mmap_list)조건을 검사하기 전에 할당해주는 방식이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)삽질...도 도움이 되겠지..?]]></title>
            <link>https://velog.io/@fishing_bear99/TIL%EC%82%BD%EC%A7%88...%EB%8F%84-%EB%8F%84%EC%9B%80%EC%9D%B4-%EB%90%98%EA%B2%A0%EC%A7%80</link>
            <guid>https://velog.io/@fishing_bear99/TIL%EC%82%BD%EC%A7%88...%EB%8F%84-%EB%8F%84%EC%9B%80%EC%9D%B4-%EB%90%98%EA%B2%A0%EC%A7%80</guid>
            <pubDate>Sun, 08 Jun 2025 14:20:23 GMT</pubDate>
            <description><![CDATA[<h1 id="mmap-munmap-test-수정">mmap-munmap test 수정</h1>
<h3 id="들어가기-전에">들어가기 전에</h3>
<p>page 하나는 4KB로 공간을 차지한다. mmap-munmap test case를 보면 file length(8192)가 인자값으로 들어간다. 딱 page 2개의 크기이다. pintos의 요구사항을 보면 lazy load 방식을 사용하라고 나와있어서 요구사항대로 구현을 하게 되면 한 번에 8192bytes를 load하지 않는다. 필요한 data만 load하기 때문에 읽을 데이터를 따로 변수에 관리해야 한다.</p>
<p>page의 전체 크기는 4KB이다. 만약 읽어야 하는 데이터의 size가 page보다 작다면 남은 부분을 어떻게 해야할까? 그렇다. 0으로 채워줘야 한다. 이렇게 하는 이유가 있는데 그건 잘 모르겠고, 0으로 기존의 데이터를 덮어씌워주지 않으면 여전히 데이터가 남아있게 되니 문제가 생기는 것 같다.</p>
<p>그러면 구체적으로 어떻게 읽어야 할까? 처음 addr은 맨 하위 주소에 존재한다. 따라서 높은 주소로 8byte씩 올라가면서 데이터를 읽을 것이다. 읽어야 되는 데이터를 모두 읽고 나서 읽어야 되는 데이터가 더 존재 한다면(인자로 받게될 Length로 확인할 것이다) loop()를 통해 다시 page를 읽을 것이다. 그리고 page에 공간이 남게 되면 read bytes한 위치에서부터 memset()을 이용해 zero로 채워준다.</p>
<p>사실 이 함수를 구현하기 전에 page단위로만 addr을 이동시키면서 데이터를 읽었다. 그렇다보니 file의 데이터를 제대로 읽지 못하는 오류가 발생했고, 디버깅 하는데만 하루를 썼다.</p>
<h3 id="구현">구현</h3>
<h4 id="do_mmap">do_mmap()</h4>
<pre><code>void *
do_mmap(void *addr, size_t length, int writable, struct file *file, off_t offset)
{
    // 1. addr로부터 페이지 생성
    // 1-1. lazy_load, aux 초기화해서 넘겨주기.
    // 1-2. 복사(length, offset, 등등) 이거 바로 해줘요? 그럼 또 lazy 아니잖아. -&gt; 이 내용이 lazy_load에서 타입 체크후에 복사 바로 하면 되지 않겠나.
    // 1-3. 나머자 내용은 0으로 채워야 함.

    void *start_addr = addr;
    size_t start_length = length;

    while (length &gt; 0)
    {
        size_t page_read_bytes = (length &lt; PGSIZE) ? length : PGSIZE;
        size_t file_left = file_length(file) - offset;

        // read_bytes, zeor_bytes 초기화
        size_t read_bytes = (file_left &lt; page_read_bytes) ? file_left : page_read_bytes;
        off_t zero_bytes = PGSIZE - read_bytes;

        // aux 초기화
        struct lazy_aux_file_backed *aux = malloc(sizeof(struct lazy_aux_file_backed));
        aux-&gt;file = file_reopen(file);
        aux-&gt;read_bytes = read_bytes;
        aux-&gt;zero_bytes = zero_bytes;
        aux-&gt;offset = offset;

        dprintfg(&quot;[do_mmap] vm_alloc_page_with_initializer()실행하기 전. 만약 이 다음에 exit()된다면 이 함수에서 문제가 발생한 것임\n&quot;);
        if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_file_backed, aux))
        {
            // page clean??
            free(aux);
            while(start_addr &lt; addr){
                dprintfg(&quot;[do_mmap] vm_dealloc_page() 실행 전\n&quot;);
                vm_dealloc_page(spt_find_page(&amp;thread_current()-&gt;spt, addr));
                dprintfg(&quot;[do_mmap] vm_dealloc_page() 실행 후\n&quot;);
                addr -= PGSIZE;                
            }
            // file_close(file);
            return NULL;
        }

        // 쓴 만큼 offset, length 업데이트.
        length -= PGSIZE;
        offset += PGSIZE;
        addr += PGSIZE;
        dprintfg(&quot;[do_mmap] length: %d, offset: %d, addr: %p\n&quot;, length, offset, addr);
    }

    struct mmap_file *mmap_file = malloc(sizeof(struct mmap_file));
    mmap_file-&gt;start_addr = start_addr;
    mmap_file-&gt;start_length = start_length;
    mmap_file-&gt;file = file_reopen(file);
    dprintfg(&quot;[do_mmap] list_push_back() 이전\n&quot;);
    list_push_back(&amp;thread_current()-&gt;mmap_list, &amp;mmap_file-&gt;elem);
    dprintfg(&quot;[do_mmap] list_push_back() 이후\n&quot;);

    return start_addr;
}</code></pre><h4 id="lazy_load_file_backed">lazy_load_file_backed()</h4>
<pre><code class="language-c">bool lazy_load_file_backed(struct page *page, void *aux)
{
    /* 파일에서 페이지 컨텐츠를 읽어옵니다. */
    /* 이 함수는 주소 VA에서 첫 페이지 폴트가 발생했을 때 호출됩니다. */
    /* 이 함수를 호출할 때 VA를 사용할 수 있습니다. */
    dprintfg(&quot;[lazy_load_file_backed] routine start. page: %p, page-&gt;va: %p\n&quot;, page, page-&gt;va);

    if (page-&gt;frame == NULL || page-&gt;frame-&gt;kva == NULL){
        PANIC(&quot;lazy_load_file_backed이 allocate 되어 있지 않습니다!!&quot;);
    }

    /* Load this page. */
    // aux 멤버 정의 필요.
    // file page 업데이트
    struct lazy_aux_file_backed *lazy_aux = (struct lazy_aux_file_backed *)aux;
    // struct file_page *file_page = &amp;page-&gt;file; //file_backed에 page 정보를 저장한다
    struct file *file = lazy_aux-&gt;file;
    size_t read_bytes = lazy_aux-&gt;read_bytes;
    size_t zero_bytes = lazy_aux-&gt;zero_bytes;
    off_t offset = lazy_aux-&gt;offset;

    dprintfg(&quot;[lazy_load_file_backed] reading file\n&quot;);
    if (file_read_at(file, page-&gt;frame-&gt;kva, read_bytes, offset) != read_bytes)
    {
        free(lazy_aux);
        return false;
    }
    memset(page-&gt;frame-&gt;kva + read_bytes, 0, zero_bytes); // zero bytes 복사.

    free(lazy_aux);
    return true;
}</code></pre>
<h2 id="test-result">test result</h2>
<h3 id="fail-22">fail 22</h3>
<pre><code class="language-c">Here are the failed tests from the list:
    1.    tests/vm/pt-write-code
    2.    tests/vm/pt-write-code2
    3.    tests/vm/pt-grow-stk-sc
    4.    tests/vm/page-merge-stk
    5.    tests/vm/page-merge-mm
    6.    tests/vm/mmap-read
    7.    tests/vm/mmap-close
    8.    tests/vm/mmap-write
    9.    tests/vm/mmap-ro
    10.    tests/vm/mmap-exit
    11.    tests/vm/mmap-bad-fd // 해결
    12.    tests/vm/mmap-clean
    13.    tests/vm/mmap-over-stk
    14.    tests/vm/mmap-remove
    15.    tests/vm/mmap-off
    16.    tests/vm/mmap-bad-off
    17.    tests/vm/mmap-kernel
    18.    tests/vm/swap-file
    19.    tests/vm/swap-anon
    20.    tests/vm/swap-iter
    21.    tests/vm/swap-fork
    22.    tests/vm/cow/cow-simple</code></pre>
<h3 id="mmap-read-test-분석">mmap-read test 분석</h3>
<pre><code class="language-c">void
test_main (void)
{
  char *actual = (char *) 0x10000000;
  int handle;
  void *map;
  size_t i;

  CHECK ((handle = open (&quot;sample.txt&quot;)) &gt; 1, &quot;open \&quot;sample.txt\&quot;&quot;);
  CHECK ((map = mmap (actual, 4096, 0, handle, 0)) != MAP_FAILED, &quot;mmap \&quot;sample.txt\&quot;&quot;);

  /* Check that data is correct. */
  if (memcmp (actual, sample, strlen (sample)))
    fail (&quot;read of mmap&#39;d file reported bad data&quot;);

  /* Verify that data is followed by zeros. */
  for (i = strlen (sample); i &lt; 4096; i++)
    if (actual[i] != 0)
      fail (&quot;byte %zu of mmap&#39;d region has value %02hhx (should be 0)&quot;,
            i, actual[i]);

  munmap (map);
  close (handle);
}</code></pre>
<pre><code class="language-c">✅ 1. 파일 내용이 메모리에 제대로 매핑되었는가?

if (memcmp (actual, sample, strlen (sample)))
  fail (&quot;read of mmap&#39;d file reported bad data&quot;);

    •    sample.txt의 앞부분이 메모리 주소 0x10000000에 정확히 복사되었는지 확인합니다.
    •    memcmp()을 통해 매핑된 메모리 영역(actual)과 사전에 정의된 참조 데이터 sample을 비교합니다.
    •    파일에서 읽은 내용이 기대한 문자열과 다르면 실패 처리합니다.

⸻

✅ 2. 매핑된 페이지의 나머지 영역이 0으로 초기화되었는가?

for (i = strlen (sample); i &lt; 4096; i++)
  if (actual[i] != 0)
    fail (&quot;byte %zu of mmap&#39;d region has value %02hhx (should be 0)&quot;, i, actual[i]);

    •    strlen(sample) 이후의 메모리 공간이 전부 0으로 채워져 있어야 합니다.
    •    이는 파일의 크기가 4096바이트보다 작을 경우, 남은 부분을 0으로 채워야 한다는 mmap의 요구사항을 테스트하는 것입니다.
    •    예를 들어, 파일이 100바이트라면, 나머지 3996바이트는 0이 되어야 합니다.</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TIL)munmap() 디버깅에 집중하기]]></title>
            <link>https://velog.io/@fishing_bear99/TILmunmap-%EB%94%94%EB%B2%84%EA%B9%85%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fishing_bear99/TILmunmap-%EB%94%94%EB%B2%84%EA%B9%85%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 07 Jun 2025 14:06:54 GMT</pubDate>
            <description><![CDATA[<h3 id="void-munmapvoid-addr"><code>void munmap(void *addr);</code></h3>
<h3 id="목표">목표</h3>
<p>매핑된 addr을 받아서 매핑을 해제한다.</p>
<h3 id="구상">구상</h3>
<ul>
<li>매핑이 file backed page인지 anon page인지 check</li>
<li>fault stack growth case인지 &amp;&amp; anon + lazy load인지 check</li>
</ul>
<h3 id="flow">flow</h3>
<p>mmap()한 걸 해제할 때</p>
<p>→ munmap(void *addr)</p>
<h3 id="참고">참고</h3>
<p>중요: vm_alloc_page_with_initializer에서 말하는 initializer는 anon_initializer, file_backed_initializer같은 페이지 이니셜라이저고, lazy_load_segment 같은 vm_initializer와는 다르다.Page initialize 과정은 “지정“과 “실행“으로 나눠 볼 수 있다.</p>
<ol>
<li>지정: vm_alloc_page_with_initializer<ol>
<li>타입 지정</li>
<li>지정된 타입으로 page initializer 지정</li>
<li>만약 주어졌다면 vm_initializer 지정</li>
</ol>
</li>
<li>수행: 첫 페이지 폴트 발생 시<ol>
<li>uninit_initialize가 수행되며 아래 루틴들 수행</li>
<li>page initializer 수행 → page operation 지정</li>
<li>vm_initializer 존재한다면 수행(lazy_load_segment 등)</li>
</ol>
</li>
</ol>
<hr>
<h3 id="do_munmap"><strong><code>do_munmap()</code></strong></h3>
<h3 id="목표-1">목표</h3>
<p>전달받은 addr을 참조하여 실제 메모리 매핑 해제 작업을 수행한다.</p>
<h3 id="구상-1">구상</h3>
<ul>
<li>모든 page를 정리한다<ul>
<li>mmap list 전체를 순회하면서 page 해제를 수행한다.<ul>
<li>for()에서 mm list의 mm file→list elem로 순회하면서 file별로 addr값을 찾는다.</li>
<li>addr위치부터 file length까지 offset만큼 높은 주소로 이동하면서</li>
<li>spt find page()를 이용해서 addr과 매핑된 page를 찾는다.</li>
<li>file backed destroy()를 이용해서 file backed과의 매핑을 해제한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="flow-1">flow</h3>
<p>→ vlidation()</p>
<p>→ file backed destroy()
-&gt; ...</p>
<h3 id="debug">debug</h3>
<pre><code class="language-c">void
test_main (void)
{
  int handle;
  void *map;

  CHECK ((handle = open (&quot;sample.txt&quot;)) &gt; 1, &quot;open \&quot;sample.txt\&quot;&quot;);
  CHECK ((map = mmap (ACTUAL, 0x2000, 0, handle, 0)) != MAP_FAILED, &quot;mmap \&quot;sample.txt\&quot;&quot;);
  msg (&quot;memory is readable %d&quot;, *(int *) ACTUAL);
  msg (&quot;memory is readable %d&quot;, *(int *) ACTUAL + 0x1000);

  munmap (map);

  fail (&quot;unmapped memory is readable (%d)&quot;, *(int *) (ACTUAL + 0x1000));
  fail (&quot;unmapped memory is readable (%d)&quot;, *(int *) (ACTUAL));
}</code></pre>
<p>설명 : 이 테스트는 <strong>mmap()에 의해 매핑된 메모리가 실제로 읽을 수 있는지</strong>와, <strong>munmap() 이후에 해당 메모리에 접근하면 실패(즉, 페이지 폴트)가 발생해야 하는지</strong>를 검증하는 테스트.</p>
<pre><code class="language-c">(mmap-unmap) begin
(mmap-unmap) open &quot;sample.txt&quot;
(mmap-unmap) mmap &quot;sample.txt&quot;
Kernel PANIC at ../../lib/kernel/list.c:158 in list_insert(): assertion `is_interior (before) || is_tail (before)&#39; failed.

0x000000800421802e: debug_panic (lib/kernel/debug.c:32)
0x0000008004218577: list_insert (lib/kernel/list.c:159)
0x000000800421881f: list_push_back (lib/kernel/list.c:204)
0x0000008004221b43: do_mmap (vm/file.c:127)
0x000000800421d55d: mmap (userprog/syscall.c:286)
0x000000800421d8c5: syscall_handler (userprog/syscall.c:376)
0x000000800421ce7c: no_sti (userprog/syscall-entry.o:?)</code></pre>
<pre><code class="language-c">mmap_file: 0x8004248138
mmap_file-&gt;elem.prev: 0xcccccccccccccccc
mmap_file-&gt;elem.next: 0xcccccccccccccccc</code></pre>
<p>before가 잘못된 주소를 참조하고 있었음.</p>
<pre><code class="language-c">#ifdef VM
    list_init(&amp;t-&gt;mmap_list); // feat: do_munmap
#endif</code></pre>
<p>해결</p>
<pre><code class="language-c">mmap_file: 0x8004248138
mmap_file-&gt;elem.prev: 0xcccccccccccccccc
mmap_file-&gt;elem.next: 0xcccccccccccccccc
[do_mmap] list_push_back() 이후
mmap-unmap: exit(-1)
mmap-unmap: exit(-1)</code></pre>
<p>mmap()으로 매핑된 file을 읽지 못하고 있음.</p>
<pre><code class="language-c">000000800422199b: file_backed_destroy (vm/file.c:83)
0x0000008004221178: vm_dealloc_page (vm/vm.c:367)
0x00000080042215fb: spt_destructor (vm/vm.c:505)
0x000000800421a613: hash_clear (lib/kernel/hash.c:59)
0x00000080042215cd: supplemental_page_table_kill (vm/vm.c:499)
0x000000800421c107: process_cleanup (userprog/process.c:458)
0x000000800421c0a3: process_exit (userprog/process.c:441)
0x0000008004207240: thread_exit (threads/thread.c:328)
0x000000800421d009: write (userprog/syscall.c:98)
0x000000800421ce1e: page_fault (userprog/exception.c:152)</code></pre>
<p>문제 정의</p>
<ol>
<li>mmap()에서 매핑해준 file이 적절하게 해제되지 않아서?</li>
<li>file backed destroy()가 중복으로 호출되었기 때문에? 이때는 munmap()이 호출되지 않아서 이 가능성을 배제했었음. process_exit()에서 밖에 호출되지 않는데 중복 호출될리 없잖아..
또한 reopen()에서 같은 file을 여러 page에서 매핑하고 있기 때문에 앞에서 삭제된 경우가 있을 수도 있지 않나? 에 대한 부분도 생각했었음.</li>
<li>spt remove page()에서 이미 제거된 page를 다시 참조하는 경우 - 뒤에서 깨달았지만 이게 정답이였다…</li>
</ol>
<pre><code class="language-c">mmap-unmap: exit(-1)
mmap-unmap: exit(-1)</code></pre>
<p>위에서 file backed destroy()가 중복되는 문제 해결</p>
<pre><code class="language-c">if(spt_find_page(spt, page-&gt;va) != NULL &amp;&amp; page-&gt;operations != NULL &amp;&amp; page-&gt;frame != NULL){
        if (pml4_is_dirty(pml4, page-&gt;va))
        {        
            file_write_at(file_page-&gt;file, page-&gt;va, file_page-&gt;size, file_page-&gt;file_ofs); // Writes SIZE bytes만큼 쓴다.
        }
        file_close(file_page-&gt;file);
        dprintfg(&quot;[file_backed_destroy] spt remove page()할 때 문제가 발생한 것 같아\n&quot;);
        // spt_remove_page(spt, page); // spt 제거 -&gt; spt에서 지우면 pml4에서 계속 업데이트가 된다?
    }    
}</code></pre>
<pre><code class="language-c">(mmap-unmap) begin
(mmap-unmap) open &quot;sample.txt&quot;
(mmap-unmap) mmap &quot;sample.txt&quot;
[do_mmap] length: 4096, offset: 4096, addr: 0x10001000
[do_mmap] length: 0, offset: 8192, addr: 0x10002000
[do_mmap] list_push_back() 이전
mmap_file: 0x8004248138
mmap_file-&gt;elem.prev: 0xcccccccccccccccc
mmap_file-&gt;elem.next: 0xcccccccccccccccc
mmap_file-&gt;elem.prev: 0x8004243070
mmap_file-&gt;elem.next: 0x8004243080
[do_mmap] list_push_back() 이후
[lazy_load_file_backed] routine start. page: 0x8004246498, page-&gt;va: 0x10000000
[lazy_load_file_backed] reading file
mmap-unmap: exit(-1)
[file backed destroy] dirty bit를 확인하기 전
Execution of &#39;mmap-unmap&#39; complete.</code></pre>
<p>한참 찾았는데 file backed destroy()에서 spt_remove_page()을 호출하게 되면 spt_remove_page()에서 다시 destroy()를 호출하게 되면서 이미 해제된 메모리를 다시 접근하는 문제가 발생한다.</p>
<h3 id="구현">구현</h3>
<pre><code class="language-c">static void
file_backed_destroy(struct page *page)
{
    //     - 파일 기반 페이지를 제거하는 함수
    // - 페이지가 **dirty 상태**면, 변경 사항을 파일에 기록(write-back)해야 함
    // - 여기서 `page` 구조체 자체를 `free`할 필요는 없음 → 호출자가 해제함
    //    - 호출자 = spt_remove_page → vm_dealloc_page
    //       - 여기서 destroy 호출 후 구조체 free까지 해줌
    // - destroy에서 구현할 로직?
    //    - 매핑된 프레임 해제?
    //    - spt_remove_page에서 구현하는것이 좋을듯하다
    //   - write-back 구현
    struct file_page *file_page = &amp;page-&gt;file; 
    struct pml4 *pml4 = thread_current()-&gt;pml4;
    struct supplemental_page_table *spt = &amp;thread_current()-&gt;spt;

    dprintfg(&quot;[file backed destroy] dirty bit를 확인하기 전\n&quot;);

    if(spt_find_page(spt, page-&gt;va) != NULL &amp;&amp; page-&gt;operations != NULL &amp;&amp; page-&gt;frame != NULL){
        if (pml4_is_dirty(pml4, page-&gt;va))
        {        
            file_write_at(file_page-&gt;file, page-&gt;va, file_page-&gt;size, file_page-&gt;file_ofs); // Writes SIZE bytes만큼 쓴다.
        }
        file_close(file_page-&gt;file);
        dprintfg(&quot;[file_backed_destroy] spt remove page()할 때 문제가 발생한 것 같아\n&quot;);
        // spt_remove_page(spt, page); // spt 제거 -&gt; spt에서 지우면 pml4에서 계속 업데이트가 된다?
    }    
}</code></pre>
<p>spt_remove_page()은 do_munmap()에서 호출해야 한다.</p>
<hr>
]]></description>
        </item>
    </channel>
</rss>