8.11 解释器文件

8.11 解释器文件

这种文件是文本文件,其起始行的形式是:

#pathname [optional- a rg u m e nt]

在惊叹号和pathname之间的空格是可任选的。最常见的是以下列行开始:

#/bin/sh

pathname通常是个绝对路径名,对它不进行什么特殊的处理(不使用PAT H进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中pathname所指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。

很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数以及空格数。

是否一定需要解释器文件呢?那也不完全如此。但是它们确实使用户得到效率方面的好处,其代价是内核的额外开销(因为内核需要识别解释器文件)。由于下述理由,解释器文件是有用的:

(1) 某些程序是用某种语言写的脚本,这一事实可以隐藏起来。例如,为了执行程序8 – 11,只需使用下列命令行:

awkexample optional- arguments

而并不需要知道该程序实际上是一个awk脚本,否则就要以下列方式执行该程序:

awk -f awkexample optional- arguments

(2) 解释器脚本在效率方面也提供了好处。再考虑一下前面的例子。仍旧隐藏该程序是一个awk脚本的事实,但是将其放在一个shell脚本中:

awk ‘BEGIN {

for (i = 0; i < ARGC; i++)

printf “ARGV[%d] = %sn”, i, ARGV[i]

exit

}’ $*

这种解决方法的问题是要求做更多的工作。首先, shell读此命令,然后试图execlp此文件名。因为shell脚本是一个可执行文件,但却不是机器可执行的,于是返回一个错误, execlp就认为该文件是一个shell脚本(它实际上就是这种文件)。然后执行/bin/sh,并以该shell脚本的路径名作为其参数。shell正确地执行我们的shell脚本,但是为了运行awk程序,它调用fork , execwait。用一个shell脚本代替解释器脚本需要更多的开销。

(3) 解释器脚本使我们可以使用除/bin/sh以外的其他shell来编写shell脚本。当execlp找到一个非机器可执行的可执行文件时,它总是调用/bin/sh来解释执行该文件。但是,用解释器脚本,则可编写成:

# ! / bin / csh

(在解释器文件中后随C shell脚本)

再一次,我们也可将此放在一个/bin/sh脚本中(然后由其调用C shell),但是要有更多的开销。如果三个shellawk没有用#作为注释符,则上面所说的都无效。

8.11 更改用户ID和组ID

8.11 更改用户ID和组ID

      UNIX系统中,特权是基于用户和组ID的,当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们就需要更换自己的用户ID或组ID

      一般而言,在设计应用程序的时候,我们总是试图使用最小特权(lease privilege)模型。

可以用setuid函数设置实际用户ID和有效用户ID。与此类似,可以用s e t g i d函数设置实际组ID和有效组ID

#include <unistd.h>

int setuid(uid_t uid) ;

int setgid(gid_t g i d) ;

//两个函数返回:若成功则为0,若出错则为-1

关于谁能更改ID有若干规则。现在先考虑有关改变用户ID的规则(在这里关于用户ID所说明的一切都适用于组ID)。

l  若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置用户ID设置为uid

l  若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户– ID,则setuid只将有效用户ID设置为uid。不改变实际用户ID和保存的设置用户– ID

l  如果上面两个条件都不满足,则e r r n o设置为E P E R M,并返回出错。

关于内核所维护的三个用户ID,还要注意下列几点:

l  只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登录时,由login ( 1 )程序设置的,而且决不会改变它。因为login是一个超级用户进程,当它调用setuid时,设置所有三个用户ID

l  仅当对程序文件设置了设置用户ID位时, exec函数设置有效用户ID。如果设置用户– ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户– ID。自然,不能将有效用户ID设置为任一随机值。

l  保存的设置用户– ID是由exec从有效用户ID复制的。在exec按文件用户ID设置了有效用户ID后,即进行这种复制,并将此副本保存起来。

8.10.1 setreuidsetregid函数

4 . 3 + BSD支持setregid函数,其功能是交换实际用户ID和有效用户ID的值。

#include <unistd.h>

int setreuid(uid_t ruid, uid_t e uid) ;

int setregid(gid_t rg i d, gid_t e g i d) ;

//两个函数返回:若成功则为0,若出错则为-1

8.10.2 seteuidsetegid函数

在对P O I X . 1的建议更改中包含了两个函数seteuidsetegid。它们只更改有效用户ID和有效组ID

#include <unistd.h>

int seteuid(uid_t uid) ;

int setegid(gid_t g i d) ;

//两个函数返回:若成功则为0,若出错则为-1

8.10 exec函数

8.10 exec函数

fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其m a i n函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是U N I X进程控制原语。fork可以创建新进程,用exec可以执行新的程序exit函数和两个wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。在后面各节中将使用这些原语构造另外一些如popensystem之类的函数。

#include <unistd.h>

int execl(const char *pathname, const char * a rg 0, ... /* (char *) 0 */);

int execv(const char *pathname, char *const a rgv [] );

int execle(const char *pathname, const char * a rg 0, ...

/* (char *)0, char *const envp [] */);

int execve(const char *pathname , char *const a rgv [], char *consten vp [] );

int execlp(const char *filename, const char * a rg 0, ... /* (char *) 0 */);

int execvp(const char *filename, char *const a rgv [] );

//六个函数返回:若出错则为- 1,若成功则不返回

      可以看出这些函数的区别是前4个取路径名为参数,后2个则取文件名作为参数。

当指定filename时:

如果filename中包含/,则就将其视为路径名。

否则就按PAT H环境变量,在有关目录中搜寻可执行文件。

PAT H变量包含了一张目录表(称为路径前缀),目录之间用冒号( : )分隔。例如下列n a m e = value环境字符串:

P A T H = / bin : / u s r / bin : / u s r / l o c a l / bin :.

如果execlpexec v p中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑程序产生的机器可执行代码文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。不是可执行代码就是shell脚本。

      第二个区别与参数表的传递有关( l表示表( l i s t )v表示矢量( vector ) )。函数execlexeclpexecle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外三个函数( exec v, exec v pexec v e ),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这三个函数的参数。

在使用ANSI C原型之前,对execl , execleexeclp三个函数表示命令行参数的一般方法是:

char * arg0 , char *arg 1, …, char *arg n, (char *) 0

应当特别指出的是:在最后一个命令行参数之后跟了一个空指针。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则它将被解释为整型参数。如果一个整型数的

长度与char *的长度不同,exec函数实际参数就将出错。

最后一个区别与向新程序传递环境表相关。e结尾的两个函数( execleexec v e

可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。(回忆7 . 9节及表7 – 2中对环境字符串的讨论。其中曾提及如果系统支持s e t e n vp u t e n v这样的函数,则可更改当前环境和后面生成的子进程的环境,但不能影响父进程的环境。)通常,一个进程允许将其环境传播给其子进程,但有时也有这种情况,进程想要为子进程指定一个确定的环境。例如,在初始化一个新登录的shell时, login程序创建一个只定义少数几个变量的特殊环境,而在我们登录时,可以通过shell起动文件,将其他变量加到环境中。在使用ANSI C 原型之前, execle 的参数是:

char * pathname, char *arg 0, , char *arg n, (char *)0, char *envp[ ]

从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在ANSI C原型中,所有命令行参数,包括空指针, envp指针都用省略号()表示。

这六个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母p表示该函数取filename作为参数,并且用PAT H环境变量寻找可执行文件。字母l表示该函数取一个参数表,它与字母v互斥。v表示该函数取一个argv[ ]。最后,字母e表示该函数取envp[ ] 数组,而不使用当前环境。表8 – 4显示了这六个函数之间的区别。

函数

pathname

filename

参数表

argv[]

environ

envp[]

execl

·

 

·

 

·

 

execlp

 

·

·

 

·

 

execle

·

 

·

 

 

·

execv

·

 

 

·

·

 

execvp

 

·

 

·

·

 

execve

·

 

 

·

 

·

字母表示

 

p

 

v

 

e

每个系统对参数表和环境表的总长度都有一个限制。在表2 – 7中,这种限制是A R G _ M A X。在P O S I X . 1系统中,此值至少是4 0 9 6字节。当使用shell的文件名扩充功能产生一个文件名表时,可能会受到此值的限制。例如,命令

grep _POSIX_SOURCE /usr/include/*/*.h

在某些系统上可能产生下列形式的shell错误:

arg list too long

前面曾提及在执行exec后,进程ID没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征:

进程ID和父进程ID

实际用户ID和实际组ID

添加组ID

进程组ID

对话期ID

控制终端。

闹钟尚余留的时间。

当前工作目录。

根目录。

文件方式创建屏蔽字。

文件锁。

进程信号屏蔽。

未决信号。

资源限制。

• tms_utime, tms_stime, tms_cutime以及t m s _ u s time值。

对打开文件的处理与每个描述符的exec关闭标志值有关。见图3 – 1以及3 . 1 3 节中对F D _ C L O EXEC的说明,进程中每个打开描述符都有一个exec关闭标志。若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用f c n t l设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开。

P O S I X . 1明确要求在exec时关闭打开目录流(见4 . 2 1节中所述的o p e n d i r函数)。这通常是由o p e n d i r函数实现的,它调用f c n t l函数为对应于打开目录流的描述符设置exec关闭标志。

注意,在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序的文件的设置用户– ID位和设置– ID位是否设置。如果新程序的设置用户– ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对组ID的处理方式与此相同。在很多U N I X实现中,这六个函数中只有一个exec v e是内核的系统调用。另外五个只是库函数,它们最终都要调用系统调用。这六个函数之间的关系示于图8 – 2中。在这种安排中,库函数execlp execvp 使用PAT H环境变量查找第一个包含名为filename的可执行文件的路径名前缀。

clip_image001

6exec函数之间的关系

程序8 – 8例示了exec函数。

#include <unistd.h>

#include <stdlib.h>

#include <sys/wait.h>



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



int main(void)

{

pid_t pid;



if ((pid = fork()) < 0) {

perror("fork error");

} else if (pid == 0) { /* specify pathname, specify environment */

if (execle("/usr/bin/lscpu", "lscpu", "myarg1",

"MY ARG2", (char *) 0, env_init) < 0)

perror("execle error");

}



if (waitpid(pid, NULL, 0) < 0)

perror("wait error");



if ((pid = fork()) < 0) {

perror("fork error");

} else if (pid == 0) { /* specify filename, inherit environment */

if (execlp("ls", "ls", ".", (char *) 0) < 0)

perror("execlp error");

}



exit(0);

}

在该程序中先调用execle,它要求一个路径名和一个特定的环境。下一个调用的是execlp,它用一个文件名,并将调用者的环境传送给新程序。execlp在这里能够工作的原因是因为目录/ bin是当前路径前缀之一。注意,我们将第一个参数(新程序中的a rgv [0])设置为路径名的文件名分量。某些shell将此参数设置为完全的路径名。

在程序8 – 8中要执行两次的程序e c h o a l l示于程序8 – 9中。这是一个普通程序,它回送其所有命令行参数及其全部环境表。

#include <stdio.h>

#include <stdlib.h>



int main(int argc, char *argv[])

{

int i;

char **ptr;

extern char **environ;



for (i = 0; i < argc; i++) /* echo all command-line args */

printf("argv[%d]: %sn", i, argv[i]);



for (ptr = environ; *ptr != 0; ptr++) /* and all env strings */

printf("%sn", *ptr);



exit(0);

}