分类 软件开发 下的文章

Rust 的高性能、高可靠性和高生产力使它适合于嵌入式系统。

 title=

在过去的几年里,Rust 在程序员中获得了热情的追捧。技术潮流来来去去,所以很难将仅仅因为某项新技术而产生的兴奋与对某项技术的优点的兴奋区分开来,但我认为 Rust 是一种真正设计良好的语言。它的目标是帮助开发者建立可靠和高效的软件,而且它从一开始就是为这个目的设计的。你可能听过一些 Rust 的关键特性,在这篇文章中,我会证明这些特性正是 Rust 也恰好适合嵌入式系统的原因。比如:

  • 高性能:它速度快,内存利用率高
  • 可靠性:在编译过程中可以消除内存错误
  • 生产力:很棒的文档,友好的编译器,有用的错误信息,以及一流的工具化。它有一个集成的包管理器和构建工具,智能的多编辑器支持自动补完和类型检查、自动格式化等等。

为什么使用 Rust 进行嵌入式开发?

Rust 的设计是为了保证安全和高性能。嵌入式软件会出现的问题主要是内存的问题。从某种程度上说,Rust 是一种面向编译器的语言,所以你可以确保在编译时安全使用内存。以下是使用 Rust 在嵌入式设备上开发的一些好处:

  • 强大的静态分析
  • 灵活的内存
  • 无畏的并发性
  • 互操作性
  • 可移植性
  • 社区驱动

在这篇文章中,我使用开源的 RT-Thread 操作系统 来演示如何使用 Rust 进行嵌入式开发。

如何在 C 语言中调用 Rust

在 C 代码中调用 Rust 代码时,你必须将 Rust 源代码打包成静态库文件。当 C 代码编译时,将其链接进去。

用 Rust 创建一个静态库

在这个过程中,有两个步骤:

1、使用 cargo init --lib rust_to_c 在 Clion 中建立一个 lib 库。在 lib.rs 中加入以下代码。下面的函数计算两个类型为 i32 的值的总和并返回结果:

#![no_std]
use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn sum(a: i32, b: i32) -> i32 {
    a + b
}

#[panic_handler]
fn panic(_info:&PanicInfo) -> !{
    loop{}
}

2、在你的 Cargo.toml 文件中添加以下代码,以告诉 Rustc 要生成什么类型的库:

[lib]
name = "sum"
crate-type = ["staticlib"]
path = "src/lib.rs"

交叉编译

你可以针对你的目标平台进行交叉编译。假设你的嵌入式系统是基于 Arm 的,步骤很简单:

$ rustup target add armv7a-none-eabi

生成静态库文件:

$ cargo build --target=armv7a-none-eabi --release --verbose
Fresh rust_to_c v0.1.0
Finished release [optimized] target(s) in 0.01s

生成头文件

你也需要头文件:

1、安装 cbindgencbindgen 工具会从 Rust 库中生成一个 C 或 C++11 的头文件:

$ cargo install --force cbindgen

2、在你的项目文件夹下创建一个新的 cbindgen.toml 文件。

3、生成一个头文件:

$ cbindgen --config cbindgen.toml --crate rust_to_c --output sum.h

调用 Rust 库文件

现在你可以对你的 Rust 库进行调用了。

1、把生成的 sum.hsum.a 文件放到 rt-thread/bsp/qemu-vexpress-a9/applications 目录下。

2、修改 SConscript 文件并添加一个静态库:

   from building import *
   
   cwd     = GetCurrentDir()
   src     = Glob('*.c') + Glob('*.cpp')
   CPPPATH = [cwd]
   
   LIBS = ["libsum.a"]
   LIBPATH = [GetCurrentDir()]
   
   group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH, LIBS = LIBS, LIBPATH = LIBPATH)
   
   Return('group')

3、在主函数中调用 sum 函数,得到返回值,并 printf 该值:

   #include <stdint.h>
   #include <stdio.h>
   #include <stdlib.h>
   #include <rtthread.h>
   #include "sum.h"
   
   int main(void)
   {
       int32_t tmp;
   
       tmp = sum(1, 2);
       printf("call rust sum(1, 2) = %d\n", tmp);
   
       return 0;
   }

4、在 RT-Thread Env 环境中,使用 scons 来编译项目并运行:

$ scons -j6
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
[...]
scons: done building targets.

$ qemu.sh
 \ | /
- RT -     Thread Operating System
 / | \     4.0.4 build Jul 28 2021
2006 - 2021 Copyright by rt-thread team
lwIP-2.1.2 initialized!
[...]
call rust sum(1, 2) = 3

加、减、乘、除

你可以在 Rust 中实现一些复杂的数学运算。在 lib.rs 文件中,使用 Rust 语言来实现加、减、乘、除:

#![no_std]
use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

#[no_mangle]
pub extern "C" fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

#[no_mangle]
pub extern "C" fn divide(a: i32, b: i32) -> i32 {
    a / b
}

#[panic_handler]
fn panic(_info:&PanicInfo) -> !{
    loop{}
}

构建你的库文件和头文件,并把它们放在应用程序目录中。使用 scons 来编译。如果在链接过程中出现错误,请在官方 Github 页面 中找到解决方案。

