Chapter 5 System calls

현대 운영체제에서, 커널은 유저 스페이스에서 동작중인 프로세스에게 시스템과 소통할 수 있는 다양한 인터페이스를 제공하고 있다. 시스템이 제공하는 인터페이스의 대부분은 하드웨어를 능률적이고 안전하게 사용할 수 있는 통신 채널로 사용된다. 그리고 이 통신채널은 '할 수 있는 것'과 '할 수 없는 것'을 명확히 구분함으로써 시스템이 안정적으로 동작할 수 있도록 한다. 이러한 인터페이스를 시스템 콜이라고 한다.

Communicating with the kernel

시스템콜은 하드웨어와 유저 프로세스간의 소통을 위한 얇은 계층이다. 시스템 콜은 3 종류의 주요한 목적이 있다.

  1. 추상화된 하드웨어 인터페이스를 유저 프로세스에게(유저 공간에게) 제공한다.
  2. 시스템의 보안과 안정성을 보장한다.
  3. 가상화된 유저 프로세스와 시스템이 소통할 수 있는 유일하고 공통적인 소통수단이어야 한다.

APIs, POSIX, and the C Library

API: Application Programming Interface

POSIX standard: UNIX 계열에서 가장 빈번하고 중요하게 사용되는 API로써 IEEE에 의해 표준으로 채택되어 사용되었다. OS의 이식성을 고려하여 고안되었다.

Portability(이식성)? 한 OS에서 사용되던 API가 다른 OS에서도 추가적인 수정 없이 목적에 맞게 실행되는 성질

C Library: C 언어를 위한 표준 라이브러리로서, ANSI C 표준에 의해 명시되었다. 이것은 상위 집합인 C POSIX 라이브러리와 동시에 개발되었다. ANSI C가 국제 표준화 기구에 의해서 채택됨에 따라, C 표준 라이브러리는 또한 ISO C library로도 불린다. C 표준 라이브러리는 매크로, 타입 정의 그리고 문자열 처리나 수학적 연산, 입출력 프로세스, 메모리 할당과 다른 운영 체제 서비스 같은 작업을 위한 함수들을 제공한다.

A meme related to interfaces in Unix is “Provide mechanism, not policy.” In other words, Unix system calls exist to provide a specific function in an abstract sense.

관련 이미지

Syscalls

커널에는 다음과 같은 형태로 시스템콜이 정의되어 있다.

SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current); // returns current->tgid
} // kernel/sys.c

SYSCALL_DEINFE#(name): 시스템콜 매크로이다. 시스템콜이 받는 인자의 갯수와 시스템콜의 이름이 표현된다. 시스템콜이 받는 인자의 갯수가 #에 들어간다. 위의 예시의 경우 getpid라는 시스템콜은 전달받는 인자의 갯수가 0개 이므로 SYSCALL_DEFINE0 매크로를 사용한다. 만약 3개의 인자를 받는 시스템콜이 있다고 하면 SYSCALL_DEFINE3을 사용한다.

  • include/linux/syscalls.h: 시스템 콜 헤더 파일
  • kernel/sys.c: 시스템 콜이 실제로 구현되어 있는 부분

System Call Numbers

시스템콜은 각자 고유한 시스템콜 번호가 있다. 시스템콜이 불렸을 때 시스템 콜과 그 고유한 번호가 매핑되어 있는 시스템콜 테이블을 보고 해당 번호에 해당하는 시스템콜을 수행한다. 한 번 부여된 시스템콜 번호는 재사용 하지 않는데, 그 이유는 기존에 존재하는 시스템들이(컴파일 된 커널들) 이미 해당 번호를 기존의 시스템콜로 인식하기 때문이다. 이러한 경우 새로운 시스템에서는 해당 시스템콜 번호를 사용하지 않는다는 의미에서 sys_ni_syscall() 함수를 시스템 콜 테이블에 매핑해둔다. 그리고 sys_ni_syscall() 함수는 시스템콜이 없다는 의미의 에러 ENOSYS 를 반환한다.

System Call Performance

리눅스의 시스템콜은 타 OS에 비해 상당히 빠르다. 리눅스의 context switch가 빠르고 시스템 콜 핸들러가 단순한 구조로 되어있기 때문이다. 따라서 시스템콜 핸들러를 수행하는 과정에서 커널에 들어가고 나오는 과정이 상당히 간결하다.

