嵌入式驱动学习第三周——linux内核链表

前言

   在 Linux 内核中使用最多的数据结构就是链表了,其中就包含了许多高级思想。 比如面向对象、类似C++模板的实现、堆和栈的实现。

   嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!

目录

  • 前言
  • 1. 链表简介
    • 1.1 单链表
    • 1.2 双链表
    • 1.3 循环链表
  • 2. 内核链表
    • 2.1 list_head
    • 2.2 链表初始化
    • 2.3 增删改查
      • 2.3.1 添加节点
      • 2.3.2 删除节点
      • 2.3.3 遍历操作
    • 2.4 搬移
    • 2.5 其他
  • 3. 实际案例
    • 3.1 定义自己的结构体
    • 3.2 初始化链表
    • 3.3 增删改查函数定义
    • 3.4 调用
    • 3.5 完整代码
  • 参考资料

1. 链表简介

   链表作为常用的数据结构,是使用指针将一系列数据连成一条链,是一种线性表,通常链表数据结构至少应包括两个域,数据域和指针域。数据域用于存储数据,指针域用于建立与下一个节点的联系。相信其特性也不需要过多介绍了,大部分人应该都知道的,这部分就简单过一遍

1.1 单链表

   单链表,其仅有一个指针域指向后继节点,对单链表只能从头到顺序访问

在这里插入图片描述

1.2 双链表

   双链表多了一个指针域,又后继和前驱,既能从头到尾,又能从尾到头访问

在这里插入图片描述

1.3 循环链表

   循环链表是在单链表与双链表的基础上,让最后一个节点的指针域指向最开始的节点,双链表的话第一个节点的前驱也要指向最后一个节点。好处是可以从任意一个节点开始访问完全部的节点。

在这里插入图片描述

2. 内核链表

   在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在include/linux/list.h实现的一个相当精彩的链表数据结构。事实上,内核链表就是采用双循环链表机制,支持FIFO(先进先出)和LIFO(后进先出)操作。

   通过使用循环链表,内核开发者可以在保持代码量最小的情况下实现多种数据结构和算法,提高了内核的性能和可维护性。此外,循环双链表还具有高效地插入和删除节点的优点,在内核开发中很重要,因为内核需要频繁地对数据结构进行修改和操作。

   内核链表有别于传统链表就在节点本身不包含数据域,只包含指针域。故而可以很灵活的拓展数据结构。

2.1 list_head

   这个结构自身意义不大,但是在内核链表中,起着衔接的作用,可以说是内核链表的核心

struct list_head {struct list_head *next, *prev;
};

2.2 链表初始化

   初始化分为宏初始化和借口初始化

   宏初始化主要有LIST_HEAD_HEAD_INITLIST_HEAD

LIST_HEAD_HEAD_INIT 宏,对于任意给定的结构指针,将【前驱】和【后继】指针都指向自己,作为链表头指针
LIST_HEAD 本质就是赋予了 name有list_head属性,是基于LIST_HEAD_HEAD_INIT 宏的视线

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)

   接口初始化比较直白,INIT_LIST_HEAD和宏实现意图相同,直接将链表头指针的前驱和后继都指向自己,定义如下:

static inline void INIT_LIST_HEAD(struct list_head *list)
{list->next = list;list->prev = list;
}

   要使用list_head的话,就需要创建一个宿主结构,让其包含list_head

typedef struct my_list {int val;struct list_head list;
} My_List;

   然后我们根据这个结构体创建一个节点:

My_List first_node = {.val = 10,.list = LIST_HEAD_INIT(first_node.list);	// 将first_node.list的前驱和后继均指向自己

   即如下所示:

在这里插入图片描述

   此处list_node高能的地方又来了,我们知道container_of宏可以根据成员查找到结构体,那么通过结合container_of宏就可以通过list_node成员访问到结构体,然后再根据结构体访问其中的数值

   container_of文章地址:嵌入式驱动学习第三周——container_of()宏

2.3 增删改查

2.3.1 添加节点

   添加节点使用的是list_add函数,其是头部插入,即总是在链表的头部插入,链表是向左生长的。其底层调用的是__list_add函数。

/* * @description: 添加内核链表节点,头插法* @param-new  : 要添加的新节点* @param-head : 要加入链表的头节点* @return     : 无*/
static inline void list_add(struct list_head *new, struct list_head *head)
{__list_add(new, head, head->next);
}static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next)
{next->prev = new;new->next = next;new->prev = prev;prev->next = new;
}

   举例说明,首先可以创建一个头节点

