2021年10月

Linux Mint 是一个 为初学者定制的流行的 Linux 发行版,同时为用户提供了与 Windows 类似的体验。事实上,它有 一些地方比 Ubuntu 做的更好,这使它成为每一类用户的合适选择。

它是基于 Ubuntu 的,完全由社区所支持。

另一方面,Fedora 是一个尝鲜发行版,它专注于纳入令人兴奋的变化,最终使其成为红帽企业 Linux(RHEL)的一部分。

与 Linux Mint 不同,Fedora 并不完全专注于个人(或非开发者)使用。即使他们提供了一个工作站版本,其目标也是开发者和有经验的 Linux 用户。

Fedora 或 Mint,应该根据什么选择?

虽然我们知道 Fedora 并不是完全面向 Linux 新手的,但许多用户喜欢使用 Fedora 作为他们的日常系统。因此,在这篇文章中,我们将阐明两者之间的一些区别,以帮助你选择一个在你的台式机上使用的操作系统。

系统要求 & 硬件兼容性

在选择任何 Linux 发行版之前,你都应该看一下它的系统要求,并检查硬件兼容性。

在这方面,Linux Mint 和 Fedora 都需要至少 2GB 的内存、20GB 的磁盘空间,以及 1024 x 768 分辨率的显示器来获得入门级的体验。

是的,官方文件可能提到 1GB 内存就可以起步,但要看实际使用情况。除非你有一台复古的电脑,想为一个特定的目的恢复它,否则它就不在考虑范围之内。

Linux Mint 资源使用情况

在技术上,两者都支持现代的和陈旧的硬件,你只有在安装时才会知道软件/驱动是否支持它。除非你有一个特殊的外围设备或具有特殊功能的硬件组件,否则硬件支持可能不是什么大问题。

Linux Mint 19 系列仍然提供了对 32 位系统的支持,你可以使用它到 2023 年 4 月。而 Fedora 已经不支持 32 位系统了。

软件更新周期

Linux Mint 更新管理器

Linux Mint 专注于长期发布版(LTS),支持时间为五年。它的维护方式与 Ubuntu 相同。但没有像 Ubuntu 那样提供付费的扩展支持。

Fedora 不提供 LTS 版本,而是每 6 个月推送一次新的更新,每个版本都会得到 13 个月的软件支持。如果你愿意,你可以跳过一个版本。

如果你只是想安装一个可以使用多年的 Linux 发行版,而不在乎是不是最新的技术/功能,那么 Linux Mint 是个不错的选择。

但是,如果你想要最新的和最伟大的 Linux 技术(在一些罕见的情况下,这也可能破坏你的计算体验),并接受适应 Fedora 推动的重大变化,Fedora 可以是一个选择。

桌面环境的选择

Linux Mint Cinnamon 版

Linux Mint 提供三种不同的 桌面环境:MATE、Cinnamon 和 Xfce。它们有相同的更新周期,并从发布之日起支持 5 年。

尽管 Fedora 不提供 LTS 版本,但你可以通过 Fedora spins 的形式选择各种桌面。你可以得到 KDE、LXQt、MATE、Cinnamon、LXDE,以及一个内置 i3 平铺窗口管理器的版本。

Fedora 34 with GNOME 40

所以,如果你想有更多的选择,Fedora 可以是一个相当令人激动的选择。

软件可用性

Linux Mint 的软件中心和软件包管理器

Linux Mint(或 Ubuntu)的默认软件库提供了大量可以安装的软件,而 Fedora 的默认软件库只坚持提供开源软件。

不仅仅限于此,Linux Mint 还配备了 Synaptic 软件包管理器,这是一个令人印象深刻的安装软件的轻量级工具。

尽管你可以 在 Fedora 中启用第三方软件库,但这又是一个额外的步骤。而且,RPM Fusion 存储库可能没有 Ubuntu 的 universe 存储库那么庞大。

Fedora 34 软件中心

所以,对于 Linux Mint 来说,总的来说,你可以得到更多可安装的软件包和各种安装软件的方法,开箱即用。

使用和安装的便利性

对于一个 Linux 的新嫩用户来说,Ubuntu 或任何基于 Ubuntu 的发行版通常都是一个很好的开端。

Ubuntu 的安装体验,到 安装软件 的简便性,同时还可以选择 LTS 版本,这让初学者觉得很方便。

而且,借助 Ubiquity 安装程序,Linux Mint 自然也有与 Ubuntu 相同的好处,因此,它的学习曲线最小,易于安装,易于使用。

虽然从表面上看 Fedora 并不复杂,但安装选项、软件包管理器以及默认存储库中缺乏的软件可能是一个耗时的因素。

如果你没有尝试过,我建议你试试我们的 VirtualBox 版的 Fedora 安装指南。这是一个测试安装体验的好方法,然后再在你的任何生产系统上进行尝试。

开箱即用的体验

最省事的体验通常是令人愉快的选择。嗯,对大多数人来说。

现在,你需要明白,根据硬件配置的不同,每个用户最终可能会有不同的“开箱即用”体验。

但是,作为参考,让我给你举个 Fedora 和 Linux Mint 的例子。

考虑到我的电脑上使用的是 NVIDIA GPU,我需要安装专有的驱动程序以获得最佳性能。

而且,当我启动 Linux Mint 时,使用“驱动程序管理器”应用程序,安装驱动程序相当容易。

但是,对于 Fedora,即使我按照我们的 在 Fedora 中安装 Nvidia 驱动程序 的指南,我在重启时还是出现了一个错误。

在 Fedora 中安装 NVIDIA 驱动程序

不仅如此,由于某些原因,我的有线网络似乎没有被激活,因此,我没有互联网连接。

是的,当你遇到问题时,你总是可以尝试着去排除故障,但是对于 Linux Mint,我不需要这么做。所以,根据我的经验,我会推荐 Linux Mint,它有更好的开箱体验。

文档

如果你依赖于文档资源并想在这个过程中挑战自己,获得不错的学习经验,我推荐你去看看 Fedora 的文档

你会发现最近和最新的 Fedora 版本的最新信息,这是件好事。

另一方面,Linux Mint 的文档 没有定期更新,但在你想深入挖掘时很有用。

社区支持

你会得到一个良好的社区支持。Linux Mint 的论坛 是一个很基础的平台,容易使用并能解决问题。

Fedora 的论坛 是由 Discourse 驱动的,它是最 流行的现代开源论坛软件 之一。

企业与社区的角度

Fedora 得到了最大的开源公司 红帽 的支持 —— 因此你可以得到良好的持续创新和长期的支持。

然而,正因为 Fedora 并不是为日常电脑用户而建立的,每一个版本的选择都可能完全影响你的用户体验。

另一方面,Linux Mint 完全由一个充满激情的 Linux 社区所支持,专注于使 Linux 在日常使用中更加容易和可靠。当然,它依赖于 Ubuntu 作为基础,但如果社区不喜欢上游的东西,Linux Mint 也会做出大胆的改变。

例如,Linux Mint 与 Ubuntu 官方发行版不同,默认情况下禁用了 snap。所以,如果你想使用它们,你就必须 在 Linux Mint 中启用 snap

总结

如果你想为你的家用电脑选择一个没有问题的、易于使用的操作系统,我的建议是 Linux Mint。但是,如果你想体验最新的、最伟大的 Linux 操作系统,同时在你的 Linux 学习经历中进行一次小小的冒险,Fedora 可以是一个不错的选择。

虽然每个操作系统都需要某种形式的故障排除,没有什么能保证你的硬件完全不出问题,但我认为 Linux Mint 对大多数用户来说可能没有问题。

