嵌入式linux驱动

本文主要参考b站up主:一口Linux

HELLOWORLD驱动编写

程序编写步骤:

  1. 确定主设备号
  2. 定义自己的 file operations结构体
  3. 实现对应的 open/read/write等函数,填入file_operations 结构体
  4. 把 file operations结构体告诉内核:注册驱动程序
  5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
  6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
  7. 其他完善:提供设备信息,自动创建设备节点

设备号:主设备号+次设备号 函数:MKDEV(a,b)可返回设备号,a为主设备号,b为次设备号

rc = alloc_chrdev_region(&devid, 0, 1, "hello"); 用于分配设备号,主设备号由内核动态分配,此设备号由自己设计

,该代码中次设备号从0开始,共1个设备号,名称为hello。

在ubantu虚拟机中跑驱动程序

用echo指令确保架构设置和没有交叉编译,使用 unset ARCH 和 unset CROSS_COMPILE确保在x86架构下 ,写好程序后,使用 su root或者 su -进入超级用户模式,此处和教程不一样,我安装的设备只能由root用户读写。建议使用su root,因为这样可以继续保留当前文件路径,而su -会回到起始路径

  • make
  • ls 查看
  • insmod hello_drv.ko 安装驱动
  • lsmod 查看驱动,ls /dev/hello -l 驱动会生成设备节点,该操作查看设备节点
  • gcc -o hello_drv_test hello_drv_test.c 编译应用程序(测试文件)
  • ./hello_drv_test 运行应用程序
  • ./hello_drv_test -w huuu,写操作
  • sudo ./hello_drv_test -r 读操作
  • sudo rmmod hello_drv 移除驱动

LED驱动程序(驱动设计思想)

hello驱动不涉及硬件操作,而led则需要硬件操作,因此可以写一个没有具体硬件的模版,把驱动拆分为通用的框架(leddrv.c)、具体的硬件操作(board_X.c)

image-20250903143657641

韦东山在led_init函数中使用了major = register_chrdev(0, "led", &led_fops);来注册驱动,而在hello驱动中使用了alloc_chrdev_region 动态分配

寄存器的值不能被优化,因此要使用volatile。如果是软件要执行a = 1,a=2;那么可以直接把a赋值为2,但是对于 int *p = addr,想让*p=1,*p=2,不能直接优化为*p=2,如1是点灯,2是灭灯,直接优化会导致无亮灯过程,而volatile易变的意思,让编译器不要优化变量

寄存器地址虚拟映射:

ioremap */
1
2
3
4
5
6
7
8
9
10
11
// IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);

// GPIO5_GDIR 地址:0x020AC004
GPIO5_GDIR = ioremap(0x020AC004, 4);

//GPIO5_DR 地址:0x020AC000
GPIO5_DR = ioremap(0x020AC000, 4);

led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled"); /* /dev/myled */

ioremapiounmap

image-20250904150320599image-20250904150342270

符号导出

KERN_SON

  1. KERN_SOH可以设置打印级别
1
2
3
4
5
6
7
8
9
10
kernel\include\linux\kern_levels.h

#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
  1. 查看当前printk打印消息的log等级

    1
    2
    # cat /proc/sys/kernel/printk
    7 4 1 7
  2. 如何修改打印等级

    1
    2
    3
    4
    // 在驱动中使用:
    printk(KERN_INFO "Driver initialized\n");
    printk(KERN_ERR "Failed to map registers\n");
    printk(KERN_DEBUG "GPIO value: %d\n", gpio_value);

符号导出

这里的符号主要指的是全局变量和函数
Linux内核采用的是以模块化形式管理内核代码。内核中的每个模块相互之间是相互独立的,也就是说A模块的全局变量和函数,B模块是无法访问的。

符号表位置:

Ubuntu中
Linux内核的全局符号表在/usr/src/linux-headers-xxxxx-generic-pae/Module.symvers
某个单独编译的内核·根目录下

打开上述Module.symvers文件,依次为:标号、地址、全局变量或函数名称、地址、通信方式

1
<CRC> <Symbol> <Module> <Export Type>

image-20250910110239500

实例

image-20250910111735347

  1. 编译模块A,然后加载模块A,在模块A编译好后,在它的当前目录会看到一个Module.symvers文件,这里存放的就是我们模块A导出的符号。

  2. 将模块A编译生成的Module.symvers文件拷贝到模块B目录下,然后编译模块B,先加载A模块,,加载模块B。

  3. 通过dmesg查看模块打印的信息,

  4. 卸载:先卸载B,再卸载A。

模块传参

它让你可以在加载模块时(使用 insmodmodmodprobe)动态地给模块内的变量赋值,而无需修改源代码并重新编译。

其中一个典型函数:

image-20250910142041758

image-20250910142421962

其他原型:字符串、数组、描述(description)

image-20250910142532974

1
module_param_named(var_out,var,int0644);//重命名,在驱动代码内定义了var,在终端调用时,使用var_out

perm定义了模块加载后,在 /sys/module/<你的模块名>/parameters/目录下生成的参数文件的访问权限

perm常用值:0644

  • 第一个数字 0:代表特殊权限位(如 setuid, setgid, sticky bit)。在这里是 0,表示不设置任何特殊权限。

  • 第二个数字 6:代表文件所有者(owner) 的权限。

  • 第三个数字 4:代表文件所属组(group) 的权限。

  • 第四个数字 4:代表其他所有用户(others) 的权限。

这些数字是由 r(读,值为4)、w(写,值为2)、x(执行,值为1)相加得到的:

数字 计算 权限 含义
6 4 (r) + 2 (w) + 0 (x) rw- 所有者可、可
4 4 (r) + 0 (w) + 0 (x) r-- 组用户仅可
4 4 (r) + 0 (w) + 0 (x) r-- 其他用户仅可

所以,0644表示:

  • 文件所有者(通常是root):可以读取修改这个参数文件(例如 echo 1 > /sys/.../parameter)。

  • 同组用户和其他所有用户:只能读取这个参数文件的值(例如 cat /sys/.../parameter)。

    其他常见权限值示例

    • S_IRUGO(等同于 0444):只读(所有者、组、其他用户都只有读权限)。
    • S_IWUSR | S_IRUGO(等同于 0644):root可读写,其他用户只读(这就是你图片中的情况,也是最常用的配置)。
    • S_IWUSR | S_IRUSR(等同于 0600):仅root用户可读可写,其他用户无任何权限(更安全)。
    • 0完全不在sysfs中创建文件。参数只能通过 insmod命令行设置,运行时无法查看或修改。

    如何修改模块参数:在安装mod时为参数赋值

    image-20250925100744019

    更正统的做法是使用DEVICE_ATTR

sysfs

sysfs是一个位于内存中的虚拟文件系统。它由内核提供,将内核内部的数据结构、对象、它们的属性和关系,以文件和目录的形式导出到用户空间。

你可以通过挂载点 /sys来访问它

sys/module下可以查看mudule,比如hello模块

image-20250910152534978

image-20250910153331531

字符设备号

image-20250910200852865

设备号是无符号整型值,高12位:主设备号,低20位:次设备号

查看设备号:cat /proc/devices

以下三个函数可以用来查看和创建设备号

image-20250910203655042

注册设备号(静态):int register_chrdev_region(dev_t from, unsigned count, const char *name)

注册设备号(动态):

1
2
3
4
5
alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name)
•dev:输出参数。内核会把分配到的第一个设备号放在这里。
•firstminor:你希望的起始次设备号,通常设为0。
•count:要申请的设备号数量。
•name:设备名。

注销设备号:void unregister_chrdev_region(dev_t from, unsigned count)

字符设备架构

1
2
3
4
5
6
7
8
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

