ioctl

손호준·2025년 5월 15일

ioctl은 I/O control의 줄임말로, 리눅스(유닉스 계열) 운영체제에서 디바이스 드라이버나 커널과 사용자 공간 프로그램이 상호작용하기 위한 시스템 콜이다.
일반적인 read/write 호출로는 처리하기 어려운 디바이스 제어 작업을 요청할 때 사용된다.

보통 파일이나 디바이스(/dev/sda, /dev/input/event0 등)는 open, read, write로 데이터를 주고받을 수 있지만,

  • 디바이스 상태 확인
  • 디바이스에 명령 보내기 (예: 화면 해상도 변경)
  • 특수한 설정값 지정

이런 동작은 일반 read/write로는 처리하기 어렵기 때문에 ioctl을 사용한다.

일반적인 read/write동작은 read() write()시스템 콜로 수행되고,
ioctl()은 read/write으로 할 수 없는 제어 명령에만 사용된다. 하지만 예외적으로 일부 특수 디바이스(카메라, 특수 센서 장치, fpga장치)들은 read/write 대신 ioctl만 구현하는 경우도 있다.

  • ioctl로도 데이터를 읽고 쓸 수는 있다 (구조체 기반)
  • 그러나 범용적인 데이터 I/O 수단은 아니다
  • read/write를 대체하지 않는다, 보완적일 뿐이다

구체적으로 ioctl은 어디에 쓰이는가?

  • 특수 장치 (/dev/loopX, /dev/tty, /dev/snd, GPU 드라이버 등)
  • 설정 변경, 상태 확인, 고급 명령 전송
  • 예: ioctl(fd, LOOP_CTL_GET_FREE) — 루프 디바이스 중 빈 번호 찾기

GPU 드라이버와 상호작용하려면 ioctl을 반드시 사용하게 된다. 특히, 리눅스에서는 /dev/dri/* 또는 /dev/nvidia* 같은 디바이스 파일을 통해 GPU 드라이버와 통신하고, 그 내부는 대부분 ioctl 기반의 제어 명령으로 작동한다.

  • GPU 메모리 할당
  • 커널-유저 공간 간 버퍼 공유
  • 커맨드 버퍼 제출 (Command Submission)
  • DMA 설정
  • 커널 모드 드라이버(KMD)와 유저 모드 드라이버(UMD) 간 통신

ioctl.h

ioctl.h는 ioctl 시스템 호출에서 사용하는 명령 번호(command code)를 만들고 해석하기 위한 매크로와 상수들을 정의한 헤더이다. 리눅스 깃헙 레포지토리linux/include/uapi/asm-generic/ioctl.h 에서 확인할 수 있다.

/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _UAPI_ASM_GENERIC_IOCTL_H // #ifndef- #define 헤더가드: 중복 포함 방지
#define _UAPI_ASM_GENERIC_IOCTL_H

/* ioctl command encoding: 32 bits total, command in lower 16 bits,
 * size of the parameter structure in the lower 14 bits of the
 * upper 16 bits.
 * Encoding the size of the parameter structure in the ioctl request
 * is useful for catching programs compiled with old versions
 * and to avoid overwriting user space outside the user buffer area.
 * The highest 2 bits are reserved for indicating the ``access mode''.
 * NOTE: This limits the max parameter size to 16kB -1 !
 */

/*
 * The following is for compatibility across the various Linux
 * platforms.  The generic ioctl numbering scheme doesn't really enforce
 * a type field.  De facto, however, the top 8 bits of the lower 16
 * bits are indeed used as a type field, so we might just as well make
 * this explicit here.  Please be sure to use the decoding macros
 * below from now on.
 */
#define _IOC_NRBITS	8 // 커맨드 번호 (nr)는 8비트
#define _IOC_TYPEBITS	8 // 타입 정보 (type)도 8비트

/*
 * Let any architecture override either of the following before
 * including this file.
 */

#ifndef _IOC_SIZEBITS
# define _IOC_SIZEBITS	14 // 구조체 크기는 최대 14비트 → 약 16KB까지 표현 가능
#endif

