进程与线程系列文章1
前言
本系列文章1主要讲解的是:进程和线程的基本概念、以及进程和线程之间的关系、多线程后续引入的安全问题。
一、进程
1.1 基本概念
简单来说:进程是操作系统资源分配的基本单位
进程是一个重要的 “软件资源”,是由操作系统内核管理的。
那么具体是怎么管理的呢? 简单来说就是: 描述 + 组织描述:使用结构体 (C语言中的结构体) 来描述进程的属性,用来描述这个进程的结构体,起了个特殊的名字,叫做 “PCB”(进程控制块)
组织:通过双向链表来组织的,意思就是把多个 PCB 给传到一起。
小结:
创建一个进程:本质上创建一个 PCB 这样的结构体对象,把它插入到组织进程的列表中。
销毁一个进程:本质上就是把链表中对应的 PCB 删除掉。
任务器查看进程列表:本质上就是遍历组织进程的链表。
1.2 进程的特性
进程的特性一共有四种,分别有:pid唯一标识符、内存指针、文件描述符表、进程的调度。
1.2.1 pid 唯一标识符
如下图所示:
1.2.2 内存指针
简单来说,就是当线程被创建的时候,操作系统会给进程分配内存空间,这个就是指向了自己的内存是哪些。
1.2.3 文件描述符表
这个就是进程操作文件上的文件资源,它和 内存指针共同描述了进程持有的哪些硬件资源。
1.2.4 进程的调度相关属性
首先,在我们计算机中,硬件资源、内存、硬盘、网卡… 这些资源都好分配,比较困难的是: CPU 的资源不好分配!!!
因为在我们电脑上,时时刻刻都会有很多的进程在运行,而这些进程都得必须依赖 CPU 上的资源,而 CPU 的数量较少。
如图,这是我电脑上的 CPU:
但是,可能上百个进程,希望能够 “同时运行”、“分时复用”,此时引入了 并行 和 并发。
并行:微观上同一时刻,两个核心只能运行一个进程,就是我们平时所认为的同步执行。
并发:微观上同一时刻,同一个核心只能运行一个进程,但是它能够对进程进行快速的切换。举个例子:比如 CPU 这个核心,先运行一下qq音乐,再运行一下微信… 只要它切换的速度足够快(2.5GHz,每秒运行25亿条指令),宏观上感知不到。
1.2.4.1 进程的状态
进程的状态分为:
就绪状态:进程随叫随到,随时前往 CPU 核心上执行。
运行状态:进程正在参与工作。
阻塞状态:好比进程暂时去出差了,短时间无法去 CPU 上执行。
1.2.4.2 优先级
计算机操作系统资源是有限的的,先给哪个进程安排,后给谁排,给谁排多,给谁排少…
上述表明,进程的调度也是有 优先级 的,操作系统并不是一碗水端平的。
1.2.4.3 上下文
操作系统在进行 进程切换 的时候,会保留下上个进程运行的 “中间状态”,下次这个进程再次被调度到 CPU 上执行时,可以恢复上次执行的状态,继续往下执行。
1.2.4.4 记账信息
操作系统会统计每个进程在 CPU 上占用的时间和执行的指令数目,来决定下一阶段如何调度。
二、内存管理
2.1 基本概念
这里介绍的内存管理,是一个 “虚拟的地址空间”,程序中获取到的地址并非是物理上的内存地址,比如内存条,而是经过一层抽象,虚拟出来的地址。
举个例子来说:
内存(物理上的内存条)可以存储很多的数据。咱们可以把内存想象成一个很长的走廊,走廊上有很多的房间,每个房间1 byte大小,每个房间上还有编号,从0开始。
其中,
这个 编号 就是 “地址”。
这个 地址 也就是 “物理地址”。
内存有个很了不起的功能:随机访问。可以访问内存地址上的任意数据,速度都极快,时间上都差不多,正是这个特点,使得取数组下标元素是 O(1) 的。
2.2 内存管理的作用
2.2.1 内存管理问题
如下图所示,按照正常的内存地址分配,进程1分配的内存空间从0x111到0x1FF,进程2分配的内存空间从0x222到0x2FF。如果进程1 和 进程2 所执行的代码均没有出现任何 bug,这种情况下,一切顺利,不会出现什么问题。
但是,假如此时进程1中执行的程序是数组下标,数组下标如果越界了,可能会导致进程1的内存访问越界了,此时就会出现问题。
2.2.2 解决方案
根据上面出现的问题,我们对进程所使用的空间,进行 “隔离”,即引入了虚拟地址空间。在代码中不再使用真正的物理地址了,而是使用虚拟地址空间,由操作系统和专门的硬件设备进行 虚拟地址 到 真实物理地址 的转换。
一旦上图的进程1中代码出现 bug 了,内存访问越界了,指针地址变成了0x3000,此时操作系统内核发现进程1访问的内存范围超出了自身的范围了,就直接向进程反馈一个错误,具体来说,就是给进程1发送个 SINGSEGEMENT FAULT信号,引起进程1的崩溃,从而并不会影响进程2的正常执行。
2.2.3 新的问题【进程间通信】
上述将各个进程之间进行 隔离,虽然能很好的处理进程之间互不干扰的问题。但是,有些时候,进程之间确实需要进行数据交互。那么怎么办呢?
此时,计算机在各个进程之间开了个 “口子”,即 【进程间通信】。意思就是,搞了一个多个进程都能访问到的一个 公共空间,基于这个公共空间来进行各个进程之间的交互。这个公共空间可以是网络和文件。
三、线程
3.1 引入
前面我们介绍的进程相关的知识,其主要的目的是为了解决 “并发编程” 的问题。那么,为啥要并发编程呢?
那是因为,如今的时代,CPU 已经进入了多核心的时代,要想进一步提高程序的执行效率,就要充分利用 CPU 的多核资源。
当然,其实进程已经能够解决好并发编程的问题了,已经可以利用CPU多核资源了。我们知道,凡事会有更好,这里也是一样,虽然进程可以解决问题,但是,进程太重
了。
创建一个进程,开销比较大。
销毁一个进程,开销也比较大。
调度一个进程,开销还比较大。
所说的进程太重,体现在进程不仅消耗资源多,而且执行速度慢。
在此背景下,线程就脱颖而生了,也叫 轻量级线程。
3.2 简单认识线程
我们引入线程,是在进程能解决 并发编程 的基础上,还能够更好的处理进程所带来的的重,让创建、销毁、调度的速度更快。为啥线程更 轻? 主要就体现在线程把申请资源 和释放资源的操作省下了。
下面我画了两个图,来帮助我们更好理解 进程 和 线程之间的区别。
假如我开了工厂,要生产一些产品,没想到生意越来越火爆,我想赚更多的钱(哈哈哈,比较俗),所以,我就的准备扩大生产。下面是两种方案的扩大生产,正好对应的是
多进程版本 和 多线程版本。
1.多进程版本
2.多线程版本
上述两种方案,很明显第二种方案更优。成本相对于多进程版本更低,场地和物流体系都复用以前的。从这也可以体会出来,多线程为啥更轻量级了。
3.3 基本概念
1.一个线程也是通过一个 PCB 来描述的。
2.一个进程里面可能包含一个 PCB 和 多个 PCB。
3.之前介绍的pid、内存指针、上下文、记账信息,都是每个线程各自有的,都是自己记录自己的。
4.一个进程中的多个线程的pid、内存指针、文件描述符表都是一样的。
5.每个进程有多个线程,每个线程都是单独在CPU 上调度的。【线程是操作系统调度执行的基本单元】
3.4 线程和进程的关系
总的来说,就是进程包含线程。
一个进程可以包含一个线程,也可以包含多个线程(不能没有,必须包含一个)。
只有当第一个线程启动的时候,开销是比较大的,因为第一个线程跟随进程一起,后续的线程就方便了。
同一个进程的多个线程公用同一套资源。(具体指内存和文件)
3.5 多线程问题
3.5.1 多线程问题1
我们先来看一个有趣的例子。
我们现在有请滑稽老铁隆重登场,滑稽老铁最近想吃鸡了,它打算要吃100只鸡。也分别对应着 多进程版本的吃鸡 和 多线程版本的吃鸡。
多进程版本吃鸡
一个滑稽老铁很难吃完100只鸡,于是找来其他的滑稽老铁,分别在不同的房间各自吃鸡,互不打扰。
多线程吃鸡版本
还是多个滑稽老铁吃100只鸡,现在将多个滑稽老铁放到一间房间里。
小结:
根据前面我们知道,多线程版本消耗的成本确实比多进程要小,创建、销毁、调度的过程也比进程更快。
但是,当我们无脑增加线程的数量的时候,也不是可以一直提高速度的,因为 CPU 的核心数量是有限的。就好比上述多线程吃鸡一样,当滑稽老铁的数量足够多的时候,有些滑稽老铁就挤不上餐桌了,就无法正常吃鸡了。
3.5.2 多线程问题2
在多线程情况下,多个滑稽老铁去共享100只鸡,此时可能会出现滑稽老铁之间相互打架的问题。比如,滑稽1和滑稽2看上了同一个鸡腿,两个人都伸手去拿,此时就会打起来。
然而在多进程中,则不会出现这个问题。(多进程中,已经把鸡分好了,滑稽老铁各自吃自己的就行。)
此时就引入了 “线程安全问题”,导致线程不安全。
3.5.3 多线程问题3
多线程还会出现这样的情况。
比如,滑稽1想吃面前的大鸡腿,但是滑稽2把鸡腿抢走吃了,此时滑稽1生气了,表示不干了,他把桌子给掀了,我吃不了,大家都别吃。
如果一个线程抛异常,处理不好,那么可能会带走整个进程。
3.5.4 多线程安全小结
总的来说,就是啥时候会有线程安全问题?
多个执行流共同去访问同一个共享资源的时候
进程模型: 天然就是资源隔离的,进行进程间通信时,多个进程同时访问同一个资源,可能会出现问题。
线程模型: 天然就是资源共享的,多线程同时争抢一个资源(一个变量)的时候,非常容易触发安全问题。
四、总结
关于进程和线程的系列介绍1 暂时先讲到这,后续会接着介绍多线程的后续内容。小伙伴们如果觉得作者写的不错的,或者能够帮助到你的,希望你能够点下关注,小编也会继续努力!!!