Input 서브시스템

Linux Input 서브시스템을 이벤트 라우팅(Routing)과 사용자 경험 품질 관점에서 심층 분석합니다. input_dev/input_handler/evdev 계층의 역할, 키보드·마우스·터치·센서 이벤트 타입 해석, 멀티터치 슬롯 프로토콜, debounce·repeat·calibration 처리, IRQ 기반 이벤트 수집과 지연(Latency) 처리 분리, power key/wakeup 같은 전원 연계 동작, udev/libinput으로 전달되는 사용자 공간(User Space) 경로, evtest/libinput-debug-events를 활용한 문제 분석까지 입력 장치 드라이버(Device Driver) 실전에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버Workqueue 문서를 먼저 읽으세요. 입출력(I/O) 인터페이스 드라이버는 데이터 경로와 제어 경로를 동시에 다루므로 큐/버퍼(Buffer)/비동기 처리 경계를 먼저 구분해야 합니다.
일상 비유: 이 주제는 콜센터 접수와 처리 라인 분리와 비슷합니다. 요청 접수와 실제 처리를 분리해 병목(Bottleneck)을 줄이듯이, 드라이버도 IRQ·큐·작업 스레드(Thread)를 역할별로 나눠야 안정적입니다.

핵심 요약

  • 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
  • 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
  • IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
  • 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
  • 운영 복구 — 오류 시 재초기화와 롤백(Rollback) 경로를 준비합니다.

단계별 이해

  1. 장치 수명주기 확인
    probe부터 remove까지 흐름을 점검합니다.
  2. 비동기 경로 설계
    IRQ, 워크큐, 타이머(Timer) 역할을 분리합니다.
  3. 자원 정합성 검증
    DMA/클록/전원 참조를 교차 확인합니다.
  4. 현장 조건 테스트
    연결 끊김/복구/부하 상황을 재현합니다.
관련 페이지(Page): 기본 디바이스 드라이버 모델은 디바이스 드라이버, 버스(Bus) 프레임워크는 I2C / SPI / GPIO 페이지를 참고하세요.

Input 서브시스템 (키보드, 마우스, 터치)

리눅스 Input 서브시스템(drivers/input/)은 키보드, 마우스, 터치스크린, 조이스틱, 리모컨 등 모든 종류의 입력 장치를 통합 관리하는 프레임워크입니다. 하드웨어별 드라이버가 이벤트를 생성하면, Input Core가 이를 적절한 핸들러(evdev, kbd, mousedev 등)로 라우팅하여 유저 공간에 전달합니다.

Input 서브시스템 아키텍처 User Space libinput / X11 evtest / evemu SDL / Qt Input systemd-logind console (VT) /dev/input/eventN, mouseN, jsN Event Handlers (input_handler) evdev kbd mousedev joydev rfkill-input input-leds input_handle (연결) Input Core (drivers/input/input.c) 디바이스 등록 · 이벤트 라우팅 · handler 매칭 · input_event 전파 Input Device Drivers (input_dev) atkbd hid-generic gpio-keys goodix_ts i2c-hid xpad custom Hardware PS/2 (i8042) USB HID I2C 터치패널 SPI 디지타이저 GPIO 버튼 Bluetooth HID serio (PS/2) · USB · I2C · SPI · GPIO · Bluetooth (hidp)

Input 서브시스템은 3계층으로 구성됩니다:

input_dev ↔ input_handler 매칭과 input_handle 연결 input_dev (디바이스) atkbd (PS/2 키보드) EV_KEY, EV_LED, EV_MSC, EV_REP psmouse (마우스) EV_REL, EV_KEY (BTN_LEFT...) goodix_ts (터치) EV_ABS, EV_KEY (BTN_TOUCH) xpad (게임패드) EV_ABS, EV_KEY, EV_FF gpio-keys EV_KEY (KEY_POWER 등) input_handler (핸들러) evdev (모든 디바이스) kbd (EV_KEY) mousedev (EV_REL/BTN) joydev (BTN_JOYSTICK) input-leds (EV_LED) input_handle dev + handler + open count 각 연결선 = input_handle 인스턴스 (N:M 관계) evdev는 flags=0으로 모든 디바이스에 매칭 / 다른 핸들러는 capability 기반 선택적 매칭

핵심 데이터 구조

/* === struct input_dev — Input 디바이스를 나타내는 핵심 구조체 ===
 * include/linux/input.h
 * 하드웨어 드라이버가 할당·등록하며, 지원하는 이벤트 유형과 코드를 비트마스크로 선언 */
struct input_dev {
    const char *name;           /* 사람이 읽을 수 있는 디바이스 이름 */
    const char *phys;           /* 물리적 경로 (예: "usb-0000:00:1d.0-1/input0") */
    const char *uniq;           /* 고유 식별자 (시리얼 번호 등) */
    struct input_id id;          /* bustype, vendor, product, version */

    /* 이벤트 능력(capability) 비트마스크 */
    unsigned long evbit[BITS_TO_LONGS(EV_CNT)];    /* 지원 이벤트 유형 */
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];  /* 지원 키 코드 */
    unsigned long relbit[BITS_TO_LONGS(REL_CNT)];  /* 상대축 코드 */
    unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];  /* 절대축 코드 */
    unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];  /* 기타 이벤트 */
    unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];  /* LED 상태 */
    unsigned long swbit[BITS_TO_LONGS(SW_CNT)];    /* 스위치 상태 */
    unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];    /* Force Feedback */

    /* 절대축 파라미터 (min, max, fuzz, flat, resolution) */
    struct input_absinfo *absinfo;

    /* 콜백 함수 */
    int (*open)(struct input_dev *dev);    /* 첫 핸들러 연결 시 */
    void (*close)(struct input_dev *dev);  /* 마지막 핸들러 해제 시 */
    int (*event)(struct input_dev *dev,
                 unsigned int type,
                 unsigned int code, int value); /* LED/FF 출력 이벤트 */

    struct device dev;            /* 내장 device 구조체 */
    struct list_head h_list;     /* 연결된 input_handle 리스트 */
    struct list_head node;       /* 전역 input_dev 리스트 */
};
/* === struct input_handler — 이벤트 핸들러 (evdev, kbd, mousedev 등) ===
 * Input Core에 등록되어 매칭되는 디바이스의 이벤트를 처리 */
struct input_handler {
    void (*event)(struct input_handle *handle,
                  unsigned int type,
                  unsigned int code, int value);
    void (*events)(struct input_handle *handle,
                   const struct input_value *vals,
                   unsigned int count);         /* 배치 이벤트 (성능 최적화) */
    bool (*filter)(struct input_handle *handle,
                   unsigned int type,
                   unsigned int code, int value); /* 이벤트 필터링 */
    bool (*match)(struct input_handler *handler,
                  struct input_dev *dev);        /* 추가 매칭 조건 */
    int  (*connect)(struct input_handler *handler,
                    struct input_dev *dev,
                    const struct input_device_id *id);
    void (*disconnect)(struct input_handle *handle);

    const char *name;
    const struct input_device_id *id_table;   /* 매칭 테이블 */
    struct list_head h_list;                   /* 연결된 handle 리스트 */
    struct list_head node;                     /* 전역 handler 리스트 */
};
/* === struct input_handle — input_dev와 input_handler 연결 ===
 * 하나의 디바이스는 여러 핸들러에 연결 가능 (예: evdev + kbd 동시) */
struct input_handle {
    void *private;                     /* 핸들러별 개인 데이터 */
    int open;                           /* 열린 파일 디스크립터 수 */
    const char *name;
    struct input_dev *dev;              /* 연결된 디바이스 */
    struct input_handler *handler;      /* 연결된 핸들러 */
    struct list_head d_node;            /* dev->h_list 노드 */
    struct list_head h_node;            /* handler->h_list 노드 */
};

/* === struct input_event — 유저 공간에 전달되는 이벤트 구조 ===
 * /dev/input/eventN에서 read() 시 이 구조체 배열로 전달됨 */
struct input_event {
    struct timeval time;   /* 이벤트 발생 시각 (또는 input_event_usec) */
    __u16 type;             /* EV_KEY, EV_REL, EV_ABS ... */
    __u16 code;             /* KEY_A, REL_X, ABS_MT_POSITION_X ... */
    __s32 value;            /* 키: 1(press)/0(release)/2(repeat), 축: 좌표값 */
};

이벤트 유형 상세

이벤트 유형상수용도주요 코드 예시
동기화EV_SYN0x00이벤트 그룹 경계 표시SYN_REPORT, SYN_MT_REPORT, SYN_DROPPED
키/버튼EV_KEY0x01키보드, 마우스 버튼, 게임패드KEY_A, BTN_LEFT, BTN_TOUCH
상대 이동EV_REL0x02마우스 이동, 스크롤 휠REL_X, REL_Y, REL_WHEEL
절대 위치EV_ABS0x03터치스크린, 태블릿, 조이스틱ABS_X, ABS_MT_POSITION_X, ABS_MT_SLOT
기타EV_MSC0x04스캔코드 등 잡다한 이벤트MSC_SCAN, MSC_TIMESTAMP
스위치EV_SW0x05덮개(lid), 헤드폰 잭, 태블릿 모드SW_LID, SW_HEADPHONE_INSERT
LEDEV_LED0x11키보드 LED 제어LED_CAPSL, LED_NUML, LED_SCROLLL
사운드EV_SND0x12비프, 클릭 사운드SND_BELL, SND_TONE
반복EV_REP0x14키 반복 파라미터REP_DELAY, REP_PERIOD
Force FeedbackEV_FF0x15진동/햅틱/포스 피드백FF_RUMBLE, FF_CONSTANT, FF_PERIODIC
전원EV_PWR0x16전원 버튼 이벤트(예약됨)
FF 상태EV_FF_STATUS0x17FF 효과 재생 상태FF_STATUS_STOPPED, FF_STATUS_PLAYING
EV_SYN과 이벤트 패킷(Packet): 입력 이벤트는 패킷 단위로 전달됩니다. 하나의 동작(예: 마우스 대각선 이동)은 EV_REL/REL_X, EV_REL/REL_Y 이벤트 후 EV_SYN/SYN_REPORT로 묶입니다. 유저 공간은 SYN_REPORT를 받을 때까지 버퍼링해야 합니다. SYN_DROPPED를 수신하면 이벤트 누락이 발생한 것이므로 디바이스 상태를 다시 동기화해야 합니다.

Input 디바이스 등록

#include <linux/input.h>
#include <linux/module.h>
#include <linux/platform_device.h>

struct my_kbd_data {
    struct input_dev *idev;
    int irq;
};

/* 인터럽트 핸들러 — 하드웨어에서 키 이벤트 수신 */
static irqreturn_t my_kbd_irq(int irq, void *dev_id)
{
    struct my_kbd_data *data = dev_id;
    u8 scancode;

    /* 하드웨어에서 스캔코드 읽기 (실제로는 MMIO/I2C/SPI 등) */
    scancode = read_hw_scancode();

    /* 이벤트 보고 — Input Core가 연결된 모든 handler에 전파 */
    input_report_key(data->idev, scancode, 1);  /* 키 누름 */
    input_sync(data->idev);                      /* SYN_REPORT */

    /* 키 해제 (간단한 예제 — 실제로는 별도 인터럽트) */
    input_report_key(data->idev, scancode, 0);  /* 키 해제 */
    input_sync(data->idev);

    return IRQ_HANDLED;
}

