GHOST系统之家 - Windows系统光盘下载网站!
当前位置:GHOST系统之家>系统教程 > Linux 进程管理之进程、线程

Linux 进程管理之进程、线程

来源:Ghost系统之家浏览:时间:2023-07-25 07:27:00

Linux 进程管理之进程、线程

作者:Linux码农 2023-03-05 16:12:41系统 Linux Unix 中关于进程的创建分为2个步骤:fork() 和 exec() (这里 exec 指 exec一系列函数,因为内核实现了多个函数,比如 execv 等)。

进程创建

Unix 中关于进程的创建分为2个步骤:fork() 和 exec() (这里 exec 指 exec一系列函数,因为内核实现了多个函数,比如 execv 等)。

fork() 调用是通过拷贝当前进程来创建子进程。此时父子进程的区别在于pid(本进程号),ppid(父进程号)和一些资源和统计量。exec() 函数用于加载可执行文件开始运行。

以上2个函数完成了进程的创建过程。

创建进程(线程)的方式有3种:fork()、vfork()、clone()。

fork 原理

有关 fork 的系统调用如下

#includepid_t fork(void);

fork() 系统调用返回信息,具体描述如下:

返回值为-1时,表示创建失败。返回值为0时,返回到新创建的子进程。返回值大于0时,返回父进程或调用者。该值为新创建的子进程的进程ID。

当用户调用 fork()时,会进入系统调用 sys_fork()

int sys_fork(struct pt_regs *regs){return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);}从 do_fork 的第一个参数 clone_flags 可知,do_fork 只传递了一个当子进程去世时向父进程发送的信号 SIGCHLD。

参数 clone_flags 有2部分组成,最低字节为信号类型,用于表示子进程去世时向父进程发送的信号。高位表示的是资源和特性的标志位,比如:

#define CSIGNAL 0x000000ff#define CLONE_VM 0x00000100#define CLONE_FS 0x00000200#define CLONE_FILES 0x00000400#define CLONE_SIGHAND 0x00000800#define CLONE_PTRACE 0x00002000#define CLONE_VFORK 0x00004000...

所以对于 fork,这一部分资源标志位全为0,表示对有关资源要进行复制而不是通过增加引用计数进行指针共享。