LIST_HEAD(listHead);

在这里插入图片描述

   然后用2.2中的方法创建第一个节点

struct my_data_list first_data =
{ .val = 1,.list = LIST_HEAD_INIT(first_data.list),
};

   创建完毕后用list_add函数将其加入到头结点中

list_add(&frist_data.list, &listHead);

在这里插入图片描述

   然后创建第二个节点并添加进去,注意看第二个节点添加的位置,是在定义的头结点之后,即头部插入

struct my_data_list second_data =
{ .val = 2,/* 也可以调用接口 初始化*/.list = LIST_HEAD_INIT(second_data.list),
};list_add(&second_data.list, &listHead);

在这里插入图片描述

   另一种是使用尾插法,即在节点尾部添加节点,函数为list_add_tail函数,其也是调用的__list_add函数,但是调用的时候二三两个参数位置反了一下:

/* * @description: 添加内核链表节点,尾插法* @param-new  : 要添加的新节点* @param-head : 要加入链表的头节点* @return     : 无*/
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{__list_add(new, head->next, head);
}

   前面操作一样,来看插入第二个节点时的操作,注意看第二个节点插入的位置,是在第一个节点之后

struct my_data_list second_data =
{ .val = 2,/* 也可以调用接口 初始化*/.list = LIST_HEAD_INIT(second_data.list),
};list_add_tail(&second_data.list, &listHead);

在这里插入图片描述

2.3.2 删除节点

   删除节点采用list_del函数或list_del_init函数

/* * @description: 删除指定节点,并将节点指向特定位置* @param-entry: 要删除的节点* @return     : 无*/
static inline void list_del(struct list_head *entry)
/* * @description: 删除指定节点,删除后将节点置为空链状态,即前驱和后继均指向自己* @param-entry: 要删除的节点* @return     : 无*/
static inline void list_del_init(struct list_head *entry)

2.3.3 遍历操作

   遍历也是一个相当重要也是相当常用的操作,首先需要介绍一下list_entry宏,list_entry宏本质是container_of宏:

#define list_entry(ptr, type, member) \container_of(ptr, type, member)

   其使用方法和container_of是一致的:

list_entry(&my_list_data.list, typeof(&my_list_data), list)

   既然是双向链表,那肯定就可以正向遍历,也可以反向遍历。

   list_for_each 它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head

/* * @description: 正向遍历双向链表* @param-pos  : 一个list_node指针,作为遍历指针* @param-head : 要遍历链表的头结点* @return     : 无*/
#define list_for_each(pos, head) \for (pos = (head)->next; pos != (head); pos = pos->next)

   其使用方法如下:

struct my_data_list* p;
struct list_head *pos; //pos指向每一个大结构体中的小结构体
list_for_each(pos, &(Head->list)) //循环遍历
{//以上循环只提供小结构体指针的遍历,但是遍历找的是大结构体的数据域,根据每个pos得到大结构体地址p = list_entry(pos, Node, list);printk(p->data);  //打印 
}

   list_for_each_entry 是遍历每一个list,然后获取宿主的结构地址:

/* * @description : 正向遍历双向链表并获取宿主的结构地址* @param-pos   : 一个自己定义结构的指针,循环体内可以直接调用自己的成员变量* @param-head  : 要遍历链表的头结点* @param-member: 自己结构体中list_head的名称* @return      : 无*/
#define list_for_each_entry(pos, head, member)    \for (pos = list_entry((head)->next, typeof(*pos), member); \&pos->member != (head);  \pos = list_entry(pos->member.next, typeof(*pos), member))

   使用案例:

struct my_data_list* p;
list_for_each_entry(p, &(Head->list), list) //循环遍历,最后一个参数是list是因为我结构体中list_head成员名字叫list
{printk(pos->val);  //打印
}

   list_for_each_prev是反向遍历,用法同list_for_each

/* * @description: 反向遍历双向链表* @param-pos  : 一个list_node指针,作为遍历指针* @param-head : 要遍历链表的头结点* @return     : 无*/
#define list_for_each_prev(pos, head) \for (pos = (head)->prev; pos != (head); pos = pos->prev)

   list_for_each_entry_reverse是反向遍历,用法同list_for_each_entry

/* * @description : 正向遍历双向链表并获取宿主的结构地址* @param-pos   : 一个自己定义结构的指针,循环体内可以直接调用自己的成员变量* @param-head  : 要遍历链表的头结点* @param-member: 自己结构体中list_head的名称* @return      : 无*/
#define list_for_each_entry_reverse(pos, head, member)   \for (pos = list_entry((head)->prev, typeof(*pos), member); \&pos->member != (head);  \pos = list_entry(pos->member.prev, typeof(*pos), member))

2.4 搬移

   内核提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:头部搬移和尾部搬移。搬移的本质就是删除加插入。

/* * @description: 从一个链表删除并添加到另一个链表中,头插法添加* @param-list : 要移动的节点* @param-head : 要添加进的链表的头结点* @return     : 无*/
static inline void list_move(struct list_head *list, struct list_head *head)
/* * @description: 从一个链表删除并添加到另一个链表中,尾插法添加* @param-list : 要移动的节点* @param-head : 要添加进的链表的头结点* @return     : 无*/
static inline void list_move_tail(struct list_head *list, struct list_head *head)

2.5 其他

   还有其他操作比如合并,替换等,详情可见参考资料[1]。

3. 实际案例

3.1 定义自己的结构体

   定义一个工程师类,然后定义一个公司类,把工程师放进去,然后最外层定义一个node,数据域就是公司,指针域就是list_head变量。

// 一个公司类
typedef struct company_Struct {char name[100];int company_val;int employee_count;     // 员工人数
} Company;typedef struct my_node {// 数据域Company company;// 指针域struct list_head list;
}Node, *pNode;

3.2 初始化链表

   之后初始化一个链表,即构建一个头节点。

// 链表初始化,构建一个头节点
pNode List_Init(void) {pNode Head = (pNode)malloc(sizeof(Node));if (Head == NULL) {perror("malloc failed!");return NULL;}INIT_LIST_HEAD(&Head->list);return Head;
}

3.3 增删改查函数定义

   之后写一些增加节点,删除节点,遍历节点的函数。

   增加节点,首先先创建一个节点,然后再添加进去。

// 增加节点函数
int Node_Insert(pNode head, Company elem)
{pNode newNode = (pNode)malloc(sizeof(Node));if (newNode == NULL) {perror("malloc failed!");return -1;}newNode->company = elem;list_add(&(newNode->list), &(head->list));return 0;
}

   删除节点,在调用list_del函数后,要用free释放节点

// 删除节点
void Node_Delete(Company elem) {pNode delNode = list_entry(&elem, Node, company);list_del(&(delNode->list));free(delNode);
}

   遍历函数

// 遍历节点
void List_Traversal(pNode head) {pNode p;struct list_head *pos;list_for_each(pos, &(head->list)) {p = list_entry(pos, Node, list);printk("%s\t%d\t%d\r\n", p->company.name, p->company.company_val, p->company.employee_count);}
}

3.4 调用

Company com1 = {"aaaa", 100, 10};
Company com2 = {"bbbb", 200, 8};
Company com3 = {"cccc", 300, 40};pNode head = List_Init();        // 初始化一个列表
Node_Insert(head, com1);
Node_Insert(head, com2);
Node_Insert(head, com3);printk("+++++++所有的节点+++++++");
List_Traversal(head);Node_Delete(com2);
printk("+++++++删除部分节点+++++++");
List_Traversal(head);

