Jump to content

C Programming/Low-level IO

From Wikibooks, open books for an open world

File descriptors

[edit | edit source]

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

[edit | edit source]

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

[edit | edit source]

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:

  1. Raw file descriptors are used instead of FILEs.
  2. The data being read from or written to the stream aren't assumed to be strings.
  3. 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.
  • read doesn'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

[edit | edit source]

FILE-file descriptor conversions

[edit | edit source]

Turn a stream into a file descriptor using:

#include <stdio.h> int fileno(FILE *stream); 

And turn a file descriptor into a stream like so:

FILE *fdopen(int fd, const char *mode); 

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 calling fdopen. 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

[edit | edit source]

Port I/O

[edit | edit source]

Port I/O functions live in sys/io.h and have the following naming scheme:

  • Direction of I/O
    • in to read
    • out to write
  • Data type
    • b for unsigned char
    • w for unsigned short
    • l for unsigned 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

[edit | edit source]
  1. read(2) and write(2), Linux Programmer's Manual, 2019-10-10
  2. fgets(3) and fputs(3), Linux Programmer's Manual, 2020-08-13