浅析操作系统的进程、线程区别

4,553 阅读9分钟

系统的硬件组成

为了理解一个程序运行时发生了什么,需要理解一个典型系统的硬件组织.
image
- 总线

总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束。

按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。

  • I/O设备
    显示器、鼠标、键盘等,每个I/O设备都通过一个控制器或适配器与 I/O 总线相连,从而传递信息.

  • 主存 一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的逻辑.

  • 处理器
    计算器能够执行命令的核心,它是解释(执行)存储在主存中指令的引擎,从系统通电开始到系统断电,CPU 一直在不断低执行指令。

操作系统

对于以上所说的硬件设施,我们的程序一般不会直接访问这些硬件设备,转而通过操作系统来控制。
操作系统有2个基本功能
1) 对普通程序对硬件的访问进行限制,防止硬件被失控的应用程序滥用
2) 向应用程序提供简单一直的机制来控制复杂而又大相径庭的低级硬件设备.

操作系统通过几个基本概念( 进程,虚拟存储器,文件)来实现这2个功能

image

其中,文件是对 I/O 设备的抽象,虚拟存储器是对 主存和磁盘I/O设备的抽象表示,进程是对 处理器、主存 和 I/O设备的抽象表示

进程

进程的概念

为了实现多任务系统,现代操作系统提出了进程的概念,在linux系统初期,进程作为 CPU 调度的基本单位,后来由于操作系统普遍引入了线程的概念,线程成为了CPU调度的基本单位,而进程只能作为资源拥有的基本单位

操作系统中,进程提供给应用程序两个重要的抽象:
1) 它在独立地使用处理器 (通过进程调度,CPU时间片分配)
1) 它在独立地使用主存(通过进程的虚拟地址空间映射)

进程描述符

有了抽象的概念 ,再来对照着看进程在linux中的实际表示形式,在linux中 task_struct描述符结构体表示一个进程的控制块,这个 task_struct结构记录了这个进程所有的context (进程上下文信息)

struct task_struct{
    //列出部分字段
    volatitle long state;//表示进程当前的状态 ,-1表示不可运行,0表示可运行,>0表示停止
    void                *stack; //进程内核栈
    unsigned int            ptrace;

    pid_t               pid;//进程号
    pid_t               tgid;//进程组号
    struct              mm_struct *mm,*active_mm //用户空间 内核空间

    struct              list_head thread_group;//该进程的所有线程链表
    struct              list_head thread_node;
    int                 leader;//表示进程是否为会话主管
    struct              thread_struct thread;//该进程在特定CPU下的状态

    //等等字段:包括一些 表示 使用的文件描述符、该进程被CPU调度的次数、时间、父子、兄弟进程等信息
}

虚拟存储器(虚拟地址空间)

虚拟存储器是一个抽象的概念,它为每一个进程提供一个假象,即每个进程都在独占的使用主存(整个内存区域)。
虚拟存储器对应 上文中的 mm_struct结构,每个进程都有自己独立的 mm_struct 结构,各个进程都在自己的地址空间中活动,互不干扰。

image

一个虚拟存储器空间包含以下内容

Text Segment、Data Segment、BBS

TextSegment 表示程序的代码段

DataSegment 表示 已经初始化且初值非0的全局变量和静态局部变量
BBS表示未初始化或初始值为0的全局变量和静态局部变量

操作系统负责这些段的加载并分配内存空间,这些段在编译期就分段完成。

Stack(栈)

栈:用于存放 局部变量函数参数函数返回地址

Heap(堆)

动态分配的内存,由用户自己管理和释放,堆的大小可以在运行时动态地拓展和收缩(C函数调用), 因为空闲地址空间时不连续的,堆在操作系统中是用链表来存储的。

Kernerl Space(内核空间)

内核空间 属于操作系统的一部分,常驻内存。操作系统不允许普通的用户应用程序读写这个区域的内容或者直接调用内核空间定义的函数。

虚拟存储器在内核中的结构图

task_struct中的一个字段指向了 mm_struct,它描述了虚拟存储器当前的状态,其中 pdg
指向了第一级页表的基地址,而 mmap指向一个 vm_area_structs 区域结构的链表,每个vm_area_structs都描述了当前虚拟地址空间的一个区域。

虚拟存储器是如何使用主存作为缓存的

物理存储器和虚拟存储器都是用页来作为磁盘和内存的存储单元。
因为有页表的存在,没有必要把虚拟存储器(在磁盘上)的所有页缓存在内存中 ,即使当CPU访问虚拟存储器中的页不存在内存中时,系统会通过查表,把需要的页从磁盘中拷贝到内存中(涉及到页面调度算法)。
image

