歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> 更多Linux >> Linux 2.2.X進程管理分析及最大進程數限制的突破

Linux 2.2.X進程管理分析及最大進程數限制的突破

日期:2017/2/27 14:11:13   编辑:更多Linux
進程管理是操作系統最關鍵的部分,它的設計和實現直接影響到整個系統的性能。在一個多進程的操作系統中,一個時間段內可以有多個進程“並發”執行。這樣就避免了較為快速的cpu等待較為低速的I/O設備的情況,提高了cpu利用率,從而提高系統的性能。另一個方面,同時運行多個進程,就可以同時提供多種服務或者同時為更多的客戶服務,這也是現代操作系統的基本任務。

在基於Intel i386體系結構的Linux操作系統中,已經提供了這樣的多進程運行的支持。通過合理的選擇進程調度算法,可以獲得比較好的平均相應時間和較高的系統性能。但是,美中不足的是,在目前的2.2.x版本的Linux內核中,存在對最大進程數的限制。也就是說,在目前的Linux系統中,最多只能有4090個用戶進程同時存在。對於一般的桌面應用,這個數目是綽綽有余。但是,對於企業級的服務器應用來說,則是不夠的。

設想一個典型的Web服務器軟件,它們一般都采用多進程/線程的結構。每當接到一個連接請求,就產生一個子進程/線程來處理。顯然,對於一個重負載的服務器來說,同時有成千上萬個連接是很常見的。而這時,采用kernel 2.2.x的系統就不能勝任了。因此,可以說正是因為存在這個最大進程數的限制,使得Linux不能勝任企業級服務器操作系統的工作。事實上,目前這個級別的操作系統一般都是較為成熟,並且沒有上述限制的Solaris、AIX、HP-UX等系統。

值得一提的是,眾多的Linux開發者已經認識到了這個問題。kernel 2.2.x的後續版本,例如試驗版本2.3.x和2.4都已經著手解決這個問題。然而,現在離2.4的發布還有很長的時間,同時2.4在使用中達到穩定,又需要一段時間。在這一段時間中,我們是不是只有等待或者選用其他系統呢?或者說,能否有一種方案,可以在2.2.x的內核上突破這個進程數的限制呢?

要回答這個問題,就必須先對2.2.x核心的進程管理加以分析。

一、i386體系結構與kernel V2.2.X存儲管理

提到進程管理就必須首先了解基本的i386體系結構和存儲管理。這是因為體系結構決定了存儲管理的實現,而進程管理又與存儲管理密不可分。在現代操作系統中,存儲管理一般都采用虛擬存儲的方式,也就是系統使用的地址空間與實際物理地址空間不同,是“虛擬”的地址。處理器提供一定的機制將“虛擬”的地址轉換為實際的地址。操作系統的存儲管理就是基於處理器提供的地址轉換機制來實現的。

基本的存儲管理有兩種方式,即段式和頁式。段式管理就是將內存劃分為不同的段(segment),通過段指針來訪問各個段的方法。在比較早的系統中,如pdp-11等就是采用這種方法。這種方法的缺點是在設計程序時必須考慮段的劃分,這是很不方便的。頁式管理就是將內存劃分為固定大小的若干個頁面(page),以頁面為單位分配使用。通過頁映射表完成地址轉換。

i386的存儲管理采用兩級虛擬的段頁式管理,就是說它先分段,再分頁。具體地說,它通過gdt(Global Descriptor Table)和ldt(Local Descriptor Table)進行分段,把虛擬地址轉換為線性地址。然後采用兩級頁表結構進行分頁,把線性地址轉換為物理地址。下圖表示了i386中虛擬地址到物理地址的轉換:

   虛擬地址------>線性地址------>物理地址
        (分段)       (分頁)

 圖1. i386虛擬地址轉換

在Linux中,操作系統處於處理器的0特權級,通過設置gdt,將它的代碼和數據放在獨立的段中,以區別於供用戶使用的用戶數據段和代碼段。所有的用戶程序都使用相同的數據段和代碼段,也就是說所有的用戶程序都處於同一個地址空間中。程序之間的保護是通過建立不同的頁表映射來完成的。Linux 2.2.x gdt表中的段描述符如下圖所示:

 ...
 ...
 APM BIOS數據段
 APM BIOS16位代碼段
 APM BIOS代碼段
 APM BIOS保留
 保留
 保留
 用戶數據段
 用戶代碼段
 內核數據段
 內核代碼段
 空描述符

 圖2. 全局描述符表gdt


