이달부터 3회에 걸쳐 연재할 내용은 특별한 하드웨어 장치 없이도 구현할 수 있는 RAM 디스크다. 이번 회에서는 드라이버에 대한 개념과 프로그래밍 개발환경, 드라이버 프로그래밍에 대한 기초적인 내용을 다루겠다.
장치 드라이버란 장치 드라이버는 ‘장치를 구동하는 프로그램’으로 줄여서 드라이버라고도 부른다. 드라이버는 하드웨어와 소프트웨어(운영체제) 중간에 위치하며, 프로그램 중에서 가장 저수준 레벨에서 처리하는 프로그램으로 볼 수 있다. 일반적으로 운영체제에서 많은 부분이 드라이버로 되어 있다고 봐도 될 것이다.
드라이버는 기본적으로 하드웨어를 제어하고 운영하는 프로그램으로 볼 수 있다. 하드웨어 자원인 메모리, I/O, 인터럽트, DMA를 처리해 사용자가 장치를 사용할 수 있게 한다. 그리고 일반적인 애플리케이션에서는 할 수 없는 일이나 운영체제의 기능을 확장할 때도 드라이버는 필요하다. 예를 들면 바이러스 백신, 보안을 위한 파일 암복호화나 네트워크 패킷 필터링을 하기 위해 드라이버를 이용하기도 한다.
드라이버는 운영체제와 밀접하게 연관되어 작동하기 때문에 운영체제에 따라 다르게 만들어진다. 즉, 윈도우·리눅스·Mac OS마다 드라이버를 새로 만들어야 한다. 윈도우에서도 윈도우 9x 계열(95·98·ME)과 윈도우 NT 계열(NT·2000·XP)은 드라이버를 다르게 만들어야 한다. 따라서 장치를 만드는 하드웨어 업체에서는 실제 소비자를 위해 모든 운영체제에 드라이버를 지원해야 하다보니 개발기간이 길어질 수밖에 없어진다.
윈도우 2000 드라이버의 종류 우리는 이번 연재에서 윈도우 2000용 드라이버를 만든다. 먼저 윈도우 2000에서는 어떤 종류의 드라이버가 있는지 알아보자.
<그림 1>은 윈도우 2000의 드라이버 종류를 나타낸 것이다. 각각의 드라이버가 어떤 것인지 간단히 설명해 보겠다.
◆ 가상 장치 드라이버 : x86 플랫폼에서 하드웨어를 액세스하는 DOS 기반 응용 프로그램을 돌아가게 하는 사용자 모드 요소다. 가상 장치 드라이버(VDD, Virtual Device Driver)는 윈도우 98의 VxD와는 다른 것이니 혼돈하지 말기 바란다.
◆ 커널 모드 드라이버 : 일반적인 장치 드라이버를 총칭한다고 보면 된다.
◆ WDM 드라이버 : 전력 관리와 PnP를 처리하는 커널 모드 드라이버다. 윈도우 98과 2000에서 소스 호환되어 드라이버 개발을 쉽게 할 수 있다. WDM 드라이버는 클래스 드라이버와 미니 드라이버로 나뉜다. 클래스 드라이버는 주로 장치를 클래스라는 종류로 나눠 처리하고, 미니 드라이버는 클래스 드라이버에 하드웨어 장치들마다 다르게 처리할 수 있는 도움을 제공한다.
◆ 비디오 드라이버 : 디스플레이와 프린터를 위한 드라이버다.
◆ 파일 시스템 드라이버 : 일반적인 로컬 하드 디스크나 네트워크로 연결된 파일 시스템을 처리한다.
◆ 레거시 드라이버 : 다른 드라이버의 도움 없이 하드웨어 장치를 직접 제어하는 커널 모드 드라이버다. 이 드라이버는 윈도우 2000에서도 작동하는 예전의 윈도우 NT 드라이버를 포함한다.
이번 연재에서는 윈도우 2000용 장치 드라이버에 중에서 WDM 드라이버에 대한 내용보다는 윈도우 NT 계열의 레거시 드라이버에 대한 내용을 다룰 것이다. WDM 드라이버도 레거시 드라이버 기반 위에 PnP와 전력 관리 부분이 추가된 것이기 때문에 기본적인 내용은 레거시 드라이버만 알아도 이해하는 데 충분하다.
윈도우 2000의 내부구조 장치 드라이버는 운영체제와 밀접하게 연관되어 있다고 말했다. 따라서 운영체제 내부를 알고 있어야 드라이버를 개발하는 데 필요한 내용을 이해하기 더 쉽다. 지금부터 윈도우 2000의 전체적인 구조를 살펴보자. 윈도우 2000의 내부구조를 보면, <그림 2>와 같다.
윈도우의 내부는 크게 커널 모드와 사용자 모드로 나눌 수 있다.
◆ 커널 모드 : 프로세서의 특권 레벨(privileged level)로 프로세서의 모든 명령을 처리할 수 있고, 시스템 자원이나 하드웨어들을 직접 액세스할 수 있다. 장치 드라이버나 운영체제의 코드들이 작동하는 모드이기도 하다.
◆ 사용자 모드 : 일반 응용 프로그램이 동작하는 모드로 커널 모드와는 달리 비특권 레벨(nonprivileged level)이기 때문에 하드웨어나 시스템 자원을 직접 이용할 수 없고 시스템 서비스(API)를 이용해 접근해야 한다. 그렇지 않으면 예외(exception)가 발생해 프로그램이 종료한다.
커널 모드는 다시 Executive, 커널, HAL(Hardware Abstraction Layer) 부분으로 나눌 수 있다.
◆ Executive : 윈도우의 기본이 되는 서비스들을 제공한다. 메모리·프로세스·쓰레드를 관리한다. 특히, Executive를 구성하는 것 중에서 I/O 관리자는 장치 드라이버와 관련해서 많은 부분을 처리한다. 그리고 나머지 구성요소도 드라이버에서 사용하는 서비스를 많이 가지고 있다.
◆ 커널 : 주로 저수준 운영체제 기능을 제공한다. 쓰레드 스케쥴링, 인터럽트와 예외 처리, 멀티 프로세서 동기화 등을 맡는다.
◆ HAL : 하드웨어 추상화 계층으로 플랫폼이 어떤 것이든지 상관없이 운영체계가 작동할 수 있게 한다. 이 계층이 있기 때문에 윈도우 NT 계열 운영체제가 멀티 플랫폼을 지원할 수 있게 된다.
장치 드라이버 개발환경 기본적으로 다음의 개발도구들로 개발한다.
◆ 비주얼 C++ 6.0 : 비주얼 C++의 개발 환경을 이용하기 위해 설치하는 것이 아니라 C 컴파일러나 링커를 이용하기 위해 설치한다.
◆ DDK(Driver Development Kit) : 장치 드라이버 개발에 필수적인 툴로 드라이버 개발에 관련된 라이브러리, 헤더 파일, 예제, 문서(도움말도 포함), 개발에 필요한 프로그램 등이 포함되어 있다. DDK는 OS 버전마다 나오기 때문에 개발하려는 OS에 맞는 DDK를 설치해 개발한다. 우리는 이번 연재에서 윈도우 2000용 드라이버를 만들기 때문에 윈도우 2000 DDK를 설치한다. http://www.microsoft.com/ddk에서 내려받을 수 있다(<화면 1>).
◆ 디버거(WinDbg, Soft-ice) : WinDbg는 DDK에 포함되어 있어서 무료로 사용할 수 있다(http://www.microsoft.com/ddk/debugging/). Soft-ice는 컴퓨웨어 누메가(Compuware Numega)에서 나오는 드라이버스튜디오(DriverStudio)에 포함되어 있다.
◆ 에디터 : 소스를 편집할 에디터가 필요하다.
이제 개발환경이 갖춰졌으면, 실제 드라이버 개발에 필요한 내용으로 들어가자.
|
|
|
IRQL이란
IRQL(Interrupt Request Levels)은 윈도우 NT부터 나온 개념으로 단일 CPU에서 동기화를 위한 방법으로 사용하고 있다. IRQL은 0~31까지의 값으로 할당한다. 값이 클수록 높은 레벨을 나타내며 현재 실행되고 있는 플랫폼에 따라서 값들이 정해진다. IRQL은 현재 CPU가 실행되고 있는 레벨이 PASSIVE_LEVEL이라면, 그보다 더 높은 레벨의 IRQL만이 인터럽트될 수 있는 규칙을 가진다. 하드웨어 IRQL은 DIRQL(Device IRQL)로 소프트웨어는 로우 레벨 IRQL(PASSIVE_LEVEL=0, APC_LEVEL=1, DISPATCH_LEVEL=2)로 구분할 수 있다.
소프트웨어 IRQL 레벨이 드라이버와 관련이 있기 때문에 그 부분들에 대해 간단히 알아보면 다음과 같다.
◆ DPC 레벨 : 쓰레드 스케쥴링과 DPC(Deferred Procedure Call) 실행 레벨 ◆ APC 레벨 : APC(Asynchronous Procedure Call) 레벨 ◆ PASSIVE 레벨 : 일반적인 쓰레드의 실행 레벨
|
|
|
|
장치 드라이버 기초 프로그래밍 커널 모드 드라이버는 응용 프로그램과는 많은 부분에서 다르게 작동한다. 간단하게 살펴보면 드라이버는 운영체제(I/O 관리자)에 의해 호출 받는 루틴의 집합이라고 볼 수 있다. 드라이버의 루틴은 I/O 관리자에 의해 호출 받을 때까지 기다린다. I/O 관리자는 주로 다음과 같은 상황에서 드라이버의 루틴을 호출한다.
◆ 드라이버가 로드될 때(DriverEntry 루틴) ◆ 드라이버가 언로드될 때와 시스템이 셧다운될 때 ◆ 응용 프로그램이 I/O 시스템 서비스를 호출했을 때 ◆ 공유하는 하드웨어 자원이 드라이버에서 사용될 때 ◆ 실제 장치가 동작하는 동안에 다양한 곳에서
앞의 상황 중에서 드라이버가 기본적으로 관심을 가져야 할 상황에 처리해야 할 루틴들을 설명하겠다.
DriverEntry 루틴 모든 프로그램은 제일 처음 실행되는 부분이 있어야 한다. C로 프로그램을 짜면 처음 호출을 받아 실행되는 부분이 main 함수다. 드라이버에서도 main 함수처럼 가장 처음 호출을 받는 부분이 DriverEntry가 된다. DriverEntry는 I/O 관리자에 의해 드라이버 로딩시에 호출된다. DriverEntry 루틴에서 기본적으로 처리해야 할 몇 가지 내용을 알아보자.
① I/O 요청을 처리할 장치 객체 생성 ② 처리할 IRP(I/O Request Packet)의 MajorFunction에 해당하는 디스패치 루틴들을 등록 ③ 응용 프로그램에서 읽기와 쓰기시에 메모리 전략 선택 ④ Win32 응용 프로그램이 드라이버로 접근하기 위해 Win32 서브시스템에 심볼릭 링크 생성
<리스트 1> DriverEntry 루틴 NTSTATUS DriverEntry ( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { ......
status = IoCreateDevice ( DriverObject, sizeof(MYDE), → ① &uniNtNameString, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject); ...... ...... if ( NT_SUCCESS(status) ) { DriverObject->MajorFunction[IRP_MJ_CREATE] = SAMPLECreate; → ②
// MajorFunction에 디스패치 루틴 등록 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=SAMPLEDeviceControl;
deviceObject->Flags |= DO_BUFFERED_IO; → ③ ...... status = IoCreateSymbolicLink( &uniWin32NameString, → ④ &uniNtNameString ); }
// 인터럽트 사용시 DPC 초기화 및 사용 객체 초기화 및 메모리 사용을 위함 // 메모리 할당
return status; }
언로드 루틴 일반적으로 드라이버는 로드된 후 시스템이 리부트하기 전까지는 남아 있다. 그러나 드라이버를 사용하다가 언로드해야 할 경우엔 언로드 루틴이 필요하며 DriverEntry에서 언로드 루틴을 등록해야 한다. 언로드 루틴은 I/O 관리자가 드라이버를 메모리에서 제거하기 전에 호출된다. 언로드 루틴에서 기본적으로 처리해야 할 몇 가지 내용을 살펴보자.
◆ 심볼릭 링크를 제거한다. ◆ DeviceObject를 제거한다. VOID Sample1UnLoad( IN PDRIVER_OBJECT DriverObject )
{ ...... // 장치 객체와의 심볼릭 링크를 해제한다. IoDeleteSymbolicLink( &uniWin32NameString ); IoDeleteDevice( DriverObject->DeviceObject ); }
디스패치 루틴 윈도우 2000에서 I/O는 패킷 드리븐 방식으로 이루어진다. I/O 요청이 있을 때 먼저 I/O 관리자가 그 요청에 해당하는 내용을 가지고 IRP를 만든다. I/O 관리자는 응용 프로그램의 요청(읽기, 쓰기 등)을 받았을 때, 그 요청에 맞는 함수 코드로 바꾼다. 그리고 처리할 요청에 대한 드라이버를 선택하고 적절한 드라이버 내의 디스패치 루틴을 호출한다.
디스패치 루틴은 요청한 내용을 보고 알맞은 처리를 한 후 결과를 I/O 관리자에 반환한다. <표 1>은 응용 프로그램에서 호출하는 함수와 대응되는 드라이버 내의 주요 함수 코드들을 보여준다.
|
Win32 API |
함수 코드 |
|
|
CreateFile |
IRP_MJ_CREATE |
|
|
|
CloseHandle |
IRP_MJ_CLOSE IRP_MJ_CLEANUP |
|
|
|
ReadFile |
IRP_MJ_READ |
|
|
|
WriteFile |
IRP_MJ_WRITE |
|
|
|
DeviceIoControl |
IRP_MJ_DEVICE_CONTROL |
|
|
<표 1> 응용 프로그램에서 호출하는 함수와 대응되는 드라이버 내의 주요 함수 코드
디스패치 루틴에서 기본적으로 처리해야 할 몇 가지 내용은 다음과 같다.
① 드라이버와 관련 있는 IRP 스택 위치에 포인터를 얻기 위해 IoGetCurrentIrpStackLocation 함수를 호출한다.
② I/O 요청에 대한 매개변수들을 가져온다.
③ IoStatus에 반환할 값들을 IRP에 채운다. Status에는 에러 코드를 채우고, Information에는 적절한 값을 채운다. 일반적으로 읽기시에는 응용 프로그램으로 복사할 데이터 크기를 알려준다.
④ I/O 요청에 대한 모든 처리를 끝내고, IRP를 더 이상 사용하지 않기 위해 IoCompleteRequest를 호출한다.
NTSTATUS Sample1Read( IN PDEVICE_OBJECT pDeviceObject, IN PIRP Irp ) { ...... // 현재 StackLoacation을 구한다. pCurrentStack = IoGetCurrentIrpStackLocation( Irp ); → ①
// 읽을 바이트 수 usize = pCurrentStack->Parameters.Read.Length; → ② ...... Irp->IoStatus.Status = STATUS_SUCCESS; → ③ Irp->IoStatus.Information = usize;
IoCompleteRequest( Irp, IO_NO_INCREMENT ); → ④ return STATUS_SUCCESS; }
앞서 설명한 루틴들은 일반적인 드라이버에서 기본적으로 쓰이는 것들이다. 이외에도 다른 루틴들도 있지만 나머지 루틴들은 다음 기회에 설명하겠다.
장치 드라이버에는 많은 자료구조가 나온다. 장치 드라이버를 공부하는 데 기본적으로 알아야 할 자료들은 DriverObject, DeviceObject, IRP가 있다. 이런 자료구조들을 아는 것이 드라이버를 이해하는 데 밑거름이 되기 때문이다. 그 중에서 이번 회에는 IRP에 대한 내용을 살펴보겠다.
|
|
|
DriverObject와 DeviceObject의 연관 관계
직렬 드라이버를 예로 들어 DriverObject와 DeviceObect의 연관 관계를 잠시 설명해보겠다. 우리가 직렬 드라이버를 만든다고 가정해 보자. 다들 알다시피 직렬 드라이버는 COM1, COM2, COM3 등의 이름을 가지고 응용 프로그램에서 접근한다. 그러나 실제로 윈도우 2000에는 COM 개수에 따라 직렬 드라이버가 각각 다르게 존재하는 것이 아니다. 하나의 드라이버가 여러 개의 COM 포트를 관리하고 처리한다. 즉, 드라이버에서는 DriverObject가 하나 존재하고, 대신 COM 포트 개수만큼 DeviceObject를 생성하는 것이다. DriverObject가 같다는 것은 IRP 처리 루틴을 같이 공유한다는 뜻이다.
예를 들어 응용 프로그램에서 COM1을 읽는 명령이 내려왔다고 가정해 보자. Win32 서브시스템(API)을 통해 커널 모드로 진입할 것이다. 그리고 명령은 I/O 관리자에 전달될 것이며, I/O 관리자는 해당 명령을 실행할 수 있는 IRP를 생성해 해당 드라이버, 즉 직렬 드라이버에 전달한다. 그리고 DriverObject의 MajorFunction에 해당하는 디스패치 루틴 함수로 분기해 처리하고, 처리 결과를 I/O 관리자에 돌려준다. 여기서 읽기 처리 루틴이 다음과 같다고 하자.
Read(…) { ...... // 선행 처리 과정 data = InPort ( 0x378 ); ...... return data; }
이와 같이 코딩되어 있다면 읽기 디스패치 루틴 실행시 0x378(COM1 포트의 주소) 값을 읽어올 것이고 응용 프로그램은 원하던 결과를 얻을 것이다. 그렇다면 응용 프로그램이 COM2 또는 COM3의 값을 읽게 하는 명령이 직렬 드라이버에 내려온다면 어떻게 될까?
그럼 COM 포트들은 같은 DriverObject를 사용하기 때문에 IRP에 관한 루틴을 공유한다. 그럼 앞과 같은 루틴이라면 COM2, COM3 포트의 값을 읽었을 때도 COM1의 값이 얻어질 것이다. 이런 문제를 해결하기 위해 DeviceExtension을 사용한다. DeviceExtension은 DeviceObject만을 위한 메모리 공간이다. 문제가 되던 data = InPort( 0x378 );을 다음과 같이 바꾸면 될 것이다.
data = InPort( DeviceObject->DeviceExtension->Port );
DeviceExtension에는 Port라는 변수가 있을 것이고, 그 변수에는 각 포트 주소가 기억되어 있다. 이와 같이 처리하면, 읽기 디스패치 루틴을 공유해도 각각의 포트 값을 읽어 올 수 있다. |
|
|
|
IRP 패킷 드리븐 방식으로 I/O 요청을 처리하는 윈도우 NT 계열 운영체제는 처리해야 할 I/O 요청을 IRP를 이용해 처리한다. IRP 자료구조는 다음과 같이 크게 두 부분으로 나눌 수 있다.
◆ 헤더 : I/O 요청에 대한 다양한 정보가 있다. 요청한 I/O의 타입, 크기, 상태 등을 저장한다. buffered I/O를 할 경우에 Associatedirp에 버퍼 포인터, 다이렉트 I/O를 할 경우엔 MdlAddress에 MDL 포인터 관련 정보를 가지고 있다.
◆ I/O 스택 위치 : 함수 코드와 매개변수들을 담고 있다. I/O 스택은 I/O 요청이 어떤 처리를 하느냐에 따라 스택 크기가 결정된다.
예를 들어 플로피 디스크에 파일을 쓰는 I/O 요청이 있으면 적어도 스택 크기는 두 개가 될 것이다. 하나는 파일 시스템 드라이버를 위한 것이고, 다른 하나는 플로피 디스크 드라이버를 위한 것이기 때문이다.
IRP 버퍼 관리 사용자 응용 프로그램들이 읽기나 쓰기 같은 요청을 했을 경우 사용자 쪽의 데이터 버퍼를 처리하는 방법에 따라서 다음과 같이 분류할 수 있다.
◆ Buffered I/O : I/O 관리자는 먼저 호출한 응용 프로그램의 사용자 버퍼와 같은 크기의 Non-paged 풀(pool) 버퍼를 할당한다. 응용 프로그램에서 쓰기시 I/O 관리자는 IRP를 만들 때 할당한 버퍼로 호출한 사용자 버퍼 데이터를 복사한다. 읽기시에 I/O 관리자는 IRP가 끝났을 때 I/O 관리자가 할당한 버퍼에서 사용자 버퍼로 복사한다. 그리고 I/O 관리자가 할당한 버퍼는 해제한다.
◆ 다이렉트 I/O : I/O 관리자는 먼저 사용자 버퍼에 해당하는 물리 메모리 페이지를 잠근다(lock). 그리고 잠근 페이지에 대한 내용을 설명하기 위한 MDL을 만든다. MDL에는 버퍼에 의해 할당한 물리적인 메모리를 지정한다. 그리고 만약 드라이버가 버퍼의 내용을 접근하려고 하면, 시스템 주소 공간으로 버퍼를 맵해 사용할 수 있다.
◆ Neither I/O : I/O 관리자는 어떤 버퍼 관리도 처리하지 않는다. 대신 버퍼 관리는 장치 드라이버의 discreation으로 남겨지고, 장치 드라이버는 I/O 관리자가 다른 버퍼 관리 타입에서 처리하는 과정을 손수 처리할 수 있도록 선택할 수 있다.
꼭 그런 것은 아니지만 일반적으로 드라이버는 호출한 부분의 데이터의 양이 한 페이지(4KB)보다 작으면 Buffered I/O 방식을 사용하고, 그것보다 클 경우엔 다이렉트 I/O 방식을 사용한다. 그리고 파일 시스템 드라이버는 Neither I/O를 주로 사용한다. 데이터를 파일 시스템 캐시에서 사용자의 버퍼로 복사할 때 버퍼 처리에 오버헤드가 없기 때문이다.
드라이버 컴파일과 실행 드라이버를 만들기 위해서는 다음 파일들이 필요하다.
◆ Makefile : DDK에서 사용하는 실제 makefile을 호출하는 내용이 있다. 내용을 수정할 필요는 없다. 일반적으로 DDK 샘플에 있는 파일을 가져다 쓴다. ◆ Sources : 드라이버 컴파일 환경 및 관련 옵션을 정해준다. ◆ 드라이버 소스 파일 : 장치 드라이버 실제 소스 ◆ RC(resource) 파일 : 드라이버 버전 정보를 담은 파일
컴파일 이 파일들이 구성됐으면 DDK에 있는 개발환경 Command 창을 띄운다(<화면 2>).
① 빌드 명령을 내린다. 그리고 나면 원하는 폴더에 *.SYS 파일이 생성된다. 그 파일을 \WINNT\System32\Drivers 폴더에 복사한다.
② 드라이버를 로드하려면 레지스트리에 정보를 기록해야 한다. 레지스트리에 직접 기록하든지, 아니면 간단한 *.reg 파일을 생성해 설치해도 된다. 레지스트리에 들어가는 키 위치를 보면 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services 밑에 서브키를 생성한다. 키 이름은 드라이버 이름과 같은 이름으로 생성한다. 예를 들면 드라이버 이름이 SAMPLE.sys라면, SAMPLE이라는 키를 만든다. 키가 만들어 졌으면, 이제 키 밑에 값들을 적는다.
Start = 3 ErrorControl = 1 Type = 1
이 값은 드라이버가 윈도우가 시작되면서 올라오는 게 아니고 우리가 언제라도 올리고 내릴 수 있는, 즉 동적으로 작동하기 위해 정한 값이다.
③ 재부팅한다.
장치 드라이버 실행 Command 창을 하나 띄운다. 그리고 net start sample1을 실행한다. ‘서비스를 시작합니다’라는 메시지가 나오면 드라이버가 제대로 로드됐다는 것을 알 수 있다(<화면 3>). 드라이버를 내리고 싶다면 net stop sample1을 실행한다(<화면 4>).
‘이달의 디스켓’에 수록된 CallApp1 프로그램은 드라이버와 응용 프로그램이 통신하는 예를 보여준다. MFC를 이용해 만들었으며 프로그램이 하는 내용은 스피커포트(0x61)에 데이터를 읽고 쓰기 위해 드라이버와 통신하고, 명령에 따라 드라이버에서는 소리를 나게 하거나 나지 않게 처리한다. 컴파일 방법이나 기타 내용은 ‘이달의 디스켓’ 소스를 참고하기 바란다.
다음달에는 RAM 디스크 소스 분석과 제작을 이번 강좌는 첫 연재로 드라이버를 개발하기 위한 기본적인 내용을 다루려고 노력했다. 장치 드라이버의 기초적인 부분들을 다루다 보니 너무 간단한 설명만 있어 깊이 있는 내용을 원하는 독자들에게는 불만이 될 듯하기도 하지만 남아 있는 연재 기간 동안 불충분한 내용은 채워나가겠다. 내용 중 잘못된 부분이 있거나 원하는 내용이 있으면 필자에게 메일을 주기 바란다. 다음 호에 글을 쓰는 데 도움이 될 것 같다. 다음 호에는 이번 호의 내용을 바탕으로 실제 RAM 디스크 소스 코드를 설명하면서 RAM 디스크를 이해하고 만들어 보겠다. @
|