Chapter 48: File I/O System Calls: open() and close()

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the concept of a file descriptor and its central role in Linux I/O.
  • Implement the open() system call to establish access to files and devices.
  • Configure file access modes and creation flags to control I/O behavior.
  • Utilize the close() system call to properly release system resources.
  • Develop robust C programs that perform basic file operations on an embedded Linux system like the Raspberry Pi 5.
  • Debug common errors related to file access, permissions, and descriptor management.

Introduction

In the world of Linux and UNIX-like operating systems, a profound and elegant design philosophy prevails: everything is a file. This is not merely a convenient abstraction but a foundational principle that shapes how software interacts with hardware and system resources. Whether your program is reading sensor data from a I2C bus, writing to a serial console, sending data over a network socket, or accessing a traditional text file on an SD card, the underlying mechanism is the same. The kernel presents all these resources as file-like objects, providing a unified and consistent API for all input/output (I/O) operations. This elegant abstraction is the cornerstone of Linux’s power and flexibility, especially in the embedded domain.

This chapter introduces the two most fundamental system calls that serve as the gateway to this unified I/O model: open() and close(). The open() system call is the key that unlocks access to a file, device, or other resource. It asks the kernel to prepare a resource for I/O and, if successful, returns a small, non-negative integer—a file descriptor—that acts as a handle for all subsequent operations. The close() system call is its essential counterpart, informing the kernel that the program has finished with the resource, allowing it to release locks and free precious system resources. For an embedded system like the Raspberry Pi 5, where resources are often more constrained than on a desktop, mastering this simple yet powerful pair of functions is not just an academic exercise; it is a critical skill for writing efficient, reliable, and robust applications.

Technical Background

The File Descriptor: A Handle to the Kernel’s I/O World

To understand file I/O in Linux, one must first grasp the concept of the file descriptor. When a process successfully opens a file, the kernel creates an entry in a system-wide table of all open files, often called the global file table. This entry contains crucial information about the file, such as its location on the storage device (its inode), the current read/write offset, and the access modes for this particular open instance (e.g., read-only).

However, the process itself does not interact with this global table directly. Instead, the kernel allocates an entry in a second, per-process table called the file descriptor table. Each entry in this table simply points to an entry in the global file table. The index of this entry in the per-process table is the file descriptor—a simple integer. By convention, UNIX-like systems reserve the first three file descriptors for standard I/O streams:

  • 0: Standard Input (stdin) – The default source of input, typically the keyboard.
  • 1: Standard Output (stdout) – The default destination for output, typically the screen or terminal.
  • 2: Standard Error (stderr) – The default destination for error messages, also typically the screen.

When you call open(), the kernel finds the lowest-numbered unused slot in your process’s file descriptor table, sets it up to point to the correct entry in the global file table, and returns that slot number to you. From that point on, when you make other I/O system calls like read()write(), or lseek(), you simply pass this file descriptor. The kernel uses it to quickly look up all the necessary context from its internal tables.

Think of it like checking into a hotel. You give the front desk your name and details (the filename and flags). In return, you don’t get the entire hotel room itself, but a simple key card with a room number (the file descriptor). For the rest of your stay, you don’t need to repeat your personal details; you just present the key card to access the elevator, your room, and other hotel services. When you check out (close()), you return the key card, and the hotel can assign that room to someone else. This level of abstraction is incredibly efficient. It decouples the program from the low-level details of the filesystem or device driver, allowing for clean, portable, and powerful code.

Unlocking the Door: The open() System Call

The open() system call is the primary function for gaining access to a file. Its prototype, found in the <fcntl.h> header, is as follows:

C
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags, ...);

Let’s break down its arguments and return value.

  • const char *pathname: This is a standard C string representing the path to the file you wish to open (e.g., /home/pi/data.txt or /dev/i2c-1). The kernel uses this path to locate the resource within the filesystem hierarchy.
  • int flags: This integer argument is a bitmask that tells the kernel how you want to open the file. It is the most critical parameter, controlling both access mode and other operational behaviors. These flags are combined using the bitwise OR (|) operator. The flags fall into two main categories: access mode flags and file creation flags.
  • ...: The optional third argument, a mode_t mode, is only required when the flags argument includes O_CREAT. It specifies the file permissions (e.g., read, write, execute for the owner, group, and others) for the newly created file. We will explore this in more detail shortly.

Access Mode Flags

