커널연구회

리눅스 임베디드보드에서 영어학습기 구현방법④
디바이스 드라이버 이해 1

리눅스 임베디드보드 구조 이해를 바탕으로 개발환경을 구축하고, 디바이스 드라이버 작성 및 리눅스 시스템 프로그래밍 방법을 익혀서, 리눅스 임베디드보드에서 영어학습기를 구현해 보도록 하자. 필자가 관련 기술들을 체계적으로 학습하고 실습한 내용으로 임베디드보드에 영어학습기를 구현한 사례를 독자 여러분들과 공유하고자 한다. 내용을 되도록이면 정확하게 전달하고자 노력했으며, 독자 여러분들의 많은 관심과 격려 있기를 바란다. 필자의 작은 노력이 관련 기술 분야에서 한줌의 민들레 꽃씨가 되었으면 한다. 
 
글: 정재준 / rgbi3307@nate.com
커널연구회(www.kernel.bz)

개요

리눅스 임베디드보드에서 영어학습기 구현을 위한 12개의 연재중에서 네번째인 리눅스 디바이스 드라이버에 대해서 기술하고자 한다. 디바이스 드라이버는 리눅스 커널과 하드웨어 장치간의 소통을 담당한다. 입력장치(키보드, 마우스, 터치패드), 유무선 통신장치, USB 장치, 비디오 장치, 오디오 장치 등이 임베디드 보드에 장착되면 리눅스 커널은 디바이스 드라이버를 통하여 이들과 소통한다. 하드웨어 장치들은 하루가 다르게 발전하고 있고 새로운 제품들이 계속 출시되고 있다. 리눅스 커널은 이들을 지원하기 위해서 디바이스 드라이버를 계속 버전업하고 있다. 하지만, 리눅스 커널이 지원하지 못하는 특정한 장치 혹은 나만의 하드웨어 장치는 따로 디바이스 드라이버를 만들어야 한다.
이번 연재에서는 리눅스 커널과 소통하기 위한 디바이스 드라이버에 대하여 기술한다.  먼저, 리눅스 커널 내부를 들여다 보고, 부팅업, 커널 모드와 사용자 모드, 프로세스와 인터럽트 문맥이해, 커널 타이머들, 커널안의 동시작용, 프로세스 파일시스템, 메모리 할당에 대해서 설명한다. 또한, 리눅스 커널 기능들, 커널 쓰레드, 유용한 인터페이스들을 기술하고 디바이스 드라이버 기본원리를 이해하도록 한다.

리눅스 커널 소스 경로

·arch: 아키텍처 구조에 의존되는 파일들을 포함하고 있다. 중앙처리장치(ARM, Motorola, S390, MIPS, Alpha, SPARC…)에 따른 하위경로에 관련 소스가 존재한다.
·block: 블록 저장 장치들을 위한 I/O 처리계획 알고리즘으로 구현되어 있다.
·crypto: 암호처리와 암호화 API를 구현하고 있다.
·Documentation: 커널에 대해서 요약 설명하는 문서들이 있다.
·drivers: 다양한 장치들을 제어하기 위한 드라이버들을 포함하고 있다. I2C, PCMCIA, PCI, USB, IDE, SCSI, CD-ROM, ATM, MTD…
·fs: 파일 시스템들을 구현하고 있다.
·include: 커널 헤더 파일들을 포함하고 있다.
·init: 초기화 및 시작 코드들이 있다.
·ipc: 메시지 큐, 세머포어, 공유 메모리들을 처리하기 위한 Inter-Process Communication (IPC)을 지원한다.
·kernel: 아키텍처에 의존되지 않은(독립적인) 기본 커널이 여기에 있다.
·lib: 범용의 커널 객체(kobject) 핸들러와 같은 라이브러리 루틴들을 포함하고 있으며 Cyclic Redundancy Code(CRC) 계산 함수들도 여기에 있다.
·mm: 메모리 관리가 여기에 구현되어 있다.
·net: 이 경로 하위에 네트워크 프로토콜들이 있다. IPv4, IPv6, IPX, Bluetooth, ATM, Infrared, LAPB, LLC 등을 포함하고 있다.
·scripts: 커널이 빌드될 때 사용된다.
·security: 보안을 위한 프레임워크를 포함하고 있다.
·sound: 리눅스 오디오 하위시스템은 이 경로를 기반으로 한다.
·usr: 이 경로에는 현재 initramfs 구현을 포함하고 있다.

부팅업

리눅스 디바이스 드라이버로 여행을 떠나기 전에, 드라이버 개발자들이 작업한 여러가지 커널 소스들을 확인해서 커널의 기초 이론에 친숙해 지도록 하자. 먼저, 커널 부트 메시지들을 판독해 보자. 관심있는 부분이 나타날 때 마다 멈춤(break)키를 눌러서 확인한다.

커널 모드와 사용자 모드

