---
title: "커널에 들어갔다 나오는 값"
description: "페이지 폴트도 스레드 전환도 파일 읽기도 전부 같은 문을 지납니다. 사용자 모드에서 커널 모드로 넘어가는 그 문이 얼마인지, 그리고 돌아온 뒤가 왜 더 비싼지 봅니다."
author: "유니얼"
pubDate: 2026-09-17
category: "programming/os"
tags: ["OS"]
canonical: https://blog.unialgames.com/posts/os-kernel-entry/
---
게임 코드는 파일을 직접 읽지 않습니다. 메모리를 직접 배정받지도 않고, 다음에 어느 스레드가 돌지도 정하지 않습니다.
**그 일들은 전부 커널이 합니다.** 우리 코드는 부탁만 합니다.

부탁할 때마다 **사용자 모드에서 커널 모드로 넘어갔다 돌아옵니다.** 이 왕복에 값이 붙습니다.
그리고 이 문은 생각보다 자주 열립니다.

## 사용자 모드와 커널 모드

CPU 에는 지금 어느 모드로 도는지를 나타내는 상태가 있습니다. 모드에 따라 할 수 있는 일이 다릅니다.

| | 사용자 모드 | 커널 모드 |
| --- | --- | --- |
| 일반 연산 | 가능 | 가능 |
| 페이지 테이블 손대기 | **불가** | 가능 |
| 장치에 직접 명령 | **불가** | 가능 |
| 남의 프로세스 메모리 | **불가** | 가능 |

주소를 번역하는 표를 프로세스가 못 고치는 이유가 이것입니다.
표를 고치려면 커널 모드여야 하고, 우리 코드는 사용자 모드로 돕니다.

그래서 메모리를 더 달라거나 파일을 열어 달라는 일은 전부 **부탁**이 됩니다.

## 문을 두드리는 세 가지 방법

이 문을 지나는 길은 셋뿐입니다. 셋 다 결과는 같습니다. 커널 코드가 돌고, 끝나면 돌아옵니다.

| 무엇 | 누가 부르나 | 예 |
| --- | --- | --- |
| 시스템 콜 | **우리 코드가** 스스로 | 파일 읽기, 메모리 요청, 시간 묻기 |
| 예외 | **CPU 가** 처리를 못 해서 | 페이지 폴트, 0 으로 나누기 |
| 인터럽트 | **장치가** 일이 끝나서 | 저장 장치 읽기 완료, 타이머, 입력 |

페이지 폴트가 두 번째 줄입니다. 프로그램이 부른 적이 없는데 커널 코드가 도는 이유가 이것입니다.
CPU 가 번역을 못 해서 대신 문을 두드린 것입니다.
그쪽에서 커널이 무슨 일을 하는지는 [페이지 폴트는 오류가 아니지만 프레임은 흔든다](/posts/os-virtual-memory-page-fault/)에 적었습니다.

**셋을 따로 배우면 셋이지만, 비용으로 보면 하나입니다.** 전부 같은 문을 지납니다.

## 문을 넘는 값

<x-steps>
<script type="application/json">
{
  "steps": [
    { "tag": "1", "title": "넘어간다", "text": "지금 레지스터 상태를 저장하고 모드를 바꿉니다. 커널 쪽 스택으로 갈아탑니다." },
    { "tag": "2", "title": "커널이 일한다", "text": "부탁받은 일을 합니다. 파일이면 읽고, 메모리면 배정하고, 폴트면 프레임을 붙입니다." },
    { "tag": "3", "title": "돌아온다", "text": "모드를 되돌리고 저장해 둔 상태를 복원합니다. 여기까지가 눈에 보이는 값입니다." },
    { "tag": "4", "title": "한동안 느리다", "text": "커널 코드가 캐시와 분기 예측기를 차지하고 나갔습니다. 돌아온 우리 코드는 식은 상태에서 다시 시작합니다." }
  ]
}
</script>
</x-steps>

1번과 3번, 그러니까 **문을 넘는 것 자체는 수백 나노초** 규모입니다.
아무 일도 안 하는 가장 단순한 시스템 콜을 재도 그렇습니다.

그런데 이 값이 몇 년 사이에 올랐습니다. 투기적 실행을 악용하는 취약점이 알려진 뒤로
**커널에 드나들 때 추가로 하는 일이 생겼기 때문**입니다. 같은 기계에서 완화 조치를 켜고 끄고 재면
두세 배까지 차이가 납니다.

