【技術篇】fork源碼剖析、寫實拷貝技術

(一)預備知識

1:clone、fork、vfork三個系統調用的實現都是通過 do_ fork()實現的,不同的是對do_ fork()的調用參數。關鍵是這些參數起的作用;系統調用clone()的主要用途是創建一個線程,這個線程可以是內核線程,也可以是用戶線程。創建用戶控件線程時,可以給定子線程用戶空間堆棧的位置,還可以指定子進程運行的起點。同時也可以用clone()創建進程,有選擇的賦值父進程的資源;fork()是全面的複製父進程的資源創建進程;vfork()的作用的創建一個線程,主要作用只是爲作爲創建進程的中間步驟,目的在於提高創建時的效率,減少系統的開銷;

2:內核通過一個task_struct的結構體來抽象一個進程,結構體的屬性用來描述一個進程所處的狀態、進程的標誌、進程是否被其他進程跟蹤、進程鎖的深度,進程的優先級、進程的pid、進程的父母、進程的孩子鏈表、進程所打開的文件描述表、進程所處的文件系統、進程的信號等一些描述進程的信息。satk_struct結構體中的部分成員如下所示:

task_struct{
    volatile long state;    
    void *stack;
    atomic_t usage;
    unsigned int flags; 
    unsigned int ptrace;

    int lock_depth;     
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
    const struct sched_class *sched_class;
    struct sched_entity se;
    struct sched_rt_entity rt;
    unsigned char fpu_counter;
    struct list_head tasks;
    struct plist_node pushable_tasks;

    struct mm_struct *mm, *active_mm;
    pid_t pid;
    struct task_struct *real_parent; 
    struct task_struct *parent; 
    struct list_head children;  
    struct fs_struct *fs;
    struct files_struct *files;
    struct signal_struct *signal;
}

3:

地址重定向(從邏輯地址到物理地址的映射稱)

  •          靜態重定向--在程序裝入主存時已經完成了邏輯地址到物理地址和變換,在程序執行期間不會再發生改變。
  •          動態重定向--程序執行期間完成,其實現依賴於硬件地址變換機構,如基址寄存器。

邏輯地址:CPU所生成的地址。CPU產生的邏輯地址被分爲 :p (頁號) 它包含每個頁在物理內存中的基址,用來作爲頁表的索引;d (頁偏移),同基址相結合,用來確定送入內存設備的物理內存地址。
物理地址:內存單元所看到的地址。

      用戶程序看不見真正的物理地址。用戶只生成邏輯地址,且認爲進程的地址空間爲0到max。物理地址範圍從R+0到R+max,R爲基地址,地址映射-將程序地址空間中使用的邏輯地址變換成內存中的物理地址的過程。由內存管理單元(MMU)來完成。fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有進程空間的各段的內容要發生變化時,纔會將父進程的內容複製一份給子進程。在fork之後exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行爲發生時,再爲子進程相應的段分配物理空間,如果不是因爲exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因爲exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。

     fork時子進程獲得父進程數據空間、堆和棧的複製,所以變量的地址(當然是虛擬地址)也是一樣的。

     每個進程都有自己的虛擬地址空間,不同進程的相同的虛擬地址顯然可以對應不同的物理地址。因此地址相同(虛擬地址)而值不同沒什麼奇怪。具體過程: fork子進程完全複製父進程的棧空間,也複製了頁表,但沒有複製物理頁面,所以這時虛擬地址相同,物理地址也相同,但是會把父子共享的頁面標記爲“只讀”(類似mmap的private的方式),如果父子進程一直對這個頁面是同一個頁面,知道其中任何一個進程要對共享的頁面“寫操作”,這時內核會複製一個物理頁面給這個進程使用,同時修改頁表。而把原來的只讀頁面標記爲“可寫”,留給另外一個進程使用。這就是所謂的“寫時複製”。正因爲fork採用了這種寫時複製的機制,所以fork出來子進程之後,父子進程哪個先調度呢?內核一般會先調度子進程,因爲很多情況下子進程是要馬上執行exec,會清空棧、堆。。這些和父進程共享的空間,加載新的代碼段。。。,這就避免了“寫時複製”拷貝共享頁面的機會。如果父進程先調度很可能寫共享頁面,會產生“寫時複製”的無用功。所以,一般是子進程先調度。

