A workstation, shell, and safe habits

Use a shell, a private workspace and careful command habits to inspect a Linux system.

8 min readChapter 2 of 168Beginner
In this chapter

Before you start#

Read What Linux actually is. You need an ordinary user account and a terminal on a Unix-like workstation. The observations below assume a POSIX-style shell such as Bash and standard utilities; exact output and tool versions vary. No root access, guest, build or kernel change is needed. The upstream source links work independently of your host kernel's version.

Learning goals#

Use a shell to identify the current directory and execute a command; distinguish standard output, standard error and exit status; explain how a file descriptor leads into the v6.12 write path; and record a repeatable observation without modifying unrelated files.

A shell is a program, too#

A terminal presents text and keyboard input. A shell reads commands and arranges for programs or shell built-ins to run. These are separate responsibilities: changing terminal software does not necessarily change the shell. A shell has its own current working directory, environment and open file descriptors. Child programs normally inherit copies or references according to the relevant process and descriptor rules. A child changing its working directory cannot change the parent's working directory; that is why cd must operate in the shell process to affect later commands typed there.

A command line is not merely a list of filenames. The shell parses quoting, expansions, redirections and pipelines before executing a command. Single quotes protect their contents from shell expansion; double quotes preserve spaces in an expanded variable while allowing expansion. For example, printf '%s\n' "$PWD" passes the current directory as one argument even if it contains spaces. Never paste a command you do not understand just because it appears in a lab: first identify what will be read, what will be written and which machine will execute it.

An exit status is a small numeric result available to the shell after a command completes. Conventionally zero indicates success and nonzero indicates failure. It is separate from output: a command can print an informative message and fail, or succeed without printing anything. Capture $? immediately after the command of interest, because the next command changes it. A successful shell command is also not proof that the system is in the state you intended; check the actual artifact.

Source walkthrough#

Writing through a file descriptor
  1. Shell redirection

    The shell arranges descriptor 1 before a command writes.

  2. System call

    write passes an integer descriptor and userspace buffer.

  3. ksys_write

    Lookup acquires a reference; a missing descriptor returns EBADF.

  4. vfs_write

    Checks and file operations handle the write; the result and position are returned.

  5. Release

    fdput_pos releases the acquired descriptor reference.

Source map. A conditional source map, not a captured shell execution. The selected file operation and position behavior depend on the open file.

On a typical Linux system, a program can write bytes to a descriptor, a small integer indexing an entry in its process's descriptor table. By convention descriptor 1 is standard output and descriptor 2 is standard error. Redirection changes where a descriptor points for a command. The shell may perform these operations itself; a shell built-in need not create a separate process. The particular library calls and entry sequence depend on the shell, architecture and runtime.

In the upstream v6.12 fs/read_write.c, the system-call definition delegates to ksys_write. The following short excerpt from ksys_write shows the initial lookup and default failure (GPL-2.0-only; surrounding statements omitted):

c
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;

Read this as state, not magic. The input fd is an integer supplied by userspace; it is not itself a kernel pointer. fdget_pos obtains a descriptor reference with the position-handling rules needed here. If no file is found, the function retains -EBADF and returns it. If it finds one, file_ppos chooses either a file position or NULL for a stream. For a positioned file, the function copies the current position into a local variable, calls vfs_write, and writes the updated position back only when the result is nonnegative. It then calls fdput_pos to release the acquired reference. This is a local lifetime: the descriptor lookup does not grant userspace a durable pointer to the struct file.

The following teaching model lists the same ksys_write steps in order:

Steps inside ksys_write
  1. fdget_pos(fd)

    Look up the descriptor and acquire its file reference; a missing file leaves -EBADF.

  2. file_ppos(file)

    Choose a copied file position, or none for a stream.

  3. vfs_write(...)

    Checks the request and dispatches through the file operations.

  4. Store position

    Write the updated position back only when the result is nonnegative.

  5. fdput_pos(f)

    Release the reference that the lookup acquired.

Source map. Source map of Linux v6.12 fs/read_write.c ksys_write; omits locking detail and error handling in vfs_write. Not a captured trace.

Follow the successful path one layer further in the same file. vfs_write checks write mode, whether writing is supported, and whether the userspace address range passes access_ok; it then checks the requested area and dispatches through the file's operations. The actual implementation differs for a regular file, terminal, pipe or device. access_ok is a preliminary range check, not proof that every later access will succeed. A write may fail, or write fewer bytes than requested, so robust programs inspect the returned byte count and errors. The VFS documentation explains the operation boundary. The system-call documentation provides context for userspace entry; architecture code determines the actual transition.

For a concrete thought experiment, suppose descriptor 1 names a regular file at offset 0 and a program successfully writes three bytes cat. ksys_write begins with a position of 0, and the selected file operation may advance its local position to 3; if it returns 3, the kernel stores that position back. Another write through a descriptor sharing the same open file description can observe the shared position. This is a conditional source-based example, not a measured execution or a claim that all streams have offsets. Descriptor sharing, concurrent access and the file operation's behavior affect the outcome. Compare the openat entry in v6.12 fs/open.c: opening a pathname is a different operation from writing through the resulting descriptor.

The openat entry is itself a thin wrapper in v6.12 fs/open.c (GPL-2.0-only; the full wrapper body is shown):

