内容简介:编写一个 Unix Shell - 第一部分
(译自 https://indradhanush.github.io)
我正在 RC(Recurse Center,纽约市的一个 程序员 教育特色社区,译者注)尝试一个项目,就是编写 UNIX shell。 这是将要发布的一系列帖子的第一篇。
什么是 Shell?
很多人都写了这一点,所以我不会太涉及这个定义的细节。 然而,用一句话来概括 -
Shell 是一个接口,使您可以与操作系统内核进行交互。
Shell 如何工作?
Shell 解析用户输入的命令并执行此操作。 为了能够做到这一点,shell 的工作流程是这样的:
- 启动 shell
- 等待用户输入
- 解析用户输入
- 执行命令并返回结果
- 返回到
2。
所有这一切都有一个重要的原因:进程。 Shell 是父进程。 它是我们程序的 main 线程,等待用户输入。 但是,我们不能在 main 线程本身中执行命令,原因如下:
- 错误的命令会导致整个 shell 停止工作。 我们想避免这种情况。
- 独立命令应该有自己的进程块。 我们称之为隔离,属于容错的范畴。
Fork(复刻/分叉)
为了能够避免这种情况,我们使用系统调用 fork 。 我原以为自己理解 fork ,直到用它写了四行代码。
fork 创建当前进程的副本。 该副本称为 child ,系统中的每个进程都有与之相关联的唯一进程标识(pid)。 我们来看看下面的代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main() {
pid_t child_pid = fork();
// The child process
if (child_pid == 0) {
printf("### Child ###\nCurrent PID: %d and Child PID: %d\n",
getpid(), child_pid);
} else {
printf("### Parent ###\nCurrent PID: %d and Child PID: %d\n",
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 ,子进程的为 85248 。 需要注意的是子进程的 pid 比父进程大,这意味着子进程是在父进程之后创建的。
在 ### Child ### 下的块中,当前进程 ID 为 85248 ,这与前一个块中的子进程的 pid 相同。 但是,这里子进程的 pid 是 0 。
实际数字在每次执行时可能有所不同。
当在 代码第 9 行 已经明确地为 child_pid 赋值后, child_pid 在同一个执行流程中怎么能够获取到两个不同的值?有这种想法情有可原。 然而,记住,调用 fork 创建出了一个与当前进程相同的新进程。 因此,在父进程中, child_pid 是刚刚创建的子进程的实际值,子进程本身没有自己的子进程,结果 child_pid 的值为 0 。
因此,我们从第 12 行到第 16 行定义的 if-else 块是必需的,用来控制在子代与父级分别执行的代码。 当 child_pid 为 0 时,代码块将在子进程下执行,而 else 块将在父进程下执行。 块被执行的顺序无法确定,取决于操作系统的调度器。
确定性简介
让我来介绍一下 sleep 系统调用。 引用 linux 手册:
sleep – suspend execution for an interval of time
时间间隔以秒为单位。
让我们给父进程添加一个 sleep(1) 调用,在代码的 else 块:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main() {
pid_t child_pid = fork();
// The child process
if (child_pid == 0) {
printf("### Child ###\nCurrent PID: %d and Child PID: %d\n",
getpid(), child_pid);
} else {
sleep(1); // Sleep for one second
printf("### Parent ###\nCurrent PID: %d and Child PID: %d\n",
getpid(), child_pid);
}
return 0;
}
而执行此操作时,输出将类似于:
### Child ### Current PID: 89743 and Child PID: 0
and after a span of 1 second, you would see
### Parent ### Current PID: 89742 and Child PID: 89743
每次执行代码时,都会看到相同的行为。 这是因为我们在父进程中执行了阻塞式的 sleep 调用, 于此同时操作系统调度器寻找到空闲 CPU 时间片执行子进程。
类似地,如果把 sleep(1) 调用加到子进程,即代码的 if 块,你会立即注意到这个父程序块的输出显示在控制台。 您也会注意到程序已经终止。 而子块的输出被转储到 stdout 。 类似于:
$ 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 调用中被阻塞一秒钟,再执行 printf 语句。
确定性的正确方式
然而,使用 sleep 来控制你的进程执行流程并不是最好的方法, 因为如果你做一次 n秒 的 sleep 调用:
- 如何保证无论你等待任何操作,都能在这
n秒内完成执行。 - 如果你所等待的完成时间比
n秒早很多,怎么办? 在这种情况下是不必要的等待。
一个更好的方法是使用 wait 系统调用(或变体之一)。 我们来使用 waitpid 系统调用。 它需要以下参数:
- 程序所等待进程的进程ID。
- 一个用于填充进程终止方式信息的变量。
- 可选标志位,定制
waitpid的行为
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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: %d\n",
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: %d\n",
getpid(), child_pid);
}
return 0;
}
当执行这个程序,你会注意到子块立即打印出来然后等待一个短暂的时刻(我们把 sleep 添加在 printf 后)。 父进程等待子进程执行完成后,它可以自由地执行自己的命令。
以上是第一部分。本文包含的所有代码都可以参看 这里 。 在下一篇文章中,我们将探讨如何从用户输入读取命令并执行。 敬请关注。
致谢
感谢 Saul Pwanson 帮助我理解 fork 的行为和 Jaseem Abid 帮助阅读草稿并提出修改建议。
参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 基于H5 canvas API 编写的扫雷游戏第一部分:资源加载
- Hive实战技能 第一部
- Go语言接口(第一部分)
- CVPR 2018摘要:第一部分
- 神经风格迁移指南(第一部分)
- WebGL基础教程:第一部分
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
ASP.NET 2.0技术内幕
埃斯帕斯托 / 施平安 / 清华大学出版社 / 2006-8 / 68.00元
《ASP.NET2.0技术内幕》围绕着ASP.NET 2.0是Web开发的重要分水岭这一主题,采用自顶向下的方式介绍ASP.NET 2.0的最新编程实践,从更广泛的特征到具体的实现和编程细节,充分展示了ASP.NET的最新编程实践。全书共15章,主题涉及HTTP运行库、安全性、缓存、状态管理、控件、数据绑定和数据访问。 《ASP.NET2.0技术内幕》主题丰富,讲解透彻,包含大量实例,是......一起来看看 《ASP.NET 2.0技术内幕》 这本书的介绍吧!