Linux下多任務介紹
首先,先簡單的介紹一下什麼叫多任務系統?任務、進程、線程分別是什麼?它們之間的區別是什麼?,從而可以宏觀的了解一下這三者,然後再針對每一個仔細的講解。
什麼叫多任務系統?多任務系統指可以同一時間內運行多個應用程序,每個應用程序被稱作一個任務。
任務定義:任務是一個邏輯概念,指由一個軟件完成的任務,或者是一系列共同達到某一目的的操作。
進程定義:進程是指一個具有獨立功能的程序在某個數據集上的一次動態執行過程,它是系統進行資源分配和調度的最小單元。
線程定義:線程是進程內獨立的一條運行路線,是處理器調度的最小單元,也可以成為輕量級進程。
看了定義,有點暈,還是通俗的說一下它們的區別吧。①通常一個任務是一個程序的一次執行,一個任務包含一個或多個完成獨立功能的子任務,這個獨立的子任務就是進程或線程。②一個進程可以擁有多個線程,每個線程必須有一個父進程。
任務
任務是一個邏輯概念,指由一個軟件完成的任務,或者是一系列共同達到某一目的的操作。通常一個任務是一個程序的一次執行,一個任務包含一個或多個完成獨立功能的子任務,這個獨立的子任務就是進程或線程。例如,一個殺毒軟件的一次運行是一個任務,目的是從各種病毒的侵害中保護計算機系統,這個任務包含多個獨立功能的子任務(進程或線程),包括實時監控功能、定時查殺功能、防火牆功能及用戶交互功能等。任務、進程和線程之間的關系如圖1所示
進程
進程的基本概念
進程是指一個具有獨立功能的程序在某個數據集上的一次動態執行過程,它是系統進行資源分配和調度的基本單元。一次任務的運行可以並發激活多個進程,這些進程相互合作來完成該任務的一個最終目標。
進程具有並發性、動態性、交互性、獨立性和異步性等主要特性。
進程和程序是有本質區別的:程序是靜態的一段代碼,是一些保存在非易失性存儲器的指令的有序集合,沒有任何執行的概念;而進程是一個動態的概念,它是程序執行的過程,包括動態創建、調度和消亡的整個過程,它是程序執行和資源管理的最小單位。
Linux下的進程結構
進程不但包括程序的指令和數據,而且包括程序計數器和處理器的所有寄存器及存儲臨時數據的進程堆棧,因此,正在執行的進程包括處理器當前的一切活動。
因為linux是一個多任務多進程的操作系統,所以其他的進程必須等到系統將處理器使用權分配給自己之後才能運行。當正在運行的進程等待其他的系統資源時,linux內核將取得處理器的控制權,並將處理器分配給其他正在等待的進程,他按照內核中的調度算法決定將處理器分配給哪一個進程,也就是說,內核不會讓處理器閒著。
內核將所有進程存放在雙向循環鏈表(進程鏈表)中,其中鏈表的頭是 init_task 描述符。鏈表的每一項都是類型為 task_struct,稱為進程描述符的結構,該結構包含了一個進程相關的所有信息,定義在<include/linux/sched.h>文件中。task_struct內核結構比較大,它能完整的描述一個進程,如進程的狀態、進程的基本信息、進程標識符、內存相關信息、父進程相關信息、與進程相關的終端信息、當前工作目錄、打開的文件信息、所接收的信號信息等。
下面詳細講解task_struct結構中最為重要的兩個域:state(進程狀態)和pid(進程標識符)。如果想具體了解task_struct,請點這裡。
(1)進程狀態
Linux中的進程有以下幾種狀態。
● 運行狀態(TASK_RUNNING):進程當前正在運行,或者正在運行隊列中等待調度。
● 可中斷的阻塞狀態(TASK_INTERRUPTIBLE):進程處於阻塞(睡眠)狀態,正在等待某些事件發生或能夠占用某些資源。處在這種狀態下的進程可以被信號中斷。接收到信號或被顯式的喚醒呼叫(如調用 wake_up 系列宏:wake_up、wake_up_interruptible等)喚醒之後,進程將轉變為 TASK_RUNNING 狀態。
● 不可中斷的阻塞狀態(TASK_UNINTERRUPTIBLE):此進程狀態類似於可中斷的阻塞狀態(TASK_INTERRUPTIBLE),只是它不會處理信號,把信號傳遞到這種狀態下的進程不能改變它的狀態。在一些特定的情況下(進程必須等待,直到某些不能被中斷的事件發生),這種狀態是很有用的。只有在它所等待的事件發生時,進程才被顯示的喚醒呼叫喚醒。
● 可終止的阻塞狀態(TASK_KILLABLE):該狀態的運行機制類似於TASK_UNINTERRUPTIBLE,只不過處在該狀態下的進程可以響應致命信號。它可以替代有效但可能無法終止的不可中斷的阻塞狀態(TASK_UNINTERRUPTIBLE),以及易於喚醒但安全性欠佳的可中斷的阻塞狀態TASK_INTERRUPTIBLE)。
● 暫停狀態(TASK_STOPPED):進程的執行被暫停,當進程收到 SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信號時,就會進入暫停狀態。
● 跟蹤狀態(TASK_TRACED):進程的執行被調試器暫停。當一個進程被另一個監控時(如調試器使用ptrace()系統調用監控測試程序),任何信號都可以把這個進程置於跟蹤狀態。
● 僵屍狀態(EXIT_ZOMBIE):進程運行結束,父進程尚未使用 wait 函數族(如調用 waitpid()函數)等系統調用來“收屍”,即等待父進程銷毀它。處在該狀態下的進程“屍體”已經放棄了幾乎所有的內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的推出狀態等信息供其他進程收集。
● 僵屍撤銷狀態(EXIT_DEAD):這是最終狀態,父進程調用 wait 函數族“收屍”後,進程徹底由系統刪除。
它們之間的轉換關系如圖2所示:
進程可以使用 set_task_state 和 set_current_state 宏來改變指定進程的狀態信息和當前進程的狀態。
(2)進程標識符
Linux內核通過唯一的進程標識符 PID 來標識每個進程(就和文件描述符一樣)。PID存放在進程描述符的 pid 字段中,新創建的 PID 通常是前一個進程的 PID 加1,不過PID的值有上限(最大值=PID_MAX_DEFAULT-1,通常為32767),讀者可以查看/proc/sys/kernel/pid_max 來確定該系統的進程數上限。
當系統啟動後,內核通常作為某一個進程的代表。一個指向task_struct的宏current用來記錄正在運行的進程。current經常作為進程描述符結構指針的形式出現在內核代碼中,例如,current->pid 表示處理器正在執行的進程的PID。當系統需要查看所有的進程時,則調用for_each_process()宏,這將比系統搜索數組的速度要快的多。
在Linux中獲得當前進程號的(PID)和父進程號(PPID)的系統調用函數分別為 getpid() 和 getppid()。
進程的創建、執行、終止
(1)進程的創建和執行
咱們首先得知道啥是創建,啥是執行哈!我剛開始看的時候沒懂。創建進程就是產生一個新的進程,這個大家都知道。而進程的執行,前邊講進程的的定義的時候,就說了正在運行的子任務,說白了,進程執行也就是讓產生的這個進程干點什麼事,別占著那啥不拉那啥。
許多操作系統提供的都是產生進程的機制,也就是說,首先在新的地址空間裡創建進程、讀入可執行文件,最後再開始執行。Linux 中進程的創建很特別,它把上述的步驟分解到兩個單獨的函數中去執行:fork()函數和exec函數族。首先,fork()函數通過復制當前進程創建一個子進程(注意此時資源還沒有被復制過來,去了解一下寫時復制頁技術吧),子進程於父進程的區別僅僅在於不同的PID、PPID和某些資源及統計量。exec函數族負責讀取可執行文件並將其載入地址空間開始運行。
(2)進程的終止
進程終結也需要很多繁瑣的工作,系統必須保證回收進程所占用的資源,並通知父進程。Linux首先把終止的進程設置為僵屍狀態,這時,進程無法投入運行,它的存在只為父進程提供信息,申請死亡。父進程得到信息後,開始調用 wait 函數族,最後終止子進程,子進程占用的所有資源被全部釋放。
進程的內存結構
Linux操作系統采用虛擬內存管理技術,使得每個進程都有各自互不干涉的進程地址空間。該地址空間是大小為 4GB的線性虛擬空間,用戶所看到和接觸到的都是該虛擬地址,無法看到實際的物理內存地址。利用這種虛擬地址不但能起到保護操作系統的效果(用戶不能直接訪問物理地址),而且,更重要的是,用戶程序可以使用比實際物理內存更大的地址空間。
4GB的進程地址空間會被分成兩個部分:用戶空間與內核空間。用戶地址空間是從0到3GB(0xC000 0000),內核地址空間占據3GB到4GB。用戶進程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內核空間的虛擬地址。只有用戶進程使用系統調用(代表用戶進程在內核態執行)時可以訪問到內核空間。每當進程切換時,用戶空間就跟著變化;而內核空間由內核負責映射,它不會跟著進程改變,是固定的。內核空間地址有自己對應的頁表,用戶進程各自有不同的頁表。每個進程的用戶空間都是完全獨立、互不相干的。進程的虛擬內存空間如圖3所示,其中用戶空間包括以下幾個功能區域:
● 只讀段: 包含程序代碼(.init和.text)和只讀數據(.rodata)。
● 數據段: 存放的是全局變量和靜態變量。其中可讀可寫數據段(.data)存放已初始化的全局變量和靜態變量,BSS數據段(.bss)存放未初始化的全局變量和靜態變量。
● 堆: 由系統自動分配釋放,存放函數的參數值、局部變量的值、返回地址等。
● 堆棧: 存放動態分配的數據,一般由程序員動態分配和��放。若程序員不釋放,程序結束時可能由操作系統回收。
● 共享庫的內存映射區域: 這是Linux動態鏈接器和其他共享代碼庫代碼的映射區域。
由於在Linux系統中每一個進程都會有/proc文件系統下與之對應的一個目錄(如將init進程的相關信息在/proc/1 目錄下的文件中描述),因此通過 proc 文件系統可以查看某個進程的地址空間的映射情況。例如,運行一個應用程序,如果它的進程號為13703,則輸入“ cat /proc/13703/maps”命令,可以查看該進程的內存映射情況。
線程
前面已經講到,進程是系統中程序執行和資源分配的基本單位。每個進程都擁有自己的數據段、代碼段和堆棧段,這就造成了進程在進行切換等動作時需要較復雜的上下文切換等動作。為了進一步減少處理機的空閒時間,支持多處理器及減少上下文切換開銷,進程在演化中出現了另一個概念---線程。它是進程內獨立的一條運行路線,是處理器調度的最小單元,也可以稱為輕量級線程。線程可以對進程的內存空間和資源分配進行訪問,並與同一進程中的其他線程共享。因此,線程的上下文切換的開銷比創建進程小得多。
一個進程可以擁有多個線程,每個線程必須有一個父進程。線程不擁有系統資源,它只具有運行時所必須的一些數據結構,如堆棧/寄存器與線程控制塊(TCB),線程與其父進程的其他進程共享該進程所擁有的全部資源。要注意的是,由於線程共享了進程的資源和地址空間,因此,任何線程對系統資源的操作都會給其他進程帶來影響。由此可知,多線程中的同步是一個非常重要的問題。在多線程系統中,進程與線程的關系如圖4所示
在Linux系統中,線程分為3種:①用戶線程 ②輕量級線程 ③內核線程