转载文章——从HelloWorld学习操作系统

转载地址:https://my.oschina.net/hosee/blog/673628?p=%7b%7bcurrentPage+1%7d%7d

本文就将系统性的串联起那些知识点,方便复习和回顾。本文适合已经有操作系统基础的同学,一起回顾知识,本文并不详细讲解每个算法,本文意在知识串联。

通过一个例子来串联所有的知识点:

写了一个C语言程序:

#include
main()
{
 
puts(“Hello World!\n”);
}

目的是希望在屏幕中看到Hello World的字样。

那么在运行这个C语言程序时,操作系统做了什么呢?

1. 首先要启动程序执行,用户要告诉操作系统执行程序

如何告知:

  • 可以鼠标双击程序
  • 命令行输入命令
  • ……

2. 操作系统知道用户的请求之后,就会根据用户提供的文件名到磁盘上找到这个程序的相关信息,找到信息之后,会去检查这个程序是不是一个可执行文件,如果是可执行文件,操作系统会根据程序首部信息来确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。

文件系统是指操作系统中统一管理信息资源的一种软件,管理文件的存储、检索、更新,提供安全可靠的共享和保护手段,并且方便用户使用。

文件按性质和用途分类:普通文件,目录文件,特殊文件(设备文件),管道文件,套接字。

文件的存储介质:磁盘(包括SSD),磁带,光盘,U盘……

物理块是信息存储、传输、分配的独立单位。存储设备划分为大小相等的物理块,统一编号。

一次访问磁盘的请求:

  • 寻道:磁头移动定位到指定磁道。
  • 旋转延迟:等待指定扇区从磁头下旋转经过。
  • 数据传输:数据在磁盘与内存之间的实际传输。

SSD没有寻道和旋转延迟的时间消耗,所以速度快。

文件控制块:为管理文件而设置的数据结构,保存管理文件所需的所有有关信息。

常用属性:文件名,文件号,文件大小,文件地址,创建时间,最后修改时间,最后访问时间,保护,口令,创建者,当前拥有者,文件类型,共享计数,各种标志。

文件目录:统一管理每个文件的元数据,以支持文件名到文件物理地址的转换。将所有文件的管理信息组织在一起,即构成文件目录。

目录文件:将文件目录以文件的形式存放在磁盘上。

目录项:构成文件目录的基本单元,目录项可以是FCB,目录是文件控制块的有序集合。

文件的物理结构:文件在存储介质上的存放方式。

物理结构:

1. 连续结构:文件的信息存放在若干连续的物理块中。

    FCB中保存文件块的起始块号和长度。

    优点:支持随机存取和顺序存取,所需的寻道时间和寻道次数最少,可以同时读入多个块,检索一个块很容易。

    缺点:文件不能动态增长,不利于文件插入和删除,有外部碎片(紧缩技术)

2. 链接结构:一个文件的信息存放在若干不连续的物理块中,各块之间通过指针连接,前一个物理块指向下一个物理块。

    FCB只需要保存起始块号

    优点:提高了磁盘空间利用率,有利于文件的插入和删除,有利于文件动态扩充。

    缺点:存取速度慢,不适于随机存取,可靠性问题(如指针出错),更多的寻道次数和寻道时间,链接指针占有一定空间。

可以对链接结构进行变形:文件分配表(FAT),早期windows和U盘使用的结构。

FAT存放了所有的链接指针,每一个物理块对应FAT的一行。

《转载文章——从HelloWorld学习操作系统》

0表示空闲物理块,-1表示文件最后一块。

文件的起始块号保存在文件的FCB中。

3. 索引结构:一个文件的信息存放在若干不连续物理块中,系统为每个文件建立一个专用数据结构——索引表,并将这些物理块的块号存放在索引表中。

索引表就是磁盘块地址数组,其中第i个条目指向文件的第i块。

FCB中用一个字段保存索引表的位置。

《转载文章——从HelloWorld学习操作系统》

索引结构优点:保持了链接结构的优点,解决了链接结构的缺点,既能顺序存取,又能随机存取,满足了文件动态增长,插入删除的要求,能充分利用磁盘。

缺点:较多的寻道时间和寻道次数,索引表本身带来了系统开销。

