從抽象的意義來講,進程是指一個正在運行的程序的實例,而線程是一個CPU指令執行流的最小單位。進程是操做系統資源分配的最小單位,線程是操做系統中調度的最小單位。從實現的角度上講,XV6系統中只實現了進程, 並無提供對線程的額外支持,一個用戶進程永遠只會有一個用戶可見的執行流。html
根據[1],進程管理的數據結構被叫作進程控制塊(Process Control Block, PCB)。一個進程的PCB必須存儲如下兩類信息:node
在XV6中,與進程有關的數據結構以下linux
// Per-process state struct proc { uint sz; // Size of process memory (bytes) pde_t* pgdir; // Page table char *kstack; // Bottom of kernel stack for this process enum procstate state; // Process state int pid; // Process ID struct proc *parent; // Parent process struct trapframe *tf; // Trap frame for current syscall struct context *context; // swtch() here to run process void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory char name[16]; // Process name (debugging) };
與前述的兩類信息的對應關係以下算法
操做系統管理進程有關的信息:內核棧kstack
,進程的狀態state
,進程的pid
,進程的父進程parent
,進程的中斷幀tf
,進程的上下文context
,與sleep
和kill
有關的chan
和killed
變量。shell
進程自己運行所須要的所有環境:虛擬內存信息sz
和pgdir
,打開的文件ofile
和當前目錄cwd
。windows
額外地,proc
中還有一條用於調試的進程名字name
。數組
在操做系統中,全部的進程信息struct proc
都存儲在ptable
中,ptable
的定義以下緩存
struct { struct spinlock lock; struct proc proc[NPROC]; } ptable;
除了互斥鎖lock
以外,一個值得注意的一點是XV6系統中容許同時存在的進程數量是有上限的。在這裏NPROC
爲64,因此XV6最多隻容許同時存在64個進程。安全
在proc.c
中,userinit()
用於建立第一個用戶進程,allocproc()
則被用於在ptable
中尋找空位並在空位上建立一個新的進程。當操做系統初始化時,經過userinit()
調用allocproc
建立第一個進程init
。絕大多數進程相關信息都會在這裏初始化。因爲XV6系統只容許中斷返回一種從內核態進入用戶態的方式,所以allocproc()
會建立中斷調用的棧結構,而userinit
會設置其中的值,彷彿是從一次真正的中斷裏返回進程同樣。最後,在mpmain()
中,系統調用schedule()
函數,開始用戶進程的調度。在init
進程被調度啓動後,會建立shell
進程,用於和用戶交互。數據結構
Linux系統的實現中並不刻意區分進程和線程,而是將其一律存儲在被稱做task_struct
的數據結構中。當兩個task_struct
共享同一個虛擬地址空間時,它們就是同一個進程的兩個線程。與Linux進程有關的數據結構定義大多數都在/include/linux/sched.h
中。task_struct
數據結構至關複雜,在32位機器上一條能佔據1.7KiB的空間。task_struct
中主要包含的數據結構有管理處理器底層信息的thread_struct
、管理虛擬內存的mm_struct
、管理文件描述符的file_struct
、管理信號的signal_struct
等等。Linux中的進程與XV6同樣都有獨立的內核棧,內核模式下的代碼是在內核棧中運行的。
操做系統維護多個task_struct
隊列來實現不一樣的功能。全部的隊列都是用雙向鏈表實現的。有一個隊列存放了全部的進程;另外一個隊列存放了全部正在運行的進程(kernel/sched.c
中的struct runqueue
);此外,對於每個會致使進程掛起的等待事件,都有一個隊列存放由於等待此事件而掛起的進程(include/linux/wait.h
中的wait_queue_t
)。
Linux會將task_struct
數據結構分配到這個進程的內核棧的頂部,將thread_info
數據結構分配到這個進程的內核棧的底部。thread_info
的名稱有些誤導,它存儲的實際上是一個task
中更加底層和更加體系結構相關的屬性。進程數據結構的分配方法被稱爲Slab Allocator,經過精心優化的虛擬內存機制來提高進程管理的效率、實現對象重用。
struct thread_info { struct task_struct *task; struct exec_domain *exec_domain; __u32 flags; __u32 status; __u32 cpu; int preempt_count; mm_segment_t addr_limit; struct restart_block restart_block; void *sysenter_return; int uaccess_err; };
在Windows NT之後的Windows系統中,進程用EPROCESS
對象表示,線程用ETHREAD
對象表示。在一個EPROCESS
對象中,包含了進程的資源相關信息,好比句柄表、虛擬內存、安全、調試、異常、建立信息、I/O轉移統計以及進程計時等。每一個EPROCESS
對象都包含一個指向ETHREAD
結構體的鏈表。值得一提的是Windows系統中EPROCESS
和ETHREAD
的設計都是分層的,KPROCESS
和KTHREAD
成員對象專門用來處理體系結構有關的細節,而Process Environment Block和Thead Environment Block對象則暴露給應用程序來訪問。
在大多數教科書使用的標準五狀態進程模型中,進程分爲New、Ready、Running、Waiting和Terminated五個狀態,狀態轉換圖如圖所示(圖出自Operating System Concepts, 7th Edition)
除去標記進程塊未被使用的UNUSED
狀態,XV6操做系統中的狀態與上述的五狀態模型徹底對應。在XV6中這五個狀態的定義爲
enum procstate { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
XV6實現中源代碼中具體的轉換關係以下
這個轉換關係圖中,標識出了在XV6系統中發生狀態轉換所須要的函數或者事件。
Linux中的進程轉換圖與Xv6大體相同,可是有以下區別:
TASK_TRACED
和TASK_STOPPED
。關於Windows的進程狀態,網上並無關於實現細節的特別詳細的解釋。一份關於Windows進程的文檔[6]使用了以下的進程轉換圖,但並無顯式地說明其與Windows進程實現之間的關係。
從中能夠看出,Windows進程可能額外多出了Suspend狀態,用於如下狀況的一種:
我認爲操做系統設計這些狀態,是出於在有限的計算機系統資源上,對管理和調度多個進程的需求。若是一個CPU核同一時間只會有一個進程運行,那就徹底不須要設置進程的狀態。可是一個實用的現代操做系統必須支持大量進程共享一個CPU,也必須支持進程的不斷建立與終止,從而實現資源利用效率的最大化和系統功能的多樣化。所以,操做系統選取了如今的五狀態進程設計,而且出於不一樣系統的需求不一樣,也會有更加細化的設計。
Xv6系統中的進程可使用fork()
系統調用建立新進程。爲了建立一個進程,操做系統必須爲這個進程分配相應的資源,包括內存、CPU時間、文件等,與此同時,操做系統必須對此進程作出相應的管理,包括設置它的進程ID、調度優先級、虛擬內存結構、運行資源限制等等。爲了可以維持多個進程在一個CPU上運行,必須對此作出相應的調度。調度算法有不少種,由簡到難以下
一個現代操做系統所使用的調度算法一般是Priority Based Multilevel Queue的一種變體。具體地說,根據操做系統的具體需求,將不一樣類別的進程賦予不一樣的優先級。好比,Windows系統中,用戶當前使用的窗體進程具備很是高的優先級。對於每個優先級內的進程都會維護一個獨自的隊列,每一個優先級可使用不一樣的調度算法。高優先級的前臺進程可使用Round-Robin,後臺進程可使用First Come First Served。若是一個進程好久沒有獲得執行,那麼能夠提高它的優先級,從低優先級隊列進入高優先級隊列,從而避免飢餓的問題。
通常而言,出於CPU資源的限制和操做系統內核空間的內存限制,操做系統會指定容許同時存在的最大進程數。在Xv6系統中,最多同時存在64個進程。操做系統會維護一個大小爲64的struct proc
數組,並在其中分配新的進程。
進程的上下文包含了這個進程執行時所須要的所有信息,主要是寄存器的值和運行時棧。在Xv6系統中,執行進程的上下文切換就意味着要保存原進程的調用保存寄存器 %ebp %ebx %esi %ebp
,棧指針%esp
和程序指針eip
,並載入新的進程的上述寄存器。特別地,Xv6中的進程切換隻會切換到內核調度器進程,並經過內核調度器切換到新的進程。
關於進程調度的具體細節,官方文檔具備精彩的描述,在此再也不贅述。
多進程和多CPU之間的關係在於,在操做系統面前,每一個進程都好似佔用了一個獨立的虛擬CPU,但事實上操做系統會將多個進程分配在一個或多個CPU上運行,進程的數量與CPU的數量之間並無直接的關係。
內核態進程,顧名思義,是在操做系統內核態下執行的進程。在內核態下運行的進程通常用於完成操做系統最底層,最爲核心,沒法在用戶態下完成的功能。好比,調度器進程是Xv6中的一個內核態進程,由於在用戶態下是沒法進行進程調度的。相比較而言,用戶態進程用於完成的功能能夠多種多樣,而且其功能只依賴於操做系統提供的系統調用,不須要深刻操做內核的數據結構。好比init進程和shell進程就是xv6中的用戶態進程。
Xv6進程在虛擬內存中的佈局如上圖。固然,其中的每一頁在物理內存中大機率並非這樣排列的,可是虛擬內存系統爲每一個進程提供了統一的內存抽象。進程的棧用於存放運行時數據和運行時軌跡,包含了函數調用的嵌套,函數調用的參數和臨時變量的存儲等。棧一般較小,不會在運行時增加,不適合存儲大量數據。相比較而言,堆提供了一個存放全局變量和動態增加的數據的機制。堆的大小一般能夠動態增加,而且通常用於存儲較大的數據和程序執行過程當中始終會被訪問的全局變量。
fork()
函數// Create a new process copying p as the parent. // Sets up stack to return as if from system call. // Caller must set state of returned proc to RUNNABLE. int fork(void) { int i, pid; struct proc *np; struct proc *curproc = myproc(); // Allocate process. if((np = allocproc()) == 0){ return -1; } // Copy process state from proc. if((np->pgdir = copyuvm(curproc->pgdir, curproc->sz)) == 0){ kfree(np->kstack); np->kstack = 0; np->state = UNUSED; return -1; } np->sz = curproc->sz; np->parent = curproc; *np->tf = *curproc->tf; // Clear %eax so that fork returns 0 in the child. np->tf->eax = 0; for(i = 0; i < NOFILE; i++) if(curproc->ofile[i]) np->ofile[i] = filedup(curproc->ofile[i]); np->cwd = idup(curproc->cwd); safestrcpy(np->name, curproc->name, sizeof(curproc->name)); pid = np->pid; acquire(&ptable.lock); np->state = RUNNABLE; release(&ptable.lock); return pid; }
fork()
函數的代碼如上。fork()
函數首先調用allocproc()
函數得到並初始化一個進程控制塊struct proc
(12-14行)。此外,在allocproc()
函數中還會對進程的內核棧進行初始化,在內核棧裏設置一個Trap Frame,把Trap Frame的上下文部分都置爲0。而後,fork()
函數使用copyuvm()
函數複製原進程的虛擬內存結構(17-24行)。爲了能讓子進程返回時處在和父進程如出一轍的狀態,Trap Frame也會被拷貝(25行,須要注意這裏的運算符優先級)。爲了讓子進程系統調用的返回值爲0,子進程的eax
寄存器會被置爲0(28行)。而後,父進程打開的文件描述符會被所有拷貝給子進程(30-32行),還有父進程所處於的目錄(33行)。這些操做都會增長文件描述符和目錄的被引用數。最後,fork()
函數拷貝了父進程的名字,設置了子進程的狀態爲RUNNABLE
,而後返回子進程pid
給父進程。子進程被建立後,在某個時刻調度子進程運行時,fork()
函數會第二次返回給子進程,此時返回值爲0。
wait()
函數// Wait for a child process to exit and return its pid. // Return -1 if this process has no children. int wait(void) { struct proc *p; int havekids, pid; struct proc *curproc = myproc(); acquire(&ptable.lock); for(;;){ // Scan through table looking for exited children. havekids = 0; for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->parent != curproc) continue; havekids = 1; if(p->state == ZOMBIE){ // Found one. pid = p->pid; kfree(p->kstack); p->kstack = 0; freevm(p->pgdir); p->pid = 0; p->parent = 0; p->name[0] = 0; p->killed = 0; p->state = UNUSED; release(&ptable.lock); return pid; } } // No point waiting if we don't have any children. if(!havekids || curproc->killed){ release(&ptable.lock); return -1; } // Wait for children to exit. (See wakeup1 call in proc_exit.) sleep(curproc, &ptable.lock); //DOC: wait-sleep } }
wait()
函數的代碼如上。wait()
函數首先必需要得到ptable
的鎖(10行),由於它有可能會對ptable
作出修改。而後它會遍歷ptable
,從中尋找本身的子進程(14-32行)。若是發現殭屍子進程,就把殭屍子進程回收,具體地說要回收它的虛擬內存,內核棧,並設置狀態爲UNUSED
(18-30行),有趣的是,在這裏wait()
函數根本沒有回收這個子進程打開的文件描述符,由於在exit()
函數內這個進程打開的文件描述符已經所有被關閉了,並且只有exit()
以後的進程纔多是ZOMBIE
狀態。對於沒有子進程的狀況,wait()
會直接返回,不然他會調用sleep()
,並傳入ptable
的鎖做爲參數。之因此要在sleep
函數中傳入ptable
鎖,是爲了不在wait()
把進程設置爲SLEEP
狀態以前,子進程就已經成爲僵死進程並在exit()
函數中調用了wakeup()
,這會使得父進程接收不到wakeup
從而進入死鎖狀態。
exit()
函數// Exit the current process. Does not return. // An exited process remains in the zombie state // until its parent calls wait() to find out it exited. void exit(void) { struct proc *curproc = myproc(); struct proc *p; int fd; if(curproc == initproc) panic("init exiting"); // Close all open files. for(fd = 0; fd < NOFILE; fd++){ if(curproc->ofile[fd]){ fileclose(curproc->ofile[fd]); curproc->ofile[fd] = 0; } } begin_op(); iput(curproc->cwd); end_op(); curproc->cwd = 0; acquire(&ptable.lock); // Parent might be sleeping in wait(). wakeup1(curproc->parent); // Pass abandoned children to init. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->parent == curproc){ p->parent = initproc; if(p->state == ZOMBIE) wakeup1(initproc); } } // Jump into the scheduler, never to return. curproc->state = ZOMBIE; sched(); panic("zombie exit"); }
exit()
函數首先關閉這個進程打開的全部文件描述符(15-20行),而後除去本身對所處的文件目錄的引用(22-25行),對文件管理相關數據結構的訪問必需要得到和釋放相關的鎖(begin_op()
和end_op()
)。清除這些引用能夠容許文件系統管理當前的緩存。若是這個進程的父進程正在等待子進程結束,那麼這個進程必須喚醒父進程(30行),只有這樣父進程纔可以在某個時刻回收殭屍子進程。若是這個進程有子進程的話,就把這個進程的子進程都傳給init
進程,並由init
進程來負責回收殭屍子進程(33-39行)。最後,這個進程的狀態會被設置爲ZOMBIE
,調度器調度其餘進程運行(42-44行)。
Operating System Concepts, 7th Edition
Computer Systems: a Programmer's Perspective, 3rd Edition
Process in Linux, https://www.cs.columbia.edu/~junfeng/10sp-w4118/lectures/l07-proc-linux.pdf
10 Things Every Linux Programmer Should Know, http://www.mulix.org/lectures/kernel_workshop_mar_2004/things.pdf
Introduction to Linux Kernel, Chapter 3 Process Management, https://notes.shichao.io/lkd/ch3/#chapter-3-process-management
A Complete Introduction to Windows Processes, Threads and Related Resources, https://www.tenouk.com/ModuleT.html
Windows進程數據結構及建立流程,https://blog.csdn.net/cuit/article/details/9200097