215.Mit6.S081-实验三-page tables

news2024/9/19 11:11:02

在本实验室中,您将探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。

一、实验准备

开始编码之前,请阅读xv6手册的第3章和相关文件:

  • kernel/memlayout.h,它捕获了内存的布局。
  • kernel/vm.c,其中包含大多数虚拟内存(VM)代码。
  • kernel/kalloc.c,它包含分配和释放物理内存的代码。

可看这一篇博客来增加理解。

211.xv6——3(page tables)-CSDN博客

要启动实验,请切换到pgtbl分支:

$ git fetch
$ git checkout pgtbl
$ make clean

二、Print a page table (easy)

1.实验要求

        为了帮助您了解RISC-V页表,也许为了帮助将来的调试,您的第一个任务是编写一个打印页表内容的函数。

  1. 定义一个名为vmprint()的函数。
  2. 它应当接收一个pagetable_t作为参数,并以下面描述的格式打印该页表。
  3. exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable),以打印第一个进程的页表。
  4. 如果你通过了pte printout测试的make grade,你将获得此作业的满分。

现在,当您启动xv6时,它应该像这样打印输出来描述第一个进程刚刚完成exec()inginit时的页表:

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
  1. 第一行显示vmprint的参数。之后的每行对应一个PTE,包含树中指向页表页的PTE。
  2. 每个PTE行都有一些“..”的缩进表明它在树中的深度。
  3. 每个PTE行显示其在页表页中的PTE索引、PTE比特位以及从PTE提取的物理地址。
  4. 不要打印无效的PTE。在上面的示例中,顶级页表页具有条目0和255的映射。
  5. 条目0的下一级只映射了索引0,该索引0的下一级映射了条目0、1和2。

您的代码可能会发出与上面显示的不同的物理地址。条目数和虚拟地址应相同。

2.提示

  • 你可以将vmprint()放在kernel/vm.c
  • 使用定义在kernel/riscv.h末尾处的宏
  • 函数freewalk可能会对你有所启发
  • vmprint的原型定义在kernel/defs.h中,这样你就可以在exec.c中调用它了
  • 在你的printf调用中使用%p来打印像上面示例中的完成的64比特的十六进制PTE和地址

3.实现

(1)首先在kernel/vm.c中添加vmprint()函数

// 递归打印页表的函数。
// pagetable是页表,level表示当前递归的深度。
void 
_vmprint(pagetable_t pagetable, int level)
{
  // 遍历页表中的每一个PTE(页表项)。
  for (int i = 0; i < 512; i++)
  {
    pte_t pte = pagetable[i]; // 获取当前的页表项。
    if(pte & PTE_V) // 如果页表项有效(存在)。
    {
      // 打印缩进,根据当前递归的深度level来决定。
      for (int j = 0; j < level; j++)
      {
        if(j)
          printf(" ");
        printf("..");
      }
      
      uint64 child = PTE2PA(pte); // 获取页表项指向的物理地址。
      printf("%d: pte %p pa %p\n", i, pte, child); // 打印页表项信息。
      
      // 如果不是叶子节点(没有R/W/X权限),继续递归打印下一级页表。
      if((pte & (PTE_W | PTE_R | PTE_X)) == 0)
      {
        _vmprint((pagetable_t)child, level + 1);
      }
    }
  }
}

// 打印页表的入口函数。
// pagetable是页表的根。
void vmprint(pagetable_t pagetable)
{
  printf("page table %p\n", pagetable); // 打印页表的根地址。
  _vmprint(pagetable, 1); // 从根页表开始递归打印,初始深度为1。
}

(2)在kernel/defs.h中添加定义

(3)在kernel/exec.c中添加

4.测试结果

三、A kernel page table per process (hard)

        Xv6有一个单独的用于在内核中执行程序时的内核页表内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x映射到物理地址仍然是xXv6还为每个进程的用户地址空间提供了一个单独的页表,只包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()的缓冲区指针)时,内核必须首先将指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针。

1.实验要求

        你的第一项工作是修改内核来让每一个进程在内核中执行时使用它自己的内核页表的副本。修改struct proc来为每一个进程维护一个内核页表,修改调度程序使得切换进程时也切换内核页表。对于这个步骤,每个进程的内核页表都应当与现有的的全局内核页表完全一致。如果你的usertests程序正确运行了,那么你就通过了这个实验。

        阅读本作业开头提到的章节和代码;了解虚拟内存代码的工作原理后,正确修改虚拟内存代码将更容易。页表设置中的错误可能会由于缺少映射而导致陷阱,可能会导致加载和存储影响到意料之外的物理页存页面,并且可能会导致执行来自错误内存页的指令。

