iOS——APP启动流程
APP启动
APP启动主要分为两个阶段:pre-main和main之后,而APP的启动优化也主要是在这两个阶段进行的。
main之后的优化:1. 减少不必要的任务,2.必要的任务延迟执行,例如放在控制器界面等等。
APP启动的大致过程:
APP启动 -> 加载libSystem -> Runtime注册回调函数 -> 加载image(镜像文件) -> 执行map_images和load_images方法 -> 调用main函数。
查看pre-main耗时,添加DYLD_PRINT_STATISTICS到(Edit Scheme -> Run -> Arguments -> Environment Variables)就可以在控制台看到耗时
缺页错误
我们应该知道:任何程序能运行都是因为存在物理内存,也就是说,程序加入到物理内存才能得以运行,也就是虚拟内存映射到物理内存。这个过程是个使用懒加载方式完成系统到CPU的交互(翻译)的过程。
而这个过程因为懒加载映射方式的缘故,它是“有多少拿多少”,所以我们会通过一页一页的方式也就是page的方式去加载的,iOS的页的大小是16kb,而macOS是4kb。
也是因为是懒加载的方式,所以如果需要用到的时候发现物理内存中没有,就会报出“page fault”的缺页错误,然后缺的页会再加载放入物理内存。这个过程很短,可能30ms,也可能是10ms。
pre-main(main函数前)
pre-main 指的是在程序的 main() 函数执行之前进行的一些初始化工作。这个过程发生在程序的启动阶段,具体是在操作系统加载可执行文件后,调用 main() 函数之前。
例如:加载我们需要的库啊,系统自己调用加载一些依赖库啊,加载类到内存中去啊,加载分类方法并插入到类的方法列表中啊等等
二进制重排
二进制重排是一种优化应用启动性能的技术。它的核心思想是通过重新排列二进制文件中的函数顺序,使得在应用启动时需要频繁调用的函数被排列在一起,从而减少缺页错误(page fault)并加快启动速度。
比如说,当我们启动APP时,就会需要加载很多的页,正常都会有几千页,虽然一页耗时少,但是那个时刻要加载那么多页数,耗时会更长了。我们可以根据Instruments
的System Trace
找到Main Thread
进行查看应用的page in
也就是启动加载页数。苹果自用了二进制重排方案就可以优化这个的耗时,例如抖音的二进制重排,怎么找到所有的函数加载,将不必须在前面执行的函数放在后面。
二进制重排的难点
难点在于如何获取并确定这些函数的顺序。
二进制重排的流程
二进制重排流程
- 应用程序的启动时刻所加载的顺序是按照Build Phases的Compile Sources的顺序
- 去Build Settings中搜索 Write Link Map File设置为YES,就是写入。然后就是Path to Link Map File的地址。
- 找到build里面的txt格式的文件,如果是模拟器则为x86_64结尾的。这个就是现在的执行顺序
- 打开终端,cd到目录下创建order文件,例如:touch test.order
- 将你想要排序的函数依次写进去,然后再在Build Settings中的Order File的路径填写为test.order的文件路径,最后编译一下。
dyld、动态链接器
dyld在各种库加载映射到内存中去起到了至关重要的作用。
我们要研究dyld从APP启动到进入main函数究竟是怎么做的?
dyld流程剖析
我们看这个流程是为了看APP启动到main函数前,也就是dyld是如何将images(镜像文件:如动静态库等)链接到内存中去的。而在objc_init的时候是做了什么操作去调起dyld,以及dyld又如何回调至objc中。
我们根据查看底层的调用栈显示+load方法的调用流程为:_dyld_start
->dyldbootstrap::start
->dyld::_main
->dyld::initializeMainExecutable
->ImageLoader::runInitializers
->ImageLoader::processInitializers
->ImageLoader::recursiveInitialization
->dyld::notifySingle
->load_images
->+[ViewController load]
。
_dyld_start
_dyld_start
是启动时的入口点,它是用汇编语言实现的。
最主要的就是调用start
方法,以及dyld层加载结束后调用我们的main
方法。
这个函数的主要作用是调用dyldbootstrap::start
函数。
dyldbootstrap::start
这个函数也是中间过程,不必知道详细,只知道通过这个函数调用到dyld::_main函数了。
dyld::_main (重要
到这里已经是dyld重中之重了,这个函数的代码行数为849近1000行代码。其实上面的函数调用栈的最大作用也就是引导我们到这里。而这里也大概就是dyld的执行流程了,包括主程序的实例化再到通知进入程序的main函数这个过程。
_main做的事:
第一步:设置运行环境。
第二步:加载共享缓存。
第三部:dyld2/dyld3(ClosureMode闭包模式)加载程序。
第四步:实例化主程序。
第五步:加载插入动态库。
第六步:链接主程序和动态库。
第七步:弱绑定主程序。
第八步:执行初始化。
第九步:返回main函数。
大致流程总结
- 条件准备:环境,平台,版本,路径,主机信息等等;
- 确定是否有共享缓存并去加载(一般是非模拟器情况)
- 载入GDB调试器通知。(老版本的不重要,没用,不知道这个名词没关系)
- 添加dyld到UUID列表中,启用堆栈符号化。(没用,不需要知道)
- 实例化主程序,instantiateFromLoadedImage(镜像文件加载器,就是以mach-o的header方式加载主程序镜像。)
- 加载任何插入的库,(使用loadInsertedDylib)
- link(链接)主程序
- link 镜像文件(前面插入的库)
- 弱引用绑定主程序
- (最重要)运行所有初始化的程序。(使用initializeMainExecutable)
- 通知dyld可以进入main函数了。(使用notifyMonitoringDyldMain)
初始化流程源码剖析
initializeMainExecutable和runInitializers和processInitializers
我们可以根据上面的调用栈的顺序知道,dyld::_main
之后调用的就是dyld::initializeMainExecutable
,同时根据上面的流程知道这一步也是最重要的一步,但是实际上这段代码和runInitializers
和processInitializers
只是起到中间作用,它们最终调用的recursiveInitialization
才是我们真正重要的一步。
recursiveInitialization
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
……if ( fState < dyld_image_state_dependents_initialized-1 ) {uint8_t oldState = fState;// break cyclesfState = dyld_image_state_dependents_initialized-1;try {// initialize lower level libraries first// 优先初始化依赖的底层的库for(unsigned int i=0; i < libraryCount(); ++i) {ImageLoader* dependentImage = libImage(i);if ( dependentImage != NULL ) {
……else if ( dependentImage->fDepth >= fDepth ) {//依赖文件递归初始化dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);}}}
……fState = dyld_image_state_dependents_initialized;oldState = fState;//这里调用传递的状态是dyld_image_state_dependents_initialized,image传递的是自己。也就是最后调用了自己的+load。从libobjc.A.dylib开始调用。context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);// initialize this image//初始化镜像文件,调用c++构造函数。libSystem的libSystem_initializer就是在这里调用的。会调用到objc_init中。_dyld_objc_notify_register 中会调用自身的+load方法,然后c++构造函数。//1.调用libSystem_initializer->objc_init 注册回调。//2._dyld_objc_notify_register中调用 map_images,load_images,这里是首先初始化一些系统库,调用系统库的load_images。比如libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib。//3.自身的c++构造函数bool hasInitializers = this->doInitialization(context);// let anyone know we finished initializing this imagefState = dyld_image_state_initialized;oldState = fState;//这里调用不到+load方法。 notifySingle内部fState==dyld_image_state_dependents_initialized 才调用+load。context.notifySingle(dyld_image_state_initialized, this, NULL);
……}
……}recursiveSpinUnLock();
}
程序需要初始化的动态库image
是从libImage()
中获取,而libImage()
的数据是在链接动态库的时recursiveLoadLibraries
中的setLibImage
保存的image
。
整个过程是一个递归的过程,先初始化最底层的依赖库,再逐步初始化到自己。
**调用notifySingle最终调用到了objc中所有的+load方法。**这里第一个notifySingle
调用的是+load
方法,第二个notifySingle
由于参数是dyld_image_state_initialized
不会调用到+load
方法。这里的dyld_image_state_dependents_initialized
意思是依赖文件初始化完毕了,可以初始化自己了。
调用doInitialization
最终调用了c++的系统构造函数。先调用的是libSystem_initializer -> objc_init
进行注册回调。在回调中调用了map_images
、load_images(+load)
。这里的load_images
是调用一些加载一些系统库,比如:libdisp
notifySingle
notifySingle
是一个函数指针,在setContext
函数里赋值。
map_images与load_images什么时候调用
因为每个镜像文件的加载时机我们是不知道的,所以当镜像文件加载完毕后得有个回调(下句柄)告诉其处理完毕,接下来dyld得需要有个状态去标识,所以我们必须要用notifySingle进行通知。
map_images
:镜像文件的加载,引出read_images
。该方法很重要
load_images
:load方法的加载
map_images
是在notifyBatchPartial
调用的,也就是注册完通知就立马去调用。
而load_images
是在notifySingle
调用。

