三類裝置#
Linux 把裝置分三類:
| 類型 | 範例 | 介面特色 |
|---|---|---|
| 字元裝置 | /dev/null、/dev/tty、感測器 | 串流式(依序 read/write),無位移概念 |
| 區塊裝置 | /dev/sda、/dev/nvme0n1 | 依固定大小區塊存取,可隨機 seek,有 buffer cache |
| 網路裝置 | eth0、wlan0、lo | 不在 /dev/ 裡,靠 socket API 存取 |
字元裝置是最簡單的一類,也是寫驅動最常見的起點。本章只談字元裝置。
從 userspace 看裝置#
ls -l /dev/null /dev/random /dev/sda
# crw-rw-rw- 1 root root 1, 3 Apr 29 10:00 /dev/null
# crw-rw-rw- 1 root root 1, 8 Apr 29 10:00 /dev/random
# brw-rw---- 1 root disk 8, 0 Apr 29 10:00 /dev/sda開頭的 c 表示字元裝置,b 表示區塊裝置。後面的兩個數字是 major 與 minor number:
- major:哪個驅動程式
- minor:同一個驅動下的哪個實例
dev_t devno = MKDEV(major, minor); // 編碼
unsigned int M = MAJOR(devno);
unsigned int m = MINOR(devno);完整流程#
寫一個能 cat /dev/myxxx 看到內容的字元裝置,需要四步:
- 配置一個
dev_t(major + minor 範圍) - 初始化
struct cdev並關聯file_operations - 用
cdev_add把它註冊進核心 - (手動建立
/dev/...節點,或用device_create讓 udev 自動建)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "myxxx"
#define BUF_SIZE 1024
static dev_t dev_no;
static struct cdev my_cdev;
static struct class *my_class;
static char *buffer;
static size_t buffer_len;
static int my_open(struct inode *inode, struct file *filp)
{
pr_info("myxxx: open\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
pr_info("myxxx: release\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
size_t avail;
if (*off >= buffer_len)
return 0; // EOF
avail = min(count, buffer_len - (size_t)*off);
if (copy_to_user(buf, buffer + *off, avail))
return -EFAULT;
*off += avail;
return avail;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *off)
{
size_t to_copy = min(count, (size_t)BUF_SIZE);
if (copy_from_user(buffer, buf, to_copy))
return -EFAULT;
buffer_len = to_copy;
return to_copy;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
static int __init myxxx_init(void)
{
int err;
buffer = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!buffer) return -ENOMEM;
err = alloc_chrdev_region(&dev_no, 0, 1, DEVICE_NAME);
if (err) goto free_buf;
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
err = cdev_add(&my_cdev, dev_no, 1);
if (err) goto unreg;
my_class = class_create(DEVICE_NAME); // 6.4+ 簽名
if (IS_ERR(my_class)) { err = PTR_ERR(my_class); goto del_cdev; }
device_create(my_class, NULL, dev_no, NULL, DEVICE_NAME);
pr_info("myxxx: registered (major=%d)\n", MAJOR(dev_no));
return 0;
del_cdev:
cdev_del(&my_cdev);
unreg:
unregister_chrdev_region(dev_no, 1);
free_buf:
kfree(buffer);
return err;
}
static void __exit myxxx_exit(void)
{
device_destroy(my_class, dev_no);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_no, 1);
kfree(buffer);
pr_info("myxxx: unregistered\n");
}
module_init(myxxx_init);
module_exit(myxxx_exit);
MODULE_LICENSE("GPL");載入後:
sudo insmod myxxx.ko
ls -l /dev/myxxx # 由 udev 自動建立
echo "hello world" > /dev/myxxx
cat /dev/myxxx # hello world
sudo rmmod myxxxfile_operations ── 介面就是這個結構#
整個字元裝置 API 都圍繞一個 vtable:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
// ...
};每個函式都對應一個 syscall:
| syscall | fop 欄位 |
|---|---|
open(2) | .open |
close(2) | .release |
read(2) | .read |
write(2) | .write |
lseek(2) | .llseek |
poll/select/epoll | .poll |
ioctl(2) | .unlocked_ioctl |
mmap(2) | .mmap |
不需要實作的填 NULL,VFS 會用合理預設或回傳 -ENOTTY/-EINVAL。
使用者空間 vs 核心空間:copy_to/from_user#
永遠不要直接 memcpy(kernel_ptr, user_ptr, n)。原因:
- user pointer 可能指向沒映射的位址(page fault)
- user pointer 可能被惡意指向核心位址(安全漏洞)
- 使用者頁可能被換出(swap),需要核心介入觸發 page-in
if (copy_to_user(user_buf, kernel_buf, n))
return -EFAULT;
if (copy_from_user(kernel_buf, user_buf, n))
return -EFAULT;這對函式回傳「沒搬完的 byte 數」,0 表示全部成功。它們會:
- 檢查 user pointer 在合法 user 範圍
- 處理 page fault
- 失敗時回傳剩餘量,由你決定怎麼處理
__user 是個 sparse 註解 ── 提醒人類「這是 user-space 指標,不能直接解參考」。一般編譯不檢查,但跑 make C=1 時 sparse 會抓出違規。
misc 裝置 ── 偷懶的字元裝置#
如果你不需要自己分配 major number,用 misc 機制更簡單:
#include <linux/miscdevice.h>
static struct miscdevice my_misc = {
.minor = MISC_DYNAMIC_MINOR, // 動態配 minor
.name = "myxxx", // 會出現在 /dev/myxxx
.fops = &my_fops,
};
static int __init myxxx_init(void) {
return misc_register(&my_misc);
}
static void __exit myxxx_exit(void) {
misc_deregister(&my_misc);
}整個 cdev_init、cdev_add、class_create、device_create 一條 misc_register 全包。所有 misc 裝置共用 major 10,minor 動態配。/dev/null、/dev/random 等都不是 misc,但很多現代驅動(如 /dev/kvm、/dev/loop-control、/dev/fuse)是。
阻塞 I/O 與 wait queue#
如果讀取時還沒資料,UNIX 慣例是阻塞直到有資料。靠 wait queue 實現:
static DECLARE_WAIT_QUEUE_HEAD(read_wq);
static int data_ready = 0;
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
int ret;
if (!data_ready) {
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN; // 非阻塞模式:立刻回
ret = wait_event_interruptible(read_wq, data_ready);
if (ret)
return -ERESTARTSYS; // 被訊號打斷
}
// 拷貝資料到 user...
data_ready = 0;
return n;
}
// 寫入端(例如中斷處理)通知讀者:
data_ready = 1;
wake_up_interruptible(&read_wq);wait_event_interruptible 三個關鍵:
- 進入 sleep state 之前先檢查條件(避免 lost wakeup)
- 可被訊號中斷(重要!否則
kill也殺不掉) - 醒來後重新檢查條件(spurious wakeup 是真的存在)
對比有兩種變體:
wait_event(wq, cond):不可被訊號中斷(極少用,會讓行程變成 D 狀態無法 kill)wait_event_timeout(wq, cond, jiffies):超時版本
poll/select/epoll 支援#
userspace 想用 poll() 多工 ── 你必須實作 .poll:
#include <linux/poll.h>
static __poll_t my_poll(struct file *filp, struct poll_table_struct *wait)
{
__poll_t mask = 0;
poll_wait(filp, &read_wq, wait); // 把當前等待者加到 wq
if (data_ready)
mask |= EPOLLIN | EPOLLRDNORM; // 可讀
if (write_space_available)
mask |= EPOLLOUT | EPOLLWRNORM;// 可寫
return mask;
}poll_wait 不會真的睡眠 ── 它只是「在 poll 表登記這個 wq」,由 poll/epoll 框架統一管理。當 wake_up_interruptible(&read_wq) 被呼叫,所有正在 poll/select/epoll_wait 等這個 fd 的 task 都會被喚醒。
ioctl ── 「其他什麼都用這個」#
read/write 是串流,但有時候你要下命令:「重置裝置」、「設定取樣率」、「查詢狀態」── 這時候用 ioctl:
#include <linux/ioctl.h>
#define MY_MAGIC 'M'
#define MY_RESET _IO(MY_MAGIC, 0)
#define MY_SET_RATE _IOW(MY_MAGIC, 1, int)
#define MY_GET_RATE _IOR(MY_MAGIC, 2, int)
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case MY_RESET:
do_reset();
return 0;
case MY_SET_RATE: {
int rate;
if (copy_from_user(&rate, (int __user *)arg, sizeof(rate)))
return -EFAULT;
set_rate(rate);
return 0;
}
case MY_GET_RATE: {
int rate = current_rate();
if (copy_to_user((int __user *)arg, &rate, sizeof(rate)))
return -EFAULT;
return 0;
}
default:
return -ENOTTY; // 慣例:未知 cmd 回 ENOTTY
}
}_IO/_IOR/_IOW/_IOWR 巨集會把 magic、方向、大小編碼進 cmd 整數。這樣核心可以驗證型別、防止傳錯命令給錯的驅動。
ioctl 是 Linux 接口設計上常被詬病的部分 ── 它違反了「介面要小」的 UNIX 哲學。新驅動應優先考慮其他機制:sysfs(給簡單參數)、netlink(給大流量配置)、eBPF(給可程式化)。但對既有硬體,ioctl 仍然是事實標準。
非同步通知(SIGIO)#
讓 userspace 收到 SIGIO 訊號當資料可用:
static int my_fasync(int fd, struct file *filp, int mode)
{
return fasync_helper(fd, filp, mode, &async_queue);
}
// fops:
.fasync = my_fasync,
// 資料到來時:
kill_fasync(&async_queue, SIGIO, POLL_IN);userspace 要先 fcntl(fd, F_SETOWN, getpid()) + fcntl(fd, F_SETFL, O_ASYNC) 才會收到訊號。實務上 epoll 幾乎完全取代了 SIGIO ── 訊號 + 非同步是兩個都不好處理的東西。
偵錯小技巧#
dmesg -w # 即時 tail 核心日誌
sudo cat /proc/devices # 已註冊的 character devices
sudo cat /proc/misc # 已註冊的 misc devices
sudo strace -e read,write,ioctl ./prog # 看 syscall 流向如果 cat /dev/myxxx 卡住沒反應 ── 通常是阻塞讀沒人 wake_up。檢查中斷處理或 timer 有沒有正確觸發 wake_up_interruptible。
小結#
字元裝置驅動的整個世界就是:
- 註冊一個 major/minor +
file_operations - 實作對應的 syscall handler
- 用
copy_to/from_user跨越 user/kernel 邊界 - 用 wait queue 處理阻塞,用
.poll支援epoll - 用 ioctl 處理「其他什麼」(盡量少用)
下一章看 syscall 機制本身,理解「read(fd, buf, n) 是怎麼從 user code 一路走到 my_read」的全鏈路。