다음 이전 차례

4. 고해상도 타이밍: 지연 시간

우선, 리눅스의 선점형 멀티태스킹 때문에 사용자 모드의 프로세스가 타이밍을 정확히 제어하는지 보증할 수 없다는 것에 주의한다. 무엇보다도, 사용자 모드의 프로세스는 멀티 태스킹과 리눅스의 선점적인 특성 때문에 정확한 타이밍을 보 장받을 수 없다는 것을 말하고 싶다. 여러분의 프로세스가 10밀리 초에서 (로드 가 크게 걸리는 시스템에서) 수 초동안 스케쥴링에서 제외될 수 있다. 그러나, I/O 포트를 사용하는 대부분의 응용 프로그램에서 이는 별로 문제가 되지 않는 다. 이러한 문제를 최소화 하려면 여러분의 프로세스를 nice 명령어로 높은 우선 순위를 부여할 수 있다. (nice(2)의 매뉴얼 페이지를 참조하기 바란다).

일반 사용자 모드의 프로세스를 정확한 타이밍으로 수행시키려 한다면, 사용자 모드의 `리얼 타임' 기능이 지원되어야 한다. 리눅스 2.x 커널에서는 소프트 리 얼 타임 기능이 지원되어야 한다; 자세한 것은 sched_setscheduler(2) 맨페이지 를 참조한다. 하드 리얼 타임을 지원하는 특별한 커널이 있다. 이에 대한 더 많 은 정보는 <URL: luz.cs.nmt.edu/~rtlinux/>를 참조한다. 이제, 더 쉬운 타이밍 호출을 시작해 보자. 몇초동안 지연하려면, 최선의 선택은 sleep(3)를 사용하는 것이다. 최소한 수십 초를 지연하려면 (최소 지연 시간이 10 밀리초 정도 될 때), usleep(3)가 그렇게 동작할 것이다. 이러한 기능은 CPU에게 다른 프로세스를 수행하도록 하므로, CPU 타임이 낭비되는 일이 없다. 자세한 것은 매뉴얼 페이지를 참조한다.

50밀리초 이하로 지연할 때는 (프로세서나 머신, 시스템 부하에 좌우되지만), 리 눅스 스케쥴러는 제어권을 돌려 받기 전에 최소한 10-30 밀리초 정도 소모하기 때문에 CPU를 포기할 수는 없다. 이러한 이유 때문에, 아주 작은 지연 시간을 둘 때, usleep(3)은 매개변수에 지정한 것 보다 최소 10밀리초 정도 더 지연을 한다.

짧은 지연 시간을 할당할 경우 (보통 50밀리초 정도 될 것이다), 다양하게 쓰일 수 있는 방법은 udelay()를 사용하는 것인데, /usr/include/asm/delay.h (linux/include/asm-i386/delay.h) 에 정의되어 있다. udelay()는 매개변수를 하나 만 주었을 때 지연하는데 몇 마이크로 초 정도의 시간이 걸리고, 아무것도 리턴 하지 않는다. 매개변수에서 지정한 것보다 몇 마이크로초 정도 더 걸릴 수도 있 는데, 이는 얼마나 기다려야 하는지 계산하는 오버헤드에서 비롯된 것이다 (delay.h에 자세한 사항이 있다).

커널 밖에서 udelay()를 사용하기 위해서, 여러분은 정확한 값으로 정의한 unsigned long 변수인 loops_per_sec를 필요로 할 것이다. 필자가 아는 한, 이 값을 커널에서 얻는 가장 빠른 길은 /proc/cpuinfo의 BogoMips를 읽어 500000을 곱하는 것이다.

리눅스 커널 2.0.x 시리즈에서, 새로운 시스템 호출인 nanosleep(2) (맨페이지 참 조)는 매우 짧은 시간 동안 잠들거나 지연하도록 할 수 있다. 이것은 프로세스가 소프트 리얼 타임 스케쥴링(sched_setscheduler(2)을 사용)을 지정한다면 지연 시간이 2 밀리초 이하일 때 udelay(2)를 사용하고, 다른 경우에는 (usleep()와 같 이) sleep을 호출한다. 여러분은 nanosleep()를 사용하기 위해서 loops_per_sec 변수를 필요로 하지는 않을 것인데, 이 시스템 호출이 그 값을 커널에서 가져오 기 때문이다.

수 마이크로초동안 지연하는 또다른 방법은 포트 I/O이다. 포트 0x80에 몇 바이 트를 읽고 쓰려면 프로세서 종류나 속도에 관계없이 정확히 1 마이크로초 정도 기다려야 한다. 몇 마이크로초동안 기다리기 위해서 여러 번 호출할 수 있다. 이 포트 출력은 표준 머신에 대해서는 심각한 부작용이 없다고 확신한다.) (그리고 커널 드라이버도 이를 사용한다.) 이는 {in|out}[bw]_p()가 지연하는 방법이다 (asm/io.h를 참조).

사실, 대부분의 범위 0-0x3ff번의 포트에서 쓰는 포트 I/O 명령은 거의 정확히 1 마이크로초 정도 점유하므로, 예를 들어 여러분이 병렬 포트를 정확히 사용하려 면 포트에 지연 시간을 주기 위해서 추가로 inb()를 호출한다. 여러분이 프로그램이 돌아갈 프로세서의 종류나 클럭 속도를 알고 있다면, 특정 한 어셈블러 명령에서 오는 지연 시간보다 더 짧은 시간을 써넣을(hard-code) 수 있다(그렇지만 기억할 것이 있다면 프로세스는 언제나 수행될 수 있으므로, 그 지연 시간은 실제로 더 길어질 수 있다는 것이다). 아래의 표에서 내부 프로 세서 속도는 클럭 사이클의 수를 결정한다. 예를 들어, 50 MHz의 프로세서 (486DX-50 또는 486DX2-50)의 클럭 사이클은 1/50000000이다.


명령          i386 클럭 사이클     i486 클럭 사이클
nop                   3                   1
xchg %ax,%ax          3                   3
or %ax,%ax            2                   1
mov %ax,%ax           2                   1
add %ax,0             2                   1

(미안하지만, 펜티엄의 경우는 잘 모른다. 아마도 486과 비슷할 것이다.) (필자는 i386에서 한 사이클을 사용하는 명령을 찾을 수 없었다) 표의 nop와 xchg 명령은 부작용이 없다. 나머지 명령은 플래그 레지스터를 변경 할 수 있지만, gcc가 그것을 발견한다고 문제가 되지는 않는다. 이를 사용하려면, 프로그램에서 asm("명령"); 을 호출한다. 위의 테이블에서 문 법에 있는 명령을 준다; 여러 명령을 넣으려면asm("명령 ; 명령 ; 명령"); 이 된 다. asm()은 gcc가 인라인 어셈블리로 변환하여 함수 호출 오버헤드가 없다. 펜티엄에서, 다음과 같은 C 코드로, 최근에 리부트 하였을 때부터 경과한 클럭 사이클의 수를 얻을 수 있다.

extern __inline__ unsigned long long int rdtsc() { unsigned long long int x; __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x)); return x; }

인텔 x86아키텍처에서 한 클럭 사이클보다 더 짧은 지연 시간을 내기는 불가능 하다.


다음 이전 차례