fork函数与vfork函数

本文围绕Linux进程管理展开,介绍了fork、vfork等创建子进程的函数,以及sleep、wait、waitpid等控制进程状态的函数。还阐述了僵尸、孤儿、守护等特殊进程的概念,同时对比了exit和_exit函数的区别,以及fork和vfork函数的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. fork函数

需要的头文件

#include <sys/types.h>
#include <unistd.h>

函数结构:pid_t fork(void) 

功能:在已有的进程基础上有创建一个子进程

参数:无

返回值

  1. 成功:>0 ,代表父进程的代码区;=0,代表子进程的代码区
  2. 失败:返回 -1,给父进程;子进程不会创建

注意:使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,父子进程是独立的。

1.2 地址空间

包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。

子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。

1.3 fork函数的执行结果

2. 创建子进程 

2.1 不区分父子进程(不推荐)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    //通过fork函数创建一个子进程
    fork();
    printf("hello world\n");

    while (1);
    return 0;
}

执行结果:

注意:主要执行一次fork函数,就会在原有的进程基础上创建一个新的子进程;而且如果fork函数之后不区分父子进程的代码区,则后面所有的代码都会执行。

 2.2 区分父子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	//通过fork函数创建一个子进程

#if 0
	//注意:主要执行一次fork,就会在原有的进程基础上创建一个新的子进程
	//而且如果fork之后不区分父子进程的代码区,则后面所有的代码都会执行
	fork();
	printf("hello world\n");
    
	while(1)
	;
#endif

	//通过fork函数的返回值来区分父子进程的独立的代码区
	pid_t pid;

	pid = fork();
	if(pid < 0)
	{
		perror("fail to fork");
		return -1;
	}
	else if(pid > 0) //父进程的代码区
	{
		while(1)
		{
			printf("parent: pid = %d, ppid = %d\n", getpid(), getppid());
			printf("pid = %d\n", pid);
			printf("this is a parent process\n");
			sleep(1);
			printf("****************\n");
		}
	}
	else //子进程的代码区
	{
		while(1)
		{
			printf("son: pid = %d, ppid = %d\n", getpid(), getppid());
			printf("this is a son process\n");
			sleep(1);
			printf("-----------------\n");
		}
	}

	return 0;
}

注意:父子进程是来回交替执行的,谁先运行,谁后运行是不确定的,不要认为父进程执行完之后才会执行子进程。

2.3 父子进程拥有独立的地址空间

代码展示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int a = 666;

int main(int argc, char *argv[])
{
	pid_t pid;
	static int b = 777;
	int c = 888;

	pid = fork();
	if(pid < 0)
    {
		perror("fail to fork");
        return -1;
    }
	if(pid > 0)  //父进程的代码区
	{
		printf("This is a parent process\n");
		a++;
		b++;
		c++;
		printf("a = %d, b = %d, c = %d\n", a, b, c);
	}
	else  //子进程的代码区
	{ 
		sleep(1);
		printf("This is a son process\n");
		printf("a = %d, b = %d, c = %d\n", a, b, c);
	}

	while(1)
	{

	}
    
	return 0;
}

执行结果: 

注意:子进程会复制父进程fork之前的所有内容;但是fork之后,父子进程完全独立,所以不管双方怎么改变(堆区、栈区、数据区等),都不会收对方影响。

2.4 子进程继承父进程的空间

代码展示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int fd;
	if((fd = open("file.txt", O_RDONLY)) == -1)
	{
		perror("fail to open");
		return -1;
	}

	pid_t pid;
	pid = fork();
	if(pid < 0)
    {
		perror("fail to fork");
        return -1;
    }
	if(pid > 0)
	{
		printf("This is a parent process\n");

		char buf[32] = "";
		if(read(fd, buf, 30) == -1)
		{
			perror("fail to read");
			return -1;
		}

		printf("buf = [%s]\n", buf);

	}
	else 
	{
		sleep(1);
		printf("This is a son process\n");

		char buf[32] = "";
		if(read(fd, buf, 30) == -1)
		{
			perror("fail to read");
			return -1;
		}

		printf("buf = [%s]\n", buf);
	}

	while(1)
	{

	}
    
	return 0;
}

执行结果:

总结:子进程会继承父进程的一些公有的区域,例如磁盘空间,内核空间;文件描述符的偏移量保存在内核空间中,所以父进程改变偏移量,则子进程获取的偏移量是改变之后的。 ​​​ 

3. sleep函数

需要的头文件

#include <unistd.h>

函数结构:unsigned int sleep(unsigned int seconds); 

功能:进程在一定的时间内没有任何动作,称为进程的挂起(进程处于等待态)