static int my_kbd_probe(struct platform_device *pdev)
{
    struct my_kbd_data *data;
    struct input_dev *idev;
    int ret, i;

    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    /* === 1. devm_input_allocate_device — managed 할당 ===
     * 드라이버 해제 시 자동으로 input_free_device() 호출됨
     * input_allocate_device()와 달리 수동 free 불필요 */
    idev = devm_input_allocate_device(&pdev->dev);
    if (!idev)
        return -ENOMEM;

    /* === 2. 디바이스 식별 정보 설정 === */
    idev->name = "My Custom Keyboard";
    idev->phys = "my-kbd/input0";
    idev->id.bustype = BUS_HOST;       /* BUS_USB, BUS_I2C, BUS_SPI 등 */
    idev->id.vendor  = 0x1234;
    idev->id.product = 0x5678;
    idev->id.version = 0x0100;

    /* === 3. 이벤트 능력(capability) 선언 ===
     * 이 디바이스가 생성할 수 있는 이벤트 유형과 코드를 선언
     * Input Core가 이 정보로 적절한 handler를 매칭 */
    input_set_capability(idev, EV_KEY, KEY_A);     /* EV_KEY + KEY_A 동시 설정 */
    input_set_capability(idev, EV_KEY, KEY_B);

    /* 또는 set_bit로 개별 설정 */
    set_bit(EV_KEY, idev->evbit);
    for (i = KEY_ESC; i <= KEY_MICMUTE; i++)
        set_bit(i, idev->keybit);

    /* 키 반복(autorepeat) 자동 지원 */
    set_bit(EV_REP, idev->evbit);

    /* === 4. open/close 콜백 (선택) ===
     * 유저 공간에서 디바이스 열기/닫기 시 호출
     * 전력 절약: 열린 핸들러가 없으면 하드웨어 비활성화 */
    idev->open  = my_kbd_open;
    idev->close = my_kbd_close;

    /* === 5. 인터럽트 등록 === */
    data->irq = platform_get_irq(pdev, 0);
    if (data->irq < 0)
        return data->irq;

    ret = devm_request_irq(&pdev->dev, data->irq,
                           my_kbd_irq, IRQF_TRIGGER_FALLING,
                           "my-kbd", data);
    if (ret)
        return ret;

    data->idev = idev;
    platform_set_drvdata(pdev, data);

    /* === 6. 디바이스 등록 ===
     * Input Core에 디바이스 등록 → 매칭되는 handler와 자동 연결
     * 등록 후에는 capability 변경 금지 */
    ret = input_register_device(idev);
    if (ret)
        return ret;  /* devm이므로 idev는 자동 해제 */

    dev_info(&pdev->dev, "keyboard registered\\n");
    return 0;
}
input_set_capability() vs set_bit(): input_set_capability(dev, EV_KEY, KEY_A)set_bit(EV_KEY, dev->evbit)set_bit(KEY_A, dev->keybit)를 한 번에 수행합니다. 이벤트 유형(evbit)을 빠뜨리는 실수를 방지하므로 가능하면 input_set_capability()를 사용하세요.

이벤트 보고 API

함수이벤트 유형설명
input_report_key(dev, code, value)EV_KEY키/버튼 누름(1), 해제(0), 반복(2)
input_report_rel(dev, code, value)EV_REL상대 이동 (마우스 X/Y, 스크롤)
input_report_abs(dev, code, value)EV_ABS절대 좌표 (터치, 조이스틱)
input_report_switch(dev, code, value)EV_SW스위치 상태 (lid, 잭 등)
input_event(dev, type, code, value)모든 유형범용 이벤트 보고 (위 함수들의 기반)
input_sync(dev)EV_SYNSYN_REPORT — 이벤트 패킷 완료 표시
input_mt_sync(dev)EV_SYNSYN_MT_REPORT — MT Protocol A 슬롯 구분
/* 마우스 이동 + 클릭 보고 예제 */
input_report_rel(idev, REL_X, dx);
input_report_rel(idev, REL_Y, dy);
input_report_rel(idev, REL_WHEEL, wheel);
input_report_key(idev, BTN_LEFT, left_pressed);
input_sync(idev);   /* 하나의 패킷으로 원자적 전달 */

/* 절대 좌표 + 압력 보고 예제 (태블릿) */
input_report_abs(idev, ABS_X, x);
input_report_abs(idev, ABS_Y, y);
input_report_abs(idev, ABS_PRESSURE, pressure);
input_report_key(idev, BTN_TOUCH, pressure > 0);
input_sync(idev);

절대축 파라미터 (ABS)

/* input_set_abs_params(dev, axis, min, max, fuzz, flat)
 *
 * min/max  : 축의 유효 범위
 * fuzz     : 노이즈 필터링 — |new - old| < fuzz이면 무시 (jitter 제거)
 * flat     : 데드존 — |value| < flat이면 0으로 처리 (조이스틱 중앙)
 * resolution: input_abs_set_res()로 별도 설정 (units/mm 등)
 */

/* 터치스크린: 1920x1080 해상도, 노이즈 필터링 4픽셀 */
input_set_abs_params(idev, ABS_X, 0, 1920, 4, 0);
input_set_abs_params(idev, ABS_Y, 0, 1080, 4, 0);
input_set_abs_params(idev, ABS_PRESSURE, 0, 255, 0, 0);

/* resolution 설정 — libinput이 물리적 크기 계산에 사용 */
input_abs_set_res(idev, ABS_X, 40);   /* 40 units/mm */
input_abs_set_res(idev, ABS_Y, 40);

/* 조이스틱: -32768 ~ 32767, 데드존 4096 */
input_set_abs_params(idev, ABS_X, -32768, 32767, 16, 4096);
input_set_abs_params(idev, ABS_Y, -32768, 32767, 16, 4096);

멀티터치 프로토콜

리눅스 커널은 멀티터치를 위한 두 가지 프로토콜을 정의합니다 (Documentation/input/multi-touch-protocol.rst):

항목Protocol A (Deprecated)Protocol B (현재 표준)
슬롯 관리커널이 관리하지 않음커널이 슬롯 할당·추적
터치 구분SYN_MT_REPORT로 분리ABS_MT_SLOT + ABS_MT_TRACKING_ID
대역폭(Bandwidth)모든 터치를 매번 전송변경된 슬롯만 전송 (효율적)
유저 공간ID 추적을 직접 해야 함슬롯 기반으로 자연스러운 추적
사용처레거시 터치 컨트롤러최신 터치스크린 (goodix, atmel, etc.)
/* === Multitouch Protocol B — 슬롯 기반 (권장) ===
 * input_mt_init_slots()로 슬롯 수를 미리 선언
 * 각 슬롯에 tracking_id를 할당하여 터치 추적 */

#include <linux/input/mt.h>

