【Linux】进程的生命之旅——诞生、消逝与守候(fork/exit/wait)

🎬 个人主页:谁在夜里看海.****

📖 个人专栏:《C++系列》** 《Linux系列》《算法系列》**

⛰️ 一念既出,万山无阻


目录

📖一、进程创建

1.fork函数

📚高层封装特性

📚fork返回值

2.写时拷贝

3.调用失败

📚资源耗尽

📚进程数限制

📚内核限制

📖二、进程终止

1.退出场景

2.status退出码

3.退出方法

📚exit函数

📚_exit函数

📚main函数返回

📖三、进程等待

1.wait方法

📚语法

📚总结

2.waitpid方法

📚语法

📚总结


📖一、进程创建

1.fork函数

操作系统中进程的创建通常是通过系统调用实现的,在Linux中是通过fork(),它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

cpp 复制代码
#include<unistd.h> // 使用需包含头文件unistd.h
pid_t pid = fork(); // 在子进程中返回0,父进程中返回子进程pid,出错返回-1

fork是操作系统提供的一种高层封装,它抽象了进程创建的复杂过程,fork将底层的一系列操作封装在一个简单的系统调用中,屏蔽了许多复杂的细节:

📚****高层封装特性

① 简化进程创建的步骤:

fork的调用接口非常简洁,只需要调用一次,系统会自动创建一个子进程并返回。父进程和子进程共享相同的代码,子进程可以继续从父进程的当前执行点运行。

② 屏蔽底层细节:

底层需要分配新的内存空间、复制父进程的状态、初始化子进程的资源,fork函数将这些细节全部封装起来。

③ 依赖操作系统:

fork的具体实现以来于操作系统内核,它负责管理进程表等关键数据结构,系统调用fork,当控制转移到内核中的fork代码后,内核做:

**1.**分配新的内存块和内核数据结构给子进程

**2.**将父进程部分数据结构内容拷贝给子进程

**3.**添加子进程到系统进程列表当中

**4.**fork返回,开始调度器调dan'ddandan

当一个进程调用fork之后就会有两个二进制代码相同的进程,并且都能运行到相同的地方,各自开始往下走:

cpp 复制代码
int main() {
    printf("Before: pid is %d, ppid is %d\n",getpid(),getppid());
    fork();
    printf("After: pid is %d, ppid is %d\n",getpid(),getppid());
    return 0;
}

这里为什么只有三行输出,子进程共享父进程的代码,并各自独立执行,应该是打印两次Before才对。分析打印结果,10226应该是父进程,确实打印了Before,而子进程10227没有打印Before,说明子进程并没有执行Before的代码,正如上面所说的,子进程继续从父进程的当前执行点运行,也就是从fork代码处往下执行,而fork之前的不会被执行:

所以,在一个进程调用fork之前,该进程单独执行,调用fork之后,父子两个进程执行流各自执行

📚fork返回值

在子进程中,fork返回0;

在父进程中,frok返回子进程pid。

❓这里提出一个问题,在父进程中fork返回值没有异议,因为fork函数是父进程调用的,自然会有返回值,但是在子进程中fork也有返回值,那么是不是子进程也调用了fork函数呢? 这不肯定,因为一个进程调用fork函数之后会创建出它的子进程,而子进程再调用fork函数再创建。。。这显然不对,所以子进程并没有调用fork函数,但是为什么会有fork函数的返回值呢?

✅上面说到,子进程是从父进程的执行点开始往下执行的,所以对上述问题合理的解释是:父进程创建子进程时的执行点在fork函数调用之后,返回之前,所以子进程往下执行也会有返回值产生

2.写时拷贝

父子进程的代码是共享的,所以它们往后执行相同的操作,那么它们的数据也是共享的吗?的确,在没有进行写入时,父子进程的数据也是共享的,只有当一方尝试对共享数据进行写入时,系统才会拷贝一份数据用于写入,这样既确保了资源的高效利用,又保证了父子进程间的独立性 。

3.调用失败

fork()调用失败通常于系统资源、权限、或操作系统限制有关,下面是常见的原因:

📚资源耗尽

当系统的资源不足时(如内存或进程表项不足),fork()会失败:

内存不足: 操作系统需要为每个新进程分配内存,如果系统内存耗尽,fork() 就会失败。

进程表已满: 每个进程都有一个进程控制块(PCB),操作系统维护一个进程表。如果系统中运行的进程数量已经达到限制,无法再为新进程分配进程控制块时,fork() 会失败。

堆栈空间不足 :如果子进程的堆栈空间无法分配(尤其在某些嵌入式或资源受限的环境中),fork() 也会失败。

📚进程数限制

大多数操作系统对一个用户或系统总共能创建的进程数有限制。若当前用户或系统已经达到了此限制,调用 fork() 时就会失败。

可以通过 ulimit -u 查看单个用户的最大进程数:

📚内核限制

