栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

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

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

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

前言摘要:1. 一些历史:批处理时代2. 现代操作系统启动过程3. 进程(process)的出现4. 线程(thread)与线程池5. 协程(coroutine)时代6. 小结参考文献

前言

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

本打算行文尽量简洁,但达不到讲精讲细的目的,所以我对本系列文章的定位是复杂知识点详细总结,在此基础上做到尽量简练。由于查阅了大量资料,耗费了很多精力,虽然谈不上尽善尽美,但也希望您的小手能支持作者一下,来个一键三联,因为作者后续的文章一定可以帮助不停探索的你。

本系列文章分七篇讲解:第一篇:进程到协程的演化,涉及程序历史的进展和计算机系统结构知识。第二篇:总结进程/线程有关的系统命令,让大家开篇时就可以对它们有一个感性的认识,而不只是生涩的文字。第三篇:如何查看linux系统源码,第一手知识是立足的根本,能接触第一手资料才有可能成为行业大咖,源码无疑是最重要的第一手资料,什么你还不会看源码,别急我在这里教会你。第四篇:进程知识串讲,由于进程、线程和协程一脉相承,所以进程大部分知识同样适用于线程和协程,对进程理解透彻,线程和协程的难点也会迎刃而解。第五篇:三者的性质辨析和实现区别,当前大部分文章开篇就在概念上大讲特讲三者的性质对比,而概念很容易让人迷糊,所以作者采用娓娓道来的模式,有了前面的铺垫,相信你再看到性质对比时,会有心领神会的感觉。性质辨析后,还会在三者的实现代码上,挑重点做一个简单的对比。第六篇:三者在内存中的调度,内存调度更能反应程序执行的过程,这里深入进程内存调度置换,带读者领略内存调度的魅力。第七篇:当前各式协程的对比总结,从boost库开始,到以boost库为基础实现的各种协程库libgo/ntyco/libco等,以及当前Python/Java/Go对协程的实现;第八篇:libgo的分析及实现,当前libgo的文章大部分由原作者写作,讲了很多概念,让人觉得这个库确实是好东东,然而实例实现较少,本篇在分析libgo的原理时,会呈现一些实例,帮助大家应用;第九篇:协程在Apollo中的应用,分析Apollo协程的源码及优缺点。


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

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

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

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

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

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

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

    第一步:在主板接通电源之后,主板芯片组会向CPU发出reset的命令让CPU开始初始化,CPU会马上从地址FFFFF0H或FFFF0H开始执行寻址指令,直接跳转到系统BIOS中真正的启动代码处,系统BIOS的启动代码首先要做的事情就是POST(Power On Self Test,上电自检)自检,整个系统由BIOS控制,自检完成后系统BIOS会找到显卡BIOS之后调用它的代码,显卡BIOS的ROM地址通常在C0000H处,完成显卡初始化,通常会有显示界面。第二步:根据BIOS设置的启动顺序和硬盘的主引导记录(Master boot record,缩写为MBR)进行硬盘引导启动,由于MBR的限制,每个硬盘只能有四个主分区且同时只有一个激活主分区,激活主分区通过引导卷启动(Volume boot record,缩写为VBR),告诉计算机这个分区里操作系统的位置,计算机就会加载操作系统了。第三步:进行内核加载。控制权转交给操作系统后,操作系统的内核首先被载入内存。以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的计算能力不足,所以引入多核,用多进程机制与之配合,为降低调度量级而使用线程,为降低创建和销毁的开销而使用线程池,而协程作为更轻量级的线程,以语言内建机制的形式出现,它是对函数的扩展,可以让函数的执行在协程组件的帮助下,能够在特定位置,主动的进行挂起和恢复。

作为推论:在单个线程中执行的协程,可以视为单线程应用。这些协程,在未执行到特定位置(基本就是阻塞操作)前,是不会被抢占,也不会和其他CPU上的上下文发生同步问题的。因此,一段协程代码,中间没有可能导致阻塞的调用,执行在单个线程中,那么这段内容可以被视为同步的,这时共享数据自动具备了“原子”操作,不需要“锁”机制。但协程本质上是并发程序,在某些应用中,尤其是Golang这种实现了M:N模式的协程调度器,锁甚至是必须的。另外,协程和线程一样,同样需要做好两个重点:第一个是协程的调度;第二是上下文的切换。

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

6. 小结

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

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

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


参考文献
    计算机启动过程详解ubuntu 下为何没有/etc/inittab文件 linux下程序的启动流程Unix / Linux 线程的实质Linux进程与线程的区别linux进程、线程、线程池和协程的由来C/C++知识总结:《进程、线程、协程》编程三兄弟的那些事~
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/725277.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号