linux程序设计——进程和信号(第十一章)

58 篇文章 0 订阅
57 篇文章 9 订阅

11.3    启动新进程

可以 在程序的内部启动另一个程序,从而创建一个新进程.这个工作可以通过库函数system来完成.
#include <stdlib.h>
int system(const char *string);
system函数的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成.命令的执行情况就如同在shell中执行如下命令:
$ sh -c string
如果无法启动shell来完成这个命令,system函数将返回错误代码127;如果是其他错误,则返回-1.否则,system将返回该命令的退出码.
下面使用system函数来编写一个程序,让它运行ps命令.
因为system函数用一个shell来启动想要执行的程序,所以可以把这个程序放在后台执行.具体做法是将system1.c中的函数调用修改为下面这样:
system("ls -l&");
第一个例子中,程序以字符串"ps ax"为参数调用system函数从而在程序中执行ps命令.程序在ps命令完成后从system调用中返回.system函数很好用,但它也有局限性,因为 程序必须等待由system函数启动的进程结束后才能继续,因此不能立刻执行其他任务.
第二个例子中,对 system函数的调用将在shell命令结束后立刻返回.由于它是一个在后台运行程序的请求,所以ls程序一启动shell就返回了,这与在shell提示符下面执行这条命令的效果是一样的.
$ ls -l &
在ls命令还未来得及打印出所有输出结果之前,system2程序就打印出字符串Done然后退出了.在system2程序退出后,ls命令继续完成它的输出.这类的处理行为往往会给用户带来很大的困惑.如果想要用好进程,就需要能够对它们的行为做更好的控制.下面来看一个用来创建进程的底层接口exec.
    一般来说, 使用system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的进程.由于在启动程序之前需要先启动一个shell,而且对shell的安装情况以及使用的依赖也很大,所以使用system函数的效率不高,在下一节中,将看到一种更好的调用程序的方法,与system调用相比,应该总是在程序中优先使用这种方法.
1.替换进程映像
exec系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的表达方式上各不相同. exec函数可以把当前进程替换为一个新进程,新进程由path或file参数指定.可以使用exec函数将程序的执行从一个程序切换到另一个程序.例如,可以在启动另一个有着受限使用策略的程序前,检查用户的凭证.exec函数比system函数更有效,因为在新的程序启动后,原来的程序就不再运行了.
#include <unistd.h>
char **environ;
int execl(const char *path, const char *arg0, ..., (char *)0);
int execlp(const char *file, const char *arg0, ..., (char *)0);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这些函数可以分为两大类,execl,execlp和execle的参数个数是可变的,参数以一个空指针结束.execv和execvp的第二个参数是一个字符串数组.不管是哪种情况,新程序在启动时会把argv数组中给定的参数传递给main函数.
这些函数通常都是用execve实现的,虽然并不是必须这样做.
以字母p结尾的函数通过搜索PATH环境变量来查找新程序的可执行文件的路径.如果可执行文件不在PATH定义的路径中,就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数.
全局变量environ用来把一个值传递给新的程序环境中,此外函数execle和execve可以通过参数envp传递字符串数组作为新程序的环境变量.
如果想通过exec函数案例启动ps程序,可以从6个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);
编写pexec程序,可以看到正常的ps的输出,但是字符串Done却根本没有出现.另外值得注意的是,ps的输出中没有pexec进程的任何信息.
程序先打印出它的第一条消息,接着调用execlp,这个函数在PATH环境变量给出的目录中搜索程序ps,然后用这个程序替换pexec程序,就好像直接使用如下所示的shell命令一样:
$ ps ax
ps命令结束时,看到一个新的shell提示符,因为并没有再返回到pexec程序中,所以第二条消息是不会打印出来的.新进程的PID,PPID和nice值与原先的完全一样.事实上,这里发生的一切其实就是,运行中的程序开始执行exec调用中指定的新的可执行文件中的代码.
对于由exec函数启动的进程来说,它的参数表和环境加在一起的总长度是有限制的.上限由ARG_MAX给出,在linux系统上它是128K字节.其他系统可能会设置非常有限的长度,这有可能导致出现问题.
一般情况下,exec函数是不会返回的,除非发生了错误,出现错误时,exec函数将返回-1,并且会设置错误变量errno.
exec启动的新进程继承了原进程的许多特性.特别地,在原进程中已打开的文件描述符在新进程中仍然保持打开,除非它们的"执行时关闭标志"(close on exec flag)被置位.任何在原进程中已打开的目录流都将在新进程中被关闭.

2. 复制进程映像

想让进程同时执行多个函数,可以使用线程或从原程序中创建一个完全分离的进程,后者就像init的做法一样,而不像exec调用那样用新程序替换当前执行的线程
可以通过调用fork创建一个新进程,这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与原进程一模一样,执行的代码与完全相同,但新进程有自己的数据空间、环境和文件描述符。fork和exec函数结合在一起使用就是创建新进程所需要的一切了

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
最初的进程---->fork()----->新进程(返回0)
                 |
                  -------->原进程继续执行(返回一个新的PID)
父进程中的fork调用返回的是新的子进程的PID,新进程将继续执行,就像原进程一样,不同之处在于,子进程中的fork调用返回的是0。父子进程可以通过这一点来判断究竟谁是父进程,谁是子进程。
如果fork失败,它将返回-1.失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时errno将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEN。
一个典型的使用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;
}
编写程序fork1.c
这个程序以两个进程的方式在运行。子进程被创建并且输出消息5次。原进程(即父进程)只输出消息3次。父进程在子进程打印完它的全部消息之前就结束了,因此可以看到在输出内容中混杂着一个shell提示符。
程序在调用fork时被分为两个独立的进程。程序通过fork调用返回的非零值确定父进程,并根据该值来设置消息的输出次数,两次消息的输出之间间隔一秒。

