`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의 주요 드라이버들이 이 방식을 씁니다.
| 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 |
