Linux字符设备驱动设计

Linux字符设备驱动设计

概述

驱动的定义与功能
image-20200701195342174

计算机系统中存在着大量的设备, 操作系统要求能够控制和管理这些硬件, 而驱动就是帮助操作系统完成这个任务。

驱动相当于硬件的接口, 它直接操作、 控制着我们的硬件, 操作系统通过驱动这个接口才能管理硬件。

image-20200701195454778
驱动程序与应用程序

驱动程序本身也是代码, 但与应用程序不同, 它不会主动去运行, 而是被调用。 这调用者就是应用程序。

驱动与应用是服务与被服务的关系。 驱动是为应用服务的。因为应用程序很多时候需要用到硬件设备, 但不能直接操作硬件设备, 所以通过系统调用陷入内核调用驱动, 从而操作硬件。

应用与驱动程序在系统中所处位置不同,决定了它们代码运行模式也不一样。

应用程序运行在用户空间(用户态)

驱动代码运行于内核空间(内核态)。

image-20200701195627056

应用要"过五关斩六将" 才能使用硬件设备。

系统调用: 内核提供给用户程序的一组“ 特殊” 函数接口,用户程序可以通过这组接口获得内核提供的服务

驱动/库/内核/应用
image-20200701195728331

应用程序调用函数库完成一系列功能, 一部分库函数通过系统调用由内核完成相应功能, 例如: printf、 fread函数等等。

内核处理系统调用, 内核在实现系统调用时会根据需要调用设备驱动程序操作硬件。

设备驱动是硬件设备的直接控制者, 它完成了内核和硬件的通信任务

库属于用户态, 驱动属于内核态, 所以驱动无法使用标准C库里面的函数。

printf();
sprintf();
strlen();

内核实现了大部分常用的函数, 供驱动使用。 有些函数名尽管相同, 实现方式是不一样的。

printk();
sprintf();
strlen();

linux驱动相关概念

在linux世界里面, 驱动可分为三大类:

  • 字符设备
  • 块设备
  • 网络设备

字符设备

  • I/O传输过程中以字符为单位进行传输。
  • 用户对字符设备发出读/写请求时, 实际的硬件读/写操作一般紧接着发生。

块设备

  • 块设备与字符相反, 它的数据传输以块( 内存缓冲) 为单位传输。
  • 用户对块设备读/写时, 硬件上的读/写操作不会紧接着发生,即用户请求和硬件操作是异步的
  • 磁盘类、 闪存类等设备都封装成块设备。

网络设备

网络设备是一类特殊的设备, 它不像字符设备或块设备那样通过对应的设备文件访问, 也不能直接通过read或write
进行数据请求, 而是通过socket接口函数进行访问。

设备文件和主/从设备号

linux把设备抽象成文件,“一切设备皆文件” 。 所以对硬件的操作全部抽象成对文件的操作。

驱动是硬件的最直接操作者, 设备文件是用户程序与设备驱动的一个接口,应用程序通过操作设备文件来调用设备驱动程序。

image-20200701200228915

设备文件存放于/dev目录下, 可以用ls -l或ll查看

每个设备文件都有其文件属性, 属性包括:

  • 设备类型(首字母 c=字符设备 b=块设备)
  • 主/从设备号

image-20200701200304941

image-20200701200311623

应用程序通过设备文件找到设备驱动

主设备号: 用于标识驱动程序,主设备号一样的设备文件将使用同一类驱动程序。 (1-254)

从设备号: 用于标识使用同一驱动程序的不同具体硬件。(0-255)

例如: 210开发板中的串口设备, 主设备号标识串口这类设备, 从设备号标识具体的某个串口。

# 查看当前系统中主设备号的使用情况和其对应的硬件设备
cat /proc/devices

linux模块编程

Linux内核抛弃把所有功能模块都编译到内核的做法, 采用了模块化的方法将各组件灵活添加和删减, 并且驱动模块还可以动态加载、 删除。

使用模块的好处:

  • 内核体积小: 不需要的组件可以不编入内核
  • 开发灵活: 模块可以同普通软件一样, 从内核中添加或删除
  • 平台无关、 节省内存
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>/*模块加载函数*/
int __init xxx_module_init (void) 
{ //... 
}/*模块卸载函数*/
void __exit xxx_module_exit (void) 
{ //... 
}
/*声明模块加载函数宏*/
module_init(xxx_module_init);/*声明模块卸载函数宏*/
module_exit(xxx_module_exit);/*声明模块作者*/
MODULE_AUTHOR(“sunplusedu” );/*模块许可证明, 描述内核模块的许可权限*/
MODULE_LICENSE("GPL");