3.5 完整代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/of_device.h>
#include <linux/list.h>
#include <stdlib.h>#define chrdevTest_CNT      1
#define chrdevTest_NAME     "chrdevTest"// 一个公司类
typedef struct company_Struct {char name[100];int company_val;int employee_count;     // 员工人数
} Company;typedef struct my_node {// 数据域Company company;// 指针域struct list_head list;
}Node, *pNode;// 设备结构体
struct chrdevTest_dev {dev_t devid;struct cdev cdev;struct class *class;struct device *device;int major;int minor;struct device_node *nd;
};struct chrdevTest_dev chrdevTest;   // 字符设备// 链表初始化,构建一个头节点
pNode List_Init(void) {pNode Head = (pNode)malloc(sizeof(Node));if (Head == NULL) {printk("malloc failed!");return NULL;}INIT_LIST_HEAD(&Head->list);return Head;
}// 增加节点函数
int Node_Insert(pNode head, Company elem)
{pNode newNode = (pNode)malloc(sizeof(Node));if (newNode == NULL) {printk("malloc failed!");return -1;}newNode->company = elem;list_add(&(newNode->list), &(head->list));return 0;
}// 删除节点
void Node_Delete(Company elem) {pNode delNode = list_entry(&elem, Node, company);list_del(&(delNode->list));free(delNode);
}// 遍历节点
void List_Traversal(pNode head) {pNode p;struct list_head *pos;list_for_each(pos, &(head->list)) {p = list_entry(pos, Node, list);printk("%s\t%d\t%d\r\n", p->company.name, p->company.company_val, p->company.employee_count);}
}static int chrdevTest_open(struct inode *inode, struct file *filp) {filp->private_data = &chrdevTest;return 0;
}static ssize_t chrdevTest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {return 0;
}static ssize_t chrdevTest_write(struct file *filp, const char __user* buf, size_t cnt, loff_t* offt) {return 0;
}static ssize_t chrdevTest_release(struct inode* inode, struct file* filp) {return 0;
}static struct file_operations chrdevTest_fops = {.owner = THIS_MODULE,.open = chrdevTest_open,.read = chrdevTest_read,.write = chrdevTest_write,.release = chrdevTest_release,
};// 入口函数
static int __init containerTest_init(void)
{Company com1 = {"aaaa", 100, 10};Company com2 = {"bbbb", 200, 8};Company com3 = {"cccc", 300, 40};pNode head = List_Init();        // 初始化一个列表Node_Insert(head, com1);Node_Insert(head, com2);Node_Insert(head, com3);printk("+++++++所有的节点+++++++");List_Traversal(head);Node_Delete(com2);printk("+++++++删除部分节点+++++++");List_Traversal(head);if (chrdevTest.major) {chrdevTest.devid = MKDEV(chrdevTest.major, 0);register_chrdev_region(chrdevTest.devid, chrdevTest_CNT, chrdevTest_NAME);} else {alloc_chrdev_region(&chrdevTest.devid, 0, chrdevTest_CNT, chrdevTest_NAME);     // 申请设备号chrdevTest.major = MAJOR(chrdevTest.devid);     // 获取主设备号chrdevTest.minor = MINOR(chrdevTest.devid);     // 获取次设备号}printk("chrdevTest major=%d, minor=%d\r\n", chrdevTest.major, chrdevTest.minor);// cdevchrdevTest.cdev.owner = THIS_MODULE;cdev_init(&chrdevTest.cdev, &chrdevTest_fops);// 添加cdevcdev_add(&chrdevTest.cdev, chrdevTest.devid, chrdevTest_CNT);// 创建类chrdevTest.class = class_create(THIS_MODULE, chrdevTest_NAME);if (IS_ERR(chrdevTest.class)) {return PTR_ERR(chrdevTest.class);}// 创建设备chrdevTest.device = device_create(chrdevTest.class, NULL, chrdevTest.devid, NULL, chrdevTest_NAME);if (IS_ERR(chrdevTest.device)) {return PTR_ERR(chrdevTest.device);}return 0;
}// 出口函数
static void __exit containerTest_exit(void)
{printk("this is exit function\r\n");cdev_del(&chrdevTest.cdev);unregister_chrdev_region(chrdevTest.devid, chrdevTest_CNT);device_destroy(chrdevTest.class, chrdevTest.devid);class_destroy(chrdevTest.class);
}module_init(containerTest_init);
module_exit(containerTest_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wp");

参考资料

[1] 一文搞懂 Linux 内核链表(深度分析)
[2] 【内核链表】数据结构——深入理解内核链表的概念和操作&笔记

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

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

相关文章

C#无法给PLC写入数据原因分析

一、背景 1.1 概述 C#中无法给PLC写入数据的原因有很多&#xff0c;这里分享网络端口号被占用导致无法写入的确认方法 1.2 环境 ①使用三菱PLC ②C#通过网口与PLC进行通讯 二、现象 1.1 代码 通过HslCommunication连接PLC时&#xff0c;连接返回成功&#xff0c;写入返回失败 …

snakeflow的springboot项目

Gitee搜索“liuxz/snakerflow”&#xff0c;它是spring boot集成了一款国产工作流引擎snakerflow。 下面是安装步骤&#xff1a; 创建数据库snaker-web&#xff0c;字符集设置成utf8mb4和utf8mb4_generic。不然的话&#xff0c;中文插入不进去。 运行sql命令 CREATE TABLE …

Kotlin:泛型

点击查看泛型中文文档 点击查看泛型英文文档 简介 与 Java 类似&#xff0c;Kotlin 中的类也可以有类型参数&#xff1a; class Box<T>(t: T) {var value t }一般来说&#xff0c;要创建这样类的实例&#xff0c;我们需要提供类型参数&#xff1a; val box: Box<…

调试安卓 gles性能瓶颈

目录 下载Arm Performance Studio编译Unity Shader运行malios调试用处和限制 原文请见&#xff1a;参考地址 使用mali offline shader compiler分析shader的性能瓶颈。 下载Arm Performance Studio 下载地址 编译Unity Shader 通常选择GLES3x。 You might need to select G…

智能控制:物联网智能插座对接文档

介绍 一开始买的某米的插座&#xff0c;但是好像接口不开放&#xff0c;所以找到了这个插座&#xff0c;然后自己开发了下&#xff0c;用接口控制插座开关。wifi的连接方式&#xff0c;通电后一般几秒后就会连接上wifi&#xff0c;这个时候通过接口发送命令给他。 产品图片 通…

idea配置自定义注释模版和其他模板

项目场景&#xff1a; idea配置自定义模版 自定义注释模版其他模板&#xff0c;包括syso快捷键&#xff0c;swith快捷键等 自定义注释模版 1、File and Code Templates 第一种类创建完后头部自动生成注释模板 打开idea&#xff0c;选择 Settings--> Editor--> File a…

nvm安装不同版本的node

在项目开发过程中&#xff0c;不同项目依赖的node版本不同&#xff0c;但频繁的卸载和安装很麻烦&#xff0c;这篇文章介绍nvm安装过程 1.nvm安装 这个网上随便找一篇跟着安装即可 nvm安装教程 2.nvm安装不同版本的node 网上普遍的方式是&#xff1a; 找到nvm安装目录下的s…

浅谈LockBit勒索病毒

在数字时代&#xff0c;随着科技的飞速发展&#xff0c;网络安全问题愈发凸显。恶意软件和勒索软件等网络威胁正不断演变&#xff0c;其中一款备受关注的勒索软件就是LockBit。 LockBit是一种高度复杂且具有破坏性的勒索软件。与传统的勒索软件相比&#xff0c;LockBit在其攻击…

NVMFS5A160PLZT1G汽车级功率MOSFET P沟道60 V 15A 满足AEC-Q101标准

关于汽车电子AEC Q101车规认证&#xff1f; 是一种针对分立半导体的可靠性测试认证程序&#xff0c;由汽车电子协会发布。这个认证程序主要是为了确保汽车电子产品在各种严苛的条件下能够正常工作和可靠运行。它包括了对分立半导体的可靠性、环境适应性、温度循环和湿度变化等…

新建项目module,但想归到一个目录下面

1. 想建几个module, 例如 component-base-service,component-config-service, 但是module多了会在CloudAction下面显示很多目录, 所以想把它们归到components模块下面去, 类似于下图的效果 2. 创建过程 右击CloudAction 新建 module -> 选maven类型 输入components, 建成后删…

Capture One 23:光影魔术师,细节掌控者mac/win版

Capture One 23&#xff0c;不仅仅是一款摄影后期处理软件&#xff0c;它更是摄影师们的得力助手和创意伙伴。这款软件凭借其卓越的性能、丰富的功能和前沿的技术&#xff0c;为摄影师们带来了前所未有的影像处理体验。 Capture One 23 软件获取 Capture One 23以其强大的色彩…

【C语言】Infiniband驱动mlx4_load_one函数

一、中文注释 以下是针对mlx4_load_one函数的主要代码路径的中文注释。该函数是用于加载并初始化Mellanox网络设备的驱动函数。通过注释&#xff0c;可以了解函数在初始化过程中执行的关键步骤。 /* mlx4_load_one函数&#xff1a;用于加载并初始化PCI设备&#xff08;例如网…

效果图代渲多少钱一张?带你详细了解它的计费规则!

不知道有没有朋友遇到过渲着渲着就崩溃的情况发生&#xff0c;不然也不会去找代渲染的平台/某宝等渠道 也就是为了图能够顺利的跑出来&#xff0c;做了后期处理后&#xff0c;及时交付给客户。 我们以渲染100云渲染来举例&#xff0c;它成立2015年&#xff0c;是一家效果图代…

接口自动化测试框架:Pytest+Allure+Excel

1. Allure 简介 简介 Allure 框架是一个灵活的、轻量级的、支持多语言的测试报告工具&#xff0c;它不仅以 Web 的方式展示了简介的测试结果&#xff0c;而且允许参与开发过程的每个人可以从日常执行的测试中&#xff0c;最大限度地提取有用信息。 Allure 是由 Java 语言开发…

Unity DropDown 组件 详解

Unity版本 2022.3.13f1 Dropdown下拉菜单可以快速创建大量选项 一、 Dropwon属性详解 属性&#xff1a;功能&#xff1a;Interactable此组件是否接受输入&#xff1f;请参阅 Interactable。Transition确定控件以何种方式对用户操作进行可视化响应的属性。请参阅过渡选项。Nav…

Titanic数据分析项目——Kaggle数据分析项目实战1

目前预测准确度达到77.511%, 会持续优化并且更新。 一、特征工程&#xff1a; 1、先对缺失值进行填充&#xff0c;先找到缺失值的位置&#xff0c;数值型数据填充众数&#xff0c;字符数据或者是离散型数据则填充出现最多的数据。 2、标准化数值型数据&#xff0c; 根据标准化…

Vue使用L2Dwidget

1、在根文件index.html中引入live2dw/lib/L2Dwidget.min.js 下载模型的文件&#xff0c;放在本地或者cdn 切换不同的模型 模型地址&#xff1a;https://github.com/xiazeyu/live2d-widget-models showLive2d(name: String) {var live2dWidget document.querySelector("…

专升本 C语言笔记-01 printf 占位符 转义符

目录 一.printf()函数简介 1.1作用 将格式化后的字符串输出(打印东西) 1.2函数原型 1.3返回值 二.常见占位符 2.1.占位符的使用 2.2.格式修饰符 2.3.输出格式说明 三.转义字符 一.printf()函数简介 1.1作用 将格式化后的字符串输出(打印东西) printf…

Python数值方法在工程和科学问题解决中的应用

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 随着计算机技术的不断发展&#xff0c;Python作…