1. 程式人生 > >Linux系統中進程的創建

Linux系統中進程的創建

linux 進程 pid

1.Linux中的進程

進程是程序執行的一個實例,也是系統資源調度的最小單位。如果同一個程序被多個用戶同時運行,那麽這個程序就有多個相對獨立的進程,與此同時他們又共享相同的執行代碼,在Linux系統中進程的概念類似於任務或者線程(task & threads.

進程是一個程序運行時候的一個實例實際上說的是它就是一個可以充分描述程序以達到了其可以運行狀態的的一個數據和代碼集合。一個進程會被產生並會復制出自己的子代,類似細胞分裂一樣。從系統的角度來看進程的任務實際上就是擔當承載系統資源的單位,系統在調度和分配資源的時候也會以他們作為基本單位開始進行分配。(系統中的資源很多例如

CPU的時間片、內存堆棧等等)

系統是通過相應的數據結構去表示每一個進程以及他們的擴展數據結構,實際上這個結構就是我們所說的PCB,在Linux這個數據結構我們呢稱為task_struct,實際上應該至少包含以下信息,比如優先級,運行狀態,所在的內存空間,文件訪問權限等等。

Task_struct的函數結構如下所示:

技術分享圖片


2.進程的狀態

進程執行時,會根據具體情況改變狀態,進程狀態是調度和對換的一句,Linux中的進程主要有下面的幾種狀態技術分享圖片


1)運行態:進程正在使用CPU運行的狀態。處於運行態的進程又稱為當前進程(current process)。

2)可運行態:進程已分配到除CPU外所需要的其它資源,等待系統把

CPU分配給它之後即可投入運行。

3)等待態:又稱睡眠態,它是進程正在等待某個事件或某個資源時所處的狀態。 等待態進一步分為可中斷的等待態和不可中斷的等待態。處於可中斷等待態的進程可以由信號(signal)解除其等待態。處於不可中斷等待態的進程,一般是直接或間接等待硬件條件。 它只能用特定的方式來解除,例如使用喚醒函數wake_up()等。

可中斷的等待狀態:進程被掛起,直到等到一個可以喚醒他的東西,例如一個硬件中斷、某項系統資源、或一個信號量。當它等到這 些喚醒條件的之後就會進入可運行狀態。

不可中斷的等待:一種常見的狀態就是這個進程正在訪問一個獨占的臨界資源,這種時候處於一種不可搶占的狀態。通常當進程接收 到

SIGSTOPSIGTSTPSIGTTIN SIGTTOU信號後就處於這種狀態。例如,正接受調試的進程就處於這種狀態。

4)暫停態:進程需要接受某種特殊處理而止運行所處的狀態。通常進程在接受到外部進程的某個信號進入暫停態,例如,正在接受調試的進程就處於這種狀態。

5)僵死狀態

進程雖然已經終止,但由於某種原因,父進程還沒有執行wait()系統調用,終止進程的信息也還沒有回收。顧名思義,處於該狀態的進程就是死進程,這種進程實際上是系統中的垃圾,必須進行相應處理以釋放其占用的資源。

我們在設置這些狀態的時候是可以直接用語句進行的比如:p>state = TASK_RUNNING。同時內核也會使用set_task_stateset_current_state

3.進程的創建

可以使用fork,vfork,clone三個系統調用來創建一個新的進程,而且都死通過do_fork實現的。

1.子進程被創建後繼承了父進程的資源。

2.子進程共享父進程的虛存空間。

3.寫時拷貝 (copy on write):子進程在創建後共享父進程的虛存內存空間,寫時拷貝技術允許父子進程能讀相同的物理頁。只要兩者有一個進程試圖寫一個物理頁,內核就把這個頁的內容拷貝到一個新的物理 頁,並把這個新的物理頁分配給正在寫的進程

4.子進程在創建後執行的是父進程的程序代碼。

負責創建進程的函數的層次結構如下:


fork()函數創建新進程是通過下列一系列函數實現的:

fork()->sys_clone()->do_fork()->copy_process()->dup_task_struct()->copy_thread()->ret_from_fork()

do_fork 函數首先會分配一個新的 PID(但是我還沒找到該調用)。接下來,do_fork 檢查調試器是否在跟蹤父進程。如果是,在 clone_flags 內設置 CLONE_PTRACE 標誌以做好執行 fork 操作的準備。

之後 do_fork 函數還會調用 copy_process,向其傳遞這些標誌、堆棧、註冊表、父進程以及最新分配的 PID

新的進程在 copy_process 函數內作為父進程的一個副本創建。此函數能執行除啟動進程之外的所有操作,啟動進程在之後進行處理。copy_process 內的第一步是驗證 CLONE 標誌以確保這些標誌是一致的。如果不一致,就會返回 EINVAL 錯誤。接下來,詢問 Linux Security Module (LSM) 看當前任務是否可以創建一個新任務。

接下來,調用 dup_task_struct 函數(在 ./linux/kernel/fork.c 內),這會分配一個新 task_struct 並將當前進程的描述符復制到其內。在新的線程堆棧設置好後,一些狀態信息也會被初始化,並且會將控制返回給 copy_process。控制回到 copy_process 後,除了其他幾個限制和安全檢查之外,還會執行一些常規管理,包括在新 task_struct 上的各種初始化。

