리눅스 디바이스 드라이버(Linux Device Driver) 개념 구조 예제

반응형
디바이스 드라이버(Device Driver)란?

디바이스 드라이버는 컴퓨터 운영 체제와 하드웨어 장치 간의 통신을 가능하게 하는 소프트웨어라고 정의하는데, 처음 들으면 이게 무슨 의미인지 감도 안 잡히는 사람도 있다.(그게 나였다.) 그래서 오늘은 내가 신입사원 때 공부했던 디바이스 드라이버(DD)에 대해 조금 더 쉽게 풀어서 써보고자 한다.

일상생활 속에서 디바이스 드라이버의 역할을 예를 들어 보겠다. 무선 마우스를 컴퓨터에 연결해서 사용하려고 할 때, 드라이버를 설치하라는 메시지를 볼 수 있다. 마우스를 컴퓨터에 연결했을 때, 마우스 자체는 클릭하거나 포인터를 움직이는 동작을 전달할 수 있지만, 컴퓨터가 이를 이해하려면 마우스의 동작을 컴퓨터 명령어로 변환하는 과정이 필요하다. 디바이스 드라이버(DD)가 없다면 마우스의 클릭이나 움직임은 단순한 전기 신호에 불과해 컴퓨터가 이를 올바르게 해석할 수 없기 때문에 컴퓨터가 특정 장치를 동작시킬 때 어떻게 동작시켜야 하는지 정의된 프로그램을 설치해야 한다.

마찬가지로, 키보드로 'A'키를 누르면 디바이스 드라이버가 이 신호를 받아 컴퓨터에게 "사용자가 'A' 문자를 입력했다"라고 알려주는 것이다. 이제 디바이스 드라이버가 컴퓨터와 외부 장치 간의 소통을 가능하게 해주는 역할을 한다는 의미가 조금은 와닿을 것이다.

컴퓨터에 USB를 꽂는 경우도 드라이버가 이 장치를 컴퓨터에 연결하여 파일을 저장하거나 불러올 수 있도록 한다. 드라이버 없이는 컴퓨터가 USB 드라이브에 저장된 데이터를 읽거나 쓸 수 있는 방법을 알 수 없다.

 

디바이스 드라이버(DD) 구조

(1) 하드웨어

가장 아래의 하드웨어는 실제 물리적인 장치인데, 각 장치마다 요구되는 드라이버가 다 다르다. 장치 종류마다 다를 수 있고, 제조사, 모델마다 다를 수 있다. 예를 들면 게이밍 마우스의 경우 추가 버튼이나 사용자 정의가 가능한 기능이 더 많을 수 있고, 해당 모델에 특화된 드라이버가 필요할 수 있다. 그래서 리눅스에서는 서로 다른 다양한 파일 시스템에 공통된 인터페이스를 제공하기 위해 VFS라는 개념이 생겼다.

(2) Kernel

위 그림에서 커널은 하드웨어와 직접 상호작용하기 위한 핵심 코어로, 이 커널에 접근하기 위해서는 applications에서 system call이나 shell을 거쳐가야 한다.

  • System Call Interface: 사용자 공간 애플리케이션과 커널 서비스 간의 통신 계층으로 애플리케이션에서 작업을 수행하고자 할 때 시스템 콜을 사용하고, 이는 커널에 의해 처리된다.
  • VFS (가상 파일 시스템): 다양한 파일 시스템에 대한 표준 인터페이스를 제공하는 커널의 추상화 계층으로 이를 통해 같은 시스템 콜 인터페이스를 다양한 종류의 파일 시스템에 사용할 수 있다. 디바이스 드라이버도 하나의 파일이므로 VFS의 인터페이스에 따라 디바이스 드라이버가 작성되어야 할 것이다.
  • Buffer Cache: 디스크에 직접적으로 수행되는 읽기 및 쓰기 작업의 수를 줄이고 성능을 개선하기 위해 데이터를 일시적으로 보관하는 메모리 공간이다.
  • Network Subsystem: 네트워크에서의 데이터 송수신과 같은 모든 네트워크 작업을 처리하는 커널 부분이다.
  • 캐릭터 디바이스 드라이버: 문자(바이트) 단위의 데이터를 읽고 쓰이는 캐릭터 디바이스(예: 키보드, 마우스)를 관리하는 드라이버로, buffer 또는 cache가 존재하지 않는다.
  • 블록 디바이스 드라이버: 블록 단위로 읽고 쓰이는 블록 디바이스를 관리하며, 데이터를 저장할 system buffer(buffer cache)가 필요하다. 보통 파일 시스템에 의해 마운트 되어 관리되며, 해당 방식을 사용하는 하드웨어로는 hdd, cdroms, rad disks 등이 있다.
  • 네트워크 디바이스 드라이버: 이더넷 어댑터나 무선 컨트롤러와 같은 네트워크 장치에서 네트워크 통신과 관련된 기능을 관리한다. ex) TCP/IP 프로토콜 스택 관리, 네트워크 인터페이스 관리 등

(3) Application