索引表有可能很大,需要多个物理块保存,就有多级索引和综合索引。

多级索引:

《转载文章——从HelloWorld学习操作系统》

UNIX三级索引结构:

《转载文章——从HelloWorld学习操作系统》

访问一个文件:文件名->文件目录->FCB->磁盘

提高文件系统性能:

磁盘调度:当有多个访盘请求等待时,采用一定的策略,对这些请求的服务顺序调整安排。降低平均磁盘服务时间,公平,高效。

磁盘调度算法:

  1. 先来先服务(FCFS),按访问请求到达的先后顺序服务。简单,公平,但是效率不高,相临两次请求可能会造成最内到最外柱面寻道,使磁头反复移动,增加了服务时间,对机械不利。
  2. 最短寻道时间优先,优先选择距当前磁头最近的访问请求进行服务,主要考虑寻道优先。改善了磁盘平均服务时间,但是造成某些访问请求长期等待得不到服务。
  3. 扫描算法(SCAN),当设备无访问请求时,磁头不动;当有访问请求时,磁头按一个方向移动,在移动过程中对遇到的访问请求进行服务,然后判断该方向上是否还有访问请求,如果有则继续

《转载文章——从HelloWorld学习操作系统》

  1. 单向扫描调度算法(C-SCAN),总是从0号柱面开始向里扫描,移动到达最后一个柱面后,立即带动读写磁头快速返回到0号柱面。减少了新请求的最大延迟。
  2. N-step-SCAN策略,把磁盘请求队列分成长度为N的子队列,每一次用SCAN处理一个子队列,克服“磁头臂的粘性”。
  3. FSCAN,使用两个子队列,扫描开始时,所有请求都在一个队列中,而另一个队列为空。扫描过程中,所有新到的请求都保存在另一个队列中,对新请求的服务延迟到处理完所有老请求之后。
  4. 旋转调度算法,根据延迟时间来决定执行次序的调度。三种情况:1. 若干等待访问者请求访问同一磁头上的不同扇区,2. 若干等待访问者请求访问不同磁头上的不同编号的扇区,3. 若干等待访问者请求访问不同磁头上具有相同的扇区。

《转载文章——从HelloWorld学习操作系统》

3. 为了执行这个helloworld程序,操作系统会创建一个新的进程,并将该程序的可执行文件格式映射到该进程结构,表示由该进程来执行这个程序。

进程是具有独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的独立单位。

PCB,进程控制块,操作系统用于管理控制进程的一个专门数据结构,进程与PCB是一一对应的。

PCB中有:

进程描述信息:进程标识符(唯一),进程名,用户标识符,进程组关系

进程控制信息:优先级,代码执行入口地址,程序的磁盘地址,运行统计信息(执行时间,页面调度),进程间同步和通信,进程的队列指针,进程的消息队列指针。

所拥有的资源和使用情况:虚拟地址空间的状况,打开文件列表

CPU现场信息:寄存器值(通用寄存器,程序计数器PC,进程状态字PSW,栈指针),指向该进程页表的指针。

进程表:所有PCB的集合。

进程状态:

《转载文章——从HelloWorld学习操作系统》

操作系统为每一类进程(就绪、等待……)建立一个或多个队列,队列元素为PCB,伴随进程状态改变,其PCB从一个队列进入另一个队列。

《转载文章——从HelloWorld学习操作系统》

进程的创建:

  • 给新进程分配一个唯一标识以及PCB
  • 为进程分配地址空间
  • 初始化PCB(设置默认值,如状态为NEW……)
  • 设置相应的队列指针(如把新进程加到就绪队列链表中)

操作系统为每个进程都分配了一个地址空间。

由于性能,开销等考虑,引入了线程的概念。

线程的开销小,创建新线程花费的时间少,线程间切换花费时间少,线程之间互相通信无须调用内核(同一进程的线程共享内存和文件)

线程是进程中的一个运行实体,是CPU的调度单位。

4. 操作系统将控制权交给调度程序,如果调度程序选中了helloworld程序,由操作系统为该程序设置CPU上下文环境,并跳到程序开始处,准备执行该程序。那么下一个指令周期就是执行helloworld程序了。

