Linux下進程的創建過程分析(_do_fork do_fork詳解)--Linux進程的管理與調度(八)

来源:https://www.cnblogs.com/linhaostudy/archive/2018/09/14/9644736.html
-Advertisement-
Play Games

Unix標準的複製進程的系統調用時fork(即分叉),但是Linux,BSD等操作系統並不止實現這一個,確切的說linux實現了三個,fork,vfork,clone(確切說vfork創造出來的是輕量級進程,也叫線程,是共用資源的進程) 系統調用 | 描述 | fork | fork創造的子進程是父 ...


Unix標準的複製進程的系統調用時fork(即分叉),但是Linux,BSD等操作系統並不止實現這一個,確切的說linux實現了三個,fork,vfork,clone(確切說vfork創造出來的是輕量級進程,也叫線程,是共用資源的進程)

系統調用 描述
fork fork創造的子進程是父進程的完整副本,複製了父親進程的資源,包括記憶體的內容task_struct內容
vfork vfork創建的子進程與父進程共用數據段,而且由vfork()創建的子進程將先於父進程運行
clone Linux上創建線程一般使用的是pthread庫 實際上linux也給我們提供了創建線程的系統調用,就是clone

fork, vfork和clone的系統調用的入口地址分別是sys_fork, sys_vfork和sys_clone, 而他們的定義是依賴於體繫結構的, 因為在用戶空間和內核空間之間傳遞參數的方法因體繫結構而異

系統調用的參數傳遞

系統調用的實現與C庫不同, 普通C函數通過將參數的值壓入到進程的棧中進行參數的傳遞。由於系統調用是通過中斷進程從用戶態到內核態的一種特殊的函數調用,沒有用戶態或者內核態的堆棧可以被用來在調用函數和被調函數之間進行參數傳遞。系統調用通過CPU的寄存器來進行參數傳遞。在進行系統調用之前,系統調用的參數被寫入CPU的寄存器,而在實際調用系統服務常式之前,內核將CPU寄存器的內容拷貝到內核堆棧中,實現參數的傳遞。

因此不同的體繫結構可能採用不同的方式或者不同的寄存器來傳遞參數,而上面函數的任務就是從處理器的寄存器中提取用戶空間提供的信息, 並調用體繫結構無關的_do_fork(或者早期的do_fork)函數, 負責進程的複製

即不同的體繫結構可能需要採用不同的方式或者寄存器來存儲函數調用的參數, 因此linux在設計系統調用的時候, 將其劃分成體繫結構相關的層次和體繫結構無關的層次, 前者複雜提取出依賴與體繫結構的特定的參數, 後者則依據參數的設置執行特定的真正操作

fork, vfork, clone系統調用的實現

關於do_fork和_do_fork

The commit 3033f14ab78c32687 (“clone: support passing tls argument via C
rather than pt_regs magic”) introduced _do_fork() that allowed to pass
@tls parameter.

參見 http://lists.openwall.net/linux-kernel/2015/03/13/30

linux2.5.32以後, 添加了TLS(Thread Local Storage)機制, clone的標識CLONE_SETTLS接受一個參數來設置線程的本地存儲區。sys_clone也因此增加了一個int參數來傳入相應的點tls_val。sys_clone通過do_fork來調用copy_process完成進程的複製,它調用特定的copy_thread和copy_thread把相應的系統調用參數從pt_regs寄存器列表中提取出來,但是會導致意外的情況。

only one code path into copy_thread can pass the CLONE_SETTLS flag, and
that code path comes from sys_clone with its architecture-specific
argument-passing order.

因此linux-4.2之後選擇引入一個新的CONFIG_HAVE_COPY_THREAD_TLS,和一個新的COPY_THREAD_TLS接受TLS參數為額外的長整型(系統調用參數大小)的爭論。改變sys_clone的TLS參數unsigned long,並傳遞到copy_thread_tls。

/* http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L2646  */
extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long);
extern long do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *);


/* linux2.5.32以後, 添加了TLS(Thread Local Storage)機制, 
    在最新的linux-4.2中添加了對CLONE_SETTLS 的支持 
    底層的_do_fork實現了對其的支持, 
    dansh*/
