1.시스템&인프라/개발 입문자를 위한 운영체제

18편. 시스템 콜의 비밀 – read()가 커널을 호출하는 진짜 과정

쿼드큐브 2025. 11. 23. 13:41
반응형
반응형

18편. 시스템 콜의 비밀 – read()가 커널을 호출하는 진짜 과정

 

📚 목차
1. 시스템 콜(System Call)이란? - 운영체제에게 일을 시키는 유일한 방법
2. 사용자 공간 vs 커널 공간 - 시스템 콜이 필요한 이유
3. 시스템 콜은 어떻게 실행 될까? - 내부 처리 흐름
4. strace 실습으로 확인하는 시스템 콜의 흐름
5. 시스템 콜의 성능 비용 - 문맥 전환과 최적화 전략
✔ 마무리 - 실무에서 시스템 콜을 이해해야 하는 이유

 

 

우리가 매일 사용하는 프로그램들은 단순한 코드처럼 보이지만, 사실 운영체제와 끊임없이 대화하며 동작합니다.

그 대화의 창구가 바로 시스템 콜(System Call)입니다.


이 글에서는 read() 같은 함수가 실제로 운영체제에 어떤 요청을 보내는지, 사용자 프로그램과 커널은 어떻게 소통하는지, 그리고 시스템 콜이 성능에 어떤 영향을 미치는지 하나씩 살펴보겠습니다.

 

1. 시스템 콜이란? – 운영체제에게 일을 시키는 유일한 방법

운영체제는 응용 프로그램이 하드웨어 자원(예: CPU, 메모리, 하드디스크)에 직접 접근하는 것을 엄격히 제한합니다. 이유는 간단합니다.

 

▸ 만약 사용자 프로그램이 직접 메모리나 디스크를 다룰 수 있다면,
▸ 실수로 다른 프로그램의 데이터를 지우거나
▸ 악의적으로 시스템 전체를 망가뜨릴 수도 있기 때문입니다.

 

그래서 운영체제는 “아무나 들어올 수 없는 공간”인 커널 공간(Kernel Space)을 따로 만들어서, 응용 프로그램은 직접 들어오지 못하도록 하고, 그 대신 공식 요청 창구인 시스템 콜을 통해 “이 작업 좀 해줘요!”라고 요청하도록 만든 것입니다.

 

🟦 시스템 콜은 OS와의 대화 수단입니다.

시스템 콜은 말 그대로 운영체제에게 특정 작업을 요청하는 기능 호출입니다. 응용 프로그램은 직접 하드웨어를 만지는 대신, 시스템 콜을 통해 운영체제에게 일을 시킵니다.


예를 들어, C 언어에서 자주 사용하는 다음과 같은 함수들이 있습니다

open()   // 파일을 연다
read()   // 파일 내용을 읽는다
write()  // 데이터를 쓴다
close()  // 파일을 닫는다

이 함수들은 단순히 라이브러리 함수가 아니라, 내부적으로 운영체제 커널에 연결된 시스템 콜을 호출합니다.

즉, read()를 호출하면 커널에 “이 파일에서 데이터를 100바이트만큼 읽어줘”라고 요청하는 것입니다.

 

🟦 Python, Java도 결국 시스템 콜을 사용합니다.

고급 언어를 사용하는 경우에도 시스템 콜은 빠지지 않습니다.

Python의 open("file.txt")이나 Java의 FileReader 객체도 결국 내부적으로는 C 언어의 read(), write() 시스템 콜을 호출하여 운영체제에게 요청합니다.

 

다시 말해, 프로그래밍 언어는 다르더라도, 실제 작업은 결국 운영체제가 처리하는 것이며, 그 통로가 바로 시스템 콜입니다.

 

2. 사용자 공간 vs 커널 공간 – 시스템 콜이 필요한 이유

우리가 컴퓨터에서 실행하는 모든 프로그램은 사실상 두 개의 세계를 오가며 동작합니다.

하나는 사용자 공간(User Space)이고, 다른 하나는 커널 공간(Kernel Space)입니다.

사용자 공간, 커널 공간 논리적 구조
사용자 공간, 커널 공간 논리적 구조

이 둘은 물리적으로는 같은 메모리 안에 존재하지만, 엄격하게 분리된 영역이며, 서로를 함부로 넘나들 수 없습니다. 왜냐하면 보안과 안정성, 그리고 시스템 전체의 안전을 위해서입니다.

 

