Linux ioctl, 디바이스를 제어하는 전용 채널

2026. 4. 6. 19:40·Dev/Linux
`read()`/`write()`로는 데이터를 주고받을 수 있지만,
"이 센서의 샘플링 레이트를 변경해줘" 같은 제어 명령은 어떻게 전달할까요?
그 답이 `ioctl`입니다.

1. ioctl이 왜 필요한가?

Linux에서 디바이스와 통신하는 기본 방법은 `read()`와 `write()`입니다.
그런데 이 두 가지로 표현하기 어려운 명령들이 있습니다.

"샘플링 레이트를 1000Hz로 설정해줘"
"현재 온도를 읽어줘"
"버퍼를 초기화해줘"
"카운터 값을 99로 교환하고 기존 값을 돌려줘"

이런 디바이스 고유의 제어 명령을 전달하는 시스템 콜이 `ioctl`(Input/Output Control)입니다.

/* 유저스페이스에서 호출하는 방식 */
int ioctl(int fd, unsigned long request, void *arg);
인자 설명
`fd` 열린 디바이스 파일 디스크립터
`request` 명령 코드 (매직 넘버 + 번호 + 방향 + 크기 인코딩)
`arg (void *)` 명령에 따라 정수값 또는 구조체 포인터

2. ioctl은 언제 쓰나요?

디바이스를 다루다 보면 read()/write()로 표현하기 애매한 순간이 생깁니다.

"샘플링 레이트를 1000Hz로 바꿔줘"
"현재 버퍼를 비워줘"
"카운터 값을 99로 설정하고 기존 값을 돌려줘"

데이터를 "읽고 쓰는" 것이 아니라 디바이스에 명령을 내리는 상황입니다. ioctl은 바로 이 용도로 설계된 시스템 콜입니다.

실제로 Linux의 주요 드라이버들이 이 방식을 씁니다.

드라이버ioctl 용도
V4L2 (카메라) 해상도, 프레임레이트 설정
ALSA (사운드) 샘플링 레이트, 채널 수 설정
네트워크 소켓 IP 주소, 인터페이스 정보 조회
TTY (시리얼) Baud rate, 흐름 제어 설정

3. ioctl 명령 코드 설계

ioctl의 핵심은 명령 코드(command number)를 어떻게 정의하느냐입니다. 커널은 명령 코드를 32비트 정수로 인코딩합니다.

32비트 명령 코드 구조:
┌────────────┬────────────┬─────────────────┬──────────────────┐
│  direction│    size   │  type (magic)  │   number (nr)  │
│   2 bits  │  14 bits  │   8 bits       │    8 bits      │
└────────────┴────────────┴─────────────────┴──────────────────┘

이걸 직접 계산하면 실수가 생깁니다. 커널이 제공하는 매크로를 씁니다.

 

`ioctl_cmd.h` — 커널과 유저가 함께 공유하는 헤더

#ifndef IOCTL_CMD_H
#define IOCTL_CMD_H

#include <linux/ioctl.h>

/* Magic number — 이 드라이버의 고유 식별자 */
#define IOCTL_MAGIC 'k'

/* 명령어 정의 */
#define IOCTL_RESET     _IO  (IOCTL_MAGIC, 0)            /* 데이터 없음 */
#define IOCTL_GET_COUNT _IOR (IOCTL_MAGIC, 1, int)       /* 커널 → 유저 */
#define IOCTL_SET_MSG   _IOW (IOCTL_MAGIC, 2, char[256]) /* 유저 → 커널 */
#define IOCTL_GET_MSG   _IOR (IOCTL_MAGIC, 3, char[256]) /* 커널 → 유저 */
#define IOCTL_EXCHANGE  _IOWR(IOCTL_MAGIC, 4, int)       /* 양방향 */

#define IOCTL_MAXNR 4

#endif

매크로 4종 정리

매크로 방향 데이터 크기 포함 용도
`_IO(magic, nr)` 없음 아니오 단순 제어 (reset, start 등)
`_IOR(magic, nr, type)` 커널 → 유저 예 값 읽기
`_IOW(magic, nr, type)` 유저 → 커널 예 값 설정
`_IOWR(magic, nr, type)` 양방향 예 값 교환

Magic Number란?

#define IOCTL_MAGIC 'k'

명령 코드의 상위 8비트를 차지하는 드라이버 고유 식별자입니다.

다른 드라이버 명령 코드와 충돌하지 않도록 드라이버마다 다른 문자를 씁니다.

커널 소스의 `Documentation/userspace-api/ioctl/ioctl-number.rst`에 이미 할당된 매직 번호 목록이 있습니다.

헤더 파일 공유 전략: `ioctl_cmd.h`를 커널 모듈(`chardev_ioctl.c`)과
유저 프로그램(`test_ioctl.c`) 양쪽에서 #include 합니다.
명령 코드를 한 곳에서 관리해 불일치를 원천 차단합니다.