Cdev成员

image-20250910203655042

把自己设备所需要的操作填充到file_operations结构体

问:用户层如何找到内核态的驱动?

答:在用户态中mknod 操作(如mknod /dev/hello c 237 0)可以实现创建文件节点,文件节点的设备号存在于VFS中, 在VFS(虚拟文件系统)中Inode 存储了每个文件的静态信息,比如创建时间、读写权限等,该结构体里面包含一个变量 dev_t i_rdev 也就是设备号,通过该设备号可以找到内核态的chrdevs,char_device_struct结构体,在此结构体里面有cdev结构体,继而找到file_operations,实现读写操作。

d83012e0-3fbc-4e3d-9544-a8090c1cce12

d05ab4b9-554d-4af4-8fe7-d488a20caff3

ad0a27ad-1fcf-4765-a603-f619cf43ec25

当注册了驱动之后,f_ops就会对应到struct file中,当使用系统函数open、write时,比如在用户测试文件中调用open,此时会直接通过vfs中的struct file来找到f_ops,而不是走上面说的左边那一条路径

f68580a4-5fd9-4818-af27-3789af527b12

连续open 两次同样的文件,文件描述符fd也会不同

字符设备注册

f6be4407-ade5-4c9e-ae0a-964974459903

image-20250923095202069

内核里面有很多kobject开头的函数,属于系统架构函数,可以不用搞懂

字符设备注册–更简单的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
/​**​
__register_chrdev() - 创建并注册一个占用连续次设备号范围的字符设备(cdev)
@major: 主设备号,若为0则表示请求动态分配
@baseminor: 请求的次设备号范围的起始编号
@count: 需要申请的次设备号数量
@name: 该设备范围的名称
@fops: 与此设备关联的文件操作结构
如果 @major == 0,本函数将自动动态分配一个主设备号并返回其数值。
如果 @major > 0,本函数将尝试预留指定主设备号,成功时返回零。
失败时返回负的错误码。

void __unregister_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name)
{
struct char_device_struct *cd;

cd = __unregister_chrdev_region(major, baseminor, count);
if (cd && cd->cdev)
cdev_del(cd->cdev);
kfree(cd);
}

image-20250923142451365

字符设备ioctl接口

1
2
3
#include <sys/ioctl.h>

int ioctl(int fd, unsigned long cmd, ...);

image-20250924095714302

内核里面的函数原型:在file_operations结构体中也使用unlocked_ioctl

image-20250924100656358

调用关系:

image-20250924101050848

cmd如何填充:

image-20250924101238316

这些cmd都在内核里面宏定义了,直接拿来用就可以

image-20250924103358487

注意,序号不要和已有的冲突

如何检查命令、地址正确性 ?
可以通过宏 IOC TYPE(nr)来判断应用程序传下来的命令type是否正确;
可以通过宏 IOC DIR(nr)来得到命令是读还是写,然后再通过宏access_ok(type,addrsize)来判断用户层传递的内存地址是否合法。

1
2
3
4
5
6
7
8
9
10
11
12
if (_IOC_TYPE(cmd) != DEV_FIFO_TYPE) {
pr_err("cmd %u, bad magic 0x%x/0x%x.\n", cmd, _IOC_TYPE(cmd), DEV_FIFO_TYPE);
return -ENOTTY;
}
if (_IOC_DIR(cmd) & _IOC_READ)
ret = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
ret = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
if (ret) {
pr_err("bad access %ld.\n", ret);
return -EFAULT;
}

常通过switch语句来使用ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case MY_DEVICE_SET_BRIGHTNESS:
/* arg 被解释为一个整数 */
int brightness = (int)arg;
// ... 设置硬件的亮度 ...
break;

case MY_DEVICE_GET_CONFIG:
/* arg 被解释为一个指向用户空间内存的指针 */
struct my_config user_config;
// ... 准备数据到 user_config ...
if (copy_to_user((void __user *)arg, &user_config, sizeof(user_config))) {
return -EFAULT;
}
break;

case MY_DEVICE_SET_CONFIG:
/* arg 被解释为一个指向用户空间内存的指针 */
struct my_config new_config;
if (copy_from_user(&new_config, (void __user *)arg, sizeof(new_config))) {
return -EFAULT;
}
// ... 根据 new_config 配置硬件 ...
break;

default:
return -ENOTTY; // 未知的命令
}
return 0; // 成功
}

static struct file_operations fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = my_ioctl,
// ... 其他操作 ...
};

进程、文件描述符、file、inode、设备号关系

进程与文件描述符

  1. isof 可知某一个文件被哪些进程打开
  2. /proc/pid/fd/ 可知一个进程打开了哪些文件

image-20250924153858412

file存放动态信息,与fd一一对应

关系图如下:

当使用fd0=open("/dev/com0",O_RDWR);

  • 当应用程序调用 open("/dev/com0")时,VFS 会找到 /dev/com0对应的 inodeinode中包含了该设备的主次设备号(如 237:0)。
  • 成功打开后,内核会创建一个 struct file对象,其中包含非常重要的 f_op指针,该指针会指向驱动层提供的 file_operations结构体。后续的 read, write等调用,VFS 都会通过这个指针来调用驱动提供的具体函数。

image-20250924154709374

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
char status;
struct inode *inode = file_inode(file);
int minor = iminor(inode);

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);

/* 根据次设备号和status控制LED */
p_led_opr->ctl(minor, status);

return 1;
}

static int led_drv_open (struct inode *node, struct file *file)
{
int minor = iminor(node);

printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 根据次设备号初始化LED */
p_led_opr->init(minor);

return 0;
}

static int led_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}

注意上述代码中:open close传的是inode和file,但是read,write 只传了file,下一节会用到。

如何支持多个次设备

open(inode,file) read(file),对于设备号,inode是储存了的,但是file并没有,如何才能让read ,write找到正确的设备呢?

image-20250925110452126

open的时候会把inode放进private_data 里,以后的read,write就可以找到设备号

image-20250925113112701

private_data(私有数据指针) - 图中重点突出部分

  • 作用:这是一个 void *类型的指针。它就像是内核为驱动开发者预留的一个“万能挂钩”,其生命周期与 struct file绑定(即从 openclose)。

  • private_data只能存一个指针,因此如果设计到多个次设备,尽量定义一个dev结构体,包含每个设备的基本信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    struct xxx_dev {
    /* 1. 内核核心结构体 (必选) */
    struct cdev cdev; // 字符设备内核对象 (实现 container_of 的关键)
    struct device *device; // 设备节点对象 (用于 device_create 创建 /dev/xxx)
    dev_t dev_id; // 设备号 (主设备号 + 次设备号)
    int major; // 主设备号 (方便调试打印)
    int id; // 自定义的设备 ID (0, 1, 2...)

    /* 2. 并发控制 (必选) */
    struct mutex mutex_lock; // 互斥锁:保护长临界区 (如 read/write 里的 buffer)
    spinlock_t spin_lock; // 自旋锁:保护短临界区 (如中断里的状态位修改)
    atomic_t available; // 原子变量:用于实现“独占访问” (一次只能被一个进程打开)

    /* 3. 数据缓冲区 (业务核心) */
    unsigned char *buffer; // 动态分配的缓冲区指针 (kmalloc)
    int buf_len; // 缓冲区大小
    int current_len; // 当前已存数据的长度

    /* 4. 硬件资源映射 (硬件驱动专用) */
    void __iomem *base_addr; // 虚拟地址基地址 (ioremap 后的地址)
    void __iomem *data_reg; // 数据寄存器地址
    void __iomem *ctrl_reg; // 控制寄存器地址
    int irq_num; // 中断号
    int gpio_pin; // GPIO 引脚号

    /* 5. 同步与阻塞 (进阶) */
    wait_queue_head_t r_wait; // 读等待队列 (读数据为空时,进程在这里睡觉)
    wait_queue_head_t w_wait; // 写等待队列 (缓冲区满时,进程在这里睡觉)
    struct fasync_struct *async_queue; // 异步通知结构体 (信号驱动 IO)

    /* 6. 定时器与工作队列 (进阶) */
    struct timer_list timer; // 内核定时器 (如按键消抖)
    struct work_struct work; // 工作队列 (用于中断下半部处理)
    };
  • 对驱动开发者的意义这是驱动开发中最重要的工具之一。你可以在 open函数中为其分配内存,存储本次打开会话所需的任何数据(如设备状态、缓冲区、锁等)。在 read, write, ioctl, release等其他所有操作函数中,你都可以通过 filp->private_data来访问这些数据。

  • 优势

    • 避免全局变量:完美解决了多进程并发访问同一设备时的数据隔离问题。每个打开实例都有自己的 private_data,互不干扰。
    • 传递上下文:它是你在驱动不同操作函数之间传递信息的标准、安全的通道。

