goroutine背后的系统知识.doc

上传人:自*** 文档编号:126905941 上传时间:2020-03-28 格式:DOC 页数:13 大小:92.51KB
返回 下载 相关 举报
goroutine背后的系统知识.doc_第1页
第1页 / 共13页
goroutine背后的系统知识.doc_第2页
第2页 / 共13页
goroutine背后的系统知识.doc_第3页
第3页 / 共13页
goroutine背后的系统知识.doc_第4页
第4页 / 共13页
goroutine背后的系统知识.doc_第5页
第5页 / 共13页
点击查看更多>>
资源描述

《goroutine背后的系统知识.doc》由会员分享,可在线阅读,更多相关《goroutine背后的系统知识.doc(13页珍藏版)》请在金锄头文库上搜索。

1、goroutine背后的系统知识Posted on 2013/03/04Go语言从诞生到普及已经三年了,先行者大都是Web开发的背景,也有了一些普及型的书籍,可系统开发背景的人在学习这些书籍的时候,总有语焉不详的感觉,网上也有若干流传甚广的文章,可其中或多或少总有些与事实不符的技术描述。希望这篇文章能为比较缺少系统编程背景的Web开发人员介绍一下goroutine背后的系统知识。1. 操作系统与运行库2. 并发与并行 (Concurrency and Parallelism)3. 线程的调度4. 并发编程框架5. goroutine1. 操作系统与运行库对于普通的电脑用户来说,能理解应用程序是

2、运行在操作系统之上就足够了,可对于开发者,我们还需要了解我们写的程序是如何在操作系统之上运行起来的,操作系统如何为应用程序提供服务,这样我们才能分清楚哪些服务是操作系统提供的,而哪些服务是由我们所使用的语言的运行库提供的。除了内存管理、文件管理、进程管理、外设管理等等内部模块以外,操作系统还提供了许多外部接口供应用程序使用,这些接口就是所谓的“系统调用”。从DOS时代开始,系统调用就是通过软中断的形式来提供,也就是著名的INT 21,程序把需要调用的功能编号放入AH寄存器,把参数放入其他指定的寄存器,然后调用INT 21,中断返回后,程序从指定的寄存器(通常是AL)里取得返回值。这样的做法一直

3、到奔腾2也就是P6出来之前都没有变,譬如windows通过INT 2E提供系统调用,Linux则是INT 80,只不过后来的寄存器比以前大一些,而且可能再多一层跳转表查询。后来,Intel和AMD分别提供了效率更高的SYSENTER/SYSEXIT和SYSCALL/SYSRET指令来代替之前的中断方式,略过了耗时的特权级别检查以及寄存器压栈出栈的操作,直接完成从RING 3代码段到RING 0代码段的转换。系统调用都提供什么功能呢?用操作系统的名字加上对应的中断编号到谷歌上一查就可以得到完整的列表 (Windows, Linux),这个列表就是操作系统和应用程序之间沟通的协议,如果需要超出此协

4、议的功能,我们就只能在自己的代码里去实现,譬如,对于内存管理,操作系统只提供进程级别的内存段的管理,譬如Windows的virtualmemory系列,或是Linux的brk,操作系统不会去在乎应用程序如何为新建对象分配内存,或是如何做垃圾回收,这些都需要应用程序自己去实现。如果超出此协议的功能无法自己实现,那我们就说该操作系统不支持该功能,举个例子,Linux在2.6之前是不支持多线程的,无论如何在程序里模拟,我们都无法做出多个可以同时运行的并符合POSIX 1003.1c语义标准的调度单元。可是,我们写程序并不需要去调用中断或是SYSCALL指令,这是因为操作系统提供了一层封装,在Wind

5、ows上,它是NTDLL.DLL,也就是常说的Native API,我们不但不需要去直接调用INT 2E或SYSCALL,准确的说,我们不能直接去调用INT 2E或SYSCALL,因为Windows并没有公开其调用规范,直接使用INT 2E或SYSCALL无法保证未来的兼容性。在Linux上则没有这个问题,系统调用的列表都是公开的,而且Linus非常看重兼容性,不会去做任何更改,glibc里甚至专门提供了syscall(2)来方便用户直接用编号调用,不过,为了解决glibc和内核之间不同版本兼容性带来的麻烦,以及为了提高某些调用的效率(譬如_NR_ gettimeofday),Linux上还是

