Android12 简单的共享内存驱动实现 参考Ashmem

Android12 共享内存驱动实现

SOC:RK3568

system:Android12

概述:

1. 概述
Ashmem(Anonymous Shared Memory,Android 匿名共享内存),它基于 mmap 系统调用,可以让不同进程将同一段物理内存映射到各自的虚拟地址中,从而实现内存共享。

它以驱动程序的形式在内核空间中实现,并通过文件描述符来传递共享内存的句柄。

工作上要使用Ashmem,但是对于C++来讲使用不方便,因为涉及到fd文件描述符传递 。于是想参考google 的Ashmem,设计一个简单的进程只通过操作文件即可获取共享的驱动

逻辑原理如下:

主要代码:

 //结构体MyAshmem_area 用来描述一块匿名共享内存

struct MyAshmem_area {
	char name[MYASHMEM_FULL_NAME_LEN];//匿名
	struct list_head unpinned_list;//暂时无用
	//每一块匿名共享内存都会在临时文件系统tmpfs中对应一个文件,也就是file
    struct file *file;
	size_t size;//文件的大小 size
	unsigned long prot_mask;//    prot_mask是访问保护位
};

struct MyAshmem_area *asma;

开机启动Myshmem驱动:

static int __init MyAshmem_init(void)
{
	int ret = -ENOMEM;
	 //创建一个使用slap缓存 用于分配 MyAshmem_area 的分配器
	MyAshmem_area_cachep = kmem_cache_create("ashmem_area_cache",
					       sizeof(struct MyAshmem_area),
					       0, 0, NULL);
	if (!MyAshmem_area_cachep) {
		pr_err("failed to create slab cache\n");
		goto out;
	}
    //注册匿名共享内存设备
	ret = misc_register(&MyAshmem_misc);
	if (ret) {
		pr_err("failed to register misc device!\n");
		goto out_free1;
	}
	pr_info("initialized\n");
	return 0;

out_free1:
	kmem_cache_destroy(MyAshmem_area_cachep);
out:
	return ret;
}
注册时调用
device_initcall(MyAshmem_init);

 匿名内存设备是一个misc设备类型,所以它使用一个miscdevice类型的结构体结构体MyAshmem_misc进行注册,定义如下:

static struct miscdevice MyAshmem_misc = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "MyAshmem",
	.fops = &MyAshmem_fops,
};

文件操作/dev/MyAshmem,MyAshmem_fops 表示其操作方法列表。:

static const struct file_operations MyAshmem_fops = {
	.owner = THIS_MODULE,
	.open = MyAshmem_open,//当应用open时调用
	.release = MyAshmem_release,
	.mmap = MyAshmem_mmap,当应用mmap 时调用
};

open具体实现方法 :

static int MyAshmem_open(struct inode *inode, struct file *file)
{
	int ret;
	//当open计数器大于1时返回
	if(op > 0){
		printk("already MyAshmem_open");
	    return 0;
	}
	printk("MyAshmem_open");
	asma = kmem_cache_zalloc(MyAshmem_area_cachep, GFP_KERNEL);
	if (!asma)
		return -ENOMEM;
    //初始化链表 ,暂时没完善
	INIT_LIST_HEAD(&asma->unpinned_list);

	memcpy(asma->name, MYASHMEM_NAME_PREFIX, MYASHMEM_NAME_PREFIX_LEN);

    //设置默认匿名
	strcpy(asma->name + MYASHMEM_NAME_PREFIX_LEN, "MyAshmemDrive");
    //设置默认长度
	asma->size = 4096;
    //设置默认保护位置
	asma->prot_mask = PROT_MASK;
	//open计数器加一
    op++;
	
	return 0;
}

mmap实现:

static int MyAshmem_mmap(struct file *file, struct vm_area_struct *vma)
{
	static struct file_operations vmfile_fops;
	int ret = 0;
	mutex_lock(&MyAshmem_mutex);
    printk("MyAshmem_mmap");
	/* user needs to SET_SIZE before mapping */
	if (!asma->size) {
		ret = -EINVAL;
		goto out;
	}

	/* requested mapping size larger than object size */
	//比较size 设置大小,否则直接失败

	if (vma->vm_end - vma->vm_start > PAGE_ALIGN(asma->size)) {
		ret = -EINVAL;
		goto out;
	}

	/* requested protection bits must match our allowed protection mask */
	//检测需要映射的虚拟内存vma的保护权限是否超过了匿名共享内存的保护权限
     //比如vma除了允许读之外还允许写,但是asma只允许读,这就算超过了,会mmap失败,直接返回。
	if ((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask, 0)) &
	    calc_vm_prot_bits(PROT_MASK, 0)) {
		ret = -EPERM;
		goto out;
	}
	vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
    //第一次mmap会创造一个临时文件用来映射共享内存,
	//第二次打开直接从这个映射文件上mmap
	if (!asma->file) {
		char *name = MYASHMEM_NAME_DEF;
		struct file *vmfile;
		struct inode *inode;

		if (asma->name[MYASHMEM_NAME_PREFIX_LEN] != '\0')
			name = asma->name;

		/* ... and allocate the backing shmem file */
		//在tmpfs中创建一个临时文件。
		vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
		if (IS_ERR(vmfile)) {
			ret = PTR_ERR(vmfile);
			goto out;
		}
		vmfile->f_mode |= FMODE_LSEEK;
		inode = file_inode(vmfile);
		lockdep_set_class(&inode->i_rwsem, &backing_shmem_inode_class);
		//记录临时文件
		asma->file = vmfile;
		/*
		 * override mmap operation of the vmfile so that it can't be
		 * remapped which would lead to creation of a new vma with no
		 * asma permission checks. Have to override get_unmapped_area
		 * as well to prevent VM_BUG_ON check for f_ops modification.
		 */
		if (!vmfile_fops.mmap) {
			vmfile_fops = *vmfile->f_op;
			vmfile_fops.mmap = MyAshmem_vmfile_mmap;
			vmfile_fops.get_unmapped_area =
					MyAshmem_vmfile_get_unmapped_area;
		}
		vmfile->f_op = &vmfile_fops;
	}
	get_file(asma->file);

	/*
	 * XXX - Reworked to use shmem_zero_setup() instead of
	 * shmem_set_file while we're in staging. -jstultz
	 */
	  //判断映射虚拟内存vma是否需要在不同进程间共享,
	if (vma->vm_flags & VM_SHARED) {
		ret = shmem_zero_setup(vma);
		if (ret) {
			fput(asma->file);
			goto out;
		}
	} else {
		vma_set_anonymous(vma);
	}

	if (vma->vm_file)
		fput(vma->vm_file);
	vma->vm_file = asma->file;

out:
	mutex_unlock(&MyAshmem_mutex);
	//返回地址
	return ret;
}

全部代码drivers\staging\android\MyAshmem.c:




#include <linux/init.h>
#include <linux/export.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/falloc.h>
#include <linux/miscdevice.h>
#include <linux/security.h>
#include <linux/mm.h>
#include <linux/mman.h>
#include <linux/uaccess.h>
#include <linux/personality.h>
#include <linux/bitops.h>
#include <linux/mutex.h>
#include <linux/shmem_fs.h>

#define PROT_MASK		(PROT_EXEC | PROT_READ | PROT_WRITE)

#define MYASHMEM_NAME_LEN		256
#define MYASHMEM_NAME_PREFIX "dev/MyAshmem/"
#define MYASHMEM_NAME_PREFIX_LEN (sizeof(MYASHMEM_NAME_PREFIX) - 1)
#define MYASHMEM_FULL_NAME_LEN (MYASHMEM_NAME_LEN + MYASHMEM_NAME_PREFIX_LEN)
#define MYASHMEM_NAME_DEF "/dev/MyAshmem"
static int op = 0;
struct MyAshmem_area {
	char name[MYASHMEM_FULL_NAME_LEN];
	struct list_head unpinned_list;
	struct file *file;
	size_t size;
	unsigned long prot_mask;
};
struct MyAshmem_area *asma;