#ifndef _IOC_DIRBITS
# define _IOC_DIRBITS	2 // 방향 정보 (읽기, 쓰기)는 2비트
#endif

// 명령어 번호에서 각 비트 필드를 정확히 분리하고 조합하기 위해 비트 마스크와 시프트 연산자를 사용한다. 덕분에 하나의 정수(ioctl 번호)로 여러 정보를 안전하게 인코딩할 수 있다.

// 마스크 정의 (비트를 자를 때 씀)
// 각 비트 필드를 추출할 때 사용하는 마스크들
#define _IOC_NRMASK	((1 << _IOC_NRBITS)-1)
#define _IOC_TYPEMASK	((1 << _IOC_TYPEBITS)-1)
#define _IOC_SIZEMASK	((1 << _IOC_SIZEBITS)-1)
#define _IOC_DIRMASK	((1 << _IOC_DIRBITS)-1)

// 시프트 (비트를 밀어서 위치를 정함)
// 명령어, 타입, 크기, 방향 정보들이 ioctl 번호 내에서 어디에 위치하는지를 정의
#define _IOC_NRSHIFT	0
#define _IOC_TYPESHIFT	(_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT	(_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT	(_IOC_SIZESHIFT+_IOC_SIZEBITS)

/*
 * Direction bits, which any architecture can choose to override
 * before including this file.
 *
 * NOTE: _IOC_WRITE means userland is writing and kernel is
 * reading. _IOC_READ means userland is reading and kernel is writing.
 */

//방향 정보 정의
#ifndef _IOC_NONE //NONE: 데이터가 오가지 않음
# define _IOC_NONE	0U
#endif

#ifndef _IOC_WRITE //WRITE: 사용자 공간이 커널로 데이터를 전달함
# define _IOC_WRITE	1U
#endif

#ifndef _IOC_READ //READ: 커널이 사용자 공간으로 데이터를 전달함
# define _IOC_READ	2U
#endif

// 메인 매크로
// ioctl 명령 번호 생성
#define _IOC(dir,type,nr,size) \
	(((dir)  << _IOC_DIRSHIFT) | \
	 ((type) << _IOC_TYPESHIFT) | \
	 ((nr)   << _IOC_NRSHIFT) | \
	 ((size) << _IOC_SIZESHIFT))

#ifndef __KERNEL__
#define _IOC_TYPECHECK(t) (sizeof(t))
#endif

//간편 매크로
/*
 * Used to create numbers.
 *
 * NOTE: _IOW means userland is writing and kernel is reading. _IOR
 * means userland is reading and kernel is writing.
 */
#define _IO(type,nr)			_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,argtype)		_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(argtype)))
#define _IOW(type,nr,argtype)		_IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(argtype)))
#define _IOWR(type,nr,argtype)		_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(argtype)))
#define _IOR_BAD(type,nr,argtype)	_IOC(_IOC_READ,(type),(nr),sizeof(argtype))
#define _IOW_BAD(type,nr,argtype)	_IOC(_IOC_WRITE,(type),(nr),sizeof(argtype))
#define _IOWR_BAD(type,nr,argtype)	_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(argtype))

// 명령어 해석용 매크로
/* used to decode ioctl numbers.. */
#define _IOC_DIR(nr)		(((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)		(((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)		(((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)		(((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

/* ...and for the drivers/sound files... */

#define IOC_IN		(_IOC_WRITE << _IOC_DIRSHIFT)
#define IOC_OUT		(_IOC_READ << _IOC_DIRSHIFT)
#define IOC_INOUT	((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
#define IOCSIZE_MASK	(_IOC_SIZEMASK << _IOC_SIZESHIFT)
#define IOCSIZE_SHIFT	(_IOC_SIZESHIFT)

#endif /* _UAPI_ASM_GENERIC_IOCTL_H */ // 헤더가드 종료

ioctl 숫자 구성

#define _IOC(dir,type,nr,size) ...

32비트 ioctl 명령 번호는 다음과 같이 구성된다.