You must specify exactly one of the following three flags to define the fundamental I/O permissions for the lifetime of the file descriptor:

  • O_RDONLY: Open the file for read-only access. Any attempt to write to the resulting file descriptor will result in an error.
  • O_WRONLY: Open the file for write-only access. Any attempt to read from the descriptor will fail.
  • O_RDWR: Open the file for read-write access. Both reading and writing are permitted.

These flags are mutually exclusive in their base form. Attempting to combine them, such as O_RDONLY | O_WRONLY, does not equal O_RDWR and leads to undefined behavior. You must choose the single flag that represents the access you need.

File Creation and Operational Flags

In addition to the access mode, you can OR several other flags to modify the behavior of the open() call. These are some of the most common and important ones in embedded development:

  • O_CREAT: If the file specified by pathname does not exist, it will be created. If it does exist, this flag has no effect unless O_EXCL is also specified. When using O_CREAT, you must provide the third mode argument to open() to set the permissions of the new file.
  • O_EXCL: Used in conjunction with O_CREAT, this flag ensures that the caller is the one who creates the file. If the file already exists, open() will fail and set the global errno variable to EEXIST. This combination provides an “atomic” test-and-set operation, which is crucial for preventing race conditions where multiple processes might try to create the same file simultaneously.
  • O_TRUNC: If the file already exists and is successfully opened for writing (O_WRONLY or O_RDWR), its length is truncated to zero. All existing data is discarded. If the file does not exist, this flag has no effect.
  • O_APPEND: This flag forces all writes to occur at the end of the file, regardless of any prior lseek() operations. This is essential for log files or any situation where you must guarantee that data is only ever added to the end. The kernel ensures that positioning the file offset and writing the data is an atomic operation, preventing data from being interleaved and corrupted if multiple processes are appending to the same file.
  • O_NONBLOCK: When opening a resource that can block, such as a FIFO (named pipe) or a serial device, this flag causes the open() call (and subsequent I/O calls) to return immediately without waiting. If opening the resource would normally require waiting for another process or for a hardware condition, the call will either succeed immediately or fail with errno set to ENXIO. This is a cornerstone of writing non-blocking, event-driven applications.

open() File Access and Operational Flags

Flag Category Flag Description
Access Modes
(Choose exactly one)
O_RDONLY Open for read-only access.
O_WRONLY Open for write-only access.
O_RDWR Open for read and write access.
Operational Flags
(Combine with `|`)
O_CREAT If the file does not exist, it will be created. Requires the `mode` argument.
O_EXCL Used with O_CREAT. Ensures the caller creates the file; `open()` fails if the file already exists. Atomic operation.
O_TRUNC If the file exists and is opened for writing, its length is truncated to 0 (all data is erased).
O_APPEND All writes will be appended to the end of the file, regardless of the current file offset. Atomic.
O_NONBLOCK Open in non-blocking mode. The `open()` call and subsequent I/O will not wait and will return immediately.

The mode Argument

When O_CREAT is used, the third argument to open() becomes mandatory. This mode_t mode argument specifies the file permissions for the new file. These permissions are represented as an octal number, similar to the chmod command-line utility. The permissions are defined in <sys/stat.h> and common values include:

File Permission Constants for `mode` Argument

Permission For Symbolic Constant Octal Value
Owner (User) S_IRUSR 0400 (Read)
S_IWUSR 0200 (Write)
S_IXUSR 0100 (Execute)
Group S_IRGRP 0040 (Read)
S_IWGRP 0020 (Write)
S_IXGRP 0010 (Execute)
Others S_IROTH 0004 (Read)
S_IWOTH 0002 (Write)
S_IXOTH 0001 (Execute)
Example: S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH is equivalent to 0644.

A common combination is 0644 in octal, which is equivalent to S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH. This grants read/write permission to the file’s owner and read-only permission to everyone else.

Tip: The permissions you specify with the mode argument are filtered by the process’s umask. The umask is a system setting that automatically turns off certain permission bits for newly created files. For example, a common umask of 0022 will prevent group and other write permissions from being set, even if you request them in the mode argument. This is a security feature to prevent accidentally creating world-writable files.

Return Value and Error Handling

The return value of open() is the key to its usage.

  • On successopen() returns a new, non-negative integer file descriptor.
  • On failureopen() returns -1.

When an error occurs, the function sets a global integer variable named errno to a value that indicates the specific reason for the failure. The <errno.h> header file defines symbolic names for these error codes (e.g., EACCES for permission denied, ENOENT for file not found).