修改 rtconfig.py 文件,并添加链接参数 --allow-multiple-definition

       DEVICE = ' -march=armv7-a -marm -msoft-float'
       CFLAGS = DEVICE + ' -Wall'
       AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp -D__ASSEMBLY__ -I.'
       LINK_SCRIPT = 'link.lds'
       LFLAGS = DEVICE + ' -nostartfiles -Wl,--gc-sections,-Map=rtthread.map,-cref,-u,system_vectors,--allow-multiple-definition'+\
                         ' -T %s' % LINK_SCRIPT
   
       CPATH = ''
       LPATH = ''

编译并运行 QEMU 来看看你的工作。

在 Rust 中调用 C 语言

Rust 可以在 C 代码中调用,但是如何在你的 Rust 代码中调用 C 呢?下面是一个在 Rust 代码中调用 rt_kprintf C 函数的例子。

首先,修改 lib.rs 文件:

    // The imported rt-thread functions list
    extern "C" {
        pub fn rt_kprintf(format: *const u8, ...);
    }
   
    #[no_mangle]
    pub extern "C" fn add(a: i32, b: i32) -> i32 {
        unsafe {
            rt_kprintf(b"this is from rust\n" as *const u8);
        }
        a + b
    }

接下来,生成库文件:

$ cargo build --target=armv7a-none-eabi --release --verbose
Compiling rust_to_c v0.1.0
Running `rustc --crate-name sum --edition=2018 src/lib.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type staticlib --emit=dep-info,link -C opt-level=3 -C embed-bitcode=no -C metadata=a
Finished release [optimized] target(s) in 0.11s

而现在,要运行代码,将 Rust 生成的库文件复制到应用程序目录中,然后重新构建:

$ scons -j6 scons: Reading SConscript files ... scons: done reading SConscript files. [...]
scons: Building targets ... scons: done building targets.

再次运行 QEMU,可以在你的嵌入式镜像中看到结果。

你可以拥有这一切

在你的嵌入式开发中使用 Rust,你可以获得 Rust 的所有功能,而不需要牺牲灵活性或稳定性。今天就在你的嵌入式系统上试试 Rust 吧。关于嵌入式 Rust 的过程(以及 RT-Thread 本身)的更多信息,请查看 RT-Thread 项目的 YouTube 频道。请记住,嵌入式也可以是开放的。


via: https://opensource.com/article/21/10/rust-embedded-development

作者:Alan Smithee 选题:lujun9972 译者:wxy 校对:wxy

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

速度和可扩展性是 Apache Cassandra 不变的追求;来学习一下如何充分发挥它的专长吧。

 title=

Apache Cassandra 是一个数据库,但又不是一个简单的数据库;它是一个复制数据库,专为可扩展性、高可用性、低延迟和良好性能而设计调整。Cassandra 可以帮你的数据在区域性中断、硬件故障时,以及很多管理员认为数据量过多的情况下幸免于难。

全面掌握数据分区知识,你就能让 Cassandra 集群实现良好的设计、极高的性能和可扩展性。在本文中,我将探究如何定义分区,Cassandra 如何使用这些分区,以及一些你应该了解的最佳实践方案和已知问题。

基本概念是这样的: 供数据库关键函数(如数据分发、复制和索引化)使用的原子单元,单个这样的数据块就是一个分区。分布式数据系统通常会把传入的数据分配到这些分区中,使用简单的数学函数(例如 identity 或 hashing 函数)执行分区过程,并用得到的 “分区键” 对数据分组,进一步再形成分区。例如,假设传入数据是服务器日志,使用 “identity” 分区函数和每个日志的时间戳(四舍五入到小时值)作为分区键,我们可以对这些数据进行分区,实现每个分区各保存一小时的日志的目的。

Cassandra 中的数据分区

Cassandra 作为分布式系统运行,并且符合前述数据分区原则。使用 Cassandra,数据分区依赖于在集群级别配置的算法和在表级别配置的分区键。

 title=

Cassandra 查询语言(CQL)使用大家很熟悉的 SQL 表、行、列等术语。在上面的示例图中,表配置的主键中包含了分区键,具体格式为: 主键 Primary Key = 分区键 Partition Key + [ 聚簇列 Clustering Columns ] 。

Cassandra 中的主键既定义了唯一的数据分区,也包含着分区内的数据排列依据信息。数据排列信息取决于聚簇列(非必需项)。每个唯一的分区键代表着服务器(包括其副本所在的服务器)中管理的若干行。

在 CQL 中定义主键

接下来的四个示例演示了如何使用 CQL 语法表示主键。定义主键会让数据行分到不同的集合里,通常这些集合就是分区。

定义方式 1(分区键:log\_hour,聚簇列:无)

CREATE TABLE server_logs(
   log_hour TIMESTAMP PRIMARYKEY,
   log_level text,
   message text,
   server text
   )

这里,有相同 log_hour 的所有行都会进入同一个分区。

定义方式 2(分区键:log\_hour,聚簇列:log\_level)

CREATE TABLE server_logs(
   log_hour TIMESTAMP,
   log_level text,
   message text,
   server text,
   PRIMARY KEY (log_hour, log_level)
   )

此定义方式与方式 1 使用了相同的分区键,但此方式中,每个分区的所有行都会按 log_level 升序排列。

定义方式 3(分区键:log\_hour,server,聚簇列:无)

CREATE TABLE server_logs(
   log_hour TIMESTAMP,
   log_level text,
   message text,
   server text,
   PRIMARY KEY ((log_hour, server))
   )

在此定义中,serverlog_hour 字段都相同的行才会进入同一个分区。

定义方式 4(分区键:log\_hour,server,聚簇列:log\_level)

CREATE TABLE server_logs(
   log_hour TIMESTAMP,
   log_level text,
   message text,
   server text,
   PRIMARY KEY ((log_hour, server),log_level)
   )WITH CLUSTERING ORDER BY (column3 DESC);

此定义方式与方式 3 分区相同,但分区内的行会依照 log_level 降序排列。

Cassandra 如何使用分区键

Cassandra 依靠分区键来确定在哪个节点上存储数据,以及在需要时定位数据。Cassandra 通过查看表中的分区键来执行这些读取和写入操作,并使用 令牌 tokens (一个 -2^{63}−263 到 +2^{63}-1+263−1 范围内的 long 类型值)来进行数据分布和索引。这些令牌通过分区器映射到分区键,分区器使用了将分区键转换为令牌的分区函数。通过这种令牌机制,Cassandra 集群的每个节点都拥有一组数据分区。然后分区键在每个节点上启用数据索引。

 title=

图中显示了一个三节点的 Cassandra 集群以及相应的令牌范围分配。这只是一个简单的示意图:具体实现过程使用了 Vnodes

数据分区对 Cassandra 集群的影响

用心的分区键设计对于实现用例的理想分区大小至关重要。合理的分区可以实现均匀的数据分布和强大的 I/O 性能。分区大小对 Cassandra 集群有若干需要注意的影响:

  • 读取性能 —— 为了在磁盘上的 SSTables 文件中找到分区,Cassandra 使用缓存、索引和索引摘要等数据结构。过大的分区会降低这些数据结构的维护效率,从而对性能产生负面影响。Cassandra 新版本在这方面取得了长足的进步:特别是 3.6 及其以上版本的 Cassandra 引擎引入了存储改进,针对大型分区,可以提供更好的性能,以及更强的应对内存问题和崩溃的弹性。
  • 内存使用 —— 大分区会对 JVM 堆产生更大的压力,同时分区的增大也降低了垃圾收集机制的效率。
  • Cassandra 修复 —— 大分区使 Cassandra 执行修复维护操作(通过跨副本比较数据来保持数据一致)时更加困难。
  • “墓碑”删除 —— 听起来可能有点骇人,Cassandra 使用称为“ 墓碑 tombstones ”的独特标记来记录要删除的数据。如果没有合适的数据删除模式和压缩策略,大分区会使删除过程变得更加困难。

虽然这些影响可能会让人更倾向于简单地设计能产生小分区的分区键,但数据访问模式对理想的分区大小也有很大影响(有关更多信息,请阅读关于 Cassandra 数据建模 的深入讲解)。数据访问模式可以定义为表的查询方式,包括表的所有 select 查询。 理想情况下,CQL 选择查询应该在 where 子句中只使用一个分区键。也就是说,当查询可以从单个分区,而不是许多较小的分区获取所需数据时,Cassandra 是最有效率的。

分区键设计的最佳实践

遵循分区键设计的最佳实践原则,这会帮你得到理想的分区大小。根据经验,Cassandra 中的最大分区应保持在 100MB 以下。理想情况下,它应该小于 10MB。虽然 Cassandra 3.6 及其以上版本能更好地支持大分区,但也必须对每个工作负载进行仔细的测试和基准测试,以确保分区键设计能够支持所需的集群性能。

具体来说,这些最佳实践原则适用于任何分区键设计:

  • 分区键的目标必须是将理想数量的数据放入每个分区,以支持其访问模式的需求。
  • 分区键应禁止无界分区:那些大小可能随着时间无限增长的分区。例如,在上面的 server_logs 示例中,随着服务器日志数量的不断增加,使用服务器列作为分区键就会产生无界分区。相比之下,使用 log_hour 将每个分区限制为一个小时数据的方案会更好。
  • 分区键还应避免产生分区倾斜,即分区增长不均匀,有些分区可能随着时间的推移而不受限制地增长。在 server_logs 示例中,在一台服务器生成的日志远多于其他服务器的情况下使用服务器列会产生分区倾斜。为了避免这种情况,可以从表中引入另一个属性来强制均匀分布,即使要创建一个虚拟列来这样做,也是值得的。
  • 使用时间元素和其他属性的组合分区键,这对时间序列数据分区很有帮助。这种方式可以防止无界分区,使访问模式能够在查询特定数据时使用时间属性,而且能够对特定时间段内的数据进行删除。上面的每个示例都使用了 log_hour 时间属性来演示这一点。

还有一些工具可用于帮助测试、分析和监控 Cassandra 分区,以检查所选模式是否高效。通过仔细设计分区键,使解决方案的数据和需求保持一致,并遵循最佳实践原则来优化分区大小,你就可以充分利用数据分区,更好地发挥 Cassandra 的可扩展性和性能潜力。


via: https://opensource.com/article/20/5/apache-cassandra

作者:Anil Inamdar 选题:lujun9972 译者:unigeorge 校对:wxy

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

增强 C 语言程序的弹性和可靠性的五种方法。

 title=

即使是最好的程序员也无法完全避免错误。这些错误可能会引入安全漏洞、导致程序崩溃或产生意外操作,具体影响要取决于程序的运行逻辑。

C 语言有时名声不太好,因为它不像近期的编程语言(比如 Rust)那样具有内存安全性。但是通过额外的代码,一些最常见和严重的 C 语言错误是可以避免的。下文讲解了可能影响应用程序的五个错误以及避免它们的方法:

1、未初始化的变量

程序启动时,系统会为其分配一块内存以供存储数据。这意味着程序启动时,变量将获得内存中的一个随机值。

有些编程环境会在程序启动时特意将内存“清零”,因此每个变量都得以有初始的零值。程序中的变量都以零值作为初始值,听上去是很不错的。但是在 C 编程规范中,系统并不会初始化变量。

看一下这个使用了若干变量和两个数组的示例程序:

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

int
main()
{
  int i, j, k;
  int numbers[5];
  int *array;

  puts("These variables are not initialized:");

  printf("  i = %d\n", i);
  printf("  j = %d\n", j);
  printf("  k = %d\n", k);

  puts("This array is not initialized:");

  for (i = 0; i < 5; i++) {
    printf("  numbers[%d] = %d\n", i, numbers[i]);
  }

  puts("malloc an array ...");
  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("This malloc'ed array is not initialized:");

    for (i = 0; i < 5; i++) {
      printf("  array[%d] = %d\n", i, array[i]);
    }

    free(array);
  }

  /* done */

  puts("Ok");
  return 0;
}

这个程序不会初始化变量,所以变量以系统内存中的随机值作为初始值。在我的 Linux 系统上编译和运行这个程序,会看到一些变量恰巧有“零”值,但其他变量并没有:

These variables are not initialized:
  i = 0
  j = 0
  k = 32766
This array is not initialized:
  numbers[0] = 0
  numbers[1] = 0
  numbers[2] = 4199024
  numbers[3] = 0
  numbers[4] = 0
malloc an array ...
This malloc'ed array is not initialized:
  array[0] = 0
  array[1] = 0
  array[2] = 0
  array[3] = 0
  array[4] = 0
Ok

很幸运,ij 变量是从零值开始的,但 k 的起始值为 32766。在 numbers 数组中,大多数元素也恰好从零值开始,只有第三个元素的初始值为 4199024。

在不同的系统上编译相同的程序,可以进一步显示未初始化变量的危险性。不要误以为“全世界都在运行 Linux”,你的程序很可能某天在其他平台上运行。例如,下面是在 FreeDOS 上运行相同程序的结果:

These variables are not initialized:
  i = 0
  j = 1074
  k = 3120
This array is not initialized:
  numbers[0] = 3106
  numbers[1] = 1224
  numbers[2] = 784
  numbers[3] = 2926
  numbers[4] = 1224
malloc an array ...
This malloc'ed array is not initialized:
  array[0] = 3136
  array[1] = 3136
  array[2] = 14499
  array[3] = -5886
  array[4] = 219
Ok

永远都要记得初始化程序的变量。如果你想让变量将以零值作为初始值,请额外添加代码将零分配给该变量。预先编好这些额外的代码,这会有助于减少日后让人头疼的调试过程。

2、数组越界

C 语言中,数组索引从零开始。这意味着对于长度为 10 的数组,索引是从 0 到 9;长度为 1000 的数组,索引则是从 0 到 999。

程序员有时会忘记这一点,他们从索引 1 开始引用数组,产生了 “大小差一” off by one 错误。在长度为 5 的数组中,程序员在索引“5”处使用的值,实际上并不是数组的第 5 个元素。相反,它是内存中的一些其他值,根本与此数组无关。

这是一个数组越界的示例程序。该程序使用了一个只含有 5 个元素的数组,但却引用了该范围之外的数组元素:

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

int
main()
{
  int i;
  int numbers[5];
  int *array;

  /* test 1 */

  puts("This array has five elements (0 to 4)");

  /* initalize the array */
  for (i = 0; i < 5; i++) {
    numbers[i] = i;
  }

  /* oops, this goes beyond the array bounds: */
  for (i = 0; i < 10; i++) {
    printf("  numbers[%d] = %d\n", i, numbers[i]);
  }

  /* test 2 */

  puts("malloc an array ...");

  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("This malloc'ed array also has five elements (0 to 4)");

    /* initalize the array */
    for (i = 0; i < 5; i++) {
      array[i] = i;
    }

    /* oops, this goes beyond the array bounds: */
    for (i = 0; i < 10; i++) {
      printf("  array[%d] = %d\n", i, array[i]);
    }

    free(array);
  }

  /* done */

  puts("Ok");
  return 0;
}

可以看到,程序初始化了数组的所有值(从索引 0 到 4),然后从索引 0 开始读取,结尾是索引 9 而不是索引 4。前五个值是正确的,再后面的值会让你不知所以:

This array has five elements (0 to 4)
  numbers[0] = 0
  numbers[1] = 1
  numbers[2] = 2
  numbers[3] = 3
  numbers[4] = 4
  numbers[5] = 0
  numbers[6] = 4198512
  numbers[7] = 0
  numbers[8] = 1326609712
  numbers[9] = 32764
malloc an array ...
This malloc'ed array also has five elements (0 to 4)
  array[0] = 0
  array[1] = 1
  array[2] = 2
  array[3] = 3
  array[4] = 4
  array[5] = 0
  array[6] = 133441
  array[7] = 0
  array[8] = 0
  array[9] = 0
Ok

引用数组时,始终要记得追踪数组大小。将数组大小存储在变量中;不要对数组大小进行 硬编码 hard-code 。否则,如果后期该标识符指向另一个不同大小的数组,却忘记更改硬编码的数组长度时,程序就可能会发生数组越界。

3、字符串溢出

字符串只是特定类型的数组。在 C 语言中,字符串是一个由 char 类型值组成的数组,其中用一个零字符表示字符串的结尾。

因此,与数组一样,要注意避免超出字符串的范围。有时也称之为 字符串溢出

使用 gets 函数读取数据是一种很容易发生字符串溢出的行为方式。gets 函数非常危险,因为它不知道在一个字符串中可以存储多少数据,只会机械地从用户那里读取数据。如果用户输入像 foo 这样的短字符串,不会发生意外;但是当用户输入的值超过字符串长度时,后果可能是灾难性的。

下面是一个使用 gets 函数读取城市名称的示例程序。在这个程序中,我还添加了一些未使用的变量,来展示字符串溢出对其他数据的影响:

#include <stdio.h>
#include <string.h>

int
main()
{
  char name[10];                       /* Such as "Chicago" */
  int var1 = 1, var2 = 2;

  /* show initial values */

  printf("var1 = %d; var2 = %d\n", var1, var2);

  /* this is bad .. please don't use gets */

  puts("Where do you live?");
  gets(name);

  /* show ending values */

  printf("<%s> is length %d\n", name, strlen(name));
  printf("var1 = %d; var2 = %d\n", var1, var2);

  /* done */

  puts("Ok");
  return 0;
}

当你测试类似的短城市名称时,该程序运行良好,例如伊利诺伊州的 Chicago 或北卡罗来纳州的Raleigh

var1 = 1; var2 = 2
Where do you live?
Raleigh
<Raleigh> is length 7
var1 = 1; var2 = 2
Ok

威尔士的小镇 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 有着世界上最长的名字之一。这个字符串有 58 个字符,远远超出了 name 变量中保留的 10 个字符。结果,程序将值存储在内存的其他区域,覆盖了 var1var2 的值:

var1 = 1; var2 = 2
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
<Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
var1 = 2036821625; var2 = 2003266668
Ok
Segmentation fault (core dumped)

在运行结束之前,程序会用长字符串覆盖内存的其他部分区域。注意,var1var2 的值不再是起始的 12

避免使用 gets 函数,改用更安全的方法来读取用户数据。例如,getline 函数会分配足够的内存来存储用户输入,因此不会因输入长值而发生意外的字符串溢出。

4、重复释放内存

“分配的内存要手动释放”是良好的 C 语言编程原则之一。程序可以使用 malloc 函数为数组和字符串分配内存,该函数会开辟一块内存,并返回一个指向内存中起始地址的指针。之后,程序可以使用 free 函数释放内存,该函数会使用指针将内存标记为未使用。

但是,你应该只使用一次 free 函数。第二次调用 free 会导致意外的后果,可能会毁掉你的程序。下面是一个针对此点的简短示例程序。程序分配了内存,然后立即释放了它。但为了模仿一个健忘但有条理的程序员,我在程序结束时又一次释放了内存,导致两次释放了相同的内存:

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

int
main()
{
  int *array;

  puts("malloc an array ...");

  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("malloc succeeded");

    puts("Free the array...");
    free(array);
  }

  puts("Free the array...");
  free(array);

  puts("Ok");
}

运行这个程序会导致第二次使用 free 函数时出现戏剧性的失败:

malloc an array ...
malloc succeeded
Free the array...
Free the array...
free(): double free detected in tcache 2
Aborted (core dumped)

要记得避免在数组或字符串上多次调用 free。将 mallocfree 函数定位在同一个函数中,这是避免重复释放内存的一种方法。

例如,一个纸牌游戏程序可能会在主函数中为一副牌分配内存,然后在其他函数中使用这副牌来玩游戏。记得在主函数,而不是其他函数中释放内存。将 mallocfree 语句放在一起有助于避免多次释放内存。

5、使用无效的文件指针

文件是一种便捷的数据存储方式。例如,你可以将程序的配置数据存储在 config.dat 文件中。Bash shell 会从用户家目录中的 .bash_profile 读取初始化脚本。GNU Emacs 编辑器会寻找文件 .emacs 以从中确定起始值。而 Zoom 会议客户端使用 zoomus.conf 文件读取其程序配置。

所以,从文件中读取数据的能力几乎对所有程序都很重要。但是假如要读取的文件不存在,会发生什么呢?

在 C 语言中读取文件,首先要用 fopen 函数打开文件,该函数会返回指向文件的流指针。你可以结合其他函数,使用这个指针来读取数据,例如 fgetc 会逐个字符地读取文件。

如果要读取的文件不存在或程序没有读取权限,fopen 函数会返回 NULL 作为文件指针,这表示文件指针无效。但是这里有一个示例程序,它机械地直接去读取文件,不检查 fopen 是否返回了 NULL

#include <stdio.h>

int
main()
{
  FILE *pfile;
  int ch;

  puts("Open the FILE.TXT file ...");

  pfile = fopen("FILE.TXT", "r");

  /* you should check if the file pointer is valid, but we skipped that */

  puts("Now display the contents of FILE.TXT ...");

  while ((ch = fgetc(pfile)) != EOF) {
    printf("<%c>", ch);
  }

  fclose(pfile);

  /* done */

  puts("Ok");
  return 0;
}

当你运行这个程序时,第一次调用 fgetc 会失败,程序会立即中止:

Open the FILE.TXT file ...
Now display the contents of FILE.TXT ...
Segmentation fault (core dumped)

始终检查文件指针以确保其有效。例如,在调用 fopen 打开一个文件后,用类似 if (pfile != NULL) 的语句检查指针,以确保指针是可以使用的。

人都会犯错,最优秀的程序员也会产生编程错误。但是,遵循上面这些准则,添加一些额外的代码来检查这五种类型的错误,就可以避免最严重的 C 语言编程错误。提前编写几行代码来捕获这些错误,可能会帮你节省数小时的调试时间。


via: https://opensource.com/article/21/10/programming-bugs

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

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

Jekyll 是一个开源的静态网站生成器。你可以使用 Markdown 编写内容,使用 HTML/CSS 来构建和展示,Jekyll 会将其编译为静态的 HTML。

 title=

近年来开始流行静态网站生成器和 JAMStack,而且理由很充分,它们不需要复杂的后端,只需要静态的 HTML、CSS 和 Javascript。没有后端意味着更好的安全性、更低的运营开销和更便宜的托管。双赢!

在本文中,我将讨论 Jekyll。在撰写本文时,我的个人网站使用的是 Jekyll。Jekyll 使用 Ruby 引擎将用 Markdown 编写的文章转换成 HTML。Sass 可以将复杂的 CSS 规则应用到普通文本文件中。Liquid 允许对静态内容进行编程控制。

安装 Jekyll

Jekyll 网站 提供了 Linux、MacOS 和 Windows 安装说明。安装完成之后,快速引导 将会安装一个基础的 Hello-World 项目。

现在在你的浏览器访问 http://localhost:4000,你可以看到你的默认“真棒”博客。

 title=

目录结构

这个默认站点包含以下的文件和文件夹:

  • _posts: 你的博客文章。
  • _site: 最终编译成的静态网站文件。
  • about.markdown: “关于页”的内容。
  • index.markdown: “主页”的内容。
  • 404.html: “404 页”的内容。
  • _config.yml: Jekyll 的全站配置文件。

创建新的博客帖子

创建帖子很简单。你需要做的就是在 _post 目录下使用正确的格式和扩展名创建一个新文件,这样就完成了。

有效的文件名像 2021-08-29-welcome-to-jekyll.markdown 这样。一个博客文件必须包含 Jekyll 所谓的 YAML 卷首块 Front Matter 。它是文件开头的一个包含元数据的特殊部分。如果你查看默认的帖子,你可以看到以下内容:

---
layout: post
title: "Welcome to Jekyll!"
date:  2021-08-29 11:28:12 +0530
categories: jekyll update
---

Jekyll 会使用上面的元数据,你也可以自定义 key: value 键值对。如果你需要一些提示,请查看我的网站的卷首。除了前面的问题,你还可以 使用内置的 Jekyll 变量 来自定义你的网站。

让我们创建一个新的帖子。在 _posts 文件夹下创建 2021-08-29-ayushsharma.markdown。内容如下:

---
layout: post
title:  "Check out ayushsharma.in!"
date:   2021-08-29 12:00:00 +0530
categories: mycategory
---
This is my first post.

# This is a heading.

## This is another heading.

This is a [link](<http://notes.ayushsharma.in>)

This is my category:

如果 jekyll serve 命令仍在运行,刷新页面,你将看到下面的新帖子。

 title=

恭喜你创建了你的第一篇帖子!这个过程看起来很简单,但是你可以通过 Jekyll 做很多事情。使用简单的 Markdown,你可以归档博客、高亮显示代码片段以及分类管理帖子。

草稿

如果你还没准备好发布你的内容,你可以创建一个 _drafts 文件夹。此文件夹中的 Markdown 文件仅通过传递 --drafts-- 参数来呈现。

布局和包含

请注意 _post 文件夹中两篇文章的卷首块,你将在其中看到 layout: post_layout 文件夹中包含所有布局。你不会在源代码中找到它们,因为 Jekyll 默认加载它们。Jekyll 使用的默认源代码在 这里。如果你点击该链接,你可以看到 post 的布局使用了默认(default)布局。默认布局包含的代码 {{ content }} 是注入内容的地方。布局文件还将包含 include 指令。它们从 include 文件夹 加载文件,并使用不同的组件组成页面。

总的来说,这就是布局的工作方式:你在卷首块定义它们并将你的内容注入其中。而包含则提供了页面的其它部分以组成整个页面。这是一种标准的网页设计技术:定义页眉、页脚、旁白和内容元素,然后在其中注入内容。这就是静态站点生成器的真正威力,完全以编程的方式控制,将你的网站组装起来并最终编译成静态的 HTML。

页面

你网站上的所有内容并不都是文章或博客。你需要“关于”页面、“联系”页面、“项目”页面或“作品”页面。这就是“页面”的用武之地。它们的工作方式与“帖子”完全一样,这意味着它们是带有卷首块的 Markdown 文件。但它们不会放到 _posts 目录。它们要么保留在你的项目根目录中,要么保留在它们自己的文件夹中。对于布局和包含,你可以使用与帖子相同的布局或创建新帖子。 Jekyll 非常灵活,你可以随心所欲地发挥你的创意!你的默认博客已经有 index.markdownabout.markdown。请随意自定义它们。

数据文件

数据文件位于 _data 目录中,可以是 .yml.json.csv 等格式的文件。例如,一个 _data/members.yml 文件可能包含:

- name: A
 github: a@a

- name: B
 github: b@b

- name: C
 github: c@c

Jekyll 在网站生成的时候读取这些内容。你可以通过 site.data.members 访问它们。

<ul>
{ % for member in site.data.members % }
 <li>
 <a href="https://github.com">
      { { member.name } } 
 </a>
 </li>
{ % endfor %}
</ul>

永久链接

你的 _config.yml 文件定义了永久链接的格式。你可以使用各种默认变量来组合你自己的自定义永久链接。

构建你最终的网站

命令 jekyll serve 非常适合本地测试。但是一旦你完成了本地测试,你将需要构建要发布的最终工作。命令 jekyll build --source source_dir --destination destination_dir 将你的网站构建到 _site 文件夹中。请注意,此文件夹在每次构建之前都会被清理,所以不要将重要的东西放在那里。生成内容后,你可以将其托管在你的静态托管服务上。

你现在应该对 Jekyll 的功能以及主要部分的功能有一个全面的了解。如果你正在寻找灵感,官方 JAMStack 网站上有一些很棒的例子

 title=

编码快乐。

本文首发于作者个人博客,经授权改编。


via: https://opensource.com/article/21/9/build-website-jekyll

作者:Ayush Sharma 选题:lujun9972 译者:perfiffer 校对:wxy

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

手动安装可以让用户更好的控制 Java 运行时环境。

 title=

使用 Linux 发行版的软件包管理工具来安装 Java 软件包 是很容易的。然而,有时你需要手动安装 Java。这对基于 Java 的应用服务器(如 Tomcat 或 JBoss)的管理员特别重要。许多开源和专有软件产品都依赖于这些服务。

开发者或运行时套件?

Java 虚拟机 Java Virtual Machine (JVM)以两种不同的形式提供: Java 开发工具包 Java Development Kit (JDK)或 Java 运行时环境 Java Runtime Environment (JRE)。

软件开发人员通常需要 JDK。它包含编译、运行和测试源代码所需的二进制文件。部署一个预先建立的 Java 应用程序通常只需要 JRE。它不包括编译器和其他开发工具。由于安全性的提高和空间的限制,通常在生产环境中安装 JRE。

获取 Java

你可以从网上下载开源的 Java 软件。你可以在 Red Hat DeveloperAdoptium.net 下载 OpenJDK 打包文件,或从 Azul 下载 Zulu 社区版

安装 Java

设置一个目录来存放 Java 文件。我喜欢创建一个简单的名为 java 的目录,这样我就可以在一个专门的目录中下载并解压打包文件:

$ mkdir -p java/jdk

让我们在这个例子中使用 JDK。将下载的文件保存到 jdk 目录下。然后换到该目录:

$ cd java/jdk
$ ls
OpenJDK11U-jdk_x64_linux_hotspot_11.0.12_7.tar.gz

提取该打包文件。注意,tar 会创建一个新的目录:

$ tar xvzf OpenJDK11U-jdk_x64_linux_hotspot_11.0.12_7.tar.gz
$ ls
jdk-11.0.12+7 OpenJDK11U-jdk_x64_linux_hotspot_11.0.12_7.tar.gz

使用 -version 选项确认新 JVM 的版本。

$ cd jdk-11.0.12+7/bin
$ ./java -version

JVM 的版本输出看起来类似这样:

openjdk version "11.0.12" 2021-07-20
OpenJDK Runtime Environment Temurin-11.0.12+7 (build 11.0.12+7)
OpenJDK 64-Bit Server VM Temurin-11.0.12+7 (build 11.0.12+7, mixed mode)

环境变量

为了确保一个特定的应用程序能够正常工作,它需要确切地知道如何定位 JVM。有两个主要的变量需要设置:JAVA_HOMEPATH

$ echo $JAVA_HOME
$ echo $PATH

这些可以在用户的 .bashrc 文件中设置。确保这些变量出现在 任何设置 PATH 的现有代码 之后:

#Set the JAVA_HOME
export JAVA_HOME=~/java/jdk/jdk-11.0.12+7
#Add the JAVA_HOME to the PATH
export PATH="$JAVA_HOME/bin:$PATH"

手动安装的情况

有几种情况需要手动安装 Java。请考虑以下三种情况:

一种情况可能是要求使用不同的,也许是较早的,在你的 Linux 发行版的软件库中已经没有的 Java 版本。

另一个例子可能是由安全推动的决定,即 Java 不会被默认安装在操作系统上或在“根级别”上。

第三种情况是可能需要几个不同版本的 Java,通常是因为 J2EE Web 应用程序的多个实例在同一台服务器上运行。由于越来越多地使用虚拟机和容器来隔离进程,这种操作系统共享在今天已经不太常见了。然而,由于需要维护不同的容器镜像,对手动安装的理解仍然至关重要。

总结

我演示了我手动安装 Java 运行时环境的方式,但你可以制定一个最适合你需求的惯例。最终,手动安装让用户可以更好的控制 Java 运行时环境。


via: https://opensource.com/article/21/9/install-java-manually-linux

作者:Alan Formy-Duval 选题:lujun9972 译者:wxy 校对:wxy

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

无论你喜欢哪个发行版和包管理器,都可以都很容易地在你的 Linux 系统上安装 Java。

 title=

把 Java 安装到你的 Linux 桌面上有多种方法。一个明显的方式是使用你的 Linux 发行版中提供的软件包。请注意,这并不适合所有人。例如,有些用户可能需要一个非常具体的 Java 版本。

在你开始之前,你必须确定你需要“哪种 Java”。你是否只需要运行一些 .class 文件或 .jar 文件?还是你正在编写一些需要编译的代码?

就我而言,我运行的大部分 Java 都是我自己(至少部分)编写的 Java,所以安装完整的 Java 开发工具包(或称 JDK)总是有意义的,它包含了 Java 编译器、库和一些非常有用的工具。当然,在这里,我们更倾向于使用开源的 JDK,称为 OpenJDK

由于我主要在 Ubuntu Linux 发行版上工作,我的软件包管理器是 apt。我可以用 apt 来查找哪些 OpenJDK 包是可用的:

apt list OpenJDK\*

这个命令的输出看起来像这样:

Listing... Done
openjdk-11-dbg/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-dbg/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-demo/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-demo/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-doc/hirsute-updates,hirsute-updates,hirsute-security,hirsute-security 11.0.11+9-0ubuntu2 all
openjdk-11-jdk-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-jdk-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-jdk/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-jdk/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-jre-dcevm/hirsute 11.0.10+1-1 amd64
openjdk-11-jre-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-jre-headless/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-jre-zero/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-jre-zero/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-jre/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 amd64
openjdk-11-jre/hirsute-updates,hirsute-security 11.0.11+9-0ubuntu2 i386
openjdk-11-source/hirsute-updates,hirsute-updates,hirsute-security,hirsute-security 11.0.11+9-0ubuntu2 all
openjdk-15-dbg/hirsute 15.0.3+3-1 amd64
openjdk-15-dbg/hirsute 15.0.3+3-1 i386
openjdk-15-demo/hirsute 15.0.3+3-1 amd64
...
openjdk-8-jre/hirsute-updates,hirsute-security 8u292-b10-0ubuntu1 i386
openjdk-8-source/hirsute-updates,hirsute-updates,hirsute-security,hirsute-security 8u292-b10-0ubuntu1 all

我在上面用 ... 省略了不少行。

事实证明,即使限制在 OpenJDK 中,我仍然有很多选择:

  • 不同的架构(在我的例子中,i386 还是 amd64)。
  • 不同的 Java 版本(就我而言,有 8、11、15、16、17 等)。
  • 纯粹的 OpenJDK 或无头版本。
  • Java 运行时环境(JRE)。
  • 用于调试、演示,以及是否包含源代码等。

同样,在我的情况中,我主要对纯粹的普通 OpenJDK 感兴趣。

假设我想为我的 amd64 架构安装 Java 11 版本的普通 OpenJDK,我可以输入:

sudo apt install -a=amd64 openjdk-11-jdk

几分钟后,我就可以编译、运行、调试和打包我的 Java 代码了。

注意,很有可能需要同时安装多个版本的 Java,有时甚至是必要的。在 Ubuntu 中,有一个有用的工具,叫做 update-java-alternatives,它可以 显示并配置在使用哪个 Java 环境

那些使用不同 Linux 发行版的人,一般来说,可以采取类似的方法。其他的几个发行版(如 Debian 和 Mint)也使用 apt ,尽管可用的软件包可能不同。发行版可能使用不同的软件包管理器。例如, Fedora 安装 Java 的文档页面 显示了如何使用 Fedora dnf 包管理器来处理安装。首先,为了显示可用的版本,输入:

dnf search openjdk

接下来,要安装完整的开发 x86\_64 架构版本,请输入:

sudo dnf install java-11-openjdk-devel.x86_64

同样地,Fedora 提供了 alternatives 工具来 显示和配置 Java 环境

再比如,很棒的 Arch Linux 维基 显示对应的软件包是 jdk11-openjdk。该维基还解释了许多在 Arch 中使用 Java 的其他重要细节,比如使用 archlinux-java 工具来显示安装了哪些 Java 环境或选择一个不同的默认环境。Arch 使用一个叫 pacman 的包管理器,它也有文档 在 Arch Linux 维基上

不管你喜欢哪个发行版和软件包管理器,在你的 Linux 系统上获得 Java 是很容易的。当然,在安装之前,要考虑版本和功能。还要记住,在同一台电脑上有管理两个或多个 Java 版本的方法。我的大多数例子都使用了 apt,但也要记得可以选择使用 dnf


via: https://opensource.com/article/21/9/install-java-linux-repositories

作者:Chris Hermansen 选题:lujun9972 译者:geekpi 校对:wxy

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