進程:「若要問我從哪裡來,這還要從作業系統的發展說起。」(進程的回憶開始了)那是在20世紀50年代中期,出現了第一代計算機,那時候還需要程式設計師親自手工操作穿孔的紙帶卡片,雖然CPU的速度在迅速提高,但是I/O設備的速度卻提高緩慢,來看看CPU和人工處理的I/O設備的日常對話。
相傳,為了解決人機矛盾及CPU和I/O設備之間速度不匹配的矛盾,脫機I/O技術誕生了,看看那時候CPU在聊些什麼。
為了提高系統資源利用率和吞吐量,單道批處理系統誕生了,這下好了,任務一個接一個來,CPU總算是能表現一下自己了,來看看這時候CPU和OS監督程序的對話。
雖然單道批處理系統提高了系統資源的利用率和系統的吞吐量,但是仍然不能充分地利用系統資源,因為在內存中僅有一道程序,程序在運行中發出I/O請求後,CPU就在那一直等,當時的情況是這樣的。
雖然如此,但是問題依舊不大,因為在20世紀60年代中期,引入了多道程序的設計技術,形成了多道批處理系統,進一步提高了資源的利用率和系統吞吐量,我也是在這個時期開始形成的,當時可把CPU樂壞了。
後來的後來,為了方便用戶,CPU繼續發展,各種作業系統也是層出不窮,例如常見的Windows 、Linux、Contiki 等,然而我的作用依舊不可替代。
講講我的故事
你以為進程我三頭六臂,真的那麼大本事,可以讓一個CPU核心同時運行多個程序?別你以為了,只是一種錯覺,只是作業系統的並發性罷了,實際上一個CPU核心在同一時刻只能運行一個程序,只不過是我快速切換,讓CPU這個高速的傢伙閒不下來,知道真相的你,是不是覺得理解錯了呢?事實上現在的CPU已經可以同一時刻運行多個程序啦,因為現在的CPU有多個核心吶。
我們也不容易呀,雖然讓CPU這傢伙完全跑了起來,有效地改善了資源利用率,提高了系統吞吐量,但是我們把作業系統弄的更複雜了,如果我們不受約束的話,就會隨意爭奪系統資源,給作業系統老大帶來麻煩,程序也會顯現出不可再現性。老大當然不允許了,於是和我約法三章,定了一些硬體同步機制、信號量機制、管程機制等一系列同步機制,好讓老大更加穩定可靠。
當然我們進程之間也經常聯繫呢,早期的通信被人們叫做低級進程通信,當然這時候我們進程的通信人們才不知道呢。後來,OS老大更強大了,制定了高級通信機制,他給人們提供了高級通信工具,我們進程之間的通信能傳送大量的數據了,對人們也透明了,這個高級通信機制,被人們歸了四類,分別是:共享存儲系統、管道通信系統、消息傳遞系統和客戶機-伺服器系統。
聽了我們進程的故事,是不是對我有所了解呢,其實我們家族的故事多著呢,有興趣可以自己去了解一番,這裡有一張金榮製作的思維導圖,有助於更好地了解我們,收好。
看看我的模樣
也許你對我已經有所了解,那麼現在就請你在Linux OS中看看我的真面目吧,要問我到底是誰,我只是一些程序和數據罷了,也可以認為我就是運行中的程序。Linux OS老大為了管住我,將我的狀態信息、連結信息、各種標識符、進程間通信信息、時間和定時器信息、調度信息、文件系統信息、虛擬內存信息、處理器環境信息等封印在一個巨大的結構體中,江湖上稱之為進程控制塊(Process Contrlo Block),簡稱PCB,在內核源碼中的定義是醬紫的,且看Linux內核3.18.43版本的源碼,具體在/include/linux下的頭文件sched.h中的第1237行代碼。
了解我的狀態,這點很重要,在一般的作業系統中,我的狀態有就緒態、運行態和等待態三種狀態,而在Linux OS中,為了管理上的方便,我的狀態被改造成了就緒態、睡眠態、暫停態和僵死態,狀態轉換是醬紫的。
當然了,我們進程也並非長生不老,陳莉君老師就對我們極為了解,不信請看看陳老師語錄:
隨著一句fork,一個新進程呱呱落地,但這時它只是老進程的一個克隆。然後,隨著exec,新進程脫胎換骨,離家獨立,開始了獨立工作的職業生涯。
人有生老病死,進程也一樣,它可以是自然死亡,即運行到主(main)函數的最後一個"}",從容地離我們而去;也可以是中途退場,退場有2種方式,一種是調用exit函數,一種是在主(main)函數內使用return,無論哪一種方式,它都可以留下留言,放在返回值里保留下來;甚至它還可能被謀殺,被其它進程通過另外一些方式結束它的生命。
進程死掉以後,會留下一個空殼,wait站好最後一班崗,打掃戰場,使其最終歸於無形。這就是進程完整的一生。
和我交朋友吧
想和我們交朋友嗎,那就了解一下我的組織方式吧。為了對我們進行有效的搜索,內核建立了幾個進程鍊表,每個進程鍊表由指向進程PCB的指針組成。當內核要根據我們的PID導出對應PCB的時候,遍歷進程鍊表檢查PCB中的PID太慢了,於是引入了哈希表,Linux OS中用了一個叫做pid_hashfn的宏,來實現把PID轉換成表的索引的哈希函數。
在這裡獻上一個內核模塊,功能為遍歷進程鍊表,列印pcb相關欄位的內核模塊,並配上詳細注釋:
模塊代碼
# include
# include
# include
# include
# include
# include
# include
# include
//內核模塊初始化函數
static int __init traverse_pcb(void)
{
\tstruct task_struct *task, *p;//定義指向task_struct類型的指針
\tstruct list_head *pos;//定義雙向鍊表指針
\tint count=0;//定義統計系統進程個數的變量
\tprintk("Printf process'message begin:\\n");//提示模塊開始運行
\ttask = &init_task;//指向0號進程的PCB
\t
\tlist_for_each(pos,&task->tasks)//使用list_for_each宏來遍歷進程鍊表
\t{
\t\tp = list_entry(pos,struct task_struct,tasks);//指向當前進程的task_struct結構
\t\tcount++;//統計系統進程個數
\t\tprintk("\\n\\n");//方便查看後續列印信息
\t\t/*
\t\t列印task_struct中的欄位,其中pid:進程的pid號;state:進程的狀態;
\t\tprio:動態優先級;static_prio:靜態優先級; parent'pid:父進程的pid號;
\t\tcount:文件系統信息,文件被使用的次數; umask:進程權限位的默認設置;
\t\t使用atomic_read原子操作是為了(p->files)->count欄位計數不被打斷
\t\t*/
\t\tprintk("pid:%d; state:%lx; prio:%d; static_prio:%d; parent'pid:%d; count:%d; umask:%d;",\t\\
\t\t\tp->pid,p->state,p->prio,p->static_prio,(p->parent)->pid,\t\t\t\t\t\t\t\t\\
\t\t\tatomic_read((&(p->files)->count)),(p->fs)->umask);
\t\t//列印進程地址空間的信息
\t\tif((p->mm)!=NULL)
\t\t\tprintk("total_vm:%ld;",(p->mm)->total_vm);//total_vm:線性區總的頁數
\t}
\tprintk("進程的個數:%d\\n",count);//列印進程個數
\treturn 0;
}
//內核模塊退出函數
static void __exit end_pcb(void)
{
\tprintk("traverse pcb is end.");
}
module_init(traverse_pcb);//入口
module_exit(end_pcb);//出口
MODULE_LICENSE("GPL");//許可證
Makefile
#產生目標文件
obj-m:=traverse_pcb.o
#路徑變量,指明當前路徑
CURRENT_PATH:=$(shell pwd)
#指明內核版本號
LINUX_KERNEL:=$(shell uname -r)
#指明內核源碼的絕對路徑
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#編譯模塊
all:
\tmake -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#清理模塊
clean:
\tmake -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
編譯加載
查看結果
上圖中,我們可以看到每個進程的pid號,進程的狀態,動態優先級,靜態優先級,父進程的pid號,文件系統信息(文件被使用的次數),進程權限位的默認設置,總進程數等。有些進程的total_vm是空的,說明他們是內核線程。