ioctl 시스템 콜(System Call)

ioctl(input/output control)은 read/write로 표현하기 어려운 디바이스 고유 제어 명령을 사용자 공간(User Space)에서 커널로 전달하는 범용 시스템 콜입니다. 명령 번호 인코딩 규약(_IO/_IOR/_IOW/_IOWR), VFS 디스패치(Dispatch) 경로, file_operations.unlocked_ioctl 구현 패턴, 32-bit 호환(compat_ioctl), 터미널·네트워크·블록·GPU 등 주요 서브시스템 사례, 보안 검증, 대안 인터페이스(sysfs/netlink/configfs) 비교, strace/ftrace 기반 디버깅(Debugging)까지 실무 중심으로 상세히 다룹니다.

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

핵심 요약

  • ioctlread/write로 표현할 수 없는 디바이스 제어 명령을 전달하는 시스템 콜입니다.
  • 명령 번호_IO/_IOR/_IOW/_IOWR 매크로(Macro)로 방향·타입·번호·크기를 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는 바이트 스트림 전달에 특화되어 있어, "보드레이트 변경", "버퍼(Buffer) 플러시(Flush)", "하드웨어 레지스터(Register) 설정" 같은 out-of-band 제어를 표현할 방법이 없었습니다. ioctl은 이 공백을 채우는 범용 제어 시스템 콜로 자리잡았습니다.

시기이벤트영향
1979Unix V7 stty/gttyioctl 통합터미널 제어 단일 인터페이스
1990sLinux 초기, BKL(Big Kernel Lock) 아래 .ioctl전역 락 병목(Bottleneck)
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디스크 토폴로지(Topology), TRIM
네트워크 소켓(Socket)중간 (~30)SIOCGIFADDR, FIONREAD인터페이스 설정 (레거시)
GPU / DRM매우 높음 (100+)DRM_IOCTL_MODE_*, GEM_*모드 설정, 버퍼, 명령 제출
KVM 가상화(Virtualization)높음 (~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()으로 얻은 파일 디스크립터(File Descriptor). /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 타이머(Timer) 설정 (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 사용자 공간 포인터를 직접 역참조(Dereference)하면 안 됩니다. 반드시 이 함수를 사용하여 접근 검증과 페이지 폴트(Page Fault) 처리를 수행합니다. 실패 시 -EFAULT를 반환합니다.
  • -ENOTTY 처리하지 않는 명령은 -ENOTTY(또는 내부 전용 -ENOIOCTLCMD)를 반환합니다. -EINVAL을 반환하는 것은 잘못된 관행입니다.
  • compat_ptr_ioctl 데이터 구조에 포인터가 포함되지 않고, 크기가 32/64비트에서 동일하면 이 헬퍼로 충분합니다. 포인터가 포함된 구조체는 별도 compat_ioctl 핸들러(Handler)가 필요합니다.
보안 경고 — 사용자 포인터 직접 접근 금지: *(int *)arg 같이 사용자 공간 포인터를 직접 역참조하면 SMEP/SMAP 위반, 커널 OOPS, 또는 권한 상승 취약점(Vulnerability)이 발생합니다. 반드시 copy_from_user()/copy_to_user() 또는 get_user()/put_user()를 사용하세요.

compat_ioctl — 32/64비트 호환

64비트 커널에서 32비트 사용자 프로세스(Process)가 ioctl을 호출하면, 포인터 크기와 구조체 패딩(Padding)이 다를 수 있습니다. 이를 처리하는 것이 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버퍼 캐시(Cache) 플러시
BLKDISCARD블록 범위 TRIM/Discard 요청
BLKROSET읽기 전용(Read-Only) 모드 설정

네트워크 소켓 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 명령별 접근 제어(Access Control) 가능
seccomp-BPF ioctl 시스템 콜 필터링 컨테이너(Container) 환경에서 허용 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 사용자 주도 트리 구성 오버헤드(Overhead), 복잡한 구현 USB Gadget, LIO 타겟
procfs/debugfs 간편, seq_file 통합 debugfs는 ABI 미보장 진단, 디버깅

고급 ioctl 구현 패턴

restartable ioctl (ERESTARTSYS)

ioctl 핸들러가 시그널(Signal)에 의해 중단될 수 있는 대기를 포함하면, -ERESTARTSYS를 반환하여 시스템 콜을 자동 재시작(Reboot)할 수 있습니다:

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 프로파일링(Profiling)

# 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 (단일 스레드(Thread))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회 (폴링(Polling))
벤치마크 주의: 위 수치는 특정 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 필드로 우선순위(Priority) 검사(INFO_FL_PRIO: 스트리밍 중 호출 가능 여부)를 자동화합니다.
  • IOCTL_INFO 매크로 ioctl 번호, 핸들러 함수, 플래그를 한 줄로 등록. video_ioctl2()가 공통 copy_from/to_user를 수행하므로 각 핸들러는 커널 공간(Kernel Space) 포인터만 받습니다.
  • 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을 구현하는 완전한 커널 모듈(Kernel Module) 예제를 제공합니다. 온도 센서를 시뮬레이션하는 드라이버로, 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 없이 호출되므로 드라이버가 직접 동기화를 관리합니다. 디바이스별 뮤텍스(Mutex)가 가장 일반적인 패턴입니다.
  • 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;
}

ioctl 번호 디코딩 실습

실제 ioctl 번호를 비트 단위로 분해하여 의미를 파악하는 방법을 실습합니다. strace 출력이나 커널 로그에서 16진수 ioctl 번호를 만났을 때 유용합니다.

셸에서 디코딩

#!/bin/bash
# ioctl 번호 디코더
decode_ioctl() {
    local cmd=$1
    local dir=$(( (cmd >> 30) & 0x3 ))
    local size=$(( (cmd >> 16) & 0x3FFF ))
    local type=$(( (cmd >> 8) & 0xFF ))
    local nr=$(( cmd & 0xFF ))

    local dir_str
    case $dir in
        0) dir_str="NONE (_IO)" ;;
        1) dir_str="WRITE (_IOW, 사용자→커널)" ;;
        2) dir_str="READ (_IOR, 커널→사용자)" ;;
        3) dir_str="READ|WRITE (_IOWR)" ;;
    esac

    printf "cmd=0x%08X\n" "$cmd"
    printf "  direction : %d (%s)\n" "$dir" "$dir_str"
    printf "  size      : %d bytes\n" "$size"
    printf "  type      : 0x%02X ('%c')\n" "$type" "$type"
    printf "  nr        : %d\n" "$nr"
}

