《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 5 章 Linux 文件系统与设备文件

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 5 章 Linux 文件系统与设备文件

参考:宋宝华 著,机械工业出版社,2015年版


5.1 Linux 文件操作

5.1.1 一切皆文件

Linux 继承了 UNIX 的设计哲学:"一切皆文件(Everything is a File)"。这是 Linux 最重要的设计思想之一。

复制代码
Linux 中"文件"的范畴:

普通文件    /home/user/hello.c        ← 文本/二进制数据
目录        /home/user/               ← 目录也是文件
字符设备    /dev/ttyS0                ← 串口设备
块设备      /dev/sda                  ← 硬盘设备
符号链接    /usr/bin/python → python3 ← 软链接
管道        /tmp/mypipe               ← 命名管道(FIFO)
套接字      /var/run/docker.sock      ← Unix 域套接字
/proc 文件  /proc/cpuinfo             ← 内核信息(虚拟文件)
/sys 文件   /sys/class/gpio/gpio23/   ← 设备属性(虚拟文件)

"一切皆文件"的意义

无论是普通文件、设备、管道还是网络套接字,应用程序都通过相同的系统调用接口(openreadwritecloseioctl)访问,极大地简化了编程模型。

5.1.2 文件描述符

文件描述符(File Descriptor,fd)是内核为每个打开的文件分配的非负整数,是用户空间访问文件的句柄:

复制代码
进程的文件描述符表:

进程 A(PID=1234)
┌─────────────────────────────────────────────────────┐
│  文件描述符表(files_struct)                         │
│  fd=0  → struct file → /dev/tty(标准输入)           │
│  fd=1  → struct file → /dev/tty(标准输出)           │
│  fd=2  → struct file → /dev/tty(标准错误)           │
│  fd=3  → struct file → /home/user/test.txt           │
│  fd=4  → struct file → /dev/ttyS0(串口)            │
│  fd=5  → NULL(未使用)                              │
└─────────────────────────────────────────────────────┘

预定义的文件描述符:
  STDIN_FILENO  = 0  ← 标准输入
  STDOUT_FILENO = 1  ← 标准输出
  STDERR_FILENO = 2  ← 标准错误

5.1.3 文件操作系统调用

Linux 提供了一套完整的文件操作系统调用:

(1)open / close
c 复制代码
#include <fcntl.h>
#include <unistd.h>

/*
 * open:打开或创建文件
 * pathname:文件路径
 * flags:打开标志
 * mode:创建文件时的权限(仅当 flags 包含 O_CREAT 时有效)
 * 返回:文件描述符(>=0),或 -1(失败)
 */
int fd = open(pathname, flags);
int fd = open(pathname, flags, mode);

/* 常用 flags:
 * O_RDONLY   ← 只读
 * O_WRONLY   ← 只写
 * O_RDWR     ← 读写
 * O_CREAT    ← 不存在则创建
 * O_TRUNC    ← 打开时截断为0长度
 * O_APPEND   ← 追加写入
 * O_NONBLOCK ← 非阻塞模式(对设备文件重要)
 * O_SYNC     ← 同步写入(每次写入都刷新到硬件)
 */

/* 示例:打开串口设备 */
int fd = open("/dev/ttyS0", O_RDWR | O_NONBLOCK);
if (fd < 0) {
    perror("open /dev/ttyS0 failed");
    return -1;
}

/* close:关闭文件描述符 */
close(fd);
(2)read / write
c 复制代码
#include <unistd.h>

/*
 * read:从文件读取数据
 * fd:文件描述符
 * buf:接收数据的缓冲区
 * count:请求读取的字节数
 * 返回:实际读取的字节数,0(EOF),-1(错误)
 */
ssize_t n = read(fd, buf, count);

/*
 * write:向文件写入数据
 * 返回:实际写入的字节数,-1(错误)
 */
ssize_t n = write(fd, buf, count);

/* 完整的读写示例 */
char buf[256];
ssize_t n;

/* 循环读取,直到读完所有数据 */
while ((n = read(fd, buf, sizeof(buf))) > 0) {
    /* 处理读取到的 n 字节数据 */
    write(STDOUT_FILENO, buf, n);
}
if (n < 0) {
    perror("read failed");
}
(3)lseek
c 复制代码
#include <unistd.h>

/*
 * lseek:移动文件读写位置
 * fd:文件描述符
 * offset:偏移量
 * whence:基准位置
 *   SEEK_SET ← 从文件头偏移
 *   SEEK_CUR ← 从当前位置偏移
 *   SEEK_END ← 从文件尾偏移
 * 返回:新的文件位置,-1(错误)
 */
off_t pos = lseek(fd, offset, whence);

/* 示例 */
lseek(fd, 0, SEEK_SET);          /* 回到文件头 */
lseek(fd, 0, SEEK_END);          /* 移到文件尾 */
off_t size = lseek(fd, 0, SEEK_END); /* 获取文件大小 */
lseek(fd, -10, SEEK_CUR);        /* 从当前位置后退10字节 */

/* 注意:字符设备通常不支持 lseek(返回 -ESPIPE) */
(4)ioctl
c 复制代码
#include <sys/ioctl.h>

/*
 * ioctl:设备控制(Input/Output Control)
 * fd:文件描述符
 * request:控制命令(设备相关)
 * ...:可选参数(通常是指针)
 * 返回:0(成功),-1(失败)
 */
