C语言程序员编写的代码可以编译为程序,程序通常存放在磁盘等存储介质中。在 Linux 中,处于运行期的程序被称作“进程”。
进程
虽说进程是处于运行期的程序,但是进程并不仅仅局限于可执行的C语言代码(Linux 称其为代码段,text section),它还包括其他资源,例如用于存放全局变量的数据段(data section)、具有内存映射的内存地址空间、要处理的数据、挂起的信号、打开的文件,可能还会包括多个执行线程等等。
事实上,进程是 Linux 操作系统抽象概念的最基本的一种,Linux 最基础最重要的工作之一就是管理系统中繁杂的各种进程。
上面提到的“执行线程”通常被简称为“线程”,它被进程包含,同一个进程可能有多个线程,每个线程都有自己独立的程序计数器、进程栈以及相关的进程寄存器。虽说 Linux 内核管理的是进程,但其实最小的调度单位是线程。
早期传统的 Unix 系统中,一个进程只能包含一个线程,所以当时进程调度和线程调度其实结果是一致的。
线程
如今的操作系统中,进程包含多个线程是非常常见的。Linux 与一些其他操作系统不同,它对进程和线程并不明确区分,对于 Linux 来说,线程不过是一种比较特殊的进程而已。
包括 Linux,现代操作系统一般都会为进程提供两种虚拟机制:虚拟处理器和虚拟内存。读者应注意“虚拟”一词,多个进程可能共同使用一个 CPU 和内存,但是“虚拟机制”会让进程活在楚门的世界一样,自以为自己独占 CPU 和全部内存。
应注意,线程之间可以共享虚拟内存,但是它们仍然拥有各自的虚拟 CPU。
到这里,读者应该明白了,编译器生成的C语言程序本身并不是进程。进程实际上是处于运行期的程序,与相关资源的总和。
事实上,无论是程序不同,还是执行时的数据不同,都会产生不同的进程。举例来说,同样一个C语言程序,是可以产生两个不同的进程的——它们的运行资源可能是不同的。反过来也是一样的,多个不同的进程也可以共享同一份资源,例如打开同一个文件,映射同一块内存空间等。
进程的产生与消亡
可能有读者认为,直接执行一个程序不就产生新进程了吗?这样说没有错,但是不够本质。在 Linux 操作系统中,通过 fork() 系统调用可以复制现有进程,并产生新进程。调用 fork() 的进程一般被称作“父进程”,而 fork() 产生的新进程则被称作“子进程”。
fork() 调用结束时,会从 Linux 内核返回两次:一次返回到父进程,父进程恢复运行。一次返回到子进程,子进程开始执行,在这之后,父子进程的进程资源彼此隔离,不再共享。
不过一般来说,如果有需求创建新的进程,一般都是为了执行不同的新的程序。这一过程通过 exec() 函数族可以方便实现,它们可以为新程序创建新的地址空间,然后加载程序执行。
Linux 操作界面的 shell 终端其实也是一个进程,通过 shell 输入的执行新程序命令(如 ./a.out )产生的新进程其实都是对应 shell 终端的子进程。
程序既然有新生,也就会有死亡,程序运行结束后,通过 exit() 系统调用退出运行,这个函数会杀死进程,并且将其占用的资源释放,通知其父进程“死亡信息”。父进程则可以通过 wait() 函数族接收子进程的“死亡信息”,并着手为子进程做后续的“收尸工作”,避免子进程编程“僵尸进程(zombie)”。
“僵尸进程”很难杀死,但是留着“僵尸进程”又会白白浪费系统资源。