This commit is contained in:
illustris
2026-01-08 18:11:30 +05:30
commit 4fb1bd90db
32 changed files with 3058 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
CC = gcc
CFLAGS = -O2 -Wall
all: read_slow read_fast read_stdio testfile
read_slow: read_slow.c
$(CC) $(CFLAGS) -o $@ $<
read_fast: read_fast.c
$(CC) $(CFLAGS) -o $@ $<
read_stdio: read_stdio.c
$(CC) $(CFLAGS) -o $@ $<
testfile:
dd if=/dev/urandom of=testfile bs=1M count=1
smallfile:
dd if=/dev/urandom of=smallfile bs=10K count=1
clean:
rm -f read_slow read_fast read_stdio testfile smallfile
.PHONY: all clean testfile smallfile

View File

@@ -0,0 +1,141 @@
# Scenario 3: The Syscall Storm
## Learning Objectives
- Understand the cost of syscalls (user-space to kernel-space transitions)
- Use `strace -c` to count and profile syscalls
- Learn why buffering matters for I/O performance
- Understand `time` output: `real`, `user`, `sys`
## Files
- `read_slow.c` - Reads file byte-by-byte with raw `read()` syscalls
- `read_fast.c` - Reads file in 64KB chunks
- `read_stdio.c` - Uses stdio's `fgetc()` (internally buffered)
- `read_python.py` - Python equivalents
## Setup
```bash
# Compile all C programs and create test file
make all
```
## Exercise 1: Observe the Problem
### Step 1: Time the slow version
```bash
time ./read_slow testfile
```
Notice that `sys` time dominates - the CPU is mostly waiting for syscalls!
### Step 2: Count the syscalls
```bash
strace -c ./read_slow testfile
```
Look at:
- `calls` column for `read`: Should be ~1,000,000 (one per byte!)
- `% time` column: Most time in `read`
### Step 3: Compare with fast version
```bash
time ./read_fast testfile
strace -c ./read_fast testfile
```
The `read` call count drops from ~1,000,000 to ~16 (1MB / 64KB).
## Exercise 2: Understanding the Time Output
```bash
time ./read_slow testfile
```
Output explanation:
- `real` - Wall clock time (what you'd measure with a stopwatch)
- `user` - CPU time in user space (your code running)
- `sys` - CPU time in kernel space (syscalls, I/O)
For `read_slow`: `sys` >> `user` because we spend most time in the kernel.
For `read_fast`: `user` > `sys` because we spend more time processing data.
## Exercise 3: strace Deep Dive
### See individual syscalls (first 50)
```bash
strace -T ./read_slow testfile 2>&1 | head -50
```
The `-T` flag shows time spent in each syscall.
### Filter to just read() calls
```bash
strace -e read -c ./read_slow testfile
```
## Exercise 4: stdio Comparison
```bash
time ./read_stdio testfile
strace -c ./read_stdio testfile
```
Questions:
- How many `read` syscalls does stdio make?
- Why is it still slower than `read_fast`?
- Hint: Check the default stdio buffer size (usually 4KB or 8KB)
## Exercise 5: Python I/O
```bash
# Create smaller file for unbuffered test
make smallfile
# Run Python comparison
python3 read_python.py smallfile
# Profile with strace
strace -c python3 read_python.py smallfile
```
## Key Insights
### Why are syscalls expensive?
1. **Context switch**: CPU saves user state, loads kernel state
2. **Security checks**: Kernel validates permissions
3. **Memory barriers**: Caches may need flushing
4. **Scheduling**: Kernel may switch to another process
### The buffering solution
Instead of:
```
read(1 byte) → kernel → read(1 byte) → kernel → ... (million times)
```
We do:
```
read(64KB) → kernel → process 64KB in user space → read(64KB) → ...
```
### Rule of thumb
- Syscall overhead: ~100-1000 nanoseconds
- Reading 64KB: ~10-100 microseconds (from cache/RAM)
- Break-even: buffer should be at least a few KB
## perf stat Comparison
```bash
perf stat ./read_slow testfile
perf stat ./read_fast testfile
```
Look at:
- `context-switches`
- `cpu-migrations`
- `instructions per cycle`
## Further Exploration
1. What happens with `read(fd, buf, 4096)` vs `read(fd, buf, 65536)`?
2. How does `mmap()` compare? (Memory-mapped I/O)
3. What about `O_DIRECT` flag? (Bypass page cache)

View File

@@ -0,0 +1,52 @@
/*
* Scenario 3: Buffered I/O - The Fix
* ===================================
* This program reads a file in large chunks, dramatically reducing syscalls.
*
* Compile: gcc -O2 -o read_fast read_fast.c
*
* EXERCISES:
* 1. Run: time ./read_fast testfile
* 2. Compare: strace -c ./read_fast testfile
* 3. Notice the ~1000x reduction in syscalls
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_SIZE 65536 /* 64KB buffer */
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
char buffer[BUFFER_SIZE];
unsigned long byte_count = 0;
unsigned long checksum = 0;
ssize_t bytes_read;
/* Read in large chunks - much better! */
while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
for (ssize_t i = 0; i < bytes_read; i++) {
checksum += (unsigned char)buffer[i];
}
byte_count += bytes_read;
}
close(fd);
printf("Read %lu bytes\n", byte_count);
printf("Checksum: %lu\n", checksum);
return 0;
}

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
Scenario 3: Syscall Storm in Python
====================================
This demonstrates buffered vs unbuffered I/O in Python.
EXERCISES:
1. Create test file: dd if=/dev/urandom of=testfile bs=1M count=1
2. Run: python3 read_python.py testfile
3. Profile: strace -c python3 read_python.py testfile
"""
import sys
import os
import time
def read_unbuffered(filename):
"""Read file byte-by-byte using os.read (raw syscalls)."""
fd = os.open(filename, os.O_RDONLY)
byte_count = 0
checksum = 0
while True:
data = os.read(fd, 1) # Read 1 byte at a time!
if not data:
break
byte_count += 1
checksum += data[0]
os.close(fd)
return byte_count, checksum
def read_buffered(filename):
"""Read file using Python's buffered I/O."""
byte_count = 0
checksum = 0
with open(filename, 'rb') as f:
while True:
# Python's file object is buffered internally
data = f.read(65536) # Read 64KB chunks
if not data:
break
byte_count += len(data)
checksum += sum(data)
return byte_count, checksum
def read_byte_by_byte_buffered(filename):
"""
Read byte-by-byte but through Python's buffered file object.
This is slow in Python but not due to syscalls!
"""
byte_count = 0
checksum = 0
with open(filename, 'rb') as f:
while True:
data = f.read(1) # Looks like 1 byte, but file is buffered
if not data:
break
byte_count += 1
checksum += data[0]
return byte_count, checksum
def benchmark(name, func, filename):
"""Run and time a function."""
start = time.perf_counter()
byte_count, checksum = func(filename)
elapsed = time.perf_counter() - start
print(f"{name:30s}: {elapsed:.3f}s ({byte_count} bytes, checksum={checksum})")
return elapsed
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <filename>")
print("\nCreate a test file with:")
print(" dd if=/dev/urandom of=testfile bs=1M count=1")
sys.exit(1)
filename = sys.argv[1]
print("Reading file with different strategies...\n")
t_buf = benchmark("Buffered (64KB chunks)", read_buffered, filename)
t_byte_buf = benchmark("Byte-by-byte (buffered file)", read_byte_by_byte_buffered, filename)
# Only run unbuffered test if file is small (< 100KB)
file_size = os.path.getsize(filename)
if file_size < 100_000:
t_unbuf = benchmark("Unbuffered (raw syscalls)", read_unbuffered, filename)
print(f"\nSpeedup (buffered vs unbuffered): {t_unbuf/t_buf:.1f}x")
else:
print(f"\nSkipping unbuffered test (file too large: {file_size} bytes)")
print("For unbuffered test, create smaller file: dd if=/dev/urandom of=smallfile bs=10K count=1")
print("Then run: strace -c python3 read_python.py smallfile")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,49 @@
/*
* Scenario 3: The Syscall Storm - Unbuffered I/O
* ===============================================
* This program reads a file byte-by-byte using raw read() syscalls.
* Each byte triggers a context switch to the kernel - extremely slow!
*
* Compile: gcc -O2 -o read_slow read_slow.c
*
* EXERCISES:
* 1. Create a test file: dd if=/dev/urandom of=testfile bs=1M count=1
* 2. Run: time ./read_slow testfile
* 3. Profile: strace -c ./read_slow testfile
* 4. Compare with read_fast.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
char c;
unsigned long byte_count = 0;
unsigned long checksum = 0;
/* Read one byte at a time - TERRIBLE for performance! */
while (read(fd, &c, 1) == 1) {
byte_count++;
checksum += (unsigned char)c;
}
close(fd);
printf("Read %lu bytes\n", byte_count);
printf("Checksum: %lu\n", checksum);
return 0;
}

View File

@@ -0,0 +1,47 @@
/*
* Scenario 3: stdio Buffering
* ===========================
* This version uses fgetc() which is buffered by stdio.
* It reads byte-by-byte in C code, but stdio buffers internally.
*
* Compile: gcc -O2 -o read_stdio read_stdio.c
*
* EXERCISES:
* 1. Run: time ./read_stdio testfile
* 2. Compare: strace -c ./read_stdio testfile
* 3. Notice fewer syscalls than read_slow, but more than read_fast
* 4. Why? Default stdio buffer is ~4KB, we use 64KB in read_fast
*/
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "rb");
if (!fp) {
perror("fopen");
return 1;
}
int c;
unsigned long byte_count = 0;
unsigned long checksum = 0;
/* fgetc is buffered internally by stdio */
while ((c = fgetc(fp)) != EOF) {
byte_count++;
checksum += (unsigned char)c;
}
fclose(fp);
printf("Read %lu bytes\n", byte_count);
printf("Checksum: %lu\n", checksum);
return 0;
}

Binary file not shown.