2019年9月

变量通常看起来像 $var 这样,但它们也有 $1、$\*、$? 和 $$ 这种形式。让我们来看看所有这些 $ 值可以告诉你什么。

有许多重要的值都存储在 Linux 系统中,我们称为“变量”,但实际上变量有几种类型,并且一些有趣的命令可以帮助你使用它们。在上一篇文章中,我们研究了环境变量以及它们定义在何处。在本文中,我们来看一看在命令行和脚本中使用的变量。

用户变量

虽然在命令行中设置变量非常容易,但是有一些有趣的技巧。要设置变量,你只需这样做:

$ myvar=11
$ myvar2="eleven"

要显示这些值,只需这样做:

$ echo $myvar
11
$ echo $myvar2
eleven

你也可以使用这些变量。例如,要递增一个数字变量,使用以下任意一个命令:

$ myvar=$((myvar+1))
$ echo $myvar
12
$ ((myvar=myvar+1))
$ echo $myvar
13
$ ((myvar+=1))
$ echo $myvar
14
$ ((myvar++))
$ echo $myvar
15
$ let "myvar=myvar+1"
$ echo $myvar
16
$ let "myvar+=1"
$ echo $myvar
17
$ let "myvar++"
$ echo $myvar
18

使用其中的一些,你可以增加一个变量的值。例如:

$ myvar0=0
$ ((myvar0++))
$ echo $myvar0
1
$ ((myvar0+=10))
$ echo $myvar0
11

通过这些选项,你可能会发现它们是容易记忆、使用方便的。

你也可以删除一个变量 – 这意味着没有定义它。

$ unset myvar
$ echo $myvar

另一个有趣的选项是,你可以设置一个变量并将其设为只读。换句话说,变量一旦设置为只读,它的值就不能改变(除非一些非常复杂的命令行魔法才可以)。这意味着你也不能删除它。

$ readonly myvar3=1
$ echo $myvar3
1
$ ((myvar3++))
-bash: myvar3: readonly variable
$ unset myvar3
-bash: unset: myvar3: cannot unset: readonly variable

你可以使用这些设置和递增选项中来赋值和操作脚本中的变量,但也有一些非常有用的内部变量可以用于在脚本中。注意,你无法重新赋值或增加它们的值。

内部变量

在脚本中可以使用很多变量来计算参数并显示有关脚本本身的信息。

  • $1$2$3 等表示脚本的第一个、第二个、第三个等参数。
  • $# 表示参数的数量。
  • $* 表示所有参数。
  • $0 表示脚本的名称。
  • $? 表示先前运行的命令的返回码(0 代表成功)。
  • $$ 显示脚本的进程 ID。
  • $PPID 显示 shell 的进程 ID(脚本的父进程)。

其中一些变量也适用于命令行,但显示相关信息:

  • $0 显示你正在使用的 shell 的名称(例如,-bash)。
  • $$ 显示 shell 的进程 ID。
  • $PPID 显示 shell 的父进程的进程 ID(对我来说,是 sshd)。

为了查看它们的结果,如果我们将所有这些变量都放入一个脚本中,比如:

#!/bin/bash

echo $0
echo $1
echo $2
echo $#
echo $*
echo $?
echo $$
echo $PPID

当我们调用这个脚本时,我们会看到如下内容:

$ tryme one two three
/home/shs/bin/tryme     <== 脚本名称
one                     <== 第一个参数
two                     <== 第二个参数
3                       <== 参数的个数
one two three           <== 所有的参数
0                       <== 上一条 echo 命令的返回码
10410                   <== 脚本的进程 ID
10109                   <== 父进程 ID

如果我们在脚本运行完毕后检查 shell 的进程 ID,我们可以看到它与脚本中显示的 PPID 相匹配:

$ echo $$
10109                   <== shell 的进程 ID

当然,比起简单地显示它们的值,更有用的方式是使用它们。我们来看一看它们可能的用处。

检查是否已提供参数:

if [ $# == 0 ]; then
    echo "$0 filename"
    exit 1
fi

检查特定进程是否正在运行:

ps -ef | grep apache2 > /dev/null
if [ $? != 0 ]; then
    echo Apache is not running
    exit
fi

在尝试访问文件之前验证文件是否存在:

if [ $# -lt 2 ]; then
    echo "Usage: $0 lines filename"
    exit 1
fi

if [ ! -f $2 ]; then
    echo "Error: File $2 not found"
    exit 2
else
    head -$1 $2
fi

在下面的小脚本中,我们检查是否提供了正确数量的参数、第一个参数是否为数字,以及第二个参数代表的文件是否存在。

#!/bin/bash

if [ $# -lt 2 ]; then
    echo "Usage: $0 lines filename"
    exit 1
fi

if [[ $1 != [0-9]* ]]; then
    echo "Error: $1 is not numeric"
    exit 2
fi

if [ ! -f $2 ]; then
    echo "Error: File $2 not found"
    exit 3
else
    echo top of file
    head -$1 $2
fi

重命名变量

在编写复杂的脚本时,为脚本的参数指定名称通常很有用,而不是继续将它们称为 $1$2 等。等到第 35 行,阅读你脚本的人可能已经忘了 $2 表示什么。如果你将一个重要参数的值赋给 $filename$numlines,那么他就不容易忘记。

#!/bin/bash

if [ $# -lt 2 ]; then
    echo "Usage: $0 lines filename"
    exit 1
else
    numlines=$1
    filename=$2
fi

if [[ $numlines != [0-9]* ]]; then
    echo "Error: $numlines is not numeric"
    exit 2
fi

if [ ! -f $ filename]; then
    echo "Error: File $filename not found"
    exit 3
else
    echo top of file
    head -$numlines $filename
fi

当然,这个示例脚本只是运行 head 命令来显示文件中的前 x 行,但它的目的是显示如何在脚本中使用内部参数来帮助确保脚本运行良好,或在失败时清晰地知道失败原因。


via: https://www.networkworld.com/article/3387154/working-with-variables-on-linux.html#tk.rss_all

作者:Sandra Henry-Stocker 选题:lujun9972 译者:MjSeven 校对:wxy

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

GIC 是一个聊天应用程序的原型,展示了一种使用 Git 的新方法。

Git 是一个少有的能将如此多的现代计算封装到一个程序之中的应用程序,它可以用作许多其他应用程序的计算引擎。虽然它以跟踪软件开发中的源代码更改而闻名,但它还有许多其他用途,可以让你的生活更轻松、更有条理。在这个 Git 系列中,我们将分享七种鲜为人知的使用 Git 的方法。

今天我们来看看 GIC,它是一个基于 Git 的聊天应用。

初识 GIC

虽然 Git 的作者们可能期望会为 Git 创建前端,但毫无疑问他们从未预料到 Git 会成为某种后端,如聊天客户端的后端。然而,这正是开发人员 Ephi Gabay 用他的实验性的概念验证应用 GIC 所做的事情:用 Node.js 编写的聊天客户端,使用 Git 作为其后端数据库。

GIC 并没有打算用于生产用途。这纯粹是一种编程练习,但它证明了开源技术的灵活性。令人惊讶的是,除了 Node 库和 Git 本身,该客户端只包含 300 行代码。这是这个聊天客户端和开源所反映出来的最好的地方之一:建立在现有工作基础上的能力。眼见为实,你应该自己亲自来了解一下 GIC。

架设起来

GIC 使用 Git 作为引擎,因此你需要一个空的 Git 存储库为聊天室和记录器提供服务。存储库可以托管在任何地方,只要你和需要访问聊天服务的人可以访问该存储库就行。例如,你可以在 GitLab 等免费 Git 托管服务上设置 Git 存储库,并授予聊天用户对该 Git 存储库的贡献者访问权限。(他们必须能够提交到存储库,因为每个聊天消息都是一个文本的提交。)

如果你自己托管,请创建一个中心化的裸存储库。聊天中的每个用户必须在裸存储库所在的服务器上拥有一个帐户。你可以使用如 GitoliteGitea 这样的 Git 托管软件创建特定于 Git 的帐户,或者你可以在服务器上为他们提供个人用户帐户,可以使用 git-shell 来限制他们只能访问 Git。

自托管实例的性能最好。无论你是自己托管还是使用托管服务,你创建的 Git 存储库都必须具有一个活跃分支,否则 GIC 将无法在用户聊天时进行提交,因为没有 Git HEAD。确保分支初始化和活跃的最简单方法是在创建存储库时提交 README 或许可证文件。如果你没有这样做,你可以在事后创建并提交一个:

$ echo "chat logs" > README
$ git add README
$ git commit -m 'just creating a HEAD ref'
$ git push -u origin HEAD

安装 GIC

由于 GIC 基于 Git 并使用 Node.js 编写,因此必须首先安装 Git、Node.js 和 Node 包管理器npm(它应该与 Node 捆绑在一起)。安装它们的命令因 Linux 或 BSD 发行版而异,这是 Fedora 上的一个示例命令:

$ sudo dnf install git nodejs

如果你没有运行 Linux 或 BSD,请按照 git-scm.comnodejs.org 上的安装说明进行操作。

因此,GIC 没有安装过程。每个用户(在此示例中为 Alice 和 Bob)必须将存储库克隆到其硬盘驱动器:

$ git clone https://github.com/ephigabay/GIC GIC

将目录更改为 GIC 目录并使用 npm 安装 Node.js 依赖项:

$ cd GIC
$ npm install

等待 Node 模块下载并安装。

配置 GIC

GIC 唯一需要的配置是 Git 聊天存储库的位置。编辑 config.js 文件:

module.exports = {
  gitRepo: '[email protected]:/home/gitchat/chatdemo.git',
  messageCheckInterval: 500,
  branchesCheckInterval: 5000
};

在尝试 GIC 之前测试你与 Git 存储库的连接,以确保你的配置是正确的:

$ git clone --quiet [email protected]:/home/gitchat/chatdemo.git > /dev/null

假设你没有收到任何错误,就可以开始聊天了。

用 Git 聊天

在 GIC 目录中启动聊天客户端:

$ npm start

客户端首次启动时,必须克隆聊天存储库。由于它几乎是一个空的存储库,因此不会花费很长时间。输入你的消息,然后按回车键发送消息。

 title=

基于 Git 的聊天客户端。 他们接下来会怎么想?

正如问候消息所说,Git 中的分支在 GIC 中就是聊天室或频道。无法在 GIC 的 UI 中创建新分支,但如果你在另一个终端会话或 Web UI 中创建一个分支,它将立即显示在 GIC 中。将一些 IRC 式的命令加到 GIC 中并不需要太多工作。

聊了一会儿之后,可以看看你的 Git 存储库。由于聊天发生在 Git 中,因此存储库本身也是聊天日志:

$ git log --pretty=format:"%p %cn %s"
4387984 Seth Kenlon Hey Chani, did you submit a talk for All Things Open this year?
36369bb Chani No I didn't get a chance. Did you?
[...]

退出 GIC

Vim 以来,还没有一个应用程序像 GIC 那么难以退出。你看,没有办法停止 GIC。它会一直运行,直到它被杀死。当你准备停止 GIC 时,打开另一个终端选项卡或窗口并发出以下命令:

$ kill `pgrep npm`

GIC 是一个新奇的事物。这是一个很好的例子,说明开源生态系统如何鼓励和促进创造力和探索,并挑战我们从不同角度审视应用程序。尝试下 GIC,也许它会给你一些思路。至少,它可以让你与 Git 度过一个下午。


via: https://opensource.com/article/19/4/git-based-chat

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

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

本教程讨论了 Linux 中交换文件的概念,为什么使用它以及它相对于传统交换分区的优势。你将学习如何创建交换文件和调整其大小。

什么是 Linux 的交换文件?

交换文件允许 Linux 将磁盘空间模拟为内存。当你的系统开始耗尽内存时,它会使用交换空间将内存的一些内容交换到磁盘空间上。这样释放了内存,为更重要的进程服务。当内存再次空闲时,它会从磁盘交换回数据。我建议阅读这篇文章,了解 Linux 上的交换空间的更多内容

传统上,交换空间是磁盘上的一个独立分区。安装 Linux 时,只需创建一个单独的分区进行交换。但是这种趋势在最近几年发生了变化。

使用交换文件,你不再需要单独的分区。你会根目录下创建一个文件,并告诉你的系统将其用作交换空间就行了。

使用专用的交换分区,在许多情况下,调整交换空间的大小是一个可怕而不可能的任务。但是有了交换文件,你可以随意调整它们的大小。

最新版本的 Ubuntu 和其他一些 Linux 发行版已经开始 默认使用交换文件。甚至如果你没有创建交换分区,Ubuntu 也会自己创建一个 1GB 左右的交换文件。

让我们看看交换文件的更多信息。

检查 Linux 的交换空间

在你开始添加交换空间之前,最好检查一下你的系统中是否已经有了交换空间。

你可以用Linux 上的 free 命令检查它。就我而言,我的戴尔 XPS有 14GB 的交换容量。

free -h
              total        used        free      shared  buff/cache   available
Mem:           7.5G        4.1G        267M        971M        3.1G        2.2G
Swap:           14G          0B         14G

free 命令给出了交换空间的大小,但它并没有告诉你它是真实的交换分区还是交换文件。swapon 命令在这方面会更好。

swapon --show
NAME           TYPE       SIZE USED PRIO
/dev/nvme0n1p4 partition 14.9G   0B   -2

如你所见,我有 14.9GB 的交换空间,它在一个单独的分区上。如果是交换文件,类型应该是 file 而不是 partition

swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file   2G   0B   -2

如果你的系统上没有交换空间,它应该显示如下内容:

free -h
              total        used        free      shared  buff/cache   available
Mem:           7.5G        4.1G        267M        971M        3.1G        2.2G
Swap:           0B          0B         0B

swapon 命令不会显示任何输出。

在 Linux 上创建交换文件

如果你的系统没有交换空间,或者你认为交换空间不足,你可以在 Linux 上创建交换文件。你也可以创建多个交换文件。

让我们看看如何在 Linux 上创建交换文件。我在本教程中使用 Ubuntu 18.04,但它也应该适用于其他 Linux 发行版本。

步骤 1:创建一个新的交换文件

首先,创建一个具有所需交换空间大小的文件。假设我想给我的系统增加 1GB 的交换空间。使用fallocate 命令创建大小为 1GB 的文件。

sudo fallocate -l 1G /swapfile

建议只允许 root 用户读写该交换文件。当你尝试将此文件用于交换区域时,你甚至会看到类似“不安全权限 0644,建议 0600”的警告。

sudo chmod 600 /swapfile

请注意,交换文件的名称可以是任意的。如果你需要多个交换空间,你可以给它任何合适的名称,如 swap_file_1swap_file_2 等。它们只是一个预定义大小的文件。

步骤 2:将新文件标记为交换空间

你需要告诉 Linux 系统该文件将被用作交换空间。你可以用 mkswap 工具做到这一点。

sudo mkswap /swapfile

你应该会看到这样的输出:

Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)
no label, UUID=7e1faacb-ea93-4c49-a53d-fb40f3ce016a

步骤 3:启用交换文件

现在,你的系统知道文件 swapfile 可以用作交换空间。但是还没有完成。你需要启用该交换文件,以便系统可以开始使用该文件作为交换。

sudo swapon /swapfile

现在,如果你检查交换空间,你应该会看到你的 Linux 系统会识别并使用它作为交换空间:

swapon --show
NAME       TYPE   SIZE USED PRIO
/swapfile  file 1024M   0B   -2

步骤 4:让改变持久化

迄今为止你所做的一切都是暂时的。重新启动系统,所有更改都将消失。

你可以通过将新创建的交换文件添加到 /etc/fstab 文件来使更改持久化。

/etc/fstab 文件进行任何更改之前,最好先进行备份。

sudo cp /etc/fstab /etc/fstab.back

现在将以下行添加到 /etc/fstab 文件的末尾:

/swapfile none swap sw 0 0

你可以使用命令行文本编辑器手动操作,或者使用以下命令:

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

现在一切都准备好了。即使在重新启动你的 Linux 系统后,你的交换文件也会被使用。

调整 swappiness 参数

swappiness 参数决定了交换空间的使用频率。swappiness 值的范围从 0 到 100。较高的值意味着交换空间将被更频繁地使用。

Ubuntu 桌面的默认的 swappiness 是 60,而服务器的默认 swappiness 是 1。你可以使用以下命令检查 swappiness

cat /proc/sys/vm/swappiness

为什么服务器应该使用低的 swappiness 值?因为交换空间比内存慢,为了获得更好的性能,应该尽可能多地使用内存。在服务器上,性能因素至关重要,因此 swappiness 应该尽可能低。

你可以使用以下系统命令动态更改 swappiness

sudo sysctl vm.swappiness=25

这种改变只是暂时的。如果要使其永久化,可以编辑 /etc/sysctl.conf 文件,并在文件末尾添加swappiness 值:

vm.swappiness=25

在 Linux 上调整交换空间的大小

在 Linux 上有几种方法可以调整交换空间的大小。但是在你看到这一点之前,你应该了解一些关于它的事情。

当你要求系统停止将交换文件用于交换空间时,它会将所有数据(确切地说是内存页)传输回内存。所以你应该有足够的空闲内存,然后再停止交换。

这就是为什么创建和启用另一个临时交换文件是一个好的做法的原因。这样,当你关闭原来的交换空间时,你的系统将使用临时交换文件。现在你可以调整原来的交换空间的大小。你可以手动删除临时交换文件或留在那里,下次启动时会自动删除(LCTT 译注:存疑?)。

如果你有足够的可用内存或者创建了临时交换空间,那就关闭你原来的交换文件。

sudo swapoff /swapfile

现在你可以使用 fallocate 命令来更改文件的大小。比方说,你将其大小更改为 2GB:

sudo fallocate -l 2G /swapfile

现在再次将文件标记为交换空间:

sudo mkswap /swapfile

并再次启用交换文件:

sudo swapon /swapfile

你也可以选择同时拥有多个交换文件。

删除 Linux 中的交换文件

你可能有不在 Linux 上使用交换文件的原因。如果你想删除它,该过程类似于你刚才看到的调整交换大小的过程。

首先,确保你有足够的空闲内存。现在关闭交换文件:

sudo swapoff /swapfile

下一步是从 /etc/fstab 文件中删除相应的条目。

最后,你可以删除该文件来释放空间:

sudo rm /swapfile

你用了交换空间了吗?

我想你现在已经很好地理解了 Linux 中的交换文件概念。现在,你可以根据需要轻松创建交换文件或调整它们的大小。

如果你对这个话题有什么要补充的或者有任何疑问,请在下面留下评论。


via: https://itsfoss.com/create-swap-file-linux/

作者:Abhishek Prakash 选题:lujun9972 译者:heguangzhi 校对:wxy

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

在几个月前的一篇文章里,我曾说过“有个一个流行的传言,const 有助于编译器优化 C 和 C++ 代码”。我觉得我需要解释一下,尤其是曾经我自己也以为这是显然对的。我将会用一些理论并构造一些例子来论证,然后在一个真实的代码库 Sqlite 上做一些实验和基准测试。

一个简单的测试

让我们从一个最简单、最明显的例子开始,以前认为这是一个 const 让 C 代码跑得更快的例子。首先,假设我们有如下两个函数声明:

void func(int *x);
void constFunc(const int *x);

然后假设我们如下两份代码:

void byArg(int *x)
{
  printf("%d\n", *x);
  func(x);
  printf("%d\n", *x);
}

void constByArg(const int *x)
{
  printf("%d\n", *x);
  constFunc(x);
  printf("%d\n", *x);
}

调用 printf() 时,CPU 会通过指针从 RAM 中取得 *x 的值。很显然,constByArg() 会稍微快一点,因为编译器知道 *x 是常量,因此不需要在调用 constFunc() 之后再次获取它的值。它仅是打印相同的东西。没问题吧?让我们来看下 GCC 在如下编译选项下生成的汇编代码:

$ gcc -S -Wall -O3 test.c
$ view test.s

以下是函数 byArg() 的完整汇编代码:

byArg:
.LFB23:
    .cfi_startproc
    pushq   %rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    movl    (%rdi), %edx
    movq    %rdi, %rbx
    leaq    .LC0(%rip), %rsi
    movl    $1, %edi
    xorl    %eax, %eax
    call    __printf_chk@PLT
    movq    %rbx, %rdi
    call    func@PLT  # constFoo 中唯一不同的指令
    movl    (%rbx), %edx
    leaq    .LC0(%rip), %rsi
    xorl    %eax, %eax
    movl    $1, %edi
    popq    %rbx
    .cfi_def_cfa_offset 8
    jmp __printf_chk@PLT
    .cfi_endproc

函数 byArg() 和函数 constByArg() 生成的汇编代码中唯一的不同之处是 constByArg() 有一句汇编代码 call constFunc@PLT,这正是源代码中的调用。关键字 const 本身并没有造成任何字面上的不同。

好了,这是 GCC 的结果。或许我们需要一个更聪明的编译器。Clang 会有更好的表现吗?

$ clang -S -Wall -O3 -emit-llvm test.c
$ view test.ll

这是 IR 代码(LCTT 译注:LLVM 的中间语言)。它比汇编代码更加紧凑,所以我可以把两个函数都导出来,让你可以看清楚我所说的“除了调用外,没有任何字面上的不同”是什么意思:

; Function Attrs: nounwind uwtable
define dso_local void @byArg(i32*) local_unnamed_addr #0 {
  %2 = load i32, i32* %0, align 4, !tbaa !2
  %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2)
  tail call void @func(i32* %0) #4
  %4 = load i32, i32* %0, align 4, !tbaa !2
  %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4)
  ret void
}