💡 사용자 공간은 우리가 만든 프로그램이 실행되는 영역이고, 커널 공간은 운영체제 자체가 동작하는 매우 민감한 영역입니다.

 

🟦 왜 나누는가?

한마디로 말해, 사고 방지용 안전장치입니다.

 

응용 프로그램이 실수로 다른 프로그램의 메모리를 덮어쓴다거나, 하드디스크를 마음대로 지우거나, CPU를 독점하게 된다면 컴퓨터 전체가 멈추는 불상사가 생길 수 있습니다.

이런 사태를 막기 위해 운영체제는 “응용 프로그램은 사용자 공간에서만 실행되고, 커널 공간은 오직 나(OS)만이 다룬다”고 룰을 정한 것입니다.


이렇게 하면, 사용자 프로그램이 아무리 버그가 있어도 시스템 전체에 영향을 주지 않고, 운영체제가 상황을 통제할 수 있게 됩니다.

 

🟦 시스템 콜은 그 사이의 유일한 다리

“그렇다면 응용 프로그램이 하드디스크에서 파일을 읽거나, 네트워크에 접속하거나, 키보드 입력을 받으려면 어떻게 해야 할까요?”

 

이러한 작업들은 모두 하드웨어 자원에 직접 접근해야 하는 일이며, 이는 커널 공간에서만 수행할 수 있는 영역입니다.

사용자 공간에 있는 프로그램은 직접 하드웨어를 제어할 권한이 없기 때문에, 반드시 운영체제의 도움을 받아야 합니다.


바로 이때 등장하는 것이 앞서 설명한 시스템 콜(System Call)입니다.


시스템 콜은 사용자 공간과 커널 공간을 연결하는 공식적이고 유일한 통로로서, 응용 프로그램이 커널에게 “이 파일을 열어줘”, “이 데이터를 전송해 줘”, “메모리를 더 할당해 줘”라고 요청을 전달하는 수단입니다.

 

즉, 시스템 콜은 응용 프로그램과 운영체제가 안전하게 협력할 수 있도록 도와주는 인터페이스인 셈입니다.

 

🟦 통제 가능한 인터페이스

운영체제는 이 시스템 콜을 통해 모든 자원 접근 요청을 감시하고, 허용하거나 거부합니다.

 

예를 들어, 권한이 없는 프로그램이 시스템 파일을 열려고 할 경우, 커널은 이를 거부할 수 있습니다.

이처럼 시스템 콜은 단순한 함수 호출이 아니라, 안전하고 통제된 인터페이스인 것입니다.

 

3. 시스템 콜은 어떻게 실행될까? – 내부 처리 흐름

이전까지는 시스템 콜이 무엇이고, 왜 필요한지를 이해했습니다.

이제는 조금 더 깊이 들어가 시스템 콜이 실제로 어떻게 동작하는지, 운영체제가 어떤 과정을 거쳐 요청을 처리하는지를 살펴보겠습니다.

 

🟦 시스템 콜의 내부 처리 흐름

응용 프로그램이 read() 또는 write()와 같은 함수를 호출한다고 해서, 바로 하드디스크에서 데이터를 읽거나 출력 장치에 쓰는 것은 아닙니다.

그 과정은 다음과 같이 여러 단계를 거쳐 커널에 도달합니다.

 

✔️ 시스템 콜 실행의 5단계 요약

🔸 함수 호출

사용자 프로그램이 read(fd, buf, size)처럼 시스템 자원을 사용하는 함수를 호출합니다.


🔸 시스템 콜 인터페이스 진입

이 함수는 내부적으로 시스템 콜 인터페이스로 연결되며, OS마다 정해진 시스템 콜 번호로 변환됩니다.

예를 들어 Linux에서 read()는 시스템 콜 번호 0번입니다.

 

🔸 문맥 전환(Context Switch)

CPU는 사용자 모드(User Mode)에서 커널 모드(Kernel Mode)로 전환되며, 시스템 콜 핸들러가 호출되어 커널 공간으로 진입합니다. 이 과정은 비교적 비용이 크며 보호된 실행 환경으로 넘어갑니다.


🔸 커널이 작업 수행

운영체제 커널이 요청받은 작업을 처리합니다. 예를 들어, 파일 디스크립터를 확인하고, 하드디스크에서 데이터를 읽어 메모리 버퍼에 복사합니다.

 

