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表中增加一項。
#p#
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) 調度步驟
- 清理當前運行中的進程
- 選擇下一個要運行的進程
- 設置新進程的運行環境
- 進程上下文切換
#p#
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結構體釋放掉。否則子進程會變成僵尸進程。