디바이스 드라이버에는 크게 아래와 같이 분류됩니다.

  • 문자 디바이스 드라이버

    대표적인 예가 시리얼 포트를 이용한 통신 디바이스 드라이버입니다. 시리얼 통신에는 자료를 서로 문자 단위로 주고 받습니다.

  • 블록 디바이스 드라이버

    자료를 버퍼를 이용하여 블록단위로 주고 받는 장치에 사용되는 디바이스 드라이버로 하드디스크가 좋은 예입니다.

  • 네트워크 디바이스 드라이버

    네트워크 층과 연결되고 사용하는 디바이스 드라이버입니다.

이렇게 3가지로 분류되었습니다만 실제 블록과 네트워크 쪽은 일부 전문적인 곳에서만 작성되고 실제로는 작성되는 경우는 대부분 문자 디바이스라고 생각됩니다.

이번 시간부터 디바이스 드라이버에 대해 예제를 들어 가면서 작성하려 합니다. 첫번째 예제는 디바이스 드라이버를 어떻게 작성하는지 전체적으로 알아 볼 수 있도록 짧은 예제를 준비했습니다만, 앞으로 강좌를 진행하면서 EZ-ETIO를 이용하여 하드웨어적으로 처리하는 방법에 대해서도 올리려고 하겠습니다.

struct file_operations

이전 모듈 프로그래밍에서 module_init()와 module_exit()를 이용하여 모듈을 커널에 등록과 해제를 위한 처리를 어떻게 할지에 대한 말씀을 드렸습니다.

이제 이 모듈이 커널에 등록이 되고(insmod) 가상 파일 시스템으로 지정된 장치명으로 등록이 되면(mknod) 일반 프로그램에서는 이 장치명을 이용하여 해당 장치명을 사용하기 위해서 open(), read(), write(), close() 뿐만 아니라 seek(), flush() 등의 함수를 사용할 것입니다.

그러므로 커널 등록뿐만 아니라 사용자가 요구하는 함수에 대해 처리할 수 있는 함수를 준비해야 겠습니다. 이를 위한 struct 가 바로 struct file_operations 입니다.

struct file_operations 은 (uname -r)-i686/include/linux 안에 있는 fs.h 에 있습니다.

/usr/src/kernels/2.6.9-42.0.8.EL-smp-i686/include/linux/fs.h

struct file_operations {
   struct module *owner;
   loff_t (*llseek) (struct file *, loff_t, int);
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
   ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
   ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
   int (*readdir) (struct file *, void *, filldir_t);
   unsigned int (*poll) (struct file *, struct poll_table_struct *);
   int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
   int (*mmap) (struct file *, struct vm_area_struct *);
   int (*open) (struct inode *, struct file *);
   int (*flush) (struct file *);
   int (*release) (struct inode *, struct file *);
   int (*fsync) (struct file *, struct dentry *, int datasync);
   int (*aio_fsync) (struct kiocb *, int datasync);
   int (*fasync) (int, struct file *, int);
   int (*lock) (struct file *, int, struct file_lock *);
   ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
   ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
   ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
   ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
   unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
   int (*check_flags)(int);
   int (*dir_notify)(struct file *filp, unsigned long arg);
   int (*flock) (struct file *, int, struct file_lock *);
};

이 스트럭쳐를 보시면 장치를 위한 함수 포인터를 지정해 줄 수 있습니다. 만들려는 디바이스 드라이버에 대해서 지원하고자 하는 함수를 작성해서 함수 포인터에 지정해 주면 됩니다.

즉, 장치를 사용하기 위해 open() 함수를 지원하기 위해서는

// Open 함수를 작성한 후


static int my_open( struct inode *inode, struct file *filp ) 
{
  printk( "[VB] openedn" );
  return 0;
}

// struct file_operations 요소 중 .open에 함수를 지정해 줍니다.

static struct file_operations vd_fops = {
  .open = my_open,
};

register_chrdev, unregister_chrdev

이렇게 open부터 read(), write(), release() 등 필요한 함수를 작성하고 지정한 후, 이 struct file_operations에 지정된 정보와 함께 문자 디바이스 드라이버로 등록하기 위해 register_chrdev()함수를 호출합니다.

물론 모듈을 초기화하는 루틴에서 사용됩니다.

int __init my_init( void )
{
  register_chrdev( MAJOR_NUMBER, "virtual_buffer", &vd_fops );
  
  ......... 기타 필요한 초기화 작업 .............
  
  return 0;
}

모듈을 커널에서 제거하게 되면 unregister_chrdev() 를 이용하여 커널에서 디바이스 드라이버의 정보를 삭제합니다.

void __exit my_exit( void )
{
  unregister_chrdev( MAJOR_NUMBER, "virtual_buffer" );
  kfree( buffer );
}