long do_fork(unsigned long clone_flags, ////资源标志unsigned long stack_start, //子进程用户态堆栈地址struct pt_regs *regs, //寄存器集合指针unsigned long stack_size, //用户态下栈大小,该参数通常是不必要的,为0int __user *parent_tidptr, //父进程在用户态下的pid的地址int __user *child_tidptr) //子进程在用户态下pid的地址{struct task_struct *p;int trace = 0;long nr;...//复制子进程,为子进程复制出一份进程信息p = copy_process(clone_flags, stack_start, regs, stack_size,child_tidptr, NULL);if (!IS_ERR(p)) {struct completion vfork;//fork系统调用要返回新进程的PID,如果设置了CLONE_NEWPID标志,fork操作可能创建了新的PID命名空间,此时要返回发出fork系统调用的进程所在命名空间的进程IDnr = (clone_flags & CLONE_NEWPID) ?task_pid_nr_ns(p, current->nsproxy->pid_ns) :task_pid_vnr(p);...//将任务放入运行队列并将其唤醒wake_up_new_task(p, clone_flags);...if (clone_flags & CLONE_VFORK) {freezer_do_not_count();wait_for_completion(&vfork);freezer_count();if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {current->ptrace_message = nr;ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);}}} else {nr = PTR_ERR(p);}//返回子进程pidreturn nr;}

do_fork 的工作流程如下:

调用 copy_process 为子进程复制一份描述符信息。将子进程加入运行队列并将其唤醒运行。若是调用 vfork() 则父进程等待子进程执行完成。

该函数的流程如下:

在 do_fork中,其核心处理程序为 copy_process,其实现如下

static struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size,int __user *child_tidptr,struct pid *pid){int retval;struct task_struct *p;int cgroup_callbacks_done = 0;if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))return ERR_PTR(-EINVAL);if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))return ERR_PTR(-EINVAL);/** 父子进程共享信号处理函数时必须共享内存地址空间* 这就是为什么fork出来的父子进程有其独立的信号处理函数,因为他们的内存地址空间不同*/if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))return ERR_PTR(-EINVAL);...retval = -ENOMEM;//为新进程创建一个内核栈,此时父子进程的描述符完全相同p = dup_task_struct(current);rt_mutex_init_task(p);retval = -EAGAIN;if (atomic_read(&p->user->processes) >=p->signal->rlim[RLIMIT_NPROC].rlim_cur) {if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&p->user != current->nsproxy->user_ns->root_user)goto bad_fork_free;}//对该user_struct结构的引用计数加1;对该用户所拥有的进程总数量加1atomic_inc(&p->user->__count);atomic_inc(&p->user->processes);get_group_info(p->group_info);//检测系统中进程的总数量(所有用户的进程数加系统的内核线程数)是否超过了max_threads所规定的进程最大数if (nr_threads >= max_threads)goto bad_fork_cleanup_count;if (!try_module_get(task_thread_info(p)->exec_domain->module))goto bad_fork_cleanup_count;if (p->binfmt && !try_module_get(p->binfmt->module))goto bad_fork_cleanup_put_domain;p->did_exec = 0;delayacct_tsk_init(p); //将从do_fork()传递来的的clone_flags赋值给子进程描述符中的对应字段copy_flags(clone_flags, p);INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);p->vfork_done = NULL;spin_lock_init(&p->alloc_lock);clear_tsk_thread_flag(p, TIF_SIGPENDING);init_sigpending(&p->pending);//初始化其中的各个字段,使得子进程和父进程逐渐区别出来p->utime = cputime_zero;p->stime = cputime_zero;...//调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占sched_fork(p, clone_flags);security_task_alloc(p);audit_alloc(p);copy_semundo(clone_flags, p);copy_files(clone_flags, p);copy_fs(clone_flags, p);copy_sighand(clone_flags, p);copy_signal(clone_flags, p);copy_mm(clone_flags, p); copy_keys(clone_flags, p); copy_namespaces(clone_flags, p); retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);if (retval)goto bad_fork_cleanup_namespaces;if (pid != &init_struct_pid) {retval = -ENOMEM;pid = alloc_pid(task_active_pid_ns(p));if (!pid)goto bad_fork_cleanup_namespaces;if (clone_flags & CLONE_NEWPID) {retval = pid_ns_prepare_proc(task_active_pid_ns(p));if (retval < 0)goto bad_fork_free_pid;}}p->pid = pid_nr(pid);p->tgid = p->pid;if (clone_flags & CLONE_THREAD)p->tgid = current->tgid; p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;.../** 如果共享VM或者vfork创建,信号栈清空*/if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)p->sas_ss_sp = p->sas_ss_size = 0;clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);p->parent_exec_id = p->self_exec_id;//exit_signal 为本进程执行exit()系统调用时向父进程发出的信号,p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);//pdeath_signal 为要求父进程在执行exit()时向本进程发出的信号p->pdeath_signal = 0;p->exit_state = 0;p->group_leader = p;INIT_LIST_HEAD(&p->thread_group);INIT_LIST_HEAD(&p->ptrace_children);INIT_LIST_HEAD(&p->ptrace_list);...if (clone_flags & (CLONE_PARENT|CLONE_THREAD))p->real_parent = current->real_parent;elsep->real_parent = current;p->parent = p->real_parent;spin_lock(¤t->sighand->siglock);/** 在fork之前,进程组和会话信号都需要送到父亲结点,而在fork之后,这些信号需要送到父亲和孩子结点。* 如果我们在将新进程添加到进程组的过程中出现一个信号,而这个挂起信号会导致当前进程退出(current),我们的子进程就不能够被kill或者退出了* 所以这里要检测父进程有没有信号被挂起。*/recalc_sigpending();if (signal_pending(current)) {spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);retval = -ERESTARTNOINTR;goto bad_fork_free_pid;}if (clone_flags & CLONE_THREAD) {...}if (likely(p->pid)) {add_parent(p);if (unlikely(p->ptrace & PT_PTRACED))__ptrace_link(p, current->parent);if (thread_group_leader(p)) {if (clone_flags & CLONE_NEWPID)p->nsproxy->pid_ns->child_reaper = p;p->signal->tty = current->signal->tty;set_task_pgrp(p, task_pgrp_nr(current));set_task_session(p, task_session_nr(current));attach_pid(p, PIDTYPE_PGID, task_pgrp(current));attach_pid(p, PIDTYPE_SID, task_session(current));list_add_tail_rcu(&p->tasks, &init_task.tasks);__get_cpu_var(process_counts)++;}attach_pid(p, PIDTYPE_PID, pid);nr_threads++;}total_forks++;spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);proc_fork_connector(p);cgroup_post_fork(p);return p;//错误处理...}

