C언어로 이해하는 리눅스 커널의 시스템 스케줄링

리눅스 커널은 현대 운영 체제의 핵심 구성 요소로, 시스템의 자원을 효율적으로 관리하고 사용자 요청을 처리합니다. 이 중 시스템 스케줄링은 CPU와 같은 제한된 자원을 여러 프로세스와 스레드가 효과적으로 공유할 수 있도록 하는 중요한 메커니즘입니다. 본 기사에서는 C언어를 통해 리눅스 커널의 시스템 스케줄링 구조와 작동 원리를 분석하며, 실제 소스 코드와 예제를 통해 이해를 심화시켜 나갑니다. 이를 통해 운영 체제의 동작 방식을 이해하고, 시스템 성능을 최적화할 수 있는 기반을 제공합니다.

목차

시스템 스케줄링의 개요


시스템 스케줄링은 운영 체제에서 CPU와 같은 핵심 자원을 여러 프로세스와 스레드가 공유하도록 관리하는 기능입니다.

스케줄링의 역할


스케줄링은 다음과 같은 역할을 합니다:

  • 공정성 보장: 모든 프로세스가 공평하게 CPU 시간을 얻도록 보장합니다.
  • 성능 최적화: CPU와 메모리 자원의 활용도를 극대화합니다.
  • 응답성 개선: 대화형 작업의 응답성을 향상시킵니다.
  • 스루풋 증가: 단위 시간당 처리되는 작업의 수를 최대화합니다.

스케줄링의 필요성


운영 체제는 다수의 프로세스와 스레드가 동시에 실행될 때 CPU 자원을 효과적으로 분배해야 합니다. 이를 위해 스케줄링은 프로세스의 우선순위를 결정하고, 어떤 프로세스가 언제 실행될지 제어합니다.

스케줄링의 기본 구성 요소

  • 큐(queue): 준비 상태의 프로세스가 저장되는 대기열입니다.
  • 우선순위(priority): 프로세스의 중요도를 나타냅니다.
  • 타임 슬라이스(time slice): 프로세스에 할당되는 CPU 실행 시간의 단위입니다.

시스템 스케줄링은 운영 체제의 안정성과 성능을 유지하는 데 중요한 역할을 하며, 이를 이해하면 리눅스 커널의 작동 원리를 더 잘 파악할 수 있습니다.

리눅스 커널의 스케줄링 개념


리눅스 커널의 스케줄링은 다양한 작업의 효율적 처리를 위해 설계된 동적이고 확장 가능한 메커니즘입니다. 리눅스는 주로 Completely Fair Scheduler (CFS)를 기본 스케줄러로 사용하며, 이는 공정성과 성능의 균형을 목표로 합니다.

리눅스 스케줄링의 주요 원칙

  • 공정성: 모든 프로세스가 CPU 자원을 균등하게 할당받도록 보장합니다.
  • 효율성: 자원의 활용도를 극대화하고, 대기 시간을 최소화합니다.
  • 적응성: 다양한 하드웨어와 워크로드에 최적화될 수 있도록 유연한 구조를 가집니다.

CFS(Completely Fair Scheduler)의 기본 개념


CFS는 각 프로세스에 대해 “가상 런타임”을 계산하고, 가장 낮은 가상 런타임을 가진 프로세스를 실행합니다.

  • 가상 런타임: 실제 실행 시간에 우선순위를 반영하여 계산된 값입니다.
  • 레드블랙 트리: 프로세스 관리에 사용되는 자료 구조로, 효율적인 검색과 업데이트를 지원합니다.

스케줄링 클래스


리눅스 커널은 다양한 스케줄링 정책을 지원하기 위해 여러 스케줄링 클래스를 정의합니다.

  • SCHED_NORMAL: 일반 사용자 프로세스에 사용됩니다.
  • SCHED_FIFO와 SCHED_RR: 실시간 프로세스를 위한 선점형 스케줄링 정책입니다.
  • SCHED_IDLE: 최소한의 CPU 자원을 사용하는 작업에 사용됩니다.

리눅스 커널의 스케줄링 구조는 다양한 요구 사항에 대응할 수 있도록 설계되었으며, 이를 이해하면 운영 체제의 핵심 동작 원리를 더 깊이 알 수 있습니다.

프로세스와 스레드의 차이


시스템 스케줄링에서는 프로세스와 스레드의 개념을 이해하는 것이 중요합니다. 이 두 요소는 CPU 자원을 사용하는 단위이지만, 작동 방식과 관리 측면에서 차이가 있습니다.

