Device Management and Drivers

Device management is one of the core functions of the I/O system. It governs the various hardware devices and exposes a uniform interface through which programs can reach them. This chapter walks through the basic concepts of device management, how a device driver is written, the allocation strategies that decide which process gets which device, and how these mechanisms are implemented in a real system.

Basic Concepts of Device Management

1. Definition of Device Management

Device management is the collection of system functions through which the operating system manages hardware devices, presents a uniform access interface, and performs allocation, reclamation, and control of those devices.

The point of the discipline is that applications should not have to know whether they are talking to a keyboard, a disk, or a network card. A program issues the same open / read / write calls regardless, and the work of reconciling the differences between devices is pushed down into the operating system.

2. Functions of Device Management

  • Device abstraction: hides hardware differences and offers a uniform interface
  • Device allocation: manages the assignment and reclamation of devices
  • Device control: controls the operation and the state of a device
  • Error handling: deals with device errors and abnormal conditions

3. Basic Structure of Device Management

The central object is the device manager entry, which records the identity, type, status, and current owner of a device, and points at the functions that initialize and tear it down.

// Basic structure of device management
typedef struct {
    int device_id;           // Device identifier
    char device_name[64];    // Device name
    int device_type;         // Device type
    int device_status;       // Device status
    int current_owner;       // Current owner
    void *device_data;       // Device-specific data
    void (*init)(void);      // Initialization function
    void (*cleanup)(void);   // Cleanup function
} device_manager_t;

// Device status values
typedef enum {
    DEVICE_FREE = 0,         // Free
    DEVICE_BUSY,             // Busy
    DEVICE_ERROR,            // Error
    DEVICE_OFFLINE           // Offline
} device_status_t;

Device Drivers

1. Basic Concepts of Device Drivers

Role of a Device Driver

  • Hardware abstraction: hides hardware details and offers a uniform interface
  • Device control: implements device initialization, configuration, and control
  • Data transfer: implements the read and write operations
  • Error handling: deals with device errors and abnormal conditions

A driver is the piece of code that actually knows how to talk to one kind of hardware. Everything above it — the buffering layer, the scheduler, the system call interface — can be written once and reused, which is why the device-independent layer is worth the effort.

Layered Structure of a Device Driver

A driver is conventionally split into three horizontal bands: the interface seen by the user layer, the device-level operations, and the primitive hardware accesses.

// Layered structure of a device driver
typedef struct {
    // User-layer interface
    int (*open)(int device_id);
    int (*close)(int device_id);
    int (*read)(int device_id, void *buffer, int size);
    int (*write)(int device_id, void *buffer, int size);
    int (*ioctl)(int device_id, int cmd, void *arg);

    // Device-layer interface
    int (*init_device)(void);
    int (*reset_device)(void);
    int (*get_status)(void);
    int (*set_config)(void *config);

    // Hardware-layer interface
    int (*hw_read)(int reg);
    int (*hw_write)(int reg, int value);
    int (*hw_interrupt)(void);
} device_driver_t;

2. Driver Development

Driver Framework

The framework carries the metadata the operating system needs in order to manage the driver itself: its name, the major and minor device numbers that route requests to it, the operation table, and a reference count that tells the kernel when the driver is no longer in use.

// Driver framework
typedef struct {
    char driver_name[64];    // Driver name
    int major_number;        // Major device number
    int minor_number;        // Minor device number
    device_driver_t ops;     // Set of operation functions
    void *private_data;      // Private data
    int ref_count;           // Reference count
    mutex_t driver_mutex;    // Driver mutex
} driver_framework_t;

// Register a driver
int register_driver(driver_framework_t *driver) {
    // Check whether the device numbers are available
    if (check_device_number(driver->major_number, driver->minor_number)) {
        return -1; // Device numbers are already in use
    }

    // Register the driver
    add_driver_to_table(driver);

    // Initialize the driver
    if (driver->ops.init_device) {
        driver->ops.init_device();
    }

    return 0;
}

