作者:赢在青春创业团队 | 来源:互联网 | 2023-07-12 13:44
冯诺依曼体系结构——现代计算机硬件体系结构
计算机应该包含五大单元:
- 输入设备:采集数据的,比如:键盘,网卡(接受网络中的数据)
- 输出设备:进行数据输出,比如:显示器,网卡(向网络中发送数据)
- 存储器:进行中间数据缓冲,比如:内存
- 运算器:进行数据运算,运算器+控制器 = 中央处理器CPU
- 控制器:进行设备控制
所有的设备都是围绕存储器工作的。
存储器实际上就是内存,为什么不是硬盘呢?
硬盘的数据吞吐量太低了:机械--200MB/s
内存的数据吞吐量:是机械硬盘的数十倍内存速度这么快,为什么内存只用于缓冲,不使用内存存储数据呢而是使用硬盘存储?
因为硬盘与内存的存储介质不同,内存是易失性介质,数据掉电就会丢失,而硬盘掉电后数据不会丢失
 
- cpu不会直接从输入设备获取数据进行处理,而是先把数据放到存储器中,cpu从存储器中获取数据处理
- cpu不会直接将数据交给输出设备进行输出,而是先把数据放到存储器中,控制输出设备从存储器中获取数据输出。
- 硬件结构,决定了软件行为。所有的设备都是围绕存储器工作的。
操作系统——内核加外部应用
操作系统:内核+应用
功能:与硬件交互,管理所有的软硬件资源
定位:搞管理的软件
目的:让计算机更加好用
操作系统如何进行软硬件的管理:先描述,再组织
系统调用接口:操作系统向用户提供的访问内核的接口
库函数与系统调用接口关系:库函数封装了系统调用接口;上下级的调用关系
进程概念
进程是什么?
浅层:运行中的程序。
深层:站在操作系统的角度,进程就是一个运行中程序描述——PCB(进程控制块),通过PCB,才能实现程序的运行调度管理。
PCB
Linux下,PCB实际就是一个结构体:struct task_struct{...}
进程如何描述一个运行中的程序:
- 内存指针——能够让操作系统调度程序运行的时候,知道进程对应的指令以及数据在内存中的位置。
- 上下文数据——保存寄存器当中的值。(程序运行过、运行中的以及即将执行的的指令和数据(让操作系统在调度切换进程运行的时候,使得cpu知道进程运行到哪里了))
- 程序计数器——程序中即将被执行的下一条指令的地址。
- 标识符PID——描述本进程的唯一标示符,用来区别其他进程。
- 进程状态,优先级,记账信息(一个进程在cpu上运行的时间),IO信息。
操作系统中的进程都是同时运行的。
cpu只有一个,如何做到多个程序同时运行?
cpu的分时机制:实现系统同时运行多个程序的技术Cpu只负责执行指令,处理数据(处理哪个程序其实cpu并不关系)因此操作系统的进程管理就体现出来,对程序的运行调度进行管理。
cpu进行程序运行处理的时候,并不会一次性将一个程序运行完毕才会运行下一个,而是每个程序都只运行一段很短的时间(给一个程序分配的一个时间片),时间片运行完毕则由操作进行调度,让另一个程序的代码数据在cpu上进行处理。
如何保持程序同时进行?时间片
时间片:程序在CPU撒谎给你运行的这段时间,运行完毕则调度切换(人在视觉上,会觉得程序在同时进行)
cpu分时机制实现cpu轮询处理每一个运行中的程序,而程序运行调度由操作系统进行管理。
操作系统将每一个程序的运行信息保存下来,进行调度管理的时候才知道这个程序上一次运行到了哪里。
并发:
并行:
进程状态
进程状态:每个进程pcb中都会描述一个运行的状态信息,通过状态信息,告诉操作系统这个进程什么时候能够干什么,是否能够被调度。
操作系统中的进程状态:
- 就绪状态:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。
- 运行状态:当一个进程在处理机上运行时,则称该进程处于运行状态。
- 阻塞状态:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
 