(二)do_fork()函數

   2.1函數原型

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)

參數clone_flags:該參數是此函數中最重要的一個參數,該值中的每個位都代表對子進程task_struct中的每種屬性的設置;標誌內容如下所示:

 #define CSIGNAL     0x000000ff  /* signal mask to be sent at exit */
 #define CLONE_VM    0x00000100  /* set if VM shared between processes */
 #define CLONE_FS    0x00000200  /* set if fs info shared between processes */
 #define CLONE_FILES 0x00000400  /* set if open files shared between processes */
 #define CLONE_SIGHAND   0x00000800  /* set if signal handlers and blocked signals shared */
 #define CLONE_PTRACE    0x00002000  /* set if we want to let tracing continue on the child too */
 #define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release */
 #define CLONE_PARENT    0x00008000  /* set if we want to have the same parent as the cloner */
 #define CLONE_THREAD    0x00010000  /* Same thread group? */                                                                       
 #define CLONE_NEWNS 0x00020000  /* New namespace group? */
 #define CLONE_SYSVSEM   0x00040000  /* share system V SEM_UNDO semantics */
 #define CLONE_SETTLS    0x00080000  /* create a new TLS for the child */
 #define CLONE_PARENT_SETTID 0x00100000  /* set the TID in the parent */
 #define CLONE_CHILD_CLEARTID    0x00200000  /* clear the TID in the child */
 #define CLONE_DETACHED      0x00400000  /* Unused, ignored */
 #define CLONE_UNTRACED      0x00800000  /* set if the tracing process can't force CLONE_PTRACE on this clone */
 #define CLONE_CHILD_SETTID  0x01000000  /* set the TID in the child */
 #define CLONE_STOPPED       0x02000000  /* Start in stopped state */
 #define CLONE_NEWUTS        0x04000000  /* New utsname group? */
 #define CLONE_NEWIPC        0x08000000  /* New ipcs */
 #define CLONE_NEWUSER       0x10000000  /* New user namespace */
 #define CLONE_NEWPID        0x20000000  /* New pid namespace */
 #define CLONE_NEWNET        0x40000000  /* New network namespace */
 #define CLONE_IO        0x80000000  /* Clone io context */


參數stack_start:子進程用戶態堆棧的開始地址
參數regs:當系統發生系統調用時,需從用戶態切換到內核態,此結構體用來保存此時用戶態進程中的通用寄存器中的值,並被存放在內核態堆棧中
參數stack_size:目前未被使用,通常設爲0
參數parent_tidptr:父進程在用戶態下pid的地址
參數child_tidptr:子進程在用戶態下pid的地址


2.2函數的實現流程

   2.2.1do_fork開始執行的第一步是定義一個新的task_struct指針;   

struct task_struct *p;

   2.2.2檢查一些矛盾的clone_flags組合;

    eg:當同時設置CLONE_NEWUSER標誌,和CLONE_THREAD標誌,時就會產生錯誤;語句如下所示:

if (clone_flags & CLONE_NEWUSER) 
{
    if (clone_flags & CLONE_THREAD)
        return -EINVAL;
}

 2.2.3copy_process()函數登場;

    copy_process()函數的流程如下:

 (1)調用dup_task_struct()函數爲新的進程創建一個內核棧,thread_info結構體和task_struct等並且值與父進程完全相同;每一個進程描述符裏都有一個thread_info指針,thread_info結構體保存的是進程上下文的信息。要修改thread_info *info,子進程的task_struct成員struct thread_info *info指向自己申請的struct thread_info。而在struct thread_info結構體中有一個struct task_struct類型的指針,這個指針應指向自己的struct task_struct。

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    unsigned long *stackend;

    int err;

    prepare_to_copy(orig);
    //爲tsk分配內存空間
    tsk = alloc_task_struct();
    if (!tsk)
        return NULL;

    //爲ti分配內存空間
    ti = alloc_thread_info(tsk);
    if (!ti) {
        free_task_struct(tsk);
        return NULL;
    }

    賦值orig屬性給新的tsk
    err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto out;

    tsk->stack = ti;

    //初始化進程緩存髒數據
    err = prop_local_init_single(&tsk->dirties);
    if (err)
        goto out;

    //設置線程棧空間
    setup_thread_stack(tsk, orig);
    stackend = end_of_stack(tsk);
    *stackend = STACK_END_MAGIC;    /* for overflow detection */

