学习操作系统

学习操作系统

1、并发和并行

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生

并发单核CPU来说,某一个时刻只有一个线程运行,隔了一段时间之后,由另外一个线程来进行运行,这个是并发。

对于一个多核CPU来说,可以同时有多个线程在多个CPU上运行,这个是并行。

超线程的到来说明了一个CPU可以有虚拟核CPU,可以实现运行多个线程(下面讲到),说明了单核CPU也是可以实现并行和并发操作的。

2、进程和线程:

进程和线程是由早期的计算机发展史引入而来的。

2.1、单处理机系统

早期的计算机用纸带让CPU来执行对应的操作。但是这种方式存在一个问题,在进行IO操作的时候,也就是从纸带读取到内存中的时候(那时候不知道有没有内存的概念),这个时候CPU处于等待状态,这是一件无法让人忍受的事情,因为理想目标是让CPU一直处于运行状态;

2.2、批处理系统

2.2.1、进程由来

在批处理系统中讲解进程由来

批量读取命令,我读取了A指令的时候,就去执行;那么CPU在执行的过程中,还在读取纸带上的命令。那么就实现了在读取的时候,CPU也在处于运行状态。但是这又有了一个新的问题。纸带上有好多命令,比如说有一段命令是进行1+1的操作,另外一段是2+2的操作,结果内存没有管理好,导致了CPU运行的是1+2,最终得到的结果和理想的结果不一致。那咋整?这时候引入了进程的概念,计算机给两个不同的命令分配两块不同的空间,两个进程各自使用各自的空间的数据,互不干扰。

进程表示单个运行活动集的计算机程序,是系统的资源分配调度的基本单元,是操作系统结构的基础。
在早期面向进程的计算机结构中,过程是程序的基本执行实体,在面向线程设计的现代计算机结构中,进程是线程的容器。程序是对指令、数据及其组织形式的描述,流程是程序的实体。

操作系统引入进程的概念的原因:从理论角度看,是对正在运行的程序过程的抽象。从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。这就是上面说的如何来划分内存区域来防止数据干扰的问题解决方式。

2.2.2、线程由来

上面介绍了进程是运行着的程序的抽象,那么一个程序中存在着多个需要执行完成的任务。

例子:

A进程:
{
    // 计算操作会导致处于运行状态
    XXX;
    // IO操作或者是文件上传操作
    YYY;
    // 需要使用到XXX中运行的结果
    ZZZ;
}

在A进程中发现,YYY这个任务和XXX和ZZZ没关系,结果就造成了ZZZ的执行需要等到YYY执行完成,然后才能执行ZZZ,但是YYY又和ZZZ没啥关系。就是YYY阻拦了ZZZ的运行,一直让ZZZ处于等待状态。这个时候程序员就很不爽了,我希望YYY执行的时候,ZZZ也执行,可能YYY执行完了或者还没有执行完,ZZZ已经执行完了,A进程任务都结束了,等你YYY执行完就可以了。那么多核CPU完全是可以胜任的。(我严重怀疑,指令重排就和这个有关系)。那么引入了线程的概念,我们这个将YYY用一个线程来进行执行,ZZZ用一个线程来执行。那么这样子操作,一来节省了时间;而来,还让CPU都处于运行状态。合理的运用了CPU的资源。

这里有现成的代码可以参考(会在最后贴出来)

线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:
1、线程状态。
2、当线程不运行时,被保存的现场资源。
3、一组执行堆栈。
4、存放每个线程的局部变量主存区。
5、访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

也就是说线程切换的时候,CPU中得保存到这里信息。

线程上下文切换。

如:线程在某一个时间段发生了切换,此时CPU需要使用寄存器等其他器件保存当前线程的状态,也就是执行现场。当线程再次执行的时候,需要恢复执行现场,再次进行执行。

3、超线程

超线程,Intel提出来的一种思想:一个物理CPU提供了两个线程逻辑,也就是模拟出两个逻辑CPU。也就是说,两个线程可以在同时在一个CPU上进行执行,虽然采用超线程技术能够同时执行两个线程,当两个线程同时需要某个资源时,其中一个线程必须让出资源暂时挂起,直到这些资源空闲以后才能继续。因此,超线程的性能并不等于两个CPU的性能。而且,超线程技术的CPU需要芯片组、操作系统和应用软件的支持,才能比较理想地发挥该项技术的优势。

4、用户空间和内核空间

