文章标题

来源:互联网 发布:花生壳 该域名被锁定 编辑:程序博客网 时间:2024/06/06 12:56

1.1 深入思考“hello world”

#include<iostream>int main(){    printf("hello world\n");    return 0;}

问题1:程序为什么要被编译后才可以运行?
问题2:编译器把c语言转换为可执行的机器码过程中做了什么,怎么做的?
问题3:可执行文件里面是什么?除了机器码还有什么?怎么存放的,怎么组织的?
问题4:把stdio.h包含进来意味着什么?c语言库是什么,怎么实现的?
问题5:不同编译器,不同硬件平台,不同操作系统最终编译出来的结果一样?为什么?
问题6:hello world程序怎么运行起来的?操作系统怎么装载它的?它从哪开始执行,到哪结束?main函数之前发生了什么?main函数结束以后又发生了什么?
问题7:如果没有操作系统,hello world可以运行吗?如果要在一台没有操作系统的机器上运行hello world需要什么?应该怎么实现?
问题8:printf怎么实现的?为什么可以有不定数量的参数?为什么可以在终端输出字符串?
问题9:hello world程序运行时,它在内存中是什么样的?
这本书就是为这些问题准备的。随着各章节的逐步展开,我们会从最基本的编译、静态链接到操作系统如何装载程序、动态链接及运行库和标准库的实现,甚至一些操作系统的机制,力争深入浅出地将这些问题层层剥开,最终使得这些程序运行背后的机制形成一个非常清晰而流畅的脉络。
在开始进入庞大而繁琐的系统软件之前,先回顾计算机系统的基本又重要的概念。回顾过程分为两部分:硬件部分和软件部分。第一章回顾只是巩固和总结计算机软硬件体系里面几个重要概念,这些概念在后面的章节会伴随着我们,失去它们的支撑,后面章节将会显得繁琐又晦涩。
1.2 万变不离其宗
我们将计算机限定在x86指令集的32位cpu的个人计算机。虽然各种平台的软硬件差别很多,但其本质上的基本概念和工作原理都是一样的。
站在软件开发者角度,我们只须抓住硬件的几个关键部件。对于系统程序开发者来说,分别是cpu、内存和I/O控制芯片,而普通程序开发者似乎只关心cpu,对于高级平台开发者(java、.net或脚本语言),连cpu都不关心,因为这些平台为他们提供了一个通用的抽象的计算机,他们只要关心这个抽象计算机就可以了。
早期cpu和内存频率一样都连在总线(Bus)上。I/O设备慢很多,为了能够让他们之间的通信以及协调I/O设备与总线之间的速度,一般每个设备都有一个相应的I/O控制器。
这里写图片描述
后来cpu频率提升,于是产生了与内存频率一致的系统总线,而cpu采用倍频的方式与系统总线通信。随着图形化操作系统普及特别是3D游戏和多媒体的发展,慢速的I/O总线无法满足图形设备的巨大需求,为了协调cpu、内存和高速的图形设备,专门设计了一个高速的北桥芯片,以便他们之间能够高速地交换数据。
由于北桥速度非常高,为了避免北桥设计过于复杂,人名设计了专门处理低速设备的南桥芯片,低速设备连接南桥上,由南桥将它们汇总后连接到北桥上。20世纪90年代pc机在系统总线上采用的是pci结构,而在低速设备上采用的是ISA总线,采用PCI/ISA及南北桥设计的硬件结构如图1-2所示。
位于中间是连接所有高速芯片的北桥(Northbridge,PCI Bridge),它就像人的心脏,连接并驱动身体的各个部位:它的左边是CPU,负责所有的控制和运算,就像人的大脑。北桥还连接着几个高速不见,包括左边的内存和下面的PCI总线。
这里写图片描述
PCI速度最高为133MHz,还是不能满足人们需求,于是发明了AGP、PCI Express等诸多总线结构和相应的控制芯片。虽然硬件结构看似越来越复杂,实际上它还是没有脱离最初的cpu、内存以及I/O的基本结构(##只是解决了之间的频率不平衡)。我们从程序开发的角度看待硬件时,可以简单地把它看成是最初的硬件模型。
SMP与内核
04年以来18个月频率翻倍的规律失效,因为制造cpu的工艺方面已经达到了物理极限,除非cpu制造工艺有本质的突破,否则CPU的频率就会一直被目前的4GHz的“天花板”所限制。
于是人们开始增加CPU的数量。最常见的是对称多处理器(SMP,Symmetrical Multi-Processing),简单地讲就是CPU在系统中的地位和功能是一样的。但实际上当问题不能被分解为完全不相干的子问题时,不能提高运算速度。
多处理器应用最多的是商用的服务器和需要处理大量计算的环境。而在个人电脑比较奢侈,成本很高。厂商开始考虑将多个处理器“合并在一起打包出售”,这些“被打包”的处理器之间共享比较昂贵的缓存部件,只保留多个核心,并且以一个处理器的外包装进行出售,这就是多核处理器(Multi-core Processor)的基本想法。其实是SMP的简化版,程序员的角度来看,它们之间的区别很小,逻辑上来看它们完全相同。只是多核和SMP在缓存共享等方面有细微差别,使得程序在优化上可以有针对性地处理。简单地讲,除非CPU的每一滴油水都榨干,否则可以把多核和SMP看成同一个概念。
1.3站得高,望得远
传统意义上将用于管理计算机本身的软件称为系统软件。系统软件分为两块:一块是平台性的,比如操作系统内核、驱动程序、运行库和数以千计的系统工具;另一块是用于程序开发的,比如编译器、汇编器、链接器等开发工具和开发库。本书将着重介绍系统软件的一部分,主要是链接器和库(包括运行库和开发库)的相关内容。
计算机系统软件体系结构采用一种层的结构,有人说过一句名言:
“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
“Any problem in computer science can be solved by another layer of indirection.”
不仅是计算机系统软件整个体系是这样的,体系里面的每个组件比如操作系统本身,很多应用程序、软件系统甚至很多硬件结构都是按照这种层次的结构组织和设计的。系统软件体系结构中,各种软件的位置如图1-3所示:
这里写图片描述
每个层次之间都需要相互通信,就必须有一个通信协议,我们一般将其称为接口(Interface),接口的下面那层是接口的提供者,由它定义接口;接口的上面那层是接口的使用者,它使用该接口来实现所需要的功能。在层次体系中,接口是被精心设计过的,尽量保持稳定不变,那么理论上层次之间只要遵循这个接口,任何一个层都可以被修改或替换。除了硬件和应用程序,其他都是所谓的中间层,每个中间层都是对它下面的那层的包装和扩展。正是这些中间层的存在,使得应用程序和硬件之间保持相对独立,比如硬件和操作系统都日新月异的发展,但最初为80386芯片和dos系统设计的软件在最新的多喝处理器和Windows Vista下还是能够运行的,这方面归功于硬件和操作系统本身保持了向后兼容性,另一方面不得不归功于这种层次结构的设计方式。最近开始流行的虚拟机技术更是在硬件和操作系统之间增加了一层虚拟层,使得一个计算机可以同时运行多个操作系统,这也是层次结构带来的好处,在尽可能少的改变甚至不改变其他层的情况下,新增加一个层次就可以提供前所未有的功能。
开发工具和应用程序属于同一个层次,因为他们都使用一个接口,那就是操作系统应用程序编程接口(Application Programming Interface)。应用程序接口的提供者是运行库,什么样的运行库提供什么样的API,比如Linux下的Glibc库提供POSIX的API;Windows的运行库提供Windows API,最常见的32Windows提供的API又被称为Win32。
运行库使用操作系统提供的系统调用接口(system call interface),系统调用接口在实现中往往以软件中断(software interrupt)的方式提供,比如Linux使用0x80号中断作为系统调用接口,Windows使用0x2e号中断作为系统调用接口(从Windows xp sp2开始,windows开始采用一种新的系统调用方式)。
操作系统内核层对于硬件层来说是硬件接口的使用者,而硬件是接口的定义者,硬件接口定义决定了操作系统内核,具体来讲就是驱动程序如何操作硬件,如何与硬件进行通信。这种接口往往被叫做硬件规格(Hardware Specification),硬件的生产厂商负责提供硬件规格,操作系统和驱动层序的开发者通过阅读硬件规格文档所规定的各种硬件编程接口标准来编写操作系统和驱动程序。
1.4操作系统做什么
操作系统的一个功能是提供抽象的接口,另一个主要功能是管理硬件资源。
计算机硬件的能力是有限的,比如一个CPU一秒钟能够执行的指令条数是1亿条或是1GB的内存能够最多同时存储1GB的数据。一个计算机中主要分CPU、存储器和I/O设备,我们分三个方面来看看如何挖掘它们的潜力。
1.4.1 不要让CPU打盹
原始的多道程序:当一个程序无须使用CPU时,监控程序就把另外正在等待CPU资源的程序启动。
分时系统:每个程序运行一段时间后主动让出CPU给其他程序。一个程序进入死循环,那么整个系统都停止了。
多任务系统:操作系统接管所有的硬件资源,并本身运行在一个受硬件保护的级别。所有应用程序都以进程的方式运行在比操作系统权限更低的级别,每个进程都有自己独立的地址空间,使得进程之间的地址空间相互隔离。CPU由操作系统根据进程优先级统一进行分配,如果运行时间超过一定时间,操作系统就会暂停该进程,将CPU资源分配给其他等待运行的进程。这种CPU分配方式即所谓的抢占式(Preemptive),操作系统可以强制剥夺CPU资源并且分配给它认为目前最需要的进程。如果分配给每个进程的时间很短。
1.4.2 设备驱动
对于操作系统上面的运行库应用程序来说,它们希望看到的是一个统一的硬件访问模式。作为应用程序的开发者,我们不希望在开发应用程序的时候直接读写硬件端口、处理硬件中断这些繁琐的事情。由于硬件之间千差万别,它们的操作方式都有区别。比如我们希望在显示器上画一条直线,对于程序员来说,最好的方式是不管计算机使用什么显卡、什么显示器,多少大小多少分辨率,我们都只要调用一个统一的LineTo()函数,具体的实现方式由操作系统来完成。
程序员逐渐从繁琐的硬件细节解放出来,这些交由操作系统,具体地讲是操作系统中的硬件驱动程序来完成。驱动程序往往和操作系统内核一起运行在特权级,但它又与操作系统内核之间有一定的独立性,使得驱动程序有较好的灵活性。操作系统开发者为硬件生产厂商提供了一系列接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用。以读取文件为例来看看操作系统和驱动程序在这个过程扮演了什么样的角色:
文件系统管理着磁盘中文件的存储方式,比如我们在Linux系统下有一个文件“/home/user/test.dat”,长度为8000个字节。那么我们在创建这个文件的时候,Linux的ext3文件系统有可能将这个文件按照这样的方式存储在磁盘中:文件的前4096字节存储在磁盘的1000号扇区到1007号扇区,每个扇区512字节,8个扇区刚好4096个字节;文件的第4097个字节到第8000个字节共3904个字节,存储在磁盘的2000号扇区到2007号扇区,8个扇区也是4096个字节,只不过只存储了3904个有效的字节,剩下的192个字节无效。如果把这个文件的存储方式看作是一个链状结构,它的结构如图所示:
这里写图片描述
穿插一个硬盘的结构介绍:
硬盘基本存储单位为扇区(sector),每个扇区一般为512字节。一个硬盘往往有多个盘片,每个盘片分两面,每面按照同心圆划分为若干磁道,每个磁道划分为若干个扇区。比如一个磁盘有两个盘片,每个盘面分65536磁道,每个磁道分1024个扇区,那么磁盘的容量是2*2*65536*1024*512=128GB。如果每个磁道拥有相同数量的扇区,外围稀疏浪费空间。但是如果扇区数不同,计算麻烦。为了屏蔽复杂硬件细节,现代硬盘普遍使用一种叫做LBA的方式,即整个硬盘所有的扇区从0开始编号,一直到最后一个扇区。当我们给出一个逻辑的扇区号时,磁盘的电子设备会将其转换为实际的盘面、磁道等这些位置。
文件系统保存了这些文件的存储结构,负责维护这些数据结构并能保证磁盘中的扇区能有效地组织和利用。当我们读取这个文件的前4096个字节时,我们会使用一个read的系统调用来实现。文件系统收到read请求后,判断出文件的前4096个字节位于磁盘的1000号扇区到1007号扇区。然后文件系统就向硬盘驱动发出一个读取逻辑扇区为1000号开始的8个扇区的请求,磁盘驱动程序收到这个请求后就向硬盘发出硬件命令。向硬件发送I/O命令的方式有很多种,其中最为常见的一种就是通过读写I/O端口寄存器来实现。在x86平台上,共有65536个硬件端口寄存器,不同的硬件被分配到了不同的I/O端口地址。CPU提供了两条专门的指令“in”和“out”来实现对硬件端口的读和写。
对IDE接口来说,它有IDE0和IDE1两个通道,每个通道上可以连接两个设备,分别为master和stave,一个pc中最多可以有4个IDE设备。假设我们的文件位于IDE0的master硬盘上。在pc中,IDE0通道的I/O端口地址是0x1F0~0x1F7及0x376~0x377。通过读写这些端口地址就能与IDE硬盘进行通信。这些端口的作用和操作方式十分复杂,我们以实现读取1000号逻辑扇区开始8个扇区为例:
1.第0x1F3~0x1F6 4个字节的端口地址是用来写入LBA地址的,那么1000号逻辑扇区的LBA地址为0x000003E8,所以我们需要往0x1F3、0x1F4写入0x00,往0x1F5写入0x03,往0x1f6写入0xe8.
2.0x1f2这个地址用来写入命令所需要读写的扇区数,比如读取8个扇区即写入8.
3.0x1f7这个地址用来写入要执行的操作的命令码,对于读取操作来说,命令字为0x20。
所以我们要执行的指令为:
out 0x1f3, 0x00
out 0x1f4, 0x00

0ut 0x1f7, 0x20
在硬盘收到这个命令后,它就会执行相应的操作,并将数据读取到事先设置好的内存地址中(这个内存地址也是通过类似的命令方式设置的)。当然这里的例子中只是最简单的情况,实际情况复杂得多,驱动程序要考虑硬件的状态(是否忙碌或读取错误)、调度和分配各个请求以达到最高的性能。
1.5内存不够怎么办
进程的总体目标是希望每个进程从逻辑上来看都可以独占计算机的资源。操作系统的多任务功能使得CPU能够在多个进程之间很好地共享,从进程的角度看好像是它独占了CPU而不用考虑与其他进程分享CPU的事情。操作系统的I/O抽象模型也很好地实现了I/O设备的共享和抽象,那么唯一剩下的就是内存的分配问题了。
如何将计算机上有限的物理内存分配给多个程序使用。
假设内存128MB,A需要10MB,B需要100MB,C需要20MB。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存前10MB分配给A,10~110MB分配给B。这样就能够实现A和B两个程序间同时运行,但这种简单的内存分配策略问题很多:
1.地址空间不隔离:臭虫程序
2.内存使用效率低:忽然执行C则需要把B换到硬盘再执行C,大量数据换入换出,效率低下。
3.程序运行的地址不稳定:分配空闲区域的位子不确定。然而程序在编写时,它访问数据和指令跳转时的目标地址很多都是固定的,这涉及程序的重定位问题。
解决办法:增加中间层,即使用一种间接的地址访问方法。我们把程序给出的地址看作是一种虚拟地址,然后通过某些映射的方法,将这个虚拟地址转换到实际的物理地址。这样只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序互不重叠,以达到地址空间隔离的效果。
1.5.1关于隔离
普通程序需要的是一个简单的执行环节,有一个单一的地址空间、有自己的CPU,好像整个程序占有整个计算机而不用关心其他程序(通信部分除外,这是程序主动要求的)。所谓地址空间是个比较抽象的概念,你可以把它想象成一个很大的数组,每个数组的元素是一个字节,而这个数组大小由地址空间的地址长度决定,比如32位的地址空间的大小为4GB,地址空间的有效地址是0x00000000~0xffffffff。地址空间分为两种,虚拟地址空间和物理地址空间。物理地址空间是实实在在的,存在于计算机中,而且对于每一台计算机来说只有唯一的一个,你可以把物理地址空间想象成物理内存,比如你的计算机用的是32位的机器,那么物理空间就是4GB。但是你的计算机上只装了512MB的内存,那么其实物理地址的真正有效部分只有0x00000000~0x1fffffff,其他部分是无效的(I/O设备占用)。虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就有效地做到了进程的隔离。
1.5.2 分段
把虚拟空间映射到地址空间。比如A需要10MB内存,那么我们假设有一个地址从0x00000000到0x00a00000的10MB大小的一个假想空间,也就是虚拟空间,然后我们从实际物理内存中分配一个相同大小的物理地址,假设物理地址是0x00100000开始到0x00b00000结束的一块空间。然后我们把他们一一映射,映射过程由软件设置,比如操作系统来设置这个映射函数,实际的地址转换由硬件完成。比如程序A中访问地址0x00001000时,CPU会将这个地址转换为实际的物理地址0x00101000。那么比如A和B在运行时,他们的虚拟空间和物理空间映射关系可能如图所示:
这里写图片描述
这基本解决了地址隔离和分配地址不稳定的问题。如果程序A访问虚拟地址超出了0x00A00000这个范围,那么硬件就会判断这是一个非法的访问,拒绝这个地址请求,并将这个请求报告给操作系统或监控程序,由它来决定如何处理。再者,对于每个程序来说,无论它们被分配到物理地址的哪一个区域,对于程序来说都是透明的,它们只需要按照从地址0x00000000到0x00A00000来编写程序、放置变量,所以程序不再需要重定位。
但是没有解决效率问题,如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上,根据程序的局部性原理,当一个层序运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的。人们很自然地想到了更小的粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是分页。
1.5.3分页(Paging)
分页的基本方法是把地址空间认为地等分成固定大小的页,每一页大小由硬件决定,或硬件支持多种大小的页,由操作系统选择页的大小。比如Intel支持4kb或4mb的页大小,那么操作系统可以选择,但同一时刻只能选择一种大小,所以对整个系统来说,页就是固定大小的。目前几乎所有pc上的操作系统都使用4kb大小的页。按4kb分页的话,总共有1048576个页。物理空间也是同样的分法。
如图,每个虚拟空间有8页,每页大小为1kb,那么虚拟地址空间就是8kb。我们假设计算机有13条地址线,即拥有2^13的物理寻址能力,那么理论上物理空间可以多达8kb。但是只能买得起6kb的内存,所以有效的只是前6kb。
那么我们把进程虚拟地址空间按页分割,常用数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用的时候再把它从磁盘里取出来即可。
这里写图片描述
假设有两个进程process1和process2,它们进程中的部分虚拟页面被映射到了物理页面,比如vp0、vp1和vp7映射到pp0、pp2和pp3;而有部分页面却在磁盘中,比如vp2和vp3位于磁盘的dp0和dp1中;另外还有一些页面如vp4、vp5和vp6可能尚未被用到或访问到,它们暂时处于未使用状态。分别叫做虚拟页、物理页和磁盘页。图中的线表示映射关系,我们可以看到虚拟空间的有些页被映射到同一个物理页,这样就可以实现内存共享。
图中process1的vp2和vp3不在内存中,但是当进程需要用到这两个页的时候,硬件会捕获到这个消息,就是所谓的页错误(page fault),然后操作系统接管进程,负责将vp2和vp3从磁盘中读出来并且装入内存,然后将内存中的这两个页与vp2和vp3之间建立映射关系。以页为单位来存取和交换这些数据非常方便,硬件本身就支持这种以页为单位的操作方式。
保护也是也映射的目的之一,简单的说就是每个页可以设置权限属性,谁可以修改,谁可以访问等,而只有操作系统有权限修改这些属性,那么操作系统就可以做到保护自己和保护进程。
这里写图片描述
一般MMU(memory management unit)都集成在CPU内部了,不会以单独的部件存在。
1.6众人拾柴火焰高
1.6.1线程基础
CPU开始向多核方向发展。多线程,作为实现软件并发执行的一个重要方法,也开始具有越来越重要的地位。
什么是线程
线程有时被称为轻量级的进程,是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(pc)、寄存器集合和堆栈组成。通常意义上,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)。一个经典的线程与进程的关系图:
这里写图片描述
多个线程可以互不干扰地并发执行,并共享进程的全局变量和堆的数据。
优点:
1.等待线程会进入睡眠状态,如等待网络响应,这可能话费数秒甚至数十秒。
2.某个操作(计数)会消耗大量时间,多线程可以让一个负责交互,另一个负责计算。
3.程序逻辑本身需要并发操作,如多端下载软件
4.多核计算机本身具备同时执行多个线程的能力
5.相对于多进程应用,多线程在数据共享方面效率高很多
线程的访问权限
栈:一般情况下认为是私有的数据
线程局部存储(TLS):是某些操作系统为线程单独提供的私有空间,但通常只有很有限的容量。
寄存器(包括pc寄存器):寄存器是执行流的基本数据,因此为线程私有
从C程序员角度来看,数据在线程间是否私有如表1-1:
这里写图片描述
线程的调度与优先级
不论是在多处理器还是单处理器,线程总是“并发”执行的。当线程数量小于等于处理器数量时(并且操作系统支持多处理器),线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此互不相干。否则,至少有一个处理器会运行多个线程。
单处理器时,操作系统会让多线程轮流执行,这样每个线程就“看起来”在同时执行。这样的一个不断在处理器上切换不同线程的行为称之为线程调度。在线程调度中,线程通常拥有至少三种状态,分别是:
运行:
就绪:
等待:
处于运行中线程拥有一段可以执行的时间,这段时间称为时间片,当用尽进入就绪。若等待先于用尽,就等待。每当离开运行时,调度系统就会选择一个就绪状态。等待的事件发生后,线程进入就绪状态:
这里写图片描述
线程调度都带有优先级调度和轮转法的痕迹。所谓轮转法,即是之前提到的让各个线程轮流执行一小段时间的方法。这决定了线程间交错执行的特点。而优先级调度则决定了线程按照什么顺序轮流执行。在具有优先级的系统调度中,线程都拥有各自的线程优先级。而低优先级的线程常常要等到系统中已经没有高优先级的可执行的线程存在时才能够执行。
线程优先级改变一般有三种方式:
1.用户指定优先级
2.根据进入等待状态的频繁程度提升或降低优先级
3.长时间得不到执行而被提升优先级
可抢占线程和不可抢占线程
时间片用尽后进入就绪状态的过程叫做抢占。早期一些系统(如Windows3.1)线程不可抢占,必须手动发出一个放弃执行的命令,才能让其他线程得到执行。在这样的调度模型下,线程必须主动进入就绪状态,而不是靠时间片。如果线程始终拒绝进入就绪状态,并且也不进行任何的等待操作,那么其他的线程将永远无法执行。在不可抢占线程中,线程主动放弃执行无非两种情况:
1.等待某事件(I/O等)
2.线程主动放弃时间片
因此,非抢占式线程调度时机是确定的,这样可以避免一些因为抢占式线程里调度时机不确定而产生的问题(见下一节:线程安全)。但即使如此,非抢占式线程在今日十分少见。
Linux多线程
Windows对进程和线程的实现如教科书一般标准,windows内核有明确的线程和进程概念。Windows API中,可以使用明确的API:createProcess和createThread来创建进程和线程,并且有一系列的API来操纵它们。但对于linux来说,线程并不是一个通用的概念。
Linux对线程的支持颇为贫乏,不存在真正意义上的线程概念。Linux将所有的执行实体都称为任务,每个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux下不同的任务之间可以选择共享内存空间,因为在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程里的线程。在Linux下,用一下方法可以创建一个新的任务:
这里写图片描述
fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数里返回:
pid_t pid;
if(pid =fork()){

}
在fork函数调用后,新的任务将启动并和本任务一起从fork函数返回。但不同的是本任务的fork函数将返回新任务的pid,而新任务的fork将返回0。
fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务一起共享一个写时复制(copy on write,cow)的内存空间。所谓写时复制指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方进行单独使用,以免影响其他的任务使用。
fork只能够产生本任务的镜像,因此须要使用exec配合才能够启动别的新任务。exec可以用新的可执行镜像替换当前的可执行镜像。因此fork产生一个新任务后,新任务可以调用exec来执行新的可执行文件。fork和exec通常用于产生新任务,而如果要产生新线程,则可以使用clone。clone函数的原型如下:
这里写图片描述
使用clone可以产生一个新的任务,从指定的位置开始执行,并且(可选的)共享当前进程的内存空间和文件等。如此就可以在实际效果上产生一个线程。
exec函数说明
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
1.6.2线程安全
多线程程序处于一个多变的环境当中,可访问的全局变量和堆数据随时都可能被其他的线程改变。因此多线程程序在并发时数据的一致性变得非常重要。
竞争与原子操作
单指令称为原子的,Windows里有一套API专门进行一些原子操作,这些API称为InterLocked API。要保证一个复杂的数据结构的原子性,原子操作指令就力不从心了。这里我们需要更加通用的手段:锁。
同步与锁
每一个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
二元信号量
对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:
1.将信号量的值减1.
2.如果信号量的值小于0,则进入等待状态,否则继续执行
访问完资源后,线程释放信号量,进行如下操作
1.将信号量的值加1
2.如果信号量的值小于1,唤醒一个等待的线程
互斥量和二元信号量很类似,区别是信号量可由另一个线程释放,而互斥量是谁获取谁释放。
临界区是比互斥量更加严格的同步手段。临界区锁的获取称为进入临界区,而把锁的释放称为离开临界区。互斥量和信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量。另一个进程视图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。
读写锁致力于一种更加特定的场合的同步。上述方法尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。
这里写图片描述
条件变量作为一种同步手段,作用类似于一个栅栏。对于条件变量,线程可以有两种操作,

exec函数:
http://blog.csdn.net/guoping16/article/details/6583383

原创粉丝点击