在實際的使用中,用戶程序還可以通過調用設置ldt的系統調用來擁有自己的地址空間。

二、Kernel 2.2.x進程管理

進程是一個程序的一次執行的過程,是一個動態的概念。在i386體系結構中,任務和進程是等價的概念。進程管理涉及了系統初始化、進程創建/消亡、進程調度以及進程間通訊等等問題。在Linux的內核中,進程實際上是一組數據結構,包括進程的上下文、調度數據、信號處理、進程隊列指針、進程標識、時間數據、信號量數據等。這組數據都包括在進程控制塊PCB(Process Control Block)中。

Linux進程管理與前面的i386體系結構關系十分密切。前面已經討論了i386所采用的段頁式內存管理的基本內容,實際上,i386中的段還有很多用處。例如,在進程管理中要用到一種特殊的段,就是任務狀態段tss(Task Status Segment)。每一個進程都必須擁有自己的tss,這是通過設置正確的tr寄存器來完成的。根據i386體系結構的定義,tr寄存器中存放的是tss的選擇符(selector),該選擇符必須由gdt中的項(即描述符descriptor)來映射。同樣的,進程的ldt也有這種限制,即ldtr對應的選擇符也必須由gdt中的項來映射。




在kernel 2.2.x中,為了滿足i386體系結構的這種要求,采用了預先分配進程所需要的gdt項目的方法。也就是為每一個進程保留2項gdt條目。進程PCB中的tr和ldtr值就是它在gdt中的選擇符。進程與它的gdt條目的對應關系可以由task數組來表示。

下面是詳細的分析。

1.系統初始化
在Linux 2.2.x中,一些與進程管理相關的數據結構是在系統初始化的時候被初始化的。其中最重要的是gdt和進程表task。
Gdt的初始化主要是確定需要為多少個進程保留空間,也就是需要多大的gdt。這是由一個宏定義NR_TASKS決定的。NR_TASKS的值就是系統的最大進程數,它是在編譯的時候被確定的。由圖2可以知道,gdt的大小是10+2+NR_TASKS*2。
進程表task實際上是一個PCB指針數組,其定義如下:

StrUCt task_struct *task[NR_TASKS] = ;

其中,init_task是系統的第0號進程,也是所有其它進程的父進程。系統在初始化的時候,必須手工設置這個進程,把它加到進程指針表中去,才能啟動進程管理機制。可以看出,這裡task的大小同樣依賴於NR_TASKS的定義。

2.進程創建
在Linux 2.2.x中,進程是通過系統調用fork創建的,新的進程是原來進程的子進程。需要說明的是,在2.2.x中,不存在真正意義上的線程(Thread)。Linux中常用的線程pthread實際上是通過進程來模擬的。也就是說linux中的線程也是通過fork創建的,是“輕”進程。fork系統調用的流程如下:

 (Yes)錯誤處理 --> 返回
                       ^
 |
Do-fork --> 創建進程PCB(a)--> 超過最大進程數?-->(No)加入進程表(b)--> 設置進程屬性 --> 復制父進程數據 --> 復制父進程地址空間(c)--> 復制父進程tss(d)--> 返回

                 圖3.fork系統調用流

比較關鍵的步驟是:
a、b) 將新進程的PCB加入到進程表中。從前面的討論可以知道,進程表task的大小是預先定義的,所以首先要從task中找到一個空的表項:nr = find_empty_process()。如果不能找到空的表項,說明系統已經達到了最大進程數的上限,不能創建新進程。返回的nr是空閒表項的下標。
c) 將父進程的地址空間復制到子進程中。在這一步中,還要復制父進程的ldt到子進程,然後在gdt中建立子進程的ldt描述符(descriptor),並將這個選擇符保存在PCB中。
d) 為子進程設置tss。同時在gdt中建立子進程的tss描述符,並將這個選擇符保存在PCB中。


3.進程調度
這裡我們暫不討論進程調度的算法,只是看一看進程切換時應該做的工作。在Linux 2.2.x中,進程切換是在switch_to函數中完成的。Switch_to基本操作如下:
1) 設置ltr,加載新進程的tss
2) 保存原進程的fs,gs寄存器到它的PCB中。
3) 如果需要,加載新進程的ldt
4) 加載新進程的頁表
加載新進程的fs和gs
以上的tss和ldt的選擇符都是從進程的PCB中取出的。


