디바이스 드라이버 소스파일 내의 init(), exit(), open(), release() 함수에 대해서 말하는 것이다.
먼저 접근 가능한 메모리인지 확인을 해보고 접근을 해야한다. 안그러면 segmentation fault
가 나버린다. 가장 fatal한 상황이다. 그래서 커널에서 pointer를 사용할 때는 정말 신중하게 사용해야 한다.
#include <linux/ioport.h>
하면 아래 함수를 통해서 체크해볼 수 있다.
이거 말고도 다양하게 더 있다.
우분투 가장머신에서 아래 명령어를 쳐보자.
라즈베리파이 커널에서 아래 명령어를 쳐보자.
정말 중요한 개념도이다. 여기서도 핵심이 되는 것은 put_user()
, copy_to_user()
, get_user()
, copy_from_user()
이 4개의 함수이다.
사용자 어플리케이션은 그 프로세스 주소 공간에서만 제어 가능하고 커널 영역의 메모리 공간에 접근할 수 없다. 마찬가지로 커널에서도 함부로 사용자 어플리케이션 주소공간에 맘대로 접근하면 안된다.
즉 서로 자기 영역의 메모리 공간이 아니기 때문에, 그 메모리 공간이 가르키는 정보에 직접 접근하여 읽거나 쓰지 않고, 중간에 버퍼를 두고 서로의 주소 공간을 복사하여 사용하는 식으로 운영하게 되는 것이다.
그래서 필요한 함수들이 put_user()
, copy_to_user()
, get_user()
, copy_from_user()
이다. 꼭 이 4개의 함수를 가지고 어플리케이션과 디바이스드라이버가 서로 읽기/쓰기가 이루어져야 한다.
교제 페이지 232쪽부터 읽어보면 왜 그런지 그 이유를 알 수 있다.
우리는 우선 open(), close(), read(), write()만 가지고 해보겠다.
/dev/ledkey c 230 0
이라고 만들면 되도록read(dev, buf, size)
로 key값 읽기write(dev, buf, size)
로 key값 쓰기#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/gpio.h> // gpio
#define LEDKEY_DEV_NAME "ledkey_dev"
#define LEDKEY_DEV_MAJOR 230 // 디바이스 주번호
#define OFF 0
#define ON 1
#define GPIOLEDCNT 8
#define GPIOKEYCNT 8
static int gpioLed[GPIOLEDCNT] = {6,7,8,9,10,11,12,13};
static int gpioKey[GPIOKEYCNT] = {16,17,18,19,20,21,22,23};
static int gpioLedInit(void);
static void gpioLedSet(long);
static void gpioLedFree(void);
static int gpioKeyInit(void);
static int gpioKeyGet(void);
static void gpioKeyFree(void);
static int gpioLedInit(void)
{
int i;
int ret=0;
char gpioName[10];
for(i=0;i<GPIOLEDCNT;i++)
{
sprintf(gpioName,"led%d",i);
ret = gpio_request(gpioLed[i],gpioName);
if(ret < 0) {
printk("Failed gpio_request() gpio%d error \n",i);
return ret;
}
ret = gpio_direction_output(gpioLed[i],OFF);
if(ret < 0) {
printk("Failed gpio_direction_output() gpio%d error \n",i);
return ret;
}
}
return ret;
}
static void gpioLedSet(long val)
{
int i;
for(i=0;i<GPIOLEDCNT;i++)
{
gpio_set_value(gpioLed[i],(val>>i) & 0x1);
}
}
static void gpioLedFree(void)
{
int i;
for(i=0;i<GPIOLEDCNT;i++)
{
gpio_free(gpioLed[i]);
}
}
static int gpioKeyInit(void)
{
int i;
int ret=0;
char gpioName[10];
for(i=0;i<GPIOKEYCNT;i++)
{
sprintf(gpioName,"key%d",gpioKey[i]);
ret = gpio_request(gpioKey[i], gpioName);
if(ret < 0) {
printk("Failed Request gpio%d error\n", 6);
return ret;
}
}
for(i=0;i<GPIOKEYCNT;i++)
{
ret = gpio_direction_input(gpioKey[i]);
if(ret < 0) {
printk("Failed direction_output gpio%d error\n", 6);
return ret;
}
}
return ret;
}
static int gpioKeyGet(void)
{
int i;
int ret;
int keyData=0;
for(i=0;i<GPIOKEYCNT;i++)
{
// ret=gpio_get_value(gpioKey[i]) << i;
// keyData |= ret;
ret=gpio_get_value(gpioKey[i]);
keyData = keyData | ( ret << i );
}
return keyData;
}
static void gpioKeyFree(void)
{
int i;
for(i=0;i<GPIOKEYCNT;i++)
{
gpio_free(gpioKey[i]);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
static int ledkey_open(struct inode *inode, struct file *filp)
{
int num0 = MINOR(inode->i_rdev);
int num1 = MAJOR(inode->i_rdev);
printk("ledkey open-> minor : %d\n", num0);
printk("ledkey open-> major : %d\n", num1);
try_module_get(THIS_MODULE);
return 0;
}
static loff_t ledkey_llseek(struct file *filp, loff_t off, int whence)
{
printk("ledkey llseek -> off : %08X, whence : %08X\n", (unsigned int)off, whence);
return 0x23;
}
static ssize_t ledkey_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
printk("ledkey read -> buf : %08X, count : %08X \n", (unsigned int)buf, count);
char kval;
kval = gpioKeyGet();
printk("%d\n", kval);
put_user(kval, buf);
// result = copy_to_user(buf, &kval, count);
return count;
}
static ssize_t ledkey_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
// 밖에서 저수준 입출력 함수로 write()할때는 void* 형으로 buf를 전달했는데, 이 함수와 연결되면서 buf가 const char* 형으로 강제 형변환되었다.
// const char *buf라고 선언을 해둬야 여기 코드 안에서 실수로 *buf가 가리키는 공간을 바꿔버리는 것을 방지하기 위함이다.
// 즉, 함수의 파라미터를 구성할 때 원본데이터가 되는 src를 const로 선언하여 수정되는 것을 방지해주는 것은 일반적인 방법이다. 아래의 strcpy() 함수를 보면 제대로 알 수 있다.
/*
char * strcpy(char * dest,const char *src)
{
char *tmp = dest;
while ((*dest++ = *src++) != '\0')
;
return tmp;
}
*/
// 이러한 개념 원리가 이 코드에도 적용되어 있는 것이다.
// const로 선언 안하고 Readonly 영역에 write할려고 하면 그 순간 segmentation fault가 일어나면서 시스템이 죽어버린다. 너무나 fatal한 상황인 것이다. 하지만 코드상에 Readonly 영역 포인터를 const로 선언해놓고 거기에 무언가 값을 write하려고 하면 컴파일 순간에 컴파일 에러가 난다. 즉 프로그램이 돌면서 segmentaion fault가 나기 전에 컴파일 타임에 error를 발생시켜서 좀더 graceful하게 실수를 방지할 수 있다는 것이다.
printk("ledkey write -> buf : %08X, count : %08X \n", (unsigned int)buf, count);
#if 0
gpioLedSet(*buf); // 어플리케이션에서 할당한 값을 참조하여 불을 켬
// buf라는 포인터 변수는 커널 공간에 할당된 것이 맞다.
// 하지만 buf라는 포인터 변수 안에 담긴 주소 값은 사용자 공간이라는 것이 핵심이다.
// 근데 이렇게 직접접근 하지 말라고 했다. 반드시 값을 복사 받아서 사용하라고 했다.
// 물론 그냥 이렇게 해도 실행이 되긴 한다. 근데 이러면 안된다는 것이다. 그 이유는 교재232쪽을 보면 알수 있다.
#else
// 버퍼가 단 1바이트라면...
char kbuff;
get_user(kbuff, buf); // kbuff는 '일반 변수'이고, buf는 '포인터'이다.
// 만약 버퍼가 1바이트가 아니라 여러 바이트였다면...
/* char kbuff[10];
int i;
for(i=0;i<count;i++)
get_user(kbuff[i],buf++);
*/
/* char kbuff[10];
copy_from_user(kbuff,buf,count);
*/
/* int result;
result = copy_from_user(&kbuff, buf, count);
*/
gpioLedSet(kbuff); // gpioLedSet은 long을 받기를 기대하고 있는데 char을 넣어줬다. 이건 문제가 안된다.
#endif
return count; // 실제로 읽은 바이트 수를 리턴해야 한다.
}
static long ledkey_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
printk("ledkey ioctl -> cmd : %08X, arg : %08X\n", cmd, (unsigned int)arg);
return 0x53;
}
static int ledkey_release(struct inode *inode, struct file *filp)
{
printk("ledkey release \n");
module_put(THIS_MODULE);
return 0;
}
// 구조체다
struct file_operations ledkey_fops = {
//.owner = THIS_MODULE,
.llseek = ledkey_llseek,
.read = ledkey_read,
.write = ledkey_write,
.unlocked_ioctl = ledkey_ioctl,
.open = ledkey_open,
.release = ledkey_release, // 저수준 입출력함수에서 close()가 .release에 대응한다.
};
static int ledkey_init(void)
{
int result;
printk("ledkey ledkey_init \n");
result = gpioLedInit();
if(result < 0)
return result;
result = gpioKeyInit();
if(result < 0)
return result;
// 캐릭터 디바이스를 메모리에 적재시키는 명령
result = register_chrdev(LEDKEY_DEV_MAJOR, LEDKEY_DEV_NAME, &ledkey_fops); // ledkey_fops의 주소값을 등록
if (result < 0)
return result;
return 0;
}
static void ledkey_exit(void)
{
printk("ledkey ledkey_exit \n");
unregister_chrdev(LEDKEY_DEV_MAJOR, LEDKEY_DEV_NAME);
gpioLedFree();
gpioKeyFree();
}
module_init(ledkey_init);
module_exit(ledkey_exit);
MODULE_AUTHOR("KCCI");
MODULE_DESCRIPTION("Device Driver pracite~!");
MODULE_LICENSE("Dual BSD/GPL");
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#pragma GCC diagnostic ignored "-Wunused-result" // scanf()쓸 때 리턴값 안받는걸로 경고 띄위지 말라는 것
int main(int argc, char *argv[])
{
int i;
int ledkey_fd;
int key_data, key_data_old = 0;
unsigned long val = 0;
char buff;
// 예외처리1
if(argc < 2)
{
printf("USAGE : %s ledVal[0x00~0xff]\n", argv[0]);
return 1;
}
// 예외처리2
val = strtoul(argv[1], NULL,16);
if(val < 0 || 0xff < val)
{
printf("Usage : %s ledValue[0x00~0xff]\n", argv[0]);
return 2;
}
// 실제 프로그램 실행 후 예외처리3
ledkey_fd = open("/dev/ledkey", O_RDWR | O_NONBLOCK);
if(ledkey_fd < 0)
{
perror("open()");
return 3;
}
// 우선 LED 켜보기
buff = (char)val; // 버퍼에 val값을 담아서 led를 켜는 디바이스 드라이버 호출
write(ledkey_fd, &buff, sizeof(buff)); // 여기서 &buff는 void* 파라미터에 대응하는 것이다.
do
{
usleep(100000);
// 이 함수를 읽어오는거랑 쓰는걸로 찢어야 한다.
// key_data = syscall(__NR_mysyscall, val);
read(ledkey_fd, &buff, sizeof(buff));
key_data = buff;
if(key_data != key_data_old)
{
key_data_old = key_data;
if(key_data)
{
write(ledkey_fd, &buff, sizeof(buff)); // 여기서 &buff는 void* 파라미터에 대응하는 것이다.
puts("0:1:2:3:4:5:6:7");
for(i=0;i<8;i++)
{
if(key_data & (0x01 << i))
putchar('O');
else
putchar('X');
if(i != 7 )
putchar(':');
else
putchar('\n');
}
putchar('\n');
}
if(key_data == 0x80)
break;
}
} while(1);
printf("mysyscall return value = %#04x\n",key_data);
return 0;
}
지금까지의 어플리케이션 프로그램은 안에서 do~while()문이 매우 빠른 속도로 무한루프 돌면서 폴링하면서 하다보니까 $top
으로 CPU 점유율을 찍어보면, 우리의 프로그램이 하나의 코어를 거의 100% 점유하고 있는 상황을 볼 수 있었다.
그래서 blocking I/O를 통해서 프로세스를 sleep모드로 바꿔버릴 것이다.
그리고 HW interrupt가 들어왔을 때만 sleep모드에서 탈출하도록 하는 것이 올바른 구현 방향이다.