본문 바로가기
임베디드 개발/임베디드 리눅스

리눅스 디바이스 모델과 디바이스 드라이버의 종류

by eteo 2026. 3. 8.
반응형

 

 

 

1. 서론

 

리눅스 디바이스 드라이버를 공부하다 보면 가장 먼저 접하게 되는 책이 Linux Device Drivers(https://lwn.net/Kernel/LDD3/)다.


이 책에서는 디바이스 드라이버를 크게 char, block, network 세 가지로 분류한다. 이 분류는 지금도 유효한 개념이지만, 리눅스 BSP 개발을 처음 경험하면서 느낀 점은 이 세 가지 분류만으로는 현대 리눅스 드라이버의 구조를 설명하기 부족하다는 것이었다.

특히 V4L2나 ALSA 같은 서브시스템 위에서 동작하는 복잡한 드라이버들을 보다 보면 “이 드라이버는 도대체 어디에 속하는거지?” 라는 질문을 하게 된다.

이 글은 그런 고민에서 출발해, 리눅스에서 디바이스(하드웨어 장치)가 어떤 모델로 동작하고, 어떤 방식으로 드라이버랑 매칭되는지, 그리고 최종적으로 사용자 공간과 어떻게 인터페이스 되는지에 대한 개인적인 이해를 정리한 글이다.

 

 

 

 

 

 

 

 

 

 

2. Discoverable devices vs Non-discoverable device

디바이스는 OS(커널)가 스스로 그 존재를 탐지할 수 있는지 여부에 따라 크게 두 가지로 분류된다.

 

 

2.1. Discoverable Devices (발견 가능한 장치)

발견 가능한 장치의 대표적인 예가 PCIe와 USB 장치이다. 우리가 리눅스 PC에 USB 마우스나 PCIe 랜카드를 꽂으면 별도의 설정 없이도 잘 동작하는데, 이건 PCIe와 USB 버스 특성 덕분이다.

 

PCIe와 USB는 프로토콜 자체에 표준화된 식별 매커니즘인 열거(Enumeration) 기능이 있다. PCIe의 경우 보통 시스템 부팅시, USB의 경우 장치가 삽입되는 즉시, 버스 컨트롤러가 버스를 스캔해서 연결된 장치들을 파악한다.

 

그리고 각 장치는 Vendor ID와 Device ID라는 고유 식별자를 가지고 있어서, 커널은 내장된 드라이버들의 ID Table을 검색해서 해당 식별자와 일치하는 드라이버를 찾아 매칭하고, 필요 시 자동으로 로드한다. 이런 일련의 과정을 Plug & Play라고 한다.

 

 

 

2.2. Non-Discoverable Devices (발견 불가능한 장치)

GPIO, I2C, SPI, Timer 등 SoC 내부의 페리퍼럴과 거기에 연결된 IC들이 여기에 해당한다. 이런 장치들은 하드웨어적으로 자신을 식별할 수 있는 표준 정보를 제공하지 않으므로, 커널이 스캔해서 존재를 알아낼 방법이 없다.

 

때문에 개발자가 디바이스 트리를 작성해서 장치가 어느 주소(레지스터)에 있고, 어떤 하드웨어인지, 어떤 드라이버를 사용할 것인지 커널에 명시적으로 정보를 전달해야한다. (ARM 보드 기준임, x86은 ACPI 사용)

 

그러면 커널이 디바이스 트리에 기술된 compatible 속성의 문자열을 기반으로 드라이버를 찾고, 매칭되는 드라이버가 있으면 로드한다.

 

 

 

 

 

 

 

 

 

 

3. Linux Device Model의 등장

과거 리눅스 커널은 Discoverable 장치와 Non-discoverable 장치를 서로 다른 방식으로 관리하고 있었는데, 이로 인해 유사한 기능의 코드가 반복적으로 구현되었고 커널 소스가 비대해지는 문제가 발생한다.

 

이러한 비효율을 해결하기위해 커널 2.6 버전부터 리눅스 디바이스 모델(Linux Device Model, LDM)이 도입되었다.

 

LDM의 핵심은 "모든 장치는 버스에 연결되어 있다"는 원칙이다. 설령 PCIe나 USB처럼 물리적인 버스가 존재하지 않는 장치라고 하더라도, Platform Bus라는 가상의 버스에 연결된 것으로 간주해서 관리 방식을 일원화했다.

 

LDM 등장 이후 모든 하드웨어 장치를 Bus, Device, Driver라는 3요소로 일관되게 관리한다.

 

 

 

 

 

 

3.1. LDM의 3 요소 (Bus, Device, Driver)

  • Bus (struct bus_type) : 장치와 드라이버가 만나는 통로이며, 버스는 자신에게 속한 드라이버와 장치를 매칭하는 역할을 한다. (ex. PCI, USB, I2C, SPI, Platform Bus)
  • Device (struct device) : 시스템에 물리적으로 존재하는 하드웨어 개체를 말한다.
  • Driver (struct device_driver) : Device를 제어할 수 있는 소프트웨어로, 하나의 드라이버는 동일한 특성을 가진 여러 장치를 동시에 지원할 수 있다.

 

https://linux-kernel-labs.github.io/refs/pull/190/merge/labs/device_model.html#basic-structures-in-linux-devices

 

 

 

 

 

 

3.2. Device의 계층 구조

그 중에서도 가장 핵심이 되는 요소는 Device라고 할 수 있다. 3요소 중에서 부모-자식 계층 구조를 가지는 객체는 오직 struct device뿐이다. Bus와 Driver는 디바이스를 분류하고 매칭하기 위한 "관리 단위"일 뿐, 트리 구조의 노드가 아니다. sysfs에서 보이는 트리 구조 역시 device 계층을 시각화한 결과이다.

 

struct device {
    struct device       *parent;
    struct kobject       kobj;
    ...
};

 

 

struct device에 있는 parent 포인터 덕분에 다음과 같은 구조가 가능해진다.

 

PCI Host Bridge (device)
 └─ PCI Device (device)
     └─ USB Host Controller (device)
         └─ USB Hub (device)
             └─ USB Keyboard (device)

 

위 구조에서 마치 Bus 아래 Bus가 있는 것처럼 보이지만 사실은 Bus를 구현하는 컨트롤러가 Device로 존재하는 것이다.

 

 

 

 

 

 

 

3.3. Bus의 핵심 역할: Device와 Driver의 매칭

LDM에서 bus는 bus_type 구조체로 표현된다. 그리고 이 구조체에서 가장 중요한 요소는 match 콜백 함수다.

 

드라이버가 로드되거나 새로운 장치가 연결되면, 해당 버스는 자신이 관리 중인 디바이스와 드라이버 리스트를 순회하면서 match 함수를 호출해서, 서로 호환되는 쌍을 찾아 바인딩한다.

 

struct bus_type {
	const char		*name;
	const char		*dev_name;
	const struct attribute_group **bus_groups;
	const struct attribute_group **dev_groups;
	const struct attribute_group **drv_groups;
	int (*match)(struct device *dev, const struct device_driver *drv); // device, device_driver 간 매치 함수
	int (*uevent)(const struct device *dev, struct kobj_uevent_env *env);
	int (*probe)(struct device *dev);
	void (*sync_state)(struct device *dev);
	void (*remove)(struct device *dev);
	void (*shutdown)(struct device *dev);
	const struct cpumask *(*irq_get_affinity)(struct device *dev, unsigned int irq_vec);
	int (*online)(struct device *dev);
	int (*offline)(struct device *dev);
	int (*suspend)(struct device *dev, pm_message_t state);
	int (*resume)(struct device *dev);
	int (*num_vf)(struct device *dev);
	int (*dma_configure)(struct device *dev);
	void (*dma_cleanup)(struct device *dev);
	const struct dev_pm_ops *pm;
	bool need_parent_lock;
};

 

그런데 struct bus_type을 살펴보면, 정작 해당 버스가 관리하는 디바이스와 드라이버의 목록은 포함되어 있지 않다. dev_groups나 drv_groups가 왠지 그 역할을 할 것처럼 보이지만, 이들은 버스에 속한 장치나 드라이버가 sysfs에 노출할 공통 속성(attribute) 파일 그룹을 정의하기 위한 용도일 뿐이다.

 

실제 버스에 등록된 디바이스와 드라이버를 관리하는 구조체는 의도적으로 분리되어 있다. bus_type이 버스의 성격과 동작을 정의하는 일종의 설계도라면, 실제 디바이스 및 드라이버 객체와의 매핑은 subsys_private에서 담당한다. 아래 subsys_private 구조체의 멤버 중에서 klist_devices와 klist_drivers가 각각 해당 버스에 등록된 디바이스와 드라이버의 목록에 해당한다.

 

struct subsys_private {
	struct kset subsys;		// bus의 kset
	struct kset *devices_kset;
	struct list_head interfaces;
	struct mutex mutex;
	struct kset *drivers_kset;
	struct klist klist_devices;	// device 리스트
	struct klist klist_drivers;	// device_driver 리스트
	struct blocking_notifier_head bus_notifier;
	unsigned int drivers_autoprobe:1;
	const struct bus_type *bus;	// bus_type 포인터
	struct device *dev_root;
	struct kset glue_dirs;
	const struct class *class;
	struct lock_class_key lock_key;
};

 

참고로 이렇게 설정용 구조체와 내부 데이터 관리 구조체를 분리한 이유는 드라이버 개발자의 실수를 방지하고, 커널의 안정성을 높이기 위함이다. 실제로 bus_type 뿐만 아니라 device와 device_driver 역시 외부에 공개되는 구조체와 별도로, 커널 내부에서만 사용하는 private 구조체를 통해 상태를 관리한다.

 

 

 

한편, 드라이버와 디바이스 간 매칭이 이루어지는 구체적인 기준은 각 버스마다 다르다.

  • PCI 버스 : VID, DID 대조
  • USB 버스 : VID, PID 대조
  • Platform 버스 : 다음 순서대로 매칭
static int platform_match(struct device *dev, const struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);

	/* 1. driver_override가 설정되어 있으면, 지정된 드라이버와 강제 매칭 */
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override, drv->name);

	/* 2. DT의 compatible 문자열과 드라이버의 of_match_table 매칭 */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* 3. ACPI 테이블의 _HID/_CID 기준 매칭 */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	/* 4. Platform 드라이버의 ID 테이블 매칭 */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;

	/* 5. name 문자열 기반 매칭 */
	return (strcmp(pdev->name, drv->name) == 0);
}

 

 

 

 

 

 

 