# 예시: TCGETS = 0x5401 (구형, 인코딩 없음)
decode_ioctl 0x5401

# 예시: KVM_CREATE_VM = _IO(0xAE, 0x01) = 0x0000AE01
decode_ioctl 0xAE01

# 예시: KVM_GET_API_VERSION = _IO(0xAE, 0x00)
decode_ioctl 0xAE00

C 코드에서 디코딩

#include <stdio.h>
#include <linux/ioctl.h>

void decode_ioctl(unsigned int cmd)
{
    printf("cmd=0x%08X\n", cmd);
    printf("  dir  = %u\n", _IOC_DIR(cmd));
    printf("  type = 0x%02X ('%c')\n", _IOC_TYPE(cmd), _IOC_TYPE(cmd));
    printf("  nr   = %u\n", _IOC_NR(cmd));
    printf("  size = %u\n", _IOC_SIZE(cmd));
}

int main(void)
{
    /* V4L2 VIDIOC_QUERYCAP = _IOR('V', 0, struct v4l2_capability) */
    decode_ioctl(0x80685600);
    return 0;
}
ioctl 예시16진수 값directiontypenrsize
TCGETS0x54010 (구형)0x54 ('T')10
KVM_CREATE_VM0xAE010 (NONE)0xAE10
VIDIOC_QUERYCAP0x806856002 (READ)0x56 ('V')0104
DRM_IOCTL_VERSION0xC04064003 (R/W)0x64 ('d')064

고급 보안 패턴

ioctl 핸들러에서 발생할 수 있는 보안 취약점과 이를 방지하는 심층 방어(Defense-in-Depth) 패턴을 다룹니다.

TOCTOU 방지

사용자 공간 데이터를 두 번 읽으면 Time-of-Check-Time-of-Use(TOCTOU) 취약점이 발생할 수 있습니다:

/* ✗ 위험: TOCTOU — 두 번째 copy_from_user 사이에 값이 변경될 수 있음 */
if (copy_from_user(&hdr, uarg, sizeof(hdr)))
    return -EFAULT;
if (hdr.size > MAX_SIZE)   /* 검증 */
    return -EINVAL;
/* ... 다른 스레드가 hdr.size를 변경 가능 ... */
copy_from_user(buf, uarg, hdr.size);  /* 다시 읽으면 변경된 값! */

/* ✓ 안전: 한 번만 복사하고 커널 복사본 사용 */
if (copy_from_user(&hdr, uarg, sizeof(hdr)))
    return -EFAULT;