copy_process 处理流程如下:

调用dup_task_struct为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些信息和父进程内容相同。此时父子进程的描述符时完全相同的。对资源限制进行检查,保证新创建子进程后,当前用户所拥有的的进程数没有超过给它分配的资源的限制。对子进程的一些信息开始设置初始值,主要是一些统计信息等。调用 sched_fork 函数执行调度器相关的设置,为这个新进程分配 CPU,把子进程的进程状态为 TASK_RUNNING。复制进程的资源信息比如打开的文件、文件系统信息,信号处理函数、进程地址空间、命令空间等。调用 copy_thread 初始化子进程内核栈。为新进程分配并设置新的 pid。返回 task_struct 进程描述符。

该函数的流程如下:

通过上述 copy_process 可知,子进程完全复制复制了父进程的一些资源信息,如下图

dup_task_struct 完成了子进程内核栈的创建static struct task_struct *dup_task_struct(struct task_struct *orig){struct task_struct *tsk;struct thread_info *ti;int err;prepare_to_copy(orig);//分配一块task_structtsk = alloc_task_struct();//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底ti = alloc_thread_info(tsk);//把父进程task_struct内容复制给子进程*tsk = *orig;//子进程的task_struct指向栈中的thread_infotsk->stack = ti;...//把父进程thread_info复制给子进程的thread_info,然后子进程的thread_info指向子进程的task_structsetup_thread_stack(tsk, orig);...return tsk;}

dup_task_struct 为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些结构中的信息完全复制了父进程信息,同时完成了 thread_info 和 task_struct 之间的关系,如下图

在 copy_process 中通过 dup_task_struct 为子进程分配了描述结构并初始化,完成内核栈的低端数据的初始化,而用作内核堆栈的高端复制初始化由 copy_thread 来完成。

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;//参数regs是保存这个cpu进入内核前夕各个寄存器中的内容而形成的一个pt_regs结构childregs = task_pt_regs(p); //指向内核栈的最高地址*childregs = *regs; //把寄存器中的值存放到内核栈的最高地址//对子进程的内核栈寄存器中的值进行调整childregs->eax = 0; //将eax设置0,子进程被调度运行从系统调用返回时,就返回该值,这也就为什么fork时子进程返回的是0childregs->esp = esp;//将thread.esp设置成子进程系统空间栈中pt_regs结构的其实地址,就好像该子进程以前曾运行过,而在进入内核以后正要返回用户空间时被切换了一样p->thread.esp = (unsigned long) childregs;//esp0 指向子进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量写入TSS 的 esp0 字段,表示这个进程进入0级运行时其堆栈的位置p->thread.esp0 = (unsigned long) (childregs+1);//eip表示当进程下一次被切换进行运行时的切入点,类似于函数调用或中断的返回地址。将此地址设置为ret_from_fork,使得子进程在首次调度运行时从这开始p->thread.eip = (unsigned long) ret_from_fork;//把段寄存器gs的值保存到p->thread.gs中savesegment(gs,p->thread.gs);//在父进程包含I/O访问许可权限位图的情况下,使新创建进程继承父进程的I/O访问许可权限位图.../** Set a new TLS for the child thread?*///在参数clone_flags包含CLONE_SETTLS标记的情况下,设置进程的TLS ...return err;}

Linux 进程管理之进程、线程

进程创建

Unix 中关于进程的创建分为2个步骤:fork() 和 exec() (这里 exec 指 exec一系列函数,因为内核实现了多个函数,比如 execv 等)。

fork() 调用是通过拷贝当前进程来创建子进程。此时父子进程的区别在于pid(本进程号),ppid(父进程号)和一些资源和统计量。exec() 函数用于加载可执行文件开始运行。

以上2个函数完成了进程的创建过程。

创建进程(线程)的方式有3种:fork()、vfork()、clone()。

fork 原理

有关 fork 的系统调用如下

#includepid_tfork(void);

fork() 系统调用返回信息,具体描述如下:

返回值为-1时,表示创建失败。返回值为0时,返回到新创建的子进程。返回值大于0时,返回父进程或调用者。该值为新创建的子进程的进程ID。

当用户调用 fork()时,会进入系统调用 sys_fork()

int sys_fork(struct pt_regs *regs){return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);}

从 do_fork 的第一个参数 clone_flags 可知,do_fork 只传递了一个当子进程去世时向父进程发送的信号 SIGCHLD。