三、突破最大進程數的限制

1.限制的起因

通過以上的討論,我們可以看出Linux 2.2.x最大進程數限制產生的原因:
Linux 2.2.x中定義的宏NR_TASKS,在編譯時靜態決定了X86系統運行時的進程數上限。該數在系統初始化時靜態決定了gdt表的總長度。但因為i386體系結構的特點,全局描述符表寄存器gdtr長度域為16位,每項描述符為8字節,故可容納的
最大描述符數 = 1《(16-3)=1《13 = 8192 個。

內核在初始化時對gdt表的前12 項已有特殊安排,如下所示:
1) 空描述符(第0項),保留描述符(第1,6,7項)
2) 內核代碼、數據段(第2,3項) 及用戶代碼、數據段(第4,5項)
3) APM BIOS 使用段(第8--11項)
同時由於每個進程需要兩項分別存放tss和ldt描述符,因此理論上
 可用於進程的數目為 = (8192-12)/2=4090個。


2.解決方案

可見,突破最大進程數的限制,必須解決沒有足夠的gdt表項的問題。Gdt的大小是硬件限制的,所以只能考慮動態地設置進程的tss和ldt描述符,取消為進程預先分配gdt空間的做法。
事實上,根據進程數據結構PCB的分析可知,內核中可以動態地尋址到每個進程的tss 和ldt (如果有的話)段,因此在任務切換時通過PCB指針即可引用上述兩段的尋址,分別為:

進程tss段:proc->tss
進程ldt段:proc->mm->segments

所以,靜態地分配並保留這兩個段的描述符是完全沒有必要的。我們可以在任何需要的時候建立起它們,例如在切換時將該兩段的段描述符動態設置到gdt表中即可。
這種方法的關鍵在於在gdt中為每個cpu保留兩個描述符,分別存放該cpu上正在運行的進程的tss、ldt描述符。這兩個描述符則是在進程切換時由操作系統根據進程PCB中的內容建立起來的。


3.方案實現

要突破上述4090進程數的限制,需要動態設置進程的TSS及LDT段描述符。具體體現在兩方面:
1)系統初始化:
 a)在head.S中將靜態設置的GDT表長度置為最大值8192。
 b)在desc.h的GDT表定義中,在進程可用的第一項位置插入NR_CPUS*2項,作為每顆CPU的專用項。用於超出GDT表之外的進程運行使用。仍留下4090-NR_CPUS=4090-32=4058項用於原有算法使用。

CPU0:SHARED_TSS_ENTRY = 12
 SHARED_LDT_ENTRY = 13
CPU1:SHARED_TSS_ENTRY+1 = 14
 SHARED_LDT_ENTRY+1 = 15
...
CPUn:SHARED_TSS_ENTRY+n = SHARED_TSS_ENTRY + n


 SHARED_LDT_ENTRY+n = SHARED_LDT_ENTRY + n
(n
c)改變宏NR_TASKS為變量 int NR_TASKS,並在初始化函數start_kernel()中合適位置動態分配進程指針數組空間:

task = kmalloc(sizeof(void*)*NR_TASKS,GFP_ATOMIC);

這樣,符號名task在內核中的使用方法與靜態分配時一樣。另外變量NR_TASKS可在lilo啟動時以參數形式傳入內核。

d)修改 parse_options(),使內核能夠識別lilo傳來的表示最大進程數的變量nrtasks,以用於task數組空間的分配。

2)任務切換

a) fork時,PCB中的tss.ldt 和 tss.tr分別用於保存該進程的ldt和tss的選擇符,以便在切換時裝載 tr寄存器和ldtr寄存器.由於現在存在大於4090的進程,按照原有算法計算,這些進程的ldt將會超出16位的范圍,所以這裡我們使用了tss結構中保留未用的short __ldth ,與short ldt合並使用變為長整數 (int) ldt,因此賦值指令變為:

*((unsigned long*)&(p->tss.ldt)) = (unsigned long)_LDT(nr);

