抽象思想解讀Linux進(jìn)程描述符
[導(dǎo)讀] 內(nèi)核是怎么工作的,首先要理解進(jìn)程管理,進(jìn)程調(diào)度,本文開始閱讀進(jìn)程管理部分,首先從進(jìn)程的抽象描述開始。抽象是軟件工程的靈魂,而對于Linux操作系統(tǒng)而言,更是將抽象思想體現(xiàn)的淋漓盡致。本文從抽象建模的角度來對Linux進(jìn)程描述符進(jìn)行個人解讀,同時也參考了內(nèi)核文檔,一些網(wǎng)絡(luò)信息。
注:代碼基于linux-5.4.31,是一個最新的長期支持穩(wěn)定版本。
整理匆忙,限于水平,文章中錯誤一定很多,真誠懇請有這方面擅長的朋友幫忙指出,不甚感激!
進(jìn)程的基本概念
進(jìn)程 or 線程 or 任務(wù)?
進(jìn)程:進(jìn)程是一個正在運行的程序?qū)嵗?,由可?zhí)行的目標(biāo)代碼組成,通常從某些硬媒介(如磁盤,閃存等)讀取并加載到內(nèi)存中。但是,從內(nèi)核的角度來看,涉及很多相關(guān)的工作內(nèi)容。操作系統(tǒng)存儲和管理有關(guān)任何當(dāng)前正在運行的程序的其他信息:地址空間,內(nèi)存映射,用于讀/寫操作的打開文件,進(jìn)程狀態(tài),線程等。
進(jìn)程是正在執(zhí)行的計算機(jī)程序的實例。它包含程序代碼及其當(dāng)前活動。取決于操作系統(tǒng)(OS),進(jìn)程可能由同時執(zhí)行指令的多個執(zhí)行線程組成。基于進(jìn)程的多任務(wù)處理使您可以在使用文本編輯器的同時運行Java編譯器。在單個CPU中采用多個進(jìn)程時,使用了各種內(nèi)存上下文之間的上下文切換。每個過程都有其自己的變量的完整集合。
但是,在Linux中,如果不討論線程(有時稱為輕量級進(jìn)程),進(jìn)程的抽象是不完整的。根據(jù)定義,線程是流程中的執(zhí)行上下文或執(zhí)行流;因此,每個進(jìn)程至少包含一個線程。包含多個執(zhí)行線程的進(jìn)程被稱為多線程進(jìn)程。一個進(jìn)程中有多個線程可以進(jìn)行當(dāng)前編程,并且在多處理器系統(tǒng)上可以實現(xiàn)真正的并行性。
線程:則是某一進(jìn)程中一路單獨運行的程序,也就是說,線程存在于進(jìn)程之中。一個進(jìn)程由一個或多個線程構(gòu)成,各線程共享相同的代碼和全局?jǐn)?shù)據(jù),但各有其自己的堆棧。由于堆棧是每個線程一個,所以局部變量對每一線程來說是私有的。由于所有線程共享同樣的代碼和全局?jǐn)?shù)據(jù),它們比進(jìn)程更緊密,比單獨的進(jìn)程間更趨向于相互作用,線程間的相互作用更容易些,因為它們本身就有某些供通信用的共享內(nèi)存:進(jìn)程的全局?jǐn)?shù)據(jù)。
線程是CPU利用率的基本單位,由程序計數(shù)器,堆棧和一組寄存器組成。執(zhí)行線程是由計算機(jī)程序的分支分解為兩個或多個同時運行的任務(wù)而產(chǎn)生的。線程和進(jìn)程的實現(xiàn)因一個操作系統(tǒng)而異,但在大多數(shù)情況下,線程包含在進(jìn)程內(nèi)部。多個線程可以存在于同一進(jìn)程中并共享資源(例如內(nèi)存),而不同進(jìn)程則不共享這些資源。同一進(jìn)程中的線程示例是自動拼寫檢查和寫入時自動保存文件。線程基本上是在相同內(nèi)存上下文中運行的進(jìn)程。線程在執(zhí)行時可能共享相同的數(shù)據(jù)。線程圖,即單線程與多線程
任務(wù):是最抽象的,是一個一般性的術(shù)語,指由軟件完成的一個活動。一個任務(wù)既可以是一個進(jìn)程,也可以是一個線程。簡而言之,它指的是一系列共同達(dá)到某一目的的操作。與線程非常相似,不同之處在于它們通常不直接與OS交互。像線程池一樣,任務(wù)不會創(chuàng)建自己的OS線程。一個任務(wù)內(nèi)部可能有一個線程,也可能沒有。例如,讀取數(shù)據(jù)并將數(shù)據(jù)放入內(nèi)存中。這個任務(wù)可以作為一個進(jìn)程來實現(xiàn),也可以作為一個線程(或作為一個中斷任務(wù))來實現(xiàn)。在RTOS中,一般會將調(diào)度的基本單元稱為任務(wù),比如freeRTOS,ucos,embOS等,在RTOS中沒有進(jìn)程的概念。
進(jìn)程 | 線程 |
---|---|
進(jìn)程是重量級的操作 | 線程是輕量級操作 |
每個進(jìn)程都有自己的內(nèi)存空間 | 線程共享它們所屬的進(jìn)程的內(nèi)存空間 |
進(jìn)程間的通信速度很慢,因為進(jìn)程具有不同的內(nèi)存地址 | 線程間通信可能比進(jìn)程間通信快,因為同一進(jìn)程的線程與其所屬的進(jìn)程共享內(nèi)存 |
進(jìn)程之間的上下文切換開銷大 | 在同一進(jìn)程的線程之間進(jìn)行上下文切換的開銷較低 |
進(jìn)程不與其他進(jìn)程共享內(nèi)存 | 線程與同一進(jìn)程的其他線程共享內(nèi)存 |
進(jìn)程間通訊機(jī)制:
-
管道(Pipe)及有名管道(named pipe):管道可用于具有親緣關(guān)系進(jìn)程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關(guān)系進(jìn)程間的通信; -
信號(Signal):信號是比較復(fù)雜的通信方式,用于通知接受進(jìn)程有某種事件發(fā)生,除了用于進(jìn)程間通信外,進(jìn)程還可以發(fā)送信號給進(jìn)程本身;linux除了支持Unix早期信號語義函數(shù)sigal外,還支持語義符合Posix.1標(biāo)準(zhǔn)的信號函數(shù) sigaction(實際上,該函數(shù)是基于BSD的,BSD為了實現(xiàn)可靠信號機(jī)制,又能夠統(tǒng)一對外接口,用sigaction函數(shù)重新實現(xiàn)了signal 函數(shù)); -
報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權(quán)限的進(jìn)程可以向隊列中添加消息,被賦予讀權(quán)限的進(jìn)程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節(jié)流以及緩沖區(qū)大小受限等缺點。 -
共享內(nèi)存:使得多個進(jìn)程可以訪問同一塊內(nèi)存空間,是最快的可用IPC形式。是針對其他通信機(jī)制運行效率較低而設(shè)計的。往往與其它通信機(jī)制,如信號量結(jié)合使用,來達(dá)到進(jìn)程間的同步及互斥。 -
信號量(semaphore):主要作為進(jìn)程間以及同一進(jìn)程不同線程之間的同步手段。 -
套接字(Socket):更為一般的進(jìn)程間通信機(jī)制,可用于不同機(jī)器之間的進(jìn)程間通信。起初是由Unix系統(tǒng)的BSD分支開發(fā)出來的,但現(xiàn)在一般可以移植到其它類Unix系統(tǒng)上:Linux和System V的變種都支持套接字。
線程間的同步機(jī)制:為啥線程間沒有討論通訊機(jī)制?因為同一進(jìn)程內(nèi)的線程共享進(jìn)程的資源。那么資源共享,則需要處理資源共享時的同步問題。
-
互斥鎖(mutex):通過鎖機(jī)制實現(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)建一些全局條件變量進(jìn)行互斥訪問控制。以下是其操作的基本接口函數(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):如同進(jìn)程一樣,線程也可以通過信號量來實現(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);
進(jìn)程在內(nèi)核中如何描述?
Linux中進(jìn)程描述在./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)非常大,集總抽象了進(jìn)程的所有信息,包括進(jìn)程ID,狀態(tài),父進(jìn)程,子進(jìn)程,同級,處理器寄存器,打開的文件,地址空間等。系統(tǒng)使用循環(huán)雙向鏈接列表進(jìn)行存儲 所有過程描述符。
像這樣的大型結(jié)構(gòu)肯定會占用大量內(nèi)存空間。為每個進(jìn)程提供較小的內(nèi)核堆棧大小(可以使用編譯時選項進(jìn)行配置,但默認(rèn)情況下限制為一頁,即對于32位體系結(jié)構(gòu)嚴(yán)格為4KB(一個頁),對于64位體系結(jié)構(gòu)嚴(yán)格為8KB(兩個頁) –內(nèi)核堆棧不具備增長或收縮),以這種浪費的方式使用資源并不是很方便。因此,決定在堆棧中放置一個更簡單的結(jié)構(gòu),并帶有指向?qū)嶋Htask_struct的指針,從而引申出thread_info。
抽象建模思想看進(jìn)程描述符
進(jìn)程首先是操作系統(tǒng)對底層進(jìn)行抽象而提供面向應(yīng)用接口的一種抽象,而進(jìn)程描述符則將底層資源、進(jìn)程本身的調(diào)度從以下幾個大的方面進(jìn)行高級別的抽象封裝:
-
應(yīng)用程序信息抽象 -
操作系統(tǒng)資源抽象 -
調(diào)度接口抽象 -
內(nèi)存管理抽象 -
賬戶信息抽象 -
......
通過預(yù)讀進(jìn)程描述符,個人將進(jìn)程描述相關(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)、當(dāng)前CPU等相關(guān)數(shù)據(jù)抽象。
-
保護(hù)機(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)試跟蹤抽象
-
安全機(jī)制抽象
-
資源管理抽象
-
雜項信息抽象
thread_info
該字段保存特定于處理器的狀態(tài)信息,并且是進(jìn)程描述符的關(guān)鍵元素。具體定義在./arch/xxx/include/asm/thread_info.h中。
-
entry.S需要立即訪問此結(jié)構(gòu)的低級任務(wù)數(shù)據(jù)應(yīng)完全適合一個緩存行,此結(jié)構(gòu)共享主管堆棧頁面 -
如果更改此結(jié)構(gòu)的內(nèi)容,則還必須更改匯編代碼。 -
因為thread_info包含了當(dāng)前進(jìn)程的指針,存儲在棧底或棧頂,取決于不同體系架構(gòu)棧的增長方向,利用thread_info可以快速的訪問當(dāng)前進(jìn)程的信息,而不必依次遍歷。
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的卻沒有當(dāng)前進(jìn)程指針,這是為何呢?沒弄明白,有誰知道告訴下我唄。
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)了進(jìn)程存儲對描述符的引用以及如何訪問它們。但是,如果task_struct不是在內(nèi)核堆棧內(nèi)部,則task_struct到底位于內(nèi)存中的什么位置?為此,Linux提供了一種特殊的內(nèi)存管理機(jī)制,稱為SLUB層。SLUB動態(tài)生成task_struct,并把thread_info存在棧底或棧頂。
volatile long state
進(jìn)程狀態(tài),可取的進(jìn)程狀態(tài):
-
TASK_RUNNING: 可執(zhí)行態(tài) -
TASK_INTERRUPTIBLE:可中斷 -
TASK_UNINTERRUPTIBLE:不可中斷 -
__TASK_STOPPED:停止態(tài) -
__TASK_TRACED:被其他進(jìn)程跟蹤的進(jìn)程
為何用volatile修飾。由于內(nèi)核經(jīng)常需要從不同位置更改進(jìn)程的狀態(tài),例如,如果在單個CPU硬件上同時將兩個進(jìn)程設(shè)置為RUNNABLE。熟悉單片機(jī)編程的朋友一定知道,當(dāng)在中斷函數(shù)中需要修改以及在中斷外部也會被修改的變量,就會使用到volatile修飾變量。
randomized_struct_fields_start
這是gcc的一個插件(插件來自于Grsecurity),其作用就是這之后的變量不會按照聲明順序存儲在內(nèi)存中,而會按照一定的隨機(jī)順序存放,這樣做是基于安全考慮,比如應(yīng)用程序的進(jìn)程描述符被劫持,如果按順序存放,則容易篡改其內(nèi)容。
—END—
如果喜歡右下點個在看,也會讓我倍感鼓舞
關(guān)注置頂:掃描左下二維碼關(guān)注公眾號加星
關(guān)注 |
加群 |
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!