static int ts_probe(struct i2c_client *client)
{
    struct input_dev *idev;
    int ret;

    idev = devm_input_allocate_device(&client->dev);

    /* 절대 좌표 설정 */
    input_set_abs_params(idev, ABS_MT_POSITION_X, 0, 1920, 0, 0);
    input_set_abs_params(idev, ABS_MT_POSITION_Y, 0, 1080, 0, 0);
    input_set_abs_params(idev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
    input_set_abs_params(idev, ABS_MT_PRESSURE, 0, 255, 0, 0);

    /* 최대 10개 동시 터치, DIRECT = 터치스크린 (POINTER = 터치패드) */
    ret = input_mt_init_slots(idev, 10, INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
    if (ret)
        return ret;

    input_set_capability(idev, EV_KEY, BTN_TOUCH);

    return input_register_device(idev);
}

/* 터치 이벤트 보고 (인터럽트 핸들러 또는 threaded irq에서) */
static void ts_report_touches(struct input_dev *idev,
                              struct touch_point *tp, int count)
{
    int i;

    for (i = 0; i < count; i++) {
        /* 슬롯 선택 — 같은 tracking_id를 같은 슬롯에 유지 */
        input_mt_slot(idev, tp[i].slot);

        /* tracking_id 할당: 양수=활성 터치, -1=터치 해제 */
        input_mt_report_slot_state(idev, MT_TOOL_FINGER, tp[i].active);

        if (tp[i].active) {
            input_report_abs(idev, ABS_MT_POSITION_X, tp[i].x);
            input_report_abs(idev, ABS_MT_POSITION_Y, tp[i].y);
            input_report_abs(idev, ABS_MT_TOUCH_MAJOR, tp[i].width);
            input_report_abs(idev, ABS_MT_PRESSURE, tp[i].pressure);
        }
    }

    /* INPUT_MT_DROP_UNUSED: 보고되지 않은 슬롯 자동 해제 */
    input_mt_sync_frame(idev);

    /* 단일 터치 이벤트도 함께 생성 (Protocol B가 자동 처리) */
    input_sync(idev);
}
input_mt_sync_frame() 필수: Protocol B에서 INPUT_MT_DROP_UNUSED 플래그를 사용하면 input_mt_sync_frame()을 반드시 호출해야 합니다. 이 함수가 현재 프레임에서 보고되지 않은 슬롯을 자동으로 비활성화합니다. 이를 빠뜨리면 "유령 터치(ghost touch)"가 발생합니다.
MT 축 코드설명일반적인 범위
ABS_MT_SLOT현재 슬롯 인덱스0 ~ (max_slots - 1)
ABS_MT_TRACKING_ID터치 추적 ID (-1 = 해제)자동 할당
ABS_MT_POSITION_X/Y터치 중심 좌표디바이스 해상도
ABS_MT_TOUCH_MAJOR터치 영역 장축 길이0 ~ 255
ABS_MT_TOUCH_MINOR터치 영역 단축 길이0 ~ 255
ABS_MT_WIDTH_MAJOR접근 도구(손가락) 너비0 ~ 255
ABS_MT_PRESSURE터치 압력0 ~ 255
ABS_MT_ORIENTATION터치 타원 방향-90 ~ 90
ABS_MT_TOOL_TYPEMT_TOOL_FINGER / MT_TOOL_PEN도구 유형
ABS_MT_DISTANCE표면과의 거리 (호버링)0 = 접촉
멀티터치 프로토콜 A vs B 이벤트 시퀀스 Protocol A (Deprecated) 매 프레임마다 모든 터치 전송 Frame 1 (2개 터치 활성) ABS_MT_POSITION_X 100 ABS_MT_POSITION_Y 200 ABS_MT_PRESSURE 50 SYN_MT_REPORT ← 터치 #0 끝 ABS_MT_POSITION_X 400 ABS_MT_POSITION_Y 500 ABS_MT_PRESSURE 60 SYN_MT_REPORT ← 터치 #1 끝 SYN_REPORT ← 프레임 끝 Frame 2 (터치 #0 이동) ABS_MT_POSITION_X 110 ← #0 변경 ABS_MT_POSITION_Y 205 ABS_MT_PRESSURE 50 SYN_MT_REPORT ABS_MT_POSITION_X 400 ← #1 동일! ABS_MT_POSITION_Y 500 ABS_MT_PRESSURE 60 SYN_MT_REPORT SYN_REPORT Protocol B (현재 표준) 변경된 슬롯만 전송 (효율적) Frame 1 (2개 터치 활성) ABS_MT_SLOT 0 ABS_MT_TRACKING_ID 45 ABS_MT_POSITION_X 100 ABS_MT_POSITION_Y 200 ABS_MT_PRESSURE 50 ABS_MT_SLOT 1 ABS_MT_TRACKING_ID 46 ABS_MT_POSITION_X 400 ABS_MT_POSITION_Y 500 SYN_REPORT Frame 2 (터치 #0만 이동) ABS_MT_SLOT 0 ABS_MT_POSITION_X 110 ← 변경분만! ABS_MT_POSITION_Y 205 SYN_REPORT ← 슬롯 1은 생략 터치 해제 (슬롯 0) ABS_MT_SLOT 0 ABS_MT_TRACKING_ID -1 ← 터치 종료

폴링(Polling) Input 디바이스

인터럽트(Interrupt)를 지원하지 않는 하드웨어의 경우, input_setup_polling()을 사용하여 커널이 주기적으로 하드웨어를 폴링합니다. 이전의 input_polled_dev 구조체(Struct)는 deprecated되었으며, v5.12부터 통합 API로 대체되었습니다.

/* === 폴링 Input 디바이스 예제 (v5.12+) === */
static void my_sensor_poll(struct input_dev *idev)
{
    struct my_sensor *sensor = input_get_drvdata(idev);
    int x, y;

    /* 하드웨어에서 현재 값 읽기 */
    x = read_sensor_x(sensor);
    y = read_sensor_y(sensor);

    input_report_abs(idev, ABS_X, x);
    input_report_abs(idev, ABS_Y, y);
    input_sync(idev);
}

static int my_sensor_probe(struct i2c_client *client)
{
    struct input_dev *idev;
    struct my_sensor *sensor;

    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    idev = devm_input_allocate_device(&client->dev);

    idev->name = "My Analog Sensor";
    input_set_abs_params(idev, ABS_X, 0, 4095, 2, 0);
    input_set_abs_params(idev, ABS_Y, 0, 4095, 2, 0);

    input_set_drvdata(idev, sensor);

    /* 폴링 설정: 콜백 + 주기 */
    input_setup_polling(idev, my_sensor_poll);
    input_set_poll_interval(idev, 20);       /* 20ms = 50Hz */
    input_set_min_poll_interval(idev, 10);   /* 최소 10ms */
    input_set_max_poll_interval(idev, 100);  /* 최대 100ms */

    /* 유저 공간에서 poll_interval 조정 가능:
     * /sys/class/input/inputN/device/poll_interval */

    return input_register_device(idev);
}
폴링 vs 인터럽트 기반 입력 처리 비교 인터럽트 기반 (권장) Hardware IRQ 발생 request_threaded_irq() hard_irq: 빠른 ACK → thread_fn: I2C 읽기 input_report_*() + input_sync() Input Core → evdev → 유저 공간 장점 - 이벤트 즉시 처리 (저지연) - CPU 유휴 시 전력 소모 없음 - 이벤트 없으면 완전 수면 가능 - wakeup-source로 시스템 깨우기 가능 폴링 기반 (IRQ 미지원 시) 타이머 만료 (poll_interval) input_setup_polling() poll_fn 콜백 → 하드웨어 상태 읽기 input_report_*() + input_sync() Input Core → evdev → 유저 공간 특징 - IRQ 불필요 (단순 하드웨어 지원) - 지연 = poll_interval / 2 (평균) - sysfs에서 poll_interval 동적 조정 - 이벤트 없어도 CPU 주기적 사용

Device Tree 바인딩

커널은 gpio-keys, gpio-keys-polled, matrix-keypad 등 범용 Input 드라이버를 제공합니다. Device Tree만으로 입력 장치를 정의할 수 있어 별도 드라이버 작성이 불필요합니다.

/* === gpio-keys: GPIO 기반 버튼 (인터럽트 지원) === */
gpio-keys {
    compatible = "gpio-keys";

    power-button {
        label = "Power Button";
        gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_POWER>;        /* input-event-codes.h 참조 */
        wakeup-source;                    /* 이 버튼으로 시스템 깨우기 */
        debounce-interval = <20>;        /* ms 단위 디바운스 */
    };

    volume-up {
        label = "Volume Up";
        gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_VOLUMEUP>;
        autorepeat;                       /* 키 반복 활성화 */
    };

    lid-switch {
        label = "Lid Switch";
        gpios = <&gpio2 3 GPIO_ACTIVE_LOW>;
        linux,input-type = <EV_SW>;      /* 스위치 이벤트 */
        linux,code = <SW_LID>;
    };
};

/* === gpio-keys-polled: 인터럽트 없는 GPIO 폴링 === */
gpio-keys-polled {
    compatible = "gpio-keys-polled";
    poll-interval = <100>;              /* 100ms 간격 폴링 */

    button-0 {
        label = "Reset";
        gpios = <&gpio3 8 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_RESTART>;
    };
};

/* === matrix-keypad: 행/열 매트릭스 키패드 === */
matrix-keypad {
    compatible = "gpio-matrix-keypad";
    row-gpios = <&gpio1 0 0>, <&gpio1 1 0>, <&gpio1 2 0>;
    col-gpios = <&gpio1 3 0>, <&gpio1 4 0>, <&gpio1 5 0>;
    debounce-delay-ms = <5>;
    col-scan-delay-us = <2>;

    linux,keymap = <
        MATRIX_KEY(0, 0, KEY_1)
        MATRIX_KEY(0, 1, KEY_2)
        MATRIX_KEY(0, 2, KEY_3)
        MATRIX_KEY(1, 0, KEY_4)
        MATRIX_KEY(1, 1, KEY_5)
        MATRIX_KEY(1, 2, KEY_6)
        MATRIX_KEY(2, 0, KEY_7)
        MATRIX_KEY(2, 1, KEY_8)
        MATRIX_KEY(2, 2, KEY_9)
    >;
};

내장 Event Handler

핸들러디바이스 노드매칭 조건역할
evdev/dev/input/eventN모든 input_dev범용 이벤트 인터페이스 — libinput, X11, Wayland에서 사용. struct input_event 배열을 read()로 전달
kbd(내부)EV_KEY 디바이스콘솔(VT) 키보드 처리 — scancode→keycode→keysym 변환, 콘솔 스위칭(Alt+Fn), SysRq
mousedev/dev/input/mouseN, /dev/input/miceEV_REL 또는 EV_ABS + BTN레거시 PS/2 마우스 프로토콜 에뮬레이션 (ImPS/2). /dev/input/mice는 모든 마우스의 통합 노드
joydev/dev/input/jsNBTN_JOYSTICK/GAMEPAD 등레거시 조이스틱 API (현재는 evdev 사용 권장)
input-leds(내부)EV_LED 디바이스Input LED를 LED 클래스 디바이스로 연결 — LED 서브시스템 trigger 사용 가능
rfkill-input(내부)KEY_RFKILL 등무선 킬 스위치 이벤트 → rfkill 서브시스템 연동
Event Handler 내부 구조 비교 evdev (범용) /dev/input/eventN evdev_client (per fd) 원형 버퍼 (input_event[]) evdev_event() → 버퍼 저장 read() → struct input_event 특징 - 모든 디바이스에 매칭 - raw 이벤트 전달 - 다중 클라이언트 지원 - GRAB/REVOKE 지원 - SYN_DROPPED 알림 - 현재 표준 인터페이스 mousedev (레거시) /dev/input/mouseN, mice mousedev_client ImPS/2 패킷 생성기 EV_REL/EV_ABS → PS/2 변환 좌표 스케일링 (ABS→REL) read() → 3~4 byte 패킷 특징 - EV_REL/EV_ABS 디바이스 - PS/2 프로토콜 에뮬레이션 - /dev/input/mice: 통합 노드 - GPM, 레거시 앱 호환 - ABS→REL 좌표 변환 joydev (레거시) /dev/input/jsN joydev_client 축/버튼 상태 캐시 ABS → 축 정규화 (-32767~32767) KEY → 버튼 인덱스 변환 read() → struct js_event 특징 - BTN_JOYSTICK/GAMEPAD - 축 값 정규화 - 초기 상태 합성 이벤트 - SDL 1.x 호환 - evdev 사용 권장 (현재)

evdev 유저 공간 인터페이스

evdev는 가장 중요한 핸들러로, /dev/input/eventN 캐릭터 디바이스를 통해 모든 이벤트를 있는 그대로(raw) 유저 공간에 전달합니다.

/* === 유저 공간에서 evdev 이벤트 읽기 === */
#include <linux/input.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int fd = open("/dev/input/event0", O_RDONLY);
    struct input_event ev;
    char name[256];

    /* 디바이스 이름 조회 */
    ioctl(fd, EVIOCGNAME(sizeof(name)), name);
    printf("Device: %s\\n", name);

    /* 이벤트 루프 */
    while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
        if (ev.type == EV_KEY)
            printf("Key %d %s\\n", ev.code,
                   ev.value ? "pressed" : "released");
    }
    close(fd);
}
ioctl방향설명
EVIOCGNAMER디바이스 이름 문자열
EVIOCGIDRstruct input_id (bus, vendor, product, version)
EVIOCGPHYSR물리적 경로 문자열
EVIOCGUNIQR고유 식별자 문자열
EVIOCGPROPR디바이스 프로퍼티 비트마스크 (INPUT_PROP_*)
EVIOCGBIT(type, size)R지원하는 이벤트 유형/코드 비트마스크
EVIOCGABS(axis)Rstruct input_absinfo (min, max, fuzz, flat, res)
EVIOCSABS(axis)W절대축 파라미터 변경 (캘리브레이션)
EVIOCGKEYR현재 키 상태 비트마스크 (눌린 키 조회)
EVIOCGLEDR현재 LED 상태 비트마스크
EVIOCGSWR현재 스위치 상태 비트마스크
EVIOCGRABW디바이스 독점(grab) — 다른 프로세스(Process)/핸들러 차단
EVIOCREVOKEWfd의 접근 권한 철회 (logind 세션 전환 시)
EVIOCSFFWForce Feedback 효과 업로드
EVIOCRMFFWForce Feedback 효과 삭제
EVIOCGEFFECTSR동시 FF 효과 수
EVIOCGRAB — 디바이스 독점: ioctl(fd, EVIOCGRAB, 1)을 호출하면 해당 fd만 이벤트를 수신합니다. 다른 모든 evdev 클라이언트와 kbd/mousedev 핸들러가 이벤트를 받지 못합니다. 게임, 스크린 잠금(Lock), 키 매핑(Mapping) 도구에서 사용되며, EVIOCGRAB, 0으로 해제합니다.

Force Feedback (FF)

Force Feedback은 게임패드 진동, 스티어링 휠 저항, 햅틱 피드백 등을 지원합니다. 커널은 FF 효과를 디바이스에 업로드하고 재생/정지를 제어하는 프레임워크를 제공합니다.

/* === 커널 드라이버: FF 지원 등록 === */
#include <linux/input.h>

static int my_ff_upload(struct input_dev *dev,
                        struct ff_effect *effect,
                        struct ff_effect *old)
{
    /* 효과를 하드웨어에 프로그래밍 */
    write_ff_to_hw(effect);
    return 0;
}

static int my_ff_playback(struct input_dev *dev,
                          int effect_id, int value)
{
    /* value: 1=재생, 0=정지 */
    if (value)
        start_hw_effect(effect_id);
    else
        stop_hw_effect(effect_id);
    return 0;
}

static int setup_ff(struct input_dev *idev)
{
    input_set_capability(idev, EV_FF, FF_RUMBLE);
    input_set_capability(idev, EV_FF, FF_PERIODIC);
    input_set_capability(idev, EV_FF, FF_CONSTANT);

    /* 최대 동시 효과 수, 업로드/재생 콜백 */
    return input_ff_create(idev, 16);
    /* input_ff_create() 이후 콜백 설정 */
    idev->ff->upload = my_ff_upload;
    idev->ff->playback = my_ff_playback;
}

/* === 간편 rumble API (많은 게임패드에서 사용) ===
 * upload/playback 콜백 없이 드라이버가 직접 모터 제어 */
static int my_play_effect(struct input_dev *dev,
                          void *data,
                          struct ff_effect *effect)
{
    u16 strong = effect->u.rumble.strong_magnitude;
    u16 weak   = effect->u.rumble.weak_magnitude;

    /* 모터 강도 설정 */
    set_motor_speed(data, strong, weak);
    return 0;
}

/* input_ff_create_memless()로 간편 등록 */
input_ff_create_memless(idev, priv, my_play_effect);
Force Feedback (FF) 서브시스템 구조 User Space 게임 애플리케이션 EVIOCSFF (업로드) SDL2 Haptic API SDL_HapticEffect fftest (디버깅) EV_FF write() libinput rumble 햅틱 피드백 evdev (FF 인터페이스) EVIOCSFF → ff_effect 업로드 / EV_FF write → 재생/정지 Input FF Core (drivers/input/ff-core.c) ff_effect 관리 · 효과 ID 할당 · upload/playback/erase 콜백 디스패치 Full FF (하드웨어 지원) upload() → 효과를 HW에 프로그래밍 하드웨어 FF 엔진 스티어링 휠 저항, 진동 모터 제어 Memoryless FF (ff-memless.c) 커널이 효과 계산 → play_effect() 콜백 단순 진동 모터 strong_magnitude / weak_magnitude 직접 설정
FF 효과 유형상수설명사용 예
RumbleFF_RUMBLE좌/우 진동 모터 강도 제어게임패드 진동 (DualSense, Xbox)
PeriodicFF_PERIODIC사인/사각/톱니파 등 주기적 효과스티어링 휠 진동, 총기 반동
ConstantFF_CONSTANT일정한 힘 (방향 + 크기)스티어링 휠 저항
SpringFF_SPRING중심으로 되돌리는 스프링 효과스티어링 휠 센터링
DamperFF_DAMPER속도에 비례하는 저항스티어링 휠 감쇠
RampFF_RAMP시작→끝 강도가 변하는 효과가속/감속 시뮬레이션
InertiaFF_INERTIA가속도에 비례하는 저항무거운 물체 시뮬레이션
FrictionFF_FRICTION움직임에 저항하는 마찰지면 질감 시뮬레이션

