The Unix environment — your new home

Real-World Analogy

Think of Unix as a building with many floors. Each floor is a directory, and every room on each floor is a file. When you open a terminal, you are standing somewhere in that building. Commands like ls (look around), cd (go to a different floor), and pwd (ask "where am I?") are how you navigate. The kernel is the building manager — when you want to do something that requires authority (reading hardware, creating processes), you must ask the manager via a system call.

Unix was designed in the 1970s around three powerful ideas: (1) every resource is a file, (2) small tools that each do one thing well, (3) text as the universal interface. Windows introduced a graphical file manager — Unix gives you a command line that is far more powerful once learned.

When you type ls -la and press Enter, your shell (bash/zsh) parses the command, finds the ls binary, creates a process, and the kernel handles the rest. Everything you do in the terminal eventually becomes a system call — a request to the kernel to perform a privileged operation.

System calls vs library functions

A library function (like printf) lives in user space — it is just C code from libc. A system call (like write) crosses into kernel space. Library functions often wrap system calls: printf eventually calls write(1, ...). You can see the system calls a program makes using strace ./myprogram.

The Unix Directory Structure

Everything starts at / (root). Key directories you must know:

DirectoryPurposeNotable contents
/Root of the filesystemAll other directories live here
/binEssential user binariesls, cp, mv, cat, bash
/usr/binNon-essential user programsgcc, vim, grep
/etcSystem configuration filespasswd, hosts, fstab
/devDevice files/dev/null, /dev/sda, /dev/tty
/procVirtual filesystem — running processes/proc/1234/status, /proc/cpuinfo
/tmpTemporary files (cleared on reboot)Your temp files
/homeUser home directories/home/alice, /home/bob
/libShared librarieslibc.so.6

The /proc Filesystem

/proc is a virtual filesystem — it is not stored on disk. The kernel creates it in memory to expose information about running processes. For process PID 1234, /proc/1234/ contains: status (memory, state), fd/ (open file descriptors), maps (memory map), cmdline (command line used to start it). This is how tools like ps and top work — they read from /proc.

File Permissions — rwx and Octal

Every file has three permission triplets: owner, group, others. Each triplet has read (r=4), write (w=2), execute (x=1).

$ ls -la myfile.c
-rwxr-xr-- 1 alice devs 1234 Jun 10 file.c
 ^  ^  ^  ^
 |  |  |  └── others: r-- = 4 (read only)
 |  |  └───── group:  r-x = 5 (read + execute)
 |  └──────── owner:  rwx = 7 (read + write + execute)
 └─────────── file type: - = regular, d = directory, l = symlink

# Octal notation: r=4, w=2, x=1
# rwx = 4+2+1 = 7,  r-x = 4+0+1 = 5,  r-- = 4+0+0 = 4
# So -rwxr-xr-- = 754 in octal

# chmod — change permissions
chmod 755 script.sh     # owner=rwx, group=r-x, others=r-x
chmod 644 data.txt      # owner=rw-, group=r--, others=r--
chmod +x  program       # add execute for all
chmod u-w readonly.c    # remove write from owner (u=user)

# chown — change owner
chown alice:devs file.c  # set owner=alice, group=devs
Key rule: To execute a script you need both read (r) AND execute (x). To list directory contents you need execute on the directory. Forgetting execute on a directory means you cannot cd into it.
Mental model locked in

Unix = everything is a file. The kernel is the gatekeeper. System calls are the door. Library functions are helpers that knock on the door for you. /proc, /dev, /etc are special directories you will use constantly in systems programming.

Essential commands and low-level syscall signatures

Core Unix Commands — Annotated

# Navigation
pwd                      # Print Working Directory — where am I?
cd /home/alice           # Change Directory (absolute path)
cd ..                    # Go up one directory
cd ~                     # Go to home directory
ls -la                   # List All files, Long format (permissions, size)

# File operations
mkdir -p dir/sub/leaf    # Make Directory, -p creates parents too
rm -rf dir               # Remove Forcibly + Recursively (dangerous!)
cp -r src/ dest/         # Copy Recursively
mv old.c new.c           # Move (also used to rename)
cat file.txt             # conCATenate — print file to stdout
head -n 20 file.txt      # First 20 lines
tail -n 20 file.txt      # Last 20 lines
wc -l file.txt           # Word Count, -l = line count only

# Searching
grep -r "main" src/      # Search recursively for "main" pattern
grep -n "TODO" *.c       # Show line numbers, all .c files

# Sorting and processing
sort names.txt           # Sort lines alphabetically
sort -n numbers.txt      # Sort numerically

# Man pages — ALWAYS check these
man ls                   # Manual for ls (shell commands)
man 2 open               # Manual section 2 = system calls
man 3 printf             # Manual section 3 = C library functions
Man sections: Section 1 = shell commands, Section 2 = system calls, Section 3 = C library functions. man 2 read vs man 3 fread — different functions!

