
저번 글에서 모듈까지의 내용을 정리했다.
이번에는 문자 드라이버(캐릭터 드라이버)에 관한 내용부터 시작할 예정이다.
아직까지 딥한 내용이 나오지는 않고
캐릭터 장치와 장치 번호에 대한 내용 위주로 작성할 예정이다.
그럼 고고고!
캐릭터 디바이스의 특징
키보드 입력 또는 센서 데이터처럼 간헐적 데이터 발생
주로 순차 접근을 하며, 바이트(Byte) 단위의 데이터 읽고 쓰기가 이루어짐
블록 디바이스와 달리 버퍼를 거치지 않고 사용자 공간과 하드웨어 간 직접 데이터를 주고받는 경우 많음
캐릭터 디바이스의 등록
등록이란 드라이버가 커널에 로드될 때(insmod) 제어할 장치를 커널에 알리는 것
커널 내부의 장치 관리 배열인 chrdevs 에 드라이버 정보를 저장
응용 프로그램이 해당 장치 파일(/dev/xxx )에 접근하면 커널이 이 배열을 참조하여 드라이버 함수를 호출
Capability 등록은 장치가 할 수 있는 기능(read, write 등)을 알리는 것
장치 관리 배열 파일과 구조체
파일이 위치한 곳 : fs/char_dev.c
chardevs 테이블에서는 장치의 Major Number를 키로 사용하여 등록된 캐릭터 드라이버를 관리
// 관리 배열(구조체)
#define CHRDEV_MAJOR_HASH_SIZE 255
static struct char_device_struct *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
캐릭터 디바이스 등록/해제 과정
insmod (모듈 로드)
module_init() 호출
→ register_chrdev() 또는 alloc_chrdev_region() + cdev_add() 호출
→ Capability 등록 : 커널의 chrdevs 배열에 드라이버 정보 추가
rmmod (모듈 제거)
module_exit() 호출
→ unregister_chrdev() 또는 cdev_del() + unregister_chrdev_region() 호출
→ Capability 해제 : 커널 배열에서 드라이버 정보 삭제
장치 번호
하드웨어를 식별하고 제어하기 위해 리눅스 커널이 사용하는 고유한 ID 체계
주번호(Major)와 부번호(Minor)로 구성됨
파일 시스템상의 장치 파일(Device File)과 연결되어 하드웨어에 접근하는 통로가 됨
문자 장치와 블록 장치는 서로 별개의 번호 체계를 가짐
주번호 (Major Number)
장치의 종류를 구분하는 식별 번호로, 장치를 담당할 드라이버를 결정
커널 내부 문자 디바이스 테이블인 chrdevs[] 의 인덱스(Key)
해당 Major 번호에 등록된 드라이버 함수를 찾아가는 데 사용됨
부번호 (Minor Number)
동일한 종류의 장치 중 개별 장치를 식별하는 번호
문자 장치와 블록 장치는 별개의 번호 체계를 가짐
장치 번호 확인하기
시스템 전체 장치 번호 : # cat /proc/devices 로 현재 등록된 드라이버와 주 번호 목록 확인
장치 파일 상세 정보 : # ls -l /dev/devicename 으로 장치 타입과 번호 확인
root@host:/dev# ls -l ttyS0
crw-rw---- 1 root dialout 4, 64 Dec 30 16:45 ttyS0
# ↑ 'c': 캐릭터 장치, Major: 4, Minor: 64
# 유저가 open("/dev/ttyS0", ...)를 호출하면
# 커널은 이 파일의 속성에서 (4, 64) 번호를 읽어내어 주번호 4번 드라이버에게 처리를 넘김
root@host:/dev# ls -l ram0
brw-rw---- 1 root disk 1, 0 Dec 30 16:45 ram0
# ↑ 'b': 블록 장치, Major: 1, Minor: 0
장치 번호의 내부 구조 : dev_t
장치 번호는 <linux/types.h>에 정의된 dev_t 자료형을 통해 관리함
일반적으로 32비트 정수이며, Major Number는 상위 12비트 / Minor Number는 하위 20비트
장치 번호 조작 매크로
<linux/kdev_t.h>에 정의되어 있으며, dev_t 타입의 변수와 주/부번호 사용에 용이함
| 매크로 | 역할 | 사용 예시 |
|---|---|---|
MAJOR(dev_t dev) | dev_t 변수에서 주번호만 추출 | int maj = MAJOR(my_dev_num); |
MINOR(dev_t dev) | dev_t 변수에서 부번호만 추출 | int min = MINOR(my_dev_num); |
MKDEV(int major, int minor) | 주번호와 부번호를 합쳐 dev_t 생성 | dev_t dev = MKDEV(240, 0); |
장치 번호의 확보
드라이버가 커널에 등록되기 위해 고유한 Major 번호를 확보해야 함
특정 번호를 지정해서 요청하거나, 커널에게 남는 번호 할당을 요청하는 방법이 있음
장치 번호 정적 지정 (Static Allocation)
개발자가 원하는 특정 주번호를 미리 정해서 등록을 요청하는 방식
커널 소스의 Documentation/devices.txt 를 참고하여 이미 예약된 표준 번호는 피함
# cat /proc/devices 명령어를 통해 현재 사용 중인 번호들을 확인하고 비어 있는 번호 선택
register_chrdev_region() 함수를 사용하여 지정
장치 번호 동적 할당 (Dynamic Allocation)
비어 있는 번호를 커널에게 자동으로 할당받아 사용하는 방식
커널은 255부터 아래로 내려오며 사용되지 않는 주번호 중 하나를 골라 할당해 줌
alloc_chrdev_region() 사용하여 할당
해제 (Release)
드라이버가 제거될 때(module_exit) 사용했던 번호를 반납해야 함
정적/동적 모두 unregister_chrdev_region() 을 사용하여 해제
정적 지정 함수 : register_chrdev_region()
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first : 할당하려는 장치 번호 범위 중 시작 번호, Major/Minor 번호를 모두 포함해야 함
count : 연속해서 사용할 부번호(Minor)의 개수 (보통 1)
name : 장치 이름 (/proc/devices 와 /sys 에 표시됨)
반환값 : 성공 시 0, 실패 시 음수 에러 코드
동적 할당 함수 : alloc_chrdev_region()
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
dev : 할당받은 장치 번호(dev_t)가 저장될 변수의 주소
firstminor : 요청할 첫 번째 Minor 번호, 보통 0
count : 필요한 장치 개수(Minor 번호 개수)
name : 장치 이름
반환값 : 성공 시 0, 실패 시 음수 에러 코드
장치 등록 해제 함수 : unregister_chrdev_region()
void unregister_chrdev_region(dev_t first, unsigned int count);
first : 반납할 장치 번호 중 시작 번호
count : 반납할 장치 개수
문자 장치를 표현하는 구조체 : struct cdev
문자 드라이버는 커널 내부에서 struct cdev로 표현되며, <linux/cdev.h>에 정의되어 있음
struct cdev {
struct kobject kobj; // (중요) 커널 객체 시스템과 연동
struct module *owner; // 장치를 소유하는 모듈(THIS_MODULE)
const struct file_operations *ops; // (중요) 장치의 file_operations 테이블
struct list_head list; // 내부 관리용 리스트
dev_t dev; // (major, minor) 장치 번호
unsigned int count; // 해당 cdev가 담당하는 디바이스 개수
...
};
문자 장치 등록 방식
고전적 방법
등록 : register_chrdev()
해제 : unregister_chrdev()
현대적 방법 : struct_cdev 사용
장치 번호 확보 : register_chrdev_region() (Static) / alloc_chrdev_region() (Dynamic)
초기화 : cdev_init() (Static) / cdev_alloc() (Dynamic)
등록 : cdev_add()
해제 : cdev_del() + unregister_chrdev_region()
cdev 관련 함수객체 초기화 : cdev_init()
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
이미 할당된 struct cdev 구조체를 기본값으로 초기화는 함수
cdev : 초기화할 cdev 포인터로, 정적 또는 전역 변수로 선언되어야 함
fops : open/read/write 등의 구현이 포함된 file_operations 구조체의 주소
등록 함수 : cdev_add()
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count);
초기화된 cdev 구조체를 커널에 등록하여 실제 장치로 인식되게 하는 함수
cdev : 등록할 문자 장치 구조체
dev : 등록할 디바이스의 시작 번호(MKDEV(major, minor))
count : 등록할 디바이스 개수, 주로 1
반환값 : 성공 시 0, 실패 시 음수 에러 코드
제거 함수 : cdev_del()
void cdev_del(struct cdev *cdev);
등록된 캐릭터 디바이스를 커널에서 제거하는 함수
cdev_del() 호출 후에는 unregister_chrdev_region()으로 장치 번호도 반납해야 함
장치 파일의 역할
/dev 아래에 위치해 있으며, 실제 하드웨어를 대변하는 파일
유저 공간 프로그램에서는 open("/dev/mydev", ...) 호출
이때 파일에 기록된 (major, minor) 번호를 통해 해당 드라이버의 file_operations 로 연결
장치 번호의 범위와 할당
예약된 번호·기본 번호
리눅스 자체에서 각 장치들에 이미 사용하고 있는 값들의 모음
커널 문서 : Documentation/devices.txt
헤더 파일 : <linux/major.h>
사용 가능한 범위
234-239 : 미할당, UNASSIGNED
240-254 (char/block) : 로컬/실험용, LOCAL/EXPERIMENTAL USE
자동 할당되는 값
alloc_chrdev_region() : 호출하면 255부터 아래로 내려가며 비어 있는 번호를 찾아 줌
장치 파일의 생성 방법
쉘에서 직접 생성 : mknod
# mknod /dev/name type major minor 형태로 사용
# mknod /dev/mydev c 236 0 # 문자 장치, Major=236, Minor=0
# mknod /dev/myblk b 8 1 # 블록 장치 예시
프로그램에서 시스템 콜 : mknod()
int mknod(const char *pathname, mode_t mode, dev_t dev);
유저 프로그램 소스 내부에서 디바이스 파일 생성 함수 사용
사용 시에는 Super User 권한이 필요
커널에서 동적 생성 : (device_create / device_destroy)
<linux/device.h> 내부의 장치 파일 생성 함수
새로 생성된 struct device 포인터를 반환
이번에 드라이버 부분을 배우면서 느낀 건데
최신 커널 함수가 꽤나 많다는 생각이 들었다.
배웠던 내용과 별개로 요즘 사용하고 있는 함수는 다른 경우가 많았으니
함수를 사용하기 전에는 한번씩 더 찾아보는 게 좋겠다.