内核的资源,如文件描述符和信号等,也可能导致 fork() 失败。例如,如果父进程持有太多打开的文件句柄,可能会达到系统文件描述符的限制。


📖二、进程终止

1.退出场景

进城退出场景无非下面三种:

①:代码运行完毕,结果正确

②:代码运行完毕,结果不正确

③:代码异常终止(没有运行完)

第一种情况自然是最好的,但是如果是另外两种情况,我们就需要进行额外处理,但是我们怎么才能知道进程退出是哪种情况呢(什么时候需要处理,什么时候不需要呢)?

这个时候就需要进程退出时,做一些标记(返回退出码),告知操作系统或程序员具体的退出情况

2.status退出码

status状态码用于表示进程的退出状态,提供了进程执行结果的信息,状态码遵循以下约定:

  • 0:表示命令成功执行,没有错误发生。
  • 非0:表示命令执行失败。具体的非0值表示不同类型的错误,具体含义通常与执行的程序或命令相关。例如:
    • 1:一般性错误。
    • 2:命令语法错误。
    • 126:命令不能执行(权限问题)。
    • 127:命令未找到。
    • 128:命令因信号导致终止(例如,程序被 kill 命令中断)。
    • 130:程序因接收到 Ctrl+C(SIGINT)信号而退出。

status通常被定义成整形,但是并不能当作一般的整形看待,而是要看作成位图:

我们有一个 32 位的 status,其中高8位用于表示退出状态,低8位用于表示因信号退出的原因。

  1. 高8位(退出状态)可以有 256 种可能的退出码:

    0:正常退出。

    1127:表示不同的错误。

    128255:表示因信号终止,计算方式为 128 + 信号编号

  2. 低8位(信号终止标志):

    如果进程是由于信号终止的,那么低8位会记录相应的信号编号(例如,SIGKILL 对应 9,SIGSEGV 对应 11)。

    如果进程不是由信号终止的,低8位通常为 0。

3.退出方法

进程退出的常见方法有:exit(),_exit()以及main()函数返回,下面依次进行介绍:

📚exit函数

exit(int status):这是进程正常终止的一种方式。调用exit()后,进程会清理其资源(文件描述符、内存等),并将状态码status返回给操作系统。当返回0时表示成功退出,返回非0表示出现错误。

在多线程程序中,exit() 会终止当前进程以及所有线程。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("This process will exit normally.\n");
    exit(0);  // 正常退出,状态码为0
}
📚_exit函数

_exit(int status):这个函数与 exit() 很相似,但它不会执行标准库的清理操作(如缓冲区刷新等),直接终止进程。来看下面这段代码:

cpp 复制代码
int main()
{
  printf("this is a process, pid is %d, ppid is %d",getpid(),getppid());
  exit(0);
}

调用exit时,正常打印;

cpp 复制代码
int main()
{
  printf("this is a process, pid is %d, ppid is %d",getpid(),getppid());
  _exit(0);
}

❓调用_exit时,没有正常打印,这是为什么呢?

✅printf输出时如果没有加上\n,此时输出的内容会存在标准输出缓冲区 中,并不会立刻显示在终端,而调用_exit函数时,由于它不会执行标准库的清理操作,所以缓冲区的内容就不会显示在终端

exit函数最后其实会调用_exit函数,只不过在调用之前,多做了如清理缓冲区的操作:

📚main函数返回

return :在 main 函数中使用时,程序会结束并返回指定的退出状态码(通常为 0 表示成功,非 0 表示错误)。return 结束当前函数的执行,但如果在 main 函数中调用,它会导致程序退出。

return返回和exit调用的效果是一样的,其实他们本质上是等价的:return 0 等价于 exit(0)

只不过在main函数中用return返回作为程序终止的标志更符合函数的语义,可读性更强。


📖三、进程等待

之前的博客讲过,子进程退出,如果父进程不做任何处理,就会引发内存泄露(进程表等信息不会被清理),产生僵尸进程。 博客链接在此:详解僵尸进程于孤儿进程

那么避免僵尸进程的办法就是**进程等待,**父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

1.wait方法

wait()是一个比较简化的系统调用,用于让父进程等待任意一个子进程的终止。wait()函数会阻塞父进程,直到有子进程终止,并且返回一个子进程的PID。

📚语法
cpp 复制代码
#include <sys/wait.h>
pid_t wait(int *status);

status:用于返回子进程的退出状态。

返回值:如果调用成功,返回子进程的PID;如果没有则返回-1。

📚总结
  1. 父进程调用wait()时会阻塞,直到有子进程结束并回收它的状态;

  2. 如果有多个子进程退出,wait()返回任意一个子进程的PID;

  3. 如果没有子进程,wait()会返回-1。

2.waitpid方法

waitpid()wait() 的更为灵活和可控制的版本,允许父进程等待特定的子进程结束,或者通过指定参数进行更精细的控制。

