fork()的功能是複製進程,首先我們要清楚進程是什麼。進程是一個正在運行的進程,是資源分配的最小單位。系統對進程的管理是通過對進程控制塊(PCB)的管理實現的。每個進程的產生分爲兩步:1.分配PCB;2.準備進程實體,如分配內存空間等。
fork()、pthread_create()、vfork()對應的系統調用分別是sys_fork(),sys_clone(),sys_vfork(),它們的底層都是通過do_fork()實現的。
do_fork()的工作步驟:1.定義PCB指針struct task_struct *p;2.分配PID;3.調用copy_process()方法,創建子進程的進程描述符,即task_struct(重點)。
/**
* 負責處理clone,fork,vfork系統調用。
* clone_flags-與clone的flag參數相同
* stack_start-與clone的child_stack相同
* regs-指向通用寄存器的值。是在從用戶態切換到內核態時被保存到內核態堆棧中的。
* stack_size-未使用,總是爲0
* parent_tidptr,child_tidptr-clone中對應參數ptid,ctid相同
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
/**
* 通過查找pidmap_array位圖,爲子進程分配新的pid參數.
*/
long pid = alloc_pidmap();
if (pid < 0)
return -EAGAIN;
/**
* 如果父進程正在被跟蹤,就檢查debugger程序是否想跟蹤子進程.並且子進程不是內核進程(CLONE_UNTRACED未設置)
* 那麼就設置CLONE_PTRACE標誌.
*/
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
/**
* copy_process複製進程描述符.如果所有必須的資源都是可用的,該函數返回剛創建的task_struct描述符的地址.
* 這是創建進程的關鍵步驟.
*/
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
do_fork()第三部工作,即調用copy_process()。該函數的功能:創建進程描述符以及子進程執行所需要的所有其他數據結構。
1.分配PCB,繼承父進程中PCB的值,只是將特有的信息改過來。通過dup_task_struct(current)來獲取進程描述符(PCB)。在dup_task_struct(current)之前都是對進程的一些判斷(檢查標誌位合法性)和安全性檢查。
/**
* 創建進程描述符以及子進程執行所需要的所有其他數據結構
* 它的參數與do_fork相同。外加子進程的PID。
*/
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
{
int retval;
struct task_struct *p = NULL;
/**
* 檢查clone_flags所傳標誌的一致性。
*/
/**
* 如果CLONE_NEWNS和CLONE_FS標誌都被設置,返回錯誤
*/
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/*
* Thread groups must share signals as well, and detached threads
* can only be started up within the thread group.
*/
/**
* CLONE_THREAD標誌被設置,並且CLONE_SIGHAND沒有設置。
* (同一線程組中的輕量級進程必須共享信號)
*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* Shared signal handlers imply shared VM. By way of the above,
* thread groups also imply shared VM. Blocking this case allows
* for various simplifications in other code.
*/
/**
* CLONE_SIGHAND被設置,但是CLONE_VM沒有設置。
* (共享信號處理程序的輕量級進程也必須共享內存描述符)
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/**
* 通過調用security_task_create以及稍後調用security_task_alloc執行所有附加的安全檢查。
* LINUX2.6提供擴展安全性的鉤子函數,與傳統unix相比,它具有更加強壯的安全模型。
*/
retval = security_task_create(clone_flags);
if (retval)
goto fork_out;
retval = -ENOMEM;
/**
* 調用dup_task_struct爲子進程獲取進程描述符。
*/
p = dup_task_struct(current);
每個進程都有thread_info指針,thread_info結構體保存的是進程上下文的信息。要修改thread_info *info,子進程的task_struct的成員struct thread_info *info指向自己的struct thread_info,而且struct thread_info結構體的成員struct task_struct *p指向子進程自己的struct task_struct。
/**
* 爲子進程獲取進程描述符。
*/
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
/**
* prepare_to_copy中會調用unlazy_fpu。
* 它把FPU、MMX和SSE/SSE2寄存器的內容保存到父進程的thread_info結構中。
* 稍後,dup_task_struct將把這些值複製到子進程的thread_info中。
*/
prepare_to_copy(orig);
/**
* alloc_task_struct宏爲新進程獲取進程描述符,並將描述符保存到tsk局部變量中。
*/
tsk = alloc_task_struct();
if (!tsk)
return NULL;
/**
* alloc_thread_info宏獲取一塊空閒內存區,用來存放新進程的thread_info結構和內核棧。
* 這塊內存區字段的大小是8KB或者4KB。
*/
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
/**
* 將current進程描述符的內容複製到tsk所指向的task_struct結構中,然後把tsk_thread_info置爲ti
* 將current進程的thread_info內容複製給ti指向的結構中,並將ti_task置爲tsk.
*/
*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
ti->task = tsk;
/* One for us, one for whoever does the "release_task()" (usually parent) */
/**
* 把新進程描述符的使用計數器usage設置爲2,用來表示描述符正在被使用而且其相應的進程處於活動狀態。
* 進程狀態既不是EXIT_ZOMBIE,也不是EXIT_DEAD
*/
atomic_set(&tsk->usage,2);
return tsk;
}
/* copy all the process information */
/**
* copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal
* copy_mm,copy_keys,copy_namespace創建新的數據結構,並把父進程相應數據結構的值複製到新數據結構中。
* 除非clone_flags參數指出它們有不同的值。
*/
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))
goto bad_fork_cleanup_sighand;
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
if ((retval = copy_keys(clone_flags, p)))
goto bad_fork_cleanup_mm;
if ((retval = copy_namespace(clone_flags, p)))
goto bad_fork_cleanup_keys;
2.複製父進程打開的文件描述符,通過copy_files()實現。
/**
* 複製進程文件描述符
*/
static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
{
struct files_struct *oldf, *newf;
struct file **old_fds, **new_fds;
int open_files, size, i, error = 0, expand;
/*
* A background process may not have any files ...
*/
oldf = current->files;
if (!oldf)
goto out;
if (clone_flags & CLONE_FILES) {
atomic_inc(&oldf->count);
goto out;
}
/*
* Note: we may be using current for both targets (See exec.c)
* This works because we cache current->files (old) as oldf. Don't
* break this.
*/
tsk->files = NULL;
error = -ENOMEM;
newf = kmem_cache_alloc(files_cachep, SLAB_KERNEL);
if (!newf)
goto out;
atomic_set(&newf->count, 1);
spin_lock_init(&newf->file_lock);
newf->next_fd = 0;
newf->max_fds = NR_OPEN_DEFAULT;
newf->max_fdset = __FD_SETSIZE;
newf->close_on_exec = &newf->close_on_exec_init;
newf->open_fds = &newf->open_fds_init;
newf->fd = &newf->fd_array[0];
spin_lock(&oldf->file_lock);
open_files = count_open_files(oldf, oldf->max_fdset);
expand = 0;
/*
* Check whether we need to allocate a larger fd array or fd set.
* Note: we're not a clone task, so the open count won't change.
*/
if (open_files > newf->max_fdset) {
newf->max_fdset = 0;
expand = 1;
}
if (open_files > newf->max_fds) {
newf->max_fds = 0;
expand = 1;
}
/* if the old fdset gets grown now, we'll only copy up to "size" fds */
if (expand) {
spin_unlock(&oldf->file_lock);
spin_lock(&newf->file_lock);
error = expand_files(newf, open_files-1);
spin_unlock(&newf->file_lock);
if (error < 0)
goto out_release;
spin_lock(&oldf->file_lock);
}
old_fds = oldf->fd;
new_fds = newf->fd;
memcpy(newf->open_fds->fds_bits, oldf->open_fds->fds_bits, open_files/8);
memcpy(newf->close_on_exec->fds_bits, oldf->close_on_exec->fds_bits, open_files/8);
for (i = open_files; i != 0; i--) {
struct file *f = *old_fds++;
if (f) {
get_file(f);
} else {
/*
* The fd may be claimed in the fd bitmap but not yet
* instantiated in the files array if a sibling thread
* is partway through open(). So make sure that this
* fd is available to the new process.
*/
FD_CLR(open_files - i, newf->open_fds);
}
*new_fds++ = f;
}
spin_unlock(&oldf->file_lock);
/* compute the remainder to be cleared */
size = (newf->max_fds - open_files) * sizeof(struct file *);
/* This is long word aligned thus could use a optimized version */
memset(new_fds, 0, size);
if (newf->max_fdset > open_files) {
int left = (newf->max_fdset-open_files)/8;
int start = open_files / (8 * sizeof(unsigned long));
memset(&newf->open_fds->fds_bits[start], 0, left);
memset(&newf->close_on_exec->fds_bits[start], 0, left);
}
tsk->files = newf;
error = 0;
out:
return error;
out_release:
free_fdset (newf->close_on_exec, newf->max_fdset);
free_fdset (newf->open_fds, newf->max_fdset);
free_fd_array(newf->fd, newf->max_fds);
kmem_cache_free(files_cachep, newf);
goto out;
}
3.複製內存空間、線性區和頁表等。
(1)複製內存空間通過copy_mm()實現。struct mm_struct *mm表示進程所擁有的內存空間的描述符,對於內核線程的mm爲NULL。 struct mm_struct *active__mm表示進程運行時所需要使用的進程描述符。
先判斷是否設置了CLONE_VM標誌。如果已設置,則創建新進程,新進程共享父進程的地址空間,將mm_user加1,然後mm=oldmm,把父進程的mm_struct指針賦給子進程的mm_struct。
如果沒有設置,copy_mm()首先會申請一個mm_struct結構體,複製父進程的mm_struct內容。我們可以想象即使是複製用戶空間,子進程和父進程能夠具有相同的目錄表嗎?顯然不能!因此,在申請一個mm_struct後,會調用mm_init(mm)爲子進程申請一個新的目錄表。接下來就是二層循環將父進程的頁表項填入到子進程相應的頁表中,這個過程中包括子進程頁表的申請。總之,子進程複製父進程的用戶空間,僅僅是將父進程頁面表項內容填到子進程的頁表中,因爲頁面表項的內容纔是真正指向頁面的地址。
/**
* 當創建一個新的進程時,內核調用copy_mm函數,
* 這個函數通過建立新進程的所有頁表和內存描述符來創建進程的地址空間。
* 通常,每個進程都有自己的地址空間,但是輕量級進程共享同一地址空間,即允許它們對同一組頁進行尋址。
*/
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
/**
* 內核線程??
*/
if (!oldmm)
return 0;
/**
* 指定了CLONE_VM標誌,表示創建線程。
*/
if (clone_flags & CLONE_VM) {
/**
* 新線程共享父進程的地址空間,所以需要將mm_users加一。
*/
atomic_inc(&oldmm->mm_users);
mm = oldmm;
/*
* There are cases where the PTL is held to ensure no
* new threads start up in user mode using an mm, which
* allows optimizing out ipis; the tlb_gather_mmu code
* is an example.
*/
/**
* 如果其他CPU持有進程頁表自旋鎖,就通過spin_unlock_wait保證在釋放鎖前,缺頁處理程序不會結果。
* 實際上,這個鎖除了保護頁表,還必須禁止創建新的輕量級進程。因爲它們共享mm描述符
*/
spin_unlock_wait(&oldmm->page_table_lock);
/**
* 在good_mm中,將父進程的地址空間賦給子進程。
* 注意前面對mm的賦值,表示了新線程使用的mm
* 完了,就這麼簡單
*/
goto good_mm;
}
/**
* 沒有CLONE_VM標誌,就必須創建一個新的地址空間。
* 必須要有地址空間,即使此時並沒有分配內存。
*/
retval = -ENOMEM;
/**
* 分配一個新的內存描述符。把它的地址存放在新進程的mm中。
*/
mm = allocate_mm();
if (!mm)
goto fail_nomem;
/* Copy the current MM stuff.. */
/**
* 並從當前進程複製mm的內容。
*/
memcpy(mm, oldmm, sizeof(*mm));
if (!mm_init(mm))
goto fail_nomem;
/**
* 調用依賴於體系結構的init_new_context。
* 對於80X86來說,該函數檢查當前進程是否有定製的局部描述符表。
* 如果有,就複製一份局部描述符表並把它插入tsk的地址空間
*/
if (init_new_context(tsk,mm))
goto fail_nocontext;
/**
* dup_mmap不但複製了線程區和頁表,也設置了mm的一些屬性.
* 它也會改變父進程的私有,可寫的頁爲只讀的,以使寫時複製機制生效。
*/
retval = dup_mmap(mm, oldmm);
if (retval)
goto free_pt;
mm->hiwater_rss = mm->rss;
mm->hiwater_vm = mm->total_vm;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
free_pt:
mmput(mm);
fail_nomem:
return retval;
fail_nocontext:
/*
* If init_new_context() failed, we cannot use mmput() to free the mm
* because it calls destroy_context()
*/
mm_free_pgd(mm);
free_mm(mm);
return retval;
}
(2)複製線性區和頁表通過dup_mmap()實現。主要作用是設置mm的一些屬性,改變父進程的私有,可寫的頁爲只讀的,以使寫時拷貝技術生效。
/**
* 既複製父進程的線性區,也複製它的頁表。
*/
static inline int dup_mmap(struct mm_struct * mm, struct mm_struct * oldmm)
{
struct vm_area_struct * mpnt, *tmp, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
struct mempolicy *pol;
down_write(&oldmm->mmap_sem);
flush_cache_mm(current->mm);
mm->locked_vm = 0;
mm->mmap = NULL;
mm->mmap_cache = NULL;
mm->free_area_cache = oldmm->mmap_base;
mm->map_count = 0;
mm->rss = 0;
mm->anon_rss = 0;
cpus_clear(mm->cpu_vm_mask);
mm->mm_rb = RB_ROOT;
rb_link = &mm->mm_rb.rb_node;
rb_parent = NULL;
pprev = &mm->mmap;
/**
* 複製父進程的每一個vm_area_struct線性區描述符,並把複製品插入到子進程的線性區鏈表和紅黑樹中。
*/
for (mpnt = current->mm->mmap ; mpnt ; mpnt = mpnt->vm_next) {
struct file *file;
if (mpnt->vm_flags & VM_DONTCOPY) {
__vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
-vma_pages(mpnt));
continue;
}
charge = 0;
if (mpnt->vm_flags & VM_ACCOUNT) {
unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
if (security_vm_enough_memory(len))
goto fail_nomem;
charge = len;
}
tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!tmp)
goto fail_nomem;
*tmp = *mpnt;
pol = mpol_copy(vma_policy(mpnt));
retval = PTR_ERR(pol);
if (IS_ERR(pol))
goto fail_nomem_policy;
vma_set_policy(tmp, pol);
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_mm = mm;
tmp->vm_next = NULL;
anon_vma_link(tmp);
file = tmp->vm_file;
if (file) {
struct inode *inode = file->f_dentry->d_inode;
get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
/* insert tmp into the share list, just after mpnt */
spin_lock(&file->f_mapping->i_mmap_lock);
tmp->vm_truncate_count = mpnt->vm_truncate_count;
flush_dcache_mmap_lock(file->f_mapping);
vma_prio_tree_add(tmp, mpnt);
flush_dcache_mmap_unlock(file->f_mapping);
spin_unlock(&file->f_mapping->i_mmap_lock);
}
/*
* Link in the new vma and copy the page table entries:
* link in first so that swapoff can see swap entries,
* and try_to_unmap_one's find_vma find the new vma.
*/
spin_lock(&mm->page_table_lock);
*pprev = tmp;
pprev = &tmp->vm_next;
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
mm->map_count++;
/**
* copy_page_range創建必要的頁表來映射線性區所包含的一組頁。並且初始化新頁表的表項。
* 對私有、可寫的頁(無VM_SHARED標誌,有VM_MAYWRITE標誌),對父子進程都標記爲只讀的。
* 爲寫時複製進行處理。
*/
retval = copy_page_range(mm, current->mm, tmp);
spin_unlock(&mm->page_table_lock);
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
if (retval)
goto out;
}
retval = 0;
out:
flush_tlb_mm(current->mm);
up_write(&oldmm->mmap_sem);
return retval;
fail_nomem_policy:
kmem_cache_free(vm_area_cachep, tmp);
fail_nomem:
retval = -ENOMEM;
vm_unacct_memory(charge);
goto out;
}
4.複製進程的內核棧。
複製進程的內核棧,通過調用copy_thread()實現,用調用do_fork()時CPU寄存器的值(它們還保存在父進程的內核棧中)來初始化子進程的內核棧。不過,copy_thread()把eax寄存器對應字段的值(fork子進程中的返回值)設置爲0。子進程描述符的thread.esp0字段初始化爲子進程內核棧的基地址,子進程被調度時,thread.esp0會寫入到TSS中的esp0,用於進程堆棧切換。ret_from_fork的地址存放在thread.eip中,用於設置子進程初次運行時指令執行起點。這就是爲什麼父子進程沿着統一位置執行,以及子進程的返回值是0。
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
struct task_struct *tsk;
int err;
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p->thread_info)) - 1;
*childregs = *regs;
childregs->eax = 0;
childregs->esp = esp;
p->thread.esp = (unsigned long) childregs;
p->thread.esp0 = (unsigned long) (childregs+1);
p->thread.eip = (unsigned long) ret_from_fork;
savesegment(fs,p->thread.fs);
savesegment(gs,p->thread.gs);
tsk = current;
if (unlikely(NULL != tsk->thread.io_bitmap_ptr)) {
p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES);
}
/*
* Set a new TLS for the child thread?
*/
if (clone_flags & CLONE_SETTLS) {
struct desc_struct *desc;
struct user_desc info;
int idx;
err = -EFAULT;
if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
goto out;
err = -EINVAL;
if (LDT_empty(&info))
goto out;
idx = info.entry_number;
if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
goto out;
desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
desc->a = LDT_entry_a(&info);
desc->b = LDT_entry_b(&info);
}
err = 0;
out:
if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = 0;
}
return err;
}
這時,一個完整的子進程已經誕生了,這時子進程還不在進程可執行隊列中,不能接受調度,但是隨後就會通過wake_up_process(p)將其加入可執行隊列接受調度。而父進程也就是當前進程繼續執行,最後,do_fork()將子進程的pid作爲返回值返回。這樣子進程被調度時的返回值和父進程從do_fork()返回時就不一樣了。