Linux內核通過一個被稱為進程描述符的 結構體來管理進程,這個結構體包含了一個進程所需的所有信息。它定義在 文件中。 談到 結構體,可以說她是linux內核源碼中最複雜的一個結構體了,成員之多,占用記憶體之大。 進程狀態 5個互斥狀態 狀態 | 描述 |: : TASK_RUNNING | 表示進程 ...
Linux內核通過一個被稱為進程描述符的task_struct
結構體來管理進程,這個結構體包含了一個進程所需的所有信息。它定義在include/linux/sched.h
文件中。
談到task_struct
結構體,可以說她是linux內核源碼中最複雜的一個結構體了,成員之多,占用記憶體之大。
進程狀態
/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_DEAD 16
#define EXIT_ZOMBIE 32
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128 /** wake on signals that are deadly **/
#define TASK_WAKING 256
#define TASK_PARKED 512
#define TASK_NOLOAD 1024
#define TASK_STATE_MAX 2048
/* Convenience macros for the sake of set_task_state */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)
5個互斥狀態
狀態 | 描述 |
---|---|
TASK_RUNNING | 表示進程要麼正在執行,要麼正要準備執行(已經就緒),正在等待cpu時間片的調度 |
TASK_INTERRUPTIBLE | 進程因為等待一些條件而被掛起(阻塞)而所處的狀態。這些條件主要包括:硬中斷、資源、一些信號……,一旦等待的條件成立,進程就會從該狀態(阻塞)迅速轉化成為就緒狀態TASK_RUNNING |
TASK_UNINTERRUPTIBLE | 意義與TASK_INTERRUPTIBLE類似,除了不能通過接受一個信號來喚醒以外,對於處於TASK_UNINTERRUPIBLE狀態的進程,哪怕我們傳遞一個信號或者有一個外部中斷都不能喚醒他們。只有它所等待的資源可用的時候,他才會被喚醒。這個標誌很少用,但是並不代表沒有任何用處,其實他的作用非常大,特別是對於驅動刺探相關的硬體過程很重要,這個刺探過程不能被一些其他的東西給中斷,否則就會讓進城進入不可預測的狀態 |
TASK_STOPPED | 進程被停止執行,當進程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信號之後就會進入該狀態 |
TASK_TRACED | 表示進程被debugger等進程監視,進程執行被調試程式所停止,當一個進程被另外的進程所監視,每一個信號都會讓進城進入該狀態 |
2個終止狀態
其實還有兩個附加的進程狀態既可以被添加到state域中,又可以被添加到exit_state域中。只有當進程終止的時候,才會達到這兩種狀態.
/* task state */
int exit_state;
int exit_code, exit_signal;
狀態 | 描述 |
---|---|
EXIT_ZOMBIE | 進程的執行被終止,但是其父進程還沒有使用wait()等系統調用來獲知它的終止信息,此時進程成為僵屍進程 |
EXIT_DEAD | 進程的最終狀態 |
而int exit_code
, exit_signal
;我們會在後面進程介紹
新增睡眠狀態
如前所述,進程狀態 TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE 都是睡眠狀態。現在,我們來看看內核如何將進程置為睡眠狀態。
內核如何將進程置為睡眠狀態
Linux 內核提供了兩種方法將進程置為睡眠狀態。
將進程置為睡眠狀態的普通方法是將進程狀態設置為 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 並調用調度程式的 schedule() 函數。這樣會將進程從 CPU 運行隊列中移除。
如果進程處於可中斷模式的睡眠狀態(通過將其狀態設置為 TASK_INTERRUPTIBLE),那麼可以通過顯式的喚醒呼叫(wakeup_process())或需要處理的信號來喚醒它。
但是,如果進程處於非可中斷模式的睡眠狀態(通過將其狀態設置為 TASK_UNINTERRUPTIBLE),那麼只能通過顯式的喚醒呼叫將其喚醒。除非萬不得已,否則我們建議您將進程置為可中斷睡眠模式,而不是不可中斷睡眠模式(比如說在設備 I/O 期間,處理信號非常困難時)。
當處於可中斷睡眠模式的任務接收到信號時,它需要處理該信號(除非它已被屏弊),離開之前正在處理的任務(此處需要清除代碼),並將 -EINTR 返回給用戶空間。再一次,檢查這些返回代碼和採取適當操作的工作將由程式員完成。
因此,懶惰的程式員可能比較喜歡將進程置為不可中斷模式的睡眠狀態,因為信號不會喚醒這類任務。
但需要註意的一種情況是,對不可中斷睡眠模式的進程的喚醒呼叫可能會由於某些原因不會發生,這會使進程無法被終止,從而最終引發問題,因為惟一的解決方法就是重啟系統。一方面,您需要考慮一些細節,因為不這樣做會在內核端和用戶端引入 bug。另一方面,您可能會生成永遠不會停止的進程(被阻塞且無法終止的進程)。
現在,我們在內核中實現了一種新的睡眠方法
Linux Kernel 2.6.25 引入了一種新的進程睡眠狀態,
狀態 | 描述 |
---|---|
TASK_KILLABLE | 當進程處於這種可以終止的新睡眠狀態中,它的運行原理類似於 TASK_UNINTERRUPTIBLE,只不過可以響應致命信號 |
它定義如下:
#define TASK_WAKEKILL 128 /** wake on signals that are deadly **/
/* Convenience macros for the sake of set_task_state */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)
換句話說,TASK_UNINTERRUPTIBLE + TASK_WAKEKILL = TASK_KILLABLE。
而TASK_WAKEKILL 用於在接收到致命信號時喚醒進程
新的睡眠狀態允許 TASK_UNINTERRUPTIBLE 響應致命信號
進程狀態的切換過程和原因大致如下圖
進程標識符(PID)
pid_t pid;
pid_t tgid;
Unix系統通過pid來標識進程,linux把不同的pid與系統中每個進程或輕量級線程關聯,而unix程式員希望同一組線程具有共同的pid,遵照這個標準linux引入線程組的概念。一個線程組所有線程與領頭線程具有相同的pid,存入tgid欄位,getpid()返回當前進程的tgid值而不是pid的值。
在CONFIG_BASE_SMALL配置為0的情況下,PID的取值範圍是0到32767,即系統中的進程數最大為32768個。
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)
在Linux系統中,一個線程組中的所有線程使用和該線程組的領頭線程(該組中的第一個輕量級進程)相同的PID,並被存放在tgid成員中。只有線程組的領頭線程的pid成員才會被設置為與tgid相同的值。註意,getpid()系統調用返回的是當前進程的tgid值而不是pid值。
進程內核棧
void *stack;
內核棧與線程描述符
對每個進程,Linux內核都把兩個不同的數據結構緊湊的存放在一個單獨為進程分配的記憶體區域中;
- 一個是內核態的進程堆棧
- 另一個是緊挨著進程描述符的小數據結構thread_info,叫做線程描述符。
Linux把thread_info(線程描述符)和內核態的線程堆棧存放在一起,這塊區域通常是8192K(占兩個頁框),其實地址必須是8192的整數倍。
在linux/arch/x86/include/asm/page_32_types.h中,
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
出於效率考慮,內核讓這8K空間占據連續的兩個頁框並讓第一個頁框的起始地址是213的倍數。
內核態的進程訪問處於內核數據段的棧,這個棧不同於用戶態的進程所用的棧。
用戶態進程所用的棧,是在進程線性地址空間中;
而內核棧是當進程從用戶空間進入內核空間時,特權級發生變化,需要切換堆棧,那麼內核空間中使用的就是這個內核棧。因為內核控制路徑使用很少的棧空間,所以只需要幾千個位元組的內核態堆棧。
需要註意的是,內核態堆棧僅用於內核常式,Linux內核另外為中斷提供了單獨的硬中斷棧和軟中斷棧
下圖中顯示了在物理記憶體中存放兩種數據結構的方式。線程描述符駐留與這個記憶體區的開始,而棧頂末端向下增長。 下圖摘自ULK3,進程內核棧與進程描述符的關係如下圖:
但是較新的內核代碼中,進程描述符task_struct結構中沒有直接指向thread_info結構的指針,而是用一個void指針類型的成員表示,然後通過類型轉換來訪問thread_info結構。
相關代碼在include/linux/sched.h中
#define task_thread_info(task) ((struct thread_info *)(task)->stack)
在這個圖中,esp寄存器是CPU棧指針,用來存放棧頂單元的地址。在80x86系統中,棧起始於頂端,並朝著這個記憶體區開始的方向增長。從用戶態剛切換到內核態以後,進程的內核棧總是空的。因此,esp寄存器指向這個棧的頂端。一旦數據寫入堆棧,esp的值就遞減。
內核棧數據結構描述thread_info和thread_union
thread_info是體繫結構相關的,結構的定義在thread_info.h中
Linux內核中使用一個聯合體來表示一個進程的線程描述符和內核棧:
union thread_union
{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
獲取當前在CPU上正在運行進程的thread_info
下麵來說說如何通過esp棧指針來獲取當前在CPU上正在運行進程的thread_info結構。
實際上,上面提到,thread_info結構和內核態堆棧是緊密結合在一起的,占據兩個頁框的物理記憶體空間。而且,這兩個頁框的起始起始地址是213對齊的。
早期的版本中,不需要對64位處理器的支持,所以,內核通過簡單的屏蔽掉esp的低13位有效位就可以獲得thread_info結構的基地址了。
我們在下麵對比了,獲取正在運行的進程的thread_info的實現方式
架構 | 版本 | 定義鏈接 | 實現方式 | 思路解析 |
---|---|---|---|---|
x86 | 3.14 | current_thread_info(void) | return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); | 屏蔽了esp的低十三位,最終得到的是thread_info的地址 |
x86 | 3.15 | current_thread_info(void) | ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE); | |
x86 | 4.1 | current_thread_info(void) | (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); |
早期版本
當前的棧指針(current_stack_pointer == sp)就是esp,
THREAD_SIZE為8K,二進位的表示為0000 0000 0000 0000 0010 0000 0000 0000。
~(THREAD_SIZE-1)的結果剛好為1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全為零,也就是剛好屏蔽了esp的低十三位,最終得到的是thread_info的地址。
進程最常用的是進程描述符結構task_struct而不是thread_info結構的地址。為了獲取當前CPU上運行進程的task_struct結構,內核提供了current巨集,由於task_struct *task在thread_info的起始位置,該巨集本質上等價於current_thread_info()->task,在include/asm-generic/current.h中定義:
#define get_current() (current_thread_info()->task)
#define current get_current()
分配和銷毀thread_info
進程通過alloc_thread_info_node
函數分配它的內核棧,通過free_thread_info
函數釋放所分配的內核棧。
# if THREAD_SIZE >= PAGE_SIZE
static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
int node)
{
struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP,
THREAD_SIZE_ORDER);
return page ? page_address(page) : NULL;
}
static inline void free_thread_info(struct thread_info *ti)
{
free_kmem_pages((unsigned long)ti, THREAD_SIZE_ORDER);
}
# else
static struct kmem_cache *thread_info_cache;
static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
int node)
{
return kmem_cache_alloc_node(thread_info_cache, THREADINFO_GFP, node);
}
static void free_thread_info(struct thread_info *ti)
{
kmem_cache_free(thread_info_cache, ti);
}
進程標記
unsigned int flags; /* per process flags, defined below */
反應進程狀態的信息,但不是運行狀態,用於內核識別進程當前的狀態,以備下一步操作
flags成員的可能取值如下,這些巨集以PF(ProcessFlag)開頭
參見
http://lxr.free-electrons.com/source/include/linux/sched.h?v4.5#L2083
例如
PF_FORKNOEXEC 進程剛創建,但還沒執行。
PF_SUPERPRIV 超級用戶特權。
PF_DUMPCORE dumped core。
PF_SIGNALED 進程被信號(signal)殺出。
PF_EXITING 進程開始關閉。
/*
* Per process flags
*/
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_EXITPIDONE 0x00000008 /* pi exit done on shut down */
#define PF_VCPU 0x00000010 /* I'm a virtual CPU */
#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_MCE_PROCESS 0x00000080 /* process policy on mce errors */
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_NPROC_EXCEEDED 0x00001000 /* set_user noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */
#define PF_USED_ASYNC 0x00004000 /* used async_schedule*(), used by module init */
#define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */
#define PF_FROZEN 0x00010000 /* frozen for system suspend */
#define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */
#define PF_KSWAPD 0x00040000 /* I am kswapd */
#define PF_MEMALLOC_NOIO 0x00080000 /* Allocating memory without IO involved */
#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */
#define PF_KTHREAD 0x00200000 /* I am a kernel thread */
#define PF_RANDOMIZE 0x00400000 /* randomize virtual address space */
#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */
#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_allowed */
#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */
#define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */
#define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK 0x80000000 /* this thread called freeze_processes and should not be frozen */
表示進程親屬關係的成員
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
在Linux系統中,所有進程之間都有著直接或間接地聯繫,每個進程都有其父進程,也可能有零個或多個子進程。擁有同一父進程的所有進程具有兄弟關係。
欄位 | 描述 |
---|---|
real_parent | 指向其父進程,如果創建它的父進程不再存在,則指向PID為1的init進程 |
parent | 指向其父進程,當它終止時,必須向它的父進程發送信號。它的值通常與real_parent相同 |
children | 表示鏈表的頭部,鏈表中的所有元素都是它的子進程 |
sibling | 用於把當前進程插入到兄弟鏈表中 |
group_leader | 指向其所在進程組的領頭進程 |