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

LDD ] PCIe 디바이스 드라이버 작성하기 - (4) with sysfs

by eteo 2025. 3. 18.

 

 

 

이번 글 역시 PCIe 디바이스를 다루는 것보다 디바이스 드라이버 작성 방식에 대해 초점을 맞추도록 한다.

 

 

 

디바이스 드라이버 with sysfs

 

직전 글에서 사용한 코드를 약간 수정하여 sysfs 인터페이스를 구현하는 디바이스 드라이버를 작성해보도록 하자.

 

 

sysfs란?

sysfs는 커널 객체(Kernel Object, kobject) 및 해당 객체의 속성 정보(attribute)를 사용자 공간에 노출하는 파일 시스템을 말하며, /sys 디렉토리에 마운트된다.

 

sysfs에서 커널 객체(kobject)는 디렉토리를 나타내고, 속성(attribute)는 디렉토리 내의 파일로 표현되어 해당 객체의 상태 정보 또는 설정값을 제공한다.

 

그리고 리눅스 커널의 디바이스(struct device)와 드라이버(struct driver)는 각각 커널 객체(kobject)를 멤버로 포함하고 있다. 이러한 설계로 인해 모든 디바이스와 드라이버가 kobject를 통해 sysfs에 디렉토리 구조로 나타나며, 관련 속성(attribute)은 해당 디렉토리 아래의 파일 형태로 노출된다.

 

결론적으로, sysfs는 리눅스 시스템의 디바이스와 드라이버를 계층적이고 체계적으로 표현하는 역할을 하는 것이다.

 

 

📝 참고.

캐릭터 디바이스 드라이버가 /dev 노출을 통해 바이트 단위 데이터 송수신을 처리하는 인터페이스라면, sysfs 인터페이스는 주로 장치의 상태를 조회하거나 속성 정보를 설정하는 데 사용되는 인터페이스이다. 캐릭터 디바이스 드라이버는 기본적으로 cdev(character device 구조체)를 사용하여 구현되고, 자동으로 sysfs에 노출되지는 않는다. 만약 sysfs에 디바이스 정보를 노출하려면 명시적으로 struct deivce를 생성하고 등록하는 과정을 수행해야한다.

 

 

 

목표

 

PCI, I2C, USB 등 버스 드라이버와 플랫폼 드라이버의 경우 커널이 디바이스(struct device)를 등록하는 과정에서 sysfs에 kobject를 자동으로 생성한다.

 

즉, PCI 드라이버의 경우 /sys/bus/pci/devices/<dev_name> 경로에 sysfs 엔트리가 자동으로 만들어지니, 단순히 pdev->dev.kobj를 통해 kobject에 접근하여, 사용자 공간에 노출하고자 하는 특정 하드웨어 속성을 쉽게 추가할 수 있다.

 

이번 글에서는 PCI 디바이스 sysfs에 leds와 buttons라는 속성을 추가하여 사용자가 다음과 같은 방법으로 3개의 leds를 출력을 설정하고 3개의 buttons 상태를 읽을 수 있는 인터페이스를 구현하는 것이 목표이다.

 

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

 

 

 

 

 

코드 작성 흐름을 크게 보면 다음과 같다.

 

1. DEVICE_ATTR 매크로를 사용해 attribute 구조체를 생성한다.

2. attribute group 정의를 통해 여러 attribute를 한번에 정의한다.

3. sysfs_create_group() 함수로 kobject에 attribute group을 연결한다.

4. 사용자 정의 store 및 show 함수를 작성한다.

5. 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. 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,
};

 

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 파일이 생성된 것을 볼 수 있다.

 

 

 

다만 여기서 봐야할 건 leds의 권한이 200이라는 점인데 처음에는 leds 파일에 쓰기 위해 echo에 sudo를 붙여 실행해봤지만 안됐다. echo의 경우 쉘의 빌트인 명령어라서 앞에 sudo를 붙여도 일반 사용자 권한으로 실행되기 때문이라고 한다. 이에 대한 해결법은 여러가지가 있으며 다음의 방법들이 있을 수 있다.

 

✔️ root 권한으로 로그인하기

    $ su

 

✔️  sudo tee 명령을 통해 root 권한으로 파일에 쓰도록 하기

    $ echo 1 0 1 | sudo tee /sys/bus/pci/devices/0000\:04\:00.0/leds

 

✔️  chmod 명령을 사용해 일시적으로 파일 권한 변경하기

    $ sudo chmod 222 > /sys/bus/pci/devices/0000\:04\:00.0/leds

 

✔️  udev 규칙을 추가해서 모듈 로드 시점에 파일 권한을 변경하기

 

 

 

leds 설정 :

$ echo 1 0 1 > /sys/bus/pci/devices/0000\:04\:00.0/leds

 

 

 

 

buttons 상태 확인 :

$ cat /sys/bus/pci/devices/0000\:04\:00.0/buttons
$ 1 0 1