꾸준히
디바이스 드라이버 구현 본문
캐릭터 디바이스 드라이버를 작성하고, 커널에 모듈로 등록하여 정상적으로 동작되는지 확인하는 과정을 정리한 글입니다.
캐릭터 디바이스 드라이버 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)
- 요구하여 사용 중인 디바이스 번호를 해제한다
- alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
- 캐릭터 디바이스 등록
- 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)
- 등록된 캐릭터 디바이스 제거
- void cdev_init(struct cdev *cdev, struct file_operations *fops)
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> 디버깅 정보
- 로그 레벨은 printk() 함수에 전달되는 문자열의 선두 문자에 “<1>”과 같이 숫자로 등급을 표현한다. linux/kernel.h에 정의된 선언문을 이용하는 것이 좋다상수 선언문 의미
'Device Driver' 카테고리의 다른 글
리눅스 디바이스 모델 (0) | 2023.08.08 |
---|---|
Virtual Memory (0) | 2023.07.16 |
디바이스 드라이버에서 변수와 메모리 할당 (0) | 2023.07.09 |
디바이스 드라이버 개요 (0) | 2023.07.01 |