在任何情况下,你可以重新审视上面提到的比较点,看看什么对你的电脑最重要。

你怎么看?你会选择 Fedora 而不是 Mint 吗?还有,为什么?请在下面的评论中告诉我。


via: https://itsfoss.com/linux-mint-vs-fedora/

作者:Ankush Das 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linus Torvalds 在采访中再次提及一些观点

Linux 创始人参加了今年的北美开源峰会,并按惯例接受了采访,就一些经常被问到的问题发表了一些并不算新的观点:

  • 在关于使用 Rust 编写 Linux 内核模块的讨论中,他说,从技术角度有没有意义不重要,重要的是要有趣。
  • 他表示真的很喜欢 C 语言,它是一种伟大的语言,当然,他也承认 C 语言存在容易被所有人忽视的陷阱,并认为 Rust 可能真的是一种解决方案。
  • 关于社区,他说,社区使 Linux 保持了活力,要不是社区,Linux 可能早被他扔在脑后了。
老王点评:这些观点我们都知道了,但是我们还是一次次不厌其烦的愿意听到 Linus 讲它们。这就是精神领袖。

上百万人遇到 Let's Encrypt 根证书到期问题

Let's Encrypt 之前使用的根证书于 9 月 30 日到期,可能受到证书过期影响的设备是那些不定期更新的设备。它早在 5 月就通知大家这一消息,并提供替代方案和变通办法,以确保设备在转换期间不受影响。周四晚上,至少有 200 万人在他们的手机、电脑或智能小工具上遇到了由于证书问题导致的一些互联网连接问题。数十种主要科技产品和服务受到证书过期的严重影响,如亚马逊、谷歌和微软的云计算服务;思科的 IT 和云安全服务;Shopify 的卖家无法登录;以及一些游戏和工作流平台等等。

老王点评:Let's Encrypt 几乎是以一己之力推动了 HTTPS 的普及,但是由于历史原因,其根证书的有效期并不很长,所以 Let's Encrypt 越成功,这种遗留问题就越大。

亚马逊、谷歌、微软和 IBM 等签署“可信云原则”

这些技术巨头联合起来建立了“可信云原则”,声称这是对保护客户权利的承诺。具体主要包括:除“特殊情况”外,政府应首先直接向企业客户而非云供应商索取数据;当政府试图直接从云服务供应商处获取客户数据时,客户应有权获得通知;云供应商应有明确的程序来质疑政府对客户数据的访问请求,包括通知相关数据保护机构,以保护客户的利益。此外,该原则还提出,政府应建立机制,提出并解决彼此之间的冲突,使云服务提供商在一国的合法合规性不等于在另一国的违法行为;政府应支持跨境数据流动。他们承诺支持允许政府通过遵守人权标准的透明程序索取数据的法律,支持在国家和国际层面改进规则和条例,以保护云客户的安全、隐私和安保以及他们对数据的所有权。

老王点评:这种业界共识需要更多方的参与,目前看起来想落地没那么多容易。

GitHub Actions 是一项为快速建立持续集成和交付(CI/CD)工作流程而提供的服务。这些工作流程在被称为“ 运行器 runner ”的主机上运行。GitHub 提供的 托管运行器 的操作系统的选择是有限的(Windows Server、Ubuntu、MacOS)。

另一个选择是使用 自托管 的运行器,这让仓库管理员对运行器有更多控制。自托管的运行程序是专门为某个存储库或组织服务的。下面的文章介绍了使用 Fedora CoreOS 配置自托管运行程序的步骤。

入门

Fedora CoreOS 是一个精简的操作系统,旨在便于大规模的部署和维护。该操作系统会自动更新,并默认提供运行容器所需的工具。由于这些原因,Fedora CoreOS 是运行 CI/CD 工作流程的一个极佳选择。

配置和配备 Fedora CoreOS 机器的第一步是生成一个 Ignition 文件。Butane 允许你使用更友好的格式(YAML)生成 Ignition 文件。

配置一个 Fedora CoreOS 运行器

要在 Fedora CoreOS 上执行 GitHub Actions,托管主机需要用于注册和运行该运行器的二进制文件和脚本。从 Actions 运行器项目 下载二进制文件和脚本,并部署在 /usr/local/sbin/actions-runner 下。

version: "1.3.0"
variant: fcos
storage:
  directories:
    - path: /usr/local/sbin/actions-runner
      mode: 0755
      user:
        name: core
      group:
        name: core
  files:
    - path: /usr/local/sbin/actions-runner/actions-runner-linux.tar.gz
      overwrite: true
      contents:
        source: https://github.com/actions/runner/releases/download/v2.278.0/actions-runner-linux-x64-2.278.0.tar.gz
      mode: 0755
      user:
        name: core
      group:
        name: core

注册和删除令牌

为一个项目配置运行器需要一个“ 令牌 token ”。这可以防止在没有正确权限的情况下从项目中注册或删除自托管的运行器。GitHub 提供的令牌有一个小时的过期时间。如果运行器在这个时间之后重新启动,它将需要一个新的注册令牌。

该令牌可能出问题,特别是在 Fedora CoreOS 自动更新时。更新过程希望托管主机在收到新数据后至少每隔几周重启一次。

幸运的是,可以使用 GitHub REST API 来获取这些令牌,并在托管主机每次重启时自动配置运行器。下面的 manage-runner.sh 脚本使用 API 来获取令牌,删除任何已经配置好的运行器,并用新的令牌注册运行器。

#!/bin/bash
# Handles the Github Action runner configuration.
# Remove and Registration token expires after 1 hour, if we want our runner
# to work after a reboot (auto update) we need to refresh the tokens.

# First remove the runner with a fresh remove token
REMOVE_TOKEN=$(curl -u ${GITHUB_USER}:${GITHUB_TOKEN} -X POST -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/actions/runners/remove-token | jq -r '.token')
/usr/local/sbin/actions-runner/config.sh remove --token ${REMOVE_TOKEN}


# Then register the runner with a fresh registration token
REGISTRATION_TOKEN=$(curl -u ${GITHUB_USER}:${GITHUB_TOKEN} -X POST -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/actions/runners/registration-token | jq -r '.token')
/usr/local/sbin/actions-runner/config.sh --url https://github.com/cverna/fcos-actions-runner --token ${REGISTRATION_TOKEN} --labels fcos --unattended

上面的脚本使用了一些环境变量,包含 GitHub 用户名和用于验证 REST API 请求的 个人访问令牌 Personal Access Token 。个人访问令牌需要存储库权限,以便成功检索运行器的注册和移除令牌。该令牌是安全敏感信息,所以最好将其存储在一个具有更严格权限的不同文件中。在这个例子中,这个文件是 actions-runner

GITHUB_USER=<user>
GITHUB_REPO=<repo>
GITHUB_TOKEN=<personal_access_token>

以下是创建这两个文件 manage-runner.shactions-runner 的 Butane 片段。

- path: /usr/local/sbin/actions-runner/manage-runner.sh
      contents:
        local: manage-runner.sh
      mode: 0755
      user:
        name: core
      group:
        name: core
    - path: /etc/actions-runner
      contents:
        local: actions-runner
      mode: 0700
      user:
        name: core
      group:
        name: core

在 Fedora CoreOS 上运行 Actions

最后,创建用于配置和启动运行器的 systemd 服务。在 Butane 配置文件中定义这些服务。

