Computing History · Mainframe Era

Channel Programs: How a Mainframe Moved Data Without the CPU

The command word format a channel executes, how chaining turns a list of commands into one operation, and why the channel is a separate processor rather than a DMA engine.

A processor that copies a buffer to a device one word at a time is a processor not doing arithmetic. The naive arrangement, a loop of load-and-store with a test for device readiness, costs several instructions per word and, worse, costs them at the device’s pace: the loop polls a status bit and spins until the mechanical device is ready for the next unit. On a machine with a card reader that delivers a column every few hundred microseconds, a program that polls is idle almost all the time, and a program that does not poll has to be interrupted at the right moment, which requires the device to have a way of raising one.

The System/360 answer was to make the transfer a separate program, run by a separate processor, with its own instruction set and its own memory access. The channel is that processor. It is small: it can fetch a command, fetch or store data, and evaluate a status, and nothing else. But it addresses main storage directly, runs concurrently with the CPU, and raises a single interruption when the whole list of commands is done. The CPU starts the channel by executing one instruction that names a device and the address of a command list, and then continues with its own work.

One command word, eight bytes

The channel’s instruction is the channel command word. It occupies eight bytes and is built from four fields.

The first byte is the command code, which selects the operation: read, write, read backward, control, or sense. Bytes 1 through 3 hold a 24-bit main storage address, which is the reason a channel program cannot touch storage above 16 megabytes without a later addressing extension. Byte 4 is the flag byte. Bytes 5 and 6 hold a 16-bit byte count, so a single command moves at most 65,535 bytes. Byte 7 is reserved and must be zero.

The flag byte is where the structure lives, and its bits are worth naming individually because each one changes the control flow:

  • The data-chain bit says the next command continues the same I/O operation with a new data address. It is how a transfer longer than 65,535 bytes is expressed as several commands.
  • The command-chain bit says the next command is a new operation, executed after this one completes.
  • The skip bit, combined with data chaining, says that the data address of the next command is not used, which allows a read to skip over part of the incoming record.
  • The suppress-length-indication bit says that a length discrepancy between the byte count and the actual transfer should not be reported as an error.
  • The program-controlled-interruption bit says the channel should raise an interruption at the end of this command rather than only at the end of the program.
  • The indirect-data-addressing bit, on later models, says the data address field points to a list of addresses rather than to data.

Chaining is the whole trick. Without it, a transfer of a megabyte would need sixteen separate start-I/O instructions and sixteen interruptions, and the CPU would be back in the loop it was trying to escape. With data chaining, the channel walks a list of command words, each naming the next 64 KB or less, and reports once. With command chaining, a sequence of distinct operations such as a seek followed by a read followed by a sense becomes one unit that either completes or fails as a whole.

A command list that reads a record

The following C structure is a faithful sketch of a two-command channel program that positions a disk and reads a block. The field layout matches the architecture: command code, three reserved bytes, a 24-bit address, flags, and a count.

#include <stdint.h>

/* Write one 8-byte CCW: op, 24-bit address, flags, 16-bit count, zero. */
static void put_ccw(uint8_t *p, uint8_t op, uint32_t addr,
                    uint8_t flags, uint16_t count)
{
    p[0] = op;
    p[1] = (uint8_t)(addr >> 16);   /* address is big-endian */
    p[2] = (uint8_t)(addr >> 8);
    p[3] = (uint8_t)addr;
    p[4] = flags;
    p[5] = (uint8_t)(count >> 8);   /* count is big-endian */
    p[6] = (uint8_t)count;
    p[7] = 0;
}

static uint8_t program[16];

void build_read_program(void)
{
    /* A seek of six parameter bytes, then a read of 4096 bytes. */
    put_ccw(program,     0x07, 0x1000, 0x40, 6);    /* 0x40: command chain */
    put_ccw(program + 8, 0x02, 0x2000, 0x00, 4096);
}

The flag 0x40 in the first command is the command-chain bit; without it the channel would stop after the seek and report completion, and the read would never execute. Writing the words as bytes rather than as a C structure is not pedantry: the count field sits at an odd offset, so a structure containing a uint16_t at that position is padded to a different size by most compilers, and the resulting array no longer matches the eight-byte format the channel decodes.

