교재는 구글드라이브에 올라가 있다. (유영창_디바이스드라이버)
우리는 캐릭터디바이스를 제어하는 디바이스 드라이버를 건드려볼 것이다.
그렇기 때문에 교재의 1~14장을 볼 것이고, 추가로 17장 정도를 볼 것이다.
(우리 수업에서는 블록디바이스를 다루지는 않기 때문에, 나중에 현업가서 또 공부해야 한다.)
아참 그리고 교재에서 배포한 소스코드도 있는데, 우리는 그걸 사용할 수는 없다. 왜냐하면 교재 소스코드는 PC 전용이라서 I/O mapped I/O 방식으로 주변장치를 관리하고, 우리같은경우는 임베디드시스템이라서 memory mapped I/O 방식으로 주변장치를 관리한다. 그래서 그대로 가져다 쓸 수가 없게 되는 것이다. (그래서 교재 소스코드를 기반으로 하되, 그걸 우리의 상황에 맞게 개량하여 사용하는 식으로 수업을 진행하겠다.)
우리는 이제부터 /dev 경로에 있는 디바이스 드라이버를 건드릴 것이다.
디바이스 드라이버 파일은 그 파일의 메타정보만 사용하기 위해 존재하는 것이다.
디바이스 드라이버 파일 구조체의 인덱스에는 주번호가 부번호가 있다.
사실 디바이스 드라이버를 개발할때는 Virtual Box를 권장하지 않는다. 왜냐하면 버추얼 박스는 윈도우의 디바이스 드라이버를 한번 거쳐서 가도록 되어있기 때문에, 리눅스에서 지원하는 모든 디바이스 드라이버를 지원하지 못하기 때문이다. (대표적으로 우리가 저번에 인텔수업에서 NVidia GPU 디바이스 드라이버를 사용하기 위해서 Virtual Box가 아닌 native로 우분투를 올렸던 것이 바로 그 이유이다.)
우리는 스위치랑 키버튼으로 디바이스 드라이버를 실습할 것이다.
디바이스 드라이버를 만들면서 리눅스 소스 탐색을 하기 위해서 tags 파일을 만들겠다. 그리고 그걸 .vimrc에 추가해주겠다. (커널 소스 탐색을 하나하나 전부다 뒤져가면서 찾으려면 너무 말도 안되게 오래 걸린다.)
// 커널이 제공하는 Makefile 형식을 이용하여 ctags 명령 실행
ubuntu@ubuntu14:~/pi_bsp/kernel/linux$ ARCH=arm make tags
// .vimrc에 추가
ubuntu@ubuntu14:~/pi_bsp/kernel/linux$ vi ~/.vimrc
.vimrc에다가 set tags+=/home/ubuntu/pi_bsp/kernel/linux/tags
을 추가해주면 된다.
매번 새로운 시스템 콜을 추가하거나 하면 시스템을 재부팅 해주어야 했다. 비효율적이어었다.
커널 소스의 80%가 디바이스 드라이버에 관련된 코드이다. 새로운 디바이스가 생기면 그에 맞는 디바이스 드라이버가 추가되고, 오래된 디바이스 드라이버는 빠지고 그런식으로 관리된다.
표준 입출력함수인 fopen()같은 것들은 중간에 버퍼가 생겨서 버퍼를 통해 데이터를 읽어온다.
open(), close(), read(), write() 같은 저수준 입출력 함수는 중간에 버퍼를 두지 않고 실시간으로 디스크 데이터에 access한다. (키보드를 제어해야 하는데, 중간에 버퍼가 있으면, 즉각적인 제어가 안될 것이다.) 즉 다시말해, 하드웨어는 실시간으로 제어해야 하기 때문에 중간에 버퍼를 두지 않는 저수준입출력 함수를 통해서 제어하는 것이다.
우리는 보통 빈 파일을 만들 때 touch
명령어로 파일을 만든다.(물론 다른 방법도 여러가지 있다.)
그런데 디바이스 파일은 저런 방식으로 만드는게 아니다. 일반 파일은 무언가를 저장하기 위해 만드는 것이 목적이다. 하지만 디바이스 파일은 목적이 그냥 디바이스를 제어하기 위한 정보만 담는 것이다.
디바이스 파일은 무조건 /dev 아래 만들어져야 한다. 그리고 /dev 폴더의 소유권한은 root:root
이다. 그래서 일반계정에서 ls -l / 으로 /dev파일의 권한을 보면
drwxr-xr-x 19 root root 4220 2월 21 10:37 dev
이렇게 쓰기 권한이 없는 것을 볼 수 있다. 그래서 커널 실습은 root계정으로 하는게 편하다고 하는 거다. 근데 그 전에 먼저 root계정에 비밀번호를 부여해야겠다.
ubuntu@ubuntu14:~$ sudo passwd root
새 암호: root
새 암호 재입력: root
passwd: 암호를 성공적으로 업데이트했습니다
이제 비밀번호를 부여했으니 root계정으로 들어가보자.
ubuntu@ubuntu14:~$ su - root
암호:
root@ubuntu14:~# pwd
/root
여기서 눈여겨볼 것은 root계정의 홈 디렉토리
는 /home
아래 잡히는게 아니라 /root
로 잡힌다는 것이다 .
그리고 루트 계정은 콘솔 표시로 #
이 뜬다.(일반계정은 $
였다.)
그리고 su - root
와 su root
는 다르다.
su
명령어는 사용자를 변경하기 위해 사용되는 명령어입니다. 그러나 사용 방법에 따라 동작에 차이가 있습니다.
su - root
: 여기에서 -
옵션은 로그인 셸을 사용하여 지정된 사용자로 전환하라는 의미입니다. 따라서 root
사용자로 전환하면서 해당 사용자의 환경 변수와 프로파일 스크립트를 적용합니다. 이것은 새로운 로그인 세션을 생성하는 것과 유사합니다.
ubuntu@ubuntu14:~$ su - root
su root
: 여기에서는 -
옵션이 없으므로 사용자를 전환하면서 해당 사용자의 환경 변수나 프로파일 스크립트를 적용하지 않습니다. 현재 세션에서 사용자만 전환되고, 새로운 로그인 세션이 시작되지 않습니다.
ubuntu@ubuntu14:~$ su root
즉, -
옵션이 있는 경우 su - root
는 로그인 세션을 새로 시작하여 지정된 사용자의 환경을 완전히 적용하며, -
옵션이 없는 경우 su root
는 로그인 세션을 유지하면서 사용자만 전환합니다.
root@ubuntu14:~# mknod /dev/devfile c 240 1
디바이스 파일을 지우는 방법은 똑같이 rm
명령을 이용한다.
root@ubuntu14:~# rm /dev/devfile
그리고 디바이스 드라이버 파일을 열때는 그냥 open()을 날리는 것이 아니다.
예를 들어 위에 실습용으로 잠깐 만들었던 것을 보면 240번 주번호에 등록되어 있는 open()함수를 호출하는 식으로 연다(?)
그래서 앞으로 디바이스 파일을 활용하는데 자주 쓸 저수준 입출력 함수는 다음과 같다.
lseek()는 file descriptor의 위치를 바꾸겠다는 뜻이다.
앞으로 디바이스 드라이버 관련 소스는 ~/pi_bsp/drivers에 다 만들겠다.
ubuntu@ubuntu14:~/pi_bsp$ mkdir drivers ; cd drivers
앞으로 교재에 있는 함수를 실습할 때는 교재 페이지 번호로 폴더를 만들어서 진행하겠다.
ubuntu@ubuntu14:~/pi_bsp/drivers$ mkdir p87 ; cd p87
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int ret;
ret = mknod("/dev/devfile", S_IRWXU | S_IRWXG | S_IFCHR, (240<<8) | 1); // 상위 8비트는 주번호로, 하위 8비트는 부번호로 쓴다.
if (ret < 0)
{
perror("mknod()");
return -ret; // 시스템 콜의 에러코드는 음수로 나온다. 그런데 bash shell에서 볼 수 있는 결과 코드는 양수여야 제대로 보인다.(음수면 보수를 취해주면서 값이 이상하게 보인다.) 그래서 한번더 -를 붙여서 양수로 바꿔준다.
}
return 0;
}
잘 되는지 테스트 해보자.
ubuntu@ubuntu14:~/pi_bsp/drivers/p87$ ./mymkond
mknod(): Permission denied
ubuntu@ubuntu14:~/pi_bsp/drivers/p87$ echo $?
1
ubuntu@ubuntu14:~/pi_bsp/drivers/p87$ sudo ./mymkond
ubuntu@ubuntu14:~/pi_bsp/drivers/p87$ ls -l /dev/devfile
crwxr-x--- 1 root root 240, 1 2월 21 14:31 /dev/devfile
ubuntu@ubuntu14:~/pi_bsp/drivers/p87$ sudo ./mymkond
mknod(): File exists
ubuntu@ubuntu14:~/pi_bsp/drivers/p87$ sudo rm /dev/devfile
당연히 sudo
를 붙여야 실행이 권한이 허가된다. 왜냐하면 디바이스 드라이버 파일은 오직 root
만 생성할 수 있기 때문이다. 그리고 확실히 코드에서 perror()
를 사용하니까 에러 표시가 정말 친절하게 나오는 것을 확인할 수 있다.
디바이스 드라이버는 모듈로 관리된다고 했다.
교재 코드를 실습해 볼 때 우리는 커널2.4 말고 커널 2.6으로 작성해야 한다.
아래는 작성한 코드 내용이다.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int hello_init(void)
{
printk("Hello, world\n");
return 0;
}
static void hello_exit(void)
{
printk("Goodbye, world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
106페이지 파일을 Makefile로 컴파일했다.
MOD := hello
obj-m := $(MOD).o
CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
#KDIR := /lib/modules/$(shell uname -r)/build
KDIR := /home/ubuntu/pi_bsp/kernel/linux
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules $(CROSS)
cp $(MOD).ko /srv/nfs
clean:
rm -rf *.ko
rm -rf *.mod.*
rm -rf .*.cmd
rm -rf *.o
rm -rf modules.order
rm -rf Module.symvers
rm -rf $(MOD).mod
rm -rf .tmp_versions
이 Makefile로 make를 하면 타겟보드인 라즈베리파이에 맞게 arm으로 컴파일이 된다. 그리고 이어서 생성된 .ko
파일(kernel module
)을 자동으로 NFS를 통해 라즈베리파이로 넘기도록 되어있다.
이제 라즈베리파이에 넘어가서 파일을 확인해서 정보를 출력해보면 아래와 같이 나오는 것을 확인할 수 있다.
pi@pi14:/mnt/ubuntu_nfs $ file hello.ko
hello.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=52fa1dd75713d0830f6aa3ac39b707285a7fd4ac, not stripped
hello.ko
파일은 실행파일이 아니다. 'excutable'과 'relocatable'은 다르다. relocatable 파일은 모듈로 메모리에 적재되어야 실행이 가능하다. 그래서 아래의 명령어를 통해서 메모리에 적재를 하고 실행해야 한다.
// 기존의 모듈이 몇개가 있는지 확인해보면 84개의 모듈이 있다는 것을 알 수 있다.
pi@pi14:/mnt/ubuntu_nfs $ lsmod | wc -l
84
// insmod: insert module 동적으로 모듈을 재배치하겠다는 것이다.
pi@pi14:/mnt/ubuntu_nfs $ sudo insmod hello.ko
// 컴파일 해온 hello.ko파일을 재배치함으로써 모듈이 한개 늘어났다.
pi@pi14:/mnt/ubuntu_nfs $ lsmod | wc -l
85
// 모듈을 삭제하겠다.(삭제할때는 .ko를 붙이면 안된다.)
pi@pi14:/mnt/ubuntu_nfs $ sudo rmmod hello
// 다시 모듈 갯수를 확인해보니 원래대로 84개가 되었다.
pi@pi14:/mnt/ubuntu_nfs $ lsmod | wc -l
84
그리고 나서 dmesg
로 커널 메시지를 확인해보면, hello.c
코드에서 작성했던대로 커널메시지가 출력되어있는 것을 확인할 수 있다.
pi@pi14:~$ dmesg
...
[15112.876484] hello: loading out-of-tree module taints kernel.
[15112.876828] Hello, world
[15231.298661] Goodbye, world
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#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 val);
static void gpioLedFree(void);
static int gpioKeyInit(void);
static int gpioKeyGet(void);
static void gpioKeyFree(void);
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 gpio_request() gpio%d error\n", i);
return ret;
}
ret = gpio_direction_input(gpioKey[i]);
if (ret < 0)
{
printk("Failed gpio_direction_input() gpio%d error\n", i);
return ret;
}
}
return ret;
}
static int gpioKeyGet(void)
{
// 여기서 for문 돌면서 순회해서 찾는거네
int ret;
int i;
int keyData = 0x00;
for (i = 0; i < GPIOKEYCNT; i++)
{
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 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) & 0x01);
}
}
static void gpioLedFree(void)
{
int i;
for (i = 0; i < GPIOLEDCNT; i++)
{
gpio_free(gpioLed[i]);
}
}
/* 모듈 코드는 꼭 맨 마지막에 와야한다 */
static int led_init(void)
{
int ret;
printk("Hello, world\n");
// LED 관련
ret = gpioLedInit();
if (ret < 0)
return ret;
gpioLedSet(0xff);
return 0;
}
static void led_exit(void)
{
printk("Goodbye, world\n");
// LED 관련
gpioLedSet(0x00);
gpioLedFree();
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("Dual BSD/GPL");
static으로 선언한 모든 것들(static지역변수, static전역변수, static함수)은 모두 해당 값에 대한 메모리가 stack영역이 아닌 data영역에 잡힌다는 것이 핵심이다. 다만 그 접근권한을 제한하겠다는 것이다.
이 얼마나 심플한 설명인가...
결국 그래서 모듈 프로그램은 전부 static으로 정의하는 것이다. 왜냐하면 다른 모듈 사이에 간섭이 일어나면 안되기 때문이다. 그래서 모든 함수와 변수를 static으로 선언함으로써 다른 모듈끼리의 간섭을 차단할 수 있다. 이것이 모듈 프로그래밍의 핵심 관습 중 하나이다.
그리고 모듈 프로그래밍에서
module_init();
,module_exit();
,MODULE_LICENSE("Dual BSD/GPL");
은 반드시 코드의 맨 끝에 오는 것이 관습 중 하나이다.
안녕하세요. 교재 링크는 어디에 공개되어 있는지 궁금합니다.