歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux下的進程類別(內核線程、輕量級進程和用戶進程)以及其創建方式--Linux進程的管理與調度(四)

Linux下的進程類別(內核線程、輕量級進程和用戶進程)以及其創建方式--Linux進程的管理與調度(四)

日期:2017/3/1 11:49:53   编辑:關於Linux

雖然我們在區分Linux進程類別, 但是我還是想說Linux下只有一種類型的進程,那就是task_struct,當然我也想說linux其實也沒有線程的概念, 只是將那些與其他進程共享資源的進程稱之為線程。

一個進程由於其運行空間的不同, 從而有內核線程用戶進程的區分, 內核線程運行在內核空間, 之所以稱之為線程是因為它沒有虛擬地址空間, 只能訪問內核的代碼和數據, 而用戶進程則運行在用戶空間, 但是可以通過中斷, 系統調用等方式從用戶態陷入內核態。

用戶進程運行在用戶空間上, 而一些通過共享資源實現的一組進程我們稱之為線程組, Linux下內核其實本質上沒有線程的概念, Linux下線程其實上是與其他進程共享某些資源的進程而已。但是我們習慣上還是稱他們為線程或者輕量級進程

因此, Linux上進程分3種,內核線程(或者叫核心進程)、用戶進程、用戶線程, 當然如果更嚴謹的,你也可以認為用戶進程和用戶線程都是用戶進程。

關於輕量級進程這個概念, 其實並不等價於線程

不同的操作系統中依據其實現的不同, 輕量級進程其實是一個不一樣的概念

詳細信息參見 維基百科-LWP輕量級進程

或者本人的另外一篇博客內核線程、輕量級進程、用戶線程三種線程概念解惑(線程≠輕量級進程)

In computer operating systems, a light-weight process (LWP) is a means of achieving multitasking. In the traditional meaning of the term, as used in Unix System V and Solaris, a LWP runs in user space on top of a single kernel thread and shares its address space and system resources with other LWPs within the same process. Multiple user level threads, managed by a thread library, can be placed on top of one or many LWPs - allowing multitasking to be done at the user level, which can have some performance benefits.[1]

In some operating systems there is no separate LWP layer between kernel threads and user threads. This means that user threads are implemented directly on top of kernel threads. In those contexts, the term “light-weight process” typically refers to kernel threads and the term “threads” can refer to user threads.[2] On Linux, user threads are implemented by allowing certain processes to share resources, which sometimes leads to these processes to be called “light weight processes”.[3][4] Similarly, in SunOS version 4 onwards (prior to Solaris) “light weight process” referred to user threads.

進程與線程


進程是一個具有獨立功能的程序關於某個數據集合的一次運行活動。它可以申請和擁有系統資源,是一個動態的概念,是一個活動的實體。它不只是程序的代碼,還包括當前的活動,通過程序計數器的值和處理寄存器的內容來表示。進程是一個“執行中的程序”。程序是一個沒有生命的實體,只有處理器賦予程序生命時,它才能成為一個活動的實體,我們稱其為進程。

通常在一個進程中可以包含若干個線程,它們可以利用進程所擁有的資源。在引入線程的操作系統中,通常都是把進程作為分配資源的基本單位,而把線程作為獨立運行和獨立調度的基本單位。由於線程比進程更小,基本上不擁有系統資源,故對它的調度所付出的開銷就會小得多,能更高效的提高系統內多個程序間並發執行的程度。

線程和進程的區別在於,子進程和父進程有不同的代碼和數據空間,而多個線程則共享數據空間,每個線程有自己的執行堆棧和程序計數器為其執行上下文。多線程主要是為了節約CPU時間,發揮利用,根據具體情況而定。線程的運行中需要使用計算機的內存資源和CPU。

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。

線程與進程的區別歸納:

地址空間和其它資源:進程間相互獨立,同一進程的各線程間共享。某進程內的線程在其它進程不可見。

通信:進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。

調度和切換:線程上下文切換比進程上下文切換要快得多。

在多線程OS中,進程不是一個可執行的實體。

內核線程


內核線程就是內核的分身,一個分身可以處理一件特定事情。這在處理異步事件如異步IO時特別有用。內核線程的使用是廉價的,唯一使用的資源就是內核棧和上下文切換時保存寄存器的空間。支持多線程的內核叫做多線程內核(Multi-Threads kernel )。

內核線程只運行在內核態,不受用戶態上下文的拖累。

處理器競爭:可以在全系統范圍內競爭處理器資源;

使用資源:唯一使用的資源是內核棧和上下文切換時保持寄存器的空間

調度:調度的開銷可能和進程自身差不多昂貴

同步效率:資源的同步和數據共享比整個進程的數據同步和共享要低一些。

