- 浏览: 241451 次
- 性别:
- 来自: 北京
-
6.8.1 进程的创建
新的进程通过克隆旧的程序(当前进程)而建立。fork() 和 clone()(对于线程)系统调用可用来建立新的进程。这两个系统调用结束时,内核在系统的物理内存中为新的进程分配新的 task_struct 结构,同时为新进程要使用的堆栈分配物理页。Linux 还会为新的进程分配新的进程标识符。然后,新 task_struct 结构的地址保存在链表中,而旧进程的 task_struct 结构内容被复制到新进程的 task_struct 结构中。
在克隆进程时,Linux 允许两个进程共享相同的资源。可共享的资源包括文件、信号处理程序和虚拟内存等(通过继承)。当某个资源被共享时,该资源的引用计数值会增加 1,从而只有两个进程均终止时,内核才会释放这些资源。图 6.23 说明了父进程和子进程共享打开的文件。
图 6.23父进程和子进程共享打开的文件
系统对进程虚拟内存的克隆过程则更加巧妙些。新的 vm_area_struct 结构、新进程自己的 mm_struct 结构以及新进程的页表必须在一开始就准备好,但这时并不复制任何虚拟内存。如果旧进程的某些虚拟内存在物理内存中,而有些在交换文件中,那么虚拟内存的复制将会非常困难和费时。实际上,Linux 采用了称为写时复制的技术,也就是说,只有当两个进程中的任意一个向虚拟内存中写入数据时才复制相应的虚拟内存;而没有写入的任何内存页均可以在两个进程之间共享。代码页实际总是可以共享的。
此外,内核线程是调用kernel_thread()函数创建的,而kernel_thread()在内核态调用了clone()系统调用。内核线程通常没有用户地址空间,即p->mm = NULL,它总是直接访问内核地址空间。
不管是fork()还是clone()系统调用,最终都调用了内核中的do_fork(),其源代码在kernel/fork.c:
尽管fork()系统调用因为传递用户堆栈和寄存器参数而与特定的平台相关,但实际上do_fork()所做的工作还是可移植的。下面给出对以上代码的解释:
· 给局部变量赋初值-ENOMEM,当分配一个新的task_struc结构失败时就返回这个错误值。
· 如果在clone_flags中设置了CLONE_PID标志,就返回一个错误(-EPERM)。因为CLONE_PID有特殊的作用,当这个标志为1时,父、子进程(线程)共用一个进程号,也就是说,子进程虽然有自己的task_struct结构,却使用父进程的pid。但是,只有0号进程(即系统中的空线程)才允许使用这个标志。
· 调用alloc_task_struct()为子进程分配两个连续的物理页面,低端用来存放子进程的task_struct结构,高端用作其内核空间的堆栈。
· 用结构赋值语句*p = *current把当前进程task_struct结构中的所有内容都拷贝到新进程中。稍后,子进程不该继承的域会被设置成正确的值。
· 在task_struct结构中有个指针user,用来指向一个user_struct结构。一个用户常常有多个进程,所以有关用户的信息并不专属于某一个进程。这样,属于同一用户的进程就可以通过指针user共享这些信息。显然,每个用户有且只有一个user_struct结构。该结构中有一个引用计数器count,对属于该用户的进程数量进行计数。可想而知,内核线程并不属于某个用户,所以其task_struct中的user指针为0。每个进程task_struct结构中有个数组rlim,对该进程占用各种资源的数量作出限制,而rlim[RLIMIT_NPROC]就规定了该进程所属用户可以拥有的进程数量。所以,如果当前进程是一个用户进程,并且该用户拥有的进程数量已经达到了规定的界限值,就不允许它fork()了。
· 除了检查每个用户拥有的进程数量外,接着要检查系统中的任务总数(所有用户的进程数加系统的内核线程数)是否超过了最大值max_threads,如果是,也不允许再创建子进程。
· 一个进程除了属于某个用户外,还属于某个“执行域”。 Linux可以运行X86平台上其它Unix类操作系统生成的符合iBCS2标准的程序。例如,一个进程所执行的程序是为Solaris开发的,那么这个进程就属于Solaris执行域PER_SOLARIS。当然,在Linux上运行的绝大多数程序属于Linux执行域。在task_struct有一个指针exec_doman,指向一个exec_doman结构。在exec_doman结构中有一个域是module,这是指向某个module结构的指针。在Linux中,一个文件系统或驱动程序都可以作为一个单独的模块进行编译,并动态地链接到内核中。在module结构中有一个计数器count,用来统计几个进程需要使用这个模块。因此,get_exec_domain(p->exec_domain)递增模块结构module中的计数器。
· 另外,每个进程所执行的程序属于某种可执行映像格式,如a.out格式、elf格式、甚至java虚拟机格式。对于不同格式的支持通常是通过动态安装的模块来实现的。所以,task_struct中有一个执行linux_binfmt结构的指针binfmt,而 __MOD_INC_USE_COUNT()就是对有关模块的使用计数进行递增。
· 紧接着为什么要把进程的状态设置成为TASK_UNINTERRUPTIBLE?这是因为后面get_pid()的操作必须独占,子进程可能因为一时进不了临界区而只好暂时进入睡眠状态。
· copy_flags()函数将clone_flags参数中的标志位略加补充和变换,然后写入p->flags。
· get_pid()函数根据clone_flags中标志位ClONE_PID的值,或返回父进程(当前进程)的pid,或返回一个新的pid。
· 前面在复制父进程的task_struct结构时把父进程的所有域都照抄过来,但实际上很多域的值必须重新赋初值,因此,后面的赋值语句就是对子进程task_struct结构的初始化。其中start_time表示进程创建的时间,而全局变量jiffies就是从系统初始化开始至当前的是时钟滴答数。local_pages表示属于该进程的局部页面形成一个双向链表,在此进行了初始化。
· copy_files()有条件地复制已打开文件的控制结构,也就是说,这种复制只有在clone_flags中的CLONE_FILES标志为0时才真正进行,否则只是共享父进程的已打开文件。当一个进程有已打开文件时,task_struct结构中的指针files指向一个fiel_struct结构,否则为0。所有与终端设备tty相联系的用户进程的头三个标准文件stdin、stdout及stderr都是预先打开的,所以指针一般不为空。
· copy_fs()也是只有在clone_flags中的CLONE_FS标志为0时才加以复制。在task_struct中有一个指向fs_struct结构的指针,fs_struct结构中存放的是进程的根目录root、当前工作目录pwd、一个用于文件操作权限的umask,还有一个计数器。类似地,copy_sighand()也是只有在CLONE_SIGHAND为0时才真正复制父进程的信号结构,否则就共享父进程的。信号是进程间通信的一种手段,信号随时都可以发向一个进程,就像中断随时都可以发向一个处理器一样。进程可以为各种信号设置相应的信号处理程序,一旦进程设置了信号处理程序,其task_struct结构中的指针sig就指向signal_struct结构(定义于include/linux/sched.h)。关于信号的具体内容将在下一章进行介绍。
· 用户空间的继承是通过copy_mm()函数完成的。进程的task_struct结构中有一个指针mm,就指向代表着进程地址空间的mm_struct结构。对mm_struct的复制也是在clone_flags中的CLONE_VM标志为0时才真正进行,否则,就只是通过已经复制的指针共享父进程的用户空间。对mm_struct的复制不只限于这个数据结构本身,还包括了对更深层次数据结构的复制,其中最主要的是vm_area_struct结构和页表的复制,这是由同一文件中的dum_mmap()函数完成的。
· 到此为止,task_struct结构中的域基本复制好了,但是用于内核堆栈的内容还没有复制,这就是copy_thread()的责任了。copy_thread()函数与平台相关,定义于arch/i386/kernel/process.c中。copy_thread()实际上只复制父进程的内核空间堆栈。堆栈中的内容记录了父进程通过系统调用fork()进入内核空间、然后又进入copy_thread()函数的整个历程,子进程将要循相同的路线返回,所以要把它复制给子进程。但是,如果父子进程的内核空间堆栈完全相同,那返回用户空间后就无法区分哪个是子进程了,所以,复制以后还要略作调整。有兴趣的读者可以结合第三、四章内容去读该函数的源代码。
· parent_exec_id表示父进程的执行域,p->self_exec_id是本进程(子进程)的执行域,swappable表示本进程的页面可以被换出。exit_signal为本进程执行exit()系统调用时向父进程发出的信号,pdeath_signal为要求父进程在执行exit()时向本进程发出的信号。另外,counter域的值是进程的时间片(以时钟滴达为单位),代码中将父进程的时间片分成两半,让父、子进程各有原值的一半。
· 进程创建后必须处于某一组中,这是通过task_struct结构中的队列头thread_group与父进程链接起来,形成一个进程组(注意,thread并不单指线程,内核代码中经常用thread通指所有的进程)。
· 建立进程的家族关系。先建立起子进程的祖先和双亲(当然还没有兄弟和孩子),然后通过SET_LINKS()宏将子进程的task_struct结构插入到内核中其它进程组成的双向链表中。通过hash_pid()将其链入按其pid计算得的哈希表中(参看第三章进程的组成方式一节)。
· 最后,通过wake_up_process()将子进程唤醒,也就是将其挂入可执行队列等待被调度。
· 但是,还有一种特殊情况必须考虑。当参数clone_flags中CLONE_VFORK标志位为1时,一定要保证子进程先运行,一直到子进程通过系统调用execve()执行一个新的可执行程序或通过系统调用exit()退出系统时,才可以恢复父进程的执行,这是通过wait_for_completion()函数实现的。为什么要这样做呢?这是因为当CLONE_VFORK标志位为1时,就说明父、子进程通过指针共享用户空间(指向相同的mm_struct结构),那也说明父进程写入用户空间的内容同时也写入了子进程的用户空间,反之亦然。如果说,在这种情况下,父子进程对数据区的写入可能引起问题的话,那么,对堆栈区的写入可能就是致命的了。而对子程序或函数的调用肯定就是对堆栈的写入。由此可见,在这种情况下,决不能让两个进程都回到用户空间并发执行,否则,必然导致两个进程的互相“捣乱”或因非法访问而死亡。解决的办法的只能是“扣留”其中的一个进程,而让另一个进程先回到用户空间,直到两个进程不再共享它们的用户空间,或其中一个进程消亡为止(肯定是先回到用户空间的进程先消亡)。
到此为止,子进程的创建已经完成,该是从内核态返回用户态的时候了。实际上,fork()系统调用执行之后,父子进程返回到用户空间中相同的的地址,用户进程根据fork()的返回值分别安排父子进程执行不同的代码。
新的进程通过克隆旧的程序(当前进程)而建立。fork() 和 clone()(对于线程)系统调用可用来建立新的进程。这两个系统调用结束时,内核在系统的物理内存中为新的进程分配新的 task_struct 结构,同时为新进程要使用的堆栈分配物理页。Linux 还会为新的进程分配新的进程标识符。然后,新 task_struct 结构的地址保存在链表中,而旧进程的 task_struct 结构内容被复制到新进程的 task_struct 结构中。
在克隆进程时,Linux 允许两个进程共享相同的资源。可共享的资源包括文件、信号处理程序和虚拟内存等(通过继承)。当某个资源被共享时,该资源的引用计数值会增加 1,从而只有两个进程均终止时,内核才会释放这些资源。图 6.23 说明了父进程和子进程共享打开的文件。

