[LKM] Character Device Driver
Kernel/Development

[LKM] Character Device Driver

반응형
Kernel version: 5.14.17

What is a device driver?

device driver는 하나의 컴퓨터 프로그램이다. 컴퓨터나 오토마톤에 접목되어 특정 타입의 device를 동작시키거나 조종한다. device는 우리가 생각하는 여러 주변 하드웨어 기기들을 일컫는 말이다. 키보드, 마우스, 모니터, USB,... 등등 많은 종류의 하드웨어가 컴퓨터와 통신하는데 주로 sequential bytes의 형태로 이루어진다. 이 말은 즉 linux에서는 모든 devices가 file이라고 본다. ([Kernel of Linux] 강의 정리 내용을 참고하자.)

결국 우리가 만들게 될 device driver는 device라는 하나의 파일을 컨트롤하기 위한 프로그램을 만드는 것이다. device driver는 이런 device의 인터페이스를 OS나 다른 프로그램들에게 제공함으로써 내부 동작을 몰라도 하드웨어에 접근할 수 있도록 해준다.

https://butter-shower.tistory.com/29

Linux Device Driver의 구조는 위의 그림과 같이 계층 구조로 이루어져 있다. 맨 아래 하드웨어, 맨 위에 응용 프로그램이 위치하고 중간에 커널이 이를 중개하고 있다. 디바이스 드라이버의 종류로는 크게 character device driver, block device driver, nerwork device driver가 있다. 위로는 VFS(Vritual File System)이 여러 종류의 FS에 대한 공통된 인터페이스를 제공하기 위해 리눅스에서 생긴 개념이다. 디바이스 드라이버도 하나의 파일이라고 했으므로 VFS의 인터페이스에 따라 디바이스 드라이버가 작성되어야 할 것이다.

 

Character Device Driver

character device driver는 byte by byte 형식으로 데이터를 순차적으로 받아오는 식으로 동작한다. 그래서 따로 데이터를 저장해두는 buffer 또는 cache가 존재하지 않는다. 이런 방식을 사용하는 하드웨어로는 키보드, 마우스, 시리얼 포트, 사운드 카드, 조이스틱 같은 것들이 있다. 비교를 위해 block device driver에 대해서도 알아보자면, 순차적으로 데이터를 받아오는 character device driver와는 달리 디스크의 블록 단위로 데이터를 받아온다. 때문에 데이터를 저장할 system buffer(buffer cache)가 필요하다. 보통 파일 시스템에 의해 마운트 되어 관리된다. 해당 방식을 사용하는 하드웨어로는 hdd, cdroms, ram disks, magnetic tape drives 등이 있다.

 

Major and Minor Numbers

UNIX 시절부터 devices는 각자 구분하기 위해 디바이스 별로 번호를 붙여 관리해왔다. Major number가 주번호, Minor number가 부번호다. 주번호를 통해 device를 구분하고 관리한다. 같은 디바이스라면 같은 주번호를 가진다. 여러 개의 같은 디바이스가 존재한다면 device driver 내부에서 각자에게 부번호를 부여해서 관리한다. "ls -al /dev"와 "cat /proc/devices" 명령어를 통해 현재 등록된 디바이스의 종류를 볼 수 있으며 같은 주번호가 문자, 블록 디바이스에 부여될 수 있다. 주번호, 부번호에 대한 자세한 내용은 device number 공식 문서를 참고하면 된다.

 

Components of Character Device Driver

자세한 device drivers의 구현 방법은 「Linux Device Drivers, Third Edition」(이하 LDD3)(pdf) 책을 참고하면 된다. 현재 글을 쓰는 시점에서 사용하는 5.14.17 버전 커널이 아닌 2.6 버전 시절을 기준으로 작성된 책이지만, 사실 저 책 하나면 현재의 디바이스 드라이버 작성 방법의 기본을 알 수 있으므로 도움이 많이 된다.

먼저 character device driver를 작성할 때 알아둬야 할 자료구조와 API들, 그리고 ioctl을 알아보자.