Input 프로퍼티 (INPUT_PROP_*)

Input 프로퍼티는 디바이스의 물리적 특성을 유저 공간에 알려줍니다. input_set_capability()가 아닌 set_bit()으로 dev->propbit에 설정합니다.

프로퍼티설명사용처
INPUT_PROP_DIRECT화면에 직접 맵핑되는 입력 (터치스크린)좌표 변환 없이 화면 좌표로 사용
INPUT_PROP_POINTER간접 포인팅 (터치패드, 트랙볼)가속 커브 적용, 커서 제어
INPUT_PROP_SEMI_MT불완전한 멀티터치 (바운딩 박스만 제공)저가 터치패드
INPUT_PROP_TOPBUTTONPAD상단에 소프트 버튼 영역 (클릭패드)ThinkPad 등 트랙포인트 버튼
INPUT_PROP_BUTTONPAD패드 전체가 버튼 (클릭패드)MacBook, 최신 노트북 터치패드
INPUT_PROP_ACCELEROMETER가속도 센서 (포인팅 아님)화면 회전, 게임 기울기
/* 터치스크린: DIRECT 프로퍼티 설정 */
set_bit(INPUT_PROP_DIRECT, idev->propbit);

/* 터치패드: POINTER + BUTTONPAD */
set_bit(INPUT_PROP_POINTER, idev->propbit);
set_bit(INPUT_PROP_BUTTONPAD, idev->propbit);

키코드 매핑과 스캔코드

Input 서브시스템은 하드웨어 스캔코드(scancode)를 리눅스 키코드(keycode)로 변환하는 2단계 매핑을 지원합니다. 키코드 테이블은 런타임에 변경 가능하여 유저 공간에서 키 재매핑이 가능합니다.

/* 커널 드라이버에서 키코드 테이블 설정 */
static const unsigned short my_keymap[] = {
    [0x01] = KEY_ESC,
    [0x02] = KEY_1,
    [0x03] = KEY_2,
    /* ... */
};

idev->keycode     = my_keymap;
idev->keycodesize = sizeof(my_keymap[0]);
idev->keycodemax  = ARRAY_SIZE(my_keymap);

/* 선택: 커스텀 getkeycode/setkeycode 콜백
 * 기본 구현은 keycode[] 배열을 인덱스로 접근
 * 희소(sparse) 매핑이 필요하면 커스텀 콜백 구현 */
idev->getkeycode = my_getkeycode;
idev->setkeycode = my_setkeycode;
# 유저 공간에서 키 재매핑 (evdev ioctl)
# EVIOCSKEYCODE — scancode → keycode 변경

# udevadm hwdb 기반 자동 매핑 (권장)
# /etc/udev/hwdb.d/70-keyboard.hwdb:
evdev:input:b0003v046DpC52B*
 KEYBOARD_KEY_70039=esc        # CapsLock → Escape
 KEYBOARD_KEY_70029=capslock   # Escape → CapsLock

# hwdb 업데이트 적용
sudo systemd-hwdb update
sudo udevadm trigger

디버깅(Debugging) 도구

# === /proc/bus/input/devices — 등록된 모든 Input 디바이스 === 
cat /proc/bus/input/devices
# I: Bus=0011 Vendor=0001 Product=0001 Version=ab54
# N: Name="AT Translated Set 2 keyboard"
# P: Phys=isa0060/serio0/input0
# S: Sysfs=/devices/platform/i8042/serio0/input/input0
# U: Uniq=
# H: Handlers=sysrq kbd leds event0
# B: PROP=0
# B: EV=120013
# B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
# B: MSC=10
# B: LED=7

# === /proc/bus/input/handlers — 등록된 핸들러 목록 ===
cat /proc/bus/input/handlers
# N: Number=0 Name=rfkill
# N: Number=1 Name=kbd
# N: Number=2 Name=mousedev Minor=32
# N: Number=3 Name=evdev Minor=64
# N: Number=4 Name=joydev Minor=0

# === sysfs Input 디바이스 정보 ===
ls /sys/class/input/input0/
# capabilities/  device/  event0/  id/  inhibited  name  phys  properties  uevent  uniq

# 디바이스 활성/비활성 (v5.14+)
echo 1 > /sys/class/input/input0/inhibited   # 일시 비활성
echo 0 > /sys/class/input/input0/inhibited   # 재활성

# === evtest — 실시간 이벤트 모니터링 (가장 유용한 도구) ===
sudo evtest /dev/input/event0
# Event: time 1234567890.123456, type 1 (EV_KEY), code 30 (KEY_A), value 1
# Event: time 1234567890.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0
# Event: time 1234567890.234567, type 1 (EV_KEY), code 30 (KEY_A), value 0
# Event: time 1234567890.234567, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0

# === libinput debug-events — 고수준 이벤트 분석 ===
sudo libinput debug-events
# -event2   KEYBOARD_KEY     +3.24s  KEY_A (30) pressed
# -event2   KEYBOARD_KEY     +3.38s  KEY_A (30) released
# -event5   POINTER_MOTION   +5.12s  12.00/ -3.00

# === evemu — 이벤트 녹화/재생 (재현 테스트용) ===
sudo evemu-record /dev/input/event0 > recording.txt   # 녹화
sudo evemu-play /dev/input/event0 < recording.txt     # 재생

# === 커널 디버그: input 이벤트 추적 ===
echo 1 > /sys/module/evdev/parameters/debug            # evdev 디버그 (if available)

# ftrace로 Input 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/input/input_event/enable
cat /sys/kernel/debug/tracing/trace_pipe
# irq/18-i8042-18 input_event: dev=input0, type=1, code=30, value=1
Input 디바이스 시뮬레이션 (uinput): /dev/uinput을 통해 유저 공간에서 가상 입력 디바이스를 생성할 수 있습니다. 자동화 테스트, 원격 입력, 매크로(Macro) 도구에 활용됩니다.
/* === uinput: 유저 공간에서 가상 Input 디바이스 생성 === */
#include <linux/uinput.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int create_virtual_kbd(void)
{
    int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
    struct uinput_setup usetup;

    /* 이벤트 유형 활성화 */
    ioctl(fd, UI_SET_EVBIT, EV_KEY);
    ioctl(fd, UI_SET_KEYBIT, KEY_A);
    ioctl(fd, UI_SET_KEYBIT, KEY_B);

    /* 디바이스 정보 설정 */
    memset(&usetup, 0, sizeof(usetup));
    usetup.id.bustype = BUS_USB;
    usetup.id.vendor  = 0x1234;
    usetup.id.product = 0x5678;
    strcpy(usetup.name, "Virtual Keyboard");

    ioctl(fd, UI_DEV_SETUP, &usetup);
    ioctl(fd, UI_DEV_CREATE);

    /* 키 이벤트 주입 */
    struct input_event ev = { .type = EV_KEY, .code = KEY_A, .value = 1 };
    write(fd, &ev, sizeof(ev));
    ev.type = EV_SYN; ev.code = SYN_REPORT; ev.value = 0;
    write(fd, &ev, sizeof(ev));

    /* 해제 */
    ioctl(fd, UI_DEV_DESTROY);
    close(fd);
    return 0;
}

이벤트 흐름 요약

Hardware IRQ 발생 또는 폴링 Device Driver input_report_*() input_sync() Input Core input_handle_event() handler에 전파 evdev Handler evdev_event() client 버퍼에 저장 User Space read() / poll() /dev/input/eventN input_report_*() → input_handle_event() → handler->event() → evdev client buffer → read() fuzz 필터링은 Input Core에서, 타임스탬프는 input_handle_event()에서 부여
주요 소스 파일:
  • drivers/input/input.c — Input Core: 디바이스/핸들러 등록, 이벤트 라우팅
  • drivers/input/evdev.c — evdev 핸들러: /dev/input/eventN 캐릭터 디바이스
  • drivers/input/mousedev.c — 레거시 PS/2 마우스 에뮬레이션
  • drivers/input/joydev.c — 레거시 조이스틱 인터페이스
  • drivers/input/keyboard/ — 키보드 드라이버 (atkbd, gpio_keys, ...)
  • drivers/input/mouse/ — 마우스/터치패드 드라이버 (psmouse, elantech, ...)
  • drivers/input/touchscreen/ — 터치스크린 드라이버 (goodix, atmel_mxt, ...)
  • drivers/input/misc/ — 기타 (uinput, pwm-beeper, ...)
  • include/linux/input.h — 핵심 헤더 (struct input_dev, 이벤트 보고 API)
  • include/uapi/linux/input-event-codes.h — 모든 이벤트 유형/코드 상수 정의

HID 서브시스템 연동

현대 입력 장치 대부분은 HID(Human Interface Device) 프로토콜을 사용합니다. USB 키보드/마우스는 물론, Bluetooth 게임패드, I2C 터치패드까지 HID 레포트 디스크립터를 통해 자신의 기능을 기술합니다. HID 서브시스템(drivers/hid/)은 이 디스크립터를 파싱하여 Input 서브시스템에 자동으로 이벤트 디바이스를 생성합니다.

User Space libinput / evtest → /dev/input/eventN → /dev/hidrawN Input Subsystem (input_dev) HID Core (drivers/hid/hid-core.c) Report Descriptor 파싱 · Usage → Input 이벤트 변환 · hidraw 인터페이스 HID Drivers hid-generic hid-logitech hid-apple hid-playstation hid-nintendo hid-multitouch HID Transport usbhid i2c-hid hidp (BT) uhid (user) intel-ish-hid Hardware USB HID 장치 · I2C-HID 터치패드 · Bluetooth HID 게임패드 · ISH 센서 허브
HID Transport커널 모듈(Kernel Module)버스사용 장치
usbhiddrivers/hid/usbhid/USBUSB 키보드, 마우스, 게임패드
i2c-hiddrivers/hid/i2c-hid/I2C노트북 터치패드/터치스크린 (ACPI/OF)
hidpnet/bluetooth/hidp/BluetoothBT 키보드, DualShock, Joy-Con
uhiddrivers/hid/uhid.cUser space/dev/uhid로 가상 HID 디바이스 생성
intel-ish-hiddrivers/hid/intel-ish-hid/ISHIntel Sensor Hub (가속도/자이로)
amd-sfhdrivers/hid/amd-sfh-hid/SFHAMD Sensor Fusion Hub
/* HID 드라이버 구조 (장치별 커스터마이징) */
static struct hid_driver my_hid_driver = {
    .name          = "my-hid",
    .id_table      = my_hid_devices,

    /* Report Descriptor 수정 (quirk 적용) */
    .report_fixup  = my_report_fixup,

    /* 특정 이벤트 가공/변환 */
    .event         = my_event,

    /* raw 데이터 직접 처리 */
    .raw_event     = my_raw_event,

    /* Input 디바이스 설정 커스터마이징 */
    .input_configured = my_input_configured,
    .input_mapping    = my_input_mapping,

    .probe         = my_probe,
    .remove        = my_remove,
};
module_hid_driver(my_hid_driver);

