DesseraLab

Technology exploration and development

0%

今天突然想起来了一个开源的 Vscode 打包,叫做 Vscodium,出于对开源的热爱(或者是某种洁癖?),我决定实际更换到该版本使用一段时间。

首先得说明一点,为什么会有这个项目?要聊这个我们得先了解 Vscode 的开源方案。简单来说,对于 Vscode 本身,它是完全开源的(你可以在Vscode on Github上查看其源代码),但是,这个开源项目并不等同于我们使用的 Vscode。我们使用的 Vscode 是微软官方发布的可执行程序,这份程序在构建过程中加入了微软的私有内容,让它不再符合开源协议。

对于一些开源爱好者而言,这是无法忍受的(或者说是别扭的),因此他们自行打包并分发 Vscode 的社区版本,即 Vscodium。

相应的还有一个社区版扩展源:Open VSX

安装 Vscodium

对于 ArchLinux 用户,可以在archlinuxcn源中下载:

1
sudo pacman -S vscodium-bin

对于其他发行版用户,我无法确定是否能在软件源中获取,不过也可以使用通用下载方法,访问Vscodium以获取最新的发布版本。

虽然有悖于我们安装这个软件的初衷,但 Open VSX 源的扩展数量确实不如微软官方源,如果想要得到更多的扩展,我们就要将 Vscodium 的扩展源替换为微软官方源。

首先,来到软件的安装目录(对于ArchLinux用户来说,通常都是/opt/vscodium-bin),编辑该目录下的resources/app/product.json,将其中extensionsGallery的值改为如下内容:

1
2
3
4
5
6
{
"extensionsGallery": {
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
"itemUrl": "https://marketplace.visualstudio.com/items"
}
}

同步方案

使用 Vscodium 遇到的第一个问题是,我们失去了配置同步的能力。解决它的最佳方案是使用shan.code-settings-sync插件。

该插件基于 Github Gist 提供同步服务,安装之后,我们可以Ctrl + Shift + P唤出顶部列表,选择Sync: 更新/上传配置进行同步操作,在这个过程中,可能会需要经过 Github 授权。

该插件最后的稳定版停止于2019年,因此使用这个插件是比较危险的,但经过测试,只有这个插件能够完美的同步所有配置文件,也算是一种无奈之举吧。

更多

本文尚未完工

说到现代C++,我们就不得不提到智能指针,这些模板类自C++11起被引入进标准库,不涉及操作系统、不涉及繁琐的内存申请流程,甚至你自己就能实现一个简单的智能指针。但即便如此,直到今天,仍然有人没有了解过,也不去使用智能指针。

实际上我认为,智能指针为 C++ 这门语言带来了一个新的概念,即所有权。智能指针表面上管理了一块由用户申请的内存,但实际上它是管理了这一块资源的所有权。这一概念在 Rust 语言中被更加明确地表达出来,但在 C++ 中,我们也可以通过智能指针来实现。

所有权

为什么会产生所有权机制?回想一下你在古老的 C/C++ 工作时,你不得不手动申请内存,手动管理他们的销毁时机,使用对象,还要时刻提防可能存在的拷贝操作(后来还要提防一些移动操作)。这些东西一开始就该由程序员管理,但是别忘了,我们在谈论一个现代语言!

在我看来,现代语言是高级语言的一个子集,它们应当在某一领域,解决过去程序员们的痛点。专注于底层的现代语言,如 Zig,它保留了内存管理的能力、并在语言层面支持了 Allocator, 扩充了内存管理的能力。而另一些现代语言,如 Rust,它在语言层面支持了所有权机制,使得程序员不再需要关心内存的申请和释放。

我们在谈论C++,从C++11开始,这门古老的语言开始了自己的现代化之路,而其中一个最显著的改变就是智能指针和其带来的所有权机制。

简单来说,所有权是一种属于关系,在这种关系下定义的对象其本身不会有隐式的复制和移动操作,将一个对象赋值、传递给另一个对象,只不过是将所有权转移给了另一个对象,在这个过程中,没有任何内存的拷贝和移动操作。

所有权的实现

对于 C++ 来说,所有权有两种,一种是独占所有权,另一种是共享所有权。独占所有权是指一个对象只能被一个所有者拥有,而共享所有权是指一个对象可以被多个所有者拥有。独占所有权在 C++ 中由 std::unique_ptr 实现,而共享所有权则由 std::shared_ptr 实现。

在一些场景(尤其是嵌入式领域)中,我们通常会针对每一个通信协议创建一个类,他们负责与其他设备的通信实现,而一些上层应用类,它们会需要调用这些协议类的方法。

创建这个协议类有两种方式:

  • 由应用类自己创建
  • 从外部创建,传递给应用类

但一般情况下,这些协议类的 IO 通道都是有限的,我们不希望应用类创建多个相同的协议类,这时候我们就可以使用智能指针来管理这些协议类的所有权。

由此可以引申出两种情况:

  • 一个协议类只服务于一个应用类,这时候我们可以使用 std::unique_ptr 来管理这个协议类的所有权
  • 一个协议类可能会被多个应用类使用,这时候我们可以使用 std::shared_ptr 来管理这个协议类的所有权

std::unique_ptr

std::unique_ptr 是一个独占所有权的智能指针,它只能有一个所有者,当这个所有者被销毁时,它所管理的资源也会被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>

class Protocol {
// ...
};

class Application {
public:
// 构造时,将协议类的所有权转移给 Application
Application(std::unique_ptr<Protocol> protocol) : protocol_(std::move(protocol)) {}

private:
std::unique_ptr<Protocol> protocol_;
};

int main() {
// 创建一个协议类,此时所有权掌握在 main 函数中
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();

// 创建一个应用类,将协议类的所有权转移给 Application
Application app(std::move(protocol));
}

std::shared_ptr

std::shared_ptr 是一个共享所有权的智能指针,它可以有多个所有者,当最后一个所有者被销毁时,它所管理的资源也会被销毁。

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
#include <memory>

class Protocol {
// ...
};

class Application {
public:
// 构造时,将协议类的所有权转移给 Application
Application(std::shared_ptr<Protocol> protocol) : protocol_(protocol) {}

private:
std::shared_ptr<Protocol> protocol_;
};

int main() {
// 创建一个协议类,此时所有权掌握在 main 函数中
std::shared_ptr<Protocol> protocol = std::make_shared<Protocol>();

// 创建一个应用类,将协议类的所有权转移给 Application
Application app(protocol);

// 创建另一个应用类,共享协议类的所有权
Application app2(protocol);
}

论所有权的转让与移动

很多人都有一个误解,认为移动语义体现了所有权的转让,但是,二者是不同的。

不论什么情况下,移动构造一定会重新创建资源对象,而至于资源内部的数据是如何移动的,这是对象的实现决定的,但针对资源对象本身,在这个过程中一定会重新创建一个资源对象。

而所有权的转让是另一回事,它直接将自己拥有的资源本身交予了另一个对象,在这个过程中,没有任何资源的拷贝和移动操作(实际上,在计算机眼中,这个过程什么也没发生)。

有趣的一点是,所有权的转让这一操作的实现依靠了移动语义,std::unique_ptr 不允许拷贝,但允许移动,刚才我们提到移动一定会创建新对象,而std::unqiue_ptr的移动虽然创建了新的std::unique_ptr,但其内部的指针仍然指向同一个资源对象,这就是所有权的转让的实现。

std::shared_ptr维护了一个引用计数,这让它可以进行拷贝,也可以进行移动,引用计数会时刻保证资源对象的生命周期,当最后一个所有者被销毁时,资源对象也会被销毁。

论资源的所有者和引用者

上文我们忽略的一种情况,有一些类或函数并不拥有资源,但他们需要暂时的资源使用权。对于我们已经建立的资源所有权模型,每一种所有权模式都有其对应的引用类型。

std::unique_ptr 对应的引用类型是原生指针,它指向资源对象本身,但在语义上不拥有它,只是暂时的使用。

std::shared_ptr 对应的引用类型是 std::weak_ptr,它指向资源对象,但不会增加引用计数,当资源对象被销毁时,std::weak_ptr 会被置空。

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
#include <memory>

class Protocol {
// ...
};

class Application {
public:
// 构造时,将协议类的所有权转移给 Application
Application(std::shared_ptr<Protocol> protocol) : protocol_(protocol) {}

private:
std::shared_ptr<Protocol> protocol_;
};

void foo(std::weak_ptr<Protocol> weak_protocol) {
// 使用 weak_ptr
}