模块加载函数

  • 完成相关资源申请、 硬件初始化以及驱动的注册
  • 若初始化成功返回0, 失败返回错误值
  • 模块加载函数必须以" module_init(函数名) "形式进行声明

模块卸载函数:

  • 释放已申请资源、 注销驱动
  • 在模块卸载时被执行, 不返回任何值
  • 函数需要以" module_exit(函数名) "的形式进行声明

Linux内核模块的编译方法有两种:

  • 放入Linux内核源码中编译
  • 采用独立的方法编译模块

放入Linux内核源码中编译

  • 将写好的模块放入Linux内核任一目录下
  • 修改相应目录下的Kconfig和Makefile文件
  • 执行make modules
  • 会在相同目录下生成与源文件同名的.ko文件

采用独立的方法编译模块

linux内核还提供了一种方法可以独立编译模块, 我们可以在自定义的目录下编译驱动程序, 其makefile内容如下:

#this is a makefile
ifeq ($(KERNELRELEASE),3.4.39)
obj-m := module_test.o #模块名字,与C文件同名
else
KERNELDIR = /…/kernel-3.4.39 #内核路径
PWD = $(shell pwd) #当前路径
default: #编译过程$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:rm -rf *.ko
endif
image-20200702084208138

Linux内核模块的使用:

lsmod 			# 列举当前系统中的所有模块
insmod xxx.ko 	# 加载指定模块到内核
rmmod xxx 		# 卸载指定模块(不需要.ko后缀)

注意事项:

若卸载时出现以下提示:

rmmod:chdir(3.4.39-sunplusedu): No such file or directory

在开发板根文件系统下建立以下目录:

/lib/modules/3.4.39-sunplusedu(跟当前内核版本同名)

字符驱动程序框架

模块是linux内核进行组件管理的一种方式, 驱动是基于模块进行注册和注销的。

不单单是字符设备, 块设备驱动和网络设备驱动都是基于模块进行加载和删除的。

字符设备是最基本、 最常用的设备。 它将千差万别的各种硬件设备采用一个统一的接口封装起来, 屏蔽硬件差异, 简化了应用层的操作。

如: 按键 , LED灯 , 触摸屏 , 温湿度传感器

应用程序的open、 read、 write函数最终会调到驱动里面的open、 read、 write函数

与系统调用函数类似, 驱动的这几个函数参数都是固定不变的, 但函数名可以自己编写

这几个函数是由我们在驱动代码上去实现的, 也就是说, read/write函数要对硬件进行怎样的操作是驱动决定的

file_operations结构体

  • 驱动的open/read/write函数实际上是由一个叫 file_operations的结构体统一管理的。
  • 这是字符驱动最重要的一个结构体(之一), 里面包含了一组函数指针。 这组函数指针指向驱动open/read/write等几个函数。
  • 一个打开的设备文件就和该结构体关联起来, 结构体中的函数实现了对文件的系统调用, 这样file_operations中的函数就 和open/read/write等系统调用函数一一对应
//  include/linux/fs.h
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 *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iopoll)(struct kiocb *kiocb, bool spin);int (*iterate) (struct file *, struct dir_context *);int (*iterate_shared) (struct file *, struct dir_context *);__poll_t (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);unsigned long mmap_supported_flags;int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endifssize_t (*copy_file_range)(struct file *, loff_t, struct file *,loff_t, size_t, unsigned int);loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,struct file *file_out, loff_t pos_out,loff_t len, unsigned int remap_flags);int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

file_operations 常用函数

struct file_operations {struct module *owner;ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);int (*open) (struct inode *, struct file *);int (*release) (struct inode *, struct file *);
}

当执行insmod命令插入一个设备驱动时, 相应的模块初始化函数被执行:

在初始化函数中根据需要申请资源和初始化。

  • 中断、 内存等资源申请
  • IO口等硬件初始化

利用register_chrdev()把驱动注册进内核

条件:

  • 主设备号
  • 设备名字
  • 填充好的file_operations结构体

字符设备注册函数

/*** @function: 字符设备注册* @parameter: *		major: 主设备号, 一般填0, 由内核自动分配*		name: 设备驱动名, 注册成功可以使用 , cat /proc/device 查看*		fops: 填充好的file_operations 结构体变量地址* @return: *     success: 分配好的主设备号*     error: * @note: */
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

字符设备注销函数