/* HID Report Descriptor → Input 이벤트 매핑 과정:
 *
 * 1. HID Transport가 raw report descriptor 수신
 * 2. hid-core가 파싱 → hid_field, hid_usage 생성
 * 3. hid_usage.hid (HID Usage Page:Usage) → Linux 이벤트 변환:
 *    - Usage Page 0x01 (Generic Desktop):
 *      Usage 0x30 (X) → EV_ABS/ABS_X 또는 EV_REL/REL_X
 *    - Usage Page 0x07 (Keyboard):
 *      Usage 0x04 (a) → EV_KEY/KEY_A
 *    - Usage Page 0x09 (Button):
 *      Usage 0x01 → EV_KEY/BTN_LEFT
 *    - Usage Page 0x0D (Digitizer):
 *      Usage 0x30 (Tip Pressure) → EV_ABS/ABS_PRESSURE
 *
 * 4. hid-generic은 표준 매핑, hid-* 드라이버는 quirk 적용
 * 5. input_dev 등록 → evdev handler 연결
 */
# HID 디버깅
# Report Descriptor 덤프
cat /sys/kernel/debug/hid/0003:046D:C52B.0001/rdesc

# hidraw로 raw 데이터 접근
cat /dev/hidraw0 | xxd | head -20

# HID 이벤트 추적
echo 1 > /sys/kernel/debug/hid/0003:046D:C52B.0001/events

# udev로 HID 드라이버 바인딩 확인
udevadm info /dev/input/event5 | grep HID

PS/2와 serio 서브시스템

PS/2(Personal System/2)는 IBM이 1987년에 도입한 키보드/마우스 인터페이스입니다. 물리적으로는 거의 사라졌지만, 가상화(Virtualization) 환경(QEMU, VMware)과 BIOS 에뮬레이션에서 여전히 활발히 사용됩니다. 커널의 serio 서브시스템이 PS/2 물리 계층을, atkbd/psmouse가 프로토콜 계층을 담당합니다.

키보드 경로 Input Core → evdev + kbd handler atkbd (AT Keyboard) scancode → keycode 변환 serio (PS/2 포트 0) serio_interrupt() 콜백 i8042 컨트롤러 IRQ 1 (키보드) 마우스 경로 Input Core → evdev + mousedev psmouse (PS/2 Mouse) 프로토콜 감지 (ImPS/2, Synaptics...) serio (PS/2 포트 1, AUX) serio_interrupt() 콜백 i8042 컨트롤러 IRQ 12 (마우스) i8042 (Intel 8042 / KBC) I/O 0x60, 0x64 · ACPI 'PNP0303' / 'PNP0F13'
프로토콜드라이버특징
AT Set 2atkbd표준 AT 키보드 스캔코드 세트 2, 3바이트 확장키
PS/2 Mousepsmouse (bare)3바이트 패킷 (X, Y, 버튼)
ImPS/2psmouse4바이트 패킷 (IntelliMouse, 스크롤 추가)
Synapticspsmouse + synaptics터치패드 절대좌표, 멀티핑거, 클릭패드
ALPSpsmouse + alpsALPS 터치패드 (v3~v8 프로토콜)
Elantechpsmouse + elantechElantech 터치패드 (v1~v4)
TrackPointpsmouse + trackpointIBM/Lenovo 트랙포인트 (감도/속도 sysfs 조정)
/* serio 드라이버 등록 — PS/2 프로토콜 드라이버 */
static struct serio_driver my_serio_drv = {
    .driver = {
        .name = "my-serio-dev",
    },
    .description = "My PS/2 Device",
    .id_table    = my_serio_ids,

    /* PS/2 포트 바인딩 시 호출 */
    .connect     = my_connect,
    .disconnect  = my_disconnect,

    /* 바이트 수신 인터럽트 콜백
     * i8042 ISR → serio_interrupt() → 이 콜백 */
    .interrupt   = my_interrupt,
};

/* PS/2 바이트 수신 콜백 */
static irqreturn_t my_interrupt(struct serio *serio,
                                unsigned char data,
                                unsigned int flags)
{
    struct my_dev *dev = serio_get_drvdata(serio);

    /* 패킷 조립 (PS/2는 바이트 단위 인터럽트) */
    dev->packet[dev->idx++] = data;

    if (dev->idx >= PACKET_SIZE) {
        my_process_packet(dev);
        dev->idx = 0;
    }

    return IRQ_HANDLED;
}

/* PS/2 명령 전송 (포트에 바이트 쓰기) */
serio_write(serio, PS2_CMD_RESET);      /* 0xFF */
ps2_command(&dev->ps2dev, param, cmd);  /* 응답 대기 포함 */

Input Core 매칭 알고리즘

Input 디바이스가 등록되면 Input Core는 모든 등록된 핸들러와 매칭을 시도합니다. 반대로 핸들러가 등록되면 모든 기존 디바이스와 매칭합니다. 매칭 기준은 input_device_id 테이블의 플래그 비트마스크입니다.

input_attach_handler() 매칭 흐름 input_register_device(dev) 모든 handler의 id_table 순회 id->flags 비트 검사 불일치 다음 id_table 일치 handler->match()? 거부 매칭 실패 수락 handler->connect() → input_handle 생성 INPUT_DEVICE_ID_MATCH_BUS INPUT_DEVICE_ID_MATCH_VENDOR INPUT_DEVICE_ID_MATCH_EVBIT INPUT_DEVICE_ID_MATCH_KEYBIT ...
/* Input Core 매칭 로직 (drivers/input/input.c) */
static bool input_match_device(
    const struct input_device_id *id,
    const struct input_dev *dev)
{
    /* 1. bustype/vendor/product/version 매칭 (flags로 선택) */
    if (id->flags & INPUT_DEVICE_ID_MATCH_BUS)
        if (id->bustype != dev->id.bustype) return false;
    if (id->flags & INPUT_DEVICE_ID_MATCH_VENDOR)
        if (id->vendor != dev->id.vendor) return false;
    /* ... product, version 동일 패턴 ... */

    /* 2. 이벤트 능력(capability) 매칭
     * id_table에 설정된 비트가 dev에도 있어야 함
     * (dev는 추가 비트가 있어도 OK → 부분 집합 매칭) */
    if (!bitmap_subset(id->evbit, dev->evbit, EV_MAX)) return false;
    if (!bitmap_subset(id->keybit, dev->keybit, KEY_MAX)) return false;
    if (!bitmap_subset(id->relbit, dev->relbit, REL_MAX)) return false;
    if (!bitmap_subset(id->absbit, dev->absbit, ABS_MAX)) return false;
    /* ... mscbit, ledbit, sndbit, ffbit, swbit ... */

    return true;
}

/* evdev의 id_table — 모든 디바이스에 매칭 */
static const struct input_device_id evdev_ids[] = {
    { .driver_info = 1 },  /* flags=0 → 모든 조건 무시 → 전부 매칭 */
    { },
};

/* joydev의 id_table — 조이스틱/게임패드만 매칭 */
static const struct input_device_id joydev_ids[] = {
    {
        .flags = INPUT_DEVICE_ID_MATCH_EVBIT |
                 INPUT_DEVICE_ID_MATCH_ABSBIT,
        .evbit = { BIT_MASK(EV_ABS) },
        .absbit = { BIT_MASK(ABS_X) },
    },
    {
        .flags = INPUT_DEVICE_ID_MATCH_EVBIT |
                 INPUT_DEVICE_ID_MATCH_KEYBIT,
        .evbit = { BIT_MASK(EV_KEY) },
        .keybit = { [BIT_WORD(BTN_JOYSTICK)] = BIT_MASK(BTN_JOYSTICK) },
    },
    { },
};

이벤트 필터링 — Fuzz, SYN_DROPPED 복구

Input Core는 두 가지 중요한 필터링 메커니즘을 제공합니다: 절대축 fuzz 필터(노이즈 제거)와 SYN_DROPPED(이벤트 유실 알림). 이 두 메커니즘을 이해하지 못하면 "떨리는 커서"나 "비동기 상태" 문제를 진단할 수 없습니다.

/* === Fuzz 필터링 (drivers/input/input.c) ===
 *
 * input_abs_set_params()에서 설정한 fuzz 값으로
 * 연속 이벤트에서 노이즈를 제거합니다.
 *
 * 알고리즘 (Hysteresis 기반):
 * - new_value가 old_value ± fuzz 범위 안이면 → 이벤트 무시
 * - 범위 밖이면 → old_value 방향으로 fuzz만큼 보정한 값 보고
 *
 * 예: fuzz=4, old=100, new=103 → 무시 (|103-100|=3 < 4)
 *     fuzz=4, old=100, new=108 → 보고값 = 108-4 = 104
 */

static int input_defuzz_abs_event(int value, int old_val, int fuzz)
{
    if (fuzz) {
        if (value > old_val - fuzz && value < old_val + fuzz)
            return old_val;  /* fuzz 범위 내 → 무시 */

        if (value > old_val + fuzz)
            return value - fuzz;
        if (value < old_val - fuzz)
            return value + fuzz;
    }
    return value;
}

/* === SYN_DROPPED 처리 ===
 *
 * evdev client 버퍼(원형 큐)가 가득 찬 경우:
 * 1. 새 이벤트가 버려짐
 * 2. 다음 SYN_REPORT 시점에 SYN_DROPPED 이벤트 삽입
 * 3. 유저 공간이 SYN_DROPPED을 감지하면 상태 재동기화 필요
 *
 * 유저 공간 복구 절차:
 * 1. SYN_DROPPED 수신
 * 2. 다음 SYN_REPORT까지 모든 이벤트 버림
 * 3. EVIOCGKEY, EVIOCGABS 등으로 현재 상태 조회
 * 4. 내부 상태를 조회 결과로 갱신
 */
/* 유저 공간 SYN_DROPPED 복구 예제 */
struct input_event ev;
bool dropped = false;

while (read(fd, &ev, sizeof(ev)) > 0) {
    if (ev.type == EV_SYN && ev.code == SYN_DROPPED) {
        dropped = true;
        continue;
    }

    if (ev.type == EV_SYN && ev.code == SYN_REPORT) {
        if (dropped) {
            /* 상태 재동기화 */
            sync_device_state(fd);
            dropped = false;
            continue;
        }
        process_event_frame();
        continue;
    }

    if (!dropped)
        accumulate_event(&ev);
}

void sync_device_state(int fd)
{
    /* 키 상태 재조회 */
    unsigned char keys[KEY_CNT / 8 + 1];
    ioctl(fd, EVIOCGKEY(sizeof(keys)), keys);

    /* 절대축 상태 재조회 */
    struct input_absinfo absinfo;
    ioctl(fd, EVIOCGABS(ABS_X), &absinfo);
    /* 내부 상태 갱신 */
}

libinput과 사용자 공간 입력 스택

libinput은 Wayland 컴포지터와 X.Org에서 사용하는 입력 처리 라이브러리입니다. evdev의 raw 이벤트를 받아 포인터 가속, 제스처 인식, 탭-투-클릭, 스크롤 에뮬레이션 등 고수준 처리를 수행합니다.

Wayland 경로 GTK/Qt Application Wayland Compositor libinput 가속, 제스처, 탭, 스크롤 /dev/input/eventN (evdev) Input Core (커널) Hardware Driver X11 경로 X11 Application X Server (Xorg) xf86-input- libinput xf86-input- evdev (레거시) /dev/input/eventN (evdev) Input Core (커널) Hardware Driver
# libinput 디버깅 및 설정

# 디바이스 목록 및 기능 확인
sudo libinput list-devices

# 실시간 고수준 이벤트 모니터링
sudo libinput debug-events --verbose
# -event5  POINTER_MOTION      +0.456s  12.34/  -5.67 ( +2.00/ -1.00)
# -event5  POINTER_BUTTON      +1.234s  BTN_LEFT pressed
# -event8  GESTURE_SWIPE_BEGIN +2.000s  3 fingers

# 포인터 가속 프로파일 확인
sudo libinput debug-events --show-keycodes

# libinput quirks 확인 (장치별 보정 데이터)
sudo libinput quirks list /dev/input/event5
# AttrPressureRange=10:8
# ModelTabletModeNoSuspend=1

# xinput로 X11 Input 속성 조정
xinput list                                     # 디바이스 목록
xinput list-props "SynPS/2 Synaptics TouchPad"  # 속성 확인
xinput set-prop 12 "libinput Tapping Enabled" 1 # 탭-투-클릭 활성화
xinput set-prop 12 "libinput Natural Scrolling Enabled" 1