📚语法
cpp 复制代码
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
  • pid:指定需要等待的子进程的 PID。可以取以下几种值:
    • pid > 0:等待指定 PID 的子进程。
    • pid == -1:等待同组进程中的任意子进程。
  • status:与 wait() 相同,保存子进程的退出状态。
  • options:控制行为的标志,常用的选项有:
    • WNOHANG:非阻塞模式,如果没有子进程退出,立即返回,而不是阻塞。
    • WUNTRACED:如果子进程已经停止(但没有退出),也返回。
  • 返回值:
    • 返回子进程的 PID,如果没有子进程或者发生错误,返回 -1
    • 如果 status 中的退出状态有特殊状态(如退出信号),需要使用宏来解析。
cpp 复制代码
int main()
{
  pid_t pid = fork();
  if(pid == 0)
  {
    // 子进程
    printf("this is child process,pid is %d,ppid is %d\n",getpid(),getppid());
    exit(20);
  }
  // 父进程
  printf("this is father process,pid is %d\n",getpid());
  int status;
  pid_t child_pid = waitpid(-1,&status,WNOHANG);
  printf("child process has exited,code is %d,pid is %d\n",WEXITSTATUS(status),child_pid);
  exit(0);
}

父进程调用 wait()waitpid() 时,它会传递一个指向 status 变量的指针,用于写入子进程的退出状态,所以我们需要在外部定义一个status变量,并通过取地址的方式传入函数内部。

❓定义成其他变量名可以吗:完全可以!

✅变量名只是内存的一个标识符,是用户自定义的,wait()waitpid() 只关心的是传递给它的地址,而不是变量的名字,只不过定义成status这样代码更加易读。

❓定义成其他类型可以吗:不可以!

status 参数必须是一个指向 int 类型的指针 。如果传递其他类型(例如 float*char*),程序可能会产生编译错误,这是因为 wait()waitpid() 会在 status 指向的内存中写入整数值,用来存储子进程的退出状态。如果指针指向的类型不匹配,内存解释将出错。

上述代码中由于waitpid内部设置为WNOHANG模式,没有子进程返回时直接退出,不阻塞:

需要sleep(1)等待子进程退出后,waitpid才能接收到退出信息:

cpp 复制代码
  // 父进程
  printf("this is father process,pid is %d\n",getpid());
  int status;
  sleep(1);
  pid_t child_pid = waitpid(-1,&status,WNOHANG);
  printf("child process has exited,code is %d,pid is %d\n",WEXITSTATUS(status),child_pid);
  exit(0);

其中WEXITSTATUS是一个宏函数,用于解码退出状态,因为上面讲过,32位status的高8位存储退出状态,所以不能直接引用status查看,而要用一个宏函数进行解码。

📚总结

|------------|-------------------|-------------------------|
| 特性 | wait() | waitpid() |
| 等待目标 | 等待任意子进程的结束 | 可以指定特定的子进程(通过 pid 参数) |
| 阻塞与非阻塞 | 总是阻塞,直到至少有一个子进程结束 | 可以通过 WNOHANG 使其非阻塞 |
| 灵活性 | 较少灵活性,只能等待任何一个子进程 | 更灵活,可以等待指定的子进程或进程组 |
| 选项 | 没有额外选项 | 支持更多控制选项,如 WNOHANG |
| 返回值 | 返回一个子进程的 PID | 返回指定子进程的 PID,或者 -1 错误 |
| 错误处理 | 如果没有子进程,返回 -1 | 如果没有子进程,返回 -1 |


以上就是【进程的生命之旅------诞生、消逝与守候】的全部内容,欢迎指正~

码文不易,还请多多关注支持,这是我持续创作的最大动力!

相关推荐
土豆炒马铃薯。15 分钟前
【深度学习】Pytorch 1.x 安装命令
linux·人工智能·pytorch·深度学习·ubuntu·centos
与君共勉1213817 分钟前
Jenkins-Gitlab 前端项目自动化部署
linux·服务器·git·gitlab·jenkins
9毫米的幻想33 分钟前
【Linux系统】—— 基本指令(三)
linux·c语言·c++·学习
可涵不会debug34 分钟前
【Linux | 计网】TCP协议详解:从定义到连接管理机制
linux·服务器·网络·tcp/ip
沿着缘溪奔向大海35 分钟前
Kali Linux语言设置成中文
linux·运维·服务器
时光の尘1 小时前
C语言菜鸟入门·关键字·union的用法
运维·服务器·c语言·开发语言·c·printf
davenian1 小时前
<OS 有关> ubuntu 24 安装 VMware Workstaion
linux·ubuntu·vmware
2201_761199041 小时前
nginx动静分离和rewrite重写和https和keepalived
运维·nginx·https
ubuntu18041 小时前
C0034.在Ubuntu中安装的Qt路径
linux·qt·ubuntu
萧鼎1 小时前
轻松解析 PDF 文档:深入了解 Python 的 pdfplumber 库
服务器·python·pdf