#ifdef CONFIG_CC_STACKPROTECTOR
    tsk->stack_canary = get_random_int();
#endif

    /* One for us, one for whoever does the "release_task()" (usually parent) */
    atomic_set(&tsk->usage,2);
    atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
#endif
    tsk->splice_pipe = NULL;

    account_kernel_stack(ti, 1);

    return tsk;

out:
    free_thread_info(ti);
    free_task_struct(tsk);
    return NULL;
}

(2)檢查並確保新創建的進程後,當前用戶所擁有的進程數量沒有超出給他分配放入資源限制

if (atomic_read(&p->real_cred->user->processes) >=                                                                      
             p->signal->rlim[RLIMIT_NPROC].rlim_cur) 
{
         if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
             p->real_cred->user != INIT_USER)
             goto bad_fork_free;
 }

   (3)子進程着手將自己和父進程分開,從父進程哪裏繼承過來的許多屬性都要被清0,或者設置成一個初始值,但是task_struct中的大多數據還是未被修改,部分代碼如下;

     spin_lock_init(&p->alloc_lock);

     init_sigpending(&p->pending);

     p->utime = cputime_zero;
     p->stime = cputime_zero;
     p->gtime = cputime_zero;
     p->utimescaled = cputime_zero;
     p->stimescaled = cputime_zero;
     p->prev_utime = cputime_zero;
     p->prev_stime = cputime_zero;

     p->default_timer_slack_ns = current->timer_slack_ns;

    task_io_accounting_init(&p->ioac);
     acct_clear_integrals(p);

     posix_cpu_timers_init(p);

     p->lock_depth = -1;     /* -1 = no lock */
     do_posix_clock_monotonic_gettime(&p->start_time);
     p->real_start_time = p->start_time;
     monotonic_to_bootbased(&p->real_start_time);
     p->io_context = NULL;
     p->audit_context = NULL;
#ifdef CONFIG_TRACE_IRQFLAGS
     p->irq_events = 0;
 #ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
     p->hardirqs_enabled = 1;
 #else
     p->hardirqs_enabled = 0;
 #endif
     p->hardirq_enable_ip = 0;
     p->hardirq_enable_event = 0;
     p->hardirq_disable_ip = _THIS_IP_;
     p->hardirq_disable_event = 0;
     p->softirqs_enabled = 1;
     p->softirq_enable_ip = _THIS_IP_;
     p->softirq_enable_event = 0;
     p->softirq_disable_ip = 0;
     p->softirq_disable_event = 0;
     p->hardirq_context = 0;
     p->softirq_context = 0;
 #endif
 #ifdef CONFIG_LOCKDEP
     p->lockdep_depth = 0; /* no locks held yet */
     p->curr_chain_key = 0;
     p->lockdep_recursion = 0;
 #endif

 #ifdef CONFIG_DEBUG_MUTEXES
     p->blocked_on = NULL; /* not blocked yet */
 #endif

     p->bts = NULL;

(4)給子進程分配一個cpu

sched_fork(p, clone_flags);

(5)拷貝父進程的資源

  •  複製父進程打開的文件描述符
 
/**
 * 複製進程文件描述符
 */
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;
}
  • 複製內存空間

    第一步: copy_mm複製地址空間,struct mm_struct *mm, *active_mm,mm表示:進程所擁有的內存空間的描述符,對於內核線程的mm爲NULL,active__mm表示:進程運行時所需要使用的進程描述符

      首先判斷是否設置了CLONE_VM標誌,如果設置,創建進程,新進程共享父進程的地址空間,將mm_user加1,然後mm=oldmm,把父進程的mm_struct指針賦給子進程的mm_struct;如果沒有設置,當前進程分配一個新的內存描述符,mm=allocate_mm(),將它的弟子放在子進程的mm中。再把父進程(*oldmm)的內容拷進(*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;
}

