分类 技术 下的文章

trace-cmd 是一个易于使用,且特性众多、可用来追踪内核函数的命令。

 title=

之前的文章 里,我介绍了如何利用 ftrace 来追踪内核函数。通过写入和读出文件来使用 ftrace 会变得很枯燥,所以我对它做了一个封装来运行带有选项的命令,以启用和禁用追踪、设置过滤器、查看输出、清除输出等等。

trace-cmd 命令是一个可以帮助你做到这一点的工具。在这篇文章中,我使用 trace-cmd 来执行我在 ftrace 文章中所做的相同任务。由于会经常参考那篇文章,建议在阅读这篇文章之前先阅读它。

安装 trace-cmd

本文中所有的命令都运行在 root 用户下。

因为 ftrace 机制被内置于内核中,因此你可以使用下面的命令进行验证它是否启用:

# mount | grep tracefs
none on /sys/kernel/tracing type tracefs (rw,relatime,seclabel)

不过,你需要手动尝试安装 trace-cmd 命令:

# dnf install trace-cmd -y

列出可用的追踪器

当使用 ftrace 时,你必须查看文件的内容以了解有哪些追踪器可用。但使用 trace-cmd,你可以通过以下方式获得这些信息:

# trace-cmd list -t
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

启用函数追踪器

在我 之前的文章 中,我使用了两个追踪器,在这里我也会这么做。用 function 启用你的第一个追踪器:

$ trace-cmd start -p function
  plugin 'function'

查看追踪输出

一旦追踪器被启用,你可以通过使用 show 参数来查看输出。这只显示了前 20 行以保持例子的简短(见我之前的文章对输出的解释):

# trace-cmd show | head -20
## tracer: function
#
# entries-in-buffer/entries-written: 410142/3380032   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
           gdbus-2606    [004] ..s. 10520.538759: __msecs_to_jiffies <-rebalance_domains
           gdbus-2606    [004] ..s. 10520.538760: load_balance <-rebalance_domains
           gdbus-2606    [004] ..s. 10520.538761: idle_cpu <-load_balance
           gdbus-2606    [004] ..s. 10520.538762: group_balance_cpu <-load_balance
           gdbus-2606    [004] ..s. 10520.538762: find_busiest_group <-load_balance
           gdbus-2606    [004] ..s. 10520.538763: update_group_capacity <-update_sd_lb_stats.constprop.0
           gdbus-2606    [004] ..s. 10520.538763: __msecs_to_jiffies <-update_group_capacity
           gdbus-2606    [004] ..s. 10520.538765: idle_cpu <-update_sd_lb_stats.constprop.0
           gdbus-2606    [004] ..s. 10520.538766: __msecs_to_jiffies <-rebalance_domains

停止追踪并清除缓冲区

追踪将会在后台继续运行,你可以继续用 show 查看输出。

要停止追踪,请运行带有 stop 参数的 trace-cmd 命令:

# trace-cmd stop

要清除缓冲区,用 clear 参数运行它:

# trace-cmd clear

启用函数调用图追踪器

运行第二个追踪器,通过 function_graph 参数来启用它。

# trace-cmd start -p function_graph
  Plugin 'function_graph'

再次使用 show 参数查看输出。正如预期的那样,输出与第一次追踪输出略有不同。这一次,它包括一个函数调用链:

