1.6 程序和进程
程序
程序是存在于磁盘上目录中的可执行文件。6个exec函数中的任意一个,都可以将一个程序读入内存中并由内核执行(感觉这句没有翻译好,原句是:A program is read into memory and is executed by the kernel as a result of one of the six exec functions.)。我们将在8.10节中介绍这些函数。
进程和进程ID
一个正在执行中的程序实例被称为进程(process),该词语(进程)几乎会出现在本书中的每一页。一些操作系统用任务(task)来称呼一个正在运行中的程序。
UNIX系统确保每一个进程都拥有一个唯一的数字标识符,称为进程ID。进程ID总是非负整数。
例子
图1.6中的程序打印出它的进程ID。
如果我们把程序编译成a.out并执行它,我们会看到
$ ./a.out
hello world from process ID 851
$ ./a.out
hello world from process ID 854
该程序运行时调用getpid函数来获得进程ID。
1 #include "apue.h"
2
3 int
4 main(void)
5 {
6 printf("hello world from process ID %d\n", getpid());
7 exit(0);
8 }
图
1.6 打印进程ID
进程控制
进程控制主要使用三个函数:fork,exec和waitpid。(exec函数有6个变体,我们通常把它们统称为exec函数。)
例子
使用一个简单的程序(图1.7)来展示UNIX系统的进程控制特性,该程序从标准输入读取命令并且执行这些命令。这是一个类似shell程序的本质(翻译得不好,原句是:This is a bare-bones implementation of a shell-like program.)。在这个30行的程序中有许多特性值得思考。
l 使用标准I/O函数fgets,一次从标准输入中读取一行。当输入文件终止符(通常是Control-D)作为一行的第一个字符时,fgets返回null指针,同时终止循环,接着终止进程。在18章中,我们描述所有特殊的终端字符(文件终止符,退格字符,整行擦除字符等等),并且介绍怎样改变它们。
l fgets返回的每一行都终止于一个换行符和跟在换行符后面的一个null字节,我们使用标准的C函数strlen来计算字符串的长度,接着把换行符替换为一个null字节。这样做是因为execlp函数需要一个以null结束的参数,而不是以换行符结束的参数。
l 调用fork函数来建立一个进程,这个进程是一个调用者的拷贝。我们把调用者称为父进程,把新建立的进程称为子进程。那么fork函数返回子进程的非负进程ID给父进程,同时返回0给子进程。因为fork创建了一个新进程,我们说fork被调用一次就返回两次,一次返回给父进程,一次返回给子进程。
l 在子进程中,调用execlp来执行从标准输入读取的命令。这就把子进程替换为一个新的程序文件。fork函数后跟exec函数的组合,就是一些操作系统所谓的产生一个新进程。(翻译得不好,原句是:This replaces the child process with the new program file. The combination of a fork, followed by an exec, is what some operating systems call spawning a new process.)在UNIX系统中,这两部分被分为了独立的函数。第八章中将会介绍更多这方面的内容。
l 因为子进程调用execlp来执行新的程序文件,父进程就会等待直到子进程结束。这些工作由waitpid函数完成。waitpid函数中的pid参数代表了子进程的进程ID,该参数用来标识出需要等待的进程。waitpid函数也可以得到子进程的终止状态。在这个简单程序中的的status变量就代表了子进程的终止状态,在程序中我们没有使用这个值,但是我们可以通过检查这个值来确定子进程的终止状态。
l 这个程序最根本的限制在于我们不能像我们所执行的命令传递参数。例如,不能列举特定的目录。我们只可以在工作目录执行ls命令。为了能够传递参数,我们需要分析输入行,按某种习惯(可能的情况是使用空格或者制表符)区别出参数,接着把每一个参数作为独立的参数传递给execlp函数。尽管如此,这个程序仍然对UNIX系统进程控制函数进行了有用说明。
运行这个程序,我们得到下面的输出。注意我们的程序有一个不同的提示符,使用百分号(%)来区别于shell的提示符。
$ ./a.out
% date
Sun Aug 1 03:04:47 EDT 2004
% who
sar :0 Jul 26 22:54
sar pts/0 Jul 26 22:54 (:0)
sar pts/1 Jul 26 22:54 (:0)
sar pts/2 Jul 26 22:54 (:0)
% pwd
/home/sar/bk/apue/2e
% ls
Makefile
a.out
shell1.c
% ^D 输入文件终止符
$ shell提示符
#include "apue.h"
#include <sys/wait.h>
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
图 1.7 从标准输入读取命令并执行它们
符号^D用代表一个控制字符。控制字符是一种特殊的的字符,可以通过同时按下控制键(在你的计算机上通常为Control键或Ctrl键)和另一个键来产生它。Control-D,或者说^D,是默认的文件终止符。在18章讨论终止I/O的时候会看到更多的控制字符。
线程和线程ID
通常,一个进程只有一个线程(原句是:Usually, a process has only one thread of control one set of machine instructions executing at a time.不知道怎么翻译,留给大虾翻译)。当有多于一个的线程来控制不同部分时,一些问题就变得很容易解决。另外,多线程在多处理器系统上能够获得平衡性(又一句翻译得不爽:Additionally, multiple threads of control can exploit the parallelism possible on multiprocessor systems.)。
一个进程中的所有线程共享同一个地址空间,文件描述符,栈和进程相关的属性。因为能够访问相同的内存,线程必须同步访问它们自己的共享数据,以避免冲突。
和进程一样,线程由线程ID标识。尽管如此,线程ID对于进程ID来说是本地的。也就是说,一个进程中的线程ID在另一个进程中是没有意义的。当我们在进程中操作线程的时候,使用线程ID来指出特定的线程。
控制线程的函数和控制进程的函数是一样的。在进程模型建立很久以后,线程才被加入到UNIX系统中,然而,线程模型和进程模型之间有一些复杂的交互,在12章将会看到这些。