# Wayland에서 libinput 설정 (sway/GNOME)
# /etc/libinput/local-overrides.quirks
[Touchpad Override]
MatchName=*Touchpad*
AttrPressureRange=10:8
libinput 기능설명관련 evdev 이벤트
포인터 가속속도에 따라 비선형 가속 커브 적용EV_REL/REL_X,Y
탭-투-클릭터치패드 탭 → 마우스 클릭 변환EV_KEY/BTN_LEFT
두 손가락 스크롤2핑거 드래그 → 스크롤 이벤트EV_ABS/ABS_MT_*
3/4핑거 제스처스와이프, 핀치 인식멀티터치 슬롯
Palm detection손바닥 터치 무시터치 크기/압력 분석
Disable-while-typing키보드 입력 중 터치패드 비활성화키보드 + 터치패드 상관관계
Middle button emulation좌+우 동시 클릭 → 중간 버튼BTN_LEFT + BTN_RIGHT
Clickfinger1/2/3 손가락 클릭 → 좌/우/중간멀티터치 + BTN_TOOL_*

터치패드 드라이버 상세

노트북 터치패드는 PS/2(Synaptics, ALPS, Elantech), I2C-HID, USB-HID 세 가지 경로로 연결됩니다. 최신 노트북 대부분은 I2C-HID를 사용하지만, PS/2 터치패드 프로토콜은 역사적으로 매우 복잡하며 여전히 활발히 유지보수됩니다.

제조사PS/2 드라이버I2C-HID프로토콜 버전특징
Synapticspsmouse (synaptics.c)hid-rmiPS/2 v7+, RMI4가장 널리 사용, clickpad, SMBus 모드
ALPSpsmouse (alps.c)hid-alpsv3~v8, U1복잡한 바이트 패킷, trackstick 통합
Elantechpsmouse (elantech.c)hid-multitouchv1~v4세밀한 멀티터치, 합리적 가격
Cypresspsmouse (cypress_ps2.c)hid-multitouch-일부 Acer/HP 제품
Apple-applespi / hid-appleSPI/USBForce Touch, 고해상도 터치
Microsoft-hid-multitouchPTP (Precision)Windows Precision Touchpad 표준
/* I2C-HID 터치패드의 Report Descriptor가 파싱되면:
 *
 * hid-multitouch.c가 MT 컬렉션을 인식:
 * 1. Usage Page: Digitizer (0x0D)
 * 2. Usage: Touch Screen (0x04) 또는 Touch Pad (0x05)
 * 3. Contact Count Maximum → input_mt_init_slots()
 * 4. 각 Contact:
 *    - Tip Switch → BTN_TOUCH
 *    - Contact ID → ABS_MT_TRACKING_ID (슬롯 할당)
 *    - X/Y → ABS_MT_POSITION_X/Y
 *    - Width/Height → ABS_MT_TOUCH_MAJOR/MINOR
 *    - Pressure → ABS_MT_PRESSURE
 *
 * Windows Precision Touchpad (PTP)는 표준화된 HID descriptor를
 * 사용하므로 hid-multitouch.c 하나로 거의 모든 최신 터치패드 지원
 */

/* Synaptics PS/2 고급 기능 sysfs 제어 */
/* /sys/devices/.../serio1/input/input5/ */
# 터치패드 연결 경로 확인
udevadm info /dev/input/event5 | grep -E 'ID_INPUT|DEVPATH'
# ID_INPUT_TOUCHPAD=1
# DEVPATH=.../i2c-ELAN0001:00/.../input5

# Synaptics PS/2 정보
cat /sys/bus/serio/devices/serio1/firmware_id
cat /sys/bus/serio/devices/serio1/protocol

# 터치패드가 I2C-HID인지 PS/2인지 확인
sudo libinput list-devices | grep -A5 -i touchpad
# Kernel: /dev/input/event5
# Group:  8
# Seat:   seat0, default
# Capabilities: pointer gesture

Input 전원 관리(Power Management)

Input 서브시스템의 전원 관리는 open/close 콜백(Callback)wakeup-source 두 메커니즘을 중심으로 동작합니다. evdev 클라이언트가 디바이스 파일을 열면 dev->open()이 호출되고, 모든 클라이언트가 닫으면 dev->close()가 호출되어 하드웨어를 비활성화할 수 있습니다.

Input 디바이스 전원 상태 전환 IDLE (비활성) IRQ 비활성, 클럭 off 첫 evdev open() → dev->open() ACTIVE (활성) IRQ 활성, 이벤트 수집 중 마지막 close() → dev->close() IDLE (비활성) 자원 해제 INHIBITED sysfs inhibited=1 → 이벤트 차단 echo 1 > inhibited System Suspend input_dev_suspend() → 상태 저장 System Resume input_dev_resume() → 상태 복원, EV_KEY replay wakeup
/* open/close 콜백으로 전력 절약 */
static int my_input_open(struct input_dev *dev)
{
    struct my_data *data = input_get_drvdata(dev);

    /* 하드웨어 활성화 */
    pm_runtime_get_sync(&data->client->dev);
    enable_irq(data->irq);
    my_hw_enable(data);

    return 0;
}

static void my_input_close(struct input_dev *dev)
{
    struct my_data *data = input_get_drvdata(dev);

    /* 하드웨어 비활성화 */
    my_hw_disable(data);
    disable_irq(data->irq);
    pm_runtime_put(&data->client->dev);
}

/* wakeup-source 설정 — 전원 버튼 등 */
static int my_probe(...)
{
    /* Device Tree에서 wakeup-source 속성이 있으면 */
    if (device_property_read_bool(dev, "wakeup-source"))
        device_init_wakeup(dev, true);
}

/* suspend 시 wakeup IRQ 설정 */
static int my_suspend(struct device *dev)
{
    if (device_may_wakeup(dev))
        enable_irq_wake(data->irq);
    else
        my_hw_disable(data);  /* wakeup 불필요 시 완전 비활성화 */

    return 0;
}

이벤트 타임스탬프와 입력 지연

입력 이벤트의 타임스탬프는 input_handle_event()에서 ktime_get()으로 부여됩니다(CLOCK_MONOTONIC 기준). 이 시점은 하드웨어에서 이벤트가 실제 발생한 시점보다 항상 늦으므로, 입력 지연(input latency)의 구성 요소를 이해해야 합니다.

지연 구간원인전형적 값최적화 방법
하드웨어 스캔디바이스 스캔 주기 (폴링 레이트)1~8ms (125~1000Hz)고속 폴링 디바이스 사용
버스 전송USB 마이크로프레임, I2C 클럭1~8ms (USB), <1ms (I2C)USB 1000Hz 폴링, I2C 400kHz+
IRQ → ISR인터럽트 지연, ISR 실행1~50usthreaded IRQ 사용 시 증가
Input Core이벤트 라우팅, 타임스탬프 부여<10us-
evdev 버퍼원형 큐 저장, wake_up<10us-
스케줄링user space 프로세스 깨우기(Wakeup)1~100usRT 스케줄러(Scheduler), CPU affinity
libinput 처리가속, 제스처, 필터링50~500us-
컴포지터Wayland/X11 프레임 동기화0~16.7ms (vsync)VRR, 고주사율 디스플레이
GPU 렌더링프레임 렌더링 → 디스플레이 출력0~16.7ms-
# 입력 지연 측정

# evhz — USB 마우스 폴링 레이트 측정
sudo evhz /dev/input/event5
# Average: 1000.2 Hz (1.0ms)

# MSC_TIMESTAMP — 하드웨어 타임스탬프 (일부 디바이스)
sudo evtest /dev/input/event5
# Event: ... type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 12345

# evdev 타임스탬프 기반 지연 분석
sudo evtest /dev/input/event0 | awk '/^Event/{print $3}'

# USB 마우스 폴링 레이트 변경 (고속)
# /etc/modprobe.d/usbhid.conf
options usbhid mousepoll=1  # 1ms = 1000Hz (기본 8ms = 125Hz)

# ftrace로 IRQ-to-userspace 지연 측정
echo 1 > /sys/kernel/debug/tracing/events/input/input_event/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
cat /sys/kernel/debug/tracing/trace_pipe

게임 컨트롤러

최신 리눅스 커널은 주요 게임 컨트롤러를 모두 지원합니다. HID 드라이버가 컨트롤러의 고유 기능(모션 센서, 터치패드, LED, 오디오)까지 활용할 수 있도록 장치별 드라이버를 제공합니다.

컨트롤러연결커널 드라이버특수 기능
Xbox (USB)USBxpad진동(rumble), LED, 무선 어댑터
Xbox (BT)Bluetoothhid-microsoftBLE 연결, 진동
DualShock 4USB/BThid-playstation터치패드, 자이로, LED bar, 오디오
DualSenseUSB/BThid-playstation적응형 트리거, 햅틱, LED, 마이크
Joy-ConBluetoothhid-nintendoIMU, HD rumble, NFC, IR 카메라
Switch ProUSB/BThid-nintendoIMU, HD rumble, NFC
Steam ControllerUSB/무선hid-steam터치패드(2), 자이로, 햅틱
8BitDoUSB/BThid-generic모드별 다른 HID descriptor
Google StadiaUSBhid-google-stadia-ff진동
/* DualSense 컨트롤러 구조 (hid-playstation.c 참고)
 *
 * Input devices created:
 * 1. 게임패드: 버튼(16+), 아날로그 스틱(L3/R3), 트리거(L2/R2)
 * 2. 모션 센서: 가속도계 3축 + 자이로 3축 (별도 input_dev)
 * 3. 터치패드: 멀티터치 2점 (Protocol B, INPUT_PROP_POINTER)
 *
 * 출력 기능 (write report):
 * - LED 색상 (RGB)
 * - 적응형 트리거 효과 (저항, 진동, 다단계)
 * - 햅틱 피드백 (FF_RUMBLE)
 * - 플레이어 LED
 * - 마이크 뮤트 LED
 *
 * 전원 관리:
 * - Bluetooth: 유휴 시 저전력 모드
 * - USB: 충전 상태 보고 (power_supply 연동)
 */
# 게임 컨트롤러 디버깅
jstest /dev/input/js0           # 레거시 조이스틱 API 테스트
evtest /dev/input/event10       # evdev 이벤트 확인

# FF (진동) 테스트
fftest /dev/input/event10

# SDL2 게임패드 매핑 확인
SDL_GAMECONTROLLERCONFIG="..." sdl2-jstest

# 컨트롤러별 sysfs LED 제어 (DualSense)
echo 255 0 0 > /sys/class/leds/playstation::dualsense-1/color
echo 1 > /sys/class/leds/playstation::dualsense-1/brightness

Input LED 서브시스템 연동

input-leds 핸들러는 Input 디바이스의 LED(Caps Lock, Num Lock, Scroll Lock 등)를 LED 클래스 디바이스로 노출합니다. 이를 통해 LED 서브시스템의 trigger 메커니즘을 활용하여 LED 동작을 커스터마이징할 수 있습니다.

LED/사운드 피드백 경로 (출력 방향) User Space Caps Lock 키 입력 sysfs LED brightness 쓰기 console beep (BEL 문자) VT (kbd handler) VT console driver Input Core — input_inject_event() / input_event() EV_LED (LED 상태 변경) / EV_SND (비프 요청) → dev->event() 콜백 호출 dev->event(EV_LED) 드라이버가 HW LED 직접 제어 키보드 LED (Caps/Num/Scroll) input-leds handler Input LED → LED class device LED 서브시스템 /sys/class/leds/input0::capslock trigger: disk-activity, heartbeat 등 dev->event(EV_SND) 드라이버가 사운드 하드웨어 제어 사운드 출력 SND_BELL: 벨 소리 SND_TONE: 주파수 지정 톤 LED/SND는 유저→커널 방향 (출력 이벤트). 입력 이벤트(EV_KEY 등)와 반대 방향으로 흐름
# 키보드 LED 확인
ls /sys/class/leds/ | grep input
# input0::capslock
# input0::numlock
# input0::scrolllock

