使用 Rust 進(jìn)行系統(tǒng)編程 — 第一部分
今天的文章是關(guān)于系統(tǒng)編程的。Rust 作為系統(tǒng)編程語(yǔ)言,自然是很適合進(jìn)行系統(tǒng)編程。
現(xiàn)代計(jì)算機(jī)是一個(gè)非常復(fù)雜的創(chuàng)造物,經(jīng)過(guò)幾十年的研究和發(fā)展演變成現(xiàn)在的狀態(tài)。有時(shí)它看起來(lái)像是黑魔法。這里面沒(méi)有魔法,只有科學(xué)。然而,一些像 Alan Turing,Charles Babbage,Ada Lovelace,John von Neumann 和許多其他人的頭腦是不可思議的,因?yàn)樗麄兪褂?jì)算機(jī)成為可能。
現(xiàn)在,讓我們深入學(xué)習(xí)系統(tǒng)編程的基礎(chǔ)知識(shí):
進(jìn)程是什么? 它們是如何創(chuàng)建和執(zhí)行的? 查看 Rust 中的一些代碼示例,并將它們與 C 進(jìn)行比較
在開(kāi)始編寫(xiě)代碼之前,我們將從操作系統(tǒng)主要組件的最底層開(kāi)始構(gòu)建。如圖 1 所示——任何計(jì)算機(jī)的最低級(jí)別是 Hardware,其次是運(yùn)行在裸機(jī)上的 Kernel 模式。這就是像 Linux 這樣的操作系統(tǒng)所在的位置。

在內(nèi)核模式之上,我們有一個(gè)用戶(hù)模式。為了使用戶(hù)能夠與內(nèi)核交互并使用其他更高級(jí)別的軟件,如網(wǎng)頁(yè)瀏覽器、電子郵件閱讀器等,它需要一個(gè)用戶(hù)界面程序。這可以是一個(gè)窗口,圖形用戶(hù)界面,也可以是一個(gè) Shell,它是一個(gè)解釋命令的命令,用于從終端讀取命令并執(zhí)行它們。
進(jìn)程:父和子
所有操作系統(tǒng)的主要概念是進(jìn)程。一個(gè)進(jìn)程基本上是一個(gè)正在運(yùn)行的程序。你可以把它想象成一個(gè)抽屜,里面包含有關(guān)這個(gè)特定程序的所有信息。有些進(jìn)程在計(jì)算機(jī)啟動(dòng)時(shí)開(kāi)始運(yùn)行,有些在后臺(tái)運(yùn)行,有些由用戶(hù)通過(guò) Shell 調(diào)用和交互。
所有進(jìn)程都有一個(gè) id。當(dāng)系統(tǒng)啟動(dòng)時(shí),將啟動(dòng)第一個(gè)進(jìn)程。這個(gè)進(jìn)程的 id 為1,稱(chēng)為 init。在此之后,init 將調(diào)用其他進(jìn)程等等。當(dāng)我們?cè)?shell 中鍵入一個(gè)命令供 OS 執(zhí)行時(shí),系統(tǒng)應(yīng)該創(chuàng)建一個(gè)新的進(jìn)程來(lái)運(yùn)行編譯器。當(dāng)進(jìn)程完成編譯后,它將進(jìn)行一個(gè)系統(tǒng)調(diào)用來(lái)終止自己。
在 UNIX 系統(tǒng)中,每個(gè)新進(jìn)程都是某個(gè)父進(jìn)程的子進(jìn)程。進(jìn)程創(chuàng)建是通過(guò)克隆父進(jìn)程來(lái)完成的,這被稱(chēng)為 forking (圖1-b)。每個(gè)進(jìn)程有一個(gè)父進(jìn)程,但可以有多個(gè)子進(jìn)程。進(jìn)程的結(jié)構(gòu)類(lèi)似于樹(shù),其中 init 是根,這意味著它位于層次結(jié)構(gòu)的頂部。
在進(jìn)程創(chuàng)建之后,除了父進(jìn)程有一個(gè)非 0 ID 號(hào),子進(jìn)程的 ID 等于 0 外,其他方面父進(jìn)程和子進(jìn)程是相同的。接下來(lái),系統(tǒng)用一個(gè)新程序替換子進(jìn)程的執(zhí)行。當(dāng)進(jìn)程完成其目的時(shí),它將正常地終止并退出(自愿的)。該進(jìn)程也可以由于一個(gè)錯(cuò)誤退出或殺死另一個(gè)進(jìn)程(非自愿)。