参数 clone_flags 有2部分组成,最低字节为信号类型,用于表示子进程去世时向父进程发送的信号。高位表示的是资源和特性的标志位,比如:

#define CSIGNAL 0x000000ff #define CLONE_VM 0x00000100 #define CLONE_FS 0x00000200 #define CLONE_FILES 0x00000400 #define CLONE_SIGHAND 0x00000800 #define CLONE_PTRACE 0x00002000 #define CLONE_VFORK 0x00004000 ...

所以对于 fork,这一部分资源标志位全为0,表示对有关资源要进行复制而不是通过增加引用计数进行指针共享。

long do_fork(unsigned long clone_flags, ////资源标志unsigned long stack_start, //子进程用户态堆栈地址struct pt_regs *regs, //寄存器集合指针unsigned long stack_size, //用户态下栈大小,该参数通常是不必要的,为0int __user *parent_tidptr, //父进程在用户态下的pid的地址int __user *child_tidptr) //子进程在用户态下pid的地址{struct task_struct *p;int trace = 0;long nr;...//复制子进程,为子进程复制出一份进程信息p = copy_process(clone_flags, stack_start, regs, stack_size,child_tidptr, NULL);if (!IS_ERR(p)) {struct completion vfork;//fork系统调用要返回新进程的PID,如果设置了CLONE_NEWPID标志,fork操作可能创建了新的PID命名空间,此时要返回发出fork系统调用的进程所在命名空间的进程IDnr = (clone_flags & CLONE_NEWPID) ?task_pid_nr_ns(p, current->nsproxy->pid_ns) :task_pid_vnr(p);...//将任务放入运行队列并将其唤醒wake_up_new_task(p, clone_flags);...if (clone_flags & CLONE_VFORK) {freezer_do_not_count();wait_for_completion(&vfork);freezer_count();if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {current->ptrace_message = nr;ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);}}} else {nr = PTR_ERR(p);}//返回子进程pidreturn nr;}

do_fork 的工作流程如下:

调用 copy_process 为子进程复制一份描述符信息。将子进程加入运行队列并将其唤醒运行若是调用 vfork() 则父进程等待子进程执行完成。

该函数的流程如下:

在 do_fork中,其核心处理程序为 copy_process,其实现如下

static struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size,int __user *child_tidptr,struct pid *pid){int retval;struct task_struct *p;int cgroup_callbacks_done = 0;if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))return ERR_PTR(-EINVAL);if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))return ERR_PTR(-EINVAL);/** 父子进程共享信号处理函数时必须共享内存地址空间* 这就是为什么fork出来的父子进程有其独立的信号处理函数,因为他们的内存地址空间不同*/if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))return ERR_PTR(-EINVAL);...retval = -ENOMEM;//为新进程创建一个内核栈,此时父子进程的描述符完全相同p = dup_task_struct(current);rt_mutex_init_task(p);retval = -EAGAIN;if (atomic_read(&p->user->processes) >=p->signal->rlim[RLIMIT_NPROC].rlim_cur) {if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&p->user != current->nsproxy->user_ns->root_user)goto bad_fork_free;}//对该user_struct结构的引用计数加1;对该用户所拥有的进程总数量加1atomic_inc(&p->user->__count);atomic_inc(&p->user->processes);get_group_info(p->group_info);//检测系统中进程的总数量(所有用户的进程数加系统的内核线程数)是否超过了max_threads所规定的进程最大数if (nr_threads >= max_threads)goto bad_fork_cleanup_count;if (!try_module_get(task_thread_info(p)->exec_domain->module))goto bad_fork_cleanup_count;if (p->binfmt && !try_module_get(p->binfmt->module))goto bad_fork_cleanup_put_domain;p->did_exec = 0;delayacct_tsk_init(p); //将从do_fork()传递来的的clone_flags赋值给子进程描述符中的对应字段copy_flags(clone_flags, p);INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);p->vfork_done = NULL;spin_lock_init(&p->alloc_lock);clear_tsk_thread_flag(p, TIF_SIGPENDING);init_sigpending(&p->pending);//初始化其中的各个字段,使得子进程和父进程逐渐区别出来p->utime = cputime_zero;p->stime = cputime_zero;...//调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占sched_fork(p, clone_flags);security_task_alloc(p);audit_alloc(p);copy_semundo(clone_flags, p);copy_files(clone_flags, p);copy_fs(clone_flags, p);copy_sighand(clone_flags, p);copy_signal(clone_flags, p);copy_mm(clone_flags, p); copy_keys(clone_flags, p); copy_namespaces(clone_flags, p); retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);if (retval)goto bad_fork_cleanup_namespaces;if (pid != &init_struct_pid) {retval = -ENOMEM;pid = alloc_pid(task_active_pid_ns(p));if (!pid)goto bad_fork_cleanup_namespaces;if (clone_flags & CLONE_NEWPID) {retval = pid_ns_prepare_proc(task_active_pid_ns(p));if (retval < 0)goto bad_fork_free_pid;}}p->pid = pid_nr(pid);p->tgid = p->pid;if (clone_flags & CLONE_THREAD)p->tgid = current->tgid; p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;.../** 如果共享VM或者vfork创建,信号栈清空*/if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)p->sas_ss_sp = p->sas_ss_size = 0;clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);p->parent_exec_id = p->self_exec_id;//exit_signal 为本进程执行exit()系统调用时向父进程发出的信号,p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);//pdeath_signal 为要求父进程在执行exit()时向本进程发出的信号p->pdeath_signal = 0;p->exit_state = 0;p->group_leader = p;INIT_LIST_HEAD(&p->thread_group);INIT_LIST_HEAD(&p->ptrace_children);INIT_LIST_HEAD(&p->ptrace_list);...if (clone_flags & (CLONE_PARENT|CLONE_THREAD))p->real_parent = current->real_parent;elsep->real_parent = current;p->parent = p->real_parent;spin_lock(¤t->sighand->siglock);/** 在fork之前,进程组和会话信号都需要送到父亲结点,而在fork之后,这些信号需要送到父亲和孩子结点。* 如果我们在将新进程添加到进程组的过程中出现一个信号,而这个挂起信号会导致当前进程退出(current),我们的子进程就不能够被kill或者退出了* 所以这里要检测父进程有没有信号被挂起。*/recalc_sigpending();if (signal_pending(current)) {spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);retval = -ERESTARTNOINTR;goto bad_fork_free_pid;}if (clone_flags & CLONE_THREAD) {...}if (likely(p->pid)) {add_parent(p);if (unlikely(p->ptrace & PT_PTRACED))__ptrace_link(p, current->parent);if (thread_group_leader(p)) {if (clone_flags & CLONE_NEWPID)p->nsproxy->pid_ns->child_reaper = p;p->signal->tty = current->signal->tty;set_task_pgrp(p, task_pgrp_nr(current));set_task_session(p, task_session_nr(current));attach_pid(p, PIDTYPE_PGID, task_pgrp(current));attach_pid(p, PIDTYPE_SID, task_session(current));list_add_tail_rcu(&p->tasks, &init_task.tasks);__get_cpu_var(process_counts)++;}attach_pid(p, PIDTYPE_PID, pid);nr_threads++;}total_forks++;spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);proc_fork_connector(p);cgroup_post_fork(p);return p;//错误处理...}

