一、创建子进程的方法
1、fork
fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。
这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write
(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork的现实意义就不大了。
fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。
在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。
2、vfork
vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
其次,子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。
但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
vfork也是在父进程中返回子进程的进程号,在子进程中返回0。
用vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的
,因此通过vfork共享内存可以减少不必要的开销
再次强调:在使用vfork()时,必须在子进程中调用exit()函数调用,否则会出现:__new_exitfn: Assertion `l != ((void *)0)’ failed 错误!而且,现在这个函数已经很少使用了!
3、clone
目前正在使用的,是fork的升级版。
系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags决决定。
fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。
二,子进程的特性
以下示例只是短暂演示,故无需回收子进程。
示例1:输出信息
$pid = pcntl_fork();
if ($pid > 0) {
echo "father" . PHP_EOL;
} else if (0 == $pid) {
echo "child" . PHP_EOL;
} else {
echo "fork失败" . PHP_EOL;
}
可见子进程的输出信息也打印在当前会话窗口上了。stdout标准输出默认是打印到会话窗口,而输出到哪个窗口,要看该进程是由哪个会话创建的。
因此子进程和父进程的输出都会打印在同一个窗口上,即 pts/3 。
示例2:父子进程都会继续向下执行
$pid = pcntl_fork();
if ($pid > 0) {
echo "father" . PHP_EOL;
} else if (0 == $pid) {
echo "child" . PHP_EOL;
} else {
echo "fork失败" . PHP_EOL;
}
echo getmypid(), PHP_EOL;
$n = 1;
$pid = pcntl_fork();
if ($pid > 0) {
echo "father" . PHP_EOL;
$n += 1;
} else if (0 == $pid) {
echo "child" . PHP_EOL;
$n += 2;
} else {
echo "fork失败" . PHP_EOL;
}
echo $n, PHP_EOL;
$socket = stream_socket_client("tcp://127.0.0.1:8888", $errno, $errstr, 30);
$pid = pcntl_fork();
if ($pid > 0) {
stream_socket_sendto($socket, 'father msg');
sleep(2);
} else if (0 == $pid) {
stream_socket_sendto($socket, 'child msg');
} else {
echo "fork失败" . PHP_EOL;
}
$str = stream_socket_recvfrom($socket, 8129);
echo getmypid(), ' get msg: ', $str, PHP_EOL;
sleep(10);
server代码:https://gitee.com/phprao/socket/blob/master/server/socketServerEpoll.php
TCP服务端信息
socket共享,意味着一方的读,写,关闭都会影响父子关系中另一方的行为。
服务端收到两条信息,因此会响应两条信息过来,stream_socket_recvfrom函数每次只会读取一条,看哪个进程先执行到这里。我们应用中绝大部分的通讯都是基于socket,所以这点需要注意。