linux進程的創建流程


線程機制式現代編程技術中常用的一種抽象概念。該機制提供了同一個程序內共享內存地址空間,打開文件和資源的一組線程。

進程的復制fork和加載execve


我們在Linux下進行進行編程,往往都是通過fork出來一個新的程序,fork從化字面意義上理解就是說”分叉”, 這其實就意味著我們的fork進程並不是真正從無到有被創建出來的。

一個進程,包括代碼、數據和分配給進程的資源,它其實是從現有的進程(父進程)復制出的一個副本(子進程),fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,然後如果我們通過execve為子進程加載新的應用程序後,那麼新的進程將開始執行新的應用

簡單來說,新的進程是通過fork和execve創建的,首先通過fork從父進程分叉出一個基本一致的副本,然後通過execve來加載新的應用程序鏡像

fork生成當前進程的的一個相同副本,該副本成為子進程

原進程(父進程)的所有資源都以適當的方法復制給新的進程(子進程)。因此該系統調用之後,原來的進程就有了兩個獨立的實例,這兩個實例的聯系包括:同一組打開文件, 同樣的工作目錄, 進程虛擬空間(內存)中同樣的數據(當然兩個進程各有一份副本, 也就是說他們的虛擬地址相同, 但是所對應的物理地址不同)等等。

execve從一個可執行的二進制程序鏡像加載應用程序, 來代替當前運行的進程

換句話說, 加載了一個新的應用程序。因此execv並不是創建新進程

所以我們在linux要創建一個應用程序的時候,其實執行的操作就是

首先使用fork復制一個舊的進程

然後調用execve在為新的進程加載一個新的應用程序

寫時復制技術


有人認為這樣大批量的復制會導致執行效率過低。其實在復制過程中,linux采用了寫時復制的策略。

寫入時復制(Copy-on-write)是一個被使用在程式設計領域的最佳化策略。其基礎的觀念是,如果有多個呼叫者(callers)同時要求相同資源,他們會共同取得相同的指標指向相同的資源,直到某個呼叫者(caller)嘗試修改資源時,系統才會真正復制一個副本(private copy)給該呼叫者,以避免被修改的資源被直接察覺到,這過程對其他的呼叫只都是通透的(transparently)。此作法主要的優點是如果呼叫者並沒有修改該資源,就不會有副本(private copy)被建立。

第一代Unix系統實現了一種傻瓜式的進程創建:當發出fork()系統調用時,內核原樣復制父進程的整個地址空間並把復制的那一份分配給子進程。這種行為是非常耗時的,這種創建地址空間的方法涉及許多內存訪問,消耗許多CPU周期,並且完全破壞了高速緩存中的內容。在大多數情況下,這樣做常常是毫無意義的,因為許多子進程通過裝入一個新的程序開始它們的執行,這樣就完全丟棄了所繼承的地址空間。

現在的Linux內核采用一種更為有效的方法,稱之為寫時復制(Copy On Write,COW)。這種思想相當簡單:父進程和子進程共享頁幀而不是復制頁幀。然而,只要頁幀被共享,它們就不能被修改,即頁幀被保護。無論父進程還是子進程何時試圖寫一個共享的頁幀,就產生一個異常,這時內核就把這個頁復制到一個新的頁幀中並標記為可寫。原來的頁幀仍然是寫保護的:當其他進程試圖寫入時,內核檢查寫進程是否是這個頁幀的唯一屬主,如果是,就把這個頁幀標記為對這個進程是可寫的。

當進程A使用系統調用fork創建一個子進程B時,由於子進程B實際上是父進程A的一個拷貝,

因此會擁有與父進程相同的物理頁面.為了節約內存和加快創建速度的目標,fork()函數會讓子進程B以只讀方式共享父進程A的物理頁面.同時將父進程A對這些物理頁面的訪問權限也設成只讀.

這樣,當父進程A或子進程B任何一方對這些已共享的物理頁面執行寫操作時,都會產生頁面出錯異常(page_fault int14)中斷,此時CPU會執行系統提供的異常處理函數do_wp_page()來解決這個異常.

do_wp_page()會對這塊導致寫入異常中斷的物理頁面進行取消共享操作,為寫進程復制一新的物理頁面,使父進程A和子進程B各自擁有一塊內容相同的物理頁面.最後,從異常處理函數中返回時,CPU就會重新執行剛才導致異常的寫入操作指令,使進程繼續執行下去.

一個進程調用fork()函數後,系統先給新的進程分配資源,例如存儲數據和代碼的空間。然後把原來的進程的所有值都復制到新的新進程中,只有少數值與原來的進程的值(比如PID)不同。相當於克隆了一個自己。

關於進程創建的

參見 Linux中fork,vfork和clone詳解(區別與聯系)

