【進階】從 linux 到 android,程式的方方面面

Anthony發表於2016-06-16

最近在閱讀《Linux核心設計與實現》,這裡做一下linux中程式相關的知識點整理,以及android中程式的淺析。

下面1,2小節整理自《Linux核心設計與實現》 第三章《程式管理》和第四章《程式排程》。第3節整理android中程式的知識點。

1 Linux中的程式管理

以下內容整理自:《Linux核心設計與實現》 第三章《程式管理》

1.1程式和執行緒

程式是資源分配的最小單位。
執行緒是作業系統排程執行的最小單位。

程式和執行緒是程式執行時狀態,是動態變化的,程式和執行緒的管理操作(比如,建立,銷燬等)都是由核心來實現的。Linux中的程式於Windows相比是很輕量級的,而且不嚴格區分程式和執行緒,執行緒是一種特殊的程式。

程式提供2種虛擬機器制:虛擬處理器和虛擬記憶體
每個程式有獨立的虛擬處理器和虛擬記憶體,每個執行緒有獨立的虛擬處理器,同一個程式內的執行緒有可能會共享虛擬記憶體。

核心把程式的列表存放在任務佇列(task list)中(雙向迴圈連結串列),連結串列的每一項型別為task_struct,我們稱之為程式描述符(process descriptor)。程式的資訊主要儲存在task_struct中(位於 include/linux/sched.h)

 1833901-b93eccc35f31487b

通過task_struct和thread_info存放和表示程式。

 1833901-6ccaad23134ab9fb

 

程式標識PID(process identification value)和執行緒標識TID(thread identification value)對於同一個程式或執行緒來說都是相等的。
Linux中可以用ps命令檢視所有程式的資訊:
ps -eo pid,tid,ppid,comm

1.2 程式的生命週期

程式的各個狀態之間的轉化構成了程式的整個生命週期。

 1833901-0a02648834412e5f

 

程式有五種程式狀態:
除了圖片上面的三種還有,_TASK_TRACED_TASK_STOPPED

1.3 程式的建立

Linux中建立程式分2步:fork()和exec()。

1 fork(): 通過拷貝當前程式建立一個子程式 (實際上最終是通過clone( ) )
2 exec(): 讀取可執行檔案,將其載入到地址空間中執行 (是一個系統呼叫族)

建立的流程:

1 呼叫dup_task_struct()為新程式分配核心棧,task_struct等,其中的內容與父程式相同。
2 check新程式(程式數目是否超出上限等)
3 清理新程式的資訊(比如PID置0等),使之與父程式區別開。
4 新程式狀態置為 TASK_UNINTERRUPTIBLE
5 更新task_struct的flags成員。
6 呼叫alloc_pid()為新程式分配一個有效的PID
7 根據clone()的引數標誌,拷貝或共享相應的資訊
8 做一些掃尾工作並返回新程式指標
9 建立程式的fork()函式實際上最終是呼叫clone()函式。

建立執行緒和程式的步驟一樣,只是最終傳給clone()函式的引數不同。
比如,通過一個普通的fork來建立程式,相當於:clone(SIGCHLD, 0)
建立一個和父程式共享地址空間,檔案系統資源,檔案描述符和訊號處理程式的程式,即一個執行緒:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

Linux通過Clone()系統呼叫實現fork()

在核心中建立的核心執行緒與普通的程式之間還有個主要區別在於:核心執行緒沒有獨立的地址空間,它們只能在核心空間執行。這與之前提到的Linux核心是個單核心有關。

 

1.4 程式的終止

發生在程式呼叫exit()系統呼叫時
和建立程式一樣,終結一個程式同樣有很多步驟:

子程式上的操作,靠do_exit()完成->定義於(kernel/exit.c)