进程上下文

进程上下文:一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈上的内容。内核在进行进程的切换时,需要保存当前进程的所有状态,即保存当前进程的上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

线程

线程在linux中有时被称为轻量级进程(LightweightProcess,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源(共享整个虚拟地址空间)。

线程的实现

在linux中,实际上线程和进程使用的是同一个结构体 task_struct,在 linux 眼里,不会区分进程和线程,内核在进程任务调度时,仅仅是根据调度算法选择一个 task_struct,至于这个task_struct,到底是进程、还是线程 内核并不关心。
属于同一个进程有一个线程组的概念,当创建一个线程时,创建一个 task_struct结构体,但是这个 task_struct 共享进程的 虚拟内存 (也就是 task_struct 结构中的 mm_sruct mm字段)。正是由于多线程共享 mm_struct mm

一个多线程在内核中的数据模型图

image
在linux C中,使用 fork()函数创建进程,使用pthread_create创建线程,
上图中,右边的task_struct 是由左边的 task_struct 通过 pthread_create()创建的。这2个task_struct 通过指针共享一个mm_struct。

上下文切换

当CPU 执行从一个线程(进程)到另一个线程(进程)时,需要先保存当前工作 线程的上下文信息,以便下次回来时重新运行。这些信息包括,
多线程同多进程一样,存在一个上下文切换的消耗,但是线程的上下文切换要比进程小的多\
image

简单的逻辑描述

  1. 进程A运行中
  2. 时钟中断发生,ring1->ring0 ,时钟中断处理程序启动
  3. 进程调度,下一个应运行的进程B 被指定
  4. 进程B被恢复,ring0->ring1
  5. 进程B运行中

ring 是CPU的特权级别,ring0 是内核级别

多线程 vs 多进程

  • 多线程之间堆内存共享,而进程相互独立,线程间通信可以直接基于共享内存来实现,比进程的常用的那些多进程通信方式更轻量。
  • 在上下文切换来说,不管是多线程还是都进程都涉及到寄存器、栈的保存,但是线程不需要切换 页面映射(虚拟内存空间)、文件描述符等,所以线程的上下文切换也比多进程轻量
  • 多进程比多线程更安全,一个进程基本上不会影响另外一个进程
    在实际的开发中,一般不同任务间(可以把一个线程、进程叫做一个任务)需要通信,使用多线程的场景比多进程多。但是多进程有更高的容错性,一个进程的crash不会导致整个系统的崩溃,在任务安全性较高的情况下,采用多进程。

协程、共行程序、Coroutine

直接通过协程的特性来理解
- 协程是用户模式下的轻量级线程,操作系统内核对协程一无所知
- 协程的调度完全有应用程序来控制,操作系统不管这部分的调度
- 一个线程可以包含一个或多个协程
- 协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下纹和栈保存起来,在切换回来- - 时恢复先前保运的寄存上下文和栈
- 协程能保留上一次调用时的状态,看到这里各种生成器(生成器是被阉割的协程)的概念浮现出来了。。
Windows下的实现叫纤程

栈溢出

public class Stack {
    static  int time = 0;
    public static void main(String[] args) {
        test();
    }
    public static void test(){
        time++;
        System.out.println("time ="+time);
        test();
    }
}
//time =6957Exception in thread "main" java.lang.StackOverflowError

Java 在创建线程时设置栈大小

    /**
    *     * <p>The virtual machine is free to treat the {@code stackSize}
     * parameter as a suggestion.  If the specified value is unreasonably low
     * for the platform, the virtual machine may instead use some
     * platform-specific minimum value; if the specified value is unreasonably
     * high, the virtual machine may instead use some platform-specific
     * maximum.  Likewise, the virtual machine is free to round the specified
     * value up or down as it sees fit (or to ignore it completely).
    */
    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }

堆溢出

public class Heap {
    public static void main(String[] args)
    {
        ArrayList v=new ArrayList();
        for(int i=0;i<25;i++)
            v.add(new byte[1*1024*1024*1024]);
    }
}
//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
//  at Heap.main(Heap.java:13)

Java 虚拟机指定堆大小 (堆是线程共享区域)

Java虚拟机的堆大小如何设置:命令行
 java –Xms128m   //JVM占用最小内存
     –Xmx512m   //JVM占用最大内存
     –XX:PermSize=64m   //最小堆大小
     –XX:MaxPermSize=128m //最大堆大小