MS-DOS와 같은 운영체제는 단일 CPU 모드에서 실행이 되지만, UNIX 계열 운영체제는 듀얼 모드를 사용하여 효과적으로 시분할 방식을 구현한다. Linux 머신에서는 CPU가 신뢰성 있는 커널 모드 혹은 제한된 사용자 모드에서 동작한다. 모든 사용자 프로세스들은 사용자 모드에서 실행되는 반면, 커널 자신은 커널 모드에서 실행된다.
커널 모드의 코드는 전체 프로세서 명령 집합과 전체 메모리 및 I/O 영역에 제한없이 접근한다. 사용자 모드의 프로세스가 이러한 특권이 필요하다면, 디바이스 드라이브나 시스템 콜과 같은 다른 커널 모드 코드를 통하여 접근해야 한다. 사용자 모드 코드에서는 페이지 폴트가 허락되지만, 커널 모드에서는 허락되지 않는다.
커널 2.4 이전 버전에서는, 사용자 모드 프로세스만이 문맥 교환하고 다른 프로세스로 교체될 수 있다. 커널 2.6 버전에서는 대부분의 커널 모드 코드가 문맥 교환 가능하며, 커널 모드 코드는 커널 선점 명령을 통하여 프로세스를 독점할 수 있는데, CPU 사용을 양도(넘겨줌, 포기)할 때나 인터럽트나 예외사항이 발생했을 때 프로세스 독점이 해제된다.

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

커널은 프로세스 문맥과 인터럽트 문맥을 조합하여 효과적으로 작업을 수행한다. 사용자 어플리케이션들에 의한 시스템 호출 서비스들은 애플리케이션 프로세스들 상에서 수행이 되며, 이것을 프로세스 문맥으로 실행된다고 할 수 있다. 반면에, 인터럽트 핸들러들은 인터럽트 문맥에 의해 비동기적으로 실행된다. 프로세스 문맥과 인터럽트 문맥은 상호 독립적이다.
프로세스 문맥에서 커널 코드 실행은 선점적이다. 그러나, 인터럽트 문맥은 항상 완료형으로 실행되며 비선점적이다. 왜냐하면, 인터럽트 문맥으로 수행되는 것들에는 제약사항이 있기 때문이다. 인터럽트 문맥으로 실행되는 코드는 다음과 같은 사항을 수행하지 못한다:

·sleep 모드로 갈 때, 혹은 프로세서를 양도하는 경우
·mutex가 요구될 때
·시간을 소모하는 임무가 수행될 때
· 가상 메모리 사용자 공간에 접근할 때

커널 타이머들

커널의 대부분 작업들은 시간의 흐름에 매우 의존한다. 리눅스 커널은 하드웨어에 의해 제공된 서로 다른 타이머들을 사용하여 busy-waiting이나 sleep-waiting과 같은 시간 의존적인 서비스들을 수행한다. 프로세서는 busy-waits 동안에는 사이클을 소비하고, sleep-waits일 때는 CPU을 양도한다. 커널은 지정된 시간 주기가 경과된 후에 실행 요청된 스케줄링 함수들을 수행한다.
먼저, 몇몇 중요한 커널 타이머 변수들인 jiffies, HZ, xtime의 의미를 알아본 다음에, 펜티엄 기반 시스템에서 펜티엄 Time Stamp Counter(TSC)를 사용하여 실행시간을 측정해 보자. 또한 리눅스가 어떻게 Real Time Clock(RTC)을 사용하는지 알아보자.

HZ 와 Jiffies
시스템 타이머들은 프로그램 가능한 주파수 대역에서 프로세서에 끼어(interrupt, pop)든다.  주파수 즉, 초당 타이머 틱들의 수는 커널 변수인 HZ에 포함되어 있다. HZ값은 협상(trade-off)하여 선택된다. 핑거 타이머에서 결과값이 큰 HZ는 좀더 좋은 스케줄링 해결책이다. 그러나, HZ 값들이 커질수록 많은 부하가 발생하며 인터럽트 문맥교환 시간은 더 많은 사이클들이 소모되기 때문에 높은 파워 소비가 발생한다. 
HZ값은 아키텍처에 의존한다. x86 시스템들의 2.4 커널에서는, HZ 값이 기본적으로 100으로 설정된다. 2.6 커널에서는 1000으로 변경된다. 그러나, 2.6.13 커널에서는 250으로 낮아진다. ARM 기반 플랫폼의 2.6 커널에서는 HZ가 100으로 설정된다. 현재의 커널은 빌드시 환경설정 메뉴를 통하여 사용자가 HZ값을 선택할 수 있다. 이 옵션의 기본 설정은 리눅스 배포판에 의존한다. 커널 2.6.21부터는 티클없는 커널(CONFIG_NO_HZ)을 지원한다고 소개 되었다.
Jiffies는 시간들의 개수를 담아두고 있고, 시스템 타이머는 부팅될 때 기동된다. 커널은 jiffies 변수를 매초당 HZ 회수만큼 증가시킨다. HZ값이 100인 커널에서, Jiffy는 10미리초 주기이고, 반면에 HZ가 1000으로 설정된 커널에서는 jiffy가 1미리초 이다.
HZ 와 jiffies를 좀더 이해하기 위해서, IDE 드라이버(drivers/ide/ide.c)에서 인용한 아래의 코드를 참조한다. 이것은 busy 상태에서 디스크 드라이브들을 선별한다.

unsigned long timeout = jiffies (3*HZ);
while (hwgroup->busy) {
/* ... */
if (time_after(jiffies, timeout)) {
return -EBUSY;
}
/* ... */
}
return SUCCESS;

busy 조건이 3초 이내에 클리어 되면 SUCCESS를 반환하고, 그렇지 않다면 -EBUSY를 반환한다. 3*HZ는 3초안에 존재하는 jiffiles의 개수이다. 계산된 timeout(jiffies 3*HZ)는 3초가 경과된 후에 jiffies의 새로운 값이 된다. time_after() 매크로는 요청된 timeout으로 현재의 jiffies값을 비교하고 오버플로우로 인한 계정을 주시한다. 비교동작 수행에 연관되는 함수들은 time_before(), time_before_eq(), time_after_eq() 등이 있다.
jiffies는 소멸되어 없어지는 것으로 정의되고, 변수로 접근하는 것을 최적화하지 않도록 컴파일러에게 묻는다. jiffies로 보증된 것은 각각의 tick 동안에 타이머 인터럽트 핸들러에 의해서 업데이트 되고, 각각의 루프를 통하여 재판독된다. jiffies를 초로 변환하기 위해서는, USB 호스트 제어 드라이버에서 인용한 drivers/usb/host/ehci-sched.c 코드를 참조한다.

