Chapter 51: File I/O System Calls: fcntl() for File Control

Chapter Objectives

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

  • Understand the role and significance of the fcntl() system call in Linux I/O operations.
  • Explain the difference between file descriptor flags and file status flags and how to manipulate them.
  • Implement non-blocking I/O on file descriptors, particularly for serial devices, to create responsive embedded applications.
  • Use fcntl() to manage file descriptor properties such as duplicating descriptors and controlling close-on-exec behavior.
  • Debug common issues related to file descriptor manipulation and non-blocking I/O.
  • Apply these concepts to build robust data acquisition and control loops on a Raspberry Pi 5.

Introduction

Applications must often juggle multiple tasks simultaneously without the overhead of traditional multi-threading in all scenarios in the world of embedded Linux. Imagine a control system for a drone. It needs to read data from its gyroscope, receive commands from a ground station via a radio link, and control motor speeds—all in near real-time. If reading from the radio link “blocks” or stalls the entire program while waiting for a command, the drone could lose stability and crash. This is where the concept of non-blocking I/O becomes not just a feature, but a fundamental requirement for safety and performance.

This chapter introduces fcntl(), a powerful and versatile system call that acts as a multi-tool for file descriptor management. While its name, “file control,” may sound mundane, it is the key to unlocking advanced I/O capabilities that are essential for modern embedded systems. The most critical of these is the ability to change a file descriptor’s behavior from blocking to non-blocking. By doing so, you can write applications that poll devices for data, handle multiple I/O streams efficiently within a single thread, and create highly responsive systems.

We will move beyond the basic open(), read(), write(), and close() calls to explore how to modify the very properties of an open file. You will learn how to query a file descriptor’s existing settings and surgically alter them to suit your application’s needs. Using the Raspberry Pi 5, we will demonstrate a practical application by configuring a serial port for non-blocking communication, a common task in embedded projects that interface with GPS modules, modems, or other microcontrollers. By mastering fcntl(), you gain finer control over how your program interacts with the Linux kernel and, by extension, the hardware it manages.

Technical Background

At the heart of the Linux philosophy is the principle that “everything is a file.” This elegant abstraction allows programs to interact with a vast array of resources—from actual files on a disk to devices like serial ports, pipes, and network sockets—using a consistent set of system calls. When a program opens one of these resources, the kernel returns a file descriptor, a small, non-negative integer that serves as a handle for all subsequent I/O operations. While we have learned to use this handle with read() and write(), we have so far treated its underlying properties as fixed. The fcntl() system call shatters this limitation, providing a generic interface to query and modify the characteristics of an open file descriptor.

The fcntl() function prototype, found in <fcntl.h>, is deceptively simple:

C
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

The function takes a file descriptor fd, a command cmd, and an optional third argument arg whose type and meaning depend entirely on the command. This variable-argument structure is what makes fcntl() so flexible. It is a single entry point for a wide range of operations, from duplicating a file descriptor to setting up signal-driven I/O. For our purposes, we will focus on the commands that are most relevant to embedded systems development: managing file descriptor flags and file status flags.

File Descriptor Flags vs. File Status Flags

To understand fcntl(), it’s crucial to distinguish between two sets of flags associated with an open file: file descriptor flags and file status flags.

The file descriptor flags are properties of the file descriptor itself, not the underlying open file description. Currently, there is only one defined file descriptor flag: FD_CLOEXEC. This flag determines the file descriptor’s behavior across an execve() system call. If FD_CLOEXEC is set, the file descriptor will be automatically and atomically closed when the current process executes a new program. This is a vital security and resource management feature. Imagine a server process that opens a sensitive file and then forks a child process to handle a request. If the child process then executes another program (e.g., /bin/sh), you would not want that new program to inherit the file descriptor pointing to the sensitive file. Setting FD_CLOEXEC prevents this potential security leak. The commands F_GETFD (get file descriptor flags) and F_SETFD (set file descriptor flags) are used to manage this flag.