; Function Attrs: nounwind uwtable
define dso_local void @constByArg(i32*) local_unnamed_addr #0 {
  %2 = load i32, i32* %0, align 4, !tbaa !2
  %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2)
  tail call void @constFunc(i32* %0) #4
  %4 = load i32, i32* %0, align 4, !tbaa !2
  %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4)
  ret void
}

某些有作用的东西

接下来是一组 const 能够真正产生作用的代码:

void localVar()
{
  int x = 42;
  printf("%d\n", x);
  constFunc(&x);
  printf("%d\n", x);
}

void constLocalVar()
{
  const int x = 42;  // 对本地变量使用 const
  printf("%d\n", x);
  constFunc(&x);
  printf("%d\n", x);
}

下面是 localVar() 的汇编代码,其中有两条指令在 constLocalVar() 中会被优化掉:

localVar:
.LFB25:
    .cfi_startproc
    subq    $24, %rsp
    .cfi_def_cfa_offset 32
    movl    $42, %edx
    movl    $1, %edi
    movq    %fs:40, %rax
    movq    %rax, 8(%rsp)
    xorl    %eax, %eax
    leaq    .LC0(%rip), %rsi
    movl    $42, 4(%rsp)
    call    __printf_chk@PLT
    leaq    4(%rsp), %rdi
    call    constFunc@PLT
    movl    4(%rsp), %edx  # 在 constLocalVar() 中没有
    xorl    %eax, %eax
    movl    $1, %edi
    leaq    .LC0(%rip), %rsi  # 在 constLocalVar() 中没有
    call    __printf_chk@PLT
    movq    8(%rsp), %rax
    xorq    %fs:40, %rax
    jne .L9
    addq    $24, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 8
    ret