实现:

image-20250926103533467

上面代码有误:dev_fifo_read中copy_to_user应为copy_to_user(buf,&cd->test,size)

定义:

  1. 定义设备上下文结构体 (struct mydev):为每个设备实例创建一个私有数据结构,用于存储该设备独有的信息(如寄存器地址 reg、测试值 test或缓冲区、锁等)。

  2. 创建设备指针数组 (pmydev[MAX_COM_NUM]):创建一个全局指针数组,数组的每个元素将指向一个设备实例的 struct mydev。数组的大小 MAX_COM_NUM决定了该驱动最多能支持多少个设备实例。

初始化:

  1. 分配设备内存:在模块初始化函数中,使用循环为每个设备实例分配内存 (kmalloc)。

  2. 初始化每个设备:为每个设备实例的独有字段赋值(例如图中将 test字段初始化为其索引值 i)。

  3. 注册字符设备:调用 cdev_initcdev_add特别注意 cdev_add(&cdev, devno, 2)中的最后一个参数 2,它告知内核这个驱动将管理 2 个连续的次设备号(从 devno中的次设备号开始)。

运行:

  • dev_fifo_open函数:这是整个模式的关键。
    1. 识别设备:通过 MINOR(inode->i_rdev)提取出所打开设备文件的次设备号。例如,打开 /dev/mydevice0次设备号为 0,打开 /dev/mydevice1则为 1。
    2. 查找实例:用次设备号作为索引,去全局数组 pmydev中找到对应的设备实例指针:cd = pmydev[MINOR(inode->i_rdev)];
    3. 关联上下文:将找到的设备实例指针 cd存入 file->private_data。这样,在后续的所有操作函数中,都可以通过这个指针来访问到本次打开操作所对应的那个特定设备实例的数据。
  • dev_fifo_read函数:展示了如何使用存储的上下文。
    1. 获取实例:从 file->private_data中取出在 open时存储的设备实例指针。
    2. 操作特定设备:现在就可以安全地访问和操作这个特定设备的数据了(例如 cd->test)。

image-20250926110429674

用完记得kfree

多设备的另一种方法

image-20251222171659665

open(struct inode *inode, ...) 被调用时,参数 inode 里面已经包含了一个指向那个 cdev 的指针(inode->i_cdev)。

既然我都拿到肚子里的 cdev 指针了,我能不能直接算出整个 my_led_dev 的地址? 能!用 container_of 宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h> // for kmalloc, kfree
#include <linux/device.h> // for class_create, device_create

#define DEVICE_COUNT 2 // 我们要创建 2 个设备
#define DEVICE_NAME "my_smart_led"

/* 1. 定义“大结构体” (核心差异点)
* 我们把 cdev 直接嵌入到这个结构体里面,而不是分开定义
*/
struct my_led_dev {
int id; // 设备编号
int status; // 设备状态
char name[16]; // 设备名字
struct cdev cdev; // 【关键】cdev 是这个结构体的一个成员!
};

/* 为了卸载驱动时能释放内存,我们还是要记一下指针。
* 但注意:Open 函数里绝对不会用这个数组!仅供 Exit 函数打扫战场用。
*/
static struct my_led_dev *led_instances[DEVICE_COUNT];

static dev_t dev_base; // 起始设备号
static struct class *my_class; // 用于自动创建节点

/* ============================================================
* 核心函数:Open (见证奇迹的时刻)
* ============================================================ */
static int driver_open(struct inode *inode, struct file *file)
{
/* * inode->i_cdev 指向了什么?
* 它指向了我们在 Init 函数里 cdev_add 进去的那个 struct cdev 成员。
* * 我们的目标:找到包含这个 cdev 的那个大结构体 struct my_led_dev
*/

// 使用 container_of 宏进行“反推”
// 参数1: 指针 (胃)
// 参数2: 大结构体类型 (人)
// 参数3: 大结构体里成员的名字 (胃在肚子里的名字)
struct my_led_dev *dev = container_of(inode->i_cdev, struct my_led_dev, cdev);

// 【关键】把找到的大结构体存入 private_data
file->private_data = dev;

printk(KERN_INFO "[Open] 成功识别设备: %s (ID=%d)\n", dev->name, dev->id);
return 0;
}

/* Write 函数 (和方法1完全一样) */
static ssize_t driver_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
// 直接从 private_data 取出,不需要知道次设备号是几
struct my_led_dev *dev = (struct my_led_dev *)file->private_data;
int val;

if (copy_from_user(&val, buf, sizeof(int)))
return -EFAULT;

dev->status = val;
printk(KERN_INFO "[Write] 设备 %s 状态更新为: %d\n", dev->name, dev->status);

return count;
}

static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = driver_open,
.write = driver_write,
};

/* ============================================================
* 初始化函数
* ============================================================ */
static int __init driver_init(void)
{
int ret, i;

// 1. 动态申请设备号 (一次申请2个)
ret = alloc_chrdev_region(&dev_base, 0, DEVICE_COUNT, DEVICE_NAME);
if (ret < 0) return ret;

// 创建类 (用于自动创建 /dev 节点)
my_class = class_create(THIS_MODULE, "my_led_class");

// 2. 循环创建每一个设备实例
for (i = 0; i < DEVICE_COUNT; i++) {
// A. 申请内存 (造房子)
led_instances[i] = kmalloc(sizeof(struct my_led_dev), GFP_KERNEL);
if (!led_instances[i]) return -ENOMEM;

// B. 初始化私有数据
led_instances[i]->id = i;
sprintf(led_instances[i]->name, "LED_%d", i);

// C. 初始化 cdev (常规)
cdev_init(&led_instances[i]->cdev, &my_fops);
led_instances[i]->cdev.owner = THIS_MODULE;

// D. 添加到内核 (关键)
// 注意:这里我们添加的是 led_instances[i]->cdev 这个成员
// 当用户 open 时,内核拿到的就是这个成员的地址
dev_t curr_dev = MKDEV(MAJOR(dev_base), i);
cdev_add(&led_instances[i]->cdev, curr_dev, 1);

// E. 创建节点 /dev/my_smart_led0 和 /dev/my_smart_led1
device_create(my_class, NULL, curr_dev, NULL, "%s%d", DEVICE_NAME, i);
}

printk(KERN_INFO "驱动加载完成,生成了 %d 个设备\n", DEVICE_COUNT);
return 0;
}