copy_process 处理流程如下:

调用dup_task_struct为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些信息和父进程内容相同。此时父子进程的描述符时完全相同的。对资源限制进行检查,保证新创建子进程后,当前用户所拥有的的进程数没有超过给它分配的资源的限制。对子进程的一些信息开始设置初始值,主要是一些统计信息等。调用 sched_fork 函数执行调度器相关的设置,为这个新进程分配 CPU,把子进程的进程状态为 TASK_RUNNING。复制进程的资源信息比如打开的文件、文件系统信息,信号处理函数、进程地址空间、命令空间等。调用 copy_thread 初始化子进程内核栈。为新进程分配并设置新的 pid。返回 task_struct 进程描述符。

该函数的流程如下:

通过上述 copy_process 可知,子进程完全复制复制了父进程的一些资源信息,如下图

dup_task_struct 完成了子进程内核栈的创建static struct task_struct *dup_task_struct(struct task_struct *orig){struct task_struct *tsk;struct thread_info *ti;int err;prepare_to_copy(orig);//分配一块task_structtsk = alloc_task_struct();//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底ti = alloc_thread_info(tsk);//把父进程task_struct内容复制给子进程*tsk = *orig;//子进程的task_struct指向栈中的thread_infotsk->stack = ti;...//把父进程thread_info复制给子进程的thread_info,然后子进程的thread_info指向子进程的task_structsetup_thread_stack(tsk, orig);...return tsk;}

dup_task_struct 为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些结构中的信息完全复制了父进程信息,同时完成了 thread_info 和 task_struct 之间的关系,如下图

在 copy_process 中通过 dup_task_struct 为子进程分配了描述结构并初始化,完成内核栈的低端数据的初始化,而用作内核堆栈的高端复制初始化由 copy_thread 来完成。

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;//参数regs是保存这个cpu进入内核前夕各个寄存器中的内容而形成的一个pt_regs结构childregs = task_pt_regs(p); //指向内核栈的最高地址*childregs = *regs; //把寄存器中的值存放到内核栈的最高地址//对子进程的内核栈寄存器中的值进行调整childregs->eax = 0; //将eax设置0,子进程被调度运行从系统调用返回时,就返回该值,这也就为什么fork时子进程返回的是0childregs->esp = esp;//将thread.esp设置成子进程系统空间栈中pt_regs结构的其实地址,就好像该子进程以前曾运行过,而在进入内核以后正要返回用户空间时被切换了一样p->thread.esp = (unsigned long) childregs;//esp0 指向子进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量写入TSS 的 esp0 字段,表示这个进程进入0级运行时其堆栈的位置p->thread.esp0 = (unsigned long) (childregs+1);//eip表示当进程下一次被切换进行运行时的切入点,类似于函数调用或中断的返回地址。将此地址设置为ret_from_fork,使得子进程在首次调度运行时从这开始p->thread.eip = (unsigned long) ret_from_fork;//把段寄存器gs的值保存到p->thread.gs中savesegment(gs,p->thread.gs);//在父进程包含I/O访问许可权限位图的情况下,使新创建进程继承父进程的I/O访问许可权限位图.../** Set a new TLS for the child thread?*///在参数clone_flags包含CLONE_SETTLS标记的情况下,设置进程的TLS ...return err;}

通过 copy_thread 初始化子进程内核栈的高端地址,修改其中的寄存器,保证了子进程被调度运行返回时能够和父进程进行了区分。

我们知道应用程调用 fork() 会返回2次,父进程返回的是子进程的 id, 子进程返回0,那子进程是怎么返回的呢?

在 copy_thread 函数将子进程的 eip 寄存器值设置为 ret_from_fork 的地址,同时将 eax 寄存器中的值赋值为0(eax 记录的就是函数返回时的值)。

ENTRY(ret_from_fork)CFI_STARTPROCpushl %eaxCFI_ADJUST_CFA_OFFSET 4call schedule_tailGET_THREAD_INFO(%ebp)popl %eaxCFI_ADJUST_CFA_OFFSET -4pushl $0x0202 # Reset kernel eflagsCFI_ADJUST_CFA_OFFSET 4popflCFI_ADJUST_CFA_OFFSET -4jmp syscall_exitCFI_ENDPROCEND(ret_from_fork)

当子进程被调度运行时,子进程进入 ret_from_fork,在调用完 schedule_tail 后调到 syscall_exit 结束系统调用返回到用户空间,用户空间从 eax 寄存器中获取返回值0,也即是调用 fork 的返回值。

写时拷贝

在 fork() 创建进程的过程中, Linux 采用了写时拷贝(copy-on-write)页的技术,该技术就是一种可以延迟拷贝或免除拷贝的技术。

其原理就是先通过复制页表项暂时共享这个物理内存页。当从父进程复制页表项时会把父进程的页表项改成写保护,然后把改成写保护的页表项设置到子进程的页表中。这样2个进程的页面都变成“只读”的了。当不管父进程还是子进程企图写入该页面时,都会引起一次页面异常,而页面异常处理程序会对此的反应是为其分配一个物理页,并把内容真正的复制到新的物理页面中。此时父子进程各自拥有自己的物理页面,然后将这2个页面表中相应的表项改成可写。

写时拷贝技术避免了在创建进程过程中进行大量根本就不会使用的数据进行拷贝而带来的开销。

fork() 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符的过程。

有关写时拷贝技术原理如下

在 fork() 后子进程完全复制了父进程的页表,但是没有复制物理页,这个时候两个进程的虚拟地址和物理地址都是相同的,子进程和父进程使用同一份物理内存页,这时的页面标记时“只读”的。

当某个进程进行修改内存时,比如子进程进行修改内存操作,这个时候操作系统系统会把父进程的物理页拷贝一份给子进程,同时修改页表,子进程在新分配的物理页中进行修改,这个时候父子物理内存也就分开了。

因此,在子进程复制父进程的地址空间和页表后,父子进程都有独立的mm_struct 和 各级页表,且其值均相等。最关键的就是下表中红色的部分,所有可写的页表项均设置为不可写,当某个进程进行写访问时,就会触发缺页异常中断。

实现红色部分属性修改的函数调用流程如下:

copy_mm()--> dup_mm()--> dup_mmap()--> copy_page_range()--> copy_pud_range()--> copy_pmd_range()--> copy_pte_range()--> copy_one_pte()--> ptep_set_wrprotect()static inline voidcopy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,unsigned long addr, int *rss){.../** If it's a COW mapping, write protect it both* in the parent and the child*/if (is_cow_mapping(vm_flags)) {ptep_set_wrprotect(src_mm, addr, src_pte);pte = pte_wrprotect(pte);}...}