操作系统采用的是虚拟地址空间,以32位操作系统举例,它的寻址空间为4G(2的32次方),这里解释二个概念:

  1. 寻址: 是指操作系统能找到的地址范围,32位指的是地址总线的位数,你就想象32位的二进制数,每一位可以是0,可以是1,是不是有2的32次方种可能,2^32次方就是可以访问到的最大内存空间,也就是4G。
  2. 虚拟地址空间:为什么叫虚拟,因为我们内存一共就4G,但操作系统为每一个进程都分配了4G的内存空间,这个内存空间实际是虚拟的,虚拟内存到真实内存有个映射关系。例如X86 cpu采用的段页式地址映射模型。(不用去管如何实现的)

操作系统将这4G可访问的内存空间分为二部分,一部分是内核空间,一部分是用户空间。

内核空间是留给操作系统中的程序的,用户空间是用来留给用户自定义的程序来进行使用的。

以linux操作系统为例,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间

-------------   0xC0000000
|            |  内核空间
|            |	给OS中的程序使用
-------------   0xFFFFFFFF
|            |  用户空间(通常比内核空间大一点)
|            |	给我们的程序来运行
------------- 

OS的作用就是为了管理计算机硬件设备。如果用户空间的程序使用到硬件设备了,那么需用通过向操作系统来进行申请使用。操作系统申请通过,那么就可以帮助用户空间的程序来执行命令。这里说的申请,其实是通过操作系统向外提供的接口来进行操作的。

java底层用C语言来进行实现的,当java启动时候,会启动一个进程。通过库函数调用系统调用接口,与计算机进行交互。

4.1、为什么有用户空间和内核空间

在早期的计算机设计中,其实是没有区别的。这里就会设计到为什么会有操作系统的由来了。在早期计算机中,如果想要去操作计算机,那么就必须通过计算机的指令来进行执行。但是计算机的指令全部都是010101组成的,这样的可读性太差了。后来出现了类似汇编指令,用一个可读的指令,如ADD,代表加操作,对应机器指令0101010100(这里是乱写的),那么汇编在经过翻译之后,对应的就是二进制数据,然后将数据运送到计算机中去进行执行。

于是乎,汇编指令将常用的二进制指令封装成人能够看懂的代码。然后编写的用户程序来写出来对应的程序来操作计算机。

随着计算机的发展,程序员们可以在裸机上实现任何想要的操作,只要懂二进制对应的汇编操作。于是软件行业日新月异,但是人们发现这种效率太低了,因为每写一个程序,都需要去查询对应的指令,然后将指令进行收集编写代码。

操作系统的出现,一统天下。我将你想要的操作都给封装起来,指令我都给你收集好。我把常用的功能也给封装好,你想要用,直接调用操作系统给你提供的接口就好了。人们也发现这种方式比较方便,于是操作系统的趋势就发展起来了。

这里的重点是操作系统给用户提供接口。如果没有对应功能的接口实现,那么还不如自己来写程序实现响应的功能。所以操作系统的优点在这块体现的比较明显。

在早期的操作系统中,存在着很大的缺陷。存在着一些恶意破坏计算机的人,这些人难道是出于好玩吗?不过还是要感谢这些人,因为有了这些人的破坏,操作系统才会发展的越来越好。早期OS的设计本来是为了提供便利,但是有些人绕过了操作系统,利用一些指令去破坏在操作系统上运行的用户的程序。比如我在用微信发信息,结果发送的信息被别人截获了,虽然信息发送出去了,但是别人也获取到了我发送的信息。那么数据安全方面无法得到保障。再比如:我电脑程序运行的好好的,突然一个指令绕过了操作系统,发送了关闭计算机的指令信号,CPU不管这个指令是从哪里来的,直接就给执行了。然后计算机直接关机了。这种情况是操作系统无法容忍的。因为带给用户的体验感极差,所以操作系统为了优化,希望将外界程序操作计算机的命令都给屏蔽掉,外界程序想要来操作,那么只能通过我的操作系统来进行操作。

如下图:

在用户编写的程序中,如果用到了计算机中操作系统中的功能,那么直接通过操作系统提供的接口来使用即可。

除了上面的文件管理,还有最常见的网络管理。OS也封装好了网络通信的功能,用户在使用的时候直接进行使用就可以了。

这样子看,简化了程序编写者的难度。

那么接着聊用户空间和内核空间,既然操作系统是为了方便外界来进行操作的。那么肯定是得为外来的数据进行保存,并且为其提供运行环境,但是操作系统又不想让你直接绕过我的操作系统,于是进行分层。操作系统内部运行的空间叫做内核空间,用户使用的叫做用户空间。用户空间的程序需要通过OS申请通过后,指令在内核空间中进行运行。那么既保护了内部程序,也让外部程序实现了对应的工作,还保证了安全性。