1 設定task_struct中的標識成員設定為PF_EXITING
2 呼叫del_timer_sync()刪除核心定時器, 確保沒有定時器在排隊和執行
3 呼叫exit_mm()釋放程式佔用的mm_struct
4 呼叫sem__exit(),使程式離開等待IPC訊號的佇列
5 呼叫exit_files()和exit_fs(),釋放程式佔用的檔案描述符和檔案系統資源
6 把task_struct的exit_code設定為程式的返回值
7 呼叫exit_notify()向父程式傳送訊號,並把自己的狀態設為EXIT_ZOMBIE
8 切換到新程式繼續執行

子程式進入EXIT_ZOMBIE之後,雖然永遠不會被排程,關聯的資源也釋放 掉了,但是它本身佔用的記憶體還沒有釋放,比如建立時分配的核心棧,task_struct結構等。這些由父程式來釋放。父程式上的操作(release_task)
父程式受到子程式傳送的exit_notify()訊號後,將該子程式的程式描述符和所有程式獨享的資源全部刪除。

從上面的步驟可以看出,必須要確保每個子程式都有父程式,如果父程式在子程式結束之前就已經結束了會怎麼樣呢?
子程式在呼叫exit_notify()時已經考慮到了這點。如果子程式的父程式已經退出了,那麼子程式在退出時,exit_notify()函式會先呼叫forget_original_parent(),然後再呼叫find_new_reaper()來尋找新的父程式。find_new_reaper()函式先在當前執行緒組中找一個執行緒作為父親,如果找不到,就讓init做父程式。(init程式是在linux啟動時就一直存在的)


2 Linux中的程式排程

以下內容整理自:《Linux核心設計與實現》 第四章《程式排程》

2.1 什麼是排程?

現在的作業系統都是多工的,為了能讓更多的任務能同時在系統上更好的執行,需要一個管理程式來管理計算機上同時執行的各個任務(也就是程式)。這個管理程式就是排程程式,它的功能說起來很簡單:

1決定哪些程式執行,哪些程式等待
2 決定每個程式執行多長時間

此外,為了獲得更好的使用者體驗,執行中的程式還可以立即被其他更緊急的程式打斷。
總之,排程是一個平衡的過程。一方面,它要保證各個執行的程式能夠最大限度的使用CPU(即儘量少的切換程式,程式切換過多,CPU的時間會浪費在切換上);另一方面,保證各個程式能公平的使用CPU(即防止一個程式長時間獨佔CPU的情況)。

2.2 排程實現的原理

前面說過,排程功能就是決定哪個程式執行以及程式執行多長時間
決定哪個程式執行以及執行多長時間都和程式的優先順序有關。為了確定一個程式到底能持續執行多長時間,排程中還引入了時間片的概念。

關於程式的優先順序

1 程式的優先順序有2種度量方法,一種是nice值,一種是實時優先順序。
2 nice值的範圍是-20~+19,值越大優先順序越低,也就是說nice值為-20的程式優先順序最大。
3 實時優先順序的範圍是0~99,與nice值的定義相反,實時優先順序是值越大優先順序越高。
4 實時程式都是一些對響應時間要求比較高的程式,因此係統中有實時優先順序高的程式處於執行佇列的話,它們會搶佔一般的程式的執行時間。
5 實時優先順序高於nice值。
6 一個程式不可能有2個優先順序。

關於時間片

有了優先順序,可以決定誰先執行了。但是對於排程程式來說,並不是執行一次就結束了,還必須知道間隔多久進行下次排程。
於是就有了時間片的概念。時間片是一個數值,表示一個程式被搶佔前能持續執行的時間。
也可以認為是程式在下次排程發生前執行的時間(除非程式主動放棄CPU,或者有實時程式來搶佔CPU)。
時間片的大小設定並不簡單,設大了,系統響應變慢(排程週期長);設小了,程式頻繁切換帶來的處理器消耗。預設的時間片一般是10ms

排程實現原理
下面舉個直觀的例子來說明:

