戴夫学会叉

2021-06-07 04:27:10

在这篇文章中,我制作了一个以Zig编写的真实的工作交互式壳牌。虽然基本上是一个玩具,它展示了Fork()系统调用的令人难以置信的优雅,并且可以绝对没有运行时内存扩展的isan示例。

你也可以直接去回购以查看Zigish的来源。但请记住,这是一个玩具壳。它的价值是在学习练习中。

这一天显然是出现的。用颤抖的双手,我达到了我的树干和#34;当我终于学习系统语言时写的节目;并从堆的顶部挑选书签:在c.a shell中写一个shell!惊人的。这是我的"铲斗列表"程序 - 就像游戏,操作系统,文本编辑器或编程语言(但比任何一个更容易)。

我与C教程一起开始。 shellloop()函数是真实的工作:

const std = @import(" std"); pub fn main()!u8 {const stdin = std.io.getstdin()。reader(); const stdout = std.io.getstdout()。作家();尝试stdout.print(" ***你好,我是一个真正的shell!*** \ n"。{});尝试shellloop(stdin,stdout);返回0;}

我的main()在它中有一点,因为与c不同,Zig标准库没有隐含到STDIN和STDOUT的局部处理。所以我得到了一个读者和作家托盘,把它们传递给我的循环函数。(事实上,有很多内容在C中隐含,并在Zig.i中明确地说明这个"没有隐藏的东西"方法。 )

如果您是Zig新的,请!U8和!Void返回这两个功能的返回类型错误联合类型的Areexamples。也就是说,其值将是指定值或错误。您还可以指定错误的错误可以返回:fooError!ReticulatedFoo。

即使它是短暂的(目前,整件事只有45行!SED' / ^ $ / d; / ^ * \ / \ // d' src / main.zig | wc -l) ,我不会经过每一条节目的一行。

让我们使用一个简单的shell提示,因为这是一个很少的shell:

再次,如果您是Zig的新手,请尝试将任何错误从打印()向上调用堆栈(到main(),在这种情况下)。这就是为什么我们必须拥有!void错误返回联盟类型。

第二个参数中的{}语法是A"元组" (匿名结构)将保存我们希望在第一个参数中打印的任何值,格式化字符串。

