曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 7772|回复: 1
打印 上一主题 下一主题

[进程] exec 函数族与执行新程序

[复制链接]

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
跳转到指定楼层
楼主
发表于 2009-6-16 15:50:27 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    使用 fork()和 vfork() 创建子进程后,子程序通常会调用 exec 函数族来执行另外一个程序,这个 exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它根据指定的文件名或目录名找到可执行文件,并用它来代替当前进程的执行映像。也就是说,exec调用并没有生成新进程,一个进程一旦调用 exec函数,它本身就“死亡”了,系统把代码段替换成新程序的代码,放弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,惟一保留的就是进程的 ID。也就是说,对系统而言,还是同一个进程,不过执行的已经是另外一个程序了。

实际上,在 linux 中并没有 exec() 函数,而是有 6 个以 exec 开头的函数族,他们之间的语法有细微的差别,函数原型如下所示:

函数原型:
[C++] 纯文本查看 复制代码
 
#include <unistd.h> 
int execl( const char *path, const char *arg, ... ) 
int execv( const char *path, char *const argv[] ) 
int execle( const char *path, const char *arg, ... , char *const envp[] ) 
int execve( const char *path, char *const argv[], char *const envp[] ) 
int execlp( const char *file, const char *arg, ...) 
int execvp( const char *file, *const argv[] )


理解 exec() 函数族的使用,首先要理解环境变量这个概念。

为了用户灵活地使用 shell ,Linux 引入了环境变量的概念,包括用户的主目录,终端类型、当前目录等,他们定义了用户的工作环境,所以成为环境变量。下面代码演示环境变量的应用:
[C++] 纯文本查看 复制代码
#include <stdio.h> 
#include <malloc.h> 

extern char **environ; 

int main( int argc, char *argv[] ) 
{ 
    int i; 
     
    printf("Argument:\n"); 
    for(i=0; i<argc; i++) { 
        printf("argv[%d] is %s\n",i, argv[i]); 
    } 

    printf("Enviroment:\n"); 
    for(i=0; environ[i] != NULL; i++) 
        printf("%s\n", environ[i]); 

    return 0; 
}

运行及输出:
beyes@linux-beyes:~/C/base> ./env.exe arg1 arg2 arg3
Argument:
argv[0] is ./env.exe
argv[1] is arg1
argv[2] is arg2
argv[3] is arg3
Enviroment:
LESSKEY=/etc/lesskey.bin
ORBIT_SOCKETDIR=/tmp/orbit-beyes
INFODIR=/usr/local/info:/usr/share/info:/usr/info
NNTPSERVER=news
... ... ... ...

说明
Argument 后面显式的是程序的命令行参数。
Enviroment 后面显示的是当前系统中各个环境变量的值,可以将其与 env 命令比较,将会发现两者的结果一样。

程序中通过系统预定义的环境变量 environ 显示各个环境变量的值。还可以通过另外一个方法得到环境变量值。

事实上, main() 函数的完整形式应该是:
int main( int argc, char *argv[], **envp );


通过打印 main 的参数 envp ,同样可以得到环境变量。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
沙发
 楼主| 发表于 2009-6-16 18:43:21 | 只看该作者
事实上,无论哪个 exec 函数,都是将可执行程序的路径,命令行参数和环境变量 3 个参数传递给可执行程序的 main() 函数

下面分别介绍各 exec 函数是如何将 main 函数需要的参数传递给它的。
      execv() 函数:execv() 通过路径名方式调用可执行文件作为新的进程的映像。它的 argv 参数用来提供给 main() 的 argv 参数。argv 参数是一个以 NULL 结尾 ( 最后一个元素必须是空指针 )的字符串数组。
      execve() 函数:在该系统调用中,参数 path 是将要执行的程序的路径名,参数 argv、envp 与 main 函数的 argv、envp 对应。
