이 글에서는 시스템 호출(system call)이 무엇인지, 어떤 용도로 사용되는지, 그리고 C 표준 라이브러리의 일반 함수들과 어떤 차이가 있는지 자세히 설명합니다. 또한 시스템 호출이 커널에서 수행하는 역할과 프로세스가 이 서비스를 활용하는 방법을 살펴봅니다. 마지막으로 가장 많이 사용되는 시스템 호출 목록을 함수별 설명과 선언된 헤더 파일 정보와 함께 정리하고, 명령어 콘솔에서 시스템 호출 동작을 실시간으로 모니터링하는 방법까지 소개합니다.
시스템은 계층 구조로 이루어져 있다
개념적으로 모든 운영체제는 여러 계층으로 구성되며, 각 계층은 고유한 기능과 권한을 가집니다. 시스템을 이루는 이 계층 스택은 최하위 수준의 하드웨어 구성 요소에서 시작해, 최상위 계층인 사용자 모드(user mode)에서 끝납니다.
컴퓨터에서 실행하는 모든 프로그램과 작성·컴파일한 모든 코드는 사용자 모드에서 실행되는 프로세스로 나타납니다. 사용자 모드에서 우리는 일상 업무를 수행하고, 다양한 애플리케이션을 실행·사용하며, 파일을 관리하는 등의 작업을 합니다. 보안 관점에서 이 계층은 시스템에서 가장 표면에 있는 계층이므로 가장 제한적이며, 기본적으로 시스템 리소스를 직접 관리하거나 사용하는 것이 차단되어 있습니다.
운영체제의 심장, 커널(Kernel)
앞서 언급했듯이 하드웨어 계층은 시스템의 최하위 수준입니다. 바로 그 위에는 리눅스 커널이 위치합니다. 커널은 운영체제의 심장으로, 하드웨어와 직접 연동되어 이를 엄격하게 관리하며, 그럴 권한을 가진 유일한 존재입니다. 예를 들어 프로세스와 메모리 관리, 하드디스크나 SSD의 데이터 접근, 네트워크 장치 제어 등이 모두 커널에서 이루어집니다. 운영체제와 프로세스가 하드웨어에 대해 수행하는 모든 작업은 커널을 거칩니다. 따라서 커널은 소프트웨어와 하드웨어를 하나로 묶어주는 일종의 펌웨어라고 할 수 있습니다.
시스템 호출(SysCall)이란 무엇인가?
사용자 모드에는 운영체제가 부과하는 리소스 접근 및 관리 제약이 있고, 이러한 리소스에는 커널을 통해서만 접근할 수 있기 때문에, 프로세스가 동작에 필요한 리소스와 상호작용할 수 있게 해주는 메커니즘이 반드시 필요합니다.
시스템 호출(system call), 줄여서 SysCall은 운영체제가 제공하는 함수 집합으로, 사용자 프로세스와 커널 사이의 인터페이스 역할을 합니다. 이 함수들을 통해 운영체제에 서비스를 요청하면, 해당 요청이 시스템 리소스를 필요로 하는 경우 커널이 이를 처리해줍니다. 중요한 점은 시스템 호출이 리소스나 그 관리에 대한 직접 접근 권한을 부여하지 않는다는 것입니다. 커널이 리소스를 관리하면서 호출한 프로세스에 결과를 제공하며, 이 모든 과정은 항상 커널의 엄격한 통제 아래에서 이루어집니다.
실제로 시스템 호출 함수는 일반 함수처럼 보이고 그렇게 구현되어 있습니다. 처리할 데이터를 전달하는 입력 인수와, 운영체제가 결과를 반환하는 출력 인수를 가집니다.
이런 함수를 호출하는 방식은 C 라이브러리의 다른 함수들과 동일합니다. 따라서 어떤 함수가 시스템 호출인지 아닌지를 구분하는 문제는 프로그래머 입장에서는 사실상 순수하게 개념적인 문제에 불과합니다.
시스템 호출 함수의 인수는 C 언어의 표준 타입을 사용합니다. 다만 그 성격상 메모리 주소, 파일 디스크립터(descriptor), 경로 등을 저장하는 포인터 형태인 경우가 대부분입니다.
현재 리눅스에는 약 350개의 시스템 호출이 존재합니다. 또한 스스로는 시스템 호출이 아니지만 내부적으로 시스템 호출을 통해 리소스를 관리하는 함수들도 매우 많습니다. 대표적으로 malloc(), free(), calloc(), realloc() 같은 함수들이 내부적으로 시스템 호출을 활용해 메모리를 동적으로 관리합니다.
리눅스 콘솔에서 strace 명령으로 시스템 호출 모니터링하기
시스템 호출 함수가 어떤 것인지 이해했다면, 이제 리눅스 명령 콘솔에서 strace 명령을 사용해 실시간으로 모니터링하는 방법을 알아보겠습니다. strace 명령은 매우 유연하며 시스템 모니터링에 다양한 옵션을 제공합니다. 다음 명령으로 전체 옵션 목록을 확인할 수 있습니다.
strace -h
이 명령을 실행하면 strace가 제공하는 모니터링 옵션 목록이 콘솔에 출력됩니다.

