分类 软件开发 下的文章

如何使用 Java 开发工具包中的 Java 监控和管理控制台。

Java 开发工具包(JDK)提供了开发 Java 应用程序的二进制文件、工具和编译器。其中一个有用的工具是 jconsole

为了演示,我将使用 WildFly J2EE 应用服务器,它是 JBOSS 开源应用服务器项目的一部分。首先,我启动了一个独立的实例。

~/wildfly/24.0.1/bin$ ./standalone.sh
=========================================================================
  JBoss Bootstrap Environment
  JBOSS_HOME: /home/alan/wildfly/24.0.1
  JAVA: /usr/lib/jvm/java-11-openjdk-11.0.11.0.9-5.fc34.x86_64/bin/java

现在,在另一个终端,输入 jconsole

$ jconsole

启动后,jconsole 会列出本地实例。选择“ 本地进程 Local Process ”,然后选择进程的名称并点击“ 连接 Connect ”。这就是连接并开始使用运行中的 Java 虚拟机(JVM)的 jconsole 的全部过程。

 title=

概述

这个 Java 监控和管理控制台在仪表板的顶部显示进程标识符(PID)。“ 概述 Overview ”标签有四个图表,显示“ 堆内存使用情况 Heap Memory Usage ”、“ 线程 Threads ”、“ Classes ”和“ CPU 使用情况 CPU Usage ”的重要信息。

 title=

沿着顶部的标签提供每个区域的更详细的视图。

内存

内存 Memory ”标签显示 JVM 所使用的内存的各个方面的图表。分配给 JVM 的服务器系统内存量被称为“ Heap ”。这个屏幕还提供了关于堆的内部组件使用情况的详细信息,例如 “ 伊甸园 Eden Space ”、“ 老年代 Old Gen ” 和 “ 幸存者区 Survivor Space ”。你也可以手动请求一个垃圾收集动作。

 title=

线程

线程 Threads ”标签显示有多少线程在运行。你也可以手动检查是否存在死锁。

 title=

Classes ”标签告诉你有多少类被加载,有多少被卸载。

 title=

虚拟机摘要

虚拟机摘要 VM Summary ”标签提供了许多关于应用程序和主机系统的细节。你可以了解你所处的操作系统和架构、系统内存总量、CPU 数量,甚至交换空间。

 title=

摘要中显示的关于 JVM 的进一步细节,包括当前和最大的堆大小以及正在使用的垃圾收集器的信息。底部的窗格列出了传递给 JVM 的所有参数。

MBeans

最后一个标签,MBeans,让你通过所有的 MBeans 向下钻取,以查看每个 MBeans 的属性和值。

 title=

总结

Java 已经存在了很长时间,它将继续为全球数百万的系统提供动力。有很多开发环境和监控系统可以使用,但像 jconsole 这样的包含在基本工具包中的工具非常有价值。


via: https://opensource.com/article/21/10/monitor-java-linux-jconsole

作者:Alan Formy-Duval 选题: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中国 荣誉推出

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

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

YAML 看起来很简单,为什么它这么难写呢?了解成功使用 YAML 的两个秘诀。

 title=

如果你曾经尝试过写 YAML,你可能一开始会对它看起来很容易感到高兴。乍一看,经常用于配置文件、Ansible 剧本和普通文件数据库的 YAML 看起来就像购物清单一样直观。然而,YAML 的结构中有很多细微的差别,它隐藏着一个危险的秘密:YAML 实际上是一种高度精确、结构化和令人惊讶的严格语言。好消息是,你只需要了解两件事就可以知道 YAML 的工作原理。

关于 YAML 的真相是,YAML 中只有两种数据结构: 序列 sequence 映射 mapping 。这是两个花哨的名字,你会发现它代表了你非常熟悉的概念。这篇文章解释了这两种结构,更重要的是,介绍了它们是如何协同工作,使 YAML 成为表示你所关心的数据的强大方式。

YAML 序列

YAML 序列是一个列表。在其最简单的形式中,每行有一个项目,每行以破折号和空格开始。

下面是一个例子:

---
- Linux
- BSD
- Illumos

不同的语言有不同的方式来表示这种数据。例如,在 Python 中,同一个列表可以写成 ['Linux', 'BSD', 'Illumos']。当你在现实生活中写一个列表时,例如在你去买菜之前,你写的可能近似于 YAML 序列。

YAML 映射

YAML 映射是一个关键术语与该术语的定义相结合。在其他语言中,映射被称为“键值对”或“词典”。

这里有一个例子:

