本节书摘来自异步社区《UNIX环境高级编程(第3版)》一书中的第1章,第1.6节,作者:【美】W. Richard Stevens , Stephen A.Rago著,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.6 程序和进程
1.程序
程序(program)是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数(7个exec函数之一),将程序读入内存,并执行程序。8.10节将说明这些exec函数。
2.进程和进程ID
程序的执行实例被称为进程(process)。本书的每一页几乎都会使用这一术语。某些操作系统用任务(task)表示正在被执行的程序。
UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一个非负整数。
实例
图1-6程序用于打印进程ID。
#include "apue.h"
int
main(void)
{
printf("hello world from process ID %ld\n", (long)getpid());
exit(0);
}
图1-6 打印进程ID
如果将该程序编译成a.out文件,然后执行它,则有:
$ ./a.out
hello world from process ID 851
$ ./a.out
hello world from process ID 854
此程序运行时,它调用函数getpid得到其进程ID。我们将会在后面看到,getpid返回一个pid_t数据类型。我们不知道它的大小,仅知道的是标准会保证它能保存在一个长整型中。因为我们必须在printf函数中指定需要打印的每一个变量的大小,所以我们必须把它的值强制转换为它可能会用到的最大的数据类型(这里是长整型)。虽然大多数进程ID可以用整型表示,但用长整型可以提高可移植性。
3.进程控制
有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体,但经常把它们统称为exec函数。)
实例
UNIX系统的进程控制功能可以用一个简单的程序说明(见图1-7)。该程序从标准输入读取命令,然后执行这些命令。它类似于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 从标准输入读命令并执行
在这个30行的程序中,有很多功能需要考虑。
用标准I/O函数fgets从标准输入一次读取一行。当键入文件结束符(通常是Ctrl+D)作为行的第一个字符时,fgets返回一个null指针,于是循环停止,进程也就终止。第18章将说明所有特殊的终端字符(文件结束、退格字符、整行擦除等),以及如何改变它们。
因为fgets返回的每一行都以换行符终止,后随一个null字节,因此用标准C函数strlen计算此字符串的长度,然后用一个null字节替换换行符。这样做是因为execlp函数要求的参数是以null结束的而不是以换行符结束的。
调用fork创建一个新进程。新进程是调用进程的一个副本,我们称调用进程为父进程,新创建的进程为子进程。fork对父进程返回新的子进程的进程ID(一个非负整数),对子进程则返回0。因为fork创建一个新进程,所以说它被调用一次(由父进程),但返回两次(分别在父进程中和在子进程中)。
在子进程中,调用execlp以执行从标准输入读入的命令。这就用新的程序文件替换了子进程原先执行的程序文件。fork和跟随其后的exec两者的组合就是某些操作系统所称的产生(spawn)一个新进程。在UNIX系统中,这两部分分离成两个独立的函数。第8章将对这些函数进行更多说明。
子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这是通过调用waitpid实现的,其参数指定要等待的进程(即pid参数是子进程ID)。waitpid函数返回子进程的终止状态(status变量)。在我们这个简单的程序中,没有使用该值。如果需要,可以用此值准确地判定子进程是如何终止的。
该程序的最主要限制是不能向所执行的命令传递参数。例如不能指定要列出目录项的目录名,只能对工作目录执行ls命令。为了传递参数,先要分析输入行,然后用某种约定把参数分开(可能使用空格或制表符),再将分隔后的各个参数传递给execlp函数。尽管如此,此程序仍可用来说明UNIX系统的进程控制功能。
如果运行此程序,将得到下列结果。注意,该程序使用了一个不同的提示符(%),以区别于shell的提示符。
$ ./a.out
% date
Sat Jan 21 19:42:07 EST 2012
% who
sar console Jan 1 14:59
sar ttys000 Jan 1 14:59
sar ttys001 Jan 15 15:28
% pwd
/home/sar/bk/apue/3e
% ls
Makefile
a.out
shell1.c
% ^D 键入文件结束符
$ 常规的shell提示符
^D表示一个控制字符。控制字符是特殊字符,其构成方法是:在键盘上按下控制键——通常被标记为Control或Ctrl,同时按另一个键。Ctrl+D或^D是默认的文件结束符。在第18章中讨论终端I/O时,会介绍更多的控制字符。
4.线程和线程ID
通常,一个进程只有一个控制线程(thread)——某一时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力。
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据时需要采取同步措施以避免不一致性。
与进程相同,线程也用ID标识。但是,线程ID只在它所属的进程内起作用。一个进程中的线程ID在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时,我们可以使用该线程的ID引用它。
控制线程的函数与控制进程的函数类似,但另有一套。线程模型是在进程模型建立很久之后才被引入到UNIX系统中的,然而这两种模型之间存在复杂的交互,在第12章中,我们会对此进行说明。