// 커널 2.4
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char *, 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 (*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 (*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 (*dmapi_map_event) (struct file *, struct vm_area_struct *, unsigned int);
};

// 커널 2.6
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 (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	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 (*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 *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
};

  주번호

리눅스에서 주번호(Major Number)는 디바이스를 구분하기 위해 사용된다. 첫 번째 IDE 하드디스크는 /dev/hdaXX로 표시되고, 주번호는 3번이다. tty 디바이스는 /dev/ttyXX로 표시되며, 주번호는 4번이다.

주번호가 다르다는 것은 물리적인 디바이스도 다르고, 사용되는 디바이스 드라이버도 다르다는 것을 의미한다.

커널 2.4에서는 주번호에 8 비트, 부번호에 8 비트를 사용하기 때문에 최대 256개의 주번호를 사용할 수 있다. 2.4 커널에서 주번호와 부번호는 kdev_t 형식에 저장된다.

typedef unsigned short kdev_t;

커널 2.6에서는 주번호 12 비트, 부번호 20 비트로 확장하여, 총 4096개의 주번호를 사용한다. 2.4 커널은 디바이스 번호에 16 비트 데이터형을 사용했으며, 2.6 커널은 디바이스 번호에 32비트 데이터형을 사용한다.

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

참고로 문자 디바이스와 블록 디바이스의 주번호는 각각 독립되어 있다. 문자 디바이스와 블록 디바이스는 서로 다른 인터페이스와 API를 제공한다.

  부번호

부번호(Minor Number)는 동일한 디바이스가 여러 개 있을 때, 이들 디바이스를 구분하기 위한 용도로 사용한다. 예를 들어, 리눅스 터미널을 의미하는 /dev/ttyXX는 모든 주번호 4번를 갖습니다. 주번호가 4인 것은 모두 같은 종류의 디바이스라는 것을 의미하지만, 부번호를 사용해서 각 터미널을 구분해서 처리하게 된다.

부번호는 커널에서 사용하지 않으며, 각 디바이스 드라이버 내부에서 디바이스를 구분하기 위해 사용한다.

  misc 디바이스

Misc(miscellaneous) 디바이스는 다소 특별한 형태의 디바이스 드라이버이다. 디바이스를 구별하기 위해 주번호 외에 부번호까지 사용하는 디바이스로 부족한 디바이스 번호할당 문제를 해결하기 위해 제안된 것이다. 때문에, 별도의 함수와 자료구조를 갖는다.

커널에서 사용하는 주번호와 사용자가 사용할 수 있는 주번호는 커널 소스의 Documentation/device-list/에서 확인 할 수 있다.

현재 시스템에 장착되어 있는 디바이스들의 정보는 커널 내 정보들을 보여주는 profs에서(cat /proc/devices)로 확인할 수 있다.


디바이스를 사용하기 위해 open()을 호출하고, 사용을 마친 후에는 close()를 호출해서 사용을 끝낸다. 만약, 여러 사람이 동시에 open()을 호출해서 사용하고 있을 때, 누군가가 close()를 호출해서 사용을 끝낸 경우에는 어떻게 될까? 디바이스를 사용하던 다른 사람들은 이상한 문제를 겪게 될 것이다. 디바이스를 얼마나 많이 사용하는지 알 수 있는 방법으로 디바이스를 사용할 때 카운트를 증가 시키고, 디바이스 사용을 끝낼 때 카운트를 감소시키는 것이다. 이것을 참조 계수기(reference counter)라고 한다.

2.4 커널에서는 open()이 호출될 때, 디바이스 드라이버에서 MOD_INC_USE_COUNT를 호출해서 카운트를 증가시켜야 하고, close()가 호출될 때는 MOD_DEC_USE_COUNT를 호출해서 카운터를 감소시켜야 한다.

2.4 커널에서 카운트는 커널 내부에서 유지하며, 카운트의 증가/감소만 MOD_INC_USE_COUNT, MOD_DEC_USE_COUNT 매크로를 사용하여 변경할 수 있다.

이 방법의 문제점은 모듈을 제거하려 할 때, 오류가 발생해서 MOD_DEC_USE_COUNT가 수행되지 못한 경우에 해당 모듈을 제거할 수 없다. 디바이스 드라이버는 이미 제거되었는데, 카운트가 유지되고 있기 때문에 시스템을 다시 시작할 때까지 카운트를 초기화시킬 방법이 없기 때문이다.

2.6 커널에서는 MOD_INC_USE_COUNT, MOD_DEC_USE_COUNT 등을 사용하지 않고, 모듈의 참조 계수를 내부적으로 관리하며, 모듈을 강제로 제거할 수 있는 기능도 제공한다. 그러나 일부 디바이스 드라이버를 작성할 때 사용 횟수를 직접 관리해야 하는 경우도 있기 때문에 사용 횟수 증가를 위한 try_module_get()과 사용 횟수 감소를 위한 module_put() 함수를 제공한다.

 // 2.4
#define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define NAME  "bsp"
#define MAJOR  240

int bsp_open(struct inode *inode, struct file *filp)
{
  MOD_INC_USE_COUNT;
  return 0;
}

ssize_t bsp_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
  return 1;
}

ssize_t bsp_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
  return 2;
}

int bsp_release(struct inode *inode, struct file *filp)
{
  MOD_DEC_USE_COUNT;
  return 0;
}

struct file_operations bsp_fops =
{
  read  : bsp_read,
  write  : bsp_write,
  open  : bsp_open,
  release  : bsp_release,
};

int init_module(void)
{
  int result;
  result = register_chrdev(MAJOR, NAME, &bsp_fops);
  if(result <0)
    return result;

  return 0;
}

