【阅读笔记】程序员的自我修养

第一章 温故而知新

​ 早期的计算机没有很复杂的图形功能,CPU的核心频率也不高,跟内存的频率一致,它们都直接连接在同一个总线上的。由于IO设备如显示设备、键盘、软盘和磁盘等速度与CPU相比还是慢很多,当时也没有复杂的图形设备,显示设备也是只能输出字符的终端,为了协调I/O设备与总线之间的速度,也为了CPU和I/O设备能够进行通信,一般每一个设备都会有一个相应的I/O控制器。

​ 后来CPU核心频率提升,内存跟不上CPU速度,于是产生了内存频率一致的总线,而CPU采用倍频的方式与总线通信,接着由于图形化的操作系统普及,使得图形芯片需要和CPU与内存进行大量的数据交换,于是产生了高速的北桥芯片

​ 由于北桥的运算速率非常高,所以如果低速的设备全都连接在北桥,北桥就需要同时处理低速的设备和高速的设备,设计会十分复杂。于是人们由设计了专门处理低速设备的南桥。芯片、键盘、鼠标、USB等连接在南桥,由南桥汇总连接到北桥。

1.文件的读取

1.1 文件系统

​ 文件系统管理着磁盘中文件的存储方式,比如在Linux系统下有一个文件“/home/user/test.dat”,长度为8000个字节,那么在创建这个文件的时候,Linux的ext3文件系统有可能将这个文件按照这样的方式存储在磁盘中:

​ 前4096个字节存储在1000号扇区到1007号扇区,每个扇区512字节,8个扇区正好4096个字节;
​ 文件的第4097个字节到8000个字节存储在磁盘的2000号扇区到2007号扇区,8个扇区也是4096个字节,只不过只存储了3904个有效的字节,剩下的192个字节无效。硬盘结构介绍

​ 当我们在Linux中,要读取文件的前4096个字节时,会使用一个read的系统调用来实现。文件系统收到read请求后判断出文件前4096个字节在1000~1007个扇区,然后文件系统就向硬盘驱动发出一个读取逻辑扇区为1000号开始的8个扇区的请求,驱动程序收到这个请求之后向硬盘发出硬件命令。

​ 向硬件发送I/O命令的方式有很多种,其中最为常见的一种就是通过读写I/O端口寄存器来实现。在x86平台上共有65536个硬件端口寄存器,不同的硬件被分配到了不同的I/O端口地址。CPU提供了两条专门的指令"in"和"out"实现对硬件端口的读和写。

​ 对IDE接口来说,它有两个通道,分别为IDE0和IDE1,每个通道可以连接两个设备,分别为Master和Slave。一个PC中最多4个IDE设备。

1.2 内存问题

如何将计算机有限的物理内存分配给多个内存使用

有128M内存 三个程序:程序A运行需要10M,程序B运行需要100M,程序C运行需要20M。

