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

来源:互联网 发布:域名ping不通 编辑:程序博客网 时间:2024/05/21 06:47

本系列博客是学习《程序员的自我修养——链接、装载与库》一书的笔记,由于工作这么久一直对于程序写完编译、到链接到执行这些过程都感到模棱两可,乘着时间空闲,学习学习。

第一章 温故而知新
1.1 从 Hello World 说起
#include <stdio.h>
int main(void)
{
printf("Hello World!\n);
return 0;
}
留下问题:
1、程序为什么要被编译器编译了之后才可以运行?
2、编译器把C 语言程序转换成可以执行的机器码的过程做了什么,怎么做的?
3、最后编译出来的可执行文件里面是什么?除了机器码还有什么?它们怎么存放的,怎么组织的?
4、#include<stdio.h>是什么意思? 把stdio.h 包含进来意味着什么? C语言库又是什么?它怎么实现的?
5、不同的编译器(Microsoft VC、GCC)和不同的硬件平台(x86、SPARC、MIPS、ARM),以及不同的操作系统(Windows、Linux、UNIX、Solaris),最终编译出来的结果一样吗?为什么?
6、Hello World 程序是怎么运行起来的?操作系统是怎么装载它的?它从哪儿开始执行,到哪儿结束?main 函数之前发生了什么? main函数结束以后又发送了什么?
7、如果没有操作系统,Hello World 可以运行吗?如果要在一台没有操作系统的机器上运行Hello World 需要什么?应该怎么实现?
8、printf 是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
9、Hello World 程序在运行时,它在内存中是什么样子的?

1.2 万变不离其宗
北桥芯片:协调CPU、内存和高速的图形设备,方便他们之间能够高速的交换数据。
南桥芯片:处理低速设备,如磁盘、USB、键盘、鼠标。
SMP:对称多处理器,有多个CPU,每个CPU在系统中所处的低位和发挥的功能都是一样的。

1.3 站得高,望得远
系统软件分成两块:一块是平台性的,比如操作系统内核、驱动程序、运行库和数以千计的系统工具; 另一块是用于程序开发的,比如编译器、汇编器、链接器、等开发工具和开发库。

1.4 操作系统做什么
操作系统的一个功能是提供抽象的接口,另外一个主要功能是管理硬件资源。

1.4.1 不要让 CPU 打盹
多道程序方法——>分时系统——>多任务系统

1.4.2 设备驱动
硬盘的结构:基本存储单位为扇区(Sector),每个扇区一般为512字节。一个硬盘往往有多个盘片,每个盘片分两面,每面按照同心圆划分为若干个磁道,每个磁道划分为若干个扇区。现代的硬盘普遍使用 LBA (Logical Block Address)的方式,即整个硬盘中的所有扇区从 0 开始编号,一直到最后一个扇区,这个扇区编号叫做逻辑扇区号。逻辑扇区号抛弃了所有复杂的磁道、盘面之类的概念。当我们给出一个逻辑的扇区号时,硬盘的电子设备会将其转换成实际的盘面、磁道等这些位置。

1.5 内存不够怎么办
假如计算机有128 MB内存,程序 A 运行需要 10MB,程序 B 需要 100 MB,程序 C 需要 20MB。如果我们需要同时运行程序 A 和 B,那么比较直接的做法是将内存的前 10 MB 分配给A, 10MB~110MB 分配给B。这样就能够实现 A 和 B两个程序同时运行,到那时这种简单的内存分配策略问题很多。
1、地址空间不隔离
2、内存使用效率低
3、程序运行的地址不确定
解决这几个问题的方法就是增加中间层,即使用一种间接的地址访问方法。把程序给出的地址看作是一种虚拟地址,然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序相互不重叠,以达到地址空间隔离的效果。

1.6 众人拾柴火焰高
线程:有时被称为轻量级进程(Lightweight Process, LWP),是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。
多个线程可以互不干扰地并发执行,并共享进程的全局变量和堆的数据。使用多线程的原因有如下几点:
1、某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程执行可以有效利用等待的时间。
2、某个操作会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多线程可以让一个线程负责交互,另一个线程负责计算。
3、程序逻辑本身就要求并发操作。
4、多CPU 或多核计算机,本身具备同时执行多个线程的能力,因此单线程程序无法全面的发挥计算机的全部计算能力。
5、相对于多进程应用,多线程在数据共享方面效率要高很多。
线程的访问权限
线程的访问非常自由,它可以访问进程内存里的所有数据。实际运用中线程也拥有自己的私有存储空间,包括以下几方面:
1、栈(尽管并非完全无法被其他线程访问,但一般情况下任然可以认为是私有的数据)。
2、线程局部存储(Thread Local Storage,TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。
3、寄存器(包括 PC 寄存器),寄存器是执行流的基本数据,因此为线程私有。
单处理器对应多线程的情况:
运行:此时线程正在执行
就绪:此时线程可以立即运行,但CPU已经被占用。
等待:此时线程正在等待某一事件(通常是I/O 或同步)发生,无法执行。
IO密集型线程:频繁等待的线程
CPU密集型线程:很少等待的线程
IO密集型线程总比CPU密集型线程容易得到优先级的提升。
在优先级调度的环境下,线程的优先级改变一般有三种方式:
用户指定优先级;
根据进入等待状态的频繁程度提升或降低优先级;
长时间得不到执行而被提升优先级。
1.6.1 线程安全
单指令的操作称为 原子指令。
同步与锁
同步:即是指在一个线程访问数据未结束的时候,其它线程不得对同一个数据进行访问。
同步最常见的方法是使用 锁。锁是非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到该锁被释放。
二元信号量(Binary Semaphore)是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。
对于允许多个线程并发访问的资源,多元信号量简称信号量(Semaphore),它是一个很好的选择。一个初始值为N 的信号量,允许N 个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:
1、将信号量的值减1;
2、如果信号量的值小于0,则进入等待状态,否则继续执行。
访问完资源之后,线程释放信号量,进行如下操作:
1、将信号量的值加 1;
2、如果信号量的值小于1,唤醒一个等待中的线程。
互斥量:和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁。
临界区:是比互斥量更加严苛的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。
读写锁
条件变量
可重入(Reentrant)与线程安全
一个函数被重入,表示这个函数没有执行完成,只有两种情况:
1、多个线程同时执行这个函数。
2、函数自身(可能是经过多层调用之后)调用自身。
一个函数可重入,必须具有如下几个特点:
1、不使用任何(局部)静态或全局的非const变量。
2、不返回任何(局部)静态或全局的非const 变量的指针。
3、仅依赖调用方提供的参数。
4、不依赖任何单个资源的锁(mutex等)。
5、不调用任何不可重入的函数。
过度优化
volatile 基本可以做到两件事情:
1、阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;
2、阻止编译器调整操作volatile 变量的指令顺序。

1.6.2 多线程内部情况
三种线程模型
1、一对一模型
对于直接支持线程的系统,一对一模型始终是最为简单的模型。对一对一模型来说,一个用户使用的线程就唯一对应一个内核使用的线程(但反过来不一定,一个内核里的线程在用户态不一定有对应的线程存在)。

一对一线程缺点有两个:
1、由于许多操作系统限制了内核线程的数量,因此一对一线程会让用户的线程数量受到限制。
2、许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。

2、多对一模型
多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此相对于一对一模型,多对一模型的线程切换要快速许多。

多对一模型问题:如果其中一个用户线程阻塞,那么所有线程都无法执行,因为此时内核里的线程也随之阻塞了。好处是:高效的上下文切换和几乎无限制的线程数量。
3、多对多模型
多对多模型,一个用户线程阻塞并不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。另外,对用户线程的数量也没有限制。


阅读全文
0 0
原创粉丝点击