Art of Pr0gr4m

[Linux Kernel 5] Character Device Driver 본문

IT/Linux Kernel

[Linux Kernel 5] Character Device Driver

pr0gr4m 2020. 4. 30. 04:05

Character Device Driver는 유저 프로세스로부터 직접 (버퍼 캐시 등을 사용하지 않고) 데이터를 읽고 쓰는 디바이스 드라이버다.

이 외에 블록 디바이스, 네트워크 디바이스 드라이버는 추후 포스팅 예정

디바이스 드라이버 구조

 

리눅스는 VFS를 통해 디바이스를 파일로 다루고 있다

/dev 아래 있는 디바이스 파일(노드)들은 디바이스와 커널 및 응용프로그램간 인터페이스를 제공한다

디바이스의 major number는 디바이스들을 구분하기 위해 사용하며

minor number는 동일한 디바이스가 여러 개 있을 때, 이들을 구분하기 위해 사용한다

 

 

1. struct file_operations

 

struct file_oeprations 구조체는 드라이버와 유저 프로세스간의 인터페이스로,

함수 포인터로 선언되어있는 멤버들을 구현해 사용한다

 

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 (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, bool spin);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	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 *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, 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 (*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 **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
 

이 중 주로 open(), release(), read(), write() 함수를 구현해 다음과 같이 등록한다

 

static struct file_operations vd_fops = {
	.read = virtual_device_read,
	.write = virtual_device_write,
	.open = virtual_device_open,
	.release = virtual_device_release
};

 

 

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("GPL");
 
#define DEVICE_NAME "chardev"
#define BUF_LEN		1024
 
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[BUF_LEN];
};
 
int __init init_chardev(void)
{
    int alloc_ret = 0;
    int cdev_err = 0;
    int minor;
    dev_t dev;
 
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DEVICE_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", cdev_err);
        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;
}
 
void __exit exit_chardev(void)
{
    int minor; 
    dev_t dev = MKDEV(chardev_major, MINOR_BASE);
 
    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)
{
    struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL);
 
    if (p == NULL) {
        printk(KERN_ERR  "kmalloc - Null");
        return -ENOMEM;
    }
 
    file->private_data = p;
    return 0;
}
 
static int chardev_release(struct inode *inode, struct file *file)
{
    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("Before calling the copy_from_user() function : [%p/%s]\n", 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]\n", 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;
 
    if(count > BUF_LEN) {
        count = BUF_LEN;
    }
 
    if (copy_to_user(buf, p->buffer, count) != 0) {
        return -EFAULT;
    }
 
    return count;
}
 
module_init(init_chardev);
module_exit(exit_chardev);

 

3. init_chardev

 

init_chardev는 디바이스 드라이버를 등록할때 초기화를 위한 엔트리이다

 

문자 디바이스의 major number와 minor number가 고정되어 있다면 register_chrdev_region 함수로 자원을 할당받고

동적으로 number와 자원을 할당받으려면 alloc_chrdev_region 함수를 사용한다

 

디바이스 번호는 dev_t 타입 변수에 저장하며, MKDEV 매크로를 이용하여 디바이스 넘버를 지정할 수 있다

또한 dev_t 변수에서 major number와 minor number는 MAJOR와 MINOR 매크로로 파싱할 수 있다

 

문자 디바이스 드라이버는 struct char_device_struct 구조체로 관리되며,

외부에는 인터페이스 역할을 하는 struct cdev 구조체가 공개된다

#define CHRDEV_MAJOR_HASH_SIZE 255
 
static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
 
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
} __randomize_layout;
 
void cdev_init(struct cdev *, const struct file_operations *);
 
struct cdev *cdev_alloc(void);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);

cdev_init 함수는 다음과 같이 문자 디바이스를 커널에 등록하기 위한 초기화를 수행한다

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

보는 것처럼 struct file_operations 구조체를 등록한다

 

문자 디바이스는 char_device_struct 구조체에서 볼 수 있는 것처럼 연결 리스트로 관리하는데,

cdev_add는 해당 리스트에 디바이스를 추가하며 cdev_del은 디바이스를 제거한다

cdev_get() 함수는 디바이스의 사용 횟수를 1 증가시키고, cdev_put()은 사용 횟수를 1 감소시킨다

 

class_create() 함수는 시스템에 생성할 디바이스의 클래스를 생성하며

device_create() 함수는 실제로 시스템에 디바이스 노드를 생성한다

 

 

4. exit_chardev

 

exit_chardev는 디바이스 드라이버가 제거될 때 실행되는 엔트리 함수다

초기화 함수에서 할당한 자원들을 역순으로 해제한다

 

device_destroy 함수는 디바이스 노드를 제거한다

class_destroy 함수는 디바이스 클래스를 제거한다

cdev_del은 문자 디바이스 관리 리스트에서 디바이스를 제거한다

unregister_chrdev_region 함수는 할당된 장치 번호 자원을 반납한다

 

 

5. open & release

 

유저 프로세스에서 해당 디바이스를 open / close 할 때 호출되는 함수이다

해당 예제에서는 내부적으로 사용하기 위한 데이터(버퍼)를 할당 및 해제하며, 파일 구조체에 등록 및 해제한다

 

 

6. read & write

 

유저 프로세스에서 read / write 함수를 이용하여 해당 디바이스에 읽고 쓸 때 호출되는 함수이다

copy_to_user 함수와 copy_from_user 함수를 이용하여 유저 데이터와 커널 데이터를 복사 한다

 

 

7. 테스트 코드

 

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
 
#define BASIC
 
#define BUF_LEN		1024
#define TEXT_LEN	5
 
int main()
{
    static char buf[1024];
    int fd;
 
    if ((fd = open("/dev/chardev0", O_RDWR)) < 0) {
		perror("open error");
    }
 
    if (write(fd, "hello", TEXT_LEN) < 0) {
		perror("write error");
    }
 
    if (read(fd, buf, TEXT_LEN) < 0) {
		perror("read error");
    } else {
        printf("%s\n", buf);
    }
 
    if (close(fd) != 0) {
		perror("close error");
    }
 
    return 0;
}

 

해당 디바이스 드라이버를 열고 쓰고 읽고 닫아서 테스트한다

 

 

8. 빌드

 

obj-m += chardev.o
 
TARGETS = chardev_app
 
KDIR := /lib/modules/$(shell uname -r)/build
 
default: ${TARGETS}
	$(MAKE) -C $(KDIR) M=$(PWD) modules
 
CC := gcc
 
%.c%:
	${CC} -o $@ $^
 
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
	rm -f ${TARGETS}

 

위 Makefile을 이용하여 빌드한다

 

 

9. 실행 결과

 

chardev 실행 결과

 

hello 문자열을 정상적으로 write한 후 read한 걸 볼 수 있다.

참고로 위와 같이 디바이스 노드를 생성하면 600 권한으로 되어있는 것을 볼 수 있다.

이 상태에선 일반 권한의 유저 프로세스는 해당 디바이스를 사용할 수 없다.

 

위와 같이 chmod로 권한을 추가해주던가, 생성 시 자동으로 권한을 설정할 수 있게

/etc/udev/rules.d/80-chardev.rules 파일에 'KERNEL == "chardev[0-9]*",GROUP="root",MODE="0666"' 를 추가해준다