分类 软件开发 下的文章

了解如何处理转储文件将帮你找到应用中难以重现的 bug。

崩溃转储、内存转储、核心转储、系统转储……这些全都会产生同样的产物:一个包含了当应用崩溃时,在那个特定时刻应用的内存状态的文件。

这是一篇指导文章,你可以通过克隆示例的应用仓库来跟随学习:

git clone https://github.com/hANSIc99/core_dump_example.git

信号如何关联到转储

信号是操作系统和用户应用之间的进程间通讯。Linux 使用 POSIX 标准中定义的信号。在你的系统上,你可以在 /usr/include/bits/signum-generic.h 找到标准信号的定义。如果你想知道更多关于在你的应用程序中使用信号的信息,这有一个信息丰富的 signal 手册页。简单地说,Linux 基于预期的或意外的信号来触发进一步的活动。

当你退出一个正在运行的应用程序时,应用程序通常会收到 SIGTERM 信号。因为这种类型的退出信号是预期的,所以这个操作不会创建一个内存转储。

以下信号将导致创建一个转储文件(来源:GNU C库):

  • SIGFPE:错误的算术操作
  • SIGILL:非法指令
  • SIGSEGV:对存储的无效访问
  • SIGBUS:总线错误
  • SIGABRT:程序检测到的错误,并通过调用 abort() 来报告
  • SIGIOT:这个信号在 Fedora 上已经过时,过去在 PDP-11 上用 abort() 时触发,现在映射到 SIGABRT

创建转储文件

导航到 core_dump_example 目录,运行 make,并使用 -c1 开关执行该示例二进制:

./coredump -c1

该应用将以状态 4 退出,带有如下错误:

 title=

“Abgebrochen (Speicherabzug geschrieben) ”(LCTT 译注:这是德语,应该是因为本文作者系统是德语环境)大致翻译为“分段故障(核心转储)”。

是否创建核心转储是由运行该进程的用户的资源限制决定的。你可以用 ulimit 命令修改资源限制。

检查当前创建核心转储的设置:

ulimit -c

如果它输出 unlimited,那么它使用的是(建议的)默认值。否则,用以下方法纠正限制:

ulimit -c unlimited

要禁用创建核心转储,可以设置其大小为 0:

ulimit -c 0

这个数字指定了核心转储文件的大小,单位是块。

什么是核心转储?

内核处理核心转储的方式定义在:

/proc/sys/kernel/core_pattern

我运行的是 Fedora 31,在我的系统上,该文件包含的内容是:

/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

这表明核心转储被转发到 systemd-coredump 工具。在不同的 Linux 发行版中,core_pattern 的内容会有很大的不同。当使用 systemd-coredump 时,转储文件被压缩保存在 /var/lib/systemd/coredump 下。你不需要直接接触这些文件,你可以使用 coredumpctl。比如说:

coredumpctl list

会显示系统中保存的所有可用的转储文件。

使用 coredumpctl dump,你可以从最后保存的转储文件中检索信息:

[stephan@localhost core_dump_example]$ ./coredump 
Application started…

(…….)

Message: Process 4598 (coredump) of user 1000 dumped core.

Stack trace of thread 4598:
#0 0x00007f4bbaf22625 __GI_raise (libc.so.6)
#1 0x00007f4bbaf0b8d9 __GI_abort (libc.so.6)
#2 0x00007f4bbaf664af __libc_message (libc.so.6)
#3 0x00007f4bbaf6da9c malloc_printerr (libc.so.6)
#4 0x00007f4bbaf6f49c _int_free (libc.so.6)
#5 0x000000000040120e n/a (/home/stephan/Dokumente/core_dump_example/coredump)
#6 0x00000000004013b1 n/a (/home/stephan/Dokumente/core_dump_example/coredump)
#7 0x00007f4bbaf0d1a3 __libc_start_main (libc.so.6)
#8 0x000000000040113e n/a (/home/stephan/Dokumente/core_dump_example/coredump)
Refusing to dump core to tty (use shell redirection or specify — output).

这表明该进程被 SIGABRT 停止。这个视图中的堆栈跟踪不是很详细,因为它不包括函数名。然而,使用 coredumpctl debug,你可以简单地用调试器(默认为 GDB)打开转储文件。输入 bt 回溯 backtrace 的缩写)可以得到更详细的视图:

Core was generated by `./coredump -c1'.
Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50  return ret;
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007fc37a9aa8d9 in __GI_abort () at abort.c:79
#2  0x00007fc37aa054af in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fc37ab14f4b "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007fc37aa0ca9c in malloc_printerr (str=str@entry=0x7fc37ab130e0 "free(): invalid pointer") at malloc.c:5339
#4  0x00007fc37aa0e49c in _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at malloc.c:4173
#5  0x000000000040120e in freeSomething(void*) ()
#6  0x0000000000401401 in main ()

与后续帧相比,main()freeSomething() 的内存地址相当低。由于共享对象被映射到虚拟地址空间末尾的区域,可以认为 SIGABRT 是由共享库中的调用引起的。共享对象的内存地址在多次调用之间并不是恒定不变的,所以当你看到多次调用之间的地址不同时,完全可以认为是共享对象。

堆栈跟踪显示,后续的调用源于 malloc.c,这说明内存的(取消)分配可能出了问题。

在源代码中,(即使没有任何 C++ 知识)你也可以看到,它试图释放一个指针,而这个指针并没有被内存管理函数返回。这导致了未定义的行为,并导致了 SIGABRT

void freeSomething(void *ptr){
    free(ptr);
}
int nTmp = 5;
int *ptrNull = &nTmp;
freeSomething(ptrNull);

systemd 的这个 coredump 工具可以在 /etc/systemd/coredump.conf 中配置。可以在 /etc/systemd/systemd-tmpfiles-clean.timer 中配置轮换清理转储文件。

你可以在其手册页中找到更多关于 coredumpctl 的信息。

用调试符号编译

打开 Makefile 并注释掉第 9 行的最后一部分。现在应该是这样的:

CFLAGS =-Wall -Werror -std=c++11 -g

-g 开关使编译器能够创建调试信息。启动应用程序,这次使用 -c2 开关。

./coredump -c2

你会得到一个浮点异常。在 GDB 中打开该转储文件:

coredumpctl debug

这一次,你会直接被指向源代码中导致错误的那一行:

Reading symbols from /home/stephan/Dokumente/core_dump_example/coredump…
[New LWP 6218]
Core was generated by `./coredump -c2'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0 0x0000000000401233 in zeroDivide () at main.cpp:29
29 nRes = 5 / nDivider;
(gdb)

键入 list 以获得更好的源代码概览:

(gdb) list
24      int zeroDivide(){
25          int nDivider = 5;
26          int nRes = 0;
27          while(nDivider > 0){
28              nDivider--;
29              nRes = 5 / nDivider;
30          }
31          return nRes;
32      }

