目录
一冯诺依曼
在我们日常中常见的计算机,笔记本,大都遵守着冯诺依曼体系去设计的!
1结构
上面所列举的结构来解释下:
输入设备:键盘,鼠标,摄像头,磁盘,网卡...
存储器:内存
运算器,控制器:CPU
输出设备:显示器,声卡,磁盘,网卡...
要谈进程就先说说上面图中的红色箭头——数据信号
输入设备接收到数据信号,指向存储器,通过CPU处理完成后返回到存储器,它再给到输出设备,最终到达另一个冯诺依曼体系(计算机)。
数据的流动要经过各种设备的加工处理,说明结构之间是要相互链接的
一个设备到达另一个设备,本质是一种拷贝!!
而数据设备之间拷贝效率,决定了计算机整体的运行效率!
2存储金字塔
在CPU内部又有很多寄存器,由于内存效率等的不同,呈现一个类似金字塔(规律):
存储:距离CPU越近,效率越高,成本越高。
在上面我们从结构的角度谈了数据流动的过程。现在,从硬件的层面了解数据流动:
(冯诺依曼体系)
1CPU不和外设打交道,只和存储器(内存)打交道
2外设(输入和输出)的数据,不是直接给CPU去处理的,而是要先放到内存中
这里,你可能会有理解了:程序运行,为什么要加载到内存中
程序=代码+数据:程序运行的过程确实是被CPU访问的过程
而程序在没有加载到内存,通常是在磁盘(外设)里以二进制的文件形式保存
说到底,原因总结为一句:冯诺依曼体系结构规定的!
3早期结构
而在早期冯诺依曼结构还没有出现之前,是没有存储器这一概念的。
当时的CPU是直接从输入设备读取数据的,而因为输入设备处理数据的时间长,CPU处理数据时间快,造成时间差:CPU要等待数据的传入...从而影响计算机整体效率。
解决问题之一:把计算机所有的硬件更换成与CPU处理数据同一等级,直接消除CPU要进行等待的问题,但这势必会引起下个问题:造价太贵,计算机只会被少数人使用。
而冯诺依曼这个人为了解决全部问题,才有了冯诺依曼这一体系结构,使平民老百姓人人都能够买得起电脑,使用的人多了,形成了如今互联网的快速发展。
虽然互联网能够得以快速发展不止是只有一体系结构这一因素,还有后面的比如:图灵计算机明显等等...但冯诺依曼是值得被铭记的!!
4解释场景
你在广东,你的朋友在北京上大学,你们现在都在网上通过QQ进行聊天,当你在QQ聊天框中发送一句:你好。请解释一下,整个消息的数据流动。(不考虑网络)
数据从你的电脑的键盘中输入,接着加载到内存中,通过QQ,CPU对数据的处理加密成二进制,通过网卡,(网络)进行传输。
你朋友的电脑通过网卡进行接收,把数据加载到内存里,通过QQ,CPU的解密完成对数据的分析,最终将数据显示到显示屏上,最终看到你好这个消息!
二操作系统(OS)
1概念
操作系统是软硬件资源管理的软件
广义的认识:操作系统的内核+操作系统的外壳周边程序(给用户提供使用操作系统的方式)
比如安卓手机:底层是Linux内核,上层是供用户使用的图形化界面
狭义的认识只有操作系统的内核
我们平常在打开电脑等待的时间就是在加载操作系统这一软件的运行
2为什么要有操作系统?
1从概念入手:对软硬件资源进行管理,是一种工具
2引申:以人为本,给用户提供一个良好(稳定,安全,高效)的运行环境
3结构示意图(不完整)
操作系统体系结构呈现层状划分结构:
既然操作系统要对软硬件资源进行管理,那么在设计时内部必定是很复杂的。如果直接让用户接触操作系统,很容易出现:五分钟一蓝屏,十分钟一重启。所以在设计时,用户与操作系统之间隔着
“一道墙”:图形化界面/shell指令,点击/使用指令间接使用操作系统才合理。
那么在管理硬件时也要有对应的‘墙’——驱动层,而驱动层就不是操作系统来提供的,而是各大硬件生产厂商来自定义的,这让我们在选择硬件方面也有更多的选择,而不是单一化!
4理解操作系统
既然系统是对软硬件资源进行管理,那什么是管理呢??管理的目的是什么呢?
在大学里,你的身份是一名学生同时也是属于被管理的对象,而管理你的人是你的辅导员。还有学校的校长。这可能要说了,学校的校长在整个大学期间基本上也就开学典礼见过面其它时候就没见过面了怎么可能管理着我?
但你想想看,管理得好不好真的一定要管理者与被管理者直接接触吗?
当然不是,校长管理着你们,实际上是管理着数据——教务系统的学生信息。
也就是说,管理本质上是对数据进行管理!!
当校长想找到某届绩点最高的一名学生名字,他可以在教务系统里去找绩点最高对应的学生名字来。如果是十几名学生的话管理起来也很容易,学生毕业了删除对应的信息很容易。
但随着被管理学生越来越多,人的信息也变得越来越多,校长很难管理。在这时校长想起自己有学过一点计算机,会写代码。他就用自己的知识以及想法将每一名学生的数据用一个结构体类型存储起来:struct stu{ char name[16] , char sex , char add[123]... struct stu* next},在结尾处存储一个struct str* next,将每一名学生的数据用链表的形式连接。
从此:校长对学生的管理工作,变成了对链表的增删查改!!
聪明如你:也许发现了,上面所提到的校长其实就是操作系统。而操作系统在管理软硬件内部采用的方法是使用各种容器(数据结构),这也从侧面说明我们为什么要学习数据结构这一学科的重要性
上面的内容总结一句话:先描述,再组织!
5结构示意图&&再谈操作系统
理解上面操作系统的几个点,看到下面操作系统完整的示意图时,或许你会有一个豁然开朗的感觉:
在这里补充一下:在上图操作系统上还有着一层system call(系统调用接口),这个就类似我们在学C语言时函数的使用一样:XXX函数(接口)对应实现XXX功能(作用)
这个系统调用接口也和我们日常中去银行进行办业务时去对应的银行柜台窗口类似:
银行不可能说:自己去把钱放到银行保险柜中,放了的多少钱就去银行系统里自己加上现金余额。
银行不相信任何人!!所以它给人们提供了业务办理窗口满足对应的需求
三进程(Linux)
在操作系统中,进程可以同时存在非常多!在上面我们通过推到总结出:管理:先描述,再组织。管理中又有数据结构的加入。那么在OS中是否也是呢?
当然,每一个进程都要有一个名为PCB的数据结构,当中存储着所有属性,next指针连接下一个进程和一个内存指针(维护代码和数据)
当我想对Processbar.c的代码进行执行之前,它先是以二进制的形式保存在磁盘中。执行时,先把文件的代码和数据加载到内存里,OS会创建一个结构体PCB来维护Processbar.c的所有数据,安排它进行排队,等候被CPU执行。
这时你说了:那进程不就=自己的代码数据吗?
那我问你:你是任何证明你是你们学校的学生呢?通过学生证?那别人拿你的学生证来冒充你不是绰绰有余?而是通过你在学校的教务系统中来证明!
所以:进程=PCB+自己的代码和数据
其中PCB时OS的统称,在Linux中PCB=task_struct时具体的称呼
一个进程中要有自己的PCB,为什么??
先描述(属性值)再组织(PCB的管理)方便操作系统进行管理
对进程的管理转换为对链表的增删查改!!
1调度运行进程
OS对进程的管理:
调度运行进程,本质上就是让进程控制块task_struct进行排队!!
接着再来理解进程动态运行:
我们的进程将来在不同的队列中进行排队等待被CPU的执行,这便是进程的动态运行
2PCB的内部属性
了解了进程的基本概念,现在让我们来聊聊task_struct(PCB)的内部属性有哪些?
周边知识
学习指令时,我们只知道./文件名是对可执行程序进行运行。但学习了进程,就知道了:这本质上
就是让系统创建进程并运行。指令本质上不就是一个个C语言代码写的可执行命令吗!
系统命令=可执行文件
2.1pid
每一个进程都要有自己唯一标识符,叫做进程pid
查看自己的pid:getpid()获取
结束进程:ctrl+c在用户层面上终止进程,kill -9 pid可以用来直接杀死进程
2.2进程创建的代码方式(C语言)
查看父进程的pid:getppid()
int main()
{
printf("ppid: %d\n", getppid());
return 0;
}
编译形成可执行程序然后运行它:
每次执行程序时,pid不同正常吗? 正常
每次执行的父进程都一样,它是谁??它就是我的bash——命令行解释器
也就是我们在权限文章说的——媒婆,媒婆招募实习生对应的是指令执行bash创建子进程
那如果我们想在C语言创建父子两个进程,有没有办法呢?
C语言给我们提供了一个函数fork(),fork()之后创建子进程
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
sleep(1);
printf("I an father process\n");
fork();
sleep(1);
printf("hello warld\n");
return 0;
}
使用指令查看当前文件进程可以用:
while :; do ps ajx | head -1 && ps ajx | grep 可执行程序名 | grep -v grep ; sleep 1; done
那子进程的数据和代码是从哪来呢?
没有特殊情况下,默认是从父进程继承下来的代码和数据,而系统要为这个子进程创建一个进程,即:在队列中多一个task_struct结构体内核来指向子进程的代码和数据
虽然创建出来了父子进程,那创建出的子进程有什么用呢?
实际创建出来的子进程是想让它执行与父进程不同的代码来达到某种目的的!!
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id=fork();
if(id==0)
{
while(1)
{
printf("I an child process\n");
sleep(2);
}
}
else
{
while(1)
{
printf("I an father process\n");
sleep(2);
}
}
return 0;
}
这里的id为什么即可以==0又可以>0
因为我们学习的语法中大多数是单进程,而在fork()函数内部指向的是多进程,即:在内部中父子进程是已经可以被调度了,fork()结束后自然父子进程会各自return一个值:
fork()规定:子进程return的值是0,而父进程return的值是>0(虚拟地址空间与写时拷贝)。
fork()之后代码被父子进程所共享,自然就能理解为什么id可以是多个值了。
2.3系统下的进程
当一个可执行文件运行时,系统会为我们根据进程的pid在proc目录中创建以pid命名的目录,当程序结束,对应的目录就自动进行销毁。
在程序执行时进入该目录上能看到两个发光的文件:
cwd:current work dir表示程序在当前工作路径(可用chdir(路径)进行路径的更改)
这个就像我们在学习C语言文件操作用函数fopen进行写入一样,如果没有这个文件会在当前路径下新建!
exe :进程的PCB会记录自己对应的可执行程序的路径
程序执行结束时:
3进程状态(Linux)
在进程的task_struct(PCB)中有一个内部属性:int status来记录当前进程的状态。
进程状态有以下几种:
R:进程正在运行
S:休眠状态
D:进程深度睡眠,不可被中断(Linux中独有的一种状态)
T/t:让进程暂停,等待被进一步唤醒
x/z:进程结束(死亡)
3.1状态演示
R状态
#include<stdio.h>
int main()
{
while(1)
{
printf("hello warld\n");
}
return 0;
}
对应的进程已经是在跑了,为什么状态是S+?
因为CPU处理代码速度相比于外设来说很快,可能几纳秒就完成了。而数据往显示器上打印一次可能需要几微秒。时间出于相同的概率很小。这就造成在打印printf的时候,进程是在等待你显示器把数据打印完,也就是进程处于S状态
把循环里的代码都去掉后:
S状态
了解了上面的情况,就可以说明:S休眠状态其实是:进程在等待“资源”就绪,而这种状态是可以直接终止掉的(kill -9 pid),我们把这种休眠状态叫做:可中断休眠
D状态
既然有可中断休眠,那么就有会有不可中断休眠——D深度睡眠状态
往往在内存空间不足时,操作系统有权力杀掉进程来释放内存空间。
而我们要讲的D状态是不可被杀。如:进程中往磁盘中写入数据时就是D状态,如果进程被杀掉了,数据丢失该怎么办?
T/t状态
进程在执行时遇到要访问外设资源时(调试或者是scanf向键盘读取数据)要将进程进行暂停:
4僵尸进程
子进程已经运行完毕要进行退出时,需要把自己的退出信息交给父进程才算是正常。
但如果子进程一意孤行没有将退出信息交给父进程,这就会产生僵尸进程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int a = 0;
int cnt = 5;
while(cnt)
{
sleep(1);
printf("I am a process, pid: %d\n", getpid());
cnt--;
}
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, cnt: %d, pid: %d\n", cnt, getpid());
sleep(1);
cnt--;
}
}
else
{
// parent
while(1)
{
printf("I am parent, running always!, pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
如果一直没有被父进程读取,僵尸进程会一直存在!
这会导致子进程的数据没有得到回收,就会造成内存泄漏
解决问题:用wait()接口来接收子进程的退出信息,这到进程控制的内容在详谈
5孤儿进程
父进程如果先退出,子进程会变成孤儿进程。孤儿进程一般都是要被1号进程(OS)所领养的
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int a = 0;
int cnt = 5;
while(cnt)
{
sleep(1);
printf("I am a process, pid: %d\n", getpid());
cnt--;
}
pid_t id = fork();
if(id == 0)
{
// child
while(1)
{
printf("I am child, cnt: %d, pid: %d\n", cnt, getpid());
sleep(1);
}
}
else
{
int cnt=5;
// parent
while(cnt--)
{
printf("I am parent, running always!, pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
为什么OS要领养子进程?
与僵尸进程一样,为了保证子进程能正常回收
在上面两个进程中,为什么我们在平时启动的进程中从来没有关系过?
答:不需要:进行命令行指令的进程,它的父进程是bash,bash会自动进行子进程的回收的
四进程(OS)
在操作系统中,进程状态分为:创建,就绪,执行,阻塞,终止状态
创建状态在Linux中没有该状态的,这个我们不谈
1就绪与执行
在Linxu中,进程 = task_struct + 进程的代码和数据
在task_struct中,有着进程的大部分属性:调度相关的信息,时间片等属性是包含在结构体里面。
(OS调度算法的一种)进程要被CPU调度时需要进行等待,即:进程需要在OS形成的等待队列里进行排队
但在Linux中不是这么调度的,它执行的是分时调度算法,讲究的是公平性。
所以,进程在等待队列里进行排队等待被CPU调度,我们就认为它此时的状态是就绪,运行状态:也就是Linux中的R状态。(进程已经是准备好可以随时被调度了)
这里就会产生一个问题:一旦进程持有CPU会调度到它结束为止吗?
不会的:这种调度是基于时间片进行轮转调用的,时间到了就换下一个进程进行。
让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码,这种过程就做并发
任意时刻,有多个进程真的在同时运行,这种过程叫做并行
2阻塞态
写代码时scanf(“%d”,&ret)要从键盘中读取数据进行写入,本质上是该进程在等待外设(键盘)资源是否就绪。在等待的过程中OS会把过程给挂到指定等待队列(键盘)中,这个过程就叫做阻塞态。
而OS对硬件的管理是上面所提到的:想描述,在组织。用链表的方式将他们统一进行管理:
例如:进程执行到scanf()时发现在管理键盘的结构体中没有数据的写入,OS会把它该进程(task_struct)放到该结构体的等待运行队列中进行资源的获取。当资源成功获取完后,就把进程重新连接到运行队列里,从scanf()后的代码进行处理!
特别注意:阻塞与运行的状态变化,PCB会在不同的队列中进行移动,是PCB结构体的移动而不是进程的代码数据在移动!!!
而阻塞状态在Linux中对应的是S,D状态
3挂起
使用过虚拟机的都知道:在选择状态时,出来重新启动与关机外,还有个挂起的功能:
选择挂起后虚拟机中的数据都暂时被保存到磁盘(swap分区)中,但进程的PCB还没有被OS回收,等到下次要用时在进行将数据唤入。
为什么要有挂起这种状态呢?(在我们用户的视角几乎是感受不到的)
主要是在OS内存比较少时,通过挂起能够更有效的使用内存资源,这本质上时用空间来换时间的做法
那在对应磁盘挂起时所放的位置(swap分区)能不能扩更大呢?这样不就能更好进行内存管理吗?
不行。这会导致OS非常依赖这块空间,频繁进行资源交换,导致效率问题。在用户层的感觉时越用越卡
而终止状态不用多说,对应的是Z,X状态
以上就将我们在Linux中的状态在OS角度下进行对应的归类完成。
4进程切换
进程被CPU调度的过程,如果时间片一到,它会立刻调度下个进程。而未完成的进程在CPU内部势必是要记录下执行到那一步了好让在下一次调度中从那一步中开始执行。
在CPU内部中有非常多的寄存器,寄存器就会去保留上个进程还未执行完的临时数据,这也就做就进程的上下文。
所以在切换进程中最重要的一件事是:上下文数据的保护与恢复
5关于CPU寄存器:
寄存器本身是硬件,有一定的存储能力,CPU的寄存器硬件只有一套!
CPU内部的数据,可以有多套,有几个进程,就有几套和该硬件对应的上下文数据
总结:寄存器!=寄存器的内容
五优先级(Linux)
1是什么
指定进程获取某种资源的先后顺序
task_struct(PCB)有一个int prio代表的是一个进程的优先级:
优先级数字越小,优先级越高
权限VS优先级:
前提条件是你有访问资源的权限才来谈优先级!
2为什么
或者说:优先级的作用是什么?
1进程在访问资源(CPU),资源是有限的
2进程的情况有很多:比如进程要访问多种资源来进行任务的完成,不可能一直占有CPU
3操作系统关于调度和优先级的原则:分时操作系统来达到基本的公平。如果进程一直得不到调度,会产生饥饿问题。
3优先级的特点&&查看方式
PRI指的就是一个进程的优先级,而NI是优先级的修正数据,即:新的优先级 = 优先级 +Ni (nice)能使一个进程达到动态修改进程优先级。
而优先级的范围在[60,99]之间,所以nice值并不能让你去任意修改,它也有范围:[-20 , 19]
而一个进程的优先级默认值是:80
4修改优先级的指令:
top
进入top后按“r”–>输入进程PID–>输入nice值(修改多次要root或sudo不建议修改)
5引申概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
六命令行参数
1是什么
在平时写代码时,总是要写int main() return 0才进行写,main函数基本上是不用进行传参的,但它也能带上参数来写:int main(int argc , int *argv[]),那这两个参数所代表的是什么呢?让我们通过代码演示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[], char *env[])
{
char *path = getenv("PATH");
if(path == NULL) return 1;
for(int i = 0; env[i]; i++)
{
printf("env[%d]->%s\n", i, env[i]);
}
return 0;
}
其中:-a -b是执行该进程匹配的选项,一般默认是要输入给父进程bash的,而bash会为我们将这些字符串(选项)用临时一张表的形式给维护起来,称为命令行参数表:
2为什么
了解了命令行参数,那它出来后有什么用呢?
从本质入手:命令行参数本质是交给我们程序有不同连续来定制不同的程序功能。像我们学习的一个指令能够有不同的选项来供我们进行选择(多样性)
3谁干的
#inlcude<stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 10000
int main()
{
pid_t pid = fork();
if(pid == 0)
{
while(1)
{
printf("I an a child process , pid:%d , ppid:%d",getpid(),getppid());
}
}
else
{
while(1)
{
printf("I an a father process , pid:%d , ppid:%d",getpid(),getppid());
}
}
}
我们的父子进程打印出来的g_val是一样的说明:父进程的数据,默认是能被子进程看到的;
在命令行启动的程序,bash会创建子进程来执行程序。在执行程序之前就有人帮我们去维护了命令行参数表,这个人就是:父进程bash!!
七环境变量
在Linux中有这样的全局的设置,来告诉bash应该去哪里寻找可执行程序,这种全局变量的设置我们就作:环境变量。PATH就是其中的一种
1现象
系统中有很多的配置(像上面的),在我们登录Linux系统时,已经被加载到了bash进程里(内存)
所以现在我们想让上面的process执行时像指令一样不用带./来执行程序
指令:PATH = (程序路径)
再次执行时就不用带./来运行。但是你会发现:但我要运行系统的指令时:ll -a 程序文件就不能够执行了:
这是我们不用慌,仅需要将程序退出重进就恢复了:
这是这么回事呢?
因为我们直接把PATH环境变量修改成对应程序路径将系统指令user/bin的程序路径给覆盖了
而重启Linux就恢复了,因为默认用指令查的环境变量都是内存级的。
要解决每次开启Linux和使指令都能运行这两个问题,我们就需要找到系统对应的配置文件中来将我们的程序路径给添加进环境变量里。
vim .bash_profile
将程序路径与前面路径格式一样添加到PATH前面即可完成不带./进行程序执行
2更多的环境变量
env:打印shell内部环境变量
echo $xxx 打印xxx的有关内容
export name=value 写入变量name的环境变量(本地变量)
unset name 取消环境变量
3理解环境变量
在C语言中,有char ** environ来获取该进程的环境变量,接下来让我们写个代码来获取下环境变量:
我们对比发现:这些环境变量不就是刚刚用env查到的shell内部的环境变量吗!
我们知道:./运行程序shell是要创建子进程来进行的:既然两者的环境变量一样的,那就说明环境变量默认也是可以被子进程拿到的!!说明:环境变量是全局的。
4理解环境变量
用env查询了shell内部环境变量,发现环境变量有几十种。那么在bash内部是任何进行组织的呢?
在Linux还未启动时,配置文件(里面包含着环境变量)保存在磁盘里,但Linux启动时,OS会在内存中开辟一块内存同时将配置文件加载进来。
bash面对几十种环境变量,选择(指针char * env[])开辟重新开辟内存存储env并在最后一个env的后面跟上NULL表示结束:
而我们在main函数中打印环境变量用char **environ来找到env进行打印的!
而这种存储环境变量的这块内存我们叫做环境变量表!!
这看着是不是有点眼熟:这与我们的(命令行参数存储方式)(命令行参数表)是一样!
从此我们可以这样来理解:Linux在启动的时候,会默认子进程形成两张表:
argv[]命令行参数表(用户输入)+env[]环境变量表(配置文件){以各种方式来交给子进程!!}
我们也可以通过main参数(char * argv[])将环境变量表打印出来这里就不演示了
总结:环境变量具有系统级的全局属性,因为环境变量本身会被子进程继承下来
5内建命令
通过上面的认识,我们知道:执行指令时bash会创建子进程来执行程序,
那上面的指令执行完后,子进程的环境变量多了个hello。这个环境变量不是不应该被bash看到的吗?为什么查询bash内部的环境变量里会有它?
像echo,export,unset这些指令,我们称为内建命令,是bash“亲自”来执行
有80%指令需要创建子进程来执行,但也不能忘记20%的内建命令是bash来执行!!
八地址空间
1问题
执行以下代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int g_val = 100;
int main()
{
pid_t id =fork();
if(id==0)
{
g_val =200;
while(1)
{
g
printf("I am childprocess,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid (),getppid(),g_val,&g_val);
sleep(1);
}
}
else
{
while(1)
{
printf("I am a fatherprocess,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpi d(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}
执行以上代码我们发现:父子进程的g_val值不同,地址却是相同的??
2分析
首先我们要明白:父子进程是具有独立性的,互不影响各自进程的执行。
而进程=内核数据结构(task_struct)+代码和数据,子进程默认是继承父进程的代码数据的。而上面的上面代码中子进程把进程中的全局变量给进行修改了,OS肯定是要做些事情来维护进程之间的独立性的,即:写时拷贝。而子进程打印出来的g_val的地址就肯定不是真实的,内存空间的地址。而是虚拟地址!
3进程地址空间
我们之前在学习C语言时,说的程序地址空间是不准确的,应该是进程地址空间(在OS内部)
在学习计组时,我们或许在教程上看到在进程地址空间中数据段的具体分布:
代码不运行时,默认是以二进制文件形式保存在磁盘里。
但你要执行代码时,OS会将其中的代码和数据加载到内存里,还会设立进程PCB,进程PCB中将执行的代码和各种数据依次放到对应位置的地址空间里,形成各种地址(虚拟地址),在其中会有一个页表的数据结构对象来帮助我们将虚拟地址与物理内存建立联系,形成映射关系,来访问我们加载到内存里的代码和数据
而当父进程里创建了子进程也是如此:将父进程的PCB,地址空间,页表各自拷贝一份就行相同的操作。
OS检测到子进程对全局变量g_val就行修改时,在页表中会中断g_val所指向的内存地址空间,重新在内存里申请空间,拷贝g_val的数据,再继续将g_val继续修改;而页表也会重新进行对应的映射关系。这个修改的过程我们称之为写时拷贝
4细节问题
Ⅱ 写时拷贝的发生时OS根据需求按需申请内存空间自主完成的
Ⅲ 如果父子进程不写呢?还要写时拷贝?
>那么在未来的一个全局变量,默认是被父子进程共享的,而代码是可读的(不可写)
>>这时在OS中就不需要在创建子进程时把父进程的数据都拷一遍,本质上OS是在“赌”你使用子进程没有修改到代码数据而决定的。当子进程有做出修改的行为在进行写实拷贝!!
>>>OS通过调整拷贝的数据顺序来达到有效节省空间的目的
Ⅳ 页表的作用不是只有地址空间与内存空间联系起来的桥梁这么简单:
我们平时说的常量具有常属性不能进行修改。但它为什么不能修改你知道吗?
原因是常量通过页表找到内存空间地址时页表会给上w权限表明你只有读权限没有写权限(修改);但你在进行修改它时OS从该内存地址中查看页表看你是否有修改的权限!
5理解地址空间
a什么是划分区域
可以理解成:
我们在读书时代,如果与同桌的关系特别的不好,在课桌上画上一条三八线来划分区域的理解
但在OS中,“这条线”是需要进行移动的,也就是进行区域的调整
b地址空间本质
地址空间本质上是内核中的一个struct结构体!内部的很多属性都是来表示各个区间在什么范围:
c理解地址空间
下面通过一个小故事来理解:
在遥远的漂亮国有一个大富翁,他有三个私生子,他们彼此是不知道对方的存在的。大富翁在不同的时间段分别对这三个孩子说:要好好生活,等我以后去世了,我的总资产10个亿都是你的。这三个孩子听完之和都很高兴。有一天,他的一个儿子跑过来对他说:老爸,要买书本费用还差300.能不能发给我一下。大富翁爽快地答应了并且马上给了他。过了几天,他的另一个儿子对他说:能不能现在就把10个亿给我,好让我去外面做生意。大富翁听完很生气没回复他,他的儿子看到他父亲脸色一下子就黑了连忙说:我只用1万就足够了。大富翁听完之后也拿钱给了他。故事的结尾,但大富翁去世时,他的家人帮其整理了财产后发现其实就只有区区10几万,也就是说:大富翁其实是在给孩子们“画大饼”
而大富翁画饼的行为就相当OS在给各个进程之间画饼:我这个地址空间里有100个G,你们尽管去申请空间,不够再来找我拿。给了进程去实现“梦想”的机会。
d为什么要有地址空间
Ⅰ将代码数据从无序变有序,让进程以统一的时间来看待物理内存以及自己运行的各个区域
Ⅱ 进程管理模块和内存管理模块进行解耦:内存空间与地址空间之间建立了联系方便后续工作开展
Ⅲ 访问内存势必要先经过地址空间与页表,有效拦截非法请求(对物理内存进行保护)
6理解写时拷贝
以问题中的代码为例:
在地址空间里,有着父进程g_val的虚拟地址在找到内存地址访问内存时不仅仅是页表的功能,还有CPU的参与:CPU内部有着CP3的寄存器保存着父进程g_val的虚拟地址,传给另一个寄存器MMU,而通过MMU+页表最终才能内存地址:而在页表里会对你当前的因为进行记:
是否在内存中?是否有r,w,x权限?
但OS在检测到错误时,会去找对应位置在页表的标记情况:
1是不是数据不在物理空间——进行缺页中断
2是不是数据需要进行写时拷贝——进行写是拷贝
3如果都不是,进行异常处理——中断程序
九Linux调度运行队列
Linux采用的分时操作系统,强调的是公平。而在一些智能汽车中搭载的操作系统采用实时操作系统,强调实时性:但汽车采取刹车时,OS要优先去执行当前进程保证安全性。
在上面我们讲到在Linux里有优先级的概念,而它是有对应的范围的:[60,99](待会用到)
在Linux系统中每一个CPU都有一个运行队列来进行对进程的管理:
queue[140]中:队列中只采用[100,139]的位置,100对应优先级的60...进行偏移
long bitmap[5]中bitmap[0]代表着queue队列里的前32个进程,依次往下数...
而32代表着32个bit,如果在进程队列中有进程在等待,就往对应位置的bit改成1来进行表示。
这里CPU在查找队列是就不要一个一个进程进行遍历,只需通过bitmap数组每32个进行遍历。提高效率(O(1))
现在,OS只要维护好这两个队列:active队列(只出不进),nr_active(只进不出)
CPU调度执行完一个队列后仅需将这两个队列:swap(active,nr_active)即可
这样的一种调度方法,我们称之为:O(1)调度算法