int ret = ioctl(fd, request, arg);

/* 示例:设置串口波特率 */
#include <termios.h>
struct termios tty;
tcgetattr(fd, &tty);
cfsetispeed(&tty, B115200);
cfsetospeed(&tty, B115200);
tcsetattr(fd, TCSANOW, &tty);

/* 示例:获取网卡 MAC 地址 */
#include <net/if.h>
struct ifreq ifr;
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
ioctl(sockfd, SIOCGIFHWADDR, &ifr);
/* ifr.ifr_hwaddr.sa_data 即为 MAC 地址 */

/* 示例:控制 GPIO(通过自定义驱动) */
#define GPIO_SET_VALUE  _IOW('G', 1, int)
#define GPIO_GET_VALUE  _IOR('G', 2, int)

int value = 1;
ioctl(fd, GPIO_SET_VALUE, &value);  /* 设置 GPIO 输出高电平 */
ioctl(fd, GPIO_GET_VALUE, &value);  /* 读取 GPIO 输入电平 */
(5)mmap
c 复制代码
#include <sys/mman.h>

/*
 * mmap:将文件或设备映射到进程地址空间
 * addr:建议映射地址(通常为 NULL,由内核决定)
 * length:映射长度
 * prot:内存保护标志(PROT_READ/PROT_WRITE/PROT_EXEC)
 * flags:映射标志(MAP_SHARED/MAP_PRIVATE)
 * fd:文件描述符
 * offset:文件偏移(必须是页大小的整数倍)
 * 返回:映射的虚拟地址,MAP_FAILED(失败)
 */
void *addr = mmap(NULL, length, prot, flags, fd, offset);

/* 示例:映射设备寄存器到用户空间(通过 /dev/mem) */
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
void *gpio_base = mmap(NULL, 0x1000,
                       PROT_READ | PROT_WRITE,
                       MAP_SHARED,
                       mem_fd, 0x3F200000);  /* GPIO 物理基地址 */

/* 直接操作寄存器 */
volatile uint32_t *gpio = (volatile uint32_t *)gpio_base;
gpio[7] = (1 << 18);   /* 设置 GPIO 输出高电平 */

/* 解除映射 */
munmap(gpio_base, 0x1000);
close(mem_fd);
(6)poll / select
c 复制代码
#include <poll.h>
#include <sys/select.h>

/*
 * poll:等待一个或多个文件描述符就绪
 * 常用于同时监控多个设备(如多个串口)
 */
struct pollfd fds[2];
fds[0].fd = fd_uart0;
fds[0].events = POLLIN;   /* 等待可读 */
fds[1].fd = fd_uart1;
fds[1].events = POLLIN;

int ret = poll(fds, 2, 1000);  /* 超时 1000ms */
if (ret > 0) {
    if (fds[0].revents & POLLIN)
        read(fd_uart0, buf, sizeof(buf));
    if (fds[1].revents & POLLIN)
        read(fd_uart1, buf, sizeof(buf));
} else if (ret == 0) {
    printf("超时,没有数据\n");
}

5.1.4 文件操作与驱动函数的对应关系

用户空间的文件操作系统调用与驱动程序中 file_operations 的对应关系:

复制代码
用户空间系统调用          内核 VFS 函数           驱动 file_operations
─────────────────────────────────────────────────────────────────────
open(path, flags)    →  vfs_open()          →  .open
close(fd)            →  vfs_release()       →  .release
read(fd, buf, n)     →  vfs_read()          →  .read
write(fd, buf, n)    →  vfs_write()         →  .write
lseek(fd, off, w)    →  vfs_llseek()        →  .llseek
ioctl(fd, cmd, arg)  →  vfs_ioctl()         →  .unlocked_ioctl
mmap(NULL, len, ...) →  vfs_mmap()          →  .mmap
poll(fds, n, to)     →  vfs_poll()          →  .poll
fsync(fd)            →  vfs_fsync()         →  .fsync

5.2 Linux 文件系统

5.2.1 文件系统的概念

文件系统是操作系统用于组织和管理存储设备上数据的方法和数据结构。Linux 通过 VFS(虚拟文件系统)支持多种文件系统:

复制代码
Linux 支持的文件系统分类:

磁盘文件系统(持久化存储):
  ext2/ext3/ext4  ← Linux 原生文件系统(最常用)
  xfs             ← 高性能日志文件系统(RHEL 默认)
  btrfs           ← 新一代写时复制文件系统
  fat/vfat/exfat  ← Windows 兼容文件系统(U盘常用)
  ntfs            ← Windows NT 文件系统(只读或第三方)
  iso9660         ← CD-ROM 文件系统

网络文件系统:
  nfs             ← Network File System
  cifs/smb        ← Windows 共享文件系统
  sshfs           ← 基于 SSH 的文件系统

虚拟文件系统(内存中,无持久化):
  proc            ← /proc,内核信息接口
  sysfs           ← /sys,设备模型接口
  tmpfs           ← 内存文件系统(/tmp 常用)
  devtmpfs        ← /dev,设备文件系统
  debugfs         ← /sys/kernel/debug,调试接口