A robust program must always check the return value of open(). Ignoring a -1 return value and attempting to use it as a file descriptor will lead to unpredictable and often disastrous results, as other system calls will fail or, even worse, operate on an unintended file (since -1 is not a valid descriptor, but other negative values might be misinterpreted). The standard way to report these errors is by using the perror() or strerror() functions, which translate the errno code into a human-readable error message.

C
#include <errno.h>
#include <string.h>

// ... inside a function
int fd = open("somefile.txt", O_RDONLY);
if (fd == -1) {
    // An error occurred. Print a message and exit.
    perror("Failed to open somefile.txt");
    // Alternatively:
    // fprintf(stderr, "Error opening file: %s\n", strerror(errno));
    return 1; // Or exit(EXIT_FAILURE);
}

Closing the Door: The close() System Call

Just as every open() call consumes a resource, a corresponding close() call is required to release it. The close() system call de-allocates the file descriptor, making it available for reuse by subsequent open() calls. It also decrements the reference count in the global file table entry. When this count drops to zero (meaning no process has this file open anymore), the kernel releases its in-memory structures related to the file.

Its prototype is deceptively simple:

C
#include <unistd.h>

int close(int fd);
  • int fd: The file descriptor to be closed.

The return value is straightforward:

  • On successclose() returns 0.
  • On failureclose() returns -1 and sets errno.

While close() can fail (for instance, if an NFS filesystem loses its connection while trying to flush buffered data), failures are rare in typical embedded use cases. However, it is still good practice to check the return value. The most common error is EBADF, which indicates that the fd argument was not a valid, open file descriptor. This often happens if you try to close a descriptor that has already been closed or was never valid in the first place (e.g., the original open() call failed).

Forgetting to close file descriptors is a classic resource leak. In a long-running embedded application, this can be fatal. Each process has a limit on the number of open file descriptors it can have (viewable with ulimit -n). If your application continuously opens files without closing them, it will eventually exhaust its file descriptor table. At that point, all subsequent calls to open()socket(), or any other function that allocates a file descriptor will fail, likely causing the entire application to stop functioning.

Warning: When a process terminates, either by calling exit() or by returning from main(), the kernel automatically closes all of its open file descriptors. While this provides a safety net, it is not a substitute for disciplined programming. Explicitly closing every file descriptor you open is a hallmark of robust, maintainable, and professional code. It makes the resource lifecycle clear and prevents subtle bugs, especially in complex applications or long-running daemons.

Practical Examples

In this section, we will walk through several practical examples on the Raspberry Pi 5. These examples assume you are working directly on the Pi with a standard Raspberry Pi OS installation, which includes the GCC compiler and all necessary development tools. You can write the code in a text editor (like nano or vim) and compile it directly on the command line.

flowchart TD
    subgraph "File I/O Workflow"
        A(Start) --> B{"Call open(path, flags, mode)"};
        style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
        style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff

        B --> C{fd == -1?};
        style C fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff

        C -- "Yes (Error)" --> D["perror('open failed')<br>Exit Program"];
        style D fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff

        C -- "No (Success)" --> E["Use fd for I/O operations"];
        style E fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
        
        E --> F{"Loop:<br>read() or write()"};
        style F fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff

        F --> G{I/O Finished?};
        style G fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        
        G -- No --> F;
        G -- Yes --> H{"Call close(fd)"};
        style H fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff

        H --> I{"close() == -1?"};
        style I fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff

        I -- "Yes (Error)" --> J["perror('close failed')<br>Report Error"];
        style J fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937

        I -- "No (Success)" --> K(End);
        style K fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
        J --> K;
    end

Example 1: Creating and Writing to a New File

This first example demonstrates the most basic use case: creating a new file, writing a short string to it, and closing it. This pattern is fundamental for creating log files, configuration files, or storing output data.

Code (create_file.c)

C
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main() {
    const char *filepath = "rpi_test.txt";
    const char *content = "Hello from Raspberry Pi 5!\n";
    int fd; // File descriptor

    // Define the flags for opening the file
    // O_WRONLY: Open for write-only access.
    // O_CREAT: Create the file if it does not exist.
    // O_TRUNC: If the file exists, truncate it to zero length.
    int flags = O_WRONLY | O_CREAT | O_TRUNC;

    // Define the permissions for the new file (if created)
    // 0644: Owner can read/write, group and others can only read.
    mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; // or just 0644

    printf("Attempting to open '%s' for writing...\n", filepath);

    // Open the file
    fd = open(filepath, flags, mode);

    // ALWAYS check the return value of open()
    if (fd == -1) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }

    printf("File opened successfully. File descriptor is: %d\n", fd);

    // Write the content to the file
    ssize_t bytes_written = write(fd, content, strlen(content));

    if (bytes_written == -1) {
        perror("Error writing to file");
        close(fd); // Attempt to close even on error
        return EXIT_FAILURE;
    }

    printf("%zd bytes written to the file.\n", bytes_written);

    // Close the file
    if (close(fd) == -1) {
        perror("Error closing file");
        return EXIT_FAILURE;
    }

    printf("File closed successfully.\n");

    return EXIT_SUCCESS;
}

