未加星标

Understanding a *nix Shell by Writing One

字体大小 | |
[系统(linux) 所属分类 系统(linux) | 发布者 店小二03 | 时间 2018 | 作者 红领巾 ] 0人收藏点击收藏

A typical *nix shell has a lot of programming-like features, but works quite differently from languages like python or C++. This can make a lot of shell features ― like process management, argument quoting and the export keyword ― seem like mysterious voodoo.

But a shell is just a program, so a good way to learn how a shell works is to write one. I’ve written a simple shell that fits in a few hundred lines of commented D source . Here’s a post that walks through how it works and how you could write one yourself.

First (Cheating) Steps

A shell is a kind of REPL (Read Evaluate Print Loop). At its heart is just a simple loop that reads commands from the input, processes them, and returns a result:

import std.process; import io = std.stdio; enum kPrompt = "> "; void main() { io.write(kPrompt); foreach (line; io.stdin.byLineCopy()) { // "Cheating" by using the existing shell for now auto result = executeShell(line); io.write(result.output); io.write(kPrompt); } }

$ dmd shell.d $ ./shell > head /usr/share/dict/words A a aa aal aalii aam Aani aardvark aardwolf Aaron > # Press Ctrl+D to quit > $

If you try out this code out for yourself, you’ll soon notice that you don’t have any nice editing features like tab completion or command history. The popular Bash shell uses a library called GNU Readline for that. You can get most of the features of Readline when playing with these toy examples just by running them under rlwrap (probably already in your system’s package manager).

DIY Command Execution (First Attempt)

That first example demonstrated the absolute basic structure of a shell, but it cheated by passing commands directly to the shell already running on the system. Obviously, that doesn’t explain anything about how a real shell processes commands.

The basic idea, though, is very simple. Nearly everything that gets called a “shell command” (e.g., ls or head or grep ) is really just a program on the filesystem. The shell just has to run it. At the operating system level, running a program is done using the execve system call (or one of its alternatives). For portability and convenience, the normal way to make a system call is to use one of the wrapper functions in the C library. Let’s try using execv() :

import core.sys.posix.stdio; import core.sys.posix.unistd; import io = std.stdio; import std.string; enum kPrompt = "> "; void main() { io.write(kPrompt); foreach (line; io.stdin.byLineCopy()) { runCommand(line); io.write(kPrompt); } } void runCommand(string cmd) { // Need to convert D string to null-terminated C string auto cmdz = cmd.toStringz(); // We need to pass execv an array of program arguments // By convention, the first element is the name of the program // C arrays don't carry a length, just the address of the first element. // execv starts reading memory from the first element, and needs a way to // know when to stop. Instead of taking a length value as an argument, // execv expects the array to end with a null as a stopping marker. auto argsz = [cmdz, null]; auto error = execv(cmdz, argsz.ptr); if (error) { perror(cmdz); } }

Here’s a sample run:

> ls ls: No such file or directory > head head: No such file or directory > grep grep: No such file or directory > _ _: No such file or directory >

Okay, so that’s not working so well. The problem is that that the execve call isn’t as smart as a shell: it just literally executes the program it’s told to. In particular, it has no smarts for finding the programs that implement ls or head . For now, let’s do the finding ourselves, and then give execve the full path to the command:

$ which ls /bin/ls $ ./shell > /bin/ls shell shell.d shell.o $

This time the ls command worked, but our shell quit and we dropped straight back into the system’s shell. What’s going on? Well, execve really is a single-purpose call: it doesn’t spawn a new process for running the program separately from the current program, it replaces the current program. (The toy shell actually quit when ls started, not when it finished.) Creating a new process is done with a different system call: traditionally fork . This isn’t how programming languages normally work, so it might seem like weird and annoying behaviour, but it’s actually really useful. Decoupling process creation from program execution allows a lot of flexibility, as will become clearer later.

Fork and Exec

To keep the shell running, we’ll use the fork() C function to create a new process, and then make that new process execv() the program that implements the command. (On modern GNU/linux systems, fork() is actually a wrapper around a system call called clone , but it still behaves like the classic fork system call.)

fork() duplicates the current process. We get a second process that’s running the same program, at the same point, with a copy of everything in memory and all the same open files. Both the original process (parent) and the duplicate (child) keep running normally. Of course, we want the parent process to keep running the shell, and the child to execv() the command. The fork() function helps us differentiate them by returning zero in the child and a non-zero value in the parent. (This non-zero value is the process ID of the child.)

Let’s try it out in a new version of the runCommand() function:

int runCommand(string cmd) { // fork() duplicates the process auto pid = fork(); // Both the parent and child keep running from here as if nothing happened // pid will be < 0 if forking failed for some reason // Otherwise pid == 0 for the child and != 0 for the parent if (pid < 0) { perror("Can't create a new process"); exit(1); } if (pid == 0) { // Child process auto cmdz = cmd.toStringz(); auto argsz = [cmdz, null]; execv(cmdz, argsz.ptr); // Only get here if exec failed perror(cmdz); exit(1); } // Parent process // This toy shell can only run one command at a time // All the parent does is wait for the child to finish int status; wait(&status); // This is the exit code of the child // (Conventially zero means okay, non-zero means error) return WEXITSTATUS(status); }

Here it is in action:

> /bin/ls shell shell.d shell.o > /bin/uname Linux >

Progress! But it still doesn’t feel like a real shell if we have to tell it exactly where to find each command.

PATH If you try using which to find the implementations of various commands, you might notice they’re all in the same small set of directo

本文系统(linux)相关术语:linux系统 鸟哥的linux私房菜 linux命令大全 linux操作系统

代码区博客精选文章
分页:12
转载请注明
本文标题:Understanding a *nix Shell by Writing One
本站链接:https://www.codesec.net/view/610892.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 系统(linux) | 评论(0) | 阅读(86)