/*** @function: 字符设备注销* @parameter: *		major: 主设备号*		name: 设备驱动名* @return: *     success: *     error: * @note: */
void unregister_chrdev(unsigned int major, const char name)

字符设备驱动编译成功生成.ko文件后, 使用字符驱动

加载驱动程序

  • insmod 内核模块文件名.ko
  • cat /proc/devices 查看当前系统中所有设备驱动程序及其主设备号

手动建立设备文件

  • 设备文件一般建立/dev目录下, 可通过命令创建
  • mknod /dev/文件名 c 主设备号 从设备号

采用mknod命令的方式创建设备文件有些繁琐, 就此linux为我们提供了一种udev机制实现设备文件的自动创建(只需在模块初始化函数中添加几行代码) 。

该方式的思想是使用insmod 插入模块时自动创建设备节点, 使用rmmod 卸载模块时删除设备节点

udev是linux2.6内核引入的设备管理器, 主要负责管理 /dev下设备节点

自动创建功能的实现分两步完成

首先定义和创建设备类:

struct class *my_class;
//name: 设备类名
my_class = class_create(THIS_MODULE, name);

根据类创建设备节点:

struct device *my_device;
//MKDEV : 主/次设备号组成一个设备号
//name: 设备节点名
my_device = device_create(my_class, NULL, MKDEV(major_nr, minor_nr), NULL, name);

卸载过程也分为两步完成

首先删除设备节点:

device_destroy(my_class, MKDEV(major_nr, minor_nr));

销毁创建好的设备类:

class_destroy(my_class);

inode结构体

inode结构体记录了文件系统中文件的相关信息, 比如 文件大小、 创建者、 创建日期等等, 我们也称之为“索引节点” , 每个文件都有与之对应的inode, 也就是说 inode表示具体的文件。

image-20200702091458397

file结构体

file结构体是一个内核结构, 它不会出现在用户程序中, 它跟用户空间FILE不是一回事

file结构体代表一个打开的文件(文件描述符) ,在open时它由内核自动创建, 并传递给file_operations所指向的各个函数, 当文件关闭后该结构体被释放。

该结构体记录了文件的读写模式、 文件当前读写位置、 还有file_operations结构体的指针等

image-20200702091603327

inode结构体中包含了设备文件的主从设备号,用户可通过以下宏获取:

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

用户态与内核态数据的传输

应用程序与驱动程序分属于不同的地址空间, 二者之间的数据应当采用以下函数进行交换

//从内核空间拷贝n字节数据到用户空间
//user_buffer: 标首地址
//kernel_buffer: 源数据首地址
copy_to_user(user_buffer, kernel_buffer, n)
//从用户空间拷贝n字节数据到内核空间
copy_from_user(kernel_buffer, user_buffer, n)
//从内核空间拷贝一数据( 任意类型) 变量到用户空间
put_user(kernel_value, user_buffer)
//从用户空间拷贝一数据( 任意类型) 变量到内核空间
get_user(kernel_value, user_buffer)
//buf :用户传过来的数据
//count: 数据的大小
ssize_t demo_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{unsigned char aver[100] = {0};if(copy_to/from_user(aver, buf, count) != 0){return -EFAULT;}return count;
}

GPIO程序设计

S5P6818 GPIO寄存器

GPIOxOUTGPIOxOUTENBGPIOxPADGPIOxALTFNn
输出高/低引脚输入/输出读输入高/低电平引脚功能(gpio或其 他固定功能)
每1位配置1个 gpio引脚每1位配置1个 gpio引脚每1位读取1个 gpio引脚每2位配置1个 gpio引脚

键盘工作原理

3*3键盘可采用轮训方式检测, 配置GPIOA_28、GPIOC_11和GPIOC_12为输入且上拉使能, 然后就是while循环读取输入引脚

image-20200702103032671

配置IO口相关寄存器的两种实现方法:

直接操作gpio相关寄存器

  • 和裸机程序gpio配置方式类似, 通过移位操作读写寄存器的值

使用内核的io操作接口函数

  • linux内核为方便用户对io口的配置提供了一系列的接口函数,而且我们建议使用该方式, 可以提高代码可移植性
