커널&유저모드, 프로세스 독점, 커널 타이머

2023. 11. 17. 16:13카테고리 없음

 

커널과 유저 모드

마이크로소프트사에서 Windows 이전에 사용한 운영체제로, MS-DOS는 유닉스는 항상 단일 CPU 모드에서 실행된다.

반면, 유닉스 같은 운영체제는 효율적인 시분할을 위해 이중 CPU 모드를 사용한다.

리눅스 CPU는 커널모드와 유저모드로 동작한다. 

  • 사용자 프로세스는 유저모드에서 동작 & 접근 권한을 가질려면 시스템 호출을 통해 디바이스 드라이버나 커널 모드로 요청을 전달해야 함  ➜ 그래서 유저 모드는 페이지 폴트가 가능함

    더보기
    페이지 폴트란?
    인터럽트의 일종
    프로세스가 실행중에 필요한 페이지가 물리 메모리에 없을때 발생한다.
        
  • 커널 자체는 커널모드에서 동작& 프로세스 명령어 집합, 전체 메모리, 입출력 영역에 대한 모든 접근 권한 소유

프로세스 독점

[유저 모드]

# 리눅스 커널 2.4버전 이전 :

유저 모드에서만 Context Switching이 일어나기 때문에 다른 프로세스로 교체가 가능함

 

# 리눅스 커널 2.6버전 이후 :

커널 모드에서도 Context Switching이 가능해짐

[커널 모드]

특정 조건을 제외하고는 프로세스를 독점함

  1. 자발적으로 CPU에게 양보하는 경우
  2. 인터럽트나 예외가 발생하는 경우

프로세스 문맥과 인터럽트 문맥

커널은 프로세스 문맥과 인터럽트 문맥을 잘 조합하여 작업을 수행한다.

[프로세스 문맥]

"사용자 애플리케이션 ⇒ 시스템호출 ⇒ 커널 코드가 대신 동작"의 과정을 '프로세스 문맥에서 수행한다'라고 표현한다.

프로세스 문맥에서 동작하는 커널 코드는 선점형으로, 프로세스가 스스로 CPU 독접을 포기해야만 다른 프로세스가 실행된다. 

[인터럽트 문맥]

인터럽트 처리기는 인터럽트 문맥에 비동기적으로 동작한다. 즉, 진행 중인 작업A가 종료될때 까지 기다리지 않고 다른 작업B을 하다가 그 작업A가 종료되면 추가 작업을 수행한다. 

안터럽트 문맥은 비선점형으로, 프로세스가 CPU를 독점하는 것이 불가능하며 운영체제가 강제로 프로세스 CPU 점유를 제어한다.

커널 타이머

 커널을 구성하는 여러 구성 요소의 동작은 시간 경과에 큰 영향을 받는다.

그러기 때문에 리눅스 커널은 하드웨어가 제공하는 타이머를 사용한다.

 

크게 두가지 방식으로 기다린다.

  • 바쁘게 기다리면서 CPU 사이클을 소비하는 것
  • 잠을 통해 기다리면서 CPU를 양보하는 것

또한, 특정 시간이 경과하면 동작하는 함수를 스케줄링하는 기능도 존재한다.

 

여기서 배우는 것들

  1. jiffies, HZ, xtime 같은 중요한 커널 타이머 변수
  2. 팬티엄 기반 시스템에서 펜티엄 TSC(Time Stamp Counter)를 이용한 수행 시간 측정 실습
  3. 실시간 클록(Real Time Clock)을 사용하는 방법

(1) HZ와 jiffies

시스템 타이머는 프로그래밍 가능한 주기로 프로세스에 인터럽트를 걸 수 있다.

해당 주기는 HZ 커널 변수에 들어가 있으며, 초당 타이머 틱 횟수이다.

  • HZ 값 ↑ : 타이머 조밀도 ↑ 스케줄링 해상도 ↑ 부하&전력 소모 ↑ ( ∵ 타이머 인터럽트 문맥에서 사이클 소비량이 큼) 

    더보기
    HZ 설정 값 변동
    [x86] 2.4 : HZ 기본 값이 100
    [x86] 2.6 : HZ 기본 값이 1000
    [x86]  2.6.13 : HZ 기본 값이 250
    [ARM]2.6 : HZ 기본 값이 100 


jiffies는 시스템 시동 이후 시스템 타이머를 호출한 횟수(tick 수)
를 저장한다.

해당 변수는 커널이 시스템의 시간을 추적하고 관리하는기 위해 사용된다.

커널은 jiffies 변수를 매초 HZ만큼 증가시킨다. 

위에서 언급했듯이 HZ값은 커널 설정마다 값이 다르지만, 기본적으로 100 또는 1000의 값을 가진다.

  • 만약 HZ 값이 100인 경우, jiffies는 10밀리초마다 증가
    HZ = 100, 1초당 100번 시스템 타이머 호출 수행
    jffies = 1초당100번 증가 = 1000밀리초당 100번 증가 = 10밀리초당 1번 증가
  • 만약 HZ 값이 1000인 경우, jiffies는 1밀리초마다 증가
    HZ = 100, 1초당 100번 시스템 타이머 호출 수행
    jffies = 1초당1000번 증가 = 1000밀리초당 1000번 증가 = 1밀리초당 1번 증가

이를 코드상에서 확인해보자

drivers/ide/ide.c

unsigned long timeout = jiffies + (3*HZ)

while (hwgroup->busy) {
	if(time_after(jiffies, timeout){
    	return -EBUSY;
    }
}
return SUCCESS;

 

위 코드는 바쁜 상태에 있는 디스크 드라이브를 폴링(polling)하는 루틴이다.

즉, 디스크 드라이브가 작업을 수행하고 있는지 확인하고 그 상태를 주기적으로 체크하는  코드 루틴을 의미한다.

  • 위 코드는 3초 내에 바쁜 조건이 정리되는 경우 SUCCESS를 반환한다.
  • 그렇지 않다면 -EBUSY를 반환한다. 
  • 여기서 3*HZ는 3초동안 발생하는 tick의 총 수를 말한다. 
  • 즉, jiffies + (3*HZ)는 현재 시간(jiffies)에 3초후의 시간(3*HZ)를 더한 시점을 나타낸다.
  • 또한, 바쁜상태는 디스크 드라이브가 작업을 수행하고 있는지를 말한다. 
  • time_after()는 두개의 jiffies 값을 비교하는 함수로, 지속적으로 증가하는 jiffies때문에 최대값을 넘어서는 오버플로우를 방지해준다. (시간 비교 함수 예시 : time_before(), time_before_eq(), time_after_eq())

    더보기
    volatile 키워드는 C/C++ 언어에서 변수가 예상치 못한 방식으로 변경될 수 있음을 컴파일러에게 알려주는 역활이다. jaffies도 volatile로 정의했기 때문에 틱마다 인터럽트 처리기가 갱신한 jiffies를 루프를 돌때마다 다시 읽어온다.
       
  • 전체적인 흐름은 다음과 같다.
    ➨ hwgroup가 바쁜 상태로 표시되는 동안 ➨ 3초가 지난 시점에도 바쁜상태이면 -EBUSY 반환 / 안 바쁘면 SUCCESS 반환  
if (stream -> rescheduled) {
	ehci_info(ehci, "ep%ds-iso rescheduled " "%lu times in %lu 
    seconds\n", stream->bEndpointAddress, is_in? "in": 
    "out", stream->rescheduled, 
    ((jiffies - stream->start)/HZ));
}

위 코드는 USB 디스크 드라이브를 폴링(polling)하는 루틴이다.

  • USB 종단 스트림의 rescheduled 값에 따라 다시 스케줄된  시간동안의 경과 시간을 초로 계산한다.
  • 이때 (jiffies-stream->start)는 다시 스케줄링을 시작한 이후 경과한 jiffies 수를 의미한다.
  • 해당 수를 HZ로 나누면 당연히 초가 변환 된다. (jiffies = 1초당 tick 수 = HZ, 초 = jiffies/HZ)
  • 32비트 jiffies 값은 HZ를 1000으로 가정할 때 약 50일이 지나면 오버플로우가 발생하는데, 시스템 가동 시간과 해당 기간의 차이가 존재할 수 있기에 jiffies는 jiffies_64으로 제공한다.

(2) 긴 지연

커널 관점에서의 jiffies 단위의 지연은 긴 주기로 취급된다. 

보통 긴 주기의 함수들은 좋지 않다. 이러한 함수들은 자신들의 자원을 남에게 주지 않으려는 선점 속성을 가지고 있기 때문이다.

1. 바쁘게 기다림

아래 예시 코드 또한 1초동안 프로세스를 독차지한다.

unsigned long timeout= HZ;
while(time_before(jiffies, timeout)) continue;

 

2. 자면서 기다림

그렇다면 좀더 나은 방법으로 자면서 기다리는 방법도 있다. 

아래 예시 코드는 프로세스를 다른 곳에 양보한 다음에 시간이 경과하기를 기다리는 코드이다.

이러한 작업은 schedule_timeout() 함수로 수행한다. (또 다른 비슷한 함수 : wait_event_timeout(), msleep())

unsigned long timeout=jiffies + HZ;
scheduled_timeout(timeout);

 

#시스템의 정확한 제어 불가능#

  • 이때 시스템은 지연되는 시간을 정확하게 제어할 수 없기때문에 최소 지연 시간으로만 보장될 수 있다. 그렇기 대문에 설정한 시간 이후에 바로 작업이 실행되는 것은 아니다.
  • 또한, 커널과 사용자 영역에서 시작되는 프로세스는 HZ보다 더 정확하게 지연시간을 제어하는 것은 어렵다. 그 이유는 커널 스케줄러가 할당하는 CPU 사용시간(타임 슬라이스)가 일정 타이머 틱 동안만 동작하기 때문이다.
  • 심지어, 프로세스가 정해진 타임아웃 이후에 동작하게 스케줄링 되어 있더라도 우선순위에 의해 다른 프로세스를 선택할 지도 모른다.

 

결론적으로 이렇게 긴 지연 기법은 '프로세스 문맥'에서만 사용 가능하다. 그 이유는 인터럽트 처리기가 schedule()이나 잠들기를 허용하지 않은 특성을 가지고 있기 때문이다. 인터럽트 처리기는 실시간으로 처리가 필요한 이벤트들이다. 그렇기 때문에 오히려 짧은 시간동안 바쁘게 기다리는것은 가능하지만 긴 시간동안 바쁘게 기다리는 것은 안된다. 또한, 인터럽트를 비활성화한 상태에서 긴 시간 동안 바쁘게 기다리는 것도 안된다.

 

타이머 API

커널은 장래 특정 시점에 함수를 수행하는 타이머 API를 제공한다. 여기서 타이머는 특정 시간이 흐른 후에 특정 함수를 실행하도록 설정할 수 있는 기능이다.

 

#include <linux/timer.h>
struct timer_list my_timer;
init_timer(&my_timer);
my_timer.expire = jiffies+ n*HZ;
my_timer.function=timer_func;
my_timer.data = func_parameter;
add_timer(&my_timer);
  • init_timer() 함수는 타이머를 초기화
  • my_timer.expire에는 타이머가 만료되는 시간을 설정
  • jiffies는 시스템이 부팅된 후부터 흐른 시간을 나타내는 값이고, HZ는 1초 동안의 tick 수이기 때문에 jiffies + n*HZ는 현재 시점에서 n초 후를 의미
  • my_timer.function에는 타이머 만료 시 실행할 함수를, my_timer.data에는 함수에 전달할 매개변수를 설정
  • add_timer() 함수를 통해 설정한 타이머를 시스템에 등록
static void timer_func(unsigned long func_parameter)
{
	/* 주기적으로 수행하게 작업한다. */
	/* ... */
	init_timer(&my_timer);
	my_timer.expire = jiffies+n*HZ;
	my_timer.data = func_parameter;
	my_timer.function = timer_func;
	add_timer(&my_timer);
}

 

위 코드는 만료 시간이 되면 한 번만 함수를 실행하는 일회성 타이머이다.

이를 주기적으로 함수를 실행하려면 실행할 함수 내부에 타이머 설정이 필요한다.

 

#추가적인 정보

  • mod_timer() 함수를 통해 타이머의 만료 시간을 변경 가능
  • del_timer() 함수를 통해 타이머 취소 가능
  • clock_settime(), clock_gettime(), setitimer(), getitimer() 등의 함수를 통해 사용자 영역에서 커널의 타이머 서비스를 이용 가능

(3) 짧은 지연

짧은 지연은 커널 관점에서 jiffy 미만일 때이다. 이러한 지연은 프로세스와 인터럽트 문맥를 공통적으로 요청한다. 

 

jiffy 미만의 지연을 구현하기 위해서는 jiffy 기반의 방법을 사용하기 불가능하기 때문에 바쁘게 기다리며 자는것이 유일한 해법이다. 

 

짧은 지연에 대한 커널 API

(아키텍쳐 마다 다름)

  • mdelay() : 밀리초 만큼 지연
  • udelay() : 마이크로초 만큼 지연
  • ndelay() : 나노초만큼 지연
     ⇨ loops_per_jiffy를 사용함
do {
    result = ehci_readl(ehci, ptr);
    /* .. */
    if (result == done) return 0;
    udelay(1)
    usec--;
} while (usec>0);

(4) 펜티엄 TSC

TSC는 Time Stamp Counter의 약자로 시동 시점부터 프로세서가 소비한 클록 사이클을 카운트한다.

TSC는 한때 프로그램에서 CPU 타이머 정보를 힉득하기 위한 방식으로