第二步:複製線性區和頁表,設置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;
}
  • 複製進程的內核棧

       調用cpoy_thread,用調用do_fork時CPU寄存器的值(它們還保存在父進程的內核棧中)來初始化子進程的內核棧。不過,copy_thread把eax寄存器對應字段的值(fork子進程中的返回值)設置爲0。子進程描述符的thread.esp字段初始化爲子進程內核棧的基地址ret_from_fork的地址存放在thread.eip中。

 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;
}

(6)調用alloc_pid爲新進程分配一個pid;

pid = alloc_pid(p->nsproxy->pid_ns);  

(7)copy_process做一些收尾工作,並返回新進程的task_struct指針;此時再次回到了do_fork,新創建的子進程被喚醒,並讓其先投入運行;

         if (unlikely(clone_flags & CLONE_STOPPED)) {
             /*
              * We'll start up with an immediate SIGSTOP.
              */
             sigaddset(&p->pending.signal, SIGSTOP);
             set_tsk_thread_flag(p, TIF_SIGPENDING);
             __set_task_state(p, TASK_STOPPED);
         } else {
             //換新新的進程
             wake_up_new_task(p, clone_flags);                                                                               
         }

(三)總結

第一抓住進程被內核抽象成了啥?它的數據結構是咋樣的(task_struct)這點我們必須有所認識,

第二抓住創建進程最主要的其實就是拷貝父進程的task_struct裏的屬性,關鍵點是拷貝哪些,哪些又是子進程和父進程所不同的,很簡單我們只需要把握住進程創建函數裏的clone_flags參數就可以知道咋麼拷貝了。

(四)拓展知識--寫實拷貝技術

         在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有進程空間的各段的內容要發生變化時,纔會將父進程的內容複製一份給子進程。

        那麼子進程的物理空間沒有代碼,怎麼去取指令執行exec系統調用呢?

         在fork之後exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行爲發生時,再爲子進程相應的段分配物理空間,如果不是因爲exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因爲exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。

          還有個細節問題就是,fork之後內核會通過將子進程放在隊列的前面,以讓子進程先執行,以免父進程執行導致寫時複製,而後子進程執行exec系統調用,因無意義的複製而造成效率的下降。(因爲如果讓父進程先執行的話,那麼會進行寫時拷貝,也就是爲子進程分配了相應的數據段、堆棧段的物理空間,如果再執行exec的話,又會爲新的程序分配新的數據段、堆棧段等,這樣fork函數的執行效率就會降低)    

          爲了節約物理內存,在調用fork生成新進程時,新進程與原進程會共享同一內存區。只有當其中一進程進行寫操作時,系統纔會爲其另外分配內存頁面。這就是寫時拷貝(copy on write)的概念的引出。

           當進程A使用系統調用fork創建一個子進程B時,由於子進程B實際上是父進程A的一個拷貝,因此會擁有與父進程相同的物理頁面。也即爲了達到節約內存和加快創建速度的目標,fork函數會讓子進程B以只讀的方式共享父進程A的物理頁面。同時將父進程A對這些物理頁面的訪問權限也設置成只讀。這樣一來當父進程A或者子進程B任何一方對這些以共享的物理頁面執行寫操作時,都會產生頁面出錯異常中斷,此時cpu會執行系統提供的異常處理函數do_wp_page來試圖解決這個異常。

             do_wp_page會對這塊導致寫入異常中斷的物理頁面進行取消共享操作(使用un_up_page),爲寫進程複製一新的物理頁面,使父進程A和子進程B各自擁有一塊內容相同的物理頁面。這時才真正地執行了複製操作(只複製這一塊物理頁面)。並且將要執行寫入操作的這塊物理頁面標記成可以寫訪問的。最後從異常處理函數中返回,cpu就會重新執行剛纔導致異常的寫入操作指令,使進程能夠繼續執行下去
 

參考文章:https://blog.csdn.net/shmily_cml0603/article/details/70215824

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章