---
Kernel: Linux
CPU: AMD
RAM: '16 GB'

不同的语言有不同的方式来表示这种数据。在 Python 中,例如,同样的数据可以写成 {"Kernel": "Linux", "CPU": "AMD", "RAM": "16 GB"}。在现实生活中,你可能会使用这种结构来计划,例如,与朋友的游戏之夜。一个朋友报名带零食,另一个报名带一副牌,另一个报名带一个棋盘游戏,等等。

组合序列和映射

你现在知道 YAML 的语法了。序列和映射是 YAML 中仅有的两种构件,你想在 YAML 中表示的任何东西都可以放在序列或映射中。

或者同时使用这二者!

是的,序列和映射可以被组合和嵌套,这就是 YAML 看起来很直观,但同时又感觉很复杂的原因之一。不过,只有四种可能的组合,一旦你学会如何看它们,YAML 就会觉得像它看起来一样简单。

序列的映射

当你想让一个键项有许多值时,你可以使用一个序列的映射。也就是说,你从一个映射(键)开始,但是给值一个列表:

---
Linux:
  - Fedora
  - Slackware
BSD:
  - FreeBSD
  - NetBSD

在这个示例代码中,Linux 是第一个键,它的值是一个序列,其中包含 FedoraSlackware。第二个键是 BSD,它的值是一个序列,包含 FreeBSDNetBSD

映射的映射

当你想让一个键项的值中既有键又有值时,你可以使用映射的映射。也就是说,你从一个映射(键)开始,但是给值另一个映射。

这可能有点欺骗性,但它揭示了 YAML 中使用特定术语的原因:因为你只不过是创建了一个映射的列表,并不意味着你创建了一个序列。这里是一个映射的映射:

---
Desktop:
  CPU: RISC-V
  RAM: '32 GB'
Laptop:
  CPU: AMD
  RAM: '16 GB'

对大多数人来说,这看起来像一个列表。而且从技术上讲,它是一个列表。但重要的是要认识到,它不是 YAML 序列。它是一个映射,其中包含映射。作为半个 YAML 专家,你可以从明显缺少破折号的地方看出区别。

在 Ansible 剧本的所有结构中,我发现这个结构最容易欺骗人。作为人类,我们喜欢列表,当我们看到一个数据结构 在字面上 是列表时,大多数人会把它当成 YAML 序列。但是在 YAML 中,虽然序列是列表,但是列表并不总是序列。

序列的序列

就像你可以嵌套映射一样,你可以将一个序列嵌套到一个序列中:

---
- [Linux, FreeBSD, Illumos]
- [YAML, XML, JSON]

这可能是我在 YAML 的实际使用中遇到的最不常见的数据结构,但有时你需要一个列表的列表。

映射的序列

你也可以创建一个包含映射的序列。对于人类排序数据的方式来说,这并不太常见,但对于计算机来说,这可能是一个重要的结构。

这里有一个例子:

---
-
  CPU: AMD
  RAM: '16 GB'
-
  CPU: Intel
  RAM: '16 GB'

对于 YAML,这可能是最不直观的语法。我发现它在 Python 中呈现时更清晰:

[{"CPU": "AMD", "RAM": "16 GB"}, {"CPU": "Intel", "RAM": "16 GB"}]

方括号代表一个列表结构,这个列表包含两个字典。每个字典都包含键值对。

构建更好的 YAML

现在你知道了 YAML 的两个组成部分,以及它们如何被组合起来以表示复杂的数据结构。问题是:你要用 YAML 构建什么?

和很多人一样,我也使用 YAML 来编写 Ansible 剧本。我还用它作为一种简单的配置格式、作为 D&D 的角色表、表示项目组织所需的目录结构,等等。只要你能适应序列和映射的概念,你会发现 YAML 是一种很容易编写、阅读和(如果有合适的库)解析的格式。

如果你发现自己经常使用 YAML,请下载我们的 YAML 速查表,以帮助你直观地了解基本数据结构及其组合,并帮助你记住一些额外的语法约定。通过一点点的练习,你会发现 YAML 真的和它看起来一样简单!


via: https://opensource.com/article/21/9/yaml-cheat-sheet

作者:Seth Kenlon 选题:lujun9972 译者:wxy 校对:wxy

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

流行的配置文件格式有若干种,每种都有其自身优势。从中找到最适合你的格式吧!

 title=

