1. 程式人生 > >【進階】從linux到android,程序的方方面面

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

最近在閱讀《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)


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

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

1.2 程序的生命週期

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

程序有五種程序狀態:
除了圖片上面的三種還有,_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應該分配10*5+10=60ms,以此類推,ProcessC分配20*5+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為每個應用程式建立一個單獨的程序,所有元件執行在該程序中,這個預設程序的名字通常與該應用程式的包名相同。比如

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
       package="com.lt.mytest" >

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

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

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

<application>元素也有一個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)

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

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

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

<manifest xmlns:android="http://schemas.android.com/apk/res/android"   
 android:sharedUserId="android.uid.phone" >

預設情況下,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為一個字串(可以為任意名字)

<service android:name=".PlaybackService"    
     android:process=":background" />

當然這裡使用冒號(‘:’) 開頭,這在上面的文章中也提到了,這保證了當前程序是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的問題


通過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來決定在任務列表清楚的時候,後臺程序是否被殺掉。

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

3.5 android程序保活

4 參考文章