.L9:
    .cfi_restore_state
    call    __stack_chk_fail@PLT
    .cfi_endproc

在 LLVM 生成的 IR 代码中更明显一点。在 constLocalVar() 中,第二次调用 printf() 之前的 load 会被优化掉:

; Function Attrs: nounwind uwtable
define dso_local void @localVar() local_unnamed_addr #0 {
  %1 = alloca i32, align 4
  %2 = bitcast i32* %1 to i8*
  call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %2) #4
  store i32 42, i32* %1, align 4, !tbaa !2
  %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 42)
  call void @constFunc(i32* nonnull %1) #4
  %4 = load i32, i32* %1, align 4, !tbaa !2
  %5 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4)
  call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %2) #4
  ret void
}

好吧,现在,constLocalVar() 成功的省略了对 *x 的重新读取,但是可能你已经注意到一些问题:localVar()constLocalVar() 在函数体中做了同样的 constFunc() 调用。如果编译器能够推断出 constFunc() 没有修改 constLocalVar() 中的 *x,那为什么不能推断出完全一样的函数调用也没有修改 localVar() 中的 *x

这个解释更贴近于为什么 C 语言的 const 不能作为优化手段的核心原因。C 语言的 const 有两个有效的含义:它可以表示这个变量是某个可能是常数也可能不是常数的数据的一个只读别名,或者它可以表示该变量是真正的常量。如果你移除了一个指向常量的指针的 const 属性并写入数据,那结果将是一个未定义行为。另一方面,如果是一个指向非常量值的 const 指针,将就没问题。

