跳转至

拓展阅读

Python 环境的另一种管理方式:Conda

Conda 是一个广泛使用的开源的包管理与环境管理系统。Miniconda 与 Anaconda 是两个广为人知的基于 Conda 的 Python 的发行版本。

Anaconda 与 Miniconda

Miniconda 和 Anaconda 都是开源的 Python 的发行版本。

Miniconda 是 Anaconda 的免费迷你版本,只包含了 Conda、Python 及其依赖,以及少量其他有用的包,例如 pip 和 zlib。而 Anaconda 则额外包含了 250 多个自动安装的科学软件包,例如 SciPy 和 NumPy,并且测试了这些软件包之间的兼容性。Anaconda 分为个人版、商业版、团队版、企业版,除了个人版以外,其余版本均为付费产品。

安装 Miniconda

从官网下载安装 Miniconda,并进入虚拟环境。

$ sh -c "$(wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O -)"

# 前面的选项可以保持默认,也可以自行修改
please answer 'yes' or 'no':
>>> yes
# 选择 Miniconda 的安装路径
Miniconda3 will now be installed into this location:
>>> ~/.miniconda3
# 添加配置信息到 ~/.bashrc 文件,这样每次打开终端时会自动激活虚拟环境
Do you wish the installer to initialize Miniconda3 by running conda init? [yes|no]
[no] >>> yes

$ source ~/.bashrc # 应用配置文件信息,激活虚拟环境

Conda 作为包管理器

类似于 pip,可以使用 conda install 来安装软件包。部分软件包既可以使用 pip 安装,也可以使用 conda 安装。 相比于 pip,conda 会执行更加严格的依赖检查,并且除了 Python 软件包外,还可以安装一些 C/C++ 软件包,例如 cudatoolkit、mkl 等。相对的,conda 支持的 Python 软件包的数量远少于 PyPI。

Conda 作为环境管理器

类似于 Virtualenv,可以使用 conda 来管理虚拟环境。

常见的使用方式如下:

$ conda create -n venv python=3.11 # 创建一个名为 venv,Python 版本为 3.11 的虚拟环境
# 请确认虚拟环境已经成功创建
$ conda activate venv # 切换到名为 venv 的虚拟环境
$ conda deactivate # 退出当前虚拟环境

导出导入环境

在一些 Python 项目中,你能找到一个 environment.yml 文件。 此文件类似于 requirements.txt,是 Conda 用以描述环境配置的文件。 你可以利用此文件来分享或复制环境,从而运行其他人的项目。

environment.yml 文件不会自动生成。 为了获取当前环境所对应的 environment.yml 文件,你需要使用以下命令:

$ conda env export > environment.yml

此文件会包含当前环境下所有已装包的版本信息以便复现。 如果你只需要导出明确由用户自己安装的包、而不包含这些包的依赖,可以使用 --from-history 选项。

通过 environment.yml 文件,你可以使用以下命令来复现环境:

$ conda env create -f environment.yml

复现出的环境的名字与原环境相同、由 environment.yml 文件的 name 字段传递。 相似的,环境的存放位置由 prefix 字段传递。

由 pip 安装的包

使用 --from-history 选项时,由 pip 安装的包不会被包含在 environment.yml 文件中。 而在不使用此选项的一般情况下,由 pip 安装的包会被记录在 environment.yml 文件中的 pip 列表内,因而可以被复现。

在大部分情况下,我们编译的程序都是动态链接的。动态链接在这里指程序文件还依赖其他库文件,可以使用 ldd 命令确认:

$ cat hello.c
#include <stdio.h>

int main() {
    puts("Hello, world!");
    return 0;
}
$ gcc -o hello hello.c
$ ldd ./hello
    linux-vdso.so.1 (0x00007ffc49703000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f36767d3000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f36769ea000)

这里,我们编译得到程序就依赖于 linux-vdso.so.1libc.so.6/lib64/ld-linux-x86-64.so.2 三个库文件,如果系统中没有这三个库文件,程序就无法执行。

这三个文件的用途

