目录
1.1 命令行参数的本质作用
1.2 环境变量实战指南
🌵关键环境变量解析
🌵测试PATH:
🌵测试HOME:
🌵环境变量的组织方式:
🌵环境变量操作命令速查表
1.3 解析Linux环境变量继承机制
二、程序地址空间深度解析
2.1 虚拟地址空间示意图
2.2 虚拟地址实验分析
2.3 进程地址空间
三、Linux2.6内核进程调度队列
3.1 O(1)调度器核心结构
3.2 活动队列
3.3 过期队列
3.4 active指针和expired指针
3.5 调度流程解析
一、命令行参数与环境变量探秘
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. 原因分析
二、程序地址空间深度解析
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){ //childprintf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{ //parentprintf("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{ //parentsleep(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)算法!