程序员的自我修养学习笔记——第一章
从helloworld说起:
#include <stdio.h>
int main()
{
printf("Hello,World\n");
return 0;
}
你能回答如下问题吗?
·程序为什么要经过编译才能运行?
·编译器把C语言转化为可执行的机器码的过程做了什么,怎么做的
·最后编译出来的可执行文件里面是什么?除了机器码之外还有什么?它们是怎么存放,怎么组织的?
·#include<stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它怎么实现的?
·不同的编译器(VC,GCC)和不同的硬件平台(x86,SPARC,MIPS,ARM),以及不同的操作系统(Windows,Linux,Unix,Solaris),最终编译出来的结果一样吗?为什么?
·Hello World程序是怎么运行起来?操作系统是怎么装载它的?它从哪儿开始执行,到哪儿结束?main函数之前发生了什么?main函数之后又发生了什么?
·如果没有操作系统,Hello World可以运行吗?如果要在一台没有操作系统的机器上运行Hello World需要什么?应该怎样实现?
·printf是怎么实现的?它为什么可以由不定数量的参数?为什么它能够在终端上输出字符串
·Hello World程序运行时,它在内存中是什么样子的?
万变不离其宗:
为了协调CPU、内存和高速的图形设备,人们设计了一个高速的北桥芯片,以便它们之间能够高速的交换数据。磁盘、USB、鼠标等低速设备都连接在南桥上。将它们汇总之后连接到北桥上。
CPU的频率被限制在了4GHz上,为了进一步提高CPU速率,采用多核。SMP(Symmetrical Multi-Processing)——对称多核处理器
系统软件可以分成两块:一块是平台性的,如操作系统、内核、驱动程序、运行库等;另一块是用于程序开发的,如编译器、汇编器、链接工具盒开发库。
系统调用的接口在实现中往往以软中断的方式提供,Linux使用0x80号中断作为系统调用接口,Windows使用0x2E号中断作为系统调用接口。
为了高效的利用CPU资源,采用了躲到程序技术
抢占式的操作系统可以强制剥夺CPU资源并且分配给它们认为目前最需要的进程。
文件系统保存了文件的存储结构,负责维护这些数据结构并且保证磁盘中的扇区能够有效地组织和利用。
内存不够怎么办:
早期的操作系统,程序运行时访问的地址都是物理地址。
内存线性分配的缺点:
- 地址空间不隔离,恶意程序很容易改写其他程序的内存数据
- 内存使用效率低,大量的数据换入换出,导致效率十分低下
- 程序运行地址的不确定,有时候地址需要重定位
解决上面这些问题,可以通过增加一个中间层来实现。我们通过把程序给出的地址看作是一种虚拟地址,然后通过某种映射机制,将这个虚拟地址转换成实际的物理地址。
地址隔离:每个进程都有自己独立的虚拟地址空间,而且每个进程只能访问自己的地址空间,这样就有效地做到了进程的隔离
分段的基本思路就是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。
通过分段解决了问题1、3,实现了地址隔离,不需要对程序进行地址重映射,因为在程序员操作的是虚拟地址。
根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,很多数据在一个时间段内都是不会被用到的。我们可以使用更小的内存 分割和映射粒度,这种方法就分页,达到提高了内存的使用率
虚拟存储的实现需要依靠硬件的支持,通过一个叫MMU的部件来进行页映射。
虚拟地址到物理地址的转换过程:
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,那就是程序本身。
引入线程的好处:
1 创建一个新线程花费的时间少。
2 两个线程(在同一进程中的)的切换时间少。
3 由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
4 线程能独立执行,能充分利用和发挥处理机与外围设备并行工作的能力。
线程的访问权限:
线程的优先级改变一般有三种方式:
·用户指定优先级
·根据进入等待状态的频繁程度提升或降低优先级
·长时间得不到执行而被提升优先级
线程在用尽时间片之后会被强制剥夺继续执行的权利,进入到就绪状态,这个过程就叫做"抢占"(Preemption)
Linux下的多线程:
Linux将所有的执行实体(无论进程还是线程)都称为任务,每个任务概念上都类似于一个单线程实体,都成为任务(Task)
同步:是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
同步最常用的方法是“锁”。
二元信号量是最简单的锁,只有两种状态:占用、与非占用
多元信号量,简称为信号量。一个初值为N的信号量允许N个线程并发访问。
互斥信号量:和二元信号量类似,
区别:一般的信号量可以被系统中的一个线程获取之后由另一个线程释放,而互斥信号量则要求哪个线程获取了互斥信号量,哪个线程就要负责释放掉这个锁。
临界区:是一种比互斥信号量更严格的同步手段,获取临界区的锁称为进入临界区,释放称为离开临界区。
区别:临界区和互斥信号量的区别在于互斥信号量在系统的任何进程里面都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该所是合法的。然而,临界区的作用范围仅限于本进程,其他进程无法获取该锁。
读写锁
条件变量
条件变量与互斥锁,信号量的区别
1.互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。
2.互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。
3.由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
4.互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。
一个函数要可重入必须具备以下特点:
·不使用任何(局部)静态或全局的非const变量
·不返回任何(局部)静态或全局的非const变量的指针
·仅依赖于调用方提供的参数
·不依赖任何单个资源的锁(mutex等)
·不调用任何不可重入的函数
上锁不一定能实现线程安全,因为编译器可能做出过度的优化。
编译器为了提高x的访问速度,把x放到了某个寄存器里面,那么我们知道不同线程的寄存器是各自独立的。因此可能上锁也不管用。
可以使用volatile关键字防止编译器过度优化:
·阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回
·阻止编译器调整操作volatile变量的指令顺序
以下部分出自:
源文档 <http://hi.baidu.com/yibobin/blog/item/e1b13cfa1a335bd1b58f317e.html>
c++new的两个步骤:分配内存;调用构造函数
// Singleton《设计模式:可复用面向对象软件基础》
volatile T* pInst=0;
T* GetInstance()
{
// 双层if让lock的开销降低到最小
if (pInst==NULL)
{
lock();
if(pInst==NULL)
/*
1.分配内存
2.在内存的位置上调用构造函数
3.将内存的地址赋值给pInst
*/
pInst=new T;
/*
上面第2,3步顺序可以颠倒,也就是说
pInst的值已经不是NULL,但对象仍然没有构造完毕,这时另一个GetInstance的并发调用此时第一个if里为false
这时就会范围尚未构造完全的对象地址pInst以提供给用户使用
*/
unlock();
}
return pInst;
}
/* 阻止换序:可以加barrier指令阻止CPU将该指令之前的指令交换到barrier之后。
T* tenp=new T;
barrier();
pInst=tenp;
*/