操作系统基础

操作系统做什么

  • 操作系统的一个功能是提供抽象的接口,另外一个主要功能是管理硬件资源。
  • 计算机硬件的能力是有限的,比如一个CPU一秒能够执行的指令条数是1亿条或是1GB的内存能够最多同时存储1GB的数据。无论你是否使用它,资源总是那么多。当我们不希望自己花钱买回来的硬件成摆设,充分挖掘硬件的能力,使得计算机运行得更有效率,在短时间内处理更多的任务,才是我们的目标。一个计算机中的资源主要分CPU、存储器(包括内存和磁盘)和I/O设备,我们从这三个方面来看看如何挖掘它们的潜力。

硬盘结构

  • 硬盘基本存储单位为扇区(Sector),每个扇区一般为512字节。一个硬盘往往有多个盘片,每个盘片分两面,每面按照同心圆划分为若干个磁道,每个磁道划分若干个扇区。比如一个硬盘有2个盘片,每个盘面分65536磁道,每个磁道分1024个扇区,那么硬盘的容量就是2 * 2 * 65536 * 1024 * 512 = 137438953472字节(128G)。但是我们可以想象,每个盘面上同心圆的周长不一样,如果按照每个磁道都拥有相同数量的扇区,那么靠近盘面外围的磁道密度肯定比内圈更加稀疏,这样是比较浪费空间的。但是如果不同的磁道扇区数又不同,计算起来就十分麻烦。为了屏蔽这些复杂的硬件细节,现代的硬盘普遍使用一种叫做LBA最后一个扇区,这个扇区编号叫做逻辑扇区号。逻辑扇区号抛弃了所有复杂的磁道、盘面之类的概念。当我们给出一个逻辑的扇区号时,硬盘的电子设备会将其转换成实际的盘面、磁道等这些位置。

不使用虚拟地址会遇到的问题

  • 地址空间不隔离
    • 所有程序都直接访问物理地址,程序所使用的内存空间不是相互隔离的。恶意的程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意的、但是有臭虫的程序可能不小心修改了其他程序的数据,就会使其他程序也崩溃,这对于需要安全稳定的计算环境的用户来说是不能容忍的。用户希望他在使用计算机的时候,其中一个任务失败了,至少不会影响其他任务。
  • 内存使用效率低
    • 由于没有有效的内存管理机制,通常需要一个程序执行时,监控程序就将整个程序装入内存中然后开始执行。如果我们忽然运行程序C,那么这时内存空间其实已经不够了,这时候我们可以用的一个办法是将其他程序的数据暂时写到磁盘里面,等到需要用到的时候再读回来。由于程序所需要的空间是连续的,那么这个例子里面,如果我们将程序A换出到磁盘所释放的内存空间是不够的,所以只能将B换出到磁盘,然后将C读入到内存开始运行。可以看到整个过程中有大量的数据在换入换出,导致效率十分低下。
    • 程序运行的地址不确定
      • 因为程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的。这给程序的编写造成了一定的麻烦,因为程序在编写时,它访问数据和指令跳转的目标地址很多都是固定的,这涉及程序的重定位的问题。
    • 解决这几个问题的思路就是增加中间层,即使用一种间接地址访问方法。整个想法是这样的,我们把程序给出的地址看作是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将整个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制整个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另一个程序相互不重叠,以达到地址空间隔离的效果。
  • 地址空间
    • 虚拟地址空间和物理地址空间。物理地址空间是实实在在存在的,存在于计算机中,而且对于每一台计算机来说只有唯一的一个,你可以把物理空间想象成物理内存,比如你的计算机用的是Intel的Pentium4的处理器,那么它是32位的机器,即计算机地址线有32条(实际上是36条地址线,不过暂时认为它只有32条),那么物理空间就有4G B。但是你的计算机上只装了512MB的内存,那么其实物理地址的真正有效部分只有0x00000000 ~ 0x1FFFFFFF,其他部分都是无效的(实际上还有一些外部I/O设备映射到物理空间的,也是有效的,但是我们暂时无视其存在)。虚拟地址空间是指虚拟的、人民想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就有效地做到了进城的隔离。

内存分段

  • 分段的方法基本解决了上面提到的3个问题中的第一个和第三个。首先它做到了地址隔离,因为程序A和程序B被映射到了两块不同的物理空间区域,它们之间没有任何重叠,如果程序A访问虚拟空间的地址超出了0X00A00000整个范围,那么硬件就会判断这是一个非法的访问,拒绝这个地址请求,并将这个请求报告给操作系统或监控程序,由它来决定如何处理,再者,对于每个程序来说,无论它们被分配到物理地址的那一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只需要按照从地址0x00000000到0x00A00000来编写程序、放置变量,所以程序不再需要重定位。
  • 但是分段的这种方法还是没有解决我们的第二个问题,即内存使用的效率的问题。分段对内存区域的映射还是按照程序位单位,如果内存不足,被换入换出到磁盘的都是整个程序,这样是比会到撑大量的磁盘访问操作,从而严重影响速度,这种方法还显得粗糙,粒度比较大。事实上,根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的,人民自然地想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率,这种方法就是分页。

线程基础

  • 栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)。

  • 线程局部存储(TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。

  • 寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此线程私有。

  • 从C程序的角度来看,数据在线程之间是否私有如下:

    • 线程私有
      • 局部变量
      • 函数的参数
      • TLS数据
    • 线程之间共享
      • 全局变量
      • 堆上的数据
      • 函数里的静态变量
      • 程序代码,任何线程都有权利读取并执行任何代码
      • 打开的文件,A线程打开的文件可以由B线程读写
  • 一般把频繁等待的线程称之为IO密集型线程,而把很少等待的线程称之为COU密集型线程。IO密集型线程总是比CPU密集型线程容易得到优先级的提升。

  • 线程间同步

    • 互斥量和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,那个线程就要负责释放这个锁,其他线程去释放互斥量是无效的。
    • 临界区是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。
    • 同步的最常见方法是使用锁。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
    • 二元信号量是最简单的一种锁,它只有两种状态:占用和非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号处于非占用状态时,第一个试图获取该二元信号量的线程会获取该锁,并将二元信号量置为占用状态,此后其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放。
    • 对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量。进行如下操作:
      • 将信号量的值减1
      • 如果信号量的值小于0,则进入等待状态,否则继续执行
      • 访问完资源之后,线程释放信号量,进行如下操作:
      • 将信号量的值加1
      • 如果信号量的值小于1,唤醒一个等待中的线程
    • 读写锁致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取总是没有问题的,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错。如果我们使用上诉信号量、互斥量或临界区中的任何一种来进行同步,尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁有两种获取方式,共享的或独占的。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。如果锁处于共享状态,其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程。然而,如果其他线程试图独占的方式获取已经处于共享状态的锁,那么它将必须等待被所有的线程释放。相应地,处于独占状态的锁将阻止任何其他线程获取该锁,不论他们试图以哪种方式获取。
    • 条件变量作为一种同步手段,作用类似于一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。

可重入函数

  • 一个函数要成为可重入的,必须具有如下几个特点:
    • 不使用任何(局部)静态或全局的非const变量
    • 不返回任何(局部)静态或全局的非const变量的指针
    • 仅依赖调用方提供的参数
    • 不依赖任何单个资源的锁(mutex等)
    • 不调用任何不可重入的函数
    • 可重入时并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

《程序员的自我修养—链接、装载与库》- 笔记

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值