🔸 결과 반환 및 사용자 공간 복귀

작업이 완료되면 CPU는 다시 사용자 공간으로 돌아가며, 결과값(읽은 바이트 수 등)을 응용 프로그램에 전달합니다.

시스템 콜 실행 흐름 다이어그램
시스템 콜 실행 흐름 다이어그램

✔️ 비유: 은행 창구 시스템

이 과정을 현실 세계에 비유하면 다음과 같습니다.

🔸고객(사용자 프로그램)이 “이 돈을 송금해주세요(read)”라고 요청서를 작성합니다.

🔸창구 직원(시스템 콜 인터페이스)이 해당 요청을 접수하고 절차에 따라 처리 번호를 붙입니다.

🔸뒤편의 창고 직원(커널)이 실제로 돈을 찾아 송금합니다.
🔸결과 영수증(반환값)을 고객에게 다시 전달합니다.

 

이처럼 시스템 콜은 사용자와 운영체제 사이에서 요청을 안전하게 중개하는 창구 역할을 합니다.

 

🟦 예시 코드로 보는 시스템 콜 흐름

다음은 실제로 시스템 콜을 사용하는 간단한 C 언어 코드입니다

#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("hello.txt", O_RDONLY);   // 시스템 콜: open()
    char buffer[100];
    read(fd, buffer, 100);                  // 시스템 콜: read()
    write(1, buffer, 100);                  // 시스템 콜: write()
    close(fd);                              // 시스템 콜: close()
    return 0;
}

위 코드를 실행하면 총 네 번의 시스템 콜이 발생합니다:

1. open() → 파일을 커널이 열고, 파일 디스크립터(fd)를 반환
2. read() → 커널이 파일 내용을 읽어 메모리에 복사
3. write() → 커널이 터미널(표준 출력)로 버퍼 내용을 출력
4. close() → 커널이 파일 핸들을 해제하고 리소스를 반납

 

이 네 줄의 코드만 보더라도, 사용자 프로그램은 커널의 도움이 없이는 아무런 I/O도 직접 할 수 없으며, 시스템 콜을 통해 커널에 작업을 맡기는 구조임을 알 수 있습니다.

 

🟦 응답 속도와 문맥 전환 비용

시스템 콜은 일반적인 함수 호출보다 훨씬 더 많은 비용이 듭니다.

그 이유는 문맥 전환(Context Switching)이 반드시 수반되기 때문입니다.

 

사용자 공간에서 커널 공간으로, 다시 사용자 공간으로 돌아오는 이 전환은 CPU 내부 구조와 보안 검사를 포함한 여러 단계를 거쳐야 하므로 성능에 영향을 줄 수 있습니다.

 

특히 고빈도 시스템 콜을 반복적으로 호출하는 경우, 전체 프로그램의 성능 병목이 되기도 합니다.

반응형

 

4. strace 실습으로 확인하는 시스템 콜의 흐름

우리가 매일 사용하는 cat, ls, echo 같은 간단한 명령어들도 사실은 여러 개의 시스템 콜을 내부적으로 호출하고 있습니다.


그 동작 과정을 추적하려면 리눅스에서 제공하는 강력한 도구인 strace를 사용할 수 있습니다.
이 도구는 프로그램이 실행되는 동안 어떤 시스템 콜이 언제 호출되고, 어떤 인자를 주고받는지를 상세히 보여줍니다.

 

🟦 Ubuntu에서 strace 설치 및 사용법

strace는 대부분의 Linux 배포판에서 기본 패키지로 제공되며, 다음 명령어로 설치할 수 있습니다.

sudo apt update
sudo apt install strace     # 최초 1회만 설치

설치가 완료되면, 다음과 같이 사용해 봅니다:

strace cat hello.txt

 

cat hello.txt 명령은 단순히 텍스트 파일을 화면에 출력하는 명령처럼 보이지만, 내부에서는 파일 열기, 읽기, 쓰기, 닫기에 해당하는 시스템 콜들이 연속적으로 호출됩니다.

 

✔️ strace 출력 예시

strace 출력 예시
strace 출력 예시

execve("/usr/bin/cat", ["cat", "test.sh"], ...)   # 프로그램 실행 시작
openat(AT_FDCWD, "test.sh", O_RDONLY) = 3         # 파일 열기
read(3, "...", 131072) = 132                      # 내용 읽기
write(1, "...", 132) = 132                        # 출력하기
close(3)                                          # 파일 닫기
exit_group(0)                                     # 정상 종료

 

