4.1 進程四要素
什么是進程?
1:有一段代碼段供其執行,這代碼段不一定是進程所專用,可以與其他進程公用。
2:每個進程有其專用的系統空間的堆棧(棧)【這個棧是進程起碼的“私有財產”】
3:在內核中,要有task_struct 進程控制塊【task_struct
進程控制塊就是像是進程的財產登記卡,記錄著進程所擁有的各項資源,只有有了task_struct,進程才能被內核所調度】
4:擁有專有的用戶空間【各進程的用戶空間是相互獨立的,但是各進程共享系統空間,且各進程不能直接(不通過系統調用)改變系統空間的內容】
如果有 1 2 3,完全沒有用戶空間 ==>
內核線程(kernel thread,比如kswapd)
如果有 1 2 3,沒有獨立的用戶空間 ==>因為有用戶空間但不是獨立的,所以稱為用戶線程
linux 系統中,進程(process)和任務(task)是同一個意思;
Unix系統的進程在Intel 的技術資料中則稱為“任務”;linux源自Unix和i386系統結構;
linux系統運行時的第一個進程是在系統初始化階段“捏造”出來的,而此后的進程或線程則都是由一個已存在的進程像細胞分裂那樣通過系統調用復制出來的,稱為fork
或 clone
Intel的i386 通過任務門 和
進程的TSS段(任務狀態段,TSS位于GDT中,包含了該進程的關鍵的狀態信息【控制信息】,但linux卻沒有使用任務門)來在硬件上實現任務的切換;【但其實因為該處理器的CISC架構以及這種切換方式,不是特別的效率高的,其實任務切換可以做的更加的簡單,i386的這個切換可以理解為一種“高級語言”,而我們在做操作系統時,往往使用效率更高的“低級語言
匯編等”】;
i386 CPU 要求軟件去設置TR 與 TSS,TR指向CPU當前正在執行的任務進程的TSS,Intel的設計意圖是
隨著任務的切換而走馬燈似的設置TR的內容;
CPU因中斷或系統調用從用戶空間進入系統空間時,會由于運行級別的變換而導致自動更換棧,不同的棧指針來自于當前任務的TSS中包含的棧指針(SS
ESP);因為Linux系統中只用到了兩個運行級別,即0級與3級,所以對于內核來說,TSS中只剩下0級的堆棧指針 即SS0
ESP0
Linux
系統在任務的切換過程中,因為效率的考慮,并不根據任務的切換去設置TR,而是直接修改TSS(Linux內核只使用這樣一個TSS,用來保存當前任務的狀態)中的SS0
ESP0==》鐵打的營盤流水的兵,就一個TSS,就像一座營盤,建立后就不再動了,而里面的內容,也就是當前任務的系統堆棧指針,則隨著進程的調度切換而流水似地變動。這是因為改變TSS中的SS0
ESP0所花的開銷比通過裝入TR以更換一個TSS要小得多。
Linux中TSS不是某個進程所獨占的,他而是全局性的公共資源。內核中雖然有多個TSS,但是每個CPU就只有(使用)一個TSS,一經裝入就不再變了。
Unix
Linux系統中任務的切換只發生在系統空間中,這點很好理解,因為共享的系統空間中擁有各個進程的各種資源;
每個進程都有一個task_struct數據結構和一片用作系統空間棧的存儲空間。內核在為每個進程分配一個task_struct結構時,實際上分配兩個連續的物理頁面(共8192個字節),這兩個頁面的底部用作進程的task_struct結構,而在結構的上面就用作進程的系統空間堆棧、
數據結構task_struct的大小約為1K字節,所以進程的系統空間的堆棧的大小約為7K字節,注意:系統空間堆棧的空間不像用戶空間堆棧那樣可以在運行時動態的擴展,如第2章所述,而是靜態的確定了的,所以,在中斷服務程序中、內核軟中斷服務程序以及其他設備驅動程序的設計中,應注意不能讓這些函數嵌套太深【避免嵌套太深,導致棧的溢出】,同時在這些函數中也不適宜使用太多、太大的局部變量。
一個進程必定又是一個內核線程(內核線程的要求是
1.有代碼段 2.有專用的系統棧
3.有task_struct數據接否);
內核中有一個宏操作current,它指向當前進程task_struct結構的指針。
接下來,可具體分析下task_struct結構,
struct task_struct
{
volatile long state;
unsigned
long flags;
int
sigpending;
mm_segment_t
addr_limit;
struct
exec_domain *exec_domain;==>除personality外,應用程序還有一些其他的版本間的差異,從而形成了不同的“執行域”,這個指針就是指向描述本進程所屬的執行域的數據結構
volatile
long need_resched;
unsigned long
ptrace;
int lock_depth;
long
counter;
long nice;
unsigned
long policy;==>適用于本進程的調度政策,詳見進程的調度與切換
struct mm_struct
*mm;
int has_cpu,
processor;
unsigned long
cpus_allowed;
struct list_head
run_list;
unsigned long
sleep_time;
struct task_struct *next_task,
*prev_task;
struct mm_struct
*active_mm;
struct
linux_binfmt *binfmt;
int
exit_code, exit_signal;
int
pdeath_signal;==>這三個詳見系統調用exit()與wait4()
unsigned
long personality;
int dumpable:1;
int did_exec:1;
pid_t
pid;==>進程號
pid_t
pgrp;
pid_t
tty_old_pgrp;
pid_tpgrp;
pid_t tgid;
int
leader;
//pgrppgrpleader
當一個用戶登陸到系統時,就開始一個進程組(session),此后創建的進程都屬于這同一個session。此外,若干進程可以通過“管道”組合在一起,如
ls | wc -l,從而形成進程組,詳見“系統調用exec”一節
struct task_struct *p_opptr,
*p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct list_head
thread_group;
struct task_struct
*pidhash_next;
struct task_struct
**pidhash_pprev;
wait_queue_head_t
wait_chldexit;
struct semaphore
*vfork_sem;
unsigned long rt_priority;==>優先級別以及“實時”優先級別,詳見進程的調度與切換
unsigned long it_real_value,
it_prof_value, it_virt_value;
unsigned long it_real_incr,
it_prof_incr, it_virt_incr;
struct timer_list
real_timer;
struct tms times;
unsigned long
start_time;
long per_cpu_utime[NR_CPUS],
per_cpu_stime[NR_CPUS];
unsigned long min_flt, maj_flt,
nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1;
uid_t
uid,euid,suid,fsuid;
gid_t
gid,egid,sgid,fsgid;==>這8個主要與文件操作權限有關,見文件系統一章
int ngroups;
gid_t
groups[NGROUPS];
kernel_cap_t
cap_effective, cap_inheritable, cap_permitted;
==>一般進程都不能"為所欲為",而是各自被賦予了
各種不同的權限。例如,一個進程是否可以通過系統調用ptrace()跟蹤另一個進程,就是由該進程是否具有CAP_SYS_PTRACE授權決定的;一個進程是否有權重新引導操作系統,則取決于該進程是否具有CAP_SYS_BOOT授權。這樣,就把進程的各種權限分細了,而不再是籠統地取決于一個進程是否是"特權用戶"進程。每一種權限都由一個標志位代表,內核中提供了一個inline函數capable(),用來檢驗當前進程是否具有某種權限。如capable(CAP_SYS_BOOT)就是檢查當前進程是否有權重引導操作系統〔返回非0表示有權)。值得注意的是,對操作權限的這種劃分與文件訪問權限結合在一起,形成了系統安全性的基礎。在現今的網絡時代,這種安全性正在變得愈來愈重要,而這方面的研究與發展也是一個重要的課題。
int
keep_capabilities:1;
struct
user_struct *user;==>指向一個user_struct結構,該數據結構代表著進程所屬的用戶。注意這跟Unix內核中每個進程的user結構時兩碼事。Linux內核中user結構是非常簡單的,詳見“系統調用fork()”一節。
struct
rlimit rlim[RLIM_NLIMITS];==>這是一個結構數組,表明進程對各種資源的使用數量所受的限制,
struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
};
unsigned short
used_math;
char comm[16];
int link_count;
struct tty_struct
*tty;
unsigned int
locks;
struct sem_undo
*semundo;
struct sem_queue
*semsleeping;
struct thread_struct
thread;
struct fs_struct
*fs;
struct files_struct
*files;
spinlock_t
sigmask_lock;
struct signal_struct
*sig;
sigset_t blocked;
struct sigpending
pending;
unsigned long
sas_ss_sp;
size_t
sas_ss_size;
int (*notifier)(void
*priv);
void
*notifier_data;
sigset_t
*notifier_mask;
u32
parent_exec_id;
u32
self_exec_id;
//parent_exec_idself_exec_id
==>與進程組session有關,詳見系統調用exit()與wait4()
spinlock_t
alloc_lock;
}
主要可分為狀態、性質、資源、和組織等幾大類;
volatile long state;表示進程當前運行的狀態
#define TASK_RUNNING ?0==>進程處于就緒態(),而不是表達該進程就是當前正在運行的進程;當進程處于這個狀態時,內核就將該進程的task_struct結構通過其隊列頭run_list掛入一個“運行隊列”
#define
TASK_INTERRUPTIBLE ?1==>進程處于睡眠狀態,因信號的到來而被喚醒。interruptible_sleep_on()
和 ?wake_up_interruptible()用于淺度睡眠;
#define TASK_UNINTERRUPTIBLE
2==>進程處于深度睡眠狀態,不受信號(signal,也稱軟中斷)的打擾;sleep_on()
和wake_up()用于深度睡眠;深度睡眠一般只用于臨界區和關鍵性的部位,而“可中斷”的睡眠那就是通用的,特別當進程在“阻塞性”的系統調用中等待某一事件的發生時,就不應該進入深度睡眠,否則就不能對別的事件作出反應,別的進程就不能通過發一個信號來殺掉這個進程;這里的INTERRUPTIBLE與UNINTERRUPTIBLE與”中斷“毫無關系,而是說睡眠能否因其他事件而中斷即喚醒,不過其他事件主要指”信號“,而信號的概率實際上與中斷的概率是相同的,所以這里所謂的INTERRUPTIBLE也是指這種”軟中斷“。
#define TASK_ZOMBIE ?4==>進程已經去世(exit),但是其戶口尚未注銷
#define TASK_STOPPED ?8==>進程處于就緒態(),主要用于調試目的,進程接收到一個SIGSTOP信號后就將運行狀態改為TASK_STOPPED而進入掛起狀態,然后在接收到一個SIGCONT信號后,又恢復繼續運行。
unsigned
long flags;;反映進程狀態的信息,但不是運行狀態,而是與管理有關的其他信息 ,見下面的注釋
#define PF_ALIGNWARN
0x00000001
#define PF_STARTING
0x00000002
#define PF_EXITING
0x00000004
#define PF_FORKNOEXEC
0x00000040
#define PF_SUPERPRIV
0x00000100
#define PF_DUMPCORE
0x00000200
#define PF_SIGNALED
0x00000400
#define PF_MEMALLOC
0x00000800
#define PF_VFORK
0x00001000
#define PF_USEDFPU
0x00100000
再看下task_struct除上述外的其他的一下狀態信息變量:
int
sigpending==>表示進程收到了信號,但尚未處理,詳見進程間通信中的信號一節
mm_segment_t addr_limit==>虛擬地址空間的上限。對進程而言是其用戶空間的上限,所以是0XBFFFFFFF,對內核線程而言則
是系統空間的上限,所以是0XFFFFFFFF。
volatile long
need_resched==>與調度有關,表示CPU從系統空間返回用戶空間前夕要進行一次調度。
long counter==>與調度有關,詳見進程的調度與切換一節
unsigned long personality==>由于Unix有許多不同的版本和變種,應用程序也就有了適用的范圍,所以根據執行程序的不
同,每個進程都有其個性。
其他的直接看上面的結構里面的注釋
最后,每一個進程都不是孤立地存在于系統中,而總是根據不同的目的、關系和需要與其它的進程相聯系。從內核的角度看,則是要按不同的目的和性質將每個進程納入不同的組織中。第一個組織是由每個進程的"家庭與社會關系"形成的"宗族"或"家譜"。這是一種樹型的組織,通過指針p_opptr、p_pptr、p_cptr、p_ysptr和p_osptr構成。其中p_opptr和p_pptr指向父進程的task_struct結構,p_cptr指向最"年輕"的子進程,而p_ysptr和p_osptr則分別指向其"弟弟"和"哥哥",從而形成一個子進程鏈。這些指針確定了一個進程在其"宗族"中的上、下、左、右關系,詳見本章中對fork()和exit()的敘述。
【三個靜態隊列描述?第一個組織第二個組織第三個組織】
這個組織雖然確定了每個進程的"宗族"關系,涵蓋了系統中所有的進程,但是,要在這個組織中根據進程號pid找到一個進程卻非易事。進程號的分配是相當隨機的,在進程號中并不包含任何可以用來找到一個進程的路徑信息,而給定一個進程號要求找到該進程的task_struct結構卻又是常常要用到的一種操作。于是,就有了第二個組織,那就是一個以雜湊表為基礎的進程隊列的陣列。當給定一個pid要找到該進程時,先對pid施行雜湊計算,以計算的結果為下標在雜湊表中找到一個隊列,再順著該隊列就可以較容易地找到特定的進程了。雜湊表pidhash是在kernel/fork.c中定義的:struct
task_struct *pidhash[PIDHASH_SZ];
雜湊表的大小PIDHASH_SZ為1024。由于每個指針的大小是4個字節,所以整個雜湊表(不包括各個隊列)正好占一個頁面。每個進程的task_struct數據結構都通過其pidhash_next和pidhash_pprev兩個指針放入
到雜湊表中的某個隊列中,同一隊列中所有進程的pid都具有相同的雜湊值。由于雜湊表的使用,要找到pid為某個給定值的進程就很迅速了。
當內核需要對每一個進程做點什么事情時,還需要將系統中所有的進程都組織成一個線性的隊列,這樣就可以通過一個簡單的for循環或while循環遍歷所有進程的task_struct結構。所以,第三個組織就是這么一個線性隊列。系統中第一個建立的進程為init_task,這個進程就是所有進程的總根,所以這個線性隊列就是以init_task為起點(也可以把它看成是一個隊列頭〕,后繼每創建一個進程就通過其init_task結構中的next_task和prev_task兩個指針鏈入這個線性隊列中。
每個進程都必然同時身處這三個隊列之中,直到進程消亡時才從這三個隊列中摘除,所以這三個隊列都是靜態的。
在運行的過程中,一個進程還可以動態地鏈接進"可執行隊列"接受系統的調度。實際上,這是最重要的隊列,一個進程只有在可執行隊列中才有可能受到調度而投入運行。與前幾個隊列不同的是,
一個進程的task_struct是通過其list_head數據結構run_list、而不是個別的指針,鏈接進可執行隊列的。以前說過,這是用于雙向鏈接的通用數據結構,具有一些與之配套的函數或宏操作,處理的效率比較高,也使代碼得以簡化。可執行隊列的變化是非常頻繁的,一個進程進入睡眠時就從隊列中脫鏈,被喚醒時則又鏈入到該隊列中,在調度的過程中也有可能會改變一個進程在此隊列中的位置。詳見本章"進程調度與進程切換"以及"系統調用nanosleep()"中的有關敘述。
4.2 進程三部曲:創建、執行與消亡
就像世上萬物都有產生、發展與消亡的過程一樣,每個進程也有被創建、執行某段程序以及最后消亡的過程。
在linux系統中,第一個進程是系統固有的、與生倶來的或者說是由內核的設計者安排好了的(系統中第一個建立的進程為init_task,這個進程就是所有進程的總根)。內核在引導并完成了基本的初始化以后,就有了系統的第一進程(實際上是內核線程)。除此之外,所有其它的進程和內核線程都由這個原始進程或其子孫進程所創建,都是這個原始進程的"后代"。在linux系統中,一個新的進程一定要由一個已經存在的進程"復制"出來,而不是"創造"出來(而所謂"創建"實際就是復制)。所以,linux系統(unix也一樣)并不向用戶(即進程)提供類似這樣的系統調用:
int creat_pro(int (*fn)(void
*), void *arg, unsigned long options);
可是在很多操作系統(包括一些unix的變種)中都采用了
"一攬子"的方法。它"創造"出一個進程,并使該進程從函數指針數指針fn所指的地方開始執行。根據不同的情況和設計,參數fn也可以換成一個可執行程序的文名。這里所謂"創造",包括為進程分配所需的資源、包括屬于最低限度的task_struct
數據結構和系統空間堆棧,并初始化這些資源;還要設置其系統空間堆棧,使得這個新進程看起來就好像是一個本來就已經存在而正在睡眠的進程。當這個進程被調度運行的時候,其"返回地址",也就是"恢復"運行時的下一條指令,則就在fn指的地方(uc/os就是這樣的)。這個"子進程"生下來時兩手空空,卻可以完全獨立,并不與其父進程共享資源。
但是,linux系統(unix也一樣)采用的方法卻不同。
linux將進程的創建與目標程序的執行分成兩步:
第一步是從已經存在的"父進程"中像細胞分裂一樣地復制出一個"子進程"。這里所謂像"細胞分裂一樣",只是打個比方,實際上,復制出來的子進程有自已的task_struct?結構和系統空間堆棧,但與父進程共享其它所有的資源。例如,要是父進程打開了五個文件,那么子進程也有五個打開的文件,而且這些文件的當前讀寫指針也停在相同的地方。所以,這一步所做的是"復制"。linux為此提供了兩個系統調用,一個是fork(),另一個是clone()。兩者的區別在于fork()是全部復制,父進程所有的資源全都通過數據結構的復制"遺傳"給子進程。而clone()則可以將資源有選擇地復制給子進程,而沒有復制的數據結構則通過指針的復制讓子進程共享。在極端的情況下,一個進程可以clone()出一個線程。所以,系統調用fork()是無參數的,而clone()則帶有參數。讀者也許已經意識到,fork()其實比clone()更接近本來意義上的"克隆"。確實是這樣,原因在于fork()在unix初期即已存在,那時候"克隆"這個詞還不像現在這么流行,而既然業已存在,就不宜更改了。否則,也許應該互換一下名字。后來,又增設了
一個系統調用vfork(),也不帶參數,但是除task_struct結構和系統空間堆棧以外的資源全都通過數據結構指針的復制"遺傳",所以vfork()出來的是線程而不是進程。讀者將會看到,vfork()主要是出于效率的考慮而設計并提供的。
第二步是目標程序的執行。一般來說,創建一個新的進程是因為有不同的目標程序要讓新的程序去執行(但也不一定),所以,復制完成以后,子進程通常要與父進程分道揚鑣,走自己的路。為此提供了一個系統調用execve(),讓一個進程執行以文件形式存在的一個可執行程序的映象。
讀者也許要問:這兩種方案到底哪一種好?應該說是各有利弊。但是更應該說,Linux從unix繼承下來的這種分兩步走,并且在第一步中采取復制方式的方案,利遠大于弊。從效率的角度看,分兩步走很有好處。所謂復制,只是進程的基本資源的復制,如task_struct數據結構、系統空間堆棧、頁面表等等,對父進程的代碼及全局變量則并不需要復制,而只是通過只讀訪問的形式實現共享,僅在需要寫的時候才通過copy_on_write的手段為所涉及的頁面建立一個新的副本。所以,總的來說復制的代價是很低的,但是通過復制而繼承下來的資源則往往對子進程很有用。讀者以后會看到,在計算機網絡的實現中,以及在client/server系統中的server—方的實現中,fork()或clone()常常是最自然、最有效、最適宜的手段。更重要的好處是,這樣有利于父、子進程間通過pipe來建立起一種簡單有效的進程間通信管道,并且從而產生了操作系統的用戶界面即shell的"管道"機制。這一點,對于unix的發展和推廣應用,對于unix程序設計環境的形成,對于unix程序設計風格的形成,都有著非常深遠的影響。可以說,這是一項天才的發明,它在很大程度上改變了操作系統的發展方向。
當然,從另一角度,也就是從程序設計界面的角度來看,則"一攬子"的方案更為簡潔。不過fork()加execve()的方案也并不復雜很多。進一步說,這也像練武或演戲一樣有個固定的"招式",一旦掌握了以后就不覺得復雜,也很少變化了。再說,如果有必要也可以通過程序庫提供一個"一攬子"的庫函數,將這兩步包裝在一起。
創建了子進程以后,父進程有三個選擇:
第一是繼續走自己的路,與子進程分道揚鑣。只是如果子進程先于父進程"去世",則由內核給父進程發一個報喪的信號。
第二是停下來,也就是進入睡眠狀態,等待子進程完成其使命而最終去世,然后父進程再繼續運行。Linux為此提供了兩個系統調用,
wait4()和wait3()。兩個系統調用基本相同,wait4()等待某個特定的子進程去世,而wait3()則等待任何一個子進程去世。
第三個選擇是"自行退出歷史舞臺",結束自己的生命。為此設置了一個系統調用exit()。這里的第三個選擇其實不過是第一個選擇的一種特例,
所以從本質上說是兩種選擇:一種是父進程不受阻的(non_blocking)方式,也稱為"異步"的方式;另一種是父進程受阻的(blocking)方式,或者稱為"同步"的方式。
4.3 系統調用fork()
vfork() clone()
fork()與clone()的區別:
pid_t fork(void);
int clone(int (*fn)(void *arg),
void *child_stack, int flags, void *arg);
系統調用__clone()的主要用途是創建一個線程,這個線程可以是內核線程,也可以是用戶線程。
創建用戶空間線程時,可以給定子線程用戶空間堆棧的位置,還可以指定子進程運行的起點。
__clone()也可以創建進程,有選擇地復制父進程的資源。而fork()則是全面地復制。還有一個系統調用vfork()其作用也是創建一個線程,但主要只是作為創建進程的中間步驟,目的在于提高創建時的效率,減少系統開銷,其程序設計接口則與fork相同。
asmlinkage int sys_fork(unsigned long r4, unsigned long
r5,unsigned long r6, unsigned long r7,
struct
pt_regs regs)
{
return do_fork(SIGCHLD, regs.regs[15], ®s, 0);
}
asmlinkage int sys_clone(unsigned long clone_flags, unsigned
long newsp,
unsigned long r6, unsigned long r7,struct pt_regs
regs)
{
if (!newsp)
newsp = regs.regs[15];
return do_fork(clone_flags, newsp, ®s, 0);
}
asmlinkage int sys_vfork(unsigned long r4, unsigned long r5,
unsigned long r6, unsigned long r7,
struct pt_regs regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD,
regs.regs[15], ®s, 0);
}
這三個系統調用 都是 通過調用
do_fork()來完成的,do_fork()通過不同的參數
在函數體內實現相關資源的拷貝;
下面來解讀下這個函數
int do_fork(unsigned long clone_flags, unsigned long
stack_start,struct pt_regs *regs, unsigned
long stack_size)
{
======================函數內解釋部分===================================
參數clone_flags由兩部分組成,其最低的字節為信號類型,用以規定子進程去世時應該向父進程發出的信號。我們已經看到,對于fork()和vfork()這個信號就是SIGCHILD,而對__clone()則該位段可由調用者決定。第二部分是一些表示資源和特性的標志位(通過這些標志位來采取相應的拷貝動作),對于fork(),這一部分為全0,表對有關的資源都要復制而不是通過指針共享[位為0表示要復制
位為1表示父子先共享]。而對vfork(),則為(CLONE_VFORK
|CLONE_VM),表示父、子進程共用(用戶)虛存區間,并且當子進程釋放其虛存區間時要喚醒父進程,至于__clone(),則這一部分完全由調用者設定而作為參數傳遞下來。其中標志位CLONE_PID有特殊的作用,當這個標志位為1時,父、子進程〔線程)共用同一個進程號,也就是說,子進程雖然有其自己的task_structt數據結構,卻使用父進程的pid。但是,只有0號進程,也就是系統中的原始進程(實際上是線程),才允許這樣來調用__clone(),所以564行對此加以檢查。
接著,通過alloc_task_struct()為子進程分配兩個連續的物理頁面,低端用作子進程的task_struct結構,高端則用作其系統空間堆棧。
注意574行的賦值為整個數據結構的賦值。這樣,父進程的整個task_struct就被復制到了子進程的數據結構中。經編譯以后,這樣的賦值是用memcpy()實現的,所以效率很高。
#define CSIGNAL 0x000000ff
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_PID 0x00001000
#define CLONE_PTRACE 0x00002000
#define CLONE_VFORK 0x00004000
#define CLONE_PARENT 0x00008000
#define CLONE_THREAD 0x00010000
#define CLONE_SIGNAL (CLONE_SIGHAND |
CLONE_THREAD)
============================================================
int retval =
-ENOMEM;
struct task_struct
*p;
DECLARE_MUTEX_LOCKED(sem);
if (clone_flags &
CLONE_PID) {
if
(current->pid)
return -EPERM;
}
current->vfork_sem =
&sem;
p =
alloc_task_struct();
if (!p)
goto fork_out;
*p = *current;
retval = -EAGAIN;
if
(atomic_read(&p->user->processes) >=
p->rlim[RLIMIT_NPROC].rlim_cur)
goto
bad_fork_free;
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
if (nr_threads >=
max_threads)
goto
bad_fork_cleanup_count;
get_exec_domain(p->exec_domain);
if (p->binfmt &&
p->binfmt->module)
__MOD_INC_USE_COUNT(p->binfmt->module);
p->did_exec =
0;
p->swappable =
0;
p->state =
TASK_UNINTERRUPTIBLE;
copy_flags(clone_flags,
p);
p->pid =
get_pid(clone_flags);
======================函數內解釋部分===================================
task_struct結構中有個指針user,用來指向一個user_struct結構。
一個用戶常常有許多個進程,所以有關用戶的一些信息并不專屬于某一個進程。這樣,屬于同一用戶的進程就可以通過指針user共享這些信息。
顯然,每個用戶有且只有一個user_struct結構。結構中有個計數器__count對屬于該用戶的進程數量計數。
可想而知,內核線程并不屬于某個用戶,所以其task_struct中的user指引為0。
#define UIDHASH_BITS 8
#define UIDHASH_SZ
(1 << UIDHASH_BITS)
static struct
user_struct *uidhash_table[UIDHASH_SZ];
這是一個雜湊(hash)表。對用戶名施以雜湊運算,就可以計算出一個下標而找到該用戶的user_struct結構。各進程的task_struct結構中還有個數組rlim,對該進程占用各種資源的數量作出限制,而rlim[RLIMIT_NPROC]就規定了該進程所屬的用戶可以擁有的進程數量。所以,如果當前進程是一個用戶進程,并且該用戶擁有的進程數量已經達到了規定的限制值,就再不允許它fork()了.那么,對于不屬于任何用戶的內核線程怎么辦呢?
587行中的兩個計數器就是為進程的總量而設的。
一個進程除了屬于某一個用戶之外,還屬于某個"執行域"。總的來說,Linux是Unix的一個變種,并且符合POSIX的規定。但是,有很多版本的操作系統同樣是Unix變種,同樣符合POSIX規定,互相之間在實現細節上卻仍然有明顯的不同。這就形成了不同的執行域。如果一個進程所執行的程序是為Solaris開發的,那么這個進程就屬于Solaris執行域PER_SOLARIS。當然,在Linux上運行的絕大多數程序都屬于Linux執行域。在task_struct結構中有一個指針exec_domain,可以指向一個exec_domain數據結構。
常數PID_MAX定義為0X8000。可見,進程號的最大值是0X7FFF
即32767。進程號0?299是為系統進程(包括內核線程)保留的,主要用于各種"保護神"進程。以上這段代碼的邏輯并不復雜,我們就不多加解釋了。
====================================================================
p->run_list.next =
NULL;
p->run_list.prev =
NULL;
if ((clone_flags &
CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) {
p->p_opptr =
current;
if (!(p->ptrace &
PT_PTRACED))
p->p_pptr =
current;
}
p->p_cptr =
NULL;
init_waitqueue_head(&p->wait_chldexit);
p->vfork_sem =
NULL;
spin_lock_init(&p->alloc_lock);
p->sigpending =
0;
init_sigpending(&p->pending);
p->it_real_value =
p->it_virt_value = p->it_prof_value = 0;
p->it_real_incr =
p->it_virt_incr = p->it_prof_incr = 0;
init_timer(&p->real_timer);
p->real_timer.data =
(unsigned long) p;
p->leader = 0;
p->tty_old_pgrp =
0;
p->times.tms_utime =
p->times.tms_stime = 0;
p->times.tms_cutime =
p->times.tms_cstime = 0;
#ifdef CONFIG_SMP
{
int i;
p->has_cpu = 0;
p->processor =
current->processor;
for(i = 0; i < smp_num_cpus;
i++)
p->per_cpu_utime[i] =
p->per_cpu_stime[i] = 0;
spin_lock_init(&p->sigmask_lock);
}
#endif
p->lock_depth =
-1;
p->start_time =
jiffies;
======================函數內解釋部分===================================
====================================================================
retval = -ENOMEM;
if (copy_files(clone_flags,
p))
goto
bad_fork_cleanup;
if (copy_fs(clone_flags,
p))
goto
bad_fork_cleanup_files;
if (copy_sighand(clone_flags,
p))
goto
bad_fork_cleanup_fs;
if (copy_mm(clone_flags,
p))
goto
bad_fork_cleanup_sighand;
retval = copy_thread(0,
clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto
bad_fork_cleanup_sighand;
p->semundo =
NULL;
p->parent_exec_id =
p->self_exec_id;
p->swappable =
1;
p->exit_signal = clone_flags
& CSIGNAL;
p->pdeath_signal =
0;
p->counter =
(current->counter + 1) >> 1;
current->counter >>=
1;
if
(!current->counter)
current->need_resched =
1;
retval =
p->pid;
p->tgid =
retval;
INIT_LIST_HEAD(&p->thread_group);
write_lock_irq(&tasklist_lock);
if (clone_flags &
CLONE_THREAD) {
p->tgid =
current->tgid;
list_add(&p->thread_group,
¤t->thread_group);
}
SET_LINKS(p);
hash_pid(p);
nr_threads++;
write_unlock_irq(&tasklist_lock);
if (p->ptrace &
PT_PTRACED)
send_sig(SIGSTOP, p,
1);
wake_up_process(p);
++total_forks;
fork_out:
if ((clone_flags &
CLONE_VFORK) && (retval >
0))
down(&sem);
return retval;
bad_fork_cleanup_sighand:
exit_sighand(p);
bad_fork_cleanup_fs:
exit_fs(p);
bad_fork_cleanup_files:
exit_files(p);
bad_fork_cleanup:
put_exec_domain(p->exec_domain);
if (p->binfmt &&
p->binfmt->module)
__MOD_DEC_USE_COUNT(p->binfmt->module);
bad_fork_cleanup_count:
atomic_dec(&p->user->processes);
free_uid(p->user);
bad_fork_free:
free_task_struct(p);
goto fork_out;
}
4.4 系統調用execve()
在大多數情況下,如果復制出來的子進程不能與父進程分道揚鑣,走自己的路,那就沒有多大的意思,所以執行一個新的可執行程序是進程生命歷程中關鍵性的一步。linux為此提供了一個系統調用execve(),而在C語言的程序庫中又在此基礎上向應用程序提供一整套的庫函數,包括execl()
execlp() execleo() execv() execvp()
系統調用execve()內核入口是sys_execve()。sys_execve()就調用do_execve(),
以完成其主體部分的工作。
顯然,先要將給定的可執行程序文件找到并打開,do_execve()就是為此而調用的.
當目標文件已經打開,下一步就要從文件中裝入可執行程序了。內核中為可執行程序的裝入定義了一個數據結構linux_binprm,這個數據結構將運行一個可執行文件時所需的信息組織在一起。
struct
linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
unsigned long p;
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted,
cap_effective;
int argc, envc;
char * filename;
unsigned long loader, exec;
}
int do_execve(char * filename, char ** argv, char ** envp,
struct pt_regs * regs)
{
struct ?linux_binprm ?bprm;
struct file *file;
int retval;
int i;
file = open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval;
bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
memset(bprm.page, 0,
MAX_ARG_PAGES*sizeof(bprm.page[0]));
bprm.file = file; ?-->保存打開文件的file結構指針
bprm.filename = filename;
bprm.sh_bang =
0;-->
bprm.loader = 0;
bprm.exec = 0;
if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0)
{
allow_write_access(file);
fput(file);
return bprm.argc;
}
if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0)
{
allow_write_access(file);
fput(file);
return bprm.envc;
}
retval = prepare_binprm(&bprm);
if (retval < 0)
goto out;
retval = copy_strings_kernel(1, &bprm.filename,
&bprm);
if (retval < 0)
goto out;
bprm.exec = bprm.p;
retval = copy_strings(bprm.envc, envp, &bprm);
if (retval < 0)
goto out;
retval = copy_strings(bprm.argc, argv, &bprm);
if (retval < 0)
goto out;
retval = search_binary_handler(&bprm,regs);
if (retval >= 0)
return retval;
out:
allow_write_access(bprm.file);
if (bprm.file)
fput(bprm.file);
for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
struct page * page = bprm.page[i];
if (page)
__free_page(page);
}
return retval;
}