Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

꾸준히

디바이스 드라이버 구현 본문

Device Driver

디바이스 드라이버 구현

S210530 2023. 7. 1. 19:25

캐릭터 디바이스 드라이버를 작성하고, 커널에 모듈로 등록하여 정상적으로 동작되는지 확인하는 과정을 정리한 글입니다.

 

캐릭터 디바이스 드라이버 API

  • 캐릭터 디바이스 번호 할당/해제
    • alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
      • 디바이스 번호를 동적으로 할당하기 위한 함수
      • 매개 변수 설명 
        *dev 성공적인 경우 디바이스 번호가 할당 됨
        firstminor 디바이스에 할당될 첫 번째 minor number, 일반적으로 0
        count 부 번호로 디바이스 개수
        *name 디바이스 이름 (/proc/devices와 sysfs에 나타남)
    • unregister_chrdev_region(dev_t first, unsiged int count)
      • 요구하여 사용 중인 디바이스 번호를 해제한다
  • 캐릭터 디바이스 등록
    • void cdev_init(struct cdev *cdev, struct file_operations *fops)
      • 구조체를 초기화
      • 매개 변수 설명
        *fops 등록할 fop 구조체 포인터
        *cdev 초기화할 cdev 구조체
    • void cdev_add(struct cdev *cdev, dev_t num, unsigned int count)
      • 준비된 cdev 구조체를 커널에 등록, 캐릭터 디바이스 등록
      • 매개 변수 설명
        num 등록할 디바이스 번호(major, minor 포함)
        count 등록할 디바이스 개수, 주로 1
        *cdev 등록할 cdev 구조체
    • struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
      • 등록한 문자 디바이스와 연결된 디바이스 파일을 만들어준다. (/dev 디렉토리에 디바이스 파일 생성)
    • int cdev_del(struct cdev *cdev)
      • 등록된 캐릭터 디바이스 제거

file_operations

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

 

문자 디바이스 드라이버와 응용 프로그램을 연결하는 고리이며, linux/fs.h에서 정의하는 이 구조체는 함수 포인터 집합이다. file_operation 구조체 내부에 선언되어 있는, 특정 동작 함수를 구현하여 가리켜야 한다. 지정하지 않으면 NULL로 남겨 두어야 한다. 이 글에서는 open, release, write, read, unlocked_ioctl 함수만 구현하겠다.

  • int open(struct inode *inode, struct file *filp)
    • 디바이스 관련 오류 확인 (디바이스가 준비되지 않았거나 이와 유사한 하드웨어 문제)
      ENODEV 하드웨어가 존재하지 않는다
      ENOMEM 커널 메모리가 부족하다
      EBUSY 디바이스 이미 사용 중이다
    • 처음으로 디바이스를 열 경우 디바이스 초기화
    • 프로세스 별 메모리 할당과 초기화
      • 보통 file 구조체 filp의 private_data에 등록하여 사용한다
      • ex) filp→private_data = vmalloc(1024)
    • Minor 번호에 대한 처리가 필요할 경우 file_operations 구조체를 갱신
  • int release(struct inode *inode, struct file *filp)
    • device_close로 부르는 경우도 있다
    • open이 filp→private_data에 할당한 데이터의 할당 삭제
    • 디바이스 종료
  • ssize_t write(struct file *filp, char *buff, size_t count, loff_t *offp)
    • 사용자 영역인 buff에서 count 바이트 만큼 읽은 후 디바이스의 offp 위치로 저장
    • struct file *filp
      • 읽기와 쓰기에 전달되는 file 구조체 변수의 선두 주소를 담은 filp는 디바이스 파일이 어떤 형식으로 열렸는가에 대한 정보를 담고 있다
      • O_RDONLY, O_NOBLOCK 등의 FLAG가 있다
    • loff_t f_pos
      • f_pos 필드 변수에는 현재의 읽기/쓰기 위치를 담는데, read(), write(), llseek()와 같이 읽기/쓰기의 위치를 변경할 수 있는 함수에 의해 변경된다
    • 주요 에러에러 설명
      EAGAIN O_NONBLOCK으로 열렸지만 write 호출 시 즉시 처리할 수 있는 상황이 아니다
      EIO I/O 에러가 발생했다
      EFAULT buf가 접근할 수 없는 주소 공간을 가리키고 있다
      ENOSPC 데이터를 위한 공간이 없다
  • ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp)
    • 디바이스의 offp 위치에서 count 바이트 만큼을 읽어서 사용자 영역인 buff로 저장해주는 기능
    • 디바이스를 열 때 옵션 사항
      • 응용 프로그램이 O_NONBLOCK이나 O_NDELAY를 지정하지 않은 상태로 디바이스 파일을 열었다면 count 값이 만족 될 때까지 기다려야 한다
      • 그렇지 않으면 현재 발생된 데이터만 버퍼에 써넣고 함수를 종료해야 한다. 반환 값은 버퍼에 써넣은 데이터 개수다.
  • long ioctl(struct file *filp, unsigned int cmd, unsigned long data)
    • filp 포인터는 응용 프로그램의 파일 디스크립터 fd와 일치하는 인수
    • cmd 인수는 명령을 나타내는 응용 프로그램의 인수 전달
    • arg 인수는 명령 실행의 결과 데이터가 전달되는 unsigend long 형의 정수 또는 포인터
    • cmd 구성

  • cmd 명령의 해석 매크로 함수매크로 함수 설명
    __IOCNR 구분 번호 필드 값을 읽는 매크로
    __IOC_TYPE 매직 넘버 필드 값을 읽는 매크로
    __IOC_SIZE 데이터의 크기 필드 값을 읽는 매크로
    __IOC_DIR 읽기와 쓰기 속성 필드 값을 읽는 매크로
  • cmd 명령의 작성 매크로 함수매크로 함수 설명
    __IO 부가적인 데이터가 없는 명령을 만드는 매크로
    __IOR 데이터를 읽어오기 위한 명령을 작성
    __IOW 데이터를 써 넣기 위한 명령을 작성
    __IOWR 디바이스 드라이버에서 읽고 쓰기 위한 명령을 작성하는 매크로

