在一个程序里执行一个命令字符串是很方便的。例如,假定我们想把一个时间日期戳放入一个特定的文件。我们可以使用在6.10节描述的函数来完成:调用
time来得到当前日历时间,然后调用localtime把它转换成分解时间,接着调用strftime来格式化这个结果,并把结果写入到这个文件里。然而,这样做会容易得多:
system("date > file");
ISO C定义了system函数,但是它的操作有很强的系统依赖性。POSIX.1包含了system接口,基于ISO C定义展开来描述在一个POSIX环境里的它的行为。
- #include <stdlib.h>
- int system(const char *cmdstring);
- 返回值如下。
如果cmdstring是一个空指针,system仅当一个命令处理器可用时返回非0值。这个特性决定了system函数是否被指定的操作系统支持。在UNIX系统下,system一直可用。
因为system是通过调用fork、exec和waitpid来实现的,所以有三种返回值的类型:
1、如果fork失败或waitpid返回一个错误而不是EINTR,system返回-1,并设置errno为指定错误。
2、如果exec失败,暗示shell不能被执行,返回值就好像shell执行了exit(127)。
3、否则,所有三个函数--fork、exec和waitpid--成功,从system的返回值是外壳的终止状态,以waitpid指定的格式。一些
system的早期实现返回一个错误(EINTR),如果waitpid被一个捕获到的信号中断。因为没有一个程序可以使用的清理策略来从这种错误类型返
回,POSIX随后加上需求,system在这种情况不返回一个错误。(我们在10.5节讨论中断的系统调用)。
下面的代码展示了一个system函数的实现。它不处理的特性就是信号。我们将在10.8节用信号处理更新这个函数。
- #include <sys/wait.h>
- #include <errno.h>
- #include <unistd.h>
- int
- system(const char *cmdstring) /* version without signal handling */
- {
- pid_t pid;
- int status;
- if (cmdstring == NULL)
- return(1); /* always a command processor with UNIX */
- if ((pid = fork()) < 0)
- status = -1; /* probaly out of processes */
- else if (pid == 0) { /* child */
- execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
- _exit(27); /* execl error */
- } else { /* parent */
- while (waitpid(pid, &status, 0) < 0) {
- if (errno != EINTR) {
- status = -1; /* error other than EINTR from waipid() */
- break;
- }
- }
- }
- }
外壳的-c选项告诉它接受下一个参数--在这种情况下是cmdstring--作为它的命令输入而不是从标准输入或从一个给定的文件读取。外壳解析这个
null终止的C字符串并把它分解为这个命令的单独的命令行参数。传给外壳的真实的命令字符串可以包含任何合法的外壳命令。例如,使用<和>
的输入和输出重定向可以被使用。
如果我们没有使用外壳来执行这个命令,但是自己尝试执行这个命令,那会更难。首先,我们想要调用execlp而不是execl,来使用PATH变量,就和
shell一样。我们也必须为execlp的调用把这个null终止的字符串分解为分隔的命令行参数。最终,我们不能使用任何外壳元字符。
注意我们调用_exit而不是exit。我们这样是避免任何通过fork从父进程拷贝到子进程的标准I/O缓冲被冲洗到子进程。
我们可以用下面的代码来测试这个版本的system。(pr_exit函数是8.5节定义的。)
- #include <sys/wait.h>
- void pr_exit(int status);
- int system(const char *cmdstring);
- int
- main(void)
- {
- int status;
- if ((status = system("date")) < 0) {
- printf("system() error\n");
- }
- pr_exit(status);
- if ((status = system("nosuchcommand")) < 0) {
- printf("system() error\n");
- }
- pr_exit(status);
- if ((status = system("who; exit 44")) < 0) {
- printf("system() error\n");
- }
- pr_exit(status);
- exit(0);
- }
程序运行结果:
$ ./a.out
2012年 03月 02日 星期五 23:51:25 CST
normal termination, exit status = 0
sh: nosuchcommand: not found
normal termination, exit status = 127
tommy pts/0 2012-03-02 21:00 (:0)
normal termination, exit status = 44
使用system而不直接使用fork和exec的优点是,system处理所有的错误和(在我们10.18节的这个函数的版本里)所有需要的信号处理。
早期系统,包括SVR3.2和4.3BSD,没有waitpid函数可用。相反,父进程等待子进程,使用一个如下的语句:
while ((lastpid = wait(&status)) != pid && lastpid != -1)
;
如果调用system的进程在调用system之前产生了它自己的子进程,则会发生一个问题。因为上面的while语句持续循环,直到system产生的
子进程终止,如果进程的任何子进程在被pid标识的进程前终止,那么进程ID和其它进程的终止状态会被while语句舍弃掉。事实上,无法等待一个指定的
子进程是POSIX.1
Rationale引入waitpid函数的原因之一。我们将在15.3节看到发生popen和pclose函数上的同样的问题,如果系统没有提供一个
waitpid函数。
设置用户ID程序
如果我们从一个设置用户ID程序调用system会发生什么呢?这样做是一个安全漏洞,而且不该这样。下面的代码展示一个简单的程序,只为它的命令行参数调用system:
- #include <stdlib.h>
- void pr_exit(const char *status);
- int
- main(int argc, char *argv[])
- {
- int status;
- if (argc < 2) {
- printf("command-line argument required\n");
- exit(1);
- }
- if ((status = system(argv[1])) < 0) {
- printf("system() error\n");
- exit(1);
- }
- pr_exit(status);
- exit(0);
- }
我们把这个程序编译成可执行文件tsys。
下面代码展示了另一个简单的程序,打印它的真实和有效用户ID:
- #include <stdio.h>
- int
- main(void)
- {
- printf("real uid = %d, effective uid = %d\n", getuid(), geteuid());
- exit(0);
- }
我们把这个程序编译成可执行文件printuids。运行这两个程序的结果为:
$ ./tsys ./printuids
real uid = 1000, effective uid = 1000
normal termination, exit status = 0
$ su
密码:
# chown root tsys
# chmod u+s tsys
# ls -l tsys
-rwsrwxr-x 1 root tommy 7334 2012-03-03 20:23 tsys
# exit
exit
$ ./tsys ./printuids
real uid = 1000, effective uid = 0
normal termination, exit status = 0
我们给tsys程序的超级用户权限通过system的fork和exec被得到了。
当/bin/sh是bash版本2时,前一个例子不会工作,因为bash将会把有效用户ID设置为真实用户ID,当它们不匹配时。
如果它用特殊权限运行时--设置用户ID或设置组ID--并想产生另一个进程,那么一个进程应该直接直接使用fork和exec,可以确保在调用exec前和fork后变回普通权限。system函数不应该从一个设置用户ID或设置组ID程序使用。
劝告的原因是system调用shell来解析命令字符串,shell使用它的IFS变量作为输入域的分隔。shell的早期版本当被调用时没有把这个变
量重置为一个普通的字符集。这允许一个恶意用户在system被调用前设置IFS,导致system来执行一个不同的程序。