Build and Execution Steps

  1. Save the Code: Save the code above into a file named create_file.c.
  2. Compile: Open a terminal on your Raspberry Pi 5 and compile the program using GCC. The -o flag specifies the name of the output executable.
    gcc create_file.c -o create_file
  3. Run the Program: Execute the compiled program.
    ./create_file

Expected Output

You should see the following output on your terminal:

Plaintext
Attempting to open 'rpi_test.txt' for writing...
File opened successfully. File descriptor is: 3
28 bytes written to the file.
File closed successfully.

Note: The file descriptor value 3 is typical because descriptors 0, 1, and 2 are already in use for stdinstdout, and stderr by your shell.

  1. Verify the Result: Use the ls -l command to check that the file was created with the correct permissions and use the cat command to view its contents.
    ls -l rpi_test.txt
    Output should look like this (owner/group might differ):-rw-r–r– 1 pi pi 28 Jul 22 02:24 rpi_test.txtcat rpi_test.txt
    Output:Hello from Raspberry Pi 5!

Example 2: Reading from an Existing File

This example shows how to open an existing file for reading and display its contents to standard output. It complements the previous example.

Code (read_file.c)

C
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define BUFFER_SIZE 128

int main() {
    const char *filepath = "rpi_test.txt";
    int fd;
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;

    printf("Attempting to open '%s' for reading...\n", filepath);

    // Open the file for read-only access.
    // No O_CREAT, so open() will fail if the file doesn't exist.
    fd = open(filepath, O_RDONLY);

    if (fd == -1) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }

    printf("File opened successfully. File descriptor is: %d\n", fd);

    // Read from the file into the buffer
    bytes_read = read(fd, buffer, BUFFER_SIZE - 1); // Leave space for null terminator

    if (bytes_read == -1) {
        perror("Error reading from file");
        close(fd);
        return EXIT_FAILURE;
    }

    // Null-terminate the buffer to treat it as a string
    buffer[bytes_read] = '\0';

    printf("Read %zd bytes. Content:\n---\n%s\n---\n", bytes_read, buffer);

    // Close the file
    if (close(fd) == -1) {
        perror("Error closing file");
        return EXIT_FAILURE;
    }

    printf("File closed successfully.\n");

    return EXIT_SUCCESS;
}


Build and Execution Steps

  1. Prerequisite: Ensure the file rpi_test.txt exists from running the first example.
  2. Save and Compile: Save the code as read_file.c and compile it.
    gcc read_file.c -o read_file
  3. Run: Execute the program.
    ./read_file

Expected Output

Plaintext
Attempting to open 'rpi_test.txt' for reading...
File opened successfully. File descriptor is: 3
Read 28 bytes. Content:
---
Hello from Raspberry Pi 5!

---
File closed successfully.

Example 3: Using O_EXCL for Safe File Creation

This example demonstrates how to use the O_CREAT | O_EXCL flag combination to safely create a “lock” file. This is a common pattern in shell scripting and system daemons to ensure that only one instance of a program is running at a time.

flowchart TD
    subgraph "Atomic Lock File Creation"
        A(Start) --> B{"open(path, O_CREAT | O_EXCL)"};
        style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
        style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff

        B --> C{"open() failed?"};
        style C fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff

        C -- "No (Success)" --> D["File was created.<br><b>Lock acquired.</b><br>Proceed with critical section."];
        style D fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff

        C -- "Yes (Failure)" --> E{errno == EEXIST?};
        style E fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff

        E -- "Yes" --> F["<b>Lock is already held.</b><br>Another instance is running.<br>Exit or wait."];
        style F fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff

        E -- "No" --> G["An unexpected error occurred.<br>(e.g., Permission Denied)<br>Handle other error."];
        style G fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937
        
        D --> H("Release lock:<br>close(fd), unlink(path)");
        style H fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
        
        H --> I(End);
        F --> I;
        G --> I;
        style I fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    end