Data Structures

dev_t

unsigned int 32-bit의 typedef로 상위 12 비트를 주번호, 하위 20 비트를 부번호로 사용한다. MAJOR(), MINOR() 메크로 함수로 파싱 해서 알아낼 수 있다.

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

커널에서 시스템에 character-type deivce를 등록할 때 사용하는 구조체다.

/* include/linux/cdev.h */
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;	/* device-specific f_ops */
	struct list_head list;				/* cdev 리스트 */
	dev_t dev;							/* device number - 주번호, 부번호 저장 */
	unsigned int count;
} __randomize_layout;

struct file_operations

VFS에서 각 파일 시스템에게 알려주는 구현할 파일의 기능들을 알려주는 인터페이스 역할을 하는 구조체다. 가상 함수들에 대한 테이블을 제공하는 역할을 한다고 해서 가상 함수 테이블(VFT; Virtual Function Table)이라고 부르기도 한다. 그런데 이들 기능 말고도 디바이스 자체를 제어하기 위한 명령을 구현하고 싶으면 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;

struct class & struct device

LDD3 책에서 character device driver 챕터 내에서는 요즘 작성된 디바이스 드라이버 코드와는 차이가 존재한다. 가장 큰 부분 중 하나는 바로 class 구조체와 device 구조체를 사용하는 부분이 존재한다는 점이다. 간단히 설명하자면 sysfs에서 devices를 관리할 때 필요한 동작을 위해 존재한다.

▶ class struct & device struct에 대한 TMI

더보기

LDD3에서 설명하던 문자 디바이스 드라이버 구현과는 달리 구현할 드라이버 코드에서 사용되는 class struct, device struct를 이해하기 위해서는 sysfs라는 개념을 알고 가야 한다.

 

Plug and Play라는 기술을 알고 있는가? 이 기술은 디바이스가 추가되고 제거되는 과정을 자동으로 해주는 것을 일컫는다. PnP 기술을 지원하기 위해서는 BIOS, OS, device가 이 기술을 지원해야만 한다. 리눅스에서는 2.6 버전의 커널에서는 PnP를 지원할 수 있는 통일된 모델이 없었는데 이후 PnP 지원을 위한 모델, Linux Device Model을 개발하게 된다.

Linux Device Model의 주요 목적은 시스템의 상태와 구조를 반영한 내부 데이터를 유지하는 데에 있다. 그런 정보로는 시스템에 무슨 디바이스들이 있는지, 전원 관리 관해서라든지, 디바이스들이 사용하고 있는 bus가 뭔지, 디바이스들이 가지고 있는 드라이버는 뭔지 등의 정보를 포함한다. 위의 정보들을 유지하기 위해 커널이 가져야 할 정보로는 "device, driver, bus, class, subsystem"이 있다.

해당 정보들을 커널에서 가상 파일 시스템으로서 실제 구현한 모델이 바로 sysfs다. 즉 가상 파일을 통해 다양한 커널 하위 시스템, 하드웨어 장치, 또 커널 장치 모델에서 사용자 공간에 이르는 관련 장치 드라이버에 대한 정보를 내보낸다. /sys 디렉터리 안에 "block, bus, class, devices, firmware, fs, kernel, module, power"의 서브 디렉터리를 마운트 해서 가지고 있다. 여기서 잠깐, kernel device model과 sysfs의 서브 디렉터리 사이의 상관관계에 대해 헷갈려할 수도 있는데 확실하게 하고 가는 것이 좋다. kernel device model은 sysfs 없이도 동작하지만 역은 성립하지 않는다. 즉 여기서 LDD3에서 sysfs와 관련된 class struct, device struct를 설명하지 않은 이유를 유추할 수 있는데 바로 sysfs와 관련된 두 구조체가 없어도 동작할 수 있기 때문이다.

 