var input_buffer:[max_input] u8 =未定义; var input_str =(尝试stdin.readuntildelimiteroreof(input_buffer [0 ..],' \ n'))orelse {// no输入,可能ctrl-d(eof )。打印换行符并退出!尝试stdout.print(" \ n" {});返回;};

这里,ReadpuntIndelimiteroreOf()函数将切片(指针向长度)带到我在阵列Input_Buffer中静态分配的块的块。它读取输入("煮熟的模式"),直到它命中whewline分隔符开字符。然后它返回包含输入的新切片。

接下来,我们需要将输入拆分为命令,它的arguments.i将使用返回splinterator的拆分()函数以最简单的方式执行以下操作:

我们将稍后回到此循环中的那一刻。现在,只要知道我们已经拆分了输入字符串并存储了碎片:ls -al foo变为ls,-al和foo。

我们有命令和任何参数。我们已准备好拼接一个新的进程并执行命令。

const fork_pid = try std.os.fork();如果(fork_pid == 0){//我们是孩子,请执行命令。} else {//我们是父父,等待孩子退出。}

我不了解你,但我盯着那个时候暂时盯着一段时间。流程ID(PID)可以在同一节目中有两个不同的东西吗?

Fork()通过重复调用进程创建新进程。新进程称为子进程。调用Process被称为父进程。子进程和父进程在单独的存储空间中运行。在Fork()时,两个内存空间都有SameContent。

因此,在我们调用上面的fork()后,我们现在有两个几乎相同的进程运行相同的代码,并且在内存中具有相同的值。

其中一个是父进程,返回值是子进程的PID。

IF语句是我们如何将父流程做一件事和儿童进程进行换句话说。

您可能会想知道是否重复了该过程,并且所有的跑步站都非常低效率。我也想知道。 Fork()的现代实现使用副本写入仅在其奠定了奠定了重复数据。在此之前,它实际上是内存中的数据。至于Program的指令代码,它处于只读段,因此它永远不需要重复。

对于这个简单的外壳,父母的工作很容易。我们只是等待着那索切尔退出。

waitpid()系统调用等待特定进程(通过PID)具有状态更改(终止,停止,恢复)。如果父母的分支,它在我们的后叉之外:

if(fork_pid == 0){//我们是孩子,请执行命令。} else {const wait_result = std.os.waitpid(fork_pid,0); if(wait_result.status!= 0){try stdout.print("命令返回{}。\ n" {wait_result.status}); }}

如果孩子的返回状态为0,则表示错误,所以我们将打印出来。

重复的子进程现在具有一个有趣的作业:用请求的命令替换自己的命令.WE使用system调用的evally of system调用。

if(fork_pid == 0){const结果= std.os.execvpez(args_ptrs [0]。?,& args_ptrs,& env);尝试stdout.print("错误:{} \ n" {结果});} else {const wait_result = std.os.waitpid(fork_pid,0); if(wait_result.status!= 0){try stdout.print("命令返回{}。\ n" {wait_result.status}); }}

这将从Zig标准库中使用execvpez()变体,该库将哪些提供者和任何环境数据到新过程。此变体也使用$ pathto解析命令而不包含名称。

所以exec()呼叫并不像Fork()那样吹脑力,但他们仍然很有趣来思考。在成功调用exec()函数后,不要在子进程中的其余代码中运行。伊萨已被新的命令流程所取代!

这就是为什么我们打印错误:下一行的消息。线路曾经执行的唯一原因是因为exec()调用失败了。

一旦孩子退出,waitpid()调用返回返回和父级继续运行,循环回提示并从用户获取下一个命令。

顺便说一下,我从C教程中获得了这个shell的基本结构,但是对于系统调用的古统,我通过Michael Kerrisk读取了Linux编程接口中的第24-27章。我期待着在优秀的书中花费更优质的时间。

我提到我会回到那个spliniderator循环的内部,在那里我们用用户输入字符串的命令和参数块做点什么。

我不介意说,我花了一个整个晚上,一晚睡觉,然后艾滋病变得正确。原因是我对TheIDEA陷入困境,即我的shell不会在运行时进行任何动态内存分配。这就是我从zig标准库中选择的execvpez()以与exec()呼叫接口。它不需要分配器。要避免分配器,它要求您将其与Sentinel终止的多项指针提供。

原则很简单:许多项目指针是Zig的说法,"这指向这种类型的一些未知数量的项目(具有已知尺寸)。"使用许多项目指针安全地,您需要了解确切的尺寸,或者您在结束时放置Sentinel值,当您到达时作为停车标志。

许多项目指针到由Sentinel值0终止的U8值列表的语法为[*:0]。

(ZIG允许您选择子类型的任何值作为Sentinel值。您可以使用81或' Q'和字母' Q'将是Sentinel价值!)

嗯,对于命令参数参数参数argv_ptr,execvpez()想要类型[*:null] const的值?[*:0] const u8,它是一个" null终止的许多项目指针到oncstant列表零终止的多项指针到常量无符号八个比特内容列表。

[*:null]意味着我们指向具有空终止器的项目列表。

?意味着以下每个值都是"可选的",即表示它们可能为null。这是必需的,因为许多项目指针的Sentinel值必须是子类型的值!

[*:0]表示(可选)的每个(可选)值本身是零终止的多项指针(零是"空字符"在ASCII和Unicode中)。

U8表示我们指向的实际数据是一系列无符号八位整数(这是我们存储ASCII或UTF-8编码的Unicode值的序列)。

我们可以跳过我的失败尝试,并直接到我最终的内容:

var args:[max_args] [max_arg_size:0] u8 =未定义; var args_ptrs:[max_args:null]?[*:0] u8 =未定义; ... var i:mexize = 0; while(tokens.next() )| TOK | {std.mem.copy(U8,& args [i],tok); args [i] [tok.len] = 0; //添加sentinel 0 args_ptrs [i] =& args [i]; i + = 1;} args_ptrs [i] = null; //添加sentinel null

我使用std.mem.copy()函数将spertaterator next()方法返回的切片中的字节复制到args存储阵列。

我在复制的字节后手动添加0值。 Zig仅强制执行阵列以终端结尾,但它不会自动将其插入到我们的阵列中,因为它无法知道这是我们想要的(对于所有知识,我们想在这个之后追加另一个字符串)。

然后我立即将指针存储到我的args_ptrs数组中的存储阵列插槽。

(在我早期的一些尝试失败的尝试中,我试图从[n] [m:0] u8到[*:null] [*:0] u8被证明是很多精神体操,令人惊讶的数量代码,我从来没有得到它。)

让我们来看看我用这个信息调用execoke的位置:

第一个参数(命令)是来自args_ptrs数组中的第一个项目。我"未包装"可选(可能为null)指针。?如果它为null,则程序将崩溃。

第二个参数只是args_ptrs数组本身的地址。 (预计,但不是必需的,它与第一个参数相同)。

最后,最后一个参数是包含环境的另一个结构的地址(您知道,环境变量如$ PATH)。对于此玩具,我刚刚在null中传递。但不仅仅是任何null,它的null:

这不仅仅是簿记。它很重要,null是多大(0000和00000000占用不同数量的内存)。

要查看此问题的示例,请查看本文遇到的(在我的失败尝试中获取我的args指针,导致efault错误):无法exec的exec。

让我们玩Zigish!顺便说一下,名字代表" Zig interactiveshell"但我也在玩" ish"英语意思的修饰符"主要是,有点,sorta"

$ zig build $ zig-cache / bin / zigish ***你好,我是一个真正的shell! ***> lsreadme.adoc build.zig src zig-cache> Datesat 6月5日13:26:59 EDT 2021> fooError:Error.FiLenotFound>>> ^ D $

值得注意的是,任何需要特定环境的东西都有问题。尝试运行Zig Build fromzigish:appdatadirunavailable时,我收到了此错误。我追溯到当I exec()时将在$ path环境变量传递到子进程的事实。呃,没什么大不了的。

最后(出于某种原因,这一直是使这个"桶列表"程序)的一部分,我想将其设置为用户的登录外壳。

首先,我将在/ usr / bin中对它进行符号链接/给予它官方Flair:

$ sudo useradd - shell / usr / bin / zigish ziggy $ sudo passwd ziggy $ su ziggy ***你好,我是一个真正的shell! ***>

是的,我真的确实重新启动并以用户&#39登录; Ziggy'之后只是看它"对于真实的和#34;

我学到了很多事情,它是(大多数)的乐趣。 我强烈推荐在某个时候写下你的壳牌。 这是您可以幸福的最大爆炸项目之一。 如果您正在寻找C替换,肯定会给Zig拍摄。 它仍然是正在进行的作用,事情正在迅速变化,但它拥有我见过的最好的社区。 好的,所以,贝壳完成了! 现在用于文本编辑器,操作系统,游戏,编程语言,以及Web浏览器。 :-)