Device Driver 작성

이제 device driver를 작성해보자

driver.c 파일을 만들고 아래 코드를 작성한다

#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
#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);

static struct file_operations fops = {
	.read = device_read,
	.write = device_write,
	.open = device_open,
	.release = device_release,
	.unlocked_ioctl = device_ioctl
};

int device_open(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "device open\n");
	return 0;
}

int device_release(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "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;
}

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

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)
{
	if(alloc_chrdev_region(&my_dev, MINOR_BASE, 1, DEVICE_NAME)){
		printk(KERN_ALERT "[%s] alloc_chrdev_region failed\n", __func__);
		goto err_return;
	}

	cdev_init(&my_cdev, &fops);

	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);
	if(device_buf != NULL)
		kfree(device_buf);
	printk("KERN_INFO [%s] successfully unregistered.\n", __func__);
}

module_init(device_init);
module_exit(device_exit);

MODULE_AUTHOR("my name");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("character device driver");

 

 

driver.c를 컴파일 하기위한 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를 호출할 application(process)가 필요하다. test_program.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/helloworld", 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);
}

 


Test

driver.c를 컴파일하고 모듈로 등록한다.

ubuntu@ubuntu:~/Study/Device_Drvier/Test$ make
ubuntu@ubuntu:~/Study/Device_Drvier/Test$ insmod driver.ko

 

이제 test_program.c 컴파일 하고 실행한다

ubuntu@ubuntu:~/Study/Device_Drvier/Test$ gcc test_program.c -o test_program
ubuntu@ubuntu:~/Study/Device_Drvier/Test$ ./test_program
fd = 3, ret write = 5, ret read = 5
content = hello

정상적으로 실행되면 위와 같은 로그가 출력 된다.

마지막으로 커널로그를 확인해 보자

ubuntu@ubuntu:~/Study/Device_Drvier/Test$ dmesg | tail -n 6

[10209142.397849] [device_init] successfully created device: Major = 511, Minor = 0
[10209291.084334] device open
[10209291.084338] [device_write} count = 5, not_copied = 0
[10209291.084339] [device_read] count = 5, not_copied = 0
[10209291.084376] [device_ioctl] IOCTL_PRINT called!
[10209291.084377] device release

 


