Files
LDD-LinuxDeviceDrivers/study/kernel/process/create
2016-05-19 12:52:27 +08:00
..
...
2016-05-19 12:52:27 +08:00
...
2016-05-19 10:35:08 +08:00
...
2016-05-19 10:35:08 +08:00

Linux下进程的创建过程

日期 内核版本 架构 作者 GitHub CSDN
2016-05-12 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux-进程管理与调度

http://www.linuxidc.com/Linux/2013-07/87011.htm http://www.linuxeye.com/Linux/1827.html http://bbs.csdn.net/topics/390872515 http://blog.csdn.net/yjzl1911/article/details/5613569 http://blog.csdn.net/dagouaofei/article/details/5644119 http://blog.chinaunix.net/uid-23769728-id-3129443.html http://baike.baidu.com/link?url=sCsQDvMUaAikV5W_eKrEL3RVijNHJtOJk8nsCnjlxtnU7yoJ9svp6cwaerQ6Dqc0I-kdoAYrOtMcocCUnzyggK http://blog.chinaunix.net/uid-21718047-id-3070635.html http://blog.163.com/boneshunter_1234/blog/static/340762320084472122207/ http://blog.sina.com.cn/s/blog_626aed8b0100hws6.html

#Linux下进程的创建流程

##进程的复制fork和加载execve

我们在Linux下进行进行编程,往往都是通过fork出来一个新的程序,fork从化字面意义上理解就是说"分叉", 这其实就意味着我们的fork进程并不是真正从无到有被创建出来的。

一个进程,包括代码、数据和分配给进程的资源,它其实是从现有的进程(父进程)复制出的一个副本(子进程),fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,然后如果我们通过execve为子进程加载新的应用程序后,那么新的进程将开始执行新的应用

简单来说,新的进程是通过fork和execve创建的,首先通过fork从父进程分叉出一个基本一致的副本,然后通过execve来加载新的应用程序镜像

  • fork生成当前进程的的一个相同副本,该副本成为子进程

原进程(父进程)的所有资源都以适当的方法复制给新的进程(子进程)。因此该系统调用之后,原来的进程就有了两个独立的实例,这两个实例的联系包括:同一组打开文件, 同样的工作目录, 进程虚拟空间(内存)中同样的数据(当然两个进程各有一份副本, 也就是说他们的虚拟地址相同, 但是所对应的物理地址不同)等等。

  • execve从一个可执行的二进制程序镜像加载应用程序, 来代替当前运行的进程

换句话说, 加载了一个新的应用程序。因此execv并不是创建新进程

所以我们在linux要创建一个应用程序的时候,其实执行的操作就是

  1. 首先使用fork复制一个旧的进程

  2. 然后调用execve在为新的进程加载一个新的应用程序

#写时复制技术

有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,linux采用了写时复制的策略。

写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。

第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。

现在的Linux内核采用一种更为有效的方法,称之为写时复制(Copy On Write,COW)。这种思想相当简单:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。

当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝,

因此会拥有与父进程相同的物理页面.为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面.同时将父进程A对这些物理页面的访问权限也设成只读.

这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常.

do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程A和子进程B各自拥有一块内容相同的物理页面.最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去.

一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值(比如PID)不同。相当于克隆了一个自己。

#0号进程与1号进程

前面我们了解到linux下的进程创建式通过父进程复制自身分叉出的一个副本, 那么我们的系统中就必然存在一个进程是所有进程的祖先,要不然我们从哪里fork分叉出一个个子进程呢,linux下这个进程就是init进程

但是问题也来了,我们的祖先进程init进程,是从哪里来的?

他总不能也是被分叉来的吧?

如果是,那么分叉它的进程是谁,那么它为什么没有成为祖先进程

如果不是, 那么好了, 它是怎么创建出来的(真正的从无到有)

Linux下有两个特殊的进程,idel进程(PID = 0)和init进程(PID = 1)

* idel进程由系统自动创建, 运行在内核态 idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork()产生的进程。完成加载系统后,演变为进程调度、交换 * init进程由idel创建, 由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程 Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。树根是系统自动构造的(或者说是由内核黑客手动创建的),即在内核态下执行的0号进程,它是所有进程的远古先祖。

由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号...的若干终端注册进程getty。

每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。

上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程

注意,上述过程描述中提到:1号内核进程调用执行init函数并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:

  1. init()函数在内核态运行,是内核代码

  2. init进程是内核启动并运行的第一个用户进程,运行在用户态下。

  3. 一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。