특정 프로세스의 시스템 호출을 모니터링하려면 다음과 같이 실행합니다.
strace ./process
실습을 위해 이전에 소개한 “C 언어 Connect 함수” 예제 코드를 컴파일해 사용해 보겠습니다. 이 코드는 매우 단순하지만 HTTP 서비스를 통해 Google 호스트에 접속하고, 명령을 전송하고 응답을 받을 수 있다는 점에서 흥미로운 예제입니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
int main (){
//step 1
int socket_id ;
int port = 80;
int error;
char buffer [1025];
struct hostent *server;
struct sockaddr_in client;
memset(&server, 0, sizeof(server));
//step 2
server = gethostbyname ( "www.google.com" );
if(server == NULL)
{
printf("\nError getting domain data.\n");
return 1;
}
//step 3
socket_id = socket ( AF_INET, SOCK_STREAM, 0 );
//step 4
client.sin_family = AF_INET;
client.sin_port = htons(port);
//step 5
bcopy((char *) server->h_addr,
(char *) &client.sin_addr.s_addr,
sizeof(server->h_length));
error=connect(socket_id, (struct sockaddr *) &client, sizeof(client));
if (error <0){
printf ("\nCould not establish connection to the server\n");
close( socket_id);
return 1;
}
printf ("\nConnect to : %s\n", inet_ntoa( client.sin_addr ));
while (1){
printf("To exit press Ctrl+c \nSend http command: ");
fgets(buffer, 1025, stdin);
send(socket_id, buffer, 1025, 0);
memset(&buffer, '\0', 1025);
recv(socket_id, buffer, 1025, 0);
printf("%s", buffer);
memset(&buffer, '\0', 1025);
}
}
코드를 컴파일하여 출력 파일 이름을 “connect”로 지정한 뒤, 앞에 strace 명령을 붙여 ./connect를 실행하면 시스템 호출을 모니터링할 수 있습니다.
strace ./connect
이 명령은 프로세스의 시스템 호출을 콘솔에 출력합니다. 아래 그림에서 볼 수 있듯이, 코드에서 직접 사용한 것은 socket(), connect(), send(), recv()뿐이지만, 실제 실행 과정에서는 우리가 사용한 함수들의 내부 코드에서 발생하는 다른 시스템 호출들도 함께 나타납니다.

다음 명령을 사용하면 프로세스가 종료될 때 시스템 호출 요약(summary)을 확인할 수 있습니다.