참고

커널 모듈

위에서 디바이스 드라이버를 등록할 때 커널 모듈로 등록하는 방법을 사용했다. 커널 모듈에 대해 간략하게 설명하면 리눅스 커널 모듈은, 시스템에 설치된 커널 바이너리와는 다르게 동적으로 기능을 추가할 수 있는 오브젝트 파일이다.
예를 들어 커널에 어떤 기능을 추가하기 위해선 코드를 수정하고, 커널을 컴파일하고 설치한 후에 재부팅을 해야한다. 하지만 모듈로 작성하면 시스템이 실행중인 도중에도 동적으로 기능을 추가하거나 제거할 수 있다. 

위 예제 코드에서 모듈 부분만 따로 작성해보면 아래와 같다

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

// 디바이스 초기화 관련 부분 담당
int __init device_init(void)
{
	if(alloc_chrdev_region(&my_dev, MINOR_BASE, 1, DEVICE_NAME)){
		printk(KERN_ALERT "[%s] alloc_chrdev_region failed\n", __func__);
		goto err_return;
	}

	cdev_init(&my_cdev, &fops);

	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);
	if(device_buf != NULL)
		kfree(device_buf);
	printk("KERN_INFO [%s] successfully unregistered.\n", __func__);
}

module_init(device_init);
module_exit(device_exit);

MODULE_AUTHOR("my name"); // 모듈 제작자
MODULE_LICENSE("GPL"); // GPL, GPL v2, Dual BSD/GPL, Proprietary 등
MODULE_DESCRIPTION("character device driver"); // 모듈이 하는 일을 설명

 

클래스

driver 코드에서 class_create 함수를 사용하였다.
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);	
})

 

위 디바이스 드라이버 예제에서 클래스 확인

ubuntu@ubuntu:~/Study/Device_Drvier/Test$ ls -l /sys/class/helloworld/
total 0
lrwxrwxrwx 1 root root 0  6月 29 20:38 helloworld -> ../../devices/virtual/helloworld/helloworld

 

copy_from_user() / copy_to_user() 함수

  • kernel 영역은 user 영역에서 접근하지 못하는 메모리 영역이기 때문에, pointer를 이용하지 못하고, 데이터를 전달하는 함수를 이용해서 데이터를 서로 건네줘야 한다
  • unsigned long copy_from_user(void *to, const void *from, unsigned long count)
    • 사용자 메모리(from)를 커널 메모(to)로 count만큼 복사한다
  • unsigned long copy_to_user(void *to, const void *from, unsigned long count)
    • 커널 메모리(from)를 사용자 메모리(to)로 count만큼 복사한다

 

 

printk()

  • 커널의 로그 메세지를 출력하고 관리
    • 메세지 기록 관리를 위한 로그 레벨의 지정
    • 원형 큐 구조의 관리, 출력 디바이스의 다중 지정
    • 콘솔에서 확인하거나 dmesg 명령을 이용해서 로그 파일을 확인
  • 로그 레벨 지정
    • 로그 레벨은 printk() 함수에 전달되는 문자열의 선두 문자에 “<1>”과 같이 숫자로 등급을 표현한다. linux/kernel.h에 정의된 선언문을 이용하는 것이 좋다상수 선언문 의미
      #define KERN_EMERG <0> 시스템이 동작하지 않는다
      #define KERN_ALERT <1> 항상 출력 된다
      #define KERN_CRIT <2> 치명적인 정보
      #define KERN_ERR <3> 오류 정보
      #define KERN_WARNING <4> 경고 정보
      #define KERN_NOTICE <5> 정상적인 정보
      #define KERN_INFO <6> 시스템 정보
      #define KERN_DEBUG <7> 디버깅 정보

 

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

리눅스 디바이스 모델  (0) 2023.08.08
Virtual Memory  (0) 2023.07.16
디바이스 드라이버에서 변수와 메모리 할당  (0) 2023.07.09
디바이스 드라이버 개요  (0) 2023.07.01