Linux内核模块编程之和设备文件对话
和设备文件对话(写和 IOCTL)设备文件应该表现物理设备。大多物理设备既作为输出也作为输入,因此必须有某个机制使内核中的设备驱动程序得到来自进程的输出以便发送到设备。通过为输出打开设备文件并向其写而做到这个,就像写一个普通文件。在下面的例子中,这是用 device_write 实现的。
这不总是足够的。想象你有一个串行口连接到一个调制解调器(即使你有一个内置的调制解调器,从CPU的观点看它仍然是通过串行口连接到调制解调器,因此你不必责备你的想象力)。自然而然的事情是使用设备文件向调制解调器写(要么是调制解调器命令,要么是要通过电话线发送的数据)和从中读(要么是命令回应,要么是接收的数据)。然而,这留下了当你需要和串行口对话时该做什么的问题,例如以什么速率接收和发送数据。
在 Unix 中,答案是使用特殊的函数调用 ioctl ( input output control 的缩写)。每个设备可以有自己的 ioctl 命令,它可以读 ioctl (从进程向内核发送信息)和写 ioctl(返回信息给进程)(注意在这儿读写的作用又是颠倒的,因此ioctl 的读是发送消息给内核而写是从内核接收消息)或者什么也不做。 ioctl 使用三个参数调用: 合适的设备文件的文件描述符, ioctl 号及一个参数,该参数是类型长度,因此你可以使用一个模型传递任何东西。 (这是不准确的。例如你不能通过ioctl传递一个结构 -- 但你可以传递那个结构的指针)
ioctl 号用主设备号, ioctl 类型,命令和参数类型编码。这个 ioctl 号通常用一个头文件中的宏调用 (_IO, _IOR, _IOW 或 _IOWR -- 取决于类型)创建。头文件必须被使用ioctl的程序(因此它们可以生成合适的ioctl)及内核模块(因此它可以理解它) #include。 在下面的范例中,头文件是 chardev.h 而使用它的程序是 ioctl.c。
如果你想在你自己的模块中使用 ioctl ,最好接受官方的 ioctl 分配,因此如果你碰巧得到别人的ioctl或它们得到你的,你就可以知道某些事是错的。需要更多信息,请参考 `Documentation/ioctl-number.txt' 内核源代码树。
范例 chardev.c
/* chardev.c** 创建输入输出的字符设备*//* Copyright (C) 1998-99 by Ori Pomerantz *//* 必要头文件 *//* 标准头文件 */#include /* 内核工作 */#include /* 明确指定是模块 *//* 处理 CONFIG_MODVERSIONS */#if CONFIG_MODVERSIONS==1#define MODVERSIONS#include#endif/* 为了字符设备 *//* 字符设备的定义在此 */#include/* 目前对后面妹妹有用的包装,但可能对未来LINUX版本的兼容性有帮助 */#include/* 我们自己的ioctl 号 */#include "chardev.h"/* 在 2.2.3 版/usr/include/linux/version.h 包含该宏,* 但 2.0.35版不包含-加入以备需要 */#ifndef KERNEL_VERSION#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))#endif#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)#include /* 为了 get_user 和 put_user */#endif#define SUCCESS 0/* 设备声明 ******************************** *//* 将出现在 /proc/devices 中设备名 */#define DEVICE_NAME "char_dev"/* 设备的消息的最大长度 */#define BUF_LEN 80/* 设备正打开?防止对同一设备的同时访问 */static int Device_Open = 0;/* 当被询问时设备将给出的消息 */static char Message[BUF_LEN];/* 进程读取消息到哪儿?如果消息的长度大于我们将用于填充在 device_read的缓冲区的大小,这将有用。*/static char *Message_Ptr;/* 这个函数在进程试图打开设备文件时被调用 */static int device_open(struct inode *inode,struct file *file){#ifdef DEBUGprintk ("device_open(%p)
", file);#endif/* 我们不想同时和两个进程对话 */if (Device_Open)return -EBUSY;/* 如果这是个进程,我们将更小心,因为一个进程可能已经刚好在另一个进程试图增加Device_Open* 之前检查过它。然而我们是在内核中,因此我们在上下文切换上被保护。** 这不是我们应该采取的态度,因为我们可能运行在一个 SMP 单元上,但我们将在后面一章处理SMP*/Device_Open++;/* 初始化消息 */Message_Ptr = Message;MOD_INC_USE_COUNT;return SUCCESS;}/* 当一个进程关闭设备文件时该函数被调用。它没有返回值因为它不能失败。不要考虑其他任何事的发生* 你总应该可以关闭一个设备(在 2.0 版中情况如此,在 2.2 版中设备文件可能不能关闭)。 */#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)static int device_release(struct inode *inode,struct file *file)#elsestatic void device_release(struct inode *inode,struct file *file)#endif{#ifdef DEBUGprintk ("device_release(%p,%p)
", inode, file);#endif/* 为下个调用者做准备 */Device_Open --;MOD_DEC_USE_COUNT;#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)return 0;#endif}/* 当一个已经打开设备文件的进程试图从它读时该函数被调用。 */#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)static ssize_t device_read(struct file *file,char *buffer, /* 填充数据的缓冲区 */size_t length, /* 缓冲区长度 */loff_t *offset) /* 文件偏移量 */#elsestatic int device_read(struct inode *inode,struct file *file,char *buffer, /* 填充数据的缓冲区 */int length) /* 缓冲区长度(一定不能在写时超过它!) */#endif{/* 实际写入缓冲区的字节数 */int bytes_read = 0;#ifdef DEBUGprintk("device_read(%p,%p,%d)
",file, buffer, length);#endif/* 如果在消息尾则返回0表示文件尾 */if (*Message_Ptr == 0)return 0;/* 实际上将数据放入缓冲区 */while (length && *Message_Ptr) {/* 因为缓冲区在用户数据段而不是内核的数据段,分配无法工作。替代的,* 我们使用将内核数据段中的数据拷贝到用户数据段的 put_user 。*/put_user(*(Message_Ptr++), buffer++);length --;bytes_read ++;}#ifdef DEBUGprintk ("Read %d bytes, %d left
",bytes_read, length);#endif/* 读函数应该返回实际插入缓冲区的字节数 */return bytes_read;}/* 当有人向我们的设备文件写时该函数被调用。 */#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)static ssize_t device_write(struct file *file,const char *buffer,size_t length,loff_t *offset)#elsestatic int device_write(struct inode *inode,struct file *file,const char *buffer,int length)#endif{int i;#ifdef DEBUGprintk ("device_write(%p,%s,%d)",file, buffer, length);#endiffor(i=0; i #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)get_user(Message[i], buffer+i);#elseMessage[i] = get_user(buffer+i);#endifMessage_Ptr = Message;/* 又一次返回使用过的输入的字节数 */return i;}/* 当一个进程试图在我们的设备文件上做 ioctl 时该函数被调用。我们需要两个额外的参数* (附加于节点结构和文件结构,那是所有的设备函数都需要的): ioctl 号和给出 ioctl 函数的参数** 如果 ioctl 是写或读/写(意味着输出被返回给调用进程), ioctl 调用返回这个函数的输出。*/int device_ioctl(struct inode *inode,struct file *file,unsigned int ioctl_num,/* ioctl 号 */unsigned long ioctl_param) /* 对它的参数 */{int i;char *temp;#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)char ch;#endif/* 根据 ioctl 调用选择 */switch (ioctl_num) {case IOCTL_SET_MSG:/* 接收指向消息的指针(在用户空间)并将它设为设备的消息 *//* 得到由进程给出的给 ioctl 的参数 */temp = (char *) ioctl_param;/* 找到消息的长度 */#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)get_user(ch, temp);for (i=0; ch && i get_user(ch, temp);#elsefor (i=0; get_user(temp) && i ;#endif/* 不要重新发明车轮-调用 device_write */#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)device_write(file, (char *) ioctl_param, i, 0);#elsedevice_write(inode, file, (char *) ioctl_param, i);#endifbreak;case IOCTL_GET_MSG:/* 将当前的消息给调用进程 - 参数是一个指针,填充它 */#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)i = device_read(file, (char *) ioctl_param, 99, 0);#elsei = device_read(inode, file, (char *) ioctl_param,99);#endif/* 警告 - 我们假设缓冲区长度是100。如果它小于那将使缓冲区溢出而导致进程倾倒核心*(生成core文件)。** 我们只允许99个字符的原因是字符串终止符 NULL 也需要空间。 *//* 将0放置在缓冲区尾使它适当的终止。 */put_user('