void cleanup_module(void)
{
  unregister_chrdev(MAJOR, NAME);
}

MODULE_LICENSE("GPL");

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

#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <asm/io.h>

#define NAME  "bsp"
#define MAJOR  240

int bsp_open(struct inode *inode, struct file *filp)
{
  try_module_get(THIS_MODULE);
  printk("open module\n");
  return 0;
}

ssize_t bsp_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
  printk("read call\n");
  return 1;
}

ssize_t bsp_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
  printk("write call\n");
  return 2;
}

int bsp_release(struct inode *inode, struct file *filp)
{
  module_put(THIS_MODULE);
  printk("release module\n");
  return 0;
}

struct file_operations bsp_fops =
{
  .owner = THIS_MODULE,
  .read = bsp_read,
  .write = bsp_write,
  .open = bsp_open,
  .release = bsp_release,
};

int init_test(void)
{
  int result;
  result = register_chrdev(MAJOR, NAME, &bsp_fops);
  if(result < 0)
    return result;
  return 0;
}

void exit_module(void)
{
  unregister_chrdev(MAJOR, NAME);
}

module_init(init_test);
module_exit(exit_module);
MODULE_LICENSE("GPL");




모듈은 커맨드 라인 인자를 받을 수 있다. 인자를 모듈로 넘기기 위해서는 커맨드 라인 인자의 값을 저장할 변수를 전역으로 선안한 후 메커니즘을 활성화 시키기 위해 MODULE_PARM() 매크로를 사용한다. 실행 시간에 insmod는 주어진 인자로 변수를 채울 것이다. 변수의 선언과 매크로들은 명확성을 위해 모듈 서두에 위치해야 한다.

MODULE_PARM() 매크로는 2개의 인자를 받는다. 변수의 이름과 타입이다.
타입 : byte "b" // short int "h" // integer "i" // long "l" // string "s"
문자열은 "char *"로 해야 하며, insmod는 그 문자열을 위한 메모리를 할당한다. 늘 변수를 초기화하는 습관을 갖길 권한다. 이것은 커널 코드이므로 반드시 방어적으로 프로그래밍 해야한다.

MODULE_PARM()은 배열 역시 지원된다. '-'에 의해 분리된 두 번호는 최대 최소값을 알려준다.

// 2.4
#include <linux/module.h>
#include <linux/kernel.h>

static int  onevalue = 1;
static char  *twostring = NULL;

MODULE_PARM(onevalue, "i");
MODULE_PARM(twostring, "s");

int init_module(void)
{
  printk("onevalue = [%d]\n", onevalue);
  printk("twostring = [%s]\n", twostring);

  return 0;
}

void cleanup_module(void)
{
  printk("Remove...\n");
}

MODULE_AUTHOR("장난감");
MODULE_DESCRIPTION("이 장난감은...");
MODULE_LICENSE("GPL")
/*
 * 실행
 * insmod test.o onevalue=0x64 twostring="test..."
 */


// 2.6
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>

static int onevalue = 1;
static char *twostring = NULL;

module_param(onevalue, int0);
module_param(twostring, charp, 0);

int init_test(void)
{
  printk("onevalue = [%d]\n", onevalue);
  printk("twostring = [%s]\n", twostring);

  return 0;
}

void exit_test(void)
{
  printk("Remove...\n");
}

module_init(init_test);
module_exit(exit_test);
MODULE_LICENSE("GPL");

/*
 * 실행
 * insmod test.ko onevalue=0x64 twostring="test..."
 */






1. 초기화와 종료 처리디바이스
1.1 드라이버가 동작하기 위한 초기화/종료에 필요한 처리 항목(대표적)
  • 디바이스 드라이버의 등록과 해제
  • 디바이스 드라이버에 내부 구조체의 메모리 할당과 해제
  • 여러 프로세스가 하나의 디바이스에 접근할 때 필요한 사전 처리 및 종료 시 처리
  • 주 번호에 종속된 부 번호를 관리하기 위한 사전 처리 및 종료 시 처리
  • 하드웨어 검출 처리 및 에러 처리
  • 하드웨어 초기화와 제거 가능한 하드웨어의 제거 처리
  • 응용 프로그램에서 디바이스 드라이버를 사용하는 경우의 초기 처리 및 사용 종료 처리
  • 부 번호에 관련된 프로세스별 처리
  • 프로세스별 메모리 할당과 해제
  • 사용하는 모듈 수의 관리

1.2 디바이스 드라이버의 초기화/종료 처리 시점
  • 모듈 적재와 커널 부팅 처리 과징 / 제거 과정
    - insmod 명령 : module_init : 모듈 적재과정
    - rmmod 명령 : module_exit : 모듈 제거 과정
  • 응용프로그램이 디바이스 파일을 여는 과정과 닫는 과정
    - open() 함수 : file_operations.open : 디바이스 파일을 여는 과정
    - close() 함수 : file_operations.release : 디바이스 파일을 닫는 과정

1.3 모듈 초기화와 종료
1.3.1 module_init의 초기화 처리
  • 디바이스 드라이버의 등록
  • 디바이스 드라이버에 내부 구조체의 메모리 할당
  • 여러 프로세스가 하나의 디바이스에 접근하는 경우에 필요한 사전 처리
  • 주 번호에 종속된 부 번호를 관리하기 위한 사전 처리
  • 하드웨어 검출 처리 및 에러 처리
  • 하드웨어 초기화

1.3.2 module_exit의 종료 처리
  • 디바이스 드라이버의 해제
  • 디바이스 드라이버에 할당된 모든 메모리의 해제

less(처리순서 예)..