systemd:
  units:
    - name: github-runner-configure.service
      enabled: true
      contents: |
        [Unit]
        Description=Configure the github action runner for a repository
        After=network-online.target boot-complete.target
        Requires=boot-complete.target
        [Service]
        EnvironmentFile=/etc/actions-runner
        Type=oneshot
        RemainAfterExit=yes
        User=core
        WorkingDirectory=/usr/local/sbin/actions-runner
        ExecStartPre=tar xvf actions-runner-linux.tar.gz --no-same-owner
        ExecStart=/usr/local/sbin/actions-runner/manage-runner.sh
        [Install]
        WantedBy=multi-user.target
    - name: github-runner.service
      enabled: true
      contents: |
        [Unit]
        Description=Run the github action runner
        After=github-runner-configure.service
        [Service]
        WorkingDirectory=/usr/local/sbin/actions-runner
        User=core
        ExecStart=/usr/local/sbin/actions-runner/run.sh
        [Install]
        WantedBy=multi-user.target

这将创建两个服务:github-runner-configure.service(在主机启动完成后运行一次)和 github-runner.service(运行 Actions 运行器二进制文件并等待新的 CI/CD 作业)。

现在 Butane 配置已经完成,从中生成一个 Ignition 文件并配备一个 Fedora CoreOS Actions 运行器。

$ podman run -i --rm -v $PWD:/code:z --workdir /code quay.io/coreos/butane:release --pretty --strict --files-dir /code config.yaml -o config.ignition

一旦 Ignition 文件生成,它就可以用来在 支持 Fedora CoreOS 的平台上配备一个运行器。

配置一个 Action 来使用一个自托管的运行器

下面的测试 Action 工作流程将测试 FCOS 的自托管的运行器。在你的 git 存储库中创建以下文件 .github/workflows/main.yml

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run.
on:
  # Triggers the workflow on push or pull request events but only for the main branch
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: fcos

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Runs a single command using the runners shell
      - name: Run a one-line script
        run: podman run --rm fedora-minimal:34 echo Hello World !

请注意,runs-on 的配置被设置为使用标签为 fcos 的运行器。

本文介绍的代码可以在 这里 中找到。


via: https://fedoramagazine.org/run-github-actions-on-fedora-coreos/

作者:Clément Verna 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解有关内存安全和效率的更多信息。

 title=

C 是一种高级语言,同时具有“ 接近金属 close-to-the-metal ”(LCTT 译注:即“接近人类思维方式”的反义词)的特性,这使得它有时看起来更像是一种可移植的汇编语言,而不像 Java 或 Python 这样的兄弟语言。内存管理作为上述特性之一,涵盖了正在执行的程序对内存的安全和高效使用。本文通过 C 语言代码示例,以及现代 C 语言编译器生成的汇编语言代码段,详细介绍了内存安全性和效率。

尽管代码示例是用 C 语言编写的,但安全高效的内存管理指南对于 C++ 是同样适用的。这两种语言在很多细节上有所不同(例如,C++ 具有 C 所缺乏的面向对象特性和泛型),但在内存管理方面面临的挑战是一样的。

执行中程序的内存概述

对于正在执行的程序(又名 进程 process ),内存被划分为三个区域: stack heap 静态区 static area 。下文会给出每个区域的概述,以及完整的代码示例。

作为通用 CPU 寄存器的替补, 为代码块(例如函数或循环体)中的局部变量提供暂存器存储。传递给函数的参数在此上下文中也视作局部变量。看一下下面这个简短的示例:

void some_func(int a, int b) {
   int n;
   ...
}

通过 ab 传递的参数以及局部变量 n 的存储会在栈中,除非编译器可以找到通用寄存器。编译器倾向于优先将通用寄存器用作暂存器,因为 CPU 对这些寄存器的访问速度很快(一个时钟周期)。然而,这些寄存器在台式机、笔记本电脑和手持机器的标准架构上很少(大约 16 个)。

在只有汇编语言程序员才能看到的实施层面,栈被组织为具有 push(插入)和 pop(删除)操作的 LIFO(后进先出)列表。 top 指针可以作为偏移的基地址;这样,除了 top 之外的栈位置也变得可访问了。例如,表达式 top+16 指向堆栈的 top 指针上方 16 个字节的位置,表达式 top-16 指向 top 指针下方 16 个字节的位置。因此,可以通过 top 指针访问实现了暂存器存储的栈的位置。在标准的 ARM 或 Intel 架构中,栈从高内存地址增长到低内存地址;因此,减小某进程的 top 就是增大其栈规模。

使用栈结构就意味着轻松高效地使用内存。编译器(而非程序员)会编写管理栈的代码,管理过程通过分配和释放所需的暂存器存储来实现;程序员声明函数参数和局部变量,将实现过程交给编译器。此外,完全相同的栈存储可以在连续的函数调用和代码块(如循环)中重复使用。精心设计的模块化代码会将栈存储作为暂存器的首选内存选项,同时优化编译器要尽可能使用通用寄存器而不是栈。

提供的存储是通过程序员代码显式分配的,堆分配的语法因语言而异。在 C 中,成功调用库函数 malloc(或其变体 calloc 等)会分配指定数量的字节(在 C++ 和 Java 等语言中,new 运算符具有相同的用途)。编程语言在如何释放堆分配的存储方面有着巨大的差异:

  • 在 Java、Go、Lisp 和 Python 等语言中,程序员不会显式释放动态分配的堆存储。

例如,下面这个 Java 语句为一个字符串分配了堆存储,并将这个堆存储的地址存储在变量 greeting 中:

String greeting = new String("Hello, world!");

Java 有一个垃圾回收器,它是一个运行时实用程序,如果进程无法再访问自己分配的堆存储,回收器可以使其自动释放。因此,Java 堆释放是通过垃圾收集器自动进行的。在上面的示例中,垃圾收集器将在变量 greeting 超出作用域后,释放字符串的堆存储。

  • Rust 编译器会编写堆释放代码。这是 Rust 在不依赖垃圾回收器的情况下,使堆释放实现自动化的开创性努力,但这也会带来运行时复杂性和开销。向 Rust 的努力致敬!
  • 在 C(和 C++)中,堆释放是程序员的任务。程序员调用 malloc 分配堆存储,然后负责相应地调用库函数 free 来释放该存储空间(在 C++ 中,new 运算符分配堆存储,而 deletedelete[] 运算符释放此类存储)。下面是一个 C 语言代码示例:
char* greeting = malloc(14);       /* 14 heap bytes */
strcpy(greeting, "Hello, world!"); /* copy greeting into bytes */
puts(greeting);                    /* print greeting */
free(greeting);                    /* free malloced bytes */

C 语言避免了垃圾回收器的成本和复杂性,但也不过是让程序员承担了堆释放的任务。

内存的 静态区 为可执行代码(例如 C 语言函数)、字符串文字(例如“Hello, world!”)和全局变量提供存储空间:

int n;                       /* global variable */
int main() {                 /* function */
   char* msg = "No comment"; /* string literal */
   ...
}

该区域是静态的,因为它的大小从进程执行开始到结束都固定不变。由于静态区相当于进程固定大小的内存占用,因此经验法则是通过避免使用全局数组等方法来使该区域尽可能小。

下文会结合代码示例对本节概述展开进一步讲解。

栈存储

想象一个有各种连续执行的任务的程序,任务包括了处理每隔几分钟通过网络下载并存储在本地文件中的数字数据。下面的 stack 程序简化了处理流程(仅是将奇数整数值转换为偶数),而将重点放在栈存储的好处上。

#include <stdio.h>
#include <stdlib.h>

#define Infile   "incoming.dat"
#define Outfile  "outgoing.dat"
#define IntCount 128000  /* 128,000 */

void other_task1() { /*...*/ }
void other_task2() { /*...*/ }

