Character Device Drivers
버퍼 캐시(Buffer cache)를 사용하지 않고 데이터를 한번에 하나의 문자를 읽고 쓰는 드라이버이다.
예) Keyboad, Sound card, Printer 드라이버
이외에도 Block Device, Network Device 드라이버가 있다.
블록 디바이스 드라이버는 버퍼 캐시를 통한 임의 접근과 블록단위로 입출력이 가능하다.
예) 하드디스크
네트워크 디바이스 드라이버는 네트워크 스택과 네트워크 하드웨어 사이에 위치해 데이터의 송수신을 담당한다.
예) Ethernet, Network interface card
struct file_operations
file_operations 구조체는 Character Device, Block Device Driver와
일반 프로그램간의 통신을 위해 제공되는 인터페이스이다.
read, write, open, release, unlocked_ioctl 등의 함수 포인터를 사용할 수 있다.
Network Device는 file_operations 구조체를 사용하지 않고,
"include/linux/netdevice.h"파일의 "net_device" 구조체를 사용한다.
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 (*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 (*setfl)(struct file *, unsigned long);
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);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);
} __randomize_layout;
예를 들어 다음과 같은 방법으로 디바이스 모듈에서 open()함수를 제공할 수 있다.
static int chardev_open(struct inode *inode, struct file *file)
{
printk("chardev_open");
return 0;
}
struct file_operations chardev_fops = {
.open = chardev_open,
};
1. file operations 구조체에 작성된 함수 포인터의 인자값을 이용하여 open 함수의 코드를 작성한다.
(해당 함수명은 "chardev_open" 이며, 해당 함수가 호출되면 "chardev_open"가 커널 메시지로 출력됨)
2. 작성한 함수명을 "struct file_operations" 형태로 선언된 구조체의
".open" 필드에 함수의 주소(chardev_open)를 저장한다.
Example [struct file_operations - .open]
ubuntu 18.10에서 진행한다.
예제 코드는 다음과 같이 동작한다.
1. 디바이스 모듈이 커널에 등록될 때 chardev_init() 함수가 동작한다.
register_chrdev() 함수에 의해 해당 문자 디바이스의 메이저 번호를 등록한다.
2. User space에서 디바이스가 open할때 chardev_open()함수가 호출된다.
3. 디바이스 모듈이 커널에서 제거될 때 chardev_exit() 함수가 동작한다.
unregister_chrdev() 함수에 의해 해당 문자 디바이스의 메이저 번호를 제거된다.
chardev.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/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "chardev"
#define DEVICE_FILE_NAME "chardev"
#define MAJOR_NUM 100
static int chardev_open(struct inode *inode, struct file *file)
{
printk("chardev_open");
return 0;
}
struct file_operations chardev_fops = {
.open = chardev_open,
};
static int chardev_init(void)
{
int ret_val;
ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &chardev_fops);
if (ret_val < 0) {
printk(KERN_ALERT "%s failed with %d\n",
"Sorry, registering the character device ", ret_val);
return ret_val;
}
printk(KERN_INFO "%s The major device number is %d.\n",
"Registeration is a success", MAJOR_NUM);
printk(KERN_INFO "If you want to talk to the device driver,\n");
printk(KERN_INFO "you'll have to create a device file. \n");
printk(KERN_INFO "We suggest you use:\n");
printk(KERN_INFO "mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM);
printk(KERN_INFO "The device file name is important, because\n");
printk(KERN_INFO "the ioctl program assumes that's the\n");
printk(KERN_INFO "file you'll use.\n");
return 0;
}
static void chardev_exit(void)
{
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
}
module_init(chardev_init);
module_exit(chardev_exit);
Makefile
obj-m := chardev.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
make 명령으로 빌드하고,
빌드된 Kernel module을 Kernel에 적재한다.
mknod 명령어를 이용하여 적재된 Module을 디바이스 파일로 생성한다.
chmod 명령어를 이용하여 일반 유저들이 읽고 쓸 수 있도록 설정한다.
chardev_open() 함수의 동작을 확인하기 위해 echo 명령어를 이용하여
디바이스 파일을 열어 'A'를 저장하도록 하면 에러 메시지와 함께 저장되지 않는다.
dmesg 명령어를 이용하여 "chardev_open" 메시지를 확인할 수 있는데,
이는 echo 명령어에 의해 chardev_open() 함수가 동작되었다는 것을 알 수 있다.
모듈 관련 명령어
insmod | 생성된 모듈을 Kernel의 symbol table을 통해 Kernel에 링크하는 명령어 |
rmmod | Kernel에 등록된 모듈을 제거하는 명령어 |
lsmod | Kernel에 등록된 모듈 목록을 출력하는 명령어 |
modinfo | 모듈에 상세한 정보를 출력하는 명령어 |
mknod | 적재된 Module을 디바이스 파일로 생성하는 명령어 |
mknod 명령어
인자 | mknod <디바이스 파일명> <디바이스 파일 형식> <Major number> <Minor number> |
디바이스 파일 형식 | p : FIFO(:12) b : 블럭 장치 파일 (block device file) c, u : 문자 파일 (character special file), unbuffered special file |
Major Number & Minor Number |
MAJOR는 블럭장치 혹은 문자장치의 그룹에 할당되는 번호다. MINOR 번호는 MAJOR로 묶여진 문자장치의 그룹중 하나에 할당되는 번호다. 이 두개의 번호를 이용해서 장치를 식별할 수 있다. |
Example [struct file_operations .open, .release, .read, .write]
이번엔 다양한 기능들이 추가된 예제다.
기능들에 대한 설명은 다음과 같다.
chardev_init()
해당 모듈이 커널에 등록될 때 chardev_init() 함수에서 다음 기능을 처리한다.
- alloc_chrdev_region() 함수를 이용하여 Character Device의 번호를 시스템에 등록
- major(), mkdev()함수를 이용하여 디바이스에서 사용할 Major, Minor 번호를 취득
- cdev_init() 함수를 이용하여 chardev_cdev 구조체를 초기화
- cdev_add() 함수를 이용하여 Character Device를 시스템에 추가
- class_create() 함수를 이용하여 시스템에 생성할 디바이스의 클래스를 생성
- device_create() 함수를 이용하여 시스템에 디바이스를 생성
chardev_exit()
해당 모듈이 커널에서 제거될 때 chardev_exit() 함수에서 다음 기능을 처리한다.
- device_destroy() 함수를 이용하여 device_create() 함수에 의해 생성된 디바이스 제거
- class_destroy() 함수를 이용하여 class_create() 함수에 의해 생성된 디바이스 클래스 소멸
- cdev_del() 함수를 이용하여 cdev_add() 함수에 의해 추가된 Character Device 제거
- unregister_chrdev_region() 함수를 이용하여 alloc_chrdev_region() 함수에 의해 등록된 장치 번호 반환
chardev_open()
사용자 공간(User space)에서 해당 디바이스를 열 때마다
chardev_open() 함수가 호출되며 다음 기능을 처리한다.
- kmalloc() 함수를 이용하여 Kernel heap 영역에 data 구조체의 크기만큼의 공간을 할당 받음
- strlcpy() 함수를 이용하여 str변수에 저장된 값을 p→buffer 영역에 복사
chardev_release()
사용자 공간(User space)에서 해당 디바이스를 닫을 때
chardev_release() 함수가 호출되며 다음 기능을 처리한다.
- kfree() 함수를 이용하여 할당받은 Heap영역을 해제
chardev_write()
사용자 공간(User space)에서 해당 디바이스로 데이터가 전송 될 때
chardev_write() 함수가 호출되며 다음 기능을 처리한다.
- copy_from_user() 함수를 이용하여 사용자 공간으로 부터 전달 받은 데이터(buf)를 "p→buffer" 변수에 복사합니다.
chardev_read()
사용자 공간(User space)에서 해당 디바이스로부터 데이터를 받을 때
chardev_read() 함수가 호출되며 다음 기능을 처리한다.
- copy_to_user() 함수를 이용하여 커널 영역에 저장된 "p→buffer" 데이터를 "buf" 변수에 복사
chardev.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/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "chardev"
#define BUFFER_SIZE 256
static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM = 2;
static unsigned int chardev_major;
static struct cdev chardev_cdev;
static struct class *chardev_class = NULL;
static int chardev_open(struct inode *, struct file *);
static int chardev_release(struct inode *, struct file *);
static ssize_t chardev_read(struct file *, char *, size_t, loff_t *);
static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *);
struct file_operations chardev_fops = {
.open = chardev_open,
.release = chardev_release,
.read = chardev_read,
.write = chardev_write,
};
struct data {
unsigned char buffer[BUFFER_SIZE];
};
static int chardev_init(void)
{
int alloc_ret = 0;
int cdev_err = 0;
int minor;
dev_t dev;
printk("The chardev_init() function has been called.");
alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
if (alloc_ret != 0) {
printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret);
return -1;
}
//Get the major number value in dev.
chardev_major = MAJOR(dev);
dev = MKDEV(chardev_major, MINOR_BASE);
//initialize a cdev structure
cdev_init(&chardev_cdev, &chardev_fops);
chardev_cdev.owner = THIS_MODULE;
//add a char device to the system
cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM);
if (cdev_err != 0) {
printk(KERN_ERR "cdev_add = %d\n", alloc_ret);
unregister_chrdev_region(dev, MINOR_NUM);
return -1;
}
chardev_class = class_create(THIS_MODULE, "chardev");
if (IS_ERR(chardev_class)) {
printk(KERN_ERR "class_create\n");
cdev_del(&chardev_cdev);
unregister_chrdev_region(dev, MINOR_NUM);
return -1;
}
for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor);
}
return 0;
}
static void chardev_exit(void)
{
int minor;
dev_t dev = MKDEV(chardev_major, MINOR_BASE);
printk("The chardev_exit() function has been called.");
for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
device_destroy(chardev_class, MKDEV(chardev_major, minor));
}
class_destroy(chardev_class);
cdev_del(&chardev_cdev);
unregister_chrdev_region(dev, MINOR_NUM);
}
static int chardev_open(struct inode *inode, struct file *file)
{
char *str = "helloworld";
int ret;
struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL);
printk("The chardev_open() function has been called.");
if (p == NULL) {
printk(KERN_ERR "kmalloc - Null");
return -ENOMEM;
}
ret = strlcpy(p->buffer, str, sizeof(p->buffer));
if(ret > strlen(str)){
printk(KERN_ERR "strlcpy - too long (%d)",ret);
}
file->private_data = p;
return 0;
}
static int chardev_release(struct inode *inode, struct file *file)
{
printk("The chardev_release() function has been called.");
if (file->private_data) {
kfree(file->private_data);
file->private_data = NULL;
}
return 0;
}
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct data *p = filp->private_data;
printk("The chardev_write() function has been called.");
printk("Before calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
if (copy_from_user(p->buffer, buf, count) != 0) {
return -EFAULT;
}
printk("After calling the copy_from_user() function : %p, %s",p->buffer,p->buffer);
return count;
}
static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct data *p = filp->private_data;
printk("The chardev_read() function has been called.");
if(count > BUFFER_SIZE){
count = BUFFER_SIZE;
}
if (copy_to_user(buf, p->buffer, count) != 0) {
return -EFAULT;
}
return count;
}
module_init(chardev_init);
module_exit(chardev_exit);
Makefile
obj-m := chardev.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
테스트 코드 작성
test.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define TEXT_LEN 12
int main()
{
static char buff[256];
int fd;
if ((fd = open("/dev/chardev0", O_RDWR)) < 0){
printf("Cannot open /dev/chardev0. Try again later.\n");
}
if (write(fd, "lazenca0x0", TEXT_LEN) < 0){
printf("Cannot write there.\n");
}
if (read(fd, buff, TEXT_LEN) < 0){
printf("An error occurred in the read.\n");
}else{
printf("%s\n", buff);
}
if (close(fd) != 0){
printf("Cannot close.\n");
}
return 0;
}
make로 모듈 빌드
(sudo rmmod chardev로 먼저 등록한 모듈을 날려준 뒤 진행)
모듈을 커널에 등록하기 전에 "/etc/udev/rules.d/" 경로에 모듈 등록 시
자동으로 생성되는 디바이스 파일의 규칙을 저장한다.
이제 insmod 명령어를 이용하여 모듈을 커널에 등록하면
"/dev/" 경로에 자동으로 chardev0, chardev1 두개의 디바이스가 생성된다.
해당 디바이스는 접근권한은 666이기 때문에 일반 유저들도 해당 디바이스를 사용할 수 있다.
테스트 프로그램을 빌드하고 실행하면 다음과 같은 내용을 확인 할 수 있다.
테스트 프로그램이 정상적으로 동작하는 것을 확인할 수 있다.
"dmesg" 명령어를 이용하여 모듈에서 출력된 메시지를 이용하여
모듈에서 작성한 모든 함수들이 정상적으로 동작하는 것을 확인 할 수 있다.
여기서 중요한것은 write()함수에 의해 커널의 Heap 영역에 데이터를 저장하고,
read() 함수를 이용하여 해당 데이터를 사용자 공간에서 출력되었다는 것이다.
다음과 같은 형태로도 동작가능하다.
동일한 디바이스를 중복으로 열 수 있으며, 동일한 모듈에 2개의 디바이스를 생성할 수도 있다.
test1.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main()
{
static char buff[256];
int fd0_A, fd0_B, fd1_A;
if ((fd0_A = open("/dev/chardev0", O_RDWR)) < 0) perror("open");
if ((fd0_B = open("/dev/chardev0", O_RDWR)) < 0) perror("open");
if ((fd1_A = open("/dev/chardev1", O_RDWR)) < 0) perror("open");
if (write(fd0_A, "0_A", 4) < 0) perror("write");
if (write(fd0_B, "0_B", 4) < 0) perror("write");
if (write(fd1_A, "1_A", 4) < 0) perror("write");
if (read(fd0_A, buff, 4) < 0) perror("read");
printf("%s\n", buff);
if (read(fd0_B, buff, 4) < 0) perror("read");
printf("%s\n", buff);
if (read(fd1_A, buff, 4) < 0) perror("read");
printf("%s\n", buff);
if (close(fd0_A) != 0) perror("close");
if (close(fd0_B) != 0) perror("close");
if (close(fd1_A) != 0) perror("close");
return 0;
}
이 테스트 프로그램을 실행하면 다음과 같은 내용을 확인 할 수 있다.
테스트 프로그램에서 전달된 값을 정상적으로 출력한다.
동일한 모듈 또는 동일한 디바이스이지만
전달된 문자열을 저장하기 위해 사용되는 Heap 영역이 다르다는 것을 알 수 있다.
https://www.lazenca.net/display/TEC/02.Character+Device+Drivers
'background > linux kernel' 카테고리의 다른 글
[Lazenca][Development of Kernel Module] 04.Creating a kernel module to privilege escalation (0) | 2024.03.29 |
---|---|
[Lazenca][Development of Kernel Module] 03.ioctl(Input/Output control) (0) | 2024.03.28 |
[Linux Kernel] Device Driver (0) | 2024.03.28 |
[Lazenca][Development of Kernel Module] 01.Hello world! (0) | 2024.03.28 |
fork-exec (1) | 2022.10.11 |