Input 서브시스템
Linux Input 서브시스템을 이벤트 라우팅과 사용자 경험 품질 관점에서 심층 분석합니다. input_dev/input_handler/evdev 계층의 역할, 키보드·마우스·터치·센서 이벤트 타입 해석, 멀티터치 슬롯 프로토콜, debounce·repeat·calibration 처리, IRQ 기반 이벤트 수집과 지연 처리 분리, power key/wakeup 같은 전원 연계 동작, udev/libinput으로 전달되는 사용자 공간 경로, evtest/libinput-debug-events를 활용한 문제 분석까지 입력 장치 드라이버 실전에 필요한 핵심을 다룹니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.
단계별 이해
- 장치 수명주기 확인
probe부터 remove까지 흐름을 점검합니다. - 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리합니다. - 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인합니다. - 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현합니다.
Input 서브시스템 (키보드, 마우스, 터치)
리눅스 Input 서브시스템(drivers/input/)은 키보드, 마우스, 터치스크린, 조이스틱, 리모컨 등 모든 종류의 입력 장치를 통합 관리하는 프레임워크입니다. 하드웨어별 드라이버가 이벤트를 생성하면, Input Core가 이를 적절한 핸들러(evdev, kbd, mousedev 등)로 라우팅하여 유저 공간에 전달합니다.
Input 서브시스템은 3계층으로 구성됩니다:
- Device Drivers: 하드웨어와 직접 통신하여
input_event를 생성 (input_report_*API) - Input Core (
drivers/input/input.c): 디바이스와 핸들러 사이의 매칭·라우팅 —input_register_device()/input_register_handler() - Event Handlers: 이벤트를 유저 공간에 전달 —
evdev(범용),kbd(콘솔 키보드),mousedev(/dev/input/mice),joydev(조이스틱)
핵심 데이터 구조
/* === 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_SYN | 0x00 | 이벤트 그룹 경계 표시 | SYN_REPORT, SYN_MT_REPORT, SYN_DROPPED |
| 키/버튼 | EV_KEY | 0x01 | 키보드, 마우스 버튼, 게임패드 | KEY_A, BTN_LEFT, BTN_TOUCH |
| 상대 이동 | EV_REL | 0x02 | 마우스 이동, 스크롤 휠 | REL_X, REL_Y, REL_WHEEL |
| 절대 위치 | EV_ABS | 0x03 | 터치스크린, 태블릿, 조이스틱 | ABS_X, ABS_MT_POSITION_X, ABS_MT_SLOT |
| 기타 | EV_MSC | 0x04 | 스캔코드 등 잡다한 이벤트 | MSC_SCAN, MSC_TIMESTAMP |
| 스위치 | EV_SW | 0x05 | 덮개(lid), 헤드폰 잭, 태블릿 모드 | SW_LID, SW_HEADPHONE_INSERT |
| LED | EV_LED | 0x11 | 키보드 LED 제어 | LED_CAPSL, LED_NUML, LED_SCROLLL |
| 사운드 | EV_SND | 0x12 | 비프, 클릭 사운드 | SND_BELL, SND_TONE |
| 반복 | EV_REP | 0x14 | 키 반복 파라미터 | REP_DELAY, REP_PERIOD |
| Force Feedback | EV_FF | 0x15 | 진동/햅틱/포스 피드백 | FF_RUMBLE, FF_CONSTANT, FF_PERIODIC |
| 전원 | EV_PWR | 0x16 | 전원 버튼 이벤트 | (예약됨) |
| FF 상태 | EV_FF_STATUS | 0x17 | FF 효과 재생 상태 | FF_STATUS_STOPPED, FF_STATUS_PLAYING |
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(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_SYN | SYN_REPORT — 이벤트 패킷 완료 표시 |
input_mt_sync(dev) | EV_SYN | SYN_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 |
| 대역폭 | 모든 터치를 매번 전송 | 변경된 슬롯만 전송 (효율적) |
| 유저 공간 | 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_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_TYPE | MT_TOOL_FINGER / MT_TOOL_PEN | 도구 유형 |
ABS_MT_DISTANCE | 표면과의 거리 (호버링) | 0 = 접촉 |
폴링 Input 디바이스
인터럽트를 지원하지 않는 하드웨어의 경우, input_setup_polling()을 사용하여 커널이 주기적으로 하드웨어를 폴링합니다. 이전의 input_polled_dev 구조체는 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);
}
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/mice | EV_REL 또는 EV_ABS + BTN | 레거시 PS/2 마우스 프로토콜 에뮬레이션 (ImPS/2). /dev/input/mice는 모든 마우스의 통합 노드 |
| joydev | /dev/input/jsN | BTN_JOYSTICK/GAMEPAD 등 | 레거시 조이스틱 API (현재는 evdev 사용 권장) |
| input-leds | (내부) | EV_LED 디바이스 | Input LED를 LED 클래스 디바이스로 연결 — LED 서브시스템 trigger 사용 가능 |
| rfkill-input | (내부) | KEY_RFKILL 등 | 무선 킬 스위치 이벤트 → rfkill 서브시스템 연동 |
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 | 방향 | 설명 |
|---|---|---|
EVIOCGNAME | R | 디바이스 이름 문자열 |
EVIOCGID | R | struct input_id (bus, vendor, product, version) |
EVIOCGPHYS | R | 물리적 경로 문자열 |
EVIOCGUNIQ | R | 고유 식별자 문자열 |
EVIOCGPROP | R | 디바이스 프로퍼티 비트마스크 (INPUT_PROP_*) |
EVIOCGBIT(type, size) | R | 지원하는 이벤트 유형/코드 비트마스크 |
EVIOCGABS(axis) | R | struct input_absinfo (min, max, fuzz, flat, res) |
EVIOCSABS(axis) | W | 절대축 파라미터 변경 (캘리브레이션) |
EVIOCGKEY | R | 현재 키 상태 비트마스크 (눌린 키 조회) |
EVIOCGLED | R | 현재 LED 상태 비트마스크 |
EVIOCGSW | R | 현재 스위치 상태 비트마스크 |
EVIOCGRAB | W | 디바이스 독점(grab) — 다른 프로세스/핸들러 차단 |
EVIOCREVOKE | W | fd의 접근 권한 철회 (logind 세션 전환 시) |
EVIOCSFF | W | Force Feedback 효과 업로드 |
EVIOCRMFF | W | Force Feedback 효과 삭제 |
EVIOCGEFFECTS | R | 동시 FF 효과 수 |
ioctl(fd, EVIOCGRAB, 1)을 호출하면 해당 fd만 이벤트를 수신합니다. 다른 모든 evdev 클라이언트와 kbd/mousedev 핸들러가 이벤트를 받지 못합니다. 게임, 스크린 잠금, 키 매핑 도구에서 사용되며, 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);
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
디버깅 도구
# === /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
/dev/uinput을 통해 유저 공간에서 가상 입력 디바이스를 생성할 수 있습니다. 자동화 테스트, 원격 입력, 매크로 도구에 활용됩니다.
/* === 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;
}
이벤트 흐름 요약
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 서브시스템에 자동으로 이벤트 디바이스를 생성합니다.
| HID Transport | 커널 모듈 | 버스 | 사용 장치 |
|---|---|---|---|
| usbhid | drivers/hid/usbhid/ | USB | USB 키보드, 마우스, 게임패드 |
| i2c-hid | drivers/hid/i2c-hid/ | I2C | 노트북 터치패드/터치스크린 (ACPI/OF) |
| hidp | net/bluetooth/hidp/ | Bluetooth | BT 키보드, DualShock, Joy-Con |
| uhid | drivers/hid/uhid.c | User space | /dev/uhid로 가상 HID 디바이스 생성 |
| intel-ish-hid | drivers/hid/intel-ish-hid/ | ISH | Intel Sensor Hub (가속도/자이로) |
| amd-sfh | drivers/hid/amd-sfh-hid/ | SFH | AMD 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년에 도입한 키보드/마우스 인터페이스입니다. 물리적으로는 거의 사라졌지만, 가상화 환경(QEMU, VMware)과 BIOS 에뮬레이션에서 여전히 활발히 사용됩니다. 커널의 serio 서브시스템이 PS/2 물리 계층을, atkbd/psmouse가 프로토콜 계층을 담당합니다.
| 프로토콜 | 드라이버 | 특징 |
|---|---|---|
| AT Set 2 | atkbd | 표준 AT 키보드 스캔코드 세트 2, 3바이트 확장키 |
| PS/2 Mouse | psmouse (bare) | 3바이트 패킷 (X, Y, 버튼) |
| ImPS/2 | psmouse | 4바이트 패킷 (IntelliMouse, 스크롤 추가) |
| Synaptics | psmouse + synaptics | 터치패드 절대좌표, 멀티핑거, 클릭패드 |
| ALPS | psmouse + alps | ALPS 터치패드 (v3~v8 프로토콜) |
| Elantech | psmouse + elantech | Elantech 터치패드 (v1~v4) |
| TrackPoint | psmouse + trackpoint | IBM/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 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 이벤트를 받아 포인터 가속, 제스처 인식, 탭-투-클릭, 스크롤 에뮬레이션 등 고수준 처리를 수행합니다.
# 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 |
| Clickfinger | 1/2/3 손가락 클릭 → 좌/우/중간 | 멀티터치 + BTN_TOOL_* |
터치패드 드라이버 상세
노트북 터치패드는 PS/2(Synaptics, ALPS, Elantech), I2C-HID, USB-HID 세 가지 경로로 연결됩니다. 최신 노트북 대부분은 I2C-HID를 사용하지만, PS/2 터치패드 프로토콜은 역사적으로 매우 복잡하며 여전히 활발히 유지보수됩니다.
| 제조사 | PS/2 드라이버 | I2C-HID | 프로토콜 버전 | 특징 |
|---|---|---|---|---|
| Synaptics | psmouse (synaptics.c) | hid-rmi | PS/2 v7+, RMI4 | 가장 널리 사용, clickpad, SMBus 모드 |
| ALPS | psmouse (alps.c) | hid-alps | v3~v8, U1 | 복잡한 바이트 패킷, trackstick 통합 |
| Elantech | psmouse (elantech.c) | hid-multitouch | v1~v4 | 세밀한 멀티터치, 합리적 가격 |
| Cypress | psmouse (cypress_ps2.c) | hid-multitouch | - | 일부 Acer/HP 제품 |
| Apple | - | applespi / hid-apple | SPI/USB | Force Touch, 고해상도 터치 |
| Microsoft | - | hid-multitouch | PTP (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 전원 관리
Input 서브시스템의 전원 관리는 open/close 콜백과 wakeup-source 두 메커니즘을 중심으로 동작합니다. evdev 클라이언트가 디바이스 파일을 열면 dev->open()이 호출되고, 모든 클라이언트가 닫으면 dev->close()가 호출되어 하드웨어를 비활성화할 수 있습니다.
/* 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~50us | threaded IRQ 사용 시 증가 |
| Input Core | 이벤트 라우팅, 타임스탬프 부여 | <10us | - |
| evdev 버퍼 | 원형 큐 저장, wake_up | <10us | - |
| 스케줄링 | user space 프로세스 깨우기 | 1~100us | RT 스케줄러, 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) | USB | xpad | 진동(rumble), LED, 무선 어댑터 |
| Xbox (BT) | Bluetooth | hid-microsoft | BLE 연결, 진동 |
| DualShock 4 | USB/BT | hid-playstation | 터치패드, 자이로, LED bar, 오디오 |
| DualSense | USB/BT | hid-playstation | 적응형 트리거, 햅틱, LED, 마이크 |
| Joy-Con | Bluetooth | hid-nintendo | IMU, HD rumble, NFC, IR 카메라 |
| Switch Pro | USB/BT | hid-nintendo | IMU, HD rumble, NFC |
| Steam Controller | USB/무선 | hid-steam | 터치패드(2), 자이로, 햅틱 |
| 8BitDo | USB/BT | hid-generic | 모드별 다른 HID descriptor |
| Google Stadia | USB | hid-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 확인
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 보안 — 세션 관리와 접근 제어
입력 장치에 대한 접근 제어는 시스템 보안의 핵심입니다. 키보드 입력을 스니핑하면 비밀번호를 탈취할 수 있고, 가상 입력을 주입하면 임의의 명령을 실행할 수 있습니다.
| 메커니즘 | 설명 | 사용처 |
|---|---|---|
| EVIOCGRAB | 디바이스 독점 — 다른 클라이언트 차단 | 게임, 스크린잠금, 키 매핑 도구 |
| EVIOCREVOKE | fd 접근 권한 철회 (재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 같은 세션 관리자가 사용
*/
흔한 실수와 트러블슈팅
set_bit(EV_KEY, dev->evbit) 없이 input_report_key()를 호출하면 이벤트가 조용히 무시됩니다. input_set_capability()를 사용하면 evbit을 자동으로 설정하므로 이 실수를 방지할 수 있습니다.
input_sync()(SYN_REPORT)를 빠뜨리면 이벤트 패킷이 닫히지 않아 evdev 클라이언트에서 이벤트가 보이지 않습니다. 하나의 하드웨어 이벤트 처리 후 반드시 input_sync()를 호출하세요.
INPUT_MT_DROP_UNUSED 플래그와 함께 input_mt_init_slots()을 사용했다면, 매 프레임마다 input_mt_sync_frame()을 호출해야 합니다. 빠뜨리면 이전에 활성이던 슬롯이 해제되지 않아 "유령 터치"가 발생합니다.
input_register_device() 이후에 set_bit()으로 새 capability를 추가하면 이미 연결된 핸들러가 새 이벤트를 인식하지 못합니다. 모든 capability는 등록 전에 설정하세요.
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);
터치패드에서 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 발생)
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.