Linux 进程管理核心机制
目录
- Linux 进程管理核心机制
- 一、命令行参数与环境变量
- 1.1 命令行参数
- 1.2 环境变量
- 1.5 子进程访问机制
- 直接访问
- 传参访问
- 1.6 Bash 如何存储环境变量
- 1.7总结
- 二、进程地址空间
- 2.1 概念
- 2.2虚拟地址
- 2.3 地址空间的优点
- 2.4 硬件协同机制
- 三、Linux 2.6 进程调度
- 3.1运行队列架构
- 位图索引机制
- 双队列轮转机制
Linux 进程管理核心机制
一、命令行参数与环境变量
1.1 命令行参数
本质:用户传递给程序的选项,用于定制程序功能,命令本质也是可执行程序,命令中携带的选项就是命令行参数
传递者:由父进程(通常是 bash 命令行解释器)传递
作用:通过不同参数组合实现程序功能的动态配置
1.2 环境变量
在linux系统中,存在一些全局的设置,用某些变量保存,就是环境变量。
举例:PATH环境变量,告诉命令行解释器(bash),应该去哪些路径下去寻找可执行程序
他们是如何加载呢?
Linux系统中存在很多的配置,在登录LInux系统的时候,已经被加载到bash进程中(加载到内存),如果不小心删了,可以重新登录来恢复(从磁盘中重新加载),因为这个是内存级别的环境变量。最开始的环境变量不是在内存中,而是在磁盘文件中。
相关操作
env:查看所有环境变量
echo &XXX ;打印环境变量内容
export XXXXXX = xxxxx;导入环境变量,即自己定义环境变量,但是实在内存里(bash进程)中修改,并非配置文件,关机后消失
unset name;取消环境变量
环境变量其他例子:
HOME:保存家目录的环境变量
PWD:当前路径,该环境变量是变化的
SHELL:命令行解释器保存路径
HISTSIZE:记录命令的默认数量,
HOSTNAME:主机名
本地变量
没有导入到环境变量里,但是在bash中实际存在,叫做本地变量
本地变量无法被子进程继承
1.5 子进程访问机制
直接访问
父进程的数据,默认可以被子进程看到并且访问
Linux系统中的环境变量是保存在配置文件里的,在磁盘上,机器开机的时候,会启动bash进程,这之后会将包含了环境变量的配置文件加载到bash进程中,我们自己运行程序或者指令程序,本质都是bash进程的子进程,因此都可以看到并且访问环境变量
传参访问
main函数支持传参
int main(int argc, char* argv[], char* env[])
-
argc:命令行参数个数
-
argv[]:命令行参数指针数组
-
env[]:环境变量指针数组(以 NULL 结尾)
1.6 Bash 如何存储环境变量
存储方式:
char *env[];
即一个指针数组,数组中每个元素指向一个字符串,这个字符串就是刚开始加载进来的环境变量。该数组必须以空指针NULL结尾
char *env[] = {"PATH=/usr/bin","HOME=/home/user",/* ... */ NULL // 结束标志
};
1.7总结
bash进程启动时,默认会生成两张表。
1.argv[],命令行参数表。这张表从命令行获取,即用户输入
2.env[], 环境变量表。这张表从os的配置文件获得
这两张表bash通过各种方式交给子进程
二、进程地址空间
2.1 概念
每个进程都有自己独立的地址空间
地址空间里所用地址都是虚拟地址,由页表进行地址映射,从而找到物理地址
地址空间本质是一个内核数据结构对象
地址空间本质是内核的一个struct结构体!内部很多的属性都是表示start,end的范围
源码中的struct mm_struct就是地址空间的结构体。其中start_code就是代码段的开始,end_code就是代码段的结束,类似的还有数据段开始和结束start_data,end_data等。
2.2虚拟地址
虚拟地址指的是程序地址空间中的地址,相对应的也有物理地址,指的是在内存中实际存放的地址,他们之间由页表进行映射,在访问某一数据时,程序只知道其虚拟地址,操作系统会帮他找到物理地址。创建子进程时,子进程会继承父进程的代码和数据,也包括页表,两个进程刚开始的代码和数据都在同一片空间。
我们看一段代码
#include<stdio.h>
#include<unistd.h>
int main()
{int val = 10;pid_t id = fork();if(id == 0){int cnt = 0;while(1){cnt++;if(cnt == 5){val = 55;printf("val change-> 55;\n");}printf("I am child process, id = %d , pid = %d ,&val = %p,val = %d\n",getpid(),getppid(),&val,val);sleep(1);}}else{while(1){printf("I am parent process, id = %d , pid = %d , &val = %p,val = %d\n",getpid(),getppid(),&val,val);sleep(1);}}return 0;
}
对于图片里的结果,父进程和子进程对于一个全局变量val,因为子进程继承了父进程的页表,所以对于某一数据来说,他们的虚拟地址和物理地址都相同。
如果是读取数据,会读取同一片物理空间的数据,不会进行父子进程数据分离。
如果要写入数据,为了保持进程运行独立性,操作系统会为子进程中要修改的数据重新开辟物理空间,并且修改子进程的页表,让修改的数据与父进程的数据不在同一物理空间,虚拟地址相同,这种特点叫做写时拷贝。
对于代码这一类的只读数据,他们不会被修改,因此共享一块物理空间,不发生写时拷贝,对于要修改的数据再去开空间,这样做即节省空间,又不破坏进程独立性原则。
如果对于父进程的整块地址空间进行拷贝,即浪费时间又浪费空间,写时拷贝是一种按需申请。
2.3 地址空间的优点
1.将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
2.进程管理模块和内存管理模块解耦
3.拦截非法请求,对物理内存进行保护
2.4 硬件协同机制
cpu内有相应的寄存器和硬件,来保证进程在访问内存时能快速的将虚拟地址转为物理地址,其中有CR3(存储进程页表指针的寄存器)和MMU(地址管理单元),他们两个配合起来就可以快速进行地址转换。
页表中除了虚拟地址和物理地址的映射关系之外,还存在其他字段,比如是否在内存中的标识位,以及该地址的权限(rwx)。
在父子进程运行中,子进程继承父进程的页表时,操作系统会将可能被修改的数据的权限设为r,在进程修改数据时因为没有w权限而识别到错误,然后进行进一步处理。
操作系统在访问内存时识别到错误:
1.是不是数据不在内存中 ,是的话发生缺页中断
2.是不是数据需要发生写时拷贝,是的话进行缺页访问
3.其他错误进行异常处理
三、Linux 2.6 进程调度
3.1运行队列架构
在LInux操作系统中每一个cpu都有一个运行队列
queue中共有140个队列(task_struct * queue[140]),其中前100个我们不使用,只用后40个,对应40个优先级,优先级相同的进程在同一个子队列里。
进程优先级的值默认为80,用nice值修正,nice值的范围是[-20,19],因此进程优先级的范围就是[60,99],加上40就可以映射到queue队列里下标[100,139]的范围。
位图索引机制
long bitmap[5]; // 160位位图(实际使用140位)
对于bitmap中前140位,若某一位为1,则改位置对应下标的运行队列不为空,即有进程。
位图操作:
-
检查 bitmap[0] 是否为0 → 跳过前32子队列队列,每次检查32位,以此类推
-
非零 bitmap[i] → 按位比较,找到对应子队列去执行
这种遍历方式时间复杂度约为O(1),也称作进程调度里的O(1)算法
双队列轮转机制
struct proi_array_t{int nr_active;long bitmap[5];task_struct * queue[140];}struct proi_array_t array[2];//2个队列,一个活跃队列,一个过期队列 task_struct *active; // 活跃队列(只出不进),直接指向活跃队列的queuetask_struct *expired; // 过期队列(只进不出),直接指向过期队列的queue
- 调度器从 active 指向的队列选取进程执行,使用O(1)算法
- 新唤醒/新建进程加入 expired指向的队列
- 当 active指向的队列空时:
swap(active, expired)