(예) 처리순서
init()
{
   // 하드웨어 검출 처리 및 에러 처리
   // 하드웨어 초기화
   // 디바이스 드라이버의 등록
   // 디바이스 드라이버의 동작에 필요한 내부 구조체의 메모리 할당
   // 여러 프로세스가 디바이스 하나에 접근하는 경우에 필요한 사전 처리
   // 주 번호에 종속된 부 번호를 관리하기 위한 사전 천리
}

exit(){
   // 디바이스 드라이버에 할당된 모든 메모리 해제
   // 디바이스 드라이버의 해제
   // 하드웨어 제거에 따른 처리
}

less(처리순서 예)..


※ 실제로 디바이스 드라비어가 사용되는 시점 : 응용프로그램에서 open()함수로 열었을때 부터

1.3.3 open()함수 호출시 초기화 처리
  • 디바이스 드라이버가 처음 열렸을 때 하드웨어 초기화
  • 디바이스 드라이버의 동작에 필요한 에러 체크
  • 부 번호에 대한 처리가 필요한 경우 파일 오퍼레이션 구조체를 갱신
  • 프로세스별 메모리 할당과 초기화
  • 모듈의 사용 횟수 증가(kernel 2.4)

1.3.4 release()함수 호출시 종료 처리
  • 프로세스별 할당 메모리 해제
  • 모듈 사용 횟수 감소(kernel 2.4)

less..

커널 2.4에서는 디바이스 드라이버의 사용 횟수를 커널에서 관리하지 않고, 디바이스 드라이버 자체에서 처리하도록 하고 있음

less..



2. 모듈 사용 횟수 관리
디바이스 드라이버가 모듈 형태라면 커널에 추가하거나 삭제하는 행위가 발생.
프로세스가 드라이버를 사용하는 도중에 디바이스 드라이버 모듈을 커널에서 제거하면 커널 자체에 문제가 생기므로 커널을 디바이스 드라이버의 사용 횟수를 감시하고, 사용하지 않을 때만 삭제할 수 있게 해야한다.

커널 2.4 : 디바이스 드라이버에서 사용 횟수 관리
/linux/modules.h
  • MOD_INC_USE_COUNT : 모듈 사용 횟수를 증가시킨다.
  • MOD_DEC_USE_COUNT : 모듈 사용 횟수를 감소시킨다.
  • MOD_IN_USE : 모듈 사용 횟수가 0이 아니면 참값을 반환한다.
open()함수 : MOD_INC_COUNT 매크로를 이용하여 모듈 사용횟수 증가

less..

int xxx_open(struct inode * inode, struct file * flip)
{
    MOD_INC_USE_COUNT;
    return 0;
}

less..


release()함수 : MOD_DEC_USE_COUNT 매크로를 이용하여 모듈 사용횟수 감소

less..

 int xxx_release(struct inode * inode, struct file * flip)
{
   MOD_DEC_USE_COUNT;
   return 0;
}

less..



커널 2.6 : 커널에서 사용 횟수 관리
핫 플러그인 처리가 필요한 경우처럼 디바이스 드라이버의 사용횟수를 직접 관리해야할 경우에 
사용하는 함수
  • try_module_get(THIS_MODULE);: 모듈의 사용 횟수를 증가시킨다.

    less..

    반환값이 0이면 열기 실패 : 디바이스 드라이버 모듈이 적재되어 있지 않은 상태
    ※ 다른 프로세스가 커널에서 모듈을 제거하는 도중에 open()함수가 호출된 상태
        (선점형 커널의 특성 때문에 발생하는 상황)

    less..

  • module_put(THIS_MODULE);: 모듈의 사용 횟수를 감소시킨다.
open()함수 : try_module_get() 함수를 이용하여 모듈 사용횟수 증가

less..

 int xxx_open(struct inode * inode, struct file * flip)
{
    if(!try_module_get(THIS_MODULE)) return -ENODEV;
    return 0;
}

less..


release()함수 :     module)put() 함수를 이용하여 모듈 사용횟수 감소

less..

int xxx_release(struct inode * inode, struct file * flip)
{
    module_put(THIS_MODULE);
   return 0;
}

less..


3. I/O 영역의 경쟁 처리 함수

3.1 I/O 포트 영역의 경쟁 처리 함수
#include <linux/ioport.h>
  • check_region : 등록할수 있는 I/O 영역인지 확인한다.
    int check_region(unsigned long from, unsigned long extent);
  • request_region : I/O 영역을 등록한다.
    void request_region(unsigned long from, unsigned long extend, const char * name);
    ※ request_region 등록시 지정하는 문자열은 디바이스 드라이버의 이름을 사용
        cat /proc/ioports
  • release_region : 등록된 I/O 영역을 해제한다.
    void release_region(unsigned long from, unsigned long extent);

자원의 충돌을 해결하기 위해 함수 사용 순서
  1. 디바이스 드라이버 초기화 루틴에서 check_region() 함수를 사용해 해당 I/O영역의 사용 여부를 확인
  2. 해당 영역을 사용하지 않고 있다면 request_region() 함수를 사용해 점유 영역을 새로 등록
  3. 디바이스 드라이버를 제거 할 때는 release_region() 함수를 사용해 등록된 영역을 제거

less..

※ I/O메모리 포트 0x3FF0400번지부터 0x3FF040F까지의 영역을 사용하는 하드웨어의 예
 int xxx_open(struct inode *inode, struct file *filp){
    int err;
    if(err = check_region(0x400), 0x10)) return err;
       request_region(0x400, 0x10, "TEST");
    return 0;
}
 

int xxx_release(struct inode *inode, struct file * filp)
{
    release_region(0x400, 0x10);
      ....
    return 0;
}

less..


