디바이스 드라이버
Linux/Device Driver

디바이스 드라이버

디바이스 드라이버(Device Driver)

 

일반적으로 디바이스란 컴퓨터에 물려있는 여러 주변장치들을 뜻한다. 네트워크 어댑터, 오디오, 키보드, 마우스 등이 디바이스에 해당하고, 이러한 디바이스들을 컨트롤하기 위한 디바이스 드라이버가 존재한다.

 

응용프로그램이 하드웨어를 직접 컨트롤하는 것이 아니라 디바이스 드라이버를 통해서 하드웨어를 조종한다. 수많은 하드웨어 장치를 컨트롤하기 위해서는 그 규격에 맞춰서 개발을 해야 하는데 이는 매우 어렵고 비효율적인 일이다. 리눅스 시스템은 VFS(Virtual File System)기능을 지원하는데, 리눅스는 디바이스를 /dev 디렉토리에 하나의 파일로써 취급한다. 디바이스 드라이버 또한 파일로 관리된다.

 

 

/dev/ 아래에 존재하는 파일들이 바로 디바이스 드라이버 인터페이스이고, 디바이스 드라이버가 파일로써 취급되기 때문에 open, close, read, write 등의 연산을 통해 엑세스 할 수 있다. 각각의 디바이스 파일들은 고유한 번호와 이름을 할당받기 때문에 디바이스 드라이버를 제작하고 등록하기 위해서는 번호 및 이름을 지정해 주어야 한다.

 

디바이스 드라이버 종류

디바이스 종류는 크게 3가지로 분류될 수 있다. 문자 디바이스, 블록 디바이스, 네트워크 디바이스 이렇게 구성되어있다. 

 

1. 문자 디바이스 (Character device)

자료의 순차성을 지닌 장치로 버퍼를 사용하지 않고 바로 읽고 쓸 수 있는 장치

(마우스, PC 스피커, 키보드 등)

 

2. 블록 디바이스 (Block device)

버퍼 캐시(Cache)를 통해 블록 단위로 입출력되며, 랜덤 액세스가 가능하고, 파일 시스템을 구축할 수 있음

(하드 디스크, CD-ROM 등)

 

3. 네트워크 디바이스 (Network device)

네트워크 통신을 통해 네트워크 패킷을 주고받을 수 있는 디바이스

