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 기반 디버깅까지 실무 중심으로 상세히 다룹니다.

관련 표준: POSIX.1-2017 §2.6 (ioctl 시맨틱), Single UNIX Specification — ioctl은 POSIX에서 "unspecified"로 분류되지만 사실상 모든 Unix 계열 시스템의 핵심 디바이스 제어 인터페이스입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 시스템 콜디바이스 드라이버 문서를 먼저 읽으세요. ioctl은 시스템 콜 진입 경로를 통해 커널에 도달하며, file_operations 구조체 기반의 드라이버 콜백을 호출합니다.
일상 비유: read/write가 편지(데이터)를 주고받는 우체통이라면, ioctl은 우체통 옆에 달린 제어판입니다. "배달 속도 변경", "수신 모드 전환" 같은 특수 명령은 편지로 보내는 것이 아니라 제어판 버튼을 눌러 지시합니다.

핵심 요약

  • ioctlread/write로 표현할 수 없는 디바이스 제어 명령을 전달하는 시스템 콜입니다.
  • 명령 번호_IO/_IOR/_IOW/_IOWR 매크로로 방향·타입·번호·크기를 32비트에 인코딩합니다.
  • unlocked_ioctl — 커널 2.6.36 이후 BKL 없이 호출되는 드라이버 콜백입니다 (구 .ioctl 대체).
  • compat_ioctl — 64비트 커널에서 32비트 사용자 프로그램의 ioctl을 변환 처리합니다.
  • copy_from/to_user — ioctl 인자가 포인터인 경우 반드시 사용자 메모리 접근 검증을 수행합니다.

학습 경로

  1. 개요에서 ioctl의 존재 이유와 역사를 파악합니다.
  2. 사용자 공간 API로 함수 시그니처와 호출 방법을 익힙니다.
  3. 명령 번호 인코딩에서 비트 레이아웃을 이해합니다.
  4. VFS 디스패치 경로로 커널 내부 호출 흐름을 추적합니다.
  5. 드라이버 구현 패턴에서 실제 코드를 작성해 봅니다.

ioctl 개요와 역사

ioctl은 1970년대 Unix V7에서 터미널 장치 제어를 위해 도입되었습니다. read/write는 바이트 스트림 전달에 특화되어 있어, "보드레이트 변경", "버퍼 플러시", "하드웨어 레지스터 설정" 같은 out-of-band 제어를 표현할 방법이 없었습니다. ioctl은 이 공백을 채우는 범용 제어 시스템 콜로 자리잡았습니다.

시기이벤트영향
1979Unix V7 stty/gttyioctl 통합터미널 제어 단일 인터페이스
1990sLinux 초기, 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 최소화 추세
설계 트레이드오프: ioctl은 강력하지만 "타입 안전하지 않은(untyped) 만능 인터페이스"라는 비판을 받습니다. 명령 번호 충돌, 문서화 부재, 32/64비트 호환 문제가 반복되어, 최신 커널에서는 netlink·configfs 등 구조화된 대안을 권장합니다. 그러나 기존 서브시스템(터미널, 블록, GPU 등)의 ioctl 인터페이스는 ABI 안정성 보장 대상이므로 여전히 핵심입니다.
read/write (데이터 경로) vs. ioctl (제어 경로) 개념 모델 사용자 애플리케이션 데이터 경로 (Data Path) read(fd, buf, count) write(fd, buf, count) 바이트 스트림 전달 파일 내용, 네트워크 패킷, 디바이스 데이터 입출력 제어 경로 (Control Path) ioctl(fd, cmd, arg) Out-of-Band 제어 명령 보드레이트 변경 (TCSETS) 디스크 크기 조회 (BLKGETSIZE64) IP 주소 설정 (SIOCSIFADDR) VM 생성 (KVM_CREATE_VM) GPU 버퍼 할당 (DRM_IOCTL_GEM_*) 하드웨어 리셋 (커스텀 _IO) 디바이스 드라이버 (file_operations) .read / .write .unlocked_ioctl / .compat_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_RUNVM/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;
}
ioctl vs. 전용 시스템 콜: 일부 ioctl은 너무 중요해서 나중에 전용 시스템 콜로 승격되었습니다. 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에 정의되어 있습니다:

