操作系统,可以理解为搞管理的软件。
1.对下,需要管理好各种硬件设备。
2.对上,需要给应用程序提供稳定的运行环境。
进程,可以理解为跑起来的程序。如何管理进程?
1.先描述:使用PCB(PCB Process Control Block)结构表示出进程的各种属性。可以粗略理解为将运行程序所要的部分东西打包放到一个PCB单元里面。
2.后组织:使用双向链表,把这些PCB结构给串起来。
PCB结构中的比较重要的属性。
1.pid(进程标识符)
2.内存指针。(进程持有的内存资源)哪些用来存储代码,哪些存储数据,哪些存储运行结果。
3.文件描述符表(进程持有的硬盘资源)
CPU 分配 —— 进程调度(Process Scheduling)
(进程持有的CPU资源),如下4-7所示,这些CPU信息是用来完成进程调度的。由于进程太多,而CPU的核心数太少,因此需要让这些进程能够轮番在CPU上执行,只要轮转的速度够快,宏观上(在用户的眼中),看起来就是这些进程在 “同时” 执行。也称为并发执行。如果说有多个进程在多个cpu上运行,而不是有先后的运行,此时就称为并行。一般统称为并发编程。
4.状态
进程有许多状态,比如 就绪状态、阻塞状态。
5.优先级
进程有优先级,比如游戏和QQ,游戏的优先级就更高。
6.上下文
进程是要不断“登台”、“休息”、“登台”的,因此当进程完成工作以后,需要把对应的结果保存下来,要保存此时CPU各种寄存器的状态,都记录到内存中,方便下一次该进程再次在CPU上计算时,读取之前的结果,继续往后执行。(存档、读档)
CPU中有些寄存器,没有特定含义,只是用来保存运算的中间结果的,还有些寄存器,是有特定含义的,特定作用的。比如
保存当前执行到哪个指令上(程序计数器),是一个2/4/8字节的整数,这个整数存的是一个内存地址,保存程序下一条要执行的指令所在的位置。exe文件里面包含了指令和数据,当运行exe时,操作系统就会把指令和数据加载到内存当中。CPU就会先从内存中取指令,然后执行指令。初始情况下,程序计数器执行进程指令的入口,可以粗略理解为main方法。每次取完一条指令,程序计数器的值就会自动更新,默认情况是指向下一条,也有遇到跳转指令、函数调用指令(jmp,jmpc,call),就会跳转到对应的地方。
维护栈相关的寄存器,通过这一组(一般是两个)维护当前程序的“调用栈”,栈也是一块内存,这个内存里就保存了当前这个程序方法调用过程中,一系列的关系。(也包含局部遍历和方法参数...),比如edp始终指向栈底,esp始终指向栈顶,修改esp的值就可以实现“入栈” / “出栈”。
其他的通用寄存器,往往用来保存计算的中间结果的,比如10+20+30,会把10+20的结果存在寄存器中,再取出这个值和30相加。假设算完10+20以后还没来得及算后面的,进程调度走了,就需要把保存10+20的寄存器的值给备份到上下文中,下次可以接着取出备份的值继续算。
一个cpu中的寄存器也没多少,大概也就几十个字节到几百个字节,数据并不多,好保存好恢复,因此都是把所有的寄存器全部打包给内存存起来。
7.记账信息
通过优先级机制,为不同的进程分配不同权重的资源,而这种方式有可能会出现极端情况,比如所有的资源都给某个进程了,其他进程一点都没分配到。
因此我们需要记录每一个进程持有的cpu的情况(在cpu上执行了多久),如果发现某个进程在cpu上使用时间太少,就让他多执行一点。
为了解决上述问题,于是出现了虚拟内存地址(进程的独立性)。不是直接分配物理内存了,而是分配虚拟的内存空间,操作系统对于内存又进行了一层抽象。(粗暴理解为缓冲带)(虚拟地址指向物理地址,称为页表,类似于hash表,一一对应),此时操作系统可以在虚拟内存上进行检查,判断当前的虚拟地址是否能够翻译成为一个合法的物理地址,如果翻译以后越界了,就不会做出修改,也就不会波及到其他进程了。
通过上述方法,把进程之间给隔离开了,但是如果在某个需求中,需要让多个进程相互配合,此时就需要做出调整。于是引入了新的机制,来实现进程之间的通信。
具体的实现方式有很多,但是每个方法的核心思想是一样的,都是借助一个公共区域,完成数据的交互。
主要是两种:通过文件、通过网络(socket)
线程(Thread)
可以粗略的将进程理解为一个大的代码文件,而线程是这个文件中的一部分代码。多个线程需要并发的执行。
在java这样的生态,并不是很鼓励使用多进程编程,更鼓励使用多线程编程。引入多个进程,最初是为了实现并发编程,因为现在的电脑都有多核cpu。
多进程实现并发编程,效果很理想。但是也有明显的缺点,最大的缺点在于进程太重了,效率不高。
创建一个进程,消耗时间比较多。
销毁一个进程,消耗时间也比较多。
调度一个进程消耗时间也比较多。
消耗是在申请资源上,进程是资源分配的基本单位。分配内存操作,就是一个大活。
操作系统内部有一定的数据结构,把空闲内存块分块管理好。当我们去进行申请内存的时候,系统就会从这样的数据结构中找到一个大小合适的空闲内存,返回给对应的进程。
如果需要频繁的创建/销毁进程,这个时候,开销就不能忽视了(这一点在早期的服务器开发中是非常常见的情况,比如C++CGI就是用多进程的方法实现网站后端的)。
为了解决上述问题,就引入了“线程”(Thread)概念,线程也叫做轻量级进程。
创建、销毁、调度线程都比进程更快。
线程不能独立存在,而是要依附与进程。换言之,进程至少包含一个线程。
一个进程,最开始的时候,至少要有一个线程,这个线程负责完成执行代码的工作,也可已根据需要,创建出更多需要的线程,从而使当前实现“并发编程的效果”。每个线程都可以独立的执行一些代码。
一个进程,是可以有多个线程的,每个线程都可以独立进行调度,每一个线程也有状态、优先级、上下文、记账信息等......
一个进程,使用PCB进行表示,一个进程可能使用一个PCB表示,也可能使用多个PCB表示,每个PCB对应一个线程。
初次之外,前面谈到的pid,内存指针,文件描述符表都是公用一份的。(可以理解为这个大的函数里面公共的部分)
上述的结构,决定了线程的特点:
每个线程都可以独立的去cpu上调度执行。而且同一个进程的多个线程之间,公用一份内存空间,和文件资源。于是,创建线程的时候,不需要重新申请资源了,直接复用之前已经分配给进程的资源。省去了资源分配的开销,于是创建效率更高了。
此时再来理解进程是资源分配的基本单位,线程是调度执行的基本单位更深刻了。因为进程里的部分资源是共享的,线程是自己去cpu上进行调度执行的。
增加线程数目,会增加程序运行效率,但是如果过度增加线程数目,效率就没法继续提升了,反而会因为要调度的线程太多了,使调度的开销更大,反而会降低效率。
如果多个线程去抢同一个cpu资源,就会发生冲突,称为线程不安全问题。
一个线程抛出异常,如果没有妥善处理(try catch抓住),就容易把整个进程都带崩,此时其他的线程也就随之消亡。
进程和线程的区别(经典面试题)
1.进程包含线程,一个进程至少包含一个线程。
2.进程和线程都是用来实现并发编程的场景,但是线程比进程更轻量,更高效。
3.同一个进程和线程之间,公用同一份资源(内存和硬盘)(可以直接放到一个变量中,后续直接访问这个变量即可),省去了申请资源的开销。
4.进程和进程之间是具有独立性的。一个进程挂了,不会影响到其他进程(QQ崩了,别的软件还在运行)。而同一个进程内的线程挂了,是可能会相互影响的(线程安全问题 + 线程出现异常)
5.进程是资源分配的基本单位,线程是调度执行的基本单位。
......
java如何进行多线程编程?
线程是操作系统的概念,操作系统提供一些API,可以操作线程。java针对上述系统API进行了封装(跨平台)。
Thread类,创建Thread对象,进一步就可以操作系统内部的线程了。
一个线程跑起来,从哪个代码开始执行?就是从它的入口方法。
运行java程序,就是跑起来一个java进程,这个进程里面至少会有一个线程,主线程,主线程的入口方法就是main方法。