目录

1.printf()缓冲区问题

1.基于exit()退出

2.基于_exit()退出

3.exit()与_exit()对比

2.主函数的参数

3.环境变量

关键补充:

4.fork

1.进程和程序的区别     

2.fork()复制进程

3.fork()的语法

4.孤儿进程和僵死进程

5.进程的三大基本状态

6.C/C++ 中的 wait()


1.printf()缓冲区问题

1.基于exit()退出
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
        printf("hello");
        sleep(3);//休息三秒

        exit(0);//结束进程,退出
}

运行结果:等待三秒,输出hello,退出进程

printf("abc")含义

  1. 将abc存放在缓冲区中,等待进程结束,输出缓冲区内容
  2. 输出方式:缓冲区->内核->硬件设备:屏幕

缓冲区输出条件:

  1. 缓冲区满了
  2. 缓冲区被强制刷新了(123后面\n就刷新缓冲区了)
  3. 进程结束时
2.基于_exit()退出
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
        printf("hello");
        sleep(3);//休息三秒

        _exit(0);//结束进程,退出(不会刷新缓冲区,直接通知内核退出)
}

运行结果:等待三秒结束进程,不会输出任何内容

3.exit()与_exit()对比
exit()   <stdib.h>  C标准库函数
_exit       <unistd.h>    Unix系统调用

exit()终止逻辑:先执行用户态的资源清理操作,再调用内核实现终止逻辑

  1. 刷新并关闭标准I/O流(强制刷新缓冲区的数据(加\n)时,缓冲区内容会被输出到终端或文件中)
  2. 清理私有资源,释放进程占用的用户态内存(堆内存,全局变量内存)
  3. 调用_exit()触发内核回收

_exit()终止逻辑:它是系统调用,会跳过用户态的所有操作,直接通知内核终止进程(不会刷新缓冲区,因为刷新缓冲区是用户态的操作)

exit,_exit应用选择:

普通单进程程序:优先使用exit()

多进程:例如父子进程,子进程,优先使用_exit(),子进程用exit()会对缓冲区进行处理,影响牵连父进程


fflush(stdout):强制把缓冲区里的打印内容,立刻输出到屏幕上

C 语言的 printf 不是马上打印,而是:

  1. 先把内容放到输出缓冲区
  2. 缓冲区满了 / 遇到换行 \n / 程序结束,才会一次性刷到屏幕

比如多个线程同时 printf,内容可能卡在缓冲区,导致:

  • 打印乱序
  • 内容延迟显示
  • 甚至混在一起输出

总结:fflush(stdout);

  • 强制刷新输出缓冲区
  • 保证打印内容立即显示在屏幕
  • 多线程 printf 时,让输出更清晰、不乱码、不混乱

2.主函数的参数

int main(int argc,char* argv[],char* envp[])
  • argc:主函数参数个数
  • argv:主函数参数内容
  • envp:环境变量

输出主函数的内容:

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

int main(int argc,char* argv[],char* envp[])//主函数参数个数,主函数参数内容,环
//境变量
{
        for(int i=0;i<argc;i++)
        {
                printf("argv[%d]=%s\n",i,argv[i]);
        }
        for(int i=0;envp[i]!=NULL;i++)
        {
                printf("envp[%d]=%s\n",i,envp[i]);
        }
        sleep(3);//休息三秒

        exit(0);//结束进程,退出
}

运行结果:

argv[0]=./2
envp[0]=SHELL=/bin/bash
envp[1]=SESSION_MANAGER=local/wangjiayi-virtual-machine:@/tmp/.ICE-unix/1736,unix/wangjiayi-virtual-machine:/tmp/.ICE-unix/1736
envp[2]=QT_ACCESSIBILITY=1
envp[3]=COLORTERM=truecolor
envp[4]=XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg
envp[5]=SSH_AGENT_LAUNCHER=gnome-keyring
envp[6]=XDG_MENU_PREFIX=gnome-
envp[7]=GNOME_DESKTOP_SESSION_ID=this-is-deprecated
envp[8]=LANGUAGE=zh_CN:en
envp[9]=LC_ADDRESS=zh_CN.UTF-8

3.环境变量

1.定义:环境变量是操作系统或程序运行时使用的动态命名值,用于存储系统路径、用户配置、临时数据等信息。通过export命令可将变量提升为环境变量,使其对当前Shell会话及其子进程可见。

2.创建环境变量:

  • MYSTR="AEDRFTG"(定义了一个名为MYSTR的环境变量,其值为字符串“AEDRFTG”)
  • export MYSTR(将MYSTR导出为环境变量,使得该变量可以在当前的shell子进程中被访问到)

3.export:

1.定义:export 用于将当前 Shell 进程中的变量标记为「环境变量」,使其能够被当前 Shell 的所有子进程(包括子 Shell、脚本、外部命令)继承和访问;未被 export 标记的变量仅为「Shell 本地变量」,仅在当前 Shell 进程内有效。

关键补充:
  • 环境变量:属于进程的属性,会随进程创建子进程时被复制到子进程的环境中;
  • 本地变量:仅存于当前 Shell 的内存空间,子进程无法感知。