假設系統中只有3個程式ProcessA(NI=+10),ProcessB(NI=0),ProcessC(NI=-10),NI表示程式的nice值,時間片=10ms
1) 排程前,把程式優先順序按一定的權重對映成時間片(這裡假設優先順序高一級相當於多5msCPU時間)。
假設ProcessA分配了一個時間片10ms,那麼ProcessB的優先順序比ProcessA高10(nice值越小優先順序越高),ProcessB應該分配105+10=60ms,以此類推,ProcessC分配205+10=110ms
2) 開始排程時,優先排程分配CPU時間多的程式。由於ProcessA(10ms),ProcessB(60ms),ProcessC(110ms)。顯然先排程ProcessC
3) 10ms(一個時間片)後,再次排程時,ProcessA(10ms),ProcessB(60ms),ProcessC(100ms)。ProcessC剛執行了10ms,所以變成100ms。此時仍然先排程ProcessC
4) 再排程4次後(4個時間片),ProcessA(10ms),ProcessB(60ms),ProcessC(60ms)。此時ProcessB和ProcessC的CPU時間一樣,這時得看ProcessB和ProcessC誰在CPU執行佇列的前面,假設ProcessB在前面,則排程ProcessB
5) 10ms(一個時間片)後,ProcessA(10ms),ProcessB(50ms),ProcessC(60ms)。再次排程ProcessC
6) ProcessB和ProcessC交替執行,直至ProcessA(10ms),ProcessB(10ms),ProcessC(10ms)。
這時得看ProcessA,ProcessB,ProcessC誰在CPU執行佇列的前面就先排程誰。這裡假設排程ProcessA
7) 10ms(一個時間片)後,ProcessA(時間片用完後退出),ProcessB(10ms),ProcessC(10ms)。
8) 再過2個時間片,ProcessB和ProcessC也執行完退出。

這個例子很簡單,主要是為了說明排程的原理,實際的排程演算法雖然不會這麼簡單,但是基本的實現原理也是類似的:

1)確定每個程式能佔用多少CPU時間(這裡確定CPU時間的演算法有很多,根據不同的需求會不一樣)
2)佔用CPU時間多的先執行
3)執行完後,扣除執行程式的CPU時間,再回到 1)

 

2.3 Linux上排程實現的方法

Linux上的排程演算法是不斷髮展的,在2.6.23核心以後,採用了“完全公平排程演算法”,簡稱CFS。
CFS演算法在分配每個程式的CPU時間時,不是分配給它們一個絕對的CPU時間,而是根據程式的優先順序分配給它們一個佔用CPU時間的百分比。

2.4 排程相關的系統呼叫

排程相關的系統呼叫主要有2類:

1) 與排程策略和程式優先順序相關 (就是上面的提到的各種引數,優先順序,時間片等等) – 下表中的前8個

2) 與處理器相關 – 下表中的最後3個

系統呼叫 描述
nice() 設定程式的nice值
sched_setscheduler() 設定程式的排程策略,即設定程式採取何種排程演算法
sched_getscheduler() 獲取程式的排程演算法
sched_setparam() 設定程式的實時優先順序
sched_getparam() 獲取程式的實時優先順序
sched_get_priority_max() 獲取實時優先順序的最大值,由於使用者許可權的問題
sched_get_priority_min() 獲取實時優先順序的最小值,理由與上面類似
sched_rr_get_interval() 獲取程式的時間片
sched_setaffinity() 設定程式的處理親和力,其實就是儲存在task_struct中的cpu_allowed這個掩碼標誌。該掩碼的每一位對應一個系統中可用的處理器,預設所有位都被設定,即該程式可以再系統中所有處理器上執行。使用者可以通過此函式設定不同的掩碼,使得程式只能在系統中某一個或某幾個處理器上執行。
sched_getaffinity() 獲取程式的處理親和力
sched_yield() 暫時讓出處理器

3 android中的程式基礎

3.1 程式