如果我们同时运行A和B,比较直接的做法是将前10M分给A,第10到110M分给B。但这样分配问题很多:

  1. 地址空间不隔离 恶意程序很可能改写其他程序的内存数据

  2. 内存使用效率低 通常在程序执行时需要将整段程序装入内存执行,如果要运行其他程序内存空间可能不够,就需要将正在运行的程序读回磁盘,等到需要运行的时候再读入内存运行。

  3. 程序运行的地址不确定 程序每次装入运行的时候都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的,但是在程序编写时它访问的数据和指令跳转时的目标地址都是确定的,这涉及到程序的重定位问题

    • 分段(Segmentation)

      将程序所需要的内存空间大小的虚拟空间映射到某个地址空间。
      比如说一段程序运行需要10M空间,我们假想有一段10M的虚拟空间,从0x00000000到0x00A00000,然后从实际的内存中分配一个相同大小的物理地址0x00100000到0x00B00000,然后将两地址空间进行一一映射。即虚拟空间中的每个字节对应物理空间的每个字节。

      ​ 分段的方法解决了‘地址空间不隔离’以及‘程序运行地址不确定’ 的问题。因为程序A和B被映射到了不同的物理空间区域,如果A访问虚拟空间的地址超出了范围,就会被硬件判断为非法的访问,拒绝这个地址请求。再者,对于每个程序来说,无论它们被分配到物理地址的哪一个区域,对于程序来说都是透明的,它们不关心物理地址的变化,只需要按照地址从0x00000000到0x00A00000来编写地址、放置变量,所以程序不再需要重定位。

      ​ 但是分段美没有解决内存使用效率的问题。分段对内存区域的映射还是按照程序为单位。如果内存不足,被换入换出到磁盘的都是整个程序。事实上,按照程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小段程序,程序地很多数据其实在一个时间段内都是不会被用到地。于是人们很自然地想到了更小粒度地内存分割和映射地方法————分页(Paging).

    • 分页(Paging)

      ​ 分页的基本方法是将地址空间人为地分成固定大小的页。每一页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。

      ​ 我们将进程的虚拟地址空间按页分割,将常用的数据和代码页装在到内存中,不常用的数据和代码保存在磁盘里,当用到的时候在将它从磁盘中取出来。当进程需要的页不在内存中时,如进程需要VP2和VP3两个页,硬件会捕获这两个消息,就是所谓的页错误,然后操作系统接管进程,负责将VP2和VP3从进程中读出来装入内存,然后将内存中的这两个页与VP2和VP3之间建立映射关系。

      ​ 虚拟空间的页叫虚拟页(VP,Virtual Page),物理内存中的页叫物理页(PP,Physical Page),磁盘中的页叫磁盘页(DP,Disk Page)。虚拟空间中的有些页,或不同进程的页可能回被映射到同一个物理页,这样就可以实现内存共享。

      虚拟存储的实现需要硬件的支持,对于不同的CPU来说是不同的。但是几乎所有的硬件都采用一个叫做MMU(Memory Management Unit)的部件来进行页映射。

      virtual adress
      physical adress
      CPU
      MMU
      Physical Memory

      在页模式下,CPU发出的是Virtual Address,即我们的程序看到的是虚拟地址,经过MMU转换后变成了Physical Address.

      一般MMU集成在CPU内部了,不会以独立的部件存在。


2.线程

线程,有时被称作轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。

一个标准的线程由线程ID当前指令指针PC、寄存器集合、和堆栈组成。通常意义上讲,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)。

  • 线程的访问非常自由,它可以访问进程内存里的所有数据(如果知道其他线程的堆栈地址,甚至也可以访问),但在实际应用中线程也有自己私有的存储空间。

    线程私有线程之间共享(进程所有)
    · 局部变量
    · 函数的参数
    · TLS数据(线程局部存储,Thread Local Storage,TLS)
    · 全局变量
    · 堆上的数据
    · 函数里的静态变量
    · 程序代码,任何线程都有权利读取并执行任何代码
    · 打开的文件,A线程打开的文件可以由B线程读写
2.1 线程调度与优先级

​ 在计算机中,线程总是“并发”执行的,当线程数量小于处理器数量时,不同的线程运行在不同的处理器上,彼此之间毫不相干;但是线程数大于处理器数量时,线程并发会碰到阻碍,这时会有处理器运行多个线程。
​ 在单处理器运行多线程时,并发是模拟出来的一种状态,操作系统会让这些多线程程序轮流执行,每次这个线程只执行一小段时间(通常只有几十到几百毫秒),模拟出线程的并发状态。这样的一个不断在处理器上切换线程的行为称之为线程调度(Thread Schedule).

​ 在线程调度中,线程通常有三种状态:运行(Running)、就绪(Ready)、等待(Waiting).

​ 运行中的线程有一段可以执行的时间,这段时间成为时间片(Time Slice).
​ 时间片用尽,线程进入就绪状态。
​ 时间片用尽之前线程在等待某个事件,线程进入等待状态。
​ 每当一个线程离开运行状态,调度系统就会选择一个其他的就绪线程继续运行。

​ 在具有优先级调度的系统中,线程具有各自的线程优先级。如在Windows和Linux中,系统会根据不同线程的表现自动调整优先级,以使得线程调度更有效率。

