標籤:
Linux進程管理知識整理
1、進程有哪些狀態?什麼是進程的可中斷等待狀態?進程退出後為什麼要等待調度器刪除其task_struct結構?進程的退出狀態有哪些?
TASK_RUNNING(可運行狀態)
TASK_INTERRUPTIBLE(可中斷等待狀態)
TASK_UNINTERRUPTIBLE(不可中斷等待狀態)
TASK_STOPPED(進程被其它進程設定為暫停狀態)
TASK_TRACED(進程被調試器設定為暫停狀態)
TASK_DEAD(退出狀態)
進程由於所需資源得不到滿足,從而進入等待隊列,但是該狀態能夠被訊號中斷。比如當一個正在啟動並執行進程因進行磁碟I/O操作而進入可中斷等待狀態 時,在I/O操作完成之前,使用者可以向該進程發送SIGKILL,從而使該進程提前結束等待狀態,進入可運行態,以便響應SIGKILL,執行進程退出代 碼,從而結束該進程。
當進程退出時(例如調用exit或者從main函數返回),需要向父進程發送訊號,父進程進行訊號處理時,需要擷取子進程的資訊,因此這時不能刪除 子進程的task_struct。另外每個進程都有一個核心態的堆棧,當進程調用exit()時,在切換到另外一個進程前,總是要使用核心態堆棧,因此當 進程調用exit()時,完成必要的處理後,就把state設定為TASK_DEAD,並切換到其他進程。當順利地切換到其他進程後,由於該進程的狀態設 置為TASK_DEAD,因此這個進程不會被調度,之後當調度器檢查到狀態為TASK_DEAD的進程時,就會刪除這個進程的task_struct結 構,這樣這個進程就徹底的消失了。
EXIT_ZOMBIE(僵死進程):父進程等待子進程結束時發送的SIGCHLD訊號(預設情況下,建立進程都會設定在進程退出的時候向父進程發送訊號的標誌,除非建立的是輕權進程),此時子進程已退出,並且SIGCHLD訊號已經發送,但是父進程還沒有被調度運行;EXIT_DEAD(僵死撤銷狀態):父進程對子進程的退出訊號“沒興趣”,或者在子進程退出時,父進程通過waitpid()調用等待子進程的SIGCHLD訊號。
2、殭屍進程
1) 怎麼產生殭屍進程
一個進程在調用exit命令結束自己的時候,其實它並沒有真正的被銷毀,只是進程不能被調度並處於EXIT_ZOMBIE狀態,它佔用的所有記憶體就 是核心棧、thread_info結構和task_struct結構。此時進程存在的唯一目的就是向它的父進程提供資訊,如果它的父進程沒有調用wait 或waitpid等待子進程結束,又沒有顯示地忽略該訊號,那麼它就一直保持EXIT_ZOMBIE狀態。
2) 怎麼查看殭屍進程
利用命令ps,看到有標記為Z的進程就是殭屍進程。
3) 怎麼清理殭屍進程
- 父進程可以調用waitpid、wait函數來等待子進程結束
- 把父進程殺掉,父進程死後,殭屍進程成為“孤兒進程”,過繼給init進程,init進程始終負責清理殭屍進程,它產生的所有殭屍進程也跟著消失。
3、PID管理
在Linux系統中用pid結構體來標識一個進程,通過pidmap位元影像來管理所有的進程號(即pid:與前面的pid結構體不是同一個意思),目 的就是要更快的找到目標進程。用pid結構體來表示進程的優點:比直接用數字pid_t更容易管理(進程退出時pid回收再分配效率高),比直接用 task_struct標識進程佔用空間小。
pid結構體如下所示:
- struct pid
- {
- atomic_t count;
- int nr; /*存放pid數值*/
- struct hlist_node pid_chain; /*把該pid鏈到雜湊表中*/
- struct hlist_head tasks[PIDTYPE_MAX];
- struct rcu_head rcu;
- };
因為對於32位系統來說,預設最大的pid為32768,由於pidmap位元影像中每一位表示這個pid是否可用,共需要32768位,正好一個物理頁的大小(4*1024*8)。
pidmap結構體如下所示:
- struct pidmap {
- /*
- *這個變數用來統計這個結構體對應的一頁實體記憶體中有多少個位
- *是0的,即空閑pid的數量
- */
- atomic_t nr_free;
- void *page; /*這個就是指向存放這個位元影像的記憶體頁的指標*/
- };
下面首先來看Linux核心啟動之初在start_kernel函數中對pidmap位元影像的初始化函數pidmap_init如下所示:
- void __init pidmap_init(void)
- {
- /*申請一頁實體記憶體,並初始化為0*/
- init_pid_ns.pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL);
- /*將第0位設定為1,表示當前進程使用pid為0,即現在就是0號進程*/
- set_bit(0, init_pid_ns.pidmap[0].page);
- /*同時更新nr_free統計空閑pid的值*/
- atomic_dec(&init_pid_ns.pidmap[0].nr_free);
- pid_cachep = KMEM_CACHE(pid, SLAB_PANIC);
- }
再來看Linux核心啟動之初在start_kernel函數中對pid hash表的初始化函數pidhash_init如下所示:
- void __init pidhash_init(void)
- {
- int i, pidhash_size;
- /*
- *nr_kernel_pages表示核心記憶體總頁數,就是系統DMA和NORMAL內
- *存頁區的實際實體記憶體總頁數
- *megabytes:統計的是核心記憶體有多少MB
- */
- unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT);
- /*從下面兩行代碼可以看出pidhash_shift是在4~12之間的*/
- pidhash_shift = max(4, fls(megabytes * 4));
- pidhash_shift = min(12, pidhash_shift);
- pidhash_size = 1 << pidhash_shift;
- printk("PID hash table entries: %d (order: %d, %Zd bytes)\n",
- pidhash_size, pidhash_shift,
- pidhash_size * sizeof(struct hlist_head));
- /*
- *由alloc_bootmem可知pid_hash是在低端實體記憶體申請的,由於
- *pidhash_init函數是在mem_init函數執行之前被調用的,所以這裡申請
- *的記憶體是不會被回收的
- */
- pid_hash = alloc_bootmem(pidhash_size * sizeof(*(pid_hash)));
- if (!pid_hash)
- panic("Could not alloc pidhash!\n");
- for (i = 0; i < pidhash_size; i++)
- /*初始化每個表的每個表項的鏈表*/
- INIT_HLIST_HEAD(&pid_hash[i]);
- }
總結:核心維護兩個資料結構來維護進程號pid,一個是雜湊表pid_hash,還有一個位元影像pidmap。在 do_fork()中每調用一次alloc_pid(),首先會通過調用alloc_pidmap()修改相應的位元影像,該函數的主要思想是:last記錄 上次分配的pid,此次分配的pid為last+1,如果pid超出最大值,那麼就迴圈回到最初值(RESERVED_PIDS),然後測試pidmap 上該pid所對應的bit是否為0,直到找到為止。其次通過hlist_add_head_rcu函數在pid_hash表中增加一項。
4、進程的堆棧
一個進程有兩個堆棧:使用者態堆棧和核心態堆棧。使用者態堆棧的空間指向使用者地址空間,核心態堆棧的空間指向核心地址空間。
當進程由於中斷或系統調用從使用者態(進程在執行使用者自己的代碼)轉換到核心態(進程在執行核心代碼)時,進程所使用的棧也要從使用者棧切換到核心棧。
使用者棧向核心棧的切換:進入核心態後,首先把使用者態的堆棧地址儲存在核心堆棧中,然後設定堆棧指標寄存器的地址為核心棧地址。
核心棧向使用者棧的切換:把儲存在核心棧中的使用者棧地址恢複到堆棧指標寄存器即可。
5、Linux下進程與線程的區別
1)進程是資源分派的基本單位,線程是CPU調度的基本單位
2)進程有獨立的地址空間,線程有自己的堆棧和局部變數,但是沒有獨立的地址空間(同一個進程內的線程共用進程的地址空間)
6、寫時拷貝機制(copy on write)
為了節約實體記憶體,在調用fork()產生新進程時,新進程與原進程會共用同一實體記憶體區(調用clone()建立線程,還會共用虛擬位址空間),只有當其中一進程進行寫操作時,系統才會為其另外分配實體記憶體頁面,這就是寫時拷貝機制。
詳細解釋如下:當進程A使用系統調用fork()建立一個子進程B時,由於子進程B實際上是父進程A的一個拷 貝,因此會擁有與父進程相同的物理頁面。為了節約記憶體和加快建立速度的目標,fork()函數會讓子進程B以唯讀方式共用父進程A的物理頁面,同時將父進 程A對這些物理頁面的存取權限也設為唯讀,這樣,當父進程A或子進程B任何一方對這些已共用的物理頁面執行寫操作時,都會產生頁面出錯異常中斷,此時 CPU會執行系統提供的異常處理函數do_wp_page()來解決這個異常,do_wp_page()會對這塊導致寫入異常中斷的物理頁面不共用操 作,為寫進程複製一份新的物理頁面。最後,從異常處理函數返回時,CPU就會重新執行剛才導致異常的寫入操作指令,使進程繼續執行下去。
7、0號進程的建立
核心啟動時“手工”建立了0號進程,即swapper進程,這是一個核心態進程,它的頁表swapper_pg_dir和核心態堆棧是在核心啟動建立的,這個進程定義如下:
- struct task_struct init_task = INIT_TASK(init_task);
init_task的各種進程資來源物件都是通過INIT_xxx進程初始化的,在start_kernel()的最後由rest_init()函數 調用kernel_thread()函數,以swapper進程為“模板”建立了kernel_init核心進程,之後這個進程會建立init進程,執行 /sbin//-init檔案,從而把啟動過程傳遞到使用者態。而swapper進程則執行cpu_idle()函數讓出CPU,以後如果沒有任何就緒的進 程可調度執行,就會調度swapper進程,執行cpu_idle()函數,這個函數將調用tick_nohz_stop_sched_tick()進入 tickless狀態。
8、進程的切換
1) 主動切換
- 當前進程主動進行可能引起阻塞的I/O操作,此時當前進程被設定為等待狀態,加入到相關資源的等待隊列,並調用schedule()函數讓出CPU。
- 進程主動通過exit系統調用退出。
2) 被動切換
- 時間片到期
- I/O中斷喚醒了某個I/O等待隊列中的更高優先順序的進程
由於這兩種情況通常發生在時鐘中斷或者其他I/O中斷處理函數中,而中斷上下文環境下不能阻塞進程,所以通常中斷處理常式中通過設定need_resched標誌請求調度,這個調度被延遲到中斷返回處理。
9、Linux系統的處理序間通訊的方式
管道(pipe):管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的進程間使用(進程的親緣關係通常是指父子進程關係)。
具名管道(named pipe):具名管道也是半雙工的通訊方式,但是它允許無親緣關係進程間的通訊。
訊號量(semophore):訊號量是一個計數器,可以用來控制多個進程對共用資源的訪問。它常作為一種鎖機制,防止某進程正在訪問共用資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。
訊息佇列(message queue):訊息佇列就是一個訊息的鏈表,存放在核心中並由訊息佇列標識符標識。訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
訊號(sinal):訊號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。
共用記憶體(shared memory):共用記憶體就是映射一段能被其他進程所訪問的記憶體,這段共用記憶體由一個進程建立,但多個進程都可以訪問。共用記憶體是最快的IPC方式,它是針對其他處理序間通訊方式運行效率低而專門設計的。它往往與其他通訊機制,如訊號配合使用,來實現進程間的同步和通訊。
通訊端(socket):通訊端也是一種處理序間通訊機制,與其他通訊機制不同的是,它可用於不同主機間的進程通訊。
10、Linux進程調度機制
1) 什麼是調度
從就緒的進程中選出最適合的一個來執行
2) 學習調度需要掌握哪些知識點
3) 調度策略
SCHED_NORMAL:普通的進程
SCHED_FIFO:先入先出的即時進程
SCHED_RR:時間片輪轉的即時進程
4) 調度器類
分為CFS調度類和即時調度類。
- CFS調度類是針對普通進程的,採用的方法是完全摒棄時間片而是分配給進程一個處理器使用比重。
- 即時調度類分為SCHED_FIFO和SCHED_RR。
SCHED_FIFO實現了一種簡單的、先入先出的調度演算法:它不使用時間片,可以一直執行下去,只有更高優先順序的SCHED_FIFO或者 SCHED_RR任務才能搶佔SCHED_FIFO任務。如果有兩個或者更多的同優先順序的SCHED_FIFO進程,它們會輪流執行,但是依然只有在它們 願意讓出處理器時才會退出。
SCHED_RR與SCHED_FIFO大體相同,只是SCHED_RR級的進程在耗盡事先分配給它的時間後就不能再繼續執行了。
5) 調度時機
在核心中直接調用schedule():當進程需要等待資源而暫時停止運行時,會把進程的狀態
設定為等待狀態,並主動請求調度,讓出CPU。
例:current->state=TASK_INTERRUPTIBLE;
schedule();
使用者搶佔:核心即將返回使用者空間的時候,如果need_resched標誌被設定,會導致schedule()被調用,此時就會發生使用者搶佔。
核心搶佔:只要重新調度是安全的,那麼核心就可以在任何時間搶佔正在執行的任務。
6) 調度步驟
- 清理當前運行中的進程
- 選擇下一個要啟動並執行進程
- 設定新進程的運行環境
- 進程環境切換
Linux進程管理之問題
1、為什麼調用fork()函數將返回兩次?
這是因為在do_fork->copy_process->copy_thread函數中,將子進程的使用者態堆棧的開始地址設定為父進 程的使用者態堆棧的開始地址,這樣當父子進程從核心態返回到使用者態的時候,返回的地址相同,這就解釋了為什麼fork一次卻返回兩次的原因。
2、為什麼要在task_struct中設定mm和active_mm兩個mm_struct成員呢?
這是由於核心線程沒有使用者態地址空間,所以它的mm設定為NULL,但是由於頁目錄的地址是儲存在mm結構中的,從其他進程切換到這個核心態線程 時,調度器可能需要切換頁表,為此增加了一個active_mm,對於mm為NULL的核心態線程,就借用其他進程的mm_struct,也就是說把它的 active_mm指向其他進程的mm結構,當進行進程切換時,統一使用active_mm就可以了。但是其他進程不是有自己獨立的頁表嗎?由於核心態線 程只使用核心地址空間,因此這不會有問題。
3、有如下說法:1.task_struct的mm成員用來描述3GB使用者態虛擬位址空間;2.核心線程可以借用上一個調用的使用者 進程的mm中的頁表來訪問核心地址空間。如果是這樣的話,那麼task_struct的mm成員能不能描述1GB的核心地址空間?如果不能的話,為什麼會 有2這種說法?
task_struct的mm成員不能描述1GB的核心地址空間,只是因為mm成員中儲存了頁目錄的資訊pgd_t,而且所有進程共用1G的核心態地址空間,所以可以使用上一個使用者進程的mm中的頁表訪問核心地址空間。(什麼意思?)
4、為什麼所有進程共用1G的核心態地址空間?
因為fork()會複製當前進程的task_struct結構,同時會為新進程複製mm結構。此時當前進程的3GB~4GB的核心態虛擬位址對應的 頁表項(頁目錄項)被複製到進程的頁表項(頁目錄項)中,所以說所有進程共用1G核心態地址空間。但是對於使用者態虛擬位址地區,則把它的進程頁表項(頁目 錄項)設定為唯讀,這樣當其中一個進程對其進行寫入操作時,do_page_fault()會分配新的物理頁面,並建立映射,從而實現COW機制。
5、父進程要求子進程退出時發送訊號,那麼父進程要求子線程退出時發送訊號嗎?為什嗎?
父進程不要求子線程退出時發送訊號,這是因為子線程共用父進程的一些資源,所以不需要父進程來擷取這些資訊,也就不需要向父進程發送訊號。這一點可以在do_fork->copy_process
p->exit_signal=(clone_flags & CLONE_THREAD) ? -1 :(clone_flags &CSIGNAL);
以及do_exit->exit_notify
if (tsk->exit_signal != -1 && thread_group_empty(tsk)) {
int signal = tsk->parent == tsk->real_parent ? tsk->exit_signal : SIGCHLD;
do_notify_parent(tsk, signal);
} else if (tsk->ptrace) {
do_notify_parent(tsk, SIGCHLD);
}
中看出來。
6、 為什麼子進程退出時,如果父進程沒有調用wait等待子進程結束,則子進程會變成殭屍進程?
分析如下:在核心源碼中有如下的代碼:
do_exit->exit_notify->
state = EXIT_ZOMBIE
if (tsk->exit_signal == -1 &&
(likely(tsk->ptrace == 0) ||
unlikely(tsk->parent->signal->flags & SIGNAL_GROUP_EXIT)))
state = EXIT_DEAD;
tsk->exit_state = state;
說明如果定義了子進程退出時向父進程發送訊號,則設定進程狀態為EXIT_ZOMBIE,否則為EXIT_DEAD。而子進程退出時一定會向父進程發送
訊號,所以進程的狀態為EXIT_ZOMBIE,如果此時父進程調用wait等待子進程結束的話,由 do_wait->wait_task_zombie函數可以將進程的狀態設定為EXIT_DEAD,並且釋放進程的核心堆棧資源,最後由 put_task_struct將其task_struct結構體釋放掉。否則子進程會變成殭屍進程。
Linux進程管理知識整理