目录
一、命令行参数与环境变量探秘
1.1 命令行参数的本质作用
命令行参数是在程序运行时传递给程序的选项,用于定制程序的功能。它们通过 main
函数的参数传递,通常包括 argc
(参数数量)和 argv
(参数数组)。例如,运行一个程序时,可以使用 ./program -a -b
传递参数 -a
和 -b
,程序可以根据这些参数执行不同的操作。
在Linux系统中,命令行参数是程序启动时接收外部配置的核心机制。通过以下代码示例可以看到参数传递的完整过程:
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
printf("参数个数: %d\n", argc);
for(int i=0; argv[i]; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
当执行./demo -a -b hello
时输出:
设计原理:
-
参数个数自动计算(argc)
-
参数指针数组连续存储(argv)
-
环境变量指针数组结尾以NULL标识
1.2 环境变量实战指南
🌵关键环境变量解析
环境变量是操作系统用来存储和传递系统环境信息的一种机制,相当于全局变量,可供系统中的各个程序、进程在运行时访问和使用。常见的环境变量包括:
变量名 | 功能说明 | 示例值 |
---|---|---|
PATH | 可执行文件搜索路径 | /usr/bin:/usr/local/bin |
HOME | 用户主目录 | /home/user |
SHELL | 默认shell程序路径 | /bin/bash |
环境变量可以通过 echo $NAME
查看,其中 NAME
是环境变量的名称。例如,echo $PATH
可以显示当前的命令搜索路径。
🌵测试PATH:
1. 创建 hello.c
文件
#include <stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
2. 对比 ./hello 执行和直接 hello 执行
gcc hello.c -o hello
生成可执行文件 hello
。
执行方式对比:
-
./hello
:直接运行当前目录下的可执行文件。 -
hello
:尝试在 PATH 指定的路径中查找hello
命令。
测试结果:
通常情况下,直接输入 hello
会报错,提示 command not found
,而 ./hello
可以正常运行。
3. 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?
-
系统命令路径已包含在 PATH 中:例如
ls
、cd
等命令所在的路径(如/bin
、/usr/bin
)默认包含在 PATH 环境变量中。 -
当前目录不在 PATH 中:系统默认不会在当前目录查找可执行文件,除非 PATH 包含
.
(当前目录)。
4. 将程序所在路径加入 PATH(export PATH=$PATH:程序所在路径)
5. 对比测试
🌵测试HOME:
1. 用 root
和普通用户分别执行 echo $HOME
,对比差异
-
查看普通用户的
HOME
环境变量:
-
切换到
root
用户:
- 查看
root
用户的HOME
环境变量:
对比结果:
普通用户:
HOME
通常指向/home/username
。root 用户:
HOME
通常指向/root
。
2. 执行 cd ~; pwd
,对应 ~
和 HOME
的关系
- 在普通用户下执行
cd ~
并查看当前目录:
- 在 root 用户下执行
cd ~
并查看当前目录:
解释:
~
是一个快捷符号,表示当前用户的主目录,即HOME
环境变量所指向的目录。
cd ~
等价于cd $HOME
,都会将用户切换到主目录。
pwd
命令用于显示当前工作目录的完整路径。
3. 总结
-
HOME
环境变量:用于存储当前用户的主目录路径。 -
~
符号:表示当前用户的主目录,等价于$HOME
。 -
普通用户与
root
用户的差异:-
普通用户的主目录通常位于
/home/username
。 -
root
用户的主目录通常位于/root
。
-
🌵环境变量的组织方式:
每个程序运行时都会收到一张环境表,它是一个字符指针数组,数组中的每个指针都指向一个以 \0
结尾的环境字符串。通过 extern char **environ
可以访问环境变量表。例如,以下代码可以打印所有环境变量:
#include <stdio.h>
int main()
{
extern char **environ;
int i = 0;
for (; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
🌵环境变量操作命令速查表
命令 | 功能说明 | 示例 |
---|---|---|
env | 显示所有环境变量 | env | grep PATH |
export | 设置/显示环境变量 | export MYVAR="test" |
unset | 删除环境变量 | unset MYVAR |
printenv | 显示指定环境变量 | printenv LANG |
1.3 解析Linux环境变量继承机制
环境变量通常是具有全局属性的,这意味着它们不仅对当前 shell 有效,还可以被子进程继承。
1. 环境变量的继承机制
当在父进程中通过 export
导出一个环境变量时,该变量会被传递给所有后续的子进程。这是因为在 Linux 系统中,进程在创建子进程时会复制自身的环境变量表,从而使子进程能够访问父进程中导出的环境变量。 例如,以下代码演示了环境变量的继承:
// 子进程
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv("MYENV");
if (env)
{
printf("%s\n", env); // 输出 "hello world"
}
return 0;
}
2. 普通变量与环境变量的区别
普通变量(未导出的变量)仅在当前 shell 中有效,不会被子进程继承。例如:
// 子进程
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv("MYENV");
if (env)
{
printf("%s\n", env); // 没有输出,因为 MYENV 未导出
}
return 0;
}
3. 导出和不导出环境变量的实验
-
不导出环境变量:
MYENV="helloworld"
./program
子进程无法访问
MYENV
,因此getenv("MYENV")
返回NULL
。
- 导出环境变量:
export MYENV="helloworld"
./program
子进程可以访问
MYENV
,因此getenv("MYENV")
返回"helloworld"
。
结论:
-
导出环境变量:子进程可以继承。
-
普通变量(未导出):子进程无法继承。
4. 原因分析
-
导出环境变量:
export
将变量标记为环境变量,使其在父进程及其子进程中都可用。 -
普通变量:仅在当前 shell 中有效,不会传递给子进程。
二、程序地址空间深度解析
2.1 虚拟地址空间示意图
2.2 虚拟地址实验分析
通过父子进程地址实验揭示虚拟内存机制:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出:
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出:
我们发现,父子进程,输出地址是一致的,但是变量内容却不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
- 但地址值是一样的,说明,该地址绝对不是物理地址。
- 在Linux地址下,这种地址叫做 虚拟地址。
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
OS必须负责将 虚拟地址 转化成 物理地址 。
2.3 进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 “进程地址空间”,那该如何理解呢?看图:
地址转换流程:
虚拟地址 -> MMU -> 页表查询 -> 物理地址
上面这张图从分页与虚拟地址空间概念入手,生动展示了同一变量在不同情境下虽地址看似相同(实为虚拟地址一致),但内容却迥异,关键在于它们被映射至了不同物理地址。通俗来讲,恰似一张“虚拟蓝图”,它让诸多人(进程)以为身处同一地图坐标(虚拟地址),但彼此所指实地点(物理地址)却大相径庭,皆因背后有一套转换体系(分页机制)在运筹帷幄,各司其职,巧妙分配。
三、Linux2.6内核进程调度队列
3.1 O(1)调度器核心结构
Linux2.6内核中进程队列的数据结构:
关键组件说明:
-
活动队列(active):存放时间片未耗尽的进程。
-
过期队列(expired):存放时间片已用完的进程。
-
优先级数组(140级):0-99实时优先级(不关心),100-139普通优先级(nice值的取值范围,可与之对应)。
-
位图索引(bitmap[5]):快速定位非空队列。
3.2 活动队列
1. 时间片还没有结束的所有进程都按照优先级放在该队列nr_active: 总共有多少个运行状态的进程。
2. queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
3. 从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
4. bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
3.3 过期队列
- 过期队列和活动队列结构一模一样。
- 过期队列上放置的进程,都是时间片耗尽的进程。
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
3.4 active指针和expired指针
- active指针永远指向活动队列。
- expired指针永远指向过期队列。
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
3.5 调度流程解析
-
从最高优先级(数组索引0)开始扫描
-
通过bitmap快速定位第一个非空队列
-
取出队列首进程分配CPU时间片
-
时间片耗尽后移入过期队列
-
活动队列空时交换active与expired指针
性能优势:
-
时间复杂度恒定为O(1)
-
优先级处理高效
-
负载均衡优化
3.6 总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!