嵌入式文件系统:
  jffs2           ← NAND/NOR Flash 日志文件系统
  ubifs           ← UBI 层上的文件系统(NAND Flash)
  squashfs        ← 只读压缩文件系统(嵌入式根文件系统)
  cramfs          ← 压缩 ROM 文件系统

5.2.2 VFS(虚拟文件系统)

VFS 是 Linux 文件系统的抽象层,为所有文件系统提供统一的接口:

复制代码
VFS 架构:

用户空间:open("/mnt/usb/file.txt")
              ↓
         系统调用接口
              ↓
┌─────────────────────────────────────────────────────┐
│                  VFS 层                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐  │
│  │superblock│  │  inode   │  │     dentry       │  │
│  │  cache   │  │  cache   │  │     cache        │  │
│  └──────────┘  └──────────┘  └──────────────────┘  │
└──────────┬──────────────┬──────────────┬────────────┘
           │              │              │
      ┌────▼────┐   ┌─────▼────┐  ┌─────▼────┐
      │  ext4   │   │   fat32  │  │  tmpfs   │
      └────┬────┘   └─────┬────┘  └─────┬────┘
           │              │              │
      ┌────▼────┐   ┌─────▼────┐        │
      │  硬盘   │   │   U盘    │      内存
      └─────────┘   └──────────┘

5.2.3 ext4 文件系统结构

ext4 是 Linux 最常用的文件系统,了解其结构有助于理解文件系统的工作原理:

复制代码
ext4 磁盘布局:

┌──────────────────────────────────────────────────────────────┐
│  引导块  │  块组0  │  块组1  │  块组2  │  ...  │  块组N      │
│ (1KB)   │         │         │         │       │             │
└──────────┴─────────┴─────────┴─────────┴───────┴─────────────┘

每个块组的结构:
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────┐
│超级块副本│块组描述符│块位图    │inode位图 │inode表   │数据块│
│(可选)    │          │(1块)     │(1块)     │          │      │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────┘

关键概念:
  超级块(Superblock):文件系统的元数据(块大小、inode数量等)
  inode:文件的元数据(大小、权限、时间戳、数据块指针)
  数据块:存储文件实际内容
  目录项(dentry):文件名到 inode 号的映射

5.2.4 文件系统的挂载

bash 复制代码
# 查看已挂载的文件系统
mount
# /dev/sda1 on / type ext4 (rw,relatime)
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev)
# sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
# proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
# devtmpfs on /dev type devtmpfs (rw,nosuid,size=10240k,nr_inodes=4096,mode=755)

# 挂载文件系统
sudo mount /dev/sdb1 /mnt/usb              # 挂载 U 盘
sudo mount -t ext4 /dev/sdb1 /mnt/data    # 指定文件系统类型
sudo mount -t tmpfs tmpfs /mnt/ramdisk -o size=100M  # 挂载内存文件系统

# 卸载文件系统
sudo umount /mnt/usb

# 查看文件系统信息
df -h
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/sda1        20G   8G   11G  43% /
# tmpfs           2.0G  1.2M  2.0G   1% /tmp

# 查看 inode 使用情况
df -i

5.3 devfs(Linux 2.4)

5.3.1 devfs 的背景

在 Linux 2.4 时代,设备文件管理面临以下问题:

复制代码
传统设备文件管理的问题(Linux 2.4 之前):

问题一:静态设备文件
  /dev 目录下的设备文件是静态创建的(mknod 命令)
  即使设备不存在,/dev/sda、/dev/sdb 等文件也存在
  /dev 目录下可能有数千个设备文件,大多数从不使用

问题二:设备号冲突
  主设备号由内核硬编码分配,不同驱动可能冲突
  新设备需要向内核社区申请设备号

问题三:热插拔支持差
  USB、PCMCIA 等热插拔设备插入时,需要手动创建设备文件
  无法自动响应设备的插入和拔出

5.3.2 devfs 的工作原理

devfs(Device File System)是 Linux 2.4 引入的解决方案,由驱动程序在 /dev动态创建和删除设备文件:

复制代码
devfs 工作流程:

驱动加载时:
  驱动调用 devfs_register() → 在 /dev 下自动创建设备文件

驱动卸载时:
  驱动调用 devfs_unregister() → 自动删除设备文件

特点:
  ✓ 设备文件动态创建,/dev 目录整洁
  ✓ 驱动直接控制设备文件的创建
  ✗ 设备命名由内核决定,用户无法自定义
  ✗ 设备文件在内存中,重启后不保留权限设置
  ✗ 策略(命名规则)与机制(驱动)混在内核中

devfs 的 API(Linux 2.4)

c 复制代码
/* Linux 2.4 devfs API(已废弃,仅作历史参考) */
#include <linux/devfs_fs_kernel.h>

/* 注册设备文件 */
devfs_handle_t handle = devfs_register(
    NULL,           /* 父目录(NULL 表示 /dev 根目录) */
    "mydevice",     /* 设备文件名 */
    DEVFS_FL_DEFAULT,
    MY_MAJOR,       /* 主设备号 */
    0,              /* 次设备号 */
    S_IFCHR | 0644, /* 字符设备,权限 644 */
    &my_fops,       /* file_operations */
    NULL            /* 私有数据 */
);

/* 注销设备文件 */
devfs_unregister(handle);

5.3.3 devfs 的废弃

