• 首页
  • 加入
  • RSS
  • Tuesday, October 31, 2023

    一、背景

    近期,我们收到用户反馈,在使用 deepin 系统过程中遇到了 CPU 功耗过高导致的设备发热、续航较差情况,而用户在这些负载场景下,CPU 的占用往往不高。为了解决这个痛点,统信软件开源社区中心特别成立专项计划,对于 deepin 的电源进行专项优化,本文旨在针对此问题根因进行分析于说明。

    在对电源进行专项优化之前,我们首先对 deepin 系统进行了深入的调查和分析,以了解其在负载场景下的实际运行情况。经过对 CPU 使用率和功耗的监测,我们发现了一个令人惊讶的事实:尽管在高负载场景下 CPU 的占用率不高,但其功耗却持续升高,最终导致设备发热并影响续航。也就是说,我们前期做的省电优化工作,不仅无效,还起了反作用(具体情况作者将在下文作出仔细说明)。

    二、问题

    1. 内核

    最开始发现问题的地方在内核。有用户将我们的内核和 ubuntu 的内核进行对比后发现,虽然我们的系统和 ubuntu 系统的性能差不多,但是在发热和续航上,我们较 ubuntu 落后较多。有用户在 deepin 系统上使用 ubuntu 和其他开源 linux 发行版的配置文件分别编译内核,发现 deepin 的主要问题存在于发热控制之上。我们的测试同事高度重视这一社区反馈,遂对社区用户反馈的问题进行复现,佐证了这一现象。

    对于这种问题,我们立即联系了内核研发部的同事,并邀请部分对内核配置有一定研究的社区用户共同参与。在大家的合力排查下,我们发现,deepin v23 中提供的 HWE 内核存在部分 debug 和无用的内核选项被开启的情况,并且部分节电功能实际未能获得启用,这些都在一定程度上导致了 deepin v23 的续航表现不佳。

    2. 系统

    在系统层面,我重新审视了 dde-daemon 提供的电源调度模块,并且对比内核文档提供的文件接口,分析我们用户使用的电源模式,发现其中存在可以优化的空间。这将是本文着重讲解的内容之一。

    三、前置知识

    1.ACPI

    ACPI 是 Advanced Configuration and Power Interface 的缩写,是一种计算机硬件和操作系统之间交换能源相关信息的接口规范。它定义了计算机硬件的能源相关信息,如电源供应器状态、设备功耗、设备功率因数等。ACPI 是操作系统控制计算机硬件能源管理的标准,同时也是硬件厂商和操作系统之间通信的标准。

    在 deepin 系统中,ACPI 负责处理计算机硬件的能源管理,它与 deepin 系统的电源管理模块进行交互,以实现对计算机硬件的能源管理。

    在分析系统层面的问题时,我们需要了解 ACPI 和电源管理模块的作用和功能,以及它们是如何协同工作的。在本文中,我们将会详细讲解 ACPI 的工作原理以及 deepin 系统中的电源调度模块工作模式,并提出可行优化建议。

    首先,让我们了解一下 ACPI 的工作原理。当计算机硬件发生电源变化时,ACPI 会收集硬件信息,并向操作系统发送电源请求。操作系统收到电源请求后,会根据用户配置自动调整各个硬件的电源策略。而 deepin 系统的电源模块则是帮助用户生成配置来调整 ACPI 的行为。所以在这一方面,我们能做的就是向 ACPI 提供合理的电源策略,在保证性能的同时,降低设备温度并提升续航表现。

    2.平台电源配置

    平台电源配置提供了三种可选模式:performance(性能模式),balance(平衡模式),low-power(节能模式)。一般情况下,用户使用平衡模式就可以。在台式机和 mini 主机类(对于功耗和发热没有任何要求)设备上默认提供性能模式,在笔记本等移动设备上默认提供平衡模式。默认不提供节能模式,因为某些 ACPI 设备在节能模式工作过程中可能出现“睡死现象”,所以为了避免此问题,默认不提供 low-power 节电模式。

    3.CPU 电源配置

    /sys/devices/system/cpu/cpufreq目录下有许多文件名为policy<x>(x 代表核心编号),这些文件对应着你电脑上的 CPU 核心,而 CPU 的电源调度细节就在这些文件夹里面。在policy<x>目录下有一个文件`scaling_driver``,使用 cat 或其他方式访问它,得到的结果就是我们当前使用的调度器:

    • intel_cpufreq / acpi_cpufreq : 使用 scaling freq 调度
    • intel_pastate : 使用 Intel Pstate 调度
    • amd-pstate : 使用 AMD Pstate 调度

     scalling freq 调度

    这是最传统的 CPU 调度方式,你可以在 policy文件夹下的 scaling_available_governors 获取可选电源模式:

    英文中文含义
    performance性能模式最极致的性能表现,最火热的 CPU 温度,最短的续航。
    powersave节能模式为绿色地球出一份力。
    balance平衡模式性能和续航兼顾。小孩子才做选择,我全都要。
    schedutil平衡模式平衡模式的一种,使用不同算法进行调度。
    ondemand平衡模式平衡模式的一种,根据当前 CPU 负载动态调整频率。当负载大于阈值时调整到最高频率,其他情况按负载比例计算频率。
    conservative平衡模式平衡模式的一种,根据当前 CPU 负载动态调整频率。当负载大于最大阈值时步进递增频率,当负载小于最低阈值时步进递减。
    userspace用户模式以用户指定的频率运行 CPU,可通过/sys/devices/system/cpu/cpuX/cpufreq/scaling_setspeed 进行配置

    你可能好奇,为啥这里有这么多平衡模式,其实这些平衡模式的作用都是是一样的:平衡性能和续航,不过使用的算法可能不同,这里我不做详细说明,我在网络上找到一些详细资料可以参考,有兴趣的朋友可自行查阅:

     Intel Pstate

    这是 Intel 近几代 CPU 独享的 moment,内核开启 intel pstate 后(V23 内核默认开启)你会发现在 policy文件夹下多了几个文件:

    我们只需要关注:

    • energy_performance_available_perference : 可用的 pstate 电源调度
    • energy_performance_perference:当前选定的 pstate 电源调度,可以更改此文件内容来更改电源调度
    • 在 Intel Pstate 中出现了两个新的调度方案:
    • balance_performance : 平衡偏性能,平时工作频率不高,在负载增大时能快速响应
    • balance_power : 平衡偏节能,电源策略较为保守 在部分电脑上还有 default 方案,此方案就是经过 pstate 优化过的 balance 策略。具体 PState 使用的黑魔法以及主动模式和被动模式的调度策略,可以参照内核文档进行分析。

    AMD PState

    这是 AMD ZEN2 以上用户,以及支持 kernel 6.4.x 用户独享的 moment。其实 AMD 在 6.1 内核已经做了 PState 的支持,不过是被动模式。

    • (Actvie Mode)主动模式

      Active Mode 仅在内核版本大于 6.4 以上,且内核选项打开 AMD PState 时可用。可能需要在 grub 内加入启动参数以打开此功能:amd_pstate=active,也可以修改文件实现 Active Mode 的电源策略和 Intel PStatewi 类似。

    • (Passive Mode)被动模式

      Passtive Mode 仅在内核大于 6.1 以上,且内核选项打开 AMD PState 时可用。可能需要在 grub 加入启动参数开启此功能:amd_pstate=passive,也可修改文件实现。

      Passive Mode 提供两种电源模式,在/sys/device/system/cpu/cpufreq/scaling_governor文件进行调整:

      • performance 使用 platform_profile 进行配置,调度积极性较高
      • scheutils 在/sys/device/system/cpu/cpufreq/schedutil/rate_limit_us文件中调整调度粒度(两次调度的间隔时间)和 ACPI 的 scheutils 类/sys/device/system/cpu/cpufreq/scaling_governor
    • (Guided Mode)引导模式

      Guided Mode 仅在内核大于 6.1 以上,且内核选项打开 AMD PState 时可用。可能需要在 grub 加入启动参数开启此功能:amd_pstate=guided,也可修改文件实现。这就类似汽车的自动挡,驱动程序请求最低和最大性能级别,平台自动选择此范围内适合当前工作负荷的性能级别。

    4.GPU 电源管理部分

     AMD GPU

    如果是 AMD GPU 则需要更改两个文件(使用 tee 命令进行写入):

    • /sys/class/drm/card0/device/power_dpm_state(这是一个遗留接口,目的是向后兼容)
    • performance 高性能模式
    • balance 平衡模式
    • battery 节能模式
    • /sys/class/drm/card0/device/power_dpm_force_performance_level

    以下设置来自 AMD 官方驱动文档:

    https://dri.freedesktop.org/docs/drm/gpu/amdgpu.html#power-dpm-force-performance-level

    drm/amdgpu AMDgpu driver — The Linux Kernel  documentation

    power_dpm_force_performance_level:

    AMD GPU 驱动程序提供了一个 sysfs API,用于调整某些与功率相关的参数。文件 power-dpm-force-performance-level 将用于执行此操作。它接受以下参数:

    • auto:当选择 auto 时,设备将尝试针对驱动中的当前条件动态选择最佳功率曲线
    • low:当选择低时,GPU 被强制到最低功率状态
    • high:当选择高时,GPU 被强制到最高功率状态
    • manual:当选择手动时,用户可以通过 sysfs pp_dpm_mclk、pp_dpm_sclk 和 pp_dpm_pcie 文件手动调整每个时钟域启用的电源状态,并通过 pp_power_profile_mode sysfs 文件调整电源状态转换方式。
    • profile_standard 固定时钟级别分析模式。此模式将时钟设置为固定级别,该级别因 ASIC 而异。这对于分析特定工作负载很有用(不常用)。
    • profile_min_sclk 最小 sclk 分析模式。此模式将 sclk 强制设置为最低级别。这对于分析最小功耗的场景很有用(不常用)。
    • profile_min_mclk 最小 mclk 分析模式。此模式将 mclk 强制设置为最低级别。这对于分析最小功耗的场景很有用(不常用)。
    • profile_peak 峰值分析模式。此模式将所有时钟(mclk、sclk、pcie)设置为最高级别。这对于分析最大性能的场景很有用(不常用)。

    测试:

    • LOW 模式的跑分

    LOW 模式的跑分

    • auto 模式的跑分

    auto 模式的跑分

    • high 模式的跑分

    high 模式的跑分

     Intel GPU

    intel GPU 使用的 i915 驱动,并不希望你对其做出调整,因为其驱动自带的电源策略已经足够聪明。不过你也可以通过 intel 提供的 intel-gpu-tools 进行调整和获取信息。

    sudo apt install intel-gpu-tools

    然后使用

    sudo intel_gpu_frequency

    来获取当前频率(当前使用的是 Intel A750)

    可以看到 intel 的显卡驱动是在 600 MHz 到 2400 MHz 之间动态调整(如上图)

    测试笔记本下 intel 核显跑分如下 图片

    Nvidia

    由于 nvidia 驱动不开源,所以在系统层面无法对其做控制。

    四、应用

    应用级别的省电,应该就是在保证用户使用流畅度的情前提下下节省性能。之前也有用户提出过,是否能参照 vivo 的 origin3 os 的不公平调度算法来实现优化。毕竟安卓系统的底层也是 linux,理论上实现难度不大。

    Cgroups,全称 Control Groups,是 Linux 内核提供的一种资源管理机制,用于对进程分组并对其资源进行限制和隔离。Cgroups 可以用于限制进程的 CPU、内存、磁盘、网络等资源,也可以用于限制进程的优先级和 IO 权限。利用其提供的能力,我们很容易实现类似不公平调度算法(我们新的 AM 天然支持 Cgroups 的操作),但是我还有一些顾虑:

    • 不同于手机操作系统,计算机操作系统是多任务并行的,在多数窗口管理器下,我们并没有一个明显的前台应用,此时使用不公平调度可能存在隐患;
    • 容易引发人机对抗。在我的观念里面,计算机是为人服务的,那么用户的意志必定是第一优先级,所以我们不应改变用户的行为,如果使用不平衡调度和用户预期不一致,会极大降低用户体验;
    • 使用前后台区分应用,可能导致开销和收益比下降,性价比不高。linux 桌面不像安卓设备有明显前后台,那么用户频繁切换应用的操作将导致我们的调度器频繁切换调度,使得开销过大。 我认为最佳的解决方案是:提供能力,但不提供方案。我们可以提供基于 Cgroups 方式修改应用组的优先级,然后让用户自己选择什么应用优先级更高,什么应用优先级低,以实现调度(比如在 dock 上右键选择优先级)或提供一套配置以供用户自由选择。

    如果一个电脑需要使用不平衡调度来保证使用流畅性,可能这并不是一个操作系统能解决的问题,而更应该考虑硬件是否需要更换,以保证多任务使用的流畅性。

    附录——常用的调试测试工具

    1. S-tui

    可以看到 CPU 频率变化,配合 stress 可以对 cpu 进行压力测试。 图片

    2. intel-gpu-tools

    可以使用 intel_gpu_frequency 来获取和调整 i965 的驱动频率。

    3. glmark2

    GPU 跑分软件。

    4. stress-ng

    CPU 压力测试软件。

    5. powertop

    电源测试软件,可以看到电源的功耗和使用情况。

    Sunday, September 24, 2023

    随笔

    在给Wireshark做完编译、调整、上架之后,我打算到论坛里看看大家对开源应用有没有什么疑惑的地方,没想到大家现在对开源的关注度这么高了。在看到最近一些闹得比较沸沸扬扬的话题之后,我也对我现在开源应用适配工作的很多地方产生了疑惑,我这样跟开源还有关系吗?

    重新适配开源应用的意义

    在一些实际的场景中,其实有很多"重复造轮子"的情况,即明确存在其他开源项目可用来替代的情况下仍然自己重头造一个项目来实现相同/类似的功能。比如各大桌面环境都有自己的一套文管:DDE的dde-file-manager、KDE的Dolphin,但严格意义上他们不是重复造轮子,而是更好地融入桌面环境中。 另外一种情况就是我现在做的适配开源应用,我的作品很大一部分可以在系统的应用商店中找到。但细心的小伙伴可能会发现:

    • “诶,这个应用不是系统仓库里有吗?为什么还要消耗资源去重复适配?”

    • “Debian 12的这个应用已经去到3.6.1了,为什么商店里的还是3.6.0?”

    • “为什么软件官方已经提供了deepin可以直接安装的包,应用商店还要另起一个新包名互不兼容?”

      想到这几个问题,大家是不是血压都上来了?这不就是妥妥重复劳动吗?别急,听我慢慢道出里面的小细节。我愿总结为两个核要义:平易、近人。

    易用性

    首先问大家一个问题,抛开命令真爱粉不谈,你认为用apt安装包方便还是在应用商店看着截图、应用描述、评论等等来一站式点击"安装"方便? 虽然命令也可以直接安装各种系统仓内的各种包,但对于新手用户个人认为还是有一定难度的。而且不使用应用商店来下载,很有可能就与应用商店各种特性擦肩而过了,比如定期的应用推荐、应用版本迭代的changelog。

    用户侧体验优化

    不知道大家有没有体验最近Wireshark-4.0.8,有使用过的朋友应该会发现我们很贴心的准备了两个desktop启动方式,一个是普通模式一个是root模式,中间的取舍故事今天就不多赘述了,大家有兴趣可以后面单独拎出来。 至于为什么会设置这么两个模式?其实我在适配开源项目前一般都会预览大家在应用商店里的反馈,这次的举动主要是由于Wireshark用普通user权限可以打开程序,但只是能看个壳子,只有root权限打开才是完整的功能。而且大家也反馈了比较多这个情况,我在评估一段之间、测试可行性之后给大家带来了"两个desktop+提示窗+双语支持"的独家体验。 “两个desktop+提示窗"是为了让大家方便以不同的模式打开,“提示窗"则是根据不同模式给大家展示相关提示信息,“双语支持"则是根据系统语言来设置中文和其他语言下分别显示中文提示和英文提示,一定程度上兼顾海外友人的体验。

    image

    我观察了下,Wireshark编译之后的原版desktop文件是不提供这种级别的优化的;而现在deepin应用商店里的版本实实在在可以做到了这个层次的优化,亲近人心。 大家可以把细节打在评论上。

    生态多样性

    严格意义上,每个包只有包名是相对唯一的,这也是因为大部分包管理器都会通过包名来检查该包的情况。但对于同一个应用特别是开源项目而言,它允许被多次修改分发,除了用版本号用以区分,也并没有强制要求每个人维护的包名都遵循同一个。 但为什么Debian等主流发行版一般包名都会随着版本迭代而保留使用,一般并不轻易修改包名?个人认为有以下几个可能:

    • 一般主流发行版仓库中某个重要项目/库为了保证长期稳定性和可维护性,一般都由同一个维护者负责维护,所以包名不会发生明显改动。

    • 包名变动过大,相关联的其他包/库均需要重新调整依赖关系,不太利于用户体验和仓库维护管理。

    • 接第一个情况,即便每个人都有编译开源项目的自由,但大家都比较自觉不会向系统仓中投递已经确切存在的包。

      除此之外,如果你同时在应用商店和外部安装一个包名一致的应用,则只会保留版本较新的一个。或者说,如果你想要保留本地安装的版本,但你在应用商店里对其进行升级,那此时你本地的包就会被替换掉了。

    稳定性测试

    其实在开源应用上架到应用商店前都会有测试流程,大家也很难保障哪一个版本会有致命问题,或者哪一个版本会丧失预期特性。所以我们在选包之后,一般是先编译一遍,没有致命问题我们才进行其他操作,尽最大努力保驾护航。

    适配开源应用的难度

    要是你觉得我们就是单纯把一些官方提供的二进制归档文件甚至是直接可用的deb包转化来上架应用商店,比如Visual Studio Code。那我很坦白告诉你,就是单纯几个字,“解包–运行–重新打包”。 但如果你是看了上文内容的话,就知道事情并不简单了。不只是从源码开始编译,核心在于如何与终端用户、各大开发者站在统一战线。当然,这里指的并不只是不断去满足用户的需要,这种可能会比较适合C端方向。我更希望的,是帮助用户在专业性比较强的问题面前能够提供满意的答卷,比如这次Wireshark,其实大部分用户真的不一定知道需要使用root来运行,所以我们做了优化。 应用/软件包只是一个普通产出物,不可替代的其实是整个适配过程中与其他人产生差异化的用户思维。

    结语

    曾经,我觉得我适配出来的开源应用只上架应用商店确实是玷污了开源,而且我并不是对程序Core本身做出了代码性贡献,这些优化充其量可能也就是普通建议而已。但后来,能看到大家对开源的支持,我也释怀了。 我希望以后有机会可以分享我在编译中发生的故事,同时也很支持使用其他发行版的小伙伴可以使用我在deepin上编译、优化调整之后的开源应用。 我也希望在开源这片蓝天里,我不是孤军奋战。

    Friday, September 22, 2023

    前不久深度科技旗下deepin社区发布了自己的 IDE:deepin-IDE,得到了全网用户尤其是开源社区用户的广泛关注,目前在 GitHub(https://github.com/linuxdeepin/deepin-unioncode)仓库的 star 数量已经达到 600 多个,说明大家的热情还是很高涨的。

    为了从技术层面给大家的热情做一个反馈,本文试着将 deepin-IDE 内部的一些实现方法进行分享,希望能够解答友友们的疑惑并得到积极的反馈。

    本篇挑了大家关心的“调试”部分进行分享。需要说明的是,deepin-IDE 的调试功能是选用 DAP(Debug Adapter Protocol )调试适配协议实现的,所以整体架构是围绕该协议搭建的,至于 DAP 具体是什么,让我们带着问号往下看。

    什么是 DAP 协议

    DAP 即调试适配协议( Debug Adapter Protocol ),顾名思义,它是用来对多种调试器进行抽象统一的适配层,将原有 IDE 和调试工具直接交互的模式更改为和 DAP 进行交互。该模式可以让 IDE 集成多种调试器变得更简单,且灵活性更好。

    IDE 中的调试功能有许多小功能组成,包括单步执行、断点、查看变量值等,常规的实现方式是在每个 IDE 中去实现这些逻辑,且因为调试工具的接口不同,还需要为每个调试工具做一些适配工作,这将导致大量且重复的工作,如下图所示:

    调试适配器协议背后的想法是标准化一个抽象协议,用于开发工具如何与具体调试器通信。这个思想和 LSP(Language Server Protocol)和 BSP(Build Server Protocol)类似,都是通过协议去统一相同功能在不同工具之间的差异性。其所处位置如下图所示,其中左边为不同的开发工具,右边为不能同的调试器,不同于开发工具和调试器直接交互的方式,DAP 将这些交互统一了起来,让开发工具和调试工具都面向 DAP 编程。

    上图中的交互是通过协议进行,所以不会像通过 API 的方式存在语言限制,可以更好的适应调试器的集成。

    DAP 如何工作

    以下部分解释了开发工具(例如 IDE 或编辑器)和调试适配器之间的交互,包括具体的协议格式说明、交互流程等。

    调试会话

    开发工具有两种基础的方式和调试器进行交互,分别是:

    【单会话模式】

    在这种模式下,开发工具启动一个调试适配器作为一个单独的进程并且通过标准的std接口进行通信。在调试会话的结束时调试适配器就终止,对于当前的调试会话,开发工具往往需要实现多个调试适配。

    【多会话模式】

    在这种模式下,开发工具不会启动调试适配器,而是假定它已经在运行并且会在特定端口上侦听连接尝试,对于每个调试会话,开发工具在特定端口上启动一个新的通信会话并在会话结束时断开连接。

    在与调试适配器建立连接后,开发工具和调试适配器之间通过基础协议进行通信。

    基础协议

    基础协议由两部分组成,包括头和内容(类似于 HTTP),头部和内容部分通过“\r\n”进行分割:

    【协议头】

    协议头部分由字段组成, 每个头字段由一个键和一个值组成,用‘:’(一个冒号和一个空格)分隔, 每个头字段都以“\r\n“结尾。由于最后一个协议头字段和整个协议头本身都以 \r\n 终止,并且由于协议头是强制性的,所以消息的内容部分总是在(并唯一标识)两个 \r\n 序列之前。当前只支持一个协议头字段:

    头字段名值类型描述
    Content-Length数字这个字段是必须的,用来记录内容字段的长度,单位是字节。

    协议头部分使用的是“ASCII”编码。

    【内容部分】

    内容部分包含了实际要传输的数据,这些数据用 JSON 格式来描述请求、响应和事件。内容部分用的是 utf-8 编码

    为了有个具体的认识,这里举个简单的例子。在调试过程中,开发人员经常会使用到下一步操作,在 DAP 中其协议为:

    Content-Length: 119\r\n
    \r\n
    {
        "seq": 153,
        "type": "request",
        "command": "next",
        "arguments": {
            "threadId": 3
        }
    }
    

    类型是“请求”,命令是下一步,参数部分可以携带多个,这里是用的线程Id。 这个协议看着挺简单的,是吧?接下来就讲讲如何使用它。

    使用方法

    详细的使用方法这里就不涉及,因为用一个时序图就可以说明:

    可以看到,初始化、请求、响应等必要的步骤都在图中。其中调试适配器可以理解为调试器的抽象,调试功能的最终执行者是由对应语言的调试工具实现的。

    在 deepin-IDE 中的实现

    在 deepin-IDE 中,调试功能的实现是结合 cppdap + debugmanager 实现的。

    cppdap 是一款基于 C++ 开发的 SDK,基本实现了 DAP 的全量协议。 deepin-IDE 的客户端和服务端都是应用的该 SDK 进行开发,据此可以实现以下功能:

    1.通信功能,包括服务端的 TCP 监听,客户端的 TCP 连接等;

    2.DAP 协议的封装,并实现协议的串行化和解串行化;

    3.提供注册回调功能,从而可以在回调内处理各种事件、请求等;

    它的层级结构如下:

    cppdap 可以减少客户端和服务端不少工作量,也统一了两边的协议数据。而 debugmanager 可以理解为调试器的抽象,包含所有必要的调试要素。整体结构如下:

    左边是客户端,右边是服务端,内部实现如下:

    客户端实现

    客户端包含了两个个主要功能,一个是和 DAP 服务端进行交互,发送调试命令或处理返回的数据;另一个是将DAP 数据转换后显示到用户界面,并响应界面发送的事件。概括起来就包含业务模块、事件模块、DAP 模块和界面4个部分。

    业务模块

    • 业务模块包含了插件类、调试参数、调试管理类等,其中插件类负责插件加载、初始化、获取上下文等,调试管理类用来组合事件、DAP、界面几个模块。 事件模块

    • 事件模块包含两个子模块,分别是事件发送和事件接收,比如页面跳转事件、添加\移除断点事件等。 DAP 模块

    DAP 模块基于 cppdap 开发,采用层级结构,底层是原始 DAP 协议封装,中间层是针对业务做的进一步封装,简化了向外提供的接口,最上层是对整个调试功能的整合,包括数据缓存、界面元素、命令收发。

    • 界面部分 界面模块包含堆栈界面、变量界面、断点列表、异步对话框等,用于 DAP 的数据展示。
    • 如上图所示,灰色部分为 DAP 客户端的界面呈现。

    服务端实现

    服务端的功能分为两个部分,一个是基于 cppdap 实现命令的收发,另一个是与 gdb 交互,实现调试程序的启动、暂停、退出等一系列动作。

    DAP

    • 和客户端一样,服务端也是基于cppdap实现的通信和协议封装和解析。 调试工具

    • 和调试工具的交互是通过进程调用的方式实现,接收进程输出得到返回信息。如果调试工具本身支持 DAP 协议,则可以直接交互。

    至此,本次的分享就到这儿啦!不知道你对 deepin-IDE 中的调试功能有所了解了吗?

    温馨提示,deepin-IDE 还包含很多有意思的功能,如果大家感兴趣可以积极反馈,后续有机会再进行分享。

    参考文档

    debug-adapter-protocol

    deepin-IDE 使用手册

    内容来源:deepin社区

    内容作者:deepin-mozart、toberyan

    转载请注明出处

    Friday, September 1, 2023

    楔子

      有这么一个说法,每多一个数学公式,读者就减少一半。深度学习想来也无法免俗,毕竟技术文章不免艰涩,而要完全绕过公式讲好深度学习与大模型,以臣妾微薄的实力实在是做不到啊。

      因此,本文先歪歪楼,讲讲深度学习与大模型的历史与八卦,一方面是让大家稍微了解下技术发展的脉络,另一方面也是尝试挽救一下读者的欢心,毕竟历史八卦,人人都爱。

    历史

      说到神经网络的起源,一般都会追溯到沃尔特·皮茨(Walter Pitts)与麦卡洛克(McCulloch),其中皮茨起到了更主要的作用,而且更具传奇性。皮茨(见图1)于1923年出生于美国底特律的铁匠家庭,家庭教育以老爸的拳头为主,而在这种家庭环境里,他主要靠自学学会了拉丁文、希腊文、逻辑和数学。在12岁那年,皮茨看完了罗素与怀特海的大厚本《数学原理》,并向罗素写邮件附上了自己发现的一些问题,罗素不免大吃一惊,还回信邀请皮茨到剑桥大学读他的研究生。由于家庭与年龄原因,皮茨当然不可能成行,不过当三年后,罗素到美国芝加哥讲学的消息传到皮茨耳朵里时,他就离家出走,而且终其一生再也没有回去过。其后皮茨遇到了麦卡洛克,两人在数学、逻辑和神经网络上有着共同的看法,并一起努力,于1943年合作完成了知名论文《A LOGICAL CALCULUS OF THE IDEAS IMMANENT IN NERVOUS ACTIVITY》,在这篇论文中,他们用二进制逻辑门来表示神经元,而且证明了此模型可以实现任何经典逻辑,从而表明了神经网络的通用性,奠定了深度学习的基础,同时也建立了神经科学和计算机科学之间的交叉研究。高中未毕业的皮茨受到了数学家与控制论之父维纳的欣赏,破格进入麻省理工学院攻读博士学位,被引荐给了冯·诺依曼,而上述论文也成为了冯·诺依曼关于计算机架构的著名论文《First Draft of a Report on the EDVAC》所引用的唯一一篇文章。但是在其后,首先是维纳与皮茨等断绝了关系,然后科学家们又发现神经网络并不能解释一切生物感知现象,皮茨深感失望,于是烧掉了自己历经数年写作的博士论文,从此退出了科研界,并死于酗酒相关的病症。

    图1 沃尔特·皮茨

      我们要介绍的下一位是弗兰克·罗森布拉特(Frank Rosenblatt),他是感知机(Perceptron)的提出者(感知机在我们之前的文章“深度学习入门”里介绍过),并且于1958年在实验了50次之后,让IBM 704自行学会了识别打孔卡上的标记是在左侧还是在右侧。他认为,通过感知机可以不依赖人类的训练与控制,就能感知、识别和辨认周边的环境,其代表作是《Principles of Neurodynamics: Perceptrons and the Theory of Brain Mechanisms》。但是,当时罗森布拉特设计的感知机实际上是只有一层的神经网络,能力非常有限,相比之下,现代成熟的神经网络则有数十层,因此同样是人工智能专家的明斯基撰文指出了感知机的局限性,并使得相应的研究进入了低潮。直到在2004年,大家重新看到了神经网络的潜力,IEEE协会为此也专门设立了罗森布拉特奖。

      在神经网络漫长的寒冬期,研究者相对较少,其中就包括了于1974年在其博士论文中提出了反向传播(backpropagation)的Werbos(并由于此贡献获得了IEEE罗森布拉特奖),还有于上个世纪八十年代提出了Hopfield 神经网络的Hopfield。

      下面隆重登场的是深度学习之父,大名鼎鼎的杰弗里·辛顿(Geoffrey Hinton)。辛顿1986年发表的论文《Learning Representations by Back-Propagating Errors》,给出了通过反向传播学习表征的算法,于2006年其推出了深度学习(Deep Learning)的概念,为深度学习与大模型的大潮解开了序幕。

    图2 杰弗里·辛顿

      另一位深度学习的大佬杨立昆(Yann LeCun)在辛顿麾下求学后就职于贝尔实验室,并在1989~1993年间发明了卷积神经网络(CNN),可以用来解决手写数字识别(如MNIST)的问题。当时可没有GPU,那时CPU的性能也相当低下。

      2012年是深度学习重要的一年。在这一年,辛顿和他的两个学生Alex Krizhevsky,以及Ilya Sutskeverz共同发布了AlexNet这个多层神经网络。这个神经网络用到了诸多的新技术,包括使用了ReLU作为激活函数,使用了CUDA利用英伟达的GPU来进行神经网络的计算,使用了dropout作为神经网络的一种优化方法等。AlexNet在知名的图像识别分类比赛ImageNet中一鸣惊人,它不仅获得了比赛的冠军,而且其错误率达到了16.4%,比当年亚军的26.2%低了将近10%,比2011年冠军的25.8%低了超过9%,几乎可以认为是降维打击。自此,深度学习名声大噪,大量研究人员都转向深度学习尝试完成计算机视觉等相关的人工智能任务。

      下面深度学习的发展就顺畅了很多,2014年Ian Goodfellow推出了GAN,通过生成式对抗网络能生成逼真的图像甚至视频;2016年 DeepMind 推出了AlphaGo,其水平很快就远远超过了人类围棋冠军;同样在 2016年,何恺明等研究者推出了ResNet,它成为了包括大语言模型在内的各神经网络的通用技术,对应论文引用截止2023年上半年已经超过了17万,成为了深度学习领域引用最高的文章。

      2017年是大模型关键技术transformer的诞生年,它是在《Attention is All You Need》这篇论文中被提出的,其后就成为包括BERT、GPT、T5等大语言模型使用的框架,而且跨界到了计算机视觉领域,形成了ViT等新的研究方向,几乎成了一统江湖的标准模型。

      2018年是深度学习三巨头辛顿、杨立昆与约书亚·本吉奥(Yoshua Bengio)的收获之年,他们因为在深度学习方面的诸多成就与影响力获得了ACM图灵奖,这项计算机科学领域的最高奖。在接下来的几年,深度学习的各项研究成果仍然难以大规模落地,诸多深度学习相关的公司持续烧钱,很多人认为这一波人工智能的热潮马上又要过去了。

      让我们快进到2022年,上半年以stable diffusion为代表的图像生成模型风靡一时,而到了下半年的十一月底,以ChatGPT为代表的大语言模型横空出世,其知识广度、推理能力与多轮对话能力使得它成为了历史上最快达到一亿用户的产品,重新点燃了人工智能产业化的火炬,并使得业界的诸多大佬认定它是划时代的产品,其影响深远,延续至今。

    图3 DNNResearch团队

      顺便说一句,GPT系列大模型的出品者是OpenAI,而OpenAI的首席科学家就是之前提到过的Ilya Sutskeverz。在2012年AlexNet一飞冲天后,多个大厂邀请辛顿等三人加入,于是辛顿团队成立了一个名为DNNResearch的公司,公司仅有他们叁,公司唯一的目的就是被大厂整体收购。竞拍的公司包括谷歌、百度、微软与DeepMind,随着竞拍价格的逐步走高,微软与DeepMind首先被淘汰出局,谷歌与百度均出价到了4400万美元。辛顿当晚暂停了竞拍,第二天早上便决定公司被谷歌收购。Ilya Sutskeverz在其后做出了诸多贡献,包括首创seq2seq(大语言模型的前身),参与深度学习框架Tensorflow与AlphaGo的开发等,最终于2015年加入OpenAI,成为了GPT等系列产品的核心技术人物。

    八卦

      关于深度学习之父辛顿,还有很多不少八卦,其家族还与中国有着不少联系。

      从头说起,George Everest是十九世纪英国的大地测量工作者,其主要的工作地域都在印度,他和其前任测量了从喜马拉雅山到印度次大陆最南端科莫林角11.5度的经向弧,由于这些贡献,珠穆朗玛峰(Mount Everest)以他的名字命名。George Everest有一个侄女婿名为乔治·布尔(George Boole),也就是著名的布尔代数的发明者,现代计算机所使用的逻辑与、或、非等运算均来自于他。

      乔治·布尔的小女儿艾捷尔·丽莲·伏尼契(Ethel Lilian Voynich)则是知名的《牛虻》的作者,这本书描写的是19世纪意大利爱国者反对奥地利统治者的斗争,是风靡一时的革命书籍,在苏联和中国都拥有广大读者。

      乔治·布尔的大女儿玛丽(Mary)则嫁给了一个姓辛顿的数学家,他们有一个孙子威廉·辛顿(William Hinton),以及孙女琼·辛顿(Joan Hinton),不过在中国,他们一般被称为韩丁与寒春。寒春是芝加哥大学核子物理研究所的研究生,也是曼哈顿计划中少数的女科学家之一,在洛斯阿拉莫斯(Los Alamos)武器试验室做费米的助手,其后反对核战争,并于1948年来到中国,进入延安,投身中国革命,并与阳早(Erwin Engst)结婚,成为了奶牛专家。现在在北京还留有他们的工作成果,那就是北京市昌平区沙河大学城农机院的学农基地,寒春与阳早均为农机院学农基地的创办人,时至今日,每年仍然有大量的北京中学生来到学农基地参加学农实习(笔者的小孩有幸成为其中的一员)。寒春和阳早把大部分生命都奉献给了中国的事业,并最终在中国去世。2004年8月,中国开始实施“绿卡”制度,寒春成为了第一个获得中国“绿卡”的外国人。

    图4 阳早与寒春

      玛丽的另一支当然就是深度学习之父杰弗里·辛顿了,他是玛丽的曾孙,因此比寒春、韩丁晚一辈。当然,他们之间并没有直接联系。

      深度学习的历史与八卦到此告一段落。下面,我们又将步入正轨,重新来讲讲产品与技术了,咱们下次再见。

    Thursday, August 24, 2023

    应用大模型

      AIGC 是基于大模型的,而大模型的基础是深度学习。上一篇文章对深度学习进行了初步介绍,首先是深度学习的神经元起源,引发了基于线性函数的模拟,又因为线性函数无法习得逻辑异或,因此引入了非线性的激活函数,再通过三层神经网络给出了MNIST手写数字识别的模型,接着又介绍了神经网络是如何通过数据与反向传播来学习与调整参数的,最后给出了神经网络的分层结构。

      大模型的直观应用当然首先体现在包括ChatGPT、文心一言、讯飞星火等问答型产品的使用上,另一方面也体现在编程上,在此先给出大模型的编程应用。以下使用的模型、库与样例均来自于Hugging Face。

      图1给出了基于大模型的英中翻译代码与运行结果。从图1中可以看到,真实的翻译代码只有14、15两行,其逻辑是使用了Helsinki-NLP的opus-mt-en-zh模型,其中mt代表机器翻译(machine translation)、en和zh分别表示英文和中文。从图1中同样可以看到,翻译结果相对还是比较准确的。

    图1 基于大模型的英翻中

      图2给出了基于大模型的文本情感分析的代码与运行结果。从图2中可以看到,实际有用的代码也仅需14、16两行,而且这次没有指定具体模型,只给出了需要text-classification这种模型。代码运行结果是认为文本情绪是负面的(NEGATIVE),准确度大概是90.1546%,这显然是符合实际的,因为文本是对商家发错货的抱怨。

    图2 基于大模型的文本情感分析

      图3给出了基于大模型的问答。这次的代码稍多一点,但实际的代码也只有三行。第14行给出了需要一个问答(question-answering)的大模型,但是没有指定大模型的名字,第15行是提问的字符串“What does the customer want”,即用户到底想要什么。第16行则使用上述文本作为上下文,提问字符串作为问题,传给问答大模型获取答案。从运行结果看来,答案还是蛮靠谱的。

    图3 基于大模型的问答

      当然,基于大模型的程序还有很多,但是从上面三个例子已经可以看出,基于大模型可以写出简短而强大的自然语言处理的程序,下面让我们走进大模型,看看它究竟是如何做到这一点的。

    走进大模型

      大模型在自然语言处理领域里大放异彩,因此首先需要了解自然语言的特点。

      自然语言的显著特点(也是难点),那就是词与词之间有着广泛的关联。比如下面两句英文:

    • Go to the bank to get some money.
    • Go to the bank to get some water.

      只有看到每一句的最后一个词,才能分辨出 bank 到底是银行还是堤坝。再比如下面这两句中文:

    • 今天太冷了,能穿多少穿多少。
    • 今天太热了,能穿多少穿多少。

      整句话唯一不同的就是冷与热这两个字,但也就是这句话的一字之差,就导致了整句话的意义完全不同了。

      因此,自然语言处理的关键点就在于如何能准确地判断词与词之间的关系,如果能准确地知道所有词之间的关系,那即使缺了一个词,也能根据关系推出缺的词应该是什么词。当下处理这一问题的主流技术是transformer,这个词不好翻译,主要因为它和变形金刚的英文一模一样。transformer的核心概念是注意力(attention),即每个词到底在注意其它的哪个词,或者说哪些词之间有什么关系。

      注意力具体由以下关键概念组成:

    • 每个词(实际上是词元,token)均有对应的 q/query(查询)、k/key(键值)与 v/value(值)这三个矩阵变量
    • q[i]用来查询本词(i)与其它词(j)之间的注意力
    • k[j]是词j回应查询的键值,具体是q[i]与k[j]相乘后缩放,接着用softmax激活函数处理,再乘以v[j],这就得到了词i针对词j的注意力att[i,j]。
    • 词i的对应输出为计算得到的注意力之和,即 y[i] = att[i,1] + att[i,2] + … + att[i,n]
    • 注意力可以有多个(multi-head),每个注意力可以关注不同的方向,例如有的注意力关注的是词与词之间的意义,有的关注的是押韵,等等

      图4给出了一个句子中各个词注意力的计算过程。

    图4 注意力计算过程

      句子是“小明、小刚、小红是小强的朋友,小明是…”,当前的词是“小红”,序号是3,可以看到小红的q值与每个词的k值相乘之后再用softmax处理(缩放操作在这里省略了),接着再与每个词的q值相乘,最后相加即可得到序号为3的输出。

      那么这些k、q、v 等的值如何确定呢?当然是通过上一篇文章里提到的反向传播进行学习的,那反向传播学习自然语言的正确输出是什么呢?在大语言模型中,其训练手段是使用大量的高质量语料,将词语按序逐批输入大模型,以原句子中的下一个词或者特意被空缺出来的词为正确输出来学习的。例如在上面的句子中,大模型在输入了“小明、小刚、小红是小强的”之后,应该能计算输出“朋友”这个词,如果输出错误,则通过反向传播调整各个参数。也就是说,大模型就是根据一个词之前的词或者周围的词是什么来计算出这个词的,这就是大模型的理论基础和学习方法。

      注意力虽然是大模型的核心概念,但除此之外,大模型还用到了其他技术,它们包括:

    • 第一步需要将词转为数值向量(vector)的嵌入层(embedding),这也是现在很火的向量数据库的那个向量,向量数据库就是用来查询哪些文本向量比较相似,从而提取出对应的文字材料的
    • 给词向量加上含位置信息的位置编码,以分辨出词的先后顺序,显然“我爱你”与“你爱我”的意义截然不同,因此词序是很重要的,而注意力本身并没有用到词序
    • 在注意力层之后的残差处理与归一化处理,技术原理并不难,感兴趣的读者可以看参考资料

      现在我们已经对大模型的主要技术原理有了粗浅的了解,下面来看看深度学习与大模型的特点。

    大模型的特点

      从对深度学习以及大模型的初步应用与原理了解,其实我们已经可以得出一些结论。

      首先,深度学习的算法模型是神经网络及其节点上的参数/权重构成的,深度学习是端到端的机器学习,它不用由人类专家预先设计特征算法,例如不用苦思冥想怎么设计一个提取图像中线条的算法,只需要用大量的高质量标注数据对其进行投喂,就有可能最终训练出质量不错的神经网络(中的各个参数)来。

      其次,它是一个黑盒子,因为没有特征设计,因此这么多数值构成的神经网络,它为什么能达到最终的目标,为什么某个参数就必须是最后训练出来的那个数值,为什么有的时候识别会失败或者生成会出错,是否可以手工修改某些特定的参数使得其成功率上升,同时又不会导致原来成功的事例失败,这些问题在现阶段都是难以回答的,这就是深度学习的可解释性问题。

      在ChatGPT之前,大模型其实也在发展,但是ChatGPT表现出来的多领域通用性和流畅性与之前的产品相比有明显的超越,所以造成了轰动。其后的GPT-4表现更为优异,在《Sparks of Artificial General Intelligence:Early Experiments with GPT-4》这篇论文里,微软团队给出了这样的一个示例:

    提问:Can you write a proof that there are infinitely many primes, with every line that rhymes?
    GPT4:Yes, I think I can,
    Though it might take a clever plan.
    I’ll start by noting Euclid’s proof,
    Which shows that primes aren’t just aloof.
    Assume we have a finite list,
    Of primes, and that none have been missed.
    Multiply them all together,
    And add one, just to be clever.
    The result will be a number,
    That has no prime factors, oh wonder!
    But every number has a prime,
    That divides it, it’s just a matter of time.
    So we’ve found a contradiction,
    And our finite list needs eviction.
    There must be infinitely many primes,
    And that’s the end of my rhyming lines.
    

      提问者让GPT-4写一首诗,来证明素数有无限多个,然后GPT-4很快就完成了这首诗,每两行押韵,而且用欧几里得的经典方法给出了证明。当然,在论文中还有很多让人印象深刻的例子,但是上面的例子已能说明GPT-4确实有了很强的能力。

      GPT中的G表示生成(Generative),这表示它主要的工作方式是生成内容,内容在这里主要是自然语言文本。按照OpenAI首席执行官Sam Altman的说法,The most important difference between ChatGPT and GPT-4, is that the GPT-4 predicts the next word with greater accuracy,即 ChatGPT与GPT-4最大的差异就在于GPT-4在预测下一个词的准确度比ChatGPT更高。

      GPT中的P表示预训练(Pretrain),即首先用大量语料训练出基础模型(foundation model),然后再用下游任务相关的语料进行精调(FT,即finetuning)。这些下游任务可能是文本分类、翻译、对话等等,这样就可以不用单独为某个特定任务从头训练了。我们可以把中学教育和通识教育看作是预训练,它为大学最终的专业选择,以及以后更细的工作分工打下了坚实的基础。反过来看,在小学年龄阶段没有上学可能会对以后的择业带来很大的限制,这也可以看成是大脑在应预训练的阶段没有进行有效的预训练导致的问题。

      GPT中的T表示transformer,这个已经在上文中介绍了。

      一般认为,GPT-4有更强能力的原因在于:

    • 它提供了大量的高质量数据,原始数据有45T,清洗后的语料是570G,清洗比例接近1%,这是之前几乎没有团队做到的
    • 数据中混合了大量的代码,原始数据中有830G代码,这一般被认为是推理能力提升的关键点之一,当然另一方面也大大提升了它的代码能力
    • 展开了大量不同种类的下游任务,如生成、问答、脑暴、闲聊、摘要、分类、提取等等,以上两点也属于多样化工作,它为GPT4的通用性打下了基础
    • 使用了基于人工反馈的增强学习(RLHF)方法,召集了40个众包团队,撰写了数十万的提示数据以对齐主流价值观

      一般认为,大模型的表现之所以如此智能,但是之前的小模型神经网络却那么智障,其原因可能在于涌现(emergence)。涌现可以简单认为是单个个体微观上简单的行为,在宏观上大量复合呈现出难以预料的规律。比如每只蚂蚁其行为其实是挺简单的,但是一群蚂蚁在一起,就可以表现出复杂的规律。又如每个神经元的行为都很简单,但是这么多神经元聚集在一起,就形成了聪明的人类大脑,这也算是一种涌现。

      涌现最直观的例子可能就是康威的生命游戏(Conwey’s Life Game)了,这个游戏是在一个网格平面(类似围棋棋盘)上发生的,每个个里要么有一个存活的细胞,(用黑色格表示),要么就是一个死亡的细胞(用白色格表示),其规则也很简单,只有以下四条:

    • 当前细胞存活时,当周围存活细胞<2时,该细胞死亡(模拟生命数量稀少)
    • 当前细胞存活时,当周围有2个或3个存活细胞时,该细胞保持存活
    • 当前细胞存活时,当周围存活细胞>3时,该细胞死亡(模拟生命数量拥挤)
    • 当前细胞死亡时,当周围存活细胞=3时,该细胞复活(模拟繁殖)

      那么图5里的四个样式就表示绝对静止的细胞群体。

    图5 康威生命游戏中绝对静止的细胞群体

      对每个样式进行分析很快就会知道为什么它们会绝对静止。以第二个样式为例,其每个存活细胞周围都刚好有两个存活细胞,按照规则2,它们都应该保持存活。而任何一个死亡细胞周围都没有四个存活细胞,因此此样式将永远不变。

      图6给出了震荡循环的细胞群体。

    图6 康威生命游戏中震荡循环的细胞群体

      按照康威生命游戏的规则,可以发现图6中的两个样式会演变几步之后又变成当前的样式。以第二个样式为例,其一共有左中右三个存活细胞。左侧与右侧的存活细胞附近只有一个存活细胞,因此按照规则1,会在下一轮死亡。中间的存活细胞附近有两个存活细胞,因此按照规则2,保持存活。同时又可以发现,中间的存活细胞上侧和下侧的死亡细胞由于其附近有三个存活细胞,因此根据规则4,在下一轮它们将复活。以此类推,样式会由横三转为纵三,又转回横三,永远震荡循环。

      康威生命游戏有着远超过上述样式的复杂度,在宏观上甚至可以看到游走、巡回、扩张、凋零等多种细胞社群的样式,因此四条简单的微观规则就衍生出了让人事先难以预料的宏观样式上的复杂度,是涌现的一个生动形象的例子。

      以ChatGPT与GPT-4为代表的大模型由于其使用了自然语言对话而引发了轰动,让普通人都能直观感受到大模型的魅力,但同时它作为一个基础设施,也提出了一个难题,就是它的编程接口是基于自然语言的,所以需要做所谓的提示工程(prompt engineering)。所谓提示工程,指的就是想让大模型好好干活,那就需要自己好好琢磨怎么和大模型好好说话。俗话说见人下菜,或者说见人说人话,见鬼说鬼话,那见了ChatGPT,当然就得说ChatGPT话了,不然它就没法理解问题,自然也没法给出好的回答了。Linux圈子里有Linus大佬的一句名言:“talk is cheap, show me your code”,中文翻译也很传神:“废话少说,放码过来”,俗一点的话那就是“少哔哔,秀代码”,但是自打GPT横空出世,以后可能就是“code is cheap, show me your talk”了,毕竟,给GPT一个提示,它可以还你百行代码。

      不过神经网络毕竟是一种信息压缩,或说是一种函数拟合,因此中间肯定会有信息损失,或说是自己瞎想的填补空白,那就避免不了GPT一本正经的胡说八道,也就是所谓的幻觉(hallucination)了。幻觉是当前大模型应用的主要障碍之一,一般认为,大模型近期的发展将沿着消减幻觉、工具集成(即能使用外部工具)、多模态(即除了文本以外,也能理解和生成图形、语音、视频等内容)、垂直领域、类脑智能、具身(embodied)智能等方向发展。

    Monday, August 21, 2023

    背景

      从去年底以来,AIGC 炙手可热,多个业界大佬都认为 AIGC 会给整个产业带来一场革命,甚至所有的软件都会用 AI 重写。从历史上来看,人机交互方式的变革往往会将操作系统带入下一个世代,著名的例子如从命令行界面的 DOS 到键鼠图形界面的 Windows,以及带来触控界面的 iPhone,领创者都成为了世界顶级企业,带动了整个生态的发展。

      从技术上来看,AIGC 是基于大模型的,而大模型的基础是深度学习,因此,为了在产品上结合 AIGC,首先从技术上首先需要对深度学习进行有深度的学习。

      对深度学习与大模型的探索将由一系列文章组成,本文是系列里的第一篇,主要关注的是深度学习的技术入门探索。

    从神经元开始

      回溯历史,深度学习起始于向人类的大脑学习如何学习。人类大脑皮质的思维活动就是通过大量中间神经元的极其复杂的反射活动,因此不妨先看看神经元的工作机制。

    图1 神经元结构

      图1给出了神经元的大体结构,左边是神经元的主体,其输入是左侧的多个树突,其输出是右侧的一个轴突。只有当输入树突的信号足够强烈的时候,输出轴突上才会有信号产生。受此启发,就可以设计一个最简单的有两个输入x1与x2,以及一个输出y的线性函数来模拟单个神经元,引入阈值θ,当 w1x1 + w2x2 ≥ θ时,y为1(表示有信号),否则y为0(表示无信号)。其中w1与w2分别是x1与x2的参数或权重(weight)。

      有了这个函数,下面来看看它究竟能做什么。按照逻辑主义的设想,数学可以通过逻辑推衍出来,那么不妨看看,上面的函数是否可以表征出基本逻辑运算,如与、或、异或等,在这里x1、x2与y的取值都只能是0或1。

      对于逻辑与来说,只有当x1与x2都是1的时候,y才是1,否则y是0,容易尝试得到一组可能的w1、w2与θ,分别是0.5、0.5与0.7,如图2所示。

    图2 逻辑与的线性函数图

      图2中横轴为x1,纵轴为x2,从图2中可以看到,(1, 1) 点为实心圆,表示y为1,在(0, 0)、(0, 1)与(1, 0)都是空心圆,表示y为0,中间的虚线表示w1x1 + w2x2 = θ这条直线,只要这条直线能将(1, 1)点与其它点划分到不同区域,则显然就可以找到至少一组w1、w2与θ满足条件。基于同样的分析,容易知道逻辑或也可以找到对应的w1、w2与θ。但是对于逻辑异或来说,问题就严重了,显然无法找到满足条件的w1、w2与θ,如图3所示。

    图3 逻辑异或的函数图

      逻辑异或是当x1与x2中一个为0,另一个为1时y才为1,否则y为0,因此在图3中,点(0,1)与点(1,0)为实心圆,而(0, 0)与(1, 1)为空心圆,显然是无法找到一条直线将两个实心圆与两个空心圆划分在两个不同区域的。因此,上述最朴素的线性神经元函数无法表示逻辑异或,也就意味着有大量的运算无法通过上述线性神经元函数来进行。

    引入激活函数

      是否能改造上述函数,让它能支持所有运算,从而能承担学习的任务呢?至少,人脑肯定是能学会异或的。现在看来,主要是因为原始的神经元函数太线性导致的这个问题。因此,在深度学习中,就引入了非线性的激活函数(activation function),如图4所示。

    图4 引入激活函数

      在图4中,首先原函数被修改成了支持多个输入和多个输出的线性变换函数,这样就能处理更多种类的问题了。因为有了多个输入x1、x2…xm与多个输出h1、h2…hn,因此权重的下标也带有两个数字,以表示每个权重的作用,例如 w12 是输入x2与输出h1间的权重。还有一个特殊的权重bi,它被称为偏置(bias),是一个待确定的常数项。这样,h就等于相应的x与w相乘后再加上b。例如,hi = xiwi1 + x2wi2 + … + xmwim + bi。   经过线性变换后得到的输出h1、h2…hn只是中间过程的输出,在之后,还需要加入一个非线性的激活函数的处理,以得到最终的输出y1~yn,如图4所示。

      在具体激活函数的选择上,比较常见的有 softmax、sigmoid 与 relu 等。其中 softmax 函数是多分类问题最常用的输出激活函数(多分类问题指的是一个问题有多个确定个数的可能答案,例如是/否问题是二分类问题,而分辨一个手写阿拉伯数字是哪个数就是一个十分类问题,因为可能答案有0~9一共十个),softmax也是包括ChatGPT在内的大模型使用的输出函数。   使用了激活函数以后,神经网络就可以学习到所有函数了。下面来看一个经典的神经网络的例子,手写数字识别问题,或MNIST问题。MNIST涉及的手写数字在网上是公开的,如图5所示。程序员们可以先想想,如果自己来写一个程序识别手写数字会怎么写。可以识别手写数字的(一个)神经网络的结构如图6所示。

    图5 MNIST手写数字样例

    图6 能识别手写数字的神经网络

      可以看到图6的神经网络一共用到了三个线性变换,并使用了两个sigmoid 激活函数,以及最后的softmax激活函数,因此可以说这个神经网络是三层的。神经网络的输入(x1~x784)是一个长度为784的数组,其实就是一个28x28=784的手写数字的黑白图像。神经网络的输出(y1~y10)分别代表了0~9的阿拉伯数字,这是一个典型的十分类问题,因此使用softmax也是非常自然的。

      图6中的神经网络一共有(784x50+50) + (50x100+100) + (100x10+10) = 45360个参数,对比ChatGPT上千亿个参数,这显然是一个微模型,但是它的识别能力却可以达到92.53%,也就是说一万个手写数字,它能正确识别出9253个来。   那问题就来了,这45360个参数是怎么来的呢?肯定不能是随便什么 45360 个数都能带来这么高的识别率的,要解决这个问题,就需要看看神经网络是怎么学习的了。

    神经网络的学习

      在上面已经看到,神经网络里有大量的参数。在最开始,这些参数会被随机分配一些数字(当然如何随机分配也有讲究的,简洁起见,此处先不提),此外也需要准备大量的数据,这些数据一般是多个输入输出的对(x, t)。例如在上面的手写数字识别问题中,输入x就是一个28x28的手写数字图像,输出t就是这个图像对应的0~9中的一个数字。   这些数据会被分成训练集与测试集。训练集中的数据用来训练神经网络,让神经网络中的参数最终达到正确的值。测试集中的数据用来测试训练后的神经网络,对比看训练后的神经网络在新的数据下得到的结果是否正确。

      神经网络的训练过程可以大体分为下面几步:

    • 对训练集中的输入输出对(x, t)进行如下处理

    • 将x输入到神经网络中,计算得到y

    • 将y与正确的输出t进行运算得到损失L,损失的计算函数一般是均方差或交叉熵,前者针对的是回归问题(连续函数拟合),后者针对的是分类问题

    • 根据L调整神经网络的参数,调整的方向是减少L,调整的方法是下面要讲的反向传播

      图7给出了神经网络训练的过程。

      图7 神经网络训练过程

        一旦训练完毕,使用的时候就不需要正确输出t,也不需要计算损失L和调整神经网络的参数了,这个过程被称为推理(inference),如图8所示。

      图8 神经网络推理过程

        顺便说一句,图中的深度神经网络与神经网络结构是一样的,但是层数较多,因此被称为深度神经网络。

        下面,再来看看神经网络究竟是怎样通过损失L来调整网络参数的。最简单,也是最直观的方法就是将每个参数都稍微调大或者调小一点,看L会如何变化,如果L变小,则保持此参数的调整,如果L变大,则将此参数反过来调整。以上即正向调整法,思路清晰,操作方法简单,但是计算量极大,因为每调整一个参数就要重新计算一遍y与L。

        另一种方法就是现在主流的反向传播(BP,backpropagation)法,此方法类似系统发生故障时的根因分析,首先分析最后一层的参数是怎样影响到L的,然后分析倒数第二层的参数是如何影响到最后一层的输入的,如此类推。在数学上,其实就是计算L对某个特定参数w的(偏)导数,因为导数就代表了w的变化会导致L如何变化。根据链式求导法则,L对w的导数等于L对中间变量h的导数乘以h对w的导数,前者相当于计算最后一层参数的导数,后者相当于计算倒数第二层参数的导数,两者相乘即为L对导数第二层参数的导数。

        下面主要通过求导来展示反向传播,如果希望更直观一点,可以阅读计算图相关的资料。假设真实函数是y=2x+1,则待求函数为wx+b(当然w与b的真实值应该是2与1)。下面通过一组数据(训练集)来通过反向传播逐步计算更新w与b,看看它们否会逐渐逼近2与1。

        由于这是一个回归问题,因此使用均方差(y-t)< sup>2< /sup>/2作为损失L的函数,显然L对y的导数是y-t,参数更新使用经典的梯度下降法(SGD),即参数新值=参数旧值 - 学习率x(L对参数的导数),梯度下降有一个粗糙但是直观的理解,那就是学习应该向着导数(梯度)相反(下降)的方向走,在这里学习率这个参数设为0.01。

        首先,将w与b随机化为0.5与0.6。

        假设第一个训练对为(0, 1),则 y = wx + b = 0.5·0 + 0.6 = 0.6,L对w的导数=L对y的导数乘以y对w的导数=(y-t)·x=(0.6-1)·0=0,L对b的导数=L对y的导数乘以y对b的导数=(y-t)·1=-0.4。则w的新值为w-0.01·0=0.5,b的新值为b-0.01·(-0.4)=0.604,显然新的w与b比原来的更接近(2, 1)。

        若第二个训练对为(1, 2.9)(本来应为1与3,但是增加了一点误差干扰),可以以同样的方法得到新的w为0.51796,而新的b为0.62196,显然比上一对w与b又接近了2与1一点。

        实际上,若继续增加2x+1附近的数据,可以发现到了十几对训练数对之后,w与b即可相当接近2与1了。

        以上例子是为了直观感受反向传播的计算而给出的,实际上这种线性函数的回归可以通过数据集基于矩阵一次性算出来,而且训练本身也要考虑收敛的问题,因此实际的深度学习会更复杂一些,但是原理是类似的。

        总地来说,深度神经网络是由多个层组成的,每一层均有前向(forward)推理的函数,用来从输入计算得到输出,这个过程即为推理。每一层也有反向(backward)传播的函数,用来从后一层传来的导数计算得到本层向前一层传递的导数,并同时更新本层的参数。如果是训练,则需要在最后一层再加上一个输入为t与y的损失层,输出为L,如图9所示。

      图9 多层神经网络结构

        通过以上几乎标准化的神经网络层,深度学习的研究者就可以像搭积木一样对多个层进行排列组合,得到多种多样的深度神经网络,并首先通过反向传播训练出神经网络的参数,继而使用神经网络进行推理应用了。

    Monday, August 14, 2023

    队员:复旦大学 朱元依、沈扬、朱俊杰
    指导老师:张亮、陈辰
    企业导师:王子冲

    本项目为2023年操作系统大赛企业赛道赛题。

    项目链接:DDE 控制中心自启动管理插件 github仓库

    1 摘要

    Deepin(原名Linux Deepin)致力于为全球用户提供美观易用,安全可靠的Linux发行版。该系统由深度科技自主开发,提供了美观易用、极简操作的桌面环境,主要由桌面、启动器、任务栏、控制中心、窗口管理器等组成。其中,控制中心是Deepin桌面环境的核心组件之一,它是用于管理和配置操作系统各种设置的集成工具。然而,在该控制中心中,并未对用户提供自启动项提供的便捷管理界面。在本项目中,我们为Deepin操作系统中自启动项的修改功能编写了简洁易用的控制中心插件,将自启动管理的系统功能集成到了控制中心中。该插件以单独的仓库提供,并能够单独构建,一键植入Deepin控制中心中。

    2 需求分析

    最终用户对自启动权限的管理目前只能通过dde-launcher(启动器/“开始菜单”)的右键菜单进行管理,而控制中心作为控制系统的门户应用反而缺少此功能。

    用户需求

    在官方发布的deepin23-Beta版本中,对于用户程序与系统程序的自启动管理方法为:找到应用的可执行程序(通常为.desktop)类型的文件,通过右键打开功能菜单的方式设置为开机自启动。但由于操作系统自带的应用程序界面中所有程序都会被展示到,所以当操作系统中应用程序过多的时候用户很难统计到那些程序被设置成了开机自启动,同时对于用户不想参与管理的应用也会展示在应用菜单中。因此我们对于本项目开发所面向的需求是在控制中心(deepin-control-center)中在不影响原有插件所提供的设置服务的基础上,为用户提供一个额外的插件用于管理开机自启动软件,同时插件可以满足用户自行选择需要手动维护的自启动程序,对于用户希望自己管理是否自启动的软件显示在面板上可以对是否自启动进行开关,用户不希望管理的软件默认不自启动不显示到面板;面板也可以提供添加和删除的功能让用户挑选出自己想要在面板上操作的应用。

    功能需求

    deepin作为国产开源的深度Linux桌面系统,不仅为用户提供了人性化、个性化以及对于中文等语言有良好支持的操作系统体验,也为Linux的开发者与学习者提供了控制中心(dde-control-center)与任务栏(dde-dock)等桌面控件的开发者接口与插件注入接口。除了可以不断的扩展完善用户需求与用户体验外,deepin深度桌面在操作系统学科的教学与深入理解方面也带来了很多的可能性与创造性。因此作为OS大赛的参与团队,我们不仅是在希望我们的开发会对deepin项目的完整性、deepin用户的体验感上带来一些帮助,同时也想通过我们自己的努力在学生的视角让操作系统的教学与后续学习有一个优秀的案例和一些开发相关的经验总结,使得我们理论层面的操作系统教学可以有更大的实践空间。

    2.1 当前方案

    目前已有的自启动管理方法是在开始菜单中对菜单中所展示应用软件单独进行自启动的设置。具体的方法是对所希望设置自启动项的应用软件选中后右键,点击“设置开机自启动”即可在每次开机时自动打开该应用软件。

    默认自启动设置方式

    然而,这种方法有两大明显的缺陷:(1)无法向用户展示所有的自启动项设置(2)大批量的自启动项修改极其不便。由此,催生了控制中心自启动管理插件的需求。

    2.2 插件需求

    为了完成控制中心插件,我们对需求进行了更细致的刻画。经过总结后,插件的需求主要分为三条:

    1、完成一个控制中心插件,能够展示当前所有开机启动项的列表; 2、能够在插件中,通过用户界面的交互来管理(添加、删除、启用、禁用)开机启动项; 3、插件以单独的仓库提供,并能够单独构建,不需要合并入 dde-control-center 项目。

    其中,第一条需求是该插件的基础。自启动项的列表一方面为用户提供了清晰的展示界面,另一方面也是程序与用户交互,获取修改操作信息的基础。 第二条需求总结了该插件需要支持的功能,即添加、删除、启用、禁用,这些功能需要在前端设计对应的交互界面,同时在后端设计对应的操作接口,调用系统接口以修改自启动设置。 第三条需求与系统发布相关。目前最新的稳定发布版本是Deepin V23 Beta,而官方版本已经在发布,若修改源码统一编译控制中心,会需要对当前已发行的操作系统版本进行修改,较为不便。故需要单独编译该插件,并将其装载到系统中的插件接口中。

    3 相关资料调研

    3.1 Deepin开机自启动系统设置

    Deepin系统通过检测固定的目录,检测自启动项。通过放置应用程序的.desktop文件在其中一个自动启动目录中,系统可以检测到该应用程序的自启动设置。通过修改.desktop文件中的对应字段,可以修改对应应用程序的自启动设置。

    3.1.1 自启动目录

    “desktop base directory specification”中的“Referencing this specification” 部分进行定义了自动启动目录是 $XDG_CONFIG_DIRS/autostart

    如果同一文件名位于多个自动启动目录下,只应使用最重要目录下的文件。

    示例:

    如果未设置$XDG_CONFIG_HOME,用户主目录中的自动启动目录为~/.config/autostart/

    如果未设置$XDG_CONFIG_DIRS,系统范围的自动启动目录为/etc/xdg/autostart/

    如果未设置$XDG_CONFIG_HOME$XDG_CONFIG_DIRS,并且两个文件/etc/xdg/autostart/foo.desktop~/.config/autostart/foo.desktop存在,那么只有文件~/.config/autostart/foo.desktop将被使用,因为~/.config/autostart//etc/xdg/autostart/更重要。

    3.1.2 应用程序的.desktop 文件

    一个应用程序的.desktop文件必须符合"桌面入口规范"中定义的格式。所有关键字应按照定义进行解释,但以下情况除外,以便考虑到位于自动启动目录中的.desktop文件不会显示在菜单中。

    Hidden关键字

    .desktop文件的Hidden关键字设置为true时,该.desktop文件必须被忽略。当多个具有相同名称的.desktop文件存在于多个目录中时,仅应考虑最重要的.desktop文件中的Hidden关键字:如果其设置为true,则其他目录中具有相同名称的所有.desktop文件也必须被忽略。

    OnlyShowInNotShowIn关键字

    OnlyShowIn项可以包含一个字符串列表,用于标识必须自动启动此应用程序的桌面环境,其他桌面环境不得自动启动此应用程序。

    NotShowIn项可以包含一个字符串列表,用于标识不得自动启动此应用程序的桌面环境,其他桌面环境必须自动启动此应用程序。

    这两个关键字中的一个,要么是OnlyShowIn,要么是NotShowIn,可以出现在单个.desktop文件中。

    TryExec关键字

    带有非空TryExec字段的.desktop文件如果TryExec关键字的值与已安装的可执行程序不匹配,则不得自动启动。TryExec字段的值可以是绝对路径,也可以是没有任何路径组件的可执行文件名。如果指定了没有任何路径组件的可执行文件名,则会搜索$PATH环境以找到匹配的可执行程序。

    注意事项

    如果通过在系统范围的自动启动目录中安装.desktop文件来自动启动应用程序,则个人用户可以通过在其个人自动启动目录中放置具有相同名称的.desktop文件来禁用此应用程序的自动启动,并在其中包含Hidden=true关键字。

    3.2 控制中心插件开发

    V23控制中心特性

    1、V23控制中心只负责框架设计,具体功能全部由插件实现; 2、V23控制中心支持多级插件系统,支持插件插入到任意位置中; 3、高度可定制,可定制任意插件是否显示,若插件支持,可定制任意插件内容是否显示。

    V23控制中心插件安装路径说明

    1、控制中心会自动加载翻译,翻译目录需要严格放置在/${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/dde-control-center/translations下,控制中心会自动加载,同时,插件的翻译和名称也有要求,命名为${Plugin_name}_{locale}.tslocale就是多语言的翻译,翻译文件必须控制和插件名称相同; 2、控制中心的so应该放置在/${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTA;LL_LIBDIR}/dde-control-center/modules下,请使用构建系统的提供的gnuinstall路径,上面举的例子是cmakemesonbuild也有自己的逻辑。

    V23控制中心开发接口说明

    1、ModuleObject类用于构建每个页面元素,其是插件的核心; 2、PluginInterface类用于规范插件信息,每个插件必须提供一个`ModuleObject对象。

    标准开发流程示例

    1、继承PluginInterface,实现其虚函数; 2、实例化一个根模块,根模块在初始化时不允许有耗时操作,若有耗时操作,应继承ModuleObject然后实现active方法,将耗时操作放入其中; 3、若根模块的子项是横向菜单列表,则可使用List储存其基础信息,继承或使用HListModule类,然后循环使用appendChild方法将菜单添加到根模块中; 4、若根模块的子项是纵向菜单列表,则可使用List储存其基础信息,继承或使用VListModule类,然后循环使用appendChild方法将菜单添加到根模块中; 5、以此类推,具体的某个子项菜单同样再次添加菜单列表,直到菜单列表的子项为PageModule时为止; 6、准备一个以上的Module继承自ModuleObject,并实现其page()方法,然后添加到PageModule中,注意,page()方法中需返回新的QWidget对象; 7、当某个菜单为PageModule时,使用其appendChild方法将上方的Module添加到其子项中,此时,控制中心会根据page的大小添加滚动条,并将多个page进行垂直排列进行显示。PageModule持支嵌套,并且其有默认边距,如果嵌套使用,嵌套的PageModule边距建议设置为0; 8、若某个VListModulePageModule页面需要附加按钮时,可调其子项ModuleObjectsetExtra,该ModuleObjectpage提供按钮,这样该ModuleObject将显示在VListModulePageModule页面的最下方。

    4 系统框架设计

    4.1 项目组织方式

    类图

    类图

    项目文件组织

    .
    ├── CMakeLists.txt
    ├── include
    │   ├── interface
    │   │   ├── ...
    │   └── widgets
    │       ├── ...
    ├── misc
    │   ├── ...
    ├── shell.sh
    ├── src
    │   ├── frame
    │   │   ├── ...
    │   ├── interface
    │   │   ├── ...
    │   ├── plugin-selfstartup
    │   │   ├── operation
    │   │   │   ├── ...
    │   │   └── window
    │   │       ├── ...
    │   └── widgets
    │       ├── ...
    └── translations
        ├── ...(translation files)
        ├── desktop
            ├──...(desktop translation files)
    

    4.1 总体思路与系统框架

    4.1.1文件框架思路

    通过阅读dde-control-center的源代码,src部分为插件的实现以及实现代码复用所存在的框架代码,代码的插件部分在src/plugin-selfstartup目录,分为operation和window部分,其中windows部分主要负责控制中心自启动项目的界面构成,operation部分负责参与控制中心对系统文件与配置的控制。translation文件夹为控制中心(dde-control-center)提供了不同语言环境下的支持,通过识别系统的语言环境选择展示到面板不同的语言。include与misc为项目注册到控制中心所必须包含的编译依赖文件。

    4.1.2 设计思路

    我们在初赛中已经完成了通过注册到dde-dock实现的自启动插件,对于插件的界面以及界面上按钮对应的功能已经有了一个大致的构想,但由于dde-dock的插件是一个较为独立的结构,而dde-control-center里面的每一个插件都需要往控制中心里注册一个module并且通过rpc方法与dde-application-manager进行远程服务的交流,因此在实现细节上dde-control-center插件和dde-dock插件有着很大的区别。

    用户逻辑方面,我们首先在控制中心实现了管理自启动应用的面板,对于添加到维护列表的应用都会被展示到面板上,对于每一项应用都会提供Enable/Disable的选择按钮和Delete的删除按钮,用于管理是否开机自启动与不希望维护改应用的删除。同时在面板有一个添加按钮,用户点击后会打开文件对话框,用户可以自行从中选择自定义路径的应用程序添加到插件维护的列表中。

    插件运行逻辑方面,分别有添加、删除、开启/关闭(反转)逻辑,分别思路如下:

    • 添加逻辑:用户在点击添加按钮后会获取该文件的路径并读取FileInfoAddButtonWidget发送requestCreateFile信号携带参数Category名称与FileInfoWorker中;Worker/home/user/.config/autostart文件下判断是否存在一个相同的应用信息,如果不存在则创建一个原.desktop文件的副本并且添加一行Hidden=false字段,把应用信息存到App结构体中,同时调用Worker中对应的Category的添加函数传入赋值好的App,Category在内存中同步一份自启动应用信息,最后更新前端页面。
    • 删除逻辑:在用户点击应用对应行的删除按钮后获取到AppID,通过getAppById得到App信息后发送requestDelUserApp信号到WorkerWoker找到autostart文件夹中对应文件并将其删去,同时把App在对应的Category中移除,最后更新前端页面。
    • 反转逻辑:在用户点击对应行的打勾按钮后想获取到AppId,通过getAppById得到App信息后发送反转请求到WorkerWorker读取磁盘中autostart里对应.desktop文件文本找到Hidden字段并将其反转,同时调用Category把内存中App结构体的Hidden成员反转,最后更新前端页面。
    • 启动逻辑:插件启动的时候会调用CategorygetAppItem函数,该函数从autostart文件夹中逐文件读取信息存在App结构体中,封装到m_appList作为初始化时参与维护的应用程序,前端从m_appList中把应用名称以及是否自启动信息列举到页面

    4.2 类功能说明

    operation部分
    ├── defappmodel.cpp
    ├── defappmodel.h
    ├── defappworker.cpp
    ├── defappworker.h
    ├── mimedbusproxy.cpp
    ├── mimedbusproxy.h
    └── qrc
    

    operation部分是插件的后端部分,对于控制中心的每个插件都有ModelWorkerDBusProxy三个部分:

    • ModelModel部分通过继承QObject注册到QT的项目中,私有变量Category实现了自启动信息在内存中的一个副本用于前端的交互。
    • WorkerWorker部分提供了插件对文件系统的操作。由于操作系统对于开机自启动的支持在于把对应的.desktop文件拷贝到/home/user/.config/autostart中并设置Hidden=false,因此对于自启动应用管理的插件必须要对文件的读写提供支持,该支持由Woker部分实现。
    • DBusProxy:由于插件要注册到控制中心并且对应用进行管理,因此需要向运行中的应用程序管理服务(dde-application-manager)进行交互,管理服务提供了rpc的调用接口,插件通过DBusProxy部分向管理服务发起远程请求。
    window部分
    ├── selfstartup.json
    ├── selfstartup.cpp
    ├── selfstartupdetailwidget.h
    ├── selfstartupplugin.cpp
    ├── selfstartupplugin.h
    └── widgets
        ├── addbuttonwidget.cpp
        ├── addbuttonwidget.h
        ├── category.cpp
        └── category.h
    

    window部分是插件的前端部分,由PluginDetailwidgetAddbuttonwidgetCategory四个部分:

    • PluginPlugin部分构造了自启动程序插件。包括插件接口的初始化,一级页面的初始化和二级页面的初始化。
    • DetailwidgetDetailwidget部分构造了自启动程序插件的 app条目。包括app条目的外形、位置,app条目的增删改查操作,以及与workermodel的交互操作(通过信号和槽函数实现)。
    • AddbuttonwidgetAddbuttonwidget部分构造了自启动程序插件的加号按钮。包括加号按钮的外形、位置,新增app的弹窗显示,新增app的路径处理,以及与workermodel的交互操作(通过信号和槽函数实现)。
    • Category: category部分为磁盘中autostart文件夹中重要信息在内存中的拷贝,用于插件的窗口部分直接获取到该文件夹中.desktop类型文件的重要字段展示到界面。

    4.3 实现描述

    4.3.1 DefAppModel

    名称功能
    DefAppModelModel构造函数
    ~DefAppModelModel析构函数
    getModSelfSetUp返回SelfSetUp内存Category

    4.3.2 DefAppWorker

    名称功能
    DefAppWorkerWorker构造函数,连接Worker和Model
    DefaultAppsCategory枚举类,用于实现插件的可扩展性,实现对不同类型软件的分类,默认状态为只有SelfSetUp类
    active向应用程序管理服务发出blockSignal(false)消息
    deactive向应用程序管理服务发出blockSignal(true)消息
    onReverseUserApp对参与维护的自启动应用开关反转处理,把autostart中的.desktop文件Hidden字段反转并同步Category
    onGetListApps与Model处理应用变化信号结束的信息提供的一个空接口,只用于承接信号处理
    onDelUserApp在autostart文件夹中删去用户不希望继续维护是否自启动的应用并同步Category
    onAddUserFile向autostart中添加用户希望维护是否自启动的应用并同步Category
    getCategory返回应用类型的分类

    4.3.3 MimeDBusProxy

    名称功能
    MimeDBusProxyDBusProxy构造函数。
    DeleteApp向应用程序管理服务发送删除App请求。
    AddUserApp向应用程序管理服务发送添加用户App请求。
    ListApps向应用程序管理服务发送展示所有App请求。
    Change向应用程序管理服务发送App变动处理请求。

    4.3.4 SelfStartupDetailWidget

    名称功能
    SelfStartupDetailWidget自启动软件条目窗口构造函数。初始化条目窗口中的文字不可编辑、icon大小、条目形状、条目不可移动,初始化存储软件列表的QStandardItemModel,初始化软件条目的布局。
    ~SelfStartupDetailWidget自启动软件条目窗口析构函数。
    setModel设置自启动软件条目窗口的当前model。根据当前窗口的分类,设置不同的窗口model(由于本插件目前只有一个分类,因此setModel功能相当于直接调用setCategory功能)。
    setCategory设置自启动软件条目窗口的当前分类。将分类的增、删、改的信号和对应的自启动软件条目窗口的槽函数连接,将分类中的软件放入存储软件列表的QStandardItemModel中,并更新自启动软件条目窗口。
    updateListView更新自启动软件条目窗口。依次读取自启动软件条目窗口的当前model中的每一个软件状态,依照软件状态,更新窗口显示(显示是否自启动、软件名称、软件icon、删除按键)。
    getAppIcon获取软件的icon。从系统中获取软件的icon,并统一调整为32*32大小。
    getAppById通过ID获取APP结构体。遍历分类中的app,返回对应的APP结构体。
    appendItemData向model中新增app信息。从APP结构体中获取app信息,向model中新增app,并更新总app数量。
    isDesktopOrBinaryFile判断文件是否属于桌面或二进制文件。
    isValid判断app是否有效。判断app的ID非空。
    reverseItem向category发出app自启动状态转换的信号。
    requestDelUserApp向category发出删除app的信号。
    onListViewClicked自启动软件条目窗口被点击后的槽函数。从自启动软件条目窗口获取app信息,并向category发出app自启动状态转换的信号。
    onDelBtnClicked自启动软件条目窗口删除按钮被点击后的槽函数。从自启动软件条目窗口获取app信息,并向category发出删除app的信号。
    onClearAll清空model中所有的app信息。
    getAppListview返回model中所有的app信息。
    AppsItemChanged重置model中所有的app信息。依次将app_list中的app信息存入model中,并连接激活、点击信号。
    onReverseAppcategory返回app自启动状态转换信号的槽函数。更新对应model中app的自启动状态,并更新窗口。
    addItemcategory返回app新增信号的槽函数。向model中新增对应的app信息,并更新窗口。
    removeItemcategory返回app删减信号的槽函数。向model中删减对应的app信息,并更新窗口。
    showInvalidText设置自启动软件条目窗口的字体、图标的位置、大小。

    4.3.5 SelfStartupPlugin

    名称功能
    SelfStartupPlugin自启动程序插件的构造函数。基于DCC_NAMESPACE::PluginInterface的接口。
    name返回自启动程序插件的名称。
    module自启动程序插件初始化函数。初始化自启动程序插件的一级页面、二级页面和加号按钮。
    location返回自启动程序插件的位置。即,在控制中心插件中的排序。

    4.3.6 SelfStartupModule

    名称功能
    SelfStartupModule自启动程序插件一级页面的构造函数。初始化一级页面的名称,描述,图标,work,model。
    ~SelfStartupModule自启动程序插件一级页面的解构函数。向m_work、m_model发送删除信号。
    work返回m_work。
    model返回m_model。
    active激活m_work。

    4.3.7 SelfStartupDetailModule

    名称功能
    SelfStartupDetailModule自启动程序插件二级页面的构造函数。初始化二级页面的名称,分类,work,model。
    page自启动程序插件二级页面的初始化函数。初始化DetailWidget,并将DetailWidget的app状态修改信号、删除信号与work的槽函数相连接。

    4.3.8 Category

    名称功能
    CategoryCategory类构造函数,继承QObject类,每个Category类里面封装相同类型的应用信息
    getName获取当前Category分类的名称
    setCategory设置当前Category的类型名称
    getappItem获取当前Category封装的应用信息
    clear清空当前Category储存的应用信息
    addUserItem把传入应用信息存到Category中并向前端发送更新信号
    delUserItem把目标应用从Category中删除并向前端发送更新信号
    reverseUserItem设置目标应用Hidden字段反转并向前端发送更新信号

    5 系统测试情况

    5.1前端测试

    完成插件安装后,可在 DDE 控制中心中看到名为“自启动程序”的管理选项:

    前端页面

    5.2 自启动管理功能测试

    点击改图标,可以进入自启动管理界面:

    自启动管理页面

    该界面展示了设置所有的自启动项的软件,如果在软件右边出现了蓝色勾,则说明该软件设置了开机自启动。可以通过点击蓝色的勾取消开机自启动的设置,再次点击则可恢复。由此实现了自启动管理的开启、禁用功能。

    底部的加号用于添加需要进行自启动管理的软件,点击后跳出选择窗口:

    应用选择页面

    进入软件所安装的文件夹,选择该软件,即可将其添加入自启动管理中。在自启动管理界面中,可以点击软件右侧灰色的叉号,以将该软件剔除出自启动管理的界面。由此实现了自启动管理的添加、删除功能。

    6 插件的可扩展性

    本次项目的插件完整的覆盖了OSCOMP-proj223的所有需求,但由于只是一个短期开发的项目,因此有构想到了很多可以对插件进行扩展的方面;同时deepin作为开源社区也为开发者提供了良好的扩展接口,我们考虑了以下几点的扩展路径:

    6.1 语言扩展

    对于translation 板块,我们修改和编译仅仅使用了dde-control-center的zh_CN部分,限于语言广度我们只能提供插件的简体中文和英文模式,对于deepin可以支持的其他语言还有待扩展

    6.2 用户体验扩展

    由于Linux系统贯彻了"everything is a file"这一思想,因此整个操作系统的磁盘布局是文件化的,用户的应用可以安装到磁盘的任意地方,同时考虑到大部分Linux使用者为具有计算机基础能力的开发者,我们在插件中设计的添加应用按钮是让用户自主找到文件路径并添加。但是对于一般用户,这样的操作可能有些许复杂,所以我们考虑一个提高用户体验的方式:deepin中系统应用和从应用商店获取的用户程序分别分布在了两个文件夹中,如果用户没有自定义路径的话,我们的插件可以在用户请求添加的时候扫描这两个文件夹,提前给用户展示出可能用户希望添加的应用程序可供直接点击添加

    6.3 实时性扩展

    我们测试了dde-control-center的大部分插件,我们发现几乎所有都没有实现实时扫描磁盘的功能——即如果我们在dde-control-center外部对插件所管理的磁盘进行了修改的话,前端的界面并不会感受到更新而刷新界面。以自启动为例,我们实现了管理插件,但是用户仍然可以在应用菜单栏从控制中心外部添加到自启动中,此时自启动并不会更新页面,需要放回上一级窗口重新启动插件进行扫描。对于这一项扩展,我们考虑了一种方法为新起一个线程不停的扫描autostart文件夹中的.desktop文件并与Category中的比较,如果出现了不一致则发送(Q_Emit)更新窗口的信号,这样可以实现不返回上一级的情况下更新。但该功能只是作为扩展性的一个构想,实际实现的话首先带来的用户体验收益并不是很大,且已经存在插件的情况下应用场景很小,同时开启一个持续扫描的线程对于内存与插件的效率有较大的开销。

    6.4 并发性扩展

    插件的工作流程包含了修改磁盘与更新内存中的映射同时展示到面板上。对于磁盘的修改会涉及到一定次数的I/O,因此时间开销一定会比内存中信息更新更大的。因此我们思考了可以在此插件的基础上在新起一个进程,用rpc的方式对插件提供服务,服务内容包括对autostart文件夹的增删查改,插件只需要修改内存中的部分并作为rpc客户端向磁盘I/O的服务发送函数调用请求即可

    6.5 一致性的扩展

    因为插件会同时修改内存与磁盘的信息,并且保证二者相同,此时就会存在一定的一致性问题:

    • 磁盘和内存写入不同步,在其中一个进行的过程中发生了崩溃程序退出,此时是否成功写入成为一个一致性问题。我们的解决方式为先写入磁盘再写入内存,同时在重启插件的时候会重新扫描一次磁盘,这样只要写入磁盘的第一阶段成功之后即使崩溃也可以在重启的时候恢复数据
    • 多进程同时操作:如果有多个控制中心进程同时进行了文件的操作,采取的方式为读磁盘,写磁盘,写内存的操作,这样可以使得在读磁盘的时候如果有同名的应用被添加到里面会被正在写入(包括添加与删除操作)的进程阻塞读取,在写入过程结束之后才会触发读磁盘,然后写内存的时候判断是否存在,如果存在则跳过第三步,如果不存在则再写入磁盘

    附录A 插件安装

    1 开发环境配置

    1.1 配置 Deepin 操作系统

    开发环境:Deepin V23Beta版

    系统架构:x86

    镜像下载链接:https://mirrors.ustc.edu.cn/deepin-cd/releases/23-Beta/

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:https://blog.csdn.net/qq_44133136/article/details/105887560

    1.2 配置 Deepin 插件环境

    安装依赖包

    sudo apt build-dep .
    sudo apt install -y qt5-default
    sudo apt-get install dde-control-center-dev
    

    1.3 插件安装测试

    安装插件:
    sudo sh install.sh
    

    安装成功后,打开控制中心,会看到以下自启动插件图标,即为安装成功:

    控制中心插件图标

    此时,如果进入/usr/lib/x86_64-linux-gnu/dde-control-center/modules/文件夹,看到编译出的.so文件已经被下载到该文件夹中:

    插件安装位置文件夹

    1.4 插件卸载测试

    卸载插件:
    sudo sh uninstall.sh
    

    重启控制中心,可以看到原本的“自启动管理”图标消失,即为卸载成功。

    附录B 开发过程问题记录

    1 开发环境配置问题

    1.1 Deepin V20 Beta与Deepen V20.9

    在开发环境配置过程中,我们遇到了比较大的问题。

    我们起初顺延初赛的思路进行开发,在Deepin V20Beta版中进行开发。我们完成相关依赖包的安装后可以顺利地编译我们所写的控制中心自启动插件。但我们发现安装插件后的控制中心只剩我们所编译的自启动插件。(如下图中仅剩测试编译的 Default Applications 和 Self Start-up 插件)

    失败编译结果-仅剩插件

    我们推测是因为系统自动配置的控制中心框架与网上的控制中心开源代码版本不同,导致插件安装后框架复写。故我们只需重新编译控制中心的代码重新安装至虚拟机系统中即可完成测试。

    然而,在尝试编译控制中心时各种尝试均失败。报错DTK版本不符,未找出解决方法。在Deepen V20.9中亦未成功。

    1.2 Deepin V23Beta

    在遇到上述问题后,我们加入了Matrix的Deepin开发者社区,在线上与其他开发者讨论我们所遇到的问题。

    经过与其他开发者的讨论,我们得知最新的开源版本的控制中心已不再支持 20 版的Deepin系统。

    于是我们重新安装最新版本的Deepin V23Beta,成功的将我们所编写的插件植入控制中心中。

    2 图标与文字颜色展示问题

    我们初步的demo展示出来的界面所有管理的应用程序都是统一显示的默认应用程序的图标,但我们还是希望要和deepin桌面上.desktop文件相同的图标;与此同时,我们发现发现插件中的应用程序名字都是红色,同时希望这里是一个经典的黑色,这两个都是关于item的展示问题。我们关注到了这一段代码:

    QIcon icon = getAppIcon(iconName, QSize(32, 32));
    act->setIcon(icon);
    act->setTextColorRole(DPalette::TextTitle);
    act->setIconText(name);
    
    • 关于icon问题,我们可以对App结构体进行扩展,增加一条icon字段,通过读取带有图标应用程序.desktop文件中的icon字段存到当中,如果没有icon字段的程序设置为“application-default-icon”,并且在展示的时候调用act->setIcon()传入icon字段
    • 关于颜色问题,上述代码中看到了act->setTextColorRole()字段,里面传入了文字颜色类型的变量,在DPalette中定义了如下常量,初始选择的红色文字为TextWarning,我们只需要替换为TextTitle即可
    TypeName颜色类型
    ItemBackground列表项的背景色
    TextTitle标题型文本的颜色
    TextTips提示性文本的颜色
    TextWarning警告类型的文本颜色
    TextLively活跃式文本颜色(不受活动色影响)
    LightLively活跃式按钮(recommend button)背景色中的亮色(不受活跃色影响)
    DarkLively活跃式按钮(recommend button)背景色中的暗色,会从亮色渐变到暗色(不受活跃色影响)
    FrameBorder控件边框颜色
    NColorTypes无颜色类型

    3 翻译问题

    翻译主要涉及到:插件名的翻译和自启动软件名的翻译。

    对于插件名的翻译。我们学习了DDE-NETWORK-CORE仓库的做法,首先我们编写翻译成不同语言的.ts翻译文件,使用CMakelists中的qt5_add_translation指令获得.qm翻译文件,再将翻译的.qm文件install进入${CMAKE_INSTALL_DATAROOTDIR}/dde-control-center/translations(通常是$/usr/share/dde-control-center/translations),在通过loadTranslator载入系统对应语言的翻译文件。

    对于自启动软件名的翻译。考虑到部分软件缺失官方翻译名称,无法软件的信息中直接获取,我们目前只采用英文格式。

    4 开机启动项处理思路问题

    为了实现插件可视化管理软件的开机自启动,了解Deepin系统开机自启动的功能实现是至关重要的。

    通过查阅资料与实践,我们了解到Deepin系统包含自启动文件夹~/.config/autostart,该文件夹类似于 Windows 下的启动文件夹,系统开机时会执行该文件夹下的每个 desktop 文件 Exec 参数指向的脚本或可执行文件。

    为了确认可行性,小组进行了该方法的验证。首先,通过Deepin系统自带的修改开机自启动设置的方法,修改开机启动项(图中修改终端的自启动项):

    默认自启动设置方式

    随后检查自启动文件夹~/.config/autostart

    yang@yang-PC:~/.config/autostart$ ls
    deepin-terminal.desktop org.deepin.browser.desktop
    

    发现deepin-terminal.desktop文件被添加入了该自启动文件夹中,并且会在开机时自启动。

    而取消终端的自启动时,~/.config/autostart中已有的deepin-terminal.desktop文件并不会被移除,而是其中的Hidden的字段会被修改为false,表示取消开机自启动设置。

    由此,可以通过在插件中检查所有~/.config/autostart文件夹中.desktop文件的Hidden字段来搜索系统所有的自启动软件;也可以通过添加.desktop文件、修改Hidden字段的方式进行开机自启动设置的修改。

    5 rpc远程通信问题

    1. 远程过程调用: RPC允许一个程序调用另一个程序在远程机器或进程上执行的过程(函数或方法),就像本地调用一样。这样,开发者可以透明地在分布式系统中调用远程的功能,无需关注底层网络细节。
    2. 通信协议: RPC通信需要定义一个协议,该协议规定了消息的格式、编码方式、序列化和反序列化方法等。通过确定的request和response协议格式制定规定了客户端和服务器之间如何进行数据交换。
    3. 序列化和反序列化: 在RPC通信中,数据需要在网络上传输,因此需要将数据进行序列化(编码)以便在网络上传输,然后在接收端进行反序列化(解码)以还原数据。序列化和反序列化过程确保数据的正确传输和解释。
    4. 数据传输: RPC通信依赖底层的网络传输协议,如TCP或HTTP。客户端和服务器通过网络传输消息,以实现远程过程调用。
    5. 错误处理: RPC通信中需要处理各种可能的错误情况,例如网络连接中断、超时、服务不可用等。合理的错误处理可以保证系统的可靠性和鲁棒性。
    6. 服务注册与发现: 在dde-application-manager中以字符串命名注册了远程调用函数的服务,dde-control-center中关于对应用的变更可以通过rpc远程调用实现就和
    7. 安全性: RPC通信涉及跨网络的数据传输,因此安全性是一个重要考虑因素。加密、认证和授权等机制可以确保通信的安全性。

    6 插件描述缺失问题

    添加描述前

    在加载插件后,我们发现插件缺失了描述信息,与其他的插件格式明显由很大的不同,通过Matrix上交流发现,是插件缺少了setDescription的代码,因此,正确加入描述后,我们获得了以下结果。

    添加描述后

    附录C 开发计划

    第一步(4/9~4/18)

    • 调研Deepindde-dockQT框架等相关内容
    • 设计项目方案
    • 分工

    第二步(4/19~5/2)

    • 搭建主体插件类的框架
    • 设计启动项管理窗口的前端展示页面

    第三步(5/3~5/13)

    • 开发部件类接口
    • 完善插件类功能

    第四步(5/14~5/21)

    • 插件类右键功能开发
    • 完成配置文件

    第五步(5/22~5/31)

    • Debug
    • 撰写文档

    (以上DDE-Dock自启动插件开发均在初赛完成,项目地址见末尾)

    第六步(6/26~7/8)

    • 调研DDE Control Center框架等相关内容
    • 设计前端界面
    • 分工

    第七步(7/9~7/15)

    • 编译教程中的Hello World控制中心插件
    • 设计插件架构

    第八步(7/16~7/22)

    • 配置环境,编译V20示例插件
    • 设计后端接口
    • 修改windowoperationcategory下的文件

    第九步(7/23~7/29)

    • 配置环境,编译Default-AppSelf Start-up插件
    • Debug

    第十步(7/30~8/10)

    • 修改翻译、文字颜色问题
    • 撰写文档

    附录D 参考资料

    文档

    Qt 插件标准:https://wiki.qt.io/Plugins

    deepin V23 dde-control-center文档:dde-control-center: dde-control-center (linuxdeepin.github.io)

    dde-control-center控制中心插件开发示例:控制中心插件 - deepin开发者平台

    qt-5手册:https://doc.qt.io/qt-5/

    deepin 应用自启动说明:https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html

    博客

    关于deepin开机自启动项的讨论:https://bbs.deepin.org/zh/post/169824、https://blog.csdn.net/qq_21137441/article/details/124825726

    仓库

    dde-control-center仓库:https://github.com/linuxdeepin/dde-control-center.git

    其他开发者的插件项目:https://github.com/linuxdeepin/dde-network-core/tree/master

    说明:由于控制中心的插件对于外观的统一性具有较高的要求,因此,我们仓库中的include/interfaceinclude/widgetssrc/interfacesrc/widgetssrc/frame下的文件均来自dde-control-center源代码仓库,以保证插件接口的一致性和外观的统一性。

    编辑的话请把自己的名字加到作者名单里

    DDE v23 首个正式版即将随 deepin v23 发布(你阅读到这个文章的时候可能已经发布了)。为了方便各个其它发行版的包维护者可以更方便的移植 DDE 到对应的发行版,这里提供一篇简要的移植指南,用以描述常见的移植问题和解决方案。

    下面对项目名称的称呼均以 GitHub 对应的原始仓库名为准。 {.note}

    概览

    即便本次从版本号字面来看可能并没有较大变动,但事实上,本次相比 beta3 -> rc 而言仍然是存在比较大的变化的。本次中,dde-shell 加载托盘插件的策略做了大幅调整,转变为通过 dde-tray-loader 加载插件,托盘区域的插件也放弃了原有的插件,转而移植并使用了来自原 UOS 20 专业版的托盘插件。此外,为了为后续的应用权限管控做准备,本次也对包括 dde-launchpad、dde-shell 等在内的项目调整了其 DSG 配置文件 所使用的应用 ID(DSG_APP_ID),故对于移植到其它发行版的情况,若存在相应的 DConfig OEM 配置则也需进行调整。另外,为了解决一些已知的开源合规问题,我们也将原本位于 dtkcore 中的日志部分分离为了一个单独的组件,名为 dtklog。

    由于这些项目的版本间互相影响,我们强烈建议移植人员参照 deepin v23 正式版所使用的包版本进行打包(也务必遵循依赖顺序打包)。下面会对主要的部分进行详细说明。

    需要注意的是,由于此文章编写时间早于版本发布时间,故最终版本镜像中使用的版本可能高于下面列出的版本。我们尽可能确保此文章的准确性,但若您需要获取 ISO 镜像中使用的确切软件版本列表,请挂载 ISO 后参阅 LIVE/FILESYSTEM.MANIFEST (也可能是 LIVE/FILESYS0.MAN)路径对应的文件的内容。

    主要组件

    DTK 与 DTK6

    DTK 是 DDE 组件与应用的基础依赖,适用于 RC 的版本参照如下:

    packageversion
    dtkcommon5.6.32
    dtklog0.0.1
    dtkcore5.6.32
    dtkgui5.6.32
    dtkwidget5.6.32
    dtkdeclarative5.6.32
    qt5integration5.6.32
    qt5platform-plugins5.6.32
    dtk6log0.0.1
    dtk6core6.0.18
    dtk6gui6.0.18
    dtk6widget6.0.18
    dtk6declarative6.0.18
    qt6integration6.0.18
    qt6platform-plugins6.0.18

    除新增的 dtklog 外,本次 DTK 版本号以及相对应的平台插件等版本号均已对齐,可直接参照打包。

    deepin-kwin wayland 功能已经废弃,未来将由 treeland 替代。目前 dwayland 包已经不再使用,依赖此包的应用比如 qt5platform-plugins, 不应该继续编译依赖 dwayland 的功能,可参照 linuxdeepin/developer-center#7217 打对应的 patch 规避。

    dtk6 曾对 Qt 6.2, Qt 6.4 和 Qt 6.6 均进行过适配,我们目前的研发与测试主要使用 Qt 6.6 版本,但当前主干也包含了对 Qt 6.7 的支持。如仍发现有 Qt 版本支持问题可在 DDE 移植群(地址见文末)反馈。

    目前,使用 dtk6 的正式组件有 dde-application-manager,dde-launchpad 与 dde-shell, 需要注意 dde-shell 的托盘组件 dde-tray-loader 仍然需要使用 qt5。

    DDE 主要组件

    下面仅涉及变化较大或影响较广的组件。其余未涉及的组件可正常参照最新 tag 进行打包与移植。

    下面涉及到的组件的版本参照如下:

    packageversion
    deepin-osconfig2024.08.06
    dde-app-services1.0.25
    dde-session1.2.13
    dde-application-manager1.2.15
    dde-tray-loader0.0.8
    dde-shell0.0.40
    dde-launchpad0.8.4
    dde-application-wizard0.1.10
    deepin-wayland-protocols1.10.0.28
    deepin-kwin5.27.2.206
    dde-launcher被 dde-launchpad 取代,不再使用
    dde-dock被 dde-shell 取代,不再使用

    deepin-osconfig

    此仓库存放了适用于 deepin 发行版的“OEM”配置。此配置一般 不需要被其它发行版原样移植,但考虑到本次存在针对 DSG 应用 ID 的变化调整,故将其列在此处,仅供移植人员参考对应的修改内容,以便当自己所移植到的目标发行版存在 OEM 配置时做出相应的调整。

    dde-application-manager

    负责启动和管理 DDE 桌面环境存在的应用程序列表的组件。此组件在 rc 阶段无太大变化(但仍总是建议升级)。

    dde-tray-loader

    这是一个新增的组件,旨在提供 DDE 的任务栏托盘部分的各个托盘插件。此项目提供了 xembed、SNI 托盘的加载支持,以及旧式 dde-dock 托盘插件的加载支持(因为并非直接兼容的旧式插件,故此项目同时也提供了这些插件)。

    此项目需要配合新的 dde-shell 一同使用。

    dde-shell

    dde-shell 旨在将 DDE 桌面环境插件化与模块化,降低开发难度,使各个组件的替换变得更加容易,并且提供更好的桌面环境集成支持。

    相较于 RC 阶段,此组件的变化主要在于任务栏托盘区域的加载逻辑的大幅调整与相关动画的调整,以及 shell 本体功能上的支撑调优。对于托盘区域,请参见新增项目 dde-tray-loader 的描述。另外,RC 阶段内附的一些第三方库(例如 networkmanager-qt)已被移除,故一般无需刻意留意是否存在 vendor libs 的问题。

    dde-launchpad

    dde-launchpad 相较于 RC 阶段变化并不大,调整大多与缺陷修复以及动画调整有关。需要注意的是,此应用的 DConfig DSG 应用 ID 有变化,故对于移植人员,若原有主动提供针对启动器的 OEM 配置,则需注意修改配置文件放置的对应路径的变化(对应文档也已更新)。

    deepin-kwin

    启动器与 shell 均依赖窗管提供一些支持(例如窗口组件的状态、位于的工作区以及层级关系的控制等),故请同时确保 deepin-kwin 使用的版本。

    需要注意的是,截至编写此文档时,GitHub 中 deepin-kwin 仓库的最新 tag 落后于上述所列的 deepin-kwin 版本,我们正在与相关项目组协商,若您阅读至此时仍无法在 GitHub 获取到对应的版本且 GitHub 所获取的最新版本存在较大问题,则请暂时先从 deepin v23 (beige) 的软件仓库获取对应源码。

    dde-session

    此项目在 RC 阶段无缺陷修复之外的较大变动。

    仍需重申,我们已在 beta3 阶段放弃了对 deepin-kwin wayland 的支持,DDE 后续所有 wayland 相关的支持均由 treeland 提供。

    技术预览组件

    为全力确保正式版的版本发布,原本涉及的技术预览组件在 RC 至 release 的这个阶段均无较大进展,故不再于此罗列。

    获取移植帮助

    如果您希望得到移植相关的帮助,请考虑加入我们 DDE 移植小组的在线交流群(下列房间有桥接,任选其一即可),一起展开相关的交流:

    Sunday, June 25, 2023

    KWin 是 KDE 开发的窗口管理器,提供了非常丰富的插件,可以对功能进行大量的定制。

    本篇文章是对窗口特效插件的开发介绍。

    插件开发

    插件定义

    KWin 的插件通常可以使用一些宏辅助生成代码,例如使用 KPluginFactory 进行插件的定义,内容是用来生成插件的入口类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #define EffectPluginFactory_iid "org.kde.kwin.EffectPluginFactory" KWIN_PLUGIN_VERSION_STRING
    #define KWIN_PLUGIN_FACTORY_NAME KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME
    #define KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, enabled ) \
    class KWIN_PLUGIN_FACTORY_NAME : public KWin::EffectPluginFactory \
    { \
    Q_OBJECT \
    Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE jsonFile) \
    Q_INTERFACES(KPluginFactory) \
    public: \
    explicit KWIN_PLUGIN_FACTORY_NAME() {} \
    ~KWIN_PLUGIN_FACTORY_NAME() {} \
    bool isSupported() const override { \
    supported \
    } \
    bool enabledByDefault() const override { \
    enabled \
    } \
    KWin::Effect *createEffect() const override { \
    return new className(); \
    } \
    };

    #define KWIN_EFFECT_FACTORY_ENABLED(className, jsonFile, enabled ) \
    KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, enabled )

    #define KWIN_EFFECT_FACTORY_SUPPORTED(className, jsonFile, supported ) \
    KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, return true; )

    #define KWIN_EFFECT_FACTORY(className, jsonFile ) \
    KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, return true; )

    大部分宏只是为了方便结构修改,我们只需要使用 K_PLUGIN_FACTORY 进行插件定义即可。

    假设我们开发了一个插件,名字叫 demo,我们只需要在 main.cpp 中使用 KWIN_EFFECT_FACTORY_SUPPORTED 定义

    1
    2
    3
    4
    5
    KWIN_EFFECT_FACTORY_SUPPORTED(
    Demo,
    "metadata.json",
    return true;
    )

    代码展开后是这样的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME : public KWin::EffectPluginFactory \
    {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE "metadata.json")
    Q_INTERFACES(KPluginFactory)
    public: \
    explicit KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME() {}
    ~KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME() {}
    bool isSupported() const override {
    return true;
    }
    bool enabledByDefault() const override {
    return true;
    }
    KWin::Effect *createEffect() const override {
    return new Demo();
    }
    };

    可以看到,其实 KWIN_EFFECT_FACTORY_SUPPORTED 只是为我们生成了工厂函数,辅助生成了一些必要的重载。

    metadata.json 文件是用来作为插件的描述信息使用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "KPlugin": {
    "Category": "Accessibility",
    "Description": "Allow clip of window content",
    "EnabledByDefault": true,
    "Id": "scissor",
    "License": "GPL",
    "Name": "ScissorWindow",
    "Name[zh_CN]": "窗口圆角"
    },
    "org.kde.kwin.effect": {
    "enabledByDefaultMethod": true
    }
    }

    特效插件

    特效插件是一类可以改变窗口画面的插件,例如我们可以在插件里对窗口进行贴图、变形和裁切,在 DDE 中,就使用特效插件完成了圆角裁切和窗口模糊。

    这里使用圆角裁切插件作为例子,首先使用 KWIN_EFFECT_FACTORY_SUPPORTED 宏对插件进行定义, KWIN_EFFECT_FACTORY_SUPPORTED 接受一个 class 作为返回的接口类,它需要继承自 Effect,第二个参数是元信息的 json 文件,第三个参数是返回是否支持,在启用插件时可对当前环境进行判断,例如插件需要使用 OpenGL 对图形进行一些操作,但是当前环境不支持 OpenGL,那么插件就不会启用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include "scissorwindow.h"

    namespace KWin
    {

    KWIN_EFFECT_FACTORY_SUPPORTED(ScissorWindow,
    "metadata.json.stripped",
    return ScissorWindow::supported();)

    } // namespace KWin

    #include "main.moc"

    在 Effect 类中有几个不同阶段的方法可以重载。

    • prePaintScreen
      • 设置是否变换窗口或整个屏幕
      • 更改将要绘制的屏幕区域
      • 做各种内务处理任务,比如初始化你的效果变量
        用于即将到来的绘画过程或更新动画的进度
    • paintScreen
      • 在窗口上画东西(调用后画 effect->paintScreen())
      • 绘制多个桌面和/或同一桌面的多个副本
    • postPaintScreen
      • 在动画的情况下安排下一次重绘,不应该在这里画任何东西。
    • prePaintWindow
      • 启用或禁用窗口的绘制(例如启用最小化窗口的绘制)
      • 将窗口设置为半透明
      • 设置要转换的窗口
      • 请求将窗口分成多个部分
    • paintWindow
      • 做各种转换
      • 改变窗口的不透明度
      • 改变亮度和/或饱和度,如果支持的话
    • postPaintWindow
      • 在动画的情况下为单个窗口安排下一次重绘
        不应该在这里画任何东西。
    • paintEffectFrame
      • 在绘制 EffectFrame 之前直接调用此方法。
      • 如果需要绑定shader或者执行,可以实现这个方法帧渲染前的其他操作。
    • drawWindow
      • 可以调用以绘制一个窗口的多个副本(例如缩略图)。
      • 可以在这里改变窗口的不透明度/亮度/等,但不能做任何转换。
      • 在基于 OpenGL 的合成中,框架确保上下文是最新的

    在方法名称中可以看出,在场景及窗口绘制的过程中,分别可以在实际绘制的前后分别执行一些动作,圆角插件就是在 drawWindow 函数中,使用 OpenGL 对窗口使用着色器进行窗口裁切,并绘制到屏幕上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    void ScissorWindow::drawWindow(EffectWindow *w, int mask, const QRegion& region, WindowPaintData &data) {
    if (w->isDesktop() || isMaximized(w)) {
    return effects->drawWindow(w, mask, region, data);
    }

    QPointF cornerRadius;
    const QVariant valueRadius = w->data(WindowRadiusRole);
    if (valueRadius.isValid()) {
    cornerRadius = w->data(WindowRadiusRole).toPointF();
    const qreal xMin{ std::min(cornerRadius.x(), w->width() / 2.0) };
    const qreal yMin{ std::min(cornerRadius.y(), w->height() / 2.0) };
    const qreal minRadius{ std::min(xMin, yMin) };
    cornerRadius = QPointF(minRadius, minRadius);
    }

    if (cornerRadius.x() < 2 && cornerRadius.y() < 2) {
    return effects->drawWindow(w, mask, region, data);
    }

    const QString& key = QString("%1+%2").arg(cornerRadius.toPoint().x()).arg(cornerRadius.toPoint().y()
    );
    if (!m_texMaskMap.count(key)) {
    QImage img(QSize(radius.x() * 2, radius.y() * 2), QImage::Format_RGBA8888);
    img.fill(QColor(0, 0, 0, 0));
    QPainter painter(&img);
    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(255, 255, 255, 255));
    painter.setRenderHint(QPainter::Antialiasing);
    painter.drawEllipse(0, 0, radius.x() * 2, radius.y() * 2);
    painter.end();

    m_texMaskMap[key] = new GLTexture(img.copy(0, 0, radius.x(), radius.y()));
    m_texMaskMap[key]->setFilter(GL_LINEAR);
    m_texMaskMap[key]->setWrapMode(GL_CLAMP_TO_EDGE);
    }

    ShaderManager::instance()->pushShader(m_filletOptimizeShader);
    m_filletOptimizeShader->setUniform("typ1", 1);
    m_filletOptimizeShader->setUniform("sampler", 0);
    m_filletOptimizeShader->setUniform("msk1", 1);
    m_filletOptimizeShader->setUniform("k", QVector2D(w->width() / cornerRadius.x(), w->height() / cornerRadius.y()));
    if (w->hasDecoration()) {
    m_filletOptimizeShader->setUniform("typ2", 0);
    } else {
    m_filletOptimizeShader->setUniform("typ2", 1);
    }
    auto old_shader = data.shader;
    data.shader = m_filletOptimizeShader;

    glActiveTexture(GL_TEXTURE1);
    m_texMaskMap[key]->bind();
    glActiveTexture(GL_TEXTURE0);
    effects->drawWindow(w, mask, region, data);
    ShaderManager::instance()->popShader();
    data.shader = old_shader;
    glActiveTexture(GL_TEXTURE1);
    m_texMaskMap[key]->unbind();
    glActiveTexture(GL_TEXTURE0);
    return;
    }

    如果窗口是桌面类型,或者已经最大化了,则无需处理,直接返回 Effect 原本的处理函数。

    之后尝试从窗口属性中取出圆角大小的值,如果没有设置圆角大小,或者值小于2,则无需处理。

    尝试查询缓存,在这里为窗口的四个角构建一份遮罩对象并缓存,使用 OpenGL 将遮罩和着色器进行关联,激活两个材质分别绘制窗口内容和四个角的遮罩,在着色器中完成窗口圆角的半透明效果。

    限于篇幅,本文不展开介绍如何实现圆角插件的全部实现过程,仅挑选关键步骤。

    安装

    将动态库复制到 /usr/share/kwin/effects/plugins ,并使用 DBus 激活插件。

    1
    qdbus --literal org.kde.KWin /Effects org.kde.kwin.Effects.loadEffect scissor

    Thursday, June 1, 2023

    在 V23 beta 版本中,DDE 试验性的开启了 Wayland 的支持,允许用户在 Wayland 协议下的桌面工作环境启动 。本篇文章会向大家介绍一下 Wayland 是什么,我们尝试做了什么改变,以及 DDE Wayland 未来会支持哪些新特性。(注:单独提出 Wayland, 通常和 Wayland 合成器、Wayland 服务器、显示服务器被视为同一个内容;X Window System 和 X11 也被视为同一个内容。)

    什么是 Wayland?

    Wayland 是一个通信协议,规定了显示服务器与客户端之间的通信方式,而使用这个协议的显示服务器称为 Wayland Compositor。Wayland 只专注于图形,并希望使用其他库与输入硬件进行通信,以降低自身的复杂度。Wayland 最大的好处也是大家都推崇的原因,那就是 Wayland 在设计上会考虑安全,例如默认不允许窗口获取其他窗口的数据,合成器和窗口管理器的合并也降低了对系统资源的消耗。

    Wayland 与 X Window System 有什么不同?

    Wayland 与 X Window System 的最大不同在于,Wayland 与 X Window System 的最大不同在于,它的窗口管理器和 Wayland Server 在同一个进程,并且客户端能够通过 EGL 以及一些 Wayland 特定的 EGL 扩充组件直接在显示内存中绘制自己的缓冲区。 窗口管理器简化成显示管理服务,专门负责绘制那些屏幕上的程序。 这比 X Window System 中的窗口管理器要更简单、高效。

    Wayland 协议有哪些组成?

    1.协议概述

    Wayland 协议被描述为异步面向对象协议。协议是异步的,这意味着不必等待回复或者响应 ACK,避免了往返时间并提高性能。协议封装为面向对象的设计,则是面向对象的设计方式,能很好的对服务器上不同窗口数据及接口进行封装。

    Wayland 合成器可以定义和公开自己的附加接口,被称为扩展协议,不同的 Wayland 会提供功能完全不同,甚至功能相反的协议,这带来了很大灵活性,但使用客户端时需要自行判断。

    2.协议架构

    Wayland 协议是一种“客户端 —— 服务器”模型,客户端是请求在屏幕上显示画面的图形应用程序,服务器是控制应用程序显示在屏幕上的管理程序。Wayland 参考实现被设计成两层协议,既:

    • 下层协议:处理客户端和服务器之间的进程间通信,以及在内部的数据封装处理。
    • 上层协议:处理客户端和服务器交换的数据,以实现窗口系统的基本功能,这一层被实现为异步面向对象协议。 下层协议是使用 C 语言开发的,而上层协议是根据 XML 格式的协议描述文件自动生成,每当 XML 协议的描述发生变化时,就可以重新生成该协议的源代码,这使得协议非常灵活、可扩展性好且防止出错。

    如下 Wayland 工作原理图:

    wayland.png

    以下对应图中所标编号作说明:

    Linux 内核中的 evdev 模块接收事件并将它们发送到 Wayland 合成器。

    Wayland 合成器查看场景图并确定哪个窗口应接收事件。场景图对应于屏幕上显示的内容,Wayland 合成器显示对应用事件的场景图中元素的转换。因此,Wayland 合成器可以反向变换以找到正确的窗口,并将屏幕上的坐标转换为窗口中的坐标。

    当客户端收到事件时,Wayland 的客户端只需通过 EGL 渲染并向合成器发送请求以通知更新的范围即可。

    Wayland 合成器 从客户端收集更改请求并重新配置屏幕。 然后,合成器直接发出 ioctl 让 KMS 重绘屏幕。

    当了解了 Wayland 相关基本介绍之后,基于它我们在 DDE 上将会作哪些适配功过呢?将从以下几个方面说一下我们在 Wayland 技术预览版里面所做的适配工作。

    DDE 适配 Wayland 都做了哪些工作

    DDE 原本设计为在 X11 协议下工作,很多组件直接或间接依赖 X11 的接口,多数组件依赖的功能并没有在 XWayland 中提供,所以就需要进行一些修改。

    首先在 Qt 插件中实现和 Wayland 特定性相关的功能,Qt 提供了一个 Wayland Shell Integration 的插件,允许我们在这里调用 DDE Wayland 合成器提供的扩展协议。

    Qt 已提供核心协议的适配,所以 DDE 只需要在现有框架下实现扩展协议即可。

    目前 DDE 提供的扩展协议有以下几个方面:

    • 设置圆角窗口
    • 请求获取窗口数据(截图权限)
    • 划分工作区可用区域 DTK 程序可以通过设置窗口的属性,或者使用 DTK 提供的平台接口,即可调用扩展的 Wayland 协议,非 DTK 程序则需要手动使用扩展协议的 XML 文件进行代码生成调用。

    DDE Wayland 未来会支持什么特性?

    1.HDR 支持

    高动态范围(HDR)是比平时更高的动态范围。HDR 的内容过于深入,在这里只简单的进行说明。

    HDR 可以保存更多的内容信息,在支持 HDR 的屏幕上观看 HDR 的内容,可以获得更好的体验,DDE 目前正在准备支持 HDR 内容的输出,这会让 DDE 拥有更好的显示效果。

    2.成体系的窗口动画

    在 X11 下,由于窗口管理和画面合成管理是两个进程,并且在启动速度上存在差异,所以只能采用一些“巧妙”的设计来规避视觉错误。

    但是在 Wayland 下,窗口管理器和窗口合成器被合并成一个进程,那么启动后就可以立即使用动画效果,例如可以设计视觉效果更好的登录动画。

    目前 DDE 的窗口动画支持并不多,且大部分是单调的线性动画,有些情况还需要客户端自己实现虚假动画,例如任务栏发生位置变化时,桌面的图标会进行计算,并自行改变大小,这引入了非必要的依赖。如果使用窗口动画,桌面并不需要关心任何外部因素,只需要设置自己在可用工作区域最大化,当任务栏发生位置改变时,合成器会自动调整桌面的大小,并产生相应的窗口动画。

    未来 DDE 会使用更多窗口动画来减少组件之间的依赖,以及实现更多更好的视觉效果。

    最后总结

    阅读至此,是不是对 Wayland 的概念及工作原理更加清晰啦?如果您有什么疑问也欢迎与我们互动探讨 Wayland。

    坦白说,DDE 的 Wayland 支持还处于初步阶段(技术预览版,请谨慎使用),未来我们会使用更多合成器提供的功能,来为桌面环境降低开发难度,提升性能。提供更好的体验。