JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

系統呼叫:程式如何向作業系統求助

你的程式無法靠自己碰觸磁碟、網路或另一個程式的記憶體。系統呼叫是它請求核心代為完成這些工作的唯一官方門戶——本篇就帶你一步步走過它敲門時究竟發生了什麼。

那道牆,以及穿過它的唯一一扇門

從上一篇你已經知道那條大分界:你的程式在使用者模式中執行,被硬體圍起來,而核心在特權的核心模式中執行,是唯一被允許碰觸裝置、發放記憶體、伸手進機器的程式碼。那道圍籬正是重點所在——它讓一個出錯或懷有惡意的程式無法毀掉其他所有人。但一道沒有門的圍籬毫無用處:一個只能在自己小盒子裡做算術、別無他能的程式,算不上什麼程式。系統呼叫就是那扇門。

想像一家銀行。你不能就這麼走進金庫把現金捧走——那是被禁止的,且理由充分。但你可以走到櫃員窗口,填一張單據,寫清楚你要什麼,受信任、待在櫃檯後面的櫃員便會檢查你的請求,並代你完成特權工作。系統呼叫就是那個櫃員窗口:從你的程式進入核心唯一的、有人看守的官方通道。穿過那道牆,沒有別的被認可的路。

所以一個程式做的幾乎所有有趣的事——開啟檔案、在螢幕上繪圖、把位元組送上網路、啟動另一個程式、要求更多記憶體——其實都是程式透過系統呼叫請求核心。純粹的運算(把數字相加、為記憶體中的清單排序)留在盒子裡,根本不需要任何系統呼叫。一旦程式想觸及外面的世界,它就得敲這唯一的一扇門。

你呼叫的,不是核心所執行的

有個幾乎人人一開始都會卡住的重點:當你寫程式時,你幾乎從不親自做原始的系統呼叫。你呼叫的是函式庫裡某個友善的函式——一個有整齊名稱、參數與回傳值的東西——由它替你抵達核心。這就是 API 與系統呼叫的區別API(應用程式介面)是給你呼叫的一組函式;系統呼叫則是底層可能發生的、真正跨入核心的那個動作。

兩者之間的對應並非一對一,這點值得釘牢。有時一次 API 呼叫剛好觸發一次系統呼叫。有時一次 API 呼叫根本不觸發任何系統呼叫——為陣列排序不需要核心幫忙,所以它做零次系統呼叫。還有些時候函式庫很聰明:像 fprintf 這樣的呼叫,可能只把你的文字暫存在你自己記憶體裡的緩衝區中,直到緩衝區滿了,稍後才發出單一一次真正的 write(...) 系統呼叫,把上千次細小的 API 呼叫,化為一次昂貴的跨越。

穿門往返,一步一步來

讓我們跟著單一一次呼叫一路進去再出來。假設你的程式執行 read(fd, buf, 100):「從已開啟的檔案 fd 讀取最多 100 個位元組到緩衝區 buf。」第一個引數 fd 是一個檔案描述符——核心稍早交給你的一個小整數,用來指名你某個已開啟的檔案,就像寄物牌指名你的外套那樣。真正的特權工作(與磁碟溝通)只有核心能做,所以這看似無害的一行,將會徹底離開使用者模式。

  1. 打包請求。程式把系統呼叫編號(它想要哪項服務——read)與引數(檔案描述符、要把位元組放哪裡、放多少)放進約定好的 CPU 暫存器,遵循一套固定的慣例。
  2. 敲門。程式執行一條特殊的陷阱指令。硬體在一個不可分割的步驟裡,把模式位元翻成核心模式、保存程式執行到哪裡以便日後恢復,並跳到核心內部單一固定的進入點。
  3. 核心查找你要的東西。它用系統呼叫編號,在自己的系統呼叫表中索引,找到正確的處理常式——就像翻到索引裡的某個條目,找到你需要的那一頁。
  4. 核心驗證每一個引數。fd 真的是你已開啟的檔案之一嗎?buf 真的落在你自己的記憶體裡嗎?它不能做任何假設——惡意程式會故意傳入壞的指標與超出範圍的數值。
  5. 核心執行特權工作——從磁碟取出位元組、把它們複製進你的緩衝區——然後把結果(它實際讀到了幾個位元組)寫進一個暫存器。
  6. 穿門返回。核心執行一條「從陷阱返回」的指令,把模式位元翻回使用者、恢復你先前保存的狀態,並在你程式的下一行恢復執行——此時它已握有資料與回傳值。

從你程式的角度看,這一切機制都看不見。read(...) 看起來就像一次平常的函式呼叫,只是異常地強大:你呼叫它、你等待、你得到答案。整趟進出核心的旅程,就發生在你程式的兩行之間。

兩種門鈴,一道門戶

陷阱指令是進入核心的禮貌、刻意的方式:你的程式選擇去敲門。但核心也可能在你程式裡沒人請求的情況下被召喚。當網路卡接收完一個封包、或你按下一個鍵時,裝置會引發一個中斷——就像櫃台前的門鈴響起。CPU 放下手邊的事、切換到核心模式、執行核心對那個裝置的處理常式,然後返回。

誠實的區別在於起因與時機。陷阱(你的系統呼叫,或像除以零這類錯誤)是同步的:它由 CPU 此刻正在執行的指令引起,發生在可預測的時刻。硬體中斷是非同步的:它由外部裝置引起,可能在你任兩條指令之間毫無預警地突然發生。它們是起因不同的不同事件——卻共用完全相同的管路來跨入核心,這也是為什麼兩者有時被籠統地合稱為「中斷」。

為何跨越有代價,又為何必須謹慎

有兩個誠實的重點,會延伸成真實的效能與真實的安全。第一,系統呼叫確實比一般函式呼叫昂貴。切換模式位元、保存與恢復狀態、驗證引數,以及(往往)在你等待慢速硬體時的一次環境切換,全都要花真實的時間。所以寫得好的程式不會做上百萬次細小的系統呼叫;它會批次處理、加緩衝,一次讀一大塊,而不是一次讀一個位元組。這正是那個 fprintf 緩衝區存在的原因。

第二,核心絕不能信任系統呼叫遞給它的引數。這道邊界是作業系統安全的最前線。惡意程式會故意傳入一個指向別人記憶體的指標,或一個遠大於它所給緩衝區的長度,盼著核心盲目地讀寫它不該碰的地方。在這唯一的門戶謹慎檢查——這個描述符是你的嗎、這個緩衝區是否落在你自己的記憶體內——正是讓一個程式無法把核心當成撬棍去對付另一個程式的核心一環。

退一步看,整幅圖景既簡單又有力。系統呼叫是每一項作業系統服務真正被抵達的方式——行程控制、檔案與裝置輸入輸出、程式之間的通訊、保護檢查,全都如此。那套確切的暫存器慣例與呼叫編號,構成了所謂的應用程式二進位介面,也就是程式與核心之間的低階合約。你很少會親手寫一個原始的系統呼叫,但現在你已經知道,每當你那友善的函式庫函式默默替你越過那道牆時,到底發生了什麼。