if (stream->rescheduled) {
ehci_info(ehci, "epds-iso rescheduled " "lu times in lu
secondsn", stream->bEndpointAddress, is_in? "in":
"out", stream->rescheduled, ((jiffies ? stream->start)/HZ));
}

위의 코드에서 진행하고 있는 디버그 문장은 USB(Universal Serial Bus) 엔드포인트 스트림안의 시간량을 초로 계산한다. 이것은 stream->rescheduled 회수를 다시 스케줄링한다.  (jiffies-stream->start)는 jiffies의 수이며 재 스케줄링이 시작될 때 경과된다. HZ로 나눗셈을 하여 초로 변환한다.
32비트 jiffies 변수는 50일에 가까워지면 오버플로우되고, 이때 HZ가 1000일 때를 가정한다. 이 기간 동안 시스템 가동시간이 많아지므로, 커널은 64비트 jiffies를 저장할 수 있는 jiffies_64 변수를 제공한다. jiffies_64의 하위 32비트는 jiffies와 나란히 연결된다. 32비트 머신에서는, 컴파일러가 하나의 u64 변수를 다른 곳에 할당하기 위해서 2번의 명령처리가 필요하기 때문에, jiffies_64 판독을 자동으로 하지 못한다. 이러한 문제로 인해서, 커널은 get_jiffies_64()라는 함수를 제공한다. drivers/cpufreq/cpufreq_stats.c에 정의된 cpufreq_stats_update() 함수를 통하여 이것의 사용 예제를 확인한다.

Long Delays
커널에서, jiffies 순서안의 지연은 오랜 기간으로 간주된다. 오랜 지연을 처리하는 방법은 busy-looping에 의한 것이다. busy-waits는 dog-in-the-manger 태도를 가지는 함수이다.  이것은 프로세서를 사용하여 유용한 작업을 하지 않으며 다른 것들이 그것을 사용하도록 내버려 두지도 않는다. 아래의 코드는 1초 동안 프로세서를 독차지한다.

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

busy-wait 대신에 sleep-wait가 더 좋은 접근법이다. 지연이 경과되는 대기시간동안, 현재의 코드는 다른 사람에게 프로세서를 양보한다. 이것은 schedule_timeout()을 사용하여 구현된다.

unsigned long timeout = jiffies HZ;
schedule_timeout(timeout);  /* Allow other parts of the kernel to run */

지연은 타임아웃의 낮은 경계치에서 보장된다. 커널 및 사용자 공간에 상관없이 타임아웃을 정밀하게 제어하는 것은 HZ에 비해서 어렵다. 왜냐하면, 프로세스 시간 조각들은 타이머 틱(tick) 동안에 커널 스케줄러에 의해서 업데이트 되기 때문이다. 또한, 사용자의 프로세스가 정해진 타임아웃 후에 실행되도록 계획되어 있더라도, 우선순위를 기반으로 하여 실행되고 있는 큐에서 또 다른 프로세스가 선택될 수 있다.
sleep-waiting 역할을 하는 2개의 서로 다른 함수들로 wait_event_timeout()과 msleep()이 있다. 이들 모두 schedule_timeout()의 도움으로 구현된다. wait_event_timeout()는 사용자 코드가 재실행을 요구할 때 사용되며, 이때 정해진 조건이 true로 되어 있거나, 타임아웃이 발생해야 하는 조건을 성립해야 한다. msleep()는 밀리초로 주어진 수만큼 수면(sleep)한다.
이러한 long-delay 기술들은 프로세스 문맥교환으로 사용될 때 적당한다. sleep-waiting은 인터럽트 문맥교환으로는 실행되지 않는다. 왜냐하면 인터럽트 핸들러가 schedule() 이나 sleep를 허락하지 않기 때문이다. 짧은 기간 동안의 busy-waiting은 인터럽트 문맥교환으로 처리가능하다. 그러나 긴 busy-waiting은 죽음으로 간주된다. 인터럽트가 불가능한 상태에서 오랜 busy-waiting은 금기 사항이다.
커널은 또한 장차 사용할 관점의 함수 실행을 위해서 타이머 API들을 제공한다.  init_timer()를 사용하여 타이머를 동적으로 정의할 수 있으며, DEFINE_TIMER()를 가지고 통계적으로 생성할 수도 있다. 이것이 실행된 후, 사용자 제어 함수로 된 주소와 파라미터로 된 timer_list가 생성되고, add_timer()를 사용하여 이것을 등록한다.

#include

struct timer_list my_timer;

init_timer(&my_timer);  /* Also see setup_timer() */
my_timer.expire = jiffies n*HZ;  /* n is the timeout in number of seconds */
my_timer.function = timer_func;    /* Function to execute after n seconds */
my_timer.data = func_parameter;    /* Parameter to be passed to timer_func */
add_timer(&my_timer);              /* Start the timer */

이것은 일회성 타이머이다. timer_func()을 주기적으로 실행하고 싶다면, timer_func() 안쪽에 적절한 코드를 추가하여 다음 타임아웃 후의 계획을 작성하도록 한다:

static void timer_func(unsigned long func_parameter)
{
/* Do work to be done periodically */
/* ... */

init_timer(&my_timer);
my_timer.expire   = jiffies n*HZ;
my_timer.data     = func_parameter;
my_timer.function = timer_func;
add_timer(&my_timer);
}

my_timer 해지를 변경하기 위해서는 mod_timer(), my_timer 취소는 del_timer()를 사용하고, my_timer가 잠시 미결정 상태인지 알아보기 위해서 timer_pending()을 사용한다.  kernel/ timer.c을 보면, schedule_timeout()이 내부적으로 이것과 동일한 API들을 사용하고 있음을 확인할 수 있다.
clock_settime()과 clock_gettime()과 같은 사용자 공간 함수들은 커널 타이머 서비스들에 접근하기 위해 사용된다. 사용자 애플리케이션은 setitimer()와 getitimer()을 사용하여 정해진 타임아웃이 종결되었을 때 경고 신호를 전달하는 방식으로 제어할 수 있다.

Short Delays
커널의 관점에서는, sub-jiffy 지연이 짧은 주기로 간주된다. 이러한 지연은 보통 프로세스와 인터럽트 문맥교환에서 요청된다. sub-jiffy 지연을 구현하기 위해서 jiffy를 기반으로 하는 방법을 사용하는 것이 불가능하기 때문에, 앞부분에서 sleep-wait에 대해서 언급한 방법들은 작은 타임아웃에는 사용할 수 없다. 유일한 해결책은 busy-wait이다.
짧은 지연을 구현하는 커널 API들에는 mdelay(), udelay(), ndelay()가 있고, 이것들은 각각 밀리초, 마이크로초, 나노초 지연에 해당한다.  이러한 함수들의 실제적인 구현은 아키텍처에 의존하며 모든 플랫폼에서 활용 가능한 것은 아니다.
짧은 주기를 위한 busy-waiting은 프로세서가 하나의 명령을 실행하기 위해 소모한 시간과 반복처리에 필요한 루프 시간 측정에 의하여 이루어진다. 이번 장의 서두에서 언급한 바와 같이, 커널은 부팅이 되는 동안 이러한 측정을 수행하고 loops_per_jiffy 변수에 그 값을 저장한다.
짧은 지연 API들은 busy-loop에서 필요로 하는 시간들의 수를 결정하기 위해 loops_per_jiffy를 사용한다. drivers/usb/host/dhci-hcd.c에 있는 USB 호스트 제어 드라이버의 핸드쉐이크 프로세스 동안에 1마이크로초 지연을 구현하기 위해서 udelay()를 호출한다.  이것은 내부적으로 loops_per_jiffy를 사용한다:

do {
result = ehci_readl(ehci, ptr);
/* ... */
if (result == done) return 0;
udelay(1);     /* Internally uses loops_per_jiffy */
usec;
} while (usec > 0);

Pentium Time Stamp Counter
Time Stamp Counter(TSC)는 펜티엄 호환 프로세스들에 존재하는 64비트 레지스터이며, 프로세서 시작에 의해서 소모되는 클록 사이클의 수를 계산한다. TSC는 프로세스 사이클 속도만큼 증가하기 때문에, 높은 해상도 타이머에 해당한다.
TSC는 진행 중인 코드의 실행시간을 마이크로초 단위로 정밀하게 측정하기 위하여 rdtsc 명령을 사용하여 접근한다. TSC 틱들(ticks)은 CPU 클럭 속도로 나누어서 초단위로 변환할 수 있으며, 커널 변수인 cpu_khz에 읽혀질 수 있다.
아래의 인용에서, low_tsc_ticks은 TSC의 하위 32비트를 포함하고 있는 반면, high_tsc_ticks는 상위 32비트를 포함하고 있다. 하위 32비트는 사용자 프로세서 속도에 따라서 몇 초 안에 오버플로우 되지만, 아래와 같이 코드를 도구화 하는 목적으로 많이 활용된다.

unsigned long low_tsc_ticks0, high_tsc_ticks0;
unsigned long low_tsc_ticks1, high_tsc_ticks1;
unsigned long exec_time;
rdtsc(low_tsc_ticks0, high_tsc_ticks0);  /* Timestamp before */
printk("Hello Worldn");                 /* Code to be profiled */
rdtsc(low_tsc_ticks1, high_tsc_ticks1);  /* Timestamp after */
exec_time = low_tsc_ticks1 - low_tsc_ticks0;

exec_time은 1.8GHz 펜티엄 머신에서 871로 측정된다.
높은 해상도 타이머(CONFIG_HIGH_RES_TIMERS) 지원은 2.6.21 커널로 병합되었다. 이것은 하드웨어에 지정된 높은 속도 타이머를 사용하여 높은 측정능력을 nanosleep()과 같은 API들에게 제공한다.
펜티엄 계열 머신들에서, 커널은 이 능력을 제공하기 위해서 TSC를 지렛대로 활용한다.

Real Time Clock
RTC는 비휘발성 메모리안의 절대적인 시간이다. x86 PC들에서, RTC 레지스터들은 배터리로 전원이 공급되는 Complementary Metal Oxide Semiconductor(CMOS) 메모리로 구성되어 있다.
임베디드 시스템에서는, RTC가 프로세서 내부에 있거나, SPI 버스나 I2C에 외부적으로 연결되어 있을 수 있다. RTC 배터리는 수년동안 지속되고 컴퓨터의 수명보다 오래가기 때문에, 배터리를 다른 것으로 교체할 필요없다. RTC는 다음과 같은 역할을 한다:
·클록의 절대치를 읽거나 설정하고, 클럭이 업데이트되는 동안에 인터럽트가 발생한다.
·2HZ에서 8192HZ 범위의 주파수에서 주기적인 인터럽트가 발행한다.
·알람을 설정한다.