# LED 상태 확인/변경
cat /sys/class/leds/input0::capslock/brightness
echo 1 > /sys/class/leds/input0::capslock/brightness

# LED trigger로 활용 (디스크 활동 표시 등)
echo disk-activity > /sys/class/leds/input0::scrolllock/trigger
/* 커널 드라이버에서 LED 지원 */

/* 1. capability 선언 */
set_bit(EV_LED, idev->evbit);
set_bit(LED_CAPSL, idev->ledbit);
set_bit(LED_NUML, idev->ledbit);

/* 2. event 콜백 — 유저 공간에서 LED 제어 시 호출 */
static int my_event(struct input_dev *dev,
                    unsigned int type,
                    unsigned int code, int value)
{
    if (type != EV_LED)
        return -1;

    switch (code) {
    case LED_CAPSL:
        set_hw_led(LED_CAPSLOCK_BIT, value);
        break;
    case LED_NUML:
        set_hw_led(LED_NUMLOCK_BIT, value);
        break;
    }
    return 0;
}
idev->event = my_event;

Input 보안 — 세션 관리와 접근 제어(Access Control)

입력 장치에 대한 접근 제어는 시스템 보안의 핵심입니다. 키보드 입력을 스니핑하면 비밀번호를 탈취할 수 있고, 가상 입력을 주입하면 임의의 명령을 실행할 수 있습니다.

메커니즘설명사용처
EVIOCGRAB디바이스 독점 — 다른 클라이언트 차단게임, 스크린잠금, 키 매핑 도구
EVIOCREVOKEfd 접근 권한 철회 (재open 불가)systemd-logind 세션 전환
logind 세션활성 세션만 입력 장치 접근 허용멀티시트, VT 전환
/dev/input 퍼미션udev 규칙으로 그룹 input 설정일반 사용자 접근 제어
uinput 제한/dev/uinput 접근에 root 또는 특정 그룹 필요가상 입력 주입 방지
sysfs inhibited디바이스를 일시적으로 비활성화태블릿 모드, 덮개 닫힘
/* systemd-logind의 EVIOCREVOKE 사용 패턴:
 *
 * 1. 사용자 로그인 → logind가 세션에 evdev fd 할당
 * 2. VT 전환 (Ctrl+Alt+F2) → 이전 세션의 모든 evdev fd에 REVOKE
 *    ioctl(fd, EVIOCREVOKE, 0);
 * 3. revoke된 fd는 read() → -ENODEV
 * 4. 새 세션 활성화 → 새로운 fd 할당
 *
 * 이를 통해 비활성 세션이 키보드를 읽는 것을 방지
 */

/* EVIOCGRAB vs EVIOCREVOKE:
 * GRAB: fd 소유자가 독점. 해제(ungrab) 가능.
 *       다른 evdev 클라이언트만 차단 (kbd handler는 차단 안됨 주의)
 * REVOKE: 외부에서 fd 무효화. 복구 불가.
 *         logind 같은 세션 관리자가 사용
 */

흔한 실수와 트러블슈팅

실수 1: evbit 설정 없이 이벤트 보고

set_bit(EV_KEY, dev->evbit) 없이 input_report_key()를 호출하면 이벤트가 조용히 무시됩니다. input_set_capability()를 사용하면 evbit을 자동으로 설정하므로 이 실수를 방지할 수 있습니다.

실수 2: input_sync() 누락

input_sync()(SYN_REPORT)를 빠뜨리면 이벤트 패킷이 닫히지 않아 evdev 클라이언트에서 이벤트가 보이지 않습니다. 하나의 하드웨어 이벤트 처리 후 반드시 input_sync()를 호출하세요.

실수 3: input_mt_sync_frame() 누락 (Protocol B)

INPUT_MT_DROP_UNUSED 플래그와 함께 input_mt_init_slots()을 사용했다면, 매 프레임마다 input_mt_sync_frame()을 호출해야 합니다. 빠뜨리면 이전에 활성이던 슬롯이 해제되지 않아 "유령 터치"가 발생합니다.

실수 4: 등록 후 capability 변경

input_register_device() 이후에 set_bit()으로 새 capability를 추가하면 이미 연결된 핸들러가 새 이벤트를 인식하지 못합니다. 모든 capability는 등록 전에 설정하세요.

실수 5: IRQ 컨텍스트에서 I2C 읽기

I2C 터치스크린 드라이버에서 hard IRQ 핸들러 안에 i2c_transfer()를 넣으면 sleep 불가 컨텍스트에서 sleep 발생 → BUG. devm_request_threaded_irq()로 threaded IRQ를 사용하세요.

devm_request_threaded_irq(&client->dev, client->irq,
    NULL, my_ts_irq,  /* hard_irq=NULL, thread_fn만 사용 */
    IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
    "my-touch", data);
실수 6: 절대축 resolution 미설정

터치패드에서 input_abs_set_res()를 설정하지 않으면 libinput이 물리적 크기를 계산할 수 없어 포인터 가속이 부정확해집니다. 터치스크린은 INPUT_PROP_DIRECT이므로 영향이 적지만, 터치패드(INPUT_PROP_POINTER)에서는 필수입니다.

실전 실습

실습 환경: 물리 장치가 없어도 uinput으로 가상 입력 장치를 만들거나, QEMU에서 PS/2 키보드를 사용할 수 있습니다.

Lab 1: evtest로 이벤트 분석

# 1. 디바이스 목록 확인
cat /proc/bus/input/devices

# 2. 키보드 이벤트 모니터링
sudo evtest /dev/input/event0
# 키를 누르고 떼면서 EV_KEY, SYN_REPORT 관찰
# value: 1=press, 0=release, 2=repeat

# 3. 마우스 이동 관찰 (EV_REL)
sudo evtest /dev/input/event3
# REL_X, REL_Y 값 변화 관찰

# 4. capability 비트 해석
# B: EV=120013 → 비트 0(SYN)+1(KEY)+4(MSC)+17(LED)+20(REP)
python3 -c "print(bin(0x120013))"
# 0b100100000000000010011

Lab 2: uinput으로 가상 마우스 만들기

#include <linux/uinput.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
    struct uinput_setup setup;

    /* 이벤트 유형 설정 */
    ioctl(fd, UI_SET_EVBIT, EV_REL);
    ioctl(fd, UI_SET_RELBIT, REL_X);
    ioctl(fd, UI_SET_RELBIT, REL_Y);
    ioctl(fd, UI_SET_EVBIT, EV_KEY);
    ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);
    ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT);

    memset(&setup, 0, sizeof(setup));
    setup.id.bustype = BUS_USB;
    strcpy(setup.name, "Virtual Mouse");
    ioctl(fd, UI_DEV_SETUP, &setup);
    ioctl(fd, UI_DEV_CREATE);

    sleep(1);  /* udev 처리 대기 */

    /* 마우스를 원 그리며 이동 */
    for (int i = 0; i < 360; i++) {
        struct input_event ev[3];
        memset(ev, 0, sizeof(ev));

        ev[0].type = EV_REL; ev[0].code = REL_X;
        ev[0].value = (int)(5 * cos(i * M_PI / 180));
        ev[1].type = EV_REL; ev[1].code = REL_Y;
        ev[1].value = (int)(5 * sin(i * M_PI / 180));
        ev[2].type = EV_SYN; ev[2].code = SYN_REPORT;

        write(fd, ev, sizeof(ev));
        usleep(10000);  /* 10ms 간격 */
    }

    ioctl(fd, UI_DEV_DESTROY);
    close(fd);
    return 0;
}

Lab 3: evemu로 이벤트 녹화/재생

# 이벤트 녹화 (키보드 타이핑 10초)
sudo timeout 10 evemu-record /dev/input/event0 > /tmp/typing.evemu

# 녹화 내용 확인
head -30 /tmp/typing.evemu
# N: AT Translated Set 2 keyboard
# I: 0011 0001 0001 ab54
# P: 00 00 00 00 00 00 00 00 ...
# B: 00 ...
# E: 0.000001 0001 0030 1    ← EV_KEY KEY_A press
# E: 0.000001 0000 0000 0    ← SYN_REPORT

# 재생 (가상 디바이스 생성 → 이벤트 주입)
sudo evemu-play /dev/input/event0 < /tmp/typing.evemu
# 녹화된 타이핑이 재현됨

Lab 4: libinput quirks 디버깅

# 터치패드 문제 진단

# 1. libinput 디버그 이벤트
sudo libinput debug-events --verbose --device /dev/input/event5
# 제스처 인식, palm detection, 압력 임계값 관찰

# 2. quirks 확인
sudo libinput quirks list /dev/input/event5

# 3. 커스텀 quirk 추가 (/etc/libinput/local-overrides.quirks)
cat > /etc/libinput/local-overrides.quirks << 'EOF'
[My Touchpad Fix]
MatchName=*ELAN*Touchpad*
AttrPressureRange=10:8
AttrPalmPressureThreshold=200
EOF

# 4. udev 규칙으로 Input 속성 확인
udevadm info --export-db | grep -B5 'ID_INPUT_TOUCHPAD=1'

Lab 5: GPIO 키 디버깅 (임베디드)

# Device Tree 적용 확인
cat /proc/device-tree/gpio-keys/power-button/linux,code
# 116 (KEY_POWER)

# GPIO 상태 확인
cat /sys/kernel/debug/gpio | grep -A2 'gpio-keys'

# 이벤트 발생 확인
sudo evtest /dev/input/event1
# 버튼 누르면: EV_KEY KEY_POWER value 1

# wakeup source 상태 확인
cat /sys/devices/.../gpio-keys/power/wakeup
# enabled
cat /sys/devices/.../gpio-keys/power/wakeup_count
# 3 (3회 wakeup 발생)

Input 디바이스 자동 감지와 udev 규칙

Input 디바이스가 등록되면 커널이 uevent를 생성하고, systemd-udevd가 이를 받아 /dev/input/ 아래 디바이스 노드를 생성합니다. udev 규칙으로 퍼미션, 심볼릭 링크, 환경 변수를 설정하여 입력 장치 관리를 자동화할 수 있습니다.