Code (lock_file.c)

C
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main() {
    const char *lockfile = "/tmp/my_app.lock";
    int fd;

    printf("Attempting to create lock file: %s\n", lockfile);

    // Atomically create the file. Fails if it already exists.
    fd = open(lockfile, O_WRONLY | O_CREAT | O_EXCL, 0644);

    if (fd == -1) {
        // EEXIST is the expected error if the lock is held
        if (errno == EEXIST) {
            fprintf(stderr, "Error: Lock file already exists. Another instance may be running.\n");
        } else {
            // Another unexpected error occurred
            perror("Error creating lock file");
        }
        return EXIT_FAILURE;
    }

    printf("Lock file created successfully. FD: %d\n", fd);
    printf("Application is now 'running'. Press Enter to release lock and exit.\n");

    // In a real application, the main logic would go here.
    // We just wait for user input as a placeholder.
    getchar();

    // Clean up by closing and deleting the lock file.
    close(fd);
    if (unlink(lockfile) == -1) {
        perror("Warning: Failed to delete lock file");
    }

    printf("Lock released. Exiting.\n");

    return EXIT_SUCCESS;
}

Build and Execution Steps

  1. Save and Compile: Save the code as lock_file.c and compile it.
    gcc lock_file.c -o lock_file
  2. First Run: Execute the program in one terminal.
    ./lock_file
    Terminal 1 Output:
    Attempting to create lock file: /tmp/my_app.lock
    Lock file created successfully. FD: 3
    Application is now 'running'. Press Enter to release lock and exit.
    The program will now wait.
  3. Second Run: While the first instance is running, open a second terminal and try to run the program again../lock_file
    Terminal 2 Output:
    Attempting to create lock file: /tmp/my_app.lock
    Error: Lock file already exists. Another instance may be running.
    This second instance fails immediately, as intended.
  4. Cleanup: Go back to the first terminal and press Enter. The program will exit, and the lock file will be removed. You can then run the program again successfully.

Common Mistakes & Troubleshooting

Even with simple functions like open() and close(), several common pitfalls can trip up new and experienced developers alike. Understanding these issues is key to writing robust code.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Forgetting to Check `open()` Return Program crashes or behaves erratically. Subsequent I/O calls like write() fail with EBADF (Bad file descriptor). Always check if the file descriptor is -1 immediately after calling open(). Use perror() to print a descriptive error message.
Missing `mode` with `O_CREAT` File is created with random, unpredictable permissions. This is a security risk and can cause future access problems. The compiler won’t warn you. If you use O_CREAT, you must provide the third mode_t argument to specify permissions, e.g., 0644.
Permission Denied open() returns -1 and errno is EACCES. perror() prints “Permission denied”. Check permissions with ls -l on the file and ls -ld on its directory. Ensure the user has read/write/execute rights. For devices (/dev/…), may require sudo or adding user to a group (e.g., `spi`, `gpio`).
Resource Leak (Descriptor Leak) In a long-running application, calls to open(), socket(), etc., eventually fail with EMFILE (Too many open files). Ensure every successful open() has a corresponding close() call on all possible code paths (including error handling branches).
Path Not Found open() fails with errno set to ENOENT. perror() prints “No such file or directory”. Check for typos in the path. If creating a file, ensure the entire directory path exists. open() will not create intermediate directories for you.

