一:linux创建新进程的过程
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个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号内核线程->1号用户进程(init进程)->getty进程->shell进程
注意,上述过程描述中提到:1号内核进程调用执行init并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
1.init()函数在内核态运行,是内核代码
2.init进程是内核启动并运行的第一个用户进程,运行在用户态下。
3.init()函数调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。
二:fork,vfork,clone都是linux的系统调用,用来创建子进程的,但是大家在使用时经常混淆,这里给出具体例子讲解三者的联系与区别。
进程由4个要素组成:
1.进程控制块:进程标志
2.进程程序块:可与其他进程共享
3.进程数据块:进程专属空间,用于存放各种私有数据以及堆栈空间。
4.独立的空间(如果没有4则认为是线程)
1、fork
extern pid_t fork(void)
由fork创建的新进程被称为子进程。Fork函数被调用一次,但返回两次,两次返回的唯一区别是子进程返回值是0,而父进程返回值是新子进程的ID.将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程获得其所有子进程ID,fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid()函数获得其父进程的ID
子进程合父进程继续执行fork之后的指令,其子进程会复制父进程的所有信息,但文件描述符是共享的!
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据或许可以共享。更糟糕的是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。
Linux的fork()使用写时拷贝 (copy- on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只 用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共 享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下---例如,fork()后立即执行exec(),地址空间 就无需被复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。
如:
运行结果
Child process 6427, data 10
Child process 6427, data 20
Parent process 6426, data 10
第 1个Child process 6427, data 10是因为子进程创建时task_struct的mm直接拷贝自parent的mm;第2个Child process 6427, data 20是因为子进程进行了“写时拷贝”,有了自己的dataa;第3个Parent process 6426, data 10输出10是因为子进程的data和父进程的data不是同一份。
总结:优点是子进程的执行独立于父进程,具有良好的并发性。缺点是两者的通信需要专门的通信机制,如pipe、fifo和system V等。有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,子进程复制了父进程的task_struct,系统堆栈空间和页面表,在子进程运行前,两者指向同一页面。而当子进程改变了父进程的变量时候,会通过copy_on_write的手段为所涉及的页面建立一个新的副本。因此fork效率并不低。
2、vfork
vfork函数创建的子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改都为父进程所见。这与fork是完全不同的,fork进程是独立的空间。另外一点不同的是vfork创建的子进程后,父进程会被阻塞,直到子进程执行exec()和exit()。如:
运行结果为:This is son, his num is: 2. and his pid is:4139
This is father, his num is: 2, his pid is: 4138
从运行结果可以看到vfork创建出的子进程(线程)共享了父进程的num变量,这一次是指针复制,2者的指针指向了同一个内存
总结:当创建子进程的目的仅仅是为了调用exec()执行另一个程序时,子进程不会对父进程的地址空间有任何引用。因此,此时对地址空间的复制是多余的,通过vfork可以减少不必要的开销。
3、clone
函数功能强大,带了众多参数,因此由他创建的进程要比前面2种方法要复杂。clone可以让你有选择性的继承父进程的资源,你可以选择像vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。
总结:
clone, fork, vfork实现方式
大致相同:
系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成.
do_fork的参数与clone系统调用的参数类似, 不过多了一个regs(内核栈保存的用户模式寄存器). 实际上其他的参数也都是用regs取的
区别在于:
clone:
clone的API外衣, 把fn, arg压入用户栈中, 然后引发系统调用. 返回用户模式后下一条指令就是fn.
sysclone: parent_tidptr, child_tidptr都传到了 do_fork的参数中
sysclone: 检查是否有新的栈, 如果没有就用父进程的栈 (开始地址就是regs.esp)
fork, vfork:
服务例程就是直接调用do_fork, 不过参数稍加修改
clone_flags:
sys_fork: SIGCHLD|0;
sys_vfork: SIGCHLD| (clone_vfork | clone_vm)
用户栈: 都是父进程的栈.
parent_tidptr, child_ctidptr都是NULL.