복잡한 커널 개념, 비유를 통해 시작하기
학교 급식실을 떠올려 보세요. 큰 솥에 밥과 국이 가득 들어 있습니다. 그런데 만약 아이들이 저마다 솥에 직접 손을 넣어 밥을 퍼간다면 어떻게 될까요? 서로 밀치다 데이고, 어떤 아이는 두 번 먹고 어떤 아이는 굶고, 솥은 금세 엉망이 됩니다.
그래서 솥(=하드웨어)은 배식 아주머니 한 분만 만질 수 있게 정합니다. 아이들(=프로그램)은 솥에 손을 못 댑니다. 대신 "밥 주세요"라고 부탁만 합니다. 그러면 아주머니가 공평하게 순서를 정해 한 명씩 나눠줍니다. 이 배식 아주머니가 바로 커널입니다.

왜 이렇게 할까요? 한 명만 솥을 만지게 하면 안전(서로 안 싸움)하고, 공평(순서대로 나눔)하고, 질서(엉키지 않음)가 생기기 때문입니다. 컴퓨터에서 이 세 가지가 무너지면 프로그램이 서로의 메모리를 짓밟고, CPU를 독차지하고, 결국 컴퓨터가 멈춥니다.
진지하게 보는 커널의 정의와 위치
이제, 비유를 걷어내고 진짜 정의를 봅시다.
커널은 운영체제(OS)의 가장 핵심(core, 그래서 이름이 '알맹이'라는 뜻의 kernel)입니다. 운영체제 전체가 다 커널은 아닙니다. 우리가 보는 바탕화면, 설정 앱, 파일 탐색기 같은 건 커널 바깥의 응용 프로그램이고, 커널은 그 아래에서 하드웨어와 직접 대화하는 가장 안쪽 층입니다.
컴퓨터를 켜면(부팅) 부트로더가 가장 먼저 커널을 메모리에 올리고, 그 뒤로 컴퓨터가 꺼질 때까지 커널은 상주하며 모든 것을 통제합니다.

