linux的进程/线程/协程系列1:进程到协程的演化

5 篇文章 2 订阅
3 篇文章 7 订阅

前言

最近学习自动驾驶系统时,碰到协程的概念。进程和线程已经迷了,又来个协程,看了很多资料后决定作总结,概括三者联系和区别,最后归结到协程在自动驾驶中的应用。初级程序员目标是搞清三者概念并应用到实际中,而资深工程师则需要在系统层面考虑三者的性能及实现代价,直到如今三者仍是Linux内核和各类编程语言持续更新完善的模块之一,所以理清三者的关系、编程应用和考量性能是进阶程序员的必修课。行文的目的,是对进程/线程/协程这一系列繁复的概念和知识点做一个全面的总结,同时尽量做到知识点讲精讲细讲全,甄别模糊概念,同时兼顾源码及编程实现,最后归结到Apollo的协程实现。

本系列文章分九篇讲解:

  1. 《进程到协程的演化》:涉及进程发展的历史和计算机系统结构知识;
  2. 《进程/线程的系统命令》:总结进程/线程有关的系统命令,让大家有一个初步感性认识,而不只是生涩的文字;
  3. 《查看linux内核源码——vim+ctags/find+grep》:如何查看linux系统源码,源码第一手资料,重要性不言而喻;
  4. 《进程/线程相关知识总结》:进程/线程知识串讲,进程、线程和协程一脉相承,对进程理解透彻,线程和协程的难点也会迎刃而解。
  5. 《协程发展史、当前现状及libgo/tbox》:Conway Melvin如何总结出协同工作机制,引出当前协程库现状,分析它们的优劣势,并给出我的推荐:libgo,并分析其源码目录,最后提引性能神器tbox。
  6. 《全面弄懂进程/线程/协程的内存调度》:三者在内存中的调度,,带读者领略内存调度的魅力。
  7. libgo功能及源码详解》:分析libgo/tbox的原理和集成功能,给出源码简读和样例。
  8. 《进程/线程/协程的性质辨析和实现对比》:列表分析三者的性质,同时根据源码,挑重点总结实现区别。
  9. 《Apollo中的协程概述》:协程在Apollo中的应用,展示及分析Apollo协程的源码及优缺点。

摘要:

本文从批处理时代讲起,由批处理时代的问题引出进程,同时简述现代操作系统的启动,然后由进程问题依次引出线程、线程池、协程。 本篇主要是概念讲解,本来打算直接上命令代码,可仔细思考后,认为文字讲解对于理解进程到协程的来龙去脉还是必不可少的,急于操作的同学可以跳到第二篇。

1. 一些历史:批处理时代

一开始并没有进程的概念,计算机都是大型机,程序代码是机器码,直接通过穿孔把程序输入到纸带上面,根据纸带里的二进制数据进行逻辑运算(后来进化到电子管、晶体管和现在的集成电路),一个纸带输入完了,接着读取下一个纸带,只有等上一个处理运算结束之后才能排队到下一个。为了改进这种排队等候的低效率问题,就有人发明了批处理系统。批处理时代可以多个纸带一起提交,计算机会集中处理,或者多写几种可能,集中让计算机处理,最后选取一个较好的结果。

为了提升效率,机器码就被汇编语言替代了,从而再也不用一串串二进制数字来写代码了。但是问题也来了,当程序在运行的时候,会一直占用CPU,有可能某个时间在写磁盘数据、读取网络设备数据等,一直霸占着CPU会造成资源的浪费,这时候完全可以把CPU的计算资源让给其他程序,直到数据读写准备就绪后再切换回来。怎么控制这个过程以及管理多个程序间的计算机资源呢?由于程序并发执行具有间断性、失去封闭性和不可再现性,可能会造成执行结果的不可再现,所以用“程序”这个概念已无法描述程序的并发执行,所以必须引入新的概念——进程来描述程序的并发执行,并要对进程进行必要的管理,以保证进程在并发执行时结果可再现。

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体。而进程则不同,它是程序在某个数据集上的执行,是一个动态实体。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消,反映了一个程序在一定的数据集上 运行的全部动态过程。