Low-Level I/O System Calls — C Signatures

/* Headers needed for all low-level I/O */
#include <fcntl.h>   // open(), O_RDONLY, O_WRONLY, O_CREAT flags
#include <unistd.h>  // read(), write(), close(), STDIN_FILENO etc.
#include <errno.h>   // errno variable + error codes (EINTR, ENOENT...)

/* open — opens a file, returns a file descriptor (int) */
int open(const char *pathname, int flags);
         ^               ^
         file path       O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_TRUNC
 returns: fd >= 3 on success, -1 on error (errno is set)

/* read — reads bytes from fd into buffer */
ssize_t read(int fd, void *buf, size_t count);
             ^       ^           ^
             fd      buffer      max bytes to read
 returns: bytes read (0 = EOF), -1 on error

/* write — writes bytes from buffer to fd */
ssize_t write(int fd, const void *buf, size_t count);
 returns: bytes written, -1 on error

/* close — releases the file descriptor */
int close(int fd);
 returns: 0 on success, -1 on error

/* Standard file descriptors — always open */
0  = STDIN_FILENO   // standard input (keyboard)
1  = STDOUT_FILENO  // standard output (terminal)
2  = STDERR_FILENO  // standard error (terminal)
Key difference from stdio.h: These operate on integer file descriptors, not FILE*. No buffering — every call goes straight to the kernel. Use errno to get detailed error info after a -1 return.

errno — Error Handling

#include <errno.h>
#include <string.h>  // for strerror()

ssize_t n = read(fd, buf, 100);
if (n < 0) {
    int err = errno;              // save immediately — next call may change it
    if (err == EINTR) {
        /* interrupted by a signal — safe to retry */
    } else if (err == ENOENT) {
        fprintf(stderr, "File not found: %s\n", strerror(err));
    }
}

/* Common errno values */
// ENOENT = No such file or directory
// EACCES = Permission denied
// EINTR  = Interrupted by signal (retry is safe)
// EBADF  = Bad file descriptor (fd not open)
// EAGAIN = Try again (non-blocking I/O not ready)
Critical rule: Save errno into a local variable immediately after the failing call. Any subsequent function call (even printf) can overwrite errno. strerror(errno) converts the integer to a human-readable string.

strace — Spying on System Calls

# strace runs a program and prints every system call it makes
strace ./myprogram
strace -e trace=read,write ./myprogram   # filter to specific calls
strace -p 1234                           # attach to running process PID 1234

# Example output of strace on "cat file.txt":
open("file.txt", O_RDONLY)    = 3
read(3, "hello\n", 4096)      = 6
write(1, "hello\n", 6)        = 6
read(3, "", 4096)             = 0  <-- EOF
close(3)                      = 0

# /proc/$PID/fd — see open file descriptors of a process
ls -la /proc/$$/fd   # $$ = current shell's PID
cat /proc/$$/status  # memory usage, state, signals
Use strace when: Your program hangs (look for blocking syscall), crashes with permission error (look for failed open), or you want to understand what a binary does. It is indispensable for debugging at the systems level.

Complete programs you can compile and run

Example 1 — Read a file using low-level syscalls (open/read/write/close) Week 5 Lecture
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        write(STDERR_FILENO, "Usage: readfile <filename>\n", 27);
        return 1;
    }

    /* open() returns a file descriptor — an integer >= 3 */
    /* O_RDONLY = open for reading only                    */
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        /* errno is set by open() on failure */
        fprintf(stderr, "Cannot open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    char buf[4096];
    ssize_t n;

    /* read() returns bytes read, 0 at EOF, -1 on error */
    while ((n = read(fd, buf, sizeof(buf))) > 0) {
        /* write to stdout (fd=1) the exact bytes we read */
        ssize_t written = write(STDOUT_FILENO, buf, n);
        if (written != n) {
            fprintf(stderr, "Write error: %s\n", strerror(errno));
            close(fd);
            return 1;
        }
    }

    if (n == -1) {
        fprintf(stderr, "Read error: %s\n", strerror(errno));
    }

    /* Always close file descriptors — they are a limited resource */
    close(fd);
    return 0;
}
Compile & run
$ gcc -Wall -o readfile readfile.c
$ echo "Hello syscalls!" > test.txt
$ ./readfile test.txt
Hello syscalls!
$ ./readfile nofile.txt
Cannot open nofile.txt: No such file or directory
Example 2 — Write a file, inspect /proc, check file permissions Week 5 Lecture + Tutorial
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>  /* for stat() and file permission bits */