devfs 在 Linux 2.6.13 中被彻底移除,原因如下:

复制代码
devfs 被废弃的原因:

1. 内核策略与机制混合
   设备命名(策略)应该由用户空间决定,不应该在内核中硬编码
   例如:同一块网卡,不同用户可能希望命名为 eth0 或 net0

2. 竞争条件(Race Condition)
   设备文件创建和应用程序访问之间存在时间窗口
   热插拔设备可能在文件创建完成前就被访问

3. 维护困难
   devfs 代码复杂,与内核其他部分耦合度高

4. udev 的出现
   udev 提供了更灵活、更强大的设备管理方案
   将设备文件管理完全移到用户空间

5.4 udev(Linux 2.6 以后)

5.4.1 udev 的设计思想

udev 是 Linux 2.6 引入的设备管理器,其核心设计思想是:

"机制与策略分离":内核只负责发现设备(机制),设备文件的创建、命名、权限设置等策略由用户空间的 udev 决定。

复制代码
udev 架构:

┌─────────────────────────────────────────────────────────────┐
│                      用户空间                                │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                    udevd 守护进程                     │   │
│  │  监听 netlink 事件 → 匹配规则 → 执行动作              │   │
│  └──────────────────────────┬─────────────────────────┘   │
│                             │ 创建/删除设备文件              │
│  ┌──────────────────────────▼─────────────────────────┐   │
│  │                    /dev 目录                         │   │
│  │  /dev/sda  /dev/ttyUSB0  /dev/input/event0  ...    │   │
│  └────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                             ↑ netlink 事件(uevent)
┌─────────────────────────────────────────────────────────────┐
│                      内核空间                                │
│  设备插入 → 驱动 probe → kobject_uevent() → 发送 netlink 消息│
└─────────────────────────────────────────────────────────────┘

5.4.2 udev 的工作流程

复制代码
udev 完整工作流程(以 USB 设备插入为例):

1. 用户插入 USB 设备
   ↓
2. 内核 USB 子系统检测到设备,加载对应驱动
   ↓
3. 驱动 probe 函数执行,调用 device_add()
   ↓
4. 内核通过 netlink 发送 uevent 消息:
   ACTION=add
   DEVPATH=/devices/pci0000:00/0000:00:1d.0/usb1/1-1
   SUBSYSTEM=usb
   DEVTYPE=usb_device
   MAJOR=189
   MINOR=1
   ↓
5. udevd 守护进程接收到 uevent 消息
   ↓
6. udevd 按顺序匹配 /etc/udev/rules.d/ 和 /lib/udev/rules.d/ 中的规则
   ↓
7. 根据匹配的规则执行动作:
   - 创建设备文件(mknod)
   - 设置权限和所有者
   - 创建符号链接
   - 运行自定义脚本
   ↓
8. 设备文件 /dev/sdb 创建完成,用户可以访问

5.4.3 udev 规则文件

udev 规则文件位于 /etc/udev/rules.d/(用户自定义)和 /lib/udev/rules.d/(系统默认),文件名格式为 数字-名称.rules,数字越小优先级越高:

bash 复制代码
# 规则文件命名示例
ls /etc/udev/rules.d/
# 10-local.rules
# 50-udev-default.rules
# 99-my-device.rules

udev 规则语法

bash 复制代码
# 规则格式:匹配键=="值", 赋值键="值"
# 匹配键(==):用于匹配设备属性
# 赋值键(=、+=、:=):用于执行动作

# ── 常用匹配键 ──────────────────────────────────────────
# ACTION      ← 事件类型(add/remove/change)
# SUBSYSTEM   ← 设备子系统(usb/tty/block/net 等)
# KERNEL      ← 内核设备名(sda、ttyUSB0 等)
# ATTR{file}  ← 设备 sysfs 属性值
# ATTRS{file} ← 设备或父设备的 sysfs 属性值
# ENV{var}    ← 环境变量
# DRIVER      ← 驱动名称

# ── 常用赋值键 ──────────────────────────────────────────
# NAME        ← 设备文件名
# SYMLINK     ← 创建符号链接
# OWNER       ← 设备文件所有者
# GROUP       ← 设备文件所属组
# MODE        ← 设备文件权限
# RUN         ← 运行程序或脚本
# LABEL       ← 规则标签(用于 GOTO)
# GOTO        ← 跳转到指定标签

# ── 规则示例 ──────────────────────────────────────────

# 示例1:为特定 USB 串口设备创建固定名称的符号链接
# 当 VID=0403, PID=6001 的 USB 串口插入时,创建 /dev/myserial 链接
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
    SYMLINK+="myserial", MODE="0666"

# 示例2:为 USB 存储设备设置权限
SUBSYSTEM=="block", KERNEL=="sd[a-z]", \
    OWNER="root", GROUP="disk", MODE="0660"

# 示例3:设备插入时运行脚本
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", \
    RUN+="/usr/local/bin/usb_inserted.sh"

# 示例4:为网卡设置固定名称(基于 MAC 地址)
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="00:11:22:33:44:55", \
    NAME="eth_fixed"

# 示例5:为 GPIO 设备设置权限(允许普通用户访问)
SUBSYSTEM=="gpio", ACTION=="add", \
    GROUP="gpio", MODE="0660"