// Unregister a driver
int unregister_driver(driver_framework_t *driver) {
    // Check whether any process is still using the driver
    if (driver->ref_count > 0) {
        return -1; // The driver is still in use
    }

    // Clean up the driver
    if (driver->ops.cleanup) {
        driver->ops.cleanup();
    }

    // Remove it from the driver table
    remove_driver_from_table(driver);

    return 0;
}

Character Device Driver Example

A character device behaves like a stream of bytes: the driver keeps a circular buffer, a reader waits on a semaphore until data is available, and a writer waits until the buffer has room again.

// Character device driver example
typedef struct {
    char *buffer;            // Device buffer
    int buffer_size;         // Buffer size
    int read_pos;            // Read position
    int write_pos;           // Write position
    int data_count;          // Number of bytes buffered
    semaphore_t read_sem;    // Read semaphore
    semaphore_t write_sem;   // Write semaphore
    mutex_t buffer_mutex;    // Buffer mutex
} char_device_data_t;

// Character device open operation
int char_device_open(int device_id) {
    char_device_data_t *data = get_device_data(device_id);

    // Initialize the device data
    data->buffer = malloc(DEFAULT_BUFFER_SIZE);
    data->buffer_size = DEFAULT_BUFFER_SIZE;
    data->read_pos = 0;
    data->write_pos = 0;
    data->data_count = 0;

    init_semaphore(&data->read_sem, 0);
    init_semaphore(&data->write_sem, DEFAULT_BUFFER_SIZE);
    init_mutex(&data->buffer_mutex);

    return 0;
}

// Character device read operation
int char_device_read(int device_id, void *buffer, int size) {
    char_device_data_t *data = get_device_data(device_id);
    int bytes_read = 0;

    // Wait until data is available
    wait_semaphore(&data->read_sem);
    lock_mutex(&data->buffer_mutex);

    // Copy the data out
    while (bytes_read < size && data->data_count > 0) {
        *((char*)buffer + bytes_read) = data->buffer[data->read_pos];
        data->read_pos = (data->read_pos + 1) % data->buffer_size;
        data->data_count--;
        bytes_read++;
    }

    unlock_mutex(&data->buffer_mutex);
    signal_semaphore(&data->write_sem);

    return bytes_read;
}

// Character device write operation
int char_device_write(int device_id, void *buffer, int size) {
    char_device_data_t *data = get_device_data(device_id);
    int bytes_written = 0;

    // Wait until the buffer has room
    wait_semaphore(&data->write_sem);
    lock_mutex(&data->buffer_mutex);

    // Copy the data in
    while (bytes_written < size && data->data_count < data->buffer_size) {
        data->buffer[data->write_pos] = *((char*)buffer + bytes_written);
        data->write_pos = (data->write_pos + 1) % data->buffer_size;
        data->data_count++;
        bytes_written++;
    }

    unlock_mutex(&data->buffer_mutex);
    signal_semaphore(&data->read_sem);

    return bytes_written;
}

Block Device Driver Example

A block device is addressed by block number, so the driver caches the block it most recently touched. If the cache is dirty, that block has to be written back to the device before a different block is loaded over it — otherwise the modification would be lost.

// Block device driver example
typedef struct {
    int block_size;          // Block size
    int total_blocks;        // Total number of blocks
    int current_block;       // Currently cached block
    void *block_buffer;      // Block buffer
    int buffer_dirty;        // Buffer dirty flag
} block_device_data_t;

// Block device read operation
int block_device_read(int device_id, int block_num, void *buffer) {
    block_device_data_t *data = get_device_data(device_id);

    // Check that the block number is valid
    if (block_num >= data->total_blocks) {
        return -1; // Invalid block number
    }

    // Write back the dirty buffer first
    if (data->buffer_dirty) {
        write_block_to_device(data->current_block, data->block_buffer);
        data->buffer_dirty = 0;
    }

    // Read the block into the buffer
    read_block_from_device(block_num, data->block_buffer);
    data->current_block = block_num;

    // Copy the data to the user buffer
    memcpy(buffer, data->block_buffer, data->block_size);

    return data->block_size;
}

