目录
Z(zombie)-僵尸进程
1. 僵尸进程概念
故事
张三每天都有跑步的习惯,这一天他和往常一样跑步,跑了两三圈,突然跑在它前面的一个人倒在地上不动了,作为热心市民张三赶紧报警并且拨打120。很快120就来了,但是没过几分钟就走了,这个人已经噶了(去世了),并交给了警察。警察这个时候会怎样做呢?警察立刻封锁了现场,并且叫来法医,法医确认了这个人的死亡原因,并且收集了所有信息。然后告诉家属信息。这个时候再把这个人抬走。
请问从这个人倒下到他被抬走,他是死亡状态吗?
不是,他是死亡了,但是他不是死亡状态,这个时候他是僵死状态,直到他被抬走他才是死亡状态。
僵尸进程的产生原因?
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
当子进程终止时,会向父进程发送 SIGCHLD
信号,此时:
-
正常情况:父进程调用
wait()
或waitpid()
系统调用,回收子进程的退出状态。 -
僵尸进程产生:父进程未处理
SIGCHLD
信号,也未回收子进程的退出状态。
2. 僵尸进程的危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护。
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 僵尸进程会导致内存泄漏
- 资源占用:占用有限的 PID 资源(PID 耗尽时无法创建新进程)。
-
系统稳定性:大量僵尸进程可能干扰系统监控和管理工具。
3. 如何查看僵尸进程
方法 1:使用 ps
命令
ps aux | grep 'Z'
4. 如何清理僵尸进程
解决方法
- 重启操作系统(建议不推荐)
- 将僵尸进程的父进程杀掉,将最终使用的子进程变为孤儿进程,从而交由init进程处理其退出信息
- 利用信号SIGCHLD,子进程状态改变后会向其父进程发送SIGCHLD信号。父进程在接受到该信号后,在信号处理函数中调用wait()或者waitpid()
方法 1:终止父进程
僵尸进程会随父进程终止而被系统回收(由 init
进程接管)。
1. 找到僵尸进程的父进程 PID(PPID):
ps -eo pid,ppid,state,cmd | grep 'Z'
父进程为72197
2. 终止父进程:
kill -9 72197 # 强制终止父进程
方法 2:手动发送
SIGCHLD
信号
若父进程设计不良未处理 SIGCHLD
,可手动触发父进程回收子进程:
kill -s SIGCHLD PPID # 向父进程发送 SIGCHLD 信号
4. 僵尸进程的模拟实现
在代码当中创建出来一个子进程,模拟让子进程先于父进程退出,通过fork函数的返回值,让父子进程走不同的分支逻辑。
- 子进程直接退出。
- 父进程别退出(while (1))。
观察:子进程目前的运行状态
代码如下:
#include <stdio.h> // 标准输入输出函数
#include <stdlib.h> // 标准库函数,如exit, EXIT_SUCCESS
#include <unistd.h> // 提供系统调用,如fork(), sleep(), getpid()
#include <sys/types.h> // 定义进程标识类型pid_t
#include <sys/wait.h> // 提供wait()相关函数
int main()
{
// 使用fork()创建子进程,id保存返回值
pid_t id = fork();
// fork失败处理:id < 0表示创建进程失败
if(id < 0)
{
perror("fork"); // 打印错误信息
return 1; // 非正常退出
}
// 父进程分支:id > 0时,当前代码在父进程运行
else if (id > 0)
{
// 打印父进程PID并进入长时间休眠
printf("parent[%d] is sleeping...\n", getpid());
sleep(100); // 父进程休眠100秒,期间未处理子进程状态
}
// 子进程分支:id == 0时,当前代码在子进程运行
else
{
// 打印子进程PID后短暂休眠并退出
printf("child[%d] is begin Z...\n", getpid());
sleep(2); // 子进程休眠2秒
exit(EXIT_SUCCESS); // 子进程正常退出,状态码0
// 父进程未调用wait,子进程将成僵尸进程(Z状态)
}
return 0; // 父进程最终正常退出(但子进程在此前可能已成僵尸)
}
/* 代码现象说明:
* 1. 子进程在2秒后退出,父进程持续休眠100秒。
* 2. 由于父进程未调用wait()回收子进程,子进程在退出后会保持僵尸状态(Z+),
* 直到父进程结束或处理子进程状态。
* 3. 可通过`ps aux | grep 程序名`在运行期间观察僵尸进程状态。
*/
编译运行如下图所示:
僵尸进程会自行消失吗?
-
不会,除非父进程终止或主动回收。
init
进程会回收僵尸进程吗?
-
会,但仅当僵尸进程的父进程终止后,
init
才会接管并回收。
孤儿进程
1. 孤儿进程概念
-
状态:父进程提前终止(如崩溃、被终止),但其子进程仍在运行。
-
系统处理:孤儿进程会被操作系统内核的
init
进程(PID=1)接管,成为新的父进程。 -
特点:
-
孤儿进程本身仍在运行,只是父进程被替换为
init
。 -
不会对系统造成危害,最终会由
init
进程正确回收资源。
-
父进程先退出,子进程被称为“孤儿进程”。
父进程结束或者异常终止,但是子进程继续运行。此时子进程的PPID被设置为1,即init进程。init进程接管了该子进程,并等待它结束,在父进程退出之后,子进程退出之前,该子进程属于孤儿进程
孤儿进程的产生原因?
当父进程终止时,其子进程若仍在运行,则会成为孤儿进程。常见场景:
-
父进程意外崩溃(如段错误)。
-
父进程未正确等待子进程结束(如未调用
wait()
/waitpid()
)。 -
父进程被强制终止(如
kill -9
)。
2. 模拟实现孤儿进程
孤儿进程:
- 模拟产生一个孤儿进程:父进程创建一个子进程,父进程先于子进程退出,子进程就是孤儿进程
- 子进程代码当中写一个死循环
- 父进程直接退出
#include <stdio.h> // 标准输入输出库(printf、perror)
#include <unistd.h> // Unix 标准库(fork、sleep、getpid)
#include <stdlib.h> // 通用工具库(exit)
int main()
{
// 创建子进程
// fork() 返回值:
// -1 : 创建失败
// 0 : 子进程分支
// >0 : 父进程分支(返回值为子进程PID)
pid_t id = fork();
if (id < 0)
{
// fork 创建失败处理
perror("fork"); // 打印错误信息(例如达到进程数上限)
return 1; // 非正常退出
}
else if (id == 0)
{
// 子进程执行分支
while (1) // 无限循环
{
// 每秒打印子进程的 PID
printf("I am child, pid : %d\n", getpid());
sleep(1); // 睡眠1秒(避免过度占用CPU)
}
}
else
{
// 父进程执行分支
printf("I am father, pid : %d\n", getpid());
sleep(10); // 父进程休眠10秒
// 父进程主动退出(不等待子进程)
exit(0); // 正常退出
// 此时子进程将成为孤儿进程(由init进程接管)
}
return 0;
}
3. 孤儿进程 vs 僵尸进程
特性 | 孤儿进程 | 僵尸进程 |
---|---|---|
状态 | 仍在运行 | 已终止但未被回收 |
父进程 | 被 init 接管 | 原父进程未调用 wait() |
资源占用 | 占用内存和 CPU | 仅占用 PID,不占内存/CPU |
危害 | 无直接危害 | 可能耗尽 PID 资源 |
处理方式 | 由 init 自动回收 | 需终止父进程或修复父进程逻辑 |
4. 总结
-
孤儿进程是父进程意外终止后由
init
接管的子进程,无害且会被自动回收。 -
僵尸进程是已终止但未被回收的进程,需手动处理。
-
通过正确的进程管理(如
wait()
、信号处理),可避免孤儿或僵尸进程的产生。
进程优先级
1. 优先级(Priority) vs 权限(Permission)
维度 | 优先级 | 权限 |
---|---|---|
本质 | 资源分配的先后顺序 | 资源访问的合法性 |
核心问题 | 谁先获得资源? | 是否有资格获得资源? |
类比场景 | 银行排队:VIP客户优先办理 | 进入银行需要身份验证(权限) |
实现方式 | 进程调度算法(如优先级数值、时间片轮转) | 用户/组权限控制(如 chmod 、sudo ) |
操作影响 | 影响进程获取资源的顺序和频率 | 决定进程能否访问文件、设备或执行操作 |
示例
-
优先级场景:两个用户同时运行 CPU 密集型任务,优先级高的进程会获得更多 CPU 时间片。
-
权限场景:普通用户无法修改系统文件(无写权限),也无法将进程优先级设为负数(需 root 权限)。
为什么会存在优先级?
优先级,排队就是在确定优先级
- 是什么:得到某种资源的先后顺序
- 为什么:本质是 cpu 或 操作系统 等等的资源不足
- 怎么做:其实就是 pcb 中的一个 int 字段,数值越小,优先级越大
- cpu 资源分配、外设 IO 的先后顺序,就是指进程的优先权(priority)
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 Linux 很有用,可以改善系统性能。
- 还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
总结如下:
-
资源不足:CPU、内存、I/O 设备等资源有限,无法同时满足所有进程的需求。
-
优化目标:
-
公平性:防止单个进程独占资源。
-
效率性:关键任务优先执行,提升系统整体性能。
-
实时性:确保高优先级任务(如音视频处理)及时响应。
-
1. 查看进程优先级
使用 ps
命令
ps -eo pid,ni,pri,comm # 查看所有进程的 Nice 值(NI)和优先级(PRI)
ps -l <PID> # 查看指定进程的优先级
ps -al
使用命令如下图所示:
参数含义如下:
- UID:代表执行者的身份,用户标识符。
- PID:代表这个进程的代号。
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
- PRI:prority代表这个进程可被执行的优先级,其值越小越早被执行。
- NI:代表这个进程的nice值。
2. PRI(Priority)和 NI(Nice)
1. PRI(Priority)与 NI(Nice)的定义
参数 | 作用 | 默认值 | 范围 | 调整权限 |
---|---|---|---|---|
PRI | 进程的实际优先级,决定调度顺序 | 80 | 60~99 | 用户无法直接修改 |
NI | 用户可调整的优先级修正值(静态) | 0 | -20~19 | 普通用户只能增大 NI |
注意:
- nice调整最小是:-20,超过部分统一当成-20
- nice调整最大是:19,超过部分统一当成19
2. PRI 与 NI 的关系
计算公式如下:
PRI(new) = PRI(old) + NI
-
PRI(old):系统基准优先级(默认 80)。
-
NI:用户设置的修正值(-20~19)。
示例如下:
-
若设置
NI = -10
,则PRI(new) = 80 + (-10) = 70
(优先级提高)。 -
若设置
NI = 15
,则PRI(new) = 80 + 15 = 95
(优先级降低)。
3. 为什么限制 PRI 和 NI 的数值范围?
(1) 避免进程饥饿(Starvation)
-
问题:若允许 PRI 无限降低(或 NI 无限小),高优先级进程可能长期独占 CPU,导致低优先级进程无法执行。
-
解决:通过范围限制(PRI 最低 60,NI 最低 -20),确保所有进程都能被公平调度。
(2) 保障系统稳定性
-
资源平衡:限制 PRI 范围(60~99)让内核在调度时有足够的调整空间,避免极端优先级导致系统负载失衡。
-
用户权限隔离:普通用户只能降低自身进程优先级(增大 NI),防止恶意抢占系统资源。
修改 NI值
用top命令更改已经存在进程的nice值
top
# 1. 进入top后按“r”
# 2. 输入进程PID
# 3. 输入nice值
3. 为啥要有优先级
在操作系统中引入 优先级(Priority) 机制,本质是为了解决 资源有限性 和 任务多样性 之间的矛盾。
1. 解决资源竞争问题(竞争性)
-
资源有限:CPU 核心数远小于进程数(例如单核 CPU 需处理成百上千个进程)。
-
任务多样性:不同进程的重要性、实时性、资源需求差异巨大(如后台日志处理 vs 用户交互界面)
优先级的作用:
每个进程不是占用 CPU 就一直运行,每隔一段时间,进程就会被从 CPU 剥离下来,时间片
Linux内核支持进程之间进行 CPU 资源抢占的(基于时间片的轮转式的抢占内核)
-
资源分配策略:通过优先级标识进程的重要性,确保关键任务优先获取 CPU 时间片。
-
避免低效轮转:若所有进程平等竞争,高价值任务可能因等待低价值任务而延迟完成。
进程调度和资源管理
在操作系统中,进程的 竞争性、独立性、并行、并发 是进程调度和资源管理的核心特性。它们共同构成了多任务系统的基础,
- 竞争性:系统进程数目众多,而CPU资源只有少量甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
- 资源隔离:每个进程拥有独立的虚拟地址空间、文件描述符、信号处理等资源,互不干扰。
- 故障隔离:一个进程崩溃不会影响其他进程或操作系统核心。
- 并行:多个进程在多个 CPU 下分别,同时进行运行,这称之为并行。
-
多核同时执行:多个进程在多个 CPU 核心上 同时运行,物理上真正并行。
-
加速任务:适用于计算密集型任务(如科学计算、图像渲染)。
-
- 并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
-
时间片轮转:单个 CPU 核心通过快速切换(时间片)模拟“同时运行”多个进程。
-
逻辑上的并行:进程在宏观上看似同时推进,微观上交替执行。
-
关键对比:并行 vs 并发
维度 | 并行(Parallelism) | 并发(Concurrency) |
---|---|---|
硬件要求 | 多核 CPU 或分布式系统 | 单核 CPU 即可实现 |
执行方式 | 物理上同时运行 | 逻辑上交替运行 |
目标 | 加速任务(利用多核性能) | 提高资源利用率(避免 CPU 空闲) |
适用场景 | 计算密集型任务(如数值模拟) | I/O 密集型任务(如网络请求处理) |
函数内定义的栈临时变量如何返回给外部?
(1) 临时变量的生命周期
-
栈分配:函数内的局部变量(临时变量)通常存储在栈上,函数返回时栈帧被销毁,变量理论上失效。
-
返回值传递:若需要将内部变量传递到外部,需通过 寄存器 或 内存 传递值(而非传递指针)。
(2) 寄存器 eax
的作用
-
返回值寄存器:在 x86 架构中,
eax
(或rax
在 64 位)寄存器用于存储函数的返回值。 -
值复制机制:函数返回时,临时变量的值会被复制到
eax
中,外部通过读取eax
获取结果。
示例如下:
int func()
{
int tmp = 42; // 栈上的临时变量
return tmp; // 将 tmp 的值复制到 eax
}
// 外部通过 eax 获取 42
(3) 为什么栈变量能安全返回?
-
值传递:返回的是值的副本,而非栈内存地址。即使栈帧销毁,外部获取的是
eax
中的副本,与原始栈变量无关。 -
寄存器中转:编译器自动生成代码,将返回值存入
eax
,不依赖原栈内存。
程序如何跟踪执行位置与函数跳转?
(1) 程序计数器 eip
(Instruction Pointer)
-
核心作用:
eip
寄存器存储下一条待执行指令的地址,CPU 按eip
的指引顺序执行代码。 -
函数跳转:
-
call
指令:调用函数时,eip
跳转到函数入口地址,并将返回地址(下一条指令地址)压入栈。 -
ret
指令:函数返回时,从栈中弹出返回地址到eip
,继续执行原流程。
-
(2) 栈帧与寄存器协作
-
ebp
和esp
:ebp
(基址指针)标记当前栈帧起始,esp
(栈指针)动态管理栈顶。 -
函数调用链:每次函数调用会创建新栈帧,
ebp
和esp
协助维护栈结构,确保返回时能正确恢复上下文。
多进程如何管理 CPU 寄存器的临时数据?
(1) 寄存器与寄存器内容的区别
-
寄存器是硬件资源:CPU 中仅有单组物理寄存器(如
eax
,ebx
,eip
等)。 -
寄存器内容是进程上下文:每个进程运行时,寄存器的值是其运行状态的快照(如
eip
指向进程代码段地址)。
(2) 进程上下文切换(Context Switch)
-
触发时机:时钟中断、系统调用、进程主动让出 CPU 等。
-
切换步骤:
-
保存当前进程上下文:将寄存器的值保存到进程控制块(PCB,如 Linux 的
task_struct
)。 -
加载下一进程上下文:从目标进程的 PCB 恢复寄存器值到物理寄存器。
-
更新
cr3
(页表基址寄存器):切换虚拟内存空间。
-
4. 关键寄存器的作用
寄存器 | 核心作用 |
---|---|
eip | 程序计数器,指向下一条指令地址 |
esp | 栈指针,指向当前栈顶地址 |
ebp | 基址指针,标记当前栈帧起始位置 |
eax | 通用寄存器,常用于函数返回值 |
cr3 | 控制寄存器,存储当前进程页表的物理地址,用于虚拟内存切换 |
cs /ds | 代码段/数据段寄存器,与保护模式下的内存分段机制相关(现代系统多用平坦模式) |
环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
为什么需要环境变量?
环境变量(envirnment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
常见的环境变量
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHEIL:当前Shell,它的值通常是/bin/bash
USER
:当前登录的用户名LANG
:系统语言设置
注意事项
-
环境变量名是大小写敏感的(例如
PATH
与Path
不同)。 -
通过
export
设置的环境变量仅在当前 Shell 会话有效,重启后失效。
查看环境变量
1. 查看所有环境变量
这会列出所有当前设置的环境变量
// 使用 printenv 命令:这会列出所有当前设置的环境变量。
printenv
// 使用 env 命令:这会列出所有当前设置的环境变量。
env
如下图所示:
2. 查看特定环境变量
# 通过 printenv + 变量名:
printenv VARIABLE_NAME
# 通过 echo + $ 符号:
echo $VARIABLE_NAME
如下图所示:
PATH环境变量
PATH用于指定命令的搜索路径
我们来做一个简单的小实验,创建一个test.cpp文件,并且编译形成一个可执行程序mybin,然后分别带路径和不带路径进行测试对比
#include<iostream>
int main()
{
std::cout<<"Hello world"<<std::endl;
return 0;
}
编译后运行
带路径运行如下图:
带路径运行如下图:
我们分别带路径和不带路径运行,发现带路径就可以运行,而不带路径无法运行。 这个时候就有个问题?
为什么有些指令可以直接执行(例如ls、cd等等),不需要带路径,而我们的二进制程序需要带路径才能执行?
如果我们的程序所在路径加入环境变量PATH当中会发生什么呢?
# 只是临时修改,离开当前shell就失效了
export PATH=$PATH:文件所在路径
此时我们发现确实不用带路径就可以运行了 。这个时候我们又有问题了。为啥这个时候有可以不带路径了,我只是把mybin的路径加入到环境变量中而已?
环境变量的组织方式
- 命令行启动的进程都是shell/bash的子进程,子进程的命令行和环境变量是父进程(bash)传递的。
- 父进程的环境变量又是哪来的呢?——》环境变量是以脚本配置文件的形式存在的
- 我们在启动shell的时候就会自动执行的一个可执行程序,就是环境变量的以脚本配置文件
在C语言中我们学到了指针数组,就是存放指针的数组,那么存放的就是各种环境变量的首地址,通过找到环境变量的首地址就可以找到对于的环境变量。
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
环境变量相关的命令
在 Linux 系统中,管理环境变量的常用命令和操作如下:
1. 查看环境变量
1.1 查看所有环境变量
printenv # 显示所有环境变量
env # 显示所有环境变量(与 printenv 类似)
set # 显示所有环境变量 + Shell 局部变量(内容较多)
1.2 查看特定环境变量
printenv VARIABLE_NAME # 例如 printenv PATH
echo $VARIABLE_NAME # 例如 echo $HOME
2. 设置环境变量
2.1 临时设置(仅在当前 Shell 有效)
export VAR_NAME="value" # 设置环境变量(子进程可继承)
VAR_NAME="value" # 设置局部变量(仅当前 Shell 有效)
export PATH=$PATH:文件所在路径 # 指定命令的搜索路径
2.2 永久设置(需写入配置文件)
用户级(仅对当前用户生效):
# 修改.bashrc 文件
echo 'export VAR_NAME="value"' >> ~/.bashrc # 追加到 ~/.bashrc
source ~/.bashrc # 立即生效
系统级(对所有用户生效):
# 修改profile 文件
sudo sh -c 'echo "export VAR_NAME=value" >> /etc/profile' # 写入全局配置
source /etc/profile # 立即生效(需管理员权限)
3. 删除环境变量
3.1 临时删除
unset VAR_NAME # 删除环境变量或局部变量
3.2 永久删除
从配置文件(如 ~/.bashrc
或 /etc/profile
)中移除对应的 export
行,并执行 source
重新加载。
source ~/.bashrc
source /etc/profile
通过代码如何获取环境变量
1. 方法 1:通过 main
的第三个参数 envp
(POSIX 标准)
#include <stdio.h>
int main(int argc, char* argv[], char* envp[])
{
// argc : 命令行参数的个数, 本质上就是argv数组的元素个数
// argv :具体的命令行参数
// envp : 环境变量的值
// 遍历命令行参数
printf("命令行参数:\n");
for (int i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
// 遍历环境变量
printf("\n环境变量:\n");
for (int i = 0; envp[i] != NULL; i++)
{
printf("envp[%d]: %s\n", i, envp[i]);
}
return 0;
}
编译运行如下图:
说明:
-
envp
是一个以NULL
结尾的字符串数组,每个元素的格式为VAR_NAME=value
。 -
这种方式是 POSIX 标准 的一部分,在 Linux/Unix 系统中可用,但 不属于 C 语言标准。
2. 方法 2:通过全局变量 environ
(更通用的方式)
另一种更通用的方式是使用全局变量 extern char** environ
(需要包含 <unistd.h>
):
#include <stdio.h>
#include <unistd.h> // 需要包含此头文件
extern char** environ; // 声明全局变量
int main(int argc, char* argv[])
{
// 遍历环境变量
printf("环境变量:\n");
for (int i = 0; environ[i] != NULL; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
编译运行如下图:
说明:
-
environ
是一个全局变量,与envp
的内容完全一致。 -
这是 POSIX 标准 的推荐方式,避免了直接依赖
main
的第三个参数。
3. 方法 3:通过 getenv
函数获取特定环境变量(标准 C 库)
如果只需要获取某个特定环境变量的值,可以使用 getenv
函数:
#include <stdio.h>
#include <stdlib.h> // 需要包含此头文件
int main()
{
// 获取特定环境变量
char* path = getenv("PATH");
if (path != NULL)
{
printf("PATH: %s\n", path);
}
else
{
printf("PATH 未设置\n");
}
return 0;
}
编译运行如下图:
说明:
-
getenv("VAR_NAME")
返回环境变量的值(字符串),若未找到则返回NULL
。 -
这是 C 标准库函数,跨平台兼容性更好。
方法的对比与选择
方法 | 特点 |
---|---|
main 的第三个参数 | 直接访问所有环境变量,但依赖 POSIX 实现,可移植性较差 |
全局变量 environ | 更通用的 POSIX 方式,适合需要遍历所有环境变量的场景 |
getenv 函数 | 标准 C 库函数,适合获取单个环境变量,跨平台兼容性好 |
注意事项如下:
1. 安全性:
- 直接操作
envp
或environ
可能修改环境变量(不推荐),而getenv
是只读的。 - 修改环境变量应使用
setenv
/putenv
/unsetenv
等函数。
2. 可移植性:main
的第三个参数 envp
在 Windows 中不可用,但 environ
和 getenv
是跨平台的。
3. 格式解析:环境变量的格式为 VAR_NAME=value
,需自行解析字符串(例如用 strtok
或 strchr
)。
本地变量和环境变量以及内键命令
1. 本地变量(Local Variables)
-
作用域:仅在当前 Shell 进程内有效,不会传递给子进程。
-
生命周期:随当前 Shell 会话结束而消失。
-
赋值语法:
VAR_NAME="value" # 赋值(等号两侧不能有空格)
示例如下:
# 定义本地变量
my_var="Hello, Local Variable"
# 输出变量
echo $my_var # 输出: Hello, Local Variable
# 在子 Shell 中无法访问
bash -c 'echo $my_var' # 输出: 空值
运行如下图:
2. 环境变量(Environment Variables)
-
作用域:可传递给当前 Shell 的 子进程。
-
生命周期:默认仅在当前 Shell 有效,持久化需写入配置文件。
-
创建方法:
export VAR_NAME="value" # 将本地变量转为环境变量
3. 内建命令(Built-in Commands)
-
实现方式:由 Shell 自身直接执行,不启动新进程,执行速度快。
-
作用:用于操作 Shell 环境(如变量管理、流程控制等)。
常见内建命令
命令 | 作用 | 示例 |
---|---|---|
export | 将变量导出为环境变量 | export VAR="value" |
unset | 删除变量 | unset VAR |
readonly | 设置只读变量 | readonly VAR="value" |
cd | 切换目录 | cd /path |
source | 执行脚本并影响当前 Shell 环境 | source ~/.bashrc |
echo | 输出内容 | echo "Hello" |
set | 设置 Shell 选项或显示变量 | set -o vi |
alias | 创建命令别名 | alias ll='ls -alF' |
判断命令是否为内建
type command_name # 例如 type cd 返回 "cd is a shell builtin"
4. 关键区别总结
特性 | 本地变量 | 环境变量 | 内建命令 |
---|---|---|---|
作用域 | 当前 Shell | 当前 Shell 及其子进程 | 直接由 Shell 执行,无子进程 |
生命周期 | 随 Shell 结束消失 | 默认随 Shell 结束消失 | 立即生效,无独立生命周期 |
创建方式 | VAR="value" | export VAR="value" | Shell 内置实现 |
查看方式 | set | printenv 或 env | type command 查看 |
典型用途 | 脚本内部临时存储数据 | 跨进程传递配置信息 | 管理 Shell 环境或执行基础操作 |
5. 注意事项
-
变量赋值规则:等号两侧不能有空格:
VAR=value
(正确),VAR = value
(错误)。 -
环境变量继承:子进程会继承父进程的环境变量,但无法反向修改父进程环境。
-
配置文件优先级:用户级配置(如
~/.bashrc
)会覆盖系统级配置(如/etc/profile
)。 -
只读变量:使用
readonly VAR="value"
设置后不可修改或删除。