계속해서 Kernel을 다루기 위해 기본적인 것들에 대해 알아보자.

Device Driver

기본적으로 Device는 Hardware를 의미하고, Driver는 그 Hardware를 컨트롤하는 Software를 의미한다. 리눅스에서는 Device를 /dev 디렉토리에 하나의 파일로써 취급한다. Device Driver는 파일로써 취급되기 때문에 open, close, read, write 등의 연산으로 접근이 가능하다.

baek@ubuntu:~$ ls -al /dev | head
total 4
drwxr-xr-x  19 root root        4420 Apr  3 19:31 .
drwxr-xr-x  27 root root        4096 Mar 19 19:15 ..
crw-------   1 root root     10, 175 Apr  3 19:31 agpgart
crw-r--r--   1 root root     10, 235 Apr  3 19:31 autofs
drwxr-xr-x   2 root root         460 Apr  3 19:31 block
drwxr-xr-x   2 root root          80 Apr  3 19:31 bsg
crw-------   1 root root     10, 234 Apr  3 19:31 btrfs-control
drwxr-xr-x   3 root root          60 Apr  3 19:31 bus
lrwxrwxrwx   1 root root           3 Apr  3 19:31 cdrom -> sr0

맨 좌측의 문자가 'c'라면, character device driver, 'b'라면, block device driver 이다. 이 중 Character device driver에 대해 자세히 살펴보자. 원래 file size가 명시되어 있어야 할 부분에 character device driver는 major, minor number가 적혀있다. 이 number를 통해 device를 구분할 수 있다.

Character Device Driver

이 Driver는 자료의 순차성을 가지고 있는 Hardware를 다룰 때 사용하며, 데이터를 문자 단위로 전달하고 읽어드린다. 대표적인 Hardware로는 터미널, 콘솔, 키보드 등이 있다.

file_operations 구조체는 Character Device Driver가 일반 프로그램과 통신을 하기 위해 제공되는 인터페이스다. read, write, open, release와 같은 함수 포인터를 사용할 수 있다.

static int chardev_open(struct inode *inode, struct file *file){
  printk("chardev_open");
  return 0;
}
struct file_operations chardev_fops={
  .open = chardev_open,
};

다음과 같은 방법으로 module에서 open() 함수를 제공할 수 있다.

device를 등록하기 위해선 register_chrdev() 함수를 이용한다.

register_chrdev(MAJOR_NUM, DEVICE_NAME, &chardev_fops);
  • Major_num : 위에서 명시한 major number
  • Device_name : 등록할 Device의 이름
  • chardev_fops : Device Driver의 operation들의 집합인 구조체다.
static struct file_operations chardev_fops={
  .read = virtual_device_read,
  .write = virtual_device_write,
  .open = virtual_device_open,
  .release = virtual_device_release
};

응용프로그램에서 open, read, write, close와 같은 연산을 수행한다면 Kernel 영역에서 system call을 호출한다. 이는 file_operations에 있는 함수를 호출하고, 이들은 mapping된 device driver의 함수를 호출한다.

Practice 1

open 함수를 사용할 수 있는 device driver를 등록해보자.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "chardev"
#define DEVICE_FILE_NAME "chardev"
#define MAJOR_NUM 100

static int chardev_open(struct inode *inode, struct file *file){
  printk("chardev_open");
  return 0;
}

sturct file_operations chardev_fops = {
  .open = chardev_open,
};

static int chardev_init(void){
  int ret_val;
  ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &chardev_fops);

  if (ret_val < 0){
    printk(KERN_ALERT "%s failed with %d\n", "Sorry, registering the character device ", ret_val);
    return ret_val;
  }

  printk(KERN_INFO "%s The major device number is %d.\n", "Registeration is a success", MAJOR_NUM);
  printk(KERN_INFO "If you want to talk to the device driver,\n");
  printk(KERN_INFO "you'll have to create a device file. \n");
  printk(KERN_INFO "We suggest you use:\n");
  printk(KERN_INFO "mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM);
  printk(KERN_INFO "The device file name is important, because\n");
  printk(KERN_INFO "the ioctl program assumes that's the\n");
  printk(KERN_INFO "file you'll use.\n");
  return 0;
}

static void chardev_exit(void){
  unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
}

module_init(chardev_init);
module_exit(chardev_exit);
obj-m := chardev.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
baek@ubuntu:~/Kernel/chardev$ make
make -C /lib/modules/5.3.0-42-generic/build M=/home/baek/Kernel/chardev modules
make[1]: Entering directory '/usr/src/linux-headers-5.3.0-42-generic'
  CC [M]  /home/baek/Kernel/chardev/chardev.o
  Building modules, stage 2.
  MODPOST 1 modules
