揭秘操作系統(tǒng)最核心技術(shù):進(jìn)程與線程是如何一步步發(fā)明出來(lái)的?
今天我想跟你聊聊一個(gè)計(jì)算機(jī)發(fā)展史上的精彩故事——進(jìn)程與線程是如何一步步被發(fā)明出來(lái)的?
讓我?guī)愦┰綍r(shí)光隧道,看看這些關(guān)鍵概念是如何從無(wú)到有,最終成為現(xiàn)代操作系統(tǒng)基石的。
一、"一人獨(dú)占"的早期計(jì)算機(jī)時(shí)代
想象一下 20 世紀(jì) 50 年代的計(jì)算機(jī)實(shí)驗(yàn)室:一臺(tái)體積龐大、價(jià)格昂貴的 UNIVAC 或 IBM 704 計(jì)算機(jī)占據(jù)了整個(gè)房間。這些早期計(jì)算機(jī)每次只能執(zhí)行一個(gè)程序,用戶必須排隊(duì)等候。
更令人沮喪的是,當(dāng)程序在等待磁帶或打印機(jī)等慢速設(shè)備時(shí),這些價(jià)值不菲的計(jì)算機(jī)就會(huì)閑置著——就像一輛豪華跑車被迫停在路邊等待紅燈一樣浪費(fèi)!
這種運(yùn)行方式被稱為"批處理系統(tǒng)"。用戶把程序和數(shù)據(jù)打在卡片上交給操作員,然后等待——可能是幾個(gè)小時(shí),甚至是整整一天!如果前面的程序出了 bug 導(dǎo)致死循環(huán),后面所有人都只能干等。
有一天,麻省理工學(xué)院的計(jì)算機(jī)科學(xué)家 Fernando Corbató 盯著實(shí)驗(yàn)室里的計(jì)算機(jī)發(fā)呆,突然靈光一閃:
"為什么要讓昂貴的計(jì)算機(jī)資源在等待慢速 I/O 操作時(shí)空閑呢?我們應(yīng)該讓它能同時(shí)處理多個(gè)任務(wù)!"
二、分時(shí)系統(tǒng):打破"獨(dú)占"的第一步
于是,Corbató 和他的團(tuán)隊(duì)開始研發(fā)全新的系統(tǒng)——CTSS(Compatible Time-Sharing System,兼容分時(shí)系統(tǒng)),這被認(rèn)為是現(xiàn)代多任務(wù)操作系統(tǒng)的雛形。
CTSS 的核心理念很簡(jiǎn)單:在內(nèi)存中同時(shí)加載多個(gè)程序,當(dāng)一個(gè)程序等待 I/O 操作時(shí),CPU 可以切換去執(zhí)行另一個(gè)程序。這就是"多道程序設(shè)計(jì)"的開端。
但問(wèn)題來(lái)了:如何讓一個(gè)正在運(yùn)行的程序暫停,然后在未來(lái)某個(gè)時(shí)刻恢復(fù)執(zhí)行呢?
答案是:必須保存程序的"上下文"信息!就像你在讀一本書時(shí),如果要暫時(shí)去做別的事,你需要記住當(dāng)前讀到第幾頁(yè)第幾行,這樣回來(lái)才能繼續(xù)讀。
于是,調(diào)查研究后,Corbató 和他的團(tuán)隊(duì)設(shè)計(jì)出了一個(gè)關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)來(lái)保存程序的"狀態(tài)快照":
struct cpu_state {
uint32_t r0, r1, r2, r3; // 通用寄存器
uint32_t pc; // 程序計(jì)數(shù)器
uint32_t sp; // 棧指針
uint32_t status; // 狀態(tài)寄存器
// 其他必要的寄存器狀態(tài)...
};
有了這個(gè)結(jié)構(gòu),當(dāng)系統(tǒng)需要切換任務(wù)時(shí),就可以保存當(dāng)前程序的所有寄存器狀態(tài),然后加載另一個(gè)程序的狀態(tài),讓 CPU 繼續(xù)執(zhí)行新的程序。
三、內(nèi)存保護(hù):解決程序"打架"問(wèn)題
但實(shí)施分時(shí)系統(tǒng)后,一個(gè)新問(wèn)題很快浮出水面。在貝爾實(shí)驗(yàn)室工作的 Dennis Ritchie(C 語(yǔ)言和 UNIX 的創(chuàng)始人之一)回憶道,他們的早期系統(tǒng)經(jīng)常崩潰,而且數(shù)據(jù)會(huì)莫名其妙地被修改。
調(diào)查后,他們發(fā)現(xiàn)了根本原因:
不同的程序在同一內(nèi)存空間中互相干擾!
原因很簡(jiǎn)單:在早期系統(tǒng)中,所有程序共享同一塊內(nèi)存空間,沒(méi)有任何隔離機(jī)制。就像幾個(gè)小孩在同一張紙上畫畫,互相涂抹對(duì)方的作品一樣混亂。
看看這個(gè)災(zāi)難性的例子:
// 計(jì)費(fèi)程序 : interest.c
struct account {
char name[50];
double balance;
} customer = {"Zhang San", 1000.0};
void update_balance() {
while(1) {
customer.balance *= 1.05; // 計(jì)算利息
sleep(1);
}
}
// 同時(shí)運(yùn)行的取款程序 : withdraw.c
struct account {
char name[50];
double balance;
} customer = {"Zhang San", 1000.0};
void withdraw() {
while(1) {
customer.balance -= 100; // 定期取款
sleep(1);
}
}
在早期系統(tǒng)中,由于缺乏地址空間隔離機(jī)制,這兩個(gè)獨(dú)立編譯的程序使用了相同的內(nèi)存地址范圍,導(dǎo)致它們的 customer 變量實(shí)際上重疊在物理內(nèi)存的同一區(qū)域,結(jié)果就是一個(gè)程序計(jì)算利息增加余額,另一個(gè)程序同時(shí)在取款減少余額,導(dǎo)致完全不可預(yù)測(cè)的結(jié)果。
系統(tǒng)科學(xué)家們意識(shí)到,必須從根本上解決這個(gè)問(wèn)題——需要在操作系統(tǒng)層面提供內(nèi)存隔離機(jī)制!
四、進(jìn)程的誕生:給每個(gè)程序一個(gè)"隔離艙"
1964年,MIT、貝爾實(shí)驗(yàn)室和通用電氣公司聯(lián)合開發(fā)了 MULTICS 系統(tǒng)。在這個(gè)過(guò)程中,一個(gè)革命性的概念誕生了——"進(jìn)程"。
什么是進(jìn)程?簡(jiǎn)單說(shuō),進(jìn)程就是一個(gè)運(yùn)行中的程序?qū)嵗?,擁有?dú)立的內(nèi)存空間和系統(tǒng)資源。
MULTICS 的設(shè)計(jì)者創(chuàng)建了一個(gè)內(nèi)存映射系統(tǒng),確保每個(gè)進(jìn)程都有自己的獨(dú)立內(nèi)存區(qū)域:
struct mem_layout {
void* text_begin; // 代碼區(qū)起始地址
size_t text_size; // 代碼區(qū)大小
void* heap_begin; // 堆區(qū)起始地址
size_t heap_size; // 堆區(qū)大小
// 其他內(nèi)存區(qū)域...
};
將這個(gè)結(jié)構(gòu)與前面的狀態(tài)快照結(jié)構(gòu)結(jié)合起來(lái),就形成了完整的進(jìn)程定義:
struct task_control {
struct cpu_state registers; // CPU狀態(tài)
struct mem_layout memory; // 內(nèi)存布局
pid_t id; // 任務(wù)標(biāo)識(shí)符
uint8_t state; // 任務(wù)狀態(tài)
resource_list_t resources; // 資源列表
// ... 其他控制信息
};
這就是操作系統(tǒng)中"進(jìn)程控制塊"(PCB)的雛形!有了它,操作系統(tǒng)就能管理多個(gè)進(jìn)程的執(zhí)行,實(shí)現(xiàn)真正的多任務(wù)處理。
但這又帶來(lái)了新問(wèn)題:如何公平地分配 CPU 時(shí)間給多個(gè)進(jìn)程?
五、時(shí)間片輪轉(zhuǎn)調(diào)度:讓每個(gè)人都有發(fā)言權(quán)
在多進(jìn)程系統(tǒng)中,如果讓一個(gè)進(jìn)程一直運(yùn)行到結(jié)束,其他進(jìn)程就得一直等待,這顯然不合理。特別是對(duì)于交互式程序,用戶會(huì)感覺(jué)系統(tǒng)反應(yīng)遲鈍。
為解決這個(gè)問(wèn)題,MIT 的研究人員設(shè)計(jì)了一種全新的調(diào)度算法——"時(shí)間片輪轉(zhuǎn)調(diào)度"(Round-Robin Scheduling)。
這個(gè)概念非常簡(jiǎn)單:給每個(gè)進(jìn)程分配一個(gè)固定長(zhǎng)度的 CPU 使用時(shí)間(稱為"時(shí)間片",通常為幾十毫秒),當(dāng)一個(gè)進(jìn)程用完自己的時(shí)間片,操作系統(tǒng)就強(qiáng)制將 CPU 分配給下一個(gè)等待的進(jìn)程。
// 時(shí)間片輪轉(zhuǎn)調(diào)度的偽代碼
void scheduler() {
while(true) {
process = get_next_process_from_queue();
set_timer(TIME_SLICE); // 設(shè)置時(shí)鐘中斷
context_switch(process); // 切換到該進(jìn)程
// 時(shí)間片用完后,時(shí)鐘中斷處理程序會(huì)調(diào)用scheduler()
}
}
這就像是一場(chǎng)公平的會(huì)議,每個(gè)人都有固定的發(fā)言時(shí)間。即使有人滔滔不絕,到時(shí)間也必須讓下一位發(fā)言。
時(shí)間片的長(zhǎng)度設(shè)置很有講究:
- 太短:進(jìn)程切換開銷占比太大,系統(tǒng)效率降低
- 太長(zhǎng):響應(yīng)時(shí)間變長(zhǎng),用戶體驗(yàn)差
典型的時(shí)間片長(zhǎng)度是 10-100 毫秒,這對(duì)人類感知來(lái)說(shuō)足夠短,讓用戶感覺(jué)多個(gè)程序在"同時(shí)"運(yùn)行,這就是我們熟悉的"并發(fā)"。
進(jìn)程的誕生和時(shí)間片輪轉(zhuǎn)調(diào)度帶來(lái)了巨大的好處:
- 不同程序之間徹底隔離,不會(huì)相互干擾
- 系統(tǒng)可以同時(shí)運(yùn)行多個(gè)程序,大大提高了效率
- 即使一個(gè)程序崩潰,也不會(huì)影響其他程序的運(yùn)行
- 所有進(jìn)程都能獲得公平的 CPU 使用時(shí)間
就像給每個(gè)畫畫的小孩分發(fā)獨(dú)立的畫紙,再也不用擔(dān)心誰(shuí)會(huì)涂抹誰(shuí)的作品了!同時(shí),每個(gè)小孩都能輪流使用那支珍貴的金色畫筆(CPU)。
六、進(jìn)程間通信與切換瓶頸
隨著進(jìn)程模型的普及,新的需求出現(xiàn)了——進(jìn)程之間需要交換數(shù)據(jù)和協(xié)調(diào)行動(dòng)。UNIX的創(chuàng)始人們發(fā)明了各種"進(jìn)程間通信"(IPC)機(jī)制,如 管道、消息隊(duì)列和共享內(nèi)存等。
但隨著系統(tǒng)中運(yùn)行的進(jìn)程越來(lái)越多,一個(gè)嚴(yán)重的問(wèn)題浮出水面——進(jìn)程切換的開銷實(shí)在太大了!每次切換進(jìn)程,系統(tǒng)都需要:
- 保存當(dāng)前進(jìn)程的struct cpu_state(所有寄存器狀態(tài))
- 更新struct mem_layout(切換完整的內(nèi)存映射)—— 最耗時(shí)
- 刷新 TLB 緩存和其他處理器緩存
- 恢復(fù)新進(jìn)程的struct cpu_state
這個(gè)過(guò)程就像一名攝影師在拍攝多個(gè)場(chǎng)景之間切換:不僅要記住每個(gè)場(chǎng)景的拍攝角度和相機(jī)設(shè)置(CPU 狀態(tài)),還要搬運(yùn)和重新布置整套燈光裝備、背景和道具(內(nèi)存映射)。即使攝影師技術(shù)再好,這種場(chǎng)景切換的時(shí)間成本也是無(wú)法避免的!
在加州大學(xué)伯克利分校,研究人員還觀察到一個(gè)有趣的現(xiàn)象:服務(wù)器應(yīng)用程序創(chuàng)建了大量進(jìn)程來(lái)處理不同的請(qǐng)求,這些進(jìn)程運(yùn)行完全相同的代碼,卻各自占用獨(dú)立的內(nèi)存空間。這簡(jiǎn)直是在浪費(fèi)資源!
面對(duì)這個(gè)問(wèn)題,他們提出了一個(gè)革命性的疑問(wèn):"如果多個(gè)任務(wù)需要執(zhí)行相同的程序代碼,為何不創(chuàng)造一種新機(jī)制,讓它們共享代碼和數(shù)據(jù),只在需要時(shí)保持獨(dú)立?"
七、線程的誕生:輕量級(jí)的執(zhí)行單元
1979年,Xerox PARC 的研究人員 David Boggs 和 Butler Lampson 在開發(fā) Alto 操作系統(tǒng)時(shí),提出了一個(gè)革命性的想法——"線程"。
線程被設(shè)計(jì)為進(jìn)程內(nèi)的"輕量級(jí)執(zhí)行單元",它們:
- 共享所屬進(jìn)程的代碼段、數(shù)據(jù)段和系統(tǒng)資源
- 擁有自己的執(zhí)行棧和寄存器狀態(tài)
- 可以獨(dú)立調(diào)度執(zhí)行
struct execution_flow {
uint32_t flow_id; // 流ID
uint8_t status; // 運(yùn)行狀態(tài)
struct cpu_state regs; // 寄存器狀態(tài)
void *stack; // ??臻g
struct task_control *owner; // 所屬任務(wù)
};
線程相比進(jìn)程有哪些優(yōu)勢(shì)?太多了!
- 創(chuàng)建和銷毀更快:不需要分配新的地址空間,只需要一個(gè)新的棧
- 切換開銷?。壕€程間切換不需要切換地址空間,速度提升好多倍!
- 資源共享自然:同一進(jìn)程的線程共享內(nèi)存,可以直接讀寫共享變量
- 通信簡(jiǎn)單高效:線程間通信不需要特殊的IPC機(jī)制
就像網(wǎng)絡(luò)游戲中的"組隊(duì)"功能——同一隊(duì)伍的玩家可以共享地圖信息,直接交流,而不同隊(duì)伍之間則需要特殊渠道才能通信。
八、實(shí)戰(zhàn)對(duì)比:進(jìn)程vs線程
理論講完了,來(lái)看個(gè)簡(jiǎn)單例子,直觀感受進(jìn)程與線程的區(qū)別:
多進(jìn)程版web服務(wù)器:
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
def run_server(port):
server = HTTPServer(('', port), BaseHTTPRequestHandler)
server.serve_forever()
# 創(chuàng)建5個(gè)進(jìn)程,每個(gè)監(jiān)聽不同端口
for i in range(5):
pid = os.fork() # 創(chuàng)建新進(jìn)程
if pid == 0: # 子進(jìn)程
run_server(8000 + i)
break# 子進(jìn)程不再繼續(xù)循環(huán)
# 父進(jìn)程需要等待子進(jìn)程(簡(jiǎn)化處理)
# ...
多線程版web服務(wù)器:
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
def run_server(port):
server = HTTPServer(('', port), BaseHTTPRequestHandler)
server.serve_forever()
# 創(chuàng)建5個(gè)線程,每個(gè)監(jiān)聽不同端口
threads = []
for i in range(5):
t = threading.Thread(target=run_server, args=(8000 + i,))
threads.append(t)
t.start()
# 等待所有線程結(jié)束
for t in threads:
t.join()
# ...
代碼看起來(lái)很相似,但背后的區(qū)別巨大:
- 多進(jìn)程版本中,每個(gè)服務(wù)器進(jìn)程有完全獨(dú)立的內(nèi)存空間,資源消耗更大
- 多線程版本中,所有線程共享同一個(gè)進(jìn)程的內(nèi)存空間,資源利用更高效
- 多進(jìn)程版更適合需要隔離的任務(wù),多線程版更適合共享數(shù)據(jù)的任務(wù)
九、線程安全與未來(lái)趨勢(shì)
線程雖然解決了很多問(wèn)題,但也帶來(lái)了新的挑戰(zhàn),最大的就是"線程安全"問(wèn)題。由于多個(gè)線程共享同一地址空間,并發(fā)訪問(wèn)共享數(shù)據(jù)會(huì)導(dǎo)致"競(jìng)態(tài)條件"。
看一個(gè)經(jīng)典的計(jì)數(shù)器問(wèn)題:
// 全局共享變量
int counter = 0;
// 線程函數(shù)
void* increment_counter(void* arg) {
for (int i = 0; i < 100000; i++) {
counter++; // 實(shí)際上是: 讀取counter, +1, 寫回counter
}
return NULL;
}
當(dāng)兩個(gè)線程同時(shí)執(zhí)行時(shí),最終 counter 值可能小于期望的 200000,因?yàn)榫€程可能同時(shí)讀取相同的值,各自加 1 后寫回,導(dǎo)致一個(gè)增量丟失。
為了解決這個(gè)問(wèn)題,操作系統(tǒng)引入了各種同步原語(yǔ),如互斥鎖、條件變量和信號(hào)量等。
技術(shù)永遠(yuǎn)在進(jìn)化。如今,新一代的并發(fā)模型已經(jīng)出現(xiàn):協(xié)程(Coroutine)比線程更輕量,由程序自己控制切換,不需要操作系統(tǒng)介入。
歷史回顧與結(jié)語(yǔ)
回顧這段歷史,我們看到了計(jì)算機(jī)從"一臺(tái)機(jī)器只能做一件事"到"同時(shí)處理成千上萬(wàn)任務(wù)"的驚人進(jìn)化:
- 批處理系統(tǒng)(1950s):一次只運(yùn)行一個(gè)程序,如早期的 IBM 704
- 分時(shí)系統(tǒng)(1961):MIT 的 CTSS 首次實(shí)現(xiàn)了多用戶時(shí)間共享
- 進(jìn)程(1964):MULTICS 系統(tǒng)引入進(jìn)程概念,為程序提供獨(dú)立執(zhí)行環(huán)境
- 線程(1979):Xerox Alto 操作系統(tǒng)引入輕量級(jí)執(zhí)行單位,共享進(jìn)程資源
- 協(xié)程(2010s):用戶態(tài)的輕量級(jí)線程,進(jìn)一步降低并發(fā)開銷
這就像交通工具從"一次只載一個(gè)人的獨(dú)木舟",逐步發(fā)展為如今的"高速磁懸浮列車"一樣神奇。
每當(dāng)你打開電腦,運(yùn)行十幾個(gè)應(yīng)用程序,同時(shí)瀏覽網(wǎng)頁(yè)、聽音樂(lè)、下載文件,這一切看似理所當(dāng)然的便利,都是幾代計(jì)算機(jī)科學(xué)家智慧的結(jié)晶。
進(jìn)程和線程,這兩個(gè)看似抽象的概念,讓我們的數(shù)字生活變得如此豐富多彩。下次當(dāng)你的電腦同時(shí)運(yùn)行多個(gè)應(yīng)用時(shí),別忘了感謝那些 60 年代的工程師們——正是他們的創(chuàng)新思維,讓今天的多任務(wù)處理成為可能!