CPU调度:按一定的调度算法从就绪队列中选择一个进程,把CPU的使用权交给被选择的进程。如果没有就绪进程,系统会安排一个空闲进程。

CPU调度需要解决三个问题:调度算法、调度时机、调度过程。

调度算法:

占有CPU的方式:

抢占式和非抢占式

时间片轮转

  • 先来先服务(FCFS)
  • 最短作业优先(SJF)
  • 最短剩余时间优先(SRTN)
  • 最高响应比优先(HRRN)
  • 多级反馈队列(Feedback)
  • 最高优先级调度
  • 轮转调度(Round Robin),为短任务改善平均响应时间,每个进程分配一个时间片

《转载文章——从HelloWorld学习操作系统》

典型系统的调度算法:

《转载文章——从HelloWorld学习操作系统》

5. 当执行helloworld程序的第一条指令时,会发生缺页异常(只有将代码和数据读入内存才能执行程序,执行第一条指令时,还没有将代码数据读入内存,那么这个时候,硬件机制会捕获出缺页异常,并且把控制权交给操作系统)

6. 操作系统管理了计算机系统中的内存,(如果是页式管理)操作系统会分配一页物理内存,根据前面计算出的程序的磁盘块地址,将helloworld程序从磁盘读入内存,然后继续执行helloworld程序。有的时候程序很大,一页内存不够,那么会多次产生缺页异常,然后从磁盘中读入程序到内存

我们已经知道,每个进程都有自己的进程地址空间,并且进程要装入内存才能运行。那么如何将进程地址空间装入内存就是一个必须解决的问题。

通过上面的描述,我们可以知道,进程中的进程地址空间的地址,并不是最终的物理地址。

因此需要地址重定位(地址转换,从进程地址转换成物理地址)来实验进程的加载。

我们把进程地址称为逻辑地址/相对地址/虚拟地址,而内存存储单元中的地址称为物理地址/绝对地址/实地址,很明显,只有物理地址能够直接寻址。

地址重定位分为静态重定位和动态重定位

静态重定位:当用户程序加载到内存时,一次性实现逻辑地址到物理地址的转换。但是要求这个程序在内存中的位置不能改变。

动态重定位:在程序加载到内存中,不改变地址,仍然是逻辑地址。在程序执行过程当中再进行地址转换,即逐条指令执行完成转换。由MMU(内存管理单元)来加速重定位。

现在已经可以将程序加载到内存了,那么该如何高效地分配内存给某个进程呢?

内存分配算法:

  • 首次适配(第一个满足)
  • 下次适配(从上次找到的空闲区往下找)
  • 最佳适配(查找整个空闲区表,找到满足要求的最小空闲区)
  • 最差适配(总是分配满足进程要求的最大空闲区)

当内存归还后,则面临着回收问题,将前后空闲空间合并。

一种经典的内存分配方案:伙伴系统

将内存按2的幂进行划分,组成若干的空闲块链表,查找该链表找到能满足进程需求的最佳匹配块。

《转载文章——从HelloWorld学习操作系统》

基本内存管理方案

《转载文章——从HelloWorld学习操作系统》

进程进入内存的连续区域:

单一连续区,内存利用率低

固定分区,浪费空间

可变分区,剩余部分导致大量外碎片,碎片解决方案,紧缩技术(移动程序,将所有小的空闲区合并成较大空闲区)

进程进入内存不连续区域:

页式存储管理:

进程地址空间被划分为大小相等的部分,称为页或者页面。内存地址空间按同样大小分为大小相等的部分,称为页框。

内存分配策略:以页为单位进行分配,并按进程需要的页数来分配,逻辑上相邻的页,物理上不一定相邻。

《转载文章——从HelloWorld学习操作系统》

 

《转载文章——从HelloWorld学习操作系统》

页表记录了从逻辑页号到页框号的映射关系。

每一个进程一个页表,存放在内存。页表的起始地址在某个寄存器中。

页式存储管理中的地址转换过程:CPU取到逻辑地址,自动划分为页号和页内地址,用页号查页表,得到页框号,再与页内地址拼接成物理地址。

段式存储管理:

用户进程地址按程序自身逻辑关系划分为若干个程序段,每个程序段都有一个段名。

内存空间被动态划分为不等长区域。

