Linux 進程必知必會

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一篇文章只是簡單的描述了一下 Linux 基本概念,通過幾個例子來說明 Linux 基本應用程序,然後以 Linux 基本內核構造來結尾。那麼本篇文章我們就深入理解一下 Linux 內核來理解 Linux 的基本概念之進程和線程。系統調用是操作系統本身的接口,它對於創建進程和線程,內存分配,共享文件和 I/O 來說都很重要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們將從各個版本的共性出發來進行探討。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基本概念"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 一個非常重要的概念就是進程,Linux 進程和我們在"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"進程和線程 這篇文章"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"中探討的進程模型非常相似。每個進程都會運行一段獨立的程序,並且在初始化的時候擁有一個獨立的控制線程。換句話說,每個進程都會有一個自己的程序計數器,這個程序計數器用來記錄下一個需要被執行的指令。Linux 允許進程在運行時創建額外的線程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c5/c587203854d07078acbbd913dd002b27.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 是一個多道程序設計系統,因此係統中存在彼此相互獨立的進程同時運行。此外,每個用戶都會同時有幾個活動的進程。因爲如果是一個大型系統,可能有數百上千的進程在同時運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在某些用戶空間中,即使用戶退出登錄,仍然會有一些後臺進程在運行,這些進程被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"守護進程(daemon)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 中有一種特殊的守護進程被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"計劃守護進程(Cron daemon)"}]},{"type":"text","text":" ,計劃守護進程可以每分鐘醒來一次檢查是否有工作要做,做完會繼續回到睡眠狀態等待下一次喚醒。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/70/7015a7b4456c2edd37fb006457b80182.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Cron 是一個守護程序,可以做任何你想做的事情,比如說你可以定期進行系統維護、定期進行系統備份等。在其他操作系統上也有類似的程序,比如 Mac OS X 上 Cron 守護程序被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"launchd"}]},{"type":"text","text":" 的守護進程。在 Windows 上可以被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"計劃任務(Task Scheduler)"}]},{"type":"text","text":"。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Linux 系統中,進程通過非常簡單的方式來創建,"},{"type":"codeinline","content":[{"type":"text","text":"fork"}]},{"type":"text","text":" 系統調用會創建一個源進程的"},{"type":"codeinline","content":[{"type":"text","text":"拷貝(副本)"}]},{"type":"text","text":"。調用 fork 函數的進程被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"父進程(parent process)"}]},{"type":"text","text":",使用 fork 函數創建出來的進程被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"子進程(child process)"}]},{"type":"text","text":"。父進程和子進程都有自己的內存映像。如果在子進程創建出來後,父進程修改了一些變量等,那麼子進程是看不到這些變化的,也就是 fork 後,父進程和子進程相互獨立。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然父進程和子進程保持相互獨立,但是它們卻能夠共享相同的文件,如果在 fork 之前,父進程已經打開了某個文件,那麼 fork 後,父進程和子進程仍然共享這個打開的文件。對共享文件的修改會對父進程和子進程同時可見。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼該如何區分父進程和子進程呢?子進程只是父進程的拷貝,所以它們幾乎所有的情況都一樣,包括內存映像、變量、寄存器等。區分的關鍵在於 "},{"type":"codeinline","content":[{"type":"text","text":"fork "}]},{"type":"text","text":" 函數調用後的返回值,如果 fork 後返回一個非零值,這個非零值即是子進程的 "},{"type":"codeinline","content":[{"type":"text","text":"進程標識符(Process Identiier, PID)"}]},{"type":"text","text":",而會給子進程返回一個零值,可以用下面代碼來進行表示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"pid = fork(); // 調用 fork 函數創建進程\nif(pid < 0){\n error()\t\t\t\t // pid < 0,創建失敗\n}\nelse if(pid > 0){\n parent_handle() // 父進程代碼\n}\nelse {\n child_handle() // 子進程代碼\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"父進程在 fork 後會得到子進程的 PID,這個 PID 即能代表這個子進程的唯一標識符也就是 PID。如果子進程想要知道自己的 PID,可以調用 "},{"type":"codeinline","content":[{"type":"text","text":"getpid"}]},{"type":"text","text":" 方法。當子進程結束運行時,父進程會得到子進程的 PID,因爲一個進程會 fork 很多子進程,子進程也會 fork 子進程,所以 PID 是非常重要的。我們把第一次調用 fork 後的進程稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"原始進程"}]},{"type":"text","text":",一個原始進程可以生成一顆繼承樹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5b/5b16b85030688e14042066294bc7a3a6.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Linux 進程間通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 進程間的通信機制通常被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"Internel-Process communication,IPC "}]},{"type":"text","text":"下面我們來說一說 Linux 進程間通信的機制,大致來說,Linux 進程間的通信機制可以分爲 6 種"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a8/a850c700d19e0f7410c30e8aa262868d.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們分別對其進行概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"信號 signal"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"信號是 UNIX 系統最先開始使用的進程間通信機制,因爲 Linux 是繼承於 UNIX 的,所以 Linux 也支持信號機制,通過向一個或多個進程發送"},{"type":"codeinline","content":[{"type":"text","text":"異步事件信號"}]},{"type":"text","text":"來實現,信號可以從鍵盤或者訪問不存在的位置等地方產生;信號通過 shell 將任務發送給子進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可以在 Linux 系統上輸入 "},{"type":"codeinline","content":[{"type":"text","text":"kill -l"}]},{"type":"text","text":" 來列出系統使用的信號,下面是我提供的一些信號"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ecabc6cdc00b70328472eecd089549b9.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程可以選擇忽略發送過來的信號,但是有兩個是不能忽略的:"},{"type":"codeinline","content":[{"type":"text","text":"SIGSTOP"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"SIGKILL"}]},{"type":"text","text":" 信號。SIGSTOP 信號會通知當前正在運行的進程執行關閉操作,SIGKILL 信號會通知當前進程應該被殺死。除此之外,進程可以選擇它想要處理的信號,進程也可以選擇阻止信號,如果不阻止,可以選擇自行處理,也可以選擇進行內核處理。如果選擇交給內核進行處理,那麼就執行默認處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作系統會中斷目標程序的進程來向其發送信號、在任何非原子指令中,執行都可以中斷,如果進程已經註冊了新號處理程序,那麼就執行進程,如果沒有註冊,將採用默認處理的方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如:當進程收到 "},{"type":"codeinline","content":[{"type":"text","text":"SIGFPE"}]},{"type":"text","text":" 浮點異常的信號後,默認操作是對其進行 "},{"type":"codeinline","content":[{"type":"text","text":"dump(轉儲)"}]},{"type":"text","text":"和退出。信號沒有優先級的說法。如果同時爲某個進程產生了兩個信號,則可以將它們呈現給進程或者以任意的順序進行處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們就來看一下這些信號是幹什麼用的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGABRT 和 SIGIOT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGABRT 和 SIGIOT 信號發送給進程,告訴其進行終止,這個 信號通常在調用 C標準庫的"},{"type":"codeinline","content":[{"type":"text","text":"abort()"}]},{"type":"text","text":"函數時由進程本身啓動"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGALRM 、 SIGVTALRM、SIGPROF"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當設置的時鐘功能超時時會將 SIGALRM 、 SIGVTALRM、SIGPROF 發送給進程。當實際時間或時鐘時間超時時,發送 SIGALRM。 當進程使用的 CPU 時間超時時,將發送 SIGVTALRM。 當進程和系統代表進程使用的CPU 時間超時時,將發送 SIGPROF。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGBUS"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGBUS 將造成"},{"type":"codeinline","content":[{"type":"text","text":"總線中斷"}]},{"type":"text","text":"錯誤時發送給進程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGCHLD"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當子進程終止、被中斷或者被中斷恢復,將 SIGCHLD 發送給進程。此信號的一種常見用法是指示操作系統在子進程終止後清除其使用的資源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGCONT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGCONT 信號指示操作系統繼續執行先前由 SIGSTOP 或 SIGTSTP 信號暫停的進程。該信號的一個重要用途是在 Unix shell 中的作業控制中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGFPE"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGFPE 信號在執行錯誤的算術運算(例如除以零)時將被髮送到進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/34/34d72231d13d0b06708c43e4a7de0400.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGUP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 SIGUP 信號控制的終端關閉時,會發送給進程。許多守護程序將重新加載其配置文件並重新打開其日誌文件,而不是在收到此信號時退出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGILL"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGILL 信號在嘗試執行非法、格式錯誤、未知或者特權指令時發出"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGINT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當用戶希望中斷進程時,操作系統會向進程發送 SIGINT 信號。用戶輸入 ctrl - c 就是希望中斷進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGKILL"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGKILL 信號發送到進程以使其馬上進行終止。 與 SIGTERM 和 SIGINT 相比,這個信號無法捕獲和忽略執行,並且進程在接收到此信號後無法執行任何清理操作,下面是一些例外情況"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"殭屍進程無法殺死,因爲殭屍進程已經死了,它在等待父進程對其進行捕獲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"處於阻塞狀態的進程只有再次喚醒後纔會被 kill 掉"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"init"}]},{"type":"text","text":" 進程是 Linux 的初始化進程,這個進程會忽略任何信號。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGKILL 通常是作爲最後殺死進程的信號、它通常作用於 SIGTERM 沒有響應時發送給進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGPIPE"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGPIPE 嘗試寫入進程管道時發現管道未連接無法寫入時發送到進程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGPOLL"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當在明確監視的文件描述符上發生事件時,將發送 SIGPOLL 信號。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGRTMIN 至 SIGRTMAX"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGRTMIN 至 SIGRTMAX 是"},{"type":"codeinline","content":[{"type":"text","text":"實時信號"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGQUIT"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當用戶請求退出進程並執行核心轉儲時,SIGQUIT 信號將由其控制終端發送給進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSEGV"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 SIGSEGV 信號做出無效的虛擬內存引用或分段錯誤時,即在執行分段違規時,將其發送到進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSTOP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSTOP 指示操作系統終止以便以後進行恢復時"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGSYS"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 SIGSYS 信號將錯誤參數傳遞給系統調用時,該信號將發送到進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SYSTERM"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們上面簡單提到過了 SYSTERM 這個名詞,這個信號發送給進程以請求終止。與 SIGKILL 信號不同,該信號可以被過程捕獲或忽略。這允許進程執行良好的終止,從而釋放資源並在適當時保存狀態。 SIGINT 與SIGTERM 幾乎相同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGTSIP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGTSTP 信號由其控制終端發送到進程,以請求終端停止。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGTTIN 和 SIGTTOU"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 SIGTTIN 和SIGTTOU 信號分別在後臺嘗試從 tty 讀取或寫入時,信號將發送到該進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" SIGTRAP"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在發生異常或者 trap 時,將 SIGTRAP 信號發送到進程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGURG"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當套接字具有可讀取的緊急或帶外數據時,將 SIGURG 信號發送到進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGUSR1 和 SIGUSR2"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGUSR1 和 SIGUSR2 信號被髮送到進程以指示用戶定義的條件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGXCPU"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 SIGXCPU 信號耗盡 CPU 的時間超過某個用戶可設置的預定值時,將其發送到進程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGXFSZ"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 SIGXFSZ 信號增長超過最大允許大小的文件時,該信號將發送到該進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGWINCH"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SIGWINCH 信號在其控制終端更改其大小(窗口更改)時發送給進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"管道 pipe"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 系統中的進程可以通過建立管道 pipe 進行通信"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/95/95c00f29b100cdcd810ee6b8d0bb5723.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在兩個進程之間,可以建立一個通道,一個進程向這個通道里寫入字節流,另一個進程從這個管道中讀取字節流。管道是同步的,當進程嘗試從空管道讀取數據時,該進程會被阻塞,直到有可用數據爲止。shell 中的"},{"type":"codeinline","content":[{"type":"text","text":"管線 pipelines"}]},{"type":"text","text":" 就是用管道實現的,當 shell 發現輸出 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"sort "},{"type":"codeinline","content":[{"type":"text","text":"進程位於內存"}]},{"type":"text","text":"被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"PIM(Process In Memory)"}]},{"type":"text","text":" ,這是馮諾伊曼體系架構的一種體現,加載到內存中並執行的程序稱爲進程。簡單來說,一個進程就是正在執行的程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程描述符可以歸爲下面這幾類"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"調度參數(scheduling parameters)"}]},{"type":"text","text":":進程優先級、最近消耗 CPU 的時間、最近睡眠時間一起決定了下一個需要運行的進程"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"內存映像(memory image)"}]},{"type":"text","text":":我們上面說到,進程映像是執行程序時所需要的可執行文件,它由數據和代碼組成。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"信號(signals)"}]},{"type":"text","text":":顯示哪些信號被捕獲、哪些信號被執行"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"寄存器"}]},{"type":"text","text":":當發生內核陷入 (trap) 時,寄存器的內容會被保存下來。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"系統調用狀態(system call state)"}]},{"type":"text","text":":當前系統調用的信息,包括參數和結果"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"文件描述符表(file descriptor table)"}]},{"type":"text","text":":有關文件描述符的系統被調用時,文件描述符作爲索引在文件描述符表中定位相關文件的 i-node 數據結構"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"統計數據(accounting)"}]},{"type":"text","text":":記錄用戶、進程佔用系統 CPU 時間表的指針,一些操作系統還保存進程最多佔用的 CPU 時間、進程擁有的最大堆棧空間、進程可以消耗的頁面數等。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"內核堆棧(kernel stack)"}]},{"type":"text","text":":進程的內核部分可以使用的固定堆棧"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"其他"}]},{"type":"text","text":": 當前進程狀態、事件等待時間、距離警報的超時時間、PID、父進程的 PID 以及用戶標識符等"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了上面這些信息,現在就很容易描述在 Linux 中是如何創建這些進程的了,創建新流程實際上非常簡單。"},{"type":"text","marks":[{"type":"strong"}],"text":"爲子進程開闢一塊新的用戶空間的進程描述符,然後從父進程複製大量的內容。爲這個子進程分配一個 PID,設置其內存映射,賦予它訪問父進程文件的權限,註冊並啓動"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當執行 fork 系統調用時,調用進程會陷入內核並創建一些和任務相關的數據結構,比如"},{"type":"codeinline","content":[{"type":"text","text":"內核堆棧(kernel stack)"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"thread_info"}]},{"type":"text","text":" 結構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">關於 thread_info 結構可以參考 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch"},{"type":"text","marks":[{"type":"italic"}],"text":"2avr32"},{"type":"text","text":"2include"},{"type":"text","marks":[{"type":"italic"}],"text":"2asm"},{"type":"text","text":"2thread_"},{"type":"text","marks":[{"type":"italic"}],"text":"info"},{"type":"text","text":"8h_source.html"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個結構中包含進程描述符,進程描述符位於固定的位置,使得 Linux 系統只需要很小的開銷就可以定位到一個運行中進程的數據結構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程描述符的主要內容是根據"},{"type":"codeinline","content":[{"type":"text","text":"父進程"}]},{"type":"text","text":"的描述符來填充。Linux 操作系統會尋找一個可用的 PID,並且此 PID 沒有被任何進程使用,更新進程標示符使其指向一個新的數據結構即可。爲了減少 hash table 的碰撞,進程描述符會形成"},{"type":"codeinline","content":[{"type":"text","text":"鏈表"}]},{"type":"text","text":"。它還將 task_struct 的字段設置爲指向任務數組上相應的上一個/下一個進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">task_struct : Linux 進程描述符,內部涉及到衆多 C++ 源碼,我們會在後面進行講解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從原則上來說,爲子進程開闢內存區域併爲子進程分配數據段、堆棧段,並且對父進程的內容進行復制,但是實際上 fork 完成後,子進程和父進程沒有共享內存,所以需要複製技術來實現同步,但是複製開銷比較大,因此 Linux 操作系統使用了一種 "},{"type":"codeinline","content":[{"type":"text","text":"欺騙"}]},{"type":"text","text":" 方式。即爲子進程分配頁表,然後新分配的頁表指向父進程的頁面,同時這些頁面是隻讀的。當進程向這些頁面進行寫入的時候,會開啓保護錯誤。內核發現寫入操作後,會爲進程分配一個副本,使得寫入時把數據複製到這個副本上,這個副本是共享的,這種方式稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"寫入時複製(copy on write)"}]},{"type":"text","text":",這種方式避免了在同一塊內存區域維護兩個副本的必要,節省內存空間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在子進程開始運行後,操作系統會調用 exec 系統調用,內核會進行查找驗證可執行文件,把參數和環境變量複製到內核,釋放舊的地址空間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在新的地址空間需要被創建和填充。如果系統支持映射文件,就像 Unix 系統一樣,那麼新的頁表就會創建,表明內存中沒有任何頁,除非所使用的頁面是堆棧頁,其地址空間由磁盤上的可執行文件支持。新進程開始運行時,立刻會收到一個"},{"type":"codeinline","content":[{"type":"text","text":"缺頁異常(page fault)"}]},{"type":"text","text":",這會使具有代碼的頁面加載進入內存。最後,參數和環境變量被複制到新的堆棧中,重置信號,寄存器全部清零。新的命令開始運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是一個示例,用戶輸出 ls,shell 會調用 fork 函數複製一個新進程,shell 進程會調用 exec 函數用可執行文件 ls 的內容覆蓋它的內存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/aa/aafb3f23ded5355a4a5a91c88c85c874.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們來討論一下 Linux 中的線程,線程是輕量級的進程,想必這句話你已經聽過很多次了,"},{"type":"codeinline","content":[{"type":"text","text":"輕量級"}]},{"type":"text","text":"體現在所有的進程切換都需要清除所有的表、進程間的共享信息也比較麻煩,一般來說通過管道或者共享內存,如果是 fork 函數後的父子進程則使用共享文件,然而線程切換不需要像進程一樣具有昂貴的開銷,而且線程通信起來也更方便。線程分爲兩種:用戶級線程和內核級線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"用戶級線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶級線程避免使用內核,通常,每個線程會顯示調用開關,發送信號或者執行某種切換操作來放棄 CPU,同樣,計時器可以強制進行開關,用戶線程的切換速度通常比內核線程快很多。在用戶級別實現線程會有一個問題,即單個線程可能會壟斷 CPU 時間片,導致其他線程無法執行從而 "},{"type":"codeinline","content":[{"type":"text","text":"餓死"}]},{"type":"text","text":"。如果執行一個 I/O 操作,那麼 I/O 會阻塞,其他線程也無法運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c1/c1f0d88726f1343a241423991896b36b.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種解決方案是,一些用戶級的線程包解決了這個問題。可以使用時鐘週期的監視器來控制第一時間時間片獨佔。然後,一些庫通過特殊的包裝來解決系統調用的 I/O 阻塞問題,或者可以爲非阻塞 I/O 編寫任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"內核級線程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內核級線程通常使用幾個進程表在內核中實現,每個任務都會對應一個進程表。在這種情況下,內核會在每個進程的時間片內調度每個線程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e8/e82c968cc32998fb925efe9f4813b97a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有能夠阻塞的調用都會通過系統調用的方式來實現,當一個線程阻塞時,內核可以進行選擇,是運行在同一個進程中的另一個線程(如果有就緒線程的話)還是運行一個另一個進程中的線程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從用戶空間 -> 內核空間 -> 用戶空間的開銷比較大,但是線程初始化的時間損耗可以忽略不計。這種實現的好處是由時鐘決定線程切換時間,因此不太可能將時間片與任務中的其他線程佔用時間綁定到一起。同樣,I/O 阻塞也不是問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"混合實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結合用戶空間和內核空間的優點,設計人員採用了一種"},{"type":"codeinline","content":[{"type":"text","text":"內核級線程"}]},{"type":"text","text":"的方式,然後將用戶級線程與某些或者全部內核線程多路複用起來"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5a/5aeff42dcfdfa3d0eb3a38d3288863cb.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種模型中,編程人員可以自由控制用戶線程和內核線程的數量,具有很大的靈活度。採用這種方法,內核只識別內核級線程,並對其進行調度。其中一些內核級線程會被多個用戶級線程多路複用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們來關注一下 Linux 系統的調度算法,首先需要認識到,Linux 系統的線程是內核線程,所以 Linux 系統是基於線程的,而不是基於進程的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了進行調度,Linux 系統將線程分爲三類"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時先入先出"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時輪詢"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分時"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時先入先出線程具有最高優先級,它不會被其他線程所搶佔,除非那是一個剛剛準備好的,擁有更高優先級的線程進入。實時輪轉線程與實時先入先出線程基本相同,只是每個實時輪轉線程都有一個時間量,時間到了之後就可以被搶佔。如果多個實時線程準備完畢,那麼每個線程運行它時間量所規定的時間,然後插入到實時輪轉線程末尾。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">注意這個實時只是相對的,無法做到絕對的實時,因爲線程的運行時間無法確定。它們相對分時系統來說,更加具有實時性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 系統會給每個線程分配一個 "},{"type":"codeinline","content":[{"type":"text","text":"nice"}]},{"type":"text","text":" 值,這個值代表了優先級的概念。nice 值默認值是 0 ,但是可以通過系統調用 nice 值來修改。修改值的範圍從 -20 - +19。nice 值決定了線程的靜態優先級。一般系統管理員的 nice 值會比一般線程的優先級高,它的範圍是 -20 - -1。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們更詳細的討論一下 Linux 系統的兩個調度算法,它們的內部與"},{"type":"codeinline","content":[{"type":"text","text":"調度隊列(runqueue)"}]},{"type":"text","text":" 的設計很相似。運行隊列有一個數據結構用來監視系統中所有可運行的任務並選擇下一個可以運行的任務。每個運行隊列和系統中的每個 CPU 有關。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Linux O(1)"}]},{"type":"text","text":" 調度器是歷史上很流行的一個調度器。這個名字的由來是因爲它能夠在常數時間內執行任務調度。在 O(1) 調度器裏,調度隊列被組織成兩個數組,一個是任務"},{"type":"text","marks":[{"type":"strong"}],"text":"正在活動"},{"type":"text","text":"的數組,一個是任務"},{"type":"text","marks":[{"type":"strong"}],"text":"過期失效"},{"type":"text","text":"的數組。如下圖所示,每個數組都包含了 140 個鏈表頭,每個鏈表頭具有不同的優先級。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6dc4f90e5f299cb8dcbf852ea3e32729.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致流程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調度器從正在活動數組中選擇一個優先級最高的任務。如果這個任務的時間片過期失效了,就把它移動到過期失效數組中。如果這個任務阻塞了,比如說正在等待 I/O 事件,那麼在它的時間片過期失效之前,一旦 I/O 操作完成,那麼這個任務將會繼續運行,它將被放回到之前正在活動的數組中,因爲這個任務之前已經消耗一部分 CPU 時間片,所以它將運行剩下的時間片。當這個任務運行完它的時間片後,它就會被放到過期失效數組中。一旦正在活動的任務數組中沒有其他任務後,調度器將會交換指針,使得正在活動的數組變爲過期失效數組,過期失效數組變爲正在活動的數組。使用這種方式可以保證每個優先級的任務都能夠得到執行,不會導致線程飢餓。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種調度方式中,不同優先級的任務所得到 CPU 分配的時間片也是不同的,高優先級進程往往能得到較長的時間片,低優先級的任務得到較少的時間片。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方式爲了保證能夠更好的提供服務,通常會爲 "},{"type":"codeinline","content":[{"type":"text","text":"交互式進程"}]},{"type":"text","text":" 賦予較高的優先級,交互式進程就是"},{"type":"codeinline","content":[{"type":"text","text":"用戶進程"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 系統不知道一個任務究竟是 I/O 密集型的還是 CPU 密集型的,它只是依賴於交互式的方式,Linux 系統會區分是"},{"type":"codeinline","content":[{"type":"text","text":"靜態優先級"}]},{"type":"text","text":" 還是 "},{"type":"codeinline","content":[{"type":"text","text":"動態優先級"}]},{"type":"text","text":"。動態優先級是採用一種獎勵機制來實現的。獎勵機制有兩種方式:"},{"type":"text","marks":[{"type":"strong"}],"text":"獎勵交互式線程、懲罰佔用 CPU 的線程"},{"type":"text","text":"。在 Linux O(1) 調度器中,最高的優先級獎勵是 -5,注意這個優先級越低越容易被線程調度器接受,所以最高懲罰的優先級是 +5。具體體現就是操作系統維護一個名爲 "},{"type":"codeinline","content":[{"type":"text","text":"sleep_avg"}]},{"type":"text","text":" 的變量,任務喚醒會增加 sleep_avg 變量的值,當任務被搶佔或者時間量過期會減少這個變量的值,反映在獎勵機制上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"O(1) 調度算法是 2.6 內核版本的調度器,最初引入這個調度算法的是不穩定的 2.5 版本。早期的調度算法在多處理器環境中說明了通過訪問正在活動數組就可以做出調度的決定。使調度可以在固定的時間 O(1) 完成。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"O(1) 調度器使用了一種 "},{"type":"codeinline","content":[{"type":"text","text":"啓發式"}]},{"type":"text","text":" 的方式,這是什麼意思?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">在計算機科學中,啓發式是一種當傳統方式解決問題很慢時用來快速解決問題的方式,或者找到一個在傳統方法無法找到任何精確解的情況下找到近似解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"O(1) 使用啓發式的這種方式,會使任務的優先級變得複雜並且不完善,從而導致在處理交互任務時性能很糟糕。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了改進這個缺點,O(1) 調度器的開發者又提出了一個新的方案,即 "},{"type":"codeinline","content":[{"type":"text","text":"公平調度器(Completely Fair Scheduler, CFS)"}]},{"type":"text","text":"。 CFS 的主要思想是使用一顆"},{"type":"codeinline","content":[{"type":"text","text":"紅黑樹"}]},{"type":"text","text":"作爲調度隊列。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">數據結構太重要了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CFS 會根據任務在 CPU 上的運行時間長短而將其有序地排列在樹中,時間精確到納秒級。下面是 CFS 的構造模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/20/2088e1107f6f8d8382b067c0692006ec.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CFS 的調度過程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CFS 算法總是優先調度哪些使用 CPU 時間最少的任務。最小的任務一般都是在最左邊的位置。當有一個新的任務需要運行時,CFS 會把這個任務和最左邊的數值進行對比,如果此任務具有最小時間值,那麼它將進行運行,否則它會進行比較,找到合適的位置進行插入。然後 CPU 運行紅黑樹上當前比較的最左邊的任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在紅黑樹中選擇一個節點來運行的時間可以是常數時間,但是插入一個任務的時間是 "},{"type":"codeinline","content":[{"type":"text","text":"O(loog(N))"}]},{"type":"text","text":",其中 N 是系統中的任務數。考慮到當前系統的負載水平,這是可以接受的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調度器只需要考慮可運行的任務即可。這些任務被放在適當的調度隊列中。不可運行的任務和正在等待的各種 I/O 操作或內核事件的任務被放入一個"},{"type":"codeinline","content":[{"type":"text","text":"等待隊列"}]},{"type":"text","text":"中。等待隊列頭包含一個指向任務鏈表的指針和一個自旋鎖。自旋鎖對於併發處理場景下用處很大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Linux 系統中的同步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面來聊一下 Linux 中的同步機制。早期的 Linux 內核只有一個 "},{"type":"codeinline","content":[{"type":"text","text":"大內核鎖(Big Kernel Lock,BKL)"}]},{"type":"text","text":" 。它阻止了不同處理器併發處理的能力。因此,需要引入一些粒度更細的鎖機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 提供了若干不同類型的同步變量,這些變量既能夠在內核中使用,也能夠在用戶應用程序中使用。在地層中,Linux 通過使用 "},{"type":"codeinline","content":[{"type":"text","text":"atomic_set"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"atomic_read"}]},{"type":"text","text":" 這樣的操作爲硬件支持的原子指令提供封裝。硬件提供內存重排序,這是 Linux 屏障的機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具有高級別的同步像是自旋鎖的描述是這樣的,當兩個進程同時對資源進行訪問,在一個進程獲得資源後,另一個進程不想被阻塞,所以它就會自旋,等待一會兒再對資源進行訪問。Linux 也提供互斥量或信號量這樣的機制,也支持像是 "},{"type":"codeinline","content":[{"type":"text","text":"mutex_tryLock"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"mutex_tryWait"}]},{"type":"text","text":" 這樣的非阻塞調用。也支持中斷處理事務,也可以通過動態禁用和啓用相應的中斷來實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 啓動"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面來聊一聊 Linux 是如何啓動的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當計算機電源通電後,"},{"type":"codeinline","content":[{"type":"text","text":"BIOS"}]},{"type":"text","text":"會進行"},{"type":"codeinline","content":[{"type":"text","text":"開機自檢(Power-On-Self-Test, POST)"}]},{"type":"text","text":",對硬件進行檢測和初始化。因爲操作系統的啓動會使用到磁盤、屏幕、鍵盤、鼠標等設備。下一步,磁盤中的第一個分區,也被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"MBR(Master Boot Record)"}]},{"type":"text","text":" 主引導記錄,被讀入到一個固定的內存區域並執行。這個分區中有一個非常小的,只有 512 字節的程序。程序從磁盤中調入 boot 獨立程序,boot 程序將自身複製到高位地址的內存從而爲操作系統釋放低位地址的內存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複製完成後,boot 程序讀取啓動設備的根目錄。boot 程序要理解文件系統和目錄格式。然後 boot 程序被調入內核,把控制權移交給內核。直到這裏,boot 完成了它的工作。系統內核開始運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內核啓動代碼是使用"},{"type":"codeinline","content":[{"type":"text","text":"彙編語言"}]},{"type":"text","text":"完成的,主要包括創建內核堆棧、識別 CPU 類型、計算內存、禁用中斷、啓動內存管理單元等,然後調用 C 語言的 main 函數執行操作系統部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這部分也會做很多事情,首先會分配一個消息緩衝區來存放調試出現的問題,調試信息會寫入緩衝區。如果調試出現錯誤,這些信息可以通過診斷程序調出來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後操作系統會進行自動配置,檢測設備,加載配置文件,被檢測設備如果做出響應,就會被添加到已鏈接的設備表中,如果沒有相應,就歸爲未連接直接忽略。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"配置完所有硬件後,接下來要做的就是仔細手工處理進程0,設置其堆棧,然後運行它,執行初始化、配置時鐘、掛載文件系統。創建 "},{"type":"codeinline","content":[{"type":"text","text":"init 進程(進程 1 )"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"守護進程(進程 2)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"init 進程會檢測它的標誌以確定它是否爲單用戶還是多用戶服務。在前一種情況中,它會調用 fork 函數創建一個 shell 進程,並且等待這個進程結束。後一種情況調用 fork 函數創建一個運行系統初始化的 shell 腳本(即 /etc/rc)的進程,這個進程可以進行文件系統一致性檢測、掛載文件系統、開啓守護進程等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後 /etc/rc 這個進程會從 /etc/ttys 中讀取數據,/etc/ttys 列出了所有的終端和屬性。對於每一個啓用的終端,這個進程調用 fork 函數創建一個自身的副本,進行內部處理並運行一個名爲 "},{"type":"codeinline","content":[{"type":"text","text":"getty"}]},{"type":"text","text":" 的程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getty 程序會在終端上輸入 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"login:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"等待用戶輸入用戶名,在輸入用戶名後,getty 程序結束,登陸程序 "},{"type":"codeinline","content":[{"type":"text","text":"/bin/login"}]},{"type":"text","text":" 開始運行。login 程序需要輸入密碼,並與保存在 "},{"type":"codeinline","content":[{"type":"text","text":"/etc/passwd"}]},{"type":"text","text":" 中的密碼進行對比,如果輸入正確,login 程序以用戶 shell 程序替換自身,等待第一個命令。如果不正確,login 程序要求輸入另一個用戶名。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整個系統啓動過程如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/df/df9efb39c4f1c9e6ad46db87da8d6f30.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cd6da7c74de719095421f0294e6c1450.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章