/dev 에 노드 등록장치명)

디바이스 드라이버 프로그램을 작성하고 컴파일을 한 후에 성공적으로 디바이스 드라이버 파일을 만드셨다면 일반 프로그램에서는 그 장치를 마치 파일처럼 다루기 위해서 즉, 가상 파일 시스템으로 접급할 수 있도록 /dev 안에 노드로 등록해야 합니다.

컴파일 결과로 만들어진 디바이스 드라이버 파일 이름이 virtual_buffer.ko 이라면 mknod(Make Node)로 /dev 에 노드로 등록해야 합니다.

]# mknod /dev/virtual_buffer c 250 0

디바이스 드라이버를 커널에 등록

디바이스 드라이버는 커널 안에 실행되는 프로그램입니다. 커널과 함께 소스 컴파일했다면 이 단계는 필요 없겠지만 모듈로 작성했다면 insnod( Insert Module) 커널에 등록해야 합니다.

]# insmod virtual_buffer.ko

왜 이렇게 복잡한거야?

소스를 전체적으로 보면 그리 복잡하지 않습니다만 위의 글들을 읽어 보시면 왠지 복잡해 보입니다. 저도 그렇게 생각했으니까요. 작업을 정리해 보면,

  1. 모듈 프로그램을 작성하고 컴파일해서 디바이스 드라이버 파일을 생성합니다. (virtual_buffer.ko)
  2. mknod 를 이용하여 /dev 안에 노드를 생성
  3. insmod 를 이용하여 디바이스 드라이버를 커널에 등록

이렇게 생성하고 등록하면서도 프로그램 내에서 또 register_chrdev( MAJOR_NUMBER, "virtual_buffer", &vd_fops ) 를 호출해 주어야 합니다.

register_chrdev()를 호출하는 이유야 디바이스 드라이버에서 제공하는 기능, 즉 함수를 지정하기 위해서 어쩔 수 없다고 하지만 장지명과 문자 디바이스라는 것을 다시 확인해 주어야 하고 다시 모듈을 제거할 때면 다시 unregister_chrdev()를 호출해 주어야 합니다.

지금까지 윈도즈 프로그램만 작성해서 이렇게 생각하는 것인지 모르겠습니다만 MS 윈도즈의 regsvr처럼 왜 한번에 처리하지 않을까 하는 생각이 들더군요.

즉, 제가 궁금한 것은 모듈을 초기화하는 부분에서 함수 포인터를 지정하는 vd_fops 처리뿐만 아니라 /dev/ 안에 노드를 생성하는 코드까지 넣으면 간단하게 insmod 만 사용하면 모든 작업이 끝날 텐데 왜 이렇게 단계를 나누었을까 하는 점이죠.

]# insmod virtual_buffer.ko

왜 일까요? 저도 아직까지 그 이유를 모르겠습니다. ^^;

예상하기에는 작업의 편리나 효율성을 높이기 위함이 아닌겠는가 하는 생각이, 그것도 어렴풋이 듭니다.

그런데, 왜 알지도 못하는 내용을 가지고 이렇게 잡설을 하듯 말을 하느냐하고 물으신다면, 디바이스 모듈을 개발하는 순서를 다시 한번 정리하듯 말씀 드리고 싶어서 였습니다.^^

예제 프로그램

예제 프로그램은 리눅스 커널 프로그래밍(저자 한동훈) 책 5장에 소개된 예제를 바탕으로 조금 수정한 예제입니다. 이 디바이스 드라이버는 직접 제어하는 하드웨어는 없지만 커널에 버퍼를 생성하고 마치 가상의 장치로부터 자료를 읽거나 쓰기를 할 수 있습니다.

전체 소스입니다.

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

#define  BUFF_SIZE      1024
#define  MAJOR_NUMBER   250

static char *buffer  = NULL;
static int   sz_data = 0;

static int my_open( struct inode *inode, struct file *filp ) 
{
  printk( "[VB] openedn" );
  return 0;
}

static int my_release( struct inode *inode, struct file *filp )
{
  printk( "[VB] releasedn" );
  return 0;
}

static ssize_t my_write( struct file *filp, const char *buf, size_t count, loff_t *f_pos )
{
   printk( "[VB] write to buffern");

   if (BUFF_SIZE < count)  sz_data  = BUFF_SIZE;
                           sz_data  = count;

   strncpy( buffer, buf, sz_data);
   return count;
}

static ssize_t my_read( struct file *filp, char *buf, size_t count, loff_t *f_pos )
{
  printk( "[VB] read from buffern" );
  copy_to_user( buf, buffer, sz_data);

  return sz_data;
}

static struct file_operations vd_fops = {
  .read = my_read,
  .write = my_write,
  .open = my_open,
  .release = my_release
};

