一、行程描述符
行程控制塊PCB:是OS控制行程運行用的資料結構,是一個task_struct結構體,
PCB包括:行程標識資訊(行程識別符號PID等)、執行現場資訊(CPU現場,行程切換時需要保存現場資訊)、行程映像資訊(行程地址空間,即行程在運行時代碼、資料、堆疊放在什么位置,方便OS對地址空間進行管理)(現場與地址空間比較重要)、行程資源資訊、信號資訊,
對PCB,說其中幾個重要的欄位:
mm_struct:有一個成員mm,標明了行程的地址空間;
thread:記錄了行程的現場,最后一個欄位;
thread_info在4.4.6版本中改成了stack,包括內核堆疊(即行程進入內核作業時需要的堆疊和用戶堆疊是分開的)和一些需要快速訪問的資料,

在4.4.6版本中,stack占用了兩個頁面,即8k,大部分是放內核堆疊的,低端約10k存放快速訪問的資訊,CPU若想訪問當前行程的快速訪問資料的話,只需要拿到當前的堆疊指標,即ESP暫存器的值,可以推算出資料所在的位置來,因此在查找他的地址的時候,訪問速度可以很快,這部分資料可以看作是行程描述符的一部分,在空間上不是連續的,但相互之間有指標,可以相互找得到,

行程狀態轉換圖,可自行搜索,
在4.4.6中,增加了被跟蹤和僵死撤銷狀態,
行程描述符是管理行程的重要資料結構,故他的組織方式非常重要,0號行程的描述符是由init_task這個變數所存盤的,從他出發,所有行程描述符構成了雙向鏈表,task_struct中包含一個成員,叫tasts,tasks型別是list_head型別,tasts本身是嵌入在行程描述符里面的,知道tasks的地址,只要送減去620就能得到行程描述符的首地址,在Linux中有很多這樣的技巧,即通過嵌入的地址,反推結構體的地址,進而找到結構體的其他成員,

行程與執行緒關系
多個執行緒構成執行緒組,共享記憶體,不共享堆疊,
一個會話對應一個終端,在終端中敲一個命令相當于創建了一個行程組來執行,
下面進行演示,建立一個檔案命名為0.gdb,檔案內容如下,直接運行
1 target remote localhost:1234 2 dir ~/aos/lab/busybox 3 add-symbol-file ~/aos/lab/busybox/busybox_unstripped 0x8048400 4 display $lx_current().pid 5 display $lx_current().comm 6 b start_kernel 7 b ls_main 8 c

執行含有下列代碼的檔案
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <pthread.h> 4 5 void loop(){ 6 while(1); 7 } 8 9 void *p1(){ 10 printf("thread-1 starting\n"); 11 loop(); 12 } 13 14 void *p2(){ 15 printf("thread-2 starting\n"); 16 loop(); 17 } 18 19 void main(){ 20 int pid1, pid2; 21 pthread_t t1,t2; 22 void *thread_result; 23 24 printf("main starting\n"); 25 26 if (!(pid1 = fork())){ 27 printf("child-1 starting\n"); 28 loop(); 29 exit(0); 30 } 31 32 if (!(pid2 = fork())){ 33 printf("child-2 starting\n"); 34 loop(); 35 exit(0); 36 } 37 38 pthread_create(&t1, NULL, p1, NULL); 39 pthread_create(&t2, NULL, p2, NULL); 40 41 pthread_join(t1, &thread_result); 42 pthread_join(t2, &thread_result); 43 44 int status; 45 waitpid(pid1, &status, 0); 46 waitpid(pid2, &status, 0); 47 printf("main exiting\n"); 48 exit(0); 49 }
可以看到do-fork可執行檔案創建了三個行程,976、977、978


979、980是新創建的兩個執行緒

再執行一次,可以看到后臺運行了兩個

新創建的三個行程是剛創建的

fg %+序號將指定的行程放到前臺,ctrl+z放到后臺

用以下三條命令依次查看執行緒組、行程組和會話的leader
命令的意思是根據行程的描述符,找到執行緒組leader的描述符,里面對應的欄位就是要顯示的ID
p $lx_task_by_pid(977).group_leader->pids[0].pid->numbers.nr
p $lx_task_by_pid(977).group_leader->pids[1].pid->numbers.nr
p $lx_task_by_pid(977).group_leader->pids[2].pid->numbers.nr
987和988是984創建的,他們處于同一個執行緒組,leader是976

二、行程調度演算法
每個行程屬于某一個調度器類,每個調度器類都有一個行程佇列,不同的佇列有不同的調度演算法,
先調度硬實時的,軟實時次之,普通行程最后,
普通行程使用CFS(完全公平)調度演算法:
虛擬時鐘,調度器總是選時鐘最小的那個行程來執行,
優先級高的行程時鐘增長得慢,
所有可運行的行程被放在一個紅黑樹中,
下面進行演示:
再次運行0.gdb,在終端輸入ls,使其被捕獲

建立檔案demo-2-2.gdb,內容如下
1 break __schedule//行程調度的時候執行這個函式 2 3 break __switch_to//調度時如果切換行程就會呼叫這個函式 4 commands 5 printf "next_p->pid: %d\n", next_p->pid 6 printf "next_p->se.vruntime: " 7 print next_p->se.vruntime 8 end 9 10 break enqueue_task_fair//如果有新行程要進入到CFS佇列時,執行這個函式 11 commands 12 printf "p->pid: %d\n", p->pid 13 printf "p->se.vruntime: " 14 print p->se.vruntime 15 end 16 17 display $lx_current().state//顯示當前行程的狀態 18 display $lx_current().se.vruntime 19 display $lx_per_cpu("runqueues").nr_running//CPU里面有多少個行程在運行 20 21 display ((struct sched_entity *)((void *)$lx_per_cpu("runqueues").cfs.rb_leftmost - 0x8))->vruntime//CFS佇列里最左邊的節點,即虛擬時鐘最小的資訊 22 display ((struct task_struct *)((void *)$lx_per_cpu("runqueues").cfs.rb_leftmost - 0x4c))->pid

由上圖可以看到,當前正在運行的是975號行程,當前的虛擬時鐘可以從runtime那里看到,state=0表示其當前的狀態是就緒的或正在運行,樹最左邊目前還沒有行程,由于enqueue_task_fair函式的作用是往行程佇列里面加入新行程,現在已經有一個,可以看到,要加的是7號行程,下面一行的虛擬時鐘是個負值,現在還暫時看不到,繼續執行,

可以看到7號行程的虛擬時鐘小于975號的,下次如果要調度,應該選7號,即將創建的是3號行程,

由上圖,975號行程的虛擬時鐘增加了,在這兩個斷點時間,存在中斷,這才導致了時鐘的增加,運行有一定的隨機性,虛擬機在虛擬的時候有一定的隨機性,繼續

運行了切換函式,下一步要切換7號行程,繼續

7號時鐘的行程的時鐘比之前也增加了,需要注意當前正在運行的行程不放在樹里面,但放在了佇列里面,佇列里面行程就是樹里面的行程加當前行程,繼續若干次

下一步要創建的是4號行程,
除了普通行程有佇列之外,其他的硬實時和軟實時都有各自的佇列,
紅黑樹的某個節點可以是另外一個樹,共用一個時鐘,
三、行程調度的時機
內核程式的入口,系統呼叫總控函式,例外處理函式,中斷處理函式、內核執行緒主函式,用bt查看堆疊頂層,根據函式的種類來確定是哪種內核呼叫,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/125347.html
標籤:Linux