不同操作系統線程的實現機制


專門線程支持的系統-LWP機制


線程更好的支持了並發程序設計技術, 在多處理器系統上, 他能保證真正的並行處理。Microsoft Windows或是Sun Solaris等操作系統都對線程進行了支持。

這些系統中都在內核中提供了專門支持線程的機制, Unix System V和Sun Solaris將線程稱作為輕量級進程(LWP-Light-weight process),在這些系統中, 相比較重量級進程, 線程被抽象成一種耗費較少資源, 運行迅速的執行單元。

Linux下線程的實現機制


但是Linux實現線程的機制非常獨特。從內核的角度來說, 他並沒有線程這個概念。Linux把所有的進程都當做進程來實現。內核中並沒有准備特別的調度算法或者定義特別的數據結構來表示線程。相反, 線程僅僅被視為一個與其他進程共享某些資源的進程。每個線程都擁有唯一隸屬於自己的task_struct, 所以在內核看來, 它看起來就像式一個普通的進程(只是線程和同組的其他進程共享某些資源)

在之前Linux進程描述符task_struct結構體詳解–Linux進程的管理與調度(一)和Linux進程ID號–Linux進程的管理與調度(三)中講解進程的pid號的時候我們就提到了, 進程task_struct中pid存儲的是內核對該進程的唯一標示, 即對進程則標示進程號, 對線程來說就是其線程號, 那麼對於線程來說一個線程組所有線程與領頭線程具有相同的進程號,存入tgid字段

因此getpid()返回當前進程的進程號,返回的應該是tgid值而不是pid的值, 對於用戶空間來說同組的線程擁有相同進程號即tpid, 而對於內核來說, 某種成都上來說不存在線程的概念, 那麼pid就是內核唯一區分每個進程的標示。

正是linux下組管理, 寫時復制等這些巧妙的實現方式

linux下進程或者線程的創建開銷很小

既然不管是線程或者進程內核都是不加區分的,一組共享地址空間或者資源的線程可以組成一個線程組, 那麼其他進程即使不共享資源也可以組成進程組, 甚至來說一組進程組也可以組成會話組, 進程組可以簡化向所有組內進程發送信號的操作, 一組會話也更能適應多道程序環境

實現機制的區別


總而言之, Linux中線程與專門線程支持系統是完全不同的

Unix System V和Sun Solaris將用戶線程稱作為輕量級進程(LWP-Light-weight process), 相比較重量級進程, 線程被抽象成一種耗費較少資源, 運行迅速的執行單元。

而對於linux來說, 用戶線程只是一種進程間共享資源的手段, 相比較其他系統的進程來說, linux系統的進程本身已經很輕量級了

舉個例子來說, 假如我們有一個包括了四個線程的進程,

在提供專門線程支持的系統中, 通常會有一個包含只想四個不同線程的指針的進程描述符。該描述符復制描述像地址空間, 打開的文件這樣的共享資源。線程本身再去描述它獨占的資源。

相反, Linux僅僅創建了四個進程, 並分配四個普通的task_struct結構, 然後建立這四個進程時制定他們共享某些資源。

內核線程


Linux內核可以看作一個服務進程(管理軟硬件資源,響應用戶進程的種種合理以及不合理的請求)。內核需要多個執行流並行,為了防止可能的阻塞,多線程化是必要的。

內核線程就是內核的分身,一個分身可以處理一件特定事情。Linux內核使用內核線程來將內核分成幾個功能模塊,像kswapd、kflushd等,這在處理異步事件如異步IO時特別有用。內核線程的使用是廉價的,唯一使用的資源就是內核棧和上下文切換時保存寄存器的空間。支持多線程的內核叫做多線程內核(Multi-Threads kernel )。內核線程的調度由內核負責,一個內核線程處於阻塞狀態時不影響其他的內核線程,因為其是調度的基本單位。這與用戶線程是不一樣的。

內核線程只運行在內核態,不受用戶態上下文的拖累。

處理器競爭:可以在全系統范圍內競爭處理器資源;

使用資源:唯一使用的資源是內核棧和上下文切換時保持寄存器的空間

調度:調度的開銷可能和進程自身差不多昂貴

同步效率:資源的同步和數據共享比整個進程的數據同步和共享要低一些。

內核線程與普通進程的異同


跟普通進程一樣,內核線程也有優先級和被調度。
當和用戶進程擁有相同的static_prio 時,內核線程有機會得到更多的cpu資源

內核線程的bug直接影響內核,很容易搞死整個系統, 但是用戶進程處在內核的管理下,其bug最嚴重的情況也只會把自己整崩潰

內核線程沒有自己的地址空間,所以它們的”current->mm”都是空的;

