Processes need to be able to communicate with the operating system and other processes, usually they can make requests in terms of identifiable objects. We could write bytes to a file or listen for connections on some named network interface. We identify these using handles.
Handle
Processes don’t share memory with the OS so can’t use its memory address, so we use opaque ‘handles’ to identify OS-level objects. The OS mediates access to the hardware and abstracts away device details so we can’t use device-level identifiers. Some ‘objects’ are also OS constructs: threads, processes.
Link to original
InfOS uses ‘handle’ - an arbitrary 64-bit integer More conventional Unix-like versions use
intfor file handles.
File Descriptors
File Descriptor
File descriptors can refer to many kinds of object:
- terminal (keyboard / display)
- files on disk
- network connections
- devices, e.g. sound card, camera
- (but not threads or processes; use
pid_t,tid_t)These objects mostly support
read()andwrite(), and some support additional operations:Link to original
seek(): seek within a file; i.e. when dealing with audio or videoioctl(): for any other operation that doesn’t fit in these operations Could infer this is dynamically typed.
By default, a Unix process has at least three open file descriptors, those being:
- : standard input
- : standard output
- : standard error
We can think of an ‘fd’ as a logical communication channel, which may be one-way or two-way, and may be read, write or read-write. It accesses an abstraction of a device such as a disk file, network connection, audio / video stream, etc.
Inter-Process Communication
OSes also provide IPC as files can be shared. (in the case of ‘network’ sockets on a special ‘loopback’ network ‘card’)
There are also IPC-only abstractions such as:
- pipes: one-way in-memory channel
- ‘psuedo-terminal’: two-way in-memory channel
- other abstractions may vary by OS (such as local procedure calls)
Improving Memory Access
We can also eliminate user-kernel crossings, since they can be quite slow by using page tables (or any other MMU support) to share just the memory we want to share, or even make it writable by one or both processes.
We could request it by using memory-map on the same file.
The Unix mmap() system call can be used to map a ‘non-anonymous’ (named file), and we can use it to share writable memory by asking for MAP_SHARED instead of MAP_PRIVATE.
Summary of I/O
OSes provide an abstracted interface onto I/O devices, as they often vary significantly, it is not possible for each program to have knowledge of each device.

Device Driver
Device drivers are the OS code for talking to a specific device. They provide a higher-level, device-agnostic interface to the rest of the OS. The OS exposes this to the user program via system calls.
Devices must also be shared (multiplexed securely and usually concurrently).
Link to original
Programmed I/O & Polling
For example, lets say we want to read some data from disk (on x86):
out $0x1f2, %a1 ; %a1 is sector count
out $0x1f3, %b1 ; %b1 is disk block address
out $0x1f7, $0x20 ; 0x20 is "read" commandNow the disk starts reading the data, but it will be millions of CPU cycles before the data is ready, nevertheless we can just keep asking whether it’s ready, this is called polling:
.retry:
in %a1, $0x1f7 ; read drive status
test %a1, 0x80 ; BSY flag set?
jne .retry ; loop if not ready yetCPU is kept busy here and this proves to be very inefficient.
For slow and predictable devices, it is sufficient to poll after ms instead.
Interrupt-driven I/O
To avoid wasting CPU cycles on waiting, we can use hardware interrupts which is both a feature of the CPU and the device. Interrupt-driven I/O lets the CPU carry on and the devices will later interrupt when finished, then jump to a well-defined place in memory (interrupt handler), and finally use programmed I/O to transfer input.

An interrupt controller multiplexes the interrupt pin between multiple devices.
On modern x86 platforms, this is called an APIC.
When a device interrupt is raised, the interrupt controller sets INTR to a logical .
The CPU stops what it’s doing and:
- saves registers on stack
- reads interrupt
#<device>over data bus - looks up interrupt handler
- jumps to interrupt handler
- handler completes I/O
- then uses a special ‘return from interrupt’ instruction (
ireton x86) - CPU reloads saved state from stack
Now the handler simply reads the data (which is now ready) from the device into memory:
mov %cx, 256 ; read 256-bit words
rep in ; into memory pointed by %di (CISC style)
iret ; return from interruptHowever, we are still tied up waiting for a slow device, but for much less time than polling.
Timer interrupts for preemptive scheduling
Timer Interrupt
We can program a timer (special device on I/O bus) to interrupt us after a certain time or with a certain frequency. When the timer interrupt fires, its interrupt handler could call the scheduler to pick another process to run.
Link to original
Direct Memory Access
Direct Memory Access (DMA)
We can improve this even more by using a direct memory access (DMA) controller which allows a device to read / write from memory by itself. This saves even more CPU time by avoiding
Link to originalin/outinstructions from being involved.
Some architectures may memory-map I/O meaning there is no special instructions, instead everything is just loaded / stored to / from special physical memory addresses. We still get an interrupt on completion.