IO密集型线程:频繁等待的线程
CPU密集型线程:很少等待的线程

  • Linux的多线程

    ​ Windows对进程和线程的实现如同教科书一般标准,Windows内核有明确的线程和进程的概念。在Windows API中,有明确的API来创建进程和线程,并由一系列的API来操纵它们。但对Linux来说,线程并不是一个通用的概念。

    ​ Linux内核中并不存在真正意义上的线程概念。在Linux中,所有的执行实体,无论是进程还是线程都被称为任务(Task),相当于一个单线程的进程,具有内存空间,执行实体,文件资源等。不过,Linux下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就构成了进程的线程。

    • 写时复制:两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响其他任务使用。
2.2 线程安全

代码中很多语句在编译为汇编代码之后不止一条指令,如自增操作++i的实现:

(1)读取i到某个寄存器X,
(2)X++,
(3)将X的内容存储回i。

因此,很可能语句++i在执行一半的时候被调度系统打断去执行别的代码。如同时有两个线程对i操作,由于寄存器X的内容在不同的线程中是不一样的,所以可能会导致意想不到的后果。

我们把单指令的操作称为原子的(Atomic),单挑指令的执行是不会被打断的。

  • 同步与锁

    为了避免多个线程同时读写同一个数据而产生不可预料的后果,我们要将各个线程对同一个数据的访问同步(Synchronization).
    同步:一个线程访问数据未结束时,其他线程不得对该数据进行访问,如此,数据的访问被原子化了。

    同步最常见的作法是使用锁(Lock)
    二元信号量(Binary Semaphore):一种最简单的锁。它只有两种状态:占用/非占用 | 适合只能被唯一一个线程独占访问的资源。占用期间,其他线程无法访问。

    信号量(Semaphore,或多元信号量),允许多个线程并发访问资源,一个初始值为N的信号量允许N个线程并发访问。信号量在整个系统中可以被任意系统获取并释放,即:同一个信号量可以由系统中的一个线程获取后再由另外一个线程释放

    互斥量(Mutex),类似二元信号量,仅同时允许一个线程访问。和信号量不同的是,互斥量要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁。

    临界区(Critical Section),比互斥量更加严格的同步手段。互斥量和信号量在系统中的任何进程都是可见的,即:一个进程创建了一个互斥量或信号量,另外一个进程试图获取这个锁是合法的。但是临界区的作用仅限于本进程,其他进程无法获取该锁。

    读写锁(Read-Write Lock),使用与数据读取特别频繁,但是写入特别少的情况。 | 读写锁有两种获取方式:共享的/独占的(Shared/Exclusive).如果锁处于自由状态,可以以共享或独占方式获取;如果锁处于共享状态,其他线程只能以共享方式获取锁。

  • 多线程内部情况:

    大多数操作系统都在内核里提供线程的支持。内核线程(与Linux内核中的kernel_thread不是一回事)由多处理器或调度实现并发,然而实际用户使用的线程并不是内核线程,而是存在于用户态的用户线程。在操作系统内核中,用户线程数量并不一定与内核线程相等。例如某些轻量级的线程库,对于用户来说如果由三个线程在同时执行,对于内核来说可能只有一个线程。

    • 一对一模型:一个用户使用的线程唯一对应一个内核使用的线程。(但反过来不一定,一个内核线程在用户态不一定由对应的线程存在)
    • 多对一模型:将多个用户线程映射到一个内核线程。
    • 多对多模型:将多个用户线程映射到少数但不止一个内核线程。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 程序员的自我修养是一本技术类图书,通过以文字方式呈现,将核心内容以PDF文件的形式呈现给读者。 《程序员的自我修养》一书是由俞甲子、俞甲子联合编写的,该书主要讲述了程序员在技术方面的自我提升与修养。书中详细介绍了程序员所需具备的技能与素质,并通过实例、案例等方式来进行详细解析。 首先,本书着重强调了程序员的技术素养。作为一名合格的程序员,必须具备扎实的编程基础知识,熟练掌握至少一种编程语言,并能够灵活应用于实际项目中。同时,还要不断学习新的编程技术和工具,提高开发效率和质量,不断追求技术的创新与突破。通过深入的技术讲解和实例分析,读者可以更好地理解和掌握这些关键技能。 其次,本书重视程序员的思维与方法。除了技术之外,合理的思维方式和解决问题的方法也是程序员必备的素养。本书通过探讨算法、数据结构、设计模式等内容,引导读者形成良好的编程思想和解决问题的思路。在实际开发过程中,程序员能够运用这些思维和方法,更加高效地解决实际问题。 此外,本书还提到了程序员的团队合作和沟通能力的重要性。现代软件开发往往需要多人合作完成,团队合作和沟通能力至关重要。本书通过讲解项目管理、代码规范、团队协作等方面的内容,帮助读者更好地适应团队开发的环境。 总之,《程序员的自我修养》在技术、思维方式和团队合作等多个方面对程序员的自我提升与修养进行了全面而详细的讲解。通过阅读文字版PDF,程序员可以更好地了解自身在技术和素养方面的不足,并通过实践和学习不断提升自己。 ### 回答2: 《程序员的自我修养》是一本由俞甲子编写的程序员必读经典之一。这本书涵盖了计算机科学的基础知识和程序员在日常工作中所需要的修养和技能。 首先,这本书强调了程序员应具备的基本素质。不仅要有扎实的计算机基础知识,还要有广泛的知识储备和求知欲。书中指出,只有具备全面的知识背景,程序员才能适应不断变化的软件开发环境,并能够快速学习和掌握新的技术。 其次,书中提到了程序员的编程能力。编程是程序员的基本技能,而编程能力的提升不仅仅局限于语法和算法的掌握,更要关注代码的可维护性、可读性和可测试性。此外,书中还提到了代码风格规范、代码重构和代码调试等重要的编程技巧。 此外,这本书还介绍了程序员应具备的工程素养。程序员的工作不仅仅是写代码,还包括需求分析、架构设计、项目管理等方方面面。程序员应该具备良好的团队合作和沟通能力,能够与其他人合作完成一个项目。此外,书中还提到了软件工程的重要原则和方法,如模块化、面向对象设计、测试驱动开发等。 除此之外,书中还特别强调了程序员的自我提升和学习能力。程序员应保持持续学习的态度,不断跟进行业的最新发展,并积极参与技术社区和开源项目,与其他程序员互动交流,共同进步。 总之,《程序员的自我修养》是一本涵盖广泛知识和技能的书籍,其中介绍了程序员应具备的基本素质、编程能力、工程素养和自我提升等方面的内容。这本书对于程序员的职业发展和能力提升有着重要的指导作用,值得每位程序员认真阅读和实践。 ### 回答3: 《程序员的自我修养》是一本由俞甲子编著的程序员修养指南,以PDF文字版的形式呈现。本书旨在帮助程序员们提升自己的理论基础和实践能力,进而追求个人的成长和职业发展。 《程序员的自我修养》主要分为理论和实践两部分。在理论部分,作者深入浅出地介绍了计算机系统的基本原理、计算机网络的工作原理、软件工程中的常用设计模式等。通过这些理论知识的学习,程序员们能够更好地理解计算机系统的运行机制,从而编写更高效、稳定的程序。 而在实践部分,本书通过丰富的实例和案例,引导程序员们进行实践和项目开发。它包括了代码调试和优化的技巧、算法与数据结构的实践应用、多线程与并发编程的方法等。通过这些实践指导,程序员们能够更好地应对实际问题,并提高代码质量和效率。 《程序员的自我修养》文字版的PDF形式具有以下优点:一是方便携带和阅读,读者可以随时随地进行学习和查阅;二是易于搜索和标注,可以快速定位到自己关注的内容,并进行个性化的学习笔记;三是多平台兼容,可以在各种设备上进行阅读,满足不同读者的需求。 总之,《程序员的自我修养》文字版的PDF形式是一本帮助程序员们提升自己的重要工具,通过学习其中的理论知识和实践经验,程序员们能够更好地应对工作中的挑战,提升自身的竞争力,实现个人价值的最大化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值