if (hdr.size > MAX_SIZE)
    return -EINVAL;
/* hdr.size는 커널 메모리에 있으므로 변경 불가 */
if (copy_from_user(buf, u64_to_user_ptr(hdr.data_ptr), hdr.size))
    return -EFAULT;

세밀한 Capability 검사

/* 명령별 차등 권한 검사 패턴 */
static long mydev_ioctl(struct file *filp,
                        unsigned int cmd, unsigned long arg)
{
    /* 읽기 전용 명령 — 권한 불필요 */
    switch (cmd) {
    case MYDEV_GET_INFO:
    case MYDEV_GET_STATUS:
        break;  /* 누구나 호출 가능 */

    /* 설정 변경 — CAP_NET_ADMIN (네트워크 디바이스인 경우) */
    case MYDEV_SET_OFFLOAD:
    case MYDEV_SET_RING_SIZE:
        if (!capable(CAP_NET_ADMIN))
            return -EPERM;
        break;

    /* 하드웨어 직접 제어 — CAP_SYS_RAWIO */
    case MYDEV_REG_READ:
    case MYDEV_REG_WRITE:
        if (!capable(CAP_SYS_RAWIO))
            return -EPERM;
        break;

    /* 펌웨어/구성 영구 변경 — CAP_SYS_ADMIN */
    case MYDEV_FLASH_FW:
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        break;
    }

    /* ... 실제 처리 ... */
}
Capability적용 범위ioctl 사용 사례
CAP_SYS_ADMIN일반 관리펌웨어 업데이트, 디바이스 리셋
CAP_SYS_RAWIO원시 I/O레지스터 직접 접근, DMA 제어
CAP_NET_ADMIN네트워크 관리NIC 오프로드 설정, 링 크기 변경
CAP_DAC_OVERRIDE파일 권한 우회다른 사용자의 디바이스 접근
CAP_PERFMON성능 모니터링PMU 레지스터 접근 (perf_event)

입력 데이터 검증 체크리스트

/* 종합 검증 패턴 */
static int validate_request(const struct mydev_request *req)
{
    /* 1. 구조체 크기 검증 (버전 호환) */
    if (req->size < MYDEV_REQUEST_MIN_SIZE)
        return -EINVAL;

    /* 2. 알 수 없는 플래그 거부 */
    if (req->flags & ~MYDEV_VALID_FLAGS)
        return -EINVAL;

    /* 3. 예약 필드가 0인지 확인 */
    if (req->__reserved[0] || req->__reserved[1])
        return -EINVAL;

    /* 4. 범위 검증 — 정수 오버플로 방지 */
    if (req->offset > MAX_OFFSET)
        return -EINVAL;
    if (req->length > MAX_LENGTH ||
        req->offset + req->length < req->offset)  /* 오버플로 검사 */
        return -EINVAL;

    /* 5. 정렬 검사 */
    if (req->offset & (MYDEV_ALIGN - 1))
        return -EINVAL;

    return 0;
}

ioctl 래퍼 라이브러리 설계

ioctl 인터페이스를 사용자 공간에서 안전하게 활용하려면 래퍼(Wrapper) 라이브러리를 설계하는 것이 좋습니다. 타입 안전성과 에러 처리를 일관되게 제공합니다.

/* libmydev.h — 타입 안전 래퍼 */
#ifndef LIBMYDEV_H
#define LIBMYDEV_H

#include <linux/mydev_uapi.h>

struct mydev_handle;

/* 디바이스 열기/닫기 */
struct mydev_handle *mydev_open(const char *path);
void mydev_close(struct mydev_handle *h);

/* 타입 안전 ioctl 래퍼 */
int mydev_get_status(struct mydev_handle *h,
                     struct mydev_status *out);
int mydev_set_config(struct mydev_handle *h,
                     const struct mydev_config *cfg);
int mydev_reset(struct mydev_handle *h);

/* 에러 정보 */
const char *mydev_strerror(int err);

#endif
/* libmydev.c — 구현 */
#include "libmydev.h"
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>

struct mydev_handle {
    int fd;
    int last_errno;
};

int mydev_get_status(struct mydev_handle *h,
                     struct mydev_status *out)
{
    int ret = ioctl(h->fd, MYDEV_GET_STATUS, out);
    if (ret < 0) {
        h->last_errno = errno;
        return -errno;
    }
    return 0;
}

