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:
#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 theerrno
variable will be set to eitherEAGAIN
(“Try again”) orEWOULDBLOCK
. 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 seterrno
toEAGAIN
orEWOULDBLOCK
, 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:
- Get: Use
fcntl(fd, F_GETFL)
to retrieve the current set of flags. - 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. - 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
andF_DUPFD_CLOEXEC
: These commands duplicate a file descriptor, similar todup()
anddup2()
.F_DUPFD
finds the lowest-numbered available file descriptor greater than or equal to the third argumentarg
and makes it a copy offd
. 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 theFD_CLOEXEC
flag on the new descriptor, providing a convenient way to prevent inheritance in one step. This can be useful for redirectingstdin
orstdout
for a child process.- File Locking:
fcntl()
is also the standard POSIX mechanism for advisory file locking, using the commandsF_SETLK
,F_SETLKW
, andF_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), whileF_SETLKW
will block until the lock is granted. - Signal-Driven I/O (
O_ASYNC
): Another powerful feature that can be enabled viaF_SETFL
is asynchronous I/O. By setting theO_ASYNC
flag, you can request that the kernel send a signal (typicallySIGIO
) 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 theSIGIO
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.
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.
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.
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.
ls -l /dev/ttyAMA0
The output should look something like this:
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.
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
:
// 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.
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.
./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.
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.
Exercises
These exercises are designed to reinforce your understanding of fcntl()
and its practical applications.
- 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), andstderr
(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 forO_RDONLY
,O_WRONLY
,O_RDWR
,O_APPEND
, andO_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 forstdout
change.
- Objective: Write a C program that inspects the file status flags (
- Toggle Append Mode:
- Objective: Write a program that opens a file (
log.txt
), usesfcntl()
to add theO_APPEND
flag, and then writes a message to it. - Guidance:
- Open
log.txt
usingO_WRONLY | O_CREAT | O_TRUNC
. - Write the string “First line\n” to it.
- Use
lseek(fd, 0, SEEK_SET)
to move the file offset back to the beginning. - Use
fcntl()
to add theO_APPEND
flag to the existing flags. - Write the string “Second line\n” to it.
- Open
- Verification: Examine the contents of
log.txt
. Thelseek()
call should have been ignored because ofO_APPEND
, and the file should contain “First line\nSecond line\n”. If you remove thefcntl()
call, the file will contain “Second line\n” because the second write would overwrite the first.
- Objective: Write a program that opens a file (
- Blocking vs. Non-Blocking
stdin
:- Objective: Create a program that demonstrates the behavioral difference between blocking and non-blocking reads from
stdin
. - Guidance:
- The program should first operate in default (blocking) mode. It should print “Enter text (blocking):” and then call
read()
onstdin
. The program will wait here. - After the first read completes, use
fcntl()
to setO_NONBLOCK
onstdin
. - Enter a loop that runs for 5 seconds. Inside the loop, try to
read()
fromstdin
. If it returnsEAGAIN
, print a message like “No input yet…”. If it succeeds, print the input and break the loop.
- The program should first operate in default (blocking) mode. It should print “Enter text (blocking):” and then call
- 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.
- Objective: Create a program that demonstrates the behavioral difference between blocking and non-blocking reads from
- Duplicating a Descriptor with
F_DUPFD
:- Objective: Write a program that opens a file and then uses
fcntl()
withF_DUPFD
to create a duplicate file descriptor. Show that both descriptors share the same file offset. - Guidance:
- Open a file (
data.txt
) for writing. - Use
fcntl(fd1, F_DUPFD, 10)
to create a new file descriptorfd2
, ensuring it is at least 10. - Write “Hello ” using the original descriptor
fd1
. - Write “World!\n” using the new descriptor
fd2
.
- Open a file (
- Verification: Close both descriptors and inspect
data.txt
. The file should contain “Hello World!\n”. This demonstrates that the write fromfd2
continued from the offset left by the write fromfd1
, proving they share the same open file description.
- Objective: Write a program that opens a file and then uses
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 setserrno
toEAGAIN
orEWOULDBLOCK
. - 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
fcntl(2)
Linux Manual Page: The definitive, authoritative reference for thefcntl()
system call. Access it on your system withman 2 fcntl
.- 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.
- 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. - POSIX.1-2017
fcntl()
Specification: The official standard defining the behavior offcntl()
. Available from The Open Group’s website. (https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html) - 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)