Before you start#
Read CPU, memory and I/O. Install Git and ensure you have several gigabytes free for a source checkout. The practice fetches upstream source into a new directory; it never changes the host kernel. An offline reader can inspect the linked files, but cannot reproduce the local Git commands without a repository.
Learning goals#
Identify the role of major source directories, distinguish tag from commit and working tree, navigate a real call and cleanup path, and state why text search cannot establish a complete runtime call graph.
A map, not a maze#
The top-level init/ contains core initialization; kernel/ contains central facilities such as scheduling; mm/ holds memory management; fs/ holds VFS and filesystems. drivers/ is a large collection of device and subsystem implementations, net/ networking, arch/ architecture-specific code, include/ shared headers, Documentation/ source documentation, and tools/ userspace tools and tests. These are useful starting points, not rigid boundaries: a single operation can cross multiple directories. An interface declaration in a header tells you its type, but not necessarily which implementation runs under a particular configuration.
Git stores objects including blobs, trees, commits and annotated tags. A commit points to a tree snapshot and parent commits; a branch is a movable name for a commit. A tag names an object and can itself be an annotated object pointing to a commit. git rev-parse 'v6.12^{commit}' resolves the tag to the commit; record that full ID in notes. A checkout has a working tree that may differ from the committed snapshot. git status and git diff distinguish local changes from the revision you intended to inspect. A shallow clone has limited history: git log and git blame are then bounded by the shallow boundary and do not prove the origin of a line. A tag name alone is not cryptographic verification of its publisher; verify a trusted signature according to your local trust policy when origin authenticity is required.
Source walkthrough#
- SYSCALL_DEFINE3(read)
The syscall wrapper forwards descriptor, buffer and count.
- ksys_read
fdget_pos finds an open file or retains EBADF.
- vfs_read
Checks and dispatch depend on the file and configuration.
- fdput_pos
The successful descriptor reference is released after use.
Follow the read operation from its syscall declaration in v6.12 fs/read_write.c (SPDX GPL-2.0; surrounding code omitted):
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
The macro names a three-argument system call: descriptor, userspace buffer and count. The body forwards these to ksys_read; it is not a direct call to a particular filesystem. Search for the definition and inspect its branches. The beginning of ksys_read contains this excerpt (same file and license; later statements omitted):
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
Lookup obtains an fd reference and initializes a bad-descriptor error. If a file exists, ksys_read obtains the appropriate position, invokes vfs_read, updates a non-stream position on a nonnegative result and releases the reference with fdput_pos. If lookup fails, the error remains. Follow vfs_read in the same source to see mode checks and dispatch. The open file and operation table declarations in v6.12 include/linux/fs.h help interpret the indirect operation. A filesystem or device can supply different callbacks; do not report a specific callback as executed without evidence about the opened object and configuration.
This is a bounded source trace. Architecture entry, a userspace library wrapper, security hooks, and the selected file operation are additional paths. The system-call development documentation describes how system-call declarations and tables fit into kernel development. The path you found is not automatically available on every build or representative of an observed workload.
Definitions, configuration and history#
git grep searches tracked files at a specified revision; ordinary rg can also search an unpacked tree but includes untracked files unless told otherwise. Search a symbol, read the enclosing function, then find its callers and the type declarations. Beware macros: SYSCALL_DEFINE3 expands into generated names and wrappers, and literal search may not expose all of them. Function pointers, inline functions, architecture overrides and generated code complicate call relationships. Kconfig determines which features can be selected, while Makefiles determine which objects are compiled under configuration conditions. A file existing in the repository does not prove it is in your kernel image.
git log -- path shows commits touching a path within available history. git blame assigns the most recent change for each displayed line, not the full rationale or the author of the concept; moved code and shallow history complicate interpretation. Read the relevant commit and discussion before making historical claims. The development process documentation explains how patches and review fit into upstream work.
Follow a question, not just a name#
A useful investigation starts with a precise question such as “what does this revision do when the descriptor is invalid?” For that question, locate the wrapper and helper, inspect the default return and conditional branch, and record the path and revision. Then search callers and declarations to understand how arguments arrive. A different question, such as “which driver handled my read?”, needs runtime evidence about the actual descriptor and selected operations. A trace can establish one execution under one configuration; source inspection can show additional possible branches. Keep those evidence types in separate notes. If a search returns no match, check spelling, macros, generated code, architecture directories and whether the file was included in the search before making an absence claim.
Git's objects and the three working states#
A Git blob stores file contents; a tree names entries and points to blobs or other trees; a commit records a tree, parent commit IDs and metadata. A branch name is a movable reference, whereas an annotated tag can be a separate object naming another object, commonly a commit. HEAD identifies the current checkout, either through a branch or directly in detached state. A remote such as origin is a configured repository location; a remote-tracking branch is a local record of a remote branch, not a live view that updates without fetching.
The working tree contains files you can edit. The index is the snapshot selected for a prospective commit; HEAD identifies the currently checked-out commit. git diff compares working-tree tracked changes with the index, while git diff --cached compares the index with HEAD. Neither command alone reports every untracked file; git status --short adds that context. A separate Git worktree can share object storage with a repository while having its own checkout and index. The source-reading exercise needs none of the user's project history and should be performed in the disposable kernel clone, not by modifying this book's repository.
- Working tree
- Files you can edit; they can differ from HEAD and the index.
- Index
- The snapshot selected for the next commit; git diff compares the working tree with it.
- HEAD
- The checked-out commit; git diff --cached compares the index with it.
- Object store
- Blobs, trees, commits and annotated tags; a separate worktree can share it.
git merge-base A B finds a best common ancestor under Git's graph rules and is useful when comparing branches; it is not necessarily the unique original author of a line or a guarantee that a patch applies. git log -p -- path shows changes to the named path in available history, and git blame attributes current lines to the changes Git identifies. Renames, copies, later edits and a shallow boundary complicate both. A full commit hash and clean status pin what you read; they do not prove a remote's identity or verify a tag signature. A signed tag still needs a trusted key and verification policy.
A complete bounded read trace: /dev/null#
Consider a Linux system where /dev/null names the conventional character device with the appropriate major/minor and the corresponding v6.12 driver is present. This is an explicit configuration and device-node assumption; a container can replace or omit that node. A program opens it for reading and makes a positive-length read with a valid userspace buffer. The generic syscall wrapper in fs/read_write.c calls ksys_read, which gets the descriptor reference and calls vfs_read. The function checks read mode, supported operations, user address range and rw_verify_area before dispatch. If the descriptor is invalid, ksys_read returns -EBADF instead; a bad address or other failed check can stop the path before the callback.
In v6.12 fs/read_write.c, SPDX GPL-2.0 (the entry checks of vfs_read; lines omitted after the rw_verify_area check):
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count);
if (ret)
return ret;
The mode check returns -EBADF for a descriptor opened without read mode, and a file without FMODE_CAN_READ gets -EINVAL. That -EBADF is a different branch from the one ksys_read returns for a missing descriptor. The excerpt does not show which f_op callback runs next; the dispatch after these checks depends on the opened file and its configuration.
In v6.12 drivers/char/mem.c, the devlist table associates minor 3 with null_fops; memory_open selects that entry. These excerpts are from that file (SPDX GPL-2.0; other cases omitted):
[3] = { "null", &null_fops, FMODE_NOWAIT, 0666 },
filp->f_op = dev->fops;
The table entry identifies the operations and mode associated with that minor. memory_open rejects an out-of-range or unimplemented minor with -ENXIO, then assigns the selected table to the open file. This is the setup that connects a generic struct file to a concrete callback; it is not proof that every pathname named /dev/null has that device identity. Device-node permissions and mount namespaces affect whether this setup is reached.
The same source defines null_fops with .read = read_null; read_null returns zero. In vfs_read, the .read member is checked before the .read_iter alternative in this v6.12 path. Under the stated assumptions and successful earlier checks, a positive-length read through that file therefore returns zero bytes, the usual end-of-file result. ksys_read releases its descriptor reference with fdput_pos; no data copy is needed from this callback. This is a verified source map, not a captured host trace. Inspecting a different device or a different kernel can produce a different operation table and return value.
Searching generated and conditional paths#
A declaration in include/ gives a type or interface, while architecture headers, #ifdef blocks and Kconfig can select different implementations. Generated headers in the output tree can depend on .config; searching only the source checkout cannot establish their contents. Conversely, generated files do not prove that every possible configuration includes the code. The syscall macro expands wrappers whose final names may not appear as literal source text. A callback invoked through file->f_op requires finding the table initializer and the code that assigns it, as in the /dev/null example.
static restricts a C symbol's linkage but does not mean the function never runs; inline helpers can disappear as separate symbols after compilation. Static calls and architecture alternatives can be patched or selected under configuration and runtime conditions. Type annotations such as __user, __iomem and __must_check signal contracts that ordinary text search will not validate. A negative rg result means the specified pattern was absent from the searched files under that command's options; it does not establish that no macro expansion, generated source, renamed implementation or indirect callback exists elsewhere. Record the revision, paths, search pattern and scope before making a negative claim.
Practice#
On a host with network access and Git, create a directory you own with at least 3 GiB free. This shallow clone is for source reading and deliberately does not supply full history. It can take several minutes and substantial network traffic. Replace neither an existing directory nor an existing checkout.
lab_dir=$(mktemp -d) || exit 1
cd "$lab_dir" || exit 1
git clone --depth 1 --branch v6.12 https://github.com/torvalds/linux.git linux
cd linux || exit 1
git status --short
git rev-parse 'v6.12^{commit}'
git describe --tags --always
git grep -n 'SYSCALL_DEFINE3(read,' v6.12 -- fs/read_write.c
git grep -n 'ssize_t ksys_read' v6.12 -- fs/read_write.c
git show v6.12:fs/read_write.c | sed -n '690,725p'
git log -1 --format='%H %s' -- fs/read_write.c
git blame -L 720,723 v6.12 -- fs/read_write.c
Use the following read-only commands after creating the chapter's v6.12 clone in the practice below to reproduce the source map. git show reads the tagged snapshot even if the checkout later acquires edits; rg searches only the explicitly selected output:
git -C "$lab_dir/linux" show v6.12:fs/read_write.c | rg -n 'ksys_read|vfs_read|file->f_op->read'
git -C "$lab_dir/linux" show v6.12:drivers/char/mem.c | rg -n 'read_null|null_fops|memory_open|filp->f_op|\[3\]'
The commands require rg; if it is unavailable, use grep -nE with equivalent patterns. The first command reports candidate lines, not a complete call graph. Read the surrounding functions to identify their conditions and the invalid-descriptor cleanup. After running the clone commands below, run these before the cleanup. A source-only deliverable can name SYSCALL_DEFINE3(read) → ksys_read → vfs_read → read_null with the device and callback assumptions and list at least one branch that stops before the callback.
Expect a detached checkout at the tag and a clean status. Record the actual commit ID rather than copying one from a web page. The line range is specific to v6.12 and shows the end of ksys_read and its syscall wrapper. On a shallow clone, the last log and blame results can be bounded by the shallow commit; they do not establish when the code was first written. If clone or tag resolution fails, check network access and the remote error; do not silently substitute a different tag. If git status shows edits, investigate before claiming a pristine snapshot.
Write a note with the commit, source path, wrapper → helper → VFS normal path, invalid-descriptor path and one possible indirect target you have not proven. To recover disk space, leave the checkout, verify that lab_dir names the new temporary directory and delete only that disposable clone using your file manager or rm -rf -- "$lab_dir/linux"; then rmdir "$lab_dir". Never apply that command to a directory containing work you intend to keep.
Knowledge check#
-
How do a tag and the commit it resolves to differ?
-
Why does a shallow
git blamenot prove the original author or date of a line? -
What is the failure path when
fdget_posdoes not find a descriptor? -
Why can the call to
vfs_readnot establish which filesystem callback executed? -
What evidence connects the generic
vfs_readdispatch toread_nullin the bounded v6.12 example?
Answers#
-
A tag is a name or annotated object referring to another object; the resolved commit identifies the tree snapshot and history parents. Record the full commit and working-tree status.
-
Blame reports attribution within available history, and a shallow boundary can hide earlier commits. Moves and later edits can also change the displayed attribution.
-
retstarts as-EBADF; the file-dependent branch is skipped and that error is returned. No successful descriptor reference needs to be released on that path. -
Dispatch depends on the opened file's operations, runtime state and compiled configuration. A source search shows possibilities, not the target of a particular execution.
-
The character-device table maps minor 3 to
null_fops,memory_openassigns that table to the open file, and the table’s read member namesread_null. The conclusion depends on the stated device and successful earlier checks.
Checkpoint#
The artifact is the tagged-commit source map and its uncertainty note. Pass when another learner can reproduce both the normal and bad-descriptor paths, and you can identify shallow history and indirect dispatch as limits. Retry the exact git show and follow each branch if you cannot. This is not certification.
Sources#
- v6.12 fs/read_write.c: read entry and helper.
- v6.12 include/linux/fs.h: file and operation types.
- Adding system calls.
- Kernel development process.
- v6.12 character-device operations: minor selection and
/dev/nullcallbacks.