3. 进程优先级
3.1 概念
进程优先级就是指的是进程的优先权,也就是指CPU资源分配的先后顺序!优先权高的进程先执行,优先级低的后执行!这样的目的是为了改善系统性能!
这里权限和优先级怎么来理解呢?权限是能不能的问题,优先级是能但是谁先谁后的问题。
3.2 查看系统进程
指令:ps -l / ps -al
[jyh@VM-12-12-centos study5]$ ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1002 4197 4196 0 80 0 - 29280 do_wai pts/0 00:00:00 bash 0 R 1002 5831 4197 0 80 0 - 38332 - pts/0 00:00:00 ps [jyh@VM-12-12-centos study5]$
这里的UID代表的是执行者身份;PID是此进程的代号;PPID是此进程对应的父进程的代号;PRI知道是优先级别,值越小优先级越高;NI是进程的nice值,也就是优先级对应的修正值,其NI的范围是[-20, 19]。其中有一个PRI的求法:PIR(new) = PRI(old) + nice。另外我们的进程会通过CPU来做计算,CPU中有调度器,这个调度器的作用就是来对进程的优先级一碗水端平,不会过度的使得其进程优先级很高,也不会过度的使得其进程优先级很低,所以这里的NI值也不会很高,这样就可以达到一碗水端平的目的。
那么这里如何修改优先级呢?使用top命令–>按r–>输入进成PID–>输入nice值,进程重新开始后其PRI和NI都恢复原值。另外这里的PRI求法中式子里面PRI(old)始终都是80。
4. 其他概念
竞争性:系统进程数目很多,但是CPU资源有限,所以进程之前具有竞争。
独立性:多进程运行需要独享资源,多线程期间互补干扰
并行:多个进程在多个CPU下分别同时进程运行
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进
5. 程序地址空间
5.1 C语言程序地址空间分布图
我们这时地址空间分布图,地址不是和内存有关系吗,这里的问题是这个程序地址空间分布图是内存吗?其实并不是内存。
那么这里就有很多问题:不是内存它是什么呢?为什么要有它呢?内存在哪呢?
5.2 切入
通过现象来切入话题:
[jyh@VM-12-12-centos study6]$ ls address.c Makefile [jyh@VM-12-12-centos study6]$ make gcc -o myaddress address.c [jyh@VM-12-12-centos study6]$ ls address.c Makefile myaddress [jyh@VM-12-12-centos study6]$ ./myaddress 我是父进程, id:20977, ppid:15064, global:100, &global:0x60105c 我是子进程, id:20978, ppid:20977, global:100, &global:0x60105c 我是父进程, id:20977, ppid:15064, global:100, &global:0x60105c 我是子进程, id:20978, ppid:20977, global:101, &global:0x60105c 我是子进程, id:20978, ppid:20977, global:102, &global:0x60105c 我是父进程, id:20977, ppid:15064, global:100, &global:0x60105c 我是父进程, id:20977, ppid:15064, global:100, &global:0x60105c 我是子进程, id:20978, ppid:20977, global:103, &global:0x60105c 我是父进程, id:20977, ppid:15064, global:100, &global:0x60105c 我是子进程, id:20978, ppid:20977, global:104, &global:0x60105c 我是子进程, id:20978, ppid:20977, global:105, &global:0x60105c 我是父进程, id:20977, ppid:15064, global:100, &global:0x60105c ^C [jyh@VM-12-12-centos study6]$ cat address.c #include <stdio.h> #include <unistd.h> #include <assert.h> int global = 100; int main() { pid_t id = fork(); assert(id >= 0); if(id == 0) { while(1) { printf("我是子进程, id:%d, ppid:%d, global:%d, &global:%p\n", getpid(), getppid(), global, &global); sleep(1); global++; } } else { while(1) { printf("我是父进程, id:%d, ppid:%d, global:%d, &global:%p\n", getpid(), getppid(), global, &global); sleep(1); } } return 0; } [jyh@VM-12-12-centos study6]$
上述现象:我们发现定义的一个全局变量,在子进程对其++操作,但是父进程没有对其进程操作,我们发现的一个奇怪现象就是:这里子进程的global是变化的,但是父进程的global是不变的,这就有意思了,我们不是说全局变量做更改,这个程序都是输出更改后的值吗?这里我们就会想到进程的独立性,这里子进程对全局数据修改了,但是并没有影响到父进程,这里我们再提到:进程=内核数据结构+代码和数据,既然进程具有独立性,那么这里的数据也具有独立性,那么这里怎么使得一个子进程修改了数据的但是不影响父进程的数据的呢,这里就用到了写时拷贝。这里还有一个很重要的现象那就是我们的global变量的地址并没有发生改变,这里居然有写时拷贝又是怎么回事呢,如果有写时拷贝那不就是有两个变量吗,一个是global,另外一个是做逐字节拷贝了global数据的变量,但是你发现这里的地址是完全一样的。假设这里是物理地址,那么可不可能同一块空间上有两份不同的数据呢?答案是不可能的,因为物理地址上是绝对的,也就是一个空间只能存储一个数据。上面我们提到了C语言程序空间分布它不是地址,这里也说明了这个问题,它绝对不是物理地址,它是虚拟地址或者是线性地址。
5.3 正式认识进程地址空间
故事:在学校中,我们通常在初三和高三作业繁忙,初三的时候我们是受家长管制的,但是高中大部分都是住读生,这里就不被管制,初中的时候家里管的严的,一般都是学校作业+回家后妈妈或者爸爸布置的另外的作业,这里一天一天的过去,也面临着此学期的期末考试了,于是妈妈说:你如果考进年级前十,下学期就不给你另外布置作业。此时的你很高兴,心里也想这次肯定考进年级前十,然后过了几天考试,考试后你的成绩下来了,你的妈妈问了问成绩,知道了你考进了年级前十,此时你也很高兴,但是马上一个暑假过去了,又开学了,你很开心,然后去报名上学了,但是到了周末后,你妈妈又给你布置了作业,你又不得不按着照做。这个故事里面妈妈给自己的孩子画的饼就是进程地址空间,而妈妈就是操作系统,这里的妈妈布置的另外的作业就是内存,而孩子就是进程。这里就有个问题:妈妈给孩子画了一张饼,此时妈妈用不用管理这张饼呢?是要管理的。所以对应的操作系统要管理进程地址空间,那么又有问题来了,我们说操作系统做管理是先描述再组织,这里操作系统如何对进程地址空间先描述再组织呢?实际上进程地址空间就是操作系统中的一个内核数据结构,这个数据结构是mm_struct。那么问题又来了:地址空间上的区域的区域划分如何去理解?故事:学校里,有个小美和小黑两个对象,小黑是个很不爱卫生的男生,而小美是个很爱干净的女生,开学第一天就他两就被老师安排在一起做,这时小美很嫌弃小黑,就对小黑画清了界限,说我们两个桌子的三分之二都是我的,你只能在三分之一内活动,这时就对区域进行了划分。在程序中模拟区域划分:struct area{int start; int end;};,那么小美的区域就是struct area xiaomei{0, 4/3 * lenght}; 小黑的区域就是struct area xiaohei{4/3 * lenght, lenght}; 所以区域划分的本质就是对线性区域进行指定start和end来完成区域划分。 如何理解这个地址空间是线性连续的呢?我们在使用visual studio 2019或者2022时,我们调试后打开内存会发现我们的地址空间中的地址是用16进制表示的,32位计算机下范围是[0x00000000, 0xFFFFFFFF],有4G的空间大小,这里的地址说明一下:地址是多少不重要,关键是地址具有唯一性,不能发生冲突,每个地址值对应的就是一个字节。所以因为地址数字是连续的,所以这里说地址空间是线性连续的。那么操作系统知道地址空间是多大吗?是知道的,这个进程地址空间是一个内核的数据结构,进程的结构体PCB中有mm_struct,操作系统有一套64位和一套32位计算机的对应的代码。那么了解上述的区域划分问题后,我们很容易知道地址空间中的各个区怎么来的了:struct mm_struct{long code_start; long code_end; long init_start; long init_end; … };这样就可以对地址空间做划分。这里限定了区域,但是区域内的数据是什么呢?也就是虚拟地址和线性地址。这里调整区域大小也就是对应的调整对象的start和end的指向。了解了地址空间是什么,我们再谈谈进程和进程地址空间以及物理内存的关系,下面我们要了解一下进程地址空间是我们直接用到是虚拟地址,我们找到地址并不是目的,而是手段,我们真正的目的是通过地址找到空间中对应的内容,而这里的内容一定是存在物理内存中的,但是这里地址空间必须和物理内存有联系才能把内容放进对应的地址的物理内存空间中,所以Linux下存在一种技术:页表,它的作用就是把虚拟地址转化为物理地址。除了转换外,还有一个硬件:MMU(memory management unit),集成在CPU中的。对应的关系是:
我们了解到了关系后,再回过头来看5.2切入中的那段代码,对其做出解释:父进程fork一个子进程,这个子进程就对父进程的task_struct进行了复制,但是子进程中对global进程了++操作,此时我们对应的task_struct和父进程是一摸一样的,mm_struct也是一摸一样的,但是在物理内存上,不允许子进程global++操作后影响父进程,所以此时在物理内存上又开辟了一块空间,拷贝global原有的值到新空间,子进程中的页表对应的物理内存存放的就是这个新空间的地址,简单来说就是父子进程的mm_struct并不改变,改变的是物理内存空间,也就是父子进程互不影响,进程具有独立性的表现之一。那么上述切入中的代码我们所观察到的是父子进程可以同时运行,但是id是一个变量,那么为什么会有大于0和等于0的两种情况同时出现,也就是因为:fork在返回的时候,父子进程都有了,return了两次,返回的本质就是写入,那么谁先返回,谁都会让操作系统发生写时拷贝。
5.4 扩展
虚拟地址空间为什么要存在?
如果没有地址空间,操作系统是如何工作的?没有地址空间的话,当我们多个进程加载到内存时,其中有个进程时访问地址的操作,然后给到CPU,CPU返回给这个进程一个不是本进程而是其他进程的地址,这样假如我们本进程要进行写入或者删除的操作,这样就会影响到了其他进程,这样就无法保证进程的独立性。所以就引入了页表和虚拟地址空间这样的东西,这样就会保证进程的独立性和安全。小结:地址空间的存在的意义:1. 防止地址随意访问,保护物理内存和其他进程 2. 将进程管理和内存管理进行解耦合 3. 让进程以统一的视角看待代码和数据
malloc的本质是什么?
我们向操作系统申请空间,操作系统是你在需要的时候才会给你,而不是立马给你。为什么呢?因为操作系统是不允许任何的浪费或者不高效的行为,也就是说在你申请成功之后和在你使用之前,这块空间有一段的闲置状态。操作系统怎么样不让空间浪费呢?这里的首先是在虚拟地址空间申请空间,然后对应的虚拟地址放进页表,但是没有映射处物理地址,物理内存上也没有申请空间,这就是缺页中断。
重新理解虚拟地址空间
首先抛出一个问题:程序在被编译的时候,没有被加载到内存中,那么程序内部有没有地址呢?是有的。那么这里就有点奇怪,源代码被编译的时候,就是按照虚拟地址空间的方式进行对代码和数据就已经编好了对应的编制。不要认为虚拟地址这样的策略只会影响OS,它也会让编译器遵守对用的规则。CPU中读取数据对应的地址,这个地址是虚拟地址还是物理地址?还是虚拟地址。