#ifndef CONFIG_HAVE_COPY_THREAD_TLS
/* For compatibility with architectures that call do_fork directly rather than
 * using the syscall entry points below. */
long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr)
{
        return _do_fork(clone_flags, stack_start, stack_size,
                        parent_tidptr, child_tidptr, 0);
}
#endif

我們會發現,新版本的系統中clone的TLS設置標識會通過TLS參數傳遞, 因此_do_fork替代了老版本的do_fork。

老版本的do_fork只有在如下情況才會定義

  • 只有當系統不支持通過TLS參數通過參數傳遞而是使用pt_regs寄存器列表傳遞時
  • 未定義CONFIG_HAVE_COPY_THREAD_TLS巨集
參數 描述
clone_flags 與clone()參數flags相同, 用來控制進程複製過的一些屬性信息, 描述你需要從父進程繼承那些資源。該標誌位的4個位元組分為兩部分。最低的一個位元組為子進程結束時發送給父進程的信號代碼,通常為SIGCHLD;剩餘的三個位元組則是各種clone標誌的組合(本文所涉及的標誌含義詳見下表),也就是若幹個標誌之間的或運算。通過clone標誌可以有選擇的對父進程的資源進行複製;
stack_start 與clone()參數stack_start相同, 子進程用戶態堆棧的地址
regs 是一個指向了寄存器集合的指針, 其中以原始形式, 保存了調用的參數, 該參數使用的數據類型是特定體繫結構的struct pt_regs,其中按照系統調用執行時寄存器在內核棧上的存儲順序, 保存了所有的寄存器, 即指向內核態堆棧通用寄存器值的指針,通用寄存器的值是在從用戶態切換到內核態時被保存到內核態堆棧中的(指向pt_regs結構體的指針。當系統發生系統調用,即用戶進程從用戶態切換到內核態時,該結構體保存通用寄存器中的值,並被存放於內核態的堆棧中)
stack_size 用戶狀態下棧的大小, 該參數通常是不必要的, 總被設置為0
parent_tidptr 與clone的ptid參數相同, 父進程在用戶態下pid的地址,該參數在CLONE_PARENT_SETTID標誌被設定時有意義
child_tidptr 與clone的ctid參數相同, 子進程在用戶太下pid的地址,該參數在CLONE_CHILD_SETTID標誌被設定時有意義

其中clone_flags如下表所示

sys_fork的實現

不同體繫結構下的fork實現sys_fork主要是通過標誌集合區分, 在大多數體繫結構上, 典型的fork實現方式與如下

早期實現:

架構 實現
arm arch/arm/kernel/sys_arm.c, line 239
i386 arch/i386/kernel/process.c, line 710
x86_64 arch/x86_64/kernel/process.c, line 706
asmlinkage long sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.rsp, &regs, 0);
}

新版本:

#ifdef __ARCH_WANT_SYS_FORK
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
}
#endif

我們可以看到唯一使用的標誌是SIGCHLD。這意味著在子進程終止後將發送信號SIGCHLD信號通知父進程,

由於寫時複製(COW)技術, 最初父子進程的棧地址相同, 但是如果操作棧地址閉並寫入數據, 則COW機制會為每個進程分別創建一個新的棧副本

如果do_fork成功, 則新建進程的pid作為系統調用的結果返回, 否則返回錯誤碼

sys_vfork的實現

早期實現

架構 實現
arm arch/arm/kernel/sys_arm.c, line 254
i386 arch/i386/kernel/process.c, line 737
x86_64 arch/x86_64/kernel/process.c, line 728
asmlinkage long sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.rsp, &regs, 0);
}

新版本

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
        return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
                        0, NULL, NULL, 0);
}
#endif

可以看到sys_vfork的實現與sys_fork只是略微不同, 前者使用了額外的標誌CLONE_VFORK | CLONE_VM

sys_clone的實現

早期實現

架構 實現
arm arch/arm/kernel/sys_arm.c, line 247
i386 arch/i386/kernel/process.c, line 715
x86_64 arch/x86_64/kernel/process.c, line 711

sys_clone的實現方式與上述系統調用類似, 但實際差別在於do_fork如下調用