(Ethernet, NIC(Network Interface Card) 등

 

모듈(Module)

커널의 일부분인 프로그램으로 커널에 추가 기능이 모듈로 관리가 된다. 디바이스 드라이버를 만들고 추가할 때 커널의 모듈로 끼워 넣으면 된다. 모듈은 커널의 일부분으로서 디바이스 드라이버가 하드웨어를 동작해야 하기 때문에 커널의 일부분으로 동작해야 한다. 모듈은 커널이 일부, 그리고 그 모듈에서 디바이스 드라이버가 동작된다. 즉 모듈은 디바이스 드라이버가 아니다. 디바이스 드라이버가 모듈로 커널이 일부분으로 추가되어 동작하는 것이다.

 

모듈이라는 개념이 없을 때 디바이스 드라이버를 만들었다면 커널이 바뀌었기 때문에 다시 커널 컴파일을 해야하는 과정이 있었다. 커널 컴파일 과정은 오래 걸리는 작업이라 시간을 많이 소비한다. 모듈의 개념이 도입된 이후 위의 과정 없이 모듈을 설치하고 해제할 수 있다. 바로 이 모듈에서 장치를 등록하거나 해제할 수 있다.

 

실제 char device 드라이버 모듈을 제작해보자. 커널 모듈이 초기에 등록될때 init 과정을 거치는데, 이 안에서 char device를 등록하는 로직을 넣어줘야 한다.

 

struct cdev (include/linux/cdev.h)

 

문자 디바이스 드라이버는 커널 내부에서 cdev 구조체로 관리된다.

/* include/linux/cdev.h */
struct cdev {
	struct kobject kobj;
	struct module *owner;	/*파일 오퍼레이션의 소유자를 나타낸다. 모듈의 ID*/
	const struct file_operations *ops; /* 디바이스에서 정의된 file_operations */
	struct list_head list; 		   /* cdev 리스트 */
	dev_t dev; 			   /* 디바이스 번호 (주번호와 부번호가 각각 저장되어있음) */
	unsigned int count;	   /* 디바이스 개수 */
} __randomize_layout;

 

kobject

kobject는 이름과 참조 카운터를 가진다. kobject는 또한 부모 포인터와 특정한 자료형을 가지며, 보통 sysfs 가상 파일 시스템으로 표현된다.

dev_t

dev_t는 디바이스를 구분하기 위한 번호이다. dev_t는 include/linux/kdev_t.h를 보면 상위 12비트를 주번호, 하위 20비트를 부번호로 사용한다는 것을 알 수 있다. 이를 가져오는 MAJOR, MINOR 매크로를 지원한다 MAJOR(dev), MINOR(dev)처럼 쓰면 된다.

/* include/linux/types.h */
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t		dev_t;

/* include/linux/kdev_t.h */
#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))

struct file_operations (include/linux/fs.h)

file_operations 구조체는 Char Device, Block Device 드라이버와 일반 응용 프로그램 간의 통신을 위해 제공되는 인터페이스라고 보면 된다. read, write, open, release 등의 함수 포인터를 사용하여 디바이스 드라이버를 제작하면 된다. 예를 들어 제작한 디바이스 모듈에서 open() 함수를 다음과 같이 제공할 수 있다. test_open이라고 함수명을 작성한 뒤, file operations 구조체의 .open 필드에 저장한다. 이렇게 되면 제작한 디바이스 모듈이 커널에 등록되고 init() 함수가 동작할 때 init() 내부에 작성한 register_chrdev(.. , test_open , ..) 함수가 호출되면서 char device가 등록된다.

 

/* 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;

 

그 후 응용프로그램에서 제작한 디바이스를 open 하게 되면, test_open 함수가 호출된다.

 

유저 공간에서 open, close, read, write 같은 함수들은 트랩에 의해 커널에게 system call을 통해 처리가 된다.

sys_.. 함수 내부에선 실제 VFS 내부의 file_operations 구조체의 함수 포인터를 참조하고, 거기에 등록된 디바이스 드라이버 함수가 실제로 호출되는 과정이다. 여기선 test_open이 위에서 my_open() 함수 위치일 것이다.

 

정리하면 다음과 같다. 제작하려는 디바이스 드라이버 내부에서 file_operations 구조체의 함수 포인터들의 포맷을 맞춰서 원하는 함수를 구현하고, 이를 file_operations 구조체의 각 필드에 등록해주면 된다. 

API

 

문자 디바이스 드라이버를 등록하기 위한 API

-  Linux Kernel v2.6 이상 (필자 ubuntu 18.04 kernel v4.9)

 

※ Linux Kernel v2.6 이상을 권하는 이유는?

   현재까지 가장 많이 사용되어졌던 Linux Kernel v2.4x에서 v2.6으로 넘어오면서

   장치 관리( 드라이버 관리 )에 대한 정책이 변경되어 한층 더 수월해졌다.

   따라서 OS( Linux Kernel )는 장치의 Major Number만 알게 되면 사용이 가능해지며,

   Minor Number에 대한 관리는 Device Driver 자체적으로 처리해 사용된다.

   ( 즉, 커널에 등록 시 Major Number만 넘겨주면 되도록 바뀌었다. )

alloc_chrdev_region (linux/fs/char_dev.c)

 alloc_chrdev_region 함수는 주번호를 명시하는 대신, 주번호를 할당받는다. 이 외에도 register_chrdrv_region 함수가 있는데 이 함수는 원하는 디바이스의 번호를 미리 알고 있을 때 사용한다. 

*kernel 2.6 이전에는 register_chrdrv() 함수를 사용하여 Character device를 등록하도록 되어 있다. 

 

dev는 할당받은 디바이스 번호를 넘겨받을 포인터이다. name이라는 이름을 갖는 문자형 디바이스를 디바이스 번호

의 minor 번호는 baseminor로 시작해서 count 개수만큼 할당받는다.

/**
 * 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); //MKDEV(int ma, int mi);    번호 설정
	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는 간단하게,  디바이스의 그룹이라고 할 수 있다. /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)

/dev 디렉터리에 실제로 디바이스 파일을 만들어주는 역할을 한다. 파라미터로 class 구조체를 필요로 하기 때문에 위에서 class_create()를 통해 만들어준 것이다. 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.c

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/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 "device" // 장치 고유 이름

static dev_t device_dev;
static struct class *device_class;
static struct cdev device_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(&device_dev, MINOR_BASE, 1, DEVICE_NAME)) {
		printk(KERN_ALERT "[%s] alloc_chrdev_region failed\n", __func__);
		goto err_return;
	}

	/* init cdev */
	cdev_init(&device_cdev, &fops);

	/* add cdev */
	if (cdev_add(&device_cdev, device_dev, 1)) {
		printk(KERN_ALERT "[%s] cdev_add failed\n", __func__);
		goto unreg_device;
	}

	if ((device_class = class_create(THIS_MODULE, DEVICE_NAME)) == NULL) {
		printk(KERN_ALERT "[%s] class_add failed\n", __func__);
		goto unreg_device;
	}

	if (device_create(device_class, NULL, device_dev, NULL, DEVICE_NAME) == NULL) {
		goto unreg_class;
	}

	printk(KERN_INFO "[%s] successfully created device: Major = %d, Minor = %d\n",
			__func__, MAJOR(device_dev), MINOR(device_dev));
	return 0;

unreg_class:
	class_destroy(device_class);

unreg_device:
	unregister_chrdev_region(device_dev, 1);

err_return:
	return -1;
}