5、用户态和内核态

操作系统为了保护自己严格控制用户程序的资源访问,不需要外部资源的程序运行状态是用户态,反之需要内核帮忙操作资源此时就是内核态。

主要设计出来的目的就是为了保证程序的准确运行。

6、线程模型

1、进程是OS分配资源的基本单位,线程是CPU执行的基本单位。

在现代操作系统中,按照用户空间和内核空间,线程模型分为有ULT(用户线程)KLT(内核线程)

比较抽象的概念,线程模型是相对于程序来说的。我们的程序在用户态进行时,如果需要使用到外部资源的时候,会通过系统调用来调用内核线程帮助我们来完成工作。

用户态是指在用户空间中正在执行的代码和数据,内核态是指在内核空间中正在执行的代码和数据。

外部资源:可以通俗的认为一件硬件设备。

那么导致线程从用户态到内核态的几种条件:1、线程中断;2、异常;3、系统调用;

常见的系统调用:进程控制相关的信息、文件管理相关操作、外部设备、系统信息、通信(进程通信和线程通信)

如:读写文件、new对象(申请内存)等

6.1、ULT

用户线程是完全建立在用户空间的线程库,线程运行在Run-Time System中,内核对此一无所知。对于内核来说,它这是在处理一个单线程的进程而已。在用户空间实现线程时,每一个进程针对自己的线程维护了一个用于保存线程运行的各种变量,比如寄存器,PC,状态等信息的线程表(Thread Table),该线程表在进程的Run-Time System中维护,当一个线程被block,她的当前运行状态会被保存在线程表中,当再次启动时,也会读取线程表中已经保存的状态,从该状态进行再次运行。用户线程的创建、调度、同步和销毁全由库函数在用户空间完成,不需要内核的帮助。

优势

  1. 线程的创建由Run-Time System通过调用现有的LibraryProcedure完成,创建和销毁进程的开销非常小。
  2. 因为线程由Run-Time System进行维护,在同一个进程内部,线程的切换没有必要和内核打交道,所以线程之间的切换的开销非常小,没有Context Switch,也没有内存缓存的刷新重置。
  3. 用户空间的线程可以自定义调度算法,程序员完全可以自己写一套针对自己程序的线程调度算法。

劣势

  1. 对于内核来说,不管进程里面有多少个线程,内核仍然按照单线程进程来处理这个进程,所以同一时间一个进程里面只能有一个线程运行,就算有多个cpu空闲,也只能有一个线程运行,所以无法最大限度的使用资源
  2. 由于只能有一个线程运行,当某一个线程被block后,整个进程都会被block。

通过上面的ULT模型知晓,线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。应用程序可以通过使用线程库设计成多线程程序,用户线程的创建、调度、同步和销毁全由线程库完成,无需利用系统调用(也就是通过接口),而是自己来进行实现。由于处理器时间片分配是以进程为基本单位,所以同一时间一个进程里面只能有一个线程运行,进程中的所有线程竞争分配到的时间片。

6.2、KLT

内核线程是建立在内核空间的线程库,只运行在内核态,不受用户态上下文的拖累。和用户线程注意区分

内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程同样有线程表(Thread Table),该线程表在内核中维护,内核线程可以在全系统内进行资源的竞争,它们的建立和销毁都是由操作系统负责、通过系统调用完成的。

在此模型下,线程的切换调度由系统内核完成,系统内核负责将多个线程执行的任务映射到各个CPU中去执行。

如下:

内核线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。这被称作”一对一”线程映射。

优势

  1. 线程表包含所有进程的线程,所以一个进程有可能有多个线程同时在多个cpu上同时运行
  2. 一个线程被block不会导致整个进程被block,CPU会看是不是有其他线程可以运行。

劣势

  1. 创建线程消耗非常大,需要在用户空间和内核之间切换。内核线程需要完整的上下文切换,修改内存映像,使高速缓存失效,这会导致了若干数量级的延迟。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型

用户进程使用系统内核提供的接口———轻量级进程(Light Weight Process,LWP)来使用系统内核线程。在此种线程模型下,由于一个用户线程对应一个LWP,因此某个LWP在调用过程中阻塞了不会影响整个进程的执行。内核线程通过线程表去查询其他的来进行执行。

由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作,但是轻量级进程具有它的局限性:

首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。

其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。

6.3、混合型线程模型

这里不再仔细去分析了。

放个链接:

1、https://blog.csdn.net/weixin_39954674/article/details/110395028?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-1&spm=1001.2101.3001.4242

2、https://www.cnblogs.com/kaleidoscope/p/9598140.html

