Linux Device Driver 기초 #5 문자 드라이버
문자 드라이버는 블록 드라이버나 네트워크 드라이버보다 이해하기 쉽고 간단한 편이다.
scull(Simple Character Utility for Loading Localities)
scull은 메모리 영역을 디바이스처럼 취급하는 문자 드라이버이다. scull은 커널에서 할당받은 메모리 상에서 동작하기 때문에 하드웨어에 의존적이지 않다는 장점이 있다. 누구나 scull을 컴파일하고 실행시킬 수 있으며 리눅스를 돌릴 수 있는 컴퓨터 아키텍쳐라면 어디나 이식도 가능하다. 하지만 커널과 드라이버 사이에 인터페이스를 보여주고 사용자가 테스트를 할 수 있다는 점을 제외한다면 디바이스 자체는 별다ㅏ른 쓸모가 없다.
드라이버 작성에서 첫단계는 디바이스가 사용자 프로그램에게 제공할 기능을 정의하는 일이다.
여기서 작성할 디바이스는 컴퓨터 메모리의 일부이므로 어떤 기능이든 가능하다. 순차 접근 디바이스나 임의 접근 디바이스를 구현할 수도 있고 디바이스 하나나 여러개를 구현할 수도 있다.
scull 소스는 다음의 디바이스를 구현한다. 모듈로 구현한 각 디바이스별 유형은 타입으로 나뉜다.
- scull0에서 scull3
- 영구적인 전역 메모리 영역으로 구성된 네가지 디바이스이다. 여기서 전역이란 디바이스를 여러번 열 경우 디바이스를 연 파일디스크립터 모두가 디바이스 내부 자료를 공유한다는 뜻이다. 영구적이란 디바이스를 닫았다 다시 열어도 자료가 사라지지 않는다는 의미이다.
- scullpipe0에서 scullpipe3
- pipe처럼 동작하는 네가지 FIFO 디바이스이다.
- 한 프로세스가 쓰는 내용을 다른 프로세스가 읽는다.
- 여러 프로세스가 같은 디바이스를 읽을 경우에는 자료를 두고 경쟁하게 된다.
- scullpipe 내부 구조는 인터럽트를 사용하지 않고서도 blocking, non-blocking I/O를 구현할 수 있음을 보여준다.
- 비록 실제 드라이버는 하드웨어 인터럽트를 사용해서 디바이스와 동기화하지만 blocking, non-blocking 동작은 인터럽트 처리와는 별개인 중요한 문제이다.
- scullsingle, scullpriv, sculluid, scullwuid
- 이들 디바이슨느 scull0과 유사하지만 디바이스 열기에 각기 다른 제한을 둔다.
- 첫번째 디바이스(scullsingle)는 한번에 한 프로세스만 사용할 수 있다.
- 반면 scullpriv는 각 가상 콘솔(또는 터미널 세션)마다 독립적이다. 이는 프로세스 메모리 영역이 콘솔/터미널마다 다르기 때문이다.
- sculluid, scullwuid는 둘 다 여러번 열 수 있으나 한 번에 한 사용자만 열 수 있다. 다른 사용자가 사용중일 경우 sculluid는 Device Busy 오류를 반환한다.
scull device는 각 드라이버마다 특색있는 다른 기능을 예시하며 난이도도 각각 다르다.
Major, Minor 번호
파일시스템에서 문자 디바이스는 이름으로 접근한다. 이러한 이름은 특수 파일 혹은 디바이스 파일, 파일시스템 트리 노드라고 부르며 관례적으로 /dev 디렉토리 아래에 둔다. 문자 드리이버용 특수 파일은 ls -l 출력 결과에서 첫 열에 있는 "c" 로 식별할 수 있다.
블록 디바이스도 /dev 아래에 있으며 "b"로 식별한다.
ls -l 명령을 수행하면 최종 수정 날짜 앞에 두 숫자를 볼 수 있다. 일반 파일의 경우 파일 크기가 나타나지만 디바이스 드라이버용 파일의 경우 다비아스 major번호와 minor 번호이다.
관례적으로 major 번호는 디바이스와 연결된 드라이버를 나타낸다. 예를 들어 /dev/null과 /dev/zero 는 모두 드라이버 1이 관리한다. 반면 가상 콘솔과 serial terminal은 드라이버 4가 관리한다. 유사하게 vcs1과 vcsa1 디바이스는 드라이버 7이 관리한다. 최근 리눅스 커널에서는 여러 드라이버가 major 번호를 공유할 수 있지만 일반적으로 우리가 사용하는 디바이스 대부분은 여전히 드라이버 하나당 major 번호 하나라는 원리에 따른다.
minor번호는 커널이 어느 디바이스인지를 정확히 결정하는데 사용한다. 드라이버 작성 방식에 따라 커널에서 디바이스를 직접 가리키는 포인터를 가져올 수도 있고 아니면 minor 번호를 디바이스 지역 배열의 index로 사용할 수도 있다.
어쨌거나 커널 자신은 minor 번호에 대해 아는 바가 거의 없다. 드라이버가 구현한 디바이스를 참조한다는 사실만 알 뿐이다.
디바이스 번호의 내부 표현
커널 내부적으로는 (<linux/types.h>에 정의되어있는) dev_t 타입에 major번호와 minor 번호를 포괄하는 디바이스 번호를 저장한다. 커널 2.6.0부터 dev_t는 32비트이며 12비트는 major 번호용으로 20비트는 minor번호 용으로 사용한다.
하지만 코드에서 디바이스 번호를 가져오려면 이런 방식으로 가져오면 안되고 <linux/kdev_t.h>의 매크로를 사용해야한다.
MAJOR(dev_t dev);
MINOR(dev_t dev);
major 번호와 minor 번호를 dev_t로바꾸려면 다음 매크로를 사용한다.
MKDEV(int major, int minor);
디바이스 번호의 할당과 해제
문자 디바이스를 설치할 때 드라이버는 가장 먼저 작업 대상 디바이스 번호를 얻어야한다. <linux/fs.h>에서 정의하는 함수인 register_chrdev_region으로 이 작업을 수행한다.
int register_chrdev_region(dev_t first, unsigned int count, char *name);
- first는 할당받으려는 디바이스 번호 범위 중 시작 번호이다. first의 minor 번호는 주로 0이지만 사실상 반드시 그래야 한다는 규칙은 없다.
- count는 요청하는 (연속적인) 디바이스 번호 개수이다. count 값이 너무 크면 요청한 범위가 다음 major 번호로 넘칠 수 있다는 점에 유의한다.
- name은 번호 범위와 연관 지을 디바이스 이름이다. 이 이름이 /proc/devices 와 sysfs에 나타난다.
다른 커널 함수 대부분과 마찬가지로 할당이 성공적이면 register_chrdev_region 반환 값은 0이다. 오류가 발생하면 이 함수는 음수 오류 코드를 반환하며 요청한 영역에 접근할 수 없게 된다.
원하는 디바이스 번호를 미리 정확하게 알고 있다면 register_chrdev_region 함수를 사용해도 문제가 없다. 그러나 대부분의 경우 디바이스는 자신이 사용할 major 번호를 미리 할지 못한다.
커널은 동작 중에 major 번호를 임의로 할당할 수 있지만 다른 함수로 할당을 요청해야한다.
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
이 함수에서 dev는 출력 전용 매개 변수이다. 함수가 성공적으로 끝나면 할당받은 범위 내에서 첫 번호가 들어간다.
- firstminor는 사용을 위해 요청하는 첫 minor번호가 되어야 하며 0이 일반적이다.
- count, name은 register_chrdev_region과 동일
디바이스 번호는 어떤 식으로 할당받든지 사용이 끝나면 해제해야한다. 다음과 같은 함수로 디바이스 번호를 해제한다.
void unregister_chrdev_region(dev_t first, unsigned int count);
보통 unregister_chrdev_region 함수는 모듈 정리 함수에서 호출한다.
이제 드라이버는 디바이스 기능을 구현한 내부함수와 해당 번호를 연결하면 어플리케이션에서 디바이스 번호를 참조할 수 있다.
Major 번호의 동적 할당
가장 널리 사용하는 디바이스는 major 번호를 정적으로 할당해 놓았다. 정적인 major 번호를 할당한 디바이스 목록은 커널 소스 트리 내
자료 구조체
드라이버 기본 함수 대부분은 file_operations, file, inode 라는 세가지 중요한 커널 자료 구조체를 사용한다.
file_operations
file_operations 구조체는문자 드라이버가 이러한 연결을 설정하는 방법이다. <linux/fs.h> 에서 정의하는 이 구조체는 함수 포인터 집합이다. 열린 파일마다 각각 관련 함수 집합이 연결되어있다. 관련 함수를 통해 open, read 등으로 이름이 붙은 시스템 콜을 구현한다. 객체 지향 프로그래밍 용어로 보면 파일을 객체, 함수를 메소드로 볼 수 있다.
여기서 리눅스 커널이 사용하는 객체 지향 프로그래밍 개념을 처음 엿볼 수 있다.
문자 디바이스 드라이버
file_operation 을 구현하는 디바이스 드라이버
- open, read, write, lseek, close 등 일반적으로 사용하는 파일 함수
- 별도의 시스템 콜 추가 없이 간편하게 새로운 기능 추가 가능 -> 가상 파일 시스템
file operations 구조체는 파일을 열고 읽고 쓰고 제어 명령을 처리하는 등의 파일 작업을 처리하는 함수 포인터들을 담고 있다.
struct file_operations {
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
loff_t (*llseek) (struct file *, loff_t, int);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
void (*release) (struct inode *, struct file *);
};
- open: 파일을 열 때 호출됨(inode는 파일의 메타데이터 구조체)
- read: user space로 파일을 읽을 때 호출
- write: user space의 데이터를 파일에 쓸 때 호출
- llseek: 파일의 읽기/쓰기 위치를 변경할 때
- unlocked_ioctl: ioctl 명령을 처리
- release: 파일을 닫을 때 호출
register_chrdev(major, name, fops)를 이용해서 새로운 문자 디바이스 드라이버 등록할 수 있다.
#include <linux/fs.h>
#define SUNGCH_DEVICE_NAME "sungch-device"
#define SUNGCH_MAJOR_NUMBER 177
static int sungch_device_open(struct inode *inode, struct file *fp)
{
int minor = iminor(inode);
printk(KENR_DEBUG, "%s -minor: %d\n", __func__, minor);
return 0;
}
static struct file_operations sungch_device_fops = {
.open = sungch_device_open,
}
static int __init sungch_module_init(void)
{
return register_chrdev(SUNGCH_MAJOR_NUMBER, SUNGCH_DEVICE_NAME, &sungch_devie_fops);
}
문자 디바이스를 등록 절차
1. device node 만들기
mknod /dev/sungch-device c 177 12
2. module load
insmod /usr/lib/modules/sungch.ko
3. device node 열기
cat /dev/sungch-device
4. module unload
rmmod /dev/sungch-device
5. device node 열기
cat /dev/sungch-device
buildroot login: root
# cd /dev
# mknod /dev/sungch c 177 34
# cat /dev/sungch
cat: can't open '/dev/sungch': No such device or address
# insmod /usr/lib/modules/sungch.ko
# cat /dev/sungch
cat: read error: Invalid argument
# ls -lah sungch
crw-r--r-- 1 root root 177, 34 Nov 2 06:13 sungch