LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

《Linux/UNIX系统编程手册》第27章 程序的执行

 关键词:execve()、system()等等。

本章介绍了exec()函数族用于执行新程序,以及文件描述符和信号相关。最后介绍了对execve()封装函数system(),以及其是如何实现的。

1. 执行新程序:execve()

execve()可以将新程序加载到某一进程的内存空间,将丢弃旧有程序,而进程的栈、数据以及堆会被新程序的相应部分替换

#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
    Never returns on success; returns –1 on error

pathname包含准备载入当前进程空间的新程序路径名,可以是绝对路径或者相对路径。

argv指定了传递给新进程命令行参数,是由字符串指针所组成的列表,以NULL结束。

envp指定了新程序的环境列表。envp对应于新程序的environ数组:由字符串指针组成的列表,以NULL结束,所指向的字符串格式为name=value。

调用execve()之后,新进程ID仍保持不变。对execve()的成功调用将永不返回,而且也无需检查execve()的返回值;一旦返回就表明发生了错误,返回值总是-1,可以通过errno来判断出错原因。

t_execve.c:
#include "tlpi_hdr.h" int main(int argc, char *argv[]) { char *argVec[10]; /* Larger than required */ char *envVec[] = { "GREET=salut", "BYE=adieu", NULL }; if (argc != 2 || strcmp(argv[1], "--help") == 0) usageErr("%s pathname\n", argv[0]); /* Create an argument list for the new program */ argVec[0] = strrchr(argv[1], '/'); /* Get basename from argv[1] */ if (argVec[0] != NULL) argVec[0]++; else argVec[0] = argv[1]; argVec[1] = "hello world"; argVec[2] = "goodbye"; argVec[3] = NULL; /* List must be NULL-terminated */ /* Execute the program specified in argv[1] */ execve(argv[1], argVec, envVec); errExit("execve"); /* If we get here, something went wrong */ }
envargs.c: #include "tlpi_hdr.h" extern char **environ; int main(int argc, char *argv[]) { int j; char **ep; /* Display argument list */ for (j = 0; j < argc; j++) printf("argv[%d] = %s\n", j, argv[j]); /* Display environment list */ for (ep = environ; *ep != NULL; ep++) printf("environ: %s\n", *ep); exit(EXIT_SUCCESS); }

执行结果:

./t_execve ./envargs
argv[0] = envargs
argv[1] = hello world
argv[2] = goodbye
environ: GREET=salut
environ: BYE=adieu

2. exec()函数

 exec()提供了多种API形式:

#include <unistd.h>
int execle(const char *pathname, const char *arg, ...
/* , (char *) NULL, char *const envp[] */ );
int execlp(const char *filename, const char *arg, ...
/* , (char *) NULL */);
int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char *arg, ...
/* , (char *) NULL */);
    None of the above returns on success; all return –1 on error

p表示PATH,允许只提供程序文件名,系统会在环境变量寻找相应执行文件。

e表示environment,可以通过envp为新程序显式指定环境变量。

l表示list,以字符串列表形式来指定参数,而不是用数组来描述argv列表。

v表示vector,以NULL结尾的数组作为参数列表。

3. 解释器脚本

解释器是能够读取并执行文本格式命令的程序。

UNIX内核运行解释器脚本的方式与二进制程序无异,前提是脚本必须满足:可执行权限;起始行必须指定运行脚本解释器的路径名。

Linux要求脚本#!起始行不得超过127个字节,其中不包括行尾的换行符。超出部分会被略去。

3.1 解释器脚本的执行

execve()如果检测到传入的文件以两字节序列"#!"开始,就会析取该行的剩余部分,然后按如下参数列表来执行解释器程序:

interpreter-path [ optional-arg ] script-path arg...

interpreter-path和optional-arg取自脚本的#!行, script-path是传递给execve()的路径名,arg...则是通过变量argv传递给execve()的参数列表。

4. 文件描述符与exec()

exec()的调用程序所打开的所有文件描述符在exec()的执行过程中会保持打开状态,且在新程序中依然有效

sheel执行命令“ls /tmp > dir.txt”时,执行了以下步骤:

1. 调用fork()创建子进程,子进程也会运行shell的一份拷贝。

2. 子shell以描述符1打开文件dir.txt用于输出。

  2.1 子shell关闭描述符1后,随即打开文件dir.txt。确保标准输出指向dir.txt。

  2.2 shell打开文件dir.txt,使用dup2()强制将标准输出复制为新描述符的副本,并将无用的新描述符关闭。

3. 子shell执行程序ls。ls将其结果输出到标准输出,即dir.txt文件。

执行时关闭(close-on-exec)标志(FD_CLOEXEC)

在执行exec()之前,程序有时需要确保关闭某些特定的文件描述符。