프로세스란 무엇인가?


프로세스는 실행 중인 프로그램의 인스턴스를 말하며, 독립적인 메모리 공간과 시스템 자원을 갖습니다.

  • 메모리 독립성: 각 프로세스는 독립적인 주소 공간을 가지며, 다른 프로세스와 격리됩니다.
  • 스케줄링 단위: 운영 체제는 프로세스를 기본 단위로 스케줄링합니다.
  • 비용: 새로운 프로세스를 생성하는 데 상대적으로 높은 오버헤드가 발생합니다.

스레드란 무엇인가?


스레드는 프로세스 내에서 실행되는 작업 단위로, 같은 프로세스 내의 다른 스레드와 메모리와 자원을 공유합니다.

  • 메모리 공유: 동일한 주소 공간을 사용하여 데이터 교환이 빠르고 효율적입니다.
  • 경량성: 스레드는 생성과 전환 비용이 낮아, 고성능 애플리케이션에 적합합니다.
  • 동기화 필요성: 공유 자원을 사용할 때는 동기화가 필요해, 관리가 복잡할 수 있습니다.

스케줄링 관점에서의 차이

  • 프로세스: 독립적인 작업 단위로, 한 프로세스가 중단되더라도 다른 프로세스에 영향을 주지 않습니다.
  • 스레드: 같은 프로세스 내에서 자원을 공유하므로, 한 스레드의 오류가 프로세스 전체에 영향을 미칠 수 있습니다.

프로세스와 스레드의 선택

  • 프로세스 사용 사례: 독립적인 작업을 수행하거나 높은 수준의 격리가 필요한 경우.
  • 스레드 사용 사례: 병렬 처리가 필요하거나 메모리 공유가 유리한 경우.

프로세스와 스레드의 차이를 이해하면, 시스템 스케줄링의 작동 원리를 보다 명확히 파악할 수 있습니다.

스케줄링 알고리즘의 종류


스케줄링 알고리즘은 CPU 자원을 어떤 방식으로 분배할지를 결정하는 규칙입니다. 리눅스 커널을 포함한 운영 체제에서는 다양한 상황에 맞게 최적화된 여러 스케줄링 알고리즘을 사용합니다.

선점형 스케줄링


선점형 스케줄링은 실행 중인 프로세스를 강제로 중단하고, 우선순위가 높은 다른 프로세스를 실행할 수 있도록 합니다.

  • 장점: 높은 응답성과 공정성 제공.
  • 단점: 빈번한 문맥 전환으로 오버헤드 발생 가능.
  • 적용 사례: 대화형 애플리케이션, 실시간 시스템.
  • 예시: 리눅스 커널의 CFS(Completely Fair Scheduler).

비선점형 스케줄링


비선점형 스케줄링은 현재 실행 중인 프로세스가 작업을 완료하거나 스스로 대기 상태로 전환될 때까지 CPU를 독점적으로 사용합니다.

  • 장점: 문맥 전환 오버헤드가 적음.
  • 단점: 낮은 우선순위의 프로세스가 무한히 대기하는 문제가 발생할 수 있음(기아 상태).
  • 적용 사례: 배치 작업, 긴 계산 작업.
  • 예시: 우선순위가 높은 작업이 항상 끝날 때까지 기다리는 방식.

주요 스케줄링 알고리즘

  1. FCFS (First-Come, First-Served):
  • 먼저 도착한 프로세스부터 실행.
  • 간단하지만, 평균 대기 시간이 길어질 수 있음.
  1. SJF (Shortest Job First):
  • 실행 시간이 가장 짧은 작업을 먼저 실행.
  • 평균 대기 시간을 줄일 수 있으나, 긴 작업이 무기한 대기하는 문제 발생 가능.
  1. Round Robin (RR):
  • 각 프로세스에 고정된 시간 슬라이스를 할당.
  • 공정성 보장이 가능하며, 대화형 시스템에 적합.
  1. Priority Scheduling:
  • 우선순위가 높은 프로세스를 먼저 실행.
  • 우선순위 기반으로, 낮은 우선순위 작업의 기아 상태를 방지하는 추가 메커니즘 필요.

알고리즘 선택의 기준


스케줄링 알고리즘을 선택할 때는 시스템 성능, 응답성, 공정성, 오버헤드 등의 요소를 종합적으로 고려해야 합니다.

