이번 글 역시 PCIe 디바이스를 다루는 것보다 디바이스 드라이버 작성 방식에 대해 초점을 맞추도록 한다.
디바이스 드라이버 with sysfs 인터페이스
직전 글에서 사용한 코드를 약간 수정하여 sysfs 인터페이스를 구현하는 디바이스 드라이버를 작성해보도록 하자.
sysfs란?
- sysfs는 커널 객체(kobject)와 그 속성(attribute)을 사용자공간에 파일 시스템 형태로 노출하는 가상 파일시스템이다.
- sysfs는 일반적으로 /sys 경로에 마운트된다.
- 리눅스 시스템의 디바이스, 드라이버, 서브시스템, 버스, 클래스 등 다양한 커널 구성 요소들은 공통적으로 struct kobject를 구조체의 멤버로 포함하고 있다. 이러한 설계 덕분에 모든 커널 구성 요소들이 sysfs 상에서 계층적인 트리 구조로 표현되고 탐색이 가능하다.
- sysfs에서 kobject는 디렉토리로 표현되고, 그 attribute는 해당 디렉토리 내의 파일로 표현된다.
- sysfs를 통해 사용자는 파일을 읽고 쓰는 것만으로 커널 내부 상태를 조회하거나 제어할 수 있다.
- sysfs는 복잡한 시스템 호출 없이도 커널 리소스에 접근할 수 있는 표준화된 인터페이스이다.
- sysfs 내부 파일은 디스크 상에 존재하는 정적 파일이 아니라, 커널이 RAM 상에서 동적으로 생성한 가상 파일이다.
- sysfs 인터페이스 기반 디바이스 드라이버
- 사용자 공간에 /sys 경로를 통해 장치 디렉토리 및 속성(attribute) 파일을 노출한다.
- show() 및 store() 함수를 통해 상태 조회와 설정 중심의 인터페이스를 제공한다.
- 바이트 단위 송수신이나 고속 데이터 처리에는 적합하지 않지만, 간단한 설정 또는 플래그 제어 목적에 적합하다.
- struct device를 명시적으로 생성하고, device_create_file()를 통해 속성을 등록해야 한다. (혹은 struct device 없이 순수 kobject만 가지고 sysfs 항목을 만들고자 할 때는 sysfs_create_file()를 사용함)
- 버스 드라이버(bus_type)를 사용하는 경우 디바이스 등록 시 내부적으로 device_register() 과정에서 kobject 및 sysfs 항목이 암시적으로 생성된다.
- 예를 들어, /sys/class/leds/led0/brightness 같은 파일은 실제로 존재하는 파일이 아니라, 사용자가 해당 경로에 접근할 경우 커널 내부에 등록된 핸들러 함수 포인터가 호출되어 처리되는 가상의 노드인 것이다.
목표
PCI, I2C, USB 등의 버스 드라이버 및 플랫폼 드라이버는, 커널이 struct device를 등록하는 과정에서 해당 구조체에 포함된 kobject를 기반으로 sysfs에 디렉토리를 자동 생성한다.
이 글에서 다루는 PCI 드라이버의 경우, /sys/bus/pci/devices/<dev_name> 경로에 sysfs 항목이 자동으로 생성되며, 코드상에서는 pdev->dev.kobj를 통해 해당 디바이스의 kobject에 접근할 수 있다. 이후 kobject에 제어할 속성을 등록함으로써, 사용자 공간에 sysfs 파일 형태로 노출할 수 있다.
(※ 본 글에서는 디바이스 객체(pdev->dev.kobj) 아래 바로 속성(attribute) 파일을 추가했으나, 프로젝트 성격에 따라 class_create() 를 통해 전용 클래스(Class)를 생성하고 그 아래에 속성 파일을 생성할 수도 있다. 클래스는 디바이스로의 심볼릭 링크를 제공하는거라 결국은 같긴 한데, 후자의 방법은 /sys/class/<class_name>/ 과 같이 사용자가 더 접근하기 쉽고 직관적인 경로를 제공할 수 있다는 장점이 있다.)
이번 글에서는 PCI 디바이스의 sysfs 인터페이스에 leds와 buttons라는 속성을 추가하여, 사용자가 아래와 같은 명령어를 통해 3개의 LED 출력을 제어하고, 3개의 버튼 상태를 조회할 수 있는 인터페이스를 구현하는 것을 목표로 한다.
buttons 상태 확인 :
$ cat /sys/bus/pci/devices/0000\:04\:00.0/buttons
$ 1 0 1
leds 설정 :
$ echo 1 0 1 > /sys/bus/pci/devices/0000\:04\:00.0/leds
코드 작성 흐름을 크게 보면 다음과 같다.
- DEVICE_ATTR() 매크로를 사용해 attribute 구조체를 정의한다.
- struct attribute * 배열과 attribute group 구조체를 통해 여러 attribute를 그룹으로 정의한다.
- sysfs_create_group() 함수로 kobject에 attribute group을 연결한다.
- 사용자 정의 store 및 show 함수를 작성한다.
- 드라이버 제거 시점에 sysfs_remove_group()으로 sysfs에 등록된 속성을 제거한다.
1. DEVICE_ATTR() 매크로를 사용해 attribute 구조체를 정의한다.
include/linux/device.h
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
#define DEVICE_ATTR_RW(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RW(_name)
#define DEVICE_ATTR_RO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RO(_name)
#define DEVICE_ATTR_WO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_WO(_name)
include/linux/sysfs.h
#define __ATTR(_name, _mode, _show, _store) { \
.attr = {.name = __stringify(_name), \
.mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \
.show = _show, \
.store = _store, \
}
#define __ATTR_RW(_name) __ATTR(_name, 0644, _name##_show, _name##_store)
#define __ATTR_RO(_name) { \
.attr = { .name = __stringify(_name), .mode = 0444 }, \
.show = _name##_show, \
}
#define __ATTR_WO(_name) { \
.attr = { .name = __stringify(_name), .mode = 0200 }, \
.store = _name##_store, \
}
즉 아래와같이 매크로 사용시
static DEVICE_ATTR_RO(buttons);
static DEVICE_ATTR_WO(leds);
다음과 같이 확장된다.
static struct device_attribute dev_attr_buttons = {
.attr = {
.name = "buttons",
.mode = 0444,
},
.show = buttons_show,
};
static struct device_attribute dev_attr_leds = {
.attr = {
.name = "leds",
.mode = 0200,
},
.store = leds_store,
};
2. struct attribute * 배열과 attribute group 구조체를 통해 여러 attribute를 그룹으로 정의한다.
static struct attribute *ch382l_attrs[] = {
&dev_attr_buttons.attr,
&dev_attr_leds.attr,
NULL,
};
static struct attribute_group ch382l_attr_group = {
.attrs = ch382l_attrs,
};
// 혹은 ATTRIBUTE_GROUPS(ch382l_attr); 매크로 함수 사용해서 생성 가능
kobject 자체에는 직접적으로 attribute나 attribute group을 저장하는 멤버 변수가 없다. 대신 sysfs_create_file() 또는 sysfs_create_group() 함수를 통해 동적으로 kobject와 연결하는 방식을 사용한다.
아래에서는 sysfs_create_group() 함수를 사용했는데, 대신 sysfs_create_file() 함수를 통해 개별 attribute를 하나씩 등록하는 방법도 있다. 만약 kobject에 등록할 속성의 개수가 적고, 추가될 가능성이 전혀 없다면 그렇게 해도 무방하다.
하지만 향후 확장성까지 고려한다면 attribute_group을 정의하여 sysfs_create_group() 함수를 통해 여러 속성을 한 번에 등록하는 것이 유리하다. 이후 속성이 추가된다면 코드를 수정하지 않고 구조체 정의만 수정하면 되니까 말이다.
3. sysfs_create_group() 함수로 kobject에 attribute group을 연결한다.
kobject에 attribute group을 추가하는 함수
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
- kobj : 속성을 추가할 대상 kobject
- grp : 추가할 속성 그룹
- 리턴 값 : 성공 시 0, 실패 시 음수 에러 코드
kobject에 attribute를 추가하는 함수
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
- kobj : 속성을 추가할 대상 kobject
- attr : 등록할 속성, 보통 struct device_attribute에서 .attr 필드로전달된다.
4. 사용자 정의 store 및 show 함수를 작성한다.
속성 파일을 읽을 때 호출되는 콜백함수로 buf에 데이터를 출력해야 하며, 출력된 데이터 크기를 반환한다.
ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf);
- dev : sysfs 속성이 속한 디바이스
- attr : 현재 속성의 구조체 정보
- buf : 사용자 공간으로 데이터를 전달할 버퍼
- 리턴 값 : sprintf 등을 통해 buf에 기록된 데이터 크기를 반환하며, 오류 발생 시 -EINVAL 등 음수 값을 반환한다.
속성 파일에 데이터를 쓸 때 호출되는 콜함수로 buf에 입력 데이터를 받아 파싱하고, 장치 상태를 변경한다.
ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);
- dev : sysfs 속성이 속한 디바이스
- attr : 현재 속성의 구조체 정보
- buf : 사용자 공간에서 전달된 데이터 버퍼
- count : 사용자 공간에서 전달된 데이터 크기
- 리턴 값 : count를 그대로 반환하면 정상 처리된 바이트 수를 의미한다. 오류 발생 시 -EINVAL 등 음수 값을 반환한다.
5. 드라이버 제거 시점에 sysfs_remove_group로 sysfs에 등록된 속성을 제거한다.
kobject에서 attribute group을 제거하는 함수
void sysfs_remove_group(struct kobject *kobj, const struct attribute_group *grp);
- kobj : 속성을 제거할 대상 kobject
- grp : 제거할 속성 그룹
kobject에서 attribute를 제거하는 함수
void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);
- kobj : 속성을 제거할 대상 kobject
- grp : 제거할 속성
전체 코드
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h>
#include <linux/io.h>
#include <linux/sysfs.h> // sysfs 관련 API 사용을 위해 추가
#undef pr_fmt
#define pr_fmt(fmt) KBUILD_MODNAME ": %s, " fmt, __func__
#define CH382L_VENDOR_ID 0x1C00
#define CH382L_DEVICE_ID 0x3050
#define BAR2 2
#define PDR_OFFSET 0x0
#define PSR_OFFSET 0x1
struct ch382l_data {
void __iomem *bar2_base;
u8 button_state; // 버튼 상태 저장
u8 led_state; // LED 상태 저장
struct pci_dev *pdev; // PCI 디바이스 포인터
};
// buttons 읽기 핸들러
static ssize_t buttons_show(struct device *dev, struct device_attribute *attr, char *buf) {
struct ch382l_data *data = dev_get_drvdata(dev);
// 버튼 상태 읽기
data->button_state = (ioread8(data->bar2_base + PSR_OFFSET) >> 4) & 0x7;
// 3개의 버튼 상태 반환
return sprintf(buf, "%d %d %d\n",
((data->button_state & 0x4) >> 2),
((data->button_state & 0x2) >> 1),
(data->button_state & 0x1));
}
// leds 쓰기 핸들러
static ssize_t leds_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) {
struct ch382l_data *data = dev_get_drvdata(dev);
int ret;
u8 led_values[3];
// 입력값 파싱
ret = sscanf(buf, "%hhu %hhu %hhu", &led_values[2], &led_values[1], &led_values[0]);
if(ret != 3) {
return -EINVAL;
}
// LED 상태 업데이트
data->led_state = ((led_values[2] & 0x1) << 2) |
((led_values[1] & 0x1) << 1) |
(led_values[0] & 0x1);
// 디바이스에 LED 상태 쓰기
iowrite8(data->led_state << 3, data->bar2_base + PDR_OFFSET);
return count;
}
// sysfs attribute를 정의하는 매크로를 사용해 dev_attr_## 이름의 atrribute 구조체를 생성한다.
static DEVICE_ATTR_RO(buttons); // buttons 속성을 읽기 전용으로 생성
static DEVICE_ATTR_WO(leds); // leds 속성을 쓰기 전용으로 생성
// attribute_group에 포함될 attribute를 관리하기 위한 배열을 정의
static struct attribute *ch382l_attrs[] = {
&dev_attr_buttons.attr,
&dev_attr_leds.attr,
NULL,
};
// attribute_group을 정의
static struct attribute_group ch382l_attr_group = {
.attrs = ch382l_attrs,
};
static int ch382l_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
struct ch382l_data *data;
int ret;
pr_info("Probing device\n");
ret = pcim_enable_device(pdev);
if (ret) {
pr_info("Failed to enable PCI device\n");
return ret;
}
ret = pcim_iomap_regions(pdev, BIT(BAR2), "ch382l_bar");
if (ret) {
pr_info("Failed to map BAR2 I/O region\n");
return ret;
}
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if(!data) {
pr_info("Failed to allocate memory for device data\n");
return -ENOMEM;
}
pci_set_drvdata(pdev, data);
data->pdev = pdev;
data->bar2_base = pcim_iomap_table(pdev)[BAR2];
if(!data->bar2_base) {
pr_info("Failed to read mapped BAR2\n");
return -ENOMEM;
}
// pci_dev에 sysfs 속성 그룹 추가
ret = sysfs_create_group(&pdev->dev.kobj, &ch382l_attr_group);
if(ret) {
pr_info("Failed to create sysfs group\n");
return ret;
}
pr_info("Device initialized successfully at /sys/bus/pci/devices/%s/\n", dev_name(&pdev->dev));
return 0;
}
static void ch382l_remove(struct pci_dev *pdev) {
// sysfs 속성 그룹 제거
sysfs_remove_group(&pdev->dev.kobj, &ch382l_attr_group);
pr_info("Device removed\n");
}
static const struct pci_device_id ch382l_pci_ids[] = {
{ PCI_DEVICE(CH382L_VENDOR_ID, CH382L_DEVICE_ID) },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, ch382l_pci_ids);
static struct pci_driver ch382l_driver = {
.name = "ch382l_driver",
.id_table = ch382l_pci_ids,
.probe = ch382l_probe,
.remove = ch382l_remove,
};
module_pci_driver(ch382l_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JSH");
MODULE_DESCRIPTION("A simple LKM using sysfs interface for a PCI to parallel port adapter CH382L");
모듈을 로드한 뒤 확인해보면 /sys/bus/pci/devices/<dev_name>/ 경로에 buttons와 leds 파일이 생성된 것을 볼 수 있다.


다만 WO 속성으로 생성된 leds 파일의 권한은 200으로, 일반 사용자로 해당 파일에 쓰기를 시도하면 권한 오류로 실패하게 된다.
단순히 sudo echo를 써도 실패하는 건 마찬가지이다. echo 명령어는 는 sudo로 실행되지만 > 리디렉션(출력 재지정)은 현재 셸의 권한으로 실행되기 때문이다.
이때는 아래처럼 echo <값> | sudo tee <sysfs 경로>로 시도해야한다. 이렇게 하면 echo가 출력한 값을 tee가 받아 루트 권한으로 해당 경로에 기록할 수 있다.
- | (파이프) : 왼쪽 명령의 표준 출력을 오른쪽 명령의 표준 입력으로 전달
- tee : 표준 입력을 받아서 동시에 표준 출력과 파일에 기록
leds 설정 :
$ echo 1 0 1 | sudo tee /sys/bus/pci/devices/0000\:04\:00.0/leds

buttons 상태 확인 :
$ cat /sys/bus/pci/devices/0000\:04\:00.0/buttons
$ 1 0 1

'임베디드 개발 > 임베디드 리눅스' 카테고리의 다른 글
| TCA9548A 데이터시트 및 리눅스 드라이버 분석 (Tested on Raspberry Pi 4) (0) | 2025.04.06 |
|---|---|
| LDD ] Device Tree (DT, 디바이스 트리) (0) | 2025.03.27 |
| LDD ] 커널 메모리 할당 함수 (kmalloc, kzalloc 등) (0) | 2025.03.12 |
| LDD ] PCIe 디바이스 드라이버 작성하기 - (3) with Interrupt (0) | 2025.03.03 |
| LDD ] 커널 시간 관련 함수 사용법 (0) | 2025.02.27 |