§ 1 线程安全的对象生命期管理

  • 对象的生与死不能由对象自身拥有的mutex(互斥器)来保护。如何避免对象析构时可能存在的race condition(竞态条件)是C++多线程编程面临的基本问题。

  • 当一个对象被多个线程同时看到,那么对象的销毁时机就会模糊不清,可能出现多种的race condition(竞态条件):

    • 即将析构一个对象时,从何而知此刻是否有别的对象正在执行该对象的成员函数?
    • 如何保证在执行成员函数期间,对象不会被另一个线程所析构?
    • 在调用某个对象的成员函数之前,如何得之这个对象还活着?它的析构函数是否会碰巧执行到一半?
  • 一个线程安全的class应当满足下面三个条件:

    • 多个线程同时访问时,其表现出正确的行为。
    • 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织(interleaving)。
    • 调用端代码无需额外的同步或其它协调动作。
  • 按照以上定义,C++标准库中的大多数类都不是线程安全的,包括std:string、std::vector、std::map等,这些class通常需要在外部加锁才能供多个线程同步访问。

Read More

cache通常被翻译为高速缓冲存储器(以下简称“高速缓存”),虽然现在cache的含义已经不单单指CPU和主存储器(也就是通常所谓的内存)之间的高速缓存了,但在本文中所谓的cache依旧特指CPU和主存储器之间的高速缓存。

这篇文章诞生的源头是我之前在stackoverflow看到的一个问题:

Why is transposing a matrix of 512×512 much slower than transposing a matrix of 513×513 ?

这个问题虽然国外的大神给出了完美的解释,但是我当时看过之后还是一头雾水。想必对x86架构上的cache没有较深入了解过的童鞋看过之后也是一样的感受吧。于是趁着寒假回家第一天还没有过多外界干扰的时候,我们就来详细的研究下x86架构下cache的组织方式吧。

我们就由这个问题开始讨论吧。这个问题说为什么转置一个512×512的矩阵反倒比513×513的矩阵要慢?(不知道什么是矩阵转置的童鞋补习线性代数去)提问者给出了测试的代码以及执行的时间。

不过我们不知道提问者测试机器的硬件架构,不过我的测试环境就是我这台笔记本了,x86架构,处理器是Intel Core i3-2310M 2.10GHz。顺便啰嗦一句,在linux下,直接用cat命令查看/proc/cpuinfo这个虚拟文件就可以查看到当前CPU的很多信息。

Read More

博客很久都没有更斯了,因为一直在忙于一个小项目的开发。

事情的起因是这样的:因为今年开设了操作系统课程,但是纯粹的理论学习始终给我一种漂浮在云中的感觉。为了能在实践中深刻理解操作系统的运行机制和x86CPU以及硬件原理,我决定自己动手写一个操作系统内核的Demo程序。

当然,一开始没有相关的基础自然要找资料去学习。在翻阅了于渊的《Orange’s 一个操作系统的实现》和川合秀实先生的《30天自制操作系统》后感觉这两本书都不是很适合初学者学习。前者体系略乱且在一开始就陷入了硬件机制的漩涡,容易让初学者找不到北;后者不需要初学者有足够的基础,但是在硬件机制等内容上过于简略。只适合一般的爱好者去使用,而作为计算机专业的同学只能作为参考。(肆意诋毁大神作品,罪过罪过……)

后来在Google上搜索到了《JamesM’s kernel development tutorials》这篇文档后,我立即被作者合理有序的安排所吸引。我理想中的教程就是这样的,应该一步一步逐渐搭建起整个系统原型,由“发现问题——寻找机制——建立策略”的流程来处理。而不是一股脑的告诉我们所有的硬件机制,然后才是一般性的实现策略。

我主张的学习方法就是先学习一个新事物的基础框架和基本的模式结构,而旁枝末节的细节问题完全可以交给实践去慢慢掌握。暂时用不到的东西就不要告诉读者,完全可以用到了再慢慢补充。同时以任务和实践的方式驱动学习过程,既提升了理论学习的速度,又充满了实践的乐趣和成就感。

不过我在实践的过程中逐渐发现了这篇教程还是过于简陋,很多地方知其然而不知其所以然,并且部分的代码存在BUG。虽然代码能在当前的测试中通过,但会导致复杂化后其它的模块出现问题。另外这里调试也存在问题,若是能实现内核代码级别的调试功能就能极大的方便学习和开发。

Read More

我们这次接着内存分页继续说。稍微插一句,虽然本系列的名字叫做保护模式汇编,可是到现在颇有些挂羊头卖狗肉的意味。我们只是在一个劲的谈理论,就连仅有的一点代码也是用C语言描述的,而不是汇编。不过我觉得这不是关键,我觉得只要我们掌握了理论就好,至于用什么语言描述都是次要的,你说呢?

言归正传,我们开始说分页机制。长时间以来,随着计算机技术的发展,存储器的容量在不断的高速增加着。但是说起内存(这里指RAM,下同)这个东西,它有一个很奇葩的特性,就是无论它有多大,都总是不够用(P.S.厨房的垃圾桶也一样)。现在我们看似拥有着以前的程序员想都不敢想的“天文数字”的内存,动辄就是几G十几G的。但是相信我,历史总是嘲弄人的。就像当年程序员们质疑32位地址线带来的4GB空间太大没有意义似的,我们也会有一天抱怨现在的内存太小的。

那么,既然内存总是不够用的,那内存不够用了怎么办?还有,使用过程中出现的内存碎片怎么办?假设我们有4GB的物理内存,现在有1、2、3、4一共4个程序分别各占据连续的1G内存,然后2、4退出,此时我们拥有着空闲的两段内存,却连一个稍大于1GB的程序都无法载入了。

当然了,这只是一个例子。不过按照一般的思路,在内存释放之后,我们如何回收呢?做碎片整理吗?即便我们不在乎整理过程带来的效率损失,光是程序加载时候的地址逐一重定位就是及其麻烦的。那怎么办?当然了,解决的办法是有的,聪明的计算机工程师们想到了采用分页的方式来管理物理内存。他们在逻辑上把内存划分为定长的物理页,同时将一个程序执行时候的线性地址地址空间划分为逻辑页,在分页机制工作的前提下,给硬件提供一组数据结构来保存这种映射关系。也就是说,线性地址是连续的,但是其实际指向的物理地址就不见得是连续的了。别忘了,RAM是随机存储器,读取任意一个地址的理论时间都是一样的(暂时让我们忘了cache吧…)。我们让CPU在寻址的时候,自动的去查找线性地址到物理地址的映射关系,从而找到实际的数据就好。严格说地址翻译是由MMU组件来进行的,但是现在MMU一般都是CPU的一个组成部分了,所以我们也不严格区分了。

Read More