預設情況下,Android為每個應用程式建立一個單獨的程式,所有元件執行在該程式中,這個預設程式的名字通常與該應用程式的包名相同。比如

那麼該程式預設的程式名為com.lt.mytest設定該屬性可以使得本應用程式與其它應用程式共享相同的程式。

注意: 標籤不支援android:process屬性

但是,如果我們想要控制讓某個特定的元件屬於某個程式,我們可以在manifest檔案中進行配置。在每種元件元素(activity、service、receiver、provider)的manifest條目中,都支援一個android:process的屬性,通過這個屬性,我們可以指定某個元件執行的程式。我們可以通過設定這個屬性,讓每個元件執行在它自己的程式中,也可以只讓某些元件共享一個程式。我們要可以通過設定android:process屬性,讓不同應用程式中的元件執行在相同的程式中,這些應用程式共享相同的Linux使用者ID,擁有相同的證書。

元素也有一個android:process屬性,可以設定一個應用於全部元件的預設值。 當可用記憶體數量低,而一些與使用者即時互動的程式又需要記憶體時,Android隨時可能會終止某個程式。執行在被終止的程式中的元件會因此被銷燬,但是,當再次需要這些元件工作時,就會再啟動一個程式。

在決定要終止哪個程式時,Android系統會權衡它們對於使用者的重要性。例如,相較於執行可見activities的程式,終止一個執行不可見activities的程式會更加合理。是否終止一個程式,依賴於執行在這個程式中的元件的狀態。

3.2程式生命週期

Android系統會盡可能讓一個應用程式程式執行更長的時間,但是它也需要移除舊的程式,為那些新建立的程式或者相比起來更加重要的程式釋放記憶體空間。要決定哪個程式保留,哪個程式終止,系統會將每個程式放置到“importance hierarchy”中,“importance hierarchy”是基於執行在程式中的元件以及這些元件的狀態的。擁有最低重要性的程式會首先被幹掉,然後就是那些次低重要性的程式,依次類推。在“importance hierarchy”中,共有五個等級。下面的列表中,按照重要性列出了五種不同型別的程式:

1、 前臺程式(Foreground process)
2、 可見程式(Visible process)
3、 服務程式(Service process)
4、 後臺程式(Background process)
5、 空程式(Empty process)

元素 Android:process屬性定義了執行Activity所在程式的名稱。通常,一個應用程式的所有元件執行在應用程式建立的預設的程式。它具有與應用程式包相同的名稱。元素的 android:process屬性可以為所有元件設定不同的預設程式名稱。但是,每個元件都可以覆蓋預設設定,讓應用程式跨多個程式。

如果分配給此屬性的名稱以一個冒號(‘:’)開頭,發將建立一個新的屬於應用程式的私有的程式,在這一程式中執行。如果程式的名稱由小寫字母開始,活動將在該名稱的全域性程式中執行,只要它有這樣做的許可權。這樣做將使在不同的應用程式中的元件共享一個程式,減少資源的使用。

與其它應用程式共享的一個Linux User Id的名字。

預設情況下,Android為每個應用程式分配一個唯一的User Id。然而,如果有多個應用程式都將該屬性設定為一個相同的值,那麼它們將共享相同的Id。如果這些應用程式再被設定成執行在一個相同的程式,它們便可以彼此訪問對方的資料。

3.3 android中的多程式使用需要注意的問題

原文請參考官方文件連結,下面內容由博主進行翻譯。

如果你的app有需要的話,將你的app中使用app會在降低記憶體消耗。但是大多數的app都是不需要多程式的,因為如果方法不當的話,反而會增加記憶體消耗。(一個可以使用多程式的app情況是,比如你需要在後臺和前臺都需要做大量的工作並且需要分別管理)
比如說音樂播放器,我們需要在後臺利用service進行長時間的音樂播放。假如我們將整個app都放在一個程式中的話,那麼即使我們在操作其他app,後臺音樂播放的時候,關於activity UI介面的許多記憶體分配以及控制音樂播放的service都會被儲存。這種情況,我們就可以使用兩個程式:一個專門針對UI介面,一個專門針對後臺音樂service的播放。
這篇文章會對你有幫助,Android 後臺任務型App多程式架構演化
所以針對service,我們就可以指定android:process為一個字串(可以為任意名字)