System Call Handler

유저 공간을 수행중인 프로세스가 커널 코드를 직접적으로 수행하는 것은 보안상의 이유로 불가능하다. 그렇기 때문에 흔히 유저 함수가 수행되듯이 시스템 콜을 function call하는 것은 불가능하다. 왜냐하면 커널의 메모리 영역은 보호되어 있기 때문이다. 만약 유저가 커널 영역에 직접적으로 접근할 수 있게 되면 커널이 추구하는 안정성과 보안성이 위협받는다.

따라서, 유저 공간에서 수행중인 프로세스는 반드시 어떠한 방식으로든지 커널에게 시스템 콜을 수행하고 싶다는 신호를 주어야 한다. 그러면 시스템이 커널에 접근 할 수 있는 커널모드로 커널 영역에 접근하여 유저 어플리케이션을 대신하여 해당 시스템콜을 수행하여 준다. 그 어떠한 방식이 바로 소프트웨어 인터럽트(software interrupt)이다.

소프트웨어 인터럽트가 수행되는 과정은 다음과 같다.

  1. Exception을 발생시킨다.
  2. 시스템이 커널 모드로 바뀐다.
  3. 커널 모드인 상태로 exception handler(interrupt handler)를 수행한다.
  4. 소프트웨어 인터럽트는 x86에서 128번(0x80) 인터럽트이다.
  5. exception handler는 128번 인터럽트, 즉 systemcall handler를 수행하게 된다.
  6. 시스템 콜을 처리하는 핸들러(syscall entry point)는 x86에서 어셈블리어로 작성되어 있다.(arch/x86/entry/entry_64.S)

  7. 시스템 콜 핸들러는 architecture-dependent하다.

  8. 최근 x86 프로세서는 SYSENTER SYSEXIT 이라는 시스템콜을 위한 빠른 명령어를 제공해주고 있다. 이는 소프트웨어 인터럽트 명령어를 이용한 시스템 콜 호출보다 성능이 좋다. 어떤 방식으로 시스템 콜이 호출되든지, 유저 모드에서 커널 모드로 넘어가기 위해서는 exception(or trap)을 발생시켜야 한다.

Denoting the Correct System Call

시스템 콜을 수행하기 위해서 커널 영역으로 진입하는 것까지 성공했다. 그러나 다양한 시스템콜이 동일한 Entry를 통해서 접근하기 때문에 어떤 시스템콜이 호출되었는지 판별할 필요가 있다. 따라서 커널에 반드시 시스템콜의 종류(시스템콜의 고유 번호)를 알려주어야 한다. x86에서는 eax 레지스터를 이용하여 커널에 전달한다. 즉, 시스템 콜을 호출하는 과정에서 커널 모드로 진입하기 전에 유저 모드에서 eax 레지스터에 호출하기 원하는 시스템 콜의 번호를 저장해둔다. 그리고 시스템 콜 핸들러가 수행되는 시점에 커널 모드에서 eax 레지스터에 저장되어 있는 시스템 콜 번호를 읽어서 해당 번호와 매핑되어 있는 시스템 콜 함수를 수행한다. 시스템 콜 번호에 매핑되어 있는 함수를 수행하기 전 보호의 차원에서 시스템 콜의 전체 갯수를 나타내는 NR_syscalls 값과 비교하여 이보다 작으면 정상적으로 해당 시스템콜 함수를 수행한다. 다른 architecture들도 비슷한 방식으로 구현되어 있다.

Parameter Passing

시스템 콜 번호와 함께 대개의 시스템 콜들은 함수를 수행하기 위해 입력 인자(parameters)들을 필요로 한다. 어떤 방식으로든 유저 영역에서 커널 영역으로 파라미터들을 보내야 하는데, 가장 쉬운 방법은 시스템 콜 번호를 eax 레지스터에 실어 보낸 것과 같이 레지스터들에 파라미터들을 실어 보내는 방법이다. ebx, ecx, edx, esi, edi 와 같은 레지스터들에 파라미터들을 유저공간에 있을 때 저장해두었다가 후에 시스템 콜이 실제 함수를 수행할 때 사용하도록 한다. 만약 레지스터의 갯수보다 더 많은 수의 인자를 넘기고 싶다면, 레지스터 하나에 유저 공간을 가리키는 포인터 값을 저장하고 해당 유저 공간에 파라미터들을 저장한다. 레지스터를 이용하여 값을 주고 받는 방식은 시스템콜의 반환값을 돌려줄 때에도 사용되며 x86에서는 eax 레지스터가 사용된다.