内存分配策略:以段为单位进行分配,每段在内存中占据连续空间,但各段之间可以不相邻。

《转载文章——从HelloWorld学习操作系统》

与页式不同的是,段号和段内地址不能自动划分,需要显示给出。

与页式相同,使用段表来记录关联关系。

《转载文章——从HelloWorld学习操作系统》

地址转换过程与页表也相似:CPU取到逻辑地址后,用段号查段表,得到该段在内存中的起始地址,与段内偏移地址拼接计算出物理地址。

段页式存储管理:

用户进程按段划分,内存划分同页式存储管理

《转载文章——从HelloWorld学习操作系统》

段页式存储管理需要段表和页表

段表记录每一段的页表起始地址和页表长度。

页表与页式存储管理相同

一个进程被分为若干段,需要一个段表,而每一段按照页式存储管理分配,又对应一张页表,所以一个进程对应一个段表和多个页表。

总结:

《转载文章——从HelloWorld学习操作系统》

那么当内存不足时该如何管理呢?

内存“扩容”技术:内存紧缩(可变分区),覆盖技术,交换技术(将某些进程暂时移到外存),虚拟存储技术。

虚拟存储技术:当进程运行时,先将其一部分装入内存,另一部分留在磁盘,当要执行的指令或者访问的数据不在内存中时,由操作系统自动完成将它们从磁盘调入内存的工作。

把内存和磁盘结合起来,得到更大的“内存”,就是虚存。

虚拟存储技术和页式存储管理相结合,就产生了虚拟页式存储管理。

虚拟页式存储管理基本思想:

进程开始执行之前,不是装入全部页面,而是装入一个或者零个页面,之后根据进程需要,动态装入其他页面,当内存已满,而又需要装入新的页面,则根据某种算法置换内存中的某个页面,以便装入新的页面。

由于页表数量太大,引入了多级页表。

按照传统的地址转换方式:虚拟地址->查页表->页框号->物理地址,这样每个进程都要一个页表。页表占据了很多空间。

解决这个问题:从物理地址出发,整个系统就建立一张页表(因为物理地址大小固定),页表项纪录进程i的某虚拟地址与页框号的映射关系。

但是仍然有一个问题存在,由于多级页表的存在,每次访问页表都要去访问内存,那么需要多次访问内存,由于CPU的指令处理速度与内存指令的访问速度差异大,CPU的速度得不到充分利用。

为了解决这个问题,由于程序访问局部性原理,从而引入了快表(TLB),用来加快地址转换的速度。

快表由cache组成,访问速度快。

引入快表后的地址转换过程:

虚页号->查快表(并行比较)

如果命中,则找到页表项

如果没有命中,用虚页号查页表得到页表项

当得到页表项后看到有效位,如果有效位是1,说明该页已经在内存中,则运行

如果是0,则抛出缺页异常。

当缺页时,操作系统就要将页面调入内存,如果内存满了,必须要将一些页面暂时调出到外存中。

那么置换的策略有哪些呢?

  1. 最佳页面置换算法(OPT)(置换之后或者最远的将来才会用到的页面)
  1. 先进先出算法(FIFO)
  2. 第二次机会算法(SCR)按照FIFO选择某一页面,检查其访问位,如果访问位为0,则置换,如果为1,说明最近被访问过,则不置换,并将访问位设为0
  3. 时钟算法(CLOCK)
  4. 最近未使用算法(NRU),选择最近一段时间未使用的一页。
  5. 最近最少使用算法(LRU),选择最后一次访问时间距离当前时间最长的一页并置换,性能最接近OPT
  6. 最不经常使用算法(NFU),选择访问次数最少的置换。

7. helloworld程序执行puts函数(系统调用),在显示器上写一字符串。

8. 由于puts函数是系统调用,控制权又交到操作系统上。操作系统找到要将字符串送往的的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程。

CPU与I/O:

减少或缓解速度差距->缓冲技术

使CPU不等待I/O->异步I/O

让CPU摆脱I/O操作->DMA,通道

9. 控制设备的进程告诉设备的窗口系统它要显示字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区。

10. 视频硬件将像素转换成显示器可以接受的一组控制/数据信号。

11. 显示器解释信号,激发液晶屏。