마지막으로 class struct와 device struct에 대해 알아보자. class는 Linux Device Model의 high-level view, 즉 구현의 디테일을 추상화하는 관점이다. 예를 들어 SCSI, ATA와 같은 drivers가 있지만 결국 disks라는 class에 속해있다. 결국 class는 디바이스 간의 연결점이나 동작을 위해 존재하는 것이 아니라 기능을 기준으로 그루핑(grouping)하는 것이다. /sys/class 디렉터리를 통해 확인할 수 있다.

그렇다면 device struct는 무엇일까? 위에서 설명한 class struct와 함께 디바이스를 표현하는 구조체라고 할 수 있는데, class 구조체는 제네릭(generic)한 특징을 표현한다면 device 구조체는 디바이스 class를 묘사해준다.

/* include/linux/device/class.h */

/**
 * struct class - device classes
 * @name:	Name of the class.
 * @owner:	The module owner.
 * @class_groups: Default attributes of this class.
 * @dev_groups:	Default attributes of the devices that belong to the class.
 * @dev_kobj:	The kobject that represents this class and links it into the hierarchy.
 * @dev_uevent:	Called when a device is added, removed from this class, or a
 *		few other things that generate uevents to add the environment
 *		variables.
 * @devnode:	Callback to provide the devtmpfs.
 * @class_release: Called to release this class.
 * @dev_release: Called to release the device.
 * @shutdown_pre: Called at shut-down time before driver shutdown.
 * @ns_type:	Callbacks so sysfs can detemine namespaces.
 * @namespace:	Namespace of the device belongs to this class.
 * @get_ownership: Allows class to specify uid/gid of the sysfs directories
 *		for the devices belonging to the class. Usually tied to
 *		device's namespace.
 * @pm:		The default device power management operations of this class.
 * @p:		The private data of the driver core, no one other than the
 *		driver core can touch this.
 *
 * A class is a higher-level view of a device that abstracts out low-level
 * implementation details. Drivers may see a SCSI disk or an ATA disk, but,
 * at the class level, they are all simply disks. Classes allow user space
 * to work with devices based on what they do, rather than how they are
 * connected or how they work.
 */
struct class {
	const char		*name;
	struct module		*owner;

	const struct attribute_group	**class_groups;
	const struct attribute_group	**dev_groups;
	struct kobject			*dev_kobj;

	int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
	char *(*devnode)(struct device *dev, umode_t *mode);

	void (*class_release)(struct class *class);
	void (*dev_release)(struct device *dev);

	int (*shutdown_pre)(struct device *dev);

	const struct kobj_ns_type_operations *ns_type;
	const void *(*namespace)(struct device *dev);

	void (*get_ownership)(struct device *dev, kuid_t *uid, kgid_t *gid);

	const struct dev_pm_ops *pm;

	struct subsys_private *p;
};

/* include/linux/device.h */
// struct device - 너무 길어서 링크로 대체합니다.
// https://elixir.bootlin.com/linux/v5.14.17/source/include/linux/device.h#L472

APIs

아래 적히는 소제목 순서는 init 과정에서 진행하는 초기화 과정의 순서와 같다.

register_chrdev_region() & alloc_chrdev_region()

커널 2.6 버전 이전만 하더라도 character device를 등록하고 해제하는 데에 register_chrdev() / unregister_chrdev() 함수를 사용했다. 하지만 이후에는 주번호의 확장 필요성의 이유로 개편이 되었다. 자세히 알고 싶다면 해당 이슈에 대한 내용을 정리한 2006년도 lwn.net 기사 링크를 참고하길 바란다.

아무튼 이렇게 해서 주번호 관리에 대한 할당 방법이 바뀌게 되었다. 이전에는 드라이버 내에서 지정한 주번호로 고정해서 넘겨주는 방식만이 있었다면 바뀐 후에는 기존 방식대로 고정으로 주번호를 주는 것도 있지만 동적으로 주번호를 할당해주는 방식이 추가로 생겨나게 되었다. register_chrdev_region() 함수와 alloc_chrdev_region() 함수가 각각 그것이다.