WARNING: modpost: missing MODULE_LICENSE() in /home/baek/Kernel/chardev/chardev.o
see include/linux/module.h for more information
  CC      /home/baek/Kernel/chardev/chardev.mod.o
  LD [M]  /home/baek/Kernel/chardev/chardev.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.3.0-42-generic'

mknod 명령어를 이용하여 적재된 Module을 Device 파일로 생성할 수 있다. open을 이용하는 명령어를 사용하여 chardev_open 함수가 호출되는지 살펴보자.

baek@ubuntu:~/Kernel/chardev$ sudo insmod chardev.ko
[sudo] password for baek: 
baek@ubuntu:~/Kernel/chardev$ dmesg | tail
[15814.210903] Registeration is a success The major device number is 100.
[15814.210904] If you want to talk to the device driver,
[15814.210904] you'll have to create a device file. 
[15814.210905] We suggest you use:
[15814.210905] mknod chardev c 100 0
[15814.210905] The device file name is important, because
[15814.210905] the ioctl program assumes that's the
[15814.210906] file you'll use.
baek@ubuntu:~/Kernel/chardev$ sudo mknod chardev c 100 0
baek@ubuntu:~/Kernel/chardev$ ls -al | grep "chardev"
crw-r--r-- 1 root root 100, 0 Apr  3 23:58 chardev
baek@ubuntu:~/Kernel/chardev$ sudo chmod 666 chardev
baek@ubuntu:~/Kernel/chardev$ echo 'hi' > chardev
bash: echo: write error: Invalid argument
baek@ubuntu:~/Kernel/chardev$ dmesg | tail
[15814.210903] Registeration is a success The major device number is 100.
[15814.210904] If you want to talk to the device driver,
[15814.210904] you'll have to create a device file. 
[15814.210905] We suggest you use:
[15814.210905] mknod chardev c 100 0
[15814.210905] The device file name is important, because
[15814.210905] the ioctl program assumes that's the
[15814.210906] file you'll use.
[16060.615557] chardev_open

echo 명령어에 의해 chardev_open() 함수가 동작되었다.

Character Device Driver API

커널은 내부적으로 character device를 표현하기 위해서 cdev 구조체를 사용한다.

struct cdev{
  struct kobject kobj;
  struct module *owner;
  const struct file_openration *ops;
  struct list_head list;
  dev_t dev;
  unsigned int count;
}
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

alloc_chrdev_region() : 디바이스 번호를 동적으로 할당하기 위한 함수

  • *dev : 성공적으로 0을 리턴하면, dev에 device 번호가 할당된다.
  • firstminor : 첫번째 minor number로 보통 0이다.
  • count : device 갯수
  • *name : device name
void unregister_chrdev_region(dev_t first, unsigned int count);

unregister_chrdev_region() : 사용중인 디바이스 번호를 해제한다.

void cdev_init(struct *cdev, struct file_operations *fops);

cdev_init() : cdev 구조체를 초기화한다.

  • *cdev : 초기화할 cdev 구조체
  • *fops : 등록할 fop 구조체 포인터
void cdev_add(struct cdev *cdev, dev_t num, unsigned int count);

cdev_add() : cdev 구조체를 커널에 등록, character device에 등록.

  • *cdev : 등록할 cdev 구조체
  • num : 등록할 device 번호
  • count : 등록할 device 개수
int cdev_del(struct cdev *cdev);

cdev_del() : 등록된 cdev를 제거한다.

Practice 2

조금 더 응용해보자.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define DRIVER_NAME "chardev"
#define BUFFER_SIZE 256

static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM  = 2;
static unsigned int chardev_major;
static struct cdev chardev_cdev;
static struct class *chardev_class = NULL;

static int     chardev_open(struct inode *, struct file *);
static int     chardev_release(struct inode *, struct file *);
static ssize_t chardev_read(struct file *, char *, size_t, loff_t *);
static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *);

struct file_operations chardev_fops = {
    .open    = chardev_open,
    .release = chardev_release,
    .read    = chardev_read,
    .write   = chardev_write,
};

struct data {
    unsigned char buffer[BUFFER_SIZE];
};

