如何用C语言写一个简单的Unix Shell
来源:才华咖 本文已影响8.73K人
来源:才华咖 本文已影响8.73K人
shell 是允许你与操作系统的核心作交互的一个界面(interface)。下面是小编为大家带来的关于如何用C语言写一个简单的 Unix Shell的知识,欢迎阅读。
shell 是什么?
关于这一点已经有很多书面资料,所以对于它的定义我不会探讨太多细节。只用一句话说明:
shell 是允许你与操作系统的核心作交互的一个界面(interface)。
shell 是怎样工作的?
shell解析用户输入的命令并执行它。为了能做到这一点,shell的工作流程看起来像这样:
启动shell
等待用户输入
解析用户输入
执行命令并返回结果
回到第 2 步。
但在这整个流程中有一个重要的部分:进程。shell是父进程。这是我们的程序的主线程,它等待用户输入。然而,由于以下原因,我们不能在主线程自身中执行命令:
一个错误的命令会导致整个shell停止工作。我们要避免此情况。
独立的命令应该有他们自己的进程块。这被称为隔离,属于容错(机制)。
Fork
为了能避免此情况,我们使用系统调用 fork。我曾以为我理解了 fork,直到我用它写了大约4行代码(才发现我没有理解)。
fork 创建当前进程的一份拷贝。这份拷贝被称为“子进程”,系统中的每个进程都有与它联系在一起的唯一的进程 id(pid)。让我们看以下代码片段:
fork.c
#include
#include
#include
int
main() {
pid_t child_pid = fork();
// The child process
if (child_pid == 0) {
printf("### Child ###nCurrent PID: %d and Child PID: %dn",
getpid(), child_pid);
} else {
printf("### Parent ###nCurrent PID: %d and Child PID: %dn",
getpid(), child_pid);
}
return 0;
}
fork 系统调用返回两次,每个进程一次。这一开始听起来是反直觉的。但让我们看一下在底层发生了什么。
通过调用 fork,我们在程序中创建了一个新的分支。这与传统的 if-else 分支不同。fork 对当前进程创建一份拷贝并从中创建了一个新的进程。最终系统调用返回子进程的进程 id。
一旦 fork 调用成功,子进程和父进程(我们的代码的主线程)会同时运行。
fork() 创建了一个新的子进程,但与此同时,父进程的执行并没有停止。子进程执行的开始和结束独立于父进程,反之亦然。
更进一步讨论以前,先说明一点:getpid 系统调用返回当前的进程 id。
如果你编译并执行这段代码,会得到类似于下面的输出:
### Parent ###
Current PID: 85247 and Child PID: 85248
### Child ###
Current PID: 85248 and Child PID: 0
在 ### Parent ### 下面的片段中,当前进程 ID 是 85247,子进程 ID 是 85248。注意,子进程的 pid 比父进程的大,表明子进程是在父进程之后创建的。(更新:正如某人在 Hacker News 上正确指出的,这并不是确定的',虽然往往是这样。原因在于,操作系统可能回收无用的老进程 id。)
在 ### Child ### 下面的片段中,当前进程 ID 是 85248,这与前面片段中子进程的 pid 相同。然而,这里的子进程 pid 为 0。
实际的数字会随着每一次执行而变化。
你可能在想,我们已经在第 9 行明确的给 child_pid 赋了一个值(译者注:应该是第7行),那么 child_pid 怎么会在同一个执行流程中呈现两个不同的值,这种想法值得原谅。但是,回想一下,调用 fork 创建了一个新进程,这个新进程与当前进程相同。因此,在父进程中,child_pid 是刚创建的子进程的实际值,而子进程本身没有自己的子进程,所以 child_pid 的值为 0。
因此,为了控制哪些代码在子进程中执行,哪些又在父进程中执行,需要我们在 12 到 16 行定义的 if-else 块(译者注:应该是 10 到 16 行)。当 child_pid 为 0 时,代码块将在子进程下执行,而 else 块却会在父进程下执行。这些块被执行的顺序是不确定的,取决于操作系统的调度程序。
引入确定性
让我向你介绍系统调用 sleep。引用 linux man 页面的话:
sleep – 暂停执行一段时间
时间间隔以秒为单位。
让我们给父进程,即我们代码中的 else 块,加一个 sleep(1) 调用:
sleep_parent.c
#include
#include
#include
int
main() {
pid_t child_pid = fork();
// The child process
if (child_pid == 0) {
printf("### Child ###nCurrent PID: %d and Child PID: %dn",
getpid(), child_pid);
} else {
sleep(1); // Sleep for one second
printf("### Parent ###nCurrent PID: %d and Child PID: %dn",
getpid(), child_pid);
}
return 0;
}
当你执行这段代码时,输出将类似这样:
### Child ###
Current PID: 89743 and Child PID: 0
1秒钟以后,你将看到
### Parent ###
Current PID: 89742 and Child PID: 89743
每次执行这段代码时你会看到同样的表现。这是因为:我们在父进程中做了一个阻塞性的 sleep 调用,与此同时,操作系统调度程序发现有空闲的 CPU 时间可以给子进程执行。
类似的,如果你反过来,把 sleep(1) 调用加到子进程,也就是我们代码中的 if 块里面,你会发现父进程块立刻输出到控制台上。但你也会发现程序终止了。子进程块的输出被转存到标准输出。看起来是这样:
$ gcc -lreadline blog/sleep_child.c -o sleep_child && ./sleep_child
### Parent ###
Current PID: 23011 and Child PID: 23012
$ ### Child ###
Current PID: 23012 and Child PID: 0
这段源代码可在 sleep_child.c 获取。
这是因为父进程在 printf 语句之后无事可做,被终止了。然而,子进程在 sleep 调用处被阻塞了 1 秒钟,之后才执行 printf 语句。
正确实现的确定性
然而,使用 sleep 来控制进程的执行流程不是最好的方法,因为你做了一个 n 秒的 sleep 调用:
你怎么确保不管你等待的是什么,都会在 n 秒内完成执行呢?
不管你等待的是什么,要是它在远远早于 n 秒时就结束了呢?在此情况下你不必要地闲置了。
有一种更好的方法是,使用 wait 系统调用(或一种变体)来代替。我们将使用 waitpid 系统调用。它带有以下参数:
你想要程序等待的进程的进程 ID。
一个变量,用来保存进程如何终止的相关信息。
选项标志,用来定制 waitpid 的行为
wait.c
#include
#include
#include
#include
int
main() {
pid_t child_pid;
pid_t wait_result;
int stat_loc;
child_pid = fork();
// The child process
if (child_pid == 0) {
printf("### Child ###nCurrent PID: %d and Child PID: %dn",
getpid(), child_pid);
sleep(1); // Sleep for one second
} else {
wait_result = waitpid(child_pid, &stat_loc, WUNTRACED);
printf("### Parent ###nCurrent PID: %d and Child PID: %dn",
getpid(), child_pid);
}
return 0;
}
当你执行这段代码,你会发现子进程块立刻被打印,然后等待很短的一段时间(这里我们在 printf 后面加了 sleep)。父进程等待子进程执行结束,之后就有空执行它自己的命令。
这里将介绍 exec 函数家族。即以下函数:
execl
execv
execle
execve
execlp
execvp
为了满足需要,我们将使用 execvp,它的签名看起来像这样:
int execvp(const char *file, char *const argv[]);
函数名中的 vp 表明:它接受一个文件名,将在系统 $PATH 变量中搜索此文件名,它还接受将要执行的一组参数。
你可以阅读 exec 的 man 页面 以得到其它函数的更多信息。
让我们看一下以下代码,它执行命令 ls -l -h -a:
execvp.c
#include
int main() {
char *argv[] = {"ls", "-l", "-h", "-a", NULL};
execvp(argv[0], argv);
return 0;
}
关于 execvp 函数,有几点需要注意:
第一个参数是命令名。
第二个参数由命令名和传递给命令自身的参数组成。并且它必须以 NULL 结束。
它将当前进程的映像交换为被执行的命令的映像,后面再展开说明。
如果你编译并执行上面的代码,你会看到类似于下面的输出:
total 32
drwxr-xr-x 5 dhanush staff 170B Jun 11 11:32 .
drwxr-xr-x 4 dhanush staff 136B Jun 11 11:30 ..
-rwxr-xr-x 1 dhanush staff 8.7K Jun 11 11:32
drwxr-xr-x 3 dhanush staff 102B Jun 11 11:32
-rw-r--r-- 1 dhanush staff 130B Jun 11 11:32
它和你在你的主 shell 中手动执行ls -l -h -a的结果完全相同。
既然我们能执行命令了,我们需要使用在第一部分中学到的fork 系统调用构建有用的东西。事实上我们要做到以下这些:
当用户输入时接受命令。
调用 fork 以创建一个子进程。
在子进程中执行命令,同时父进程等待命令完成。
回到第一步。
我们看看下面的函数,它接收一个字符串作为输入。我们使用库函数 strtok 以空格分割该字符串,然后返回一个字符串数组,数组也用 NULL来终结。
include
#include
char **get_input(char *input) {
char **command = malloc(8 * sizeof(char *));
char *separator = " ";
char *parsed;
int index = 0;
parsed = strtok(input, separator);
while (parsed != NULL) {
command[index] = parsed;
index++;
parsed = strtok(NULL, separator);
}
command[index] = NULL;
return command;
}
如果该函数的输入是字符串 “ls -l -h -a”,那么函数将会创建这样形式的一个数组:[“ls”, “-l”, “-h”, “-a”, NULL],并且返回指向此队列的指针。
现在,我们在主函数中调用 readline 来读取用户的输入,并将它传给我们刚刚在上面定义的 get_input。一旦输入被解析,我们在子进程中调用 fork 和 execvp。在研究代码以前,看一下下面的图片,先理解 execvp 的含义:
当 fork 命令完成后,子进程是父进程的一份精确的拷贝。然而,当我们调用 execvp 时,它将当前程序替换为在参数中传递给它的程序。这意味着,虽然进程的当前文本、数据、堆栈段被替换了,进程 id 仍保持不变,但程序完全被覆盖了。如果调用成功了,那么 execvp 将不会返回,并且子进程中在这之后的任何代码都不会被执行。这里是主函数:
#include
#include
#include
#include
#include
#include
int main() {
char **command;
char *input;
pid_t child_pid;
int stat_loc;
while (1) {
input = readline("unixsh> ");
command = get_input(input);
child_pid = fork();
if (child_pid == 0) {
/* Never returns if the call is successful */
execvp(command[0], command);
printf("This won't be printed if execvp is successuln");
} else {
waitpid(child_pid, &stat_loc, WUNTRACED);
}
free(input);
free(command);
}
return 0;
}
全部代码可在此处的单个文件中获取。如果你用 gcc -g -lreadline shell.c 编译它,并执行二进制文件,你会得到一个最小的可工作 shell,你可以用它来运行系统命令,比如 pwd 和 ls -lha:
unixsh> pwd
/Users/dhanush/
unixsh> ls -lha
total 28K
drwxr-xr-x 6 root root 204 Jun 11 18:27 .
drwxr-xr-x 3 root root 4.0K Jun 11 16:50 ..
-rwxr-xr-x 1 root root 16K Jun 11 18:27
drwxr-xr-x 3 root root 102 Jun 11 15:32
-rw-r--r-- 1 root root 130 Jun 11 15:38 execvp.c
-rw-r--r-- 1 root root 997 Jun 11 18:25 shell.c
unixsh>
注意:fork 只有在用户输入命令后才被调用,这意味着接受用户输入的用户提示符是父进程。
错误处理
到目前为止,我们一直假设我们的命令总会完美的运行,还没有处理错误。所以我们要对 shell.c做一点改动:
fork – 如果操作系统内存耗尽或是进程数量已经到了允许的最大值,子进程就无法创建,会返回 -1。我们在代码里加上以下内容:
...
while (1) {
input = readline("unixsh> ");
command = get_input(input);
child_pid = fork();
if (child_pid < 0) {
perror("Fork failed");
exit(1);
}
...
execvp – 就像上面解释过的,被成功调用后它不会返回。然而,如果执行失败它会返回 -1。同样地,我们修改 execvp 调用:
...
if (execvp(command[0], command) < 0) {
perror(command[0]);
exit(1);
}
...
注意:虽然fork之后的exit调用终止整个程序,但execvp之后的exit 调用只会终止子进程,因为这段代码只属于子进程。
malloc – It can fail if the OS runs out of memory. We should exit the program in such a scenario:
malloc – 如果操作系统内存耗尽,它就会失败。在这种情况下,我们应该退出程序:
char **get_input(char *input) {
char **command = malloc(8 * sizeof(char *));
if (command == NULL) {
perror("malloc failed");
exit(1);
}
...
动态内存分配 – 目前我们的命令缓冲区只分配了8个块。如果我们输入的命令超过8个单词,命令就无法像预期的那样工作。这么做是为了让例子便于理解,如何解决这个问题留给读者作为一个练习。
上面带有错误处理的代码可在这里获取。
内建命令
如果你试着执行 cd 命令,你会得到这样的错误:
cd: No such file or directory
我们的 shell 现在还不能识别cd命令。这背后的原因是:cd不是ls或pwd这样的系统程序。让我们后退一步,暂时假设cd 也是一个系统程序。你认为执行流程会是什么样?在继续阅读之前,你可能想要思考一下。
流程是这样的:
用户输入 cd /。
shell对当前进程作 fork,并在子进程中执行命令。
在成功调用后,子进程退出,控制权还给父进程。
父进程的当前工作目录没有改变,因为命令是在子进程中执行的。因此,cd 命令虽然成功了,但并没有产生我们想要的结果。
因此,要支持 cd,我们必须自己实现它。我们也需要确保,如果用户输入的命令是 cd(或属于预定义的内建命令),我们根本不要 fork 进程。相反地,我们将执行我们对 cd(或任何其它内建命令)的实现,并继续等待用户的下一次输入。,幸运的是我们可以利用 chdir 函数调用,它用起来很简单。它接受路径作为参数,如果成功则返回0,失败则返回 -1。我们定义函数:
int cd(char *path) {
return chdir(path);
}
并且在我们的主函数中为它加入一个检查:
while (1) {
input = readline("unixsh> ");
command = get_input(input);
if (strcmp(command[0], "cd") == 0) {
if (cd(command[1]) < 0) {
perror(command[1]);
}
/* Skip the fork */
continue;
}
...
带有以上更改的代码可从这里获取,如果你编译并执行它,你将能运行 cd 命令。这里是一个示例输出:
unixsh> pwd
/Users/dhanush/
unixsh> cd /
unixsh> pwd
/
unixsh>
第二部分到此结束。这篇博客帖文中的所有代码示例可在这里获取。在下一篇博客帖文中,我们将探讨信号的主题以及实现对用户中断(Ctrl-C)的处理。敬请期待。
How to Learn English Well 如何学好英语作文(通用6篇)
linux shell编程对变量的赋值
如何在Linux系统上安装Eclipse
Linux中Firefox如何安装Flash插件
Linux Shell文本处理工具
Health in developing countries大学六级英语写作范文
Linux shell常用命令汇总
初二年级Lesson11Lily learns about China英语家庭作业
雅思阅读模拟试题:Sun's fickle heart may leave us cold
托福英语写作范文:Examinations exert a pernicious influence on educat
英语阅读:African Union Leaders Talk Mali, Chinese Investmen
十个Linux bash shell小技巧
如何在Linux CentOS上编译并安装Clang
Linux使用shell脚本监控高速网络流量的方法
英语阅读:Life is inevitable consequence of physics
英语作文Why Are Bicycles So Commonly Used in China
如何用Windows通过网络安装RedHat Linux
The Sprint in College Entrance Exam英语作文
如何写个人的简历
php中allow-url-include的应用分析
Linux如何安装使用logwatch以便处理日志禁止
Linux Shell脚本系列教程详细介绍
Excel中的countIf函数如何使用
人教PEP版小学五年级英语下册Unit3Myschoolcalender.ALet’stalk
如何用数字语言书写个人简历
Scientists Build Robots to Live With Humans英语美文
linux上使用strace查看C语言级别的php源码的方法
Win10中如何安装SUSE Linux子系统
如何写一份简单的辞职报告
UN Chief Urges Leaders to Deliver on Climate Change美文欣赏
Wuhan Heping Middle School 英语作文
英语阅读:Are You Tricked by These Optical Illusions
linux shell编程变脸赋值和字符串操作
高中英语作文:How to learn English well 如何学好英语
雅思口语范文:My Experience as an English Learner
Linux系统xlsatom命令如何使用
5BUnit9TheEnglishClub的教学设计
如何写好一份简单的英文简历
Linux关闭selinux安全子系统的技巧
linuxApache如何支持asp配置