More central to our discussion are the file status flags, which are properties of the open file description shared by all file descriptors that point to it (created via dup() or fork()). These flags affect the semantics of I/O operations themselves. They are initialized by the flags passed to open() (e.g., O_RDWR, O_APPEND, O_NONBLOCK), but can be retrieved and modified later using fcntl(). The commands for this are F_GETFL (get file status flags) and F_SETFL (set file status flags).

The ability to modify these flags after a file has been opened is immensely powerful. A library function, for instance, might be given a file descriptor without knowing how it was opened. If the function requires non-blocking behavior, it cannot simply re-open the file. Instead, it can use fcntl() to retrieve the current flags, add the O_NONBLOCK flag, and then perform its operations. Before returning, it can restore the original flags, leaving the file descriptor in its original state for the calling code.

The Mechanics of Non-Blocking I/O

The most compelling use of fcntl() in embedded Linux is enabling non-blocking I/O. By default, when you call read() on a file descriptor for a device like a serial port or a pipe, the call will block if no data is available. The kernel puts your process to sleep and only wakes it up when data arrives. In a simple, single-purpose program, this is efficient. But in a complex system, it’s a liability.

By setting the O_NONBLOCK flag using F_SETFL, you change this fundamental behavior. When O_NONBLOCK is enabled:

  • A read() call on an empty file descriptor will not block. Instead, it will return immediately with a value of -1, and the errno variable will be set to either EAGAIN (“Try again”) or EWOULDBLOCK. These two error codes are typically interchangeable on Linux.
  • A write() call on a full file descriptor (e.g., a pipe whose buffer is full) will also return -1 and set errno to EAGAIN or EWOULDBLOCK, rather than blocking until space becomes available.

This change allows an application to implement a polling loop. The main loop of the program can run continuously, performing various tasks. On each iteration, it can attempt to read() from a device. If data is present, the read succeeds, and the data is processed. If no data is present, the call returns instantly, and the program can move on to other tasks, such as updating a display, checking other sensors, or adjusting actuator outputs. This creates a responsive system that is always running and never stalled waiting for a single I/O operation.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    subgraph Non-Blocking Read Cycle
        A["Start: Call read(fd, buf, size)"]
        style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff

        B{Is data available in kernel buffer?}
        style B fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff

        A --> B

        B -- Yes --> C["read() returns bytes_read > 0"]
        style C fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff

        C --> D[Process the received data]
        style D fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff

        D --> E[Continue main loop]
        style E fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff

        B -- No --> F["read() returns -1"]
        style F fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff

        F --> G{errno == EAGAIN or<br>errno == EWOULDBLOCK?}
        style G fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff

        G -- Yes --> H["This is normal.<br>No data is available."]
        style H fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff

        H --> E

        G -- No --> I["A real error occurred.<br>Handle error (e.g., log, exit)"]
        style I fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937
    end

The correct procedure for modifying file status flags is critical. You must never simply set the flags you want, as this will overwrite all existing flags. The proper sequence is an atomic read-modify-write operation:

  1. Get: Use fcntl(fd, F_GETFL) to retrieve the current set of flags.
  2. Modify: Use bitwise OR (|) to add the desired flag (e.g., O_NONBLOCK) to the retrieved set. To remove a flag, you would use bitwise AND (&) with the bitwise NOT (~) of the flag.
  3. Set: Use fcntl(fd, F_SETFL, new_flags) to apply the modified set of flags back to the file descriptor.

This ensures that you preserve all other status flags, such as the access mode (O_RDONLY, O_WRONLY, or O_RDWR), which cannot be changed by fcntl() but are part of the returned flags.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    subgraph "Safe Flag Modification: Read-Modify-Write"
        direction TB
        
        Start[Start]
        style Start fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
        
        Step1["<b>Step 1: GET</b><br>int flags = fcntl(fd, F_GETFL);"]
        style Step1 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
        
        Check1{flags == -1?}
        style Check1 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff
        
        Step2["<b>Step 2: MODIFY</b><br>flags |= O_NONBLOCK;"]
        style Step2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
        
        Step3["<b>Step 3: SET</b><br>fcntl(fd, F_SETFL, flags);"]
        style Step3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
        
        Check2{result == -1?}
        style Check2 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff
        
        Error["Handle Error<br>(e.g., perror, exit)"]
        style Error fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937
        
        Success[End: Flag set successfully]
        style Success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
        
        Start --> Step1
        Step1 --> Check1
        Check1 -- Yes --> Error
        Check1 -- No --> Step2
        Step2 --> Step3
        Step3 --> Check2
        Check2 -- Yes --> Error
        Check2 -- No --> Success
    end

