Linux进程(三):进程的创建
fork
fork用来创建一个进程,当我们的进程执行到了fork的时候,系统将为我们复制一份进程资源,并且两个进程都将从fork函数返回。
从内核的调度层面来说,只要一个进程存在task_struct,那么该进程就可以被调度。所以在我们的父进程把子进程fork出来的时刻,父进程会将该进程内的资源拷贝给子进程。

当一个进程刚刚被创建出来的时候执行的是一个copy,但是任何秀海都将造成父子进程资源的分类,如:chroot、open、写memory、mmap、sigaction等。
task_struct中,文件资源、信号资源等资源的分裂都比较好实现,唯一困难的就是内存资源的分裂,因为我们需要探测是谁在写哪一块内存内容,且对于fork来讲,有一个很讨厌的东西叫exec系列的系统调用,它会勾引子进程另起炉灶。如果创建子进程就要内存拷贝的的话,一执行exec,辛辛苦苦拷贝的内存又被完全放弃了。由于fork()后会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,linux中引入了“写时复制技术-Copy-On-Write”。接下来就是内存资源分裂的写时分裂技术。
copy-on-write(COW)

在最开始时,父进程的虚拟地址为virt1,物理地址为phy1,此时进程根据MMU可通过虚拟地址查询到物理地址并且获取内存中的数据。且当前内存数据段的权限为R+W。
当父进程通过fork创建出子进程时,此时子进程的虚拟地址以及物理地址都和父进程的相同,但是Linux将页表当中这一页所对应的访问权限变成了RD-ONLY只读权限。
若子进程或父进程在某个需要修改内存中的数据时,CPU一旦往一块RD-ONLY权限的内存页中写数据,将会收到一个page fault(缺页中断)。假设此刻子进程需要向这块内存中写入数据,CPU收到中断之后将分配一块新的物理内存给子进程,此时子进程将得到一片新的物理地址(如上图中的phy2),然后Linux内核会将之前的phy1中的内容拷贝到phy2中去,然后修改子进程的页表,使得子进程的虚拟地址virt1指向新的物理地址phy2。此时,父子进程的虚拟地址都是相同的,但指向的物理地址则是各自不同的。
在此之后,Linux内核将两个进程的页表的访问权限都改成R+W,父子进程实现内存分裂。
copy-on-write技术严重依赖于CPU中的MMU(memory management unit)。若CPU中没有MMU,则fork是不能工作的。
在Linux2.6之前(2.6版本之后Linux系统支持了无MMU的CPU),在没有MMU的CPU中是不可能执行copy-on-write的,所以在这样的CPU中去跑Linux时是没有fork的,只有vfork。
vfork
vfork和fork的其中一点区别主要在于**vfork在调用之后将阻塞父进程,直到子进程调用_exit或exec这两个系统调用。**同时vfork和fork还有一点区别:内存分裂技术不同。
在执行vfork时,父进程的mm_struct不再对拷给子进程,而是子进程的task_struct中的mm指针直接指向父进程mm_struct指向的结构体。

可用以下代码做一个小测试:
int data = 10;
int child_process()
{
printf("Child process %d, data %d\n",getpid(),data);
//注意:执行data=20这行代码的代价时Linux内核其实是做了一系列操作:
//首先,由于此块内存时只读的,所以将发生缺页中断
//发生缺页中断后Linux内核将申请一块新的内存
//申请完内存后内核把该进程的虚拟地址指向新的物理地址,并把老的物理页中的内容拷贝到新的物理页中
//拷完之后Linux把父子进程中的内存访问权限都改为R+W,并把pc指针再次指向data=20执行
data = 20;
printf("Child process %d, data %d\n",getpid(),data);
_exit(0);
}
int main(int argc, char* argv[])
{
if(vfork()==0) {
child_process();
}
else{
sleep(1);
printf("Parent process %d, data %d\n",getpid(), data);
}
}
这个程序的输出为:

在此处,vfork相当于clone函数将flags标志设置为CLONE_VM和CLONE_VFORK,接下来我们就介绍更加强大的函数:clone
clone
clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。
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`的值);
arg就是传给子进程的参数一般为(0);
flags为要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共享,在这里设置参数:
下面是flags可以取的值:
| 参数标志 | 含义 |
|---|---|
CLONE_PARENT | 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” |
CLONE_FS | 子进程与父进程共享相同的文件系统,包括root、当前目录、umask |
CLONE_FILES | 子进程与父进程共享相同的文件描述符(file descriptor)表 |
CLONE_NEWNS | 为子进程创建新的命名空间 |
CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signal handler)表 |
CLONE_PTRACE | 若父进程被trace,子进程也被trace,继续调试子进程 |
CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
CLONE_VM | 子进程与父进程运行于相同的内存空间 |
CLONE_PID | 子进程在创建时PID与父进程一致 |
CLONE_THREAD | Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 |
CLONE_IDLETASK | 将PID设置为0(只供idle进程使用) |
CLONE_SETTID | 将TID会写至用户空间 |
CLONE_SITTLS | 为子进程创建新的TLS |
CLONE_SYSVSEM | 父子进程共享System V SEM_UNDO语义 |
CLONE_UNTRACE | 防止跟踪进程在子进程上强制执行CLONE_PTRACE |
CLONE_STOP | 以TASK_STOPPED状态开始子进程 |
当我们使用pthread_create创建线程时,本质上也是通过调用Linux系统中的clone()函数,但此时pthread_create函数调用clone时会将子进程的各种资源指针全部设为和父进程相同的指针:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
上面的代码产生的结果和调用fork()差不多,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。换个说法就是,新建的进程和它的父进程就是流行的所谓线程。这种方式其实就是LWP的实现。
clone, fork, vfork区别与联系
系统调用服务例程sys_clone,sys_fork,sys_vfork三者最终都是调用do_fork函数完成.
do_fork的参数与clone系统调用的参数类似, 不过多了一个regs(内核栈保存的用户模式寄存器). 实际上其他的参数也都是用regs取的。
三者具体调用do_fork时的参数不同。
referfence
http://blog.youkuaiyun.com/gogokongyin/article/details/51334773
本文深入解析Linux中进程创建的三种方式:fork、vfork和clone。详细阐述了copy-on-write技术如何解决内存复制效率问题,以及vfork和clone在资源复制和共享上的独特机制。
:进程的创建&spm=1001.2101.3001.5002&articleId=107939191&d=1&t=3&u=6b5f155882a94d3789ceaddd48e1842b)
1638

被折叠的 条评论
为什么被折叠?