# 示例6:为 I2C 设备设置权限
SUBSYSTEM=="i2c-dev", ACTION=="add", \
    GROUP="i2c", MODE="0660"

5.4.4 驱动中触发 udev 的代码

驱动程序通过 class_createdevice_create 触发 udev 自动创建设备文件:

c 复制代码
#include <linux/device.h>
#include <linux/cdev.h>

static struct class  *my_class;
static struct device *my_device;
static dev_t          devno;

static int __init my_driver_init(void)
{
    int ret;

    /* 1. 申请设备号 */
    ret = alloc_chrdev_region(&devno, 0, 1, "mydevice");
    if (ret < 0) return ret;

    /* 2. 注册字符设备 */
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, devno, 1);
    if (ret) goto err_cdev;

    /*
     * 3. 创建设备类(在 /sys/class/ 下创建目录)
     * class_create 会在 /sys/class/mydevice/ 下创建目录
     */
    my_class = class_create(THIS_MODULE, "mydevice");
    if (IS_ERR(my_class)) {
        ret = PTR_ERR(my_class);
        goto err_class;
    }

    /*
     * 4. 创建设备(触发 udev 创建 /dev/mydevice0)
     * device_create 会:
     *   a. 在 /sys/class/mydevice/mydevice0/ 下创建设备属性文件
     *   b. 发送 uevent 给 udev
     *   c. udev 根据规则在 /dev/ 下创建设备文件
     */
    my_device = device_create(my_class, NULL, devno, NULL, "mydevice%d", 0);
    if (IS_ERR(my_device)) {
        ret = PTR_ERR(my_device);
        goto err_device;
    }

    pr_info("mydevice: 驱动加载,设备文件 /dev/mydevice0 已创建\n");
    return 0;

err_device:
    class_destroy(my_class);
err_class:
    cdev_del(&my_cdev);
err_cdev:
    unregister_chrdev_region(devno, 1);
    return ret;
}

static void __exit my_driver_exit(void)
{
    /* 逆序清理:device_destroy → class_destroy → cdev_del → unregister */
    device_destroy(my_class, devno);    /* 触发 udev 删除 /dev/mydevice0 */
    class_destroy(my_class);            /* 删除 /sys/class/mydevice/ */
    cdev_del(&my_cdev);
    unregister_chrdev_region(devno, 1);
    pr_info("mydevice: 驱动卸载\n");
}

5.4.5 udev 调试工具

bash 复制代码
# 监控 udev 事件(实时)
udevadm monitor
# KERNEL[1234.567] add    /devices/pci.../usb1/1-1 (usb)
# UDEV  [1234.890] add    /devices/pci.../usb1/1-1 (usb)

# 查看设备的 udev 属性(用于编写规则)
udevadm info -a -n /dev/ttyUSB0
# looking at device '/devices/.../ttyUSB0':
#   KERNEL=="ttyUSB0"
#   SUBSYSTEM=="tty"
#   DRIVER==""
# looking at parent device '/devices/.../1-1:1.0':
#   ATTRS{idVendor}=="0403"
#   ATTRS{idProduct}=="6001"
#   ATTRS{manufacturer}=="FTDI"

# 测试规则匹配(不实际执行)
udevadm test /sys/class/tty/ttyUSB0

# 重新加载规则(修改规则文件后)
sudo udevadm control --reload-rules
sudo udevadm trigger

# 手动触发 uevent
sudo udevadm trigger --action=add /sys/class/tty/ttyUSB0

5.5 sysfs 文件系统与 Linux 设备模型

5.5.1 Linux 设备模型的背景

Linux 2.6 引入了统一的设备模型(Device Model),解决了 Linux 2.4 时代设备管理混乱的问题:

复制代码
Linux 2.4 的设备管理问题:
  - 没有统一的设备树,各子系统各自为政
  - 电源管理困难(不知道设备间的依赖关系)
  - 热插拔支持不完善
  - 设备信息分散,难以统一查询

Linux 2.6 设备模型的目标:
  ✓ 统一描述系统中所有设备和总线的关系
  ✓ 支持电源管理(按依赖顺序挂起/恢复设备)
  ✓ 支持热插拔(自动加载/卸载驱动)
  ✓ 通过 sysfs 向用户空间暴露设备信息

5.5.2 设备模型的核心数据结构

Linux 设备模型基于三个核心概念:总线(Bus)设备(Device)驱动(Driver)

复制代码
设备模型的三角关系:

        总线(Bus)
       /           \
      /             \
设备(Device)  驱动(Driver)
      \             /
       \           /
        匹配(Match)

工作原理:
  1. 设备注册到总线(device_register)
  2. 驱动注册到总线(driver_register)
  3. 总线负责匹配设备和驱动(bus->match)
  4. 匹配成功后调用驱动的 probe 函数
kobject ------ 设备模型的基石

kobject 是 Linux 设备模型的最底层结构,所有设备相关的结构体都内嵌了 kobject

c 复制代码
/* kobject 定义(简化版,include/linux/kobject.h) */
struct kobject {
    const char          *name;      /* 对象名称(对应 sysfs 目录名) */
    struct list_head     entry;     /* 链表节点 */
    struct kobject      *parent;    /* 父对象(对应 sysfs 父目录) */
    struct kset         *kset;      /* 所属集合 */
    struct kobj_type    *ktype;     /* 对象类型(含 sysfs 操作函数) */
    struct kernfs_node  *sd;        /* sysfs 目录项 */
    struct kref          kref;      /* 引用计数 */
    /* ... */
};

