目录
- 1.冯诺依曼体系结构
- 硬件[2~5]
- 2.为什么要有内存?
- 3.为什么不用CPU中的寄存器做存储单元?
- 4.为什么我们的程序必须先被加载到内存中?
- 5.在硬件层面数据流是如何流向的?
- 软件[6~10]
- 6.操作系统Operator System
- 7.操作系统的作用
- 8.操作系统是如何“管理”软硬件的?
- 9.操作系统为什么要对软硬件资源进行管理?
- 10.系统调用
- 进程[11~20]
- 11.当有多个进程从磁盘加载到内存中时,OS如何管理这些进程?
- 12.查看进程常用命令
- 13.进程属性
- 0.UID
- 1.PID
- 2.PPID
- 3.STAT
- 4.PRI
- 5.NI
- 14.如何创建子进程
- 15.fork函数的返回值
- 16.fork创建子进程的原理
- 1.fork做了什么
- 2.fork如何看待代码和数据
- 3.fork如何理解两个返回值
- 17.进程状态
- 18.阻塞与挂起
- 阻塞的原理
- 19.Linux下的进程状态
- Z状态:僵尸状态
- 20.孤儿进程
- 为什么要存在孤儿进程?
- 学习的指令
- 1.杀进程
- 2.暂停进程
- 3.查看进程退出码
1.冯诺依曼体系结构
计算器、笔记本、服务器大都遵循冯诺依曼体系结构。
结构如下图所示:
硬件[2~5]
2.为什么要有内存?
- 外设:速度相对慢,价格相对较低(输入输出设备都属于外设)
- 内存:速度相对快,价格相对较高,数据掉电易失
- CPU:速度最快,价格高
1.那么是否可以不通过存储器,直接使用输入设备将数据传给CPU,再由CPU处理后传给输出设备呢?
——可以。
2.那么为什么不这么做呢?
——因为外设的速度相对于CPU太慢了,整体的效率就会以外设为主,所以引入了内存来缓解效率下降的问题。
3.内存是如何缓解效率下降问题的?
——运行速度:外设<内存<CPU;而内存可以临时存储数据,当CPU处理一项任务A的时候,可以预先将另一项任务B的数据存储在内存中,等待CPU处理完任务A以后,直接从内存读取任务B的数据来执行,即预加载。所以整体的效率就以内存为主,从而提高了运行效率。
总结:
内存是为了适配外设与CPU的速度。
在数据层面:CPU一般不与外设直接沟通,而是直接与内存沟通。
3.为什么不用CPU中的寄存器做存储单元?
虽然速度快,但是价格过高。
4.为什么我们的程序必须先被加载到内存中?
可执行程序是一个文件,存储在磁盘上,而磁盘属于外设。
冯诺依曼体系结构规定:CPU只直接与内存沟通,所以我们的程序需要预先加载到内存中,再传递给CPU计算。
5.在硬件层面数据流是如何流向的?
都遵循冯诺依曼体系结构!
1.单机:
2.跨主机:(不考虑网络)
总结:
在数据层面,数据要传递给CPU,必须要先从外设到达内存,然后再给CPU。
软件[6~10]
6.操作系统Operator System
操作系统是计算机基本的程序集合,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库, shell程序…)
操作系统是软件,而软件需要预先加载到内存中再运行,那么操作系统是什么时候加载到内存中的呢?
——开机加载的时候。
7.操作系统的作用
操作系统是一款进行软硬件管理的软件。
它的作用是:
- 与硬件交互,管理所有的软硬件资源
- 为用户程序提供一个良好的执行环境
8.操作系统是如何“管理”软硬件的?
——把被管理的对象转换成对某种数据结构的增删查改。
管理的本质:先描述,再组织
- 描述:用struct结构体描述硬件信息
- 组织:用链表或其他高效的数据结构链接整合描述的信息
9.操作系统为什么要对软硬件资源进行管理?
对下:管理好软硬件资源
对上:为用户提供安全、稳定、高效的执行环境
10.系统调用
系统调用其实就是OS提供的C语言函数。
用户可以通过调用这些函数来使用操作系统的功能。
我们在使用printf打印数据时,底层其实就调用了系统调用来输出到显示器这个硬件。
库函数其实就是对系统调用的封装(不是所有的库函数都会调用系统调用)。
操作系统提供系统调用接口是为了保证自身的封装性,所以通过接口的形式给用户使用,而不是直接将操作系统暴露给用户。
而系统调用的使用成本较高,所以就有人基于系统调用为其他用户完成了:
图形化界面、shell和工具集、开发工具
又有人基于上述完成的功能来实现了上层应用(应用层)
进程[11~20]
任何启动并运行程序的行为,都由操作系统帮助我们把程序转换成进程来完成特定的任务。
11.当有多个进程从磁盘加载到内存中时,OS如何管理这些进程?
如果不进行管理,那么无法区分哪部分内存是属于哪个进程的。
那么操作系统如何管理进程呢?
——先描述,再组织:
- 描述:当进程加载到内存时,操作系统会在内核中创建一个数据结构对象(pcb / task_struct是Linux下的pcb),pcb提取了进程的大部分属性,其中有一个指针指向自己进程所属的内存空间。
- 组织:每个pcb中又存有一个指向下个pcb的指针,将所有pcb链接起来。(Linux中用双链表链接)
总结:
对进程的管理,通过PCB就转化成了对链表的增删查改。
进程 = 内核中进程的属性数据结构 + 内存中加载的代码和数据
二者相加才是进程,只有内存中的代码和数据不能算是一个完整的进程。
注意区分:
1.文件 = 内容 + 属性
2.进程 = 内核中进程的属性数据结构 + 内存中加载的代码和数据
这里文件中的属性和内核数据结构中进程的属性不是一个属性,没有关系
12.查看进程常用命令
ps axj:查看所有进程
——————————
ps axj | head -1:只查看ps axj输出结果的第一行,也就是查看属性列表
——————————
ps axj | grep 进程名/pid :查看指定的进程
——————————
ps axj | head -1 && ps axj | grep 进程名/pid:查看进程属性名+指定的进程(会多打印一行grep进程,因为grep自己也是进程也会打印出来)
——————————
ps axj | head -1 && ps axj | grep 进程名 | grep -v grep:查看进程属性名+指定的进程(不显示grep进程)
查看进程还有另一个方法:
在根目录下有一个/proc
目录,就是保存进程相关属性的目录,proc是内存级的文件目录,只有当操作系统启动时它才会存在。
当相应的进程终止时,proc中相应进程的目录也会消失。
可以直接cd进入proc目录查看进程属性。
13.进程属性
0.UID
用户标识符,操作系统通过UID来标识区分用户。
1.PID
PID:当前进程的编号
getpid:获取进程的pid,谁运行的时候调用这个函数就获取谁的pid
——头文件:<sys/types.h>、<unistd.h>
——返回值:pid_t getpid(void);其实就是有符号整数
每次调用同一个进程都会给新的pid值,不是一样的。
2.PPID
PPID:父进程的编号
getppid:获取当前进程的父进程的pid
——头文件:<sys/types.h>、<unistd.h>
——返回值:pid_t getpid(void);
bash(命令行解释器)也是一个进程,有独立的pid。
命令行启动的所有程序都会变成进程,该进程的父进程都是bash。
也就是执行我们自己的程序都是通过bash创建子进程来执行的。
我们所使用的指令,比如ls、touch等也是bash的子进程。
3.STAT
当前进程所处的状态。(具体细节下面部分解析)
linux进程状态有:
- R (running)
- S (sleeping)
- D (disk sleep)
- T (stopped)
- t (tracing stop)
- X (dead)
- Z (zombie)
4.PRI
进程的优先级。
5.NI
进程优先级的修正数据。
14.如何创建子进程
fork:创建子进程
函数:pid_t fork(void);
头文件:#include <unistd.h>
创建子进程,直接调用fork函数即可:
fork();
或者pid_t ret = fork();
创建并接收返回值。
- fork之后,执行流会变成两个
- fork之后,谁先运行由调度器决定
- fork之后吗,fork后面的代码共享,通常以if和else if来进行执行流分流。
15.fork函数的返回值
返回值:pid_t,也是整形。
给父进程返回子进程pid,给子进程返回0,创建子进程失败返回-1。
可以在调用时创建pid_t类型的变量获取pid_t ret = fork();
。
注意这个返回值,在父子进程中的值不一样,“看似”有两个返回值:
- 在父子进程种两个ret的值不一样
- 在父子进程中两个ret的地址一样
如下例所示:
可以通过fork的返回值来进行执行流分流父子进程:
pid_t ret = fork();
if(ret == 0)
{
//子进程
}
else if(ret > 0)
{
//父进程
}
16.fork创建子进程的原理
1.fork做了什么
进程 = 内核数据结构 + 进程的代码和数据
fork相当于在内核中创建新的子进程数据结构pcb,其内容与父进程数据结构基本一致,pid和ppid不同。
其进程的代码和数据不会拷贝一份新的,父子进程共享。
父子进程pcb与子进程pcb都指向相同的代码和数据。
2.fork如何看待代码和数据
进程运行时,具有独立性:一个进程终止与否不影响其他进程。
父子进程也一样具有独立性。
但是父子进程共享代码和数据,独立性要如何保证?
1.代码:代码是只读的,不会变化。
2.数据:当一个执行流尝试修改数据的时候,OS会给当前的进程触发一种机制:写时拷贝。所以写的进程写入位置发生变化,不会影响读的进程读取原始数据。
写时拷贝:
字面理解,当想要写入修改数据时,将原始数据拷贝一份到另一个地方,修改拷贝的数据而不是原始数据。写时拷贝时原始数据与拷贝的数据地址相同,是因为存在虚拟地址空间(后续解释,c/c++中的所有地址都不是物理地址,是虚拟地址)。
3.fork如何理解两个返回值
fork其实就是创建了一个子进程pcb并进行了初始化。
fork的本质就是OS提供的一个函数;
函数在return的时候,其主体功能就已经完成了;
fork作为一个函数,当返回返回值的时候,其创建子进程并初始化的工作已经完成了,只差return没有进行;
return也是语句,因为子进程已经创建完成了,所以这条return语句会被父子进程分别执行,一共执行两次,就会看似有两个返回值。
当使用变量接收返回值时,会发生写时拷贝pid_t ret = fork;
,实际上存在两个ret数据,存在两个物理空间中,但是其虚拟地址空间相同。
17.进程状态
进程状态有:
- R (running)
- S (sleeping)
- D (disk sleep)
- T (stopped)
- t (tracing stop)
- X (dead)
- Z (zombie)
进程的运行模式:
一个进程并不是一直在CPU中运行的,而是运行一段时间然后再换另一个进程到CPU中运行,让所有的进程代码在同一时间段内都有所推进。而我们作为用户作为人是感受不到进程的切换的,因为切换的速度很快,所以我们看到的就好像是多个进程在同一个时间段内同时运行。
18.阻塞与挂起
阻塞:
进程因为等待某种条件就绪,而导致的一种不推进的状态。(其实就是进程卡住了)
阻塞一定是在等待某种资源——比如:
进程在等待CPU调度,就是在等待占用CPU资源;
下载时卡住了,就是在进程在等待占用网络资源;挂起:
当内存中的空间很紧张的时候,OS会将内存中不被调度的、闲置的一些进程的数据和代码交换到磁盘当中,这部分代码和数据就相当于被释放了,当该进程需要再次被调度的时候,重新将这部分代码和数据交换到内存中,给CPU来运行。将代码和数据暂时由OS交换到磁盘的这段时间,该进程就处于挂起状态。
阻塞:进程在等待某种资源就绪的过程。
这里的资源指的是:磁盘、网卡、显卡等各种硬件外设。
那么操作系统是如何管理硬件外设的呢?
——先描述,再组织:通过结构体来描述硬件、然后用链表的形式链接,将对硬件的管理转换成对链表的增删查改。(比如当电脑插入一个键盘的时候,OS就要创建一个键盘对应的数据结构来对其进行管理)
同时OS中也存在着大量的进程,如何管理进程?
——先描述,再组织:每个进程都是tesk_struct定义的一个对象。
阻塞的原理
当CPU调用某个进程时,拿到它对应的tesk_struct数据结构,发现其中有某项资源没有就绪,就让它进入阻塞状态,将其从CPU特定的队列中移动到所需要等待的资源处排队,就是将该进程对应的的tesk_struct链入到所需的外设资源的结构体中等待资源。(在硬件对应的struct中有一个结构体指针可以接收tesk_struct)
而当进程在硬件的队列中阻塞时,从硬件获得数据后就会链回到CPU的特定队列来运行。
所以,PCB时可以被维护在不同的队列当中的!
19.Linux下的进程状态
tesk_struct是一个结构体,其中包含各种属性,其中就有进程状态。
struct tesk_struct
{
//...
int status;//进程状态
}
linux下的进程状态有:
- R (running)
- S (sleeping)
- D (disk sleep)
- T (stopped)
- t (tracing stop)
- X (dead)
- Z (zombie)
一个进程被新建,默认是R状态;就绪也相当于是R状态;运行状态也是R状态;阻塞为S状态;终止为X或Z状态。(如下图)
-
R状态:运行
进程处于R状态,不一定在运行,比如有很多进程处于R状态,其中只有几个在CPU上运行。
在进程运行时要维护一个运行队列,处于R状态的进程结构体都会被链接在上面,CPU就会在其中挑选指定的进程来运行。
也就是说在CPU上运行的进程+在运行队列中排队的进程都处于R状态。
这个运行队列不在CPU中,由OS自己维护。
(队列中不是进程的数据和代码,是进程结构体对象tesk_struct)
补充:
但是当循环打印的时候,查看进程的状态会发现处于S(休眠)状态,而不是R状态,这是因为当往显示器打印字符的时候,有可能进程需要等待显示器的资源,所以CPU会将进程放到显示器的结构体中去等待资源,此时处于阻塞状态(S状态也是一种阻塞状态),当获得显示器资源的时候回到CPU变成R状态,但是因为大部分时间都在等待,运行占小部分时间,二者切换速度过快,所以我们查看进程状态的时候很难看到处于R状态。 -
S状态:可中断休眠
休眠状态本质就是一种阻塞状态。
可中断指的是:处于S状态的进程可以被直接中断,或者是在等待获取资源,取得资源后会变成R状态运行。 -
D状态:不可中断休眠
当CPU调用某个存储数据时,进程等待磁盘存储,自身处于S状态阻塞,CPU去调用其他程序,在此时,如果内存的资源极其紧张,进程再多OS就要挂了。OS就可能会将这个处于阻塞状态的进程结束(杀进程),而此时,磁盘存储数据如果失败了,会返回进程,但是此时进程已经被OS杀死不在内存中了。
———为了防止出现这种问题,就有了D状态,D状态的进程不可以被中断。(甚至OS也不能将其中断)
当进程进入这种状态,只有等待其自己醒来,或者关机重启(有些情况甚至无法关机),没有别的办法可以将其结束。如果发现出现D状态,那么计算机也基本快要宕机了。如果直接拔电源,那么之前进程在往磁盘写入的数据就会丢失。 -
T状态:暂停
也是一种阻塞状态,暂停≠终止。
暂停进程的方法:
kill -19 pid :暂停信号——暂停编号为pid的进程;
kill -18 pid :继续信号——让被暂停的信号继续运行;
在进程运行时,输入kill -19 pid命令会将进程暂停,此时查看进程状态变为T状态。
注意:
在进入T状态之前的进程状态的符号后面都会有一个“+”,但是在进入一次T状态然后再解除后会发现进程状态后面没有“+”了,并且按下ctrl+c无法终止进程。
因为带“+”的进程,表示该进程在前台运行;不带“+”的进程,表示该进程在后台运行;
此时你可以处理其他的shell指令,比如ls,cd等,但是后台进程会一直自动执行。
此时输入kill -9 pid
或者killall 进程名称
可以结束该进程。
所以,前台进程可以用ctrl+c或kill -9 pid结束,后台进程只能用kill -9 pid结束。 -
t状态:追踪暂停
我们在写程序的时候,加入断点的本质就是让进程暂停!
此时该进程暂停等待跟踪他的进程进行操作,比如写程序加断点。 -
X状态:终止
X时一个瞬时状态,很难查看到,在进程终止的瞬间为X状态。
Z状态:僵尸状态
(重点状态,单独记忆)
1.知识补充:
在写c/c++时,main函数结束会有一个返回值return 0;
return 0是进程的退出码, 这个退出码可以检查程序退出的结果是否正确。
查看进程退出码的方法:
进程运行结束后,命令行输入:echo $?
比如,在ls命令后如果输入例如-asdaslf这种不存在的选项,则用echo $?会显示退出码为2.
2.为什么需要僵尸状态?
如果一个进程退出了,马上进入X状态退出,父进程是否有机会获取退出码呢?
——没有。
那么在linux中当进程退出时,进程一般不会立马退出,而是会维持一个Z状态,便于后续父进程(OS)获取子进程的退出结果。
3.观察僵尸进程的方法
同时运行一个父进程和子进程,此时查看父子进程都为S+状态;
kill子进程,此时查看父进程为S+状态,子进程为Z+状态,并且在子进程的属性列表末尾还多了一个,此时子进程就是僵尸状态,是一个僵尸进程,因为它还没有被回收。
4.僵尸进程会导致内存泄漏问题
处于Z状态的进程称之为僵尸进程,并且僵尸进程的数据没有被完全释放,会占一部分资源,如果我们不回收,OS就要消耗一部分资源来维护僵尸进程的数据结构pcb,这部分内存就会一直被占用,就会造成内存的泄漏。
所以僵尸进程必须要回收。
5.为什么我们退出父进程,查看进程属性时发现父进程不会进入僵尸状态而是会直接退出?
因为父进程是bash的子进程,bash会自动回收父进程的退出结果。
20.孤儿进程
如果父进程退出,而它的子进程没有退出:
此时子进程会被1号进程领养,1号作为新的父进程,1号进程其实就是OS。
对于这种被领养的进程,我们称之为孤儿进程。
孤儿进程被OS领养后,会自动变为后台进程(没有+)。
为什么要存在孤儿进程?
因为如果没有孤儿进程这个概念,当后续子进程退出的时候,没有父进程接收其退出结果,子进程就会一直保持僵尸状态,造成内存泄漏。
学习的指令
1.杀进程
kill -9 pid:结束编号为pid的进程
killall [进程名称] :结束指定名称的进程
2.暂停进程
kill -19 pid :暂停信号——暂停编号为pid的进程
kill -18 pid :继续信号——让被暂停的信号继续运行
3.查看进程退出码
查看进程退出码的方法:
进程运行结束后,命令行输入:echo $?