可以看到,即使是 Hello, world 这么简单的程序,也需要外部库文件。

  • linux-vdso.so.1:这个库是为了减小用户程序调用系统调用产生的切换模式(用户态 -> 内核态 -> 用户态)的开销而设计的。这个文件事实上并不存在。在内核加载程序时,这一部分会被自动加载入程序内存中。详情可参考 vdso(7) 文档。
  • libc.so.6:C 运行时库,提供各种 C 函数的实现。
  • ld-linux-x86-64.so.2:动态链接加载器。当程序需要动态链接库中的函数时负责查找并加载对应的函数。

我们在编写程序时,有时需要使用到第三方的库,此时需要加上 -l 参数指定在链接时链接到的库。

$ gcc -o thread thread.c -lpthread  # 编译一个依赖于 pthread 线程库的应用
$ ldd ./thread  # 可以看到多出了 libpthread.so.0 的动态链接依赖
    linux-vdso.so.1 (0x00007ffe6ad93000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fad173c7000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fad171d5000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fad17407000)

对于复杂的应用来说,下载后可能会因为没有动态链接库而无法运行。这一点在打包、分发自己编写的程序时也要特别注意。

$ ldd MegaCli64
    linux-vdso.so.1 (0x00007ffca1868000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa77f6c9000)
    libncurses.so.5 => not found
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa77f6c3000)
    libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fa77f4e1000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa77f392000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fa77f375000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa77f183000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa77f704000)
$ ./MegaCli64  # 缺少 libncurses.so.5,从而无法执行
./MegaCli64: error while loading shared libraries: libncurses.so.5: cannot open shared object file: No such file or directory

而静态链接则将依赖的库全部打包到程序文件中。

$ gcc -o hello-static hello.c -static  # 编译一个静态链接的应用
$ ldd ./hello-static  # 没有动态链接库的依赖
    not a dynamic executable

此时编译得到的程序文件没有额外的依赖,在其他机器上一般也能顺利运行。近年来流行的 Go 语言的一大特点也是静态链接,编译得到的程序有着很好的兼容性。

但是静态链接也存在一些问题。首先是程序大小,比较一下前面编译的 hello-static 和 hello 的大小吧:

$ ls -l hello hello-static
-rwxrwxr-x 1 ustc ustc  17K Feb 28 14:43 hello
-rwxrwxr-x 1 ustc ustc 852K Feb 28 14:39 hello-static

可以看到,动态链接的程序比较小,而静态链接的程序大小接近 1M,况且这还只是一个最简单的 hello world!并且在大小方面,静态链接的程序在运行时无法共享运行库的内存,从而导致内存占用也有一定程度增加。

其次,当运行库出现问题时,用户可以选择更新库,此时所有动态链接的程序都能得到修复,但是静态链接的程序由于不使用系统库,就不得不一个个重新编译。

最后一点是,Linux 的默认 C 库 glibc 对静态链接并不友好,如果程序使用了一部分函数,在静态链接时会受到限制。