计算机上有数以千计的配置文件。你可能永远不会直接与其中的大部分文件打交道,但它们确实散落在你的 /etc 以及 ~/.config~/.local/usr 文件夹中。还有一些可能在 /var,甚至 /opt 文件夹中。如果无意中打开过或更改过它们,你就可能会有疑问:为什么有些配置文件看起来是某一种格式,而另一些则是看起来完全不同的格式?

存储配置是一项很灵活的任务,因为只要开发人员知道他们的代码是如何将数据存入文件的,他们就可以轻松编写代码来根据需要提取数据。然而,科技行业非常青睐有详细文档的标准化事物,因此多年来出现了几种比较普遍的格式用来简化配置任务。

为什么我们需要配置文件

配置文件对于现代计算来说很重要。它们使你能够自定义与应用程序交互的方式,或自定义应用程序与系统内其他程序的交互方式。有了配置文件,每当你启动某个应用程序时,它都会有“记忆”,记录了你喜欢如何去使用该程序。

配置文件的结构可以很简单,而且通常确实也很简单。例如,如果你要编写一个应用程序,程序唯一需要知道的是其用户的偏好名字,那么它的唯一配置文件就可以只包含一个词:用户名。就像下面这样:

Tux

但通常应用程序需要追踪的不仅仅是一条信息,因此配置文件通常会有一个键和一个值:

NAME='Tux'
SPECIES='Penguin'

即使没有编程经验,你也可以想象出代码如何解析这些数据。这里有两个简单的例子,一个使用 awk 命令,另一个使用 grep 命令。两个例子都是只关注包含 NAME “键”的行,并返回出现在等号 (=) 之后的“值”:

$ awk -F'=' '/NAME/ { print $2; }' myconfig.ini
'Tux'
$ grep NAME fake.txt | cut -d'=' -f2
'Tux'

同样的原则适用于任何编程语言和任何配置文件。只要你有统一的数据结构,就可以在需要的时候编写简单的代码来提取和解析它。

选择格式

为了保证普遍有效性,配置文件最重要的一点是它们是一致的和可预测的。你绝对不会想做这样的事:以保存用户首选项的名义,将信息随意存储到文件中,然后花好几天时间逆向工程,来找到最终出现在文件中的随机信息。

流行的配置文件格式有若干种,每种格式都有自己的优势。

INI

INI 文件采用了键值对的格式:

[example]
name=Tux
style=widgety,fidgety
enabled=1

这种简单的配置风格很直观,只要你别选择使用糟糕的键名(比如用 unampref 这样的神秘键名来代替 name)就好。这些键值对很容易解析和编辑。

除了键和值之外,INI 格式还可以分 section 。在下列示例代码中,[example][demo] 就是配置文件中的两个节:

[example]
name=Tux
style=widgety,fidgety
enabled=1

[demo]
name=Beastie
fullscreen=1

这几个配置语句解析起来有点复杂,因为有两个 name 键。想象一下,一个粗心的程序员在这个配置文件中查询 name,结果总是返回 Beastie,因为这是文件中对 name 的最后一个定义值。在解析这样的文件时,开发人员必须加倍小心地在各节中搜索键,这可能会很棘手,具体取决于用来解析该文件的语言。然而,它仍然是一种很流行的格式,大多数语言都会有一个现成的库来帮助程序员解析 INI 文件。

YAML

YAML 文件 是结构化列表,可以包含值或者键值对:

---
Example:
  Name: 'Tux'
  Style:
   - 'widgety'
    - 'fidgety'
  Enabled: 1

YAML 格式很流行,部分原因是它看起来很整洁。数据要放置到相对其上层数据的特定位置,除此之外没有太多其他语法。然而,对于某些人来说的这种特色,在其他人眼中可能就是一个问题。许多开发人员不愿使用 YAML,正是因为它很看重本质上 不存在 的东西。如果你在 YAML 中缩进错误,YAML 解析器可能会将你的文件视为无效文件,即使不视为无效,返回的数据也可能是错误的。

大多数语言都有 YAML 解析器,并且有很好的开源 YAML linters(验证语法的应用程序)来帮你确保 YAML 文件的完整性。

JSON

JSON 文件在技术上来说是 YAML 的子集,因此其数据结构是相同的,尽管其语法完全不同:

{
  "Example": {
    "Name": [
      "Tux"
    ],
    "Style": [
      "widgety",
      "fidgety"
    ],
    "Enabled": 1
  }
}

JSON 在 JavaScript 程序员中很流行,这并不奇怪,因为 JSON 全称为 JavaScript 对象符号 JavaScript Object Notation 。由于与 Web 开发密切相关,JSON 是 Web API 的常见输出格式。大多数编程语言都有解析 JSON 的库。