Other fcntl() Commands

While our primary focus is on O_NONBLOCK, fcntl() serves several other purposes that are useful in embedded contexts.

  • F_DUPFD and F_DUPFD_CLOEXEC: These commands duplicate a file descriptor, similar to dup() and dup2(). F_DUPFD finds the lowest-numbered available file descriptor greater than or equal to the third argument arg and makes it a copy of fd. The new and old file descriptors share the same open file description (file offset, status flags). F_DUPFD_CLOEXEC does the same but also sets the FD_CLOEXEC flag on the new descriptor, providing a convenient way to prevent inheritance in one step. This can be useful for redirecting stdin or stdout for a child process.
  • File Locking: fcntl() is also the standard POSIX mechanism for advisory file locking, using the commands F_SETLK, F_SETLKW, and F_GETLK. These allow a process to place a read (shared) or write (exclusive) lock on a region of a file. While less common in simple sensor-based embedded applications, this is critical in multi-process systems where different programs might need to coordinate access to a shared resource, such as a configuration file or a data log on an SD card. F_SETLK is non-blocking (it returns an error if the lock cannot be acquired), while F_SETLKW will block until the lock is granted.
  • Signal-Driven I/O (O_ASYNC): Another powerful feature that can be enabled via F_SETFL is asynchronous I/O. By setting the O_ASYNC flag, you can request that the kernel send a signal (typically SIGIO) to your process when I/O becomes possible on the file descriptor. This provides an alternative to polling. Instead of constantly checking the descriptor in a loop, your program can perform other tasks, and it will be interrupted by the SIGIO signal handler only when there is data to be read. This is an event-driven model that can be more efficient than polling, as it eliminates the CPU cycles spent on checking empty descriptors. However, it introduces the complexity of signal handling, which requires careful implementation to avoid race conditions and other concurrency issues.
Command Target Purpose & Use Case
F_GETFL / F_SETFL File Status Flags Get or set the open file status flags (e.g., O_NONBLOCK, O_APPEND, O_ASYNC). Essential for changing I/O behavior dynamically.
F_GETFD / F_SETFD File Descriptor Flags Get or set file descriptor flags. The only current flag is FD_CLOEXEC, used to prevent descriptor inheritance across execve() calls.
F_DUPFD / F_DUPFD_CLOEXEC File Descriptor Duplicate a file descriptor, returning the lowest available descriptor number >= arg. A flexible alternative to dup() and dup2().
F_SETLK / F_SETLKW / F_GETLK File Locks Manage advisory file locks. Used to coordinate access to a shared file between multiple processes, preventing data corruption.

In summary, fcntl() is the gateway to a more sophisticated level of interaction with the Linux kernel’s I/O subsystem. It allows a program to dynamically adapt the behavior of its file descriptors to meet the demands of the application, with non-blocking I/O being the most transformative capability for real-time and responsive embedded systems.

I/O Model Comparison

I/O Model Description CPU Usage Use Case
Blocking I/O (Default) The process is put to sleep by the kernel until the I/O operation can be completed. The read() or write() call does not return until it’s done. Very low. The process consumes no CPU while blocked. Simple, single-task applications where waiting for I/O is acceptable (e.g., a command-line tool waiting for user input).
Non-Blocking I/O (Polling) The process immediately gets a return value, even if the I/O operation is not complete. If no data is ready, read() returns -1 with errno as EAGAIN. Potentially high. The application must repeatedly check the file descriptor in a loop, which can consume CPU if not managed. Responsive UIs and control loops where the main thread must never stall. Requires a usleep() or similar to yield CPU.
Signal-Driven I/O (Async) The process requests the kernel to send it a signal (SIGIO) when I/O is possible. The application can do other work and is interrupted by the signal. Low. The application only uses CPU when the signal handler is executing. No polling is required. Event-driven systems where the overhead of a polling loop is undesirable. More complex to implement correctly due to signal handling rules.