💡 인사이트
"OS = 커널 + 그 위의 도구들"입니다. 윈도우의 커널은 NT 커널, 맥/iOS는 XNU, 리눅스/안드로이드는 Linux 커널입니다. 안드로이드폰 안에도 리눅스 커널이 들어 있다는 게 이 구분 덕에 이해됩니다.
커널의 역할 ①·② — 프로세스 관리와 메모리 관리
커널이 실제로 하는 일은 크게 네 가지입니다. 먼저 둘.
① 프로세스 관리 — CPU 시간을 나눠준다
CPU 코어 하나는 한 순간에 딱 하나의 명령만 실행합니다. 그런데 우리는 음악을 들으며 웹서핑을 합니다. 어떻게? 커널이 아주 빠르게(수 ms 단위로) 프로그램을 번갈아 가며 CPU를 넘겨주기 때문입니다. 이걸 스케줄링이라 하고, 프로그램을 바꿔 끼우는 순간의 작업을 문맥 전환(Context switching)이라 합니다.
② 메모리 관리 — 칸을 배분하고 침범을 막는다
프로그램마다 "이만큼의 메모리를 쓰겠다"고 커널에 요청합니다. 커널은 빈 메모리를 찾아 배정하고, 한 프로그램이 다른 프로그램의 메모리 칸을 침범하지 못하게 막습니다. 큰 컴퓨터에서는 MMU라는 하드웨어가 이 격리를 강제합니다. 침범을 시도하면 그 유명한 Segmentation fault가 납니다.
소형 MCU(예: Cortex-M0/M3)에는 MMU가 없습니다. 그래서 메모리 격리가 약하거나 없고, 잘못된 포인터 하나가 전체 시스템을 망가뜨릴 수 있습니다. 임베디드에서 메모리 버그가 특히 무서운 이유입니다. (일부 칩은 MMU 대신 거친 MPU를 제공)
커널의 역할 ③·④ — 파일시스템과 장치 드라이버
③ 파일시스템 — 0과 1 덩어리를 '파일'로 보여준다
디스크나 플래시 메모리에는 사실 그냥 0과 1의 거대한 배열만 있습니다. 어디부터 어디까지가 "사진.jpg"인지는 약속이 필요합니다. 커널의 파일시스템 계층이 이 약속(FAT32, ext4, NTFS 등)을 해석해, 우리에게 "폴더와 파일"이라는 익숙한 모습으로 보여줍니다. open / read / write / close 같은 요청을 디스크의 실제 블록 위치 계산으로 번역하는 것이죠.
④ 장치 드라이버 — 하드웨어와의 통역사
키보드, 마우스, 화면, CAN 컨트롤러, 온도 센서… 제조사마다 신호 방식이 전부 다릅니다. 드라이버는 각 장치를 다루는 방법을 아는 전문 통역사로, 커널에게 "이 장치는 이렇게 말 걸면 됩니다"를 제공합니다. 그래서 응용 프로그램은 키보드가 USB든 PS/2든 신경 쓰지 않고 똑같이 "키 입력"만 받으면 됩니다.
임베디드 개발자가 매일 만지는 HAL(STM32 등)도 일종의 얇은 드라이버 계층입니다. HAL_UART_Transmit()을 부르면 그 안에서 UART 레지스터를 직접 두드립니다 — "통역사" 역할 그대로입니다.
커널 모드 vs 유저 모드 — '솥은 아주머니만~'의 정체를 파악해보자.
앞의 비유에서 "솥은 아주머니만 만진다"던 규칙. 이게 하드웨어로 강제됩니다.
현대 CPU는 최소 두 단계의 권한 레벨을 하드웨어로 가집니다.
| 커널 모드 (privileged) | 유저 모드 (unprivileged) | |
| 누가 도나 | 커널 코드, 드라이버, 인터럽트 핸들러 | 일반 응용 프로그램 |
| 권한 | 모든 CPU 명령·전 메모리·주변장치 직접 접근 | 제한된 자기 메모리만, 특권 명령 금지 |
| 위반 시 | — | 예외(fault) 발생 → 커널이 강제 종료 |
왜 나눌까요? 버그나 악성코드가 있는 프로그램이 시스템 전체를 무너뜨리지 못하게 격리하기 위해서입니다. 유저 모드 프로그램이 디스크를 직접 지우려 하거나 다른 앱 메모리를 읽으려 하면, CPU가 즉시 막고 커널에게 넘깁니다. 하드웨어가 강제하므로 소프트웨어로 우회할 수 없습니다.
소형 Cortex-M MCU도 Thread/Handler 모드와 Privileged/Unprivileged 구분이 하드웨어로 존재합니다. 다만 MMU가 없어 격리 수준이 거칠고, 대부분의 소형 RTOS 프로젝트는 모드 분리 없이 전부 privileged로 돌립니다. 안전인증(IEC 61508, ISO 26262)에서 태스크 격리를 증명해야 할 때 비로소 MPU 기반 분리가 의미를 가집니다.
시스템 콜 — 부탁하는 공식 창구
유저 모드 프로그램이 솥(하드웨어)이 필요할 때, 어떻게 아주머니를 부를까?
유저 프로그램은 하드웨어를 직접 못 만지므로, 커널에게 "이 일 좀 해주세요"라고 공식 요청을 보냅니다. 이게 시스템 콜입니다. 파일 열기, 메모리 더 달라기, 네트워크 보내기 — 전부 시스템 콜로 들어갑니다.
시스템 콜이 일어나는 순간 CPU가 유저 모드에서 커널 모드로 잠깐 승격되고, 커널이 일을 처리한 뒤 다시 유저 모드로 내려오면서 결과를 돌려줍니다. 이 전환에는 비용(수십~수백 사이클)이 들기 때문에, 성능이 중요한 코드는 시스템 콜 횟수를 줄이는 게 최적화 포인트가 됩니다.