/* ============================================================
* 退出函数
* ============================================================ */
static void __exit driver_exit(void)
{
int i;
dev_t curr_dev;

for (i = 0; i < DEVICE_COUNT; i++) {
curr_dev = MKDEV(MAJOR(dev_base), i);

// 销毁节点
device_destroy(my_class, curr_dev);

// 删除 cdev
cdev_del(&led_instances[i]->cdev);

// 释放内存
kfree(led_instances[i]);
}

class_destroy(my_class);
unregister_chrdev_region(dev_base, DEVICE_COUNT);
printk(KERN_INFO "驱动已卸载\n");
}

module_init(driver_init);
module_exit(driver_exit);
MODULE_LICENSE("GPL");

Linux中的并发机制

并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。

竟态:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竟态状态。

临界资源:多个进程访问的资源

临界区:多个进程访问的代码段

  1. 并发场合1:单CPU之间进程间的并发。时间片轮转,调度进程。A进程访问打印机时间片用完,OS调度B进程访问打印机。

  2. 并发场合2:单cpu上进程和中断之间并发。CPU必须停止当前进程的执行去执行中断,如:进程A访问串口,此时产生中断请求,此时OS必须放弃进程的执行,去执行中断处理函数,进行中断处理

  3. 并发场合3:多CPU之间:CPU1访问打印机,CPU2也访问打印机

  4. 并发场合4:单CPU上中断之间的并发:中断都有优先级,如果CPU在处理中断时候,来了一个优先级更高的中断,此时CPU就会放弃此次中断处理而去响应优先级的中断如:中断A访问串口,中断B产生,中断B优先级>中断A,CPU会立即响应中断B,而B也要访问串口资源。

Linux并发解决方案:

中断、原子操作、自旋锁、信号量、互斥体

一个有问题的并发控制

image-20251009202923970

原子操作

原子操作是指不被打断的操作,即它是最小的执行单位。
最简单的原子操作就是一条条的汇编指令(不包括一些伪指令,伪指令会被汇编器解释成多条汇编指令)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 静态定义并初始化
atomic_t my_counter = ATOMIC_INIT(0); // 初始化为0

// 动态初始化
atomic_set(&my_counter, 0); // 将原子变量设置为0
int atomic_read(atomic_t *v); // 读取原子变量的值
void atomic_set(atomic_t *v, int i); // 设置原子变量的值为 i
void atomic_inc(atomic_t *v); // v = v + 1
void atomic_dec(atomic_t *v); // v = v - 1

void atomic_add(int i, atomic_t *v); // v = v + i
void atomic_sub(int i, atomic_t *v); // v = v - i
// 以下函数返回操作后的新值

int atomic_inc_return(atomic_t *v); // v = v + 1, 然后返回 v 的新值
int atomic_dec_return(atomic_t *v); // v = v - 1, 然后返回 v 的新值

// 以下函数返回一个布尔值,用于检查操作后的结果

int atomic_inc_and_test(atomic_t *v); // v = v + 1, 然后检查结果是否为0?是则返回1,否则返回0。
int atomic_dec_and_test(atomic_t *v); // v = v - 1, 然后检查结果是否为0?常用于引用计数。

int atomic_sub_and_test(int i, atomic_t *v); // v = v - i, 然后检查结果是否为0?

互斥锁

互斥概念
信号量是在并行处理环境中对多个处理器访问某个公共资源进行保护的机制,mutex用于互斥操作。
mutex的语义相对于信号量要简单轻便一些,在锁争用激烈的测试场景下,mutex比信号量执行速度更快,可扩展性更好,另外mutex数据结构的定义比信号量小,

mutex使用注意事项:

  • 同一时刻只有一个线程可以持有mutex。
  • 只有锁持有者可以解锁,不能在一个进程中持有mutex,在另外一个进程中释放他。
  • 不允许递归地加锁和解锁。
  • 当进程持有mutex时,进程不可以退出。
  • mutex必须使用官方API来初始化。mutex可以睡眠,所以不允许在中断处理程序或者中断下半部中使用,例如tasklet、定时器等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
初始化:
静态定义:DEFINE_MUTEX(name);
动态初始化:mutex_init(&mutex);
加锁:mutex_lock(strct mutex*);
解锁:mutex_unlock(struct mutex*)
尝试获取指定的mutex,成功则返回1,否则锁被获取,返回0:mutex_trylock(struct mutex*)
如果锁已被征用,则返回1,否则返回0:mutex_is_lock(struct mutex*)

使用实例:
struct mutex mutex;
mutex_init(&mutex);/*定义*/
//加锁
mutex_lock(&mutex);

//临界区

//解锁
mutex_unlock(&mutex)

信号量

信号量又称为信号灯它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。

它的核心操作是“等待”(P操作)和“释放”(V操作)。P:如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列然后让其睡眠。
V:当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量

image-20251013100332801

简单总结:

  • Mutex:是“钥匙”,一把钥匙只对应一把锁,谁拿了钥匙谁就得负责还回来。用于互斥

  • Semaphore:是“票券”,有N张票,拿到票的人可以进场,出来时把票放回。用于控制并发数同步

    1
    2
    3
    4
    5
    初始化:
    #include <linux/semaphore.h>

    struct semaphore sem;
    sema_init(&sem,1) %数字表示信号量的值,即进程数

PV操作:

P 操作 (获取信号量)

P 操作对应 down_*系列函数,用于尝试获取一个信号量。如果信号量计数器大于0,则获取成功并递减;如果等于0,则调用者通常会进入睡眠等待。

  1. down_interruptible(struct semaphore *sem)
    • 最常用:尝试获取信号量,如果不可用则进入可中断睡眠
    • 返回值:成功获取返回 0;如果等待过程被信号(如 Ctrl+C)中断,则返回非零值(通常为 -ERESTARTSYS)。
    • 使用场景:绝大多数需要睡眠等待的情况。务必检查返回值以处理被中断的情况。
  2. down_killable(struct semaphore *sem)
    • down_interruptible类似,但只响应致命信号
    • 使用场景:适用于那些只应被致命信号(如 SIGKILL)终止的等待操作。
  3. down_trylock(struct semaphore *sem)
    • 非阻塞版本:尝试获取信号量,如果立即可用则获取并返回 0;如果不可用则立即返回非零值,而不会睡眠。
    • 使用场景:适用于不能等待或需要实现乐观锁的场景。
  4. down_timeout(struct semaphore *sem, long jiffies)
    • 带超时的等待:尝试获取信号量,如果在指定的 jiffies(内核时间单位)时间内未能获取,则超时返回。
    • 返回值:成功获取返回 0;超时返回 -ETIME
    • 使用场景:避免无限期等待,为操作设置一个最长等待时间。

关键点:所有返回 int的函数都有 __must_check属性,这意味着编译器会警告你没有检查它们的返回值。这是一个必须遵守的编程规范,否则会导致难以调试的并发问题。

• V 操作 (释放信号量)

V 操作对应 up函数,用于释放一个信号量。

  • up(struct semaphore *sem)
    • 作用:释放信号量,递增其计数器。如果发现有进程在该信号量上睡眠等待,则会唤醒其中一个。
    • 特点:该函数不会失败,且总是能立即完成。

自旋锁

内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:一个是原地等待, 一个是挂起当前进程,调度其他进程执行(睡眠)。
Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。

优点:

  • 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
  • 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

注意事项:

  • 进程拥有自旋锁的时候,该cpu上是禁止抢占的
  • 一般用于多cpu之间的资源竞争
  • 由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 — 自旋锁不应该被长时间的持有(消耗 CPU资源),一般应用在中断上下文。

加锁:spin_lock(&lock);

解锁:spin_unlock(&lock);

使用步骤:

  1. 我们要访问临界资源需要首先申请自旋锁
  2. 获取不到锁就自旋,如果能获得锁就进入临界区
  3. 当自旋锁释放后,自旋在这个锁的任务即可获得锁并进入临界区,退出临界区的任务必须释放自旋锁