사용자 공간의 애플리케이션을 나타낸다. 이 애플리케이션들은 디바이스로부터 데이터를 읽거나 디바이스에 데이터를 쓰는 등의 작업을 커널과 상호작용하며 수행한다.

 

 
리눅스에서 디바이스 드라이버 확인하기

 

 

/dev 살펴보기

 

필자의 경우 Ubuntu 22.04 LTS 버전으로 사용 중이다. 아래 사진에서 c로 시작하면 character device, b로 시작하면 block device를 의미한다.

위 그림에서 노란색으로 표시한 '10, 235'는 각각 major number(주 번호), minor number(부 번호)를 나타낸다. major는 디바이스 종류 구분을 위해, minor는 같은 종류 중에서도 실제 제어해야 할 디바이스를 구분하기 위함이다.

ls -al /devcat /proc/devices 명령어를 통해 현재 등록된 디바이스의 종류를 볼 수 있으며 같은 주번호가 캐릭터, 블록 디바이스에 부여될 수 있다.

 

 

 

/dev/block 살펴보기

  • /dev/sr0: SCSI CD-ROM 디바이스를 나타낸다. sr은 SCSI optical drive를 의미한다.
  • /dev/dm-0: 디바이스 매퍼(Device Mapper). 일반적으로 LVM(Logical Volume Manager)이나 암호화된 볼륨을 위해 사용된다.
  • /dev/loop0부터 /dev/loop7: 루프백 디바이스. 루프백 디바이스는 파일을 마치 블록 디바이스인 것처럼 접근할 수 있게 해준다. 이를 통해 ISO 이미지와 같은 파일을 마운트하고, 마치 실제 물리 디바이스인 것처럼 사용할 수 있다.
  • /dev/sda: 이것은 첫 번째 SCSI/SATA 하드 디스크를 나타낸다.
  • /dev/sda1, /dev/sda2, /dev/sda3: 이 파일들은 /dev/sda 디스크의 첫 번째, 두 번째, 세 번째 파티션을 나타낸다. 파티션은 디스크 상에 구분된 공간으로, 각기 다른 파일 시스템을 가질 수 있다.

 

디바이스 드라이버 생성하기

  • 소스코드를 작성하고 컴파일을 한 뒤 insmod로 커널에 생성한 모듈을 적재한다.
  • 그다음, 생성할 커널 모듈을 위한 디바이스 파일을 만들어야 하기 때문에 mknod 명령어를 이용한다.
  • 해당 명령어로 디바이스 파일을 만들었으면, 이제 응용 프로그램에서 open, read 등을 이용하여 디바이스 파일에 접근한다.
  • Char Device Driver 모듈 제작
    • 커널 모듈이 초기에 등록될 때 init 과정을 거치는데, 이 안에서 char device를 등록하는 로직을 넣어줘야 한다. 이는 register_chrdev() 함수를 이용하면 된다.
    • register_chrdev("major number", "device name", "file_operations 구조체 변수") char device 등록 함수는 위와 같이 3개의 인수를 가진다.
    • 첫 번째는 major 번호, 두 번째는 디바이스 이름, 마지막은 file_operations 구조체이다.
    • file_operations 구조체는 Char Device, Block Device 드라이버와 일반 응용 프로그램 간의 통신을 위해 제공되는 인터페이스라고 보면 된다.
    • read, write, open, release 등의 함수 포인터를 사용하여 디바이스 드라이버를 제작하면 된다.

  • 유저 공간에서 open, close, read, write 같은 함수들은 트랩에 의해 커널에게 system call을 통해 처리가 된다.
  • sys_...( ) 함수 내부에선 실제 VFS 내부의 file_operations 구조체의 함수 포인터를 참조하고, 거기에 등록된 디바이스 드라이버 함수가 실제로 호출되는 과정을 나타낸다.
디바이스 드라이버 생성 예제

 

[test_device.c 작성]

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h> // 문자열 관련 함수 사용을 위해 추가



int main() {
    int dev;
    char buff[1024] = {0}; // 버퍼를 0으로 초기화

    printf("Device driver test.\n");

    dev = open("/dev/test_device", O_RDWR);
    if (dev < 0) {
        perror("Failed to open the device"); // 에러 메시지 출력
        return errno;
    }
    printf("dev = %d\n", dev);

    const char* msg = "덮어쓸 메시지"; // 쓰려는 문자열을 변수에 저장
    ssize_t bytes_written = write(dev, msg, strlen(msg)); // strlen을 사용하여 정확한 길이 전달
    if (bytes_written < 0) {
        perror("Failed to write to the device");
        return errno;
    }

    ssize_t bytes_read = read(dev, buff, sizeof(buff)); // 버퍼 크기만큼 읽기
    if (bytes_read < 0) {
        perror("Failed to read from the device");
        return errno;
    }

    printf("read from device: %s\n", buff);
    close(dev);

    exit(EXIT_SUCCESS);
}

 

[make 후 lsmod를 통해 현재 커널에 로드된 모듈 확인]

make

make install

lsmod | grep test

응용 프로그램을 실행하여 디바이스 드라이버가 정상적으로 실행됐는지 확인한다. 나는 내 이름으로 덮어쓰기 해서 내용은 가렸다.

 

[dmesg 확인]

 

반응형