一份关于系统语言的经验报告 已翻译 100%

oschina 投递于 06/14 11:16 (共 13 段, 翻译完成于 06-22)
阅读 1622
收藏 14
2
加载中

最近,系统语言社区出现了很多混乱。我们有“Rust”福音派的传教,促使我们把所有的东西都用Rust重写。我们有C++17派,他们承诺C++有现代编程语言的安全性和易用性又有c的性能。然后还有一大堆众多处于长尾的“系统”编程语言,比如Nim、Reason/OCaml、Crystal、Go和Pony。

就我个人而言,我非常兴奋,我们在编程语言理论领域看到了一些有趣的工作。这让我很兴奋地了解到那里的情况。我解决的很多问题通常都是用C语言中解决的。最近,Go语言已经开始蚕食C的领地。我喜欢C和Go一样多——他们是很好的语言。很多时候,他们留下了很多值得期待的东西,也让我很羡慕那些使用Flow、Typescript和Dialyzer等语言的程序员,他们有那么多工具。在使用Erlang的开发过程中,即使是它的类型系统很基础,函数式编程对我来说也很容易。

雪落无痕xdj
翻译于 06/14 19:33
0

什么是系统语言?

让我们稍微回顾一下。 什么是系统语言? 那么,我认为这取决于你在什么位置,以及你问的是谁。 一般来说,我会建议系统语言的定义是一种可以用来实现系统运行的组件语言。

例如,对于JavaScript程序员来说,他们的代码运行在V8和Spidermonkey之上,并将C ++作为他们选择的系统语言。 大多数情况下,程序员不需要担心它们下面的内容。 一个Javascript程序员不需要知道V8是如何工作的,他们也不需要阅读V8代码。

同样,Java程序员必须了解JVM的运行时行为,但在OpenJDK之上,实现语言是不相关的。 即使这些生态系统已经相当成熟,但当计算机出现故障时,总会有人需要了解系统下面发生了什么。

大多数时候,用系统编程语言编写的东西很少能提供直接的商业价值,但却是构建底层基础架构以提供商业价值的基础。 他们是一个必须要付出的代价,对于大多数组织来说,投资他们主要就是成本,因为他们的使用不是产品成功的核心。

溪边九节
溪边九节
翻译于 06/14 16:36
0

系统对他人而言

在服务器端运行的是一套很复杂的系统集合。应用程序与业务逻辑经常使用Javascript、Python、Ruby,Elixir、Java或是用Express、Flask、Sinatra、Phoenix、Dropwizard等自定义工具包来写。这些框架总是依赖于系统后台很少孤立运行,比如说和数据库、数据管道、负载平衡器或是高速缓存。

从应用程序开发者的角度来看,数据库,RPC 库,负载平衡器和高速缓存只是另一个架构的一块。从根本上讲,这个基础架构由各种系统组件构成。我们看到它的组件由各种语言写就的。例如:数据库已经有不同语言写的,像用Java 写的Cassandra、C++ 写的MongoDB、Erlang 写的CouchDB和Riak、C 写的Postgresql还有Go 写的CockroachDB。数据管道也实现了,例如基于Java 写的Kafka和Spark或是基于C++ 的Heron。高速缓存很大程度上是C的领域(Redis和Memcache)。

很多项目利用多年发展的共享架构的影响,像LevelDB / RocksDB (C++) 这样的遍地开花,SQLite 几乎在任何系统上能够运行。

lnovonl
lnovonl
翻译于 06/15 17:05
0

什么支撑着这座大山?

最后,几乎所有这些系统都依赖于我们操作系统的 Userland 和 Linux 核心。多数仍然使用 Userland 的 Linux 发行版在很大程度上是由 C 语言编写。虽然 GNU C 拥有数量大得难以致信的扩展,提供了形式化内存模型、线程局部变量等各种能力,但 C 就是 C。目前已经存在使用 GoRust 和 Nim 来编写 Userland 的尝试,但这些项目都还没有广泛应用。

最后,几乎所有人都在Linux 内核中运行软件。Linux 核心是 C 编写的,而且并没有很好的理由来转向 C++。也存在一些其它语言编写的内核,比如 Fuschia 和 MirageOS,但它们都还没有成熟到可以应用于生产环境。

边城
边城
翻译于 06/16 22:50
0

逃离C

我不愿看到我在不同的语言中总是选择C来编写一个非常简单的用户实用程序。目前Go已经在很大程度上取代了我使用C的习惯。选择Go与其说出于我的喜好,不如说出自它的务实。 我得到了内存管理,大量的构建工具和更简单的语言。 我失去了泛型、宏,以及对我所做的事情的洞察力。

用例