void process_data(const char* infile,
          const char* outfile,
          const unsigned n) {
  int nums[n];
  FILE* input = fopen(infile, "r");
  if (NULL == infile) return;
  FILE* output = fopen(outfile, "w");
  if (NULL == output) {
    fclose(input);
    return;
  }

  fread(nums, n, sizeof(int), input); /* read input data */
  unsigned i;
  for (i = 0; i < n; i++) {
    if (1 == (nums[i] & 0x1))  /* odd parity? */
      nums[i]--;               /* make even */
  }
  fclose(input);               /* close input file */

  fwrite(nums, n, sizeof(int), output);
  fclose(output);
}

int main() {
  process_data(Infile, Outfile, IntCount);
  
  /** now perform other tasks **/
  other_task1(); /* automatically released stack storage available */
  other_task2(); /* ditto */
  
  return 0;
}

底部的 main 函数首先调用 process_data 函数,该函数会创建一个基于栈的数组,其大小由参数 n 给定(当前示例中为 128,000)。因此,该数组占用 128000 * sizeof(int) 个字节,在标准设备上达到了 512,000 字节(int 在这些设备上是四个字节)。然后数据会被读入数组(使用库函数 fread),循环处理,并保存到本地文件 outgoing.dat(使用库函数 fwrite)。

process_data 函数返回到其调用者 main 函数时,process_data 函数的大约 500MB 栈暂存器可供 stack 程序中的其他函数用作暂存器。在此示例中,main 函数接下来调用存根函数 other_task1other_task2。这三个函数在 main 中依次调用,这意味着所有三个函数都可以使用相同的堆栈存储作为暂存器。因为编写栈管理代码的是编译器而不是程序员,所以这种方法对程序员来说既高效又容易。

在 C 语言中,在块(例如函数或循环体)内定义的任何变量默认都有一个 auto 存储类,这意味着该变量是基于栈的。存储类 register 现在已经过时了,因为 C 编译器会主动尝试尽可能使用 CPU 寄存器。只有在块内定义的变量可能是 register,如果没有可用的 CPU 寄存器,编译器会将其更改为 auto。基于栈的编程可能是不错的首选方式,但这种风格确实有一些挑战性。下面的 badStack 程序说明了这点。

#include <stdio.h>;

const int* get_array(const unsigned n) {
  int arr[n]; /* stack-based array */
  unsigned i;
  for (i = 0; i < n; i++) arr[i] = 1 + 1;

  return arr;  /** ERROR **/
}

int main() {
  const unsigned n = 16;
  const int* ptr = get_array(n);
  
  unsigned i;
  for (i = 0; i < n; i++) printf("%i ", ptr[i]);
  puts("\n");

  return 0;
}

badStack 程序中的控制流程很简单。main 函数使用 16(LCTT 译注:原文为 128,应为作者笔误)作为参数调用函数 get_array,然后被调用函数会使用传入参数来创建对应大小的本地数组。get_array 函数会初始化数组并返回给 main 中的数组标识符 arrarr 是一个指针常量,保存数组的第一个 int 元素的地址。

当然,本地数组 arr 可以在 get_array 函数中访问,但是一旦 get_array 返回,就不能合法访问该数组。尽管如此,main 函数会尝试使用函数 get_array 返回的堆栈地址 arr 来打印基于栈的数组。现代编译器会警告错误。例如,下面是来自 GNU 编译器的警告:

badStack.c: In function 'get_array':
badStack.c:9:10: warning: function returns address of local variable [-Wreturn-local-addr]
return arr;  /** ERROR **/

一般规则是,如果使用栈存储实现局部变量,应该仅在该变量所在的代码块内,访问这块基于栈的存储(在本例中,数组指针 arr 和循环计数器 i 均为这样的局部变量)。因此,函数永远不应该返回指向基于栈存储的指针。

堆存储

接下来使用若干代码示例凸显在 C 语言中使用堆存储的优点。在第一个示例中,使用了最优方案分配、使用和释放堆存储。第二个示例(在下一节中)将堆存储嵌套在了其他堆存储中,这会使其释放操作变得复杂。

#include <stdio.h>
#include <stdlib.h>

int* get_heap_array(unsigned n) {
  int* heap_nums = malloc(sizeof(int) * n); 
  
  unsigned i;
  for (i = 0; i < n; i++)
    heap_nums[i] = i + 1;  /* initialize the array */
  
  /* stack storage for variables heap_nums and i released
     automatically when get_num_array returns */
  return heap_nums; /* return (copy of) the pointer */
}

int main() {
  unsigned n = 100, i;
  int* heap_nums = get_heap_array(n); /* save returned address */
  
  if (NULL == heap_nums) /* malloc failed */
    fprintf(stderr, "%s\n", "malloc(...) failed...");
  else {
    for (i = 0; i < n; i++) printf("%i\n", heap_nums[i]);
    free(heap_nums); /* free the heap storage */
  }
  return 0; 
}

上面的 heap 程序有两个函数: main 函数使用参数(示例中为 100)调用 get_heap_array 函数,参数用来指定数组应该有多少个 int 元素。因为堆分配可能会失败,main 函数会检查 get_heap_array 是否返回了 NULL;如果是,则表示失败。如果分配成功,main 将打印数组中的 int 值,然后立即调用库函数 free 来对堆存储解除分配。这就是最优的方案。

get_heap_array 函数以下列语句开头,该语句值得仔细研究一下:

int* heap_nums = malloc(sizeof(int) * n); /* heap allocation */

malloc 库函数及其变体函数针对字节进行操作;因此,malloc 的参数是 nint 类型元素所需的字节数(sizeof(int) 在标准现代设备上是四个字节)。malloc 函数返回所分配字节段的首地址,如果失败则返回 NULL .

如果成功调用 malloc,在现代台式机上其返回的地址大小为 64 位。在手持设备和早些时候的台式机上,该地址的大小可能是 32 位,或者甚至更小,具体取决于其年代。堆分配数组中的元素是 int 类型,这是一个四字节的有符号整数。这些堆分配的 int 的地址存储在基于栈的局部变量 heap_nums 中。可以参考下图:

                 heap-based
 stack-based        /
     \        +----+----+   +----+
 heap-nums--->|int1|int2|...|intN|
              +----+----+   +----+

一旦 get_heap_array 函数返回,指针变量 heap_nums 的栈存储将自动回收——但动态 int 数组的堆存储仍然存在,这就是 get_heap_array 函数返回这个地址(的副本)给 main 函数的原因:它现在负责在打印数组的整数后,通过调用库函数 free 显式释放堆存储:

free(heap_nums); /* free the heap storage */

malloc 函数不会初始化堆分配的存储空间,因此里面是随机值。相比之下,其变体函数 calloc 会将分配的存储初始化为零。这两个函数都返回 NULL 来表示分配失败。

heap 示例中,main 函数在调用 free 后会立即返回,正在执行的程序会终止,这会让系统回收所有已分配的堆存储。尽管如此,程序员应该养成在不再需要时立即显式释放堆存储的习惯。

嵌套堆分配

下一个代码示例会更棘手一些。C 语言有很多返回指向堆存储的指针的库函数。下面是一个常见的使用情景:

1、C 程序调用一个库函数,该函数返回一个指向基于堆的存储的指针,而指向的存储通常是一个聚合体,如数组或结构体:

SomeStructure* ptr = lib_function(); /* returns pointer to heap storage */

2、 然后程序使用所分配的存储。

3、 对于清理而言,问题是对 free 的简单调用是否会清理库函数分配的所有堆分配存储。例如,SomeStructure 实例可能有指向堆分配存储的字段。一个特别麻烦的情况是动态分配的结构体数组,每个结构体有一个指向又一层动态分配的存储的字段。下面的代码示例说明了这个问题,并重点关注了如何设计一个可以安全地为客户端提供堆分配存储的库。