static DEFINE_MUTEX(MyAshmem_mutex);
static struct kmem_cache *MyAshmem_area_cachep __read_mostly;



static int MyAshmem_open(struct inode *inode, struct file *file)
{
	int ret;
	//当open计数器大于1时返回
	if(op > 0){
		printk("already MyAshmem_open");
	    return 0;
	}
	printk("MyAshmem_open");
	asma = kmem_cache_zalloc(MyAshmem_area_cachep, GFP_KERNEL);
	if (!asma)
		return -ENOMEM;
    //初始化链表 ,暂时没完善
	INIT_LIST_HEAD(&asma->unpinned_list);

	memcpy(asma->name, MYASHMEM_NAME_PREFIX, MYASHMEM_NAME_PREFIX_LEN);

    //设置默认匿名
	strcpy(asma->name + MYASHMEM_NAME_PREFIX_LEN, "MyAshmemDrive");
    //设置默认长度
	asma->size = 4096;
    //设置默认保护位置
	asma->prot_mask = PROT_MASK;
	//open计数器加一
    op++;
	
	return 0;
}

static int MyAshmem_release(struct inode *ignored, struct file *file)
{
	if (asma->file)
		fput(asma->file);
	kmem_cache_free(MyAshmem_area_cachep, asma);
    op--;
	return 0;
}
static int MyAshmem_vmfile_mmap(struct file *file, struct vm_area_struct *vma)
{
	/* do not allow to mmap ashmem backing shmem file directly */
	return -EPERM;
}

static unsigned long
MyAshmem_vmfile_get_unmapped_area(struct file *file, unsigned long addr,
				unsigned long len, unsigned long pgoff,
				unsigned long flags)
{
	return current->mm->get_unmapped_area(file, addr, len, pgoff, flags);
}

static inline vm_flags_t calc_vm_may_flags(unsigned long prot)
{
	return _calc_vm_trans(prot, PROT_READ,  VM_MAYREAD) |
	       _calc_vm_trans(prot, PROT_WRITE, VM_MAYWRITE) |
	       _calc_vm_trans(prot, PROT_EXEC,  VM_MAYEXEC);
}

static struct lock_class_key backing_shmem_inode_class;
static int MyAshmem_mmap(struct file *file, struct vm_area_struct *vma)
{
	static struct file_operations vmfile_fops;
	int ret = 0;
	mutex_lock(&MyAshmem_mutex);
    printk("MyAshmem_mmap");
	/* user needs to SET_SIZE before mapping */
	if (!asma->size) {
		ret = -EINVAL;
		goto out;
	}

	/* requested mapping size larger than object size */
	//比较size 设置大小,否则直接失败

	if (vma->vm_end - vma->vm_start > PAGE_ALIGN(asma->size)) {
		ret = -EINVAL;
		goto out;
	}

	/* requested protection bits must match our allowed protection mask */
	//检测需要映射的虚拟内存vma的保护权限是否超过了匿名共享内存的保护权限
     //比如vma除了允许读之外还允许写,但是asma只允许读,这就算超过了,会mmap失败,直接返回。
	if ((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask, 0)) &
	    calc_vm_prot_bits(PROT_MASK, 0)) {
		ret = -EPERM;
		goto out;
	}
	vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
    //第一次mmap会创造一个临时文件用来映射共享内存,
	//第二次打开直接从这个映射文件上mmap
	if (!asma->file) {
		char *name = MYASHMEM_NAME_DEF;
		struct file *vmfile;
		struct inode *inode;

		if (asma->name[MYASHMEM_NAME_PREFIX_LEN] != '\0')
			name = asma->name;

		/* ... and allocate the backing shmem file */
		//在tmpfs中创建一个临时文件。
		vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
		if (IS_ERR(vmfile)) {
			ret = PTR_ERR(vmfile);
			goto out;
		}
		vmfile->f_mode |= FMODE_LSEEK;
		inode = file_inode(vmfile);
		lockdep_set_class(&inode->i_rwsem, &backing_shmem_inode_class);
		//记录临时文件
		asma->file = vmfile;
		/*
		 * override mmap operation of the vmfile so that it can't be
		 * remapped which would lead to creation of a new vma with no
		 * asma permission checks. Have to override get_unmapped_area
		 * as well to prevent VM_BUG_ON check for f_ops modification.
		 */
		if (!vmfile_fops.mmap) {
			vmfile_fops = *vmfile->f_op;
			vmfile_fops.mmap = MyAshmem_vmfile_mmap;
			vmfile_fops.get_unmapped_area =
					MyAshmem_vmfile_get_unmapped_area;
		}
		vmfile->f_op = &vmfile_fops;
	}
	get_file(asma->file);

	/*
	 * XXX - Reworked to use shmem_zero_setup() instead of
	 * shmem_set_file while we're in staging. -jstultz
	 */
	  //判断映射虚拟内存vma是否需要在不同进程间共享,
	if (vma->vm_flags & VM_SHARED) {
		ret = shmem_zero_setup(vma);
		if (ret) {
			fput(asma->file);
			goto out;
		}
	} else {
		vma_set_anonymous(vma);
	}

	if (vma->vm_file)
		fput(vma->vm_file);
	vma->vm_file = asma->file;

