多任務是指用戶可以在同一時間內運行多個應用程序。Linux就是一種支持多任務的操作系統,它支持多進程、多線程等多任務處理和任務之間的多種通信機制。掌握多任務開發,會有助于大家成為系統工程師。
Linux下多任務機制的介紹
Linux就是一個支持多任務的操作系統,它比單任務系統的功能增強了許多。當多任務操作系統使用某種任務調度策略允許兩個或更多進程并發共享一個處理器時,事實上處理器在某一時刻只會給一個任務提供服務。由于任務調度機制保證了不同的任務之間的切換速度十分迅速,因此給人多個任務同時運行的錯覺。多任務系統中有3個功能單位:任務、進程和線程,下面分別進程介紹。
1、任務
任務是一個邏輯概念,指由一個軟件完成的活動,或者是一系列共同達到某一個目的的操作。通常一個任務是一個程序的一次運行,一個任務包含一個或多個完成獨立功能的子任務,這個獨立的子任務就是進程或線程。例如,一個殺毒軟件的一次運行是一個任務,目的是從各種病毒的侵害中保護計算機系統,這個任務包含多個獨立功能的子任務(進程或線程),包含實時監控功能、定時查殺功能、防火墻功能及用戶交互功能等。個人理解:就好比假設一個應用程序中由一個或多個可執行文件共同執行組成,那么此應用程序的一次執行就是一個任務,而這些可執行文件的執行就是一個進程的執行,而可執行文件是由一個線程或多個線程構成的,當只有一個線程構成了這個進程,則此時進程和線程就是一樣的概念(可執行文件的一次運行)。
2、進程
進程的基本概念
進程是一個具有獨立功能的程序在某個數據集上的一次動態執行過程,它是系統進行資源分配和調度的基本單位(個人理解:系統好比是一個大型的任務,由多個進程(可執行文件)構成,而資源分配和資源調度分別都是一個進程,所以進程是系統進行資源分配和調度的基本單位)。一次任務的運行可以并發激活多個進程,這些進程相互合作來完成該任務的一個最終的目標。
進程具有并發性、動態性、交互性、獨立性和異步性等主要特性
· 并發性:指的是系統中多個進程可以同時并發執行,相互之間不受干擾。
· 動態性:指的是進程都有完整的生命周期,而且在進程的生命周期內,進程的狀態是不斷變化的。另外,進程具有動態的地址空間(包括代碼、數據和進程控制塊)。
· 交互性:指的是進程在執行過程中可能會與其他進程發生直接和間接的交互操作,如進程同步和進程互斥等,需要為此添加一定的進程處理機制。
· 獨立性:指的是進程是一個相對完整的資源分配和調度的基本單位,各個進程的地址空間是相互獨立的,只有采用某些特定的通信機制才能實現進程間的通信。
· 異步性:指的是每個進程都按照各自獨立的、不可預知的速度向前執行。
進程和程序是有本質的區別:程序是靜態的一段代碼,是一些保存在非易失性存儲器的指令的有序集合,沒有任何執行的概念;而進程是一個動態的概念,它是程序執行的過程,包括動態創建、調度和消亡的整個過程,它是程序執行和資源管理的最小單位。
Linux系統中包括以下幾種類型的過程:
· 交互式過程:這類進程進程與用戶進程交互,因此要花很多時間等待用戶的交互操作(鍵盤和鼠標操作等)。當接收到用戶的交互操作后,這類進程應該很快被允許,而且相應時間的變化也應該很小,否則用戶就會覺得系統反應遲鈍或者不太穩定。典型的交互式進程有shell命令進程、文本編輯器和圖形應用程序運行等。
· 批處理進程:這類進程不必與用戶進行交互,因此進程在后臺運行。因為這類進程通常不必很快地相應,因此往往受到調度器的“慢待”。典型的批處理進程有編譯器的編譯操作、數據庫搜索引擎等。
· 實時進程:這類進程通常對調度響應時間有很高的要求,一般不會被低優先級的進程阻塞。它們不僅要求很短的響應時間,而且更重要的是響應時間的變化應該很小。典型的實時進程有視頻和音頻的應用程序、實時數據采集系統程序等。
Linux下的進程結構
進程不但包括程序的指令和數據,而且包括程序計數器和處理器的所有寄存器以及存儲臨時數據的進程堆棧,因此正在執行的進程包括處理器當前的一切活動。
因為Linux是一個多進程的操作系統,所以其他的進程必須等到系統將處理器使用權分配各自己之后才能運行。當正在運行的進程等待其他的系統資源時,Linux內核將取得處理器的控制權,并將處理器分配給其他正在等待的進程,它按照內核中的調度算法決定處理器分配給哪個進程。
內核將所有進程存放在雙向循環鏈表(進程鏈表)中,其中鏈表的頭是init_task描述符。鏈表的每一項都是類型為task_struct,稱為進程描述符的結構,該結構包含了與一個進程相關的所有信息,定義在
下面詳細講解task_struct結構中最為重要的兩個域:state(進程狀態)和pid(進程標識符,即進程號)。
1)進程狀態,Linux中的進程有以下幾種狀態
· 運行狀態(TASK_RUNNING):進程當前正在運行,或者正在運行隊列中等待調度。
創建一個task.c文件,task.c文件內容如下:
保存后,輸入gcc task.c -o task編譯生成二進制代碼task,輸入./task運行task進程
打開另一個終端,輸入ps -aux查看進程狀態:(ps -axjf 可查看進程有哪些子進程,ps -e 也 可以查到進程的狀態,但只顯示進程的PID、TTY、TIME和CMD)
ps工具標識進程的5中狀態碼:
D 不可中斷 uninterruptible sleep (usually IO)
R 運行 runnable (on run queue)
S 中斷 sleeping
T 停止 traced or stopped
Z 僵尸 a defunct ("zombie") process
注:其它狀態還包括W(無駐留頁),<(高優先級進程),N(低優先級進程),L(內存鎖頁)
每列對應關系:
USER:進程所有者
PID:進程ID
%CPU:占用CPU的使用率
%MEM:占用內存的使用率
VSZ:占用虛擬內存大小
RSS:占用內存大小
TTY:終端次要裝置號碼
STAT:進程狀態
START:進程啟動時間
TIME:進程消耗cup時間
COMMAND:命令的名稱和參數
· 可中斷的阻塞狀態(TASK_INTERRUPTIBLE):進程處于阻塞(睡眠)狀態,正在等待某些事件發生或能夠占用某些資源。處在這種狀態下的進程可以被信號中斷。接收到信號或被顯式的喚醒呼叫(如調用wake_up系列宏:wake_up、wake_up_interruptible等)喚醒之后,進程轉變為TASK_RUNNING狀態。
· 不可中斷的阻塞狀態(TASK_UNINTERRUPTIBLE):此進程狀態類似于可中斷的阻塞狀態(TASK_INTERRUPTILBE),只是它不會處理信號,把信號傳遞到這種狀態下的進程不能改變它的狀態。在一些特定的情況下(進程必須等待,直到某些不可被中斷的事件發生),這種狀態是很有用的。只有在它所等待的事件發生時,進程被顯式的喚醒呼叫喚醒。
· 可終止的阻塞狀態(TASK_KILLABLE):Linux內核2.6.25引入了一種新的進程狀態,名為TASK_KILLABLE。該狀態的運行機制類似于TASK_UNINTERRUPTILBE,只不過在該狀態下的進程可以響應致命信號。它可以替代有效但可能無法終止的不可中斷的阻塞狀態,以及易于喚醒安全性欠佳的可中斷的阻塞狀態。
· 暫停狀態(TASK_STOPPED):進程的執行被暫停,當進程收到SIGTOP、SIGTSTP、SIGTTIN、SIGTTOU等信號時,就會進入暫停狀態。
· 跟蹤狀態(TASK_TRACED):進程的執行被調試器暫停。當一個進程被另一個進程監控是(如調試器使用ptrace()系統調用監控測試程序),任何信號都可以把這個進程置于跟蹤狀態。
· 僵尸狀態(EXIT_ZOMBIE):進程運行結束,父進程尚未使用wait函數族(如使用waitpid()函數)等系統調用來“收尸”,即等待父進程銷毀它。處于該狀態下的進程“實體”已經放棄了幾乎所有的內存空間,沒有任何可執行代碼,也不能調度,僅僅在進程列表保留一個位置,記載該進程的退出狀態等信息供其他進程收集。
· 僵尸撤銷狀態(EXIT_DEAD):這是最終狀態,父進程調用wait函數族“收尸”后,進程徹底有系統刪除。
它們之間的轉換關系如圖所示:
內核可以使用set_task_state和set_current_state宏來改變指定進程的狀態和當前執行進程的狀態。
2)進程標識符
Linux內核通過唯一的進程標識符PID來標識每個進程。PID存放進程描述符的pid字段中,新創建的PID通常是前一個進程的PID加1,不過PID的值有上限(最大值 = PID_MAX_DEFAULT - 1,通常為32767),我們可以在終端輸入 vim /proc/sys/kernel/pid_max 來確定該系統的進程數上限。
當系統啟動后,內核通常作為一個進程的代表。一個指向task_struct的宏current用來記錄正在運行的進程。current經常作為進程描述符結構指針的形式出現在內核代碼中,例如,current->pid表示處理器正在執行進程的PID。當系統需要查看所有的進程時,則調用for_each_process()宏,這將比系統搜索數組的速度要快得多。
在Linux中獲得當前進程的進程號(PID)和父進程號(PPID)的系統調用函數分別為getpid()和getppid()。
測試代碼:
測試結果:
輸入 ps -axjf 命令查看所有進程與父進程
我們在次輸入ps -aux命令查看所有進程,可以得知父進程為bash
進程的創建、執行和終止
1)進程的創建和執行
許多操作系統提供的都是產生進程的機制,也就是說,首先在新的地址空間里創建進程、讀入可執行文件、最后在開始執行。Linux中進程的穿件很特別,它把上述步驟分解到兩個單獨的函數中去執行:fork()和exec函數族。首先fork()函數通過復制當前進程創建一個子進程,子進程與父進程的區別在于不同的PID、PPID和某些資源及統計量。exec函數族負責讀取可執行文件并將其載入地址空間開始運行。
要注意的是,Linux中的fork()函數使用的是寫時復制頁的技術,也就是內核在創建進程時,其資源并沒有被復制過來,資源的復制僅僅只有在需要寫入數據時才發生,在此之前只是以只讀的方式共享數據。寫時復制技術可以使Linux擁有快速執行的能力,因此這個優化是非常重要的。
2)進程的終止
進程終結也需要做很多繁瑣的收尾工作,系統必須保證回收進程所占的資源,并通知父進程。Linux首先把終止的進程設置為僵尸狀態,這時,進程無法投入運行,它的存在只為父進程提供信息,申請死亡。父進程得到信息后,開始調用wait函數族,最后終止子進程,子進程占用的所有資源被全部釋放。
進程的內存結構
Linux操作系統采用虛擬內存管理技術,使得每個進程都有各自互不干涉的進程地址空間。該地址空間是大小為4GB的線性虛擬空間(當然是指32位系統),用戶所看到和接觸到的都是該虛擬地址,無法看到實際的物理內存地址。利用這種虛擬地址不但能起到保護操作系統的效果(用戶不能直接訪問物理內存),而且更重要的是,用戶程序可以使用比實際物理內存更大的地址空間。
我們可以通過命令getconf LONG_BIT 來查詢當前自己的系統是多少位的?
我的安裝的UbuntKylin是64位的,即實際內存最大可能達到2^64 = 128GB。(2^10 = 1kb,2^30 = 1GB)。
4GB的進程地址空間會被分出兩部分:用戶空間與內核空間。用戶地址空間是從0~3GB(0xC0000000),內存地址空間占據3GB~4GB。用戶進程通常情況下只能訪問用戶控件的虛擬地址,不能訪問內核空間的虛擬地址。只有用戶進程使用系統調用(代表用戶進程在內核執行)時可以訪問內核空間的虛擬空間。每當進程切換時,用戶空間就會跟著變化;而內核空間有內核負責映射,它并不會跟著進程改變而改變,是固定的。內核空間地址有自己對應的頁表,用戶進程各自用不同的頁表。每個進程用戶空間都是完全獨立、互不相干的。進程的虛擬內存地址空間如圖所示:
其中用戶空間包括以下幾個功能區域:
· 只讀段:包含程序代碼(.init和.exit)和只讀數據(.rodata)
· 數據段:存放的是全局變量和靜態變量。其中可讀可寫數據段(.data)存放已經初始化的全局變量和靜態變量,BSS數據段(.bss)存放未初始化的全局變量和靜態變量
· 堆:由系統自動分配釋放,存放函數的參數值、局部變量的值、返回地址等
· 堆棧:存放動態分配的數據,一般由程序員動態分配和釋放。若程序員不釋放,程序結束時可能由操作系統回收。
· 共享庫的內存映射區域:這是Linux動態連接器和其他共享庫代碼的映射區域。
由于在Linux系統中每一個進程都會有/proc文件系統下與之對應的一個目錄(如將init進程的相關信息在/proc/1 目錄下的文件中描述,1表示init進程的進程號),因此通過proc文件系統可以查看某個進程的地址空間的映射情況。
測試代碼:
運行此程序:
輸入 size task
text:存放的是代碼 data:存放的是初始化過的全局變量或靜態變量 bss:存放的是未初始化的全局變量或靜態變量
輸入命令 cat /proc/3834/maps 其中3834是task的PID
3、線程
前面已經提到,進程是系統中程序執行和資源分配的基本單位。每個進程都擁有自己的數據段、代碼段和堆棧段,這就造成了進程進程切換等操作時需要較復雜的上下文切換等動作。為了進一步減少處理機制的空轉時間,支持多處理器及減少上下文切換開銷,進程在演化中出現了另一個概念——線程。它是進程內獨立的一條運行路線,是處理器調用的最小單元,也可以成為輕量級進程。線程可以對進程的內存空間和資源進程訪問,并與同一個進程中的其他線程共享。因此,線程上下文切換的開銷比創建進程小得多。
一個進程可以擁有多個線程,每個線程必須有一個父進程。線程不擁有系統資源,它只具有運行所必需的一些數據,如堆棧、寄存器與線程控制塊(TCB),線程與其父進程的其他線程共享該進程所擁有的全部資源。要注意的是,由線程共享了進程的資源和地址空間,因此,任何線程對系統資源的操作都會給其他線程帶來影響。由此可知,多線程中的同步是非常重要的問題。在多線程系統中,進程與線程的關系如圖所示:
在Linux系統中,線程可以分為以下3種:
用戶級線程
用戶級線程主要解決的是上下文切換的問題,它的調度算法和調度過程全部由用戶自己選擇決定,在運行時不需要特定的內核支持。在這里,操作系統往往會提供一個用戶空間的線程庫,該線程庫提供了線程的創建、調度和撤銷等功能,而內核仍然僅對進程進行管理。如果一個進程中的某一個線程調用了一個阻塞的系統調用函數,那么該進程好吧該進程中的其他所有線程也同時被阻塞。這種用戶級線程的主要缺點是在一個進程的多個線程的調度中無法發揮多處理器的優勢。
輕量級進程
輕量級進程是內核支持的用戶線程,是內核線程的一種抽象對象。每個線程擁有一個或多個輕量級進程,而每個輕量級進程分別被綁定在一個內核線程上。
內核線程
內核線程允許不同進程中的線程按照同一相對優先調度方法進行調度,這樣就可以發揮多處理器的并發優勢。現在大多數系統都采用用戶級線程與核心級線程并存的方法。一個用戶級線程可以對應一個或幾個核心級線程,也就是“一對一”或“多對一”模型。這樣既可以滿足多處理系統的需要,也可以最大限度地減少調度開銷。
使用線程機制大大加快了上下文切換速度,而節省了很多資源。但是因為在用戶態和內核態均要實現調度管理,所有會增加實現的復雜度和引起優先級翻轉的可能性。同時,一個多線程程序的同步設計與調試也會增加程序實現的難道。