casmlinkage int sys_clone(struct pt_regs regs)
{
    /* 註釋中是i385下增加的代碼, 其他體繫結構無此定義
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx;*/
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0);
}

新版本

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 unsigned long, tls,
                 int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
                 int __user *, parent_tidptr,
                 int __user *, child_tidptr,
                 unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
                int, stack_size,
                int __user *, parent_tidptr,
                int __user *, child_tidptr,
                unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 int __user *, child_tidptr,
                 unsigned long, tls)
#endif
{
        return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif

我們可以看到sys_clone的標識不再是硬編碼的, 而是通過各個寄存器參數傳遞到系統調用, 因而我們需要提取這些參數。

另外,clone也不再複製進程的棧, 而是可以指定新的棧地址, 在生成線程時, 可能需要這樣做, 線程可能與父進程共用地址空間, 但是線程自身的棧可能在另外一個地址空間

另外還指令了用戶空間的兩個指針(parent_tidptr和child_tidptr), 用於與線程庫通信

創建子進程的流程

_do_fork和早期do_fork的流程

_do_fork和do_fork在進程的複製的時候並沒有太大的區別, 他們就只是在進程tls複製的過程中實現有細微差別

所有進程複製(創建)的fork機制最終都調用了kernel/fork.c中的_do_fork(一個體繫結構無關的函數),

_do_fork以調用copy_process開始, 後者執行生成新的進程的實際工作, 並根據指定的標誌複製父進程的數據。在子進程生成後, 內核必須執行下列收尾操作:

  1. 調用 copy_process 為子進程複製出一份進程信息
  2. 如果是 vfork(設置了CLONE_VFORK和ptrace標誌)初始化完成處理信息
  3. 調用 wake_up_new_task 將子進程加入調度器,為之分配 CPU
  4. 如果是 vfork,父進程等待子進程完成 exec 替換自己的地址空間

對比,我們從《深入linux內核架構》中找到了早期的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;

    /*
     * Determine whether and which event to report to ptracer.  When
     * called from kernel_thread or CLONE_UNTRACED is explicitly
     * requested, no event is reported; otherwise, report if the event
     * for the type of forking is enabled.
     */
    if (!(clone_flags & CLONE_UNTRACED)) {
    if (clone_flags & CLONE_VFORK)
        trace = PTRACE_EVENT_VFORK;
    else if ((clone_flags & CSIGNAL) != SIGCHLD)
        trace = PTRACE_EVENT_CLONE;
    else
        trace = PTRACE_EVENT_FORK;

    if (likely(!ptrace_event_enabled(current, trace)))
        trace = 0;
    }
    /*  複製進程描述符,copy_process()的返回值是一個 task_struct 指針  */
    p = copy_process(clone_flags, stack_start, stack_size,
         child_tidptr, NULL, trace, tls);
    /*
     * Do this prior waking up the new thread - the thread pointer
     * might get invalid after that point, if the thread exits quickly.
     */
    if (!IS_ERR(p)) {
    struct completion vfork;
    struct pid *pid;

    trace_sched_process_fork(current, p);
    /*  得到新創建的進程的pid信息  */
    pid = get_task_pid(p, PIDTYPE_PID);
    nr = pid_vnr(pid);

    if (clone_flags & CLONE_PARENT_SETTID)
        put_user(nr, parent_tidptr);

    /*  如果調用的 vfork()方法,初始化 vfork 完成處理信息 */
    if (clone_flags & CLONE_VFORK) {
        p->vfork_done = &vfork;
        init_completion(&vfork);
        get_task_struct(p);
    }
    /*  將子進程加入到調度器中,為其分配 CPU,準備執行  */
    wake_up_new_task(p);

    /* forking complete and child started to run, tell ptracer */
    if (unlikely(trace))
        ptrace_event_pid(trace, pid);

    /*  如果是 vfork,將父進程加入至等待隊列,等待子進程完成  */
    if (clone_flags & CLONE_VFORK) {
        if (!wait_for_vfork_done(p, &vfork))
        ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
    }

    put_pid(pid);
    } else {
    nr = PTR_ERR(p);
    }
    return nr;
}

