前言
1. Linux真实的调度算法
首先cpu中有一个叫做runqueue的东西,这个东西就是去弄进程的调度的,里面有很多东西,这里我们就写这些了
其中task_struct*这个数组指向的是140个元素,其中0~99就是系统默认的进程,后面的四十个才是我们用的,这也是为什么进程优先级只有四十的区间了
这个就是一个哈希桶
这里task_struct和nr_active和bit_map共同组成了一个结构体queue,然后这样的结构体还有两个,构成了一个数组,queue[0],queue[1],struct queueactive指向一个,expired指向另一个
active指向的那一个,就是正在运行的那一堆进程,运行完了,或者时间片到了,就运到expired,或者有新的也运到expired
其中nr_active是记录active里面的进程数量的
当nr_active为0时,才切换active为expired,然后继续运行
然后bit_map这个是位图,是用来看这些地方还有没有进程,不然挨着挨着遍历看有没有进程,速度有点慢
bit_map每个元素是int,然后int是32个比特位,这样325=160,就足以覆盖那140个了
遍历是否有进程的时候,就可以这样,先看一个int类型是否为0,为0就跳下一个,不为0,就把int的32个比特位挨着遍历,就知道哪里有进程了
一个arr元素,后面连着很多task_struct,然后task_struct中又是这样连的
task_struct里面有一个link,它只有两个指针,一个pre,一个next
通过这个来相连,然后通过局部的next指针,就可以访问其他属性了
(task_struct)0->pre这个pre的偏移量x,然后真正的pre减去x就可以了
(task_struct*)(pre-x)->就可以访问了
2. 命令行参数
其实我们在写main函数的时候,还可以增加这两个参数
注意如果版本没有达到C99的话,是不能使用for里面定义变量的,
在$^的后面加上-std=c99就可以了
看这个我们就知道了,我们在输入命令的时候,有几个参数,argc就为几,空格与空格间为一个,然后这些命令会存在argv中,这也是为什么一个命令常常后面跟一些选项了,比如ls -l -a因为它就可以通过argv来进行不同的操作了
如果argc为0,那么就执行main()
不为0,就执行main(argc,argv)
3. 环境变量
除了前面两个参数,还可以加第三个参数
这就是环境变量,因为环境变量最后一个是以NULL结尾,所有可以这样干
或者命令行直接输入env,这样也可以查看环境变量
3.1 PATH
接下来我们来讲一下为什么我们要./test才能执行呢,为什么不可以直接test呢,这是因为,命令都是要去bin路径那里去找的,你输入test就是默认去bin找了,所有不行
所有的命令都是这样的,有一个默认的查找路径,就是PATH
如何查看PATH呢
第一种不行,因为会默认为打印这个PATH字符串
第二种就可以了
这就查找路径
每种路径之间用冒号分隔开
如何修改路径呢
直接PATH=就可以修改路径了
修改完之后,好多指令都不可以用了,但是还是有些可以用的
因为路径被全覆盖了,但是没有影响的,我们把机器关掉再打开,就和原来一样了
这样一来就覆盖不了了
而且还可以使用我们的程序,不用./,但是要注意一下,因为系统默认有test的指令,所有我们的还不行暂时
改一下名字就可以了
为什么一关掉xshell,路径就恢复了呢,因为这个PATH是内存级别的,所以你关掉程序就没了,只有文件级别的,关掉才会继续有
其实是这样的,shell里面有一个和环境变量相关的配置文件,一启动程序xshell,就才会去配置环境变量
所以我们只需要改配置文件,PATH就会不变了,关掉xshell也不会变
我们回到我们的家目录
注意这两个,这两个就是配置文件
在pash_profile中你就会发现,PATH就在这里面
修改就可以了,而且下次登陆还是一样的
3.2 USER
记得我们原来讲进程的时候说过,进程有一个属性就是uid,是用来看它是哪一个用户创建的
这是怎么判断的呢,其实也是环境变量,环境变量记录了uid
最底下有个USER=ck这个就是得到uid的来源,每个用户的环境变量都不同,uid也不同
3.3 HOME
我们切换用户,用户的HOME环境变量就变了说白了就是变成了家目录
HOME就是查看家目录
这些环境变量都是存在bash中的,bash也是一个进程,每个用户都有一个bash
然后这也是bash的工作路径,然后所有的进程都是bash进程的子进程,然后所以进程都会继承bash,所以说所有的进程的工作路径肯定都是属于bash的进程的工作路径的,都是在它里面的
这个是查看我们shell这个版本的
3.4 PWD
pwd是查看当前进程的路径的
3.5 getenv
getenv这个函数也可以获得环境变量,去搜索的,传入PWD字符串,然后就会返回PWD字符串对应的环境变量后面=的内容
注意如果没有找到的话就会返回NULL
3.6 LOGNAME
看些我们可以知道,USER和LOGNAME是一样的,su -会切换到超级用户,su也会切换到超级用户,两个的区别是什么呢,区别就是su不会改变环境变量,只会改变用户,su -就会切换环境变量
然后logout就会退出当前用户,返回上一次的用户,也会改变环境变量
logout对应su -,,,,su对应exit
3.7 OIDPWD
这个OLDPWD就是用来记录上一次的路径的,这也是cd -的实现原理
3.8 本地变量
环境变量其实就是一个具有全局属性的变量
因为所有的进程都会继承它,同样也会继承它的环境变量,所以所以进程都有一样的环境变量
而在命令行创建的本地变量,只属于bash,是无法被子进程继承的
i=10
这个i就是本地变量
echo $i这个不是打印i,而是打印i里面的内容
本地变量必须是什么=什么的形式
3.9 set
set就是用来显示我们的环境变量和本地变量的
3.10 export
这个就是用来直接把一个本地变量打入环境变量的
有两种打印方式
3.11 unset
这个就是用来删除某个环境变量的
这样里面就没有这两个环境变量
3.12 全局性
这个就是bash进程,这个是由操作系统创建的,它里面有几个表,有环境变量表,有命令行参数,还有本地变量
但是由它创建的子进程就没有本地变量表了,就不能使用本地变量了
只有打入了环境变量,才可以使用
3.13 environ
下面我们来介绍这个函数,不,这不是一个函数,这是一个指针,指向的是环境变量表,是一个二级指针
使用这个前要声明
就这样我们利用这个指针也可以访问环境变量了
4. 进程地址空间
4.1 现象
先看一个现象
看这个现象我们就有疑问了,因为以前说过,子进程和父进程的数据是各自有一份的,但是为什么会地址相同呢,这个就不科学了
其实这个根本不是物理地址,而是虚拟地址,如果是物理地址的话,肯定不科学啊
4.2 空间布局
正文代码就是放代码的地方
为什么有共享区呢,因为栈和堆是会变大的,栈向下扩展(函数调用),堆向上扩展(开辟空间)
内存是如何划分区间的呢,其实只需要记录那个区间的起始地址和末尾的地址就可以了
4.3 解释原因
先解释一下,task_struct中有一个指针,指向一个task_mm_struct结构体,这个结构体就是虚拟地址,它记录了每个物理地址区间的起始位置和末尾位置,这样就可以按照不同类型来存了
然后每次创建变量的时候就会在区间里面取一个地址,然后指向一个页表的东西,指向的就是页表的虚拟地址那一行,然后对应页表物理地址那一行才会去指向实际的物理地址
比如gval,task_mm_struct里面的虚拟地址0x1111,对应实际的物理地址0x1234
又因为子进程会继承父进程,会继承所有
task_struct会继承,task_mm_struct会继承,页表也会继承,也是一模一样的
如果不修改gval的话,继承过来,子进程的虚拟地址和父进程一模一样的,物理地址也是一样的,如果要修改的话,那么子进程就会修改物理地址,自己重新去指向一个空间,这就是写时修改
所以我们程序打印的是虚拟地址,所以才是一样的,但其实物理地址是不不一样的
所以这里我们完善一下,进程=内核数据结构+自己的代码和数据
内核数据结构包括task_struct+task_mm_struct+页表
因为每个东西基本都是各自有一份,所以是独立的,代码共享不影响独立,数据开始共享,修改的话就分开,所以很独立
4.4 页表
页表中,还有一些标志位,这里我们只举出两个
一个是对物理地址的读写权限
char *str="hello bit";
*str="H";
比如这个,我们这样写,编译阶段不会报错,但是进程运行的时候就会报错,这个是因为"hello bit"这个在已初始化区域,这个是只有读的权限,没有修改的权限,这里的报错是系统报错的
const char *str="hello bit";
*str="H";
这个就是在编译阶段就会报错了
接下来我们来讲一下第二个isexists
在此之前我们先说明一些事情
首先我们很早就说过了,可执行程序是在磁盘中的
当可执行程序很大的时候,就会分批加载,意思是只加载一部分
或者挂起状态的时候,把可执行程序加载到磁盘的swap分区了
这样的话,可执行程序就可能没有完全加载到内存中,那么在代码区通过页表访问物理内存的时候就会访问不到,就会出错,有了isexists的话,取访问的时候,就会告诉你不可访问,然后就会去磁盘中加载到内存,其实就是一个需要才去加载的操作
4.5 task_mm_struct的初始化
task_mm_struct是怎么初始化的呢
首先,肯定是先创建task_struct和task_mm_struct,再加载代码的
然后也是先销毁代码
我们先讲一下exe可执行程序,这里面不仅包含了代码,还有一些属性,比如每个区域所占大小,那么就可以根据这个属性来初始化task_mm_struct,就不用先加载代码了,然后就是边运行变加载代码
这个命令就可以查看每个区域的属性了,什么大小,什么都有
4.6 为什么要有进程地址空间
第一可以保护内存,万一你要访问一个野指针,但是对应没有这个虚拟地址,那么就会报错了
第二因为有了页表,所以进程管理和内存管理就不会直接关联,那么这就有利于解耦合
第三就是让进程以统一的视角看待物理内存
意思就是代码和数据按理说可以加载到物理内存的任何位置,因为虚拟地址和物理地址是一一对应的,所以不管怎么放,都能找到,但是一般是连续存放的,因为这样访问效率高,就和链表一样,你访问链表的效率比数组低的原因一样
总结
上面这些就是我们讲的进程的一些基本知识,下一讲会将一些进程的一些实操,当然也少不了知识点