fork()函数定义如下:
pid_t fork( void);
(pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
函数说明
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。 子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
接下来我们看一些例子:
## fork0
void fork0()
{
if (fork() == 0) {
printf("Hello from child\n");
}
else {
printf("Hello from parent\n");
}
}
从上述运行结果来看,有2次输出,第一次运行到if语句时,由于是父进程fork()!=0,所以执行else语句;
在进入子进程时(与父进程同理),由于fork()==0,所以执行if语句。
**
## fork1
void fork1()
{
int x = 1;
pid_t pid = fork();
if (pid == 0) {
printf("Child has x = %d\n", ++x);
}
else {
printf("Parent has x = %d\n", --x);
}
printf("Bye from process %d with x = %d\n", getpid(), x);
}
这个程序与fork0原理相同,第一次执行父进程,pid!=0所以执行else语句,接下来在执行子进程时,由于子进程是父进程的独立的副本(详情见开头的fork函数说明),x=1,所以再执行if语句;同时也可以看出子进程与父进程的pid是连着的,子进程的pid是父进程的+1。
## fork2
void fork2()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
上述程序:首先父进程进行输出L0,然后运行第一个fork(),产生一个子进程1,然后父进程输出L1,接着在第二个fork时又产生一个子进程2,同时在子进程1中也产生了一个子进程(1,2),然后父进程输出Bye,接着在父进程的子进程2中输出Bye,然后回到子进程1中,子进程1输出L1,接着跟父进程类似,输出Bye,Bye。
## fork3
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
## fork4
void fork4()
{
printf("L0\n");
if (fork() != 0) {
printf("L1\n");
if (fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
## fork5
void fork5()
{
printf("L0\n");
if (fork() == 0) {
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
## fork6
void cleanup(void) {
printf("Cleaning up\n");
}
void fork6()
{
atexit(cleanup);
fork();
exit(0);
}
这里注意一下:int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func。您可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。
父进程退出的时候执行一次Cleanup,子进程退出时也执行一次。
## fork7
void fork7()
{
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
这里,父进程并没有退出,所以导致程序一直在运行,成为僵尸(zombies.)进程,一直在后台运行,具体表现如下,无法在终端创建新程序,只有Ctrl+C或者Ctrl+Z退出此程序才可以进行新的操作。
## fork8
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
} else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
和前面的一样,父进程返回非0,所以输出PPID;然后进入子进程,输出PID,但是之后进入死循环,子进程并没有退出,成为僵尸进程。
## fork9
void fork9()
{
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}
这里注意一下:pid_t wait(int *status)
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
上述程序,父进程需在调用wait()时等待子进程结束,再执行下面的操作。
## fork10
void fork10()
{
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
这个程序一直for循环,让每个子进程有了自己的状态值,相应的父进程一直在wait着子进程,当最深的子进程推出时,输出pid和状态值,之后再进行上一层的输出。
## fork11
void fork11()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
这里注意:pid_t waitpid(pid_t pid,int *status,int options);
返回:如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1
以上程序在父进程中等待子进程退出,并输出他们的pid。
总结
1.fork 函数被调用一次,但返回两次
2.将子进程 ID 返回给父进程的理由是:一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程 ID
3.fork 使子进程得到返回值 0 的理由是:一个进程只会有一个父进程,所以子进程可以通过调用 getppid 函数,来获得其父进程的进程 ID
4.进程 ID 0 总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为 0
5.fork 之后进程的动作:子进程和父进程继续执行 fork 调用之后的指令。
6.子进程是父进程的副本。fork 之后,子进程获得父进程数据空间、堆和栈的副本。注意:这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段
其实创建子进程,就是把父进程的 PCB 拷贝过来,稍加修改,例如,修改子进程的进程 ID(肯定不能和父进程一样), 就变成子进程的 PCB 了。
7.进程调度:父进程和子进程谁先被执行,完全由进程调度器决定,谁先调度,是不确定的。