 |  |
|
난이도 : 중급 M. Tim Jones, 컨설턴트 엔지니어, Emulex Corp.
옮긴이: 박재호 이해영 dwkorea@kr.ibm.com
2008 년 6 월 17 일 리눅스(Linux®) 시스템 호출은 우리가 매일 사용하는 기능입니다. 하지만 시스템 호출이 사용자 영역에서 커널 영역으로 어떻게 넘어가는지 알고 있나요? 리눅스 시스템 호출 인터페이스(SCI, System Call Interface)를 탐험하고 새로운 시스템 호출을 추가하는 방법(과 다른 대안)을 배우고, SCI 관련 유틸리티를 살펴보겠습니다.
리눅스 시스템 호출은 우리가 매일 사용하는 기능이다. 하지만 시스템 호출이 사용자 영역에서 커널 영역으로 어떻게 넘어가는지 알고 있는가? 리눅스 시스템 호출 인터페이스(SCI, System Call Interface)를 탐험하고 새로운 시스템 호출을 추가하는 방법(과 다른 대안)을 배우고, SCI 관련 유틸리티를 살펴보자.
이 기사에서, 리눅스 SCI를 탐험하고, 2.6.20 커널에 시스템 호출을 추가하는 방법과 이 함수를 사용자 영역에서 사용하는 방법을 보여줄 계획이다. 또한 시스템 호출 개발에 유용한 몇몇 함수와 시스템 호출 대안을 살펴보겠다. 마지막으로 프로세스 사용을 추적하는 기능과 같이 시스템 호출과 관련이 있는 몇몇 종속 메커니즘을 살펴본다.
SCI
리눅스에서 시스템 호출 구현은 아키텍처에 따라 다르지만, 특정 아키텍처 내부에서조차 달라질 수 있다. 예를 들어, 예전 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. 시스템 호출 테이블과 다양한 연관 관계
리눅스 시스템 호출 추가하기
 |
시스템 호출 역다중화
몇몇 시스템 호출에 대해 커널이 역다중화 작업을 수행한다. 예를 들어, BSD(Berkeley Software Distribution) 소켓 호출(socket, bind, connect 등)은 단일 시스템 호출 색인(__NR_socketcall)에 묶여 있지만, 또 다른 인수를 통해 커널에서 일어나는 역다중화로 적절한 호출이 가능해진다. ./linux/net/socket.c에서 sys_socketcall을 살펴보기 바란다.
|
|
새로운 시스템 호출을 추가하는 작업은 대부분 절차에 따라 가능하다. 물론 몇 가지 사항은 주의해야 한다. 이 절에서는 새로운 시스템 호출을 구현하고 사용자 영역에 존재하는 응용 프로그램이 활용하는 방법을 예를 들어 살펴보기로 하자.
커널에 새로운 시스템 호출을 추가하는 세 가지 기본 단계는 다음과 같다.
- 새로운 함수 추가
- 헤더 파일 갱신
- 새로운 함수를 위한 시스템 호출 테이블 갱신
주의: 이 과정에는 사용자 영역 쪽 내용이 빠져 있는데, 나중에 다시 살펴보겠다.
종종 함수를 위한 새로운 파일을 생성한다. 하지만 설명의 초점을 흐리지 않도록 존재하는 원시 파일에 새 함수를 추가하겠다. 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;
}
|
 |
커널 jiffies
리눅스 커널은 jiffies라는 전역 변수를 유지하는데, 기계가 시작한 이후에 흘러간 타이머 틱 숫자를 표현한다. 이 변수는 0으로 초기화되며, 타이머 인터럽트가 걸릴 때마다 증가한다. get_jiffies_64 함수로 jiffies를 읽을 수 있으며, jiffies_to_msecs로 밀리 초(msec) 값이나 jiffies_to_usecs로 마이크로초(usec) 값으로 jiffies 값을 변환할 수도 있다. jiffies 전역과 관련된 함수는 ./linux/include/linux/jiffies.h에 선언되어 있다.
|
|
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 함수는 아키텍처에 밀접하며, 커널에 제어권을 넘기는 메커니즘을 사용한다. 인수는 __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 )
|
 |
사용자 영역과 __NR 상수
Listing 6에서 __NR 심볼 상수를 제공했다는 사실에 주목하자. (표준 시스템 호출에 대한 내용은) /usr/include/asm/unistd.h에서 찾을 수 있다
|
|
_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로 시스템 호출 추적하기
리눅스 커널은 (프로세스가 받은 시그널은 물론이고) 프로세스가 호출한 시스템 호출을 추적하는 유용한 방법을 제공한다. 이 유틸리티는 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를 파고 들려면 참고자료를 읽어보기 바란다.
참고자료 교육
제품 및 기술 얻기
-
SEK for Linux 주문: CD 네 장짜리 최신 IBM 평가판 소프트웨어다. DB2®, Lotus®, Rational®, Tivoli®, WebSphere® 등과 같은 소프트웨어 평가판을 무료로 사용할 수 있다.
-
IBM 평가판 소프트웨어: developerWorks에서 직접 내려받아 다음 리눅스 프로젝트 개발에 활용하자.
토론
필자소개  | 
|  | M. Tim Jones는 임베디드 소프트웨어 아키텍트이자 GNU/Linux Application Programming, AI Application Programming, BSD Sockets Programming from a Multilanguage Perspective의 저자이기도 한다. Jones의 공학 배경은 정지 위성을 위한 커널 개발에서 시작해 임베디드 시스템 아키텍처와 네트워크 프로토콜 개발에 이르기까지 다양한 분야를 아우른다. Jones는 콜로라도 주, 롱몬트 소재 Emulex 사에서 컨설턴트 엔지니어로 활약한다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |