在 Linux 中,進程是我們非常熟悉的東東了,哪怕是只寫過一天代碼的人也都用過它。但是你確定它不是你最熟悉的陌生人?我們今天通過深度剖析進程的創(chuàng)建過程,幫助你提高對進程的理解深度。
在這篇文章中,我會用 Nginx 創(chuàng)建 worker 進程的例子作為引入,然后帶大家了解一些進程的數(shù)據(jù)結(jié)構(gòu) task_struct,最后再帶大家看一下 fork 執(zhí)行的過程。
學(xué)習(xí)完本文,你將深度理解進程中的那些關(guān)鍵要素,諸如進程地址空間、當(dāng)前目錄、父子進程關(guān)系、進程打開的文件 fd 表、進程命名空間等。也能學(xué)習(xí)到內(nèi)核在保存已經(jīng)使用的 pid 號時是如何優(yōu)化內(nèi)存占用的。我們展開今天的拆解!
一、Nginx 之 fork 創(chuàng)建 worker
在 Linux 進程的創(chuàng)建中,最核心的就是 fork 系統(tǒng)調(diào)用。不過我們先不著急介紹它,先拿多進程服務(wù)中的一個經(jīng)典例子 - Nginx,來看看他是如何使用 fork 來創(chuàng)建 worker 的。
Nginx 服務(wù)采用的是多進程方式來工作的,它啟動的時候會創(chuàng)建若干個 worker 進程出來,來響應(yīng)和處理用戶請求。創(chuàng)建 worker 子進程的源碼位于 nginx 源碼的 src/os/unix/ngx_process_cycle.c 文件中。通過循環(huán)調(diào)用 ngx_spawn_process 來創(chuàng)建 n 個 worker 出來。
?
//file:src/os/unix/ngx_process_cycle.c
static?void?ngx_start_worker_processes(...)
{
?...
?for?(i?=?0;?i?
?
我們在來看下負責(zé)具體進程創(chuàng)建的 ngx_spawn_process 函數(shù)。
?
//file:?src/os/unix/ngx_process.c
ngx_pid_t?ngx_spawn_process(ngx_cycle_t?*cycle,?ngx_spawn_proc_pt?proc,...)
{
?pid?=?fork();
?switch?(pid)?{
??case?-1:?//出錯了
???...?
??case?0:?//子進程創(chuàng)建成功
???...
???proc(cycle,?data);
???break;
?}
?...
}
?
在 ngx_spawn_process 中調(diào)用 fork 來創(chuàng)建進程,創(chuàng)建成功后 Worker 進程就將進入自己的入口函數(shù)中開始工作了。
二、Linux 中對進程的表示
在深入理解進程創(chuàng)建之前,我們先來看一下進程的數(shù)據(jù)結(jié)構(gòu)。
在 Linux 中,是用一個 task_struct 來實現(xiàn) Linux 進程的(其實 Linux 線程也同樣是用 task_struct 來表示的,這個我們以后文章單獨再說)。

我們來看看 task_struct 具體的定義,它位于 include/linux/sched.h
?
//file:include/linux/sched.h
struct?task_struct?{
?//2.1?進程狀態(tài)?
?volatile?long?state;
?//2.2?進程線程的pid
?pid_t?pid;
?pid_t?tgid;
?//2.3 進程樹關(guān)系:父進程、子進程、兄弟進程
?struct?task_struct?__rcu?*parent;
?struct?list_head?children;?
?struct?list_head?sibling;
?struct?task_struct?*group_leader;?
?//2.4?進程調(diào)度優(yōu)先級
?int?prio,?static_prio,?normal_prio;
?unsigned?int?rt_priority;
?//2.5?進程地址空間
?struct?mm_struct?*mm,?*active_mm;
?//2.6?進程文件系統(tǒng)信息(當(dāng)前目錄等)
?struct?fs_struct?*fs;
?//2.7?進程打開的文件信息
?struct?files_struct?*files;
?//2.8?namespaces?
?struct?nsproxy?*nsproxy;
}
?
2.1 進程線程狀態(tài)
進程線程都是有狀態(tài)的,它的狀態(tài)就保存在 state 字段中。常見的狀態(tài)中 TASK_RUNNING 表示進程線程處于就緒狀態(tài)或者是正在執(zhí)行。TASK_INTERRUPTIBLE 表示進程線程進入了阻塞狀態(tài)。
一個任務(wù)(進程或線程)剛創(chuàng)建出來的時候是 TASK_RUNNING 就緒狀態(tài),等待調(diào)度器的調(diào)度。調(diào)度器執(zhí)行 schedule 后,任務(wù)獲得 CPU 后進入 ?執(zhí)行進行運行。當(dāng)需要等待某個事件的時候,例如阻塞式 read 某個 socket 上的數(shù)據(jù),但是數(shù)據(jù)還沒有到達的時候,任務(wù)進入 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 狀態(tài),任務(wù)被阻塞掉。
當(dāng)?shù)却氖录竭_以后,例如 socket 上的數(shù)據(jù)到達了。內(nèi)核在收到數(shù)據(jù)后會查看 socket 上阻塞的等待任務(wù)隊列,然后將之喚醒,使得任務(wù)重新進入 TASK_RUNNING 就緒狀態(tài)。任務(wù)如此往復(fù)地在各個狀態(tài)之間循環(huán),直到退出。
一個任務(wù)(進程或線程)的大概狀態(tài)流轉(zhuǎn)圖如下。

全部的狀態(tài)值在 include/linux/sched.h 中進行了定義。
?
//file:include/linux/sched.h
#define?TASK_RUNNING????????????0
#define?TASK_INTERRUPTIBLE??????1
#define?TASK_UNINTERRUPTIBLE????2
#define?__TASK_STOPPED??4
#define?__TASK_TRACED??8
...
#define?TASK_DEAD??64
#define?TASK_WAKEKILL??128
#define?TASK_WAKING??256
#define?TASK_PARKED??512
#define?TASK_STATE_MAX??1024
......
?
2.2 進程 ID
我們知道,每一個進程都有一個進程 id 的概念。在 task_struct 中有兩個相關(guān)的字段,分別是 pid 和 tgid。
?
//file:include/linux/sched.h
struct?task_struct?{
?......
?pid_t?pid;
?pid_t?tgid;
}
?
其中 pid 是 Linux 為了標(biāo)識每一個進程而分配給它們的唯一號碼,稱做進程 ID 號,簡稱 PID。對于沒有創(chuàng)建線程的進程(只包含一個主線程)來說,這個 pid 就是進程的 PID,tgid 和 pid 是相同的。

2.3 進程樹關(guān)系
在 Linux 下所有的進程都是通過一棵樹來管理的。在操作系統(tǒng)啟動的時候,會創(chuàng)建 init 進程,接下來所有的進程都是由這個進程直接或者間接創(chuàng)建的的。通過 pstree 命令可以查看你當(dāng)前服務(wù)器上的進程樹信息。
?
init-+-atd
??|-cron
??|-db2fmcd
??|-db2syscr-+-db2fmp---4*[{db2fmp}]
??|??????????|-db2fmp---3*[{db2fmp}]
??|??????????|-db2sysc---13*[{db2sysc}]
??|??????????|-3*[db2syscr]
??|??????????|-db2vend
??|??????????`-{db2syscr}
??|-dbus-daemon
?
那么,這棵進程樹就是由 task_struct 下的 parent、children、sibling 等字段來表示的。這幾個字段將系統(tǒng)中的所有 task 串成了一棵樹。

2.4 進程調(diào)度優(yōu)先級
在 task_struct 中有幾個字段是表示進程優(yōu)先級的,在進程調(diào)度的時候會根據(jù)這幾個字段來決定優(yōu)先讓哪個任務(wù)(進程或線程)開始執(zhí)行。
static_prio: 用來保存靜態(tài)優(yōu)先級,可以調(diào)用 nice 系統(tǒng)直接來修改取值范圍為 100~139
rt_priority: 用來保存實時優(yōu)先級,取值范圍為 0~99
prio: 用來保存動態(tài)優(yōu)先級
normal_prio: 它的值取決于靜態(tài)優(yōu)先級和調(diào)度策略
2.5 進程地址空間
對于用戶進程來講,內(nèi)存描述符 mm_struct( mm 代表的是 memory descriptor)是非常核心的數(shù)據(jù)結(jié)構(gòu)。整個進程的虛擬地址空間部分都是由它來表示的。
進程在運行的時候,在用戶態(tài)其所需要的代碼,全局變量數(shù)據(jù),以及 mmap 內(nèi)存映射等全部都是通過 mm_struct 來進行內(nèi)存查找和尋址的。這個數(shù)據(jù)結(jié)構(gòu)的定義位于 include/linux/mm_types.h 文件下。
?
//file:include/linux/mm_types.h
struct?mm_struct?{
?struct?vm_area_struct?*?mmap;??/*?list?of?VMAs?*/
?struct?rb_root?mm_rb;
?unsigned?long?mmap_base;??/*?base?of?mmap?area?*/
?unsigned?long?task_size;??/*?size?of?task?vm?space?*/
?unsigned?long?start_code,?end_code,?start_data,?end_data;
?unsigned?long?start_brk,?brk,?start_stack;
?unsigned?long?arg_start,?arg_end,?env_start,?env_end;
}
?
其中 start_code、end_code 分別指向代碼段的開始與結(jié)尾、start_data 和 end_data 共同決定數(shù)據(jù)段的區(qū)域、start_brk 和 brk 中間是堆內(nèi)存的位置、start_stack 是用戶態(tài)堆棧的起始地址。整個 mm_struct 和地址空間、頁表、物理內(nèi)存的關(guān)系如下圖。

在內(nèi)核內(nèi)存區(qū)域,可以通過直接計算得出物理內(nèi)存地址,并不需要復(fù)雜的頁表計算。而且最重要的是所有內(nèi)核進程、以及用戶進程的內(nèi)核態(tài),這部分內(nèi)存都是共享的。

另外要注意的是,mm(mm_struct)表示的是虛擬地址空間。而對于內(nèi)核線程來說,是沒有用戶態(tài)的虛擬地址空間的。所以內(nèi)核線程的 mm 的值是 null。
2.6 進程文件系統(tǒng)信息(當(dāng)前目錄等)
進程的文件位置等信息是由 fs_struct 來描述的,它的定義位于 include/linux/fs_struct.h 文件中。
?
//file:include/linux/fs_struct.h
struct?fs_struct?{
?...
?struct?path?root,?pwd;
};
//file:include/linux/path.h
struct?path?{
?struct?vfsmount?*mnt;
?struct?dentry?*dentry;
};
?
通過以上代碼可以看出,在 fs_struct 中包含了兩個 path 對象,而每個 path 中都指向了一個 struct dentry。在 Linux 內(nèi)核中,denty 結(jié)構(gòu)是對一個目錄項的描述。

拿 pwd 來舉例,該指針指向的是進程當(dāng)前目錄所處的 denty 目錄項。假如我們在 shell 進程中執(zhí)行 pwd,或者用戶進程查找當(dāng)前目錄下的配置文件的時候,都是通過訪問 pwd 這個對象,進而找到當(dāng)前目錄的 denty 的。
2.7 進程打開的文件信息
每個進程用一個 files_struct 結(jié)構(gòu)來記錄文件描述符的使用情況, 這個 files_struct 結(jié)構(gòu)稱為用戶打開文件表。它的定義位于 include/linux/fdtable.h。
注意:飛哥用的內(nèi)核源碼一直是 3.10.0, 所以本文也不例外。不同版本的源碼這里稍微可能有些出入。
?
//file:include/linux/fdtable.h
struct?files_struct?{
?......
?//下一個要分配的文件句柄號
?int?next_fd;?
?//fdtable
?struct?fdtable?__rcu?*fdt;
}
struct?fdtable?{
?//當(dāng)前的文件數(shù)組
?struct?file?__rcu?**fd;
?......
};
?
在 files_struct 中,最重要的是在 fdtable 中包含的 file **fd 這個數(shù)組。這個數(shù)組的下標(biāo)就是文件描述符,其中 0、1、2 三個描述符總是默認分配給標(biāo)準輸入、標(biāo)準輸出和標(biāo)準錯誤。這就是你在 shell 命令中經(jīng)常看到的 2>&1 的由來。這幾個字符的含義就是把標(biāo)準錯誤也一并打到標(biāo)準輸出中來。

在數(shù)組元素中記錄了當(dāng)前進程打開的每一個文件的指針。這個文件是 Linux 中抽象的文件,可能是真的磁盤上的文件,也可能是一個 socket。
2.8 namespaces
在 Linux 中,namespace 是用來隔離內(nèi)核資源的方式。通過 namespace 可以讓一些進程只能看到與自己相關(guān)的一部分資源,而另外一些進程也只能看到與它們自己相關(guān)的資源,這兩撥進程根本就感覺不到對方的存在。
具體的實現(xiàn)方式是把一個或多個進程的相關(guān)資源指定在同一個 namespace 中,而進程究竟是屬于哪個 namespace,都是在 task_struct 中由 *nsproxy 指針表明了這個歸屬關(guān)系。
?
//file:include/linux/nsproxy.h
struct?nsproxy?{
?atomic_t?count;
?struct?uts_namespace?*uts_ns;
?struct?ipc_namespace?*ipc_ns;
?struct?mnt_namespace?*mnt_ns;
?struct?pid_namespace?*pid_ns;
?struct?net???????*net_ns;
};
?

命名空間包括PID命名空間、掛載點命名空間、網(wǎng)絡(luò)命名空間等多個。飛哥在咱們「開發(fā)內(nèi)功修煉」前面的一篇文章《動手實驗+源碼分析,徹底弄懂Linux網(wǎng)絡(luò)命名空間》這一文中詳細介紹過網(wǎng)絡(luò)命名空間,感興趣的同學(xué)可以詳細閱讀。
三、解密 fork 系統(tǒng)調(diào)用
前面我們看了 Nginx 使用 fork 來創(chuàng)建 worker 進程,也了解了進程的數(shù)據(jù)結(jié)構(gòu) task_struct ,我們再來看看 fork 系統(tǒng)調(diào)用的內(nèi)部邏輯。
這個 fork 在內(nèi)核中是以一個系統(tǒng)調(diào)用來實現(xiàn)的,它的內(nèi)核入口是在 kernel/fork.c 下。
?
//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{
?return?do_fork(SIGCHLD,?0,?0,?NULL,?NULL);
}
?
這里注意下調(diào)用 do_fork 時傳入的第一個參數(shù),這個參數(shù)是一個 flag 選項。它可以傳入的值包括 CLONE_VM、CLONE_FS 和 CLONE_FILES 等等很多,但是這里只傳了一個 SIGCHLD(子進程在終止后發(fā)送 SIGCHLD 信號通知父進程),并沒有傳 CLONE_FS 等其它 flag。
?
//file:include/uapi/linux/sched.h
//cloning?flags:
...
#define?CLONE_VM?0x00000100
#define?CLONE_FS?0x00000200?
#define?CLONE_FILES?0x00000400?
...
?
在 do_fork 的實現(xiàn)中,核心是一個 copy_process 函數(shù),它以拷貝父進程的方式來生成一個新的 task_struct 出來。
?
//file:kernel/fork.c
long?do_fork(unsigned?long?clone_flags,
????unsigned?long?stack_start,
????unsigned?long?stack_size,
????int?__user?*parent_tidptr,
????int?__user?*child_tidptr)
{
?//復(fù)制一個?task_struct?出來
?struct?task_struct?*p;
?p?=?copy_process(clone_flags,?stack_start,?stack_size,
????child_tidptr,?NULL,?trace);
?//子任務(wù)加入到就緒隊列中去,等待調(diào)度器調(diào)度
?wake_up_new_task(p);
?...
}
?
在創(chuàng)建完畢后,調(diào)用 wake_up_new_task 將新創(chuàng)建的任務(wù)添加到就緒隊列中,等待調(diào)度器調(diào)度執(zhí)行。
copy_process 的代碼很長,我對其進行了一定程度的精簡,參加下面的代碼。
?
//file:kernel/fork.c
static?struct?task_struct?*copy_process(...)
{
?//3.1?復(fù)制進程?task_struct?結(jié)構(gòu)體
?struct?task_struct?*p;
?p?=?dup_task_struct(current);
?...
?//3.2?拷貝?files_struct
?retval?=?copy_files(clone_flags,?p);
?//3.3?拷貝?fs_struct
?retval?=?copy_fs(clone_flags,?p);
?//3.4?拷貝?mm_struct
?retval?=?copy_mm(clone_flags,?p);
?//3.5?拷貝進程的命名空間?nsproxy
?retval?=?copy_namespaces(clone_flags,?p);
?//3.6?申請?pid?&&?設(shè)置進程號
?pid?=?alloc_pid(p->nsproxy->pid_ns);
?p->pid?=?pid_nr(pid);
?p->tgid?=?p->pid;
?if?(clone_flags?&?CLONE_THREAD)
??p->tgid?=?current->tgid;
?......
}
?
可見,copy_process 先是復(fù)制了一個新的 task_struct 出來,然后調(diào)用 copy_xxx 系列的函數(shù)對 task_struct 中的各種核心對象進行拷貝處理,還申請了 pid。接下來我們分小節(jié)來查看該函數(shù)的每一個細節(jié)。
3.1 復(fù)制進程 task_struct 結(jié)構(gòu)體
注意一下,上面調(diào)用 dup_task_struct 時傳入的參數(shù)是 current,它表示的是當(dāng)前進程。在 dup_task_struct 里,會申請一個新的 task_struct 內(nèi)核對象,然后將當(dāng)前進程復(fù)制給它。需要注意的是,這次拷貝只會拷貝 task_struct 結(jié)構(gòu)體本身,它內(nèi)部包含的 mm_struct 等成員只是復(fù)制了指針,仍然指向和 current 相同的對象。

我們來簡單看下具體的代碼。
?
//file:kernel/fork.c
static?struct?task_struct?*dup_task_struct(struct?task_struct?*orig)
{
?//申請?task_struct?內(nèi)核對象
?tsk?=?alloc_task_struct_node(node);
?//復(fù)制?task_struct
?err?=?arch_dup_task_struct(tsk,?orig);
?...
}
?
其中 alloc_task_struct_node 用于在 slab 內(nèi)核內(nèi)存管理區(qū)中申請一塊內(nèi)存出來。關(guān)于 slab 機制請參考-?內(nèi)核內(nèi)存管理
?
//file:kernel/fork.c
static?struct?kmem_cache?*task_struct_cachep;
static?inline?struct?task_struct?*alloc_task_struct_node(int?node)
{
?return?kmem_cache_alloc_node(task_struct_cachep,?GFP_KERNEL,?node);
}
?
申請完內(nèi)存后,調(diào)用 arch_dup_task_struct 進行內(nèi)存拷貝。
?
//file:kernel/fork.c
int?arch_dup_task_struct(struct?task_struct?*dst,
?????????struct?task_struct?*src)
{
?*dst?=?*src;
?return?0;
}
?
3.2 拷貝 files_struct
由于進程之間都是獨立的,所以創(chuàng)建出來的新進程需要拷貝一份獨立的 files 成員出來。

我們看 copy_files 是如何申請和拷貝 files 成員的。
?
//file:kernel/fork.c
static?int?copy_files(unsigned?long?clone_flags,?struct?task_struct?*tsk)
{
?struct?files_struct?*oldf,?*newf;
?oldf?=?current->files;
?if?(clone_flags?&?CLONE_FILES)?{
??atomic_inc(&oldf->count);
??goto?out;
?}
?newf?=?dup_fd(oldf,?&error);
?tsk->files?=?newf;
?...
}
?
看上面代碼中判斷了是否有 CLONE_FILES 標(biāo)記,如果有的話就不執(zhí)行 dup_fd 函數(shù)了,增加個引用計數(shù)就返回了。前面我們說了,do_fork 被調(diào)用時并沒有傳這個標(biāo)記。所以還是會執(zhí)行到 dup_fd 函數(shù):
?
//file:fs/file.c
struct?files_struct?*dup_fd(struct?files_struct?*oldf,?...)
{
?//為新?files_struct?申請內(nèi)存
?struct?files_struct?*newf;
?newf?=?kmem_cache_alloc(files_cachep,?GFP_KERNEL);
?//初始化?&?拷貝
?new_fdt->max_fds?=?NR_OPEN_DEFAULT;
?...
}
?
這個函數(shù)就是到內(nèi)核中申請一塊內(nèi)存出來,保存 files_struct 使用。然后對新的 files_struct 進行各種初始化和拷貝。至此,新進程有了自己獨立的 files 成員了。
3.3 拷貝 fs_struct
同樣,新進程也需要一份獨立的文件系統(tǒng)信息 - fs_struct 成員的。

我們來看 copy_fs 是如何申請和初始化 fs_struct 的。
?
//file:kernel/fork.c
static?int?copy_fs(unsigned?long?clone_flags,?struct?task_struct?*tsk)
{
?struct?fs_struct?*fs?=?current->fs;
?if?(clone_flags?&?CLONE_FS)?{
??fs->users++;
??return?0;
?}
?tsk->fs?=?copy_fs_struct(fs);
?return?0;
}
?
在創(chuàng)建進程的時候,沒有傳遞 CLONE_FS 這個標(biāo)志,所會進入到 copy_fs_struct 函數(shù)中申請新的 fs_struct 并進行賦值。
?
//file:fs/fs_struct.c
struct?fs_struct?*copy_fs_struct(struct?fs_struct?*old)
{
?//申請內(nèi)存
?struct?fs_struct?*fs?=?kmem_cache_alloc(fs_cachep,?GFP_KERNEL);
?//賦值
?fs->users?=?1;
?fs->root?=?old->root;
?fs->pwd?=?old->pwd;
?...
?return?fs;
}
?
3.4 拷貝 mm_struct
前面我們說過,對于進程來講,地址空間是一個非常重要的數(shù)據(jù)結(jié)構(gòu)。而且進程之間地址空間也必須是要隔離的,所以還會新建一個地址空間。

創(chuàng)建地址空間的操作是在 copy_mm 中執(zhí)行的。
?
//file:kernel/fork.c
static?int?copy_mm(unsigned?long?clone_flags,?struct?task_struct?*tsk)
{
?struct?mm_struct?*mm,?*oldmm;
?oldmm?=?current->mm;
?if?(clone_flags?&?CLONE_VM)?{
??atomic_inc(&oldmm->mm_users);
??mm?=?oldmm;
??goto?good_mm;
?}
?mm?=?dup_mm(tsk);
good_mm:
?return?0;?
}
?
do_fork 被調(diào)用時也沒有傳 CLONE_VM,所以會調(diào)用 dup_mm 申請一個新的地址空間出來。
?
//file:kernel/fork.c
struct?mm_struct?*dup_mm(struct?task_struct?*tsk)
{
?struct?mm_struct?*mm,?*oldmm?=?current->mm;
?mm?=?allocate_mm();
?memcpy(mm,?oldmm,?sizeof(*mm));
?...
}
?
在 dup_mm 中,通過 allocate_mm 申請了新的 mm_struct,而且還將當(dāng)前進程地址空間 current->mm 拷貝到新的 mm_struct 對象里了。
地址空間是進程線程最核心的東西,每個進程都有獨立的地址空間
3.5 拷貝進程的命名空間 nsproxy
在創(chuàng)建進程或線程的時候,還可以讓內(nèi)核幫我們創(chuàng)建獨立的命名空間。在默認情況下,創(chuàng)建進程沒有指定命名空間相關(guān)的標(biāo)記,因此也不會創(chuàng)建。新舊進程仍然復(fù)用同一套命名空間對象。

3.6 申請pid
接下來 copy_process 還會進入 alloc_pid 來為當(dāng)前任務(wù)申請 PID。
?
//file:kernel/fork.c
static?struct?task_struct?*copy_process(...)
{
?...
?//申請pid
?pid?=?alloc_pid(p->nsproxy->pid_ns);
?//賦值
?p->pid?=?pid_nr(pid);
?p->tgid?=?p->pid;
?...
}
?
注意下,在調(diào)用 alloc_pid 的時候,其參數(shù)傳遞的是新進程的 pid namespace。我們來深看一下 alloc_pid 的執(zhí)行邏輯。
?
//file:kernel/pid.c
struct?pid?*alloc_pid(struct?pid_namespace?*ns)
{
?//申請?pid?內(nèi)核對象
?pid?=?kmem_cache_alloc(ns->pid_cachep,?GFP_KERNEL);
?if?(!pid)
??goto?out;
?//調(diào)用到alloc_pidmap來分配一個空閑的pid編號
?//注意,在每一個命令空間中都需要分配進程號
?tmp?=?ns;
?pid->level?=?ns->level;
?for?(i?=?ns->level;?i?>=?0;?i--)?{
??nr?=?alloc_pidmap(tmp);
??pid->numbers[i].nr?=?nr;
??...
?}
?...
?return?pid
}
?
這里的 PID 并不是一個整數(shù),而是一個結(jié)構(gòu)體,所以先試用 kmem_cache_alloc 把它申請出來。接下來調(diào)用 alloc_pidmap 到 pid 命名空間中申請一個 pid 號出來,申請完后賦值記錄。
回顧我們開篇提到的一個問題:操作系統(tǒng)是如何記錄使用過的進程號的?在 Linux 內(nèi)部,為了節(jié)約內(nèi)存,進程號是通過 bitmap 來管理的。

在每一個 pid 命名空間內(nèi)部,會有一個或者多個頁面來作為 bitmap。其中每一個 bit 位(注意是 bit 位,不是字節(jié))的 0 或者 1 的狀態(tài)來表示當(dāng)前序號的 pid 是否被占用。
?
//file:include/linux/pid_namespace.h
#define?BITS_PER_PAGE??(PAGE_SIZE?*?8)
#define?PIDMAP_ENTRIES??((PID_MAX_LIMIT+BITS_PER_PAGE-1)/BITS_PER_PAGE)
struct?pid_namespace?{
?struct?pidmap?pidmap[PIDMAP_ENTRIES];
?...
}
?
在 alloc_pidmap 中就是以 bit 的方式來遍歷整個 bitmap,找到合適的未使用的 bit,將其設(shè)置為已使用,然后返回。
?
//file:kernel/pid.c
static?int?alloc_pidmap(struct?pid_namespace?*pid_ns)
{
?...
?map?=?&pid_ns->pidmap[pid/BITS_PER_PAGE];
}
?
在各種語言中,一般一個 int 都是 4 個字節(jié),換算成 bit 就是 32 bit。而使用這種 bitmap 的思想的話,只需要一個 bit 就可以表示一個整數(shù),相當(dāng)?shù)墓?jié)約內(nèi)存。所以,在很多超大規(guī)模數(shù)據(jù)處理中都會用到這種思想來進行優(yōu)化內(nèi)存占用的。
3.7 進入就緒隊列
當(dāng) copy_process 執(zhí)行完畢的時候,表示新進程的一個新的 task_struct 對象就創(chuàng)建出來了。接下來內(nèi)核會調(diào)用 wake_up_new_task 將這個新創(chuàng)建出來的子進程添加到就緒隊列中等待調(diào)度。
?
//file:kernel/fork.c
long?do_fork(...)
{
?//復(fù)制一個?task_struct?出來
?struct?task_struct?*p;
?p?=?copy_process(clone_flags,?stack_start,?...);
?//子任務(wù)加入到就緒隊列中去,等待調(diào)度器調(diào)度
?wake_up_new_task(p);
?...
}
?
等操作系統(tǒng)真正調(diào)度開始的時候,子進程中的代碼就可以真正開始執(zhí)行了。
四、總結(jié)
在這篇文章中,我用 Nginx 創(chuàng)建 worker 進程的例子作為引入,然后帶大家了解一些進程的數(shù)據(jù)結(jié)構(gòu) task_struct,最后又帶大家看一下 fork 執(zhí)行的過程。
在 fork 創(chuàng)建進程的時候,地址空間 mm_struct、掛載點 fs_struct、打開文件列表 files_struct 都要是獨立擁有的,所以都去申請內(nèi)存并初始化了它們。但由于今天我們的例子父子進程是同一個命名空間,所以 nsproxy 還仍然是共用的。

其中 mm_struct 是一個非常核心的數(shù)據(jù)結(jié)構(gòu),用戶進程的虛擬地址空間就是用它來表示的。對于內(nèi)核線程來講,不需要虛擬地址空間,所以 mm 成員的值為 null。 另外還學(xué)到了內(nèi)核是用 bitmap 來管理使用和為使用的 pid 號的,這樣做的好處是極大地節(jié)約了內(nèi)存開銷。而且由于數(shù)據(jù)存儲的足夠緊湊,遍歷起來也是非常的快。一方面原因是數(shù)據(jù)小,加載起來快。另外一方面是會加大提高 CPU 緩存的命中率,訪問非???。
今天的進程創(chuàng)建過程就學(xué)習(xí)完了。不過細心的同學(xué)可能發(fā)現(xiàn)了,我們這里只介紹了子進程的調(diào)用。但是對于 Nginx 主進程如何加載起來執(zhí)行的還沒有講到。我們將來還會展開敘述,敬請期待!
審核編輯:湯梓紅
電子發(fā)燒友App





























評論