/* include/linux/cdev.h, fs/char_dev.c */

/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}

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

실습할 때는 init 과정에서 제일 먼저 주번호를 할당할 것이다. 주번호를 할당하는 방식으로는 alloc_chrdev_region()을 통해 동적으로 주번호를 할당하고 얻은 주번호는 나중에 cdev 구조체에 저장된 것을 메크로 함수를 통해 얻어 사용하는 식으로 만들어볼 것이다.

cdev_init()

주번호를 할당받았으니 이제는 character device를 나타내는 cdev 구조체를 먼저 초기화해준다.

/* include/linux/cdev.h, fs/char_dev.c */

/**
 * 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()

초기화해줬으니 커널이 알 수 있도록 알려주기 위해 cdev_add() 함수를 사용한다.

/* include/linux/cdev.h, fs/char_dev.c */

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

여기까지가 LDD3에서 설명하는 부분이다. 여기까지만 해도 디바이스 드라이버가 동작하는 데에는 아무 문제가 없다. 대신 PnP 기능을 사용하거나 할 수는 없겠고, /dev 디렉터리에 수동으로 파일을 등록해주어야 하며, sysfs에서 관리되지 않아 필요하면 따로 그 디바이스 드라이버에 대한 룰을 작성해두어야 한다.

아래부터는 sysfs에 등록하고 자동으로 /dev에 디바이스를 등록해주기 위해 필요한 과정이다.

class_create()

디바이스 그룹을 나타내는 class struct를 만들기 위해 필요한 함수다. 호출하게 되면 해당 class가 sysfs의 /sys/class 디렉터리에 등록된다.

/* include/linux/device/class.h, drivers/base/class.c */

/**
 * 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()

/dev 디렉터리에 실제로 디바이스 파일을 만들어주는 역할을 한다. 파라미터로 class 구조체를 필요로 하기 때문에 위에서 class_create()를 통해 만들어준 것이다. fmt 파라미터에 넘겨준 이름으로 /dev에 만들어준다. 예를 들어 "karatus"를 주면 "/dev/karatus"로 생성되는 식이다.

/* include/linux/device.h, drivers/base/core.c */

/**
 * 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;
}
EXPORT_SYMBOL_GPL(device_create);

디바이스를 등록하는 과정은 이걸로 끝이다. 지금껏 설명하지 않은 unregistration 과정에서 필요한 함수들(unregister_chrdev_region(), cdev_del() 등)은 역할과 동작이 예측 가능하며 내용도 할당해준 오브젝트들을 해제해주는 것이 대부분이다. 그러므로 자세한 내용이 궁금하다면 따로 찾아보거나 doc 링크를 보길 추천한다.


IOCTL

[wikipedia] [man] [doc-1] [The new way of ioctl()]

ioctl은 디바이스의 input/output을 컨트롤하기 위해 일반적인 시스템 콜 이외의 동작을 수행하기 위한 시스템 콜이다. 개념, 사용법, 구현 방법 등 위의 링크를 참고해 더 자세히 알아보면 좋다.

마찬가지로 file_operations struct에 등록해서 사용하는데 ioctl 관련 필드가 두 가지 존재한다. unlocked_ioctl, compact_ioctl이 그것인데 compact_ioctl은 과거 BKL(Big Kernel Lock)을 사용해서 ioctl을 호출할 때가 있었는데 이를 위한 backward-compatibility고 unlocked_ioctl이 최신인데 디바이스 차원에서 락을 관리하도록 만든 것이다. 그래서 unlocked_ioctl에 등록해서 사용하면 된다.

 

Implementation of Character Device Driver

virtual_device.c

#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/device.h>
#include <linux/slab.h>		// kmalloc()
#include <linux/sched.h>
#include <asm/current.h>
#include <linux/uaccess.h>	// copy_to_user(), copy_from_user()

#define DEVICE_NAME "virtual_device"
#define BUF_LEN     1024

#define IOCTL_CALL  1

static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM  = 1;
static dev_t vdev_dev;
static struct cdev vdev_cdev;
static struct class *vdev_class;

static ssize_t vdev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos);
static ssize_t vdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos);
static int vdev_open(struct inode *inode, struct file *file);
static int vdev_release(struct inode *inode, struct file *file);
static long vdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg);

static struct file_operations vdev_fops = {
	.owner = THIS_MODULE,
	.read = vdev_read,
	.write = vdev_write,
	.open = vdev_open,
	.release = vdev_release,
	.unlocked_ioctl = vdev_ioctl
};

struct data {
	uint8_t vdev_buf[BUF_LEN];
};

/* called by read() */
static ssize_t vdev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	int fail_to_copy = 0;
	struct data *p = file->private_data;

	if (count > BUF_LEN)
		count = BUF_LEN;

	if ((fail_to_copy = copy_to_user(buf, p->vdev_buf, count)) != 0) {
		printk(KERN_ERR "[%s] read count: %ld, failed: %d\n", __func__, count, fail_to_copy);
		return -EFAULT;
	}

	return count; 
}