注意:参数 argv 和参数 envp 的大小都是受限制的。linux 系统通过宏 ARG_MAX 来限制她们的大小,如果它们的容量之和超过 ARG_MAX 定义的值将会发生错误。这个宏定义在 <linux/limits.h> 头文件中。
      execl() 函数:该函数与 execv() 函数用法类似。只是在传递 argv 参数的时候,每个命令行参数都声明为一个单独的参数( 参数中使用 “..." 说明参数的个数是不确定的 ),要注意的是这些参数要以一个空指针作为结束。
      execle() 函数:该函数与 execl 函数用法类似,只是要显式指定环境变量。环境变量位于命令行参数最后一个参数的后面,也就是位于空指针之后。
      execvp() 函数:该函数和 execv() 函数用法类似,不同的是参数 filename。该参数如果包含 “/”,就相当于路径名;如果不包含 "/",函数就到 PATH 环境变量定义的目录中寻找可执行文件。
      execlp() 函数:该函数类似于 execl() 函数,他们的区别和 execvp() 与 execv() 的区别一样。
exec 函数族的 6 个函数中只有 execve() 是系统调用。其它 5 个都是库函数,他们实现时都调用 execve() 。

由上面注意到,有 2 个函数( 以 p 结尾的两个函数 )可以只给出文件名,系统就会自动从环境变量 $PATH 所指出的路径中进行查找,其余 4 个函数的查找方式都是完整的文件目录路径。

关于参数的传递方式:
exec 函数族的参数传递方式有两种:一种是列举方式;一种是将所有参数整体构造指针数组传递。
在这里,是以函数名的第 5 位字母来区分的,字母为" l " ( list ) 的表示逐个列举方式,其语法为 char *arg
字母为" v "( vector ) 的表示将所有参数整体构造指针数组传递,其语法为 *const argv[]


在正常情况下,这些函数不会返回的,因为进程的执行映像已经被替换,没有接收返回值的地方了。如果有一个错误时间,将返回 -1。这些错误通常是由文件名或参数错误引起的。其含义如下表所示:
errno
错误描述
EACCES
指向的文件或脚本文件没有设置可执行位,即指定的文件是不可执行的
E2BIG
新程序的命令行参数与环境变量容量之和超过 ARG_MAX
ENOEXEC
没有正确的格式,指定的文件无法执行
ENOMEM
没有足够的内存空间来执行指定的程序
ETXTBUSY
指定的文件被一个或多个进程以可写的方式打开
EIO
从文件系统读入文件时发生 I/O 错误

在 linux 系统下,exec 函数族可以执行二进制的可执行文件,也可以执行 shell 脚本程序,但 shell 脚本必须以下面所示的格式开头:
第一行必须为: #!interpretername [arg]
其中 interpretername 可以是 shell 或其它解释器,例如 /bin/sh 或 /usr/bin/perl,arg 是传递给解释器的参数。


测试代码
程序一:用来替换进程映像的程序
[C++] 纯文本查看 复制代码
include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[], char **environ)
{
    int i;
    printf("I am a process image\\n");
    printf("My pid = %d, parentpid = %d\\n", getpid(), getppid());
    printf("uid = %d ,gid = %d\\n", getuid(), getgid());
    
    for(i = 0; i < argc; i++) 
        printf("argv[%d]:%s\\n", i, argv[i]);
}

程序二:exec 函数实例,这里用了 execve() 函数
[C++] 纯文本查看 复制代码
#include <stdio.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <stdlib.h> 

int main(int argc, char *argv[], char **environ) 
{ 
    pid_t    pid; 
    int    stat_val; 
    
    printf("Exec example\\n"); 
    pid = fork(); 
    switch(pid) { 
        case -1: 
        perror("Process Creation failed\\n"); 
        exit(1); 
        
        case 0: 
        printf("Child process is running\\n"); 
        printf("My pid = %d, parentpid = %d\\n", getpid(), getppid()); 
        printf("uid = %d, gid  = %d\\n", getuid(), getgid()); 
        execve("processimage.exe", argv, environ); 
        printf("process never go to hear!\\n"); 
        exit(0); 
    
        default: 
        printf("Parent process is running\\n"); 
        break; 
    } 
    wait(&stat_val); 
    exit(0); 
}