我试图编写一个程序来完成工作上的小事。 这是一个POSIX信号帮手。 实际上,它是容器的入口点,负责在容器终止期间调用关闭脚本。 它将运行那些注定会运行到终止的服务,因此,出于程序员的错误或机器故障或由于外部信号才会导致这些服务终止。

通常,我们想要在关机前做一些事情。 时而是把我们从服务搜索中注销,时而是在关闭后保存一些应用程序的状态。 几乎所有的传统PID 1具有这种功能,但是将诸如systemd之类的东西放入容器中是不起作用的。

大部分工作仅是在系统调用,像forkexecsigmaskwait4sigtimedwait。事实上它们在容器里运行,这就意味着我们不能凭借大量运行时间或是一组可用共享库。我们最多能依赖libc.so.6

lnovonl
lnovonl
翻译于 06/18 12:09
0

评估

我试图写出这一堆语言的评估。在大多数情况下,这就是一个错误的开始,并且这种语言不能满足我对技能的需求。为了尽早解决这件糟糕的事情,我最终妥协了,在Go上编写代码。即使与Go很接近,它仍然很尴尬。由于fork/exec通过os/exec进行管理,因此不能简单地在不破坏exec的情况下开始监听主goroutine中收到的所有信号。

Nim

Nim从一开始就是一个错误。我需要在独立的进程组中运行代码,而不是信号包装器,因此当进程组获得信号时。如果您手动执行fork/exec过程并且调用setpgid,则可以“手动”执行此操作。

更大的问题是信号处理器的安全性。信号处理程序的安全性在Nim中没有很好的定义,并且存在一个开放的Github 问题。运行时似乎需要独立的信号处理进程。

一般来说,它看起来像一个整洁的语言,我真的希望它发展壮大。在指定和类型上也存在尴尬。我最喜欢这门语言的地方是它可以编译到多个后端,并且可以探索中间层状态。

凉凉_
凉凉_
翻译于 06/22 09:23
0

Pony

Pony仍处于起步阶段。我没有真正使用它的原始用例。我一定会找出语言所缺失的功能。

有了这个说法,这个语言本身就非常有趣。它是一种简单的语言,带有一个简单的工具链。 “构建工具”(ponyc)“正常工作”。它也产生一个小的二进制文件,它具有最小的依赖性。

这就是说,这也是一个错误的开始。首先,没有程序退出,没有办法只听信号。我向Pony团队提交了一个PR,并且他们合并在这个功能中。

另一个问题是fork和exec进程的机制并不适合我的需求。它没有能力在文件描述符周围进行随机排列,也没有能力在fork之后运行诸如setpgid之类的东西。

好处在于,核心Pony代码非常简单,所以我开始讽刺ASIO子系统开始启用这些功能。考虑到它的运行时间有多简单,它不需要太多工作就可以让Pony完成这项任务,但我没有时间和精力去编写RFC。

围绕这个问题的复杂性很多是这样,尽管我可能会开始撞击FFI,并粘上一些位,但我失去了语言的很多好处。这个语言有一个概念,在我看来PLT首先是“能力安全”的概念,但在进一步的理解中,它是一个非常强大的工具。我认为它解决了C ++社区所面临的一个大问题,即使你编写了很好的代码,你带入的库也可以像YOLO一样编写。

Pony的团队也有一个伟大的哲学。他们明确关心程序员的生产力。

Pony哲学

本着理查德加布里埃尔的精神,Pony哲学既不是“正确的事物”,也不是“更糟糕或更好的”。而是“完成东西”。

正确性

错误是不允许的。如果你无法保证结果是正确的,那么尝试完成任务是毫无意义的。

性能

运行速度比正确性更重要。如果性能必须牺牲正确性,试着想出一种新的方式来做事。程序可以让东西做得越快,效果就越好。除了一个正确的结果,这比任何事都重要。

简单

简单性可以牺牲性能。接口比实现更简单更重要。程序员做得越快,效果就越好。为了提高性能,让程序员的工作变得更加困难是可以的,但更重要的是让程序员更容易,而不是让语言/运行时更容易。

[节选]

凉凉_
凉凉_
翻译于 06/22 09:53
0

Reason (OCaml)

我曾尝试使用OCaml上Facebook风格的Reason编写此功能。在此列表上看到一个函数式编程语言真是太棒了。在写这篇文章时,Reason主要针对前端开发人员,而不是他们称之为的“原生”开发人员。

Reason是OCaml的原因是什么呢 - Reason实际上就是他们所声称的“Transpiler”,并依靠OCaml来完成繁重的搬移。这意味着我们拥有了20年的OCaml传统和工具集。这几乎包括opam软件包库中的所有内容。

没有人告诉我的是OCaml的运行时不能同时完成两件事。它有一个Python风格的GIL。它仅在2015年推出了多核支持。

大致的实现如下,其中部分省略,因为它们嵌入到内部系统中了。