該系統(tǒng)還跟蹤所有的進(jìn)程,將它們的數(shù)據(jù)保存在所謂的進(jìn)程表中。它包含諸如進(jìn)程 id、進(jìn)程所有者、進(jìn)程優(yōu)先級(jí)、每個(gè)進(jìn)程的環(huán)境變量、父進(jìn)程等信息。除此之外,它還保存特定進(jìn)程處于何種狀態(tài)的信息。每個(gè)進(jìn)程可以處于以下四種狀態(tài)之一:
RUNNABLE — 進(jìn)程正在運(yùn)行/主動(dòng)使用 CPU SLEEPING — 該進(jìn)程是可運(yùn)行的,但是正在等待另一個(gè)進(jìn)程先停止/完成 STOPPED — 此狀態(tài)表示進(jìn)程已暫停以便進(jìn)一步運(yùn)行。它可以通過(guò)信號(hào)重新啟動(dòng)再次運(yùn)行 ZOMBIE — 當(dāng)調(diào)用 “system exit” 或其他人終止進(jìn)程時(shí),進(jìn)程將終止。但是,該進(jìn)程尚未從進(jìn)程表中刪除
通常進(jìn)程必須相互交互,并且可以改變狀態(tài),從 Running 到 Sleeping,然后回到 Running (圖1-c)。這通常由 SIGSTOP 信號(hào)完成,該信號(hào)由 Ctrl + Z 發(fā)出(我們將在接下來(lái)的部分中深入討論信號(hào))。與停止的進(jìn)程一樣,它可以重新啟動(dòng)。一旦被殺死進(jìn)入 Zombie 狀態(tài)就不能重新啟動(dòng)或繼續(xù)。