int main(void) {
    /* Create + write a file: O_CREAT|O_WRONLY|O_TRUNC with permissions 0644 */
    /* 0644 octal = rw-r--r-- : owner read+write, group+others read only    */
    int fd = open("output.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd == -1) {
        fprintf(stderr, "open failed: %s\n", strerror(errno));
        return 1;
    }

    const char *msg = "Hello from low-level write!\n";
    write(fd, msg, strlen(msg));
    close(fd);

    /* Use stat() to read back file metadata */
    struct stat st;
    if (stat("output.txt", &st) == 0) {
        printf("File size:        %ld bytes\n", (long)st.st_size);
        printf("Permissions (oct): %o\n", st.st_mode & 0777);
        printf("inode number:      %lu\n", (unsigned long)st.st_ino);
    }

    /* Show our own PID and check /proc */
    pid_t pid = getpid();
    printf("\nThis process PID: %d\n", pid);
    printf("Try: cat /proc/%d/status\n", pid);
    printf("Try: ls -la /proc/%d/fd\n", pid);

    return 0;
}
Sample output
File size: 28 bytes
Permissions (oct): 644
inode number: 2097353

This process PID: 18742
Try: cat /proc/18742/status
Try: ls -la /proc/18742/fd
Example 3 — errno and EINTR: handling signal interruption in read() Week 5 Lecture
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

/* Robust read: retries on EINTR (signal interrupted the syscall) */
ssize_t robust_read(int fd, void *buf, size_t count) {
    ssize_t result;
    do {
        result = read(fd, buf, count);
        /* EINTR means a signal interrupted read() before it could start */
        /* It is safe to retry in this case                               */
    } while (result == -1 && errno == EINTR);
    return result;  /* caller checks for -1 (real error) or 0 (EOF) */
}

int main(void) {
    char buf[256];
    printf("Type something (Ctrl+C is handled, Ctrl+D to quit):\n");

    ssize_t n = robust_read(STDIN_FILENO, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';  /* null-terminate for printf */
        printf("You typed: %s", buf);
    } else if (n == 0) {
        printf("EOF received.\n");
    } else {
        fprintf(stderr, "read() failed: %s\n", strerror(errno));
    }
    return 0;
}
Key insight
read() can return -1 with errno=EINTR when a signal arrives mid-call.
This is NOT a real error — you should retry. Failing to handle EINTR
is a common systems programming bug.

Practice problems with solutions

P1 — What octal permission does chmod 750 set? Describe each group. Tutorial Wk6

For a file with permissions chmod 750 script.sh, write out the full rwx notation and describe what the owner, group, and others can do.

# 750 = 7 5 0
# 7 = 4+2+1 = rwx  → owner  can read, write, execute
# 5 = 4+0+1 = r-x  → group  can read and execute, NOT write
# 0 = 0+0+0 = ---  → others have NO permissions at all

chmod 750 script.sh
# Result: -rwxr-x--- 1 alice devs script.sh

# Verify:
ls -la script.sh
# -rwxr-x--- 1 alice devs 42 Jun 10 script.sh
Memory trick: Add r=4, w=2, x=1 for each position. 7=rwx (all), 6=rw-, 5=r-x, 4=r--, 1=--x, 0=---. The most common permission sets are 755 (world-executable program), 644 (world-readable file), 600 (private file), 700 (private program).
P2 — Spot the bug: three errors in this low-level file copy program Week 5 Lecture pattern

The code below has three bugs. Find and fix each one.

#include <fcntl.h>
#include <unistd.h>
int main(void) {
    int fd = open("input.txt", O_RDONLY);
    char buf[512];
    ssize_t n = read(fd, buf, 512);    /* Bug 1 */
    write(1, buf, n);                  /* Bug 2 */
    /* Bug 3 is missing entirely */
    return 0;
}
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(void) {
    int fd = open("input.txt", O_RDONLY);
    if (fd == -1) {                            /* Fix 1: check open() return */
        fprintf(stderr, "open: %s\n", strerror(errno));
        return 1;
    }
    char buf[512];
    ssize_t n;
    while ((n = read(fd, buf, 512)) > 0) {     /* Fix 2: loop until EOF */
        write(STDOUT_FILENO, buf, n);
    }
    close(fd);                                  /* Fix 3: always close fd */
    return 0;
}
Bug 1: No check on open() return value. If the file does not exist, fd = -1 and the subsequent read(-1, ...) will fail with EBADF.
Bug 2: One read() call only reads up to 512 bytes — a large file would be truncated. Must loop until read() returns 0 (EOF).
Bug 3: Missing close(fd). File descriptors are a limited per-process resource. Forgetting to close is a resource leak — in long-running programs this eventually causes "too many open files" errors.
P3 — Write a shell command sequence to find all .c files larger than 1KB Tutorial Wk6

Using only standard Unix commands, find all .c files in the current directory tree, show their permissions and sizes, then count how many there are. Use commands you know: ls, find, wc, grep.