XML

XML 使用标签作为键,将配置值围绕起来:

<example>
  <name>Tux</name>
  <style priority="user">widgety</style>
  <style priority="fallback">fidgety</style>
  <enabled>1</enabled>
</example>

XML 经常被 Java 程序员使用,Java 有一套丰富的 XML 解析器。虽然 XML 以非常严格而著称,但同时也非常灵活。与有一系列特定标签的 HTML 不同,XML 中可以随意发明自己的标签。只要始终坚持相同的构建规则,并有一个良好的库来解析它,你就可以准确而轻松地提取数据。

有一些很好的开源 linter 可以帮你验证 XML 文件,并且大多数编程语言都提供用于解析 XML 的库。

二进制格式

Linux 以纯文本配置为傲。这样做的优点是可以使用 cat 等基本工具查看配置数据,甚至可以使用你 最喜欢的文本编辑器 来编辑配置。

但是,某些应用程序使用二进制格式配置,就意味着数据以某种非自然语言的格式进行编码。这些文件通常需要一个特殊的应用程序(通常是它们要配置的应用程序)来解释它们的数据。你无法查看这些文件,至少无法以任何有意义的方式查看,并且无法在其宿主应用程序之外编辑它们。选用二进制格式的一些原因如下:

  • 速度: 程序员可以使用自定义符号在二进制配置文件中的某些点注册特定的信息位。提取数据时不涉及搜索,因为所有内容都已标注了索引。
  • 大小: 文本文件可能会变大,如果选择压缩文本文件,实际上是在将其转换为二进制格式。二进制文件可以通过编码技巧变得更小(文本文件也是如此,但在某些时候,你的优化会使数据变得晦涩,以至于文件也成了二进制文件)。
  • 晦涩: 一些程序员甚至不希望人们查看他们的配置文件,因此将它们编码为二进制数据。这通常只会让用户感到沮丧,并不是使用二进制格式的好理由。

如果必须使用二进制格式进行配置,请使用已作为开放标准存在的格式,例如 NetCDF

找到有效的配置格式

配置格式帮助开发人员存储应用程序所需的数据,并帮助用户存储他们希望应用程序如何操作的偏好项。对于应该使用什么格式的问题,可能没有错误的答案,只要你觉得所使用的语言能很好地支持就可以。在开发应用程序时,查看可用格式,用一些样例数据建模,查看和评估你的编程语言提供的库和实用程序,然后选择你觉得最合适的一种格式吧。


via: https://opensource.com/article/21/6/what-config-files

作者:Seth Kenlon 选题:lujun9972 译者:unigeorge 校对:wxy

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

如果你正在用 React.js 或 React Native 来开发用户界面的话,试试这些框架。

 title=

React.js 和 React Native 都是用来开发用户界面(UI)的很受欢迎的开源平台。在 StackOverflow 2019 年度开发者调查里,两个框架的可取性和使用情况都排名靠前。React.js 是 Facebook 在 2011 年开发的一个 JavaScript 库,来实现跨平台,动态以及高性能的 UI 设计需求;而 React Native 则是 Facebook 在 2015 年发布的框架,目的是使用 JavaScript 构建原生应用。

下面介绍 13 个最好的 React JavaScript 框架,都是开源项目。前 11 个(和 React 一样)都使用 MIT 许可证授权,后面两个使用 Apache 2.0 许可证。

1、Create React App

这个 Facebook 开发的命令行工具是 React Native 项目一定要用的。因为 Create React App 使用很简单,还可以避免你自己手动设定和配置应用,因此能节省大量的时间和精力。仅仅使用给一个简单的命令,就可以为你准备好创建 React 原生项目所需的一切。你可以用它来创建分类和文件,而且该框架还自带了工具用来构建,测试和启动应用。

# 安装软件包
$ npm install -g create-react-native-web-app
 
# 运行 create-react-native-web-app <项目目录>
$ create-react-native-web-app myApp
 
# 切换到创建的 <项目目录>
$ cd myApp
 
# 运行 Web/Ios/Android 调试
# Web
$ npm run web
 
# IOS(模拟)
$ npm run ios
 
# Android(实际连接的设备)
$ npm run android

为什么选择 Create React App

  1. 使用配置包、转码器,以及测试运行器进行开发的一流工具
  2. 在应用架构里不需要配置以及没有额外文件
  3. 确定的开发栈
  4. 高效快速的开发工具

2、Material Kit React