많은 애플리케이션들은 절대적인 시간이나 기준시간이 필요하다. jiffies는 시스템이 부팅 되었을 때의 상대적인 시간이므로 절대적인 기준시간을 포함하고 있지 않다. 커널은 xtime이라는 변수에서 기준 시간을 유지 관리한다. 부팅이 진행되는 동안, RTC를 판독하여 xtime을 현재의 기준 시간으로 초기화 한다. 시스템이 정지 되었을 때, 기준 시간은 RTC에 다시 저장된다. 사용자는 do_gettimeofday()를 사용하여 하드웨어가 제공하는 최상의 해상도로 이루어진 기준시간을 읽을 수 있다.

#include
static struct timeval curr_time;
do_gettimeofday(&curr_time);
my_timestamp = cpu_to_le32(curr_time.tv_sec); /* Record timestamp */

사용자 영역의 코드가 기준 시간에 접근 가능하도록 하는 함수들이 있다.  이 함수들은 아래와 같은 내용을 포함하고 있다:

·time()은 달력 시간을 가져오거나 신시대(1970년 1월 1일 00:00:00) 부터 초 단위 숫자를 반환한다.
· localtime()은 숫자열 형태의 달력 시간을 반환한다.
·mktime()는 localtime()을 역변환한다.
·gettimeofday()는 마이크로초 단위의 달력 시간을 반환하는데, 이때 사용자 플랫폼이 이것을 지원해야 한다.
사용자 영역에서 RTC를 사용하는 또 다른 방식은 /dev/rtc 문자 장치이다. 단지 하나의 프로세스가 이 장치에 한번 접근하도록 허락된다.

커널안의 동시성

멀티코어 랩탑들의 출현으로, 대칭적인 다중 프로세싱(SMP: Symmetric Multi Processing)이 고급기술 사용자들의 영역에 많이 개방되었다. SMP와 커널 선점은 다중적인 쓰레드 실행을 가능하게 하는 시나리오들이다. 이러한 쓰레드들은 공유된 커널 데이터 구조체에서 동시에 동작할 수 있다. 이러한 데이터 구조체에는 연속적인 순서로 접근되어야 한다.
공유된 커널 자원들이 동시 접근으로 부터 보호되어야 하는 기본지식에 대해서 논의해보자.  간단한 예제로 시작하고, 점차적으로 복잡한 것들인 인터럽트, 커널 선점, SMP 등을 소개한다.

Spinlocks and Mutexes
공유된 자원들에 접근하는 코드영역을 크리티컬 섹션이라고 한다. Spinlock과 mutex들(간단히, 상호배제)은 2개의 기본적인 장치들이 있으며, 커널에서 크리티컬 섹션들을 보호하는데 사용된다.  각각을 알아보도록 하자.
spinlock은 한 번에 하나의 쓰레드만이 크리티컬 섹션에 들어가도록 보장해 준다. 크리티컬 섹션에 들어가고자 하는 다른 쓰레드는 첫 번째 쓰레드가 종료될 때까지 입구 쪽에서 대기하고 있어야 한다. 커널 쓰레드이기 보다, 우리는 실행 쓰레드를 참조하는 쓰레드로 사용한다는 것을 알아두자.
spinlock의 기본적인 사용은 아래와 같다:

#include
spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */

/* Acquire the spinlock. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, spinlock() has to busy-wait.
*/
spin_lock(&mylock);

/* ... Critical Section code ... */

spin_unlock(&mylock); /* Release the lock */

spinlock들은 바쁜 크리티컬 섹션에 들어가고자 할때, 쓰레드들을 스핀에 넣어두는 반면, mutex들은 크리티컬 섹션을 점유할 때까지 쓰레드들을 수면(sleep)상태로 한다. 스핀에서는 프로세서 사이클들을 소모해서 좋지 않으므로, 평가된 대기시간이 긴 경우에, mutex가 spinlock보다 크리티컬 섹션을 보호하는데 더 적당하다. mutex의 관점에서, 2개의 문맥 교환보다 더 많은 것들은 오랜 시간으로 간주한다. 왜냐하면, mutex는 쓰레드를 수면상태로 전환했다가, 깨어날 때 다시 원래의 상태로 되돌려야 하기 때문이다.
대부분의 경우, spinlock 혹은 mutex 중에서 어떤 것을 사용해야 되는지 판단하는 것은 쉽다:
크리티컬 섹션이 수면(sleep)을 필요로 한다면, 선택의 고민없이 mutex를 사용해야 한다. spinlock이 요구된 후에, 대기 큐상에서 스케줄, 선점, 수면(sleep)은 금지된다.
·경합 상황에서 mutex들은 호출되는 쓰레드를 수면(sleep)에 넣기 때문에, 선택의 여지가 없으나, 인터럽트 핸들러 안에서는 spinlock을 사용해야 한다.

기본적인 mutex 사용법은 아래와 같다:

#include

/* Statically declare a mutex. To dynamically create a mutex, use mutex_init() */
static DEFINE_MUTEX(mymutex);

/* Acquire the mutex. This is inexpensive if there
 * is no one inside the critical section. In the face of
 * contention, mutex_lock() puts the calling thread to sleep.
 */
mutex_lock(&mymutex);

/* ... Critical Section code ... */

mutex_unlock(&mymutex);      /* Release the mutex */

