fork函数介绍
一个现有的进程可以调用fork函数创建一个新进程。
UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。
#inlcude <unistd.h>
pid_t fork(void); //返回值:子进程返回0,父进程返回子进程ID,出错返回-1
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。
fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID。
fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。
父子进程关系
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。
父子进程共享正文段。
注意:子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下。在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
父子进程运行顺序
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。
实例
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char *argv[]){
pid_t pid=fork();
if ( pid < 0 ) {
fprintf(stderr,"错误!");
} else if( pid == 0 ) {
printf("子进程空间");
exit(0);
} else {
printf("父进程空间,子进程pid为%d",pid);
}
// 可以使用wait或waitpid函数等待子进程的结束并获取结束状态
exit(0);//正常运行程序并退出程序
}
父子进程文件共享
需要注意的一点是:在重定向父进程的标准输出时,子进程的标准输出也会被重定向。实际上,fork的一个特性是父进程所有打开的文件描述符都被复制到子进程中。我们说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项。
一个进程具有三个不同的打开文件:标准输入、标准输出和标准错误。
重要的一点是, 父进程和子进程共享同一个文件偏移量。考虑下述情况: 一个进程fork了一个子进程, 然后等待子进程终止. 假定,作为普通处理的一部分, 父进程和子进程都向标准输出进行写操作。如果父进程的标准输出己重定向(很可能是由shell实现的), 那么子进程写到该标准输出时, 它将更新与父进程共亭的该文件的偏移量. 在这个例子中, 当父进程等待子进程时, 子进程写到标准输出:而在子进程终止后, 父进程也写到标准输出上, 并且知道其输出会追加到子进程所写数据之后。如果父进程和子进程不共享同一文件偏移量,要实现这种形式的交互就要困难得多。
例:
#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#define MAXLEN 2048
int main()
{
pid_t pid;
char buf[MAXLEN];
int file_fd;
file_fd=open("test.txt",O_RDWR|O_CREAT|O_TRUNC); //打开一个文件
if((pid=fork())<0)
{
fprintf(stderr,"fork error:[%s]\n",strerror(errno));
exit(1);
}
else if(pid>0)
{
sleep(1);
write(file_fd,"I am parent\n",19); //父进程写入
}
else
{
write(file_fd,"I am child\n",18); //子进程写入
}
exit(0);
}
test.txt文件结果为:
I am parent
I am child
使用方法
fork有以下两种用法:
(1)一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的:父进程等待客户端的服务请求,当这种请求达到时,父进程fork,使子进程处理此请求,父进程则继续等待下一服务请求。
(2)一个进程要执行不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
注:当进程调用exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段以及堆栈。
vfork函数
vfork函数的调用序列和返回值fork相同,但两者的语义不同。
vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序(如fork的第二种用法)。
fork()用法与fork()相似.但是也有区别,具体区别归结为以下3点:
1、fork():子进程拷贝父进程的数据段,代码段. vfork():子进程与父进程共享数据段.
2、fork():父子进程的执行次序不确定.vfork():保证子进程先运行,在调用exec或_exit之前与父进程数据是共享的,在它调用exec或_exit之后父进程才可能被调度运行。
3、 vfork()保证子进程先运行,在它调用exec或_exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
4、当需要改变共享数据段中变量的值,则拷贝父进程。