6、对部分系统调用做了一层封装,就是VDSO (早期叫linux-gate.so)。可是,我们写程序也很少直接调用NTDLL或者VDSO,而是通过更上一层的封装,这一层处理了参数准备和返回值格式转换、以及出错处理和错误代码转换,这就是我们所使用语言的运行库,对于C语言,Linux上是glibc,Windows上是kernel32(或调用msvcrt),对于其他语言,譬如Java,则是JRE,这些“其他语言”的运行库通常最终还是调用glibc或kernel32。“运行库”这个词其实不止包括用于和编译后的目标执行程序进行链接的库文件,也包括了脚本语言或字节码解释型语言的运行环境,譬如Python,C#

7、的CLR,Java的JRE。对系统调用的封装只是运行库的很小一部分功能,运行库通常还提供了诸如字符串处理、数学计算、常用数据结构容器等等不需要操作系统支持的功能,同时,运行库也会对操作系统支持的功能提供更易用更高级的封装,譬如带缓存和格式的IO、线程池。所以,在我们说“某某语言新增了某某功能”的时候,通常是这么几种可能:1. 支持新的语义或语法,从而便于我们描述和解决问题。譬如Java的泛型、Annotation、lambda表达式。2. 提供了新的工具或类库,减少了我们开发的代码量。譬如Python 2.7的argparse3. 对系统调用有了更良好更全面的封装,使我们可以做到以前在这个语言

8、环境里做不到或很难做到的事情。譬如Java NIO但任何一门语言,包括其运行库和运行环境,都不可能创造出操作系统不支持的功能,Go语言也是这样,不管它的特性描述看起来多么炫丽,那必然都是其他语言也可以做到的,只不过Go提供了更方便更清晰的语义和支持,提高了开发的效率。2. 并发与并行 (Concurrency and Parallelism)并发是指程序的逻辑结构。非并发的程序就是一根竹竿捅到底,只有一个逻辑控制流,也就是顺序执行的(Sequential)程序,在任何时刻,程序只会处在这个逻辑控制流的某个位置。而如果某个程序有多个独立的逻辑控制流,也就是可以同时处理(deal)多件事情,我们就

9、说这个程序是并发的。这里的“同时”,并不一定要是真正在时钟的某一时刻(那是运行状态而不是逻辑结构),而是指:如果把这些逻辑控制流画成时序流程图,它们在时间线上是可以重叠的。并行是指程序的运行状态。如果一个程序在某一时刻被多个CPU流水线同时进行处理,那么我们就说这个程序是以并行的形式在运行。(严格意义上讲,我们不能说某程序是“并行”的,因为“并行”不是描述程序本身,而是描述程序的运行状态,但这篇小文里就不那么咬文嚼字,以下说到“并行”的时候,就是指代“以并行的形式运行”)显然,并行一定是需要硬件支持的。而且不难理解:1. 并发是并行的必要条件,如果一个程序本身就不是并发的,也就是只有一个逻辑控

10、制流,那么我们不可能让其被并行处理。2. 并发不是并行的充分条件,一个并发的程序,如果只被一个CPU流水线进行处理(通过分时),那么它就不是并行的。3. 并发只是更符合现实问题本质的表达方式,并发的最初目的是简化代码逻辑,而不是使程序运行的更快;这几段略微抽象,我们可以用一个最简单的例子来把这些概念实例化:用C语言写一个最简单的HelloWorld,它就是非并发的,如果我们建立多个线程,每个线程里打印一个HelloWorld,那么这个程序就是并发的,如果这个程序运行在老式的单核CPU上,那么这个并发程序还不是并行的,如果我们用多核多CPU且支持多任务的操作系统来运行它,那么这个并发程序就是并行

11、的。还有一个略微复杂的例子,更能说明并发不一定可以并行,而且并发不是为了效率,就是Go语言例子里计算素数的sieve.go。我们从小到大针对每一个因子启动一个代码片段,如果当前验证的数能被当前因子除尽,则该数不是素数,如果不能,则把该数发送给下一个因子的代码片段,直到最后一个因子也无法除尽,则该数为素数,我们再启动一个它的代码片段,用于验证更大的数字。这是符合我们计算素数的逻辑的,而且每个因子的代码处理片段都是相同的,所以程序非常的简洁,但它无法被并行,因为每个片段都依赖于前一个片段的处理结果和输出。并发可以通过以下方式做到:1. 显式地定义并触发多个代码片段,也就是逻辑控制流,由应用程序或操