/*
 * kobject 的层次关系直接映射到 sysfs 目录结构:
 *
 * kobject A(name="bus")
 *   └── kobject B(name="i2c")
 *         └── kobject C(name="i2c-0")
 *
 * 对应 sysfs 路径:/sys/bus/i2c/i2c-0/
 */
bus_type ------ 总线
c 复制代码
/* 总线类型结构体(简化版) */
struct bus_type {
    const char          *name;      /* 总线名称(如 "i2c"、"spi"、"platform") */
    struct bus_attribute *bus_attrs;/* 总线属性 */
    struct device_attribute *dev_attrs; /* 设备属性 */
    struct driver_attribute *drv_attrs; /* 驱动属性 */

    /* 设备与驱动的匹配函数 */
    int (*match)(struct device *dev, struct device_driver *drv);

    /* 热插拔事件处理 */
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);

    /* 驱动绑定到设备时调用 */
    int (*probe)(struct device *dev);

    /* 驱动从设备解绑时调用 */
    int (*remove)(struct device *dev);

    /* 电源管理 */
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    /* ... */
};

/* 注册/注销总线 */
int  bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);
device ------ 设备
c 复制代码
/* 设备结构体(简化版) */
struct device {
    struct device       *parent;    /* 父设备 */
    struct kobject       kobj;      /* 内嵌 kobject */
    const char          *init_name; /* 设备初始名称 */
    struct device_type  *type;      /* 设备类型 */
    struct bus_type     *bus;       /* 所属总线 */
    struct device_driver *driver;   /* 绑定的驱动 */
    void                *platform_data; /* 平台相关数据 */
    void                *driver_data;   /* 驱动私有数据 */
    struct device_node  *of_node;   /* 设备树节点 */
    dev_t                devt;      /* 设备号 */
    struct class        *class;     /* 所属类 */
    /* ... */
};

/* 注册/注销设备 */
int  device_register(struct device *dev);
void device_unregister(struct device *dev);

/* 设置/获取驱动私有数据 */
void  dev_set_drvdata(struct device *dev, void *data);
void *dev_get_drvdata(const struct device *dev);
device_driver ------ 驱动
c 复制代码
/* 驱动结构体(简化版) */
struct device_driver {
    const char          *name;      /* 驱动名称 */
    struct bus_type     *bus;       /* 所属总线 */
    struct module       *owner;     /* 所属模块 */

    /* 设备树匹配表 */
    const struct of_device_id *of_match_table;

    /* 驱动绑定到设备时调用 */
    int (*probe)(struct device *dev);

    /* 驱动从设备解绑时调用 */
    int (*remove)(struct device *dev);

    /* 电源管理 */
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    /* ... */
};

/* 注册/注销驱动 */
int  driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);

5.5.3 sysfs 文件系统

sysfs 是一个内存文件系统 ,挂载在 /sys 目录下,将内核的设备模型以文件系统的形式暴露给用户空间:

复制代码
/sys 目录结构:

/sys/
├── block/          ← 块设备(/dev/sda 等)
│   └── sda/
│       ├── dev     ← 设备号(8:0)
│       ├── size    ← 设备大小
│       └── queue/  ← I/O 队列参数
├── bus/            ← 系统中所有总线
│   ├── i2c/
│   │   ├── devices/    ← 该总线上的设备(符号链接)
│   │   └── drivers/    ← 该总线上的驱动
│   ├── spi/
│   ├── usb/
│   └── platform/
├── class/          ← 设备类(按功能分类)
│   ├── gpio/
│   │   └── gpiochip0/
│   ├── leds/
│   │   └── heartbeat/
│   │       ├── brightness  ← 可读写的属性文件
│   │       └── trigger
│   ├── net/
│   │   └── eth0/
│   └── tty/
│       └── ttyS0/
├── devices/        ← 所有设备的全局视图(树形结构)
│   ├── platform/   ← 平台设备
│   ├── pci0000:00/ ← PCI 设备
│   └── virtual/    ← 虚拟设备
├── firmware/       ← 固件相关
├── fs/             ← 文件系统相关
├── kernel/         ← 内核参数
│   └── debug/      ← debugfs(调试接口)
└── module/         ← 已加载模块信息
    └── my_driver/
        ├── parameters/  ← 模块参数
        └── refcnt       ← 引用计数

5.5.4 sysfs 属性文件

sysfs 中的每个文件对应设备的一个属性(Attribute),可以通过读写这些文件来查询和控制设备:

c 复制代码
#include <linux/sysfs.h>
#include <linux/device.h>

/*
 * 方法一:使用 DEVICE_ATTR 宏创建设备属性
 */

/* 定义属性的 show(读)函数 */
static ssize_t my_value_show(struct device *dev,
                              struct device_attribute *attr,
                              char *buf)
{
    struct my_dev *mydev = dev_get_drvdata(dev);
    return sprintf(buf, "%d\n", mydev->value);
}

