reference: "리눅스 커널 내부구조" / 백승제, 최종무
설계: 메모리의 일부를 가상적인 디스크 디바이스 공간으로 사용.
간단한 가상 문자 디바이스 드라이버를 구현하여 이 공간에 쓰기와 데이터 읽기 연산 제공.
#include <linux/kernel.h> // 현재 디바이스 드라이버 프로그램은 커널 공간에서 모듈로써 동작하게 됨으로써
#include <linux/module.h> // kernel.h, module.h 헤더 필요
#include <linux/slab.h> // for kmalloc(), kfree() (커널 공간 할당/해제)
#include <linux/fs.h>
#include <linux/cdev.h> // 하나의 디바이스 드라이버를 관리하기 위한 리눅스 자료구조.
// ops 필드에는 드라이버 제공하는 file_operations 구조체 저장
#include <linux/device.h>
#include <asm/uaccess.h>
#define DEVICE_NAME "Mydrv" // 디바이스 드라이버 이름
#define MYDRV_MAX_LENGTH 4096 // 가상 디스크 공간으로 활용한 커널 공간의 크기
#define MIN(a, b) (((a) < (b)) ? (a) : (b)) // MIN() 메크로 함수
struct cdev *mycdev; // cdev 구조체
dev_t mydev; // for 디바이스 드라이버 등록
struct class *myclass; // for 장치파일
struct device *mydevice; // for 장치파일
static char *mydrv_data;
static int mydrv_read_offset;
static int mydrv_write_offset;
// file_operations 자료구조에 등록시킬 함수들
// open(), release() (=close), read(), write(), ioctl()
/*
* 커널 공간을 할당받아 가상의 디스크를 만들고, 이에 대한 디바이스 드라이브
* 따라서 mydrv_opne() 또는 mydrv_release()는 주 번호를 확인하는 것 외에 특별한 작업을 하지 않음.
* 만약 실제 물리적인 하드웨어를 구동하는 것이라면 하드웨어 초기화와 같은 작업들을 수행해야 함.
*/
static int mydrv_open(struct inode *inode, struct file *file){
printk("%s \n", __FUNCTION__);
return 0;
}
static int mydrv_release(struct inode *inode, struct file *file){
printk("%s \n", __FUNCTION__);
return 0;
}
/*
* mydrv_data 커널 공간에 데이터(문자열)을 쓰거나 읽는 작업
* 커널과의 블록 메모리 복사 함수인 copy_to_user, copy_from_user를 file_operations 구조체에 매핑시키기 위한 일종의 래퍼라 할 수 있음.
* copy_to_user(): Kernel 메모리를 User 메모리로 블록 데이터를 복사함.
* copy_from_user(): User 메모리를 Kernel 메모리로 블록 데이터를 복사함.
* mydrv_read(): 몇가지 예외 조건을 검사한 후, mydrv_data 커널 공간에 존재하는 데이터를 사용자에게 전달
* mydrv_write(): 몇가지 예외 조건을 검사한 후, mydrv_data 커널 공간에 사용자 요청 데이터를 씀. (write_offset 이동)
*/
static ssize_t mydrv_read(struct file *file, char *buf, size_t count, loff_t *ppos){
if( buf == NULL || count < 0 )
return -EINVAL;
if( mydrv_write_offset <= mydrv_read_offset )
return 0;
count = MIN((mydrv_write_offset-mydrv_read_offset), count);
if( copy_to_user(buf, mydrv_data + mydrv_read_offset, count) ) // 커널 메모리 => 유저 메모리, 블록 데이터 복사
return -EFAULT;
mydrv_read_offset += count;
return count;
}
static ssize_t mydrv_write(struct file *file, const char *buf, size_t count, loff_t *ppos){
if( buf == NULL || count < 0)
return -EINVAL;
if( count + mydrv_write_offset >= MYDRV_MAX_LENGTH )
return 0;
if( copy_from_user(mydrv_data + mydrv_write_offset, buf, count) ) // 유저 메모리 => 커널 메모리, 블록 데이터 복사
return -EFAULT;
mydrv_write_offset += count;
return count;
}
// 위에서 정의한 함수들의 시작 주소 등록
struct file_operations mydrv_fops = {
.owner = THIS_MODULE,
.read = mydrv_read,
.write = mydrv_write,
.open = mydrv_open,
.release = mydrv_release,
};
// 모듈 초기화 함수
int mydrv_init(void){
// alloc_register_chrdev() 함수를 통해 디바이스 드라이버 주 번호를 동적으로 할당 받음.
if( alloc_chrdev_region(&mydev, 0, 1, DEVICE_NAME) < 0 )
return -EBUSY;
// 커널에서는 udev를 통한 동적인 장치파일 생성/제거를 지원할 수도 있음.
// class_create(), device_create() 사용
myclass = class_create(THIS_MODULE, "mycharclass");
if(IS_ERR(myclass)){
unregister_chrdev_region(mydev, 1);
return PTR_ERR(myclass);
}
mydevice = device_create(myclass, NULL, mydev, NULL, "mydevicefile");
if(IS_ERR(mydevice)){
class_destroy(myclass);
unregister_chrdev_region(mydev, 1);
return PTR_ERR(mydevice);
}
mycdev = cdev_alloc(); // cdev 구조체 할당받기
mycdev->ops = &mydrv_fops; // 드라이버 랩퍼 함수들의 시작주소를 저장한 file_operations 구조체(mydrv_fops)를 등록
mycdev->owner = THIS_MODULE;
if( cdev_add(mycdev, mydev, 1) < 0) { // 초기화된 cdev 구조체를 커널에 등록
device_destroy(myclass, mydev);
class_destroy(myclass);
unregister_chrdev_region(mydev, 1);
return -EBUSY;
}
// 가상 디스크 공간으로 사용할 커널 공간 할당받기, 이름: mydrv_data
mydrv_data = (char *)kmalloc(MYDRV_MAX_LENGTH * sizeof(char), GFP_KERNEL);
// 오프셋 초기화
mydrv_read_offset = mydrv_write_offset = 0;
return 0;
}
// 모듈 제거 시 호출될 함수
void mydrv_cleanup(void){
kfree(mydrv_data);
cdev_del(mycdev);
device_destroy(myclass, mydev);
class_destroy(myclass);
unregister_chrdev_region(mydev, 1);
}
// 매크로(module_init, module_exit 사용, mydrv_init(), mydrv_exit() 지정)
module_init(mydrv_init);
module_exit(mydrv_cleanup);
MODULE_LICENSE("GPL");
디바이스 드라이버 컴파일
$ make
생성된 모듈 커널에 적재
$ insmod chr_test.ko
"insmod" 명령어를 통해 모듈을 커널에 적재한 후에 비로소 장치파일 생성
insmod 명령어를 통해 디바이스 드라이버를 커널에 적재하면 module_init() 매크로에 지정된 함수를 호출 => mydrv_open() 함수 호출
mydrv_open() 함수는 내부적으로 cdev_add() 호출, 커널 내부 자료구조인 cdev_map 자료구조에 가상 디스크 디바이스 드라이버의 file_operations 자료구조를 등록한다. 또한 장치파일은 udev를 잉요해 자동 생성된다.
사용자 애플리케이션에서 이 파일을 열고 사용하려한다면 우선 장치파일의 아이노드 객체를 찾아서 i_mode와 i_rdev를 확인한다.
문자 장치파일이므로 i_mode는 'S_IFCHR'이고, 문자 장치파일임을 인식한 후 디폴트 open인 chrdev_open()을 호출한다.
chrdev_open(): 문자형 디바이스 드라이버를 위한 자료구조인 cdev_map에서 i_rdev 필드에 저장된 주 번호를 인자로 하여 인덱싱하고, file_operations 자료구조를 찾는다. 그리고 이 구조체를 파일 객체의 f_op필드에 연결함으로써 추후 애플리케이션이 read()나 write()를 호출할 때 디바이스 드라이버를 만들 때 작성한 함수들(mydrv_read() 또는 mydrv_write() 등의 함수, mydrv_fops 내 등록)을 호출하게 한다.
(리눅스 내부작동)
1. 장치파일 이름을 인자로 파일 열기 요청.
2. 장치파일의 아이노드 객체를 통해 주 번호, 부 번호를 얻고 장치파일의 유형을 얻음.
3. 장치파일의 유형을 통해 cdev_map 자료구조에 접근하여 주 번호를 인자로 cdev 구조체 검색.
4. ops의 필드를 통해 file_operations 구조체에 등록된 open() 호출 => mydrv_open() 호출.
#include <stdio.h>
#include <fcntl.h>
#define MAX_BUF 26
char buf_in[MAX_BUF];
char buf_out[MAX_BUF];
int main(void){
int fd, i, c=65;
if( (fd=open("/dev/mydevicefile", O_RDWR)) < 0){ // 생성된 장치파일에 접근해 가상 장치 접근
perror("OPEN ERR");
return -1;
}
for(i=0; i<MAX_BUF; i++){
buf_out[i] = c++; // ABCDEFGH....
buf_in[i] = 65; // AAAAAAAA....
}
for(i=0; i<MAX_BUF; i++){
fprintf(stderr, "%c", buf_in[i]); // AAAAAA....
}
fprintf(stderr, "\n");
// 가상 장치에 I/O
write(fd, buf_out, MAX_BUF); // ABCDEFG.. 쓰기
read(fd, buf_in, MAX_BUF); // (커널 메모리 내)ABCDEFG.. 읽기
for(i=0; i<MAX_BUF; i++){
fprintf(stderr, "%c", buf_in[i]); // ABCDEFG..
}
fprintf(stderr, "\n");
close(fd);
return 0;
}