使用命令 info locals 从应用程序失败的时间点检索局部变量的值:

(gdb) info locals
nDivider = 0
nRes = 5

结合源码,可以看出,你遇到的是零除错误:

nRes = 5 / 0

结论

了解如何处理转储文件将帮助你找到并修复应用程序中难以重现的随机错误。而如果不是你的应用程序,将核心转储转发给开发人员将帮助她或他找到并修复问题。


via: https://opensource.com/article/20/8/linux-dump

作者:Stephan Avenwedde 选题:lujun9972 译者:wxy 校对:wxy

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

用这个微软的开源工具,获取进程信息。

微软越来越心仪 Linux 和开源,这并不是什么秘密。在过去几年中,该公司稳步地增加了对开源的贡献,包括将其部分软件和工具移植到 Linux。2018 年底,微软宣布将其 Sysinternals 的部分工具以开源的方式移植到 Linux,Linux 版的 ProcDump是其中的第一个。

如果你在 Windows 上从事过调试或故障排除工作,你可能听说过 Sysinternals,它是一个“瑞士军刀”工具集,可以帮助系统管理员、开发人员和 IT 安全专家监控和排除 Windows 环境的故障。

Sysinternals 最受欢迎的工具之一是 ProcDump。顾名思义,它用于将正在运行的进程的内存转储到磁盘上的一个核心文件中。然后可以用调试器对这个核心文件进行分析,了解转储时进程的状态。因为之前用过 Sysinternals,所以我很想试试 ProcDump 的 Linux 移植版。

开始使用 Linux 上的 ProcDump

要试用 Linux 上的 ProcDump,你需要下载该工具并编译它。(我使用的是 Red Hat Enterprise Linux,尽管这些步骤在其他 Linux 发行版上应该是一样的):

$ cat /etc/redhat-release
Red Hat Enterprise Linux release 8.2 (Ootpa)
$
$ uname -r
4.18.0-193.el8.x86_64
$

首先,克隆 Linux 版 ProcDump 的版本库。

$ git clone https://github.com/microsoft/ProcDump-for-Linux.git
Cloning into 'ProcDump-for-Linux'...
remote: Enumerating objects: 40, done.
remote: Counting objects: 100% (40/40), done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 414 (delta 14), reused 14 (delta 6), pack-reused 374
Receiving objects: 100% (414/414), 335.28 KiB | 265.00 KiB/s, done.
Resolving deltas: 100% (232/232), done.
$
$ cd ProcDump-for-Linux/
$
$ ls
azure-pipelines.yml  CONTRIBUTING.md  docs     INSTALL.md  Makefile    procdump.gif  src
CODE_OF_CONDUCT.md   dist             include  LICENSE     procdump.1  README.md     tests
$

接下来,使用 make 构建程序。它能准确地输出编译源文件所需的 GCC 命令行参数。

$ make
rm -rf obj
rm -rf bin
rm -rf /root/ProcDump-for-Linux/pkgbuild
gcc -c -g -o obj/Logging.o src/Logging.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/Events.o src/Events.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/ProcDumpConfiguration.o src/ProcDumpConfiguration.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/Handle.o src/Handle.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/Process.o src/Process.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/Procdump.o src/Procdump.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/TriggerThreadProcs.o src/TriggerThreadProcs.c -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/CoreDumpWriter.o src/CoreDumpWriter.c -Wall -I ./include -pthread -std=gnu99
gcc -o bin/procdump obj/Logging.o obj/Events.o obj/ProcDumpConfiguration.o obj/Handle.o obj/Process.o obj/Procdump.o obj/TriggerThreadProcs.o obj/CoreDumpWriter.o -Wall -I ./include -pthread -std=gnu99
gcc -c -g -o obj/ProcDumpTestApplication.o tests/integration/ProcDumpTestApplication.c -Wall -I ./include -pthread -std=gnu99
gcc -o bin/ProcDumpTestApplication obj/ProcDumpTestApplication.o -Wall -I ./include -pthread -std=gnu99
$

编译过程中会创建两个新的目录。第一个是 obj/ 目录,存放编译期间创建的对象文件。第二个(也是更重要的)目录是 bin/,它是存储编译出的 procdump 程序的地方。它还会编译另一个名为 ProcDumpTestApplication 的测试二进制文件:

$ ls obj/
CoreDumpWriter.o  Handle.o   ProcDumpConfiguration.o  ProcDumpTestApplication.o  TriggerThreadProcs.o
Events.o          Logging.o  Procdump.o               Process.o
$
$
$ ls bin/
procdump  ProcDumpTestApplication
$
$ file bin/procdump
bin/procdump: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6e8827db64835ea0d1f0941ac3ecff9ee8c06e6b, with debug_info, not stripped
$
$ file bin/ProcDumpTestApplication
bin/ProcDumpTestApplication: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c8fd86f53c07df142e52518815b2573d1c690e4e, with debug_info, not stripped
$

在此情况下,每次运行 procdump 实用程序时,你都必须移动到 bin/ 文件夹中。要使它在系统中的任何地方都可以使用,运行 make install。这将这个二进制文件复制到通常的 bin/ 目录中,它是你的 shell $PATH 的一部分:

$ which procdump
/usr/bin/which: no procdump in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin)
$
$ make install
mkdir -p //usr/bin
cp bin/procdump //usr/bin
mkdir -p //usr/share/man/man1
cp procdump.1 //usr/share/man/man1
$
$ which procdump
/usr/bin/procdump
$

安装时,ProcDump 提供了一个手册页,你可以用 man procdump 访问:

$ man procdump
$

运行 ProcDump

要转储一个进程的内存,你需要向 ProcDump 提供它的进程 ID(PID)。你可以使用机器上任何正在运行的程序或守护进程。在这个例子中,我将使用一个永远循环的小 C 程序。编译程序并运行它(要退出程序,按 Ctrl+C,如果程序在后台运行,则使用 kill 命令并输入 PID):

$ cat progxyz.c
#include <stdio.h>

int main() {
        for (;;)
        {
                printf(".");
                sleep(1);
        }
        return 0;
}
$
$ gcc progxyz.c -o progxyz
$
$ ./progxyz &
[1] 350498
$

运行该程序,你可以使用 pgrepps 找到它的 PID。记下 PID:

$ pgrep progxyz
350498
$
$ ps -ef | grep progxyz
root      350498  345445  0 03:29 pts/1    00:00:00 ./progxyz
root      350508  347350  0 03:29 pts/0    00:00:00 grep --color=auto progxyz
$

当测试进程正在运行时,调用 procdump 并提供 PID。下面的输出表明了该进程的名称和 PID,并报告它生成了一个核心转储文件,并显示其文件名:

$ procdump -p 350498