if(*((unsigned long*)&(p->tss.ldt))<(unsigned long)(8292<<3))
set_ldt_desc(nr,ldt,LDT_ENTRIES);// 此句為原有代碼;
else{ //什麼也不做,留在切換時作類似設置工作}

這種方法的一個好處是僅僅通過判斷PCB中ldt的值,就可以知道該進程是否大於4090,這大大方便了下面的進程切換的實現。

b) 切換時:我們已允許超出gdt表數目的進程數存在,即超出部分的進程無法在gdt表中預先分配,此時可以使用上述為每顆CPU預留的gdt入口,在gdt表中的尋址為:

SHARED_TSS_ENTRY+smp_processor_id();

這樣,超出GDT表的進程段描述符的設置使用如下算法,而未超出部分的處理不變:

...
#define set_shared_tss_desc(addr,cpu)\
 _set_tssldt_desc(gdt_table+SHARED_TSS_ENTRY+2*cpu,(int)addr,235,0x89);

#define set_shared_ldt_desc(addr,size,cpu)\
 _set_tssldt_desc(gdt_table+SHARED_LDT_ENTRY+2*cpu,(int)addr,((size<<3)-1),0x82);
....
void __switch_to(task_struct *prev,task_struct *next){
...
if(next->tss.tr <= 0x0000ffff)
 {
 由原來的代碼處理...
 }else
 {
 set_shared_tss_desc(&next->tss),smp_processor_id());
 set_shared_ldt_desc(&next->mm->segments,LDT_ENTRIES,smp_processor_id());
 }

 //接下來用適當的值裝載ldtr 和tr 寄存器。
 ...
}

綜上所述,可以以盡量簡單的改動來突破最大進程數的限制,並在lilo設置文件中寫上特定參數,啟動時自動傳入內核,就可以實現在kernel v2.2.x下超過4090個的進程數了。參數的格式為:

append = “nrtasks=40000”

4.方案總結

經過以上修改,使系統理論上的進程數上限數可設置到2的31次方.但實際使用中,每增加一個進程純內核內存空間分配的代價為:

PCB及內核棧2頁 + 頁目錄1頁 + 頁表1頁(至少) = 4頁(共16k)

因此,假設機器內存1G,進程內存管理開銷以20k計算,操作系統占據約20M,則最大進程數為:

(1G-20M)/20k=(1073741824-20971520)/20480 = 51404 ~= 5 萬.

實際應用進程的開銷不止20k,若假設為30k,則

1G機器實際最大進程數為=5萬*(2/3) = 33000(個)

需要說明的是,Linux 2.2.x原來的實現方法在進程切換時是比較快的。我們的方案則在進程數小於4090時與原來相近,超過4090個進程後會略微慢一點。



 }

 //接下來用適當的值裝載ldtr 和tr 寄存器。
 ...
}

綜上所述,可以以盡量簡單的改動來突破最大進程數的限制,並在lilo設置文件中寫上特定參數,啟動時自動傳入內核,就可以實現在kernel v2.2.x下超過4090個的進程數了。參數的格式為:

append = “nrtasks=40000”

4.方案總結

經過以上修改,使系統理論上的進程數上限數可設置到2的31次方.但實際使用中,每增加一個進程純內核內存空間分配的代價為:

PCB及內核棧2頁 + 頁目錄1頁 + 頁表1頁(至少) = 4頁(共16k)

因此,假設機器內存1G,進程內存管理開銷以20k計算,操作系統占據約20M,則最大進程數為:

(1G-20M)/20k=(1073741824-20971520)/20480 = 51404 ~= 5 萬.

實際應用進程的開銷不止20k,若假設為30k,則

1G機器實際最大進程數為=5萬*(2/3) = 33000(個)

需要說明的是,Linux 2.2.x原來的實現方法在進程切換時是比較快的。我們的方案則在進程數小於4090時與原來相近,超過4090個進程後會略微慢一點。




因此,假設機器內存1G,進程內存管理開銷以20k計算,操作系統占據約20M,則最大進程數為:

(1G-20M)/20k=(1073741824-20971520)/20480 = 51404 ~= 5 萬.

實際應用進程的開銷不止20k,若假設為30k,則

1G機器實際最大進程數為=5萬*(2/3) = 33000(個)

需要說明的是,Linux 2.2.x原來的實現方法在進程切換時是比較快的。我們的方案則在進程數小於4090時與原來相近,超過4090個進程後會略微慢一點。



Copyright © Linux教程網 All Rights Reserved