最近在重新翻阅《Unix环境高级编程》的时候,被书上的一段例程所困扰,那段代码是分别在主线程和子线程中使用 getpid() 函数打印进程标识符PID,书上告诉我们是不同的值,但是测试结果是主线程和子线程中打印出了相同的值。

在我的印象中《Linux内核设计与实现》这本书曾经谈到线程时如是说:从内核的角度来说,它并没有线程这个概念。Linux内核把所有的线程都当成进程来实现……在内核中,线程看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间)。

《Unix环境高级编程》第二版著书时的测试内核是2.4.22,而《Linux内核设计与实现》这本书是针对2.6.34内核而言的(兼顾2.6.32),而我的内核是3.9.11,难道是内核发展过程中线程的实现发生了较大的变化?百度一番之后发现资料乱七八糟不成系统,索性翻阅诸多文档和网页,整理如下。如有偏差,烦请大家指正。

在 Linux 创建的初期,内核一直就没有实现“线程”这个东西。后来因为实际的需求,便逐步产生了LinuxThreads 这个项目,其主要的贡献者是Xavier Leroy。LinuxThreads项目使用了 clone() 这个系统调用对线程进行了模拟,按照《Linux内核设计与实现》的说法,调用 clone() 函数参数是clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0),即创建一个新的进程,同时让父子进程共享地址空间、文件系统资源、文件描述符、信号处理程序以及被阻断的信号等内容。也就是说,此时的所谓“线程”模型符合以上两本经典巨著的描述,即在内核看来,没有所谓的“线程”,我们所谓的“线程”其实在内核看来不过是和其他进程共享了一些资源的进程罢了。

通过以上的描述,我们可以得到以下结论:

  1. 此时的内核确实不区分进程与线程,内核没有“线程”这个意识。
  2. 在不同的“线程”内调用 getpid() 函数,打印的肯定是不同的值,因为它们在内核的进程链表中有不同的 task_struct 结构体来表示,有各自不同的进程标识符PID。

Read More

从文章的题目我们就知道今天是以一个进程的角度来看待自身的运行环境。我们先提出第一个问题,什么是进程?对于这个问题,各种参考资料上给出的定义都显得过于抽象而难以理解,下面是我自己的定义:

进程是一个动态的概念,它是静态的可执行文件执行过程的描述,其包含了一个静态程序运行时的状态和其所占据的系统资源的总和。

还是很抽象吗?那么,我们可以这样比喻,如果说菜谱是程序代码,厨具是硬件的话,那么炒菜的整个过程就是一个进程。这下理解了吧?那我们继续。

每个程序在启动之后都会拥有自己的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机平台决定,具体一点说由操作系统的位数和CPU的地址总线宽度所决定,其中CPU的地址总线宽度决定了地址空间的理论上限(先不考虑主板…)。

比如32位的硬件平台可编址范围就是0x00000000~0xFFFFFFFF,即就是4GB。而64位的硬件平台达到了理论上0x0000000000000000~0xFFFFFFFFFFFFFFFF的寻址空间,即就是17179869184GB的大小(事实上我自己的64位 Intel Core i3 处理器也仅有36位地址总线而已,因为暂时用不到那么大的物理地址范围)。

为了行文的简单,我就以32位硬件平台来描述吧(事实上我对64位所知甚少,不敢信口开河…),同时指定环境为32位的Linux操作系统。

可能看到这里你反而更迷惑了,我一直在说一个进程拥有4GB的线性地址空间(以下只讨论32位),可是操作系统上同时在运行着N个进程,难不成每个都有4GB的线性地址空间不成?没错,每个都有。我们一直在使用术语“线性地址空间”而非“主存储器(内存)”,因为线性地址空间并非和主存等价。我们平时只要一提到“地址”这个概念,想必大家自然而然的就想到了主存储器。但事实上并非线性地址就一定指向主存储器的物理地址,如果你对“线性地址空间”不理解的话,我建议你先去看看我的另一篇博文《基于Intel 80×86 CPU的IBM PC及其兼容计算机的启动流程》。

Read More

上回书我们说到了链接以前,今天我们来研究最后的链接问题。

链接这个话题延伸之后完全可以跑到九霄云外去,为了避免本文牵扯到过多的话题导致言之泛泛,我们先设定本文涉及的范围。我们今天讨论只链接进行的大致步骤及其规则、静态链接库与动态链接库的创建和使用这两大块的问题。至于可执行文件的加载、可执行文件的运行时储存器映像之类的内容我们暂时不讨论。

首先,什么是链接?我们引用CSAPP的定义:链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。

需要强调的是,链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行。

说了这么多,了解链接有什么用呢?生命这么短暂,我们干嘛要去学习一些根本用不到的东西。当然有用了,继续引用CSAPP的说法,如下:

  1. 理解链接器将帮助你构造大型程序。
  2. 理解链接器将帮助你避免一些危险的编程错误。
  3. 理解链接将帮助你理解语言的作用域是如何实现的。
  4. 理解链接将帮助你理解其他重要的系统概念。
  5. 理解链接将使你能够利用共享库。
    ……

言归正传,我们开始吧。为了避免我们的描述过于枯燥,我们还是以C语言为例吧。想必大家通过我们在上篇中的描述,已经知道C代码编译后的目标文件了吧。目标文件最终要和标准库进行链接生成最后的可执行文件。那么,标准库和我们生成的目标文件是什么关系呢?

Read More

有位学弟想让我说说编译和链接的简单过程,我觉得几句话简单说的话也没什么意思,索性写篇博文稍微详细的解释一下吧。其实详细的流程在经典的《Linkers and Loaders》和《深入理解计算机系统》中均有描述,也有国产的诸如《程序员的自我修养——链接、装载与库》等大牛著作。不过,我想大家恐怕很难有足够的时间去研读这些厚如词典的书籍。正巧我大致翻阅过其中的部分章节,干脆也融入这篇文章作为补充吧。

我的环境:Fedora 16 i686 kernel-3.6.11-4 gcc 4.6.3

其实MSVC的编译器在编译过程中的流程是差不多的,只是具体调用的程序和使用的参数不同罢了。不过为了描述的流畅性,我在行文中不会涉及MSVC的具体操作,使用Windows的同学可以自行搜索相关指令和参数。但是作为Linuxer,我还是欢迎大家使用Linux系统。如果大家确实需要,我会挤时间在附言中给出MSVC中相对应的试验方法。

闲话不多说了,我们进入正题。在正式开始我们的描述前,我们先来引出几个问题:

  1. C语言代码为什么要编译后才能执行?整个过程中编译器都做了什么?
  2. C代码中经常会包含头文件,那头文件是什么?C语言库又是什么?
  3. 有人说main函数是C语言程序的入口,是这样吗?难道就不能把其它函数当入口?
  4. 不同的操作系统上编译好的程序可以直接拷贝过去运行吗?

如果上面的问题你都能回答的话,那么后文就不用再看下去了。因为本文是纯粹的面向新手,所以注定了不会写的多么详细和深刻。如果你不知道或者不是很清楚,那么我们就一起继续研究吧。

Read More