一、创建进程 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;
}
调试
- 打印当前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;
}
函数调用成功返回两次
- 父进程和子进程
#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
后 打印了两次
- 父进程先运行打印了一遍
- 子进程再运行进行了打印
只是返回值不同进行了不同的打印
- 调试判断返回值
#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;
}
前面是注释前的结果后面是注释后的结果
- 调试判断父进程和子进程的关系
#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
创建子进程的主要目的:
-
并行执行:
fork
允许在多核或多处理器系统上并行执行多个任务。父进程和子进程可以同时运行,以提高程序的性能。 -
任务隔离:
每个进程都有自己独立的内存空间和执行上下文,因此fork
可用于隔离不同任务或功能,以防止它们相互干扰。这在操作系统中是实现多任务处理的基础。 -
创建后台服务:
通过fork
,可以创建后台服务进程,以便在不中断用户界面的情况下执行后台任务,例如网络服务器或守护进程。 -
多进程通信:
父进程和子进程可以通过进程间通信(IPC)机制(如管道、共享内存、消息队列等)进行数据交换和协作。 -
容错性:
在某些情况下,fork
可用于实现容错机制。当一个进程崩溃时,可以通过fork
创建一个新的进程,以继续执行相同的任务。 -
资源分配:
fork
允许将系统资源(如文件描述符、内存等)复制到新进程,以便新进程可以继续使用这些资源,而不会干扰父进程。
需要注意的是,
fork
创建的子进程是父进程的副本,但有一些区别,例如它们有不同的进程ID(PID),并且通常具有不同的执行上下文。父进程和子进程之间的主要区别通常在于它们的返回值,父进程接收到子进程的PID,而子进程接收到0。这使得程序可以根据返回值来执行不同的代码分支,以区分父子进程的行为。
四、vfork()函数 也可以创建进程与fork的区别
vfork
和 fork
的区别
都是在Unix-like操作系统中用于创建子进程的系统调用,但它们之间有一些重要的区别。这些区别主要涉及到进程的创建方式、父子进程之间的关系和执行时的行为。
- 进程创建方式:
fork
创建子进程时,会复制父进程的整个地址空间(包括数据、堆、栈等),并且子进程与父进程是完全独立的,它们各自拥有自己的内存副本。vfork
(“v"代表"virtual”,虚拟)创建子进程时,不会立即复制整个地址空间,而是共享父进程的地址空间。子进程在启动时与父进程共享相同的内存,直到它调用exec
或exit
为止。
- 父子进程关系:
- 在使用
fork
创建子进程后,父子进程是完全独立的,它们可以同时运行,但它们之间的内存是分开的。父子进程之间没有共享内存,因此需要通过进程间通信(IPC)机制来进行数据交换。- 在使用
vfork
创建子进程后,父子进程共享同一地址空间,这意味着它们共享相同的内存,包括变量和数据。父进程在子进程调用exec
或exit
之前会挂起,直到子进程执行完毕。
- 执行时的行为:
- 由于
vfork
子进程与父进程共享内存,因此子进程必须非常小心地操作内存,以避免破坏父进程的数据。通常,vfork
主要用于创建子进程,然后子进程立即通过exec
来执行新的程序,以避免操纵共享内存。
fork
创建的子进程是独立的,可以自由操作其内存空间。总结:
vfork
和fork
都用于创建子进程,但它们的行为和用途有重要区别。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函数
定义
进程退出是指一个正在运行的进程结束其执行并释放其资源,将控制权返回给操作系统。进程退出可以是有意的,也可以是由于错误或异常情况而发生。以下是一些关于进程退出的相关信息:
-
正常退出:
一个进程通常会正常退出,以完成其任务。正常退出通常是通过调用操作系统提供的退出函数(例如在Unix/Linux中是exit
(),在Windows中是 ExitProcess())来实现的。进程退出时,它会返回一个退出状态码(通常是整数),这个状态码可以被父进程或操作系统检索。 -
异常退出:
有时进程可能会由于异常情况而退出,例如访问无效内存、除零错误、未捕获的异常等。这样的异常退出可能导致操作系统记录有关错误的信息,并且通常会返回一个非零的退出状态码,以指示出现问题。 -
父进程的处理:
当子进程退出时,父进程通常可以采取一些行动。父进程可以调用 wait() 或 waitpid() 等函数来等待子进程的退出,以获取子进程的退出状态码,并释放子进程的资源。父进程还可以忽略子进程的退出,这通常用于后台进程或守护进程,它们不需要与子进程进行交互。 -
进程终止信号:
在Unix/Linux系统中,进程可以通过发送信号来终止另一个进程,其中最常见的是 SIGTERM 和 SIGKILL。SIGTERM 允许进程进行清理工作并正常退出,而 SIGKILL 强制终止进程,不允许进程进行清理。 -
资源释放:
进程退出时,它通常会释放已分配的资源,例如内存、文件描述符等。这确保了系统资源的有效管理。
进程的退出是操作系统管理的一个重要方面,它允许系统有效地调度和管理多个进程,并确保系统的稳定性和性能。进程退出状态码也可以用于传递信息给父进程,以便父进程知道子进程的执行状态。
正常退出
- Main函数调用
return
- 进程调用
exit
(),标准c库- 进程调用
_exit
()或者_Exit
(),属于系统调用补充:
- 进程最后一个线程返回
- 最后一个线程调用
pthread_exit
异常退出
- 调用
abort
- 当进程收到某些信号时,如
ctrl+C
- 最后一个线程对取消(
cancellation
)请求做出响应
exit(), _exit(), _Exit()和 _EXIT
exit(), _exit(), 和 _EXIT 是与进程退出相关的三个不同的概念或函数,通常在C和C++编程中使用。它们之间的区别如下:
- exit() 函数:
exit() 是C和C++标准库中提供的函数,用于正常终止一个进程。
当调用 exit() 函数时,进程会执行一些清理工作,例如关闭文件、释放内存,然后终止。它还会返回一个整数值(退出状态码),用于表示进程的退出状态。通常,0 表示成功,非零值表示出现错误。
通常,exit() 函数还会调用 atexit() 注册的函数,这些函数用于在进程退出时执行特定的清理操作。 - _exit() 函数:
_exit() 是一个较低级别的系统调用函数,用于立即终止进程,而不执行标准的清理工作。
调用 _exit() 不会执行 atexit() 注册的函数,也不会关闭已打开的文件描述符,不会刷新缓冲区等。进程会被立即终止,不会等待清理操作完成。
_exit() 函数通常用于在子进程中进行异常退出或在多线程程序中,以避免引发死锁或资源泄漏问题。 - _Exit()函数
_Exit() 是一个 C 标准库函数,通常位于 stdlib.h 头文件中,用于立即终止程序的执行。
调用 _Exit() 会导致程序立即退出,不会运行任何后续的清理工作(例如 atexit 注册的清理函数)。它会立即关闭程序的进程,不进行正常的程序终止流程。
_Exit() 通常用于在程序出现严重错误或需要立即停止执行时,以避免执行进一步的代码。 - _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