一、進程描述符 進程式控制制塊PCB:是OS控制進程運行用的數據結構,是一個task_struct結構體。 PCB包括:進程標識信息(進程標識符PID等)、執行現場信息(CPU現場,進程切換時需要保存現場信息)、進程映像信息(進程地址空間,即進程在運行時代碼、數據、棧放在什麼位置,方便OS對地址空間進行 ...
一、進程描述符
進程式控制制塊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查看棧頂層,根據函數的種類來確定是哪種內核調用。