open Unix;
open Printf;
open Sys;
open ExtUnix.All;
open Thread;
open CCBlockingQueue;
open CCTimer;

/*
 * How long do we wait for the discovery deregistration
 * process to complete
 */
let discovery_deregistration_timeout = 30.;

/*
 * How long do we wait after successfully removing
 * outselves out of discovery before we begin to
 * forward signals
 */
let discovery_grace_timeout = 60.;

type exn +=
  | Unknown_process_state;

let rec waitpid_loop = (pid: int) =>
  /* TODO, Wrap in Misc.restart_on_EINTR */
  switch (Unix.waitpid([], pid)) {
  | (_, WEXITED(return_code)) =>
    Log.debug("Process exited with return code: %d\n", return_code);
    Pervasives.exit(return_code);
  | (_, WSIGNALED(signal_code)) =>
    Log.info("Process exited with signal code: %d\n", signal_code);
    Pervasives.exit(128 + signal_code);
  | exception (Unix.Unix_error(Unix.EINTR, "waitpid", _)) =>
    Log.debug("Received unix interrupt error, restarting waitpid loop");
    waitpid_loop(pid);
  | _ => raise(Unknown_process_state)
  };

type signal_msg =
  | DiscoveryDeregistrationComplete
  | DiscoveryGracePeriodComplete
  | DiscoveryTimeout
  | Signal(int);

type exn +=
  | UnexpectedMessage(signal_msg);

let rec signal_listener_forwarder = (pid, sigq) => {
  switch (CCBlockingQueue.take(sigq)) {
  | Signal(sig_val) =>
    Log.info("Forwarder processing signal: %d", sig_val);
    Unix.kill(pid, sig_val);
  | DiscoveryTimeout => ()
  | DiscoveryDeregistrationComplete => ()
  | DiscoveryGracePeriodComplete => ()
  /* add arbitrary wait bit */
  };
  signal_listener_forwarder(pid, sigq);
};

let rec signal_listener_wait = (pid, sigq, first_signal) => {
  /* Wait for some arbitrary timeout, and then forward signals, or if we get a signal,
   * start forwarding all signals
   */
  switch (CCBlockingQueue.take(sigq)) {
  | Signal(sig_val) =>
    Log.info(
      "Going into forwarding signal mode, during wait, due to signal: %d",
      sig_val
    );
    Unix.kill(pid, first_signal);
    Unix.kill(pid, sig_val);
    ignore(signal_listener_forwarder(pid, sigq));
  | DiscoveryGracePeriodComplete =>
    Log.info("Going into forwarding signal mode, wait is completed");
    Unix.kill(pid, first_signal);
    ignore(signal_listener_forwarder(pid, sigq));
  /* Both of these messages can come in late */
  | DiscoveryTimeout => ()
  | DiscoveryDeregistrationComplete => ()
  /* add arbitrary wait bit */
  };
  signal_listener_wait(pid, sigq, first_signal);
};

let signal_listener_phase1 = (pid, sigq, first_signal, discovery_timer) =>
  /* TODO: kickoff discovery registration */
  switch (CCBlockingQueue.take(sigq)) {
  | DiscoveryTimeout =>
    Log.error("Received discovery timeout");
    signal_listener_wait(pid, sigq, first_signal);
  | DiscoveryDeregistrationComplete =>
    Log.info("Discover deregistration completed");
    CCTimer.stop(discovery_timer);
    /*
     * Even though we stopped the timer here,
     * we still might get a message from it
     * since it's async
     */
    let grace_period_timer = CCTimer.create();
    CCTimer.after(grace_period_timer, discovery_grace_timeout, () =>
      assert (CCBlockingQueue.try_push(sigq, DiscoveryGracePeriodComplete))
    );
    signal_listener_wait(pid, sigq, first_signal);
  | Signal(sig_val) =>
    Log.info(
      "Going into forwarding signal mode, during discovery de-registration, due to 2nd signal: %d",
      sig_val
    );
    CCTimer.stop(discovery_timer);
    Unix.kill(pid, first_signal);
    Unix.kill(pid, sig_val);
    signal_listener_forwarder(pid, sigq);
  | e => raise(UnexpectedMessage(e))
  /* Add successful discovery completion here */
  /* add wait for discovery bit */
  };

let discovery_deregistration = sigq => {
  Log.info("Beginning discovery deregistration");
  Unix.sleep(5);
  assert (CCBlockingQueue.try_push(sigq, DiscoveryDeregistrationComplete));
};