out:
	mutex_unlock(&MyAshmem_mutex);
	//返回地址
	return ret;
}

static const struct file_operations MyAshmem_fops = {
	.owner = THIS_MODULE,
	.open = MyAshmem_open,
	.release = MyAshmem_release,
	.mmap = MyAshmem_mmap,
};

static struct miscdevice MyAshmem_misc = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "MyAshmem",
	.fops = &MyAshmem_fops,
};

static int __init MyAshmem_init(void)
{
	int ret = -ENOMEM;
	 //创建一个使用slap缓存 用于分配 MyAshmem_area 的分配器
	MyAshmem_area_cachep = kmem_cache_create("ashmem_area_cache",
					       sizeof(struct MyAshmem_area),
					       0, 0, NULL);
	if (!MyAshmem_area_cachep) {
		pr_err("failed to create slab cache\n");
		goto out;
	}
    //注册匿名共享内存设备
	ret = misc_register(&MyAshmem_misc);
	if (ret) {
		pr_err("failed to register misc device!\n");
		goto out_free1;
	}
	pr_info("initialized\n");
	return 0;

out_free1:
	kmem_cache_destroy(MyAshmem_area_cachep);
out:
	return ret;
}
device_initcall(MyAshmem_init);

drivers/staging/android/Makefile 

ccflags-y += -I$(src)                   # needed for trace events
  
obj-y                                   += ion/
obj-$(CONFIG_FIQ_DEBUGGER)              += fiq_debugger/

obj-$(CONFIG_ASHMEM)                    += ashmem.o
obj-$(CONFIG_ANDROID_VSOC)              += vsoc.o
+++obj-y                           += MyAshmem.o

进程write:

#include <stdio.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<unistd.h>
#define ASHMEM_DEVICE  "/dev/MyAshmem"
int main(void){

   int fd = open(ASHMEM_DEVICE, O_RDWR);
   char *addr = (char*)mmap(NULL, 4096 , PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
   int i =0;
   while(1){
      *addr = i++;
      printf("write:%d\n",*addr);
      sleep(1);
   }
   return 0;
}

进程read:

#include <stdio.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<unistd.h>
#define ASHMEM_DEVICE  "/dev/MyAshmem"
int main(void){
   int fd2 = open(ASHMEM_DEVICE, O_RDWR);
   char *addr2 = (char*)mmap(NULL, 4096 , PROT_READ | PROT_WRITE, MAP_SHARED, fd2, 0);
   int i =0;
   while(1){
      printf("read:%d\n",*addr2);
      sleep(1);
   }
   return 0;
}

编译/mnt/h/android-ndk-r21e/android-ndk-r21e/install/bin/aarch64-linux-android-g++ write.cpp -o write.out

/mnt/h/android-ndk-r21e/android-ndk-r21e/install/bin/aarch64-linux-android-g++ read.cpp  -o read.out

结果:

BUG:1.目前只能使用一次,得重新开机才能使用

2.不支持多组,只支持一组应用使用 

下一章节改进,觉得有用喜欢的话就给个点赞+收藏

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DHCP服务

DHCP简介 DHCP&#xff08;DynamicHost ConfigurationProtocol&#xff0c;动态主机配置协议&#xff09;通常被应用在大型的局域网络环境中&#xff0c;主要作用是集中的管理、分配IP地址&#xff0c;使网络环境中的主机动态的获得IP地址、Gateway地址、DNS服务器地址等信息&…

Flutter应用在App Store上架的完整指南

本文探讨了使用Flutter开发的iOS应用能否上架&#xff0c;以及上架的具体流程。苹果提供了App Store作为正式上架渠道&#xff0c;同时也有TestFlight供开发者进行内测。合规并通过审核后&#xff0c;Flutter应用可以顺利上架。但上架过程可能存在一些挑战&#xff0c;因此可能…

Git版本管理使用手册 - 8 -拉取开发分支、提交代码、查看提交日志

开发者从仓库获取开分支&#xff08;检出分支以及分支切换&#xff09; 根据仓库地址克隆的本地仓库&#xff0c;目录中默认是master的代码&#xff0c;即工作区是master代码&#xff0c;需要某一开发分支时&#xff0c;需要在工作区切换或者idea中先更新再切换分支&#xff0…

【java】关于String、StringBuffer和StringBuilder的那些事

在之前的文章中我们曾简单介绍过String这个引用类型变量&#xff0c;其实它还有许多特性&#xff0c;还有StringBuffer和StringBuilder这两个方法在字符串操作中也有非常重要的地位&#xff0c;接下来就由小编带大家梳理一下吧&#x1f44a; 目录 一、String 1、构造方法 2、…

RIP协议(路由信息协议)

一、RIP协议概述 RIP协议&#xff08;Routing Information Protocol&#xff0c;路由信息协议&#xff09;是一种基于距离矢量的内部网关协议&#xff0c;即根据跳数来度量路由开销&#xff0c;进行路由选择。 相比于其它路由协议&#xff08;如OSPF、ISIS等&#xff09;&#…

【Web】记录Polar靶场<中等>难度题一遍过(全)

目录 到底给不给flag呢 写shell 注入 某函数的复仇 xxe SSTI unpickle BlackMagic 反序列化 找找shell 再来ping一波啊 wu 代码审计1 你的马呢&#xff1f; ezphp 随机值 phpurl search file PlayGame csdn 反正持续一个月&#xff0c;感觉XYCTF…

MS SQL Server STUFF 函数实战 统计记录行转为列显示

目录 范例运行环境 视图样本设计 数据统计要求 STUFF函数实现 小结 范例运行环境 操作系统&#xff1a; Windows Server 2019 DataCenter 数据库&#xff1a;Microsoft SQL Server 2016 视图样本设计 假设某一视图 [v_pj_rep1_lname_score] 可查询对某一被评价人的绩效…

有单片机基础,准备过一遍freertos,之后再学linux

应届生我觉得继续把单片机深入一下比较好。 站在公司的角度思考一下&#xff0c;个人觉得应该有以下结论&#xff1a; 1 大公司具备技术资金等优势&#xff0c;因此喜欢学历好&#xff0c;理论扎实能够攻坚的人。倾向于培养各个领域的专家&#xff0c;共同构建自己产品的竞争…

网络安全之IP地址证书的重要性

在数字化时代&#xff0c;网络空间已成为各类活动的重要载体&#xff0c;无论是商业交易、信息交流还是远程办公&#xff0c;都离不开互联网的支撑。然而&#xff0c;网络环境的开放性与匿名性也带来了安全风险&#xff0c;如何确保网络交互中的身份真实可信&#xff0c;成为了…

C++中的面向对象到底是什么

C中的面向对象到底是什么 对象嘛&#xff0c;就和大家都有的对象一样&#xff0c;两只眼睛、一个嘴巴、两条腿…… 对不起跑题了&#xff0c;C的面向对象中的对象可不是显示中的对象哦&#xff0c;但是有一些相似之处&#xff0c;有对象的同学可以参考着去学习C面向对象的概念…

3. WiFi基本原理

1. WiFi简介 WiFi的全称是Wireless Fidelity。它是一种无线网络通信技术&#xff0c;由Wi-Fi联盟拥有&#xff0c;目的是改善基于IEEE 802.11标准的无线网络产品之间的互通性&#xff0c;允许电子设备在没有物理连接的情况下进行高速数据传输。此外&#xff0c;WiFi也被视为IE…

SpringCloudAlibaba服务消费者调用nacos服务报错:java.net.UnknownHostException: xxx

确保服务情况正常 通过服务名字调用(注意不需要添加端口号) RequestMapping("/add") public String add() {String url "http://stock-nacos/stock/reduce";String result restTemplate.getForObject(url, String.class);System.out.println("下单…

Vue性能优化--gZip

一、gZip简单介绍 1.1 什么是gzip gzip是GNUzip的缩写&#xff0c;最早用于UNIX系统的文件压缩。HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术&#xff0c;web服务器和客户端&#xff08;浏览器&#xff09;必须共同支持gzip。目前主流的浏览器&#xff0c;Chro…

UE4 方块排序动画

【动画效果】 入动画&#xff1a; 出动画&#xff1a; 【分析】 入动画&#xff1a;方块动画排序方式为Z字形&#xff0c;堆砌方向为X和Y轴向 出动画&#xff1a;方块动画排序方式为随机 【关键蓝图】 1.构建方块砌体 2.入/出动画

解锁AI提示工程新纪元:你的提问是关键!

文章目录 一、AI的潜力&#xff1a;无尽的宝藏等待解锁二、提问的艺术&#xff1a;挖掘AI潜力的关键三、AI的回应&#xff1a;超越预期的答案与启示四、提问的力量&#xff1a;推动AI不断进步与发展五、用提问开启与AI的智慧对话《向AI提问的艺术&#xff1a;提示工程入门与应用…

AdaBoost算法详解自用笔记(1)二分类问题举例分析

AdaBoost算法详解自用笔记&#xff08;1&#xff09;二分类问题举例分析 提升方法的思路 AdaBoost作为一种提升方法&#xff0c;其需要回答两个问题&#xff1a;一是每一轮如何改变训练数据的权重或概率分布&#xff1b;二是如何将弱分类器组合成一个强分类器。对于第一个问题…

【御控物联】JavaScript JSON结构转换(7):数组To数组——键值互换属性重组

文章目录 一、JSON结构转换是什么&#xff1f;二、案例之《JSON数组 To JSON数组》三、代码实现四、在线转换工具五、技术资料 一、JSON结构转换是什么&#xff1f; JSON结构转换指的是将一个JSON对象或JSON数组按照一定规则进行重组、筛选、映射或转换&#xff0c;生成新的JS…

《FDTD Solutions仿真全面教程:从基础入门到论文复现》

2024年5月11日-5月12日 2024年5月18日-5月19日 适用人群&#xff1a; 1.光电工程、物理学、材料科学等相关专业的硕博研究生及科研人员 2.工作于微纳光学、超构材料、光通信、光电子器件等领域的研发工程师 3.对光电仿真技术有浓厚兴趣且希望提升自身科研能力的高校教师和企业研…

ArcGIS操作(四)

任务&#xff1a; 根据江苏省夜间灯光影像完成数据处理与分析查阅统计年鉴数据以提取建成区空间分布位置完成建成区重心转移轨迹 步骤&#xff1a; 这里选取江苏省2000年、2004年、2008年夜光数据进行分析 加载影像 掩膜提取 投影 栅格转面 融合 栅格转面数据时分冗余&…

2024年,短期PMP备考上岸攻略!

今天分享一个PMP备考经验&#xff0c;正在备考PMP的小伙伴&#xff0c;咱们共勉&#xff01; 第一周、阅读教材 之前是第六版教材&#xff0c;花了很长时间阅读和梳理框架。现在是第七版教材&#xff0c;内容少了很多&#xff0c;只有300多页&#xff0c;不会特别耗时间&…