c
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
		umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;
	return do_sys_open(dfd, filename, flags, mode);
}

Read it as a handoff. The wrapper adds O_LARGEFILE only under a configuration-dependent condition and passes the arguments to do_sys_open; the shared open path, which returns the new descriptor number, is where lookup, permission checks and file installation happen. The excerpt does not show those steps, and it does not show which descriptor number a shell will choose. A redirection such as > depends on this open succeeding before the command runs.

Practice#

The following is a small host userspace exercise. It creates files only in a newly allocated temporary directory belonging to your user. Read each command before running it. Do not substitute a directory that contains work you want to keep.

sh
lab_dir=$(mktemp -d) || exit 1
printf 'lab: %s\n' "$lab_dir"
cd "$lab_dir" || exit 1
printf 'alpha\n' > output.txt
printf 'beta\n' >> output.txt
cat output.txt

The expected file contains alpha and beta on separate lines. > creates or truncates the named file; >> appends. The quoted variable prevents spaces in a path from becoming multiple arguments. mktemp -d allocates a new directory, reducing the risk of overwriting an existing file. The redirection is handled by the shell, while the selected command writes to its standard output. The actual number of kernel writes is implementation-dependent; the two lines in the file do not prove two particular system calls occurred.

Now deliberately request a missing file and separate the channels:

sh
cat missing.txt > result.txt 2> error.txt
status=$?
printf 'status=%s\n' "$status"
wc -c result.txt
cat error.txt

Expect a nonzero status, an empty result.txt, and a diagnostic in error.txt; wording varies by utility and locale. The shell creates or truncates the redirection targets before cat reports the error. This is the counterexample to the idea that a failed command leaves no files behind. status=$? must be immediately after cat; measuring the status after wc would measure a different command. If your output differs, record it and check whether the file already existed and which shell and utility were used.

The order of operations for that second command is shown below as a teaching model. The shell prepares the child's descriptors before the program image changes:

Redirections before cat runs
Redirections before cat runs1. Shell to Child before exec: fork. 2. Child before exec to Kernel: openat result.txt, O_TRUNC. 3. Child before exec to Kernel: openat error.txt. 4. Child before exec to Kernel: dup2 onto fd 1 and fd 2. 5. Child before exec to cat after exec: execve replaces the image. 6. cat after exec to Kernel: openat missing.txt fails. 7. cat after exec to Kernel: write diagnostic to fd 2. 8. cat after exec to Shell (return): exit status.ShellChild before execKernelcat after execforkopenat result.txt,O_TRUNCopenat error.txtdup2 onto fd 1 andfd 2execve replaces the imageopenat missing.txtfailswrite diagnostic tofd 2exit status
  1. ShellChild before exec

    fork

  2. Child before execKernel

    openat result.txt, O_TRUNC

  3. Child before execKernel

    openat error.txt

  4. Child before execKernel

    dup2 onto fd 1 and fd 2

  5. Child before execcat after exec

    execve replaces the image

  6. cat after execKernel

    openat missing.txt fails

  7. cat after execKernel

    write diagnostic to fd 2

  8. cat after execShell

    Return. exit status

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Shell → Child before exec: fork
  2. Child before exec → Kernel: openat result.txt, O_TRUNC
  3. Child before exec → Kernel: openat error.txt
  4. Child before exec → Kernel: dup2 onto fd 1 and fd 2
  5. Child before exec → cat after exec: execve replaces the image
  6. cat after exec → Kernel: openat missing.txt fails
  7. cat after exec → Kernel: write diagnostic to fd 2
  8. cat after exec → Shell (return): exit status
Conceptual model. Conceptual model of one shell creating a command with redirections; the syscall names are typical on Linux, not a trace of your shell.

Deliver a note containing the commands, observed status, file contents, and a diagram showing descriptor 1 and 2 pointing to different files. Include one distinction between the actual observation and the source-based ksys_write explanation. To clean up, first leave the directory with cd; inspect printf '%s\n' "$lab_dir" to confirm it is your new temporary directory, then remove only output.txt, result.txt and error.txt inside it and remove the now-empty directory with rmdir. If it is not empty, inspect it rather than recursively deleting it.

Knowledge check#

  1. Why does status=$? appear immediately after the failing cat?
  2. What happens to result.txt even though cat cannot read missing.txt?
  3. In ksys_write, what happens if the descriptor lookup fails, and what releases a successful lookup?
  4. Does a successful access_ok establish that a whole write must succeed?

Answers#

  1. The shell replaces its last-command status after later commands. Saving it immediately preserves the cat result rather than the result of printf or wc.
  2. The shell prepares the redirection first, so the output file is created or truncated even though the command later fails. The error message is routed separately to error.txt.
  3. The initialized -EBADF remains the return value when no file is found. On the successful lookup path, fdput_pos releases the reference after the VFS operation and position handling.
  4. No. It checks a userspace address range; later access, filesystem operations or other conditions can fail, and a write can be partial.

Checkpoint#

Keep the practice note and its descriptor diagram. Pass when you can reproduce the file contents and nonzero status, explain the pre-command redirection side effect, and distinguish the source map from your host observation. If not, inspect each file in the temporary directory and repeat the two commands with a fresh directory; then revisit the lookup and return paths. This is a learning checkpoint, not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.