linux c程序的执行

2018-06-10 13:29:22

执行新程序:execve

系统调用 execve() 可以将新程序加载到某一进程的内存空间。进程的栈、数据段、堆都会被新程序的相应部件所替换。

由fork() 生成的子进程对 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 则指定了传递给新进程的命令行参数。该数组对应于C语言main() 函数的第二个参数argv。argv[0] 通常等于pathname的basename部分。
envp指定新程序的环境列表。

argv和envp的详细解释可以参考 linux进程 这篇文章,这里只放2张图


/proc/PID/exe 文件是一个符号链接,指向可执行程序的绝对路径。调用execve之后,进程id没变。

如果pathname所指定的程序文件设置了 set-user-ID,set-group-ID位,那么会在执行此文件时将进程的有效用户id和有效组id置为该文件的属主id和组id以便临时获取权限,比如passwd程序。

如果出错了,将返回-1,errno将置为如下:

EACCESS pathname没有指向一个常规文件,未对该文件赋予可执行权限,或者因为pathname中的某一级目录不可搜索。
ENOENT pathname不存在
ENOEXEC 文件有执行权限,但是由于某种原因不能执行。
ETXTBSY 存在一个或多个以写入方式打开了pathname。
E2BIG 参数列表和环境列表所需空间总和超出了允许的最大值。

例子-main.c

#include <signal.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */
#include <string.h>     /* Commonly used string-handling functions */

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){
        printf("%s pathname\n", argv[0]);
        return 1;
    }  

    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 */

    execve(argv[1], argVec, envVec);
    perror("execve");          /* If we get here, something went wrong */
}

例子-args.c

#include <signal.h>
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */
#include <string.h>     /* Commonly used string-handling functions */

extern char **environ;

int main(int argc, char *argv[]){
    int j;
    char **ep;

    for (j = 0; j < argc; j++)
        printf("argv[%d] = %s\n", j, argv[j]);

    for (ep = environ; *ep != NULL; ep++)
        printf("environ: %s\n", *ep);

    exit(EXIT_SUCCESS);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# gcc main.c 
[root@izj6cfw9yi1iqoik31tqbgz c]# gcc args.c -o args
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out /root/c/args
argv[0] = args
argv[1] = hello freecls
argv[2] = goodbye
environ: GREET=salut
environ: BYE=adieu


exec()库函数

所有这些函数均构建于execve()之上,只是在为新程序指定程序名、参数列表以及环境变量的方式上有所不同。

#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)参数的描述 (v,l)环境变量来源 (e,-)
 execve() 路径名 数组 envp参数
 execle() 路径名 列表 envp参数
 execlp() 文件名+PATH 列表 调用者的environ
 execvp() 文件名+PATH 数组 调用者的environ
 execv() 路径名 数组 调用者的environ
 excel() 路径名 列表 调用者的environ

环境变量:PATH

函数 execvp() 和 execlp() 允许调用者只提供欲执行的程序的文件名。二者均使用环境变量 PATH 来搜索文件。

[root@izj6cfw9yi1iqoik31tqbgz c]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

PATH 里指定的路径名既可以是绝对路径名,也可以是相对路径名,相对路径名的诠释是根据进程的当前工作目录。

例子-execlp

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[]){
    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s pathname\n", argv[0]);
        return 0;
    }

    execlp(argv[1], argv[1], "hello freecls", (char *) NULL);
    perror("execlp"); //如果能到这一步,就证明出错了,因为execlp执行成功不返回。
}
#其实就相当于 echo hello freecls
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out echo
hello freecls

例子-execle

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[]){
    char *envVec[] = { "name=沧浪水", "url=www.freecls.com", NULL };
    char *filename;

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s pathname\n", argv[0]);
        return 0;
    }   

    //获取文件名
    filename = strrchr(argv[1], '/');
    if (filename != NULL)
        filename++;
    else
        filename = argv[1];

    execle(argv[1], filename, "hello freecls", "goodbye", (char *) NULL, envVec);
    perror("execle");
}
[root@izj6cfw9yi1iqoik31tqbgz c]# which echo
/usr/bin/echo