12. 在显示器上看到hello world。

 

从上面的过程中,我们能发现,CPU上时而运行用户程序,时而运行操作系统程序。运行操作系统程序,我们称CPU处在内核态,运行用户程序,我们称CPU处在用户态

而这两种CPU状态之间的转换:

从用户态到内核态,只能通过中断/异常/陷入进制(陷入指令是一条特殊的指令,提供给用户程序的接口,用于调用操作系统的功能/服务,例如int,trap,syscall,sysenter/sysexit)

而内核态到用户态,设置程序状态字PSW.

中断/异常机制,是操作系统的驱动力,我们可以说,操作系统是中断驱动的。

中断/异常的概念:CPU对系统发生的某个事件作出的反应。

CPU暂停正在执行的程序,保留现场后自动转去执行相应事件的处理程序。处理完成后返回断点,继续执行刚才被打断的程序。

中断和异常的区别在于,中断是由外部引发的,而异常则是该程序自己产生的。

CPU何时去响应中断?CPU会在每一条指令执行最后,去扫描中断寄存器,查看是否有中断。若有中断,中断硬件将该中断触发器内容按规定编码送入PSW的相应位,称为中断码,通过查中断向量表引出中断处理程序。

除此之外,当然还有进程互斥同步问题。

进程互斥:由于各进程要求使用共享资源(变量、文件等),而这些资源需要排他性使用,各进程间竞争使用这些资源。这一关系称为进程互斥。

进程互斥软件解决方案:

Dekker算法:

P进程:

pturn = true;
while(qturn)
{
   
if(turn == 2)
    {
       pturn =
false;
      
while(turn == 2);
       pturn =
true;
    }
}

临界区
turn = 2;
pturn = false;

Q进程:

qturn = true;
while(pturn)
{
   
if(turn == 1)
    {
       qturn =
false;
      
while(turn == 1);
       qturn =
true;
    }
}

临界区
turn = 2;
qturn = false;

pturn和qturn表示对应的进程想进临界区,如果都想进临界区,则通过turn来判断自己是否要让出CPU。从而实现互斥。

Peterson算法:

克服了Dekker算法强制轮流的缺点。

i表示进程号

进程i:
……
enter_region(i);
临界区
leave_region(i);
……

int turn;//轮到谁
int interested[N];//兴趣数组,初始都为false,表示某个进程想进临界区
void enter_region(int process)//假设这里两个进程的进程号是0和1
{
    
int other;//表示另一个进程的进程号
     other =
1 – process;
     interested[process] =
true;
     turn = process;
    
while(turn == process && interested[other] == true);
}
void leave_region(int process)
{
   interseted[process] =
false;
}

这里的turn变量要注意一下,turn表示轮到谁来进入临界区,如果两个进程都想进入临界区,可以发现trun变量会被后赋值的进程替换到先赋值的进程。

Peterson算法希望先想进临界区的进程先进去,那么在while循环中就产生了判断,如果turn是当前进程号(表示该进程是后想进入临界区的),则一直在while循环中等待,当然还需要判断另一个进程是否想进入临界区(interested[other]==true),如果另一个进程不想进入临界区,就没必要等待了。

Peterson算法Java实现:

public class Peterson implements Runnable {

private static boolean[] in = { false, false };
   
private static volatile int turn = -1;

public static void main(String[] args) {
       
new Thread(new Peterson(0), “Thread – 0”).start();
       
new Thread(new Peterson(1), “Thread – 1”).start();
    }

private final int id;

public Peterson(int i) {
        id = i;
    }

private int other() {
       
return id == 0 ? 1 : 0;
    }

@Override
   
public void run() {
       
in[id] = true;
        turn = other();
       
while (in[other()] && turn == other()) {
            System.out.println(
“[“ + id + “] – Waiting…”);
        }
        System.out.println(
“[“ + id + “] – Working (“
                + ((!
in[other()]) ? “other done” : “my turn”) + “)”);
       
in[id] = false;
    }}

进程的同步:指系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成一项任务。

解决方法:

  • 信号量
  • 管程(信号量编程易出错),Java中的synchronize
  • 进程间通信IPC(由于信号量和管程只能传递少量信息,不能传递大量信息,并且管程不使用与多处理器的情况),进程通信的基本方式有1.消息传递 2.共享内存 3.管道 4.套接字 5.远程过程调用

