계속해서 Kernel을 다루기 위해 기본적인 것들에 대해 알아보자.
Device Driver
기본적으로 Device는 Hardware를 의미하고, Driver는 그 Hardware를 컨트롤하는 Software를 의미한다. 리눅스에서는 Device를 /dev 디렉토리에 하나의 파일로써 취급한다. Device Driver는 파일로써 취급되기 때문에 open, close, read, write 등의 연산으로 접근이 가능하다.
baek@ubuntu:~$ ls -al /dev | head
total 4
drwxr-xr-x 19 root root 4420 Apr 3 19:31 .
drwxr-xr-x 27 root root 4096 Mar 19 19:15 ..
crw------- 1 root root 10, 175 Apr 3 19:31 agpgart
crw-r--r-- 1 root root 10, 235 Apr 3 19:31 autofs
drwxr-xr-x 2 root root 460 Apr 3 19:31 block
drwxr-xr-x 2 root root 80 Apr 3 19:31 bsg
crw------- 1 root root 10, 234 Apr 3 19:31 btrfs-control
drwxr-xr-x 3 root root 60 Apr 3 19:31 bus
lrwxrwxrwx 1 root root 3 Apr 3 19:31 cdrom -> sr0
맨 좌측의 문자가 'c'라면, character device driver
, 'b'라면, block device driver
이다. 이 중 Character device driver에 대해 자세히 살펴보자. 원래 file size가 명시되어 있어야 할 부분에 character device driver는 major, minor number가 적혀있다. 이 number를 통해 device를 구분할 수 있다.
Character Device Driver
이 Driver는 자료의 순차성을 가지고 있는 Hardware를 다룰 때 사용하며, 데이터를 문자 단위로 전달하고 읽어드린다. 대표적인 Hardware로는 터미널, 콘솔, 키보드 등이 있다.
file_operations 구조체는 Character Device Driver가 일반 프로그램과 통신을 하기 위해 제공되는 인터페이스다. read, write, open, release와 같은 함수 포인터를 사용할 수 있다.
static int chardev_open(struct inode *inode, struct file *file){
printk("chardev_open");
return 0;
}
struct file_operations chardev_fops={
.open = chardev_open,
};
다음과 같은 방법으로 module에서 open() 함수를 제공할 수 있다.
device를 등록하기 위해선 register_chrdev()
함수를 이용한다.
register_chrdev(MAJOR_NUM, DEVICE_NAME, &chardev_fops);
- Major_num : 위에서 명시한 major number
- Device_name : 등록할 Device의 이름
- chardev_fops : Device Driver의 operation들의 집합인 구조체다.
static struct file_operations chardev_fops={
.read = virtual_device_read,
.write = virtual_device_write,
.open = virtual_device_open,
.release = virtual_device_release
};
응용프로그램에서 open, read, write, close와 같은 연산을 수행한다면 Kernel 영역에서 system call을 호출한다. 이는 file_operations에 있는 함수를 호출하고, 이들은 mapping된 device driver의 함수를 호출한다.
Practice 1
open 함수를 사용할 수 있는 device driver를 등록해보자.
#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;
}
sturct 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);
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
baek@ubuntu:~/Kernel/chardev$ make
make -C /lib/modules/5.3.0-42-generic/build M=/home/baek/Kernel/chardev modules
make[1]: Entering directory '/usr/src/linux-headers-5.3.0-42-generic'
CC [M] /home/baek/Kernel/chardev/chardev.o
Building modules, stage 2.
MODPOST 1 modules
WARNING: modpost: missing MODULE_LICENSE() in /home/baek/Kernel/chardev/chardev.o
see include/linux/module.h for more information
CC /home/baek/Kernel/chardev/chardev.mod.o
LD [M] /home/baek/Kernel/chardev/chardev.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.3.0-42-generic'
mknod
명령어를 이용하여 적재된 Module을 Device 파일로 생성할 수 있다. open을 이용하는 명령어를 사용하여 chardev_open 함수가 호출되는지 살펴보자.
baek@ubuntu:~/Kernel/chardev$ sudo insmod chardev.ko
[sudo] password for baek:
baek@ubuntu:~/Kernel/chardev$ dmesg | tail
[15814.210903] Registeration is a success The major device number is 100.
[15814.210904] If you want to talk to the device driver,
[15814.210904] you'll have to create a device file.
[15814.210905] We suggest you use:
[15814.210905] mknod chardev c 100 0
[15814.210905] The device file name is important, because
[15814.210905] the ioctl program assumes that's the
[15814.210906] file you'll use.
baek@ubuntu:~/Kernel/chardev$ sudo mknod chardev c 100 0
baek@ubuntu:~/Kernel/chardev$ ls -al | grep "chardev"
crw-r--r-- 1 root root 100, 0 Apr 3 23:58 chardev
baek@ubuntu:~/Kernel/chardev$ sudo chmod 666 chardev
baek@ubuntu:~/Kernel/chardev$ echo 'hi' > chardev
bash: echo: write error: Invalid argument
baek@ubuntu:~/Kernel/chardev$ dmesg | tail
[15814.210903] Registeration is a success The major device number is 100.
[15814.210904] If you want to talk to the device driver,
[15814.210904] you'll have to create a device file.
[15814.210905] We suggest you use:
[15814.210905] mknod chardev c 100 0
[15814.210905] The device file name is important, because
[15814.210905] the ioctl program assumes that's the
[15814.210906] file you'll use.
[16060.615557] chardev_open
echo 명령어에 의해 chardev_open()
함수가 동작되었다.
Character Device Driver API
커널은 내부적으로 character device를 표현하기 위해서 cdev 구조체를 사용한다.
struct cdev{
struct kobject kobj;
struct module *owner;
const struct file_openration *ops;
struct list_head list;
dev_t dev;
unsigned int count;
}
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
alloc_chrdev_region()
: 디바이스 번호를 동적으로 할당하기 위한 함수
*dev
: 성공적으로 0을 리턴하면, dev에 device 번호가 할당된다.firstminor
: 첫번째 minor number로 보통 0이다.count
: device 갯수*name
: device name
void unregister_chrdev_region(dev_t first, unsigned int count);
unregister_chrdev_region()
: 사용중인 디바이스 번호를 해제한다.
void cdev_init(struct *cdev, struct file_operations *fops);
cdev_init()
: cdev 구조체를 초기화한다.
*cdev
: 초기화할 cdev 구조체*fops
: 등록할 fop 구조체 포인터
void cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
cdev_add()
: cdev 구조체를 커널에 등록, character device에 등록.
*cdev
: 등록할 cdev 구조체num
: 등록할 device 번호count
: 등록할 device 개수
int cdev_del(struct cdev *cdev);
cdev_del()
: 등록된 cdev를 제거한다.
Practice 2
조금 더 응용해보자.
#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.");
// allocate the number of character device
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);
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
baek@ubuntu:~/Kernel/chardev2$ ls -al /dev | grep "chardev"
crw------- 1 root root 240, 0 Apr 4 01:13 chardev0
crw------- 1 root root 240, 1 Apr 4 01:13 chardev1
device를 등록하였다. 이후 이 device를 사용하는 코드를 작성해보자.
#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, "L000Z1K", 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;
}
프로그램을 실행해보면, 모듈에서 작성한 모든 함수들이 동작하는 것을 확인할 수 있다.
baek@ubuntu:~/Kernel/chardev2$ gcc -o test test.c
baek@ubuntu:~/Kernel/chardev2$ ./test
baek@ubuntu:~/Kernel/chardev2$ dmesg | tail
[21062.482299] The chardev_open() function has been called.
[21062.482303] The chardev_write() function has been called.
[21062.482305] Before calling the copy_from_user() function : 00000000c23d3406, helloworld
[21062.482307] After calling the copy_from_user() function : 00000000c23d3406, L000Z1K
[21062.482308] The chardev_read() function has been called.
중요한 점은 write() 함수에 의해 Kernel의 Heap 영역에 data를 저장하고, read() 함수를 이용하여 해당 데이터를 출력했다는 것이다.
참고문헌
[1] https://www.lazenca.net/display/TEC/02.Character+Device+Drivers
'Linux Kernel' 카테고리의 다른 글
ioctl(Input/Output control) (0) | 2020.04.07 |
---|---|
Kernel Module (0) | 2020.04.04 |