2.提示

  • struct proc中为进程的内核页表增加一个字段
  • 为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable。你将会考虑在allocproc中调用这个函数。
  • 确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到allocproc
  • 修改scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()
  • 没有进程运行时scheduler()应当使用kernel_pagetable
  • freeproc中释放一个进程的内核页表
  • 你需要一种方法来释放页表,而不必释放叶子物理内存页面。
  • 调式页表时,也许vmprint能派上用场
  • 修改XV6本来的函数或新增函数都是允许的;你或许至少需要在kernel/vm.ckernel/proc.c中这样做(但不要修改kernel/vmcopyin.ckernel/stats.cuser/usertests.c, 和user/stats.c
  • 页表映射丢失很可能导致内核遭遇页面错误。这将导致打印一段包含sepc=0x00000000XXXXXXXX的错误提示。你可以在kernel/kernel.asm通过查询XXXXXXXX来定位错误。

3.具体实现

本实验主要是让每个进程都有自己的内核页表,这样在内核中执行时使用它自己的内核页表的副本。

(1)首先在kernel/proc.h里面的struct proc增加内核页表的字段,表示内核态页表。

(2)在vm.c中添加新的方法proc_kpt_init,该方法用于在allocproc 中初始化进程的内核页表。这个函数还需要一个辅助函数uvmmap,该函数和kvmmap方法几乎一致,不同的是kvmmap是对Xv6的内核页表进行映射,而uvmmap将用于进程的内核页表进行映射。

//用于映射虚拟地址到物理地址
void uvmmap(pagetable_t pagetable,uint64 va,uint64 pa,uint64 sz,int perm)
{
  if(mappages(pagetable,va,sz,pa,perm)!=0)
  {
    panic("uvmmap");
  }
}

//用于初始化内核页表
pagetable_t ukvminit()
{
  pagetable_t kernelpt = uvmcreate();
  if(kernelpt==0)
    return 0;
  uvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kernelpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  uvmmap(kernelpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  uvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  uvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  uvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return kernelpt;
}

(3)在 kernel/proc.c 中的 allocproc 函数里添加调用函数的代码:

记得在 kernel/defs.h 添加函数声明:pagetable_t ukvminit(void);

(4)在内核栈的初始化原来是在 kernel/proc.c 中的 procinit 函数内,这部分要求将函数内的代码转移到 allocproc 函数内,因此在上一步初始化内核态页表的代码下面接着添加初始化内核栈的代码:

kvminithart是用于原先的内核页表,我们将进程的内核页表传进去就可以。在vm.c里面添加一个新方法proc_inithart

然后在scheduler()内调用即可,但在结束的时候,需要切换回原先的kernel_pagetable。直接调用调用上面的kvminithart()就能把Xv6的内核页表加载回去。

(6) 在freeproc中释放一个进程的内核页表。首先释放页表内的内核栈,调用uvmunmap可以解除映射,最后的一个参数(do_free)为一的时候,会释放实际内存。

// free the kernel stack in the RAM
uvmunmap(p->kernelpt, p->kstack, 1, 1);
p->kstack = 0;

然后释放进程的内核页表,先在kernel/proc.c里面添加一个方法proc_freekernelpt。如下,历遍整个内核页表,然后将所有有效的页表项清空为零。如果这个页表项不在最后一层的页表上,需要继续进行递归。

void proc_freekernelpt(pagetable_t kernelpt)
{
  // similar to the freewalk method
  // there are 2^9 = 512 PTEs in a page table.
  for (int i = 0; i < 512;i++)
  {
    pte_t pte = kernelpt[i];
    if(pte&PTE_V)
    {
      kernelpt[i] = 0;
      if((pte&(PTE_R|PTE_W|PTE_X))==0)
      {
        uint64 child = PTE2PA(pte);
        proc_freekernelpt((pagetable_t)child);
      }
    }
  }
  kfree((void *)kernelpt);
}

(6). 将需要的函数定义添加到 kernel/defs.h 中

(7). 修改vm.c中的kvmpa,将原先的kernel_pagetable改成myproc()->kernelpt,使用进程的内核页表。

最后,在 vm.c 中添加头文件:

#include "spinlock.h"
#include "proc.h"

最后修改kvmpa函数

uint64
kvmpa(uint64 va)
{
  uint64 off = va % PGSIZE;
  pte_t *pte;
  uint64 pa;

  pte = walk(myproc()->kernelpt, va, 0); // 修改这里
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}

4.测试结果

四、Simplify copyin/copyinstr

        内核的copyin函数读取用户指针指向的内存。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点。这个转换是通过在软件中遍历进程页表来执行的。在本部分的实验中,您的工作是将用户空间的映射添加到每个进程的内核页表(上一节中创建),以允许copyin(和相关的字符串函数copyinstr)直接解引用用户指针。

 1.实验要求

  • 将定义在kernel/vm.c中的copyin的主题内容替换为对copyin_new的调用(在kernel/vmcopyin.c中定义);
  • copyinstrcopyinstr_new执行相同的操作。
  • 为每个进程的内核页表添加用户地址映射,以便copyin_newcopyinstr_new工作。
  • 如果usertests正确运行并且所有make grade测试都通过,那么你就完成了此项作业。

        此方案依赖于用户的虚拟地址范围不与内核用于自身指令和数据的虚拟地址范围重叠。Xv6使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000,即PLIC寄存器的地址;请参见kernel/vm.c中的kvminit()kernel/memlayout.h和文中的图3-4。您需要修改xv6,以防止用户进程增长到超过PLIC的地址。

2.提示

  • 先用对copyin_new的调用替换copyin(),确保正常工作后再去修改copyinstr
  • 在内核更改进程的用户映射的每一处,都以相同的方式更改进程的内核页表。包括fork()exec(), 和sbrk().
  • 不要忘记在userinit的内核页表中包含第一个进程的用户页表
  • 用户地址的PTE在进程的内核页表中需要什么权限?(在内核模式下,无法访问设置了PTE_U的页面)
  • 别忘了上面提到的PLIC限制

        Linux使用的技术与您已经实现的技术类似。直到几年前,许多内核在用户和内核空间中都为当前进程使用相同的自身进程页表,并为用户和内核地址进行映射以避免在用户和内核空间之间切换时必须切换页表。然而,这种设置允许边信道攻击,如Meltdown和Spectre。

3.实现

        本实验是实现将用户空间的映射添加到每个进程的内核页表,将进程的页表复制一份到进程的内核页表就好。

        首先添加复制函数。需要注意的是,在内核模式下,无法访问设置了PTE_U的页面,所以我们要将其移除。

(1)复制页表内容

void u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz) {
  pte_t *pte_from, *pte_to;

  // 将 oldsz 向上取整到最近的页边界
  oldsz = PGROUNDUP(oldsz);

  // 遍历 [oldsz, newsz) 范围内的每一页
  for (uint64 i = oldsz; i < newsz; i += PGSIZE) {
    // 从用户页表中获取地址 i 对应的 PTE
    if ((pte_from = walk(pagetable, i, 0)) == 0)
      panic("u2kvmcopy: src pte does not exist");
    
    // 确保用户页表中的页表项是有效的(即包含 PTE_V 标志)
    if (!(*pte_from & PTE_V))
      panic("u2kvmcopy: src pte not valid");

    // 获取或创建内核页表中地址 i 对应的 PTE
    if ((pte_to = walk(kernelpt, i, 1)) == 0)
      panic("u2kvmcopy: pte walk failed");

    // 从用户页表项中获取物理地址
    uint64 pa = PTE2PA(*pte_from);

    // 从用户页表项中获取标志,并移除用户权限标志 PTE_U
    uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);

    // 设置内核页表项
    *pte_to = PA2PTE(pa) | flags;
  }
}

