进程是UNIX系统中仅次于文件的基本抽象概念,当目标代码执行时,正在运行的进程不仅仅是汇编代码,而是由数据、资源、状态和虚拟的计算机组成。
1、程序、进程和线程区分
程序(program)是指编译过的、可执行的二进制代码,保存在存储介质如磁盘上,不运行。规模很大的二进制程序集可以称为应用。
进程(process)是指正在运行的程序。进程包括二进制镜像,加载到内存中,还涉及很多其他方面:虚拟内存示例、内核资源如打开的文件、安全上下文关联的用户,以及一个或多个线程。
线程(thread)是进程内的活动单元。每个线程包含自己的虚拟存储器,包括栈、进程状态如寄存器,以及指令指针。
在单线程的进程中,进程即线程。一个进程只有一个虚拟内存实例,一个虚拟处理器。在多线程的进程中,一个进程有多个线程。由于虚拟内存是和进程关联的,所有线程会共享相同的内存地址空间。
2、进程ID
每个进程都是由一个唯一的标识符表示的,即进程ID,简称pid。系统保证在任意时刻pid都是唯一的。
空闲进程(idle process),即当没有其他程序在运行时,内核所运行的进程,其pid值为0。在启动后,内核运行的第一个进程称为init进程,其pid值为1。一般来说,Linux中init进程就是init程序。"init"这个术语不但表示内核运行的第一个进程,也表示完成该目的的程序名称。
除非用户显示告诉内核要运行哪个程序(通过init内核命令行参数),否则内核就必须自己指定合适的init程序,这种情况很少见,是内核策略的一个特例。
Linux内核会尝试四个可执行文件,顺序如下:
(1)、/sbin/init:init最可能存在的地方,也是期望存在的地方。
(2)、/etc/init:Intit另一个可能存在的地方。
(3)、/bin/init:init可能存在的位置。
(4)、/bin/sh:Bourne Shell所在的位置,当内核没有找到init程序时,就会尝试运行它。
在以上四个可能位置中,最先被发现的就会当作init运行。如果四个运行都失败了,内核就会报警,系统挂起。内核交出控制后,init会接着完成后续的启动过程。一般而言,这个过程包括初始化系统、启动各种服务以及启动登录进程。
2.1 分配进程ID
缺省情况下,内核将进程ID的最大值设置为32768,这是为了和老的UNIX系统兼容,因为这些系统使用了有符号16位数来表示进程ID,可以通过修改/proc/sys/kernel/pid_max把这个值设置成更大的值,但是会牺牲一些兼容性。
内核分配进程ID是以严格的线性方式执行的。如果当前pid的最大值是17,那么分配给新进程的pid值就为18,即使当新进程开始运行时,pid为17的进程已经不再运行了。内核分配的pid值达到了/proc/sys/kernel/pid_max之后,才会重用以前已经分配过的pid值。因此,尽管内核不保证长时间的进程ID的唯一性,但这种分配方式至少可以保证pid在短时间内是稳定且唯一的。
2.2 进程体系
创建新进程的那个进程称为父进程,而新进程被称为子进程。每个进程都是由其他进程创建的(除了init进程),因此每个子进程都有一个父进程。这种关系保存在每个进程的父进程ID号(ppid)中。
每个进程都属于某个用户和某个组。这种从属关系可以用来实现访问控制。对于内核来说,用户和组都不过是些整数值。通过/etc/passwd和/etc/group这两个文件,这些整数被映射成人们易读的形式。每个子进程都继承了父进程的用户和组。
每个进程都是某个进程组(process group)的一部分,进程组表示的是该进程和其他进程之间的关系,不应混淆。子进程通常属于其父进程所在的那个进程组。此外,当通过shell建立管道时(如用户输入了命令ls | less),所有和管道相关的命令都是同一个进程组。进程组这个概念使得在管道上的进程之间发送信号或者获取信息变得很容易,同样,也适用于管道中的子进程。从用户角度来看,进程组和作业(iob)是紧密关联的。
2.3 pid_t
进程ID是由数据类型pid_t来表示的,pid_t在头文件<sys/types.h>中定义,在Linux中,pid_t通常定义成C语言的int类型。
2.4 获取进程ID和父进程ID
系统调用getpid()会返回调用进程的进程ID,用法如下:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
系统调用getppid()会返回调用进程的父进程ID,用法如下:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
这两个系统调用都不会返回错误。
3、运行新进程
在UNIX中,把程序载入内存并执行程序映像的操作与创建新进程的操作是分离的。一次系统调用会把二进制程序加载到内存中,替换地址空间原来的内容,并开始执行。这个过程称为“执行(executing)”一个新的程序,是通过一系列exec系统调用来完成的。
同时,另一个不同的系统调用是用于创建一个新的进程,它基本上相当于复制其父进程。通常情况下,新的进程会立即执行新的程序。创建新进程的操作称为派生(fork),是系统调用fork()来完成这个功能。在新进程中执行一个新的程序需要两个步骤:首先,创建一个新的进程,然后,通过exec系统调用把新的二进制程序加载到改进程中。
3.1 exec系统调用
不存在单一的exec函数,而是基于单个系统调用,由一系列的exec函数构成。我们先来看看其中最简单的调用execl():
#include <unistd.h>
int execl(const char *path,
const char *arg,
...);