/* 定义属性的 store(写)函数 */
static ssize_t my_value_store(struct device *dev,
                               struct device_attribute *attr,
                               const char *buf, size_t count)
{
    struct my_dev *mydev = dev_get_drvdata(dev);
    int val;

    if (kstrtoint(buf, 10, &val))
        return -EINVAL;

    mydev->value = val;
    return count;
}

/*
 * DEVICE_ATTR(name, mode, show, store)
 * 创建名为 dev_attr_my_value 的属性
 * 对应 sysfs 文件:/sys/class/mydevice/mydevice0/my_value
 */
static DEVICE_ATTR(my_value, 0644, my_value_show, my_value_store);

/* 只读属性(store 为 NULL) */
static DEVICE_ATTR(status, 0444, status_show, NULL);

/* 只写属性(show 为 NULL) */
static DEVICE_ATTR(command, 0200, NULL, command_store);

/* 在 probe 函数中创建属性文件 */
static int my_probe(struct platform_device *pdev)
{
    int ret;

    /* ... 其他初始化 ... */

    /* 创建 sysfs 属性文件 */
    ret = device_create_file(&pdev->dev, &dev_attr_my_value);
    if (ret) {
        dev_err(&pdev->dev, "创建 sysfs 属性失败\n");
        return ret;
    }

    ret = device_create_file(&pdev->dev, &dev_attr_status);
    if (ret) {
        device_remove_file(&pdev->dev, &dev_attr_my_value);
        return ret;
    }

    return 0;
}

/* 在 remove 函数中删除属性文件 */
static int my_remove(struct platform_device *pdev)
{
    device_remove_file(&pdev->dev, &dev_attr_status);
    device_remove_file(&pdev->dev, &dev_attr_my_value);
    return 0;
}

使用属性组批量创建属性

c 复制代码
/* 使用属性组一次性创建多个属性文件 */
static DEVICE_ATTR(voltage,     0444, voltage_show,     NULL);
static DEVICE_ATTR(current_ma,  0444, current_show,     NULL);
static DEVICE_ATTR(temperature, 0444, temperature_show, NULL);
static DEVICE_ATTR(enable,      0644, enable_show,      enable_store);

/* 属性数组 */
static struct attribute *my_attrs[] = {
    &dev_attr_voltage.attr,
    &dev_attr_current_ma.attr,
    &dev_attr_temperature.attr,
    &dev_attr_enable.attr,
    NULL    /* 必须以 NULL 结尾 */
};

/* 属性组 */
static const struct attribute_group my_attr_group = {
    .attrs = my_attrs,
};

/* 在 probe 中一次性创建所有属性 */
ret = sysfs_create_group(&pdev->dev.kobj, &my_attr_group);

/* 在 remove 中一次性删除所有属性 */
sysfs_remove_group(&pdev->dev.kobj, &my_attr_group);

5.5.5 通过 sysfs 控制设备的实际案例

案例一:通过 sysfs 控制 LED

bash 复制代码
# 查看 LED 设备
ls /sys/class/leds/
# heartbeat  mmc0::  power

# 查看 LED 属性
ls /sys/class/leds/heartbeat/
# brightness  max_brightness  trigger  uevent

# 读取当前亮度
cat /sys/class/leds/heartbeat/brightness
# 0

# 点亮 LED(写入亮度值)
echo 1 > /sys/class/leds/heartbeat/brightness

# 熄灭 LED
echo 0 > /sys/class/leds/heartbeat/brightness

# 查看可用触发器
cat /sys/class/leds/heartbeat/trigger
# none rc-feedback kbd-scrolllock kbd-numlock kbd-capslock
# kbd-kanalock kbd-shiftlock kbd-altgrlock kbd-ctrllock
# kbd-altlock kbd-shiftllock kbd-shiftrlock kbd-ctrlllock
# kbd-ctrlrlock [heartbeat] timer oneshot

# 设置为定时器触发(闪烁)
echo timer > /sys/class/leds/heartbeat/trigger
echo 500 > /sys/class/leds/heartbeat/delay_on   # 亮 500ms
echo 500 > /sys/class/leds/heartbeat/delay_off  # 灭 500ms

案例二:通过 sysfs 查看 GPIO 信息

bash 复制代码
# 导出 GPIO(使其在 sysfs 中可见)
echo 23 > /sys/class/gpio/export

# 查看 GPIO 属性
ls /sys/class/gpio/gpio23/
# active_low  direction  edge  power  subsystem  uevent  value

# 设置为输出方向
echo out > /sys/class/gpio/gpio23/direction

# 输出高电平
echo 1 > /sys/class/gpio/gpio23/value

# 输出低电平
echo 0 > /sys/class/gpio/gpio23/value

# 设置为输入方向
echo in > /sys/class/gpio/gpio23/direction

# 读取输入电平
cat /sys/class/gpio/gpio23/value
# 1

# 取消导出
echo 23 > /sys/class/gpio/unexport

案例三:通过 sysfs 查看 I2C 设备

bash 复制代码
# 查看 I2C 总线上的设备
ls /sys/bus/i2c/devices/
# i2c-0  i2c-1  0-0048  0-0050
# 0-0048 表示:I2C 总线0,地址 0x48(LM75 温度传感器)
# 0-0050 表示:I2C 总线0,地址 0x50(AT24C02 EEPROM)

# 查看 LM75 温度传感器属性
ls /sys/bus/i2c/devices/0-0048/
# driver  hwmon  modalias  name  power  subsystem  uevent

