如果你認爲本系列文章對你有所幫助,請大家有錢的捧個錢場,點擊此處贊助,贊助額0.1元起步,多少隨意
聲明:本文只用於個人學習交流,若不慎造成侵權,請及時聯繫我,立即予以改正
鋒影
email:[email protected]
1.Introduction
在QNX Neutrino中,微內核與進程管理器一起組成procnto
模塊,所有運行時系統都需要這個模塊。
進程管理器可用於創建多個POSIX進程(每個進程可能包含多個POSIX線程),它的主要職責包括:
- 進程管理,管理進程的創建、銷燬、屬性處理(用戶ID和組ID)等;
- 內存管理,管理一系列的內存保護功能、共享庫、進程間POSIX共享內存等;
- 路徑名管理,管理資源管理器可能附加到的路徑名空間;
用戶進程可以通過內核調用訪問微內核函數,也可以通過向procnto
發送消息來訪問進程管理器函數。
在procnto
中執行線程去調用微內核的方式與其他進程中的線程完全相同,進程管理器代碼和微內核共享相同的地址空間並不意味着有一套特殊的或私有的接口,系統中的所有線程共享相同的內核接口,並且在調用內核時執行特權切換。
2. Process management
procnto
的首要任務就是動態創建新進程,創建的進程也會依賴procnto
提供的內存管理和路徑名管理相關功能。
進程管理包括進程創建、銷燬、屬性(進程ID、用戶ID、組ID)管理。包含以下接口:
-
posix_spawn()
,POSIX接口,通過直接指定要加載的可執行文件來創建子進程。熟悉UNIX系統的人可能知道,這個函數相當於在fork()
之後調用exec*()
,但是更高效,因爲不需要在像fork()
函數中那樣需要複製地址空間,而是在exec*()
調用時直接銷燬和替代; -
spawn()
,QNX Neutrino接口,功能類似於posix_spawn()
,使用這個接口可以控制進程的屬性信息,比如文件描述符、進程組ID、信號、調度策略、調度優先級、堆棧、運行掩碼(SMP系統); -
fork()
,POSIX接口,創建子進程,子進程與父進程共享相同的代碼,並複製父進程的數據。大多數的進程資源都是繼承的,不能繼承的資源包括:進程ID、父進程ID、文件鎖、pending信號和alarms,定時器。fork()
函數可以在兩種情況下使用:
- 創建當前執行環境的新實例,可用
pthread_create()
替代; - 創建一個運行不同程序的新進程,可用
posix_spawn()
來替代;
-
vfork()
,UNIX BSD擴展接口,vfork()
只能在單線程進程中調用。vfork()
函數與fork()
函數不同之處在於,它與父進程共享數據段,在調用exec*()
或exit()
函數之前數據都是共享的,調用exec*()
或exit()
函數之後父進程才能運行; -
exec*()
,POSIX接口,exec*()
系列函數,會用可執行文件加載的新進程,替換當前進程,由於調用進程被替換,因此不會有成功返回。這些函數通常用在fork()
或vfork()
之後,用於加載子進程。更好的方式是使用posix_spawn()
接口。
3. Memory management
在某些實時內核中,會在開發環境中提供內存保護支持,卻很少爲運行時配置提供內存保護,原因是內存和性能的損失。隨着內存保護在很多嵌入式處理器中越來越普遍,內存保護的好處遠遠超過了它帶來的性能損失,最關鍵的一點就是提高了軟件的魯棒性。
內存保護對地址空間進行了隔離,避免了一個進程中的錯誤影響其他進程或內核。啓用MMU後,操作系統可以在發生內存訪問衝突時中止進程,並立刻反饋給程序員,而不是在運行一段時間後突然崩潰。
3.1 MMU
典型的MMU操作方式是將物理內存劃分爲4KB頁面,在內存中會存儲一組頁表,頁表裏存放着虛擬地址到物理地址的映射關係,CPU根據頁表內容來訪問物理內存。
Virtual address mapping(on an X86)
爲了提高性能,通常會使用TLB來提高頁表條目的查找效率。頁表條目中會對頁面進行讀寫等權限控制。當CPU進行上下文切換的時候,如果是不同進程之間,則需要通過MMU來切換不同的地址空間,如果是在一個進程內部,則不需要這步操作。
3.2 Memory protection at run time
在許多嵌入式系統中,會使用硬件看門狗來檢測是否有軟件或硬件異常,在出現異常時則進行重啓。
在內存保護系統中,有一種更好的方式,可以稱爲軟件看門狗。當軟件出現間歇性錯誤時,操作系統可以捕獲事件,並將控制權交給用戶線程,而不是直接進行內存轉儲。用戶線程則可以有選擇的做決定,而不是像硬件看門狗那樣直接重啓。軟件看門狗可以做:
- 中止由於內存訪問衝突而失敗的進程,並在不關閉系統其餘部分的情況下重啓該進程;
- 中止失敗進程以及任何相關進程,將硬件初始化爲安全狀態,再重啓相關進程;
- 如果故障很嚴重,則關閉整個系統,併發出警報;
很顯然,軟件看門狗能更好的進行控制,還可以收集有關軟件故障的信息,有利於事後的診斷。
3.3 Quality control
通過將嵌入式系統劃分成一組協作的、受內存保護的進程,我們可以很容易重用這些組件。加上有明確的接口定義,這些進程可以放心的集成到應用程序中,確保它們不會破壞系統的整體可靠性。當然,應用程序不可能做到完全沒有bug,系統應該設計成能夠容忍並從故障中恢復的架構,而利用MMU提供內存保護正是朝着這個方向邁出了良好的一步。
3.4 Full-protection model
在全保護模型中,QNX Neutrino首先會將image中的所有代碼重定位到一個新的虛擬空間中,使能MMU,設置好初始頁表。這就允許procnto
在支持MMU的環境中啓動,隨後,進程管理器便會接管該環境,再根據啓動的進程來修改頁表。
Full protection VM
3.5 Locking memory
QNX Neutrino支持內存鎖定,進程可以通過鎖定內存來避免獲取內存頁的延遲。
內存鎖定分爲以下幾級:
- Unlocked,未鎖定的內存可以換入換出,內存在映射時完成分配,但是不會創建頁表條目。當第一次訪問內存時會失敗,內存管理器會進行內存的初始化並創建頁表條目,此時線程的狀態爲
WAITPAGE
; - Locked,被鎖定的內存不能被換入換出,會在訪問時發生頁面錯誤;
- Superlocked,QNX Neutrino的擴展實現,不允許任何錯誤,所有內存都必須初始化和私有化,並且在內存映射時設置權限,覆蓋整個線程的地址空間;
3.6 Defragmenting physical memory
就像磁盤碎片一樣,程序的運行也有可能帶來內存碎片問題。
碎片整理的任務包括更改現有的內存分配和映射,以便使用不同的底層物理頁面。通過交換底層的物理內存單元,操作系統可以將碎片化空間合併成連續的區域,但是在移動某些類型的內存時需要小心,因爲這類內存的映射表不能被安全的修改。
- 內核分配的一對一映射的內存區域不能移動,因爲操作系統不能在不更改虛擬地址的情況下更改物理地址;
- 被應用程序鎖定(
mlock()/mlockall()
)的內存不能移動; - 具有IO特權的應用程序默認會鎖定所有頁面,因爲設備驅動通常需要物理地址;
- 目前沒有移動具有互斥鎖對象的內存頁,互斥鎖對象通過物理地址向內核註冊,如果移動帶有互斥鎖對象的頁面,則需要重新編寫這些對象。
4. Pathname management
procnto
允許資源管理器通過提供標準的API接口,管理路徑名空間子集作爲自己的“授權域”。當一個進程打開一個文件時,兼容POSIX的open庫函數會向procnto
發送路徑名消息,procnto
會根據路徑的前綴來判斷由哪一個資源管理器來處理。當一個前綴被重疊註冊時,會使用與最長的前綴關聯的資源管理器來處理。
啓動時,procnto
會創建以下路徑名:
4.1 Resolving pathnames
可以舉個例子來說明一下最長路徑名匹配,假設有以下路徑名進行了註冊,並有對應的模塊:
下表展示了路徑名解析的最長匹配規則:
4.2 Single-device mountpoints
假設有三個服務器:
- 服務器A,QNX 4文件系統,掛載點是
/
,包含兩個文件bin/true
和bin/false
; - 服務器B,Flash文件系統,掛載點是
/bin/
,包含文件ls
和echo
; - 服務器C,產生數字的設備,掛載點是
/dev/random
;
在進程管理器內部,掛載點列表如下:
假設一個客戶端想往服務器C發送消息,客戶端的代碼如下:
int fd;
fd = open("/dev/random", ...);
read(fd, ...);
close(fd);
在這種情況下,C庫代碼會請求進程管理器提供處理路徑/dev/random
的服務器,進程管理器將返回服務器列表:
- 服務器C(可能性最大,最長路徑匹配)
- 服務器A(可能性最小,最短路徑匹配)
根據這些信息,C庫將以此與每個服務器進行聯繫,併發送open
的消息和路徑組件,而服務器會對路徑組件進行驗證: - 服務器C收到空路徑,因爲請求與掛載在同一個路徑下;
- 服務器A收到路徑
dev/random
,因爲它的掛載點是/
;
一旦一個服務器確認了請求,C庫就不會聯繫其他服務器,這意味着只有服務器C拒絕請求時,纔會去聯繫服務器A。
4.3 Unioned filesystem mountpoints
假設有兩個服務器:
- 服務器A, QNX 4文件系統,掛載點是
/
,包含兩個文件bin/true
和bin/false
; - 服務器B, Flash文件系統,掛載點是
/bin
,包含兩個文件ls
和echo
;
兩個服務器都有/bin
路徑,但是包含不同的內容,當兩個服務器都掛載後,可以看到聯合掛載點如下: /
,服務器A;/bin
,服務器A和B;/bin/echo
,服務器B;/bin/false
,服務器A;/bin/ls
,服務器B;/bin/true
,服務器A;
當執行以下代碼,路徑解析跟之前一樣,但是不是將返回限制成一個連接ID,而是去聯繫所有的服務器並詢問它們對路徑的處理。
DIR *dirp;
dirp = opendir("/bin", ...);
closedir(dirp);
結果爲:
- 服務器B將收到一個空路徑,因爲請求與掛載在同一個路徑下;
- 服務器A將收到
bin
,因爲掛載點是/bin
;
結論是,對於處理路徑/bin
的服務器(本例中A和B),我們有一組文件描述符,當調用readdir()
時,可以依次讀取實際的目錄名條目。如果目錄中的任何名稱都是通過常規的open
訪問,那就會執行正常的解析過程,並且只能訪問一個服務器。
4.4 Symbolic prefixes
類似於Linux系統中的鏈接,在QNX中,可以通過ln -s
來建立鏈接,比如:ln -Ps /net/neutron/bin /bin
此時,/bin/ls
會被替換成/net/neutron/bin/ls
,在進行路徑名匹配時,會匹配到/net
上,而/net
指向的是lsm-qnet
,lsm-qnet
資源管理器會去解析neutron
組件,並將進一步的解析請求發送到叫neutron
的網絡節點上,從而在neutron
節點上完成/bin/ls
的解析。符號鏈接允許我們像訪問本地文件系統一樣訪問遠程文件系統。
執行重定向不需要運行本地文件系統,無磁盤工作站的路徑名組織可能如下圖,本地設備(如;dev/ser1
或/dev/console
)將被路由到本地字符設備管理器,而對其他路徑的請求將會被路由到遠程文件系統。
4.5 File descriptor namesapce
文件描述符空間屬於進程內部資源,資源管理器通過使用SCOID
(Server Connection ID)和FD
(File Descriptor)來標識OCB
(open control block),其中在IO管理器中,使用稀疏矩陣完成三者之間的映射:
Sparse array
OCB
結構包含了打開資源的活動信息,下圖中表示一個進程打開文件兩次,另一個進程打開同一文件一次:
Two processes open the same file
也可以多個不同進程之間的描述符指向同一個OCB
結構,通常是在dup()/dup2()/fcntl()
或者vfork()/fork()/posix_spawn()/spawn()
接口調用時產生的,此時一個進程對OCB
操作可能會影響與之關聯的進程,如下圖: