目录
一、前言
二、Starting New Processes
1、system1.c
2、Front Contents
(1)Replacing a Process Image(更换进程镜像)
(2)pexec.c
(3)Duplicating a Process Image(复制进程映像)
(4)fork1.c
3、Waiting for a Process
(1)wait.c
4、Zombie Processes
(1)fork2.c
5、Input and Output Redirection
(1)upper.c
(2)useupper.c
6、Threads
一、前言
进程和信号是 Linux 操作环境的基本组成部分。它们控制 Linux 和所有其他类 unix 计算机系统执行的几乎所有活动。了解 Linux 和 UNIX 如何管理进程对任何系统程序员、应用程序程序员或系统管理员都有好处。
在本中,我们将学习如何在 Linux 环境中处理进程,以及如何找出计算机在任何给定时间正在做什么。我们还将了解如何从我们自己的程序中启动和停止其他进程,如何使进程发送和接收消息,以及如何避免僵尸进程。特别是,我们将了解:
(1)进程的结构、类型和调度;
(2)以不同的方式启动新进程;
(3)父进程、子进程和僵尸进程;
(4)什么是信号以及如何使用它们。
二、Starting New Processes
我们可以使用系统库函数使一个程序在另一个程序内部运行,从而创建一个新的进程。
系统函数以字符串的形式运行“传递给它的命令”,并等待它完成。执行该命令时,就像给一个 shell 下发了该命令一样。如果 shell 无法启动以运行该命令,则系统返回 127,如果发生另一个错误则返回 -1。否则,系统将返回命令的退出码。
1、system1.c
我们可以使用 system 来编写运行 ps 的程序。尽管这本身并不是非常有用,但是我们将在后面的示例中看到如何开发这种技术。(为了简单起见,我们没有在示例中检查系统调用是否实际工作。)
#include<stdlib.h>
#include<stdio.h>
int main()
{
printf("Running ps with system\n");
system("ps ax");
printf("Done.\n");
exit(0);
}
因为系统函数使用 shell 来启动所需的程序,我们可以通过更改 system1.c 中的函数调用来将其置于后台:
system(“ps ax &“);
How It Works:
在第一个例子中,程序用字符串"ps ax"调用 system,执行 ps 程序。当 ps 命令完成时,程序从对系统的调用返回。该系统功能可以相当有用,但也有局限性。因为程序必须等待,直到调用系统所启动的进程结束,所以无法继续执行其他任务。
在第二个示例中,shell 命令一完成,对系统的调用就返回。因为它是一个在后台运行程序的请求,所以一旦 ps 程序启动,shell 就会返回,就像在 shell 提示符下键入一样。system2 程序然后输出 Done。并在 ps 命令有机会完成其所有输出之前退出。ps 输出在 system2 退出后继续产生输出,在本例中不包含 system2 的条目。这种流程行为可能会让用户非常困惑。要很好地利用流程,我们需要对流程的操作进行更精细的控制。让我们看看用于进程创建的较低级别接口 exec。
通常,使用 system 并不是启动其他进程的理想方式,因为它使用 shell 调用所需的程序。这不仅效率低,因为 shell 是在程序启动之前启动的,而且非常依赖于所使用的 shell 和环境的安装。在之后的内容,我们将看到调用程序的一种更好的方法,这种方法几乎总是优先于系统调用。
2、Front Contents
(1)Replacing a Process Image(更换进程镜像)
在 exec heading 下有一整套相关的函数组。它们的不同之处在于启动进程和呈现程序参数的方式。exec 函数用 path 或 file 参数指定的新进程替换当前进程。我们可以使用 exec 函数将程序的执行“移交”给另一个人。例如,我们可以在启动另一个具有限制使用策略的应用程序之前检查用户的凭据。exec 函数比 system 函数更高效,因为在新程序启动后,原来的程序将不再运行。
这些函数属于两种类型。execl、execlp 和 execle接受以空指针结尾的可变数量的参数。execv 和 execvp 的第二个参数是一个字符串数组。在这两种情况下,新程序都以传递给 main 的 argv 数组中的给定参数开始。
这些函数通常是使用 execve 实现的,但没有要求一定要用这种方式实现。
名称以 p 结尾的函数的不同之处在于它们将搜索 PATH 环境变量以查找新的程序可执行文件。如果可执行文件不在路径上,则需要将包含目录的绝对文件名作为参数传递给函数。全局变量environ 可用于传递新程序环境的值。另外,execle 和 execve 函数的一个附加参数可用于传递用作新程序环境的字符串数组。如果你想使用一个 exec 函数来启动 ps 程序,你可以从六个exec家族函数中进行选择,如下面的代码片段中的调用所示:
#include <unistd.h>
/* Example of an argument list */
/* Note that we need a program name for argv[0] */
char *const ps_argv[] = {“ps”, “ax”, 0};
/* Example environment, not terribly useful */
char *const ps_envp[] = {“PATH=/bin:/usr/bin”, “TERM=console”, 0};
/* Possible calls to exec functions */
execl(“/bin/ps”, “ps”, “ax”, 0); /* assumes ps is in /bin */
execlp(“ps”, “ps”, “ax”, 0); /* assumes /bin is in PATH */
execle(“/bin/ps”, “ps”, “ax”, 0, ps_envp); /* passes own environment */
execv(“/bin/ps”, ps_argv);
execvp(“ps”, ps_argv);
execve(“/bin/ps”, ps_argv, ps_envp);
(2)pexec.c
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("Running ps with execlp\n");
execlp("ps","ps","ax",0);
printf("Done.\n");
exit(0);
}
当我们运行这个程序 pecec .c 时,我们会得到通常的 ps 输出,但没有 Done. 消息。还要注意,在输出中没有名为 pexec 的进程的引用。
How It Works:
程序打印它的第一个消息,然后调用 execlp, execlp 在 PATH 环境变量给出的目录中搜索名为 ps 的程序。然后它执行这个程序来代替 pexec 程序,启动它就像我们已经给出 shell 命令一样。
$ ps ax
当 ps 完成时,我们会得到一个新的 shell 提示符。程序不会返回到 pexec,因此不会打印第二条消息。新进程的 PID 与原进程相同,父进程的 PID 和 nice 值也一样。实际上,所发生的一切只是正在运行的程序开始从 exec 调用中指定的新可执行文件中执行新代码。
对于由 exec 函数启动的进程,参数列表和环境的组合大小是有限制的。这是由 ARG_MAX 给出的,在 Linux 系统上是 128K 字节。其他系统可能设置更多。降低可能导致问题的限制。POSIX 规范指出 ARG_MAX 至少应该是 4,096 字节。
除非发生错误,否则 exec 函数通常不返回,在这种情况下,设置错误变量 errno, exec 函数返回 -1。
exec 启动的新进程继承了原始进程的许多特性。特别是,打开的文件描述符在新进程中保持打开状态,除非设置了它们的 “close on exec标志” (更多细节见“文件处理”中的fcntl系统调用)。原始进程中的所有打开的目录流都被关闭。
(3)Duplicating a Process Image(复制进程映像)
要使用进程一次执行多个功能,可以使用线程 (在后面会介绍),也可以像 init 那样在程序中创建一个完全独立的进程,而不是像 exec 那样替换当前执行的线程。
我们可以通过调用 fork 来创建一个新的进程。这个系统调用复制当前进程,在进程表中创建一个具有许多与当前进程相同属性的新条目。新的进程与原来的进程几乎相同,执行相同的代码,但有自己的数据空间、环境和文件描述符。结合 exec 函数,fork 就是创建新进程所需要的全部。
如下图所示,调用 fork 在父进程中返回新子进程的 PID(非0)。新进程继续执行,就像原来的进程一样,除了子进程调用 fork 返回 0。这使得父进程和子进程都可以确定哪个是哪个。
如果fork失败,将返回 -1。
这通常是由于父进程可能拥有的子进程数量有限制 (CHILD_MAX),在这种情况下,errno 将被设置为 EAGAIN。如果进程标签中没有足够的空间容纳一个条目,或者没有足够的虚拟内存,errno 变量将被设置为 ENOMEM。
使用 fork 的典型代码片段是:
pid_t new_pid;
new_pid = fork();
switch(new_pid) {
case -1 : /* Error */
break;
case 0 : /* We are child */
break;
default : /* We are parent */
break;
}
(4)fork1.c
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *message;
int n;
printf("fork program starting\n");
pid=fork();
switch(pid)
{
case -1:
perror("fork program starting\n");
exit(1);
case 0:
message="This is the child";
n=5;
break;
default:
message="This is the parent";
n=3;
break;
}
for(;n>0;n--){
puts(message);
sleep(1);
}
exit(0);
}
这个程序作为两个进程运行。创建一个子节点并打印一条消息五次。原始进程(父进程)只打印一条消息三次。父进程在子进程打印所有消息之前完成,因此下一个 shell 提示符($)混合在输出中出现。(因为父进程完成了,但是子进程还没有完成)
How It Works:
当 fork 被调用时,这个程序分成两个单独的进程。父进程由 fork 的非零返回来标识,并用于设置要打印的大量消息,每个消息之间间隔1秒。
3、Waiting for a Process
当我们使用 fork 启动子进程时,它有自己的生命并独立运行。有时希望知道子进程何时完成。例如,在前面的程序中,父程序先于子程序完成,当子程序继续运行时,我们会得到一些杂乱的输出。这种情况可以通过调用 wait 来安排父进程等待子进程完成后再继续。
等待系统调用导致父进程暂停,直到其子进程停止。调用返回子进程的 PID。
这通常是已终止的子进程。状态信息允许父进程确定子进程的退出状态,即从 main 返回的值或传递给 exit 的值。如果 stat_loc 不是空指针,则状态信息将写入它所指向的位置。
我们可以使用 sys/wait.h 中定义的宏来解释状态信息,如下表所示。
(1)wait.c
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *message;
int n;
int exit_code;
printf("fork program starting\n");
pid=fork();
switch(pid)
{
case -1:
perror("fork program starting\n");
exit(1);
case 0:
message="This is the child";
n=5;
exit_code=37;
break;
default:
message="This is the parent";
n=3;
exit_code=0;
break;
}
for(;n>0;n--){
puts(message);
sleep(1);
}
if(pid!=0){
int stat_val;
pid_t child_pid;
child_pid=wait(&stat_val);
printf("Child has finished: PID=%d\n",child_pid);
if(WIFEXITED(stat_val))
printf("Child exited with code %d\n",WEXITSTATUS(stat_val));
else
printf("Child terminated abnormally\n");
}
exit(exit_code);
}
How It Works:
从 fork 调用获得非零返回的父进程使用 wait 系统调用暂停自己的执行,直到状态信息对子进程可用。这发生在子进程调用 exit 时,我们给它的退出码是37。然后父进程继续执行,通过测试等待调用的返回值确定子进程是否正常终止,并从状态信息中提取退出代码。
4、Zombie Processes
使用 fork 创建进程可能非常有用,但必须跟踪子进程。当子进程终止时,与其父进程的关联将继续存在,直到父进程正常终止或调用 wait。因此,不会立即释放进程表中的子流程项。
尽管子进程不再活动,但它仍然在系统中,因为需要存储它的退出代码,以防父进程随后调用 wait。它变成了所谓的失效进程,或僵尸进程。
如果你更改了 fork 示例程序中的消息数量,我们可以看到正在创建一个僵尸进程。如果子进程打印的消息比父进程少,那么它将先完成,并作为僵尸存在,直到父进程完成。
(1)fork2.c
fork2.c 与 fork1.c相同,不同之处在于子进程和父进程打印的消息数量是互换的(子进程 n=3,父进程 n=5)。
How It Works:
如果使用 ./fork2 & 运行前面的程序,然后在子程序完成之后但父程序还没有完成之前调用 ps 程序,我们将看到如下的一行。(有些系统可能会显示 <zombie> 而不是 <defunct>。)
如果父进程随后异常终止,则子进程自动获取 PID1 (init) 作为父进程的进程。子进程现在是僵尸进程,不再运行,但由于父进程的异常终止而被 init 继承。僵尸将保留在进程表中,直到被 init 进程收集为止。表越大,这个过程越慢。我们需要避免僵尸进程,因为它们会消耗资源,直到 init 清除它们。
还有另一个系统调用可以用来等待子进程。它被称为 waitpid,我们可以使用它来等待特定进程的终止。
pid 参数指定要等待的特定子进程的 pid。如果是 -1,waitpid 将返回任何子进程的信息。与 wait 类似,如果 stat_loc 指向的位置不是空指针,它将把状态信息写入该位置。
options 参数允许修改 waitpid 的行为。最有用的选项是 WNOHANG,它防止对 waitpid 的调用暂停调用者的执行。我们可以使用它来确定是否有任何子进程已终止,如果没有,则继续。其他选项与 wait 相同。
因此,如果你想让父进程定期检查特定的子进程是否已终止,我们可以使用该调用:
如果子进程没有终止或停止,则返回 0,如果已经终止或停止,则返回 child_pid。waitpid 在出错时将返回 -1 并设置 errno。如果没有子进程 (errno 设置为 ECHILD),调用被信号中断 (EINTR),或者选项参数无效(EINVAL),就会发生这种情况。
5、Input and Output Redirection
我们可以利用跨 fork 和 exec 调用保存打开文件描述符这一事实,利用进程的知识来改变程序的行为。
下一个例子涉及一个过滤器程序,该程序从标准输入读取数据并写入标准输出,同时执行一些有用的转换。
(1)upper.c
下面是一个非常简单的过滤器程序,upper.c,它读取输入并将其转换为大写字母:
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
int main()
{
int ch;
while((ch=getchar())!=EOF){
putchar(toupper(ch));
}
exit(0);
}
当然,我们可以使用 shell 重定向将文件转换为大写。
(2)useupper.c
如果我们想从另一个程序中使用这个过滤器,该怎么办? 这个程序 useupper.c 接受文件名作为参数,如果调用不正确,它将响应一个错误。
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char *argv[])
{
char *filename;
if(argc!=2){
fprintf(stderr,"usage: useupper file\n");
exit(1);
}
filename=argv[1];
/* 重新打开标准输入,在此过程中再次检查是否有错误,然后使用execl调用upper。*/
if(!freopen(filename,"r",stdin)){
fprintf(stderr,"could not redirect stdin from file %s\n",filename);
exit(2);
}
execl("./upper","upper",0);
/*不要忘记 execl 替换当前进程;如果没有错误,则不执行其余行。*/
perror("could not exec ./upper");
exit(3);
}
当运行这个程序时,我们可以给它一个要转换成大写的文件。该工作由程序上层完成,它不处理文件名参数。注意,你不需要 upper 的源代码,你可以用这种方式运行任何可执行程序:
useupper 程序使用 freopen 关闭标准输入,并将文件流 stdin 与作为程序参数给出的文件关联起来。然后调用 execl,用上面程序的代码替换正在运行的进程代码。因为打开的文件描述符在对 execl 的调用中被保留了下来,所以上面的程序运行起来和 shell 命令下完全一样:
6、Threads
Linux 进程可以相互协作,可以相互发送消息,也可以相互中断。它们甚至可以安排在彼此之间共享内存段,但它们本质上是操作系统内的独立实体。
它们不容易共享变量。有一类进程称为线程,在许多 UNIX 和 Linux 系统中都可用。尽管线程很难编程,但在某些应用程序中,例如多线程数据库服务器,它们可能有很大的价值。
在 Linux (和UNIX) 上编程线程不像使用多进程那样常见,因为 Linux 进程非常轻量级,而且编程多个协作进程比编程线程容易得多。线程将在下一板块中介绍。
以上,进程与信号(二)
祝好