分类专栏: Linux—01—Linux简介与安装
一.GRUB简介
首先搞清楚与 GNU GRUB的关系。 GNU GRUB 分为 GNU GRUB Legacy 和 GNU GRUB2 两代 。GNU GRUB Legacy 其实就是原来的 GNU GRUB 0.xx ,最新版是 2005 年发布的 GNU GRUB 0.97 。目前已停止开发,并改名为 GNU GRUB Lagecy 。GNU GRUB2 是第二代 GRUB,它将取代原来的 GNU GRUB (例如0.9x版),但目前还处于开发阶段,尚未发布正式版。
而 GRUB4DOS 则是对 GNU GRUB Lagecy 的二次开发。该项目最早由 不点 在2003年发起,目前主要由他、bean 和另外几位中国人维护,很多人贡献了代码,再加上广大网友热心帮助,使得 GRUB4DOS 不断完善。在当前 新的 GRUB2 没有到位、老的 GNU GRUB Lagecy 又有诸多不完善 的情况下,GRUB4DOS 受到越来越多人的欢迎。不少人已经把 GRUB4DOS 当作仍然处于活跃开发当中的 grub legacy 了,当他们给 grub 0.XX 的补丁被搁浅时,他们又把补丁投递给了 GRUB4DOS。
1.引导加载程序简介
引导加载程序可以引导操作系统启动。当计算机要引导操作系统时,BIOS会读取引导介质上最前面的MBR记录。在单一的MBR中只能存储一个操作系统的引导记录,当需要多个操作系统时就会出现问题,所以需要更灵活的引导加载程序。
主引导记录本身要包含两类内容:引导加载程序和分区表。当BIOS引导时,它会寻找硬盘驱动器第一个扇区(MBR)中存储的数据,BIOS使用存储在MBR中的数据激活引导加载程序。
由于BIOS只能访问很少的数据,所以大部分引导加载程序分两个阶段进行引导。在引导的第1个阶段中,BIOS引导一部分引导加载程序,即初始程序加载程序(IPL)。IPL查询分区表,从而能够加载位于不同介质上任意位置的数据。首先通过这步操作来定位第二阶段引导加载程序(其中包含加载程序的其余部分)。
第2阶段引导加载程序是引导加载程序的主体,这是引导加载程序的真正部分,它包含有加载程序更需要磁盘空间的部分,比如用户界面和内核引导程序。
引导加载程序通常配置为主引导程序或二级引导加载程序。主引导程序是安装在MBR上的第一阶段引导加载程序。二级引导加载程序是安装在可引导分区的第一阶段引导加载程序。必须在MBR上安装单独的引导加载程序,并配置它将控制权转交给二级引导加载程序。
很多较新的Linux引导加载程序特别实用,因为它们提供了不同程度的交互,比如高级的GUI和加密的口令,以及通过选择操作系统进行引导的能力。这样,可以在具有多个物理磁盘的同一机器上共存多个操作系统。
2.GRUB概述
GNU GRUB(GRand Unified Bootloader)是一个将引导加载程序安装到主引导记录的程序,主引导记录位于一个硬盘开始的扇区。它允许位于主引导记录区中特定的命令来加载一个GRUB菜单或GRUB的命令环境。这使得用户能够开始操作系统的选择,在内核引导时传递特定命令给内核,或是在内核引导前确定一些系统参数(如可用的RAM大小)。
GRUB支持直接和链式加载的引导方法。GRUB能用于几乎所有操作系统、绝大多数流行的文件系统以及几乎所有的系统BIOS所能识别的硬盘。
3.GRUB特性
GRUB包含许多特性,这使得GRUB比其他可用的引导加载程序更加优越,下面列出一些比较重要的特性。
(1)GRUB在x86机器上提供一个真正基于命令行的、先于操作系统启动的环境。
(2)GRUB支持逻辑块寻址(LBA方式)。
LBA的主要作用是寻找驱动器上文件的地址并将其置于驱动器的硬件中,它被用在许多IDE和SCSI硬盘中。在使用LBA之前,硬盘驱动器遇到一个1024柱面的限制,即BIOS不能找到在1024柱面后的文件(比如一个引导加载程序或是内核文件)。只要系统BIOS能支持LBA模式(大多数都支持),那么LBA就允许GRUB超越1024柱面的限制,引导操作系统。
(3)GRUB的配置能在每次系统引导时被读取。
这就避免了用户每次改变引导选项时都要重写一次主引导记录。大多数的引导加载程序都不能读取配置文件,并使用它们来设置引导选项。比如,用户必需改变一个LILO的配置文件,然后执行一个命令将新的配置数据重写回系统的主引导记录。这种方法比GRUB所采用的方法更加危险,因为一个错误配置的主引导记录将使系统无法引导。在使用GRUB中,如果配置文件被错误配置并且引导,那它也仅仅简单地转到一个默认的命令行,允许用户手工输入命令来运行操作系统。
二.GRUB设备名称
GRUB 要求设备名被括在一个 ( ) 中。fd表示软盘,hd 表示硬盘(不区分 IDE 还是 SCSI)。其次设备是从 0 开始编号,分区也是如此,分区和设备之间用一个 ‘,’ 分开。
下面给出几个例子 :
(fd0) :表示整个软盘
(hd0,1) :表示 BIOS 中的第一个硬盘的第2个分区
(hd0,0)/boot/vmlinuz :表示 BIOS 中的第一个硬盘的第一个分区下的 boot/ 目录下的vmlinuz 文件。
如果没有指定某个分区,则表示使用整个设备,否则只使用指定的分区。
硬盘代号
硬盘搜索顺序
在GRUB中的代号
第一个
(hd0)(hd0,0)(hd0,1)(hd0,4)(hd0,5)… …
第二个
(hd1)(hd1,0)(hd1,1)(hd1,4)(hd1,5)… …
第三个
(hd2)(hd2,0)(hd2,1)(hd2,4)(hd2,5)… …
第四个
(hd3)(hd3,0)(hd3,1)(hd3,4)(hd3,5)… …
… …
… … … …
硬盘搜索顺序
LILO
GRUB
IDE1 master
hda, hda1, hda2
(hd0), (hd0,0), (hd0,1)
IDE1 slave
hdb, hdb1, hdb2
(hd1), (hd1,0), (hd1,1)
IDE2 master
hdc, hdc1, hdc2
(hd2), (hd2,0), (hd2,1)
IDE2 slave
hdd, hdd1, hdd2
(hd3), (hd3,0), (hd3,1)
三.GRUB配置文件
GRUB的配置文件默认为“/boot/grub/grub.conf”,在GRUB成功安装到硬盘主引导扇区(MBR)后,只要编辑该文件就可实现对GRUB的配置,无须重写GRUB到MBR。GRUB的配置都是通过位于/boot/grub/grub.conf的一个配置文件来完成的。
password –-md5 <密码1>
default=0
timeout=5
splashimage=(hd0,0) /boot/grub/splash.xpm.gz
color red/black green/blue
title FC
password –-md5 <密码2>
root (hd0,1)
kernel /vmlinuz-2.6.12-EL ro root=/dev/hda1 quiet vga=787
initrd /initrd-2.6.12-EL.img
title Windows
password –-md5 <密码3>
rootnoverify (hd0,0)
chainloader +1
1.#
以"#"开头的是注释行。
2.default=0
这个必须与title进行对照。以上面的清单为例,我们不是有两个title吗?按照前后顺序来排列,第一个title表示0,第二个title1,以此类推。这个default说的是,如果启动过程中,并没有选择其他的项目,那么就会用默认值(第一个title)来启动。
3.timeout=5
启动时显示菜单的秒数(单位是秒)。此时如果没有按下任何按键,就会用default设置值来进行启动。
4.splashimage=(hd0,0) /boot/grub/splash.xpm.gz
splashimage是在菜单上显示的一些图片或者是相关的影像数据。该设置有个地方比较有趣。因为在启动的过程中并没有硬盘,所以我们必须要明确指出某个文件在哪个分区的哪个目录;因此,上面设置的意思是:在(hd0,0)分区内的/boot/grub/splash.xpm.gz,该文件为启动时显示的画面。
5.color red/black green/blue
GRUB界面的颜色可以用color指令来指定。color中指定了两组颜色,第一组是正常文本的颜色,第二组是加亮文本(当前选择的菜单项)的颜色。对应于每组颜色,又需要指定两种颜色。第一种是前景的颜色,第二种是背景的颜色。因此,在color总共需要指定四种颜色,分别是,正常文本前景,正常文本背景,加亮文本前景和加亮文本背景。
颜色代码
black (黑色) red (红色) green (绿色) brown (棕色) blue (蓝色) magenta (紫色) cyan (青色) light-gray (灰色)
dark-gray (暗灰) light-red (亮红) light-green (亮绿) yellow (黄色) light-blue (亮蓝) light-magenta (亮紫) light-cyan (亮青) white (白色)
前景色可以使用全部16种颜色,而背景色只能使用前面8种颜色。
color命令一般是作为全局命令,在第一个title前使用。
6.hiddenmenu
表示启动时隐藏菜单,除非在 timeout 之前按下 ESC 才能看到菜单。如果想显示菜单,就将这个设置值注释掉。
7.title Fedora Core()
定义引导项目的名称。
8.password --md5 $opeVt0$Y.br.18LyAasRsGdSk
用于定义进入 GRUB 命令模式的密码。你还可以为每个操作系统都定义一个密码,方法是把 password 命令放在 title行之后。而且每个操作系统的引导密码可以不同,也可以和进入命令模式的那个密码不同,最大程度的保证 GRUB 的安全。在这里指定了使用MD5加密的口令。
9.root (hd0,0)
用于指定含有 stage 文件的分区。如果有 /boot 分区,则 root device 就是 /boot 分区,否则就是 / 分区。
10.kernel /boot/vmlinuz-2.6.12-EL ro root=/dev/hda1 quiet vga=787
指定操作系统内核文件,还可以在内核文件名后加上参数。例如kernel /vmlinuz ro root=LABEL=/ 表示以只读的方式挂载 / 分区,且根分区设备是 label 为 / 的那个设备;也可以直接给出设备名。
十进制代码与对应的分辨率与颜色质量
颜色质量/分辨率
640x480
800x600
1024x768
1280x1024
位
256
769
771
773
775
8
32768
784
787
790
793
15
65536
785
788
791
794
16
16.8M
786
789
792
795
32
11.initrd /boot/initrd-2.6.12-EL.img
用于指定 RAM Disk 文件,例如 initrd /initrd-2.xx.img 。
12.rootnoverify (root)
和 root 类似,但不会尝试挂载该分区。例如用于指定 windows 操作系统所在的分区。
13.chainloader +1
用于加载另外一个 boot loader ,通常是用于加载 windows 的 boot loader 。它的参数是一个 block list ,例如 chainloader (hd0,0)0+1 表示加载第一个磁盘的第一个分区的第1块,0 是开始位置(block 从0开始编号),+1 表示总共读取多少个 block。 所以 chainloader 2+0 表示读取第3个block (编号为2)。
14.makeactive
让启动区的引导项(记得 fdisk –l的显示结果吗?)具有活动(active)标志,不管是否加都可以。
与LILO的配置文件不同,grub.conf会在引导时被读取,当被修改时不必去更新MBR。
在grub.conf文件中可以使用很多其他参数,不过上面的参数就足以让机器可用了。要获得关于grub.conf的这些及其他参数的进一步资料,请参考GRUB的手册页(man grub.conf)。
四.GRUB安装
[root @test root]# grub
# 先安装在 Super Block 底下试看看:
grub> root (hd0,0) <==这里输入我要的 root 硬盘扇区!
Filesystem type is ext2fs, partition type 0x83
grub> setup (hd0,0) <==实际安装上来!使用 setup 指令!
Checking if "/boot/grub/stage1" exists... yes
Checking if "/boot/grub/stage2" exists... yes
Checking if "/boot/grub/e2fs_stage1_5" exists... yes
Running "embed /boot/grub/e2fs_stage1_5 (hd0,0)"... failed (this is not fatal)
Running "embed /boot/grub/e2fs_stage1_5 (hd0,0)"... failed (this is not fatal)
Running "install /boot/grub/stage1 (hd0,0) /boot/grub/stage2 p /boot/grub/menu
.lst "... succeeded
Done.
# 再安装在 MBR 试看看:
grub> root (hd0,0)
Filesystem type is ext2fs, partition type 0x83
grub> setup (hd0)
Checking if "/boot/grub/stage1" exists... yes
Checking if "/boot/grub/stage2" exists... yes
Checking if "/boot/grub/e2fs_stage1_5" exists... yes
Running "embed /boot/grub/e2fs_stage1_5 (hd0)"... 17 sectors are embedded.
succeeded
Running "install /boot/grub/stage1 (hd0) (hd0)1+17 p (hd0,0)/boot/grub/stage2
/boot/grub/menu.lst"... succeeded
Done.
grub> quit <==离开 grub 啰!
五.GRUB的MD5加密方法
1.用grub-md5-crypt成生GRUB的md5密码
通过grub-md5-crypt对GRUB的密码进行加密码运算,比如我们想设置grub的密码是123456,所以我们先要用md5进行对123456这个密码进行加密
# /sbin/grub-md5-crypt
PassWord: 在这里输入123456
Retype password: 再输入一次123456
$1$7uDL20$eSB.XRPG2A2Fv8AeH34nZ0
$1$7uDL20$eSB.XRPG2A2Fv8AeH34nZ0 就是通过grub-md5-crypt进行加密码后产生的值。这个值我们要记下来,还是有点用。
2.更改 /etc/grub.conf
比如我原来的/etc/grub.conf文件的内容是下面的。
default=1
timeout=10
splashimage=(hd0,7)/boot/grub/splash.xpm.gz
title Fedora Core (2.4.22-1.2061.nptl)
root (hd0,7)
kernel /boot/vmlinuz-2.4.22-1.2061.nptl ro root=LABEL=/
initrd /boot/initrd-2.4.22-1.2061.nptl.img
title Windows XP
rootnoverify (hd0,0)
chainloader +1
所以我要在/etc/grub.conf中加入 password --md5 $1$7uDL20$eSB.XRPG2A2Fv8AeH34nZ0 这行,及lock,应该加到哪呢,请看下面的更改实例;
timeout=10
splashimage=(hd0,7)/boot/grub/splash.xpm.gz
password --md5 $1$7uDL20$eSB.XRPG2A2Fv8AeH34nZ0
title Fedora Core (2.4.22-1.2061.nptl)
lock
root (hd0,7)
kernel /boot/vmlinuz-2.4.22-1.2061.nptl ro root=LABEL=/
initrd /boot/initrd-2.4.22-1.2061.nptl.img
title 视窗系统XP
rootnoverify (hd0,0)
chainloader +1
lock的意思就是把Redhat Fedora锁住了。如果启动时会提示错误。这时就应该按P键,然后输入密码就行了。
使用password,lock命令实现几种加密方法如下:
1) 单纯对GRUB界面加密,而不对被引导的系统加密 在timeout一行下面加一行: password --md5 PASSWORD
2) 对GRUB界面加密,同时对被引导的系统加密 在timeout一行下面加一行: password --md5 PASSWORD 在title一行下面加一行: lock
3) 同时存在多个被引导系统,针对特定的系统实例分别加密(未对GRUB操作界面加密) 在title一行下面加一行: lock 在lock一行下面紧贴着再加一行: password --md5 PASSWORD 注:lock不能独立使用.
我们仔细看一下,从上面的我们改过的/etc/grub.conf中是不是已用到了我们在第一步通过/grub-md5-crypt所产生的密码呢??是不是有点安全感了?
六.GRUB界面
1.启动菜单界面
正确安装Linux操作系统后,可从硬盘引导系统进入GRUB,启动菜单界面如下图所示,在该界面中可以使用的按键如下表所示。在该菜单界面中可以选择GRUB配置文件
按键
功能说明
↓↑
使用上下方向键,在启动菜单项间进行移动
Enter
输入回车键启动当前的菜单项
e
选择“e”键编辑当前的启动菜单项
a
选择“a”键添加内核的启动参数
c
选择“c”键进入GRUB的命令行方式(shell)
2.启动菜单项编辑界面
在GRUB的启动菜单界面中选择“e”键进入GRUB的启动菜单项编辑界面,如下图所示。该界面下可以使用的操作按键如下表所示。该界面提供了灵活的配置接口,对于调试操作系统启动配置非常有用。
按键
功能说明
↓↑
使用上下方向键,在启动菜单项间进行移动
b
选择“b”键启动当前的菜单项
e
选择“e”键编辑当前选中的行
d
选择“d”键删除当前行
c
选择“c”键进入GRUB的命令行方式
o
选择“o”键在当前行后面插入一行
O
选择“O”键在当前行前面插入一行
Esc
选择“Esc”键返回
3.命令行界面(shell)
GRUB有两种方法可以进入命令行界面,从GRUB启动菜单进入命令行界面或者在shell状态下使用GRUB命令进入命令行界面,使用两种方法获得的命令行界面稍有不同。由于grub命令是运行在Linux操作系统中的,受操作系统的限制很多命令不能使用。而从GRUB启动菜单进入命令行界面支持的命令比较完整。
(1)GRUB命令行界面的特点
GRUB命令行界面提供了方便友好的命令行交互方式,其主要特点包括:
提供在线帮助命令“help”,并且可以获得每条命令的详细帮助。
可使用左右方向键编辑行命令。
可使用上下方向键滚动历史命令。
可使用“Tab”键补全命令和路径。
(2)从GRUB启动菜单进入命令行界面
从GRUB的启动菜单界面或菜单项编辑界面选择“c”键可进入GRUB的命令行界面。如下图所示。说明:使用“Esc”键可返回菜单界面,使用help命令获得GRUB当前可使用的命令,把某个命令作为help命令的参数,可获得该命令的详细帮助说明。
(3)从Linux的shell进入GRUB命令行界面
使用grub命令也可以进入GRUB命令行界面,该命令的完整路径为“/sbin/grub”。
七.GRUB命令参考
GRUB中的命令可分为三类:
菜单命令,只能用于配置文件的全局部分。
常规命令,即能用于配置文件的全部部分,又能在命令行界面使用。
命令行和菜单项命令,即能用于配置文件的菜单项定义部分,以能用于命令行界面。
1.菜单命令
菜单命令只能用于grub配置文件的全局配置部分,不能用在grub命令行交互界面,菜单命令在配置文件中应放在其它命令之前。
1、default //设置默认启动的菜单项
2、fallback //设置启动某菜单项失败后反回的菜单项
3、hiddenmenu //隐藏菜单界面
4、timeout //设置菜单自动启动的延时时间
5、title //开始一个菜单项
2.常规命令
常规命令可以应该于配置文件和grub命令行交互界面,可使用的常规命令有
1、bootp //通过bootp初始化网络设备
2、color //设置菜单界面的颜色
3、device //指定设备文件作为驱动器
4、dhcp //通过DHCP初始化网络设备
5、hide //隐藏某分区
6、ifconfig //手工配置网络设备
7、pager //改变内部页程序的状态
8、partnew //新建一个主分区
9、parttype //改变分区的类型
10、password 为菜单界面设置口令
11、rarp //通过RARP初始化网络设置
12、serial //设置串口设备
13、setkey //设置键盘映射
14、splashimage //设置GRUB启动时的背景图片文件
15、termainal //选择终端类型
16、tftpserver //指定TFTP服务器
17、unhide //还原某隐藏分区
3.命令行和菜单项命令
命令行和菜单项命令可应该于GRUB配置文件的菜单项设置中,也可以用在GRUB命令交互界面。
1、bolcklist //显示某文件所在分区位置(block list notation)
2、boot //启动操作系统
3、cat //显示文件内容
4、chainloader //把启动控制权软交给另外的启动引导器
5、cmp //比较两个文件
6、configfile //加载已存在的GRUB配置文件
7、debug //设置为debug模式
8、displayapm //显示APM BIOS信息
9、displaymem //显示内存配置
10、embed //嵌入Stage 1.5文件
11、find //查找包括某文件的所有设备
12、fstest //测试文件系统
13、geometry //显示某驱动器的物理信息
14、halt //停止计算机运行(软件关机)
15、help //显示GRUB的命令帮助信息
16、impsprobe //查询对称多处理器(SMP)的信息
17、initrd //加载initrd文件
18、install //安装GRUB
19、ioprobe //查询某驱动器的输入输出(I/O)端口
20、kernel //引导操作系统内核
21、lock //锁定某GRUB导菜单项,使其输入密码后才可启动
22、makeactive //激活某主分区
23、map //虚拟映射某驱动器
24、md5crypt //使用MD5加密口令
25、module //加载模块
26、modulenounzip //加载模块不进行解压
27、pause //暂停并等待按键
28、quit //退出GRUB
29、reboot //重新启动计算机
30、read //读取内存中的内容
31、root //设置GRUB的root设备
32、rootnoverify //设备GRUB的root设备但不装载文件系统
33、savedefault //保存当前的启动菜单项为默认启动
34、setup //自动安装GRUB
35、testload //从文件系统中测试读取某文件
36、testvbe //测试VESA BIOS EXTENSION
37、uppermem //强制设置主机上位内存的大小
38、vbeprobe //查询VESA BIOS EXTENSION信息
八.GRUB修复
1.grub没有显示菜单怎么办?
当开机后进入grub界面但没了菜单,只剩下一个grub>提示符,怎么启动呢?别急,看下面:
grub>cat (hd0,6)/boot/grub/grub.conf (为了看参数)
grub>root (hd0,6)
grub>kernel (hd0,6) /vmlinuz-2.4.18-14 ro root=LABEL=/
grub>initrd (hd0,6) /initrd-2.4.18-14.img
grub>boot
启动了吧!以上有些数字要根据你的实际情况更改。以上这个方法也可以用于测试新编译的内核。
2.恢复被Windows破坏的grub
如果你用grub来引导Linux和windows,当windows出毛病重新安装后,会破坏MBR中的grub,这时需要恢复grub。
1.把linux安装光盘的第一张放到光驱,然后重新启动机器,在BOIS中把系统用光驱来引导。
2.等安装界面出来后,按F4键,也就是linux rescue模式。
3.一系列键盘以及几项简单的配制,过后就“继续”了这个过程,这里不说了,比较简单。
4.然后会出现这样的提示符: sh#
5.我们就可以操作GRUB了。输入grub: sh#grub会出现这样的提示符: grub>
我们就可以在这样的字符后面,输入: grub>root (hdX,Y) grub>setup (hd0)
如果成功会有一个successful......
这里的X,如果是一个盘,就是0,如果你所安装的Linux的根分区在第二个硬盘上,那X就是1了;Y,就是装有Linux系统所在的根分区。 setup (hd0)就是把GRUB写到硬盘的MBR上。
3.Linux操作系统中用安装盘来修复Grub
1.把安装盘的第一张放到光驱,然后重新启动机器,在BOIS中把系统用光驱来引导。
2.等安装界面出来后,按〔F4〕键,也就是linux rescue模式。
3.一系列键盘以及几项简单的配制,过后就〔继续〕了。。。这个过程,我不说了,比较简单。
4.然后会出现这样的字符:
sh#
5.我们就可以操作GRUB了。
sh#grub
会出现这样的字符:
grub>
我们就可以在这样的字符后面,输入:
grub>root (hdX,Y)
grub>setup (hd0)
如果成功会有一个successful......
这里的X,如果是一个盘,就是0,如果你所安装的linux的根分区在第二个硬盘上,那X就是1了;Y,就是装有linux系统所在的根分区。 setup (hd0)就是把GRUB写到硬盘的MBR上。
我来举个例子吧,如果以我的硬盘为例。我在第一个硬盘上装了XP,在第一个硬盘的hda9个装了RH73.我总共有两个硬盘。如果我把GRUB丢了,就用这种办法找回来。 如果你不知道你的linux安装到哪个分区上,也就是说,不知道这个Y是多少,这也不要紧,先输入root (hdX,然后用[TAB]来查看,一下就明白了。
操作如下:〔前面开机启动的上面有说明〕
sh# grub
会出现下面的字样的:
grub>
然后再这样操作,如果我知道我的linux装在第一个硬盘上,但我不知道装在哪个分区上,就可以先输入root (hd0,然后用〔TAB〕键来补齐,然后就明白了。 grub>root (hd0,8)
grub>setup (hd0)
最后就是按一下〔RESET〕键,重新启动,就OK了。
如果有多个Windows 系统,怎么才能引导出来呢?应该用hide 和unhide指令操作;比如我们安装了两个Windows ,一个是位于(hd0,0)的windows 98 ,另一个是安装的是位于(hd0,1)的WindowsXP;这时我们就要用到hide指令了;
title Win98
unhide (hd0,0)
hide (hd0,1)
rootnoverify (hd0,0)
chainloader +1
makeactive
title WinXP
unhide (hd0,1)
hide (hd0,0)
rootnoverify (hd0,1)
chainloader +1
makeactive
————————————————
版权声明:本文为CSDN博主「ztguang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ztguang/article/details/51011784