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.
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;
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;
}
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;
}
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;
}
}
| Term | Abbreviation | Explanation |
|---|---|---|
| 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 Request | IRQ | The signal that requests an interrupt |
| Interrupt Mask | - | Disabling interrupts |
| Polling | - | Repeatedly querying the device status |
| Direct Memory Access | DMA | The device accesses memory on its own |
| DMA Controller | DMAC | The hardware that controls DMA |
| Channel | - | A dedicated I/O processor |