4. 커널 측 구현 - `unlocked_ioctl`

file_operations에 등록

static struct file_operations fops = {
    .owner          = THIS_MODULE,
    .open           = device_open,
    .release        = device_release,
    .read           = device_read,
    .write          = device_write,
    .unlocked_ioctl = device_ioctl,  /* ioctl 핸들러 */
};

unlocked_ioctl vs ioctl

필드 커널 버전 특징
`.ioctl` 2.6.36 이전 호출 전 BKL(Big Kernel Lock) 자동 획득
`.unlocked_ioctl` 2.6.36 이후 BKL 없이 호출 — 드라이버가 동기화 책임

현대 커널에서는 반드시 `. unlocked_ioctl`을 써야 합니다.

ioctl 핸들러 구현

static long device_ioctl(struct file *file, unsigned int cmd,
                         unsigned long arg)
{
    int value;
    char user_msg[MSG_SIZE];

    /* ① 매직 넘버 검증 — 엉뚱한 명령 차단 */
    if (_IOC_TYPE(cmd) != IOCTL_MAGIC)
        return -ENOTTY;

    /* ② 명령 번호 범위 검증 */
    if (_IOC_NR(cmd) > IOCTL_MAXNR)
        return -ENOTTY;

    /* ③ 명령별 처리 */
    switch (cmd) {

    case IOCTL_RESET:
        access_count = 0;
        strcpy(message, "Reset!");
        break;

    case IOCTL_GET_COUNT:
        /* 커널 → 유저: copy_to_user() */
        if (copy_to_user((int __user *)arg, &access_count, sizeof(int)))
            return -EFAULT;
        break;

    case IOCTL_SET_MSG:
        /* 유저 → 커널: copy_from_user() */
        if (copy_from_user(user_msg, (char __user *)arg, MSG_SIZE))
            return -EFAULT;
        user_msg[MSG_SIZE - 1] = '\0';
        strcpy(message, user_msg);
        break;

    case IOCTL_GET_MSG:
        if (copy_to_user((char __user *)arg, message, MSG_SIZE))
            return -EFAULT;
        break;

    case IOCTL_EXCHANGE:
        /* 유저 값 읽기 → 기존 카운터 반환 → 새 값 적용 */
        if (copy_from_user(&value, (int __user *)arg, sizeof(int)))
            return -EFAULT;
        if (copy_to_user((int __user *)arg, &access_count, sizeof(int)))
            return -EFAULT;
        access_count = value;
        break;

    default:
        return -ENOTTY;  /* 알 수 없는 명령 */
    }

    return 0;
}

왜 -ENOTTY를 반환하나?

"Not a typewriter"의 약자로, 역사적으로 터미널 제어에서 유래했습니다.

오늘날에는 "이 디바이스는 이 ioctl을 지원하지 않는다"는 POSIX 표준 오류 코드로 쓰입니다.

 

`IOCTL_EXCHANGE` 양방향 동작 원리

포인터 하나로 값을 읽고 같은 포인터에 다른 값을 씁니다.

유저:  value = 999  →  ioctl(fd, IOCTL_EXCHANGE, &value)
                              │
커널:  copy_from_user → value 읽음 (999)
       copy_to_user  → access_count 반환
       access_count  = 999  적용
                              │
유저:  value 에 기존 카운터 값이 들어있음

5. 유저스페이스 측 구현

#include "ioctl_cmd.h"   /* 커널과 동일한 명령 코드 사용 */
#include <sys/ioctl.h>
#include <fcntl.h>