리눅스는 다양한 환경에서의 유연성을 위해 여러 알고리즘을 혼합해 사용하며, 각각의 알고리즘이 가진 장점을 최대한 활용합니다.

CFS 스케줄링 알고리즘의 작동 원리


리눅스 커널의 기본 스케줄러인 Completely Fair Scheduler (CFS)는 공정성과 효율성을 목표로 설계되었습니다. 프로세스 실행 시간을 균등하게 분배하며, 선점형 방식으로 동작합니다.

가상 런타임(Virtual Runtime)


CFS는 프로세스마다 “가상 런타임”을 계산하여 스케줄링의 기준으로 삼습니다.

  • 정의: 프로세스가 CPU에서 실행된 누적 시간을 우선순위로 조정한 값.
  • 원리: 낮은 가상 런타임을 가진 프로세스가 우선 실행됩니다.
  • 공정성 보장: 프로세스의 가상 런타임이 균등해지도록 스케줄링합니다.

레드블랙 트리(Red-Black Tree)


CFS는 프로세스 관리에 효율적인 레드블랙 트리 자료 구조를 사용합니다.

  • 특징: 자동으로 정렬되는 이진 검색 트리.
  • 효율성: 삽입, 삭제, 검색이 O(log n)의 시간 복잡도를 가짐.
  • 사용 방식: 각 프로세스의 가상 런타임을 기준으로 정렬하며, 루트 노드에 위치한 프로세스가 가장 낮은 가상 런타임을 가짐.

타임 슬라이스(Time Slice) 계산

  • 프로세스 우선순위: 가상 런타임에 따라 CPU 실행 시간이 조정됩니다.
  • 공정한 분배: 각 프로세스에 할당된 시간은 전체 프로세스의 가중치 합에 비례합니다.

선점 조건


CFS는 선점형 스케줄러로, 다음과 같은 조건에서 현재 실행 중인 프로세스를 선점합니다:

  1. 준비 상태의 프로세스가 현재 실행 중인 프로세스보다 낮은 가상 런타임을 가질 때.
  2. 실행 중인 프로세스가 할당된 타임 슬라이스를 모두 소진했을 때.

프로세스 상태 변경 처리

  • 새로운 프로세스 추가: 레드블랙 트리에 삽입되어 정렬됩니다.
  • 프로세스 종료: 레드블랙 트리에서 제거되며, 최상위 프로세스가 실행됩니다.

CFS의 장점

  • 공정하고 예측 가능한 스케줄링.
  • 높은 응답성과 CPU 자원의 효율적 사용.
  • 다양한 우선순위와 작업 유형을 처리할 수 있는 유연성.

CFS는 리눅스의 범용 스케줄러로, 대화형 작업부터 고성능 서버 작업까지 다양한 환경에서 성능을 발휘합니다. 이를 이해하면 리눅스 커널의 스케줄링 구조와 작동 원리를 명확히 파악할 수 있습니다.

C 코드로 보는 스케줄링 구현


리눅스 커널의 스케줄링은 C 언어로 작성된 코드로 구현되어 있으며, 이를 분석하면 스케줄링 메커니즘의 구체적인 동작 방식을 이해할 수 있습니다.

스케줄러 엔트리 포인트


리눅스 커널에서 스케줄링 관련 함수는 kernel/sched/ 디렉토리에 정의되어 있습니다. 핵심 엔트리 포인트는 다음과 같습니다:

  • schedule() 함수: 스케줄러가 호출되는 메인 함수.
  • pick_next_task() 함수: 다음에 실행할 프로세스를 선택.
  • context_switch() 함수: 프로세스 간의 문맥 전환 수행.

schedule() 함수의 구조


schedule() 함수는 다음과 같은 주요 단계를 포함합니다:

  1. 현재 프로세스 상태 업데이트:
   current->state = TASK_RUNNING;


현재 프로세스의 상태를 TASK_RUNNING으로 설정.

  1. 다음 프로세스 선택:
   next = pick_next_task(rq, prev);


pick_next_task() 함수는 레드블랙 트리를 기반으로 가상 런타임이 가장 낮은 프로세스를 선택합니다.

  1. 문맥 전환 수행:
   if (prev != next)
       context_switch(rq, prev, next);


선택된 프로세스가 이전 프로세스와 다르면 문맥 전환을 수행합니다.

pick_next_task() 함수 분석