C VS Rust
在 C 語(yǔ)言中(這是目前 Linux 內(nèi)核編程語(yǔ)言),進(jìn)程創(chuàng)建首先通過(guò) fork 新進(jìn)程來(lái)完成,然后顯式地要求系統(tǒng)在子進(jìn)程上執(zhí)行一個(gè)新指令。如果我們不這樣做,父進(jìn)程和子進(jìn)程將執(zhí)行相同的指令。下面是執(zhí)行 ls 命令的一個(gè)例子,它列出了給定目錄的文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
switch (pid = fork()) {
case -1:
perror("fork failed");
break;
case 0:
printf("I'm child process and I will execute ls command");
char *argv_list[] = {NULL};
if (execv("ls", argv_list) == -1) {
perror("Error in execve");
exit(EXIT_FAILURE);
}
break;
default:
printf("I'm parent process and I'll just print this");
}
return 0;
}
正如你所看到的,我們必須手動(dòng)管理進(jìn)程,并監(jiān)控執(zhí)行是否成功。此外,我們還必須處理錯(cuò)誤。如果我們希望一個(gè)命令只能由一個(gè)子進(jìn)程執(zhí)行,那么我們必須手動(dòng)檢查當(dāng)前進(jìn)程是否是一個(gè)子進(jìn)程,這是按照 case 0 來(lái)完成的。在 Rust 中,標(biāo)準(zhǔn)庫(kù)的進(jìn)程模塊[1]也可以做到這一點(diǎn):
use std::process::Command;
fn main() {
let child = Command::new("ls")
.env("PATH", "/bin")
.output()
.expect("failed to execute process");
// if no error, program will continue..
}這里的 Command::new() 是一個(gè)進(jìn)程構(gòu)建器,負(fù)責(zé)生成和處理子進(jìn)程。就像在 C 代碼中一樣,我們提供要執(zhí)行的命令、環(huán)境變量、命令參數(shù)和調(diào)用輸出方法。輸出將以子進(jìn)程的形式執(zhí)行該命令,等待它完成,并返回收集到的輸出。
除了 output() 之外,我們還可以使用 status() 或 spawn()。這些方法中的每一個(gè)都負(fù)責(zé) fork 一個(gè)具有細(xì)微差異的子進(jìn)程:
output():只有在子進(jìn)程完成運(yùn)行后,才運(yùn)行程序并返回Output的結(jié)果;status():將運(yùn)行程序并在進(jìn)程編譯后返回ExitStatus結(jié)果。這允許檢查編譯程序的狀態(tài);spawn():將運(yùn)行程序并返回結(jié)果,該結(jié)果是一個(gè)子進(jìn)程。這不需要等待程序編譯。該選項(xiàng)允許wait和kill指令,或者我們可以獲得該進(jìn)程的 ID。
在這里,env() 是可選的,因?yàn)?Command 非常聰明,可以查找 /bin 文件夾的路徑。最后,所有的錯(cuò)誤處理都由 expect() 完成。如果 Ok 表示程序成功執(zhí)行或者 Err 表示出現(xiàn)錯(cuò)誤,進(jìn)而 panic!)。如果遇到 Err,希望程序不要終止,可以這樣做:
use std::process::Command;
main() {
let user_input = get_user_input(); // helper function to get user input
if let Err(_) = Command::new(&user_input)
.envs("PATH", "/bin")
.status() {
println!("{}: command not found!", &cmd);
}
// the rest of the program...
}
這里的 status() 更方便,如果用戶(hù)提供合法命令并執(zhí)行,則調(diào)用它將返回 Ok。但我們只對(duì)提供不可用命令時(shí)的處理感興趣。這就是為什么我們只檢查 Err 是否返回,如果返回,則在終端中打印 “command was not found” 并繼續(xù)當(dāng)前程序執(zhí)行,而不是終止。
最后,spawn() 用于管理多個(gè)子進(jìn)程和父進(jìn)程之間的執(zhí)行順序。它包含 stdin stdout 和 stderr 字段,并且具有 c 程序員所熟悉的 wait() , kill() 和 id() 方法。我們將在下一部分中看到進(jìn)程的這一部分,當(dāng)兩個(gè)或多個(gè)線程可以訪問(wèn)共享數(shù)據(jù)并且它們?cè)噲D同時(shí)更改這些數(shù)據(jù)時(shí),我們還將看到 Rust 是如何處理競(jìng)態(tài)條件的。
總結(jié)
在這個(gè)介紹性的部分中,我們回顧了什么是進(jìn)程,它們是如何創(chuàng)建的,并將 Rust 對(duì)進(jìn)程創(chuàng)建和命令執(zhí)行的實(shí)現(xiàn)與 C 進(jìn)行了比較。我們看到,Rust 代碼不僅不容易出現(xiàn)人為錯(cuò)誤,而且不那么冗長(zhǎng),很簡(jiǎn)潔。在接下來(lái)的部分中,我們將介紹如何管理進(jìn)程執(zhí)行時(shí)間和狀態(tài),以及處理系統(tǒng)信號(hào)。
原文鏈接:https://www.bexxmodd.com/post/systems-programming-with-rust-1
參考資料
進(jìn)程模塊: https://doc.rust-lang.org/std/process/struct.Command.html
推薦閱讀
覺(jué)得不錯(cuò),點(diǎn)個(gè)贊吧
掃碼關(guān)注「Rust編程指北」
