[導讀] 內(nèi)核是怎么工作的,首先要理解進程管理,進程調(diào)度,本文開始閱讀進程管理部分,首先從進程的抽象描述開始。抽象是軟件工程的靈魂,而對于Linux操作系統(tǒng)而言,更是將抽象思想體現(xiàn)的淋漓盡致。本文從抽象建模的角度來對Linux進程描述符進行個人解讀,同時也參考了內(nèi)核文檔,一些網(wǎng)絡(luò)信息。
注:代碼基于linux-5.4.31,是一個最新的長期支持穩(wěn)定版本。
整理匆忙,限于水平,文章中錯誤一定很多,真誠懇請有這方面擅長的朋友幫忙指出,不甚感激!
進程的基本概念
進程 or 線程 or 任務?
進程:進程是一個正在運行的程序?qū)嵗?,由可?zhí)行的目標代碼組成,通常從某些硬媒介(如磁盤,閃存等)讀取并加載到內(nèi)存中。但是,從內(nèi)核的角度來看,涉及很多相關(guān)的工作內(nèi)容。操作系統(tǒng)存儲和管理有關(guān)任何當前正在運行的程序的其他信息:地址空間,內(nèi)存映射,用于讀/寫操作的打開文件,進程狀態(tài),線程等。
進程是正在執(zhí)行的計算機程序的實例。它包含程序代碼及其當前活動。取決于操作系統(tǒng)(OS),進程可能由同時執(zhí)行指令的多個執(zhí)行線程組成?;谶M程的多任務處理使您可以在使用文本編輯器的同時運行Java編譯器。在單個CPU中采用多個進程時,使用了各種內(nèi)存上下文之間的上下文切換。每個過程都有其自己的變量的完整集合。
但是,在Linux中,如果不討論線程(有時稱為輕量級進程),進程的抽象是不完整的。根據(jù)定義,線程是流程中的執(zhí)行上下文或執(zhí)行流;因此,每個進程至少包含一個線程。包含多個執(zhí)行線程的進程被稱為多線程進程。一個進程中有多個線程可以進行當前編程,并且在多處理器系統(tǒng)上可以實現(xiàn)真正的并行性。
線程:則是某一進程中一路單獨運行的程序,也就是說,線程存在于進程之中。一個進程由一個或多個線程構(gòu)成,各線程共享相同的代碼和全局數(shù)據(jù),但各有其自己的堆棧。由于堆棧是每個線程一個,所以局部變量對每一線程來說是私有的。由于所有線程共享同樣的代碼和全局數(shù)據(jù),它們比進程更緊密,比單獨的進程間更趨向于相互作用,線程間的相互作用更容易些,因為它們本身就有某些供通信用的共享內(nèi)存:進程的全局數(shù)據(jù)。
線程是CPU利用率的基本單位,由程序計數(shù)器,堆棧和一組寄存器組成。執(zhí)行線程是由計算機程序的分支分解為兩個或多個同時運行的任務而產(chǎn)生的。線程和進程的實現(xiàn)因一個操作系統(tǒng)而異,但在大多數(shù)情況下,線程包含在進程內(nèi)部。多個線程可以存在于同一進程中并共享資源(例如內(nèi)存),而不同進程則不共享這些資源。同一進程中的線程示例是自動拼寫檢查和寫入時自動保存文件。線程基本上是在相同內(nèi)存上下文中運行的進程。線程在執(zhí)行時可能共享相同的數(shù)據(jù)。線程圖,即單線程與多線程
任務:是最抽象的,是一個一般性的術(shù)語,指由軟件完成的一個活動。一個任務既可以是一個進程,也可以是一個線程。簡而言之,它指的是一系列共同達到某一目的的操作。與線程非常相似,不同之處在于它們通常不直接與OS交互。像線程池一樣,任務不會創(chuàng)建自己的OS線程。一個任務內(nèi)部可能有一個線程,也可能沒有。例如,讀取數(shù)據(jù)并將數(shù)據(jù)放入內(nèi)存中。這個任務可以作為一個進程來實現(xiàn),也可以作為一個線程(或作為一個中斷任務)來實現(xiàn)。在RTOS中,一般會將調(diào)度的基本單元稱為任務,比如freeRTOS,ucos,embOS等,在RTOS中沒有進程的概念。
進程 | 線程 |
---|---|
進程是重量級的操作 | 線程是輕量級操作 |
每個進程都有自己的內(nèi)存空間 | 線程共享它們所屬的進程的內(nèi)存空間 |
進程間的通信速度很慢,因為進程具有不同的內(nèi)存地址 | 線程間通信可能比進程間通信快,因為同一進程的線程與其所屬的進程共享內(nèi)存 |
進程之間的上下文切換開銷大 | 在同一進程的線程之間進行上下文切換的開銷較低 |
進程不與其他進程共享內(nèi)存 | 線程與同一進程的其他線程共享內(nèi)存 |
進程間通訊機制:
-
管道(Pipe)及有名管道(named pipe):管道可用于具有親緣關(guān)系進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關(guān)系進程間的通信; -
信號(Signal):信號是比較復雜的通信方式,用于通知接受進程有某種事件發(fā)生,除了用于進程間通信外,進程還可以發(fā)送信號給進程本身;linux除了支持Unix早期信號語義函數(shù)sigal外,還支持語義符合Posix.1標準的信號函數(shù) sigaction(實際上,該函數(shù)是基于BSD的,BSD為了實現(xiàn)可靠信號機制,又能夠統(tǒng)一對外接口,用sigaction函數(shù)重新實現(xiàn)了signal 函數(shù)); -
報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權(quán)限的進程可以向隊列中添加消息,被賦予讀權(quán)限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節(jié)流以及緩沖區(qū)大小受限等缺點。 -
共享內(nèi)存:使得多個進程可以訪問同一塊內(nèi)存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設(shè)計的。往往與其它通信機制,如信號量結(jié)合使用,來達到進程間的同步及互斥。 -
信號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。 -
套接字(Socket):更為一般的進程間通信機制,可用于不同機器之間的進程間通信。起初是由Unix系統(tǒng)的BSD分支開發(fā)出來的,但現(xiàn)在一般可以移植到其它類Unix系統(tǒng)上:Linux和System V的變種都支持套接字。
線程間的同步機制:為啥線程間沒有討論通訊機制?因為同一進程內(nèi)的線程共享進程的資源。那么資源共享,則需要處理資源共享時的同步問題。
-
互斥鎖(mutex):通過鎖機制實現(xiàn)線程間的同步。同一時刻只允許一個線程執(zhí)行一個關(guān)鍵部分的代碼。這部分代碼常稱為臨界區(qū)。哪些可能是臨界區(qū)呢?簡言之,多個線程可能競爭訪問的資源。以下一些函數(shù)是互斥鎖的API函數(shù)。
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_destroy(pthread_mutex *mutex);
int pthread_mutex_unlock(pthread_mutex *
-
全局條件變量(condition variable): 創(chuàng)建一些全局條件變量進行互斥訪問控制。以下是其操作的基本接口函數(shù):
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
-
信號量(semaphore):如同進程一樣,線程也可以通過信號量來實現(xiàn)通信,其基本操作接口API:
int sem_init (sem_t *sem , int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem);
進程在內(nèi)核中如何描述?
Linux中進程描述在./include/linux/sched.h中定義:
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/* 必須是首個元素 */
struct thread_info thread_info;
#endif
/* -1 unrunnable, 0 runnable, >0 stopped: */
volatile long state;
/* 前面是與調(diào)度密切相關(guān)的信息添加在這之前 */
randomized_struct_fields_start
void *stack;
refcount_t usage;
/* Per task flags (PF_*), defined further below: */
unsigned int flags;
unsigned int ptrace;
.........
};
該結(jié)構(gòu)非常大,集總抽象了進程的所有信息,包括進程ID,狀態(tài),父進程,子進程,同級,處理器寄存器,打開的文件,地址空間等。系統(tǒng)使用循環(huán)雙向鏈接列表進行存儲 所有過程描述符。
像這樣的大型結(jié)構(gòu)肯定會占用大量內(nèi)存空間。為每個進程提供較小的內(nèi)核堆棧大?。梢允褂镁幾g時選項進行配置,但默認情況下限制為一頁,即對于32位體系結(jié)構(gòu)嚴格為4KB(一個頁),對于64位體系結(jié)構(gòu)嚴格為8KB(兩個頁) –內(nèi)核堆棧不具備增長或收縮),以這種浪費的方式使用資源并不是很方便。因此,決定在堆棧中放置一個更簡單的結(jié)構(gòu),并帶有指向?qū)嶋Htask_struct的指針,從而引申出thread_info。
抽象建模思想看進程描述符
進程首先是操作系統(tǒng)對底層進行抽象而提供面向應用接口的一種抽象,而進程描述符則將底層資源、進程本身的調(diào)度從以下幾個大的方面進行高級別的抽象封裝:
-
應用程序信息抽象 -
操作系統(tǒng)資源抽象 -
調(diào)度接口抽象 -
內(nèi)存管理抽象 -
賬戶信息抽象 -
......
通過預讀進程描述符,個人將進程描述相關(guān)信息大致分為以下幾個大類抽象:
-
調(diào)度相關(guān)抽象
涉及thread_info、優(yōu)先級、棧、上下文切換、調(diào)度相關(guān)鏈表等關(guān)鍵數(shù)據(jù)。
-
CPU相關(guān)抽象
涉及SMP多核處理抽象、CPUSET子系統(tǒng)相關(guān)、當前CPU等相關(guān)數(shù)據(jù)抽象。
-
保護機制抽象
-
內(nèi)存管理抽象
-
緩存相關(guān)抽象
參考閱讀:https://www.cnblogs.com/20135228guoyao/p/5334985.html
https://blog.csdn.net/wang_xya/article/details/35234429
-
信號通信抽象
-
接口相關(guān)抽象
-
調(diào)試跟蹤抽象
-
安全機制抽象
-
資源管理抽象
-
雜項信息抽象
thread_info
該字段保存特定于處理器的狀態(tài)信息,并且是進程描述符的關(guān)鍵元素。具體定義在./arch/xxx/include/asm/thread_info.h中。
-
entry.S需要立即訪問此結(jié)構(gòu)的低級任務數(shù)據(jù)應完全適合一個緩存行,此結(jié)構(gòu)共享主管堆棧頁面 -
如果更改此結(jié)構(gòu)的內(nèi)容,則還必須更改匯編代碼。 -
因為thread_info包含了當前進程的指針,存儲在棧底或棧頂,取決于不同體系架構(gòu)棧的增長方向,利用thread_info可以快速的訪問當前進程的信息,而不必依次遍歷。
ARM32的定義:
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
#ifdef CONFIG_STACKPROTECTOR_PER_TASK
unsigned long stack_canary;
#endif
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
從書上和網(wǎng)上看到都是前面這樣描述的,但是對于ARM64的卻沒有當前進程指針,這是為何呢?沒弄明白,有誰知道告訴下我唄。
struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
u64 ttbr0; /* saved TTBR0_EL1 */
#endif
union {
u64 preempt_count; /* 0 => preemptible, <0 => bug */
struct {
#ifdef CONFIG_CPU_BIG_ENDIAN
u32 need_resched;
u32 count;
#else
u32 count;
u32 need_resched;
#endif
} preempt;
};
};
利用如下的幾種方式,可以獲取thread_info信息:
-
static inline struct thread_info *current_thread_info(void) -
#define GET_THREAD_INFO(reg) -
...
SLUB 分配器
thread_info實現(xiàn)了進程存儲對描述符的引用以及如何訪問它們。但是,如果task_struct不是在內(nèi)核堆棧內(nèi)部,則task_struct到底位于內(nèi)存中的什么位置?為此,Linux提供了一種特殊的內(nèi)存管理機制,稱為SLUB層。SLUB動態(tài)生成task_struct,并把thread_info存在棧底或棧頂。
volatile long state
進程狀態(tài),可取的進程狀態(tài):
-
TASK_RUNNING: 可執(zhí)行態(tài) -
TASK_INTERRUPTIBLE:可中斷 -
TASK_UNINTERRUPTIBLE:不可中斷 -
__TASK_STOPPED:停止態(tài) -
__TASK_TRACED:被其他進程跟蹤的進程
為何用volatile修飾。由于內(nèi)核經(jīng)常需要從不同位置更改進程的狀態(tài),例如,如果在單個CPU硬件上同時將兩個進程設(shè)置為RUNNABLE。熟悉單片機編程的朋友一定知道,當在中斷函數(shù)中需要修改以及在中斷外部也會被修改的變量,就會使用到volatile修飾變量。
randomized_struct_fields_start
這是gcc的一個插件(插件來自于Grsecurity),其作用就是這之后的變量不會按照聲明順序存儲在內(nèi)存中,而會按照一定的隨機順序存放,這樣做是基于安全考慮,比如應用程序的進程描述符被劫持,如果按順序存放,則容易篡改其內(nèi)容。
—END—
如果喜歡右下點個在看,也會讓我倍感鼓舞
關(guān)注置頂:掃描左下二維碼關(guān)注公眾號加星
關(guān)注 |
加群 |
免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!