Practical Examples

In this section, we will apply the theory of fcntl() to a practical problem on the Raspberry Pi 5: reading from a serial (UART) device in a non-blocking fashion. This is a common requirement for projects involving GPS modules, cellular modems, or inter-microcontroller communication. Our goal is to create a program that continuously checks for incoming serial data without ever blocking, allowing a main loop to run at full speed.

Hardware Integration: Connecting a UART Device

For this example, we will simulate a UART device by connecting the Raspberry Pi 5’s transmit (TX) pin to its own receive (RX) pin, creating a loopback circuit. This allows us to write data to the serial port and immediately read it back, providing a reliable way to test our non-blocking I/O code without needing an external device.

Components Required:

  • Raspberry Pi 5
  • One female-to-female jumper wire

Wiring Instructions:

The primary UART on the Raspberry Pi 5 is ttyAMA0, which is exposed on the 40-pin GPIO header.

  • GPIO 14 (Pin 8) is the default TXD0 (Transmit) pin.
  • GPIO 15 (Pin 10) is the default RXD0 (Receive) pin.

Connect the jumper wire between Pin 8 (TX) and Pin 10 (RX) on the GPIO header.

Warning: Always ensure your Raspberry Pi is powered off when making or changing GPIO connections. Connecting the wrong pins can damage the device. The TX-to-RX loopback is safe, but caution is always advised.

Build and Configuration Steps

First, we need to ensure the serial port is enabled and accessible to our user program.

1. Enable the Serial Port:

On Raspberry Pi OS, the serial port can be enabled using the raspi-config utility.

Bash
sudo raspi-config

Navigate to 3 Interface Options -> I6 Serial Port.

  • When asked “Would you like a login shell to be accessible over serial?”, answer No. A login shell will interfere with our program’s ability to control the port.
  • When asked “Would you like the serial port hardware to be enabled?”, answer Yes.
  • Finish and reboot the Raspberry Pi for the changes to take effect.

This configuration will make the primary UART available at the device file /dev/ttyAMA0.

2. Verify Device Permissions:

Check the permissions of the serial device file.

Bash
ls -l /dev/ttyAMA0

The output should look something like this:

Plaintext
crw-rw---- 1 root dialout 204, 64 Jul 22 10:30 /dev/ttyAMA0

The device is owned by root and the dialout group. To access it without sudo, add your user to the dialout group.

Bash
sudo usermod -a -G dialout $USER

You will need to log out and log back in for this group membership change to take effect.

Code Snippet: Non-Blocking Serial Read

Now, we will write a C program that demonstrates the use of fcntl() to set the O_NONBLOCK flag. The program will open the serial port, configure it for non-blocking reads, and then enter a loop. Inside the loop, it will attempt to read from the port and also periodically write a message to the port. Because of our loopback wire, any message we write will become available for reading.

Create a file named nonblocking_serial.c:

C
// nonblocking_serial.c
// Demonstrates using fcntl() for non-blocking serial port I/O on Raspberry Pi 5.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h> // For serial port configuration
#include <time.h>

// The serial port device file
const char* SERIAL_PORT = "/dev/ttyAMA0";

// Function to set file descriptor to non-blocking mode
int set_nonblock(int fd) {
    // 1. Get the current file status flags
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl(F_GETFL)");
        return -1;
    }

    // 2. Modify the flags to include O_NONBLOCK
    flags |= O_NONBLOCK;

    // 3. Set the new file status flags
    if (fcntl(fd, F_SETFL, flags) == -1) {
        perror("fcntl(F_SETFL)");
        return -1;
    }

    return 0;
}