void __exit	device_exit(void) {
	device_destroy(device_class, device_dev);
	class_destroy(device_class);
	cdev_del(&device_cdev);
	unregister_chrdev_region(device_dev, 1);
	printk("KERN_INFO [%s] successfully unregistered.\n", __func__);
}

module_init(device_init);
module_exit(device_exit);

MODULE_AUTHOR("Test");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("character device driver");

Makefile

KERNDIR=/lib/modules/$(shell uname -r )/build
obj-m += device.o
objs += device.o
PWD=$(shell pwd)

default:
	make -C $(KERNDIR) M=$(PWD) modules

clean:
	make -C $(KERNDIR) M=$(PWD) clean
	rm -rf *.ko
	rm -rf *.o

make로 컴파일을 진행 그 후 sudo insmod device.ko로 모듈을 등록해 준다.

 

dmesg를 통해 확인해보면 다음과 같은 출력을 확인할 수 있다.

 

참고 사이트

https://hyeyoo.com/85

 

[LInux Kernel] 문자 디바이스 드라이버 작성

디바이스 드라이버란 디바이스 드라이버란 마우스, 키보드, 모니터, 디스크, 네트워크 인터페이스 카드 등 컴퓨터의 주변 장치를 제어하기 위한 프로그램이다. 디바이스 드라이버가 없다면 주

hyeyoo.com

https://jeongzero.oopy.io/c5c9c223-d17f-4bbc-b054-4d9fa1faffd1

 

linux Character Device Drivers 만들기

출처 : https://temp123.tistory.com/16?category=877924

jeongzero.oopy.io

 

'Linux > Device Driver' 카테고리의 다른 글

read(), write()  (0) 2022.11.25
open(), close()  (0) 2022.11.25
ioctl()  (0) 2022.11.25