【Linux】平台设备驱动

devtools/2024/10/24 22:53:11/

在设备驱动模型中,引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象,与我们SOC中物理总线的概念并不严格相等。

  • 物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
  • 驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为他们进行配对。

一般对于I2CSPIUSB这些常见类型的物理总线来说,Linux内核会自动创建与之相应的驱动总线,因此I2C设备、SPI设备、USB设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对他们进行控制并不需要特殊的时序。他们也就没有相应的物理总线,比如led、蜂鸣器和按键等,Linux内核将不会为他们创建相应的驱动总线。为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux内核引入了一种虚拟的总线–平台总线(platform_bus)

平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来进行表示,其继承了设备驱动模型中的device结构体。而平台驱动使用platform_driver结构体来进行表示,其则是继承了设备驱动模中的device_driver结构体。

平台设备

platform_device结构体
platform_device结构体(内核源码/include/linux/platform_device.h) struct platform_device {const char *name;int id;struct device dev;u32 num_resources;struct resource *resource;const struct platform_device_id *id_entry;/* 省略部分成员 */};
  • name: 设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致;
  • id: 指定设备的编号,Linux支持同名的设备,而同名设备之间则是通过该编号进行区分;
  • dev: Linux设备模型中的device结构体,linux内核大量使用了面向对象思想,platform_device通过继承该结构体可复用它的相关代码,方便内核管理平台设备;
  • num_resources: 记录资源的个数,当结构体成员resource存放的是数组时,需要记录resource数组的个数,内核提供了宏定义ARRAY_SIZE用于计算数组的个数;
  • resource: 平台设备提供给驱动的资源,如irq,dma,内存等等。该结构体会在接下来的内容进行讲解;
  • id_entry: 平台总线提供的另一种匹配方式,原理依然是通过比较字符串,这部分内容会在平台总线小节中讲,这里的id_entry用于保存匹配的结果;
何为设备信息

平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。

  • 硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源和IO口等。
  • 软件信息:以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等。

对于硬件信息,使用结构体struct resource来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下:

resource结构体(内核源码/include/linux/ioport.h)
/*
* Resources are tree-like, allowing
* nesting etc..
*/struct resource {resource_size_t start;resource_size_t end;const char *name;unsigned long flags;/* 省略部分成员 */
};
  • name: 指定资源的名字,可以设置为NULL;
  • start、end: 指定资源的起始地址以及结束地址
  • flags: 用于指定该资源的类型,在Linux中,资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型,最常见的有以下几种:
资源宏定义描述
IORESOURCE_IO用于IO地址空间,对应于IO端口映射方式
IORESOURCE_MEM用于外设的可直接寻址的地址空间
IORESOURCE_IRQ用于指定该设备使用某个中断
IORESOURCE_DMA用于指定使用的DMA通道

设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口,主要有IO端口映射和IO內存映射两种方式。 对应于IO端口映射方式,只能通过专门的接口函数(如inb、outb)才能访问; 采用IO内存映射的方式,可以像访问内存一样,去读写寄存器。在嵌入式中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM。

在资源的起始地址和结束地址中,对于IORESOURCE_IO或者是IORESOURCE_MEM,他们表示要使用的内存的起始位置以及结束位置; 若是只用一个中断引脚或者是一个通道,则它们的start和end成员值必须是相等的。

而对于软件信息,这种特殊信息需要我们以私有数据的形式进行封装保存,我们注意到platform_device结构体中, 有个device结构体类型的成员dev。在前面章节,我们提到过Linux设备模型使用device结构体来抽象物理设备, 该结构体的成员platform_data可用于保存设备的私有数据。platform_data是void *类型的万能指针, 无论你想要提供的是什么内容,只需要把数据的地址赋值给platform_data即可, 还是以GPIO引脚号为例,示例代码如下:

unsigned int pin = 10;struct platform_device pdev = {.dev = {.platform_data = &pin;}
}

将保存了GPIO引脚号的变量pin地址赋值给platform_data指针,在驱动程序中通过调用平台设备总线中的核心函数,可以获取到我们需要的引脚号。

注册/注销平台设备
platform_device_register函数(内核源码/drivers/base/platform.c)int platform_device_register(struct platform_device *pdev)

函数参数和返回值如下:

参数: pdev: platform_device类型结构体指针

返回值:

  • 成功: 0
  • 失败: 负数
platform_device_unregister函数(内核源码/drivers/base/platform.c)void platform_device_unregister(struct platform_device *pdev)

函数参数和返回值如下:

参数: pdev: platform_device类型结构体指针

返回值:

平台驱动