copy_process流程

  1. 調用 dup_task_struct 複製當前的 task_struct
  2. 檢查進程數是否超過限制
  3. 初始化自旋鎖、掛起信號、CPU 定時器等
  4. 調用 sched_fork 初始化進程數據結構,並把進程狀態設置為 TASK_RUNNING
  5. 複製所有進程信息,包括文件系統、信號處理函數、信號、記憶體管理等

對比,我們從《深入linux內核架構》中找到了早期的do_fork流程圖,基本一致,可以用來參考學習和對比

主要的區別其實就是最後的copy_thread更改成為copy_thread_tls

/*
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static 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 retval;
    struct task_struct *p;

    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;
    //  複製當前的 task_struct
    retval = -ENOMEM;
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    ftrace_graph_init_task(p);

    //初始化互斥變數
    rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
    DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
    DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif

    //檢查進程數是否超過限制,由操作系統定義
    retval = -EAGAIN;
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }
    current->flags &= ~PF_NPROC_EXCEEDED;

    retval = copy_creds(p, clone_flags);
    if (retval < 0)
        goto bad_fork_free;

    /*
     * If multiple threads are within copy_process(), then this check
     * triggers too late. This doesn't hurt, the check is only there
     * to stop root fork bombs.
     */
    //檢查進程數是否超過 max_threads 由記憶體大小決定
    retval = -EAGAIN;
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    delayacct_tsk_init(p);  /* Must remain after dup_task_struct() */
    p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
    p->flags |= PF_FORKNOEXEC;
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    rcu_copy_process(p);
    p->vfork_done = NULL;

    //  初始化自旋鎖
    spin_lock_init(&p->alloc_lock);
    //  初始化掛起信號
    init_sigpending(&p->pending);

    //  初始化 CPU 定時器
    posix_cpu_timers_init(p);
    //  ......

    /* Perform scheduler related setup. Assign this task to a CPU. 
        初始化進程數據結構,並把進程狀態設置為 TASK_RUNNING
    */
    retval = sched_fork(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = perf_event_init_task(p);

    /*  複製所有進程信息,包括文件系統、信號處理函數、信號、記憶體管理等
       形式類似於copy_xxx的形式   */
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = audit_alloc(p);
    if (retval)
        goto bad_fork_cleanup_perf;
    /* copy all the process information */
    shm_init_task(p);
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_audit;
    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;
    /*    初始化子進程內核棧
        linux-4.2新增處理TLS
        之前版本是   retval = copy_thread(clone_flags, stack_start, stack_size, p);
        */
    retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
    if (retval)
        goto bad_fork_cleanup_io;

    /*  為新進程分配新的pid  */
    if (pid != &init_struct_pid) {
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (IS_ERR(pid)) {
            retval = PTR_ERR(pid);
            goto bad_fork_cleanup_io;
        }
    }

    /*  設置子進程的pid  */
    /* ok, now we should be set up.. */
    p->pid = pid_nr(pid);
    if (clone_flags & CLONE_THREAD) {
        p->exit_signal = -1;
        p->group_leader = current->group_leader;
        p->tgid = current->tgid;
    } else {
        if (clone_flags & CLONE_PARENT)
            p->exit_signal = current->group_leader->exit_signal;
        else
            p->exit_signal = (clone_flags & CSIGNAL);
        p->group_leader = p;
        p->tgid = p->pid;
    }

    p->nr_dirtied = 0;
    p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
    p->dirty_paused_when = 0;

    p->pdeath_signal = 0;
    INIT_LIST_HEAD(&p->thread_group);
    p->task_works = NULL;

    /*
     * Make it visible to the rest of the system, but dont wake it up yet.
     * Need tasklist lock for parent etc handling!
     */
    write_lock_irq(&tasklist_lock);

    /*  調用fork的進程為其父進程  */
    /* CLONE_PARENT re-uses the old parent */
    if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
        p->real_parent = current->real_parent;
        p->parent_exec_id = current->parent_exec_id;
    } else {
        p->real_parent = current;
        p->parent_exec_id = current->self_exec_id;
    }

    spin_lock(&current->sighand->siglock);

    // ......

    return p;
}