#include <fcntl.h>
int
main(int argc, char *argv[])
{
    int flags;

    if (argc > 1) {
        flags = fcntl(STDOUT_FILENO, F_GETFD);              /* Fetch flags */
        if (flags == -1)
            errExit("fcntl - F_GETFD");

        flags |= FD_CLOEXEC;                    /* Turn on FD_CLOEXEC */

        if (fcntl(STDOUT_FILENO, F_SETFD, flags) == -1)     /* Update flags */
            errExit("fcntl - F_SETFD");
    }

    execlp("ls", "ls", "-l", argv[0], (char *) NULL);
    errExit("execlp");
}

5. 信号与exec()

exec()在执行时会将现有进程的文本段丢弃,该文本段可能包含了有调用进程创建的信号处理程序。内核会将所有已设信号处置重置为SIG_DFL,而对其他所有信号的处置保持不变。

6. 执行shell命令:system()

函数system()创建一个子进程来运行shell,并执行命令commnd。

#include <stdlib.h>
int system(const char *command);
    See main text for a description of return value

 system()的主要优点在于简便:

  • 无需处理对fork()、exec()、wait()、exit调用细节。
  • system()会代为处理错误和信号。
  • 因为system()使用shell来执行命令,所以会在执行command之前对齐进行所有的常规shell处理、替换以及重定向操作。

执行一个system()运行命令需要创建字少两个进程:一个用于运行shell,另一个或多个则用于shell所执行的命令。所以system()的这些优点是以低效率为代价的。

system()返回值如下:

  • command为NULL指针时,如果shell可用则system()返回非0值,若不可用则返回0。
  • 如果无法创建子进程或是无法获取其终止状态,那么system()返回-1
  • 子进程不能执行shell,则system()的返回值会与子shell调用_exit(127)终止时一样
  • 如果所有的系统调用都成功,system()会返回执行command的子shell的终止状态。shell终止状态是其执行最有一条命令的退出状态;如果命令为信号所杀,大多数shell会以值128+n退出,n为信号编号

 当设置了用户ID和组ID的程序在特权模式下运行时,绝不能调用system()

7. system()的实现

#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>

int
system(const char *command)
{
    sigset_t blockMask, origMask;
    struct sigaction saIgnore, saOrigQuit, saOrigInt, saDefault;
    pid_t childPid;
    int status, savedErrno;

    if (command == NULL)                /* Is a shell available? */
        return system(":") == 0;

    /* The parent process (the caller of system()) blocks SIGCHLD
       and ignore SIGINT and SIGQUIT while the child is executing.
       We must change the signal settings prior to forking, to avoid
       possible race conditions. This means that we must undo the
       effects of the following in the child after fork(). */

    sigemptyset(&blockMask);            /* Block SIGCHLD */
    sigaddset(&blockMask, SIGCHLD);
    sigprocmask(SIG_BLOCK, &blockMask, &origMask);

    saIgnore.sa_handler = SIG_IGN;      /* Ignore SIGINT and SIGQUIT */
    saIgnore.sa_flags = 0;
    sigemptyset(&saIgnore.sa_mask);
    sigaction(SIGINT, &saIgnore, &saOrigInt);
    sigaction(SIGQUIT, &saIgnore, &saOrigQuit);

    switch (childPid = fork()) {
    case -1: /* fork() failed */
        status = -1;
        break;          /* Carry on to reset signal attributes */

    case 0: /* Child: exec command */

        /* We ignore possible error returns because the only specified error
           is for a failed exec(), and because errors in these calls can't
           affect the caller of system() (which is a separate process) */

        saDefault.sa_handler = SIG_DFL;
        saDefault.sa_flags = 0;
        sigemptyset(&saDefault.sa_mask);

        if (saOrigInt.sa_handler != SIG_IGN)
            sigaction(SIGINT, &saDefault, NULL);
        if (saOrigQuit.sa_handler != SIG_IGN)
            sigaction(SIGQUIT, &saDefault, NULL);

        sigprocmask(SIG_SETMASK, &origMask, NULL);

        execl("/bin/sh", "sh", "-c", command, (char *) NULL);
        _exit(127);                     /* We could not exec the shell */

    default: /* Parent: wait for our child to terminate */

        /* We must use waitpid() for this task; using wait() could inadvertently
           collect the status of one of the caller's other children */

        while (waitpid(childPid, &status, 0) == -1) {
            if (errno != EINTR) {       /* Error other than EINTR */
                status = -1;
                break;                  /* So exit loop */
            }
        }
        break;
    }

    /* Unblock SIGCHLD, restore dispositions of SIGINT and SIGQUIT */

    savedErrno = errno;                 /* The following may change 'errno' */

    sigprocmask(SIG_SETMASK, &origMask, NULL);
    sigaction(SIGINT, &saOrigInt, NULL);
    sigaction(SIGQUIT, &saOrigQuit, NULL);

    errno = savedErrno;

    return status;
}

 

8. 总结

execve()以一新程序替换当前运行的程序;构建于execve()之上,存在多种命名相似,功能相同,接口不同的函数。

所有exec()函数均可用于加载二进制可执行文件或是执行解释器脚本。

system()函数是fork()、exec()、exit()、wait()等组合实现的。

posted on 2020-11-21 00:00  ArnoldLu  阅读(268)  评论(0编辑  收藏  举报

导航