platform_driver结构体
platform_driver结构体(内核源码/include/platform_device.h)struct platform_driver {int (*probe)(struct platform_device *);int (*remove)(struct platform_device *);struct device_driver driver;const struct platform_device_id *id_table;.......
};
  • probe: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化。
  • remove: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是probe函数实现操作的逆过程。
  • driver: Linux设备模型中用于抽象驱动的device_driver结构体,platform_driver继承该结构体,也就获取了设备模型驱动对象的特性;
  • id_table: 表示该驱动能够兼容的设备类型。

platform_device_id结构体原型如下所示:

id_table结构体(内核源码/include/linux/mod_devicetable.h)struct platform_device_id {char name[PLATFORM_NAME_SIZE];kernel_ulong_t driver_data;};
  • name是数组用于指定驱动的名称,总线进行匹配时,会依据该结构体的name成员与platform_device中的变量name进行比较匹配
  • driver_data则是用于来保存设备的配置。我们知道在同系列的设备中,往往只是某些寄存器的配置不一样,为了减少代码的冗余, 尽量做到一个驱动可以匹配多个设备的目的
注册/注销平台驱动
platform_driver_register函数int platform_driver_register(struct platform_driver *drv);

函数参数和返回值如下:

参数: drv: platform_driver类型结构体指针

返回值:

  • 成功: 0
  • 失败: 负数
platform_driver_unregister函数(内核源码/drivers/base/platform.c)void platform_driver_unregister(struct platform_driver *drv);

参数: drv: platform_driver类型结构体指针

返回值:

平台驱动获取设备信息

platform_get_resource()函数通常会在驱动的probe函数中执行,用于获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针,该函数原型如下:

struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

参数:

  • dev: 指定要获取哪个平台设备的资源;
  • type: 指定获取资源的类型,如IORESOURCE_MEM、IORESOURCE_IO等;
  • num: 指定要获取的资源编号。每个设备所需要资源的个数是不一定的,为此内核对这些资源进行了编号,对于不同的资源,编号之间是相互独立的。

返回值:

  • 成功: struct resource结构体类型指针
  • 失败: NULL

假若资源类型为IORESOURCE_IRQ,平台设备驱动还提供以下函数接口,来获取中断引脚,

int platform_get_irq(struct platform_device *pdev, unsigned int num)

参数:

  • pdev: 指定要获取哪个平台设备的资源;
  • num: 指定要获取的资源编号。

返回值:

  • 成功: 可用的中断号
  • 失败: 负数

对于存放在device结构体中成员platform_data的软件信息,我们可以使用dev_get_platdata函数来获取,函数原型如下所示:

static inline void *dev_get_platdata(const struct device *dev)
{return dev->platform_data;
}

参数:

  • dev: struct device结构体类型指针

返回值: device结构体中成员platform_data指针

平台总线

平台总线注册和匹配方式

在Linux的设备驱动模型中,总线是最重要的一环。每当有新的设备或者新的驱动加入到总线时,总线便会调用platform_match函数对新增的设备或驱动,进行配对。内核中使用bus_type来抽象描述系统重的总线,平台总线结构体原型如下:

platform_bus_type结构体(内核源码/driver/base/platform.c)struct bus_type platform_bus_type = {.name           = "platform",.dev_groups     = platform_dev_groups,.match          = platform_match,.uevent         = platform_uevent,.pm             = &platform_dev_pm_ops,};EXPORT_SYMBOL_GPL(platform_bus_type);

内核用platform_bus_type来描述平台总线,该总线在linux内核启动的时候自动进行注册。这里重点是platform总线的match函数指针,该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线, 它都必须实例化该函数指针。

id_table匹配方式

在定义结构体platform_driver时,我们需要提供一个id_table的数组,该数组说明了当前的驱动能够支持的设备。当加载该驱动时,总线的match函数发现id_table非空, 则会比较id_table中的name成员和平台设备的name成员,若相同,则会返回匹配的条目。

示例

编译一个driver.ko和两个device.ko文件,两个device都是杂项设备,共用主设备号,动态分配不同的次设备号,共用一个驱动。