使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static spinlock_t lock;
static int flage = 1;% 1表示临界区空闲 0表示被占用
spin_lock_init(&lock);

static int hello_open(struct inode *inode,struct file *filep)
{
spin_lock(&lock);
if(flage !=1)
{
spin_unlock(&lock);%防止锁一直该进程占用,若是不退出,则会导致死锁
return -EBUSY;
}
flage = 0;
spin_unlock(&lock);

return 0;
}
static int hello_release(struct inode *inode,struct file *filep)
{
spin_lock(&lock); // 获取锁,保护对 flage 的修改
flage = 1; // 标记设备为空闲
spin_unlock(&lock); // 释放锁
return 0;
}

有获取锁,必定有释放锁,无论有没有打开成功。

自旋锁-死锁

自旋锁的死锁是指一个或多个执行单元因为无法获取所需的锁而永远陷入循环等待的状态,导致系统局部或全部卡死

死锁的两种情况:

  • 拥有自旋锁的进程A在内核态阻塞了,(在持有锁的临界区内,进程 A 执行了一个可能引发阻塞(睡眠)的函数,例如:

    kmalloc(size, GFP_KERNEL);(在内存紧张时可能睡眠);copy_from_user(…);(在访问用户空间内存时可能触发缺页异常,进而可能导致睡眠);mutex_lock(…);`(互斥锁会导致睡眠)),进程 A 因此进入睡眠状态,被移出运行队列。内核调度B进程,碰巧B进程也要获得自旋锁,此时B只能自旋转。而,政此时抢占已经关闭,(单核CPU)不会调度A进程了,B永远自旋生死锁。

  • 进程A拥有自旋锁,中断到来(如,硬件中断发生(例如,网卡收到数据包、定时器到期等,并且这个中断被分配到了同一个 CPU 上处理。CPU 暂停执行进程 A,转而执行相应的中断处理程序(ISR)),CPU执行中断函数中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,只能自旋,产生死锁。

image-20251015210255168

spin_lock_irqsave做了两件事:

  1. 保存当前中断状态(Flags)。

  2. 关闭本地中断(Disable Interrupts)。

  3. 获取自旋锁

    结果: 当你用它拿锁时,中断根本进不来。既然中断进不来,就不会发生“中断抢占持锁进程”的情况,从而彻底根除了上述死锁问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /* 这种写法是通用的,在哪里用都安全 */
    void my_driver_write_data() {
    unsigned long flags; // 定义一个变量存状态,分配在栈上

    // 1. 保存状态 + 关中断 + 上锁
    spin_lock_irqsave(&my_lock, flags);

    // ... 临界区 (Critical Section) ...
    // 此时绝对安静,没有中断打扰

    // 2. 解锁 + 恢复状态
    spin_unlock_irqrestore(&my_lock, flags);
    }

同步机制总结

自旋锁和互斥体使用场合

image-20251019151608094

在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体

自旋锁忙等待,拿不到锁时,CPU 会在一个紧凑的循环中空转,不断检查锁是否被释放。这消耗CPU时间,但避免了进程切换的开销。

互斥体睡眠等待,拿不到锁时,主动放弃CPU,进入睡眠状态,虽然不消耗CPU时间,但是引入了进程切换的开销

中断上下文是不能睡眠的,所以只能使用自旋锁

持有锁需要睡眠,如果使用自旋锁会导致尝试获取锁的进程一直自旋,占用大量的CPU资源

信号量、互斥体与原子变量

互斥体和信号量很相似,内核中两者共存会全人混淆。所幸,它们的标准使用方式都有简单规范:除非mutex的某个约束妨碍你使用,否则相比信号量要优先使用mutex。
只有碰到特殊场合(一般是当你写新代码时,很底层代码)才会需要使用信号量。因此建议选mutex。如果发现不能满足其约束条件,且没有其他别的选择时,再考虑选择信号量
原子变量用在简单的变量控制上面

IO模型

I/O:input与output,针对不同的操作对象,可以分为磁盘I/O模型,网络I/O模型,内存映射I/O模型,Direct I/O和数据库I/O

48e66cab-46db-4319-87f4-d43b3daa59a1

由图可见,从系统调用的接口再往下,Linux下的IO栈致大致有三个层次:

文件系统层,以 write(2) 为例,内核拷贝了write(2)参数指定的用户态数据到文件系统Cache中,并适时向下层同步

块层,管理块设备的IO队列,对IO请求进行合并、排序(操作系统课程的IO调度算法)

设备层,通过DMA与内存直接交互,完成数据和具体设备之间的交互

结合这个图,想想Linux系统编程里用到的Buffered IO、mmap(2)、Direct IO。

9192e18c-bce0-452d-9bf2-d74db39423fc

五种I/O模型

  1. 阻塞I/O(blocking IO)

    指调用者在调用某一个函数后,一直在等待该函数的返回值,线程处于挂起状态。好比你去商场试衣间,里面有人,那你就一直在门外等着。(全程阻塞)

    63b4e18d-a2f5-4eeb-9f8e-7ae0f3fdc84c

    优点:
    1.能够及时返回数据,无延迟;2.对内核开发者来说这是省事了;3.阻塞期间不占用系统资源
    缺点:对用户来说处于等待就要付出性能的代价了

  2. 非阻塞I/O

    指调用者在调用某一个函数后,不等待该函数的返回值,线程继续运行其他程序(执行其他操作或者一直遍历该函数是否返回了值)。好比你要喝水,水还没烧开,你就隔段时间去看一下饮水机,直到水烧开为止。(复制数据时阻塞)

    63b4e18d-a2f5-4eeb-9f8e-7ae0f3fdc84c

    优点:多任务同时执行

    缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

  3. I/O多路复用

    I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个连接。比如课堂上学生做完了作业就举手,老师就下去检查作业。(对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听,可以同时对多个读/写操作的IO函数进行轮询检测,直到有数据可读或可写时,才真正调用IO操作函数。) 类比一个人钓十根鱼竿

    这种模型其实和BIO是一模一样的,都是阻塞的,只不过在socket上加了一层代理select,select可以通过监控多个socekt是否有数据,通过这种方式来提高性能。
    一旦检测到一个或多个文件描述有数据到来,select函数就返回,这时再调用recv函数(这块也是阻塞的),数据从内核空间拷贝到用户空间,recv函数返回。

    7528e759-41e4-41be-b0a3-43a9ec6aab32

  4. 信号驱动I/O

    在用户态程序安装SIGIO信号处理函数(用sigaction函数或者signal函数来安装自定义的信号处理函数),即recv函数。然后用户态程序可以执行其他操作不会被阻塞。
    一旦有数据到来,操作系统以信号的方式来通知用户态程序,用户态程序跳转到自定义的信号处理函数。
    在信号处理函数中调用recv函数,接收数据。数据从内核空间拷贝到用户态空间后,recv函数返回。recv函数不会因为等待数据到来而阻塞。
    这种方式使异步处理成为可能,信号是异步处理的基础。

    8a367da6-5024-4f71-aca3-e2b3b8305afa

    信号驱动 I/O多路复用 I/O区别

    核心区别:通知机制与轮询机制

    特性 信号驱动 I/O (Signal-Driven I/O) 多路复用 I/O (I/O Multiplexing)
    核心机制 异步通知 (Push 模型) 同步轮询 (Pull 模型)
    工作原理 内核在 I/O 就绪时主动发送信号通知应用程序。 应用程序主动调用系统函数 (select, poll, epoll_wait等) 询问内核哪些 I/O 已就绪。
    主动性 内核主动 应用程序主动
    比喻 快递员(内核)把包裹(I/O 就绪)送到你家门口,然后**按门铃(发信号)**通知你。 你(应用程序)**每隔一段时间就打电话(调用系统函数)**给快递公司(内核)问:“我的包裹到了吗?”
  5. 异步I/O
    异步IO的效率是最高的。
    异步IO通过aio_read函数实现,aio_read提交请求,并递交一个用户态空间下的缓冲区。即使内核中没有数据到来,aio_read函数也立刻返回,应用程序就可以处理其他的事情。
    当数据到来后,操作系统自动把数据从内核空间拷贝到aio_read函数递交的用户态缓冲区。拷贝完成以信号的方式通知用户态程序,用户态程序拿到数据后就可以执行后续操作。
    异步 I/O 的核心思想是:应用程序发起一个 I/O 操作后,立即返回,不会被阻塞。内核会独立完成整个 I/O 操作(例如,将数据从磁盘读取到应用程序的缓冲区),操作完成后,内核会通过某种方式通知应用程序。

    4354d6b2-2a72-4b82-b31e-c93b5526df56

等待队列 wait queue

互斥锁的睡眠功能是通过等待队列实现的!

通过 add_wait_queue()来将进程添加到waitqueue,当资源准备好了以后,由资源提供方通过wake_up()函数来唤醒等待的进程

1
2
3
4
5
6
7
8
9
定义头文件:
#include<linux/wait.h>
定义实例:
// 静态初始化
static DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);

// 或动态初始化
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

阻塞接口

阻塞接口是Linux驱动开发中“等待-通知”机制的基石。当进程请求的资源(如数据、硬件状态)尚未就绪时,内核不是让CPU空转(忙等待),而是将进程置为睡眠状态,直到条件满足后再将其唤醒。这极大地提高了系统的效率和并发能力。

特点 适用场景
wait_event(wq, condition) 不可中断睡眠。进程会一直睡眠,直到条件为真,忽略所有信号(包括Ctrl+C)。 必须完成的关键任务,不允许被用户中断。例如,对系统稳定性至关重要的硬件初始化。
wait_event_interruptible(wq, condition) 可中断睡眠。在睡眠期间,如果进程接收到信号,函数会返回-ERESTARTSYS 最常用。用于大多数设备驱动(如读取键盘、鼠标)。允许用户通过信号中断长时间等待,避免进程“卡死”。
wait_event_timeout(wq, condition, timeout) 带超时的不可中断睡眠。在指定的时间(jiffies单位)内等待条件为真,超时后无论条件如何都会返回。 需要限制最大等待时间的操作。避免因硬件故障等原因导致无限期等待。

condition为条件表达式,当wake up后,ccondition为真时,唤醒阻塞进程,为假时,继续睡眠

关键点每次被唤醒后都会重新检查条件。这是因为一次唤醒(wake_up)可能会唤醒队列中的多个进程,但资源可能只满足其中一个进程的条件。

解除阻塞接口

睡眠函数 唤醒函数 说明
wait_event wake_up 唤醒所有在等待队列上的进程(不可中断和可中断的都会唤醒)。
wait_event_interruptible wake_up_interruptible 只唤醒处于可中断睡眠的进程。这是最常用的组合。

例子:以字符设备为例,在没有数据的时候,在read函数中实现读阻塞,当向内核写入数据时,则唤醒阻塞在该等待队列的所有任务

字符设备poll方法实现

多路复用机制Select,poll,epoll

I/O多路复用就是通过一种机制个进程可以监视多人描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

Select,poll和epol是Linux API提供的I/O复用方式

重点学习poll

常用事件标志:

POLLIN**** 数据可读 普通数据或优先数据可读
POLLOUT**** 数据可写 可以无阻塞地写入数据

.poll方法实现

在linux内核源码里,有这样的用法例子

.poll是 struct file_operations中的一个函数指针,简单来说:驱动层的 .poll是应答机制。用户空间问:”这个设备现在能读吗?能写吗?”,你的驱动通过 .poll方法来回答。

1
2
3
4
5
6
7
8
9
10
\#include <linux/poll.h>

static struct file_operations my_fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
.poll = my_poll, *//* *关键:实现 poll 机制*
.open = my_open,
.release = my_release,
};

Poll方法原型:

**unsigned int (*poll) (struct file filp, struct poll_table_struct wait);

参数1: struct file *filp

作用:指向内核中代表已打开文件的file结构体的指针。

参数2: struct poll_table_struct *wait

作用:这是内核传递的”注册工具”,驱动用它来告诉内核:”当我的设备状态变化时,应该通过哪个等待队列来唤醒进程。”

**void poll_wait(struct file *filp, wait_queue_head_t wait_address, poll_table p);

参数 类型 作用
filp struct file * 当前打开的文件实例指针
wait_address wait_queue_head_t * 驱动定义的等待队列(核心参数)
p poll_table * 内核传递的 poll_table结构(来自 .poll方法的参数)

作用:注册机制–告诉内核——“如果当前没有数据,当数据就绪时,请到 wait_address这个等待队列中唤醒等待的进程。

如何实现poll方法?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static unsigned int my_poll(struct file *filp, struct poll_table_struct *wait)
{
struct my_device *dev = filp->private_data;
unsigned int mask = 0;

printk(KERN_INFO "mypoll: .poll() method called\n");

// 必须调用 poll_wait 注册等待队列(对应图片中的 poll_wait 函数)
poll_wait(filp, &dev->read_queue, wait);
poll_wait(filp, &dev->write_queue, wait);

mutex_lock(&dev->lock);

// 检查可读条件:缓冲区中有数据
if (dev->data_len > 0) {
mask |= POLLIN | POLLRDNORM; // 设置可读事件标志
printk(KERN_INFO "mypoll: data available, setting POLLIN\n");
}

// 检查可写条件:缓冲区还有空间
if (dev->data_len < BUFFER_SIZE) {
mask |= POLLOUT | POLLWRNORM; // 设置可写事件标志
printk(KERN_INFO "mypoll: space available, setting POLLOUT\n");
}

mutex_unlock(&dev->lock);

printk(KERN_INFO "mypoll: returning event mask=0x%x\n", mask);
return mask;
}

// READ方法实现(与poll配合)
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct my_device *dev = filp->private_data;
ssize_t retval = 0;
size_t to_read;

printk(KERN_INFO "mypoll: read() called, count=%zu\n", count);

// 等待数据可读(阻塞)
if (wait_event_interruptible(dev->read_queue, (dev->data_len > 0))) {
return -ERESTARTSYS;
}

mutex_lock(&dev->lock);

if (dev->data_len > 0) {
// 计算可读取的数据量
to_read = min(count, dev->data_len);

// 从内核缓冲区拷贝到用户空间
if (copy_to_user(buf, dev->buffer + dev->read_pos, to_read)) {
retval = -EFAULT;
goto out;
}

// 更新缓冲区状态
dev->read_pos = (dev->read_pos + to_read) % BUFFER_SIZE;
dev->data_len -= to_read;
retval = to_read;

printk(KERN_INFO "mypoll: read %zu bytes, remaining %zu\n",
to_read, dev->data_len);

// 数据被读取,可能有空间可写了,唤醒写等待者
wake_up_interruptible(&dev->write_queue);
}

out:
mutex_unlock(&dev->lock);
return retval;
}

信号驱动IO-SIGIO

linux命令 kill -l可以查看信号

SIGIO 是 Linux 系统中用于异步 I/O 通知的信号(Signal 29)。它允许内核在文件描述符准备好 I/O 操作时通知进程,而无需进程主动轮询。

模型 工作方式 优点 缺点
阻塞 I/O 进程睡眠等待 I/O 完成 简单 无法处理并发
轮询 (poll/select) 主动查询多个描述符 支持多路复用 CPU 占用高
SIGIO 内核主动通知进程 实时响应,低延迟 编程复杂,信号处理受限

接收到信号以后有三种操作:忽略、捕获、默认
忽略:接收到信号后不做任何反应。
捕获:用自定义的信号处理函数来执行特定的动作。
默认:接收到信号后按系统默认的行为处理该信号。这是多数应用采取的处理方式。

信号注册

1
2
3
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

image-20251027142230040

.fasync方法是字符设备的一种操作方法,放在file_operation里,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .fasync 方法实现
static int my_fasync(int fd, struct file *filp, int on)
{
struct my_device *dev = filp->private_data;

// 核心:使用内核辅助函数 fasync_helper
int retval = fasync_helper(fd, filp, on, &dev->async_queue);

if (retval < 0)
return retval;
return 0;
}
//发送信号 内核向用户空间发送SIGNO,图中第三步
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);

用户层应用程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void func(int signo)
{
printf("signo= %d\n",signo);
read(fd,buff,sizeof(buff));
printf("buff=%s\n",buff);
return ;
}

main()
{

int flage;

fd = open("/dev/hellodev",O_RDWR);
if(fd<0)
{
perror("open fail \n");
return;
}

fcntl(fd, F_SETOWN, getpid()); // 步骤1:设置接收信号的进程
flage = fcntl(fd, F_GETFL); // 步骤2:获取当前文件状态标志
fcntl(fd, F_SETFL, flage | FASYNC); // 步骤3:添加 FASYNC 标志
signal(SIGIO, func); // 步骤4:注册信号处理函数
while(1);

close(fd);
}

platform总线基础

什么是总线?

SOC上:数据总线、地址总线、控制总线,物理总线:USB\I2C,SPI,

Platform 总线是 Linux 内核中一种虚拟总线,用于管理片上系统(SoC)中无物理总线的外设控制器(如 GPIO、I2C、SPI 控制器等)

内核如何表示总线?

定义bus_type结构体变量

1
2
3
4
5
6
7
struct bus_type platform_bus_type = {
.name = "platform", // 总线名称
.dev_groups = platform_dev_groups, // 设备属性组 可不填
.match = platform_match, // 设备-驱动匹配函数
.uevent = platform_uevent, // 热插拔事件处理 可不填
.pm = &platform_dev_pm_ops, // 电源管理操作
};

platform总线三大组件

  1. Platform Device(硬件描述)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    struct platform_device {
    const char *name; // 设备名称,用于与驱动匹配
    int id; // 设备ID,用于区分同名设备
    bool id_auto; // 是否自动分配ID
    struct device dev; // 内嵌的设备基类,继承通用设备属性
    u32 num_resources; // 资源数量
    struct resource *resource; // 资源数组指针(内存、中断等)
    const struct platform_device_id *id_entry; // 设备ID表
    struct mfd_cell *mfd_cell; // MFD(多功能设备)单元指针
    struct pdev_archdata archdata; // 架构特定数据
    };

    设备树节点:
    leds: leds {
    compatible = "my-company,my-leds"; // 硬件型号
    reg = <0x10000000 0x1000>; // 寄存器地址
    interrupts = <42 IRQ_TYPE_EDGE_RISING>; // 中断信息
    };
  2. Platform Driver(驱动程序)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct platform_driver {
    int (*probe)(struct platform_device *); // 探测函数:匹配成功后调用
    int (*remove)(struct platform_device *); // 移除函数:设备断开时调用
    int (*shutdown)(struct platform_device *); // 关机函数:系统关机时调用
    int (*suspend)(struct platform_device *, pm_message_t); // 挂起函数
    int (*resume)(struct platform_device *); // 恢复函数
    struct device_driver driver; // 内嵌的驱动基类
    const struct platform_device_id *id_table; // 支持的设备ID表
    };

    设备树节点:
    static struct platform_driver my_led_driver = {
    .probe = my_led_probe, // 匹配成功后的回调
    .driver = {
    .name = "my-leds", // 直接名称匹配
    .of_match_table = my_led_of_match, // 设备树匹配
    },
    };

    // 设备树匹配表(根据compatible属性匹配)
    static const struct of_device_id my_led_of_match[] = {
    { .compatible = "my-company,my-leds" }, // 匹配设备树中的硬件
    {},
    };
  3. Platform Bus(匹配中介)

​ 自动匹配设备和驱动

image-20251029155919634

注册:

1
2
platform_device_register(&my_device);
platform_device_register(struct pltform_device *pdev)

Platform进阶

在struct platform_device中有个resource,用于描述硬件资源的核心数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct resource {
resource_size_t start; // 资源起始地址(红色标注)
resource_size_t end; // 资源结束地址(红色标注)
const char *name; // 资源名称标识
unsigned long flags; // 资源类型标志(绿色标注)
struct resource *parent, *sibling, *child; // 资源树管理指针
};
核心资源类型:flags变量的一些宏定义
#define IORESOURCE_IO 0x00000100 // I/O 端口资源
#define IORESOURCE_MEM 0x00000200 // 内存映射资源(最常用)
#define IORESOURCE_REG 0x00000300 // 寄存器偏移量
#define IORESOURCE_IRQ 0x00000400 // 中断请求资源
#define IORESOURCE_DMA 0x00000800 // DMA 通道资源
#define IORESOURCE_BUS 0x00001000 // 总线编号资源

宏ARRAY_SIZE(x)可以实现计算数组的大小,可以在定义platform_device时使用:num resources =ARRAY SIZE(res)

1. match函数何时被调用到?

2. probe函数何时被调用到

内核定时器

Linux 内核定时器(timer_list)是内核中用于延迟执行任务的核心机制,它允许你在指定的时间(基于jiffies)执行特定的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <linux/timer.h>

struct timer_list {
struct hlist_node entry; // 链表节点
unsigned long expires; // 到期时间(jiffies值)
void (*function)(struct timer_list *); // 超时回调函数
u32 flags; // 标志位
};

定义、初始化定时器:
// 方法1:静态定义(推荐)
static DEFINE_TIMER(my_timer, my_timer_callback);

// 方法2:动态初始化
struct timer_list my_timer;

void init_my_timer(void)
{
timer_setup(&my_timer, my_timer_callback, 0);
}

API:
// 初始化定时器(现代方式,替代init_timer)
void timer_setup(struct timer_list *timer,
void (*function)(struct timer_list *),
unsigned int flags);

// 添加/修改定时器(设置到期时间)
int mod_timer(struct timer_list *timer, unsigned long expires);

// 删除定时器(非安全版本)
int del_timer(struct timer_list *timer);

// 安全删除定时器(等待定时器处理完成)
int del_timer_sync(struct timer_list *timer);

jiffies值:
jiffies是 Linux 内核中最核心的时间计量单位,它记录了系统启动后经过的定时器中断次数,每次定时器中断,jiffies 值增加 1
// 内核配置的 HZ (频率)值(通常在 .config 中定义)
#define HZ 250 // 常见值:100, 250, 300, 1000
1 秒 = HZ 个 jiffies
1 jiffy = 1/HZ 秒

框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h> // Platform 总线
#include <linux/mutex.h> // 互斥体
#include <linux/spinlock.h> // 自旋锁
#include <linux/wait.h> // 等待队列
#include <linux/poll.h> // Poll
#include <linux/interrupt.h> // 中断
#include <linux/device.h>
#include <linux/slab.h> // kzalloc
#include <linux/io.h> // ioremap

#define DRIVER_NAME "my_demo_driver"
#define BUF_SIZE 1024

/* * 1. 定义“全能”设备结构体
* 这是驱动的灵魂,把所有资源打包在一起
*/
struct my_demo_dev {
/* 内核核心结构 */
dev_t dev_id; // 设备号
struct cdev cdev; // 字符设备对象
struct device *device; // 设备节点对象
struct class *class; // 类对象

/* 硬件资源 */
void __iomem *base_addr; // 寄存器虚拟基地址
int irq_num; // 中断号

/* 并发与同步 */
struct mutex mutex_lock; // 互斥锁:保护 buffer 和长临界区
spinlock_t spin_lock; // 自旋锁:保护中断上下文共享数据
wait_queue_head_t r_wq; // 读等待队列

/* 数据缓冲区 */
char kbuf[BUF_SIZE]; // 内核缓冲区
int data_len; // 当前数据长度
};

/* * 2. 中断服务程序 (ISR) - 模拟硬件产生数据
*/
static irqreturn_t my_isr_handler(int irq, void *dev_id)
{
struct my_demo_dev *dev = (struct my_demo_dev *)dev_id;

// 中断里必须用自旋锁,不能睡!
spin_lock(&dev->spin_lock);

// 模拟硬件写入数据
// 注意:实际开发中这里通常是读硬件寄存器
if (dev->data_len < BUF_SIZE) {
dev->kbuf[dev->data_len] = 'A'; // 存入一个字符
dev->data_len++;
}

spin_unlock(&dev->spin_lock);

// 唤醒在 read 里睡觉的进程
wake_up_interruptible(&dev->r_wq);

return IRQ_HANDLED;
}

/* * 3. File Operations - Open
*/
static int my_open(struct inode *inode, struct file *file)
{
// 【核心】通过 container_of 找到我们的大结构体
struct my_demo_dev *dev = container_of(inode->i_cdev, struct my_demo_dev, cdev);

// 【核心】存入 private_data,供 read/write 使用
file->private_data = dev;

return 0;
}

/* * 4. File Operations - Read (阻塞 IO)
*/
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct my_demo_dev *dev = file->private_data;
int ret = 0;

// A. 阻塞等待:如果没有数据,就睡在等待队列上
if (wait_event_interruptible(dev->r_wq, (dev->data_len > 0))) {
return -ERESTARTSYS; // 被信号打断
}

// B. 有数据了,加互斥锁进行拷贝
if (mutex_lock_interruptible(&dev->mutex_lock)) {
return -ERESTARTSYS;
}

// C. 修正读取长度
if (count > dev->data_len)
count = dev->data_len;

// D. 拷贝到用户空间
if (copy_to_user(buf, dev->kbuf, count)) {
ret = -EFAULT;
goto out;
}

// E. 模拟消费数据(清空)
dev->data_len = 0;
ret = count;

out:
mutex_unlock(&dev->mutex_lock); // 别忘了解锁!
return ret;
}

/* * 5. File Operations - Write
*/
static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
struct my_demo_dev *dev = file->private_data;
int ret = 0;

// 加互斥锁
if (mutex_lock_interruptible(&dev->mutex_lock))
return -ERESTARTSYS;

if (count > BUF_SIZE) count = BUF_SIZE;

// 拷贝用户数据到内核
if (copy_from_user(dev->kbuf, buf, count)) {
ret = -EFAULT;
goto out;
}

dev->data_len = count;
ret = count;

// 写完数据,也可以唤醒读进程(如果业务需要)
wake_up_interruptible(&dev->r_wq);

out:
mutex_unlock(&dev->mutex_lock);
return ret;
}

/* * 6. File Operations - Poll (多路复用)
*/
static __poll_t my_poll(struct file *file, struct poll_table_struct *wait)
{
struct my_demo_dev *dev = file->private_data;
__poll_t mask = 0;

mutex_lock(&dev->mutex_lock);

// 1. 挂号 (不阻塞)
poll_wait(file, &dev->r_wq, wait);

// 2. 查状态
if (dev->data_len > 0)
mask |= POLLIN | POLLRDNORM; // 可读

mutex_unlock(&dev->mutex_lock);
return mask;
}

/* 7. 关闭设备 */
static int my_release(struct inode *inode, struct file *file)
{
// 通常这里不需要做什么,除非有专门的硬件关闭操作
return 0;
}

/* 绑定 fops */
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.poll = my_poll,
.release = my_release,
};

/* * =================================================================
* 8. Platform Driver Probe (开业大吉:初始化所有资源)
* =================================================================
*/
static int my_probe(struct platform_device *pdev)
{
struct my_demo_dev *dev;
struct resource *res;
int ret;

dev_info(&pdev->dev, "开始 Probe...\n");

/* A. 申请结构体内存 (使用 devm_ 自动管理,卸载时自动释放) */
dev = devm_kzalloc(&pdev->dev, sizeof(struct my_demo_dev), GFP_KERNEL);
if (!dev) return -ENOMEM;

/* B. 初始化锁和等待队列 */
mutex_init(&dev->mutex_lock);
spin_lock_init(&dev->spin_lock);
init_waitqueue_head(&dev->r_wq);

/* C. 获取硬件资源 (Platform 特性) */
// 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
// 映射寄存器地址
dev->base_addr = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dev->base_addr)) return PTR_ERR(dev->base_addr);
}

// 获取中断号
dev->irq_num = platform_get_irq(pdev, 0);
if (dev->irq_num > 0) {
// 申请中断
ret = devm_request_irq(&pdev->dev, dev->irq_num, my_isr_handler,
IRQF_TRIGGER_RISING, DRIVER_NAME, dev);
if (ret) dev_err(&pdev->dev, "中断申请失败\n");
}

/* D. 注册字符设备 (标准流程) */
// 1. 申请设备号
ret = alloc_chrdev_region(&dev->dev_id, 0, 1, DRIVER_NAME);
if (ret < 0) return ret;

// 2. 初始化 cdev
cdev_init(&dev->cdev, &my_fops);

// 3. 添加 cdev
ret = cdev_add(&dev->cdev, dev->dev_id, 1);
if (ret < 0) goto free_devid;

/* E. 创建设备节点 (/dev/my_demo_driver) */
dev->class = class_create(THIS_MODULE, DRIVER_NAME);
dev->device = device_create(dev->class, NULL, dev->dev_id, NULL, DRIVER_NAME);

/* F. 保存 dev 指针到 pdev,方便 remove 使用 */
platform_set_drvdata(pdev, dev);

dev_info(&pdev->dev, "驱动初始化完成!\n");
return 0;

free_devid:
unregister_chrdev_region(dev->dev_id, 1);
return ret;
}

/* * 9. Platform Driver Remove (关门歇业:清理资源)
* 注意:使用了 devm_ 的资源 (内存, ioremap, irq) 会自动释放,不需要手动清理
*/
static int my_remove(struct platform_device *pdev)
{
struct my_demo_dev *dev = platform_get_drvdata(pdev);

// 销毁节点
device_destroy(dev->class, dev->dev_id);
class_destroy(dev->class);

// 删除 cdev
cdev_del(&dev->cdev);

// 释放设备号
unregister_chrdev_region(dev->dev_id, 1);

dev_info(&pdev->dev, "驱动已卸载\n");
return 0;
}

/* 10. 匹配表 (对应设备树中的 compatible) */
static const struct of_device_id my_match_table[] = {
{ .compatible = "my_company,my_demo_device" },
{ },
};

/* 11. 驱动结构体 */
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = my_match_table,
},
};

/* 12. 模块入口 */
module_platform_driver(my_driver); // 这是一个宏,替你写了 module_init/exit

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gemini");
MODULE_DESCRIPTION("A Complete Linux Platform Driver Framework");