【程序與執行緒】Linux中程序與執行緒的區別
1.執行緒的建立方法
建立執行緒具體呼叫pthread_create函式,這個函式實在glibc庫中實現。在glibc中pthread_create的呼叫路徑是__pthread_create_2_1->create_thread。其中create_thread很重要,它設定了建立執行緒時使用的各種flag標記。
// file:nptl/sysdeps/pthread/createthread.c static int create_thread (struct pthread *pd, ...) { int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM | 0); int res = do_clone (pd, attr, clone_flags, start_thread, STACK_VARIABLES_ARGS, 1); ... }
這裡我們傳入了 CLONE_VM、CLONE_FS、CLONE_FILES 等標記,接下來的 do_clone 最終會呼叫一段彙編程式,在彙編裡進入 clone 系統呼叫,之後會進入核心中進行處理。
//file:sysdeps/unix/sysv/linux/i386/clone.S
ENTRY (BP_SYM (__clone))
...
movl $SYS_ify(clone),%eax
...
二、核心中對執行緒的表示
程序和執行緒的相同點要遠遠大於不同點。主要依據就是在 Linux 中,無論程序還是執行緒,都是抽象成了 task 任務,在原始碼裡都是用 task_struct 結構來實現的。
我們來看 task_struct 具體的定義,它位於 include/linux/sched.h
//file:include/linux/sched.h struct task_struct{ // 1.1 task狀態 volatile long state; // 1.2 程序執行緒的pid pid_t pid; pid_t tgid; // 執行緒所屬程序的PID // 1.3 task樹關係:父程序、子程序、兄弟程序 struct task_struct __rcu *parent; struct list_head children; struct list_head sibling; struct task_struct *group_leader; // 1.4 task排程優先順序 int prio,static_prio,normal_prio; unsigned int rt_priority; // 1.5 地址空間 struct mm_struct *mm,*active_mm; // 1.6 檔案系統資訊(當前目錄等) struct fs_struct *fs; // 1.7 開啟的檔案資訊 struct files_struct *files; // 1.8 namespaces struct nsproxy *nsproxy; }
對於執行緒來講,所有的欄位都是和程序一樣的(本來就是一個結構體來表示的)。包括狀態、pid、task 樹關係、地址空間、檔案系統資訊、開啟的檔案資訊等等欄位,執行緒也都有。
程序和執行緒的相同點要遠遠大於不同點,本質上是同一個東西,都是一個 task_struct !正因為程序執行緒如此之相像,所以在 Linux 下的執行緒還有另外一個名字,叫輕量級程序。
pid 和 tgid 這兩個欄位。在 Linux 中,每一個 task_struct 都需要被唯一的標識,它的 pid 就是唯一標識號。
//file:include/linux/sched.h
struct task_struct {
......
pid_t pid;
pid_t tgid;
}
對於程序來說,這個 pid 就是我們平時常說的程序 pid。
對於執行緒來說,我們假如一個程序下建立了多個執行緒出來。那麼每個執行緒的 pid 都是不同的。但是我們一般又需要記錄執行緒是屬於哪個程序的。這時候,tgid 就派上用場了,通過 tgid 欄位來表示自己所歸屬的程序 ID。
這樣核心通過 tgid 可以知道執行緒屬於哪個程序。
三、執行緒建立過程
3.1 程序建立
程序執行緒建立的時候,使用的函式看起來不一樣。但實際在底層實現上,都是通過同一個函式實現的
fork 呼叫主要就是執行了 do_fork 函式,fork 函式呼叫 do_fork 的傳的引數分別是SIGCHLD、0,0,NULL,NULL。
//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
do_fork 函式又呼叫 copy_process 完成程序的建立。
//file:kernel/fork.c
long do_fork(...)
{
//複製一個 task_struct 出來
struct task_struct *p;
p = copy_process(clone_flags, ...);
...
}
3.2執行緒的建立
庫函式 pthread_create 會呼叫到 clone 系統呼叫,為其傳入了一組 flag。
//file:nptl/sysdeps/pthread/createthread.c
static int
create_thread (struct pthread *pd, ...)
{
int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
| 0);
int res = do_clone (pd, attr, clone_flags, ...);
...
}
clone 系統呼叫的實現
//file:kernel/fork.c
SYSCALL_DEFINE5(clone, ......)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
同樣,do_fork 函式還是會執行到 copy_process 來完成實際的建立。
3.3 程序執行緒建立區別
可見和建立程序時使用的 fork 系統呼叫相比,建立執行緒的 clone 系統呼叫幾乎和 fork 差不多,也一樣使用的是核心裡的 do_fork 函式,最後走到 copy_process 來完整建立。
不過建立過程的區別是二者在呼叫 do_fork 時傳入的 clone_flags 裡的標記不一樣!。
- 建立程序時的 flag:僅有一個 SIGCHLD
- 建立執行緒時的 flag:包括 CLONE_VM、CLONE_FS、CLONE_FILES、CLONE_SIGNAL、CLONE_SETTLS、CLONE_PARENT_SETTID、CLONE_CHILD_CLEARTID、CLONE_SYSVSEM。
關於這些 flag 的含義,我們選幾個關鍵的做一個簡單的介紹,後面介紹 do_fork 細節的時候會再次涉及到。
- CLONE_VM: 新 task 和父程序共享地址空間
- CLONE_FS:新 task 和父程序共享檔案系統資訊
- CLONE_FILES:新 task 和父程序共享檔案描述符表
四、do_fork()系統呼叫
程序和執行緒建立都是呼叫核心中的 do_fork 函式來執行的。在 do_fork 的實現中,核心是一個 copy_process 函式,它以拷貝父程序(執行緒)的方式來生成一個新的 task_struct 出來。
//file:kernel/fork.c
long do_fork(unsigned long clone_flags, ...)
{
//複製一個 task_struct 出來
struct task_struct *p;
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
//子任務加入到就緒佇列中去,等待排程器排程
wake_up_new_task(p);
...
}
在建立完畢後,呼叫 wake_up_new_task 將新建立的任務新增到就緒佇列中,等待排程器排程執行。
//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
//4.1 複製程序 task_struct 結構體
struct task_struct *p;
p = dup_task_struct(current);
...
//4.2 拷貝 files_struct
retval = copy_files(clone_flags, p);
//4.3 拷貝 fs_struct
retval = copy_fs(clone_flags, p);
//4.4 拷貝 mm_struct
retval = copy_mm(clone_flags, p);
//4.5 拷貝程序的名稱空間 nsproxy
retval = copy_namespaces(clone_flags, p);
//4.6 申請 pid && 設定程序號
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 先是複製了一個新的 task_struct 出來,然後呼叫 copy_xxx 系列的函式對 task_struct 中的各種核心物件進行拷貝處理,還申請了 pid 。
4.1 複製task_struct結構體
上面呼叫 dup_task_struct 時傳入的引數是 current,它表示的是當前任務。在 dup_task_struct 裡,會申請一個新的 task_struct 核心物件,然後將當前任務複製給它。需要注意的是,這次拷貝只會拷貝 task_struct 結構體本身,它內部包含的 mm_struct 等成員不會被複制。
具體程式碼:
//file:kernel/fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
//申請 task_struct 核心物件
tsk = alloc_task_struct_node(node);
//複製 task_struct
err = arch_dup_task_struct(tsk, orig);
...
}
其中 alloc_task_struct_node 用於在 slab 核心記憶體管理區中申請一塊記憶體出來。
//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);`
}
申請完記憶體後,呼叫 arch_dup_task_struct 進行記憶體拷貝。
//file:kernel/fork.c
int arch_dup_task_struct(struct task_struct *dst,
struct task_struct *src)
{
*dst = *src;
return 0;
}
4.2 拷貝開啟檔案列表
建立執行緒呼叫 clone 系統呼叫的時候,傳入了一堆的 flag,其中有一個就是 CLONE_FILES。如果傳入了 CLONE_FILES 標記,就會複用當前程序的開啟檔案列表 - files 成員。
對於建立程序來講,沒有傳入這個標誌,就會新建立一個 files 成員出來。
繼續看 copy_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(建立執行緒的時候),只是在原有的 files_struct 裡面 +1 就算是完事了,指標不變,仍然是複用建立它的程序的 files_struct 物件。
這就是程序和執行緒的其中一個區別,對於程序來講,每一個程序都需要獨立的 files_struct。但是對於執行緒來講,它是和建立它的執行緒複用 files_struct 的。
4.3 拷貝檔案目錄資訊
建立執行緒的時候,傳入的 flag 裡也包括 CLONE_FS。如果指定了這個標誌,就會複用當前程序的檔案目錄 - fs 成員。
對於建立程序來講,沒有傳入這個標誌,就會新建立一個 fs 出來。
copy_fs 的實現。
//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;
}
和 copy_files 函式類似,在 copy_fs 中如果指定了 CLONE_FS(建立執行緒的時候),並沒有真正申請獨立的 fs_struct 出來,近幾年只是在原有的 fs 裡的 users +1 就算是完事。
而在建立程序的時候,由於沒有傳遞這個標誌,會進入到 copy_fs_struct 函式中申請新的 fs_struct 並進行賦值拷貝。
4.4 拷貝記憶體地址空間
建立執行緒的時候帶了 CLONE_VM 標誌,而建立程序的時候沒帶。接下來在 copy_mm 函式 中會根據是否有這個標誌來決定是該和當前執行緒共享一份地址空間 mm_struct,還是建立一份新的。
//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;
}
對於執行緒來講,由於傳入了 CLONE_VM 標記,所以不會申請新的 mm_struct 出來,而是共享其父程序的。
多執行緒程式中的所有執行緒都會共享其父程序的地址空間。
而對於多程序程式來說,每一個程序都有獨立的 mm_struct(地址空間)。
因為在核心中執行緒和程序都是用 task_struct 來表示,只不過執行緒和程序的區別是會和建立它的父程序共享開啟檔案列表、目錄資訊、虛擬地址空間等資料結構,會更輕量一些。所以在 Linux 下的執行緒也叫輕量級程序。
在開啟檔案列表、目錄資訊、記憶體虛擬地址空間中,記憶體虛擬地址空間是最重要的。因此區分一個 Task 任務該叫執行緒還是該叫程序,一般習慣上就看它是否有獨立的地址空間。如果有,就叫做程序,沒有,就叫做執行緒。
這裡展開多說一句,對於核心任務來說,無論有多少個任務,其使用地址空間都是同一個。所以一般都叫核心執行緒,而不是核心程序。
五、結論
建立執行緒的整個過程介紹完了。總結一下,對於執行緒來講,其地址空間 mm_struct、目錄資訊 fs_struct、開啟檔案列表 files_struct 都是和建立它的任務共享的。
但是對於程序來講,地址空間 mm_struct、掛載點 fs_struct、開啟檔案列表 files_struct 都要是獨立擁有的,都需要去申請記憶體並初始化它們。
在 Linux 核心中並沒有對執行緒做特殊處理,還是由 task_struct 來管理。從核心的角度看,執行緒本質上還是一個程序。只不過和普通程序比,稍微“輕量”了那麼一些。
執行緒具體能輕量多少呢?通過程序和執行緒的上下文切換開銷測試。程序的測試結果是一次上下文切換平均 2.7 - 5.48 us 之間。執行緒上下文切換是 3.8 us左右。總的來說,程序執行緒切換還是沒差太多。