ioctl 명령 번호 32-bit 레이아웃 bit 31..30 bit 29..16 bit 15..8 bit 7..0 direction 2 bits size 14 bits (최대 16383) type (매직 번호) 8 bits ('T', 'V', 0xAE …) nr 8 bits (0~255) direction 값: 00 = _IOC_NONE (데이터 전송 없음) 01 = _IOC_WRITE (사용자→커널, userspace writes data) 10 = _IOC_READ (커널→사용자, userspace reads data) 11 = _IOC_READ|_IOC_WRITE (양방향) 매크로 매핑: _IO(type, nr) → direction=00 _IOW(type, nr, datatype) → direction=01 _IOR(type, nr, datatype) → direction=10 _IOWR(type, nr, datatype)→ direction=11 ※ direction의 READ/WRITE는 사용자 공간 관점입니다 (커널 관점과 반대)
/* 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 deviceinclude/uapi/linux/nvme_ioctl.h
0xAEKVM 가상화include/uapi/linux/kvm.h
'd' (0x64)DRM/GPUinclude/uapi/drm/drm.h
0x89소켓/네트워크include/uapi/linux/sockios.h
매직 번호 충돌 방지: 새 ioctl을 정의할 때는 Documentation/userspace-api/ioctl/ioctl-number.rst 파일에서 기존 할당을 확인하세요. 같은 매직 타입이라도 nr 범위가 겹치지 않으면 공존할 수 있습니다.

VFS ioctl 디스패치 경로

사용자 공간에서 ioctl(fd, cmd, arg)를 호출하면, 커널은 다음 경로를 따라 최종 드라이버 콜백에 도달합니다:

ioctl() 커널 진입 → 드라이버 콜백 디스패치 User Space ioctl(fd, cmd, arg) Kernel Space sys_ioctl() → ksys_ioctl() fdget(fd) → struct file * do_vfs_ioctl(file, fd, cmd, arg) VFS 공통 ioctl 처리 FIOCLEX, FIONCLEX, FIONBIO, FIOASYNC, FIOQSIZE, FS_IOC_* vfs_ioctl(file, cmd, arg) file→f_op→unlocked_ioctl (file, cmd, arg) 32-bit 프로세스인 경우: file→f_op→compat_ioctl(file, cmd, arg) (없으면 -ENOTTY 반환)

핵심 함수 호출 체인을 코드로 살펴보겠습니다:

/* 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 vs -ENOTTY: 드라이버가 처리하지 않는 명령에 대해 -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);
    }
}
포인터 포함 구조체의 32/64-bit 레이아웃 차이 32-bit 프로세스 (total = 8 bytes) size (__u32) 4 bytes *data (ptr) 4 bytes offset 0 offset 4 offset 8 64-bit 프로세스 (total = 16 bytes!) size (__u32) 4 bytes PAD (정렬) 4 bytes *data (ptr) 8 bytes offset 0 offset 4 offset 8 offset 16 compat_ioctl이 필요한 이유 1. 포인터 크기: 4 → 8 bytes 2. 정렬 패딩: 추가 4 bytes 삽입 3. 구조체 크기: 8 → 16 bytes → copy_from_user() 크기 불일치! → _IOC_SIZE(cmd) 값도 달라짐! → compat_ioctl에서 수동 변환 필요 해결 패턴: 포인터 없는 구조체 설계 __u64 data_ptr; 로 포인터 대체 → 32/64비트 크기 동일 → compat_ioctl 불필요 최신 서브시스템(io_uring, DRM, KVM 최신 ioctl)은 이 패턴을 따릅니다 예: struct kvm_userspace_memory_region { __u64 userspace_addr; }
__u64 포인터 패턴: 새로운 ioctl을 설계할 때는 포인터 대신 __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소켓 수신 버퍼 대기 바이트 수
네트워크 ioctl → Netlink 전환: 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_OPENGEM 버퍼 오브젝트 열기
DRM_IOCTL_PRIME_FD_TO_HANDLEDMA-BUF fd를 GEM 핸들로 변환

V4L2 비디오 캡처 ioctl 플로우

V4L2는 ioctl을 가장 체계적으로 사용하는 서브시스템 중 하나입니다. 카메라에서 프레임을 캡처하는 전체 ioctl 시퀀스:

V4L2 비디오 캡처 ioctl 시퀀스 ① 초기화 (Negotiation) open("/dev/video0") VIDIOC_QUERYCAP VIDIOC_S_FMT (해상도, 포맷) VIDIOC_S_PARM (프레임레이트) ② 버퍼 설정 (Buffer Setup) VIDIOC_REQBUFS (버퍼 N개 요청) VIDIOC_QUERYBUF × N mmap() × N (버퍼 매핑) VIDIOC_QBUF × N (큐에 제출) ③ 스트림 시작 (Start) VIDIOC_STREAMON → DMA 전송 시작 → 인터럽트 활성화 → 캡처 하드웨어 동작 ④ 캡처 루프 (반복) poll(fd) 프레임 대기 VIDIOC_DQBUF 완료 버퍼 회수 프레임 처리 인코딩/표시 VIDIOC_QBUF 버퍼 재제출 반복 ⑤ 종료 (Teardown) VIDIOC_STREAMOFF → DMA 중지 munmap() × N → 버퍼 해제 close(fd) → 디바이스 닫기 커널: V4L2 프레임워크 → videobuf2 → DMA Engine → Camera ISP video_ioctl2() → v4l_ioctl_ops 테이블 디스패치 → vb2_ioctl_* → 드라이버 콜백 자세한 내용은 V4L2 서브시스템 심화 문서를 참고하세요

KVM ioctl 계층 구조

KVM은 ioctl을 3단계 파일 디스크립터 계층으로 구조화한 대표적인 사례입니다. 각 fd에서 사용 가능한 ioctl 집합이 다릅니다:

KVM ioctl 3단계 fd 계층 kvm_fd = open("/dev/kvm") KVM_GET_API_VERSION, KVM_CHECK_EXTENSION KVM_CREATE_VM → vm_fd 반환 ioctl(kvm_fd, KVM_CREATE_VM) vm_fd (가상 머신 인스턴스) KVM_SET_USER_MEMORY_REGION (메모리 슬롯 설정) KVM_CREATE_IRQCHIP, KVM_SET_TSS_ADDR KVM_CREATE_VCPU → vcpu_fd 반환 vcpu_fd[0] (vCPU 0) KVM_RUN (게스트 실행) KVM_GET/SET_REGS KVM_GET/SET_SREGS vcpu_fd[1] (vCPU 1) KVM_RUN (게스트 실행) KVM_INTERRUPT KVM_SET_CPUID2 각 fd 수준에서 사용 가능한 ioctl이 다름 → QEMU는 이 계층으로 전체 가상 머신을 구성

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 + io_uring: Linux 5.19부터 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 보안 검증 체인 (5단계) ① seccomp-BPF 시스템 콜 진입 전 cmd 값 필터링 → EPERM / KILL ② LSM 훅 security_file_ioctl() SELinux/AppArmor 정책별 접근 제어 ③ VFS 공통 검사 do_vfs_ioctl() fd 유효성 검증 파일 모드 확인 ④ Capability capable(CAP_*) 드라이버 내부 → EPERM ⑤ uaccess 검증 copy_from/to_user() access_ok() → EFAULT ioctl 에러 코드 종합 -ENOTTY (25) 지원하지 않는 ioctl 명령 (가장 흔한 에러) -EFAULT (14) 잘못된 사용자 공간 포인터 (copy_from/to_user 실패) -EPERM (1) 권한 부족 (capability 검사 실패) -EINVAL (22) 잘못된 인자 값 (범위 초과, 잘못된 플래그) -EBUSY (16) 디바이스가 사용 중 (배타적 접근) -ENOMEM (12) 메모리 할당 실패 -ERESTARTSYS 시그널 인터럽트 (시스템 콜 자동 재시작) -ENOIOCTLCMD 커널 내부 전용 (VFS가 -ENOTTY로 변환)

ioctl vs. 대안 인터페이스

커널↔사용자 공간 제어 인터페이스 비교 User Space Application ioctl() 디바이스 고유 제어 타입 안전 ✗ 바이너리 인터페이스 ABI 안정성 높음 sysfs 속성별 파일 (1값=1파일) 텍스트 기반 셸에서 직접 접근 단순 read/write Netlink 소켓 기반 메시지 비동기 이벤트 지원 멀티캐스트 그룹 확장성 우수 configfs 사용자 주도 설정 트리 mkdir/rmdir로 객체 관리 USB Gadget, LIO 타겟 구조화된 계층 procfs/debugfs 프로세스/디버그 정보 텍스트 기반 debugfs는 ABI 미보장 진단/모니터링 목적 Kernel Subsystems / Device Drivers 권장 용도 기존 ABI 유지 고성능 디바이스 제어 GPU, KVM, DRM 단순 속성 조회/설정 디바이스 모델 통합 LED, PWM, cpufreq 네트워크 설정 이벤트 알림 라우팅, NIC 설정 복합 객체 구성 사용자 주도 생성 USB Gadget, LIO 런타임 진단 디버그 정보 노출 프로세스, 트레이싱
인터페이스장점단점적합한 사례
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;
확장 가능 ioctl의 3대 원칙:
  • 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));
    /* ... */
}
디코딩 팁: strace가 16진수로만 출력하는 ioctl은 _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 passthrough (Linux 5.19+): io_uring_cmd를 사용하면 ioctl과 동일한 드라이버 명령을 io_uring SQE로 비동기 제출할 수 있습니다. NVMe 서브시스템이 최초로 이를 도입했으며, file_operations.uring_cmd 콜백으로 구현합니다.
ioctl vs. io_uring passthrough 경로 비교 동기 ioctl 경로 ioctl(fd, NVME_CMD, &cmd) SYSCALL 진입 (모드 전환) copy_from_user (cmd) NVMe 큐 제출 + 완료 대기 copy_to_user (결과) SYSRET (모드 전환) 동기 대기: 스레드 블로킹 명령당 2회 모드 전환 io_uring passthrough 경로 SQE 작성 (mmap 공유 링) io_uring_enter (배치 제출) 커널: SQE → io_uring_cmd NVMe 큐 제출 (비동기) 완료 → CQE 링에 기록 사용자: CQE 폴링 (mmap) 비동기: N개 명령 배치 제출 SQPOLL 모드: 모드 전환 0회 vs

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회 (폴링)
벤치마크 주의: 위 수치는 특정 NVMe SSD와 CPU에서의 대략적인 참고값입니다. 실제 성능은 디바이스, 큐 깊이, I/O 크기, CPU 아키텍처에 따라 크게 다릅니다. 정확한 비교는 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),
};
DRM 보안 모델: DRM은 /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_VMarg에 머신 타입 번호(보통 0)를 직접 전달하고, 반환값으로 VM fd를 받습니다. FIONBIOint __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을 사용하세요.

필수 관련 문서: 참고 문서: