目录
虚拟地址空间究竟是什么? 回答上面同一个变量同时有两个不同的值的现象。
一、冯诺依曼体系结构 ![](https://img-blog.csdnimg.cn/24d0f7a469de4c95bddc62d67dfccca7.gif)
这里的存储器就是内存
输入设备:键盘,摄像头,话筒,磁盘,网卡
输出设备:显示器,音箱,磁盘(你可以把数据写入磁盘也可以把数据从磁盘读取出来,所以磁盘即是输入设备也是输出设备),网卡……
CPU:运算器(算术运算如加减乘除)(逻辑运算如if判断)
控制器:CPU是可以响应外部事件的,协调外部就绪时间,比如拷贝数据到内存
1.为什么我们需要存在一个存储器的结构呢?
为什么不能由 输入设备->CPU->输出设备呢?
输入设备:产生数据的
输出设备:保存或者显示数据
对于数据的读取速度以下列顺序递减,并且是数量级别的差别
1.CPU&&寄存器>内存>磁盘/SSD>光盘>磁带
这里的话,由于木桶原理,我们计算机总的运行速度就会被运行最慢的那一个设备所限制。就好比高u低显,cpu的速度就被GPU的速度限制了。在我们上面的假设中,如果我们没有一个内存来充当一个读取速度介于外设和CPU之间的设备,我们的计算器的整体运行速度就会被外设的速度拖慢。当我们把外设中的数据经过一些软件的合理调用,读取到内存中,我们的CPU就可以直接读取内存中的数据,这样就可以大大提高我们计算机的运行效率。
2.那为什么还要有外存呢?我们不能将我们的存储设备全部变成内存吗?
内存如果断电的话,里面的全部数据就会丢失,而我们的外存在断电之后还是会保存在里面的。并且我们的内存的价格远远大于我们外存的价格,会使我们计算机的造价直线上升。
这里我们需要知道一个概念,凡是被广泛传播的产品,一定是价格便宜,质量OK的。
1.CPU读取(数据+代码),都是要从内存中读取的。站在数据的角度,我们认为CPU不和外设直接交互。
2.CPU需要处理数据,需要先将外设中的数据,加载到内存当中。站在数据的角度,外设直接只和对应的内存打交道。
我们将数据从外设写入内存中称为input,从内存中将数据刷新到外设上称为output。这两个过程合并起来称为io操作。就像是scanf就是input的过程,printf就是output的过程,scanf和printf都是io操作。
程序要运行都必须要被加载到内存中。(程序本身就是一个文件,它存在于磁盘上)
3.那么为什么我们运行一个程序就必须被加载到内存呢?
因为我们的CPU只能在内存中读取代码和数据。我们的程序编译好就仅仅是一个文件。所以我们的程序只有被加载到内存中才能被CPU处理。这是由于我们的冯诺依曼的体系结构决定的
4.假设老冯通过QQ给王大队长发送消息“啊哈哈哈,鸡汤来喽”,那这个过程中,数据是如何流向的呢?
首先老冯和王大队长的手中都有两台冯诺依曼的体系结构的电脑,在这里我们先不考虑网络。老冯需要先通过输入设备(键盘输入)到内存中(虽然老冯好像是在QQ对话框中输入的,但事实上这个QQ软件此时是被加载到内存里的),然后CPU内存中读取了这个要发消息的请求,将我们的数据进行了包装,然后又重新写回内存中,内存又将这些数据刷新到网卡中,通过互联网,这个数据包就被王大队长的网卡收到了,根据冯诺依曼规定,内存就会从网卡中读取收到的数据,内存将这个数据包读取给CPU,CPU将这个数据包解读之后又将解读后的数据写入到内存中,而内存又将我们的内容刷新到王大队长的显示器上。
二、操作系统![](https://img-blog.csdnimg.cn/73652a32790c40d29115047c69f1a831.gif)
计算机最底层的是硬件。
底层硬件之上是驱动程序。驱动程序是操纵具体的硬件的,并且向上提供一个访问硬件的软件接口。
驱动程序之上是操作系统操作系统是一个对我们计算机的软硬件进行统一管理的软件。人不是直接和硬件打交道的,而是通过操作系统。操作系统能给用户提供一个稳定、安全、简单的执行环境。
那么操作系统是怎么实现上述的功能的呢?(软硬件资源管理)
1.理解管理
校长并不会每天都和我们打交道,可能也只是在新生入学和毕业上发表一下演讲。但是辅导员会管理学生,实时提醒你是不是挂科挂得太多了。在这个体系中,被管理者是学生,管理者是校长,管理者和被管理者在原则上可以不直接沟通。
那么校长是如何管理学生的呢?
作为校长,只需要知道这个学生挂了多少们科目,有没有达到挂科的极限,就会要不要对这个学生劝退而做出决策管理,所以说拿到被管理者的数据,来进行管理决策,这才是最重要的。所以说管理者的核心工作就是做决策。管理并不是对于学生进行管理,而是对数据进行管理。
那么校长如何拿到数据,校长又如何下达你被开除了的决策呢?
这个角色就是辅导员,担任的是执行者的决策。
通过我们上面的例子,这里的校长就是我们的操作系统,学生就是我们的硬件,辅导员就是驱动程序。
假设我们现在有五万学生,辅导员将他们的数据全部收集起来,交给校长,但是面对如此庞大的数据,校长没有办法直接高效地处理完。校长就用c++设计了一个学生类,里面设计了姓名,电话,籍贯等等的属性。然后校长就创建了一个学生数组,并且叫辅导员将学生的数据填入其中。假如这时候,校长想要开除总分倒数的前三名角色,校长就可以编写一个按照总分排名的快速排序算法,从而直接找到总分导数的三个角色。校长就可以快速地找到这三名同学并且通知辅导员将他们三个开除,并且从刚刚的学生数组中删除了刚刚那三个学生的信息。
从上面的例子可以看出,管理者首先要对被管理者进行描述,再根据我们对应的描述类型定义对应的对象,可以把对象组织成数组的样子,然后对学生的管理工作就变成了对数组的增删改查工作。
上面的校长依旧是操作系统,学生依旧是硬件。所以操作系统想要管理不同的硬件,就可以先描述不同的硬件,然后将其组织成为一定的数据结构,然后将对于硬件的管理转化对于数据结构的管理。
Linux系统的内核是由c语言写的,也就是通过struct来描述不同的类型的对象,然后将对象进行组织形成数据结构,再进行对数据结构进行增删改查的管理。
所以操作系统内部一定存在着大量的数据结构和算法。
操作系统如何对硬件、软件进行管理?
再打个比方,银行有许多桌椅、电脑、宿舍、仓库,然后有对应的仓管,保洁等。在这些底层人员之上,会有银行的工作人员,业务人员。在这些工作人员之上还有银行行长。银行行长会通过业务人员的组长、经理来进行对工作人员进行管理。
银行行长、业务人员、项目经理等的工作称为管理工作。
银行行长可以从保洁阿姨那边了解到底层的银行的硬件设备的情况,也就是操作系统对于硬件的管理。当然也可以从项目经理中了解到每一个工作人员的业绩如何,也就是操作系统对于软件的管理。
银行通过柜台的形式给用户提供服务,其是假设所有人都不值得信任,所以需要将自身的体系封闭起来,保证自身的安全,同时又为了满足和外界的业务处理,所以设立了柜台。
同样的话,操作系统也不能将自身完全暴露给用户,可能会导致操作系统不安全。所以操作系统假设全部的人都不值得信任。但是操作系统又需要给用户提供各种各样的功能,所以需要提供各种各样功能的系统接口。(我们学的Linux是用c语言写的,system call本质就是用C语言提供的函数)
我们和银行工作人员沟通帮助我们存钱,实质上是让银行工作人员调用某些方法将我们的存款单上记上存钱记录,并且将这笔钱存入银行的仓库。
我们调用系统的接口,实质上也是让我们的操作系统去操作一些底层的数据或者功能,来达到我们想要的目的。操作系统为我们的用户提供服务叫做系统调用。
一个好的产品,都是为用户提供傻瓜式操作的,让什么都不懂的用户都能轻易上手。打个比方:银行的具体业务要填一大堆单子,一个小白用户完全不会,但是我们可以通过与银行的引导人员进行沟通,得知如何操作。我们的系统调用也是这样,使用成本比较高,没有接触过操作系统的人基本上没法直接使用。但是现在我们创建了shell外壳和图形化界面,或者是对系统调用接口的封装(第三方库),从而让用户更好地去使用计算机。
三、进程 ![](https://img-blog.csdnimg.cn/2d1977435e494940b3aacad2ea90f067.gif)
其实我们自己启动一个软件,本质其实就是启动了一个进程。
在Linux内,运行一条命令, ./xxx,运行的时候,其实就是在系统层面创建了一个进程。
Linux是可以同时加载多个程序的,Linux是可能同时存在大量的进程在系统中的(OS,内存)->Linux系统要不要管理进程呢?必须要管->Linux系统是如何管理大量进程的呢?先描述再组织。
当一个个可执行程序加载到内存当中,我们的Linux系统会为每一个进程都创建一个类(PCB,里面包含了进程的所有属性)
如何理解类呢?(struct)
人认识世界,是通过“属性”来认识的。
->属性是数据吗?是
->属性和进程内的代码和数据有关吗?
文件=内容+属性,文件就是我们的可执行文件,也就是我们编好的代码,我们把它加载到内存里,它就变成了进程,不再是程序了。有pcb结构来描述这个进程的具体属性,(优先级,状态,地址在哪里,等等)不同的PCB我们可以使用链表的方式连接在一起,对进程的管理就变成了对进程PCB结构体的增删改查。
什么叫做进程:进程=对应的代码和数据+进程对应的PCB结构体。
1.PCB ![](https://img-blog.csdnimg.cn/5254622fcdff4b41a5f782680a374946.gif)
为什么要有PCB,就是为了方便系统管理进程。
假设你在清华大学读书,你需要将你的籍贯、身份证号、电话、家庭信息等录入大学的教务管理系统。学校可以通过这些信息来知道你的具体属性。PCB也是如此。
创建进程就是将代码和数据进而PCB放入内存当中,当一个进程结束的时候,内存中的代码,数据和PCB都要从内存中删除。
PCB(process control block),不同的操作系统中,PCB的名字不同
Linux系统中的为struct task_struct
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的属性信息。
2.task_ struct内容分类 ![](https://img-blog.csdnimg.cn/f3413364b18f4db29b82c400f44b57c8.gif)
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。(确定谁先谁后)
程序计数器: 程序中即将被执行的下一条指令的地址。(CPU内一个寄存器指针,保存着当前正在执行的指令的下一条的地址)
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。(比如说我们买云服务器,上面有一个数据为带宽,这也是一种i/o信息)
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。(比如说这个进程已经累积被处理器处理了多长时间,消耗了多少的资源)
其他信息
3.查看进程![](https://img-blog.csdnimg.cn/9b68d5bd4836496b83b2ef18dd5949af.gif)
使用下面的代码查看所有的进程
ps axj
使用top命令可以查看当前的所有进程,按q退出
top
在根目录下有一个proc目录,Linux是可以让你采用文件的形式去查看每一个进程的具体属性。
ls /proc
这里我们查看到我们的左边的一大片蓝色的数字就是我们的进程编号
![]()
cwd currect work director当前进程的工作目录
这和我们的文件工作目录很相似
FILE*fp=fopen("log.txt','w');
这里的log.txt是默认打开当前路径
bin(binary二进制)->运行的->每个进程都会有一个属性,来保存自己所在的工作路径。
4.getpid![](https://img-blog.csdnimg.cn/ffc2d0cc5212486e8c906f8cca054f46.gif)
所有函数只有在变成进程这里是我们的代码 ,getpid()能够获取到当前的进程编号,其编号是一个整型。
这里我们创建的这个程序为一个死循环,因为如果该程序一下子就进行完了,我们就没办法再查看其运行时的进程编号了。(其实我们的操作系统都是死循环,当你执行关机之类的操作时才从死循环中跳脱出来。)
使用ctrl+c或者下面的代码再加上对应的进程编号就能杀死一个进程。
kill -9 +进程编号
使用Ctrl+c(Mac为command+c)
获得父进程id
getppid()
我们所有的进程都是以bash(也就是我们的shell的外壳程序)的子进程的形式进行的。
从上面的代码中我们可以查看到我们当前的子进程为5709,这个子进程的父进程是4384,那么这个4384进程是谁呢?我们进一步查看
从上面的测试中,我们可以看到我们5709的父进程就是我们的bash进程
5.系统调用接口和创建子进程
fork创建一个子进程,要求包含<unistd.h>的头文件 pid_t fork(void)
pid_t fork()
fork的返回值有两大情况,
1.创建失败的时候返回-1
2.创建成功的时候,a.子进程的pid会返回给父进程
b.给子进程返回0
使用上面的代码之后,我们很吃惊的发现系统打印了两个you can see me
我们再尝试下面的代码
执行完上面的代码后,我们发现我们的ret竟然同时有两个值,我们再执行下面的代码进行进一步地查看。
这里我们可以看出,我们原来的进程的编号为6625,然后我们使用fork()新创建了一个进程,创建成功,子进程为6226
然后我们的父进程这里先进行了打印,也就是6225,其父进程的父进程也就是4384也就是我们的bash,也就是我们的shell外壳进程(这里在上面我们有介绍,在同一次使用我们的Linux系统中,这个shell的进程编号是不变的)
然后我们的子进程也进行了打印,其ret值根据我们上面的fork的用法,创建成功给子进程返回0,然后打印子进程自身的编号为6226,其父进程为6225
从中我们可以看出,在fork之后,有两个进程进行了下面的代码,所以产生了两个返回的结果。
6.fork基本用法
fork帮我们创建了一个新的进程,也就是说帮助我们做事的人多了。
一般而言:fork之后,代码是父子共享的,也就是说运行之后父进程和子进程都能看到。
但是如果我们想要让我们的父进程和子进程执行不一样的代码,我们可以通过父子进程ID不一样的特点给我们的父子进程分配不同的任务。
下面我们让两个死循环同时执行
注意:id在父进程里面是子进程的pid在子进程里面是0,所以如果id==0的话,那么就是子进程,那么返回的ID是子进程的话就是父进程。
从上面的测试代码中我们可以看出父子进程执行了不同的代码
3 #include <unistd.h>
4 int main()
5 {
6 printf("I am parent process:pid :%d\n",getpid());
7 pid_t id = fork();
8 if(id <0)
9 {
10 //failded;
11 perror("fork");
12 return 1;
13 }
14 else if(id==0)
15 {
16 //child process
17 while(1)
18 {
19 printf("I am child,pid:%d ppid:%d\n",getpid(),getppid());
20 sleep(1);
21 }
22 }
23 else{
24
25 //parent process
26 while(1)
27 {
28 printf("I am parant,pid:%d ppid:%d\n",getpid(),getppid());
29 sleep(1);
30 }
31 }
32
33 // printf("ret :%d,pid:%d,ppid:%d\n",ret,getpid(),getppid());
34
35 }
创建进程的时候,OS要做什么?本质,就是系统多了一个进程(要创建一个task_struct)
子进程的内部属性要以父进程为模板,将父进程的内部属性拷贝下来,复制给子进程。
fork() { //创建子进程的逻辑 return id; }
每一个CPU都会维护一个自己的运行队列,操作系统和CPU运行某一个进程,本质从task_struct形成的队列中挑选一个tasj_struct,来执行它的代码。
进程调度,变成了在task_struct的队列中选择一个进程的过程。
当我们已经准备return了,我们的核心代码已经执行完了吗?
①所以我们的return的时候,我们的计算结果已经完成,我们的核心代码其实已经被运行完了,我们的父进程和子进程都会return。
②但是返回两次并不代表着会保存两次。
struct task struct{ struct task_struct *next; int pid; int ppid; 状态; 优先级; …… }
只要想到进程,优先想到进程对应的task_struct。
父子进程创建出来,哪一个先运行呢?
不一定。由于我们的CPU是分时操作系统,要按照每一个进程被划分到的时间片来运行每一个进程。所以只有系统底层的进程调度算法才能够知道哪一个进程先运行, 我们目前无法得知。
四、进程状态
系统中一定是存在各种资源的(不仅是CPU),网卡,磁盘,显卡等其他设备。
系统中不一定只是存在一种队列,其中与CPU配套的队列称为运行队列
以下为操作系统的理论状态,不同操作系统有不同的具体实现。
新建:我们前面fork了一个新的进程就是新建状态
运行:task_struct结构体在运行队列中,就叫做运行态
阻塞:等待非CPU资源就绪,阻塞状态。
int main() { int a=0; scanf("%d",&a); }
如果我们执行了上面的代码,但是我们就是没有把a的值从键盘输入进去,这时我们的这个进程就进入了阻塞状态。 有时候我们打开的软件卡死了,比如exe未响应,可能就是因为这个进程阻塞了。
挂起:当我们的内存快要不足的时候,操作系统会将长时间不执行的进程代码和数据换出到磁盘(磁盘中的swap分区,我们用户无法查看到)。(挂起和阻塞的区别,两种状态都是暂时得不到CPU的付出,但挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中)如果挂起态想要再次被运行,需要将之前挂起的进程数据重新调入内存中,进入运行队列。
如果是因为阻塞而挂起就称为阻塞挂起。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */这里的小t和大T都是暂停状态,只不过t是调试过程中的暂停状态,也就是我们的gdb在我们的打断点的位置对我们的进程发送了第19号信号,让我们的进程停下来了。
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
使用下面的代码可以捕捉我们的myproc程序的当前的进程
while :; do ps axj|head -1 && ps ajx |grep myproc | grep -v grep;sleep 1; done;
进程可以分为前台进程和后台进程
以下我们直接运行我们的死循环进程,也就是我们的前台进程
如果我们在我们运行的后面加上一个&就是后台进程,
这个时候是没办法使用ctrl+c停止的,需要使用kill -9 再加上对应的进程编号
具体的
R运行状态(running)(运行态) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping)(阻塞状态): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。(这里我们写的是一个死循环,这里的加号表示这是一个前台任务,就是说此时bash的解释器没办法再解释其他的命令,并且可以被ctrl+c终止。)
D磁盘休眠状态(Disk sleep)(阻塞状态)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。(只有等到这个磁盘读写进程的结果出来的时候系统才能干掉这个进程。)(可通过dd命令)
T停止状态(stopped)(阻塞状态): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。(我们给我们的四循坏代码输入kill -19 再加上进程号,就能让我们的进程停止,再输入kill -18 再加上进程号就能够让我们刚刚暂停的进程重新进行)(调试的过程中就是停滞状态)
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态 (终止状态。瞬时性非常强,这个状态出现了,操作系统马上就会将这个进程的所占用的资源全部回收,咱可能看不到这个状态。)
进程状态查看
使用下面的代码可以查看进程状态的查看
ps aux / ps axj
Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
僵尸状态是什么?
举一个例子:如果一个程序员连续几天加班之后突然寄了,那么这个时候,这个人不是立马抬走,而是会有警方介入,封锁现场,确认调查结果,确认真的是过度加班才导致死亡的时候,才会将人送走。这个过程中, 警方调查死因的过程中,这个程序员,也就好比是我们的进程,就是僵尸进程了。(一个进程已经被退出,但是还不允许OS被释放,处于一个被检测状态--僵尸状态。维持僵尸状态是为了让父进程和OS来进行回收)
父进程或者操作系统主动去检测子进程是不是僵尸进程。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 #include<stdlib.h>
5 int main()
6 {
7 printf("I am parent process:pid :%d\n",getpid());
8 pid_t id = fork();
9 if(id <0)
10 {
11 //failded;
12 perror("fork");
13 return 1;
14 }
15 else if(id==0)
16 {
17 //child process
18 while(1)
19 {
20 printf("I am child,pid:%d ppid:%d\n",getpid(),getppid());
21 sleep(3);
22 break;
23 }
24 exit(0);
25 }
26 else{
27
28 //parent process
29 while(1)
30 {
31 printf("I am parant,pid:%d ppid:%d\n",getpid(),getppid());
32 sleep(1);
33 }
34 }
在上面的代码中,我们的子进程已经挂掉了,而父进程还在运行,所以子进程就呈现出僵尸状态。
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
内存泄漏?是的!
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init进程(系统本身)领养,需要要由init进程回收。
在下面的代码中我们的子进程一直在进行死循环,但是我们的父进程在循环了大约五次之后就会退出。
从我们下面的代码中,我们观察到在我们的子进程是16144,我们的父进程是16143,当我们的父进程16143退出的时候,我们的子进程的PPID也就是父进程就变成了1,也就是我们的上述的init进程,也就相当于是系统本身。也就是说我们的孤儿进程被系统领养了
注意,这里当我们的父进程退出之后,我们的子进程从一个前台进程变成了一个后台进程,不能直接通过ctrl+c退出,需要使用kill -9 +进程编号来将我们的子进程杀死。
五、进程优先级
为什么要有优先级?
就是因为CPU是有限的,进程太多,需要通过某装方式竞争资源。
什么是优先级?
确认是谁应该先获得某种资源,谁后获得。
我们是可以用一些数字表名优先级的。
在计算机中评判优先级的是调度器,决定哪一个进程可以进入CPU进行处理。
Linux下具体的优先级做法
优先级=老的优先级+nice值
ps -al | head -1 && ps -la |grep myproc
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
输入top,然后输入r,再输入我们的进程号,在输入我们要调整的优先级数值。
这是已经按了r之后的截图
输入要修改优先级的进程编号
回车之后再输入我们想要增加或者减少的nice值
从之前的测试代码中我们知道我们当前进程的优先级为80,在我们输入r,然后输入10之后,我们的优先级变成了90
我们再次输入-30就变成了60
其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。( 操作系统是按照时间片的形式来处理大量的进程的,每一个进程只会占用CPU一定的时间片的时间,然后这个进程就会下处理机,别的进程上处理机。)
抢占:优先级更高的进程想使用处理机资源的时候,当前优先级较低的进程会被迫下处理机。更高优先级的进程会上处理机。
如果进程A正在被运行,CPU内的寄存器里面,一定保存的是进程A的临时数据。寄存器中的临时数据叫做A的上下文。
上下文数据可以被丢弃吗?绝对不可以。如果上下文被丢弃了,进程就进行不下去了。
当进程A暂时被切换下处理机的时候,需要进程A顺便带走自己的上下文数据。
带走的目的:就是为了下次回来的时候,能恢复上去,就能按照之前的逻辑继续向后运行,就如同没有中断过一样。
CPU内的寄存器只有一份,但是上下文可以有多分,分别对应不同的进程。
六、环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
为什么Linux系统的ls啥的命令就能够自己运行,不用带上路径,但是我们的执行我们的程序却需要./myproc就是需要带上路径呢?为什么我输入myproc就会报错(command not found?)如果我想让运行普通命令一样运行一个代码程序呢?
这里我们使用where pwd找到了pwd的路径位置,
我们再使用echo $PATH来查看我们系统的环境路径。
所以因为pwd在我们的系统环境中,所以我们的系统能够直接读取到这个命令(每一个路径使用冒号分割的)
我们可以将我们的程序放入这个目录下,但是这样会污染系统的程序
我们可以使用下面的指令将我们当前的路径添加到我们的系统环境中,但是只有当前会话有效。然后我们就可以不输入路径,直接输入程序名就可以了
export PATH=$PATH:/home/zhuyuan/test0804
家目录
echo $HOME
![]()
与环境变量相关的指令:
1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
环境变量其实是被当做字符串以指针数组的形式传递给我们的
编写如下程序查看系统传递给我们的环境变量。
或者
也可以使用getenv来获取环境变量
环境变量信息可以通过父进程那里继承下来的。
默认所有的环境变量都会被子进程继承,所以环境变量具有全局属性。
#include <stdio.h> #include <stdlib.h> int main() { char * env = getenv("MYENV"); if(env){ printf("%s\n", env); } return 0; }
直接查看,发现没有结果,说明该环境变量根本不存在
导出环境变量
export MYENV="hello world"
再次运行程序,发现结果有了,说明:环境变量是可以被子进程继承下去的。
int main(int argc, char *argv[], char *env[])
argc和argv是命令行参数
argc决定有几个命令行参数
argv存储具体的命令行参数
这里由于我们并没有设定命令行参数,所以默认的argc就是1,argv就是执行我们代码的指令。
当然我们也可以使用一定的方法让我们的程序加上一定的选项,编写如下代码
命令行参数的最大意义,就是我们能调用不同的参数来让我们的程序执行不同的功能了。
我们还可以输入以下的代码,来看看我们的agrc和argv此时里面装的是什么
七、地址空间
栈区向下增长,堆区向上增长
这是我们的测试代码
从我们下面的测试结果可以与我们的上面的进程地址空间图一一对应,
text存放在代码区,是下面所有地址中最小的
init是初始化过了的全局变量,放在代码区的上面,未初始化全局变量的下面,也就是比我们的text的地址大,比我们uninit的地址小
uninit就是我们的未初始化的变量,其地址比已初始化全局区的地址大,比下面heep,也就是堆区的地址小
从下面的五个heep地址的增长过程中,我们可以看出堆区的地址是向上增长的。
栈区是下面测试部分中地址在最上面的,并且每开辟一个栈区的地址,其地址就向下增长,也就是逐渐变小。
什么是地址空间(是什么)?
这个地址空间是内存吗?
这个地址空间不是内存。
#include <stdio.h>
#include <unistd.h>
int g_val=100;
int main()
{
pid_t id =fork();
if(id==0)
{
int cnt=0;
//child
while(1)
{
printf("I am child,pid:%d ,ppid: %d ,g_val:%d ,&g_val:%p\n",\
getpid(),getppid(),g_val,&g_val);
sleep(1);
cnt++;
if(cnt==5)
{
g_val=200;
printf("child change g_val 100->200 success \n");
}
}
}
else
//father
{
while(1)
{
printf("I am father,pid :%d ,ppid: %d,g_val: %d,&g_val:%p\n",\
getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
}
怎么可能同一个地址,同时读取的时候,出现了不同的值?
这里的地址,绝对不可能是物理内存的地址!
这里的地址不是物理地址,是虚拟地址(线性地址)!
所以,几乎所有的语言,如果他有地址的概念,这个地址一定不是物理地址,而是虚拟地址
可能你的物理内存只有2G,很多外部设备也有很多寄存器,我们可以将我们当前计算机的内存和外设的内存统一编址,从而更加方便内存的管理(从全0的地址到全F的地址)。这里我们是以统一的视角去看待全部的地址,其实使用的就是虚拟地址
上面全部的地址空间全部都是进程在打印地址,全部都是在程序运行之后打印的。
例子:
1.大富翁。有一位富豪有10亿美金的家产,这个大富翁有三个私生子a,b,c,但是这三个私生子并不知道彼此的存在。a是一个学生,b是做生意的,c社会上班族。大富翁为了让这三个儿子都能好好工作,对a说要好好念书,以后这10个亿的美金全部都是你的,a听了之后一下子开始认真好好学自。富豪对b说要好好做生意,以后这10亿美金和产业全部都是你的,b听了之后马上更加努力地做生意。富豪对c说要好好上班,之后这10亿家产全部都是你的,c听了之后更加努力上班工作。
这三个儿子都认为这3亿美金是自己的,自己就是这3亿美金的合法继承人
但是站在我们的视角来看,这个大富翁给每一个儿子画了一个大饼,每一个儿子都对这10个亿充满了愿景。
这三个儿子零零星星地向着这个富豪要钱,这个富豪都满足了这三个儿子的要求。
突然有一天,三儿子想向富豪要一个亿,但是这个富豪说自己没有那么多钱。这个三儿子骂骂咧咧地回去了,但是他还是认为自己是这十个亿的合法继承人,自己依旧拥有这10个亿。
但是站在上帝视角的我们,知道此时这个富豪真的没有一个亿了。
这里我们将这里的富豪看做是操作系统,这三个儿子分别看做小a,b,c三个进程,那么我们将富豪给儿子画的饼称作地址空间。
现实老板给我们画大饼,说好好干,今年就给你涨工资,对另外一个人说,好好干,今年让你做部门经理,对第三个人说,好好干,今年让你但任项目主管。这个现实中的饼就是我们计算机中的物理内存,这里的饼就是我们的进程,通过先描述,再组织的方法,我们的内核中的地址空间,本质上将来也一定是一种数据结构,将来一个要和一个特定的进程关联起来。(也就是说老板画的每一张饼要和每一个具体的人一一对应)
地址空间是如何设计的(怎么办)?
以前设计计算机的时候,我们是直接访问物理内存的。通过一个进程的起始地址再加上偏移量,我们就能够知道一个进程所存储的地址范围。
所以操作系统在调度的时候,就是从内存中的众多进程中选择一个,放到处理机上进行运行。
但是如果我们的指针非法访问了别的空间(内存本身是随时可以被读写的),比方说进程2中存储了一些关于用户密码的数据,而我们的进程一直接读取了这些数据,这就可以看出这种方法使得我们的内存非常不安全。
历史上的进程是不具有独立性的,这种不安全的原因是在于我们直接使用的是物理内存,所采用的的是物理地址。这里我们就意识到我们不能直接使用物理地址!
现代计算机:提出了下面的方式!
每一个进程都有专属的pcb,也就是数据结构task_struct,每一个pcb中操作系统都为其分配了一块虚拟地址空间,这块地址空间是从全0地址到全F地址的。我们将其称之为虚拟地址。然后我们的系统存在一种映射机制,将我们用户的代码通过一定的映射机制映射到我们的物理内存中。
也就是说我们需要访问物理内存的话,我们需要先进行映射!!(虚拟地址->物理地址的过程)
但是我们最终还是要访问物理地址的呀?万一我们最后的物理地址访问了别的进程的物理地址,那不是还是干扰了别的进程吗?
例子:我们过年的时候,每个孩子都有压岁钱,但是爸爸妈妈怕孩子将压岁钱乱花,所以会将孩子的压岁钱“代理”。孩子想要去买本书,爸爸妈妈同意,将这钱给了你,孩子又想去买玩具,爸爸妈妈不同意,不把钱给你。
虽然钱自己随便花非常开心,但是容易乱花钱,将钱交给爸爸妈妈,花钱虽然会受到约束,但是花钱会更加合理。
这里爸爸妈妈作用就是甄别你的消费。也就是在你和商店老板之间加入了一个爸爸妈妈的角色。这里的虚拟地址也就是你爸爸妈妈的角色的作用,也就是说,如果你访问的是一个非法的地址,你就无法通过虚拟地址空间访问非法的地址空间,也就是说你没办法“乱花钱”。
如何理解区域划分?
上面我们所说的物理地址空间是被划分成一块块不同的区间的,那么如何理解这个区域划分呢?
假设幼儿园中有一对同桌,小胖和小花,小花告诉小胖,不要越过这张桌子的三八线,小花所做的就是划分区域。(假设桌子一共长100cm,小胖占1-50,小花占51-100)
那么如何用c语言去划分三八区呢?
struct destop{ int start; int end; } struct desktop one=[1,50]; struct desktop two=[51,100];
这里的区域划分本质上就是在一个范围里定义start和end
每一个进程都要有地址空间,操作系统要给每一个进程都画一张饼。操作系统需要对每一个地址空间进行先描述,再管理。我们将地址空间从全0地址到全F地址空间。地址空间是一种内核数据结构,它里面至少要有:各个区域的划分
struct addr_room { int code_start; int code_end; int init_start; int init_end; int uninit_start; int uninit_end; int heap_start; int heap_end; int stack_start; int stack_end; …… 其他的属性 }
对于所有的地址空间来说,并不是一尘不变的,比方说栈和堆的地址就是会动态增长的。
比方说小胖越过了三八线20cm,也就是说小胖的end+=20,小花的begin+=20
所谓的范围的变化,本质其实就是对start或者end标记值+-特定的范围即可。
每一个进程都有各自的pcb,每一个pcb都有对应的地址空间
地址空间和页表(用户级),是每一个进程都私有一份。
只要保证,每一个进程的页表,映射的是物理内存的不同区域,就能够做到,进程之间不会相互干扰,并且保存进程的独立性。
struct mm struct {}
虚拟地址空间究竟是什么? 回答上面同一个变量同时有两个不同的值的现象。
在创建父进程中创建子进程的时候,页表和地址空间最初是都是和父进程一样的。最开始的时候两个进程所指向的是同一个值。
当我们的操作系统识别到我们的子进程要将我们的值修的时候,我们的操作系统为了实现进程的独立性,所以更改了子进程的页表中的映射,也就是说我们的子进程和父进程的虚拟地址是一样的,但是由于页表的映射不同了,所以我们的子进程中的这个值是修改后的值,而我们的父进程中这个值还是原来的值。这就呈现出了我们之前子进程和父进程中同一个变量竟然有两个不同的值的结果。这就是所谓的写时拷贝!!!
当我们的程序,在编译的时候,形成可执行程序的时候,没有被加载到内存中的时候,请问:我们的程序内部,有地址吗???
使用objdump可以进行反汇编
从上面的测试结果来看,在我们的程序还没有被加载到程序的内部的时候,就已经存在虚拟地址了。
假设我们要找printf,我们只要找到这个库中的printf的地址,将其拷贝到我们的程序中,我们就能够调用到我们的printf。
地址空间不要仅仅理解称为是OS(操作系统)内部需要遵守的,其实编译器也要遵守!!,即编译器编译代码的时候,就已经给我们形成了各个区域,(代码区,数据区……),并且,采用和Linux内核中一样的编织方式,给每一个变量,每一行代码都进行了编制。故,程序在编译的时候,早已经具有了一个虚拟地址!!!
比方说下面的代码(仅仅是示例),其实程序的可执行程序早已经将我们的地址加载进去了
int a=10; 0x300 int b=20; 0x200 printf("aaaa") 0x100
程序内部的地址,依旧用的是编译器编译好的虚拟地址
当程序加载到内存的时候,每行代码,每个变量便都拥有了一个物理地址(外部的)
程序虚拟地址的最大和最小地址就可以填入我们之前所说的mm_struct也就是我们的用于维护地址的数据结构,再从我们的mm_struct中的地址通过页表映射到物理内存中。
深入理解虚拟地址
当CPU读到指令的时候,指令内部也有地址,CPU读到的是虚拟地址,跳转到指定的位置,通过指定的页表,跳转到指定的物理地址,然后读取到具体的数据或者是指令。
在下面的图中,我们的磁盘中有我们的刚刚写好的代码。我们的代码在编写的时候编译器就已经给我们分配虚拟地址了,我们的代码中的mytest中的0x1的地址要跳转到0x10,0x10的地址需要跳转到0x100,然后其具体的映射的内存的时候,其物理地址是0xA,0xAA,0xAAA,我们将物理地址和虚拟地址填写到我们的页表的左右的对应位置。
这时候我们运行我们的程序,我们的tast_struct对应我们的pcb然后pcb中有专门的数据结构mm_struct来指示我们原来的程序中的对应的不同分区的数据(堆区的起始地址,结束地址,栈区的起始地址,结束地址,等等)。
然后我们的cpu在运行的时候,就从我们的进程pcb中的相关信息得知需要取的地址,从而从我们的mm_struct中找到所对应的虚拟地址,然后从页表中找到虚拟地址所对应的物理地址。所以我们的CPU是看不到物理地址的,只会接触到虚拟地址。
映射关系的维护是谁做的?
从上面的过程来看,是页表的进行映射关系的维护的。
验证地址空间
我们如何验证下面的内存结构呢?
首先使用我们的项目自动化创建工具makefile
$@代表的是hello ,$^代表的是以hello开始的所有的后缀
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
//static修饰局部变量,其实是本质上是将该变量开辟的全局区域,但只是在该函数内才能访问
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
//这里的堆空间的地址就是我们自己所开辟的空间的地址,也就是我们上面的heap_men,以此类推
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
//栈上开辟的临时变量属于栈区的地址
printf("test stack addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
//栈空间的地址是我们函数中的变量的地址,在我们的main函数中,我们上面的heap_men等等
//就是其中的变量,也就是说只要&heap_men就能够得到栈区的地址
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
//这里我们打印的不是字符串的地址,而是字符串本身
//从测试结果中可以看出所有的字面常量都是硬编码进我们的代码的,
//也就是属于我们的正文代码区上面有一个小小的区域叫字符常量区
//它和正文都是只读属性的
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
堆栈相对而生
从上面的图中我们注意到堆区每一个地址都是相差了十六进制的20,那么为什么会相差这么多呢?
我们不妨想一下,我们在手动开辟了一块空间之后,free这块空间的时候,为什么只需要将首地址的指针传入即可释放对应的空间?
比方说这一次我们申请了十个字节,但是我们的系统给我们多开辟了10个字节,一共二十个字节,这多开辟的十个字节就可以被称为是属性信息,也可以称为cookie数据,也就是用这部分数据来记录你本次申请的属性,所以我们的系统根据free的起始地址就能够找到我们堆空间的相关的属性数据。
int a = 10; //字面常量 const char *str = "helloworld"; 10; 'a';
int a =10,10是字面常量,将字面常量放入我们的变量a中
1.用户空间 vs内核空间
在32位下,一个进程的地址空间,取值范围是从0x0000 0000~0xFFFF FFFF
[0,3GB]:用户空间
用户空间是按照我们上面验证的空间排布方式排布的
[3GB,4GB]:内核空间
其他整体认识
2.Linux vs windows
上面的验证代码,在window下会跑出不一样的结果
所以我们上面的结论,默认只在Linux下有效。
地址空间本质上就是操作系统让进程看待内存的一种操作方案(数据结构)。让进程的pcb指向具体的虚拟内存,然后通过页表来找到对应的物理内存地址。
页表是不是简单的映射关系?
页表也是一种数据结构。比方说数据结构中的map或者是哈希表。
在下面的代码中,我们可以通过3这个下标找到hello world,这个3就被称为key值。
char* map[10];
map[3]="hello world";
实际上我们的页表也是一张映射表,但不仅仅是映射关系,还有权限管理等等
为什么要有地址空间?
1.凡是非法的访问或者映射,操作系统(OS)都会识别到,并终止这个进程
int main()
{
char *str="hello world";
//试图将hello world的第一个字符改成H
*str='H'
}
字符常量不可以直接被修改。代码区只能被读取,不能被修改。因为页表去中除了存储了映射关系,还存储了读和写权限,这里的字符常量就是不可以被写入的。 (这里的页表的权限有点像是文件的读写权限)
C或C++中有野指针的时候,在运行的时候一定会崩溃的(所有的进程崩溃,本质上就是进程退出。操作系统是进程的管理者,进程的退出时操作系统杀掉了这个进程。)所以一旦在语言层面上出现了问题,是系统层面将这个问题结束的。
我们因为有地址空间+页表的存在,我们可以对用户的非法访问进行阻止,在一定程度上,保护了我们的物理内存,也便保护了物理内存中的所有合法数据和各个进程,以及内核的相关数据。(就好比是你妈阻止你乱花钱。)
所有的地址空间都是操作系统创建并且维护的,凡是想要使用地址空间和页表进行映射,都需要在操作系统的监管之下来进行访问。
2.因为有地址空间的存在,因为有页表的映射的存在,我们的物理内存中,是不是可以对未来的数据进行任意位置的加载?
当然可以。物理内存的分配就可以和进程的管理,可以做到没有关系 。
物理内存在Linux系统下,属于内存模块管理,和进程模块就完成了解耦合(内存管理和进程管理就是相对于独立的,通过页表将这两个模块联系起来)(软件在工程中,耦合度越低,维护起来越方便)
我们地址空间的存在将进程管理和内存管理在系统层面上进行了解耦。
所以我们在c/c++语言上,new,malloc空间的时候,本质上是在哪里申请的?
在语言级别上进行new,malloc的时候,根本没有在物理内存上申请空间,仅仅是在虚拟内存上申请了一块空间。
3.如果我申请了物理空间,但是如果我不立马使用,是不是空间的浪费呢?
是的。物理空间就好比是你想买一个玩具,要花500块钱,但是你没时间去玩。那么这500块钱就被浪费掉了。但是你妈许诺你以后想玩的时候会给你买的。你知道了你会有这个玩具,所以你也没有立马将这500块花掉。
本质上,因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你。而你真正进行物理地址空间进行访问的时候(这里的操作是由操作系统自动完成的,对于用户和进程是透明的(透明就是看不见,0感知的意思)(缺页中断)。(就好比你妈许诺你了要买这个玩具,然后反手将这500块借给了别人,但是当你要买这个玩具的时候,这500块钱已经又被收回来了,并且你根本不知晓这500块曾经被借出去过)),才执行内存的相关管理算法,帮你申请内存,然后,才让你进行内存的访问。
在分配内存的时候,我们可以使用延迟分配的策略,来提高整机的运行速度(只有当用的时候才给你分配内存,也就是相当于内存的有效分配是100%,不存在申请了但是不用的情况,这样内存的使用率就提高了)
4.因为在物理内存中理论上可以任意位置加载,那么是不是物理内存中几乎所有的数据和代码在内存中是乱序的?
但是因为页表的存在,可以将地址空间上的虚拟地址和物理地址进行映射,那么是不是在进程视角,所有的内存分布,都可以是有序的!!
地址空间+页表的存在可以将内存的分布有序化。
5.地址空间是操作系统(OS)给进程画的大饼,操作系统为什么要给进程画大饼?(操作系统为什么要给进程划分地址空间)
进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,是不是便很容易做到进程独立性的实现。
进程的独立性可以通过地址空间+页表的方式实现
因为有地址空间的存在,每个进程都认为自己拥有4GB空间(32位),并且各个区域都是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性(每一个进程不知道其他进程的存在的,每一个进程都认为自己独占内存)(独立性除了 不干扰别人之外还不知道彼此的存在)
什么是挂起?
系统是如何加载的?
加载的本质就是创建进程,那么是不是必须非得立马将所有的程序的代码和数据加载到内存中,并创建内核数据结构,并建立对应的映射关系呢?
在最极端的情况下,甚至只有内核结构被创建出来!
这样的进程的状态被称为是新建状态
理论上可以实现对程序的分批加载!
比方说我们的游戏有120GB,但是我们的内存只有16GB,根本不可能一下子将游戏中的全部的内容都加载进来。 所以操作系统会对其进行分匹配加载。
既然可以分批加载,可以分批换出吗?
加载的本质其实就是一个换入的过程,相当于将磁盘中的内容和数据换入我们的内存当中。(游戏中的开机界面其实只需要加载一次就可以了)
既然可以分批加载,当然可以分批换出,甚至这个进程短时间内不会再被执行了,比如阻塞了(比方说打印机被占用),所以操作系统就可以将这个进程换出内存,此时这个进程就可以被称为挂起状态
这个换出和之前的新建状态有什么区别?
没有本质区别,都只剩下一个进程结构保存在内存当中。
实际上页表映射的时候,可不仅仅映射的是内存,磁盘上的位置也可以映射
假设要读取的数据并不在内存中,而是在磁盘的指定的位置,在操作系统进行换入操作的时候,操作系统就可以从磁盘中将对应的代码和数据换入。(只要在页表中所指向的位置是磁盘中的地址就可以了)
当程序编译好,形成可执行程序的时候,这个可执行程序的内部的地址称为逻辑地址,那么当它加载到内存的时候, 然后把进程的地址空间给建立好,此时的地址称为虚拟地址,那么当cpu在寻址的时候,通过地址空间去寻址的时候,查找的是线性地址。
但在Linux环境中,上述的地址都是虚拟地址,在不同的阶段起了不同的名字,仅此而已。