Linux系统编程之进程(创建和退出)

一、创建进程 fork函数的使用
二、创建进程 内存是怎么分配的
三、fork函数定义 及 创建进程的目的
四、vfork函数 也可以创建进程与fork的区别
五、进程退出 exit函数

一、创建进程函数fork()的使用

fork()函数初识

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

       pid_t fork(void);

fork函数调用成功返回两次

  • 返回值为0, 代表当前进程是子进程
  • 返回值非负数,代表当前进程为父进程
  • 调用失败,返回-1

fork()函数代码实现

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

int main()
{
	pid_t pid;

	//pid_t getpid(void);
	pid = getpid();
	//pid_t getppid(void);
	
	fork();

	printf("my pid is %d\n",pid);

	while(1);

	return 0;
}       

请添加图片描述

调试

  1. 打印当前getpid()
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid;

	//pid_t getpid(void);
	pid = getpid();
	//pid_t getppid(void);
	
	fork();

	printf("my pid is: %d,current pro id: %d\n",pid,getpid());

	while(1);

	return 0;
}       

函数调用成功返回两次
请添加图片描述

  1. 父进程和子进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid1;
	pid_t pid2;

	//pid_t getpid(void);
	pid1 = getpid();
	printf("before fork: pid = %d\n",pid1);

	fork();
	pid2 = getpid();
	printf("after fork: pid = %d\n",pid2);

	if(pid1 == pid2){
		printf("this is parent process\n");
	}else{
		printf("this is child process,child pid = %d\n",getpid());
	}

	return 0;
}

fork前 只打印了一次
fork后 打印了两次

  • 父进程先运行打印了一遍
  • 子进程再运行进行了打印

只是返回值不同进行了不同的打印

请添加图片描述

  1. 调试判断返回值
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid;

	printf("parent process: pid = %d\n",getpid());
	//pid_t getpid(void);
	pid = fork();

	if(pid > 0){
		printf("this is parent process,pid = %d\n",getpid());
	}
/*	else if(pid == 0){
		printf("this is child process,child pid = %d\n",getpid());
	}
*/
	return 0;
}

前面是注释前的结果后面是注释后的结果

请添加图片描述

  1. 调试判断父进程和子进程的关系
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid1;
	pid_t pid2;
	pid_t retpid;

	//pid_t getpid(void);
	pid1 = getpid();
	printf("before fork: pid1 = %d\n",pid1);

	retpid = fork();

	pid2 = getpid();
	printf("after fork: pid2 = %d\n",pid2);

	if(pid1 == pid2){
		printf("this is parent process:retpid = %d\n",retpid);
	}else{
		printf("this is child process:retpid = %d,child pid = %d\n",retpid,getpid());
	}

	return 0;
}
  • 每一个进程都有他的存储空间
  • fork函数创建了一个新进程
  • 父子进程谁先跑取决于进程调度

请添加图片描述

  • 发现父进程返回值是大于0(19754)的,且刚好是子进程的pid号
  • 子进程通过fork后获得了父进程的pid进行了复制并把pid返回值赋予成0

二、创建进程内存是怎么分配的

“完全拷贝"和"写时复制 (Copy-On-Write, COW)”

是两种不同的策略,用于处理数据的复制和共享。它们有不同的实现方式和应用场景:

完全拷贝 (Full Copy):
  • 完全拷贝是一种立即进行数据复制的策略,即在进行数据复制操作时,会复制所有的数据。
  • 这意味着数据被复制到一个新的内存区域,生成一个完全独立的副本。这两个副本之间没有共享数据。
  • 完全拷贝通常用于立即创建独立的副本,以确保数据完全独立,修改一个副本不会影响其他副本。
  • 这种策略的主要优点是独立性和可预测性,但可能会占用更多的内存和计算资源。
写时复制 (Copy-On-Write, COW):
  • 写时复制是一种推迟数据复制的策略,即在数据需要被修改时才进行复制,而在数据未被修改时,多个引用可以共享相同的数据。
  • 初始时,多个引用指向相同的数据。当某个引用试图修改数据时,系统会执行数据复制操作,将数据复制到一个新的内存区域,然后使修改操作仅影响新复制的数据,而不会影响其他引用。
  • 写时复制通常用于减少资源占用,特别是在多个引用大型数据时,可以避免不必要的数据复制。
  • 这种策略的主要优点是资源效率,但需要谨慎管理数据的复制,以避免潜在的并发问题。