Material Kit React 是受谷歌的 Material Design 系统启发开发的,很适合用来创建 React UI 组件。这个库最大的优点是提供了大量的组件,可以互相搭配做出非常好的效果。有超过一千个完全编码的组件,每一个都有用文件夹组织起来的独立层。这样你就可以有上千个选项可以选择。它同时也包含一些示例页面,方便你从中寻找灵感,或者向别人分享你的点子或创意。

安装 Material Kit

$ npm install @material-ui/core

使用

import React from 'react';
import Button from '@material-ui/core/Button';

const App = () => (
  <Button variant="contained" color="primary">
    Hello World
  </Button>
);

Material-UI 组件不需要其他额外设置,也不会干扰全局变量空间。

优点

这个 React 组件支持简易快速的网页开发。你可以用它创建自己的设计系统,或者直接开始 Material Design。

3、Shards React

这个现代的 React UI 工具为了追求高效率,是从最底层开始构建的。它拥有现代的设计系统,可以让你按自己的想法任意定制。你甚至可以下载源文件,然后从代码级别定制。另外,它用来设计样式的 SCSS 语法提高了开发体验。

Shards React 基于 Shards,使用了 React Datepicker、React Popper(一个定位引擎)和 noUISlider。还带有非常优秀的 Material Design 图标。还有很多设计好的版本,可以帮你寻找灵感或上手。

用 Yarn 或 NPM 安装 Shards

# Yarn
yarn add shards-react

# NPM
npm i shards-react

优点

  1. Shards 是一个轻量级的脚本,压缩后大概 13kb
  2. Shards 默认支持响应式,图层可以适配任意大小屏幕
  3. Shards 有完整的文档,可以快速开始构建漂亮的界面

4、Styled Components

这个高效的 CSS 工具可以用来为应用的可视界面创建小型可重用的组件。使用传统的 CSS,你可能会不小心覆盖掉网站其他位置的选择器,但 Styled Components 通过使用直接内嵌到组件里的 CSS 语法,可以完全避免这个问题。

安装

npm install --save styled-components

使用

const Button = styled.button`
  background: background_type;
  border-radius: radius_value;
  border: abc;
  color: name_of_color;
  Margin: margin_value;
  padding: value`;

优点

  1. 让组件有更好的可读性
  2. 组件样式依赖 JavaScript
  3. 创建定制 CSS 组件
  4. 内嵌样式
  5. 简单地调用 styled() 可以将组件甚至是自定义组件转换成样式组件

5、Redux

Redux 是一个为 JavaScript 应用提供状态管理的方案。常用于 React.js,也可以用在其他类 React 框架里。

安装

sudo npm install redux
sudo npm install react-redux

使用

import { createStore } from "redux";
import rotateReducer from "reducers/rotateReducer";

function configureStore(state = { rotating: value}) {
  return createStore(rotateReducer,state);
}

export default configureStore;

优点

  1. 可预计的状态更新有助于定义应用里的数据流
  2. 逻辑上测试更简单,使用 reducer 函数进行时间旅行调试也更容易
  3. 统一管理状态

6、React Virtualized

这个 React Native JavaScript 框架帮助渲染 large-list 和 tabular-data。使用 React Virtualized,你可以限制请求和文档对象模型(DOM)元素的数量,从而提高 React 应用的性能。

安装

npm install react-virtualized

使用

import 'react-virtualized/styles.css'
import { Column, Table } from 'react-virtualized'
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'
import List from 'react-virtualized/dist/commonjs/List'
{
  alias: {
    'react-virtualized/List': 'react-virtualized/dist/es/List',
  },
  ...等等
}

优点

  1. 高效展示大量数据
  2. 渲染超大数据集
  3. 使用一系列组件实现虚拟渲染

7、React DnD

React DnD 用来创建复杂的拖放界面。拖放控件库有很多,选用 React DnD 是因为它是基于 HTML5 的拖放 API 的,创建界面更简单。

安装

npm install react-dnd-preview

使用

import Preview from 'react-dnd-preview';
 
const generatePreview = ({itemType, item, style}) => {
  return <div class="item-list" style={style}>{itemType}</div>;
};

class App extends React.Component {
 ...
  render() {
    return (
      <DndProvider backend={MyBackend}>
        <ItemList />
        <Preview generator={generatePreview} />
        // or
        <Preview>{generatePreview}</Preview>
      </DndProvider>
    );
  }
}