static int chardev_init(void)
{
    int alloc_ret = 0;
    int cdev_err = 0;
    int minor;
    dev_t dev;

    printk("The chardev_init() function has been called.");

      // allocate the number of character device
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
    if (alloc_ret != 0) {
        printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
        return -1;
    }

    //Get the major number value in dev.
    chardev_major = MAJOR(dev);
    dev = MKDEV(chardev_major, MINOR_BASE);

    //initialize a cdev structure
    cdev_init(&chardev_cdev, &chardev_fops);
    chardev_cdev.owner = THIS_MODULE;

    //add a char device to the system
    cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM);
    if (cdev_err != 0) {
        printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    chardev_class = class_create(THIS_MODULE, "chardev");
    if (IS_ERR(chardev_class)) {
        printk(KERN_ERR  "class_create\n");
        cdev_del(&chardev_cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor);
    }

    return 0;
}

static void chardev_exit(void)
{
    int minor; 
    dev_t dev = MKDEV(chardev_major, MINOR_BASE);

    printk("The chardev_exit() function has been called.");

    for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_destroy(chardev_class, MKDEV(chardev_major, minor));
    }

    class_destroy(chardev_class);
    cdev_del(&chardev_cdev);
    unregister_chrdev_region(dev, MINOR_NUM);
}

static int chardev_open(struct inode *inode, struct file *file)
{
    char *str = "helloworld";
    int ret;

    struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL);

    printk("The chardev_open() function has been called.");

    if (p == NULL) {
        printk(KERN_ERR  "kmalloc - Null");
        return -ENOMEM;
    }

    ret = strlcpy(p->buffer, str, sizeof(p->buffer));
    if(ret > strlen(str)){
        printk(KERN_ERR "strlcpy - too long (%d)",ret);
    }

    file->private_data = p;
    return 0;
}

static int chardev_release(struct inode *inode, struct file *file)
{
    printk("The chardev_release() function has been called.");
    if (file->private_data) {
        kfree(file->private_data);
        file->private_data = NULL;
    }
    return 0;
}

static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct data *p = filp->private_data;

    printk("The chardev_write() function has been called.");   
    printk("Before calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
    if (copy_from_user(p->buffer, buf, count) != 0) {
        return -EFAULT;
    }
    printk("After calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
    return count;
}

static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct data *p = filp->private_data;

    printk("The chardev_read() function has been called.");

    if(count > BUFFER_SIZE){
        count = BUFFER_SIZE;
    }

    if (copy_to_user(buf, p->buffer, count) != 0) {
        return -EFAULT;
    }

    return count;
}

module_init(chardev_init);
module_exit(chardev_exit);
obj-m := chardev.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
baek@ubuntu:~/Kernel/chardev2$ ls -al /dev | grep "chardev"
crw-------   1 root root    240,   0 Apr  4 01:13 chardev0
crw-------   1 root root    240,   1 Apr  4 01:13 chardev1

device를 등록하였다. 이후 이 device를 사용하는 코드를 작성해보자.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define TEXT_LEN 12

int main()
{
    static char buff[256];
    int fd;

    if ((fd = open("/dev/chardev0", O_RDWR)) < 0){
        printf("Cannot open /dev/chardev0. Try again later.\n");
    }

    if (write(fd, "L000Z1K", TEXT_LEN) < 0){
        printf("Cannot write there.\n");
    }

    if (read(fd, buff, TEXT_LEN) < 0){
        printf("An error occurred in the read.\n");
    }else{
        printf("%s\n", buff);
    }

    if (close(fd) != 0){
        printf("Cannot close.\n");
    }
    return 0;
}

프로그램을 실행해보면, 모듈에서 작성한 모든 함수들이 동작하는 것을 확인할 수 있다.

baek@ubuntu:~/Kernel/chardev2$ gcc -o test test.c
baek@ubuntu:~/Kernel/chardev2$ ./test
baek@ubuntu:~/Kernel/chardev2$ dmesg | tail
[21062.482299] The chardev_open() function has been called.
[21062.482303] The chardev_write() function has been called.
[21062.482305] Before calling the copy_from_user() function : 00000000c23d3406, helloworld
[21062.482307] After calling the copy_from_user() function : 00000000c23d3406, L000Z1K
[21062.482308] The chardev_read() function has been called.

중요한 점은 write() 함수에 의해 Kernel의 Heap 영역에 data를 저장하고, read() 함수를 이용하여 해당 데이터를 출력했다는 것이다.


참고문헌

[1] https://www.lazenca.net/display/TEC/02.Character+Device+Drivers

[2] https://temp123.tistory.com/16

[3] https://chardoc.tistory.com/6

'Linux Kernel' 카테고리의 다른 글

ioctl(Input/Output control)  (0) 2020.04.07
Kernel Module  (0) 2020.04.04

+ Recent posts