int main() {
    printf("Starting non-blocking serial test...\n");

    // Open the serial port in read-write mode
    int serial_fd = open(SERIAL_PORT, O_RDWR | O_NOCTTY);
    if (serial_fd == -1) {
        perror("Error opening serial port");
        fprintf(stderr, "Ensure the port exists and you have permissions (member of 'dialout' group?)\n");
        return 1;
    }

    printf("Serial port %s opened successfully. fd = %d\n", SERIAL_PORT, serial_fd);

    // --- Configure Serial Port (termios) ---
    // This part is standard for serial communication, independent of fcntl.
    struct termios options;
    tcgetattr(serial_fd, &options); // Get current options
    cfsetispeed(&options, B9600);   // Set baud rate to 9600
    cfsetospeed(&options, B9600);
    options.c_cflag &= ~PARENB;     // No parity
    options.c_cflag &= ~CSTOPB;     // 1 stop bit
    options.c_cflag &= ~CSIZE;      // Mask character size bits
    options.c_cflag |= CS8;         // 8 data bits
    options.c_cflag &= ~CRTSCTS;    // No hardware flow control
    options.c_cflag |= CREAD | CLOCAL; // Enable receiver, ignore modem control lines
    options.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow control
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // Raw input
    options.c_oflag &= ~OPOST; // Raw output
    options.c_cc[VMIN] = 0;    // Read returns immediately
    options.c_cc[VTIME] = 0;   // No timeout
    tcsetattr(serial_fd, TCSANOW, &options); // Apply the settings

    // --- Set the file descriptor to non-blocking mode ---
    printf("Setting port to non-blocking mode...\n");
    if (set_nonblock(serial_fd) == -1) {
        close(serial_fd);
        return 1;
    }
    printf("Port is now in non-blocking mode.\n");

    char read_buf[256];
    int loop_count = 0;
    time_t last_write_time = time(NULL);

    while (1) {
        // --- Attempt to read from the serial port ---
        ssize_t bytes_read = read(serial_fd, read_buf, sizeof(read_buf) - 1);

        if (bytes_read > 0) {
            // Data was successfully read
            read_buf[bytes_read] = '\0'; // Null-terminate the string
            printf("RX (%ld bytes): %s\n", bytes_read, read_buf);
        } else if (bytes_read == 0) {
            // This case is less common with serial ports but good to handle
            printf("Read 0 bytes (EOF?).\n");
        } else { // bytes_read == -1
            // No data available to read
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // This is the expected outcome of a non-blocking read with no data
                // We don't print anything here to avoid flooding the console.
            } else {
                // An actual error occurred
                perror("read failed");
                break;
            }
        }

        // --- Main loop work ---
        // The program does not block on read, so this part always runs.
        // We can do other things here, like processing other data or updating a status.
        printf("Main loop running... Count: %d\r", loop_count++);
        fflush(stdout);

        // --- Periodically write data to the port ---
        // Due to the loopback, this data will become available for reading.
        time_t current_time = time(NULL);
        if (current_time - last_write_time >= 5) {
            char write_msg[64];
            snprintf(write_msg, sizeof(write_msg), "Hello from loop %d!", loop_count);
            ssize_t bytes_written = write(serial_fd, write_msg, strlen(write_msg));
            if (bytes_written > 0) {
                printf("\nTX (%ld bytes): %s\n", bytes_written, write_msg);
            } else {
                perror("\nwrite failed");
            }
            last_write_time = current_time;
        }

        // Sleep for a short period to prevent the loop from consuming 100% CPU
        usleep(100000); // 100ms
    }

    close(serial_fd);
    printf("\nProgram terminated.\n");
    return 0;
}

Build, Flash, and Boot Procedures

Since we are developing directly on the Raspberry Pi 5, the process is straightforward compilation and execution. There is no cross-compilation or flashing step required.

1. Compile the Code:

Open a terminal on your Raspberry Pi and compile the program using GCC.

Bash
gcc -o nonblocking_serial nonblocking_serial.c

