[導讀] 核心是怎麼工作的,首先要了解程序管理,程序排程,本文開始閱讀程序管理部分,首先從程序的抽象描述開始。抽象是軟體工程的靈魂,而對于Linux作業系統而言,更是将抽象思想展現的淋漓盡緻。本文從抽象模組化的角度來對Linux程序描述符進行個人解讀,同時也參考了核心文檔,一些網絡資訊。
注:代碼基于linux-5.4.31,是一個最新的長期支援穩定版本。
整理匆忙,限于水準,文章中錯誤一定很多,真誠懇請有這方面擅長的朋友幫忙指出,不甚感激!
程序的基本概念
程序 or 線程 or 任務?
程序:程序是一個正在運作的程式執行個體,由可執行的目标代碼組成,通常從某些硬媒介(如磁盤,閃存等)讀取并加載到記憶體中。 但是,從核心的角度來看,涉及很多相關的工作内容。 作業系統存儲和管理有關任何目前正在運作的程式的其他資訊:位址空間,記憶體映射,用于讀/寫操作的打開檔案,程序狀态,線程等。
程序是正在執行的計算機程式的執行個體。它包含程式代碼及其目前活動。取決于作業系統(OS),程序可能由同時執行指令的多個執行線程組成。基于程序的多任務處理使您可以在使用文本編輯器的同時運作Java編譯器。在單個CPU中采用多個程序時,使用了各種記憶體上下文之間的上下文切換。每個過程都有其自己的變量的完整集合。
但是,在Linux中,如果不讨論線程(有時稱為輕量級程序),程序的抽象是不完整的。 根據定義,線程是流程中的執行上下文或執行流; 是以,每個程序至少包含一個線程。 包含多個執行線程的程序被稱為多線程程序。 一個程序中有多個線程可以進行目前程式設計,并且在多處理器系統上可以實作真正的并行性。
線程:則是某一程序中一路單獨運作的程式,也就是說,線程存在于程序之中。一個程序由一個或多個線程構成,各線程共享相同的代碼和全局資料,但各有其自己的堆棧。由于堆棧是每個線程一個,是以局部變量對每一線程來說是私有的。由于所有線程共享同樣的代碼和全局資料,它們比程序更緊密,比單獨的程序間更趨向于互相作用,線程間的互相作用更容易些,因為它們本身就有某些供通信用的共享記憶體:程序的全局資料。
線程是CPU使用率的基本機關,由程式計數器,堆棧和一組寄存器組成。執行線程是由計算機程式的分支分解為兩個或多個同時運作的任務而産生的。線程和程序的實作因一個作業系統而異,但在大多數情況下,線程包含在程序内部。多個線程可以存在于同一程序中并共享資源(例如記憶體),而不同程序則不共享這些資源。同一程序中的線程示例是自動拼寫檢查和寫入時自動儲存檔案。線程基本上是在相同記憶體上下文中運作的程序。線程在執行時可能共享相同的資料。線程圖,即單線程與多線程
任務:是最抽象的,是一個一般性的術語,指由軟體完成的一個活動。一個任務既可以是一個程序,也可以是一個線程。簡而言之,它指的是一系列共同達到某一目的的操作。與線程非常相似,不同之處在于它們通常不直接與OS互動。 像線程池一樣,任務不會建立自己的OS線程。 一個任務内部可能有一個線程,也可能沒有。例如,讀取資料并将資料放入記憶體中。這個任務可以作為一個程序來實作,也可以作為一個線程(或作為一個中斷任務)來實作。在RTOS中,一般會将排程的基本單元稱為任務,比如freeRTOS,ucos,embOS等,在RTOS中沒有程序的概念。
程序 | 線程 |
---|---|
程序是重量級的操作 | 線程是輕量級操作 |
每個程序都有自己的記憶體空間 | 線程共享它們所屬的程序的記憶體空間 |
程序間的通信速度很慢,因為程序具有不同的記憶體位址 | 線程間通信可能比程序間通信快,因為同一程序的線程與其所屬的程序共享記憶體 |
程序之間的上下文切換開銷大 | 在同一程序的線程之間進行上下文切換的開銷較低 |
程序不與其他程序共享記憶體 | 線程與同一程序的其他線程共享記憶體 |
程序間通訊機制:
- 管道(Pipe)及有名管道(named pipe):管道可用于具有親緣關系程序間的通信,有名管道克服了管道沒有名字的限制,是以,除具有管道所具有的功能外,它還允許無親緣關系程序間的通信;
- 信号(Signal):信号是比較複雜的通信方式,用于通知接受程序有某種事件發生,除了用于程序間通信外,程序還可以發送信号給程序本身;linux除了支援Unix早期信号語義函數sigal外,還支援語義符合Posix.1标準的信号函數 sigaction(實際上,該函數是基于BSD的,BSD為了實作可靠信号機制,又能夠統一對外接口,用sigaction函數重新實作了signal 函數);
- 封包(Message)隊列(消息隊列):消息隊列是消息的連結表,包括Posix消息隊列system V消息隊列。有足夠權限的程序可以向隊列中添加消息,被賦予讀權限的程序則可以讀走隊列中的消息。消息隊列克服了信号承載資訊量少,管道隻能承載無格式位元組流以及緩沖區大小受限等缺點。
- 共享記憶體:使得多個程序可以通路同一塊記憶體空間,是最快的可用IPC形式。是針對其他通信機制運作效率較低而設計的。往往與其它通信機制,如信号量結合使用,來達到程序間的同步及互斥。
- 信号量(semaphore):主要作為程序間以及同一程序不同線程之間的同步手段。
- 套接字(Socket):更為一般的程序間通信機制,可用于不同機器之間的程序間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支援套接字。
線程間的同步機制:為啥線程間沒有讨論通訊機制?因為同一程序内的線程共享程序的資源。那麼資源共享,則需要處理資源共享時的同步問題。
- 互斥鎖(mutex):通過鎖機制實作線程間的同步。同一時刻隻允許一個線程執行一個關鍵部分的代碼。這部分代碼常稱為臨界區。哪些可能是臨界區呢?簡言之,多個線程可能競争通路的資源。以下一些函數是互斥鎖的API函數。
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): 建立一些全局條件變量進行互斥通路控制。以下是其操作的基本接口函數:
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):如同程序一樣,線程也可以通過信号量來實作通信,其基本操作接口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);
程序在核心中如何描述?
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;
/* 前面是與排程密切相關的資訊添加在這之前 */
randomized_struct_fields_start
void *stack;
refcount_t usage;
/* Per task flags (PF_*), defined further below: */
unsigned int flags;
unsigned int ptrace;
.........
};
該結構非常大,集總抽象了程序的所有資訊,包括程序ID,狀态,父程序,子程序,同級,處理器寄存器,打開的檔案,位址空間等。系統使用循環雙向連結清單進行存儲 所有過程描述符。
像這樣的大型結構肯定會占用大量記憶體空間。 為每個程序提供較小的核心堆棧大小(可以使用編譯時選項進行配置,但預設情況下限制為一頁,即對于32位體系結構嚴格為4KB(一個頁),對于64位體系結構嚴格為8KB(兩個頁) –核心堆棧不具備增長或收縮),以這種浪費的方式使用資源并不是很友善。 是以,決定在堆棧中放置一個更簡單的結構,并帶有指向實際task_struct的指針,進而引申出thread_info。
抽象模組化思想看程序描述符
程序首先是作業系統對底層進行抽象而提供面向應用接口的一種抽象,而程序描述符則将底層資源、程序本身的排程從以下幾個大的方面進行進階别的抽象封裝:
- 應用程式資訊抽象
- 作業系統資源抽象
- 排程接口抽象
- 記憶體管理抽象
- 賬戶資訊抽象
- ......
通過預讀程序描述符,個人将程序描述相關資訊大緻分為以下幾個大類抽象:
涉及thread_info、優先級、棧、上下文切換、排程相關連結清單等關鍵資料。
- CPU相關抽象
涉及SMP多核處理抽象、CPUSET子系統相關、目前CPU等相關資料抽象。
- 保護機制抽象
- 緩存相關抽象
- 信号通信抽象
- 接口相關抽象
- 調試跟蹤抽象
- 安全機制抽象
- 資源管理抽象
- 雜項資訊抽象
最後附上些整理搜集到資料域的一些較詳細的介紹。
thread_info
該字段儲存特定于處理器的狀态資訊,并且是程序描述符的關鍵元素。具體定義在./arch/xxx/include/asm/thread_info.h中。
- entry.S需要立即通路此結構的低級任務資料應完全适合一個緩存行,此結構共享主管堆棧頁面
- 如果更改此結構的内容,則還必須更改彙編代碼。
- 因為thread_info包含了目前程序的指針,存儲在棧底或棧頂,取決于不同體系架構棧的增長方向,利用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
};
從書上和網上看到都是前面這樣描述的,但是對于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實作了程序存儲對描述符的引用以及如何通路它們。 但是,如果task_struct不是在核心堆棧内部,則task_struct到底位于記憶體中的什麼位置? 為此,Linux提供了一種特殊的記憶體管理機制,稱為SLUB層。SLUB動态生成task_struct,并把thread_info存在棧底或棧頂。
volatile long state
程序狀态,可取的程序狀态:
- TASK_RUNNING: 可執行态
- TASK_INTERRUPTIBLE:可中斷
- TASK_UNINTERRUPTIBLE:不可中斷
- __TASK_STOPPED:停止态
- __TASK_TRACED:被其他程序跟蹤的程序
為何用volatile修飾。 由于核心經常需要從不同位置更改程序的狀态,例如,如果在單個CPU硬體上同時将兩個程序設定為RUNNABLE。熟悉單片機程式設計的朋友一定知道,當在中斷函數中需要修改以及在中斷外部也會被修改的變量,就會使用到volatile修飾變量。
randomized_struct_fields_start
這是gcc的一個插件(插件來自于Grsecurity),其作用就是這之後的變量不會按照聲明順序存儲在記憶體中,而會按照一定的随機順序存放,這樣做是基于安全考慮,比如應用程式的程序描述符被劫持,如果按順序存放,則容易篡改其内容。
文章出自微信公衆号:嵌入式客棧,更多更新内容請關注,版權所有,嚴禁商用