dyld3或dyld2(ClosureMode闭包模式)加载程序
iOS11引入dyld3闭包模式,以回调的方式加载,闭包模式加载速度更快,效率更高。iOS13后动态库和三方库都使ClosureMode加载。
dyld2和dyld3的调用是在dyld::_main
函数中的
dyld3:
使用mainClosure
来加载。
找到/创建mainClosure
后,通过launchWithClosure
启动主程序,启动失败后会有重新创建mainClosure
重新启动的逻辑。成功后返回result
(主程序入口main函数)。launchWithClosure
中的逻辑和dyld2启动主程序逻辑基本相同。
dyld2:启动主程序
实例化主程序instantiateFromLoadedImage
。sMainExecutable
是通过instantiateFromLoadedImage
赋值的,也就是把主程序加入allImages
中。
插入&加载动态库 loadInsertedDylib
。加载在loadInsertedDylib
中调用load(主程序和动态库都会添加到allImages
中loadAllImages
)
链接主程序和链接插入动态库(link,主程序链接在前)。在这个过程中记录了dyld加载的时长。可以通过配置环境变量打印出来。
绑定符号(非懒加载、弱符号),懒加载在调用时绑定。
初始化主程序initializeMainExecutable
,这个时候还没有执行到主程序中的代码。
找到主程序入口 LC_MAIN
(main函数),然后返回主程序。
1.1 动态库和静态库的认识
1.1.1 介绍
库是已写好的、供开发者使用的可复用代码,每个程序都要依赖很多基础的底层库。从本质上,库是一种可执行代码的二进制形式。可以被操作系统载入内存执行。库分为两种:静态库(.a .lib)和 动态库 (framework .so .dll)。 .a是纯二进制文件,.framework中除了有二进制文件外还有资源文件,.a文件不能直接使用,至少需要.h文件配合,而.framework可以直接使用。 .a + .h + sourceFile = .framework
所谓静态和动态是指链接过程,动静态是相对于编译期和运行期的,静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要载入静态库。而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
1.1.2 静态库
在链接阶段,会将汇编生成的目标文件.o 与 引用的库一起链接到可执行文件中。对应的链接方式称为 静态链接。 静态库中的所有指令都会包含进最终生成的文件中,静态库不能再包含其他的动态库或静态库,在动态链接库中还可以再包含其他的动态或静态链接库。
如果多个进程需要引用到【静态库】,在内存中就会存在多份拷贝,如上图中进程1 用到了静态库1、5,进程2也用到了静态库1、5,那么静态库1、5在编译期就分别被链接到了进程1和进程2中,假设静态库1占用2M内存,如果有20个这样的进程需要用到静态库1,将占用40M的空间。
特点:
- 静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。
- 使可执行文件变大,浪费空间和资源(占空间)
- 程序的更新、部署与发布不方便,需要全量更新。如果 某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。
优缺点: 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖 缺点:由于静态库会存在多分,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大
1.1.3 动态库
动态库在程序构建时并不会链接到目标代码中,而是在运行时才被载入,不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。
理解:
- 动态库包含一些可供应用程序或其他动态链接库调用的函数
- 在应用程序调用一个动态链接库里面的函数的时候,操作系统会将动态链接库的文件映射到进程的地址空间中,这样进程中所有的线程就可以调用动态链接库中的函数了
- 动态链接库加载完成后,并没有将代码编译到可执行文件中,这个时候动态链接库对于进程来说只是一些被放在地址进程空间附加的代码和数据
- 动态库在内存中只有一个,操作系统也只会加载一次到内存中。只是针对不同的进程进行各自的映射
- 代码段在内存中的权限都是只读的,所以多个程序虽然使用同一个动态库,但是并不会修改源代码
- 动态函数库的名字一般是libxxx.so,相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。
- 【动态库】在内存中只存在一份拷贝,如果某一进程需要用到动态库,只需在运行时动态载入即可。
特点:
- 动态库把对一些库函数的链接载入推迟到程序运行时期(占时间)。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单,不需要重新编译,属于增量更新。
优缺点:
优点:
- 减少打包后APP的大小,因为不需要拷贝至目标程序中
- 共享内存、节约资源,因为同一份库被多个程序使用
- 通过更新动态库即可更新程序,因为不需要重新编译 缺点:
- 动态库载入会带来一部分性能损失
注意:系统的.framework是动态库,自己建立的.framework是静态库
相关文章:

iOS——APP启动流程
APP启动 APP启动主要分为两个阶段:pre-main和main之后,而APP的启动优化也主要是在这两个阶段进行的。 main之后的优化:1. 减少不必要的任务,2.必要的任务延迟执行,例如放在控制器界面等等。 APP启动的大致过程&#…...

LLM模型:代码讲解Transformer运行原理
视频讲解、获取源码:LLM模型:代码讲解Transformer运行原理(1)_哔哩哔哩_bilibili 1 训练保存模型文件 2 模型推理 3 推理代码 import torch import tiktoken from wutenglan_model import WutenglanModelimport pyttsx3# 设置设备为CUDA(如果…...

虚幻引擎VR游戏开发02 | 性能优化设置
常识:VR需要保持至少90 FPS的刷新率,以避免用户体验到延迟或晕眩感。以下是优化性能的一系列设置(make sure the frame rate does not drop below a certain threshold) In project setting-> (以下十个设置都在pr…...

Web应用监控:URL事务监测指标解读
监控易是一款功能强大的IT监控软件,它能够实时监控各种IT资源和应用的运行状态,确保业务的连续性和稳定性。在Web应用监控方面,监控易提供了URL事务监测功能,通过模拟用户访问流程,监测整个事务的执行过程和性能表现。…...

redis之缓存淘汰策略
1.查看redis的最大占用内存 使用redis-cli命令连接redis服务端,输入命令:config get maxmemory 输出的值为0,0代表redis的最大占用内存等同于服务器的最大内存。 2.设置redis的最大占用内存 编辑redis的配置文件,并重启redis服务…...

CMake/C++:一个日志库spdlog
项目仓库 GitHub - gabime/spdlog: Fast C logging library.Fast C logging library. Contribute to gabime/spdlog development by creating an account on GitHub.https://github.com/gabime/spdlog 知乎参考贴 https://zhuanlan.zhihu.com/p/674073158 先将仓库clone一下 然…...

rig——管理不同R语言版本的工具
在Python中,我可以用Conda去管理多个版本的Python,包括一些Python模块,因此想在R语言中也找一个类似的工具。 之前在Mac上,有一个名为 Rswitch 的R语言版本管理工具,可以管理不同版本的R以及相应的R包。 现在想在Win…...

Java内存模型详解
1. 引言 在Java中,内存模型是非常重要的概念,它涉及到线程之间如何共享数据以及保证数据的一致性。了解Java内存模型对于开发高质量的多线程程序是至关重要的。 本篇博客将详细介绍Java内存模型的概念、原则、规则以及相关的概念和术语。同时ÿ…...

空气能热泵热水器
空气能热泵热水器压缩机把低温低压气态冷媒转换成高压高温气态,压缩机压缩功能转化的热量为q1,高温高压的气态冷媒与水进行热交换,高压的冷媒在常温下被冷却、冷凝为液态。这过程中,冷媒放出热量用来加热水,使水升温变…...

计算机毕业设计选题推荐-消防站管理系统-社区消防管理系统-Java/Python项目实战
✨作者主页:IT毕设梦工厂✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…...

移动UI:新手指引页面,跟着指引不迷路。
移动端新手指引在提供用户引导、提升用户体验、提高用户留存率、促进功能使用和降低用户流失率方面都有积极的作用。 移动端新手指引在应用程序或移动网站中有以下几个作用: 1. 提供用户引导: 新手指引可以帮助用户快速了解应用程序或移动网站的功能和…...

数据库MySQL基础
目录 一、数据库的介绍 1.数据库概述 (1)数据的存储方式 (2)数据库 2.常见数据库排行榜 二、数据库的安装与卸载 1.数据库的安装 2.数据库的卸载 三、数据库服务的启动与登录 1.Windows 服务方式启动 (1&…...

AUTOSAR_EXP_ARAComAPI的5章笔记(3)
5.3.4 Finding Services Proxy Class提供类(静态)方法来查找“连接”的服务实例。由于服务实例的可用性本质上是动态的(因为它有一个生命周期),所以ara::com提供了如下两种不同的方法来实现“FindService ”: StartFindService是一个类方法,它在后台启…...

【Godot4.3】基于纯绘图函数自定义的线框图控件
概述 同样是来自2023年7月份的一项实验性工作,基于纯绘图函数扩展的一套线框图控件。初期只实现了三个组件,矩形、占位框和垂直滚动条。 本文中的三个控件类已经经过了继承化的修改,使得代码更少。它们的继承关系如下: 源代码 W…...

申万宏源证券完善金融服务最后一公里闭环,让金融服务“零距离、全天候”
在数字化转型的浪潮中,申万宏源作为金融行业的先锋,持续探索科技如何赋能金融服务,以提升企业效率并优化客户服务体验。面对日益增长的视频化需求,传统的图文形式已难以满足市场与用户的新期待。为了应对这一挑战,申万…...

无需更换摄像头,无需施工改造,降低智能化升级成本的智慧工业开源了。
智慧工业视觉监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上…...

系统架构师考试学习笔记第三篇——架构设计高级知识(19)嵌入式系统架构设计理论与实践
本章考点: 第19课时主要学习嵌入式系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分)。在历年考试中,案例题对该部分内容都有固定考查,综合知识选择题目中有固定分值…...

centos8stream 修改为阿里云yum源
centos8stream 官方已经不再维护,导致该系统官方源实效,可以使用阿里云源进行替换 阿里云文档:centos-vault镜像_centos-vault下载地址_centos-vault安装教程-阿里巴巴开源镜像站 (aliyun.com) 咱们只需要执行下面命令,即可替换官…...

python转换并提取pdf文件中的图片
#安装fitz包 pip install pymupdf 脚本如下所示: import fitz import re import os import time import sysarguments sys.argvfor arg in arguments:print(arg)def file_name_list(base_dir):for i, j, k in os.walk(base_dir):name [i.replace(.pdf, ) for i …...

【MySQL】MySQL常用的数据类型——表的操作
前言: 🌟🌟本期讲解关于MySQL常用数据类型,表的简单使用,希望能帮到屏幕前的你。 🌈上期博客在这里:http://t.csdnimg.cn/wwaqe 🌈感兴趣的小伙伴看一看小编主页:GGBondl…...

自然语言处理系列五十三》文本聚类算法》文本聚类介绍及相关算法
注:此文章内容均节选自充电了么创始人,CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》(人工智能科学与技术丛书)【陈敬雷编著】【清华大学出版社】 文章目录 自然语言处理系列五十三文本聚类算法》文本聚类介绍及相关算法K…...

计算机网络(一) —— 网络基础入门
目录 一,关于网络 二,协议 2.1 协议是什么,有什么用? 2.2 协议标准谁定的? 2.3 协议分层 2.4 OSI 七层模型 2.5 TCP/IP 四层模型 三,网络传输基本流程 3.1 局域网中两台主机通信* 3.2 报文的封装与…...

从监控到智能:EasyCVR视频汇聚平台助力加油站安全监管升级转型
随着科技的不断进步,视频监控技术在各个行业的应用日益广泛,尤其在加油站这一关键领域,视频智能监管系统的应用显得尤为重要。TSINGSEE青犀视频EasyCVR视频汇聚平台作为一款基于“云-边-端”一体化架构的视频融合与AI智能分析平台,…...

日志服务管理
系统日志管理 sysklogd 系统日志服务 在 CentOS5 以及之前的发行版中,其采用的 sysklogd 服务来记录和管理系统日志的。 sysklogd 服务有两个模块: klogd: 用于记录 linux kernel 相关的日志 syslogd:用于记录用户空间应用日志…...

ROS 工具箱系统要求
ROS 工具箱系统要求 要为 ROS 或 ROS 2 生成自定义消息,或从 MATLAB 或 Simulink 软件中部署 ROS 或 ROS 2 节点,您必须构建必要的 ROS 或 ROS 2 软件包。要构建这些软件包,您必须具备 Python 软件、CMake 软件以及适用于您的平台的 C 编译器…...

CSS解析:定位和层叠上下文
许多开发人员对定位的理解很粗略,如果不完全了解定位,就很容易给自己挖坑。有时候可能会把错误的元素放在其他元素前面,要解决这个问题却没有那么简单。 一般的布局方法是用各种操作来控制文档流的行为。定位则不同:它将元素彻底…...

无名管道与有名管道的区别(C语言)
目录 一、引言 二、无名管道(匿名管道) 1.概念 2.特点 3.使用方法 三、有名管道(命名管道) 1.概念 2.特点 3.使用方法 四、总结 本文将详细介绍在C语言中无名管道(匿名管道)与有名管道(命名…...

Vue+Nginx前端项目多种方式部署一文搞定(练习源码自取)
目录 介绍 本地项目部署 nginx部署 云端服务器部署 介绍 对于Vue项目而言,Nginx可以轻松地配置来处理SPA的路由问题,即对于所有未定义的路径请求返回index.html,这样前端路由机制就可以接管URL的处理。此外,Nginx支持反向代理设…...

MATLAB 中双引号 ““ 和单引号 ‘‘ 的区别详解
在 MATLAB 中,双引号 "" 和单引号 都可以用来表示字符串,但它们的作用和底层类型是不同的。理解它们之间的区别,对于正确使用字符串处理功能非常重要。本文将深入探讨 MATLAB 中 "" 和 的区别,以及在实际编…...

Linux概述、远程连接、常用命令
Linux介绍 Linux操作系统介绍 Linux操作系统的特点 开源免费安全稳定可移植性好 Linux可以安装在不同的设备上 高性能 Linux的使用领域 应用服务器数据库服务器网络服务器虚拟化云计算嵌入式领域个人PC移动手机 Linux文件系统和目录 /:根目录,唯一/h…...