시스템 호출: 사용자 프로그램이 커널 기능을 요청하는 방식

시스템 호출의 역할과 종류, 사용자 모드·커널 모드 전환, fork 호출 흐름과 성능 고려사항을 정리한다.

2026-08-14 · 최초 발행 2025-12-30

커널에 직접 닿지 않고 자원을 쓰는 경계

사용자 프로그램은 하드웨어나 커널 자원에 곧바로 접근하지 않는다. 파일을 열고, 프로세스를 만들고, 네트워크 연결을 맺는 요청은 시스템 호출(System Call)을 거쳐 운영체제 커널에 전달된다. 이 경계가 하드웨어 제어와 자원 접근처럼 특권이 필요한 작업을 통제한다.

시스템 호출은 사용자 프로그램과 OS 커널을 잇는 인터페이스다. 호출이 시작되면 실행은 사용자 모드에서 커널 모드로 넘어가며, 커널은 권한과 인자를 확인한 뒤 작업 결과를 다시 사용자 공간으로 돌려준다.

이 구조는 몇 가지 역할을 함께 수행한다. 사용자 프로그램의 직접적인 하드웨어 접근을 막고, CPU·메모리·디스크·네트워크 자원을 조정하며, 권한 검사와 격리를 제공한다. 개별 하드웨어의 복잡성을 감춘 일관된 인터페이스라는 점도 중요하다.

사용자 프로그램System Call(인터페이스)커널(특권 모드)하드웨어 제어파일 시스템메모리 관리네트워크 스택보호된 자원 접근

자원 성격에 따라 나뉘는 호출

프로세스 관리에는 새 프로세스를 만드는 fork(), 현재 프로세스 이미지를 교체해 새 프로그램을 실행하는 execve(path, argv, envp)가 있다. getpid()는 현재 프로세스 ID를 돌려주며 로깅과 디버깅에 쓰인다. signal(sig, handler)은 SIGINT, SIGTERM 등의 비동기 이벤트를 처리할 핸들러를 등록한다. wait(status)는 자식 프로세스의 종료를 기다리고 종료 상태를 수집해 좀비 프로세스를 방지하며, exit(status)는 프로세스를 끝내고 자원을 해제한 뒤 종료 코드를 반환한다.

메모리 영역은 brk(addr)mmap(addr, length, prot, flags, fd, offset)로 다룬다. brk(addr)는 힙 크기를 조정하며 malloc() 내부에서 사용된다. mmap()은 파일을 메모리에 매핑하거나 공유 메모리를 만들고 대용량 파일 처리에 사용된다. 매핑을 해제하고 자원을 반환할 때는 munmap(addr, length)를 호출한다.

파일 시스템에서는 open(path, flags, mode)이 파일 디스크립터를 반환한다. O_RDONLY, O_WRONLY, O_RDWR 등이 여기에 쓰인다. read(fd, buf, count)는 파일에서 데이터를 읽고 읽은 바이트 수를 반환하며 블로킹 I/O로 동작한다. write(fd, buf, count)는 데이터를 쓰고, close(fd)는 디스크립터를 닫아 자원을 해제한다. lseek(fd, offset, whence)는 파일 오프셋을 움직여 랜덤 액세스를 가능하게 하고, stat(path, buf)는 크기·권한·타임스탬프 등의 파일 정보를 조회한다.

네트워크 작업도 호출 경계 안에 있다. socket(domain, type, protocol)은 AF_INET(IPv4), SOCK_STREAM(TCP) 등의 조건으로 소켓을 만들고 소켓 디스크립터를 돌려준다. bind(sockfd, addr, addrlen)은 IP 주소와 포트를 소켓에 할당한다. 서버는 listen(sockfd, backlog)으로 연결 요청을 기다리고 accept(sockfd, addr, addrlen)으로 연결을 수락해 새 소켓을 만든다. 클라이언트는 connect(sockfd, addr, addrlen)으로 서버에 연결하며, 데이터는 send/recv(sockfd, buf, len, flags)로 송수신한다.

fork() 호출이 커널에 도달하는 경로

프로그램이 fork()를 호출했다고 해서 곧바로 프로세스 복제가 일어나는 것은 아니다. 먼저 사용자 프로그램은 라이브러리에 연결된 함수를 호출한다.

#include <unistd.h>
pid_t pid = fork();

fork() 함수 정보는 libc.so 같은 라이브러리에 있으며, 동적 링크 또는 정적 링크를 통해 함수 주소가 결정된다. 라이브러리의 wrapper 함수는 시스템 호출 번호와 인자를 준비한 뒤 소프트웨어 인터럽트 또는 시스템 호출 명령을 실행한다.

fork:
    mov eax, __NR_fork  ; 시스템 호출 번호
    swi 0x80            ; 소프트웨어 인터럽트 (x86)
    ; 또는
    syscall             ; (x86-64)
    ret

swi 또는 syscall 명령을 만나면 CPU는 사용자 모드에서 커널 모드로 전환하고 인터럽트 벡터 테이블을 참조한다. 이때 사용자 모드의 레지스터를 저장한다. 시스템 호출 번호는 eax/rax 레지스터로 전달되고, 인자는 ebx, ecx, edx 또는 rdi, rsi, rdx에 전달된다.