3.2 I/O 메모리 영역의 경쟁 처리 함수
 #include <linux/ioport.h>
  • check_mem_region : 등록된 자원인가를 확인한다.
    int check_mem_region(unsigned long from, unsigned long extent);
  • request_mem_region() : 자원을 등록한다.
    void request_mem_region(unsigned long from, unsigned long extent, const char * name);
  • release_mem_region() : 등록된 자원을 제거한다.
    void release_mem_reion(unsigned long from, unsinged long extent);

less..

   int xxx_open(struct inode *inode, struct file *filp){
    int err;
    if(err = check_mem_region(0x3FF0400), 0x10)) return err;
       request_mem_region(0x3FF0400, 0x10, "TEST");
    return 0;
}
 

int xxx_release(struct inode *inode, struct file * filp)
{
    release_mem_region(0x3FF0400, 0x10);
      ....
    return 0;
}

less..


* 디바이스 드라이버 초기화와 종료 함수 정리

 System.map 파일은 커널을 컴파일 할 때마다 새로 생성되는 파일로 커널에 들어 있는 심벌에 대한 정보를 담고 있다. 이 파일은 커널 부팅 과정에서 사용되지 않고, 부팅 이후 디버깅을 하는 프로그램 등에 의해 사용된다. 그렇더라도 부팅 과정에서 파일 버전이 틀리다고 불평하는 경우가 있다.

 우선 가장 손쉬운 해결 방법은 커널 컴파일을 하면 생기는 System.map 파일을 /boot/ 디렉토리에 복사를 하는 것이다. System.map 파일은 /usr/src/linux 디렉토리에 있다. /boot에는 System.map 파일이 있는데, 파일이 있는 경우도 있지만 버전별로 System.map-(version) 파일이 있고, 여기에 심벌릭 링크로 되어 있는 경우도 있다. 심벌릭 링크로 되어 있는 경우는 링크를 새로 만들어서 복사를 하면 된다.

 시스템에 여러 버전의 커널이 설치되어 있고, LILO를 이용하여 다른 버젼의 커널로 부팅할 수도 있다. 이 때는 부팅하는 버전별로 System.map 파일을 자동으로 바꿀 수 있다. 먼저 /boot에 버전별로 System.map 파일을 만들어 둔다. 예를 들어 2.2.16 버전의 System.map 파일은 /boot/System.map-2.2.16으로 만든다. 그리고 /etc/rc.d/rc.sysinit에 root filesystem을 Read/Write로 마운트를 한 부분 뒤에 다음 스크립트를 추가한다.

kernel_version='uname -r'
rm -f /boot/System.map
ln -s /boot/System.map-$(kernel_version) /boot/System.map

ln -sf /boot/System.map-$(uname -r) /boot/System.map

 이 스크립트는 부팅한 커널 버전에 따라서 자동으로 심벌릭 링크를 만들어 준다.


1절. 소개

1.1절. 이 문서에 대해서

이 문서는 The Linux Kernel Module Programming Guide을 참고했으며, 많은 부분 원문을 그대로 번역하였다. 그러나 본문을 실제 테스트 하면서 내용이 미흡한 부분을 보완하였으며, 몇몇 틀린 부분에 대한 수정도 이루어졌다.


1.2절. 커널 모듈이란 ?

커널 모듈에 대해서 이해하고 프로그래밍을 하기 위해서 당연히 여러분은 C언어와 리눅스 시스템에 대한 기본적인 이해를 하고 있어야 한다. 이 문서는 리눅스(유닉스) 시스템과 C에 대한 기본 이해를 하고 있다는 가정하에 작성될 것이다.

커널 모듈이란 필요에 따라 커널에 로드하거나 언로드 할 수 있는 특정한 기능을 수행하는 코드(프로그램)이다. 이렇게 하므로써 쉽게 커널의 기능을 확장할 수 있을 뿐만 아니라 운영체제를 리부팅 하지 않고도 원하는 기능을 수행할 수 있도록 만들 수 있다.