参数:seconds:指定要挂起的秒数

返回值:若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数

注意:进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态

代码展示:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    while(1)
    {
        printf("hello world\n");

        sleep(2);
    }
    
    return 0;
}

总结:当运行到sleep函数后,程序会在此位置等待设定的秒数,当秒数到大后,代码会接着执行sleep运行时进程为等待态,时间到达后会先切换到就绪态,如果代码继续运行,再切换到运行态

4. 进程等待

4.1 wait函数

父子进程有时需要简单的进程间同步,如父进程等待子进程的结束

Linux下提供了两种等待函数:wait();waitpid()

需要包含的头文件 

#include <sys/types.h>
#include <sys/wait.h>

函数结构:pid_t  wait(int*  status); 

功能:

  1. 等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
  2. 调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
  3. 若调用进程没有子进程或它的子进程已经结束,该函数立即返回。

返回值 

  1. 成功:返回子进程号
  2. 失败:返回-1

status:函数返回时,参数status中包含子进程退出时的状态信息,子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段。子进程可以通过 exit 或者 _exit 函数发送退出状态。

取出子进程的退出信息

  • WIFEXITED(status)如果子进程是正常终止的,取出的字段值非零。

  • WEXITSTATUS(status)返回子进程的退出状态,退出状态保存在status变量的8~16位,在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。

注意:此status是个 wait 的参数指向的整型变量。

代码展示(wait):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	pid_t pid;
	
	pid=fork();
	if(pid<0)
	{
		perror("fail to fork");
        return -1;
    }
	if(pid == 0)
	{
		int i = 0;
		for(i=0;i<5;i++)
		{
			printf("this is son process\n");
			sleep(1);
		}

		//使用exit退出当前进程并设置退出状态
		exit(2);
	}
	else 
	{
		//使用wait在父进程中阻塞等待子进程的退出
		//不接收子进程的退出状态
		//wait(NULL);

		//接收子进程的退出状态,子进程中必须使用exit或者_exit函数退出进程是发送退出状态
		int status = 0;
		wait(&status);

		if(WIFEXITED(status) != 0)
		{
			printf("The son process return status: %d\n", WEXITSTATUS(status));
		}

		printf("this is father process\n");	
	}

	return 0;
}

执行结果 : 

4.2 waitpid函数

需要包含的头文件 

#include <sys/types.h>
#include <sys/wait.h>

函数结构:pid_t  waitpid(pid_t pid, int *status, int options); 

参数pid的值有以下几种类型:

  1. pid>0:等待进程ID等于pid的子进程
  2. pid=0:等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
  3. pid=-1:等待任一子进程,此时waitpid和wait作用一样。
  4. pid<-1:等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
  5. status:参数中包含子进程退出时的状态信息
  6. options:参数能进一步控制waitpid的操作
  7. options = 0:同wait,阻塞父进程,等待子进程退出。
  8. options = WNOHANG:没有任何已经结束的子进程,则立即返回。
  9. options = WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(跟踪调试,很少用到)

功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。

返回值:

  1. 成功:返回状态改变了的子进程的进程号;如果设置了选项WNOHANG并且pid指定的进程存在则返回0。
  2. 失败:返回‐1。当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。

总结:wait(status) <==> waitpid(‐1, status, 0)

4.3 wait 和 waitpid 的区别
  • waitpid相比于wait具有更多的灵活性,可以指定要等待的子进程的PID以及等待的条件。
  • wait只能等待任意一个子进程的结束,而waitpid可以根据参数的不同等待指定的子进程。
  • waitpid还可以通过设置WNOHANG选项来进行非阻塞等待。

代码展示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	pid_t pid;
	
	pid=fork();
	if(pid < 0)
    {
		perror("fail to fork");
        return -1;
    }
	if(pid == 0)
	{
		int i = 0;
		for(i=0;i<5;i++)
		{
			printf("this is son process\n");
			sleep(1);
		}
		exit(0);
	}
	else 
	{		
		waitpid(pid, NULL, 0);
		printf("this is father process\n");	
	}
	return 0;
}

执行结果:

5. 特殊进程

5.1 僵尸进程(Zombie  Process)
  • 进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程。
  • 子进程已运行结束,父进程未调用wait或者waitpid函数回收子进程的资源是子进程变为僵尸进程的原因。
5.2 孤儿进程(Orphan  Process)

父进程运行结束,但子进程未运行结束的子进程。

5.3 守护进程(Zombie  Process)

守护进程又叫精灵进程,守护进程是特殊的孤儿进程,这样进程脱离终端,在后台运行

6. 进程终止

6.1 exit函数

需要的头文件