pick_next_task()는 CFS의 핵심 로직을 구현하며, 다음 단계를 거칩니다:

  • 레드블랙 트리 탐색:
  struct sched_entity *se = rb_entry(rb_first(&cfs_rq->tasks), struct sched_entity, run_node);


가상 런타임이 가장 낮은 프로세스를 탐색합니다.

  • 프로세스 반환:
    선택된 프로세스를 반환하여 실행 준비를 합니다.

context_switch() 함수 분석


context_switch()는 실행 프로세스를 변경하는 과정을 구현합니다:

  • 문맥 저장: 이전 프로세스의 레지스터, 메모리 상태를 저장합니다.
  • 문맥 복원: 새 프로세스의 레지스터와 메모리 상태를 로드합니다.
  • CPU 제어권 전환: 다음 프로세스가 CPU를 사용할 수 있도록 설정합니다.

코드 분석의 중요성


리눅스 커널 스케줄링 코드를 분석하면 다음과 같은 점에서 유용합니다:

  • 작동 원리 이해: 스케줄링 알고리즘의 구체적인 작동 방식을 학습.
  • 문제 해결: 성능 문제를 디버깅하거나 최적화 아이디어를 도출.
  • 학습 응용: 자신만의 스케줄러를 구현하거나 커널 코드를 수정하는 데 응용 가능.

C 코드를 통해 리눅스 커널 스케줄링의 실제 구현을 이해하면, 이론을 넘어 시스템 전반의 작동 원리를 심도 있게 파악할 수 있습니다.

스케줄링 최적화 및 문제 해결


리눅스 커널의 스케줄링은 시스템 성능과 안정성에 중요한 영향을 미칩니다. 스케줄링을 최적화하고 문제를 해결하기 위한 방법을 이해하면, 시스템 성능을 효과적으로 개선할 수 있습니다.

스케줄링 최적화 방법

  1. 우선순위 조정:
    프로세스의 우선순위를 적절히 설정하여 중요한 작업이 더 빠르게 처리되도록 합니다.
   renice -n -10 -p <PID>


위 명령은 특정 프로세스의 우선순위를 높입니다.

  1. 타임 슬라이스 조정:
    타임 슬라이스 값을 최적화하여 대화형 작업의 응답성을 높이거나 배치 작업의 처리 속도를 개선합니다.
  • 대화형 작업: 짧은 타임 슬라이스.
  • 배치 작업: 긴 타임 슬라이스.
  1. 커널 매개변수 튜닝:
    /proc/sys/kernel/ 디렉토리의 매개변수를 조정하여 스케줄링 정책을 최적화합니다.
   echo 1 > /proc/sys/kernel/sched_child_runs_first


자식 프로세스를 우선 실행하도록 설정합니다.

  1. CPU 핀닝 및 NUMA 최적화:
    프로세스를 특정 CPU 코어에 바인딩하거나 NUMA(Locality-Aware Memory Allocation)를 최적화합니다.
   taskset -c 0-3 <command>


특정 CPU 코어에 작업을 할당합니다.

스케줄링 문제 해결

  1. 높은 문맥 전환율:
    문맥 전환이 잦으면 시스템 성능이 저하될 수 있습니다.
  • 해결책: top 또는 perf를 사용해 문맥 전환 원인을 분석하고, 프로세스 수를 줄이거나 우선순위를 조정합니다.
  1. 기아 상태(Starvation):
    낮은 우선순위의 프로세스가 무한 대기 상태에 빠질 수 있습니다.
  • 해결책: 우선순위 보정 메커니즘(Nice Value)을 사용하거나 공정성 기반의 스케줄러(CFS)를 활용합니다.
  1. CPU 과부하:
    CPU 사용량이 과도하게 높을 경우, 스케줄링이 비효율적일 수 있습니다.
  • 해결책: 작업 부하를 분산하거나, 불필요한 프로세스를 종료합니다.
  1. 부적절한 스케줄링 정책 선택:
    특정 워크로드에 맞지 않는 스케줄링 정책이 적용되었을 수 있습니다.
  • 해결책: chrt 명령을 사용해 적절한 스케줄링 클래스를 적용합니다.
   chrt -f 10 <command>


실시간 스케줄링 정책으로 변경합니다.

문제 해결 사례

  • 사례 1: 웹 서버 응답 지연 문제 → CPU 핀ning으로 웹 서버 프로세스를 특정 코어에 고정하여 해결.
  • 사례 2: 대규모 데이터 처리 지연 → NUMA 최적화로 메모리 접근 시간을 줄여 성능 개선.