Linux下的进程运行状态:
- 运行态R(running)——处于就绪状态的和运行状态的
- 可中断休眠状态S(sleeping)——可以被打断的休眠,通常满足运行条件,或者被一些中断打断休眠之后进入运行状态。
- 不可中断休眠状态D(disk sleep)——只能通过唤醒条件自然满足才会进入运行状态,不会被一些中断打断休眠。
- 停止状态T(stoped)——可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- 死亡状态X(dead)
- 追踪状态t(tracing stop)
- 僵尸状态(Zzombie)——描述的是一个进程退出了,但是进程资源没有完全被释放,等待被处理的一种状态。
查看进程的信息——ps
注:ctrl+z没有杀死进程,只是将进程停止了
- ulimit -a 可以查看用户所能创建的最大进程数
动态查看活跃进程的信息——top
僵尸进程
- 僵尸进程:处于僵尸状态的进程。
- 僵尸进程的产生:
子进程先于父进程退出,为了保存退出原因,因此资源并没有完全被释放。因此子进程退出时,操作系统会通知父进程,让父进程获取子进程的退出原因,然后释放子进程的所有资源。如果父进程当前没有关注子进程退出状态,则子进程就会成为僵尸进程。
- 僵尸进程的危害:资源泄露,导致正常进程无法正常启动。
- 僵尸进程的避免:进程等待——一直关注子进程,退出了就能立马发现。
- 僵尸进程的处理:退出父进程。
- 为什么创建子进程?
子进程和父进程干的事情一样,但是是用返回值分流之后可以有所不同。有任务之后创建子进程,让子进程去做,出了问题,崩溃的就是子进程,父进程不会受到影响,保护父进程。
- 孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,会被运行在后台,父进程成为1号进程(孤儿进程退出不会成为僵尸进程)
- 1号进程在centos7之前叫做init进程,在centos7之后叫做systemd进程
- 守护进程(精灵进程):特殊的孤儿进程。[在孤儿进程的基础上,脱离登陆会话] 通常运行在后台,默默工作,不希望受到终端会话的影响。
进程创建:
pid_t fork(void)
解释:creates a new process by duplicating the calling process. The new process, referred to as the child, is an exact duplicate of the calling process,referred to as the parent。(通过复制正在调用的进程来创建新进程。新进程称为子进程,正在调用的进程,称为父进程)
通过复制调用进程创建一个子进程,子进程因为拷贝了父进程PCB里面很多数据,因此子进程退出时,操作系统会通知父进程,让父进程获取子进程的退出原因。
子进程与父进程内存指针以及程序计数器、上下文数据都相同,所以运行的代码以及运行的位置都一样。
返回值
- == -1 创建子进程失败
- == 0 对于子进程,返回值是0
- > 0 对于父进程,返回值是子进程的PID
- 通过返回值可以进行父子进程代码分流
创建父进程和子进程:
#include
#include
#include
int main()
{
pid_t id = fork();
if(id <0)
{perror("fork\n");return 1;
}
else if(id == 0)
{
//如果是子进程,让子进程先于父进程退出printf("I am a child,pid = %d\n",getpid());exit(0);
}
else
{printf("I am a parent,pid = %d\n",getpid());sleep(10);
}
return 0;
}
运行结果:
打开两个终端,一个终端运行该程序。
&#160;
另一个终端查看这两个进程的详细信息,可以看出当子进程先于父进程退出时,子进程的状态会变成僵尸状态。
fork创建进程之后,父子进程的运行顺序是不一定的,关键是看操作系统的调度。
进程优先级:决定进程cpu资源的优先分配权 --PRI NI
(数字越小,优先级越高)
程序分类:cpu密集/io密集
进程分类:批处理/交互式
renice -n 19 -p pid;
设置进程优先级(nice值的范围为-20 ~ 19)
环境变量——存终端shell中进行系统运行环境配置的变量
设置环境变量作用:
相关指令
- echo(打印指定变量的内容) --- echo $PATH——查看PATH变量的内容
- set(查看所有变量,包含环境变量在内)
- export: 声明\定义\转换一个环境变量
注:shell中的普通变量可以起到环境配置的作用,但是无法进行数据传递。
典型环境变量
- PATH : 存储程序默认的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash
代码操作:
char* getenv(const char* name);
//通过环境变量名称,获取环境变量内容
获取环境变量的三种方式
法一:通过环境变量名称获取变量数据
#include
#include
#include
int main()
{
//char *getenv(const char *name);
//通过环境变量名称获取内容
char *ptr = getenv("MYVALUE");
if(ptr == NULL)printf("have no MYVALUE\n");
elseprintf("MYVALUE = %s\n",ptr);
return 0;
}
运行结果
法二:全局变量
#include
#include
int main()
{
//声明这个变量,这个变量是库中的全局变量,只要声明就能够使用
extern char **environ;
int i = 0;
for(; environ[i]; i++){printf("%s\n", environ[i]);
}
return 0;
}
运行结果
法三:通过main函数的第三个参数
#include
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
运行结果
程序地址空间——虚拟地址空间
32位操作系统最大支持的内存大小是4G(2^32),因为寻址空间只有这么大,指针大小只有4个字节
- 地址:内存地址---对内存以字节为存储单元的一个编号;通过地址就能找到具体对应的内存单元。
- 程序:就是一堆死代码,保存在程序文件中(硬盘)编译器在编译程序生成可执行程序文件时,就会对每一条指令,每一个数据,进行一个地址排号。当程序运行的时候,就会将指令以及数据放到指定的内存地址位置。cpu就会根据地址偏移逐步去执行指令,以及找到对应的数据进行处理。
- 程序运行之后才会占据内存,因此程序地址空间通常被称为——进程地址空间。
- 虚拟内存的产生:
&#160; &#160; 5. 虚拟地址空间:是操作系统为进程所描述的一个假的地址空间并非物理内存地址。是操作系统为进程通过一个mm_struct结构体所描述的一个假的地址空间mm_struct(task_size,start_code,end_code),通过大小以及区域的编号描述(32位下task_size = 4G)
&#160; &#160; 6. 为什么操作系统不让进程直接访问物理内存,而是弄了一个虚拟地址空间,让进程访问虚拟地址呢??
若进程直接访问物理内存,则:
-
程序在编译时,编译器就会给指令和数据进行地址编号;但是如果某个地址内存已经被占用,则这个程序就运行不起来了--编译器的地址管理麻烦(无法动态的获知什么时候那块内存是否被使用,也就无法进行代码以及数据的地址赋值)
-
进程直接访问物理内存,如果有一个野指针,你在操作的时候有可能就把其它进程中的数据改变了(无法进行内存访问控制)
-
程序运行加载通常需要使用一块连续的内存空间,对内存的利用率比较低。
&#160; &#160; 7. 虚拟地址空间目的是为了让进程认为自己拥有一块连续的线性的完整的地址空间。但是实际上一个进程使用的内存并非连续存储,而是通过页表映射了虚拟地址与物理地址之间的关系;让进程通过页表获取物理地址,进而实现数据的离散式存储。
&#160; &#160; 8. 虚拟地址空间的作用
MMU——内存管理单元(memory management unit)
MMU是存储器管理单元的缩写,是用来管理虚拟内存系统的器件。MMU通常是CPU的一部分,本身有少量存储空间存放从虚拟地址到物理地址的匹配表。此表称作TLB(转换旁置缓冲区)。所有数据请求都送往MMU,由MMU决定数据是在RAM内还是在大容量存储器设备内。如果数据不在存储空间内,MMU将产生页面错误中断。
MMU的两个主要功能是:
1. 将虚地址转换成物理地址。
2. 控制存储器存取允许。MMU关掉时,虚地址直接输出到物理地址总线。
创建一个子进程的流程——写实拷贝技术
- 创建pcb
- 拷贝父进程pcb中的数据(拥有相同的虚拟地址空间,相同的页表...)
- 父子进程一开始映射同一块物理地址内存
- 等到物理内存中的数据发生修改的时候,会为子进程重新开辟内存,拷贝数据过去(因为进程要保持独立性)
代码共享,数据独有——因为代码段是只读的,不会被修改,因此会一直映射同一块物理内存
写时拷贝技术——子进程创建出来后,与父进程映射访问同一块物理内存,当物理内存中数据即将发生改变时,重新为子进程开辟物理存,拷贝数据过去。(为了避免直接给子进程开辟空间,拷贝数据,而子进程不使用,降低了进程创建效率,造成内存冗余数据)
&#160;
内存管理方式
将虚拟地址的组成分为 段号+段内偏移量(比如全局数据段有很多变量,他们的段号都是一样的,也就意味着物理内存段的起始地址一样,但是每个变量的偏移量不同)因此,通过段号对应的物理内存段起始地址,以及虚拟地址中的偏移量组成一个完整的物理地址,找到对应的物理内存单元。
分段式的优点:对编译器的地址管理比较友好;但是没有解决数据连续存储内存利用率低的问题。因为一个段管理了很多变量数据,这些变量就都是通过同一个起始地址进行偏移的,也就在物理地址中使用了连续的地址空间(分段式管理中,同一个段内地数据都使用了连续的地址空间)
因为通常物理块比较小,并且不要求同一个进程的多个数据必须在同一个块内,因此分页式实现了数据在物理内存中的离散式存储,提高了内存利用率并且页表会在进行内存访问的时候进行内存访问控制(是否有权限)。
分页式内存管理的优点:实现数据离散式存储,提高内存利用率,并且通过页表进行内存访问控制。
程序的局部性原理:
程序运行时,无需全部装入内存,装载部分即可
如果访问页不在内存,则发出缺页中断,发起页面置换
从用户层面看,程序拥有很大的空间,即是虚拟内存
缺页中断
进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
那么需要将哪些数据交换出去呢?页面置换算法
页面置换算法
先进先出算法(FIFO)
◆把高速缓存看做是一个先进先出的队列
◆优先替换最先进入队列的字块
最不经常使用算法(LFU)
◆优先淘汰最不经常使用的字块
◆需要额外的空间记录字块的使用频率
最近最少使用算法(LRU)
◆优先淘汰一段时间内没有使用的字块
◆有多种实现方法,一般使用双向链表
◆把当前访问节点置于链表前面(保证链表头部节点是最近使用的)
&#160;