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만 구현하는 경우도 있다.
GPU 드라이버와 상호작용하려면 ioctl을 반드시 사용하게 된다. 특히, 리눅스에서는 /dev/dri/* 또는 /dev/nvidia* 같은 디바이스 파일을 통해 GPU 드라이버와 통신하고, 그 내부는 대부분 ioctl 기반의 제어 명령으로 작동한다.
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 */ // 헤더가드 종료
#define _IOC(dir,type,nr,size) ...
32비트 ioctl 명령 번호는 다음과 같이 구성된다.
| 필드 이름 | 비트 수 | 의미 |
|---|---|---|
| dir | 2 bits | 데이터 방향(읽기/쓰기/없음) |
| type | 8 bits | 디바이스 종류(주로 문자 하나로 표현됨) |
| nr | 8 bits | 명령 번호(같은 디바이스 타입 안에서 유일) |
| size | 14 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 번호에서 정보를 추출할 수 있는 매크로.
#define _IOC_DIR(nr)
#define _IOC_TYPE(nr)
#define _IOC_NR(nr)
#define _IOC_SIZE(nr)
→ 커널은 ioctl 명령 번호를 이 매크로들로 디코딩하여 요청을 처리함
[dependencies]
libc = "0.2"
_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);
// 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이라는 전화번호가 있다는 걸 내가 알고 있으니, 나중에 전화 걸게!" 라고 선언한 것과 같다.
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),
}
}
v4l2 (웹캠), loop 장치 등의 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);
}
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는 일부 ioctl 매크로를 제공한다. 그러나 모든 디바이스별 ioctl은 제공되지 않으므로 직접 매크로를 정의하는 게 일반적이다.
[dependencies]
nix = "0.27"