Input 디바이스 등록 → udev → 유저 공간 경로 Device Driver input_register_device() → uevent 생성 Input Core handler 매칭 evdev 연결 systemd-udevd 규칙 매칭 디바이스 노드 생성 /dev/input/ eventN, mouseN by-id/, by-path/ udev 규칙 처리 (/lib/udev/rules.d/60-input-id.rules) 입력 유형 식별 ID_INPUT=1 ID_INPUT_KEYBOARD=1 ID_INPUT_TOUCHPAD=1 퍼미션 설정 GROUP="input" MODE="0660" TAG+="uaccess" (logind) 심볼릭 링크 생성 by-id/usb-Vendor-kbd-event by-path/pci-...-event-kbd 안정적 디바이스 경로 제공 TAG+="seat" → systemd-logind → 세션별 입력 장치 할당 (EVIOCREVOKE 기반 접근 제어) libinput quirks: /usr/share/libinput/*.quirks → 장치별 보정 데이터 자동 적용
# === udev 규칙 예제 ===

# 특정 USB 키보드에 커스텀 규칙 적용
# /etc/udev/rules.d/90-my-keyboard.rules
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="0295", \
  ENV{ID_INPUT_KEYBOARD}="1", SYMLINK+="input/my-keyboard", MODE="0660", GROUP="input"

# 바코드 스캐너를 키보드로 인식하지 않도록 설정
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", \
  ENV{ID_INPUT_KEYBOARD}="", ENV{LIBINPUT_IGNORE_DEVICE}="1"

# udev 디버깅
udevadm info /dev/input/event5                  # 디바이스 속성 확인
udevadm info --attribute-walk /dev/input/event5  # 부모 디바이스 속성 탐색
udevadm monitor --property --subsystem-match=input  # 실시간 uevent 모니터링
udevadm test /sys/class/input/event5             # 규칙 시뮬레이션

# by-id 링크로 안정적 디바이스 참조
ls -la /dev/input/by-id/
# usb-Logitech_USB_Receiver-event-kbd -> ../event3
# usb-Logitech_USB_Receiver-if01-event-mouse -> ../event5

# by-path 링크 (물리적 위치 기반)
ls -la /dev/input/by-path/
# pci-0000:00:14.0-usb-0:2:1.0-event-kbd -> ../event3

이벤트 코얼레싱과 성능 최적화

고속 입력 장치(1000Hz 마우스, 고해상도 터치스크린)는 초당 수천 개의 이벤트를 생성합니다. 이벤트 처리 지연이나 과부하를 방지하려면 적절한 최적화 전략이 필요합니다.

Input 이벤트 성능 최적화 지점 이벤트 흐름 방향 하드웨어 수준 - USB 폴링 1000Hz (mousepoll=1) - I2C 400~1000kHz - SPI 고속 전송 - 하드웨어 FIFO (멀티터치 일괄 전송) 드라이버 수준 - threaded IRQ (I2C sleep 허용) - DMA 일괄 읽기 - 폴링 주기 최적화 - fuzz 값 적절 설정 (노이즈 이벤트 제거) Input Core 수준 - fuzz 필터링 (중복 이벤트 제거) - 이벤트 변환 최소화 - handler 매칭 캐시 - SYN_DROPPED 알림 (버퍼 오버플로 대응) 유저 공간 수준 - epoll() 이벤트 루프 - 일괄 read() (여러 이벤트 한번에) - SYN_DROPPED 복구 - CPU affinity 설정 - RT 스케줄러 (게임) evdev 클라이언트 버퍼 구조와 코얼레싱 원형 버퍼 (evdev_client) 기본 크기: 64개 이벤트 (커널 빌드 설정) 일괄 read() 권장 struct input_event buf[64]; read(fd, buf, sizeof(buf)); evdev 버퍼 오버플로 시: 이전 이벤트 덮어쓰기 → SYN_DROPPED 삽입 → 유저 공간 재동기화 필요 EVIOCGBIT(EV_SYN) 체크 후 EVIOCGABS/EVIOCGKEY로 상태 재구성 팁: CLOCK_MONOTONIC 기반 타임스탬프로 이벤트 순서 검증 가능
최적화 항목설정 방법효과주의사항
USB 폴링 레이트options usbhid mousepoll=1125Hz→1000Hz (1ms 폴링)CPU 사용량 약간 증가
fuzz 조정input_abs_set_params() fuzz 값노이즈 이벤트 제거너무 크면 반응성 저하
폴링 주기sysfs poll_interval지연 vs 전력 트레이드오프최소값 = 장치 응답 시간
evdev 버퍼커널 빌드 설정버퍼 오버플로(Buffer Overflow) 방지메모리 사용량 증가
CPU affinitytaskset / irqbalanceIRQ-유저 공간 같은 CPUL1 캐시(Cache) 히트율 개선
스케줄러SCHED_FIFO / chrt입력 처리 우선순위(Priority) 보장다른 태스크(Task)에 영향

게임패드/조이스틱 드라이버 구현 패턴

커스텀 게임 컨트롤러 드라이버를 작성할 때 따라야 하는 핵심 패턴입니다. USB HID 표준을 따르지 않는 독자적 프로토콜을 사용하는 컨트롤러(아케이드 스틱, 커스텀 레이싱 휠 등)에 해당합니다.

/* === 커스텀 게임패드 드라이버 템플릿 ===
 * USB HID 비표준 프로토콜을 사용하는 컨트롤러 */

#include <linux/module.h>
#include <linux/usb.h>
#include <linux/input.h>

struct my_gamepad {
    struct input_dev *input;
    struct usb_device *usbdev;
    struct urb *irq_in;
    unsigned char *data;
    dma_addr_t data_dma;
};

/* 인터럽트 URB 완료 콜백 — 이벤트 보고 */
static void my_gamepad_irq(struct urb *urb)
{
    struct my_gamepad *gp = urb->context;
    struct input_dev *dev = gp->input;
    unsigned char *data = gp->data;

    if (urb->status) goto resubmit;

    /* 아날로그 스틱 (중심 보정 포함) */
    input_report_abs(dev, ABS_X,  data[1] - 128);
    input_report_abs(dev, ABS_Y,  data[2] - 128);
    input_report_abs(dev, ABS_RX, data[3] - 128);
    input_report_abs(dev, ABS_RY, data[4] - 128);

    /* 트리거 (0~255) */
    input_report_abs(dev, ABS_Z,  data[5]);  /* L2 */
    input_report_abs(dev, ABS_RZ, data[6]);  /* R2 */

    /* D-Pad (HAT 스위치) — 비트마스크 → 축 변환 */
    input_report_abs(dev, ABS_HAT0X,
        !!(data[7] & 0x02) - !!(data[7] & 0x01));
    input_report_abs(dev, ABS_HAT0Y,
        !!(data[7] & 0x08) - !!(data[7] & 0x04));

    /* 버튼 */
    input_report_key(dev, BTN_A,      data[8] & 0x01);
    input_report_key(dev, BTN_B,      data[8] & 0x02);
    input_report_key(dev, BTN_X,      data[8] & 0x04);
    input_report_key(dev, BTN_Y,      data[8] & 0x08);
    input_report_key(dev, BTN_TL,     data[8] & 0x10);  /* L1 */
    input_report_key(dev, BTN_TR,     data[8] & 0x20);  /* R1 */
    input_report_key(dev, BTN_START,  data[8] & 0x40);
    input_report_key(dev, BTN_SELECT, data[8] & 0x80);
    input_report_key(dev, BTN_THUMBL, data[9] & 0x01);  /* L3 */
    input_report_key(dev, BTN_THUMBR, data[9] & 0x02);  /* R3 */

    input_sync(dev);

resubmit:
    usb_submit_urb(urb, GFP_ATOMIC);
}

/* open/close 콜백 — 전력 관리 */
static int my_gamepad_open(struct input_dev *dev)
{
    struct my_gamepad *gp = input_get_drvdata(dev);
    return usb_submit_urb(gp->irq_in, GFP_KERNEL);
}

static void my_gamepad_close(struct input_dev *dev)
{
    struct my_gamepad *gp = input_get_drvdata(dev);
    usb_kill_urb(gp->irq_in);
}

static int my_gamepad_probe(struct usb_interface *intf,
                            const struct usb_device_id *id)
{
    struct my_gamepad *gp;
    struct input_dev *input;

    gp = devm_kzalloc(&intf->dev, sizeof(*gp), GFP_KERNEL);
    input = devm_input_allocate_device(&intf->dev);

    input->name = "My Custom Gamepad";
    input->id.bustype = BUS_USB;
    input->open = my_gamepad_open;
    input->close = my_gamepad_close;

    /* 아날로그 축 설정 — fuzz=16으로 데드존 노이즈 제거 */
    input_set_abs_params(input, ABS_X,  -128, 127, 16, 16);
    input_set_abs_params(input, ABS_Y,  -128, 127, 16, 16);
    input_set_abs_params(input, ABS_RX, -128, 127, 16, 16);
    input_set_abs_params(input, ABS_RY, -128, 127, 16, 16);
    input_set_abs_params(input, ABS_Z,  0, 255, 0, 0);
    input_set_abs_params(input, ABS_RZ, 0, 255, 0, 0);
    input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
    input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);

    /* 버튼 capability */
    input_set_capability(input, EV_KEY, BTN_A);
    input_set_capability(input, EV_KEY, BTN_B);
    input_set_capability(input, EV_KEY, BTN_X);
    input_set_capability(input, EV_KEY, BTN_Y);
    input_set_capability(input, EV_KEY, BTN_TL);
    input_set_capability(input, EV_KEY, BTN_TR);
    input_set_capability(input, EV_KEY, BTN_START);
    input_set_capability(input, EV_KEY, BTN_SELECT);
    input_set_capability(input, EV_KEY, BTN_THUMBL);
    input_set_capability(input, EV_KEY, BTN_THUMBR);

    /* 게임패드 프로퍼티 설정 — SDL/Steam이 인식 */
    set_bit(INPUT_PROP_BUTTONPAD, input->propbit);

    /* Force Feedback (rumble) */
    input_set_capability(input, EV_FF, FF_RUMBLE);
    input_ff_create_memless(input, gp, my_play_effect);

    input_set_drvdata(input, gp);
    gp->input = input;

    /* URB 설정 (인터럽트 IN 엔드포인트) ... */

    return input_register_device(input);
}
SDL2 게임패드 매핑: SDL2는 /dev/input/eventN의 capability를 분석하여 게임패드 버튼 매핑을 자동 생성합니다. 표준 BTN_A/B/X/Y, ABS_X/Y/RX/RY, ABS_HAT0X/Y를 사용하면 대부분의 게임에서 즉시 인식됩니다. 비표준 매핑은 SDL_GAMECONTROLLERCONFIG 환경 변수로 오버라이드할 수 있습니다.

커널 6.x Input 서브시스템 변경사항

커널 6.x 시리즈에서 Input 서브시스템은 지속적으로 개선되고 있습니다. 주요 변경사항을 버전별로 정리합니다.

커널 버전변경사항영향
6.1hid-playstation: DualSense Edge 지원, 적응형 트리거 프로파일PS5 컨트롤러 완전 지원
6.2input_mt_drop_unused()input_mt_sync_frame() 통합 강화멀티터치 프레임 동기화 간소화
6.3hid-nintendo: Joy-Con 페어링 개선, IMU 캘리브레이션Switch 컨트롤러 안정성 향상
6.4i2c-hid: 절전 모드(Suspend) 개선, ACPI 바인딩 확장노트북 터치패드 wakeup 안정성
6.5input: INPUT_PROP_ACCELEROMETER 속성 활용 확대모션 센서 디바이스 식별 강화
6.6 (LTS)hid-multitouch: Win8+ 터치패드 PTP(Precision Touchpad) 개선Windows 표준 터치패드 호환성
6.7evdev: 타임스탬프 정밀도 향상 (CLOCK_MONOTONIC 정합성)입력 지연 측정 정확도 개선
6.8hid-steam: Steam Deck 컨트롤러 햅틱/자이로 개선Steam Deck 네이티브 지원 강화
6.9input: inhibited 상태에서 wakeup 이벤트 처리 개선태블릿 모드 전환 안정성
6.10hid-google-hammer: Chromebook 내장 키보드/터치패드 개선Chrome OS 기기 호환성
6.11input: input_dev_poller sysfs 인터페이스 개선폴링 디바이스 런타임 조정 용이
6.12hid-playstation: DualSense USB/BT 전환 시 상태 유지연결 방식 변경 시 끊김 방지
# 커널 버전별 Input 관련 변경 확인
git log --oneline drivers/input/ v6.6..v6.12 | head -50

# HID 드라이버 변경 확인
git log --oneline drivers/hid/ v6.6..v6.12 | head -50

# 특정 드라이버의 최신 변경
git log --oneline drivers/hid/hid-playstation.c

# Kconfig 옵션 확인
grep -r 'INPUT' /boot/config-$(uname -r) | grep '=y\|=m' | head -30
# CONFIG_INPUT=y
# CONFIG_INPUT_EVDEV=y (필수!)
# CONFIG_INPUT_POLLDEV=m
# CONFIG_HID=y
# CONFIG_USB_HID=y
# CONFIG_I2C_HID_ACPI=m
필수 Kconfig 옵션:
  • CONFIG_INPUT — Input 서브시스템 코어 (거의 항상 =y)
  • CONFIG_INPUT_EVDEV — evdev 핸들러 (유저 공간 접근에 필수)
  • CONFIG_INPUT_POLLDEV — 폴링 디바이스 지원 (v5.12+ 통합)
  • CONFIG_HID — HID 코어 (USB/BT/I2C 입력 장치에 필수)
  • CONFIG_USB_HID — USB HID 트랜스포트
  • CONFIG_I2C_HID_ACPI / I2C_HID_OF — I2C-HID 터치패드
  • CONFIG_INPUT_FF_MEMLESS — memoryless FF 지원 (게임패드 진동)
  • CONFIG_INPUT_UINPUT — uinput 가상 디바이스

참고자료

커널 공식 문서

커널 소스 코드

규격 문서

외부 자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.