System Call Implementation

시스템 콜 하나를 구현하는 것은 시스템 콜이라는 개념을 고안해내고 구현하는 것보다는 훨씬 쉬운 일이다.

시스템 콜을 구현한 함수를 만들고, 번호를 할당시켜 시스템 콜 테이블에 등록하자!

Implementing System Calls

시스템 콜을 구현하기 위해서 가장 먼저 해야할 일은 시스템 콜의 목적을 명확하게 하는 일이다. 단일한 시스템 콜은 단 하나의 목적을 수행해야 한다. 시스템 콜은 최대한 적은 인자를 받아야 하며, 간단하고 깔끔한 인터페이스를 가져야만한다.

Verifying the Parameters

시스템 콜은 반드시 인자들을 검사해야한다. 혹여나 유효하지 않은 값이라면 커널 모드에서는 유저 영역에 대한 제한이 없으므로 함부로 접근하여 값을 변경시키는 일이 일어날 수 있다. 이러한 실수는 시스템의 안전성이나 안정성을 침해하게 된다.

가장 중요한 체크리스트 중 하나는 유저로부터 제공된 포인터들이 유효한 포인터들인지를 확인하는 것이다.

  • 포인터는 유저 공간을 가리키고 있어야한다. 커널로 하여금 커널 영역의 데이터를 읽도록 꾀를 부리면 안 된다.
  • 포인터는 해당 프로세스의 주소 공간을 가리키고 있어야 한다. 커널로 하여금 다른 프로세스의 데이터를 참조하도록 꾀를 부리면 안 된다.
  • 만약 읽는다면, 메모리가 읽을 수 있는 메모리인지를 확인하여야 한다. 만약 쓴다면, 메모리가 쓸 수 있는 메모리인지를 확인하여야 한다. 만약 실행한다면 메모리가 실행 가능한 메모리인지를 확인하여야 한다. 이렇듯 프로세스는 메모리 접근 권한을 넘어서는 행동을 하면 안 된다.

copy_from_user() 함수와 copy_to_user() 함수는 커널이 유저 영역의 데이터를 읽거나 쓸 때 여러 유의점들을 확인하여 주고 만약 에러가 있다면 에러를 도출해낸다.

System Call Context

시스템 콜이 수행중일 때에는 커널이 해당 시스템 콜을 부른 프로세스의 context 속에 있다. current 는 현재 task를 가리키고 있으며, 프로세스 context에 있는 경우 커널은 잠들 수도 있고 CPU를 빼앗기기도 한다.

Final Steps in Binding a System Call

시스템 콜을 새로 작성했다면, 새롭게 만들어진 그 시스템 콜을 등록하는 것은 별로 어렵지 않다.

  1. 시스템 콜 테이블의 마지막에 새로운 시스템 콜의 진입점을 추가한다.
  2. 각 architecture 마다 해당 시스템 콜의 번호를 <asm/unistd.h>에 추가한다.
  3. 추가된 시스템 콜을 커널 이미지에 컴파일하여 최신 커널 이미지를 생성한다.

Accessing the System Call from User-space

일반적으로, C 라이브러리는 시스템 콜을 제공한다. 유저 프로그램은 C 라이브러리의 함수들을 이용하여 시스템 콜을 이용할 수 있다.

리눅스는 시스템 콜을 쉽게 사용할 수 있도록 몇 가지의 매크로를 제공하고 있다. 이 매크로들은 시스템콜을 수행하기 앞서 레지스터들에 내용물을 채우거나 트랩 명령어를 발생시키는 일과 같은 일련의 준비 작업들을 해준다. _syscalln()과 같은 형태의 매크로이다.(n은 넘겨주는 인자의 갯수)

Why Not to implement a System call

현재 리눅스는 안정성과 시스템 콜 계층의 단순화를 위해 가급적이면 새로운 시스템 콜을 추가하지 않는다.

results matching ""

    No results matching ""