概览
冯诺依曼体系结构
现代计算机的硬件体系结构 ——计算机应该包含五大硬件单元
- 输入设备 —— 采集数据的,比如典型的键盘,网卡接收网络中的数据
- 输出设备 —— 进行数据输出,比如典型显示器,网卡向网络中发送数据
- 存储器 —— 进行中间数据缓冲(内存),硬盘为外存
- 运算器:进行数据运算 运算器+控制器=CPU中央处理器
- 控制器:进行设备控制
所有的设备都是围绕存储器工作的
- cpu不会直接从输入设备获取数据进行处理,而是先把数据放到存储器中,cpu从存储器中获取数据处理
- cpu不会直接将数据交给输出设备进行输出,而是先把数据报道存储器中,控制输出设备从存储器中获取数据输出
存储器实际上就是内存,为什么不是硬盘?
硬盘的数据吞吐量太低了:200MB/s
内存的数据吞吐量:是机械硬盘的数十倍
内存速度这么快,为什么内存只用于缓冲,不使用内存存储数据,而是使用硬盘存储?
因为硬盘与内存的存储介质不同,内存是易失性介质,数据掉电就会丢失,而硬盘掉电后数据不会丢失。
操作系统
—— 一个搞管理的软件(安装在计算机上的一个程序),管理计算机上的软硬件资源
操作系统:
内核(负责系统的核心功能 —— 软件管理,硬件管理,内存管理,文件管理…)+外部应用
用户不能直接访问系统内核(危险太高),因此为了控制风险,系统让干什么才能干什么
操作系统会向外提供访问内核的接口 —— 每个接口完成的功能都是固定,系统调用接口
- 操作系统如何进行软硬件的管理 —— 先描述,再组织
系统调用接口:操作系统向用户提供的访问内核的接口
库函数与系统调用接口的关系:库函数封装了系统调用接口(上下级的封装调用关系)
进程概念
进程 —— 运行中的程序
-
一个程序运行起来,有数据以及指令需要被cpu执行处理,根据冯诺依曼体系结构cpu不会直接去硬盘上找到程序文件进行执行处理需要先将程序数据信息,加载到内存中,然后cpu从内存中获取数据以及指令进行执行处理(程序运行会被加载到内存中)
-
竞争性 —— 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
-
独立性 —— 多进程运行,需要独享各种资源,多进程运行期间互不干扰
-
并行 —— 多核cpu上,多个进程同时占据cpu进行数据处理(例:十个人过马路)
-
并发 —— cpu资源不够的情况下,采用cpu分时机制,任务轮询处理。也就是多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程都得以推进。(例:十个人过独木桥)
CPU的分时机制
- 实现系统同时运行多个程序的技术。cpu只负责执行指令,处理数据(处理哪个程序其实cpu并不关心),因此操作系统的进程管理就体现出来,对程序的运行调度进行管理。
- cpu进行程序运行处理的时候,并不会一次性将一个程序运行完毕才会运行下一个,而是每个程序都只运行一段很短的时间(给一个程序分配的一个时间片),时间片运行完毕则由操作进行调度,让另一个程序的代码数据在cpu上进行处理。
- cpu分时机制实现cpu轮询处理每一个运行中的程序,而程序运行调度由操作系统进行管理,将cpu处理程序运行的过程划分为时间片,每个程序在cpu上只运行一段很短的时间,时间片完毕后切换下一个进程
管理思路:
- 操作系统将每一个程序的运行信息保存下来,进行调度管理的时候才能知道这个程序上一次运行到了哪里。
- 程序的指令,数据在内存中是死的,谈不上进程
- 操作系统通过对一个程序运行的描述,让一个程序运行起来,让一个程序动起来,这才能称之为运行中的程序。
- 对于操作系统来说,进程就是PCB,是一个程序运行的动态描述,通过PCB,才能实现程序的运行调度管理。
- 对操作系统来说,管理程序的运行,就将程序的运行描述起来,然后组织起来进行管理,描述的运行信息对于操作系统来说就是运行中的程序就是进程。
描述进程 —— PCB
task_struct——PCB的一种
- linux下的PCB就是一个struct task_struct结构体
- task_struct是Linux内核的一种数据结构,他会被装载到RAM(内存)里并且包含着进程的信息。
task_struct内容分类:
- 标识符 —— 能够让操作系统识别唯一的运行中的程序。
- 状态 —— 任务状态,退出代码,退出信号等。
- 优先级 —— 相对于其他进程的优先级
- 程序计数器 —— 也属于上下文数据,保存的就是指令位置,切换回来知道从哪里继续运行
- 内存指针—— 能够让操作系统调度程序运行的时候,知道进程对应的指令以及数据在内存中位置
- 上下文数据 —— 程序运行过的指令和数据,程序运行中的指令和数据,程序即将执行的指令和数据,操作调度切换进程运行的时候能够让cpu这个进程接着切换之前继续运行,继续处理之前没有处理完的数据(切换时保存正在执行的指令以及数据信息)
- IO状态信息 —— 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息 —— 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息。
调度
操作系统,将程序的信息,放到cpu寄存器中,让cpu知道执行哪个程序的什么指令,处理什么数据
如果有多个程序运行,cpu分时机制实现程序的切换运行 —— 调度。
- cpu调度 —— 分配cpu资源给进程,就是运行这个进程,管理cpu资源(cpu调度就是分配cpu资源,切换运行程序)
- 资源调度站 —— 分配资源,管理资源
进程状态
—— 每个进程pcb中都会描述一个运行的状态信息,通过状态信息,告诉操作系统这个进程现在应该干什么
例如:在桌子上摆一个正在忙,告诉别人不要来打扰我,别人看到你的状态就不会来打扰你了
时间片—— 操作系统给每一个程序分配的cpu处理事件,时间片运行完了就切换另一个程序,实际上就是一个cpu运行处理时间段,一个进程拿到时间片,表示的就是分配到了cpu的处理时间段
一般的状态:
- 就绪 —— 拿到时间片就能运行
- 运行 —— 正在被cpu处理运行
- 阻塞 —— 暂时不满足某些条件则不运行
linux中更加细分的状态:
- 运行状态® —— 包含就绪以及运行,也就说正在运行的,以及拿到时间片就能运行的都称之为运行状态,操作系统遇到pcb这个状态就会调度运行(并不意味着一定在运行中,它表明进程要么在运行中要么在运行队列里)
- 可中断休眠状态(S) —— 可以被打断的休眠,通常指自然满足唤醒条件后唤醒,也可以被中间打断,中断休眠之后进入运行状态。(意味着进程在等待着事件完成)
- 休眠 —— 暂时不需要cpu调度运行,让出cpu资源,休眠也有唤醒条件,操作系统调度程序运行的时候,会查看状态,如果是休眠,就会看唤醒条件是否满足,如果满足,则置为运行状态,进行处理,如果不满足则切换下一个进程。
- 不可中断休眠状态(D) —— 只能通过满足条件自然醒,进入运行状态,不会被一些中断打断休眠状态(在这个状态的进程通常会等待IO的结束)
- 停止(T) —— 停止与休眠不一样,休眠操作系统会去查看唤醒条件是否满足,而停止是只能手动唤醒(可以通过发送SIGSTOP信号给进程来停止进程,这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行)
- 僵尸状态(Z) —— 描述的是一个进程退出了,但是进程资源没有完全被释放,等待处理的一种状态。
- 死亡状态(X) —— 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
特殊进程:
- 僵尸进程 —— 处于僵尸状态的进程,退出了但是资源没有完全被释放的进程
危害:资源泄露(可能会导致正常进程起不来)
产生:一个进程先于父进程退出,父进程没有关注子进程退出状态(没有读取到子进程退出的返回代码时),导致子进程资源无法完全被释放,进入僵尸状态
解决︰进程等待 —— 一直关注子进程,退出了就能直接发现 - 孤儿进程 —— 父进程先于子进程退出,子进程就会成为孤儿进程;
特性:让出终端,进入系统后台运行,并且父进程成为1号进程(孤儿进程退出不会成为僵尸进程,因为它的退出会被1号进程直接处理)
1号进程,在centos7之前,叫init进程,在centos7之后叫做systemd
系统中父进程是1号进程的进程,通常都会以d结尾,表示自己是个在后台默默运行的服务 - 守护进程 —— 特殊的孤儿进程,不但运行在后台,父进程成为1号进程,并且还与登录终端以及会话脱离关系(为了让一个进程稳定不受影响的运行在后台)
阻塞想要描述的不是程序不运行,而是描述不被运行。
挂起是一种操作,指的是让进程陷入阻塞状态
挂起不是让进程从内存放到外存
进程优先级
基本概念:
- cpu资源分配的先后顺序,就是指进程的优先权
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程:
在linux或者unix系统中,用 ps -l 命令则会输出以下内容:
重要信息:
- UID —— 代表执行者的身份
- PID —— 代表这个进程的代号
- PPID —— 代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI —— 代表这个进程可被执行的优先级,其值越小越早被执行
- NI —— 代表这个进程的nice值
解析PRI和NI
- PRI即进程的优先级,通俗点说就是程序被cpu执行的先后顺序,此值越小进程的优先级别越高
- NI即为nice值,其表示进程可被执行的优先级的修正数值,PRI值越小越快被执行,加入nice值后,将会使PRI变为:PRI(new) = PRI(old)+nice ,这样当nice值为负值的时候,那么该程序的优先级值会将变小,即其优先级会变高,则其越快被执行。nice的取值范围是 -20 至 19,一共40个级别。
注意:调整进程优先级,就是调整进程nice值,nice值不是进程的优先级,但是进程nice值会影响进程的优先级变化,可以理解nice值为进程优先级的修正数据。
查看进程优先级的命令
—— top
用top命令更改已存在进程的nice:
进入top后,按r之后,输入要更改进程的PID,然后更改nice值
环境变量
环境变量:终端shell中进行系统运行环境配置的变量(通俗来说就是一个变量,用于存储系统运行的环境参数)
常见环境变量:
- PATH —— 指定命令的搜索路径
- HOME —— 指定用户的主工作目录(即用户登录到Linux系统中时,默认的目录)
- SHELL —— 当前Shell,他的值通常是 /bin/bash
作用:
1.通过修改环境变量的值,可以使系统环境配置更加灵活(不像修改配置文件后还得加载配置问题),
2.子进程的继承性(子进程默认会拥有与父进程相同的环境变量——可以通过环境变量进行进程之间的数据传递)
操作指令∶
- env-查看所有环境变量
查看单个环境变量示例:env | grep 变量名
- echo–直接打印某个变量的内容;
示例:echo $PATH
- set-查看所有变量,不只环境变量,还有普通变量;
示例:set | grep 变量名
- export-声明定义转换环境变量
示例:export 变量名
- unset-删除变量,包括环境变量(用于删除变量,不是用于删除变量中的某个数据)
指令应用:
-
echo $PATH($ 表示后边的字符串是一个变量名称) ;
测试HOME:-
root用户:
-
普通用户:
-
-
export PATH=$PATH:. —— 将当前目录加入到PATH环境变量(如果要改变环境变量的数据,只能重新赋值)
测试PATH:- 没有将当前目录加入PATH环境变量时:
创建hello.c 文件
- 将当前目录加入PATH环境变量时:
- 没有将当前目录加入PATH环境变量时:
注意事项:
- 典型唤醒变量 —— PATH(存储程序运行默认的搜索路径),运行一个程序的时候,如果没有指定程序路径,只有程序名称,则shell会去PATH环境变量保存的路径中去找这个程序,如果没有找到,就报错没有这个命令,如果将我们自己的程序所在路径加入到PATH环境变量中,则这个程序就可以直接运行了。
- shell中的普通变量可以起到环境配置的作用,但是无法进行数据传递
- 环境变量只是临时存储,重新打开终端就会没有,如果想要永久配置,需要把信息加入到配置文件中,环境变量是每次打开终端时从配置文件中加载的。
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
通过代码获取环境变量:
- main函数的第三个参数 —— int main(int argc,char *argv[], char *env[])
#include<stdio.h>
int main(int argc,char *argv[],char *env[])
{
int i=0;
for(;env[i];i++)
{
printf("%s\n",env[i]);
}
return 0;
}
- 使用extern char **environ —— 库中的全局变量,每个节点指向一个环境变量,但是使用时需要声明,告诉编译器有这个变量
#include<stdio.h>
int main(int argc,char *argv[],char *env[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
- char *getenv (char *key) —— 通过环境变量名称获取环境变量内容
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[],char *env[])
{
char *s = getenv("PATH");
if(s == NULL){
printf("have no env var:PATH\n");
}else{
printf("PATH:[%s]\n",s);
}
return 0;
}
- 代码中设置环境变量:
- setenv(char *key,char *val,int override)
作用:增加或者修改环境变量。
函数说明:setenv()用来改变或增加环境变量的内容。参数name为环境变量名称字符串。参数 value则为变量内容,参数overwrite用来决定是否要改变已存在的环境变量。如果没有此环境变量则无论overwrite为何值均添加此环境变量。若环境变量存在,当overwrite不为0时,原内容会被改为参数value所指的变量内容;当overwrite为0时,则参数value会被忽略。返回值 执行成功则返回0,有错误发生时返回-1。
- setenv(char *key,char *val,int override)
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[],char *env[])
{
char *s = getenv("MYVALUE");
if(s == NULL){
printf("have no env var:MYVALUE\n");
}else{
printf("MYVALUE:[%s]\n",s);
}
int ret = setenv("MYVALUE","100",1); //当第三个参数非零时,会将MYVALUE的值修改为100
if(ret == 0){ //修改成功返回0
printf("%s\n",getenv("MYVALUE"));
}
return 0;
}
注意:通过此函数并不能添加或修改 shell 进程的环境变量,或者说通过setenv函数设置的环境变量只在本进程,而且是本次执行中有效。如果在某一次运行程序时执行了setenv函数,进程终止后再次运行该程序,上次的设置是无效的,上次设置的环境变量是不能读到的。
- putenv(char *key=val)
同样通过此函数并不能添加或修改 shell 进程的环境变量,或者说通过setenv函数设置的环境变量只在本进程,而且是本次执行中有效。
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[],char *env[])
{
putenv("MYVALUE=100");
printf("%s\n",getenv("MYVALUE"));
return 0;
}
程序地址空间
程序地址空间回顾
- 地址(内存地址):对内存以字节为存储单元的一个编号;通过地址就能找到具体对应的内存单元
- 程序:就是一堆死代码,保存在程序文件中(硬盘)
- 编译器在编译程序生成可执行程序文件是,就会对每一条指令,每一个数据,进行一个地址排号,当程序运行的时候,就会将指令以及数据放到指定的内存地址位置
- cpu就会根据地址偏移逐步去执行指令,以及找到对应的数据进行处理
- 程序运行之后才会占据内存,因此程序地址空间通常被称为进程地址空间
- 一个内存地址只能指向一个唯一的内存单元 —— 一个内存单元只能存储一个数据
来段代码感受一下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int val = 100;
int main()
{
pid_t pid = fork();//创建子进程
if(pid>0){
//父进程
printf("parents[%d]:%d %p\n",getpid(),val,&val);
}else if(pid == 0){
//子进程
printf("child[%d]:%d %p\n",getpid(),val,&val);
}else{
perror("fork");
return 0;
}
return 0;
}
输出结果:
不难发现,输出出来的变量值和地址是一模一样的,很好理解,因为子进程按照父进程为模版,父子并没有对变量进行任何修改。
将代码稍加改动:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int val = 100;
int main()
{
pid_t pid = fork();//创建子进程
if(pid>0){
//父进程
val = 200;
sleep(3); //父进程先修改,完成之后,子进程再读取
printf("parents[%d]:%d %p\n",getpid(),val,&val);
}else if(pid == 0){
//子进程
printf("child[%d]:%d %p\n",getpid(),val,&val);
}else{
perror("fork");
return 0;
}
return 0;
}
输出结果:
不难发现,父子进程输出地址一致,但是变量值不一样,得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址,并非物理内存地址。
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址 。
进程地址空间
所谓的程序地址空间,实际上也是一个虚拟的地址空间,是操作系统通过一个mm_struct结构体所描述的一个假的地址空间
mm_struct(task_size,start_code,end_code) —— 通过大小以及区域的编号描述
分页&虚拟地址空间:
说明:
- 同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。
- 通过虚拟地址空间映射到物理内存上进行数据存储,可以实现数据在物理内存上的离散式存储,提高内存的利用率。
- 每个进程都有自己的虚拟地址空间,因此对于每一个进程来说,都会拥有自己的一块连续的空间的使用。
若进程直接访问物理内存,会出现的问题:
- 程序在编译时,编译器就会给指令和数据进行地址编号;但是如果某个地址内存已经被占用,则这个程序就运行不起来了。编译器的地址管理麻烦(无法动态的获知什么时候哪一块内存是否被使用,也就无法进行代码以及数据的地址赋值)
- 进程直接访问物理内存,如果有一个野指针,你在操作的时候有可能就把其它进程中的数据改变了(无法进行内存访问控制)。
- 程序运行加载通常需要使用一块连续的内存空间,对内存的利用率比较低