int main(void)
{
    int fd = open("/dev/ioctl_dev", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    int count;
    char message[256];

    /* 카운터 읽기 */
    ioctl(fd, IOCTL_GET_COUNT, &count);
    printf("Count: %d\n", count);

    /* 메시지 설정 */
    strcpy(message, "Hello from OnePaperHoon!");
    ioctl(fd, IOCTL_SET_MSG, message);

    /* 메시지 읽기 */
    memset(message, 0, sizeof(message));
    ioctl(fd, IOCTL_GET_MSG, message);
    printf("Message: %s\n", message);

    /* 값 교환 */
    int value = 999;
    ioctl(fd, IOCTL_EXCHANGE, &value);
    printf("Exchanged, got back: %d\n", value);

    /* 리셋 */
    ioctl(fd, IOCTL_RESET, NULL);

    close(fd);
    return 0;
}

6. 빌드 및 실행

파일 구성

day5-ioctl/
├── chardev_ioctl.c   # 커널 모듈
├── ioctl_cmd.h       # 명령 코드 정의 (공유 헤더)
├── test_ioctl.c      # 유저 테스트 프로그램
└── Makefile

Makefile 구성

이 Makefile은 커널 모듈과 유저 프로그램을 함께 빌드하고, 디바이스 파일 생성까지 관리합니다.

obj-m := chardev_ioctl.o
KDIR  := /lib/modules/$(shell uname -r)/build
PWD   := $(shell pwd)
USER_PROG := test_ioctl

all: module userspace

module:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

userspace:
	gcc -o $(USER_PROG) test_ioctl.c

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
	rm -f $(USER_PROG)

load:
	sudo insmod chardev_ioctl.ko
	dmesg | tail -10

create-dev:
	@read -p "Enter major number: " MAJOR; \
	sudo mknod /dev/ioctl_dev c $$MAJOR 0; \
	sudo chmod 666 /dev/ioctl_dev

test:
	./$(USER_PROG)
	dmesg | tail -20

unload:
	sudo rmmod chardev_ioctl
	sudo rm -f /dev/ioctl_dev

.PHONY: all module userspace clean load create-dev test unload

Makefile 타깃 요약

명령어 동작
`make` 커널 모듈 + 유저 프로그램 빌드
`make load` 모듈 로드 + dmesg
`make create-dev` `/dev/ioctl_dev` 생성
`make test` 테스트 프로그램 실행
`make unload` 모듈 언로드 + 디바이스 파일 삭제

실행 순서

# 1. 빌드
$ make

# 2. 모듈 로드
$ make load
[ 100.001] ioctl_dev: Registered with major 240
[ 100.002] ioctl_dev: Create device:
[ 100.003]   sudo mknod /dev/ioctl_dev c 240 0

# 3. /dev 파일 생성 (메이저 번호는 dmesg 확인)
$ make create-dev
Enter major number: 240
/dev/ioctl_dev 생성 완료

# 4. 테스트 실행
$ make test
====================================
ioctl Test Program
====================================

1. Getting count...
   Current count: 1

2. Setting message...
   Message set: Hello from OnePaperHoon!

3. Getting message...
   Message: Hello from OnePaperHoon!

4. Exchanging value...
   Sending: 999
   Received: 1

5. Getting count (after exchange)...
   Current count: 999

6. Resetting...
   Reset done!

7. Getting count (after reset)...
   Current count: 0

====================================
Test Complete!
====================================

# 5. 언로드
$ make unload

7. 마치며

ioctl은 /proc보다 복잡하지만, 실제 드라이버에서 훨씬 많이 사용됩니다.

명령 코드는 `_IO` / `_IOR` / `_IOW` / `_IOWR` 매크로로 만든다.
헤더 파일을 커널과 유저가 공유해 명령 코드 불일치를 막는다.
핸들러에서 매직 넘버를 검증하고 알 수 없는 명령은 `-ENOTTY`를 반환한다.
unlocked_ioctl을 쓰면 동기화는 드라이버가 직접 책임진다.

카메라(`V4 L2`), 사운드(`ALSA`), 네트워크(`SIOCGIFADDR`) 등

Linux의 주요 드라이버 인터페이스는 대부분 `ioctl` 기반입니다. 이 구조를 이해하면 그 코드들이 낯설지 않을 겁니다.


참고 자료

  • Linux Kernel Documentation: Documentation/userspace-api/ioctl/ioctl-number.rst
  • linux/ioctl.h 커널 소스
  • man 2 ioctl
  • LDD3(Linux Device Drivers 3rd Edition) — Chapter 6. Advanced Char Driver Operations

'Dev > Linux' 카테고리의 다른 글

Linux/proc 파일시스템, 커널과 대화하기  (0) 2026.04.03
Linux Kernel Module Parameters  (0) 2026.04.01
Linux Kernel Module 개발 입문 - Hello World 부터 시작하기  (0) 2026.03.28
'Dev/Linux' 카테고리의 다른 글
  • Linux/proc 파일시스템, 커널과 대화하기
  • Linux Kernel Module Parameters
  • Linux Kernel Module 개발 입문 - Hello World 부터 시작하기
onepaperhoon
onepaperhoon
한장훈님의 블로그 입니다.
  • onepaperhoon
    OnePaperHoon Blog
    onepaperhoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (20) N
      • Dev (18) N
        • System Programming (2)
        • Linux (4) N
        • CS (0)
        • Network, Protocol (0)
        • Grapics (11)
        • Web, App (0)
        • Design Pattern (1)
      • Projects (0)
      • TIL (2)
      • Life (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    OpenGL
    커널
    NVIC
    IPC
    공유 메모리
    module_param
    Linux
    graphics
    STM32
    Cortx-M
    드라이버
    named pipe
    KernelModule
    graphicapi
    raspberrypi
    임베디드
    glfw
    CharDevice
    CUBEMX
    그래픽스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
onepaperhoon
Linux ioctl, 디바이스를 제어하는 전용 채널
상단으로

티스토리툴바