// Block device write operation
int block_device_write(int device_id, int block_num, void *buffer) {
    block_device_data_t *data = get_device_data(device_id);

    // Check that the block number is valid
    if (block_num >= data->total_blocks) {
        return -1; // Invalid block number
    }

    // Write back the dirty buffer unless it is the same block
    if (data->buffer_dirty && data->current_block != block_num) {
        write_block_to_device(data->current_block, data->block_buffer);
    }

    // Copy the data into the buffer
    memcpy(data->block_buffer, buffer, data->block_size);
    data->current_block = block_num;
    data->buffer_dirty = 1;

    return data->block_size;
}

Device Allocation Strategies

1. Basic Concepts of Device Allocation

Types of Allocation Strategies

  • Static allocation: the device stays assigned to the process for its whole lifetime
  • Dynamic allocation: the device is assigned on demand and reclaimed as soon as it is released
  • Shared allocation: several processes may use the same device at the same time

The three strategies trade exclusivity for utilization. A printer is normally dedicated, because interleaving two print jobs on one sheet is meaningless, while a disk is normally shared, because interleaving reads and writes is exactly what it is good at.

Device Allocation Table

The allocation table records which process holds which device, when the assignment happened, and under which strategy it was made.

// Device allocation table
typedef struct {
    int device_id;           // Device ID
    int process_id;          // Process ID
    int allocation_time;     // Time of allocation
    int allocation_type;     // Allocation type
    int priority;            // Priority
} device_allocation_t;

// Device allocation manager
typedef struct {
    device_allocation_t *allocations;  // Allocation table
    int allocation_count;              // Number of allocations
    int max_allocations;              // Maximum number of allocations
    mutex_t allocation_mutex;         // Allocation mutex
} device_allocation_manager_t;

2. Static Allocation

Implementation

Under static allocation the device is bound to its process once and stays bound. The bookkeeping is trivial, but the device is idle whenever its owner is not using it.

// Static allocation implementation
typedef struct {
    int device_id;           // Device ID
    int allocated_process;   // Process the device is assigned to
    int allocation_time;     // Time of allocation
    int is_allocated;        // Whether the device is allocated
} static_allocation_t;

// Allocate a device statically
int static_allocate_device(int device_id, int process_id) {
    static_allocation_t *allocation = get_device_allocation(device_id);

    lock_mutex(&allocation->mutex);

    if (allocation->is_allocated) {
        unlock_mutex(&allocation->mutex);
        return -1; // The device has already been allocated
    }

    // Allocate the device
    allocation->allocated_process = process_id;
    allocation->allocation_time = get_current_time();
    allocation->is_allocated = 1;

    unlock_mutex(&allocation->mutex);
    return 0;
}

// Release a statically allocated device
int static_release_device(int device_id, int process_id) {
    static_allocation_t *allocation = get_device_allocation(device_id);

    lock_mutex(&allocation->mutex);

    if (!allocation->is_allocated ||
        allocation->allocated_process != process_id) {
        unlock_mutex(&allocation->mutex);
        return -1; // The device is not allocated, or does not belong to this process
    }

    // Release the device
    allocation->is_allocated = 0;
    allocation->allocated_process = -1;

    unlock_mutex(&allocation->mutex);
    return 0;
}

3. Dynamic Allocation

Implementation

Dynamic allocation queues the requests and lets a scheduler hand out whichever device happens to be free, so a device can move between processes as they come and go.

// Dynamic allocation request queue
typedef struct {
    int process_id;          // Process ID
    int device_id;           // Device ID
    int priority;            // Priority
    int request_time;        // Time of the request
} device_request_t;

// Dynamic allocation manager
typedef struct {
    device_request_t *request_queue;   // Request queue
    int queue_head;                    // Head of the queue
    int queue_tail;                    // Tail of the queue
    int queue_size;                    // Queue size
    int current_allocations[MAX_DEVICES]; // Current allocations
    mutex_t allocation_mutex;          // Allocation mutex
    semaphore_t request_sem;           // Request semaphore
} dynamic_allocation_manager_t;