这份 constFunc() 的可能实现揭示了这意味着什么:

// x 是一个指向某个可能是常数也可能不是常数的数据的只读指针
void constFunc(const int *x)
{
  // local_var 是一个真正的常数
  const int local_var = 42;

  // C 语言规定的未定义行为
  doubleIt((int*)&local_var);
  // 谁知道这是不是一个未定义行为呢?
  doubleIt((int*)x);
}

void doubleIt(int *x)
{
  *x *= 2;
}

localVar() 传递给 constFunc() 一个指向非 const 变量的 const 指针。因为这个变量并非常量,constFunc() 可以撒个谎并强行修改它而不触发未定义行为。所以,编译器不能断定变量在调用 constFunc() 后仍是同样的值。在 constLocalVar() 中的变量是真正的常量,因此,编译器可以断定它不会改变 —— 因为在 constFunc() 去除变量的 const 属性并写入它会是一个未定义行为。

第一个例子中的函数 byArg()constByArg() 是没有可能优化的,因为编译器没有任何方法能知道 *x 是否真的是 const 常量。

补充(和题外话):相当多的读者已经正确地指出,使用 const int *x,该指针本身不是限定的常量,只是该数据被加个了别名,而 const int * const extra_const 是一个“双向”限定为常量的指针。但是因为指针本身的常量与别名数据的常量无关,所以结果是相同的。仅在 extra_const 指向使用 const 定义的对象时,*(int*const)extra_const = 0 才是未定义行为。(实际上,*(int*)extra_const = 0 也不会更糟。)因为它们之间的区别可以一句话说明白,一个是完全的 const 指针,另外一个可能是也可能不是常量本身的指针,而是一个可能是也可能不是常量的对象的只读别名,我将继续不严谨地引用“常量指针”。(题外话结束)

但是为什么不一致呢?如果编译器能够推断出 constLocalVar() 中调用的 constFunc() 不会修改它的参数,那么肯定也能继续在其他 constFunc() 的调用上实施相同的优化,是吗?并不。编译器不能假设 constLocalVar() 根本没有运行。如果不是这样(例如,它只是代码生成器或者宏的一些未使用的额外输出),constFunc() 就能偷偷地修改数据而不触发未定义行为。

你可能需要重复阅读几次上述说明和示例,但不要担心,它听起来很荒谬,它确实是正确的。不幸的是,对 const 变量进行写入是最糟糕的未定义行为:大多数情况下,编译器无法知道它是否将会是未定义行为。所以,大多数情况下,编译器看见 const 时必须假设它未来可能会被移除掉,这意味着编译器不能使用它进行优化。这在实践中是正确的,因为真实的 C 代码会在“深思熟虑”后移除 const

简而言之,很多事情都可以阻止编译器使用 const 进行优化,包括使用指针从另一内存空间接受数据,或者在堆空间上分配数据。更糟糕的是,在大部分编译器能够使用 const 进行优化的情况,它都不是必须的。例如,任何像样的编译器都能推断出下面代码中的 x 是一个常量,甚至都不需要 const

int x = 42, y = 0;
printf("%d %d\n", x, y);
y += x;
printf("%d %d\n", x, y);

总结,const 对优化而言几乎无用,因为:

  1. 除了特殊情况,编译器需要忽略它,因为其他代码可能合法地移除它
  2. 在 #1 以外的大多数例外中,编译器无论如何都能推断出该变量是常量

C++

如果你在使用 C++ 那么有另外一个方法让 const 能够影响到代码的生成:函数重载。你可以用 const 和非 const 的参数重载同一个函数,而非 const 版本的代码可能可以被优化(由程序员优化而不是编译器),减少某些拷贝或者其他事情。

void foo(int *p)
{
  // 需要做更多的数据拷贝
}

void foo(const int *p)
{
  // 不需要保护性的拷贝副本
}

int main()
{
  const int x = 42;
  // const 影响被调用的是哪一个版本的重载函数
  foo(&x);
  return 0;
}

一方面,我不认为这会在实际的 C++ 代码中大量使用。另一方面,为了导致差异,程序员需要假设编译器无法做出,因为它们不受语言保护。

用 Sqlite3 进行实验

有了足够的理论和例子。那么 const 在一个真正的代码库中有多大的影响呢?我将会在代码库 Sqlite(版本:3.30.0)上做一个测试,因为:

  • 它真正地使用了 const
  • 它不是一个简单的代码库(超过 20 万行代码)
  • 作为一个数据库,它包括了字符串处理、数学计算、日期处理等一系列内容
  • 它能够在绑定 CPU 的情况下进行负载测试

此外,作者和贡献者们已经进行了多年的性能优化工作,因此我能确定他们没有错过任何有显著效果的优化。

配置