总结:完全拷贝和写时复制是两种不同的数据复制策略,它们的主要区别在于何时进行数据复制操作。完全拷贝是立即复制所有数据,生成独立副本,而写时复制是推迟复制操作,只在需要修改数据时才进行复制,以减少资源占用。选择哪种策略取决于具体的应用需求,资源约束和性能考虑。

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

int main()
{
	pid_t pid;
	int a = 10;

	printf("parent process: pid = %d\n",getpid());
	//pid_t getpid(void);
	pid = fork();

	if(pid > 0){
		printf("this is parent process,pid = %d\n",getpid());
	}
	else if(pid == 0){
		printf("this is child process,child pid = %d\n",getpid());
		a += 10;
	}
	printf("a = %d\n",a);

	return 0;
}

请添加图片描述

三、fork函数定义及创建进程的目的

fork函数定义

一个现存进程调用fork函数是UNIX内核创建一个新进程的唯一方法 (这并不适用于前节提
及的交换进程、init进程和页精灵进程。这些进程是由内核作为自举过程的一部分以特殊方式
创建的)。

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回:子进程中为0,父进程中为子进程I D,出错为-1

由fork创建的新进程被称为子进程( child process)。该函数被调用一次,但返回两次。两次返
回的区别是子进程的返回值是 0,而父进程的返回值则是新子进程的进程 ID。将子进程ID返回
给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以
获得其所有子进程的进程 ID。fork使子进程得到返回值 0的理由是:一个进程只会有一个父进
程,所以子进程总是可以调用getppid以获得其父进程的进程ID (进程ID 0总是由交换进程使用,
所以一个子进程的进程ID不可能为0 )。
子进程和父进程继续执行 fork之后的指令。子进程是父进程的复制品。例如,子进程获得
父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这
些存储空间部分。如果正文段是只读的,则父、子进程共享正文段 (见7.6节)。
现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在 fork之后经常跟随着
exec。作为替代,使用了在写时复制( Copy-On-Write, COW)的技术。这些区域由父、子进程共
享,而且内核将它们的存取许可权改变为只读的。如果有进程试图修改这些区域,则内核为有
关部分,典型的是虚存系统中的“页”,做一个拷贝。Bach〔1986〕的9.2节和Leffler等〔1989〕
的5.7节对这种特征做了更详细的说明。

fork有两种用法:

(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程
中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进
程处理此请求。父进程则继续等待下一个服务请求。
(2) 一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程在
从f o r k返回后立即调用exec (我们将在8.9节说明exec)。

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

int main()
{
	pid_t pid;
	int a = 10;
	
	while(1){
		print("please input a \n");
		scanf("%d",&a);
		if(a == 1){
			pid = fork();

			if(pid > 0){
			}
			else if(pid == 0){
				while(1){
					printf("do net request,pid = %d\n",getpid());
					sleep(3);
				}	
			}
		}
	}
	else{	
	printf("a = %d\n",a);
	}

	return 0;
}

服务器不断接收客户端的连接
不影响主进程的运行也能键入fork创建新的进程

请添加图片描述
请添加图片描述

fork创建一个子进程的目的

fork 是一个在类Unix操作系统中用于创建子进程的系统调用。它的主要目的是在新进程中创建一个与父进程几乎完全相同的副本,以便执行不同的任务,或者在多进程编程中用于实现并行性。以下是 fork 创建子进程的主要目的:

  1. 并行执行:
    fork 允许在多核或多处理器系统上并行执行多个任务。父进程和子进程可以同时运行,以提高程序的性能。

  2. 任务隔离:
    每个进程都有自己独立的内存空间和执行上下文,因此 fork 可用于隔离不同任务或功能,以防止它们相互干扰。这在操作系统中是实现多任务处理的基础。

  3. 创建后台服务:
    通过 fork,可以创建后台服务进程,以便在不中断用户界面的情况下执行后台任务,例如网络服务器或守护进程。

  4. 多进程通信:
    父进程和子进程可以通过进程间通信(IPC)机制(如管道、共享内存、消息队列等)进行数据交换和协作。

  5. 容错性:
    在某些情况下,fork 可用于实现容错机制。当一个进程崩溃时,可以通过 fork 创建一个新的进程,以继续执行相同的任务。

  6. 资源分配:fork 允许将系统资源(如文件描述符、内存等)复制到新进程,以便新进程可以继续使用这些资源,而不会干扰父进程。

需要注意的是,fork 创建的子进程是父进程的副本,但有一些区别,例如它们有不同的进程ID(PID),并且通常具有不同的执行上下文。父进程和子进程之间的主要区别通常在于它们的返回值,父进程接收到子进程的PID,而子进程接收到0。这使得程序可以根据返回值来执行不同的代码分支,以区分父子进程的行为。

四、vfork()函数 也可以创建进程与fork的区别

vforkfork 的区别

都是在Unix-like操作系统中用于创建子进程的系统调用,但它们之间有一些重要的区别。这些区别主要涉及到进程的创建方式、父子进程之间的关系和执行时的行为。

  1. 进程创建方式:
  • fork 创建子进程时,会复制父进程的整个地址空间(包括数据、堆、栈等),并且子进程与父进程是完全独立的,它们各自拥有自己的内存副本。
  • vfork(“v"代表"virtual”,虚拟)创建子进程时,不会立即复制整个地址空间,而是共享父进程的地址空间。子进程在启动时与父进程共享相同的内存,直到它调用execexit为止。
  1. 父子进程关系:
  • 在使用 fork 创建子进程后,父子进程是完全独立的,它们可以同时运行,但它们之间的内存是分开的。父子进程之间没有共享内存,因此需要通过进程间通信(IPC)机制来进行数据交换。
  • 在使用 vfork 创建子进程后,父子进程共享同一地址空间,这意味着它们共享相同的内存,包括变量和数据。父进程在子进程调用execexit之前会挂起,直到子进程执行完毕。
  1. 执行时的行为:
  • 由于 vfork 子进程与父进程共享内存,因此子进程必须非常小心地操作内存,以避免破坏父进程的数据。通常,vfork 主要用于创建子进程,然后子进程立即通过exec来执行新的程序,以避免操纵共享内存。
    fork 创建的子进程是独立的,可以自由操作其内存空间。

总结
vforkfork 都用于创建子进程,但它们的行为和用途有重要区别。 vfork 用于在父子进程之间共享内存的情况,通常用于创建子进程后立即执行新程序。fork 创建独立的子进程,适用于需要子进程在独立的内存空间中执行的情况。

代码进行对比

fork的运行

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

int main()
{
	pid_t pid;

	pid = fork();

	if(pid > 0){
		while(1){
			printf("this is parent process,pid = %d\n",getpid());
			sleep(1);
		}
	}else if(pid == 0){
		while(1){
			printf("this is child process,child pid = %d\n",getpid());
			sleep(1);
		}	
	}

	return 0;
}

请添加图片描述

vfork的运行

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

int main()
{
	pid_t pid;
	int cnt = 0;

	pid = vfork();

	if(pid > 0){
		while(1){
			printf("this is parent process,pid = %d\n",getpid());
			sleep(1);
		}
	}else if(pid == 0){
		while(1){
			printf("cnt = %d\n",cnt);
			printf("this is child process,child pid = %d\n",getpid());
			sleep(1);
			cnt++;
			if(cnt == 3){
				exit(0);
				break;
			}
		}	
	}

	return 0;
}

请添加图片描述

关键区别

关键区别一:

  • vfork 直接使用父进程存储空间,不拷贝。

关键区别二:

  • vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。

五、进程退出 exit函数

定义

进程退出是指一个正在运行的进程结束其执行并释放其资源,将控制权返回给操作系统。进程退出可以是有意的,也可以是由于错误或异常情况而发生。以下是一些关于进程退出的相关信息:

  1. 正常退出:
    一个进程通常会正常退出,以完成其任务。正常退出通常是通过调用操作系统提供的退出函数(例如在Unix/Linux中是 exit(),在Windows中是 ExitProcess())来实现的。进程退出时,它会返回一个退出状态码(通常是整数),这个状态码可以被父进程或操作系统检索。

  2. 异常退出:
    有时进程可能会由于异常情况而退出,例如访问无效内存、除零错误、未捕获的异常等。这样的异常退出可能导致操作系统记录有关错误的信息,并且通常会返回一个非零的退出状态码,以指示出现问题。

  3. 父进程的处理:
    当子进程退出时,父进程通常可以采取一些行动。父进程可以调用 wait() 或 waitpid() 等函数来等待子进程的退出,以获取子进程的退出状态码,并释放子进程的资源。父进程还可以忽略子进程的退出,这通常用于后台进程或守护进程,它们不需要与子进程进行交互。

  4. 进程终止信号:
    在Unix/Linux系统中,进程可以通过发送信号来终止另一个进程,其中最常见的是 SIGTERM 和 SIGKILL。SIGTERM 允许进程进行清理工作并正常退出,而 SIGKILL 强制终止进程,不允许进程进行清理。

  5. 资源释放:
    进程退出时,它通常会释放已分配的资源,例如内存、文件描述符等。这确保了系统资源的有效管理。

进程的退出是操作系统管理的一个重要方面,它允许系统有效地调度和管理多个进程,并确保系统的稳定性和性能。进程退出状态码也可以用于传递信息给父进程,以便父进程知道子进程的执行状态。

正常退出

  • Main函数调用return
  • 进程调用exit(),标准c库
  • 进程调用_exit()或者_Exit(),属于系统调用

补充:

  • 进程最后一个线程返回
  • 最后一个线程调用pthread_exit

异常退出

  • 调用abort
  • 当进程收到某些信号时,如ctrl+C
  • 最后一个线程对取消(cancellation)请求做出响应

exit(), _exit(), _Exit()和 _EXIT

exit(), _exit(), 和 _EXIT 是与进程退出相关的三个不同的概念或函数,通常在C和C++编程中使用。它们之间的区别如下:

  1. exit() 函数:
    exit() 是C和C++标准库中提供的函数,用于正常终止一个进程。
    当调用 exit() 函数时,进程会执行一些清理工作,例如关闭文件、释放内存,然后终止。它还会返回一个整数值(退出状态码),用于表示进程的退出状态。通常,0 表示成功,非零值表示出现错误。
    通常,exit() 函数还会调用 atexit() 注册的函数,这些函数用于在进程退出时执行特定的清理操作。
  2. _exit() 函数:
    _exit() 是一个较低级别的系统调用函数,用于立即终止进程,而不执行标准的清理工作。
    调用 _exit() 不会执行 atexit() 注册的函数,也不会关闭已打开的文件描述符,不会刷新缓冲区等。进程会被立即终止,不会等待清理操作完成。
    _exit() 函数通常用于在子进程中进行异常退出或在多线程程序中,以避免引发死锁或资源泄漏问题。
  3. _Exit()函数
    _Exit() 是一个 C 标准库函数,通常位于 stdlib.h 头文件中,用于立即终止程序的执行。
    调用 _Exit() 会导致程序立即退出,不会运行任何后续的清理工作(例如 atexit 注册的清理函数)。它会立即关闭程序的进程,不进行正常的程序终止流程。
    _Exit() 通常用于在程序出现严重错误或需要立即停止执行时,以避免执行进一步的代码。
  4. _EXIT 宏:
    _EXIT 不是一个函数,而是一个宏,用于表示进程的退出状态码。通常,0 表示成功,非零值表示出现错误。
    _EXIT 常常与 exit() 或 _exit() 一起使用,以设置进程的退出状态码。
    _EXIT 是一个可能与特定操作系统或编译器相关的宏或变量。它通常不是标准的 C 或 C++ 函数。
    在某些上下文中, _EXIT 可能被用于表示程序退出的状态或码,通常是非零值表示错误,而零值表示成功。例如,程序可以在退出时返回 _EXIT 码来指示发生了错误。
    请注意, _Exit() 是一个标准函数,而 _EXIT 是一个可能在特定环境中定义的变量或宏。如果您要退出程序并指定退出状态码,通常建议使用 exit(status) 函数,其中 status 是表示退出状态的整数值。这是 C 标准库中的标准函数,适用于大多数情况。

status 状态码
exit()等于是对-exit()和_Exit()进行封装

SYNOPSIS
       #include <stdlib.h>

       void exit(int status);
SYNOPSIS
       #include <unistd.h>

       void _exit(int status);

       #include <stdlib.h>

       void _Exit(int status);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       _Exit():
           _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖喱年糕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值