第二步,fork(),sbrk(),exec()

然后在内核更改进程的用户映射的每一处 (fork()exec(), 和sbrk()),都复制一份到进程的内核页表。

fork()

exec()

sbrk(), 在kernel/sysproc.c里面找到sys_sbrk(void),可以知道只有growproc是负责将用户内存增加或缩小 n 个字节。以防止用户进程增长到超过PLIC的地址,我们需要给它加个限制。

然后替换掉原有的copyin()copyinstr()

// Copy from user to kernel.
// Copy len bytes to dst from virtual address srcva in a given page table.
// Return 0 on success, -1 on error.
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable, dst, srcva, len);
}

// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  return copyinstr_new(pagetable, dst, srcva, max);
}

并且添加到 kernel/defs.h 中

// vmcopyin.c
int             copyin_new(pagetable_t, char *, uint64, uint64);
int             copyinstr_new(pagetable_t, char *, uint64, uint64);

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

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

相关文章

什么是渲染:两种渲染类型、工作原理

如果您是网页设计师或数字艺术家&#xff0c;您可能熟悉渲染过程的概念。这是数字艺术中的重要步骤&#xff0c;帮助您将图形模型转换为最终结果。在本文中&#xff0c;您将了解数字艺术中的渲染是什么、它的工作原理以及它的类型。 一、什么是渲染? 渲染是使用计算机软件对数…

