在用戶空間我們可以通過fork()函數來創建一個新的進程。fork()是一個glibc标準庫函數,在内核裡邊會有一個系統調用與之對應--sys_fork()。同樣的,我們是通過pthread_create()來創建一個線程,内核中對應的系統調用是clone()。現在通過分析sys_fork()和clone()的異同來看進程和線程的區别。本文是基于5.0版本的Linux内核和2.21版本的glibc。
fork
首先看一下sys_fork(),定義在kernel\fork.c文件中:
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
可以看到sys_fork 會調用 _do_fork。
pthread_create
對于線程的創建,我們先來看glibc源碼(glibc-2.21\sysdeps\unix\sysv\linux\createthread.c):
static int
create_thread (struct pthread *pd, const struct pthread_attr *attr,
bool stopped_start, STACK_VARIABLES_PARMS, bool *thread_ran)
{
......
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETtls | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
TLS_DEFINE_INIT_TP (tp, pd);
if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
clone_flags, pd, &pd->tid, tp, &pd->tid)
== -1))
return errno;
........
}
我們這裡可以看到,在glibc中先設置了一個clone_flag(這個flag标記很重要,最後和進程的一個很大的區别就在這裡),然後陷入到的clone()系統調用:
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
這裡我們發現,創建線程時最後也會調用到 _do_fork函數!那麼在_do_fork()中,創建進程和創建線程有什麼區别呢?我們繼續往下看。
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
......
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
......
if (!IS_ERR(p)) {
struct PID *pid;
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
......
wake_up_new_task(p);
......
put_pid(pid);
}
......
相關視頻推薦
6道經典的linux操作系統面試題,助你了解操作系統底層原理
5000道C “八股文”面試題,還需要死記硬背嗎?
【C 後端】2023年最新技術圖譜,c 後端的8個技術維度,助力你快速成為大牛
需要C/C Linux服務器架構師學習資料加qun812855908獲取(資料包括C/C ,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
在_do_fork這個函數之中最重要的步驟就是 copy_process, copy_process函數中最重要的則是創建新任務的task_struct結構體。對于内核來講進程和線程都是一個task任務,而内核用task_struct來封裝一個任務。 task_struct 的結構示意圖如下所示(圖來源于極客時間趣談Linux操作系統):
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
int retval;
struct task_struct *p;
......
p = dup_task_struct(current, node);
dup_task_struct 主要做了下面幾件事情:
到這裡,整個 task_struct 複制了一份,而且内核棧也創建好了。 繼續看copy_process:
/* copy all the process information */
shm_init_task(p);
retval = security_task_alloc(p, clone_flags);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_security;
retval = copy_files(clone_flags, p);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
if (retval)
goto bad_fork_cleanup_io;
這一段代碼主要是用來複制任務的信息,内容比較多,我們可以從函數名稱就大概猜出具體複制些什麼,這裡主要挑五個比較重要的來講。
copy_files |
主要用于複制一個進程打開的文件信息。這些信息用一個結構 files_struct 來維護,每個打開的文件都有一個文件描述符。 |
copy_fs |
主要用于複制一個進程的目錄信息。這些信息用一個結構 fs_struct 來維護。 |
copy_sighand |
分配一個新的 sighand_struct。這裡最主要的是維護信号處理函數 |
copy_signal |
複制用于維護發給這個進程的信号的數據結構 |
copy_mm |
進程都有自己的内存空間,用 mm_struct 結構來表示。copy_mm 函數中調用 dup_mm,分配一個新的 mm_struct 結構,調用 memcpy 複制這個結構。 |
接下來一個個分析這幾個函數
copy_files
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;
out:
return error;
}
這個函數中我們看到了在glibc create_thread中設置的clone_flags,在上面的代碼中可以看出,如果設置了CLONE_FLES這個位,那麼隻是給主線程的task_struct的files_struct的引用計數count原子地加1,然後就返回了。而如果是進程的創建,CLONE_FLES位沒有被标記,則會在dup_fd裡邊創建一個新的 files_struct,然後将所有的文件描述符數組 fdtable 拷貝一份。dup_fd函數定義在linux-5.0\fs\file.c文件中,這裡就不貼代碼了。
copy_fs
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_fs,在創建進程時是調用 copy_fs_struct 複制一個 fs_struct。如果是創建線程則由于 CLONE_FS 标識位已經被設置,所以也是将原來的 fs_struct 的用戶數加一。這個邏輯和copy_files是一樣的。
copy_sighand
對于和信号相關的的兩個結構體,sighand_struct、signal_struct,處理過程也是類似,如果是進程創建則會複制一分,而線程創建也僅僅是将将原來的 sighand_struct 引用計數加一。
static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)
{
struct sighand_struct *sig;
if (clone_flags & CLONE_SIGHAND) {
atomic_inc(¤t->sighand->count);
return 0;
}
sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
atomic_set(&sig->count, 1);
memcpy(sig->action, current->sighand->action, sizeof(sig->action));
return 0;
copy_signal
對于 copy_signal,進程的創建是創建一個新的 signal_struct,而線程的創建則因為 CLONE_THREAD 直接返回了。
static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)
{
struct signal_struct *sig;
if (clone_flags & CLONE_THREAD)
return 0;
sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);
tsk->signal = sig;
init_sigpending(&sig->shared_pending);
......
}
copy_mm
對于 copy_mm,進程創建是是調用 dup_mm 複制一個 mm_struct,線程的創建則因為 CLONE_VM 标識位而直接指向了原來的 mm_struct。
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) {
mmget(oldmm);
mm = oldmm;
goto good_mm;
}
mm = dup_mm(tsk);
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
}
總結
綜上所述,創建進程的時候調用的系統調用是 fork,在 copy_process 函數裡面,會将五個最重要的結構體 files_struct、fs_struct、sighand_struct、signal_struct、mm_struct 都複制一遍,從此父進程和子進程各用各的數據結構,各有一個獨立的内存空間。而創建線程的話,調用的是系統調用 clone,在 copy_process 函數裡面, 五大結構僅僅是引用計數加一,也即線程共享進程的數據結構。
這裡在次引用極客時間趣談Linux操作系統的一張圖總結一下:
,
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!