let signal_listener_thread = (pid, sigq) => {
  /*
   * In this state, the loop is just listening, and waiting for a signal.
   * Once we receive a signal, we kick off deregistration in discovery,
   * and we run that with timeout N. Either timeout N must elapse, or
   * the discovery deregistration must finish. Once that happens,
   * we forward the signal that we last received.
   *
   * If at any point, during this we receive another signal,
   * all bets are off, and we immediately start forwarding
   * signals.
   */
  let sig_val =
    switch (CCBlockingQueue.take(sigq)) {
    | Signal(sig_val) => sig_val
    | e => raise(UnexpectedMessage(e))
    };
  let _ = Thread.create((_) => discovery_deregistration(sigq), ());
  let timer = CCTimer.create();
  CCTimer.after(timer, discovery_deregistration_timeout, () =>
    assert (CCBlockingQueue.try_push(sigq, DiscoveryTimeout))
  );
  signal_listener_phase1(pid, sigq, sig_val, timer);
};

let rec signal_cb_thread = (sigq, signals) => {
  let my_sig = Thread.wait_signal(signals);
  assert (CCBlockingQueue.try_push(sigq, Signal(my_sig)));
  signal_cb_thread(sigq, signals);
};

let parent = (pid: int, signals) => {
  let sigq = CCBlockingQueue.create(max_int);
  let _ = Thread.create((_) => ignore(signal_listener_thread(pid, sigq)), ());
  let _ = Thread.create((_) => ignore(signal_cb_thread(sigq, signals)), ());
  waitpid_loop(pid);
};

let child = () => {
  /* Replace with real child execution code */
  Sys.set_signal(
    Sys.sigint,
    Signal_handle((_: int) => Log.info("Saw SIGINT"))
  );
  Sys.set_signal(
    Sys.sigterm,
    Signal_handle((_: int) => Log.info("Saw SIGTERM"))
  );
  let _ = Unix.sigprocmask(SIG_UNBLOCK, [Sys.sigint, Sys.sigterm]);
  ExtUnix.All.setpgid(0, 0);
  Log.info("In child");
  let _ = Unix.sleep(1000);
  Log.info("Done sleeping");
  Pervasives.exit(1);
};

let () = {
  Log.set_log_level(Log.DEBUG);
  Log.set_output(Pervasives.stderr);
  Log.color_on();
  let signals = [Sys.sigint, Sys.sigterm];
  let _ = Unix.sigprocmask(SIG_BLOCK, signals);
  switch (Unix.fork()) {
  | 0 => child()
  | pid => parent(pid, signals)
  };
};

所以,这真的可以正常运行。

Tocy
Tocy
翻译于 06/19 09:37
0

Channels

最初,我曾使用的是CCBlockingQueue以提供同步机制。这是在等待信号的线程和正在协同的线程之间传递信息的好方法。我在队列中使用了sum类型,所以我可以继续并使用匹配。这是一个状态机的穷人版实现 - 除了我必须能够详尽地处理所有点上的所有消息。模式匹配使得这一切变得轻而易举,但它仍然略显笨拙。

从这一点上来看,这有点令人生厌,因为我在整个队列中使用了一种sum类型的消息,因为没有显式的方式可以一次等待多个队列。在我的Rust实现中,我使用了channel_select!宏中的channel(https://github.com/BurntSushi/chan)。如果能够在OCaml中执行相同的操作,或者存在可用的库来处理这个问题,那将是非常好的。

我遇到的另一个问题是处理定时器。同样,因为我使用的机制依赖于这个单一队列,所以我需要或是使用一个单线程作为计时器的轮毂来推送到期消息,或者为每个计时器启动一个线程。

Tocy
Tocy
翻译于 06/15 10:08
0

信号处理

这是进一步深入文档。当我注册一个信号处理程序,而不是使用wait_signal (sigtimedwait)时,很难排查为什么处于死锁状态。我已了解到信号处理程序在OCaml运行时(参见:GIL)中是“不安全的”,并且可能阻止其他线程执行。

系统API

OCaml最棒的部分之一是对系统API的访问。诸如调用fork、setpgrp等的直接机制非常棒。跨平台的信号转换逻辑有点令人费解,但并不存在使用文档无法解决的问题。

构建系统

我认为OCaml人应该学习下Rust人的作法。我最终手工编写了一个Makefile,并使用了重建,但我设想下:如果项目变得复杂得多,或者涉及多个文件,手工完成这项工作将变得很笨拙。目前有Oasisocamlbuild, 和Jenga--但所有这些都比语言本身的学习曲线更陡峭。

Tocy
Tocy
翻译于 06/15 09:49
0
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(4)

hsl727261250
hsl727261250
go可以当系统语言???
zhjphp
zhjphp
编程语言都是复杂到简单的方式进化的就像c到go
Artrener
Artrener
其它语言如果不能做到远比原有语言的易学,开发效率高,稳定安全,那它就无法取代原有语言,充其量打打辅助
MikeManilone
MikeManilone
很难想象一个会用 OCaml 的人会觉得 Rust 囊括了 C++ 的复杂性…
返回顶部
顶部