한 줄 정리
시스템 콜은 유저 모드와 커널 모드 사이의 '유일하게 허가된 문'이다. 모든 하드웨어 요청은 이 문을 통과한다.
임베디드 관점 — 베어메탈 vs RTOS 커널
여기서 임베디드 개발자에게 진짜 중요한 갈림길이 나옵니다.
지금까지 설명한 건 PC·서버급 OS 커널입니다. 그런데 STM32 같은 MCU 세계에는 두 갈래가 있습니다.
베어메탈 — 커널이 없다 (내가 곧 커널)
베어메탈은 OS가 전혀 없습니다. main()의 while(1) 슈퍼루프 안에서 함수를 순서대로 돌리고, 급한 일은 인터럽트로 처리합니다. 스케줄러도, 메모리 보호도 없습니다. 대신 단순하고, 동작이 결정론적이며, 검증과 인증이 쉽습니다. 양산 베어메탈 펌웨어가 안전인증 제품에서 선호되는 이유입니다.
RTOS 커널 — 작은 스케줄러를 얹는다
RTOS 커널은 여러 태스크를 우선순위에 따라 번갈아 돌리는 작은 스케줄러를 제공합니다. PC 커널의 축소판이라고 보면 됩니다 — 프로세스 대신 태스크, 시스템 콜 대신 커널 API(xQueueSend 등)를 씁니다.
| 베어메탈 | RTOS 커널 | |
| 구조 | while(1) 슈퍼루프 + ISR | 다중 태스크 + 스케줄러 |
| 전환 방식 | 협력적 (함수가 끝나야 다음) | 선점 가능 (타이머가 강제 전환) |
| 최악 응답시간 | 루프 전체 길이 | 상위 우선순위 태스크들의 합 |
| 메모리/오버헤드 | 최소 | 태스크별 스택 + 문맥 전환 비용 |
| 적합 상황 | 단순·타이밍 느슨·인증 중시 | 복잡·응답성 보장 필요 |
"동시에 도는 것처럼 보이느냐"는 둘 다 가능합니다. 진짜 갈림길은 "우선순위 높은 작업의 응답 시간을 하드웨어적으로 보장해야 하는가"입니다. 그게 필요하면 RTOS, 아니면 베어메탈이 더 싸고 안전합니다.
FreeRTOS 스케줄러의 실제
RTOS 커널의 심장인 스케줄러가 실제로 어떻게 도는지 봅시다.
FreeRTOS는 기본적으로 우선순위 기반 선점형(preemptive priority) 스케줄러입니다. 규칙은 단순합니다: 실행 가능한 태스크 중 우선순위가 가장 높은 것을 항상 실행한다. 같은 우선순위가 여럿이면 라운드 로빈으로 시간을 나눕니다.
전환의 방아쇠는 SysTick 타이머 인터럽트입니다. 매 tick마다 커널이 깨어나 "지금 더 급한 태스크가 준비됐나?"를 확인하고, 그렇다면 즉시 문맥 전환합니다.

// FreeRTOS 태스크 생성 — 숫자가 클수록 높은 우선순위
xTaskCreate(vSensorTask, "sensor", 256, NULL, 3, NULL); // 높음
xTaskCreate(vLogTask, "log", 256, NULL, 1, NULL); // 낮음
void vSensorTask(void *p){
for(;;){
read_sensor();
vTaskDelay(pdMS_TO_TICKS(10)); // 10ms마다 — 그동안 Low가 CPU 사용
}
}
우선순위만 잘 줬다고 끝이 아닙니다. 우선순위 역전(저우선 태스크가 mutex를 쥐어 고우선 태스크를 막음), 스택 오버플로(태스크별 스택 부족), ISR에서 잘못된 API 호출(...FromISR 버전을 안 씀) — 이 셋이 RTOS 입문자가 가장 자주 밟는 지뢰입니다.