/* called by write() */
static ssize_t vdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	int overflow = 0;
	int fail_to_copy = 0;
	struct data *p = file->private_data;

	printk(KERN_INFO "[%s] before writing: %s\n", __func__, p->vdev_buf);
	if (count > BUF_LEN) {
		overflow = BUF_LEN - count;
		count = BUF_LEN;
	}
	// fail_to_copy variable always have zero as long as there is a overflow check condition above.
	fail_to_copy = copy_from_user(p->vdev_buf, buf, count);
	printk(KERN_INFO "[%s] before writing: %s\n", __func__, p->vdev_buf);

	return count;
}

/* called by open() */
static int vdev_open(struct inode *inode, struct file *file)
{
	struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL);
	
	if (p == NULL) {
		printk(KERN_ERR "[%s] kmalloc - NULL", __func__);
		return -ENOMEM;
	}

	// file private_data field:
	// information can be stored at open which is then available in the read, write, release, etc. routines.
	file->private_data = p;
	return 0;
}

/* called by last close() 
 * The reason why the function's name is "release" is
 * there is a possibility that multiple processes have opened and been using this file.
 */
static int vdev_release(struct inode *inode, struct file *file)
{
	if (file->private_data) {
		kfree(file->private_data);
		file->private_data = NULL;
	}

	return 0;
}

/* called by ioctl() */
static long vdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct data *p = file->private_data;

	switch (cmd) {
	case IOCTL_CALL:
			printk(KERN_INFO "[%s] IOCTL_CALL: buf addr - %p\n", __func__, p->vdev_buf);
			break;
	default:
			printk(KERN_ALERT "[%s] unknown cmd\n", __func__);
			break;
	}

	return 0;
}

int __init vdev_init(void)
{
	int err = -1;
	struct device *dev_struct;

	printk(KERN_INFO "vdev_init start!\n");

	// allocate the number of character device
	if ((err = alloc_chrdev_region(&vdev_dev, MINOR_BASE, MINOR_NUM, DEVICE_NAME)) != 0) {
		printk(KERN_ERR "[%s] failed to allocate the device number\n", __func__);
		goto err_return;
	}

	// init cdev
	cdev_init(&vdev_cdev, &vdev_fops);

	// add cdev
	if ((err = cdev_add(&vdev_cdev, vdev_dev, MINOR_NUM)) != 0) {
		printk(KERN_ERR "[%s] failed to add a cdev struct\n", __func__);
		goto unreg_device_num;
	}

	// create class
	vdev_class = class_create(THIS_MODULE, "chardev");
	if (IS_ERR(vdev_class)) {
		printk(KERN_ERR "[%s] failed to create a class struct\n", __func__);
		goto unreg_cdev;
	}

	// create device file to /dev
	dev_struct = device_create(vdev_class, NULL, vdev_dev, NULL, "chardev");
	if (IS_ERR(dev_struct)) {
		printk(KERN_ERR "[%s] failed to create a device file\n", __func__);
		goto unreg_class;
	}

	printk(KERN_INFO "MAJOR: %d, MINOR: %d", MAJOR(vdev_dev), MINOR(vdev_dev));

	return 0;

unreg_class:
	class_destroy(vdev_class);

unreg_cdev:
	cdev_del(&vdev_cdev);

unreg_device_num:
	unregister_chrdev_region(MKDEV(vdev_dev, MINOR_BASE), MINOR_NUM);

err_return:
	return err ? err : -1;
}