int mydev_set_config(struct mydev_handle *h,
                     const struct mydev_config *cfg)
{
    /* 사용자 측에서도 사전 검증 */
    if (!cfg || cfg->interval_ms == 0)
        return -EINVAL;

    int ret = ioctl(h->fd, MYDEV_SET_CONFIG, cfg);
    if (ret < 0) {
        h->last_errno = errno;
        return -errno;
    }
    return 0;
}

ioctl 테스트 패턴

커널 ioctl 인터페이스를 체계적으로 검증하기 위한 테스트 패턴을 소개합니다.

네거티브 테스트

/* 잘못된 명령 번호 테스트 */
static void test_invalid_cmd(int fd)
{
    int ret;

    /* 존재하지 않는 ioctl 명령 */
    ret = ioctl(fd, _IO('M', 255), 0);
    assert(ret == -1 && errno == ENOTTY);

    /* NULL 포인터 전달 (포인터 인자 필수 명령) */
    ret = ioctl(fd, MYDEV_GET_STATUS, NULL);
    assert(ret == -1 && errno == EFAULT);

    /* 잘못된 fd */
    ret = ioctl(-1, MYDEV_RESET, 0);
    assert(ret == -1 && errno == EBADF);

    /* 범위 초과 값 */
    struct mydev_config cfg = { .interval_ms = 0 };  /* 0은 무효 */
    ret = ioctl(fd, MYDEV_SET_CONFIG, &cfg);
    assert(ret == -1 && errno == EINVAL);
}

/* 권한 테스트 */
static void test_privilege(int fd)
{
    /* 비특권 사용자로 특권 명령 호출 */
    int ret = ioctl(fd, MYDEV_FLASH_FW, &fw_data);
    if (getuid() != 0)
        assert(ret == -1 && errno == EPERM);
}

/* 스트레스 테스트: 다중 스레드에서 ioctl 동시 호출 */
static void *stress_worker(void *arg)
{
    int fd = *(int *)arg;
    for (int i = 0; i < 10000; i++) {
        struct mydev_status st;
        ioctl(fd, MYDEV_GET_STATUS, &st);
    }
    return NULL;
}

32비트 호환 테스트

# 64비트 커널에서 32비트 바이너리로 ioctl 테스트
# 크로스 컴파일:
gcc -m32 -o test_ioctl_32 test_ioctl.c
./test_ioctl_32

# strace로 compat_ioctl 경로 확인
strace -e trace=ioctl ./test_ioctl_32 2>&1
# compat_ioctl이 올바르게 호출되는지 확인

# ioctl 구조체 크기가 32/64비트에서 동일한지 확인
# (C 코드에서 sizeof 비교)
gcc -m32 -o check32 -c check_sizeof.c
gcc -m64 -o check64 -c check_sizeof.c
# 출력이 동일하면 compat_ptr_ioctl로 충분

ioctl 퍼징(Fuzzing)

syzkaller는 ioctl을 포함한 시스템 콜 퍼징(Fuzzing)을 위한 대표적인 커널 퍼저입니다:

# syzkaller용 ioctl 디스크립션 예시
resource fd_mydev[fd]

openat$mydev(fd const[AT_FDCWD], file ptr[in, string["/dev/mydev"]],
             flags flags[open_flags]) fd_mydev

ioctl$MYDEV_RESET(fd fd_mydev, cmd const[MYDEV_RESET])
ioctl$MYDEV_GET_STATUS(fd fd_mydev, cmd const[MYDEV_GET_STATUS],
                       arg ptr[out, mydev_status])
ioctl$MYDEV_SET_CONFIG(fd fd_mydev, cmd const[MYDEV_SET_CONFIG],
                       arg ptr[in, mydev_config])

mydev_status {
    value   int32
    flags   int32
}

mydev_config {
    interval_ms int32[1:10000]
    threshold   int32[0:100000]
    flags       flags[mydev_config_flags]
    reserved    const[0, int32]
}

mydev_config_flags = MYDEV_FLAG_AUTO, MYDEV_FLAG_VERBOSE
syzkaller 참고: syzkaller는 커버리지 기반 커널 퍼저로, ioctl 인터페이스의 보안 결함을 자동으로 탐지합니다. ioctl 디스크립션을 작성하면 구조체 필드를 의미 있는 범위 내에서 무작위 변형하여 테스트합니다.

io_uring_cmd와 ioctl 통합

Linux 5.19+에서 도입된 io_uring_cmd를 통해 기존 ioctl 명령을 비동기로 실행할 수 있습니다. 드라이버 측에서 두 경로를 모두 지원하는 패턴입니다.