int main() {
// 创建一个协议类,此时所有权掌握在 main 函数中
std::shared_ptr<Protocol> protocol = std::make_shared<Protocol>();

// 创建一个应用类,将协议类的所有权转移给 Application
Application app(protocol);

// 创建一个 weak_ptr,暂时使用协议类
std::weak_ptr<Protocol> weak_protocol = protocol;

// 调用函数,传递 weak_ptr
foo(weak_protocol);
}

值得注意的一点是,std::weak_ptr 不能直接使用,它需要通过 lock 方法转换为 std::shared_ptr 才能使用。

总结

新的资源管理模型标志着现代 C++ 的到来,我一直秉承着一个观点——任何拥有内存申请和释放的程序都应当使用智能指针,这不仅仅是为了避免内存泄漏,更是为了通过所有权这一概念更好地构建应用逻辑模型。

Nvidia 驱动程序,一个令 Linux 爱好者闻风丧胆的词语,它不同于开源的Nouveau驱动开箱即用,也不如AMD的显卡驱动那样安装简单。很多人——包括半年前的我,都禁不住疑惑,这个驱动为什么这么容易崩溃?

在我更换工作站到 Arch Linux 后,我终于有了机会去探索这个问题。实际上,如果你的大部分程序处于稳定版,并正确配置了所有关于显卡的设置,Nvidia 驱动程序完全可以正常工作。当然,我并不是说 Nvidia 驱动的崩溃全部都是使用者的问题,毕竟该驱动的不稳定性以及兼容性有目共睹。我只是想证明,Nvidia 驱动并不像传说中那么难以使用。

安装

不管在别的系统上如何折磨人,在 Arch Linux 的世界里,这一切都是如此简单,只要你使用的是一般的linux内核,安装驱动只需要一个命令:

1
sudo pacman -S nvidia nvidia-utils

如果你用的是其他内核、多内核甚至是自己编译的内核,你觉得会很复杂吗?不,只需要把上述命令中的nvidia换成nvidia-dkms即可。

当你重启之后,可能会有两个结果:一是你的系统正常启动,你看到了 SDDM 、GDM 或者 LightDM 的登录界面;二是你的系统卡在了启动界面,没有任何反应。

对于前者来说,恭喜你,你的驱动安装成功了。对于后者,也恭喜你,你的驱动安装成功了,只是你的系统没有正确加载驱动。这时候,你需要进入 tty,并查看一些配置是否正确。

这篇文章并不是一个错误排查指南,我只会告诉你一些浅显的东西,Nvidia 驱动如何加载、如何参与你桌面的启动、如何查看驱动是否加载等等。

Nvidia 驱动的加载流程

这里假定你使用的是 GRUB2 引导加载程序。

当你按下启动键,您首先看到的是您厂商的 BIOS/UEFI 启动界面,然后是 GRUB 启动界面。

什么是 GRUB?GRUB 是一个引导加载程序,它会让你选择你想要启动的系统(或者内核)。在这个阶段,Nvidia 驱动并没有加载。但你仍然可以添加一些参数来控制 Nvidia 驱动的行为,这些参数被称为内核引导参数。

最经典的莫过于nvidia_drm.modeset参数,它能够启用 DRM 内核级显示模式(这是一个显示管理系统,不在今天的讨论范围内)。你可以打开/etc/default/grub文件,找到GRUB_CMDLINE_LINUX_DEFAULT这一行,添加该条目:

1
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 nowatchdog nvidia_drm.modeset=1"

然后运行sudo grub-mkconfig -o /boot/grub/grub.cfg来更新 GRUB 配置文件。

还有一种添加模块参数的方法,你可以在/etc/modprobe.d目录下创建一个文件,比如nvidia.conf,添加如下内容:

1
options nvidia-drm modeset=1

接下来是内核启动阶段,其存在一个早启动阶段,你可以指定这个阶段启动的模块,这些模块会最早加载到内核中。你可以打开/etc/mkinitcpio.conf文件,找到MODULES这一行,添加nvidia nvidia_modeset nvidia_uvm nvidia_drm这几个模块:

1
MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)

然后运行sudo mkinitcpio -P来更新内核镜像。

如果这些内容没有被指定,一般情况下,显卡驱动可能会晚于显示管理器启动,这样会导致你的系统卡在启动界面。

与显示管理器的交互

这里必须要声明一点,显示管理器不是你的桌面!大部分情况下,它指代你的登录界面。

一般情况下,显示管理器根据其所使用的显示协议,有不同的方式来加载 Nvidia 驱动。