dup_task_struct 流程

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    int node = tsk_fork_get_node(orig);
    int err;

    //分配一個 task_struct 節點
    tsk = alloc_task_struct_node(node);
    if (!tsk)
        return NULL;

    //分配一個 thread_info 節點,包含進程的內核棧,ti 為棧底
    ti = alloc_thread_info_node(tsk, node);
    if (!ti)
        goto free_tsk;

    //將棧底的值賦給新節點的棧
    tsk->stack = ti;

    //……

    return tsk;

}
  1. 調用alloc_task_struct_node分配一個 task_struct 節點
  2. 調用alloc_thread_info_node分配一個 thread_info 節點,其實是分配了一個thread_union聯合體,將棧底返回給 ti
union thread_union {
   struct thread_info thread_info;
  unsigned long stack[THREAD_SIZE/sizeof(long)];
};
  1. 最後將棧底的值 ti 賦值給新節點的棧
  2. 最終執行完dup_task_struct之後,子進程除了tsk->stack指針不同之外,全部都一樣!

sched_fork 流程

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
    unsigned long flags;
    int cpu = get_cpu();

    __sched_fork(clone_flags, p);

    //  將子進程狀態設置為 TASK_RUNNING
    p->state = TASK_RUNNING;

    //  ……

    //  為子進程分配 CPU
    set_task_cpu(p, cpu);

    put_cpu();
    return 0;
}

我們可以看到sched_fork大致完成了兩項重要工作,

  • 一是將子進程狀態設置為 TASK_RUNNING,
  • 二是為其分配 CPU

copy_thread和copy_thread_tls流程

我們可以看到linux-4.2之後增加了copy_thread_tls函數和CONFIG_HAVE_COPY_THREAD_TLS巨集

但是如果未定義CONFIG_HAVE_COPY_THREAD_TLS巨集預設則使用copy_thread同時將定義copy_thread_tls為copy_thread

單獨將這個函數是因為這個複製操作與其他操作都不相同, 這是一個特定於體繫結構的函數,用於複製進程中特定於線程(thread-special)的數據, 重要的就是填充task_struct->thread的各個成員,這是一個thread_struct類型的結構, 其定義是依賴於體繫結構的。它包含了所有寄存器(和其他信息),內核在進程之間切換時需要保存和恢復的進程的信息。

該函數用於設置子進程的執行環境,如子進程運行時各CPU寄存器的值、子進程的內核棧的起始地址(指向內核棧的指針通常也是保存在一個特別保留的寄存器中)

#ifdef CONFIG_HAVE_COPY_THREAD_TLS
extern int copy_thread_tls(unsigned long, unsigned long, unsigned long,
            struct task_struct *, unsigned long);
#else
extern int copy_thread(unsigned long, unsigned long, unsigned long,
            struct task_struct *);

/* Architectures that haven't opted into copy_thread_tls get the tls argument
 * via pt_regs, so ignore the tls argument passed via C. */
static inline int copy_thread_tls(
        unsigned long clone_flags, unsigned long sp, unsigned long arg,
        struct task_struct *p, unsigned long tls)
{
    return copy_thread(clone_flags, sp, arg, p);
}
#endif

下麵我們來看32位架構的copy_thread_tls函數,他與原來的copy_thread變動並不大, 只是多了後面TLS的設置信息

