树莓派开始,玩转Linux14:从程序到进程
计算机不止是存储数据的仓库,它还可以进行多种多样的活动,比如收发电子邮件、播放电影、陪人们下棋。应用程序给计算机带来了丰富的动作。Linux系统在应用层面的活动都以"进程"为单位进行。本章我们将初探进程。
1.指令:
计算机实际上可以做的事情非常简单。我们给CPU发出指令(Instruction),CPU就会执行这些基础动作。
指令通常由一串二进制的序列构成。
CPU会识别并执行这些指令。
每一款CPU都有一套指令集,比如ARM CPU使用的精简指令集。
一条指令能做到的事情很少,比如计算寄存器中两个数的和,又如把内存中的数据移入寄存器。寄存器(Register)是CPU的临时存储空间。
在树莓派中,即使是计算内存中两个数的和,也需要多条指令。
· 指令1:把内存1号地址的数值放入寄存器a。
· 指令2:把内存2号地址的数值放入寄存器b。
· 指令3:对寄存器a和寄存器b中的数值求和,放入寄存器a。
· 指令4:把寄存器a中的结果放入内存3号地址。
整个过程就像是厨师做番茄炒蛋。厨师要先从库房的两个货架上分别拿来番茄和蛋放在案板上,然后用锅把两种原料炒在一起。在计算机中,内存像是库房,寄存器像是案板,而CPU中的运算单元就是炒菜锅,如图1所示。
除了搬运数据和计算,CPU指令还可以控制计算机内部的其他硬件乃至外设。
早期程序员必须背熟CPU的指令集,然后用指令写程序。那个时候的程序员必须把任务分解成一条条CPU可以直接理解的指令。这样的程序称为机器程序(Machine Code)。机器程序可以用汇编语言
(Assembly Language)编写。比如加法程序可以用汇编语言写出来:
AX和BX代表了寄存器的两个位置,而[20H]和[10H]是内存中的两个位置。汇编程序里的每一行都直接对应了一条CPU可以解读的指令。MOV表示移动数据,ADD表示相加。程序会把内存中的两条数据移入寄存器,计算两条数据的和,再把结果移回内存。注意,这段程序简化了很多内容。根据CPU的不同,相同功能的汇编程序也会不同。
无论如何,我们已经从上面的汇编程序看到一种编程方式。写汇编程序时,程序员说明硬件级别的动作,所以汇编程序运行起来很快。这就像是赛车手开手动挡的汽车,可以充分发挥出汽车的性能,驾驶得更流畅,也更加省油。但是,一旦所要实现的功能变复杂,那么汇编程序需要的指令数会快速增加。此外,由于程序在指令使用上没有限制,也经常会造成很多错误。这也如手动挡汽车,一旦挂挡不当,就很容易熄火。
2.C语言:
程序员一边痛苦地写着汇编程序,一边探索简化程序编写的方法。他们很快发现,汇编程序的语句之间会有某些固定的组合模式。比如,计算机经常会重复执行某些特定任务。就拿高斯求和来说,从1开始,加2,加3…一直加到100。在整个过程中,程序就是反复执行相同的加法操作。
那么与其手动重复相同的指令,不如用"循环"这样的语法来表示重复执行的任务,让计算机自己去完成重复。因此,程序员们发明了高级语言,用一些特殊的语法来抽象某些常见的指令组合。Linux系统的大部分程序,正是由C语言这一高级语言写成的。
先来看一个C语言的程序文件,这个文件的名字是demo.c,你可以
用nano来写C语言文件:
C程序用{}来表示一个程序块。上述程序定义了函数main。函数(Function)是对一个程序块的抽象。函数main是C语言中的特殊函数。当程序运行时,会自动调用其中的main函数。所谓的函数调用就是执行函数包含的程序块。函数运行完成后会有一个返回值(Return
Value)。正如main前面的int表明的,main函数的返回值是整数类型(Integer)。
函数main的调用是自动进行的。除此之外,其他函数必须在某个语句中被调用,才可以执行。比如,函数main中调用了函数printf。函数printf会在屏幕上打印括号中的内容。当main函数执行到这一句时,printf函数就会被调用。写程序时,只要写清楚函数名,就可以调用一个功能复杂的程序块。
函数中创建了3个变量(Variable),分别用字母a、b、c表示。变量用于存储特定类型的数据。3个变量都是整数类型,用int表示,可以用来存储11、-24或1986这样的整数。变量是主存储器的一块空间,可用来存储数据。3个整型变量可以存储3个整数。
创建了变量之后,我们就可以往变量中读取或写入数据,例如:
就读取变量a和变量b中的数据,将它们相加,再存入变量c。"="是赋值(Assignment)符号,用于把数据写入变量。
这段程序的功能,就是计算整数相加的结果,并以特定格式打印出来:
我们再来看高斯求和的实现方式:
这一段C程序,和16.1节中的汇编语言的功能类似,但语法上区别显著。一方面,高级语言提供了很多便利。比如函数语法允许程序员来代表复杂的程序块,从而提高了程序的可复用性。另一方面,高级语言也给出了很多限制,比如通过变量类型,给不同的变量以特定的内存空间,从而减少了出错的可能。通过提供便利和限制,高级语言提高了程序员的生产力。
3.程序编译:
因为C语言中包含了很多抽象语法,所以计算机不能直接理解C语言的语法。高级C语言程序必须先编译成汇编程序,再转成机器程序运行。我们可以用gcc来编译一个C程序,比如:
编译完成后,当前目录下会出现一个名为a.out的二进制可执行文
件。用下面的方式执行该文件:
程序运行,在Shell中打印:
我们看到,计算机按照程序的描述执行了特定的活动,即计算两个
数的和并打印出来。
C语言的编译是把程序员可读的C语言文本,翻译成计算机可读的机器程序。编译产生的a.out就是可执行文件,内含二进制文本。计算机可以直接读懂并执行该程序。这个二进制文本其实就是指令式的程序。通过apt-get下载的应用,大部分都是已经编译好的二进制可执行文件。
我们可以直接使用这些程序,免去了编译的步骤。但有些时候,软件商店中没有我们需要的软件,那么我们不得不从源代码出发,对程序进行编译。
大部分Linux应用的编译都比上面例子的过程复杂。一般来说,源代码中还包含了编译大型工程所需的辅助文件。按照惯例,一般会有一个名为configure的脚本用于设置。
编译的第一步,就是运行该脚本,根据提示进行设置:
随后,你需要运行make命令:
命令make会根据工程中的Makefile来解析代码文件之间的依赖关系。通常来说,一个工程会包含多个C语言程序。由于C语言可以跨文件地调用函数和变量,因此在编译时,代码文件之间相互依赖。命令make会根据依赖关系来编译文件。
最后,把编译好的二进制可执行文件放到configure设定的目标路径中:
4.看一眼进程:虽然程序规定了活动的动作,但是应用程序并不等于进程(Process)。
进程是程序的一个具体实现。我们只有运行程序,才能产生一个进程。程序和进程的关系,类似于食谱和做菜的关系。
对于一个厨师来说,只有食谱没什么用,只有按食谱的指点一步步实行,才能做出菜肴。进程是执行程序的过程,类似于按照食谱真正去做菜的过程。
在Linux系统中,我们可以用ps命令来查询正在运行的进程:
在这个命令中,-e选项表示列出全部进程,-eo pid,cmd选项表示
我们需要的信息。执行结果的一部分示例如表所示。
在ps返回的结果中,每一行代表了一个进程。每一行又分为两列。
第一列是一个整数,即进程ID(PID,Process IDentity)。每一个进程都有唯一的PID来代表自己的身份。无论是内核,还是其他进程,都可以根据PID来识别出该进程。
第二列CMD是进程所对应的程序,以及运行时传递给程序的参数。第二列中有一些由中括号括起来的。它们是内核的一部分功能,被打扮成进程的样子以方便操作系统管理。
此外,PID为1的进程一定是由/sbin/init程序运行而形成的。当Linux启动的时候,init是系统创建的第一个进程,这个进程会一直存在,直到关闭计算机。
其他的进程就是常见的应用进程,例如cron进程和ps进程。
同一个程序可以执行多次,从而产生多个进程。
即使一个程序的进程还没有完成,我们还是可以用同一程序运行出更多的进程。
操作系统的一个重要功能就是管理进程,为进程提供必需的计算机资源,比如,为进程分配内存空间,管理进程的相关信息等。