一.冯诺依曼体系
在计算机中,CPU(中央处理器)是不直接跟外部设备直接进行通信的,因为CPU处理速度太快了,而设备的数据读取和输入有太慢,而是CPU以及外设直接跟存储器(内存)打交道,外部设备将数据输入进内存中,然后CPU从内存中读取数据,运算完数据再将数据放回内存,然后输出设备就从内存中读取数据,这一个体系的好处就是可以很好的利用了CPU的高速运算的优势
二.操作系统
2.1什么是操作系统
操作系统就是一个软件,它管理计算机的所有的软硬件资源,包括进程管理,内存管理,文件管理,驱动管理
2.2.什么是管理
计算机软件体系
像我们用过的cout函数就是会贯穿这整一个软件体系。
在上面那个图中,操作系统相当于大脑,而驱动程序就相当于四肢,硬件设备就相当于武器,为了很好的管理四肢,就需要用到先描述再组织的方法,就是说大脑先要知道每个肢体的具体属性,然后将他们以结构体的形式描述出来,最后将每个肢体再通过链表的方式组织起来,方便查找管理。
2.3操作系统如何向上一层提供基本功能呢
OS是不信任任何用户的,那么OS就通过系统调用接口来给用户提供基本功能,这样子就可以防止恶意用户对于操作系统的破坏
2.4系统调用接口与库函数的关系
上下级关系!
库函数一定是在系统调用之上的,将系统调用进行封装,然后供用户去使用!
三.进程的概念
3.1什么是进程(笼统认识)
在书本中,加载到内存中的程序,就叫做进程。
而我们上面有说过操作系统管理的理念是先描述再组织,由于在操作系统中有大量的进程,那么按照先描述再管理的理念,如果描述进程呢?Linux中采用的是PCB(进程控制块)来描述进程
PCB又是什么呢?
task_struct里面有什么属性字段?
进程是怎么看的?
程序运行起来之后,在linux中使用命令ps axj | head -l && ps axj | grep "目标文件" 来查看运行中的进程
曾经我们所有的启动程序的过程,本质都是在系统上面创建进程
那么程序和进程有什么区别呢?
在我们打开磁盘上的可执行文件的时候,那么可执行程序就会被加载到内存当中,然后操作系统为了管理这一段在内存中的程序,就会生成一个PCB(进程控制块)来管理它,所以说,从操作系统的角度,我们知道进程就相当于程序文件内容+与进程相关的数据结构
在上面这个图中,绿色的小框框表示进程代码+数据,红色的小框框表示包含了进程内部所有属性的进程控制块,各个进程控制块会以链表的方式连接在一起,这样子操作系统就可以通过先描述再组织的方式来管理各个进程了,而操作系统想要管理进程只需要管理进程地址控制块,就可以达到管理进程的效果,所以说有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系!!!,与进程对应的内核创建的该进程的PCB强相关!
进程和程序的区别:进程 = 程序 + 操作系统维护进程的相关数据结构!
3.2 PCB的内部构成
标识符:
获取进程pid的方法:系统调用:getpid(),getppid(),其中getppid()是获取父进程的pid。
tips:两种方式杀掉进程:1.ctrl + c,2. kill -9 进程pid
状态:
包括进程结束时候的返回值,退出码,如main函数的return 0,exit(0);还包括进程运行时候的运行状态,等待状态,死亡状态,阻塞状态,挂起状态
优先级:
因为CPU资源是有限的,而进程控制块的数量却有很多,所以CPU想要运行不同的进程,就会有先后顺序,那么就会排队,就会有优先级问题
I/O状态信息:
这里的I/O是指进程打开了文件,然后从设备/文件中读数据,然后向设备/文件中写数据
记账信息:
在操作系统中,有个调度模块,它可以较为均衡的调度每个进程获取CPU资源,保证每个进程都可以被执行,该记账信息就包括了该进程占用的所有的软硬件资源的总和,这样子调度模块就根据这个记账信息来均衡的调度每个进程获取CPU资源
3.3 如何理解上下文数据:
在CPU中,有很多的寄存器,里面存放着正在运行的进程的临时数据;在操作系统中,有着很多进程,每一个进程都需要占用CPU中的寄存器资源来执行自己的程序代码,并且进程的代码可能不是很短的时间就能运行完毕,如果,有一个进程的执行时间很久,并且CPU一直在运行它,那么其他进程的程序和代码就不能运行,这样子就很不公平,
所以说,操作系统规定了每个进程单词运行的时间片,比如10ms,如果某个进程在CPU中运行的时间超过10ms,就会暂定该进程的运行,然后将该进程的运行数据从CPU寄存器中拷贝下来放入到PCB中,然后将该PCB放到运行队列的尾部,让它重新排队,因此我们用户感受到的是多个进程是在同时运行的,这个本质就是CPU的快速切换PCB完成的
由于CPU中的寄存器只有一套,如果进程1占用了CPU的寄存器,然后时间片一过,CPU切换了另外的进程,当CPU再次切换运行进程1的时候,由于没有保存它之前的上下文数据,那么就不能恢复出它的上下文数据,它的临时运行数据就会丢失,所以说CPU在对进程进行切换的过程a中,就需要保存上下文,还要恢复上下文
有了保存上下文和恢复上下文,那么进程就可以在CPU不断切换进程的过程中,保证继续执行上次被切换之前的程序,并且保证上次运行过程中产生的临时数据不会丢失
所以说这里说的上下文数据就是进程在运行的过程中,在CPU寄存器中产生的与进程强相关的临时数据
tips:另外一个查看进程的方法:ls /proc
每创建一个进程,都会在这个/proc目录下创建一个以该进程pid命名的文件夹,这个文件夹中包含了该进程运行中的属性,我们打开该文件夹,发现这个进程有一个exe属性,它表示当前进程在执行哪个文件,还有一个cwd属性表示当前工作路径,这里的当前工作路径就解释了为什么在代码中可以直接找到并使用当前工作目录下的文件
四.子进程
4.1创建子进程
系统调用接口: pid_t fork(void)
在执行这段代码的时候,我们发现,有两个进程 ,执行了同一段代码
上图可以看出进程28787的父进程是28786,而进程28786的父进程是26555
这里的26555是什么呢?
是bash进程:
就相当于bash创建了进程28786,然后进程28786又创建了进程28787
tips:如何在不退出vim的情况下,查看man手册?在命令行输入!man xxx 即可,同理,在命令行输入!make 就可以编译程序,在命令行输入!./xxxx 就可以运行程序
4.2 如何理解fork创建子进程?
1.在Linux中创建进程又三种方式:1. ./xxx执行可执行程序,2. 命令行执行命令 3. fork()
在操作系统角度,上面的创建进程的方式,没有差别
2. 当我们调用fork()创建子进程的时候,那么系统中就多了个进程,就相当于多了一个描述进程相关的内核数据解雇还有进程的代码和数据,这个子进程的代码和数据继承自父进程的代码和数据,它的内核数据结构也会以父进程作为模板,这就解释了为什么上面执行的代码会重复执行两次,因为子进程继承了父进程的代码,
所以说,子进程和父进程代码和数据是共享的 ,不过代码是不可以修改的,数据是可以修改的,但是我们知道,进程之间具有独立性,那如果子进程修改了数据,会影响父进程的数据嘛?答案是不会,因为操作系统通过写诗拷贝的方法来保证进程间数据的独立性
tips:什么是写时拷贝?
进程1和进程2共享数据a,他们在内存中共享同一块地址空间,当进程1想要修改a的时候,操作系统会重新在内存中开辟一个新的空间存放修改过后的a的值,然后进程1中a的值指向这个地址,这样子就可以解决创建进程时,造成的空间浪费问题。
3.fork()的返回值
当我们调用下面这段代码的时候,我们发现
结果有两个不同的返回值,说明父进程返回了子进程的pid,而子进程返回了0;
1.如何理解有两个返回值呢?
当调用fork函数,到达return语句之前,说明子进程已经被创建出来了,此时父进程和子进程共同去执行return语句,就会发生写时拷贝,造成返回值不同
2.如何理解两个返回值的设置
因为父进程跟子进程的数量关系是一对多的,就是父进程可以有多个子进程,所以就给父进程返回了子进程的pid,而子进程只有一个父进程,通过调用getppid即可获得父进程的pid
我们创建子进程,难度就是为了让父子进程做一样的事情嘛,不是的,这样子没有意义,所以我们结合fork返回值,通过if else分流,让父子进程做不一样的事情
输出结果:
在上面,我们可以看到,子进程和父进程在做不一样的事情
五.进程的状态
进程的状态信息在哪里呢?
在task_struct(PCB)中
进程状态的意义:方便操作系统快速判断进程,完成特定的功能,比如调度,例如,当进程处于挂起阻塞状态时,操作系统就不会调度这个进程;它的本质是一种分类
5.1 R状态
R状态是什么?
R状态是运行态,进程处于该状态不一定占用CPU资源
R状态的意义:
在操作系统中,有一个运行队列,里面存放着进程的控制块(PCB),CPU从这个队列中读取PCB,来运行每一个进程,而处于这个队列中的进程都是处于R状态,就是运行状态
R状态验证:
调用
5.2 S状态和D状态T状态
当我们想要完成某种任务的时候,任务条件不具备,需要进程进行某种等待时,就需要S和D状态,比如,进程想要从网卡,磁盘,显示器等外设读取数据的时候,但是这些外设还没有就绪,那么进程就会进入等待状态,在这个等待过程中,该进程不会被调度,直到外设准备就绪,该进程就会从等待队列中剥离出来,然后状态修改为R状态,被放入运行队列中等待CPU处理
所谓的进程,在运行的时候,有可能因为运行需要,可以会在不同的队列里!!!在不同的队列里,所处的状态是不一样的!
tips:当进程在等待CPU资源的时候,他处在运行队列中,当进程等待外设资源时,它处于等待队列中
我们把,从运行状态的task_struct(run_queue),放到等待队列中,就叫做挂起等待(阻塞),从等待队列,放到运行队列,被CPU调度就叫做唤醒进程!!
S状态:可中断睡眠状态,就是说该进程处于该抓状态时可以随时被终止
D状态:深度睡眠状态,如果该进程处于该状态时不可以被终止,就比如,进程要从磁盘中写数据时,进程等待磁盘响应,这个等待过程中,为了防止进程被操作系统杀掉,就变为D状态。
T状态:暂停状态,进程处于这个状态所有的数据都暂停更新了
S状态验证:
由于CPU的处理速度远大于显示器的读写速度,所以进程的大部分时间都处于S状态
T状态验证:
通过Kill命令发送stop信号来验证T状态
tips:状态后面带+号,说明该进程是前台进程,可以通过ctrl + c杀掉进程,但是在进程运行期间,不能输入命令行, 状态后面不带+号,说明该进程是后台进程,不可以通过ctrl + c杀掉进程,可以通过Kill -9 pid杀掉,在进程运行期间,可以输入命令行,
5.3X状态和Z状态
X状态:称为死亡状态,进程处于该状态,就会回收进程资源,包括进程相关的内核数据结构以及代码和数据
Z状态:称为僵尸状态,进程在死亡之前,操作系统会将进程的退出信息写入到PCB中,等待父进程或操作系统回收,这一过程称之为僵尸状态
为什么会有僵尸进程呢?因为操作系统或是父进程需要知道子进程是因为什么原因结束进程的,如果没有僵尸状态,那么进程因何而死就无从得知。
如果大量的僵尸进程不回收,就会造成内存泄漏问题
验证僵尸进程:
代码:
程序正常运行的时候:状态为S:
发出杀掉进程的命令:
进程状态变为Z:
TIPS:在子进程运行期间,父进程挂掉了,那么子进程就被称为孤儿进程,他的退出信息将由操作系统来回收
六.进程优先级
一.什么是进程优先级
优先级是什么?谁先做什么,谁后做什么?优先级本质是分配资源的一种方式
为什么会有优先级
因为资源太少,每个进程都需要获得资源,那就需要排队,所以就有了各种队列,所以就有了优先级来决定哪个进程先获得资源,哪个进程要后获得资源
tips:
程序转后台运行:
在执行程序后面加&
程序转前台运行:命令行输入fg
查看进程优先级命令:ps -al
上面的PRI和NI组合就是进程优先级,PRI的值越小,优先级越高,越容易获得某种资源
tip: UID表示的是谁启动了这个程序,就相当于我的用户名在操作系统的编号
PRI和NI的关系:
二.调整优先级
通过Top命令来调整优先级
更改NI值为10
显示优先级结果,这里的PRI = 80 + 10
如果设置更改NI值为5
显示优先级结果,这里的PRI = 80 + 5
为什么上面更改NI值为10后,新的PRI = 90,而后面在该基础上更改NI值为5,却变成了85呢?
因为PRI(new) = PRI(old) + NI,这里的PRI(old)一般情况下是等于80的,不会变。NI值得变化范围在-20到19
为什么NI值要是一个相对比较小得范围呢?
因为优先级再怎么设置,也只能是一种相对得优先级,不能出现绝对的优先级,不然就会有进程一直处在最高优先级,一直占用着资源,造成严重的进程”饥饿问题“,如果进程的优先级修改范围过大,也会导致某些优先级低的进程可以快速的抢先分配到资源,导致另外那些苦苦等待很久的进程就分配不到资源,也会造成严重的进程”饥饿问题“。
在操作系统中,有一个调度器器,它通过进程优先级,会较为均衡的让每个资源都享受到CPU资源
三.其他概念
进程具有独立性,各个进程之间的运行互不干扰
并行:两个进程分别在两个CPU上运行,真正意义上的多进程同时运行的情况
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
四.环境变量
4.1环境变量的概念和操作
为何运行自己的文件,需要带./xxx,但是运行系统的命令就不用带路径了呢?
因为环境变量!!PATH
因为我们系统在执行命令的时候会去系统的环境变量PATH路径种去查找可执行文件的路径
那这样子我就可以将该执行程序的路径添加到环境变量PATH中,实现像运行系统的命令一样运行我们自己的程序
如何将特定路径设置到环境变量中:
命令行:export PATH = $PATH:(当前文件路径)
查找环境变量命令:
echo $PATH;
echo $HOME
echo $ SHELL
查看所有的环境变量的数据:env
系统存在大量的属性变量来保证当前系统的正常运行,这些属性变量就是环境变量,其中有的环境变量是帮助我们查命令的,有的是保存当前路径的
4.2 和环境变量相关的命令
本地变量:只在本次会话中生效的环境变量,直接在命令行中输入:变量名=xxx;
验证set:
export可将本将本地变量变为环境变量:export 本地变量
main函数的输入参数有两个,分别是argc和argv,其中一个是表示argv,表示命令行传入了多少个字符串,另一个是argv,他表示命令行传入的具体字符串是什么
有了这个argc和argv,那么我们就可以通过命令行传入参数的方式来达到控制程序执行不一样的功能的目的
同时我们也可以在代码中通过参数env来获取环境变量:
还可以通过getenv()在代码中获取环境变量
环境变量是可以被子进程继承的:
我们知道通过命令行启动的应用程序,它的父进程是bash,那子进程的环境变量就继承自bash,而bash又从操作系统中读取环境变量。
我们将本地变量设置为环境变量,然后在代码中读取该环境变量来证实这一点:
当前的理解:环境变量具有“全局属性”,影响整个“用户”系统的本质是:环境变量可以被子进程继承下去
五.程序地址空间
5.1什么是地址空间:
C/C++程序地址空间:
这个程序地址空间是内存嘛
根本就不是内存,那他是什么呢?
验证程序地址空间:
我们通过下面的代码让父子进程读取同一份数据,然后子进程在运行过程中更改数据,发生写时拷贝,但是数据的地址却没有变化
如果C/C++打印出来的地址是物理内存的地址,这种现象是不可能存在的!我们在这里使用的地址不是物理地址,我们称之为虚拟地址,我们称这个程序地址空间为进程地址空间。
什么是进程地址空间:
每个进程都有一个地址空间,都认为自己在独占物理内存,由于系统中有大量的进程,那么就有大量的地址空间,OS为了管理这些地址空间,那么就会采用先描述再组织的方式来管理它们,在内核中,地址空间是一个数据结构,里面存放着具体的地址空间的变量
地址空间数据结构:
虽然里面只有start 和 end,但是每个进程都认为mm_struct代表整个内存,且所有的地址为0x000000..000~0xFFFFFFF...FFF,每个进程都认为地址空间的划分是按照4GB空间划分的,这样子每个进程都可以按照相同的方式去看待内存,都认为自己独占内存。在地址空间上进行区域划分是,对应的线性位置我们称为虚拟地址,在LInux中虚拟地址等价于线性地址
但是实际的数据和代码都是在物理内存保存的,那么操作系统是如何通过虚拟地址找到这些数据的呢?
通过页表!!这个页表是由操作系统给每个进程维护的一张表,这张表的左侧是虚拟地址,右侧是物理地址,它的核心工作是将虚拟地址转换为物理地址,它就是一张映射表,或是一张哈希表,进而再去访问代码和数据
tips:MMU是内存管理单元,在CPU内部集成,用来查页表
地址空间+页面就代表了操作系统在管理,在监督进程对内存的读写操作,比如我们定义的const变量,为什么就不能修改呢?因为在页表中,对于这个变量所对应的物理内存,该进程只有读权限,而没有写权限,当进程想要通过虚拟地址来改变物理内存中的该值时,将不被允许
当进程要向内存申请空间的时候,进程不一定立马读写这个空间,在操作系统角度,如果空间立马给进程,但是进程又没有立马读写,就意味着整个系统就会一部分空间,本来可以给别人立马使用的,现在却被你闲置着。所以说操作系统就会给进程设计一套基于缺页中断进行物理内存申请的机制,具体的操作是当进程向内存申请空间时,操作系统不会立马给进程开辟相应的物理内存空间,而是在地址空间中的相应字段增加数值范围,当进程要读写该段内存空间的时候,操作系统就会通过页表映射的方式为进程开辟相应的内存空间。因此操作系统做的内存申请动作对于进程来说是不透明的
当CPU调度到该进程执行程序的时候,CPU就会从main函数开始执行程序,但是有个问题,每个进程都有main函数,如果没有相同的main函数入口地址,那CPU就不知道从何处开始执行程序,所以说操作系统通过虚拟地址给cpu提供程序的入口地址,然后该虚拟地址再通过页表,映射到真实的物理内存中,找到真正的main函数入口地址,这样子就保证了CPU可以从相同的main函数地址去执行程序!!同样的,在虚拟地址空间中,每个部分都有自己的虚拟地址,然后映射到物理内存中,这样子在CPU看来,进程统一使用4GB空间,而且每个空间区域的相对位置是比较确定的!
为什么需要地址空间?
1. 通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全!
2.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软作上面的分离!
3.站在CPU和应用层的角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的
为什么需要页表这个东西呢?
加了这个页表映射的中间层,是非常利于操作系统对进程的内存操作进行管理的,可以很好的判断进程对于内存中数据的操作是否合理,是否安全,假设现在有操作系统中有三个进程,如果进程可以直接去访问物理内存,那么不同的进程就可以去访问其他进程的数据和代码,造成数据安全问题。
写时拷贝:
子进程在被创建出来的时候,父进程的数据和代码也会被子进程继承,此时当子进程修改数据时,发生写时拷贝,然后子进程和父进程拥有了各自的不同的数据,但是我们通过下面的代码知道,数据的地址没有发生改变,这是为什么呢?
因为子进程在被创建出来的时候, 会继承父进程的大部分进程数据,包括进程地址空间,也就是说此时在虚拟地址空间和物理内存中,子进程的数据地址和父进程的数据地址是一样的,当子进程的数据发生改变的时候,子进程通过虚拟地址空间中的虚拟地址通过找到数据的物理内存地址,然后在内存中开辟新的空间,将新值放置在空间中,最后通过页表将虚拟地址和新的物理内存地址见了映射关系,这样子就完成了写时拷贝
命令行参数和环境变量在虚拟地址空间中的位置:在栈的上面。