3.4. Platform 버스 : 소속 없는 장치들을 위한 버스

struct bus_type에 연결되지 않은 드라이버를 만드는 것은 가능하긴 하나 권장되지 않는다. LDM 원칙에 위배되기도 하고, 버스가 기본으로 제공하는 다음과 같은 기능들을 굳이 포기할 필요가 없기 때문이다.

 

  • 매칭 : 호환되는 디바이스와 드라이버를 자동으로 연결
  • 전원 관리 : 시스템이 절전모드에 진입하거나 깨어날 때, 디바이스의 전원 상태 일괄 제어
  • sysfs 경로 생성: /sys/... 아래에 장치와 드라이버의 계층 구조를 자동으로 생성

 

따라서 제어하려는 하드웨어 장치가 어느 버스에도 속하지 않는다면, 고민없이 플랫폼 드라이버로 개발하면 된다. 이렇게 말하면 pci_bus_type, usb_bus_type이 아닌 것은 다 platform_bus_type인가? 싶을 수도 있는데 그렇지 않다. 후술하겠지만 커널에는 훨씬 더 다양한 종류의 bus가 존재한다.

 

 

 

 

 

 

 

 

 

 

4. sysfs

4.1. kobject와 kset

Bus, Device, Driver와 같은 추상적 개념인 객체들이 실제로 커널 내부에서 어떻게 관리되고, 어떻게 sysfs라는 파일 시스템 형태로 사용자 공간에 노출되는지는 전부 kobject와 kset이라는 하위 메커니즘을 통해 구현된다.

 

 