我做了两份源码拷贝,并且正常编译其中一份。而对于另一份拷贝,我插入了这个特殊的预处理代码段,将 const 变成一个空操作:

#define const

(GNU) sed 可以将一些东西添加到每个文件的顶端,比如 sed -i '1i#define const' *.c *.h

在编译期间使用脚本生成 Sqlite 代码稍微有点复杂。幸运的是当 const 代码和非 const 代码混合时,编译器会产生了大量的提醒,因此很容易发现它并调整脚本来包含我的反 const 代码段。

直接比较编译结果毫无意义,因为任意微小的改变就会影响整个内存布局,这可能会改变整个代码中的指针和函数调用。因此,我用每个指令的二进制大小和汇编代码作为识别码(objdump -d libsqlite3.so.0.8.6)。举个例子,这个函数:

000000000005d570 <sqlite3_blob_read>:
   5d570:       4c 8d 05 59 a2 ff ff    lea    -0x5da7(%rip),%r8        # 577d0 <sqlite3BtreePayloadChecked>
   5d577:       e9 04 fe ff ff          jmpq   5d380 <blobReadWrite>
   5d57c:       0f 1f 40 00             nopl   0x0(%rax)

将会变成这样:

sqlite3_blob_read   7lea 5jmpq 4nopl

在编译时,我保留了所有 Sqlite 的编译设置。

分析编译结果

const 版本的 libsqlite3.so 的大小是 4,740,704 字节,大约比 4,736,712 字节的非 const 版本大了 0.1% 。在全部 1374 个导出函数(不包括类似 PLT 里的底层辅助函数)中,一共有 13 个函数的识别码不一致。

其中的一些改变是由于插入的预处理代码。举个例子,这里有一个发生了更改的函数(已经删去一些 Sqlite 特有的定义):

#define LARGEST_INT64  (0xffffffff|(((int64_t)0x7fffffff)<<32))
#define SMALLEST_INT64 (((int64_t)-1) - LARGEST_INT64)

static int64_t doubleToInt64(double r){
  /*
  ** Many compilers we encounter do not define constants for the
  ** minimum and maximum 64-bit integers, or they define them
  ** inconsistently.  And many do not understand the "LL" notation.
  ** So we define our own static constants here using nothing
  ** larger than a 32-bit integer constant.
  */
  static const int64_t maxInt = LARGEST_INT64;
  static const int64_t minInt = SMALLEST_INT64;

  if( r<=(double)minInt ){
    return minInt;
  }else if( r>=(double)maxInt ){
    return maxInt;
  }else{
    return (int64_t)r;
  }
}

删去 const 使得这些常量变成了 static 变量。我不明白为什么会有不了解 const 的人让这些变量加上 static。同时删去 staticconst 会让 GCC 再次认为它们是常量,而我们将得到同样的编译输出。由于类似这样的局部的 static const 变量,使得 13 个函数中有 3 个函数产生假的变化,但我一个都不打算修复它们。

Sqlite 使用了很多全局变量,而这正是大多数真正的 const 优化产生的地方。通常情况下,它们类似于将一个变量比较代替成一个常量比较,或者一个循环在部分展开的一步。(Radare toolkit 可以很方便的找出这些优化措施。)一些变化则令人失望。sqlite3ParseUri() 有 487 个指令,但 const 产生的唯一区别是进行了这个比较:

test %al, %al
je <sqlite3ParseUri+0x717>
cmp $0x23, %al
je <sqlite3ParseUri+0x717>

并交换了它们的顺序:

cmp $0x23, %al
je <sqlite3ParseUri+0x717>
test %al, %al
je <sqlite3ParseUri+0x717>

基准测试

Sqlite 自带了一个性能回归测试,因此我尝试每个版本的代码执行一百次,仍然使用默认的 Sqlite 编译设置。以秒为单位的测试结果如下:

const非 const
最小值10.658s10.803s
中间值11.571s11.519s
最大值11.832s11.658s
平均值11.531s11.492s

就我个人看来,我没有发现足够的证据来说明这个差异值得关注。我是说,我从整个程序中删去 const,所以如果它有明显的差别,那么我希望它是显而易见的。但也许你关心任何微小的差异,因为你正在做一些绝对性能非常重要的事。那让我们试一下统计分析。

我喜欢使用类似 Mann-Whitney U 检验这样的东西。它类似于更著名的 T 检验,但对你在机器上计时时产生的复杂随机变量(由于不可预测的上下文切换、页错误等)更加健壮。以下是结果:

const非 const
N100100
Mean rank121.3879.62
Mann-Whitney U2912
Z-5.10
2-sided p value<10-6
HL median difference-0.056s
95% confidence interval-0.077s – -0.038s

U 检验已经发现统计意义上具有显著的性能差异。但是,令人惊讶的是,实际上是非 const 版本更快——大约 60ms,0.5%。似乎 const 启用的少量“优化”不值得额外代码的开销。这不像是 const 启用了任何类似于自动矢量化的重要的优化。当然,你的结果可能因为编译器配置、编译器版本或者代码库等等而有所不同,但是我觉得这已经说明了 const 是否能够有效地提高 C 的性能,我们现在已经看到答案了。

那么,const 有什么用呢?

尽管存在缺陷,C/C++ 的 const 仍有助于类型安全。特别是,结合 C++ 的移动语义和 std::unique_pointerconst 可以使指针所有权显式化。在超过十万行代码的 C++ 旧代码库里,指针所有权模糊是一个大难题,我对此深有感触。

但是,我以前常常使用 const 来实现有意义的类型安全。我曾听说过基于性能上的原因,最好是尽可能多地使用 const。我曾听说过当性能很重要时,重构代码并添加更多的 const 非常重要,即使以降低代码可读性的方式。当时觉得这没问题,但后来我才知道这并不对。


via: https://theartofmachinery.com/2019/08/12/c_const_isnt_for_performance.html

作者:Simon Arneaud 选题:lujun9972 译者:LazyWolfLin 校对:wxy

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

根据你的工作需要,可能有比 Java 更好的语言,但是我还没有看到任何能把我拉走的语言。