11.3.1    等待一个进程

当用fork启动一个子进程时,子进程就有了它自己的声明周期并将独立运行。有时,希望知道一个子进程何时结束。例如,在前面的示例程序中,父进程在子进程之前结束,由于子进程还在继续运行,所以得到的输出结果有点乱。可以通过在父进程中调用wait函数让父进程等待子进程的结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);
wait系统调用将暂停父进程直到它的子进程结束为止。这个调用返回子进程的PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。如果stat_loc不是空指针,状态信息将被写入它所指向的位置。
可以用sys/wait.h文件中定义的宏来解释状态信息。如下所示:
宏                        说明
WIFEXITED(stat_val)        如果子进程正常结束,它就去一个非零值
WEXITSTATUS(stat_val)    如果WIFEXITED非零,它返回子进程的退出码
WIFSIGNALED(stat_val)    如果子进程因为一个未捕获的信号而终止,它就取一个非零值
WTERMSIG(stat_val)        如果WIFSIGNALED非零,它返回一个信号代码
WIFSTOPPED(stat_val)    如果子进程意外终止,它就取一个非零值
WSTOPSIG(stat_val)        如果WIFSTOPPED非零,它就返回一个信号代码
编写wait.c
wait.c函数比fork1.c函数多出如下代码:

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");
}
父进程(从fork调用中获得一个非零的返回值)用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用exit的时候。将子进程的退出码设置为37。父进程然后继续运行,通过测试wait调用的返回值来判断子进程是否正常终止。如果是,这就从状态信息中提取出子进程的退出码。

11.3.2    僵尸进程
用fork来创建进程确实很有用,但必须清楚子进程的运行情况.子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才告结束.因此,进程表中代表子进程的表项不会立刻释放.虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用.这是它将成为一个死(defunct)进程或僵尸(zombie)进程.
如果修改fork示例程序中的消息输出次数,就能看到僵尸进程.如果子进程输出消息的次数少于父进程,它就会率先结束并成为僵尸进程直到父进程也结束.
如果用./fork2 &命令来运行上面的这个程序,然后在子进程结束之后父进程结束之前调用ps程序,将会发现 fork2 <defunct>
如果此时父进程异常终止,子进程将自动把PID为1的进程(即init)作为自己的父进程.子进程现在是一个不再运行的僵尸进程,但因为其父进程异常终止,所以它由init进程接管.僵尸进程将一直保留在进程表中直到被init进程发现并释放.进程表越大,这一过程就越慢.应该 尽量避免产生僵尸进程,因为在init清理它们之前,它们将一直消耗系统资源.
还有 另一个系统调用可用来等待子进程的结束,它是waitpid函数,可以用它来等待某个特定进程的结束.
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *stat_loc, int options);
pid参数指定需要等待的子进程的PID,如果它的值为-1,waitpid将返回任一子进程的信息.与wait一样,如果stat_loc不是空指针,waitpid将把状态信息写到它所指向的位置.option参数可用来改变waitpid的行为,其中最有用的一个选项是WNOHANG,它的作用是防止waitpid调用将调用者的执行挂起.可以用这个选项来查找是否有子进程已经结束,如果没有,程序将继续执行.其他的选项和wait掉用的选项相同.
因此,如果想让父进程周期性检查某个特定的子进程是否已终止,就可以使用如下的调用方式:
waitpid(child_pid, (int *)0, WNOHANG);
如果子进程没有结束或意外终止,它就返回0,否则返回child_pid.如果waitpid失败,它将返回-1并设置errno.失败的情况包括:没有子进程(errno设置为ECHILD),调用被某个信号中断(EINTR)或选项参数无效(EINVAL)
11.3.3    输入和输出重定向
已经 打开的文件描述符将在fork和exec调用之后保留下来,可以利用对进程这方面知识的理解来改变程序的行为.下一个例子涉及一个过滤程序:它从标准输入读取数据,然后像标准输入写数据,同时在输入和输出之间对数据做一些有用的转换.
编写过滤程序upper.c,它读取输入并将输入字符转换为大写
还可以利用shell的重定向把一个文件的内容全部转换为大写,先touch file.txt,然后输入如下内容:
this is the file, file.txt, is is all lower case.
保存之后,使用命令./upper < file.txt
THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.
如果想在想在另一个程序中使用这个过滤程序会发生什么情况呢?下面这个程序useupper.c接受一个文件名作为命令行参数,如果对它的调用不正确,它将响应一个错误信息。
编写程序useupper.c
运行这个程序时,可以提供给它一个文件,让它把该文件的内容全部转换为大写。这项工作由程序upper完成,但它并不处理文件名参数。可以用下面这种方法来运行任何可执行程序:
$ ./useupper file.txt
u seupper程序用freopen函数先关闭标准输入,然后将文件流stdin与程序给定的文件名关联起来,接下来,它调用execl用upper程序替换调用正在运行的进程代码。因为已经打开的文件描述符会在execl调用之后保留下来,所以upper程序的运行情况与它在提示符下的运行情况完全一样。
11.3.4    线程
linux系统中的进程可以互相协助、发送消息、互相中断,甚至可以共享内存段。但从本质上来说,它们是操作系统内各自独立的实体,想要在它们之间共享变量并不是很容易。
在许多UNIX和linux系统中都有一类进程叫做线程,涉及线程的编程时比较困难的,但它在某些应用软件(如多线程数据库服务器)中又有很大的用处。在linux系统中编写线程程序并不像编写多进程程序那么常见,因为linux中的进程都是非常轻量级的,而且编写多个互相协作的进程比编写线程要容易的多。在第12章介绍线程。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值