| Max Bruning, OpenSolaris Community |
|
커널 지원 유틸리티
드라이버 상태 관리
Per-instance 상태를 관리하는 것을 돕기 위해 DDI/DKI는 여러 가지 루틴을 제공 합니다. Per-instance 상태는 주어진 디바이스 인스턴스의 상태를 추적하는데 필요한 모든 것을 포함하고 있습니다. 이러한 루틴은 상태를 위한 메모리 공간을 유지 합니다. 사용자는 간단히 공간 할당을 요구한 다음 할당된 공간의 포인터를 받아 오고 상태를 초기화 시키고 상태를 업데이트 하고 인스턴스가 더 이상 존재하지 않을 때 상태를 초기화 시키면 됩니다. 커널은 사용자를 위해 상태를 관리해 줍니다.(링크된 리스트, 배열, 트리 등등) 모든 상태 루틴들은 dev_info_t 혹은 인스턴스 번호를 매개변수로 취합니다. 드라이버는 마이너 디바이스 번호에서 인스턴스 넘버를 불러 오는 방법이 필요합니다. 그러나 구현은 드라이버에 달렸습니다. 가장 간단하게 인스턴스 넘버를 만드는 방법은 마이너 디바이스 넘버와 같도록 만드는 것입니다. 알아두어야 할 점은 인스턴스 넘버는 dev_info_t 의 필드이고, ddi_get_instance(9f)에 의해 불려질 수 있습니다.
상태 관리 루틴은 다음과 같습니다:
ddi_soft_state_init(9f): 드라이버의_init(9e)에 의해 불려지며DDI 상태 루틴에 의해 사용되는 핸들을 할당.ddi_soft_state_zalloc(9f):attach(9e)에 의해 불려지며 주어진 인스턴스의 상태를 할당하고 초기화 하는데 사용.ddi_get_soft_state(9f): 드라이버에 의해 주어진 인스턴스의 상태 구조를 불러 오는데 사용.ddi_soft_state_free(9f):detach(9e)에 의해 불려지며 주어진 인스턴스의 디바이스에 상태 구조를 초기화 하는데 사용.ddi_soft_state_fini(9f):_fini(9e)에 의해 불려지며ddi_soft_state_init(9f)에 의해 초기화 된 핸들을 해제 시키는데 사용.
물론 디바이스는 디바이스는 상태를 가지고 있지 않을 수도 있고 혹은 사용자만의 상태를 관리하도록 선택할 수도 있습니다.
동기화 메커니즘
솔라리스는 커널에서 돌아가는 여러 쓰레드의 동시 접근, 수정을 보호 하는 방법을 위해 4가지 동기화 메커니즘을 제공합니다. 이러한 메커니즘은 권장사항이므로 굳이 사용할 필요는 없습니다. 그러나 필요한 곳에 적절히 쓰지 못한 다면 오류는 당연히 발생할 것입니다. 동기화 메커니즘은 다음과 같은 것이 있습니다:
- 뮤텍스(Mutexes), 혹은 상호 배타적 락. 이러한 것은 spin 혹은 adaptive가 될 수 있습니다. spin 뮤텍스는 만약 다른 쓰레드가 mutex를 소유 하고 있다면 빈 루프를 한 바퀴 돌게 됩니다. adaptive mutex는 쓰레드가 만약 프로세서 안에서 수행되고 있다면 한 바퀴 돌게 되고 쓰레드가 수행되고 있지 않다면 멈추게(blocks:switch out) 하게 됩니다. 추가적으로 spin mutex는 mutex와 연관된 레벨의 인터럽트를 매스킹 합니다.
- Condition variables, "sleep/wakeup" 타입의 메커니즘을 제공합니다.
- Semaphores,
카운팅(counting)혹은 바이너리. p와v연산에 의해 구현됩니다. - Reader/writer locks, 혹은 shared/exclusive 락
mutex는 기본적으로 다른 메커니즘에 바탕이 되는 메커니즘 입니다. 다음은 mutex의 기본적인 예입니다.
kmutex_t mp;
/* must be global, may be dynamically allocated, usually in per-instance state */
<-- in _init() or xxx_attach() -->
/* initialize mutex(es) */
ddi_get_iblock_cookie(devinfop, 0, &iblock_cookie);
/* needed if device interrupts */
mutex_init(&mp, NULL, MUTEX_DRIVER, iblock_cookie);
/* otherwise, iblock_cookie = NULL */
<-- to use the mutex -->
mutex_enter(&mp); /* acquire the mutex */
critical code goes here;
mutex_exit(&mp); /* release the mutex */
<-- in _fini() or xxx_detach() -->
mutex_destroy(&mp);
반복적인 mutex 진입은 패닉 상태를 유발합니다.(Linux와 다름) 계층적인 데드락 또한 패닉 상태를 유발합니다.
다음의 코드 예제는 condition variable을 사용하는 예입니다. 초기화 단계는 생략되었습니다.
kcondvar_t cv; /* the condition variable itself is opaque */
kmutex_t mp; /* must use mutex */
int flag = 0;
/* represents the condition, event, resource, etc. you are waiting on */
<--thread that needs to wait-->
mutex_enter(&mp);
while (flag == 0)
/* while condition not met, resource not available, event has not occurred, etc. */
cv_wait(&cv, &mp); /* mutex is released and thread is switched out */
/* before returning, mutex is re-acquired */
handle condition, resource, event, etc; /* here we own the mutex */
mutex_exit(&mp);
<--wakeup thread (typically interrupt handler) -->
mutex_enter(&mp);
++flag;
cv_signal(&cv); /* or cv_broadcast(&cv), i.e, wakeup one or all */
mutex_exit(&mp);
동적 메모리 할당
동적 메모리 할당은 전형적으로 kmem_alloc(9f) 와 kmem_free(9f) 를 사용합니다. 또한 드라이버는 kmem_cache_create(9f), kmem_cache_destroy(9f), kmem_cache_alloc(9f), 그리고 kmem_cache_free(9f)을 사용할 수 있습니다. DMA 가능한 메모리 할당을 위해서 ddi_dma_mem_alloc(9f) 을 사용합니다.
kmem_alloc(9f) 루틴은 바이트 사이즈를 차지 하고 플래그를 변수로 사용합니다. 이 플래그는 간단히 공간이 사용가능하지 않을 때 무엇을 해야 할지 지정하는 일을 합니다. -- KM_SLEEP 혹은 KM_NOSLEEP. KM_NOSLEEP 의 경우에 kmem_alloc() 은 NULL 포인터를 리턴하고 호출자에 복구를 떠넘깁니다.
디바이스 레지스터 접근
디바이스 레지스터(그리고 디바이스 메모리)에 접근 하기 위해 ddi_regs_map_setup(9f) and ddi_get/ddi_put 루틴을 사용합니다. 메모리 맵 된 디바이스를 다루기 위해(예를 들어 프레임 버퍼) 버퍼의 맵은 ddi_regs_map_setup(9f)을 사용하고 디바이스를 액세스 하기 위해 리턴된 어드레스를 사용 합니다. 다음의 간단한 예를 참고하시기 바랍니다.
ddi_device_acc_attr_t foo_acc = { /* used for endianess, ordering constraints */
DDI_DEVICE_ATTR_V0, /*revision number of structure */
DDI_STRUCTURE_LE_ACC, /* device is little endian */
DDI_STRICTORDER_ACC
/* all references are issued by cpu in program order, (no re-ordering) */
};
char *dev_regs; /* will point to device registers, or contain port number */
ddi_acc_handle_t *foo_acc_handle; /* filled by ddi_regs_map_setup */
<-- in foo_attach -->
ddi_regs_map_setup(devinfop, 0, &dev_regs, 0, 0, &foo_acc, &foo_acc_handle);
<-- to access device registers -->
x = ddi_get8(foo_acc_handle, dev_regs); /* retrieve 8 byte register */
인터럽트 다루기
솔라리스의 인터럽트 다루는 방법은 리눅스와 비슷합니다. 사용자는 인터럽트 후 리턴하는 하나의 인터럽트 핸들러를 구현하거나 두 개의 인터럽트 핸들러를 구현할 수 있습니다. 첫번째는 하드웨어 인터럽트를 다루고 두번째 것은 소프트웨어 인터럽트 같은 하위 우선순위를 다룹니다. 리눅스 태스크릿(tasklet)은 timeout(9f) 에 의해 수행되는 루틴을 이용하여 구현할 수 있습니다. 그러나 현재 DDI/DKI 에는 사용할 수 있는 프로그래밍 인터페이스가 없습니다.
솔라리스는 인터럽트를 15개의 우선순위 레벨로 매핑 시켜서 레벨이 높을 수록 더 높은 우선 순위를 가집니다. 10 이상의 레벨을 갖는 인터럽트를 "상위 인터럽트"라고 부릅니다. 그들은 그들 고유의 쓰레드에서 수행 됩니다. 이것은 즉 block을 유발 할 수도 있다는 얘기 입니다( 예를 들어 이미 락된 mutex를 얻어오고자 시도할 수도 있음) 레벨 10 이상의 인터럽트는 인터럽트가 발생했을때 수행되는 쓰레드의 문맥 하에서 수행 됩니다. 고레벨의 인터럽트들과 관련된 mutex는 spin mutex입니다. 그리고 mutex와 관련된 레벨의 인터럽트들을 매스킹 합니다. 시스템 clock() 루틴은 타임 슬라이싱을 담당하고 인터럽트 레벨 10에서 수행됩니다. 버스 드라이버는 nexus에서 연결된 디바이스의 인터럽트 레벨을 결정 합니다. PCI버스 드라이버는 인터럽트 레벨을 할당하기 위해 클래스-코드 설정 공간을 사용합니다. ISA와 EISA nexus 드라이버에서 모든 디바이스의 레벨은 IPL5 입니다. 주어진 디바이스 인스턴스는 driver.conf(4) 파일의 interrupt-priorities=level 설정에 의해 오버라이드 됩니다. 좀더 자세한 정보는 sysbus(4)를 참고 바랍니다.
인터럽트를 다루기 위한 스텝은 다음과 같습니다:
attach(9e)에서 인터럽트를 등록하는 것은 viaddi_add_intr(9f)를 사용하거나선택적으로ddi_add_softintr(9f)를 사용합니다.- 핸들러는 반드시:
- 디바이스가 인터럽트 됐는지 체크해야 합니다. 그렇지 않다면
DDI_INTR_UNCLAIMED을 리턴합니다. 만약 드라이버가DDI_INTR_UNCLAIMED를 리턴한다면 드라이버의 코드는 같은 우선순위 레벨의 인터럽트를 가진 다음 디바이스(만약 있다면) 를 호출 하려고 할 것입니다. 다시 말해서 솔라리스는 인터럽트 우선순위 레벨을 기반하여 드라이버 인터럽트 핸들러를 폴링 합니다. - 하드웨어-특수한 작업을 수행하여 인터럽트를 다룹니다.
- 디바이스에 인터럽트가 다루어졌음을 알립니다. 이 과정은 레벨-트리거 인터럽트를 사용하는 디바이스를 위해 필요합니다.
DDI_INTR_CLAIMED를 리턴합니다.
- 디바이스가 인터럽트 됐는지 체크해야 합니다. 그렇지 않다면
상위 인터럽트를 위해 최소한의 가능한 상위 핸들러를 호출하고 나머지 처리를 위해 소프트웨어 인터럽트를 트리거 할 것을 권장합니다.
다음의 예는 attach(9e) 가 인터럽트 핸들러를 등록하고 핸들러가 작업을 하는 것을 보여 줍니다.
static uint_t foo_intr(caddr_t arg);
static int
foo_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
int instance;
struct foo_state *fsp;
instance = ddi_get_instance(dip);
ddi_soft_state_zalloc(statep, instance);
fsp = ddi_get_soft_state(statep, instance);
initialize fsp;
initialize mutexes, etc.;
ddi_add_intr(dip, 0, NULL, &idevice_cookie, foo_intr, (caddr_t)fsp);
other initialization (ddi_create_minor_node, etc.);
}
static uint
foo_intr(caddr_t arg)
{
struct foo_state *fsp = (struct foo_state *)arg;
read device register to determine if device is generating interrupt;
if (not my device interrupting)
return DDI_INTR_UNCLAIMED;
handle device interrupt;
write to device to tell it the interrupt has been handled;
return DDI_INTR_CLAIMED;
}
DMA
솔라리스와 리눅스는 둘다 DMA 플랫폼의 차이점을 숨기기 위해 데이타 추상화 레이어를 제공합니다. 범용적인 DMA 레이어는 리눅스 2.6의 새로운 기능 입니다. 여기서 우리는 솔라리스 드라이버에서 DMA 를 사용하는 법을 다룹니다. 알아두어야할 점은 디바이스가 SPARC에 있든 x86에 있든 코드는 완전히 동일해야 된다는 것입니다.
DMA를 위한 과정은 다음과 같습니다:
- 디바이스와 DMA-엔진-특수한 속성을 기술하는
ddi_dma_attr(9s)를 할당하고 초기화 함 - A
attach(9e)에서ddi_dma_alloc_handle(9f)를 이용해 DMA 핸들을 할당하거나 특수한 DMA 전송 작업을 하도록 프로그래밍 ddi_dma_addr_bind_handle(9f)혹은ddi_dma_buf_bind_handle(9f)를 이용하여 핸들에 어드레스를 바인드 시킴. 이러한 루틴은 디바이스가 DMA를 수행하기 위해 어드레스와 용량을 포함한 DMA "cookie"를 리턴(혹은 cookie들)- 디바이스가 DMA를 수행하도록 프로그래밍. 이것은 디바이스마다 다름
- 완료하기 위해
ddi_dma_sync(9f).를 호출하여 캐쉬를 싱크 시킴 - I/O를 완료하기 위해
ddi_dma_unbind_handle(9f)를 호출하여 DMA 리소스를 해제 시킴 - 핸들 처리를 완료하기 위해 (혹은
detach(9e)에서),ddi_dma_free_handle(9f)를 사용
PCI 디바이스의 예제를 보시기 바랍니다.
static ddi_dma_attr_t foo_dma_attr = {
DMA_ATTR_V0, /* version of this structure */
0, /* lowest usable address */
0xffffffffU, /* highest usable address */
0x0ffffff, /* maximum DMAable byte count */
1, /* alignment in bytes */
0x7f, /* bitmap of burst sizes */
1, /* minimum transfer */
0x0ffffffU, /* maximum transfer */
0x0ffffffU, /* maximum segment length */
1, /* maximum number of segments */
1, /* granularity */
0, /* flags (reserved) */
};
ddi_dma_handle_t foo_dma_handle; /* typically in per-instance state */
foo_attach(dev_info_t *dip, ddi_attach_cmd_t cmd))
{
...
if (ddi_dma_alloc_handle(dip, &foo_dma_attr,
DDI_DMA_SLEEP, 0, &foo_dma_handle) != DDI_SUCCESS)
return (DDI_FAILURE);
...
}
foo_startio(...) /* the routine to do the DMA */
{
ddi_dma_cookie_t dma_cookie;
uint32_t ncookies;
caddr_t addr; /* do dma to/from this address, */
/* possible from ddi_dma_mem_alloc(9F) */
size_t len; /* number of bytes to do dma to/from */
uint_t flags;
...
flags = DDI_DMA_RDWR | DDI_DMA_STREAMING; /* or DDI_DMA_CONSISTENT, etc. */
ddi_dma_addr_bind_handle(foo_dma_handle, 0, addr, len, flags,
DDI_DMA_DONTWAIT, NULL, &dma_cookie, &ncookies);
ddi_put32(foo_acc_handle, dev_regs+DMA_ADDR, dma_cookie.dmac_address);
ddi_put32(foo_acc_handle, dev_regs+DMA_SIZE, dma_cookie.dma_size);
ddi_put32(foo_acc_handle, dev_regs+CMD, GO);
}
완료 과정에서 드라이버 인터럽트 핸들러는 ddi_dma_unbind_handle(9f) 와 종종 ddi_dma_sync(9f)를 호출합니다. detach(9e) 루틴은 ddi_dma_free_handle(9f) 를 호출합니다.
타이밍과 타이머
솔라리스 타이밍은 실시간 클럭을 이용하여 프로세서 스피드 바운드 내에서 클럭 인터럽트를 생성합니다. 스케쥴링을 위해 각 10 밀리(ms)세컨드마다 동작합니다. 리눅스에서는 "tick"이라고 합니다. 주의할 점은 리눅스 2.6은 기본 설치 시 일초당 1000-tick을 사용 합니다. 반면 솔라리스나 다른 버전의 리눅스는 일초당 100-tick을 이용합니다. 솔라리스상의 유저-레벨 프로그램은 나노(nano)세컨드 단위의 클럭을 프로그램 할 수도 있습니다. 그러나 고-레벨 타이머를 이용하는 프로그램 인터페이스는 DDi/DKI에서 사용 가능하지 않습니다. 유저 레벨의 정보를 위해 clock_settime(3rt) 을 참조 하시고 솔라리스의 고레벨 타이밍에 대한 정보는 usr/src/uts/common/os/cyclic.c 에서 확인하시기 바랍니다.
또한 솔라리스에서는 /etc/system 파일에서 hz 나 클럭의 틱/초를 hires_tick 을 1로 설정하거나 원하는 시간으로 바꿈으로서 변경할 수 있습니다. 고해상도 클럭의 기본값(hires_hz)은 일초에 1000tick입니다. 다음의 예제를 확인 바랍니다.
set hires_tick=1
set hires_hz=10000 <--- 10000 ticks per second.
hires_hz 에 높은 값을 설정하는 것은 추천하지 않습니다. 위의 값으로 인해 clock() 루틴은 1초에 10000번 수행 되고 사용자가 원하거나 필요한 것 보다 더 높은 오버헤드를 가지게 될 것입니다. DDI/DKI에 시간과 관련된 루틴은 다음과 같습니다:
timeout_id_t timeout(void(* func)(void *), void *arg, clock_t ticks)는 클럭틱에 의해 지정된 시간에 따라 함수를 스케줄하게 됩니다. 이것은 "원-샷" 타이머 입니다. 다시한번 수행 하고자 한다면 사용자의func()에서timeout()을 다시 한번 호출합니다.clock_t untimeout(timeout_id_t id), 은 이전의timeout(9f)호출을 취소합니다. 락을 조심하시기 바랍니다. 메뉴얼 페이지에서 자세한 사항을 확인 하시기 바랍니다.void drv_usecwait(clock_t microsecs)는 밀리세컨드 동안 busy-wait을 하도록 합니다. 리얼 타임 클럭(10ms 클럭이 아닌) 은 마이크로(micro) 세컨드 단위를 사용합니다. 사용자는 이 spin 동안 선점 혹은 인터럽트되도록 할 수 있습니다. 만약 이것을 원하지 않는다면ddi_enter_critical(9f)/ddi_exit_critical(9f)를 대신 사용하시기 바랍니다.clock_t drv_usectohz(clock_t microsecs)는마이크로세컨드를클럭 틱으로 변환합니다. 클럭 틱을 요구하는timeout(9f)를 호출하기 전에 주어진 밀리세컨드 동안 몇번의 틱이 있었는지 확인 하는데 사용합니다.void delay(clock_t ticks)는 주어진 숫자 동안의 클럭 틱을 딜레이 하는데 사용합니다.
스택
커널은 각 쓰레드 단위로 스택 공간을 할당합니다. 각 커널 쓰레드는 x86에서 8192 bytes SPARC에서 24576 bytes를 할당받습니다. 추가적으로 REDZONE 페이지는 가상적으로 스택 오버 플로우의 가드로 활동하는 권한을 할당 받지 못합니다. 이것은 로컬 변수에 큰 배열을 할당할때 조심해야 함을 의미 합니다. 물론 높은 차원의 재귀 함수도 스택 오버 플로우 문제를 유발 할 수 있습니다. 만약 로컬 변수에 큰 배열을 할당하기 원한다면 kmem_alloc() 을 이용하여 배열을 힙에 할당할 것을 권장하고 리턴하기 전에 kmem_free() 을 호출해야 합니다.
오류 처리
일반적으로 드라이버 엔트리 포인트는 성공했을 때 DDI_SUCCESS (0) 를 리턴하고 실패했을 때 양수를 리턴(실제적으로 errno) 합니다. 리눅스에서는 음수 errno 값을 오류 지정값으로 사용합니다. errno 는 엔트리 포인트에서 멘 페이지에 기술된 errnos 와 일관성을 가져야 합니다. 예를 들어 드라이버 열기 루틴이 오류를 리턴해야 한다면 open(9e) 을 참고하여 가능한 오류 리턴 리스트를 참고해야 합니다.
오류 보고를 위해서 솔라리스 드라이버는 cmn_err(9f) 을 사용합니다. 리눅스에서 가장 많이 사용 되는 printk()의 용도와 비슷합니다. 오류를 다루는 방법은 이 글에서는 다루지 않습니다. 예를 들어 kmem_alloc(9f) 의 오류를 다루는 그 자체로 방법은 커다란 주제가 될 수 있습니다.
결론
솔라리스 드라이버 개발을 시작한다면 DDI와 DKI의 문서를 따르는 것이 가장 좋은 방법입니다. 물론 경계를 벗어나는 것이 필요 할 때도 있겠지만 정의된 인터페이스를 고수하면서 계속 개발을 해나갈 것을 권합니다.
"오픈솔라리스" 카테고리의 다른 글
- 오픈솔라리스의 빌드와 설치 (Part 2) (댓글 1개 / 트랙백 0개) 2006/03/23
- OpenSolaris 코드 브라우저를 이용하여 코드베이스 탐색하기 (댓글 1개 / 트랙백 1개) 2005/09/23
- 커널 프로그래머를 위한 에너지 스타 가이드라인 (댓글 1개 / 트랙백 0개) 2006/07/23
- 오픈솔라리스 domU 를 리눅스 dom0 에 설치하기 (댓글 0개 / 트랙백 0개) 2008/03/11
- Indiana 란 무엇이고 어떻게 참여할 수 있나요? (댓글 0개 / 트랙백 0개) 2008/04/17
- FAQ: 오픈솔라리스 ON(OS/Net) 번역 FAQ (댓글 0개 / 트랙백 0개) 2008/02/18
- 오픈 솔라리스를 위한 무선 네트워킹 (댓글 1개 / 트랙백 0개) 2006/01/23
- Xen: 다운로드, 설치 및 설정 정보 (댓글 0개 / 트랙백 0개) 2008/01/21
- FAQ: OpenSolaris.org (댓글 3개 / 트랙백 0개) 2006/09/23
- OpenSolaris 2008.05 발표 (댓글 0개 / 트랙백 0개) 2008/05/13
댓글을 달아 주세요
좋은 정보 감사해요~
2007/09/19 04:25