The eight-byte width is a design constraint with visible consequences. A 16-bit count means that any transfer larger than 64 KB requires chaining, and a 24-bit address means that a channel program running on the original architecture cannot reach storage above the 16 MB line, the same ceiling that dynamic address translation later lifted for programs. When machines grew past that, the fix was not a wider command word but a new addressing mode that let the address field point to an indirect list of full-width addresses, which preserved the format and the channel’s decoder while lifting the limit.

Why a processor rather than a DMA engine

A DMA engine copies from a device to memory and back. A channel does that too, but it also executes a program, and the difference matters in three places.

The first is device-specific sequencing. A disk operation is not a single transfer: the device must be positioned, the transfer must begin at the right rotational moment, and the result must be checked. A DMA engine needs the CPU to perform the positioning, either by programming registers or by polling, and that reintroduces CPU involvement at exactly the points where the device is slowest. A channel executes the positioning commands itself, so the CPU is not involved between the start and the final interruption.

The second is error handling within the operation. When a channel encounters a unit check, it can execute the command list’s remaining commands rather than aborting; a sense command placed after a read in the same chain retrieves the device’s error status without a round trip to the CPU. The channel then reports both the failure and the sense data in one interruption, and the CPU’s handler has everything it needs. This is the origin of the sense/read/sense pattern that appears in every driver for these devices.

The third is that the channel is shared. A single channel processor serves several control units and their devices, so the channel’s own scheduling determines how concurrent transfers interleave. This is why the machine’s configuration is described as a channel-to-control-unit-to-device tree, and why the address of a device is a three-part number: channel, control unit, device. Two devices on the same channel contend for it, and the contention is visible as queueing delay rather than as a CPU cost.

The cost of the arrangement is that the channel is a second program running in the same storage as the first, with no memory protection of its own beyond the storage keys attached to each 2 KB block. A channel program with a bad data address writes wherever the key permits, and the CPU may never learn until the data it depended on is gone. This is why channel programs are built by the access method rather than by application code, and why the addresses in them are validated before the start-I/O instruction executes.

Completion, interruption, and the cost of a lost one

The channel raises an interruption when the command list ends, when a command ends with the program-controlled-interruption bit set, or when an error occurs. The interruption carries a status word that the CPU’s handler reads to learn which device, which kind of completion, and what residual count remained untransferred.

Three failure modes recur.

A missing interruption is the serious one. If the handler clears the device’s status before reading the full status word, or if the interruption is masked at the moment it arrives, the device remains in a state where it believes it has reported, and the CPU believes the operation is still pending. The I/O remains outstanding forever, and a job that waits on it hangs. The defence is that the status is latched in the device until the handler explicitly accepts it, and a handler that runs with interruptions disabled must re-enable and re-check rather than assume no interruption occurred.

A residual count that is not examined hides short transfers. A read that transferred 3,000 of 4,096 bytes reports success with a residual of 1,096. A program that treats success as “the buffer is full” processes uninitialised storage as data. Length checking exists as a separate condition for exactly this reason, and suppressing it is a decision a program should make knowingly.

A unit check with no sense data leaves the handler unable to distinguish a permanent error from a transient one. The convention is to issue a sense command on the same device immediately, before any other operation can clear the sense bytes, and to retry a bounded number of times based on the sense code. Retrying without limit turns a dead device into a hang, and the retry budget is the only thing that converts it back into an error.

The channel is also the reason batch throughput improved without any change to the instruction set’s arithmetic. Because transfers proceed in parallel with computation, a program that reads a tape, computes, and writes a printer line can have all three in flight. That overlap is the mechanism behind spooling, which replaces the physical unit-record devices with disk data sets so that the channel work is disk work and the mechanical devices run independently. The same channel hardware that makes overlap possible makes spooling worthwhile.

References

  1. IBM System/360 Principles of Operation — IBM, GA22-6821
  2. IBM System/360 I/O Interface Channel to Control Unit Original Equipment Manufacturers Information — IBM
  3. IBM z/Architecture Principles of Operation — IBM, SA22-7832