int copy_thread_tls(unsigned long clone_flags, unsigned long sp,
    unsigned long arg, struct task_struct *p, unsigned long tls)
{
    struct pt_regs *childregs = task_pt_regs(p);
    struct task_struct *tsk;
    int err;
    /*  獲取寄存器的信息  */
    p->thread.sp = (unsigned long) childregs;
    p->thread.sp0 = (unsigned long) (childregs+1);
    memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

    if (unlikely(p->flags & PF_KTHREAD)) {
        /* kernel thread
            內核線程的設置  */
        memset(childregs, 0, sizeof(struct pt_regs));
        p->thread.ip = (unsigned long) ret_from_kernel_thread;
        task_user_gs(p) = __KERNEL_STACK_CANARY;
        childregs->ds = __USER_DS;
        childregs->es = __USER_DS;
        childregs->fs = __KERNEL_PERCPU;
        childregs->bx = sp;     /* function */
        childregs->bp = arg;
        childregs->orig_ax = -1;
        childregs->cs = __KERNEL_CS | get_kernel_rpl();
        childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
        p->thread.io_bitmap_ptr = NULL;
        return 0;
    }
    /*  將當前寄存器信息複製給子進程  */
    *childregs = *current_pt_regs();
    /*  子進程 eax 置 0,因此fork 在子進程返回0  */
    childregs->ax = 0;
    if (sp)
        childregs->sp = sp;
    /*  子進程ip 設置為ret_from_fork,因此子進程從ret_from_fork開始執行  */
    p->thread.ip = (unsigned long) ret_from_fork;
    task_user_gs(p) = get_user_gs(current_pt_regs());

    p->thread.io_bitmap_ptr = NULL;
    tsk = current;
    err = -ENOMEM;

    if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
        p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
                        IO_BITMAP_BYTES, GFP_KERNEL);
        if (!p->thread.io_bitmap_ptr) {
            p->thread.io_bitmap_max = 0;
            return -ENOMEM;
        }
        set_tsk_thread_flag(p, TIF_IO_BITMAP);
    }

    err = 0;

    /*
     * Set a new TLS for the child thread?
     * 為進程設置一個新的TLS
     */
    if (clone_flags & CLONE_SETTLS)
        err = do_set_thread_area(p, -1,
            (struct user_desc __user *)tls, 0);

    if (err && p->thread.io_bitmap_ptr) {
        kfree(p->thread.io_bitmap_ptr);
        p->thread.io_bitmap_max = 0;
    }
    return err;
}

copy_thread_tls 這段代碼為我們解釋了兩個相當重要的問題!

  1. 為什麼 fork 在子進程中返回0,原因是childregs->ax = 0;這段代碼將子進程的 eax 賦值為0
  2. p->thread.ip = (unsigned long) ret_from_fork;將子進程的 ip 設置為 ret_form_fork 的首地址,因此子進程是從 ret_from_fork 開始執行的

總結

fork, vfork和clone的系統調用的入口地址分別是sys_fork, sys_vfork和sys_clone, 而他們的定義是依賴於體繫結構的, 而他們最終都調用了_do_fork(linux-4.2之前的內核中是do_fork),在_do_fork中通過copy_process複製進程的信息,調用wake_up_new_task將子進程加入調度器中

fork系統調用對應的kernel函數是sys_fork,此函數簡單的調用kernel函數_do_fork。一個簡化版的_do_fork執行如下:

  1. copy_process()此函數會做fork的大部分事情,它主要完成講父進程的運行環境複製到新的子進程,比如信號處理、文件描述符和進程的代碼數據等。
  2. wake_up_new_task()。計算此進程的優先順序和其他調度參數,將新的進程加入到進程調度隊列並設此進程為可被調度的,以後這個進程可以被進程調度模塊調度執行。

簡化的copy_process()流程

  1. dup_task_struct()。分配一個新的進程式控制制塊,包括新進程在kernel中的堆棧。新的進程式控制制塊會複製父進程的進程式控制制塊,但是因為每個進程都有一個kernel堆棧,新進程的堆棧將被設置成新分配的堆棧。
  2. 初始化一些新進程的統計信息,如此進程的運行時間
  3. copy_semundo()複製父進程的semaphore undo_list到子進程。
  4. copy_files()、copy_fs()。複製父進程文件系統相關的環境到子進程
  5. copy_sighand()、copy_signal()。複製父進程信號處理相關的環境到子進程。
  6. copy_mm()。複製父進程記憶體管理相關的環境到子進程,包括頁表、地址空間和代碼數據。
  7. copy_thread()/copy_thread_tls。設置子進程的執行環境,如子進程運行時各CPU寄存器的值、子進程的kernel棧的起始地址。
  8. sched_fork()。設置子進程調度相關的參數,即子進程的運行CPU、初始時間片長度和靜態優先順序等。
  9. 將子進程加入到全局的進程隊列中
  10. 設置子進程的進程組ID和對話期ID等。

