ioctl Implementation in Kernel Device Drivers Overview ioctl (Input/Output Control) is a powerful system call in Linux used to perform device-specific operations that are not covered by standard system calls like read, write, or open. It allows user-space applications to interact with kernel-space drivers for device-specific configurations and data exchanges.
How ioctl Works 1. User-Space Interaction: A user-space application invokes ioctl using the following prototype: int ioctl(int fd, unsigned long cmd, void *arg); fd: File descriptor for the device. cmd: Command defining the operation. arg: Pointer to the data or argument passed between user-space and kernel-space. 2. Driver-Side Handling: The ioctl system call is routed to the driver by the kernel. The driver implements a specific unlocked_ioctl or compat_ioctl callback in the file_operations structure. 3. Data Flow: Arguments passed via arg can be pointers to user-space data, requiring the driver to use helper functions like copy_from_user and copy_to_user for secure data transfer. Steps to Implement ioctl in a Kernel Driver 1. Define ioctl Commands: Use macros to define command numbers, typically with the _IO, _IOR, _IOW, and _IOWR macros provided in <linux/ioctl.h>. #define MY_IOCTL_BASE 'M' #define IOCTL_CMD_GET _IOR(MY_IOCTL_BASE, 1, int) #define IOCTL_CMD_SET _IOW(MY_IOCTL_BASE, 2, int) _IOR: Read data from the kernel. _IOW: Write data to the kernel. _IOWR: Read and write data. _IO: Command without data. 2. Implement ioctl Callback: Define the unlocked_ioctl function in the driver. Handle commands appropriately based on cmd. static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { int value; switch (cmd) { case IOCTL_CMD_GET: value = 1234; // Example value if (copy_to_user((int __user *)arg, &value, sizeof(value))) return -EFAULT; break; case IOCTL_CMD_SET: if (copy_from_user(&value, (int __user *)arg, sizeof(value))) return -EFAULT; pr_info("Value set by user: %d\n", value); break; default: return -ENOTTY; // Command not supported } return 0; } 3. Integrate into file_operations: Register the ioctl handler in the file_operations structure. static const struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .unlocked_ioctl = my_ioctl, }; 4. Test the ioctl Implementation: Write a user-space application to interact with the driver. #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #define MY_IOCTL_BASE 'M' #define IOCTL_CMD_GET _IOR(MY_IOCTL_BASE, 1, int) #define IOCTL_CMD_SET _IOW(MY_IOCTL_BASE, 2, int) int main() { int fd, value = 42; fd = open("/dev/my_device", O_RDWR); if (fd < 0) { perror("Failed to open device"); return -1; } if (ioctl(fd, IOCTL_CMD_SET, &value) < 0) { perror("ioctl SET failed"); } if (ioctl(fd, IOCTL_CMD_GET, &value) < 0) { perror("ioctl GET failed"); } else { printf("Value from device: %d\n", value); } close(fd); return 0; } Best Practices for ioctl Use Explicit Command Definitions: Follow a consistent naming convention for command macros. Secure User-Kernel Data Transfer: Always validate pointers and sizes. Use copy_from_user and copy_to_user for safe data exchange. Error Handling: Return appropriate error codes for unsupported commands or invalid inputs. Limit ioctl Usage: Avoid using ioctl for operations that can be implemented using read or write. Magic Number: Ensure it’s unique (check Documentation/ioctl/ioctl-number.txt in kernel sources). Atomicity: Use locks if hardware operations are not atomic. Cross-Platform: Handle 32/64-bit compatibility with compat_ioctl if needed. Real-World Example: Custom ARM Board For a custom ARM board, you might need an ioctl to configure hardware parameters like GPIO modes or clock frequencies.
...