▸ execve는 cat 명령 자체를 실행하는 시스템 콜입니다.

▸ openat으로 test.sh 파일을 열고,

▸ read로 파일 내용을 읽은 뒤,

▸ write로 터미널(표준 출력)에 출력합니다.

▸ 작업이 끝나면 close로 파일을 닫고, exit_group으로 프로세스를 종료합니다.

 

🟦 Windows 유사 도구

Windows 환경에서는 Microsoft의 Process Monitor (Procmon) 도구를 사용하면 시스템 호출과 파일 접근을 GUI로 실시간 확인할 수 있습니다.

1. Procmon 다운로드 페이지에서 ZIP 파일 다운로드
2. 압축 해제 후 Procmon64.exe 실행
3. 관리자 권한으로 실행되는 것을 확인
4. 상단 필터 아이콘 클릭 → Process Name is notepad.exe 와 같이 필터 설정
5. notepad 실행 → 실제 열리는 파일, DLL, 설정 정보 등 추적 가능

Microsoft의 Process Monitor 화면 예시
Microsoft의 Process Monitor 화면 예시

 

5. 시스템 콜의 성능 비용 – 문맥 전환과 최적화 전략

시스템 콜은 단순한 함수 호출처럼 보일 수 있지만, 실제로는 사용자 공간에서 커널 공간으로 진입하는 문맥 전환(Context Switch)을 수반하는 복잡한 작업입니다.

이 전환 과정은 CPU가 현재의 실행 상태를 저장하고, 커널 모드로 전환한 뒤, 작업을 처리한 후 다시 사용자 모드로 돌아와야 하기 때문에 상당한 자원이 소모되는 비용 높은 작업입니다.


특히 read(), write()와 같은 입출력(I/O) 관련 시스템 콜은 단순히 메모리만 다루는 것이 아니라, 디스크, 네트워크, 키보드 등의 하드웨어 대기 시간도 함께 포함되므로 응답 속도가 상대적으로 느릴 수밖에 없습니다.

 

그래서 고성능 서버나 실시간 시스템에서는 시스템 콜 사용 횟수를 줄이거나, 커널에 요청하는 방식 자체를 최적화하는 것이 중요합니다.

 

✔️ 예시: write() 호출 방식에 따른 성능 차이

아래 두 가지 코드를 비교해 보겠습니다

# 1. 비 효율적인 방식
for (int i = 0; i < 1000; i++) {
    write(1, "*", 1);  // 1바이트씩 1000번 호출
}


# 2. 효율적인 방식
char buffer[1000];
memset(buffer, '*', 1000);
write(1, buffer, 1000);  // 1번 호출로 1000바이트 출력

▸ 첫 번째 코드는 시스템 콜을 1000번 호출하며, 그만큼 문맥 전환도 1000번 발생합니다.

▸ 반면 두 번째는 단 1번의 호출로 동일한 출력을 구현하므로 훨씬 빠르고 효율적입니다.

 

✔️실무 시나리오 : 고빈도 로그 시스템

서버에서 로그를 매 요청마다 남긴다고 가정해보면

for (int i = 0; i < 10000; i++) {
    write(log_fd, log_line, strlen(log_line));
}

▸ 이처럼 수천 번의 write()가 성능 병목이 되므로,

▸ 로그 버퍼링, 비동기 로깅, log aggregator 사용 등의 최적화가 실무에서 적용됩니다.

 

✔ 마무리 - 실무에서 시스템 콜을 이해해야 하는 이유

시스템 콜은 우리가 직접 호출하지 않아도, 대부분의 코드에서 파일 I/O, 네트워크, 메모리 작업을 할 때마다 작동합니다.

단순한 함수 호출처럼 보여도, 커널 공간 진입과 문맥 전환 비용이 크기 때문에 성능에 영향을 줄 수 있습니다.


실무에서는 다음과 같은 점을 고려해야 합니다:

🔸write()나 read()는 가능하면 버퍼링 후 한 번에 호출
🔸고빈도 작업은 비동기 처리로 시스템 콜 횟수 최소화
🔸strace 같은 도구로 시스템 콜 병목을 추적


시스템 콜의 원리를 이해하면, 더 안전하고 효율적인 코드를 만들 수 있습니다.
작은 차이가 누적되면, 실무에서는 큰 성능 차이로 이어집니다.

 

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형