haptic_dev0.c/** @Date: 2024-10-17 16:10:22* @LastEditors: zdk* @LastEditTime: 2024-10-18 10:50:21* @FilePath: \kernel\drivers\haptics\haptic_dev0.c*/
/**  Silicon Integrated Co., Ltd haptic sih688x haptic driver file**  Copyright (c) 2021 heater <daokuan.zhu@si-in.com>** This program is free software; you can redistribute it and/or modify it* under the terms of the GNU General Public License version 2 as published by* the Free Software Foundation*/#include <linux/init.h>  //包含宏定义的头文件
#include <linux/module.h>   //包含初始化加载模块的头文件
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/platform_device.h>//定义的资源
//寄存器地址 和 大小
static struct resource haptic_dev_res[]=
{[0]= DEFINE_RES_MEM(0x01,1),[1]= DEFINE_RES_MEM(0x02,1)
};//定义私有数据
//整形数据表示状态 0,1,2等(可自定义)
static int status = 0;static void haptic_dev_release(struct device* dev)
{printk("%s\n",__func__);
}static struct platform_device haptic_dev=
{.name = "haptic_dev0",.id = 0,.num_resources = ARRAY_SIZE(haptic_dev_res),.resource = haptic_dev_res,.dev={.platform_data = &status,.release = haptic_dev_release,},
};static int __init haptic_dev_init(void)
{int ret = 0;//内核层只能使用printk,不能使用printfprintk(KERN_EMERG "%s\n",__FUNCTION__); ret = platform_device_register(&haptic_dev);return ret;
}static void __exit haptic_dev_exit(void)
{platform_device_unregister(&haptic_dev);printk(KERN_EMERG "%s\n",__FUNCTION__); 
}module_init(haptic_dev_init);//模块入口
module_exit(haptic_dev_exit);//模块出口MODULE_AUTHOR("<daokuan.zhug@si-in.com>");//声明作者信息
MODULE_DESCRIPTION("Haptics Device V1.0.0"); //对这个模块作一个简单的描述
MODULE_LICENSE("GPL v2");//声明开源许可证// "GPL" 是指明 这是GNU General Public License的任意版本// “GPL v2” 是指明 这仅声明为GPL的第二版本
haptic_dev1.c/** @Date: 2024-10-17 16:10:22* @LastEditors: zdk* @LastEditTime: 2024-10-18 10:50:28* @FilePath: \kernel\drivers\haptics\haptic_dev1.c*/
/**  Silicon Integrated Co., Ltd haptic sih688x haptic driver file**  Copyright (c) 2021 heater <daokuan.zhu@si-in.com>** This program is free software; you can redistribute it and/or modify it* under the terms of the GNU General Public License version 2 as published by* the Free Software Foundation*/#include <linux/init.h>  //包含宏定义的头文件
#include <linux/module.h>   //包含初始化加载模块的头文件
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/platform_device.h>//定义的资源
//寄存器地址 和 大小
static struct resource haptic_dev_res[]=
{[0]= DEFINE_RES_MEM(0x03,1),[1]= DEFINE_RES_MEM(0x04,1)
};//定义私有数据
//整形数据表示状态 0,1,2等(可自定义)
static int status = 1;static void haptic_dev_release(struct device* dev)
{printk("%s\n",__func__);
}static struct platform_device haptic_dev=
{.name = "haptic_dev1",.id = 1,.num_resources = ARRAY_SIZE(haptic_dev_res),.resource = haptic_dev_res,.dev={.platform_data = &status,.release = haptic_dev_release,},
};static int __init haptic_dev_init(void)
{int ret = 0;//内核层只能使用printk,不能使用printfprintk(KERN_EMERG "%s\n",__FUNCTION__); ret = platform_device_register(&haptic_dev);return ret;
}static void __exit haptic_dev_exit(void)
{platform_device_unregister(&haptic_dev);printk(KERN_EMERG "%s\n",__FUNCTION__); 
}module_init(haptic_dev_init);//模块入口
module_exit(haptic_dev_exit);//模块出口MODULE_AUTHOR("<daokuan.zhug@si-in.com>");//声明作者信息
MODULE_DESCRIPTION("Haptics Device V1.0.0"); //对这个模块作一个简单的描述
MODULE_LICENSE("GPL v2");//声明开源许可证// "GPL" 是指明 这是GNU General Public License的任意版本// “GPL v2” 是指明 这仅声明为GPL的第二版本
haptic_drv.c/** @Date: 2024-10-17 16:08:08* @LastEditors: zdk* @LastEditTime: 2024-10-18 17:18:05* @FilePath: \kernel\drivers\haptics\haptic_drv.c*/#include <linux/init.h>  //包含宏定义的头文件
#include <linux/module.h>   //包含初始化加载模块的头文件
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include "haptic_ioctl.h"//打开设备
static int haptics_open(struct inode* inode,struct file * filp)
{	//驱动最好不使用全局变量,所以在probe函数中获取的资源,该如何在这里拿到呢???//使用字符设备的时候 可以使用container_of结合inode->i_cdev拿到//但是杂项设备该怎么办呢?printk("%s minor=%d\n",__FUNCTION__, MINOR(inode->i_rdev));return 0;
}//关闭设备
static int haptics_release(struct inode* inode ,struct file* filp)
{printk("%s\n",__FUNCTION__);return 0;
}//ioctl
static long haptics_ioctl(struct file * filp, unsigned int cmd, unsigned long arg)
{int ret = 0;ioctl_protocol_t msg;//反解cmd中的字段int type = _IOC_TYPE(cmd);int dir = _IOC_DIR(cmd);int nr = _IOC_NR(cmd);int size = _IOC_SIZE(cmd);printk("dir=%d size=%d\n",dir,size);//检验cmd是否正确//1.校验cmd_typeif(DEVICE_TYPE != type){printk(KERN_ERR "cmd type error\n");ret =-1;return ret;}if(HAPTICS_READ_REG == cmd)//读寄存器{//校验nrif(HAPTICS_READ_REG_NR == nr){ret = copy_from_user(&msg, (ioctl_protocol_t __user *)arg, sizeof(ioctl_protocol_t));printk("read_reg:%#02x\n",msg.reg_addr);msg.reg_value=0xff;//这里模拟读寄存器的值//将读取到的值传给用户空间ret = copy_to_user((ioctl_protocol_t __user *)arg, &msg, sizeof(ioctl_protocol_t));}}else if(HAPTCIS_WRITE_REG == cmd)//写寄存器{//校验nrif(HAPTICS_WRITE_REG_NR == nr){ret = copy_from_user(&msg, (ioctl_protocol_t __user *)arg, sizeof(ioctl_protocol_t));printk("write_reg:%#02x=%#02x\n",msg.reg_addr,msg.reg_value);//模拟写寄存器的值}}else//{printk(KERN_ERR "unknown cmd\n");}return ret;
}typedef struct
{struct miscdevice miscdev;//定义一个杂项设备结构体struct resource* res;int status;
}haptic_miscdev_t;static int haptic_drv_probe(struct platform_device *pdev)
{int ret = 0;int* status=NULL;struct resource* res0=NULL;struct resource* res1=NULL;haptic_miscdev_t *hap_miscdev=NULL;struct file_operations *haptics_fops=NULL;res0 = platform_get_resource(pdev,IORESOURCE_MEM,0);res1 = platform_get_resource(pdev,IORESOURCE_MEM,1);printk("res0 start=%d size=%d\n",(int)res0->start,(int)(res0->end-res0->start+1));printk("res1 start=%d size=%d\n",(int)res1->start,(int)(res1->end-res1->start+1));status = dev_get_platdata(&pdev->dev);printk("status=%d\n",*status);haptics_fops = devm_kzalloc(&pdev->dev,sizeof(struct file_operations), GFP_KERNEL);haptics_fops->open = haptics_open;haptics_fops->release = haptics_release;haptics_fops->unlocked_ioctl = haptics_ioctl;hap_miscdev = devm_kzalloc(&pdev->dev,sizeof(haptic_miscdev_t), GFP_KERNEL);hap_miscdev->res = res0;hap_miscdev->status = *status;hap_miscdev->miscdev.name = pdev->name;hap_miscdev->miscdev.fops = haptics_fops;hap_miscdev->miscdev.minor = MISC_DYNAMIC_MINOR,ret = misc_register(&hap_miscdev->miscdev);/* save as drvdata *///platform_set_drvdata函数,将设备数据信息存入在平台驱动结构体中pdev->dev->driver_data中platform_set_drvdata(pdev, hap_miscdev);return ret;
}static int haptic_drv_remove(struct platform_device *pdev)
{int ret = 0;haptic_miscdev_t *hap_miscdev = platform_get_drvdata(pdev);misc_deregister(&hap_miscdev->miscdev);return ret;
}static struct platform_device_id haptic_dev_id[] =
{{.name = "haptic_dev0"},{.name = "haptic_dev1"},{}
};static struct platform_driver haptic_drv=
{.driver.name = "haptics_drv",.probe = haptic_drv_probe,.remove = haptic_drv_remove,.id_table = haptic_dev_id,
};static int __init haptic_drv_init(void)
{int ret = 0;//内核层只能使用printk,不能使用printfprintk(KERN_EMERG "%s\n",__FUNCTION__); ret = platform_driver_register(&haptic_drv);return ret;
}static void __exit haptic_drv_exit(void)
{platform_driver_unregister(&haptic_drv);printk(KERN_EMERG "%s\n",__FUNCTION__); 
}module_init(haptic_drv_init);//模块入口
module_exit(haptic_drv_exit);//模块出口MODULE_AUTHOR("<daokuan.zhug@si-in.com>");//声明作者信息
MODULE_DESCRIPTION("Haptics Device V1.0.0"); //对这个模块作一个简单的描述
MODULE_LICENSE("GPL v2");//声明开源许可证// "GPL" 是指明 这是GNU General Public License的任意版本// “GPL v2” 是指明 这仅声明为GPL的第二版本
haptic_ioctl.h#ifndef __HAPTCIS_IOCTL_H__
#define __HAPTCIS_IOCTL_H__/** @Date: 2024-10-12 15:53:37* @LastEditors: zdk* @LastEditTime: 2024-10-18 17:22:18* @FilePath: \kernel\drivers\haptics\haptic_ioctl.h*/#include <linux/ioctl.h>
// #include <sys/ioctl.h>   // 用户空间/*这里使用ioctl定义两个协议,读寄存器和写寄存器
*用户空间和内核空间共用的头文件,包含ioctl命令及相关宏定义,可以理解为一份“协议”文件
*/
//cmd中的type
#define DEVICE_TYPE           'H'
#define HAPTICS_READ_REG_NR   (0)
#define HAPTICS_WRITE_REG_NR  (1)
#define HAPTICS_READ_REG      _IO(DEVICE_TYPE,HAPTICS_READ_REG_NR)  
#define HAPTCIS_WRITE_REG     _IO(DEVICE_TYPE,HAPTICS_WRITE_REG_NR)typedef struct
{uint8_t reg_addr;uint8_t reg_value;
}ioctl_protocol_t;#endif