#include <stdlib.h>

函数结构:void   exit(int  status);

参数:status:退出状态,由父进程通过wait函数接收这个状态,成功一般设置0;失败一般设置非0

功能:退出当前进程

返回值:

6.2 _exit函数

需要的头文件

#include <unistd.h>

函数结构:void  _exit(int status);

功能:退出当前进程

参数:status:退出状态,由父进程通过wait函数接收这个状态,成功一般设置0;失败一般设置非0

功能:退出当前进程

返回值:

8.3 exit和_exit函数的区别

exit为库函数,而_exit为系统调用,exit会刷新缓冲区,但是_exit不会刷新缓冲区;一般会使用exit

代码展示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void myfun()
{
    printf("nihao beijing");

    //使用return
    //return除了可以返回值以外,在主函数中使用可以退出进程,但是在子函数中使用只能退出当前函数
    //return ;

    //使用exit
    //exit可以退出一个进程并且可以刷新缓冲区
    exit(0);
    
    //使用_exit
    //_exit可以退出一个进程,但是不会刷新缓冲区
    //_exit(0);

    printf("welcome to 1000phone\n");
}

int main(int argc, char const *argv[])
{
    printf("hello world\n");

    myfun();

    printf("hello kitty\n");
    
    return 0;
}

总结:

  1. 使用return,return除了可以返回值以外,在主函数中使用可以退出进程,但是在子函数中使用只能退出当前函数;
  2. 使用exit,exit可以退出一个进程并且可以刷新缓冲区
  3. 使用_exit,_exit可以退出一个进程,但是不会刷新缓冲区

7. 进程退出清理

7.1 atexit

需要的头文件

#include <stdlib.h>

函数结构:int  atexit(void (*function)  (void));

功能:注册进程正常结束前调用的函数,进程退出执行注册函数

参数

funtion: 

  1. 进程结束前,调用函数的入口地址;
  2. 一个进程中可以多次调用atexit函数注册清理函数;
  3. 正常结束前调用函数的顺序和注册时的顺序相反

返回值:

  1. 成功:0
  2. 失败:非0 

代码展示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void clear_fun1(void)
{
	printf("perform clear fun1 \n");
}

void clear_fun2(void)
{
	printf("perform clear fun2 \n");
}

void clear_fun3(void)
{
	printf("perform clear fun3 \n");
}

int main(int argc, char *argv[])
{
    //atexit函数在进程结束时才会去执行参数对应的回调函数
    //atexit多次调用后,执行顺序与调用顺序相反
	atexit(clear_fun1);
	atexit(clear_fun2);
	atexit(clear_fun3);
	printf("process exit 3 sec later!!!\n");
	sleep(3);
	return 0;
}

执行结果:

 8. vfork

需要包含的头文件

#include <unistd.h>
#include <sys/types.h>

函数结构:pid_t   vfork(void) 

功能:vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。

返回值:

  1. 成功:子进程中返回0,父进程中返回子进程ID
  2. 失败:‐1。
8.1 fork和vfork函数的区别
  1. vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
  2. vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间
8.2 子进程在父进程之前运行

代码展示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	pid_t pid;
	
	//使用vfork函数创建完子进程后
	//子进程会先执行,直到子进程执行exit或者exec后,父进程才会执行
	pid = vfork();
	if(pid < 0)
    {
		perror("fail to vfork");
        exit(1);
    }
	if(pid == 0) //子进程的代码区
	{
		int i = 0;
		for(i=0;i<5;i++)
		{
			printf("this is son process\n");
			sleep(1);
		}
		exit(0);
	}
	else  //父进程代码区
	{
		while(1)
		{
			printf("this is father process\n");
			sleep(1);
		}
	}
    
	return 0;
}

执行结果: 

注意: 使用vfork函数创建完子进程后;子进程会先执行,直到子进程执行 exit 或者 exec 后,父进程才会执行

8.3 子进程和父进程共享同一块空间 

代码执行:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int a = 10;
int main(int argc, char *argv[])
{
	pid_t pid;
	int b = 9;

	//使用vfork创建完子进程
	//在子进程执行exit或者exec之前,父子进程共有同一块地址空间
	pid = vfork();
	if(pid < 0)
	{
		perror("fail to vfork");
        exit(1);
    }
	if(pid == 0)
	{
		a++;
		b++;
		printf("in son process a=%d, b=%d\n", a, b);
		exit(0);
	}
	else
	{
		printf("in father process a=%d, b=%d\n", a, b);
	}
	return 0;
}

执行结果: 

注意:使用vfork函数创建子进程子进程;在子进程执行exit或者exec之前,父子进程共用同一块地址空间。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值