目录
一、冯诺依曼体系
1、是什么
冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同。数学家冯·诺依曼提
出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。
冯诺依曼体系结构中分为 输入设备,输出设备,存储器,运算器,控制器。
存储器 主要说的是内存,并不是我们所说的磁盘
输入设备:键盘,鼠标,摄像头,话筒,磁盘,网卡……
输出设备:显示器,音响,磁盘,网卡……
CPU主要包含两个部分:运算器,现实生活中的运算主要就是两种,算数运算,计算数字例如1+1=2,还有一种是逻辑运算,主要是用来判断对错,if() else……
控制器:CPU响应外部事件,协调外部就绪事件,例如拷贝数据到内存。
注意点:我们经常会对内存和磁盘搞混两者经常傻傻的分不清,
我们以手机举例:我们知道手机有8+128,12+256的,其中8和12就是内存,128和256就是磁盘
2、为什么
我们一定会特别好奇,为什么输入设备不直接与CPU进行交互,而是要经过存储器呢?
我们要知道CPU的速度特别的快,与CPU相比输入设备,输出设备的速度就慢很多了
现代计算机的存储设备一般有 Cache、内存、HDD(SSD) 硬盘。这些存储设备越靠近 CPU 速度越快,容量越小,价格越贵。
- 寄存器(Register):寄存器与其说是存储器,其实更像是 CPU 本身的一部分,只能存放极其有限的信息,但是速度非常快,和 CPU 同步。
- 高速缓存(CPU Cache):使用 SRAM(Static Random-Access Memory,静态随机存取存储器)的芯片。
- 内存(DRAM):使用 DRAM(Dynamic Random Access Memory,动态随机存取存储器)的芯片,比起 SRAM 来说,它的密度更高,有更大的容量,而且它也比 SRAM 芯片便宜不少。
- 硬盘:如 SSD(Solid-state drive 或 Solid-state disk,固态硬盘)、HDD(Hard Disk Drive,硬盘)。
不同层次存储器设备特点:
- 越靠近 CPU 速度越快,容量越小,价格越贵。
- 每一种存储器设备只和它相邻的存储设备打交道。比如,CPU Cache 是从内存里加载而来的,或者需要写回内存,并不会直接写回数据到硬盘,也不会直接从硬盘加载数据到 CPU Cache 中,而是先加载到内存,再从内存加载到 Cache 中。
也就是说:
CPU&&寄存器 >> 内存 >> 磁盘/SSD >> 光盘 >> 磁带
它们的速度差别是十分巨大的基本上每一级相差一个数量级,且速度越快价格越昂贵
为了使电脑能够广泛传播,降低售价。所以引入了内存的概念。
内存与输入设备速度的相差,与内存和CPU的速度差相差不大。所以降低了成本,提高了速度。
3、怎么做
CPU读取数据(这里的数据不单单指的是数据,还有代码),都是要从内存中读取,站在数据的角度,我们认为CPU不直接跟外设直接交互,而是通过内存这个桥梁间接与外设交互
CPU要处理数据,需要先将外设中的数据,加载到内存,站在数据的角度,我们认为外设直接和内存打交道。
也就是说程序必须要先加载到内存,才能够被CPU执行,这是由它的体系结构的特点决定的。
这里有一个脑筋急转弯:我们写好的代码是放在哪里的呢?
答案是在磁盘中,我们写好的代码并没有运行起来,所以也就没有加载到内存中,所以并不是在代码段中。
二、简单认识操作系统
1、概念
操作系统(英语:Operating System,缩写:OS)是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务来组织用户交互的相互关联的系统软件程序,同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。
2、设计目的
我们在这张图中发现操作系统上面有软件层,下面有硬件驱动。
操作系统就是用来进行管理的软件,给用户提供一个稳定安全简单的执行环境。
操作系统就是管理者,硬件就是被管理者,管理者与被管理者可以不直接沟通就可以进行管理,因为拿到被管理者的核心数据,来进行决策,才是最重要的。
3、设计理念
操作系统对下管理硬件驱动,对上要提供系统调用接口。操作系统的功能有内存管理,文件系统,进程管理,驱动管理四大功能
操作系统就好比决策者,它需要有与之相应被管理对象的数据,然后对被管理对象进行描述,根据描述类型,定义对象,就可以把对象组织成数组,对学生的
管理工作就变成了对数组的增删查改。
因此操作系统的设计理念就是先描述,再组织
因为Linux操作系统是使用C语言和部分汇编实现的,因此我们也就很容易猜出,对对象的描述就是使用的struct,我们也就可以进一步断言,操作系统内部一定有大量的数据结构和算法。
小结:
操作系统的管理理念:先描述,再组织
操作系统提供了系统接口:shell外壳程序,图形化界面,第三方库(C库,C++库,boost库等)
操作系统对于所有人都不信任对外提供接口的方式叫做系统调用。
OS包括内核和其它程序
内核:进程管理,文件管理,内存管理,驱动管理
其它程序:shell外壳程序,函数库
操作系统的目的是:与硬件交互,管理所有的软硬件资源,为用户程序提供良好的执行环境。
三、进程
1、进程概念
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
启动一个软件:本质是启动了一个进程
代码是储存在磁盘中,在磁盘中无法被操作系统管理,所以软件要被加载到内存中才能被操作系统管理。
在Linux下运行一条命令,在系统层面上就是创建了一个进程,变成进程才能被操作系统管理。
Linux可以同时加载多个程序,同时存在大量的进程在OS中。
同理对于操作系统管理进程也是先描述,再组织
对于一个可执行程序来说:它包含代码和数据,它们的本质都是文件。
2、描述进程
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct
task_strcut包含以下信息
标示符: 描述本进程的唯一标示符,用来区别其他进程。状态: 任务状态,退出代码,退出信号等。优先级: 相对于其他进程的优先级。程序计数器: 程序中即将被执行的下一条指令的地址内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。其他信息……
所有运行在系统里的进程都以task_struct结构体链表形式存在内核中。
3、查看进程
Linux查看进程的指令是ps
什么选项不加我们只能查看当前终端的进程
我们在ps后面加上axj选项就可以查看系统中的全部进程
这里就截取了一部分进程
我们编写一个程序,然后让它运行起来,再观察情况
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
while(1)
{
printf("I am process\n");
sleep(1);
}
return 0;
}
这就实现了一个死循环,然后我们再次使用ps命令查看进程
我们就发现了test这个进程,然后我们在ps命令中加入更多选项使我们能够更清楚的查看
我们发现有PID和PPID这两个id值
PID就是进程id
PPID就是该进程的父进程
还有一种查看进程的方法,Linux中有一个proc目录,我们可以使用ls命令查看目录内容
这个目录内部的子目录就是我们的进程
我们还记得test进程的pid是27591
我们进入这个目录
我们发现这里面也有很多东西
我们就说明两个
cwd 是当前进程的工作目录,每个进程都会有一个属性,来保存自己所在的工作路径
exe是可执行程序的所在目录
4、创建子进程
既然我们已经知道PID和PPID以及进程的相关概念,那么如何创建子进程呢?
首先,我们要知道如何使用代码获取程序的pid和ppid
我们可以查看man
使用getpid和getppid这两个函数来获取pid和ppid,它们的返回值是pid_t是无符号整型
然后再查询fork函数
我们这里只截取了一段信息,大概可以知道fork函数可以用来创建子进程
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("I am a process\n");
fork();
sleep(1);
printf("you can see me\n");
pid_t id = getpid();
printf("My pid : %d \n",id);
return 0;
}
这里发生的现象可能有点怪
printf语句执行了两次,id值也不同了,但是这两个id值是用一个变量保存的怎么可能会这样呢?
这就是我们创建的两个进程
fork函数的返回值也会有点怪,如果进程创建失败会返回-1.创建成功,会对父进程返回子进程的pid对于子进程返回0
这个也与我们正常语言层面的理解不符,一个函数只能返回一次,而fork函数竟然返回了两次。
我们先想一个问题:在一个函数马上返回时,它的核心代码执行完成了吗?
答案很显然,已经完成了。
对于fork函数来说,在它返回id值之前,它已经执行完了,创建子进程的相关代码,也就是说在返回id值之前,就已经创建好了子进程,对于父进程和子进程来说,它们创建好子进程之后的代码是共享的,fork最后一步返回时父进程会返回一次,子进程也会返回一次,这就导致了fork会有两个返回值。
我们可以根据fork的返回值不同来使父进程和子进程分别执行不同的代码
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
printf("I am child process\n");
}
else
{
printf("I am parent process\n");
}
return 0;
}
这里还需要补充一点
bash就是我们的shell外壳程序,它每次启动时PID都会发生变化,在同一个终端下PPID不会发生变化,在同一个终端中bash的PID都是固定的。
每次重新打开一个终端,操作系统就会创建一个子进程bash
bash通过创建子进程来进行工作,关闭程序并不会关闭bash
为什么给子进程返回0,父进程返回子进程的PID?
因为子进程只能有唯一确定的父进程,而父进程可以有多个子进程。
父进程想要管理子进程时就需要知道子进程的pid
通过上面的 例子,我们大概明白了操作系统是如何管理进程的。
每个CPU要想执行进程,都会给自己维护一个运行队列run_queue
操作系统和CPU运行某一个进程,本质是从task_struct形成的队列中挑选一个task_struct来执行它的代码。
这里又有一个问题:父子进程被创建出来,哪一个进程先被执行?
它们的执行顺序是不一定的,由操作系统调度器来决定。
5、进程状态
我们知道操作系统管理一个进程,首先先要描述进程,描述进程的方式就是PCB,而所谓的进程状态就是PCB结构体里面的成员,对于不同的状态相应的成员具有不同的值
由此也就描述了进程的不同状态。
下面我们就介绍具体的进程状态
首先是:新建状态,就是字面意思,操作系统刚把相应的PCB创建出来。
运行状态:task_struct结构体在运行队列中排队就叫做运行状态。
就好比你在食堂排队打饭,而你的朋友问你在干什么,你说在吃饭的意思一样。你可能没有吃上饭,也就是说,该进程的代码可能没有运行起来。
系统中一定存在除了CPU以外的其它资源,如网卡,磁盘,显卡,系统不只存在运行队列。
阻塞:等待其它资源就绪(非CPU资源)的状态就叫做阻塞状态。
例如我们常用的scanf cin 等等,只有我们在键盘中输入数据,CPU才会进行下一步的处理。
我们不给它输入,这不就是等待键盘资源就绪吗?
挂起状态:当内存快要不足的时候,操作系统通过适当的置换进程的代码和数据到磁盘中的SWAP分区,这种状态叫做挂起。
当服务器压力过大的时候,操作系统会通过一定的手段,杀掉一些进程,来起到节省空间的作用。
也就是说挂起状态的进程只剩下PCB结构体,代码数据全部在磁盘中,保留PCB是为了在内存资源不紧张的时候,将该进程的代码和数据恢复到内存中。
挂起和阻塞状态都和CPU无关了。
上面讲述的是操作系统抽象的进程状态,下面具体讲述Linux操作系统的进程状态。
Linux操作系统一共有7种状态。
进程分为前台进程和和后台进程
R:就是上面的运行态。
S:对应的就是上面的阻塞状态,S就是可中断睡眠。可中断睡眠的意思是可以随时对S状态的进程发信号,可以使用kill指令对S状态修改
D:睡眠状态,磁盘睡眠,深度睡眠,不可以被中断,不可被动的唤醒。
D状态是一种针对于磁盘的状态,它的情况是:假如进程A向磁盘中刷新数据,进程A等待磁盘刷新结果,若A是S状态,操作系统资源紧张,操作系统会杀掉A进程,磁盘向进程A发送刷新成功或者失败的信号,因为A已经被杀掉应对这种我们可以使用D状态。来防止CPU将进程杀掉。
6、僵尸进程
1、介绍
Z状态就是僵尸进程,我们看到僵尸进程的旁边有<defunct>失效的说明。
僵尸进程是进程退出,但是父进程没有读取到子进程退出的返回代码,就会产生。
子进程的代码和数据退出了,但是PCB没有被释放,因为维护僵尸进程也是一种状态,也需要被维护。占用内存资源,会产生内存泄漏。
进程退出的状态必须被维持,如果父进程一直不读取子进程的退出状态,子进程一直处于Z状态。
维护退出状态本身也属于进程的基本信息,保存在task_struct中,Z状态一直不退出,PCB一直都要维护。
2、模拟实现僵尸进程
僵尸进程十分容易被实现,僵尸进程也就是子进程退出了,父进程没有处理子进程。
我们可以让子进程先退出,父进程一直死循环。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
size_t cnt = 0;
while(cnt < 5)
{
printf("I am child pid %d \n",getpid());
cnt++;
}
}
else
{
while(1)
{
printf("I am parent %d \n",getpid());
}
}
return 0;
}
7、孤儿进程
1、介绍
孤儿进程是指父进程先退出,子进程依然在运行。
孤儿进程会被1号进程init进程领养,为了子进程在退出时能够被回收资源。
2、模拟实现
我们在僵尸进程的基础上进一步修改,就可以得到孤儿进程
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 if(id < 0)
8 {
9 perror("fork");
10 return 1;
11 }
12 else if(id == 0)
13 {
14 while(1)
15 {
16 printf("I am child\n");
17 }
18 }
19 else
20 {
21 size_t cnt = 0;
22 while(cnt < 5)
23 {
24 printf("I am parent\n");
25 cnt++;
26 }
27 }
28
29 return 0;
30 }
我们再使用ps指令查看进程状态
我们发现这个进程的状态是S还是后台进程,我们无法使用ctrl + c来终止程序,我们只能使用9号信号杀掉这个进程。
这个进程的PPID是1号init进程。这也就验证了,我们成功实现了一个孤儿进程。
8、进程优先级
1、为什么
为什么要有优先级?因为CPU是有限的,进程太多,需要通过某种方式竞争资源,确认是谁应该获得某种资源,谁后获得。可以使用数据来表示优先级:PCB中的一些字段。优先级是调度器的调度指标。优先级必须要被调度器所使用才具有一定的意义。
2、怎么做
Linux的具体优先级做法是
PRI(new) = PRI(old) z + nice
优先级 = 老的优先级 + nice值
nice值是进程优先级的修正数据
PRI代表这个进程可被执行的优先级,值越小越早被执行
NI代表当前进程的nice值,它的取值范围是(-29~19)
PRI值减小,优先级提高
调整优先级的本质是调整nice值
3、调整优先级
使用top指令来修改优先级,在系统默认情况下优先级的值只能调大nice值,也就是减小优先级。我们可以使用sudo指令来提高优先级
在top下输入r
然后输入想改优先级的PID,输入nice值,输入好后使用ps指令查看优先级
它的优先级就会发生改变
为什么系统只允许我们修改的nice值的范围是(-20~19)它系统默认的PRI是80,为什么范围不能更大?
因为对于一款好的操作系统要保证每个进程的优先级不能相差太大,尽量保证每个进程运行的公平性。
四、其它概念
1、竞争性
系统内的进程数目众多,CPU资源只有少量,进程之间具有竞争性,为了高效的完成任务,所以进程具有优先级。
2、独立性
多进程运行时,需要独享各种资源,多进程运行之间互不干扰。
一个进程挂掉并不会影响其它进程,例如使用浏览器看B站,然后你的QQ挂掉了,并不会影响你看视频。
3、并行
多个进程在多个CPU下分别同时进行运行。
并行的前提是具有多个CPU
4、并发
多个进程在一个CPU下采用的进程切换的方式,在一段时间内让多个进程都得以进行推进。
每一个进程都不是永久占用CPU,要不我们写一个简单的死循环程序就会导致系统崩溃。
5、时间片的抢占与出让
如果我的时间片没有到,我就跑完了,操作系统就会将我拔下来,让运行队列的下一个接着跑。
到我该跑了,我的优先级是80,时间片是10ms,我执行到5ms来了一个优先级更高的,就会抢占我。操作系统将我拔下来让优先级更高的先跑。
一个CPU在任何时刻都是只有一个进程在跑,但是在一个时间段内,多个进程都得以进行推进。
在并行中的每一个CPU,都是并发的。
6、切换
CPU的寄存器分为 给用户进程所使用的可显寄存器,和CPU内部的自己所维护的寄存器。
CPU中存在大量的寄存器。
我们所说的把数据加载到CPU中实际上是加载到CPU中的寄存器。
如果有一个进程A,在运行进程A时,CPU中的寄存器保存的都是A的临时数据。寄存器中的临时数据叫做A的上下文数据。
上下文数据是不可以被丢弃的。当进程A暂时被切下来时需要进程A带走自己的上下文数据
带走上下文数据保存的目的:就是为了下次回来的时候能恢复上去,就能按照之前的逻辑继续向后运行没就如同没有被中断过一样。
CPU内的寄存器只有一份,但是上下文数据可以有多分,分别对应不同的进程。
五、环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
1、PATH
我们知道一个现象,系统命令可以直接运行,但是我们自己写的程序必须要带上路径
这是因为环境变量PATH中保存了系统命令的路径,我们输入系统命令,它就会在PATH中的路径寻找。
echo $PATH
查看PATH中的路径
这里的路径以冒号为分隔符,它是由几段路径所组成的
我们也可以设置自己的程序路径,但是我们最好不要这样做,我们写的程序或多或少会有一些bug,在服务器上运行几万次很有可能就会出现错误,而且会误导人,别人查看命令时出现了几个Linux系统没有的几条命令。
下面是设置自己程序路径的方法
export PATH=$PATH+你的程序路径
1 #include <stdio.h>
2
3 int main()
4 {
5 printf("Hello World\n");
6 return 0;
7 }
我们先写了一个简单的程序
然后我们使用上述方法添加程序路径
我们先pwd找到我们的程序路径然后添加。
我们发现成功添加了
2、HOME
HOME就相对简单了它是指定用户的主工作目录
3、SHELL
当前shell值,通常是/bin/bash
4、其它常见的指令
env:显示所有的环境变量
echo:显示某个环境变量
unset:清楚环境变量
set:显示本地的shell变量和环境变量
5、命令行参数
问大家一个问题main函数可以有几个参数?
答案是三个参数:分别是argc 、argv、 env
int main(int argc, char* argv[], char* env[])
前两个是命令行参数,最后一个是环境变量参数:系统给该进程传递的环境变量参数。
后两个参数都是指针数组
通过上面的例子我们知道了,main函数可以获取环境变量。
1、env
我们可以尝试打印env中的元素
1 #include <stdio.h>
2
3 int main(int argc, char* argv[], char* env[])
4 {
5 for(size_t i = 0; env[i]; i++)
6 {
7 printf("%s\n",env[i]);
8 }
9
10 return 0;
11 }
12
我们发现它env的内容与我们直接输入env指令出来的内容类似。
我们还有一种获取环境变量的方式,通过借助第三方变量environ来获取全部的环境变量
我们通过man手册查看该变量
发现它这个第三方变量的用法与env类似。
下面是演示代码
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 extern char** environ;
7 for(size_t i = 0; environ[i]; i++)
8 {
9 printf("%s\n",environ[i]);
10 }
11
12 return 0;
13 }
14
前面的两种方法很少使用,我们主要使用getenv
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 printf("begin………………\n");
8
9 printf("%s\n",getenv("Hello"));
10
11 printf("end……………………\n");
12 return 0;
13 }
我们的操作系统中现在没有Hello这个环境变量
他会出现段错误。
是因为操作系统中没有Hello这个环境变量,他没有找到返回NULL,而打印是使用的%s,它会从低地址一直向高地址打印直到找到\0
然后我们使用export添加Hello这个环境变量
这时我们打印就不会出现问题
一般环境变量由父进程继承下来,在命令行上运行命令,所有的命令的父进程都叫做当前bash
我们使用export导入的环境变量,系统并不认识,关掉重启后就会消失。默认所有的环境变量都会被子进程所继承,bash的环境变量从操作系统中来,环境变量具有全局属性,从bash开始的多叉树一路继承下去。
2、argc、argv
argc是传入的参数的个数,argv是一行传入的所有字符串
我们写一个简单的程序证明一下
1 #include <stdio.h>
2
3 int main(int argc, char* argv[])
4 {
5 for(size_t i = 0; i < argc; i++)
6 {
7 printf("%s\n",argv[i]);
8 }
9
10 return 0;
11 }
有人会问这有什么用?
我们输入的Linux指令很多后面都带有选项,我们就可以通过这两个参数实现类似与Linux系统命令的程序了。
总结
例如:以上就是今天要讲的内容,本文仅仅简单介绍了进程相关概念。