4.1.1. kobject

kobject는 커널 객체를 이루는 최소 단위로, 단독으로 쓰이기보다는 다른 구조체에 내장되어 사용된다. struct device, struct device_driver, struct bus_type 등도 모두 내부 멤버로 kobject를 포함하고 있다.

 

kobject의 핵심 역할은 다음 세 가지다.

  • 참조 카운트 기반의 생명주기 관리
  • 객체 간의 계층 구조 형성
  • sysfs와의 연결

kobject 하나는 sysfs에서 디렉토리 하나로 대응된다. kobject의 name이 디렉토리 이름이 되고, 해당 kobject에 연결된 attribute들이 그 안의 파일로 노출된다. 이때 sysfs의 디렉토리 계층 구조는 kobject가 가진 두 가지 연결 관계로 결정된다.

 

  • kobject->parent : 디렉토리 트리의 상하위 관계를 결정한다. sysfs에서 보이는 부모/자식 디렉토리 구조는 kobject의 parent 체인을 그대로 반영한 것이다.
  • kobject->kset : 해당 객체가 어떤 그룹에 속하는지를 나타낸다. kset은 내부적으로 여러 kobject를 링크드 리스트로 관리한다.

 

struct kobject {
	const char		*name;
	struct list_head	entry;		// kset 안에서 쓰이는 리스트 노드
	struct kobject		*parent;	// 계층 구조 상 부모
	struct kset		*kset;		// 소속된 kset
	const struct kobj_type	*ktype;
	struct kernfs_node	*sd;		// sysfs 디렉토리 엔트리
	struct kref		kref;		// 참조 계수
	unsigned int state_initialized:1;
	unsigned int state_in_sysfs:1;
	unsigned int state_add_uevent_sent:1;
	unsigned int state_remove_uevent_sent:1;
	unsigned int uevent_suppress:1;
};

 

 

 