簡單的說,copy_process()就是將父進程的運行環境複製到子進程並對某些子進程特定的環境做相應的調整。

此外應用程式使用系統調用exit()來結束一個進程,此系統調用接受一個退出原因代碼,父進程可以使用wait()系統調用來獲取此代碼,從而知道子進程退出的原因。對應到kernel,此系統調用sys_exit_group(),它的基本流程如下:

  1. 將信號SIGKILL加入到其他線程的信號隊列中,並喚醒這些線程。
  2. 此線程執行do_exit()來退出。

do_exit()完成線程退出的任務,其主要功能是將線程占用的系統資源釋放,do_exit()的基本流程如下:

  1. 將進程記憶體管理相關的資源釋放
  2. 將進程ICP semaphore相關資源釋放
  3. __exit_files()、__exit_fs()。將進程文件管理相關的資源釋放。
  4. exit_thread()。只要目的是釋放平臺相關的一些資源。
  5. exit_notify()。在Linux中進程退出時要將其退出的原因告訴父進程,父進程調用wait()系統調用後會在一個等待隊列上睡眠。
  6. schedule()。調用進程調度器,因為此進程已經退出,切換到其他進程。

進程的創建到執行過程如下圖所示


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 發現自己的linux水平楞個瓜皮,找個視屏教程學習一哈。 1 linux系統簡介 1.1 UNIX和Linux發展史 unix發展歷史:1969年,美國貝爾實驗室的肯.湯普森開發出unix系統,1971年丹尼斯·里奇發明C語言,1973年,unix用c重寫 硬體平臺的概念 也就是cpu架構 Powe... ...
  • 紅帽企業或CentOS的Linux上安裝MongoDB的社區版: https://docs.mongodb.com/manual/tutorial/install-mongodb-on-red-hat/ 一、安裝 1、配置yum源,在yum源目錄下創建一個文件 mongodb-org-4.0.rep ...
  • 一. shell類型 1.1 互動式 bin/ shell程式 當用戶登錄到某個虛擬控制台終端或是在GUI中啟動終端模擬器時,預設的shell程式就會開始運行。系統啟動什麼樣的shell程式取決於你個人的用戶ID配置,在etc/passwd文件中。如下圖所示,root用戶使用bash shell作為 ...
  • 內核線程 為什麼需要內核線程 Linux內核可以看作一個服務進程(管理軟硬體資源,響應用戶進程的種種合理以及不合理的請求)。 內核需要多個執行流並行,為了防止可能的阻塞,支持多線程是必要的。 內核線程就是內核的分身,一個分身可以處理一件特定事情。內核線程的調度由內核負責,一個內核線程處於阻塞狀態時不 ...
  • 初次接觸分散式文件系統,有很多迷惑。通過參考網路文章,這裡進行對比一下Hadoop 分散式文件系統(HDFS)與 傳統文件系統之間的關係: inode 記錄文件存放的數據區的block指針 每個磁碟都有預設的數據塊大小,這是磁碟進行數據讀/寫的最小單位。而構建於單個磁碟之上的文件系統(linux文件 ...
  • 使用場景: 有時候線上伺服器掛了,或者一些數據推送不正常,一般來說我們需要做的就是將項目重啟運行,或者檢查核對出問題的位置,來快速解決,很多時候我們不得不登上伺服器來查看,這個對於目前工作日益繁忙的我們是一個不小的工作量,所以在此分享大家在linux中做定時任務 環境:在linux或者mac os系 ...
  • 使用場景:前段時間交易所項目需要在伺服器上用到 根據websocket推送價格數據,在交易所內進行下單撤單處理,但是由於有多個交易對,在伺服器上部署時候,略顯繁瑣。(撮合引擎同樣有此問題,可以一併解決) 1:shell使用:在git項目後,這裡每個交易對單獨配一個文件,負責各自的交易處理,此處做項目 ...
  • 接著上文 "IO多路復用(一) Select、Poll、Epoll" ,接下來將演示一個TCP回射程式,源代碼來自於該博文https://www.cnblogs.com/Anker/p/3258674.html 博主的幾篇相關的文章,在這裡將其進行了整合,突出select、poll和epoll不同方 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...