图 6.23父进程和子进程共享打开的文件
系统对进程虚拟内存的克隆过程则更加巧妙些。新的 vm_area_struct 结构、新进程自己的 mm_struct 结构以及新进程的页表必须在一开始就准备好,但这时并不复制任何虚拟内存。如果旧进程的某些虚拟内存在物理内存中,而有些在交换文件中,那么虚拟内存的复制将会非常困难和费时。实际上,Linux 采用了称为写时复制的技术,也就是说,只有当两个进程中的任意一个向虚拟内存中写入数据时才复制相应的虚拟内存;而没有写入的任何内存页均可以在两个进程之间共享。代码页实际总是可以共享的。
此外,内核线程是调用kernel_thread()函数创建的,而kernel_thread()在内核态调用了clone()系统调用。内核线程通常没有用户地址空间,即p->mm = NULL,它总是直接访问内核地址空间。
不管是fork()还是clone()系统调用,最终都调用了内核中的do_fork(),其源代码在kernel/fork.c:
/* * Ok, this is the main fork-routine. It copies the system process * information (task[nr]) and sets up the necessary registers. It also * copies the data segment in its entirety. The "stack_start" and * "stack_top" arguments are simply passed along to the platform * specific copy_thread() routine. Most platforms ignore stack_top. * For an example that's using stack_top, see * arch/ia64/kernel/process.c. */ int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size) { int retval; struct task_struct *p; struct completion vfork; retval = -EPERM; /* * CLONE_PID is only allowed for the initial SMP swapper * calls */ if (clone_flags & CLONE_PID) { if (current->pid) goto fork_out; } retval = -ENOMEM; p = alloc_task_struct(); if (!p) goto fork_out; *p = *current; retval = -EAGAIN; /* * Check if we are over our maximum process limit, but be sure to * exclude root. This is needed to make it possible for login and * friends to set the per-user process limit to something lower * than the amount of processes root is running. -- Rik */ if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur && !capable(CAP_SYS_ADMIN) !capable(CAP_SYS_RESOURCE)) goto bad_fork_free; atomic_inc(&p->user->__count); atomic_inc(&p->user->processes); /* * Counter increases are protected by * the kernel lock so nr_threads can't * increase under us (but it may decrease). */ if (nr_threads >= max_threads) goto bad_fork_cleanup_count; get_exec_domain(p->exec_domain); if (p->binfmt && p->binfmt->module) __MOD_INC_USE_COUNT(p->binfmt->module); p->did_exec = 0; p->swappable = 0; p->state = TASK_UNINTERRUPTIBLE; copy_flags(clone_flags, p); p->pid = get_pid(clone_flags); p->run_list.next = NULL; p->run_list.prev = NULL; p->p_cptr = NULL; init_waitqueue_head(&p->wait_chldexit); p->vfork_done = NULL; if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); } spin_lock_init(&p->alloc_lock); p->sigpending = 0; init_sigpending(&p->pending); p->it_real_value = p->it_virt_value = p->it_prof_value = 0; p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0; init_timer(&p->real_timer); p->real_timer.data = (unsigned long) p; p->leader = 0; /* session leadership doesn't inherit */ p->tty_old_pgrp = 0; p->times.tms_utime = p->times.tms_stime = 0; p->times.tms_cutime = p->times.tms_cstime = 0; #ifdef CONFIG_SMP { int i; p->cpus_runnable = ~0UL; p->processor = current->processor; /* ?? should we just memset this ?? */ for(i = 0; i < smp_num_cpus; i++) p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0; spin_lock_init(&p->sigmask_lock); } #endif p->lock_depth = -1; /* -1 = no lock */ p->start_time = jiffies; INIT_LIST_HEAD(&p->local_pages); retval = -ENOMEM; /* copy all the process information */ if (copy_files(clone_flags, p)) goto bad_fork_cleanup; if (copy_fs(clone_flags, p)) goto bad_fork_cleanup_files; if (copy_sighand(clone_flags, p)) goto bad_fork_cleanup_fs; if (copy_mm(clone_flags, p)) goto bad_fork_cleanup_sighand; retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); if (retval) goto bad_fork_cleanup_mm; p->semundo = NULL; /* Our parent execution domain becomes current domain These must match for thread signalling to apply */ p->parent_exec_id = p->self_exec_id; /* ok, now we should be set up.. */ p->swappable = 1; p->exit_signal = clone_flags & CSIGNAL; p->pdeath_signal = 0; /* * "share" dynamic priority between parent and child, thus the * total amount of dynamic priorities in the system doesnt change, * more scheduling fairness. This is only important in the first * timeslice, on the long run the scheduling behaviour is unchanged. */ p->counter = (current->counter + 1) >> 1; current->counter >>= 1; if (!current->counter) current->need_resched = 1; /* * Ok, add it to the run-queues and make it * visible to the rest of the system. * * Let it rip! */ retval = p->pid; p->tgid = retval; INIT_LIST_HEAD(&p->thread_group); /* Need tasklist lock for parent etc handling! */ write_lock_irq(&tasklist_lock); /* CLONE_PARENT and CLONE_THREAD re-use the old parent */ p->p_opptr = current->p_opptr; p->p_pptr = current->p_pptr; if (!(clone_flags & (CLONE_PARENT | CLONE_THREAD))) { p->p_opptr = current; if (!(p->ptrace & PT_PTRACED)) p->p_pptr = current; } if (clone_flags & CLONE_THREAD) { p->tgid = current->tgid; list_add(&p->thread_group, ¤t->thread_group); } SET_LINKS(p); hash_pid(p); nr_threads++; write_unlock_irq(&tasklist_lock); if (p->ptrace & PT_PTRACED) send_sig(SIGSTOP, p, 1); wake_up_process(p); /* do this last */ ++total_forks; if (clone_flags & CLONE_VFORK) wait_for_completion(&vfork); fork_out: return retval; bad_fork_cleanup_mm: exit_mm(p); bad_fork_cleanup_sighand: exit_sighand(p); bad_fork_cleanup_fs: exit_fs(p); /* blocking */ bad_fork_cleanup_files: exit_files(p); /* blocking */ bad_fork_cleanup: put_exec_domain(p->exec_domain); if (p->binfmt && p->binfmt->module) __MOD_DEC_USE_COUNT(p->binfmt->module); bad_fork_cleanup_count: atomic_dec(&p->user->processes); free_uid(p->user); bad_fork_free: free_task_struct(p); goto fork_out; }
尽管fork()系统调用因为传递用户堆栈和寄存器参数而与特定的平台相关,但实际上do_fork()所做的工作还是可移植的。下面给出对以上代码的解释:
· 给局部变量赋初值-ENOMEM,当分配一个新的task_struc结构失败时就返回这个错误值。
· 如果在clone_flags中设置了CLONE_PID标志,就返回一个错误(-EPERM)。因为CLONE_PID有特殊的作用,当这个标志为1时,父、子进程(线程)共用一个进程号,也就是说,子进程虽然有自己的task_struct结构,却使用父进程的pid。但是,只有0号进程(即系统中的空线程)才允许使用这个标志。
· 调用alloc_task_struct()为子进程分配两个连续的物理页面,低端用来存放子进程的task_struct结构,高端用作其内核空间的堆栈。
· 用结构赋值语句*p = *current把当前进程task_struct结构中的所有内容都拷贝到新进程中。稍后,子进程不该继承的域会被设置成正确的值。
· 在task_struct结构中有个指针user,用来指向一个user_struct结构。一个用户常常有多个进程,所以有关用户的信息并不专属于某一个进程。这样,属于同一用户的进程就可以通过指针user共享这些信息。显然,每个用户有且只有一个user_struct结构。该结构中有一个引用计数器count,对属于该用户的进程数量进行计数。可想而知,内核线程并不属于某个用户,所以其task_struct中的user指针为0。每个进程task_struct结构中有个数组rlim,对该进程占用各种资源的数量作出限制,而rlim[RLIMIT_NPROC]就规定了该进程所属用户可以拥有的进程数量。所以,如果当前进程是一个用户进程,并且该用户拥有的进程数量已经达到了规定的界限值,就不允许它fork()了。
· 除了检查每个用户拥有的进程数量外,接着要检查系统中的任务总数(所有用户的进程数加系统的内核线程数)是否超过了最大值max_threads,如果是,也不允许再创建子进程。
· 一个进程除了属于某个用户外,还属于某个“执行域”。 Linux可以运行X86平台上其它Unix类操作系统生成的符合iBCS2标准的程序。例如,一个进程所执行的程序是为Solaris开发的,那么这个进程就属于Solaris执行域PER_SOLARIS。当然,在Linux上运行的绝大多数程序属于Linux执行域。在task_struct有一个指针exec_doman,指向一个exec_doman结构。在exec_doman结构中有一个域是module,这是指向某个module结构的指针。在Linux中,一个文件系统或驱动程序都可以作为一个单独的模块进行编译,并动态地链接到内核中。在module结构中有一个计数器count,用来统计几个进程需要使用这个模块。因此,get_exec_domain(p->exec_domain)递增模块结构module中的计数器。
· 另外,每个进程所执行的程序属于某种可执行映像格式,如a.out格式、elf格式、甚至java虚拟机格式。对于不同格式的支持通常是通过动态安装的模块来实现的。所以,task_struct中有一个执行linux_binfmt结构的指针binfmt,而 __MOD_INC_USE_COUNT()就是对有关模块的使用计数进行递增。
· 紧接着为什么要把进程的状态设置成为TASK_UNINTERRUPTIBLE?这是因为后面get_pid()的操作必须独占,子进程可能因为一时进不了临界区而只好暂时进入睡眠状态。
· copy_flags()函数将clone_flags参数中的标志位略加补充和变换,然后写入p->flags。
· get_pid()函数根据clone_flags中标志位ClONE_PID的值,或返回父进程(当前进程)的pid,或返回一个新的pid。
· 前面在复制父进程的task_struct结构时把父进程的所有域都照抄过来,但实际上很多域的值必须重新赋初值,因此,后面的赋值语句就是对子进程task_struct结构的初始化。其中start_time表示进程创建的时间,而全局变量jiffies就是从系统初始化开始至当前的是时钟滴答数。local_pages表示属于该进程的局部页面形成一个双向链表,在此进行了初始化。
· copy_files()有条件地复制已打开文件的控制结构,也就是说,这种复制只有在clone_flags中的CLONE_FILES标志为0时才真正进行,否则只是共享父进程的已打开文件。当一个进程有已打开文件时,task_struct结构中的指针files指向一个fiel_struct结构,否则为0。所有与终端设备tty相联系的用户进程的头三个标准文件stdin、stdout及stderr都是预先打开的,所以指针一般不为空。
· copy_fs()也是只有在clone_flags中的CLONE_FS标志为0时才加以复制。在task_struct中有一个指向fs_struct结构的指针,fs_struct结构中存放的是进程的根目录root、当前工作目录pwd、一个用于文件操作权限的umask,还有一个计数器。类似地,copy_sighand()也是只有在CLONE_SIGHAND为0时才真正复制父进程的信号结构,否则就共享父进程的。信号是进程间通信的一种手段,信号随时都可以发向一个进程,就像中断随时都可以发向一个处理器一样。进程可以为各种信号设置相应的信号处理程序,一旦进程设置了信号处理程序,其task_struct结构中的指针sig就指向signal_struct结构(定义于include/linux/sched.h)。关于信号的具体内容将在下一章进行介绍。
· 用户空间的继承是通过copy_mm()函数完成的。进程的task_struct结构中有一个指针mm,就指向代表着进程地址空间的mm_struct结构。对mm_struct的复制也是在clone_flags中的CLONE_VM标志为0时才真正进行,否则,就只是通过已经复制的指针共享父进程的用户空间。对mm_struct的复制不只限于这个数据结构本身,还包括了对更深层次数据结构的复制,其中最主要的是vm_area_struct结构和页表的复制,这是由同一文件中的dum_mmap()函数完成的。
· 到此为止,task_struct结构中的域基本复制好了,但是用于内核堆栈的内容还没有复制,这就是copy_thread()的责任了。copy_thread()函数与平台相关,定义于arch/i386/kernel/process.c中。copy_thread()实际上只复制父进程的内核空间堆栈。堆栈中的内容记录了父进程通过系统调用fork()进入内核空间、然后又进入copy_thread()函数的整个历程,子进程将要循相同的路线返回,所以要把它复制给子进程。但是,如果父子进程的内核空间堆栈完全相同,那返回用户空间后就无法区分哪个是子进程了,所以,复制以后还要略作调整。有兴趣的读者可以结合第三、四章内容去读该函数的源代码。
· parent_exec_id表示父进程的执行域,p->self_exec_id是本进程(子进程)的执行域,swappable表示本进程的页面可以被换出。exit_signal为本进程执行exit()系统调用时向父进程发出的信号,pdeath_signal为要求父进程在执行exit()时向本进程发出的信号。另外,counter域的值是进程的时间片(以时钟滴达为单位),代码中将父进程的时间片分成两半,让父、子进程各有原值的一半。
· 进程创建后必须处于某一组中,这是通过task_struct结构中的队列头thread_group与父进程链接起来,形成一个进程组(注意,thread并不单指线程,内核代码中经常用thread通指所有的进程)。
· 建立进程的家族关系。先建立起子进程的祖先和双亲(当然还没有兄弟和孩子),然后通过SET_LINKS()宏将子进程的task_struct结构插入到内核中其它进程组成的双向链表中。通过hash_pid()将其链入按其pid计算得的哈希表中(参看第三章进程的组成方式一节)。
· 最后,通过wake_up_process()将子进程唤醒,也就是将其挂入可执行队列等待被调度。
· 但是,还有一种特殊情况必须考虑。当参数clone_flags中CLONE_VFORK标志位为1时,一定要保证子进程先运行,一直到子进程通过系统调用execve()执行一个新的可执行程序或通过系统调用exit()退出系统时,才可以恢复父进程的执行,这是通过wait_for_completion()函数实现的。为什么要这样做呢?这是因为当CLONE_VFORK标志位为1时,就说明父、子进程通过指针共享用户空间(指向相同的mm_struct结构),那也说明父进程写入用户空间的内容同时也写入了子进程的用户空间,反之亦然。如果说,在这种情况下,父子进程对数据区的写入可能引起问题的话,那么,对堆栈区的写入可能就是致命的了。而对子程序或函数的调用肯定就是对堆栈的写入。由此可见,在这种情况下,决不能让两个进程都回到用户空间并发执行,否则,必然导致两个进程的互相“捣乱”或因非法访问而死亡。解决的办法的只能是“扣留”其中的一个进程,而让另一个进程先回到用户空间,直到两个进程不再共享它们的用户空间,或其中一个进程消亡为止(肯定是先回到用户空间的进程先消亡)。
到此为止,子进程的创建已经完成,该是从内核态返回用户态的时候了。实际上,fork()系统调用执行之后,父子进程返回到用户空间中相同的的地址,用户进程根据fork()的返回值分别安排父子进程执行不同的代码。
相关推荐
在“易语言监视新进程创建”的主题中,我们主要探讨如何使用易语言来实现对计算机中新创建的进程进行实时监控。 首先,我们需要理解进程的基本概念。在操作系统中,进程是程序的一次执行实例,它包含了程序运行时所...
### Linux进程创建与进程通信知识点解析 #### 实验背景与目的 在本次实验中,学生通过实际操作加深了对Linux环境下进程创建与进程间通信的理解。实验的主要目标是熟悉Linux系统调用,并学习如何在Linux中创建进程。...
进程创建、控制与撤消 一、目的: 通过进程的创建和控制的设计来达到如下目的: 1、加深对进程概念的理解,明确进程和程序的区别; 2、进一步认识并发执行的概念,区别顺序执行和并发执行; 3、分析进程争用临界资源...
### 进程创建与控制详解 #### 进程的基本概念 在计算机系统中,进程是程序的一次执行过程,它是操作系统进行资源分配和调度的基本单位。一个程序在执行时会创建一个或多个进程,每个进程都有自己的地址空间、状态...
进程创建与撤销 进程创建与撤销是操作系统中最基本的概念之一。了解进程创建和撤销的机理,对理解操作系统的基本原理至关重要。在本实验中,我们将通过模拟实现进程创建和撤销功能,来深入了解进程的概念和管理机理...
在IT领域,监控进程创建是一项重要的系统管理任务,它能够帮助我们了解系统中正在运行的程序,检测潜在的恶意活动,以及优化系统性能。本文将深入探讨如何通过源码实现进程监控,特别是针对Windows XP和Win7 32位...
本实验"进程创建模拟试验"旨在帮助学生深入理解进程创建的理论基础,掌握具体的创建方法,并熟悉与进程相关的数据结构。以下是关于这些知识点的详细说明: 1. **进程创建相关理论**: - **进程的概念**:进程是...
实验二“进程创建及进程间通信1”主要涵盖了操作系统中的核心概念,如进程创建、进程同步、进程互斥以及管道通信。在这个实验中,你将深入理解Linux操作系统中进程的生命周期,以及如何通过编程实现进程间的通信。 ...
在计算机科学中,"父进程创建子进程"是操作系统中多任务并发执行的一个核心概念。在C++编程语言中,我们可以利用操作系统提供的API(应用程序接口)来实现这一功能。本实验旨在让学生理解进程的创建机制,以及如何在...
APIHOOK拦截指定进程创建新进程。@按键精灵。
### 操作系统 进程创建实验报告 #### 实验背景 在现代计算机系统中,操作系统作为用户和硬件之间的桥梁,负责管理和协调计算机的各种资源。进程是操作系统中的一个重要概念,它是程序的一次执行过程,也是系统进行...
在Windows操作系统中,监视进程创建是一项重要的系统级编程任务,常用于系统安全、性能分析或调试目的。本主题将深入探讨如何通过API拦截技术,特别是hook `CreateProcess`函数,来实现在Windows XP系统上监控进程...
监视新进程创建是系统监控中的一个重要功能,它可以帮助开发者、安全研究人员或系统管理员了解系统上运行的进程动态,及时发现异常行为。在这个场景下,我们有一个名为“WMIProcessCreateMonitor”的压缩包文件,...
在Unix/Linux系统中,最常用的进程创建函数是`fork()`. 它会创建一个与父进程几乎完全相同的子进程,包括其内存映像、打开的文件等。实验中提到的“进程树”是指由一个父进程创建多个子进程,这些子进程又可以创建...
操作系统课程设计中的“进程创建系统”是一个重要的学习实践环节,旨在深入理解操作系统的内核机制,特别是进程管理方面的概念和技术。在这个项目中,学生需要模拟一个简化版的操作系统,实现进程的创建、调度和销毁...
实验1 进程创建模拟实验 实验内容:分析进程创建函数createpc程序模块 输入实验提供的代码后,可以输入createpc命令创建进程,输入showdetail显示每个进程及其子进程的信息,测试命令解释如下: createpc 创建进程...
以普通程序员身份认识高级语言VC++/Java/C#的进程创建与撤销工具。
### 进程创建、终止、阻塞、调度、唤醒原语 #### 一、进程创建原语 在操作系统中,进程的创建是一项重要的任务。进程创建原语是用于创建一个新进程的操作。新进程通常继承父进程的部分属性,并获得一个唯一的进程...
漫谈兼容内核之十:Windows的进程创建和映像装入》这篇文档中,作者毛德操详细探讨了Windows操作系统中进程创建与映像装载的过程。这篇文章主要基于《Microsoft Windows Internals 4e》一书的第六章内容,并结合作者...
利用fork()函数编写一个程序,要求父进程创建两个子进程,父进程、子进程并发执行,输出并发执行的消息,如: 父进程正在执行….. 子进程1正在执行….. 子进程2正在执行….. 一段时间后,父进程杀死子进程,然后...