This command compiles nonblocking_serial.c and creates an executable file named nonblocking_serial.

2. Run the Program:

Execute the compiled program.

Bash
./nonblocking_serial

Expected Output:

You will see the program start and the “Main loop running…” message will update rapidly. The program is not blocked waiting for input. Every 5 seconds, the program will write a “Hello…” message. Because of the loopback wire, this message is immediately sent from the TX pin to the RX pin and becomes available in the serial port’s input buffer. On a subsequent iteration of the while loop, the read() call will find this data, print it to the console, and the loop will continue.

Plaintext
Starting non-blocking serial test...
Serial port /dev/ttyAMA0 opened successfully. fd = 3
Setting port to non-blocking mode...
Port is now in non-blocking mode.
Main loop running... Count: 48
TX (20 bytes): Hello from loop 49!
Main loop running... Count: 49
RX (20 bytes): Hello from loop 49!
Main loop running... Count: 50
...
Main loop running... Count: 98
TX (21 bytes): Hello from loop 99!
Main loop running... Count: 99
RX (21 bytes): Hello from loop 99!
Main loop running... Count: 100
...

This output clearly demonstrates the power of non-blocking I/O. The main loop is never stalled. It continuously runs, printing its status count. When data is written and becomes available to read, the read() call picks it up. When no data is available, read() returns immediately with EAGAIN, allowing the loop to proceed without delay. This is the fundamental pattern for building responsive, single-threaded event loops in embedded Linux.

Common Mistakes & Troubleshooting

Working with fcntl() and non-blocking I/O can be subtle. Here are some common pitfalls and how to avoid them.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Overwriting Existing Flags File access mode changes unexpectedly; program fails with permission errors after setting O_NONBLOCK.
Incorrect: fcntl(fd, F_SETFL, O_NONBLOCK);
Correct (Read-Modify-Write):
  1. Get flags: int flags = fcntl(fd, F_GETFL);
  2. Modify flags: flags |= O_NONBLOCK;
  3. Set flags: fcntl(fd, F_SETFL, flags);
Misinterpreting read() Return Program exits or reports an error when no data is available, instead of continuing its loop. When read() returns -1 on a non-blocking descriptor, you must check errno.

If errno == EAGAIN or errno == EWOULDBLOCK, it is not an error. It means “try again later”. Continue the loop. Any other errno value indicates a real error.
CPU Hogging in Polling Loop System becomes sluggish; CPU usage for the process is at or near 100%. A tight polling loop runs as fast as possible. Introduce a small delay to yield the CPU to other processes.

Solution: Add usleep(10000); (for 10ms) or a similar delay inside the loop where no data is read. For more advanced cases, use select(), poll(), or epoll().
Forgetting termios Config When reading from a serial port, you receive garbled data, no data, or data that doesn’t match what was sent. Setting O_NONBLOCK does not configure the serial port itself. You must use the termios API to set parameters like:
  • Baud Rate (e.g., B9600)
  • Data Bits (e.g., CS8)
  • Parity (e.g., ~PARENB)
  • Stop Bits
This should be done after open() but before the main I/O loop.
Permissions Error on Device The open() call fails with “Permission denied”. The user running the program does not have read/write access to the device file (e.g., /dev/ttyAMA0).

Solution: Add your user to the group that owns the device (often dialout for serial ports): sudo usermod -a -G dialout $USER. You must log out and log back in for the change to take effect.

Exercises

