[이론]리눅스 드라이버 원리
리눅스에서는 디바이스, 디렉터리, named pipe, 소켓 등 입출력에 필요한 기본 디바이스들이 파일 취급된다. => 파일들을 다루기 위해서는 파일 처리와 관련된 시스템 호출에 대해 알아야 한다!!
다른 이야기이지만 UNIX와 POSIX에 대한 구분은 다음과 같다.
UNIX : 벨 연구소에서 개발한 운영체제, 다양한 현대적 컴퓨터 운영체제의 원형, 크게 Unix System V 계열(Solaris 등)과 BSD 계열(NetBSD, FeeBSD)로 구분된다.
POSIX : 각기 다른 유닉스 시스템 간의 호환성 및 이식성 확보를 위한 표준 => POSIX 규격을 따르면 UNIX와 연관이 없어도 unix-like 운영체제라 한다.
저준준 입출력 함수는 여기서 참고
https://messy-developing-diary.tistory.com/171
[이론] 리눅스 프로그래밍 - 입출력 함수
리눅스의 기본 구조와 파일 시스템디바이스, 디렉터리, named pipe, socket 등이 파일로 취급된다.리눅스 시스템은 파일 권한을 이용하여 보안 기능을 제공한다. (user-group-other) 응용 프로그램 - OS[커
messy-developing-diary.tistory.com
daemon : 백그라운드에서 여러가지의 작업을 수행하는 프로그램, 상시 메모리에 머무르며 요청이 오면 서비스를 처리한다. 자세한 건 아래 블로그 참고. 자료가 잘 정리되어 있다.
https://velog.io/@qlgks1/%EB%A6%AC%EB%88%85%EC%8A%A4-%EB%8D%B0%EB%AA%ACDaemon
리눅스 데몬(Daemon) - service, systemctl(systemd), daemon 실행 및 생성하기
이름의 유래는 맥스웰의 도깨비에서 따왔다고 한다, "맥스웰의 사고 실험 - 열역학 제2법칙은 같은 온도를 갖는 두 물체가 자발적으로 서로 온도가 달라지는 것" 의 실험 설명에서 작은 '도깨비'
velog.io
모듈(module) : 모놀리식 커널의 단점(커널에 새로운 부분이 추가되면 다시 커널을 빌드해야 한다.)을 보완하는 개념, 커널처럼 동적으로 올렸다가 내렸다가 할 수 있다.
사용자 영역 : 어플리케이션이 실행되는 영역, 레지스터 등 잘못 건드리면 시스템 전체가 마비되는 영역과 분리되어 있다. 순차적
커널 영역 : 디바이스, 레지스터 등 직접 접근 가능, 시스템 호출 또는 인터럽트에 의해 실행
리눅스의 어플리케이션은 시스템 함수를 이용하여 커널로부터 필요한 동작을 요구한다.
리눅스 디바이스 드라이버 : 리눅스 커널이 주변 장치를 관리하기 위해서는 디바이스 드라이버가 필요하다.
커널의 일부분, 디바이스 관리에 필요한 정형화된 인터페이스 구현에 필요한 함수, 자료 구조의 집합체, 사용자 여역의 가상 디바 이스와 실제 디바이스 간 매핑을 제공 => 시스템 호출 인터페이스로 리눅스는 가상 파일 시스템을 제공한다.(전에 배웠던 저수준 입출력 함수들로 접근이 가능하다.)
디바이스 드라이버 유형
1) 문자 디바이스(character device) : 입출력 단위가 문자이다.(ex) 키보드) (c)
2) 블록 디바이스(block device) : 입출력 단위가 블록이다.(ex) 하드디스크) (d)
3) 네트워크 디바이스 : 네트워크 관련 장비를 관리한다.(ex) NIC)
리눅스는 디바이스 드라이버를 특수 파일로 취급한다.
주 번호(major number) : 디바이스 종류를 구분한다.
부 번호(minor number) : 같은 종류의 디바이스가 여러 개 있을 때, 각 기기를 구분
Linux Kernel은 git에 올려져 있다.(Raspberry pi version)
https://github.com/raspberrypi/linux/tree/rpi-6.12.y
GitHub - raspberrypi/linux: Kernel source tree for Raspberry Pi-provided kernel builds. Issues unrelated to the linux kernel sho
Kernel source tree for Raspberry Pi-provided kernel builds. Issues unrelated to the linux kernel should be posted on the community forum at https://forums.raspberrypi.com/ - raspberrypi/linux
github.com
***커널 모듈을 추가하는 방법은 두 가지정도 있다.
1) 커널을 빌드할 때 추가하기 => 나중에 다루어볼 예정(이 글에서는 이론만)
먼저 시스템 콜 시 불러올 함수를 작성해주고, 나중에 어플리케이션 단에서 호출하기
2) 커널이 미리 빌드된 상태에서 모듈을 만들고 추가해주기
주요 커널 함수
함수명 | 설명 | 헤더 파일 |
printk | 커널 로그 메시지 출력 함수. 다양한 로그 레벨 지원. | <linux/kernel.h> |
kmalloc | 커널 모드에서 동적 메모리 할당 함수. | <linux/slab.h> |
kfree | kmalloc으로 할당된 메모리 해제. | <linux/slab.h> |
copy_to_user | 커널 공간에서 사용자 공간으로 데이터를 복사. | <linux/uaccess.h> |
copy_from_user | 사용자 공간에서 커널 공간으로 데이터를 복사. | <linux/uaccess.h> |
init_waitqueue_head | 대기 큐(wait queue) 초기화 함수. | <linux/wait.h> |
schedule | 현재 프로세스의 실행을 중단하고 스케줄러를 호출하여 다른 프로세스로 전환. | <linux/sched/sched.h> |
mutex_lock | 뮤텍스 잠금 함수. | <linux/mutex.h> |
mutex_unlock | 뮤텍스 잠금 해제 함수. | <linux/mutex.h> |
spin_lock | 스핀락 잠금 함수. | <linux/spinlock.h> |
spin_unlock | 스핀락 잠금 해제 함수. | <linux/spinlock.h> |
init_timer | 타이머 초기화 함수 (커널 버전에 따라 함수명과 사용법 변경 가능). | <linux/timer.h> |
add_timer | 타이머 활성화 함수. | <linux/timer.h> |
del_timer | 타이머 비활성화 및 삭제 함수. | <linux/timer.h> |
register_chrdev | 문자 디바이스 드라이버 등록 함수. | <linux/fs.h> |
unregister_chrdev | 문자 디바이스 드라이버 해제 함수. | <linux/fs.h> |
copy_page | 페이지 단위 메모리 복사 함수. | <linux/mm.h> |
wake_up | 대기 큐에 있는 프로세스 중 하나 이상을 깨우는 함수. | <linux/wait.h> |
init_waitqueue_entry | 대기 큐 엔트리 초기화 함수. | <linux/wait.h> |
자세한 내용은 리눅스 커널 document를 참고하면 된다.
https://docs.kernel.org/core-api/kernel-api.html
The Linux Kernel API — The Linux Kernel documentation
The Linux kernel provides more basic utility functions. Text Searching INTRODUCTION The textsearch infrastructure provides text searching facilities for both linear and non-linear data. Individual search algorithms are implemented in modules and chosen by
docs.kernel.org
커널 모듈과 데몬은 사용자의 개입 없이도 메모리에 적재된 상태로 대기한다는 점에서 서로 비슷한 면이 있다. 서로 비교한 내용을 표로 정리한 것이다.
구분 | 커널 모듈 (Kernel Module) | 데몬 (Daemon) |
정의 | 커널 공간에서 실행되며, 커널 기능을 확장하거나 드라이버 등을 동적으로 추가하는 코드 | 사용자 공간에서 백그라운드로 실행되는 서비스 프로그램 |
실행 환경 | 커널 공간 (Ring 0, 고권한) | 사용자 공간 (Ring 3, 제한된 권한) |
역할 | 하드웨어 제어, 시스템 자원 관리, 커널 기능 확장 등 | 시스템 서비스 제공, 백그라운드 작업 수행, 사용자 요청 처리 |
언어 | C (커널 API 사용) | 주로 C, C++, Python, 쉘 스크립트 등 다양함 |
메모리 보호 | 커널 공간에 직접 접근, 시스템 안정성에 큰 영향 가능 | 사용자 공간에서 실행, 커널과 분리되어 상대적으로 안전 |
재시작 가능성 | 모듈을 제거 후 재로드 가능하지만 시스템 재부팅 필요 없는 경우 많음 | 일반적으로 프로세스 재시작을 통해 재가동 가능 |
에러 발생 시 영향 | 시스템 전체 불안정 또는 커널 패닉 가능성 있음 | 해당 프로세스만 종료되며 시스템에는 큰 영향 없음 |
로드 및 언로드 | insmod, rmmod 명령으로 동적 로드 및 언로드 가능 | 데몬 실행 및 종료 명령으로 관리 |
주요 사용 사례 | 디바이스 드라이버, 파일 시스템, 네트워크 프로토콜 구현 | 로그 관리, 네트워크 서비스, 백업, 모니터링 등 |
개발 난이도 | 커널 내부 구조와 동작 이해 필요, 개발 및 디버깅 복잡 | 상대적으로 개발 및 디버깅이 용이 |
커널 모듈 관련 명령어
명령어 | 설명 | 주요 옵션 및 설명 |
lsmod | 현재 로드된 커널 모듈 목록 출력 | - |
insmod | 커널 모듈(.ko 파일)을 커널에 삽입 (로드) | - |
rmmod | 커널 모듈을 커널에서 제거 (언로드) | - |
modprobe | 의존성 자동 처리 후 커널 모듈 로드 또는 제거 | -r: 모듈 제거, 자동 의존성 처리 |
modinfo | 커널 모듈의 정보(버전, 라이선스, 매개변수 등) 출력 | - |
dmesg | 커널 메시지 버퍼 출력 (모듈 로드/언로드 로그 확인) | --ctime: 시간 형식으로 출력 |
depmod | 모듈 의존성 데이터베이스 생성 및 갱신 | - |
ls /lib/modules/$(uname -r)/kernel/ | 현재 커널 버전에 맞는 모듈 디렉터리 확인 | - |
- insmod는 단순히 지정한 모듈을 로드하므로, 의존성 있는 모듈이 있을 경우 수동으로 먼저 로드해야 함.
- modprobe는 의존성까지 자동으로 처리하기 때문에 일반적으로 모듈 로드/제거에 권장됨.
- lsmod는 로드된 모듈 목록과 각 모듈의 사용 카운트, 의존성 정보를 간단히 보여줌.
- modinfo로 모듈의 매개변수 설정 방법과 저작권 정보 등을 확인할 수 있어 개발 시 유용함.
커널 모듈 프로그래밍 - module_init으로 모듈을 올리고 module_exit로 모듈을 내린다.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
/* 모듈의 초기화 부분 */
static int initModule(void)
{
printk(KERN_INFO "Hello module!\n");
return 0;
}
/* 모듈이 내려질 때의 정리 부분 */
static void cleanupModule(void)
{
printk(KERN_INFO "Good-bye module!\n");
}
/* 모듈 함수 등록 부분 */
module_init(initModule);
module_exit(cleanupModule);
최종적으로 모듈은 .ko 형식의 파일로 생성된다.
CPU와 주변 장치는 버스로 연결된다. 버스는 address bus, data bus, control bus로 구성되며 제어 신호에 맞추어 address에 데이터를 전송하는 방식이다. (Memory In Memory Out, MIMO)
리눅스는 시스템의 모든 자원이 메모리 상에 존재하는 파일로 본다. => 특정 하드웨어 자원에 접근하기 위해서는 특정 메모리 주소를 알아야한다.(baremetal일 때와 약간은 다르다.(매핑에 대응해주어야 한다.) + 해당 방식으로 자원에 접근하기 위해서는 kernel 모드여야 한다.)
gpio_module 예제
#include <linux/fs.h> /* open( ), read( ), write( ), close( ) 커널 함수 */
#include <linux/cdev.h> /* 문자 디바이스 */
#include <linux/module.h>
#include <linux/io.h> /* ioremap( ), iounmap( ) 커널 함수 */
#include <linux/uaccess.h> /* copy_to_user( ), copy_from_user( ) 커널 함수 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YoungJin Suh");
MODULE_DESCRIPTION("Raspberry Pi GPIO LED Device Module");
#if 0
//#define BCM_IO_BASE 0x20000000 /* Raspberry Pi B/B+의 I/O Peripherals 주소 */
#define BCM_IO_BASE 0x3F000000 /* Raspberry Pi 2/3의 I/O Peripherals 주소 */
#else
#define BCM_IO_BASE 0xFE000000 /* Raspberry Pi 4의 I/O Peripherals 주소 */
#endif
#define GPIO_BASE (BCM_IO_BASE + 0x200000) /* GPIO 컨트롤러의 주소 */
#define GPIO_SIZE (256) /* 0x7E2000B0 - 0x7E2000000 + 4 = 176 + 4 = 180 */
/* GPIO 설정 매크로 */
#define GPIO_IN(g) (*(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))) /* 입력 설정 */
#define GPIO_OUT(g) (*(gpio+((g)/10)) |= (1<<(((g)%10)*3))) /* 출력 설정 */
#define GPIO_SET(g) (*(gpio+7) = 1<<g) /* 비트 설정 */
#define GPIO_CLR(g) (*(gpio+10) = 1<<g) /* 설정된 비트 해제 */
#define GPIO_GET(g) (*(gpio+13)&(1<<g)) /* 현재 GPIO의 비트에 대한 정보 획득 */
/* 디바이스 파일의 주 번호와 부 번호 */
#define GPIO_MAJOR 200
#define GPIO_MINOR 0
#define GPIO_DEVICE "gpioled" /* 디바이스 디바이스 파일의 이름 */
#define GPIO_LED 18 /* LED 사용을 위한 GPIO의 번호 */
volatile unsigned *gpio; /* I/O 접근을 위한 volatile 변수 */
static char msg[BLOCK_SIZE] = {0}; /* write( ) 함수에서 읽은 데이터 저장 */
/* 입출력 함수를 위한 선언 */
static int gpio_open(struct inode *, struct file *);
static ssize_t gpio_read(struct file *, char *, size_t, loff_t *);
static ssize_t gpio_write(struct file *, const char *, size_t, loff_t *);
static int gpio_close(struct inode *, struct file *);
/* 유닉스 입출력 함수들의 처리를 위한 구조체 */
static struct file_operations gpio_fops = {
.owner = THIS_MODULE,
.read = gpio_read,
.write = gpio_write,
.open = gpio_open,
.release = gpio_close,
};
struct cdev gpio_cdev;
int init_module(void)
{
dev_t devno;
unsigned int count;
static void *map; /* I/O 접근을 위한 변수 */
int err;
printk(KERN_INFO "Hello module!\n");
try_module_get(THIS_MODULE); //참조하는 프로세스 관리
/* 문자 디바이스를 등록한다. */
devno = MKDEV(GPIO_MAJOR, GPIO_MINOR);
register_chrdev_region(devno, 1, GPIO_DEVICE);
/* 문자 디바이스를 위한 구조체를 초기화한다. */
cdev_init(&gpio_cdev, &gpio_fops);
gpio_cdev.owner = THIS_MODULE;
count = 1;
err = cdev_add(&gpio_cdev, devno, count); /* 문자 디바이스를 추가한다. */
if (err < 0) {
printk("Error : Device Add\n");
return -1;
}
printk("'mknod /dev/%s c %d 0'\n", GPIO_DEVICE, GPIO_MAJOR);
printk("'chmod 666 /dev/%s'\n", GPIO_DEVICE);
map = ioremap(GPIO_BASE, GPIO_SIZE); /* 사용할 메모리를 할당한다. physical */
if(!map) {
printk("Error : mapping GPIO memory\n");
iounmap(map);
return -EBUSY;
}
gpio = (volatile unsigned int *)map;
GPIO_IN(GPIO_LED); /* LED 사용을 위한 초기화 */
GPIO_OUT(GPIO_LED);
return 0;
}
void cleanup_module(void)
{
dev_t devno = MKDEV(GPIO_MAJOR, GPIO_MINOR);
unregister_chrdev_region(devno, 1); /* 문자 디바이스의 등록을 해제한다. */
cdev_del(&gpio_cdev); /* 문자 디바이스의 구조체를 해제한다. */
if (gpio) {
iounmap(gpio); /* 매핑된 메모리를 삭제한다. */
}
module_put(THIS_MODULE);
printk(KERN_INFO "Good-bye module!\n");
}
static int gpio_open(struct inode *inod, struct file *fil)
{
printk("GPIO Device opened(%d/%d)\n", imajor(inod), iminor(inod));
return 0;
}
static int gpio_close(struct inode *inod, struct file *fil)
{
printk("GPIO Device closed(%d)\n", MAJOR(fil->f_path.dentry->d_inode->i_rdev));
return 0;
}
static ssize_t gpio_read(struct file *inode, char *buff, size_t len, loff_t *off)
{
int count;
strcat(msg, " from Kernel");
count = copy_to_user(buff, msg, strlen(msg)+1); /* 사용자 영역으로 데이터를 보낸다. */
printk("GPIO Device(%d) read : %s(%d)\n",
MAJOR(inode->f_path.dentry->d_inode->i_rdev), msg, count);
return count;
}
static ssize_t gpio_write(struct file *inode, const char *buff, size_t len, loff_t *off)
{
short count;
memset(msg, 0, BLOCK_SIZE);
count = copy_from_user(msg, buff, len); /* 사용자 영역으로부터 데이터를 가져온다. */
/* 사용자가 보낸 데이터가 0인 경우 LED를 끄고, 0이 아닌 경우 LED를 켠다. */
(!strcmp(msg, "0"))?GPIO_CLR(GPIO_LED):GPIO_SET(GPIO_LED);
printk("GPIO Device(%d) write : %s(%d)\n",
MAJOR(inode->f_path.dentry->d_inode->i_rdev), msg, len);
return count;
}
디바이스 드라이버를 사용하기 위해서는 커널에 등록하는 절차가 필요 - 디바이스 종류에 따라서 제공되는 함수도 다르다.
함수명 | 설명 | 사용 위치 | 헤더 파일 |
copy_to_user | 커널 → 사용자 공간으로 데이터 복사 | 커널 공간 | <linux/uaccess.h> |
copy_from_user | 사용자 → 커널 공간으로 데이터 복사 | 커널 공간 | <linux/uaccess.h> |
get_user | 사용자 공간의 단일 값 읽기 | 커널 공간 | <linux/uaccess.h> |
put_user | 커널 공간의 단일 값 쓰기 | 커널 공간 | <linux/uaccess.h> |
ioread8/16/32 | MMIO 주소에서 8/16/32비트 읽기 | 커널 공간 | <linux/io.h> |
iowrite8/16/32 | MMIO 주소에 8/16/32비트 쓰기 | 커널 공간 | <linux/io.h> |
readb/readw/readl | 메모리 매핑된 레지스터 읽기 | 커널 공간 | <asm/io.h> |
writeb/writew/writel | 메모리 매핑된 레지스터 쓰기 | 커널 공간 | <asm/io.h> |
ioremap | 물리 메모리 주소를 커널 가상 주소로 매핑 | 커널 공간 | <linux/io.h> |
iounmap | ioremap으로 매핑된 주소 해제 | 커널 공간 | <linux/io.h> |
request_region | I/O 포트 사용 요청 | 커널 공간 | <linux/ioport.h> |
release_region | I/O 포트 해제 | 커널 공간 | <linux/ioport.h> |
request_mem_region | I/O 메모리 주소 범위 사용 요청 | 커널 공간 | <linux/ioport.h> |
release_mem_region | I/O 메모리 주소 범위 해제 | 커널 공간 | <linux/ioport.h> |
register_chrdev | 문자 디바이스를 커널에 등록 | 커널 공간 | <linux/fs.h> |
unregister_chrdev | 등록된 문자 디바이스를 해제 | 커널 공간 | <linux/fs.h> |
alloc_chrdev_region | 동적으로 주 번호/부 번호를 할당 받아 문자 디바이스 등록 준비 | 커널 공간 | <linux/fs.h> |
unregister_chrdev_region | 할당된 문자 디바이스 번호 해제 | 커널 공간 | <linux/fs.h> |
cdev_init | struct cdev 구조체 초기화 | 커널 공간 | <linux/cdev.h> |
cdev_add | cdev 구조체를 커널에 등록 | 커널 공간 | <linux/cdev.h> |
cdev_del | cdev 구조체 등록 해제 | 커널 공간 | <linux/cdev.h> |
device_create | 사용자 공간의 /dev에 디바이스 노드 생성 (udev와 함께 사용됨) | 커널 공간 | <linux/device.h> |
device_destroy | 생성된 디바이스 노드 제거 | 커널 공간 | <linux/device.h> |
class_create | 디바이스 클래스 생성 | 커널 공간 | <linux/device.h> |
class_destroy | 디바이스 클래스 제거 | 커널 공간 | <linux/device.h> |
kernel에서 gpio, interrupt, timer 등 low-level 입출력 라이브러리를 제공한다 => 사용하면 된다.
여러 사용 예제는 다음을 참고하자.(이 글의 참고 문헌이기도 하다!!)
https://github.com/valentis/LinuxProgrammingWithRaspberryPi/tree/master/Chapter12
LinuxProgrammingWithRaspberryPi/Chapter12 at master · valentis/LinuxProgrammingWithRaspberryPi
Linux Programming With Raspberry Pi. Contribute to valentis/LinuxProgrammingWithRaspberryPi development by creating an account on GitHub.
github.com