#include <stdio.h>
#include <stdlib.h>

typedef struct {
  unsigned id;
  unsigned len;
  float*   heap_nums;
} HeapStruct;
unsigned structId = 1;

HeapStruct* get_heap_struct(unsigned n) {
  /* Try to allocate a HeapStruct. */
  HeapStruct* heap_struct = malloc(sizeof(HeapStruct));
  if (NULL == heap_struct) /* failure? */
    return NULL;           /* if so, return NULL */

  /* Try to allocate floating-point aggregate within HeapStruct. */
  heap_struct->heap_nums = malloc(sizeof(float) * n);
  if (NULL == heap_struct->heap_nums) {  /* failure? */
    free(heap_struct);                   /* if so, first free the HeapStruct */
    return NULL;                         /* then return NULL */
  }

  /* Success: set fields */
  heap_struct->id = structId++;
  heap_struct->len = n;

  return heap_struct; /* return pointer to allocated HeapStruct */
}

void free_all(HeapStruct* heap_struct) {
  if (NULL == heap_struct) /* NULL pointer? */
    return;                /* if so, do nothing */
  
  free(heap_struct->heap_nums); /* first free encapsulated aggregate */
  free(heap_struct);            /* then free containing structure */  
}

int main() {
  const unsigned n = 100;
  HeapStruct* hs = get_heap_struct(n); /* get structure with N floats */

  /* Do some (meaningless) work for demo. */
  unsigned i;
  for (i = 0; i < n; i++) hs->heap_nums[i] = 3.14 + (float) i;
  for (i = 0; i < n; i += 10) printf("%12f\n", hs->heap_nums[i]);

  free_all(hs); /* free dynamically allocated storage */
  
  return 0;
}

上面的 nestedHeap 程序示例以结构体 HeapStruct 为中心,结构体中又有名为 heap_nums 的指针字段:

typedef struct {
  unsigned id;
  unsigned len;
  float*   heap_nums; /** pointer **/
} HeapStruct;

函数 get_heap_struct 尝试为 HeapStruct 实例分配堆存储,这需要为字段 heap_nums 指向的若干个 float 变量分配堆存储。如果成功调用 get_heap_struct 函数,并将指向堆分配结构体的指针以 hs 命名,其结果可以描述如下:

hs-->HeapStruct instance
        id
        len
        heap_nums-->N contiguous float elements

get_heap_struct 函数中,第一个堆分配过程很简单:

HeapStruct* heap_struct = malloc(sizeof(HeapStruct));
if (NULL == heap_struct) /* failure? */
  return NULL;           /* if so, return NULL */

sizeof(HeapStruct) 包括了 heap_nums 字段的字节数(32 位机器上为 4,64 位机器上为 8),heap_nums 字段则是指向动态分配数组中的 float 元素的指针。那么,问题关键在于 malloc 为这个结构体传送了字节空间还是表示失败的 NULL;如果是 NULLget_heap_struct 函数就也返回 NULL 以通知调用者堆分配失败。

第二步尝试堆分配的过程更复杂,因为在这一步,HeapStruct 的堆存储已经分配好了:

heap_struct->heap_nums = malloc(sizeof(float) * n);
if (NULL == heap_struct->heap_nums) {  /* failure? */
  free(heap_struct);                   /* if so, first free the HeapStruct */
  return NULL;                         /* and then return NULL */
}

传递给 get_heap_struct 函数的参数 n 指明动态分配的 heap_nums 数组中应该有多少个 float 元素。如果可以分配所需的若干个 float 元素,则该函数在返回 HeapStruct 的堆地址之前会设置结构的 idlen 字段。 但是,如果尝试分配失败,则需要两个步骤来实现最优方案:

1、 必须释放 HeapStruct 的存储以避免内存泄漏。对于调用 get_heap_struct 的客户端函数而言,没有动态 heap_nums 数组的 HeapStruct 可能就是没用的;因此,HeapStruct 实例的字节空间应该显式释放,以便系统可以回收这些空间用于未来的堆分配。

2、 返回 NULL 以标识失败。

如果成功调用 get_heap_struct 函数,那么释放堆存储也很棘手,因为它涉及要以正确顺序进行的两次 free 操作。因此,该程序设计了一个 free_all 函数,而不是要求程序员再去手动实现两步释放操作。回顾一下,free_all 函数是这样的:

void free_all(HeapStruct* heap_struct) {
  if (NULL == heap_struct) /* NULL pointer? */
    return;                /* if so, do nothing */
  
  free(heap_struct->heap_nums); /* first free encapsulated aggregate */
  free(heap_struct);            /* then free containing structure */  
}

检查完参数 heap_struct 不是 NULL 值后,函数首先释放 heap_nums 数组,这步要求 heap_struct 指针此时仍然是有效的。先释放 heap_struct 的做法是错误的。一旦 heap_nums 被释放,heap_struct 就可以释放了。如果 heap_struct 被释放,但 heap_nums 没有被释放,那么数组中的 float 元素就会泄漏:仍然分配了字节空间,但无法被访问到——因此一定要记得释放 heap_nums。存储泄漏将一直持续,直到 nestedHeap 程序退出,系统回收泄漏的字节时为止。

关于 free 库函数的注意事项就是要有顺序。回想一下上面的调用示例:

free(heap_struct->heap_nums); /* first free encapsulated aggregate */
free(heap_struct);            /* then free containing structure */

这些调用释放了分配的存储空间——但它们并 不是 将它们的操作参数设置为 NULLfree 函数会获取地址的副本作为参数;因此,将副本更改为 NULL 并不会改变原地址上的参数值)。例如,在成功调用 free 之后,指针 heap_struct 仍然持有一些堆分配字节的堆地址,但是现在使用这个地址将会产生错误,因为对 free 的调用使得系统有权回收然后重用这些分配过的字节。

使用 NULL 参数调用 free 没有意义,但也没有什么坏处。而在非 NULL 的地址上重复调用 free 会导致不确定结果的错误:

free(heap_struct);  /* 1st call: ok */
free(heap_struct);  /* 2nd call: ERROR */

内存泄漏和堆碎片化

“内存泄漏”是指动态分配的堆存储变得不再可访问。看一下相关的代码段:

float* nums = malloc(sizeof(float) * 10); /* 10 floats */
nums[0] = 3.14f;                          /* and so on */
nums = malloc(sizeof(float) * 25);        /* 25 new floats */

假如第一个 malloc 成功,第二个 malloc 会再将 nums 指针重置为 NULL(分配失败情况下)或是新分配的 25 个 float 中第一个的地址。最初分配的 10 个 float 元素的堆存储仍然处于被分配状态,但此时已无法再对其访问,因为 nums 指针要么指向别处,要么是 NULL。结果就是造成了 40 个字节(sizeof(float) * 10)的泄漏。

在第二次调用 malloc 之前,应该释放最初分配的存储空间:

float* nums = malloc(sizeof(float) * 10); /* 10 floats */
nums[0] = 3.14f;                          /* and so on */
free(nums);                               /** good **/
nums = malloc(sizeof(float) * 25);        /* no leakage */

即使没有泄漏,堆也会随着时间的推移而碎片化,需要对系统进行碎片整理。例如,假设两个最大的堆块当前的大小分别为 200MB 和 100MB。然而,这两个堆块并不连续,进程 P 此时又需要分配 250MB 的连续堆存储。在进行分配之前,系统可能要对堆进行 碎片整理 以给 P 提供 250MB 连续存储空间。碎片整理很复杂,因此也很耗时。

