12.1 Linux 상에서 디바이스의 정의
1. Linux 상에서 제어 가능한 디바이스 종류
Linux에서 제어하는 디바이스는 윗 그림의 밑 부분 Device 3가지이다.
디바이스 이름 | 설명 | 디바이스 예 |
Block Device | 중간에 있는 버퍼를 통해 디바이스 제어에 순서를 마음대로 조정할 수 있는 디바이스들 | 하드 디스크 (대부분 메모리 관련 디바이스들) |
Character Device | 중간에 버퍼가 없기 때문에, 디바이스 제어 순서를 마음대로 조정할 수 없는 디바이스들 | 키보드, 마우스, LED, 버튼 |
Network Device | 성격은 Block Device와 유사하나, 중간에 있는 TCP/IP 프로토콜 스택에 의해 관리되는 디바이스들 | Ethernet, Wi-Fi |
2. Linux에서 디바이스를 정의하기 위해 필요한 것들
-1. 디바이스 그룹(Device Group) 정보
가장 먼저 제어하려는 디바이스가 Block, Character, Network Device 그룹 중 어디에 속하는 지에 대한 정보가 필요하다.
COM1 시리얼 포트 디바이스는 Character Device이다. 순서가 중요하기 때문이다.
시리얼 통신 포트를 통해 A->B->C라는 순서로 데이터를 전송했다고 가정하면, 데이터를 수신하는 디바이스 드라이버가 데이터를 수신 받는 순서를 마음대로 조작했을 경우에 통신이 되지 않는다.
-2. 디바이스 종류 정보: Major Number
디바이스 드라이버에서 제어하려는 디바이스를 정의하기 위해서는 같은 그룹 내에 있는 많은 종류의 디바이스들 중에서 자신이 제어해야 할 디바이스가 어떤 종류에 속하는지를 알기 위해 번호를 붙여 관리한다.
운영체제의 본래 목적이 수 많은 프로세스를 관리하기 위함이므로, 운영체제가 프로세스들을 관리하기 위해 붙인 번호를 PID(Process ID)라고 한다. 마찬가지로 Linux에서 그룹 내 디바이스 종류들을 구분하기 위해 붙이는 번호를 Major Number라고 한다.
-3. 디바이스 구분 정도: Minor Number
PC 케이스 뒷면에 25핀 Femal으로 되어 있는 커넥터(“프린터 포트”)와 함께 COM1, COM2 두 개의 9핀 Male 시리얼 커넥터가 있었다. 요즘엔 USB 포트를 시리얼 포트로 변환해주는 USB-to-Serial 변환 케이블을 통해 COM3, COM4, COM5 ... 등 가상 시리얼 포트들도 계속 추가적으로 확장될 수 있다.
이 경우, 우리가 제어하려는 디바이스가 COM1 포트인지 아니면 COM3 포트인지 가려낼 수 있어야 한다.
따라서 디바이스를 제어하기 위해 마지막으로 필요한 정보는 같은 종류의 디바이스들 중에서 실제 제어해야 할 디바이스를 구분하기 위한 정보가 필요하며, 이 역시 Major Number와 같은 번호로 되어 있다. 이를 Minor Number라고 한다.
리눅스 커널 2.4에서는 Major Number + Minor Number에 각각 8비트 씩 총 16비트를 할당하여 디바이스들을 관리하였으나, 갈수록 늘어나는 디바이스 종류에 부응하기 위해
리눅스 커널 2.6에서는 Major Number 12비트, Minor Number에 20비트, 총 32비트를 할당하여 기능이 확장되었다.
세 가지 정보(디바이스 그룹 정보, 종류 정보, 구분 정보)는 “디바이스 파일”에 제작되어 User Application과 Device Driver 간 연동을 담당하는 역할을 한다.
12.2 디바이스 드라이버 구동 과정 분석
1. 디바이스 드라이버 등록
가장 먼저 수행해야 할 사항은 디바이스를 제어할 디바이스 드라이버를 리눅스 커널 내에 등록(Registeration) 시키는 것이다. 실제 디바이스를 제어하는 것은 리눅스 커널이 아니라 커널 내 등록되어 있는 디바이스 드라이버이기 때문이다.
등록 시키는 방법은 Kernel Module 형태로 컴파일한 뒤 해당 *.ko 파일을 “insmod” 명령어를 통해 커널 메모리 영역 내에 삽입(추가)하는 형태로 등록 시키거나,
아예 커널 소스 내에 포함 시켜 같이 컴파일 시키는 방법이 있다.
주로 개발 초기에 Kernel Module 형태로 개발을 하고, 개발이 모두 종료되면 커널 소스 내에 포함 시켜 컴파일 하거나, 혹은 Kernel Module 파일을 시스템이 부팅 할 때 자동적으로 삽입되도록 “insmod” 명령어 실행 부분을 미리 시스템 부팅 스크립트 파일들(ex /etc/inittab)에 기입해 놓는 방식으로 개발이 된다.
2. 디바이스 파일 생성
Linux는 자신이 관리해야 할 모든 자원(Resource)들을 “파일” 단위로 관리한다. 일반 파일들은 물론 실행되고 있는 프로세스들도 모두 파일 단위로 관리한다. 마찬가지로, 주변 디바이스들 역시 파일로 관리하고 있으며, 이 때 사용되는 파일을 “디바이스 파일”이라고 한다.
앞서 디바이스 파일은 실제 디바이스를 가리키는 C 언어에서의 포인터와 같은 역할을 한다고 했다. User Application이 직접 주변 디바이스를 제어하거나 접근할 수 없기 때문에, 간접적으로 디바이스 파일을 통해 해결할 수 밖에 없다. 즉, User Application이 디바이스에 무언가 데이터를 쓰고 싶다면, 디바이스와 연결된 디바이스 파일에 쓰면 된다.
이러한 디바이스 파일은 일명 “디바이스 노드(Device Node)” 라고도 불리며, “mknod” 명령어를 통해 생성할 수 있다.
3. 디바이스 파일 접근
User Application은 가상 메모리 기법에 의해 직접적으로 디바이스에 접근할 수 없다. 따라서 실제 디바이스에 접근하여 User Application이 원하는 대로 제어하는 것을 Device Driver가 담당하게 된다. 이러한 Device Driver는 스스로(독립적으로) 실행될 수가 없다. 오직 접근하려고 할 때 리눅스 커널이 이를 알아채고 실행시켜주어야만 한다.
알아채는 방식은 주변 디바이스와 일대일로 연결(링크)되어 디바이스 파일을 User Application이 open() 함수를 이용하여 열게(Open) 되면, open() 함수를 서비스해주기 위해 리눅스 커널이 해당 파일로 접근하게 되고, 이 파일이 디바이스 파일임을 알게 되면 해당 디바이스를 제어하겠다고 자신(Kernel)에게 등록한 Device Driver들을 검색하게 된다. 만약 해당 Device Driver를 찾지 못하게 되면, 서비스를 요청한 User Application에 오류 메세지를 보내준 후 프로그램을 종료시키고, 해당 Device Driver를 찾게 되면 실행시켜 연동되도록 서비스해준다.
4. 디바이스 드라이버 검색
User Application이 특정 디바이스 파일을 열게 되면, 해당 디바이스 파일과 연결(링크)되어 있는 디바이스를 제어하겠다고 자신(Linux Kernel)에게 미리 등록한 Device Driver가 있는지 각 디바이스(Device Character, Block, Network) 그룹별로 검색하게 된다.
5. 디바이스 드라이버 연동
검색되었다면 이를 실행시켜 User Application과 연동되게 해준다. 이후부터 User Application이 해당 디바이스 파일에 파일 제어 함수들을 이용하여 제어 요청을 하게 되면, Device Driver가 해당 요청을 받아 직접 디바이스를 제어하게 된다.
12.3 “SKELETON” 디바이스 드라이버
1. 소스 코드
Device Driver를 공부하기 위해 알아야 할 소스 코드의 종류는 3가지
1. Device Driver 소스 코드
2. 연동될 User Application 소스 코드 ( Device Driver는 해당 디바이스를 사용할 수 있도록 지원하기 위함)
3. Device Driver와 User Application 소스 코드를 함께 컴파일할 수 있는 Makefile
(1) Device Driver 소스 코드
가상의 디바이스 “SKELETON”을 제어하기 위한 소스
/**
** SKELETON Device Driver Example for Linux Kernel 2.6.x
**
** -- modified & tested by EROS.YOO.
**/
#include <linux/module.h> 헤더파일 전체 Device Driver를 구현하기 위해 필요한 헤더파일
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/fs.h> /** ... for MAJOR(), MINOR() macro **/
#include <asm/uaccess.h> /** ... for copy_to,from_user() **/
#include <asm/io.h> /** ... for in/b/w/l(), out/b/w/l/() **/
int SKELETON26_MAJOR = 0;
int skeleton26_open (struct inode *inode, struct file *filp)
/* 초기화 함수 : Device를 초기화하기 위해 필요한 함수*/
{
printk(KERN_INFO "SKELETON26: skeleton26_open() is called.. \n");
// MOD_INC_USE_COUNT; ---> for Linux Kernel 2.4.x
printk(KERN_INFO "SKELETON26: \t Major number = %d \n", MAJOR(inode->i_rdev));
printk(KERN_INFO "SKELETON26: \t Minor number = %d \n", MINOR(inode->i_rdev));
return 0;
}
int skeleton26_release (struct inode *inode, struct file *filp)
/* 종료 함수 : Device의 사용을 종료하기 위해 필요한 함수 */
{
printk("SKELETON26: skeleton26_release() is called.. \n");
// MOD_DEC_USE_COUNT; ---> for Linux Kernel 2.4.x
return 0;
}
ssize_t skeleton26_read (struct file *filp, char *buf, size_t count, loff_t *f_pos)
/* 읽기 함수 : Device로부터 데이터를 읽어 User Application에 해당 데이터를 전달해 주기 위한 함수*/
{
char *dev_data = "ABCD";
int err;
// dev_data = kmalloc(count, GFP_KERNEL);
// because 'dev_data' has initial data("ABCD"), we don't have to kmalloc
if( (err = copy_to_user(buf, dev_data, count)) < 0 )
return err;
/**
** copy_to_user(to, from, n)
**/
printk(KERN_INFO "SKELETON26: skeleton26_read() is called.. \n");
// kfree(dev_data);
return count;
}
ssize_t skeleton26_write (struct file *filp, const char *buf, size_t count, loff_t *f_pos)
/* 쓰기 함수 : User Application으로부터 받은 데이터를 실제 Device에 쓰기 위해 필요한 함수*/
{
char *dev_data;
int err;
dev_data = kmalloc(count, GFP_KERNEL);
if( (err = copy_from_user(dev_data, buf, count)) < 0 )
return err;
/**
** copy_from_user(to, from, n)
**/
printk(KERN_INFO "SKELETON26: skeleton26_write() is called.. \n");
printk(KERN_INFO "SKELETON26: \t User write data = %s \n", dev_data);
kfree(dev_data);
return count;
}
int skeleton26_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
/* 제어 함수 : User Applicaton으로부터 별도의 명령어를 통해 Device를 제어하고자 할 때 필요한 함수 */
{
printk(KERN_INFO "SKELETON26: skeleton26_ioctl() is called.. \n");
switch(cmd)
{
case 1:
{
printk("\n");
printk("SKELETON26: Keyboard = [1] \n");
break;
}
case 2:
{
printk("\n");
printk("SKELETON26: Keyboard = [2] \n");
break;
}
case 3:
{
printk("\n");
printk("SKELETON26: Keyboard = [3] \n");
break;
}
case 4:
{
printk("\n");
printk("SKELETON26: Keyboard = [4] \n");
break;
}
default:
return 0;
}
return 0;
}
struct file_operations skeleton26_fops = {
/* 구조체 Device Driver에서 지원하는 각 함수들의 구조체*/
/**
** for Linux Kernel 2.4.x
-----------------------------
open : skeleton26_open,
release : skeleton26_release,
read : skeleton26_read,
write : skeleton26_write,
ioctl : skeleton26_ioctl,
-----------------------------
**/
.open = skeleton26_open,
.release = skeleton26_release,
.read = skeleton26_read,
.write = skeleton26_write,
.ioctl = skeleton26_ioctl,
};
int skeleton26_init(void)
/* 등록 함수 : Device Driver를 리눅스 커널에 등록하기 위해 필요한 함수와 함수 매크로 */
{
int result;
printk(KERN_INFO "SKELETON26: skeleton26_init() is called.. \n");
result = register_chrdev(SKELETON26_MAJOR, "SKELETON26", &skeleton26_fops);
if (result < 0) {
printk(KERN_WARNING "SKELETON26: \t Can't get major number! \n");
return result;
}
if(SKELETON26_MAJOR == 0)
SKELETON26_MAJOR = result;
printk("SKELETON26: SKELETON26_MAJOR = %d \n", SKELETON26_MAJOR);
return 0;
}
void skeleton26_exit(void)
/* 해제 함수 : Device Driver를 리눅스 커널에서 삭제하기 위해 필요한 함수와 함수 매크로 */
{
unregister_chrdev(SKELETON26_MAJOR, "SKELETON26");
}
module_init(skeleton26_init);
module_exit(skeleton26_exit);
MODULE_LICENSE("Dual BSD/GPL");
🔸 헤더 파일
Kernel Module에서 살펴본 <linux/module.h> <linux/kernel.h> <linux/init.h>
Device Driver도 기본적으로 Kernel Module의 문법을 따른다.
<linux/errno.h> | 에러 정보(번호)를 출력하기 위해 필요한 헤더 파일 |
<linux/fs.h> | Device Driver의 Major Number를 알아내기 위한 함수 매크로인 MAJOR()와 Minor Number를 알아내기 위한 함수 매크로 MINOR()를 사용하기 위해 필요한 헤더파일 |
<asm/uaccess.h> | Device Driver에서 읽기, 쓰기를 구현하기 위해 필요한 copy_to_user(), copy_from_user() 함수들을 사용하기 위해 필요한 헤더파일 |
<asm/io.h> | 실제로 디바이스에서 읽기, 쓰기를 구현하기 위해 필요한 in(), out() 함수들을 사용하기 위해 필요한 헤더 파일 |
uaccess.h, io.h 헤더 파일들은 디렉토리 이름에서도 알 수 있듯이 하드웨어(Assembly) 종속적인 내용들을 다루는 헤더들만 별도로 모아 놓은 “asm” 디렉토리 미에 정의된 헤더 파일.
이 헤더 파일들은 주로 시스템 성능을 위해 C 언어가 아닌 Assemby 언어로 구현된 경우가 많은데, 가령 CPU 외부에 있는 주변 디바이스에 메모리 연산을 통해 특정 데이터를 읽고 쓰려면 다른 작업에 비해 상당한 시간이 필요하게 되므로, 조금이라도 시간을 줄이기 위해 C언어가 아닌 Assembly 언어로 구현하는 경우가 많다. 이때 사용하는 함수가 in(), out() 함수들인데 이들은 읽거나 쓰는 데이터의 사이즈에 따라 다음과 같이 세분화 한다.
읽기 | 쓰기 | ||
inb() | Byte 단위 | outb() | Byte 단위 |
inw() | Word 단위 | outw() | Word 단위 |
inl() | Long 단위 | outl() | Long 단위 |
(2) 등록 함수
int skeleton26_init(void)
/* 등록 함수 : Device Driver를 리눅스 커널에 등록하기 위해 필요한 함수와 함수 매크로 */
{
int result;
printk(KERN_INFO "SKELETON26: skeleton26_init() is called.. \n");
result = register_chrdev(SKELETON26_MAJOR, "SKELETON26", &skeleton26_fops); <= 실제로 리눅스 커널 내에 등록시킴
if (result < 0) {
printk(KERN_WARNING "SKELETON26: \t Can't get major number! \n");
return result;
}
if(SKELETON26_MAJOR == 0)
SKELETON26_MAJOR = result;
printk("SKELETON26: SKELETON26_MAJOR = %d \n", SKELETON26_MAJOR);
return 0;
}
User Application이 특정 디바이스를 사용하고자 할 때에는 가장 먼저 해당 디바이스를 실제로 제어하는 Device Driver가 리눅스 커널 내에 등록되어 있어야 한다. 이 때 사용되는 함수가 Kernel Module에서도 살펴보았던 init_module() 함수로서, 커널 내 심볼의 중복을 피하기 위해 “디바이스이름_init()” 함수로 사용하고, 이를 다시 리눅스 커널과의 인터페이스에 맞도록 변환하기 위해 module_init() 함수 매크로를 사용하게 된다. => module_init(skeleton26_init);
반드시 기억해야 할 것은 Device Driver를 실제 리눅스 커널 내에 등록 시키는 함수 => register_chrdev()
● Character Device : register_chrdev()
● Block Device : register_blkdev()
● Network Device : register_netdev()
register_chrdev() 함수는 3개의 인자 값을 가지고 있다.
● 해당 Device Driver의 Major Number
입력 가능한 값
0 : Major Number를 리눅스 커널에 자동으로 할당해 달라는 요청이며,
리눅스 커널은 중복되지 않은 Major Nubmer를 할당하고 할당된 값이 반환 됨
Number : 해당 Number 값으로 중복되는 등 문제가 있다면 -1, 할당에 문제가 없으면 0
반환 됨
● 해당 Device Driver와 연결(링크)된 디바이스 파일
디바이스 파일을 가리키는 방법에는 절대 경로, 상대 경로를 사용하는 법
절대 경로 : /로 시작하는 경우
상대 경로 : /로 시작하지 않음
Linux는 디렉토리 구조와 파일 이름에 엄격하므로, 가급적 디바이스 파일들은 해당 파일들만
모아 놓은 /dev 디렉토리 밑에 모아두는 것이 좋다.
디바이스 파일은 디바이스 노드(Node)라고 불리며 “mknod” 명령어를 사용하여 생성
[root@ ]#mknod /dev/[디바이스 파일] [디바이스 그룹] [Major Number] [Minor Number]
● 해당 Device Driver에서 지원하는 함수들을 묶어 놓은 구조체
(3) 해제 함수
Device Driver 해제 함수는 rmmod 명령어를 실행시켰을 때 실행되는 함수
리눅스 커널 메모리 영역에 등록되어 있던 Device Driver의 심볼들(함수, 전역 변수)을 삭제하는 역할을 하며
cleanup_module() 함수 사용.
디바이스이름_exit()로 함수 이름을 변경하되, module_exit() 함수 매크로 사용
(4) 초기화 함수
int skeleton26_open (struct inode *inode, struct file *filp)
/* 초기화 함수 : Device를 초기화하기 위해 필요한 함수*/
{
printk(KERN_INFO "SKELETON26: skeleton26_open() is called.. \n");
// MOD_INC_USE_COUNT; ---> for Linux Kernel 2.4.x
printk(KERN_INFO "SKELETON26: \t Major number = %d \n", MAJOR(inode->i_rdev));
printk(KERN_INFO "SKELETON26: \t Minor number = %d \n", MINOR(inode->i_rdev));
return 0;
}
초기화 함수는 실제로 디바이스를 제어하기 전에 먼저 초기화를 실행하는 함수로, 등록 함수인 init_module() 함수에서도 사실 해당 디바이스의 초기화를 실행할 수 있다. 그러나 그렇게 하지 않고 init_module() 함수에서는 등록만 처리하고, 초기화 함수인 “디바이스이름_open()”에서 초기화를 진행 시키는 이유는
같은 디바이스를 사용하고자 하는 User Application의 개수가 많을 수 있기 때문이다.
Linux와 같은 운영체제에서는 같은 디바이스(자원)에 대해 많은 수의 User Application들이 동시에 사용하고자 요청할 수 있다. 예를 들어, 네트워크 카드는 한 장 뿐인데 우리는 메신저 프로그램과 웹 브라우저, E-mail 프로그램들을 동시에 쓰고자 하기 때문이다. 이 경우, 디바이스 초기화 함수와 등록 함수를 분리해 놓지 않는다면, 최초로 디바이스를 사용하고자 요청한 User Application이 종료하기 전 까지 다른 User Application 들은 해당 디바이스를 사용할 수가 없다. 이를 방지하고자 함수를 별도로 분리해 놓는다.
User Application으로부터 Device Driver까지 함수의 실행 흐름
open() -> sys_open() -> skeleton26_open()
sys_open() 함수는 시스템 콜(System call) 함수로, User Application이 어떤 함수들을 실행했는지 각 함수 별로 정의된 “시스템 콜 번호”를 이용해 알아낸 뒤 Block, Character, Network Device 그룹별 Device Driver의 “디바이스이름_open()” 함수를 실행시킨다.
디바이스이름_open(struct inode *inode, struct file *filp)
Linux에서는 파일에 접근하기 위해 해당 파일이 어디에 있는 지에 대한 경로 정보와 함께 이 파일의 특성 정보, 즉 inode 정보를 알아야 한다.
Device Driver의 Major, Minor Number를 알아내기 위해 MAJOR(), MINOR() 함수 매크로 사용
(5) 종료 함수
int skeleton26_release(struct inode *inode, struct file *filp)
(6) 읽기 함수
<그림> Device Driver에서 읽기, 쓰기 함수의 동작 흐름
User Application이 디바이스로부터 데이터를 읽어 오고자 할 때 read() 함수를 호출하면 시스템 콜 “sys_read()” 함수에 의해 분석되어, 결국 skeleton26_read() 함수가 호출된다. Device Driver가 User Application으로부터 디바이스로부터 데이터를 읽어 오고자 한다는 서비스 요청을 받으면 디바이스로부터 데이터를 읽어 오는데 사용되는 inb(), inw(), inl() 함수 등을 사용하여 데이터를 읽어 온 후, copy_to_user() 함수를 이용하여 User Application에게 데이터를 보내준다.
copy_to_user() 함수와 copy_from_user() 함수의 사용은 Device Driver 기준이다.
copy_to_user()는 User Application에게 데이터를 복사 즉, 전달해준다., copy_from_user()는 User Application 로부터 데이터를 전달 받아, 그 데이터를 디바이스에게 전달.
(7) 쓰기 함수
User Application이 어떤 데이터를 해당 디바이스에게 쓰고자 할 때 호출되는 함수
디바이스에 쓰고자 할 데이터를 먼저 받아와야 하는데, 그러기 위해서는 받아 온 데이터를 임시적으로 저장할 버퍼가 필요하다. 보내주는 데이터의 단위 사이즈인 size_t_count 만큼 메모리를 할당 받아야 하며, 이를 위해 kmalloc() 함수가 사용된다.
kmalloc()함수는 User Application에서 메모리를 할당 받기 위해 사용하는 malloc() 함수와 사용법이 비슷하다.
할당 해제(Release)를 위해 kfree()함수에 의해 사용된 커널 메모리를 해제.
kmalloc() 함수는 인자가 하나 더 필요한데, 일반적으로 GFP_KERNEL이 쓰인다.
GFP_KERNEL은 Device Driver가 원하는 메모리만큼 할당해 줄 수 있을 때까지 무한히 대기(Sleep) 하므로, 항상 성공하는 옵션
'Embedded Linux' 카테고리의 다른 글
3. C 프로그램에서 하드웨어 접근 방법 (2) | 2015.12.22 |
---|---|
2. 임베디드 시스템 개발 환경의 특징 (0) | 2015.12.22 |
리눅스 커널 모듈 프로그래밍 (0) | 2015.12.21 |
Makefile 기반 리눅스 프로그래밍 (0) | 2015.12.21 |
임베디드 리눅스 개발 환경 구축 실습: 타겟 보드 구동 (1) | 2015.12.21 |