Linux—系统编程基础(fork / 缓冲区 / 环境变量 / 进程状态)
目录
1.printf()缓冲区问题
1.基于exit()退出
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello");
sleep(3);//休息三秒
exit(0);//结束进程,退出
}
运行结果:等待三秒,输出hello,退出进程
printf("abc")含义
- 将abc存放在缓冲区中,等待进程结束,输出缓冲区内容
- 输出方式:缓冲区->内核->硬件设备:屏幕
缓冲区输出条件:
- 缓冲区满了
- 缓冲区被强制刷新了(123后面\n就刷新缓冲区了)
- 进程结束时
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()终止逻辑:先执行用户态的资源清理操作,再调用内核实现终止逻辑
- 刷新并关闭标准I/O流(强制刷新缓冲区的数据(加\n)时,缓冲区内容会被输出到终端或文件中)
- 清理私有资源,释放进程占用的用户态内存(堆内存,全局变量内存)
- 调用_exit()触发内核回收
_exit()终止逻辑:它是系统调用,会跳过用户态的所有操作,直接通知内核终止进程(不会刷新缓冲区,因为刷新缓冲区是用户态的操作)
exit,_exit应用选择:
普通单进程程序:优先使用exit()
多进程:例如父子进程,子进程,优先使用_exit(),子进程用exit()会对缓冲区进行处理,影响牵连父进程
fflush(stdout):强制把缓冲区里的打印内容,立刻输出到屏幕上
C 语言的 printf 不是马上打印,而是:
- 先把内容放到输出缓冲区
- 缓冲区满了 / 遇到换行
\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()的语法
- 有头文件<unistd.h>
- 创建子进程:pid_t fork(void) pid_t 是进程ID的整数类型
返回值:有三种不同的返回值
- 在父进程中,返回子进程的pid,用于父进程识别和管理子进程
- 子进程中,返回0,用于子进程确认自身身份
- 出错时,返回-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()的执行过程:
-
执行第一个
fork(),创建进程P1(假设原始进程为P0)- P0中返回子进程PID,逻辑或判断为真,不再执行第二个
fork() - P1中返回0,需要继续执行第二个
fork()
- P0中返回子进程PID,逻辑或判断为真,不再执行第二个
-
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):
- 执行第一个
fork():创建子进程 P1。- P0(父):fork () 返回 P1 的 PID(非 0,真),触发
||短路,第二个 fork () 不执行。 - P1(子):fork () 返回 0(假),需要执行第二个
fork(),创建子进程 P2。
- P0(父):fork () 返回 P1 的 PID(非 0,真),触发
- 执行第一个
- 最终进程数: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. 僵尸进程的本质:「退出状态未被回收」
进程的完整生命周期分为两步:
- 执行阶段:进程运行代码,占用 CPU、内存等资源;
- 退出阶段:进程执行
exit()后,会释放大部分资源(内存、文件句柄等),但会保留「最小信息」(PID、退出状态、运行时间等),等待父进程通过wait()/waitpid()调用回收;- 如果父进程一直不调用 wait (),这些「最小信息」就会一直占用系统资源,进程变成僵尸进程(Z 状态);
- 只有父进程调用
wait(),或父进程退出,这些信息才会被回收。
总结:
- PID 独立性:子进程的 PID 由系统分配,父进程退出不会导致子进程 PID 消失,子进程会继续运行;
- 僵尸进程成因:子进程退出后,父进程未调用
wait()回收其退出状态,而非「父进程先退出」; - 孤儿进程的处理:父进程先退出,子进程被 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
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)