동시발생 보호의 사용을 보여주기 위해서, 프로세스 문맥에만 존재하는 크리티컬 섹션으로 시작하여, 아래 순서와 같이 점차적으로 복잡한 경우를 고려해 보자.

1. 비선점적인 커널이 실행되고 있는 단일프로세서(UP) 머신에서 프로세스 문맥만 존재하는 크리티컬 섹션.
2. 비선점적인 커널이 실행되고 있는 단일프로세서(UP) 머신에서 프로세스와 인터럽트 문맥들이 존재하는 크리티컬 섹션.
3. 선점적인 커널이 실행되고 있는 단일프로세서(UP) 머신에서 프로세스와 인터럽트 문맥들이 존재하는 크리티컬 섹션.
4. 선점적인 커널이 실행되고 있는 SMP 머신에서 프로세스와 인터럽트 문맥들이 존재하는 크리티컬 섹션.

사례 1: 프로세스 문맥, UP 머신, 비선점
이것은 가장 간단한 경우이며 잠금(lock)이 필요없어 논의하지 않아도 된다.
사례 2: 프로세스와 인터럽트 문맥들, UP 머신, 비선점
이 경우, 크리티컬 섹션영역을 보호하기 위해서 인터럽트들만 불가능하게 하면 된다.  왜냐하면, A와 B를 프로세스 문맥 쓰레드라고 가정하고, C를 인터럽트 문맥 쓰레드라고 가정한다면, 동일한 크리티컬 섹션에 들어가기 위해서 모두 경쟁한다.

쓰레드 C는 인터럽트 문맥으로 실행되고 있고 쓰레드 A 혹은 쓰레드 B에게 양보하기 전에 항상 완료형으로 실행되기 때문에, 보호에 대해서 걱정할 필요 없다. 커널이 비선점적이기 때문에, 쓰레드 A는 쓰레드 B에 대해서 연관되어 있을 필요 없다(역방향도 성립). 쓰레드 A와 쓰레드 B는 그들이 같은 섹션에 있는 동안 쓰레드 C가 크리티컬 섹션을 두드리고 있을 가능성에 대해서만 안전장치를 할 필요가 있다.
크리티컬 섹션에 들어가기에 앞서서 인터럽트들을 불가능하게 하여 목적을 달성한다:

Point A:
local_irq_disable();  /* Disable Interrupts in local CPU */
/* ... Critical Section ...  */
local_irq_enable();   /* Enable Interrupts in local CPU */

그러나, 실행이 Point A에 도달할 때 인터럽트가 이미 불가능으로 되어 있다면, local_irq_enable()는 인터럽트 상태를 복구하는 것에 비해서, 인터럽트들을 재가능하도록 할 때 불쾌한 예외 사항을 발생시킨다.  이것을 아래와 같이 수정할 수 있다:

unsigned long flags;

Point A:
 local_irq_save(flags);     /* Disable Interrupts */
 /* ... Critical Section ... */
local_irq_restore(flags);  /* Restore state to what it was at Point A */

이것은 Point A에서 인터럽트 상태에 상관없이 정확하게 동작한다.

사례 3: 프로세스와 인터럽트 문맥들, UP 머신, 선점
선점이 가능하다면, 인터럽트들을 불가능하게 하는 것으로 크리티컬 영역이 보호되지 못한다.  프로세스 문맥교환에서 여러개의 쓰레드들이 동시에 크리티컬 섹션에 진입할 가능성이 있다.  쓰레드 A와 B는 각각 자신들을 보호하고 쓰레드 C에 대항하여 안전장치를 추가하는것이 필요하다.  명백한 해결책은, 크리티컬 섹션의 시작전에 커널 선점을 불가능하게 하고 종료시에 재가능하도록 하고, 인터럽트들을 불가능/재가능 하도록 추가하는 것이다.  이것을 위해, 쓰레드 A와 B는 spinlock 변수 irq를 사용한다:

unsigned long flags;

Point A:
/* Save interrupt state.
* Disable interrupts - this implicitly disables preemption */
spin_lock_irqsave(&mylock, flags);

/* ... Critical Section ... */

/* Restore interrupt state to what it was at Point A */
spin_unlock_irqrestore(&mylock, flags);

선점 상태는 Point A에서 무엇을 나타내는지 명시적으로 재저장할 필요없다. 왜냐하면 커널이 내부적으로 선점 카운터라는 변수를 통하여 이 역할을 하기 때문이다. 이 카운터는 선점이 불가능(preempt_disable() 사용됨) 으로 될때마다 증가되고, 선점이 가능(preempt_enable() 사용됨)으로 될때마다 감소된다. 이 카운터가 0이라면, 선점은 효력을 나타낸다.

사례 4: 프로세스와 인터럽트 문맥들, SMP 머신, 선점
크리티컬 섹션이 SMP 머신에서 실행되고 있다고 가정하자. 사용자의 커널은 CONFIG_SMP와 CONFIG_PREEMPT가 켜져 있는 것으로 환경설정 되었다.
이것과 거리가 멀게 논의된 시나리오들에서, 초기의 spinlock은 가능/불가능 선점과 인터럽트들보다 더 적게 행동했다. 실제 락킹은 함수적으로 컴파일 된다. SMP 내에서는, 락킹 논리가 컴파일되고, 초기의 spinlock은 SMP-보호를 나타낸다.  SMP-가능 의미는 다음과 같다:

unsigned long flags;

Point A:
/*
- Save interrupt state on the local CPU
- Disable interrupts on the local CPU. This implicitly disables  preemption.
- Lock the section to regulate access by other CPUs
*/
spin_lock_irqsave(&mylock, flags);

