Low-level I/O
File descriptors
While not specified by the C standard, all modern operating systems use file descriptors (sometimes abbreviated as fd) under-the-hood to identify open files. The FILE type from stdio.h and its associated functions encapsulates the low-level details of a stream, which includes a file descriptor the operating system is keeping track of.
This section explores file descriptors in POSIX systems, like Linux.
Standard streams as file descriptors
During process creation, the operating system allocates (among other resources) three standard streams: stdin, stdout, and stderr. Typically, their FILE-based definitions in stdio.h, as covered in an earlier section, are used to interact with them. These streams can also be interacted with through their raw file descriptors:
unistd.h symbol |
stream | File descriptor |
|---|---|---|
STDIN_FILENO |
stdin |
0 |
STDOUT_FILENO |
stdout |
1 |
STDERR_FILENO |
stderr |
2 |
These file descriptors are the same for every process, even though the standard streams contain different data for each process. File descriptors are not unique system-wide; each process has a different view of which file descriptors map to which streams, just like how each process has a different view of the system's virtual address space.
Basic reading and writing
Reading to and writing from a file descriptor can be done with these functions:[1]
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
Compare them with the FILE-based functions:[2]
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
Three differences are apparent:
- Raw file descriptors are used instead of
FILEs. - The data being read from or written to the stream aren't assumed to be strings.
- The return values' types are consistent.
read and fgets have similar parameters: something representing the stream, a buffer, and a size. Also, if the amount of data read equals the requested size, the buffer will have the same contents, regardless of the function used. However, these functions behave differently when less data is read:
fgets, meant for strings, stops reading early if a newline is encountered, and the function may block if it is waiting for the rest of the string to appear in the stream.readdoesn't block; it stops reading early if not all the requested data is in the stream yet.
This makes read more appropriate for situations where the programmer needs more control over the type of the data being read or is willing to trade receiving partially-read data for reducing the number of blocking I/O operations.
write needs an explicit size parameter since it can't assume a NULL-terminated string (or a string at all) is being written, and it will return the number of bytes written so the program can determine whether the passed data was fully written to the stream.
Obtaining and discarding file descriptors
FILE-file descriptor conversions
Turn a stream into a file descriptor using:
And turn a file descriptor into a stream like so:
Like most stream-based functions, it will return NULL on failure. See C programming/Stream IO#Opening Files for mode values.
Warning
The original stream is still usable after calling
fileno, and the original file descriptor is still usable after callingfdopen. Mixing certain actions between the original and converted resources, such as closing one version while continuing to use the other, can cause the C runtime's information for the stream and the operating system's bookkeeping of the file descriptor to become out of sync. Because of this, don't continue to use the original resource post-conversion except when you absolutely have to.
Security through openat
Port I/O
Port I/O functions live in sys/io.h and have the following naming scheme:
- Direction of I/O
into readoutto write
- Data type
bforunsigned charwforunsigned shortlforunsigned int
Here are example function signatures for reading and writing:
#include <sys/io.h>
unsigned char inb(unsigned short port);
void outw(unsigned short value, unsigned short port);
Note
On most computers, programs running with normal permissions can't make use of port I/O. Programs that use these functions may need to be run using a superuser or administrator account.
References
- read(2) and write(2), Linux Programmer's Manual, 2019-10-10
- fgets(3) and fputs(3), Linux Programmer's Manual, 2020-08-13
Source: Wikibooks – Low-level I/O. Licensed under CC BY-SA 3.0.