$ gcc -o getaddrinfo-example getaddrinfo.c -static
/usr/bin/ld: /tmp/ccPZTyKT.o: in function `main':
getaddrinfo.c:(.text+0xd2): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

Musl libc

近年来,musl libc 开始流行,并且它也是 Alpine Linux 的 C 运行时库。它的特点是轻量、快速、对静态链接友好。我们也可以来试一下:

$ sudo apt install musl-tools  # musl-tools 提供了 musl-gcc 等方便的编译工具
$ musl-gcc -o hello-static-musl hello.c -static  # 静态链接 musl libc
$ ls -lha hello hello-static hello-static-musl
-rwxrwxr-x 1 ustc ustc  17K Feb 28 14:43 hello
-rwxrwxr-x 1 ustc ustc 852K Feb 28 14:39 hello-static
-rwxrwxr-x 1 ustc ustc  26K Feb 28 15:00 hello-static-musl
$ # 可以看到静态链接 musl 得到的文件与动态链接接近,并且远小于静态链接 glibc 得到的文件。
$ musl-gcc -o getaddrinfo-example-musl getaddrinfo.c -static
$ # musl libc 的 getaddrinfo() 实现不依赖于额外的系统组件,所以可以正常静态链接

交叉编译示例

有时候,我们需要为其他的平台编写程序,例如:

  • 我正在使用的电脑是 x86_64 架构的,但是我现在需要给树莓派编写程序(体系结构不同)。
  • 我正在使用 Linux,但是我现在需要编译出一个 Windows 程序(操作系统不同)。

怎么办呢?只能用虚拟化程序运行目标架构,然后在上面跑编译了吗?这样会很麻烦、速度可能会很慢,甚至有的时候不可行(例如性能低下的嵌入式设备,可能连编译器都加载不了)。

这时候就需要交叉编译了。对于常见的架构,Ubuntu/Debian 提供了对应的交叉编译器,很大程度方便了使用。以下将给出交叉编译简单的示例。

在 x86_64 架构编译 aarch64 的程序

Aarch64 是 ARM 指令集的 64 位架构。

$ sudo apt install gcc-aarch64-linux-gnu  # 安装交叉编译到 aarch64 架构的编译器,同时也会安装对应架构的依赖库
$ aarch64-linux-gnu-gcc -o hello-aarch64 hello.c  # 直接编译即可
$ file hello-aarch64  # 看一下文件信息,可以看到是 aarch64 架构的
hello-aarch64: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=09d2ad67b8e2f3b4befe3ce846182743d27910db, for GNU/Linux 3.7.0, not stripped
$ ./hello-aarch64  # 无法直接运行,因为架构不兼容
-bash: ./hello-aarch64: cannot execute binary file: Exec format error
$ sudo apt install qemu-user-static  # 安装 qemu 模拟器
$ qemu-aarch64-static ./hello-aarch64  # 使用 aarch64 模拟器运行程序
/lib/ld-linux-aarch64.so.1: No such file or directory
$ # 为什么仍然无法运行?这是因为 qemu 不知道从哪里找运行时库
$ # 需要补充 QEMU_LD_PREFIX 环境变量
$ QEMU_LD_PREFIX=/usr/aarch64-linux-gnu/ qemu-aarch64-static ./hello-aarch64
Hello, world!

在 Linux 下编译 Windows 程序

这里使用 mingw 来进行交叉编译。

$ sudo apt install gcc-mingw-w64  # 安装 mingw 交叉编译器
$ sudo apt install wine  # 安装 wine Windows 兼容层(默认仅安装 64 位架构支持)
$ x86_64-w64-mingw32-gcc -o hello.exe hello.c  # 编译为 64 位的 Windows 程序
$ file hello.exe  # 确认为 Windows 程序
hello.exe: PE32+ executable (console) x86-64, for MS Windows
$ wine hello.exe  # 使用 wine 运行
it looks like wine32 is missing, you should install it.
as root, please execute "apt-get install wine32"
wine: created the configuration directory '/home/ubuntu/.wine'
(忽略首次配置的输出)
wine: configuration in L"/home/ubuntu/.wine" has been updated.
Hello, world!

MinGW 也可以编译 Windows 下的图形界面应用程序。以下的程序例子来自 Windows Hello World Sample(MIT License),稍作修改以符合 C 语言的语法。

// winhello.c
#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE _, PWSTR pCmdLine, int nCmdShow)
{

    // Register the window class.
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";

    WNDCLASS wc = { };

    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    // Create the window.

    HWND hwnd = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Learn to Program Windows",    // Window text
        WS_OVERLAPPEDWINDOW,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

        NULL,       // Parent window
        NULL,       // Menu
        hInstance,  // Instance handle
        NULL        // Additional application data
        );

    if (hwnd == NULL)
    {
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);

    // Run the message loop.
    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

            // All painting occurs here, between BeginPaint and EndPaint.
            FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
            EndPaint(hwnd, &ps);
        }
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
$ x86_64-w64-mingw32-gcc -o winhello.exe winhello.c
/usr/bin/x86_64-w64-mingw32-ld: /usr/lib/gcc/x86_64-w64-mingw32/9.3-win32/../../../../x86_64-w64-mingw32/lib/libmingw32.a(lib64_libmingw32_a-crt0_c.o): in function `main':
./build/x86_64-w64-mingw32-x86_64-w64-mingw32-crt/./mingw-w64-crt/crt/crt0_c.c:18: undefined reference to `WinMain'
collect2: error: ld returned 1 exit status
$ # 编译失败,这是因为编译 Windows Unicode(UTF-16)程序需要额外的参数 -municode。
$ # 参见 https://sourceforge.net/p/mingw-w64/wiki2/Unicode%20apps/
$ x86_64-w64-mingw32-gcc -o winhello.exe winhello.c -municode
$ wine winhello.exe  # 需要在图形界面下执行,或者复制到 Windows 中执行亦可