Rust運行時指南(官方文檔翻譯)

Rust運行時指南(官方文檔翻譯)

A Guide to the Rust Runtime, by Alex Crichton and Brian Anderson

翻譯:莊曉立(Liigo),[email protected],G+,Weibo,CSDN,Rust中文圈

日期:2014年2月。


2015年5月20日譯者Liigo注:此文形成於Rust 1.0之前的開發動盪期,目前已經嚴重過時(outdated)!相關設施在Rust 1.0標準化過程中發生了巨大變化(參見RFC #230),Runtime已經不存在了。特此聲明,以免誤導讀者。


Rust編程語言的標準發行版包含兩個運行時庫(libgreen和libnative),提供I/O等基礎設施的統一接口。但對Rust語言本身而言,運行時(runtime)並不是必需的;Rust編譯器可以生成在所有環境中運行的代碼,包括內核(kernel)環境。Rust語言也不需要運行時提供內存安全,因爲它的類型系統本身已經足夠安全——通過編譯時靜態驗證給予保證。運行時只是利用語言的安全特性提供一系列便利的、安全的、高層的抽象。

如果Rust沒有運行時(runtime),我們編程能做的事情非常有限,所以Rust需要提供運行時。這份指導手冊將探討Rust用戶空間(user-space)的運行時、如何使用它、它能做什麼。

1、什麼是運行時?

Rust運行時可以被視爲提供以下功能代碼的組合:輸入/輸出(I/O)、任務孵化(task spawning)、任務本地存儲(TLS)等等。本質上,它提供一少部分對象,爲實現常見功能提供便利支持。Rust運行時自身的實現是自包含的(self-contained),避免干擾其他庫。

運行時目前提供以下功能特性(不完整列表):

  • 輸入/輸出(I/O)
  • 任務孵化(task spawning)
  • 消息傳遞(message passing)
  • 任務同步(task synchronization)
  • 任務本地存儲(TLS, task-local storage)
  • 日誌(logging)
  • 本地堆(local heaps)(GC heaps)
  • 任務展開(task unwinding)

1.1、運行時的目標是什麼?

運行時的設計初衷是達成以下目標:

  • Rust庫能夠在多種環境中運行,而不需要關心各種環境的具體細節。經常被提及的兩種環境是M:N和1:1。Rust運行時最初首先支持M:N,現在也同時支持了1:1。
  • Rust運行時在達成在多種環境中運行的目標時,不需要強制區分不同的編譯模式。一個庫被編譯一次就能在多種不同的環境中永久運行,是我們明確的設計目標。
  • Rust運行時應當高效運行。在其架構設計上,不應該有阻止程序高效運行的障礙。但也不是說非得在任何情況下都必須以最快的速度運行。
  • Rust運行時應當儘可能對用戶透明。不鼓勵用戶直接與運行時交互。

2、運行時的體系結構

本節將介紹目前的Rust運行時的體系結構。Rust運行時曾經被重寫了幾遍,本節僅涉及當前最新版本。

2.1、本地任務(a local task)

Rust運行時的核心抽象概念是任務(Task)。任務代表了運行Rust代碼的“線程”,但此“線程”並不一定直接對應於操作系統裏的線程。運行時裏的大多數服務都是通過Task提供給用戶,因而可以做到單個任務內部決策。

採用這種策略的結果是,要求所有使用標準庫的Rust代碼,都有一個本地的Task結構體(local Task structure)。該結構體被存儲在操作系統的線程局部存儲(TLS, Thread Local Storage)內部,以便高效訪問。

一定有這麼一個Task結構體存在,是Rust運行時本質上唯一的假定。這是一個核心假定,令所有使用標準庫的代碼受益,因此Task被定義在標準庫內。幾乎所有運行時服務都是通過Task提供的。

2.2、輸入/輸出(I/O)

當處理I/O時,通常有一些約定俗成的方法,但這些方法未必在任何情況下都正確。I/O的處理非常複雜,幾乎不可能在多種環境中使用一致的處理方案。不能保證Rust任務(Task)有權限處理I/O,也不能保證它以何種方式處理I/O。

這意味着,標準庫中無法定義處理I/O功能的具體實現代碼,只能定義一批I/O操作接口,由各環境下的Task各自實現具體功能。這些I/O接口被設計爲以同步I/O調用爲核心。此架構不會根本性的阻礙以其他形式處理I/O,但目前還沒有別的處理方式。

運行時(runtime)必須實現的這些I/O接口被定義在std::rt::rtio模塊內。注意這些接口是不穩定的,是不對用戶公開的(僅作爲標準庫內部實現細節)。

(譯者Liigo注:任務的接口被定義在標準庫libstd中,任務的具體實現被定義在運行時庫libgreen/libnative中。)

2.3、任務孵化(Task spawning)

任務(Task)的一項常見操作是,孵化(spawning)一個子任務(child task),在其中執行某些工作。這意味着並行執行被啓用。如何孵化子任務,沒有一個統一方法(未在標準庫中定義),由各(運行時內的)任務自行決定。

任務孵化被解釋爲“孵化一個子任務(原文爲sibling,疑爲child之筆誤)”,其高層操作接口定義在std::task模塊中。孵化子任務前,可以事先設定子任務的參數,運行時的實現必須依據這些參數,執行具體的孵化行爲。

任務的另一個操作是處理自身的運行狀態,如阻塞(block)和喚醒(wake up)任務。具體操作細節由任務自己決定,標準庫未做規定。

2.4、運行時接口(trait Runtime)和任務結構體(struct Task)

運行時的所有特性都被定義在接口Runtime和結構體Task。在不同運行時庫(libgreen、libnative)中,結構體Task都是相同的,而接口Runtime的實現各不相同。Task內部存儲了Runtime接口的實現對象,因而可以調用其接口函數。

3、運行時的實現(implementations)

Rust發行版提供了兩個運行時庫,分別是1:1線程模型的libnative和M:N線程模型的libgreen。就像許多計算機科學問題一樣,你很難說選擇哪個運行時庫是正確的,它們各自都有優勢和劣勢。下面分別介紹兩個運行時庫提供的功能和沒有的功能,供程序員參考並自行決定選擇使用哪一個。

3.1、1:1 - 使用libnative

libnative運行時庫的實現,是基於操作系統本地線程,加上libc阻塞I/O調用。因其用戶空間的線程一一對應於操作系統線程,而被稱爲1:1線程模型。

在這種模型下,每一個Rust任務(Task)對應於一個操作系統線程,並且每一個I/O對象唯一對應一個文件描述符(fd)(或者其他系統內的對等物)。

使用libnative的一些優勢:

  • 保證與FFI綁定(外部函數接口綁定)交互操作。即使你調用的外部C庫函數(例如數據庫驅動)阻塞在線程I/O,也不會干擾其他Rust任務(Task)正常執行(因爲它們在不同的系統線程內)。

  • 在某些情況下相比M:N有更少的I/O損耗。並非所有M:N I/O都保證盡最大可能的快,而且有些東西(比如文件系統API)在某些平臺下不是真正的異步操作,意味着M:N實現可能會比1:1實現引發更多損耗。

3.2、M:N - 使用libgreen

運行時庫libgreen基於異步I/O框架libuv實現了“綠色線程”。M:N線程模型中的M是指當前進程內的操作系統本地線程個數,N是指Rust任務個數(即綠色線程個數)。在這種模型中,M個系統線程調度運行N個Rust綠色線程,在用戶空間中進行線程上下文切換(context switching)。(譯者Liigo注:通常情況下,N遠大於M。)

M:N模型中很重要的一點是,Rust任務不能使用同步系統調用,阻塞任務自身。一旦被阻塞,任務所屬的系統本地線程就整個僵化,無法再運行其他Rus任務。這意味着M個本地線程被(暫時)廢掉了一個(但還有M-1個本地線程繼續工作,因而能夠做到0死鎖)。通過在底層調用異步I/O接口(但從用戶使用的角度看仍然像同步接口),系統本地線程永遠不會阻塞。

libgreen庫沒有任何I/O實現,僅實現了Rust綠色線程的調度器(Schedulers)。真正的I/O實現位於libuv的封裝庫librustuv中。這麼做的目的是希望將來會有不依賴libuv的I/O實現版本(當然目前還沒有)。

使用libgreen的一些優勢:

  • 任務孵化的速度快。在M:N模型中,孵化新任務(綠色線程)時可以完全避免系統調用,效率更高。

  • 任務切換的速度快。因爲上下文切換是在用戶空間進行的,所有任務間競爭操作(互斥、管道等)不用執行系統調用,因而速度更快。更高效的上下文切換也會促成更大的吞吐量。

3.2.1、調度器池(Pool of Schedulers)

M:N線程模型基於以下思路構建:通過M個操作系統本地線程(在libgreen中被稱爲M個調度器,它們共同組成一個調度器池),調度運行N個Rust任務(或稱之綠色線程)。通過green::SchedPool類型可以細粒度地控制調度器池。SchedPool還是唯一能夠孵化新的M:N任務的類型。新孵化的任務,跟當前任務一樣,平等的從屬於同一個調度器池。新任務必然持有調度器池的句柄,用於內部調用以便孵化其他任務。

3.3、選擇哪一個?

既然有兩個運行時庫的實現,顯然要做出決定選擇使用哪一個。默認情況下,編譯器總是鏈接其中之一。當前默認的運行時庫是libgreen,但是今後默認的運行時庫將會是libnative。

編譯器默認選擇鏈接一個運行時庫,滿足了用戶的簡單需求。而且這種默認行爲不是強制性的,用戶還可以自行選擇使用哪個運行時庫。

例如,這個程序將鏈接到默認運行時庫:

fn main() {}

然而下面這個程序就是由用戶決定明確的鏈接到特定的運行時庫(libgreen):

extern mod green;

#[start]
fn start(argc: int, argv: **u8) -> int {
    green::start(argc, argv, main);
}

fn main() {}

兩個運行時庫libgreen和libnative都提供了上層的start函數,用於在各自運行時中啓動初始的第一個Rust任務(Task)。

4、運行時在哪裏?

運行時的源代碼分散在以下多個地方:

---------------- 全文完 ----------------

譯者Liigo注:這篇文章英文原文多有重複混亂處,而我限於英文水平和Rust技術水平,有時也不能完全理解原意。如有翻譯不周,敬請諒解,並懇請指正。我的聯繫方式在本文開頭。

本文地址:

發佈了275 篇原創文章 · 獲贊 442 · 訪問量 244萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章