从代码中可以看到,父子进程的页表项均设置成了写保护属性。

vfork 原理

在linux中还有一种创建进程的方式,那就是vfork。

除了不拷贝父进程的页表项外,vfork( ) 系统调用和 fork() 系统调用功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或者执行 execv()。

vfork 系统调用最终还是通过 do_fork 系统调用完成的,如下

int sys_vfork(long r10, long r11, long r12, long r13, long mof, long srp, struct pt_regs *regs){return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, rdusp(), regs, 0, NULL, NULL);}

在调用 do_fork 时 vfork 比 fork的 clone_flags 参数多了 CLONE_VFORK | CLONE_VM 标志,接下来通过这2个标志来进行分析。

vfork 在调用 copy_process 时,由于存在 CLONE_VM 标志,所以在 拷贝 copy_mm 时子进程并不对父进程 mm_struct 结构进行复制,而是子进程指向父进程的 mm_struct结构进行共享。在执行 do_fork 时,子进程的 vfork_done结构会指向一个特定的地址。子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过 vfork_done 指针向它发送信号。在调用 mm_release() 时,该函数用于进程退出内存地址空间,并且检查 vfork_done 是否为空,若不为空,则会向父进程发送信号。回到 do_fork,父进程醒来并返回。

若一切执行顺利,子进程在新的地址空间里运行而父进程也恢复了在原地址空间的运行。