优点

  1. 漂亮自然的控件移动
  2. 强大的键盘和屏幕阅读支持
  3. 极限性能
  4. 强大整洁的接口
  5. 标准浏览器支持非常好
  6. 中性样式
  7. 没有额外创建 DOM 节点

8、React Bootstrap

这个 UI 库将 Bootstrap 的 JavaScript 替换成了 React,可以更好地控制每个组件的功能。每个组件都构建成能轻易访问,因此 React Bootstrap 有利于构建前端框架。有上千种 bootstrap 主题可以选择。

安装

npm install react-bootstrap bootstrap

使用

import 'bootstrap/dist/css/bootstrap.min.css';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

优点

  1. 可以简单导入所需的代码/组件
  2. 通过压缩 Bootstrap 节省了输入和问题
  3. 通过压缩 Bootstrap 减少了输入工作和冲突
  4. 使用简单
  5. 使用元素封装

9、React Suite

React Suite 是另一个高效的 React.js 框架,包含了大量组件库,方便开发企业级产品。支持所有主流浏览器和平台,适用于任何系统。还支持服务器端渲染。

安装

npm i rsuite --save

使用

import { Button } from 'rsuite';
import 'rsuite/styles/less/index.less';
ReactDOM.render(<Button>Button</Button>, mountNode);

优点

  1. 通过全局访问特性轻松管理应用
  2. 使用 Redux 库集中管理系统状态
  3. Redux 库有灵活的 UI 层,以及广泛的生态
  4. Redux 库减少系统复杂度,并提供了全局访问特性

10、PrimeReact

PrimeReact 最值得推荐的是它提供了几乎覆盖所有基本 UI 需求的组件,比如输入选项,菜单,数据展示,消息,等等。这个框架还优化了移动体验,帮助你设计触摸优化的元素。

安装

npm install primereact --save
npm install primeicons --save

使用

import {Dialog} from 'primereact/dialog';
import {Accordion,AccordionTab} from 'primereact/accordion';
dependencies: {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "react-transition-group": "^2.2.1",
    "classnames": "^2.2.5",
    "primeicons": "^2.0.0"
}

优点

  1. 简单而高效
  2. 容易使用
  3. Spring 应用
  4. 创建复杂用户界面
  5. 好用而简单

11、React Router

React Router 在 React Native 开发社区很受欢迎,因为它上手很容易。只需要你在电脑上安装 Git 和 npm 包管理工具,有 React 的基础知识,以及好学的意愿。没什么特别难的地方。

安装

$ npm install --save react-router

使用

import { Router, Route, Switch } from "react-router";
 
// 使用 CommonJS 模块
var Router = require("react-router").Router;
var Route = require("react-router").Route;
var Switch = require("react-router").Switch;

优点

  1. 动态路由匹配
  2. 在导航时支持不同页面的 CSS 切换
  3. 统一的应用结构和行为

12、Grommet

Grommet 常用于开发响应式、可访问的移动网页应用。这个用 Apache 2.0 许可证授权的 JavaScript 框架最大的优点是用很小的包提供了可访问性、模块化、响应式以及主题功能。这可能是它被一些公司广泛使用的主要原因,比如奈飞、通用电气、优步以及波音。

安装 for yarn and npm

$ npm install grommet styled-components --save

使用

"grommet-controls/chartjs": {
          "transform": "grommet-controls/es6/chartjs/${member}",
          "preventFullImport": true,
          "skipDefaultConversion": true

优点

  1. 创建一个工具包来打包
  2. 把开放政策发挥到极致
  3. 重构有助于影响已成立的组织

13、Onsen UI

Onsen UI 另一个使用 HTML5 和 JavaScript 的手机应用开发框架,集成了 Angular、Vue 和 React,使用 Apache 2.0 许可证授权。

Onsen 提供了标签、侧边栏、堆栈导航以及其他组件。这个框架最好的地方是,它所有的组件都支持 iOS 和安卓 Material Design 自动适配,会根据不同的平台切换应用的外观。

安装

npm install onsenui

使用

(function() {
    'use strict';
    var module = angular.module('app', ['onsen']);

    module.controller('AppController', function($scope) {
      // more to come here
    });

})();

优点

  1. Onsen UI 基于自由而开源代码
  2. 不强制基于它开发的应用使用任何形式的 DRM
  3. 内置了 JavaScript 和 HTML5 代码
  4. 给最终用户带来原生体验

你最喜欢哪个 React JavaScript 框架?请在评论区分享。


via: https://opensource.com/article/20/1/react-javascript-frameworks

作者:Amit Dua 选题:lujun9972 译者:zpl1025 校对:wxy

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