//操作gpio相关寄存器(地址映射)
//头文件
#include <asm/io.h>
#include <linux/ioport.h>unsigned int reg;//寄存器虚拟首地址(地址映射)
//映射后的寄存器地址排列顺序和间隔同映射前一致, 因此虚拟首地址+ 偏移量即可得到具体寄存器地址
gpioa_base = ioremap(0xC001A000, 0x64);//读GPIOAOUTENB寄存器
reg = readl(gpioa_base + 4);//写GPIOAOUTENB寄存器
writel(reg, gpioa_base + 4);
//linux内核提供了一组函数来操作IO口寄存器, 驱动程序员可直接使用
#include <plat/gpio-cfg.h>
#include <mach/gpio.h>//LED硬件连接
//GPIO 引脚A30/B00/B10/A8 分别连接 led 1/2/3/4//LED的IO初始化
//GPIOxALTFNn: 配置为GPIO功能
//GPIOxOUTENB: 配置为输出功能//点亮LED
//GPIOxOUT: 输出高电平
LED驱动
file_operations
open
release
read
write_
获取用户传来命令
获取LED对应IO口
init
注册字符设备
生成设备节点
exit
设备文件删除
注销字符设备
编译调试
编译makefile
调试

ioctl驱动接口实现

通过设备驱动程序执行各种类型的硬件控制, 而ioctl可以对此支持

ioctl中的cmd与arg参数都是由程序员自行规定的,具有很高的灵活性

int (*unlocked_ioctl)(struct file *,unsigned int, unsigned long);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/326608.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

升级Microsoft 365后,SAP GUI中无法打开Excel的解决方案

最近&#xff0c;我们遇到了一个棘手的问题&#xff0c;一位客户在升级到Microsoft 365后&#xff0c;无法在SAP GUI中打开Excel。这个问题不仅影响了工作效率&#xff0c;也给用户的日常操作带来了不便。在本文中&#xff0c;我们将探讨问题的成因&#xff0c;并提供一种解决方…

gin自定义验证器+中文翻译

gin自定义验证器中文翻译 1、说明2、global.go3、validator.go4、eg&#xff1a;main.go5、调用接口测试 1、说明 gin官网自定义验证器给的例子相对比较简单&#xff0c;主要是语法级别&#xff0c;便于入门学习&#xff0c;并且没有给出翻译相关的处理&#xff0c;因此在这里记…

PaddleOCR_PP-Structure

静态IP设置 # 修改网卡配置文件 vim /etc/sysconfig/network-scripts/ifcfg-ens33# 修改文件内容 TYPEEthernet PROXY_METHODnone BROWSER_ONLYno BOOTPROTOstatic IPADDR192.168.15.132 NETMASK255.255.255.0 GATEWAY192.168.15.2 DEFROUTEyes IPV4_FAILURE_FATALno IPV6INIT…

YOLOv8小白中的小白安装环境教程!没一个字废话,看一遍不踩坑!

文章目录 去哪里下代码?怎么下代码?怎么装环境?命令行界面(CLI)指令和Python脚本区别?附录1 conda常用指令附录2 git常用指令附录3 项目代码文件作用去哪里下代码? 下载代码请大家直接去 YOLOv8的官方仓库下载,名字叫 ultralytics,有些镜像网站和个人发的等来历不明的代…

CMakeLists.txt语法规则:foreach 循环基本用法

一. 简介 cmake 中除了 if 条件判断之外&#xff0c;还支持循环语句&#xff0c;包括 foreach()循环、while()循环。 本文学习 CMakeLists.txt语法中的循环语句。 CMakeLists.txt语法中 有两种 循环实现方式&#xff1a;foreach循环与 while循环。 二. CMakeLists.txt语法规则…

解决 SyntaxError: Unexpected token ‘.‘ 报错问题

这个报错一般是编译问题&#xff0c;浏览器的版本过低没通过代码 解决办法&#xff1a; 在package.json文件中加上这个 "browserslist": ["> 1%","last 2 versions","not dead","not ie < 6","Android > 4&…

AI算法工程师课程学习-数学基础-高数1-微积分

基础数学学习目的&#xff1a;为机器学习(尤其是算法)和深度学习打基础。 学习路线&#xff1a;1.高中数学-->大学2.微积分-->3.线性代数-->4.概率论-->5.优化理论。&#xff08;因为自己40多岁年龄了&#xff0c;做了7年的.net开发&#xff0c;后面都一直在做软件…

工器具管理(基于若依)

文章目录 前言一、工器具管理项目总览 二、入库功能1. 前端1.1 界面展示1.2 具体操作实现1.3 js文件 2. 后端2.1 工器具信息回显2.2 工器具入库 三、领用功能1. 前端1.1 界面展示1.2 具体实现操作1.3 js文件 2. 后端2.1 工器具信息回显2.2 工器具领用 遇到的问题1. 同一页面展示…

2024 年中国大学生程序设计竞赛全国邀请赛(郑州)暨第六届CCPC河南省大学生程序 设计竞赛Problem L. Toxel 与 PCPC II