4.1.2. kset

kset은 비슷한 성격의 kobject들을 묶는 집합체다. kset 자체도 내부에 kobject를 포함하고 있어서, sysfs 상에서 하나의 디렉토리로 표현되며 그 안에 소속된 kobject들이 하위 디렉토리로 나열되는 구조다.

 

예를 들면 /sys/bus, /sys/bus/<bus>/drivers/, /sys/devices/가 kset이다.

 

/sys/bus/                  ← kset (bus_kset)
  ├─ usb/                  ← kobject
  └─ platform/             ← kobject

/sys/devices/              ← kset (devices_kset)
  ├─ platform/             ← kobject
  └─ pci0000:00/           ← kobject
  
/sys/bus/platform/drivers/   ← kset (drivers_kset)
  ├─ my_driver/              ← kobject
  └─ another_driver/         ← kobject

 

 

kset의 또 다른 역할은 소속된 kobject들에 공통된 uevent 정책을 적용하는 것이다. kobject가 추가되거나 제거될 때, 해당 kset의 uevent_ops를 통해 사용자 공간으로 알림을 보낸다. 이를 통해 udev와 같은 데몬이 새로운 장치의 등장을 인식하고, 적절한 디바이스 노드를 생성하는 등의 후속 처리를 수행할 수 있게 된다.

 

struct kset {
	struct list_head	list;      // kset에 속한 kobject들의 리스트 헤드
	spinlock_t		list_lock; // 리스트 접근 시 동기화를 위한 스핀락
	struct kobject		kobj;      // kset 자체가 가지는 kobject
	const struct kset_uevent_ops *uevent_ops; // 객체 상태 변화 시 사용자 공간으로 알림을 보내는 함수 포인터 집합
};

 