对于经典的Xorg服务器,它会先检测显卡驱动,并在大部分情况下选择 Nouveau 驱动。但且慢!我们没有提及一个重要的问题,nvidia软件包做了很多幕后工作,其中之一就是对 Xorg 服务器的配置。

值得一提的是,nvidia软件包会自动禁用 Nouveau 驱动,如果你手动移除了其针对 Xorg 的配置文件,Xorg毫无疑问地会加载失败。

你可以在/usr/share/X11/xorg.conf.d目录下找到一个名为10-nvidia-drm-outputclass.conf的文件,这个文件会告诉 Xorg 服务器使用 Nvidia 驱动。如果你没有这个文件,你可以手动创建一个:

1
2
3
4
5
6
7
8
Section "OutputClass"
Identifier "nvidia"
MatchDriver "nvidia-drm"
Driver "nvidia"
Option "AllowEmptyInitialConfiguration"
ModulePath "/usr/lib/nvidia/xorg"
ModulePath "/usr/lib/xorg/modules"
EndSection

这个文件会告诉 Xorg 服务器使用 Nvidia 驱动,而不是 Nouveau 驱动。

可笑的是,有相当一部分人会通过移除这个文件来”解决“Nvidia驱动的问题,这是一个错误的做法。

在这种情况下,通过 Xorg 启动桌面后,你会发现nvidia-smi命令的输出中永远都包含Xorg进程,这是完全正常的现象。

为什么?因为一切的页面都会通过 Xorg 服务器来显示,我们不可避免地会让软件运行在 Nvidia 显卡上(特别是对于游戏程序)。

而对于 Wayland 来说,一般来说设置内核引导参数就足够了,如果在这种情况下仍然无法进入桌面,请查看你的混成器文档。

多显卡

很多笔记本都是经典的双显卡设计,一个是 Intel 集成显卡,一个是 Nvidia 独立显卡。这种情况下,在上述配置完备的情况下,只要安装nvidia-prime软件包,你就可以指定程序使用哪个显卡。

1
sudo pacman -S nvidia-prime

要使用 Nvidia 显卡,你可以在程序启动时添加prime-run前缀:

1
prime-run glxgears

这样,你就可以在 Nvidia 显卡上运行程序了。

某些DM或者WM可能会默认使用Nvidia显卡作为渲染设备,为了功耗考虑,你可能需要手动设置以选择Intel显卡作为渲染设备。

对于 Hyprland 来说,它需要一个环境变量来指定使用哪个显卡:

1
export WLR_DRM_DEVICES=/dev/dri/card0

对于不同的DM或者WM,可能需要不同的设置,具体请查看相关文档。

总结

这篇文章并不是错误排查指南,也不是Nvidia驱动指南,正如我开篇提到的,本篇文章只是讲述一些浅显的内容,提供给你修复Nvidia显卡驱动问题的一些思路。

而对于更多的内容,你可以查看 Arch Wiki 上的 Nvidia 驱动页面,它包含了大量的内容,包括错误排查、多显卡设置、性能优化等等。

一直以来,我们管理 Python 项目的依赖都是通过手动创建虚拟环境,然后在虚拟环境中安装依赖。这种方式简单且有效,但如果读者尝试过 cargo、npm、yarn 等现代化的依赖管理工具,就会发现 Python 的依赖管理方式有些落后。

何出此言?我们来对比一下 cargo 和 conda 的依赖管理方式:

cargo针对每个项目都会创建一个 Cargo.toml 文件,用于记录项目的依赖信息。这样,我们只需要在 Cargo.toml 中添加依赖,然后执行 cargo build 即可安装依赖。这个过程是声明式的,我们随时可以看到项目的依赖信息,而这份信息也严格地与项目真正的依赖保持一致。

而对于 conda,即便有 environment.yml 文件与 requirements.txt 文件,我们也无法保证这份文件中的依赖与项目真正的依赖保持一致。往往我们改变了项目的依赖后,忘记更新这份文件,导致项目无法正常运行。

从以上的对比我们可以看出, conda 式的项目管理实际上是命令式的,这种以交互的方式构建项目环境的方式天生具有可变性,而 cargo 式的项目管理则是声明式的,这种方式实际上代表了不可变性。而后者是构建可复现系统的基础。

由此,我们引出了一个问题,如何让 Python 的依赖管理更像 cargo 呢?今天,我们介绍一个现代的 Python 依赖管理工具:Poetry。