ProcDump v1.1.1 - Sysinternals process dump utility
Copyright (C) 2020 Microsoft Corporation. All rights reserved. Licensed under the MIT license.
Mark Russinovich, Mario Hewardt, John Salem, Javid Habibi
Monitors a process and writes a dump file when the process exceeds the
specified criteria.

Process:                progxyz (350498)
CPU Threshold:          n/a
Commit Threshold:       n/a
Polling interval (ms):  1000
Threshold (s):  10
Number of Dumps:        1

Press Ctrl-C to end monitoring without terminating the process.

[03:30:00 - INFO]: Timed:
[03:30:01 - INFO]: Core dump 0 generated: progxyz_time_2020-06-24_03:30:00.350498
$

列出当前目录的内容,你应该可以看到新的核心文件。文件名与 procdump 命令显示的文件名一致,日期、时间、PID 都会附加在文件名上:

$ ls -l progxyz_time_2020-06-24_03\:30\:00.350498
-rw-r--r--. 1 root root 356848 Jun 24 03:30 progxyz_time_2020-06-24_03:30:00.350498
$
$ file progxyz_time_2020-06-24_03\:30\:00.350498
progxyz_time_2020-06-24_03:30:00.350498: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './progxyz', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './progxyz', platform: 'x86_64'
$

用 GNU 项目调试器分析核心文件。

要查看是否可以读取该转储文件,调用 GNU 项目调试器gdb)。记得提供测试二进制文件的路径,这样你就可以看到堆栈上所有的函数名。在这里,bt(回溯)表明,当转储被采集时,sleep() 函数正在执行:

$ gdb -q ./progxyz ./progxyz_time_2020-06-24_03\:30\:00.350498
Reading symbols from ./progxyz...(no debugging symbols found)...done.
[New LWP 350498]
Core was generated by `./progxyz'.
#0  0x00007fb6947e9208 in nanosleep () from /lib64/libc.so.6
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-101.el8.x86_64
(gdb) bt
#0  0x00007fb6947e9208 in nanosleep () from /lib64/libc.so.6
#1  0x00007fb6947e913e in sleep () from /lib64/libc.so.6
#2  0x00000000004005f3 in main ()
(gdb)

gcore 怎么样?

Linux 用户会很快指出,Linux 已经有一个叫 gcore 的命令,大多数 Linux 发行版都有这个命令,它的作用和 ProcDump 完全一样。你说的对。如果你从来没有使用过它,可以尝试用 gcore 来转储一个进程的核心。再次运行测试程序,然后运行 gcore,并提供 PID 作为参数:

$ ./progxyz &
[1] 350664
$
$
$ pgrep progxyz
350664
$
$
$ gcore 350664
0x00007fefd3be2208 in nanosleep () from /lib64/libc.so.6
Saved corefile core.350664
[Inferior 1 (process 350664) detached]
$

gcore 打印一条消息,说它已将核心文件保存到一个特定的文件中。检查当前目录,找到这个核心文件,然后再次使用 gdb 加载它:

$
$ ls -l  core.350664
-rw-r--r--. 1 root root 356848 Jun 24 03:34 core.350664
$
$
$ file core.350664
core.350664: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './progxyz', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './progxyz', platform: 'x86_64'
$
$ gdb -q ./progxyz ./core.350664
Reading symbols from ./progxyz...(no debugging symbols found)...done.
[New LWP 350664]
Core was generated by `./progxyz'.
#0  0x00007fefd3be2208 in nanosleep () from /lib64/libc.so.6
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-101.el8.x86_64
(gdb) bt
#0  0x00007fefd3be2208 in nanosleep () from /lib64/libc.so.6
#1  0x00007fefd3be213e in sleep () from /lib64/libc.so.6
#2  0x00000000004005f3 in main ()
(gdb) q
$

为了使 gcore 可以工作,你需要确保以下设置到位。首先,确保为核心文件设置了 ulimit,如果设置为 0,核心文件将不会被生成。第二,确保 /proc/sys/kernel/core_pattern 有正确的设置来指定核心模式:

$ ulimit -c
unlimited
$

你应该使用 ProcDump 还是 gcore?

有几种情况下,你可能更喜欢使用 ProcDump 而不是 gcore,ProcDump 有一些内置的功能,在一些情况下可能很有用。

等待测试二进制文件的执行

无论是使用 ProcDump 还是 gcore,测试进程必须被执行并处于运行状态,这样才能提供一个 PID 来生成核心文件。但 ProcDump 有一个功能,就是等待特定的二进制文件运行,一旦发现运行的测试二进制文件与给定的名称相匹配,它就会为该测试二进制文件生成一个核心文件。它可以使用 -w 参数和程序名称而不是 PID 来启用。这个功能在测试程序快速退出的情况下很有用。

下面是它的工作原理。在这个例子中,没有名为 progxyz 的进程在运行:

$ pgrep progxyz
$

-w 参数调用 procdump,让它保持等待。在另一个终端,调用测试二进制 progxyz

$ procdump -w progxyz

ProcDump v1.1.1 - Sysinternals process dump utility
Copyright (C) 2020 Microsoft Corporation. All rights reserved. Licensed under the MIT license.
Mark Russinovich, Mario Hewardt, John Salem, Javid Habibi
Monitors a process and writes a dump file when the process exceeds the
specified criteria.

Process:                progxyz (pending)
CPU Threshold:          n/a
Commit Threshold:       n/a
Polling interval (ms):  1000
Threshold (s):  10
Number of Dumps:        1

Press Ctrl-C to end monitoring without terminating the process.

[03:39:23 - INFO]: Waiting for process 'progxyz' to launch...

然后,从另一个终端调用测试二进制 progxyz

$ ./progxyz &
[1] 350951
$

ProcDump 立即检测到该二进制正在运行,并转储这个二进制的核心文件:

[03:39:23 - INFO]: Waiting for process 'progxyz' to launch...
[03:43:22 - INFO]: Found process with PID 350951
[03:43:22 - INFO]: Timed:
[03:43:23 - INFO]: Core dump 0 generated: progxyz_time_2020-06-24_03:43:22.350951
$

$ ls -l progxyz_time_2020-06-24_03\:43\:22.350951
-rw-r--r--. 1 root root 356848 Jun 24 03:43 progxyz_time_2020-06-24_03:43:22.350951
$
$ file progxyz_time_2020-06-24_03\:43\:22.350951
progxyz_time_2020-06-24_03:43:22.350951: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './progxyz', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './progxyz', platform: 'x86_64'
$

多个核心转储

另一个重要的 ProcDump 功能是,你可以通过使用命令行参数 -n <count> 指定要生成多少个核心文件。核心转储之间的默认时间间隔是 10 秒,但你可以使用 -s <sec> 参数修改。这个例子使用 ProcDump 对测试二进制文件进行了三次核心转储:

$ ./progxyz &
[1] 351014
$
$ procdump -n 3 -p 351014