在讲解进程之前,让我们先看一看现代操作系统的启动过程,看看pid为1的1号进程是怎么来的。

2. 现代操作系统启动过程

先思考一个经典问题:当按下电源键之后,计算机如何把自己由静止启动起来的?简述如下:

  1. 第一步:在主板接通电源之后,主板芯片组会向CPU发出reset的命令让CPU开始初始化,CPU会马上从地址FFFFF0H或FFFF0H开始执行寻址指令,直接跳转到系统BIOS中真正的启动代码处,系统BIOS的启动代码首先要做的事情就是POST(Power On Self Test,上电自检)自检,整个系统由BIOS控制,自检完成后系统BIOS会找到显卡BIOS之后调用它的代码,显卡BIOS的ROM地址通常在C0000H处,完成显卡初始化,通常会有显示界面。
  2. 第二步:根据BIOS设置的启动顺序和硬盘的主引导记录(Master boot record,缩写为MBR)进行硬盘引导启动,由于MBR的限制,每个硬盘只能有四个主分区且同时只有一个激活主分区,激活主分区通过引导卷启动(Volume boot record,缩写为VBR),告诉计算机这个分区里操作系统的位置,计算机就会加载操作系统了。
  3. 第三步:进行内核加载。控制权转交给操作系统后,操作系统的内核首先被载入内存。以Linux系统为例,先载入/boot目录下面的kernel。内核加载成功后,第一个运行的程序是/sbin/init。它根据配置文件(Ubuntu系统是/etc/init.d/rc#,Debian系统是/etc/inittab)产生init进程。这是Linux启动后的第一个进程,pid编号为1,其他进程都是它的后代。然后,init进程加载系统的各个模块,比如窗口程序和网络程序,直至执行/bin/login程序,跳出登录界面,等待用户输入用户名和密码。

至此,全部启动过程完成。

3. 进程(process)的出现

上面操作系统的启动过程可以描述为上帝创造万物的过程,第一个被创造出来的进程是0号进程,可以理解为BIOS程序,这个进程在操作系统层面是不可见的,但它存在着。0号进程完成了操作系统的功能加载与初期设定,然后它创造了1号进程(init),这个1号进程就是操作系统的“耶稣”。1号进程是上帝派来管理整个操作系统的,所以在用pstree查看进程树可知,1号进程位于树根。再之后,系统的很多管理程序都以进程身份被1号进程创造出来,还创造了与人类沟通的桥梁——shell。从那之后,人类可以跟操作系统进行交流,可以编写程序,可以执行任务等。而这一切,都是基于进程的。

进程(Process)是可并发执行的程序在一个数据集合上的运行过程。从结构上,进程实体由程序段、数据段和进程控制块三部分组成,UNIX中称为“进程映象”。进程具有动态性、并发性、独立性和异步性等。每一个任务(进程)被创建时,系统会为他分配存储空间等必要资源,然后在内核管理区为该进程创建管理节点,以便后来控制和调度该任务的执行。进程真正进入执行阶段之前,还需要获得CPU的使用权及其它资源,这一切都是操作系统掌管着,也就是所谓的调度。在各种条件满足的情况下,启动进程的执行过程。

对于操作系统而言,进程是核心之核心,它是内存中加载指令的最小单位。整个现代操作系统的根本,就是以进程为单位在执行任务,系统的管理架构也是基于进程层面的。有了上面的引入,我们可以对进程做一个简要的总结:进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。它的执行需要系统分配资源创建实体之后才能进行

4. 线程(thread)与线程池

即使划分了资源管理的最小单元,但是一个进程在运行的过程中,不可能一直占据着CPU进行逻辑运算,运行过程中很可能在进行磁盘I/O或者网络I/O,CPU资源还是有些浪费。另外,在执行一些细小任务时,虽然本身无需单独分配内存,但进程的实现机制依然会繁琐的将内存分割,这样既造成内存浪费又消耗时间。为了更加充分利用CPU运算资源,提高内存利用率,有人设计了线程的概念。线程最大的特点就是和创建它的进程共享地址空间,在不需要独立内存资源的情况下就可以运行,并且一个进程可以拥有多个线程,这样在某个线程IO时,无需切换内存,其他线程就可以抢占CPU进行运算。

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,降低通信开销,可以拥有自己的栈空间和独立的执行序列。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间,线程和进程一样,均由操作系统的调度器来统一调度。

下面对线程做一个总结:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源

另外,线程本身的数据结构需要占用内存,频繁创建和销毁线程会加大系统的压力,如果开辟太多线程,系统调度的开销会很大。线程池就是在这样的场景下提出的,线程池可以在初始化的时候批量创建线程,然后用户通过队列等方式提交业务,线程池中的线程进行业务的消费工作,线程池可以降低线程创建和销毁的开销,但是调度的开销还是存在的。

用户线程和内核支持线程的概念以及用户线程和内核线程池的对应关系将在后续讲解,这里主要了解各种技术出现的历史背景。

5. 协程(coroutine)时代

本质上,进程的出现,除了解决多任务的场景,也反映了当前硬件技术的瓶颈:单个CPU的计算能力不足,所以引入多核,用多进程机制与之配合,后来为降低调度量级而使用线程,为降低创建和销毁的开销而使用线程池,而协程作为更轻量级的线程,以语言内建机制的形式出现,它是对函数的扩展,可以让函数的执行在协程组件的帮助下,能够在特定位置主动的进行挂起和恢复。

因为线程操作存在诸多不便,比如线程切换的随机性和线程Context的跟随,出入栈的保存和恢复,相关数据的锁和读写控制,这才是多线程的复杂性,如果再加异步引起的数据的非连续性和事件的非必然性操作,就更加增强了多线程遇到问题的判别和断点的准确,而协程则规避了这些。另外,由于线程是操作系统的最小执行单元,因此也可以得出,协程是基于线程实现的,协程的创建、切换、销毁都是在某个线程中来进行的。

所谓协程(coroutine),就是协作式程序运行模式,它是一种轻量级的用户态线程,运行在线程之上,在线程的基础上通过分时复用的方式运行多个协程,实现的是一种非抢占式调度,协程执行完成后或在特定位置(基本就是阻塞操作),可以主动让出CPU(比如yield调用)。协程的运行和切换发生在用户态,系统是无感知的,所有也不存在用户态到内核态的切换,代价更低。协程不像进程或线程那样需要让系统负责相关的调度工作,它需要用户自己调用调度器。

6. 小结

个人认为,从无进程到提出进程是操作系统资源管理第一个重大质的飞跃;从进程到线程和线程池是第二大飞跃;从线程和线程池到协程是第三大质的飞跃。

多进程/多任务的出现是为了提升CPU的利用率,特别是I/O密集型运算,不管是多核还是单核,开多个进程必然能有效提升CPU的利用率。但进程间依然有资源利用优化空间,以及进程间通信的麻烦问题。多线程则可以共享同一进程地址空间上的资源,能在更好的利用空闲资源,且不存在进程间通信的麻烦。但线程的创建和销毁会造成资源的浪费。为了降低线程创建和销毁的开销,又出现了线程池的概念,在一开始就创建批量的线程。虽然减少了部分创建和销毁线程所消耗的资源,但调度的开销依然存在。为了提升用户线程的最大利用效率,又提出了协程的概念,可以充分提高单核的CPU利用率,降低调度的开销(协程因不受操作系统资源管理的自动调度,如果需要可以手工或写代码调度)。

为了先有一个直观的认识,而不是一味的陈列文字,下一章讲解进程/线程相关的系统命令。由于协程一般单独实现,与系统无关,故不涉及进程。


参考文献

  1. 计算机启动过程详解
  2. ubuntu 下为何没有/etc/inittab文件 linux下程序的启动流程
  3. Unix / Linux 线程的实质
  4. Linux进程与线程的区别
  5. linux进程、线程、线程池和协程的由来
  6. C/C++知识总结:《进程、线程、协程》编程三兄弟的那些事~
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值