由于子进程指向父进程的mm_struct结构,所以当子进程修改数据的时候父进程能够感知到。

创建线程

Linux 中实现线程的机制很特别。从内核的角度来看,并没有线程的概念。Linux 把所有的线程当做进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct。所以在内核中,它看起来像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间等)。

在用户态中我们常用 pthread_create 来创建线程,而 pthread_create 在libc 库中调用 create_thread(), 最终调用 clone()。

__pthread_create_2_1 ()-->ALLOCATE_STACK () 分配线程栈空间--> create_thread ()--> __clone2 ()

从上述调用过程可以知道,线程在创建时候,通过 libc 库创建了线程的栈,所以每个线程都有自己的私有栈。

在内核实现中,最终还是调用 do_fork。

int sys_clone(unsigned long clone_flags, unsigned long usp,int __user *parent_tidp, void __user *child_threadptr,int __user *child_tidp, int p6,struct pt_regs *regs){...return do_fork(clone_flags, usp, regs, 0, parent_tidp, child_tidp);}

线程的创建和普通进程的创建类似,只不过在调用 clone() 的时候需要传递一些参数标志来指明需要共享的资源。

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

从上面的标志位可以知道,线程共享了父进程的地址空间、打开的文件、文件系统信息、信号处理函数及被阻断的信号等信息。

exec调用

有关 exec 系列函数的调用,本文不再分析,可以参考文章 《Linux 可执行文件程序载入和执行过程》。

责任编辑:华轩 来源:今日头条 Linux进程线程

