운영체제 역할: 하드웨어와 애플리케이션 사이의 자원 중재자
운영체제는 CPU, 메모리, 디스크, 네트워크 같은 물리적 자원을 프로세스들에게 안전하고 공정하게 배분하는 소프트웨어 계층이다. 커널 모드와 유저 모드를 분리하고 시스템 콜을 통해서만 하드웨어에 접근하게 함으로써 안정성과 보안을 확보한다. 웹 개발자가 흔히 마주치는 프로세스 관리, 메모리 할당, 파일 디스크립터, 컨텍스트 스위칭 같은 개념이 모두 이 계층에
1. 개념
운영체제(Operating System, OS)는 하드웨어 자원(CPU, 메모리, 디스크, 네트워크 인터페이스)과 애플리케이션 사이에 위치해서 자원을 관리하고 공통 인터페이스를 제공하는 소프트웨어다. 애플리케이션 개발자는 특정 CPU 모델의 레지스터 구조나 디스크 컨트롤러의 명령어 세트를 몰라도 파일을 읽고, 네트워크 소켓을 열고, 메모리를 할당할 수 있는데, 이는 OS가 그 복잡성을 시스템 콜(system call)이라는 표준화된 인터페이스 뒤로 숨기기 때문이다.
OS의 역할은 크게 네 가지로 나뉜다.
- 프로세스 관리: CPU 시간을 여러 프로세스에 분배(스케줄링)
- 메모리 관리: 물리 메모리를 프로세스별 가상 주소 공간으로 매핑
- 파일 시스템 관리: 디스크의 바이트 나열을 파일과 디렉터리 구조로 추상화
- 입출력 관리: 디바이스 드라이버를 통해 다양한 하드웨어를 통일된 인터페이스로 제공
2. 왜 사용하는가
OS가 없다면 두 가지 문제가 생긴다.
첫째, 모든 애플리케이션이 하드웨어를 직접 제어해야 한다. 디스크 컨트롤러 레지스터에 직접 값을 쓰거나 네트워크 카드의 인터럽트를 직접 처리해야 하는데, 이는 하드웨어마다 다르고 개발 비용이 막대하다.
둘째, 여러 프로그램이 동시에 실행될 때 자원 충돌이 발생한다. 프로그램 A가 메모리 주소 0x1000을 쓰고 있는 동안 프로그램 B가 같은 주소에 다른 값을 쓰면 데이터가 깨진다. CPU도 마찬가지로, 한 프로그램이 무한 루프에 빠지면 다른 프로그램은 영원히 실행 기회를 얻지 못한다.
OS는 이 두 문제를 각각 하드웨어 추상화(abstraction)와 자원 격리(isolation)로 해결한다. 프로세스마다 독립된 가상 메모리 공간을 주고, CPU 시간을 일정 단위(타임 슬라이스)로 쪼개서 순환 배분한다.
3. 동작 원리
커널 모드와 유저 모드
CPU는 물리적으로 두 가지 실행 모드를 지원한다. 커널 모드(kernel mode)에서는 모든 명령어와 메모리 영역에 접근할 수 있고, 유저 모드(user mode)에서는 제한된 명령어만 실행 가능하다. 일반 애플리케이션은 유저 모드에서 실행되며, 디스크 읽기 같은 특권 작업이 필요하면 시스템 콜을 발생시켜 커널 모드로 전환한다.
# strace로 시스템 콜 추적 (Linux)
# cat 명령어가 파일을 열고 읽는 과정에서 발생하는 syscall 확인
strace -c cat /etc/hostname실행 결과에는 openat, read, close 같은 syscall이 나타난다. 애플리케이션 코드에서 fopen()을 호출하면 결국 이 syscall들로 귀결된다.
프로세스 스케줄링
CPU 코어 하나는 한 순간에 하나의 명령어 스트림만 실행할 수 있다. 여러 프로세스를 동시에 실행하는 것처럼 보이게 하려면 스케줄러가 각 프로세스에 짧은 시간(타임 퀀텀, 보통 수 밀리초 단위)을 할당하고 빠르게 전환한다. 이 전환 과정을 컨텍스트 스위칭(context switching)이라 하며, 레지스터 상태와 메모리 매핑 정보를 저장하고 복원하는 작업이 포함된다.
가상 메모리
프로세스는 물리 메모리 주소를 직접 다루지 않고 가상 주소(virtual address)를 사용한다. OS는 MMU(Memory Management Unit)와 협력해서 가상 주소를 물리 주소로 변환하는 페이지 테이블을 관리한다. 이 덕분에 프로세스 A가 아무리 큰 배열을 할당해도 프로세스 B의 메모리 공간을 침범할 수 없다.
import os
# 현재 프로세스의 PID와 부모 프로세스 PID 확인
print(f"현재 프로세스 PID: {os.getpid()}")
print(f"부모 프로세스 PID: {os.getppid()}")
# fork()로 자식 프로세스 생성 (Unix 계열)
pid = os.fork()
if pid == 0:
print(f"자식 프로세스 실행, PID: {os.getpid()}")
else:
print(f"부모 프로세스에서 자식 PID {pid} 생성 완료")
os.wait() # 자식 프로세스 종료 대기fork()를 호출하면 커널이 현재 프로세스의 메모리 공간을 복사(실제로는 Copy-on-Write 방식)해서 새 프로세스를 만든다. 부모와 자식은 독립된 가상 주소 공간을 가지므로 한쪽이 변수 값을 바꿔도 다른 쪽에 영향을 주지 않는다.
4. 코드 예제
파일 디스크립터와 메모리 사용량은 OS가 프로세스별로 추적하는 대표적인 자원이다.
import resource
import os
# 현재 프로세스가 사용 중인 파일 디스크립터 개수 확인 (Linux)
fd_dir = f"/proc/{os.getpid()}/fd"
open_fds = os.listdir(fd_dir)
print(f"열린 파일 디스크립터 개수: {len(open_fds)}")
# 프로세스 자원 사용량 (getrusage)
usage = resource.getrusage(resource.RUSAGE_SELF)
print(f"최대 상주 메모리 크기(KB): {usage.ru_maxrss}")
print(f"사용자 모드 CPU 시간(초): {usage.ru_utime}")
print(f"커널 모드 CPU 시간(초): {usage.ru_stime}")ru_utime은 프로세스 코드가 유저 모드에서 실행된 시간, ru_stime은 시스템 콜 처리를 위해 커널 모드에서 실행된 시간이다. 이 값이 비정상적으로 크면 시스템 콜을 과도하게 호출하는 코드일 가능성이 있다.
5. 시간 복잡도 또는 성능 특성
- 시스템 콜 호출 비용: 커널 모드 전환에는 컨텍스트 저장/복원 오버헤드가 있다. Linux 환경에서 단순한 syscall(
getpid등)은 수십에서 수백 나노초 수준이지만,read/write처럼 실제 I/O가 발생하는 syscall은 디바이스 응답 시간에 따라 수 마이크로초에서 수 밀리초까지 걸릴 수 있다. - 컨텍스트 스위칭 비용: CPU 레지스터 저장/복원 자체는 마이크로초 이하지만, 캐시(L1/L2)와 TLB(Translation Lookaside Buffer)가 새 프로세스의 데이터로 교체되면서 발생하는 캐시 미스 비용이 실질적으로 더 크다. 이 때문에 스레드 수가 CPU 코어 수 대비 과도하게 많으면 처리량이 오히려 떨어질 수 있다.
- 가상 메모리 주소 변환: TLB 히트 시 주소 변환은 CPU 파이프라인 내에서 거의 추가 지연 없이 처리되지만, TLB 미스가 발생하면 페이지 테이블을 순회(page table walk)해야 하므로 수십 사이클의 지연이 추가된다.
6. 실무 사용 사례
- Node.js의 이벤트 루프는 내부적으로
epoll(Linux) 또는kqueue(macOS/BSD) 같은 OS 제공 I/O 멀티플렉싱 syscall을 사용해서 수천 개의 소켓 연결을 적은 스레드로 처리한다. - 컨테이너(Docker)는 새로운 커널을 실행하는 것이 아니라, 리눅스 커널의 네임스페이스(namespace)와 cgroup 기능을 이용해서 프로세스 그룹의 자원 사용을 격리하고 제한한다.
- 데이터베이스 서버(PostgreSQL 등)는 파일 시스템 캐시와 자체 버퍼 캐시 사이의 상호작용을 고려해서 설계된다.
fsyncsyscall 호출 시점과 빈도가 데이터 내구성과 쓰기 성능 사이의 트레이드오프를 결정한다.
7. 주의할 점
- 스레드나 프로세스를 무분별하게 늘리면 컨텍스트 스위칭 오버헤드가 누적되어 오히려 전체 처리량이 떨어진다. CPU 코어 수를 초과하는 CPU 바운드 스레드는 이득이 없다.
- 파일 디스크립터는 프로세스당 제한(
ulimit -n으로 확인 가능)이 있다. 소켓이나 파일을 열고 닫지 않으면 이 한계에 도달해서Too many open files오류가 발생한다. - 메모리 할당(
malloc, 언어 런타임의 힙 할당)은 즉시 물리 메모리를 확보하는 것이 아니라 가상 주소 공간만 예약하는 경우가 많다(lazy allocation). 실제 물리 메모리는 해당 페이지에 처음 접근할 때(page fault) 할당된다. 이 때문에 메모리 사용량 모니터링 시 가상 메모리 크기(VSZ)와 실제 상주 메모리 크기(RSS)를 구분해야 한다.
8. 핵심 정리
운영체제는 하드웨어를 직접 다루는 복잡성을 시스템 콜이라는 표준 인터페이스로 감추고, 프로세스마다 독립된 메모리 공간과 공정한 CPU 시간을 배분해서 여러 프로그램이 안전하게 공존하도록 만드는 자원 중재자다. 웹 개발자가 다루는 이벤트 루프, 스레드 풀, 커넥션 풀, 파일 디스크립터 한계 같은 실무 이슈는 결국 이 OS 계층의 동작 방식에서 비롯된다.