//sort bug下标 遍历dp. //没修负的bug肯定连续 #include<bits/stdc.h> using namespace std; #define int long long const int n1e611; int a,b,c[n],dp[n]; signed main() {ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>a>>b;for(int i1;…

【JVM】从可达性分析,到JVM垃圾回收算法,再到垃圾收集器

《深入理解Java虚拟机》[1]中&#xff0c;有下面这么一段话&#xff1a; 在JVM的各个区域中&#xff0c;如虚拟机栈中&#xff0c;栈帧随着方法的进入和退出而有条不紊的执行者出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的&#xff08;尽管在…

抖音上有可以长久赚钱的副业吗?当然有,只有它最稳定长久!

大家好&#xff0c;我是电商糖果 现在有很多年轻人在大城市上班&#xff0c;发现辛辛苦苦一年也赚不到多少钱。 如果说自己有了房贷&#xff0c;车贷&#xff0c;那更是一点儿不敢歇。 为了可以有更多的收入&#xff0c;年轻人都希望可以靠着下班时间&#xff0c;找一个可以…

[前后端基础]图片详解

[前后端基础]图片传输与异步-CSDN博客 https://juejin.cn/post/6844903782959022093#heading-3 base64、file和blob用JS进行互转的方法大全【前端】_js base64转blob-CSDN博客 后端存储方式 对于第一种存储方式&#xff0c;我们前端直接将存储路径赋值给 src 属性即可轻松显示。…

在go-zero中使用jwt

gozero使用jwt 两个步骤 获取token验证token 前端获取token 先编写 jwt.api 文件&#xff0c;放在api目录下 syntax "v1"info (title: "type title here"desc: "type desc here"author: "type author here"email: &quo…

【KMP算法最详细讲解】28. 实现 strStr()

实现 strStr() 函数。 给定一个 haystack 字符串和一个 needle 字符串&#xff0c;在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在&#xff0c;则返回 -1。 示例 1: 输入: haystack "hello", needle "ll" 输出: 2 示…

C++进阶:哈希(1)

目录 1. 简介unordered_set与unordered_map2. 哈希表&#xff08;散列&#xff09;2.1 哈希表的引入2.2 闭散列的除留余数法2.2.1 前置知识补充与描述2.2.2 闭散列哈希表实现 2.3 开散列的哈希桶2.3.1 结构描述2.3.2 开散列哈希桶实现2.3.3 哈希桶的迭代器与key值处理仿函数 3.…

Spring框架概述

目录 1. Spring框架的起源 2. Spring框架的构成 3. Spring的发展历程 4. Spring的开发环境 4.1. Maven安装与配置 &#xff08;1&#xff09;Maven的下载与安装 &#xff08;2&#xff09;配置Maven的环境变量 &#xff08;3&#xff09;本地仓库的配置 &#xff08;4…

WIFI模块的AT指令联网数据交互--第十天

1.1.蓝牙&#xff0c;ESP-01s&#xff0c;Zigbee, NB-Iot等通信模块都是基于AT指令的设计 初始配置和验证 ESP-01s出厂波特率正常是115200, 注意&#xff1a;AT指令&#xff0c;控制类都要加回车&#xff0c;数据传输时不加回车 1.2.上电后&#xff0c;通过串口输出一串系统…

如何使用dockerfile文件将项目打包成镜像

要根据Dockerfile文件来打包一个Docker镜像&#xff0c;你需要遵循以下步骤。这里假设你已经安装了Docker环境。 1. 准备Dockerfile 确保你的Dockerfile文件已经准备就绪&#xff0c;并且位于你希望构建上下文的目录中。Dockerfile是一个文本文件&#xff0c;包含了用户可以调…

绘制一个单级放大电路原理图过程,保姆级教程

新手在学习pads的使用最好最快的方法就是实际上手去画原理图&#xff0c;画PCB图&#xff0c;在这个过程中&#xff0c;就能够更快速得掌握PADS软件的使用。 本篇就是对于实际画原理图过程的一个记录&#xff0c;手把手教学&#xff0c;如果有纰漏或者有更好的一些技巧&#xf…

thinkphp6使用layui分页组件做分页效果

博主用的是layui2.9.8的版本&#xff0c;但这个版本的分页组件是动态效果的&#xff0c;但我需要的是静态分页&#xff0c;所以我自己封装了一个生成layui的分页代码生成代码。代码如下&#xff1a; 1、先创建文件&#xff0c;路径是extent/layui/LayuiPage.php&#xff0c;加…