From 8e492188696edbe1e7b27c281ffc66d0d651d5b3 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 21:09:35 +0800 Subject: [PATCH 01/13] Add GitHub Actions for MkDocs deployment --- README.md | 76 +- docs/index.md | 12 +- docs/ohlab/ohlab-part1.md | 1290 ++++++++++++------------- docs/ohlab/ohlab-part2.md | 1436 ++++++++++++++-------------- docs/shelllab/shelllab.md | 1688 ++++++++++++++++----------------- docs/syscalllab/syscalllab.md | 1586 +++++++++++++++---------------- docs/todo.md | 30 +- mkdocs.yml | 54 +- requirements.txt | 4 +- workflows/deploy.yml | 26 + 10 files changed, 3114 insertions(+), 3088 deletions(-) create mode 100644 workflows/deploy.yml diff --git a/README.md b/README.md index dc62528..becb88b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,38 @@ -# USTC 操作系统原理与设计 实验文档 - -所有实验文档位于 [./docs](./docs/) 目录中,每个实验单独存放独立的子目录,文档中使用的图片存放在相应子目录的 assets 目录里。目录与实验对应如下: - -* [vmlab](./docs/vmlab/):虚拟机和 Linux 基本环境准备。 -* [syscalllab](./docs/syscalllab/):为 Linux 内核添加系统调用。 -* [shelllab](./docs/shelllab/):实现简单的 Shell。 -* [malloclab](./docs/malloclab/):实现简单的内存分配器。 -* [ohlab](./docs/ohlab/):OpenHarmony实现简单的端侧推理。 - -另外,[todo.md](./docs/todo.md) 中记录了一些待完成事项。 - -书写规范: - -* 注意图片引用使用相对路径而不是绝对路径。 - -## 生成和测试 - -本项目使用 MkDocs 构建。使用下面命令可生成静态网站: - -```bash -mkdocs build -``` - -使用下面命令在本地运行网站,进行测试: - -```bash -mkdocs serve -a localhost:8000 # 可将8000改为其它端口 -``` - - - -## 致谢 - -本文档参考了以下项目: - -* [计算机系统结构系列实验文档](https://soc.ustc.edu.cn/) -* [USTC VLab 使用文档](https://vlab.ustc.edu.cn/docs/)([仓库](https://github.com/USTC-vlab/docs)) +# USTC 操作系统原理与设计 实验文档 + +所有实验文档位于 [./docs](./docs/) 目录中,每个实验单独存放独立的子目录,文档中使用的图片存放在相应子目录的 assets 目录里。目录与实验对应如下: + +* [vmlab](./docs/vmlab/):虚拟机和 Linux 基本环境准备。 +* [syscalllab](./docs/syscalllab/):为 Linux 内核添加系统调用。 +* [shelllab](./docs/shelllab/):实现简单的 Shell。 +* [malloclab](./docs/malloclab/):实现简单的内存分配器。 +* [ohlab](./docs/ohlab/):OpenHarmony实现简单的端侧推理。 + +另外,[todo.md](./docs/todo.md) 中记录了一些待完成事项。 + +书写规范: + +* 注意图片引用使用相对路径而不是绝对路径。 + +## 生成和测试 + +本项目使用 MkDocs 构建。使用下面命令可生成静态网站: + +```bash +mkdocs build +``` + +使用下面命令在本地运行网站,进行测试: + +```bash +mkdocs serve -a localhost:8000 # 可将8000改为其它端口 +``` + + + +## 致谢 + +本文档参考了以下项目: + +* [计算机系统结构系列实验文档](https://soc.ustc.edu.cn/) +* [USTC VLab 使用文档](https://vlab.ustc.edu.cn/docs/)([仓库](https://github.com/USTC-vlab/docs)) diff --git a/docs/index.md b/docs/index.md index 27edd9d..9a776d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -# USTC 操作系统原理与设计 实验文档 - -本站展示中国科大操作系统原理与设计课的实验文档。目前只是把已有的文档直接放了上来,没有调整格式错误等。 - -临时搭出来的网站,目前还很不完善,请大家包涵。 - +# USTC 操作系统原理与设计 实验文档 + +本站展示中国科大操作系统原理与设计课的实验文档。目前只是把已有的文档直接放了上来,没有调整格式错误等。 + +临时搭出来的网站,目前还很不完善,请大家包涵。 + 请以课程主页上的信息为准:[课程主页](http://staff.ustc.edu.cn/~ykli/os2025/) \ No newline at end of file diff --git a/docs/ohlab/ohlab-part1.md b/docs/ohlab/ohlab-part1.md index e01d728..e844a8d 100644 --- a/docs/ohlab/ohlab-part1.md +++ b/docs/ohlab/ohlab-part1.md @@ -1,645 +1,645 @@ -# 移动操作系统与端侧AI推理初探-移动操作系统(part1) - -## 实验目的 - -- 了解一个 “实用” 的操作系统还需要什么? -- 了解移动操作系统应用开发流程,了解移动操作系统与桌面/服务器操作系统的区别。 - - 了解交叉编译等跨架构开发中用到的基本概念。 - - 体验实际的移动应用开发。 - -- 了解开源鸿蒙整体框架并尝试使用开源鸿蒙。 - - -## 实验环境 - -- OS: - - 烧录:Windows 10 / 11 - - 编译:Ubuntu 24.04.4 LTS - -- Platform : VMware - -## 实验时间安排 - -> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 -> -> 注: 所有的实验所需要的素材都可以在睿客网盘链接:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 中找到。 -> -> 此次实验只有两周时间,本文档为第一阶段的实验文档。在第一周内,完成本文档的部分任务,可以获得额外的分数,我们由此鼓励大家尽快开始实验,以避免最后时间太短导致的来不及完成/开发板使用冲突。 -> -> **虽然本次实验进行了分组,但每个人仍然要独自完成所有实验内容。分组仅为了共用开发板。** - -- 5.16晚实验课,讲解实验、检查实验 -- 5.23晚实验课,检查实验 -- 5.30晚实验课,补检查实验 - -## 友情提示/为什么要做这个实验? - -- **本实验难度并不高,几乎没有代码上的要求,只是让大家了解完整的移动应用开发流程,并在此过程中,体会移动操作系统与我们之前使用的桌面/服务端操作系统的不同。** -- 如果同学们遇到了问题,请先查询在线文档,也欢迎在文档内/群内/私聊助教提问。在线文档地址:[https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) - - 为了提供足够的信息,方便助教助教更快更好地解答你的疑问,我们推荐你阅读(由LUG撰写的)[提问指南](https://lug.ustc.edu.cn/wiki/doc/howtoask/)。**当然,这并不是必须的,你可以随时提问,助教都会尽可能提供帮助。** - - - -# 实验内容简介 - -> 本节提供对本次实验的概览,让大家能更好地理解本次实验要做什么,目标是什么。实验的具体步骤可以参考本文档后面章节。 - -本次实验中,我们将在提供的 DAYU200 开发板上,运行 OpenHarmony 操作系统,并开发能运行在该开发板上和 OpenHarmony 上的大模型推理应用。为了实现这个目标,需要依次完成以下几个任务: - -1. 将 OpenHarmony 系统安装到开发板上并运行。 - -2. 安装并配置 OpenHarmony 应用的开发环境,成功开发并在开发板上运行一个示例应用。 - -3. 完成大语言模型推理应用的开发,其中包括: - - * 通过交叉编译,将大模型推理框架(Llama.cpp)编译为能够在开发板上使用的动态链接库。 - - * 调用上述库,完成应用,并运行在开发板上。 - -在实验的第一阶段,我们主要完成前两部分。 - - - -# 第一部分 移动操作系统以及润和DAYU200开发板介绍 - -## 1.1 什么是“实用”的操作系统? - -> 以下只是助教自己的理解。和实验好像也不是那么相关,所以大家想跳过也不是不行。O(∩_∩)O - -在操作系统理论课中,我们经常接触各种概念模型、设计原则和算法。但当我们走出课堂,进入实际开发或应用场景时,“实用性”(Practicality)就成为了衡量一个操作系统好坏的关键标准之一。那么,什么构成了一个“实用”的操作系统呢? - -“实用”并非一个严格的操作系统分类术语(如分时系统、实时系统等),而是更侧重于一个操作系统在现实世界中能否有效、可靠、高效地完成其设计目标,并被用户和开发者方便地使用。一个“实用”的操作系统通常具备以下几个关键特征: - -- 易用性 (Usability): - - 用户易用性: 对于有用户交互界面的系统,界面需要直观、易于学习和操作。 - - 开发者易用性: 需要提供清晰的文档、完善的软件开发工具包(SDK)、调试工具和活跃的开发者社区支持。这对于本次实验尤其重要,我们需要使用OpenHarmony的SDK进行开发。 - - Android Studio(Android); Devco Studio(HarmonyOS/OpenHarmony) -- 性能 (Performance) 与效率 (Efficiency): - - 系统需要在其目标硬件上表现出可接受的性能。这意味着响应用户操作要及时,执行任务要高效。 - - 对于移动或嵌入式系统,**资源效率**(特别是功耗和内存占用)至关重要,直接关系到电池续航和成本。低延迟与高能效比(适用于IoT、边缘计算) -- 可维护性 (Maintainability) 与生态系统 (Ecosystem): - - 操作系统需要能够被持续更新和维护,以修复bug、堵塞安全漏洞、适应新的硬件和需求(例如手机不断地更新推送,Windows Update等)。 - - 一个健康的生态系统(包括硬件制造商、软件开发者、用户社区)是操作系统生命力的体现,也是其保持“实用”的关键。 -- 安全性 (Security): - - - 必须提供必要的安全机制,保护系统自身和用户数据免受恶意软件、未授权访问等威胁。安全性的重要程度取决于应用场景 -总结来说,一个“实用”的操作系统,不仅仅是理论概念的堆砌,更是能够在特定的硬件平台上,稳定、高效、安全地运行,满足用户需求,并为开发者提供便利,从而真正在现实世界中创造价值的软件系统。 - -在我们本次实验中,我们将使用的OpenHarmony,其目标就是成为面向多种智能设备的“实用”操作系统。我们将通过具体的开发实践,体验其作为开发平台的“实用性”,例如利用其SDK进行交叉编译,并在真实的DAYU200开发板上运行我们集成的AI推理库。 -## 1.2 移动操作系统简介 - -### 1.2.1 什么是移动操作系统 -移动操作系统是一种专门为便携式、手持设备(如智能手机、平板电脑、智能手表等)设计的操作系统。与大家更为熟悉的桌面操作系统(如Windows、macOS、Linux发行版如Ubuntu)一样,移动操作系统的核心功能也是管理设备的硬件资源(CPU、内存、存储、网络、传感器等)、提供用户交互界面、以及运行应用程序。 - -然而,由于移动设备与桌面计算机在硬件形态、使用场景、性能功耗等方面存在显著差异,移动操作系统在设计理念和功能侧重上与桌面操作系统有着明显的不同。 - -### 1.2.2 目前主流的移动操作系统 - -目前市场上主流的移动操作系统主要有: - -1. Android (Google): 由Google主导开发,基于Linux内核,是目前市场份额最大的移动操作系统。其开放性吸引了众多设备制造商和开发者。 - -2. iOS(Apple): 由Apple公司为其iPhone、iPad等设备开发的闭源操作系统。以其流畅的用户体验、严格的应用生态和安全性著称。 - -3. HarmonyOS (华为) / OpenHarmony (开源鸿蒙开放原子基金会): 由华为开发,旨在面向万物互联时代,可部署于手机、平板、智能穿戴、智慧屏、车机等多种智能终端。OpenHarmony是HarmonyOS的开源版本,也是本次实验我们将使用的操作系统。 - - -### 1.2.3 移动操作系统 vs. 桌面操作系统 -为了更好地理解移动操作系统的特性,我们将其与大家常用的桌面操作系统进行对比: - -| 特性 | 移动操作系统 (如 Android, iOS, OpenHarmony) | 桌面操作系统 (如 Windows, macOS, Ubuntu) | -| :--------------- | :--------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | -| **设计目标** | 优先考虑便携性、低功耗、触控交互、持续连接 | 优先考虑强大的计算能力、多任务处理、外设扩展性、精确输入(键鼠) | -| **硬件平台** | 通常基于ARM等低功耗架构的SoC(片上系统),资源(CPU、内存、存储)相对受限 | 通常基于x86/x64架构,拥有更强的处理器、更大的内存和存储空间 | -| **用户交互** | 以触摸屏为主要输入方式,支持手势操作,界面为单窗口或分屏应用优化 | 以键盘、鼠标为主要输入方式,支持多窗口、复杂的图形用户界面(GUI) | -| **电源管理** | 极其重要,采用积极的休眠策略、后台任务限制等机制以延长电池续航 | 电源管理相对宽松,虽然也在不断优化,但通常连接电源使用,对续航要求不如移动设备苛刻 | -| **应用生态** | 通常依赖官方或第三方应用商店分发应用,应用运行在沙盒(Sandbox)环境中,权限管理严格 | 软件来源多样(安装包、商店、源码编译等),沙盒机制相对不普遍,权限管理模型不同 | -| **连接性** | 高度依赖无线网络(蜂窝数据、Wi-Fi、蓝牙),内置多种传感器(GPS、加速度计等) | 对有线网络(以太网)支持普遍,无线网络也很常见,但对传感器依赖较少 | -| **系统更新** | 更新通常由设备制造商或运营商推送,有时碎片化问题较严重(Android) | 更新通常由操作系统供应商直接提供,用户可控性相对较高 | -| **开发范式** | 常使用特定的SDK(如Android SDK, iOS SDK, OpenHarmony SDK),注重UI框架和生命周期管理 | 开发工具和语言选择更广泛,系统API调用方式和应用模型不同 | - -在本次实验中,我们将会体会到其中部分差异:硬件平台(交叉编译),应用生态(沙盒),开发范式(SDK) - -### 1.3 OpenHarmony -在了解了移动操作系统的一般概念和特性后,现在我们将焦点转向本次实验的主角——OpenHarmony -#### 1.3.1 什么是 OpenHarmony? - -OpenHarmony(中文常称为“开源鸿蒙”)是一个由开放原子开源基金会(OpenAtom Foundation)孵化和运营的开源项目。它并非仅仅是传统意义上的手机或平板操作系统,而是一个面向全场景、可分布式部署的智能终端操作系统。简单来说,它的目标是成为驱动未来各种智能设备(从小型物联网设备到功能丰富的智能手机、平板、智慧屏等)的统一基础平台。 - -#### 1.3.2 OpenHarmony核心理念与愿景 - -OpenHarmony 旨在打破单一设备的应用边界,其核心设计理念之一是分布式技术。这意味着: - -1. 一次开发,多端部署: 开发者编写的应用,理论上可以通过适配层部署到多种不同形态、不同屏幕尺寸的 OpenHarmony 设备上。 -2. 硬件互助,资源共享: 不同设备可以组成“超级终端”,互相调用对方的硬件能力(例如,用手机的键盘输入文字到智慧屏,或用平板控制无人机的摄像头)。 -3. 无缝流转,协同交互: 应用和数据可以在不同设备间平滑迁移和协同工作,提供一体化的用户体验。 - -> 虽然OpenHarmony的愿景宏伟,但本次实验中,我们仅会体验其在DAYU200开发板上的单设备部署,不会涉及到分布式部署的特性。(因为工作量可能会很大/(ㄒoㄒ)/~~,并且开发板数量较少) - -#### 1.3.3 OpenHarmony关键特性 - -OpenHarmony技术架构如下所示: - -image-20240416151431461 - - -- 分层架构: OpenHarmony 采用了清晰的分层架构,主要包括内核层、系统服务层、框架层和应用层。 - - - 内核层 (Kernel Subsystem): 关键在于其可按需选择内核。对于资源受限的轻量级设备(如内存为 KiB 或 MiB 级别),可选用 LiteOS 内核;对于资源较丰富的标准系统设备(如本次实验使用的 DAYU200 开发板),则可选用 Linux 内核。理解这一点对于后续的交叉编译环境配置非常重要。 - - 系统服务层 (System Service Layer): 提供一系列核心系统能力和通用的基础服务,如分布式能力、图形、多媒体、安全等。 - - 框架层 (Framework Layer): 为应用开发提供必要的 API 和框架,包括应用框架、UI 框架(如 ArkUI)等。 - - 应用层 (Application Layer): 包含系统应用和第三方应用。 -- 组件化设计: 系统可以根据硬件的具体能力进行灵活的组件化裁剪和按需加载,使其能够适配各种内存和性能规格的设备。 -- 开放源代码: 作为一个开源项目,其源代码对全球开发者开放,便于学习、定制和共同发展生态。 - -### 1.3.4 OpenHarmony 与 HarmonyOS 的关系: - -OpenHarmony 是 HarmonyOS(华为鸿蒙操作系统)的开源基础版本。华为将 HarmonyOS 的基础能力贡献给了开放原子开源基金会,形成了 OpenHarmony 项目。其他厂商或开发者可以基于 OpenHarmony 构建自己的操作系统发行版,HarmonyOS 就是基于 OpenHarmony 的一个面向消费者的商业发行版。 - -> 类似于Android与AOSP的关系 - -### 1.3.5 为什么在本次实验中使用 OpenHarmony? - -选择 OpenHarmony 作为本次实验平台,主要基于以下考虑: - -- 代表性: 它代表了现代操作系统(特别是面向物联网和多设备协同)的一个发展方向。 -- 实践平台: 为我们提供了一个真实的、可操作的移动操作系统环境(运行在 DAYU200 开发板上)。 -- 开发体验: 允许我们实践移动平台的开发流程,特别是本次实验重点关注的原生 C++ 代码(Native C++)的交叉编译、库集成与调用。 -- 可及性: 相关的 SDK、开发工具(DevEco Studio)和文档资源相对完善,便于学生学习和使用。 - -在接下来的实验环节中,我们将亲自动手,完成 OpenHarmony 系统镜像的烧录、开发环境的搭建,并最终在其上运行我们自己编译的 C++ AI 推理库,从而深入理解在移动操作系统上进行原生开发和集成的过程。 - -> 其实是为了统一实验平台,单独设计Android实验可能部分同学无法做这个实验。 - -## 1.3 DAYU200开发板 -前面我们介绍了作为软件基础的 OpenHarmony 操作系统,现在我们来认识一下承载这个系统并供我们进行实际操作的硬件平台——DAYU200 开发板。 - -> **为什么实验要使用开发板?**如同实验目的中提到的,本实验希望大家体会移动操作系统与我们平时使用的桌面操作系统的区别。而操作系统的区别很大程度也是由硬件的区别带来的。可以把开发板当成是**一台拓展性比较强的手机**(或者平板,虽然性能比较差),方便大家体验移动开发流程。 - -### 1.3.1 DAYU200开发板介绍 - -> 虽然本节详细介绍了了开发板的各类硬件规格,但本次实验中你需要知道的其实只有: -> -> * 开发板的处理器是RK3568芯片,使用的指令集是**32位的ARM**。(具体而言,是`armeabi-v7a`。) -> * 大家应该在《组成原理》课上,应该已经知道了不同指令集的区别。ARM是现在移动设备上最流行的指令集。大家使用的手机、智能手表,以及新款 MacBook,使用的都是ARM架构。 -> * 开发板的内存(RAM)大小是2GB,存储容量是32GB。 - -为了能够流畅运行功能相对完整的 OpenHarmony 标准系统,并支持复杂的应用开发与调试,DAYU200 配备了较为强大的硬件资源。其核心规格通常包括: -- 处理器 (SoC): 核心是瑞芯微 (Rockchip) RK3568 芯片。这是一款高性能、低功耗的应用处理器,集成了: - - CPU: 四核 32 位 ARM Cortex-A55,主频最高可达 2.0GHz。注意这里的 32位 ARM 架构 (arm32 / armeabi),这决定了我们后续交叉编译的目标平台。 - - GPU: Mali-G52 2EE 图形处理器,支持 OpenGLES 3.2, Vulkan 1.1。 - - NPU (可选): 部分版本集成神经网络处理单元,可提供约 1 TOPS 的 AI 算力,用于硬件加速人工智能应用(本次实验主要使用 CPU 进行推理,但了解 NPU 的存在有助于理解硬件加速潜力)。 -- 内存 (RAM): 开发板内存为2GB的 LPDDR4/LPDDR4X 内存。充足的内存对于运行标准系统和我们的 AI 推理任务至关重要。(2GB对于推理较大模型就有点不够,附录中我们会教大家如何创建交换分区) -- 存储 (Storage): 板载 eMMC 闪存作为主要的系统和数据存储介质,开发板容量为 32GB。 - -同时,开发板还提供了多种接口,可以连接各种外设。(虽然我们实验没有用到,但感兴趣的同学可以自行了解。) - -> 更多信息可以查看链接 [润和HH-SCDAYU200开发套件](https://gitee.com/hihope_iot/docs/tree/master/HiHope_DAYU200#https://gitee.com/hihope_iot/docs/blob/master/HiHope_DAYU200/docs/README.md) - -### 1.3.2 为什么选择使用DAYU200开发板 - -- 官方与社区支持: DAYU200 是 OpenHarmony 官方和社区重点支持的开发板之一,有持续的软件版本适配和丰富的文档、教程资源。这意味着我们可以更容易地获取到可运行的 OpenHarmony 标准系统镜像和解决遇到的问题。 -- 性能适中: 其硬件配置足以流畅运行 OpenHarmony 标准系统,并能够承载我们本次实验中编译 C++ 代码、运行中小型语言模型(如 Llama.cpp 在 CPU 上推理)的需求。 -> 主要是HUAWEI推荐的╮(╯▽╰)╭ - -### 1.3.2 开发板的使用 - -在发放的开发板中,有以下物品: - -1. DAYU200开发板 -2. DAYU200电源适配线 -3. 公对公USB数据线(用于烧写) -4. mini USB B数据线(用于串口调试,本实验可以忽略) - Cable - -当开发板开机后,部分开发板运行的是Openharmony4.0版本的mini system(去年同学们遗留下来的),其没有图形化界面,会直接卡在LOGO,这是正常现象;后面实验我们将教大家如何烧录Openharmony5.0版本的全量系统。 - -当接上电源后,开发板一般会自行启动,如果没有启动请查看开发板上的按钮,根据按钮的功能进行尝试打开。 - -如下图红框,开发板上有六个按钮,按钮下有对应的名称。第一排的三个按钮(RESET、SELECT、MUTE),本次实验不涉及。第二排的三个按钮类似普通手机侧面的三个按钮,分别是: - -* Power:电源键,负责开关机、锁屏、亮屏。 -* VOL+/RECOVERY:音量+/恢复模式键,平时负责增加音量,也可引导手机进入恢复模式。(后面文档会说明。) -* VOL-:音量-键。 - -​ Cable - -### 1.3.3 开发板使用规范 - -开发板的使用采用分组负责人制度。即三人一组,由一人担任负责人并且保管开发板,发放和回收开发板向负责人进行。 - -
- -# 第二部分 将开源鸿蒙系统安装到开发板上 - -在这一部分,我们将介绍 OpenHarmony 镜像的烧录。为了方便大家体验开发版,助教给大家准备了已经编译好的完整版 OpenHarmony 镜像,可以直接烧录到开发板上。 - -> 本实验**不要求**大家编译 OpenHarmony 操作系统,是因为完整版的操作系统编译资源开销较大,且需要花费较长时间。(我们在实验一编译了最小版本的 Linux 内核,就需要花费15~30分钟,而 OH 系统在此基础上增加了一系列组件(如GUI和应用支持),完整编译需要上百GB存储空间和数小时的时间。) -> -> 如果大家对编译 OpenHarmony 系统的过程感兴趣,可以参考 OpenHarmony 社区文档: -> -> https://gitee.com/openharmony/docs/blob/master/zh-cn/device-dev/subsystems/subsys-build-all.md -> -> (当然,想实际编译还会遇到无数的坑,助教们去年就踩了很久。^_^) - -## 2.1 OpenHarmony 烧录 - -在实验一中,我们曾经为虚拟机安装了操作系统。当时,安装系统的大致流程为: - -1. 将镜像连接到虚拟机(相当于将系统光盘插到电脑里); -2. 从光盘启动系统,并将系统按设置安装(写入)到虚拟机里。 - -在大部分个人电脑/服务器上,系统安装流程也是类似的。 - -然而,对于移动设备而言,以上安装方式是**不可行**的。一方面,移动设备的硬件结构多种多样,难以将系统封装为一个在任何设备上都能启动的镜像。另一方面,移动设备厂商通常也不希望用户能任意更换操作系统,通常也不允许从外部存储设备启动。所以,移动设备更换系统的流程通常更为复杂,且和具体设备相关。 - -对于我们的开发板,安装系统的方式为,通过开发板制造商提供的软件,直接将系统数据**硬件级传输**到开发板存储设备的对应位置。这种模式在嵌入式开发中很常见,我们称为**烧录(Burning)**。在本节中,我们将介绍 RK3568 开发板的烧录流程。 - -> [!IMPORTANT] -> -> RK3568 开发板的烧录软件目前**只支持 Windows 系统**,请在 Windows 系统下进行本章节的步骤。 - -### 2.1.1 软件环境准备 - -#### 2.1.1.1 下载及解压 - -在烧录前,需要大家下载以下内容,并解压到你喜欢的文件夹。(本文档以`D:/OHLab/`为例。) - -* **DAYU200 烧录工具** - * 下载地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 - * 选择`DAYU200烧写工具及指南.zip` - * 备用下载链接:[DAYU200 烧录工具 - GitLab](https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/DAYU200%E7%83%A7%E5%86%99%E5%B7%A5%E5%85%B7%E5%8F%8A%E6%8C%87%E5%8D%97.zip) - -* **Openharmony5.0全量系统镜像** - * 下载地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 - * 选择`OpenHarmony5.0-image.zip` - * 备用下载链接:https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/OpenHarmony5.0-image.zip - -#### 2.1.1.2 安装USB驱动 - -在解压后的`烧写工具及指南`文件夹里,进入`windows/`目录,解压其中的`DriverAssitant_v5.1.1.zip`文件至单独文件夹, - -然后,双击击解压后的`DriverInstall.exe`打开安装程序,点击下图所示的“驱动安装”按钮: - -usb驱动1usb驱动2 - -显示“安装驱动成功”即可,然后可关闭该程序。 - -> 驱动程序,简称驱动,是规定电脑软件与特定硬件如何交互的程序。现在,主流操作系统安装时通常会自带大量常用硬件的驱动,因此我们不需要手动安装。但当需要与操作系统中没有的硬件交互时,就需要我们手动安装驱动了。 - -### 2.1.2 开发板的连接 - -1. 连接开发板 - -* 按照下图提示连接**电源线**和**USB烧写线**。注意,开发板上有三个 USB插口,请连接到中间那个。(接口旁标注了`USB 3.0 OTG`。) - - * 烧写线另一端连接你的计算机。电源线插入电源插座。(这就不用说了吧! (∩_∩)) - - -> 不是说是手机吗,为什么还需要连接电源?大概是为了简化开发板设计吧…… - -连接示意图 - -### 2.1.3烧录步骤 - -#### 2.1.3.1. 打开烧写工具 - -* 如果你的开发板没有打开,请按开发板上的Power键打开开发板。(显示DAYU图标,或进入系统即可。) - -* 双击烧录工具(`烧写工具及指南`文件夹)中 `windows\RKDevTool.exe` 打开烧写工具。 - -* 如图所示,如果你的连接正确,且开发板电源开启,则开发工具下方状态栏会显示:发现一个MASKROM设备。 - - * 如果开发工具状态栏显示“没有发现设备”,则说明连接没有成功。请检查电源线、烧写线是否正确连接。重新拔插烧写线,并尝试长按开发板Power键强制关机,然后再次按Power键开机,等待15秒左右。并检查是否成功。 - - - image-20240416110119355 - -* 导入编译文件镜像包中的 `config.cfg`配置,该配置文件在刚刚解压的系统镜像目录里,路径为 `OpenHarmony\config.cfg`,导入的方法为:(如下面三张图所示) - -1. 在烧写工具左侧空白处右键,单击导入配置。 - -image-20240416110550450 - -2. 选择`OpenHarmony\config.cfg`文件(注意,开发工具里本身也有一个`config.cfg`,但我们要选择的不是这个,**而是系统镜像解压后目录里的`config.cfg`**。 - -image-20240416110804770 - -3. 点击空白栏,逐一配置每个镜像文件对应的路径(也可以双击路径,手动修改路径)。 - * 如,你的镜像解压目录为`D:\OHLab\OpenHarmony`,则第一行应该选择`D:\OHLab\OpenHarmony\MiniLoaderAll.bin`文件,其余各行均应选择相应的img文件。 - -> **这一步在干什么?**实际上,这一步就是在设置,我们要将哪些数据写入到开发板的哪些位置。例如,第一行代表了我们会将`MiniLoaderAll.bin`写入到开发板地址`0x00000000`处(这虽然看起来是个内存地址,但有可能对应了开发板上某个存储器的空间。开发板启动时,会按厂商设计,从某些对应地址读取启动所需要的数据和代码,因此,正确将系统写入到对应位置,就能成功安装系统。(当然,具体写入规则和镜像生成方式,就要参考厂商的文档啦。在本实验中,配置已经给大家写好啦!) - -image-20240416111036833 - -image-20240416111404005 - -image-20240416111714578 - -#### 2.1.3.2. 进入LOADER烧写模式 - -* 默认烧写工具是 `MASKROM` 模式,烧写工具上状态栏会显示“发现一个MASKROM设备”,我们需要将设备进入`Loader`模式。 - -* 进入 `LOADER` 烧写模式 - - 1. 按住 `VOL+/RECOVERY` 按键(图中标注的①号键) 和 `RESET` 按钮(图中标注的②号键)不松开, 此时,烧录工具会显示“没有发现设备” - - 2. 松开 `RESET` 键(②号键), 烧录工具显示“发现一个 `LOADER` 设备” , 说明此时已经进入烧写模式,可以松开`VOL+/RECOVERY`键。 - - loadernon-device - - - - image-20240416113233886 - -* 进行烧录 - - 当烧录工具显示“发现一个LOADER设备”后,可以点击“执行”按钮,进行烧录。烧录进度会显示在右侧。烧录大概需要几分钟,如果烧写成功, 最后在工具界面右侧会显示“下载完成”。此时,烧写即完成,可以断开烧写线了。(连着也无妨。) - - 烧写成功后,开发板会自动启动,并在约30s后,成功打开系统,系统和普通的手机系统类似,但只有很少的几个应用。另外,由于开发板性能较弱,系统可能稍微有些卡顿,这也是正常现象。大家可以自行体验。 - - image-20240416113523492 - -> **什么是MASKROM?什么是LOADER?**可以认为是开发板厂商设计的开发板的不同状态。MASKROM类似一种只读模式,LOADER则是烧录模式。参考:[瑞芯微系列:系统烧录和登录系统 - 知乎](https://zhuanlan.zhihu.com/p/634585861) - - -# 第三部分 安装应用开发环境并开发简单应用 - -在前两部分,我们了解了移动操作系统、OpenHarmony 以及实验使用的 DAYU200 开发板,并完成了基础的系统烧录。接下来,我们将安装官网为 OpenHarmony 和 HarmonyOS 提供的应用开发环境 DevEco Studio,并实际体验一个简单的 OH 应用开发流程。这部分包含两个主要步骤: - -* 在 Windows 上安装集成开发环境 DevEco Studio。 -* 将配置并编译官方的示例应用,并将示例应用运行在开发板上。 - -## 3.1 在 Windows 上安装 DevEco Studio -DevEco Studio 是 HUAWE 推出的官方集成开发环境(IDE),用于 HarmonyOS 和 OpenHarmony 应用开发。它集成了代码编辑、编译、调试、应用签名、HAP 打包、模拟器/预览器以及 SDK 管理等功能。 - -> HAP 是 OpenHarmony 和 HarmonyOS 的应用格式,类似 Android 的 APK。 - -### 3.1.1 下载安装DevEco Studio -* 下载 DevEco Studio - * 下载地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 - * 选择`devecostudio-windows-5.0.11.100.zip` - * 你也可以在官方下载地址下载最新版:https://developer.huawei.com/consumer/cn/deveco-studio/ - * 官方下载地址需要登录华为账号 - -* 安装DevEco Studio,安装过程较为简单,选择默认配置即可。 - -> 注意:DevEco Studio安装包体积较大,请确保网络连接稳定。 -> -> 安装需要15GB左右空间,请选择合适的安装位置,保证空间足够。 -> -> DevEco Studio 中文设置:https://developer.huawei.com/consumer/cn/forum/topic/0204171044047287810 -* 安装完毕后,第一次打开 DevEco 时,提示 Import DevEco Studio Settings,此时选择Do not import settings即可。后续需要同意开发协议,单击同意即可。 - -## 3.2 在 DevEco Studio 上开发应用 - -本节我们将尝试使用 DevEco 创建第一个项目,并尝试将其运行在开发板上。 - -> OpenHarmony 和 HarmonyOS 的应用通常使用 [ArkTS语言](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/arkts-utils/arkts-overview.md) 编写,OpenHarmony 和 HarmonyOS 提供的系统 API 也通常都是 ArkTS接口的(正如我们开发 Android 时离不开 Java 语言一样)。ArkTS 是基于 TypeScript 拓展的新语言(而 TypeScript 则是基于 JavaScript 拓展的强类型语言,套娃了)。 -> -> 当然,为了减轻大家工作量,**本实验不涉及 ArkTS 具体语法内容**,感兴趣的同学可自行了解。为了尽可能使用大家熟悉的语言,我们的应用会使用 OpenHarmony 提供的 Native C++ 开发模式,该模式允许 ArkTS 和 C++ 互相交互,在 OpenHarmony 上运行 C++ 代码。 - -### 3.2.1 DevEco Studio的使用:项目创建 - -1. 通过如下两种方式,打开工程创建向导界面。 - - 如果当前未打开任何工程,可以在DevEco Studio的欢迎页,选择Create Project开始创建一个新NDK工程。 - - - 如果已经打开过工程,可以在菜单栏选择File > New > Create Project来创建一个新NDK工程。 - - > NDK: Native C++ Develop Kit,本地C++开发组件。总之就是为了让开发板能跑C++的神奇妙妙工具,下一阶段实验会进一步解释这是怎么回事。 -2. 根据工程创建向导,选择Native C++工程模板,然后单击Next。(如下图。) - - ![创建项目](./assets/3.1.2/1.png) - -3. 在`Configure Your Project`页面可以修改`Project Name`、项目目录和其它选项,然后单击Finish。 - - * Project Name 就是项目名,推荐使用**自己的学号**(方便检查)。 - * Save Location 是项目存储位置,大家可选择存放在自己喜欢的目录下。 - * 其它选项默认即可。 - -创建项目后,会进入项目开发界面,如下图,和其它IDE类似,开发界面主要分为三个部分: - -* 项目目录(图中①),显示项目中主要文件。 -* 编辑区(图中②),用来写代码。 -* 终端区域(图中③),显示各类信息,或者终端等。 - -![DevEco](./assets/DevEco界面.png) - - - -### 3.2.2 DevEco Studio的使用:项目预览 - -DevEco Studio内置了预览器,能预览我们开发的应用,以方便开发者开发,不需要每次修改代码后都实际将应用传输到设备中运行。由于 DevEco Studio 创建的项目本身就是完整可运行的,我们可以直接启动预览,来体验该应用的效果。 - -为了启动预览,需要: - -1. 点击开发界面右上角,“设备”下拉框(绿色三角运行按钮的左侧的下拉框),选择“Previewer”,如下图。 - -image-20250511204310530 - -2. 点击运行按钮(绿色三角形),选择`2in1`,点击OK。(这一步在选择设备类型,`phonoe`是手机,`tablet`是平板,`2in1`是两种皆可的,根据选择的不同,显示屏幕的比例会有区别。) - -由于编译和渲染预览所需时间较慢,这一步后需要等待约30~60秒。之后,预览会显示在开发界面右侧,如下图: - -![image-20250511205246282](./assets/DevEco-preview-hello.png) - -预览界面类似一个平板屏幕,上面有一行`Hello World`。你可以单击这行字,稍等片刻,内容会变为`Welcome`,如下图: - -image-20250511205539721 - -这就是我们创建的示例应用的功能。如果文字成功改变,你可以单击右上角停止按钮,结束预览。(红色正方形,在运行按钮右侧。) - -> 虽然我们不打算讲解 ArkTS 语法,但我们还是愿意稍微解释一下这个点击变换的效果是怎么实现的,以便感兴趣的同学了解。 -> -> * 应用的主体逻辑在已经默认打开的`Index.ets`文件中。该文件主要部分是`Row`、`Column`、`Text`几个函数的嵌套,这几个函数定义了应用的界面(一行一列,中间有一段文本,文本内容是`this.message`,也就是`'Hello World'`。) -> * 在`Text(this.message)`下,`.onClick`中定义了,这一行Text被点击后,将会执行的代码。 -> * `this.message = 'Welcome';`一行,将`this.message`赋值为`Welcome`,所以,我们看到的文字改变了。 -> * 供感兴趣的同学思考:目前这个改变是单向的,如果我希望将应用效果改为每单击一次,就在`Welcome`和`Hello World`间变换一次,应该如何实现? -> * 提示:ArkTS当然也有`if`语句,语法和C语言一样。 - - - -### 3.2.3 DevEco Studio的使用:将应用运行到开发板上 - -为了将应用运行在开发板上,我们需要按和[2.1.2 开发板的连接](#2.1.2 开发板的连接)一节中相同方式,将电脑与开发板进行连接。 - -正确连接后,再次点击右上角设备下拉框,可以看到设备列表中出现`OpenHarmony 3.2[....]`这样的选项(如下图)。该选项则为开发板所对应的设备。 - -image-20250511211250931 - -但选择该设备并直接运行时,IDE会提示错误如下: - -``` -compatibleSdkVersion and releaseType of the app do not match the apiVersion and releaseType on the device. -``` - -这是因为 DevEco Studio 默认创建的是运行于 HarmonyOS 的项目,而我们的设备运行的是 OpenHarmony。因此,我们需要进行相应设置,将项目编译为 OpenHarmony 应用。 - -#### 3.2.3.1 安装 OpenHarmony SDK - -为了编译 OpenHarmony 项目,我们需要首先安装OpenHarmony SDK。步骤如下: - -1. 点击界面左上角`File > Settings`打开设置。 -2. 选择右侧 OpenHarmony SDK,在 Location 一栏中,选择希望的 OpenHarmony SDK 安装位置。 - * OpenHarmony SDK大小较大(约5GB),请选择合适的安装位置。 -3. 勾选API Version 14中所有的选项,点击`Apply`,等待自动下载完毕并解压到相应目录。 - -> SDK 是 Software Development Kit,软件开发组件,和前面的NDK一样,你可以理解为,用来编译OpenHarmony软件的妙妙小工具。 - -![安装OpenHarmony SDK](./assets/3.1.2/2.png) - -#### 3.2.3.2 修改项目配置文件 - -1. 修改`build-profile.josn5` - - **安装完毕SDK后**,在界面左侧项目目录中,打开**项目根目录下的**`build-profile.json5`,文件开头内容如下所示: - - * 注意,`entry`目录下也有一个`build-profile.json5`,但两者内容不一样(我们接下来也要修改),不要搞混了。 - -```json -{ - "app": { - "signingConfigs": [], - "products": [ - { - "name": "default", - "signingConfig": "default", - "targetSdkVersion": "5.0.5(17)", - "compatibleSdkVersion": "5.0.5(17)", - "runtimeOS": "HarmonyOS", - "buildOption": { -...... -``` -我们需要将其中第8、9、10三行内容,改为以下4行(注意,数字周围没有双引号。): - -```json - "compatibleSdkVersion": 14, - "compileSdkVersion": 14, - "targetSdkVersion": 14, - "runtimeOS": "OpenHarmony" -``` - -修改后的文件如图,这几行分别定义了我们使用的设备操作系统是`OpenHarmony`,用来编译、生成的目标和兼容的SDK版本都是 14。 - -![修改配置文件](./assets/3.1.2/4.png) - - - -> 还记得我们上一步安装的SDK版本吗?也是14。这个版本必须和设备系统上的版本一致,你可以认为,这是移动操作系统的“系统调用”的版本。(实际上是API版本,但某种意义上说,移动应用中,系统API的地位和其它系统中系统调用差不多,因为为了安全和隐私性考虑,移动应用没法直接调用大部分系统调用。) - -2. 修改`entry/build-profile.json5` - -接下来,在左侧界面项目目录里,打开`entry/build-profile.json5`,在第7行`cppFlags`后进行如下修改: - -修改前: - -```json - "externalNativeOptions": { - "path": "./src/main/cpp/CMakeLists.txt", - "arguments": "", - "cppFlags": "", - } -``` - -修改后: - -```json - "externalNativeOptions": { - "path": "./src/main/cpp/CMakeLists.txt", - "arguments": "", - "cppFlags": "", - "abiFilters": ["armeabi-v7a","arm64-v8a"] - } -``` - -即,添加一行`"abiFilters": ["armeabi-v7a","arm64-v8a"]`。该行定义了我们运行的处理器架构是`armeabi-v7a`和`arm64-v8a`。 - -> 介绍开发板时,我们提到过,开发板处理器指令集是`armeabi-v7a`,那为什么还要加`arm64-v8a`?因为经过测试,不加这个好像应用跑不起来,我们也不知道原因。╮(╯_╰)╭ - -3. 同步项目设置 - -修改完两个文件后,DevEco应该会出现如下图提示,说明项目没有同步。请点击Sync Now进行同步。 - -![sync](./assets/3.1.2/6.png) - -如果没有出现该提示,你也可以点击界面右上角`File`,选择`Sync and Refresh Project`手动同步。 - -同步过程中可能出现如下`Sync Check`提示,这是因为OpenHarmony应用只支持`default`一种设备类型(而不是前面我们选择过的`phone`、`tablet`、`2in1`几种)。这里点击Yes即可。 - -image-20250511214915860 - -等待一段时间,如果没有错误提示,下方显示`Process finished with exit code 0`,则说明项目配置同步成功。 - -> 同步是为了将正确的项目配置告诉 DevEco,让其能够按照配置正确编译我们的应用。 - -#### 3.2.3.3 在开发板上运行应用 - -在配置完上述所有内容后,我们终于可以实际在开发板上运行我们的应用了。和本节开始所述一样,**连接好开发板后**,在右上角下拉框中选择`OpenHarmony`开头的设备如下: - -image-20250511211250931 - -然后,将项目调整为Debug模式,点击上述下拉框左侧的`entry`下拉框,选择开头图标是`H`的选项,如下图: - -image-20250511221121326 - -> 这一步中,上方的entry选项代表`Release`即正式版应用,下方为`Debug`即调试版应用。调试版应用不需要正式签名,只能在开发过程中使用,正式版应用经过编译优化性能更好,也可以发布给他人,但需要登录华为账号后签名才能使用。参见[下一节](#3.2.3.4 对软件进行签名(可选))的内容。 - -然后,点击运行按钮(右侧绿色三角形),运行项目。应用会被自动安装到开发板上并运行。(运行中可能提示要求生成`debug signature`,如下,点击OK即可。) - -image-20250511221354600 - -在多次运行应用时,软件可能由于版本不同,导致签名不匹配或者安装出错等,出现类似下方的提示。你可以点击`uninstall and reinstall the modules`来强制卸载开发板上已有的应用版本: - -![image-20250511221702724](./assets/image-20250511221702724.png) - -运行成功时,开发板会自动点亮,并自动运行程序,如下。(点击`Hello World`字体,也能成功变为`Welcome`) - -image-20250511221953571 - -#### 3.2.3.4 对软件进行签名(可选) - -> 该部分是可选的,也不包含任何分数。移动设备应用通常需要开发者进行“签名”,否则不允许发布和运行,以保证应用安全。这里的“签名”是密码学术语,同学们以后的课程中会学习到。 -> -> 签名是为了发布自己的应用,或者使用正式版应用。介绍本步骤是为了让大家了解完整的应用开发流程。没有该需求的同学可以跳过这一步。本步骤不占分数。 - -1. 点击界面左上角`File > Project Structure` ,打开项目结构界面。 - -2. 选择左侧`Project`,然后选择上方` Signing Configs`选项卡。 - -3. 在界面中勾选`Support HarmonyOS` ,此时,下方出现提示并提示`Failed to auto generate signing, please sign in first.`这是因为,如果要发布正式版的应用,需要登录华为账号,通过华为账号生成签名。如果你希望发布你的应用,或者使用Release版本的应用,请点击`Sign in`,在网页中登录。登录、签名成功后,界面类似下图。 - - 登录账号 - -
- -# 第四部分:第一阶段实验内容与检查标准 - -## 4.1 实验内容 - -> 如果你一致跟随实验文档的内容完成到这里,那么前三点你已经完成了,你需要做的只有第4点。 - -1. 将提供的 Openharmony5.0 全量标准系统烧录到开发板中,**体验完整版 OpenHarmony 系统**。(参考[2.1 OpenHarmony烧录](#2.1 OpenHarmony 烧录)。) -2. 安装DevEco Studio,创建第一个项目,并在Previewer中运行。(参考[3.1](#3.1 在 Windows 上安装 DevEco Studio)到[3.2.2](3.2.2 DevEco Studio的使用:项目预览)节。) -3. 并尝试创建名为自己学号的空白Demo(例如PB23011000)在开发板运行。(参考[3.2.3 将应用运行到开发板上](#3.2.3 DevEco Studio的使用:将应用运行到开发板上)) -4. 将单击`Hello World`变成`Welcome`改为单击后**变成你的学号**,然后将该应用重新运行到开发板上。 - * 提示:你需要修改`entry/src/main/ets/Index.ets`。修改很简单,你不需要知道ArkTS的语法。 - * 该文件的大体逻辑参考[3.2.2 项目预览](#3.2.2 DevEco Studio的使用:项目预览)最后的介绍。**其实不参考也很简单啦**。 - * 修改完成后,你需要重新参照[3.2.3.3 在开发板上运行应用](#3.2.3.3 在开发板上运行应用) 中的步骤,把应用再次跑到开发板上。 - - -## 4.2 实验评分标准 - -本次实验共 10 分,第一阶段满分为 4 分(如按时完成检查点,有额外的2分),实验检查要求和评分标准如下: - -1. 成功烧录Openharmony5.0 全量标准系统烧录到开发板。(1分) -2. 完成 DevEco Studio 的安装,并在成功在Previewer中运行项目。(1分) - * **检查点:**在2025年5月16日前完成前两个任务。可额外获得2分(不与阶段2叠加)。 -3. 成功将示例应用在开发板上运行(开发板上实现单击后`Hello World`变为`Welcome`)。(1分) -4. 成功将修改后的应用在开发板上运行(开发板上实现单机后`Hello World`变为**你的学号**)。(1分) - -> 在5月16日前完成前两个任务,额外获得2分,但不与阶段二分数叠加。 -> -> 即,如果你在本周完成阶段一所有任务,你将获得6分。但你仍需要完成完成阶段二所有任务,才能拿到本次实验的满分10分。 - - - +# 移动操作系统与端侧AI推理初探-移动操作系统(part1) + +## 实验目的 + +- 了解一个 “实用” 的操作系统还需要什么? +- 了解移动操作系统应用开发流程,了解移动操作系统与桌面/服务器操作系统的区别。 + - 了解交叉编译等跨架构开发中用到的基本概念。 + - 体验实际的移动应用开发。 + +- 了解开源鸿蒙整体框架并尝试使用开源鸿蒙。 + + +## 实验环境 + +- OS: + - 烧录:Windows 10 / 11 + - 编译:Ubuntu 24.04.4 LTS + +- Platform : VMware + +## 实验时间安排 + +> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 +> +> 注: 所有的实验所需要的素材都可以在睿客网盘链接:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 中找到。 +> +> 此次实验只有两周时间,本文档为第一阶段的实验文档。在第一周内,完成本文档的部分任务,可以获得额外的分数,我们由此鼓励大家尽快开始实验,以避免最后时间太短导致的来不及完成/开发板使用冲突。 +> +> **虽然本次实验进行了分组,但每个人仍然要独自完成所有实验内容。分组仅为了共用开发板。** + +- 5.16晚实验课,讲解实验、检查实验 +- 5.23晚实验课,检查实验 +- 5.30晚实验课,补检查实验 + +## 友情提示/为什么要做这个实验? + +- **本实验难度并不高,几乎没有代码上的要求,只是让大家了解完整的移动应用开发流程,并在此过程中,体会移动操作系统与我们之前使用的桌面/服务端操作系统的不同。** +- 如果同学们遇到了问题,请先查询在线文档,也欢迎在文档内/群内/私聊助教提问。在线文档地址:[https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) + - 为了提供足够的信息,方便助教助教更快更好地解答你的疑问,我们推荐你阅读(由LUG撰写的)[提问指南](https://lug.ustc.edu.cn/wiki/doc/howtoask/)。**当然,这并不是必须的,你可以随时提问,助教都会尽可能提供帮助。** + + + +# 实验内容简介 + +> 本节提供对本次实验的概览,让大家能更好地理解本次实验要做什么,目标是什么。实验的具体步骤可以参考本文档后面章节。 + +本次实验中,我们将在提供的 DAYU200 开发板上,运行 OpenHarmony 操作系统,并开发能运行在该开发板上和 OpenHarmony 上的大模型推理应用。为了实现这个目标,需要依次完成以下几个任务: + +1. 将 OpenHarmony 系统安装到开发板上并运行。 + +2. 安装并配置 OpenHarmony 应用的开发环境,成功开发并在开发板上运行一个示例应用。 + +3. 完成大语言模型推理应用的开发,其中包括: + + * 通过交叉编译,将大模型推理框架(Llama.cpp)编译为能够在开发板上使用的动态链接库。 + + * 调用上述库,完成应用,并运行在开发板上。 + +在实验的第一阶段,我们主要完成前两部分。 + + + +# 第一部分 移动操作系统以及润和DAYU200开发板介绍 + +## 1.1 什么是“实用”的操作系统? + +> 以下只是助教自己的理解。和实验好像也不是那么相关,所以大家想跳过也不是不行。O(∩_∩)O + +在操作系统理论课中,我们经常接触各种概念模型、设计原则和算法。但当我们走出课堂,进入实际开发或应用场景时,“实用性”(Practicality)就成为了衡量一个操作系统好坏的关键标准之一。那么,什么构成了一个“实用”的操作系统呢? + +“实用”并非一个严格的操作系统分类术语(如分时系统、实时系统等),而是更侧重于一个操作系统在现实世界中能否有效、可靠、高效地完成其设计目标,并被用户和开发者方便地使用。一个“实用”的操作系统通常具备以下几个关键特征: + +- 易用性 (Usability): + - 用户易用性: 对于有用户交互界面的系统,界面需要直观、易于学习和操作。 + - 开发者易用性: 需要提供清晰的文档、完善的软件开发工具包(SDK)、调试工具和活跃的开发者社区支持。这对于本次实验尤其重要,我们需要使用OpenHarmony的SDK进行开发。 + - Android Studio(Android); Devco Studio(HarmonyOS/OpenHarmony) +- 性能 (Performance) 与效率 (Efficiency): + - 系统需要在其目标硬件上表现出可接受的性能。这意味着响应用户操作要及时,执行任务要高效。 + - 对于移动或嵌入式系统,**资源效率**(特别是功耗和内存占用)至关重要,直接关系到电池续航和成本。低延迟与高能效比(适用于IoT、边缘计算) +- 可维护性 (Maintainability) 与生态系统 (Ecosystem): + - 操作系统需要能够被持续更新和维护,以修复bug、堵塞安全漏洞、适应新的硬件和需求(例如手机不断地更新推送,Windows Update等)。 + - 一个健康的生态系统(包括硬件制造商、软件开发者、用户社区)是操作系统生命力的体现,也是其保持“实用”的关键。 +- 安全性 (Security): + + - 必须提供必要的安全机制,保护系统自身和用户数据免受恶意软件、未授权访问等威胁。安全性的重要程度取决于应用场景 +总结来说,一个“实用”的操作系统,不仅仅是理论概念的堆砌,更是能够在特定的硬件平台上,稳定、高效、安全地运行,满足用户需求,并为开发者提供便利,从而真正在现实世界中创造价值的软件系统。 + +在我们本次实验中,我们将使用的OpenHarmony,其目标就是成为面向多种智能设备的“实用”操作系统。我们将通过具体的开发实践,体验其作为开发平台的“实用性”,例如利用其SDK进行交叉编译,并在真实的DAYU200开发板上运行我们集成的AI推理库。 +## 1.2 移动操作系统简介 + +### 1.2.1 什么是移动操作系统 +移动操作系统是一种专门为便携式、手持设备(如智能手机、平板电脑、智能手表等)设计的操作系统。与大家更为熟悉的桌面操作系统(如Windows、macOS、Linux发行版如Ubuntu)一样,移动操作系统的核心功能也是管理设备的硬件资源(CPU、内存、存储、网络、传感器等)、提供用户交互界面、以及运行应用程序。 + +然而,由于移动设备与桌面计算机在硬件形态、使用场景、性能功耗等方面存在显著差异,移动操作系统在设计理念和功能侧重上与桌面操作系统有着明显的不同。 + +### 1.2.2 目前主流的移动操作系统 + +目前市场上主流的移动操作系统主要有: + +1. Android (Google): 由Google主导开发,基于Linux内核,是目前市场份额最大的移动操作系统。其开放性吸引了众多设备制造商和开发者。 + +2. iOS(Apple): 由Apple公司为其iPhone、iPad等设备开发的闭源操作系统。以其流畅的用户体验、严格的应用生态和安全性著称。 + +3. HarmonyOS (华为) / OpenHarmony (开源鸿蒙开放原子基金会): 由华为开发,旨在面向万物互联时代,可部署于手机、平板、智能穿戴、智慧屏、车机等多种智能终端。OpenHarmony是HarmonyOS的开源版本,也是本次实验我们将使用的操作系统。 + + +### 1.2.3 移动操作系统 vs. 桌面操作系统 +为了更好地理解移动操作系统的特性,我们将其与大家常用的桌面操作系统进行对比: + +| 特性 | 移动操作系统 (如 Android, iOS, OpenHarmony) | 桌面操作系统 (如 Windows, macOS, Ubuntu) | +| :--------------- | :--------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | +| **设计目标** | 优先考虑便携性、低功耗、触控交互、持续连接 | 优先考虑强大的计算能力、多任务处理、外设扩展性、精确输入(键鼠) | +| **硬件平台** | 通常基于ARM等低功耗架构的SoC(片上系统),资源(CPU、内存、存储)相对受限 | 通常基于x86/x64架构,拥有更强的处理器、更大的内存和存储空间 | +| **用户交互** | 以触摸屏为主要输入方式,支持手势操作,界面为单窗口或分屏应用优化 | 以键盘、鼠标为主要输入方式,支持多窗口、复杂的图形用户界面(GUI) | +| **电源管理** | 极其重要,采用积极的休眠策略、后台任务限制等机制以延长电池续航 | 电源管理相对宽松,虽然也在不断优化,但通常连接电源使用,对续航要求不如移动设备苛刻 | +| **应用生态** | 通常依赖官方或第三方应用商店分发应用,应用运行在沙盒(Sandbox)环境中,权限管理严格 | 软件来源多样(安装包、商店、源码编译等),沙盒机制相对不普遍,权限管理模型不同 | +| **连接性** | 高度依赖无线网络(蜂窝数据、Wi-Fi、蓝牙),内置多种传感器(GPS、加速度计等) | 对有线网络(以太网)支持普遍,无线网络也很常见,但对传感器依赖较少 | +| **系统更新** | 更新通常由设备制造商或运营商推送,有时碎片化问题较严重(Android) | 更新通常由操作系统供应商直接提供,用户可控性相对较高 | +| **开发范式** | 常使用特定的SDK(如Android SDK, iOS SDK, OpenHarmony SDK),注重UI框架和生命周期管理 | 开发工具和语言选择更广泛,系统API调用方式和应用模型不同 | + +在本次实验中,我们将会体会到其中部分差异:硬件平台(交叉编译),应用生态(沙盒),开发范式(SDK) + +### 1.3 OpenHarmony +在了解了移动操作系统的一般概念和特性后,现在我们将焦点转向本次实验的主角——OpenHarmony +#### 1.3.1 什么是 OpenHarmony? + +OpenHarmony(中文常称为“开源鸿蒙”)是一个由开放原子开源基金会(OpenAtom Foundation)孵化和运营的开源项目。它并非仅仅是传统意义上的手机或平板操作系统,而是一个面向全场景、可分布式部署的智能终端操作系统。简单来说,它的目标是成为驱动未来各种智能设备(从小型物联网设备到功能丰富的智能手机、平板、智慧屏等)的统一基础平台。 + +#### 1.3.2 OpenHarmony核心理念与愿景 + +OpenHarmony 旨在打破单一设备的应用边界,其核心设计理念之一是分布式技术。这意味着: + +1. 一次开发,多端部署: 开发者编写的应用,理论上可以通过适配层部署到多种不同形态、不同屏幕尺寸的 OpenHarmony 设备上。 +2. 硬件互助,资源共享: 不同设备可以组成“超级终端”,互相调用对方的硬件能力(例如,用手机的键盘输入文字到智慧屏,或用平板控制无人机的摄像头)。 +3. 无缝流转,协同交互: 应用和数据可以在不同设备间平滑迁移和协同工作,提供一体化的用户体验。 + +> 虽然OpenHarmony的愿景宏伟,但本次实验中,我们仅会体验其在DAYU200开发板上的单设备部署,不会涉及到分布式部署的特性。(因为工作量可能会很大/(ㄒoㄒ)/~~,并且开发板数量较少) + +#### 1.3.3 OpenHarmony关键特性 + +OpenHarmony技术架构如下所示: + +image-20240416151431461 + + +- 分层架构: OpenHarmony 采用了清晰的分层架构,主要包括内核层、系统服务层、框架层和应用层。 + + - 内核层 (Kernel Subsystem): 关键在于其可按需选择内核。对于资源受限的轻量级设备(如内存为 KiB 或 MiB 级别),可选用 LiteOS 内核;对于资源较丰富的标准系统设备(如本次实验使用的 DAYU200 开发板),则可选用 Linux 内核。理解这一点对于后续的交叉编译环境配置非常重要。 + - 系统服务层 (System Service Layer): 提供一系列核心系统能力和通用的基础服务,如分布式能力、图形、多媒体、安全等。 + - 框架层 (Framework Layer): 为应用开发提供必要的 API 和框架,包括应用框架、UI 框架(如 ArkUI)等。 + - 应用层 (Application Layer): 包含系统应用和第三方应用。 +- 组件化设计: 系统可以根据硬件的具体能力进行灵活的组件化裁剪和按需加载,使其能够适配各种内存和性能规格的设备。 +- 开放源代码: 作为一个开源项目,其源代码对全球开发者开放,便于学习、定制和共同发展生态。 + +### 1.3.4 OpenHarmony 与 HarmonyOS 的关系: + +OpenHarmony 是 HarmonyOS(华为鸿蒙操作系统)的开源基础版本。华为将 HarmonyOS 的基础能力贡献给了开放原子开源基金会,形成了 OpenHarmony 项目。其他厂商或开发者可以基于 OpenHarmony 构建自己的操作系统发行版,HarmonyOS 就是基于 OpenHarmony 的一个面向消费者的商业发行版。 + +> 类似于Android与AOSP的关系 + +### 1.3.5 为什么在本次实验中使用 OpenHarmony? + +选择 OpenHarmony 作为本次实验平台,主要基于以下考虑: + +- 代表性: 它代表了现代操作系统(特别是面向物联网和多设备协同)的一个发展方向。 +- 实践平台: 为我们提供了一个真实的、可操作的移动操作系统环境(运行在 DAYU200 开发板上)。 +- 开发体验: 允许我们实践移动平台的开发流程,特别是本次实验重点关注的原生 C++ 代码(Native C++)的交叉编译、库集成与调用。 +- 可及性: 相关的 SDK、开发工具(DevEco Studio)和文档资源相对完善,便于学生学习和使用。 + +在接下来的实验环节中,我们将亲自动手,完成 OpenHarmony 系统镜像的烧录、开发环境的搭建,并最终在其上运行我们自己编译的 C++ AI 推理库,从而深入理解在移动操作系统上进行原生开发和集成的过程。 + +> 其实是为了统一实验平台,单独设计Android实验可能部分同学无法做这个实验。 + +## 1.3 DAYU200开发板 +前面我们介绍了作为软件基础的 OpenHarmony 操作系统,现在我们来认识一下承载这个系统并供我们进行实际操作的硬件平台——DAYU200 开发板。 + +> **为什么实验要使用开发板?**如同实验目的中提到的,本实验希望大家体会移动操作系统与我们平时使用的桌面操作系统的区别。而操作系统的区别很大程度也是由硬件的区别带来的。可以把开发板当成是**一台拓展性比较强的手机**(或者平板,虽然性能比较差),方便大家体验移动开发流程。 + +### 1.3.1 DAYU200开发板介绍 + +> 虽然本节详细介绍了了开发板的各类硬件规格,但本次实验中你需要知道的其实只有: +> +> * 开发板的处理器是RK3568芯片,使用的指令集是**32位的ARM**。(具体而言,是`armeabi-v7a`。) +> * 大家应该在《组成原理》课上,应该已经知道了不同指令集的区别。ARM是现在移动设备上最流行的指令集。大家使用的手机、智能手表,以及新款 MacBook,使用的都是ARM架构。 +> * 开发板的内存(RAM)大小是2GB,存储容量是32GB。 + +为了能够流畅运行功能相对完整的 OpenHarmony 标准系统,并支持复杂的应用开发与调试,DAYU200 配备了较为强大的硬件资源。其核心规格通常包括: +- 处理器 (SoC): 核心是瑞芯微 (Rockchip) RK3568 芯片。这是一款高性能、低功耗的应用处理器,集成了: + - CPU: 四核 32 位 ARM Cortex-A55,主频最高可达 2.0GHz。注意这里的 32位 ARM 架构 (arm32 / armeabi),这决定了我们后续交叉编译的目标平台。 + - GPU: Mali-G52 2EE 图形处理器,支持 OpenGLES 3.2, Vulkan 1.1。 + - NPU (可选): 部分版本集成神经网络处理单元,可提供约 1 TOPS 的 AI 算力,用于硬件加速人工智能应用(本次实验主要使用 CPU 进行推理,但了解 NPU 的存在有助于理解硬件加速潜力)。 +- 内存 (RAM): 开发板内存为2GB的 LPDDR4/LPDDR4X 内存。充足的内存对于运行标准系统和我们的 AI 推理任务至关重要。(2GB对于推理较大模型就有点不够,附录中我们会教大家如何创建交换分区) +- 存储 (Storage): 板载 eMMC 闪存作为主要的系统和数据存储介质,开发板容量为 32GB。 + +同时,开发板还提供了多种接口,可以连接各种外设。(虽然我们实验没有用到,但感兴趣的同学可以自行了解。) + +> 更多信息可以查看链接 [润和HH-SCDAYU200开发套件](https://gitee.com/hihope_iot/docs/tree/master/HiHope_DAYU200#https://gitee.com/hihope_iot/docs/blob/master/HiHope_DAYU200/docs/README.md) + +### 1.3.2 为什么选择使用DAYU200开发板 + +- 官方与社区支持: DAYU200 是 OpenHarmony 官方和社区重点支持的开发板之一,有持续的软件版本适配和丰富的文档、教程资源。这意味着我们可以更容易地获取到可运行的 OpenHarmony 标准系统镜像和解决遇到的问题。 +- 性能适中: 其硬件配置足以流畅运行 OpenHarmony 标准系统,并能够承载我们本次实验中编译 C++ 代码、运行中小型语言模型(如 Llama.cpp 在 CPU 上推理)的需求。 +> 主要是HUAWEI推荐的╮(╯▽╰)╭ + +### 1.3.2 开发板的使用 + +在发放的开发板中,有以下物品: + +1. DAYU200开发板 +2. DAYU200电源适配线 +3. 公对公USB数据线(用于烧写) +4. mini USB B数据线(用于串口调试,本实验可以忽略) + Cable + +当开发板开机后,部分开发板运行的是Openharmony4.0版本的mini system(去年同学们遗留下来的),其没有图形化界面,会直接卡在LOGO,这是正常现象;后面实验我们将教大家如何烧录Openharmony5.0版本的全量系统。 + +当接上电源后,开发板一般会自行启动,如果没有启动请查看开发板上的按钮,根据按钮的功能进行尝试打开。 + +如下图红框,开发板上有六个按钮,按钮下有对应的名称。第一排的三个按钮(RESET、SELECT、MUTE),本次实验不涉及。第二排的三个按钮类似普通手机侧面的三个按钮,分别是: + +* Power:电源键,负责开关机、锁屏、亮屏。 +* VOL+/RECOVERY:音量+/恢复模式键,平时负责增加音量,也可引导手机进入恢复模式。(后面文档会说明。) +* VOL-:音量-键。 + +​ Cable + +### 1.3.3 开发板使用规范 + +开发板的使用采用分组负责人制度。即三人一组,由一人担任负责人并且保管开发板,发放和回收开发板向负责人进行。 + +
+ +# 第二部分 将开源鸿蒙系统安装到开发板上 + +在这一部分,我们将介绍 OpenHarmony 镜像的烧录。为了方便大家体验开发版,助教给大家准备了已经编译好的完整版 OpenHarmony 镜像,可以直接烧录到开发板上。 + +> 本实验**不要求**大家编译 OpenHarmony 操作系统,是因为完整版的操作系统编译资源开销较大,且需要花费较长时间。(我们在实验一编译了最小版本的 Linux 内核,就需要花费15~30分钟,而 OH 系统在此基础上增加了一系列组件(如GUI和应用支持),完整编译需要上百GB存储空间和数小时的时间。) +> +> 如果大家对编译 OpenHarmony 系统的过程感兴趣,可以参考 OpenHarmony 社区文档: +> +> https://gitee.com/openharmony/docs/blob/master/zh-cn/device-dev/subsystems/subsys-build-all.md +> +> (当然,想实际编译还会遇到无数的坑,助教们去年就踩了很久。^_^) + +## 2.1 OpenHarmony 烧录 + +在实验一中,我们曾经为虚拟机安装了操作系统。当时,安装系统的大致流程为: + +1. 将镜像连接到虚拟机(相当于将系统光盘插到电脑里); +2. 从光盘启动系统,并将系统按设置安装(写入)到虚拟机里。 + +在大部分个人电脑/服务器上,系统安装流程也是类似的。 + +然而,对于移动设备而言,以上安装方式是**不可行**的。一方面,移动设备的硬件结构多种多样,难以将系统封装为一个在任何设备上都能启动的镜像。另一方面,移动设备厂商通常也不希望用户能任意更换操作系统,通常也不允许从外部存储设备启动。所以,移动设备更换系统的流程通常更为复杂,且和具体设备相关。 + +对于我们的开发板,安装系统的方式为,通过开发板制造商提供的软件,直接将系统数据**硬件级传输**到开发板存储设备的对应位置。这种模式在嵌入式开发中很常见,我们称为**烧录(Burning)**。在本节中,我们将介绍 RK3568 开发板的烧录流程。 + +> [!IMPORTANT] +> +> RK3568 开发板的烧录软件目前**只支持 Windows 系统**,请在 Windows 系统下进行本章节的步骤。 + +### 2.1.1 软件环境准备 + +#### 2.1.1.1 下载及解压 + +在烧录前,需要大家下载以下内容,并解压到你喜欢的文件夹。(本文档以`D:/OHLab/`为例。) + +* **DAYU200 烧录工具** + * 下载地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 + * 选择`DAYU200烧写工具及指南.zip` + * 备用下载链接:[DAYU200 烧录工具 - GitLab](https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/DAYU200%E7%83%A7%E5%86%99%E5%B7%A5%E5%85%B7%E5%8F%8A%E6%8C%87%E5%8D%97.zip) + +* **Openharmony5.0全量系统镜像** + * 下载地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 + * 选择`OpenHarmony5.0-image.zip` + * 备用下载链接:https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/OpenHarmony5.0-image.zip + +#### 2.1.1.2 安装USB驱动 + +在解压后的`烧写工具及指南`文件夹里,进入`windows/`目录,解压其中的`DriverAssitant_v5.1.1.zip`文件至单独文件夹, + +然后,双击击解压后的`DriverInstall.exe`打开安装程序,点击下图所示的“驱动安装”按钮: + +usb驱动1usb驱动2 + +显示“安装驱动成功”即可,然后可关闭该程序。 + +> 驱动程序,简称驱动,是规定电脑软件与特定硬件如何交互的程序。现在,主流操作系统安装时通常会自带大量常用硬件的驱动,因此我们不需要手动安装。但当需要与操作系统中没有的硬件交互时,就需要我们手动安装驱动了。 + +### 2.1.2 开发板的连接 + +1. 连接开发板 + +* 按照下图提示连接**电源线**和**USB烧写线**。注意,开发板上有三个 USB插口,请连接到中间那个。(接口旁标注了`USB 3.0 OTG`。) + + * 烧写线另一端连接你的计算机。电源线插入电源插座。(这就不用说了吧! (∩_∩)) + + +> 不是说是手机吗,为什么还需要连接电源?大概是为了简化开发板设计吧…… + +连接示意图 + +### 2.1.3烧录步骤 + +#### 2.1.3.1. 打开烧写工具 + +* 如果你的开发板没有打开,请按开发板上的Power键打开开发板。(显示DAYU图标,或进入系统即可。) + +* 双击烧录工具(`烧写工具及指南`文件夹)中 `windows\RKDevTool.exe` 打开烧写工具。 + +* 如图所示,如果你的连接正确,且开发板电源开启,则开发工具下方状态栏会显示:发现一个MASKROM设备。 + + * 如果开发工具状态栏显示“没有发现设备”,则说明连接没有成功。请检查电源线、烧写线是否正确连接。重新拔插烧写线,并尝试长按开发板Power键强制关机,然后再次按Power键开机,等待15秒左右。并检查是否成功。 + + + image-20240416110119355 + +* 导入编译文件镜像包中的 `config.cfg`配置,该配置文件在刚刚解压的系统镜像目录里,路径为 `OpenHarmony\config.cfg`,导入的方法为:(如下面三张图所示) + +1. 在烧写工具左侧空白处右键,单击导入配置。 + +image-20240416110550450 + +2. 选择`OpenHarmony\config.cfg`文件(注意,开发工具里本身也有一个`config.cfg`,但我们要选择的不是这个,**而是系统镜像解压后目录里的`config.cfg`**。 + +image-20240416110804770 + +3. 点击空白栏,逐一配置每个镜像文件对应的路径(也可以双击路径,手动修改路径)。 + * 如,你的镜像解压目录为`D:\OHLab\OpenHarmony`,则第一行应该选择`D:\OHLab\OpenHarmony\MiniLoaderAll.bin`文件,其余各行均应选择相应的img文件。 + +> **这一步在干什么?**实际上,这一步就是在设置,我们要将哪些数据写入到开发板的哪些位置。例如,第一行代表了我们会将`MiniLoaderAll.bin`写入到开发板地址`0x00000000`处(这虽然看起来是个内存地址,但有可能对应了开发板上某个存储器的空间。开发板启动时,会按厂商设计,从某些对应地址读取启动所需要的数据和代码,因此,正确将系统写入到对应位置,就能成功安装系统。(当然,具体写入规则和镜像生成方式,就要参考厂商的文档啦。在本实验中,配置已经给大家写好啦!) + +image-20240416111036833 + +image-20240416111404005 + +image-20240416111714578 + +#### 2.1.3.2. 进入LOADER烧写模式 + +* 默认烧写工具是 `MASKROM` 模式,烧写工具上状态栏会显示“发现一个MASKROM设备”,我们需要将设备进入`Loader`模式。 + +* 进入 `LOADER` 烧写模式 + + 1. 按住 `VOL+/RECOVERY` 按键(图中标注的①号键) 和 `RESET` 按钮(图中标注的②号键)不松开, 此时,烧录工具会显示“没有发现设备” + + 2. 松开 `RESET` 键(②号键), 烧录工具显示“发现一个 `LOADER` 设备” , 说明此时已经进入烧写模式,可以松开`VOL+/RECOVERY`键。 + + loadernon-device + + + + image-20240416113233886 + +* 进行烧录 + + 当烧录工具显示“发现一个LOADER设备”后,可以点击“执行”按钮,进行烧录。烧录进度会显示在右侧。烧录大概需要几分钟,如果烧写成功, 最后在工具界面右侧会显示“下载完成”。此时,烧写即完成,可以断开烧写线了。(连着也无妨。) + + 烧写成功后,开发板会自动启动,并在约30s后,成功打开系统,系统和普通的手机系统类似,但只有很少的几个应用。另外,由于开发板性能较弱,系统可能稍微有些卡顿,这也是正常现象。大家可以自行体验。 + + image-20240416113523492 + +> **什么是MASKROM?什么是LOADER?**可以认为是开发板厂商设计的开发板的不同状态。MASKROM类似一种只读模式,LOADER则是烧录模式。参考:[瑞芯微系列:系统烧录和登录系统 - 知乎](https://zhuanlan.zhihu.com/p/634585861) + + +# 第三部分 安装应用开发环境并开发简单应用 + +在前两部分,我们了解了移动操作系统、OpenHarmony 以及实验使用的 DAYU200 开发板,并完成了基础的系统烧录。接下来,我们将安装官网为 OpenHarmony 和 HarmonyOS 提供的应用开发环境 DevEco Studio,并实际体验一个简单的 OH 应用开发流程。这部分包含两个主要步骤: + +* 在 Windows 上安装集成开发环境 DevEco Studio。 +* 将配置并编译官方的示例应用,并将示例应用运行在开发板上。 + +## 3.1 在 Windows 上安装 DevEco Studio +DevEco Studio 是 HUAWE 推出的官方集成开发环境(IDE),用于 HarmonyOS 和 OpenHarmony 应用开发。它集成了代码编辑、编译、调试、应用签名、HAP 打包、模拟器/预览器以及 SDK 管理等功能。 + +> HAP 是 OpenHarmony 和 HarmonyOS 的应用格式,类似 Android 的 APK。 + +### 3.1.1 下载安装DevEco Studio +* 下载 DevEco Studio + * 下载地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 + * 选择`devecostudio-windows-5.0.11.100.zip` + * 你也可以在官方下载地址下载最新版:https://developer.huawei.com/consumer/cn/deveco-studio/ + * 官方下载地址需要登录华为账号 + +* 安装DevEco Studio,安装过程较为简单,选择默认配置即可。 + +> 注意:DevEco Studio安装包体积较大,请确保网络连接稳定。 +> +> 安装需要15GB左右空间,请选择合适的安装位置,保证空间足够。 +> +> DevEco Studio 中文设置:https://developer.huawei.com/consumer/cn/forum/topic/0204171044047287810 +* 安装完毕后,第一次打开 DevEco 时,提示 Import DevEco Studio Settings,此时选择Do not import settings即可。后续需要同意开发协议,单击同意即可。 + +## 3.2 在 DevEco Studio 上开发应用 + +本节我们将尝试使用 DevEco 创建第一个项目,并尝试将其运行在开发板上。 + +> OpenHarmony 和 HarmonyOS 的应用通常使用 [ArkTS语言](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/arkts-utils/arkts-overview.md) 编写,OpenHarmony 和 HarmonyOS 提供的系统 API 也通常都是 ArkTS接口的(正如我们开发 Android 时离不开 Java 语言一样)。ArkTS 是基于 TypeScript 拓展的新语言(而 TypeScript 则是基于 JavaScript 拓展的强类型语言,套娃了)。 +> +> 当然,为了减轻大家工作量,**本实验不涉及 ArkTS 具体语法内容**,感兴趣的同学可自行了解。为了尽可能使用大家熟悉的语言,我们的应用会使用 OpenHarmony 提供的 Native C++ 开发模式,该模式允许 ArkTS 和 C++ 互相交互,在 OpenHarmony 上运行 C++ 代码。 + +### 3.2.1 DevEco Studio的使用:项目创建 + +1. 通过如下两种方式,打开工程创建向导界面。 + - 如果当前未打开任何工程,可以在DevEco Studio的欢迎页,选择Create Project开始创建一个新NDK工程。 + + - 如果已经打开过工程,可以在菜单栏选择File > New > Create Project来创建一个新NDK工程。 + + > NDK: Native C++ Develop Kit,本地C++开发组件。总之就是为了让开发板能跑C++的神奇妙妙工具,下一阶段实验会进一步解释这是怎么回事。 +2. 根据工程创建向导,选择Native C++工程模板,然后单击Next。(如下图。) + + ![创建项目](./assets/3.1.2/1.png) + +3. 在`Configure Your Project`页面可以修改`Project Name`、项目目录和其它选项,然后单击Finish。 + + * Project Name 就是项目名,推荐使用**自己的学号**(方便检查)。 + * Save Location 是项目存储位置,大家可选择存放在自己喜欢的目录下。 + * 其它选项默认即可。 + +创建项目后,会进入项目开发界面,如下图,和其它IDE类似,开发界面主要分为三个部分: + +* 项目目录(图中①),显示项目中主要文件。 +* 编辑区(图中②),用来写代码。 +* 终端区域(图中③),显示各类信息,或者终端等。 + +![DevEco](./assets/DevEco界面.png) + + + +### 3.2.2 DevEco Studio的使用:项目预览 + +DevEco Studio内置了预览器,能预览我们开发的应用,以方便开发者开发,不需要每次修改代码后都实际将应用传输到设备中运行。由于 DevEco Studio 创建的项目本身就是完整可运行的,我们可以直接启动预览,来体验该应用的效果。 + +为了启动预览,需要: + +1. 点击开发界面右上角,“设备”下拉框(绿色三角运行按钮的左侧的下拉框),选择“Previewer”,如下图。 + +image-20250511204310530 + +2. 点击运行按钮(绿色三角形),选择`2in1`,点击OK。(这一步在选择设备类型,`phonoe`是手机,`tablet`是平板,`2in1`是两种皆可的,根据选择的不同,显示屏幕的比例会有区别。) + +由于编译和渲染预览所需时间较慢,这一步后需要等待约30~60秒。之后,预览会显示在开发界面右侧,如下图: + +![image-20250511205246282](./assets/DevEco-preview-hello.png) + +预览界面类似一个平板屏幕,上面有一行`Hello World`。你可以单击这行字,稍等片刻,内容会变为`Welcome`,如下图: + +image-20250511205539721 + +这就是我们创建的示例应用的功能。如果文字成功改变,你可以单击右上角停止按钮,结束预览。(红色正方形,在运行按钮右侧。) + +> 虽然我们不打算讲解 ArkTS 语法,但我们还是愿意稍微解释一下这个点击变换的效果是怎么实现的,以便感兴趣的同学了解。 +> +> * 应用的主体逻辑在已经默认打开的`Index.ets`文件中。该文件主要部分是`Row`、`Column`、`Text`几个函数的嵌套,这几个函数定义了应用的界面(一行一列,中间有一段文本,文本内容是`this.message`,也就是`'Hello World'`。) +> * 在`Text(this.message)`下,`.onClick`中定义了,这一行Text被点击后,将会执行的代码。 +> * `this.message = 'Welcome';`一行,将`this.message`赋值为`Welcome`,所以,我们看到的文字改变了。 +> * 供感兴趣的同学思考:目前这个改变是单向的,如果我希望将应用效果改为每单击一次,就在`Welcome`和`Hello World`间变换一次,应该如何实现? +> * 提示:ArkTS当然也有`if`语句,语法和C语言一样。 + + + +### 3.2.3 DevEco Studio的使用:将应用运行到开发板上 + +为了将应用运行在开发板上,我们需要按和[2.1.2 开发板的连接](#2.1.2 开发板的连接)一节中相同方式,将电脑与开发板进行连接。 + +正确连接后,再次点击右上角设备下拉框,可以看到设备列表中出现`OpenHarmony 3.2[....]`这样的选项(如下图)。该选项则为开发板所对应的设备。 + +image-20250511211250931 + +但选择该设备并直接运行时,IDE会提示错误如下: + +``` +compatibleSdkVersion and releaseType of the app do not match the apiVersion and releaseType on the device. +``` + +这是因为 DevEco Studio 默认创建的是运行于 HarmonyOS 的项目,而我们的设备运行的是 OpenHarmony。因此,我们需要进行相应设置,将项目编译为 OpenHarmony 应用。 + +#### 3.2.3.1 安装 OpenHarmony SDK + +为了编译 OpenHarmony 项目,我们需要首先安装OpenHarmony SDK。步骤如下: + +1. 点击界面左上角`File > Settings`打开设置。 +2. 选择右侧 OpenHarmony SDK,在 Location 一栏中,选择希望的 OpenHarmony SDK 安装位置。 + * OpenHarmony SDK大小较大(约5GB),请选择合适的安装位置。 +3. 勾选API Version 14中所有的选项,点击`Apply`,等待自动下载完毕并解压到相应目录。 + +> SDK 是 Software Development Kit,软件开发组件,和前面的NDK一样,你可以理解为,用来编译OpenHarmony软件的妙妙小工具。 + +![安装OpenHarmony SDK](./assets/3.1.2/2.png) + +#### 3.2.3.2 修改项目配置文件 + +1. 修改`build-profile.josn5` + + **安装完毕SDK后**,在界面左侧项目目录中,打开**项目根目录下的**`build-profile.json5`,文件开头内容如下所示: + + * 注意,`entry`目录下也有一个`build-profile.json5`,但两者内容不一样(我们接下来也要修改),不要搞混了。 + +```json +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "5.0.5(17)", + "compatibleSdkVersion": "5.0.5(17)", + "runtimeOS": "HarmonyOS", + "buildOption": { +...... +``` +我们需要将其中第8、9、10三行内容,改为以下4行(注意,数字周围没有双引号。): + +```json + "compatibleSdkVersion": 14, + "compileSdkVersion": 14, + "targetSdkVersion": 14, + "runtimeOS": "OpenHarmony" +``` + +修改后的文件如图,这几行分别定义了我们使用的设备操作系统是`OpenHarmony`,用来编译、生成的目标和兼容的SDK版本都是 14。 + +![修改配置文件](./assets/3.1.2/4.png) + + + +> 还记得我们上一步安装的SDK版本吗?也是14。这个版本必须和设备系统上的版本一致,你可以认为,这是移动操作系统的“系统调用”的版本。(实际上是API版本,但某种意义上说,移动应用中,系统API的地位和其它系统中系统调用差不多,因为为了安全和隐私性考虑,移动应用没法直接调用大部分系统调用。) + +2. 修改`entry/build-profile.json5` + +接下来,在左侧界面项目目录里,打开`entry/build-profile.json5`,在第7行`cppFlags`后进行如下修改: + +修改前: + +```json + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "", + } +``` + +修改后: + +```json + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "", + "abiFilters": ["armeabi-v7a","arm64-v8a"] + } +``` + +即,添加一行`"abiFilters": ["armeabi-v7a","arm64-v8a"]`。该行定义了我们运行的处理器架构是`armeabi-v7a`和`arm64-v8a`。 + +> 介绍开发板时,我们提到过,开发板处理器指令集是`armeabi-v7a`,那为什么还要加`arm64-v8a`?因为经过测试,不加这个好像应用跑不起来,我们也不知道原因。╮(╯_╰)╭ + +3. 同步项目设置 + +修改完两个文件后,DevEco应该会出现如下图提示,说明项目没有同步。请点击Sync Now进行同步。 + +![sync](./assets/3.1.2/6.png) + +如果没有出现该提示,你也可以点击界面右上角`File`,选择`Sync and Refresh Project`手动同步。 + +同步过程中可能出现如下`Sync Check`提示,这是因为OpenHarmony应用只支持`default`一种设备类型(而不是前面我们选择过的`phone`、`tablet`、`2in1`几种)。这里点击Yes即可。 + +image-20250511214915860 + +等待一段时间,如果没有错误提示,下方显示`Process finished with exit code 0`,则说明项目配置同步成功。 + +> 同步是为了将正确的项目配置告诉 DevEco,让其能够按照配置正确编译我们的应用。 + +#### 3.2.3.3 在开发板上运行应用 + +在配置完上述所有内容后,我们终于可以实际在开发板上运行我们的应用了。和本节开始所述一样,**连接好开发板后**,在右上角下拉框中选择`OpenHarmony`开头的设备如下: + +image-20250511211250931 + +然后,将项目调整为Debug模式,点击上述下拉框左侧的`entry`下拉框,选择开头图标是`H`的选项,如下图: + +image-20250511221121326 + +> 这一步中,上方的entry选项代表`Release`即正式版应用,下方为`Debug`即调试版应用。调试版应用不需要正式签名,只能在开发过程中使用,正式版应用经过编译优化性能更好,也可以发布给他人,但需要登录华为账号后签名才能使用。参见[下一节](#3.2.3.4 对软件进行签名(可选))的内容。 + +然后,点击运行按钮(右侧绿色三角形),运行项目。应用会被自动安装到开发板上并运行。(运行中可能提示要求生成`debug signature`,如下,点击OK即可。) + +image-20250511221354600 + +在多次运行应用时,软件可能由于版本不同,导致签名不匹配或者安装出错等,出现类似下方的提示。你可以点击`uninstall and reinstall the modules`来强制卸载开发板上已有的应用版本: + +![image-20250511221702724](./assets/image-20250511221702724.png) + +运行成功时,开发板会自动点亮,并自动运行程序,如下。(点击`Hello World`字体,也能成功变为`Welcome`) + +image-20250511221953571 + +#### 3.2.3.4 对软件进行签名(可选) + +> 该部分是可选的,也不包含任何分数。移动设备应用通常需要开发者进行“签名”,否则不允许发布和运行,以保证应用安全。这里的“签名”是密码学术语,同学们以后的课程中会学习到。 +> +> 签名是为了发布自己的应用,或者使用正式版应用。介绍本步骤是为了让大家了解完整的应用开发流程。没有该需求的同学可以跳过这一步。本步骤不占分数。 + +1. 点击界面左上角`File > Project Structure` ,打开项目结构界面。 + +2. 选择左侧`Project`,然后选择上方` Signing Configs`选项卡。 + +3. 在界面中勾选`Support HarmonyOS` ,此时,下方出现提示并提示`Failed to auto generate signing, please sign in first.`这是因为,如果要发布正式版的应用,需要登录华为账号,通过华为账号生成签名。如果你希望发布你的应用,或者使用Release版本的应用,请点击`Sign in`,在网页中登录。登录、签名成功后,界面类似下图。 + + 登录账号 + +
+ +# 第四部分:第一阶段实验内容与检查标准 + +## 4.1 实验内容 + +> 如果你一致跟随实验文档的内容完成到这里,那么前三点你已经完成了,你需要做的只有第4点。 + +1. 将提供的 Openharmony5.0 全量标准系统烧录到开发板中,**体验完整版 OpenHarmony 系统**。(参考[2.1 OpenHarmony烧录](#2.1 OpenHarmony 烧录)。) +2. 安装DevEco Studio,创建第一个项目,并在Previewer中运行。(参考[3.1](#3.1 在 Windows 上安装 DevEco Studio)到[3.2.2](3.2.2 DevEco Studio的使用:项目预览)节。) +3. 并尝试创建名为自己学号的空白Demo(例如PB23011000)在开发板运行。(参考[3.2.3 将应用运行到开发板上](#3.2.3 DevEco Studio的使用:将应用运行到开发板上)) +4. 将单击`Hello World`变成`Welcome`改为单击后**变成你的学号**,然后将该应用重新运行到开发板上。 + * 提示:你需要修改`entry/src/main/ets/Index.ets`。修改很简单,你不需要知道ArkTS的语法。 + * 该文件的大体逻辑参考[3.2.2 项目预览](#3.2.2 DevEco Studio的使用:项目预览)最后的介绍。**其实不参考也很简单啦**。 + * 修改完成后,你需要重新参照[3.2.3.3 在开发板上运行应用](#3.2.3.3 在开发板上运行应用) 中的步骤,把应用再次跑到开发板上。 + + +## 4.2 实验评分标准 + +本次实验共 10 分,第一阶段满分为 4 分(如按时完成检查点,有额外的2分),实验检查要求和评分标准如下: + +1. 成功烧录Openharmony5.0 全量标准系统烧录到开发板。(1分) +2. 完成 DevEco Studio 的安装,并在成功在Previewer中运行项目。(1分) + * **检查点:**在2025年5月16日前完成前两个任务。可额外获得2分(不与阶段2叠加)。 +3. 成功将示例应用在开发板上运行(开发板上实现单击后`Hello World`变为`Welcome`)。(1分) +4. 成功将修改后的应用在开发板上运行(开发板上实现单机后`Hello World`变为**你的学号**)。(1分) + +> 在5月16日前完成前两个任务,额外获得2分,但不与阶段二分数叠加。 +> +> 即,如果你在本周完成阶段一所有任务,你将获得6分。但你仍需要完成完成阶段二所有任务,才能拿到本次实验的满分10分。 + + + diff --git a/docs/ohlab/ohlab-part2.md b/docs/ohlab/ohlab-part2.md index 9dd5c6a..9496c63 100644 --- a/docs/ohlab/ohlab-part2.md +++ b/docs/ohlab/ohlab-part2.md @@ -1,719 +1,719 @@ -# 移动操作系统与端侧AI推理初探-端侧推理应用实现(Part2) - -## 实验目的 - -- 了解AI的基础概念,了解什么是端侧AI推理 -- 学会交叉编译出动态链接库并且在应用开发时使用 - -## 实验环境 - -- OS: - - 交叉编译:Ubuntu 24.04.4 LTS - - OH 应用开发:Windows - -- Platform : VMware - -## 实验时间安排 - -> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 -> -> 注: 所有的实验所需要的素材都可以在睿客网盘链接:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 中找到。 -> -> 此次实验只有两周时间,本文档为第二阶段的实验文档,阅读完毕后可以在第一阶段的基础上开始做第二阶段的实验。 - -- 5.16晚实验课,讲解实验、检查实验 -- 5.23晚实验课,检查实验 -- 5.30晚实验课,补检查实验 - -## 友情提示/为什么要做这个实验? - -- **本实验难度并不高,几乎没有代码上的要求,只是让大家了解完整的移动应用开发流程,并在此过程中,体会移动操作系统与我们之前使用的桌面/服务端操作系统的不同。** -- 如果同学们遇到了问题,请先查询在线文档,也欢迎在文档内/群内/私聊助教提问。在线文档地址:[https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) - - 为了提供足够的信息,方便助教助教更快更好地解答你的疑问,我们推荐你阅读(由LUG撰写的)[提问指南](https://lug.ustc.edu.cn/wiki/doc/howtoask/)。**当然,这并不是必须的,你可以随时提问,助教都会尽可能提供帮助。** - - - -> [!IMPORTANT] -> -> 助教的话: -> -> 本实验虽然没有代码,但涉及到很多背景知识。同学们做实验时难免会遇到不太理解的地方,没关系,按步骤跟着做即可,很多知识/工具,在你未来的学习中还会遇到很多次,你有很多机会慢慢理解。 - -# 实验内容简介 - -> 本节提供对本次实验的概览,让大家能更好地理解本次实验要做什么,目标是什么。实验的具体步骤可以参考本文档后面章节。 - -本次实验中,我们将在提供的 DAYU200 开发板上,运行 OpenHarmony 操作系统,并开发能运行在该开发板上和 OpenHarmony 上的大模型推理应用。为了实现这个目标,需要依次完成以下几个任务: - -1. 将 OpenHarmony 系统安装到开发板上并运行。 - -2. 安装并配置 OpenHarmony 应用的开发环境,成功开发并在开发板上运行一个示例应用。 - -3. 完成大语言模型推理应用的开发,其中包括: - - * 通过交叉编译,将大模型推理框架(Llama.cpp)编译为能够在开发板上使用的动态链接库。 - - * 调用上述库,完成应用,并运行在开发板上。 - -在上一阶段中,我们已经完成了前两个目标。在这一阶段,我们将学习如何使用第三方库,并通过交叉编译,生成能在开发板上使用的动态链接库。最后,我们将调用编译好的动态链接库,在开发板上实现端侧推理功能。 - - - -# 第一部分:第三方库的编译与使用 - -我们本次实验的目标,是在开发板上实现大模型推理应用。然而,很明显,我们是操作系统课程,大部分同学也没有系统学习过人工智能和大语言模型的相关知识。要求大家在一周内学习并实现大语言模型推理,显然是不现实的。 - -> 相信很多同学已经使用过了一些主流的大模型(如国内的 DeepSeek、豆包、千问,国外的 ChatGPT、Claude、Gemini等)和使用这些大模型开发的工具和应用(如 Github Copilot 等)。但大家对大模型的原理可能还不太了解。虽然本实验不涉及大模型的具体原理,但我们还是写了一篇简短的介绍,感兴趣的同学可以阅读[附录A](#附录A: 大模型推理与 llama.cpp 简介)。 -> -> 大模型系统的优化也是我们课题组近年的研究方向之一,欢迎感兴趣的同学联系[李永坤老师](http://staff.ustc.edu.cn/~ykli/),加入我们。(●'◡'●) - -幸运的是,在计算机领域,我们可以常常可以使用前人已经完成的工作。甚至,对于开源软件,我们还能拿到软件源代码,只要遵守开源协议,我们就能对软件做出修改,增添功能,或者移植到我们想要的平台。 - -> 在计算机领域,在已经存在的库/软件基础上做改进甚至是被鼓励的。“不重复造轮子”是计算机领域的常被提及的原则。当然,“重复造轮子”本身是很好的学习过程,我们之前的实验也通过重新“制造” Shell、内存分配器,学习了操作系统相关知识。 - -在本次实验中,我们就将直接使用著名的大模型推理框架 [llama.cpp](https://github.com/ggml-org/llama.cpp/tree/master),来实现我们的大模型推理功能。不过,在深入研究Llama.cpp之前,让我们首先在熟悉的Ubuntu系统上,学习一下什么是动态链接库,以及如何编译和使用一个像Llama.cpp这样的实际第三方库。 - - -## 1.1 什么是动态链接库 (Dynamic Link Libraries / Shared Libraries)? - -在软件开发中,“库 (Library)”是一系列预先编写好的、可重用代码的集合,它们提供了特定的功能,例如数学计算、文件操作、网络通信等。开发者可以在自己的程序中调用这些库提供的功能,而无需从头编写所有代码。 - -链接库主要有两种形式:静态链接库和动态链接库。 - -1. 静态链接库 (Static Libraries): - - - 在程序**编译链接**阶段,静态库的代码会被完整地复制并合并到最终生成的可执行文件中。 - - **优点:** 程序部署简单,因为它不依赖外部库文件;所有代码都在一个文件里。 - - **缺点:** - - 体积大: 如果多个程序都使用了同一个静态库,那么每个程序都会包含一份库代码的副本,导致总体磁盘占用和内存占用增加。 - - 更新困难: 如果静态库更新了(比如修复了一个bug),所有使用了该库的程序都需要重新编译链接才能使用新版本的库。 - - 在Linux中,静态库通常以 .a (archive) 为后缀。 - -2. 动态链接库 (Dynamic Link Libraries / Shared Libraries): - - - 动态库的代码并**不会**在编译时复制到可执行文件中。相反,可执行文件中只包含了对库中函数和变量的引用(或称为“存根”)。 - - 当程序运行时,操作系统会负责在内存中查找、加载所需的动态库,并将程序中的引用指向实际的库代码。 - - **优点:** - - 代码共享,节省资源: 多个程序可以共享内存中同一份动态库的实例,减少了磁盘占用和物理内存的消耗。 - - 独立更新: 动态库可以独立于使用它的程序进行更新。只要库的接口保持兼容,更新后的库可以被所有依赖它的程序自动使用,无需重新编译这些程序。 - - 模块化: 使得大型软件可以被分解成多个更小、更易于管理的模块。 - - **缺点:** - - 运行时依赖: 程序运行时必须能够找到并加载其依赖的动态库文件,否则无法运行(可能会出现“找不到.so文件”的错误)。 - - 版本兼容性问题 (DLL Hell / SO Hell): 如果不同程序依赖同一动态库的不同版本,且这些版本不兼容,可能会导致问题。 - - 在Linux(包括Ubuntu)和OpenHarmony(标准系统)中,动态链接库通常以 .so (shared object) 为后缀。在Windows中,它们则以 .dll (dynamic-link library) 为后缀。 - -**Llama.cpp 项目的核心部分就可以被编译成一个动态链接库 (libllama.so),然后其提供的各种示例程序(如 main, Llama-Demo等)会调用这个库来实现具体功能。本次实验,我们将首先在Ubuntu上体验这个过程** - -## 1.2 使用 Llama.cpp 体验动态链接库的编译与使用 (Ubuntu环境) -在上一节,我们了解了动态链接库的基本概念。现在,我们将以Llama.cpp为例,在Ubuntu环境下,一步步将其核心代码编译成一个动态链接库。Llama.cpp项目支持使用多种构建系统,其中CMake是一个强大且跨平台的选择,非常适合管理C++项目的编译。 - -### 1.2.1 Llama.cpp 简介与源代码获取 -Llama.cpp 是一个用纯C/C++编写的开源项目,旨在高效地在多种硬件平台(包括CPU)上运行Llama系列以及其他架构的大型语言模型(LLM)。它的主要优势在于性能优化、支持模型量化(减小模型体积和内存占用)以及良好的跨平台兼容性,使其非常适合在资源相对受限的端侧设备上进行LLM推理。 - -- 通过压缩包下载: -1. 使用`wget`下载Llama.cpp压缩包 - - ```sh - $ cd ~/oslab - $ wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/llama.cpp.zip - ``` -2. 解压Llama.cpp压缩包,解压后得到llama.cpp文件夹 - - ```sh - $ unzip llama.cpp.zip - ``` - -- 第二种方式(通过git下载): - -1. 安装git - - ```sh - $ sudo apt-get install git - ``` - -2. 下载Llama.cpp - - ```sh - $ git clone https://github.com/ggml-org/llama.cpp.git - ``` - 这会在当前目录下创建一个名为 llama.cpp 的文件夹,其中包含所有源代码。 - -### 1.2.2 Cmake简介 - -CMake本身不是一个编译器,而是一个构建系统生成器。它读取名为 `CMakeLists.txt` 的配置文件(由项目开发者编写),并根据其中的指令为你当前的平台和工具链生成实际的构建脚本(例如Linux上的Makefiles或Ninja文件)。然后,你再使用这些生成的脚本来编译项目。 - -> 我们曾在Lab1中学习过怎么使用makefile文件来进行自动化编译。当我们的项目很复杂时,手写 Makefile 也会变得非常麻烦,而且不方便动态修改。这时候,我们就可以通过编写 `CMakeLists.txt` 来让 CMake 帮我们自动生成 Makefile。(当然,CMake 本身也可能变得很复杂,于是,还有一些简化 CMakeLists 编写的工具,套娃了。) - -**优点:** - -- 跨平台: 同一份CMakeLists.txt通常可以在多个操作系统和编译器上工作。 -- 依赖管理: 能较好地处理项目内和项目间的依赖关系。 -- 灵活性: 支持复杂的构建配置和自定义选项。 - -### 1.2.2 使用Cmake在 Ubuntu 上编译 Llama.cpp 动态链接库 (libllama.so) - -我们将采用“out-of-source build”(在源代码目录之外进行构建)的方式,这是一种良好的CMake实践,可以保持源代码目录的整洁。 - -> 简单说,就是把编译过程的中间文件,和最后生成的结果,都放在单独的目录下。 - -> 虽然如上一节所言,CMake不仅能生成Makefile,但为了简洁起见,以下都暂时认为,CMake就是用来生成(和调用)Makefile的工具,这也是CMake最常见的用法。 - -1. 运行CMake配置项目: - 确保您当前位于 llama.cpp 的根目录下。执行以下命令: - - ```Bash - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_CURL=OFF - ``` - 命令的核心是`cmake -S . -B build`,意思是从当前目录(`.`)读取配置(也就是`CMakeLists.txt`文件),在`build`目录下生成一个`Makefile`,剩余的部分规定了一些具体的编译选项。 - 具体而言,每个参数的意义是: - - - `-S .`: (Source Directory) 指定CMake的从当前目录(`.`)读取配置,以生成相应的`Makefile`。 - - `-B build`: (Build Directory) 指定CMake生成构建系统文件以及后续编译产物的目录为当前目录下的 `build` 子目录(简单说,把生成结果全部放在`build`目录下)。如果 `build` 目录不存在,CMake 会自动创建它。 - - `-DCMAKE_BUILD_TYPE=Release`: 指定构建类型为Release,会开启优化(例如,为`gcc`添加`-O2`等优化选项),生成的库性能更好。如果需要调试,可以使用Debug。 - - `-DLLAMA_BUILD_TESTS=OFF` 和 `-DLLAMA_BUILD_EXAMPLES=OFF`: 这两个参数是库的编译选项。CMakeLists 允许库作者提供自定义选项,让使用者根据自己的需求进行选择。这两个选项的意思分别是“不编译测试”以及“不编译示例程序”,因为我们当前的目标只是生成`libllama.so`库文件,并且后续会单独编译我们自己的llama-Demo.cpp。 - - `-DLLAMA_CURL=OFF`:也是一个编译选项,意义是不使用`CURL`库。(有时候,库可以根据是否有其它某些库,来提供不同的功能。这里我们为了避免安装更多的依赖,关闭该选项。) - - 如果配置成功,终端会显示相关信息,并且build目录下会生成`Makefile`(和其他一些文件)。 - - - - - -2. 执行编译与安装: - 在build目录下,执行以下命令: - - a. 首先,编译 llama 库目标: - - ```bash - cmake --build build -j$(nproc) - ``` - * `--build build` : 告诉CMake执行目录(即build目录)下的构建脚本。 - * `-j$(nproc)`: (可选) 使用所有可用的CPU核心并行编译,以加快速度。 - - > 实际上,`cmake --build build`会让cmake进入`build`目录,然后自动调用`make`。 - - b. 然后,执行安装命令: - - 此命令会将已编译好的目标(根据CMakeLists.txt中的install规则,包括libllama.so和头文件llama.h)安装到指定的 --prefix 下。 - ```bash - cmake --install build --prefix "build/install" - ``` - * `--install build`: 执行构建目录`build`中的安装规则。 - * `--prefix "build/install"`: 指定安装路径的前缀。因为我们当前在 `llama.cpp` 目录下,这会在 `build/` 内部创建一个名为 `install` 的子目录 (即 `llama.cpp/build/install/`),并将库文件安装到 `llama.cpp/build/install/lib/`,头文件安装到 `llama.cpp/build/install/include/`。 - - > 实际上,`cmake --install build`会让cmake进入`build`目录,然后帮助我们调用`make install`,并添加合适的参数,将编译好的文件复制到指定位置。 - -3. 查找并验证编译产物: - 编译成功后,libllama.so 文件通常会生成在`llama.cpp/build/install/lib` 目录下。 - - ``` Bash - ls -l install/lib/libllama.so - file install/lib/libllama.so - ``` - file 命令的输出应该类似:`libllama.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, ...`,表明它是一个为当前Ubuntu x86-64架构编译的动态链接库。 - -现在,我们已经拥有了在Ubuntu上本地编译好的libllama.so。 - -> 如果你仔细观察,会发现`build/install`目录下生成了三个子目录,分别是`bin`、`lib`和`include`。实际上,三个目录都是编译后的产物,每个目录存放的内容如下: -> -> `bin`:一些编译好的可执行文件,用于将模型转换为llama.cpp需要的格式等。在调用库时用不到。 -> -> `lib`:实际上编译出的动态链接库。编译出的`.so`文件其实不止一个,每个都包含了一些函数,它们相互之间可能还会调用。因此,需要用到所有这些`.so`文件,才能正常使用库。(由于所有需要的库文件都在该目录,下一步中,我们会将把该目录指定为需要查找的存放动态链接库的目录。) -> -> `include`:在写代码时,我们必须要`#include`合适的头文件,来告诉我们可以使用哪些函数。该目录就是存放 llama.cpp 所提供的头文件,这些头文件内定义的函数,与生成的`.so`文件里的函数是对应的。因此,我们可以通过`#include`这些头文件,来使用`.so`文件里的对应函数。 - -### 1.2.3 在 Ubuntu 上编译 Llama.cpp 示例程序(llama-Demo.cpp) - -接下来,我们将编译提供的llama-Demo.cpp文件。这个程序是一个独立的C++应用,它将通过调用我们刚刚编译的libllama.so库来实现加载模型和执行推理的功能。(提供gguf模型文件与prompt,llama-Demo.cpp文件将提供的prompt续写生成一段话) - -> 在实际工作流程中,该`llama-Demo.cpp`文件就是由同学们编写的,用来调用llama库的代码。(而llama库则是直接从网上下载的,他人提供的库。)然而,考虑到实验难度,在本次实验中,助教帮大家写好了这个文件。 -> -> 同学们可能会问,如果是自己使用时,我怎么知道要怎么调用库中的函数,用哪个函数呢?这就要阅读库所附带的文档了。通常,库作者同时会提供一个库的说明(或者示例程序),通过阅读这些说明,能够学习到库的用法。例如,llama.cpp的示例程序可以在源码中的`examples`目录下找到。助教的`llama-Demo.cpp`就是按照其中的`simple/simple.cpp`修改得到的。 - -#### 1.2.3.1 下载并编译llama-Demo.cpp -假设我们已经将llama-Demo.cpp放到了一个工作目录,例如`~/oslab/llama-Demo.cpp`。并且,llama.cpp的源代码位于`~/oslab/llama.cpp/`,我们编译好的libllama.so位于`~/oslab/llama.cpp/build/install/lib`,得到的头文件位于``~/oslab/llama.cpp/build/install/include``。 - -1. 下载llama-Demo.cpp并进入llama-Demo.cpp所在目录: - ```Bash - cd ~/oslab/ - wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/llama-Demo.cpp - ``` -2. 执行编译命令: - ```Bash - g++ -o llama-Demo llama-Demo.cpp \ - -I./llama.cpp/build/install/include \ - -L./llama.cpp/build/install/lib \ - -lllama -lggml -lggml-base -lggml-cpu \ - -std=c++17 - ``` - 参数解析: - - `-o llama-Demo`: 指定输出可执行文件的名称为llama-Demo。 - - `-I./llama.cpp/build/install/include`: 指定头文件的搜索路径(相对路径)。 - - `-L./llama.cpp/build/install/lib`: 指定库文件的搜索路径(相对路径)。 - - `-lllama -lggml -lggml-base -lggml-cpu`: (小写L) 告诉链接器链接名为llama,ggml,ggml-base,ggml-cpu的库(即libllama.so等)。(你可以把`-lllama`断句为`-l`和`llama`,虽然它们必须要连着写。意思是链接(link)llama 库。看到这句话的编译器会自动寻找名字为`libllama.so`或`.a`的文件,并从中寻找函数的实现。) - - `-std=c++17`: 指定C++标准版本为C++17。 - - > 从这一步的参数中也可以看出,C/C++中使用第三方库需要两类文件:**头文件**和**库文件**。头文件定义了函数的接口,库文件内存放了函数的实现(机器码)。 - -#### 1.2.3.2 运行llama-Demo - -1. 添加环境变量(意味着程序运行时从哪里找到动态链接库) - - ```Bash - # 注意修改lib的路径 - export LD_LIBRARY_PATH=~/oslab/llama.cpp/build/install/lib:$LD_LIBRARY_PATH - ``` - - `export`: 用于设置环境变量。 - - `LD_LIBRARY_PATH`: 一个环境变量,用于指定动态链接库的搜索路径。 - - `~/oslab/llama.cpp/build/install/lib`: 指定的库文件路径。指向了我们刚刚生成的库的路径。存放了需要的所有`.so`文件。 - - `:$LD_LIBRARY_PATH`: 保留原有的`LD_LIBRARY_PATH`值。(在环境变量中,我们常常用`:`来连接多个路径。例如,当我们要查找两个路径`./a`和`./b`时,会写成`./a:./b`。这里用冒号连接了原来的`LD_LIBRARY_PATH`,意思是查找完提供的路径,还要查找原来环境变量中的路径。) - - 为什么还要查找原来的路径呢?实际上任何C/C++程序都会自动链接一些库文件。比如标准库,里面提供了类似`printf`之类的语言自带函数。 - -2. 使用wget下载模型文件,选择其一即可 - ```Bash - # Tinystories模型,用于生成一个小故事,大小为668MB - wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/tinystories2.Q4_K_M.gguf - # qwen3.0-0.6B模型,用于通用任务,大小为379MB - wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/Qwen3-0.6B-Q4_K_M.gguf - ``` - -3. 运行llama-Demo - ```sh - # `model.gguf`为模型文件路径,当前假设模型文件与可执行文件在同一目录下,`n_predict`为生成的长度,`prompt`为用户输入的提示,程序会根据提示续写出一段话。 - $ ./llama-Demo -m ./model.gguf [-n n_predict] [prompt] - ``` - -如果执行成功应该能看到程序加载模型后,根据提示开始生成文本,这证明了llama-Demo成功调用了动态链接库libllama.so中的功能。示例输出如下所示: -```bash - $ ./llama-Demo -m ./Qwen3-0.6B-Q4_K_M.gguf -n 128 "I'm a student from USTC, My student ID is PB23011000" - ... # 各种配置信息 - I'm a student from USTC,My student ID is PB230110001, and I'm a student in the 2023-2024 academic year. I'm interested in studying in the field of Computer Science. I want to know if I can get a scholarship for the 2023-2024 academic year. I need to apply for the scholarship, and I need to provide the following information: my student ID, my name, my major, my academic year, and my current status. Please help me to fill out the application form. I need to know if I can get a scholarship for the 2023-2024 academic - ... # 性能信息 -``` - -#### 1.2.3.3 llama-Demo.cpp 工作流程 - -这一节中,我们将简要的介绍`llama-Demo.cpp`的代码。由于AI推理实际上是个涉及多个函数调用的复杂过程,我们并不会非常完整地介绍代码中的每一个细节,同学们也不需要完全理解本节的介绍,只需要知道**这份代码通过调用llama.cpp库中提供的函数,实现了运行下载的GGUF模型,并续写用户提供的提示词的功能**。 - -你可以在[附录C](#附录C: llama-Demo.cpp代码说明)中找到更细节的说明。 - -> 如果你还想了解更多,可以参考llama.cpp源码中`include/llama.h`中的注释,`examples/`中的样例程序,以及[llama.cpp 的Github仓库]([ggml-org/llama.cpp: LLM inference in C/C++](https://github.com/ggml-org/llama.cpp))。 - -llama-Demo.cpp的主要工作流程如下: - -- 包含 llama.h 头文件以使用Llama.cpp库的API。 - ```cpp - #include "llama.h" - ``` - - > 整份代码中,所有以小写`llama_`开头的函数,都是`llama.h`中定义的函数。这种统一的命名格式是库作者的良好习惯,能有效避免和其它库的函数出现命名冲突。 - -- 调用`ParseArgs`函数,解析命令行参数,获取模型文件路径、用户提示以及输出的长度设置。 - - ```cpp - int main(int argc, char **argv) { - std::string prompt = "Hello world"; - std::string model_path; - int n_predict = 128; // Default number of tokens to predict - // Parse command line arguments - ParseArgs(argc, argv, model_path, prompt, n_predict); - ... - } - ``` - -- 加载指定的GGUF模型,并且获取词汇表。其中`LoadModel`函数中,又调用了多个`llama_`的库中函数,实现模型加载,并返回模型指针(`llama_model`类型也是`llama.h`中定义的)。 - - > GGUF是llama.cpp使用的模型格式。不同框架可能使用不同模型格式。 - - ```cpp - int main(int argc, char **argv){ - ... - // initialize the model - llama_model* model = LoadModel(model_path); - //get vocab - const llama_vocab * vocab = llama_model_get_vocab(model); - ... - } - ``` - -- 对用户提示进行分词 (Tokenization)。 - - > “tokenization” 通常是大语言模型处理的第一步,和传统NLP的分词不同,它实际上是把文本转成一个个向量组成的列表,每个“词元”(token)对应一个向量。 - > - > `std::vector`是C++中提供的一种“容器”,你可以认为是长度可以变化的数组,和上面说的向量也不是一回事。 - - ```cpp - int main(int argc, char **argv){ - ... - // tokenize the prompt - std::vector prompt_tokens = TokenizePrompt(vocab, prompt); - ... - } - ``` - -- 初始化推理上下文 (Context) 和采样器 (Sampler)。 - - > 也是模型推理中需要的步骤。 - - ```cpp - int main(int argc, char **argv){ - ... - // initialize the context - int n_prompt = prompt_tokens.size(); // number of prompt - llama_context * ctx = InitializeContext(model, n_prompt, n_predict); - // initialize the sampler - llama_sampler * smpl = InitializeSampler(); - ... - } - ``` - -- 执行推理循环,逐个生成词元 (Token),并将词元转换回文本输出。 - - > `GenerateTokens`函数里是个循环,每循环一次生成一个词元(token)。我们平时网上用大模型,会发现模型输出是一个个词往外吐的,这并不是网页为了好看做的美化,而是因为大模型真的是一个个词生成输出的。 - - ```cpp - int main(int argc, char **argv){ - ... - // generate tokens - GenerateTokens(prompt_tokens, ctx, vocab, smpl, n_prompt, n_predict); - ... - } - ``` - -- 打印性能信息并且释放资源。 - - > 又调用了好多 llama.cpp 中的函数。 - - ```cpp - int main(int argc, char **argv) { - ... - // print performance - fprintf(stderr, "\n"); - llama_perf_sampler_print(smpl); - llama_perf_context_print(ctx); - fprintf(stderr, "\n"); - - // free the resources - llama_sampler_free(smpl); - llama_free(ctx); - llama_model_free(model); - } - ``` - - - -# 第二部分 通过交叉编译跨架构生成库文件 - -## 2.1 交叉编译的概念 - -在第一部分,我们在 Ubuntu 内体验了如何使用第三方库编写程序,然而,要在移动设备(在本次实验中,也就是我们的开发板)上使用第三方库,还没有那么简单,我们还需要解决两个关键问题: - -1. 移动设备处理器的体系结构与主机不同,编译出来的库无法直接使用。 -2. 移动设备应用开发模式(如开发语言)不同,需要跨语言调用。 - -在这一部分中, 我们将通过**交叉编译**技术解决第一个问题,而第二个问题则留到下一部分讨论。 - -首先,我们需要理解,为什么处理器体系结构(包括指令集)不同,会给第三方库的使用带来问题。在上一部分中,我们知道,Linux 中第三方库会被编译为`.so`格式的动态链接库。可以想象,为了正常调用,`.so`文件里实际上**存储了库函数执行所需的机器码**(当然,还包括一些其它信息)。不同指令集的机器码不一样,因此,为 `x86_64` 指令集编译的库文件自然没法用于ARM架构的硬件上(即使上面都运行着Linux内核)。 - -因此,为了让开发板能使用 llama.cpp 库,我们需要用我们的电脑,编译出使用 ARM (具体而言,`armeabi-v7a`)架构的动态库。而这种“在一种体系结构的平台上,编译出另一种体系结构平台上可执行代码”的过程,就被称为**交叉编译**。(例如,从使用`x86_64`的我们的电脑上,编译出使用的`armeabi-v7a`的开发板能执行的动态库。) - -> **为什么不在开发板上编译?** -> -> 看了上面的介绍,同学们可能还有疑问。既然主机和开发板架构不同,那我们直接使用开发板编译不就可以了吗?为什么一定要在主机上完成交叉和编译呢? -> -> 实际上,在嵌入式系统和移动设备开发中,交叉编译非常普遍且必要,主要原因包括: -> -> 1. **目标机资源受限**: 像 DAYU200 这样的开发板或许多嵌入式设备,其处理器性能、内存大小、存储空间都远不如桌面PC(例如,有些嵌入式设备只有几MB内存)。在这些设备上直接进行大型项目(如操作系统内核、复杂的 C++ 应用如 Llama.cpp)的编译会非常缓慢,甚至因资源不足而无法完成。 -> 2. **目标机缺乏开发环境**: 很多目标设备可能没有安装编译器、链接器、库文件等完整的开发工具链。它们被设计为运行特定应用,而不是进行软件开发。(OpenHarmony中没有开发工具链) -> 3. **开发效率和便利性**: 开发者通常更习惯在功能强大、工具齐全的PC上进行代码编写、调试和项目管理。交叉编译使得开发者可以在熟悉的开发环境中为资源受限或环境不同的目标设备构建软件。 -> 4. **特定架构需求:** 有些软件就是为特定非主流架构设计的,开发者可能没有该架构的物理机器用于本地编译。(我开发一个跨平台库,难道还需要把世界上所有存在的架构的计算机都买一台吗?) - -## 2.2 准备交叉编译所需的工具(OpenHarmony Native SDK) - -本次实验的核心任务之一是交叉编译 C++ 代码库(Llama.cpp)。这个过程需要在 Linux 环境下进行,并且需要一套特定的交叉编译工具链和 OpenHarmony 系统库/头文件(统称为 Native SDK 或 Toolchain)。这套工具运行在您的 Ubuntu 系统上,但其编译产生的目标代码是运行在 DAYU200 (arm32 架构) 上的 OpenHarmony 系统。 - -1. 使用wget下载OpenHarmony Native SDK - - 下载地址:https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/native-linux-x64-5.0.3.135-Release.zip - - 备用地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 (选择`第二阶段素材 -> native-linux-x64.zip`) - -2. 解压,注意解压目录,后续需要使用(我们后续假设解压路径是`~/ohlab/`) -```sh -$ unzip native-linux-x64-5.0.3.135-Release.zip -``` - -> 这里估计又会有疑问,在3.1步中明明下载过SDK,为什么这里还需要下载SDK? -> -> 最主要原因为Linux和Windows使用的链接库不同,在开发板运行的OpenHarmony的内核是Linux,所以我们需要在Linux上编译出链接库使用,并且我们这里只使用了Native SDK,即编译CPP程序需要的工具,而在3.1步中,我们还下载了编译前端的SDK。 - - -> 还有同学可能会好奇,这个“Native SDK”里,到底**包含了什么**呢? -> -> 实际上,这里的 SDK (也即第一阶段所说的妙妙小工具),包含了一系列编译所需要的工具、库文件、配置等等。例如`gcc`这样的编译器;`make`, `cmake`这样的配置工具;`libstdc++.so`(标准C++库)这样的库文件等。这些工具、文件都是为特定架构生成的,因此与 Ubuntu 中我们用`apt`安装的那些不一样。OH 的 SDK 还包含了用于鸿蒙系统的编译配置,我们下面也会使用到。 - -## 2.3 交叉编译应用过程 - -在前面的步骤中,我们了解了交叉编译的概念,并在 Ubuntu 系统上准备了 OpenHarmony Native SDK,其中包含了针对 DAYU200 开发板(arm32架构)的交叉编译工具链和系统库。现在,我们将实践一个简单的交叉编译过程,同样,我们以之后将会使用的 llama.cpp 为例。 - -### 2.3.1 使用 SDK 中的 CMake 进行交叉编译 - -#### 2.3.1.1 使用SDK中的`cmake`生成`Makefile` - -在第一阶段中,我们演示了使用`cmake`编译 llama.cpp 的过程。实际上,交叉编译的过程几乎一样,我们只需要将编译使用的`cmake`,换成刚刚下载的 `SDK` 中的 `cmake`即可。具体而言,步骤如下: - -1. 确认你的 SDK 的路径,以及其中`cmake`的位置。 - -假设你的 SDK 路径为 `~/ohlab/native-linux-x64-5.0.3.135-Release`,你应该可以在该目录下`./native/build-tools/cmake/bin/`处找到`cmake`程序。例如,运行如下指令(注意替换路径): - -```bash -cd ~/ohlab/native-linux-x64-5.0.3.135-Release -./native/build-tools/cmake/bin/cmake --version -``` - -可以得到类似输出: - -```bash -cmake version 3.28.2 - -CMake suite maintained and supported by Kitware (kitware.com/cmake). -``` - -2. 进入你的`llama.cpp`源码路径。 - -```bash -cd llama.cpp # 请自行修改路径 -``` - -3. 使用 SDK 中的 `cmake` 编译 `llama.cpp`。 - -该过程和第一部分类似,只是需要将`cmake`命令,换成前面确认过的,SDK中的`cmake`的路径,并增加一些额外配置。 - -由于完整路径可能很长,我们可以通过定义环境变量来避免重复书写完整路径,如下: - -```bash -# 定义环境变量,注意将改行改成你自己的路径,请使用绝对路径以避免出错。 -export OHOS_SDK_ROOT="/home/[username]/ohlab/native-linux-x64-5.0.3.135-Release" - -# (可选)使用ls验证环境变量是否设置成功 - -echo "OpenHarmony SDK Linux Native Root: ${OHOS_SDK_ROOT}" -ls "${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake" || echo "SDK bundled CMake not found!" -ls "${OHOS_SDK_ROOT}/build/cmake/ohos.toolchain.cmake" || echo "OHOS Toolchain file not found!" - -# 使用对应的cmake编译文件(以下是一个命令,'\'在bash中表示命令没写完,下一行可以继续写。这条命令太长了,分行写看起来好看。你想写在同一行也是可以的。注意'\'字符后面不能有空格。) -${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake \ - -S . \ - -B build-ohos \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_TOOLCHAIN_FILE=${OHOS_SDK_ROOT}/build/cmake/ohos.toolchain.cmake \ - -DOHOS_ARCH=armeabi-v7a \ - -DCMAKE_CXX_FLAGS="-Wno-c++11-narrowing" \ - -DGGML_NATIVE=OFF \ - -DGGML_OPENMP=OFF \ - -DLLAMA_BUILD_TESTS=OFF \ - -DLLAMA_CURL=OFF -``` - -这一步和 [1.2.2](#1.2.2 使用Cmake在 Ubuntu 上编译 Llama.cpp 动态链接库 (libllama.so))节第二步类似,但明显,我们命令长了很多,添加了许多选项。 - -> 回顾一下,1.2.2节的命令是: -> -> `cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_CURL=OFF` - -我们来具体分析一下命令发生了哪些变化: - -- `${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake`: 我们使用了一个完整的路径名(`${OHOS_SDK_ROOT}`会被替换成我们通过`export`定义的变量),指定使用 OpenHarmony SDK 中自带的 CMake 可执行文件,而不是直接使用`cmake`命令。 -- `-B build-ohos`: 类似`-B build`,只是这次将编译的中间文件和结果放在`build-ohos`而不是`build`目录下,方便我们查找。 -- `-DCMAKE_TOOLCHAIN_FILE=${OHOS_SDK_ROOT}/build/cmake/ohos.toolchain.cmake`: **该参数是交叉编译的核心。**`-DCMAKE_TOOLCHAIN_FILE`会让`cmake`从指定的文件里读取用到的工具的位置(例如编译器`gcc`,和`make`等)。后面的文件是SDK中已经写好的,编译到OpenHarmony的特定配置,它指定了使用 SDK 中的`gcc`进行编译。 -- `-DOHOS_ARCH=armeabi-v7a`: 这个参数用于指定目标CPU架构为 `armeabi-v7a` (ARM 32位架构)。上一个参数的文件内会读取该参数。 -- `-DCMAKE_CXX_FLAGS="-Wno-c++11-narrowing"`:这个参数为GCC添加了一个编译标志,告诉GCC允许范围缩小的强制类型转换。(这是因为我们目标架构是32位的,llama.cpp没考虑这种情况,存在一些把64位整数转换成32位的情况。不加这个参数会导致编译错误。) -- `-DGGML_NATIVE=OFF`、`-DGGML_OPENMP=OFF`:这俩也是编译选项,我们又关了一些东西,因为开发板上没有特定的依赖库。(我们几乎把所有能关的选项都关了。) - -如果命令成功运行,那和之前一样`build-ohos32`下会生成`Makefile`。(和上一阶段类似。) - -#### 2.3.1.2 生成并“安装”库文件 - -这个过程和上一阶段类似,只是把`cmake`换成了`SDK`中的`cmake`罢了。 - -1. 编译Llama.cpp生成动态链接库与头文件: - - ```sh - ${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake --build build-ohos --config Release -j - ${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake --install build-ohos --prefix "build-ohos/install" - ``` - - (注意,这一次,我们把生成的库文件和头文件,安装到了`build-ohos/install`里。) - -2. 查找编译产物: - - 编译成功后,生成的库文件和可执行文件位于刚刚指定的安装目录里,如: - - - 动态链接库 libllama.so 可能位于:`build-ohos/install/lib`。 - - 头文件可能位于:`build-ohos/install/include`。 请检查这些常见位置。例如: - - ```sh - # ls -l build-ohos/install/lib - cmake libggml-base.so libggml-cpu.so libggml.so libllama.so libllava_shared.so libmtmd_shared.so pkgconfig - # ls -l build-ohos/install/include - ggml-alloc.h ggml-blas.h ggml-cpp.h ggml-cuda.h ggml-kompute.h ggml-opt.h ggml-sycl.h gguf.h llama.h ggml-backend.h ggml-cann.h ggml-cpu.h ggml.h ggml-metal.h ggml-rpc.h ggml-vulkan.h llama-cpp.h - ``` - -3. 验证编译产物 - -为了确认我们确实交叉编译出了32位 ARM架构的库文件,我们可以使用 `file` 命令检查生成的 `libllama.so` 文件,确认它的架构: - -```bash -# 在Ubuntu宿主机上,使用 file 命令检查生成的 libllama.so 和 main 文件,确认它们的架构: -$ file build-ohos/install/lib/libllama.so -build-ohos/install/lib/libllama.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=9b36a4f7b3365492d0c566681e81b6aebaafcb8d, with debug_info, not stripped -``` - -输出应明确指出它们是为 `ARM32` 架构(例如上面的`ARM,EABI5`)。 - -完成这些步骤后,你已经成功将 llama.cpp 交叉编译为可以在目标设备( 架构为armeabi-v7a 的 OpenHarmony 系统)上运行的库了。**真不容易,恭喜你!** - -下一阶段,我们会尝试在应用开发中,使用这个编译好的库。 - - - -# 第三部分:将 Llama.cpp 集成到应用中 - -> 由于我们课程毕竟不是 ArkTS 语言学习或者 OpenHarmony 应用开发。本节还是几乎不需要大家完成代码。以介绍流程和引发大家兴趣为主。 - -## 3. 1 在 OpenHarmony 应用中调用 C++ 代码 - -还记得第一阶段中你创建的那个以自己学号命名的 DevEco Studio 中的项目吗?创建项目时,我们选择了 Native C++ 项目,但第一阶段运行时,好像并没有出现什么跟 C++ 有关的内容。 - -实际上,作为 Native C++ 的模板应用,这个项目中隐藏了在ArkTS中调用C++函数的示例。下面,我们将介绍这个例子,简单了解 OpenHarmony 应用调用 C++ 函数的思路。 - -> [!NOTE] -> -> 接下来的内容将在 Windows 中进行,请打开DevEco,开启你之前创建的项目。(或新建一个 Native C++项目。) - -### 3.1.1 Native C++ 代码结构介绍 - -> 在上一阶段,我们虽然通过修改项目配置文件,将一个应用运行在了开发板上,但并没有对项目结构做太多介绍。这一节,我们将首先介绍项目的代码结构。 - -我们可以在DevEco左侧的项目目录中看到项目大体结构。为了简单起见,我们只关注对开发应用最重要的代码部分(其余部分留给感兴趣的同学自行探索吧)。项目主要代码均存放在`entry/src/main`这个目录底下,如图,可以看到,目录下有3个子目录和一个文件。 - -image-20250513223421181 - -这些目录和文件主要功能如下: - -* `cpp`: 存放项目的 C++ 代码,和编译所需的`CMakeLists.txt`、给ArkTS提供的接口等。 -* `ets`: 存放项目的 ArkTS 代码。`ets/pages/Index.ets`可以被认为是项目的入口。 -* `resources`: 存放项目的资源文件。 -* `module.json5`: 该模块(`entry`)的主要配置。 - -> **什么是资源文件?** -> -> 移动应用中,为了安全起见(大家都不想其它不经允许,读取你的微信聊天记录吧),每个应用都运行在“沙盒”之中,只能看到自己应用需要的一小部分文件。(联系课上所学的“虚拟内存空间”,你可以认为,移动应用也有一个“虚拟文件空间”。)但应用本身就会用到许多文件,例如作为背景的图片、音效等等。为了方便应用使用这些文件,项目中会指定一个目录,专门存放这些文件,这些文件能被应用看到,称为资源文件。(这里解释其实没那么严谨,大家意会吧^_^) -> -> **什么是模块?** -> -> 一个应用其实可以有多个“模块”组成,每个模块都有自己独立的依赖库等,方便管理。我们的示例应用中只有一个模块`entry`,即应用的入口模块,你可以认为,应用一打开就会进入这个`entry`模块中`Index.ets`所描述的页面。相当于OH应用中的主进程、main函数了。 - -### 3.1.2 尝试调用 C++ 函数 - -项目中,其实已经给我们定义好了一个C++函数`Add`,和它的ArkTS接口函数`add`,功能是将两个数字相加。 - -> 怎么定义的?具体定义在`src/main/cpp/napi_init.cpp`这个文件中,如果你想了解,可以参考[附录D](#附录D: Native C++ 应用中 C++ 函数定义)。 - -相当于,只要我们在 ArkTS 代码中调用 `add` 函数,应用后台经过一系列神奇转换后,就会调用 C++ 函数 `Add` 执行运算,并返回结果。我们现在就来尝试这一点。 - -打开`src/ets/pages/Index.ets`,还记得阶段一中你们修改为学号的一行吗?现在,将其修改为: - -```typescript -this.message = testNapi.add(2, 3).toString(); -``` - -这里`testNapi.add(2, 3)`会调用C++函数,将两个参数相加,并返回结果`5`,而`toString`将把结果转成字符串。然后,这个字符串被赋值给`this.message`。 - -然后,我们运行previewer(参考阶段一),再试试应用功能吧。 - -> 注意,运行Previewer需要使用Release版本,即,将下图下拉框改为图中的选项(上面的entry): -> -> image-20250513232313625 - -点击"Hello World",文字变成如下: - -image-20250513232354639 - -这样我们就成功调用了C++函数啦,欢呼~ - -> 我猜读到这里,大部分同学可能仍然很迷茫,我在哪里,我在干什么,这和C++有什么关系。如果这样,助教表示很抱歉。确实,这次实验涉及的背景知识过多,从 Cmake 到库的使用,从交叉编译到移动应用开发,从 OpenHarmony 到 llama.cpp 。助教已经尽力尝试让大家理解发生了什么了,如果没能做到,我们再次表示抱歉。 -> -> 其实这次实验不止是操作系统,更像是一次计算机学科工具的导论,我们希望这次实验能至少激发大家对某一部分的兴趣,或者对某些工具、流程留下一些印象。如果你没有理解,没关系,也不要气馁,这不是你的错,跟着步骤做一遍实验就可以啦。作为计算机系的学生,在未来几年,实验中的很多概念、工具你还会遇到很多次,你有很多机会不断加深对它们的理解。所以,放轻松吧。 -> -> 当然,如果理解了其中某些东西,我们也由衷为你感到高兴,祝贺你! -> -> 虽然说了那么多,但实验其实还没有结束,不过后续没有想让大家理解的内容啦。助教们把完整的大模型推理应用写好了(虽然很简陋),大家跟着步骤安装使用就好了。当然,我们还是会尽力尝试解释我们在其中做了什么,感兴趣的同学可以尝试了解。 - - - -## 3.2 实现 OpenHarmony 大模型推理应用 - -本阶段的目的是实现能在开发板上运行的大模型推理应用。即将 llama.cpp 的编译产物集成到 Demo 应用中。 - -理论上,根据我们两个阶段的所有前置知识,再加上一点点新知识,本阶段的任务是有可能完成的。实践中,因为背景知识过多,代码较为复杂,我们不要求大家理解具体的实现流程。我们会提供实现好的源代码,你只需要把文件夹替换掉,并按第一阶段中的方法,将项目运行到开发板上即可。 - -### 3.2.1 新建项目并替换代码 - -1. 新建 Native C++ 项目。(参考 第一阶段 3.2.1 节,项目名自定,请记住项目位置,下一步需要找到该项目。) - -2. (在Windows中)打开项目所在目录,删除项目中的 `entry` 文件夹,替换为助教提供的`entry`文件夹。 - - `entry`文件夹的下载方式: - - * 下载链接:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 (选择 `第二阶段素材 -> entry.zip`) - * 解压后替换原`entry`文件夹。 - -### 3.2.2 将交叉编译所需文件复制到项目中 - -1. 在`entry/libs`文件夹下创建`armeabi-v7a`文件夹。如下图,你可以右键`libs`,选择`New`->`Directory`,然后输入文件夹的名字。 - ![create folder](./assets/3.2.3/1.png) -2. 将 2.3.1 节中,在Ubuntu里交叉编译好的库文件,复制到`armeabi-v7a`文件夹下。注意,你需要复制`llama.cpp/build-ohos/install/libs`下所有的`.so`文件。 - -3. 类似第一步,在`entry/src/main/cpp`目录下创建`include`文件夹,将3.1.2中编译得到的头文件(.h文件)复制到该文件夹下。注意,你需要复制`llama.cpp/build-ohos/install/include`下的所有文件。 -4. 构建应用并且在开发板上运行,上面输入框可以输入提示词,效果如下所示: -![alt text](./assets/Llama效果.jpg) - -> 注意:这里运行只能在开发板上运行,不能使用Previewer运行,因为推理过程使用的是真实的硬件(例如CPU),Previewer模拟的硬件环境无法支持。 -> -> 这里效果很差,原因是我们为了大家的体验,选用了特别小的Tinystory模型,该模型将会生成一个小故事,如果你对如何运行其他可以使用的模型比较感兴趣,查看附录E即可。 - - - -### 3.2.3 代码里做了什么?(可选) - -助教的`entry`代码中,大致做了以下修改: - -1. 编写了ArkTS和C++代码。(`src/main/ets/pages/Index.ets`和`src/main/cpp/napi_init.cpp`两个文件。) -2. 添加了C++、ArkTS互相调用的接口。(`src/main/cpp/types/libentry/Index.d.ts`文件。) -3. 修改了C++部分的编译配置,添加对项目中`llama.cpp`库相关文件的依赖。(`src/main/cpp/CMakeLists.txt`文件。) - - - -# 第四部分:第二阶段实验内容与检查标准 - -## 4.1 实验内容 - -1. 使用`cmake`编译`llama.cpp`第三方库,并成功用其编译并运行`llama-Demo.cpp`,能够运行并获得输出。(参考[第一部分](#第一部分:第三方库的编译与使用)。) -2. 交叉编译llama.cpp得到32位的动态链接库`libllama.so,libggml-base.so,libggml-cpu.so,libggml.so,libllama.so,libllava_shared.so`与头文件(参考[第二部分](#第二部分 通过交叉编译跨架构生成库文件)。) -3. 成功在自己的项目中,成功调用C++函数。(参考[3.1节](#3. 1 在 OpenHarmony 应用中调用 C++ 代码)。) -4. 成功在开发板上运行大模型推理应用。(参考[3.2节](#3.2 实现 OpenHarmony 大模型推理应用)。) - - -## 4.2 实验评分标准 - -本次实验共 10 分,第二阶段满分为 6 分,实验检查要求和评分标准如下: - -1. 成功编译动态链接库,并运行`llama-Demo`,得到输出。(2分) -2. 成功交叉编译,得到`.so`库文件,并用`file`命令验证其架构。(2分) -3. 成功在OpenHarmony应用中调用C++函数,在previewer中,实现点击Hello World变为5的效果。(1分) -4. 成功在开发板上运行大模型推理应用。(1分) - - - +# 移动操作系统与端侧AI推理初探-端侧推理应用实现(Part2) + +## 实验目的 + +- 了解AI的基础概念,了解什么是端侧AI推理 +- 学会交叉编译出动态链接库并且在应用开发时使用 + +## 实验环境 + +- OS: + - 交叉编译:Ubuntu 24.04.4 LTS + - OH 应用开发:Windows + +- Platform : VMware + +## 实验时间安排 + +> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 +> +> 注: 所有的实验所需要的素材都可以在睿客网盘链接:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 中找到。 +> +> 此次实验只有两周时间,本文档为第二阶段的实验文档,阅读完毕后可以在第一阶段的基础上开始做第二阶段的实验。 + +- 5.16晚实验课,讲解实验、检查实验 +- 5.23晚实验课,检查实验 +- 5.30晚实验课,补检查实验 + +## 友情提示/为什么要做这个实验? + +- **本实验难度并不高,几乎没有代码上的要求,只是让大家了解完整的移动应用开发流程,并在此过程中,体会移动操作系统与我们之前使用的桌面/服务端操作系统的不同。** +- 如果同学们遇到了问题,请先查询在线文档,也欢迎在文档内/群内/私聊助教提问。在线文档地址:[https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) + - 为了提供足够的信息,方便助教助教更快更好地解答你的疑问,我们推荐你阅读(由LUG撰写的)[提问指南](https://lug.ustc.edu.cn/wiki/doc/howtoask/)。**当然,这并不是必须的,你可以随时提问,助教都会尽可能提供帮助。** + + + +> [!IMPORTANT] +> +> 助教的话: +> +> 本实验虽然没有代码,但涉及到很多背景知识。同学们做实验时难免会遇到不太理解的地方,没关系,按步骤跟着做即可,很多知识/工具,在你未来的学习中还会遇到很多次,你有很多机会慢慢理解。 + +# 实验内容简介 + +> 本节提供对本次实验的概览,让大家能更好地理解本次实验要做什么,目标是什么。实验的具体步骤可以参考本文档后面章节。 + +本次实验中,我们将在提供的 DAYU200 开发板上,运行 OpenHarmony 操作系统,并开发能运行在该开发板上和 OpenHarmony 上的大模型推理应用。为了实现这个目标,需要依次完成以下几个任务: + +1. 将 OpenHarmony 系统安装到开发板上并运行。 + +2. 安装并配置 OpenHarmony 应用的开发环境,成功开发并在开发板上运行一个示例应用。 + +3. 完成大语言模型推理应用的开发,其中包括: + + * 通过交叉编译,将大模型推理框架(Llama.cpp)编译为能够在开发板上使用的动态链接库。 + + * 调用上述库,完成应用,并运行在开发板上。 + +在上一阶段中,我们已经完成了前两个目标。在这一阶段,我们将学习如何使用第三方库,并通过交叉编译,生成能在开发板上使用的动态链接库。最后,我们将调用编译好的动态链接库,在开发板上实现端侧推理功能。 + + + +# 第一部分:第三方库的编译与使用 + +我们本次实验的目标,是在开发板上实现大模型推理应用。然而,很明显,我们是操作系统课程,大部分同学也没有系统学习过人工智能和大语言模型的相关知识。要求大家在一周内学习并实现大语言模型推理,显然是不现实的。 + +> 相信很多同学已经使用过了一些主流的大模型(如国内的 DeepSeek、豆包、千问,国外的 ChatGPT、Claude、Gemini等)和使用这些大模型开发的工具和应用(如 Github Copilot 等)。但大家对大模型的原理可能还不太了解。虽然本实验不涉及大模型的具体原理,但我们还是写了一篇简短的介绍,感兴趣的同学可以阅读[附录A](#附录A: 大模型推理与 llama.cpp 简介)。 +> +> 大模型系统的优化也是我们课题组近年的研究方向之一,欢迎感兴趣的同学联系[李永坤老师](http://staff.ustc.edu.cn/~ykli/),加入我们。(●'◡'●) + +幸运的是,在计算机领域,我们可以常常可以使用前人已经完成的工作。甚至,对于开源软件,我们还能拿到软件源代码,只要遵守开源协议,我们就能对软件做出修改,增添功能,或者移植到我们想要的平台。 + +> 在计算机领域,在已经存在的库/软件基础上做改进甚至是被鼓励的。“不重复造轮子”是计算机领域的常被提及的原则。当然,“重复造轮子”本身是很好的学习过程,我们之前的实验也通过重新“制造” Shell、内存分配器,学习了操作系统相关知识。 + +在本次实验中,我们就将直接使用著名的大模型推理框架 [llama.cpp](https://github.com/ggml-org/llama.cpp/tree/master),来实现我们的大模型推理功能。不过,在深入研究Llama.cpp之前,让我们首先在熟悉的Ubuntu系统上,学习一下什么是动态链接库,以及如何编译和使用一个像Llama.cpp这样的实际第三方库。 + + +## 1.1 什么是动态链接库 (Dynamic Link Libraries / Shared Libraries)? + +在软件开发中,“库 (Library)”是一系列预先编写好的、可重用代码的集合,它们提供了特定的功能,例如数学计算、文件操作、网络通信等。开发者可以在自己的程序中调用这些库提供的功能,而无需从头编写所有代码。 + +链接库主要有两种形式:静态链接库和动态链接库。 + +1. 静态链接库 (Static Libraries): + + - 在程序**编译链接**阶段,静态库的代码会被完整地复制并合并到最终生成的可执行文件中。 + - **优点:** 程序部署简单,因为它不依赖外部库文件;所有代码都在一个文件里。 + - **缺点:** + - 体积大: 如果多个程序都使用了同一个静态库,那么每个程序都会包含一份库代码的副本,导致总体磁盘占用和内存占用增加。 + - 更新困难: 如果静态库更新了(比如修复了一个bug),所有使用了该库的程序都需要重新编译链接才能使用新版本的库。 + - 在Linux中,静态库通常以 .a (archive) 为后缀。 + +2. 动态链接库 (Dynamic Link Libraries / Shared Libraries): + + - 动态库的代码并**不会**在编译时复制到可执行文件中。相反,可执行文件中只包含了对库中函数和变量的引用(或称为“存根”)。 + - 当程序运行时,操作系统会负责在内存中查找、加载所需的动态库,并将程序中的引用指向实际的库代码。 + - **优点:** + - 代码共享,节省资源: 多个程序可以共享内存中同一份动态库的实例,减少了磁盘占用和物理内存的消耗。 + - 独立更新: 动态库可以独立于使用它的程序进行更新。只要库的接口保持兼容,更新后的库可以被所有依赖它的程序自动使用,无需重新编译这些程序。 + - 模块化: 使得大型软件可以被分解成多个更小、更易于管理的模块。 + - **缺点:** + - 运行时依赖: 程序运行时必须能够找到并加载其依赖的动态库文件,否则无法运行(可能会出现“找不到.so文件”的错误)。 + - 版本兼容性问题 (DLL Hell / SO Hell): 如果不同程序依赖同一动态库的不同版本,且这些版本不兼容,可能会导致问题。 + - 在Linux(包括Ubuntu)和OpenHarmony(标准系统)中,动态链接库通常以 .so (shared object) 为后缀。在Windows中,它们则以 .dll (dynamic-link library) 为后缀。 + +**Llama.cpp 项目的核心部分就可以被编译成一个动态链接库 (libllama.so),然后其提供的各种示例程序(如 main, Llama-Demo等)会调用这个库来实现具体功能。本次实验,我们将首先在Ubuntu上体验这个过程** + +## 1.2 使用 Llama.cpp 体验动态链接库的编译与使用 (Ubuntu环境) +在上一节,我们了解了动态链接库的基本概念。现在,我们将以Llama.cpp为例,在Ubuntu环境下,一步步将其核心代码编译成一个动态链接库。Llama.cpp项目支持使用多种构建系统,其中CMake是一个强大且跨平台的选择,非常适合管理C++项目的编译。 + +### 1.2.1 Llama.cpp 简介与源代码获取 +Llama.cpp 是一个用纯C/C++编写的开源项目,旨在高效地在多种硬件平台(包括CPU)上运行Llama系列以及其他架构的大型语言模型(LLM)。它的主要优势在于性能优化、支持模型量化(减小模型体积和内存占用)以及良好的跨平台兼容性,使其非常适合在资源相对受限的端侧设备上进行LLM推理。 + +- 通过压缩包下载: +1. 使用`wget`下载Llama.cpp压缩包 + + ```sh + $ cd ~/oslab + $ wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/llama.cpp.zip + ``` +2. 解压Llama.cpp压缩包,解压后得到llama.cpp文件夹 + + ```sh + $ unzip llama.cpp.zip + ``` + +- 第二种方式(通过git下载): + +1. 安装git + + ```sh + $ sudo apt-get install git + ``` + +2. 下载Llama.cpp + + ```sh + $ git clone https://github.com/ggml-org/llama.cpp.git + ``` + 这会在当前目录下创建一个名为 llama.cpp 的文件夹,其中包含所有源代码。 + +### 1.2.2 Cmake简介 + +CMake本身不是一个编译器,而是一个构建系统生成器。它读取名为 `CMakeLists.txt` 的配置文件(由项目开发者编写),并根据其中的指令为你当前的平台和工具链生成实际的构建脚本(例如Linux上的Makefiles或Ninja文件)。然后,你再使用这些生成的脚本来编译项目。 + +> 我们曾在Lab1中学习过怎么使用makefile文件来进行自动化编译。当我们的项目很复杂时,手写 Makefile 也会变得非常麻烦,而且不方便动态修改。这时候,我们就可以通过编写 `CMakeLists.txt` 来让 CMake 帮我们自动生成 Makefile。(当然,CMake 本身也可能变得很复杂,于是,还有一些简化 CMakeLists 编写的工具,套娃了。) + +**优点:** + +- 跨平台: 同一份CMakeLists.txt通常可以在多个操作系统和编译器上工作。 +- 依赖管理: 能较好地处理项目内和项目间的依赖关系。 +- 灵活性: 支持复杂的构建配置和自定义选项。 + +### 1.2.2 使用Cmake在 Ubuntu 上编译 Llama.cpp 动态链接库 (libllama.so) + +我们将采用“out-of-source build”(在源代码目录之外进行构建)的方式,这是一种良好的CMake实践,可以保持源代码目录的整洁。 + +> 简单说,就是把编译过程的中间文件,和最后生成的结果,都放在单独的目录下。 + +> 虽然如上一节所言,CMake不仅能生成Makefile,但为了简洁起见,以下都暂时认为,CMake就是用来生成(和调用)Makefile的工具,这也是CMake最常见的用法。 + +1. 运行CMake配置项目: + 确保您当前位于 llama.cpp 的根目录下。执行以下命令: + + ```Bash + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_CURL=OFF + ``` + 命令的核心是`cmake -S . -B build`,意思是从当前目录(`.`)读取配置(也就是`CMakeLists.txt`文件),在`build`目录下生成一个`Makefile`,剩余的部分规定了一些具体的编译选项。 + 具体而言,每个参数的意义是: + + - `-S .`: (Source Directory) 指定CMake的从当前目录(`.`)读取配置,以生成相应的`Makefile`。 + - `-B build`: (Build Directory) 指定CMake生成构建系统文件以及后续编译产物的目录为当前目录下的 `build` 子目录(简单说,把生成结果全部放在`build`目录下)。如果 `build` 目录不存在,CMake 会自动创建它。 + - `-DCMAKE_BUILD_TYPE=Release`: 指定构建类型为Release,会开启优化(例如,为`gcc`添加`-O2`等优化选项),生成的库性能更好。如果需要调试,可以使用Debug。 + - `-DLLAMA_BUILD_TESTS=OFF` 和 `-DLLAMA_BUILD_EXAMPLES=OFF`: 这两个参数是库的编译选项。CMakeLists 允许库作者提供自定义选项,让使用者根据自己的需求进行选择。这两个选项的意思分别是“不编译测试”以及“不编译示例程序”,因为我们当前的目标只是生成`libllama.so`库文件,并且后续会单独编译我们自己的llama-Demo.cpp。 + - `-DLLAMA_CURL=OFF`:也是一个编译选项,意义是不使用`CURL`库。(有时候,库可以根据是否有其它某些库,来提供不同的功能。这里我们为了避免安装更多的依赖,关闭该选项。) + + 如果配置成功,终端会显示相关信息,并且build目录下会生成`Makefile`(和其他一些文件)。 + + + + + +2. 执行编译与安装: + 在build目录下,执行以下命令: + + a. 首先,编译 llama 库目标: + + ```bash + cmake --build build -j$(nproc) + ``` + * `--build build` : 告诉CMake执行目录(即build目录)下的构建脚本。 + * `-j$(nproc)`: (可选) 使用所有可用的CPU核心并行编译,以加快速度。 + + > 实际上,`cmake --build build`会让cmake进入`build`目录,然后自动调用`make`。 + + b. 然后,执行安装命令: + + 此命令会将已编译好的目标(根据CMakeLists.txt中的install规则,包括libllama.so和头文件llama.h)安装到指定的 --prefix 下。 + ```bash + cmake --install build --prefix "build/install" + ``` + * `--install build`: 执行构建目录`build`中的安装规则。 + * `--prefix "build/install"`: 指定安装路径的前缀。因为我们当前在 `llama.cpp` 目录下,这会在 `build/` 内部创建一个名为 `install` 的子目录 (即 `llama.cpp/build/install/`),并将库文件安装到 `llama.cpp/build/install/lib/`,头文件安装到 `llama.cpp/build/install/include/`。 + + > 实际上,`cmake --install build`会让cmake进入`build`目录,然后帮助我们调用`make install`,并添加合适的参数,将编译好的文件复制到指定位置。 + +3. 查找并验证编译产物: + 编译成功后,libllama.so 文件通常会生成在`llama.cpp/build/install/lib` 目录下。 + + ``` Bash + ls -l install/lib/libllama.so + file install/lib/libllama.so + ``` + file 命令的输出应该类似:`libllama.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, ...`,表明它是一个为当前Ubuntu x86-64架构编译的动态链接库。 + +现在,我们已经拥有了在Ubuntu上本地编译好的libllama.so。 + +> 如果你仔细观察,会发现`build/install`目录下生成了三个子目录,分别是`bin`、`lib`和`include`。实际上,三个目录都是编译后的产物,每个目录存放的内容如下: +> +> `bin`:一些编译好的可执行文件,用于将模型转换为llama.cpp需要的格式等。在调用库时用不到。 +> +> `lib`:实际上编译出的动态链接库。编译出的`.so`文件其实不止一个,每个都包含了一些函数,它们相互之间可能还会调用。因此,需要用到所有这些`.so`文件,才能正常使用库。(由于所有需要的库文件都在该目录,下一步中,我们会将把该目录指定为需要查找的存放动态链接库的目录。) +> +> `include`:在写代码时,我们必须要`#include`合适的头文件,来告诉我们可以使用哪些函数。该目录就是存放 llama.cpp 所提供的头文件,这些头文件内定义的函数,与生成的`.so`文件里的函数是对应的。因此,我们可以通过`#include`这些头文件,来使用`.so`文件里的对应函数。 + +### 1.2.3 在 Ubuntu 上编译 Llama.cpp 示例程序(llama-Demo.cpp) + +接下来,我们将编译提供的llama-Demo.cpp文件。这个程序是一个独立的C++应用,它将通过调用我们刚刚编译的libllama.so库来实现加载模型和执行推理的功能。(提供gguf模型文件与prompt,llama-Demo.cpp文件将提供的prompt续写生成一段话) + +> 在实际工作流程中,该`llama-Demo.cpp`文件就是由同学们编写的,用来调用llama库的代码。(而llama库则是直接从网上下载的,他人提供的库。)然而,考虑到实验难度,在本次实验中,助教帮大家写好了这个文件。 +> +> 同学们可能会问,如果是自己使用时,我怎么知道要怎么调用库中的函数,用哪个函数呢?这就要阅读库所附带的文档了。通常,库作者同时会提供一个库的说明(或者示例程序),通过阅读这些说明,能够学习到库的用法。例如,llama.cpp的示例程序可以在源码中的`examples`目录下找到。助教的`llama-Demo.cpp`就是按照其中的`simple/simple.cpp`修改得到的。 + +#### 1.2.3.1 下载并编译llama-Demo.cpp +假设我们已经将llama-Demo.cpp放到了一个工作目录,例如`~/oslab/llama-Demo.cpp`。并且,llama.cpp的源代码位于`~/oslab/llama.cpp/`,我们编译好的libllama.so位于`~/oslab/llama.cpp/build/install/lib`,得到的头文件位于``~/oslab/llama.cpp/build/install/include``。 + +1. 下载llama-Demo.cpp并进入llama-Demo.cpp所在目录: + ```Bash + cd ~/oslab/ + wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/llama-Demo.cpp + ``` +2. 执行编译命令: + ```Bash + g++ -o llama-Demo llama-Demo.cpp \ + -I./llama.cpp/build/install/include \ + -L./llama.cpp/build/install/lib \ + -lllama -lggml -lggml-base -lggml-cpu \ + -std=c++17 + ``` + 参数解析: + - `-o llama-Demo`: 指定输出可执行文件的名称为llama-Demo。 + - `-I./llama.cpp/build/install/include`: 指定头文件的搜索路径(相对路径)。 + - `-L./llama.cpp/build/install/lib`: 指定库文件的搜索路径(相对路径)。 + - `-lllama -lggml -lggml-base -lggml-cpu`: (小写L) 告诉链接器链接名为llama,ggml,ggml-base,ggml-cpu的库(即libllama.so等)。(你可以把`-lllama`断句为`-l`和`llama`,虽然它们必须要连着写。意思是链接(link)llama 库。看到这句话的编译器会自动寻找名字为`libllama.so`或`.a`的文件,并从中寻找函数的实现。) + - `-std=c++17`: 指定C++标准版本为C++17。 + + > 从这一步的参数中也可以看出,C/C++中使用第三方库需要两类文件:**头文件**和**库文件**。头文件定义了函数的接口,库文件内存放了函数的实现(机器码)。 + +#### 1.2.3.2 运行llama-Demo + +1. 添加环境变量(意味着程序运行时从哪里找到动态链接库) + + ```Bash + # 注意修改lib的路径 + export LD_LIBRARY_PATH=~/oslab/llama.cpp/build/install/lib:$LD_LIBRARY_PATH + ``` + - `export`: 用于设置环境变量。 + - `LD_LIBRARY_PATH`: 一个环境变量,用于指定动态链接库的搜索路径。 + - `~/oslab/llama.cpp/build/install/lib`: 指定的库文件路径。指向了我们刚刚生成的库的路径。存放了需要的所有`.so`文件。 + - `:$LD_LIBRARY_PATH`: 保留原有的`LD_LIBRARY_PATH`值。(在环境变量中,我们常常用`:`来连接多个路径。例如,当我们要查找两个路径`./a`和`./b`时,会写成`./a:./b`。这里用冒号连接了原来的`LD_LIBRARY_PATH`,意思是查找完提供的路径,还要查找原来环境变量中的路径。) + - 为什么还要查找原来的路径呢?实际上任何C/C++程序都会自动链接一些库文件。比如标准库,里面提供了类似`printf`之类的语言自带函数。 + +2. 使用wget下载模型文件,选择其一即可 + ```Bash + # Tinystories模型,用于生成一个小故事,大小为668MB + wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/tinystories2.Q4_K_M.gguf + # qwen3.0-0.6B模型,用于通用任务,大小为379MB + wget https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/Qwen3-0.6B-Q4_K_M.gguf + ``` + +3. 运行llama-Demo + ```sh + # `model.gguf`为模型文件路径,当前假设模型文件与可执行文件在同一目录下,`n_predict`为生成的长度,`prompt`为用户输入的提示,程序会根据提示续写出一段话。 + $ ./llama-Demo -m ./model.gguf [-n n_predict] [prompt] + ``` + +如果执行成功应该能看到程序加载模型后,根据提示开始生成文本,这证明了llama-Demo成功调用了动态链接库libllama.so中的功能。示例输出如下所示: +```bash + $ ./llama-Demo -m ./Qwen3-0.6B-Q4_K_M.gguf -n 128 "I'm a student from USTC, My student ID is PB23011000" + ... # 各种配置信息 + I'm a student from USTC,My student ID is PB230110001, and I'm a student in the 2023-2024 academic year. I'm interested in studying in the field of Computer Science. I want to know if I can get a scholarship for the 2023-2024 academic year. I need to apply for the scholarship, and I need to provide the following information: my student ID, my name, my major, my academic year, and my current status. Please help me to fill out the application form. I need to know if I can get a scholarship for the 2023-2024 academic + ... # 性能信息 +``` + +#### 1.2.3.3 llama-Demo.cpp 工作流程 + +这一节中,我们将简要的介绍`llama-Demo.cpp`的代码。由于AI推理实际上是个涉及多个函数调用的复杂过程,我们并不会非常完整地介绍代码中的每一个细节,同学们也不需要完全理解本节的介绍,只需要知道**这份代码通过调用llama.cpp库中提供的函数,实现了运行下载的GGUF模型,并续写用户提供的提示词的功能**。 + +你可以在[附录C](#附录C: llama-Demo.cpp代码说明)中找到更细节的说明。 + +> 如果你还想了解更多,可以参考llama.cpp源码中`include/llama.h`中的注释,`examples/`中的样例程序,以及[llama.cpp 的Github仓库]([ggml-org/llama.cpp: LLM inference in C/C++](https://github.com/ggml-org/llama.cpp))。 + +llama-Demo.cpp的主要工作流程如下: + +- 包含 llama.h 头文件以使用Llama.cpp库的API。 + ```cpp + #include "llama.h" + ``` + + > 整份代码中,所有以小写`llama_`开头的函数,都是`llama.h`中定义的函数。这种统一的命名格式是库作者的良好习惯,能有效避免和其它库的函数出现命名冲突。 + +- 调用`ParseArgs`函数,解析命令行参数,获取模型文件路径、用户提示以及输出的长度设置。 + + ```cpp + int main(int argc, char **argv) { + std::string prompt = "Hello world"; + std::string model_path; + int n_predict = 128; // Default number of tokens to predict + // Parse command line arguments + ParseArgs(argc, argv, model_path, prompt, n_predict); + ... + } + ``` + +- 加载指定的GGUF模型,并且获取词汇表。其中`LoadModel`函数中,又调用了多个`llama_`的库中函数,实现模型加载,并返回模型指针(`llama_model`类型也是`llama.h`中定义的)。 + + > GGUF是llama.cpp使用的模型格式。不同框架可能使用不同模型格式。 + + ```cpp + int main(int argc, char **argv){ + ... + // initialize the model + llama_model* model = LoadModel(model_path); + //get vocab + const llama_vocab * vocab = llama_model_get_vocab(model); + ... + } + ``` + +- 对用户提示进行分词 (Tokenization)。 + + > “tokenization” 通常是大语言模型处理的第一步,和传统NLP的分词不同,它实际上是把文本转成一个个向量组成的列表,每个“词元”(token)对应一个向量。 + > + > `std::vector`是C++中提供的一种“容器”,你可以认为是长度可以变化的数组,和上面说的向量也不是一回事。 + + ```cpp + int main(int argc, char **argv){ + ... + // tokenize the prompt + std::vector prompt_tokens = TokenizePrompt(vocab, prompt); + ... + } + ``` + +- 初始化推理上下文 (Context) 和采样器 (Sampler)。 + + > 也是模型推理中需要的步骤。 + + ```cpp + int main(int argc, char **argv){ + ... + // initialize the context + int n_prompt = prompt_tokens.size(); // number of prompt + llama_context * ctx = InitializeContext(model, n_prompt, n_predict); + // initialize the sampler + llama_sampler * smpl = InitializeSampler(); + ... + } + ``` + +- 执行推理循环,逐个生成词元 (Token),并将词元转换回文本输出。 + + > `GenerateTokens`函数里是个循环,每循环一次生成一个词元(token)。我们平时网上用大模型,会发现模型输出是一个个词往外吐的,这并不是网页为了好看做的美化,而是因为大模型真的是一个个词生成输出的。 + + ```cpp + int main(int argc, char **argv){ + ... + // generate tokens + GenerateTokens(prompt_tokens, ctx, vocab, smpl, n_prompt, n_predict); + ... + } + ``` + +- 打印性能信息并且释放资源。 + + > 又调用了好多 llama.cpp 中的函数。 + + ```cpp + int main(int argc, char **argv) { + ... + // print performance + fprintf(stderr, "\n"); + llama_perf_sampler_print(smpl); + llama_perf_context_print(ctx); + fprintf(stderr, "\n"); + + // free the resources + llama_sampler_free(smpl); + llama_free(ctx); + llama_model_free(model); + } + ``` + + + +# 第二部分 通过交叉编译跨架构生成库文件 + +## 2.1 交叉编译的概念 + +在第一部分,我们在 Ubuntu 内体验了如何使用第三方库编写程序,然而,要在移动设备(在本次实验中,也就是我们的开发板)上使用第三方库,还没有那么简单,我们还需要解决两个关键问题: + +1. 移动设备处理器的体系结构与主机不同,编译出来的库无法直接使用。 +2. 移动设备应用开发模式(如开发语言)不同,需要跨语言调用。 + +在这一部分中, 我们将通过**交叉编译**技术解决第一个问题,而第二个问题则留到下一部分讨论。 + +首先,我们需要理解,为什么处理器体系结构(包括指令集)不同,会给第三方库的使用带来问题。在上一部分中,我们知道,Linux 中第三方库会被编译为`.so`格式的动态链接库。可以想象,为了正常调用,`.so`文件里实际上**存储了库函数执行所需的机器码**(当然,还包括一些其它信息)。不同指令集的机器码不一样,因此,为 `x86_64` 指令集编译的库文件自然没法用于ARM架构的硬件上(即使上面都运行着Linux内核)。 + +因此,为了让开发板能使用 llama.cpp 库,我们需要用我们的电脑,编译出使用 ARM (具体而言,`armeabi-v7a`)架构的动态库。而这种“在一种体系结构的平台上,编译出另一种体系结构平台上可执行代码”的过程,就被称为**交叉编译**。(例如,从使用`x86_64`的我们的电脑上,编译出使用的`armeabi-v7a`的开发板能执行的动态库。) + +> **为什么不在开发板上编译?** +> +> 看了上面的介绍,同学们可能还有疑问。既然主机和开发板架构不同,那我们直接使用开发板编译不就可以了吗?为什么一定要在主机上完成交叉和编译呢? +> +> 实际上,在嵌入式系统和移动设备开发中,交叉编译非常普遍且必要,主要原因包括: +> +> 1. **目标机资源受限**: 像 DAYU200 这样的开发板或许多嵌入式设备,其处理器性能、内存大小、存储空间都远不如桌面PC(例如,有些嵌入式设备只有几MB内存)。在这些设备上直接进行大型项目(如操作系统内核、复杂的 C++ 应用如 Llama.cpp)的编译会非常缓慢,甚至因资源不足而无法完成。 +> 2. **目标机缺乏开发环境**: 很多目标设备可能没有安装编译器、链接器、库文件等完整的开发工具链。它们被设计为运行特定应用,而不是进行软件开发。(OpenHarmony中没有开发工具链) +> 3. **开发效率和便利性**: 开发者通常更习惯在功能强大、工具齐全的PC上进行代码编写、调试和项目管理。交叉编译使得开发者可以在熟悉的开发环境中为资源受限或环境不同的目标设备构建软件。 +> 4. **特定架构需求:** 有些软件就是为特定非主流架构设计的,开发者可能没有该架构的物理机器用于本地编译。(我开发一个跨平台库,难道还需要把世界上所有存在的架构的计算机都买一台吗?) + +## 2.2 准备交叉编译所需的工具(OpenHarmony Native SDK) + +本次实验的核心任务之一是交叉编译 C++ 代码库(Llama.cpp)。这个过程需要在 Linux 环境下进行,并且需要一套特定的交叉编译工具链和 OpenHarmony 系统库/头文件(统称为 Native SDK 或 Toolchain)。这套工具运行在您的 Ubuntu 系统上,但其编译产生的目标代码是运行在 DAYU200 (arm32 架构) 上的 OpenHarmony 系统。 + +1. 使用wget下载OpenHarmony Native SDK + + 下载地址:https://git.ustc.edu.cn/KONC/oh_lab/-/raw/main/native-linux-x64-5.0.3.135-Release.zip + + 备用地址:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 (选择`第二阶段素材 -> native-linux-x64.zip`) + +2. 解压,注意解压目录,后续需要使用(我们后续假设解压路径是`~/ohlab/`) +```sh +$ unzip native-linux-x64-5.0.3.135-Release.zip +``` + +> 这里估计又会有疑问,在3.1步中明明下载过SDK,为什么这里还需要下载SDK? +> +> 最主要原因为Linux和Windows使用的链接库不同,在开发板运行的OpenHarmony的内核是Linux,所以我们需要在Linux上编译出链接库使用,并且我们这里只使用了Native SDK,即编译CPP程序需要的工具,而在3.1步中,我们还下载了编译前端的SDK。 + + +> 还有同学可能会好奇,这个“Native SDK”里,到底**包含了什么**呢? +> +> 实际上,这里的 SDK (也即第一阶段所说的妙妙小工具),包含了一系列编译所需要的工具、库文件、配置等等。例如`gcc`这样的编译器;`make`, `cmake`这样的配置工具;`libstdc++.so`(标准C++库)这样的库文件等。这些工具、文件都是为特定架构生成的,因此与 Ubuntu 中我们用`apt`安装的那些不一样。OH 的 SDK 还包含了用于鸿蒙系统的编译配置,我们下面也会使用到。 + +## 2.3 交叉编译应用过程 + +在前面的步骤中,我们了解了交叉编译的概念,并在 Ubuntu 系统上准备了 OpenHarmony Native SDK,其中包含了针对 DAYU200 开发板(arm32架构)的交叉编译工具链和系统库。现在,我们将实践一个简单的交叉编译过程,同样,我们以之后将会使用的 llama.cpp 为例。 + +### 2.3.1 使用 SDK 中的 CMake 进行交叉编译 + +#### 2.3.1.1 使用SDK中的`cmake`生成`Makefile` + +在第一阶段中,我们演示了使用`cmake`编译 llama.cpp 的过程。实际上,交叉编译的过程几乎一样,我们只需要将编译使用的`cmake`,换成刚刚下载的 `SDK` 中的 `cmake`即可。具体而言,步骤如下: + +1. 确认你的 SDK 的路径,以及其中`cmake`的位置。 + +假设你的 SDK 路径为 `~/ohlab/native-linux-x64-5.0.3.135-Release`,你应该可以在该目录下`./native/build-tools/cmake/bin/`处找到`cmake`程序。例如,运行如下指令(注意替换路径): + +```bash +cd ~/ohlab/native-linux-x64-5.0.3.135-Release +./native/build-tools/cmake/bin/cmake --version +``` + +可以得到类似输出: + +```bash +cmake version 3.28.2 + +CMake suite maintained and supported by Kitware (kitware.com/cmake). +``` + +2. 进入你的`llama.cpp`源码路径。 + +```bash +cd llama.cpp # 请自行修改路径 +``` + +3. 使用 SDK 中的 `cmake` 编译 `llama.cpp`。 + +该过程和第一部分类似,只是需要将`cmake`命令,换成前面确认过的,SDK中的`cmake`的路径,并增加一些额外配置。 + +由于完整路径可能很长,我们可以通过定义环境变量来避免重复书写完整路径,如下: + +```bash +# 定义环境变量,注意将改行改成你自己的路径,请使用绝对路径以避免出错。 +export OHOS_SDK_ROOT="/home/[username]/ohlab/native-linux-x64-5.0.3.135-Release" + +# (可选)使用ls验证环境变量是否设置成功 + +echo "OpenHarmony SDK Linux Native Root: ${OHOS_SDK_ROOT}" +ls "${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake" || echo "SDK bundled CMake not found!" +ls "${OHOS_SDK_ROOT}/build/cmake/ohos.toolchain.cmake" || echo "OHOS Toolchain file not found!" + +# 使用对应的cmake编译文件(以下是一个命令,'\'在bash中表示命令没写完,下一行可以继续写。这条命令太长了,分行写看起来好看。你想写在同一行也是可以的。注意'\'字符后面不能有空格。) +${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake \ + -S . \ + -B build-ohos \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=${OHOS_SDK_ROOT}/build/cmake/ohos.toolchain.cmake \ + -DOHOS_ARCH=armeabi-v7a \ + -DCMAKE_CXX_FLAGS="-Wno-c++11-narrowing" \ + -DGGML_NATIVE=OFF \ + -DGGML_OPENMP=OFF \ + -DLLAMA_BUILD_TESTS=OFF \ + -DLLAMA_CURL=OFF +``` + +这一步和 [1.2.2](#1.2.2 使用Cmake在 Ubuntu 上编译 Llama.cpp 动态链接库 (libllama.so))节第二步类似,但明显,我们命令长了很多,添加了许多选项。 + +> 回顾一下,1.2.2节的命令是: +> +> `cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_CURL=OFF` + +我们来具体分析一下命令发生了哪些变化: + +- `${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake`: 我们使用了一个完整的路径名(`${OHOS_SDK_ROOT}`会被替换成我们通过`export`定义的变量),指定使用 OpenHarmony SDK 中自带的 CMake 可执行文件,而不是直接使用`cmake`命令。 +- `-B build-ohos`: 类似`-B build`,只是这次将编译的中间文件和结果放在`build-ohos`而不是`build`目录下,方便我们查找。 +- `-DCMAKE_TOOLCHAIN_FILE=${OHOS_SDK_ROOT}/build/cmake/ohos.toolchain.cmake`: **该参数是交叉编译的核心。**`-DCMAKE_TOOLCHAIN_FILE`会让`cmake`从指定的文件里读取用到的工具的位置(例如编译器`gcc`,和`make`等)。后面的文件是SDK中已经写好的,编译到OpenHarmony的特定配置,它指定了使用 SDK 中的`gcc`进行编译。 +- `-DOHOS_ARCH=armeabi-v7a`: 这个参数用于指定目标CPU架构为 `armeabi-v7a` (ARM 32位架构)。上一个参数的文件内会读取该参数。 +- `-DCMAKE_CXX_FLAGS="-Wno-c++11-narrowing"`:这个参数为GCC添加了一个编译标志,告诉GCC允许范围缩小的强制类型转换。(这是因为我们目标架构是32位的,llama.cpp没考虑这种情况,存在一些把64位整数转换成32位的情况。不加这个参数会导致编译错误。) +- `-DGGML_NATIVE=OFF`、`-DGGML_OPENMP=OFF`:这俩也是编译选项,我们又关了一些东西,因为开发板上没有特定的依赖库。(我们几乎把所有能关的选项都关了。) + +如果命令成功运行,那和之前一样`build-ohos32`下会生成`Makefile`。(和上一阶段类似。) + +#### 2.3.1.2 生成并“安装”库文件 + +这个过程和上一阶段类似,只是把`cmake`换成了`SDK`中的`cmake`罢了。 + +1. 编译Llama.cpp生成动态链接库与头文件: + + ```sh + ${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake --build build-ohos --config Release -j + ${OHOS_SDK_ROOT}/build-tools/cmake/bin/cmake --install build-ohos --prefix "build-ohos/install" + ``` + + (注意,这一次,我们把生成的库文件和头文件,安装到了`build-ohos/install`里。) + +2. 查找编译产物: + + 编译成功后,生成的库文件和可执行文件位于刚刚指定的安装目录里,如: + + - 动态链接库 libllama.so 可能位于:`build-ohos/install/lib`。 + - 头文件可能位于:`build-ohos/install/include`。 请检查这些常见位置。例如: + + ```sh + # ls -l build-ohos/install/lib + cmake libggml-base.so libggml-cpu.so libggml.so libllama.so libllava_shared.so libmtmd_shared.so pkgconfig + # ls -l build-ohos/install/include + ggml-alloc.h ggml-blas.h ggml-cpp.h ggml-cuda.h ggml-kompute.h ggml-opt.h ggml-sycl.h gguf.h llama.h ggml-backend.h ggml-cann.h ggml-cpu.h ggml.h ggml-metal.h ggml-rpc.h ggml-vulkan.h llama-cpp.h + ``` + +3. 验证编译产物 + +为了确认我们确实交叉编译出了32位 ARM架构的库文件,我们可以使用 `file` 命令检查生成的 `libllama.so` 文件,确认它的架构: + +```bash +# 在Ubuntu宿主机上,使用 file 命令检查生成的 libllama.so 和 main 文件,确认它们的架构: +$ file build-ohos/install/lib/libllama.so +build-ohos/install/lib/libllama.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=9b36a4f7b3365492d0c566681e81b6aebaafcb8d, with debug_info, not stripped +``` + +输出应明确指出它们是为 `ARM32` 架构(例如上面的`ARM,EABI5`)。 + +完成这些步骤后,你已经成功将 llama.cpp 交叉编译为可以在目标设备( 架构为armeabi-v7a 的 OpenHarmony 系统)上运行的库了。**真不容易,恭喜你!** + +下一阶段,我们会尝试在应用开发中,使用这个编译好的库。 + + + +# 第三部分:将 Llama.cpp 集成到应用中 + +> 由于我们课程毕竟不是 ArkTS 语言学习或者 OpenHarmony 应用开发。本节还是几乎不需要大家完成代码。以介绍流程和引发大家兴趣为主。 + +## 3. 1 在 OpenHarmony 应用中调用 C++ 代码 + +还记得第一阶段中你创建的那个以自己学号命名的 DevEco Studio 中的项目吗?创建项目时,我们选择了 Native C++ 项目,但第一阶段运行时,好像并没有出现什么跟 C++ 有关的内容。 + +实际上,作为 Native C++ 的模板应用,这个项目中隐藏了在ArkTS中调用C++函数的示例。下面,我们将介绍这个例子,简单了解 OpenHarmony 应用调用 C++ 函数的思路。 + +> [!NOTE] +> +> 接下来的内容将在 Windows 中进行,请打开DevEco,开启你之前创建的项目。(或新建一个 Native C++项目。) + +### 3.1.1 Native C++ 代码结构介绍 + +> 在上一阶段,我们虽然通过修改项目配置文件,将一个应用运行在了开发板上,但并没有对项目结构做太多介绍。这一节,我们将首先介绍项目的代码结构。 + +我们可以在DevEco左侧的项目目录中看到项目大体结构。为了简单起见,我们只关注对开发应用最重要的代码部分(其余部分留给感兴趣的同学自行探索吧)。项目主要代码均存放在`entry/src/main`这个目录底下,如图,可以看到,目录下有3个子目录和一个文件。 + +image-20250513223421181 + +这些目录和文件主要功能如下: + +* `cpp`: 存放项目的 C++ 代码,和编译所需的`CMakeLists.txt`、给ArkTS提供的接口等。 +* `ets`: 存放项目的 ArkTS 代码。`ets/pages/Index.ets`可以被认为是项目的入口。 +* `resources`: 存放项目的资源文件。 +* `module.json5`: 该模块(`entry`)的主要配置。 + +> **什么是资源文件?** +> +> 移动应用中,为了安全起见(大家都不想其它不经允许,读取你的微信聊天记录吧),每个应用都运行在“沙盒”之中,只能看到自己应用需要的一小部分文件。(联系课上所学的“虚拟内存空间”,你可以认为,移动应用也有一个“虚拟文件空间”。)但应用本身就会用到许多文件,例如作为背景的图片、音效等等。为了方便应用使用这些文件,项目中会指定一个目录,专门存放这些文件,这些文件能被应用看到,称为资源文件。(这里解释其实没那么严谨,大家意会吧^_^) +> +> **什么是模块?** +> +> 一个应用其实可以有多个“模块”组成,每个模块都有自己独立的依赖库等,方便管理。我们的示例应用中只有一个模块`entry`,即应用的入口模块,你可以认为,应用一打开就会进入这个`entry`模块中`Index.ets`所描述的页面。相当于OH应用中的主进程、main函数了。 + +### 3.1.2 尝试调用 C++ 函数 + +项目中,其实已经给我们定义好了一个C++函数`Add`,和它的ArkTS接口函数`add`,功能是将两个数字相加。 + +> 怎么定义的?具体定义在`src/main/cpp/napi_init.cpp`这个文件中,如果你想了解,可以参考[附录D](#附录D: Native C++ 应用中 C++ 函数定义)。 + +相当于,只要我们在 ArkTS 代码中调用 `add` 函数,应用后台经过一系列神奇转换后,就会调用 C++ 函数 `Add` 执行运算,并返回结果。我们现在就来尝试这一点。 + +打开`src/ets/pages/Index.ets`,还记得阶段一中你们修改为学号的一行吗?现在,将其修改为: + +```typescript +this.message = testNapi.add(2, 3).toString(); +``` + +这里`testNapi.add(2, 3)`会调用C++函数,将两个参数相加,并返回结果`5`,而`toString`将把结果转成字符串。然后,这个字符串被赋值给`this.message`。 + +然后,我们运行previewer(参考阶段一),再试试应用功能吧。 + +> 注意,运行Previewer需要使用Release版本,即,将下图下拉框改为图中的选项(上面的entry): +> +> image-20250513232313625 + +点击"Hello World",文字变成如下: + +image-20250513232354639 + +这样我们就成功调用了C++函数啦,欢呼~ + +> 我猜读到这里,大部分同学可能仍然很迷茫,我在哪里,我在干什么,这和C++有什么关系。如果这样,助教表示很抱歉。确实,这次实验涉及的背景知识过多,从 Cmake 到库的使用,从交叉编译到移动应用开发,从 OpenHarmony 到 llama.cpp 。助教已经尽力尝试让大家理解发生了什么了,如果没能做到,我们再次表示抱歉。 +> +> 其实这次实验不止是操作系统,更像是一次计算机学科工具的导论,我们希望这次实验能至少激发大家对某一部分的兴趣,或者对某些工具、流程留下一些印象。如果你没有理解,没关系,也不要气馁,这不是你的错,跟着步骤做一遍实验就可以啦。作为计算机系的学生,在未来几年,实验中的很多概念、工具你还会遇到很多次,你有很多机会不断加深对它们的理解。所以,放轻松吧。 +> +> 当然,如果理解了其中某些东西,我们也由衷为你感到高兴,祝贺你! +> +> 虽然说了那么多,但实验其实还没有结束,不过后续没有想让大家理解的内容啦。助教们把完整的大模型推理应用写好了(虽然很简陋),大家跟着步骤安装使用就好了。当然,我们还是会尽力尝试解释我们在其中做了什么,感兴趣的同学可以尝试了解。 + + + +## 3.2 实现 OpenHarmony 大模型推理应用 + +本阶段的目的是实现能在开发板上运行的大模型推理应用。即将 llama.cpp 的编译产物集成到 Demo 应用中。 + +理论上,根据我们两个阶段的所有前置知识,再加上一点点新知识,本阶段的任务是有可能完成的。实践中,因为背景知识过多,代码较为复杂,我们不要求大家理解具体的实现流程。我们会提供实现好的源代码,你只需要把文件夹替换掉,并按第一阶段中的方法,将项目运行到开发板上即可。 + +### 3.2.1 新建项目并替换代码 + +1. 新建 Native C++ 项目。(参考 第一阶段 3.2.1 节,项目名自定,请记住项目位置,下一步需要找到该项目。) + +2. (在Windows中)打开项目所在目录,删除项目中的 `entry` 文件夹,替换为助教提供的`entry`文件夹。 + + `entry`文件夹的下载方式: + + * 下载链接:https://rec.ustc.edu.cn/share/dfbc3380-2b3c-11f0-aee2-27696db61006 (选择 `第二阶段素材 -> entry.zip`) + * 解压后替换原`entry`文件夹。 + +### 3.2.2 将交叉编译所需文件复制到项目中 + +1. 在`entry/libs`文件夹下创建`armeabi-v7a`文件夹。如下图,你可以右键`libs`,选择`New`->`Directory`,然后输入文件夹的名字。 + ![create folder](./assets/3.2.3/1.png) +2. 将 2.3.1 节中,在Ubuntu里交叉编译好的库文件,复制到`armeabi-v7a`文件夹下。注意,你需要复制`llama.cpp/build-ohos/install/libs`下所有的`.so`文件。 + +3. 类似第一步,在`entry/src/main/cpp`目录下创建`include`文件夹,将3.1.2中编译得到的头文件(.h文件)复制到该文件夹下。注意,你需要复制`llama.cpp/build-ohos/install/include`下的所有文件。 +4. 构建应用并且在开发板上运行,上面输入框可以输入提示词,效果如下所示: +![alt text](./assets/Llama效果.jpg) + +> 注意:这里运行只能在开发板上运行,不能使用Previewer运行,因为推理过程使用的是真实的硬件(例如CPU),Previewer模拟的硬件环境无法支持。 +> +> 这里效果很差,原因是我们为了大家的体验,选用了特别小的Tinystory模型,该模型将会生成一个小故事,如果你对如何运行其他可以使用的模型比较感兴趣,查看附录E即可。 + + + +### 3.2.3 代码里做了什么?(可选) + +助教的`entry`代码中,大致做了以下修改: + +1. 编写了ArkTS和C++代码。(`src/main/ets/pages/Index.ets`和`src/main/cpp/napi_init.cpp`两个文件。) +2. 添加了C++、ArkTS互相调用的接口。(`src/main/cpp/types/libentry/Index.d.ts`文件。) +3. 修改了C++部分的编译配置,添加对项目中`llama.cpp`库相关文件的依赖。(`src/main/cpp/CMakeLists.txt`文件。) + + + +# 第四部分:第二阶段实验内容与检查标准 + +## 4.1 实验内容 + +1. 使用`cmake`编译`llama.cpp`第三方库,并成功用其编译并运行`llama-Demo.cpp`,能够运行并获得输出。(参考[第一部分](#第一部分:第三方库的编译与使用)。) +2. 交叉编译llama.cpp得到32位的动态链接库`libllama.so,libggml-base.so,libggml-cpu.so,libggml.so,libllama.so,libllava_shared.so`与头文件(参考[第二部分](#第二部分 通过交叉编译跨架构生成库文件)。) +3. 成功在自己的项目中,成功调用C++函数。(参考[3.1节](#3. 1 在 OpenHarmony 应用中调用 C++ 代码)。) +4. 成功在开发板上运行大模型推理应用。(参考[3.2节](#3.2 实现 OpenHarmony 大模型推理应用)。) + + +## 4.2 实验评分标准 + +本次实验共 10 分,第二阶段满分为 6 分,实验检查要求和评分标准如下: + +1. 成功编译动态链接库,并运行`llama-Demo`,得到输出。(2分) +2. 成功交叉编译,得到`.so`库文件,并用`file`命令验证其架构。(2分) +3. 成功在OpenHarmony应用中调用C++函数,在previewer中,实现点击Hello World变为5的效果。(1分) +4. 成功在开发板上运行大模型推理应用。(1分) + + + > 第一阶段的奖励分数和该阶段不叠加。即使你第一阶段获得了6分,也需要完成1.和2.,才能获得8分。完成全部实验,才能获得10分。 \ No newline at end of file diff --git a/docs/shelllab/shelllab.md b/docs/shelllab/shelllab.md index 5ebcbd5..5fe3268 100644 --- a/docs/shelllab/shelllab.md +++ b/docs/shelllab/shelllab.md @@ -1,844 +1,844 @@ -# 实验一:Linux基础与系统调用——Shell工作原理(part2) - -## 实验目的 - -- 学习如何使用Linux系统调用:实现一个简单的shell - - 学习如何编写makefile:实现一个简单的makefile来测试shell - -## 实验环境 - -- 虚拟机:VMware -- 操作系统:Ubuntu 24.04.2 LTS - -> 系统的安装形式可以自由选择,双系统,虚拟机都可以,系统版本则推荐使用本文档所用版本。注意:由于Linux各种发行版非常庞杂且存在较大差异,因此本试验在其他Linux发行版可能会存在兼容性问题。如果想使用其他环境(如vlab)或系统(如Arch、WSL等),请根据自己的系统**自行**调整实验步骤以及具体指令,达成实验目标即可,但其中出现的兼容性问题助教**无法**保证能够一定解决。 - - -## 实验时间安排 - -> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 - - -- 3.28 晚实验课,讲解实验第一部分、第二部分,检查实验 -- 4.4 清明节放假 -- 4.11 晚实验课,讲解实验第三部分,检查实验 -- 4.18 晚实验课,检查实验 -- 4.25 晚及之后实验课,补检查实验 - -> 补检查分数照常给分,但会**记录**此次检查未按时完成,此记录在最后综合分数时作为一种参考(即:最终分数可能会低于当前分数)。 - -检查时间、地点:周五晚18: 30~22: 00,电三楼406/408。 - -## 如何提问 - -- 请同学们先阅读《提问的智慧》。[原文链接](https://lug.ustc.edu.cn/wiki/doc/smart-questions/) -- 提问前,请先**阅读报错信息**、查询在线文档,或百度。[在线文档链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR); -- 在向助教提问时,请详细描述问题,并提供相关指令及相关问题的报错截图; -- 在QQ群内提问时,如遇到长时未收到回复的情况,可能是由于消息太多可能会被刷掉,因此建议在在线文档上提问; -- 如果助教的回复成功地帮你解决了问题,请回复“问题已解决”,并将问题及解答更新到在线文档。这有助于他人解决同样的问题。 - -## 为什么要做这个实验 - -- 为什么要学会使用Linux? - - Linux的安全性、稳定性更好,性能也更好,配置也更灵活方便,所以常用于服务器和开发环境。实验室和公司的服务器一般也都用Linux; - - Linux是开源系统,代码修改方便,很多学术成果都基于Linux完成; - - Windows是闭源系统,代码无法修改,无法进行后续实验。 -- 为什么要使用虚拟机? - - 虚拟机对你的电脑影响最低。双系统若配置不正确,可能导致无法进入Windows,而虚拟机自带的快照功能也可以解决部分误操作带来的问题。 - - 本实验并不禁止其他环境的使用,但考虑其他环境(如WSL)变数太大,比如可能存在兼容性或者其他配置问题,会耽误同学们大量时间浪费在实验内容以外的琐事,因此建议各位同学尽量保持与本试验一致或类似的环境。 -- 为什么要学会编译Linux内核? - - 这是后续实验的基础。在后续实验中,我们会让大家通过阅读Linux源码、修改Linux源码、编写模块等方式理解一个真实的操作系统是怎么工作的。 - -## 其他友情提示 - -- **合理安排时间,强烈不建议在ddl前赶实验**。 -- 本课程的实验实践性很强,请各位大胆尝试,适当变通,能完成实验任务即可。 -- pdf上文本的复制有时候会丢失或者增加不必要的空格,有时候还会增加不必要的回车,有些指令一行写不下分成两行了,一些同学就漏了第二行。如果出了bug,建议各位先仔细确认自己输入的指令是否正确。要**逐字符**比对。每次输完指令之后,请观察一下指令的输出,检查一下这个输出是不是报错。**请在复制文档上的指令之前先理解一下指令的含义。** 我们在检查实验时会抽查提问指令的含义。 -- 如果你想问“为什么PDF的复制容易出现问题”,请参考[此文章](https://type.cyhsu.xyz/2018/09/understanding-pdf-the-digitalized-paper/)。 -- 如果同学们遇到了问题,请先查询在线文档。在线文档地址:[链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) - -
- -# 章节一 续·Linux、Shell指令教学 及 Makefile 编写和使用 - -## 1.0 环境准备 - -本次实验二三部分需要用到较多依赖工具,请使用包管理器安装以下包: - -- qemu-system-x86 (Ubuntu 20.04以上) 或 qemu (Ubuntu 其他版本) -- git -- build-essential (里面包含了make/gcc/g++等,省去了单独安装的麻烦) -- libelf-dev -- xz-utils -- libssl-dev -- bc -- libncurses5-dev -- libncursesw5-dev - -为了方便同学们复制粘贴,将所有的依赖放在这里:`qemu-system-x86 git build-essential libelf-dev xz-utils libssl-dev bc libncurses5-dev libncursesw5-dev` - -## 1.1 Linux指令 - -### 1.1.1 echo - -输出一个字符串。用法:`echo string`。 - -同样也可以输出一个文件的内容,如`echo file`将文件file的内容以纯文本形式读并输出。 - -经常在Shell脚本中使用该指令,以打印指定信息。 - -### 1.1.2 top - -ps 命令可以一次性给出当前系统中进程状态,但使用此方式得到的信息缺乏时效性,并且,如果管理员需要实时监控进程运行情况,就必须不停地执行 ps 命令,这显然是缺乏效率的。 - -为此,Linux 提供了top命令。top 命令动态地持续监听进程地运行状态,与此同时,该命令还提供了一个交互界面,用户可以根据需要,人性化地定制自己的输出,进而更清楚地了进程的运行状态。直接输入`top`即可使用。 - -| 参数 | 含义 | -| ---------- | ------------------------------------------------------------ | -| -d 秒数 | 指定 top 命令每隔几秒更新。默认是 3 秒。 | -| -b | 使用批处理模式输出。一般和`-n` 选项合用,用于把 top 命令重定向到文件中。 | -| -n 次数 | 指定 top 命令执行的次数。一般和`-b` 选项合用。 | -| -p 进程PID | 仅查看指定 ID 的进程。 | -| -u 用户名 | 只监听某个用户的进程。 | - -在 top 命令的显示窗口中,还可以使用如下按键,进行以下交互操作: - -| 按键 (注意大小写) | 含义 | -| ----------------- | ------------------------------------------------------------ | -| ? 或 h | 显示交互模式的帮助。 | -| P | 按照 CPU 的使用率排序,默认就是此选项。 | -| M | 按照内存的使用率排序。 | -| N | 按照 PID 排序。 | -| k | 按照 PID 给予某个进程一个信号。一般用于中止某个进程,信号 9 是强制中止的信号。 | -| q | 退出 top 命令。 | - -### 1.1.3 sleep - -`sleep n`可以使当前终端暂停n秒。常见于shell脚本中,用于实现两条指令间的等待。 - -### 1.1.4 grep - -筛选并高亮指定字符串。尝试在终端下运行`grep a`,它会等待用户输入。若输入一行不带a的字符串并按回车,它什么都不会输出。若输入多行字符串,其中某些行带a,它会筛选出带a的行,并将该行的a高亮后输出。 - -### 1.1.5 wc - -英文全拼:wordcount - -常见用法:`wc [-lw] [filename]`,用于统计字数。 - -| 参数 | 含义 | -| -------- | ------------------------------------------------------------ | -| -l | 统计行数。 | -| -w | 统计字数。 | -| filename | 统计指定文件的行数/字数。若不指定,会从标准输入(C/C++的stdin/scanf/cin)处读取数据。 | - -### 1.1.6 kill - -发送指定的信号到相应进程。不指定信号将发送`SIGTERM(15)`终止指定进程。如果无法终止该程序可用`“-KILL” `参数,其发送的信号为`SIGKILL(9) `,将强制结束进程。使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。 - -常见用法:`kill [信号] [PID]` - -## 1.2 Shell指令 - -### 1.2.1 管道符 | - -问题:我们想统计Linux下进程的数量,应该如何解决? - -一个很麻烦的解决方法是:先`ps aux`输出所有进程的列表,然后复制它的输出,执行`wc -l`,将之前`ps`输出的内容粘贴到标准输入,传递给`wc`以统计`ps`输出的行数,即进程数量。有没有方法可以免去复制粘贴的麻烦? - -管道符`|`可以将前面一个命令的标准输出(对应C/C++的stdout/printf/cout)传递给下一个命令,作为它的**标准输入**(对应C/C++的stdin/scanf/cin)。 - -- 例1:在终端中运行`ps aux | wc -l`可以显示所有进程的数量。 -- 例2:先打开Linux下的firefox,然后在终端运行`ps aux | grep firefox`可以显示所有进程名含firefox的进程。 -- 例3:接例2,在终端运行`ps aux | grep firefox | wc -l`可以显示所有进程名含firefox的进程的数量。 - -> 思考:`wc -l`的作用是统计输出的行数。所以`ps aux | wc -l`统计出的数字真的是进程数量吗? - -### 1.2.2 重定向符 >/>>/< - -重定向符 `>` , `>>`:它可以把前面一个命令的标准输出保存到文件内。如果文件不存在,则会创建文件。`>` 表示覆盖文件,`>>`表示追加文件。 - -- 举例:`ps -aux > ps.txt`可以把当前运行的所有进程信息输出到ps.txt。ps.txt的原有内容会被覆盖。 -- 举例:`ps -aux >> ps.txt`可以把当前运行的所有进程信息追加写到ps.txt。 - -重定向符 `<`:可以将`<`前的指令的标准输入重定向为`<`后文件的内容。 - -- 举例:`wc -l < ps.txt`可以把ps.txt中记录的信息作为命令的输入,即统计ps.txt中的行数。 - -### 1.2.3 分隔符 ; - -子命令:每行命令可能由若干个子命令组成,各子命令由`;`分隔,这些子命令会被按序依次执行。 - -如:`ps -a; pwd; ls -a` 表示:先打印当前用户运行的进程,然后打印当前shell所在的目录,最后显示当前目录下所有文件。 - -## 1.3 Shell 脚本 - -考虑到往届在检查实验时,大多数时间都浪费在了敲指令上,希望大家可以学会如何编写Shell脚本。 - -### 1.3.1 Shell脚本的编写 - -Shell脚本类似于Windows的.bat批处理文件。一个最简单的Shell脚本长这样: - -```shell -#!/bin/bash -第一条命令 -第二条命令 -第三条命令,以此类推 -``` - -其中,第一行的`#!/bin/bash`是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。Ubuntu下默认的shell是bash。脚本一般命名为`xxx.sh`。 - -### 1.3.2 脚本的运行 - -运行Shell脚本有两种方式。 - -1. 通过执行`sh xxx.sh`来直接调用解释器运行脚本。其中,sh就是我们的Shell。这种方式运行的脚本,不需要在第一行指定解释器信息。 -2. 将该脚本视为可执行程序。首先,保存脚本之后,要通过`chmod +x xxx.sh`给脚本赋予执行权限,然后直接`./xxx.sh`运行脚本。 - -> 注意:脚本的执行和在终端下执行这些语句是完全一致的,依然需要注意绝对路径和相对路径的问题。 - -举例:首先编译abc.cpp并输出一个名为aabbcc的二进制可执行文件,然后执行aabbcc,最后删掉aabbcc,上述操作可以使用如下脚本来实现: - -```shell -#!/bin/bash -gcc -o aabbcc abc.cpp -./aabbcc -rm aabbcc -``` - -之后的实验会介绍更多Shell脚本的语法。 - -## 1.4 Makefile 的编写 - -> 你可以在阅读完本文 [章节二 实现一个Linux Shell](#章节二 实现一个Linux Shell) 后,再阅读这一节。 - -在本实验第一部分 2.3.17 (编译指令)中,我们提到了 `make`,在编译内核和 busybox 时,我们也使用了`make`、`make install`等命令。为什么用一个`make`命令就能编译好庞大的内核呢?它又是怎么工作的呢?在这节,我们就将学习如何使用 Make 工具,以及如何编写 Makefile ,使得我们的程序可以通过`make` 命令编译。 - -### 1.4.1 Make 介绍 - -什么是 Make 呢?Make 是一个用于自动化编译和构建的工具。在工程需要编译的文件较多时,每次都手输编译命令较为繁琐 。而 Make工具就能帮助我们将各个步骤的编译自动化,省去了每次输入编译命令的时间,也方便了将我们写好的程序分发给他人(其他人只要使用`make`命令就好啦)。 - -什么时候我们要使用 Make 呢?简略地说,当我们的项目编译过程比较复杂,诸如涉及文件较多、依赖关系较为复杂,或者需要生成多个不同的文件等时,我们就倾向于使用 Make,把杂乱的编译统一整理起来。Make 还能优化编译速度,当源代码的某一部分发生变化时,`Make`只会重新编译那些直接或间接依赖于已更改部分的代码,而不是重新编译整个程序(在多次编译内核时,你会发现第二次及以后编译会比第一次快得多,就是这个原因 )。 - -我们如何使用 Make 呢?Make 是通过读取名为`Makefile`的文件来工作的,这个文件包含了关于如何构建(编译)程序的规则。我们将在下一节介绍 Makefile 的编写方式。 - -总的来说,Make 是一个强大的工具,它可以帮助我们管理和自动化编译过程,从而提高效率,减少错误,并确保结果的一致性。 - -### 1.4.2 Makefile - -> 该部分内容只包含了本次实验所必需的部分,你也可以参考[跟我一起写Makefile - github.io](https://seisman.github.io/how-to-write-makefile/index.html) 或 [跟我一起写Makefile - Ubuntu中文](https://wiki.ubuntu.org.cn/index.php?title=跟我一起写Makefile:MakeFile介绍&variant=zh-cn) 来了解更多内容。 - -#### 1.4.2.1 使用脚本自动化编译和其局限性 - -在1.3中,我们介绍了shell脚本基本用法,那么编译项目当然也可以使用shell脚本实现。假设我们有一个 C 文件`hello.c`,我们要将其编译为名为`hello`可执行文件,(正如 Lab1 的 2.3.17 节介绍的那样)可以编写以下脚本: - -```bash -# make.sh -gcc -o hello hello.c -``` - -这个命令实际上读取了文件`hello.c`,生成可执行文件`hello`。从编译过程的角度考虑,我们可以说:生成`hello` 的过程,是依赖于文件`hello.c`的。或者说,`hello`是生成的**目标**,而`hello.c`是这个目标的**依赖项**。 - -很多时候,我们的项目不止一个源文件,要生成的目标文件也不止一个,例如我们可能有一个`hello.h`文件存放通用的函数,而希望把`hello1.c`和`hello2.c`(都 include 了`hello.h`)分别编译为`hello1`和`hello2`,我们可能会写出这样一个脚本: - -```bash -# make.sh -gcc -o hello1 hello1.c -gcc -o hello2 hello2.c -``` - -虽然我们的命令里没有出现`hello.h`,但它被包含在两个源文件里,因此`hello1`实际依赖于`hello1.c`和`hello.h`,而`hello2`则依赖于`hello2.c`和`hello.h`。 - -一切似乎都还好,似乎用脚本也能完成自动化编译的任务。但实际上,我们的脚本存在一个关键的问题,那就是运行它时,`hello1`和`hello2`一定会被同时重新编译。 这看起来似乎没什么,但假设我们的`hello1.c`和`hello2.c`都很复杂,每一个都需要编译很长时间。而假设我们某次只修改了`hello1.c`,而没有修改`hello.h`和`hello2.c`,这时候我们用这个脚本,它仍然会重新编译`hello1`,浪费了编译时间。 - -那我们能否拆开成两个脚本呢?大型项目里,要编译的文件可能有千千万万,我们不可能为每个文件(甚至每种组合)都写一个脚本,于是,只靠脚本管理编译过程就显得有些力不从心了。 - -实际上,当一个大项目编译时,可能会出现 `A` 依赖于 `B`,`B`又依赖于`C`的情况。例如,多文件的项目中,为了避免重复编译,通常会把`.c`文件先编译为`.o`文件,再进行链接生成最终的可执行文件或库文件 。这种情况,Make的作用就更加关键了。(本次实验不涉及这样的多级依赖,你可以参考[附录3](#3.C语言程序编译流程)和节开头的参考链接了解更多 C编译相关知识。) - -#### 1.3.2.2 Makefile 基础规则 - -既然脚本没法解决所有问题,那`make`又是怎么解决的呢?正如前文所说,Make 是通过读取名为`Makefile`的文件来工作的。而`Makefile`里实际就记录了整个项目需要生成的所有文件的依赖关系,和生成规则。 - -我们先粗略的看一下 Makefile 的结构,Makefile 由以下格式的块构成,每个块代表一个或多个目标的生成方式和依赖。 - -```makefile -[目标] ... : [依赖项] ... - [生成命令] - ... - ... -``` - -+ `[目标]` - - 代表一个要生成的目标文件名,有时候也可以只是一个标签不对应真实文件(见后续介绍). - -+ `[依赖项]` - - 生成`[目标]`的依赖项,相当于编译所需要的“输入文件”。 - -+ `[生成命令]` - - 生成`[目标]`所需要运行的 Shell 指令。 - -例如,对上一节提到的`hello.c`编译为`hello`的依赖关系,可以用以下`Makefile`来描述(`#`开头的行是 Makefile 里的注释): - -```makefile -# hello 依赖于 hello.c ,通过 gcc -o hello hello.c 生成。 -hello: hello.c - gcc -o hello hello.c -``` - -当我们运行`make`命令时,make会尝试读取当前目录名为 `Makefile`的文件,并按其中描述运行命令。(显然,上面的例子中,它会运行`gcc -o hello hello.c`。) - -一个 `Makefile` 里可以有多个目标,一个目标可以有多个依赖项(也可以没有依赖),如之前`hello1`和`hello2`编译的依赖关系可表示为: - -```makefile -hello1: hello1.c hello.h - gcc -o hello1 hello1.c - -hello2: hello2.c hello.h - gcc -o hello2 hello2.c -``` - -当我们运行`make`时,Make 会尝试生成 `Makefile` 中第一个目标,即`hello1`。如果要生成其它目标,可以在`make`后面添加目标名,如`make hello2`。`make`运行时,会自动检查生成目标是否已经存在,如果存在,会自动判断目标和依赖项的修改时间,如果目标生成之后,依赖项有过修改,`make`会重新生成目标,否则,说明上次生成后,依赖项没有被修改过,这意味着没必要重新编译,`make`将什么都不做。这样,Make 就节省了宝贵的编译时间。 (在多级依赖的场合,如果依赖项不存在,`make`会尝试首先生成依赖项,这也非常符合直觉。) - -#### 1.3.2.3 伪目标和清空目录的规则 - -编译时,除了生成某些东西,有时我们还想做些不会生成对应文件的任务。例如,我们有时可能想删除所有之前编译的结果(如`hello1`和`hello2`)。我们可以这样写: - -```makefile -clean: - rm -rf hello1 hello2 -``` - -这个规则中`[目标]`为clean,`[依赖项]`为空,`[生成命令]`为`rm -f ...`。但是,我们并不打算实际生成一个名为`clean`的文件。这时候,我们称类似`clean`这样的目标为“**伪目标**”。 “伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法通过它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效(以`make clean`来使用该目标)。 - -为了避免和文件重名导致的错误(例如我们目录下真的有名为`clean`的文件 ),我们可以使用一个特殊的标记`.PHONY`来显式地指明一个目标是“伪目标”,向 Make 说明,不管是否有这个文件,这个目标就是“伪目标”。 - -```makefile -.PHONY : clean -``` - -只要有这个声明,不管是否有`clean`文件,以及文件修改时间如何,`make clean`都会执行对应的命令。于是整个过程可以这样写: - -```makefile -.PHONY : clean -clean: - rm -rf hello1 hello2 -``` - -`.PHONY`后可以有多个目标名,也可以写在伪目标的后面。例如,我们可能想用一个命令`make`同时生成`hello1`和`hello2`,可以写一个伪目标`all`,放在`makefile`的开头: - -```makefile -all: hello1 hello2 - -hello1: hello1.c hello.h - gcc -o hello1 hello1.c - -hello2: hello2.c hello.h - gcc -o hello2 hello2.c - -.PHONY : all clean - -clean: - rm -rf hello1 hello2 -``` - -注意这实际上是个多级依赖,`all`依赖于`hello1`和`hello2`,所以这两个依赖不存在时,`make`会尝试生成它们。 - -在我们的实验中,需要使用伪目标`test`来自动运行测试,方法也类似如此,大家可以自行尝试。 - -#### 1.3.2.4 Makefile的文件名 - -默认情况下,make命令会在当前目录下按顺序寻找名为 `GNUmakefile` 、 `makefile` 和 `Makefile` 的文件。在这三个文件名中,最好使用 `Makefile` 这个文件名,因为这个文件名在排序上靠近其它比较重要的文件,比如 `README`。最好不要用 `GNUmakefile`,因为这个文件名只能由GNU `make` ,其它版本的 `make` 无法识别,但是基本上来说,大多数的 `make` 都支持 `makefile` 和 `Makefile` 这两种默认文件名。 - - - -
- -# 章节二 实现一个Linux Shell - -> 注意: -> -> - 本部分的主要目的是锻炼大家**使用** Linux系统调用编写程序的能力,不会涉及编写一个完整的系统调用。 -> - 本部分主要是代码填空,费力的部分已由助教完成,代码量不大,大家不必恐慌。**需要大家完成的代码已经用注释`TODO:`标记,可以通过搜索轻松找到**,使用支持TODO高亮编辑器(如vscode装TODO highlight插件)的同学也可以通过高亮找到要添加内容的地方。 - -## 2.1 Shell的基本原理 - -我们在各种操作系统中会遇到各式各样的用于输入命令、基于文字与系统交互的终端,Windows下的cmd和powershell、Unix下的sh、bash和zsh等等,这些都是不同的shell,我们可以看到它们的共同点——基于“命令” (command) 与系统交互,完成相应的工作。 - -Shell处理命令的方式很好理解,每条输入的“命令”其实都对应着一个可执行文件,例如我们输入`top`时,**shell程序会创建一个新进程并在新进程中运行可执行文件。具体而言,shell程序在特定的文件夹中搜索名为"top"的可执行文件,搜索到之后运行该可执行文件,并将进程的输出定向到指定的位置 (如终端设备) **,这一过程也是我们本次实验要实现的内容。具体来讲: - -- **创建新进程:**用到我们课上讲过的`fork()`。 -- **运行可执行文件:**使用课上讲的`exec()`系列函数。 - - exec开头的函数有很多,如`execvp`、`execl`等,可以使用man查看不同exec函数的区别及使用方法,最后选取合适的函数。 - - exec中需要指定可执行文件,因为用户的命令给出的只有文件名本身,没有给出文件的所在位置,所以在系统中通常会定义一个环境变量PATH来记录一组文件夹,所有命令对应的可执行文件通常存在于某个文件夹下,找不到则返回不存在。可以使用 `echo $PATH`来查看你现在使用的shell都会去哪些文件夹下查找可执行程序。 -- **结果输出:**命令默认会从STDIN_FILENO (默认值为0) 中读输入,并输出到STDOUT_FILENO (默认值为1) 中。在shell中也会有其他的输入来源和输出目标,如将一个命令的输出作为另一个命令的输入,在这种场合下就需要进行进程间的通信,比如采用我们课上讲过的管道pipe,执行两个命令的进程一个在写端输出内容,一个在读端读取输入。此外,命令还可以设置从文件输入和输出到文件,比如将执行命令程序中的STDOUT_FILENO使用文件描述符覆盖后,命令的执行结果就输出到文件中。 - -## 2.2 Shell内建指令 - -`cd`命令可以用来切换shell的当前目录。但需要指出的是,不同于其他命令(比如`ls`,我们可以在/bin下面找到一个名为`ls`的可执行文件),`cd`命令其实是一个shell内置指令。由于子进程无法修改父进程的参数,所以若不使用内建命令而是fork出一个子进程并且在子进程中exec一个`cd`程序,因为子进程执行结束后会回到了父shell环境,而父shell的路径根本没有被改变,最终无法得到期望的结果。同理,不仅是`cd`,**改变当前shell的参数(如`source`命令、`exit`命令、`kill`命令)基本都是由shell内建命令实现的** 。 - -## 2.3 有关管道的背景知识 - -“一切皆文件”是Unix/Linux的基本哲学之一。普通文件、目录、I/O设备等在Unix/Linux都被当做文件来对待。虽然他们的类型不同,但是linux系统为它们提供了一套统一的操作接口,即文件的open/read/write/close等。当我们调用Linux的系统调用打开一个文件时,系统会返回一个文件描述符,每个文件描述符与一个打开的文件唯一对应。 之后我们可以通过文件描述符来对文件进行操作。管道也是一样,我们可以通过类似文件的read/write操作来对管道进行读写。为便于理解,本次实验使用匿名管道。匿名管道具有以下特点: - -1. 只能用于父子进程等有血缘关系的进程; -2. 写端不关闭,并且不写,读端读完,继续等待,此时阻塞,直到有数据写入才继续(就好比你的C程序在scanf,但你一直什么都不输入,程序会停住); - - 尤其地,假如一条管道有多个写端,那么只有在所有写端都关闭之后(管道的引用数降为0),读端才会解除阻塞状态。 -3. 读端不关闭,并且不读,写端写满管道buffer,此时阻塞,直到管道有空位才继续; -4. 读端关闭,写端在写,那么写进程收到信号SIGPIPE,通常导致进程异常中止; -5. 写端关闭,读端在读,那么读端在读完之后再读会返回0; -6. 匿名管道的通信通常是一次性的,如果需要反复通信,可以使用命名管道。 - -一般来说,匿名管道的使用方法是: - - - -- 首先,父进程调用pipe函数创建一个匿名管道。pipe的函数原型是`int pipe(int pipefd[2])` 。我们传入一个长为2的一维数组`pipefd`,Linux会将`pipefd[0]`设为**读端**的文件描述符,并将`pipefd[1]`设为**写端**的文件描述符。(注:此时管道已被打开,相当于已调用了`open`函数打开文件)但是需要注意:此时管道的读端和写端接在了同一个进程上。如果你此时往`pipefd[1]`里写入数据,这些数据可以从`pipefd[0]`里读出来。不过这种“我传我自己”(原地tp) 通常没什么意义,我们接下来要把管道应用于进程通信。 -- 其次,使用`fork`函数创建一个子进程。`fork`完成之后,数组`pipefd`也会被复制。此时,子进程也拥有了对管道的控制权。若目的是父进程向子进程发送数据,那么父进程就是写端,子进程就是读端。我们应该把父进程的读端关闭,把子进程的写端关闭,进而便于数据从父进程流向子进程。 - - 如果不关闭子进程的写端,子进程会一直等待(参考2.3.2)。 -- 因为匿名管道是单向的,所以如果想实现从子进程向父进程发送数据,就得另开一个管道。 -- 父子进程调用`write`函数和`read`函数写入、读出数据。 - - `write`函数的原型是:`ssize_t write(int fd, const void * buf, size_t count);` - - `read`函数的原型是:`ssize_t read(int fd, void * buf, size_t count);` - - 如果你要向管道里读写数据,那么这里的`fd`就是上面的`pipefd[0]`或`pipefd[1]`。 - - 这两个函数的使用方法在此不多赘述。如有疑问,可以百度。 - -> **注意:如果你的数组越了界,或在read/write的时候count的值比buf的大小更大,则会出现很多奇怪的错误(如段错误(Segmentation Fault)、其他数组的值被改变、输出其他数组的值或一段乱码(注意,烫烫烫是Visual C的特性,Linux下没有烫烫烫)等)。提问前请先排查是否出现了此类问题。** - -## 2.4 输入/输出的重定向 - -> 注:本节描述的是如何将一个程序产生的标准输出转移到其他非标准输出的地方,不特指`>`和`>>`符号。 - -在使用 `|` , `>`, `>>` `<` 时,我们需要将程序输出的内容重定向为文件输出或其他程序的输入。在本次实验中,为方便起见,我们将shell作为重定向的中转站。 - -- 当出现前三种符号时,我们需要把前一指令的标准输出重定向为管道,让父进程(即shell)截获它的标准输出,然后由shell决定将前一指令的输出转发到下一进程、文件或标准输出(即屏幕)。 -- 当出现`|` 和 `<`时,我们需要把后一指令的标准输入重定向为管道,让父进程(即shell)把前一进程被截获的标准输出/指定文件读出的内容通过管道发给后一进程。 - -重定向使用`dup2`系统调用完成。其原型为:`int dup2(int oldfd, int newfd);`。该函数相当于将`newfd`标识符变成`oldfd`的一个拷贝,与`newfd`相关的输入/输出都会重定向到`oldfd`中。如果`newfd`之前已被打开,则先将其关闭。举例:下述程序在屏幕上没有输出,而在文件输出"hello!goodbye!"。屏幕上不会出现"hello!"和"goodbye"。 - -```c -#include -#include -#include -#include -#include -#include - -int main(void) -{ - int fd; - - fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); - - // 此代码运行在dup2之前,本应显示到屏幕,但实际上是暂时放到屏幕输出缓冲区 - printf("hello!"); - - // 程序默认是输出到STDOUT_FILENO即1中,现在我们让1重定向到fd。 - // 也就是将标准输出的内容重定向到文件,因此屏幕输出缓冲区以及后面的printf - // 语句本应输出到屏幕(即标准输出,fd为STDOUT_FILENO)的内容重定向到文件中。 - dup2(fd, 1); - - printf("goodbye!\n"); - return 0; -} -``` - -> 易混淆的地方:向文件/管道内写入数据实际是程序的一个输出过程。 - -## 2.5 一些shell命令的例子和结果分析 - -> 本部分旨在帮助想要进一步理解真实shell行为的同学,希望下述示例及结果分析可以为同学们带来启发。 - -这一节中,我们给出一些shell命令的例子,并分析命令的输出结果,让大家能够更直观的理解shell的运行。**我们并不要求实验中实现的shell行为和真实的shell完全一致,但你自己需要能够清楚地解释自己实现的处理逻辑。**但在实现shell时,可以参考真实shell的这些行为。这些实验都是在bash下测试的,大家也可以自己在Ubuntu等的终端中重复这些实验。在这一节的代码中,每行开头为`$` 的,是输入的shell命令,剩下的行是shell的输出结果。如: - -```shell -$ cd /bin -$ pwd -/bin -``` - -表示在shell中了先运行了`cd /bin` ,然后运行了`pwd` ,第一条命令没有任何输出,第二条命令输出为`/bin` 。`pwd` 命令会显示shell的当前目录,我们先用`cd` 命令进入了`/bin` 目录,所以`pwd` 输出`/bin` 。 - -### 2.5.1 多个子命令 - -由`;` 分隔的多个子命令,和在多行中依次运行这些子命令效果相同。 - -```shell -$ cd /bin ; pwd -/bin -$ echo hello ; echo my ; echo shell -hello -my -shell -``` - -`echo` 会将它的参数打印到标准输出,如`echo hello world` 会输出`hello world` 。这个实验用`;`分隔了三个shell命令,结果和在三行中分别运行这些命令一致。 - -### 2.5.2 管道符 - -这个实验展示了shell处理管道时的行为,相同的命令在管道中行为可能和单独运行时不同。 - -```shell -$ echo hello | echo my | echo shell -shell -``` - -如上,虽然管道中的上一条命令的输出被重定向至下一条命令的输入,但是因为`echo` 命令本身不接受输入,所以前两个`echo` 的结果不会显示。如果实验检查中,你提供了这样的测试样例,是不能证明正确实现了管道的。 - -```shell -$ cd /bin ; pwd -/bin -$ cd /etc | pwd -/bin -``` - -如上,`cd /etc` 并没有改变shell的当前目录,这是因为**管道中的内置命令也是在新的子进程中运行的**,所以不会改变当前进程(shell)的状态。而在**不包含管道的命令中,内置命令在shell父进程中运行,外部命令在子进程中运行**,所以,不包含管道的内置命令能够改变当前进程(shell)的状态。可以用`type` 命令检查命令是否是内部命令。 - -```shell -$ type cd -cd is a shell builtin -$ type cat -cat is hashed (/bin/cat) -``` - -说明`cd` 是内置命令,而`cat` 则是调用`/bin/cat` 的外置命令。接下来我们测试管道中命令的运行顺序。 - -```shell -$ sleep 0.02 | sleep 0.02 | sleep 0.02 | ps | grep sleep -15840 tty1 00:00:00 sleep -$ sleep 0.02 | sleep 0.02 | sleep 0.02 | ps | grep sleep -15845 tty1 00:00:00 sleep -15846 tty1 00:00:00 sleep -15847 tty1 00:00:00 sleep -``` - -可以看到,运行了两次相同的命令,却得到了不同的结果,多次运行该命令,每次输出的sleep行数不同,从0行到3行都有可能(根据电脑速度和核数,可能需要将sleep后面的数字增大或缩小来重复该实验)。这说明**管道中各个命令是并行执行的**,`ps` 命令运行时,前面的`sleep` 命令可能执行结束,也有可能仍在执行。 - -### 2.5.3 重定向 - -**以下命令均在bash运行,如果使用zsh可能会产生不同结果,详见附录1** - -```shell -$ cd /tmp ; mkdir test ; cd test -$ echo hello > a >> b > c -$ ls -a b c -$ cat a -$ cat b -$ cat c -hello -``` - -当一个命令中出现多个输出重定向时,虽然所有文件都会被建立,但是只有最后一个文件会真正被写入命令的输出。 - -```shell -$ cd /tmp ; mkdir test ; cd test -$ echo hello > out | grep hello -$ cat out -hello -``` - -当输出重定向和管道符同时使用时,命令会将结果输出到文件中,而管道中的下一个命令将接收不到任何字符。 - -```shell -$ cd /tmp ; mkdir test ; cd test -$ > out2 echo hello ; cat out2 -hello -``` - -重定向符可以写在命令前。 - -## 2.6 总结:本次实验中shell执行命令的流程 - -- 第一步:打印命令提示符(类似shell ->)。 -- 第二步:把分隔符`;`连接的各条命令分割开。(多命令选做内容) -- 第三步:对于单条命令,把管道符`|`连接的各部分分割开。 -- 第四步:如果命令为单一命令没有管道,先根据命令设置标准输入和标准输出的重定向(重定向选做内容);再检查是否是shell内置指令:是则处理内置指令后进入下一个循环;如果不是,则fork出一个子进程,然后在fork出的子进程中exec运行命令,等待运行结束。(如果不fork直接exec,会怎么样?) -- 第五步:如果只有一个管道,创建一个管道,并将子进程1的**标准输出重定向到管道写端**,然后fork出一个子进程,根据命令重新设置标准输入和标准输出的重定向(重定向选做内容),在子进程中先检查是否为内置指令,是则处理内置指令,否则exec运行命令;子进程2的**标准输入重定向到管道读端**,同子进程1的运行思路。(注:2.4的样例分析中我们得出,管道的多个命令之间,虽然某个命令可能会因为等待前一个进程的输出而阻塞,但整体是没有顺序执行的,即并发执行。所以我们为了让多个内置指令可以并发,需要在fork出子进程后才执行内置指令) -- 第六步:如果有多个管道,参考第三步,n个进程创建n-1个管道,每次将子进程的**标准输出重定向到管道写端**,父进程保存**对应管道的读端**(上一个子进程向管道写入的内容),并使得下一个进程的**标准输入重定向到保存的读端**,直到最后一个进程使用标准输出将结果打印到终端。(多管道选做内容) -- 第七步:根据第二步结果确定是否有剩余命令未执行,如果有,返回第三步执行(多命令选做内容);否则进入下一步。(分隔符多命令和管道连接的多命令实现方式上有什么区别?为什么?) -- 第八步:打印新的命令提示符,进入下一轮循环。 - -## 2.7 任务目标 - -代码填空实现一个shell。这个shell不要求在qemu下运行。功能包括: - -### 必做部分 - -- 实现运行单条命令(非shell内置命令)。 -- 支持一条命令中有单个管道符`|` 。 -- 实现exit, cd, kill三个shell内置指令。 -- 在shell上能显示当前所在的目录。如:{ustc}shell: [/home/ustc/exp2/] > (后面是用户的输入) - -> kill内置命令:涉及SIGTERM、SIGKILL等多种信号量,命令执行方式可以**自定义**,输入的信号量用编号表示即可,例如:kill 1234 9,表示强制终止PID为1234的进程(9为SIGKILL信号量编号);kill 1234,表示以默认的方式(SIGTERM信号量)终止PID为1234的进程。注意(kill 1234 9只是自定义的命令执行方法,bash中kill的执行方式见1.1.6) - -### Makefile部分 - -补全助教提供的 Makefile 文件,实现自动编译和测试。要求包括: - -+ 可以正确编译 `testsh`,`simple_shell`。 -+ 可以使用 `test` 为目标使用 `testsh` 测试 `simple_shell`。 - -### 二选一部分 - -- 实现子命令符`;`和重定向符`>`, `>>`, `<`。 -- 支持一条命令中有多个管道符`|`。如果你确信你的多管道功能可以正常实现单管道功能,代码填空里的单管道可以不做。 - -### 其他说明 - -- 实验文件中提供了一个testsh.c作为本次shell实验的测试脚本,testdata作为测试脚本所需要的文件(不可修改或删除,否则测试样例不能通过),测试脚本中前7条为必做实验部分,8-12为选做1,13-14测试选做2,使用方法为先编译出可执行文件,再 `testsh shell_name`,你可以在testsh中得到本次实验的部分提示。(详细测试样例说明,请查看附录2) -- 在实现cd命令时,不要求实现`cd -`,`cd ~`等方式 -- 我们使用的是Linux系统调用,请不要尝试在Windows下运行自己写的shell程序。那是不可能的。 -- 需要自行设计测试样例以验证你的实现是正确的。如测试单管道时使用`ps aux | wc -l`,与自带的shell输出结果进行比较。 -- 不限制分隔符、管道符、重定向符的符号优先级。你可以参考我们代码框架中实现、提示的优先级。 -- 不要求实现不加空格的重定向符,不要求实现使用'~'表示家目录。 -- 请尽量使用系统调用完成实验,不准用system函数。 -- 我们提供了本次实验使用的系统调用API的范围,请同学们自行查询它们的使用方法。你在实验中可能会用到它们中的一部分。 你也可以使用不属于本表的系统调用。 - - read/write - - open/close - - pipe - - dup/dup2 - - getpid/getcwd - - fork/vfork/clone/wait/exec - - mkdir/rmdir/chdir - - exit/kill - - shutdown/reboot - - chmod/chown -- 如果你想问如何编译、运行自己写的代码,请参考lab1的2.3.17和2.2.3,以及本实验的1.3.2。 -- 得分细则见文档3.1。 - - - - - -
- -# 章节三: 检查要求 - -本部分实验无实验报告。 - -本部分实验共4分。 - -## 3.1 “实现一个Shell”部分检查标准 (4') - -- 支持基本的单条命令运行、支持两条命令间的管道`|`、内建命令(只要求`cd`/`exit`/`kill`),得2分。 -- 选做: - - 支持多条命令间的管道`|`操作,得1分。 - - 支持重定向符`>`、`>>`、`<`和分号`;`,得1分。 -- 你需要流畅地说明你是如何实现实验要求的(即,现场讲解代码),**并且使用make命令展示测试结果**。本部分共1分。若未能完成任何一个功能,则该部分分数无效。 - - - - - -# 附录 - -## 1. Unix不同Shell的区别和联系 - -我们在2.1中提到了Shell的基本原理,其中提到了Unix中不同的Shell,本部分先说明不同shell的区别,最后讲解为什么在重定向中Bash和Zsh的输出并不完全相同 - -### 1.1 基本shell - -Unix系统中主要要两种类型的shell:“Bourne Shell”和“C Shell”。并且这两种类别的shell分别有几种子类别 - -Bourne Shell 的子类别有: - -+ [Bourne Shell (sh)](https://en.wikipedia.org/wiki/Bourne_shell) -+ [KornShell (ksh)](https://en.wikipedia.org/wiki/KornShell) -+ [Bourne Again Shell (Bash)](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) - -C-Type shells的子类别有: - -+ [C Shell (csh)](https://en.wikipedia.org/wiki/C_shell) -+ [TENEX/TOPS C shell (tcsh)](https://en.wikipedia.org/wiki/Tcsh) - -其中Bourne-Again Shell即Bash,是用于取代默认Bourne Shell 的 Unix shell。它在各方面都融合了Korn和C Shell的功能。这意味着用户可以获得 Bourne Shell 的语法兼容性和跨平台性,并在此基础上使用这些其他 shell 的功能进行扩展。 - -> 查看本机上已经安装的shell可使用 `cat /etc/shells`来查看,安装使用和修改默认shell方法自行查询 - -### 1.2 当前热门的Shell - -1. Fish(Friendly Interactive Shell) - - Fish是2005年发布的开源Shell,专门开发为易于使用,并具有开箱即用功能的shell。其风格化的颜色编码对新程序员也很有帮助,因为它突出了语法,使其更易于阅读。Fish Shell 的功能包括制表符补全、语法突出显示、自动补全建议、可搜索的命令历史记录等。(**该Shell的语法与Bash差别较大,有些bash脚本可能无法正常使用,需要修改为特定语法**) - - > Ubuntu安装方法: - > - > ```sh - > $ sudo apt install fish - > $ fish - > ``` - -2. Zsh(Z shell) - - Z-shell 被设计为现代创新和交互式外壳。Zsh 在其他 Unix 和开源 Linux shell (包括 tcsh、ksh、Bash 等)之上提供了一组独特的功能。这个开源 shell 易于使用、可定制,提供拼写检查、自动完成和其他生产力功能。 - - > Ubuntu安装方法: - > - > ```sh - > $ sudo apt install zsh - > $ zsh - > ``` - -### 1.3 重定向中zsh和bash的表现不同的原因 - - 在文档2.5.3节中阐述了shell中重定向的使用方法,如果使用bash,输出结果如下所示 - -```shell -$ cd /tmp ; mkdir test ; cd test -$ echo hello > a >> b > c -$ ls -a b c -$ cat a -$ cat b -$ cat c -hello -``` - - 但是如果使用zsh,输出结果则如下所示 - -```shell -$ cd /tmp ; mkdir test ; cd test -$ echo hello > a >> b > c -$ ls -a b c -$ cat a -hello -$ cat b -hello -$ cat c -hello -``` - - 首先,先看一个简单的例子 - -```shell -$ echo hello world > a -$ cat a -hello world -``` - - 该例子即使用 `>`重定向符号将echo的标准输出重定向到文件a,我们可以查看 `/proc/PID_OF_PROCESS/fd`来查看具体情况,由于echo命令执行速度较快,使用如下例子 - -```shell - $ cat > a - - # Open another Shell - $ ls -l /proc/$(pidof cat)/fd - total 0 - lrwx------ 1 ridx ridx 64 Mar 28 15:31 0 -> /dev/pts/0 - l-wx------ 1 ridx ridx 64 Mar 28 15:31 1 -> ~/a - lrwx------ 1 ridx ridx 64 Mar 28 15:31 2 -> /dev/pts/0 -``` - - cat等待用户输入后重定向给文件a直到Ctrl-D结束,可以看到cat的标准输出已经指向a。0 是标准输入(即用户输入端),1 是标准输出(即正常情况的输出端),2 是错误输出(即异常情况的输出端),想要重定向错误输出端,可以使用 `2>`,本实验不要求实现该方法。 - -```shell - #Use Bash Shell - $ cat > a >> b >c - - # Open another Shell - $ ls -l /proc/$(pidof cat)/fd - total 0 - lrwx------ 1 ridx ridx 64 Mar 28 15:44 0 -> /dev/pts/0 - l-wx------ 1 ridx ridx 64 Mar 28 15:44 1 -> ~/c - lrwx------ 1 ridx ridx 64 Mar 28 15:44 2 -> /dev/pts/0 -``` - - 回到最开始的重定向例子中,由于例子中echo命令过快,我们使用cat来代替.使用bash执行命令 `cat > a >> b > c`。可以看到cat标准输出只有c,所以cat程序只往文件c中写入数据 - -```shell - #Use Zsh - $ cat > a >> b >c - - # Open another Shell - $ pstree -p | grep cat - |-sh(3365094)---node(3365156)-+-node(3365181)-+-zsh(3416851)---cat(3416921)---zsh(3416922) - $ ls -l /proc/3416921/fd - total 0 - lrwx------ 1 ridx ridx 64 Mar 28 15:48 0 -> /dev/pts/0 - l-wx------ 1 ridx ridx 64 Mar 28 15:48 1 -> 'pipe:[457492940]' - lrwx------ 1 ridx ridx 64 Mar 28 15:48 2 -> /dev/pts/0 - $ ls -l /proc/3416922/fd - total 0 - l-wx------ 1 ridx ridx 64 Mar 28 15:50 11 -> ~/a - l-wx------ 1 ridx ridx 64 Mar 28 15:50 16 -> ~/b - l-wx------ 1 ridx ridx 64 Mar 28 15:50 18 -> ~/c - lr-x------ 1 ridx ridx 64 Mar 28 15:50 17 -> 'pipe:[457492940]' -``` - - 可以看到 cat 的标准输出是重定向到管道,管道对面是 zsh 进程,然后 zsh 打开了那三个文件。实际将内容写入文件的是 zsh,而不是 cat。所以在zsh中 `echo hello > a >> b > c`,三个文件均更新. - -> 在fish中,重定向的表现和bash相同,不过fish无法使用$(...)语法,感兴趣可以直接使用 `ls -l /proc/(pidof cat)/fd`或者通过pstree查看cat进程号手动验证 - -## 2. shell实验测试样例详细说明 - -### 2.1编译运行 - -testsh.c为标准c程序代码,编译测试请参考请参考本实验第一部分的2.3.17和2.2.3。测试程序接受一个字符串参数 `shell_name`,请先编译出自己的shell程序,然后编译testsh,运行命令请使用 `testsh shell_name`。(当然我们鼓励使用makefile或者sh脚本来编译运行测试文件) - -如果同时完成选做1和选做2,运行结果如下 - -```sh -kill: PASS -cd: PASS -current path: PASS -simple echo: PASS -simple grep: PASS -two commands: PASS -simple pipe: PASS -more test: PASS -output redirection(use >): PASS -output redirection(use >>): PASS -input redirection: PASS -both redirections: PASS -pipe and redirection: PASS -multipipe: PASS -two commands with pipes: PASS -passed all tests -``` - -### 2.2testsh详细说明 - -测试程序中,每一个测试样例为一个函数,下面表格为所有的测试样例。testsh使用的前提是你的shell先完成**exit**命令的功能 - -| 函数名 | 测试样例/功能 | 命令 | -| ------ | -------------------------- | ------------------------------------------------------------ | -| t0 | kill | `kill [pid]` | -| t1 | cd | `cd /sys/class/net` | -| t2 | current path | 查看shell是否打印了当前目录 | -| t3 | simple echo | `echo hello goodbye` | -| t4 | simple grep | `grep professor testdata` | -| t5 | two commands | `echo x\necho goodbye\n` | -| t6 | simple pipe | `cat file | cat` | -| t7 | more test | `ps | grep ps` | -| t8 | output redirection(use >) | `echo data > file` | -| t9 | output redirection(use >>) | `echo data > file;echo data >> file` | -| t10 | input redirection | `cat < file` | -| t11 | both redirections | `grep USTC < testdata > testsh.out` | -| t12 | pipe and redirections | `grep system < testdata | wc > testsh.out` | -| t13 | multipipe | `cat testdata | grep system | wc -l` | -| t14 | two commands with pipes | `echo hello 2025 | grep hello ; echo nihao 2025 | grep 2025` | - -### 2.3可能出现的问题 - -1. 运行该程序时,可能会出现 `unexpected wait() return`的报错,如果出现该报错,请尝试再次运行程序 -2. 在测试 `simple pipe`功能时可能会有卡顿,属于正常情况,请耐心等待 -3. 本程序必须要求你的simple_shell具有exit功能,否则无法正常工作 -4. 如果你的ßshell无关信息输出过多,可能会有 `testsh: saw expected output, but too much else as well`出现,请调整代码 -5. 如果shell测试失败,可能会产生临时乱码文件,属于正常现象,测试完毕后把临时文件删除即可 -6. 本程序如果出现各种问题,请及时在QQ群/文档/ 提供问题描述或建议 - -### 3.C语言程序编译流程 - -1. 预处理(Preprocessing) - 预处理是编译过程的第一步,主要使用预处理器来处理源代码文件。在预处理阶段,源代码中的宏定义、条件编译指令以及头文件的包含等将被展开和处理,生成经过处理的中间代码。预处理的结果是一个经过宏定义替换、头文件包含等操作后的中间代码文件。 -2. 编译(Compiling) - 编译是将预处理之后的中间代码翻译成汇编代码的过程。编译器会将中间代码文件转化为与特定平台相关的汇编语言代码文件。在编译阶段,进行语法分析、语义分析等操作,生成与特定平台相关的汇编代码。 -3. 汇编(Assembling) - 汇编是将汇编语言代码翻译为机器代码的过程。汇编器将汇编代码转换为目标文件,其中包含了与特定平台相关的机器指令和数据。目标文件中包含了汇编语言代码转换而成的机器码。 -4. 链接(Linking) - 链接是将各个目标文件(包括自己编写的文件和库文件)合并为一个可执行文件的过程。链接器将目标文件中的符号解析为地址,并将它们连接到最终的可执行文件中。链接过程包括符号解析、地址重定位等步骤,确保各个模块能够正确地相互调用。 -5. 可执行文件(Executable) - 最终的输出是一个可执行文件,其中包含了所有必要的指令和数据,可以在特定平台上运行。这个可执行文件经过编译链接过程,包含了源代码的所有功能和逻辑,可以被操作系统加载并执行。 +# 实验一:Linux基础与系统调用——Shell工作原理(part2) + +## 实验目的 + +- 学习如何使用Linux系统调用:实现一个简单的shell + - 学习如何编写makefile:实现一个简单的makefile来测试shell + +## 实验环境 + +- 虚拟机:VMware +- 操作系统:Ubuntu 24.04.2 LTS + +> 系统的安装形式可以自由选择,双系统,虚拟机都可以,系统版本则推荐使用本文档所用版本。注意:由于Linux各种发行版非常庞杂且存在较大差异,因此本试验在其他Linux发行版可能会存在兼容性问题。如果想使用其他环境(如vlab)或系统(如Arch、WSL等),请根据自己的系统**自行**调整实验步骤以及具体指令,达成实验目标即可,但其中出现的兼容性问题助教**无法**保证能够一定解决。 + + +## 实验时间安排 + +> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 + + +- 3.28 晚实验课,讲解实验第一部分、第二部分,检查实验 +- 4.4 清明节放假 +- 4.11 晚实验课,讲解实验第三部分,检查实验 +- 4.18 晚实验课,检查实验 +- 4.25 晚及之后实验课,补检查实验 + +> 补检查分数照常给分,但会**记录**此次检查未按时完成,此记录在最后综合分数时作为一种参考(即:最终分数可能会低于当前分数)。 + +检查时间、地点:周五晚18: 30~22: 00,电三楼406/408。 + +## 如何提问 + +- 请同学们先阅读《提问的智慧》。[原文链接](https://lug.ustc.edu.cn/wiki/doc/smart-questions/) +- 提问前,请先**阅读报错信息**、查询在线文档,或百度。[在线文档链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR); +- 在向助教提问时,请详细描述问题,并提供相关指令及相关问题的报错截图; +- 在QQ群内提问时,如遇到长时未收到回复的情况,可能是由于消息太多可能会被刷掉,因此建议在在线文档上提问; +- 如果助教的回复成功地帮你解决了问题,请回复“问题已解决”,并将问题及解答更新到在线文档。这有助于他人解决同样的问题。 + +## 为什么要做这个实验 + +- 为什么要学会使用Linux? + - Linux的安全性、稳定性更好,性能也更好,配置也更灵活方便,所以常用于服务器和开发环境。实验室和公司的服务器一般也都用Linux; + - Linux是开源系统,代码修改方便,很多学术成果都基于Linux完成; + - Windows是闭源系统,代码无法修改,无法进行后续实验。 +- 为什么要使用虚拟机? + - 虚拟机对你的电脑影响最低。双系统若配置不正确,可能导致无法进入Windows,而虚拟机自带的快照功能也可以解决部分误操作带来的问题。 + - 本实验并不禁止其他环境的使用,但考虑其他环境(如WSL)变数太大,比如可能存在兼容性或者其他配置问题,会耽误同学们大量时间浪费在实验内容以外的琐事,因此建议各位同学尽量保持与本试验一致或类似的环境。 +- 为什么要学会编译Linux内核? + - 这是后续实验的基础。在后续实验中,我们会让大家通过阅读Linux源码、修改Linux源码、编写模块等方式理解一个真实的操作系统是怎么工作的。 + +## 其他友情提示 + +- **合理安排时间,强烈不建议在ddl前赶实验**。 +- 本课程的实验实践性很强,请各位大胆尝试,适当变通,能完成实验任务即可。 +- pdf上文本的复制有时候会丢失或者增加不必要的空格,有时候还会增加不必要的回车,有些指令一行写不下分成两行了,一些同学就漏了第二行。如果出了bug,建议各位先仔细确认自己输入的指令是否正确。要**逐字符**比对。每次输完指令之后,请观察一下指令的输出,检查一下这个输出是不是报错。**请在复制文档上的指令之前先理解一下指令的含义。** 我们在检查实验时会抽查提问指令的含义。 +- 如果你想问“为什么PDF的复制容易出现问题”,请参考[此文章](https://type.cyhsu.xyz/2018/09/understanding-pdf-the-digitalized-paper/)。 +- 如果同学们遇到了问题,请先查询在线文档。在线文档地址:[链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) + +
+ +# 章节一 续·Linux、Shell指令教学 及 Makefile 编写和使用 + +## 1.0 环境准备 + +本次实验二三部分需要用到较多依赖工具,请使用包管理器安装以下包: + +- qemu-system-x86 (Ubuntu 20.04以上) 或 qemu (Ubuntu 其他版本) +- git +- build-essential (里面包含了make/gcc/g++等,省去了单独安装的麻烦) +- libelf-dev +- xz-utils +- libssl-dev +- bc +- libncurses5-dev +- libncursesw5-dev + +为了方便同学们复制粘贴,将所有的依赖放在这里:`qemu-system-x86 git build-essential libelf-dev xz-utils libssl-dev bc libncurses5-dev libncursesw5-dev` + +## 1.1 Linux指令 + +### 1.1.1 echo + +输出一个字符串。用法:`echo string`。 + +同样也可以输出一个文件的内容,如`echo file`将文件file的内容以纯文本形式读并输出。 + +经常在Shell脚本中使用该指令,以打印指定信息。 + +### 1.1.2 top + +ps 命令可以一次性给出当前系统中进程状态,但使用此方式得到的信息缺乏时效性,并且,如果管理员需要实时监控进程运行情况,就必须不停地执行 ps 命令,这显然是缺乏效率的。 + +为此,Linux 提供了top命令。top 命令动态地持续监听进程地运行状态,与此同时,该命令还提供了一个交互界面,用户可以根据需要,人性化地定制自己的输出,进而更清楚地了进程的运行状态。直接输入`top`即可使用。 + +| 参数 | 含义 | +| ---------- | ------------------------------------------------------------ | +| -d 秒数 | 指定 top 命令每隔几秒更新。默认是 3 秒。 | +| -b | 使用批处理模式输出。一般和`-n` 选项合用,用于把 top 命令重定向到文件中。 | +| -n 次数 | 指定 top 命令执行的次数。一般和`-b` 选项合用。 | +| -p 进程PID | 仅查看指定 ID 的进程。 | +| -u 用户名 | 只监听某个用户的进程。 | + +在 top 命令的显示窗口中,还可以使用如下按键,进行以下交互操作: + +| 按键 (注意大小写) | 含义 | +| ----------------- | ------------------------------------------------------------ | +| ? 或 h | 显示交互模式的帮助。 | +| P | 按照 CPU 的使用率排序,默认就是此选项。 | +| M | 按照内存的使用率排序。 | +| N | 按照 PID 排序。 | +| k | 按照 PID 给予某个进程一个信号。一般用于中止某个进程,信号 9 是强制中止的信号。 | +| q | 退出 top 命令。 | + +### 1.1.3 sleep + +`sleep n`可以使当前终端暂停n秒。常见于shell脚本中,用于实现两条指令间的等待。 + +### 1.1.4 grep + +筛选并高亮指定字符串。尝试在终端下运行`grep a`,它会等待用户输入。若输入一行不带a的字符串并按回车,它什么都不会输出。若输入多行字符串,其中某些行带a,它会筛选出带a的行,并将该行的a高亮后输出。 + +### 1.1.5 wc + +英文全拼:wordcount + +常见用法:`wc [-lw] [filename]`,用于统计字数。 + +| 参数 | 含义 | +| -------- | ------------------------------------------------------------ | +| -l | 统计行数。 | +| -w | 统计字数。 | +| filename | 统计指定文件的行数/字数。若不指定,会从标准输入(C/C++的stdin/scanf/cin)处读取数据。 | + +### 1.1.6 kill + +发送指定的信号到相应进程。不指定信号将发送`SIGTERM(15)`终止指定进程。如果无法终止该程序可用`“-KILL” `参数,其发送的信号为`SIGKILL(9) `,将强制结束进程。使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。 + +常见用法:`kill [信号] [PID]` + +## 1.2 Shell指令 + +### 1.2.1 管道符 | + +问题:我们想统计Linux下进程的数量,应该如何解决? + +一个很麻烦的解决方法是:先`ps aux`输出所有进程的列表,然后复制它的输出,执行`wc -l`,将之前`ps`输出的内容粘贴到标准输入,传递给`wc`以统计`ps`输出的行数,即进程数量。有没有方法可以免去复制粘贴的麻烦? + +管道符`|`可以将前面一个命令的标准输出(对应C/C++的stdout/printf/cout)传递给下一个命令,作为它的**标准输入**(对应C/C++的stdin/scanf/cin)。 + +- 例1:在终端中运行`ps aux | wc -l`可以显示所有进程的数量。 +- 例2:先打开Linux下的firefox,然后在终端运行`ps aux | grep firefox`可以显示所有进程名含firefox的进程。 +- 例3:接例2,在终端运行`ps aux | grep firefox | wc -l`可以显示所有进程名含firefox的进程的数量。 + +> 思考:`wc -l`的作用是统计输出的行数。所以`ps aux | wc -l`统计出的数字真的是进程数量吗? + +### 1.2.2 重定向符 >/>>/< + +重定向符 `>` , `>>`:它可以把前面一个命令的标准输出保存到文件内。如果文件不存在,则会创建文件。`>` 表示覆盖文件,`>>`表示追加文件。 + +- 举例:`ps -aux > ps.txt`可以把当前运行的所有进程信息输出到ps.txt。ps.txt的原有内容会被覆盖。 +- 举例:`ps -aux >> ps.txt`可以把当前运行的所有进程信息追加写到ps.txt。 + +重定向符 `<`:可以将`<`前的指令的标准输入重定向为`<`后文件的内容。 + +- 举例:`wc -l < ps.txt`可以把ps.txt中记录的信息作为命令的输入,即统计ps.txt中的行数。 + +### 1.2.3 分隔符 ; + +子命令:每行命令可能由若干个子命令组成,各子命令由`;`分隔,这些子命令会被按序依次执行。 + +如:`ps -a; pwd; ls -a` 表示:先打印当前用户运行的进程,然后打印当前shell所在的目录,最后显示当前目录下所有文件。 + +## 1.3 Shell 脚本 + +考虑到往届在检查实验时,大多数时间都浪费在了敲指令上,希望大家可以学会如何编写Shell脚本。 + +### 1.3.1 Shell脚本的编写 + +Shell脚本类似于Windows的.bat批处理文件。一个最简单的Shell脚本长这样: + +```shell +#!/bin/bash +第一条命令 +第二条命令 +第三条命令,以此类推 +``` + +其中,第一行的`#!/bin/bash`是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。Ubuntu下默认的shell是bash。脚本一般命名为`xxx.sh`。 + +### 1.3.2 脚本的运行 + +运行Shell脚本有两种方式。 + +1. 通过执行`sh xxx.sh`来直接调用解释器运行脚本。其中,sh就是我们的Shell。这种方式运行的脚本,不需要在第一行指定解释器信息。 +2. 将该脚本视为可执行程序。首先,保存脚本之后,要通过`chmod +x xxx.sh`给脚本赋予执行权限,然后直接`./xxx.sh`运行脚本。 + +> 注意:脚本的执行和在终端下执行这些语句是完全一致的,依然需要注意绝对路径和相对路径的问题。 + +举例:首先编译abc.cpp并输出一个名为aabbcc的二进制可执行文件,然后执行aabbcc,最后删掉aabbcc,上述操作可以使用如下脚本来实现: + +```shell +#!/bin/bash +gcc -o aabbcc abc.cpp +./aabbcc +rm aabbcc +``` + +之后的实验会介绍更多Shell脚本的语法。 + +## 1.4 Makefile 的编写 + +> 你可以在阅读完本文 [章节二 实现一个Linux Shell](#章节二 实现一个Linux Shell) 后,再阅读这一节。 + +在本实验第一部分 2.3.17 (编译指令)中,我们提到了 `make`,在编译内核和 busybox 时,我们也使用了`make`、`make install`等命令。为什么用一个`make`命令就能编译好庞大的内核呢?它又是怎么工作的呢?在这节,我们就将学习如何使用 Make 工具,以及如何编写 Makefile ,使得我们的程序可以通过`make` 命令编译。 + +### 1.4.1 Make 介绍 + +什么是 Make 呢?Make 是一个用于自动化编译和构建的工具。在工程需要编译的文件较多时,每次都手输编译命令较为繁琐 。而 Make工具就能帮助我们将各个步骤的编译自动化,省去了每次输入编译命令的时间,也方便了将我们写好的程序分发给他人(其他人只要使用`make`命令就好啦)。 + +什么时候我们要使用 Make 呢?简略地说,当我们的项目编译过程比较复杂,诸如涉及文件较多、依赖关系较为复杂,或者需要生成多个不同的文件等时,我们就倾向于使用 Make,把杂乱的编译统一整理起来。Make 还能优化编译速度,当源代码的某一部分发生变化时,`Make`只会重新编译那些直接或间接依赖于已更改部分的代码,而不是重新编译整个程序(在多次编译内核时,你会发现第二次及以后编译会比第一次快得多,就是这个原因 )。 + +我们如何使用 Make 呢?Make 是通过读取名为`Makefile`的文件来工作的,这个文件包含了关于如何构建(编译)程序的规则。我们将在下一节介绍 Makefile 的编写方式。 + +总的来说,Make 是一个强大的工具,它可以帮助我们管理和自动化编译过程,从而提高效率,减少错误,并确保结果的一致性。 + +### 1.4.2 Makefile + +> 该部分内容只包含了本次实验所必需的部分,你也可以参考[跟我一起写Makefile - github.io](https://seisman.github.io/how-to-write-makefile/index.html) 或 [跟我一起写Makefile - Ubuntu中文](https://wiki.ubuntu.org.cn/index.php?title=跟我一起写Makefile:MakeFile介绍&variant=zh-cn) 来了解更多内容。 + +#### 1.4.2.1 使用脚本自动化编译和其局限性 + +在1.3中,我们介绍了shell脚本基本用法,那么编译项目当然也可以使用shell脚本实现。假设我们有一个 C 文件`hello.c`,我们要将其编译为名为`hello`可执行文件,(正如 Lab1 的 2.3.17 节介绍的那样)可以编写以下脚本: + +```bash +# make.sh +gcc -o hello hello.c +``` + +这个命令实际上读取了文件`hello.c`,生成可执行文件`hello`。从编译过程的角度考虑,我们可以说:生成`hello` 的过程,是依赖于文件`hello.c`的。或者说,`hello`是生成的**目标**,而`hello.c`是这个目标的**依赖项**。 + +很多时候,我们的项目不止一个源文件,要生成的目标文件也不止一个,例如我们可能有一个`hello.h`文件存放通用的函数,而希望把`hello1.c`和`hello2.c`(都 include 了`hello.h`)分别编译为`hello1`和`hello2`,我们可能会写出这样一个脚本: + +```bash +# make.sh +gcc -o hello1 hello1.c +gcc -o hello2 hello2.c +``` + +虽然我们的命令里没有出现`hello.h`,但它被包含在两个源文件里,因此`hello1`实际依赖于`hello1.c`和`hello.h`,而`hello2`则依赖于`hello2.c`和`hello.h`。 + +一切似乎都还好,似乎用脚本也能完成自动化编译的任务。但实际上,我们的脚本存在一个关键的问题,那就是运行它时,`hello1`和`hello2`一定会被同时重新编译。 这看起来似乎没什么,但假设我们的`hello1.c`和`hello2.c`都很复杂,每一个都需要编译很长时间。而假设我们某次只修改了`hello1.c`,而没有修改`hello.h`和`hello2.c`,这时候我们用这个脚本,它仍然会重新编译`hello1`,浪费了编译时间。 + +那我们能否拆开成两个脚本呢?大型项目里,要编译的文件可能有千千万万,我们不可能为每个文件(甚至每种组合)都写一个脚本,于是,只靠脚本管理编译过程就显得有些力不从心了。 + +实际上,当一个大项目编译时,可能会出现 `A` 依赖于 `B`,`B`又依赖于`C`的情况。例如,多文件的项目中,为了避免重复编译,通常会把`.c`文件先编译为`.o`文件,再进行链接生成最终的可执行文件或库文件 。这种情况,Make的作用就更加关键了。(本次实验不涉及这样的多级依赖,你可以参考[附录3](#3.C语言程序编译流程)和节开头的参考链接了解更多 C编译相关知识。) + +#### 1.3.2.2 Makefile 基础规则 + +既然脚本没法解决所有问题,那`make`又是怎么解决的呢?正如前文所说,Make 是通过读取名为`Makefile`的文件来工作的。而`Makefile`里实际就记录了整个项目需要生成的所有文件的依赖关系,和生成规则。 + +我们先粗略的看一下 Makefile 的结构,Makefile 由以下格式的块构成,每个块代表一个或多个目标的生成方式和依赖。 + +```makefile +[目标] ... : [依赖项] ... + [生成命令] + ... + ... +``` + ++ `[目标]` + + 代表一个要生成的目标文件名,有时候也可以只是一个标签不对应真实文件(见后续介绍). + ++ `[依赖项]` + + 生成`[目标]`的依赖项,相当于编译所需要的“输入文件”。 + ++ `[生成命令]` + + 生成`[目标]`所需要运行的 Shell 指令。 + +例如,对上一节提到的`hello.c`编译为`hello`的依赖关系,可以用以下`Makefile`来描述(`#`开头的行是 Makefile 里的注释): + +```makefile +# hello 依赖于 hello.c ,通过 gcc -o hello hello.c 生成。 +hello: hello.c + gcc -o hello hello.c +``` + +当我们运行`make`命令时,make会尝试读取当前目录名为 `Makefile`的文件,并按其中描述运行命令。(显然,上面的例子中,它会运行`gcc -o hello hello.c`。) + +一个 `Makefile` 里可以有多个目标,一个目标可以有多个依赖项(也可以没有依赖),如之前`hello1`和`hello2`编译的依赖关系可表示为: + +```makefile +hello1: hello1.c hello.h + gcc -o hello1 hello1.c + +hello2: hello2.c hello.h + gcc -o hello2 hello2.c +``` + +当我们运行`make`时,Make 会尝试生成 `Makefile` 中第一个目标,即`hello1`。如果要生成其它目标,可以在`make`后面添加目标名,如`make hello2`。`make`运行时,会自动检查生成目标是否已经存在,如果存在,会自动判断目标和依赖项的修改时间,如果目标生成之后,依赖项有过修改,`make`会重新生成目标,否则,说明上次生成后,依赖项没有被修改过,这意味着没必要重新编译,`make`将什么都不做。这样,Make 就节省了宝贵的编译时间。 (在多级依赖的场合,如果依赖项不存在,`make`会尝试首先生成依赖项,这也非常符合直觉。) + +#### 1.3.2.3 伪目标和清空目录的规则 + +编译时,除了生成某些东西,有时我们还想做些不会生成对应文件的任务。例如,我们有时可能想删除所有之前编译的结果(如`hello1`和`hello2`)。我们可以这样写: + +```makefile +clean: + rm -rf hello1 hello2 +``` + +这个规则中`[目标]`为clean,`[依赖项]`为空,`[生成命令]`为`rm -f ...`。但是,我们并不打算实际生成一个名为`clean`的文件。这时候,我们称类似`clean`这样的目标为“**伪目标**”。 “伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法通过它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效(以`make clean`来使用该目标)。 + +为了避免和文件重名导致的错误(例如我们目录下真的有名为`clean`的文件 ),我们可以使用一个特殊的标记`.PHONY`来显式地指明一个目标是“伪目标”,向 Make 说明,不管是否有这个文件,这个目标就是“伪目标”。 + +```makefile +.PHONY : clean +``` + +只要有这个声明,不管是否有`clean`文件,以及文件修改时间如何,`make clean`都会执行对应的命令。于是整个过程可以这样写: + +```makefile +.PHONY : clean +clean: + rm -rf hello1 hello2 +``` + +`.PHONY`后可以有多个目标名,也可以写在伪目标的后面。例如,我们可能想用一个命令`make`同时生成`hello1`和`hello2`,可以写一个伪目标`all`,放在`makefile`的开头: + +```makefile +all: hello1 hello2 + +hello1: hello1.c hello.h + gcc -o hello1 hello1.c + +hello2: hello2.c hello.h + gcc -o hello2 hello2.c + +.PHONY : all clean + +clean: + rm -rf hello1 hello2 +``` + +注意这实际上是个多级依赖,`all`依赖于`hello1`和`hello2`,所以这两个依赖不存在时,`make`会尝试生成它们。 + +在我们的实验中,需要使用伪目标`test`来自动运行测试,方法也类似如此,大家可以自行尝试。 + +#### 1.3.2.4 Makefile的文件名 + +默认情况下,make命令会在当前目录下按顺序寻找名为 `GNUmakefile` 、 `makefile` 和 `Makefile` 的文件。在这三个文件名中,最好使用 `Makefile` 这个文件名,因为这个文件名在排序上靠近其它比较重要的文件,比如 `README`。最好不要用 `GNUmakefile`,因为这个文件名只能由GNU `make` ,其它版本的 `make` 无法识别,但是基本上来说,大多数的 `make` 都支持 `makefile` 和 `Makefile` 这两种默认文件名。 + + + +
+ +# 章节二 实现一个Linux Shell + +> 注意: +> +> - 本部分的主要目的是锻炼大家**使用** Linux系统调用编写程序的能力,不会涉及编写一个完整的系统调用。 +> - 本部分主要是代码填空,费力的部分已由助教完成,代码量不大,大家不必恐慌。**需要大家完成的代码已经用注释`TODO:`标记,可以通过搜索轻松找到**,使用支持TODO高亮编辑器(如vscode装TODO highlight插件)的同学也可以通过高亮找到要添加内容的地方。 + +## 2.1 Shell的基本原理 + +我们在各种操作系统中会遇到各式各样的用于输入命令、基于文字与系统交互的终端,Windows下的cmd和powershell、Unix下的sh、bash和zsh等等,这些都是不同的shell,我们可以看到它们的共同点——基于“命令” (command) 与系统交互,完成相应的工作。 + +Shell处理命令的方式很好理解,每条输入的“命令”其实都对应着一个可执行文件,例如我们输入`top`时,**shell程序会创建一个新进程并在新进程中运行可执行文件。具体而言,shell程序在特定的文件夹中搜索名为"top"的可执行文件,搜索到之后运行该可执行文件,并将进程的输出定向到指定的位置 (如终端设备) **,这一过程也是我们本次实验要实现的内容。具体来讲: + +- **创建新进程:**用到我们课上讲过的`fork()`。 +- **运行可执行文件:**使用课上讲的`exec()`系列函数。 + - exec开头的函数有很多,如`execvp`、`execl`等,可以使用man查看不同exec函数的区别及使用方法,最后选取合适的函数。 + - exec中需要指定可执行文件,因为用户的命令给出的只有文件名本身,没有给出文件的所在位置,所以在系统中通常会定义一个环境变量PATH来记录一组文件夹,所有命令对应的可执行文件通常存在于某个文件夹下,找不到则返回不存在。可以使用 `echo $PATH`来查看你现在使用的shell都会去哪些文件夹下查找可执行程序。 +- **结果输出:**命令默认会从STDIN_FILENO (默认值为0) 中读输入,并输出到STDOUT_FILENO (默认值为1) 中。在shell中也会有其他的输入来源和输出目标,如将一个命令的输出作为另一个命令的输入,在这种场合下就需要进行进程间的通信,比如采用我们课上讲过的管道pipe,执行两个命令的进程一个在写端输出内容,一个在读端读取输入。此外,命令还可以设置从文件输入和输出到文件,比如将执行命令程序中的STDOUT_FILENO使用文件描述符覆盖后,命令的执行结果就输出到文件中。 + +## 2.2 Shell内建指令 + +`cd`命令可以用来切换shell的当前目录。但需要指出的是,不同于其他命令(比如`ls`,我们可以在/bin下面找到一个名为`ls`的可执行文件),`cd`命令其实是一个shell内置指令。由于子进程无法修改父进程的参数,所以若不使用内建命令而是fork出一个子进程并且在子进程中exec一个`cd`程序,因为子进程执行结束后会回到了父shell环境,而父shell的路径根本没有被改变,最终无法得到期望的结果。同理,不仅是`cd`,**改变当前shell的参数(如`source`命令、`exit`命令、`kill`命令)基本都是由shell内建命令实现的** 。 + +## 2.3 有关管道的背景知识 + +“一切皆文件”是Unix/Linux的基本哲学之一。普通文件、目录、I/O设备等在Unix/Linux都被当做文件来对待。虽然他们的类型不同,但是linux系统为它们提供了一套统一的操作接口,即文件的open/read/write/close等。当我们调用Linux的系统调用打开一个文件时,系统会返回一个文件描述符,每个文件描述符与一个打开的文件唯一对应。 之后我们可以通过文件描述符来对文件进行操作。管道也是一样,我们可以通过类似文件的read/write操作来对管道进行读写。为便于理解,本次实验使用匿名管道。匿名管道具有以下特点: + +1. 只能用于父子进程等有血缘关系的进程; +2. 写端不关闭,并且不写,读端读完,继续等待,此时阻塞,直到有数据写入才继续(就好比你的C程序在scanf,但你一直什么都不输入,程序会停住); + - 尤其地,假如一条管道有多个写端,那么只有在所有写端都关闭之后(管道的引用数降为0),读端才会解除阻塞状态。 +3. 读端不关闭,并且不读,写端写满管道buffer,此时阻塞,直到管道有空位才继续; +4. 读端关闭,写端在写,那么写进程收到信号SIGPIPE,通常导致进程异常中止; +5. 写端关闭,读端在读,那么读端在读完之后再读会返回0; +6. 匿名管道的通信通常是一次性的,如果需要反复通信,可以使用命名管道。 + +一般来说,匿名管道的使用方法是: + + + +- 首先,父进程调用pipe函数创建一个匿名管道。pipe的函数原型是`int pipe(int pipefd[2])` 。我们传入一个长为2的一维数组`pipefd`,Linux会将`pipefd[0]`设为**读端**的文件描述符,并将`pipefd[1]`设为**写端**的文件描述符。(注:此时管道已被打开,相当于已调用了`open`函数打开文件)但是需要注意:此时管道的读端和写端接在了同一个进程上。如果你此时往`pipefd[1]`里写入数据,这些数据可以从`pipefd[0]`里读出来。不过这种“我传我自己”(原地tp) 通常没什么意义,我们接下来要把管道应用于进程通信。 +- 其次,使用`fork`函数创建一个子进程。`fork`完成之后,数组`pipefd`也会被复制。此时,子进程也拥有了对管道的控制权。若目的是父进程向子进程发送数据,那么父进程就是写端,子进程就是读端。我们应该把父进程的读端关闭,把子进程的写端关闭,进而便于数据从父进程流向子进程。 + - 如果不关闭子进程的写端,子进程会一直等待(参考2.3.2)。 +- 因为匿名管道是单向的,所以如果想实现从子进程向父进程发送数据,就得另开一个管道。 +- 父子进程调用`write`函数和`read`函数写入、读出数据。 + - `write`函数的原型是:`ssize_t write(int fd, const void * buf, size_t count);` + - `read`函数的原型是:`ssize_t read(int fd, void * buf, size_t count);` + - 如果你要向管道里读写数据,那么这里的`fd`就是上面的`pipefd[0]`或`pipefd[1]`。 + - 这两个函数的使用方法在此不多赘述。如有疑问,可以百度。 + +> **注意:如果你的数组越了界,或在read/write的时候count的值比buf的大小更大,则会出现很多奇怪的错误(如段错误(Segmentation Fault)、其他数组的值被改变、输出其他数组的值或一段乱码(注意,烫烫烫是Visual C的特性,Linux下没有烫烫烫)等)。提问前请先排查是否出现了此类问题。** + +## 2.4 输入/输出的重定向 + +> 注:本节描述的是如何将一个程序产生的标准输出转移到其他非标准输出的地方,不特指`>`和`>>`符号。 + +在使用 `|` , `>`, `>>` `<` 时,我们需要将程序输出的内容重定向为文件输出或其他程序的输入。在本次实验中,为方便起见,我们将shell作为重定向的中转站。 + +- 当出现前三种符号时,我们需要把前一指令的标准输出重定向为管道,让父进程(即shell)截获它的标准输出,然后由shell决定将前一指令的输出转发到下一进程、文件或标准输出(即屏幕)。 +- 当出现`|` 和 `<`时,我们需要把后一指令的标准输入重定向为管道,让父进程(即shell)把前一进程被截获的标准输出/指定文件读出的内容通过管道发给后一进程。 + +重定向使用`dup2`系统调用完成。其原型为:`int dup2(int oldfd, int newfd);`。该函数相当于将`newfd`标识符变成`oldfd`的一个拷贝,与`newfd`相关的输入/输出都会重定向到`oldfd`中。如果`newfd`之前已被打开,则先将其关闭。举例:下述程序在屏幕上没有输出,而在文件输出"hello!goodbye!"。屏幕上不会出现"hello!"和"goodbye"。 + +```c +#include +#include +#include +#include +#include +#include + +int main(void) +{ + int fd; + + fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); + + // 此代码运行在dup2之前,本应显示到屏幕,但实际上是暂时放到屏幕输出缓冲区 + printf("hello!"); + + // 程序默认是输出到STDOUT_FILENO即1中,现在我们让1重定向到fd。 + // 也就是将标准输出的内容重定向到文件,因此屏幕输出缓冲区以及后面的printf + // 语句本应输出到屏幕(即标准输出,fd为STDOUT_FILENO)的内容重定向到文件中。 + dup2(fd, 1); + + printf("goodbye!\n"); + return 0; +} +``` + +> 易混淆的地方:向文件/管道内写入数据实际是程序的一个输出过程。 + +## 2.5 一些shell命令的例子和结果分析 + +> 本部分旨在帮助想要进一步理解真实shell行为的同学,希望下述示例及结果分析可以为同学们带来启发。 + +这一节中,我们给出一些shell命令的例子,并分析命令的输出结果,让大家能够更直观的理解shell的运行。**我们并不要求实验中实现的shell行为和真实的shell完全一致,但你自己需要能够清楚地解释自己实现的处理逻辑。**但在实现shell时,可以参考真实shell的这些行为。这些实验都是在bash下测试的,大家也可以自己在Ubuntu等的终端中重复这些实验。在这一节的代码中,每行开头为`$` 的,是输入的shell命令,剩下的行是shell的输出结果。如: + +```shell +$ cd /bin +$ pwd +/bin +``` + +表示在shell中了先运行了`cd /bin` ,然后运行了`pwd` ,第一条命令没有任何输出,第二条命令输出为`/bin` 。`pwd` 命令会显示shell的当前目录,我们先用`cd` 命令进入了`/bin` 目录,所以`pwd` 输出`/bin` 。 + +### 2.5.1 多个子命令 + +由`;` 分隔的多个子命令,和在多行中依次运行这些子命令效果相同。 + +```shell +$ cd /bin ; pwd +/bin +$ echo hello ; echo my ; echo shell +hello +my +shell +``` + +`echo` 会将它的参数打印到标准输出,如`echo hello world` 会输出`hello world` 。这个实验用`;`分隔了三个shell命令,结果和在三行中分别运行这些命令一致。 + +### 2.5.2 管道符 + +这个实验展示了shell处理管道时的行为,相同的命令在管道中行为可能和单独运行时不同。 + +```shell +$ echo hello | echo my | echo shell +shell +``` + +如上,虽然管道中的上一条命令的输出被重定向至下一条命令的输入,但是因为`echo` 命令本身不接受输入,所以前两个`echo` 的结果不会显示。如果实验检查中,你提供了这样的测试样例,是不能证明正确实现了管道的。 + +```shell +$ cd /bin ; pwd +/bin +$ cd /etc | pwd +/bin +``` + +如上,`cd /etc` 并没有改变shell的当前目录,这是因为**管道中的内置命令也是在新的子进程中运行的**,所以不会改变当前进程(shell)的状态。而在**不包含管道的命令中,内置命令在shell父进程中运行,外部命令在子进程中运行**,所以,不包含管道的内置命令能够改变当前进程(shell)的状态。可以用`type` 命令检查命令是否是内部命令。 + +```shell +$ type cd +cd is a shell builtin +$ type cat +cat is hashed (/bin/cat) +``` + +说明`cd` 是内置命令,而`cat` 则是调用`/bin/cat` 的外置命令。接下来我们测试管道中命令的运行顺序。 + +```shell +$ sleep 0.02 | sleep 0.02 | sleep 0.02 | ps | grep sleep +15840 tty1 00:00:00 sleep +$ sleep 0.02 | sleep 0.02 | sleep 0.02 | ps | grep sleep +15845 tty1 00:00:00 sleep +15846 tty1 00:00:00 sleep +15847 tty1 00:00:00 sleep +``` + +可以看到,运行了两次相同的命令,却得到了不同的结果,多次运行该命令,每次输出的sleep行数不同,从0行到3行都有可能(根据电脑速度和核数,可能需要将sleep后面的数字增大或缩小来重复该实验)。这说明**管道中各个命令是并行执行的**,`ps` 命令运行时,前面的`sleep` 命令可能执行结束,也有可能仍在执行。 + +### 2.5.3 重定向 + +**以下命令均在bash运行,如果使用zsh可能会产生不同结果,详见附录1** + +```shell +$ cd /tmp ; mkdir test ; cd test +$ echo hello > a >> b > c +$ ls +a b c +$ cat a +$ cat b +$ cat c +hello +``` + +当一个命令中出现多个输出重定向时,虽然所有文件都会被建立,但是只有最后一个文件会真正被写入命令的输出。 + +```shell +$ cd /tmp ; mkdir test ; cd test +$ echo hello > out | grep hello +$ cat out +hello +``` + +当输出重定向和管道符同时使用时,命令会将结果输出到文件中,而管道中的下一个命令将接收不到任何字符。 + +```shell +$ cd /tmp ; mkdir test ; cd test +$ > out2 echo hello ; cat out2 +hello +``` + +重定向符可以写在命令前。 + +## 2.6 总结:本次实验中shell执行命令的流程 + +- 第一步:打印命令提示符(类似shell ->)。 +- 第二步:把分隔符`;`连接的各条命令分割开。(多命令选做内容) +- 第三步:对于单条命令,把管道符`|`连接的各部分分割开。 +- 第四步:如果命令为单一命令没有管道,先根据命令设置标准输入和标准输出的重定向(重定向选做内容);再检查是否是shell内置指令:是则处理内置指令后进入下一个循环;如果不是,则fork出一个子进程,然后在fork出的子进程中exec运行命令,等待运行结束。(如果不fork直接exec,会怎么样?) +- 第五步:如果只有一个管道,创建一个管道,并将子进程1的**标准输出重定向到管道写端**,然后fork出一个子进程,根据命令重新设置标准输入和标准输出的重定向(重定向选做内容),在子进程中先检查是否为内置指令,是则处理内置指令,否则exec运行命令;子进程2的**标准输入重定向到管道读端**,同子进程1的运行思路。(注:2.4的样例分析中我们得出,管道的多个命令之间,虽然某个命令可能会因为等待前一个进程的输出而阻塞,但整体是没有顺序执行的,即并发执行。所以我们为了让多个内置指令可以并发,需要在fork出子进程后才执行内置指令) +- 第六步:如果有多个管道,参考第三步,n个进程创建n-1个管道,每次将子进程的**标准输出重定向到管道写端**,父进程保存**对应管道的读端**(上一个子进程向管道写入的内容),并使得下一个进程的**标准输入重定向到保存的读端**,直到最后一个进程使用标准输出将结果打印到终端。(多管道选做内容) +- 第七步:根据第二步结果确定是否有剩余命令未执行,如果有,返回第三步执行(多命令选做内容);否则进入下一步。(分隔符多命令和管道连接的多命令实现方式上有什么区别?为什么?) +- 第八步:打印新的命令提示符,进入下一轮循环。 + +## 2.7 任务目标 + +代码填空实现一个shell。这个shell不要求在qemu下运行。功能包括: + +### 必做部分 + +- 实现运行单条命令(非shell内置命令)。 +- 支持一条命令中有单个管道符`|` 。 +- 实现exit, cd, kill三个shell内置指令。 +- 在shell上能显示当前所在的目录。如:{ustc}shell: [/home/ustc/exp2/] > (后面是用户的输入) + +> kill内置命令:涉及SIGTERM、SIGKILL等多种信号量,命令执行方式可以**自定义**,输入的信号量用编号表示即可,例如:kill 1234 9,表示强制终止PID为1234的进程(9为SIGKILL信号量编号);kill 1234,表示以默认的方式(SIGTERM信号量)终止PID为1234的进程。注意(kill 1234 9只是自定义的命令执行方法,bash中kill的执行方式见1.1.6) + +### Makefile部分 + +补全助教提供的 Makefile 文件,实现自动编译和测试。要求包括: + ++ 可以正确编译 `testsh`,`simple_shell`。 ++ 可以使用 `test` 为目标使用 `testsh` 测试 `simple_shell`。 + +### 二选一部分 + +- 实现子命令符`;`和重定向符`>`, `>>`, `<`。 +- 支持一条命令中有多个管道符`|`。如果你确信你的多管道功能可以正常实现单管道功能,代码填空里的单管道可以不做。 + +### 其他说明 + +- 实验文件中提供了一个testsh.c作为本次shell实验的测试脚本,testdata作为测试脚本所需要的文件(不可修改或删除,否则测试样例不能通过),测试脚本中前7条为必做实验部分,8-12为选做1,13-14测试选做2,使用方法为先编译出可执行文件,再 `testsh shell_name`,你可以在testsh中得到本次实验的部分提示。(详细测试样例说明,请查看附录2) +- 在实现cd命令时,不要求实现`cd -`,`cd ~`等方式 +- 我们使用的是Linux系统调用,请不要尝试在Windows下运行自己写的shell程序。那是不可能的。 +- 需要自行设计测试样例以验证你的实现是正确的。如测试单管道时使用`ps aux | wc -l`,与自带的shell输出结果进行比较。 +- 不限制分隔符、管道符、重定向符的符号优先级。你可以参考我们代码框架中实现、提示的优先级。 +- 不要求实现不加空格的重定向符,不要求实现使用'~'表示家目录。 +- 请尽量使用系统调用完成实验,不准用system函数。 +- 我们提供了本次实验使用的系统调用API的范围,请同学们自行查询它们的使用方法。你在实验中可能会用到它们中的一部分。 你也可以使用不属于本表的系统调用。 + - read/write + - open/close + - pipe + - dup/dup2 + - getpid/getcwd + - fork/vfork/clone/wait/exec + - mkdir/rmdir/chdir + - exit/kill + - shutdown/reboot + - chmod/chown +- 如果你想问如何编译、运行自己写的代码,请参考lab1的2.3.17和2.2.3,以及本实验的1.3.2。 +- 得分细则见文档3.1。 + + + + + +
+ +# 章节三: 检查要求 + +本部分实验无实验报告。 + +本部分实验共4分。 + +## 3.1 “实现一个Shell”部分检查标准 (4') + +- 支持基本的单条命令运行、支持两条命令间的管道`|`、内建命令(只要求`cd`/`exit`/`kill`),得2分。 +- 选做: + - 支持多条命令间的管道`|`操作,得1分。 + - 支持重定向符`>`、`>>`、`<`和分号`;`,得1分。 +- 你需要流畅地说明你是如何实现实验要求的(即,现场讲解代码),**并且使用make命令展示测试结果**。本部分共1分。若未能完成任何一个功能,则该部分分数无效。 + + + + + +# 附录 + +## 1. Unix不同Shell的区别和联系 + +我们在2.1中提到了Shell的基本原理,其中提到了Unix中不同的Shell,本部分先说明不同shell的区别,最后讲解为什么在重定向中Bash和Zsh的输出并不完全相同 + +### 1.1 基本shell + +Unix系统中主要要两种类型的shell:“Bourne Shell”和“C Shell”。并且这两种类别的shell分别有几种子类别 + +Bourne Shell 的子类别有: + ++ [Bourne Shell (sh)](https://en.wikipedia.org/wiki/Bourne_shell) ++ [KornShell (ksh)](https://en.wikipedia.org/wiki/KornShell) ++ [Bourne Again Shell (Bash)](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) + +C-Type shells的子类别有: + ++ [C Shell (csh)](https://en.wikipedia.org/wiki/C_shell) ++ [TENEX/TOPS C shell (tcsh)](https://en.wikipedia.org/wiki/Tcsh) + +其中Bourne-Again Shell即Bash,是用于取代默认Bourne Shell 的 Unix shell。它在各方面都融合了Korn和C Shell的功能。这意味着用户可以获得 Bourne Shell 的语法兼容性和跨平台性,并在此基础上使用这些其他 shell 的功能进行扩展。 + +> 查看本机上已经安装的shell可使用 `cat /etc/shells`来查看,安装使用和修改默认shell方法自行查询 + +### 1.2 当前热门的Shell + +1. Fish(Friendly Interactive Shell) + + Fish是2005年发布的开源Shell,专门开发为易于使用,并具有开箱即用功能的shell。其风格化的颜色编码对新程序员也很有帮助,因为它突出了语法,使其更易于阅读。Fish Shell 的功能包括制表符补全、语法突出显示、自动补全建议、可搜索的命令历史记录等。(**该Shell的语法与Bash差别较大,有些bash脚本可能无法正常使用,需要修改为特定语法**) + + > Ubuntu安装方法: + > + > ```sh + > $ sudo apt install fish + > $ fish + > ``` + +2. Zsh(Z shell) + + Z-shell 被设计为现代创新和交互式外壳。Zsh 在其他 Unix 和开源 Linux shell (包括 tcsh、ksh、Bash 等)之上提供了一组独特的功能。这个开源 shell 易于使用、可定制,提供拼写检查、自动完成和其他生产力功能。 + + > Ubuntu安装方法: + > + > ```sh + > $ sudo apt install zsh + > $ zsh + > ``` + +### 1.3 重定向中zsh和bash的表现不同的原因 + + 在文档2.5.3节中阐述了shell中重定向的使用方法,如果使用bash,输出结果如下所示 + +```shell +$ cd /tmp ; mkdir test ; cd test +$ echo hello > a >> b > c +$ ls +a b c +$ cat a +$ cat b +$ cat c +hello +``` + + 但是如果使用zsh,输出结果则如下所示 + +```shell +$ cd /tmp ; mkdir test ; cd test +$ echo hello > a >> b > c +$ ls +a b c +$ cat a +hello +$ cat b +hello +$ cat c +hello +``` + + 首先,先看一个简单的例子 + +```shell +$ echo hello world > a +$ cat a +hello world +``` + + 该例子即使用 `>`重定向符号将echo的标准输出重定向到文件a,我们可以查看 `/proc/PID_OF_PROCESS/fd`来查看具体情况,由于echo命令执行速度较快,使用如下例子 + +```shell + $ cat > a + + # Open another Shell + $ ls -l /proc/$(pidof cat)/fd + total 0 + lrwx------ 1 ridx ridx 64 Mar 28 15:31 0 -> /dev/pts/0 + l-wx------ 1 ridx ridx 64 Mar 28 15:31 1 -> ~/a + lrwx------ 1 ridx ridx 64 Mar 28 15:31 2 -> /dev/pts/0 +``` + + cat等待用户输入后重定向给文件a直到Ctrl-D结束,可以看到cat的标准输出已经指向a。0 是标准输入(即用户输入端),1 是标准输出(即正常情况的输出端),2 是错误输出(即异常情况的输出端),想要重定向错误输出端,可以使用 `2>`,本实验不要求实现该方法。 + +```shell + #Use Bash Shell + $ cat > a >> b >c + + # Open another Shell + $ ls -l /proc/$(pidof cat)/fd + total 0 + lrwx------ 1 ridx ridx 64 Mar 28 15:44 0 -> /dev/pts/0 + l-wx------ 1 ridx ridx 64 Mar 28 15:44 1 -> ~/c + lrwx------ 1 ridx ridx 64 Mar 28 15:44 2 -> /dev/pts/0 +``` + + 回到最开始的重定向例子中,由于例子中echo命令过快,我们使用cat来代替.使用bash执行命令 `cat > a >> b > c`。可以看到cat标准输出只有c,所以cat程序只往文件c中写入数据 + +```shell + #Use Zsh + $ cat > a >> b >c + + # Open another Shell + $ pstree -p | grep cat + |-sh(3365094)---node(3365156)-+-node(3365181)-+-zsh(3416851)---cat(3416921)---zsh(3416922) + $ ls -l /proc/3416921/fd + total 0 + lrwx------ 1 ridx ridx 64 Mar 28 15:48 0 -> /dev/pts/0 + l-wx------ 1 ridx ridx 64 Mar 28 15:48 1 -> 'pipe:[457492940]' + lrwx------ 1 ridx ridx 64 Mar 28 15:48 2 -> /dev/pts/0 + $ ls -l /proc/3416922/fd + total 0 + l-wx------ 1 ridx ridx 64 Mar 28 15:50 11 -> ~/a + l-wx------ 1 ridx ridx 64 Mar 28 15:50 16 -> ~/b + l-wx------ 1 ridx ridx 64 Mar 28 15:50 18 -> ~/c + lr-x------ 1 ridx ridx 64 Mar 28 15:50 17 -> 'pipe:[457492940]' +``` + + 可以看到 cat 的标准输出是重定向到管道,管道对面是 zsh 进程,然后 zsh 打开了那三个文件。实际将内容写入文件的是 zsh,而不是 cat。所以在zsh中 `echo hello > a >> b > c`,三个文件均更新. + +> 在fish中,重定向的表现和bash相同,不过fish无法使用$(...)语法,感兴趣可以直接使用 `ls -l /proc/(pidof cat)/fd`或者通过pstree查看cat进程号手动验证 + +## 2. shell实验测试样例详细说明 + +### 2.1编译运行 + +testsh.c为标准c程序代码,编译测试请参考请参考本实验第一部分的2.3.17和2.2.3。测试程序接受一个字符串参数 `shell_name`,请先编译出自己的shell程序,然后编译testsh,运行命令请使用 `testsh shell_name`。(当然我们鼓励使用makefile或者sh脚本来编译运行测试文件) + +如果同时完成选做1和选做2,运行结果如下 + +```sh +kill: PASS +cd: PASS +current path: PASS +simple echo: PASS +simple grep: PASS +two commands: PASS +simple pipe: PASS +more test: PASS +output redirection(use >): PASS +output redirection(use >>): PASS +input redirection: PASS +both redirections: PASS +pipe and redirection: PASS +multipipe: PASS +two commands with pipes: PASS +passed all tests +``` + +### 2.2testsh详细说明 + +测试程序中,每一个测试样例为一个函数,下面表格为所有的测试样例。testsh使用的前提是你的shell先完成**exit**命令的功能 + +| 函数名 | 测试样例/功能 | 命令 | +| ------ | -------------------------- | ------------------------------------------------------------ | +| t0 | kill | `kill [pid]` | +| t1 | cd | `cd /sys/class/net` | +| t2 | current path | 查看shell是否打印了当前目录 | +| t3 | simple echo | `echo hello goodbye` | +| t4 | simple grep | `grep professor testdata` | +| t5 | two commands | `echo x\necho goodbye\n` | +| t6 | simple pipe | `cat file | cat` | +| t7 | more test | `ps | grep ps` | +| t8 | output redirection(use >) | `echo data > file` | +| t9 | output redirection(use >>) | `echo data > file;echo data >> file` | +| t10 | input redirection | `cat < file` | +| t11 | both redirections | `grep USTC < testdata > testsh.out` | +| t12 | pipe and redirections | `grep system < testdata | wc > testsh.out` | +| t13 | multipipe | `cat testdata | grep system | wc -l` | +| t14 | two commands with pipes | `echo hello 2025 | grep hello ; echo nihao 2025 | grep 2025` | + +### 2.3可能出现的问题 + +1. 运行该程序时,可能会出现 `unexpected wait() return`的报错,如果出现该报错,请尝试再次运行程序 +2. 在测试 `simple pipe`功能时可能会有卡顿,属于正常情况,请耐心等待 +3. 本程序必须要求你的simple_shell具有exit功能,否则无法正常工作 +4. 如果你的ßshell无关信息输出过多,可能会有 `testsh: saw expected output, but too much else as well`出现,请调整代码 +5. 如果shell测试失败,可能会产生临时乱码文件,属于正常现象,测试完毕后把临时文件删除即可 +6. 本程序如果出现各种问题,请及时在QQ群/文档/ 提供问题描述或建议 + +### 3.C语言程序编译流程 + +1. 预处理(Preprocessing) + 预处理是编译过程的第一步,主要使用预处理器来处理源代码文件。在预处理阶段,源代码中的宏定义、条件编译指令以及头文件的包含等将被展开和处理,生成经过处理的中间代码。预处理的结果是一个经过宏定义替换、头文件包含等操作后的中间代码文件。 +2. 编译(Compiling) + 编译是将预处理之后的中间代码翻译成汇编代码的过程。编译器会将中间代码文件转化为与特定平台相关的汇编语言代码文件。在编译阶段,进行语法分析、语义分析等操作,生成与特定平台相关的汇编代码。 +3. 汇编(Assembling) + 汇编是将汇编语言代码翻译为机器代码的过程。汇编器将汇编代码转换为目标文件,其中包含了与特定平台相关的机器指令和数据。目标文件中包含了汇编语言代码转换而成的机器码。 +4. 链接(Linking) + 链接是将各个目标文件(包括自己编写的文件和库文件)合并为一个可执行文件的过程。链接器将目标文件中的符号解析为地址,并将它们连接到最终的可执行文件中。链接过程包括符号解析、地址重定位等步骤,确保各个模块能够正确地相互调用。 +5. 可执行文件(Executable) + 最终的输出是一个可执行文件,其中包含了所有必要的指令和数据,可以在特定平台上运行。这个可执行文件经过编译链接过程,包含了源代码的所有功能和逻辑,可以被操作系统加载并执行。 diff --git a/docs/syscalllab/syscalllab.md b/docs/syscalllab/syscalllab.md index eceadb2..382e798 100644 --- a/docs/syscalllab/syscalllab.md +++ b/docs/syscalllab/syscalllab.md @@ -1,794 +1,794 @@ -# 实验一:Linux基础与系统调用——添加系统调用(part3) - -## 实验目的 - -- 掌握Linux内核编译方法; -- 掌握使用gdb调试内核的方法及步骤; -- 学习如何添加新的系统调用。 - -## 实验环境 - -- 虚拟机:VMware -- 操作系统:Ubuntu 24.04.2 LTS - -> 系统的安装形式可以自由选择,双系统,虚拟机都可以,系统版本则推荐使用本文档所用版本。注意:由于Linux各种发行版非常庞杂且存在较大差异,因此本试验在其他Linux发行版可能会存在兼容性问题。如果想使用其他环境(如vlab)或系统(如Arch、WSL等),请根据自己的系统**自行**调整实验步骤以及具体指令,达成实验目标即可,但其中出现的兼容性问题助教**无法**保证能够一定解决。 - - -## 实验时间安排 - -> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 - - -- 3.28 晚实验课,讲解实验第一部分、第二部分,检查实验 -- 4.4 清明节放假 -- 4.11 晚实验课,讲解实验第三部分,检查实验 -- 4.18 晚实验课,检查实验 -- 4.25 晚及之后实验课,补检查实验 - -> 补检查分数照常给分,但会**记录**此次检查未按时完成,此记录在最后综合分数时作为一种参考(即:最终分数可能会低于当前分数)。 - -检查时间、地点:周五晚18: 30~22: 00,电三楼406/408。 - -## 如何提问 - -- 请同学们先阅读《提问指南》。[原文链接](https://lug.ustc.edu.cn/wiki/doc/howtoask/) -- 提问前,请先**阅读报错信息**、查询在线文档,或百度。[在线文档链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR); -- 在向助教提问时,请详细描述问题,并提供相关指令及相关问题的报错截图; -- 在QQ群内提问时,如遇到长时未收到回复的情况,可能是由于消息太多可能会被刷掉,因此建议在在线文档上提问; -- 如果助教的回复成功地帮你解决了问题,请回复“问题已解决”,并将问题及解答更新到在线文档。这有助于他人解决同样的问题。 - -## 为什么要做这个实验 - -- 为什么要学会使用Linux? - - Linux的安全性、稳定性更好,性能也更好,配置也更灵活方便,所以常用于服务器和开发环境。实验室和公司的服务器一般也都用Linux; - - Linux是开源系统,代码修改方便,很多学术成果都基于Linux完成; - - Windows是闭源系统,代码无法修改,无法进行后续实验。 -- 为什么要使用虚拟机? - - 虚拟机对你的电脑影响最低。双系统若配置不正确,可能导致无法进入Windows,而虚拟机自带的快照功能也可以解决部分误操作带来的问题。 - - 本实验并不禁止其他环境的使用,但考虑其他环境(如WSL)变数太大,比如可能存在兼容性或者其他配置问题,会耽误同学们大量时间浪费在实验内容以外的琐事,因此建议各位同学尽量保持与本试验一致或类似的环境。 -- 为什么要学会编译Linux内核? - - 这是后续实验的基础。在后续实验中,我们会让大家通过阅读Linux源码、修改Linux源码、编写模块等方式理解一个真实的操作系统是怎么工作的。 - -## 其他友情提示 - -- **合理安排时间,强烈不建议在ddl前赶实验**。 -- 本课程的实验实践性很强,请各位大胆尝试,适当变通,能完成实验任务即可。 -- pdf上文本的复制有时候会丢失或者增加不必要的空格,有时候还会增加不必要的回车,有些指令一行写不下分成两行了,一些同学就漏了第二行。如果出了bug,建议各位先仔细确认自己输入的指令是否正确。要**逐字符**比对。每次输完指令之后,请观察一下指令的输出,检查一下这个输出是不是报错。**请在复制文档上的指令之前先理解一下指令的含义。** 我们在检查实验时会抽查提问指令的含义。 -- 如果你想问“为什么PDF的复制容易出现问题”,请参考[此文章](https://type.cyhsu.xyz/2018/09/understanding-pdf-the-digitalized-paper/)。 -- 如果同学们遇到了问题,请先查询在线文档。在线文档地址:[链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) - -
- -# 章节零 实验总览 - -![image-20250327223439365](.\assets\os-overview.png) - - - -# 章节一:编译、调试Linux内核,并使用你自己的shell - -## 1.1 先导知识 - -### 1.1.1 系统内核启动过程 - -Linux kernel在自身初始化完成之后,需要能够找到并运行第一个用户程序(此程序通常叫做“init”程序)。用户程序存在于文件系统之中,因此,内核必须找到并挂载一个文件系统才可以成功完成系统的引导过程。 - -在grub中提供了一个选项“root=”用来指定第一个文件系统,但随着硬件的发展,很多情况下这个文件系统也许是存放在USB设备,SCSI设备等等多种多样的设备之上,如果需要正确引导,USB或者SCSI驱动模块首先需要运行起来,可是不巧的是,这些驱动程序也是存放在文件系统里,因此会形成一个悖论。 - -为解决此问题,Linux kernel提出了一个RAM disk的解决方案,把一些启动所必须的用户程序和驱动模块放在RAM disk中,这个RAM disk看上去和普通的disk一样,有文件系统,有cache,内核启动时,首先把RAM disk挂载起来,等到init程序和一些必要模块运行起来之后,再切换到真正的文件系统之中。 - -但是,这种RAM disk的方案(下称initrd)虽然解决了问题但并不完美。 比如,disk有cache机制,对于RAM disk来说,这个cache机制就显得很多余且浪费空间;disk需要文件系统,那文件系统(如ext2等)必须被编译进kernel而不能作为模块来使用。 - -Linux 2.6 kernel提出了一种新的实现机制,即initramfs。顾名思义,initramfs只是一种RAM filesystem而不是disk。initramfs实际是一个cpio归档,启动所需的用户程序和驱动模块被归档成一个文件。因此,不需要cache,也不需要文件系统。 - -### 1.1.2 什么是initramfs - -initramfs 是一种以 cpio 格式压缩后的 rootfs 文件系统,它通常和 Linux 内核文件一起被打包成boot.img 作为启动镜像。 - -BootLoader 加载 boot.img,并启动内核之后,内核接着就对 cpio 格式的 initramfs 进行解压,并将解压后得到的 rootfs 加载进内存,最后内核会检查 rootfs 中是否存在 init 可执行文件(该init 文件本质上是一个执行的 shell 脚本),如果存在,就开始执行 init 程序并创建 Linux 系统用户空间 PID 为 1 的进程,然后将磁盘中存放根目录内容的分区真正地挂载到 / 根目录上,最后通过 `exec chroot . /sbin/init` 命令来将 rootfs 中的根目录切换到挂载了实际磁盘分区文件系统中,并执行 /sbin/init 程序来启动系统中的其他进程和服务。 - -基于ramfs开发的initramfs取代了initrd。 - -### 1.1.3 什么是initrd - -initrd代指内核启动过程中的一个阶段:临时挂载文件系统,加载硬盘的基础驱动,进而过渡到最终的根文件系统。 - -initrd也是早期基于ramdisk生成的临时根文件系统的名称。现阶段虽然基于initramfs,但是临时根文件系统也依然存在某些发行版称其为initrd。例如,CentOS 临时根文件系统命名为 initramfs-\`uname -r\`.img,Ubuntu 临时根文件系统命名为 initrd-\`uname -r`.img(uname -r是系统内核版本)。 - -### 1.1.4 QEMU - -QEMU是一个开源虚拟机。可以在里面运行Linux甚至Windows等操作系统。 - -本次实验需要在虚拟机中安装QEMU,并使用该QEMU来运行编译好的Linux内核。这么做的原因如下: - -- 在Windows下编译LInux源码十分麻烦,且QEMU在Windows下速度很慢; -- 之后的实验会涉及修改Linux源码。如果直接修改Ubuntu的内核,改完代码重新编译之后需要重启才能完成更改,但带GUI的Ubuntu系统启动速度较慢。另外,操作失误可能导致Ubuntu系统损坏无法运行。 - -## 1.2 下载、安装 - -> 为防止表述混乱,本文档指定`~/oslab`为本次实验使用的的目录。同学们也可以根据需要在其他目录中完成本次实验。如果在实验中遇到“找不到`~/oslab`”的报错,请先创建相关目录。 - -### 1.2.0 安装 qemu 和 Linux 编译依赖库 - -我们在part2已经让各位同学安装了相关库,如果还未安装,使用包管理器安装以下包: - -- qemu-system-x86 (Ubuntu 20.04以上) 或 qemu (Ubuntu 其他版本) -- git -- build-essential (里面包含了make/gcc/g++等,省去了单独安装的麻烦) -- libelf-dev -- xz-utils -- libssl-dev -- bc -- libncurses5-dev -- libncursesw5-dev - -为了方便同学们复制粘贴,将所有的依赖放在这里:`qemu-system-x86 git build-essential libelf-dev xz-utils libssl-dev bc libncurses5-dev libncursesw5-dev` - -### 1.2.1 下载 Linux 内核源码 - -- 下载Linux内核源码,保存到`~/oslab/`目录(提示:使用`wget`)。源码地址: - - [https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.263.tar.xz](https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.263.tar.xz) - - 备用链接1:[https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/linux-4.9.263.tar.xz](https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/linux-4.9.263.tar.xz) - - 备用链接2(该链接直接使用浏览器下载,希望大家优先使用上面的链接):[https://rec.ustc.edu.cn/share/fd142c30-e345-11ee-a1e6-d9366df5cc79](https://rec.ustc.edu.cn/share/fd142c30-e345-11ee-a1e6-d9366df5cc79) -- 解压Linux内核源码(提示:使用`tar`。xz格式的解压参数是`Jxvf`,参数区分大小写)。 - -### 1.2.2 下载 busybox - -- 下载busybox源码,保存到`~/oslab/`目录(提示:使用`wget`)。源码地址: - - [https://busybox.net/downloads/busybox-1.32.1.tar.bz2](https://busybox.net/downloads/busybox-1.32.1.tar.bz2) - - 备用链接1:[https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/busybox-1.32.1.tar.bz2](https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/busybox-1.32.1.tar.bz2) - - 备用链接2(该链接直接使用浏览器下载,希望大家优先使用上面的链接):[https://rec.ustc.edu.cn/share/92eb3b20-e345-11ee-a46a-fd7a50991a4a](https://rec.ustc.edu.cn/share/92eb3b20-e345-11ee-a46a-fd7a50991a4a) -- 解压busybox源码(提示:使用`tar`。bz2格式的解压参数是`jxvf`)。 - -如果1.2.1和1.2.2执行正确的话,你应该能在`~/oslab/`目录下看到两个子目录,一个叫`linux-4.9.263`,是**Linux内核源码路径**。另一个叫`busybox-1.32.1`,是**busybox源码路径**。 - -### 1.2.3 编译 Linux 源码 - -1. 精简配置:将我们提供的`.config`文件下载到Linux内核源码路径(提示:该路径请看1.2.2最后一段的描述,使用`wget`下载)。`.config`文件地址: - - - [https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/.config](https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/.config) - - 备用链接:[https://git.lug.ustc.edu.cn/gloomy/ustc_os/-/raw/master/term2021/lab1/.config](https://git.lug.ustc.edu.cn/gloomy/ustc_os/-/raw/master/term2021/lab1/.config) - - > 提示:以`.`开头的文件是隐藏文件。如果你下载后找不到它们,请参考[Linux ls命令](https://www.runoob.com/linux/linux-comm-ls.html)。 - - -2. 内核配置:在Linux内核源码路径下运行`make menuconfig`以进行编译设置。本次实验直接选择Save,然后Exit。 - -3. 编译:在Linux内核源码路径下运行```make -j $((`nproc`-1))```以编译内核。作为参考,助教台式机CPU为i5-7500(4核4线程),使用VitrualBox虚拟机,编译用时5min左右。 - - > 提示: - > - > 1. `nproc`是个shell内置变量,代表CPU核心数。如果虚拟机分配的cpu数只有1(如Hyper-V默认只分配1核),则需先调整虚拟机分配的核心数。 - > 2. 如果你的指令只有`make -j`,后面没有加上处理器核数,那么编译器会无限开多线程。因为Linux内核编译十分复杂,这会直接吃满你的系统资源,导致系统崩溃。 - > 3. `nproc`前的`` `是反引号,位于键盘左上侧。**不**是enter键旁边那个。 - - 若干其他问题及其解决方案: - - > 问题1: 编译内核时遇到 make[1]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'. Stop. - > 解决方案:用文本编辑器(vim 或 gedit)打开 PATH-TO-linux-4.9.263/.config文件, 找到并注释掉包含CONFIG_SYSTEM_TRUSTED_KEY 和 CONFIG_MODULE_SIG_KEY 的两行即可. - > 解决方案链接: https://unix.stackexchange.com/questions/293642/attempting-to-compile-kernel-yieldsa-certification-error - - > 问题2:make menuconfig时遇到以下错误:Your display is too small to run Menuconfig! It must be at least - > 19 lines by 80 columns. - > 解决方案:请阅读报错信息并自行解决该问题。 - -4. 如果编译成功,我们可以在Linux内核源码路径下的`arch/x86_64/boot/`下看到一个`bzImage`文件,这个文件就是内核镜像文件。 - - > 若怀疑编译/安装有问题,可以先在Linux内核源码路径下运行`make clean`之后从1.2.3.1开始。 - -### 1.2.4 编译busybox - -1. 在busybox的源码路径下运行`make menuconfig`以进行编译设置。修改配置如下:(空格键勾选) - - ```shell - Settings –> - Build Options - [*] Build static binary(no share libs) - ``` - -2. 在busybox的源码路径下执行`rm networking/tc.c`指令。 - - > 新版本内核相关结构体定义发生改变,导致tc指令安装会失败,本实验我们无需该指令,所以删除它,再进行编译。 - -3. 编译:在busybox的源码路径下运行``make -j $((`nproc`-1))``以编译busybox。本部分的提示与1.2.3.3相同。 - -4. 安装:在busybox的源码路径下运行``sudo make install``。 - - > 若怀疑编译/安装有问题,可以先在busybox的源码路径下运行`make clean`之后从1.2.4.1开始。 - -### 1.2.5 制作根文件系统 - -1. 将工作目录切换为busybox源码目录下的`_install`目录。 - -2. **使用`sudo`创建**一个名为`dev`的文件夹(提示:参考[Linux mkdir命令](https://www.runoob.com/linux/linux-comm-mkdir.html))。 - -3. 使用以下指令创建`dev/ram`和`dev/console`两个设备文件: - - * `sudo mknod dev/console c 5 1` - * `sudo mknod dev/ram b 1 0 ` - -5. **使用`sudo`创建**一个名为`init`的文件(提示:参考第一部分2.3.16)。**使用文本编辑器编辑文件**,文件内容如下: - - > 特别提示:这一步**不是**让你在终端里执行这些命令。在复制粘贴时,请注意空格、回车是否正确地保留。 - > - > 在命令行下启动gedit会在终端里报warning,忽略即可。[参考链接](https://askubuntu.com/questions/798935/set-document-metadata-failed-when-i-run-sudo-gedit) - - ```shell - #!/bin/sh - echo "INIT SCRIPT" - mkdir /proc - mkdir /sys - mount -t proc none /proc - mount -t sysfs none /sys - mkdir /tmp - mount -t tmpfs none /tmp - echo -e "\nThis boot took $(cut -d' ' -f1 /proc/uptime) seconds\n" - exec /bin/sh - ``` - -6. 赋予`init`执行权限:`sudo chmod +x init` - -7. 将x86-busybox下面的内容打包归档成cpio文件,以供Linux内核做initramfs启动执行: - - `find . -print0 | cpio --null -ov --format=newc | gzip -9 > 你们自己指定的cpio文件路径` - - > 注意1:该命令一定要在busybox的 _install 目录下执行。 - > - > 注意2:每次修改`_install`,都要重新执行该命令。 - > - > 注意3:请自行指定cpio文件名及其路径。示例:`~/oslab/initramfs-busybox-x64.cpio.gz` - > - > 示例指令:`find . -print0 | cpio --null -ov --format=newc | gzip -9 > ~/oslab/initramfs-busybox-x64.cpio.gz` - - -### 1.2.6 运行qemu - -我们本次实验需要用到的qemu指令格式: -`qemu-system-x86_64 [-s] [-S] [-kernel ImagePath] [-initrd InitPath] [--append Para] [-nographic] ` - -| 参数 | 含义 | -| ----------------- | ------------------------------------------------------------ | -| -s | 在1.3.2介绍 | -| -S | 在1.3.2介绍 | -| -kernel ImagePath | 指定系统镜像的路径为ImagePath。在本次实验中,该路径为1.2.4.4所述的bzImage文件路径。 | -| -initrd InitPath | 指定initramfs(1.2.5生成的cpio文件)的路径为InitPath。在本次实验中,该路径为1.2.5.7所述的cpio文件路径。 | -| --append Para | 指定给内核启动赋的参数。 | -| -nographic | 设置无图形化界面启动。 | - -在`--append`参数中,本次实验需要使用以下子参数: - -| 参数 | 含义 | -| ------------- | ------------------------------------------------------------ | -| nokaslr | 关闭内核地址随机化,便于调试。 | -| root=/dev/ram | 指定启动的第一个文件系统为ramdisk。我们在1.2.5.3创建了`/dev/ram`这个设备文件。 | -| init=/init | 指定init进程为/init。我们在1.2.5.4创建了init。 | -| console=ttyS0 | **对于无图形化界面启动**(如WSL),需要使用本参数指定控制台输出位置。 | - -这些参数之间以空格分隔。 - -总结(TL;DR): - -> 运行下述指令前,请注意先**理解指令含义**,**注意换行问题**。 -> -> 如果报错找不到文件,请先检查:**是否把pdf的换行和不必要的空格复制进去了?前几步输出的东西是不是在期望的位置上?** 我们不会提示下述指令里哪些空格/换行符是应有的,哪些是不应有的。请自行对着上面的指令含义排查。 -> 如果报错Failed to execute /init (error -8),建议从1.2.5开始重做。若多次重做仍有问题,建议直接删掉解压好的busybox和Linux源码目录,从3.2.4重做。目前已知init文件配置错误、Linux内核编译出现问题都有可能导致本错误。 - - -* 以图形界面,弹出窗口形式运行内核: - - ```shell - qemu-system-x86_64 -kernel ~/oslab/linux-4.9.263/arch/x86_64/boot/bzImage -initrd ~/oslab/initramfs-busybox-x64.cpio.gz --append "nokaslr root=/dev/ram init=/init" - ``` - -* Ubuntu 20.04/20.10 环境下如果出现问题,可执行以下指令: - - ```shell - qemu-system-x86_64 -kernel ~/oslab/linux-4.9.263/arch/x86/boot/bzImage -initrd ~/oslab/initramfs-busybox-x64.cpio.gz --append "nokaslr root=/dev/ram init=/init" - ``` - -* 如不希望qemu以图形界面启动,希望以无界面形式启动(如WSL),输出重定向到当前shell,使用以下命令: - - ```shell - qemu-system-x86_64 -kernel ~/oslab/linux-4.9.263/arch/x86_64/boot/bzImage -initrd ~/oslab/initramfs-busybox-x64.cpio.gz --append "nokaslr root=/dev/ram init=/init console=ttyS0 " -nographic - ``` - -其他常见问题: - -1. 如何检查运行是否正确? - - **弹出的黑色窗口就是qemu**。因为系统内核在启动的时候会输出一些log,所以qemu界面里偶尔会蹦出一两条log是正常的,** 只要这些log不是诸如"kernel panic"之类的报错 **即可。建议尝试输入一条命令,比如在弹出的窗口里面输入`ls`回车,如果能够显示相关的文件列表,即说明运行正确。 - -2. 鼠标不见了,该怎么办? - - 请观察窗口上方的标题栏,"Press (请自行观察标题栏上的说明) to release grab" - -3. 如何关闭qemu? - - 对于图形化界面,直接点击右上角的叉就行了。对于非图形化界面,请先按下`Ctrl+A`,松开这两个键之后再按键盘上的`X`键。 - - - -## 1.3 使用 gdb调试内核 - -gdb是一款命令行下常用的调试工具,可以用来打断点、显示变量的内存地址,以及内存地址中的数据等。使用方法是`gdb 可执行文件名`,即可在gdb下调试某二进制文件。 - -一般在使用gcc等编译器编译程序的时候,编译器不会把调试信息放进可执行文件里,进而导致gdb知道某段内存里有内容,但并不知道这些内容是变量a还是变量b。或者,gdb知道运行了若干机器指令,但不知道这些机器指令对应哪些C语言代码。所以,在使用gdb时需要在编译时加入`-g`选项,如:`gcc -g -o test test.c`来将调试信息加入可执行文件。而Linux内核采取了另一种方式:它把符号表独立成了另一个文件,在调试的时候载入符号表文件就可以达到相同的效果。 - -* gdb里的常用命令 - - ``` shell - r/run # 开始执行程序 - b/break # 在location处添加断点,location可以是代码行数或函数名 - b/break if # 在location处添加断点,仅当condition条件满足才中断运行 - c/continue # 继续执行到下一个断点或程序结束 - n/next # 运行下一行代码,如果遇到函数调用直接跳到调用结束 - s/step # 运行下一行代码,如果遇到函数调用则进入函数内部逐行执行 - ni/nexti # 类似next,运行下一行汇编代码(一行c代码可能对应多行汇编代码) - si/stepi # 类似step,运行下一行汇编代码 - l/list # 显示当前行代码 - p/print # 查看表达式expression的值 - q # 退出gdb - ``` - -### 1.3.1 安装 gdb - -使用包管理器安装名为gdb的包即可。 - -### 1.3.2 启动 gdb server - -使用1.2.6所述指令运行qemu。但需要加上`-s`和`-S`两个参数。这两个参数的含义如下: - -| 参数 | 含义 | -| ---- | ------------------------------------------------------------ | -| -s | 启动gdb server调试内核,server端口是1234。若不想使用1234端口,则可以使用`-gdb tcp:xxxx`来取代此选项。 | -| -S | 若使用本参数,在qemu刚运行时,CPU是停止的。你需要在gdb里面使用c来手动开始运行内核。 | - -### 1.3.3 建立连接 - -**另开**一个终端,运行gdb,然后在gdb界面里运行如下命令: - -```shell -target remote:1234 # 建立gdb与gdb server间的连接。这时候我们看到输出带有??,还报了一条warning - # 这是因为没有加载符号表,gdb不知道运行的是什么代码。 -c # 手动开始运行内核。执行完这句后,你应该能发现旁边的qemu开始运行了。 -q # 退出gdb。 -``` - -### 1.3.4 重新编译Linux内核使其携带调试信息 - -刚才因为没有加载符号表,所以gdb不知道运行了什么代码。所以我们要重新编译Linux来使其携带调试信息。 - -1. 进入Linux源码路径。 - -2. 执行下列语句(这条语句没有回车,注意文档显示时产生的额外换行问题): - `./scripts/config -e DEBUG_INFO -e GDB_SCRIPTS -d DEBUG_INFO_REDUCED -d DEBUG_INFO_SPLIT -d DEBUG_INFO_DWARF4` - -3. 重新编译内核。``make -j $((`nproc`-1))`` - - 作为参考,助教台式机CPU为i5-7500(4核4线程),编译用时8.5min左右。 - -### 1.3.5 加载符号表、设置断点 - -1. 重新执行1.3.2. - -2. **另开**一个终端,运行gdb,然后在gdb界面里运行如下命令: - - ```shell - target remote:1234 # 建立gdb与gdb server间的连接。这时候我们看到输出带有??, - # 这是因为没有加载符号表,gdb不知道运行的是什么代码。 - file Linux源码路径/vmlinux # 加载符号表。(不要把左边的东西原封不动地复制过来,这一步可能会有warning,属于正常现象,请忽略) - n # 单步运行。此时可以看到右边不是??,而是具体的函数名了。 - break start_kernel # 设置断点在start_kernel函数。 - c # 运行到断点。 - l # 查看断点代码。 - p init_task # 查看断点处名为"init_task"的变量值。这个变量是个结构体,里面有一大堆成员。 - ``` - - - -
- - - -# 章节二 编写系统调用实现一个Linux strace - -## 2.0 如何阅读Linux源码 - -因为直接在VSCode、Visual Studio等本地软件中阅读Linux源码会让电脑运行速度变得很慢,所以我们建议使用在线阅读。一个在线阅读站是[https://elixir.bootlin.com/linux/v4.9.263/source](https://elixir.bootlin.com/linux/v4.9.263/source)。左栏可以选择内核源码版本,右边是目录树或代码,右上角有搜索框。下面举例描述一次查阅Linux源码的过程。 - -假定我们要查询Linux中进程名最长有多长。首先,我们知道进程在内存中的数据结构是结构体 `task_struct`,所以我们首先查找 `task_struct`在哪。搜索发现在 `include/linux/sched.h`。(下面那个从名字来看像个附属模块,所以大概可以确定结构体的声明应该在 `sched.h`。) - -pic-3.0.1 - -进入 `sched.h`查看 `task_struct`的声明。我们不难猜到,进程名应该是个一维char数组。所以我们开始找 `char`数组。看了几十个成员之后,我们找到了 `char comm[TASK_COMM_LEN]`这个东西。右边注释写着 `executable name excluding path`,应该就是这个了。所以它的长度就应该是 `TASK_COMM_LEN`。 - -pic-3.0.2 - -于是我们点击一下这个 `TASK_COMM_LEN`,找到这个宏在哪定义。发现定义也在 `sched.h`。点开就能看到宏定义了。至于它的长度到底是多少,留作习题,给大家自己实践查询。 - - - -## 2.1 若干名词解释 - -### 2.1.1 用户空间、内核空间 - -- **用户空间**:用户空间是操作系统为**用户级应用程序**分配的虚拟内存区域。应用程序(如文本编辑器、浏览器等)在此空间中运行,仅能访问受限的资源和内存。 -- **内核空间**:内核空间是操作系统**内核及核心功能**运行的虚拟内存区域,负责硬件管理、进程调度、内存分配等核心任务。 - -### 2.1.2 系统调用 - -**系统调用 (System call, Syscall)**是操作系统提供给**用户程序访问内核空间的合法接口**。 - -系统调用**运行于内核空间,可以被用户空间调用**,是内核空间和用户空间划分的关键所在,它保证了两个空间必要的联系。从用户空间来看,系统调用是一组统一的抽象接口,用户程序**无需考虑接口下面是什么**;从内核空间来看,用户程序不能直接进行敏感操作,所有对内核的操作都必须通过功能受限的接口间接完成,保证了**内核空间的稳定和安全**。 - -> 我们平时的编程中**为什么没有意识到系统调用的存在**?这是因为应用程序现在一般**通过应用编程接口 (API)来间接使用系统调用**,比如我们如果想要C程序打印内容到终端,只需要使用C的标准库函数API `printf()`就可以了,而不是使用系统调用 `write()` 将内容写到终端的输出文件 (`STDOUT_FILENO`) 中。 - -### 2.1.3 glibc - -glibc是GNU发布的c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现,其内容包罗万象。glibc内含的档案群分散于系统的目录树中,像支架一般撑起整个操作系统。 - -`stdio.h`, `malloc.h`, `unistd.h`等都是glibc实现的封装。 - -## 2.2 系统调用是如何执行的 - -1. 调用应用程序调用**库函数(API)** - Linux使用的开源标准C运行库glibc(GNU libc)有一个头文件 `unistd.h`,其中声明了很多**封装好的**系统调用函数,如 `read/write`。这些API会调用实际的系统调用。 - -2. API将**系统调用号存入**EAX,然后**触发软中断**使系统进入内核空间。 - 32位x86机器使用汇编代码 `int $0x80`触发中断。具体如何触发软中断可以去参考Linux的实现源码(如 `arch/x86/entry/entry_32.S`)。因为这部分涉及触发中断的汇编指令,这部分代码是用汇编语言写的。 - - > 对于64位x86机器,现在往往使用syscall来触发中断,具体如何触发软中断可以去参考Linux的实现源码(如 `arch/x86/entry/entry_64.S`重点关注该文件的144行和157行两个处理逻辑,第一段为保存用户空间的指针,紧接着 `call do_syscall_64`即为真正开始执行内核态系统调用) - -3. 内核的中断处理函数根据系统调用号,调用对应的**内核函数**(也就是课上讲的的系统调用)。前面的 `read/write`,实际上就是调用了内核函数 `sys_read/sys_write` 。(对于64位机器,即进入 `do_syscall_64`函数,该函数在 `arch/x86/entry/common.c`中定义,并且最后使用**syscall_return_slowpath(regs)**函数来返回) - - > 总结:添加系统调用需要**注册中断处理函数和对应的调用号**,内核才能找到这个系统调用,并执行对应的内核函数。 - -4. 内核函数完成相应功能,将返回值存入EAX,返回到中断处理函数; - - > 注1:系统执行相应功能,调用的是C代码编写的函数,而不是汇编代码,减轻了实现系统调用的负担。系统调用的函数定义在 `include/linux/syscalls.h`中。因为汇编代码到C代码的参数传递是通过栈实现的,所以,所有系统调用的函数前面都使用了 `asmlinkage` 宏,它意味着编译时限制只使用栈来传递参数。如果不这么做,用汇编代码调用C函数时可能会产生异常。 - > 注2:用户空间和内核空间各自的地址是不能直接互相访问的,需要借助函数拷贝来实现数据传递,后面会说明。 - -5. 中断处理函数返回到API中; - - > 返回前执行**syscall_return_slowpath(regs)**函数,该函数用于处理内核中的状态,参数 `regs`中包含执行的系统调用号(`regs->orig_ax`),以及系统调用返回值(`regs->ax`)。执行该函数的进程的pid可以通过task得到(`task->pid`) - -6. API将EAX返回给应用程序。 - - > 注:当glibc库没有封装某个系统调用时,我们就没办法通过使用封装好的API来调用该系统调用,而使用 `int $0x80`触发中断又需要进行汇编代码的编写,这两种方法都不适合我们在添加过新系统调用后再编写测试代码去调用。因此我们后面采用的是第三种方法,使用glibc提供的 `syscall`库函数,这个库函数只需要传入调用号和对应内核函数要用到的参数,就可以直接调用我们新写的系统调用。具体详见2.4.1. - -## 2.3 添加系统调用的流程 - -> Linux 4.9的文档 `linux-4.9.263/Documentation/adding-syscalls.txt`中有说明如何添加一个系统调用。本实验文档假设所有同学使用的平台都是x86,采用的是x86平台的系统调用添加方法。其他平台的同学可以参考上述文档给出的其他平台系统调用添加方法。 - -在实现系统调用之前,要先考虑好添加系统调用的函数原型,确定函数名称、要传入的参数个数和类型、返回值的意义。在实际设计中,还要考虑加入系统调用的必要性。 - -我们这里以一个统计系统中进程个数的系统调用 `ps_counter(int *num)` 作为演示,num是返回值对应的地址。 - -> 因为系统调用的返回值表示系统调用的完成状态(0是正常,其他值是异常,代表错误编号),所以不建议用返回值传递信息。 - -本节的各步是没有严格顺序的,但在编译前都要完成: - -### 2.3.1 注册系统调用 - -内核的汇编代码会在 `arch/x86/include/generated/asm/syscalls_64.h` 中查找调用号。为便于添加系统调用,x86平台提供了一个专门用来注册系统调用的文件 `/arch/x86/entry/syscalls/syscall_64.tbl`。在编译时,脚本 `arch/x86/entry/syscalls/syscalltbl.sh`会被运行,将上述 `syscall_64.tbl`文件中登记过的系统调用都生成到前面的 `syscalls_64.h`文件中。因此我们需要修改 `syscall_64.tbl`。 - -打开 `linux源码路径/arch/x86/entry/syscalls/syscall_64.tbl` 。 - -``` -# linux-4.9.263/arch/x86/entry/syscalls/syscall_64.tbl -# ... 前面还有,但没展示 -329 common pkey_mprotect sys_pkey_mprotect -330 common pkey_alloc sys_pkey_alloc -331 common pkey_free sys_pkey_free -# 以上是系统自带的,不是我写的,下面这个是 -332 common ps_counter sys_ps_counter -``` - -这个文件是**x86平台下的系统调用注册表**,记录了很多数字与函数名的映射关系。我们在这个文件中注册系统调用。 -每一行从左到右各字段的含义,及填写方法是: - -- 系统调用号:请为你的系统调用找一个没有用过的数字作为调用号(记住调用号,后面要用) -- 调用类型:本次实验请选择common(含义为:x86_64和x32都适用) -- 系统调用名xxx -- 内核函数名sys_xxx - -> 提示:为保持代码美观,每一列用制表符排得很整齐。但经本人测试,无论你是用制表符还是用空格分隔,无论你用多少空格或制表符,只要你把每一列分开了,无论是否整齐,对这一步的完成都没有影响。 - -### 2.3.2 声明内核函数原型 - -打开 `linux-4.9.263/include/linux/syscalls.h`,里面是对于系统调用函数原型的定义,在最后面加上我们创建的新的系统调用函数原型,格式为 `asmlinkage long sys_xxx(...)` 。注意,如果传入了用户空间的地址,需要加入 `__user`宏来说明。 - -```c -asmlinkage long sys_mlock2(unsigned long start, size_t len, int flags); -asmlinkage long sys_pkey_mprotect(unsigned long start, size_t len, - unsigned long prot, int pkey); -asmlinkage long sys_pkey_alloc(unsigned long flags, unsigned long init_val); -asmlinkage long sys_pkey_free(int pkey); -//这里是我们新增的系统调用 -asmlinkage long sys_ps_counter(int __user * num); - -#endif -``` - -### 2.3.3 实现内核函数 - -你可以在 `linux-4.9.264/kernel/sys.c` 代码的最后添加你自己的函数,这个文件中有很多已经实现的系统调用的函数作为参考。我们给出了一段示例代码: - -```c -SYSCALL_DEFINE1(ps_counter, int __user *, num){ - struct task_struct* task; - int counter = 0; - printk("[Syscall] ps_counter\n"); - for_each_process(task){ - counter ++; - } - copy_to_user(num, &counter, sizeof(int)); - return 0; -} -``` - -以下是这段代码的解释: - -1. 使用宏 `SYSCALL_DEFINEx`来简化实现的过程 ,其中x代表参数的个数。传入宏的参数为:调用名(不带sys_)、以及每个参数的类型、名称(注意这里输入时为两项)。 - - 以我们现在的 `sys_ps_counter`为例。因为使用了一个参数,所以定义语句是 `SYSCALL_DEFINE1(ps_counter, int *, num)`。如果该系统调用有两个参数,那就应该是 `SYSCALL_DEFINE2(ps_counter, 参数1的类型, 参数1的名称, 参数2的类型, 参数2的名称)`,以此类推。 - - > 无法通过 `SYSCALL_DEFINEx`定义二维数组(如 `char (*p)[50]`)为参数。你可以尝试定义一个,然后结合编译器的报错信息分析该宏的实现原理,并分析不能定义的原因。 - -2. `printk()`是内核中实现的print函数,和 `printf()`的使用方式相同,`printk()`会将信息写入到内核日志中。尤其地,在qemu下调试内核时,系统日志会直接打印到屏幕上,所以我们可以直接在屏幕上看到 `printk`打印出的内容。`printf()`是使用了C的标准库函数的时候才能使用的,而内核中无法使用标准库函数。你不能 `#include`,自然不能用 `printf()`。 - - > 在使用 `printk`时,行首输出的时间等信息是去不掉的。 - -3. 为了获取当前的所有进程,我们使用了宏函数 `for_each_process(p)`(定义在 `include/linux/sched.h`中),遍历当前所有任务的信息结构体 `task_struct`(同样定义在 `include/linux/sched.h`中),并将地址赋值给参数 `p`。后面实验内容中我们会进一步用到 `task_struct`中的成员变量来获取更多相关信息,注意我们暂时不讨论多线程的场景,每一个 `task_struct`对应的可以认为就是一个进程。 - -4. 前面说到,系统调用是在内核空间中执行,所以如果我们要将在内核空间获取的值传递给用户空间,需要使用函数 `copy_to_user(void __user *to, const void *from, unsigned long n)` 来完成从内核空间到用户空间的复制。 其中,`from` 和 `to` 是复制的来源和目标地址,`n`是复制的大小,我们这里就是 `sizeof(int)`。 - -## 2.4 测试 - -> 若想使用VS Code调试Linux内核,可以参考此文章:[https://zhuanlan.zhihu.com/p/105069730](https://zhuanlan.zhihu.com/p/105069730),直接从“VS Code配置”一章阅读即可。实际操作中,你可能还需要在VS Code中安装"C/C++"和"C/C++ Extension Pack"插件。 - -### 2.4.1 编写测试代码 - -在你的Linux环境下(不是打开qemu后弹出的那个)编写测试代码。我们在这里命名其为 `get_ps_num.c`。新增的系统调用可以使用 `long int syscall (long int sysno, ...)` 来触发,`sysno`就是我们前面添加的调用号,后面跟着要传入的参数,下面是一个简单的测试代码样例: - -```c -#include -#include -#include -int main(void) -{ - int result; - syscall(332, &result); - printf("process number is %d\n",result); - return 0; -} -``` - -> 提示: -> -> - 这里的测试代码是用户态代码,不是内核态代码,所以可以使用 `printf`。 -> - 为使用系统调用号调用系统调用,需要使用 `sys/syscall.h`内的 `syscall` 函数。 -> - 我们推荐使用C语言而不是C++,因为C++在下一步(3.4.2)的静态编译时容易出现问题。使用C语言时需要注意C和C++之间的语法差异。 - -### 2.4.2 编译 - -使用gcc编译器编译 `get_ps_num.c`。**本次实验需要使用静态编译(`-static`选项)**。要用到的gcc指令原型是:`gcc [-static] [-o outfile] infile`。 - -> 不使用静态编译会导致在qemu内运行测试程序时报错找不到文件,因为找不到链接库。 - -### 2.4.3 运行测试程序 - -1. 将2.4.2编译出的可执行文件 `get_ps_num`放到 `busybox-1.32.1/_install`下面,重新制作initramfs文件(重新执行1.2.5.6的find操作),这样我们才能在qemu中看见编译好的 `get_ps_num`可执行文件。 - - > 注意:因为_install文件夹只有root用户才有修改权限,所以复制文件应该在命令行下使用 `sudo`。 - -2. 重新编译linux源码(重新执行1.2.3.3,无需重新make menuconfig) - -3. 运行qemu(重新执行1.2.6,我们在这里不要求使用gdb调试) - -4. 在qemu中运行 `get_ps_num`程序 ,得到当前的进程数量(这个进程数量是包括 `get_ps_num`程序本身的,下图中,除了 `get_ps_num`本身外,还有55个进程)。 - -5. 验证:使用 `ps aux | wc -l` 获取当前进程数。但这个输出的结果与上一步中得到的不同。请自己思考数字不同的原因。 - - > 在实际操作中,系统中的进程数量可能不是55。只要与 `ps aux | wc -l`得到的结果匹配即可。 - -2022-03-29 16-10-49 的屏幕截图.png - -## 2.5 附加知识:正则表达式 & strace命令 - -> 本部分在后续的实验中会用到,请不要跳过 - -### 2.5.1 正则表达式 - -> 参考链接:https://www.runoob.com/regexp/regexp-syntax.html - -正则表达式,又称规则表达式、常规表示法(Regular Expression,简写为 regex、regexp 或 RE),是计算机科学领域中对字符串操作的一种逻辑公式。它由事先定义好的特定字符及这些字符的组合构成一个 “规则字符串”,以此来表达对其他字符串的过滤逻辑,通常用于查找、替换符合特定特征的字符串。(如果你使用的IDE是vscode,Ctrl-f后,查找框最后一个选项即使用正则表达式匹配) - -基本语法: - -| 符号 | 说明 | 示例 | -| ------ | ------------------------------ | ------------------------ | -| . | 匹配任意单个字符(除换行符) | `a.c` → “abc”、“a1c” | -| \d | 匹配数字(等价于 [0-9]) | `\d\d` → “12”、“99” | -| \w | 匹配字母、数字、下划线 | `\w+` → “hello_123” | -| \s | 匹配空白字符(空格、制表符等) | `\s+` → 匹配连续空格 | -| ^ | 匹配字符串开头 | `^abc` → 以 “abc” 开头 | -| $ | 匹配字符串结尾 | `xyz$` → 以 “xyz” 结尾 | -| ^ /$ | 字符串开始 / 结束 | `^start / end$` | -| [abc] | 匹配括号内的任意字符 | `[aeiou]` → 匹配元音字母 | -| [^abc] | 匹配不在括号内的任意字符 | `[^0-9]` → 非数字字符 | - -举个栗子🌰: - -如果想要找系统调用中含有'read'的,例如('readlinkat','read','readv'等),则直接使用正则表达式 `read`即可匹配。 - -如果想要完全匹配,即只想找'read'系统调用,则使用正则表达式 `\bread\b`(\b表示单词边界,更多的语法规则自行google) - -如果想要完全匹配'read'或者'write',即只想过滤这两个系统调用,则可以使用正则表达式 `\bread\b|\bwrite\b`('|'表示选择匹配,只要匹配其中之一即可) - -> 本实验将会使用到c语言的正则表达式库 `regex.h`,该库在各个Linux发行版中存在,但是在内核中不存在,所以不能在内核中使用该头文件,使用方法主要分为两个步骤 -> -> - 1.编译正则表达式(代码已给出) -> -> ```c -> if (regcomp(®ex, argv[1], REG_EXTENDED) != 0) { -> fprintf(stderr, "Invalid regex: %s\n", argv[1]); -> exit(1); -> } -> ``` -> -> - 2.匹配正则表达式,使用第一步得到的regex,若下面函数执行返回值为0,即 `need_judge`字符串符合pattern,即被匹配到 -> -> ```c -> regexec(®ex, need_judge, 0, NULL, 0) -> ``` -> -> 后续编译原理课程将会系统学习该工具,上面的例子足够完成本次实验,不再赘述 - -### 2.5.2 strace命令 - -> 参考链接:https://www.cnblogs.com/machangwei-8/p/10388883.html -> -> 下载strace使用命令 `sudo apt install strace` - -在Linux系统中,strace命令是一个集诊断、调试、统计与一体的工具,可用来追踪调试程序。(其使用ptrace系统调用来实现) - -举个栗子🌰: -从其他机器copy了一个程序,但是运行直接报错,没有源码的情况下,怎么判断哪里出错了?(反汇编太麻烦了😭) - -使用strace追踪其使用的系统调用流程,可以看到在报错信息的那一行,到底是哪个系统调用,其执行的参数是什么,根据该信息可以进一步查找问题 - -> 本次实验将添加类似ptrace系统调用,并且完成一个简单的strace程序 - -## 2.6 任务目标 - -- 在linux4.9下创建适当的(可以是一个或多个)系统调用。利用新实现的系统调用,实现在Linux4.9下,追踪程序使用哪些系统调用(使用正则表达式过滤),参考命令 `strace`(注意,本实验需要关注后续的 `实验提示`!!!!): - -```shell -$ strace ls -execve("/usr/bin/ls", ["ls"], 0x7ffd025c7530 /* 50 vars */) = 0 -brk(NULL) = 0x555996f2b000 -arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd825cdf80) = -1 EINVAL (Invalid argument) -mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f48990c4000 -access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) -.......此处省略 -close(1) = 0 -close(2) = 0 -exit_group(0) = ? -+++ exited with 0 +++ -``` - -- 输出的信息需要包括: - - - 进程的PID - - 需要追踪的系统调用名称 - - 需要追踪的系统调用返回值 -- 具体评分规则见3.3。 我们不限制大家创建系统调用的数量、功能,也不限制测试程序的实现。能完成实验内容即可。 -- 下面是示例输出(由于是内核态打印的信息,所以前面[]中的数字不定)。 - -``` -$ ./trace "." ls -[ 216.310823] 986: syscall execve -> 0 -[ 216.312476] 986: syscall brk -> 11907072 -[ 216.312911] 986: syscall brk -> 11910464 -[ 216.313688] 986: syscall arch_prctl -> 0 -[ 216.314377] 986: syscall set_tid_address -> 986 -[ 216.314805] 986: syscall set_robust_list -> 0 -[ 216.315553] 986: syscall prlimit64 -> 0 -[ 216.316537] 986: syscall readlinkat -> 12 -[ 216.316996] 986: syscall getrandom -> -11 -.... -[ 216.331815] 986: syscall close -> 0 -[ 216.332365] 986: syscall fstat -> 0 -[ 216.332715] 986: syscall ioctl -> 0 -bin init proc sbin tmp usr -[ 216.334948] 986: syscall write -> 109 -dev linuxrc root sys trace -[ 216.336595] 986: syscall write -> 92 -$ ./trace "\bfstat\b" ls -[ 5348.862758] 997: syscall fstat -> 0 -[ 5348.864303] 997: syscall fstat -> 0 -bin init proc sbin tmp usr -dev linuxrc root sys trace -``` - -## 2.7 任务提示 - -- 在系统调用的内核函数中,如果要得到当前进程的进程结构体,可以使用`struct task_struct *task = current;` - -- 实验需要修改进程结构体 `task_struct`(定义在 `include/linux/sched.h`),需要添加变量(用于判断该进程是否需要被追踪)和数组(用于记录需要追踪的系统调用号) - -- 实验需要修改系统调用返回函数 `syscall_return_slowpath`(定义在 `arch/x86/entry/common.c`),当某个系统调用执行完毕后,是否需要打印信息到终端?系统调用号、其返回值,以及进程pid获得方法查看2.2节系统调用的执行流程。 - -- 修改系统调用返回函数( `syscall_return_slowpath`)时,可能会用到 `trace.c`中提供的系统调用表 `syscall_names`,自行复制到该函数之前即可。 - -- 实验需要补全 `trace.c`文件,其用于对正则表达式进行判断需要追踪哪些系统调用并传递给内核,不在 `syscall_names`中的系统调用不进行追踪。 - -- 追踪到的系统调用信息直接在内核打印即可,在内核打印使用什么函数? - -- 如果需要将用户空间的变量复制到内核空间(例如解析得到的需要追踪的系统调用号),用 `copy_from_user(void *to, const void __user *from, unsigned long n)`。 - -- 内核使用for循环不允许直接定义变量,例如 `for(int i = 0; i < cnt; i++)`,需要先定义i,然后进行使用 - -- 内核空间对栈内存(如局部变量等)的使用有较大限制。你可能无法在内核态开较长的数组作为局部变量。但本次实验的预期解法不使用很多的内核态栈内存。 - -- 本次实验无需在内核态动态分配空间,如有需求请自行查询实现方法。 - -- **如果访问数组越界,会出现很多奇怪的错误(如程序运行时报malloc错误、段错误、输出其他数组的值或一段乱码(注意,烫烫烫是Visual C的特性,Linux下没有烫烫烫)、其他数组的值被改变等)。提问前请先排查是否出现了此类问题。** - -- 一些常见错误: - - - 拼写错误,如 `asmlkage`、`commom`、`__uesr`、中文逗号 - - 语法错误,如 `sizeof(16 * int)` - - C标准问题,如C语言声明结构体、算 `sizeof`时应该用 `struct xxx`而不是直接 `xxx` - - 上述错误均可以通过阅读编译器报错信息查出。 - -
- -# 章节三: 检查要求 - -本部分实验无实验报告。 - -本部分实验共11分。 - -## 3.1 第三部分检查要求(5') - -现场能启动虚拟机并启动gdb调试,即现场执行1.3.5,共5分。本部分检查中,允许学生对照实验文档操作。 - -* 你需要能够简要解释符号表的作用。若不能解释,本部分减1分。 -* 此外,**为提高检查效率,请自己编写一个【shell脚本】启动qemu(即,使用Part2 1.3所述方法完成本部分1.3.5的第一步)。** 若不能使用脚本启动qemu,本部分减1分。你不需要使用脚本启动或操作gdb。 - - - -## 3.2 知识问答 (2') - -我们会使用随机数发生器随机地从下面题库中抽三道题,每回答正确一题得一分,满分2分。也就是说,允许答错一题。请按照自己的理解答题,答题时不准念实验文档或念提前准备的答案稿。 -下面是题库: - -1. 解释 `wc`和 `grep`指令的含义。 -2. 解释 `ps aux | grep firefox | wc -l`的含义。 -3. `echo aaa | echo bbb | echo ccc`是否适合做shell实验中管道符的检查用例?说明原因。 -4. 对于匿名管道,如果写端不关闭,并且不写,读端会怎样? -5. 对于匿名管道,如果读端关闭,但写端仍尝试写入,写端会怎样? -6. 假如使用匿名管道从父进程向子进程传输数据,这时子进程不写数据,为什么子进程要关闭管道的写端? -7. fork之后,是管道从一分为二,变成两根管道了吗?如果不是,复制的是什么? -8. 解释系统调用 `dup2`的作用。 -9. 什么是shell内置指令,为什么不能fork一个子进程然后 `exec cd`? -10. 为什么 `ps aux | wc -l`得出的结果比 `get_ps_num`多2? -11. 进程名的最大长度是多少?这个长度在哪定义? -12. strace可以追踪通过fork创建的子进程的系统调用,fork系统调用使用了哪个函数来实现这一行为?(提示:fork使用的函数定义在 `linux/fork.c:copy_procss`中) -13. 找到你的学号的后两位对应的64位机器的系统调用,并说明该系统调用的功能(例如学号为PB23111678,则查看78对应的系统调用)。 -14. 在修改内核代码的时候,能用 `printf`调试吗?如果不能,应该用什么调试? -15. `read()、write()、dup2()`都能直接调用。现在我们已经写好了一个名为ps_counter的系统调用。为什么我们不能在测试代码中直接调 `ps_counter()`来调用系统调用? - -大家可以自己思考或互相讨论这些题目的答案(但不得在本课程群内公布答案)。助教不会公布这些题目的答案。 - - - -## 3.3 “编写系统调用”部分检查标准 (4') - -- 你的程序可以追踪系统调用,即可以打印所有系统调用执行信息以及进程pid。本部分共1分。 -- 你的程序可以过滤追踪部分系统调用,即可以指定打印部分系统调用执行信息以及进程pid。本部分共1分。 -- 你需要在现场阅读Linux源码,展示pid的数据类型是什么(即,透过多层 `typedef`找到真正的pid数据类型)。本部分共1分。 +# 实验一:Linux基础与系统调用——添加系统调用(part3) + +## 实验目的 + +- 掌握Linux内核编译方法; +- 掌握使用gdb调试内核的方法及步骤; +- 学习如何添加新的系统调用。 + +## 实验环境 + +- 虚拟机:VMware +- 操作系统:Ubuntu 24.04.2 LTS + +> 系统的安装形式可以自由选择,双系统,虚拟机都可以,系统版本则推荐使用本文档所用版本。注意:由于Linux各种发行版非常庞杂且存在较大差异,因此本试验在其他Linux发行版可能会存在兼容性问题。如果想使用其他环境(如vlab)或系统(如Arch、WSL等),请根据自己的系统**自行**调整实验步骤以及具体指令,达成实验目标即可,但其中出现的兼容性问题助教**无法**保证能够一定解决。 + + +## 实验时间安排 + +> 注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准 + + +- 3.28 晚实验课,讲解实验第一部分、第二部分,检查实验 +- 4.4 清明节放假 +- 4.11 晚实验课,讲解实验第三部分,检查实验 +- 4.18 晚实验课,检查实验 +- 4.25 晚及之后实验课,补检查实验 + +> 补检查分数照常给分,但会**记录**此次检查未按时完成,此记录在最后综合分数时作为一种参考(即:最终分数可能会低于当前分数)。 + +检查时间、地点:周五晚18: 30~22: 00,电三楼406/408。 + +## 如何提问 + +- 请同学们先阅读《提问指南》。[原文链接](https://lug.ustc.edu.cn/wiki/doc/howtoask/) +- 提问前,请先**阅读报错信息**、查询在线文档,或百度。[在线文档链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR); +- 在向助教提问时,请详细描述问题,并提供相关指令及相关问题的报错截图; +- 在QQ群内提问时,如遇到长时未收到回复的情况,可能是由于消息太多可能会被刷掉,因此建议在在线文档上提问; +- 如果助教的回复成功地帮你解决了问题,请回复“问题已解决”,并将问题及解答更新到在线文档。这有助于他人解决同样的问题。 + +## 为什么要做这个实验 + +- 为什么要学会使用Linux? + - Linux的安全性、稳定性更好,性能也更好,配置也更灵活方便,所以常用于服务器和开发环境。实验室和公司的服务器一般也都用Linux; + - Linux是开源系统,代码修改方便,很多学术成果都基于Linux完成; + - Windows是闭源系统,代码无法修改,无法进行后续实验。 +- 为什么要使用虚拟机? + - 虚拟机对你的电脑影响最低。双系统若配置不正确,可能导致无法进入Windows,而虚拟机自带的快照功能也可以解决部分误操作带来的问题。 + - 本实验并不禁止其他环境的使用,但考虑其他环境(如WSL)变数太大,比如可能存在兼容性或者其他配置问题,会耽误同学们大量时间浪费在实验内容以外的琐事,因此建议各位同学尽量保持与本试验一致或类似的环境。 +- 为什么要学会编译Linux内核? + - 这是后续实验的基础。在后续实验中,我们会让大家通过阅读Linux源码、修改Linux源码、编写模块等方式理解一个真实的操作系统是怎么工作的。 + +## 其他友情提示 + +- **合理安排时间,强烈不建议在ddl前赶实验**。 +- 本课程的实验实践性很强,请各位大胆尝试,适当变通,能完成实验任务即可。 +- pdf上文本的复制有时候会丢失或者增加不必要的空格,有时候还会增加不必要的回车,有些指令一行写不下分成两行了,一些同学就漏了第二行。如果出了bug,建议各位先仔细确认自己输入的指令是否正确。要**逐字符**比对。每次输完指令之后,请观察一下指令的输出,检查一下这个输出是不是报错。**请在复制文档上的指令之前先理解一下指令的含义。** 我们在检查实验时会抽查提问指令的含义。 +- 如果你想问“为什么PDF的复制容易出现问题”,请参考[此文章](https://type.cyhsu.xyz/2018/09/understanding-pdf-the-digitalized-paper/)。 +- 如果同学们遇到了问题,请先查询在线文档。在线文档地址:[链接](https://docs.qq.com/sheet/DU1JrWXhKdFFpWVNR) + +
+ +# 章节零 实验总览 + +![image-20250327223439365](.\assets\os-overview.png) + + + +# 章节一:编译、调试Linux内核,并使用你自己的shell + +## 1.1 先导知识 + +### 1.1.1 系统内核启动过程 + +Linux kernel在自身初始化完成之后,需要能够找到并运行第一个用户程序(此程序通常叫做“init”程序)。用户程序存在于文件系统之中,因此,内核必须找到并挂载一个文件系统才可以成功完成系统的引导过程。 + +在grub中提供了一个选项“root=”用来指定第一个文件系统,但随着硬件的发展,很多情况下这个文件系统也许是存放在USB设备,SCSI设备等等多种多样的设备之上,如果需要正确引导,USB或者SCSI驱动模块首先需要运行起来,可是不巧的是,这些驱动程序也是存放在文件系统里,因此会形成一个悖论。 + +为解决此问题,Linux kernel提出了一个RAM disk的解决方案,把一些启动所必须的用户程序和驱动模块放在RAM disk中,这个RAM disk看上去和普通的disk一样,有文件系统,有cache,内核启动时,首先把RAM disk挂载起来,等到init程序和一些必要模块运行起来之后,再切换到真正的文件系统之中。 + +但是,这种RAM disk的方案(下称initrd)虽然解决了问题但并不完美。 比如,disk有cache机制,对于RAM disk来说,这个cache机制就显得很多余且浪费空间;disk需要文件系统,那文件系统(如ext2等)必须被编译进kernel而不能作为模块来使用。 + +Linux 2.6 kernel提出了一种新的实现机制,即initramfs。顾名思义,initramfs只是一种RAM filesystem而不是disk。initramfs实际是一个cpio归档,启动所需的用户程序和驱动模块被归档成一个文件。因此,不需要cache,也不需要文件系统。 + +### 1.1.2 什么是initramfs + +initramfs 是一种以 cpio 格式压缩后的 rootfs 文件系统,它通常和 Linux 内核文件一起被打包成boot.img 作为启动镜像。 + +BootLoader 加载 boot.img,并启动内核之后,内核接着就对 cpio 格式的 initramfs 进行解压,并将解压后得到的 rootfs 加载进内存,最后内核会检查 rootfs 中是否存在 init 可执行文件(该init 文件本质上是一个执行的 shell 脚本),如果存在,就开始执行 init 程序并创建 Linux 系统用户空间 PID 为 1 的进程,然后将磁盘中存放根目录内容的分区真正地挂载到 / 根目录上,最后通过 `exec chroot . /sbin/init` 命令来将 rootfs 中的根目录切换到挂载了实际磁盘分区文件系统中,并执行 /sbin/init 程序来启动系统中的其他进程和服务。 + +基于ramfs开发的initramfs取代了initrd。 + +### 1.1.3 什么是initrd + +initrd代指内核启动过程中的一个阶段:临时挂载文件系统,加载硬盘的基础驱动,进而过渡到最终的根文件系统。 + +initrd也是早期基于ramdisk生成的临时根文件系统的名称。现阶段虽然基于initramfs,但是临时根文件系统也依然存在某些发行版称其为initrd。例如,CentOS 临时根文件系统命名为 initramfs-\`uname -r\`.img,Ubuntu 临时根文件系统命名为 initrd-\`uname -r`.img(uname -r是系统内核版本)。 + +### 1.1.4 QEMU + +QEMU是一个开源虚拟机。可以在里面运行Linux甚至Windows等操作系统。 + +本次实验需要在虚拟机中安装QEMU,并使用该QEMU来运行编译好的Linux内核。这么做的原因如下: + +- 在Windows下编译LInux源码十分麻烦,且QEMU在Windows下速度很慢; +- 之后的实验会涉及修改Linux源码。如果直接修改Ubuntu的内核,改完代码重新编译之后需要重启才能完成更改,但带GUI的Ubuntu系统启动速度较慢。另外,操作失误可能导致Ubuntu系统损坏无法运行。 + +## 1.2 下载、安装 + +> 为防止表述混乱,本文档指定`~/oslab`为本次实验使用的的目录。同学们也可以根据需要在其他目录中完成本次实验。如果在实验中遇到“找不到`~/oslab`”的报错,请先创建相关目录。 + +### 1.2.0 安装 qemu 和 Linux 编译依赖库 + +我们在part2已经让各位同学安装了相关库,如果还未安装,使用包管理器安装以下包: + +- qemu-system-x86 (Ubuntu 20.04以上) 或 qemu (Ubuntu 其他版本) +- git +- build-essential (里面包含了make/gcc/g++等,省去了单独安装的麻烦) +- libelf-dev +- xz-utils +- libssl-dev +- bc +- libncurses5-dev +- libncursesw5-dev + +为了方便同学们复制粘贴,将所有的依赖放在这里:`qemu-system-x86 git build-essential libelf-dev xz-utils libssl-dev bc libncurses5-dev libncursesw5-dev` + +### 1.2.1 下载 Linux 内核源码 + +- 下载Linux内核源码,保存到`~/oslab/`目录(提示:使用`wget`)。源码地址: + - [https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.263.tar.xz](https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.263.tar.xz) + - 备用链接1:[https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/linux-4.9.263.tar.xz](https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/linux-4.9.263.tar.xz) + - 备用链接2(该链接直接使用浏览器下载,希望大家优先使用上面的链接):[https://rec.ustc.edu.cn/share/fd142c30-e345-11ee-a1e6-d9366df5cc79](https://rec.ustc.edu.cn/share/fd142c30-e345-11ee-a1e6-d9366df5cc79) +- 解压Linux内核源码(提示:使用`tar`。xz格式的解压参数是`Jxvf`,参数区分大小写)。 + +### 1.2.2 下载 busybox + +- 下载busybox源码,保存到`~/oslab/`目录(提示:使用`wget`)。源码地址: + - [https://busybox.net/downloads/busybox-1.32.1.tar.bz2](https://busybox.net/downloads/busybox-1.32.1.tar.bz2) + - 备用链接1:[https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/busybox-1.32.1.tar.bz2](https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/busybox-1.32.1.tar.bz2) + - 备用链接2(该链接直接使用浏览器下载,希望大家优先使用上面的链接):[https://rec.ustc.edu.cn/share/92eb3b20-e345-11ee-a46a-fd7a50991a4a](https://rec.ustc.edu.cn/share/92eb3b20-e345-11ee-a46a-fd7a50991a4a) +- 解压busybox源码(提示:使用`tar`。bz2格式的解压参数是`jxvf`)。 + +如果1.2.1和1.2.2执行正确的话,你应该能在`~/oslab/`目录下看到两个子目录,一个叫`linux-4.9.263`,是**Linux内核源码路径**。另一个叫`busybox-1.32.1`,是**busybox源码路径**。 + +### 1.2.3 编译 Linux 源码 + +1. 精简配置:将我们提供的`.config`文件下载到Linux内核源码路径(提示:该路径请看1.2.2最后一段的描述,使用`wget`下载)。`.config`文件地址: + + - [https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/.config](https://git.lug.ustc.edu.cn/cggwz/osexpr/-/raw/main/.config) + - 备用链接:[https://git.lug.ustc.edu.cn/gloomy/ustc_os/-/raw/master/term2021/lab1/.config](https://git.lug.ustc.edu.cn/gloomy/ustc_os/-/raw/master/term2021/lab1/.config) + + > 提示:以`.`开头的文件是隐藏文件。如果你下载后找不到它们,请参考[Linux ls命令](https://www.runoob.com/linux/linux-comm-ls.html)。 + + +2. 内核配置:在Linux内核源码路径下运行`make menuconfig`以进行编译设置。本次实验直接选择Save,然后Exit。 + +3. 编译:在Linux内核源码路径下运行```make -j $((`nproc`-1))```以编译内核。作为参考,助教台式机CPU为i5-7500(4核4线程),使用VitrualBox虚拟机,编译用时5min左右。 + + > 提示: + > + > 1. `nproc`是个shell内置变量,代表CPU核心数。如果虚拟机分配的cpu数只有1(如Hyper-V默认只分配1核),则需先调整虚拟机分配的核心数。 + > 2. 如果你的指令只有`make -j`,后面没有加上处理器核数,那么编译器会无限开多线程。因为Linux内核编译十分复杂,这会直接吃满你的系统资源,导致系统崩溃。 + > 3. `nproc`前的`` `是反引号,位于键盘左上侧。**不**是enter键旁边那个。 + + 若干其他问题及其解决方案: + + > 问题1: 编译内核时遇到 make[1]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'. Stop. + > 解决方案:用文本编辑器(vim 或 gedit)打开 PATH-TO-linux-4.9.263/.config文件, 找到并注释掉包含CONFIG_SYSTEM_TRUSTED_KEY 和 CONFIG_MODULE_SIG_KEY 的两行即可. + > 解决方案链接: https://unix.stackexchange.com/questions/293642/attempting-to-compile-kernel-yieldsa-certification-error + + > 问题2:make menuconfig时遇到以下错误:Your display is too small to run Menuconfig! It must be at least + > 19 lines by 80 columns. + > 解决方案:请阅读报错信息并自行解决该问题。 + +4. 如果编译成功,我们可以在Linux内核源码路径下的`arch/x86_64/boot/`下看到一个`bzImage`文件,这个文件就是内核镜像文件。 + + > 若怀疑编译/安装有问题,可以先在Linux内核源码路径下运行`make clean`之后从1.2.3.1开始。 + +### 1.2.4 编译busybox + +1. 在busybox的源码路径下运行`make menuconfig`以进行编译设置。修改配置如下:(空格键勾选) + + ```shell + Settings –> + Build Options + [*] Build static binary(no share libs) + ``` + +2. 在busybox的源码路径下执行`rm networking/tc.c`指令。 + + > 新版本内核相关结构体定义发生改变,导致tc指令安装会失败,本实验我们无需该指令,所以删除它,再进行编译。 + +3. 编译:在busybox的源码路径下运行``make -j $((`nproc`-1))``以编译busybox。本部分的提示与1.2.3.3相同。 + +4. 安装:在busybox的源码路径下运行``sudo make install``。 + + > 若怀疑编译/安装有问题,可以先在busybox的源码路径下运行`make clean`之后从1.2.4.1开始。 + +### 1.2.5 制作根文件系统 + +1. 将工作目录切换为busybox源码目录下的`_install`目录。 + +2. **使用`sudo`创建**一个名为`dev`的文件夹(提示:参考[Linux mkdir命令](https://www.runoob.com/linux/linux-comm-mkdir.html))。 + +3. 使用以下指令创建`dev/ram`和`dev/console`两个设备文件: + + * `sudo mknod dev/console c 5 1` + * `sudo mknod dev/ram b 1 0 ` + +5. **使用`sudo`创建**一个名为`init`的文件(提示:参考第一部分2.3.16)。**使用文本编辑器编辑文件**,文件内容如下: + + > 特别提示:这一步**不是**让你在终端里执行这些命令。在复制粘贴时,请注意空格、回车是否正确地保留。 + > + > 在命令行下启动gedit会在终端里报warning,忽略即可。[参考链接](https://askubuntu.com/questions/798935/set-document-metadata-failed-when-i-run-sudo-gedit) + + ```shell + #!/bin/sh + echo "INIT SCRIPT" + mkdir /proc + mkdir /sys + mount -t proc none /proc + mount -t sysfs none /sys + mkdir /tmp + mount -t tmpfs none /tmp + echo -e "\nThis boot took $(cut -d' ' -f1 /proc/uptime) seconds\n" + exec /bin/sh + ``` + +6. 赋予`init`执行权限:`sudo chmod +x init` + +7. 将x86-busybox下面的内容打包归档成cpio文件,以供Linux内核做initramfs启动执行: + + `find . -print0 | cpio --null -ov --format=newc | gzip -9 > 你们自己指定的cpio文件路径` + + > 注意1:该命令一定要在busybox的 _install 目录下执行。 + > + > 注意2:每次修改`_install`,都要重新执行该命令。 + > + > 注意3:请自行指定cpio文件名及其路径。示例:`~/oslab/initramfs-busybox-x64.cpio.gz` + > + > 示例指令:`find . -print0 | cpio --null -ov --format=newc | gzip -9 > ~/oslab/initramfs-busybox-x64.cpio.gz` + + +### 1.2.6 运行qemu + +我们本次实验需要用到的qemu指令格式: +`qemu-system-x86_64 [-s] [-S] [-kernel ImagePath] [-initrd InitPath] [--append Para] [-nographic] ` + +| 参数 | 含义 | +| ----------------- | ------------------------------------------------------------ | +| -s | 在1.3.2介绍 | +| -S | 在1.3.2介绍 | +| -kernel ImagePath | 指定系统镜像的路径为ImagePath。在本次实验中,该路径为1.2.4.4所述的bzImage文件路径。 | +| -initrd InitPath | 指定initramfs(1.2.5生成的cpio文件)的路径为InitPath。在本次实验中,该路径为1.2.5.7所述的cpio文件路径。 | +| --append Para | 指定给内核启动赋的参数。 | +| -nographic | 设置无图形化界面启动。 | + +在`--append`参数中,本次实验需要使用以下子参数: + +| 参数 | 含义 | +| ------------- | ------------------------------------------------------------ | +| nokaslr | 关闭内核地址随机化,便于调试。 | +| root=/dev/ram | 指定启动的第一个文件系统为ramdisk。我们在1.2.5.3创建了`/dev/ram`这个设备文件。 | +| init=/init | 指定init进程为/init。我们在1.2.5.4创建了init。 | +| console=ttyS0 | **对于无图形化界面启动**(如WSL),需要使用本参数指定控制台输出位置。 | + +这些参数之间以空格分隔。 + +总结(TL;DR): + +> 运行下述指令前,请注意先**理解指令含义**,**注意换行问题**。 +> +> 如果报错找不到文件,请先检查:**是否把pdf的换行和不必要的空格复制进去了?前几步输出的东西是不是在期望的位置上?** 我们不会提示下述指令里哪些空格/换行符是应有的,哪些是不应有的。请自行对着上面的指令含义排查。 +> 如果报错Failed to execute /init (error -8),建议从1.2.5开始重做。若多次重做仍有问题,建议直接删掉解压好的busybox和Linux源码目录,从3.2.4重做。目前已知init文件配置错误、Linux内核编译出现问题都有可能导致本错误。 + + +* 以图形界面,弹出窗口形式运行内核: + + ```shell + qemu-system-x86_64 -kernel ~/oslab/linux-4.9.263/arch/x86_64/boot/bzImage -initrd ~/oslab/initramfs-busybox-x64.cpio.gz --append "nokaslr root=/dev/ram init=/init" + ``` + +* Ubuntu 20.04/20.10 环境下如果出现问题,可执行以下指令: + + ```shell + qemu-system-x86_64 -kernel ~/oslab/linux-4.9.263/arch/x86/boot/bzImage -initrd ~/oslab/initramfs-busybox-x64.cpio.gz --append "nokaslr root=/dev/ram init=/init" + ``` + +* 如不希望qemu以图形界面启动,希望以无界面形式启动(如WSL),输出重定向到当前shell,使用以下命令: + + ```shell + qemu-system-x86_64 -kernel ~/oslab/linux-4.9.263/arch/x86_64/boot/bzImage -initrd ~/oslab/initramfs-busybox-x64.cpio.gz --append "nokaslr root=/dev/ram init=/init console=ttyS0 " -nographic + ``` + +其他常见问题: + +1. 如何检查运行是否正确? + + **弹出的黑色窗口就是qemu**。因为系统内核在启动的时候会输出一些log,所以qemu界面里偶尔会蹦出一两条log是正常的,** 只要这些log不是诸如"kernel panic"之类的报错 **即可。建议尝试输入一条命令,比如在弹出的窗口里面输入`ls`回车,如果能够显示相关的文件列表,即说明运行正确。 + +2. 鼠标不见了,该怎么办? + + 请观察窗口上方的标题栏,"Press (请自行观察标题栏上的说明) to release grab" + +3. 如何关闭qemu? + + 对于图形化界面,直接点击右上角的叉就行了。对于非图形化界面,请先按下`Ctrl+A`,松开这两个键之后再按键盘上的`X`键。 + + + +## 1.3 使用 gdb调试内核 + +gdb是一款命令行下常用的调试工具,可以用来打断点、显示变量的内存地址,以及内存地址中的数据等。使用方法是`gdb 可执行文件名`,即可在gdb下调试某二进制文件。 + +一般在使用gcc等编译器编译程序的时候,编译器不会把调试信息放进可执行文件里,进而导致gdb知道某段内存里有内容,但并不知道这些内容是变量a还是变量b。或者,gdb知道运行了若干机器指令,但不知道这些机器指令对应哪些C语言代码。所以,在使用gdb时需要在编译时加入`-g`选项,如:`gcc -g -o test test.c`来将调试信息加入可执行文件。而Linux内核采取了另一种方式:它把符号表独立成了另一个文件,在调试的时候载入符号表文件就可以达到相同的效果。 + +* gdb里的常用命令 + + ``` shell + r/run # 开始执行程序 + b/break # 在location处添加断点,location可以是代码行数或函数名 + b/break if # 在location处添加断点,仅当condition条件满足才中断运行 + c/continue # 继续执行到下一个断点或程序结束 + n/next # 运行下一行代码,如果遇到函数调用直接跳到调用结束 + s/step # 运行下一行代码,如果遇到函数调用则进入函数内部逐行执行 + ni/nexti # 类似next,运行下一行汇编代码(一行c代码可能对应多行汇编代码) + si/stepi # 类似step,运行下一行汇编代码 + l/list # 显示当前行代码 + p/print # 查看表达式expression的值 + q # 退出gdb + ``` + +### 1.3.1 安装 gdb + +使用包管理器安装名为gdb的包即可。 + +### 1.3.2 启动 gdb server + +使用1.2.6所述指令运行qemu。但需要加上`-s`和`-S`两个参数。这两个参数的含义如下: + +| 参数 | 含义 | +| ---- | ------------------------------------------------------------ | +| -s | 启动gdb server调试内核,server端口是1234。若不想使用1234端口,则可以使用`-gdb tcp:xxxx`来取代此选项。 | +| -S | 若使用本参数,在qemu刚运行时,CPU是停止的。你需要在gdb里面使用c来手动开始运行内核。 | + +### 1.3.3 建立连接 + +**另开**一个终端,运行gdb,然后在gdb界面里运行如下命令: + +```shell +target remote:1234 # 建立gdb与gdb server间的连接。这时候我们看到输出带有??,还报了一条warning + # 这是因为没有加载符号表,gdb不知道运行的是什么代码。 +c # 手动开始运行内核。执行完这句后,你应该能发现旁边的qemu开始运行了。 +q # 退出gdb。 +``` + +### 1.3.4 重新编译Linux内核使其携带调试信息 + +刚才因为没有加载符号表,所以gdb不知道运行了什么代码。所以我们要重新编译Linux来使其携带调试信息。 + +1. 进入Linux源码路径。 + +2. 执行下列语句(这条语句没有回车,注意文档显示时产生的额外换行问题): + `./scripts/config -e DEBUG_INFO -e GDB_SCRIPTS -d DEBUG_INFO_REDUCED -d DEBUG_INFO_SPLIT -d DEBUG_INFO_DWARF4` + +3. 重新编译内核。``make -j $((`nproc`-1))`` + + 作为参考,助教台式机CPU为i5-7500(4核4线程),编译用时8.5min左右。 + +### 1.3.5 加载符号表、设置断点 + +1. 重新执行1.3.2. + +2. **另开**一个终端,运行gdb,然后在gdb界面里运行如下命令: + + ```shell + target remote:1234 # 建立gdb与gdb server间的连接。这时候我们看到输出带有??, + # 这是因为没有加载符号表,gdb不知道运行的是什么代码。 + file Linux源码路径/vmlinux # 加载符号表。(不要把左边的东西原封不动地复制过来,这一步可能会有warning,属于正常现象,请忽略) + n # 单步运行。此时可以看到右边不是??,而是具体的函数名了。 + break start_kernel # 设置断点在start_kernel函数。 + c # 运行到断点。 + l # 查看断点代码。 + p init_task # 查看断点处名为"init_task"的变量值。这个变量是个结构体,里面有一大堆成员。 + ``` + + + +
+ + + +# 章节二 编写系统调用实现一个Linux strace + +## 2.0 如何阅读Linux源码 + +因为直接在VSCode、Visual Studio等本地软件中阅读Linux源码会让电脑运行速度变得很慢,所以我们建议使用在线阅读。一个在线阅读站是[https://elixir.bootlin.com/linux/v4.9.263/source](https://elixir.bootlin.com/linux/v4.9.263/source)。左栏可以选择内核源码版本,右边是目录树或代码,右上角有搜索框。下面举例描述一次查阅Linux源码的过程。 + +假定我们要查询Linux中进程名最长有多长。首先,我们知道进程在内存中的数据结构是结构体 `task_struct`,所以我们首先查找 `task_struct`在哪。搜索发现在 `include/linux/sched.h`。(下面那个从名字来看像个附属模块,所以大概可以确定结构体的声明应该在 `sched.h`。) + +pic-3.0.1 + +进入 `sched.h`查看 `task_struct`的声明。我们不难猜到,进程名应该是个一维char数组。所以我们开始找 `char`数组。看了几十个成员之后,我们找到了 `char comm[TASK_COMM_LEN]`这个东西。右边注释写着 `executable name excluding path`,应该就是这个了。所以它的长度就应该是 `TASK_COMM_LEN`。 + +pic-3.0.2 + +于是我们点击一下这个 `TASK_COMM_LEN`,找到这个宏在哪定义。发现定义也在 `sched.h`。点开就能看到宏定义了。至于它的长度到底是多少,留作习题,给大家自己实践查询。 + + + +## 2.1 若干名词解释 + +### 2.1.1 用户空间、内核空间 + +- **用户空间**:用户空间是操作系统为**用户级应用程序**分配的虚拟内存区域。应用程序(如文本编辑器、浏览器等)在此空间中运行,仅能访问受限的资源和内存。 +- **内核空间**:内核空间是操作系统**内核及核心功能**运行的虚拟内存区域,负责硬件管理、进程调度、内存分配等核心任务。 + +### 2.1.2 系统调用 + +**系统调用 (System call, Syscall)**是操作系统提供给**用户程序访问内核空间的合法接口**。 + +系统调用**运行于内核空间,可以被用户空间调用**,是内核空间和用户空间划分的关键所在,它保证了两个空间必要的联系。从用户空间来看,系统调用是一组统一的抽象接口,用户程序**无需考虑接口下面是什么**;从内核空间来看,用户程序不能直接进行敏感操作,所有对内核的操作都必须通过功能受限的接口间接完成,保证了**内核空间的稳定和安全**。 + +> 我们平时的编程中**为什么没有意识到系统调用的存在**?这是因为应用程序现在一般**通过应用编程接口 (API)来间接使用系统调用**,比如我们如果想要C程序打印内容到终端,只需要使用C的标准库函数API `printf()`就可以了,而不是使用系统调用 `write()` 将内容写到终端的输出文件 (`STDOUT_FILENO`) 中。 + +### 2.1.3 glibc + +glibc是GNU发布的c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现,其内容包罗万象。glibc内含的档案群分散于系统的目录树中,像支架一般撑起整个操作系统。 + +`stdio.h`, `malloc.h`, `unistd.h`等都是glibc实现的封装。 + +## 2.2 系统调用是如何执行的 + +1. 调用应用程序调用**库函数(API)** + Linux使用的开源标准C运行库glibc(GNU libc)有一个头文件 `unistd.h`,其中声明了很多**封装好的**系统调用函数,如 `read/write`。这些API会调用实际的系统调用。 + +2. API将**系统调用号存入**EAX,然后**触发软中断**使系统进入内核空间。 + 32位x86机器使用汇编代码 `int $0x80`触发中断。具体如何触发软中断可以去参考Linux的实现源码(如 `arch/x86/entry/entry_32.S`)。因为这部分涉及触发中断的汇编指令,这部分代码是用汇编语言写的。 + + > 对于64位x86机器,现在往往使用syscall来触发中断,具体如何触发软中断可以去参考Linux的实现源码(如 `arch/x86/entry/entry_64.S`重点关注该文件的144行和157行两个处理逻辑,第一段为保存用户空间的指针,紧接着 `call do_syscall_64`即为真正开始执行内核态系统调用) + +3. 内核的中断处理函数根据系统调用号,调用对应的**内核函数**(也就是课上讲的的系统调用)。前面的 `read/write`,实际上就是调用了内核函数 `sys_read/sys_write` 。(对于64位机器,即进入 `do_syscall_64`函数,该函数在 `arch/x86/entry/common.c`中定义,并且最后使用**syscall_return_slowpath(regs)**函数来返回) + + > 总结:添加系统调用需要**注册中断处理函数和对应的调用号**,内核才能找到这个系统调用,并执行对应的内核函数。 + +4. 内核函数完成相应功能,将返回值存入EAX,返回到中断处理函数; + + > 注1:系统执行相应功能,调用的是C代码编写的函数,而不是汇编代码,减轻了实现系统调用的负担。系统调用的函数定义在 `include/linux/syscalls.h`中。因为汇编代码到C代码的参数传递是通过栈实现的,所以,所有系统调用的函数前面都使用了 `asmlinkage` 宏,它意味着编译时限制只使用栈来传递参数。如果不这么做,用汇编代码调用C函数时可能会产生异常。 + > 注2:用户空间和内核空间各自的地址是不能直接互相访问的,需要借助函数拷贝来实现数据传递,后面会说明。 + +5. 中断处理函数返回到API中; + + > 返回前执行**syscall_return_slowpath(regs)**函数,该函数用于处理内核中的状态,参数 `regs`中包含执行的系统调用号(`regs->orig_ax`),以及系统调用返回值(`regs->ax`)。执行该函数的进程的pid可以通过task得到(`task->pid`) + +6. API将EAX返回给应用程序。 + + > 注:当glibc库没有封装某个系统调用时,我们就没办法通过使用封装好的API来调用该系统调用,而使用 `int $0x80`触发中断又需要进行汇编代码的编写,这两种方法都不适合我们在添加过新系统调用后再编写测试代码去调用。因此我们后面采用的是第三种方法,使用glibc提供的 `syscall`库函数,这个库函数只需要传入调用号和对应内核函数要用到的参数,就可以直接调用我们新写的系统调用。具体详见2.4.1. + +## 2.3 添加系统调用的流程 + +> Linux 4.9的文档 `linux-4.9.263/Documentation/adding-syscalls.txt`中有说明如何添加一个系统调用。本实验文档假设所有同学使用的平台都是x86,采用的是x86平台的系统调用添加方法。其他平台的同学可以参考上述文档给出的其他平台系统调用添加方法。 + +在实现系统调用之前,要先考虑好添加系统调用的函数原型,确定函数名称、要传入的参数个数和类型、返回值的意义。在实际设计中,还要考虑加入系统调用的必要性。 + +我们这里以一个统计系统中进程个数的系统调用 `ps_counter(int *num)` 作为演示,num是返回值对应的地址。 + +> 因为系统调用的返回值表示系统调用的完成状态(0是正常,其他值是异常,代表错误编号),所以不建议用返回值传递信息。 + +本节的各步是没有严格顺序的,但在编译前都要完成: + +### 2.3.1 注册系统调用 + +内核的汇编代码会在 `arch/x86/include/generated/asm/syscalls_64.h` 中查找调用号。为便于添加系统调用,x86平台提供了一个专门用来注册系统调用的文件 `/arch/x86/entry/syscalls/syscall_64.tbl`。在编译时,脚本 `arch/x86/entry/syscalls/syscalltbl.sh`会被运行,将上述 `syscall_64.tbl`文件中登记过的系统调用都生成到前面的 `syscalls_64.h`文件中。因此我们需要修改 `syscall_64.tbl`。 + +打开 `linux源码路径/arch/x86/entry/syscalls/syscall_64.tbl` 。 + +``` +# linux-4.9.263/arch/x86/entry/syscalls/syscall_64.tbl +# ... 前面还有,但没展示 +329 common pkey_mprotect sys_pkey_mprotect +330 common pkey_alloc sys_pkey_alloc +331 common pkey_free sys_pkey_free +# 以上是系统自带的,不是我写的,下面这个是 +332 common ps_counter sys_ps_counter +``` + +这个文件是**x86平台下的系统调用注册表**,记录了很多数字与函数名的映射关系。我们在这个文件中注册系统调用。 +每一行从左到右各字段的含义,及填写方法是: + +- 系统调用号:请为你的系统调用找一个没有用过的数字作为调用号(记住调用号,后面要用) +- 调用类型:本次实验请选择common(含义为:x86_64和x32都适用) +- 系统调用名xxx +- 内核函数名sys_xxx + +> 提示:为保持代码美观,每一列用制表符排得很整齐。但经本人测试,无论你是用制表符还是用空格分隔,无论你用多少空格或制表符,只要你把每一列分开了,无论是否整齐,对这一步的完成都没有影响。 + +### 2.3.2 声明内核函数原型 + +打开 `linux-4.9.263/include/linux/syscalls.h`,里面是对于系统调用函数原型的定义,在最后面加上我们创建的新的系统调用函数原型,格式为 `asmlinkage long sys_xxx(...)` 。注意,如果传入了用户空间的地址,需要加入 `__user`宏来说明。 + +```c +asmlinkage long sys_mlock2(unsigned long start, size_t len, int flags); +asmlinkage long sys_pkey_mprotect(unsigned long start, size_t len, + unsigned long prot, int pkey); +asmlinkage long sys_pkey_alloc(unsigned long flags, unsigned long init_val); +asmlinkage long sys_pkey_free(int pkey); +//这里是我们新增的系统调用 +asmlinkage long sys_ps_counter(int __user * num); + +#endif +``` + +### 2.3.3 实现内核函数 + +你可以在 `linux-4.9.264/kernel/sys.c` 代码的最后添加你自己的函数,这个文件中有很多已经实现的系统调用的函数作为参考。我们给出了一段示例代码: + +```c +SYSCALL_DEFINE1(ps_counter, int __user *, num){ + struct task_struct* task; + int counter = 0; + printk("[Syscall] ps_counter\n"); + for_each_process(task){ + counter ++; + } + copy_to_user(num, &counter, sizeof(int)); + return 0; +} +``` + +以下是这段代码的解释: + +1. 使用宏 `SYSCALL_DEFINEx`来简化实现的过程 ,其中x代表参数的个数。传入宏的参数为:调用名(不带sys_)、以及每个参数的类型、名称(注意这里输入时为两项)。 + + 以我们现在的 `sys_ps_counter`为例。因为使用了一个参数,所以定义语句是 `SYSCALL_DEFINE1(ps_counter, int *, num)`。如果该系统调用有两个参数,那就应该是 `SYSCALL_DEFINE2(ps_counter, 参数1的类型, 参数1的名称, 参数2的类型, 参数2的名称)`,以此类推。 + + > 无法通过 `SYSCALL_DEFINEx`定义二维数组(如 `char (*p)[50]`)为参数。你可以尝试定义一个,然后结合编译器的报错信息分析该宏的实现原理,并分析不能定义的原因。 + +2. `printk()`是内核中实现的print函数,和 `printf()`的使用方式相同,`printk()`会将信息写入到内核日志中。尤其地,在qemu下调试内核时,系统日志会直接打印到屏幕上,所以我们可以直接在屏幕上看到 `printk`打印出的内容。`printf()`是使用了C的标准库函数的时候才能使用的,而内核中无法使用标准库函数。你不能 `#include`,自然不能用 `printf()`。 + + > 在使用 `printk`时,行首输出的时间等信息是去不掉的。 + +3. 为了获取当前的所有进程,我们使用了宏函数 `for_each_process(p)`(定义在 `include/linux/sched.h`中),遍历当前所有任务的信息结构体 `task_struct`(同样定义在 `include/linux/sched.h`中),并将地址赋值给参数 `p`。后面实验内容中我们会进一步用到 `task_struct`中的成员变量来获取更多相关信息,注意我们暂时不讨论多线程的场景,每一个 `task_struct`对应的可以认为就是一个进程。 + +4. 前面说到,系统调用是在内核空间中执行,所以如果我们要将在内核空间获取的值传递给用户空间,需要使用函数 `copy_to_user(void __user *to, const void *from, unsigned long n)` 来完成从内核空间到用户空间的复制。 其中,`from` 和 `to` 是复制的来源和目标地址,`n`是复制的大小,我们这里就是 `sizeof(int)`。 + +## 2.4 测试 + +> 若想使用VS Code调试Linux内核,可以参考此文章:[https://zhuanlan.zhihu.com/p/105069730](https://zhuanlan.zhihu.com/p/105069730),直接从“VS Code配置”一章阅读即可。实际操作中,你可能还需要在VS Code中安装"C/C++"和"C/C++ Extension Pack"插件。 + +### 2.4.1 编写测试代码 + +在你的Linux环境下(不是打开qemu后弹出的那个)编写测试代码。我们在这里命名其为 `get_ps_num.c`。新增的系统调用可以使用 `long int syscall (long int sysno, ...)` 来触发,`sysno`就是我们前面添加的调用号,后面跟着要传入的参数,下面是一个简单的测试代码样例: + +```c +#include +#include +#include +int main(void) +{ + int result; + syscall(332, &result); + printf("process number is %d\n",result); + return 0; +} +``` + +> 提示: +> +> - 这里的测试代码是用户态代码,不是内核态代码,所以可以使用 `printf`。 +> - 为使用系统调用号调用系统调用,需要使用 `sys/syscall.h`内的 `syscall` 函数。 +> - 我们推荐使用C语言而不是C++,因为C++在下一步(3.4.2)的静态编译时容易出现问题。使用C语言时需要注意C和C++之间的语法差异。 + +### 2.4.2 编译 + +使用gcc编译器编译 `get_ps_num.c`。**本次实验需要使用静态编译(`-static`选项)**。要用到的gcc指令原型是:`gcc [-static] [-o outfile] infile`。 + +> 不使用静态编译会导致在qemu内运行测试程序时报错找不到文件,因为找不到链接库。 + +### 2.4.3 运行测试程序 + +1. 将2.4.2编译出的可执行文件 `get_ps_num`放到 `busybox-1.32.1/_install`下面,重新制作initramfs文件(重新执行1.2.5.6的find操作),这样我们才能在qemu中看见编译好的 `get_ps_num`可执行文件。 + + > 注意:因为_install文件夹只有root用户才有修改权限,所以复制文件应该在命令行下使用 `sudo`。 + +2. 重新编译linux源码(重新执行1.2.3.3,无需重新make menuconfig) + +3. 运行qemu(重新执行1.2.6,我们在这里不要求使用gdb调试) + +4. 在qemu中运行 `get_ps_num`程序 ,得到当前的进程数量(这个进程数量是包括 `get_ps_num`程序本身的,下图中,除了 `get_ps_num`本身外,还有55个进程)。 + +5. 验证:使用 `ps aux | wc -l` 获取当前进程数。但这个输出的结果与上一步中得到的不同。请自己思考数字不同的原因。 + + > 在实际操作中,系统中的进程数量可能不是55。只要与 `ps aux | wc -l`得到的结果匹配即可。 + +2022-03-29 16-10-49 的屏幕截图.png + +## 2.5 附加知识:正则表达式 & strace命令 + +> 本部分在后续的实验中会用到,请不要跳过 + +### 2.5.1 正则表达式 + +> 参考链接:https://www.runoob.com/regexp/regexp-syntax.html + +正则表达式,又称规则表达式、常规表示法(Regular Expression,简写为 regex、regexp 或 RE),是计算机科学领域中对字符串操作的一种逻辑公式。它由事先定义好的特定字符及这些字符的组合构成一个 “规则字符串”,以此来表达对其他字符串的过滤逻辑,通常用于查找、替换符合特定特征的字符串。(如果你使用的IDE是vscode,Ctrl-f后,查找框最后一个选项即使用正则表达式匹配) + +基本语法: + +| 符号 | 说明 | 示例 | +| ------ | ------------------------------ | ------------------------ | +| . | 匹配任意单个字符(除换行符) | `a.c` → “abc”、“a1c” | +| \d | 匹配数字(等价于 [0-9]) | `\d\d` → “12”、“99” | +| \w | 匹配字母、数字、下划线 | `\w+` → “hello_123” | +| \s | 匹配空白字符(空格、制表符等) | `\s+` → 匹配连续空格 | +| ^ | 匹配字符串开头 | `^abc` → 以 “abc” 开头 | +| $ | 匹配字符串结尾 | `xyz$` → 以 “xyz” 结尾 | +| ^ /$ | 字符串开始 / 结束 | `^start / end$` | +| [abc] | 匹配括号内的任意字符 | `[aeiou]` → 匹配元音字母 | +| [^abc] | 匹配不在括号内的任意字符 | `[^0-9]` → 非数字字符 | + +举个栗子🌰: + +如果想要找系统调用中含有'read'的,例如('readlinkat','read','readv'等),则直接使用正则表达式 `read`即可匹配。 + +如果想要完全匹配,即只想找'read'系统调用,则使用正则表达式 `\bread\b`(\b表示单词边界,更多的语法规则自行google) + +如果想要完全匹配'read'或者'write',即只想过滤这两个系统调用,则可以使用正则表达式 `\bread\b|\bwrite\b`('|'表示选择匹配,只要匹配其中之一即可) + +> 本实验将会使用到c语言的正则表达式库 `regex.h`,该库在各个Linux发行版中存在,但是在内核中不存在,所以不能在内核中使用该头文件,使用方法主要分为两个步骤 +> +> - 1.编译正则表达式(代码已给出) +> +> ```c +> if (regcomp(®ex, argv[1], REG_EXTENDED) != 0) { +> fprintf(stderr, "Invalid regex: %s\n", argv[1]); +> exit(1); +> } +> ``` +> +> - 2.匹配正则表达式,使用第一步得到的regex,若下面函数执行返回值为0,即 `need_judge`字符串符合pattern,即被匹配到 +> +> ```c +> regexec(®ex, need_judge, 0, NULL, 0) +> ``` +> +> 后续编译原理课程将会系统学习该工具,上面的例子足够完成本次实验,不再赘述 + +### 2.5.2 strace命令 + +> 参考链接:https://www.cnblogs.com/machangwei-8/p/10388883.html +> +> 下载strace使用命令 `sudo apt install strace` + +在Linux系统中,strace命令是一个集诊断、调试、统计与一体的工具,可用来追踪调试程序。(其使用ptrace系统调用来实现) + +举个栗子🌰: +从其他机器copy了一个程序,但是运行直接报错,没有源码的情况下,怎么判断哪里出错了?(反汇编太麻烦了😭) + +使用strace追踪其使用的系统调用流程,可以看到在报错信息的那一行,到底是哪个系统调用,其执行的参数是什么,根据该信息可以进一步查找问题 + +> 本次实验将添加类似ptrace系统调用,并且完成一个简单的strace程序 + +## 2.6 任务目标 + +- 在linux4.9下创建适当的(可以是一个或多个)系统调用。利用新实现的系统调用,实现在Linux4.9下,追踪程序使用哪些系统调用(使用正则表达式过滤),参考命令 `strace`(注意,本实验需要关注后续的 `实验提示`!!!!): + +```shell +$ strace ls +execve("/usr/bin/ls", ["ls"], 0x7ffd025c7530 /* 50 vars */) = 0 +brk(NULL) = 0x555996f2b000 +arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd825cdf80) = -1 EINVAL (Invalid argument) +mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f48990c4000 +access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) +.......此处省略 +close(1) = 0 +close(2) = 0 +exit_group(0) = ? ++++ exited with 0 +++ +``` + +- 输出的信息需要包括: + + - 进程的PID + - 需要追踪的系统调用名称 + - 需要追踪的系统调用返回值 +- 具体评分规则见3.3。 我们不限制大家创建系统调用的数量、功能,也不限制测试程序的实现。能完成实验内容即可。 +- 下面是示例输出(由于是内核态打印的信息,所以前面[]中的数字不定)。 + +``` +$ ./trace "." ls +[ 216.310823] 986: syscall execve -> 0 +[ 216.312476] 986: syscall brk -> 11907072 +[ 216.312911] 986: syscall brk -> 11910464 +[ 216.313688] 986: syscall arch_prctl -> 0 +[ 216.314377] 986: syscall set_tid_address -> 986 +[ 216.314805] 986: syscall set_robust_list -> 0 +[ 216.315553] 986: syscall prlimit64 -> 0 +[ 216.316537] 986: syscall readlinkat -> 12 +[ 216.316996] 986: syscall getrandom -> -11 +.... +[ 216.331815] 986: syscall close -> 0 +[ 216.332365] 986: syscall fstat -> 0 +[ 216.332715] 986: syscall ioctl -> 0 +bin init proc sbin tmp usr +[ 216.334948] 986: syscall write -> 109 +dev linuxrc root sys trace +[ 216.336595] 986: syscall write -> 92 +$ ./trace "\bfstat\b" ls +[ 5348.862758] 997: syscall fstat -> 0 +[ 5348.864303] 997: syscall fstat -> 0 +bin init proc sbin tmp usr +dev linuxrc root sys trace +``` + +## 2.7 任务提示 + +- 在系统调用的内核函数中,如果要得到当前进程的进程结构体,可以使用`struct task_struct *task = current;` + +- 实验需要修改进程结构体 `task_struct`(定义在 `include/linux/sched.h`),需要添加变量(用于判断该进程是否需要被追踪)和数组(用于记录需要追踪的系统调用号) + +- 实验需要修改系统调用返回函数 `syscall_return_slowpath`(定义在 `arch/x86/entry/common.c`),当某个系统调用执行完毕后,是否需要打印信息到终端?系统调用号、其返回值,以及进程pid获得方法查看2.2节系统调用的执行流程。 + +- 修改系统调用返回函数( `syscall_return_slowpath`)时,可能会用到 `trace.c`中提供的系统调用表 `syscall_names`,自行复制到该函数之前即可。 + +- 实验需要补全 `trace.c`文件,其用于对正则表达式进行判断需要追踪哪些系统调用并传递给内核,不在 `syscall_names`中的系统调用不进行追踪。 + +- 追踪到的系统调用信息直接在内核打印即可,在内核打印使用什么函数? + +- 如果需要将用户空间的变量复制到内核空间(例如解析得到的需要追踪的系统调用号),用 `copy_from_user(void *to, const void __user *from, unsigned long n)`。 + +- 内核使用for循环不允许直接定义变量,例如 `for(int i = 0; i < cnt; i++)`,需要先定义i,然后进行使用 + +- 内核空间对栈内存(如局部变量等)的使用有较大限制。你可能无法在内核态开较长的数组作为局部变量。但本次实验的预期解法不使用很多的内核态栈内存。 + +- 本次实验无需在内核态动态分配空间,如有需求请自行查询实现方法。 + +- **如果访问数组越界,会出现很多奇怪的错误(如程序运行时报malloc错误、段错误、输出其他数组的值或一段乱码(注意,烫烫烫是Visual C的特性,Linux下没有烫烫烫)、其他数组的值被改变等)。提问前请先排查是否出现了此类问题。** + +- 一些常见错误: + + - 拼写错误,如 `asmlkage`、`commom`、`__uesr`、中文逗号 + - 语法错误,如 `sizeof(16 * int)` + - C标准问题,如C语言声明结构体、算 `sizeof`时应该用 `struct xxx`而不是直接 `xxx` + + 上述错误均可以通过阅读编译器报错信息查出。 + +
+ +# 章节三: 检查要求 + +本部分实验无实验报告。 + +本部分实验共11分。 + +## 3.1 第三部分检查要求(5') + +现场能启动虚拟机并启动gdb调试,即现场执行1.3.5,共5分。本部分检查中,允许学生对照实验文档操作。 + +* 你需要能够简要解释符号表的作用。若不能解释,本部分减1分。 +* 此外,**为提高检查效率,请自己编写一个【shell脚本】启动qemu(即,使用Part2 1.3所述方法完成本部分1.3.5的第一步)。** 若不能使用脚本启动qemu,本部分减1分。你不需要使用脚本启动或操作gdb。 + + + +## 3.2 知识问答 (2') + +我们会使用随机数发生器随机地从下面题库中抽三道题,每回答正确一题得一分,满分2分。也就是说,允许答错一题。请按照自己的理解答题,答题时不准念实验文档或念提前准备的答案稿。 +下面是题库: + +1. 解释 `wc`和 `grep`指令的含义。 +2. 解释 `ps aux | grep firefox | wc -l`的含义。 +3. `echo aaa | echo bbb | echo ccc`是否适合做shell实验中管道符的检查用例?说明原因。 +4. 对于匿名管道,如果写端不关闭,并且不写,读端会怎样? +5. 对于匿名管道,如果读端关闭,但写端仍尝试写入,写端会怎样? +6. 假如使用匿名管道从父进程向子进程传输数据,这时子进程不写数据,为什么子进程要关闭管道的写端? +7. fork之后,是管道从一分为二,变成两根管道了吗?如果不是,复制的是什么? +8. 解释系统调用 `dup2`的作用。 +9. 什么是shell内置指令,为什么不能fork一个子进程然后 `exec cd`? +10. 为什么 `ps aux | wc -l`得出的结果比 `get_ps_num`多2? +11. 进程名的最大长度是多少?这个长度在哪定义? +12. strace可以追踪通过fork创建的子进程的系统调用,fork系统调用使用了哪个函数来实现这一行为?(提示:fork使用的函数定义在 `linux/fork.c:copy_procss`中) +13. 找到你的学号的后两位对应的64位机器的系统调用,并说明该系统调用的功能(例如学号为PB23111678,则查看78对应的系统调用)。 +14. 在修改内核代码的时候,能用 `printf`调试吗?如果不能,应该用什么调试? +15. `read()、write()、dup2()`都能直接调用。现在我们已经写好了一个名为ps_counter的系统调用。为什么我们不能在测试代码中直接调 `ps_counter()`来调用系统调用? + +大家可以自己思考或互相讨论这些题目的答案(但不得在本课程群内公布答案)。助教不会公布这些题目的答案。 + + + +## 3.3 “编写系统调用”部分检查标准 (4') + +- 你的程序可以追踪系统调用,即可以打印所有系统调用执行信息以及进程pid。本部分共1分。 +- 你的程序可以过滤追踪部分系统调用,即可以指定打印部分系统调用执行信息以及进程pid。本部分共1分。 +- 你需要在现场阅读Linux源码,展示pid的数据类型是什么(即,透过多层 `typedef`找到真正的pid数据类型)。本部分共1分。 - 你需要流畅地展示你修改了哪些文件的代码,允许对着实验文档描述(即,现场讲解代码)。本部分共1分。若未能完成任何一个功能,则该部分分数无效。 \ No newline at end of file diff --git a/docs/todo.md b/docs/todo.md index 8a0b5ea..af6141d 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,15 +1,15 @@ -# TODO Lists - -## 文档内容优化 - -* 修改各文档中的图片文件名,使其和内容相符。 -* 修复文档中各种图片不存在和链接错误等。 - - -## 网站和文档生成 - -* 使用主题(Material for MkDocs)。 -* 增加转换为 PDF 的脚本(pandoc)。 - * 可考虑其它转为PDF的方式,以统一语法和格式?例如[print-site](https://github.com/timvink/mkdocs-print-site-plugin)。 - * 或者:[mkdocs-exporter](https://github.com/adrienbrignon/mkdocs-exporter),后者好像好看一些。 -* 增加评论功能([giscus](https://giscus.app/zh-CN))。 +# TODO Lists + +## 文档内容优化 + +* 修改各文档中的图片文件名,使其和内容相符。 +* 修复文档中各种图片不存在和链接错误等。 + + +## 网站和文档生成 + +* 使用主题(Material for MkDocs)。 +* 增加转换为 PDF 的脚本(pandoc)。 + * 可考虑其它转为PDF的方式,以统一语法和格式?例如[print-site](https://github.com/timvink/mkdocs-print-site-plugin)。 + * 或者:[mkdocs-exporter](https://github.com/adrienbrignon/mkdocs-exporter),后者好像好看一些。 +* 增加评论功能([giscus](https://giscus.app/zh-CN))。 diff --git a/mkdocs.yml b/mkdocs.yml index d2e268d..46e09de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,27 +1,27 @@ -site_name: "USTC OS Docs" -site_description: "中国科学技术大学操作系统课程实验文档" -site_author: "USTC OS Team" - -use_directory_urls: false - -theme: - name: 'material' - language: 'zh' - -markdown_extensions: - - admonition - - markdown_gfm_admonition - - pymdownx.details - - pymdownx.superfences - -nav: - - 简介: index.md - - 实验一: - - "Part1 环境准备": vmlab/vmlab.md - - "Part2 实现Shell": shelllab/shellab.md - - "Part3 实现系统调用": syscalllab/syscalllab.md - - 实验二: malloclab/malloclab.md - - 实验三: - - "Part1 开源鸿蒙初探": ohlab/ohlab-part1.md - - "Part2 端侧推理应用实现": ohlab/ohlab-part2.md - - "附录": ohlab/appendix.md +site_name: "USTC OS Docs" +site_description: "中国科学技术大学操作系统课程实验文档" +site_author: "USTC OS Team" + +use_directory_urls: false + +theme: + name: 'material' + language: 'zh' + +markdown_extensions: + - admonition + - markdown_gfm_admonition + - pymdownx.details + - pymdownx.superfences + +nav: + - 简介: index.md + - 实验一: + - "Part1 环境准备": vmlab/vmlab.md + - "Part2 实现Shell": shelllab/shellab.md + - "Part3 实现系统调用": syscalllab/syscalllab.md + - 实验二: malloclab/malloclab.md + - 实验三: + - "Part1 开源鸿蒙初探": ohlab/ohlab-part1.md + - "Part2 端侧推理应用实现": ohlab/ohlab-part2.md + - "附录": ohlab/appendix.md diff --git a/requirements.txt b/requirements.txt index 47a5d14..eebef36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -mkdocs -mkdocs-material +mkdocs +mkdocs-material markdown-gfm-admonition \ No newline at end of file diff --git a/workflows/deploy.yml b/workflows/deploy.yml new file mode 100644 index 0000000..638d9cf --- /dev/null +++ b/workflows/deploy.yml @@ -0,0 +1,26 @@ +name: Deploy MkDocs site +on: + push: + branches: + - main # ͵ main ֧ʱ + +jobs: + build-and-deploy: + runs-on: ubuntu-latest # ʹ Ubuntu ¾ + + steps: + - name: Checkout repo + uses: actions/checkout@v4 # ȡֿ:contentReference[oaicite:3]{index=3} + + - name: Setup Python + uses: actions/setup-python@v5 # Python :contentReference[oaicite:4]{index=4} + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install mkdocs mkdocs-material # װ MkDocs :contentReference[oaicite:5]{index=5} + + - name: Deploy to GitHub Pages + run: mkdocs gh-deploy --force --clean # ͵ gh-pages ֧:contentReference[oaicite:6]{index=6} + env: + CI: true # CI From 060d5634d7bca8c90a69fff5661391fbd0bc4900 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 22:09:12 +0800 Subject: [PATCH 02/13] Update --- docs/todo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/todo.md b/docs/todo.md index af6141d..08c671d 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -13,3 +13,4 @@ * 可考虑其它转为PDF的方式,以统一语法和格式?例如[print-site](https://github.com/timvink/mkdocs-print-site-plugin)。 * 或者:[mkdocs-exporter](https://github.com/adrienbrignon/mkdocs-exporter),后者好像好看一些。 * 增加评论功能([giscus](https://giscus.app/zh-CN))。 +* 嘻嘻 From ae8d755e224f0fc10433b7c9eadf4d1777bd42d3 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 22:10:46 +0800 Subject: [PATCH 03/13] Update --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 9a776d1..91436c9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,4 +4,5 @@ 临时搭出来的网站,目前还很不完善,请大家包涵。 -请以课程主页上的信息为准:[课程主页](http://staff.ustc.edu.cn/~ykli/os2025/) \ No newline at end of file +请以课程主页上的信息为准:[课程主页](http://staff.ustc.edu.cn/~ykli/os2025/) +嘻嘻 \ No newline at end of file From d62c382d6c9d341d3d79197d2604a0f91d8c21b3 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 22:22:19 +0800 Subject: [PATCH 04/13] ignore workflows --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3bf8003..8483f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/site/ \ No newline at end of file +/site/ +/workflows/ \ No newline at end of file From a596a265d921d63a283e5bc235ad8439ea0a85f8 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 22:35:01 +0800 Subject: [PATCH 05/13] new --- .gitignore | 2 +- mkdocs.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8483f7c..c32191b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /site/ -/workflows/ \ No newline at end of file +# /workflows/ \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 46e09de..008821b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,3 +25,5 @@ nav: - "Part1 开源鸿蒙初探": ohlab/ohlab-part1.md - "Part2 端侧推理应用实现": ohlab/ohlab-part2.md - "附录": ohlab/appendix.md + +dev_addr: 127.0.0.1:9999 From 831751642521779f0de0261b24173c5dfe8e0963 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 23:21:40 +0800 Subject: [PATCH 06/13] new --- workflows/deploy.yml | 56 ++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/workflows/deploy.yml b/workflows/deploy.yml index 638d9cf..7f6681c 100644 --- a/workflows/deploy.yml +++ b/workflows/deploy.yml @@ -1,26 +1,52 @@ -name: Deploy MkDocs site +name: Deploy MkDocs to GitHub Pages # ̵ + on: push: branches: - - main # ͵ main ֧ʱ + - main # ָ push main ֧ʱ + # ֧Ʋ main޸Ĵ˴ master jobs: - build-and-deploy: - runs-on: ubuntu-latest # ʹ Ubuntu ¾ + deploy: # ҵ ID + runs-on: ubuntu-latest # ָҵµ Ubuntu ⻷ + + permissions: + contents: read # ȡֿ + pages: write # д GitHub Pages IJ + id-token: write # ȡ OIDC tokenڲ Pages + + environment: + name: github-pages # ָ GitHub Pages + url: ${{ steps.deployment.outputs.page_url }} # Ӳȡ Pages URL steps: - - name: Checkout repo - uses: actions/checkout@v4 # ȡֿ:contentReference[oaicite:3]{index=3} + - name: Checkout code # ƣȡ + uses: actions/checkout@v4 # ʹ actions/checkout Actionȡֿ뵽⻷ - - name: Setup Python - uses: actions/setup-python@v5 # Python :contentReference[oaicite:4]{index=4} + - name: Setup Python environment # ƣ Python + uses: actions/setup-python@v5 # ʹ actions/setup-python Action with: - python-version: '3.x' + python-version: '3.x' # ָʹõ Python 汾 3.x ʾµ Python 3 汾 + cache: pip # pip ٹ + + - name: Install dependencies # ƣװ + run: | # ж + python -m pip install --upgrade pip # pip + pip install mkdocs mkdocs-material # װ MkDocs Material (ʹõĻ) + # ʹڴ˴Ӱװ - - name: Install dependencies - run: pip install mkdocs mkdocs-material # װ MkDocs :contentReference[oaicite:5]{index=5} + - name: Build MkDocs site # ƣվ + run: mkdocs build # mkdocs build ɾ̬ļĬ site Ŀ¼ + + - name: Setup Pages # ƣ GitHub Pages + uses: actions/configure-pages@v3 # ʹ actions/configure-pages Action Pages + + - name: Upload artifact # ƣϴõľ̬ļ + uses: actions/upload-pages-artifact@v3 # ʹ actions/upload-pages-artifact Action + with: + path: './site' # ָҪϴĿ¼mkdocs build Ĭ site Ŀ¼ - - name: Deploy to GitHub Pages - run: mkdocs gh-deploy --force --clean # ͵ gh-pages ֧:contentReference[oaicite:6]{index=6} - env: - CI: true # CI + - name: Deploy to GitHub Pages # ƣ GitHub Pages + id: deployment # һ ID + uses: actions/deploy-pages@v4 # ʹ actions/deploy-pages Action в + # Action Զһϴ artifact лȡļ \ No newline at end of file From bb2f61bef94dee6128c23b2480671f506d3310f4 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 23:24:29 +0800 Subject: [PATCH 07/13] new --- {workflows => .github/workflows}/deploy.yml | 102 ++++++++++---------- 1 file changed, 51 insertions(+), 51 deletions(-) rename {workflows => .github/workflows}/deploy.yml (97%) diff --git a/workflows/deploy.yml b/.github/workflows/deploy.yml similarity index 97% rename from workflows/deploy.yml rename to .github/workflows/deploy.yml index 7f6681c..a03a020 100644 --- a/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,52 +1,52 @@ -name: Deploy MkDocs to GitHub Pages # ̵ - -on: - push: - branches: - - main # ָ push main ֧ʱ - # ֧Ʋ main޸Ĵ˴ master - -jobs: - deploy: # ҵ ID - runs-on: ubuntu-latest # ָҵµ Ubuntu ⻷ - - permissions: - contents: read # ȡֿ - pages: write # д GitHub Pages IJ - id-token: write # ȡ OIDC tokenڲ Pages - - environment: - name: github-pages # ָ GitHub Pages - url: ${{ steps.deployment.outputs.page_url }} # Ӳȡ Pages URL - - steps: - - name: Checkout code # ƣȡ - uses: actions/checkout@v4 # ʹ actions/checkout Actionȡֿ뵽⻷ - - - name: Setup Python environment # ƣ Python - uses: actions/setup-python@v5 # ʹ actions/setup-python Action - with: - python-version: '3.x' # ָʹõ Python 汾 3.x ʾµ Python 3 汾 - cache: pip # pip ٹ - - - name: Install dependencies # ƣװ - run: | # ж - python -m pip install --upgrade pip # pip - pip install mkdocs mkdocs-material # װ MkDocs Material (ʹõĻ) - # ʹڴ˴Ӱװ - - - name: Build MkDocs site # ƣվ - run: mkdocs build # mkdocs build ɾ̬ļĬ site Ŀ¼ - - - name: Setup Pages # ƣ GitHub Pages - uses: actions/configure-pages@v3 # ʹ actions/configure-pages Action Pages - - - name: Upload artifact # ƣϴõľ̬ļ - uses: actions/upload-pages-artifact@v3 # ʹ actions/upload-pages-artifact Action - with: - path: './site' # ָҪϴĿ¼mkdocs build Ĭ site Ŀ¼ - - - name: Deploy to GitHub Pages # ƣ GitHub Pages - id: deployment # һ ID - uses: actions/deploy-pages@v4 # ʹ actions/deploy-pages Action в +name: Deploy MkDocs to GitHub Pages # ̵ + +on: + push: + branches: + - main # ָ push main ֧ʱ + # ֧Ʋ main޸Ĵ˴ master + +jobs: + deploy: # ҵ ID + runs-on: ubuntu-latest # ָҵµ Ubuntu ⻷ + + permissions: + contents: read # ȡֿ + pages: write # д GitHub Pages IJ + id-token: write # ȡ OIDC tokenڲ Pages + + environment: + name: github-pages # ָ GitHub Pages + url: ${{ steps.deployment.outputs.page_url }} # Ӳȡ Pages URL + + steps: + - name: Checkout code # ƣȡ + uses: actions/checkout@v4 # ʹ actions/checkout Actionȡֿ뵽⻷ + + - name: Setup Python environment # ƣ Python + uses: actions/setup-python@v5 # ʹ actions/setup-python Action + with: + python-version: '3.x' # ָʹõ Python 汾 3.x ʾµ Python 3 汾 + cache: pip # pip ٹ + + - name: Install dependencies # ƣװ + run: | # ж + python -m pip install --upgrade pip # pip + pip install mkdocs mkdocs-material # װ MkDocs Material (ʹõĻ) + # ʹڴ˴Ӱװ + + - name: Build MkDocs site # ƣվ + run: mkdocs build # mkdocs build ɾ̬ļĬ site Ŀ¼ + + - name: Setup Pages # ƣ GitHub Pages + uses: actions/configure-pages@v3 # ʹ actions/configure-pages Action Pages + + - name: Upload artifact # ƣϴõľ̬ļ + uses: actions/upload-pages-artifact@v3 # ʹ actions/upload-pages-artifact Action + with: + path: './site' # ָҪϴĿ¼mkdocs build Ĭ site Ŀ¼ + + - name: Deploy to GitHub Pages # ƣ GitHub Pages + id: deployment # һ ID + uses: actions/deploy-pages@v4 # ʹ actions/deploy-pages Action в # Action Զһϴ artifact лȡļ \ No newline at end of file From 8ecf2399780acab243452967497b294d8a0327ac Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 23:30:29 +0800 Subject: [PATCH 08/13] new --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 008821b..beb8a6a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,7 +10,7 @@ theme: markdown_extensions: - admonition - - markdown_gfm_admonition + # - markdown_gfm_admonition - pymdownx.details - pymdownx.superfences From 6409b34c742da0b77802107eec4263bcc24e56ee Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Thu, 15 May 2025 23:53:08 +0800 Subject: [PATCH 09/13] add dark mode --- mkdocs.yml | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index beb8a6a..a4d7fcc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,11 +2,42 @@ site_name: "USTC OS Docs" site_description: "中国科学技术大学操作系统课程实验文档" site_author: "USTC OS Team" -use_directory_urls: false +use_directory_urls: true + +# Repository +repo_name: 'oslab-docs' +repo_url: 'https://github.com/stdlibstring/oslab-docs' theme: name: 'material' language: 'zh' + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo # 你可以选择喜欢的主色 + accent: indigo # 你可以选择喜欢的强调色 + toggle: + icon: octicons/brightness-7 # Light mode icon + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate # 'slate' 通常用于暗色模式 + primary: indigo + accent: indigo + toggle: + icon: octicons/moon-stars-24 # Dark mode icon + name: Switch to light mode + + # Palette toggle for light mode (explicitly) + - media: "(prefers-color-scheme: light)" # Optional: explicitly define light mode toggle + scheme: default + primary: indigo + accent: indigo + toggle: + icon: octicons/brightness-7 + name: Switch to dark mode markdown_extensions: - admonition From cd5866ebcf46fe53d0e29348be54bfc4edf7d39f Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Fri, 16 May 2025 00:00:33 +0800 Subject: [PATCH 10/13] add dark mode --- mkdocs.yml | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index a4d7fcc..a843632 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,32 +12,20 @@ theme: name: 'material' language: 'zh' palette: - # Palette toggle for automatic mode - - media: "(prefers-color-scheme: light)" - scheme: default - primary: indigo # 你可以选择喜欢的主色 - accent: indigo # 你可以选择喜欢的强调色 + - scheme: default + media: "(prefers-color-scheme: light)" + primary: blue + accent: blue toggle: - icon: octicons/brightness-7 # Light mode icon - name: Switch to dark mode - - # Palette toggle for dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate # 'slate' 通常用于暗色模式 - primary: indigo - accent: indigo - toggle: - icon: octicons/moon-stars-24 # Dark mode icon - name: Switch to light mode - - # Palette toggle for light mode (explicitly) - - media: "(prefers-color-scheme: light)" # Optional: explicitly define light mode toggle - scheme: default - primary: indigo - accent: indigo + icon: material/toggle-switch-off-outline + name: 切换至深色模式 + - scheme: slate + media: "(prefers-color-scheme: dark)" + primary: light blue + accent: light blue toggle: - icon: octicons/brightness-7 - name: Switch to dark mode + icon: material/toggle-switch + name: 切换至浅色模式 markdown_extensions: - admonition From 5a77365d8105551ffe4137e7d2a86b7a83b38858 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Fri, 16 May 2025 00:09:09 +0800 Subject: [PATCH 11/13] change color of light mode --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index a843632..32ef632 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,8 +14,8 @@ theme: palette: - scheme: default media: "(prefers-color-scheme: light)" - primary: blue - accent: blue + # primary: blue + # accent: blue toggle: icon: material/toggle-switch-off-outline name: 切换至深色模式 From 8aeb6fdedbfe1982601d2d24a5d0ba1fa2ab7ae5 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Fri, 16 May 2025 00:12:27 +0800 Subject: [PATCH 12/13] change color of dark mode --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 32ef632..38806fe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,8 +21,8 @@ theme: name: 切换至深色模式 - scheme: slate media: "(prefers-color-scheme: dark)" - primary: light blue - accent: light blue + # primary: light blue + # accent: light blue toggle: icon: material/toggle-switch name: 切换至浅色模式 From 01f597cd6815a7a1a0cfa7c69caf1f3a134704f6 Mon Sep 17 00:00:00 2001 From: lucas <2238019055@qq.com> Date: Fri, 16 May 2025 00:18:33 +0800 Subject: [PATCH 13/13] Update --- .gitignore | 1 - docs/index.md | 1 - docs/todo.md | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index c32191b..46ff246 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /site/ -# /workflows/ \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 91436c9..9740c13 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,4 +5,3 @@ 临时搭出来的网站,目前还很不完善,请大家包涵。 请以课程主页上的信息为准:[课程主页](http://staff.ustc.edu.cn/~ykli/os2025/) -嘻嘻 \ No newline at end of file diff --git a/docs/todo.md b/docs/todo.md index 08c671d..6a0e522 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -13,4 +13,4 @@ * 可考虑其它转为PDF的方式,以统一语法和格式?例如[print-site](https://github.com/timvink/mkdocs-print-site-plugin)。 * 或者:[mkdocs-exporter](https://github.com/adrienbrignon/mkdocs-exporter),后者好像好看一些。 * 增加评论功能([giscus](https://giscus.app/zh-CN))。 -* 嘻嘻 +* ciallo~