init
This commit is contained in:
24
scenario3-syscall-storm/Makefile
Normal file
24
scenario3-syscall-storm/Makefile
Normal 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
|
||||
141
scenario3-syscall-storm/README.md
Normal file
141
scenario3-syscall-storm/README.md
Normal 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)
|
||||
52
scenario3-syscall-storm/read_fast.c
Normal file
52
scenario3-syscall-storm/read_fast.c
Normal 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;
|
||||
}
|
||||
106
scenario3-syscall-storm/read_python.py
Normal file
106
scenario3-syscall-storm/read_python.py
Normal 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()
|
||||
49
scenario3-syscall-storm/read_slow.c
Normal file
49
scenario3-syscall-storm/read_slow.c
Normal 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;
|
||||
}
|
||||
47
scenario3-syscall-storm/read_stdio.c
Normal file
47
scenario3-syscall-storm/read_stdio.c
Normal 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;
|
||||
}
|
||||
BIN
scenario3-syscall-storm/testfile
Normal file
BIN
scenario3-syscall-storm/testfile
Normal file
Binary file not shown.
Reference in New Issue
Block a user