ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11. 타이머와 시간 관리
    Operating System 2019. 12. 30. 16:24

    커널에 있어 시간의 흐름은 중요하다. 대다수의 커널 함수가 이벤트 기반이 아닌 시간 기반으로 동작하기 때문이다. 시간 기반으로 동작하는 함수로 스케줄러 실행 큐의 균형을 조절하는 함수나 화면을 갱신하는 함수 등을 예로 들 수 있다. 이런 함수는 초당 100회와 같이 정해진 주기에 따라 실행된다. 지연된 디스크 입출력을 처리하는 함수처럼 커널이 특정 함수를 상대적인 미래 시점에 실행해야 하는 경우도 있다. 예를 들면, 특정 작업을 지금으로부터 500ms 후에 실행해야 하는 경우가 있을 수 있다. 그리고 커널은 시스템의 가동 시간과 현재 날짜 및 시간도 관리해야 한다.

    상대 시간과 절대 시간의 차이점을 주의하자. 앞으로 5초 후의 작업을 스케줄링하는데는 절대 시간 개념이 필요하지 않으며 상대시간 개념만 있으면 된다. 반면 커널이 현재 날짜와 시간을 관리하려면 시간의 흐름뿐 아니라 절대적인 시간 측적값도 알고 있어야 한다. 시간 관리에 있어서는 이 두 가지 개념 모두 중요하다.

    그리고 작업을 주기적으로 처리하는 경우와 미래의 특정해진 시점에 처리하는 경우에 대한 커널 구현도 다르다. 10밀리초에 한 번처럼 주기적으로 발생하는 작업은 시스템 타이머가 처리한다. 시스템 타이머는 설정이 가능한 하드웨어 장치로 정해진 주기마다 인터럽트를 발생시킨다. 이 타이머를 처리하는 인터럽트(타이머 인터럽트)는 시스템 시간을 갱신하고 주기적인 작업을 처리한다. 시스템 타이머와 타이머 인터럽트는 리눅스의 중심적인 부분이며 이 장에서 주로 다룰 내용이다.

    이 장의 다른 주요 내용은 특정 시간이 경과된 이후 한 번 실행되는 작업을 처리하는데 사용하는 동적 타이머다 플로피 디스크 장치 드라이버가 특정 시간 동안 사용되지 않는 경우 드라이브 모터를 정지시키는데 사용하는 타이머를 예로 들 수 있다. 커널은 동적으로 타이머를 생성하고 제거할 수 있다. 이 장에서는 타이머의 커널 구현과 이를 코드에서 사용하는 인터페이스에 대해 알아본다.

     

    커널의 시간의 개념

    하드웨어는 커널이 시간의 흐름을 측정하는데 사용할 수 있는 시스템 타이머를 제공한다. 이 시스템 타이머는 전자 시계나 프로세서 주파수와 같은 전기적 시간 신호를 이용해 동작한다. 시스템 타이머는 진동수라고 하는 미리 설정된 주파수마다 울린다. 시스템 타이머가 울리면, 인터럽트가 발생하고, 커널은 특별한 인터럽트 핸들러를 이용해 이를 처리한다. 커널은 미리 설정된 진동수를 알고 있기 때문에 연속된 두 타이머 인터럽트 사이의 경과시간을 알 수 있다. 이 시간을 틱이라고 부르며, 이 값은 진동수 분의 1초가 된다. 커널은 이 방법을 사용해 현재 시각과 시스템 가동시간을 기록한다.

     

    타이머 인터럽트를 통해 주기적으로 실행되는 작업에는 다음과 같은 것들이 있다.

    • 시스템 가동시간 갱신
    • 현재 시각 갱신
    • 대칭형 다중 프로세서 시스템은 스케줄러 실행 큐 간의 균형을 조절
    • 설정 시간에 다다른 동적 타이머 실행
    • 자원 사용 현황과 프로세스 시간 통계 갱신

     

    진동수: HZ

    시스템 타이머의 빈도(진동수)는 시스템이 시작할 때 정적 전처리 지시자인 HZ 값에 의해 정해진다. HZ의 실제 값은 지원 아키텍처마다 다르다. 일부 아키텍처에서는 장비 유형에 따라 달라지기도 한다.

    커널은 <asm/param.h> 파일에 이 값을 정해 둔다. x86 아키텍처를 예로 들면 HZ의 기본값은 100이다. 따라서 i386에서 타이머 인터럽트의 주파수는 100Hz가 되고, 초당 100회 발생한다. 100분의 1초마다, 즉 10밀리초마다 발생한다.

     

    시간을 인식하는 모든 커널 기능은 시스템 타이머의 주기성을 통해 구현된다. 성공적인 관계를 만들 때처럼 올바른 값을 선택하는 것이 타협점을 찾는 핵심이다.

     

    1. 이상적인 HZ 값

    리눅스 초기 버전부터 i386 아키텍처의 타이머 인터럽트 주파수는 100Hz였다. 그러나 2.5 개발 버전에서는 주파수가 1000Hz로 올라갔고 논쟁거리가 되었다. 주파수 값은 다시 100Hz로 돌아왔지만, 이제 사용자가 원하는 HZ 값을 지정해 커널을 컴파일할 수 있도록 설정 가능한 옵션이 생겼다. 시스템이 너무나도 많은 부분이 타이머 인터럽트에 영향을 받기 때문에 주파수 변경은 시스템에 상당한 영향을 끼친다. 큰 HZ 값은 작은 HZ 값과 비교했을 때 장점과 단점이 있다.

    진동수를 올린다는 것은 타이머 인터럽트가 더 자주 발생한다는 뜻이다. 따라서 인터럽트로 인해 처리하는 작업들이 더 자주 실행된다. 이로 인해 다음과 같은 이점을 얻을 수 있다.

    • 타이머 이터럽트의 해상도가 높아진다. 따라서 시간과 관련된 모든 이벤트의 해상도가 높아진다.
    • 시간과 관련 이벤트의 정확도가 향상된다.

    해상도는 진동수와 동일한 비율로 증가한다. 예를 들어, 100Hz인 타이머의 정밀도는 10밀리초가 된다. 다시 말해, 모든 주기적 이벤트는 타이머 인터럽트의 주기인 10밀리초의 배수 주기로 발생하며, 이 이상의 정밀도를 보장할 수 없다. 하지만 1000Hz가 되면, 해상도가 1밀리초가 되어 10배 더 세밀해진다. 100Hz인 경우에도 커널 코드에서 1밀리초의 해상도를 갖는 타이머를 생성할 수는 있지만, 타이머를 10밀리초 이하의 간격으로 정확하게 실행되는 것은 보장할 수 없다.

    정확도 역시 같은 방식으로 개선된다. 임의의 시간에 커널이 타이머를 시작한다고 가정하면 타이머는 아무 때나 울릴 수 있지만, 타이머 인터럽트가 발생한 순간에만 처리가 가능하므로 타이머는 평균적으로 타이머 인터럽트 주기의 절반의 기간동안 대기한다. 예를 들어, 100Hz이면, 이벤트는 평균적으로 원하는 시간보다 +-5밀리초 정도 어긋나 실행된다. 즉 평균 오차가 5밀리초가 된다. 1000Hz이면, 평균 오차는 0.5 밀리초가 되어 10배 개선된다.

     

    2. 큰 HZ 값의 정점

    해상도가 높고 정확도가 높을수록 다음과 같은 여러 가지 장점이 있다.

    • 더 정교한 해상도와 향상된 정확도로 커널 타이머를 실행할 수 있다.
    • 타임아웃 값을 선택적으로 사용할 수 있는 poll()이나 select() 같은 시스템 호출을 더 정밀하게 실행할 수 있다.
    • 자원 사용률, 시스템 가동 시간 같은 측정값을 더 세밀하게 기록할 수 있따.
    • 프로세스 선점이 더 정확하게 처리된다.

     

    쉽게 얻어지는 주목할 만한 성능 향상으로 poll()과 select() 타임 아웃의 정밀도 향상 효과를 들 수 있다. 이 시스템 호출을 많이 사용하는 애플리케이션의 경우 실제 타임 아웃 시간이 지났어도 타이머 인터럽트 발생을 기다리는데 상당한 시간을 허비하는 경우가 많기 때문에, 정밀도 향상으로 인해 얻어지는 효과가 상당히 클 수 있다. 높은 진동수의 또 다른 장점으로 스케줄링 지연 시간이 줄어듬에 따라 보다 정확한 프로세스 선점이 가능하다는 점을 들 수 있다. 실행 프로세스의 타임슬라이스 값을 조정 작업이 타이머 인터럽트를 통해 처리된다. 타임 슬라이스 값이 0이 되면, need_resched 변수가 설정되고 커널은 최대한 빨리 스제줄러를 실행한다. 어떤 프로세스가 실행 중이고, 타임 슬라이스가 2밀리초 정도 남았다고 하자. 2밀리초가 지나면, 스케줄러는 실행 중인 프로세스를 선점하고 새로운 프로세스를 실행해야 한다. 안타깝게도 이런 일은 다음 타이머 인터럽트가 발생할 때까지는 일어나지 않으며, 타이머 인터럽트는 2밀리초 안에 발생하지 않을 수 있다. 최악의 경우 다음 번 인터럽트가 1/HZ초 이후에 발생할 수도 있다는 것이다. HZ 값이 100이면, 이 프로세스는 거의 10밀리초에 가까운 실행 시간을 더 얻을 수 있다. 물론 이런 불공정한 스케줄링은 모든 태스크에 적용되기 때문에 전체적으로는 공평함이 유지된다고 불 수 있겠지만, 문제는 그것이 아니다. 실제 문제는 선점이 미루어지면서 발생하는 지연 시간에 있다. 스케줄링될 태스크가 오디어 버퍼를 채우는 작업과 같이 시간에 민감한 작업을 처리하는 경우라면, 지연으로 인해 문제가 발생할 수 있다. 진동수를 1000Hz로 높이면, 스케줄링 지연시간을 최악의 경우 1밀리초, 평균 0.5밀리초로 줄일 수 있다.

     

    3. 큰 HZ 값의 단점

    진동수를 높이는 데도 분명 단점이 있다. 그렇지 않으면 애초에 1000Hz(또는 더 높은 값)를 사용했을 것이다. 실제 아주 큰 문제가 하나 있다. 진동수가 높으면 타이머 인터럽트가 자주 발생하며, 이로 인해 프로세스가 타이머 인터럽트 핸들러를 실행하는 시간이 더 많아지기 때문에 부가 비용이 늘어나게 된다. 진동수가 높아질수록 프로세서는 타이머 인터럽트를 처리하는데 더 많은 시간을 소모하게 된다. 이 문제는 프로세서가 다른 작업을 실행할 수 있는 시간을 줄일 뿐 아니라, 프로세서 캐쉬 정보가 더 자주 소실되며 전력 소모도 늘어나는 현상을 일으킨다. 부가 비용이 어느 정도인지에 대해서는 논란이 있다. 하지만 애초에 그 부가 비용을 얼마나 실질적인것으로 볼 수 있을까? 결국 적어도 현대적인 시스템의 경우에는 1000Hz이 용납할 수 없을 정도의 부가 비용을 만들어내지 않으며 1000Hz 타이머로 변경하는 것이 성능을 크게 해치지 않는다고 결론지었다. 하지만 2.6 커넣은 Hz 값을 변경해 컴파일 할 수 있다.

     

     

    지피

    전역 변수인 jiffies에는 시스템 시작 이후 발생한 진동횟수가 저장된다. 시스템 시작시 커널은 이 값을 0으로 설정하고, 타이머 인터럽트가 발생할 때마다 1씩 증가시킨다. 타이머 인터럽트가 초당 HZ회 발생시키므로, 초당 지피 수는 HZ가 된다. 따라서 시스템 가동 시간은 jiffies/HZ초가 된다. 실제 벌어지는 일은 조금 더 복작하다. 커널은 버그 식별을 위해 jiffies 변수의 자릿수 넘침 현상이 더 자주 일어나게 하기 위해 jiffies 변수를 0이 아닌 특별한 값으로 초기화한다. 실제 jiffies 값이 필요한 경우에는 이 차이 값을 먼저 빼야 한다.

     

    jiffies 변수는 <linux/jiffies.h> 파일에 다음과 같이 선언된다.

    extern unsigned long volatile jiffies;

     

    다음 절에서 약간 특이한 실제 정의를 살펴볼 것이다. 일단 커널 코드의 사용 예를 살펴보자. 초를 jiffies 단위로 변환하기 위해서는 다음 식을 사용한다.

    (seconds * HZ)

     

    마찬가지로 다음 식을 사용해 jiffies 단위를 초로 변환할 수 있다.

    (jiffies / HZ)

     

    초를 진동수로 바꾸는 첫 번째 식을 더 많이 사용한다. 예를 들면, 미래의 특정 시간에 해당하는 지피 값이 필요한 경우가 많다.

    unsigned long time_stamp = jiffies;             // 현재

    unsigned long next_tick = jiffies + 1;           // 현재로부터 한 틱 후

    unsigned long later = jiffies + (5 * HZ);        // 현재로부터 2초 후

    unsigned long fraction = jiffies + HZ / 10;    // 현재로부터 1/10초 후

     

    커널은 절대 시간을 신경 쓰는 경우가 거의 없기 때문에, 진동수를 초로 변환하는 경우는 보통 사용자 공간과 통신하는 경우에만 사용한다.

     

    1. 지피의 내부 표현

    jiffies 변수의 형은 항상 unsigned long이었기 때문에, 32비트 아키텍처에서는 크기가 32비트이고 64비트 아키텍처에서는 크기가 64비트다. 진동수가 100인 경우, 32비트 jiffies 변수는 약 497일 후에 자릿수 넘침 현상이 발생한다. 그러나 HZ 값이 1000으로 올라가면 32비트인 경우 49.7일만에 자릿수 넘침 현상이 발생한다. 모든 아키텍처에서 jiffies 변수가 64비트라면, 웬만한 HZ 값에 대해서는 자릿수 넘침 현상이 평생 발생하지 않을 것이다.

    성능과 역사적인 이유로 인해, 특해 기존 커널 코드와의 호환성 때문에 커널 개발자들은 jiffies 변수의 자료형을 unsigned long으로 유지하고 싶어했다. 몇 가지 멋진 생각과 작은 링커 기법을 이용해 이 문제를 해결했다.

    앞서 보았듯이 jiffies는 unsigned long 형으로 정의된다.

    extern unsigned long volatile jiffies;

     

    다른 두 번째 변수도 <linux/jiffes.h>에 정의된다.

    extern u64 jiffies_64;

     

    주 커널 이미지(x86은 arch/x86/kernel/vmlinux.lds.S)를 링크할 때 사용하는 ld(1) 스크립트는 jiffie_64 변수의 시작 위치에 jiffies 변수를 겹처 놓는다.

    jiffies = jiffies_64;

     

    이러면 jiffies 변수는 jiffies_64 변수의 전체 64비트 중 하위 32비트가 된다. 코드 상에서는 이전과 똑같은 방식으로 jiffies 변수에 접근할 수 있다. 대부분의 코드는 단순히 시간 경과를 측정하는 용도로 jiffies 변수를 사용하기 때문에, 하위 32비트 부분에만 신경 써도 된다. 하지만 시간 관리 코드는 64비트 전체를 사용하므로 전체 64비트 값의 자릿수 넘침 현상을 처리해야 한다. 

    jifiies 변수를 사용하는 코드는 jifiies_64 변수의 하위 32비트만을 사용한다. get_jiffies_64() 함수를 사용해 전체 64비트 값을 읽을 수 있다. 이런 동작이 필요한 경우는 아주 드믈기 때문에 대부분의 코드에는 직접 jiffies 변수를 사용해 하위 32비트 값만을 사용한다. 64비트 아키텍처의 경우 jiffies_64 변수와 jiffies 변수는 같은 대상을 가리킨다. jiffies 변수를 사용하는 코드나 get_jiffies_64() 함수를 사용하는 코드 모두 같은 효과를 가진다.

     

    2. 지피 값 되돌아감

    C의 다른 정수들과 마찬가지로 jiffies 변수도 최대 저장 한계를 초과할 정도로 값이 커지면, 자릿수 넘침 현상이 발생한다. 32비트 unsigned 정수인 경우, 자릿수 넘침 현상이 발생하지 않는 타이머 발생 횟수의 최대 값은 4294967295가 된다. 카운터 값이 최대가 되면, 0으로 다시 되돌아 간다. 이러한 되돌림의 예를 살펴보자

     

    unsigned long timeout = jiffies + HZ/2;

     

    // 작업을 수행

     

    // 작업이 너무 오래 걸리지 않았는지 확인

    if(timeout > jiffies)

    {

           //시한을 넘기지 않았음. 양호

    }

    else

    {

           // 시한을 넘겼음. 오류

    }

     

    이 코드의 의도는 미래의 특정 시점으로, 여기서는 지금으로부터 0.5초 뒤로 시한을 설정하는 것이다. 그 다음 특정 작업을 수행한다. 하드웨어에 신호를 보내고 응답을 기다리는 상황을 생각해 볼 수 있다. 작업을 마쳤을 때 전체 일 처리가 시한을 넘겼다면 오류를 적절히 처리한다.

    하지만, jiffies 값이 시한을 설정한 이후 0으로 되돌아 간 경우를 생각해 보자. jiffies 값이 논리적으로는 설정한 timeout 값보다 커졌지만, 개념적으로는 jiffies 값이 더 작아져서 첫 번째 조건문이 거짓이 된다. 하지만, jiffies 값의 자릿수가 최대 값을 넘어갔으므로 0 근처의 아주 작은 값이 된다. 값이 되돌아 감으로 인해 if 문의 결과가 뒤바뀐다.

    다행이 커널에서는 타이머 카운트 값의 되돌아 감을 고려해 값을 올바르게 비교할 수 있는 네 가지 매크로를 제공한다. 이 매크로는 <linux/jiffies.h> 파일에 정의되어 있다. 각 매크로를 단순하게 표현하면 다음과 같다.

     

    #define time_after(unknown, known) ((long)(known) - (long)(unknown) < 0)

    #define time_before(unknown, known) ((long)(unknown) - (long)(known) < 0)

    #define time_after_eq(unknown, known) ((long)(unknown) - (long)(known) >= 0)

    #define time_before_eq(unknown, known) ((long)(known) - (long)(unknown) >= 0)

     

    unknown 인자 자리에는 보통 jiffies 값이 들어가며, 비교하고자 하는 값이 known 인자 자리에 들어간다.

    time_after(unknown, known) 매크로는 unknown이 가리키는 시간이 known이 가리키는 시간보다 나중인 경우 true를, 그렇지 않으면 false를 반환한다. time_before(unknown, known) 매크로는 unknown이 가리키는 시간이 known이 가리키는 시간보다 이전인 경우 true를, 그렇지 않은 경우에는 false를 반환한다. 뒷 부분의 두 매크로는 두 인자 값이 같은 경우에도 true를 반환한다는 점만 빼면, 앞의 두 매크로와 동일하다.

    타이머 값이 되돌아 가도 문제 없도록 앞의 예제를 수정하면 다음과 같다.

     

    unsinged long timeout = jiffies + HZ/2; // 0.5초 후 타임아웃

     

    if(time_before(jiffies, timeout))

    {

           // 시한을 넘기지 않음. 양호

    }

    else

    {

           // 시한을 넘겼음. 오류

    }

     

    다시확인!!!

     

     

    3. 사용자 공간과 HZ 값

    2.6 이전 버전의 커널의 경우 HZ 값을 변경하면 사용자 공간에서 이상 현상이 발생했다. 이는 HZ 값이 초당 진동수 단위 값으로 사용자 공간에 제공되었기 때문에 발생한 일이다. 이런 인터페이스가 고정되면서 애플리케이션은 특정 HZ 값을 가정하게 된다. 결과적으로 HZ 값을 바꾸면 이 사실을 모르는 사용자 공간에서는 여러 값들이 특정 상수 배수로 바뀌는 현상이 발생한다. 실제 2시간인 시스템 가동시간을 20시간으로 표시하게 된다.

     

    이 문제를 해결하기 위해서는 커널이 jiffies 값을 조정해서 내보내야 한다. 커널은 사용자 공간이 기대하는 HZ 값을 USER_HZ 값으로 정의하는 방식으로 이를 해결한다. x86에서는 그 동안 HZ 값이 100이었기 때문에, USER_HZ 값도 100이 된다. 이후 kernel/time.c 파일에 정의된 jiffies_to_clock_t() 함수를 사용해 HZ 기반의 진동수 값을 USER_HZ 기반의 진동수 값으로 변환할 수 있다. USER_HZ값과 HZ값이 정수배인지 여부, USER_HZ 값이 HZ 값보다 작거나 같은지 여부에 따라 변환식이 결정된다. 대부분 시스템에서 처럼 정수배이며 작거나 같은 경우라면 변환식은 다음과 같이 간단하다.

     

    return x / (HZ / USER_HZ);

     

    정수배가 아닌 경우에는 좀 더 복잡한 알고리즘을 사용한다.

    마지막으로, 64비트 jiffies 값을 HZ 단위에서 USER_HZ 단위로 바꾸기 위해 jiffies_64_to_clock_t() 함수를 제공한다. 사용자 공간에서 초당 진동수 단위로 표현되는 값을 내보내는 모든 곳에서 이 함수들을 사용한다. 예를 들면 다음과 같다.

     

    unsigned long start;

    unsigned long total_time;

     

    start = jiffies;

    // 작업을 처리한다.

    total_time = jiffies - start;

    printk("That took %lu tick\n", jiffies_to_clock_t(total_time));

     

    사용자 공간에서는 이 값을 HZ=USER_HZ인 상태의 값으로 인식한다. 동일하지 않은 상태라고 해도, 매크로가 적절한 값으로 조정해 주므로 문제가 생기지 않는다.

     

    하드웨어 시계와 타이머

    시간 기록을 위해 아키텍처는 두 가지 하드웨어 장치를 제공한다. 지금까지 알아본 시스템 타이머와 실시간 시계가 그것이다. 장비마다 이 장치들의 동작과 구현방식은 다르지만, 일반적인 목적과 설계는 같다.

     

    1. 실시간 시계

    실시간 시계(RTC, real time clock)는 시스템 시간을 저장하는 비휘발성 장치다. RTC는 보통 시스템 기판에 붙어 있는 작은 배터리를 통해 시스템이 꺼져있는 동안에도 시간을 기록한다. PC 아키텍처의 경우에는 RTC와 CMOS가 통합되어 있어, 하나의 배터리로 RTC를 동작시키고 BIOS 설정도 보존한다. 

    커널은 시스템 시작 시 RTC를 읽고, 이를 이용해 xtime 변수에 저장되는 현재 시간을 초기화한다. 커널은 보통 이 작업 이후에는 RTC 값을 읽지 않는다. 하지만 x86 같은 일부 아키텍처는 주기적으로 현재 시간을 RTC에 저장한다. 어쨋든 실시간 시계는 주로 xtime 값이 초기화되는 시작 과정에만 중요하다.

     

    2. 시스템 타이머

    시스템 타이머는 커널의 시간 기록에 있어 훨씬 더 중요한(그리고 빈번한) 역할을 한다. 시스템 타이머 배후의 개념은 아키텍처와 상관없이 동일하다. 주기적으로 인터럽트를 발생시키는 체계를 제공하는 것이다. 어떤 아키텍처는 설정 가능한 주기로 진동하는 전자 시계를 이용해 이 기능을 구현한다. 설정한 초기 값에서부터 0이 될 때까지 일정 비율로 값이 줄어드는 카운터인 감쇠계를 사용하는 아키텍처도 있다. 카운트 값이 0이 되면 인터럽트를 생성한다. 어떤 방식이든 효과는 같다. x86의 경우, 설정 가능 인터럽트 타이머(PIT, programmable interrupt timer)가 주 시스템 타이머다. PIT는 모든 PC 장비에 있으며 DOS 시절부터 인터럽트를 생성하고 있다. 커널은 시스템 시작 시에 PIT가 HZ 값을 주기로 시스템 타이머 인터럽트(0번 인터럽트)를 생성하도록 설정한다. 이 장치는 제한된 동작만 가능한 단순한 장치이지만, 제 역할을 충분히 수행한다. 시간을 생성하는 다른 x86 장치로는 로컬 APIC 타이머와 프로세서의 타임스탬프 카운터(TSC)를 들 수 있다.

     

    타이머 인터럽트 핸들러

    이제 HZ, jiffies와 시스템 타이머의 역할을 이해했으니, 타이머 인터럽트 핸들러의 실제 구현을 살펴보자. 타이머 인터럽트는 아키텍처 종속적인 부분과 독립적인 두 부분으로 나눌 수 있다.

    아키텍처 종속적인 부분은 시스템 타이머의 인터럽트 핸들러 형태로 되어 있으며, 타이머 인터럽트가 발생했을 때 실행 된다. 인터럽트 핸들러의 정확한 작업 내용은 물론 아키텍처에 따라 다르지만, 대부분의 핸들러는 최소한 다음 작업을 처리한다.

     

    • jiffies_64 및 현재 시간을 저장하는 xtime 변수에 안전하게 접근하기 위해 xtime_lock을 얻는다.
    • 필요에 따라 시스템 타이머를 확인하고 재설정한다.
    • 갱신된 현재 시간을 주기적으로 실시간 시계에 반영한다.
    • 아키텍처에 종속적 타이머 함수인 tick_perioidc() 함수를 호출한다.
    • jiffies_64 카운터 값을 1 증가 시킨다.(앞에서 xtime_lock 잠금을 얻었기 때문에 32비트 아키텍처에서도 안전하게 이 작업을 처리할 수 있다).
    • 현재 실행 중인 프로세스가 소모한 시스템 시간, 사용자 시간과 같은 자원 사용 통계값을 갱신한다.
    • 설정 시간이 지난 동적 타이머를 실행한다.
    • scheduler_tick() 함수를 실행한다.
    • xtime에 저장된 현재 시간을 갱신한다.
    • 악명높은 평균 로드를 계산한다.

     

    호출하는 함수가 대부분의 작업을 처리하므로 함수 자체는 간단하다.

     

    static void tick_periodic(int cpu)

    {

           if(tick_do_timer_cpu == cpu)

           {

                  write_seqlock(&xtime_lock);

     

                  // 다음 타이머 발생 시간을 기록한다.

                  tick_next_period = ktime_add(tick_next_period, tick_period);

     

                  do_timer(1);

                  write_sequnlock(&xtime_lock);

           }

     

           update_process_times(user_mode(get_irq_regs()));

           profile_tick(CPU_PROFILING);

    }

     

    대부분의 중요한 작업은 do_timer()와 update_process_times()에서 처리한다. 전자의 함수가 실제 jiffies_64 값을 증가시키는 작업을 담당한다.

     

    void do_timer(unsigned long ticks)

    {

           jiffies_64 += ticks;

           update_wall_time();

           calc_global_load();

    }

     

    이름에서 유츄할 수 있듯이 update_wall_time() 함수는 진동수 경과에 맞춰 현재 시간을 갱신하는 함수이며, calc_global_load() 함수는 시스템의 평균 로드 통계를 갱신한다.

    최종적으로 do_timer() 함수가 반환되면 update_process_timers() 함수를 호출해 진동수 경과에 따른 여러 가지 통계값을 갱신하며, user_tick을 이용해 사용자 공간, 커널 공간 어느 쪽에서 일어난 시간 경과인지를 표시한다.

     

    void update_process_times(int user_tick)

    {

           struct task_struct *p = current;

           int cpu = smp_processor_id;

     

           // 주의 지금 실행 중인 타이머 인터럽트 컨텍스트도 고려해야 한다.

           account_process_tick(p, user_tick);

           run_local_times();

           rcu_check_callbacks(cpu, user_tick);

           printk_tick();

           scheduler_tick();

           run_posix_cpu_timers(p);

    }

     

    tick_periodic() 함수에서 시스템 레지스터를 확인해 user_tick 값을 설정했다.

     

    update_process_times(user_mode(get_irq_regs()));

     

    account_process_tick() 함수에서 실제 프로세서 시간 갱신 작업을 처리한다.

     

    void account_process_tick(struct task_struct *p, int user_tick)

    {

           cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);

           struct rq *rq = this_rq();

     

           if(user_tick)

           {

                 account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);

           }

           else if((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))

           {

                 account-system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy, one_jiffy_scaled);

           }

           else

           {

                 account_idle_time(cputime_one_jiffy);

           }

    }

     

    이런 방식을 취함으로써 커널은 한 프로세스가 타이머 인터럽트가 발생했을 때의 프로세서 모드에 생관없이 직전진동 주기 전체 시간 동안 실행된 것으로 간주한다는 점을 알아차렸을 것이다. 실제 프로세스는 지난 진동 주기 동안 커널 모드에 여러번 들어갔다 나왔을 수도 있다. 사실 이전 진동 주기 동안 하나 이상의 프로세스가 실행되었을 수도 있다. 이런 엉성한 프로세스 기록 방식은 전통적인 유닉스 방식으로 , 훨씬 더 복잡한 기록 방식을 사용하지 않는 한 커널이 할 수 있는 최선의 방식이다. 이는 높은 진동수를 사용하는 것이 더 좋은 또 하나의 이유이기도 하다. 그 다음 run_local_times() 함수가 softirq를 발생시켜 시간이 만료된 타이머를 실행한다. 마지막으로 scheduler_tick() 함수는 현재 실행 프로세스의 타임슬라이스  값을 줄이고, 필요한 경우 need_sched 플로그를 설정한다. SMP 장비의 경우에는 필요한 경우 이 함수에서 프로세서별 실행큐의 균형을 조절한다. tick_periodic() 함수는 처음의 아키텍처 독립적인 핸들러 함수로 반환되고, 핸들러 함수는 필요한 정리 작업을 처리한 다음 xtime_lock 잠긍을 풀고 반환한다. 이 모든 일이 1/HZ초 마다 벌어진다. x86 시스템이라면 초당 100번 또는 1000번 일어나는 것이다.

     

    날짜와 시간

    현재 날짜와 시간(the wall time)은 kernel/time/timekeeping.c 파일에 정의된다.

     

    strcut timespec xtime;

     

    timespec 구조체는 <linux/time.h> 파일에 다음과 같이 정의된다.

     

    struct timespec{

           __kernel_time_t tv_sec; // 초

           long tv_nsec; // 나노초(백만분의 일초)

    };

     

    xtime.tv_sec 변수에는 1970년 1월 1일 이후 몇 초가 지났는지가 저장된다. 이 기준 날짜를 기원이라고 부른다. 대부분의 유닉스 시스템은 이 기원으로부터의 상대적인 시간으로 현재 시간을 표현한다. xtime.v_nsec 값에는 이전 초에서 몇 나노초가 경과되었는지가 저장된다.

    xtime 변수를 읽고 쓸 때에는 xtime_lock을 사용해야 하며, 이 락은 보통 스핀락이 아닌 순차적 락이다.

    xtime 값을 갱신하려면 쓰기용 seq 락이 필요하다.

     

    write_seqlock(&xtime_lock);

     

    // xtime 갱신

     

    write_sequnlock(&xtime_lock);

     

    xtime 값을 읽으려면 read_seqbegin()과 read_seqretry() 함수를 사용해야 한다.

     

    unsigned long seq;

     

    do{

           unsigned long lost;

           seq = read_seqbegin(&xtime_lock);

           usec = timer->get_offset();

           lost - jiffies - wall_jiffies;

           if(lost)

           {

                 usec += lost * (1000000 / HZ);

           }

           sec = xtime.tv_sec;

           usec += (xtime.tv_nsec / 1000);

    } while (read_seqretry(&xtime_lock, seq));

     

    라이터의 간섭 없이 리더가 데이터 읽는 작업을 마칠 때까지 루프를 돈다. 루프를 도는 중에 타이머 인터럽트가 발생해 xtime이 변경되는 경우에는 순차 카운터 값이 바뀌므로 루프가 다시 시작된다.

    현재 시간을 알아낼 때 주로 사용하는 사용자 인터페이스는 gettimeofday() 함수이며, 이 함수는 kernel/time.c 파일의 sys_gettimeofday() 함수를 통해 구현된다.

           

    asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz)

    {

           if (likely(tv))

           {

                  struct timeval ktv;

                  do_gettimeofday(&ktv);

                  if (copy_to_user(tv, &ktv, sizeof(ktv))

                  {

                         return -EFAULT;

                  }

           }

     

           if (unlikely(tv))

           {

                 if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))

                 {

                       return -EFAULT;

                 }

           }

           

           return 0;

    }

           

    사용자가 tv 인자에 NULL이 아닌 값을 지정하면 아키텍처 종속적인 do_gettimeofday() 함수가 호출된다. 이 함수는 주로 앞서 살펴본 xtime 읽기 루프를 수행한다. tz 인자가 NULL이 아닌 경우에는 시스템 타임존(sys_tz에 저장된다) 정보를 사용자에게 반환한다. 현재 시간이나 타임존 정보를 사용자에게 제공하는 도중에 오류가 발생하면 -EFAULT 값을 반환한다. 성공한 경우에는 0을 반환한다.

    커널은 time() 시스템 호출도 제공하지만, 대부분 gettimeofday() 함수를 사용한다. C 라이브러리도 ftime(), ctime() 등의 현재 시각 관련 라이브러리 함수를 제공한다. settimeofday() 시스템 호출은 현재 시간을 지정한 값으로 설정한다. 이 함수를 사용하려면 CAP_SYS_TIME 권한이 필요하다.

    xtime 값을 갱신하는 작업을 빼면, 커널은 사용자 공간에서처럼 현재 시각 정보를 거의 사용하지 않는다. inode (접근 시간, 수정 시간 등) 타임스탬프 값을 저장하는 파일시스템 코드 정도가 예외적인 경우라 할 수 있다.

    타이머

    동적 타이머 또는 커널 타이머라고 부리그도 하는 타이머는 코널 코드상에서 시간의 흐름을 관리하는 데 필수적인 요소다. 커널 코드에서는 특정 함수의 실행을 얼마 이후로 미루어야 하는 경우가 많다. 앞 장에서 작업을 나중으로 지연시키는 훌륭한 방법인 후반부 처리 방식에 대해 살펴본 적이 있다. 안타깝게도 여기서 말하는 '나중'의 뜻은 의도적으로 모호하게 정의되어 있다. 후반부 처리의 목적은 작업을 얼마나 나중으로 미룰지 정하는 것이 아니라 단순히 지금이 아닌 나중으로 작업을 미루는 것이다. 지금은 작업을 지정한 시간만큼 미룰 수 있는 도구가 필요하다. 지정한 시간이 지나기 전에는 안 되며, 되도록 지정한 시간보다 너무 늦지 안하야 한다. 이에 대한 해결책이 커널 타이머이다.

    타이머는 쉽게 사용할 수 있다. 초기화 작업을 한 다음, 만료시간과 만료되었을 때 실행할 함수를 지정하고 타이머를 활성화시키면 된다. 타이머가 만료되면 지정한 함수가 실행된다. 타이머는 반복되지 않는다. 타이머는 만료된 이후 소멸된다. 이름에 동적이라는 말이 붙은 이유가 이 때문이다. 타이머는 수시로 생성되고 소멸되며, 타이머 개수의 제한은 없다. 타이머는 커널 전체에 걸쳐 많이 사용된다.

     

    1. 타이머 사용

    타이머는 <linux/timer/h> 파일에 정의된 struct timer_list 구조체로 표현된다.

     

    struct timer_list{

           struct list_head entry;                  // 타이머 연결 리스트 항목

           unsigned long expires;                // jiffies 단위로 표시된 만료 시간

           void (*function) (unsigned long);   // 타이머 핸들러 함수

           unsigned long data;                   // 핸들러 함수에 넘어가는 인자

           struct tvec_t_base_s *base;           // 타이머 내부 처리용 항목. 사용 금지..;;

    };

     

    타이머를 생성하는 첫 단계는 먼저 타이머를 정의하는 것이다.

     

    struct timer_list my_timer;

     

    다음으로 타이머의 내부 변수들을 초기화해야 한다. 타이머를 타이머 관리 함수에 사용하기 전에 반드시 해야 한다.

     

    init_timer(&my_timer);

     

    이제 남은 값을 필요한 값으로 채워준다.

     

    my_timer.expires = jiffies + delay;     // delay 값으로 지정한 시간이 지나면 타이머 만료

    my_timer.data = 0;                        // 타이머 핸들러 인자로 0을 전달

    my_timer.function = my_function;     // 타이머가 만료되었을 때 실행할 함수

     

    만료 시간을 지정하는 my_timer.expires 값은 진동수 절대값이다. jiffies 카운터 값이 my_timer.expires 값을 넘어가면, my_timer.data 인자를 가지고 my_timer.function 함수가 실행된다. timer_list 정의에서 알 수 있듯이 이 함수의 원형은 다음과 같다.

    void my_timer_function(unsigned long data);

     

    data 인자를 이용하면 여러 개의 타이머에 같은 핸들러를 등록하고 인자 값을 통해 각 타이머를 구별하는 방법을 사용할 수 있다. 인자가 필요하지 않다면 그냥 0을 지정한다.

     

    마지막으로 다음과 같이 타이머를 활성화시킨다.

    add_timer(&my_timer);

     

    커널은 헌재 진동수 카운터 값이 지정한 만료 시간과 같거나 더 커지면 타이머 핸들러 함수를 실행한다. 커널은 만료 시간이 지나기 전에는 타이머를 싱행하지 않지만, 타이머 실행에는 지연이 있을 수 있다. 보통 만료 시간에 거의 맞추어서 타이머가 실행되자만, 만료 시간이 지나도 진동수 한 번의 주기 정도 지연이 발생할 수 있다. 따라서 타이머를 이용해서는 정교한 실시간 처리를 구현할 수 없다.

     

    이미 설정된 타이머의 만료 시간을 수정할 필요가 있을 수 있다. 커널은 지정한 타이머의 만료 시간을 수정할 수 있는 mod_timer() 함수를 제공한다.

    mod_timer(&my_timer, jiffies + new_delay);  // 새로운 만료 시간

     

    mod_timer() 함수는 초기화되었지만 아직 활성화되지 않은 타이머에도 동작한다. mod_timer() 함수는 타이머가 비활성화 상태인 경우에는 활성화 시킨다. 이 함수는 타이머가 비활성화 상태였을 때에는 0을 반환하고, 활성화 상태인 경우에는 1을 반환한다. 어느 쪽이든 mod_timer() 함수는 타이머를 새로운 만료 값으로 지정하고 활성화시킨다.

     

    타이머를 만료 시간 전에 비활성화시키고자 하는 경우에는 del_timer() 함수를 사용한다

    del_timer(&my_timer);

     

    이 함수도 타이머 활성화 여부와 상관없이 동작한다. 타이머가 비활성화 상태인 경우에는 0을 반환하고, 활성화 상태인 경우에는 1을 반환한다. 타이머가 만료된 경우에는 자동으로 비활성화되기 때문에, 이 함수를 호출할 필요가 없다.

     

    타이머를 제거할 때는 경쟁 조건이 발생할 잠재적인 가능성이 있기 때문에 보완책이 필요하다. del_timer() 함수가 반환되었다는 것은 타이머가 비활성화 되었다는(즉, 앞으로 타이머가 실행되지 않는다는) 것을 뜻한다. 하지만, 다중 프로세서 장비에서는 타이머 핸들러가 이미 다른 프로세서 실행 중일 수 있다. 타이머를 비활성화시키고 실행 중인 타이머 핸들러가 종료될 때까지 대기하기 위해서는 del_timer_sync() 함수를 사용한다.

    del_timer_sync(&my_timer);

     

    del_timer()와 달리 del_timer_sync() 함수는 인터럽트 컨텍스트에서 사용할 수 없다.

    2. 타이머 경쟁 조건

    타이머는 현재 실행 중인 코드에 대해 비동기적으로 실행되므로 경쟁 조건이 발생할 잠재적인 가능성이 몇 가지 있다

    먼저 다음 작업을 단순한 mod_timer() 함수로 처리하는 것은 다중 프로세스 장비에서 안전하지 않으므로 절대 하면 안된다.

    del_timer(my_timer);

    my_timer->expires = jiffies + new_delay;

    add_timer(my_timer);

     

    둘째, 거의 모든 경우에 del_timer() 보다는 del_timer_sync() 함수를 사용해야 한다. 타이머가 현재 실행 중이 아니라고 확신할 수 없으므로, 이 함수를 먼저 실행해야 한다. 타이머를 제거한 다음에 타이머 코드가 계속 실행된다거나 타이머 핸들러가 사용하는 자원을 조작하는 경우를 생각해 봐라. 따라서 동기화 버전을 사용하는 편이 좋다.

    마지막으로 타이머 핸들러가 사용하는 공유 데이터를 적절히 보호해야 한다. 커널은 타이머 함수를 다른 코드에 대해 비동기적으로 실행한다.

     

    3. 타이머 구현

    커널은 softirq와 마찬가지로 타이머 인터럽트가 끝난 후 후반부 처리 컨텍스트에서 타이머 핸들러를 실행한다. 타이머 인터럽트 핸들러는 update_process_times() 함수를 호출하고, 이 함수는 run_local_timers() 함수를 호출한다.

    void run_local_timers(void)

    {

           hrtimer_run_queues();

           raise_softirq(TIMER_SOFTIRQ);  // 타이머 softirq를 올린다.

           sofrlockup_tick();

    }

     

    TIMER_SOFRIRQ는 run_timer_softirq() 함수가 처리한다. 이 함수는 현재 프로세스의 만려된 타이머가 있다면 모두 실행한다.

    타이머는 연결리스트로 저장된다. 하지만 만료된 타이머를 찾기 위해 커널이 항상 전체 리스트를 탐색하거나 리스트를 만료 시간 순으로 정렬 상태를 유지하는 일은 너무 불편하다. 정렬 상태를 유지하려면 타이머 추가 및 제거 작업이 너무 복잡해진다. 대신 커널은 타이머를 만료 시간 값에 따라 크게 다섯 개의 묶음으로 나눈다. 분할을 통해 대부분의 타이머 sofrirq 실행 시에 진행되는, 만료된 타이머를 찾는 작업에 들어가는 커널의 수고를 많이 덜 수 있다.

     

    실행 지연

    커널 코드는 (특히 드라이버 경우) 타이머나 후반부 처리 방식을 사용하지 않고도 일정 시간 실행을 지연시켜야 하는 경우가 많다. 보통 이를 이용해 하드웨어가 특정 작업을 완료하기를 기다린다. 대게 이 시간은 아주 짧다. 예를 들어, 표준에 따르면 네트워크 카드의 이더넷 모드 변경 시간은 2마이크로초다. 드라이버가 원하는 속도를 설정한 다음에는 진행하기 전에 최소한 2마이크로초를 기다려야 한다.

    커널은 지연 작업의 성격에 따른 몇 가지 해결책을 가지고 있다. 이런 해결책은 각자 다른 특성이 있다. 일부 방식은 지연되는 동안 프로세서를 독점하기 때문에 실질적으로 다른 작업이 진행되는 것을 막는다. 다른 방식은 프로세서를 독점하지는 않지만 코드가 정확히 지정한 시간 이후에 실행되는 것을 보장하지 않는다.

    1. 루프 반복

    가장 구현이 간단한 해결책은 대기 반복 또는 루프 반복이다. 이 방식은 필요한 지연 시간이 수 진동수에 해당하는 짧은시간이거나 정밀도가 그다지 중요하지 않은 경우에 사용한다.

    개념은 간단하다 원하는 클럭 진동수가 지날 때까지 루프를 반복하는 것이다. 예를 들면 다음과 같다.

    unsigned long timeout = jiffies + 10;  // 진동수 열 번

     

    while (time_before(jiffies, timeout))

    ;

     

    jiffies 값이 delay 값보다 커질 때까지, 클럭 진동수 열번이 지날 때까지 루프를 반복한다. x86의 경우 HZ 값이 1000이므로 결국 10밀리초가 지연된다. 비슷한 방식으로 다음과 같이 할 수도 있다.

    unsigned long delay = jiffies + 2*HZ;  // 2초

     

    while (time_before(jiffies, timeout))

    ;

     

    이 코드는 2*HZ 클럭 진동수, 즉 클럭과 상관없이 2초가 지날 때까지 루프를 반복한다.

    이 방식은 시스템의 여타 부분에 친절한 방식은 아니다. 코드가 지연되는 동안 프로세서는 무의미한 루프를 돌아야 하므로, 유용한 작업을 수행하지 못한다. 이런 멍청한 방식이 필요한 경우는 거의 없지만, 실행을 지연시키는 명확하고 간단한 방법의 일례로 들어보았다.

    더 좋은 방법은 대기 중에 프로세서가 다른 일을 할 수 있도록 현재 프로세스를 재스케줄링하는 것이다.

    unsigned long delay = jiffies + 5*HZ;

     

    while (time_before(jiffies, delay));

           cond_resched();

     

    cond_resched()를 호출하면 need_resched 플래그가 설정된 경우에만 다른 프로세스를 스케줄링한다. 다시 말하지면, 이 경우에는 실행할 더 중요한 작업이 있는 경우에만 선택적으로 스케줄러를 호출한다는 뜻이다. 이 방법은 스케줄러 호출이 필요하기 때문에 인터럽트 핸들러는 최대한 빨리 실행해야 하기 때문에(루프 반복 방법은 이 목표를 달성에도 도움이 되지 않는다) 여기서 언급한 방법은 모두 프로세스 컨텍스트에서 사용하는 것이 좋다. 그리고 잠금이 설정되어 있거나 인터럽트가 비활설화된 상태에서는 가능한 어떤 형태의 실행 지연도 있어서는 안 된다.

     

    2. 작은 지연

    간혹 (역시 드라이버 같은) 커널 코드는 (클럭 진동수 주기 이하의) 짧지만, 정확한 지연 시간이 필요한 경우가 있다. 역시나 동작 완료를 기다리기 위한 최소 시간 같은 것으로 하드웨어와의 동기화를 위한 1밀리초 이하의 시간이 되는 경우가 많다. 이렇게 짧은 시간은 앞에서 살펴본 jiffies 기반 지연 방식으로는 처리가 불가능하다. 100Hz의 타이머 인터럽트라면, 클럭 진동수 주기는 10밀리초가 된다. 1000Hz의 타이머 인터럽트라 해도, 클럭 진동수 주기는 여전히 1밀리초다. 더 작고 정교한 지연 처리를 위한 해결책이 필요하다.

    다행이 커널에서는 jiffies 값을 사용하지 않는 마이크로초, 나노초, 밀리초 지연 처리를 위한 세 가지 함수를 제공하며, <linux/delay.h>와 <asm/delay.h> 파일에 정의된다.

     

    void udelay(unsigned long usecs);

    void ndelay(unsigned long nsecs);

    void mdelay(unsigned long msecs);

     

    첫 번째 함수는 지정한 마이크로초만큼 루프를 반복함으로써 실행을 지연시키고, 마지막 함수는 지정한 밀리초만큼 실행을 지연시킨다. 1초는 1,000밀리초며, 1,000,000마이크로초에 해당한다. 사용법은 간단하다.

     

    udelay(150); // 150마이크로초만큼 지연

     

    지연 시간을 길게 주면 빠른 시스템에서는 자릿수 넘침 현상이 발생할 수 있기 때문에, udelay() 함수는 지연 시간이 작은 경우에 사용해야 한다. 규칙을 정하자면, 1밀리초 이상의 지연 시간에 대해서는 udelay() 함수를 사용하면 안된다. 지연 시간이 더 긴 경우에는 mdelay() 함수를 사용한다. 루프 반복을 통해 실행을 지연시키는 다른 방법과 마찬가지로, 이런 함수는(특히 지연 시간이 더 긴 mdelay() 함수의 경우) 절대적으로 필요한 경우가 아니면 쓰지 말아야 한다. 잠금이 설정되어 있거나 인터럽트가 비활성화된 상태에서 루프반복을 사용하는 것은 시스템 반응 및 성능에 불리한 영향을 미친다는 점을 명심해야 한다. 그러나 정확하게 실행 시간을 지연시켜야 하는 경우라면, 이런 함수가 최선의 선택이 될 수 있다. 보통 마이크로초 단위의 짧은 시간 동안 지연시켜야 하는 경우에 이 루프 반복 함수를 사용하는 경우가 많다.

     

    3. schedule_timeout()

    실행을 지연시키는 더 적당한 해결책은 schedule_timeout() 함수를 사용하는 것이다. 이 함수는 해당 작업을 지정한 시간이 지나 때까지 휴면 상태로 전환한다. 실제 휴면 시간이 정확히 지정한 시간이 된다는 보장은 없다. 최소한 지정한 시간은 지나야 한다는 것이다.

    지정한 시간이 지나면 커널은 작업을 깨워 실행 대기열에 다시 넣는다. 사용법은 간단하다.

    // 태스크의 상태를 중단 가능한 휴면 상태로 설정한다.

    set_current_state(TASK_INTERRUPTIBLE);

     

    // 태스크를 휴면시키고 s초가 지난 후에 깨운다.

    schedule_timeout(s * HZ);

     

    필요한 인자는 jiffies 단위의 상대값으로 표현한 만료시간분이다. 위 예에서는 태스크를 s초 동안 중단 가능한 휴면 상태로 전환시켰다. 태스크의 상태가 TASK_INTERRUPTBLE이기 때문에, 시그널을 받으면 지정한 시간보다 빨리 깨어날 수 있다. 시그널 처리가 필요하지 않다면, TASK_UNINTERRUPTIBLE을 대신 사용할 수 있다. schedule_timeout()을 호출하기 전에 태스크의 상태가 이 두 가지 중 하나여야 한다. 그렇지 않으면 휴면 상태로 전환되지 않는다.

    schedule_timeout() 함수가 스케줄러를 호출하기 때문에, 이를 사용하는 코드도 휴면이 가능해야한다. 짧게 말하지면, 이 함수는 프로세스 컨텍스트에서 잠금을 설정하지 않는 상태에서만 사용할 수있다.

     

    signed long schedule_timeout(signed long timeout)

    {

           timer_r timer;

           unsigned long expires;

     

           switch (timeout)

           {

                 case MAX_SCHEDULE_TIMEOUT:

                         schedule();

                        goto out;

                 default:

                        if (timeout < 0)

                        {

                               printk(KERN_ERR "schedule_timeout: wrong timeout"

                                      "value %1x fron %p\n", timeout,

                                      __builtin_return_address(0));

                               current->state = TASK_RUNNING;

                               goto out;

                        }

           }

     

           expire = timeout + jiffies;

           init_timer(&timer);

     

           timer.expires = expire;

           timer.data = (unsigned long) currnet;

           timer.function = process_timeout;

     

           add_timer(&timer);

           schedule();

           del_timer_sync(&timer);

     

           timeout = expire - jiffies;

     

    out:

          return timeout < 0 ? 0 : timeout;

    }

     

    이 함수의 이름은 timer이고, 만료 시간이 timeout 클럭 진동수 이후인 타이머를 만든다. 타이머가 만료되면 process_timeout() 함수를 실행하도록 설정한다. 그 다음 타이머를 활성화시키고 schedule() 함수를 호출한다. 현재 태스크의 상태가 TASK_INTERRUPTBLE 또는 TASK_UNINTERRUPTBLE일 것이므로 스케줄러는 현재 태스크를 계속 실행하는 대신 새로운 태스크를 선택한다.

    타이머가 만료되면 process_timeout() 함수가 실행된다.

    void process_timeout(unsigned long data)

    {

           wake_up_process((task_t *) data);

    }

     

    이 함수는 해당 태스크의 상태를 TASK_RUNNING으로 변경하고, 실행 대기열에 추가한다.

    태스크가 다시 스케줄링되면 schedule_timeout() 함수에서 중단되었던 부분(schedule() 함수를 호출한 바로 다음)으로 돌아오게 된다. (시그널 수신 등으로 인해) 태스크가 예정보다 빨ㄹ리 깨어나게 되면, 타이머가 제거된다. 그리고 이 함수는 휴면했던 시간을 반환한다.

    switch 구문의 코드는 이 함수의 일반적인 사용법에 해당하지 않는 특별한 경우를 처리하기 위한 것이다. MAX_SCHEDULE_TIMEOUT을 확인해, 태스크를 계속 휴면 상태로 둘 수 있다. 이 경우에는 휴면 기한을 정할 수 없기 때문에 타이머를 설정하지 않고, 바로 스케줄러를 호출한다. 이 방법을 사용할 때는 태스크를 꺠울 다른 방법이 있어야만 한다.

     

    4. 만료시간을 가지고 대기열에서 휴면

    커널의 프로세스 컨텍스트 코드가 특정 조건을 기다리는 경우 자신을 실행 대기열에 추가하고 스케줄러를 호출해 다른 태스크를 실행하는 과정을 알아보았다. 다른 곳에서 원하는 조건이 발생하면 wake_up() 함수가 호출되고 대기열에서 휴면 중이던 태스크가 깨어나 실행을 계속하게 된다.

    간혹 특정 조건이 발생하거나 또는 특정 시간이 경과하는 두 가지 조건 중 하나라도 만족하기를 기다려야 하는 경우가 있을 수 있다. 이 경우에는 태스크를 대기열에 추가한 다음, schedule() 대신 schedule_timeout() 함수를 호출하면 된다. 원하는 조건이 발생하거나 지정한 시간이 지나면 태스크를 깨운다. 지정한 조건이 조건 발생, 시간 경과 또는 시그널 수신 등 여러 이유로 태스크가 깨어날 수 있기 때문에 태스크가 깨어난 이유를 확인하는 코드가 필요하다.

     

    결론

    이 장에서는 커널의 시간 개념과 커널이 현재 시간과 시스템 가동 시간을 관리하는 방법을 살펴보았다. 상대 시간과 절대 시간, 일회성 작업과 반복작업도 비교해보았다. 그 다음 타이머 인터럽트, 타이머 진동수, HZ, jiffies 등의 시간 개념을 알아보았다.

    타이머의 구현 및 커널 코드에서 타이머를 사용하는 방법도 살펴보았다. 코드에서 일정 시간을 보내는 여러 방법을 알아보면서 이 장을 마무리했다.

    커널 코드를 작성할 때는 시간 및 시간의 흐름에 대한 이해가 필요하다. 아마도 많은 경우, 특히 커널 드라이버를 파고들어야 하는 경우, 커널 타이머가 필요할 것이다. 이 장은 그냥 시간을 보내는 것 이상의 의미가 있다.

    댓글

Designed by Tistory.