리눅스 시스템 호출은 우리가 매일 사용하는 기능이다. 하지만 시스템 호출이 사용자 영역에서 커널 영역으로 어떻게 넘어가는지 알고 있는가? 리눅스 시스템 호출 인터페이스(SCI, System Call Interface)를 탐험하고 새로운 시스템 호출을 추가하는 방법(과 다른 대안)을 배우고, SCI 관련 유틸리티를 살펴보자.
이 기사에서, 리눅스 SCI를 탐험하고, 2.6.20 커널에 시스템 호출을 추가하는 방법과 이 함수를 사용자 영역에서 사용하는 방법을 보여줄 계획이다. 또한 시스템 호출 개발에 유용한 몇몇 함수와 시스템 호출 대안을 살펴보겠다. 마지막으로 프로세스 사용을 추적하는 기능과 같이 시스템 호출과 관련이 있는 몇몇 종속 메커니즘을 살펴본다.
리눅스에서 시스템 호출 구현은 아키텍처에 따라 다르지만, 특정 아키텍처 내부에서조차 달라질 수 있다. 예를 들어, 예전 x86 프로세서는 사용자 영역에서 커널 영역으로 이주하는 과정에서 인터럽트 메커니즘을 활용했지만, 신형 IA-32 프로세서는 이런 전환을 최적화하는 (sysenter와 sysexit와 같은) 명령어를 제공한다. 여러 옵션이 존재하며 최종 결과도 복잡하므로, 인터페이스 세부 사항에 대해서는 주마간산으로 빠르게 넘어가겠다. 세부 사항은 이 기사 마지막에 정리한 참고자료를 참조하자.
SCI를 수정하기 위해 SCI 내부를 완벽하게 이해하고 있을 필요는 없기에, 시스템 호출 과정을 단순하게 묘사해보았다(그림 1 참조). 각 시스템 호출은 단일 진입점을 통해 커널로 다중화되어 들어간다. eax 레지스터를 사용해서 호출 대상 특정 시스템 호출을 지정하며, 이는 (사용자 영역 응용 프로그램에서 부르는 호출마다) C 라이브러리로 정의되어 있다. C 라이브러리가 시스템 호출 색인과 인수를 받으면, 소프트웨어 인터럽트를 호출하며(인터럽트 0x80). 이 결과로 (인터럽트 처리기를 통해) system_call 함수를 수행한다. 이 함수는 eax 내용에 따라 모든 시스템 호출을 다룬다. 몇 가지 간단한 테스트를 해보면, system_call_table과 eax에 담긴 색인을 사용해서 실제 시스템 호출이 이뤄진다. 시스템 호출에서 반환된 값은 결국 syscall_exit에 도달하며 resume_userspace 호출을 통해 사용자 영역으로 되돌아간다. 실행은 C 라이브러리에서 다시 계속되며, 사용자 응용 프로그램으로 돌아오는 과정을 밟는다.
그림 1. 인터럽트 방법을 활용하는 시스템 호출을 단순한 흐름으로 표현
SCI 핵심은 시스템 호출 역다중화(demultiplexing) 테이블이다. 그림 2에 보여주는 이 테이블은 eax에 넘어오는 색인을 사용해 테이블(sys_call_table)에서 호출할 시스템 호출을 파악한다. 테이블 내용 예와 항목 위치 역시 그림에 나타나 있다. (역다중화 관련 내용이 궁금하면, "시스템 호출 역다중화" 보충 기사를 읽어보기 바란다.)
그림 2. 시스템 호출 테이블과 다양한 연관 관계
새로운 시스템 호출을 추가하는 작업은 대부분 절차에 따라 가능하다. 물론 몇 가지 사항은 주의해야 한다. 이 절에서는 새로운 시스템 호출을 구현하고 사용자 영역에 존재하는 응용 프로그램이 활용하는 방법을 예를 들어 살펴보기로 하자.
커널에 새로운 시스템 호출을 추가하는 세 가지 기본 단계는 다음과 같다.
- 새로운 함수 추가
- 헤더 파일 갱신
- 새로운 함수를 위한 시스템 호출 테이블 갱신
주의: 이 과정에는 사용자 영역 쪽 내용이 빠져 있는데, 나중에 다시 살펴보겠다.
종종 함수를 위한 새로운 파일을 생성한다. 하지만 설명의 초점을 흐리지 않도록 존재하는 원시 파일에 새 함수를 추가하겠다. Listing 1에 나오는 두 함수는 시스템 호출을 위한 간단한 예다. Listing 2는 포인터 인수를 사용하는 좀 더 복잡한 함수를 소개한다.
Listing 1. 시스템 호출 예를 위한 간단한 커널 함수
asmlinkage long sys_getjiffies( void )
{
return (long)get_jiffies_64();
}
asmlinkage long sys_diffjiffies( long ujiffies )
{
return (long)get_jiffies_64() - ujiffies;
}
|
Listing 1에서, jiffies 감시를 위해 함수 둘을 제공한다(jiffies에 대한 정보가 필요하면 보충 기사인 "커널 jiffies"를 읽어본다). 첫 함수는 현재 jiffies를 반환하며, 둘째 함수는 현재 값과 호출자가 전달한 값을 비교해 차이점을 반환한다. asmlinkage 변경자에 주목하자. (linux/include/asm-i386/linkage.h에 정의된) 이 매크로는 모든 함수 인수를 스택으로 전달하도록 컴파일러에 요청한다.
Listing 2. 시스템 호출 예를 위한 최종 커널 함수
asmlinkage long sys_pdiffjiffies( long ujiffies,
long __user *presult )
{
long cur_jiffies = (long)get_jiffies_64();
long result;
int err = 0;
if (presult) {
result = cur_jiffies - ujiffies;
err = put_user( result, presult );
}
return err ? -EFAULT : 0;
}
|
Listing 2는 셋째 함수를 제공한다. 이 함수는 long과 __user로 정의된 long을 가리키는 포인터라는 인수 둘을 받아들인다. __user 매크로는 단순히 컴파일러에게 (noderef를 통해) 포인터는 역참조되면 안 된다는 사실을 알려준다(현재 주소 공간에서는 의미가 없기 때문이다). 이 함수는 두 jiffies 값의 차이점을 계산해서 사용자 영역 포인터를 통해 사용자에게 결과를 제공한다. put_user 함수는 presult가 지정하는 위치에서 사용자 영역으로 결과 값을 저장한다. 이 연산 도중에 오류가 발생하면, 반환되면서 사용자 영역 호출자에게 문제를 보고한다.
단계 2에서, 헤더 파일을 수정해 시스템 호출 테이블에 새로운 함수가 들어갈 자리를 만든다. 이렇게 하기 위해 linux/include/asm/unistd.h 헤더 파일에 새로운 시스템 호출 번호를 갱신한다. Listing 3에서 굵은 글씨에 주목하자.
Listing 3. unistd.h를 수정해 새로운 시스템 호출이 들어갈 자리를 만든다.
#define __NR_getcpu 318
#define __NR_epoll_pwait 319
#define __NR_getjiffies 320
#define __NR_diffjiffies 321
#define __NR_pdiffjiffies 322
#define NR_syscalls 323
|
여기까지 작업하면서 시스템 호출 구현부와 이를 대표하는 번호까지 따내었다. 이제 (테이블 색인인) 번호 등록과 함수 자체를 구현하는 작업이 남아있다. 이는 3단계로 시스템 호출 테이블 갱신이다. Listing 4에서 linux/arch/i386/kernel/syscall_table.S를 수정해 Listing 3에서 보여준 특정 색인에 연결된 새로운 함수를 등록한다.
Listing 4. 새로운 함수를 추가해 시스템 호출 테이블을 수정하기
.long sys_getcpu
.long sys_epoll_pwait
.long sys_getjiffies /* 320 */
.long sys_diffjiffies
.long sys_pdiffjiffies
|
주의: 이 테이블 크기는 심볼 상수인 NR_syscalls가 정의한다.
이 시점에서, 커널은 갱신되었다. 사용자 응용 프로그램 테스트에 앞서 커널을 다시 컴파일해 부팅이 가능한 새 이미지를 준비해야 한다.
리눅스 커널은 사용자 영역과 시스템 호출 인수를 주고받기 위해 여러 함수를 제공한다. 먼저 (get_user나 put_user처럼) 기본 유형을 위한 단순한 함수 집합이 있다. 구조체나 배열과 같은 자료 블록을 움직이기 위해서는 copy_from_user나 copy_to_user라는 다른 함수 집합을 사용한다. NULL 문자로 끝나는 문자열을 움직이려면 strncpy_from_user와 strlen_from_user 같은 또 다른 함수 집합을 사용한다. 사용자 영역 포인터가 유효한지 확인하기 위해 access_ok라는 함수를 호출할 수도 있다. 이런 함수 집합은 linux/include/asm/uaccess.h에 정의되어 있다.
주어진 연산을 위해 사용자 영역 포인터 검증하는 데 access_ok 매크로를 사용한다. 이 함수는 접근 유형(VERIFY_READ나 VERIFY_WRITE), 사용자 영역 메모리 블록을 가리키는 포인터, 블록 크기(바이트 단위)를 받는다. 이 함수는 성공일 때 0을 반환한다.
int access_ok( type, address, size ); |
커널과 사용자 영역 사이에 (int나 long과 같은) 단순 타입 자료를 이동하려면 get_user와 put_user를 사용해 쉽게 프로그래밍이 가능하다. 이 매크로는 값과 값을 가리키는 포인터를 받아들인다. get_user 함수는 사용자 영역 주소(ptr)가 지정하는 값을 지정된 커널 변수(var)로 옮긴다. put_user 함수는 커널 변수(var)가 지정하는 값을 사용자 영역 주소(ptr)로 옮긴다. 두 함수는 성공일 때 0을 반환한다.
int get_user( var, ptr ); int put_user( var, ptr ); |
구조체나 배열과 같은 더 큰 객체를 이동하려면, copy_from_user와 copy_to_user 함수를 사용한다. 이 두 함수는 사용자 영역과 커널 사이에 전체 자료 블록을 옮긴다. copy_from_user 함수는 사용자 영역에서 커널 영역으로 자료 블록을 옮기며, copy_to_user는 커널에서 사용자 영역으로 자료 블록을 옮긴다.
unsigned long copy_from_user( void *to, const void __user *from, unsigned long n ); unsigned long copy_to_user( void *to, const void __user *from, unsigned long n ); |
마지막으로, NULL로 끝나는 문자열을 사용자 영역에서 커널로 복사하려면 strncpy_from_user 함수를 사용한다. 이 함수 호출 전에 strlen_from_user 매크로를 호출해 사용자 영역 문자열 크기를 얻을 수 있다.
long strncpy_from_user( char *dst, const char __user *src, long count ); strlen_user( str ); |
이런 함수 집합은 커널과 사용자 영역 사이에 메모리를 이동하는 기본 기능을 제공한다. (수행할 검사량을 줄이기 위해) 몇몇 추가 함수가 존재한다. uaccess.h에서 이런 함수를 찾을 수 있다.
이제 새로운 시스템 호출을 커널에 추가했으므로, 사용자 영역 응용 프로그램에서 새로운 시스템 호출을 사용하기 위해 필요한 사항을 점검해 보자. 새로운 커널 시스템 호출을 사용하는 방법은 두 가지다. 첫 번째 방법은 (실제 동작 코드에 사용하기에는 부족하지만) 편리한 방법이며, 두 번째 방법은 좀 더 손이 많이 가는 전통적인 방법이다.
첫 번째 방법에서는, syscall 함수에 새로운 함수를 가리키는 색인을 넣어 호출한다. syscall 함수를 사용하면, 호출 색인과 인수 집합을 지정하는 방법으로 시스템 호출을 부를 수 있다. 예를 들어, Listing 5에 나온 간단한 응용 프로그램은 색인을 사용해 sys_getjiffies를 호출한다.
Listing 5. 시스템 호출을 위해 syscall 사용하기
#include <linux/unistd.h>
#include <sys/syscall.h>
#define __NR_getjiffies 320
int main()
{
long jiffies;
jiffies = syscall( __NR_getjiffies );
printf( "Current jiffies is %lx\n", jiffies );
return 0;
}
|
코드를 보면 알겠지만, syscall 함수는 첫 번째 인수로 사용할 시스템 호출 테이블 색인을 포함한다. 계속해서 호출 색인 다음에 전달할 인수가 이어진다. 대다수 시스템 호출은 SYS_ 심볼 상수를 포함해서 __NR_ 색인에 사상을 명시한다. 예를 들어, syscall에 __NR_getpid 색인을 넘기는 방법은 다음과 같다.
syscall( SYS_getpid )
|
syscall 함수는 아키텍처에 밀접하며, 커널에 제어권을 넘기는 메커니즘을 사용한다. 인수는 __NR 색인을 (libc를 빌드할 때 정의되는) /usr/include/bits/syscall.h에서 제공하는 SYS_ 심볼로 사상하는 방법에 의존한다. 절대로 이 파일을 직접 참조하지 말자. 그 대신 /usr/include/sys/syscall.h를 사용하자.
전통적인 방법은 시스템 호출 색인 관점에서 커널 서비스에 대한 함수(이렇게 해야 올바른 커널 서비스 호출이 가능해진다)와 인수가 일치하도록 요구한다. 리눅스는 이런 기능을 위해 매크로 집합을 제공한다. _syscallN 매크로는 /usr/include/linux/unistd.h에 정의되어 있으며, 형식은 다음과 같다.
_syscall0( ret-type, func-name )
_syscall1( ret-type, func-name, arg1-type, arg1-name )
_syscall2( ret-type, func-name, arg1-type, arg1-name, arg2-type, arg2-name )
|
_syscall 매크로는 인수 여섯 개까지 정의가 가능하다(여기서는 세 개만 사용하고 있다).
이제 _syscall 매크로를 사용해 새로운 시스템 호출을 사용자 영역으로 보이게 만드는 방법을 살펴볼 차례다. Listing 6은 _syscall 매크로가 정의한 시스템 호출을 사용하는 응용 프로그램을 보여준다.
Listing 6. 사용자 영역 응용 프로그램 개발을 위해 _syscall 매크로를 활용하기
#include <stdio.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
#define __NR_getjiffies 320
#define __NR_diffjiffies 321
#define __NR_pdiffjiffies 322
_syscall0( long, getjiffies );
_syscall1( long, diffjiffies, long, ujiffies );
_syscall2( long, pdiffjiffies, long, ujiffies, long*, presult );
int main()
{
long jifs, result;
int err;
jifs = getjiffies();
printf( "difference is %lx\n", diffjiffies(jifs) );
err = pdiffjiffies( jifs, &result );
if (!err) {
printf( "difference is %lx\n", result );
} else {
printf( "error\n" );
}
return 0;
}
|
__NR 색인이 응용 프로그램에서 필요한 이유는 _syscall 매크로는 __NR 색인을 만들기 위해 함수 이름을 사용하기 때문이다(getjiffies -> __NR_getjiffies). 하지만 이와 같은 결과로 인해 다른 시스템 호출과 마찬가지로 이름으로 커널 함수를 호출할 수 있다.
시스템 호출은 커널에서 서비스를 요청하는 효과적인 방법이다. 시스템 호출의 가장 큰 문제점은 표준화된 인터페이스라는 사실이다. 새로운 시스템 호출을 커널에 추가하기가 어렵기 때문에, 시스템 호출을 추가하는 대신 다른 방법을 통해 비슷한 목표를 달성해야 한다. 공개 리눅스 커널에 시스템 호출을 끼워넣을 의사가 없다면, 커널 서비스를 사용자 영역에 제공하기 위한 편리하고 효율적인 방법이 바로 시스템 호출이다.
서비스를 사용자 영역으로 보이게 만드는 또 다른 방법은 /proc 파일 시스템이다. /proc 파일 시스템은 디렉터리와 파일 형태로 사용자에게 보이는 가상 파일 시스템이며, 새로운 서비스를 위한 커널 내부 인터페이스를 파일 시스템 인터페이스 형식으로 보여준다(읽기, 쓰기, 기타 등등).
리눅스 커널은 (프로세스가 받은 시그널은 물론이고) 프로세스가 호출한 시스템 호출을 추적하는 유용한 방법을 제공한다. 이 유틸리티는 strace로, 추적을 원하는 응용 프로그램을 인수로 넘겨 명령행에서 실행 가능하다. 예를 들어, date 명령을 실행하는 도중에 호출되는 시스템 호출을 알고 싶다면 다음과 같이 명령을 내린다.
strace date |
결과는 상당히 큰 덤프로 date 명령을 수행하는 과정에서 호출하는 다양한 시스템 호출을 보여준다. 공유 라이브러리 올리기, 메모리 사상은 물론이고, 추적 끝부분에는 표준 출력으로 내보내는 날짜 정보까지 보여준다.
... write(1, "Fri Feb 9 23:06:41 MST 2007\n", 29Fri Feb 9 23:06:41 MST 2007) = 29 munmap(0xb747a000, 4096) = 0 exit_group(0) = ? $ |
이런 추적 기법은 현재 시스템 호출 요청에 syscall_trace라는 특별한 필드를 설정했을 때 do_syscall_trace 함수를 호출하는 방식으로 커널이 수행한다. 또한 ./linux/arch/i386/kernel/entry.S(syscall_trace_entry 참조)에서 시스템 호출 요청의 일부로 호출을 추적하는 코드를 확인할 수 있다.
시스템 호출은 사용자 영역과 커널 사이를 오가는 효율적인 방법으로 커널 영역에 존재하는 서비스를 요청한다. 하지만 시스템 호출은 또한 아주 빡빡하게 통제되므로, 사용자/커널 상호 작용을 제공하기 위해서는 새로운 /proc 파일 시스템 항목을 추가하는 편이 훨씬 더 간단하다. 속력이 중요하다면, 시스템 호출은 응용 프로그램을 벗어나 최고 성능을 쥐어짜는 이상적인 방법이다. SCI를 파고 들려면 참고자료를 읽어보기 바란다.
교육
- "Access the Linux kernel using the /proc filesystem"(developerWorks, 2006년 3월): 사용자 영역/커널 통신을 위해 /proc 파일 시스템을 활용하는 커널 코드 개발 기법을 배운다.
- Manugarg가 쓴 "Sysenter Based System Call Mechanism in Linux 2.6": 이 문서는 사용자 영역 응용 프로그램과 커널 사이에서 시스템 호출 출구를 세부적으로 다룬다. 이 문서는 2.6 커널에서 제공하는 전환 과정에 초점을 맞춘다.
- 이 논문은 사용자 영역과 커널 사이에서 일어나는 어셈블리어 연결 세부 사항을 다룬다.
-
GNU
C라이브러리(glibc)는 GNU C를 위한 표준 라이브러리다. 리눅스는 물론이고 다양한 운영체제를 위한 glibc를 찾을 수 있다. GNUC라이브러리는 ISOC99, POSIX, UNIX98을 포함한 수 많은 표준을 따른다. GNU 프로젝트에서 glibc에 대한 추가 정보를 찾을 수 있다. -
리눅스 syscalls 매뉴얼 페이지는 리눅스에서 사용 가능한 완벽한 시스템 호출 목록을 제공한다.
- 위키백과는 시스템 호출에 대한 흥미로운 관점을 제공한다. 물론 역사와 전형적인 구현 방법도 포함한다.
- 조금 낡긴 했지만, 리눅스 응용 프로그래밍 인터페이스(API)는 (커널 내에서) 일반적인 사용을 위한 커널 함수 중 다수를 문서화했다. 이 문서는 다른 함수는 물론이고 사용자 영역 메모리 관리 함수도 포함한다.
-
developerWorks 리눅스 영역: 리눅스 개발자를 위한 다양한 자료를 제공한다.
-
developerWorks 기술 행사와 웹 캐스트를 놓치지 말자.
제품 및 기술 얻기
-
IBM 평가판 소프트웨어: developerWorks에서 직접 내려받아 다음 리눅스 프로젝트 개발에 활용하자.
토론
-
developerWorks 블로그를 읽어보고, developerWorks 공동체에 참여하자.