内存泄漏会创建处于已分配状态但不可访问的堆块,从而会加速碎片化。因此,释放不再需要的堆存储是程序员帮助减少碎片整理需求的一种方式。

诊断内存泄漏的工具

有很多工具可用于分析内存效率和安全性,其中我最喜欢的是 valgrind。为了说明该工具如何处理内存泄漏,这里给出 leaky 示例程序:

#include <stdio.h>
#include <stdlib.h>

int* get_ints(unsigned n) {
  int* ptr = malloc(n * sizeof(int));
  if (ptr != NULL) {
    unsigned i;
    for (i = 0; i < n; i++) ptr[i] = i + 1;
  }
  return ptr;
}

void print_ints(int* ptr, unsigned n) {
  unsigned i;
  for (i = 0; i < n; i++) printf("%3i\n", ptr[i]);
}

int main() {
  const unsigned n = 32;
  int* arr = get_ints(n);
  if (arr != NULL) print_ints(arr, n);

  /** heap storage not yet freed... **/
  return 0;
}

main 函数调用了 get_ints 函数,后者会试着从堆中 malloc 32 个 4 字节的 int,然后初始化动态数组(如果 malloc 成功)。初始化成功后,main 函数会调用 print_ints函数。程序中并没有调用 free 来对应 malloc 操作;因此,内存泄漏了。

如果安装了 valgrind 工具箱,下面的命令会检查 leaky 程序是否存在内存泄漏(% 是命令行提示符):

% valgrind --leak-check=full ./leaky

绝大部分输出都在下面给出了。左边的数字 207683 是正在执行的 leaky 程序的进程标识符。这份报告给出了泄漏发生位置的详细信息,本例中位置是在 main 函数所调用的 get_ints 函数中对 malloc 的调用处。

==207683== HEAP SUMMARY:
==207683==   in use at exit: 128 bytes in 1 blocks
==207683==   total heap usage: 2 allocs, 1 frees, 1,152 bytes allocated
==207683== 
==207683== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==207683==   at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==207683==   by 0x109186: get_ints (in /home/marty/gc/leaky)
==207683==   by 0x109236: main (in /home/marty/gc/leaky)
==207683== 
==207683== LEAK SUMMARY:
==207683==   definitely lost: 128 bytes in 1 blocks
==207683==   indirectly lost: 0 bytes in 0 blocks
==207683==   possibly lost: 0 bytes in 0 blocks
==207683==   still reachable: 0 bytes in 0 blocks
==207683==   suppressed: 0 bytes in 0 blocks

如果把 main 函数改成在对 print_ints 的调用之后,再加上一个对 free 的调用,valgrind 就会对 leaky 程序给出一个干净的内存健康清单:

==218462== All heap blocks were freed -- no leaks are possible

静态区存储

在正统的 C 语言中,函数必须在所有块之外定义。这是一些 C 编译器支持的特性,杜绝了在另一个函数体内定义一个函数的可能。我举的例子都是在所有块之外定义的函数。这样的函数要么是 static ,即静态的,要么是 extern,即外部的,其中 extern 是默认值。

C 语言中,以 staticextern 修饰的函数和变量驻留在内存中所谓的 静态区 中,因为在程序执行期间该区域大小是固定不变的。这两个存储类型的语法非常复杂,我们应该回顾一下。在回顾之后,会有一个完整的代码示例来生动展示语法细节。在所有块之外定义的函数或变量默认为 extern;因此,函数和变量要想存储类型为 static ,必须显式指定:

/** file1.c: outside all blocks, five definitions  **/
int foo(int n) { return n * 2; }     /* extern by default */
static int bar(int n) { return n; }  /* static */
extern int baz(int n) { return -n; } /* explicitly extern */

int num1;        /* extern */
static int num2; /* static */

externstatic 的区别在于作用域:extern 修饰的函数或变量可以实现跨文件可见(需要声明)。相比之下,static 修饰的函数仅在 定义 该函数的文件中可见,而 static 修饰的变量仅在 定义 该变量的文件(或文件中的块)中可见:

static int n1;    /* scope is the file */
void func() {
   static int n2; /* scope is func's body */
   ...
}

如果在所有块之外定义了 static 变量,例如上面的 n1,该变量的作用域就是定义变量的文件。无论在何处定义 static 变量,变量的存储都在内存的静态区中。

extern 函数或变量在给定文件中的所有块之外定义,但这样定义的函数或变量也可以在其他文件中声明。典型的做法是在头文件中 声明 这样的函数或变量,只要需要就可以包含进来。下面这些简短的例子阐述了这些棘手的问题。

假设 extern 函数 foofile1.c定义,有无关键字 extern 效果都一样:

/** file1.c **/
int foo(int n) { return n * 2; } /* definition has a body {...} */

必须在其他文件(或其中的块)中使用显式的 extern 声明 此函数才能使其可见。以下是使 extern 函数 foo 在文件 file2.c 中可见的声明语句:

/** file2.c: make function foo visible here **/
extern int foo(int); /* declaration (no body) */

回想一下,函数声明没有用大括号括起来的主体,而函数定义会有这样的主体。

为了便于查看,函数和变量声明通常会放在头文件中。准备好需要声明的源代码文件,然后就可以 #include 相关的头文件。下一节中的 staticProg 程序演示了这种方法。

至于 extern 的变量,规则就变得更棘手了(很抱歉增加了难度!)。任何 extern 的对象——无论函数或变量——必须 定义 在所有块之外。此外,在所有块之外定义的变量默认为 extern

/** outside all blocks **/
int n; /* defaults to extern */

但是,只有在变量的 定义 中显式初始化变量时,extern 才能在变量的 定义 中显式修饰(LCTT 译注:换言之,如果下列代码中的 int n1; 行前加上 extern,该行就由 定义 变成了 声明):

/** file1.c: outside all blocks **/
int n1;             /* defaults to extern, initialized by compiler to zero */
extern int n2 = -1; /* ok, initialized explicitly */
int n3 = 9876;      /* ok, extern by default and initialized explicitly */

要使在 file1.c 中定义为 extern 的变量在另一个文件(例如 file2.c)中可见,该变量必须在 file2.c 中显式 声明extern 并且不能初始化(初始化会将声明转换为定义):

/** file2.c **/
extern int n1; /* declaration of n1 defined in file1.c */

为了避免与 extern 变量混淆,经验是在 声明 中显式使用 extern(必须),但不要在 定义 中使用(非必须且棘手)。对于函数,extern 在定义中是可选使用的,但在声明中是必须使用的。下一节中的 staticProg 示例会把这些点整合到一个完整的程序中。

staticProg 示例

staticProg 程序由三个文件组成:两个 C 语言源文件(static1.cstatic2.c)以及一个头文件(static.h),头文件中包含两个声明:

/** header file static.h **/
#define NumCount 100               /* macro */
extern int global_nums[NumCount];  /* array declaration */
extern void fill_array();          /* function declaration */

两个声明中的 extern,一个用于数组,另一个用于函数,强调对象在别处(“外部”)定义:数组 global_nums 在文件 static1.c 中定义(没有显式的 extern),函数 fill_array 在文件 static2.c 中定义(也没有显式的 extern)。每个源文件都包含了头文件 static.hstatic1.c 文件定义了两个驻留在内存静态区域中的数组(global_numsmore_nums)。第二个数组有 static 修饰,这将其作用域限制为定义数组的文件 (static1.c)。如前所述, extern 修饰的 global_nums 则可以实现在多个文件中可见。

/** static1.c **/
#include <stdio.h>
#include <stdlib.h>

#include "static.h"             /* declarations */

int global_nums[NumCount];      /* definition: extern (global) aggregate */
static int more_nums[NumCount]; /* definition: scope limited to this file */

