ch7. process scheduling
CPU는 하나인데,
왜 여러 프로그램이 동시에 도는 것처럼 보일까?
운영체제는 아주 짧은 시간 단위로 실행할 프로세스를 끊임없이 교체합니다. 누가, 언제, 얼마나 오래 CPU를 차지할지 결정하는 주체가 바로 스케줄러입니다. 스케줄러는 시스템의 공평성과 반응성 사이에서 최적의 균형점을 찾는 정교한 타협 장치라 할 수 있습니다.
오늘 강의의 큰 그림WHY SCHEDULING EXISTS
CPU 코어가 하나라면, 정말 여러 프로세스가 동시에 실행될 수 있을까요?
엄밀히 말해 정답은 "아니요"입니다. 단일 CPU 코어는 한 순간에 오직 하나의 실행 흐름만 처리할 수 있습니다. 하지만 운영체제는 사람이 인지하기 힘들 만큼 짧은 시간 간격으로 실행할 프로세스를 번갈아 교체합니다. 이 속도가 워낙 빠르다 보니 사용자 입장에서는 마치 여러 프로그램이 '동시에' 실행되는 것처럼 느끼게 됩니다.
스케줄링은 본질적으로 다음 두 가지 질문에 대한 해답을 찾는 과정입니다. 첫째, 언제 프로세스를 교체할 것인가? 둘째, 다음에는 어떤 프로세스에게 CPU를 넘겨줄 것인가? 이 두 가지 핵심 질문이 오늘 살펴볼 스케줄링 챕터 전체를 관통합니다.
- 정책(Policy)
- 대화형 프로세스는 빠르게, 배치 작업도 굶주리지 않게, 실시간 프로세스는 강하게 보장하는 스케줄러의 철학입니다.
- 알고리즘
- runqueue, active/expired 배열, 동적 우선순위 계산, bitmap 기반의 O(1) 탐색, scheduler_tick 등 실제 구현 방식입니다.
- 시스템 호출
- nice, setpriority, sched_setscheduler, sched_setaffinity 등 사용자가 스케줄링에 개입할 수 있는 인터페이스입니다.
CPU를 하나의 무대, 프로세스들을 무대에 오르고 싶은 배우들, 스케줄러를 무대 감독이라고 상상해 봅시다. 대사를 쳐야 하는 배우(대화형 프로세스)를 무대 뒤에서 너무 오래 기다리게 하면 안 되고, 묵묵히 준비하는 엑스트라(배치 작업)도 언젠가는 무대에 올려야 합니다. 또한, VIP 공연(실시간 프로세스)이 있다면 가장 먼저 무대를 내어주어야 하죠.
핵심 문장: 스케줄러는 공평함과 반응성을 동시에 잡으려는 치열한 타협의 산물입니다. 리눅스 스케줄러는 반응성, 공정성, 실시간성은 물론이고 캐시 효율과 멀티프로세서의 부하 균형까지 종합적으로 고려하는 정교한 시스템입니다.
이번 장은 Linux 2.6 버전의 O(1) 스케줄러를 기준으로 설명합니다. runqueue, active/expired 배열 구조, 동적 우선순위 계산, 평균 수면 시간(sleep time)을 활용한 효율적인 프로세스 선택 방식을 중점적으로 다룹니다.
핵심 O/X 퀴즈
1. 스케줄링의 핵심 질문은 "언제 교체하고, 다음에 어떤 프로세스를 실행할 것인가"이다.
2. 시분할(time sharing) 시스템에서는 응용 프로그램이 코드 내에 명시적으로 CPU 양보 명령을 넣어야만 공평한 실행이 가능하다.
3. 이번 장의 큰 뼈대는 스케줄링 정책, 핵심 알고리즘, 관련된 시스템 호출로 나눌 수 있다.
Time Sharing과 우선순위: 스케줄러의 기본 철학TIME SHARING & PRIORITY
리눅스 스케줄링의 근간은 time sharing(시분할)입니다. 단일 CPU의 시간을 아주 잘게 쪼개어 여러 프로세스에 공평하게 분배하는 방식입니다. 이때 한 프로세스가 할당받는 작은 시간 조각을 time slice 또는 quantum이라고 부릅니다.
그렇다면 모든 프로세스에게 시간을 기계적으로 똑같이 나눠주면 완벽할까요? 텍스트 에디터와 백그라운드에서 실행되는 무거운 컴파일러를 똑같이 취급한다면, 사용자는 키보드 입력 시 화면 반응이 느려져 시스템이 버벅거린다고 느낄 것입니다. 이를 해결하기 위해 스케줄러는 프로세스에 dynamic priority(동적 우선순위)를 부여하고, 각 프로세스의 과거 행동 패턴을 바탕으로 우선순위를 유연하게 조정합니다.
입출력 작업의 비중이 높습니다. 디스크, 네트워크, 키보드 입력을 기다리느라 CPU를 스스로 놓고 자주 대기(sleep) 상태에 빠집니다.
연산 작업의 비중이 높습니다. 이미지 렌더링, 대규모 과학 계산 등 CPU를 한 번 잡으면 오래 사용하려는 경향이 있습니다.
사용자와 지속적으로 상호작용합니다. 셸, 웹 브라우저, 에디터 등 즉각적인 반응 시간(응답성)이 가장 중요합니다.
사용자 개입 없이 백그라운드에서 돌아갑니다. 컴파일러, 데이터베이스 인덱싱 등 즉각적인 반응보다는 전체 처리량(throughput)이 중요합니다.
낮은 우선순위 작업에 의해 실행이 지연되어서는 안 되며, 예측 가능하고 엄격한 응답 시간이 보장되어야 합니다. 미디어 재생, 하드웨어 제어 프로그램이 이에 속합니다.
I/O-bound와 interactive 프로세스는 동작 패턴이 비슷해 보이지만 완전히 동일하지는 않습니다. 예를 들어 대규모 데이터베이스 서버는 전형적인 batch 작업이지만 디스크 I/O를 굉장히 많이 발생시킵니다. 스케줄러는 이처럼 프로세스의 본래 목적, 행동 패턴, 대기 시간, 실시간 처리 요구사항 등을 종합적으로 고려하여 판별합니다.
핵심 O/X 퀴즈
1. I/O-bound 프로세스는 입출력 장치의 응답을 기다리느라 자주 sleep 상태에 들어가는 경향이 있다.
2. batch 작업으로 분류되는 프로세스는 항상 CPU 연산만 수행하는 CPU-bound 성향을 띤다.
3. 리눅스 스케줄러는 각 프로세스가 과거에 보인 행동(예: 대기 시간)을 바탕으로 동적으로 우선순위를 조정한다.
선점과 quantum: 언제 CPU를 빼앗을 것인가?PREEMPTION & TIME QUANTUM
프로세스가 자발적으로 CPU를 양보하지 않더라도 운영체제 커널이 강제로 실행 흐름을 빼앗을 수 있습니다. 이를 preemption(선점)이라고 합니다. 선점이 발생하는 대표적인 두 가지 경우는 다음과 같습니다. 첫째, 현재 실행 중인 프로세스에 할당된 타임 퀀텀(time quantum)이 모두 소진되었을 때입니다. 둘째, 현재 프로세스보다 우선순위가 더 높은 프로세스가 방금 대기 상태에서 깨어나 실행 가능한(runnable) 상태가 되었을 때입니다.
무거운 컴파일러가 CPU를 열심히 쓰고 있는 도중, 사용자가 텍스트 에디터에서 키보드를 누릅니다. 커널은 즉시 상황을 비교합니다. "방금 깨어난 에디터의 동적 우선순위가 현재 돌고 있는 컴파일러보다 높은가?" 조건이 맞다면 커널은 현재 프로세스의 TIF_NEED_RESCHED 플래그를 활성화합니다. 이는 마치 "가장 가까운 안전한 지점에서 스케줄러를 호출해 교체 작업을 수행하라"는 포스트잇 메모를 붙여두는 것과 같습니다.
여기서 주의할 점이 있습니다. 선점당한 프로세스가 무조건 대기(suspended) 상태로 빠지는 것은 아닙니다. TASK_RUNNING 상태는 "지금 당장 물리적 CPU 위에서 실행 중"이라는 뜻만이 아닙니다. "현재 실행 가능하며, CPU만 다시 할당받으면 언제든 즉시 실행될 준비가 되어 있다"는 포괄적인 의미를 지닙니다.
퀀텀(quantum)의 길이를 정하는 것은 까다로운 타협의 문제입니다. 길이가 너무 짧으면 프로세스를 쉴 새 없이 교체하느라 문맥 교환(context switch) 비용이 기하급수적으로 늘어나고, 너무 길면 시스템의 반응성이 크게 떨어지게 됩니다.
- 너무 짧을 때
- 레지스터 복구와 캐시 미스 등 문맥 교환 비용이 전체 CPU 시간에서 차지하는 비중이 과도하게 커집니다. 발표자가 5초 발표하고 무대에서 내려오는데 교체 시간이 5초나 걸리는 셈입니다.
- 너무 길 때
- 다른 프로세스들이 순서를 기다리는 시간이 길어집니다. 운 나쁘게 CPU-bound 프로세스가 먼저 선택되어 긴 퀀텀을 소모하면, 키보드 입력을 기다리던 대화형 프로세스는 그 긴 시간을 꼼짝없이 기다려야 합니다.
- 리눅스의 원칙
- 가능한 한 퀀텀을 길게 주어 오버헤드를 줄이되, 시스템의 전반적인 반응성을 해치지 않는 최적의 선을 유지한다.
핵심 O/X 퀴즈
1. 스케줄러에 의해 선점된 프로세스는 반드시 sleep(대기) 상태로 전환된다.
2. 할당되는 타임 퀀텀이 너무 짧으면 빈번한 문맥 교환 오버헤드로 인해 시스템 전체의 효율이 떨어질 수 있다.
3. 대화형 프로세스는 우선순위 우대를 받을 수 있으므로, 키 입력 등으로 깨어날 때 실행 중인 배치 프로세스를 즉각적으로 선점할 수 있다.
세 가지 스케줄링 클래스SCHED_NORMAL · SCHED_FIFO · SCHED_RR
리눅스의 모든 프로세스는 스케줄링 방식에 따라 다음 세 가지 정책(Policy) 중 하나로 분류됩니다.
셸, 텍스트 편집기, 컴파일러 등 우리가 쓰는 대부분의 애플리케이션이 속합니다. 동적 우선순위, 평균 수면 시간(sleep_avg), 보너스 점수 시스템이 복잡하게 맞물려 작동합니다.
실시간 — First In, First Out
일단 CPU를 선점하면 더 높은 우선순위의 실시간 프로세스가 나타나거나, 스스로 I/O 대기에 빠지거나, 명시적으로 CPU를 양보(yield)하지 않는 한 계속해서 멈추지 않고 실행됩니다.
실시간 — Round Robin
SCHED_FIFO와 비슷하지만, '같은 우선순위'를 가진 실시간 프로세스들 사이에서 공평하게 CPU를 나누기 위해 타임 퀀텀을 적용합니다. 주어진 퀀텀을 다 쓰면 동일 우선순위 대기열의 맨 뒤로 이동합니다.
SCHED_FIFO는 초청 강연자의 독점 마이크와 같습니다. 한 번 마이크를 잡으면 본인이 내려놓기 전까지는 누구도 간섭할 수 없습니다. 반면 SCHED_RR은 패널 토론입니다. 같은 등급의 패널(동일 우선순위)끼리 10분씩 발언 시간을 정해두고 타이머가 울리면 다음 사람에게 마이크를 넘깁니다. 마지막으로 SCHED_NORMAL은 스케줄러가 알아서 "너는 대화형이네, 먼저 해", "너는 배치 작업이네, 조금 기다려"라며 유연하게 통제하는 일반 시민들입니다.
주의할 점이 있습니다. 일반 프로세스의 내부 정적 우선순위(static priority) 값은 100에서 139 사이의 범위를 가지며, 숫자가 작을수록 우선순위가 높습니다. 반면 시스템 호출을 통해 지정하는 실시간 프로세스의 우선순위는 1부터 99까지의 범위를 가지며, 이때는 숫자가 클수록(99가 최고) 우선순위가 높습니다. 현재 언급하는 우선순위가 커널 내부 지표인지, 시스템 호출 인터페이스 지표인지 명확히 구분해야 혼동을 피할 수 있습니다.
핵심 O/X 퀴즈
1. SCHED_FIFO로 설정된 프로세스는 동일한 우선순위의 다른 프로세스가 대기 중이면 퀀텀 만료 시 자동으로 차례를 넘겨준다.
2. SCHED_RR 정책은 동일한 실시간 우선순위를 가진 프로세스들 사이에서만 라운드 로빈 방식으로 시간을 배분한다.
3. 우리가 일상적으로 사용하는 웹 브라우저나 워드 프로세서는 보통 SCHED_NORMAL 정책에 의해 관리된다.
일반 프로세스의 우선순위: static priority, nice, base quantumSTATIC PRIORITY & NICE
일반 프로세스의 static priority(정적 우선순위) 값은 커널 내부적으로 100부터 139까지의 범위로 관리됩니다. 반복해서 강조하지만, 숫자가 작을수록 더 높은 우선순위를 의미합니다. 특별히 변경하지 않은 프로세스의 기본 정적 우선순위는 중간값인 120으로 설정됩니다.
사용자는 nice 값을 통해 이 정적 우선순위를 간접적으로 조정할 수 있습니다. nice 값은 전통적인 유닉스 시스템에서 −20부터 +19까지의 범위를 갖습니다. 단어 뜻 그대로 nice 값이 클수록 다른 프로세스에게 CPU를 너그럽게 양보하는 "착한" 프로세스가 되며, 결과적으로 정적 우선순위는 낮아집니다. 반대로 nice 값을 음수로 내려 이기적으로(우선순위를 높게) 만들려면 관리자 권한이 필요합니다.
정적 우선순위의 가장 중요한 역할은 프로세스가 다음 번에 할당받을 기본 타임 퀀텀(base time quantum)의 크기를 결정하는 것입니다.
| static priority | nice 값 | 할당되는 base quantum |
|---|---|---|
| 100 | -20 | 약 800ms (넉넉함) |
| 120 | 0 (기본값) | 약 100ms (표준) |
| 139 | +19 | 약 5ms (매우 짧음) |
우선순위가 높은 프로세스는 상대적으로 훨씬 긴 CPU 실행 조각을 부여받고, 낮은 프로세스는 짧은 조각만 부여받습니다. 하지만 "우선순위가 높다고 해서 영원히 CPU를 독점할 수 있다"는 뜻은 아닙니다. 뒤에서 다룰 active/expired 큐 분리 구조와 동적 우선순위 보정 로직이 특정 프로세스의 기아 상태(starvation)를 막아주기 때문입니다.
핵심 O/X 퀴즈
1. 일반 프로세스의 정적 우선순위 값(100~139)은 숫자가 작을수록 스케줄러 내에서 높은 우선순위로 대우받는다.
2. 프로세스의 nice 값을 증가시키면 일반적으로 프로세스는 우선순위가 낮아지고 기본 타임 퀀텀도 짧아진다.
3. 정적 우선순위가 월등히 높은 일반 프로세스가 존재하면 우선순위가 낮은 프로세스는 영원히 실행 기회를 얻지 못한다.
dynamic priority와 average sleep time"자주 대기하는 프로세스는 왜 우대받을까?"
정적 우선순위가 퀀텀의 길이를 정한다면, 스케줄러가 다음에 당장 실행할 프로세스를 '고를 때' 실제로 비교하는 값은 dynamic priority(동적 우선순위)입니다. 이 값 역시 100부터 139 사이의 범위를 가지며, 작을수록 유리합니다.
- 계산식
dynamic priority = max(100, min(static priority − bonus + 5, 139))- bonus의 범위
- 0부터 10까지입니다. 공식 구조상 보너스가 5보다 작으면 사실상 벌점(우선순위 수치 증가 = 하락)을 받고, 5보다 크면 혜택(수치 감소 = 상승)을 받습니다.
- bonus 결정의 핵심 기준
- average sleep time (평균 대기 시간) — 프로세스가 자발적으로 잠들어 있던(sleep) 시간이 길수록 주어지는 보너스가 커집니다.
- 무한 상승 방지
- 평균 대기 시간은 계속 누적되지 않으며, 최대 1초(커널 설정값)까지만 인정되도록 상한선이 제한되어 있습니다.
왜 잠을 많이 잔 프로세스에게 보너스를 줄까요? 텍스트 에디터는 사용자가 키보드를 누르기 전까지는 계속 대기(sleep) 상태입니다. 이런 대화형 프로그램은 CPU를 장시간 소모하는 게 목적이 아니라, 깨어났을 때 '번개처럼' 한 번 반응하고 다시 자는 것이 목적입니다. 반면 3D 렌더링 프로그램은 눈 떠있는 내내 CPU 연산만 하므로 수면 시간이 거의 없습니다. 스케줄러는 "자주, 오래 잠들어 있던 프로세스가 깨어났다면 사용자와 상호작용하는 대화형 작업일 확률이 높으니, 최우선으로 처리해 주자"라고 영리하게 추론하는 것입니다.
스케줄러가 특정 프로세스를 확실한 대화형(interactive)으로 간주하여 특별 우대하는 휴리스틱 조건은 대략 다음과 같습니다.
dynamic priority ≤ 3 × static priority / 4 + 28
이 수식의 의미를 음미해 보면 재미있습니다. 애초에 기본 정적 우선순위(static priority)가 높은 프로세스는 조금만 대기해도 쉽게 대화형으로 인정받아 우선순위 보정을 받습니다. 반면 nice 값이 높아 정적 우선순위가 바닥(139)인 프로세스는 아무리 오래 잠자고 일어나도 스케줄러가 대화형 프로세스로 인정해 주지 않습니다. 태생적 한계가 존재하는 셈입니다.
핵심 O/X 퀴즈
1. 스케줄러가 다음 프로세스를 런큐에서 선택할 때 직접적으로 비교하는 기준은 정적 우선순위(static priority)이다.
2. 프로세스의 평균 대기(sleep) 시간이 길게 누적될수록 보너스 값이 증가하여 대화형 프로세스로 인정받기 유리해진다.
3. 산출 공식에 따라 bonus 값이 커지면 최종 dynamic priority 수치는 작아지며, 이는 곧 스케줄링 우선순위가 높아짐을 뜻한다.
active와 expired: 굶주림을 막는 두 개의 대기실Figure 7-1 — prio_array_t
동적 우선순위만 믿고 있다가는 우선순위가 조금이라도 높은 프로세스가 CPU를 영원히 장악하는 사태가 벌어질 수 있습니다. 이를 막기 위해 리눅스 2.6 스케줄러는 런큐 내부의 실행 가능(runnable) 프로세스들을 명확히 두 집합으로 격리합니다.
- active 배열
- 이번 주기에 자신에게 할당된 타임 퀀텀을 아직 다 쓰지 않고 남아있는 프로세스들의 집합입니다. 스케줄러는 오직 이 배열 안에서만 다음 실행 프로세스를 고릅니다.
- expired 배열
- 자신의 타임 퀀텀을 완전히 탕진한 프로세스들이 쫓겨나 대기하는 공간입니다. 이들은 active 배열이 완전히 텅 빌 때까지 절대 다시 CPU를 받을 수 없습니다.
- 배열 교체(Swap)
- active 배열의 모든 프로세스가 퀀텀을 소진해 텅 비게 되면, 스케줄러는 active 포인터와 expired 포인터가 가리키는 주소만 서로 맞바꿉니다. 무거운 데이터를 복사할 필요 없이 포인터 스왑 한 번으로 O(1) 시간에 새 주기가 시작됩니다.
유명한 놀이공원의 인기 어트랙션 대기열에 비유해 볼까요. 'active 줄'은 아직 어트랙션을 한 번도 타지 못한 사람들의 줄이고, 'expired 줄'은 이미 한 번 타고 나와서 다시 타려는 사람들의 줄입니다. 운영요원은 무조건 active 줄의 손님들만 태워줍니다. active 줄이 완전히 비워지면 그제서야 두 줄 앞의 안내 팻말("입장 가능", "대기")만 휙 바꿔 달아 expired 줄을 새로운 active 줄로 만듭니다. 줄을 서 있는 사람들은 제자리에 가만히 있어도 자연스럽게 새 기회를 얻게 됩니다.
이 우아한 구조의 궁극적인 목적은 앞서 언급한 기아 상태(starvation)를 원천 차단하는 것입니다. 아무리 우선순위가 낮은 프로세스라도, 언젠가는 active 배열이 비워지고 팻말이 바뀌어 무조건 한 번은 실행될 기회를 보장받기 때문입니다.
우대 조건의 예외 규칙: 스케줄러는 응답성이 중요한 대화형(interactive) 프로세스가 퀀텀을 다 쓰면 예외적으로 expired가 아닌 active 배열로 다시 슬쩍 넣어줄 수 있습니다. 하지만 이 꼼수에도 한계가 있습니다. expired 배열에서 기다리는 프로세스들 중 '가장 오래 기다린 녀석'의 대기 시간이 임계치를 넘었거나, expired 배열 쪽에 현재 대화형 프로세스보다 정적 우선순위가 월등히 높은 프로세스가 기다리고 있다면, 대화형 프로세스라도 자비 없이 expired 배열로 강제 퇴출됩니다. 스케줄러의 대원칙은 "대화형을 최대한 우대하되, 남들을 굶겨 죽이지는 않는다"입니다.
각 prio_array_t 구조체 내부에는 140개(우선순위 레벨 수)의 연결 리스트 헤더와 함께 비트맵(bitmap)이 존재합니다. 스케줄러는 140개의 큐를 일일이 순회하며 비어있는지 확인할 필요 없이, 하드웨어 명령어를 통해 비트맵에서 첫 번째로 1이 세팅된 비트의 위치를 단숨에 찾아내어 해당 큐의 맨 앞 프로세스를 O(1)의 속도로 뽑아냅니다.
핵심 O/X 퀴즈
1. active 큐에 속한 프로세스들은 아직 자신의 타임 퀀텀을 전부 소모하지 않은 실행 가능 프로세스들이다.
2. expired 큐로 넘어간 프로세스는 프로세스가 종료될 때까지 다시는 CPU를 할당받을 수 없다.
3. 대화형 프로세스라 하더라도 다른 프로세스들의 극심한 기아 상태(starvation)를 유발할 우려가 감지되면 어쩔 수 없이 expired 큐로 이동해야 한다.
runqueue와 process descriptor: 스케줄러가 보는 장부DATA STRUCTURES
런큐(runqueue)는 현재 실행 가능한 상태(runnable)인 프로세스들을 묶어 관리하는 핵심 자료구조입니다. Linux 2.6 스케줄러 아키텍처의 큰 특징은 시스템의 각 CPU 코어마다 독립적인 자신만의 런큐를 가진다는 점입니다. 이를 통해 전역 락(Global Lock) 경쟁을 없애고, 프로세스가 동일한 CPU에서 계속 실행되도록 유도하여 CPU L1/L2 캐시의 적중률(Cache Hit)을 극대화합니다.
- nr_running
- 현재 이 런큐에 담긴 전체 실행 가능 프로세스의 총합.
- curr
- 현재 해당 CPU를 물리적으로 점유하고 실행 중인 프로세스의 디스크립터 포인터.
- idle
- 실행할 프로세스가 하나도 없을 때 돌아가는 해당 CPU 전용 swapper(idle) 프로세스 포인터.
- active / expired
- 앞서 설명한 두 개의
prio_array_t(우선순위 배열)를 각각 가리키는 포인터. - expired_timestamp
- expired 배열로 넘어간 프로세스 중 가장 오래된 녀석이 언제 넘어갔는지 시간을 기록하여, 기아 상태 판단의 기준으로 삼음.
- best_expired_prio
- 현재 expired 큐에 존재하는 프로세스들 중 가장 높은 정적 우선순위 값을 추적 추적.
- cpu_load 등
- 이후 설명할 멀티프로세서(SMP) 환경에서의 부하 분산(Load Balancing)을 위한 통계 데이터 필드들.
- prio
- 동적 우선순위(dynamic priority). 스케줄러가 런큐 내 위치를 결정할 때 실제로 참조하는 최종 평가 점수.
- static_prio
- 사용자의 nice 설정값과 직결된 기본 정적 우선순위.
- sleep_avg
- 보너스 계산의 핵심이 되는 평균 대기(sleep) 시간 누적치.
- time_slice
- 현재 부여받은 퀀텀 내에서 아직 사용하지 않고 남은 타이머 틱(tick) 횟수.
- array
- 해당 프로세스가 현재 묶여있는 active 또는 expired 우선순위 배열에 대한 포인터.
- policy
- SCHED_NORMAL, SCHED_FIFO, SCHED_RR 중 어느 스케줄링 정책의 적용을 받는지 식별.
- cpus_allowed
- 이 프로세스가 실행될 수 있도록 허락된 CPU 코어들을 나타내는 비트마스크 (CPU Affinity).
- activated
- 대기 상태에서 깨어날 때, 어떤 사건(인터럽트, 시스템콜 등)에 의해 깨어났는지 분류하는 상태 코드.
- rt_priority
- 실시간 프로세스일 경우 적용되는 1~99 사이의 리얼타임 전용 우선순위.
- first_time_slice
- 프로세스 생성 후 아직 첫 번째 퀀텀을 다 쓰지 않았는지를 나타내는 플래그.
fork() 시스템 콜의 영리한 제약: 새로 생성된 자식 프로세스는 무에서 유로 퀀텀을 뚝딱 만들어 공짜로 부여받지 못합니다. 부모 프로세스에게 현재 남아있는 time_slice의 절반을 떼어 부모와 자식이 공평하게 나누어 갖게 됩니다. 이는 악의적으로 짠 프로그램이 끊임없이 fork()를 호출해 시스템 전체의 CPU 시간을 부당하게 집어삼키는(fork bomb 유사 공격) 것을 원천 차단하기 위한 정교한 방어 설계입니다.
핵심 O/X 퀴즈
1. Linux 2.6 스케줄러 아키텍처에서는 락 경쟁 해소와 캐시 효율 증대를 위해 CPU마다 독립적인 런큐(runqueue)를 관리한다.
2. task_struct 내의 time_slice 필드는 프로세스가 이번 턴에 사용할 수 있는 남은 타임 퀀텀을 나타낸다.
3. 프로세스가 fork()를 호출하여 자식 프로세스를 생성하면, 자식은 부모의 상태와 무관하게 시스템 기본 퀀텀을 온전히 100% 새로 발급받는다.
scheduler_tick(): 매 tick마다 스케줄러가 하는 일TIMER INTERRUPT HANDLER
시스템의 하드웨어 타이머 인터럽트가 발생할 때마다(매 tick) 커널 내부에서 scheduler_tick() 함수가 어김없이 호출됩니다. 이는 시스템의 규칙적인 심장박동과 같으며, 현재 실행 중인 프로세스의 잔여 time_slice를 깎아내고 스케줄러의 개입이 필요한지 판단하는 핵심 루틴입니다.
timestamp_last_tick 필드에 현재 시각(jiffies)을 기록하여 시간의 흐름을 갱신합니다.TIF_NEED_RESCHED 플래그를 세워 교체를 지시합니다.rebalance_tick() 함수를 연계 호출하여 여러 CPU 런큐 간의 부하 밸런싱이 필요한지 확인합니다.- SCHED_FIFO 정책
- 애초에 타임 슬라이스(시간 제약) 개념이 없습니다. 더 높은 우선순위의 실시간 프로세스가 깨어나거나, 스스로 I/O 대기에 빠지거나, 명시적으로
yield()하지 않는 한 이 단계에서 강제로 내쫓기지 않고 무시됩니다. - SCHED_RR 정책
- 타이머를 깎다가 0이 되면 기본 타임 슬라이스를 새로 만땅 충전해 줍니다. 그리고
TIF_NEED_RESCHED플래그를 세운 뒤, 해당 프로세스를 동일한 실시간 우선순위 리스트의 맨 뒤(Tail)로 조용히 옮겨 버립니다. - SCHED_NORMAL 정책
- 타이머가 0이 되면 현재 active 큐에서 빼냅니다. 그다음 평균 대기 시간을 바탕으로 동적 우선순위를 새롭게 계산하여 적용하고 타임 슬라이스를 재충전합니다. 마지막으로 이 프로세스가 '대화형'인지 엄격히 심사하여 그 결과에 따라 다시 active 큐에 넣어주거나, 원칙대로 expired 큐로 추방합니다.
핵심 O/X 퀴즈
1. scheduler_tick() 함수는 하드웨어 타이머 틱마다 호출되어 현재 실행 중인 프로세스의 타임 슬라이스를 깎고 문맥 교환 필요성을 검토한다.
2. SCHED_FIFO 정책을 따르는 프로세스도 scheduler_tick() 내부 로직에 의해 퀀텀이 다 닳으면 무조건 동일 우선순위 큐의 맨 뒤로 밀려난다.
3. 일반(SCHED_NORMAL) 프로세스가 퀀텀을 모두 소진하면 커널은 동적 우선순위를 재계산한 후, 대화형 우대 조건과 기아 방지 조건을 종합 판단하여 active 혹은 expired 배열로 배치한다.
try_to_wake_up()과 recalc_task_prio(): 잠든 프로세스가 깨어나는 순간WAKEUP & PRIORITY RECALC
프로세스가 I/O 작업이나 사용자 입력을 기다리며 대기(sleep) 상태에 머물다, 마침내 원하는 데이터가 도착해 깨어날 때 핵심적으로 호출되는 함수가 try_to_wake_up()입니다.
TIF_NEED_RESCHED 플래그를 올려 스케줄러 개입을 요청합니다.프로세스를 런큐에 넣기 직전, recalc_task_prio() 함수를 호출하여 평균 대기 시간(sleep_avg)과 동적 우선순위를 최신 상태로 업데이트합니다. 이때 프로세스가 잠들어 있었던 총 시간은 대략 (현재 시각 - 잠들기 직전 기록해둔 타임스탬프)로 산출하되, 값이 한없이 커져 스케줄러 균형을 파괴하는 것을 막기 위해 최대 1초 한도 내에서만 반영(clamp)합니다.
TASK_UNINTERRUPTIBLE 상태의 미묘한 예외: 디스크 쓰기 등 하드웨어 장치 I/O를 기다리기 위해 깊은 수면에 빠졌던 경우, 시스템 부하 탓에 기다린 것임에도 깨어난 후 다시 한참을 기다리게 하는 건 가혹합니다. 커널은 이런 프로세스들에게 어느 정도의 sleep_avg 보너스를 부여하여 빨리 실행되게 배려합니다. 단, 무거운 I/O 작업을 하는 녀석이 에디터 같은 순수 대화형 프로그램의 권리까지 침해할 정도로 과도하게 우대받지 못하도록 보너스 상승치에 명확한 한계(threshold)를 설정해 둡니다.
프로세스 구조체 안의 activated 필드는 프로세스가 구체적으로 '무엇 때문에' 깨어났는지 단서를 남깁니다. 사용자의 키보드 입력 인터럽트를 타고 깨어났다면 매우 높은 확률로 응답성이 생명인 대화형 프로그램일 것입니다. 반면 내부 시스템 콜이나 백그라운드 커널 스레드의 작업 완료 신호를 받고 깨어난 것이라면 대화형이 아닐 확률이 높죠. 스케줄러는 이 미세한 힌트까지 긁어모아 수면 시간 계산과 보너스 부여 방식에 차등을 둡니다.
핵심 O/X 퀴즈
1. try_to_wake_up() 함수는 대기 상태의 프로세스를 깨워 TASK_RUNNING 상태로 변경하고 적절한 런큐에 편입시킨다.
2. 깨어나는 프로세스는 복잡한 계산을 피하기 위해 항상 이벤트를 처리한 현재 CPU의 로컬 런큐에만 강제로 삽입된다.
3. recalc_task_prio() 함수는 방금까지 잠들어 있던 시간을 정산하여 동적 우선순위(dynamic priority)를 새롭게 갱신한다.
schedule() 1단계: 스케줄러는 어떻게 호출되는가?INVOCATION PATHS
schedule() 함수의 핵심 역할은 현재 쥐고 있는 CPU 통제권을 현재 프로세스에게 계속 유지시킬지, 아니면 다른 더 적합한 프로세스를 찾아 문맥(context)을 넘겨줄지 결단하고 집행하는 것입니다.
- 직접 호출 (Direct Invocation)
- 당장 하드웨어나 자원을 기다려야 해서 진행이 불가능할 때 프로세스 스스로 호출합니다. 자신이 기다려야 할 자원의 wait queue(대기열)에 스스로를 집어넣고 → 자신의 상태를 TASK_INTERRUPTIBLE 등으로 변경한 후 → 자발적으로
schedule()을 호출하여 통제권을 반납합니다. 가끔 무거운 디바이스 드라이버가 긴 루프를 도는 와중에TIF_NEED_RESCHED플래그를 확인하고 양심적으로 직접 호출하기도 합니다. - 지연 호출 (Lazy / Deferred Invocation)
- 당장 그 자리에서 스케줄러를 부르지 않고, 프로세스 구조체에
TIF_NEED_RESCHED깃발(플래그)만 꽂아 둡니다. 커널이 하드웨어 인터럽트 처리를 마치거나 시스템 콜을 끝내고 다시 유저 모드(User Mode)로 복귀하기 직전, 이 깃발이 꽂혀있는지 힐끗 확인합니다. 깃발이 꽂혀있다면 그때서야 안전하게schedule()을 호출하여 실행 흐름을 낚아챕니다.
TIF_NEED_RESCHED 플래그는 당장 그 찰나에 무대에서 끌어내리라는 멱살잡이가 아닙니다. 무대 감독이 올려둔 "이 씬(Scene)이 무사히 마무리되면 다음 배우로 교체할 것"이라는 큐 사인 메모입니다. 타이머 틱에 의해 퀀텀이 바닥났을 때, 잠자던 고우선순위 프로세스가 방금 깨어났을 때, 혹은 시스템 콜로 우선순위 정책이 강제 변경되었을 때 커널은 조용히 이 메모를 붙여 둡니다.
schedule() 함수가 호출되어 막 실행을 시작하면 초반에 다음과 같은 정비 작업을 거칩니다.
커널 내 선점을 일시적으로 비활성화(방해 금지) → 현재 돌고 있는 녀석의 포인터를 prev 변수에 안전하게 보관 → 자신이 속한 로컬 런큐(rq) 구조체 포인터 획득 → 필요시 Big Kernel Lock을 잠깐 풀고 통계 타임스탬프 갱신 → 다른 CPU가 런큐를 건드리지 못하게 런큐 스핀락(Lock) 꽉 걸어 잠그기 → prev 녀석의 상태 점검 (만약 TASK_INTERRUPTIBLE 상태로 자발적으로 자러 온 줄 알았는데, 그사이 취소/종료 시그널이 와 있다면 안 재우고 TASK_RUNNING으로 도로 살려둠)의 과정을 거치게 됩니다.
핵심 O/X 퀴즈
1. 스케줄러 메인 함수인 schedule()은 프로세스가 자발적으로 직접 호출할 수도 있고, 플래그를 통한 지연 방식으로 커널 진출입 시점에 호출될 수도 있다.
2. 커널 로직을 수행하다 TIF_NEED_RESCHED 플래그가 세워지는 그 즉시, 현재 실행 중인 코드는 무조건 중단되고 그 자리에서 문맥 교환(context switch)이 일어난다.
3. 현재 프로세스가 디스크 입출력 완료를 기다려야 한다면, 스스로를 대기 큐(wait queue)에 넣은 후 직접 schedule()을 호출하여 실행 권한을 우아하게 넘겨준다.
schedule() 2단계: next를 고르고 실제로 바꾸기NEXT SELECTION & CONTEXT SWITCH
본격적인 교체 작업입니다. 만약 런큐에 실행 가능(runnable)한 프로세스가 단 한 개도 없다면, 스케줄러는 즉시 idle_balance()를 호출하여 이웃한 다른 CPU의 런큐를 뒤져 일거리를 구걸해 옵니다. 그래도 가져올 프로세스가 없다면 어쩔 수 없이 대타 전문인 idle(swapper) 프로세스를 next로 지명합니다. 정상적인 runnable 프로세스가 존재한다면 active 배열을 열어젖힙니다.
next로 확정합니다. 실행 대기자가 만 명이든 십만 명이든 탐색에 걸리는 시간은 변함이 없습니다.next가 어떤 방식으로 깊은 잠에서 깨어났었는지 추적하여, 런큐에서 차례를 기다린 시간의 일부를 sleep_avg 보너스에 유리하게 더해주는 섬세한 보정을 거칩니다.curr 포인터를 next로 갈아치웁니다. 내부적으로 switch_mm()을 불러 사용자 메모리 주소 공간의 페이지 테이블(Page Table)을 교체하고, 핵심 하드웨어 매크로인 switch_to()를 통해 CPU의 각종 레지스터 내용물과 스택 포인터를 새 프로세스의 것으로 통째로 갈아 끼웁니다.커널 스레드(Kernel Thread)를 위한 교묘한 최적화: 백그라운드 작업을 전담하는 커널 스레드들은 오직 커널 공간에서만 돌기 때문에 자신만의 전용 사용자 주소 공간 데이터 구조체(mm_struct)를 굳이 가지지 않습니다 (mm = NULL). 스케줄러가 보기에 다음 차례(next)가 마침 커널 스레드라면, 방금 전 쫓겨난 프로세스(prev)가 쓰던 페이지 테이블 공간(active_mm)을 버리지 않고 슬쩍 빌려 씁니다. 이렇게 하면 하드웨어의 무거운 TLB(Translation Lookaside Buffer) 플러시 작업을 통째로 건너뛸 수 있어 시스템 성능이 비약적으로 상승합니다.
코드를 분석하다 보면 매우 신기한 점을 발견하게 됩니다. 하드웨어 레지스터를 바꾸는 switch_to() 매크로 뒤에 적힌 코드들(뒷정리 코드)은, 나중에 쫓겨났던 이 프로세스가 아주 오랜 시간이 지나 스케줄러의 선택을 받아 CPU로 다시 복귀하는 그 찰나에 비로소 이어서 실행됩니다. 두꺼운 책을 읽다 책갈피를 단단히 꽂고 덮어둔 뒤 한참 후 다시 펼쳐 그 문장 직후부터 자연스럽게 읽어 내려가는 것과 완벽히 동일한 원리입니다.
핵심 O/X 퀴즈
1. schedule() 실행 도중 active 큐가 비어있음을 확인하면, active 큐와 expired 큐의 포인터만 맞바꿈으로써 O(1) 시간 복잡도로 새로운 실행 주기를 시작할 수 있다.
2. 런큐에서 가장 우선순위가 높은 프로세스를 골라봤더니 현재 실행 중이던 프로세스와 동일한 녀석이었다면, 시스템의 일관성을 위해 무조건 context_switch()를 한 번 껐다 켜듯 실행해 주어야 한다.
3. 다음 실행할 대상이 유저 주소 공간이 없는 커널 스레드라면, 직전 프로세스의 active_mm을 임시로 빌려 쓰며 값비싼 TLB 플러시를 회피하는 최적화가 적용된다.
멀티프로세서 스케줄링: CPU마다 런큐가 있으면 왜 균형 조정이 필요할까?SMP & SCHEDULING DOMAINS
CPU마다 각자 고유한 런큐를 관리하면 락(lock) 경쟁 지연과 L1/L2 캐시 미스(Cache Miss)를 극적으로 줄일 수 있습니다. 하지만 이 방식에는 치명적인 부작용이 있습니다. 우연히 무거운 프로세스 100개가 CPU 0의 런큐에만 잔뜩 몰려 들어가고, CPU 1, 2, 3은 할 일이 없어 텅 비어 멍하니 놀게 되는 극심한 불균형 사태가 벌어질 수 있습니다. 커널 전체의 자원 활용을 위해 정기적인 런큐 밸런싱(Runqueue Balancing)이 필수적인 이유입니다.
- 고전적인 SMP (Symmetric Multi-Processing)
- 독립된 여러 개의 물리 CPU 코어가 시스템의 단일 메인 메모리(RAM)를 똑같은 거리에서 동등하게 공유하는 전통적 구조입니다.
- Hyper-Threading / SMT
- 단일 물리 CPU 코어 안에 레지스터 등을 복제하여 하드웨어적으로 두 개 이상의 논리적(Logical) 코어처럼 보이게 속이는 기술입니다. 내부 연산 유닛(ALU)과 캐시를 공유하므로 서로 완전히 간섭 없이 독립적이지 않습니다.
- NUMA (Non-Uniform Memory Access)
- 수십 개의 CPU 코어와 메모리 뱅크들을 몇 개의 구역(Node)으로 묶어 배치한 거대한 구조입니다. 내 코어가 속한 Node 안의 로컬 메모리에 접근하는 속도는 대단히 빠르지만, 다른 Node의 원격 메모리에 접근할 때는 네트워크 홉을 타듯 접근 시간이 확연히 느려집니다.
단순 무식하게 놀고 있는 CPU로 아무 프로세스나 닥치는 대로 넘겨버리면 NUMA 환경이나 캐시 정책에서 큰 손해를 봅니다. 그래서 Linux 2.6.7 이후의 현대적 스케줄러는 스케줄링 도메인(Scheduling Domain)이라는 개념을 도입했습니다. 간단히 말해 "부하 균형을 우선적으로 맞춰야 하는 CPU들의 논리적 집합"입니다. 이 도메인들은 하드웨어 구조를 반영하여 양파 껍질처럼 계층적(Hierarchical)으로 구성됩니다.
- 고전적인 2-CPU SMP 환경
- 전체 CPU를 아우르는 단일 최상위 도메인 1개만 존재하며, 그 안에서 각 CPU가 하나의 독립 그룹으로 취급됩니다.
- 2 물리 CPU + 하이퍼스레딩 켬 (총 4 논리 코어)
- 가장 좁은 하위 도메인: 하나의 물리 코어 내부에 속한 두 개의 논리 코어끼리 묶어 캐시를 공유하며 밸런싱을 맞춥니다.
상위 도메인: 물리 코어 덩어리(그룹)들 사이에 부하를 맞추는 넓은 도메인이 덮어씌워 집니다. - 거대한 8-CPU NUMA 머신
- 기본적으로 가장 비용이 싼 도메인 계층(물리 코어 내부)부터 밸런싱을 맞추고, 그 다음 노드 내부, 최후의 수단으로 노드와 노드 사이의 무거운 상위 도메인 밸런싱을 가끔씩 수행합니다. 거리가 멀수록 이동 비용(캐시 손실, 메모리 접근 지연)이 끔찍하므로 이동 빈도를 극단적으로 낮춥니다.
거대한 전국 단위 물류 택배망을 생각해 봅시다. 물량이 넘쳐난다고 서울 택배 기사의 짐을 무턱대고 부산 기사에게 떼어주지 않습니다. 일단 송파구 기사의 물량을 강동구 기사에게 나눠보는 식으로 '가장 가까운 관할 구역(하위 도메인)'끼리 우선 조정합니다. 도저히 답이 안 나올 정도로 수도권 전체가 과부하가 걸렸을 때만, 아주 조심스럽게 타 지역망(상위 NUMA 도메인)의 지원을 받아 물량을 크게 넘깁니다. 스케줄링 도메인도 이동에 따르는 '운송 비용(Cache/Memory Penalty)'이 가장 낮은 범위부터 균형을 맞추도록 설계된 지능적인 시스템입니다.
핵심 O/X 퀴즈
1. 각 CPU마다 런큐를 두면 락 병목 현상과 캐시 미스 오버헤드를 막는 데 유리하지만, 반대급부로 프로세스들이 특정 CPU 런큐에만 집중되어 다른 CPU가 노는 불균형 문제가 발생할 수 있다.
2. NUMA(Non-Uniform Memory Access) 아키텍처는 모든 물리적 메모리에 접근하는 속도가 완벽히 동일하므로, 프로세스를 시스템 내 어느 CPU로 이동시키든 메모리 접근 지연 손해는 발생하지 않는다.
3. 스케줄링 도메인(Scheduling Domain)은 부하 분산을 수행할 때 이동 비용과 토폴로지를 고려하여 계층적(Hierarchical)으로 구성된 논리적인 CPU 집합이다.
rebalance_tick(), load_balance(), move_tasks(): 프로세스를 언제 어떻게 옮길까?LOAD BALANCING INTERNALS
매 타이머 틱의 scheduler_tick() 작업이 끝날 무렵, rebalance_tick() 함수가 호출되며 부하 분산의 필요성을 타진합니다. 이때 현재 CPU가 완전히 텅 비어 노는(idle) 상태라면 다급하므로 밸런싱을 매우 자주 시도하고, 현재 프로세스를 돌리느라 바쁘다면 캐시 오버헤드를 줄이기 위해 아주 드물게 띄엄띄엄 시도합니다.
find_busiest_group()을 호출해 가장 부하가 극심하게 몰려있는 CPU 그룹을 색출합니다.can_migrate_task() 함수를 돌려 까다로운 이주 적격 심사를 통과해야만 선별합니다.TIF_NEED_RESCHED를 세워 당장 실행시켜 줍니다.- 실제 실행 중 (Running) 불가
- 원격 CPU 코어 위에서 지금 이 순간 물리적으로 실행 중인 프로세스는 도저히 중간에 낚아챌 수 없습니다.
- cpus_allowed (CPU Affinity) 제한
- 사용자가 특정 CPU에서만 돌라고 명시해 둔 비트마스크에 지금 훔쳐 오려는 내 로컬 CPU가 허용되어 있지 않다면 강제로 데려올 수 없습니다.
- 캐시의 온도 (Cache Hot)
- 원격 CPU에서 방금 직전까지 신나게 실행되다 잠깐 대기 중인 프로세스는, 원격 CPU의 L1/L2 하드웨어 캐시에 프로세스의 데이터와 코드가 뜨끈뜨끈하게(Hot) 살아있습니다. 당장 데려오면 막대한 캐시 적중 이득을 포기해야 하므로, 스케줄러는 일정 시간이 지나 캐시가 완전히 식어버린(Cold) 녀석들만 우선적으로 옮기려 고집합니다.
위의 복잡한 move_tasks() 심사 기준이 너무 깐깐해서, 분명 노는 CPU가 있는데도 이주가 자꾸 실패하는 경우가 생깁니다. 이런 막다른 골목에서는 최후의 수단으로 마이그레이션 커널 스레드(migration kernel thread)가 무력을 행사합니다. 시스템 부팅 시 각 CPU 코어마다 하나씩 숨어 대기하던 이 특수 스레드는, 극도로 불균형한 상황이 오면 직접 소매를 걷어붙이고 강압적이고 적극적인 프로세스 이주 작업을 대행해 줍니다.
핵심 O/X 퀴즈
1. 현재 CPU가 실행할 프로세스가 없어 노는(idle) 상태라면, rebalance_tick() 로직은 상대적으로 훨씬 높은 빈도로 부하 분산(Load Balancing)을 시도하여 일감을 찾아온다.
2. move_tasks() 함수는 부하를 맞추기 위해서라면 프로세스의 cpus_allowed 마스크나 캐시가 뜨거운(Cache Hot) 상태인지 여부를 완전히 무시하고 당장 런큐로 옮겨버린다.
3. 정규 밸런싱 절차에서 자격 조건 미달로 프로세스 이주가 계속 실패하는 곤란한 상황이 오면, 각 CPU에 배정된 마이그레이션 커널 스레드가 개입하여 보다 강력하게 이주를 집행할 수 있다.
nice(), getpriority(), setpriority(): 일반 프로세스 우선순위 조정SYSCALLS FOR PRIORITY
스케줄링은 기본적으로 커널의 고유 권한이지만, 시스템 콜(System Call)을 통해 유저 스페이스 프로그램이 약간의 힌트나 강제력을 행사할 수 있는 창구를 열어두었습니다. 가장 고전적인 nice() 시스템 콜은 프로세스의 기본 정적 우선순위(static priority)를 위아래로 조정합니다. 단어 뜻 그대로 파라미터로 넘기는 nice 값이 양수로 클수록 자신의 몫을 포기하고 남들에게 더 CPU를 양보하여, 결과적으로 시스템 내에서의 취급(우선순위)은 낮아집니다.
- PRIO_PROCESS
- 지정된 프로세스 ID(PID)와 일치하는 딱 하나의 프로세스만을 정밀 타격하여 우선순위를 가져오거나 덮어씁니다.
- PRIO_PGRP
- 프로세스 그룹 ID(PGID)를 기준으로, 해당 그룹에 속한 여러 개의 프로세스 무리 전체의 우선순위를 일괄 통제합니다.
- PRIO_USER
- 사용자 ID(UID)를 식별자로 삼아, 특정 유저가 구동한 시스템 내의 모든 프로세스의 멱살을 잡고 우선순위를 조정합니다.
setpriority()는 위에서 열거한 범위 대상들의 기본 우선순위를 지정한 값으로 강제 세팅하며, getpriority()는 해당 그룹 안에서 '가장 우선순위가 높은(값이 가장 낮은)' 값을 대표로 뽑아 사용자에게 반환합니다. 여기서 유닉스 C API의 고질적인 골칫거리가 하나 있습니다. 보통 시스템 콜은 내부 오류가 나면 음수(-1)를 던져 에러를 보고하는데, 하필 getpriority()가 정상적으로 돌려줘야 할 진짜 우선순위(nice) 값 자체가 원래 음수(−20 ~ 0)일 수 있다는 점입니다. 이를 해결하기 위해 커널 내부에서는 음수 nice 범위를 비음수 대역으로 평행이동시켜 반환하는 트릭을 사용합니다.
우선순위 조작의 엄격한 권한 규칙: 일반 사용자는 자신의 프로세스가 가진 권한을 스스로 낮추는 방향("내가 조금 덜 쓰고 남들 줄게")으로는 자유롭게 nice()나 setpriority()를 부를 수 있습니다. 그러나 자신의 우선순위를 이기적으로 올리거나, 심지어 다른 사용자가 돌리는 프로세스의 우선순위를 함부로 건드리려면 CAP_SYS_NICE라는 강력한 커널 권한(Capability)이 필요합니다. "스스로 깎아내리는 것은 허용하되, 남의 밥그릇을 뺏어 내 배를 불리려면 관리자 승인을 받아라" — 이것이 유닉스 계열 운영체제의 근본적인 공정성 철학입니다.
핵심 O/X 퀴즈
1. 애플리케이션 안에서 nice 값을 양수로 크게 주어 호출하면, 스케줄러 내에서 부여받는 프로세스의 권한(우선순위)은 일반적으로 깎이고 낮아진다.
2. 리눅스 환경에서 일반 사용자는 특별한 시스템 권한이 없더라도, 자기가 실행한 프로세스에 한해서는 언제든 자유롭게 우선순위를 가장 높은 상태로 격상시킬 수 있다.
3. setpriority() 시스템 콜의 타겟 플래그를 활용하면 특정 PID뿐만 아니라, 특정 프로세스 그룹 전체나 특정 유저가 실행한 전체 프로세스의 우선순위를 한 번에 일괄 변경할 수 있다.
CPU affinity와 실시간 관련 시스템 호출AFFINITY & RT SYSCALLS
CPU 코어 선호도(CPU affinity)는 특정 프로세스가 여러 개의 CPU 코어 중 어느 코어에서만 실행을 허락받을지 강제하는 비트마스크 지도로, 프로세스 구조체의 cpus_allowed 필드에 고스란히 저장됩니다.
- sched_getaffinity / sched_setaffinity
- 현재 프로세스에 걸린
cpus_allowed비트마스크를 확인하거나 강제로 변경합니다. 이 함수를 통해 현재 열심히 돌고 있는 CPU의 비트를 0으로 꺼버리면, 눈치 빠른 마이그레이션 스레드가 개입하여 당장 프로세스를 허용된 다른 CPU 코어로 강제 이주시켜 버립니다. - sched_getscheduler / sched_setscheduler
- 프로세스가 따르는 스케줄링 거시 정책(SCHED_NORMAL, SCHED_FIFO, SCHED_RR) 자체를 통째로 바꾸고 그에 맞는 우선순위를 부여합니다. 일반 프로세스를 절대권력인 실시간(RT) 정책으로 승격시키는 강력한 권한이므로 필히
CAP_SYS_NICE특권이 필요합니다. - sched_getparam / sched_setparam
- 스케줄링 정책의 큰 틀은 그대로 둔 채, 내부의 세부 파라미터(대표적으로 실시간 프로세스의 1~99 우선순위 수치)만을 미세 조정합니다. POSIX 운영체제 표준 규약을 만족시키기 위해 별도로 분리된 인터페이스입니다.
- sched_yield
- 지금 당장 써도 되는 내 CPU 실행 권한을 자발적으로 깔끔하게 양보합니다. 단, 잠드는(sleep) 것은 아니므로 여전히 TASK_RUNNING 상태를 유지합니다. 일반 프로세스가 호출하면 뒤도 안 돌아보고 expired 배열로 추방당하며, 실시간 프로세스가 호출하면 동일 우선순위 대기열의 가장 맨 뒷자리로 가서 다시 차례를 기다립니다.
- sched_get_priority_min / max
- 현재 지정된 스케줄링 정책 내부에서 안전하게 부여할 수 있는 실시간 우선순위의 최소/최댓값 범위를 시스템에 질의합니다. 리눅스 환경에서 실시간 우선순위는 1부터 99까지며, 일반 우선순위와 반대로 최댓값(99) 쪽이 압도적으로 높습니다.
- sched_rr_get_interval
- SCHED_RR 정책으로 돌고 있는 실시간 프로세스에게 커널이 얼마나 긴 타임 퀀텀을 허락하고 있는지 그 구체적인 시간을 조회합니다. 무한 점유가 특징인 SCHED_FIFO 정책에게 이를 물어보면, 커널은 관례상 퀀텀이 없다는 의미로 0을 반환합니다.
실시간 스케줄링 함수들을 남용하는 것은 시스템의 심장에 칼을 꽂는 행위일 수 있습니다. 만약 무한 루프를 도는 버그 덩어리 코드를 작성하고 이를 SCHED_FIFO 정책과 가장 높은 실시간 우선순위로 세팅해 실행해 버리면, 시스템 내의 일반 프로세스(운영체제 핵심 데몬조차도)들은 단 1나노초의 CPU 시간도 받지 못한 채 모조리 굶어 죽고 맙니다. 실시간 권한 설정이 시스템 전체를 즉각적인 마비 상태로 몰아넣을 수 있는 대단히 파괴적이고 막강한 조작이므로, 루트 관리자의 엄격한 통제하에만 허용되는 것입니다.
이제 전체 강의 내용을 요약하며 마무리하겠습니다. 오늘 우리는 리눅스 스케줄러라는 거대하고 정교한 톱니바퀴를 세 개의 큰 층위에서 해부했습니다. 첫째, 거시적인 정책(Policy) 차원에서는 대화형 작업은 빠르게 반응하게 띄워주고, 배치 작업도 영원히 굶지 않게 배려하며, 실시간 프로세스 앞에서는 무조건 길을 비키는 철학을 보았습니다. 둘째, 내부 알고리즘과 자료구조의 층위에서는 각 코어별 런큐, active/expired 큐의 절묘한 스왑 춤, 비트맵을 통한 O(1) 마법 탐색, 수면 시간을 기반으로 한 동적 보너스 부여, 심장박동 같은 scheduler_tick, 교체 사령탑인 schedule(), 그리고 멀티코어 부하를 맞추는 지능적인 밸런싱을 뜯어보았습니다. 마지막으로 시스템 콜의 층위에서는 유저가 아주 제한적이고 통제된 방식으로 시스템 우선순위와 코어 선호도(affinity)의 멱살을 쥐고 흔드는 방법을 다루었습니다.