运行及输出
beyes@linux-beyes:~/C> ./execve.exe
Exec example
Child process is running
My pid = 31369, parentpid = 31368
uid = 1000, gid  = 100
I am a process image
My pid = 31369, parentpid = 31368
uid = 1000 ,gid = 100
argv[0]:./execve.exe
Parent process is running

说明
从执行结果可以看到,执行新程序保持了原来进程的进程 ID、父进程ID、实际用户 ID 和实际组 ID。同时还可以看到,当调用新的可执行程序后,原有的子进程的映像被替代,不再被执行。子进程永远不会执行到 printf("process never go to here!\\n"); ,因为子进程已经被新的执行映像所替代。

执行新程序后的进程除了保持原来的进程 ID、父进程 ID、实际用户 ID 和实际组 ID 之外,进程还保持了许多原有的特征,主要有:
      当前工作目录
      根目录
      创建文件时使用的屏蔽字
      进程信号屏蔽字
      未决警告
      和进程相关的使用处理器的时间
      控制终端
      文件锁



下面示例演示 execle() 和 execlp() 的用法。

1. 在 execle() 和在 execlp() 里调用的程序 echoall 代码:
[C++] 纯文本查看 复制代码
#include <stdio.h>

int main(int argc, char *argv[])
{
        int     i;
        char    **ptr;
        extern char **environ;

        for (i = 0; i < argc; i++)      /*echo all comman-line args*/
                printf ("argv[%d] : %s\\n", i, argv[i]);

        for (ptr = environ; *ptr != 0; ptr++)
                printf ("%s\\n", *ptr);

        return (0);
}

上面代码的两个 for 循环中分别打印了命令行上的所有参数以及所有的环境变量。环境变量 environ 也是个指针数组。

具有 execle() 和 execlp() 函数演示的代码:
[C++] 纯文本查看 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

char    *env_init[] = { "USER=unknow", "PATH=/tmp", NULL };

void err_sys (char *str)
{
        perror (str);
        exit (EXIT_FAILURE);
}


int main(void)
{
        pid_t   pid;

        if ((pid = fork()) < 0) {
                err_sys("fork error");
        } else if (pid == 0) {
                if (execle("/root/bin/echoall", "echoall", "myarg1", "MY ARG2", (char *)0, env_init) < 0) {
                        err_sys("execle error");
                }
        }
        if (waitpid (pid, NULL, 0) < 0) {
                err_sys("waitpid error");
        }

        if ((pid = fork()) < 0) {
                err_sys ("fork error");
        } else if (pid == 0) {
                if (execlp ("echoall", "echoall", "only 1 arg", (char *)0) < 0)
                        err_sys ("execlp error");
        }

        exit (EXIT_SUCCESS);
}

由于这里是用 root 用户来运行这段程序(不一定非得要用root来执行),所以在运行上面程序之前,要将 echoall 拷贝到 /root/bin 目录下,因为这个目录是 $PATH 环境变量里包含的目录之一,所以在执行时 execlp() 能够从 $PATH 的目录中搜索到 echoall 。

在 execle() 和 execlp() 的将参数列表里第一个参数(echoall)设置为文件名,即 argv[0] 为 echoall 。一般情况下,如果在 shell 里直接用指定路径的方式来执行 echoall ,那么 argv[0] 就会显示执行时的全路径,比如:
$ /home/beyes/C/syscall/exec/echoall
argv[0] : /home/beyes/C/syscall/exec/echoall
$ ./echoall
argv[0] : ./echoall
实际上,argv[0] 我们完全可以自定义为任意的字符串;路径名的显示方式只是一种惯例。

在 execle() 中(execve() 函数也是),最后的一个参数可以传递一个自定义的指向环境字符串(这些字符串也自定义,如像上面程序中的 env_init )指针数组的指针;其它的函数则使用调用进程中的 environ 变量为新程序复制现有的环境。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2025-5-3 23:22 , Processed in 0.085710 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表