Vi Editor Advanced Commands and Unix File System Internals
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Vi editor modes and the yank-put buffer — covered in Lecture 6 (Linux Commands and the VI Editor) and Lecture 7 (The vi Editor — Modes, Navigation, Yank-Put, Recovery and Search)
- Searching within vi (character search with f/F and pattern search with / and n) — covered in Lecture 7 (The vi Editor — Modes, Navigation, Yank-Put, Recovery and Search)
- File system hierarchy, absolute and relative paths, and navigation with cd and ls — covered in Lecture 4 (Linux Commands and File System Navigation)
- Inodes, file types, permissions and links, and the everything-is-a-file principle — covered in Lecture 5 (Linux File System — Inodes, File Types, Permissions and Links) and Lecture 1 (Introduction to Systems Programming)
# Vi Editor Advanced Commands and Unix File System Internals
8.1 Vi Editor — Search Within Line, Matching Brackets, and Substitution
Hook — why hunt character by character? Imagine your cursor is at the start of a long C line if (count == three && total == three) { and you need to change the second three. Arrow keys would take a dozen taps. A single f command jumps straight to the character you name — but only on this line, so you never overshoot into the next statement.
The professor opened this block with three tightly related tools that all answer the same question: how do you move or change text precisely without leaving command mode? The flow was deliberate — first search sideways within a line, then jump between matching brackets across lines, then replace text by range.
8.1.1 Searching for a Character Within the Same Line and Repeating the Search
An inline character search — finding a specific character on the current line without leaving the line — is done with f and F in vi. The command f followed by a character searches forward on the same line for that character, while F followed by a character searches backward on the same line for that character. Both stay strictly within the current line and do not wrap to the next line.
Intuition — like scanning a printed sentence with your finger. Think of the current line as one sentence printed on a strip of paper. Pressing fh is like sliding your finger to the right until you hit the next letter h; Fh slides left. The semicolon ; is like saying “keep sliding the same way to the next h.” The strip ends at the line break — you cannot slide onto the next strip, which is why f never wraps. The analogy breaks only in that paper has no notion of a cursor, but the bounded, single-line search is exactly the same.
Formalize — the three keys and how they compose.
f<char>— find forward: from the cursor, move to the next occurrence of<char>to the right on the same line. If the character is not on the line, the cursor does not move.F<char>— find backward: same, but to the left.;— repeat same search, same direction, same character: afterforF,;moves to the next occurrence in the original direction. The complement,repeats in the opposite direction (useful when you overshoot).
These are distinct from two other repeat keys students often confuse:
| Key | What it repeats | Scope |
|---|---|---|
; / , |
last f/F character search |
same line only |
n / N |
last / or ? pattern search |
whole file |
. |
last change (insert, delete, substitute) | wherever the change applies |
A concrete trace: on the line three times three happens with the cursor at column 0, typing fh jumps to the h in the first three (column 2), ; jumps to the h in the second three (column 13), and a further ; reports no more h on this line.
A common point of confusion is how to repeat that same character search in the same direction. The repeat key is the semicolon ;. After an f or F search, pressing ; repeats the search in the original direction for the same character, moving the cursor to the next occurrence on that line. This is distinct from n or N which repeat pattern searches (/ and ?), and distinct from . which repeats the last change.
8.1.2 The Percentage Command for Matching Brackets
The percent command — typed as % in command mode — jumps to the matching counterpart of a bracket-like delimiter. It works for three pairs: parentheses ( and ), square brackets [ and ], and curly braces { and } often called flower brackets. Placing the cursor on any one of these six characters and pressing % moves the cursor to its matching open or close partner, even across many lines.
Formalize — what % needs and what it guarantees.
- Precondition: the cursor is on one of
()[]{}. In command mode, press%. - Effect: the cursor jumps to the matching bracket that balances nesting. For
if (a[b] == c) { x = (y+z); }with the cursor on the first(,%lands on its matching)afterc, skipping the inner[b]. With the cursor on the opening{,%lands on the closing}many lines below. - Bidirectional: from either end,
%goes to the other end. - Scope: the file is scanned with nesting counted, so
%works correctly even when blocks are nested dozens of lines apart.
This is why % is checked before any large paste or compile — it proves the bracket structure is balanced without manual counting.
Visual intuition: picture the buffer with line numbers on the left and the cursor highlighted. On a C function that opens with int main() { on line 1 and closes with } on line 24, placing the cursor on the { on line 1 and pressing % makes the viewport jump so that the matching } on line 24 is now under the cursor. If a closing brace is missing, pressing % on the opener stays put or beeps — an immediate signal that a brace is unmatched. The shape to notice is nesting depth: each % confirms one pair, and repeated % toggles between the two ends.
Real-world: When working in C or any language that uses braces and brackets to define blocks and modules, % is invaluable for verifying that every opening brace has a closing brace and for navigating quickly between the start and end of a function, loop, or conditional block. It helps spot a missing brace without manual scanning. In a 500-line file, % replaces a minute of scrolling with one keystroke.
8.1.3 Substitution — The :s Command, Ranges, Delimiters, and Flags
A substitution — finding a pattern and replacing it with new text — is done with the s command in command-line mode. The conversation stressed that there are two different uses of forward slash / that learners often conflate. One use is the forward search /pattern in command mode. The other use is as a delimiter inside the substitute command itself, where / simply separates the fields and does not mean forward search.
Formalize — the full substitute form and what each piece means.
:N1,N2s/old_text/new_text/[flags]
:— enters command-line mode (from command mode after pressingEscape). The colon prompt appears at the bottom line.N1,N2— range, the line numbers between which to operate. Examples:1,5means lines 1 through 5;1,24means lines 1 through 24;%means all lines (1,\\$);\\$alone means the last line. If no range is given, only the current line is affected.s— the substitute command itself (short for substitute, from the ex editor)./— delimiter that separatesold_textfromnew_text. It is not a search direction here; any character can serve as delimiter ifold_textcontains slashes (e.g.,:s#old/new#when the pattern contains/).old_text— the search key (a string or regular expression in the full treatment).new_text— the replacement word.[flags]—gfor global (every occurrence on each line in the range) andcfor confirmation (asky/nper match). Flags can be combined asgc.
The verbal description preserved alongside the structure is: "N1 and N2 are nothing but the line numbers between what and what line do you want to search for the key and replace it with the new word." The forward slashes surrounding old and new text are described as a differentiator, not a search direction.
Ranges can be any line interval. Examples shown include 1,5 for lines one through five, 1,25 or 1,24 for larger ranges, and the idea of the whole file. If no range is given and only :s/old/new/ is typed, the substitution applies only to the current line, and only to its first occurrence unless a flag extends it.
Two flags were demonstrated in detail. The g flag stands for global, meaning every occurrence on each line in the range should be replaced, not just the first occurrence per line. Without g, only the first match on each line in the range is replaced. The c flag requests confirmation for each substitution, prompting interactively so the user can choose y or n per match.
Scope — when a substitute touches many lines and when it touches one.
- With an explicit range (
:1,5s/.../.../g), the operation visits every line in the interval and on each line replaces either the first match (nog) or every match (g). - With no range (
:s/.../.../g), only the line where the cursor sits is touched, regardless ofg. This is the most frequent surprise: addinggdoes not extend to the whole file, it only widens within the already-selected lines. - Whole-file shorthand:
:%s/old/new/g(where%means1,\\$) or:1,\\$s/old/new/g(where\\$is the last line). Hard-coding:1,24s/.../.../gis brittle because the file grows;:%sis the portable idiom. - Confirmation mode (
corgc) is interactive: vi shows each match highlighted with carets and waits fory(yes),n(no),a(all remaining),q(quit), orl(last). Usecwhen a blind global could change a word you did not intend to rename.
8.1.4 Worked Examples
Example 1 — Substitute a single number in a limited range (:1,5s/three/two/).
Setup: a file of about 24 lines contains lines such as 1: three times three and scattered occurrences of three and happens. The cursor is anywhere in command mode.
Step 1 — enter command-line mode: press Escape, then : → the : prompt appears at the bottom.
Step 2 — type :1,5s/three/two/ and press Enter.
Step 3 — vi scans lines 1 through 5. On each of those lines it replaces only the first three with two (no g flag).
Result: the two occurrences of three that lie inside lines 1–5 become two; lines 6–24 are untouched. Visual check: moving through lines 1–5 with j shows two where three was; :1,5p would print the changed window. This confirms that the range gate works before the pattern is applied.
Sense-check: if the file had three on line 6, it would remain three — proving the range is a hard boundary, not a hint.
Example 2 — Attempt to replace across the whole file and learning the range requirement (happens → something).
Goal: change every happens to something everywhere in the file.
Attempt A — :s/happens/something/g with no range → only the current line changes. The professor paused here to highlight the trap: g means global per line, not global across the file. The rest of the file still contains happens.
Attempt B — :1,25s/happens/something/g → error E16: Invalid range because the file has only 24 lines and line 25 does not exist. Vi refuses to invent lines.
Correction — first check the file length. In command-line mode :set number or Ctrl-g shows 24 lines; :1,24p or :\\$ confirms the last line. Then the correct explicit form is :1,24s/happens/something/g → vi reports the number of substitutions and every happens in lines 1–24 is replaced.
Takeaway: prefer :%s/happens/something/g or :1,\\$s/happens/something/g — both mean all lines regardless of current length and never produce an invalid-range error.
Sense-check: after either successful whole-file command, :/happens should report Pattern not found, while :/something should find matches — confirming the replacement was file-wide.
Example 3 — Global versus first-occurrence and confirmation mode (17 substitutions).
With the same file, two variants were compared side by side on the range 1,24:
:1,24s/happens/something/(nog): on a line containinghappens happens happens, only the firsthappensbecomessomething; the other two remain. This is why withoutgthe count of changed lines is less than the count of changed words.:1,24s/happens/something/g: on that same line all threehappensbecomesomething— every occurrence on each line in the range is replaced.
Adding confirmation — :1,24s/happens/something/gc — makes vi stop at each match and display the line with ^^^ under the match and the prompt replace with something (y/n/a/q/l/^E/^Y)?. Pressing y changes that occurrence, n skips it, a changes all remaining, q aborts. When y was pressed for all hits in the demo, the editor reported “17 substitutions on 17 lines” — meaning 17 occurrences were found and each was on a distinct line, so line count and substitution count matched.
A further note: an invalid range such as 1,25 on a 24-line file always produces an "invalid range" error and must be corrected to the actual last line number or, better, replaced by %.
Sense-check: running :1,24s/happens/something/g twice — the second run reports Pattern not found or 0 substitutions because the old pattern no longer exists, confirming the first run was exhaustive.
Example 4 — The whole-file idiom and an open question.
Typing 1,24 is a brittle way to mean the whole file because the file length changes after inserts. The idiomatic whole-file forms are:
:%s/old/new/g—%is the ex shorthand for1,\\$(every line).:1,\\$s/old/new/g—\\$literally means the last line, so1,\\$is first through last.
Both were identified as the intended solution that should exist, but the demonstration encountered difficulty recalling the exact form in the moment. That pending item was explicitly noted for later verification: the correct shorthand for "entire file" needs to be confirmed and should not be replaced by hard-coding the last line number.
Confirmation from the text: :%s/editer/editor/g substitutes every editer with editor throughout the file and is equivalent to :g/editer/s//editor/g and to :1,\\$s/editer/editor/g. The c flag variant :%s/old/new/gc is the safe refactoring form. These are the forms to use in exams and in practice; hard-coded ranges are only for the windowed case like 1,5.
Pitfalls — the three traps students hit most often.
- Delimiter vs search confusion. In command mode
/happensis a forward search; inside:s/happens/something/the same/is just a field separator. The professor flagged this explicitly: seeing/in:sdoes not mean forward search, it means “end of old_text, start of new_text.” If your pattern itself contains/(a path), switch delimiters::%s#/home/spiderman#/home/user#gavoids escaping. - Forgetting
greplaces only the first per line.:1,24s/happens/something/on a line with twohappensleaves one behind. Exam answers that omitgwhen asked for “replace all occurrences” lose marks; always state whethergis present and why. - Invalid range by hard-coding the last line.
:1,25on a 24-line file errors;:1,24breaks after the next insert. Habit: use:%sfor whole-file and only useN1,N2for intentional windows. Also remember that without any range,:stouches only the current line — a frequent debugging surprise when “nothing happened.”
Visual intuition: picture the file as a vertical column of 24 numbered lines, the range 1,5 highlighted as a shaded window, the range 1,24 covering the whole column, and % coloring the entire column regardless of number. For each line in the highlighted window, imagine the old_text words marked with yellow underlines; s/old/new/ paints only the first yellow per line, s/old/new/g paints every yellow per line, and s/old/new/gc pauses with a hand icon over each yellow asking for permission.
8.1.5 Student Questions and Answers
Q: How do you enter command-line mode to issue a substitute or shell command? A: First press Escape to leave insert mode and return to command mode. Escape is the safe key in vi — if unsure where you are, press Escape. From command mode, press : to enter command-line mode. The colon prompt appears at the bottom and is where :s, :!, and :r! are typed. The completion was confirmed when a student answered "Escape colon" and the explanation added that after : the editor is in command-line mode ready for a substitute or external command. Memory hook: Escape = “back to base,” : = “open the command basement.”
8.1.6 Industry Applications
Real-world: Substitution with ranges and g/c flags is the standard way to rename a variable, fix a repeated typo, or update a string literal across a source file without leaving vi. Confirmation mode c is the safe choice for large refactorings where blind global replacement would be risky. On a remote Unix host over SSH, where no graphical IDE is available, :%s/old_name/new_name/gc plus f/F/; for quick local edits and % for bracket checks is the complete edit loop. Build scripts and one-off data patches also benefit: a single :%s/DEBUG/INFO/g can retag an entire log-format file before re-running a pipeline.
Recap + Bridge — what 8.1 gave you and where it leads. Inline search f/F plus repeat ; moves you sideways without leaving the line; % proves bracket balance across the whole file; :s with range N1,N2 and flags g/c replaces text with surgical scope control. The whole-file habit is :%s or :1,\\$s, not a hard-coded last line. Bridge: these are all command or command-line mode moves. The next section keeps you in that same mode family but opens a door out of vi entirely — running a compiler or a directory listing without quitting — so the edit-compile-check loop stays inside one vi session.
Exam note: Be ready to write the full form :N1,N2s/old/new/gc from memory, explain what / does as delimiter versus search, state what happens without g (only first per line) and without a range (only current line), and translate :%s and :1,\\$s as “whole file” idioms. For f/F/;, know the exact repeat key is ; (not n or .) and that the search never wraps. For %, know the three pairs () [] {} and that the cursor must be on a bracket.
8.2 Vi Editor — Running Shell Commands Without Leaving the Editor
Hook — do you really have to quit vi to compile? Beginners routinely save, quit, run gcc in the shell, read the error, reopen vi, fix, and repeat — five steps for a one-character fix. The shell escape collapses that to two: :w then :!gcc without ever leaving the buffer, and :r! can paste the compiler or program output back into the file as evidence.
8.2.1 Executing a Shell Command with :! and Returning with Ctrl-D
A shell escape — running a Unix command without quitting vi — is done from command-line mode with an exclamation mark ! followed by the Unix command. The sequence is: press Escape to reach command mode, press : to reach command-line mode, then type ! and the command name and arguments, for example :!gcc sample.c or :!ls -l.
Formalize — the two shell-related colon commands and how they differ.
:!cmd— execute and display: runs the Unix commandcmdin a subshell, shows its standard output and standard error on the vi screen, and waits for a continuation key. The buffer is not modified. Example::!gcc sample.ccompiles the file currently on disk (so you must:wfirst or the compiler sees the old version).:r!cmd— read and insert: runscmd, captures its standard output, and inserts that output into the buffer at the cursor position. Example::r!ls -lpastes the directory listing into the file;:r!./a.outpastes the program’s printed output.
In both cases the key idea preserved from the lecture is: "it basically captures the standard output stream and copies it and pastes it back" — for :! the pasting is to the screen, for :r! the pasting is into the file.
Return path: after :!cmd vi shows a prompt such as Press ENTER or type command to continue. Pressing Enter returns to the editor. An alternative return described was pressing Ctrl-D, which was presented as the way to get back to the prompt and then back into vi. In practice Enter is the standard, and Ctrl-D signals end-of-input to the shell. The key idea is that there is no need to quit vi, run the command in a separate shell, and reopen the file — compilation and directory listing can be done in place.
After the command runs, vi shows its standard output and standard error and displays a prompt such as "Press Enter or type command to continue." Pressing Enter or the indicated continuation key returns to the editor. An alternative return described was pressing Ctrl-D, which was presented as the way to get back to the prompt and then back into vi. The key idea is that there is no need to quit vi, run the command in a separate shell, and reopen the file — compilation and directory listing can be done in place.
Scope — what is captured and what is not.
:!cmdand:r!cmdboth runcmdwith its standard output connected to vi. Standard output (printfin C,echoin shell) is reliably shown or inserted.- Standard error (
fprintf(stderr,…)or compiler warnings) is displayed on the screen by:!but is not inserted by:r!into the buffer unless the shell redirects it (e.g.,:r!gcc sample.c 2>&1to capture both streams). - Interactive input (
scanf,read) is not fed automatically. Ifcmdwaits for keyboard input, the vi shell escape will appear to hang or will not receive the typing, because the subshell’s standard input is not the terminal in the same way. The lecture explicitly left this as an empirical question: try a program that alternatesprintfandscanfand observe whether:r!./a.outhandles it or whether you must run./a.outvia:!instead. - Buffer vs disk:
:!gcc sample.ccompiles the file on disk, not the unsaved buffer. Always:wbefore:!gccor the compiler tests yesterday’s code. This was the source of the transient “flower bracket next to STD” error in the demo — an edit had not been written, so the compiler saw the old, broken text; after:wthe same:!gccgave only the expectedimplicit-intwarning.
8.2.2 Inserting Command Output into the File with :r!
The read-insert variant :r! runs a Unix command and inserts its standard output directly into the current file at the cursor position. The description preserved is: "it basically captures the standard output stream and copies it and pastes it back to this particular file." The demonstration used :r!ls -l inside a file. After execution, the listing that normally appears on the terminal — including entries such as a.out, content.txt, practice, and sample.c — was pasted into the file contents.
This sits alongside the ordinary ex :r file (read a file into the buffer). The ! is what turns a file name into a command: :r content.txt inserts the file, :r!ls -l inserts the output of a command. Both insert at the current line, pushing the rest of the buffer down.
8.2.3 Worked Examples
Example 1 — Compile inside vi and check warnings (sample.c → a.out).
Initial state: sample.c is open in vi, cursor somewhere in command mode. The file on disk may be stale if you have just edited.
Step 1 — write the buffer: :w → vi reports "sample.c" 12L, 340C written. Now disk and buffer are in sync.
Step 2 — compile without leaving vi: :!gcc sample.c → the screen flips to show compiler output. First run in the lecture showed a spurious error like expected '}' before 'STD' (a stray flower bracket in the stale disk copy). The fix was to notice the edit had not been written.
Step 3 — corrected run: after :w, type :!gcc sample.c again → this time only warning: return type defaults to 'int' (the classic implicit-int when main is written as main() without int). No hard error remains.
Step 4 — verify the artifact: :!ls -l → listing now includes -rwxr-xr-x 1 user group 16456 ... a.out alongside sample.c and others, confirming the executable was produced.
Step 5 — return: press Enter at the Press ENTER ... prompt → cursor is back in vi exactly where it was, ready to fix the warning if desired.
Sense-check: if :!ls -l does not show a.out, the compile failed; the warning-vs-error distinction in the compiler output is the signal — warnings still produce a.out, errors do not.
Example 2 — List files and insert the listing (:r!ls -l plus cat proof).
From inside sample.c in command mode:
Step 1 — view without insertion: :!ls -l → terminal output shows lines like:
-rw-r--r-- 1 user group 340 ... sample.c
-rwxr-xr-x 1 user group 16456 ... a.out
-rw-r--r-- 1 user group 890 ... content.txt
drwxr-xr-x 2 user group 4096 ... practice
Press Enter to return. Buffer is unchanged.
Step 2 — insert: place the cursor where the listing should appear (e.g., at the end with G), then :r!ls -l → vi runs ls -l again but this time the same text is spliced into the buffer at that position. The buffer now has the original C source followed by the listing lines.
Step 3 — persist and prove: :wq (write and quit), then in the shell cat sample.c → the terminal shows the original source followed by the pasted a.out ... sample.c block, confirming that standard output was captured into the file.
Variation: :r!./a.out after a successful compile would paste the program’s printed output instead of a directory listing — the canonical “show code plus execution” submission technique.
Sense-check: :r!ls -l always inserts at the cursor line; if the cursor was in the middle of a function, the listing would split the function — so move to G (last line) first.
Example 3 — Standard versus interactive programs (the deliberate open question).
After :r! succeeded with non-interactive ls, the discussion raised: what if the command is ./a.out where a.out does scanf("%d",&n); printf("%d\n",n*2); interleaved?
Observed rule: plain printf output is reliably inserted, because it goes to standard output. The open question is whether the scanf read — which needs standard input from the keyboard — is handled when invoked via :r!. The guidance given was to try it hands-on:
- Write a tiny C program that reads with
scanfand writes withprintf. - Inside vi, try
:!./a.out(which should allow typing input at the prompt) versus:r!./a.out(which may appear to hang or may capture only the output after you type). - Observe the result and note that only non-interactive print output is guaranteed to be captured.
The lecture flagged this as an open question to try empirically rather than answered from theory alone, with the note that capturing the standard output stream is the core mechanism and interactive input needs a real terminal.
Sense-check: if :r!./a.out does not prompt for input, this confirms that :r! is for non-interactive commands; use :!./a.out for interactive runs and :r!./a.out < input.txt when you want to feed prepared input non-interactively.
Pitfalls — where beginners lose time.
- Compiling the stale buffer. Editing
sample.cand immediately running:!gcc sample.cwithout:wcompiles yesterday’s disk file, so fixes seem to have no effect and errors persist. Habit: every compile is:wthen:!gcc. Some users map this to a single:w | !gcc %style workflow or setautowrite. - Expecting
:r!to handlescanf.:r!./a.outwith an interactive program will not behave like running./a.outin a full shell. Do not use:r!for programs that prompt; use:!./a.outto see interactive prompts, and reserve:r!for batch commands likels,date,gcc, or./a.out < input.txt. - Inserting at the wrong place.
:r!inserts at the cursor, not at the end. If the cursor sits mid-file, the inserted listing bisects your code. Move withGbefore:r!when you intend an append.
Visual intuition: picture vi as a room with a trapdoor to a shell. :!ls -l opens the trapdoor, lets you peer at the shell’s output, then closes it when you press Enter — the room is unchanged. :r!ls -l opens the same trapdoor but pulls the output up through it and drops it onto the floor of the room (the buffer) at your feet (the cursor). The first is a peek, the second is an import.
8.2.4 Student Questions and Answers
Q: What if the program I run with :r! is interactive, with scanf and printf interleaved — will its output still be inserted into the file? A: The mechanism captures the standard output stream. For a program that only prints, the output is reliably inserted. For an interactive program that reads with scanf and writes with printf in sequence, the behavior needs empirical testing, because the shell escape must handle both input and output streams. The question was intentionally left open for hands-on trial rather than answered from theory alone. Practical rule: use :!./a.out for interactive testing (so you can type input) and :r!./a.out < input.txt or :r!./a.out only for batch, non-interactive runs where output alone matters.
8.2.5 Industry Applications
Real-world: The shell escape workflow — edit, :w, :!gcc or :!make, :r!./a.out to paste a test run — is a classic Unix development loop that keeps the edit-compile-run cycle inside a single vi session. Inserting ls or program output into a submission file is also a practical exam technique for showing both source and execution evidence in one file. On headless servers, in containers, or over SSH where no GUI editor exists, this loop is the fastest path from bug to verified fix. Variants like :!make, :!python3 script.py, or :r!date to timestamp a log follow the same pattern — the editor stays open, the tool runs outside, and the evidence comes back in.
Recap + Bridge. :!cmd peeks at a shell command, :r!cmd imports its standard output into the buffer. Both require Escape then : to reach command-line mode, and compiles require :w first. Interactive input is the boundary: :r! is for batch output, :! for interactive runs. Bridge: you now control every vi mode (insert, command, command-line) and can shell out without quitting. The next section steps back to name those three modes explicitly and sort every operator you have met into the mode where it lives, so you never guess where a command belongs.
Exam note: Be ready to write :!gcc sample.c, :!ls -l, and :r!ls -l from memory, state which requires :w first and why, explain that Enter (or the indicated continuation key) returns from :! to the buffer, and contrast :! (display) versus :r! (insert). For the interactive-program question, answer that standard output is captured but scanf-style input needs empirical testing — the safe exam answer is that :r! reliably inserts only non-interactive print output.
8.3 Vi Editor — Modes Recap and Operator Summary
Hook — three rooms, three sets of keys. Vi feels cryptic until you picture it as three rooms: the typing room, the moving room, and the command basement. Almost every “why did my key do the wrong thing?” story is just being in the wrong room. This section names each room, its door key, and exactly which operators live inside.
8.3.1 The Three Modes and How to Move Between Them
Vi works in three modes. Insert mode (also called edit mode) is where typing inserts text. Command mode is where cursor motion, copy-paste, search, and bracket matching happen. Command-line mode is where colon commands such as :w, :q, :s, :!, and :r! are typed at the bottom line.
Formalize — the mode map and the door keys.
- Insert mode — keystrokes become text in the buffer. Entry keys:
i(insert at cursor),a(append after cursor),o(open a new line below and enter insert). AlsoI(insert at start of line),A(append at end),O(open above) as extensions. - Command mode — the default and the hub. Keys are operators: motion (
h j k l,f/F/;,/,?,%,yy/p,dd,u,Ctrl-b/Ctrl-f). From here you can go to either other mode. - Command-line mode (also called ex mode) — a single line at the bottom starting with
:for ex commands::w,:q,:s,:!,:r!,:set number,:e!.
Door keys between modes:
- To enter insert from command:
i,a, oro(and variants). The answerewas explicitly corrected: it isi,a, ando, note(emoves to end of word in command mode). - To leave insert to command:
Escape. Prof’s rule: “if unsure where you are, press Escape” — it is the safe key that always lands in command mode. - To enter command-line from command:
:— a colon prompt appears at the bottom. You are now typing an ex command. - To leave command-line to command:
EscapeorEnterafter executing/dismissing the command.
The reliable path from editing to a colon command is Escape then : — two taps that work regardless of current state. The reverse path is Escape alone.
Movement between modes was clarified through direct questioning. To enter insert mode, the keys are i, a, and o — i inserts at the cursor, a appends after the cursor, o opens a new line. The answer e was explicitly corrected: it is i, a, and o, not e. To leave insert mode and return to command mode, press Escape. To go from command mode to command-line mode, press :. The sequence Escape then : is the reliable path from editing to issuing a colon command.
A tiny state trace helps: start in command mode → i → buffer shows -- INSERT --, typing inserts → Escape → -- INSERT -- disappears, you are in command → : → bottom line shows :, type w + Enter → file written, you are back in command. Every vi session cycles through this triangle.
8.3.2 Operators Grouped by Mode
A recap quiz grouped operations by the mode they require. Copy and paste can be done in both command mode (with yy and p family operators) and command-line mode (with range-based copy operations), so the complete answer is "both command and command-line modes can be used to do copy and paste" — answering only "command mode" was marked as partial and then extended. Forward search uses / followed by the pattern in command mode, which was confirmed as "forward slash" in the quiz. Copying a line uses yy in command mode. Search and replace with :s requires command-line mode. This mapping reinforces that motion and yank-put live in command mode, while substitution and shell escapes live in command-line mode.
Formalize — sorting the operators you have met into their home mode.
| Operator | Home mode | What it does |
|---|---|---|
i a o |
command → insert (entry keys) | start typing |
Escape |
any → command | return to hub |
: |
command → command-line | open ex prompt |
h j k l w b e 0 \\$ |
command | cursor motion (note e lives here) |
f<char> F<char> ; , |
command | inline character search |
/pattern ?pattern n N |
command | pattern search whole file |
% |
command | bracket match |
yy p P dd u |
command | yank/paste/delete/undo |
:w :q :wq :e! |
command-line | file control |
:N1,N2s/old/new/gc |
command-line | substitution |
:!cmd :r!cmd |
command-line | shell escape / insert |
Copy-paste nuance: yy + p (yank a line, put after) lives in command mode; :<range>co<addr> or :<range>t<addr> (ex copy) and :<range>m<addr> (move) live in command-line mode. Hence the complete quiz answer: both command and command-line modes support copy-paste, just with different commands. Answering only “command mode” is correct but incomplete.
Pitfalls — the mode errors the professor corrected on the spot.
- Answering
efor insert entry. A student offeredeas an insert-mode entry key. The correction was immediate:eis a command-mode motion (to end of word), not an entry key; the insert entries arei,a,o(plusI/A/O). This is worth memorizing becauseeandisit next to each other on the keyboard. - Answering only “command mode” for copy-paste. The first student answer “command mode” was marked partial, then extended: command-line mode also does copy via ex ranges. In an exam, the full answer names both and gives one example from each (e.g.,
yy/pand:1,3co5). - Forgetting
Escapeis the universal reset. When a key does something unexpected, you are in the wrong mode. PressingEscapeand then re-issuing the intended key is the safe recovery — the professor repeated this as the one habit to build.
Visual intuition: draw a triangle with Command at the top, Insert bottom-left, Command-line bottom-right. Label the edges: top→bottom-left with i/a/o, bottom-left→top with Escape, top→bottom-right with :, bottom-right→top with Enter/Escape. Color the top vertex as the hub — every operation either starts or ends there. Place f/F/;/%/yy/p// inside the top, typing inside bottom-left, and :s/:!/:r!/:w inside bottom-right. The picture makes the quiz answers immediate: substitution is bottom-right, yy is top.
8.3.3 Student Questions and Answers
Q: If I want to type or edit text, what keys take me into insert mode? A: i, a, and o enter insert mode. i was confirmed as correct, a and o were added as the other two standard entry points. e is not an insert-mode entry key; that answer was corrected on the spot. Extension: i inserts before the cursor, a after, o opens a new line below — three distances of entry.
Q: How many modes does vi work in and what are their names? A: Three modes: insert mode (edit mode), command mode, and command-line mode. Multiple students answered "insert, command, command-line" and the summary confirmed that set as the canonical three. Some texts call command-line mode “ex mode” or “last-line mode” — same room, different label.
Q: If I want to do copy-paste, which mode should I be in? A: The initial answer "command mode" is correct but incomplete. Both command mode and command-line mode support copy-paste operations, so a complete answer names both. The correction was made by adding command-line mode as the second valid context for yank and put via ranges (e.g., yy/p versus :1,3t5). Remember to mention both for full credit.
Q: Which character is used for forward search, and which command copies a line? A: Forward search uses / in command mode. Copying uses yy in command mode. Both were confirmed directly in the rapid quiz, with "forward slash" accepted for search and "yy" accepted for copy. Complement: ? is backward search, p pastes after the yank.
Q: In which mode does search-and-replace with :s belong? A: Command-line mode, because it is typed after : at the bottom line with a range and flags (:s, :%s, :1,24s/.../.../g). The colon is the visible signal that you have left command mode.
Recap + Bridge. The triangle is Command (hub) ↔ Insert via i/a/o ↔ Command via Escape ↔ Command-line via : ↔ Command via Enter. Motion, inline search, bracket match, and yank-put live in command; substitution and shell escapes live in command-line; typing lives in insert. Bridge: with the editor’s mechanics now fully mapped, the lecture shifts layer — from the tool you type in to the storage the tool writes to. The next section starts the file system: what a file system is, what a path means, and how vi sample.c turns a human name into a disk location.
Exam note: Be ready to list the three modes with their entry/exit keys, correct e if offered as an insert entry, state that copy-paste is both modes (give yy/p and a :co/t example), and map / to forward search, yy to copy line, and :s to command-line. A diagram of the mode triangle with labeled edges earns quick marks.
8.4 File System — Directories, Paths, and How a File Is Found
Hook — you type cat /home/spiderman/hello.txt and text appears. How does the kernel know which disk blocks to read? The name you typed is a human path, not a disk address. Between the name and the bytes lies a hierarchy of directories and a hidden integer — the inode number — that translates each path component into a location. This section follows that translation step by step.
8.4.1 What a File System Is
A file system — the collection of files and directories together with the rules that organize them — was defined as a self-contained collection of files and directories with a hierarchical structure starting from a single root directory. The root is written as / and is the uppermost directory present on a disk-based file system. Most disk-based file systems also contain a lost+found directory where orphan files are stored. The file system has no dependencies on other file systems for its internal organization; it is self-contained.
Intuition — a file system as a self-contained library. Think of a file system as a library building that is complete unto itself: it has a front door (/), every shelf is inside the building, and its catalogue does not point to a different library for internal lookups. The lost+found is the lost-property shelf where the filesystem checker (fsck) puts recovered fragments that lost their parent directory — like books found without a shelf mark. The analogy breaks in that libraries shelve books contiguously while file systems scatter data blocks, but the self-containment and rooted hierarchy are exact.
An everything-is-a-file principle for Linux was emphasized: directories, regular files, and many other objects are all treated as files at the system level. That means a directory itself has an inode and data blocks, just like a regular file, and the same underlying mechanisms apply to it. The practical consequence is that reading a directory is, at the lowest layer, reading a file whose bytes are directory entries rather than user text.
Formalize — the three identifying pieces of any file or directory.
Every file or directory is uniquely identified by three things together:
- Name — the human-readable component like
hello.txtorspiderman. - Parent directory — the directory in which that name is listed.
- Inode number — inode number, a hidden integer that is the stable index into the disk’s inode array. It is the on-disk identity, while the name is the human identity.
Two different directories can each contain a file called hello.txt with different inode numbers; conversely, one inode can have two names (hard links, covered in 8.7). The inode number is what survives renames — moving a file within the same filesystem changes its name and parent but not its inode number, which is why open file handles keep working after a rename.
8.4.2 How a Path Names a File and How the System Locates It
A path — the sequence of directory names separated by / that leads from the root to a file — is how users name files that are not in the current directory. In Linux, forward slash / is the separator between two components of a path. Examples discussed include vi sample.c for a file in the current directory, vi /home/spiderman/hello.txt or cat /home/spiderman/hello.txt for a file reached through a multi-component path, and gcc sample.c or gcc /some/other/path/sample.c for compiling a file whether it is local or reached via a path.
Formalize — path forms and the lookup problem they pose.
- Relative path — does not start with
/; resolved from the current working directory. Example:sample.cmeans “in this directory, find an entry namedsample.c.” - Absolute path — starts with
/; resolved from the root. Example:/home/spiderman/hello.txtmeans start at root/, findhomeinside/, thenspidermaninsidehome, thenhello.txtinsidespiderman.
The motivating question posed was: when a command is given a file name or a path, how does the system find where that file is? The answer begins with the directory and its entries. A directory contains the names of the files present in it and the subdirectories present in it. To locate cat /home/spiderman/hello.txt, the system must translate that path component by component to the underlying storage identifier. Each step is a directory lookup: the directory’s data blocks contain (name, inode number) pairs; the kernel reads those blocks, matches the name, extracts the inode number, reads that inode, and repeats for the next component.
The motivating question posed was: when a command is given a file name or a path, how does the system find where that file is? The answer begins with the directory and its entries. A directory contains the names of the files present in it and the subdirectories present in it. To locate cat /home/spiderman/hello.txt, the system must translate that path component by component to the underlying storage identifier.
Each file or directory is uniquely identified by three things together: its name, the directory in which it resides, and its unique identifier called the inode number. The inode number is the key that bridges the human-readable name hierarchy to the disk layout.
A concrete walk helps: suppose the current directory is /home/spiderman. Typing vi sample.c requires one lookup: in the directory spiderman, find the entry sample.c and return its inode number. Typing vi /home/spiderman/hello.txt requires four lookups starting from the known root inode (conventionally 2): root → home inode → spiderman inode → hello.txt inode. The kernel follows the chain directory entry → inode array → data blocks at each step, and the full chain to the bytes is consolidated in Sections 8.5 and 8.7.
Scope — what a path does and does not guarantee.
- A path is a naming convention; the permission to traverse each directory component is checked at every step. Even if the final file is readable, a missing
x(search) permission on an intermediate directory likespidermanmakes the lookup fail withPermission denied— the directory’sxbit means “allowed to pass through and look up names inside.” - Relative versus absolute is not a performance distinction but a starting-point distinction.
vi sample.cstarts from wherever you are;vi /home/spiderman/hello.txtstarts from root and works identically from any directory. - The file system is self-contained per device: the inode numbers in one filesystem are independent of those in another mounted filesystem (mount points are where one filesystem’s root replaces a directory in another — the in-core inode flags this case).
Visual intuition: draw the hierarchy as a tree with / at the top, home as its child, spiderman as a child of home, and hello.txt and sample.c as leaves under spiderman. Beside it, draw the disk as three layers: directory blocks (containing name→inode pairs), the inode array (indexed by inode number), and data blocks (the actual bytes). Curved arrows from each (name,inode) pair in the tree descend to the corresponding slot in the inode array, then to the data blocks. The path /home/spiderman/hello.txt is a finger that walks down the tree one edge at a time, following those arrows.
8.4.3 Student Questions and Answers
Q: When I type vi followed by a file name or gcc followed by a file name, with or without a path, how does the system know where the file actually is? A: It uses the directory structure and inode numbers. Directories store file names paired with inode numbers, and the system walks the path component by component, resolving each directory entry to an inode and then to data blocks, until it reaches the final file’s inode and therefore its data. For a bare name like sample.c the walk is one step in the current directory; for /home/spiderman/hello.txt it is root → home → spiderman → hello.txt, using the inode array as the index at every hop. Extension: gcc /some/other/path/sample.c follows the same walk; the tool does not need to know the walk — the kernel’s name-to-inode translation does it before gcc ever opens the file.
Recap + Bridge. A file system is a rooted, self-contained hierarchy starting at / (with a lost+found safety net), and the everything-is-a-file principle means directories are just files whose bytes are name→inode maps. A path is the human walk; the inode number is the disk index that makes the walk land on bytes. Bridge: naming the walk is only half the story. The next section opens the inode itself — what static fields the disk keeps, what the kernel copies into memory, and how a tiny tree of direct and indirect pointers lets one inode address everything from an empty file to a 16 GB file without scattering metadata.
Exam note: Be ready to define a file system as a self-contained hierarchy rooted at / with lost+found, state the three identifiers of a file (name, parent directory, inode number), distinguish relative vs absolute paths and give two examples of each, and explain in one paragraph the component-by-component walk for /home/spiderman/hello.txt using directory entries and inode numbers.
8.5 Inodes — Disk Inode, In-Core Inode, and the Data-Block Tree
Hook — you delete a filename but the disk space does not come back. Why? Because the name and the bytes live in different places. The name lives in a directory entry; the bytes live in data blocks; the bridge between them is a small on-disk record — the inode — and whether space is freed depends on that bridge’s link count, not on whether one name disappeared.
8.5.1 Disk Inode Versus In-Core Inode
An inode — short for index node, a data-structure node that holds metadata about a file — exists in two forms. The disk inode is the static form that lives on the hard disk. It is a node in a structure like a linked list and holds metadata fields. The in-core inode is the kernel’s in-memory copy that it reads from disk when the file needs to be accessed and then manipulates during open, read, write, and permission changes. The phrasing preserved is: "it exists in the disk in a very static form, but when you want to read the file, the kernel reads this particular inode, which is called as in-core inode, and it will manipulate them."
Formalize — the two representations and what each carries.
- Disk inode — persistent, stored in the inode array on disk. Fields (from R6 §4.1): file owner identifier (individual + group), file type (regular, directory, character/block special, FIFO), access permissions, access times (last accessed, last modified, inode last changed), link count, file size, and the table of contents — the disk block numbers of the data. The inode number itself is not stored inside the disk inode; it is the index position in the array.
- In-core inode — transient, in kernel memory. Contains a copy of the disk fields plus: lock and wait flags (whether the inode is locked and whether someone waits for it), whether the in-core copy differs from disk (file or inode modified, file accessed, mount point), the logical device number of the filesystem, the inode number, hash-queue and free-list pointers, and a reference count (how many active file references, e.g., open file descriptors, currently use this inode).
Lifecycle: iget(device, inode_number) → kernel hashes (device, inode number), finds or allocates an in-core slot, locks it, reads the disk inode into it if needed, increments reference count. The process then reads/writes via bmap and buffer cache. On release, iput decrements reference count; if it drops to 0 and the in-core differs from disk, the kernel writes the inode back; if link count is 0, it frees blocks and the inode itself (via free / ifree). Knowing this flow explains why permissions or size changes update the inode but not the name, and why deleting the last name with open handles delays block freeing until iput.
A subtle but important correction was made: when a file changes, it is not the inode number that changes — the inode number is a stable unique identifier. What changes is the content of the inode. File size changes, permission changes, link-count changes, and timestamp updates all modify fields inside the inode while the inode number itself stays the same. Renaming hello.txt to greeting.txt rewrites a directory entry, not the inode number; chmod rewrites the permission bits inside the inode, not its index.
Intuition — disk inode as a library card, in-core inode as the card on the librarian’s desk. The card in the cabinet (disk inode) is the permanent record with owner, type, permissions, size, and shelf addresses. When you ask for the book, the librarian pulls the card onto the desk (in-core inode), scribbles temporary notes (locked, waiting, device, reference count), and works from the desk copy. When done, the desk copy is copied back to the cabinet if anything changed, and the desk copy is put back in a free pile for the next reader — but the card’s cabinet number (inode number) never changes. The analogy breaks where the OS must handle concurrency (locks, hash queues) which a library does not.
8.5.2 What the Disk Inode Contains
The disk inode was enumerated field by field. It contains the file owner identifier (who owns the file), the file type (what kind of file it is), access permissions, file access times, the count of links to the file, the size of the file, and the disk addresses of the data blocks that hold the file’s contents.
Formalize — the permission triple and the size rule.
- Owner + group owner — two identifiers defining who counts as “owner” versus “group member”; superuser bypasses checks.
- Type — regular, directory, character/block special, FIFO (pipe). Vi’s “everything is a file” is the type field taking different values while the rest of the structure stays uniform.
- Permissions — three bits — read (
r), write (w), execute (x) — for three classes — user (u), group (g), others (o). Written asrwxr-xr--etc. For directories,xmeans “permission to search/lookup inside,” not “run the directory.” Example from R6 Fig 4.1:rwxr-xr-xmeans the owner can read/write/search, group and others can read/search but not write. - Times — last accessed, last modified (data), last inode-changed (metadata). Changing permissions updates inode-changed time without modifying data.
- Link count — how many directory entries (hard links) point to this inode.
rmdecrements it; blocks are freed only when it reaches 0 and no process holds the inode open. - Size — 1 plus the highest byte offset written. Writing one byte at offset 1000 makes the size 1001 even though bytes 0–999 are a hole (often implemented as zeros without allocating blocks until written).
- Table of contents — the disk block numbers, addressed by byte offset from 0, via the direct/indirect tree below.
Access permissions were broken down as three permission bits — read, write, execute — applied to three categories — user, group, and others. The verbal list was "user, group, and others" with the three bits read/write/execute for each, which maps to the familiar rwx triple for each category.
A critical clarification was emphasized: "where is my data? Data will not be present as part of inode content." The inode is only metadata and block pointers. The actual file data lives in separate data blocks elsewhere on the hard disk, and the inode holds the addresses that point to those blocks. Those data blocks do not have to be contiguous; they can be scattered across the disk and are found only through the pointers in the inode. This non-contiguity is why a file can grow and shrink without moving existing blocks and without fragmenting the free-space pool into one contiguous reservation.
8.5.3 Direct, Single Indirect, Double Indirect, and Triple Indirect Blocks
A file’s data is addressed through a small tree rooted in the inode. Four kinds of pointer slots were described, each one word in length (one address):
- Direct blocks — each direct pointer slot directly stores the disk address of a data block. If the inode has eight direct slots, those eight addresses point straight to eight data blocks.
- Single indirect block — one slot stores the address of a single block that itself is not a data block but a block of addresses. That block of addresses then points to many data blocks. This is one level of indirection.
- Double indirect block — one slot stores the address of a block of addresses, where each entry points to another block of addresses, and only the lowest level points to data blocks. This is two levels of indirection and gives a much larger fan-out.
- Triple indirect block — one slot adds a third level: address to block of addresses to block of addresses to block of addresses to data blocks. This is three levels of indirection for very large files.
Formalize — the tree shape, word size, and fan-out.
Imagine the inode as a small array of words (one word = one disk block number = 32 bits = 4 bytes in this lecture’s problem). In the lecture configuration there are 11 words: 8 direct, 1 single-indirect, 1 double-indirect, 1 triple-indirect (the classic Unix FS layout varies between systems; the numbers here are set by the exam problem, not a universal constant).
- Direct region — 8 leaves: 8 data blocks, no extra I/O beyond reading the inode.
- Single-indirect region — 1 pointer → 1 address block → data blocks, where is the number of addresses per block. One extra read to fetch the address block before the data block.
- Double-indirect region — 1 pointer → 1 address block → address blocks → data blocks. Two extra reads before data.
- Triple-indirect region — 1 pointer → data blocks. Three extra reads before data.
With bytes and bytes, . The shape is a shallow tree where most bytes live in the deepest, widest branch (triple indirect), but the shallow branches (direct) give the fastest path for small files — the core efficiency trade-off.
Concrete scaling with the lecture’s numbers: direct = 8 blocks, single = 256 blocks, double = 65,536 blocks, triple = 16,777,216 blocks. Data-block count grows geometrically while metadata overhead grows slowly, which is why the same inode structure handles a 100-byte file and a 16 GB file.
The shape was summarized as a tree where data blocks are the leaves. One part of the tree is reached via direct pointers, another part via single indirect, another via double indirect, and another via triple indirect. All these pointer values are stored initially as words inside the inode, and only the indirect paths require extra on-disk blocks to hold the intermediate address arrays.
It was also noted that the data blocks themselves reside on the hard disk, not in main memory, correcting an earlier slip where "main memory" was said. The location is disk blocks; main memory holds only the in-core inode copy and cached data during access. Buffers may be cached in memory after reading, but the authoritative copy stays on disk.
Visual intuition: draw the inode as a small rectangle on the left with 11 slots (8 green direct arrows straight to 8 data squares; 1 yellow arrow to a single address-block row of 256 pointers fanning to 256 squares; 1 orange arrow to a two-level fan of 65k squares; 1 red arrow to a three-level fan of 16M squares). Label the x-axis “byte offset in file” and the y-axis “levels of indirection.” The takeaway shape is a lopsided tree: a thin fast lane on top (direct) and a wide, slightly slower highway below (indirect levels). This is the same shape reproduced in the file-size arithmetic of Section 8.6.
8.5.4 The Inode Array and Directory Blocks
The inode array — the on-disk sequence of inodes inode, inode, inode, ... — was shown as a contiguous array where each entry is one inode with all the fields listed above plus its block pointers. An inode in the array points to its own set of data blocks: first data block, second data block, third data block, and so on through whichever levels the file size requires.
Formalize — how the array is indexed and how a directory block differs from a data block.
- Inode array — a linear array on disk where position determines inode number. Kernel formula: the disk block containing inode is
((i-1) / inodes_per_block) + start_block_of_inode_list, and the byte offset inside that block is((i-1) % inodes_per_block) × sizeof(disk_inode). This is why the inode number is the index, not a field in the disk inode. - Directory block vs data block — both are disk blocks, but their bytes mean different things. A regular file’s blocks hold user bytes (C source, log lines); a directory’s blocks hold directory entries of the form (inode number, name). Both kinds of blocks are found via the same inode pointer tree — the difference is only in interpretation.
A directory block was contrasted with a data block. A directory is also a file, so it also has an inode and blocks. But the contents of a directory’s data blocks are directory entries (name plus inode number), not user file data. The picture described was: a directory entry stores an inode number followed by a file name; that inode number indexes into the inode array; the inode entry then points to the data blocks of that file. The full chain is directory entry to inode array to data blocks. That chain was deferred for a later segment after directory entries were explained and then revisited with the diagram.
8.5.5 File Versus Directory — No Fundamental Difference
Because everything in this system is a file, there is no fundamental difference between a file and a directory at the inode level. Both have an inode number, both have a data block area, and both are located through the inode array. The difference is only in how the data blocks are interpreted: a regular file’s blocks hold user data, a directory’s blocks hold name-to-inode mappings. This uniformity is why the same iget/iput/bmap path serves both, and why tools like ls -i can show inode numbers for both kinds of objects.
Scope — when the inode tree applies and when it does not, and what breaks if assumptions fail.
- Scope of this model: classic Unix System V / BSD inodes with an on-disk array and a fixed indirection tree. Modern Linux ext4 and similar filesystems extend it with extents, journaling, and dynamic allocation, but the examination model and the size computation in Section 8.6 use this fixed tree with the numbers given (8 direct at 1 KB, 32-bit addresses).
- Assumptions: block size and address size are fixed for the filesystem; address-block fan-out is integer; data blocks need not be contiguous. If the underlying device uses a different block size, changes and all capacities scale accordingly.
- What breaks if violated: assuming data lives in the inode leads to searching the wrong layer; assuming blocks are contiguous leads to failed growth or unnecessary defragmentation; assuming inode number changes on write leads to broken hard links — hard links share one inode precisely because the number is stable.
8.5.6 Student Questions and Answers
Q: Can we consider Windows registers as a collection of inodes? A: The response was that this angle had not been researched before and no definitive mapping was given. The question was acknowledged with interest, and the commitment was to try to find what represents the inode equivalent in Windows and whether the registry can be viewed that way. No equivalence was asserted in the session. Context to carry forward: the registry is a hierarchical database of configuration keys, while an inode is per-file metadata plus block pointers. The closer Windows analogue is the Master File Table (MFT) entry in NTFS, which plays a role similar to an inode (one record per file with attributes and extent pointers). The registry as a whole is not a file collection, though individual registry hives are files that themselves have MFT entries — so they sit above, not beside, the MFT/inode layer.
Q: Is this structure of inode and inode array the same for huge data sets used for different purposes like AIML and data science? A: Yes, at the hardware and storage level the data ultimately has to come to this stage to be stored. Above that there are multiple levels of abstraction for programming purposes — for example a document database stores documents with name-value pairs (NoSQL), or a relational database stores tables that can be joined and queried — but beneath all of those abstractions the persisted form is still a file. That file needs an inode value, and with the inode come its permissions, owner, and all other inode fields. Whatever fancy name a data set has, underneath it is still stored as blocks reached through inodes. Concrete: a 10 GB training CSV that a Python script treats as a DataFrame is, on disk, one or a few inodes whose triple-indirect tree holds the 16 million 1 KB blocks — the DataFrame is the abstraction, the inode tree is the persistence.
Pitfalls — the two “where is my data?” traps.
- Thinking data is inside the inode. The professor repeated “where is my data? Data will not be present as part of inode content” because beginners read the field list and assume bytes follow the size field. The inode holds addresses; the addresses lead elsewhere; the elsewhere may be scattered. Always picture the arrow leaving the inode.
- Thinking inode number changes when the file is edited. Editing changes the bytes in blocks and updates size/times/link count inside the inode, but the integer that indexes the inode array stays fixed. If numbers changed, every hard link and every open file descriptor would break — they do not, confirming stability.
8.5.7 Industry Applications
Real-world: The inode indirection tree is the reason Unix-like file systems can efficiently handle both tiny files (using only direct blocks with no extra I/O) and huge files (using double and triple indirection with logarithmically few extra reads). Database engines, AIML data pipelines, and document stores all sit on top of this same block layer, so understanding direct versus indirect addressing explains both performance and maximum file size. Choosing a larger block size (e.g., 4 KB on modern defaults) raises and pushes the triple-indirect ceiling from 16 GB toward terabytes — the same arithmetic with different powers of two — which is why filesystem creation options (mkfs -b) matter for workloads dominated by very large sequential files versus many tiny files.
Recap + Bridge. An inode is metadata plus a small fan-out tree: disk inode (permanent) versus in-core inode (working copy with lock, device, reference count). Only data blocks hold bytes, addressed by 8 direct plus single/double/triple indirect pointers, found via the inode array. The inode number is stable; its contents evolve. Bridge: the tree’s width is the engine of scale. The next section turns that engine into numbers — deriving from a 1 KB block and a 32-bit address, then computing how many bytes each branch can hold and why the four capacities are added, not multiplied.
Exam note: Memorize the disk inode field list (owner ID + group, type, rwx for user/group/others, link count, size, times, block addresses), state the disk vs in-core distinction with two extra in-core fields (reference count, lock/device), reproduce the “where is my data?” clarification with the non-contiguous scattering point, and sketch the four-level tree (direct, single, double, triple) as a fan-out diagram with the word-size note (one address = 4 bytes here).
8.6 Worked Computation — Largest File Size with Direct and Indirect Blocks
Hook — how does the same tiny inode handle an 8 KB config file and a 16 GB dataset? The answer is not a bigger inode but a fan-out tree: one pointer that points to 256 pointers that each point to 256 pointers. This section derives exactly how many bytes each branch can address and why the branches add rather than multiply.
8.6.1 Problem Statement
A file system that uses Unix-like inodes to keep track of sectors allocated to files is given. The word "sectors" is used loosely to mean blocks — multiple blocks allocated to files. Assume disk blocks are one kilobyte in size, disk block addresses are 32 bits, and the inode has space for eight direct blocks, one singly indirect block, one doubly indirect block, and one triply indirect block. What is the largest file that can be stored using this system, approximately?
The verbal description preserved alongside the reconstruction is: "disk block are one kilobyte in size, disk block addresses are 32 bits, the inode has space of eight direct blocks, one singly indirect block, one doubly indirect block, and one triply indirect block."
Intuition — an address book inside an address book. Think of an inode as a wallet with 11 business cards. Eight cards are direct: each has the street address of one data building (one block). One card is single-indirect: it has the address of a filing cabinet (one block) that contains 256 business cards, each pointing to a building — so one wallet card reaches 256 buildings via one cabinet. Double indirect is a card that points to a cabinet whose 256 cards each point to another cabinet of 256 — 65,536 buildings. Triple indirect is three nested cabinets: buildings. The wallet never gets thicker; it just points to bigger cabinets when the file grows.
8.6.2 Mathematical Formulation
Let the block size be
because the statement "one kilobyte is two power ten" was used explicitly in the derivation.
Let the address size be
since 32 bits equals four bytes.
Define the number of addresses that fit in one block as
The verbal step preserved is: "how many addresses of 32 bits can I have in one kilobyte block? That we have to find out. So what is one kilobyte block? It is two power ten. And what is the length of the address? 32 bits, which is nothing but four bytes. So this two power ten divided by four bytes. What is four? It is two power two. So two power ten divided by two power two, which is nothing but two power eight."
Here is the fan-out at each level of indirection: one address block holds pointers, each of which can point to a data block or to another address block.
Formalize — the three parameters and the derived fan-out, with units tracked.
| Symbol | Meaning | Value in this problem | Power-of-two form |
|---|---|---|---|
| block size (bytes per data block) | 1024 bytes | ||
| address size (bytes per pointer) | 4 bytes | ||
| addresses per block, | 256 |
Check units: is bytes per block, is bytes per address, so is addresses per block — dimensionally clean (bytes cancel, leaving addresses).
The inode layout for this problem:
Each word is one address ( bytes inside the inode). Only the words that are actually needed are followed: a tiny file uses only direct pointers; a huge file uses all four regions.
Capacity per region (bytes) in closed form:
Substituting and gives the powers of two that the exam expects to see written out. The derivation is deliberately left as powers of two because adding them is easier in exponent form and because the approximation “dominated by the triple term” is immediate.
8.6.3 Worked Examples
Example 1 — Direct blocks.
The inode has eight direct block pointers. Each direct pointer reaches one data block of size bytes. With eight such blocks,
Since ,
The verbal step preserved is: "What is the size of each of this particular block? It is one kilobyte. Likewise, eight times it should be. What is eight? It is two power three. So two power three into two power ten is two power thirteen." When only direct blocks exist, the file size is bytes.
Numerical spot-check: bytes = 8 KB. This is the ceiling for a file that uses no indirection at all — eight 1 KB blocks. Any file larger than 8192 bytes must use at least the single-indirect branch, which is why even a modest 10 KB file already touches the indirect layer.
Sense-check: direct capacity grows linearly with the count of direct slots; doubling direct slots from 8 to 16 would double this region from to — still tiny compared to indirect regions.
Example 2 — Single indirect block.
One singly indirect pointer points to one address block that holds addresses, each pointing to a distinct data block of size bytes. Therefore
which is bytes or 256 KB. The verbal step preserved is: "each of this particular address points to one kilobyte block. How many addresses are there? Two power eight and each one points to one kilobyte block. How many addresses are there? Two power eight and each one points to one kilobyte block. If it is pointing to one kilobyte block then basically it is two power ten bytes. So in total it is going to be two power eighteen bytes using a single indirect block."
Dimensional check: is addresses per block (256), times bytes per address gives bytes — units close.
Concrete scale: with one indirect block the filesystem can address bytes = 262,144 bytes. Adding the direct 8,192 bytes, the file could already be about 270 KB before needing double indirection. One extra disk read (the address block) buys a factor over direct.
Sense-check: if block size were doubled to 2 KB (), would become and single indirect would rise to MB — the same pattern with shifted exponents.
Example 3 — Double indirect block.
One doubly indirect pointer points to an address block with entries, each entry points to another address block with entries, each of which points to a data block. The capacity is therefore
which is bytes or 64 MB. The verbal step preserved is: "only one block is there and for that one block how many addresses can I have? Again 32 addresses but there are multiple levels over here. So this is one address which points to a block and here you have two power eight addresses. Those two power eight addresses map to two power eight blocks, so each of this one again points to the data block."
Step-by-step expansion to show where the levels multiply:
Cost view: reaching a byte in the double-indirect region requires two address-block reads before the data block. The fan-out compensates: data blocks from one inode word is a ratio of 65,536:1 metadata to data pointers at that level.
Sense-check: double indirect alone ( MB) is already about 8,000 times larger than direct, showing why the tree is dominated by deeper levels.
Example 4 — Triple indirect block.
One triply indirect pointer adds a third level of fan-out:
which is bytes, about 16 GB. The verbal step preserved is: "if I move to the three power eight it is taking it to the next level by having two power eight into two power eight into two power eight into two power ten. What is this two power ten? It is the size of the block which it said one kilobyte. So it is two power eight into two power eight into two power eight into two power ten resulting in two power thirty-four bytes."
Expanded:
since GiB. This is the ceiling of the tree’s widest branch; three extra reads buy sixteen million data blocks from one inode word.
Sense-check: triple indirect is times wider than double indirect (), the same geometric ratio as each deeper level — a consistency check that no exponent was dropped.
Example 5 — Total maximum file size (the central correction: add, do not multiply).
The four regions contribute additively, not multiplicatively. The total is the sum of all four capacities:
The correction emphasized was: "Please note it is not multiply, but you have to add all these. That is two power thirteen from eight direct blocks, two power eighteen from a single indirect block, two power twenty-six from double indirect block and two power thirty-four from triple indirect block. Finally, it results in two power thirteen plus two power eighteen plus two power twenty-six plus two power thirty-four." Approximately, the sum is dominated by the triple indirect term, so
with the exact sum being
If a quick approximation is needed, bytes is the quoted order of magnitude. The reason for addition is structural: the four sets of data blocks are disjoint leaves of the tree — direct blocks are not inside the single-indirect blocks, and so on. Adding counts disjoint sets; multiplying would count a Cartesian product that does not exist.
Visual check: on a log-scale bar, direct and single are slivers, double is a thin stripe, triple is the bar. The sum is visually indistinguishable from the triple bar, which is why “ GB” is an acceptable approximation and why exam answers that write the sum as a product are immediately identifiable as wrong.
Exam note: The most common mistake is to multiply the four terms or to confuse with . Remember that comes from dividing block size by address size , and that double and triple indirection multiply fan-out as and before the final .
Scope — when this exact arithmetic holds and what changes if parameters shift.
- Holds when: block size is fixed at KB, address size at 32 bits, and the inode has exactly 8 direct + 1+1+1 indirect pointers as stated. These are the lecture’s problem parameters, not a claim about every filesystem.
- Changes when: modern filesystems often default to 4 KB blocks ( bytes) and 64-bit block numbers or extents, so or larger and capacities jump by orders of magnitude; ext4 with extents does not use this triple-indirect tree at all. Always restate and before plugging in exponents.
- What breaks if violated: using bytes and bits interchangeably (e.g., dividing 1024 by 32 without converting 32 bits to 4 bytes) halves the exponent; forgetting that 8 = (not ) inflates direct capacity by ; multiplying regions instead of adding invents phantom bytes that share no block.
Pitfalls — the three exam traps around this computation.
- Multiply vs add. The four regions are disjoint sets of blocks hanging off different inode words. The total is a sum; a product would imply every direct block is combined with every indirect block, which has no physical meaning. The professor corrected this explicitly: “it is not multiply, but you have to add.”
- Exponent confusion — vs vs . is the block ( KB), is the address (4 bytes), is the fan-out (). The step is the one line to write first in any answer — it proves you tracked units.
- Dropping the conversion. Direct is , not . The in the problem is a count of pointers, not a block’s fan-out.
Visual intuition: sketch the inode as a root node with four branches labeled “8×B” (thin), “N×B” (wider), “N²×B” (much wider), “N³×B” (widest). Annotate each branch with its exponent tower (e.g., triple: ). Draw the four resulting block pools as stacked bars on a log axis from to ; the stack is visually the triple bar plus hairlines. The one-sentence takeaway: width grows as powers of , so depth dominates and the deepest branch sets the maximum.
Recap + Bridge. From and you get addresses per block. Capacities are , , , ; the maximum file is their sum GB. The additive structure is the lesson — disjoint branches add. Bridge: those bytes live wherever the addresses say — blocks need not be contiguous and blocks of a directory are directory entries. The next section maps the last missing piece: how a directory entry’s (inode number, name) pair is laid out in bytes, what historical and current limits it carries, and how the full chain entry → inode array → blocks resolves any path.
Exam note: Always write the template in this order for partial credit: 1) , 2) , 3) , 4) , , , , 5) sum and approximate as GB. State explicitly “add, do not multiply” and keep powers of two rather than converting to decimal mid-way. If the problem changes block size, recompute first — do not reuse 256.
8.7 File System Layout — Directory Entries, Links, Limits, and Inode Numbers
Hook — why is a file’s name not inside its inode? If the name were in the inode, a file could have only one name. But Unix allows two different names in two directories to point to the same bytes — a hard link — and renaming would then have to rewrite the inode instead of a tiny directory entry. Separating names (directory entries) from metadata and block pointers (inodes) makes sharing, renaming, and lookup both possible and cheap.
8.7.1 What a Directory Entry Contains
A directory — a file whose data blocks hold name-to-inode mappings — contains entries that link a human-readable name to an inode number and thus to data. Each directory entry was described in two historical variants.
Formalize — the two entry layouts and the limits they imply.
- Older Unix (fixed-length, e.g., System V / early BSD): 16 bytes per entry, laid out as a table with byte offsets
0, 16, 32, 48, …. Inside each 16-byte slot: a 2-byte inode number followed by a 14-character file name (NAME_MAX = 14). The fixed stride makes random access by entry index trivial: entry starts at byte . Wasteful when most names are short; overflow when a name exceeds 14 (truncation or error).
- Current structure (variable-length, Linux ext families): record-oriented layout with offsets
0, 4, 6, 8, …indicating a header before the name. Typical header includes: record length (how long this entry is), inode number (now wider than 2 bytes on modern filesystems — 4 bytes on ext4), file-type byte, and the name bytes themselves, padded to alignment. The name field now supports up to 255 characters (NAME_MAX = 255), and the header lets entries have different lengths so short names pack tightly and long names still fit.
In both cases the entry holds (inode number, name) — the minimal link. No type for regular files beyond what the inode stores; no size; no permissions — those live in the inode. The verbal update preserved was: "Unix limits the file names to 14 bytes. Whereas Linux extends it to 255 bytes and it is defined in limits dot H. File name is limited to 255 characters. Whereas the path name itself, including the whole path, is limited to 4096 characters."
The path-name limit (PATH_MAX = 4096) is the sum of all components plus slashes and a terminating null — deep nesting with long names can still exceed it even when each name is within 255.
The older Unix fixed-length structure presented as a table with byte offsets 0, 16, 32, 48, ... uses 16 bytes per entry: a 2-byte inode number followed by a 14-character file name, with the remaining bytes covering the offset bookkeeping. The description was: byte offset in the directory at 0, then a 2-byte information which is the inode number, then the name of the file limited to 14 characters.
The current structure described replaces that with offsets 0, 4, 6, 8, ... and a variable-length name field: an offset or record-length field, a 2-byte inode number area, and a name field that now supports up to 255 characters. The verbal update was: "Unix limits the file names to 14 bytes. Whereas Linux extends it to 255 bytes and it is defined in limits dot H. File name is limited to 255 characters. Whereas the path name itself, including the whole path, is limited to 4096 characters."
Intuition — directory entries as index cards in a drawer. Picture a directory block as a drawer of index cards. Each card has a number in the top-left (inode number) and a handwritten label (file name). To find hello.txt, you flip cards until the label matches, copy the number, walk to the cabinet (inode array) at that numbered slot, and open the shelf addresses inside. Renaming a file is just erasing the label on one card — the cabinet number stays the same, so no shelf changes. The fixed 16-byte layout is like every card being exactly 16 cm wide; the variable layout is like cards cut to fit the label, with a small header noting each card’s length so you still know where the next card starts.
Real-world: limits.h is the header where NAME_MAX (255) and PATH_MAX (4096) are defined. These limits matter when constructing deep paths or when writing portable code that must handle the longest legal file name. In C, buffer sizing for pathnames commonly uses char path[PATH_MAX] and checks strlen(name) > NAME_MAX before creat or link to avoid silent truncation. Build systems and packaging tools that generate nested node_modules-style trees are the classic way to hit PATH_MAX in practice.
Visual intuition: sketch a directory block as a row of entries with irregular widths. Label each entry’s header: rec_len (arrow spanning the entry), inode (2–4 bytes), name[0..len-1] (variable), padding (hatched). Below it, overlay the old fixed layout as uniform 16-byte slots for contrast — the same information, but with white space where names were short. The takeaway shape is compact packing for variable names versus simple arithmetic for fixed slots — a classic space-versus-complexity trade-off.
8.7.2 Names, Paths, and Links
A link — the file name stored inside a directory that connects the directory hierarchy to an inode and thus to data — was defined as: "File name in the directory is called a link. It links the name in the directory hierarchy to the inode and to the data." The path separator / divides two components of a path, so /home/spiderman/hello.txt is parsed as root /, then home, then spiderman, then hello.txt, with the last component being the file name and the preceding components being directories.
Formalize — hard links, paths, and what “link count” actually counts.
- Directory entry = hard link. Each (name, inode number) pair is a hard link. Creating a second name for the same inode (
ln hello.txt greeting.txt) adds a new directory entry with the same inode number and increments the inode’s link count. Deleting a name (rm greeting.txt) removes one entry and decrements the count; data blocks are freed only when count reaches 0 and no process holds the inode open (iputcheck). - Path parsing:
/home/spiderman/hello.txtis split on/into componentshome,spiderman,hello.txt. The leading/means start at the known root inode (conventionally 2). The kernel then iterates: for each component, look it up in the current directory’s entries (scanning its data blocks), fetch the inode number,igetthat inode, verify it is a directory (except possibly the last component), and repeat. - No name in the inode — intentional. This is what allows one inode to have many names and what keeps renames (
mvwithin one filesystem) cheap: only directory entries move; the inode and its blocks do not.
A tiny trace: /home/spiderman/hello.txt → start with root inode 2 → read root’s directory blocks, find home → inode → read ’s blocks, find spiderman → inode → read ’s blocks, find hello.txt → inode → that inode’s tree leads to the data blocks of hello.txt. This is the full resolution promised since Section 8.4.
Directory entries include the inode number of the file or subdirectory plus the name that should resolve to that inode. Variable-length names are the modern norm, but the fixed 16-byte / 14-character form was retained for historical understanding.
Scope — what the directory→inode split allows and where it stops.
- Allows: multiple hard links to the same inode, atomic renames within a filesystem (just a directory-entry rewrite), and open-file persistence after deletion (the inode stays until last close).
- Stops at filesystem boundary: hard links cannot span filesystems because inode numbers are local to one inode array; use symbolic links (a separate file type whose bytes store a path string) for cross-filesystem names — symlinks were not the lecture’s focus but are the reason
ln -sexists. - Assumes flat name visibility: a name is visible only via the directory that lists it; there is no global name table. Searching “where is inode 1234 used?” requires scanning directories (e.g.,
find / -inum 1234).
8.7.3 Inode Numbers — Conventions and History
Inode numbers carry conventions. The root directory conventionally has inode number 2. Inode numbers 0 and 1 were historically designated — inode 1 was set aside to collect bad sectors — but history showed that 0 and 1 are not used in practice and remain unused. This history was probed live by running commands such as ls -i or ls -il / and ls -i on the current directory and on /, observing entries with inode 1 and 2 and noting that 2 is associated with the root. The exact shell results were treated as secondary to the convention: root is 2, and 0/1 are reserved and unused.
Intuition — why numbering starts at 2. Think of house numbers on a street where numbers 0 and 1 were once planned for a utility shed (bad-block list) that was never built to scale. The first real house got number 2, and to avoid renumbering every map, number 2 stayed as “root” forever. The convention is now baked into tools and boot loaders — changing it would break more than it fixes.
Formalize — what the probing commands show and why inode numbers matter beyond trivia.
ls -i— list names with inode numbers:12345 hello.txt. Useful to see that two hard links share a number.ls -il— long form with inode (ls -i -l): adds-rwxr-xr-x 2 owner ... 12345 hello.txt, exposing link count adjacent to inode number so you can correlate “how many names” with “which number.”- Probing
/:ls -il /shows2next to.and/in many traditional layouts because2is the root’s own inode. Seeing1in a listing concerns the bad-block reservation, not a user file — hence “ and remain unused in practice.”
Why convention, not law? Modern filesystems (ext4, XFS, Btrfs) may place the root at a different number (e.g., ext4 often uses 2, but it is not architecturally forced). The exam-stable facts are: and are reserved/unused, is the traditional root, and the general principle that inode numbers are per-filesystem indexes — not global identifiers.
8.7.4 Putting It Together — Directory Entry to Inode Array to Data Blocks
The complete lookup chain was summarized with a diagram described as: a directory block holds entries, each entry has an inode number followed by a file name; that inode number indexes into the inode array; the inode entry in the array holds metadata and the direct and indirect block pointers; those pointers reach the data blocks of the file. The phrasing preserved is: "This is a directory entry. What does it have? It has the inode number followed by the file name. And this inode, it is pointing to the inode entry here in the inode array. And finally using the inode, what is it I can get to? I can get to the data blocks."
Formalize — the three-layer chain and the path walk that repeats it.
Layer view for a single file:
Directory block (bytes = entries)
└─ entry: [ inode_number | file_name ]
└─ inode array[ inode_number ]
├─ metadata (owner, rwx, link count, size, times)
└─ block pointers → data blocks (direct / indirect tree)
└─ file bytes
Repeated walk for the full path /home/spiderman/hello.txt:
- Start at the known root inode (conventionally 2, in-core after
igetof device,2). - Read root’s data blocks (directory entries), search for
home, extract inode . iget(device, i_{1})— if directory, read its blocks, search forspiderman, extract .iget(device, i_{2}), search its blocks forhello.txt, extract .iget(device, i_{3})— this inode’s block pointers lead (viabmap) to the data blocks ofhello.txt. The chain for non-directory final components stops at delivering bytes; for directories it delivers more entries.
This is exactly the namei (“name to inode”) algorithm in R6 §4.1 — namei walks components, iget fetches inodes, bmap translates file byte offsets to disk blocks via the indirection tree. The next class was scoped to continue this conversion in full — walking a path component by component with the diagram — that topic was deferred rather than started, so the chain above is the preview.
The next class was explicitly scoped to continue this chain with the conversion of a path name to its inode — walking /home/spiderman/hello.txt component by component, looking up each name in the current directory’s blocks, following its inode number, and repeating until the final file’s inode is reached. That topic was deferred rather than started.
Visual intuition: draw three horizontal bands — top band: a directory block row with entries [42|home][101|spiderman][…]; middle band: the inode array as a numbered column with slots 42 and 101 highlighted, each slot containing a small tree icon (the four pointer types); bottom band: scattered disk data blocks. Curved arrows descend from each highlighted entry to its inode slot, then fan to the data blocks. Path resolution is an animation that lights one entry → one slot → next entry → next slot, repeating until the final inode’s fan-out is lit.
8.7.5 Student Questions and Answers
Q: Why does the directory entry need both a name and an inode number — is the name not enough? A: The name alone is not a stable disk location. The inode number is the unique identifier that indexes into the inode array where all metadata and block addresses live. The name in the directory is the link that binds the human-readable hierarchy to that array index, and from there to the data blocks. Deeper view: this split is what makes hard links and renames possible — many names can share one number (ln adds an entry with the same number; mv rewrites one entry’s name), and the bytes stay put because only the directory entry changed. If the name were inside the inode, each inode could have only one name and every rename would move bytes.
Recap + Bridge. A directory entry is the link — (inode number, name) — laid out historically as a fixed 16-byte (2+14) record and now as a variable-length record with NAME_MAX=255 (PATH_MAX=4096) from limits.h. Paths are split on /, each component is a directory lookup via (entry → inode array → blocks), and inode numbers carry the convention reserved, = root. Bridge: with both the editor that writes bytes and the filesystem that finds them now complete, the lecture is exactly at the syllabus midpoint the professor named. The two summary appendices distill what to memorize for exams and what to carry into industry: vi as the portable edit-compile loop and the inode tree as the scaling engine under every database and data pipeline.
Exam note: Be ready to sketch the fixed 16-byte entry (2-byte inode + 14-char name at offsets 0,16,32,…) versus the variable-length entry (rec_len header + inode + 255-char name), state NAME_MAX 255 and PATH_MAX 4096 from limits.h, define a link as the filename in the directory, give the root convention (2, with 0/1 reserved for bad sectors/unused), and trace /home/spiderman/hello.txt as directory entry → inode array → data blocks repeated per component. Also know that hard links share an inode number and that renames within one filesystem are directory-entry operations.
Exam Guidance Summary
Exam note — vi editor: motions, bracket match, and substitution (heavily hands-on). Expect to be asked to demonstrate or recognize f<ch>/F<ch> followed by ; for same-line character search (never wrapping), % for matching () [] {} with the cursor on a bracket, and the full substitute form :N1,N2s/old/new/gc with g for global-per-line and c for confirmation. A common trap is conflating the substitute delimiter / with forward search /, and forgetting that without g only the first occurrence per line in the range changes. Be able to state the shape of :N1,N2s/old/new/g from memory, including what : does.
Exam note — substitution range and whole-file idioms. For whole-file replacement do not hard-code 1,24 or 1,25. The intended portable idioms are :%s/old/new/g or :1,\\$s/old/new/g where % means all lines (1,\\$) and \\$ means the last line. An invalid-range error (E16) appears if the upper bound exceeds the file length; practice checking length before a hard-coded range (e.g., Ctrl-g, :set number, or :\\$ to see the last line). Prefer % for portability. The text () confirms :%s/editer/editor/g ≡ :1,\\$s/editer/editor/g ≡ :g/editer/s//editor/g — know at least one whole-file form fluently.
Exam note — shell integration inside vi. Shell integration is examinable as :!command to run and view output and :r!command to capture standard output into the buffer. The ls -l plus :r!ls -l pattern and the compile-and-insert pattern :!gcc sample.c followed by :r!./a.out for non-interactive programs are the canonical demonstrations. Remember :w before :!gcc because the compiler reads the disk file, not the unsaved buffer, and Enter (or the indicated continuation key) returns from :! to the buffer. For interactive programs using scanf and printf, be prepared to discuss that only standard-output capture is guaranteed and to state that the interactive case needs empirical testing — the safe answer is “:r! reliably inserts only non-interactive print output; use :!./a.out for interactive runs.”
Exam note — file-size computation (the only numerical problem in this segment). Memorize the derivation template and show every step: block size bytes, address size bytes, addresses per block , then , , , , sum GB. Do not multiply the terms; do not confuse bytes and bits; do not reuse if the problem changes block size — recompute from first. Present the answer as powers of two, then give the decimal sum bytes only if asked, and always write the one-line justification “disjoint branches add” next to the sum. Tabulating the four branches as a table earns partial credit even if one exponent is off.
Exam note — inode and file-system theory. Theory questions will ask for the disk inode field list (owner ID and group, file type, permissions as rwx for user/group/others — with x on directories meaning search — link count, size as one plus highest offset, access/modification/inode-changed times, disk addresses), the disk versus in-core distinction (in-core adds lock, device number, inode number as index, hash/free pointers, reference count), the correction that inode number stays fixed while inode contents change, the direct versus single/double/triple indirect fan-out ( direct blocks vs fan per indirect level), the inode array picture (linear array indexed by inode number, formula for block and offset), and the directory-entry → inode-array → data-blocks chain. Also know the ancillary facts: root inode with /1\) reserved and unused (bad-sector history), lost+found, the historical @@BNMATH31efbcdbc1634f94b40df45cf6e1e106@@-character versus @@BNMATH7b1215749fab4d14828cf2d3b2ddc36a@@-character file-name limits, limits.h with NAME_MAX=255/PATH_MAX=4096, that directories are files whose blocks hold directory entries, and the three identifiers of a file (name, parent directory, inode number). The path-to-inode walk for /home/spiderman/hello.txt` is the standard long-answer trace.
Key Industry Applications
A vi-centered workflow and an inode-aware view of storage remain the two portable skills that transfer from any laptop to any server, container, or embedded device without installing anything new.
The inside-editor development loop. The full vi loop — edit with i/a/o, navigate with f/F/; and %, refactor with :%s/old/new/gc plus confirmation, save with :w, compile with :!gcc or :!make, run with :!./a.out (interactive) or :r!./a.out (batch capture), and document with :r!ls -l or :r!date — runs without ever leaving the editor. On a remote host over SSH, in a Docker build container, or on a network appliance that lacks a GUI, this loop is the fastest path from bug report to verified fix. Confirmation mode c is the production safety net when renaming a symbol across a 50-file codebase; f/F/; is the quickest way to land on the exact character in a long declaration line; % is the one-key proof that a brace-heavy C module still compiles.
The block layer under every data store. The inode tree with direct and indirect blocks explains how Unix-like file systems scale from a 200-byte Makefile that needs only two direct blocks to a 12 GB database file that uses triple indirection or, on modern filesystems, extents. The same block layer underlies both SQL tables and NoSQL document stores, so AIML data sets, log files, image corpora, and application databases all benefit from and are bounded by this addressing structure. Knowing explains why creating a filesystem with larger blocks (mkfs -b 4096 vs 1024) trades small-file efficiency for large-file reach and fewer indirect levels, and why choosing a 16 GB ceiling versus a terabyte ceiling is an arithmetic consequence of and , not a hard-coded magic number. When a data engineer sizes a partition for a nightly 100 GB ingest or a systems programmer debugs “no space left” versus “no inodes left” (df vs df -i), they are reasoning directly from the inode array, link counts, and indirect fan-out described here.
Pathnames and limits in portable code. Directory entries as links, the NAME_MAX 255 and PATH_MAX 4096 limits from limits.h, and the root-inode 2 convention are practical details that affect pathname handling in portable C programs, build systems, file-system utilities, and security reviews. Buffer sizing as char path[PATH_MAX], rejecting >NAME_MAX names before creation, and handling deep node_modules trees without overflowing are not academic exercises but daily tasks in packaging, firmware builds, and container image construction. The ls -i command to view inode numbers, find -inum to locate hard links, and cat /path/to/file to follow a path are everyday tools that exercise exactly the directory-to-inode-to-data-block chain described here, and the iget/iput/bmap/namei flow is the kernel code that implements the same chain system-call after system-call.
SP Lecture 8 notes · Vi Editor Advanced Commands and Unix File System Internals
Sections Breakdown
Inline character search with f/F and repeat ;, bracket matching with %, and ranged substitution :s with delimiters and g/c flags including whole-file idioms.
Shell escapes :! for display and :r! for inserting standard output, compile workflow with :w before :!gcc, and the batch vs interactive boundary for r!.
Three modes (insert, command, command-line) with door keys i/a/o, Escape, and :, plus quiz-sorted operators and the both-modes answer for copy-paste.
Rooted self-contained hierarchy, everything-is-a-file, path types, and component-by-component name-to-inode walk to locate data blocks.
Disk vs in-core inode fields, the where-is-my-data separation, and the direct/single/double/triple indirect tree rooted in inode words with inode-array indexing.
Derived N=256 from 1KB block and 32-bit address, then direct 2^13, single 2^18, double 2^26, triple 2^34 bytes with total as sum ≈16 GB and add-not-multiply correction.
Directory entry layouts old vs current, link as (name,inode), limits from limits.h, root inode 2 convention, and full entry→inode array→blocks chain for path resolution.
Consolidated exam checklist for vi motions/substitution, shell escapes, file-size derivation template, and inode/filesystem theory.
Vi loop for headless development and inode block layer under databases and AIML pipelines plus portable pathname limits.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Vi Editor — Search Within Line, Matching Brackets, and Substitution
Must-know: Range N1,N2 gates lines, g widens per line, no range means current line only, % or 1,$ means whole file; f/F/; stays on one line; % jumps between matching brackets.
⚠️ Top pitfall: Confusing / as delimiter in :s with / as forward search; forgetting g leaves later occurrences on same line; hard-coding 1,24 instead of % causes invalid range.
Self-check: On a line with happens twice, what does :1,5s/happens/something/ change vs :1,5s/happens/something/g ?
Connects to: 8.2, 8.3
Vi Editor — Running Shell Commands Without Leaving the Editor
Must-know: :! runs and shows, :r! runs and inserts stdout at cursor; :w before :!gcc because compiler reads disk; Enter returns from :!.
⚠️ Top pitfall: Compiling stale buffer without :w; using :r! for scanf-interactive programs; inserting at wrong cursor position.
Self-check: What is the difference in effect between :!ls -l and :r!ls -l, and where does the output go in each case?
Connects to: 8.1, 8.3
Vi Editor — Modes Recap and Operator Summary
Must-know: Insert via i/a/o, Escape to command, : to command-line; e is motion not insert; copy-paste is both command (yy/p) and command-line (:co).
⚠️ Top pitfall: Calling e an insert entry; answering only command mode for copy-paste; forgetting Escape as universal reset.
Self-check: Which mode does :s live in and why does the colon tell you the answer?
Connects to: 8.1, 8.2, 8.4
File System — Directories, Paths, and How a File Is Found
Must-know: File system is rooted at / and self-contained; every file identified by name + parent + inode number; absolute vs relative; lookup walks component by component via directory entries → inode array.
⚠️ Top pitfall: Thinking a file name alone is enough; forgetting intermediate directory x permission; confusing path as disk address.
Self-check: How does the kernel resolve cat /home/spiderman/hello.txt differently from vi sample.c?
Connects to: 8.5, 8.7
Inodes — Disk Inode, In-Core Inode, and the Data-Block Tree
Must-know: Disk inode fields, in-core extras, data not in inode but in scattered blocks, 8 direct + single/double/triple fan, inode number stable while contents change.
with B=2^{10}, A=2^{2} as bridge to next section
⚠️ Top pitfall: Thinking bytes live in inode; thinking inode number changes on edit; assuming data blocks are contiguous.
Self-check: Why does deleting one name not always free disk space, in inode terms?
Connects to: 8.4, 8.6, 8.7
Worked Computation — Largest File Size with Direct and Indirect Blocks
Must-know: B=2^10, A=2^2, N=2^8; capacities 2^13,2^18,2^26,2^34; total is sum ≈2^34≈16 GB, not product; N is B/A in bytes.
⚠️ Top pitfall: Multiplying regions instead of adding; mixing 2^10 with 2^8; forgetting 8=2^3; dividing bytes by bits without converting.
Self-check: With 1 KB blocks and 4-byte addresses, why is single indirect 2^18 bytes and double 2^26?
Connects to: 8.5, 8.7
File System Layout — Directory Entries, Links, Limits, and Inode Numbers
Must-know: Entry is (inode,name) with old 16-byte/14-char vs variable 255-char, PATH_MAX 4096; link is filename; root is 2 (0,1 reserved/unused); chain is entry→inode array→blocks repeated per path component.
⚠️ Top pitfall: Thinking name is inside inode; mixing 14-char Unix limit with 255-char Linux limit; thinking inode numbers are global.
Self-check: Why can two names point to the same data, and what does deleting one name do to the blocks?
Connects to: 8.4, 8.5, 8.6
Exam Guidance Summary
Must-know: Vi range/flag traps, whole-file idioms, :r! vs :! , file-size sum with powers of two, inode field list and entry→array→blocks chain.
⚠️ Top pitfall: See individual concept pitfalls; here the meta-pitfall is forgetting to show derivation steps for partial credit.
Self-check: Write the whole-file substitute idiom and the file-size sum with exponents without looking.
Connects to: 8.1, 8.2, 8.6
Key Industry Applications
Must-know: Edit-compile-run inside vi as portable workflow; inode tree explains scaling from tiny to huge files under every data store.
⚠️ Top pitfall: Confusing editor convenience with storage limits; ignoring limits.h in portable code.
Self-check: Give one command that shows inode numbers and one that pastes execution evidence into a file.
Connects to: 8.1, 8.5, 8.7
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.