I/O and Python




OS Role

Our focus here is on understanding how I/O devices operate and how the operating system interfaces with them to provide I/O to users and other parts of the system. Recall that an OS has two basic roles: Both roles are very evident with I/O, because:

Software Goals

I/O software goals include the following:

I/O Modes of Operations

Synchronous vs. Asynchronous

One important consideration in Input/Output operations is whether the transfer mode operation is synchronous or asynchronous, with respect to CPU execution: The Tanenbaum text somewhat conflates these transfer modes with blocking vs. non-blocking system calls at the OS level; the ideas should be considered distinct. Programmed I/O (discussed next) is synchronous (in terms of CPU communication with I/O hardware devices), while Interrupt-Driven I/O and Direct Memory Access (DMA) are asynchronous, from the CPU perspective.

As mentioned in the IPC section, the OS system calls for data transfer can be implemented as either blocking or non-blocking calls, from the process' perspective. The idea of being blocked or not is a concept we associate with processes or threads, not with hardware and CPUs. Hardware/CPU operations are either synchronous or asynchronous.

Programmed I/O

In Programmed I/O, the CPU does all the work. It is a synchronous transfer mode. A user process requests a device. Once it has access, the user process makes a system call to the OS. If the operation is output, the CPU actively copies the data from user space to kernel space, then the CPU manually transfers the bytes from kernel memory to the device. For input operations, the sequence is reversed.

(a) Programmed I/O output operation.
The big limitation of this approach to I/O is that the CPU, while executing data transfer to/from the device, has to constantly check/wait to see if the device is ready for the next piece of data. This hardware-level activity is sometimes called polling and is a form of busy waiting, because the CPU can't do anything else useful while it waits for the I/O device to be ready for the next transfer operation.

Interrupt-Driven I/O

With Interrupt-Driven I/O, the CPU can perform other processing until the data and device are available for a transfer. However, the actual transfer of bytes between RAM and the device controller is still performed by the CPU.

The general sequence of steps for interrupt-driven I/O is as follows: Some info on how the OS must be written to handle interrupts is available here

In a Linux terminal, view detailed interrupt information by executing: cat /proc/interrupts

Limitations. Interrupt-driven I/O is better than Programmed I/O, but still has two drawbacks:

Direct Memory Access (DMA) I/O

Both of the above limitations are alleviated by DMA. With DMA-based transfer, the CPU tells the DMA module what data needs to be transferred, how much, plus source and destination addresses. The CPU turns to other business, and the DMA module does the transfer. When the data transfer is done, the DMA module signals the CPU via an interrupt. Instead of the possibly many interrupts required by Interrupt-Driven I/O, DMA transfers only require a single interrupt.

In DMA I/O, we can assume the CPU accesses all devices and memory via a single system bus that connects the CPU, memory, and the I/O devices. DMA is a hardware construct that is accessible by the OS. There can be multiple DMA controllers (one per device), or one DMA controller that serves multiple devices.

Direct Memory Access (DMA).
An important distinction is that the DMA module can access the system bus independent of the CPU. DMA contrasts with interrupt-driven I/O as follows: with interrupt-driven I/O, the device controller processes a transfer, then issues a hardware interrupt. The CPU answers the interrupt, then the CPU transfers the data from the controller's buffer to RAM. With DMA, the CPU is no longer responsible for moving bytes of data between device buffers and RAM.

I/O Devices and Hardware

I/O Devices

There are two basic types of I/O devices: The classification is inexact, and some I/O devices may not fit neatly into one category or the other.

Device Controllers

Most I/O devices have a Device Controller. This is an electronic unit that communicates with the device hardware. Example controller types include PCIe, SATA, SCSI, Thunderbolt, and USB. The controller's job is to convert a bit stream into a block of bytes and perform any necessary error correction.

I/O Memory Addressing

Each I/O controller normally has:
There are two normal methods by which the CPU communicates with the I/O controller:

I/O in Linux

Device Drivers

A device driver is a piece of code that runs in kernel space and interacts with a device. Usually it is a kernel module that can be loaded as needed (if the device is present), or unloaded, if the device is removed. A device driver will often implement an interrupt handler, and respond to interrupts, based on communications with the hardware device.

File System Devices. Linux implements device drivers supporting the file system, as follows: Device Input. Try the following terminal command: sudo cat /dev/input/mice (depending on your setup, also try /dev/input/mouse* or /dev/input/js*) and move your mouse. The input movements are represented by a character stream. The device driver is transferring these bytes between the device and, in this case, stdout, via buffers in kernel memory.

Device Numbers. I/O in Linux is implemented by a collection of device drivers, one per device type. When a user accesses a special file, the file system determines the major and minor device numbers and whether it is a block special file or character special file. The major device number is used to locate an index into the device driver procedure calls used to open the device, read, write, or close the device.

Raw I/O

Whether a system has port-mapped I/O, memory-mapped I/O, or both, is a function of the architecture. On Intel x86 architectures, both kinds of I/O are supported, but memory-mapped I/O is most commonly used. In order to perform memory-mapped I/O or DMA operations in Linux, you need to be in kernel space.

I/O Ports. in Linux, to see the I/O ports supported by the system architecture, use the following terminal command:
$ sudo cat /proc/ioports
0000-0cf7 : PCI Bus 0000:00
  0000-001f : dma1
  0020-0021 : pic1
  0040-0043 : timer0
  0050-0053 : timer1
  0060-0060 : keyboard
  0064-0064 : keyboard
  0070-0071 : rtc_cmos
    0070-0071 : rtc0
  0080-008f : dma page reg
  00a0-00a1 : pic2
  00c0-00df : dma2
  00f0-00ff : fpu
Example. We can write a primitive program to observe the changes in raw port values, as keys are pressed on the keyboard, using the inb() function (see man inb), which is just a macro (defined in io.h) that the compiler turns into the equivalent x86 machine instruction, inb:
/*  demo.c: very simple example of port I/O
    Compile: gcc −O demo.c -o demo
    Run as sudo/root  */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/io.h>
#define BASEPORT 0x60 /* keyboard */

int main()
  int current = 0;
  int last = 0;
  /* Get access to the port */
  if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);}
  while (1) {

    current = inb(BASEPORT);
    if (current != last) printf("\tread: %d\n", inb(BASEPORT));
    last = current;

 /* reset port permissions */
 if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);}
Note that this is an inexact way of reading input; creating a device driver that registers an interrupt handler with the OS kernel is the preferred method. Also, though the demo code does detect keypresses from the keyboard, the output values observed do not correspond directly to ASCII:
$ gcc -O demo.c -o demo
$ sudo ./demo
  read: 28
  read: 156
a  read: 158
b  read: 176
c  read: 174
d  read: 160
e  read: 146
f  read: 161
g  read: 34
  read: 162
h  read: 163
  read: 29


Python Intro/Refresher Lab