內核線程只能在內核空間操作,不能與用戶空間交互;

內核線程不需要訪問用戶空間內存,這是再好不過了。所以內核線程的task_struct的mm域為空

但是剛才說過,內核線程還有核心堆棧,沒有mm怎麼訪問它的核心堆棧呢?這個核心堆棧跟task_struct的thread_info共享8k的空間,所以不用mm描述。

但是內核線程總要訪問內核空間的其他內核啊,沒有mm域畢竟是不行的。
所以內核線程被調用時, 內核會將其task_strcut的active_mm指向前一個被調度出的進程的mm域, 在需要的時候,內核線程可以使用前一個進程的內存描述符。

因為內核線程不訪問用戶空間,只操作內核空間內存,而所有進程的內核空間都是一樣的。這樣就省下了一個mm域的內存。

內核線程創建


在內核中,有兩種方法可以生成內核線程,一種是使用kernel_thread()接口,另一種是用kthread_create()接口

kernel_thread


先說kernel_thread接口,使用該接口創建的線程,必須在該線程中調用daemonize()函數,這是因為只有當線程的父進程指向”Kthreadd”時,該線程才算是內核線程,而恰好daemonize()函數主要工作便是將該線程的父進程改成“kthreadd”內核線程;默認情況下,調用deamonize()後,會阻塞所有信號,如果想操作某個信號可以調用allow_signal()函數。

int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags); 
            // fn為線程函數,arg為線程函數參數,flags為標記
void daemonize(const char * name,...); // name為內核線程的名稱

kthread_create


而kthread_create接口,則是標准的內核線程創建接口,只須調用該接口便可創建內核線程;默認創建的線程是存於不可運行的狀態,所以需要在父進程中通過調用wake_up_process()函數來啟動該線程。

struct task_struct *kthread_create(int (*threadfn)(void *data),void *data,
                                  const char namefmt[], ...);
 //threadfn為線程函數;data為線程函數參數;namefmt為線程名稱,可被格式化的, 類似printk一樣傳入某種格式的線程名

線程創建後,不會馬上運行,而是需要將kthread_create() 返回的task_struct指針傳給wake_up_process(),然後通過此函數運行線程。

kthread_run


當然,還有一個創建並啟動線程的函數:kthread_run

struct task_struct *kthread_run(int (*threadfn)(void *data),
                                    void *data,
                                    const char *namefmt, ...);

線程一旦啟動起來後,會一直運行,除非該線程主動調用do_exit函數,或者其他的進程調用kthread_stop函數,結束線程的運行。

int kthread_stop(struct task_struct *thread);

kthread_stop() 通過發送信號給線程。
如果線程函數正在處理一個非常重要的任務,它不會被中斷的。當然如果線程函數永遠不返回並且不檢查信號,它將永遠都不會停止。

```c
int wake_up_process(struct task_struct *p); //喚醒線程
struct task_struct *kthread_run(int (*threadfn)(void *data),void *data,
                                const char namefmt[], ...);//是以上兩個函數的功能的總和

因為線程也是進程,所以其結構體也是使用進程的結構體”struct task_struct”。

內核線程的退出

當線程執行到函數末尾時會自動調用內核中do_exit()函數來退出或其他線程調用kthread_stop()來指定線程退出。

總結


Linux使用task_struct來描述進程和線程

一個進程由於其運行空間的不同, 從而有內核線程用戶進程的區分, 內核線程運行在內核空間, 之所以稱之為線程是因為它沒有虛擬地址空間, 只能訪問內核的代碼和數據, 而用戶進程則運行在用戶空間, 不能直接訪問內核的數據但是可以通過中斷, 系統調用等方式從用戶態陷入內核態,但是內核態只是進程的一種狀態, 與內核線程有本質區別

用戶進程運行在用戶空間上, 而一些通過共享資源實現的一組進程我們稱之為線程組, Linux下內核其實本質上沒有線程的概念, Linux下線程其實上是與其他進程共享某些資源的進程而已。但是我們習慣上還是稱他們為線程或者輕量級進程

因此, Linux上進程分3種,內核線程(或者叫核心進程)、用戶進程、用戶線程, 當然如果更嚴謹的,你也可以認為用戶進程和用戶線程都是用戶進程。

內核線程擁有 進程描述符、PID、進程正文段、核心堆棧

用戶進程擁有 進程描述符、PID、進程正文段、核心堆棧 、用戶空間的數據段和堆棧

用戶線程擁有 進程描述符、PID、進程正文段、核心堆棧,同父進程共享用戶空間的數據段和堆棧

用戶線程也可以通過exec函數族擁有自己的用戶空間的數據段和堆棧,成為用戶進程。

Copyright © Linux教程網 All Rights Reserved