reference : 리눅스 커널 심층 분석, 로버트 러브 지음, 황정동 옮김
커널과 통신
시스템 콜은 하드웨어와 사용자 공간 프로세스 사이에 있는 계층이다. 이 계층은 다음과 같은 세 가지 역할을 한다.
첫째, 사용자 공간에 하드웨어 인터페이스를 추상화된 형태로 제공(파일 입출력 시 애플리케이션은 디스크나 저장 매체의 형식이나 파일시스템 형식 같은 것을 신경 쓸 필요가 없다.
둘째, 시스템 콜은 시스템 보안 및 안정성을 제공(커널이 시스템 자원과 사용자 공간 사이에서 중재자 역할을 하기 때문에, 커널이 접근권한과 같은 기준을 적용해 통제할 수 있다. 예를 들면, 하드웨어를 잘못 사용하거나 다른 프로세스의 자원을 빼았는 등의 동작을 막을 수 있다.
셋째, 사용자 공간(user-space)과 기타 시스템 사이에 계층을 둠으로써 프로세스별 가상 시스템 환경을 제공할 수 있다. 만약 애플리케이션이 아무런 제약없이 시스템 자원에 접근할 수 있다면 멀티태스킹이나 갓아 메모리를 구현하는 것은 거의 불가능에 가깝다.
리눅스의 시스템 콜은 사용자 공간에서 커널과 상호작용할 수 있는 유일한 수단이다. 트랩(trap)을 제외하면 시스템 콜은 정상적인 방법으로 커널로 진입하는 유일한 수단이다. 장치파일이나 /proc 파일시스템을 이용하는 다른 인터페이스도 결국은 시스템 호출을 통하게 되어 있다.
APIs, POSIX, and the C Library
애플리케이션은 일반적으로 시스템 콜을 직접 사용하지 않고, 사용자 공간에 구현된 애플리케이션 프로그래밍 인터페이스(API, Application Programming Interface)를 이용한다. 이 때문에 애플리케이션이 사용하는 인터페이스와 커널이 제공하는 인터페이스 사이에 직접적인 연관이 없다는 점이 아주 중요하다. API는 애플리케이션이 사용하는 프로그래밍 인터페이스다. 이 인터페이스는 하나 또는 그 이상의 시스템 콜을 사용해 구현되며, 경우에 따라 시스템 콜을 전혀 사용하지 않을 수도 있다. 또 시스템에 따라 내부 구현이 전혀 달라도(시스템 콜 구현 방식 등이 달라도) 같은 형태의 API를 사용함으로써 시스템이 달라도 애플리케이션에 동일한 인터페이스를 제공할 수도 있다. 다음 그림은 POSIX API와 C Library, 시스템 콜 사이의 관계를 보여준다.
유닉스에서 가장 유명한 API는 바로 POSIX 표준이다. 기술적인 용어로 말하자면, POSIX는 유닉스 기반 운영체제 간의 이식성 제공을 목적으로 정해진 IEEE 표준의 집합이다. 리눅스도 역시 POSIX 및 SUSv3 표준을 따른다.
POSIX는 API와 시스템 콜간의 관계를 보여주는 아주 훌륭한 예다. 대부분의 유닉스 시스템은 POSIX에 정의된 API와 시스템 콜 간에 밀접한 대응 관계를 가지고 있다. 사실 POSIX 표준은 초기 유닉스 시스템의 인터페이스를 본따 만든 것이다.
리눅스의 시스템 콜 인터페이스는 다른 유닉스 시스템과 마찬가지로 C Library 형태로 제공된다. C Library에는 표준 C Library와 시스템 콜 인터페이스 등 유닉스 시스템의 주요 API가 구현되어 있다. 이러한 C Library는 모든 C 프로그램이 사용하며, C의 특성으로 인해 다른 프로그래밍 언어의 프로그램에서 사용할 수 있게 감싼 형태를 쉽게 만들 수 있다. C Library는 대다수의 POSIX API를 추가로 제공한다.
애플리케이션 개발자 관점에서 보면 시스템 콜은 중요하지 않고 API에만 관심이 있다. 커널은 시스템 콜만 신경 쓴다. 어떤 라이브러리가 콜하는지 어떤 애플리케이션이 시스템 콜을 사용하는지에 대해서는 커널이 신경쓰지 않는다. 하지만 커널은 어떤 시스템 콜이 많이 사용될지, 시스템 콜의 유연함을 어떻게 유지할 것인지를 염두에 두어야 한다.
시스콜(Syscalls)
리눅스에서는 대체로 시스템 콜을 시스콜이라고 줄여서 부른다. 시스템 콜은 보통 C Library에 정의된 함수를 호출하는 방식으로 사용한다. 이 함수는 0개 혹은 하나 이상의 인자(arguments)를 받고, 하나 이상의 side effect를 발생시킬 수 있다. 파일에 데이터를 기록하거나 지정한 포인터에 특정 데이터를 저장하는 동작 등을 예로 들 수 있다. 시스템 콜은 성공과 실패에 대한 정보를 제공하는 long 형의 값을 반환하기도 한다.(64비트 아키텍처의 호환성을 위해 long 형을 사용한다.) 일반적으로 오류가 발생한 경우에는 음수값을 반환하고, 성공한 경우에는 0을 반환한다(항상 그런 것은 아님). 시스템 콜에서 오류가 발생할 경우 C Library는 전역 변수인 errno에 특정 오류코드를 기록한다. 라이브러리 함수인 perror()를 사용하면 이 변수의 값을 사람이 보기 편한 문자열 형태로 바꿀 수 있다.
시스템 콜은 정의된 특정 동작을 수행한다. 예를 들어, getpid()라는 시스템 콜은 현재 프로세스의 PID 값에 해당하는 정수값을 반환한다.이 시스콜은 커널에서 아주 간단하게 구현된다.
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current); // current->tgid 값을 반환한다.
}
함수 정의로는 구현 내용에 대해 아무것도 알 수 없다는 점에 주의하자. 커널은 시스템 콜이 의도하는 동작을 반드시 제공해야 하지만, 결과가 정확하기만 하다면 구현 방식은 어떻게 해도 상관 없다.
SYSCALL_DEFINE0는 인자가 없는(그래서 0이 붙음) 시스템 콜을 정의하는 매크로다. 이 매크로는 실제로 다음과 같이 확장된다.
asmlinkage long sys_getpid(void)
시스템 콜을 정의하는 방법을 살펴보자. 먼저, 함수 정의 부분에 asmlinkage 지시자가 있다. 이 지시자는 해당 함수의 인자를 스택에서만 찾으라고 컴파일러에게 알려준다. 모든 시스템 콜에는 이 지시자가 사용된다. 그다음 함수의 반환값은 long 형이다. 32비트 시스템과 64비트 시스템 간의 호환성을 유지하기 위해 사용자 공간에서 int형을 반환하는 시스템 콜은 커널 내부에서는 long 형을 반환한다.마지막으로 getpid() 시스템 콜은 커널 내부에서는 sys_getpid()라는 이름으로 정의된다. 이는 리눅스의 모든 시스템 호출이 사용하는 명명규칙이다. bar()라는 시스템 콜이 있으면 커널에는 sys_bar()라는 함수로 구현한다.