struct list_head {
	struct list_head *next, *prev;
};

 

 

 

 

 

 

 

 

4.2 sysfs 구조

LDM은 sysfs라는 가상 파일 시스템을 통해 사용자 공간(Userspace)에 그 구조를 노출한다. sysfs는 /sys 디렉터리에 마운트되며,커널 내부의 객체인 kobject들과 그들 간의 관계를 파일 시스템 형태로 시각화한 인터페이스라고 볼 수 있다.

 

  • /sys/
    • devices/ : 시스템에 연결된 모든 장치의 실제 계층 구조를 보여주는 핵심 디렉토리이다.
    • bus/ : 버스 타입(PCI, USB, Platform 등) 별로 디바이스와 드라이버를 분류해 보여주는 View이다.
    • class/ : 장치의 기능(class)적 분류(네트워크, 사운드, 입력 장치 등)에 따라 device를 보여주는 View이다.
    • module/ : 현재 커널에 로드된 모듈 목록을 보여준다.

 

결국 디바이스의 실제 계층 구조는 /sys/devices/에 있고, /sys/bus/와 /sys/class/는 동일한 디바이스를 다른 분류 관점에서 심볼릭 링크로 보여주는 뷰다.


한편, 드라이버의 경우 *_driver_register() 함수를 통해 버스의 관리 대상이 되면, /sys/bus/<bus>/drivers/<driver_name>/ 경로에 노출된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

5. 드라이버의 종류 (버스/바인딩 기준)

이 분류는 드라이버 개발 시 어떤 struct *_driver를 사용할 것인가에 대한 결정이다. 버스별로 장치를 인식하고 드라이버를 바인딩하는 방식이 다르므로, 해당 장치가 어떤 버스에 연결되느냐 따라 드라이버 구조체가 결정된다.

 

대표적인 드라이버 유형은 다음과 같다.

 

  1. 자동 열거(Auto-Enumeration)
    • pci_driver : PCI Host Bridge가 부팅 시 버스 스캔을 통해 연결된 PCI/PCIe 장치의 VID, DID를 확인하고, 커널은 이를 기반으로 적절한 드라이버와 바인딩한다.
    • usb_driver : USB 컨트롤러가 ID핀, CC핀의 전압 변화를 통해 장치 연결을 감지한 뒤 VID, PID 정보를 읽어오고, 커널은 이를 기반으로 적절한 드라이버와 바인딩한다 .
  2. DT 정적 정의 기반(Non-Discoverable)
    • i2c_driver : I2C는 주소 기반 스캔을 통해 장치가 존재하는지 여부는 확인할 수 있지만, 표준화된 자기 식별 정보가 없어서 어떤 장치가 연결되어 있는지는 알 수 없다. 따라서 DT에 장치 정보를 명시해야 하며, compatible 문자열과 reg = <addr> 값을 기반으로 드라이버와 바인딩된다.
    • spi_driver : SPI는 프로토콜 특성상 버스 스캔 기능이 없다. DT에 장치 정보를 기술하면 compatible 문자열과 reg(Chip Select 번호)를 보고 커널이 적절한 드라이버와 바인딩한다.
    • platform_driver : SoC 내부에 통합된 Memory-Mapped I/O 장치와 버스 스캔이 불가능한 모든 독립 장치 제어에 사용된다. DT에 정의된 compatible 정보를 기준으로 커널이 적절한 드라이버와 바인딩한다.

 

이 외에도 mdio_driver, sdio_driver 등 다양한 유형의 드라이버가 존재한다.

 

 

 

 

 

 

 

 

6. 드라이버의 종류 (사용자 인터페이스 기준)

