컴퓨터 과학/리눅스 프로그래밍

[이론]리눅스 드라이버 원리

잡학다식을꿈꾼다 2025. 5. 19. 15:59
반응형

이 글의 출처!!

 

리눅스에서는 디바이스, 디렉터리, 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 모드여야 한다.)

linux 메모리 관리

 

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

 

반응형