int main() {
  fill_array(); /** defined in file static2.c **/

  unsigned i;
  for (i = 0; i < NumCount; i++)
    more_nums[i] = i * -1;

  /* confirm initialization worked */
  for (i = 0; i < NumCount; i += 10) 
    printf("%4i\t%4i\n", global_nums[i], more_nums[i]);
    
  return 0;  
}

下面的 static2.c 文件中定义了 fill_array 函数,该函数由 main(在 static1.c 文件中)调用;fill_array 函数会给名为 global_numsextern 数组中的元素赋值,该数组在文件 static1.c 中定义。使用两个文件的唯一目的是凸显 extern 变量或函数能够跨文件可见。

/** static2.c **/
#include "static.h" /** declarations **/

void fill_array() { /** definition **/
  unsigned i;
  for (i = 0; i < NumCount; i++) global_nums[i] = i + 2;
}

staticProg 程序可以用如下编译:

% gcc -o staticProg static1.c static2.c

从汇编语言看更多细节

现代 C 编译器能够处理 C 和汇编语言的任意组合。编译 C 源文件时,编译器首先将 C 代码翻译成汇编语言。这是对从上文 static1.c 文件生成的汇编语言进行保存的命令:

% gcc -S static1.c

生成的文件就是 static1.s。这是文件顶部的一段代码,额外添加了行号以提高可读性:

    .file    "static1.c"          ## line  1
    .text                         ## line  2
    .comm    global_nums,400,32   ## line  3
    .local    more_nums           ## line  4
    .comm    more_nums,400,32     ## line  5
    .section    .rodata           ## line  6
.LC0:                             ## line  7
    .string    "%4i\t%4i\n"       ## line  8
    .text                         ## line  9
    .globl    main                ## line 10
    .type    main, @function      ## line 11
main:                             ## line 12
...

诸如 .file(第 1 行)之类的汇编语言指令以句点开头。顾名思义,指令会指导汇编程序将汇编语言翻译成机器代码。.rodata 指令(第 6 行)表示后面是只读对象,包括字符串常量 "%4i\t%4i\n"(第 8 行),main 函数(第 12 行)会使用此字符串常量来实现格式化输出。作为标签引入(通过末尾的冒号实现)的 main 函数(第 12 行),同样也是只读的。

在汇编语言中,标签就是地址。标签 main:(第 12 行)标记了 main 函数代码开始的地址,标签 .LC0:(第 7 行)标记了格式化字符串开头所在的地址。

global_nums(第 3 行)和 more_nums(第 4 行)数组的定义包含了两个数字:400 是每个数组中的总字节数,32 是每个数组(含 100 个 int 元素)中每个元素的比特数。(第 5 行中的 .comm 指令表示 common name,可以忽略。)

两个数组定义的不同之处在于 more_nums 被标记为 .local(第 4 行),这意味着其作用域仅限于其所在文件 static1.s。相比之下,global_nums 数组就能在多个文件中实现可见,包括由 static1.cstatic2.c 文件翻译成的汇编文件。

最后,.text 指令在汇编代码段中出现了两次(第 2 行和第 9 行)。术语“text”表示“只读”,但也会涵盖一些读/写变量,例如两个数组中的元素。尽管本文展示的汇编语言是针对 Intel 架构的,但 Arm6 汇编也非常相似。对于这两种架构,.text 区域中的变量(本例中为两个数组中的元素)会自动初始化为零。

总结

C 语言中的内存高效和内存安全编程准则很容易说明,但可能会很难遵循,尤其是在调用设计不佳的库的时候。准则如下:

  • 尽可能使用栈存储,进而鼓励编译器将通用寄存器用作暂存器,实现优化。栈存储代表了高效的内存使用并促进了代码的整洁和模块化。永远不要返回指向基于栈的存储的指针。
  • 小心使用堆存储。C(和 C++)中的重难点是确保动态分配的存储尽快解除分配。良好的编程习惯和工具(如 valgrind)有助于攻关这些重难点。优先选用自身提供释放函数的库,例如 nestedHeap 代码示例中的 free_all 释放函数。
  • 谨慎使用静态存储,因为这种存储会自始至终地影响进程的内存占用。特别是尽量避免使用 externstatic 数组。