怎么样的主食冻干算好冻干?品质卓越、安全可靠的主食冻干分享

当前主食冻干市场产品质量参差不齐。一些品牌过于追求营养数据的堆砌和利润的增长&#xff0c;却忽视了猫咪健康饮食的基本原则&#xff0c;导致市场上出现了以肉粉冒充鲜肉、修改产品日期等不诚信行为。更令人担忧的是&#xff0c;部分产品未经过严格的第三方质量检测便上市销…

Python实现傅里叶级数可视化工具

Python实现傅里叶级数可视化工具 flyfish 有matlab实现&#xff0c;我没matlab&#xff0c;我有Python&#xff0c;所以我用Python实现。 整个工具的实现代码放在最后,界面使用PyQt5开发 起源 傅里叶级数&#xff08;Fourier Series&#xff09;由法国数学家和物理学家让-巴…

Apache网页优化(企业网站结构部署与优化)

本章结构 一、Apache网页优化 在使用 Apache 作为 Web 服务器的过程中&#xff0c;只有对 Apache 服务器进行适当的优化配置&#xff0c;才能让 Apache 发挥出更好的性能。反过来说&#xff0c;如果 Apache 的配置非常糟糕&#xff0c;Apache可能无法正常为我们服务。因此&…

链接报错undefined reference to + libc++和libstdc++

1 问题现象 subscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) 描述&#xff1a;编译的时候&#xff0c;最后的链接中一直没成功 2 可能原因 2.1 链接时缺失了相关目标文件&#xff08;.o&#x…

Visual Studio 2022 安装及使用

一、下载及安装 VS 官网&#xff1a;Visual Studio: IDE and Code Editor for Software Developers and Teams 下载免费的社区版 得到一个.exe文件 右键安装 选择C开发&#xff0c;并修改安装位置 等待安装 点击启动 二、VS的使用 1.创建项目 打开VS&#xff0c;点击创建新项…

1招搞定maven打包空间不足问题

目录 一、工具应用问题 二 、使用效果 三、使用方法 四、练习手段 一、工具应用问题 使用maven的package功能打包失败&#xff0c;报错“Java heap space”错误。 二 、使用效果 修改IDEA中maven内存使用大小后&#xff0c;打包成功。 三、使用方法 点击菜单“File->Set…

openWrt(4) - uci

uci show 1) uci show - 查看所有配置文件列表 2)查看特定配置文件的详细信息&#xff1a; uci show network 我们以 network 为例 3&#xff09;查看特定配置项的详细信息&#xff1a; uci show network.wan 添加一个新的配置条目&#xff1a;uci add network interface …

Apifox报错404:网络错误,请检查网络,或者稍后再试的解决办法

详细报错如图&#xff1a; 解决办法&#xff1a; 1、检查 请求方法&#xff08;get&#xff0c;post&#xff09;是否正确&#xff0c;请求的URL是否正确&#xff0c;如果不正确&#xff0c;修改后重新发起请求&#xff1b;如果都正确&#xff0c;看2 2、复制curl用postman来…

安防监控/视频汇聚平台EasyCVR设备录像回看请求播放时间和实际时间对不上,是什么原因?

安防监控EasyCVR视频汇聚平台可提供多协议&#xff08;RTSP/RTMP/国标GB28181/GAT1400/海康Ehome/大华/海康/宇视等SDK&#xff09;的设备接入、音视频采集、视频转码、处理、分发等服务&#xff0c;系统具备实时监控、云端录像、回看、告警、平台级联以及多视频流格式分发等视…