在这里插入图片描述
再来看看sys目录下
在这里插入图片描述
在这里插入图片描述

总结

  • 驱动代码中最好不要使用全局变量,因为驱动一般是支持多设备的,如果有全局变量会有冲突。

  • 字符设备驱动中在open函数中可以使用container_of拿到设备信息,但是杂项设备驱动中怎么拿到设备信息暂时未知。


http://www.ppmy.cn/devtools/128529.html

相关文章

云原生-降本增效最佳案例分享-学习笔记

云原生&#xff08;以技术为“内核”&#xff0c;一个核心的位置&#xff09;产业发展态势分析&#xff0c;云原生&#xff08;运维和研发测&#xff09;国内发展迅猛2021年市场的规模已经达到了千亿元&#xff0c;企业对云原生的投入和支出&#xff01;不同行业对于云原生的重…

java第三天(游戏开发)

学到 双缓存机制 public void paint(Graphics g) {// super.paint(g);//createImage(width, height) 是一个方法调用&#xff0c;它创建了一个指定宽度和高度的空图像缓冲区。这个方//法可以在 Component 类中找到&#xff0c;因此任何从 Component 继承的类&#xff08;比…

深度学习的程序实例

以下是一个使用深度学习的程序实例&#xff1a; import tensorflow as tf from tensorflow.keras.datasets import mnist from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.optimizers import RMSp…

二、Linux 入门教程:开启大数据领域的神奇之旅

Linux 入门教程&#xff1a;开启大数据领域的神奇之旅 在当今这个飞速发展的数字化时代&#xff0c;大数据所具有的重要性正日益凸显出来。而 Linux 作为一种极为强大的操作系统&#xff0c;在大数据这一广阔的领域当中发挥着至关重要、不可或缺的关键作用。倘若你怀有涉足大数…

背包九讲——完全背包问题

目录 完全背包问题 问题定义 动态规划解法 状态转移方程 初始化 遍历顺序 三种解法&#xff1a; 朴素版——枚举k 进阶版——dp正推&#xff08;一维滚动数组&#xff09; 背包问题第三讲——完全背包问题 背包问题是一类经典的组合优化问题&#xff0c;通常涉及在限定…

线程同步之双摄

如何实现两个摄像头进行同步&#xff0c;并利用同步的信号做一些事情&#xff0c; 比如stereo camera 做深度&#xff0c;如果是自己整的两个camera&#xff0c;同步就需要自己做&#xff0c; 那么这时候可以利用线程同步手写一个&#xff0c;下面给一个示例代码&#xff1a; …

Java 代码优化 修饰器模式(Decorator Pattern)

在软件设计中&#xff0c;装饰模式是一种非常有用的结构型设计模式&#xff0c;它允许你在不修改现有类的情况下&#xff0c;动态地为对象添加新的功能。这个模式通过将对象包裹在装饰器对象中&#xff0c;实现功能的扩展和增强。 装饰模式的核心思想 核心问题&#xff1a;有时…

数据结构之队列

Hello&#xff0c;各位小伙伴们上期我们学习了栈这样的数据结构&#xff0c;今天让我们一起学习一下它的孪生兄弟队列。 队列的基本概念和结构 概念&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出F…