예를 들어서 어떤 하드웨어를 제어하기 위한 문자 장치(device drive)를 작성해야 한다고 생각해보자. 만약 모듈기능을 제공하지 않는 커널이라면 커널을 직접수정하는 방식을 동원해서 커널에 필요한 기능을 추가시켜야 할 것이다. 프로그램 자체가 어려워지는 것은 물론이고 기능을 테스트 하기 위해서는 계속적인 리부팅 작업이 필요하게 되므로 개발기간 역시 극적으로 늘어날 수 밖에 없을 것이다. 또한 커널에 필요한 기능이 추가될 때마다 커널에 계속해서 코드가 추가 됨으로 커널의 크기도 매우 커지게 될것이다. 사운드카드를 위한 기능을 추가했는데 해당 사운드카드를 가지지 않는 유저도 있을 것이다. 이럴 경우는 그야말로 쓸데 없는 자원낭비가 되는 셈이다. ` 커널 모듈로써 작동하도록 만들었다면 쓸데없는 기능을 하는 모듈은 언로드 시키면 그만이다.

이 문서는 리눅스 커널 2.4를 기준으로 작성되었다.


2절. 커널 모듈 프로그래밍의 기본

2.1절. 커널에 모듈 적재시키기

현재 커널에서 작동중인 모듈의 목록은 lsmod 명령을 통해서 확인할 수 있다.

# lsmod
Module                  Size  Used by    Tainted: P  
via82cxxx_audio        18304   1  (autoclean)
uart401                 6560   0  (autoclean) [via82cxxx_audio]
ac97_codec              9504   0  (autoclean) [via82cxxx_audio]
sound                  59052   0  (autoclean) [via82cxxx_audio uart401]
soundcore               4324   4  (autoclean) [via82cxxx_audio sound]
autofs                 10948   0  (autoclean) (unused)
ne2k-pci                5568   1 
8390                    6736   0  [ne2k-pci]
ipchains               37704   0 
ide-scsi                8192   0 
scsi_mod               95848   1  [ide-scsi]
ide-cd                 27360   0  (autoclean)
cdrom                  28480   0  (autoclean) [ide-cd]
usb-uhci               21764   0  (unused)
usbcore                51744   1  [usb-uhci]
ext3                   61568   4 
			
lsmod는 /proc/modules 파일의 내용을 그대로 출력한다.

그럼 커널은 이러한 모듈을 어떻게 찾아서 적재시키는 걸까. 커널이 어떤 모듈을 포함하고자 할때 해당 모듈이 아직 커널에 적재되어 있지 않다면 모듈 데몬(daemon)인 kmod가 modprobe를 실행시켜서 모듈을 읽어들이게 된다. 이때 modprobe는 다음중 하나의 방법을 이용해서 읽어들여야할 모듈을 찾게 된다.

  • softdog, ppp와 같은 모듈이름을 직접 찾는다.

  • char-major-10-30 과 같은 일반적인 식별자(generic identifier)를 이용한다.

만약 modprobe가 식별자를 이용할 경우, 해당 식별자에 대한 진짜 모듈이름을 알아와야 할것이다. 이에 대한 정보는 /etc/modules.conf에 저장되어 있다.

alias char-major-10-30 softdog
			
별칭목록을 확인함으로써 식별자를 위해서 softdog.o모듈을 적재시켜야 된다는 정보를 얻을 수 있게된다.

다음 modprobe는 /lib/modules/version/module.dep파일을 검사한다. 여기에는 해당모듈이 실행되기위해 필요한 다른 모듈들 즉 모듈의존성에 관한 정보들이 있어서 softdog.o를 적재하기 위해서 다른 모듈이 필요한지 확인하고 미리 적재시킨다. 이 파일은 depmod -a명령으로 생성시킬 수 있다. 예를 들어 msdos.o 모듈은 fat.o모듈이 우선적으로 적재되어 있어야만 한다. modprobe는 modeule.dep파일을 참조해서 의존성을 검사하게 된다.

마지막으로 modprobe는 insmod를 이용해서 원하는 모듈을 적재하기 위해서 우선적으로 필요한 모듈을 적재시키게 된다. insmod는 /lib/modules/version/을 직접참조해서 모듈을 적재한다. 최종적으로 여러분이 msdos 모듈을 올리기를 원한다면 다음과 같이 하면 된다.

# insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
# insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o
			
그러나 위와 같이 할경우 모듈 의존성을 직접 검사해줘야 하는데, 이럴 경우 modprobe를 이용하면 된다.
 
# modprobe -a msdos
			

리눅스에서 사용되는 modprobe, insmod, depmod와 같은 프로그램은 modutils(혹은 mod-utils) 패키지에 포함된다.

그럼 /etc/modules.conf를 간략하게 살펴보고 이번장을 끝마치도록 하겠다.

# This file is automatically generated by update-modules

path[misc]=/lib/modules/2.4.?/local
alias eth0 ne2k-pci
alias eth1 ne2k-pci
			
'#'은 주석을 위해서 사용되며 공백라인은 무시된다.

path[misc]는 misc모듈을 찾을 경로의 지정을 위해서 사용된다.

alias 는 kmode가 식별자 eth0을 호출 했을 때 ne2k-pci를 호출하도록 한다. alias는 꽤 중요하게 사용될 수 있는데 하나의 시스템에 동일한 장치가 2개 이상 붙어 있을때 이를 식별할 수 있도록 해준다.


2.2절. 초간단 모듈제작 : Hello World

어떤 역사적인 이유가 있는지 모르겠지만 대부분 프로그래밍입문 을 하는데 있어서 가장 먼저 "Hello World"를 출력하는 코드를 장성하는 데에서 부터 시작한다. Hello World 출력 코드와 관련된 재미있는 글이 있는데 한번 읽어 보기 바란다. Hello World의 변천사

여기에서도 "Hello World"를 출력하는 코드를 만드는 것으로 모듈 프로그래밍으로의 발걸음을 내딛도록 하겠다. 이것은 매우 간단한 모듈인데, 아직 컴파일 방법을 다루지는 않을 것이다. 모듈 컴파일은 2.3절에서 다루도록 하겠다.

#include <linux/module.h>
#include <linux/kernel.h>

int init_module(void)
{
    printk("<1>Hello World 1.\n");
    return 0;
}

void cleanup_module(void)
{
    printk(KERN_ALERT "Goodbye world 1.\n");
}
			
커널모듈은 최소한 2개의 함수를 가지고 있어야만 한다. 하나는 init_module()라는 이름의 시작(초기화)함수로써 insmod에 의해서 커널로 적재될때 호출된다. 다른 하나는 cleanup_module()라는 이름의 종료함수로써 rmmod를 호출해서 모듈을 삭제할때 호출된다.


2.2.1절. printk()에 대해서

일반적으로 printk를 이용하면 (함수이름의 어감 때문에) 특정한 메시지를 표준출력할 것으로 생각하는 경우가 많은데 printk는 유저를 위한 어떤 출력도 하지 않는다. 이름과는 달리 로그나 경고 메시지를 남기기 위한 커널로깅 목적으로 사용된다.

일반적인 로그관련 라이브러리나 함수들이 그렇듯이 printk도 우선순위(priority)를 가진다. 모두 8단계의 우선순위를 가지며 <1> KERN_ALERT 와 같은 방식으로 결정할 수 있다. 이들 우선순위에 대한 선언정보는 linux/kernel.h에서 확인할 수 있다. 만약 우선순위를 정하기 귀찮거나 정할 수 없다면 기본 우선순위 DEFAULT_MESSAGE_LOGLEVEL을 사용하면 된다.

만약 syslogd와 klogd가 실행중이라면 메시지는 /var/log/messages에 추가 된다. 다음은 실제 저장된 로그들이다.

Oct  6 01:15:39 localhost kernel: Hello World 1.
Oct  6 01:16:11 localhost kernel: Goodbye world 1.
				


2.3절. 커널 모듈 컴파일 하기

커널 모듈을 컴파일하기 위해서는 특별한 gcc 옵션과 더불어 몇가지 값들의 정의(symbols define)가 필요하다. 이유는 커널모듈 컴파일시 사용되는 커널 헤더들이 커널버젼에 매우 의존적일 수 있기 때문이다.

이러한 정의는 gcc의 -D옵션을 이용하거나 혹은 #define 선행처리자를 이용하면 된다. 이번 장에서는 커널컴파일을 하기 위해서 필요한 내용들에 대해서 다룰 것이다.

  • -c : 커널모듈은 독립적으로 실행되지 않으며 (main함수 자체를 포함하고 있지 않다) object파일 형태로 커널에 링크되어서 실행된다. 결과적으로 -c 옵션을 이용해서 오브젝트 형태로 만들어 주어야 한다.

  • -O2 : 커널은 inline함수를 매우 많이 사용하며, 그런 이유로 모듈은 반드시 최적화(optimization) 옵션을 사용해서 컴파일 되어야 한다. 최적화 옵션을 사용하지 않을 경우 어셈블러 매크로등을 사용하는데 있어서 문제가 생길수 있다. 이럴경우 모듈의 적제가 실패하게 될것이다.

  • -D__KERNEL__ : 이 코드가 유저 프로세스가 아닌 커널모드에서 작동할 것이라는걸 커널헤더에 알려준다.

  • -W -Wall : 모듈 프로그램은 커널에 매우 민감한 영향을 끼칠 수 있으며 커널을 다운 시킬 수도 있다. 그러므로 가능한한 모든 종류의 경고메시지를 검사해야할 필요가 있다. 이 옵션을 사용하면 컴파일러가 발생시킬수 있는 모든 경고메시지를 출력한다.

  • -DMODULE : 커널모듈로 작성되는 코드라는걸 알려주기 위해서 사용한다.

이외에도 컴파일에 사용될 헤더파일을 찾기 위해서 -I대신에 -isystem을 사용하며 "unused varaiable"과 같은 경고 메시지의 출력을 위해서 -W -Wall을 이용할 것이다. -isystem은 gcc-3.x이상에서 지원되는 옵션이다.

참고: -isystem 도 -I 처럼 헤더파일의 경로 지정을 위해서 사용된다는 점에서 비슷하다. -I의 경우 표준 (헤더파일_시스템 경로를 검사하기 전에 -I로 지정된 경로를 먼저 검사하는 반면 -isystem은 가장 마지막에 지정된 경로에 대한 검사를 한다.

다음은 커널 모듈을 컴파일하기 위한 전형적인 Makefile이다.

TARGET  := hello
WARN    := -W -Wall -Wstrict-prototypes -Wmissing-prototypes
INCLUDE := -isystem /lib/modules/`uname -r`/build/include
CFLAGS  := -O2 -DMODULE -D__KERNEL__ ${WARN} ${INCLUDE}
CC      := gcc
	
${TARGET}.o: ${TARGET}.c

.PHONY: clean

clean:
    rm -rf ${TARGET}.o
			
쉽게 이해 가능할 것이다. make를 실행하면 hello.c를 컴파일하고 그결과 커널 모듈(오브젝트) 파일인 hello.o를 생성해낸다. 생성된 커널 모듈은 insmod ./hello.o를 통해서 적재 할 수 있다. 이걸로 당신은 최초의 커널 모듈작성에 성공했다. 예상외로 간단하지 않은가 ? 적재된 커널모듈은 rmmod hello로 제거할 수 있다. printk()출력은 /var/log/message에 쌓일 것이다. 확인해 보기 바란다.

2.2절에 있는 예제를 보면 init_module()에서 0을 리턴하고 있다. 그런데 다른 값을 리턴하도록 하면 어떻게 될까 ? 지금한번 테스트 해보기 바란다.


2.4절. Hello World 2

init함수와 cleanup함수의 이름이 반드시 init_module()와 cleanup_module()로 작성되어야 한다는 것은 (비록 혼동을 줄여주긴 하겠지만) 왠지 이치에 맞지 않는것 같다.

리눅스 커널 2.4부터는 이들 고정된 이름대신 다른 이름으로 사용가능하며, 이를 위해서 module_init()와module_exit()함수를 제공한다.

예제 : hello_re.c

#include <linux/module.h>
#include <linux/tty.h>
#include <linux/init.h>
#include <linux/kernel.h>

int hello_init(void)
{
    printk(KERN_ALERT "HELLO, World\n");
    return 0;
}

void hello_exit(void)
{
    printk(KERN_ALERT "bye bye\n");
}

module_init(hello_init);
module_exit(hello_exit);
			


2.5절. Hello World 3 : 라이센스와 모듈에 대한 정보

커널모듈은 다른 시스템/유저프로그램에 비해 운영체제에 더욱민감한 영향을 끼칠 수 있다. 그런이유로 최소한 커널모듈에는 커널작성자에 대한 정보가 들어가도록 작성하는게 좋을 것이다.

리눅스 커널 2.4이상에서 지금까지 우리가 작성한 커널 모듈을 적재하려고 하면 다음과 경고 메시지를 출력할 것이다.

# insmod ./hello.o
Warning: loading ./hello.o will taint the kernel: no license
See http://www.tux.org/lkml/#export-tainted for information about tainted modules
			

참고: 라이센스정보관련 경고메시지 출력은 커널 옵션을 어떻게 주고 컴파일 했느냐에 따라 출력되지 않을 수도 있다. 몇몇 배포판의 경우 경고메시지가 출력되지 않을 것이다.

특히 많은 개발자들은 해당 모듈이 GPL(혹은 이와 비슷한)과 같은 공개된 라이스정책을 따르는지 그렇지 않은지에 대해서 민감할 수 있는데, MODULE_LICENSE() 매크로를 이용해서 라이센스를 명시할 수 있다. 이러한 라이센스에 대한 메커니즘은 linux/module.h에 정의되어 있다.

이와 비슷하게 MODULE_DESCRIPTION()과 MODULE_AUTHOR()매크로를 이용해서 모듈의 원저작자와 모듈에 대한 간단한 설명을 곁들일 수도 있다.

이러한 모든 매크로는 linux/module.h에 정의 되어있다. 이들 매크로 값들은 커널에 의해서 직접 이용되지는 않지만objdump와 같은 도구를 이용할때 모듈에 대한 정보를 얻는데 도움을 준다.

# objdump -s hello_li.o 
...
 0000 6b65726e 656c5f76 65727369 6f6e3d32  kernel_version=2
 0010 2e342e32 30006c69 63656e73 653d4750  .4.20.license=GP
 0020 4c000000 00000000 00000000 00000000  L...............
 0030 00000000 00000000 00000000 00000000  ................
 0040 61757468 6f723d79 756e6472 65616d20  author=yundream 
 0050 3c79756e 64726561 6d406a6f 696e632e  <yundream@joinc.
 0060 636f2e6b 723e0064 65736372 69707469  co.kr>.descripti
 0070 6f6e3d41 2073696d 706c6520 64726976  on=A simple driv
 0080 65720064 65766963 653d7465 73746465  er.device=testde
 0090 76696365 00                          vice.           
			
다음은 이들 메크로를 포함시킨 예이다.

예제 : hello_li.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DRIVER_AUTHOR "yundream <yundream@joinc.co.kr>"
#define DRIVER_DESC   "A simple driver"

int init_hello_3(void);
void cleanup_hello_3(void);


static int init_hello_4(void)
{
   printk("<2>Hello, world 4\n");
   return 0;
}


static void cleanup_hello_4(void)
{
   printk("<2>Goodbye, world 4\n");
}


module_init(init_hello_4);
module_exit(cleanup_hello_4);

MODULE_LICENSE("GPL");

MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_SUPPORTED_DEVICE("testdevice");
			


2.6절. 명령행 인자의 처리

커널 모듈도 명령행 인자를 받아들일 수 있다. 그러나 일반적으로 이용하는 argc/argv 기법을 사용할 수는 없다.

모듈로의 아규먼트 전달은 MODULE_PARM()매크로를 통해서 이루어진다. MODULE_PARM()매크로는 2개의 인자를 가진다. 첫 번째 인자는 값이 저장될 변수명이고, 두번째 인자는 저장될 데이터의 타입을 나타낸다. 데이터 타입은 "b" : 바이트, "h": short int, "i": integer, "l": long int, "s":string(문자열)가 있다. 문자열은 char * 타입이며 insmod로 호출될때 메모리가 할당된다. 다음은 간단한 활용예이다.

int myint = 3;
char *mystr;

MODULE_PARM (myint, "i");
MODULE_PARM (mystr, "s");
			

배열도 지원되는데, '-'를 이용해서 배열의 최소크기와 최대크기를 지정할 수 있다. 이는 주어질수 있는 인자의 최소와 최대 갯수를 정할 수 있음을 의미한다.

int myshortArray[4];
MODULE_PARM(myintArray, "2-4i");
			

이제 실제 모듈을 실행시키면서 인자를 넘기는 방법을 알아보도록 하자. 인자는 [변수명]=[값]의 형태로 넘어간다. 만약 모듈 코드상에 MODULE_PARM(myint, "i"); 로 되어 있다면 다음과 같은 방법으로 인자를 넘긴다.

# insmod ./hello.o myint=50
			
꽤나 독특한 방법으로 넘기고 있음을 알 수 있다.

배열의 경우에는 인자가 지정한 최대/최소의 범위를 벗어날 경우 에러메시지를 출력하며 모듈이 적재되지 않는다. 일반 애플리케이션에서 수행하는 argc를 통한 아규먼트 갯수 검사와 비슷한 형태라고 보면 된다. 배열의 각 요소는 ','를 통해서 구분되어 진다.

int myarray[4];

MODULE_PARM(myarray, "2-4");
			
와 같이 되어 있다고 할때, 다음과 같은 방법으로 값을 넘길 수 있다.
# insmod ./hello.o myarray=1,4,3
			
다음은 간단한 예제코드이다.

예제 : hello_arg.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yundream@joinc.co.kr");

static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";

MODULE_PARM (myshort, "h");
MODULE_PARM (myint, "i");
MODULE_PARM (mylong, "l");
MODULE_PARM (mystring, "s");


static int __init hello_5_init(void)
{
   printk(KERN_ALERT "Hello, world 5\n=============\n");
   printk(KERN_ALERT "myshort is a short integer: %hd\n", myshort);
   printk(KERN_ALERT "myint is an integer: %d\n", myint);
   printk(KERN_ALERT "mylong is a long integer: %ld\n", mylong);
   printk(KERN_ALERT "mystring is a string: %s\n", mystring);
   return 0;
}


static void __exit hello_5_exit(void)
{
   printk(KERN_ALERT "Goodbye, world 5\n");
}


module_init(hello_5_init);
module_exit(hello_5_exit);
			


2.7절. 모듈별 분할 컴파일

보통 조금이라도 규모가 있는 시스템/유저 애플리케이션을 작성할 때는 소스의 관리를 위해서 함수/기능별로 소스를 분할해서 컴파일한다.

커널 모듈역시 이러한 분할 컴파일을 지원하는데, 아래의 형식을 따라주어야 한다.

  1. 모든 소스파일 혹은 하나 이상의 소스파일에 #define _NO_VERSION__ 이 포함되어 있어야 한다. 모듈 컴파일을 위해서 포함시키는 module.h 내에 커널 버젼정보가 포함되어 있으며 이 정보는 모듈에 전역적으로 사용되므로 _NO_VERSION__의 사용은 꽤나 중요해진다. 만약에 version.h를 직접 포함시켜야 되는 경우가 생긴다면 _NO_VERSION__을 정의하기 바란다. module.h에는 이게 정의되어 있지 않기 때문이다.

  2. 일반적인 방법으로 컴파일한다.

  3. 만들어진 여러개의 오브젝트파일을 하나로 만들어 줘야 한다. x86하에서는 d -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o>

다음은 모듈분할 방식으로 작성된 커널 모듈 예제들이다.

예제 : start.c

#include <linux/kernel.h>       /* We're doing kernel work */
#include <linux/module.h>       /* Specifically, a module */

int init_module(void)
{
  printk("Hello, world - this is the kernel speaking\n");
  return 0;
}
			

예제 : stop.c

#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
   #include <linux/modversions.h> /* Will be explained later */
   #define MODVERSIONS
#endif        
#include <linux/kernel.h>  /* We're doing kernel work */
#include <linux/module.h>  /* Specifically, a module  */
#define __NO_VERSION__     /* It's not THE file of the kernel module */
#include <linux/version.h> /* Not included by module.h because of
	                                      __NO_VERSION__ */
	
void cleanup_module()
{
   printk("<1>Short is the life of a kernel module\n");
}
			

다음은 컴파일을 위한 Makefile이다.

CC=gcc
MODCFLAGS := -O -Wall -DMODULE -D__KERNEL__
hello.o:    start.o stop.o
	ld -m elf_i386 -r -o hello.o start.o stop.o

start.o: start.c
	${CC} ${MODCFLAGS} -c start.c

stop.o: stop.c
	${CC} ${MODCFLAGS} -c stop.c
			

출 처
  커널 2.4 버젼...
 //소스 파일
/* hellomodule.c */

// #define MODULE
#include <linux/module.h>
#include <linux/kernel.h>

int init_module()
{
  printk("Module init\n");
  printk("Hello Linux Module!\n");

  return 0;
}

void cleanup_module()
{
  printk("Module Cleaned up\n");
}

MODULE_LICENSE("GPL");

 // Makefile
# 변수 설정...
KERNELDIR=/lib/modules/$(shell uname -r)/build
CFLAGS=-D__KERNEL__ -DMODULE -I$(KERNELDIR)/include -O
# CFLAGS=-D__KERNEL__ -I$(KERNELDIR)/include -O
CC=gcc

all : test.o 
#  $(CC) $(CFLAGS) -c test.c

rebuild : clean all

insert : all
  insmod test.o

remove :
  rmmod test

clean :
  rm -rf *.


  커널 2.6 버젼...
 //소스 파일
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

int init_hello()
{
  printk("Module init\n");
  printk("Hello Linux Module\n");
  
  return 0;
}

void exit_hello()
{
  printk("Module Cleaned up\n");
}

module_init(init_hello);
module_exit(exit_hello);
MODULE_LICENSE("GPL");

 // Makefile
KERENLPATH=/usr/src/linux

obj-m := hello.o

KDIR := /usr/src/linux
PWD := $(shell pwd)

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

clean :
  rm -rf *.o


 모듈 작성은 2.4 커널과 2.6 커널이 다르다. 실제로 2.6 커널의 모듈 작성 방법은 2.4.20 이후 버젼의 커널에서도 동일하게 사용할 수 있다.

 2.6 커널에서는 linux/init.h 헤더파일이 추가되었다. 그리고 init_module()과 cleanup_module()이라는 함수 이름 규약을 더 이상 따르지 않아도 된다. 초기화 함수와 종료 함수는 자신이 원하는 이름으로 작성하면 되고, module_init()과 module_exit() 매크로에서 초기화 함수와 종료 함수의 이름만 지정해주면 된다. 물론, 2.4 커널의 init_module(), cleanup_module()을 사용해도 되지만, 현재 커널 해커들은 이들 함수를 전혀 사용하고 있지 않다. 2.4.20 이후 버전의 커널에서는 위와 같은 2.6 형식의 코드를 사용하고, gcc를 사용해서 모듈로 빌드할 수 있다. 2.4.20 이후 버젼과 2.6 커널의 모듈은 동일한 코드를 사용할 수 있으며, 빌드 과정만 다르다.

 2.6 커널부터는 커널 빌드에 대해서 kbuild 시스템을 사용합니다. 따라서 모듈을 빌드하기 위해서 gcc를 사용할 수 없고, Makefile을 정의하고, kbuild 시스템으로 모듈을 빌드해야 한다.

 모듈을 로드하기 위해 insmod 명령을 실행한다. 모듈에 2.4 커널은 .o 형식을 사용하지만, 2.6 커널은 .ko 형식을 사용한다.
2.4 커널의 경우
# insmod hello.o
2.6 커널의 경우
# insmod hello.ko

 모듈이 로드된 것을 확인하기 위해 lsmod 명령을 사용한다.

 rmmod 명령으로 모듈을 제거한다. 커널 버전에 관계없이 동일하며, 제거할 모듈 이름만 지정한다.
# rmmod hello

+ Recent posts