/* ... Critical Section ... */

/*
- Restore interrupt state and preemption to what it was at Point A for the local CPU
- Release the lock
*/
  spin_unlock_irqrestore(&mylock, flags);

SMP 시스템에서는, spinlock이 요구될 때 로컬 CPU상의 인터럽트들만이 불가능으로 된다. 그래서, 프로세스 문맥 쓰레드(쓰레드 A)는 하나의 CPU에서 실행되어야 하고, 반면에 인터럽트 핸들러(쓰레드 C)는 또다른 CPU에서 실행된다. 로컬이 아닌 프로세서상의 인터럽트 핸들러는 로컬 프로세서상의 프로세스 문맥교환 코드가 크리티컬 섹션에서 빠져나올 때까지 spin-wait하기 위해 필요하다. 인터럽트 문맥교환 코드를 spin_lock()/spin_unlock()라 하며 다음과 같은 역할을 한다:

spin_lock(&mylock);

/* ... Critical Section ... */

spin_unlock(&mylock);

irq 변수들과 유사하게, spinlock 또한 bottom half(BH) 요소들을 가진다.  lock이 요구될 때, spin_lock_bh()는 BH들을 불가능하게 하고, 반면에 spin_unlock_bh()는 lock이 풀릴 때 BH들을 재가능하도록 한다. BH들에 대해서는 다음 연재에서 기술한다.

프로세스 파일시스템

프로세스 파일시스템(procfs)은 커널 내부로 창들을 생성하는 가상 파일시스템이다.  procfs 내용물을 확인할 때 나타나는 데이터는 커널 on-the-fly에 의해서 생성된다.  procfs 안의 파일들은 커널 매개값들을 환경설정하고, 커널 구조체들을 확인하고, 디바이스 드라이버들의 통계자료들을 수집하고, 일반적인 시스템 정보를 가져오기 위해 사용된다.
Procfs는 가상(pseudo)의 파일시스템이다. 즉, procfs에 존재하는 파일들은 하드디스크와 같이 물리적으로 저장되는 장치들에 연관되지 않는다는 것이다. 대신에, 이 파일들안의 데이터는 커널에 의해 연관되는 지점의 요청으로 동적 생성된다. 이런 이유로, procfs안의 파일 크기들은 0으로 표현된다. Procfs는 보통 커널이 부트 되는 동안에 /proc 경로 하위에 마운트된다; mount 명령을 통하여 이것을 확인할 수 있다.
procfs의 특성을 확인해 보기 위해, /proc/cpuinfo, /proc/meminfo, /proc/interrupts, /proc/tty/driver/se rial, /proc/bus/usb/devices, /proc/stat 등의 내용물들을 점검한다. /proc/sys/에 파일들을 작성함으로써 몇몇 커널 파라미터들을 실행시에 변경할 수 있다.
예를 들면, /proc/sys/kernel/printk에 새로운 값들을 설정하여 커널의 printk 로그 레벨들을 변경할 수 있다. 많은 도구들(ps와 같은것)과 시스템 능력 점검 도구들(sysstat와 같은것)은 내부적으로 /proc 하위에 있는 파일들에서 정보를 참조한다. 커널 2.6에서 소개되는 Seq 파일들은 대량의 procfs 동작들을 간소화한다.

메모리 할당

몇몇 디바이스 드라이브들은 존재하는 메모리 영역을 알고 있어야 한다. 또한, 많은 드라이브들이 메모리 할당 함수들을 필요로 한다. 이번 섹션에서는 이들에 대해서 기술한다.
커널은 물리적 메모리를 페이지들로 편성한다. 페이지 크기는 아키텍처에 의존된다. x86기반 머신들에서는 페이지 크기가 4096 바이트이다. 물리적 메모리안의 각각의 페이지는 페이지 구조체를 가진다(include/linux/mm_types.h에 정의되어 있음).

struct page {
unsigned long flags; /* Page status */
atomic_t _count;     /* Reference count */
/* ... */
void * virtual;      /* Explained later on */
};

32비트 x86 시스템들에서는, 기본적인 커널 환경설정에서 활용 가능한 4GB 주소 공간을 사용자 프로세스를 위한 3GB 가상 메모리 공간과 커널을 위한 1GB 공간으로 분리한다.  커널이 다룰 수 있는 물리적 메모리 공간은 1GB로 제한되어 있음을 알 수 있다. 실제적으로 896MB로 제한되는데, 왜냐하면 주소공간의 128MB는 커널 데이터 구조체들에 의해서 점유되기 때문이다. 커널 환경설정에서 3GB/1GB 분리를 변경함으로써 이러한 제한을 증가시킬 수 있으나, 사용자 프로세스의 가상 주소공간이 줄어들게 되면, 메모리 집약적인 애플리케이션들에게 나쁜 영양을 미칠 수 있다.
896MB 하위에 매핑된 커널 주소들은 물리적 주소와는 다른 논리적 주소이다. "high memory" 지원으로, 커널은 특별한 매핑방법을 사용한 가상 주소를 통하여 896MB 이상의 메모리에 접근할 수 있다. 모든 논리적 주소들은 커널 가상 주소들이지만, 역방향은 성립하지 않는다.
커널 메모리 공간에 대해서 설명하면 다음과 같다:
1. ZONE_DMA (<16MB), Direct Memory Access(DMA)를 위해 사용하는 영역. 전통적인 ISA 장치들은 24개의 주소선을 가지고 있기 때문에 첫 번째 16MB에만 접근할 수 있고, 커널은 이 영역을 이러한 장치들에게 제공해 준다.
2. ZONE_NORMAL (16MB to 896MB), 일반적인 주소로 접근가능 영역, low memory라 호칭한다. low memory 페이지들을 위한 구조체 페이지 내에서 "virtual" 필드는 논리적인 주소들과 연관되는 것을 포함한다.
3. ZONE_HIGH (>896MB), ZONE_NORMAL 안의 영역들에 상주하는 페이지들을 매핑한 후에 커널만이 접근 가능한 공간.(kmap()과 kunmap()을 사용). 연관되는 커널 주소들은 가상적이고 논리적이지 않다. high memory 페이지들을 위한 구조체 페이지 안에 "virtual" 필드는 페이지가 kmapped 되지 않았을 때, NULL 값을 가르킨다.