필드 이름비트 수의미
dir2 bits데이터 방향(읽기/쓰기/없음)
type8 bits디바이스 종류(주로 문자 하나로 표현됨)
nr8 bits명령 번호(같은 디바이스 타입 안에서 유일)
size14 bits인자로 쓰이는 구조체의 크기(최대 16kb-1)

예시

_IOC(_IOC_READ, 'T', 123, sizeof(struct my_cmd))

디렉션 상수

#define _IOC_NONE  0U
#define _IOC_WRITE 1U
#define _IOC_READ  2U
의미
_IOC_NONE인자 없음
_IOC_WRITE사용자->커널
_IOC_READ커널->사용자

_IOC_READ | _IOC_WRITE 를 함께 쓰면 양방향

매크로 설명

  • _IO(type, nr) → 인자 없음
  • _IOR(type, nr, argtype) → Read (커널이 사용자에게 씀)
  • _IOW(type, nr, argtype) → Write (사용자가 커널에게 전달)
  • _IOWR(type, nr, argtype) → 양방향 (주고받기)
  • _IOC(...) → 위 매크로들의 내부 구현
  • _IOC_TYPECHECK(t) → 크기 구할 때 타입 확인용

예시

#define MY_IOCTL _IOW('M', 1, struct my_data)

-> 디바이스 타입 'M', 명령 번호 1, 사용자 → 커널 방향, 인자는 struct my_data

ioctl 번호 파싱 매크로

반대로 ioctl 번호에서 정보를 추출할 수 있는 매크로.

#define _IOC_DIR(nr)
#define _IOC_TYPE(nr)
#define _IOC_NR(nr)
#define _IOC_SIZE(nr)

→ 커널은 ioctl 명령 번호를 이 매크로들로 디코딩하여 요청을 처리함

ioctl은 왜 중요한가?

  • 디바이스 드라이버 개발 시 필수다.
  • 유저 프로그램에서 ioctl 호출 시 이 매크로들로 명령어를 정의한다.
  • 호환성과 안전성을 확보하기 위해 size, direction, type을 명확히 구분한다.
  • Rust에서 nix::ioctl! 같은 매크로도 이 구조를 기반으로 한다.

rust에서 ioctl 구현

방법1. _IOC 구조화

1. libc 추가

[dependencies]
libc = "0.2"

2. ioctl 상수 정의

_IOR, _IOW, _IOWR 등의 매크로는 C에서 제공되지만, Rust에서는 직접 계산해줘야 한다. 예시로 다음과 같은 커맨드를 정의한다.

// src/ioctl.rs
use libc::{c_int, c_ulong};

// 커널 ioctl 번호 계산 방식에 맞춰 매크로 동작을 수동으로 구현
const IOC_NRBITS: u8 = 8;
const IOC_TYPEBITS: u8 = 8;
const IOC_SIZEBITS: u8 = 14;
const IOC_DIRBITS: u8 = 2;

const IOC_NRSHIFT: u8 = 0;
const IOC_TYPESHIFT: u8 = IOC_NRSHIFT + IOC_NRBITS;
const IOC_SIZESHIFT: u8 = IOC_TYPESHIFT + IOC_TYPEBITS;
const IOC_DIRSHIFT: u8 = IOC_SIZESHIFT + IOC_SIZEBITS;

const IOC_NONE: u8 = 0;
const IOC_WRITE: u8 = 1;
const IOC_READ: u8 = 2;

// IOC 매크로 계산 (명령 번호 생성)
const fn _IOC(dir: u8, type_: u8, nr: u8, size: u16) -> c_ulong {
    ((dir as c_ulong) << IOC_DIRSHIFT)
        | ((type_ as c_ulong) << IOC_TYPESHIFT)
        | ((nr as c_ulong) << IOC_NRSHIFT)
        | ((size as c_ulong) << IOC_SIZESHIFT)
}

// 실제 IOCTL 명령 정의 예시
pub const MY_IOCTL_CMD: c_ulong = _IOC(IOC_READ | IOC_WRITE, b'M', 1, std::mem::size_of::<u32>() as u16);