推荐系统

  • 电脑公司Ghost Win8.1 x32 精选纯净版2022年7月(免激活) ISO镜像高速下载

    电脑公司Ghost Win8.1 x32 精选纯净版2022年7月(免激活) ISO镜像高速下载

    语言:中文版系统大小:2.98GB系统类型:Win8

    电脑公司Ghost Win8.1x32位纯净版V2022年7月版本集成了自2022流行的各种硬件驱动,首次进入系统即全部硬件已安装完毕。电脑公司Ghost Win8.1x32位纯净版具有更安全、更稳定、更人性化等特点。集成最常用的装机软件,精心挑选的系统维护工具,加上绿茶独有

  • 微软Win11原版22H2下载_Win11GHOST 免 激活密钥 22H2正式版64位免费下载

    微软Win11原版22H2下载_Win11GHOST 免 激活密钥 22H2正式版64位免费下载

    语言:中文版系统大小:5.13GB系统类型:Win11

    微软Win11原版22H2下载_Win11GHOST 免 激活密钥 22H2正式版64位免费下载系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。Win11 64位 Office办公版(免费)优化  1、保留 Edge浏览器。  2、隐藏“操作中心”托盘图标。  3、保留常用组件(微软商店,计算器,图片查看器等)。  5、关闭天气资讯。 

  • Win11 21H2 官方正式版下载_Win11 21H2最新系统免激活下载

    Win11 21H2 官方正式版下载_Win11 21H2最新系统免激活下载

    语言:中文版系统大小:4.75GB系统类型:Win11

    Ghost Win11 21H2是微软在系统方面技术积累雄厚深耕多年,Ghost Win11 21H2系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。Ghost Win11 21H2是微软最新发布的KB5019961补丁升级而来的最新版的21H2系统,以Windows 11 21H2 22000 1219 专业版为基础进行优化,保持原汁原味,系统流畅稳定,保留常用组件

  • windows11中文版镜像 微软win11正式版简体中文GHOST ISO镜像64位系统下载

    windows11中文版镜像 微软win11正式版简体中文GHOST ISO镜像64位系统下载

    语言:中文版系统大小:5.31GB系统类型:Win11

    windows11中文版镜像 微软win11正式版简体中文GHOST ISO镜像64位系统下载,微软win11发布快大半年了,其中做了很多次补丁和修复一些BUG,比之前的版本有一些功能上的调整,目前已经升级到最新版本的镜像系统,并且优化了自动激活,永久使用。windows11中文版镜像国内镜像下载地址微软windows11正式版镜像 介绍:1、对函数算法进行了一定程度的简化和优化

  • 微软windows11正式版GHOST ISO镜像 win11下载 国内最新版渠道下载

    微软windows11正式版GHOST ISO镜像 win11下载 国内最新版渠道下载

    语言:中文版系统大小:5.31GB系统类型:Win11

    微软windows11正式版GHOST ISO镜像 win11下载 国内最新版渠道下载,微软2022年正式推出了win11系统,很多人迫不及待的要体验,本站提供了最新版的微软Windows11正式版系统下载,微软windows11正式版镜像 是一款功能超级强大的装机系统,是微软方面全新推出的装机系统,这款系统可以通过pe直接的完成安装,对此系统感兴趣,想要使用的用户们就快来下载

  • 微软windows11系统下载 微软原版 Ghost win11 X64 正式版ISO镜像文件

    微软windows11系统下载 微软原版 Ghost win11 X64 正式版ISO镜像文件

    语言:中文版系统大小:0MB系统类型:Win11

    微软Ghost win11 正式版镜像文件是一款由微软方面推出的优秀全新装机系统,这款系统的新功能非常多,用户们能够在这里体验到最富有人性化的设计等,且全新的柔软界面,看起来非常的舒服~微软Ghost win11 正式版镜像文件介绍:1、与各种硬件设备兼容。 更好地完成用户安装并有效地使用。2、稳定使用蓝屏,系统不再兼容,更能享受无缝的系统服务。3、为

  • 雨林木风Windows11专业版 Ghost Win11官方正式版 (22H2) 系统下载

    雨林木风Windows11专业版 Ghost Win11官方正式版 (22H2) 系统下载

    语言:中文版系统大小:4.75GB系统类型:

    雨林木风Windows11专业版 Ghost Win11官方正式版 (22H2) 系统下载在系统方面技术积累雄厚深耕多年,打造了国内重装系统行业的雨林木风品牌,其系统口碑得到许多人认可,积累了广大的用户群体,雨林木风是一款稳定流畅的系统,一直以来都以用户为中心,是由雨林木风团队推出的Windows11国内镜像版,基于国内用户的习惯,做了系统性能的优化,采用了新的系统

  • 雨林木风win7旗舰版系统下载 win7 32位旗舰版 GHOST 免激活镜像ISO

    雨林木风win7旗舰版系统下载 win7 32位旗舰版 GHOST 免激活镜像ISO

    语言:中文版系统大小:5.91GB系统类型:Win7

    雨林木风win7旗舰版系统下载 win7 32位旗舰版 GHOST 免激活镜像ISO在系统方面技术积累雄厚深耕多年,加固了系统安全策略,雨林木风win7旗舰版系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。win7 32位旗舰装机版 v2019 05能够帮助用户们进行系统的一键安装、快速装机等,系统中的内容全面,能够为广大用户