2.作用:

  • 全局可见性:将普通Shell变量转为环境变量,使子进程(如脚本、程序)能够继承该变量。
  • 跨进程通信:允许不同程序共享配置,例如PATH变量指定可执行文件搜索路径。
  • 临时配置:在终端会话中动态修改环境,无需永久写入配置文件。

4.fork

1.进程和程序的区别     

        进程:是动态的执行过程,占用CPU,内存资源

        程序:是静态的代码和数据,是待执行的指令集合,不占用CPU,内存资源

2.fork()复制进程

父进程和子进程的进程号不一样,内容一样,子进程会继承父进程缓冲区里的内容

3.fork()的语法
  1. 有头文件<unistd.h>     
  2. 创建子进程:pid_t fork(void)    pid_t 是进程ID的整数类型

返回值:有三种不同的返回值

  1. 在父进程中,返回子进程的pid,用于父进程识别和管理子进程
  2. 子进程中,返回0,用于子进程确认自身身份
  3. 出错时,返回-1(比如内存不足,系统进程数达到上限,设置error表示错误原因(perror查看出错原因))
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
	int n=0;
	char*s=NULL;

	pid_t a=fork();//复制出来子进程
	if(a==-1)
		exit(1);
	if(a==0)//满足条件,子进程进入
	{
		s="son";
		n=3;
	}
	else////满足条件,父进程进入
	{
		s="father";
		n=7;
	}
	for(int i=0;i<n;i++)//打印结果
	{
		printf("s=%s\n",s);
		sleep(1);
	}
}

运行结果:

s=father
s=son
s=father
s=son
s=father
s=son
s=father
s=father
s=father
s=father

getpid():获取当前进程的ID号,getppid():获取当前进程的父进程的ID号

printf("pid=%d,ppid=%d\n",getpid(),getppid());

打印结果:

s=father
pid=4676,ppid=2494
s=son
pid=4677,ppid=4676
s=father
pid=4676,ppid=2494
s=son
pid=4677,ppid=4676
s=father
pid=4676,ppid=2494
s=son
pid=4677,ppid=4676
s=father
pid=4676,ppid=2494
s=father
pid=4676,ppid=2494
s=father
pid=4676,ppid=2494
s=father
pid=4676,ppid=2494

如果需要查看程序运行时的进程状态,可以在程序运行期间使用另一个终端窗口执行 ps 命令。或者在当前终端中让程序在后台运行,再查看进程状态。

后台运行进程并查看

将进程 ./1 放入后台运行,使用以下命令:

./1 &
 

查看进程状态

使用 ps 命令查看当前运行的进程:

ps
 
pid=4691,ppid=2494
s=father
pid=4691,ppid=2494
ps
    PID TTY          TIME CMD
   2494 pts/0    00:00:00 bash
   4691 pts/0    00:00:00 1
   4692 pts/0    00:00:00 1 <defunct>//表示子进程已经运行完了,子进程僵死
   4695 pts/0    00:00:00 ps

思考1:结果会打印几个A?

int main()
{
        fork()||fork();
        printf("A\n");

        exit(0);
}

表达式fork()||fork()的执行过程:

  1. 执行第一个fork(),创建进程P1(假设原始进程为P0)

    • P0中返回子进程PID,逻辑或判断为真,不再执行第二个fork()
    • P1中返回0,需要继续执行第二个fork()
  2. P1执行第二个fork(),创建进程P2

    • P1中返回子进程PID,逻辑或判断为真
    • P2中返回0

最终产生三个进程:P0、P1和P2,每个进程都会执行printf("A\n")

这种写法利用了逻辑运算符的短路特性实现特定fork模式


思考2:结果会打印几个A?

int main()
{
        fork()||fork();
        printf("A");

        exit(0);
}

结果还是AAA


思考3:结果会打印几个A?

int main()
{
        printf("A");
        fork()||fork();
        exit(0);
}

结果还是AAA

1. printf ("A") 的缓冲区行为

  • 默认情况下,stdout(标准输出)是行缓冲:只有遇到\n、缓冲区满、调用fflush(stdout)或进程退出时,才会把缓冲区内容刷到终端。
  • 这里printf("A")没有\n,所以 "A" 只是被存入缓冲区,此时终端还看不到任何输出

2. fork ()||fork () 的执行逻辑(重点)

||是逻辑或,遵循短路原则:左边表达式为真(非 0)时,右边表达式不会执行。

  • 初始进程(父进程 P0)
    1. 执行第一个fork():创建子进程 P1。
      • P0(父):fork () 返回 P1 的 PID(非 0,真),触发||短路,第二个 fork () 不执行
      • P1(子):fork () 返回 0(假),需要执行第二个fork(),创建子进程 P2。
  • 最终进程数:P0、P1、P2,共 3 个进程。

3. 缓冲区复制与输出

每个 fork () 会完整复制父进程的地址空间,包括未刷新的 stdout 缓冲区(里面有 "A")。当每个进程执行exit(0)时,会触发缓冲区刷新:

  • P0 退出:刷新缓冲区,输出 "A";
  • P1 退出:刷新自己复制的缓冲区,输出 "A";
  • P2 退出:刷新自己复制的缓冲区,输出 "A";最终终端会输出 AAA