# trace-cmd show | head -20
## tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 4)   0.079 us    |        } /* rcu_all_qs */
 4)   0.327 us    |      } /* __cond_resched */
 4)   0.081 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {
 4)   0.078 us    |        rcu_all_qs();
 4)   0.243 us    |      }
 4)   0.080 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {
 4)   0.078 us    |        rcu_all_qs();
 4)   0.241 us    |      }
 4)   0.080 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {
 4)   0.079 us    |        rcu_all_qs();
 4)   0.235 us    |      }
 4)   0.095 us    |      rcu_read_unlock_strict();
 4)               |      __cond_resched() {

使用 stopclear 命令来停止追踪和清除缓存区:

# trace-cmd stop
# trace-cmd clear

调整追踪以增加深度

如果你想在函数调用中看到更多的深度,你可以对追踪器进行调整:

# trace-cmd start -p function_graph --max-graph-depth 5
  plugin 'function_graph'

现在,当你将这个输出与你之前看到的进行比较时,你应该看到更多的嵌套函数调用:

# trace-cmd show | head -20
## tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 6)               |        __fget_light() {
 6)   0.804 us    |          __fget_files();
 6)   2.708 us    |        }
 6)   3.650 us    |      } /* __fdget */
 6)   0.547 us    |      eventfd_poll();
 6)   0.535 us    |      fput();
 6)               |      __fdget() {
 6)               |        __fget_light() {
 6)   0.946 us    |          __fget_files();
 6)   1.895 us    |        }
 6)   2.849 us    |      }
 6)               |      sock_poll() {
 6)   0.651 us    |        unix_poll();
 6)   1.905 us    |      }
 6)   0.475 us    |      fput();
 6)               |      __fdget() {

了解可被追踪的函数

如果你想只追踪某些函数而忽略其他的,你需要知道确切的函数名称。你可以用 list -f 参数来得到它们。例如搜索常见的内核函数 kmalloc,它被用来在内核中分配内存:

# trace-cmd list -f | grep kmalloc
bpf_map_kmalloc_node
mempool_kmalloc
__traceiter_kmalloc
__traceiter_kmalloc_node
kmalloc_slab
kmalloc_order
kmalloc_order_trace
kmalloc_large_node
__kmalloc
__kmalloc_track_caller
__kmalloc_node
__kmalloc_node_track_caller
[...]

下面是我的测试系统中可被追踪的函数总数:

# trace-cmd list -f | wc -l
63165

追踪内核模块相关的函数

你也可以追踪与特定内核模块相关的函数。假设你想追踪 kvm 内核模块相关的功能,你可以通过以下方式来实现。请确保该模块已经加载:

# lsmod | grep kvm_intel
kvm_intel 335872 0
kvm 987136 1 kvm_intel

再次运行 trace-cmd,使用 list 参数,并从输出结果中,grep 查找以 ] 结尾的行。这将过滤掉内核模块。然后 grep 内核模块 kvm_intel ,你应该看到所有与该内核模块有关的函数。

# trace-cmd list -f | grep ]$  | grep kvm_intel
vmx_can_emulate_instruction [kvm_intel]
vmx_update_emulated_instruction [kvm_intel]
vmx_setup_uret_msr [kvm_intel]
vmx_set_identity_map_addr [kvm_intel]
handle_machine_check [kvm_intel]
handle_triple_fault [kvm_intel]
vmx_patch_hypercall [kvm_intel]

[...]

vmx_dump_dtsel [kvm_intel]
vmx_dump_sel [kvm_intel]

追踪特定函数

现在你知道了如何找到感兴趣的函数,请用一个例子把这些内容用于时间。就像前面的文章一样,试着追踪与文件系统相关的函数。我的测试系统上的文件系统是 ext4

这个过程略有不同;你在运行命令时,不使用 start 参数,而是在 record 参数后面加上你想追踪的函数的“模式”。你还需要指定你想要的追踪器;在这种情况下,就是 function_graph。该命令会继续记录追踪,直到你用 Ctrl+C 停止它。所以几秒钟后,按 Ctrl+C 停止追踪:

# trace-cmd list -f | grep ^ext4_

# trace-cmd record -l ext4_* -p function_graph
  plugin 'function_graph'
Hit Ctrl^C to stop recording
^C
CPU0 data recorded at offset=0x856000
    8192 bytes in size
[...]

查看追踪记录

要查看你之前的追踪记录,运行带有 report 参数的命令。从输出结果来看,很明显过滤器起作用了,你只看到 ext4 相关的函数追踪:

