Device File và ioctl¶
Device file là node trong /dev mà ứng dụng mở như một file bình thường, nhưng mọi thao tác trên nó (read, write, ioctl, mmap, poll) được chuyển tới callback của driver. Đây là interface phổ biến nhất giữa ứng dụng và driver thiết bị.
Trang này tập trung vào phần interface: các callback trong file_operations, ý nghĩa giá trị trả về, cách thiết kế lệnh ioctl và header UAPI. Phần đăng ký device number và struct cdev đã có ở Character Device Driver.
1. Từ system call đến driver¶
Khi ứng dụng gọi open("/dev/vux_demo", O_RDWR):
- VFS tra đường dẫn và tìm thấy inode của
/dev/vux_demo. Inode này có kiểuS_IFCHRvà mang device number<major, minor>tronginode->i_rdev. - Với mọi character device,
inode->i_foptrỏ tớidef_chr_fops, có callbackopenlàchrdev_open(). chrdev_open()dùngi_rdevđể tìmstruct cdevđã đăng ký, thayfile->f_opbằngcdev->opscủa driver, rồi gọiopencủa driver nếu có.- Kernel trả về một file descriptor. Từ đây,
read(fd, ...)đi thẳng tớifile->f_op->readcủa driver mà không cần tra cứu lại.
Mỗi lần open() tạo ra một struct file riêng. Driver dùng trường file->private_data để gắn trạng thái riêng cho từng lần mở (ví dụ con trỏ tới instance của thiết bị), và file->f_pos lưu vị trí đọc/ghi hiện tại.
2. struct file_operations¶
Khai báo trong <linux/fs.h>. Driver chỉ cần điền các callback mình hỗ trợ; callback để NULL sẽ có hành vi mặc định (thường là trả về lỗi).
| Callback | System call tương ứng | Ghi chú |
|---|---|---|
open(inode, file) |
open() |
Khởi tạo trạng thái cho lần mở, gán file->private_data |
release(inode, file) |
close() cuối cùng |
Chỉ gọi khi tham chiếu cuối cùng tới struct file được đóng (sau dup(), fork()) |
read(file, buf, count, ppos) |
read() |
buf là con trỏ __user |
write(file, buf, count, ppos) |
write() |
|
llseek(file, offset, whence) |
lseek() |
default_llseek cho thiết bị có offset; noop_llseek nếu offset không có ý nghĩa |
poll(file, wait) |
poll(), select(), epoll |
Xem Thông báo bất đồng bộ |
unlocked_ioctl(file, cmd, arg) |
ioctl() |
Xem mục 3 |
compat_ioctl(file, cmd, arg) |
ioctl() từ tiến trình 32-bit trên kernel 64-bit |
Xem mục 3.6 |
mmap(file, vma) |
mmap() |
Xem Chia sẻ bộ nhớ |
2.1. Quy ước giá trị trả về của read và write¶
Ứng dụng dựa vào các quy ước này để biết khi nào dừng đọc, khi nào thử lại, nên driver phải tuân thủ đúng:
| Trả về | Ý nghĩa với ứng dụng |
|---|---|
> 0 |
Số byte đã đọc/ghi. Có thể nhỏ hơn count; ứng dụng sẽ gọi tiếp |
0 khi read |
End of file. cat dừng ở đây |
-EAGAIN |
Chưa có dữ liệu và file được mở với O_NONBLOCK |
-EINTR / -ERESTARTSYS |
Bị signal ngắt trong lúc chờ |
-EFAULT |
Con trỏ buf không hợp lệ |
-ENOSPC |
Hết chỗ khi write |
Driver phải tự cập nhật *ppos sau khi đọc/ghi. Nếu read luôn trả về dữ liệu mà không bao giờ trả về 0, lệnh cat /dev/xxx sẽ chạy vô hạn.
3. ioctl¶
3.1. Khi nào cần ioctl¶
read và write chỉ truyền một luồng byte. Nhiều thao tác với thiết bị không phải là luồng byte: đổi baud rate, lấy thống kê, reset phần cứng, cấu hình nhiều tham số cùng lúc. ioctl (input/output control) cung cấp một kênh lệnh riêng cho các thao tác như vậy:
/* user space */
int ioctl(int fd, unsigned long request, ...);
/* kernel, trong struct file_operations */
long (*unlocked_ioctl)(struct file *file, unsigned int cmd, unsigned long arg);
cmd là mã lệnh, arg thường là một con trỏ user trỏ tới struct tham số, được truyền dưới dạng unsigned long.
Vì sao tên là unlocked_ioctl
Callback cũ .ioctl được kernel gọi trong khi giữ Big Kernel Lock. unlocked_ioctl (từ 2.6.11) không giữ khóa nào, và .ioctl đã bị xóa hẳn ở 2.6.36. Driver phải tự lo đồng bộ, ví dụ bằng mutex.
3.2. Cấu trúc mã lệnh¶
Mã lệnh là một số 32 bit chia thành bốn trường. Với asm-generic, dùng cho x86, arm, arm64, riscv:
| Bit | Trường | Ý nghĩa |
|---|---|---|
| 31–30 | dir |
Chiều dữ liệu: _IOC_NONE, _IOC_WRITE, _IOC_READ |
| 29–16 | size |
Kích thước tham số, 14 bit |
| 15–8 | type |
Magic number, xác định driver hoặc subsystem |
| 7–0 | nr |
Số thứ tự lệnh trong driver |
PowerPC, MIPS, SPARC và Alpha dùng 3 bit cho dir và 13 bit cho size, nên không bao giờ hardcode giá trị số của mã lệnh; luôn tạo bằng macro.
Các macro trong <linux/ioctl.h>:
| Macro | Dữ liệu | Ví dụ |
|---|---|---|
_IO(type, nr) |
Không có tham số | RESET |
_IOR(type, nr, T) |
Kernel ghi ra, user đọc | GET_CONFIG |
_IOW(type, nr, T) |
User ghi vào, kernel đọc | SET_CONFIG |
_IOWR(type, nr, T) |
Hai chiều | Gửi yêu cầu và nhận kết quả |
Hai lỗi thường gặp:
- Chiều dữ liệu tính theo góc nhìn của user, giống
read()/write(). LệnhSET_*là_IOWdù kernel thực tế là bên đọc dữ liệu. - Tham số thứ ba là kiểu dữ liệu, không phải
sizeof. Viết_IOW('x', 1, sizeof(struct foo))sẽ mã hóa kích thước củasize_tthay cho kích thước củastruct foo.
Ví dụ giải mã lệnh VUX_DEMO_GET_CONFIG trong driver mẫu ở mục 6, có giá trị 0x8008d501:
0x8008d501 = 10 | 00000000001000 | 11010101 | 00000001
│ │ │ └─ nr = 0x01
│ │ └──────────── type = 0xD5
│ └─────────────────────────── size = 8 (sizeof(struct vux_demo_config))
└───────────────────────────────────── dir = _IOC_READ
Trong kernel, có thể tách các trường bằng _IOC_DIR(cmd), _IOC_TYPE(cmd), _IOC_NR(cmd), _IOC_SIZE(cmd).
3.3. Chọn magic number¶
Magic number nên là duy nhất trên toàn hệ thống. Khi ứng dụng gửi nhầm lệnh tới một thiết bị khác, nó sẽ nhận lỗi thay vì kích hoạt một thao tác bất ngờ. Bảng đăng ký nằm ở Documentation/userspace-api/ioctl/ioctl-number.rst. Driver muốn đưa vào mainline nên chọn một khối còn trống, đủ cho 32–256 lệnh, và gửi patch cập nhật bảng này.
Driver mẫu dùng 0xD5, chưa có trong bảng tại thời điểm viết (kernel mainline 10/2026). Đây chỉ là ví dụ cho driver out-of-tree.
3.4. Viết hàm xử lý¶
Khung chuẩn của một hàm unlocked_ioctl:
static long foo_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
void __user *argp = (void __user *)arg;
struct foo_config cfg;
switch (cmd) {
case FOO_SET_CONFIG:
if (copy_from_user(&cfg, argp, sizeof(cfg)))
return -EFAULT;
if (cfg.mode > FOO_MODE_MAX || cfg.reserved) /* kiểm tra trên bản sao */
return -EINVAL;
/* ... áp dụng cfg dưới mutex ... */
return 0;
default:
return -ENOTTY;
}
}
Các nguyên tắc:
- Lệnh không hỗ trợ trả về
-ENOTTY, không phải-EINVAL.-ENOTTY("Inappropriate ioctl for device") cho ứng dụng và các lớp trung gian biết rằng thiết bị không hiểu lệnh này.-EINVALdành cho lệnh hợp lệ nhưng tham số sai. - Copy một lần rồi kiểm tra trên bản sao. Xem mục 2.2 trang tổng quan.
- Yêu cầu trường
reservedbằng 0. Nhờ vậy, phiên bản sau có thể gán ý nghĩa cho trường đó mà ứng dụng cũ (luôn gửi 0) vẫn chạy đúng. - Kiểm tra quyền cho thao tác nguy hiểm. Quyền mở file chỉ là lớp bảo vệ đầu tiên; thao tác như ghi firmware nên kiểm tra thêm
capable(CAP_SYS_ADMIN)hoặc một capability phù hợp hơn. - Không giữ spinlock khi gọi
copy_*_user(), vì hàm này có thể ngủ. Copy ra biến cục bộ trước, rồi mới khóa để cập nhật trạng thái.
3.5. Thiết kế struct tham số¶
Struct tham số là một phần của ABI và phải có cùng bố cục bộ nhớ trên mọi kiến trúc, mọi compiler, và giữa tiến trình 32-bit với kernel 64-bit:
- Dùng kiểu có kích thước cố định từ
<linux/types.h>:__u8,__u16,__u32,__u64(và các kiểu__s*). Không dùngint,long,size_t,bool,enum. - Không đặt con trỏ trong struct. Nếu cần truyền địa chỉ buffer, dùng
__u64và chuyển lại trong kernel bằngu64_to_user_ptr(). - Tránh padding ngầm. Trên i386,
__u64chỉ căn chỉnh 4 byte, nênstruct { __u32 a; __u64 b; }dài 12 byte trên i386 nhưng 16 byte trên x86_64. Kích thước khác nhau làm mã lệnh khác nhau. Hãy thêm trường padding tường minh, hoặc sắp xếp các trường lớn lên trước. Công cụpaholein ra bố cục thực tế của struct, kể cả các lỗ padding, nên dùng để kiểm tra.
3.6. compat_ioctl¶
Tiến trình 32-bit chạy trên kernel 64-bit gọi compat_ioctl thay cho unlocked_ioctl. Có hai trường hợp:
- Struct có cùng bố cục trên 32 và 64 bit (đã tuân thủ mục 3.5): chỉ cần chuyển
argtừ địa chỉ 32 bit sang con trỏ 64 bit. Kernel cung cấp sẵn hàmcompat_ptr_ioctl()(từ 5.5) làm đúng việc này, chỉ cần khai báo.compat_ioctl = compat_ptr_ioctl. - Struct có bố cục khác (thường là lỗi thiết kế từ trước, có
longhoặc con trỏ): phải viết hàm compat riêng, định nghĩa mộtstruct foo_compatvà chuyển đổi từng trường.
Nếu bỏ trống compat_ioctl, mọi ioctl từ tiến trình 32-bit sẽ thất bại với -ENOTTY.
3.7. Khi nào không nên dùng ioctl¶
ioctl dễ thêm nhưng cũng dễ bị lạm dụng, vì mỗi lệnh là một ABI riêng do driver tự định nghĩa, không có cấu trúc chung và khó dùng từ shell. Trước khi thêm một lệnh, hãy cân nhắc các lựa chọn khác:
- Đọc/ghi một giá trị → sysfs, dùng được bằng
cat/echo. - Cần kernel chủ động gửi sự kiện hoặc message có nhiều trường tùy chọn → netlink.
- Đã có subsystem chuẩn cho loại thiết bị (IIO, input, V4L2, GPIO character device, SPI/I2C dev...) → dùng ioctl và interface của subsystem đó, không tự tạo mới.
4. misc device¶
Đăng ký một character device đầy đủ cần nhiều bước: alloc_chrdev_region(), cdev_init(), cdev_add(), class_create(), device_create(). Với driver chỉ cần một node trong /dev, misc device gói tất cả các bước đó:
#include <linux/miscdevice.h>
static struct miscdevice foo_misc = {
.minor = MISC_DYNAMIC_MINOR, /* kernel tự cấp minor */
.name = "foo", /* tạo /dev/foo */
.fops = &foo_fops,
.mode = 0600, /* quyền của node, tùy chọn */
};
/* Trong probe/init và remove/exit: */
misc_register(&foo_misc);
misc_deregister(&foo_misc);
/* Hoặc, nếu module chỉ làm mỗi việc này: */
module_misc_device(foo_misc);
Mọi misc device dùng chung major 10 (MISC_MAJOR) và được phân biệt bằng minor. Danh sách misc device đang có xem ở /proc/misc. Node trong /dev được devtmpfs tạo tự động, udev có thể đổi quyền hoặc tạo symlink theo rule.
Khi open(), misc core gán file->private_data trỏ tới struct miscdevice của driver. Driver hỗ trợ nhiều instance có thể nhúng struct miscdevice vào struct riêng và lấy lại bằng container_of().
5. Header UAPI¶
Ứng dụng và driver phải dùng cùng một định nghĩa cho mã lệnh và struct tham số. Định nghĩa này đặt trong một header UAPI riêng:
- Driver in-tree:
include/uapi/linux/<tên>.h, được cài ra user space quamake headers_install. - Driver out-of-tree: một header dùng chung, include ở cả driver lẫn ứng dụng.
Quy tắc cho header UAPI:
- Dòng license
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */, để ứng dụng không phải GPL vẫn include được. - Chỉ include các header UAPI khác (
<linux/types.h>,<linux/ioctl.h>), không include header nội bộ của kernel. - Không dùng kiểu và macro chỉ có trong kernel (
u32,BIT(),__user). - Sau khi phát hành, không được đổi bố cục struct hoặc giá trị mã lệnh đã có. Muốn mở rộng thì thêm lệnh mới hoặc dùng trường
reserved. Xem ABI, bảo mật và độ ổn định.
6. Ví dụ đầy đủ: vux_demo¶
Driver vux_demo tạo misc device /dev/vux_demo chứa một buffer 4 KiB:
writeghi dữ liệu vào buffer,readđọc ra; hỗ trợlseek.VUX_DEMO_SET_CONFIGbật chế độ chuyển chữ hoa khi ghi.VUX_DEMO_GET_STATStrả về số byte đã đọc/ghi.VUX_DEMO_RESETxóa buffer và thống kê.
Cấu trúc thư mục:
vux_demo/
├── include/uapi/vux_demo.h # header dùng chung
├── vux_demo.c # driver
├── vux_demo_test.c # chương trình user
└── Makefile
6.1. Header UAPI¶
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _UAPI_VUX_DEMO_H
#define _UAPI_VUX_DEMO_H
#include <linux/ioctl.h>
#include <linux/types.h>
#define VUX_DEMO_BUF_SIZE 4096
enum vux_demo_mode {
VUX_DEMO_MODE_NORMAL = 0,
VUX_DEMO_MODE_UPPER = 1, /* chuyển dữ liệu ghi vào thành chữ hoa */
};
struct vux_demo_config {
__u32 mode; /* enum vux_demo_mode */
__u32 reserved; /* phải bằng 0, dành cho mở rộng sau này */
};
struct vux_demo_stats {
__u64 bytes_read;
__u64 bytes_written;
};
#define VUX_DEMO_IOC_MAGIC 0xD5
#define VUX_DEMO_RESET _IO(VUX_DEMO_IOC_MAGIC, 0x00)
#define VUX_DEMO_GET_CONFIG _IOR(VUX_DEMO_IOC_MAGIC, 0x01, struct vux_demo_config)
#define VUX_DEMO_SET_CONFIG _IOW(VUX_DEMO_IOC_MAGIC, 0x02, struct vux_demo_config)
#define VUX_DEMO_GET_STATS _IOR(VUX_DEMO_IOC_MAGIC, 0x03, struct vux_demo_stats)
#endif /* _UAPI_VUX_DEMO_H */
6.2. Driver¶
// SPDX-License-Identifier: GPL-2.0
#include <linux/ctype.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include "include/uapi/vux_demo.h"
struct vux_demo {
struct mutex lock; /* bảo vệ toàn bộ các trường bên dưới */
char buf[VUX_DEMO_BUF_SIZE];
size_t len; /* số byte hợp lệ trong buf */
struct vux_demo_config cfg;
struct vux_demo_stats stats;
};
static struct vux_demo vdev = {
.lock = __MUTEX_INITIALIZER(vdev.lock),
};
static ssize_t vux_demo_read(struct file *file, char __user *ubuf,
size_t count, loff_t *ppos)
{
ssize_t ret;
mutex_lock(&vdev.lock);
if (*ppos >= vdev.len) {
ret = 0; /* EOF */
goto out;
}
count = min_t(size_t, count, vdev.len - *ppos);
if (copy_to_user(ubuf, vdev.buf + *ppos, count)) {
ret = -EFAULT;
goto out;
}
*ppos += count;
vdev.stats.bytes_read += count;
ret = count;
out:
mutex_unlock(&vdev.lock);
return ret;
}
static ssize_t vux_demo_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
ssize_t ret;
size_t i;
if (*ppos >= VUX_DEMO_BUF_SIZE)
return -ENOSPC;
count = min_t(size_t, count, VUX_DEMO_BUF_SIZE - *ppos);
mutex_lock(&vdev.lock);
if (copy_from_user(vdev.buf + *ppos, ubuf, count)) {
ret = -EFAULT;
goto out;
}
if (vdev.cfg.mode == VUX_DEMO_MODE_UPPER)
for (i = 0; i < count; i++)
vdev.buf[*ppos + i] = toupper(vdev.buf[*ppos + i]);
*ppos += count;
vdev.len = max_t(size_t, vdev.len, *ppos);
vdev.stats.bytes_written += count;
ret = count;
out:
mutex_unlock(&vdev.lock);
return ret;
}
static long vux_demo_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
void __user *argp = (void __user *)arg;
struct vux_demo_config cfg;
struct vux_demo_stats stats;
switch (cmd) {
case VUX_DEMO_RESET:
mutex_lock(&vdev.lock);
vdev.len = 0;
memset(&vdev.stats, 0, sizeof(vdev.stats));
mutex_unlock(&vdev.lock);
return 0;
case VUX_DEMO_GET_CONFIG:
mutex_lock(&vdev.lock);
cfg = vdev.cfg;
mutex_unlock(&vdev.lock);
if (copy_to_user(argp, &cfg, sizeof(cfg)))
return -EFAULT;
return 0;
case VUX_DEMO_SET_CONFIG:
if (copy_from_user(&cfg, argp, sizeof(cfg)))
return -EFAULT;
if (cfg.mode > VUX_DEMO_MODE_UPPER || cfg.reserved)
return -EINVAL;
mutex_lock(&vdev.lock);
vdev.cfg = cfg;
mutex_unlock(&vdev.lock);
return 0;
case VUX_DEMO_GET_STATS:
mutex_lock(&vdev.lock);
stats = vdev.stats;
mutex_unlock(&vdev.lock);
if (copy_to_user(argp, &stats, sizeof(stats)))
return -EFAULT;
return 0;
default:
return -ENOTTY;
}
}
static const struct file_operations vux_demo_fops = {
.owner = THIS_MODULE,
.read = vux_demo_read,
.write = vux_demo_write,
.llseek = default_llseek,
.unlocked_ioctl = vux_demo_ioctl,
.compat_ioctl = compat_ptr_ioctl,
};
static struct miscdevice vux_demo_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "vux_demo",
.fops = &vux_demo_fops,
.mode = 0666,
};
module_misc_device(vux_demo_misc);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Demo character device: read/write and ioctl");
Một vài điểm đáng chú ý:
- Chỉ có một instance, nên trạng thái để trong biến toàn cục
vdev, bảo vệ bằng một mutex. Mutex (không phải spinlock) cho phép gọicopy_*_user()trong khi giữ khóa. readtrả về0khi*pposchạm cuối dữ liệu, nêncat /dev/vux_demodừng đúng chỗ.- Trong
ioctl,GET_*copy trạng thái ra biến cục bộ dưới mutex rồi mới gọicopy_to_user();SET_CONFIGcopy và kiểm tra trước, chỉ khóa khi cập nhật. - Các struct UAPI đều không có padding và có cùng kích thước trên 32/64 bit, nên dùng thẳng
compat_ptr_ioctl. .mode = 0666để test không cần root. Driver thật nên hạn chế quyền và để udev cấp cho đúng nhóm người dùng.
6.3. Makefile¶
obj-m := vux_demo.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
6.4. Chương trình user¶
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include "include/uapi/vux_demo.h"
#define DEV "/dev/vux_demo"
int main(void)
{
struct vux_demo_config cfg = { .mode = VUX_DEMO_MODE_UPPER };
struct vux_demo_stats stats;
const char *msg = "hello kernel";
char buf[64] = { 0 };
int fd;
fd = open(DEV, O_RDWR);
if (fd < 0) {
perror("open " DEV);
return 1;
}
if (ioctl(fd, VUX_DEMO_RESET) < 0 ||
ioctl(fd, VUX_DEMO_SET_CONFIG, &cfg) < 0) {
perror("ioctl");
return 1;
}
if (write(fd, msg, strlen(msg)) < 0) {
perror("write");
return 1;
}
lseek(fd, 0, SEEK_SET);
if (read(fd, buf, sizeof(buf) - 1) < 0) {
perror("read");
return 1;
}
printf("read back : %s\n", buf);
if (ioctl(fd, VUX_DEMO_GET_STATS, &stats) < 0) {
perror("ioctl GET_STATS");
return 1;
}
printf("stats : read=%llu written=%llu\n",
(unsigned long long)stats.bytes_read,
(unsigned long long)stats.bytes_written);
/* Lệnh không tồn tại: driver trả về -ENOTTY */
if (ioctl(fd, _IO(VUX_DEMO_IOC_MAGIC, 0x7f)) < 0)
printf("bad cmd : %s\n", strerror(errno));
/* Con trỏ không hợp lệ: copy_to_user() thất bại, driver trả về -EFAULT */
if (ioctl(fd, VUX_DEMO_GET_STATS, (void *)1) < 0)
printf("bad ptr : %s\n", strerror(errno));
if (ioctl(fd, VUX_DEMO_GET_STATS, (void *)0xffffffff81000000UL) < 0)
printf("kernel ptr: %s\n", strerror(errno));
close(fd);
return 0;
}
6.5. Chạy thử¶
Build và chạy:
make # build vux_demo.ko
gcc -Wall -o vux_demo_test vux_demo_test.c
sudo insmod vux_demo.ko
ls -l /dev/vux_demo
grep vux /proc/misc
./vux_demo_test
cat /dev/vux_demo
Output dưới đây thu được khi chạy trên kernel Ubuntu 6.8.0-147-generic (QEMU x86_64). Minor 261 do kernel tự cấp nên có thể khác trên máy khác:
crw-rw-rw- 1 0 0 10, 261 Oct 24 05:42 /dev/vux_demo
261 vux_demo
read back : HELLO KERNEL
stats : read=12 written=12
bad cmd : Inappropriate ioctl for device
bad ptr : Bad address
kernel ptr: Bad address
HELLO KERNEL
- Dữ liệu
hello kernelđược chuyển thành chữ hoa vìSET_CONFIGđã bậtVUX_DEMO_MODE_UPPER. - Lệnh
nr = 0x7fkhông tồn tại, driver trả về-ENOTTY. - Hai con trỏ sai làm
copy_to_user()thất bại; driver trả về-EFAULT, kernel không oops. catđọc lại được dữ liệu vì buffer nằm trong driver, không gắn với một file descriptor cụ thể.
7. Khi nào dùng / không nên dùng¶
| Nên dùng device file | Nên cân nhắc interface khác |
|---|---|
| Thiết bị có luồng dữ liệu tự nhiên: UART, ADC, cảm biến, FIFO của FPGA | Chỉ có vài tham số cấu hình đơn → sysfs |
Cần kiểm soát quyền truy cập theo từng node (chmod, chown, udev rule) |
Chỉ phục vụ debug → debugfs |
Cần kết hợp read/write với poll và mmap trên cùng một fd |
Kernel cần gửi sự kiện tới nhiều bên nghe → netlink, uevent |
| Lệnh điều khiển có cấu trúc, cần thực hiện nguyên tử (ioctl) | Loại thiết bị đã có subsystem chuẩn → dùng interface của subsystem |
8. Công cụ debug¶
strace hiển thị từng ioctl cùng giá trị trả về. Với mã lệnh không biết tên, strace giải mã thành dạng _IOC(dir, type, nr, size). Output thật khi chạy strace -e trace=ioctl ./vux_demo_test > /dev/null trên môi trường ở mục 6.5:
ioctl(3, _IOC(_IOC_NONE, 0xd5, 0, 0), 0) = 0
ioctl(3, _IOC(_IOC_WRITE, 0xd5, 0x2, 0x8), 0x7ffd9f558dd8) = 0
ioctl(1, TCGETS, 0x7ffd9f558960) = -1 ENOTTY (Inappropriate ioctl for device)
ioctl(3, _IOC(_IOC_READ, 0xd5, 0x3, 0x10), 0x7ffd9f558de0) = 0
ioctl(3, _IOC(_IOC_NONE, 0xd5, 0x7f, 0), 0) = -1 ENOTTY (Inappropriate ioctl for device)
ioctl(3, _IOC(_IOC_READ, 0xd5, 0x3, 0x10), 0x1) = -1 EFAULT (Bad address)
ioctl(3, _IOC(_IOC_READ, 0xd5, 0x3, 0x10), 0xffffffff81000000) = -1 EFAULT (Bad address)
Dòng TCGETS trên fd 1 không đến từ driver: đó là glibc kiểm tra xem stdout có phải terminal không, trước lần printf() đầu tiên.
Các công cụ khác:
cat /proc/misc,ls -l /dev/<tên>: kiểm tra node đã được tạo với đúng major/minor và quyền.cat /proc/devices: danh sách major của character và block device.udevadm info -a /dev/<tên>: xem thuộc tính để viết udev rule.- Dynamic Debug: thêm
pr_debug()/dev_dbg()vào callback và bật lúc chạy. - FTrace: đặt
set_ftrace_filtervào hàmvux_demo_ioctlđể xem tần suất và thời gian thực thi.
Tham khảo¶
- Tổng quan Kernel Space, User Space Interfaces
- Character Device Driver
- Kernel source:
include/linux/fs.h,include/linux/miscdevice.h,include/uapi/asm-generic/ioctl.h - Kernel docs:
Documentation/userspace-api/ioctl/ioctl-number.rst,Documentation/driver-api/ioctl.rst - Linux Device Drivers, 3rd Edition (LDD3), chương 3 và 6