3. ioctl 실행 함수 구현

// src/device.rs (또는 별도 ioctl.rs)
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
use libc::{ioctl, c_ulong};
use crate::ioctl::MY_IOCTL_CMD;

pub fn call_my_ioctl(device_path: &str) -> std::io::Result<u32> {
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(device_path)?;

    let mut value: u32 = 42;

    unsafe {
        let ret = ioctl(file.as_raw_fd(), MY_IOCTL_CMD, &mut value);
        if ret < 0 {
            return Err(std::io::Error::last_os_error());
        }
    }

    Ok(value)
}

여기서 libc::ioctl api를 살펴보면 아래와 같은데, extern "C"는 C ABI(Application Binary Interface)를 따르는 외부 함수를 선언함을 의미한다. Rust는 이 선언을 통해 나중에 libc.so 또는 링커에서 이 심볼(ioctl)을 찾아 호출할 수 있다. extern 블록 안에서는 시그니처만 알려주고 이 함수의 실제 구현은 커널이나 C 표준 라이브러리(glibc) 쪽에 있다.

extern "C" {
    #[cfg_attr(
        not(any(target_env = "musl", target_env = "ohos")),
        link_name = "__xpg_strerror_r"
    )]
    //...
    pub fn ioctl(fd: c_int, request: Ioctl, ...) -> c_int;
   //...
}

Rust에서 "C에 ioctl이라는 전화번호가 있다는 걸 내가 알고 있으니, 나중에 전화 걸게!" 라고 선언한 것과 같다.

4. main.rs에서 호출

mod device;
mod ioctl;

fn main() {
    match device::call_my_ioctl("/dev/mydevice") {
        Ok(val) => println!("ioctl result: {}", val),
        Err(e) => eprintln!("ioctl failed: {}", e),
    }
}

주의

  • 실제 동작을 하려면 /dev/mydevice가 존재하고, 해당 ioctl 번호를 이해하는 커널 드라이버가 있어야 함.
  • 테스트용으로 fuse, loop, null, random 같은 기본 디바이스로도 ioctl 테스트 가능하긴 하나 제한적임

커널 드라이버 없이 테스트하려면?

v4l2 (웹캠), loop 장치 등의 ioctl을 조회/호출해볼 수 있음

  • 예: /dev/loop-control에서 LOOP_CTL_GET_FREE 호출

방법2. ior! 매크로 사용

1. Rust 코드 예시 (C ioctl 대응)

C 코드 예:

#define MY_IOCTL_GET _IOR('M', 1, int)
int value;
ioctl(fd, MY_IOCTL_GET, &value);

Rust 버전 예:

use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::mem;
use libc::ioctl;

const MY_IOCTL_GET: u64 = ior!(b'M', 1, i32);

fn main() {
    let file = File::open("/dev/mydevice").unwrap();
    let fd = file.as_raw_fd();

    let mut value: i32 = 0;
    unsafe {
        ioctl(fd, MY_IOCTL_GET, &mut value);
    }
    println!("Got value: {}", value);
}

2. ior! 매크로 정의

Rust에서는 C 헤더처럼 _IOR 매크로가 없기 때문에 직접 구현해야 함:

macro_rules! ior {
    ($ty:expr, $nr:expr, $t:ty) => {
        ((libc::_IOC_READ as u64) << libc::_IOC_DIRSHIFT) |
        (($ty as u64) << libc::_IOC_TYPESHIFT) |
        (($nr as u64) << libc::_IOC_NRSHIFT) |
        ((std::mem::size_of::<$t>() as u64) << libc::_IOC_SIZESHIFT)
    };
}
// 이 매크로는 linux/ioctl.h의 _IOR 매크로와 동일하게 작동함

대체 방법: nix crate

nix crate는 일부 ioctl 매크로를 제공한다. 그러나 모든 디바이스별 ioctl은 제공되지 않으므로 직접 매크로를 정의하는 게 일반적이다.

[dependencies]
nix = "0.27"
profile
Rustacean🦀

0개의 댓글