進程與線程
a) 進程和線程是操作系統的概念
b) linux系統屬於分時操作系統,可處理併發任務同時保證快速響應,採用時間片輪轉調度機制,即
操作系統將cpu時間劃分爲時間片,每個任務只佔用一個時間片時間,然後調度隊列中的下一個任務執行
1. 進程
- 程序: 文本,描述數據的組織結構和對其進行的操作,存放在磁盤中
- 進程: 運行中的程序,操作系統將其從磁盤加載進內存,再加載進寄存器
爲其分配資源,包括cpu時間,內存,I/O設備 - 描述進程的邏輯內容:
- 可執行代碼(規則)
- 輸入輸出數據(main函數的輸入輸出)
- 文件描述符(I/O資源)
- 調用堆棧(函數的調用關係)
- 堆棧(保存運行過程中產生的數據,堆由程序員管理,棧由操作系統管理)
- 進程控制塊PCB(包含進程的描述信息和控制信息)
包含進程id, 進程狀態, 進程切換時(中斷/異常後進入中斷/異常處理程序,也是進程切換)
保存的寄存器狀態, 虛擬地址空間和物理地址的對應關係, 打開它的終端的信息,
當前工作目錄, umask掩碼, 文件描述符, 信號相關的信息, 用戶id和組id, 會話和進程組,
可使用的資源上限($ ulimit -a查看當前shell所能使用的資源)
附: c++內存模型:
- 棧(編譯器管理, 局部變量,函數參數,返回值)
- 堆(程序員管理)
- 靜態區(操作系統管理, 全局變量,靜態變量)
- 文字常量區(操作系統管理, 常量字符串)
- 程序代碼區(內存中)
-
不同進程的內容完全隔離,不共享
-
進程間通信提供了進程間數據共享的途徑,開銷都比較大(類比深拷貝)
進程間通信的目的: + 數據傳輸: 一個進程將數據發送給另一個進程 + 資源共享: 多個進程共享同樣的資源,需要內核提供互斥和同步機制 + 通知事件: 一個進程向別的進程發送消息,通知他們發生了某種事件 + 進程控制: 一個進程完全控制另一個進程的執行(如DEBUG進程),控制進程希望能夠攔截 另一個進程的所有陷入和異常及其狀態改變 通信方式包括: + 信號(用於軟中斷和異常處理) + 信號量(用於進程同步,避免數據競爭) + 管道 + 消息隊列 + 共享內存 + socket(包括網絡套接字和unix域套接字)
進程的狀態:
阻塞(or睡眠,不可中斷睡眠和可中斷睡眠) 就緒 運行 停止 中止linux內核爲需要共享資源的多個進程提供的互斥和同步機制:
-
什麼是互斥?什麼是同步?
互斥指的是保證不同進程對同一資源的訪問不衝突的機制
同步指的是實現不同進程對同一資源的訪問順序的機制 -
爲什麼需要同步機制?
+ 多個併發進程對共享資源的爭用
+ 中斷,異常處理,內核態搶佔導致進程以交錯的方式運行,可能對關鍵數據結構交錯修改,
導致這些數據結構狀態的不一致,導致系統的崩潰因此,linux系統中廣義的進程併發包括:
a) 中斷和異常,源程序和中斷處理程序訪問同一個臨界資源
b) 內核態搶佔,更高優先級的進程在當前進程未結束時被調度,訪問同一臨界資源
c) 多個處理器上進程的併發疑問: 難道用戶態搶佔不會造成併發嗎?
內核態搶佔: 當前進程進入內核態後,尚未返回到用戶態,更高優先級的進程被調度執行
用戶態搶佔: 當前進程進入內核態後,cpu發現需要執行另一個更高優先級的進程,但直到當前進程
返回到用戶態後高優先級進程才被調度執行 -
linux內核提供了哪些同步機制?
臨界區: 對共享資源進行訪問的代碼片段,需要在進入臨界區前啓用同步機制,離開後釋放
1) 禁用中斷(單核不可搶佔系統)
2) 自旋鎖(多處理器系統,需要輔助禁用中斷,禁用內核態搶佔)設計目的: 在多處理器系統中保護共享數據
實現方法: 使用全局變量V表示鎖,V=1時鎖定,V=0時解鎖.若處理器A上的代碼要進入臨界區,首先
讀取V的值,若V=1,則忙等(自旋,運行狀態,佔用cpu時間),否則設置V=1,進入臨界區,離開臨界區時
設置V=0,這裏對V的測試和設置爲原子操作實現細節:
對所有系統,禁用中斷,因爲中斷處理程序和當前程序訪問同一全局資源時,導致死鎖.當前進程拿到了
共享資源的鎖,此時發生中斷,中斷處理進程恰好也要訪問該共享資源,由於該鎖已被佔有,因此
中斷處理進程進入忙等狀態,無法返回,則當前進程處於沒有執行完的狀態,也不會釋放鎖.導致死鎖a) 對單核系統,鎖定只是禁用內核態搶佔,解鎖只是啓用內核態搶佔
b) 對多核系統,鎖定時首先禁止內核態搶佔,然後嘗試鎖定,鎖定失敗時執行死循環等待自旋鎖被釋放,
解鎖時首先釋放自旋鎖,然後使能內核態搶佔爲什麼需要禁用內核態搶佔?
自旋的含義是持有cpu時間不鬆手,一直處於運行狀態,如果允許其他進程搶佔
內核,則當前進程就不能保持自旋狀態存在問題:
a) 忙等佔用cpu,當共享資源鎖定時間很短時較高效,但共享資源鎖定時間較長或不確定時浪費cpu資源
b) 未對讀和寫操作進行區分,影響併發性能變體:
a) 讀寫自旋鎖: 允許多個進程同時讀,同一時刻只允許一個進程寫
(不互斥: 讀-讀, 互斥: 讀-寫,寫-讀,寫-寫)
b) 順序自旋鎖: 放寬 讀-寫,避免因爲長時間的讀導致寫進程餓死思路: 寫進程擁有更高的優先級,寫鎖定請求出現時,立即滿足寫鎖定的請求,無論此時 是否有讀進程. 實現: 讀不加鎖,寫加鎖. 爲保證讀取時不會因爲寫入者的出現導致共享數據的更新,在讀取者和寫入者 之間引入整型變量,稱爲順序值sequence,讀取者讀取前後分別讀取該sequence, 如果不一致,說明發生了數據更新,讀取操作無效.注意被保護的共享資源不能 含有指針,因爲寫進程可能使得指針失效,讀進程再訪問該指針時會出錯
3) 信號量(seamaphore)
解決問題: 自旋鎖上鎖失敗時自旋運行佔用cpu時間
解決方法:
a) 進程上鎖失敗時主動釋放cpu資源,進入可中斷睡眠狀態(不再佔用cpu時間),阻塞
在共享資源的等待隊列,其他進程釋放共享資源時發出信號喚醒該隊列中的一個進程
b) 共享資源可允許多個進程訪問,信號量使用計數器實現,並提供兩個原子操作up(),
down()(原子操作要麼全執行,要麼全不執行),如果操作後的結果不小於0(操作前不
小於1)則獲得信號量,否則進入等待隊列.訪問完畢後調用up()釋放信號量
c) 信號量不需要輔助禁用內核態搶佔,因爲被搶佔後該進程進入可中斷睡眠狀態,效果
等同於上鎖失敗,猜測被搶佔時仍然保留有鎖,因爲鎖機制是爲了保護共享資源,而
進程搶佔是爲了滿足實時性的要求疑問:
- 信號量如何解決多個進程同時進入臨界區的衝突問題?
信號量並不能解決,需要額外增加多個同時進入臨界區的進程之間的互斥機制 - 信號量不用禁用中斷嗎?如果不用禁用是否是因爲中斷可以正常獲得鎖或者進入睡眠而不會
造成死鎖? - 中斷處理程序中不能使用信號量,因爲信號量可能會導致中斷處理程序進入睡眠,而中斷處理程序
不是一個獨立的進程(依附於原進程),無法被信號喚醒
- 信號量如何解決多個進程同時進入臨界區的衝突問題?
存在問題:未對讀和寫操作進行區分,影響併發性能 變體: 1) 讀寫信號量 實現: 讀-讀不互斥,進程無法獲得鎖時,睡眠並進入等待隊列,喚醒時按先進先出順序, 如果喚醒的第一個是讀進程,則繼續喚醒下一個讀進程,直到遇見一個寫進程停止 2) 互斥量: 計數器的值爲1,一次只能允許一個進程進入臨界區
4) RCU(待學習)
-
2. 線程
線程是進程內的實體,一個進程可以包含一個或多個線程,線程之間獨享與棧相關的資源,共享
與棧無關的資源,獨享的資源包括:
1) 棧
2) 寄存器(已加載進寄存器的所有程序指令)
3) 程序計數器(執行到指令的哪個位置)
4) 狀態字(線程切換時寄存器的狀態,用戶線程切換回來後恢復現場)
引入線程的目的是提高任務執行的靈活性,因爲線程之間共享進程的資源,更加輕量化
可重入and線程安全
可重入: 針對函數執行轉入中斷處理或異常處理的情況.如果中斷處理函數恰好也調用了該函數,
則稱爲重入,如果中斷調用返回後不會對該函數的運行造成影響,則稱爲可重入函數.
可重入函數必須是無狀態函數,不使用任何在函數間共享的數據.
1) 不在函數內部使用靜態或全局數據
2) 不返回靜態或全局數據
3) 只使用本地數據,或製作全局數據的本地拷貝來保護全局數據
4) 不調用不可重入函數 —!!!—(malloc, free, 標準IO庫函數)
線程安全: 針對多線程環境執行同一個函數可能導致的程序運行邏輯錯誤的情形.
可以使用共享數據,只要保證對共享數據的訪問是互斥的即可,常用加鎖實現.
可重入函數 is 線程安全函數, 線程安全函數 not necessarily 可重入函數
3. linux中進程和線程的區別
- linux使用進程的系統調用來創建線程,其內部並不區分進程和線程,線程相關的函數是庫函數,
面向用戶,而非系統調用 - 進程可看成只包含一個線程的進程
- 進程是最小的資源分配單位,線程是最小的執行單位和調度的基本單位
4. linux系統與用戶程序的關係
- linux系統的職責是管理用戶程序,即調度(Scheduler, Dispatcher),提供任務切換的基本服務.
- 用戶程序的訪問權限需要受到限制,如隔離不同程序的內存地址空間,禁止訪問外圍設備,這些
訪問統一由操作系統提供,因此將程序佔用cpu的狀態被分爲兩級: 用戶態和內核態 - 內核態: 程序可訪問所有的內存數據,包括外圍設備,如硬盤,網卡,也可以主動切換到另一個程序
用戶態: 程序只能訪問受限的內存,不允許訪問外圍設備,佔用的cpu資源可被其他程序獲取 - 內核態到用戶態的切換的場景
a) 系統調用(用戶主動操作),比如讀寫文件,socket操作,進程間通信,創建進程
b) 異常,如缺頁異常
c) 外圍設備的中斷(硬中斷,接作用於cpu,比如硬盤告訴cpu我數據準備好了,你可以讀了)
5. linux系統創建進程的方式
- 開機時,內核只創建一個init進程
- linux內核不提供直接建立新進程的系統調用,剩下的進程都是init進程通過fork機制建立的
新進程通過老進程複製自身得到,即爲fork的原理,fork是系統調用 - 進程存活於內存中,每個進程在內存中都有自己專屬的內存空間
- 進程調用fork時,linux在內存中開闢一塊新的內存空間給新的進程,並將老進程空間的內容複製
到新的空間中,此後2個進程同時運行 - 老進程成爲新進程的父進程,一個進程除了有PID外,還有一個PPID存儲父進程的PID
- 所有運行的進程構成了以init爲根的樹狀結構
- 子進程終結時,通知父進程,並清空自己佔據的內存,並在內核裏留下自己的退出信息(exit
code, 爲0表示順利結束,>0表示有錯誤或異常) - 父進程得知子進程終結時,有責任對子進程使用wait系統調用,從內核中取出子進程的退出信息,
並清空該信息在內核中佔據的空間 - 若父進程早於子進程終結,子進程成爲孤兒進程
- 孤兒進程會被過繼給init進程,init進程負責該子進程終結時調用wait函數
- 當父進程未能調用wait函數時,子進程的退出信息滯留在內核中,子進程成爲僵死進程,大量
僵死進程累積時,內存空間會被擠佔