These exercises are designed to reinforce your understanding of fcntl() and its practical applications.

  1. Report File Descriptor Flags:
    • Objective: Write a C program that inspects the file status flags (F_GETFL) of the standard file descriptors: stdin (0), stdout (1), and stderr (2).
    • Guidance: Your program should call fcntl() for each of these three file descriptors. It should then print the flags in a human-readable format. For example, check for O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, and O_NONBLOCK using bitwise AND (&) and print which ones are set.
    • Verification: Run your program normally (./my_program). Then, run it with its output redirected to a file (./my_program > output.txt). Observe how the flags for stdout change.
  2. Toggle Append Mode:
    • Objective: Write a program that opens a file (log.txt), uses fcntl() to add the O_APPEND flag, and then writes a message to it.
    • Guidance:
      1. Open log.txt using O_WRONLY | O_CREAT | O_TRUNC.
      2. Write the string “First line\n” to it.
      3. Use lseek(fd, 0, SEEK_SET) to move the file offset back to the beginning.
      4. Use fcntl() to add the O_APPEND flag to the existing flags.
      5. Write the string “Second line\n” to it.
    • Verification: Examine the contents of log.txt. The lseek() call should have been ignored because of O_APPEND, and the file should contain “First line\nSecond line\n”. If you remove the fcntl() call, the file will contain “Second line\n” because the second write would overwrite the first.
  3. Blocking vs. Non-Blocking stdin:
    • Objective: Create a program that demonstrates the behavioral difference between blocking and non-blocking reads from stdin.
    • Guidance:
      1. The program should first operate in default (blocking) mode. It should print “Enter text (blocking):” and then call read() on stdin. The program will wait here.
      2. After the first read completes, use fcntl() to set O_NONBLOCK on stdin.
      3. Enter a loop that runs for 5 seconds. Inside the loop, try to read() from stdin. If it returns EAGAIN, print a message like “No input yet…”. If it succeeds, print the input and break the loop.
    • Verification: When you run the program, it will wait for your first input. After you press Enter, it will immediately start the 5-second non-blocking loop, printing the “No input yet…” message until you type something and press Enter again.
  4. Duplicating a Descriptor with F_DUPFD:
    • Objective: Write a program that opens a file and then uses fcntl() with F_DUPFD to create a duplicate file descriptor. Show that both descriptors share the same file offset.
    • Guidance:
      1. Open a file (data.txt) for writing.
      2. Use fcntl(fd1, F_DUPFD, 10) to create a new file descriptor fd2, ensuring it is at least 10.
      3. Write “Hello ” using the original descriptor fd1.
      4. Write “World!\n” using the new descriptor fd2.
    • Verification: Close both descriptors and inspect data.txt. The file should contain “Hello World!\n”. This demonstrates that the write from fd2 continued from the offset left by the write from fd1, proving they share the same open file description.

Summary

  • The fcntl() system call is a versatile tool for manipulating the properties of open file descriptors.
  • File descriptor flags (e.g., FD_CLOEXEC) are per-descriptor properties, while file status flags (e.g., O_NONBLOCK, O_APPEND) are shared by all descriptors pointing to the same open file description.
  • Setting the O_NONBLOCK flag is the key to implementing non-blocking I/O, which is essential for creating responsive embedded applications that must handle multiple I/O streams.
  • In non-blocking mode, a read() call that finds no data returns -1 immediately and sets errno to EAGAIN or EWOULDBLOCK.
  • Modifying flags must be done using a read-modify-write sequence (F_GETFL, bitwise operations, F_SETFL) to avoid overwriting existing flags.
  • Polling loops using non-blocking I/O should often include a short sleep (usleep) to prevent high CPU utilization.
  • Beyond non-blocking I/O, fcntl() can be used for duplicating file descriptors (F_DUPFD) and managing file locks (F_SETLK).

Further Reading

  1. fcntl(2) Linux Manual Page: The definitive, authoritative reference for the fcntl() system call. Access it on your system with man 2 fcntl.
  2. The Linux Programming Interface by Michael Kerrisk: Chapter 5 provides an exhaustive overview of file I/O, and Chapter 63 covers alternative I/O models, including non-blocking I/O.
  3. Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago: Chapter 3 covers file I/O in detail, providing foundational knowledge. Chapter 14 discusses fcntl in the context of advanced I/O.
  4. POSIX.1-2017 fcntl() Specification: The official standard defining the behavior of fcntl(). Available from The Open Group’s website. (https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html)
  5. Raspberry Pi Documentation – The GPIO header: Official documentation detailing the pinout of the Raspberry Pi 5. (https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#gpio-and-the-40-pin-header)

Leave a Comment

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

Scroll to Top