Exercises

  1. File Existence Checker:
    • Objective: Write a C program named check_exist.c that takes a single command-line argument (a file path).
    • Steps:
      1. The program should attempt to open() the file in O_RDONLY mode.
      2. If the open() call succeeds, print a message “File ‘[filename]’ exists.” and then close() the file descriptor.
      3. If open() fails, check errno. If errno is ENOENT, print “File ‘[filename]’ does not exist.” If it’s another error (like EACCES), use perror() to print the specific error.
    • Verification: Run your program with the path to an existing file, a non-existent file, and a file you don’t have permission to read (e.g., /etc/shadow).
  2. Append to a Log File:
    • Objective: Create a program log_append.c that appends a timestamped message to a log file.
    • Steps:
      1. Define a log file path, e.g., /tmp/app.log.
      2. Open the file using the flags O_WRONLY | O_CREAT | O_APPEND. Use permissions 0644.
      3. If successful, create a message string containing the current time (you can use the time() and ctime() functions for this).
      4. Write this message to the file and close it.
    • Verification: Run the program multiple times. Use cat /tmp/app.log to verify that each run adds a new line to the end of the file without overwriting the previous content.
  3. Conditional File Creation:
    • Objective: Write a program create_if_not_exist.c that creates a configuration file, but only if it doesn’t already exist. It should write a default setting into the file upon creation.
    • Steps:
      1. Attempt to open a file (e.g., config.ini) using O_WRONLY | O_CREAT | O_EXCL.
      2. If the call succeeds, it means the file was newly created. Write a default line like “VOLUME=10\n” into the file, then close it. Print a message “config.ini created with default settings.”
      3. If the call fails because errno is EEXIST, print a message “config.ini already exists. No action taken.”
    • Verification: Run the program once and check the file’s content. Run it a second time and verify that the content is unchanged and the correct message is displayed.
  4. File Descriptor Inspector:
    • Objective: Write a program inspect_fds.c to see how file descriptors are allocated.
    • Steps:
      1. Open a file (e.g., file1.txt) and print its file descriptor. Do not close it yet.
      2. Open another file (e.g., file2.txt) and print its file descriptor.
      3. Open a third file (e.g., file3.txt) and print its descriptor.
      4. Now, close() the second file descriptor (for file2.txt).
      5. Finally, open a fourth file (e.g., file4.txt) and print its descriptor. Observe which number is reused.
      6. Close all remaining open file descriptors before exiting.
    • Verification: The output should show sequentially increasing file descriptors (e.g., 3, 4, 5). After closing descriptor 4, the next open() should reuse it, and the new descriptor should also be 4.
  5. Simulating a cat Utility:
    • Objective: Create a simplified version of the cat utility named my_cat.c that reads a file specified on the command line and prints its entire content to standard output.
    • Steps:
      1. Check that a filename was provided as a command-line argument.
      2. open() the specified file for reading (O_RDONLY).
      3. In a loop, read() data from the file into a buffer (e.g., 4096 bytes).
      4. The read() call returns the number of bytes actually read. If this number is greater than 0, write() that many bytes from the buffer to standard output (file descriptor 1).
      5. The loop should continue until read() returns 0 (end of file) or -1 (error).
      6. Handle all errors and close() the file descriptor before exiting.
    • Verification: Run ./my_cat /etc/os-release and compare its output to running the real cat /etc/os-release. They should be identical.

Summary

  • Unified I/O Model: In Linux, every resource, including hardware devices and network connections, can be treated as a file.
  • File Descriptors: A file descriptor is a non-negative integer returned by open() that serves as a handle for all subsequent I/O operations on that resource. The first three descriptors (0, 1, 2) are reserved for stdinstdout, and stderr.
  • open() System Call: This is the gateway to file I/O. It takes a pathname and a set of flags as arguments. The flags control the access mode (O_RDONLYO_WRONLYO_RDWR) and other behaviors (O_CREATO_TRUNCO_APPENDO_EXCL).
  • mode Argument: When creating a file with O_CREAT, a third mode argument is mandatory to specify the file’s access permissions (e.g., 0644).
  • close() System Call: This function is essential for releasing a file descriptor and its associated kernel resources. Failing to close descriptors leads to resource leaks.
  • Error Handling: It is critical to always check the return value of system calls. A return of -1 indicates an error, and the global errno variable contains a code specifying the cause. perror() is the standard tool for printing human-readable error messages.

Further Reading

  1. open(2) Linux Programmer’s Manual: The definitive, authoritative documentation for the open system call. Accessible on your Linux system via man 2 open or at https://man7.org/linux/man-pages/man2/open.2.html
  2. close(2) Linux Programmer’s Manual: The official man page for the close system call. Accessible via man 2 close.
  3. The Linux Programming Interface by Michael Kerrisk. Chapters 4 and 5 provide an exhaustive and highly respected treatment of file I/O and system calls.
  4. Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago. A classic text that provides deep insight into the design philosophy of UNIX-style I/O.
  5. Raspberry Pi Documentation – The Linux kernel: Official documentation from the Raspberry Pi Foundation regarding their customized Linux kernel, useful for understanding device-specific behavior. https://www.raspberrypi.com/documentation/computers/linux_kernel.html
  6. Buildroot User Manual: While not directly about system calls, understanding how an embedded Linux system is built with tools like Buildroot provides context for where device files and filesystems originate. https://buildroot.org/downloads/manual/manual.html
  7. LWN.net: A premier source for in-depth articles about Linux kernel development, including detailed explanations of how system calls are implemented and evolve. https://lwn.net

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top