當然這裡使用冒號(‘:’) 開頭,這在上面的文章中也提到了,這保證了當前程式是app私有的。

注意點:
1 如果你需要將你的app劃分為多程式,那麼只能讓一個程式負責UI處理,其他程式應當避免UI處理,否則你的記憶體會急速上升,一旦UI繪製之後,想降低記憶體消耗也會是一個難題。
2 當在android中使用多程式的時候,應當保持程式碼的精簡。應為對於共同的實現操作現在會在不同的程式裡造成多餘的系統開銷。假如你使用enums(雖然你不應該使用enums),那麼記憶體會需要在不同的程式裡建立和初始化這些變數。關於adapters的任何抽象以及臨時變數都會造成重複的開銷。
3 關於多程式的另外一個關注點就是其中存在的一些共同的依賴關係,比如說你的app有一個content provider 執行在預設的程式中(包含UI的程式),那麼後臺程式使用content provider ,那麼content provider也會需要你的記憶體中有UI程式。這時候,如果你的目標是後臺程式獨立於繁重的前臺程式,那麼它肯定也就不能使用UI程式中content provider 或者service那些了。

 

3.4 關於我們從最近任務列表中清除app的問題

 1833901-a2bbaa6305fcedf0

 

通過3.3, 我們也會聯想到平常使用音樂軟體(比如音樂),當我們選擇退出應用的時候,音樂都會在後臺播放,當時當我們從任務列表中清除音樂軟體的時候,音樂就會停止了,那麼當我們從任務列表中清除app,到底發生了什麼?直接看看stackexchange這個回答吧 what-actually-happens-when-you-swipe-an-app-out-of-the-recent-apps-list

簡單來說,這和多次按返回鍵退出應用一樣,系統會殺掉後臺程式,但優勢也不是這樣。
從最近任務中移除一個條目會移除這個app存在的後臺程式。但是它並不會直接結束service,當他們在任務列表中被清除的時候,其實他們自己有相應的api(onTaskRemoved被呼叫)處理service是否應當被結束。也就是說,你使用的e-mail接收的app即使你在任務列表中把它清除了,它的service也會接收e-mail資訊。
當然如果你想要完全停止一個app,你可以通過設定->應用管理 ->進入應用資訊頁面,點選強制退出。強制退出會讓該app的所有程式被殺掉,所有的service停止,所有的通知被移除,所有的提醒被關閉等。該app除了被再次呼叫的情況下,不會再被啟動。
也就是說,是由app來決定在任務列表清楚的時候,後臺程式是否被殺掉。

這也就解釋了為什麼我們在最近任務列表中清除了支付寶,但是支付寶卻還在我們的後臺執行程式裡面了。如果我們直接在應用資訊介面強行停止了,這時候,支付寶就完全退出了。

 1833901-738c907eb0ffee0a

 

3.5 android程式保活

關於 Android 程式保活,你所需要知道的一切
Android App 不死之路
Android 後臺任務型App多程式架構演化

4 參考文章

http://developer.android.com/guide/topics/manifest/activity-element.html

https://developer.android.com/training/articles/memory.html#MultipleProcesses

http://android.stackexchange.com/questions/19987/what-actually-happens-when-you-swipe-an-app-out-of-the-recent-apps-list#_=_

《Linux核心設計與實現》讀書筆記(三)- Linux的程式

《Linux核心設計與實現》讀書筆記(四)- 程式的排程

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

【進階】從 linux 到 android,程式的方方面面 【進階】從 linux 到 android,程式的方方面面

相關文章