디바이스 드라이버란
디바이스 드라이버란 마우스, 키보드, 모니터, 디스크, 네트워크 인터페이스 카드 등 컴퓨터의 주변 장치를 제어하기 위한 프로그램이다. 디바이스 드라이버가 없다면 주변 장치들을 사용할 수가 없다. read()라는 작업을 디스크 관점에서 어떻게 처리할 것인가?와 같은 하드웨어에 밀접한 기능들은 모두 디바이스 드라이버로 구현된다. 디바이스 드라이버는 리눅스 커널에서 모듈로 구현하므로, 먼저 리눅스 커널 모듈에 대해서 알아야 한다. 이건 글로 정리해두었다.
디바이스의 종류
디바이스는 문자 디바이스 드라이버, 블록 디바이스 드라이버로 나눌 수 있다. 문자 디바이스는 시간 순으로 들어오는 데이터를 처리한다. 예를 들어 마우스, 키보드의 입력과 같이 순서대로 처리하는 것이 의미가 있는 데이터들이다. 이와 달리 블록 디바이스는 블록 단위로 random access가 가능한 데이터들을 처리한다. (문자 디바이스 드라이버는 random access가 불가능) 하드디스크가 대표적인 예이다. 이해를 돕기 위해 예시를 생각해보자. 이어폰은 블록 디바이스일까, 문자 디바이스일까? 당연히 문자 디바이스일 것이다. 왜냐하면 이어폰에서 "1시간 전에 들었던 것을 다시 듣기"는 의미없는 작업이기 때문이다.
Major and Minor numbers
디바이스는 major number (주번호)와 minor number (부번호)를 갖는다. 주번호는 디바이스를 구분하기 위한 번호이다. 예를 들어서 하드디스크를 위한 디바이스 와, 마우스를 위한 디바이스는 서로 다르다. 하지만 하드디스크를 여러개 등록한 경우에는 주번호가 같을 것이다. 서로 다른 하드디스크는 서로 다른 부번호를 부여해서 관리할 수 있다. 부번호는 디바이스 드라이버가 관리한다. cat /proc/devices 명령어로 현재 등록된 디바이스의 종류를 볼 수 있다. 명령어를 실행해보면 알겠지만, 문자 디바이스의 주번호와 블록 디바이스의 주번호는 독립적이다. 즉, 같은 주번호는 문자와 블록 디바이스에 각각 할당될 수 있다.
문자 디바이스 드라이버를 만들어보자
자, 문자 디바이스와 블록 디바이스의 차이를 간단하게 알아보았다. 그럼 문자 디바이스 드라이버를 만들어보자. 다행히도 문자 디바이스 드라이버가 블록 디바이스 드라이버보다 만들기 쉽다.
디바이스는 파일이다.
리눅스에는 많은 것이 파일로 구현된다. 그중에는 디바이스도 포함된다. 디바이스를 만드는 것은 파일을 만드는 것과 같다. 디바이스가 파일이라는 것은, "디바이스를 읽고 쓰는 행위"가 무엇인지 정의해야 한다는 의미이다. 다시 말해, VFS의 struct file_operations에 나온 항목들을 필요에 따라 구현해야 한다.
struct cdev (include/linux/cdev.h)
문자 디바이스 드라이버는 커널 내부에서 cdev 구조체로 관리된다.
/* include/linux/cdev.h */
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; /* 디바이스에서 정의된 file_operations */
struct list_head list; /* cdev 리스트 */
dev_t dev; /* 디바이스 번호 (주번호와 부번호가 각각 저장되어있음) */
unsigned int count;
} __randomize_layout;
dev_t
dev_t는 디바이스를 구분하기 위한 번호이다. dev_t는 include/linux/kdev_t.h를 보면 상위 12비트를 주번호, 하위 20비트를 부번호로 사용한다는 것을 알 수 있다. 이를 가져오는 MAJOR, MINOR 매르로를 지원한다 MAJOR(dev), MINOR(dev)처럼 쓰 된다.
struct file_operations (include/linux/fs.h)
file_operations에 함수는 되게 많다. 우리는 여기서 open, release, read, write, ioctl (unlocked_ioctl)를 구현할 것이다.
/* 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 (*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;
API
자료구조를 살펴봤으니, 이제 커널에서 문자 디바이스 드라이버를 등록하기 위한 API를 살펴보자.
alloc_chrdev_region (linux/fs/char_dev.c)
옛날에는 직접 주번호를 명시해서 디바이스를 만들었다. 하지만 그렇게 하면 서로 번호가 겹치고 난리도 아닐 것이다. alloc_chrdev_region 함수는 주번호를 명시하는 대신, 주번호를 할당받는다. 그래서 dev_t *dev포인터를 받는다. baseminor는 부번호의 시작 번호이고, count는 말 그대로 할당받을 부번호의 개수이다. 아무래도 디바이스 드라이버가 1개 이상의 부번호를 사용할 수 있는 상황을 고려한 것 같다. name은 디바이스 드라이버의 이름이다. 성공한 경우 0을 리턴한다.
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
cdev_init (linux/fs/char_dev.c)
자, dev_t를 할당받았으니 이제 cdev 구조체를 초기화하고, 커널에 등록하는 함수를 살펴보자. cdev_init은 cdev와 file_operations의 포인터를 받아 cdev를 초기화한다.
/**
* 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;
}
cdev_add (linux/fs/char_dev.c)
cdev_add는 초기화한 cdev 구조체를 커널에 등록한다.
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
if (WARN_ON(dev == WHITEOUT_DEV))
return -EBUSY;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
class_create (include/linux/device/class.h, drivers/base/class.c)
우리가 만들 디바이스를 위해, class도 하나 만들어두자. class는 간단하게, 디바이스의 그룹이라고 할 수 있다. /sys/class 폴더에서 클래스의 목록을 확인할 수 있다. class_create를 호출하면, sysfs에 우리가 만드는 class가 등록된다.
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to device_create().
*
* Returns &struct class pointer on success, or ERR_PTR() on error.
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
device_create (drivers/base/core.c)
자, 아까 cdev_add를 추가해주었지만, 아직 /dev 디렉토리에 디바이스 파일은 생성되지 않았다. device_create는 우리가 앞에서 등록한 문자 디바이스와 연결된 디바이스 파일을 만들어준다. 각 파라미터에 대한 설명은 주석에 적혀있다.
/**
* device_create - creates a device and registers it with sysfs
* @class: pointer to the struct class that this device should be registered to
* @parent: pointer to the parent struct device of this new device, if any
* @devt: the dev_t for the char device to be added
* @drvdata: the data to be added to the device for callbacks
* @fmt: string for the device's name
*
* This function can be used by char device classes. A struct device
* will be created in sysfs, registered to the specified class.
*
* A "dev" file will be created, showing the dev_t for the device, if
* the dev_t is not 0,0.
* If a pointer to a parent struct device is passed in, the newly created
* struct device will be a child of that device in sysfs.
* The pointer to the struct device will be returned from the call.
* Any further sysfs files that might be required can be created using this
* pointer.
*
* Returns &struct device pointer on success, or ERR_PTR() on error.
*
* Note: the struct class passed to this function must have previously
* been created with a call to class_create().
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, vargs);
va_end(vargs);
return dev;
}
자, 여기까지가 디바이스를 등록하는 함수의 목록이었다. 이제 device와 class를 등록하는 간단한 문자 디바이스 드라이버를 작성해보자. unregister_chrdev, class_destroy, device_destroy 등은 기능이 너무 명확하므로 설명을 생략하겠다.
Makefile
NAME = driver
obj-m += ${NAME}.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm -f Module.symvers modules.order
rm -f ${NAME}.o ${NAME}.mod ${NAME}.mod.c ${NAME}.mod.o
fclean: clean
rm -f ${NAME}.ko
re: fclean all
driver.c
여러 곳에서 에러가 남으로 goto를 사용했다. 보통 왠만하면 쓰지 말라곤 하지만 이럴 때는 goto가 유용하다.
/*
* driver.c
* This is buffer character device without any function
* only register and unregisters class and device
*/
#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/device.h>
#include <linux/errno.h>
#include <linux/unistd.h>
#include <linux/cdev.h>
#define MINOR_BASE 0 /* starting number of minor number */
#define DEVICE_NAME "example"
static dev_t my_dev;
static struct class *my_class;
static struct cdev my_cdev;
/* overriding functions of Virtual File System,
* used C99 feature
*/
static struct file_operations fops = {
};
int __init device_init(void) {
/* try allocating character device */
if (alloc_chrdev_region(&my_dev, MINOR_BASE, 1, DEVICE_NAME)) {
printk(KERN_ALERT "[%s] alloc_chrdev_region failed\n", __func__);
goto err_return;
}
/* init cdev */
cdev_init(&my_cdev, &fops);
/* add cdev */
if (cdev_add(&my_cdev, my_dev, 1)) {
printk(KERN_ALERT "[%s] cdev_add failed\n", __func__);
goto unreg_device;
}
if ((my_class = class_create(THIS_MODULE, DEVICE_NAME)) == NULL) {
printk(KERN_ALERT "[%s] class_add failed\n", __func__);
goto unreg_device;
}
if (device_create(my_class, NULL, my_dev, NULL, DEVICE_NAME) == NULL) {
goto unreg_class;
}
printk(KERN_INFO "[%s] successfully created device: Major = %d, Minor = %d\n",
__func__, MAJOR(my_dev), MINOR(my_dev));
return 0;
unreg_class:
class_destroy(my_class);
unreg_device:
unregister_chrdev_region(my_dev, 1);
err_return:
return -1;
}
void __exit device_exit(void) {
device_destroy(my_class, my_dev);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev, 1);
printk("KERN_INFO [%s] successfully unregistered.\n", __func__);
}
module_init(device_init);
module_exit(device_exit);
MODULE_AUTHOR("hyeyoo");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My first character device driver");
make로 컴파일하고, sudo insmod driver.ko로 설치하고, dmesg를 실행해보자.
/dev/example, /sys/class/example도 잘 생성되었을 것이다.
open, release, read, write, ioctl을 구현해보자.
간단하게 file_operations에 있는 open, release, read, write, ioctl을 구현해보자. 주의할 점은, 우리가 이번에 작성할 디바이스 드라이버는 thread safe하지 않다. thread safe하게 만들려면 락을 사용해야 한다. 코드를 작성하기 전에 release, ioctl에 대해서 설명해보겠다.
close와 release의 차이
우리는 파일을 닫을 때 close를 사용하는데, file_operations에는 release 함수를 구현하라고 되어있다. 왜 이름이 다를까? 그건 파일은 동시에 여러번 open될 수 있기 때문이다. 즉, close는 파일을 닫을 때마다 호출되지만, release는 close가 호출된 후, 더이상 이 파일을 열고있는 프로세스가 존재하지 않을 때 호출된다.
ioctl
ioctl 함수는 read, write와는 다르게 딱히 정해진 기능이 없다. 종종 read와 write로는 디바이스 드라이버를 구현하기에 충분하지 않기 때문에, 별도의 기능을 구현할 수 있도록 제공되는 인터페이스다. ioctl 함수의 프로토타입은 다음과 같다.
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
그런데 ioctl이라는 함수는 어디로 가고 왜 unlocked_ioctl, compat_ioctl일까? 우선 unlocked_ioctl부터 보자. 왜 unlocked라는 말이 붙었을까? 옛날 (2.6 이전)에는 ioctl을 호출할 때, BKL을 사용했다. BKL은, 커널 전체에 걸리는 락으로, 동시에 여러 개의 프로세스가 커널모드에 진입하지 못하도록 막는 락이다. 듣기만해도 엄청나게 비효율적이란걸 알 수 있다. 그래서 2.6.39를 기준으로 완전히 제거되었다. 이제는 ioctl을 호출할 때 BKL이 아니라, 디바이스 드라이버가 스스로 락을 관리해야한다. 따라서 이름이 unlocked_ioctl로 변경되었다. compat_ioctl은 32비트와의 호환성을 위한 함수이다.
A complete example
기능은 매우 간단하다. write를 하면 디바이스 드라이버에 메모리를 할당해서 저장해두다가, read를 하면 저장해둔 메모리로부터 유저 공간으로 복사한다. 유저 공간의 권한 체크를 위해 copy_to_user, copy_from_user 함수를 사용한다. (두 함수는 모두 복사에 성공하면 0, 실패하면 복사에 실패한 바이트 수를 리턴한다.)
driver.c
/*
* driver.c
* This is buffer character device
* write - save data device buffer
* read - read device buffer
*/
#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/device.h>
#include <linux/errno.h>
#include <linux/unistd.h>
#include <linux/cdev.h>
#define MINOR_BASE 0 /* starting number of minor number */
#define DEVICE_NAME "helloworld"
#define IOCTL_PRINT 1
static dev_t my_dev;
static struct class *my_class;
static struct cdev my_cdev;
static int size = 0;
static char *device_buf = NULL;
int device_open(struct inode *inode, struct file *filp);
int device_release(struct inode *inode, struct file *filp);
ssize_t device_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);
ssize_t device_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos);
long device_ioctl(struct file *filp, unsigned int cmd, unsigned long data);
/* overriding functions of Virtual File System,
* used C99 feature
*/
static struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
.unlocked_ioctl = device_ioctl
};
/* when open() is called */
int device_open(struct inode *inode, struct file *filp) {
printk(KERN_INFO "hyeyoo device open\n");
return 0;
}
/* when last close() is called */
int device_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "hyeyoo device release\n");
return 0;
}
long device_ioctl(struct file *filp, unsigned int cmd, unsigned long data) {
switch (cmd) {
case IOCTL_PRINT:
printk(KERN_INFO "[%s] IOCTL_PRINT called!", __func__);
break;
default:
printk(KERN_INFO "[%s] unknown command!", __func__);
break;
}
return 0;
}
/* Copy userspace buffer to kernel buffer
*/
ssize_t device_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos) {
int not_copied;
if (device_buf != NULL)
kfree(device_buf);
if ((device_buf = kmalloc(count + 1, GFP_KERNEL)) == NULL)
return -ENOMEM;
not_copied = copy_from_user(device_buf, buf, count);
printk("[%s] count = %ld, not_copied = %u\n", __func__,
count, not_copied);
size = count - not_copied;
return count - not_copied;
}
/* copy kernel buffer to userspace buffer, saved by write */
ssize_t device_read(struct file *filp, char *buf, size_t count, loff_t *fpos) {
int not_copied;
if (device_buf == NULL)
return -1;
if (count > size)
count = size;
not_copied = copy_to_user(buf, device_buf, count);
printk("[%s] count = %ld, not_copied = %u\n", __func__,
count, not_copied);
return (count - not_copied);
}
int __init device_init(void) {
printk(KERN_INFO "[%s]\n", __func__);
/* try allocating character device */
if (alloc_chrdev_region(&my_dev, MINOR_BASE, 1, DEVICE_NAME)) {
printk(KERN_ALERT "[%s] alloc_chrdev_region failed\n", __func__);
goto err_return;
}
/* init cdev */
cdev_init(&my_cdev, &fops);
/* add cdev */
if (cdev_add(&my_cdev, my_dev, 1)) {
printk(KERN_ALERT "[%s] cdev_add failed\n", __func__);
goto unreg_device;
}
if ((my_class = class_create(THIS_MODULE, "example")) == NULL) {
printk(KERN_ALERT "[%s] class_add failed\n", __func__);
goto unreg_device;
}
if (device_create(my_class, NULL, my_dev, NULL, "example") == NULL) {
goto unreg_class;
}
printk("[%s] successfully created device: Major = %d, Minor = %d",
__func__, MAJOR(my_dev), MINOR(my_dev));
return 0;
unreg_class:
class_destroy(my_class);
unreg_device:
unregister_chrdev_region(my_dev, 1);
err_return:
return -1;
}
void __exit device_exit(void) {
device_destroy(my_class, my_dev);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev, 1);
if (device_buf != NULL)
kfree(device_buf);
}
module_init(device_init);
module_exit(device_exit);
MODULE_AUTHOR("hyeyoo");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My first character device driver");
test_program.c
/*
* test_program.c
* by hyeyoo
*
* description:
* this program tests device driver (made by driver.c)
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#define IOCTL_PRINT 1
int main(void) {
int fd;
char buf[1000];
int read_ret, write_ret;
fd = open("/dev/example", O_RDWR);
if (fd < 0) {
printf("failed opening device: %s\n", strerror(errno));
return 0;
}
write_ret = write(fd, "hello", 5);
read_ret = read(fd, buf, 5);
printf("fd = %d, ret write = %d, ret read = %d\n", fd, write_ret, read_ret);
printf("content = %s\n", buf);
ioctl(fd, IOCTL_PRINT, NULL);
close(fd);
}
끝
참고 문서
'Kernel' 카테고리의 다른 글
[Linux Kernel] 인터럽트와 후반부 처리의 개념 (0) | 2021.04.28 |
---|---|
[Linux Kernel] proc 파일시스템과 seq_file 인터페이스 (1) | 2021.04.18 |
[Linux Kernel] 가상 파일시스템이란 (VFS, Virtual Filesystem Switch) (0) | 2021.04.13 |
[Linux Kernel] 리눅스 커널 모듈 작성 (0) | 2021.04.13 |
리눅스 커널에 커밋 해보자 (0) | 2021.03.14 |
댓글