# 通过 hwmon 接口读取温度
cat /sys/bus/i2c/devices/0-0048/hwmon/hwmon0/temp1_input
# 25500   ← 25.5°C(单位:毫摄氏度)

5.5.6 platform 总线与平台设备

platform 总线是 Linux 设备模型中最重要的虚拟总线,用于管理不能自动发现的片上外设(如 UART、SPI 控制器、GPIO 控制器等):

c 复制代码
/*
 * platform 驱动的完整框架
 */
#include <linux/platform_device.h>
#include <linux/of.h>

/* 驱动私有数据结构 */
struct my_platform_dev {
    void __iomem    *base;
    int              irq;
    struct clk      *clk;
};

/* probe 函数:设备与驱动匹配时调用 */
static int my_platform_probe(struct platform_device *pdev)
{
    struct my_platform_dev *priv;
    struct resource *res;
    int ret;

    /* 分配驱动私有数据(devm_ 自动管理) */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 从设备树获取内存资源并映射 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* 从设备树获取中断号 */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    /* 申请中断 */
    ret = devm_request_irq(&pdev->dev, priv->irq,
                           my_irq_handler, 0, "my_device", priv);
    if (ret)
        return ret;

    /* 保存私有数据 */
    platform_set_drvdata(pdev, priv);

    dev_info(&pdev->dev, "设备初始化成功,base=%p, irq=%d\n",
             priv->base, priv->irq);
    return 0;
}

/* remove 函数:设备移除时调用 */
static int my_platform_remove(struct platform_device *pdev)
{
    /* devm_ 申请的资源自动释放 */
    dev_info(&pdev->dev, "设备已移除\n");
    return 0;
}

/* 设备树匹配表 */
static const struct of_device_id my_of_match[] = {
    { .compatible = "myvendor,my-device-v1" },
    { .compatible = "myvendor,my-device-v2" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

/* platform 驱动结构体 */
static struct platform_driver my_platform_driver = {
    .probe  = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name           = "my_device",
        .owner          = THIS_MODULE,
        .of_match_table = my_of_match,
    },
};

/* 简化的模块注册宏(等价于 module_init + module_exit) */
module_platform_driver(my_platform_driver);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("Platform 驱动示例");

对应的设备树节点

dts 复制代码
/* arch/arm/boot/dts/my-board.dts */
/ {
    my_device: my-device@44E09000 {
        compatible = "myvendor,my-device-v1";
        reg = <0x44E09000 0x1000>;      /* 寄存器基地址和大小 */
        interrupts = <0 72 IRQ_TYPE_LEVEL_HIGH>; /* 中断号 */
        clocks = <&uart0_fck>;           /* 时钟 */
        clock-names = "fck";
        status = "okay";
    };
};

5.5.7 设备模型与 sysfs 的关系总结

复制代码
设备模型层次结构与 sysfs 路径的对应关系:

内核数据结构                    sysfs 路径
─────────────────────────────────────────────────────────
bus_type "platform"         →  /sys/bus/platform/
  device "my-device"        →  /sys/bus/platform/devices/my-device/
  driver "my_driver"        →  /sys/bus/platform/drivers/my_driver/

class "leds"                →  /sys/class/leds/
  device "heartbeat"        →  /sys/class/leds/heartbeat/
    attribute "brightness"  →  /sys/class/leds/heartbeat/brightness

kobject 层次(devices/):
  root                      →  /sys/devices/
    platform bus            →  /sys/devices/platform/
      my-device             →  /sys/devices/platform/my-device/
        属性文件             →  /sys/devices/platform/my-device/my_value

本章小结

章节 核心知识点 关键 API / 命令
5.1 Linux文件操作 一切皆文件;文件描述符;open/read/write/lseek/ioctl/mmap/poll 系统调用;与驱动 file_operations 的对应关系 open()ioctl()mmap()poll()
5.2 Linux文件系统 文件系统分类(磁盘/网络/虚拟/嵌入式);VFS 架构;ext4 磁盘布局;挂载命令 mountdfVFS
5.3 devfs Linux 2.4 的设备文件管理;动态创建设备文件;被废弃的原因(策略与机制混合) devfs_register()(已废弃)
5.4 udev 机制与策略分离;udev 工作流程;规则文件语法(匹配键/赋值键);class_create/device_create 触发 udev udevadmclass_create()device_create()
5.5 sysfs与设备模型 kobject/bus_type/device/driver 四大核心结构;sysfs 目录结构;DEVICE_ATTR 创建属性文件;platform 总线驱动框架 DEVICE_ATTR()sysfs_create_group()module_platform_driver()

设备文件管理的演进

复制代码
Linux 设备文件管理演进历史:

Linux 2.4 之前:静态 /dev
  → 手动 mknod 创建设备文件
  → /dev 下有数千个静态文件
  → 不支持热插拔

Linux 2.4:devfs
  → 驱动动态创建设备文件
  → 解决了静态文件问题
  → 但策略与机制混合,被废弃

Linux 2.6+:udev + sysfs + 设备模型
  → 内核只负责发现设备(机制)
  → udev 负责创建设备文件(策略)
  → sysfs 提供设备信息查询接口
  → 完整支持热插拔和电源管理
  → 这是目前的标准方案

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年