进程概念:
冯诺依曼体系结构,操作系统,进程概念,进程状态,环境变量,程序地址空间
冯诺依曼体系结构:
现代计算机的硬件体系结构,规定了现代计算机应该有哪些硬件单元
硬件单元:
输入设备——键盘;输出设备——显示器;存储器——内存条;运算器+控制器(中央处理器CPU)
主频:2.5GHz——CPU每秒可以处理多少指令
缓存:CPU的评判标准,越大越好
所有设备都是围绕存储器工作的,存储器作为中间的数据缓冲而存在
内存——内存条:8G
外存——硬盘:500G
硬件结构决定了软件行为,比如helloworld程序,printf进行数据输出,程序运行,会被加载到内存中。
操作系统:Linux
组成:内核+应用
定位:计算机上搞管理的软件,管理计算机上的软硬件资源
管理:库函数->系统调用接口->操作系统内核->硬件驱动->硬件
用户通过用户操作接口(库函数shell命令)等调用系统调用接口,访问系统内核,驱动硬件(键盘,鼠标……)
系统调用接口:操作系统提供的用于访问系统内核的接口
系统调用把应用程序的请求传输给系统内核执行
系统调用通过中断完成,这一过程中系统由用户态变为内核态(又程系统态)。 在内核态下,系统可以无限制的访问内核资源
库函数与系统调用接口的关系:库函数实际上就是对系统调用接口进行的一层封装后的接口。
系统调用给用户屏蔽了设备访问的细节
系统调用保护了一些只能在内核模式执行的操作指令
print和scan都是C库函数,read,这是一个linux下的系统函数,C库函数相应的函数是fread。
进程:运行中的程序,对于操作系统来说,进程就是一个程序运行的描述,通过这个描述,操作系统可以进行程序的调度运行管理,实际上对于系统来说,进程就是pcb。
cpu的分时机制:可以使系统快速地在处理各个程序之间切换。
时间片:cpu调度运行程序的时间段,时间片过后,就该切换调度下一个进程了。
并发:一种轮询处理的方式,一个处理完了再处理下一个
并行:同时运行
程序是静态的概念;进程是程序在处理机上一次执行的过程,是动态的概念,一个程序可以作为多个进程的运行程序。
一个进程是一个程序对某个数据集的执行过程,是分配资源的基本单位。作业是用户需要计算机完成的某项任务,是要求计算机所做工作的集合,一个作业可由多个进程组成,且必须至少由一个进程组成。
进程是运行中的程序,有前台与后台之分(前台进程可以理解为当前占用终端的进程,正在与用户交互的进程),而程序只是一段静态指令集,并没有前后台之分
***进程控制块(PCB)***:(在linux下是个task_struct结构体)对程序运行的描述。通过程序运行的描述,操作系统就可以调度哪个程序可以占用cpu去运行指令,要运行哪个程序,则系统找到对应程序的pcb,在pcb取出程序的运行所需信息,加载到cpu上,cpu就开始运行这个程序。进程信息放在该数据结构中。
程序描述信息:
标示符: 描述本进程的唯一标示符,用来区别其他进程;
状态: 任务状态,退出代码,退出信号等;
优先级: 相对于其他进程的优先级;
程序计数器: 程序中即将被执行的下一条指令的地址;
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。 I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等;
其他信息。
进程的简单操作
创建进程:进程就是一个PCB,是一个task_struct结构体(Linux中),创建一个进程实际上就是创建了一个task_struct结构体。
shell中运行的程序,父进程都是shell;或者说,shell中运行的程序都是shell的子进程。shell就是命令行解释器,是一个程序,bash是这种程序中的具体的一个。
创建进程的接口——pid_t fork(void),通过复制调用这个接口的进程(父进程),创建一个新的进程(子进程)。fork有两个返回值,父进程返回值为子进程id,子进程返回0。父子进程代码共享,数据各自开辟空间。
getpid()——返回值为进程ID
getppid()——返回值为父进程ID
exit()——退出进程——谁调用退出谁
ps -ef / -aux :打印所有进程信息
PID:进程ID
PPID:父进程ID
ps -ef | grep fork:匹配ps -ef的结果中包含fork的语句
可以看到程序加入fork();后运行了两遍。fork函数创建了一个子进程,子进程从fork之后的程序语句开始运行。
可以看到,“你”只打印了一次。
可以看到,“你”和第一个“我”的进程ID一致,第二个“我”的进程不一样。
进程状态:用于操作系统对于进程的管理(什么状态该对进程进行什么样的操作)
运行态,就绪态,阻塞态
Linux中的进程状态:
运行态(R):正在运行或者轮转到时间片则能够运行的状态统称运行态
可中断休眠态(S):可以被中断的休眠状态,满足唤醒条件或者休眠被中断则进入运行态
不可中断休眠态(D):不可被中断的休眠状态,满足唤醒条件之后才进入运行态
停止态(T):程序停止运行的状态(依然会被调度,但什么都不做),只有收到继续运行的指令的时候才开始运行
死亡态(X):进程退出了就是死亡态,一闪而逝
僵尸态(Z):进程已经退出不再调度了,但这个进程的资源还没有完全释放等待处理的状态。
僵尸进程:处于僵尸态的进程,是一种退出了但资源没有完全被释放的进程,产生原因:子进程先于父进程退出,但是父进程没有关注到子进程的退出,因此系统不会完全释放子进程的资源,这个子进程进入僵尸态。子进程退出后,在进程pcb中保存了自己的退出返回值,在父进程没有关注处理的情况下,pcb资源是不会被释放的。
僵尸进程的危害:资源泄露,一是pcb所占的内存资源一直无法被回收,二是一个用户所能创建的进程数量是有限的。
处理方法:退出父进程
如何避免:进程等待
孤儿进程(不需要处理,有自己的应用方向):父进程先于子进程退出,子进程就会变成孤儿进程,运行在后台,父进程成为1号进程(早期名字叫init进程,后期叫systemd)
精灵进程(守护进程):特殊的孤儿进程
环境变量:
环境变量也是一种变量,是一种保存系统运行环境参数的变量;便于运行环境参数的配置以及进程之间的数据通信。
命令操作:env——查看环境变量
echo——查看指定变量
set——查看shell当中所有变量,不限于环境变量
export——声明或设置环境变量
unset——删除环境变量
常见环境变量:
PATH:程序运行的默认搜索路径——在命令行终端中输入命令名称可以直接执行对应名称的命令程序,实际上是因为shell捕捉到输入的命令名称之后,然后去PATH环境变量指定的路径下去找这个程序,找到了就运行,找不到就报错
$表示后面的是变量名
环境变量接口:
getenv():通过环境变量名称获取值
通过代码获取环境变量
extern char** environ;
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
#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;
}
程序地址空间
地址:通常所说的地址,都是内存的地址,是内存单元的编号
在进程中一个物理内存单元只能存储一个数据——实际上在进程中,程序访问的这些地址(变量地址等)实际上是一个假地址,将其称之为虚拟地址;每个进程都有自己独立的虚拟地址空间,也就是有自己的mm_struct内存描述
我们所说的程序地址空间实际上叫做进程的虚拟地址空间。
虚拟地址空间:在Linux下是系统给进程所描述的一个假的地址空间,是一个mm_struct结构体
系统会为每一个进程都描述一个假的地址空间。
进程访问的都是虚拟地址,访问内存数据的时候,先将虚拟地址转换为物理地址然后访问。
系统为每个进程都描述一个完整的连续的线性的虚拟地址空间,实际物理内存用的时候再给进程分配,这样对于每个进程看起来都有一块完整的连续的内存可以使用
页表:映射虚拟地址和物理地址。给每个进程一个虚拟地址空间,让进程访问完整连续的地址,但是这些虚拟地址在使用的时候通过页表映射一块物理内存地址,进行访问物理内存。通过映射的这种方式,实现了数据在物理内存上的离散式存储,提高内存利用率。
总结:虚拟地址空间,是Linux系统为每个进程通过mm_struct结构体虚拟的一个地址空间,使用虚拟地址空间的目的是为了让进程能够访问一块连续的完整的地址,并且经过页表映射到物理内存之后,可以实现数据在物理内存上的离散式存储,提高内存利用率,并且在页表中可以进行内存访问控制。
在CPU和物理内存之间进行地址转换时,( MMU)将地址从虚拟(逻辑)地址空间映射到物理地址空间。
MMU:内存管理单元,一种负责处理中央处理器(CPU)的内存访问请求,功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制。
那么虚拟地址如何通过页表获取到物理地址呢?
内存管理方式:
分段式内存管理:将地址空间分为多段(代码段,数据段…),便于编译器进行地址管理。但是需要连续的地址空间。
分段式虚拟地址组成:段号+段内的地址偏移量;
在系统中有一个段表:段表中有一个个段表项(段号+物理内存段起始地址)
通过段号将虚拟地址和段表匹配,再将起始地址与偏移量相加就得到数据
分页式内存管理:将地址空间分为多块(页),提高内存利用率,实现数据离散式存储。
分页式虚拟地址组成:页号+页内偏移;
在系统中有一个页表(页号,物理内存块起始地址,权限控制,缺页中断位…)
缺页中断:缺页中断表示当前地址要访问的数据不在物理内存中。
交换内存:当物理内存不够使用时,则将物理内存中不活跃的数据保存到磁盘的交换分区中,腾出的物理内存可以给新的程序数据使用。
不活跃的数据:
最久未使用(LRU):是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
页号占8位,则有2的8次方个页表项。
取出虚拟地址的页号,在页表中找匹配的页号,看各项是否符合,若符合则找物理块起始地址,与页内偏移相加,即得变量占据的内存
例如有个虚拟地址为十进制5480,页面大小为4096个字节,问物理地址为多少。
页号=5480/4096
页内偏移=5480%4096
页号乘以4096 +(后缀) 页内偏移即为物理地址
虚存地址空间4GB(2的32次方),则虚地址长度为32位,页面大小4KB(2^12),则页内地址长度为低12位,即页内偏移
虚拟地址03FF F180H中 : (这里的H表示16进制)
高20位03FFF为虚拟页号
低12位180为页内偏移
查TLB(快表)发现,该页在主存中,03FFF虚拟页号对应物理块号为0153,
所以虚实地址变换后的结果为0153180H。也就是物理块号加上页内偏移
段页式内存管理:将虚拟地址空间进行分段,在每个分段内进行分页式管理,集合了分段分页的优点进行内存管理
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。
用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值