windows进程创建流程
windows系统提供给用户的创建函数接口名称为CreateProcess(),下面我们通过分析系统中的这个函数的具体实现过程来阐述系统是如何
从无到有将一个新进程带入到操作系统的运行中去的。
来源:《进程的多对多(M:N)线程模型研究》李晋
首先发起创建行为的线程打开将要被执行的文件对象,并新建一个内存区对象,这个对象就是之后进程需要完成的功能函数所在的文件的描述符。接着创建windows执行体进程对象,其中包括建立EPROCESS块,创建初始的进程地址空间,初始化KPROCESS进程块,建立PEB等。完成进程相关控制结构后,下一步工作是创建初始线程以及它的栈和执行环境 。
windows系统的进程线程控制数据结构也是在这个阶段建立的。第一步递增进程对象中的线程计数器值;第二步创建并初始化管理层线程控制块ETHREAD块;第三步接着为新建线程生成一个线程ID;第四步在进程的用户模式地址空间建立TEB;第五步保存用户线程起始地址到ETHREAD中;第六步调用KelnitThread建立起KTHREAD块。
系统中标识了进程ID与线程ID的数据结构都是管理层数据结构,而核心层数据结构则没有这种用于区分的成员,而是直接使用了管理层中的ID。这也充分区分了Windows系统中的层次结构:管理层负责将多个相同类型的结构管理起来并加以区分;核心层不负责区分管理工作,而只处理由管理层交付下来的处理任务。
图中两个进程管理模块中都有一个链表头指针,而两个线程管理块中都有一个连入链表的LIST_ENTRY成员。所以这四个数据结构的整体架构就明确了:管理层和核心层分别为一个进程保存了两个线程队列,线程数据结构根据其所在操作系统层级的不同分别挂入对应的队列中;接着每一层中的线程队列的队首指针放入该层中的进程数据结构中,这样一个分层的多对一线程结构就构建好了。
创建线程在完成这四个数据结构的创建及初始化后,调用任何已登记的系统全局范围的线程创建通知例程并设置好线程的访问令牌,进程的初级建设工作就完成了。最后,将新进程通知Windows子系统,启动初始线程的执行并在新进程环境下执行进程初始化,这样一个进程就被初始化好并且等待执行了。
Linux进程创建过程
Linux的进程创建是通过系统调用fork()、vfork()库函数来完成的,而他们又用各自的参数标志去调用clone(),该函数中copy_process()函数完成进程的创建工作,从其名称可以看出它主要是拷贝父进程的内容并以此创建子进程。以下是函数copy_process()的具体执行流程:
1,创建进程的内核栈、thread_info结构和task_struct结构,并将父进程的相关数据结构的值全部复制到子进程的地址空间中去,可见Linux系统在创建之初的目标是完全复制出一个相同的进程。
2,将进程描述符内的进程运行统计信息清除。因为这些信息从父进程中直接复制过来的,在子进程中没有任何的参考价值,所以作为新进程统计信息还是需要重新计算。
3,将子进程的状态设置为不可中断睡眠,防止子进程在创建中途被从父进程中进程来的某些信号处理程序唤醒而使程序进入一个不可知的状态。
4,设置进程尚未调用exec()函数的标识PF_FORKNOEXEC。Linux特殊的单独层次进程线程管理结构使得在创建之初的默认程序段为父进程的程序段。这样当fork()调用的目的是创建共享资源的线程而不是拥有新程序需要运行的进程时,可以少一次程序段的载入过程,这样节约了非常大的进程创建开销。
5,为新进程分配一个无其他进程使用的进程标识符,从此该进程就与其他进程区别开了。
6,根据父进程中的打开文件和信号处理函数等资源标识确定子进程的状态。创建新的进程时,以上的进容都是与父进程完全不一样的,需要将这些信息从进程创建函数的参数表中拷贝进来。如果是创建新线程时,则直接将这些访问标识符所在的地址空间设置为与新线程共享。
从上述Linux系统创建进程的执行流程我们可以看出,这是一个由父进程复制出子进程,然后根据用户需求不断将子进程特性化的一个过程。因为在创建的过程中大部分复杂的数据结构都通过slab分配器来分配内存空间并直接将父进程已经结构化好的数据存储进去,没有其他系统那样的逐项初始化的过程, 所以Linux系统在进程管理方面要累量高效。
传统的fork()系统调用是直接将父进程所有资源统统复制给新创建进程,但是之后开发者们发现在新创建进程的过程中拷贝的大量数据并不会被共享,且如果创建新进程需要执行新的映像,则之前的大部分拷贝将出现未使用就被立刻替换的情况。因此Linux系统在使用copy-on-write机制来免除这种无谓操作的出现。其主要实现机制就是在创建新进程时并不先为其创建新地址空间,而是让父进程与子进程共享一片地址空间拷贝,这时两个进程的mm域指向内存中同一个mm_struct结构。因为Linux会优先让父进程运行,所以如果子进程直接调用Excec()函数载入新的程序映像时,我们就免除了拷贝父进程代码段等这些复杂而又无用的操作,大大提升了新进程的创建速度。
**原作中写的linux会优先让子进程运行,个人觉得可能错了。