// Allocate a device dynamically
int dynamic_allocate_device(int device_id, int process_id, int priority) {
    dynamic_allocation_manager_t *manager = get_allocation_manager();

    // Create the allocation request
    device_request_t request;
    request.process_id = process_id;
    request.device_id = device_id;
    request.priority = priority;
    request.request_time = get_current_time();

    // Append it to the request queue
    lock_mutex(&manager->allocation_mutex);

    if ((manager->queue_tail + 1) % manager->queue_size == manager->queue_head) {
        unlock_mutex(&manager->allocation_mutex);
        return -1; // The request queue is full
    }

    manager->request_queue[manager->queue_tail] = request;
    manager->queue_tail = (manager->queue_tail + 1) % manager->queue_size;

    unlock_mutex(&manager->allocation_mutex);
    signal_semaphore(&manager->request_sem);

    // Wait until the allocation completes
    wait_for_allocation(device_id, process_id);

    return 0;
}

// Allocation scheduler
void *allocation_scheduler(void *arg) {
    dynamic_allocation_manager_t *manager = (dynamic_allocation_manager_t*)arg;

    while (1) {
        // Wait for an allocation request
        wait_semaphore(&manager->request_sem);

        lock_mutex(&manager->allocation_mutex);

        // Process the request queue
        while (manager->queue_head != manager->queue_tail) {
            device_request_t request = manager->request_queue[manager->queue_head];
            manager->queue_head = (manager->queue_head + 1) % manager->queue_size;

            // Check whether the device is available
            if (manager->current_allocations[request.device_id] == -1) {
                // Allocate the device
                manager->current_allocations[request.device_id] = request.process_id;
                notify_allocation_complete(request.device_id, request.process_id);
            }
        }

        unlock_mutex(&manager->allocation_mutex);
    }

    return NULL;
}

4. Shared Allocation

Implementation

A shared device keeps a list of the processes that currently hold it. The list bounds how many may use the device at once, and makes it easy to refuse a request from a process that already holds the device.

// Shared device manager
typedef struct {
    int device_id;                   // Device ID
    int *shared_processes;           // List of sharing processes
    int process_count;               // Number of processes
    int max_processes;               // Maximum number of processes
    semaphore_t access_sem;          // Access semaphore
    mutex_t device_mutex;            // Device mutex
} shared_device_manager_t;

// Request access to a shared device
int request_shared_device(int device_id, int process_id) {
    shared_device_manager_t *manager = get_shared_device_manager(device_id);

    // Check whether the maximum number of processes has been reached
    if (manager->process_count >= manager->max_processes) {
        return -1; // The device has reached its maximum number of sharers
    }

    // Check whether the process is already in the list
    for (int i = 0; i < manager->process_count; i++) {
        if (manager->shared_processes[i] == process_id) {
            return 0; // The process is already using the device
        }
    }

    // Add the process to the shared list
    lock_mutex(&manager->device_mutex);
    manager->shared_processes[manager->process_count] = process_id;
    manager->process_count++;
    unlock_mutex(&manager->device_mutex);

    return 0;
}

// Release access to a shared device
int release_shared_device(int device_id, int process_id) {
    shared_device_manager_t *manager = get_shared_device_manager(device_id);

    lock_mutex(&manager->device_mutex);

    // Remove the process from the shared list
    for (int i = 0; i < manager->process_count; i++) {
        if (manager->shared_processes[i] == process_id) {
            // Shift the following elements down
            for (int j = i; j < manager->process_count - 1; j++) {
                manager->shared_processes[j] = manager->shared_processes[j + 1];
            }
            manager->process_count--;
            break;
        }
    }

    unlock_mutex(&manager->device_mutex);
    return 0;
}

Device Error Handling

1. Error Detection Mechanism

Errors are classified by cause so that the handler can react appropriately: a timeout may be worth retrying, a hardware failure usually needs a reset, and a software error is often fixed by re-initializing the device.