思考4:结果会打印几个A?

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
	for(int i=0;i<2;i++)
	{
		fork();
		printf("A\n");
	}
}

A

A

A

A

A

A

注意:子进程复制的是父进程的「完整执行状态」,包括程序计数器(PC),所以它会从 fork() 调用之后的指令开始执行,而不是从 main() 或循环开头重新跑一遍。

4.孤儿进程和僵死进程

孤儿进程(Orphan Process)

孤儿进程是指父进程先于子进程终止,导致子进程失去父进程。这类进程会被系统的 init 进程(PID 为 1 的进程)接管,init 进程会定期调用 wait() 清理孤儿进程的资源,避免资源泄漏。

父进程先于子进程结束,子进程变成孤儿进程,但系统会给子进程重新分配新的父进程,因为目的是确保子进程不会成为僵死进程,导致内存占用

特点

  • 父进程终止后,子进程被 init 进程收养。
  • 不会造成资源泄漏,因为 init 进程会负责回收。
  • 常见于后台服务或守护进程(daemon)的实现中,通过主动让父进程退出,使子进程成为孤儿进程并脱离终端控制。

僵死进程(Zombie Process)

僵死进程是指子进程终止后,父进程未调用 wait() 或 waitpid() 回收其退出状态,导致子进程的进程描述符仍保留在系统中。这类进程不占用内存,但会占用进程表项,可能导致系统无法创建新进程。

子进程先于父进程结束,父进程由于没有获得子进程的退出码,子进程变成僵死进程

特点

  • 子进程已终止,但父进程未回收其资源(进程号还在)。
  • 进程状态为 Z(Zombie)。
  • 长期存在的僵死进程会浪费系统资源(如 PID)。

注意:

1.父进程结束 ≠ 子进程 PID 消失,子进程会继续运行,而僵尸进程的产生和「父进程是否结束」无关,和「父进程是否回收子进程的退出状态」有关。

2. PID 是系统级的唯一标识,和父进程无关

  • 每个进程被创建时,操作系统会分配一个唯一的 PID(属于系统资源,不是父进程的私有资源);
  • 父进程只是「创建者」,但无权销毁子进程的 PID,即使父进程退出,子进程只要还在运行,PID 就会一直存在;
  • 只有子进程自己退出,且操作系统回收了它的所有资源,PID 才会被释放。

3. 僵尸进程的本质:「退出状态未被回收」

进程的完整生命周期分为两步:

  1. 执行阶段:进程运行代码,占用 CPU、内存等资源;
  2. 退出阶段:进程执行exit()后,会释放大部分资源(内存、文件句柄等),但会保留「最小信息」(PID、退出状态、运行时间等),等待父进程通过wait()/waitpid()调用回收;
    • 如果父进程一直不调用 wait (),这些「最小信息」就会一直占用系统资源,进程变成僵尸进程(Z 状态);
    • 只有父进程调用wait(),或父进程退出,这些信息才会被回收。

总结:

  1. PID 独立性:子进程的 PID 由系统分配,父进程退出不会导致子进程 PID 消失,子进程会继续运行;
  2. 僵尸进程成因:子进程退出后,父进程未调用wait()回收其退出状态,而非「父进程先退出」;
  3. 孤儿进程的处理:父进程先退出,子进程被 init 接管,init 会自动调用wait()清理,因此孤儿进程不会变成僵尸进程。

简单说:僵尸进程的核心是「父进程不回收」,而孤儿进程的父进程没了,系统会派 init 来「兜底回收」,这就是两者的关键区别。

5.进程的三大基本状态

就绪态:进程已具备运行条件,等待被操作系统调度分配CPU资源。此时进程所需的其他资源(如内存、I/O设备)已准备就绪,只需获得CPU时间片即可执行。

运行态:进程正在CPU上执行指令。同一时刻,单核CPU只能有一个进程处于运行态,多核CPU可并行运行多个进程。

阻塞态:进程因等待某些事件(如I/O操作完成、信号量释放)而暂停执行,即使分配CPU也无法运行。当事件满足后,进程会重新进入就绪态等待调度。

6.C/C++ 中的 wait()

在 C/C++ 中,wait() 通常用于进程控制:

  • 等待子进程终止wait() 系统调用会暂停父进程,直到子进程结束。
  • 阻塞当前进程,将阻塞后的退出码的值传给参数的地址中,回收子进程资源,避免僵死进程,同时获取退出状态(1,2,3)

s=son
pid=5371,ppid=5370
s=son
pid=5371,ppid=5370
s=son
pid=5371,ppid=5370
val=256   //阻塞后的退出码 0001 0000 0000
s=father
pid=5370,ppid=2494
s=father
pid=5370,ppid=2494
s=father
pid=5370,ppid=2494
s=father
pid=5370,ppid=2494
s=father
pid=5370,ppid=2494
s=father
pid=5370,ppid=2494
s=father
pid=5370,ppid=2494

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