• 操作系统 第二章 进程管理:进程与线程、处理机调度


    目录

    1.进程与线程

    1.1进程的概念、组成、特征

    1.1.1概念

    1.1.2组成

    ①进程控制块(PCB)

    ②程序段

    ③数据段

    1.1.3特征

    1.2进程的状态与转换

    1.2.1进程的五种状态

    1.2.2进程的状态转换

    1.3进程控制

    1.3.1如何实现原语的“原子性”

    1.3.2进程的创建

    1.3.3进程的终止

    1.3.4进程的阻塞和唤醒

    1.3.5进程的切换

    1.3.6小结

    1.4进程通信(IPC)

    1.4.1共享存储

    1.4.2消息传递

    1.4.3管道通信

    1.5线程

    1.5.1概念

    1.5.2线程的实现方式和多线程模型

    线程的实现方式

    用户级线程

    内核级线程

    混合形式

    多线程模型

    一对一模型

    多对一模型

    多对多模型

    1.5.3线程的状态与转换

    1.5.4线程的组织与控制

    2.处理机调度

    2.1七状态模型

    2.2三级调度

    高级调度(作业调度)

    中级调度(内存调度)

    低级调度(进程调度)

    2.3进程调度的时机、方式

    2.4调度算法

    2.4.1评价指标

    CPU利用率

    系统吞吐量

    周转时间

    等待时间

    响应时间

    2.4.2先来先服务(FCFS)调度算法

    2.4.3短作业优先(SJF)调度算法

    非抢占式—SJF

    抢占式—最短剩余时间优先(SRTN)

    2.4.4高响应比优先(HRRN)调度算法

    2.4.5时间片轮转(RR)调度算法

    2.4.6最高优先级调度算法

    2.4.7多级反馈队列调度算法


    1.进程与线程

    1.1进程的概念、组成、特征

    1.1.1概念

    进程就是程序的一次执行过程,是系统进行资源分配和调度的基本单位

    注意:此时还未引入线程概念。

    程序,是存放在硬盘中的一个可执行的文件,即,一系列的指令集合。在多道程序下,操作系统允许程序并发执行,这就导致这些程序之间失去了封闭性,为了更好地描述和控制程序的并发执行,操作系统引入了进程的概念

    当我们启动一个应用程序A后,可以在控制面板中看到有一个与之对应的进程,如果启动多个应用程序A以后,就会出现多个相对应的进程。对于被启动的多个应用程序A,其指令集合是相同的,但是其对应的每个进程,都有着各自的数据,即同一个应用程序执行多次会对应着多个不同的进程

    1.1.2组成

    在介绍进程的组成之前,我们将要引入一个新的概念:进程实体(进程映像)。

    进程实体静态的进程动态的。我们可以把进程实体理解为进程在动态执行过程中某一时刻的快照,进程实体可以反映进程在某一时刻的状态

    如果不进行专门的区分,我们一般将进程实体就称为进程。它由进程控制块(PCB)程序段以及数据段三部分组成。

    进程控制块(PCB)

    进程控制块(Process Control Block,PCB)是一个专门为进程配置的数据结构,用于描述进程的基本情况和运行状态。

    进程在创建时,操作系统会为它创建一个PCB,随后,这个PCB会被放置到内存中,在进程执行时,系统通过该进程的PCB可以了解进程的现行的状态信息,由此操作系统就可以对其进行控制和管理。当进程结束时,操作系统也会将PCB从内存中删除,进程也就随之消亡了。由此可以得知,PCB是进程存在的唯一标志

    系统是如何使用PCB控制进程的

    1. 当操作系统想要调度某个进程运行时,要从该进程的PCB中查出其现在的状态以及优先级。
    2. 当操作系统调度该进程成功后,要根据其PCB中保存的处理机状态信息,设置该进程恢复运行(从上次中断的地方继续执行),并且根据其PCB中的程序和数据的内存始址,找到其程序和数据。
    3. 进程在执行的过程中,如果需要和与之合作的进程实现同步、通信或访问文件时,也需要访问PCB。
    4. 当进程由于某种原因暂停执行时,需要将其断点的处理机环境保存在PCB中。

    由上述过程可知,在进程的整个生命周期中,系统都是通过PCB对进程进行控制的,即系统唯有通过PCB才能感知到该进程的存在

    PCB的组织方式

    由于操作系统中,往往会存在着许多进程,有的处于就绪态、有的处于阻塞态,并且阻塞的原因各不相同。为了方便进程的调度和管理,就需要将各个进程的PCB用适当的结构组织起来。常见的方式有链接方式索引方式两种。

    • 链接方式即将PCB连接成一个队列,不同的状态对应不同的队列,根据进程的不同状态分为不同的队列,如就绪队列和阻塞队列,甚至会根据阻塞的不同原因分成不同的阻塞队列。
    • 索引方式即将PCB放入一个索引表中,不同的状态对应不同的索引表,如就绪索引表和阻塞索引表等。

    大多数操作系统使用的都是链接方式

    ②程序段

    程序段就是被进程调度程序调度到CPU执行的程序代码段,即该进程在CPU中要被执行的代码段。

    注意:一个程序可以被多个进程共享(程序段相同),即多个进程可以运行同一个程序。

    数据段

    进程的数据段包含了进程对应的程序加工处理的原始数据和进程运行过程中产生的中间/最终结果。

    1.1.3特征

    1.2进程的状态与转换

    1.2.1进程的五种状态

    进程有着完整的生命周期,主要包括五种状态:创建态就绪态运行态阻塞态以及结束态

    • 创建态:进程正在被创建时,就处于创建态,在这个阶段操作系统会为该进程分配各种资源并且会初始化PCB。

    创建进程时,会分为多个步骤

    1. 申请一个空白的PCB,并向PCB中填写用于控制和管理进程的信息
    2. 为该进程分配运行时所必须的资源
    3. 把该进程转入就绪态并放置在就绪队列中,如果此时进程所需要的资源无法得到满足,就无法转入成就绪态,此时该进程的创建工作就没有完成,该进程就处于创建态
    • 就绪态:进程获取到了除了CPU之外的所有必要资源后,就会被转为就绪态,在该状态下的进程,一旦被CPU调用,就会立刻执行。由于单核CPU同一时刻只会执行一个进程,因此,系统中会存在着大量的就绪态的进程,这些进程会被放置在一个队列中,该队列被称为就绪队列
    • 运行态进程在CPU上运行时所处的状态就是运行态。

    • 阻塞态:当运行中的进程需要等待某一事件而暂停运行后所处的状态就是阻塞态。在该状态的进程会持续等待某个资源(例如,等待I/O设备),处于该状态的进程,即使CPU是空闲的,也不会被执行。由于系统中的很多资源都是独占式共享,因此系统中也会存在着多个处于阻塞态的进程,这些进程会被放置在队列中,这个队列被称为阻塞队列系统甚至会根据阻塞的原因,将这些阻塞的进程放置在不同的阻塞队列中

    • 结束态:当进程需要被结束运行时,系统就会将该进程转为结束态。

    进程PCB中,会有一个变量 state 来表示进程的当前状态。如:1表示创建态、2表示就绪态、3表示运行态

    1.2.2进程的状态转换

    就绪态、运行态、阻塞态被称为进程的3种基本状态

    注意:一个进程从运行态转为就阻塞态是进程主动的行为,从阻塞态转为就绪态是被动地行为,需要有其他进程与中断处理程序的协助。

    1.3进程控制

    进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。即,进程控制就是实现进程状态的转换

    如何实现进程控制

    进程控制即实现进程状态的转换。进程状态的转换过程需要保证“一气呵成”的完成。因此,我们可以使用原语来实现进程控制

    为什么一定要保证进程状态的转换要“一气呵成”的完成呢

    假设此时有一个进程处于阻塞态(即该进程的PCB处于阻塞队列中),现在要将该进程转换为就绪态,此时负责进程控制的内核程序至少要做以下两件事:

    • 将该进程的PCB的状态设置为就绪态
    • 将该进程的PCB放置在就绪队列中

    如果不能保证上述两个步骤“一气呵成”的完成,例如在完成了第一步后,CPU收到了中断信号,进而去执行相应的中断处理程序,那么此时,该进程的PCB的状态是就绪态,但是PCB处于阻塞队列中,这就导致操作系统中的关键数据结构信息的不统一,从而影响操作系统进行后续的管理工作

    1.3.1如何实现原语的“原子性”

    1.3.2进程的创建

    1.3.3进程的终止

    1.3.4进程的阻塞和唤醒

    1.3.5进程的切换

    1.3.6小结

    其实无论哪个进程控制原语,要做的无非三类事情:

    1. 更新PCB中的信息:修改进程状态 (state),保存/恢复运行环境
    2. 将PCB插入合适的队列
    3. 分配/回收资源

    1.4进程通信(IPC)

    进程通信(IPC)进程与进程之间的信息交换(数据交换)。进程间的通信依赖于操作系统提供的支持

    比方说向微信好友分享文章,就需要进程通信

    为什么进程间的通信依赖于操作系统的支持

    我们知道,进程是操作系统分配系统资源的基本单位,各个进程拥有的内存地址是相互独立的。因此各个进程之间如果要进行数据交换,就必须依赖操作系统的支持。

    为什么各个进程拥有的内存地址要相互独立

    从系统安全的层面来理解,如果各个进程之间拥有的内存地址不是相互独立的,这就意味着,各个进程之间可以相互访问对方的数据。这将会是一个十分危险的事情,例如,你的某些APP中存储有你的私密信息(银行账户、身份证信息等等),此时,你安装了一个恶意的APP,当你同时运行这两个APP时,这个恶意的APP就可以随意的访问你的存有私密信息的APP的数据,那么你的私密信息就会存在暴露的风险。

    因此,为了保证安全,一个进程不能直接访问另外一个进程的内存地址

    进程间的通信可以分为三类:共享存储消息传递以及管道通信

    1.4.1共享存储

    共享存储是指需要进行通讯的若干进程向操作系统申请一块共享空间,该共享空间可以由这些进程直接访问,通过对这片共享空间进行读/写操作实现进程之间的信息交换。操作系统会将这块共享空间映射到对应的进程的虚拟地址空间

    • 为了避免出现错误,在对共享空间进程读/写操作时,需要使用同步互斥工具(如P/V操作)对读/写进行控制。
    • 操作系统只负责为通信进程提供共享空间以及同步互斥工具,数据交换则由用户自行安排读/写指令完成。

    共享存储可以进一步的分为基于数据结构的共享基于存储区的共享

    • 基于数据结构的共享(低级):操作系统在内存中定义好某种数据结构的共享空间供各个进程使用,比如共享空间里只能放大小为10的数组,这种共享方式速度慢、限制多。
    • 基于存储区的共享(高级):操作系统在内存中划分一块共享存储区,而不指定数据结构,数据存放的形式和位置都由进程控制,这种共享方式速度快。

    1.4.2消息传递

    消息传递指进程间以格式化的信息为单位进行数据交换,如果通信的进程之间不存在共享空间,则可以利用操作系统提供的消息传递方法实现进程通信。

    格式化的信息包括信息头信息体两部分

    • 信息头:包括发送进程ID、接收进程ID、信息长度等信息。
    • 信息体:进程要交换的内容。

    消息传递依赖与操作系统提供的发送消息接收消息的两个原语进行的数据交换。

    • 发送消息原语:send(pid,msg)
    • 接收消息原语:receive(pid,&msg)

    消息传递可以进一步分为两种:直接通信方式间接通信方式

    • 直接通信方式:发送进程通过发送原语,直接通过接收进程的PID将消息发送给接收进程,并且将它挂在接收进程的消息缓冲队列上,接收进程从该队列中获取信息。
    • 间接通信方式:发送进程把消息发送到某个中间实体,接收进程从该中间实体获取信息。这种中间实体一般称为”信箱“,因此间接通信方式也被称为信箱通信方式。

    直接通信和间接通信的区别相当于:点外卖直接送到门口与放到外卖柜里

    1.4.3管道通信

    管道通信是指通过管道这种特殊的共享文件(pipe文件)进行进程间的信息交换。

    管道是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件(pipe文件)。向该文件中写入数据的进程被称为写进程,从该文件中读取数据的进程被称为读进程

    特点:

    • 管道中的数据采用FIFO的方式,即先进先出,管道相当于一个循环阻塞队列。写进程往管道写数据,即便管道没被写满,只要管道没空,读进程就可以从管道读数据读进程从管道读数据,即便管道没被读空,只要管道没满,写进程就可以往管道写数据
    • 管道只能采用半双工通信,即同一时间段内只能实现单向的传输,如果想要进行双向传输,则需要两个管道
    • 由操作系统控制各类进程互斥地访问管道。(tips:共享存储需要通信进程自己负责实现互斥)

    管道中的数据一旦被读取,就会彻底的消失,因此,当多个进程读取同一个管道的数据时,可能会发生错乱,此时通常有两种解决方案:

    • 一个管道允许多个写进程和一个读进程。
    • 一个管道允许有多个写进程和多个读进程,且操作系统会让各个读进程轮流从管道中读取数据(Linux操作系统的解决方案)

    管道通信与共享存储的区别

    • 共享存储的互斥访问由通信进程实现,管道通信的互斥访问由操作系统实现。
    • 共享存储中,允许进程在共享空间中的任意位置进行读/写数据,管道通信中,只允许从一边写入,另一边读取数据。
    • 共享存储中,其他进程的写操作会阻塞读操作;管道通信中,只要管道中有数据,就可以从管道中读取数据,不必担心因为其他进程正在对管道进行写操作而阻塞。

    1.5线程

    1.5.1概念

    为什么要引入线程

    引入线程后的变化

    线程的主要属性

    • 线程不拥有系统资源,它共享所属进程的资源。
    • 线程是CPU独立调度的基本单位,多个线程可以并发执行。
    • 一个线程被创建后,便开始了它的生命周期,直至终止,线程在生命周期内会经历阻塞态、就绪态和运行态等各种状态变化。
    • 每个线程都有一个线程ID、线程控制块 (TCB)
    • 同一进程中的线程切换,不会引起进程切换,切换同进程内的线程,系统开销很小。不同进程中的线程切换,会引起进程切换

    1.5.2线程的实现方式和多线程模型

    线程的实现方式

    线程的实现可以分为两类:用户级线程(ULT)内核级线程(KLT)

    用户级线程

    使用用户级线程时,有关线程管理的所有工作由应用程序通过线程库实现。线程库是指由编程语言为程序员提供的创建和管理线程的API库。

    下图是自己编程模拟了一个非常弱智的“线程库”:

    此时,操作系统内核无法感应到线程的存在,只能感应到进程的存在。即,在用户看来是多个线程在并发执行,但是对于操作系统内核而言,只有一个进程在运行。(只有用户能体会)

    内核级线程

    使用内核级线程时,有关线程管理的所有工作由操作系统内核程序实现操作系统会为每个内核级线程设置一个TCB,以此来对内核级线程进行管理

    混合形式

    混合形式是指,将用户级线程和内核级线程组合起来使用。在该实现方式中,内核支持多个内核级线程的建立、调度和管理,也允许用户程序建立、调度和管理用户级线程。一个内核级线程可以对应多个用户级线程(这些用户级线程时分多路复用内核级线程)。使用该实现方式的系统的结构图如下图所示。

    使用上述结构的操作系统,CPU的调度单位是内核级线程,但是允许应用程序通过线程库管理用户级线程

    这种方式结合了用户级线程和内核级线程的优点。

    多线程模型

    当操作系统同时支持用户级线程和内核级线程,即使用的是上文介绍的混合型方式时,由于用户级线程和内核级线程的连接方式的不同,可以分为三种不同的多线程模型:一对一模型多对一模型以及多对多模型

    一对一模型

    多对一模型

    这就是纯粹地使用用户线程的方式

    多对多模型

    1.5.3线程的状态与转换

    与进程十分类似,线程也具有五种状态:新建状态、就绪状态、运行状态、阻塞状态、终止状态。由于线程可以理解为对进程进行了进一步的划分,因此这五种状态以及五种状态之间的转换,与进程基本相同。

    1.5.4线程的组织与控制

    与进程类似,操作系统为每个线程也配备了一个线程控制块TCB,用于记录控制和管理线程的信息。

    2.处理机调度

    当同时有多个事情要处理时,由于资源有限,无法同时处理这些事情,此时就需要通过按照某种规则来决定处理这些事情的先后顺序,这就是“调度”。

    2.1七状态模型

    之前我们学习了进程的五种状态,此时为了学习CPU调度,我们需要额外的学习进程的一种特殊的状态挂起状态(挂起态),挂起态可以进一步的被细分为就绪挂起阻塞挂起两种状态,因此进程的五状态模型就可以进一步延伸为七状态模型

    什么时候会出现挂起状态的进程

    由于内存有限,需要将某些进程的数据调出暂存至外存,待这些进程需要运行且内存空闲时再重新调入内存,这些被暂存至外存等待的进程的状态就是挂起状态。处于挂起状态的进程的PCB会被组织成挂起队列

    七状态模型的转换图如下图所示

    阻塞与挂起的比较

    • 相同之处:都处于暂时无法获得CPU的服务
    • 差异之处:阻塞状态的进程映像依旧在内存中,而挂起状态的进程映像处于外存中。

    2.2三级调度

    在操作系统中将调度分为了三级,分别是高级调度,中级调度,低级调度。

    高级调度(作业调度)

    高级调度也被称为作业调度,是指按照一定的原则从外存的作业后备队列中挑选一个作业调入内存,并且创建相应的进程,以便于其可以竞争CPU。每一个作业只调入一次,调出一次当作业被调入时就会为其建立PCB,调出时就会撤销其PCB

    作业:一个具体的任务

    用户向系统提交一个作业= 用户让操作系统启动一个程序(来处理一个具体的任务)

    内存空间是有限的,因此当内存空间已满时,又有用户向操作系统提交了作业,此时这些作业就会被放入内存之外的作业后备队列中。

    中级调度(内存调度)

    中级调度也被称为内存调度,是指按照某种规则决定将哪个处于挂起状态的进程重新调入内存一个进程可能被多次调出和调入内存内存调度的频次比作业调度要高。

    低级调度(进程调度)

    低级调度也被称为进程调度,是指按照某种规则从就绪队列中选取一个进程,将CPU分配给该进程。

    进程调度是操作系统中最基本的一种调度频率最高,一般几十毫秒一次。

    2.3进程调度的时机、方式

    进程调度(低级调度)是通过调度程序完成的,调度程序是操作系统内核程序。当请求调度的事件发生后,才可能运行调度程序,调度了新的就绪进程后,才会发生进程的切换。然而并非所有情况下,请求调度后就一定可以发生进程的调度和切换。

    可以进行进程调度与切换的情况

    1. 当前进程主动放弃CPU
      • 进程正常终止时,会发生进程调度与切换
      • 进程运行过程中,发生异常而终止时,会发生进程调度与切换
      • 进程主动请求阻塞(请求I/O等)时,会发生进程调度与切换
    2. 当前进程被动放弃CPU
      • 分配给当前进程的时间片用完时,会发生进程调度与切换
      • 有优先级更高的进程要运行时,会发生进程调度与切换
      • 有更紧急的事情要处理(如I/O中断)时,会发生进程调度与切换

    不可以进行进程调度与切换的情况

    1. 在处理中断的过程中,不能进行进程调度与切换。因为中断处理过程十分复杂,在实现上很难做到进程切换。
    2. 进程在操作系统内核的临界区中,不能进行进程调度与切换。当进程进入操作系统内核临界区后,需要对该临界区加锁,不允许其他进程访问,在解锁前不应该切换到其他进程,以避免发生临界区无法解锁的情况发生。
    3. 在进行原子性(原语)操作时,不能进行进程调度与切换。在进行原子性操作时,连中断都要屏蔽,就更不应该进行进程调度与切换了。

    • 临界资源一个时间段内只允许一个进程使用的资源,各个进程要互斥地访问临界资源。
    • 临界区:访问临界资源的代码片段。
    • 内核临界区一般用于访问某种操作系统内核数据结构,例如进程的就绪队列等,因此内核临界区必须要尽快的释放锁,否则可能会影响操作系统内核的其他管理工作。

    基于上述介绍,进程在操作系统内核临界区中不能进行进程调度与切换,但是进程在普通临界区(如一些IO设备等)中时,可以进行进程调度与切换

    进程调度的方式

    进程调度方式是指根据既定规则,选择一个处于就绪状态的进程并将CPU资源分配给它的过程。通常情况下有两种进程调度方式:非抢占式调度抢占式调度

    "进程调度"与"进程切换"的区别

    • 狭义的进程调度:从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程也可能是另一个进程,后一种情况就需要进程切换)
    • 进程切换:一个进程让出处理机,由另一个进程占用处理机的过程。进程切换的过程主要完成了 对原来运行进程各种数据的保存 以及 对新的进程各种数据的恢复
    • 广义的进程调度:包含了选择一个进程和进程切换两个步骤。

    2.4调度算法

    2.4.1评价指标

    就像每个人有每个人不同的优点一般,不同的调度算法也有着不同的特性,为了对调度算法的性能进行评价,人们提出了很多评价标准。主要的评价标准有:CPU利用率系统吞吐量周转时间等待时间以及响应时间

    CPU利用率

    CPU处于忙碌状态的时间占比。

    CPU利用率=CPU有效工作时间/(CPU有效工作时间+CPU空闲等待时间)

    系统吞吐量

    系统吞吐量指单位时间内CPU完成作业的数量。

    周转时间

    周转时间是从提交一个进程或作业到完成其执行的总时间。它包括等待时间和执行时间。即:

    1. 作业在外存后备队列上等待作业调度(高级调度)的时间
    2. 进程在就绪队列上等待进程调度(低级调度)的时间
    3. 进程在CPU上执行的时间
    4. 进程等待I/0操作完成的时间。

    后三项在一个作业的整个处理过程中,可能发生多次。

    周转时间=作业完成时间-作业提交时间

    对于操作系统而言,更加关心整体作业的周转表现,因此延伸出了另外一个评价标准——平均周转时间,平均周转时间是指多个作业周转时间的平均值。

    均周转时间=各个作业周转时间总和/作业数

    对于不同的作业,较大的作业周转时间一般较长,较小的作业周转时间一般较短,因此延伸出了一个评价指标——带权周转时间,带权周转时间是指作业周转时间与作业实际运行时间的比值

    带权周转时间=作业周转时间/作业实际运行时间

    基于带权周转时间自然而然的就延伸出了另外一个评价标准——平均带权周转时间。

    平均带权周转时间=各作业带权周转时间/作业数

    等待时间
    • 等待时间是指一个进程或作业在就绪队列中等待分配CPU时间片的时间。
    • 等待时间是周转时间中等待执行的部分,它表示一个进程或作业在系统中花费的时间,但不包括实际执行时间。

    注意:等待时间不包含等待I/O设备操作完成的时间,因为在这个时间内,进程正在被该I/O设备服务。

    等待时间=周转时间-运行时间-IO操作的时间;若为纯计算型则无IO操作

    响应时间

    响应时间是指从用户提交请求到系统至首次产生响应所用的时间,响应时间越短,用户体验越好。响应时间不考虑任务的完成,只关注系统开始处理请求的速度。

    2.4.2先来先服务(FCFS)调度算法

    • 作业调度决定哪些作业应该被加载到内存中,以便进程可以执行。
    • 进程调度决定哪个进程应该获得CPU时间片执行。

    First Come First Severd, FCFS

    2.4.3短作业优先(SJF)调度算法

    Shortest Job First, SJF

    非抢占式—SJF

    抢占式—最短剩余时间优先(SRTN)

    • 非抢占式的调度算法:当前进程主动放弃处理机时发生调度。
    • 抢占式的调度算法:当前进程主动放弃处理机时发生调度。另外,当就绪队列发生改变时也需要检查是会发生抢占。

    2.4.4高响应比优先(HRRN)调度算法

    三种算法的对比

    2.4.5时间片轮转(RR)调度算法

    时间片轮转(Round Robin, RR)

    时间片为2举例:

    以时间片为5举例:

    与FCFS对比

    2.4.6最高优先级调度算法

    非抢占式例子

    抢占式例子

    2.4.7多级反馈队列调度算法

    三种算法的对比总结

  • 相关阅读:
    Java是如何制作月饼的——制作、下单和售卖
    利用PerconaTookit工具在线修改表结构
    香港金融交易解决方案提供商【移动财经】申请840美元纳斯达克IPO上市
    Golioth 发布基于乐鑫 ESP-IDF 的开源 SDK
    Spark SQL函数
    vivado流程导航器详细介绍【全网最详细】
    Kafka是什么,以及如何使用SpringBoot对接Kafka
    WebDAV之π-Disk派盘 + Diarium
    Java程序中调用Python脚本(兼容Windows与Linux)
    荐书 | 《大脑的奥秘:人人要懂的脑科学》:大脑里面有什么
  • 原文地址:https://blog.csdn.net/qq_62767608/article/details/132756412