部分dup_task_struct源碼如下:

//dup_task_struct根據父進程創建子進程內核棧和進程描述符:

static struct task_struct *dup_task_struct(struct task_struct *orig)

{

struct task_struct *tsk;

struct thread_info *ti;

int node = tsk_fork_get_node(orig);

int err;

/*創建進程描述符對象*/

tsk = alloc_task_struct_node(node);

if (!tsk)

return NULL;

/*給新進程分配一個新的內核堆棧*/

ti = alloc_thread_info_node(tsk, node);

if (!ti) /*如果thread info結構沒申請到,釋放tsk*/

goto free_tsk;

/*復制task_struct,使子進程描述符和父進程一致*/

err = arch_dup_task_struct(tsk, orig);

if (err)

goto free_ti;

tsk->stack = ti; /*task對應棧*/

#ifdef CONFIG_SECCOMP

tsk->seccomp.filter = NULL;

#endif

/*初始化thread info結構*/

setup_thread_stack(tsk, orig);//使子進程thread_info內容與父進程一致但task指向子進程task_struct

clear_user_return_notifier(tsk);

clear_tsk_need_resched(tsk);

set_task_stack_end_magic(tsk);

#ifdef CONFIG_CC_STACKPROTECTOR

tsk->stack_canary = get_random_int();

/*初始化stack_canary變量*/

.......

之後,會調用一系列復制函數來復制此進程的各個方面,比如復制開放文件描述符(copy_files)、復制符號信息(copy_sighand copy_signal)、復制進程內存(copy_mm)以及最終復制線程(copy_thread)。

最終復制線程(copy_thread)部分源碼

//新進程有自己的堆棧且會根據task_pt_regs中的內容進行修改。

int copy_thread(unsigned long clone_flags,

unsigned long sp, unsigned long arg,

struct task_struct *p)

{

struct pt_regs *childregs = task_pt_regs(p);

struct task_struct *tsk;

int err;

//調度到子進程時的內核棧頂

p->thread.sp = (unsigned long) childregs;

p->thread.sp0 = (unsigned long) (childregs+1);

memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

if (unlikely(p->flags & PF_KTHREAD))

{

/* kernel thread */

memset(childregs, 0, sizeof(struct pt_regs));

p->thread.ip =(unsignedlong)ret_from_kernel_thread;

task_user_gs(p) = __KERNEL_STACK_CANARY;

childregs->ds = __USER_DS;

childregs->es = __USER_DS;

childregs->fs = __KERNEL_PERCPU;

childregs->bx = sp; /* function */

childregs->bp = arg;

childregs->orig_ax = -1;

childregs->cs = __KERNEL_CS | get_kernel_rpl(); childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;

p->thread.io_bitmap_ptr = NULL;

return 0;

}

*childregs = *current_pt_regs();//復制內核堆棧

childregs->ax = 0;//eax寄存器值強置為0,即子進程返回到用戶態時返回值為0

if (sp)

childregs->sp = sp;//sp為父進程傳給子進程的用戶態棧,可以與父進程共享

p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址

task_user_gs(p) = get_user_gs(current_pt_regs());

p->thread.io_bitmap_ptr = NULL;

tsk = current;

之後,這個新任務會被指定給一個處理程序,同時對允許執行進程的處理程序進行額外的檢查(cpus_allowed)。新進程的優先級從父進程的優先級繼承後,執行一小部分額外的常規管理,而且控制也會被返回給 do_fork。在此時,新進程存在但尚未運行。do_fork 函數通過調用 wake_up_new_task 來修復此問題。此函數(可在 ./linux/kernel/sched.c 內找到)初始化某些調度程序的常規管理信息,將新進程放置在運行隊列之內,然後將其喚醒以便執行。最後,一旦返回至 do_fork,此 PID 值即被返回給調用程序,進程完成。

4.總結

實際上,用戶空間的寄存器、用戶態堆棧等信息在切換到內核態的上下文時保存在內核棧中,父進程在內核態(dup_task_struct)復制出子進程,但子進程作為一個獨立的進程,之後被調度運行時必須有一個指令地址,進程切換時,ip地址及當前內核棧的位置esp都存在於thread_info中,由copy_thread設置其thread.ip指向ret_from_fork作為子進程執行的第一條語句,並完成了內核態到用戶態的切換。

進程創建由系統調用來建立新進程,歸根結底都是調用do_fork來實現。do_fork主要就是調用copy_process

copy_process()主要完成進程數據結構,各種資源的初始化。初始化方式可以重新分配,也可以共享父進程資源,主要根據clone_flags參數來確定。將task_struct結構體分配給子進程,並為其分配pid,最後將其加入可運行隊列中。

dup_task_struct()為子進程獲取進程描述符。

copy_thread()函數將父進程內核棧復制到子進程中,同時設置子進程調度後執行的第一條語句地址為do_frok返回,並將保存返回值的寄存器eax值置為0,因此子進程返回為0,而父進程繼續執行之後的初始化,最後返回子進程的pidtgid)。


Linux系統中進程的創建