我们常见的计算机,笔记本、服务器等,大多遵守冯诺依曼体系结构。
目前,我们认识的计算机都是由一个个硬件组成的。
我们不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备),外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。所有设备都只能直接和内存打交道。
内存是体系结构的核心设备!
输入设备和输出设备的读写速度是以毫秒、秒为单位,cpu是以纳秒为单位,如果cpu直接和输入输出设备打交道,他俩的速度差别极大,cpu早都干完活了,你的东西还没送来,而内存的速度在这之间,就很好的起到了连接作用。
所以有了内存,cpu就不用再和外设打交道了!
⭐举个例子从我们登录QQ开始,给某人发送消息,到那个人接收到消息,整个过程是怎样的?
任何外设,在数据层面,基本优先和内存打交道;
cpu,在数据层面也是直接和内存打交道。
操作系统需要启动起来才有意义,就像你想发送消息,你起码得打开QQ,接收消息的人也得打开QQ。启动操作系统,就可以将软件数据与代码加载到内存中了。
总结一下:
冯诺依曼体系结构计算机的基本原理是什么?
答:存储程序+程序控制!计算机就是为了完成指定数据处理,而通过指令按指定流程完成指定功能,指令的合集就是一段程序。
OS是什么?
:一款专门针对软硬件资源进行管理工作的软件!
任何计算机系统都包含一个基本的程序集合,称为操作系统OS。
简单理解,操作系统包括:
1、内核(进程管理、内存管理、文件管理、驱动管理)。
2、其他程序(函数库、shell程序等)。
通过,与硬件交互,管理所有的软硬件资源的方式,为用户程序(应用程序)提供一个稳定的、高效的、安全的良好执行环境。
在整个计算机软硬件架构中,操作系统的定位是:一款纯种的“搞管理”的软件!
举例:
校长是管理者,学生是被管理者,老师是执行者。
校长并不和我们直接打交道,他只负责做决策,做决策要有依据,依据就是我们学生的属性信息数据,校长把决策告诉老师,让老师去执行,老师把决策告知学生。
那么站在校长的角度,怎么知道一个同学的信息呢?他肯定有这个学生的信息档案,档案上有这个学生的姓名、学号、电话、地址、事迹等等信息。
这个档案是不是就像一个结构体。
那有了一个学生的信息,全校几万学生,怎么让他们之间关联起来,是不是就像我们使用一个双向循环链表一样,将结构体结点关联起来。
所以怎么管理?
对目标的管理转换成对数据的管理,每个数据要先描述(使用一个结构体),
再组织起来每个数据对象(使用特性的数据结构)。
最后,对学生的管理工作,变成了对数据结构的增删查改!
那进程怎么管理?
同样:⭐先描述,再组织!
描述进程的结构体称为PCB(process control block)进程控制块!
为什么要有PCB?
因为管理需要先描述、再组织,而描述得用结构体,PCB就是一个结构体,来描述对象,描述进程相关属性信息!
现在我们已经知道:
操作系统对下要管理好软硬件资源,对上要为用户提供良好的运行环境,为程序员也得提供各种基本功能。
那么如何提供?
答:OS是不相信任何用户的,在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。但是OS提供的系统调用接口较为复杂,所以大佬对部分系统调用进行适度封装,从而形成库,有了库,就便于更上层用户或者开发者进行二次开发。
课本上说:加载到内存的程序,叫做进程。
内核的观点是:担当分配系统资源(cpu时间、内存)的实体。
如果管理进程:先描述、再组织!
OS层面,PCB是进程控制块,语言层面,PCB就是个结构体。
任何进程在形成之时,OS要为该进程创建PCB(进程控制块),进程信息就放在这里面,就是进程属性的集合。它会被装载到RAM(内存)里。
Linux下的PCB是:task_struct
。
为什么要有PCB前面已经说了。
ps axj | grep xxx
ps axj | head -1 && ps axj | grep xxx
曾经我们启动一个程序的过程,本质都是在系统上创建进程!
程序是什么?
:程序就是你的.exe可执行程序文件,这个文件要加载到内存上,操作系统要创建PCB,task_struct去管理这个文件。
进程=程序(代码、数据)+相关的数据结构(PCB)!
task_struct包含了进程内部的所有属性信息!
有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系,而与进程对应的内核(OS)创建的该进程的PCB强相关。也就是说,OS执行程序,不去找你代码本身,而是直接找PCB!
①标识符:描述该进程的唯一标识符,用来区分其他进程(pid)
#include
#include
getpid();//获得此进程pid
getppid();//获得父进程pid
在命令行上运行的进程,父进程一般是bash。
②状态:任务状态,退出代码,退出信号等
echo $?
输出最近执行命令的退出码。
return 0
退出码就是0。
③优先级:相对于其他进程的优先级
优先级就是资源使用先后的问题。
④程序计数器:程序中即将被执行的下一条指令的地址
函数栈帧,寄存器(eip)。
⑤内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
⑥I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
⑦记账信息:可能包括处理器时间总和使用的时钟数总和,时间限制,记账号等
OS的进程调度模块,较为均衡的调度每个进程,给每个进程分配cup资源。
所以为了均衡调度,应该记账它历史上所用过的资源和所用时间。
⑧上下文数据(重点):进程执行时处理器的寄存器中的数据
怎么理解上下文数据?
寄存器:当前正在运行进程的临时数据。
我们的计算机一般是单核cpu的,但是进程会有很多个,那单个cpu是怎么处理那么多进程呢?
答:进程的代码可能不是很短的时间就能执行完毕的,操作系统规定每个进程单次运行的时间片(比如一个进程每次最多执行10ms),在单cpu情况下,用户感受到多个进程同时进行,实质是cpu快速切换完成的!
在进程运行期间是有切换的,进程可能存在大量的临时数据,暂时在cpu的寄存器中保存。
cpu只有一套,也就是说寄存器只有一套。那么OS是怎么操作上下文数据来完成对这么多进程的管理的?
这里有两个概念:保存上下文、恢复上下文
虽然寄存器硬件只有一份,但是寄存器里的数据是你这个进程的。
进程先进先出(跟队列一样)
通过上下文,我们现在能感受到进程是被切换的!
在抢占式多任务处理中,进程被抢占时,哪些运行环境需要被保存下来?
fork()用于创建子进程!本质系统里多了一个进程,多一个进程就要增加一份与进程相关的内核数据结构(PCB)+进程的代码和数据,但是我们只是fork了一下,创建了子进程,那对应的代码和数据呢?
答:默认情况下会继承父进程的代码和数据,内核数据结构PCB也会以父进程为模板初始化子进程的PCB。
fork之后,子进程和父进程的代码是共享的!
父子代码只有一份,代码是不可修改的,所以代码不会影响独立性。
那数据呢?
默认情况下,数据也是共享的,但是如果有谁想修改数据,就会通过写时拷贝来完成,以保证数据的独立性。
进程是具有独立性的!
难道我们创建子进程就是为了跟父进程做一样的事?
一般当然要做不一样的事,这个就要通过fork的返回值来完成。
fork()的返回值,失效时返回<0;成功时,
给父进程返回子进程的pid
给子进程返回0
如何理解有两个返回值?
答:
如何理解返回值的设置?
因为一个父可以有很多子进程,而子进程只有一个父进程,所以父进程得靠子进程的pid来记住谁是谁,而子进程就唯一的父亲,那你还记啥。
所以根据返回值的不同,我们就能用if else if 等条件语句,让父子进程干不同的事了。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。
进程的状态信息在哪里?
:task_struct(PCB)。
进程状态存在的意义?
答:方便OS快速判断进程完成特定的功能、比如调度,本质事一种分类。
具体有什么状态?
R:(running)运行状态
运行状态不一定正在占用cpu!
S(sleeping)浅度睡眠,可中断睡眠
D(disk sleeping)深度睡眠,不可中断
当我们完成某种任务的时候,任务条件不具备需要进程进行某种等待。
所以不要认为进程只会等待cpu资源,可能也需要等待外设启动。
所谓的进程,在运行时,有可能因为运行需要,可能会在不同的队列里!
在不同的队列里,所处状态就不一样。
⭐我们把从运行状态的task_struct(run_queue),放到等待队列中,就叫做挂起等待(阻塞)。从等待队列放到运行队列,被cpu调度,就叫做唤醒进程。本质就是队列之间的切换。
进程如果处于D状态,是不可被杀掉的包括OS。
T(stopped)暂停
t(tracing stop)追踪(调试打断点)
X(dead)死亡
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
回收进程资源=进程相关内核数据结构+你的代码数据。
Z(zombie)僵尸状态
如果死人没有得到回收,就是僵尸状态。
为什么要有僵尸状态?
答:为了辨别退出死亡的原因,这里就包括进程退出的信息(数据),写在PCB里的。其实就跟犯罪现场,警察要拉警戒线,保持现场进行信息收集,最后再处理现场。
所以进程要先进入僵尸状态,然后再到死亡状态。
while(1)
{
cout<<"hello"<<endl;
}
为什么大部分处于S+(前台)状态?
因为要进行打印,需要外设IO,而外设相比cpu太慢了,进程一直在等待就绪。
以下为信号操作:
kill -l
kill -19 xxxx暂停
kill -18 xxxx继续(S此时变到前台了)
kill -9在后台干掉某进程
./a.out
前台进程
./a.out &
后台进程
僵尸进程:
如果子进程先于父进程退出,但是父进程还处于运行状态,就是没有对子进程的资源进行检测和回收那么子进程就处于僵尸状态了。
僵尸进程的危害:
如果父进程一直不读取,子进程就一直处于僵尸状态!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护。
那一个父进程创建了很多子进程,就是不回收,就造成内存资源的浪费!因为数据结构对象本身就要占用内存!
这不就导致内存泄漏了嘛!
孤儿进程:
如果子进程还在运行,但是父进程先走一步了。此时的子进程就变成孤儿进程了,它现在由pid=1的1号(Init)进程领养了,那它就由此进程对其回收呗。所以孤儿进程不会内存泄漏。孤儿进程运行在系统后台。
守护进程(精灵进程):运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。
cpu资源分配的先后顺序,就是指进程的优先级。
为什么要有优先级?
答:资源太少,本质是分配资源的方式
ps -l
/ps -al
看一下进程
UID:代表执行者身份(相当于身份证号)。
PID:进程代号 PPID(父进程)。
PRI:代表这个进程可被执行的优先级,值越小越先被执行。
NI:代表此进程的nice值(优先级修正数据)。
NI的作用是可以看出来调整的幅度,NI的取值为(-20~19)40个级别。
新的优先级PRI等于老的优先级+nice值(调整NI来调整PRI)。
以下为修改优先级
top
r
PID
q
如果重新设置会又从一开始80开始不会叠加!
nice为何范围设置这么小呢?
优先级再怎么设置,也只是一种相对的优先级,不能出现绝对优先级,否则会出现严重的进程饥饿问题(某进程长时间得不到资源)。
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
进程之间互不干扰,具有独立性!
并发:多个进程在一个cpu下,采用进程切换的方式,在一段时间内,让多个进程都得到推进!
并行:多个进程在多个cpu下分别同时进行运行!
调度器:较为均衡的让每个进程享受到cpu资源。
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,在系统当中通常具有全局特性
命令、程序、工具,本质都是一个可执行文件。
比如说:
./test
来执行一个可执行程序,./
的作用就是帮系统确认对应的程序在哪。
但是为何有的系统命令不用加./
呢?
答:因为环境变量!
PATH : 指定命令的搜索路径。
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)。
SHELL : 当前Shell,它的值通常是/bin/bash。
环境变量本质是OS在内存/磁盘文件中开辟的空间,用来保存相关数据。
系统上还存在一种变量,是与本次登录有关的变量,只在本次登录有效:本地变量。
myval=1234现在就是个本地变量
set | grep myval
myval=1234找到了
env | grep myval
找不到
因为它只是本地变量,不是环境变量
export myval将本地变量保存到环境变量(重启就又没了)
env | grep myval
myval=1234出现了
先来看一下命令行参数
#include
#include
using namespace std;
int main(int argc,char*argv[])
{
for(int i=0;i<argc;i++)
{
printf("argv[%d]->%s\n",i,argv[i]);
}
return 0;
}
为什么要有命令行参数?
答:指令有很多选项,用来完成同一个命令(程序)的不同子功能,选项底层就是使用命令行参数。
比如你:ls -a -l
后面的就是命令行参数
1、命令行第三个参数
#include
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
#include
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
3、常用方法
#include
#include
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
环境变量具有全局属性!
命令行上启动的进程,其父进程一般都是bash,相当于bash进行fork()。
环境变量具有全局属性的本质是环境变量可以被子进程继承下去!
举例:
myenv="hello world"//本地变量
int main()
{
printf("%s\n",getenv("myenv"));
}
运行./test
发现啥也没打印
但是如果
export myenv="hello world"
再运行上面的程序,发现结果被打印出来了!
说明环境变量可以被子进程继承下去。
⭐感谢阅读,我们下期再见
如有错 欢迎提出一起交流