커널은 시스템 호출 번호를 읽어 시스템 호출 테이블의 항목을 찾고 해당 커널 함수 주소를 얻는다. fork()에 해당하는 내부 처리는 다음과 같은 형태를 가진다.

// 커널 내부
SYSCALL_DEFINE0(fork) {
    return do_fork(...);
}

long do_fork(...) {
    // 프로세스 복제 로직
    struct task_struct *p = copy_process(...);
    wake_up_new_task(p);
    return p->pid;
}

커널 함수는 호출 프로세스의 권한과 자원 접근 권한을 검증한다. 실패하면 에러를 반환한다. 작업이 끝나면 반환값을 레지스터에 설정하고, 에러인 경우 errno를 설정한 뒤 사용자 모드로 복귀한다.

가상 파일 시스템(VFS)은 이 흐름에서 파일·디렉터리·디바이스를 통합된 방식으로 접근하게 한다. /proc, /sys, /dev도 이 관점에서 다뤄진다. VFS 아래에는 ext4, NTFS 등의 실제 파일 시스템, 디바이스 드라이버, 네트워크 스택이 놓인다.

사용자 프로그램fork() 호출라이브러리swi/syscall커널 모드 전환시스템 호출 번호 판독sys_fork() 실행do_fork()프로세스 복제VFS 접근(필요 시)결과 반환사용자 모드 복귀

프로세스를 복제할 때 반환값이 갈리는 이유

fork()는 libc의 wrapper 함수를 거쳐 시스템 호출을 준비하고, swi 0x80 (x86) 또는 syscall (x86-64)로 커널에 진입한다. 이 과정에서 CPU 모드 비트가 바뀌고 커널 스택으로 전환된다.

커널은 __NR_fork (예: 2번)를 읽어 sys_fork()를 호출하고, 실제 구현인 do_fork()를 실행한다. 이후 task_struct를 복사하고 메모리 맵을 COW로 복사하며 파일 디스크립터를 복사한 뒤 PID를 할당한다.

성공 시 부모 프로세스에는 자식 PID가, 자식 프로세스에는 0이 반환된다. 오류가 발생하면 -1이 반환된다.

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // System Call!

    if (pid < 0) {
        // 에러
        perror("fork failed");
    } else if (pid == 0) {
        // 자식 프로세스
        printf("Child: PID = %d\n", getpid());
    } else {
        // 부모 프로세스
        printf("Parent: Child PID = %d\n", pid);
    }

    return 0;
}

권한 모드가 만드는 보호 경계

사용자 모드는 일반 애플리케이션이 실행되는 제한된 권한의 영역이다. 하드웨어에 직접 접근할 수 없으며 x86에서는 Ring 3에 해당한다. 반대로 커널 모드는 운영체제 커널이 실행되는 모든 권한의 영역으로, 하드웨어를 직접 제어할 수 있고 x86에서는 Ring 0에 해당한다.

두 모드 사이의 전환은 시스템 호출 외에도 하드웨어 인터럽트와 예외(Exception)에서 발생한다. 시스템 호출에는 swi, syscall, sysenter가 사용될 수 있다. 이 전환에는 레지스터 저장·복원과 TLB 플러시 가능성이 따르며, 모드 전환 비용은 수백 사이클이다.

커널 경계는 프로그램 간 격리, 커널과 사용자 공간의 격리, 메모리 보호를 가능하게 한다. 또한 각 호출에서 UID/GID와 접근 제어 목록(ACL)을 검증하며, 잘못된 인자·NULL 포인터·버퍼 오버플로를 검사한다.

System Call반환사용자 모드모드 전환커널 모드모드 전환Ring 3제한된 권한Ring 0모든 권한

호출 횟수가 성능에 미치는 영향

시스템 호출은 모드 전환을 포함하므로 비용이 크다. 같은 작업을 반복해야 한다면 가능한 범위에서 배치 처리하고 버퍼링을 활용하는 편이 낫다. 파일을 1바이트씩 읽는 경우와 한 번에 읽는 경우의 차이는 다음 코드에서 드러난다.

// 비효율적: 1바이트씩 1000번 호출
for (int i = 0; i < 1000; i++) {
    read(fd, &buf[i], 1);  // System Call 1000번
}

// 효율적: 한 번에 1000바이트 호출
read(fd, buf, 1000);       // System Call 1번

fread, fwrite 같은 stdio 함수는 내부 버퍼를 사용해 시스템 호출 횟수를 줄인다. 메모리 매핑(mmap), 공유 메모리, 비동기 I/O(aio)도 호출 빈도를 다루는 대안이 될 수 있다.

라이브러리 함수와 호출 경계의 차이

구분 System Call Library Function
실행 위치 커널 모드 사용자 모드
예시 read(), write(), fork() strlen(), printf()
모드 전환 있음 없음
오버헤드 높음 낮음
하드웨어 접근 가능 불가능
이식성 OS 종속 표준화 (libc)

라이브러리 함수가 시스템 호출을 감싸는 경우도 있다. printf()는 라이브러리 함수이며 버퍼링과 포맷팅을 제공한 뒤 내부적으로 write() 시스템 호출을 호출한다.

// 라이브러리 함수
printf("Hello\n");

// 내부적으로 System Call
write(1, "Hello\n", 6);
애플리케이션Library Function(printf)버퍼링포맷팅System Call(write)커널 모드하드웨어(화면)
시스템 호출운영체제커널프로세스VFS