㈠ linux裡面,進程與線程到底有什麼本質的區別
線程:是進程中執行的一條路徑,是系統調度的最小單位。
進程:是正在運行的程序,是系統分配資源的最小單位。
線程與進程關系
1.一個進程可以有多個線程,一個線程只能屬於一個進程。
2.同一個進程下的所有線程共享該進程下的所有資源。
3.真正在處理機上運行的是線程,不是進程,線程是進程內的一個執行單元,是進程內的可調度實體。
Linux線程與進程區別
進程:
優點:多進程可以同時利用多個CPU,能夠同時進行多個操作。
缺點:耗費資源(創建一個進程重新開辟內存空間)。
進程不是越多越好,一般進程個數等於cpu個數。
線程:
優點:共享內存,尤其是進行IO操作(網路、磁碟)的時候(IO操作很少用cpu),可以使用多線程執行並發操作。
缺點:搶占資源。
㈡ 麻煩解釋一下linux下進程和線程有什麼區別和聯系,linux下多線程和多進程通信的實現方法,請通俗解釋
兄弟看到你這么高的分我就找了些資料:也算是對昨天學的知識總結一下吧
一、先說概念不管是windows還是linux下的進程和線程概念都是一樣的,只是管理進程和線程的方式不一樣,這個是前提,到時候你可別問我windows下進程和線程啊。這個涉及到操作系統原理。下面給你解答。
說道進程不得不提作業這個名詞 ,我想兄弟你電腦里不會有一個程序吧對不?當你的系統啟動完畢後你看看你的任務管理器里是不是有很多進程呢?那麼多程序是怎麼調如內存呢?能理解嗎?這里要明白程序和進程的關系,程序是你磁碟上的一個文件,當你需要它時進入內存後才成為進程,好比QQ在磁碟上就是一個文件而已,只有進入了內存才成為進程,進程是活動的。QQ要掃描你文件啊,記錄你聊天記錄啊,偷偷上傳個啥東西什麼的你也不知道對不,他是活動的。這個能明白嗎?
再看作業,這個作業可不是你寫作業的那個作業啊。系統一看好傢伙你個QQ那麼大的傢伙你想一下子進入內存啊?沒門!慢慢來嘛,系統就把QQ程序分為好幾塊,這幾塊不能亂分的,要符合自然結構就是循環啦選擇啦這樣的結構,你把人家循環結構咔嚓截斷了,怎麼讓人家QQ運行啊?這就是作業要一塊一塊的進入內存,同時要為作業產生JCB(JOB CONTROL BLOCK)作業控制塊,你進入內存不能亂跑啊,要聽系統的話,你要是進入系統自己的內存。框一下,內存不能讀寫 對話框就出來了,嚴重點直接藍臉給你!你懂得。這是window下的,linux下直接給你報錯!沒事了就!所一系統通過jcb控制進程。JCB包含了進程號優先順序好多內容,你打開你的windows任務管理器看看進程是不是有好多屬性啊?那就是PCB(PRCESS,CONTROL BLOCK)同理作業也包含那些內容只是多少而已。下面寫出進程特點:
1、進程是分配計算機資源最小的單位。你想啊人是要用程序幹活的吧?你把程序調入內存成了就成了進程,所以說進程是分配資源的最小單位。你在linux下打開終端輸入top命令看是不是有好多進程?
2、進程有操作系統為作業產生。有「父進程」產生「子進程」之間是父子關系,並可以繼續向下產生「子進程」。還拿QQ來說,你雙擊QQ.exe。QQ啟動了輸入賬號密碼打開主界面了。這時候你要聊天,QQ進程趕緊產生個「兒子」說 「兒子你去陪主人聊天去吧。這樣子進程產生了。突然你想看美女要傳照片這時候那個」兒子「有」生「了一個」兒子「說」兒子「你去傳照片。那個「兒子領到任務去傳照片了。這時你想關了QQ,QQ提示你說」你還有個「兒子」和「孫子」還在幹活呢你真要結束嗎?你蒽了確定。QQ對他「兒子」(你聊天窗口)說:」兒子啊對不起了,主人要關閉我你也不能活啊「咔嚓一下」兒子「死了,兒子死之前對他兒子說:「兒子啊你爺爺不讓我活了,你也別活了咔嚓孫子也死了。最後世界安靜了。這就是進程的父子關系。能明白嗎?記住:進程之活動在內存中。不能使用CPU,只管分配資源。
再說線程:線程也產生在內存中並且在內存中存在相當長的時間,但它的活動區域主要在CPU中,並且運行和滅亡都存在於CPU中,可以這么說,線程是程序中能被系統調度進入CPU中最小程序單位,它能直接使用進程分配的CPU的資源。
還拿QQ來說當你要傳文件時QQ總要判斷一下文件的擴展名吧,ok這時那個」兒子「趕緊對它爸爸說我需要一個線程判斷擴展名QQ趕緊對一個管這個的線程說:」快點去CPU里計算下那個擴展名是什麼然後向主人報告計算完了就「死了」消亡了,但是它的線程還在內存中!還等著你下一次傳文件然後計算然後消亡!
線程之間是相互獨立的。一個在CPU,一個在內存里還能有關系嗎對不?CPU在每一個瞬間只能進入一個線程,當線程進入CPU時立即產生一個新的線程,新線程仍停留在內存中,就好比上面那個傳文件還會等著你再傳文件再計算擴展名。
線程相對線程是獨立的,但它在內存中並不是獨立的,這就好比你不開QQ能用QQ傳輸文件嗎?它只存在與進程分配的資源中,也就是說計算擴展名這個線程只能停留在QQ這個進程中,不能跑到別的進程里!!相當於程序產生了新的進程和線程,進程向CPU申請資源,再有線程來使用,他們都是為程序服務的只是分工不同!
因為你沒提問linux下是怎麼管理進程和線程的所以我就不回答了,這個問題我建議你還是看看《笨兔兔的故事》裡面講到了linux是怎麼管理進程和線程的。挺幽默的比我說得還好。
你第二個問題說實話我回答不了你!我想你現在連進程和線程還沒理解第二個你更理解不了了你說對不?我猜的其實你用C/C++不管是在windows下編程還是在Linux下編程思想都是一樣的對吧,如果你理解了在windows下線程間通信,在linux更沒問題了!
參考資料:黑客手冊2009合訂本非安全第一二季244頁,245頁,328頁,329頁,398頁,399頁
淺談操作系統原理 (一 二三)
ubuntu中文論壇 笨兔兔的故事
http://forum.ubuntu.org.cn/viewtopic.php?f=120&t=267518
希望我的回答你能理解
㈢ linux中內核線程與用戶線程在調度上有什麼區別
用戶級實現線程時,內核調度是以進程為單位的,內核並不知道用戶級線程的存在,因此某個用戶級線程的阻塞即會引起整個進程的阻塞。
內核級線程阻塞時,內核完全可以調度同進程內的其它線程運行,也就是沒有阻塞整個線程
㈣ linux調度是基於進程還是線程
在LINUX系統之中,被調度的應該是進程。因為只有進程才擁有一個獨立的上下文環境,是分配系統資源的最小單位……而線程在SMP體系中加速了執行的效率……
在LINUX之中,線程也可稱作輕量級進程,它能享有自己的堆棧,線程ID等獨立資源,但大多還是要依賴其創建進程,比如地址空間,信號,文件句柄……
㈤ Linux如何進行進程調度引入線程機制後,進程管理內容包括哪些
進程調度的演算法有很多,簡單來說就是每個進程都有一個自己的時間片,時間到了,就會被掛起,然後系統挑選下一個合適的進程來執行。至於誰合適,那就要看演算法了,優先順序,是不是飢餓,I/O型還是運算型,都要考慮的。
調度演算法比較復雜龐大,不是這里說的清楚的。
進程切換的過程大概就是保存當前上下文,也就是各種寄存器的狀態,包括指令寄存器。然後把下一個進程的上下文載入上來。
有了線程機制之後,進程管理主要管理線程之間的數據共享,管理進程地址空間,進程的交換空間。因為這些資源是屬於進程的,線程之間是共享的。
現代操作系統調度基本是圍繞線程進行的,進程更多的是起到資源管理分配的作用。
㈥ linux系統中線程同步實現機制有哪些
LinuxThread的線程機制
LinuxThreads是目前Linux平台上使用最為廣泛的線程庫,由Xavier Leroy ([email protected]) 負責開發完成,並已綁定在GLIBC中發行。它所實現的就是基於核心輕量級進程的"一對一"線程模型,一個線程實體對應一個核心輕量級進程,而線程之間的 管理在核外函數庫中實現。
1.線程描述數據結構及實現限制
LinuxThreads定義了一個struct _pthread_descr_struct數據結構來描述線程,並使用全局數組變數 __pthread_handles來描述和引用進程所轄線程。在__pthread_handles中的前兩項,LinuxThreads定義了兩個全 局的系統線程:__pthread_initial_thread和__pthread_manager_thread,並用 __pthread_main_thread表徵__pthread_manager_thread的父線程(初始為 __pthread_initial_thread)。
struct _pthread_descr_struct是一個雙環鏈表結構,__pthread_manager_thread所在的鏈表僅包括它 一個元素,實際上,__pthread_manager_thread是一個特殊線程,LinuxThreads僅使用了其中的errno、p_pid、 p_priority等三個域。而__pthread_main_thread所在的鏈則將進程中所有用戶線程串在了一起。經過一系列 pthread_create()之後形成的__pthread_handles數組將如下圖所示:
圖2 __pthread_handles數組結構
新創建的線程將首先在__pthread_handles數組中占據一項,然後通過數據結構中的鏈指針連入以__pthread_main_thread為首指針的鏈表中。這個鏈表的使用在介紹線程的創建和釋放的時候將提到。
LinuxThreads遵循POSIX1003.1c標准,其中對線程庫的實現進行了一些范圍限制,比如進程最大線程數,線程私有數據區大小等等。在 LinuxThreads的實現中,基本遵循這些限制,但也進行了一定的改動,改動的趨勢是放鬆或者說擴大這些限制,使編程更加方便。這些限定宏主要集中 在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下幾個:
每進程的私有數據key數,POSIX定義_POSIX_THREAD_KEYS_MAX為128,LinuxThreads使用 PTHREAD_KEYS_MAX,1024;私有數據釋放時允許執行的操作數,LinuxThreads與POSIX一致,定義 PTHREAD_DESTRUCTOR_ITERATIONS為4;每進程的線程數,POSIX定義為64,LinuxThreads增大到1024 (PTHREAD_THREADS_MAX);線程運行棧最小空間大小,POSIX未指定,LinuxThreads使用 PTHREAD_STACK_MIN,16384(位元組)。
2.管理線程
"一對一"模型的好處之一是線程的調度由核心完成了,而其他諸如線程取消、線程間的同步等工作,都是在核外線程庫中完成的。在LinuxThreads 中,專門為每一個進程構造了一個管理線程,負責處理線程相關的管理工作。當進程第一次調用pthread_create()創建一個線程的時候就會創建 (__clone())並啟動管理線程。
在一個進程空間內,管理線程與其他線程之間通過一對"管理管道(manager_pipe[2])"來通訊,該管道在創建管理線程之前創建,在成功啟動 了管理線程之後,管理管道的讀端和寫端分別賦給兩個全局變數__pthread_manager_reader和 __pthread_manager_request,之後,每個用戶線程都通過__pthread_manager_request向管理線程發請求, 但管理線程本身並沒有直接使用__pthread_manager_reader,管道的讀端(manager_pipe[0])是作為__clone ()的參數之一傳給管理線程的,管理線程的工作主要就是監聽管道讀端,並對從中取出的請求作出反應。
創建管理線程的流程如下所示:
(全局變數pthread_manager_request初值為-1)
圖3 創建管理線程的流程
初始化結束後,在__pthread_manager_thread中記錄了輕量級進程號以及核外分配和管理的線程id, 2*PTHREAD_THREADS_MAX+1這個數值不會與任何常規用戶線程id沖突。管理線程作為pthread_create()的調用者線程的 子線程運行,而pthread_create()所創建的那個用戶線程則是由管理線程來調用clone()創建,因此實際上是管理線程的子線程。(此處子 線程的概念應該當作子進程來理解。)
__pthread_manager()就是管理線程的主循環所在,在進行一系列初始化工作後,進入while(1)循環。在循環中,線程以2秒為 timeout查詢(__poll())管理管道的讀端。在處理請求前,檢查其父線程(也就是創建manager的主線程)是否已退出,如果已退出就退出 整個進程。如果有退出的子線程需要清理,則調用pthread_reap_children()清理。
然後才是讀取管道中的請求,根據請求類型執行相應操作(switch-case)。具體的請求處理,源碼中比較清楚,這里就不贅述了。
3.線程棧
在LinuxThreads中,管理線程的棧和用戶線程的棧是分離的,管理線程在進程堆中通過malloc()分配一個THREAD_MANAGER_STACK_SIZE位元組的區域作為自己的運行棧。
用戶線程的棧分配辦法隨著體系結構的不同而不同,主要根據兩個宏定義來區分,一個是NEED_SEPARATE_REGISTER_STACK,這個屬 性僅在IA64平台上使用;另一個是FLOATING_STACK宏,在i386等少數平台上使用,此時用戶線程棧由系統決定具體位置並提供保護。與此同 時,用戶還可以通過線程屬性結構來指定使用用戶自定義的棧。因篇幅所限,這里只能分析i386平台所使用的兩種棧組織方式:FLOATING_STACK 方式和用戶自定義方式。
在FLOATING_STACK方式下,LinuxThreads利用mmap()從內核空間中分配8MB空間(i386系統預設的最大棧空間大小,如 果有運行限制(rlimit),則按照運行限制設置),使用mprotect()設置其中第一頁為非訪問區。該8M空間的功能分配如下圖:
圖4 棧結構示意
低地址被保護的頁面用來監測棧溢出。
對於用戶指定的棧,在按照指針對界後,設置線程棧頂,並計算出棧底,不做保護,正確性由用戶自己保證。
不論哪種組織方式,線程描述結構總是位於棧頂緊鄰堆棧的位置。
4.線程id和進程id
每個LinuxThreads線程都同時具有線程id和進程id,其中進程id就是內核所維護的進程號,而線程id則由LinuxThreads分配和維護。
㈦ 進程調度的Linux 原理
1,SCHED_OTHER 分時調度策略,
2,SCHED_FIFO實時調度策略,先到先服務
3,SCHED_RR實時調度策略,時間片輪轉
實時進程將得到優先調用,實時進程根據實時優先順序決定調度權值,分時進程則通過nice和counter值決定權值,nice越小,counter越大,被調度的概率越大,也就是曾經使用了cpu最少的進程將會得到優先調度。
SHCED_RR和SCHED_FIFO的不同:
當採用SHCED_RR策略的進程的時間片用完,系統將重新分配時間片,並置於就緒隊列尾。放在隊列尾保證了所有具有相同優先順序的RR任務的調度公平。
SCHED_FIFO一旦佔用cpu則一直運行。一直運行直到有更高優先順序任務到達或自己放棄。
如果有相同優先順序的實時進程(根據優先順序計算的調度權值是一樣的)已經准備好,FIFO時必須等待該進程主動放棄後才可以運行這個優先順序相同的任務。而RR可以讓每個任務都執行一段時間。
相同點:
RR和FIFO都只用於實時任務。
創建時優先順序大於0(1-99)。
按照可搶占優先順序調度演算法進行。
就緒態的實時任務立即搶占非實時任務。
所有任務都採用linux分時調度策略時。
1,創建任務指定採用分時調度策略,並指定優先順序nice值(-20~19)。
2,將根據每個任務的nice值確定在cpu上的執行時間(counter)。
3,如果沒有等待資源,則將該任務加入到就緒隊列中。
4,調度程序遍歷就緒隊列中的任務,通過對每個任務動態優先順序的計算(counter+20-nice)結果,選擇計算結果最大的一個去運行,當這個時間片用完後(counter減至0)或者主動放棄cpu時,該任務將被放在就緒隊列末尾(時間片用完)或等待隊列(因等待資源而放棄cpu)中。
5,此時調度程序重復上面計算過程,轉到第4步。
6,當調度程序發現所有就緒任務計算所得的權值都為不大於0時,重復第2步。
所有任務都採用FIFO時,
1,創建進程時指定採用FIFO,並設置實時優先順序rt_priority(1-99)。
2,如果沒有等待資源,則將該任務加入到就緒隊列中。
3,調度程序遍歷就緒隊列,根據實時優先順序計算調度權值(1000+rt_priority),選擇權值最高的任務使用cpu,該FIFO任務將一直佔有cpu直到有優先順序更高的任務就緒(即使優先順序相同也不行)或者主動放棄(等待資源)。
4,調度程序發現有優先順序更高的任務到達(高優先順序任務可能被中斷或定時器任務喚醒,再或被當前運行的任務喚醒,等等),則調度程序立即在當前任務堆棧中保存當前cpu寄存器的所有數據,重新從高優先順序任務的堆棧中載入寄存器數據到cpu,此時高優先順序的任務開始運行。重復第3步。
5,如果當前任務因等待資源而主動放棄cpu使用權,則該任務將從就緒隊列中刪除,加入等待隊列,此時重復第3步。
所有任務都採用RR調度策略時
1,創建任務時指定調度參數為RR,並設置任務的實時優先順序和nice值(nice值將會轉換為該任務的時間片的長度)。
2,如果沒有等待資源,則將該任務加入到就緒隊列中。
3,調度程序遍歷就緒隊列,根據實時優先順序計算調度權值(1000+rt_priority),選擇權值最高的任務使用cpu。
4,如果就緒隊列中的RR任務時間片為0,則會根據nice值設置該任務的時間片,同時將該任務放入就緒隊列的末尾。重復步驟3。
5,當前任務由於等待資源而主動退出cpu,則其加入等待隊列中。重復步驟3。
系統中既有分時調度,又有時間片輪轉調度和先進先出調度
1,RR調度和FIFO調度的進程屬於實時進程,以分時調度的進程是非實時進程。
2,當實時進程准備就緒後,如果當前cpu正在運行非實時進程,則實時進程立即搶占非實時進程。
3,RR進程和FIFO進程都採用實時優先順序做為調度的權值標准,RR是FIFO的一個延伸。FIFO時,如果兩個進程的優先順序一樣,則這兩個優先順序一樣的進程具體執行哪一個是由其在隊列中的未知決定的,這樣導致一些不公正性(優先順序是一樣的,為什麼要讓你一直運行?),如果將兩個優先順序一樣的任務的調度策略都設為RR,則保證了這兩個任務可以循環執行,保證了公平。 調度程序運行時,要在所有處於可運行狀態的進程之中選擇最值得運行的進程投入運行。選擇進程的依據是什麼呢?在每個進程的task_struct 結構中有這么四項:
policy, priority , counter, rt_priority
這四項就是調度程序選擇進程的依據.其中,policy是進程的調度策略,用來區分兩種進程-實時和普通;priority是進程(實時和普通)的優先順序;counter 是進程剩餘的時間片,它的大小完全由priority決定;rt_priority是實時優先順序,這是實時進程所特有的,用於實時進程間的選擇。
首先,Linux 根據policy從整體上區分實時進程和普通進程,因為實時進程和普通進程度調度是不同的,它們兩者之間,實時進程應該先於普通進程而運行,然後,對於同一類型的不同進程,採用不同的標准來選擇進程:
對於普通進程,Linux採用動態優先調度,選擇進程的依據就是進程counter的大小。進程創建時,優先順序priority被賦一個初值,一般為0~70之間的數字,這個數字同時也是計數器counter的初值,就是說進程創建時兩者是相等的。字面上看,priority是「優先順序」、counter是「計數器」的意思,然而實際上,它們表達的是同一個意思-進程的「時間片」。Priority代表分配給該進程的時間片,counter表示該進程剩餘的時間片。在進程運行過程中,counter不斷減少,而priority保持不變,以便在counter變為0的時候(該進程用完了所分配的時間片)對counter重新賦值。當一個普通進程的時間片用完以後,並不馬上用priority對counter進行賦值,只有所有處於可運行狀態的普通進程的時間片(p->;;counter==0)都用完了以後,才用priority對counter重新賦值,這個普通進程才有了再次被調度的機會。這說明,普通進程運行過程中,counter的減小給了其它進程得以運行的機會,直至counter減為0時才完全放棄對CPU的使用,這就相對於優先順序在動態變化,所以稱之為動態優先調度。至於時間片這個概念,和其他不同操作系統一樣的,Linux的時間單位也是「時鍾滴答」,只是不同操作系統對一個時鍾滴答的定義不同而已(Linux為10ms)。進程的時間片就是指多少個時鍾滴答,比如,若priority為20,則分配給該進程的時間片就為20個時鍾滴答,也就是20*10ms=200ms。Linux中某個進程的調度策略(policy)、優先順序(priority)等可以作為參數由用戶自己決定,具有相當的靈活性。內核創建新進程時分配給進程的時間片預設為200ms(更准確的,應為210ms),用戶可以通過系統調用改變它。
對於實時進程,Linux採用了兩種調度策略,即FIFO(先來先服務調度)和RR(時間片輪轉調度)。因為實時進程具有一定程度的緊迫性,所以衡量一個實時進程是否應該運行,Linux採用了一個比較固定的標准。實時進程的counter只是用來表示該進程的剩餘時間片,並不作為衡量它是否值得運行的標准,這和普通進程是有區別的。上面已經看到,每個進程有兩個優先順序,實時優先順序就是用來衡量實時進程是否值得運行的。
這一切看來比較麻煩,但實際上Linux中的實現相當簡單。Linux用函數goodness()來衡量一個處於可運行狀態的進程值得運行的程度。該函數綜合了上面提到的各個方面,給每個處於可運行狀態的進程賦予一個權值(weight),調度程序以這個權值作為選擇進程的唯一依據。
Linux根據policy的值將進程總體上分為實時進程和普通進程,提供了三種調度演算法:一種傳統的Unix調度程序和兩個由POSIX.1b(原名為POSIX.4)操作系統標准所規定的「實時」調度程序。但這種實時只是軟實時,不滿足諸如中斷等待時間等硬實時要求,只是保證了當實時進程需要時一定只把CPU分配給實時進程。
非實時進程有兩種優先順序,一種是靜態優先順序,另一種是動態優先順序。實時進程又增加了第三種優先順序,實時優先順序。優先順序是一些簡單的整數,為了決定應該允許哪一個進程使用CPU的資源,用優先順序代表相對權值-優先順序越高,它得到CPU時間的機會也就越大。
? 靜態優先順序(priority)-不隨時間而改變,只能由用戶進行修改。它指明了在被迫和其他進程競爭CPU之前,該進程所應該被允許的時間片的最大值(但很可能的,在該時間片耗盡之前,進程就被迫交出了CPU)。
? 動態優先順序(counter)-只要進程擁有CPU,它就隨著時間不斷減小;當它小於0時,標記進程重新調度。它指明了在這個時間片中所剩餘的時間量。
? 實時優先順序(rt_priority)-指明這個進程自動把CPU交給哪一個其他進程;較高權值的進程總是優先於較低權值的進程。如果一個進程不是實時進程,其優先順序就是0,所以實時進程總是優先於非實時進程的(但實際上,實時進程也會主動放棄CPU)。
當policy分別為以下值時:
1) SCHED_OTHER:這是普通的用戶進程,進程的預設類型,採用動態優先調度策略,選擇進程的依據主要是根據進程goodness值的大小。這種進程在運行時,可以被高goodness值的進程搶先。
2) SCHED_FIFO:這是一種實時進程,遵守POSIX1.b標準的FIFO(先入先出)調度規則。它會一直運行,直到有一個進程因I/O阻塞,或者主動釋放CPU,或者是CPU被另一個具有更高rt_priority的實時進程搶先。在Linux實現中,SCHED_FIFO進程仍然擁有時間片-只有當時間片用完時它們才被迫釋放CPU。因此,如同POSIX1.b一樣,這樣的進程就象沒有時間片(不是採用分時)一樣運行。Linux中進程仍然保持對其時間片的記錄(不修改counter)主要是為了實現的方便,同時避免在調度代碼的關鍵路徑上出現條件判斷語句 if (!(current->;;policy&;;SCHED_FIFO)){...}-要知道,其他大量非FIFO進程都需要記錄時間片,這種多餘的檢測只會浪費CPU資源。(一種優化措施,不該將執行時間佔10%的代碼的運行時間減少到50%;而是將執行時間佔90%的代碼的運行時間減少到95%。0.9+0.1*0.5=0.95>;;0.1+0.9*0.9=0.91)
3) SCHED_RR:這也是一種實時進程,遵守POSIX1.b標準的RR(循環round-robin)調度規則。除了時間片有些不同外,這種策略與SCHED_FIFO類似。當SCHED_RR進程的時間片用完後,就被放到SCHED_FIFO和SCHED_RR隊列的末尾。
只要系統中有一個實時進程在運行,則任何SCHED_OTHER進程都不能在任何CPU運行。每個實時進程有一個rt_priority,因此,可以按照rt_priority在所有SCHED_RR進程之間分配CPU。其作用與SCHED_OTHER進程的priority作用一樣。只有root用戶能夠用系統調用sched_setscheler,來改變當前進程的類型(sys_nice,sys_setpriority)。
此外,內核還定義了SCHED_YIELD,這並不是一種調度策略,而是截取調度策略的一個附加位。如同前面說明的一樣,如果有其他進程需要CPU,它就提示調度程序釋放CPU。特別要注意的就是這甚至會引起實時進程把CPU釋放給非實時進程。 真正執行調度的函數是schele(void),它選擇一個最合適的進程執行,並且真正進行上下文切換,使得選中的進程得以執行。而reschele_idle(struct task_struct *p)的作用是為進程選擇一個合適的CPU來執行,如果它選中了某個CPU,則將該CPU上當前運行進程的need_resched標志置為1,然後向它發出一個重新調度的處理機間中斷,使得選中的CPU能夠在中斷處理返回時執行schele函數,真正調度進程p在CPU上執行。在schele()和reschele_idle()中調用了goodness()函數。goodness()函數用來衡量一個處於可運行狀態的進程值得運行的程度。此外,在schele()函數中還調用了schele_tail()函數;在reschele_idle()函數中還調用了reschele_idle_slow()。這些函數的實現對理解SMP的調度非常重要,下面一一分析這些函數。先給出每個函數的主要流程圖,然後給出源代碼,並加註釋。
goodness()函數分析
goodness()函數計算一個處於可運行狀態的進程值得運行的程度。一個任務的goodness是以下因素的函數:正在運行的任務、想要運行的任務、當前的CPU。goodness返回下面兩類值中的一個:1000以下或者1000以上。1000或者1000以上的值只能賦給「實時」進程,從0到999的值只能賦給普通進程。實際上,在單處理器情況下,普通進程的goodness值只使用這個范圍底部的一部分,從0到41。在SMP情況下,SMP模式會優先照顧等待同一個處理器的進程。不過,不管是UP還是SMP,實時進程的goodness值的范圍是從1001到1099。
goodness()函數其實是不會返回-1000的,也不會返回其他負值。由於idle進程的counter值為負,所以如果使用idle進程作為參數調用goodness,就會返回負值,但這是不會發生的。
goodness()是個簡單的函數,但是它是linux調度程序不可缺少的部分。運行隊列中的每個進程每次執行schele時都要調度它,因此它的執行速度必須很快。
//在/kernel/sched.c中
static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm)
{ int weight;
if (p->;;policy != SCHED_OTHER) {/*如果是實時進程,則*/
weight = 1000 + p->;;rt_priority;
goto out;
}
/* 將counter的值賦給weight,這就給了進程一個大概的權值,counter中的值表示進程在一個時間片內,剩下要運行的時間.*/
weight = p->;;counter;
if (!weight) /* weight==0,表示該進程的時間片已經用完,則直接轉到標號out*/
goto out;
#ifdef __SMP__
/*在SMP情況下,如果進程將要運行的CPU與進程上次運行的CPU是一樣的,則最有利,因此,假如進程上次運行的CPU與當前CPU一致的話,權值加上PROC_CHANGE_PENALTY,這個宏定義為20。*/
if (p->;;processor == this_cpu)
weight += PROC_CHANGE_PENALTY;
#endif
if (p->;;mm == this_mm) /*進程p與當前運行進程,是同一個進程的不同線程,或者是共享地址空間的不同進程,優先選擇,權值加1*/
weight += 1;
weight += p->;;priority; /* 權值加上進程的優先順序*/
out:
return weight; /* 返回值作為進程調度的唯一依據,誰的權值大,就調度誰運行*/
}
schele()函數分析
schele()函數的作用是,選擇一個合適的進程在CPU上執行,它僅僅根據'goodness'來工作。對於SMP情況,除了計算每個進程的加權平均運行時間外,其他與SMP相關的部分主要由goodness()函數來體現。
流程:
①將prev和next設置為schele最感興趣的兩個進程:其中一個是在調用schele時正在運行的進程(prev),另外一個應該是接著就給予CPU的進程(next)。注意:prev和next可能是相同的-schele可以重新調度已經獲得cpu的進程.
②中斷處理程序運行「下半部分」.
③內核實時系統部分的實現,循環調度程序(SCHED_RR)通過移動「耗盡的」RR進程-已經用完其時間片的進程-到隊列末尾,這樣具有相同優先順序的其他RR進程就可以獲得CPU了。同時,這補充了耗盡進程的時間片。
④由於代碼的其他部分已經決定了進程必須被移進或移出TASK_RUNNING狀態,所以會經常使用schele,例如,如果進程正在等待的硬體條件已經發生,所以如果必要,這個switch會改變進程的狀態。如果進程已經處於TASK_RUNNING狀態,它就無需處理了。如果它是可以中斷的(等待信號),並且信號已經到達了進程,就返回TASK_RUNNING狀態。在所以其他情況下(例如,進程已經處於TASK_UNINTERRUPTIBLE狀態了),應該從運行隊列中將進程移走。
⑤將p初始化為運行隊列的第一個任務;p會遍歷隊列中的所有任務。
⑥c記錄了運行隊列中所有進程最好的「goodness」-具有最好「goodness」的進程是最易獲得CPU的進程。goodness的值越高越好。
⑦遍歷執行任務鏈表,跟蹤具有最好goodness的進程。
⑧這個循環中只考慮了唯一一個可以調度的進程。在SMP模式下,只有任務不在cpu上運行時,即can_schele宏返回為真時,才會考慮該任務。在UP情況下,can_schele宏返回恆為真.
⑨如果循環結束後,得到c的值為0。說明運行隊列中的所有進程的goodness值都為0。goodness的值為0,意味著進程已經用完它的時間片,或者它已經明確說明要釋放CPU。在這種情況下,schele要重新計算進程的counter;新counter的值是原來值的一半加上進程的靜態優先順序(priortiy),除非進程已經釋放CPU,否則原來counter的值為0。因此,schele通常只是把counter初始化為靜態優先順序。(中斷處理程序和由另一個處理器引起的分支在schele搜尋goodness最大值時都將增加此循環中的計數器,因此由於這個原因計數器可能不會為0。顯然,這很罕見。)在counter的值計算完成後,重新開始執行這個循環,找具有最大goodness的任務。
⑩如果schele已經選擇了一個不同於前面正在執行的進程來調度,那麼就必須掛起原來的進程並允許新的進程運行。這時調用switch_to來進行切換。
㈧ 如何實現linux下多線程之間的互斥與同步
Linux設備驅動中必須解決的一個問題是多個進程對共享資源的並發訪問,並發訪問會導致競態,linux提供了多種解決競態問題的方式,這些方式適合不同的應用場景。
Linux內核是多進程、多線程的操作系統,它提供了相當完整的內核同步方法。內核同步方法列表如下:
中斷屏蔽
原子操作
自旋鎖
讀寫自旋鎖
順序鎖
信號量
讀寫信號量
BKL(大內核鎖)
Seq鎖
一、並發與競態:
定義:
並發(concurrency)指的是多個執行單元同時、並行被執行,而並發的執行單元對共享資源(硬體資源和軟體上的全局變數、靜態變數等)的訪問則很容易導致競態(race conditions)。
在linux中,主要的競態發生在如下幾種情況:
1、對稱多處理器(SMP)多個CPU
特點是多個CPU使用共同的系統匯流排,因此可訪問共同的外設和存儲器。
2、單CPU內進程與搶占它的進程
3、中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間
只要並發的多個執行單元存在對共享資源的訪問,競態就有可能發生。
如果中斷處理程序訪問進程正在訪問的資源,則競態也會會發生。
多個中斷之間本身也可能引起並發而導致競態(中斷被更高優先順序的中斷打斷)。
解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執行單元在訪問共享資源的時候,其他的執行單元都被禁止訪問。
訪問共享資源的代碼區域被稱為臨界區,臨界區需要以某種互斥機制加以保護,中斷屏蔽,原子操作,自旋鎖,和信號量都是linux設備驅動中可採用的互斥途徑。
臨界區和競爭條件:
所謂臨界區(critical regions)就是訪問和操作共享數據的代碼段,為了避免在臨界區中並發訪問,編程者必須保證這些代碼原子地執行——也就是說,代碼在執行結束前不可被打斷,就如同整個臨界區是一個不可分割的指令一樣,如果兩個執行線程有可能處於同一個臨界區中,那麼就是程序包含一個bug,如果這種情況發生了,我們就稱之為競爭條件(race conditions),避免並發和防止競爭條件被稱為同步。
死鎖:
死鎖的產生需要一定條件:要有一個或多個執行線程和一個或多個資源,每個線程都在等待其中的一個資源,但所有的資源都已經被佔用了,所有線程都在相互等待,但它們永遠不會釋放已經佔有的資源,於是任何線程都無法繼續,這便意味著死鎖的發生。
二、中斷屏蔽
在單CPU范圍內避免競態的一種簡單方法是在進入臨界區之前屏蔽系統的中斷。
由於linux內核的進程調度等操作都依賴中斷來實現,內核搶占進程之間的並發也就得以避免了。
中斷屏蔽的使用方法:
local_irq_disable()//屏蔽中斷
//臨界區
local_irq_enable()//開中斷
特點:
由於linux系統的非同步IO,進程調度等很多重要操作都依賴於中斷,在屏蔽中斷期間所有的中斷都無法得到處理,因此長時間的屏蔽是很危險的,有可能造成數據丟失甚至系統崩潰,這就要求在屏蔽中斷之後,當前的內核執行路徑應當盡快地執行完臨界區的代碼。
中斷屏蔽只能禁止本CPU內的中斷,因此,並不能解決多CPU引發的競態,所以單獨使用中斷屏蔽並不是一個值得推薦的避免競態的方法,它一般和自旋鎖配合使用。
三、原子操作
定義:原子操作指的是在執行過程中不會被別的代碼路徑所中斷的操作。
(原子原本指的是不可分割的微粒,所以原子操作也就是不能夠被分割的指令)
(它保證指令以「原子」的方式執行而不能被打斷)
原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷。在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間。這也是某些CPU指令系統中引入了test_and_set、test_and_clear等指令用於臨界資源互斥的原因。但是,在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由於系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。我們以decl (遞減指令)為例,這是一個典型的"讀-改-寫"過程,涉及兩次內存訪問。
通俗理解:
原子操作,顧名思義,就是說像原子一樣不可再細分。一個操作是原子操作,意思就是說這個操作是以原子的方式被執行,要一口氣執行完,執行過程不能夠被OS的其他行為打斷,是一個整體的過程,在其執行過程中,OS的其它行為是插不進來的。
分類:linux內核提供了一系列函數來實現內核中的原子操作,分為整型原子操作和位原子操作,共同點是:在任何情況下操作都是原子的,內核代碼可以安全的調用它們而不被打斷。
原子整數操作:
針對整數的原子操作只能對atomic_t類型的數據進行處理,在這里之所以引入了一個特殊的數據類型,而沒有直接使用C語言的int型,主要是出於兩個原因:
第一、讓原子函數只接受atomic_t類型的操作數,可以確保原子操作只與這種特殊類型數據一起使用,同時,這也確保了該類型的數據不會被傳遞給其它任何非原子函數;
第二、使用atomic_t類型確保編譯器不對相應的值進行訪問優化——這點使得原子操作最終接收到正確的內存地址,而不是一個別名,最後就是在不同體系結構上實現原子操作的時候,使用atomic_t可以屏蔽其間的差異。
原子整數操作最常見的用途就是實現計數器。
另一點需要說明原子操作只能保證操作是原子的,要麼完成,要麼不完成,不會有操作一半的可能,但原子操作並不能保證操作的順序性,即它不能保證兩個操作是按某個順序完成的。如果要保證原子操作的順序性,請使用內存屏障指令。
atomic_t和ATOMIC_INIT(i)定義
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }
在你編寫代碼的時候,能使用原子操作的時候,就盡量不要使用復雜的加鎖機制,對多數體系結構來講,原子操作與更復雜的同步方法相比較,給系統帶來的開銷小,對高速緩存行的影響也小,但是,對於那些有高性能要求的代碼,對多種同步方法進行測試比較,不失為一種明智的作法。
原子位操作:
針對位這一級數據進行操作的函數,是對普通的內存地址進行操作的。它的參數是一個指針和一個位號。
為方便其間,內核還提供了一組與上述操作對應的非原子位函數,非原子位函數與原子位函數的操作完全相同,但是,前者不保證原子性,且其名字前綴多兩個下劃線。例如,與test_bit()對應的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已經用鎖保護了自己的數據),那麼這些非原子的位函數相比原子的位函數可能會執行得更快些。
四、自旋鎖
自旋鎖的引入:
如 果每個臨界區都能像增加變數這樣簡單就好了,可惜現實不是這樣,而是臨界區可以跨越多個函數,例如:先得從一個數據結果中移出數據,對其進行格式轉換和解 析,最後再把它加入到另一個數據結構中,整個執行過程必須是原子的,在數據被更新完畢之前,不能有其他代碼讀取這些數據,顯然,簡單的原子操作是無能為力 的(在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間),這就需要使用更為復雜的同步方法——鎖來提供保護。
自旋鎖的介紹:
Linux內核中最常見的鎖是自旋鎖(spin lock),自旋鎖最多隻能被一個可執行線程持有,如果一個執行線程試圖獲得一個被爭用(已經被持有)的自旋鎖,那麼該線程就會一直進行忙循環—旋轉—等待鎖重新可用,要是鎖未被爭用,請求鎖的執行線程便能立刻得到它,繼續執行,在任意時間,自旋鎖都可以防止多於一個的執行線程同時進入理解區,注意同一個鎖可以用在多個位置—例如,對於給定數據的所有訪問都可以得到保護和同步。
一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費處理器時間),所以自旋鎖不應該被長時間持有,事實上,這點正是使用自旋鎖的初衷,在短期間內進行輕量級加鎖,還可以採取另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖重新可用時再喚醒它,這樣處理器就不必循環等待,可以去執行其他代碼,這也會帶來一定的開銷——這里有兩次明顯的上下文切換, 被阻塞的線程要換出和換入。因此,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時,當然我們大多數人不會無聊到去測量上下文切換的耗時,所以我們讓持 有自旋鎖的時間應盡可能的短就可以了,信號量可以提供上述第二種機制,它使得在發生爭用時,等待的線程能投入睡眠,而不是旋轉。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號量,因為它們會導致睡眠),在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,首先禁止本地中斷(在 當前處理器上的中斷請求),否則,中斷處理程序就會打斷正持有鎖的內核代碼,有可能會試圖去爭用這個已經持有的自旋鎖,這樣以來,中斷處理程序就會自旋, 等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執行完畢前不可能運行,這正是我們在前一章節中提到的雙重請求死鎖,注意,需要關閉的只是當前處理器上的中斷,如果中斷發生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。
自旋鎖的簡單理解:
理解自旋鎖最簡單的方法是把它作為一個變數看待,該變數把一個臨界區或者標記為「我當前正在運行,請稍等一會」或者標記為「我當前不在運行,可以被使用」。如果A執行單元首先進入常式,它將持有自旋鎖,當B執行單元試圖進入同一個常式時,將獲知自旋鎖已被持有,需等到A執行單元釋放後才能進入。
自旋鎖的API函數:
其實介紹的幾種信號量和互斥機制,其底層源碼都是使用自旋鎖,可以理解為自旋鎖的再包裝。所以從這里就可以理解為什麼自旋鎖通常可以提供比信號量更高的性能。
自旋鎖是一個互斥設備,他只能會兩個值:「鎖定」和「解鎖」。它通常實現為某個整數之中的單個位。
「測試並設置」的操作必須以原子方式完成。
任何時候,只要內核代碼擁有自旋鎖,在相關CPU上的搶占就會被禁止。
適用於自旋鎖的核心規則:
(1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些情況下也不能放棄CPU,如中斷服務也要獲得自旋鎖。為了避免這種鎖陷阱,需要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發生在許多無法預期的地方)。否則CPU將有可能永遠自旋下去(死機)。
(2)擁有自旋鎖的時間越短越好。
需 要強調的是,自旋鎖別設計用於多處理器的同步機制,對於單處理器(對於單處理器並且不可搶占的內核來說,自旋鎖什麼也不作),內核在編譯時不會引入自旋鎖 機制,對於可搶占的內核,它僅僅被用於設置內核的搶占機制是否開啟的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啟內核搶占功能。如果內核不支持搶 占,那麼自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;
對於不支持SMP的內核來說,struct raw_spinlock_t什麼也沒有,是一個空結構。對於支持多處理器的內核來說,struct raw_spinlock_t定義為
typedef struct {
unsigned int slock;
} raw_spinlock_t;
slock表示了自旋鎖的狀態,「1」表示自旋鎖處於解鎖狀態(UNLOCK),「0」表示自旋鎖處於上鎖狀態(LOCKED)。
break_lock表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶占的SMP內核上才起作用。
自旋鎖的實現是一個復雜的過程,說它復雜不是因為需要多少代碼或邏輯來實現它,其實它的實現代碼很少。自旋鎖的實現跟體系結構關系密切,核心代碼基本也是由匯編語言寫成,與體協結構相關的核心代碼都放在相關的目錄下,比如。對於我們驅動程序開發人員來說,我們沒有必要了解這么spinlock的內部細節,如果你對它感興趣,請參考閱讀Linux內核源代碼。對於我們驅動的spinlock介面,我們只需包括頭文件。在我們詳細的介紹spinlock的API之前,我們先來看看自旋鎖的一個基本使用格式:
#include
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock(&lock);
....
spin_unlock(&lock);
從使用上來說,spinlock的API還很簡單的,一般我們會用的的API如下表,其實它們都是定義在中的宏介面,真正的實現在中
#include
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned long flags)
spin_unlock_irqsace(spinlock_t *, unsigned long flags)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)
• 初始化
spinlock有兩種初始化形式,一種是靜態初始化,一種是動態初始化。對於靜態的spinlock對象,我們用 SPIN_LOCK_UNLOCKED來初始化,它是一個宏。當然,我們也可以把聲明spinlock和初始化它放在一起做,這就是 DEFINE_SPINLOCK宏的工作,因此,下面的兩行代碼是等價的。
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init 函數一般用來初始化動態創建的spinlock_t對象,它的參數是一個指向spinlock_t對象的指針。當然,它也可以初始化一個靜態的沒有初始化的spinlock_t對象。
spinlock_t *lock
......
spin_lock_init(lock);
• 獲取鎖
內核提供了三個函數用於獲取一個自旋鎖。
spin_lock:獲取指定的自旋鎖。
spin_lock_irq:禁止本地中斷並獲取自旋鎖。
spin_lock_irqsace:保存本地中斷狀態,禁止本地中斷並獲取自旋鎖,返回本地中斷狀態。
自旋鎖是可以使用在中斷處理程序中的,這時需要使用具有關閉本地中斷功能的函數,我們推薦使用 spin_lock_irqsave,因為它會保存加鎖前的中斷標志,這樣就會正確恢復解鎖時的中斷標志。如果spin_lock_irq在加鎖時中斷是關閉的,那麼在解鎖時就會錯誤的開啟中斷。
另外兩個同自旋鎖獲取相關的函數是:
spin_trylock():嘗試獲取自旋鎖,如果獲取失敗則立即返回非0值,否則返回0。
spin_is_locked():判斷指定的自旋鎖是否已經被獲取了。如果是則返回非0,否則,返回0。
• 釋放鎖
同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。
spin_unlock:釋放指定的自旋鎖。
spin_unlock_irq:釋放自旋鎖並激活本地中斷。
spin_unlock_irqsave:釋放自旋鎖,並恢復保存的本地中斷狀態。
五、讀寫自旋鎖
如 果臨界區保護的數據是可讀可寫的,那麼只要沒有寫操作,對於讀是可以支持並發操作的。對於這種只要求寫操作是互斥的需求,如果還是使用自旋鎖顯然是無法滿 足這個要求(對於讀操作實在是太浪費了)。為此內核提供了另一種鎖-讀寫自旋鎖,讀自旋鎖也叫共享自旋鎖,寫自旋鎖也叫排他自旋鎖。
讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了「自旋」的概念,但是在寫操作方面,只能最多有一個寫進程,在讀操作方面,同時可以有多個讀執行單元,當然,讀和寫也不能同時進行。
讀寫自旋鎖的使用也普通自旋鎖的使用很類似,首先要初始化讀寫自旋鎖對象:
// 靜態初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//動態初始化
rwlock_t *rwlock;
...
rw_lock_init(rwlock);
在讀操作代碼里對共享數據獲取讀自旋鎖:
read_lock(&rwlock);
...
read_unlock(&rwlock);
在寫操作代碼里為共享數據獲取寫自旋鎖:
write_lock(&rwlock);
...
write_unlock(&rwlock);
需要注意的是,如果有大量的寫操作,會使寫操作自旋在寫自旋鎖上而處於寫飢餓狀態(等待讀自旋鎖的全部釋放),因為讀自旋鎖會自由的獲取讀自旋鎖。
讀寫自旋鎖的函數類似於普通自旋鎖,這里就不一一介紹了,我們把它列在下面的表中。
RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)
六、順序瑣
順序瑣(seqlock)是對讀寫鎖的一種優化,若使用順序瑣,讀執行單元絕不會被寫執行單元阻塞,也就是說,讀執行單元可以在寫執行單元對被順序瑣保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作才去進行寫操作。
但是,寫執行單元與寫執行單元之間仍然是互斥的,即如果有寫執行單元在進行寫操作,其它寫執行單元必須自旋在哪裡,直到寫執行單元釋放了順序瑣。
如果讀執行單元在讀操作期間,寫執行單元已經發生了寫操作,那麼,讀執行單元必須重新讀取數據,以便確保得到的數據是完整的,這種鎖在讀寫同時進行的概率比較小時,性能是非常好的,而且它允許讀寫同時進行,因而更大的提高了並發性,
注意,順序瑣由一個限制,就是它必須被保護的共享資源不含有指針,因為寫執行單元可能使得指針失效,但讀執行單元如果正要訪問該指針,將導致Oops。
七、信號量
Linux中的信號量是一種睡眠鎖,如果有一個任務試圖獲得一個已經被佔用的信號量時,信號量會將其推進一個等待隊列,然後讓其睡眠,這時處理器能重獲自由,從而去執行其它代碼,當持有信號量的進程將信號量釋放後,處於等待隊列中的哪個任務被喚醒,並獲得該信號量。
信號量,或旗標,就是我們在操作系統里學習的經典的P/V原語操作。
P:如果信號量值大於0,則遞減信號量的值,程序繼續執行,否則,睡眠等待信號量大於0。
V:遞增信號量的值,如果遞增的信號量的值大於0,則喚醒等待的進程。
信號量的值確定了同時可以有多少個進程可以同時進入臨界區,如果信號量的初始值始1,這信號量就是互斥信號量(MUTEX)。對於大於1的非0值信號量,也可稱為計數信號量(counting semaphore)。對於一般的驅動程序使用的信號量都是互斥信號量。
類似於自旋鎖,信號量的實現也與體系結構密切相關,具體的實現定義在頭文件中,對於x86_32系統來說,它的定義如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
信號量的初始值count是atomic_t類型的,這是一個原子操作類型,它也是一個內核同步技術,可見信號量是基於原子操作的。我們會在後面原子操作部分對原子操作做詳細介紹。
信號量的使用類似於自旋鎖,包括創建、獲取和釋放。我們還是來先展示信號量的基本使用形式:
static DECLARE_MUTEX(my_sem);
......
if (down_interruptible(&my_sem))
{
return -ERESTARTSYS;
}
......
up(&my_sem)
Linux內核中的信號量函數介面如下:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
• 初始化信號量
信號量的初始化包括靜態初始化和動態初始化。靜態初始化用於靜態的聲明並初始化信號量。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
對於動態聲明或創建的信號量,可以使用如下函數進行初始化:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)
顯然,帶有MUTEX的函數始初始化互斥信號量。LOCKED則初始化信號量為鎖狀態。
• 使用信號量
信號量初始化完成後我們就可以使用它了
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
down函數會嘗試獲取指定的信號量,如果信號量已經被使用了,則進程進入不可中斷的睡眠狀態。down_interruptible則會使進程進入可中斷的睡眠狀態。關於進程狀態的詳細細節,我們在內核的進程管理里在做詳細介紹。
down_trylock嘗試獲取信號量, 如果獲取成功則返回0,失敗則會立即返回非0。
當退出臨界區時使用up函數釋放信號量,如果信號量上的睡眠隊列不為空,則喚醒其中一個等待進程。
八、讀寫信號量
類似於自旋鎖,信號量也有讀寫信號量。讀寫信號量API定義在頭文件中,它的定義其實也是體系結構相關的,因此具體實現定義在頭文件中,以下是x86的例子:
struct rw_semaphore {
signed long count;
spinlock_t wait_lock;
struct list_head wait_list;
};
㈨ 為什麼說LINUX 內核調度的是線程,而不是進程呢 難道內核中進程是不切換,只切換線程
貌似不對哦,在LINUX系統之中,被調度的應該是進程。因為只有進程才擁有一個獨立的上下文環境,是分配系統資源的最小單位……而線程在SMP體系中加速了執行的效率……
在LINUX之中,線程也可稱作輕量級進程,它能享有自己的堆棧,線程ID等獨立資源,但大多還是要依賴其創建進程,比如地址空間,信號,文件句柄……