kmalloc()는 메모리 할당 함수이며 ZONE_NORMAL로 부터 연속적인 메모리를 반환한다.
이것의 원형은 아래와 같다:

void * kmalloc(int count, int flags);
여기서 count는 할당한 바이트들의 숫자이고, flags는 모드 특성을 나타낸다.  include/linux/ gfp.h 안에서 모든 flags를 리스트로 제공하지만, (gfp는 get free pages를 의미한다) 이들은 일반적으로 하나씩 사용된다:
GFP_KERNEL는 프로세스 문맥교환 코드가 메모리 할당을 위해서 사용한다. 이 플래그가 정해지면, kmalloc()는 수면(sleep)으로 진입하고 자유롭게 해제되는 페이지들을 위해 대기한다.
GFP_ATOMIC는 인터럽트 문맥교환 코드에서 메모리를 소유하기 위해서 사용한다. 이 모드에서는, kmalloc()가 자유로운 페이지들을 위해서 sleep-wait하는 것을 허락하지 않으므로, GFP_ATOMIC으로 할당될 수 있는 성공확률은 GFP_KERNEL에 비해서 낮다.
kmalloc()에 의해서 제공되는 메모리는 이전에 구체화한 내용물들을 유지하고 있지만, 이것이 사용자 공간으로 노출되면 보안 위험이 있을 수 있다.
메모리를 0으로 kmalloc 하려면, kzalloc()를 사용한다. 대용량의 메모리 버퍼들 할당이 필요하다면, 물리적으로 연속적인 메모리를 요청할 수 없으며, kmalloc() 보다는 vmalloc()을 사용한다.

void *vmalloc(unsigned long count);

여기서 count는 할당을 요청하는 크기이다. 함수는 커널 가상 주소들을 반환한다. 
vmalloc()는 kmalloc()에서 제한된 것 보다 더 큰 할당크기를 즐겨 쓰지만, 속도가 느리고 인터럽트 문맥 교환으로부터 호출될 수 없다.
게다가, 메모리를 직접적으로 접근(DMA)하는 것을 수행하기 위해, vmalloc()에 의해서 제공되는 물리적으로 비연속적인 메모리를 사용할 수 없다. 높은 수행능력을 가진 네트워크 드라이브들은 많은 기술적 링들을 장치가 열릴 때 할당하기 위해 vmalloc()을 일반적으로 사용한다.
맺음말

이번 연재에서는 리눅스 디바이스 드라이브를 이해하기 위한 배경지식에 대하여 기술했다.  다시 요약하면,
·리눅스 커널 소스 경로: 리눅스 커널 소스가 위치하고 있는 경로 설명
·부팅업: 부팅시 리눅스 커널이 실행되는 순서
·커널 모드와 사용자 모드: 커널 모드와 사용자 모드 비교
·프로세스 문맥과 인터럽트 문맥: 프로세스 문맥과 인터럽트 문맥 교환에 대한 배경지식
·커널 타이머들: 커널 타이머에 대한 이해
·커널안의 동시성: 커널에서 동일한 자원에 접근하는 문제
·프로세스 파일시스템: 프로세스 파일시스템에 대한 이해
·메모리 할당: 리눅스 디바이스 드라이버에서 필요로 하는 커널 메모리 할당

위의 사항들은 리눅스 커널과 소통하기 위한 디바이스 드라이버의 배경지식에 해당한다. 이것을 바탕으로 다음 연재에서는 디바이스 드라이버를 좀더 심도있게 기술할 예정이다.

참고문헌
[1] Computer Organization and Design (c)2007
 by David A. Patterson, John L. Hennessy
[2] Embedded Linux Primer (c)2006
 by Christopher Hallinan
[3] Essential Linux Device Drivers (c)2008
 by Sreekrishnan Venkateswaran
[4] LINUX System Programming (c)2007
 by Robert Love
[5] DATA STRUCTURES (c)2005
 by Richard F. Gilberg, Behrouz A. Forouzan
[6] (주)에프에이리눅스 포럼
 http://forum.falinux.com/zbxe/
[7] RMI사의 Au1200 User's Guide
 http://www.rmicorp.com/
[8] Linux Kernel In a Nutshell (c)2007 O'Reilly
 by Greg Kroah-Hartman
회원가입 후 이용바랍니다.
개의 댓글
0 / 400
댓글 정렬
BEST댓글
BEST 댓글 답글과 추천수를 합산하여 자동으로 노출됩니다.
댓글삭제
삭제한 댓글은 다시 복구할 수 없습니다.
그래도 삭제하시겠습니까?
댓글수정
댓글 수정은 작성 후 1분내에만 가능합니다.
/ 400
내 댓글 모음
저작권자 © 테크월드뉴스 무단전재 및 재배포 금지