목차17개 항목
출처와 참고
「시스템 콜과 라이브러리 함수」·「man 페이지」 절은 팔만코딩경(42Seoul 커뮤니티 위키)의 「write는 라이브러리 함수가 아니다? 커널과 시스템 콜에 대해」, 「man」, 「[unix] 나만의 manpage 작성하기」를 바탕으로 보강했다. 동기식 입출력의 구현 방식과 커널 주소 공간은 고무오리 CS 스터디의 KOCW 반효경 「운영체제」 강의 정리를 바탕으로 보강했다. 「인터럽트」 절의 동기·비동기 구분과 예외 분류, 「모드 비트」 절의 슈퍼바이저 플래그와 시스템 콜 관찰 방법은 강민철의 「혼자 공부하는 컴퓨터 구조+운영체제」 강의(한빛미디어)를 바탕으로 보강했다. 「장치 제어기와 장치 드라이버」 절의 드라이버 추상화 설명은 CrashCourse의 「Crash Course Computer Science」 강의를 바탕으로 보강했다. 「시스템 콜은 번호로 구분한다」 절은 fru1tworld/cs-basic 모의 면접 질문 목록을 점검해 빠진 개념을 보강했다. 「커널 입출력 서브시스템」 절과 블로킹·논블로킹·비동기 비교는 고려사이버대학교 「운영체제이론 및 실습」(2024-1학기) 강의 내용으로 보강했다.
운영체제가 어떻게 하드웨어를 통제하는지 이해하려면 먼저 컴퓨터 시스템의 구성 요소와, 이들이 인터럽트로 CPU와 소통하는 방식을 알아야 한다. 현대 운영체제는 "인터럽트에 의해 구동된다"고 할 만큼 인터럽트가 핵심이다.
구성 요소 한눈에 보기
| 요소 | 역할 |
|---|---|
| CPU · 레지스터 | 메모리의 명령어를 순서대로 실행. 레지스터는 메모리보다 빠른 저장 공간 |
| 모드 비트 | 지금 CPU에서 실행 중인 것이 OS인지 사용자 프로그램인지 구분 |
| 인터럽트 라인 | 키보드 입력·디스크 완료 같은 외부 이벤트를 CPU에 알림 |
| 타이머 | 한 프로그램이 CPU를 독점하지 못하게 함 |
| 장치 제어기 + 로컬 버퍼 | 각 I/O 장치를 관리하는 작은 CPU와 그 데이터 버퍼 |
| DMA 제어기 | CPU 대신 로컬 버퍼의 데이터를 메모리로 직접 복사 |
| 메모리 제어기 | CPU와 DMA가 동시에 메모리에 접근할 때 충돌 조정 |
디스크는 저장 장치이면서 동시에 입출력 장치이기도 하다.
모드 비트 (Mode bit)
사용자 프로그램의 잘못된 동작이 다른 프로그램이나 OS를 망가뜨리지 않도록 하드웨어가 두 가지 실행 모드를 지원한다.
1사용자 모드: 사용자 프로그램 실행0커널 모드(모니터·시스템 모드): OS 코드 실행
보안에 영향을 주는 명령은 특권 명령으로 지정되어 커널 모드에서만 실행할 수 있다. 사용자 모드에서 특권 명령을 실행하려 하면 CPU가 예외를 일으켜 명령이 실행되지 않는다. 교재의 CPU 모델에서는 플래그 레지스터의 슈퍼바이저 플래그가 이 모드 비트 역할을 하고, 실제 x86에서는 CS 레지스터에 담긴 현재 특권 수준(CPL, 링 0~3)으로 표시한다. 인터럽트나 예외가 발생하면 하드웨어가 모드 비트를 0으로 바꾸고, OS는 사용자 프로그램에 CPU를 넘기기 직전에 다시 1로 바꾼다. 이중 모드가 왜 필요한지와 시스템 호출의 입문 설명은 운영체제 개요에 있고, 여기서는 그 메커니즘을 하드웨어 요소 단위로 본다.
타이머 (Timer)
무한 루프 같은 이유로 한 프로그램이 CPU를 계속 쥐고 있는 것을 막는다. OS는 CPU를 넘겨줄 때 타이머에 아주 짧은 시간을 설정하고, 시간이 다 되면 타이머 인터럽트로 제어권을 되찾는다. 시분할 시스템의 기반이다.
장치 제어기와 장치 드라이버
- 장치 제어기(device controller): 각 I/O 장치를 통제하는 일종의 작은 CPU(하드웨어). 제어·상태 레지스터와 로컬 버퍼를 가진다. 실제 I/O는 장치와 로컬 버퍼 사이에서 일어나고, 끝나면 인터럽트로 CPU에 알린다.
- 장치 드라이버(device driver): OS 코드 중 장치별 처리 루틴(소프트웨어). 제조사·모델마다 다른 하드웨어 세부를 드라이버가 감추고 OS는 표준화된 입출력 API만 내놓는다. 그래서 응용 프로그램은 "출력하라"는 같은 호출로 어떤 프린터에든 출력하고, 장치 모델별 코드를 직접 짜지 않는다. 드라이버가 들어가는 입출력 계층 전체는 아래 「커널 입출력 서브시스템」에 있다.
인터럽트
인터럽트가 들어오면 CPU는 현재 명령어를 마친 뒤 PC와 레지스터를 저장하고 인터럽트 처리 루틴으로 넘어간다. 하드웨어 쪽 처리 순서는 입출력 구조에 있다.
| 종류 | 발생 원인 |
|---|---|
| 하드웨어 인터럽트 | 장치 제어기, 타이머 등 하드웨어 |
| 트랩(소프트웨어 인터럽트): 예외 | 0으로 나누기, 잘못된 메모리 접근 등 프로그램 오류 |
| 트랩: 시스템 콜 | 사용자 프로그램이 커널 함수를 호출 |
CPU 쪽에서 보면 명령어를 실행하다 그 명령어 때문에 생기는 예외는 동기 인터럽트, 실행 중인 명령어와 상관없는 시점에 장치가 보내는 하드웨어 인터럽트는 비동기 인터럽트다. 예외는 처리한 뒤 어디서 다시 시작하는지에 따라 다시 나뉜다. 이때의 트랩은 위 표보다 좁은 뜻이다.
| 예외 | 처리 뒤 재개 위치 | 예 |
|---|---|---|
| 폴트(fault) | 예외를 낸 명령어를 다시 실행 | 페이지 폴트. 페이지를 메모리에 올린 뒤 같은 명령어를 재시도한다 |
| 트랩(trap) | 예외를 낸 명령어의 다음 명령어 | 디버거 중단점, 시스템 콜 명령 |
| 중단(abort) | 재개하지 못하고 프로그램을 끝낸다 | 복구할 수 없는 하드웨어 오류 |
- 인터럽트 벡터: 인터럽트 종류별로 처리 루틴의 주소를 담은 테이블
- 인터럽트 처리 루틴(ISR, 인터럽트 핸들러): 실제로 인터럽트를 처리하는 커널 함수
인터럽트 우선순위(데이지 체인, 마스크 레지스터)와 DMA·IOP도 같은 노트에 있다.
시스템 콜과 입출력
모든 입출력 명령은 특권 명령이다. 그렇다면 사용자 프로그램은 어떻게 I/O를 할까?
- 사용자 프로그램이 시스템 콜로 OS에 I/O를 요청한다.
- 트랩이 발생해 인터럽트 벡터의 해당 위치로 이동하고, 모드가 커널 모드로 바뀐다.
- OS가 올바른 요청인지 확인한 뒤 I/O를 수행한다.
- 완료되면 제어권을 시스템 콜 다음 명령으로 돌려준다.
시스템 콜은 번호로 구분한다
커널 진입 명령은 하나뿐이므로, 어떤 시스템 콜인지는 함수 이름이 아니라 번호로 알린다. 커널은 시스템 콜 번호를 인덱스로 하는 표(Linux의 sys_call_table)를 두고, 트랩이 들어오면 정해진 레지스터의 번호로 처리 함수를 찾아 부른다.
| Linux 아키텍처 | 진입 명령 | 번호 | 인자 | 반환값 |
|---|---|---|---|---|
| x86-64 | syscall | rax | rdi, rsi, rdx, r10, r8, r9 | rax |
| ARM64 | svc #0 | x8 | x0~x5 | x0 |
- 같은 시스템 콜도 아키텍처마다 번호가 다르다. x86-64 Linux에서
read는 0,write는 1이다. C에서는<sys/syscall.h>의SYS_write같은 상수로 쓰고, 전체 표는 커널 소스의syscall_64.tbl에 있다. 과거 32비트 x86은int 0x80소프트웨어 인터럽트로 진입했다. - 실패하면 커널은 음수 오류 번호(
-ENOENT등)를 돌려주고, libc 래퍼가 이를errno에 넣은 뒤 -1을 반환한다. - 래퍼 함수가 없는 시스템 콜도
syscall(SYS_gettid)처럼 번호로 직접 부를 수 있다. - 한번 정한 번호는 바꾸지 않는다. 바꾸면 이미 컴파일된 프로그램이 엉뚱한 시스템 콜을 부르게 되므로, 사용자 공간 ABI를 깨지 않는다는 Linux 원칙의 한 예다.
동기식 vs 비동기식 입출력
- 동기식(synchronous): I/O가 끝날 때까지 기다렸다가 제어가 사용자 프로그램으로 넘어간다.
- 비동기식(asynchronous): I/O를 시작만 시켜 두고 즉시 제어가 돌아와 다른 일을 한다.
어느 쪽이든 I/O가 끝나면 장치 제어기가 인터럽트로 알린다. 동기식은 기다리는 동안 CPU를 어떻게 쓰느냐에 따라 두 가지로 구현할 수 있다.
| 동기식 구현 | 기다리는 동안 | 문제 |
|---|---|---|
| 요청한 프로그램이 CPU를 쥔 채 기다림 | CPU가 아무 일도 하지 않는다 | CPU가 낭비되고, 매 순간 I/O를 하나만 진행할 수 있다 |
| 요청한 프로그램을 blocked로 두고 CPU를 다른 프로그램에 줌 | 다른 프로그램이 CPU를 쓴다 | 없음. 여러 장치의 I/O가 동시에 진행되므로 현대 OS가 쓰는 방식이다 |
비동기식에서도 I/O 결과를 써야 하는 지점에 이르면 결국 기다려야 하지만, 결과가 필요 없는 쓰기 요청 같은 경우에는 기다리지 않고 바로 다음 일을 한다.
응용 프로그램이 시스템 콜에서 보는 동작으로 나누면 세 가지다.
| 방식 | 데이터가 아직 없을 때 |
|---|---|
| 블로킹 | 커널이 프로세스를 대기 큐로 옮기고, 끝나면 준비 큐로 돌려놓는다. 프로그램이 가장 단순하다 |
| 논블로킹 | 기다리지 않고 지금까지 된 만큼(없으면 EAGAIN)을 바로 돌려준다. 다시 시도하는 코드를 직접 짠다 |
| 비동기 | 요청만 걸어 두고 바로 돌아오며, 완료되면 커널이 알려 준다 |
논블로킹 fd 여러 개를 한꺼번에 감시하는 select·poll·epoll은 UNIX 파일 입출력에, 애플리케이션 수준의 비동기·논블로킹 구분은 동시성·병렬·비동기·논블로킹과 동시성 모델에 있다.
DMA (Direct Memory Access)
바이트마다 인터럽트를 받아 CPU가 데이터를 옮기면 오버헤드가 너무 크다. DMA 제어기는 장치 버퍼의 내용을 CPU 개입 없이 블록 단위로 메모리에 복사하고, 블록 전송이 끝났을 때 한 번만 인터럽트를 건다. 버스 요청·승인 절차와 입출력 프로세서(IOP)까지 포함한 하드웨어 동작은 입출력 구조에 있다.
I/O 장치에 주소를 붙이는 두 방식(격리형 I/O와 메모리 맵 I/O)의 비교, 폴링·인터럽트·DMA 중 무엇을 고를지도 입출력 구조를 참고한다.
커널 입출력 서브시스템
입출력 장치는 CPU보다 훨씬 느리고 종류마다 데이터 단위와 제어 방법이 다르다. 운영체제의 입출력 시스템은 이 차이를 숨겨 응용 프로그램이 디스크든 키보드든 open·read·write·close로 다루게 하고, 느린 장치 때문에 CPU가 노는 시간을 줄인다. 그래서 장치에 공통인 부분과 장치마다 다른 부분을 층으로 나눈다.
flowchart TB app["응용 프로그램<br>read(fd, buf, n)"] kio["커널 입출력 서브시스템<br>VFS, 페이지 캐시, 입출력 스케줄러, 버퍼, 권한 검사"] drv["장치 드라이버<br>장치별 코드: driver_read(), driver_write(), ioctl 처리"] ctl["장치 컨트롤러<br>상태·제어·데이터 레지스터, 내부 버퍼"] dev["장치<br>디스크, 키보드, NIC ..."] app ---|"시스템 콜 경계"| kio --- drv ---|"하드웨어 경계"| ctl --- dev
- 커널 입출력 서브시스템은 모든 장치에 공통인 일(같은 API, 스케줄링, 버퍼와 캐시, 오류 처리, 보호)을 한다.
- 장치 드라이버는 서브시스템이 정한 함수 표(
open,read,write,ioctl…)를 장치에 맞게 채운다. 위쪽 코드는 장치를 몰라도 되고, 새 장치를 지원하려면 드라이버만 더하면 된다. - 장치 컨트롤러는 CPU가 레지스터에 쓴 비트대로 장치를 움직이고, 기계적인 타이밍과 신호 형식을 처리한다.
장치의 세 분류
| 분류 | 단위와 접근 | 예 | Linux에서 |
|---|---|---|---|
| 블록 장치 | 고정 크기 블록, 임의 위치 접근(seek) | 디스크, SSD | ls -l /dev에서 b. 파일 시스템이 그 위에 올라간다 |
| 문자 장치 | 바이트 스트림, 대개 순차 접근 | 키보드, 마우스, 직렬 포트, 터미널 | ls -l /dev에서 c |
| 네트워크 장치 | 패킷, 소켓 인터페이스 | NIC | /dev에 없고 ip link로 본다 |
블록 장치만 페이지 캐시와 입출력 스케줄러를 거치고, 문자 장치는 대개 드라이버로 바로 간다. 표준 연산으로 표현하기 어려운 장치별 기능(터미널 속도, 디스크 형상 질의)은 ioctl이라는 비상구로 넘긴다.
서브시스템이 제공하는 서비스
| 서비스 | 하는 일 |
|---|---|
| 입출력 스케줄링 | 요청 순서를 바꿔 장치 효율과 공정성을 높인다. 디스크는 디스크 관리와 스케줄링 |
| 버퍼링 | 장치와 응용 사이에서 데이터를 잠시 담아 속도·전송 단위 차이를 흡수한다(입출력 버퍼링) |
| 캐싱 | 자주 쓰는 데이터의 사본을 메모리에 두어 장치 접근 자체를 줄인다. 파일 데이터는 페이지 캐시(파일 시스템 구현) |
| 스풀링 | 한 번에 한 작업만 받는 느린 장치(프린터) 앞에 디스크를 큰 대기열로 둔다. 여러 프로세스가 동시에 출력해도 섞이지 않고 차례로 나간다 |
| 장치 예약 | 테이프처럼 공유할 수 없는 장치를 한 프로세스에 독점 할당한다 |
| 오류 처리 | 일시적 오류는 재시도하고, 실패하면 errno로 알린다 |
| 보호 | 입출력 명령은 특권 명령이라 사용자 프로그램은 시스템 콜로만 장치에 닿는다 |
버퍼는 데이터의 유일한 사본일 수 있고 캐시는 원본이 따로 있는 사본이다. 페이지 캐시는 두 역할을 함께 한다. write()가 돌아오면 커널이 그 시점의 내용을 자기 버퍼로 복사해 두었으므로 응용은 버퍼를 곧바로 다시 써도 되지만, 디스크에 실제로 써졌다는 뜻은 아니다. 그것이 필요하면 fsync를 부른다.
입출력 성능을 높이는 원칙
실제 프로그램은 대부분 계산보다 입출력을 기다리는 시간이 길다. 그래서 연산을 다듬는 것보다 입출력 경로를 줄이는 쪽이 효과가 크다.
| 원칙 | 예 |
|---|---|
| 문맥 교환을 줄인다 | 작은 write 여러 번 대신 버퍼에 모아 한 번에(stdio 버퍼, writev) |
| 메모리 복사를 줄인다 | 파일을 소켓으로 보낼 때 read+write(사용자 공간을 거쳐 복사가 늘어남) 대신 sendfile로 커널 안에서 바로 보낸다 |
| 인터럽트 빈도를 줄인다 | 큰 블록 전송, NIC의 인터럽트 결합(coalescing), 부하가 높을 때 폴링 |
| CPU 대신 하드웨어가 옮기게 한다 | DMA, 체크섬·TCP 분할을 NIC에 맡기는 오프로드 |
| 병목을 옮겨 가며 균형을 맞춘다 | CPU, 메모리, 버스, 장치 중 하나만 빠르게 해도 전체는 가장 느린 곳에 맞춰진다 |
저장장치 계층과 프로그램 실행
- Primary(레지스터, 캐시, 메인 메모리): CPU가 바이트 단위로 직접 접근해 실행할 수 있다.
- Secondary(디스크 등): 섹터 단위로 접근하며, 실행하려면 메모리로 올려야 한다.
- 위로 갈수록 빠르고 비싸며 용량이 작다(메모리 계층 구조).
실행 파일은 실행되면 자신만의 주소 공간(code, data, stack)을 가진 프로세스가 된다. 당장 필요한 부분만 물리 메모리에 올리고 나머지는 디스크의 스왑 영역에 둔다. 커널도 하나의 프로그램이라 자기 주소 공간을 가지며, 모든 프로세스가 이를 공유한다.
| 커널 영역 | 담는 것 |
|---|---|
| code | 시스템 콜 처리, 인터럽트 처리, 자원 관리(CPU·메모리·장치) 코드 |
| data | 하드웨어 자원을 관리하는 자료구조와, 프로세스마다 하나씩 있는 PCB(Process Control Block) (프로세스와 스레드) |
| stack | 프로세스마다 따로 둔 커널 스택. 어느 프로세스든 시스템 콜로 커널 코드를 실행할 수 있으므로, 커널 안의 함수 호출 기록도 프로세스별로 쌓아야 한다 |
프로그램이 쓰는 함수
- 사용자 정의 함수, 라이브러리 함수: 프로세스 자신의 주소 공간에서 실행 (사용자 모드)
- 커널 함수: OS의 함수. 호출하는 것이 곧 시스템 콜 (커널 모드)
프로그램은 실행되는 동안 사용자 모드와 커널 모드를 계속 오간다.
시스템 콜과 라이브러리 함수
커널은 하드웨어를 직접 다룰 수 있는 유일한 프로그램이다. 디스크·네트워크·화면에 무언가를 하려면 프로그램은 커널에 부탁해야 하고, 그 창구가 시스템 콜이다. C에서 read, write, open, fork, execve, unlink는 시스템 콜을 감싼 얇은 함수라 일반 함수처럼 부른다.
| 구분 | 시스템 콜 | 라이브러리 함수 |
|---|---|---|
| 실행 위치 | 커널 모드로 전환해 커널 코드 실행 | 사용자 모드, 프로세스 자신의 주소 공간 |
| 비용 | 모드 전환이 있어 상대적으로 비싸다 | 일반 함수 호출 |
| 예 | write, read, fork, mmap | printf(내부에서 write 호출), strlen(시스템 콜 없음), malloc(필요할 때만 brk·mmap) |
| 매뉴얼 | man 2 | man 3 |
라이브러리 함수는 시스템 콜을 쓸 수도, 안 쓸 수도 있다. printf가 매번 write를 부르지 않고 버퍼에 모았다가 한 번에 부르는 것처럼, 라이브러리는 시스템 콜 횟수를 줄이는 역할도 한다(UNIX 파일 입출력). 프로그램이 부르는 시스템 콜은 strace(Linux)·dtruss(macOS)로 볼 수 있다. strace ls나 한 줄짜리 hello world를 추적해 보면 execve, mmap, openat, read, write 같은 시스템 콜이 수십 번 지나간다. 단순해 보이는 프로그램도 사용자 모드와 커널 모드를 쉴 새 없이 오간다는 것을 눈으로 확인하는 가장 빠른 방법이다. 자세한 사용법은 C 디버깅 도구에 있다.
| 범주 | UNIX | Windows |
|---|---|---|
| 프로세스 제어 | fork, exec, wait, exit | CreateProcess, WaitForSingleObject, ExitProcess |
| 파일 관리 | open, read, write, close | CreateFile, ReadFile, WriteFile, CloseHandle |
| 장치 관리 | ioctl, read, write | SetConsoleMode, ReadConsole, WriteConsole |
| 정보 관리 | getpid, alarm, sleep | GetCurrentProcessId, SetTimer, Sleep |
| 통신 | pipe, shm_open, mmap | CreatePipe, CreateFileMapping, MapViewOfFile |
| 보호 | chmod, umask, chown | SetFileSecurity, SetSecurityDescriptorGroup |
man 페이지
UNIX 계열의 매뉴얼은 섹션으로 나뉜다. 이름이 같은 항목이 여러 섹션에 있을 수 있어서, man printf는 셸 명령(섹션 1)을 보여 주고 C 함수는 man 3 printf로 봐야 한다. 시스템 콜 write는 man 2 write다.
| 섹션 | 내용 | 예 |
|---|---|---|
| 1 | 사용자 명령 | ls, chmod |
| 2 | 시스템 콜 | open, fork |
| 3 | 라이브러리 함수 | printf, strlen |
| 4 | 장치 파일 (/dev) | null, tty |
| 5 | 설정 파일 형식 | passwd, crontab |
| 6 | 게임 | |
| 7 | 개요·규약 | signal, ascii, regex |
| 8 | 관리자 명령 | mount, useradd |
- 문서는 NAME, SYNOPSIS(원형·헤더), DESCRIPTION, RETURN VALUE, ERRORS, EXAMPLES, SEE ALSO 순으로 구성된다. 함수를 쓸 때는 RETURN VALUE와 ERRORS를 꼭 확인한다.
man -k 키워드(=apropos)로 검색하고,man -f 이름(=whatis)으로 어느 섹션에 있는지 본다. 같은 이름의 섹션을 찾는 순서는 시스템마다 설정이 다르다.- 매뉴얼 파일은
/usr/share/man/man<섹션>/에 roff 형식을 gzip으로 압축해 둔다. 직접 쓴 페이지는man -l 파일로 바로 볼 수 있다. 문법은man 7 groff_man에 있다(.TH제목,.SH절,.B굵게).
관련 노트: 운영체제 개요, UNIX 파일 입출력과 파일 디스크립터, UNIX 시그널, 운영체제의 시간과 타이머, 입출력 구조와 시스템 버스, 디스크 관리와 스케줄링, 동시성 모델
원본 기록9
- Notion 원문 (새 창) app.notion.com/p/95029389562e43c9aa04537faa8d0022
- Notion 원문 (새 창) 80000coding.notion.site/82228175c0df477992b7b23e8afb8f16
- Notion 원문 (새 창) 80000coding.notion.site/2287b35ba1644ade8d8015559f00fbe8
- Notion 원문 (새 창) 80000coding.notion.site/99627aac4d8945a8bc45735301b4fb3d
- 원문 (새 창) www.youtube.com/watch?v=3Yz7OnVUM28
- 원문 (새 창) www.youtube.com/watch?v=R4j_hDQuBOc
- 원문 (새 창) www.youtube.com/watch?v=BoJ1eaE5F-I
- 원문 (새 창) www.youtube.com/watch?v=26QPDBe-NB8
- GitHub 원문 (새 창) github.com/fru1tworld/cs-basic/blob/main/interview/cs/os.md