12、作系统对它们进行调度。它们可以是独立无关的,也可以是相互依赖需要交互的,譬如上面提到的素数计算,其实它也是个经典的生产者和消费者的问题:两个逻辑控制流A和B,A产生输出,当有了输出后,B取得A的输出进行处理。线程只是实现并发的其中一个手段,除此之外,运行库或是应用程序本身也有多种手段来实现并发,这是下节的主要内容。2. 隐式地放置多个代码片段,在系统事件发生时触发执行相应的代码片段,也就是事件驱动的方式,譬如某个端口或管道接收到了数据(多路IO的情况下),再譬如进程接收到了某个信号(signal)。并行可以在四个层面上做到:1. 多台机器。自然我们就有了多个CPU流水线,譬如Hadoop集群里

13、的MapReduce任务。2. 多CPU。不管是真的多颗CPU还是多核还是超线程,总之我们有了多个CPU流水线。3. 单CPU核里的ILP(Instruction-level parallelism),指令级并行。通过复杂的制造工艺和对指令的解析以及分支预测和乱序执行,现在的CPU可以在单个时钟周期内执行多条指令,从而,即使是非并发的程序,也可能是以并行的形式执行。4. 单指令多数据(Single instruction, multiple data. SIMD),为了多媒体数据的处理,现在的CPU的指令集支持单条指令对多条数据进行操作。其中,1牵涉到分布式处理,包括数据的分布和任务的同步等等

14、,而且是基于网络的。3和4通常是编译器和CPU的开发人员需要考虑的。这里我们说的并行主要针对第2种:单台机器内的多核CPU并行。关于并发与并行的问题,Go语言的作者Rob Pike专门就此写过一个幻灯片:http:/talks.golang.org/2012/waza.slide在CMU那本著名的Computer Systems: A Programmers Perspective里的这张图也非常直观清晰:3. 线程的调度上一节主要说的是并发和并行的概念,而线程是最直观的并发的实现,这一节我们主要说操作系统如何让多个线程并发的执行,当然在多CPU的时候,也就是并行的执行。我们不讨论进程,进程的

15、意义是“隔离的执行环境”,而不是“单独的执行序列”。我们首先需要理解IA-32 CPU的指令控制方式,这样才能理解如何在多个指令序列(也就是逻辑控制流)之间进行切换。CPU通过CS:EIP寄存器的值确定下一条指令的位置,但是CPU并不允许直接使用MOV指令来更改EIP的值,必须通过JMP系列指令、CALL/RET指令、或INT中断指令来实现代码的跳转;在指令序列间切换的时候,除了更改EIP之外,我们还要保证代码可能会使用到的各个寄存器的值,尤其是栈指针SS:ESP,以及EFLAGS标志位等,都能够恢复到目标指令序列上次执行到这个位置时候的状态。线程是操作系统对外提供的服务,应用程序可以通过系统

16、调用让操作系统启动线程,并负责随后的线程调度和切换。我们先考虑单颗单核CPU,操作系统内核与应用程序其实是也是在共享同一个CPU,当EIP在应用程序代码段的时候,内核并没有控制权,内核并不是一个进程或线程,内核只是以实模式运行的,代码段权限为RING 0的内存中的程序,只有当产生中断或是应用程序呼叫系统调用的时候,控制权才转移到内核,在内核里,所有代码都在同一个地址空间,为了给不同的线程提供服务,内核会为每一个线程建立一个内核堆栈,这是线程切换的关键。通常,内核会在时钟中断里或系统调用返回前(考虑到性能,通常是在不频繁发生的系统调用返回前),对整个系统的线程进行调度,计算当前线程的剩余时间片,如果需要切换,就在“可运行”的线程队列里计算优先级,选出目标线程后,则保存当前线程的运行环境,并恢复目标线程的运行环境,其中最重要的,就是切换堆栈指针ESP,然后再把EIP指向目标线程上次被移出CPU时的指令。

展开阅读全文
相关资源
相关搜索

当前位置:首页 > IT计算机/网络 > 计算机应用/办公自动化

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号