이 분류는 드라이버가 제공하는 하드웨어 기능을 어떤 인터페이스를 통해 사용자 공간에 노출할 것인가에 대한 결정이다.

 

인터페이스에 따른 주요 드라이버 유형은 다음과 같다.

 

  1. Character Device 드라이버
    • /dev 노드를 생성하고 read(), write(), ioctl(), mmap() 등 파일 오퍼레이션(fops)을 통해 사용자 공간과 데이터를 주고받는 가장 범용적인 방식이다.
  2. sysfs 활용 드라이버
    • /sys 경로에 속성(attribute) 파일을 생성하고 show(), store() 함수를 통해 간단한 설정값이나 상태 정보를 텍스트 형태로 주고받는 방식이다.
  3. Block device 드라이버
    • 블록 단위 I/O를 제공하는 인터페이스로 파일시스템 및 페이지 캐시와 연동되어 동작한다. 사용자 공간에는 /dev/sdX, /dev/mmcblkX 등의 블록 디바이스로 노출된다.
  4. Network device 드라이버
    • 네트워크 스택을 통해 장치를 사용자 공간에 노출하는 방식이다. 드라이버는 네트워크 인터페이스(struct net_device)를 등록하며, 사용자 공간에서는 socket API를 통해 장치에 접근한다.
  5. Subsystem framework 기반 드라이버 : 커널이 미리 정의한 표준 모델을 따르는 방식이다. 드라이버는 각 서브시스템이 요구하는 구조체와 콜백을 구현하고, 사용자 공간에는 정형화된 인터페이스를 제공한다.
    1. V4L2 : 비디오 입력 및 스트리밍 장치 전용 표준 인터페이스
    2. ALSA : 사운드 입출력 장치 전용 표준 인터페이스
    3. Input System : 키보드, 마우스, 터치스크린 등 입력 장치를 위한 표준 인터페이스
    4. IIO(Industrial I/O) : 가속도 센서, 조도 센서 등 센서, ADC/DAC 장치 전용 표준 인터페이스
    5. RTC : Real Time Clock 장치 전용 표준 인터페이스
    6. HWMON : 전압, 팬, 온도 센서 등 하드웨어 모니터링 목적의 표준 인터페이스
    7. ...

 

 

이 중에서 5번의 복잡한 Subsystem framework 기반 드라이버라고 해서 완전히 새로운 통신 방식을 만든 것은 아니다. 기존에 존재하던 인터페이스를 기반으로 특정 장치 유형에 맞는 사용 모델과 API를 표준화한 상위 레이어라고 볼 수 있다.

 

예를 들면, V4L2 같은 표준 프레임워크 역시 내부적으로는 Character Device 드라이버 구조를 활용한다. 장치는 /dev/video* 형태로 노출하고, 스트림 데이터 전송에는 mmap()을 통해 프레임 버퍼를 사용자 공간에 매핑하는 방식 등을 쓰며, 장치 제어나 설정은 ioctl()로 이루어진다.

 

IIO나 HWMON 서브시스템 드라이버의 경우에도 메타데이터나 단발성 데이터는 sysfs 인터페이스를 통해 노출하고, 연속적이고 대용량의 데이터는 Character Device 기반 인터페이스를 사용하는 방식으로 구현된다.

 

 

또한, 앞서 살펴본 5장(바인딩 기준)과 6장(인터페이스 기준)의 드라이버 분류는 서로 배타적인 개념이 아니라, 하나의 드라이버를 설계할 때 동시에 고려되어야 하는 두 가지 측면이다.

 

예를 들어, 카메라 드라이버를 구현하는 경우 사용자 인터페이스는 V4L2 Subsystem을 사용하면서, 장치가 USB 웹캠이면 usb_driver를, CSI 카메라 센서라면 i2c_driver를 사용해 커널에 등록하는 식으로 구성하게 된다.

 

 

 

 

참고:

https://linux-kernel-labs.github.io/refs/heads/master/labs/device_model.html

  •  
 
 
 
반응형