// Device error types
typedef enum {
    DEVICE_ERROR_NONE = 0,      // No error
    DEVICE_ERROR_TIMEOUT,       // Timeout error
    DEVICE_ERROR_HARDWARE,      // Hardware error
    DEVICE_ERROR_SOFTWARE,      // Software error
    DEVICE_ERROR_COMMUNICATION  // Communication error
} device_error_type_t;

// Device error handling
typedef struct {
    device_error_type_t error_type;  // Error type
    int error_code;                  // Error code
    char error_message[256];         // Error message
    int retry_count;                 // Number of retries so far
    int max_retries;                 // Maximum number of retries
    void (*error_handler)(int device_id, device_error_type_t error);
} device_error_handler_t;

// Error handling function
void handle_device_error(int device_id, device_error_type_t error) {
    device_error_handler_t *handler = get_error_handler(device_id);

    handler->error_type = error;
    handler->retry_count++;

    switch (error) {
        case DEVICE_ERROR_TIMEOUT:
            if (handler->retry_count < handler->max_retries) {
                // Retry the operation
                retry_device_operation(device_id);
            } else {
                // Report the error
                report_device_error(device_id, "Device timeout error");
            }
            break;

        case DEVICE_ERROR_HARDWARE:
            // A hardware error requires resetting the device
            reset_device(device_id);
            break;

        case DEVICE_ERROR_SOFTWARE:
            // A software error requires re-initializing the device
            reinitialize_device(device_id);
            break;

        default:
            // Unknown error
            report_device_error(device_id, "Unknown device error");
            break;
    }
}

2. Device Recovery Mechanism

Recovery goes beyond the immediate error. It picks a strategy — reset, re-initialize, or even replace the device — and gives up after a bounded number of attempts, marking the device unavailable so that no further requests are sent to it.

// Device recovery strategy
typedef struct {
    int device_id;               // Device ID
    int recovery_strategy;       // Recovery strategy
    int max_recovery_attempts;   // Maximum number of recovery attempts
    int current_attempts;        // Number of attempts so far
    void (*recovery_procedure)(int device_id);
} device_recovery_t;

// Device recovery procedure
void device_recovery_procedure(int device_id) {
    device_recovery_t *recovery = get_device_recovery(device_id);

    if (recovery->current_attempts >= recovery->max_recovery_attempts) {
        // The attempt limit is reached: mark the device as unavailable
        mark_device_unavailable(device_id);
        return;
    }

    recovery->current_attempts++;

    switch (recovery->recovery_strategy) {
        case RECOVERY_STRATEGY_RESET:
            // Reset the device
            reset_device(device_id);
            break;

        case RECOVERY_STRATEGY_REINIT:
            // Re-initialize the device
            reinitialize_device(device_id);
            break;

        case RECOVERY_STRATEGY_REPLACE:
            // Replace the device
            replace_device(device_id);
            break;

        default:
            // Default recovery strategy
            default_recovery_procedure(device_id);
            break;
    }
}

Glossary

TermAbbreviationExplanation
Device Management-The mechanism that manages I/O devices
Device Driver-The program that controls a hardware device
Device Controller-The hardware circuit that controls a device
Device Adapter-The interface card that connects a device
Device Registration-Registering a device with the system
Device Initialization-Initializing a device
Device Allocation-Assigning a device to a process
Device Deallocation-Reclaiming a device
Device Sharing-Several processes sharing one device
Device Protection-Preventing illegal access to a device
Error Handling-Dealing with device errors
Error Detection-Detecting device errors
Error Recovery-Recovering from an error
Device Status-The current state of a device
Busy-The device is working
Idle-The device is free
Error-The device has failed
Interrupt-The device notifying the CPU
Interrupt Vector-The entry point of an interrupt handler
Interrupt RequestIRQThe signal that requests an interrupt
Interrupt Mask-Disabling interrupts
Polling-Repeatedly querying the device status
Direct Memory AccessDMAThe device accesses memory on its own
DMA ControllerDMACThe hardware that controls DMA
Channel-A dedicated I/O processor

Found an issue with this content?

Tell us what could be improved.