ProcDump v1.1.1 - Sysinternals process dump utility
Copyright (C) 2020 Microsoft Corporation. All rights reserved. Licensed under the MIT license.
Mark Russinovich, Mario Hewardt, John Salem, Javid Habibi
Monitors a process and writes a dump file when the process exceeds the
specified criteria.

Process:                progxyz (351014)
CPU Threshold:          n/a
Commit Threshold:       n/a
Polling interval (ms):  1000
Threshold (s):  10
Number of Dumps:        3

Press Ctrl-C to end monitoring without terminating the process.

[03:45:20 - INFO]: Timed:
[03:45:21 - INFO]: Core dump 0 generated: progxyz_time_2020-06-24_03:45:20.351014
[03:45:31 - INFO]: Timed:
[03:45:32 - INFO]: Core dump 1 generated: progxyz_time_2020-06-24_03:45:31.351014
[03:45:42 - INFO]: Timed:
[03:45:44 - INFO]: Core dump 2 generated: progxyz_time_2020-06-24_03:45:42.351014
$
$ ls -l progxyz_time_2020-06-24_03\:45\:*
-rw-r--r--. 1 root root 356848 Jun 24 03:45 progxyz_time_2020-06-24_03:45:20.351014
-rw-r--r--. 1 root root 356848 Jun 24 03:45 progxyz_time_2020-06-24_03:45:31.351014
-rw-r--r--. 1 root root 356848 Jun 24 03:45 progxyz_time_2020-06-24_03:45:42.351014
$

基于 CPU 和内存使用情况的核心转储

ProcDump 还可以让你在测试二进制或进程达到一定的 CPU 或内存阈值时触发核心转储。ProcDump 的手册页显示了调用 ProcDump 时使用的命令行参数:

  • -C:当 CPU 超过或等于指定值时,触发核心转储生成(0 到 100 * nCPU)。
  • -c:当 CPU 小于指定值时,触发核心转储生成(0 到 100 * nCPU)。
  • -M:当内存提交超过或等于指定值(MB)时,触发核心转储生成。
  • -m:当内存提交小于指定值(MB)时,触发核心转储生成。
  • -T:当线程数超过或等于指定值时触发。
  • -F:当文件描述符数量超过或等于指定值时触发。
  • -I:轮询频率,单位为毫秒(默认为 1000)。

例如,当给定 PID 的 CPU 使用率超过 70% 时,可以要求 ProcDump 转储核心:

procdump -C 70 -n 3 -p 351014

结论

ProcDump 是一长串被移植到 Linux 的 Windows 程序中的一个有趣的补充。它不仅为 Linux 用户提供了额外的工具选择,而且可以让 Windows 用户在 Linux 上工作时更有熟悉的感觉。


via: https://opensource.com/article/20/7/procdump-linux

作者:Gaurav Kamathe 选题:lujun9972 译者:wxy 校对:wxy

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

ZeroMQ 是一个快速灵活的消息库,用于数据收集和不同编程语言间的数据共享。

作为软件工程师,我有多次在要求完成指定任务时感到浑身一冷的经历。其中有一次,我必须在一些新的硬件基础设施和云基础设施之间写一个接口,这些硬件需要 C 语言,而云基础设施主要是用 Python。

实现的方式之一是 用 C 写扩展模块,Python 支持 C 扩展的调用。快速浏览文档后发现,这需要编写大量的 C 代码。这样做的话,在有些情况下效果还不错,但不是我喜欢的方式。另一种方式就是将两个任务放在不同的进程中,并使用 ZeroMQ 消息库 在两者之间交换消息。

在发现 ZeroMQ 之前,遇到这种类型的情况时,我选择了编写扩展的方式。这种方式不算太差,但非常费时费力。如今,为了避免那些问题,我将一个系统细分为独立的进程,通过 通信套接字 发送消息来交换信息。这样,不同的编程语言可以共存,每个进程也变简单了,同时也容易调试。

ZeroMQ 提供了一个更简单的过程:

  1. 编写一小段 C 代码,从硬件读取数据,然后把发现的东西作为消息发送出去。
  2. 使用 Python 编写接口,实现新旧基础设施之间的对接。

Pieter Hintjens 是 ZeroMQ 项目发起者之一,他是个拥有 有趣视角和作品 的非凡人物。

准备

本教程中,需要:

Fedora 系统上的安装方法:

$ dnf install clang zeromq zeromq-devel python3 python3-zmq

Debian 和 Ubuntu 系统上的安装方法:

$ apt-get install clang libzmq5 libzmq3-dev python3 python3-zmq

如果有问题,参考对应项目的安装指南(上面附有链接)。

编写硬件接口库

因为这里针对的是个设想的场景,本教程虚构了包含两个函数的操作库:

  • fancyhw_init() 用来初始化(设想的)硬件
  • fancyhw_read_val() 用于返回从硬件读取的数据

将库的完整代码保存到文件 libfancyhw.h 中:

#ifndef LIBFANCYHW_H
#define LIBFANCYHW_H

#include <stdlib.h>
#include <stdint.h>

// This is the fictitious hardware interfacing library

void fancyhw_init(unsigned int init_param)
{
    srand(init_param);
}

int16_t fancyhw_read_val(void)
{
    return (int16_t)rand();
}

#endif

这个库可以模拟你要在不同语言实现的组件间交换的数据,中间有个随机数发生器。

设计 C 接口

下面从包含管理数据传输的库开始,逐步实现 C 接口。

需要的库

开始先加载必要的库(每个库的作用见代码注释):

// For printf()
#include <stdio.h>
// For EXIT_*
#include <stdlib.h>
// For memcpy()
#include <string.h>
// For sleep()
#include <unistd.h>

#include <zmq.h>

#include "libfancyhw.h"

必要的参数

定义 main 函数和后续过程中必要的参数:

int main(void)
{
    const unsigned int INIT_PARAM = 12345;
    const unsigned int REPETITIONS = 10;
    const unsigned int PACKET_SIZE = 16;
    const char *TOPIC = "fancyhw_data";

    ...

初始化

所有的库都需要初始化。虚构的那个只需要一个参数:

fancyhw_init(INIT_PARAM);

ZeroMQ 库需要实打实的初始化。首先,定义对象 context,它是用来管理全部的套接字的:

void *context = zmq_ctx_new();

if (!context)
{
    printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

之后定义用来发送数据的套接字。ZeroMQ 支持若干种套接字,各有其用。使用 publish 套接字(也叫 PUB 套接字),可以复制消息并分发到多个接收端。这使得你可以让多个接收端接收同一个消息。没有接收者的消息将被丢弃(即不会入消息队列)。用法如下:

void *data_socket = zmq_socket(context, ZMQ_PUB);

套接字需要绑定到一个具体的地址,这样客户端就知道要连接哪里了。本例中,使用了 TCP 传输层(当然也有 其它选项,但 TCP 是不错的默认选择):

const int rb = zmq_bind(data_socket, "tcp://*:5555");

if (rb != 0)
{
    printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

下一步, 计算一些后续要用到的值。 注意下面代码中的 TOPIC,因为 PUB 套接字发送的消息需要绑定一个主题。主题用于供接收者过滤消息:

const size_t topic_size = strlen(TOPIC);
const size_t envelope_size = topic_size + 1 + PACKET_SIZE * sizeof(int16_t);

printf("Topic: %s; topic size: %zu; Envelope size: %zu\n", TOPIC, topic_size, envelope_size);

发送消息

启动一个发送消息的循环,循环 REPETITIONS 次:

for (unsigned int i = 0; i < REPETITIONS; i++)
{
    ...

发送消息前,先填充一个长度为 PACKET_SIZE 的缓冲区。本库提供的是 16 个位的有符号整数。因为 C 语言中 int 类型占用空间大小与平台相关,不是确定的值,所以要使用指定宽度的 int 变量:

int16_t buffer[PACKET_SIZE];

for (unsigned int j = 0; j < PACKET_SIZE; j++)
{
    buffer[j] = fancyhw_read_val();
}

printf("Read %u data values\n", PACKET_SIZE);

消息的准备和发送的第一步是创建 ZeroMQ 消息,为消息分配必要的内存空间。空白的消息是用于封装要发送的数据的:

zmq_msg_t envelope;

const int rmi = zmq_msg_init_size(&envelope, envelope_size);
if (rmi != 0)
{
    printf("ERROR: ZeroMQ error occurred during zmq_msg_init_size(): %s\n", zmq_strerror(errno));

    zmq_msg_close(&envelope);

    break;
}

现在内存空间已分配,数据保存在 ZeroMQ 消息 “信封”中。函数 zmq_msg_data() 返回一个指向封装数据缓存区顶端的指针。第一部分是主题,之后是一个空格,最后是二进制数。主题和二进制数据之间的分隔符采用空格字符。需要遍历缓存区的话,使用类型转换和 指针算法。(感谢 C 语言,让事情变得直截了当。)做法如下:

memcpy(zmq_msg_data(&envelope), TOPIC, topic_size);
memcpy((void*)((char*)zmq_msg_data(&envelope) + topic_size), " ", 1);
memcpy((void*)((char*)zmq_msg_data(&envelope) + 1 + topic_size), buffer, PACKET_SIZE * sizeof(int16_t))

通过 data_socket 发送消息:

const size_t rs = zmq_msg_send(&envelope, data_socket, 0);
if (rs != envelope_size)
{
    printf("ERROR: ZeroMQ error occurred during zmq_msg_send(): %s\n", zmq_strerror(errno));

    zmq_msg_close(&envelope);

    break;
}

使用数据之前要先解除封装:

zmq_msg_close(&envelope);

printf("Message sent; i: %u, topic: %s\n", i, TOPIC);

清理

C 语言不提供 垃圾收集) 功能,用完之后记得要自己扫尾。发送消息之后结束程序之前,需要运行扫尾代码,释放分配的内存:

const int rc = zmq_close(data_socket);

if (rc != 0)
{
    printf("ERROR: ZeroMQ error occurred during zmq_close(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

const int rd = zmq_ctx_destroy(context);

if (rd != 0)
{
    printf("Error occurred during zmq_ctx_destroy(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

return EXIT_SUCCESS;

完整 C 代码

保存下面完整的接口代码到本地名为 hw_interface.c 的文件:

// For printf()
#include <stdio.h>
// For EXIT_*
#include <stdlib.h>
// For memcpy()
#include <string.h>
// For sleep()
#include <unistd.h>

#include <zmq.h>

#include "libfancyhw.h"

int main(void)
{
    const unsigned int INIT_PARAM = 12345;
    const unsigned int REPETITIONS = 10;
    const unsigned int PACKET_SIZE = 16;
    const char *TOPIC = "fancyhw_data";

    fancyhw_init(INIT_PARAM);

    void *context = zmq_ctx_new();

    if (!context)
    {
        printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    void *data_socket = zmq_socket(context, ZMQ_PUB);

    const int rb = zmq_bind(data_socket, "tcp://*:5555");

    if (rb != 0)
    {
        printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    const size_t topic_size = strlen(TOPIC);
    const size_t envelope_size = topic_size + 1 + PACKET_SIZE * sizeof(int16_t);

    printf("Topic: %s; topic size: %zu; Envelope size: %zu\n", TOPIC, topic_size, envelope_size);

    for (unsigned int i = 0; i < REPETITIONS; i++)
    {
        int16_t buffer[PACKET_SIZE];

        for (unsigned int j = 0; j < PACKET_SIZE; j++)
        {
            buffer[j] = fancyhw_read_val();
        }

        printf("Read %u data values\n", PACKET_SIZE);

        zmq_msg_t envelope;
   
        const int rmi = zmq_msg_init_size(&envelope, envelope_size);
        if (rmi != 0)
        {
            printf("ERROR: ZeroMQ error occurred during zmq_msg_init_size(): %s\n", zmq_strerror(errno));
   
            zmq_msg_close(&envelope);
   
            break;
        }
       
        memcpy(zmq_msg_data(&envelope), TOPIC, topic_size);

        memcpy((void*)((char*)zmq_msg_data(&envelope) + topic_size), " ", 1);

        memcpy((void*)((char*)zmq_msg_data(&envelope) + 1 + topic_size), buffer, PACKET_SIZE * sizeof(int16_t));
   
        const size_t rs = zmq_msg_send(&envelope, data_socket, 0);
        if (rs != envelope_size)
        {
            printf("ERROR: ZeroMQ error occurred during zmq_msg_send(): %s\n", zmq_strerror(errno));
   
            zmq_msg_close(&envelope);
   
            break;
        }
   
        zmq_msg_close(&envelope);

        printf("Message sent; i: %u, topic: %s\n", i, TOPIC);

        sleep(1);
    }

    const int rc = zmq_close(data_socket);

    if (rc != 0)
    {
        printf("ERROR: ZeroMQ error occurred during zmq_close(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    const int rd = zmq_ctx_destroy(context);

    if (rd != 0)
    {
        printf("Error occurred during zmq_ctx_destroy(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

用如下命令编译:

$ clang -std=c99 -I. hw_interface.c -lzmq -o hw_interface

如果没有编译错误,你就可以运行这个接口了。贴心的是,ZeroMQ PUB 套接字可以在没有任何应用发送或接受数据的状态下运行,这简化了使用复杂度,因为这样不限制进程启动的次序。

运行该接口:

$ ./hw_interface
Topic: fancyhw_data; topic size: 12; Envelope size: 45
Read 16 data values
Message sent; i: 0, topic: fancyhw_data
Read 16 data values
Message sent; i: 1, topic: fancyhw_data
Read 16 data values
...
...

输出显示数据已经通过 ZeroMQ 完成发送,现在要做的是让一个程序去读数据。

编写 Python 数据处理器

现在已经准备好从 C 程序向 Python 应用传送数据了。

需要两个库帮助实现数据传输。首先是 ZeroMQ 的 Python 封装:

$ python3 -m pip install zmq

另一个就是 struct 库,用于解码二进制数据。这个库是 Python 标准库的一部分,所以不需要使用 pip 命令安装。

Python 程序的第一部分是导入这些库:

import zmq
import struct

重要参数

使用 ZeroMQ 时,只能向常量 TOPIC 定义相同的接收端发送消息:

topic = "fancyhw_data".encode('ascii')

print("Reading messages with topic: {}".format(topic))

初始化

下一步,初始化上下文和套接字。使用 subscribe 套接字(也称为 SUB 套接字),它是 PUB 套接字的天生伴侣。这个套接字发送时也需要匹配主题。

with zmq.Context() as context:
    socket = context.socket(zmq.SUB)

    socket.connect("tcp://127.0.0.1:5555")
    socket.setsockopt(zmq.SUBSCRIBE, topic)

    i = 0

    ...

接收消息

启动一个无限循环,等待接收发送到 SUB 套接字的新消息。这个循环会在你按下 Ctrl+C 组合键或者内部发生错误时终止:

    try:
        while True:

            ... # we will fill this in next

    except KeyboardInterrupt:
        socket.close()
    except Exception as error:
        print("ERROR: {}".format(error))
        socket.close()

这个循环等待 recv() 方法获取的新消息,然后将接收到的内容从第一个空格字符处分割开,从而得到主题:

binary_topic, data_buffer = socket.recv().split(b' ', 1)

解码消息

Python 此时尚不知道主题是个字符串,使用标准 ASCII 编解码器进行解码:

topic = binary_topic.decode(encoding = 'ascii')

print("Message {:d}:".format(i))
print("\ttopic: '{}'".format(topic))

下一步就是使用 struct 库读取二进制数据,它可以将二进制数据段转换为明确的数值。首先,计算数据包中数值的组数。本例中使用的 16 个位的有符号整数对应的是 struct 格式字符 中的 h

packet_size = len(data_buffer) // struct.calcsize("h")

print("\tpacket size: {:d}".format(packet_size))

知道数据包中有多少组数据后,就可以通过构建一个包含数据组数和数据类型的字符串,来定义格式了(比如“16h”):

struct_format = "{:d}h".format(packet_size)

将二进制数据串转换为可直接打印的一系列数字:

data = struct.unpack(struct_format, data_buffer)

print("\tdata: {}".format(data))

完整 Python 代码

下面是 Python 实现的完整的接收端:

#! /usr/bin/env python3

import zmq
import struct

topic = "fancyhw_data".encode('ascii')

print("Reading messages with topic: {}".format(topic))

with zmq.Context() as context:
    socket = context.socket(zmq.SUB)

    socket.connect("tcp://127.0.0.1:5555")
    socket.setsockopt(zmq.SUBSCRIBE, topic)

    i = 0

    try:
        while True:
            binary_topic, data_buffer = socket.recv().split(b' ', 1)

            topic = binary_topic.decode(encoding = 'ascii')

            print("Message {:d}:".format(i))
            print("\ttopic: '{}'".format(topic))

            packet_size = len(data_buffer) // struct.calcsize("h")

            print("\tpacket size: {:d}".format(packet_size))

            struct_format = "{:d}h".format(packet_size)

            data = struct.unpack(struct_format, data_buffer)

            print("\tdata: {}".format(data))

            i += 1

    except KeyboardInterrupt:
        socket.close()
    except Exception as error:
        print("ERROR: {}".format(error))
        socket.close()

将上面的内容保存到名为 online_analysis.py 的文件。Python 代码不需要编译,你可以直接运行它。

运行输出如下:

$ ./online_analysis.py
Reading messages with topic: b'fancyhw_data'
Message 0:
        topic: 'fancyhw_data'
        packet size: 16
        data: (20946, -23616, 9865, 31416, -15911, -10845, -5332, 25662, 10955, -32501, -18717, -24490, -16511, -28861, 24205, 26568)
Message 1:
        topic: 'fancyhw_data'
        packet size: 16
        data: (12505, 31355, 14083, -19654, -9141, 14532, -25591, 31203, 10428, -25564, -732, -7979, 9529, -27982, 29610, 30475)
...
...

小结

本教程介绍了一种新方式,实现从基于 C 的硬件接口收集数据,并分发到基于 Python 的基础设施的功能。借此可以获取数据供后续分析,或者转送到任意数量的接收端去。它采用了一个消息库实现数据在发送者和处理者之间的传送,来取代同样功能规模庞大的软件。

本教程还引出了我称之为“软件粒度”的概念,换言之,就是将软件细分为更小的部分。这种做法的优点之一就是,使得同时采用不同的编程语言实现最简接口作为不同部分之间沟通的组件成为可能。

实践中,这种设计使得软件工程师能以更独立、合作更高效的方式做事。不同的团队可以专注于数据分析的不同方面,可以选择自己中意的实现工具。这种做法的另一个优点是实现了零代价的并行,因为所有的进程都可以并行运行。ZeroMQ 消息库 是个令人赞叹的软件,使用它可以让工作大大简化。


via: https://opensource.com/article/20/3/zeromq-c-python

作者:Cristiano L. Fontana 选题:lujun9972 译者:silentdawn-zz 校对:wxy

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

Altair 作为一个 Python 数据制图库,提供了优雅的接口及自有的绘图语言。

Python 中的 绘图库 提供了呈现数据的多种方式,可以满足你不同的偏好,如灵活性、布局、易用性,或者特殊的风格。

和其它方式相比,我发现,Altair 提供的是一种不同的解决方案,且总体而言使用起来更为简单。得益于声明式的绘图语言 Vega,Altair 拥有一套优雅的接口,可以直接定义要绘的图应该是什么样子,而不是通过写一大堆循环和条件判断去一步步构建。

绘图流程

我通过绘制同一个多柱状图比较了多个 Python 绘图库的差异。正式开始之前,你需要将你的 Python 环境调整到能运行下面代码的状态。具体就是:

  • 安装最新版的 Python( LinuxMacWindows 系统下的安装方法)
  • 确认该版本 Python 可以运行本教程所使用的库

演示用数据可从网络下载,并且可以用 pandas 直接导入:

import pandas as pd
df = pd.read_csv('https://anvil.works/blog/img/plotting-in-python/uk-election-results.csv')

准备开始吧。为了做个比较,先看下面这个用 Matplotlib 做的图:

 title=

使用 Matplotlib 需要 16 行代码,图柱的位置需要自己计算。

使用 Altair 绘制相似的图,代码如下:

    import altair as alt

    chart = alt.Chart(df).mark_bar().encode(
        x='party',
        y='seats',
        column='year',
        color='party',
    )

    chart.save('altair-elections.html')

真是简洁多了!与 Seaborn 类似,Altair 所用数据的组织形式是每个变量一列(即 数据列 )。这种方式下可以将每个变量映射到图的一个属性上 —— Altair 称之为“通道”。在上例中,我们期望每个 “党派” 在 x 轴上显示为一组图柱, 其 “席位” 显示在 y 轴,且将图柱按照 “年份” 分开为 “列”。我们还想根据 “党派” 给图柱使用不同的 “颜色”。用语言表述需求的话就是上面这个样子,而这也正是代码所要表述的!

现在把图画出来:

 title=

调整样式

这和我们期待的效果有点接近了。与 Matplotlib 方案相比,主要区别在于 Altair 方案中,每个 year 组显示的时候,内部之间都有个小空白 —— 这不是问题,这只是 Altair 多柱状图显示的一个特性。

所以说呢,还需要对显示样式再做一些改进。

非整形数据

两个不是整数的年份名称(Feb 1974Oct 1974)显示为 NaN 了。这可以通过将年份数值 year 转换为字符串来解决:

    df['year'] = df['year'].astype(str)

指定数据排序方法

还需要让 Altair 知道如何对数据进行排序。Altair 允许通过传给它一个 Column 对象,来设定 Column 通道的更多细节。现在让 Altair 按照数据在数据集中出现的顺序排列:

    chart = alt.Chart(df).mark_bar().encode(
        # ...
        column=alt.Column('year', sort=list(df['year']), title=None),
        # ...
    )

移除坐标轴标签

我们通过设置 title=None 移除了图顶的 "year" 标签。下面再一处每列数据的 "party" 标签:

    chart = alt.Chart(df).mark_bar().encode(
        x=alt.X('party', title=None),
        # ...
    )

指定颜色图

最后,我们还想自己指定图柱的颜色。Altair 允许建立 domain 中数值与 range 中颜色的映射来实现所需功能,太贴心了:

    cmap = {
        'Conservative': '#0343df',
        'Labour': '#e50000',
        'Liberal': '#ffff14',
        'Others': '#929591',
    }

    chart = alt.Chart(df).mark_bar().encode(
        # ...
        color=alt.Color('party', scale=alt.Scale(domain=list(cmap.keys()), range=list(cmap.values())))
    )

样式调整后的最终代码

应用上述样式调整之后,代码看起来不那么悦目了,但我们仍然是用声明的方式实现的,这正是 Altair 如此有弹性的原因所在。实现过程中,仍然是使用的异于显示数据的独立变量来分离图中不同属性的,而不是像在 Matplotlib 中那样直接对显示数据做复杂的操作。唯一的不同是,我们的变量名字封装在类似 alt.X() 的对象中,从而实现对显示效果的控制:

    import altair as alt
    from votes import long as df

    cmap = {
        'Conservative': '#0343df',
        'Labour': '#e50000',
        'Liberal': '#ffff14',
        'Others': '#929591',
    }

    df['year'] = df['year'].astype(str)

    # We're still assigning, e.g. 'party' to x, but now we've wrapped it
    # in alt.X in order to specify its styling
    chart = alt.Chart(df).mark_bar().encode(
        x=alt.X('party', title=None),
        y='seats',
        column=alt.Column('year', sort=list(df['year']), title=None),
        color=alt.Color('party', scale=alt.Scale(domain=list(cmap.keys()), range=list(cmap.values())))
    )

    chart.save('altair-elections.html')

现在与 Matplotlib 方案扯平了,代码数量达到了 16 行!

下图是使用我们的样式调整方案之后的 Altair 效果图:

 title=

结论

尽管在代码数量上,使用 Altair 绘图没有表现出优势,但它的声明式绘图语言使得对图层的操控更为精密,这是我比较欣赏的。Altair 还提供了清晰而独立的方式来调校显示样式,这使得 相关代码与绘图的代码块分离开来。Altair 确实是使用 Python 绘图时又一个很棒的工具库。

本文首次发布于 这里,蒙允编辑后再次发布。


via: https://opensource.com/article/20/6/altair-python

作者:Shaun Taylor-Morgan 选题:lujun9972 译者:silentdawn-zz 校对:wxy

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

UFCS 能让你能够编写自然的可重用代码而不会牺牲便利性。

早在 2017 年,我就写过为什么 D 语言是开发的绝佳选择的文章。但是 D 语言中有一个出色的功能我没有充分的展开介绍: 通用函数调用语法 Universal Function Call Syntax (UFCS)。UFCS 是 D 语言中的一种语法糖,它可以在类型(字符串、数字、布尔值等)上链接任何常规函数,就像该类型的成员函数一样。

如果你尚未安装 D 语言,请安装 D 语言编译器,以便你可以自己运行 D 代码

看一下以下示例代码:

// file: ufcs_demo.d

module ufcs_demo;

import std.stdio : writeln;

int[] evenNumbers(int[] numbers)
{
    import std.array : array;
    import std.algorithm : filter;

    return numbers.filter!(n => n % 2 == 0).array;
}

void main()
{
    writeln(evenNumbers([1, 2, 3, 4]));
}

使用你喜欢的 D 语言编译器进行编译,查看这个简单示例应用做了什么:

$ dmd ufcs_demo.d
$ ./ufcs_demo
[2, 4]

但是,使用作为 D 语言的内置功能的 UFCS ,你还可以自然方式编写代码:

...
writeln([1, 2, 3, 4].evenNumbers());
...

或完全删除现在多余的括号,使 evenNumbers 看起来像是一个属性:

...
writeln([1, 2, 3, 4].evenNumbers); // prints 2, 4
...

因此,完整的代码现在变为:

// file: ufcs_demo.d

module ufcs_demo;

import std.stdio : writeln;

int[] evenNumbers(int[] numbers)
{
    import std.array : array;
    import std.algorithm : filter;

    return numbers.filter!(n => n % 2 == 0).array;
}

void main()
{
    writeln([1, 2, 3, 4].evenNumbers);
}

使用你最喜欢的 D 语言编译器进行编译,然后尝试一下。 如预期的那样,它产生相同的输出:

$ dmd ufcs_demo.d
$ ./ufcs_demo
[2, 4]

在编译过程中,编译器自动地将数组作为函数的第一个参数。这是一个常规模式,使得使用 D 语言成为一种乐趣,因此,它与你自然而然考虑代码的感觉非常相似。结果就是函数式编程。

你可能会猜出这打印的是什么:

//file: cool.d
import std.stdio : writeln;
import std.uni : asLowerCase, asCapitalized;

void main()
{
    string mySentence = "D IS COOL";
    writeln(mySentence.asLowerCase.asCapitalized);
}

确认一下:

$ dmd cool.d
$ ./cool
D is cool

结合其他 D 语言的功能,UFCS 使你能够编写可重用的代码,并在不牺牲便利性的情况下自然地进行编写。

是时候尝试 D 语言了

就像我之前写的那样,D 语言是一种很棒的开发语言。从 D 语言的下载页面可以很容易地进行安装,因此请下载编译器,查看示例,并亲自体验 D 语言。


via: https://opensource.com/article/20/7/d-programming

作者:Lawrence Aberba 选题:lujun9972 译者:geekpi 校对:wxy

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

RTOS 之类的实时嵌入式操作系统使嵌入式系统的编程更简单

物联网(IoT)无处不在,它意味着几乎所有的产品,从冰箱到口袋手表,都可以连网。为了实现这个目标,每个产品都需要拥有一个嵌入式计算机来运行网络栈,有些产品几乎是小的不可思议。这就是嵌入式软件出现的场景:现代科技打造的非常小的计算机,硬编码到硬件芯片,不需要任何板外处理器、内存或者存储盘。

就传统视角而言,这种场景意味着没有操作系统可用,但是因为种种 原因,开发者发现,类似于 RTOS 之类的实时嵌入式操作系统能让嵌入式系统的编程变得非常简单。

虽然 RT-Thread 嵌入式操作系统致力于鼓励程序员新手进入物联网领域,但并非所有的人都具备使用 C 语言硬编码微芯片的能力。幸运的是,MicroPython 通过让开发者创建运行于嵌入式系统的 Python 代码的方式,逐渐填补了这个鸿沟。为了让整个过程更简单,RT-Thread 有一个可以用于 VSCode 和 VSCodium 的插件,为开发者提供了物联网方面的开发环境。其部分特性包括:

  • 方便的连接方式,可以很容易的通过串口、网络或者 USB 连接到开发板(如果你使用过 Arduino,肯定会很熟悉这个流程)。
  • 支持上传文件或目录到开发板。
  • 支持基于 MicroPython 的代码,而且具有智能的代码补完、语法检查功能。
  • 支持 MicroPython REPL 交互式环境。
  • 丰富的示例代码和演示程序。
  • 完整的工程同步功能。
  • 快速运行内存中的程序代码。
  • 运行函数的代码片段。
  • 支持主流的 MicroPython 开发板。
  • 支持 Linux 和 Windows 操作系统,并经过充分测试。

准备

在开始之前,如果你使用的是 Windows 操作系统,那么必须保证 VSCode 的默认终端是 PowerShell。运行 VSCodium,从 “Terminal” 菜单项启动一个终端. 在显示在 VSCodium 窗口底部的终端界面,从下拉菜单顶端选择 “PowerShell”。 不论你是在 Windows 还是 Linux 系统下工作,必须保证安装了 Python 3(在 Linux 上,它可能已经安装了,否则可以通过软件库安装它)。

还要安装的是微软提供的通用 VSCode Python 插件。安装方法是点击 “File” 菜单,找到 “Preferences” 子菜单,从中打开 “Extensions” 面板。在扩展中,搜索 “Python”,找到来自微软的 Python 插件。

 title=

当然,在上述操作之前,你要先正确安装 VSCodiumVSCode

安装插件

安装 MicroPython 开发插件的方法与安装 Python 插件一样,点击 “File” 菜单,找到 “Preferences” 子菜单,选择 “Extensions”。

在扩展中,搜索 “MicroPython”,安装 RT-Thread 插件。

 title=

使用插件

你的开发板必须能访问串口,这需要组策略的允许。你可能需要将自己的账户加入该组,通常在默认情况下你的账户可能并不在该组。首先,确认你的账户不在 “dialout” 组:

$ groups
tux users

本例中,用户“tux”只是“tux”组和“users”组的成员,所以需要将用户“tux”添加到“dialout”组:

$ sudo usermod --append --groups dialout tux

退出登录或重启以载入你新的组权限。

创建 MicroPython 工程

MicroPython 开发的第一步是创建 MicroPython 工程用来编写和运行代码。使用 MicroPython 插件创建工程的方法是,点击左下方的 “Create MicroPython project” 按钮。

 title=

之后会有一些提示,让你选择创建空白工程还是包含例程的工程。

连接到开发板

点击 VSCodium 左下方的 “Connection” 按钮,进行 VSCodium 与开发板的连接,在弹出的列表中,选择要连接的物理设备。

查看例程

MicroPython 插件提供了许多例程和例库,供学习和调用。获取例程的过程是,激活 MicroPython 之后,VSCodium 左侧按钮条会出现“新建”快捷图标,点击该图标就可以了。点击“文档”快捷图标,会显示例程文件列表;点击“文件夹”快捷图标,会显示例库列表。

 title=

直接在开发板上运行 MicroPython 文件

在 VSCodium 中可以通过在开发板上运行单个程序文件,很方便快捷的进行程序调试。快捷键 Alt+Q 会触发一个特定的插件,该插件会将当前的 Python 文件上传到开发板内存中。你还可以在当前 Python 文档界面点击右键,然后选择 “Run the MicroPython file directly on the device” 实现同样的功能。

 title=

如果你需要以不上传代码的方式检查一组代码,可以使用“代码片段”功能。要运行 MicroPython REPL 环境中的代码片段,在编辑器中选中要运行的片段,右键菜单中点击 “Execute the selected MicroPython code on the device” (也可以使用 Alt+Q 快捷键)。

加载文件和目录到开发板

如果需要加载文件或目录到开发板,有现成的方法可用:选中工程中要加载到开发板的文件或目录,在选中的对象上右键,点击右键菜单中的 “Download the file/folder to the device”。

注意,如果开发板中已有与要加载的文件或目录同名者,加载操作将导致开发板原有的内容被覆盖。

在 REPL 环境中运行 os.listdir() 命令,可以检查文件和目录是否成功加载。当然,也可以通过相应的命令删除 REPL 中的文件或目录。

删除文件的命令如下:

os.remove('file_to_delete')

删除目录的命令如下:

os.rmdir('folder_to_delete')

工程的同步

点击左下角“同步”按钮可以启动工程同步,该操作将把本地工程中所有的文件和目录同步到开发板的文件系统。建议在完成程序调试之后进行该操作,调试过程中不需要频繁进行同步操作。

工程的同步操作完成后,开发板上的文件列表可以在 “Device Files List” 列看到。

放手尝试

RT-Thread 以开源的方式发布 MicroPython 插件,意在为新老开发者提供帮助,它的很多特性,如代码自动补全等,与开源插件之间互有影响和促进。你如果想为嵌入式系统或物联网系统编写软件,这就是最简单的方式,没有之一。


via: https://opensource.com/article/20/7/python-rt-thread

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

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