目录
冯诺依曼体系结构
![](https://i-blog.csdnimg.cn/blog_migrate/950600bf54e981f79f1865cfc80b402f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/56800b56f3c1e57c5fcaf03551b1dcbd.png)
操作系统(Operator System)
概念
设计OS的目的
如何理解 "管理"
首先说明,管理的本质就是对数据进行管理。
为什么要管理呢?
通过合理的管理软硬件资源(对下),从而给用户提供稳定,安全,高效的执行环境。(对上)
管理的方法:先描述,后组织
什么意思呢?对于硬件的管理,我们可以抽取出他们之间的性质,归结成一个类,然后通过数据结构链表的知识,我们就可以把他们链接起来,此时,我们管理硬件,不就是对链表的管理了吗,那不就很简单了,我们只需要写上增删查改的算法就能对链表进行很好的管理了。硬件是如此,软件其实也是这样。
但是由于用户并不知道怎么调用系统的接口,所以就有人写了库或者是shell来为用户提供服务,这就形成了比较完整的体系。
总结
进程
首先什么是进程?先说结论 进程就是内核的数据结构+进程对应的磁盘代码
进程是运行起来的程序,那么程序是什么呢?程序的本质就是文件,在磁盘中存放的,那么程序运行起来是要加载到内存的,有那么多的程序都要加载到内存,操作系统要怎么处理呢?
答案是:先描述,再组织。我们可以把程序的特征描述出来,然后再用特定的数据结构去维护他
这里的描述就提到了PCB进程控制块,PCB进程控制块就控制着磁盘加载到内存的数据,那么操作系统对进程的管理就变成了对PCB进程控制块的管理,也就是对数据结构的增删查改,这样就能很方便的实现管理了。
描述进程-PCB
task_struct-PCB的一种
task_ struct内容分类
组织进程
查看进程
![](https://i-blog.csdnimg.cn/blog_migrate/439af9745ef5ae8510c6d008a1a74cff.png)
大多数进程信息同样可以使用top和ps这些用户级工具来获取
下面写一段简单的代码,来看看我们如何查看我们写的程序的进程
#include <stdio.h>
#include <sys.h>
#include <unistd.h>
int main()
{
while(1)
{
sleep(1);
}
return 0;
}
通过以上指令我们就可以查看到当前我们写的代码的进程了。上述的死循环代码,我们可以通过ctrl +c就可以让其停下来。
通过系统调用获取进程标示符
首先要知道父进程和子进程两个概念, 首先一般情况下的父进程都是bash,一般你的程序不是由父进程维护的,一般都是子进程进行维护,其中的子进程时由父进程来管理的。
![](https://i-blog.csdnimg.cn/blog_migrate/14333afccfa269b5f011b5b79eb6f953.png)
![](https://i-blog.csdnimg.cn/blog_migrate/fbce2498fb2bce5e5e7aef9b86fc88a0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9d66339c5c190e85fffd0adc8e3f86c6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1e503b8c79ed7593680933cd74acaec4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/fbd88819bc5e1db84aee0bdd32d4e29d.png)
这个结果应该很让你震惊,首先if ,else语句竟然能同时跑起来,并且死循环下两个语句竟然没有一点干扰。这就是进程的知识,这也就是为什么操作系统和语言不同的地方。
为什么出现这个情况呢?
首先我们必须承认的是刚开始的时候这有两个进程,一个父进程,一个子进程,其中的父进程是bash,之后有创建了子进程。而这个子进程就是上个子子进程的子进程,也就是这里包含了爷子孙子的关系。fork创建之后,后面的代码被父子进程共享了。就会导致这个现象的发生,父子进程各自执行各自的,互不影响。那么那个id和返回值又是怎么回事呢?我们可以通过man来查看fork成功之后的返回值,就知道ret的问题,然后创建的子进程是要比父进程的id更大的,这个是普遍的情况。
通过手册就能很好的解释了返回值的问题。
进程状态
因为每个操作系统的进程状态的叫法都不太相同,我们先看看操作系统这门课程中的3个重要的状态:运行、阻塞、挂起。
前面已经解释过了CPU的速度很快,但他只有一个,所以进程进入CPU的运行队列 中,如果该进程在CPU的运行队列中,那么就说明它正处于R(也就是运行状态)
如果进程需要进行IO过程,那么该进程就不能在CPU的运行队列中,那么他们就会在IO的接口的队列中等待(因为外设都是很慢的,相对CPU来说),那么他们处于这种状态就是处于阻塞状态。
如果我们加载了大量的数据到内存中,内存有可能不够用的时候,处于阻塞状态的数据就会显得很浪费空间,这时操作系统可能就会把这些正在处于阻塞状态的数据先加载到磁盘中,并用指针记录他们的位置,方便下次调用。这种情况就是进程处于挂起状态。
看看Linux内核源代码
/*
* 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 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
通过查看Linux的内核源代码,我们就会发现好像linux的处理方式和操作系统这本书有点不同,因为操作系统这本书概括的是所有的操作系统,不能把每个操作系统都覆盖,通过看Linux操作系统的设计,我们就能发现:Linux操作系统的进程状态是整数维护的。也就是上面的整数分别代表不同的状态。
![](https://i-blog.csdnimg.cn/blog_migrate/283d45abfb79454b4e0f18b2ca7b2fc3.png)
有IO过程的代码,基本都会处于S状态,那么为什么代码明明在运行,而且刚刚也看到了那个死循环的代码确实是一直在输出的,怎么就处于了S状态了?因为CPU的速度远远高于IO设备,所以那些指令大多都是在IO的队列中排队,等待IO的输出,所以基本上都处于阻塞状态也就是Sleep状态了。至于这里的+是什么意思呢?这里的+是前台运行的意思。如果没有+就是后台运行,后台运行的代码我们就不能使用ctrl + c去终止它,我们只能通过kill -9 加id来杀掉这个进程。
进程状态查看
我们可以通过指令ps axj | grep +文件名 | grep -v grep这样就可以过滤掉除了当前文件进程之外的进程,这样就可以很好的查看到当前的进程了。
Z(zombie)-僵尸进程
除了前面涉及的Linux进程之外还有两个特殊的进程,一个僵尸进程,一个孤儿进程。
首先为什么要有僵尸进程呢?进程在退出的时候不是立刻释放进程的资源,应该保存一段时间由父进程或者操作系统来读取。此时子进程就会变成僵尸进程。
僵死状态( Zombies )是一个比较特殊的状态。当进程退出并且父进程(这里暂时不做讲解)没有读取到子进程退出的返回代码时就会产生僵死 ( 尸 ) 进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以, 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态下面通过代码的方式来见见僵尸进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t ret = fork();
if (ret < 0)
{
printf("fork fail");
return -1;
}
else if (ret == 0)
{
//子进程
while (1)
{
printf("pid:%d ,ret = %d\n", getpid(), ret);
sleep(30);
exit(-1);
}
}
else
{
while (1)
{
//父进程
printf("ppid:%d, ret = %d\n", getppid(), ret);
sleep(2);
}
}
return 0;
}
我们看看运行的结果:
我们可以看到子进程就处于僵死状态了。
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。 父进程如果一直不读取,那子进程就一直处于Z状态维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 task_struct(PCB) 中,换句话说,Z 状态一直不退出, PCB 一直都要维护。如果 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费 ,因为数据结构对象本身就要占用内存,想想C 中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。这个的话就必然导致一个大问题就是 内存泄露!
孤儿进程
与僵尸进程相对,孤儿进程就是父进程提前结束,然后剩下了子进程,这样的进程就是孤儿进程,名字也十分的形象。那么孤儿进程怎么回收呢?孤儿进程会被1号init进程领养,当然要有init进程回收。
下面看看Linux下的孤儿进程,代码如下:
先看看结果:
其中要注意的几点:首先如果父进程退出,那么子进程将会在后台运行,看上面的结果我们也知道了,该进程处于S状态,这个时候我们没有办法通过ctrl + c来结果程序,我们应该必须通过kill指令才能终止这个程序。就-9就是杀掉后面的进程的。
进程优先级
基本概念
首先有一个问题:为什么要有优先级?答案是 资源太少了,想想那么多的进程,但是CPU只有一个,就明白优先级对计算机的效率的影响有多大。cpu 资源分配的先后顺序,就是指进程的优先权( priority )。优先权高的进程有优先执行权利 。配置进程优先权对多任务环境的 linux 很有用, 可以改善系统性能。还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU ,可以大大改善系统整体性能。优先级的本质就是PCB中的一个或者几个的整形的数字,而Linux中是支持调整优先级的。下面会讲到如何通过调整nice值来调整优先级。
查看系统进程
在Linux中我们可以通过ps -l来查看进程
UID : 代表执行者的身份PID : 代表这个进程的代号PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号PRI :代表这个进程可被执行的优先级, 其值越小越早被执行NI :代表这个进程的 nice值
PRI and NI
用top命令更改已存在进程的nice:
![](https://i-blog.csdnimg.cn/blog_migrate/5b488c9c5d9c8fac68e72a4453459f0a.png)
刚才我改的nice值是-100,但是最终正如Linux所展示的一样,最小也是-20,最大则是19。
PRI vs NI
需要强调一点的是, 进程的nice值不是进程的优先级 ,他们不是一个概念,但是 进程nice值会影响到进程的优先级变化。可以理解 nice 值是进程优先级的修正修正数据,根据上面的公式,我们不难理解, nice的改变是会影响pri值的,从而影响优先级。
其他概念
竞争性 : 系统进程数目众多,而 CPU 资源只有少量,甚至 1 个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰并行 : 多个进程在多个 CPU 下分别,同时进行运行,这称之为并行并发 : 多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发这里重点讲解并发概念, 因为很多计算机都是只要一个CPU的,但是进程却很多。那么怎么合理使用CPU呢?前面在介绍优先级的时候就提到了进程在CPU运行的时间是有限的,我们把这个时间称为时间片,也就是说在这个时间片里面,CPU只属于这个进程,此时进程的所有信息就会加载到寄存器中,等这个时间片过去了这个进程必须把当前的信息保存(上下文),方便下一次CPU运行该进程的时候知道运行到哪里了。等到下次再次运行的时候再把上下文信息加载到CPU,这样的运行方式就是并发。
环境变量
基本概念
环境变量 (environment variables) 一般是指在 操作系统中用来指定操作系统运行环境的一些参数 ,如:我们在编写C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在 系统当中通常具有全局特性(环境变量可以理解为我们呢学习C语言是的全局变量,那么对应的局部变量就是你在xshell中,以命令行的形式直接定义的变量就是本地的变量(也可以理解为局部变量))
![](https://i-blog.csdnimg.cn/blog_migrate/53ab10511fd5ff2dc2c33d3a08fd0211.png)
我们可以通过set来查看到我们写在本地的变量(可以看成局部变量,看上面我们使用env来查看环境变量的时候,我们发现是找不到的,这也就证实了刚刚所说的)
常见环境变量
PATH : 指定命令的搜索路径HOME : 指定用户的主工作目录 ( 即用户登陆到 Linux 系统中时 , 默认的目录 )SHELL : 当前 Shell, 它的值通常是 /bin/bash
查看环境变量方法
测试PATH
在Linux中为什么我们写的二进制可执行程序需要./ + 可执行程序才能运行呢?而有些指令确实可以不需要的。在解决这个问题前,我想说明一个结论就是指令就是程序。那么指令是怎么来的呢?其实指令的底层实现就是C语言以及汇编代码写出来的。因为这些指令使用的频率非常高,所以就把他们设置成环境变量,因为这是全局的,所以我们不需要在当前路径下才能找到,而是在全局的任何地方都是能使用的。
在我们还没有将当前文件设置成环境变量的时候就没有办法找到,我们可以通过以下方法进行环境变量的修改
方法一: 通过拷贝我们的指令到系统默认的路径下就可以实现,拷贝的本质就是安装
这是删除方法:
方法二:export加之前的环境变量加上当前文件路径(不能直接加文件路径,这样就会把之前的路径覆盖掉,不过也不需要担心,只要我们重新登录一下就可以解决这个问题)
测试HOME
为什么这里root路径的主工作目录和普通用户的主工作目录不同呢?这里其实和环境变量PWD有关,也就是我们平时使用的pwd。它会记录你当前的路径,下面通过比较USER中的普通用户和root的区别
通过这段代码我们就可以知道指令是怎么实现的了。下面看看结果;
在当前用户下查看:
但是在root下查看就不是这种情况了:
这就是环境变量的作用,同时这也说明了指令其实就是程序,只不过把这些程序添加到了环境变量中罢了,这样我们就可以随时使用。
和环境变量相关的命令
环境变量的组织方式
通过代码如何获取环境变量
1.通过命令行第三个参数
![](https://i-blog.csdnimg.cn/blog_migrate/b286e27b1d8997024a228aec82b84330.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2f27525e3298b1ad4d8027ff92ca1014.png)
我们就可以得到这个结果:
这也就解释了指令其实也是通过C语言或者汇编语言写的,上面就是具体的实现方式。
通过第三方变量environ获取
其实第三方的environ和上面的实现方式本质是一样的。二级指针和指针数组是等价的。
下面是实现方式:
结果当然和上面的结果是一样的。我们可以通过增加环境变量看看代码是否正确;
我们就可以找到刚刚我们输入的环境变量。
通过系统调用获取或设置环境变量
这里的mytest会获取到这个环境变量的根本原因就是环境变量可以被子进程继承。
我们写的程序就是一个bash的子进程,因为环境变量具有全局属性。所以子进程也能获得环境变量,从而显示出来。继承的目的就是更加方便的使用,让bash帮我们找指令路径,身份认证
程序地址空间(不准确)
在讲解之前我们先看看一个用之前的知识不能解决的问题:
我们看看这个结果:
这个结果应该是相当让人震惊。子进程把全局变量修改之后,父进程的g_val竟然还是0,这个还好理解,因为父子进程共享代码,同时他们不是同一个进程,所以两个值不同很正常。但最让人感到震惊的是他们的地址竟然是相同的。如果用我们以前的知识来回答就没有办法回答了。因为同一地址的值不可能相等,除非他们的地址不相同。那该怎么解释呢?
通过上面的分析可以得到以下结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量但地址值是一样的,说明,该地址绝对不是物理地址!在Linux地址下,这种地址叫做 虚拟地址我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理O S必须负责将 虚拟地址 转化成 物理地址 。
那么我们该怎么理解这些结论呢:
首先我们可以这么认为每一个进程都是独立的。站在进程的角度我们可以认为进程在一个人想用内存的资源。所以每一个进程都有一个虚拟的地址空间。而这个空间和内存大小相同。那么进程的地址空间也是要被操作系统管理的,那怎么管理呢?当然是先描述,后组织。
我们可以用一个数据结构对他进行管理,也就是一个对象mm_struct。所以进程地址空间的本质就是内核的数据结构。
根据我们之前所学进程就等于内核的数据结构+进程对应的代码和数据。其中数据结构是独立的,代码和数据也是独立的,那不就等于进程是独立的吗。
那么上述步骤的完整过程就是以下这样的:
进程地址空间
刚刚我们解释了进程地址空间是什么,现在我们来谈谈为什么有程序地址空间
第一:如果进程直接访问物理内存,这是不安全的。一旦访问越界那么就可能导致程序改掉了。这样我们用户是访问不到实际的物理地址的。
第二:为了方便进程和进程数据的解耦,这样保证了进程的独立性。
第三:让进程以统一的视角来看待进程的代码和数据的区域,方便编译器也以统一的方式来编译代码。因为这具有规则性。所以都要遵守。
前两点比较好理解,那么第三点怎么理解呢:
在我们平时写代码调试的时候相信大家都调试过。尤其在我们看反汇编的时候,我们就会发现在那个阶段就有了地址了。这个也就是虚拟地址。所以在编译器来看,这些都是虚拟地址。这是大家都有遵守的。怎么理解呢?
下面通过画图来理解:
实际上这里是有两套地址的。第一套就标识了物理存在中的代码和数据,另外一套就是程序互相跳转使用的虚拟地址。
其是cpu很“笨”,cpu只会分析指令,但是他的速度很快。通过我们对进程地址空间的学习,我想大家应该对操作系统有了一定的了解。可以说没有操作系统就等于没有计算机。相信学习到这里都能感受到操作系统的强大。
通过进程地址空间的学习,我们知道了实际的地址并不是我们在C语言中学习到的简单的栈,堆,静态区等等的空间,只是语言这个学科没有办法解释这个问题才使用的不准确的回答。通过学习操作系统我们就很深刻的理解了地址的底层原理以及为什么要这样设计。