Linux进程(三):进程的创建
- fork
- copy-on-write(COW)
- clone, fork, vfork区别与联系
- referfence
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.csdn.net/gogokongyin/article/details/51334773