int __init my_init( void )
{
  register_chrdev( MAJOR_NUMBER, "virtual_buffer", &vd_fops );
  buffer = (char*) kmalloc( BUFF_SIZE, GFP_KERNEL );
  memset( buffer, 0, BUFF_SIZE);

  printk( "[VB] initializedn");

  return 0;
}

void __exit my_exit( void )
{
  unregister_chrdev( MAJOR_NUMBER, "virtual_buffer" );
  kfree( buffer );
  printk( "[VB] exitedn");
}

module_init( my_init );
module_exit( my_exit );

MODULE_LICENSE( "GPL" );

Makefile 은 아래와 같습니다. 컴파일이 성공적으로 수행이되면 virtual_buffer.ko 파일이 생성됩니다.

KERNELDIR =	/lib/modules/$(shell	uname	-r)/build

obj-m	:= virtual_buffer.o

KDIR	:=	/lib/modules/$(shell	uname	-r)/build
PWD	:=	$(shell pwd)

default:
	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

clean:
	rm	-rf *.ko
	rm	-rf *.mod.*
	rm	-rf .*.cmd
	rm	-rf *.o
	rm	-rf .tmp_versions

예제 테스트

우선 디바이스 드라이버를 /dev와 커널에 등록하겠습니다. 실행하는 순서와는 관계없습니다.

]# mknod /dev/virtual_buffer c 250 0
]# insmod virtual_buffer.ko
[VB] initialized

mknod의 인수는 아래와 같습니다.

명령 노드명 디바이스 타입 주 번호 부 번호
mknod /dev/virtual_buffer c 250 0
    문자 디바이스    

/dev/에 등록된 장치에 대해서는 가상 파일 시스템 지원으로 장치를 파일로 처리할 수 있습니다. 그러므로 디바이스 드라이버를 만들면 파일처럼 처리해서 기타 프로그램 작성없이 디바이스 드라이버를 테스트할 수 있습니다.

가장 간단한 방법은 cat 를 이용하는 것입니다.

]# cat > /dev/virtual_buffer

즉, 키보드로 타입프한 문자열을 /dev/virtual_buffer로 출력하겠다는 것입니다.

주의 사항 테스트하실 때에는 반드시 /dev/ 이렇게 노드를 모아둔 디렉토리명을 잊지 마십시오.
/dev/ 디렉토리 안에서 테스트하면 관계 없습니다만 다른 디렉토리에서 아래와 같이 테스트하시면 디바이스 드라이버를 테스트하는 것이 아니라 virtual_buffer 이라는 파일을 생성하고 엉뚱하게 virtual_buffer 파일과 테스트하는 꼴이 됩니다.

]# cat > virtual_buffer

자, 주의 사항에 주의하시면서 테스트해 보겠습니다. 주황색은 키보드로 입력해 달라는 것입니다. ^^

]# cat > /dev/virtual_buffer
[VB] opened
12345{enter}
{ctrl-D}
[VB] write to buffer
[VB] released
]#

이제 읽어 보겠습니다.

]# cat /dev/virtual_buffer
[VB] opened
[VB] readfrom buffer
12345
[VB] readfrom buffer
12345
[VB] readfrom buffer
12345
[VB] readfrom buffer
12345
[VB] readfrom buffer
12345
{ctrl-C}
[VB] readfrom buffer
[VB] released
]#

cat 로 /dev/virtual_buffer의 내용을 출력하게 되면, /dev/virtual_buffer에서 eof 를 주지 않기 때문에 계속 읽기를 수행하게 됩니다. 그래서 계속 버퍼의 내용을 출력하게 되므로 ctrl-C로 중단하셔야 합니다.

일반 프로그램으로 테스트

#include <stdio.h>
#include <string.h>  // strlen()
#include <unistd.h>  // read, write
#include <fcntl.h>   // open, close, O_RDWR, O_NONBLOCK

#define  BUFF_SIZE   1024

int   main()
{
   int   fd;
   char *str_hello   = "Hello, Virtual Buffer!!";
   char  buff[BUFF_SIZE];

   if ( 0  < ( fd = open( "/dev/virtual_buffer", O_RDWR)))
   {   
      write( fd, str_hello, strlen( str_hello)+1); // +1: NULL까지 포함되도록

      read(  fd, buff, BUFF_SIZE);
      printf( "%sn", buff);

      close( fd);
   }

   return 0;
}

프로그램을 보시면 /dev/virtual_buffer을 open()과 read() wirte()를 이용해서 마치 파일을 다루듯이 처리한 것을 보실 수 있습니다.

이 파일을 컴파일해서 실행해 보겠습니다.

]# ./a.out
[VB] opened
[VB] write to buffer
[VB] read from buffer
Hello, Virtual Buffer!!
[VB] released
]#

태그: *디바이스드라이버