当海底捞开始“抠”服务,火锅还能好吗

在胖东来因好服务在互联网上被献上诸多赞誉时&#xff0c;另一家曾因服务被赞为“学不会”的海底捞&#xff0c;却在经受质疑。 最近&#xff0c;社交媒体上&#xff0c;海底捞的消息有点儿多。先是5月30日&#xff0c;#海底捞玩具大人不能随便拿了#登上热搜&#xff0c;后是6…

开发TEE的踩坑之开发TEE

系统&#xff1a;Ubuntu20.04&#xff08;双系统&#xff0c;非虚拟机&#xff09; 一、前置说明1、TEE平台的选择2、机器间的通信方式3、程序和数据集的示例4、结果文件的解密 二、服务器部署三、客户端部署四、工程应用 本系列为笔者开发TEE&#xff08;Trusted Execution En…

华为USG6000V防火墙v1

目录 一、实验拓扑图 二、要求 三、IP地址规划 四、实验配置 1&#x1f923;防火墙FW1web服务配置 2.网络配置 要求1&#xff1a;DMZ区内的服务器&#xff0c;办公区仅能在办公时间内(9:00-18:00)可以访问&#xff0c;生产区的设备全天可以访问 要求2&#xff1a;生产区不…

【ARMv8/v9 GIC 系列 1.7 -- GIC PPI | SPI | SGI | LPI 中断使能配置概述】

请阅读【ARM GICv3/v4 实战学习 】 文章目录 GIC 各种中断使能配置PPIs(每个处理器私有中断)SPIs(共享外设中断)SGIs(软件生成的中断)LPIs(局部中断)GIC 各种中断使能配置 在ARM GICv3和GICv4架构中,不同类型的中断(如PPIs、SPIs、SGIs和LPIs)可以通过不同的方式进…

09磁盘管理

一、磁盘管理 1.磁盘基础知识 &#xff08;1&#xff09;磁盘接口类型 个人电脑&#xff0c; 硬盘接口分为IDE类型和SATA类型 服务器版分为SCSI类型和SAS类型 &#xff08;2&#xff09;磁盘命名方式 windows中硬盘命名方式是c&#xff0c;d,e盘 linux中硬盘命名方式为 系统…

uiautomation: debug记录

文章目录 1. pyinstaller打包后运行涉及uiautomation时出现的的错误1.1 [WinError 3] The system cannot find the path specified: C:\\Users\\g\\AppData\\Local\\Temp\\_MEI192482\\uiautomation\\bin 1. pyinstaller打包后运行涉及uiautomation时出现的的错误 1.1 [WinErr…

JUC八股(持续更新中)

写在前面&#xff1a;本文为个人八股复习所用&#xff0c;整合了其他平台的答案加自己的理解&#xff0c;希望能对大家的八股复习有所帮助&#xff0c;答案可能存在出入&#xff0c;请大家理性食用~~ 1. 进程和线程的区别 进程&#xff1a; 进程是操作系统资源分配的基本单位…

神器!3个免费PPT成品网站推荐+3款AIPPT工具盘点!

熬夜加班做PPT却没有头绪&#xff1f;别再自己憋着想了&#xff01;现在凡事主打一个“抄作业”&#xff0c;想做ppt却没想法&#xff0c;可以去到ppt成品网站搜集PPT模板&#xff0c;或是使用时下流行的AI生成PPT工具&#xff0c;只需输入PPT主题&#xff0c;即可快速生成一份…

Linux 命令探秘:揭秘那些有趣的小命令

目录 1.发现隐藏在终端的惊喜小命令 2.小火车 1.安装EPEL 2.小火车出发准备 3.输入命令 3.linux_logo 1.安装linux_logo 2.输入命令 3.linux_logo介绍 4.牛讲话 1.安装命令 2.输入命令 5. figlet 1.安装命令 2.输入命令 “如果您在解决类似问题时也遇到了困…

前端面试题49(如何集成和使用CSP 来增强前端安全性?)

Content Security Policy (CSP) 是一种帮助防止跨站脚本(XSS)和其他代码注入攻击的安全策略。通过明确指定浏览器可以加载或执行哪些资源&#xff0c;CSP 有效限制了恶意内容的执行。下面是如何在实际项目中集成和使用 CSP 的步骤&#xff1a; 1. 确定CSP策略 首先&#xff0…