void __exit vdev_exit(void)
{
	dev_t dev = MKDEV(vdev_dev, MINOR_BASE);

	// this form is only valid when MINOR_NUM is 1.
	device_destroy(vdev_class, vdev_dev);
	class_destroy(vdev_class);
	cdev_del(&vdev_cdev);
	unregister_chrdev_region(dev, MINOR_NUM);
}

module_init(vdev_init);
module_exit(vdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Karatus");
MODULE_DESCRIPTION("hand-made char device driver");

Makefile

"CONFIG_MODULE_SIG=n

obj-m := virtual_device.o

KDIR := /lib/modules/$(shell uname -r)/build
TARGETS = virtual_device_app1 

CC := /usr/bin/gcc

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

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
	rm -rf $(TARGETS)

%.c%:
	$(CC) -o $@ @^

virtual_device_app1.c  (for test)

#include <stdio.h>
#include <unistd.h>       // orw methods
#include <sys/fcntl.h>    // file flags like O_RDWR
#include <errno.h>
#include <string.h>	   // strerror()
#include <sys/ioctl.h>	   // ioctl()

#define IOCTL_CALL  1

int main()
{
  int dev;
  char buf[1024];

  printf("Device driver test.\n");

  dev = open("/dev/chardev", O_RDWR);
  if (dev < 0) {
    printf("device file open error: %s\n", strerror(dev));
    return -1;
  }

  memset(buf, '\0', sizeof(buf));
  write(dev, "1234", 4);
  read(dev, buf, 4);
  printf("read from device: %s\n", buf);
  
  ioctl(dev, IOCTL_CALL, NULL);
  
  /* close() will call "virtual_device_release()" function.
   * because linux is a multiple user system.
   */
  close(dev); 
  return 0;
}

처음 올리게 되면 root가 아닌 이상 권한을 설정해주는 과정이 필요하다. 이후 출력되는 결과들을 보면 잘 되는 것을 확인할 수 있다.


Reference

 

Character device drivers — The Linux Kernel documentation

Overview In UNIX, hardware devices are accessed by the user through special device files. These files are grouped into the /dev directory, and system calls open, read, write, close, lseek, mmap etc. are redirected by the operating system to the device driv

linux-kernel-labs.github.io

 

Linux Device Model — The Linux Kernel documentation

As noted above, in Linux Device Model all devices are connected by a bus, even if it has a corresponding physical hardware or it is virtual. The kernel already has implemented most buses using a bus_type structure and functions to register/unregister drive

linux-kernel-labs.github.io

 

 

Porting device drivers to the 2.6 kernel [LWN.net]

The 2.6 kernel contains a long list of changes which affect device driver writers. As part of the task of porting the Linux Device Drivers sample code to 2.6, your humble LWN Kernel Page author is producing a set of articles describing the changes which mu

lwn.net

 

 

The cdev interface [LWN.net]

Since time immemorial, the basic registration interface for char devices in the kernel has been: int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops); int unregister_chrdev(unsigned int major, const char *name); In

lwn.net

 

02.Character Device Drivers - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List Character Device Drivers Character Device 드라이버는 버퍼 캐시(Buffer cache)를 사용하지 않고 데이터를 한번에 하나의 문자를 읽고 쓰는 드라이버입니다.예) Ke

www.lazenca.net

 

 

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

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

hyeyoo.com

 

반응형