/* ioctl과 io_uring_cmd를 통합 지원하는 드라이버 */
static int mydev_do_cmd(struct mydev_priv *priv,
                       unsigned int cmd,
                       void __user *uarg,
                       bool async)
{
    /* 공통 명령 처리 로직 */
    switch (cmd) {
    case MYDEV_SUBMIT: {
        struct mydev_submit sub;
        if (copy_from_user(&sub, uarg, sizeof(sub)))
            return -EFAULT;
        return mydev_submit_cmd(priv, &sub, async);
    }
    default:
        return -ENOTTY;
    }
}

/* 전통적 ioctl 경로 (동기) */
static long mydev_ioctl(struct file *filp,
                        unsigned int cmd, unsigned long arg)
{
    return mydev_do_cmd(filp->private_data, cmd,
                        (void __user *)arg, 0);
}

/* io_uring passthrough 경로 (비동기) */
static int mydev_uring_cmd(struct io_uring_cmd *ioucmd,
                          unsigned int issue_flags)
{
    struct file *filp = ioucmd->file;
    unsigned int cmd = ioucmd->cmd_op;
    void __user *uarg = io_uring_sqe_cmd(ioucmd->sqe);

    return mydev_do_cmd(filp->private_data, cmd, uarg, 1);
}

static const struct file_operations mydev_fops = {
    .owner          = THIS_MODULE,
    .unlocked_ioctl = mydev_ioctl,
    .uring_cmd      = mydev_uring_cmd,     /* io_uring passthrough */
    .uring_cmd_iopoll = mydev_uring_poll,  /* 폴링 모드 (선택) */
};
io_uring_cmd 장점: 동일한 명령 처리 로직을 공유하면서, ioctl(동기)과 io_uring(비동기/배치)을 모두 지원할 수 있습니다. NVMe, UBLK 등이 이 패턴을 사용합니다. 자세한 내용은 io_uring 문서를 참고하세요.

고급 디버깅 기법

ioctl 관련 문제를 추적하기 위한 고급 기법을 소개합니다.

ftrace로 ioctl 핸들러 추적

# 1. 특정 ioctl 핸들러의 함수 호출 그래프
cd /sys/kernel/debug/tracing
echo function_graph > current_tracer
echo mydev_ioctl > set_graph_function
echo 1 > tracing_on

# 사용자 공간에서 ioctl 호출
./test_mydev

# 결과 확인
cat trace
#  1)               |  mydev_ioctl() {
#  1)   0.245 us    |    _copy_from_user();
#  1)               |    mydev_apply_config() {
#  1)   1.032 us    |      mutex_lock();
#  1)   0.150 us    |      mydev_hw_write();
#  1)   0.098 us    |      mutex_unlock();
#  1)   2.540 us    |    }
#  1)   3.890 us    |  }

echo 0 > tracing_on

strace 고급 활용

# ioctl 호출만 필터링, 16진수 값 포함
strace -e trace=ioctl -v -x ./my_app 2>&1

# 특정 fd의 ioctl만 필터 (fd=3)
strace -e trace=ioctl -e inject=ioctl:when=1..5 ./my_app

# 타임스탬프와 소요 시간 포함
strace -e trace=ioctl -r -T ./my_app

# ioctl 에러 원인 분석 (errno 포함)
strace -e trace=ioctl -Z ./my_app 2>&1 | head -20
# 출력 예시:
# ioctl(3, _IOC(_IOC_WRITE, 0x4d, 0x02, 0x10), 0x7ffd..) = -1 EPERM (Operation not permitted)

# perf를 이용한 ioctl 지연 시간 측정
perf trace -e ioctl --duration 100 ./my_app

kprobe를 이용한 동적 추적

# mydev_ioctl 진입 시 cmd 인자 로깅
cd /sys/kernel/debug/tracing
echo 'p:myioctl mydev_ioctl cmd=%si:x32' > kprobe_events
echo 1 > events/kprobes/myioctl/enable
echo 1 > tracing_on

# 테스트 후 확인
cat trace_pipe
# test_app-1234  [001] ....  1234.567: myioctl: (mydev_ioctl+0x0/0x1a0) cmd=0x40104d02

# 정리
echo 0 > events/kprobes/myioctl/enable
echo '-:myioctl' > kprobe_events

ioctl 에러 코드 참조

ioctl에서 반환하는 주요 에러 코드와 올바른 사용 시나리오를 정리합니다:

에러 코드올바른 사용 시나리오흔한 실수
-ENOTTY25 지원하지 않는 ioctl 명령 -EINVAL을 반환하는 것 (올바르지 않음)
-ENOIOCTLCMD515 내부 전용: VFS에게 "이 드라이버는 처리 안 함"을 알림 사용자 공간에 직접 반환 (VFS가 -ENOTTY로 변환)
-EFAULT14 copy_from/to_user 실패 포인터 검증 없이 직접 접근 후 커널 패닉
-EINVAL22 유효하지 않은 인자 값 (범위 초과, 잘못된 플래그) 지원하지 않는 cmd에 사용 (→ -ENOTTY가 올바름)
-EPERM1 Capability 부족 (capable() 검사 실패) -EACCES와 혼동
-EBUSY16 디바이스가 다른 작업 중 동시 접근 제어 미흡
-ERESTARTSYS512 시그널에 의한 중단 (시스템 콜 자동 재시작) 인터럽트 불가능 대기에서 사용
ioctl 에러 코드 의사결정 흐름 ioctl 핸들러 진입 cmd 지원 여부? No -ENOTTY Yes 권한(Capability) 충분? No -EPERM Yes copy_from_user 성공? No -EFAULT Yes 입력 값 유효? No -EINVAL Yes 명령 실행 return 0 에러 -EBUSY (사용 중) -EIO (하드웨어 오류) -ENOMEM (메모리 부족)
ioctl 핸들러에서 에러 코드를 선택하는 의사결정 흐름도. 각 단계에서 적절한 errno를 반환합니다.

ioctl 커널 버전별 주요 변화

커널 버전변경사항영향
2.6.11 unlocked_ioctl 도입 BKL 없이 ioctl 호출 가능
2.6.36 .ioctl 필드 제거 unlocked_ioctl이 유일한 경로
3.0 compat_ptr_ioctl 헬퍼 도입 단순 구조체의 32/64비트 호환 간소화
5.5 copy_struct_from_user() 도입 확장 가능 구조체의 안전한 복사 (미래 필드 자동 제로화)
5.6 compat_ptr_ioctl 기본 할당 확대 많은 드라이버에서 별도 compat_ioctl 불필요
5.19 io_uring_cmd (uring passthrough) 도입 ioctl과 동일한 명령을 비동기로 실행 가능
6.2 copy_struct_from_user() 사용 확대 io_uring, perf_event 등에서 확장 가능 ioctl 패턴 표준화

copy_struct_from_user() — 확장 가능 구조체 복사

커널 5.5+에서 도입된 copy_struct_from_user()는 구조체 버전 차이를 자동 처리합니다:

/* 확장 가능 구조체 복사 — 구 커널 사용자/신 커널, 신 커널 사용자/구 커널 모두 처리 */
struct mydev_cmd_v2 cmd = {};  /* 먼저 0으로 초기화 */

/* ksize: 커널이 아는 구조체 크기
 * usize: 사용자 공간이 전달한 크기 (cmd.size 필드)
 *
 * usize < ksize → 새 필드를 0으로 유지 (하위 호환)
 * usize > ksize → 초과 부분이 0인지 확인 (상위 호환)
 * usize == ksize → 그대로 복사
 */
int err = copy_struct_from_user(&cmd, sizeof(cmd),
                                uarg, usize);
if (err)
    return err;

자주 묻는 질문 (FAQ)

Q: ioctl 반환값으로 양수를 사용해도 되나요?

네. ioctl()의 반환값은 관례상 성공 시 0이지만, KVM의 KVM_GET_API_VERSION처럼 양수를 반환하는 것도 허용됩니다. 단, glibc 래퍼가 -4095 ~ -1 범위를 에러로 해석하므로, 반환값이 이 범위와 겹치지 않도록 주의하세요. 파일 디스크립터를 반환하는 ioctl(KVM_CREATE_VM)도 흔한 패턴입니다.

Q: unlocked_ioctl에서 동기화는 어떻게 하나요?

BKL이 제거되었으므로 드라이버가 직접 동기화를 관리해야 합니다. 일반적인 패턴:

  • 디바이스별 mutex로 직렬화(Serialization)
  • 읽기 전용 쿼리는 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 자체는 동시 호출을 허용합니다. 안전성은 전적으로 드라이버 구현에 달려 있습니다.

  • 대부분의 드라이버는 뮤텍스나 스핀락(Spinlock)으로 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을 사용하세요.

참고자료

커널 공식 문서

커널 소스 코드

외부 자료

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