深入理解计算机系统
Posted longjiang-uestc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解计算机系统相关的知识,希望对你有一定的参考价值。
深入理解计算机系统
计算机系统漫游
- 源文件到目标文件的翻译过程可分为四个阶段, 这四个阶段的程序被称为预处理器,编译器,汇编器和链接器,它们一起构成了编译系统(compilation system)。
- 缓冲区溢出是造成大多数网络和Internet服务器上安全漏洞的主要原因。
- 主存是一个临时存储设备,用来存放程序和程序处理的数据。
- 从物理上来说, 主存是由一组动态随机存储器(DRAM)芯片组成。
- 逻辑上, 主存是一个线性的字节数组,每个字节都有唯一的地址, 这些地址从零开始。
- 中央处理器(CPU)的核心是程序计数器(PC),PC都指向主存中的某条机器语言指令(即含有该条指令的地址)。
- 处理器一直在不断地执行程序计数器指向的指令, 再更新程序计数器,使其指向下一条指令。
- 指令集结构描述的是每条机器代码指令的效果; 而微体系结构描述的是处理器实际上是如何实现的。
- 复制就是开销, 利用高级缓存减少数据间的拷贝。
- 操作系统有两个基本功能:
- 防止硬件被失控的应用程序滥用;
- 向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备。
- 操作系统有几个抽象概念: 进程, 虚拟存储器和文件。
- 文件是对I/O设备的抽象表示;
- 虚拟存储器是对主存和磁盘I/O设备的抽象表示;
- 进程是对处理器,主存和I/O设备的抽象表示。
- 进程是操作系统对一个正在运行的程序的一种抽象,每个进行好像独占地使用硬件。
- 一个进程实际上可以由多个线程的执行单元组成,每个线程都运行在进程的上下文中,并共享全局的代码和全局的数据。
- 每个进程看到的是一致的存储器,称为虚拟地址空间。
- 并发与并行:
- 并发是指同时有多个活动的系统;
- 并行是指用并发使一个系统运行得更快。
- 超线程也叫做同时多线程,允许一个CPU执行多个控制流的技术。 --- 允许一个CPU某些硬件有多个备份,而另外硬件部分只能有一份。(4核实际上可以执行8个线程)
- 抽象是计算机科学中最为重要的概念之一。
程序结构与执行
- 浮点数虽然可以编码一个较大的数值范围,但是这种表示只是近似的。
- Java创造了一套新的数字表示和运算标准。
- 所有可能地址的集合称为虚拟地址空间, 虚拟地址空间(virtual address space)只是展现给机器级程序的概念性映像。
- typedef提供了一种给数据类型命名的方式。
- Java只支持有符号数。
- 在Java中, 单字节数据类型称为byte,而不是char,而且没有long long数据类型。
程序的机器级表示
- 目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入地址的全局值。
- 机器级程序的格式和行为,定义为指令集体系结构(Instruction set architecture, ISA),定义了处理器状态指令的格式,以及每条指令对状态的影响。
- 汇编代码不区分有符号或无符号整数,不区分各种类型的指针,甚至不区分指针和整数。
- 程序存储器用虚拟地址来寻址。在任给定的时刻,只认为有限的一部分虚拟地址是合法的。
- 操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址。
- 一条机器指令只执行一个非常基本的操作。
- 机器世纪之星的程序是一系列指令惊喜编码的字节序列。
- 查看目标代码文件的内容,最有价值的是反汇编(disassembler) ---
objdump -d code.o
.
汇编中一些语句的说明
- 以‘.’开头的行都是指导汇编器和链接器的命令,通常可以忽略这些行。
- 有些指令以固定的寄存器作为源寄存器或目的寄存器。
- 寄存器%ebp和%esp保存着指向程序栈中重要的位置指针。
- %ebp是帧指针。
- %esp是栈指针。
- 存储器引用(memory),会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。
- 数据传送指令:
- 数据的条件转移是指先计算一个条件操作的两种结果,然后再根据条件是否满足从而选一个。
- 算术和逻辑操作:
- 加载有效地址(load effective address)指令leal实际上是movl指令的变形,实际上是从寄存器读数据到寄存器中。
- 将有效地址写入到目的操作数。
- 将有效地址写入到目的操作数。
- 一元操作指只有一个操作数,即是源又是目的。
- 特殊的算术操作:
.
- 加载有效地址(load effective address)指令leal实际上是movl指令的变形,实际上是从寄存器读数据到寄存器中。
- 控制:
- 除了整数寄存器, CPU还维护着一组单个位的条件码(condition code)寄存器,它们描述了最近的算术或逻辑操作的属性。
- CF(carry flag): 进位标志,最近的操作使最高位产生了进位,可以用来检查无符号操作数的溢出。
- ZF(zero flag): 零标志,最近的操作得出的结果为0.
- SF(symbol flag): 符号标志位,最近的操作得到的结果为负数。
- OF(over flag): 溢出标志,最近的操作导致一个补码溢出---正溢出或负溢出。
- CMP指令根据两个操作数之差来设置条件码,如果两个操作数之差相等,这些指令会将零标志设置为1.
- TEST根据两个操作数的与来设置零标志和负数标志。
- 汇编语言入门教程.
- 跳转(jump)指令会导致执行切换到程序中一个全新的位置 --- jmp .L1(标号, label)。
- jmp指令是无条件跳转,它可以是直接跳转,也可以间接跳转。
.
- jmp指令是无条件跳转,它可以是直接跳转,也可以间接跳转。
- 基于条件数据传送的代码比基于条件控制转移的代码性能要好。
- 处理器采用非常精密的分支预测逻辑试图猜测每条跳转指令是否会执行。
- 除了整数寄存器, CPU还维护着一组单个位的条件码(condition code)寄存器,它们描述了最近的算术或逻辑操作的属性。
- 过程:
- 机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。
. - 当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。
- 帧指针(%ebp)在当前帧的末尾。
- 当前帧的上面是调用者的帧。
. - BP和SP指针讲解.
- BP指针可以理解为base pointer,基址指针。
- SI和DI寄存器的用途.
- SI是源变址寄存器,DI是目的变址寄存器。
- 汇编中各个寄存器的作用.
- .cfi_startproc的作用是确定每个函数调用的帧(基地址)缓存正确.
- IP(Instruction Pointer)指令指针用于指向当前需要取出 的指令字节,IP在取出一个指令后会自动加1,并指向下一个指令。--- 感觉和PC的含义差不多吧。
- 机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。
- 几个有用的命令工具:
gcc -S 源文件.c
--- 将源文件变为汇编文件。gcc -c 源文件.c
--- 将源文件变为.o的二进制文件。objdump -d 目标文件或二进制可执行文件
--- 进行反汇编。
- 汇编用条件测试和跳转组合起来实现循环的效果。
- 流水线通过重叠连续指令的步骤来获得高性能。
- 分支预测逻辑试图猜测每条跳转指令是否执行, 只要猜测比较可靠(90%以上的成功率),指令流水线中就会充满着指令。
- 错误预测会导致严重的惩罚,大约20~40个时钟周期的浪费。
- 当两个表达式都很容易计算时,才使用条件传送改进代码的效率。
- switch语句的汇编语句会做一个跳转表放到.text文本段,优化多重分支跳转的效率。
- 转移控制:
- call Label --- 过程调用。
- call *Operand --- 过程调用。
- leave --- 为返回准备栈。
- ret --- 从过程调用中返回。
- call的效果是将返回地址入栈(eip所指的地址入栈)。
- 程序寄存器组是唯一能被所有过程共享的资源。
- 寄存器%eax, %edx和%ecx被互粉为调用者保存寄存器。
- 寄存器%ebx, %esi, %edi被互粉为被调用者保存寄存器。(调用前把他们放到栈中, 调用后返回)
- 必须保持寄存器%ebp和%esp.
- 每个调用在栈中都有它自己的私有空间,多个未完成调用的局部变量不会互相影响。
- leal指令可以用来产生地址。
- 结构的所有组成部分都放到存储器中的一段连续区域,而指向结构的指针就是结构第一个字节的地址。
- 为了对齐和访问结构的字段,编译器产生的代码要将结构的地址加上适当的编译。
- 联合绕过了C语言类型系统提供的安全措施。
- 数据对齐:
- 要求某种类型对象的地址必须是某个值K(通常是2,4或8)的倍数。
- 对齐限制是为了简化处理器和存储器系统之间接口的硬件设计。
- 不同的操作系统可能对齐方式也会不一样,Linux8字节的double进行4字节对齐,而Windows采取8字节对齐。
.align 4
表示它后面的数据的起始地址是4的倍数。- struct结构体的对齐更为严格,内部成员都要保证对齐。
- 函数指针的值是该函数机器代码表示中第一条指令的地址。
- 对越界的数组元素的写操作会破坏存储在栈中的状态信息。(缓冲区溢出, buffer overflow)
- 栈随机化的思想使得栈的位置在程序每次运行时都有变化。
- 实现方式是在程序开始时,在栈上分配一段0~n字节之间的随机大小的空间。
- 地址空间布局随机化(Address-Space Layout Randomization):程序代码,库代码,栈,全局变量,堆数据都会被加载到不同的区域。
- gcc版本中加入了一种栈保护者机制(stack protector), 用来检测缓冲区越界.
- 在栈帧中任何缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值, 也称为哨兵值(guard value).
- 最新的NX(No-eXecute)技术支持栈可读可写但不可执行。
- callq指令将一个64位返回地址存储在栈上。
- 许多函数不再需要栈帧,只有那些不能将所有的局部变量都放到寄存器中的函数才需要在栈上分配空间。
- 函数最多可以访问超过当前栈帧值的128个字节的栈上存储空间。
- 一些信息存储在栈上而不需修改栈指针。
- 最多有6个参数可用寄存器进行传参。
- Java的目标代码是一种特殊的二进制表示,称为Java字节代码, 这种代码可以看作是虚拟机的机器级程序。
- 用软件解释器处理字节代码,模拟虚拟机的行为。
处理器体系结构
- 一个处理器支持的指令和指令的字节级编码称为它的指令集体系结构(Instruction-Set Architecture, ISA).
- 通过同时处理多条指令的不同部分,处理器可以获得更好的性能(流水线工作模式)。
- 硬件和操作系统软件联合起来将虚拟地址翻译成实际地址(物理地址)。
- RISC指令集和CISC指令集:
. - 控制逻辑是设计微处理器最难的部分。
- 一条指令可以划分为几个部分:
- 取地址(fetch): 从存储器读取指令字节,地址为程序计数器(PC)的值。
- 译码(decode): 译码阶段从寄存器文件读入最多两个操作数。
- 执行(execute): 算术逻辑单元要么执行指令指明的操作,计算存储器引用的有效地址,要么增加或减少栈指针。
- 访存(memory): 访问阶段可以将数据写入存储器,或者从存储其读出数据。
- 写回(write back): 写回阶段最多可以写两个结果到寄存器文件。
- 更新PC(PC update):将PC设置成下一条指令的地址。
.
.
- 流水线冒险:
- 数据相关:下一条指令会用到这一条指令的结果。 --- 数据冒险。
- 用暂停来避免数据冒险,处理器暂停一条或多条指令,直到冒险条件不满足。(加入nop指令)
- 用转发避免数据冒险,从一个流水线阶段传到较早阶段。(旁路)。
- 用暂停(加载互锁)和转发结合起来,避免加载/使用数据冒险。
- 控制相关: 一条指令要确定下一条指令的位置。--- 控制冒险。
- 预测错误的分支。
- 数据相关:下一条指令会用到这一条指令的结果。 --- 数据冒险。
- 从处理器角度来看,将用暂停来处理短时间的告诉缓存不命中和用异常来处理长时间的缺页结合起来,能估计到存储器访问时由于存储器层次结构引起的所有不可预测性。
优化程序性能
- 每元素的周期数作为一种表示程序性能并指导我们改进代码的方法。
- 消除循环的低效率。
- 减少过程调用。
- 理解现代处理器。
- 超标量的意思是每个时钟周期执行多个操作。
- 循环展开: 增加每次迭代计算的元素数量,减少循环的迭代次数。
- 提高并行性。
性能提高技术
- 高级设计:
- 为遇到的问题选择适当的算法和数据结构。
- 基本编码原则:
- 避免限制优化的因素,这样编译器能产生高效的代码。
- 消除连续的函数调用,将计算移到循环外,考虑妥协模块性以获得更大的效率。
- 消除不必要的存储器引用,引入临时变量来保存中间结构,只有在最后值计算出来时,才将结果放到数组或全局变量中。
- 用功能的风格重写条件操作,使得编译器采用条件数据传送。
存储器层次结构
- 随机访问存储器(Random-Access Memory, RAM), 分为静态和动态的, 静态RAM比动态RAM更快。
- 动态RAM有干扰将不会恢复。
- 固态硬盘(Solid State Disk, SSD)是由一个或多个闪存芯片和闪存翻译层组成。
- 一个编写良好的计算机程序常常具有良好的局部性(locality): 时间局部性和空间局部性。
- 局部性原理: 倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。
- 被引用过一次的存储器可能在不远的将来会再被多次引用。
- 在存储器一个位置上被引用了一次,那么在不远的将来可能会引用附近的一个存储器位置。
。 - 冷不命中: 通常是短暂的事件,不会在反复访问访问存储器使得缓存暖身之后的稳定状态中出现。
- 高速缓存友好的代码:
- 对局部变量的反复引用是好的。(利用时间局部性)
- 步长为1的引用模式是好的。(利用空间局部性)
- 一旦从存储器中读入了一个数据对象,就尽可能多地使用它,从而使程序中的时间局部性最大。
。 - 存储器系统性能是一座时间和空间局部性的山,这座山的上升高度差别可以超过一个数量级。
- 利用时间局部性, 使得频繁使用的字从L1中取出。
- 利用空间局部性, 使得尽可能多的字从一个L1高速缓存中访问到。
链接
- 链接器程序ld,将main.o和swap.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件。
- 可重定向目标文件。
- 可执行目标文件。
- 共享目标文件。
- 编译器和汇编器生成可重定位目标文件(包括共享目标文件)。
- 链接器生成可执行目标文件。
- 可重定位目标文件:
- .text:已编译程序的机器代码。
- .rodata:只读数据,switch语句中的跳转表。
- .data:已初始化的全局C变量。
- .bss: 未初始化的全局C变量。不占实际空间,仅仅是个占位符。
- .symtab: 一个符号表, 它存放在程序中定义和引用的函数和全局变量的信息。但不包含局部变量的信息。
- .rel.text: 一个.text节中位置的列表,当链接器把这个目标文件和其他文件结合时, 需要修改这些位置。
- .rel.data: 被模块引用或定义的任何全局变量的重定位信息。
- .debug: 一个调试符号表,全局变量及类型定义,定义和引用的全局变量,以及原始的C源文件。
- .line: 原始C源文件中的行号和.text节中机器指令之间的映射。(-g)
- .strtab: 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。
- static全局变量在.data或.bss中分配空间,并在符号表中穿件一个有唯一名字的本地链接器符号。
- ‘cpp‘在Unix系统中是c预处理器。
cc1
是C编译器。as
是C汇编器。ld
是链接器。
.
- 函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
- 不允许有多个强符号。
- 如果有一个强符号和多个弱符号, 那么选择强符号。
- 如果有多个弱符号,那么冲这些弱符号中任意选择一个。
- 链接器通常不会表明它检测到多个x的定义。
- 遇到多重定义的全局符号时,输出一条警告信息。
- 把所有相关的目标模块打包成一个单独的文件,称为静态库(static library),它可以用作链接器的输入。
- 使用AR工具可以创建静态库。
。
- 使用AR工具可以创建静态库。
- 可执行文件的连续片(chunk)被映射到连续的存储器段。
- 段头部表(segment header table)描述了这种映射关系。
. - 通过调用留在存储器中称为加载器(loader)的操作系统代码来运行它。
- 通过execve函数来调用加载器, 加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令和入口点(entry point --- 符号表_start的地址)来运行程序。
. - 当shell运行一个程序时,父外壳进程生成一个子进程,它是父进程的一个复制品, 子进程通过execve系统调用启动加载器;加载器删除子进程现有的虚拟存储器段,并穿件一组新的代码、数据、堆和栈段,然后初始化为可执行文件的内容,最后跳转到_start地址调用main函数执行。
- 操作系统利用页面调度机制自动将页面从磁盘传递到存储器。
- 通过execve函数来调用加载器, 加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令和入口点(entry point --- 符号表_start的地址)来运行程序。
- 段头部表(segment header table)描述了这种映射关系。
- 共享库(shared library)是一个目标模块, 在运行时,可以加载到任意的存储器地址,并和另一个存储器中的程序连接起来,以.so后缀表示。
- 一个共享库中的.text段的一个副本可以被不同的长在运行的进程共享。
。 - 共享库的一个主要目的是允许多个正在运行的进程共享存储器中相同的库代码,因而节约宝贵的存储器资源。
- 位置无关码(Position-Independent Code, PIC), GCC的
-fPIC
产生位置无关码。- 数据段使用一个全局偏移量表(Global Offset Table, GOT)。
- 不需要链接器修改库代码就可以在任何地址加载和执行的代码。
- 一个共享库中的.text段的一个副本可以被不同的长在运行的进程共享。
- Java有一个本地接口可以调用本地的C和C++函数。
异常控制流
- 异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。
。 - 异常可以分为四类:
- 中断(interrupt). --- 来自I/O设备的信号,是异步的。
- 陷阱(trap). --- 有意的异常, 同步的。
- 故障(fault). --- 潜在可恢复的错误, 是同步的。
- 终止(abort). --- 不可恢复的错误, 是同步的。
- 异常是允许操作系统提供进程(一个执行中程序的实例)的概念所需要的基本构造块。
- 进程提供应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
- 一个私有的地址空间, 它提供一个假象, 好像我们的程序独占地使用存储器系统。
- 一个程序为每个函数提供自己的私有空间,不能被其他进程写。
- 地址空间底部是保留给用户程序的,包括通常的文本、数据、堆和栈。
- 对32位Linux系统进程来说,代码段从0x08048000开始;
- 对64位Linux系统进程来说,代码段从0x00400000开始。
- 地址空间顶部保留给内核(内核执行指令时的代码、数据和栈)。
.
- 限制应用可以执行的指令以及它可以访问的地址空间范围。
- 控制寄存器中的一个位模式来提供这种功能。
- 该寄存器描述了进程当前享有的特权。
- 内核模式,超级用户模式。
- 用户模式, 不允许用户模式的进程直接引用地址空间中内核区的代码和数据。 --- 必须通过系统调用。
- 进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、陷入系统调用等异常。
- proc/文件系统,将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。
- 控制寄存器中的一个位模式来提供这种功能。
- 上下文是指内核重新启动一个被抢占的进程所需的状态。
- 通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态栈和各种内核数据结构(页表,进程表,文件表)。
.
- 通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态栈和各种内核数据结构(页表,进程表,文件表)。
- 一个终止了但还未被回收的进程称为僵死进程(zombie)。
.- 一个待处理的信号最多只能被处理一次,发送类型为k的信号,内核会设置pending的第k位,接收则清除pending的第k位。
- 进程组:
- 每个进程都有一个进程组,进程组是由一个正整数进程组ID来标识的。
- getgrp返回当前进程的进程组ID。
- 一个进程默认和它的父进程同属于一个进程组。
- 进程组的作用。
- 非本地跳转通过setjmp和longjmp函数来提供。
- 操作进程的工具:
- strace: 打印进程调用每个系统调用的轨迹。
- ps: 列出当前系统中的进程(包括僵死进程)。
- top: 打印关于当前进程资源使用的信息。
- pmap: 显示进程的存储器映射。
- 中断会污染高速缓存。
虚拟存储器
- 一个系统中海的进程是与其他进程共享CPU和主存资源的。
- 虚拟存储器(VM)是硬件异常,硬件地址翻译、主存、磁盘文件和内核软件的完美交互,为每个进程提供了一致的私有地址空间。
- 虚拟存储器提供三个重要的能力:
- 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,根据需要在磁盘和主存之间来回传送数据。
- 为每个进程提供了一致的地址空间,从而简化了存储器管理。
- 保护每个进程的地址空间不被其它进程破坏。
.- CPU通过生成一个虚拟地址(virtual address, VA)来访问主存。
- 把虚拟地址转换为物理地址的任务叫做地址翻译(address translation).
- CPU芯片上叫做存储器管理单元(Memory Management Unit, MMU)的专用硬件, 利用存放在主存中的查询表来动态翻译虚拟地址, 该表由操作系统管理。
- 地址空间(address space)是一个非负整数地址的有序集合。
- 主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
- 虚拟存储器(VM)被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。
- 每字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引。
- 磁盘上数组的内容被缓存在主存中。
- VM系统通过将虚拟存储器分割为大小固定的虚拟页(Virtual Page, VP). 物理存储器分割为物理页(Physiscal Page, PP).
- 任何虚拟页都可以放置在任何物理页中。
- 存放在物理存储器中的页表(page table)数据结构,将虚拟也映射到物理页。
。 - 许多虚拟页面可以映射到统一个共享物理页面上。
。 - 带许可位的页表:
. - 使用页表的地址翻译:
. - 翻译后备缓冲器(Translation Lookaside Buffer, TLB)是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE(页表项)组成的块。
。
.
.
. - 内部碎片的数量只取决于以前强求的模式和分配器的实现方式。
- 内部碎片实在一个已分配块比有效载荷大时发生的。
系统级I/O
- linux文件:
.
.
.- I/O重定向: 允许用户将磁盘文件和标准输入输出联系起来。
网络编程
- .
- .
- Web服务器,Web客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做HTTP(Hypertext Transfer Protocol, 超文本传输协议)。
- 磁盘文件称为静态内容。
- 运行可执行文件产生的输出称为动态内容。
- HTTP事务, 使用Unix的TELNET程序来和因特网上的任何Web服务器执行事务。
- HTTP请求,一个请求行(request line), 后面跟随零个或更多个请求报头,再跟随一个空的文本行来终止报头列表。
- HTTP支持许多不同的方法: 包括GET,POST,OPTIONS,HEAD,PUT,DELETE和TRACE。
- GET方法指导服务器生成和返回URI(Uniform Resource Identifier, 统一资源标识符)标识的内容。
- 基于线程化的并发服务器:
. - 可重复性:
- 有一类重要的线程安全函数,叫做可重入函数(reentrant function), 当被多个线程调用时,不会引用任何共享数据。
- 可重入函数不需要同步操作。
- 如果所有的函数参数都是值传递,并且所有的数据引用都是本地的自动栈变量(没有引用静态或全局变量),那么这个函数就是显式可重入的。
以上是关于深入理解计算机系统的主要内容,如果未能解决你的问题,请参考以下文章