# Find all .c files recursively
find . -name "*.c"

# Find .c files larger than 1KB and show details
find . -name "*.c" -size +1k -ls

# Count how many .c files exist
find . -name "*.c" | wc -l

# Show permissions of all .c files (ls long format)
ls -la *.c          # current directory only
ls -laR **/*.c      # recursive (bash globstar)

# Check if any .c files are world-writable (security check)
find . -name "*.c" -perm -o+w

# Search for all files containing "malloc"
grep -rn "malloc" . --include="*.c"
find vs ls: Use find for recursive searches with conditions (size, date, permissions). Use ls for quick listing of the current directory. find . -name "*.c" | wc -l is a classic Unix pipeline — find generates the list, wc -l counts the lines.
P4 — Explain the difference between a system call and a library function Wk6 Tutorial B

Compare fread() from <stdio.h> with read() from <unistd.h>. What are they each doing at the hardware/OS level? When would you choose one over the other?

/* fread — library function (stdio.h)                              */
/* Lives in user space, wraps read() internally                    */
/* Has an internal buffer (usually 4096 or 8192 bytes)             */
/* Many small fread() calls → few actual kernel read() calls       */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

/* read — system call (unistd.h)                                   */
/* Every call crosses user→kernel boundary (expensive context switch) */
/* No buffering — what you ask for is what the kernel does         */
ssize_t read(int fd, void *buf, size_t count);

/*
 * When to use fread (buffered):
 * - Text processing, reading structured data
 * - Lots of small reads (buffers accumulate them → fewer syscalls)
 * - Portability required
 *
 * When to use read (unbuffered):
 * - Need precise control over I/O (non-blocking, signals)
 * - Reading/writing to devices, pipes, sockets
 * - Implementing your own buffer strategy
 * - Size of each operation matters (e.g. network protocols)
 */
Key insight: Every read() call causes a context switch from user space to kernel space — this has overhead. fread() avoids most of these switches by buffering. However, buffering means data may not be written to disk/terminal immediately — fflush() forces the buffer. For interactive programs or network sockets, you often want unbuffered I/O.
P5 — What does /proc/PID/fd tell you? How do you list your open files? Lecture Week 5 + Tutorial

Write a C program that opens two files and then prints a message. After running it under strace, what syscalls do you expect to see for the open and read operations? Also explain what ls -la /proc/$$​/fd shows in the shell.

/* Program that opens two files */
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
    int fd1 = open("a.txt", O_RDONLY);   /* gets fd = 3 */
    int fd2 = open("b.txt", O_RDONLY);   /* gets fd = 4 */
    printf("fd1=%d fd2=%d\n", fd1, fd2);
    /* At this point /proc/PID/fd has entries 0,1,2,3,4 */
    pause(); /* hang so you can inspect /proc */
    close(fd1); close(fd2);
    return 0;
}
# strace output (abbreviated):
openat(AT_FDCWD, "a.txt", O_RDONLY)  = 3
openat(AT_FDCWD, "b.txt", O_RDONLY)  = 4
write(1, "fd1=3 fd2=4\n", 12)        = 12

# ls -la /proc/PID/fd shows symlinks:
# lrwxrwxrwx  0 -> /dev/pts/0   (stdin = terminal)
# lrwxrwxrwx  1 -> /dev/pts/0   (stdout = terminal)
# lrwxrwxrwx  2 -> /dev/pts/0   (stderr = terminal)
# lrwxrwxrwx  3 -> /home/user/a.txt
# lrwxrwxrwx  4 -> /home/user/b.txt
/proc/PID/fd is a directory of symbolic links — each link is a file descriptor number pointing to what that fd is connected to. Running ls -la /proc/$$/fd in bash shows your shell's open files. This is how you diagnose "too many open files" errors — you can see exactly which files a process has leaked.

Key concepts to memorize

Card 1 of 10
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 17 Quiz — Unix Commands & Syscalls Score: 0 / 6
1
What does chmod 644 file.txt set the permissions to?LO2
multiple choice
2
True or False: The standard file descriptors 0, 1, and 2 refer to stdin, stdout, and stderr respectively, and are always open when a process starts.LO2
true / false
3
What does read() return when it reaches end-of-file?LO2
multiple choice
4
Fill in the blank: The errno code set when a system call is interrupted by a signal (and should be retried) is E____. Type the full constant name.LO2
fill in the blank
5
Spot the bug: what is the critical error in this code?LO2
int fd = open("data.txt", O_RDONLY);
char buf[100];
read(fd, buf, 100);
printf("%s\n", buf);
spot the bug — multiple choice
6
Which directory in the Unix filesystem contains virtual files exposing information about running processes?LO2
multiple choice
0/6
Quiz complete!