시스템 콜의 특징

이번 절에서는 시스템 콜의 특징을 알아보겠습니다.

앞서 알아봤듯이 시스템 콜은 유저 모드에서 커널 모드로 진입하는 관문입니다. 소프트웨어 구조 관점에서 보면 시스템 콜은 유저 공간과 커널 공간 사이의 가상 계층으로 볼 수도 있습니다. 이 계층은 다음과 같은 특징이 있습니다.

1. 시스템 콜 계층으로 시스템 안정성과 보안을 지킬 수 있습니다. 유저 모드에서 애플리케이션이 커널 공간에 아무런 제약 없이 접근한다고 가정해 봅시다. 실수로 애플리케이션이 커널 코드 영역의 메모리를 오염시키면 시스템이 오동작할 가능성이 높습니다. 그래서 유저 모드에서 시스템 콜로만 커널 모드에 진입해서 제한된 메모리 공간에 접근하는 것입니다.

2. 유저 애플리케이션에서 추상화된 하드웨어 인터페이스를 제공합니다. 유저 모드에서 구동 중인 애플리케이션 입장에서 하나의 파일 시스템 위에서 구동 중인 것으로 착각하게 합니다.

3. 시스템 콜 구현으로 유저 애플리케이션의 호환성과 이식성을 보장할 수 있습니다. 대부분의 리눅스 배포판은 시스템 콜 인터페이스를 POSIX(Portable Operating System Interface)라는 유닉스 표준 규약에 맞게 구현합니다. 이를 통해 유저 애플리케이션 코드를 라즈베리 파이, 안드로이드 등 리눅스 계열의 시스템과 유닉스 운영체제에서도 구동할 수 있습니다.

4. 유저 공간에서 실행되는 애플리케이션은 커널과 독립적으로 구동됩니다. 유저 애플리케이션 입장에서는 파일 시스템과 프로세스 생성과 같은 내부 동작에 신경 쓸 필요가 없습니다. 

또한 시스템 콜은 ARM 아키텍처와 연관이 깊은 동작입니다. ARM 프로세서는 시스템 콜을 익셉션의 한 종류인 소프트웨어 인터럽트로 실행하기 때문입니다. ARM 프로세스 관점에서 시스템 콜을 어떻게 구현했는지 함께 살펴봅시다.

이전 절에서 시스템 콜을 구성하는 주요 개념을 알아봤습니다. 이번에는 시야를 넓혀 전체 리눅스 시스템에서의 시스템 콜 실행 흐름을 살펴보겠습니다.  


시스템 콜의 전체 흐름 파악하기

 


다음 그림은 이번 장에서 다룰 시스템 콜의 전체 흐름입니다.

 

 
그림 11.2 시스템 콜의 전체 흐름

먼저 위 그림에서 유저 공간이라고 표시된 부분을 눈으로 따라가 봅시다.

open(), write(),read() 함수는 파일을 열거나 읽고 쓰는 파일 입출력 동작이고, fork()와 exit() 함수는 프로세스 생성 및 종료와 연관된 동작을 실행합니다. 이를 리눅스 저수준 함수라고 부릅니다. 다른 관점에서 GNU C 라이브러리로 진입하는 함수이며, API(Application Programming Interface)라고도 합니다.


리눅스 시스템에는 390여 개의 표준 함수가 있는데 위 그림에서 대표적인 함수 5개를 표현한 것입니다.

라즈베리 파이에서 다음 파일을 열어보면 시스템 콜 번호를 확인할 수 있습니다.

/usr/include/arm-linux-gnueabihf/asm/ unistd-common.h
#define __NR_restart_syscall  (__NR_SYSCALL_BASE+  0)
#define __NR_exit (__NR_SYSCALL_BASE+  1)
#define __NR_fork (__NR_SYSCALL_BASE+  2)
...
#define __NR_pkey_mprotect (__NR_SYSCALL_BASE+394)
#define __NR_pkey_alloc (__NR_SYSCALL_BASE+395)
#define __NR_pkey_free (__NR_SYSCALL_BASE+396)

 


시스템 콜의 세부 실행 단계

 

 


시스템 콜을 제대로 이해하려면 시스템 콜을 발생시키는 유저 공간부터 시스템 콜 핸들러를 실행하는 커널 공간 계층까지 전체 흐름을 살펴볼 필요가 있습니다. 시스템 콜의 동작 흐름은 크게 4단계로 나눌 수 있습니다.

1단계: 리눅스 저수준 표준 함수 호출


유저 애플리케이션에서 파일 시스템에 접근해서 파일을 열고 읽고 쓰려면 open(), write(), read() 함수를 각각 호출해야 합니다. 혹은 프로세스를 생성하거나 종료할 때 fork()나 exit() 함수를 호출합니다. 이 함수들을 API라고 하며, 유저 애플리케이션에서 리눅스 커널에서 제공하는 기능을 사용하기 위해 만든 인터페이스를 의미합니다. 이 인터페이스는 모두 리눅스 시스템에서 제공하는 GNU C 라이브러리 내부에 구현돼 있습니다.

2단계: 유저 공간에서 시스템 콜 실행


리눅스 시스템의 저수준 함수를 호출하면 GNU C 라이브러리 내부에 있는 어셈블리 코드가 실행됩니다. 이때 시스템 콜이 발생합니다. 이 과정을 제대로 이해하려면 어떤 ARM 어셈블리 명령어로 시스템 콜을 발생시키는지 살펴볼 필요가 있습니다.

3단계: 커널 공간에서 시스템 콜 실행

 

시스템 콜이 실행되면 커널 공간으로 이동해 시스템 콜 테이블에 접근합니다. 이 시스템 콜 테이블로 시스템 콜 번호에 해당하는 시스템 콜 핸들러 함수로 분기됩니다. 시스템 콜 동작에 따라 호출되는 시스템 콜 핸들러 함수는 다음과 같습니다.

 가상 파일 시스템: sys_open()/sys_write()/sys_read() 함수
 프로세스 생성 및 종료: sys_clone()/sys_exit_group() 함수


시스템 콜 핸들러 함수는 리눅스 저수준 함수 앞에 sys_ 접두사가 붙는 경우가 대부분입니다. write() 함수의 시스템 콜 핸들러는 sys_write() 함수이고, read() 함수의 시스템 콜 핸들러는 sys_read() 함수입니다. 하지만 모든 시스템 콜 핸들러 함수가 이 규칙을 따르지는 않습니다. 리눅스 저수준 함수인 fork()의 경우 sys_clone() 시스템 콜 핸들러가 실행됩니다.

4단계: 시스템 콜 핸들러 실행

 

시스템 콜 핸들러에서는 유저 공간에서 전달한 인자에 오류가 있는지 체크합니다. 이후 시스템 콜의 종류에 따라 가상 파일 시스템 계층이나 프로세스 관리 함수에 접근합니다.

여기까지 유저 공간에서 커널 공간까지 시스템 콜의 처리 과정을 알아봤습니다.

+ Recent posts