그중 하나는 앞의 이야기와 직접 닿아 있습니다. 커널의 주소 공간을 사용자 주소 공간에서 떼어 놓는 방식인데,
**문을 넘을 때마다 번역의 기준이 되는 표를 바꾸는 셈**이라 TLB 에 담아 둔 번역이 흔들립니다.
시스템 콜 하나가 작은 주소 공간 전환처럼 되어 버린 것입니다.

## 돌아온 뒤가 더 비쌀 수 있다

4번이 눈에 안 보이는 값입니다.

커널 코드도 코드라서 캐시를 씁니다. 우리 데이터가 있던 자리를 밀어내고 자기 것을 올립니다.
돌아왔을 때 우리 코드는 **자기가 쓰던 캐시를 다시 채워야** 합니다.

그래서 시스템 콜 하나의 값은 "문을 넘는 시간" 으로 끝나지 않습니다.
돌아온 뒤 한동안 느려진 시간까지 합해야 실제 값이 됩니다.
**호출 하나하나는 싸 보이는데 합쳐 놓으면 커지는 이유가 이것입니다.**

<x-calc>
<script type="application/json">
{
  "label": "한 프레임에 몇 번이면",
  "inputs": [
    { "id": "n", "label": "한 프레임의 호출 횟수", "options": [100, 1000, 10000], "default": 1000, "suffix": "회" },
    { "id": "c", "label": "호출 하나의 값", "options": [["완화 조치 없던 시절", 80], ["요즘 기기", 240]], "default": 240 }
  ],
  "bars": [
    { "label": "문을 넘는 데 쓴 시간", "expr": "n * c / 1000000", "unit": "ms", "accent": true },
    { "label": "한 프레임 예산", "expr": "16.6", "unit": "ms" }
  ],
  "verdict": { "expr": "bars[0] > 1.66", "yes": "프레임의 십분의 일을 문 넘는 데 씀", "no": "티가 안 나는 크기" },
  "note": "문을 넘는 값만 센 것입니다. 돌아온 뒤 캐시가 식어서 느려지는 시간은 여기 안 들어가 있습니다."
}
</script>
</x-calc>

## 게임에서 자주 두드리는 문

한 프레임에 만 번은 과장 같지만, 모르고 쓰면 금방 갑니다.

| 무엇 | 왜 문을 두드리나 |
| --- | --- |
| 로그 출력 | 한 줄마다 쓰기 요청입니다. 루프 안에서 찍으면 그 루프가 통째로 느려집니다 |
| 파일 읽기 | 한 번에 조금씩 여러 번 읽으면 횟수만큼 두드립니다 |
| 시간 재기 | 자주 부르는 자리에서 매번 현재 시각을 물으면 쌓입니다 |
| 락 경합 | 경합이 없으면 사용자 모드에서 끝나지만, 기다려야 하면 커널이 스레드를 재웁니다 |
| 네트워크 | 패킷 하나씩 보내면 하나마다 두드립니다 |

재워진 스레드가 다시 올라올 때 붙는 값은 [프로세스와 스레드, 그리고 갈아 끼우는 값](/posts/os-context-switch/)에 적었습니다.

대응은 셋 다 같은 모양입니다.

- **모아서 한 번에.** 여러 번 쓸 것을 버퍼에 쌓았다가 한 번에 내보냅니다
- **프레임 밖으로.** 로딩 구간이나 별도 스레드로 옮깁니다
- **아예 안 부르기.** 프레임 시작에 한 번 받아 둔 값을 그 프레임 동안 재사용합니다

시간 재기는 요즘 기계에서 문을 안 넘고 끝나기도 합니다.
커널이 시간 값을 사용자 쪽에서 읽을 수 있는 자리에 올려 두는 방식이 있어서 그렇습니다.
**그런 예외가 있다는 것 자체가 이 문이 얼마나 비싼지를 보여줍니다.**

## 정리

- 우리 코드는 사용자 모드로 돕니다. 페이지 테이블도 장치도 직접 못 건드려서 **커널에 부탁해야** 합니다
- 그 문을 지나는 길은 셋입니다. **우리가 부르는 시스템 콜, CPU 가 부르는 예외, 장치가 부르는 인터럽트**
- 페이지 폴트는 두 번째입니다. 프로그램이 부른 적 없는데 커널이 도는 이유가 그것입니다
- 문을 넘는 값은 수백 나노초 규모이고, 투기적 실행 취약점 완화가 들어간 뒤로 **두세 배까지 올랐습니다**
- **돌아온 뒤가 더 비쌀 수 있습니다.** 커널이 캐시를 차지하고 나가서 우리 코드가 식은 채로 다시 시작합니다
- 그래서 대응은 하나로 모입니다. **덜 두드리기.** 모아서 한 번에 보내고, 프레임 밖으로 옮기고, 받아 둔 값을 재사용합니다