我记得我是从 1997 年开始使用 Java 的,就在 Java 1.1 刚刚发布不久之后。从那时起,总的来说,我非常喜欢用 Java 编程;虽然我得承认,这些日子我经常像在 Java 中编写“严肃的代码”一样编写 Groovy 脚本。

来自 FORTRANPL/1Pascal) 以及最后的 C 语言) 背景,我发现了许多让我喜欢 Java 的东西。Java 是我面向对象编程的第一次重要实践经验。到那时,我已经编程了大约 20 年,而且可以说我对什么重要、什么不重要有了一些看法。

调试是一个关键的语言特性

我真的很讨厌浪费时间追踪由我的代码不小心迭代到数组末尾而导致的模糊错误,特别是在 IBM 大型机上的 FORTRAN 编程时代。另一个不时出现的隐晦问题是调用一个子程序时,该子程序带有一个四字节整数参数,而预期有两个字节;在小端架构上,这通常是一个良性的错误,但在大端机器上,前两个字节的值通常并不总是为零。

在那种批处理环境中进行调试也非常不便,通过核心转储或插入打印语句进行调试,这些语句本身会移动错误的位置甚至使它们消失。

所以我使用 Pascal 的早期体验,先是在 MTS 上,然后是在 IBM OS/VS1 上使用相同的 MTS 编译器,让我的生活变得更加轻松。Pascal 的强类型和静态类型是取得这种胜利的重要组成部分,我使用的每个 Pascal 编译器都会在数组的边界和范围上插入运行时检查,因此错误可以在发生时检测到。当我们在 20 世纪 80 年代早期将大部分工作转移到 Unix 系统时,移植 Pascal 代码是一项简单的任务。

适量的语法

但是对于我所喜欢的 Pascal 来说,我的代码很冗长,而且语法似乎要比代码还要多;例如,使用:

if ... then begin ... end else ... end

而不是 C 或类似语言中的:

if (...) { ... } else { ... }

另外,有些事情在 Pascal 中很难完成,在 C 中更容易。但是,当我开始越来越多地使用 C 时,我发现自己遇到了我曾经在 FORTRAN 中遇到的同样类型的错误,例如,超出数组边界。在原始的错误点未检测到数组结束,而仅在程序执行后期才会检测到它们的不利影响。幸运的是,我不再生活在那种批处理环境中,并且手头有很好的调试工具。不过,C 对于我来说有点太灵活了。

当我遇到 awk 时,我发现它与 C 相比又是另外一种样子。那时,我的很多工作都涉及转换字段数据并创建报告。我发现用 awk 加上其他 Unix 命令行工具,如 sortsedcutjoinpastecomm 等等,可以做到事情令人吃惊。从本质上讲,这些工具给了我一个像是基于文本文件的关系数据库管理器,这种文本文件具有列式结构,是我们很多字段数据的保存方式。或者,即便不是这种格式,大部分时候也可以从关系数据库或某种二进制格式导出到列式结构中。

awk 支持的字符串处理、正则表达式关联数组,以及 awk 的基本特性(它实际上是一个数据转换管道),非常符合我的需求。当面对二进制数据文件、复杂的数据结构和关键性能需求时,我仍然会转回到 C;但随着我越来越多地使用 awk,我发现 C 的非常基础的字符串支持越来越令人沮丧。随着时间的推移,更多的时候我只会在必须时才使用 C,并且在其余的时候里大量使用 awk

Java 的抽象层级合适

然后是 Java。它看起来相当不错 —— 相对简洁的语法,让人联想到 C,或者这种相似性至少要比 Pascal 或其他任何早期的语言更为明显。它是强类型的,因此很多编程错误会在编译时被捕获。它似乎并不需要过多的面向对象的知识就能起步,这是一件好事,因为我当时对 OOP 设计模式毫不熟悉。但即使在刚刚开始,我也喜欢它的简化继承模型背后的思想。(Java 允许使用提供的接口进行单继承,以在某种程度上丰富范例。)

它似乎带有丰富的功能库(即“自备电池”的概念),在适当的水平上直接满足了我的需求。最后,我发现自己很快就会想到将数据和行为在对象中组合在一起的想法。这似乎是明确控制数据之间交互的好方法 —— 比大量的参数列表或对全局变量的不受控制的访问要好得多。

从那以后,Java 在我的编程工具箱中成为了 Helvetic 军刀。我仍然偶尔会在 awk 中编写程序,或者使用 Linux 命令行实用程序(如 cutsortsed),因为它们显然是解决手头问题的直接方法。我怀疑过去 20 年我可能没写过 50 行的 C 语言代码;Java 完全满足了我的需求。

此外,Java 一直在不断改进。首先,它变得更加高效。并且它添加了一些非常有用的功能,例如可以用 try 来测试资源,它可以很好地清理在文件 I/O 期间冗长而有点混乱的错误处理代码;或 lambda,它提供了声明函数并将其作为参数传递的能力,而旧方法需要创建类或接口来“托管”这些函数;或,它在函数中封装了迭代行为,可以创建以链式函数调用形式实现的高效数据转换管道。

Java 越来越好

许多语言设计者研究了从根本上改善 Java 体验的方法。对我来说,其中大部分没有引起我的太多兴趣;再次,这更多地反映了我的典型工作流程,并且(更多地)减少了这些语言带来的功能。但其中一个演化步骤已经成为我的编程工具中不可或缺的一部分:Groovy。当我遇到一个小问题,需要一个简单的解决方案时,Groovy 已经成为了我的首选。而且,它与 Java 高度兼容。对我来说,Groovy 填补了 Python 为许多其他人所提供的相同用处 —— 它紧凑、DRY(不要重复自己)和具有表达性(列表和词典有完整的语言支持)。我还使用了 Grails,它使用 Groovy 为非常高性能和有用的 Java Web 应用程序提供简化的 Web 框架。

Java 仍然开源吗?

