操作系统: 一组做计算机资源管理的软件的统称。
目前常见的操作系统有:Windows系列、Unix系列、Linux系列、OSX系列、Android系列、iOS系列、鸿蒙等。
操作系统的定位:
蓝色部分:系统调用、操作系统内核、驱动程序都属于操作系统。
操作系统由两个基本功能:
进程/任务: 就是跑起来的程序(运行起来的可执行文件),进程是操作系统对一个正在运行的程序的一种抽象,
换言之,可以把进程看做程序的一次运行过程。同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。
因为操作系统支持多任务了,程序员也就需要并发编程了,通过进程,完全可以实现并发编程。
创建进程:先创建 PCB, 然后把 PCB 加到双向链表中。
销毁进程:找到链表上的 PCB,然后从链表中删除。
(我们平时查看任务管理器其实就是遍历这个链表。)
计算机内部要管理任何现实事物,都需要将其抽象成一组有关联的、互为一体的数据。在 Java 语言中,我们可以通过类/对象来描述这一特征。
// 以下代码是 Java 代码的伪码形式
class PCB {
// 进程的唯一标识 —— pid;
// 进程关联的程序信息,例如哪个程序,加载到内存中的区域等
// 分配给该资源使用的各个资源
// 进度调度信息
}
PID:
进程 ID -> 进程的身份证号。
内存指针:
指明这个进程要执行的代码/指令以及这个进程执行中所依赖的数据存放在内存的哪块区域。
运行可执行文件时,操作系统会把这个 可执行文件(包含进程执行的二进制指令以及数据)加载到内存中变成进程。
文件描述符表:
程序运行过程中,经常要和文件打交道,进程每打开一个文件,就会在文件描述符表上添加一项。
(这个表可视为一个数据,里面的每个元素又是一个结构体,对应一个文件信息。)
一个进程只要启动,不管代码中是否包含打开/操作文件的代码,都会默认打开三个文件(系统自动打开)
(1) 标准输入流
(2) 标准输出流
(3) 标准错误流
状态:
描述当前进程接下来应该怎样调度
(1) 就绪状态:随时可以去 CPU 上执行
(2) 阻塞状态:暂时不可去 CPU 上执行, 比如正在进行 IO 密集型操作,如读写数据。
(3) 运行状态:正在 CPU 上执行
优先级:
根据进程的优先级来决定:先给哪个进程分配时间,后给哪个进程分配时间,以及给哪个进程分配的时间多, 给哪个进程分配的时间少。
上下文:
表示 该进程上次被调出 CPU 时,当时该进程的执行状态,下次该进程上 CPU 时,可恢复之前的状态,然后继续往下执行。
进程被调度出 CPU 之前,需要把 CPU 寄存器里面的数据都保存到内存中(就是该 上下文字段),相当于存档了,下次 再被调上 CPU 执行时,就从刚才的内存中恢复这些数据到寄存器中,相当于时读档了,存档+读档存储的信息就被称为 上下文。
记账信息:
统计了每个进程都分别执行了多久,分别执行了哪些指令,分别排队等了多久,从而给进程调度提供知道依据。
以上只是几个核心属性。
这样,每一个 PCB 对象,就代表着一个实实在在运行着的程序,也就是进程。
操作系统再通过这种数据结构,例如线性表、搜索树等将 PCB 对象组织起来,方便管理时进行增删查改的操作。
就是 操作系统考虑 CPU 资源如何给各个进程分配。
操作系统对内存资源的分配,采用的是空间模式 —— 不同进程使用内存中的不同区域(虚拟地址空间),互相之间不会干扰。
虚拟地址空间: 程序中获取到的内存地址并不是真正的物理内存地址,而是经过一层抽象,虚拟出来的地址。
操作系统上同时运行多个进程,如果某个进程出现 bug 崩溃了,不会影响到其他进程(进程之间相互独立),就是因为内存按照虚拟地址空间分配。
一旦进程访问越界了,MMU 硬件设备就会向进程反馈,从而终止进程的非法操作。
进程是操作系统进行资源分配的最小单位,这意味着各个进程互相之间是无法感受到对方存在的,这就是操作系统抽象出进程这一概念的初衷,这样便带来了进程之间互相具备 “隔离性(Isolation)”。
但现代的应用,要完成一个复杂的业务需求,往往无法通过一个进程独立完成,总是需要进程和进程进行配合地达到应用的目的,如此,进程之间就需要有进行“信息交换“的需求。进程间通信的需求就应运而生。
目前,主流操作系统提供的进程通信机制有如下:
其中,网络是一种相对特殊的 IPC 机制,它除了支持同主机两个进程间通信,还支持同一网络内部非同一主机上的进程间进行通信。
线程: 一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码.
比如如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。
为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
首先, “并发编程” 成为 “刚需”.
其次,用进程实现并发编程有点问题:
解决:
为什么线程比进程更轻量?进程重在哪?
多加一些线程,执行效率会不会进一步提高 ?
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程” (Coroutine)
进程和线程都是为了解决并发编程这样的场景, 但是使用进程进行并发编程的话会有一些问题,频繁的创建、调度和销毁线程时效率低,相比之下,线程更轻量,创建、释放以及调度的效率高,只有创建第一个线程和释放最后一个线程时才需要资源的申请或释放。
进程是系统分配资源的最小单位,线程是系统 CPU 调度的最小单位。
进程之间具有独立性,每个进程有自己独立的虚拟地址空间,一个进程挂了,不会影响其他进程。一个进程中的多个线程共用同一块内存空间,一个线程挂了,可能影响其他线程,甚至导致整个进程崩溃。
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
class Main {
private static class MyThread extends Thread {
@Override
public void run() {
// 每个线程的任务都是循环 10 次
for (int i = 1; i <= 10; i++) {
// 打印当前线程的名字
System.out.println(Thread.currentThread().getName() + "正在运行!");
try {
// 休眠 1 s
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
// main 线程也循环 10 次
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在运行!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
使用 JDK 中自带的 jconsole 工具就可以查看对应的线程:
注意:
好啦! 以上就是进程与线程的基本讲解,希望能帮到你 !
评论区欢迎指正 !