프로세스 관리 시스템 호출
| 함수 | 설명 | 헤더 |
|---|---|---|
| execl() execlp() execle() execv() execvp() execvpe() | 부모 프로세스의 PID와 리소스를 물려받아 한 프로세스를 다른 프로세스로 교체하는 시스템 호출 함수군 | unistd.h |
| fork() | 프로세스를 복제한다. | unistd.h |
| getpid() | 호출한 프로세스의 PID를 가져온다. | unistd.h |
| getppid() | 호출한 프로세스의 부모 프로세스 PID를 가져온다. | unistd.h |
| exit() | 프로세스의 종료 상태를 보고한다. | stdlib.h |
| getcwd() getwd() | 호출한 프로세스의 현재 작업 디렉터리를 문자열로 반환한다. | unistd.h |
| pause() | 시그널이 도착할 때까지 프로세스를 일시 중지한다. | unistd.h |
| wait() | 부모 프로세스가 자식 프로세스의 종료를 기다린다. | wait.h |
파일 관리 시스템 호출
| 함수 | 설명 | 헤더 |
|---|---|---|
| open() openat() | 파일을 연다. | fcntl.h |
| close() | 파일을 닫는다. | unistd.h |
| read() | 파일을 읽는다. | unistd.h |
| poll() | 특정 작업을 위해 파일의 사용 가능 여부를 확인한다. | poll.h |
| write() | 파일에 쓴다. | unistd.h |
| fcntl() | 파일 디스크립터를 조작한다. | fcntl.h |
| truncate() ftruncate() | 파일을 지정한 길이로 자른다. | unistd.h |
| creat() | 파일을 생성한다. | fcntl.h |
| chmod() fchmod() | 파일의 권한을 설정한다. | sys/stat.h |
| unlink() | 파일을 삭제한다. | unistd.h |
시그널 처리 시스템 호출
| 함수 | 설명 | |
|---|---|---|
| signal() | 시그널을 핸들러(handler)와 연결한다. | signal.h |
| kill() | 프로세스에 시그널을 보낸다. | signal.h |
| killpg() | 프로세스 그룹에 시그널을 보낸다. | signal.h |
| raise() | 호출한 프로세스 안에서 시그널의 동작을 수행한다. | signal.h |
| sigaction() | 시그널의 동작을 설정한다. | signal.h |
| sigwait() | 지정한 시그널이 도착할 때까지 호출한 스레드의 실행을 중단한다. | signal.h |
| sighold() | 호출한 프로세스의 시그널 마스크에 시그널을 추가한다. | signal.h |
| sigignore() | 시그널을 무시한다. | signal.h |
| sigblock() | 시그널을 차단한다. | signal.h |
| psignal() psiginfo() | 시그널에 대한 설명이 담긴 메시지를 출력한다. | signal.h |
소켓 처리 시스템 호출
| 함수 | 설명 | |
|---|---|---|
| socket() | 소켓을 생성한다. | socket.h |
| bind() | 소켓에 이름을 바인딩한다. | socket.h |
| connect() | 소켓을 연결한다. | socket.h |
| listen() | 소켓에서 연결 요청을 기다린다. | socket.h |
| accept() | 소켓에서 연결을 수락한다. | socket.h |
| recv() recvfrom() recvmsg() | 소켓에서 메시지를 수신한다. | socket.h |
| send() sendto() sendmsg() | 소켓으로 메시지를 전송한다. | socket.h |
| getsockname() | 디스크립터로 지정한 소켓의 주소를 반환한다. | socket.h |
| getsockopt() | 소켓 옵션을 설정하거나 가져온다. | socket.h |
마무리
이 글에서는 시스템 호출 함수가 무엇인지, 그리고 시스템 리소스를 사용하는 작업을 위해 커널 서비스를 요청하는 데 어떻게 활용되는지 설명했습니다. 사용자 프로세스가 무엇이며, 시스템 호출을 통해 커널과 어떻게 상호작용하는지도 간단히 살펴보았습니다.
독자 여러분이 이러한 기능을 빠르게 실무에 적용할 수 있도록, 가장 많이 사용되는 시스템 호출 목록과 함께 각 함수의 간단한 설명, 그리고 코드에 포함해야 할 헤더 파일 정보를 정리해 드렸습니다. 또한 명령 콘솔에서 strace 명령으로 프로세스의 시스템 호출을 모니터링하는 방법도 함께 소개했습니다. 이 내용이 리눅스 시스템 호출을 이해하고 활용하는 데 도움이 되기를 바랍니다.