最近,对 OpenJDK 越来越多的支持进一步提高了我对 Java 的舒适度。许多公司以各种方式支持 OpenJDK,包括 AdoptOpenJDK、Amazon 和 Red Hat。在我的一个更大、更长期的项目中,我们使用 AdoptOpenJDK 来在几个桌面平台上生成自定义的运行时环境

有没有比 Java 更好的语言?我确信有,这取决于你的工作需要。但我一直对 Java 非常满意,我还没有遇到任何可能会让我失望的东西。


via: https://opensource.com/article/19/9/why-i-use-java

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

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

Linux 系统上的日志文件包含了很多信息——比你有时间查看的还要多。以下是一些建议,告诉你如何正确的使用它们……而不是淹没在其中。

Greg Lobinski (CC BY 2.0)

在 Linux 系统上管理日志文件可能非常容易,也可能非常痛苦。这完全取决于你所认为的日志管理是什么。

如果你认为是如何确保日志文件不会耗尽你的 Linux 服务器上的所有磁盘空间,那么这个问题通常很简单。Linux 系统上的日志文件会自动翻转,系统将只维护固定数量的翻转日志。即便如此,一眼看去一组上百个文件可能会让人不知所措。在这篇文章中,我们将看看日志轮换是如何工作的,以及一些最相关的日志文件。

自动日志轮换

日志文件是经常轮转的。当前的日志会获得稍微不同的文件名,并建立一个新的日志文件。以系统日志文件为例。对于许多正常的系统 messages 文件来说,这个文件是一个包罗万象的东西。如果你 cd 转到 /var/log 并查看一下,你可能会看到一系列系统日志文件,如下所示:

$ ls -l syslog*
-rw-r----- 1 syslog adm 28996 Jul 30 07:40 syslog
-rw-r----- 1 syslog adm 71212 Jul 30 00:00 syslog.1
-rw-r----- 1 syslog adm  5449 Jul 29 00:00 syslog.2.gz
-rw-r----- 1 syslog adm  6152 Jul 28 00:00 syslog.3.gz
-rw-r----- 1 syslog adm  7031 Jul 27 00:00 syslog.4.gz
-rw-r----- 1 syslog adm  5602 Jul 26 00:00 syslog.5.gz
-rw-r----- 1 syslog adm  5995 Jul 25 00:00 syslog.6.gz
-rw-r----- 1 syslog adm 32924 Jul 24 00:00 syslog.7.gz

轮换发生在每天午夜,旧的日志文件会保留一周,然后删除最早的系统日志文件。syslog.7.gz 文件将被从系统中删除,syslog.6.gz 将被重命名为 syslog.7.gz。日志文件的其余部分将依次改名,直到 syslog 变成 syslog.1 并创建一个新的 syslog 文件。有些系统日志文件会比其他文件大,但是一般来说,没有一个文件可能会变得非常大,并且你永远不会看到超过八个的文件。这给了你一个多星期的时间来回顾它们收集的任何数据。

某种特定日志文件维护的文件数量取决于日志文件本身。有些文件可能有 13 个。请注意 syslogdpkg 的旧文件是如何压缩以节省空间的。这里的考虑是你对最近的日志最感兴趣,而更旧的日志可以根据需要用 gunzip 解压。

# ls -t dpkg*
dpkg.log       dpkg.log.3.gz  dpkg.log.6.gz  dpkg.log.9.gz   dpkg.log.12.gz
dpkg.log.1     dpkg.log.4.gz  dpkg.log.7.gz  dpkg.log.10.gz
dpkg.log.2.gz  dpkg.log.5.gz  dpkg.log.8.gz  dpkg.log.11.gz

日志文件可以根据时间和大小进行轮换。检查日志文件时请记住这一点。

尽管默认值适用于大多数 Linux 系统管理员,但如果你愿意,可以对日志文件轮换进行不同的配置。查看这些文件,如 /etc/rsyslog.conf/etc/logrotate.conf

使用日志文件

对日志文件的管理也包括时不时的使用它们。使用日志文件的第一步可能包括:习惯每个日志文件可以告诉你有关系统如何工作以及系统可能会遇到哪些问题。从头到尾读取日志文件几乎不是一个好的选择,但是当你想了解你的系统运行的情况或者需要跟踪一个问题时,知道如何从日志文件中获取信息会是有很大的好处。这也表明你对每个文件中存储的信息有一个大致的了解了。例如:

$ who wtmp | tail -10           显示最近的登录信息
$ who wtmp | grep shark         显示特定用户的最近登录信息
$ grep "sudo:" auth.log         查看谁在使用 sudo
$ tail dmesg                    查看(最近的)内核日志
$ tail dpkg.log                 查看最近安装和更新的软件包
$ more ufw.log                  查看防火墙活动(假如你使用 ufw)

你运行的一些命令也会从日志文件中提取信息。例如,如果你想查看系统重新启动的列表,可以使用如下命令:

$ last reboot
reboot   system boot  5.0.0-20-generic Tue Jul 16 13:19   still running
reboot   system boot  5.0.0-15-generic Sat May 18 17:26 - 15:19 (21+21:52)
reboot   system boot  5.0.0-13-generic Mon Apr 29 10:55 - 15:34 (18+04:39)

使用更高级的日志管理器

虽然你可以编写脚本来更容易地在日志文件中找到感兴趣的信息,但是你也应该知道有一些非常复杂的工具可用于日志文件分析。一些可以把来自多个来源的信息联系起来,以便更全面地了解你的网络上发生了什么。它们也可以提供实时监控。这些工具,如 Solarwinds Log & Event ManagerPRTG 网络监视器(包括日志监视)浮现在脑海中。

还有一些免费工具可以帮助分析日志文件。其中包括:

  • Logwatch — 用于扫描系统日志中感兴趣的日志行的程序
  • Logcheck — 系统日志分析器和报告器

在接下来的文章中,我将提供一些关于这些工具的见解和帮助。


via: https://www.networkworld.com/article/3428361/how-to-manage-logs-in-linux.html

作者:Sandra Henry-Stocker 选题:lujun9972 译者:heguangzhi 校对:wxy

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