本文 C 语言代码示例可在我的网站(https://condor.depaul.edu/mkalin)上找到。


via: https://opensource.com/article/21/8/memory-programming-c

作者:Marty Kalin 选题:lujun9972 译者:unigeorge 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

二十年后,ATI Radeon R300 的 Linux 驱动仍有零星改进

ATI R300 图形处理器推出于 2002 年.虽然 AMD 的开源 Linux 开发者在很长一段时间内不再关注这个 R300g 驱动,但由于开源的性质,社区能够对这个传统的硬件支持进行偶尔的改进。仅今年就已经提交了 14 个补丁。当然,大多数与这个近 20 年历史的 GPU 驱动有关的提交都是围绕着错别字和普通修复。顺便说一句,R300g 并不是 ATI/AMD 官方的硬件 Linux 驱动,但正是通过这种逆向工程和业余工作,导致这些开源开发者中的几个人陆续加入了 AMD,并受雇于该公司为其开发更多现代驱动。

老王点评:这就是开源的好处,各种意义上的。

谷歌赞助 Linux 基金会的安全开源计划 100 万美元

安全开源(SOS)试点计划会奖励广泛的改进措施,主动加强关键开源项目和支持基础设施,以应对针对应用程序和供应链的攻击。为了补充现有的奖励漏洞管理的计划,还会直接支持项目开发者。奖励范围从 10000 美元以上的复杂、高影响和持久的改进,几乎可以肯定地防止受影响的代码或支持的基础设施中的重大漏洞,到 500 美元的小改进,但至少从安全的角度来看需要有好处。谷歌初期向该计划投资 100 万美元,并计划根据社区反馈扩大该计划的范围。

老王点评:主动去加强开源软件的安全,我觉得谷歌这钱花的值。

Telegram 机器人正试图窃取你的一次性密码

虽然双因素认证(2FA)这样的一次性密码可以改进单独使用密码的安全性,但威胁者很快就开发出拦截这些 2FA 的方法,如通过恶意软件或社会工程。根据英特尔 471 团队的消息,自 6 月以来,一些用于规避 2FA 的服务正在滥用 Telegram 的消息服务。由 Telegram 驱动的机器人正被用来窃取 2FA 安全中所要求的一次性密码。

老王点评:虽然 2FA 是一个好的安全实践,但是安全最薄弱的环节往往是人。

PKGBUILD 文件是为 Arch Linux 及其衍生版(如 Manjaro)构建和创建软件包的方式。

如果你曾经使用过 AUR(即 Arch Linux 的用户维护的 PKGBUILD 存储库),你甚至可能也遇到过它们。

但是,到底是如何从 PKGBUILD 到可安装软件包的呢?这两者之间到底发生了什么,如何把自己的软件制作成软件包呢?你将在这篇文章中了解这些。

PKGBUILD 基础知识

对于那些熟悉 Bash 或其他 shell 的人来说,你可能知道,PKGBUILD 就是一个带有一些变量的 shell 脚本。

PKGBUILD 文件由变量和函数组成,所有这些都是用来定义软件包本身,以及如何构建它。

为了从 PKGBUILD 中创建一个软件包,需要使用 makepkg 命令行工具。在获得 PKGBUILD 文件后,你只需在包含 PKGBUILD 的目录中运行 `makepkg',就可以得到一个可安装的软件包了。

在本教程中,你将会看到我刚刚制作的软件包,它在运行时打印出 “Hello World!”。

准备

为了继续学习本教程,你需要创建几个文件。

首先,你需要创建一个名为 PKGBUILD 的文件,它将作为构建你的软件包的“配方”。

你需要做的另一个文件是一个叫 hello-world.sh 的文件。我稍后会解释它的用途。

你也可以用一个命令来创建这两个文件:

touch PKGBUILD hello-world.sh

你可以用 ls 命令检查这些文件是否被创建。

然后你就可以开始了!

设置你的 PKGBUILD 文件

我不会让你复制粘贴整个文件,而是和你一起键入每一行,这样你就能更好地理解每一行的目的。如果你不喜欢这种学习方式,我强烈推荐 Arch 维基 中为 Arch Linux 创建软件包的文章。

这篇文章也没有介绍 PKGBUILD 中可以设置的每一个选项,只是介绍了一些常用的选项,以便你能尽快上手。

说完了这些,打开你的文本编辑器,让我们直接进入正题吧。

pkgname

首先是 pkgname 变量。这是安装时定义软件包名称的东西,也是 Arch Linux 的软件包管理器 pacman 跟踪软件包的方式。

这个变量(以及其他一些变量)的格式是 variable=value,变量名在左边,变量的值在右边,用等号隔开。

要设置包的名称,请在 PKGBUILD 中输入以下内容:

pkgname="hello-world"
  • 要设置一个不同的软件包名称,用你的软件包的名称替换 hello-world
  • 这并不设置用于运行程序的命令,这将在下面的 package() 部分中处理。

pkgver

正如变量名称本身所述,它设置了你的软件包的版本(即 1.0.0)。这在用户更新他们的系统时很有用,因为设置更高的版本会提示用户升级。

要设置版本号,请在 PKGBUILD 中输入以下内容(在前一行之后):

pkgver="1.0.0"

pkgrel

这与 pkgver 变量有关,通常不需要知道。不过和 pkgver 变量一样,如果它被换到一个更高的数字,就将通知用户进行升级。

它适用于任何需要保持 pkgver 不变的情况下,例如 PKGBUILD 本身发生了变化。如果你为一个你使用的程序创建了一个 PKGBUILD(并希望保持软件包的版本相同),而你需要修复 PKGBUILD 本身的一个错误,这将是非常有用的。

要设置这个变量,请在 PKGBUILD 中输入以下内容:

pkgver="1"

这个变量应该 总是1 开始,然后一次一次地向上移动。当 pkgver 本身向上移动时,这个变量可以(也应该)重置为 1,因为 pkgver 本身会通知用户升级。

pkgdesc

这将设置软件包的描述,用于帮助更好地识别该软件包。

要设置它,只需将描述放在引号内:

pkgdesc="Hello world in your terminal!"

arch

这个变量设置软件包所兼容的 硬件架构。如果你不明白什么是架构,那也没关系,因为在大多数情况下,这个变量几乎是无用的。

无论如何,makepkg 仍然需要设置它,这样它就知道这个软件包与我们的系统是兼容的。

这个变量支持设置多个值,所以 makepkg 需要一个不同的语法,如下所示。

要设置它,请在 PKGBUILD 中输入以下内容:

arch=("x86_64")

如果你要设置多个值,需要用空格和引号分隔每个值,像这样。arch=(“x86_x64" "arm")

depends

这列出了提供了我们的软件包所需功能的所有软件包。与 arch 一样,它也可以包含多个值,因此必须使用括号语法。

由于我们的软件包没有任何依赖关系,所以我们不需要在 PKGBUILD 中输入这个字段。然而,如果我们的软件包有依赖关系,我们就会使用与 arch 相同的语法。

optdepends

这里列出了那些并不是提供所需功能而是额外功能的软件包。

这与 depends 的语法相同。

conflicts

这告诉 pacman 哪些软件包会导致我们的软件包出现问题,或者以我们不希望的方式行事。

这里列出的任何软件包都会在我们的软件包被安装之前被卸载。

这与 depends 的语法相同。

license

这定义了你的程序所采用的 软件许可证。如果你需要帮助你选择一个许可证,Arch 维基 提供了一些信息。如果你不知道该怎么设置,将其设置为 custom 也可以。

这与 archdepends 的语法相同:

license=("custom")

source

这就是 makepkg 如何知道要用什么文件来构建我们的软件包。它可以包含各种不同类型的源,包括本地文件和 URL。

在添加本地文件时,要输入相对于 PKGBUILD 文件的文件路径,比如以下目录布局:

PKGBUILD
file.txt
src/file.sh

如果你想在我们的 PKGBUILD 中包括 file.sh,你需要输入 src/file.sh 作为其名称。

当输入 URL 时,你只需输入完整的 URL,即 https://mirrors.creativecommons.org/presskit/logos/cc.logo.large.png

你的这个软件包只需要 hello-world.sh 文件,由于它和 PKGBUILD 在同一个目录中,你只需输入它的名字作为 source 的值。

这个变量也使用与 archdepends 相同的语法:

source=("hello-world.sh")

sha512sums

这是用来验证 source 中的文件没有被修改或下载错误。如何获得这个值的信息可以在 Arch 维基关于 PKGBUILD 的文章 中找到。

如果你宁愿不设置这个(或者你只是不需要,例如对于本地文件),你可以为 source 变量中的每个文件输入 SKIP

sha512sums=("SKIP")

package()

这是最后一个,也是实际制作我们的包的最重要的部分。在处理这个问题时,知道两个变量很重要。

  • ${srcdir}:这是 makepkg 放置 source 变量中文件的地方。在这个目录中,你可以与这些文件进行交互,并对文件进行任何其他需要的修改。
  • ${pkgdir}:这是我们放置将被安装在系统中的文件的地方。 ${pkgdir} 的文件夹结构是按照实际系统中的情况设置的(例如,使用 pacman 安装时,${pkgdir}/usr/bin/hello-world 会创建文件 /usr/bin/hello-world)。

package() 包含一个用于创建软件包的命令列表。

因此,如果(假设)你需要有个在 /usr/share/motto.txt 写着 “Linux is superior to Windows ”的文件,你会运行这样的东西:

package() {
  mkdir -p "${pkgdir}/usr/share"
  echo "Linux is superior to Windows" | tee "${pkgdir}/usr/share/motto.txt"
}

关于上述命令的一些说明:

  • ${pkgdir} 里面最初是 不包含 目录的。如果你跳过了 mkdir 命令tee 会输出一个错误,说这个目录不存在。
  • 在指定目录时,总是 在它们前面加上 ${pkgdir}${srcdir} 变量。如果输入 /usr/share/motto.txt,就会按照字面意义指向你当前运行的系统中的 /usr/share/motto.txt

对于你的 PKGBUILD,你将把 hello-world.sh 文件放在目标系统的 /usr/bin/hello-world 中。你还将使该文件在运行时说 “Hello to you!”。

要做到这一点,请在 PKGBUILD 中输入以下内容:

package() {
  echo 'Hello to you!' > "${srcdir}/hello-world.sh"
  mkdir -p "${pkgdir}/usr/bin"
  cp "${srcdir}/hello-world.sh" "${pkgdir}/usr/bin/hello-world"
  chmod +x "${pkgdir}/usr/bin/hello-world"
}

然后就完成了!用 makepkg -si 构建和安装软件包,然后在终端运行 hello-world,查看其输出。

总结

就这样,你已经制作了你的第一个 PKGBUILD!你走在了为自己甚至是为 AUR 制作实际的软件包的路上。

有什么问题,或者有什么地方不对吗?请随时在下面的评论区发表。


via: https://itsfoss.com/create-pkgbuild/

作者:Hunter Wittenborn 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出