스케줄링 최적화의 중요성


효율적인 스케줄링은 시스템 성능, 사용자 경험, 에너지 소비 최적화에 직접적으로 기여합니다. 스케줄링 문제를 파악하고 해결하는 능력은 운영 체제 및 시스템 개발자에게 필수적인 기술입니다.

실습: 간단한 스케줄러 구현


스케줄러의 기본 동작 원리를 이해하기 위해, C언어를 활용해 간단한 선점형 스케줄러를 구현합니다. 이 실습에서는 타임 슬라이스 기반으로 프로세스를 스케줄링하는 예제를 다룹니다.

스케줄러 코드 개요

  • 프로세스는 이름과 실행 시간을 포함하는 구조체로 정의됩니다.
  • 큐에 프로세스를 추가하고, 타임 슬라이스를 기반으로 실행합니다.
  • 실행이 완료된 프로세스는 큐에서 제거됩니다.

코드 구현

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TIME_SLICE 2  // 타임 슬라이스 (초 단위)

// 프로세스 구조체 정의
typedef struct {
    char name[20];
    int burst_time;
} Process;

// 노드 구조체 정의 (링크드 리스트 사용)
typedef struct Node {
    Process process;
    struct Node *next;
} Node;

// 프로세스 추가 함수
void add_process(Node **head, char *name, int burst_time) {
    Node *new_node = (Node *)malloc(sizeof(Node));
    strcpy(new_node->process.name, name);
    new_node->process.burst_time = burst_time;
    new_node->next = NULL;

    if (*head == NULL) {
        *head = new_node;
    } else {
        Node *temp = *head;
        while (temp->next != NULL) {
            temp = temp->next;
        }
        temp->next = new_node;
    }
}

// 스케줄러 실행 함수
void run_scheduler(Node **head) {
    while (*head != NULL) {
        Node *current = *head;
        printf("Running process: %s\n", current->process.name);

        if (current->process.burst_time > TIME_SLICE) {
            current->process.burst_time -= TIME_SLICE;
            printf("Remaining time for %s: %d\n", current->process.name, current->process.burst_time);

            *head = current->next;  // 현재 프로세스 제거
            Node *temp = current;
            while (temp->next != NULL) {
                temp = temp->next;
            }
            temp->next = current;  // 프로세스를 리스트 끝에 추가
            current->next = NULL;
        } else {
            printf("Process %s completed.\n", current->process.name);
            *head = current->next;  // 완료된 프로세스 제거
            free(current);
        }
    }
}

int main() {
    Node *head = NULL;

    // 프로세스 추가
    add_process(&head, "Process1", 5);
    add_process(&head, "Process2", 3);
    add_process(&head, "Process3", 8);

    // 스케줄러 실행
    run_scheduler(&head);

    return 0;
}

코드 실행 결과


프로그램을 실행하면 각 프로세스가 타임 슬라이스에 따라 실행되는 과정을 출력합니다.

Running process: Process1  
Remaining time for Process1: 3  
Running process: Process2  
Process Process2 completed.  
Running process: Process3  
Remaining time for Process3: 6  
...
Process Process3 completed.  

확장 가능성


이 간단한 스케줄러를 기반으로 다음과 같은 기능을 추가할 수 있습니다:

  • 우선순위 기반 스케줄링.
  • 프로세스 동적 생성 및 추가.
  • 실행 시간 분석 및 최적화.

실습의 목적


이 예제는 리눅스 커널의 복잡한 스케줄링 메커니즘을 단순화하여, 타임 슬라이스 기반의 기본 원리를 이해하도록 돕습니다. 이를 바탕으로 운영 체제의 스케줄링 구현 방식을 심도 있게 학습할 수 있습니다.

요약


본 기사에서는 리눅스 커널의 시스템 스케줄링을 C언어를 통해 이해하는 방법을 다뤘습니다. 스케줄링의 기본 개념, 리눅스 커널의 CFS 알고리즘 구조, 프로세스와 스레드의 차이점, 다양한 스케줄링 알고리즘, 최적화 및 문제 해결 방법을 포함해 실습 코드 예제까지 소개했습니다. 이를 통해 운영 체제의 핵심 메커니즘을 학습하고, 성능 최적화와 문제 해결 능력을 향상시킬 수 있습니다.

목차