Poetry

Poetry 是一个 Python 项目的依赖管理工具,它的目标是让 Python 项目的依赖管理更加现代化。Poetry 通过一个 pyproject.toml 文件来管理项目的依赖,这个文件类似于 cargo 的 Cargo.toml 文件。

要安装 Poetry,官方推荐使用 pipx

1
pipx install poetry

如果你正在使用 Arch Linux,你可以直接通过 pacman 安装 Poetry:

1
sudo pacman -S python-poetry

如果你想同时使用多个版本的 Poetry,你可以通过 asdf 来管理 Poetry 的版本:

1
2
3
asdf plugin add poetry
asdf install poetry latest # 安装最新版本的 Poetry
asdf global poetry latest # 设置全局的 Poetry 版本

安装完成后,我们可以通过 poetry --version 来查看 Poetry 的版本:

1
poetry --version

然后,我们使用 Poetry 来创建一个新的 Python 项目:

1
poetry new poetry-demo

这个命令会在当前目录下创建一个名为 poetry-demo 的 Python 项目。我们可以进入这个项目,然后查看整个项目结构:

1
2
3
4
5
6
7
.
├── poetry_demo
│   └── __init__.py
├── pyproject.toml
├── README.md
└── tests
└── __init__.py

pyproject.toml

我们先来熟悉一下 pyproject.toml 文件,以下是一个新建项目的 pyproject.toml 文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["dessera <1533653159@qq.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

基本配置没有什么好说的,这里只强调一些重要的配置项,更多的配置项可以参考 官方文档

  • tool.poetry.package-mode:指定项目的性质,true 代表项目是一个库,false 代表项目是一个应用程序。
  • tool.poetry.dependencies:项目的依赖,这里我们可以指定项目的依赖,例如 requests = "^2.31.0"
  • build-system:构建系统的配置,可以指定构建系统。

还有一些进阶配置,比如依赖分组、依赖源管理、extra 等,但这些不适合在这篇文章中展开讨论。

添加依赖

我们可以通过 poetry add 命令来添加依赖,例如我们添加一个 requests 依赖:

1
poetry add requests

也可以直接在 pyproject.toml 文件中手动添加依赖:

1
2
3
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31.0"

二者的区别是,poetry add 会自动同步虚拟环境中的依赖,而手动添加依赖则需要执行 poetry install 来同步虚拟环境中的依赖。

当添加好依赖后,我们可以执行 poetry run python 来进入虚拟环境,然后引入依赖来测试:

1
poetry run python
1
2
3
>>> import requests
>>> requests.__version__
'2.31.0'

构建项目

只有库项目才能使用 poetry build 命令来构建项目,这个命令会在项目根目录下生成一个 dist 目录,里面包含了项目的构建产物。

1
2
3
4
5
poetry build

# 查看构建产物
ls dist
poetry_demo-0.1.0-py3-none-any.whl poetry_demo-0.1.0.tar.gz

发布项目

如果我们想发布项目到 PyPI,我们可以使用 poetry publish 命令:

1
poetry publish

这个命令会将项目发布到 PyPI,当然,你需要先在 PyPI 上注册一个账号,鉴于现在的 PyPI 状态,这个步骤暂时无法完成。

作为应用程序

如果我们的项目是一个应用程序,我们可以使用 poetry run 命令来运行项目:

1
poetry run python -m poetry_demo

这需要我们在包中拥有一个 __main__.py 文件,这个文件会在我们执行 poetry run python -m poetry_demo 时被执行。

或者,你可以手动执行项目的入口文件:

1
poetry run python path/to/entry.py

该入口文件可以是任意 Python 文件。

环境兼容

Poetry 可以使用 requirements.txt 文件来兼容 pip,这样我们可以在 Poetry 与 pip 之间无缝切换。

1
poetry export -f requirements.txt > requirements.txt

这个命令会将项目的依赖导出到 requirements.txt 文件中,然后我们可以使用 pip 来安装这些依赖:

1
pip install -r requirements.txt

总结

Poetry 实际上更像是一个现代化的 Python 库管理工具,但个人认为,使用 Poetry 的意义在于让我们的项目更加现代化,更加可维护。Poetry 的出现,让我们的 Python 项目管理更加像 cargo,这是一个好的方向。

Poetry 还有诸多进阶功能,将在后续的文章中展开讨论。