7、java线程模型

证明下java使用的是KLT模型。

/**
 * 测试java使用的线程模型
 */
public class ThreadDemoFour {
    public static void main(String[] args) {
        // 创建一千个线程
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                try {
                    // 让每个线程睡眠5秒钟
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

打开windows任务管理器,选择性能,然后看下线程数,然后运行程序看看是不是大概增长了一千个线程。

经过验证,结论是正确的。

总结

1、线程、进程的由来及其带来的好处

2、线程和进程是两个相对独立的概念,线程更多是对执行序列的抽象,进程更多是运行空间的抽象,它们是交叉的,按OS实现的方便,有时可以切换执行序列而不切换运行空间(例如Linux的进程内线程切换),有时可以切换运行空间而不切换执行序列。所以说这两种情况在计算机中都是可能发生的。所以在java高并发中,我们的通常理解成就是空间不发生切换,而让线程发生切换,让线程在独立进行运行。进程切换需要消耗大量资源,线程切换消耗资源少。

3、线程发生上下文的切换,最重要的地方就是频繁创建线程销毁线程,那么将会导致资源频繁消耗。可能线程所携带的任务执行时间都没有创建线程消耗的时间多,那么将会导致频繁浪费。所以推荐使用线程池来进行使用。

另外采用KLT线程模型,在内核空间用一张线程表来进行保存,当使用到的时候直接通过系统调用即可完成;java中采用了比较干脆的方式来进行操作,通过用户态到内核态的映射,通过操作的时候来进行调用即可,然后从内核态切换到用户态,下次使用的使用再次切换过来;

4、OS带来的优势及为什么会有用户空间、内核空间和用户态以及内核态

对应的参考模型案列:

https://blog.csdn.net/m0_37606574/article/details/87805473

https://blog.csdn.net/qq_26963495/article/details/79015110

https://blog.csdn.net/snvjd/article/details/102963259

/**
 * 线程池配置
 */
@Configuration
@EnableAsync//开启异步调用
public class ThreadExecutorConfig {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    /** 核心线程数 */
    private int corePoolSize = 10;
    /** 最大线程数 */
    private int maxPoolSize = 200;
    /** 队列数 */
    private int queueCapacity = 10;

    /**
     * @Configuration = <beans></beans>
     * @Bean = <bean></bean>
     * 返回值类型为<bean></bean>中的属性"class"对应的value
     * 方法名为<bean></bean>中的属性"id"对应的value
     * @return
     */
    @Bean
    public ExecutorService testFxbDrawExecutor(){
        logger.info("start executor testExecutor ");
        // 这里使用的是org.springframework.scheduling.concurrent提供的定时调度的并发包中的类
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setThreadNamePrefix("test-fxb-draw-service-");

        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 执行初始化
        executor.initialize();
        return executor.getThreadPoolExecutor();
    }

}

在业务层来进行使用的时候:

@Resource(name="testFxbDrawExecutor") // 参数名字
ExecutorService executorService

在业务逻辑中来进行使用:

// 使用submit方法来执行,也就是说希望得到返回值。但是这里执行的时候,会立马返回一个伪值!也就是说不是真的处理完毕后的值。
// 真正处理完毕的值在下面
Future<Map> queryAcct = asyncService.submit(() -> {
    logger.info("--------------------1.1二类户查询------------------");
    return accountInfoApi.queryCardInfo(conCurrParams);
});

Future<String> queryCoreDate = asyncService.submit(() -> {
    logger.info("--------------------1.2查询核心时间------------------");
    return getCoreDateService.getCoreDate();
});

Future<Map> queryCustomCertifiedInfo = asyncService.submit(() -> {
    logger.info("--------------------1.3验证身份证到期日------------------");
    return customInfoApi.queryCustomCertifiedInfo(conCurrParams);
});

try {
    // 只有在进行get的之后,这里会阻塞。直到处理当先线程处理结束得到真正的结果才会结束。
    Map acctInfo = queryAcct.get();//异步调dubbo查询二类户的返回结果
    String coreDate = queryCoreDate.get();//异步调dubbo查询核心时间的返回结果
    Map customCertifiedInfo = queryCustomCertifiedInfo.get();//异步调dubbo查询身份证信息的返回结果
} catch (Exception e) {
    e.printStackTrace();
}

其实很多时候,这些都应该用这种方式来进行处理。因为如果说真正的要等到每个方法执行之后再来操作的话,效率上会达不到。

那么使用线程池和使用异步的区别在哪里呢?

posted @ 2021-08-01 00:06  雩娄的木子  阅读(128)  评论(0编辑  收藏  举报