ioctl 시스템 콜 심화
ioctl(input/output control)은 read/write로 표현하기 어려운 디바이스 고유 제어 명령을 사용자 공간에서 커널로 전달하는 범용 시스템 콜입니다. 명령 번호 인코딩 규약(_IO/_IOR/_IOW/_IOWR), VFS 디스패치 경로, file_operations.unlocked_ioctl 구현 패턴, 32-bit 호환(compat_ioctl), 터미널·네트워크·블록·GPU 등 주요 서브시스템 사례, 보안 검증, 대안 인터페이스(sysfs/netlink/configfs) 비교, strace/ftrace 기반 디버깅까지 실무 중심으로 상세히 다룹니다.
read/write가 편지(데이터)를 주고받는 우체통이라면, ioctl은 우체통 옆에 달린 제어판입니다.
"배달 속도 변경", "수신 모드 전환" 같은 특수 명령은 편지로 보내는 것이 아니라 제어판 버튼을 눌러 지시합니다.
핵심 요약
- ioctl —
read/write로 표현할 수 없는 디바이스 제어 명령을 전달하는 시스템 콜입니다. - 명령 번호 —
_IO/_IOR/_IOW/_IOWR매크로로 방향·타입·번호·크기를 32비트에 인코딩합니다. - unlocked_ioctl — 커널 2.6.36 이후 BKL 없이 호출되는 드라이버 콜백입니다 (구
.ioctl대체). - compat_ioctl — 64비트 커널에서 32비트 사용자 프로그램의 ioctl을 변환 처리합니다.
- copy_from/to_user — ioctl 인자가 포인터인 경우 반드시 사용자 메모리 접근 검증을 수행합니다.
학습 경로
- 개요에서 ioctl의 존재 이유와 역사를 파악합니다.
- 사용자 공간 API로 함수 시그니처와 호출 방법을 익힙니다.
- 명령 번호 인코딩에서 비트 레이아웃을 이해합니다.
- VFS 디스패치 경로로 커널 내부 호출 흐름을 추적합니다.
- 드라이버 구현 패턴에서 실제 코드를 작성해 봅니다.
ioctl 개요와 역사
ioctl은 1970년대 Unix V7에서 터미널 장치 제어를 위해 도입되었습니다. read/write는 바이트 스트림 전달에 특화되어 있어, "보드레이트 변경", "버퍼 플러시", "하드웨어 레지스터 설정" 같은 out-of-band 제어를 표현할 방법이 없었습니다. ioctl은 이 공백을 채우는 범용 제어 시스템 콜로 자리잡았습니다.
| 시기 | 이벤트 | 영향 |
|---|---|---|
| 1979 | Unix V7 stty/gtty → ioctl 통합 | 터미널 제어 단일 인터페이스 |
| 1990s | Linux 초기, BKL(Big Kernel Lock) 아래 .ioctl | 전역 락 병목 |
| 2006 | 커널 2.6.11 — unlocked_ioctl 도입 | BKL 없는 ioctl 경로 |
| 2010 | 커널 2.6.36 — 구 .ioctl 콜백 완전 제거 | unlocked_ioctl 필수 |
| 2020s | 대안 인터페이스(sysfs, netlink, configfs) 확산 | 새 서브시스템은 ioctl 최소화 추세 |
위 다이어그램처럼 read/write는 데이터 바이트 스트림을 전달하는 반면, ioctl은 디바이스 고유 제어 명령을 전달합니다.
이 분리 덕분에 데이터 전송 경로를 변경하지 않고도 디바이스 동작 모드를 자유롭게 제어할 수 있습니다.
ioctl이 사용되는 대표적인 디바이스 클래스를 정리하면:
| 디바이스 클래스 | ioctl 사용 밀도 | 대표 명령 | 설명 |
|---|---|---|---|
| 터미널 (TTY) | 높음 (~50) | TCGETS, TIOCGWINSZ | 보드레이트, 에코 모드, 창 크기 |
| 블록 디바이스 | 중간 (~20) | BLKGETSIZE64, BLKDISCARD | 디스크 토폴로지, TRIM |
| 네트워크 소켓 | 중간 (~30) | SIOCGIFADDR, FIONREAD | 인터페이스 설정 (레거시) |
| GPU / DRM | 매우 높음 (100+) | DRM_IOCTL_MODE_*, GEM_* | 모드 설정, 버퍼, 명령 제출 |
| KVM 가상화 | 높음 (~80) | KVM_CREATE_VM, KVM_RUN | VM/vCPU 생성·실행 |
| V4L2 비디오 | 높음 (~60) | VIDIOC_QUERYCAP, STREAMON | 캡처/출력 스트림 관리 |
| Watchdog | 낮음 (~10) | WDIOC_SETTIMEOUT | 타임아웃, keepalive |
| NVMe char device | 낮음 (~5) | NVME_IOCTL_ADMIN_CMD | 관리 명령 패스스루 |
사용자 공간 API
glibc에서 제공하는 ioctl 함수 시그니처는 다음과 같습니다:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
/*
* fd — 열려 있는 파일 디스크립터 (디바이스 파일, 소켓, 터미널 등)
* request — 명령 번호 (direction | type | nr | size 인코딩)
* ... — 명령에 따라 포인터 또는 정수 인자 (가변 인자)
*
* 반환: 성공 시 0 (또는 양수), 실패 시 -1 (errno 설정)
*/
인자 상세 설명
-
fd
open()으로 얻은 파일 디스크립터./dev/이하 캐릭터/블록 디바이스, 소켓, 터미널(/dev/tty) 등 모두 가능합니다. -
request
32비트 명령 번호.
_IO/_IOR/_IOW/_IOWR매크로로 생성합니다. 이 값이 커널에서 switch-case로 분기하는 핵심 키입니다. -
...
가변 인자. 대부분 사용자 공간 버퍼의 포인터(
void *)를 전달하며, 일부 명령은 정수 값을 직접 전달합니다.
사용자 공간에서의 전형적인 호출 예시:
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fs.h> /* BLKGETSIZE64 */
int main(void)
{
int fd = open("/dev/sda", O_RDONLY);
unsigned long long size;
if (ioctl(fd, BLKGETSIZE64, &size) == 0)
printf("디스크 크기: %llu 바이트\n", size);
close(fd);
return 0;
}
터미널 창 크기 조회 (TIOCGWINSZ)
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
int main(void)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0)
printf("터미널 크기: %d행 × %d열\n",
ws.ws_row, ws.ws_col);
return 0;
}
네트워크 인터페이스 정보 조회 (SIOCGIFFLAGS)
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <sys/socket.h>
int main(void)
{
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct ifreq ifr;
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) {
printf("eth0 플래그: 0x%x", ifr.ifr_flags);
if (ifr.ifr_flags & IFF_UP)
printf(" [UP]");
if (ifr.ifr_flags & IFF_RUNNING)
printf(" [RUNNING]");
printf("\n");
}
close(sock);
return 0;
}
Watchdog 타이머 설정 (WDIOC_SETTIMEOUT)
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/watchdog.h>
int main(void)
{
int fd = open("/dev/watchdog", O_WRONLY);
int timeout = 30; /* 30초 */
/* 타임아웃 설정 — 커널이 지원 가능한 값으로 조정 */
ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
printf("실제 설정된 타임아웃: %d초\n", timeout);
/* keepalive 핑 */
ioctl(fd, WDIOC_KEEPALIVE, 0);
/* 매직 클로즈: 'V' 기록 후 close → watchdog 비활성화 */
write(fd, "V", 1);
close(fd);
return 0;
}
epoll_create/epoll_ctl/epoll_wait는 원래 /dev/epoll의 ioctl이었고,
inotify_init/inotify_add_watch도 유사한 진화를 거쳤습니다.
최신 사례로 io_uring_setup/io_uring_enter/io_uring_register가 있습니다.
명령 번호 인코딩 (ioctl command number)
Linux는 ioctl 명령 번호를 32비트로 구조화하여 방향(direction), 매직 타입(type), 일련번호(nr), 데이터 크기(size)를 인코딩합니다.
이 규약은 include/uapi/asm-generic/ioctl.h에 정의되어 있습니다:
/* include/uapi/asm-generic/ioctl.h */
#define _IOC_NRBITS 8
#define _IOC_TYPEBITS 8
#define _IOC_SIZEBITS 14
#define _IOC_DIRBITS 2
#define _IOC(dir, type, nr, size) \
(((unsigned int)(dir) << _IOC_DIRSHIFT) | \
((unsigned int)(type) << _IOC_TYPESHIFT) | \
((unsigned int)(nr) << _IOC_NRSHIFT) | \
((unsigned int)(size) << _IOC_SIZESHIFT))
#define _IO(type, nr) _IOC(_IOC_NONE, (type), (nr), 0)
#define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), sizeof(size))
#define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), sizeof(size))
#define _IOWR(type, nr, size) _IOC(_IOC_READ|_IOC_WRITE, (type), (nr), sizeof(size))
디코딩 매크로
명령 번호에서 각 필드를 추출하는 매크로도 함께 제공됩니다:
- _IOC_DIR(cmd) 방향 필드 추출 (0=NONE, 1=WRITE, 2=READ, 3=RW)
- _IOC_TYPE(cmd) 매직 타입 추출 (서브시스템 식별자)
- _IOC_NR(cmd) 일련번호 추출 (서브시스템 내 명령 구분)
- _IOC_SIZE(cmd) 데이터 크기 추출 (바이트 단위)
주요 서브시스템의 매직 타입 할당 예시:
| 매직 타입 | 서브시스템 | 헤더 파일 |
|---|---|---|
'T' (0x54) | 터미널 (TTY) | include/uapi/asm-generic/ioctls.h |
'V' (0x56) | V4L2 비디오 | include/uapi/linux/videodev2.h |
0x12 | 블록 디바이스 | include/uapi/linux/fs.h |
'N' (0x4E) | NVMe character device | include/uapi/linux/nvme_ioctl.h |
0xAE | KVM 가상화 | include/uapi/linux/kvm.h |
'd' (0x64) | DRM/GPU | include/uapi/drm/drm.h |
0x89 | 소켓/네트워크 | include/uapi/linux/sockios.h |
Documentation/userspace-api/ioctl/ioctl-number.rst 파일에서 기존 할당을 확인하세요.
같은 매직 타입이라도 nr 범위가 겹치지 않으면 공존할 수 있습니다.
VFS ioctl 디스패치 경로
사용자 공간에서 ioctl(fd, cmd, arg)를 호출하면, 커널은 다음 경로를 따라 최종 드라이버 콜백에 도달합니다:
핵심 함수 호출 체인을 코드로 살펴보겠습니다:
/* fs/ioctl.c (Linux 6.x 기준, 핵심 경로만 발췌) */
long vfs_ioctl(struct file *filp,
unsigned int cmd, unsigned long arg)
{
int error = -ENOTTY;
if (!filp->f_op->unlocked_ioctl)
goto out;
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD)
error = -ENOTTY;
out:
return error;
}
static int do_vfs_ioctl(struct file *filp,
unsigned int fd,
unsigned int cmd,
unsigned long arg)
{
switch (cmd) {
case FIOCLEX: /* close-on-exec 설정 */
set_close_on_exec(fd, 1);
return 0;
case FIONCLEX: /* close-on-exec 해제 */
set_close_on_exec(fd, 0);
return 0;
case FIONBIO: /* 논블로킹 모드 전환 */
return ioctl_fionbio(filp, (int __user *)arg);
case FIOASYNC: /* 비동기 통지 설정 */
return ioctl_fioasync(fd, filp, (int __user *)arg);
/* ... 기타 공통 ioctl ... */
default:
break;
}
/* 드라이버 고유 ioctl로 전달 */
return vfs_ioctl(filp, cmd, arg);
}
-ENOIOCTLCMD를 반환하면, VFS 계층이 이를 사용자 공간에 -ENOTTY로 변환합니다.
-ENOIOCTLCMD는 커널 내부 전용 에러 코드이며 사용자 공간에 노출되지 않습니다.
드라이버 ioctl 구현 패턴
Character device 드라이버에서 ioctl을 구현하는 전형적인 패턴입니다:
/* 1. 명령 번호 정의 (uapi 헤더) */
#define MYDEV_MAGIC 'M'
#define MYDEV_RESET _IO(MYDEV_MAGIC, 0)
#define MYDEV_GET_STATUS _IOR(MYDEV_MAGIC, 1, struct mydev_status)
#define MYDEV_SET_CONFIG _IOW(MYDEV_MAGIC, 2, struct mydev_config)
#define MYDEV_XFER_DATA _IOWR(MYDEV_MAGIC, 3, struct mydev_xfer)
/* 2. ioctl 핸들러 구현 */
static long mydev_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
struct mydev_priv *priv = filp->private_data;
void __user *uarg = (void __user *)arg;
switch (cmd) {
case MYDEV_RESET:
return mydev_hw_reset(priv);
case MYDEV_GET_STATUS: {
struct mydev_status st;
mydev_read_status(priv, &st);
if (copy_to_user(uarg, &st, sizeof(st)))
return -EFAULT;
return 0;
}
case MYDEV_SET_CONFIG: {
struct mydev_config cfg;
if (copy_from_user(&cfg, uarg, sizeof(cfg)))
return -EFAULT;
return mydev_apply_config(priv, &cfg);
}
case MYDEV_XFER_DATA: {
struct mydev_xfer xfer;
if (copy_from_user(&xfer, uarg, sizeof(xfer)))
return -EFAULT;
/* 양방향: 처리 후 결과를 다시 복사 */
mydev_process_xfer(priv, &xfer);
if (copy_to_user(uarg, &xfer, sizeof(xfer)))
return -EFAULT;
return 0;
}
default:
return -ENOTTY;
}
}
/* 3. file_operations에 등록 */
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_release,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl, /* BKL-free ioctl */
.compat_ioctl = compat_ptr_ioctl, /* 포인터 크기만 다른 경우 */
};
구현 주의사항
-
copy_from_user / copy_to_user
사용자 공간 포인터를 직접 역참조하면 안 됩니다. 반드시 이 함수를 사용하여 접근 검증과 페이지 폴트 처리를 수행합니다. 실패 시
-EFAULT를 반환합니다. -
-ENOTTY
처리하지 않는 명령은
-ENOTTY(또는 내부 전용-ENOIOCTLCMD)를 반환합니다.-EINVAL을 반환하는 것은 잘못된 관행입니다. -
compat_ptr_ioctl
데이터 구조에 포인터가 포함되지 않고, 크기가 32/64비트에서 동일하면 이 헬퍼로 충분합니다. 포인터가 포함된 구조체는 별도
compat_ioctl핸들러가 필요합니다.
*(int *)arg 같이 사용자 공간 포인터를 직접 역참조하면 SMEP/SMAP 위반, 커널 OOPS, 또는 권한 상승 취약점이 발생합니다.
반드시 copy_from_user()/copy_to_user() 또는 get_user()/put_user()를 사용하세요.
compat_ioctl — 32/64비트 호환
64비트 커널에서 32비트 사용자 프로세스가 ioctl을 호출하면, 포인터 크기와 구조체 패딩이 다를 수 있습니다.
이를 처리하는 것이 compat_ioctl 콜백입니다:
| 시나리오 | 콜백 | 설명 |
|---|---|---|
| 64-bit user, 64-bit kernel | unlocked_ioctl |
네이티브 경로 |
| 32-bit user, 64-bit kernel | compat_ioctl |
인자 변환 후 처리 |
| 32-bit user, 32-bit kernel | unlocked_ioctl |
네이티브 경로 |
/* compat_ioctl이 필요한 경우: 구조체에 포인터가 포함될 때 */
struct mydev_buf_native {
__u32 size;
void *data; /* 64-bit: 8바이트, 32-bit: 4바이트 */
};
struct mydev_buf_compat {
__u32 size;
compat_uptr_t data; /* 항상 4바이트 */
};
static long mydev_compat_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
switch (cmd) {
case MYDEV_SUBMIT_BUF: {
struct mydev_buf_compat cb;
struct mydev_buf_native nb;
if (copy_from_user(&cb, compat_ptr(arg), sizeof(cb)))
return -EFAULT;
nb.size = cb.size;
nb.data = compat_ptr(cb.data); /* 32→64 포인터 변환 */
return mydev_submit_buf_internal(filp, &nb);
}
default:
/* 포인터 없는 명령은 네이티브 핸들러로 직접 전달 */
return mydev_ioctl(filp, cmd, arg);
}
}
__u64 타입을 사용하세요.
사용자 공간에서 (__u64)(uintptr_t)ptr로 변환하면 32/64비트 모두 동일한 구조체 크기가 됩니다.
커널에서는 u64_to_user_ptr() 헬퍼로 안전하게 역변환합니다.
주요 서브시스템 ioctl 사례
터미널 (TTY) ioctl
| 명령 | 매크로 | 설명 |
|---|---|---|
TCGETS | _IOR('T', 1, struct termios) | 현재 터미널 속성 조회 |
TCSETS | _IOW('T', 2, struct termios) | 터미널 속성 즉시 변경 |
TIOCGWINSZ | _IOR('T', 0x13, struct winsize) | 터미널 창 크기 조회 |
TIOCSWINSZ | _IOW('T', 0x14, struct winsize) | 터미널 창 크기 설정 |
TIOCSTI | _IOW('T', 0x12, char) | 입력 큐에 문자 삽입 (보안 이유로 제한됨) |
블록 디바이스 ioctl
| 명령 | 설명 |
|---|---|
BLKGETSIZE64 | 디바이스 전체 크기(바이트) 조회 |
BLKSSZGET | 논리 섹터 크기 조회 |
BLKFLSBUF | 버퍼 캐시 플러시 |
BLKDISCARD | 블록 범위 TRIM/Discard 요청 |
BLKROSET | 읽기 전용 모드 설정 |
네트워크 소켓 ioctl
| 명령 | 설명 |
|---|---|
SIOCGIFADDR | 인터페이스 IP 주소 조회 |
SIOCSIFADDR | 인터페이스 IP 주소 설정 |
SIOCGIFFLAGS | 인터페이스 플래그 조회 (UP/DOWN 등) |
SIOCGIFHWADDR | 하드웨어(MAC) 주소 조회 |
FIONREAD | 소켓 수신 버퍼 대기 바이트 수 |
SIOCGIFADDR 같은 네트워크 설정 ioctl은 레거시입니다. 현대 도구(ip 명령)는 Netlink 소켓을 사용합니다.
새로운 네트워크 제어 인터페이스는 ioctl 대신 Netlink나 BPF로 구현하세요.
GPU / DRM ioctl
DRM(Direct Rendering Manager) 서브시스템은 GPU 버퍼 할당, 명령 제출, 모드 설정을 모두 ioctl로 수행합니다. 단일 서브시스템에서 100개 이상의 ioctl을 정의하는 대표적인 사례입니다:
| 명령 | 설명 |
|---|---|
DRM_IOCTL_MODE_GETRESOURCES | 디스플레이 리소스(CRTC, 커넥터, 인코더) 조회 |
DRM_IOCTL_MODE_SETCRTC | 디스플레이 모드(해상도, 주사율) 설정 |
DRM_IOCTL_GEM_OPEN | GEM 버퍼 오브젝트 열기 |
DRM_IOCTL_PRIME_FD_TO_HANDLE | DMA-BUF fd를 GEM 핸들로 변환 |
V4L2 비디오 캡처 ioctl 플로우
V4L2는 ioctl을 가장 체계적으로 사용하는 서브시스템 중 하나입니다. 카메라에서 프레임을 캡처하는 전체 ioctl 시퀀스:
KVM ioctl 계층 구조
KVM은 ioctl을 3단계 파일 디스크립터 계층으로 구조화한 대표적인 사례입니다. 각 fd에서 사용 가능한 ioctl 집합이 다릅니다:
NVMe 관리 명령 패스스루
#include <linux/nvme_ioctl.h>
/* NVMe Identify Controller 명령 패스스루 */
struct nvme_admin_cmd cmd = {
.opcode = 0x06, /* Identify */
.nsid = 0,
.addr = (__u64)(uintptr_t)buf, /* __u64 포인터 패턴 */
.data_len = 4096,
.cdw10 = 1, /* CNS=1: Controller */
};
ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd);
/* buf에 4096바이트 Identify Controller 데이터 수신됨 */
NVME_URING_CMD_IO/NVME_URING_CMD_ADMIN으로 ioctl 대신 io_uring 비동기 패스스루가 가능합니다.
fio의 --ioengine=io_uring_cmd으로 측정하면 ioctl 대비 최대 2~3배 IOPS 향상을 확인할 수 있습니다.
Watchdog ioctl 커널 내부
/* drivers/watchdog/watchdog_dev.c — 커널 watchdog ioctl 핸들러 (발췌) */
static long watchdog_ioctl(struct file *file,
unsigned int cmd,
unsigned long arg)
{
struct watchdog_core_data *wd_data = file->private_data;
struct watchdog_device *wdd;
void __user *argp = (void __user *)arg;
int val;
mutex_lock(&wd_data->lock);
wdd = wd_data->wdd;
switch (cmd) {
case WDIOC_GETSUPPORT:
return copy_to_user(argp, wdd->info,
sizeof(struct watchdog_info))
? -EFAULT : 0;
case WDIOC_SETTIMEOUT:
if (get_user(val, (int __user *)argp))
return -EFAULT;
/* 드라이버가 실제 적용한 값으로 갱신 */
wdd->ops->set_timeout(wdd, val);
wdd->timeout = val;
/* 설정된 값을 반환 (하드웨어 제한으로 요청과 다를 수 있음) */
return put_user(wdd->timeout, (int __user *)argp);
case WDIOC_KEEPALIVE:
return watchdog_ping(wdd);
}
mutex_unlock(&wd_data->lock);
}
ioctl 보안 고려사항
ioctl은 디바이스에 대한 직접 제어를 허용하므로 보안 검증이 필수입니다:
| 검증 항목 | 메커니즘 | 설명 |
|---|---|---|
| 파일 접근 권한 | open() 시 권한 검사 |
fd를 열 수 있으면 ioctl도 호출 가능 |
| Capability 검사 | capable(CAP_SYS_ADMIN) |
위험한 ioctl은 특권 요구 |
| 사용자 메모리 검증 | access_ok(), copy_from/to_user() |
포인터 인자의 유효성 검증 |
| 입력값 범위 검증 | 드라이버 내 직접 검사 | 버퍼 크기 오버플로, 잘못된 enum 값 차단 |
| LSM / SELinux 훅 | security_file_ioctl() |
LSM 모듈이 ioctl 명령별 접근 제어 가능 |
| seccomp-BPF | ioctl 시스템 콜 필터링 | 컨테이너 환경에서 허용 ioctl 제한 |
/* 보안 검증 패턴 예시 */
static long mydev_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
switch (cmd) {
case MYDEV_FIRMWARE_UPDATE:
/* 펌웨어 업데이트는 특권 필요 */
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
/* ... */
break;
case MYDEV_SET_BUFFER_SIZE: {
__u32 size;
if (get_user(size, (__u32 __user *)arg))
return -EFAULT;
/* 범위 검증: 4KB ~ 16MB */
if (size < 4096 || size > (16 << 20))
return -EINVAL;
/* ... */
break;
}
}
return 0;
}
seccomp-BPF를 이용한 ioctl 필터링
컨테이너 환경에서 특정 ioctl 명령만 허용하는 seccomp-BPF 필터 예시:
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/prctl.h>
/*
* seccomp-BPF 필터: ioctl 시스템 콜의 두 번째 인자(cmd)를 검사하여
* TIOCSTI (터미널 입력 삽입) 명령을 차단합니다.
* 컨테이너 탈출 방지를 위해 Docker 기본 seccomp 프로필에도 포함됨.
*/
struct sock_filter filter[] = {
/* syscall 번호 로드 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
/* ioctl(16)이 아니면 ALLOW */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_ioctl, 0, 3),
/* ioctl 두 번째 인자(cmd) 로드 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, args[1])),
/* TIOCSTI(0x5412)이면 KILL */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TIOCSTI, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
/* 그 외 모든 syscall/ioctl 허용 */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = ARRAY_SIZE(filter),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
ioctl vs. 대안 인터페이스
| 인터페이스 | 장점 | 단점 | 적합한 사례 |
|---|---|---|---|
| ioctl | 빠름, 바이너리 전달, ABI 안정 | 타입 안전 없음, 문서화 어려움 | GPU, KVM, 블록 디바이스 |
| sysfs | 셸에서 직접 접근, 단순 | 복합 데이터 표현 어려움 | LED, PWM, cpufreq |
| Netlink | 비동기 이벤트, 멀티캐스트 | 구현 복잡 | 네트워크 설정 |
| configfs | 사용자 주도 트리 구성 | 오버헤드, 복잡한 구현 | USB Gadget, LIO 타겟 |
| procfs/debugfs | 간편, seq_file 통합 | debugfs는 ABI 미보장 | 진단, 디버깅 |
고급 ioctl 구현 패턴
restartable ioctl (ERESTARTSYS)
ioctl 핸들러가 시그널에 의해 중단될 수 있는 대기를 포함하면, -ERESTARTSYS를 반환하여 시스템 콜을 자동 재시작할 수 있습니다:
case MYDEV_WAIT_EVENT: {
int ret;
ret = wait_event_interruptible(priv->wq,
priv->event_ready);
if (ret)
return -ERESTARTSYS; /* 시그널 수신 시 자동 재시작 */
/* 이벤트 데이터를 사용자 공간으로 복사 */
if (copy_to_user(uarg, &priv->event, sizeof(priv->event)))
return -EFAULT;
return 0;
}
ioctl 테이블 디스패치 (DRM 스타일)
많은 ioctl을 관리해야 하는 서브시스템은 테이블 기반 디스패치를 사용합니다.
DRM 서브시스템의 drm_ioctl()이 대표적입니다:
/* DRM 스타일: 테이블 기반 ioctl 디스패치 */
struct mydev_ioctl_desc {
unsigned int cmd;
int flags; /* AUTH_REQUIRED, ROOT_ONLY 등 */
long (*func)(struct file *, void *);
size_t data_size;
};
static const struct mydev_ioctl_desc mydev_ioctls[] = {
[0] = { MYDEV_GET_STATUS, 0, mydev_get_status, sizeof(struct mydev_status) },
[1] = { MYDEV_SET_CONFIG, AUTH_REQUIRED, mydev_set_config, sizeof(struct mydev_config) },
[2] = { MYDEV_XFER_DATA, AUTH_REQUIRED, mydev_xfer_data, sizeof(struct mydev_xfer) },
};
static long mydev_ioctl(struct file *filp,
unsigned int cmd, unsigned long arg)
{
unsigned int nr = _IOC_NR(cmd);
const struct mydev_ioctl_desc *desc;
char kdata[128]; /* 스택 버퍼 (작은 구조체용) */
if (nr >= ARRAY_SIZE(mydev_ioctls))
return -ENOTTY;
desc = &mydev_ioctls[nr];
if (!desc->func)
return -ENOTTY;
/* 방향에 따라 copy_from_user 또는 memset */
if (_IOC_DIR(cmd) & _IOC_WRITE) {
if (copy_from_user(kdata, (void __user *)arg, desc->data_size))
return -EFAULT;
} else {
memset(kdata, 0, desc->data_size);
}
long ret = desc->func(filp, kdata);
if (!ret && (_IOC_DIR(cmd) & _IOC_READ)) {
if (copy_to_user((void __user *)arg, kdata, desc->data_size))
ret = -EFAULT;
}
return ret;
}
- copy_from/to_user를 공통 로직으로 일원화하여 누락 방지
- 권한 검사 플래그를 테이블에서 관리
- 데이터 크기 검증 자동화
- 새 ioctl 추가 시 테이블에 한 줄만 추가
ioctl 구조체 설계 가이드
ioctl 인터페이스를 올바르게 설계하려면 ABI 안정성, 32/64비트 호환성, 확장성을 고려해야 합니다. 커널 커뮤니티에서 검증된 설계 원칙을 정리합니다:
1. __u64 포인터 패턴 (compat_ioctl 회피)
/* ✗ 나쁜 설계: 포인터 직접 사용 → 32/64비트 크기 달라짐 */
struct bad_ioctl_data {
__u32 count;
void *buffer; /* 4 or 8 bytes! */
};
/* ✓ 좋은 설계: __u64로 포인터 대체 → 항상 동일한 크기 */
struct good_ioctl_data {
__u32 count;
__u32 __reserved; /* 명시적 패딩 */
__u64 buffer_ptr; /* 사용자 공간 포인터를 __u64로 */
};
/* 사용자 공간에서: */
data.buffer_ptr = (__u64)(uintptr_t)my_buffer;
/* 커널에서: */
void __user *ubuf = u64_to_user_ptr(data.buffer_ptr);
2. 예약 필드와 플래그를 이용한 확장
/* 확장 가능한 구조체 설계 패턴 */
struct mydev_ioctl_v1 {
__u32 size; /* sizeof(struct), 버전 식별 */
__u32 flags; /* 비트 플래그 (확장 지점) */
__u64 data_ptr;
__u32 data_len;
__u32 __reserved[3]; /* 향후 확장용, 0으로 설정 필수 */
};
/* 커널 검증: */
if (req.size < sizeof(struct mydev_ioctl_v1))
return -EINVAL;
/* 예약 필드가 0이 아니면 거부 → 미래 확장 시 호환성 보장 */
if (req.__reserved[0] || req.__reserved[1] || req.__reserved[2])
return -EINVAL;
/* 알 수 없는 플래그 비트 거부 */
if (req.flags & ~MYDEV_KNOWN_FLAGS)
return -EINVAL;
- size 필드: 구조체 첫 필드에
sizeof(struct)를 넣어 버전을 식별합니다 (io_uring, DRM 사용) - flags 필드: 비트 플래그로 선택적 기능을 추가하고, 알 수 없는 플래그는
-EINVAL로 거부합니다 - reserved 필드: 여분의
__reserved[N]을 두고, 0이 아니면 거부하여 미래 확장을 예약합니다
3. 정렬 규칙
| 규칙 | 설명 | 예시 |
|---|---|---|
| 자연 정렬 | N바이트 타입은 N바이트 경계에 배치 | __u64는 8바이트 경계 |
| 명시적 패딩 | 컴파일러 암묵 패딩 대신 명시적 __reserved 사용 |
__u32 pad; 추가 |
| 크기 8배수 | 구조체 전체 크기를 8의 배수로 맞춤 | 배열 내 패딩 문제 방지 |
| 고정 크기 타입 | __u32/__u64 사용, int/long 회피 |
아키텍처 독립적 크기 |
| packed 금지 | __attribute__((packed)) 사용 금지 |
비정렬 접근 성능 저하 |
/* ✗ 나쁜 예: 암묵 패딩, long 사용, packed */
struct bad_layout {
char type; /* offset 0, 7바이트 패딩 발생! */
long value; /* 4 or 8 bytes (아키텍처 의존) */
};
/* ✓ 좋은 예: 명시적 패딩, 고정 크기 타입 */
struct good_layout {
__u8 type;
__u8 __pad[3]; /* 명시적 패딩 */
__u32 flags;
__u64 value; /* 8바이트 경계에 정렬 */
}; /* total: 16 bytes, 32/64비트 동일 */
ioctl 디버깅 기법
strace를 이용한 추적
사용자 공간에서 어떤 ioctl이 호출되는지 가장 빠르게 확인하는 방법입니다:
# 특정 프로세스의 ioctl 호출만 필터링
strace -e ioctl -p 1234
# 출력 예시:
# ioctl(3, TCGETS, {B9600 opost isig icanon echo ...}) = 0
# ioctl(4, BLKGETSIZE64, [1000204886016]) = 0
# ioctl(5, SIOCGIFFLAGS, {ifr_name="eth0", ...}) = 0
# 알려지지 않은 ioctl은 16진수로 표시됨
# ioctl(6, _IOC(_IOC_READ, 0x4d, 0x01, 0x10), ...) = 0
ftrace를 이용한 커널 내부 추적
# do_vfs_ioctl 함수 진입 추적
echo "do_vfs_ioctl" > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 테스트 실행 ...
cat /sys/kernel/debug/tracing/trace
커널 로그 기반 디버깅
/* 드라이버 내 디버그 출력 */
static long mydev_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
dev_dbg(priv->dev,
"ioctl cmd=0x%x dir=%u type=0x%x nr=%u size=%u\n",
cmd, _IOC_DIR(cmd), _IOC_TYPE(cmd),
_IOC_NR(cmd), _IOC_SIZE(cmd));
/* ... */
}
_IOC_DIR, _IOC_TYPE, _IOC_NR, _IOC_SIZE 매크로로 분해할 수 있습니다.
파이썬 원라이너: python3 -c "cmd=0x80044d01; print(f'dir={cmd>>30} type={chr((cmd>>8)&0xff)} nr={cmd&0xff} size={(cmd>>16)&0x3fff}')"
bpftrace를 이용한 ioctl 프로파일링
# 1. 프로세스별 ioctl 호출 빈도 히스토그램
bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl {
@[comm] = count();
}'
# 2. 특정 디바이스의 ioctl 명령 번호 추적
bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl /comm == "qemu-system-x86"/ {
printf("pid=%d fd=%d cmd=0x%x\n", pid, args->fd, args->cmd);
}'
# 3. ioctl 지연 시간 측정 (마이크로초)
bpftrace -e '
tracepoint:syscalls:sys_enter_ioctl { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_ioctl /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 4. ioctl cmd 디코딩 (방향/타입/번호/크기)
bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl /pid == $1/ {
$cmd = args->cmd;
$dir = ($cmd >> 30) & 0x3;
$size = ($cmd >> 16) & 0x3fff;
$type = ($cmd >> 8) & 0xff;
$nr = $cmd & 0xff;
printf("cmd=0x%x dir=%d type=0x%x nr=%d size=%d\n",
$cmd, $dir, $type, $nr, $size);
}'
perf trace를 이용한 시스템 콜 프로파일링
# ioctl 시스템 콜만 추적하며 지연 시간 표시
perf trace -e ioctl -p 1234 --duration 0.1
# 출력 예시:
# 0.024 ( 0.003 ms): qemu/1234 ioctl(fd: 11, cmd: KVM_RUN) = 0
# 0.156 ( 0.001 ms): qemu/1234 ioctl(fd: 11, cmd: KVM_RUN) = 0
# --duration 0.1: 0.1ms 이상 걸린 호출만 필터링
# ioctl 호출 통계 요약
perf trace -s -e ioctl -p 1234 -- sleep 5
# 5초간 호출 횟수, 평균/최대 지연 시간 등 요약 출력
- strace — 가장 간편. ioctl 명령 이름을 자동 디코딩해 줌. 성능 오버헤드 큼
- perf trace — strace보다 낮은 오버헤드. 프로덕션 환경에서도 사용 가능
- bpftrace — 가장 유연. 커스텀 필터/집계/히스토그램. 커널 내부까지 추적 가능
- ftrace — 커널 내장. function_graph으로 커널 내부 호출 체인 시각화
- dev_dbg — 드라이버 개발 시 가장 직접적.
echo 'file mydev.c +p' > dynamic_debug/control
성능 고려사항
ioctl은 시스템 콜이므로 사용자↔커널 모드 전환 비용이 발생합니다. 고빈도 제어가 필요한 경우의 최적화 전략:
| 전략 | 설명 | 사례 |
|---|---|---|
| 배치 ioctl | 여러 명령을 하나의 ioctl에 배열로 전달 | DRM SUBMIT_CMD |
| mmap 공유 버퍼 | 제어 데이터를 mmap으로 공유하고 ioctl은 통지만 | io_uring SQ/CQ 링 |
| ioctl 최소화 | 초기 설정만 ioctl, 이후는 read/write/mmap | V4L2 STREAMON/DQBUF |
| io_uring passthrough | io_uring_cmd으로 ioctl을 비동기화 |
NVMe passthrough |
io_uring_cmd를 사용하면 ioctl과 동일한 드라이버 명령을 io_uring SQE로 비동기 제출할 수 있습니다.
NVMe 서브시스템이 최초로 이를 도입했으며, file_operations.uring_cmd 콜백으로 구현합니다.
NVMe 4K 랜덤 읽기 기준 대략적인 성능 비교 (참고 수치):
| 인터페이스 | IOPS (단일 스레드) | IOPS (멀티) | 지연(p99) | 모드 전환 |
|---|---|---|---|---|
| ioctl (동기) | ~150K | ~600K (4t) | ~15μs | 명령당 2회 |
| io_uring passthrough | ~400K | ~1.5M (4t) | ~8μs | 배치당 1회 |
| io_uring + SQPOLL | ~500K | ~2M+ (4t) | ~5μs | 0회 (폴링) |
fio --ioengine=io_uring_cmd으로 직접 측정하세요.
실제 커널 코드 분석
KVM ioctl — 가상 머신 생성
/* virt/kvm/kvm_main.c — KVM ioctl 핸들러 (발췌) */
static long kvm_dev_ioctl(struct file *filp,
unsigned int ioctl,
unsigned long arg)
{
switch (ioctl) {
case KVM_GET_API_VERSION:
return KVM_API_VERSION; /* 정수 직접 반환 */
case KVM_CREATE_VM:
return kvm_dev_ioctl_create_vm(arg);
case KVM_CHECK_EXTENSION:
return kvm_vm_ioctl_check_extension_generic(NULL, arg);
default:
return -EINVAL;
}
}
/* 사용자 공간 (QEMU): */
/* int kvm_fd = open("/dev/kvm", O_RDWR); */
/* int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0); */
/* int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0); */
블록 디바이스 — BLKGETSIZE64
/* block/ioctl.c — 블록 디바이스 공통 ioctl (발췌) */
static int blkdev_common_ioctl(struct block_device *bdev,
unsigned int cmd,
unsigned long arg,
fmode_t mode)
{
switch (cmd) {
case BLKGETSIZE64:
return put_user(bdev_nr_bytes(bdev),
(u64 __user *)arg);
case BLKSSZGET:
return put_user(bdev_logical_block_size(bdev),
(int __user *)arg);
case BLKFLSBUF:
return blkdev_flushbuf(bdev, mode, cmd, arg);
/* ... */
}
}
V4L2 ioctl 디스패치 구조
/* drivers/media/v4l2-core/v4l2-ioctl.c (발췌)
* V4L2는 video_ioctl2()를 통해 테이블 디스패치를 사용합니다.
* 각 ioctl은 v4l2_ioctls[] 테이블에 정의됩니다.
*/
struct v4l2_ioctl_info {
unsigned int ioctl;
u32 flags; /* INFO_FL_PRIO, INFO_FL_CTRL 등 */
const char *name; /* 디버그용 이름 */
union {
int (*func)(const struct v4l2_ioctl_ops *ops,
struct file *file, void *fh,
void *p);
} u;
};
/* 테이블 예시 (약 80개 항목 중 일부) */
static const struct v4l2_ioctl_info v4l2_ioctls[] = {
IOCTL_INFO(VIDIOC_QUERYCAP, v4l_querycap, 0),
IOCTL_INFO(VIDIOC_S_FMT, v4l_s_fmt, INFO_FL_PRIO),
IOCTL_INFO(VIDIOC_REQBUFS, v4l_reqbufs, INFO_FL_PRIO),
IOCTL_INFO(VIDIOC_QBUF, v4l_qbuf, 0),
IOCTL_INFO(VIDIOC_DQBUF, v4l_dqbuf, 0),
IOCTL_INFO(VIDIOC_STREAMON, v4l_streamon, INFO_FL_PRIO),
IOCTL_INFO(VIDIOC_STREAMOFF, v4l_streamoff, INFO_FL_PRIO),
/* ... */
};
V4L2 ioctl 디스패치 특징
-
v4l2_ioctl_info
DRM과 유사한 테이블 디스패치 패턴.
flags필드로 우선순위 검사(INFO_FL_PRIO: 스트리밍 중 호출 가능 여부)를 자동화합니다. -
IOCTL_INFO 매크로
ioctl 번호, 핸들러 함수, 플래그를 한 줄로 등록.
video_ioctl2()가 공통 copy_from/to_user를 수행하므로 각 핸들러는 커널 공간 포인터만 받습니다. -
v4l_querycap
드라이버의
v4l2_ioctl_ops.vidioc_querycap를 호출합니다. V4L2는file_operations.unlocked_ioctl에서video_ioctl2를 직접 연결하고, 내부에서 이 테이블을 룩업합니다.
DRM ioctl 보안 플래그
/* include/drm/drm_ioctl.h — DRM ioctl 플래그 시스템 */
#define DRM_AUTH 0x1 /* 인증된 클라이언트만 (DRM Master) */
#define DRM_MASTER 0x2 /* DRM Master 전용 */
#define DRM_ROOT_ONLY 0x4 /* root만 (사실상 미사용) */
#define DRM_RENDER_ALLOW 0x20 /* /dev/dri/renderD* 에서 허용 */
/* DRM ioctl 테이블 (발췌) */
static const struct drm_ioctl_desc drm_ioctls[] = {
DRM_IOCTL_DEF(DRM_IOCTL_GEM_CLOSE,
drm_gem_close_ioctl,
DRM_RENDER_ALLOW),
DRM_IOCTL_DEF(DRM_IOCTL_MODE_SETCRTC,
drm_mode_setcrtc,
DRM_MASTER), /* Master만 디스플레이 설정 */
DRM_IOCTL_DEF(DRM_IOCTL_PRIME_FD_TO_HANDLE,
drm_prime_fd_to_handle_ioctl,
DRM_RENDER_ALLOW | DRM_AUTH),
};
/dev/dri/card0(디스플레이 제어)과 /dev/dri/renderD128(GPU 연산 전용)로 fd를 분리합니다.
DRM_RENDER_ALLOW 플래그가 있는 ioctl만 renderD에서 허용되어, 비특권 GPU 연산이 디스플레이를 변경할 수 없습니다.
자세한 내용은 GPU (DRM/KMS) 문서를 참고하세요.
실전 튜토리얼: 완전한 ioctl 드라이버
이 섹션에서는 character device에 ioctl을 구현하는 완전한 커널 모듈 예제를 제공합니다. 온도 센서를 시뮬레이션하는 드라이버로, 3가지 ioctl을 지원합니다:
uapi 헤더 (사용자·커널 공유)
/* include/uapi/linux/thermal_sim.h */
#ifndef _UAPI_THERMAL_SIM_H
#define _UAPI_THERMAL_SIM_H
#include <linux/ioctl.h>
#include <linux/types.h>
#define TSIM_MAGIC 'Z'
struct tsim_temp {
__s32 celsius_m; /* 밀리섭씨 (예: 42500 = 42.5°C) */
__u32 flags; /* 상태 플래그 */
};
struct tsim_config {
__u32 interval_ms; /* 샘플링 주기 (밀리초) */
__u32 threshold_m; /* 경고 임계값 (밀리섭씨) */
__u32 flags;
__u32 __reserved; /* 확장용, 0 필수 */
};
#define TSIM_GET_TEMP _IOR(TSIM_MAGIC, 0, struct tsim_temp)
#define TSIM_SET_CONFIG _IOW(TSIM_MAGIC, 1, struct tsim_config)
#define TSIM_RESET _IO(TSIM_MAGIC, 2)
#define TSIM_FLAG_CRITICAL (1 << 0)
#define TSIM_FLAG_ENABLED (1 << 1)
#endif
커널 모듈 ioctl 핸들러
/* thermal_sim.c — 핵심 ioctl 핸들러 부분 */
static long tsim_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
struct tsim_dev *dev = filp->private_data;
void __user *uarg = (void __user *)arg;
switch (cmd) {
case TSIM_GET_TEMP: {
struct tsim_temp temp;
mutex_lock(&dev->lock);
temp.celsius_m = dev->current_temp;
temp.flags = 0;
if (dev->current_temp >= dev->threshold)
temp.flags |= TSIM_FLAG_CRITICAL;
mutex_unlock(&dev->lock);
if (copy_to_user(uarg, &temp, sizeof(temp)))
return -EFAULT;
return 0;
}
case TSIM_SET_CONFIG: {
struct tsim_config cfg;
if (copy_from_user(&cfg, uarg, sizeof(cfg)))
return -EFAULT;
/* 예약 필드 검증 */
if (cfg.__reserved)
return -EINVAL;
/* 범위 검증 */
if (cfg.interval_ms < 100 || cfg.interval_ms > 60000)
return -EINVAL;
if (cfg.threshold_m > 125000) /* 최대 125°C */
return -EINVAL;
mutex_lock(&dev->lock);
dev->interval = cfg.interval_ms;
dev->threshold = cfg.threshold_m;
mutex_unlock(&dev->lock);
return 0;
}
case TSIM_RESET:
mutex_lock(&dev->lock);
dev->current_temp = 25000; /* 25.0°C */
dev->interval = 1000; /* 1초 */
dev->threshold = 85000; /* 85.0°C */
mutex_unlock(&dev->lock);
return 0;
default:
return -ENOTTY;
}
}
static const struct file_operations tsim_fops = {
.owner = THIS_MODULE,
.open = tsim_open,
.release = tsim_release,
.unlocked_ioctl = tsim_ioctl,
.compat_ioctl = compat_ptr_ioctl,
};
설계 포인트 해설
-
__reserved 검증
예약 필드가 0이 아니면
-EINVAL을 반환합니다. 향후 이 필드에 새 기능을 추가할 때, 구 버전 커널은 해당 값을 거부하므로 사용자 프로그램이 지원 여부를 판단할 수 있습니다. -
mutex_lock
unlocked_ioctl은 BKL 없이 호출되므로 드라이버가 직접 동기화를 관리합니다. 디바이스별 뮤텍스가 가장 일반적인 패턴입니다. -
compat_ptr_ioctl
구조체에 포인터가 없고 고정 크기 타입(
__s32,__u32)만 사용하므로, 범용 compat 헬퍼로 충분합니다. - 범위 검증 interval_ms와 threshold_m의 물리적 한계를 명확히 검사합니다. 부호 없는 타입을 사용하여 음수 입력을 구조적으로 방지합니다.
사용자 공간 테스트 프로그램
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "thermal_sim.h"
int main(void)
{
int fd = open("/dev/tsim0", O_RDWR);
struct tsim_temp temp;
struct tsim_config cfg = {
.interval_ms = 500,
.threshold_m = 70000, /* 70.0°C */
};
/* 설정 변경 */
if (ioctl(fd, TSIM_SET_CONFIG, &cfg) < 0)
perror("SET_CONFIG");
/* 온도 조회 */
if (ioctl(fd, TSIM_GET_TEMP, &temp) == 0)
printf("온도: %d.%03d°C%s\n",
temp.celsius_m / 1000,
temp.celsius_m % 1000,
(temp.flags & TSIM_FLAG_CRITICAL)
? " [경고!]" : "");
/* 리셋 */
ioctl(fd, TSIM_RESET, 0);
close(fd);
return 0;
}
자주 묻는 질문 (FAQ)
Q: ioctl 반환값으로 양수를 사용해도 되나요?
네. ioctl()의 반환값은 관례상 성공 시 0이지만, KVM의 KVM_GET_API_VERSION처럼 양수를 반환하는 것도 허용됩니다.
단, glibc 래퍼가 -4095 ~ -1 범위를 에러로 해석하므로, 반환값이 이 범위와 겹치지 않도록 주의하세요.
파일 디스크립터를 반환하는 ioctl(KVM_CREATE_VM)도 흔한 패턴입니다.
Q: unlocked_ioctl에서 동기화는 어떻게 하나요?
BKL이 제거되었으므로 드라이버가 직접 동기화를 관리해야 합니다. 일반적인 패턴:
- 디바이스별
mutex로 직렬화 - 읽기 전용 쿼리는
rw_semaphore의 read lock - lock-free 가능한 상태 조회는
READ_ONCE()/WRITE_ONCE()
Q: 새 서브시스템에서 ioctl을 사용해도 되나요?
커널 커뮤니티의 일반적인 가이드라인:
- 네트워크 설정 → Netlink 권장
- 단순 속성 → sysfs 권장
- 고성능 디바이스 제어, 기존 ABI 확장 → ioctl 허용
- 복합 객체 관리 → configfs 권장
ioctl을 사용하기로 했다면, 반드시 ioctl-number.rst에 등록하고 uapi 헤더에 명확히 문서화하세요.
Q: seccomp으로 특정 ioctl 명령만 차단할 수 있나요?
seccomp-BPF는 시스템 콜 인자를 검사할 수 있으므로, ioctl 시스템 콜의 두 번째 인자(cmd)를 필터링하여 특정 명령만 허용/차단할 수 있습니다.
컨테이너 런타임(Docker, Podman)에서 이 기법을 활발히 사용합니다.
자세한 내용은 커널 보안 문서를 참고하세요.
Q: ioctl 인자로 포인터 대신 정수를 직접 전달할 수 있나요?
네. _IO(type, nr)로 정의된 명령은 세 번째 인자가 없거나, 정수 값을 직접 arg에 전달합니다.
예: KVM_CREATE_VM은 arg에 머신 타입 번호(보통 0)를 직접 전달하고, 반환값으로 VM fd를 받습니다.
FIONBIO는 int __user *를 받아 get_user()로 읽습니다.
관례상:
_IO— 데이터 없음 또는 정수 직접 전달_IOR/_IOW/_IOWR— 포인터를 통한 구조체 전달
Q: 같은 fd에 read/write와 ioctl을 동시에 호출해도 안전한가요?
VFS 자체는 동시 호출을 허용합니다. 안전성은 전적으로 드라이버 구현에 달려 있습니다.
- 대부분의 드라이버는 뮤텍스나 스핀락으로 ioctl과 read/write 간 동기화를 수행합니다
- V4L2처럼 스트리밍 중 일부 ioctl만 허용하는 서브시스템도 있습니다 (
INFO_FL_PRIO플래그) - 네트워크 소켓은 ioctl(SIOCGIFFLAGS 등)과 send/recv가 독립적이므로 안전합니다
멀티스레드 환경에서 fd를 공유할 때는 드라이버 문서를 반드시 확인하세요.
Q: ioctl 명령 번호의 크기 필드가 0인 구형 ioctl은 어떻게 처리하나요?
Linux 초기에 정의된 ioctl(예: TCGETS = 0x5401)은 _IO/_IOR/_IOW/_IOWR 매크로를 사용하지 않아
direction=0, size=0으로 인코딩됩니다. 이를 "구형(old-style) ioctl"이라 합니다.
커널은 이런 명령도 정상 처리합니다 — do_vfs_ioctl()의 switch-case가 매직 번호가 아닌 전체 cmd 값으로 매칭하기 때문입니다.
새로운 ioctl을 정의할 때는 반드시 _IO/_IOR/_IOW/_IOWR 매크로를 사용하세요.
Q: ioctl에서 대량의 데이터를 전달하는 가장 좋은 방법은?
ioctl의 _IOC_SIZE 필드는 14비트(최대 16383바이트)로 제한됩니다. 대량 데이터 전달 전략:
- 간접 포인터: 구조체에
__u64 data_ptr와__u32 data_len을 넣어 별도 버퍼를 가리킴 (DRM, KVM 방식) - mmap: 대량 데이터는
mmap으로 공유하고, ioctl은 제어/통지만 (V4L2, io_uring 방식) - read/write + ioctl: 데이터는 read/write로, 설정은 ioctl로 분리 (일반적인 캐릭터 디바이스)
16KB 이상의 데이터는 ioctl 구조체에 직접 담지 말고 간접 포인터나 mmap을 사용하세요.
관련 문서
- 시스템 콜 (System Call) — ioctl의 시스템 콜 진입 경로와 SYSCALL/SYSRET 메커니즘
- 디바이스 드라이버 — file_operations, character device 등록, 드라이버 생명주기
- VFS (Virtual File System) — 파일 오퍼레이션 디스패치 경로와 inode/file 구조체
- 커널 보안 — seccomp-BPF, LSM 훅, capability 검사
- Netlink — ioctl의 네트워크 설정 대안
- procfs/sysfs/debugfs — ioctl 대안 인터페이스 (파일 기반)
- io_uring — io_uring_cmd를 통한 비동기 ioctl 대안
- Serial / TTY — 터미널 ioctl의 대표적 사용처
- GPU (DRM/KMS) — DRM ioctl 테이블 디스패치 패턴
- 가상화 (KVM) — KVM ioctl 기반 VM 제어
- Block I/O — 블록 디바이스 ioctl (BLKGETSIZE64 등)