Unix命令处理器或者Shell都是进程,它获取用户键入的命令,fork出一个进程,子进程调用exec来执行用户的命令,父进程等待子进程执行结束。
这是一段简单的shell伪代码:
pid_t pid;
while (1) {
GetNextCommand();
pid = fork();
if (pid == 0) {
ExecCommand();
PrintErrorMsg()
exit(0);
}
else if (pid > 0)
wait();
else
HandleForkFailure();
}
Unix 允许用户通过在命令行中添加与号 (&) 来实现在后台运行命令,当进程在后台运行时,他不能从终端接收用户输入,并立即显示 shell 提示符,允许用户在后台进程完成之前输入新命令。对于上面的伪代码,如果想要在后台运行,开发者要做些什么呢?
这是在Win32上创建新进程的API:
BOOL CreateProcess(
LPCTSTR lpApplicationName, /* executable program */
LPTSTR lpCommandLine, /* command line args */
LPSECURITY_ATTRIBUTES lpProcessAttributes, /* use NULL */
LPSECURITY_ATTRIBUTES lpThreadAttributes, /* use NULL */
BOOL bInheritHandles, /* does proc inherit parents open handles */
DWORD dwCreationFlags,
LPVOID lpEnvironment, /* if NULL, use parent environment */
LPCTSTR lpCurrentDirectory, /* if NULL, use parent curr dir */
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
typedef struct PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;
CreateProcess相当于Unix上的fork()和exec()。相比于Unix的系统调用,Win32 APIs几乎都会有很多的参数。
- LPCTSTR lpApplicationName 这是要执行的进程的路径名,相当于 Unix execl 命令的第一个参数,它可以是相对路径名或绝对路径名。
- LPTSTR lpCommandLine 这个参数通常是 NULL
- LPSECURITY_ATTRIBUTES lpProcessAttributes 进程属性,通常是NULL
- LPSECURITY_ATTRIBUTES lpThreadAttributes 线程属性,通常是NULL
- BOOL bInheritHandles 如果该值为 TRUE,则每表示个打开的文件句柄(或其他句柄)也可以在子进程中打开
- DWORD dwCreationFlags 通过该值设置一些标志,默认值是0
- LPVOID lpEnvironment 允许开发者改变环境,如果该值取NULL表示子进程继承父进程的环境
- LPCTSTR lpCurrentDirectory 允许开发者改变目录,如果为 NULL,则子进程与父进程有相同的当前工作目录
- LPSTARTUPINFO lpStartupInfo 这是一个指向有关如何呈现新进程的信息的指针。 在Windows环境中,一个新进程通常是一个新窗口,它会告诉操作系统该窗口放置在哪里,窗口的高度和宽度以及其他信息。
- LPPROCESS_INFORMATION lpProcessInformation 这是一个向父级返回有关子级的信息的结构,例如进程 ID 和句柄。
与大多数 Win32 API 一样,此调用在成功时返回 TRUE,在失败时返回 FALSE。
下面是一段简短的示例代码,用于启动记事本。
#include <windows.h>
#include <stdio.h>
#include <string.h>
char *GetErrorMessage()
{
char *ErrMsg;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &ErrMsg,
0,
NULL
);
return ErrMsg;
}
int main()
{
char commandline[255];
PROCESS_INFORMATION ProcessInfo;
STARTUPINFO StartupInfo;
strcpy(commandline,"notepad");
GetStartupInfo(&StartupInfo);
if (CreateProcess (
"c:\\winnt\\notepad.exe",
commandline, NULL, NULL,
FALSE, 0, NULL, NULL,
&StartupInfo,
&ProcessInfo) == TRUE)
{
printf("Create was successful\n");
printf("Proc id is %d\n",ProcessInfo.dwProcessId);
}
else
{
printf("error, CreateProcess failed, error number %d\n",GetLastError());
printf("%s\n",GetErrorMessage());
}
return 0;
}
请特别注意 CreateProcess 的第一个参数,由于反斜杠是字符串中的转义字符,因此字符串中的反斜杠字符必须用双反斜杠表示。
这在 Unix 中不会出现太多,但在 Windows 中却是个一直存在的问题,因为反斜杠是目录分隔符。
不管在 Unix上,还是在Windows 上,当创建一个新进程时,都会自动创建三个 I/O 流,它们称之为标准输入、标准输出和标准错误。
监视器默认有两个单独的输出流的原因是,有时用户希望将正常输出重定向到文件,但希望将错误消息显示在终端上(或发送到 不同的文件)。
用户可以在 shell 提示符下使用 > 运算符将进程的输出从终端重定向到文件。 例如:
ls -l > outfile
这个操作会将 ls -l
命令的输出发送到名为 outfile 的文件,而不是终端。
shell 还可以将标准输入从键盘重定向到带有 < 字符的文件。
每个 Unix 进程都有一个与其关联的文件描述符数组。系统调用 open 就返回一个文件描述符,文件描述符是一个低位正整数。
按照约定,标准输入是文件描述符 0,标准输出是文件描述符 1,标准错误是文件描述符 2。
当程序打开文件时,通常会为其分配最低的未使用文件描述符。写入到终端的函数(例如 printf)或者从键盘读取的函数(例如 scanf、getchar)都会调用写入和读取系统调用,并将文件描述符设置为标准输出和标准输入。
如果开发者想要将标准输入或标准输出从程序内重定向到文件,请使用 dup 或 dup2 系统调用。 这两个调用会复制一个文件描述符,对 dup2 的调用需要两个参数:fd1 和 fd2,它们都是文件描述符。 第一个参数 fd1 必须是已打开的文件描述符, 该调用使 fd2 引用与 fd1 相同的文件。 如果fd2打开,则首先要把它关闭。
请看下面的例子:
/* dup2.c - a demo of the dup2 call */
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
int main()
{
int fd, retval;
fd = open("temp",O_WRONLY | O_CREAT | O_TRUNC, 0200 | 0400);
/* fd is probably 3, the lowest unused file descriptor */
if (fd < 0) {
perror("Error on opening temp");
exit(0);
}
printf("This line written on the terminal\n");
retval = dup2(fd,1);
if (retval != 1) {
perror("Error on dup2");
exit(0);
}
printf("This line is written to the file temp\n");
return 0;
}
该程序打开一个名为 temp 的文件进行写入,然后它调用 printf,将一行写入终端,下一行是程序的关键语句:
retval = dup2(fd, 1);
该行使文件描述符 1 引用 fd 引用的文件,由于文件描述符 1 引用标准输出,因此首先要将其关闭。
在 printf 函数中,有一行代码如下所示:
n = write(1,.....
通常,文件描述符1指的是标准输出,但我们的程序重新定义了它,使其指的是文件temp,因此,对 write 的调用将写入文件而不是终端。如果成功,dup2 返回其第二个参数的值,否则返回负值。
dup 是 dup2 的老版本,它只使用一个参数,即要复制的文件描述符,并将最低的未使用文件描述符设置为等效。 要使用此功能,请在调用 dup 之前根据需要关闭文件描述符 0 或 1。
请参见dup2的帮助文档。
- NAME
dup2 - 复制打开的文件描述符- SYNOPSIS
#include <unistd.h>
int dup2(int fildes, int fildes2);- DESCRIPTION
dup2() 函数使文件描述符 fildes2 引用与 fildes 相同的文件。 fildes 参数是引用打开文件的文件描述符,fildes2 是一个非负整数,小于调用进程允许的打开文件描述符最大数量的当前值。 请参阅 getrlimit(2)。 如果 fildes2 已经引用了一个打开的文件,而不是 fildes,则首先将其关闭。 如果fildes2引用fildes,或者fildes不是有效的打开文件描述符,则fildes2不会首先被关闭。dup2() 函数相当于 fcntl(fildes, F_DUP2FD, fildes2).- RETURN VALUES
成功完成后,将返回表示文件描述符的非负整数。 否则,返回-1并设置errno以指示错误。- ERRORS
- EBADF - fildes参数不是有效的打开文件描述符。.
- EBADF - files2参数为负数或不小于 getrlimit(RLIMIT_NOFILE, …) 返回的当前资源限制。
- EINTR - 在调用dup2时返回了一个信号。
- EMFILE - 进程打开了过多的文件。
调用 fork 后会创建一个与旧进程相同的新进程,父进程的文件描述符表也会复制到子进程,因此如果父进程有一个打开的文件,子进程将继承这个打开的文件。 此外,如果父进程复制了文件描述符,子进程也将继承它,文件描述符表也会通过对系统调用 exec 系列成员的调用进行复制。
这对于 shell 的实现很重要,shell 分叉出一个新进程来执行命令,如果用户指定输出到文件而不是标准输出,那么shell 可以使用 dup2 来将标准输出重定向到文件,然后再调用 exec 来执行命令。
祭出一个编程小作业:
请编写一个简单的 shell,它在一个无限循环中重复执行以下操作:
用户输入的命令行应包含一个命令,该命令可以是相对路径名或绝对路径名,后跟零个或多个参数,然后是输入和输出重定向。 stdin 或 stdout 都可以重定向,或者两者都不能重定向。
如果用户附加一个与号 (&),则该进程应在后台运行。
以下都是有效的命令行:
a.out
a.out arg1 arg2
a.out arg1 arg2 > outfile < infile
a.out arg1 arg2>outfile &
这个作业最难的部分是解析命令行,您可以下载一个文件 ParseCommandLine.h,它定义了一个 CommandData 结构体,包含执行命令所需的命令行中的所有数据。
struct CommandData {
char *command; /* the pathname of the command */
char *args[11]; /* arguments to the command */
int numargs; /* the number of arguments */
char *infile; /* the file for input redirection, NULL if none */
char *outfile; /* the file for output redirection, NULL if none */
int background; /* 0 if process is to run in foreground, 1 if in background */
};
它还定义了一个函数:
int ParseCommandLine(char *line, struct CommandData *data)
它接受两个参数,即用户输入的命令行,以及指向 CommandData 结构的指针(其内存必须由调用函数分配)。 该函数解析命令行并填充 CommandData 结构:
在后一种情况下,数据的内容是不确定的。
某些对于真实 shell 具有特殊含义的字符不能与此 shell 一起使用。
如果命令是 quit 你的程序应该终止。
您的 shell 应该执行该命令,根据需要重定向 stdin 和 stdout,并将参数传递给程序。 请注意,按照约定(因此,对于此分配),argv[0] 是实际命令,您必须填写此命令。
如果命令行是:
a.out arg1 arg2 <infile> outfile
当a.out启动时,argv[0]的值应该是a.out,argv[1]的值应该是arg1,argv[2]的值应该是arg2。
您的 shell 不需要搜索路径,所有相对路径名都应从当前工作目录开始。 请注意,这意味着如果命令行是 ls,您的程序将回复 command not find(除非当前目录中碰巧存在名为 ls 的可执行文件),要运行 ls,用户必须输入 /bin/ls (并注意您的 shell 必须将 argv[0] 设置为 ls)。