[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out /usr/bin/echo
hello freecls goodbye

[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out /root/c/args
argv[0] = args
argv[1] = hello freecls
argv[2] = goodbye
environ: name=沧浪水
environ: url=www.freecls.com

例子-execl

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[]){
    printf("Initial value of USER: %s\n", getenv("USER"));
    if (putenv("USER=freecls") != 0)
        perror("putenv");

    execl("/usr/bin/printenv", "printenv", "USER", "SHELL", (char *) NULL);
    perror("execl");
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
Initial value of USER: root
freecls
/bin/bash


解释器脚本

所谓解释器,就是能够读取并执行文本格式的程序。比如awk、sed、perl、python之类的程序都是解释器。

内核运行解释器脚本的方式与二进制程序一样,前提是脚本必须满足两点要求:

1.脚本文件必须可执行。
2.文件的起始行必须制定运行脚本解释器路径名:#! interpreter-path [optional-arg]。

execve() 来运行脚本时,如果检测到文件以 #! 2字符开头,就会提取该行剩余部分来执行程序。

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


文件描述符与exec()

默认情况下,由exec()调用程序所打开的所有文件描述符在exec()的执行过程中会保持打开状态,且在新程序中依然有效,这通常很有用。例如io重定向:

ls /tmp > dir.txt

上述命令会执行以下步骤。

1.调用fork()创建子进程。
2.子shell以文件描述符1(标准输出)来打开文件 dir.txt 用于输出,可能会用如下方式,对dup不了解的可以参考 linxu文件io

fd = open("dir.txt", O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
/* rw-rw-rw- */
if (fd != STDOUT_FILENO) {
    dup2(fd, STDOUT_FILENO);
    close(fd);
}

3.子shell执行程序ls。ls 将结果输出到标准输出,也就是dir.txt。

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

有些时候处于安全或某方面的考虑,需要在调用exec成功的时候关闭打开文件描述符。

int flags;
flags = fcntl(fd, F_GETFD);
if (flags == -1)
    perror("fcntl");

flags |= FD_CLOEXEC;
if (fcntl(fd, F_SETFD, flags) == -1)
    perror("fcntl");

例子

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[]){
    int flags;

    if (argc > 1) {
        //获取标准输出的文件描述符标志
        flags = fcntl(STDOUT_FILENO, F_GETFD);
        if (flags == -1)
            perror("fcntl - F_GETFD");

        //加上 FD_CLOEXEC 标志
        flags |= FD_CLOEXEC;

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

    //上面的argc>1 则STDOUT_FILENO 会主动关闭
    execlp("ls", "ls", "-l", argv[0], (char *) NULL);
    perror("execlp");
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
-rwxr-xr-x 1 root root 8616 Jun 10 12:23 ./a.out

[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 1
ls: write error: Bad file descriptor


执行shell命令:system()

程序通过调用system() 函数来执行任意的 shell 命令。在以后文章将会讲解popen(),也可以用来执行shell命令。

#include <stdlib.h>

int system(const char *command);

system() 的主要优点在于方便。

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

这些优点是以低效率为代价的。因为system() 运行至少要创建2个进程,一个用户运行shell,另一个或多个用于shell 所执行的命令,如果对效率或者速度有所要求,最好还是直接调用fork() 加 exec来执行既定程序。

system()返回值如下:

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

最后两种情况跟waitpid()所返回的等待状态形式相同,可以参考 linux c监控子进程

例子

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

#define MAX_CMD_LEN 200

void printWaitStatus(const char *msg, int status);

void printWaitStatus(const char *msg, int status){
    if (msg != NULL)
        printf("%s", msg);

    if (WIFEXITED(status)) {
        printf("child exited, status=%d\n", WEXITSTATUS(status));

    } else if (WIFSIGNALED(status)) {
        printf("child killed by signal %d (%s)",
                WTERMSIG(status), strsignal(WTERMSIG(status)));
#ifdef WCOREDUMP        /* Not in SUSv3, may be absent on some systems */
        if (WCOREDUMP(status))
            printf(" (core dumped)");
#endif
        printf("\n");

    } else if (WIFSTOPPED(status)) {
        printf("child stopped by signal %d (%s)\n",
                WSTOPSIG(status), strsignal(WSTOPSIG(status)));

#ifdef WIFCONTINUED     /* SUSv3 has this, but older Linux versions and
                           some other UNIX implementations don't */
    } else if (WIFCONTINUED(status)) {
        printf("child continued\n");
#endif

    } else {            /* Should never happen */
        printf("what happened to this child? (status=%x)\n",
                (unsigned int) status);
    }
}

int main(int argc, char *argv[]){
    char str[MAX_CMD_LEN];
    int status;

    for (;;) {
        printf("Command: ");
        fflush(stdout);
        if (fgets(str, MAX_CMD_LEN, stdin) == NULL)
            break;              /* end-of-file */

        status = system(str);
        printf("system() returned: status=0x%04x (%d,%d)\n",
                (unsigned int) status, status >> 8, status & 0xff);

        if (status == -1) {
            perror("system");
        } else {
            if (WIFEXITED(status) && WEXITSTATUS(status) == 127)
                printf("(Probably) could not invoke shell\n");
            else                /* Shell successfully executed command */
                printWaitStatus(NULL, status);
        }
    }

    exit(EXIT_SUCCESS);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
Command: ls -l
-rwxr-xr-x 1 root   root    9056 Jun 10 12:51 a.out
-rw-rw-r-- 1 root   root    2164 Jun 10 12:51 main.c
system() returned: status=0x0000 (0,0)
child exited, status=0

Command: ll
sh: ll: command not found
system() returned: status=0x7f00 (127,0)
(Probably) could not invoke shell


system()的实现

system的简化实现,假设要实现如下命令

sh -c "ls | wc"
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int
system(char *command)
{
    int status;
    pid_t childPid;

    switch (childPid = fork()) {
    case -1:
        return -1;

    case 0:
        execl("/bin/sh", "sh", "-c", command, (char *) NULL);
        _exit(127);

    default:
        if (waitpid(childPid, &status, 0) == -1)
            return -1;
        else
            return status;
    }
}

其实给system() 实现带来复杂性的是对信号的正确处理。

首先我们考虑的是 SIGCHLD。假设调用 system() 的程序还创建了其他子进程,对SIGCHLD 的信号处理器自身也执行了wait()。此时,如果system() 所创建的子进程终止产生的SIGCHLD信号被主程序率先处理,而system()没有捕获到,会产生两种不良后果。

1.主程序以为某个子进程终止了,其实只是system()下的子程序终止。
2.system()函数却无法获取其所创建的子进程的终止状态。

所以在system() 运行期间,主程序必须阻塞SIGCHLD 信号。

其他要关注的信号为 SIGINT SIGQUIT信号。考虑到如下调用的后果:

system("sleep 20");

此时会有3个进程在运行,主程序、chell进程、sleep进程。

在输入ctrl + c 时会将信号发送给所有3个进程。shell在等待子进程期间会忽略 SIGINT 和 SIGQUIT信号,不过默认情况下会杀死主程序和sleep进程。

所以SUSv3规定如下:

1.调用进程在执行命令期间应忽略 SIGINT 和 SIGQUIT信号。
2.子进程对上述2信号的处理,跟调用fork()和exec()一样。

例子-改进版

#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;

    //屏蔽 SIGCHLD
    sigemptyset(&blockMask);
    sigaddset(&blockMask, SIGCHLD);
    sigprocmask(SIG_BLOCK, &blockMask, &origMask);

    //忽略 SIGINT SIGQUIT
    saIgnore.sa_handler = SIG_IGN;
    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;

    case 0:
        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 */
        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;
}


总结

讲解system()实现主要是为了让我们对程序的执行更加了解,顺带复习下信号。读者记住一点,如果对性能没啥要求,就用system()来执行系统命令,如果对性能要求比较高,则最好自己通过fork() + exec()事项,如果有疑问可以给我留言。


备注
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/53

©著作权归作者所有
收藏
推荐阅读
  • err
    linux c监控子进程

    接下来描述两种监控子进程的技术:系统调用wait()及其变体,SIGCHLD信号。系统调用wait()#include &lt;sys/wait.h&gt; pid_t...

  • err
    linux c进程的创建、终止

    对进程不是很了解的同学可以参考linux 进程这篇文章。进程的创建系统调用fork() 创建一新进程(子进程)。#include &lt;unistd.h&gt; pi...

  • linux c定时器与休眠

    定时器是进程规划自己在未来某一时刻接获通知的一种机制。休眠则能是进程(或线程)暂停执行一段时间。间隔定时器系统调用 setitimer() 创建一个间隔式定时器,一定时间后到期,这个定时器也可以每个一...

  • linux c信号(3)-高级特性

    core dump文件coredump 文件 内含进程终止时内存映像的一个文件,把它加载到调试器中,即可查明信号到达时程序代码和数据的状态。 引发core dump文件除了程序中调用 abort()外...

  • err
    linux c信号(2)-处理函数

    一般而言信号处理函数设计的越简单越好。其中的一个重要原因在于,这将降低引发竞争条件的风险。下面是信号处理函数的两种常见设计。1.信号处理器函数设置全局性标记变量并退出。...

  • nginx模块 ngx_http_headers_module

    ngx_http_headers_module 模块是用来增加 Expires 和 Cache-control,或者是任意的响应头。Syntax: add_header name value [alw...

  • nginx模块 ngx_http_gunzip_module、ngx_http_gzip_module、ngx_http_gzip_static_module

    ngx_http_gunzip_module 模块将文件解压缩后并在响应头加上 "Content-Encoding: gzip" 返回给客户端。为了解决客户端不支持gzip压缩。编译的时候带上 --w...

  • nginx模块 ngx_http_flv_module、ngx_http_mp4_module

    ngx_http_flv_module模块提供了对 flv 视频的伪流支持。编译的时候带上 --with-http_flv_module。它会根据指定的 start 参数来指定跳过多少字节,并在返回数...

  • nginx模块 ngx_http_fastcgi_module

    ngx_http_fastcgi_module 模块使得nginx可以与 fastcgi 服务器通信。比如目前要使得 nginx 支持 php 就得使用 fastcgi技术,在服务器上装上 nginx...

  • nginx模块 ngx_http_autoindex_module

    ngx_http_autoindex_module 模块可以将uri以 / 结尾时,列出里面的文件和目录。Syntax: autoindex on | off; Default: autoindex ...

简介
天降大任于斯人也,必先苦其心志。