• 首页
  • 加入
  • RSS
  • Wednesday, November 8, 2023

    本文简述了deepin v23系统使用5G WWAN网卡连接互联网的方式,仅仅作为一个临时解决方案和技术验证使用,后期会在系统中内置此功能。

    第一步:安装modemmanager

    modemmanager是一个由freedesktop托管的项目,旨在在linux设备上运行调制解调器以让linux设备获得蜂窝无线网络连接的能力,所以我们第一步骤就是安装此软件。目前deepin已经支持此软件最新版本:

    sudo apt install modemmanager
    

    同时,你需要确保你的内核模块已经正确加载了你的WWAN驱动,你可以通过使用lspci -vvv查看详细信息

    在安装了modemmanager后,你可以通过mmcli 命令使用命令行与其交互,使用mmcli —help-all来获取全部的帮助选项。

    第二步:使用mmcli连接5G网络

    使用mmcli -L 获取你的wwan卡信息,返回的信息有三个内容,分别为:DBus地址、设备类型 、设备ID,其中DBus地址的最后一位为设备编号,你可以使用mmcli --modem=<设备编号>的方式查看设备详细支持信息。

    如果你的SIM卡设置了PIN锁,则需要在连接网络之前使用mmcli --modem=0 --sim=0 --pin=**** 的方式连接,而后我们就可以启动相关设备了:

    mmcli --modem=<设备编号> --enable
    

    然后你需要使用simple connect连接网络

    mmcli -m <设备编号> --simple-connect='apn=<apn名>,ip-type=ipv4v6'
    

    比如我的连接方式为

    mmcli -m 4 --simple-connect='apn=ctnet,ip-type=ipv4v6'
    

    此时你再使用mmcli -m <设备编号> 查看信息的时候 可以查看Bearer的信息,Bearer的DBus的最后一位为其编号。使用命令查看Bearer的信息:

    mmcli -m <设备编号> -b <Bearer编号>
    

    如果你看到Bearer相关信息,就几乎接近成功了:

      ------------------------------------
      General            |           path: /org/freedesktop/ModemManager1/Bearer/0
                         |           type: default
      ------------------------------------
      Status             |      connected: yes
                         |      suspended: no
                         |    multiplexed: no
                         |      interface: wwan0
                         |     ip timeout: 20
      ------------------------------------
      Properties         |            apn: ctnet
                         |        roaming: allowed
                         |        ip type: ipv4
                         |   allowed-auth: none, pap, chap, mschap, mschapv2, eap
                         |           user: [email protected]
                         |       password: vnet.mobi
      ------------------------------------
      IPv4 configuration |         method: static
                         |        address: 10.122.58.19
                         |         prefix: 8
                         |        gateway: 10.122.58.17
                         |            dns: 202.103.24.68, 202.103.44.150
                         |            mtu: 1420
      ------------------------------------
      Statistics         |     start date: 2023-11-07T05:40:08Z
                         |       duration: 1260
                         |   uplink-speed: 1250000000
                         | downlink-speed: 4670000000
                         |       attempts: 1
                         | total-duration: 1260
    
    

    第三步:使用nmcli打开连接

    networkmanager对mm是有做支持,在你完成上述步骤之后,可以通过ip a命令查看,可以看到一个被down掉的接口,我们使用nmcli查看其详细信息:

    nmcli device show
    

    你就可以 看到一个以wwan开头的设备:

    GENERAL.DEVICE:                         wwan0mbim0
    GENERAL.TYPE:                           gsm
    GENERAL.HWADDR:                         (未知)
    GENERAL.MTU:                            1420
    GENERAL.STATE:                          100(已连接)
    GENERAL.CONNECTION:                     wwan0mbim0
    GENERAL.CON-PATH:                       /org/freedesktop/NetworkManager/ActiveConnection/3
    IP4.ADDRESS[1]:                         10.122.58.19/8
    IP4.GATEWAY:                            10.122.58.17
    IP4.ROUTE[1]:                           dst = 10.0.0.0/8, nh = 0.0.0.0, mt = 700
    IP4.ROUTE[2]:                           dst = 0.0.0.0/0, nh = 10.122.58.17, mt = 700
    IP4.DNS[1]:                             202.103.24.68
    IP4.DNS[2]:                             202.103.44.150
    IP6.GATEWAY:                            --
    
    

    使用下述命令打开此设备

    nmcli d connect <设备名>
    

    上述设备名就是以wwan开头的设备

    然后你就可以使用5G网络连接了

    Saturday, November 4, 2023

    介绍

    xtrace是一个用于跟踪分析X11图形协议通信的工具,它可以监控和记录X11服务器上的各种场景,以帮助开发人员诊断和调试与图形界面相关的问题。作为一款强大的工具,xtrace可用于逆向工程、调试分析、性能分析等领域。在Linux X11系统中,xtrace能够记录一个程序在运行时所发起的X11协议请求和XServer发送给程序的事件,以及这些调用的参数。这对于在不阅读源码情况排查程序中的问题、理解程序行为、分析性能瓶颈以及进行协议审计都非常有用。笔者在工作过程中使用该工具深度剖析过腾讯会议、simplescreenrecorder等应用程序的实现,在没有阅读代码的前提下可以获得软件录屏的工作流程,配合阅读常规的X11录屏代码,可分析出其部分工功能异常原因,这些经验在xwayland适配X11应用程序截图录屏项目中通过实战解决了一系列问题。

    本文主要介绍的是X11协议的监视工具,希望您在读完本篇文章后可以对如何监控X11协议有比较深刻的认知,在工作中经常会碰到一些X11应用程序运行时功能异常,在没有应用程序源码的情况下通过xtrace进行调试是一个不错的选择,希望在阅读完这篇文章后,能丰富您的调试技巧。祝您阅读愉快!

    xtrace的安装和使用非常简单,打开终端像如下输入命令即可启动工具:

    // UOS上安装xtrace,deepin上没有可以在http://snapshot.debian.org/上下载
    sudo apt install xtrace
    
    // 运行之后协议电报内容会存储在/tmp/dde-calendar.xtrace中
    xtrace -n -o /tmp/dde-calendar.xtrace dde-calendar	
    
    命令行选项含义或用途
    –display, -d用于ssh远程调试,例如:xtrace -d :0 dde-calendar
    –outfile, -o用于将通信内容转存到磁盘,例如:xtrace -o /tmp/x.log dde-calendar
    –stopwhendone, -s进程退出后停止xtrace,例如:xtrace -s dde-calendar

    工作原理

    如图1所示,X11其主要有两种通信方式,在同一个计算机内主要使用unix domain socket进行通信;在不同计算机之间使用TCP/IP通信。只需要“截获”通信消息,将其转为易于读取的格式输出即可达到监控目的。这是xtrace工作的基本原理。

    图1. 不同客户端使用同一个XServer显示器框架图

    如下图2是X11窗口创建的基本流程,因为篇幅限制,图中只绘制了基础的请求和事件,但X11协议远比图中绘制的复杂,笔者在此只介绍一下基础的协议交互模型,方便在读者在查看xtrace追踪日志时有基础的认知。

    图2. X11协议交互流程

    总之xtrace是Xorg X Server自带的一个工具,通过分析xtrace的输出,你可以了解X Server是如何处理客户端应用程序的请求,以及可能的问题所在。xtrace 工具的作用:

    • 调试X问题:如果你遇到了与图形界面相关的问题,例如窗口无法正常显示、图形卡驱动问题等,你可以使用xtrace来捕获X Server的活动,帮助你找出问题所在。
    • 性能分析:xtrace可以记录X Server内部的操作,帮助你分析系统的图形性能,找出潜在的瓶颈。
    • 理解X协议交互:X Server与客户端应用程序之间的通信是通过X协议进行的。xtrace可以捕获这些通信,帮助你理解应用程序与X Server之间的交互方式。
    • 功能逆向:对于依赖X11协议的软件无法查看源码,可以通过观察通信协议,逆向其部分核心功能的实现 请注意,xtrace的输出可能会非常详细,因此在使用时需要注意过滤和分析输出,以便关注于你感兴趣的信息。

    X11在文件系统中暴露了通信用的socket套接字文件,这使得第三方应用程序可以监控套接字文件从而“窥视"X11客户端和服务端之间的通信,这一技术点是xtrace工作的主要基础。

    // X11 socket套接字文件在文件系统中的位置
    ls /tmp/.X11-unix
    X0  X9
    

    如下日志所示xtrace工作的本质就是不断地获取wrote和received的数据然后将其解析为易于阅读的描述语言打印出来。

    // 全量日志
    001:>:received 32 bytes
    001:>:09dc:32: Reply to InternAtom: atom=0x250("_NET_KDE_COMPOSITE_TOGGLING")
    001:>:wrote 32 bytes
    001:>:received 32 bytes
    001:>:09dc: Event XKEYBOARD-XkbEvent(85) type=2 time=0x018d8071 device=0x03 not-yet-supported=0x10,0x00,0x00,0x10,0x01,0x00,0x00,0x00,0x00,0x01,0x90,0x10,0x10,0x10,0x90,0x00,0x01,0x90,0x11,0x00,0x00,0x87,0x05;
    001:>:wrote 32 bytes
    001:>:received 32 bytes
    001:>:09dc: Event XKEYBOARD-XkbEvent(85) type=2 time=0x018d8072 device=0x03 not-yet-supported=0x10,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x00,0x01,0x90,0x11,0x00,0x00,0x87,0x05;
    001:>:wrote 32 bytes
    001:>:received 32 bytes
    001:>:09dc: Event XKEYBOARD-XkbEvent(85) type=2 time=0x018d8073 device=0x03 not-yet-supported=0x10,0x00,0x00,0x10,0x01,0x00,0x00,0x00,0x00,0x01,0x90,0x10,0x10,0x10,0x90,0x00,0x01,0x90,0x11,0x00,0x00,0x87,0x05;
    

    其工作流程大致如下:

    • 通过套接字地址族AF_INET判断是tcp通信还是本地domain socket通信,然后通过generateSocketName或者calculateTCPport拿到addr相关的信息
    • 进入mainqueue死循环读取socket通信数据
    • 通过parse_server(c)翻译Event事件通信,然后打印输出
    • parse_client(c)翻译Requst请求通信,然后打印输出

    相关的代码调用堆栈如下:

    Breakpoint 1, startline (c=0x4f8270, d=TO_SERVER, format=0x417a71 "%04x:%3u: Request(%hhu): %s ") at parse.c:52
    52              if( (print_timestamps || print_reltimestamps)
    (gdb) bt
    #0  startline (c=0x4f8270, d=TO_SERVER, format=0x417a71 "%04x:%3u: Request(%hhu): %s ") at parse.c:52
    #1  0x000000000040b5f5 in print_client_request (c=0x4f8270, bigrequest=false) at parse.c:1692
    #2  0x000000000040cc2d in parse_client (c=0x4f8270) at parse.c:1996
    #3  0x0000000000403b4a in mainqueue (listener=4) at main.c:406
    #4  0x00000000004045d4 in main (argc=4, argv=0x7fffffffdef8) at main.c:706
    (gdb) c
    Continuing.
    
    Breakpoint 1, startline (c=0x4f8270, d=TO_CLIENT, format=0x417b32 "%04x:%u: Reply to %s: ") at parse.c:52
    52              if( (print_timestamps || print_reltimestamps)
    (gdb) bt
    #0  startline (c=0x4f8270, d=TO_CLIENT, format=0x417b32 "%04x:%u: Reply to %s: ") at parse.c:52
    #1  0x000000000040c1f3 in print_server_reply (c=0x4f8270) at parse.c:1859
    #2  0x000000000040d0e5 in parse_server (c=0x4f8270) at parse.c:2064
    #3  0x00000000004035ed in mainqueue (listener=4) at main.c:336
    #4  0x00000000004045d4 in main (argc=4, argv=0x7fffffffdef8) at main.c:706
    
    // connect socket
    Breakpoint 2, generateSocketName (addr=0x7fffffffdd30, display=9) at x11common.c:114
    114             snprintf(addr->sun_path,sizeof(addr->sun_path),"/tmp/.X11-unix/X%d",display);
    (gdb) p display 
    $5 = 9
    (gdb) c
    Continuing.
    [Detaching after fork from child process 7682]
    Got connection from unknown(local)
    
    Breakpoint 2, generateSocketName (addr=0x7fffffffd9d0, display=0) at x11common.c:114
    114             snprintf(addr->sun_path,sizeof(addr->sun_path),"/tmp/.X11-unix/X%d",display);
    (gdb) p display 
    $6 = 0
    (gdb) bt
    #0  generateSocketName (addr=0x7fffffffd9d0, display=0) at x11common.c:114
    #1  0x0000000000404c92 in connectToServer (displayname=0x7fffffffe363 ":0", family=1, hostname=0x0, display=0) at x11client.c:77
    #2  0x0000000000402766 in acceptConnection (listener=3) at main.c:95
    #3  0x0000000000403edd in mainqueue (listener=3) at main.c:452
    #4  0x00000000004045d4 in main (argc=2, argv=0x7fffffffdf18) at main.c:706
    (gdb) 
    

    协议分析

    xtrace通过拦截X11协议通信来进行分析。它捕获传输到X服务器的请求以及服务器对这些请求的响应,将他们解析化以日志输出的形式打印出来。所以本质来说协议分析指的是X11协议交互分析,需要对 X11相关的协议做到非常了解,即每个协议有什么功能,在xcb中是如何处理,在xserver中又是如何处理。受限于篇幅,笔者在此不会阐述所有的协议分析,而是拿我们平时常用的一些软件做一些分析和介绍。

    日志流关键字含义或用途
    Present-Request(148,1)客户端用于GLX等送显
    Request(1): CreateWindow请求创建X窗口,shm、glx都有
    GLX-Request(152,3): glXCreateContext请求创建GLX上下文,可以用来判断客户端是否GLX应用
    MIT-SHM-Request(130,3): PutImageX11 shm客户端请求更新图像
    Event XKEYBOARD-XkbEvent(85)xserver键盘事件传递给X客户端
    Event Generic(35) XInputExtension鼠标事件,后面带着ButtonPress、ButtonRelease、Motion等
    Request(36): GrabServergrab请求
    Request(37): UngrabServer解除grab请求
    DeleteProperty请求删除X11窗口的一些属性
    ChangeProperty改变X11窗口的一些属性
    PropertyNotify窗口属性改变发送事件通知客户端
    MIT-SHM-Request(130,4): GetImageshm方式获取屏幕图像
    Request(62): CopyArea复制屏幕一部分区域图像(离屏)
    Request(53): CreatePixmap创建图像(离屏)

    上述表格中只是介绍了常见部分的协议,X协议非常的丰富,完整的模块如下所示:

    • XCB BigRequests API 发送和接受超过请求长度(65535字节)限制的数据
    • XCB Composite API 支持窗口合成,将多个窗口的内容合成最终的显示图像
    • XCB Damage API 用于跟踪窗口或者绘图上下文的可视区域的改变
    • XCB DPMS API 用于管理显示器的电源管理功能,控制显示器电源模式
    • XCB DRI2 API 支持直接渲染和硬件加速
    • XCB DRI3 API 支持直接渲染和硬件加速,对DRI2的扩展和改进
    • XCB Glx API 提供GLX接口创建OpenGL上下文,进行图形渲染和交互
    • XCB Present API 实现高性能的图像呈现在屏幕上
    • XCB RandR API 管理显示器的分辨率、屏幕方向和显示器布局等
    • XCB Record API 记录X服务器的事件流,以便进行调试、分析等
    • XCB Render API xrender绘图
    • XCB ScreenSaver API 管理屏幕保护程序的行为和状态
    • XCB Shape API 用于创建和操作不规则窗口的形状
    • XCB Shm API 应用程序能够通过共享内存的方式高效的传输图像
    • XCB Sync API 实现同步操作和时间戳的管理,确保预期的时序
    • XCB XCMisc API 提供额外的杂项函数和功能,获取一些服务器信息
    • XCB Core API 核心部分,提供了基本通信功能和操作
    • XCB Xevie API 拦截和处理X服务器上的事件流
    • XCB XF86Dri API 直接渲染,直接访问图形硬件,提高图形性能和效率
    • XCB XFixes API 增强功能:光标、窗口形状、窗口属性、窗口位置
    • XCB Xinerama API 用于管理多个显示器的配置和操作
    • XCB Input API 处理输入事件(如键盘、鼠标、触摸屏等)交互
    • XCB xkb API 配置与操作键盘相关的设置,键盘布局和状态
    • XCB XPrint API 直接从应用程序打印文档、图像和其他内容
    • XCB API xcb基础功能
    • XCB SELinux API 在应用程序中管理selinux安全策略和执行安全操作
    • XCB Test API 测试协议,常用于远程控制
    • XCB Xv API xvideo相关,用于视频渲染、视频加速、获取视频信息
    • XCB XvMC API 在GPU上执行视频解码和运动补偿功能 笔者在工作中也有时对xtrace日志无法分析到有用信息,此时会查看XCB帮助文档,通过分析对比查找到相关的xcb函数,从而逐渐熟悉X11协议。

    总结

    总体来说xtrace是一个有用的工具,对于笔者来说经常会接触生态软件的图形显示问题,对于少部分软件开发商不愿意提供代码和问题复现最小demo,此时其软件对于笔者来说是一个黑盒,当问题边界靠近X相关的技术时,笔者会使用xtrace去详细分析该软件的详细功能,往往这可以在底层剖析软件的显示工作方式,配合系统上相关的组建库代码,可以方便地处理客户的紧急问题。 笔者编写这篇文档是希望可以鼓励更多的同事使用xtrace,这个工具可以帮助你熟悉X11的工具原理,同时也可以理解像qt、gtk等UI库的底层实现,在定位系统复制粘贴、图形显示、拖拽、窗口相关的问题时可以提供更加底层的日志,方便更加精准地定位问题根因!

    参考资料

    Wednesday, November 1, 2023

    介绍

    随着Linux图形发展的需求增长,需要更多的图形功能和性能,这促成了Direct Rending Infastructure(DRI)和Direct Rendering Manager(DRM)的出现,DRM是一个内核级别的子系统,提供了对图形设备的管理和访问控制,它允许用户空间的图形库和应用程序直接访问GPU进行渲染,而无需操作系统介入,它的主要作用如下:

    • 图形硬件管理:DRM负责管理图形硬件资源,如显存、显卡以及显示设备。它允许内核与这些硬件设备进行通信和控制。
    • 图形加速:提供对硬件加速功能的支持,例如3D渲染和视频加速。这使得图形渲染更加高效和流畅。
    • 多显示器支持:允许多个显示器的管理和配置,包括扩展桌面、镜像模式等。
    • 用户空间接口:提供了用户空间图形库(如Mesa 3D等)和应用程序与内核中DRM子系统进行通信的接口。
    • 支持不同的图形协议:DRM在支持X Window System(X11)的同时,也能与新一代图形协议,如Wayland,进行整合。

    这种更好的性能、3D加速和对硬件的更直接访问使得它可以支持X Window System和Wayland的图形输出和管理。其现状如下:

    • 持续发展:随着硬件技术和图形需求的不断演进,DRM在Linux内核中也在不断发展。新的功能和改进不断加入,以满足新一代图形硬件和应用程序的需求。
    • 多厂商支持:DRM的发展得到了多家硬件厂商的支持,这包括AMD、Intel、NVIDIA等,它们为Linux内核开发并贡献了各自硬件的DRM驱动程序。
    • 支持新技术:DRM也在逐步支持新的图形技术,例如,对于低功耗图形处理的优化、支持更高分辨率、HDR显示以及机器学习和AI加速等方面的改进。
    • Wayland和DRM集成:Wayland作为新一代图形显示协议,与DRM更为紧密地整合,提供更为直接和高效的图形渲染和显示方式。

    总体而言,现如今很多Linux发行版上图形服务器后端都是对接的drm实现图形输出和相关控制,DRM在Linux图形子系统中扮演着关键角色,随着技术和需求的不断发展,它持续进化以适应新的硬件和应用场景,为Linux系统提供了强大的图形渲染支持。

    图1. DRM时间线

    DRM剖析

    Framebuffer && DRM

    在谈drm之前必须先了解一下它的前辈Framebuffer,Framebuffer是指一块内存区域,用户存储显示设备上每个像素的颜色信息。在早期的计算机系统中,操作系统直接将图形数据写入这块显存区域,这被称为直接显存访问(Direct Framebuffer Access),这种方式简单、直接,但随着图形复杂程度增加和硬件发展,它变得难以满足高分辨率和复杂图形的需求,随着事件的推移,由DRM提供了更多先进的功能,它追加能替代了Framebuffer,并称为linux图形系统的主要组成部分(图形框架演变如下图2)。然而Frambuffer仍然在某些特定场景下有其用户之地,尤其是在一些嵌入式系统和特定的硬件上。 总的来说,Framebuffer到DRM的发展代表了图形处理在Linux系统中的进步和演进,从最初的直接访问到高级的硬件加速和更加强大的图形功能。drm为显示硬件的适配和图形的发展贡献了很大的力量。

    图2. Framebuffer到DRM

    drm-plane

    drm_plane本质是对显示控制器中scanout硬件的抽象。简单来说,给定一个plane,可以让其与一个framebuffer关联表示进行scanout的数据,同时控制scanout时进行的额外操作,比如colorspace的改变,旋转、拉伸、偏移等操作。drm_plane是与硬件强相关的,显示控制器支持的plane是固定的,其支持的功能也是由硬件决定的。所有的drm_plane必为三种类型之一:

    • Primary - 主plane,一般控制整个显示器的输出。CRTC必须要有一个这样的plane。
    • Curosr - 表示鼠标光标图层(可选),一般启用的话称其为开启硬件光标,代码如下,流程如下图3。
    • Overlay - 叠加plane,可以在主plane上叠加一层输出,可选。
    ....
    if (cursor != NULL && drm_connector_is_cursor_visible(conn)) {
    	struct wlr_drm_fb *cursor_fb = get_next_cursor_fb(conn);
    	if (cursor_fb == NULL) {
    		wlr_drm_conn_log(conn, WLR_DEBUG, "Failed to acquire cursor FB");
    		return false;
    	}
    
    	drmModeFB *drm_fb = drmModeGetFB(drm->fd, cursor_fb->id);
    	if (drm_fb == NULL) {
    		wlr_drm_conn_log_errno(conn, WLR_DEBUG, "Failed to get cursor "
    			"BO handle: drmModeGetFB failed");
    		return false;
    	}
    	uint32_t cursor_handle = drm_fb->handle;
    	uint32_t cursor_width = drm_fb->width;
    	uint32_t cursor_height = drm_fb->height;
    	drmModeFreeFB(drm_fb);
        // 设置硬件光标,更新光标图像buffer
    	int ret = drmModeSetCursor(drm->fd, crtc->id, cursor_handle,
    			cursor_width, cursor_height);
    	int set_cursor_errno = errno;
    	if (drmCloseBufferHandle(drm->fd, cursor_handle) != 0) {
    		wlr_log_errno(WLR_ERROR, "drmCloseBufferHandle failed");
    	}
    	if (ret != 0) {
    		wlr_drm_conn_log(conn, WLR_DEBUG, "drmModeSetCursor failed: %s",
    			strerror(set_cursor_errno));
    		return false;
    	}
    
        // 移动硬件光标位置
    	if (drmModeMoveCursor(drm->fd,
    			crtc->id, conn->cursor_x, conn->cursor_y) != 0) {
    		wlr_drm_conn_log_errno(conn, WLR_ERROR, "drmModeMoveCursor failed");
    		return false;
    	}
    } else {
    	if (drmModeSetCursor(drm->fd, crtc->id, 0, 0, 0)) {
    		wlr_drm_conn_log_errno(conn, WLR_DEBUG, "drmModeSetCursor failed");
    		return false;
    	}
    }
    ....
    

    图3. drm-plane框架图

    图4. 多drm-plane合成流程图

    在笔者看来,drm-plane与layer的概念类似,只是更加偏向硬件底层概念,开发者可以通过控制显示相关的参数将plane投显到显示器上任何区域(参考图5),这种映射机制提供了非常方便地偏移、缩放方法,如果在这一层上实现移动和缩放等动画,效率将会比使用OpenGL等API制作高,将频繁变化的图像抽象工作在一个单独的drm-plane也可以达到提高性能和省电的目的。

    名字描述
    SRC_X当前framebuffer crop区域的起始偏移x坐标
    SRC_Y当前framebuffer crop区域的起始偏移y坐标
    SRC_W当前framebuffer crop区域的宽度
    SRC_H当前framebuffer crop区域的高度
    CRTC_X屏幕显示区域的起始偏移x坐标
    CRTC_Y屏幕显示区域的起始偏移y坐标
    CRTC_W屏幕显示区域的宽度
    CRTC_H屏幕显示区域的高度

    表1. drm-plane部分属性表

    图5. drm-plane送显参数控制

    crtc(Cathode Ray Tube Controller)

    CRTC 是显示控制器中的一个重要部分,主要用于管理显示设备的扫描和刷新。CRTC 负责生成视频信号的定时和同步,控制屏幕上像素的扫描和刷新,以确保正确的图像显示。它管理像素的输出到屏幕上的确切位置,以及刷新率、分辨率等显示参数。在现代的图形处理中,CRTC通常由图形处理单元(GPU)或显示控制器中的专用部分来控制,以确保正确的图像输出。

    图6. crtc框架图

    drm legacy链路通过调用drmModePageFlip请求crtc更新显示器图像,其代码如下,流程如图6所示:

    static bool legacy_crtc_commit(struct wlr_drm_connector *conn,
    		const struct wlr_drm_connector_state *state,
    		uint32_t flags, bool test_only) {
    	....
    
    	if (flags & DRM_MODE_PAGE_FLIP_EVENT) {
    		uint32_t page_flags = DRM_MODE_PAGE_FLIP_EVENT;
    		if (flags & DRM_MODE_PAGE_FLIP_ASYNC) {
    			page_flags |= DRM_MODE_PAGE_FLIP_ASYNC;
    		}
    
    		if (drmModePageFlip(drm->fd, crtc->id, fb_id,
    				page_flags, drm)) {
    			wlr_drm_conn_log_errno(conn, WLR_ERROR, "drmModePageFlip failed");
    			return false;
    		}
    	}
    
    	return true;
    }
    

    图7. buffer更新流程图

    encoder

    编码器从CRTC获取像素数据,并将其转换为适合任何连接器的格式。在某些设备上,CRTC可以将数据发送到多个编码器。在这种情况下,两个编码器将从同一扫描输出缓冲区接收数据,从而在连接到每个编码器的连接器上产生“克隆”显示配置。 在Linux系统中,DRM(Direct Rendering Manager)是用于处理图形显示的子系统,负责在用户空间和图形硬件之间提供接口。DRM编码器在Linux中扮演了关键角色,主要用于对图形和视频内容进行编码、解码、处理和显示。在DRM框架中,编码器的作用包括但不限于:

    • 视频压缩和解压缩: 编码器负责对视频内容进行压缩,以减小数据量并更有效地传输和存储视频流。解码器用于解压缩视频内容,以便图形硬件能够正确地渲染和显示内容。
    • 图形处理和渲染: DRM编码器能够处理图形和视频流,进行渲染和合成,然后将图像数据发送到显示设备以在屏幕上显示。
    • 硬件加速: DRM编码器还可以利用硬件加速功能,以便更快地处理图形和视频内容。这有助于提高性能和效率,特别是对于高分辨率视频或图形内容的处理。
    • 支持多种编解码标准: DRM编码器能够支持多种视频编解码标准,如H.264、H.265等,确保对不同格式的视频流进行正确的处理。 在Linux系统中,DRM编码器与图形驱动程序、图形处理单元(GPU)和显示设备等硬件密切相关。它允许Linux系统管理和控制图形硬件,处理视频流并在屏幕上显示图像,同时也与DRM系统中的访问控制和安全性机制集成,确保受保护内容的安全传输和显示。

    图8. encoder框架图

    connector

    DRM connectors是用于管理和描述图形设备的连接器或端口。它们提供了对显示设备的连接和属性描述。在图形系统中,这些连接器可以代表诸如HDMI、DisplayPort、DVI等物理连接接口,而它们也可以描述内部连接,例如LCD panels。DRM connectors 在 Linux 中的作用包括:

    • 显示设备连接描述: DRM connectors 提供了关于显示设备的物理连接信息,比如类型(HDMI、VGA、DisplayPort等)、连接状态以及支持的分辨率和刷新率等信息。
    • 动态连接管理: 它们可以监测连接和断开事件。当显示设备插入或拔出时,DRM connectors 可以检测到这些变化,并通知系统相应的变化。这使系统能够动态调整图形配置以适应新的连接或断开状态。
    • 多显示器支持: DRM connectors 允许系统管理多个显示器的连接和配置。这使得可以同时使用多个显示器或监视器,或者在不同的显示设备上显示不同的内容。
    • 显示属性查询和配置: 通过 DRM connectors,可以查询连接器支持的分辨率、刷新率以及其他显示属性。这使系统可以调整和配置图形设备,以最佳方式显示图形内容。

    总体来说,DRM connectors 在 Linux 中的作用是管理显示设备的物理连接、状态和属性,使系统能够动态适应不同的显示设备、支持多个显示器,并提供相应的配置信息,以便正确地显示图形内容。

    图9. connector框架图

    framebuffer

    framebuffer是一个重要概念。Framebuffers是用于存储屏幕上每个像素的颜色和其他相关信息的内存区域。在DRM中,framebuffer提供了一个抽象接口,允许用户空间程序访问和操作显存,以便渲染图形数据。其作用包括:

    • 图形数据存储: Framebuffers提供一个区域,用于存储图形数据,包括每个像素的颜色、透明度等信息。这些数据构成了屏幕上显示的图像。
    • 直接访问屏幕数据: 通过framebuffers,用户空间程序或操作系统内核能够直接访问和操作图形数据,而无需经过额外的复杂处理。
    • 硬件抽象层: Framebuffers提供了一个硬件无关的抽象接口,这意味着不同的图形设备和硬件都可以通过相同的接口进行访问和操作。
    • 显示图像: Framebuffers存储了最终用于在屏幕上显示的图像数据。图形数据在 framebuffer中进行组织和处理,然后通过显示控制器输出到屏幕。

    在DRM 中,framebuffers通常与CRTC和显示控制器等组件一起工作。framebuffers提供的抽象层允许操作系统或应用程序以统一的方式对图形数据进行处理和管理,无论具体的硬件设备是什么样的。总之,framebuffers 在 Linux 的 DRM 中提供了一个接口和数据结构,用于存储、操作和管理图形数据,使其能够在屏幕上正确地显示图像。

    图10. framebuffer内存结构图

    drm调试工具

    drm_info

    用于转储有关 DRM 设备信息的小实用程序

    drm_info
    
    Node: /dev/dri/card1
    ├───Driver: i915 (Intel Graphics) version 1.6.0 (20201103)
    │   ├───DRM_CLIENT_CAP_STEREO_3D supported
    │   ├───DRM_CLIENT_CAP_UNIVERSAL_PLANES supported
    │   ├───DRM_CLIENT_CAP_ATOMIC supported
    │   ├───DRM_CLIENT_CAP_ASPECT_RATIO supported
    │   ├───DRM_CLIENT_CAP_WRITEBACK_CONNECTORS supported
    │   ├───DRM_CAP_DUMB_BUFFER = 1
    │   ├───DRM_CAP_VBLANK_HIGH_CRTC = 1
    │   ├───DRM_CAP_DUMB_PREFERRED_DEPTH = 24
    │   ├───DRM_CAP_DUMB_PREFER_SHADOW = 1
    │   ├───DRM_CAP_PRIME = 3
    │   ├───DRM_CAP_TIMESTAMP_MONOTONIC = 1
    │   ├───DRM_CAP_ASYNC_PAGE_FLIP = 1
    │   ├───DRM_CAP_CURSOR_WIDTH = 256
    │   ├───DRM_CAP_CURSOR_HEIGHT = 256
    │   ├───DRM_CAP_ADDFB2_MODIFIERS = 1
    │   ├───DRM_CAP_PAGE_FLIP_TARGET = 0
    │   ├───DRM_CAP_CRTC_IN_VBLANK_EVENT = 1
    │   ├───DRM_CAP_SYNCOBJ = 1
    │   └───DRM_CAP_SYNCOBJ_TIMELINE = 1
    ├───Device: PCI 8086:46aa Intel Corporation Alder Lake-UP4 GT2 [Iris Xe Graphics]
    │   └───Available nodes: primary, render
    ├───Framebuffer size
    │   ├───Width: [0, 16384]
    │   └───Height: [0, 16384]
    ├───Connectors
    │   ├───Connector 0
    │   │   ├───Object ID: 236
    │   │   ├───Type: eDP
    │   │   ├───Status: connected
    │   │   ├───Physical size: 290x180 mm
    │   │   ├───Subpixel: unknown
    │   │   ├───Encoders: {0}
    │   │   ├───Modes
    │   │   │   └───[email protected] preferred driver phsync nvsync 
    │   │   └───Properties
    │   │       ├───"EDID" (immutable): blob = 266
    │   │       ├───"DPMS": enum {On, Standby, Suspend, Off} = On
    │   │       ├───"link-status": enum {Good, Bad} = Good
    │   │       ├───"non-desktop" (immutable): range [0, 1] = 0
    │   │       ├───"TILE" (immutable): blob = 0
    │   │       ├───"CRTC_ID" (atomic): object CRTC = 80
    │   │       ├───"scaling mode": enum {Full, Center, Full aspect} = Full aspect
    │   │       ├───"panel orientation" (immutable): enum {Normal, Upside Down, Left Side Up, Right Side Up} = Normal
    │   │       ├───"Broadcast RGB": enum {Automatic, Full, Limited 16:235} = Automatic
    │   │       ├───"max bpc": range [6, 12] = 10
    │   │       ├───"Colorspace": enum {Default, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, RGB_WIDE_FIXED, RGB_WIDE_FLOAT, BT601_YCC} = Default
    │   │       ├───"HDR_OUTPUT_METADATA": blob = 0
    │   │       └───"vrr_capable" (immutable): range [0, 1] = 0
    │   ├───Connector 1
    │   │   ├───Object ID: 245
    │   │   ├───Type: DisplayPort
    │   │   ├───Status: disconnected
    │   │   ├───Encoders: {1}
    │   │   └───Properties
    │   │       ├───"EDID" (immutable): blob = 0
    │   │       ├───"DPMS": enum {On, Standby, Suspend, Off} = Off
    │   │       ├───"link-status": enum {Good, Bad} = Good
    │   │       ├───"non-desktop" (immutable): range [0, 1] = 0
    │   │       ├───"TILE" (immutable): blob = 0
    │   │       ├───"CRTC_ID" (atomic): object CRTC = 0
    │   │       ├───"subconnector" (immutable): enum {Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native} = Unknown
    │   │       ├───"audio": enum {force-dvi, off, auto, on} = auto
    │   │       ├───"Broadcast RGB": enum {Automatic, Full, Limited 16:235} = Automatic
    │   │       ├───"max bpc": range [6, 12] = 12
    │   │       ├───"Colorspace": enum {Default, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, RGB_WIDE_FIXED, RGB_WIDE_FLOAT, BT601_YCC} = Default
    │   │       ├───"HDR_OUTPUT_METADATA": blob = 0
    │   │       ├───"vrr_capable" (immutable): range [0, 1] = 0
    │   │       ├───"Content Protection": enum {Undesired, Desired, Enabled} = Undesired
    │   │       └───"HDCP Content Type": enum {HDCP Type0, HDCP Type1} = HDCP Type0
    │   └───Connector 2
    │       ├───Object ID: 258
    │       ├───Type: DisplayPort
    │       ├───Status: disconnected
    │       ├───Encoders: {6}
    │       └───Properties
    │           ├───"EDID" (immutable): blob = 0
    │           ├───"DPMS": enum {On, Standby, Suspend, Off} = Off
    │           ├───"link-status": enum {Good, Bad} = Good
    │           ├───"non-desktop" (immutable): range [0, 1] = 0
    │           ├───"TILE" (immutable): blob = 0
    │           ├───"CRTC_ID" (atomic): object CRTC = 0
    │           ├───"subconnector" (immutable): enum {Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native} = Unknown
    │           ├───"audio": enum {force-dvi, off, auto, on} = auto
    │           ├───"Broadcast RGB": enum {Automatic, Full, Limited 16:235} = Automatic
    │           ├───"max bpc": range [6, 12] = 12
    │           ├───"Colorspace": enum {Default, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, RGB_WIDE_FIXED, RGB_WIDE_FLOAT, BT601_YCC} = Default
    │           ├───"HDR_OUTPUT_METADATA": blob = 0
    │           ├───"vrr_capable" (immutable): range [0, 1] = 0
    │           ├───"Content Protection": enum {Undesired, Desired, Enabled} = Undesired
    │           └───"HDCP Content Type": enum {HDCP Type0, HDCP Type1} = HDCP Type0
    ├───Encoders
    │   ├───Encoder 0
    │   │   ├───Object ID: 235
    │   │   ├───Type: TMDS
    │   │   ├───CRTCS: {0, 1, 2, 3}
    │   │   └───Clones: {0}
    │   ├───Encoder 1
    │   │   ├───Object ID: 244
    │   │   ├───Type: TMDS
    │   │   ├───CRTCS: {0, 1, 2, 3}
    │   │   └───Clones: {1}
    
    .........
    
    ├───CRTCs
    │   ├───CRTC 0
    │   │   ├───Object ID: 80
    │   │   ├───Legacy info
    │   │   │   ├───Mode: [email protected] preferred driver phsync nvsync 
    │   │   │   └───Gamma size: 256
    │   │   └───Properties
    │   │       ├───"ACTIVE" (atomic): range [0, 1] = 1
    │   │       ├───"MODE_ID" (atomic): blob = 271
    │   │       │   └───[email protected] preferred driver phsync nvsync 
    │   │       ├───"OUT_FENCE_PTR" (atomic): range [0, UINT64_MAX] = 0
    │   │       ├───"VRR_ENABLED": range [0, 1] = 0
    │   │       ├───"SCALING_FILTER": enum {Default, Nearest Neighbor} = Default
    │   │       ├───"DEGAMMA_LUT": blob = 0
    │   │       ├───"DEGAMMA_LUT_SIZE" (immutable): range [0, UINT32_MAX] = 129
    │   │       ├───"CTM": blob = 0
    │   │       ├───"GAMMA_LUT": blob = 270
    │   │       └───"GAMMA_LUT_SIZE" (immutable): range [0, UINT32_MAX] = 1024
    
    ........
    
    
    └───Planes
        ├───Plane 0
        │   ├───Object ID: 31
        │   ├───CRTCs: {0}
        │   ├───Legacy info
        │   │   ├───FB ID: 237
        │   │   │   ├───Object ID: 237
        │   │   │   ├───Size: 2880x1800
        │   │   │   ├───Format: ARGB2101010 (0x30335241)
        │   │   │   ├───Modifier: I915_FORMAT_MOD_Y_TILED (0x100000000000002)
        │   │   │   └───Planes:
        │   │   │       └───Plane 0: offset = 0, pitch = 11520 bytes
        │   │   └───Formats:
        │   │       ├───C8 (0x20203843)
        │   │       ├───RGB565 (0x36314752)
        │   │       ├───XRGB8888 (0x34325258)
        │   │       ├───XBGR8888 (0x34324258)
        │   │       ├───ARGB8888 (0x34325241)
        │   │       ├───ABGR8888 (0x34324241)
        
        .....
    

    drm_monitor

    用于监控 KMS 状态的 CLI 工具

    drm_monitor -d /dev/dri/card1
    CRTC 80: seq=30095325875 ns=3076787603594 delta_ns=16668012 Hz=59.995157
    CRTC 131: seq=0 ns=0 delta_ns=0 Hz=0.000000
    CRTC 182: seq=0 ns=0 delta_ns=0 Hz=0.000000
    CRTC 233: seq=0 ns=0 delta_ns=0 Hz=0.000000
    

    参考资料

    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源代码仓库,以保证插件接口的一致性和外观的统一性。