当然还有死锁问题:

产生死锁的必要条件:

  1. 互斥使用(资源独占):一个资源每次只能给一个进程使用。
  2. 占有且等待:进程在申请新的资源的同时保持对原有资源的占有。
  3. 不可抢占:资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放。
  4. 循环等待:存在一个进程等待队列,形成一个进程等待环路。

资源分配图:用有向图描述系统资源和进程的状态。

如:

《转载文章——从HelloWorld学习操作系统》

如果资源分配图中没有环路,则系统中没有死锁,如果图中存在环路,能可能存在死锁。

《转载文章——从HelloWorld学习操作系统》

如果每个资源类中只包含一个资源实例,则环路是死锁存在的充分必要条件。

死锁预防:

  1. 破坏“互斥使用/资源独占”条件:资源转换技术,把独占资源变成共享资源,SPOOLING技术的引入。
  2. 破坏“占有且等待”条件:1.必须一次性申请所有资源,2.必须释放资源才能申请
  3. 破坏“不可抢占”条件:通过优先级不同,能够抢占。
  4. 破坏“循环等待”条件:资源有序分配法,进程在申请资源时必须严格按资源编号的递增次序进行,否则操作系统不予分配。

死锁避免:银行家算法,安全状态一定没有死锁发生。

银行家算法总的来说就是,给每个用户贷款的钱不会超过银行钱的总量,但是所有用户贷款的钱的总和是可以超过银行钱的总量的。

死锁检测与解除:允许死锁发生,但是操作系统会不断监视死锁是否真的发生。一旦死锁发生,就会采用专门的措施,以最小代价来解除死锁,恢复操作系统运行。

让我们再次总结一下HelloWorld程序的运行。

当我们运行HelloWorld程序时,操作系统会根据文件名去找文件目录,然后找到了FCB,通过FCB里的物理地址找到磁盘上对应的文件。

那么FCB是如何得到文件的物理地址的呢?这和文件的物理结构有关,文件的物理结构有连续结构、链表结构、索引结构,不同结构中FCB保存的信息不同。

得到物理地址以后,从磁盘上读取文件需要经过寻道,旋转延迟,数据传输三部分。那么如何高效地从磁盘上读取文件呢?就可以运用不同的磁盘调度算法,譬如先来先服务,最短寻道时间优先,扫描算法,旋转调度算法等等。

得到文件后,操作系统会创建一个新的进程去执行这个程序。进程与PCB是一一对应的,PCB中保存了进程的各类信息,系统为每个进程分配一个地址空间,这个地址空间是虚拟地址。

有了进程去运行这个程序后,就等着CPU调度这个进程了。CPU调度算法有先来先服务,最短作业优先,最短剩余时间优先,最高响应比优先,轮换调度等等。

当CPU选择调度了这个程序,想要运行这个程序的第一条指令时,会发生缺页异常,因为代码数据还没有读入内存,有的时候程序很大,一页内存不够,那么会多次产生缺页异常,进程必须进入内存才能被运行,需要通过地址重定位将进程的虚拟地址转换成物理地址,不同的内存管理方式会有不同的转换方式,比如页式存储管理,段式存储管理,段页式存储管理,加了虚拟存储技术以后,还有虚拟页式存储管理,由于使用虚拟存储技术,在内存满时,需要将一些页面暂时调到外存,置换的算法有最佳页面置换算法,先进先出算法,最近未使用算法,最近最少使用算法等等。

现在进程被加载到了内存,该如何高效地分配内存给这个进程呢?内存分配算法:首次匹配,下次匹配,最佳匹配,最差匹配。如果此时内存满了,则会调用刚刚说的置换算法。

此时CPU已经成功运行了这个程序。之后需要显示的字符串将会交给显示设备的进程。最后是一系列硬件上的处理,成功让显示屏显示HelloWorld。

 

来自 <https://my.oschina.net/hosee/blog/673628?p={{currentPage+1}}>

 

    原文作者:伊甸一点
    原文地址: https://www.cnblogs.com/zpfbuaa/p/5987295.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