# trace-cmd report | head -20
[...]
cpus=8
       trace-cmd-12697 [000] 11303.928103: funcgraph_entry:                   |  ext4_show_options() {
       trace-cmd-12697 [000] 11303.928104: funcgraph_entry:        0.187 us   |    ext4_get_dummy_policy();
       trace-cmd-12697 [000] 11303.928105: funcgraph_exit:         1.583 us   |  }
       trace-cmd-12697 [000] 11303.928122: funcgraph_entry:                   |  ext4_create() {
       trace-cmd-12697 [000] 11303.928122: funcgraph_entry:                   |    ext4_alloc_inode() {
       trace-cmd-12697 [000] 11303.928123: funcgraph_entry:        0.101 us   |      ext4_es_init_tree();
       trace-cmd-12697 [000] 11303.928123: funcgraph_entry:        0.083 us   |      ext4_init_pending_tree();
       trace-cmd-12697 [000] 11303.928123: funcgraph_entry:        0.141 us   |      ext4_fc_init_inode();
       trace-cmd-12697 [000] 11303.928123: funcgraph_exit:         0.931 us   |    }
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.081 us   |    ext4_get_dummy_policy();
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.133 us   |    ext4_get_group_desc();
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.115 us   |    ext4_free_inodes_count();
       trace-cmd-12697 [000] 11303.928124: funcgraph_entry:        0.114 us   |    ext4_get_group_desc();

追踪一个特定的 PID

假设你想追踪与一个进程(PID)有关的函数。打开另一个终端,注意运行中的 shell 的PID:

# echo $$
10885

再次运行 record 命令,用 -P 选项传递PID。这一次,让终端运行(也就是说,先不要按 Ctrl+C ):

# trace-cmd record -P 10885 -p function_graph
  Plugin 'function_graph'
Hit Ctrl^C to stop recording

在 shell 上运行一些命令

移动到另一个终端,在那里你有一个以特定 PID 运行的 shell,并运行任何命令,例如,ls 命令用来列出文件:

# ls
Temp-9b61f280-fdc1-4512-9211-5c60f764d702
tracker-extract-3-files.1000
v8-compile-cache-1000
[...]

移动到你启用追踪的终端,按 Ctrl+C 停止追踪:

# trace-cmd record -P 10885 -p function_graph
  plugin 'function_graph'
Hit Ctrl^C to stop recording
^C
CPU1 data recorded at offset=0x856000
    618496 bytes in size
[...]

在追踪的输出中,你可以看到左边是 PID 和 Bash shell,右边是与之相关的函数调用。这对于缩小你的追踪范围是非常方便的:

# trace-cmd report  | head -20

cpus=8
          <idle>-0     [001] 11555.380581: funcgraph_entry:                   |  switch_mm_irqs_off() {
          <idle>-0     [001] 11555.380583: funcgraph_entry:        1.703 us   |    load_new_mm_cr3();
          <idle>-0     [001] 11555.380586: funcgraph_entry:        0.493 us   |    switch_ldt();
          <idle>-0     [001] 11555.380587: funcgraph_exit:         7.235 us   |  }
            bash-10885 [001] 11555.380589: funcgraph_entry:        1.046 us   |  finish_task_switch.isra.0();
            bash-10885 [001] 11555.380591: funcgraph_entry:                   |  __fdget() {
            bash-10885 [001] 11555.380592: funcgraph_entry:        2.036 us   |    __fget_light();
            bash-10885 [001] 11555.380594: funcgraph_exit:         3.256 us   |  }
            bash-10885 [001] 11555.380595: funcgraph_entry:                   |  tty_poll() {
            bash-10885 [001] 11555.380597: funcgraph_entry:                   |    tty_ldisc_ref_wait() {
            bash-10885 [001] 11555.380598: funcgraph_entry:                   |      ldsem_down_read() {
            bash-10885 [001] 11555.380598: funcgraph_entry:                   |        __cond_resched() {

试一试

这些简短的例子显示了使用 trace-cmd 命令而不是底层的 ftrace 机制,是如何实现既容易使用又拥有丰富的功能,许多内容本文并没有涉及。要想了解更多信息并更好地使用它,请查阅它的手册,并尝试使用其他有用的命令。


via: https://opensource.com/article/21/7/linux-kernel-trace-cmd

作者:Gaurav Kamathe 选题:lujun9972 译者:萌新阿岩 校对:wxy

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

树莓派 4 绝对是数百万人的最爱,特别是在极客社区里,我也不例外。但是你知道树莓派在没有适当冷却的情况下会限制性能吗?

在这里,我将介绍 树莓派 4 官方外壳 的一些严重缺点,同时也分享一些缓解这些缺点的方法。

树莓派 4 官方外壳

在第一次启动后,我的安装在 树莓派 4 官方外壳 内的树莓派 4(8GB 内存版),在无人值守的升级启动时,会高达 80℃。我在 Ubuntu 上进行了所有的 固件更新,显然是为了 解决发热问题

就算在空闲时,这个烫手的香草和草莓蛋糕也绝不会低于 75℃。

我几乎无法使用它,直到我取下外壳顶部的白色盖子。它闲置时的温度降到只有 67℃ 左右 —— 你相信吗?即使是在我重新启动一段时间后再次检查也是这样。很明显,这仍然是不太可接受。如果我买了这个外壳并打算长期使用,我为什么要一直把盖子打开?

为什么会发生这样的事情?这都是因为官方的树莓派外壳的设计非常糟糕。

官方的树莓派 4 外壳是一个发热怪物!

简单地说,热节流 就是降低你的树莓派处理器(CPU)的性能,以使温度不超过极限高温(如 80℃)而 导致损坏

这个外壳是由塑料制成的,它是热的不良导体(简单的 传统物理学 知识),因此无法将热量有效地散布到整个外壳和板子之外。因此,板上的处理器会发热,一旦温度达到惊人的程度,它的性能就会被降到一个极低的水平。我注意到,在第一次开机后,在无人值守的情况下进行升级时,CPU 的温度为 80℃,CPU 的使用率为 100%。

虽然这个官方的外壳看起来很美,但它对树莓派的性能造成了很大的影响。

如果你真的想让你的树莓派发挥最大的性能,你也必须负责它的冷却。这些发热问题不能被简单地忽视。

热量被困在内部

一旦你把树莓派安装在这个外壳里,它甚至没有一个通风口可以让多余的热量排出。所以热量就一直在里面积累,直到达到那些疯狂的温度并触发了节流阀。

没有风扇通风口(非常需要)

顶部的白色盖子上可以有一个圆形的通风口,至少可以把 树莓派 4 的官方风扇 放在上面使用。

没有被动冷却

如果外壳是金属的,它就可以作为散热器,有效地将树莓派板上的处理器的热量散发出去。

除了发热问题之外,还有其他的缺点

树莓派 4 官方外壳还有一些缺点:

  1. 不便于 SD 卡管理:将树莓派板子装入外壳内,并将 SD 卡端口放在正确的方向上,以便以后能够换卡,这不是很方便。
  2. 没有螺丝钉系统:没有提供螺丝,也许是因为它可能会破坏机箱底座上的假支架,这些假支架看起来就像你可以用四颗螺丝把板子牢牢地固定在底座上。

你可以做什么来控制树莓派 4 的过热?

在做了一些紧张的研究之后,我找到了市场上一些最好的冷却解决方案 —— 这一切都要归功于我们了不起的改装社区。

使用冰塔式冷却器

我首先发现了 Jeff Geerling's 对各种树莓派散热器的深入性能评估,他在网上被称为 geerlingguy。在看完温度统计后,我直接选择了冰塔式散热器,并组装了它:

树莓派 4 冰塔冷却器

它空闲和低载时的温度下降到 30℃,现在保持在 45℃ 左右。我还没有为它改装一个合适的外壳。我准备找个给冷却器提供了足够的空间的现成外壳。也许可以在亚马逊或其他网上商店找到这种外壳。

但我没有找到这种产品。

使用铝制散热器进行被动散热

网上也有一个关于 被动冷却 的出色视频,测评了一个用铝制散热片做的外壳。

它提供了一个热垫,它相当于台式机处理器上使用的散热膏。按照视频中显示的方式放置它,热量就会从树莓派板上的处理器散发到整个外壳内。这就是科学的神奇之处!

改装官方的树莓派外壳

如果你仍然想买官方的外壳,建议你至少要做一个风扇的改装。

潜在的制造解决方案

这里有一些解决方案,通过应用 DevOps 启发的改进,可以使整个制造过程更容易。

  • 想一想,从外壳顶部切下的那块圆形塑料可以回收,用来制造更多的树莓派 4 外壳,不是吗?这显然会是一个双赢的局面,同时也降低了成本!
  • 铝是地球上最丰富的金属,但 全球供应中断 可能是一个挑战。即使如此,还有其他的 导电性解决方案 来探索用于设计案例的材料!

总结

希望这篇文章能帮助你从树莓派 4 中获得最大的收益。我很想知道你的想法、建议和经验,请在下面的评论中留言。请不要犹豫地分享。


via: https://itsfoss.com/raspberry-pi-case-overheating/

作者:Avimanyu Bandyopadhyay 选题:lujun9972 译者:wxy 校对:wxy

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

Fortran 是在打孔卡时代编写的语言,因此它的语法非常有限。但你仍然可以用它编写有用和有趣的程序。

 title=

Fortran 77 是我学习的第一门编译型编程语言。一开始时,我自学了如何在 Apple II 上用 BASIC 编写程序,后来又学会在 DOS 上用 QBasic 编写程序。但是当我去大学攻读物理学时,我又学习了 Fortran

Fortran 曾经在科学计算中很常见。曾几何时,所有计算机系统都有一个 Fortran 编译器。Fortran 曾经像今天的 Python 一样无处不在。因此,如果你是像我这样的物理学专业学生,在 1990 年代工作,那你肯定学习了 Fortran。

我一直认为 Fortran 与 BASIC 有点相似,所以每当我需要编写一个简短程序,来分析实验室数据或执行其他一些数值分析时,我都会很快想到 Fortran。我在空闲时用 Fortran 编写了一个“猜数字”游戏,其中计算机会在 1 到 100 之间选择一个数字,并让我猜这个数字。程序会一直循环,直到我猜对了为止。

“猜数字”程序练习了编程语言中的几个概念:如何为变量赋值、如何编写语句以及如何执行条件判断和循环。这是学习新编程语言时一个很好的的实践案例。

Fortran 编程基础

虽然 Fortran 这些年来一直在更新,但我最熟悉的还是 Fortran 77,这是我多年前学习的实现版本。Fortran 是程序员还在打孔卡上编程的年代创建的,因此“经典” Fortran 仅限于处理可以放在打孔卡上的数据。这意味着你只能编写符合以下限制条件的经典 Fortran 程序(LCTT 译注:后来的 Fortran 95 等版本已经对这些限制做了很大的改进,如有兴趣建议直接学习新版):

  • 每张卡只允许一行源代码。
  • 仅识别第 1-72 列(最后八列,73-80,保留给卡片分类器)。
  • 行号(“标签”)位于第 1-5 列。
  • 程序语句在第 7-72 列。
  • 要表示跨行,请在第 6 列中输入一个连续字符(通常是 +)。
  • 要创建注释行,请在第 1 列中输入 C*
  • 只有字符 AZ(大写字母)、09(数字)和特殊字符 = + - * / ( ) , . $ ' : 和空格能够使用。

虽然有这些限制,你仍然可以编写非常有用和有趣的程序。

在 Fortran 中猜数字

通过编写“猜数字”游戏来探索 Fortran。这是我的实现代码:

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
C     PROGRAM TO GUESS A NUMBER 1-100
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
      PROGRAM GUESSNUM
      INTEGER SEED, NUMBER, GUESS

      PRINT *, 'ENTER A RANDOM NUMBER SEED'
      READ *, SEED
      CALL SRAND(SEED)

      NUMBER = INT( RAND(0) * 100 + 1 )

      PRINT *, 'GUESS A NUMBER BETWEEN 1 AND 100'
 10   READ *, GUESS

      IF (GUESS.LT.NUMBER) THEN
         PRINT *, 'TOO LOW'
      ELSE IF (GUESS.GT.NUMBER) THEN
         PRINT *, 'TOO HIGH'
      ENDIF

      IF (GUESS.NE.NUMBER) GOTO 10

      PRINT *, 'THATS RIGHT!'
      END

如果你熟悉其他编程语言,你大概可以通过阅读源代码来弄清楚这个程序在做什么。前三行是注释块,表示程序的功能。第四行 PROGRAM GUESSNUM 将其标识为一个 程序 program ,并由最后一行的 END 语句关闭。

定义变量后,程序会提示用户输入随机数种子。Fortran 程序无法从操作系统初始化随机数生成器,因此你必须始终使用“种子”值和 SRAND 子程序 subroutine 启动随机数生成器。

Fortran 使用 RAND(0) 函数生成 0 到 0.999…… 之间的随机数。参数 0 告诉 RAND 函数生成一个随机数。将此随机数乘以 100 以生成 0 到 99.999…… 之间的数字,然后加 1 得到 1 到 100.999…… 之间的值。INT 函数将结果截断为整数;因此,变量 NUMBER 就是一个介于 1 到 100 之间的随机数。

程序会给出提示,然后进入一个循环。Fortran 不支持更现代的编程语言中可用的 whiledo-while 循环(LCTT 译注:Fortran 95 等新版支持,也因此在一定程度上减少了 GOTO 的使用)。相反,你必须使用标签(行号)和 GOTO 语句来构建自己的循环。这就是 READ 语句有一个行号的原因:你可以在循环末尾使用 GOTO 跳转到此标签。

穿孔卡片没有 <(小于)和 >(大于)符号,因此 Fortran 采用了另一种语法来进行值比较。要测试一个值是否小于另一个值,请使用 .LT.(小于)。要测试一个值是否大于另一个值,请使用 .GT.(大于)。等于和不等于分别是 .EQ..NE.

在每次循环中,程序都会验证用户的猜测值。如果用户的猜测值小于随机数,程序打印 TOO LOW,如果猜测大于随机数,程序打印 TOO HIGH。循环会一直持续,直到用户的猜测值等于目标随机数为止。

当循环退出时,程序打印 THATS RIGHT! 并立即结束运行。

$ gfortran -Wall -o guess guess.f

$ ./guess
 ENTER A RANDOM NUMBER SEED
93759
 GUESS A NUMBER BETWEEN 1 AND 100
50
 TOO LOW
80
 TOO HIGH
60
 TOO LOW
70
 TOO LOW
75
 TOO HIGH
73
 TOO LOW
74
 THATS RIGHT!

每次运行程序时,用户都需要输入不同的随机数种子。如果你总是输入相同的种子,程序给出的随机数也会一直不变。

在其他语言中尝试

在学习一门新的编程语言时,这个“猜数字”游戏是一个很好的入门程序,因为它以非常简单的方式练习了几个常见的编程概念。通过用不同的编程语言实现这个简单的游戏,你可以弄清一些核心概念以及比较每种语言的细节。

你有最喜欢的编程语言吗?如何用你最喜欢的语言来编写“猜数字”游戏?跟随本系列文章来查看你可能感兴趣的其他编程语言示例吧。


via: https://opensource.com/article/21/1/fortran

作者:Jim Hall 选题:lujun9972 译者:unigeorge 校对:wxy

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

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中国 荣誉推出

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中国 荣誉推出