Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1066580
  • 博文数量: 226
  • 博客积分: 10000
  • 博客等级: 上将
  • 技术积分: 2504
  • 用 户 组: 普通用户
  • 注册时间: 2006-06-21 14:12
文章分类

全部博文(226)

文章存档

2011年(1)

2010年(2)

2009年(68)

2008年(4)

2007年(27)

2006年(124)

我的朋友

分类: LINUX

2006-10-24 17:39:15

和设备文件对话(写和 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 DEBUG
printk ("device_open(%p)\n", 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)
#else
static void device_release(struct inode *inode,
struct file *file)
#endif
{
#ifdef DEBUG
printk ("device_release(%p,%p)\n", 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) /* 文件偏移量 */
#else
static int device_read(
struct inode *inode,
struct file *file,
char *buffer, /* 填充数据的缓冲区 */
int length) /* 缓冲区长度(一定不能在写时超过它!) */
#endif
{
/* 实际写入缓冲区的字节数 */
int bytes_read = 0;

#ifdef DEBUG
printk("device_read(%p,%p,%d)\n",
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 DEBUG
printk ("Read %d bytes, %d left\n",
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)
#else
static int device_write(struct inode *inode,
struct file *file,
const char *buffer,
int length)
#endif
{
int i;

#ifdef DEBUG
printk ("device_write(%p,%s,%d)",
file, buffer, length);
#endif

for(i=0; i #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
get_user(Message[i], buffer+i);
#else
Message[i] = get_user(buffer+i);
#endif

Message_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);
#else
for (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);
#else
device_write(inode, file, (char *) ioctl_param, i);
#endif
break;

case IOCTL_GET_MSG:
/* 将当前的消息给调用进程 - 参数是一个指针,填充它 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
i = device_read(file, (char *) ioctl_param, 99, 0);
#else
i = device_read(inode, file, (char *) ioctl_param,
99);
#endif
/* 警告 - 我们假设缓冲区长度是100。如果它小于那将使缓冲区溢出而导致进程倾倒核心
*(生成core文件)。
*
* 我们只允许99个字符的原因是字符串终止符 NULL 也需要空间。 */

/* 将0放置在缓冲区尾使它适当的终止。 */
put_user('\0', (char *) ioctl_param+i);
break;

case IOCTL_GET_NTH_BYTE:
/* 这个 ioctl 既输入 (ioctl_param) 也输出(这个函数的返回值) */
return Message[ioctl_param];
break;
}

return SUCCESS;
}

/* 模块声明 *************************** */

/* 这个结构将保存当进程对我们创建的设备做什么时将调用的函数。因为这个结构的指针被保存在
* 设备表中,所以它不能对 init_module是局部的。 NULL 是为未实现的函数保留的。 */
struct file_operations Fops = {
NULL, /* 偏移 */
device_read,
device_write,
NULL, /* 读目录 */
NULL, /* 选择 */
device_ioctl, /* ioctl */
NULL, /* mmap */
device_open,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
NULL, /* 刷新 */
#endif
device_release /* 又名关闭 */
};

/* 初始化模块 - 登记字符设备 */
int init_module()
{
int ret_val;

/* 登记字符设备(至少是试图登记) */
ret_val = module_register_chrdev(MAJOR_NUM,
DEVICE_NAME,
&Fops);

/* 负数表示错误 */
if (ret_val < 0) {
printk ("%s failed with %d\n",
"Sorry, registering the character device ",
ret_val);
return ret_val;
}

printk ("%s The major device number is %d.\n",
"Registeration is a success",
MAJOR_NUM);
printk ("If you want to talk to the device driver,\n");
printk ("you'll have to create a device file. \n");
printk ("We suggest you use:\n");
printk ("mknod %s c %d 0\n", DEVICE_FILE_NAME,
MAJOR_NUM);
printk ("The device file name is important, because\n");
printk ("the ioctl program assumes that's the\n");
printk ("file you'll use.\n");

return 0;
}

/* 清除 - 从 /proc 中注销相关文件 */
void cleanup_module()
{
int ret;

/* 注销设备 */
ret = module_unregister_chrdev(MAJOR_NUM, DEVICE_NAME);

/* 如果有错误,报告它 */
if (ret < 0)
printk("Error in module_unregister_chrdev: %d\n", ret);
}

范例 chardev.h

/* chardev.h - 包含 ioctl 定义的头文件。
*
* 这里的声明必须在头文件中,因为他们需要被内核模块(chardev.c)和调用它的进程 (ioctl.c)知道。
*/

#ifndef CHARDEV_H
#define CHARDEV_H

#include

/* 主设备号。我们不能再依赖于动态的登记,因为 ioctl 需要知道它。 */
#define MAJOR_NUM 100

/* 设置设备驱动程序的消息 */
#define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *)
/* _IOR 意思是我们正在为从用户进程到内核模块的信息创建一个 ioctl 命令号。
*
* 第一个参数, MAJOR_NUM,是我们使用的主设备号。
*
* 第二个参数是命令号(可能是几个带有不同的意思的)。
*
* 第三个参数是我们想得到的从进程传到内核的类型。
*/

/* 得到设备驱动程序的消息 */
#define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *)
/* 这个 IOCTL 用于输出,得到设备驱动程序的消息。然而我们仍然需要缓冲区作为输入放置消息,因为
* 它已经被进程分配了。
*/

/* 得到消息的第n个字节 */
#define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int)
/* 这个 IOCTL 既用于输入也用于输出。它接受来自用户的一个数 n ,然后返回 Message[n]。 */

/* 设备文件名 */
#define DEVICE_FILE_NAME "char_dev"

#endif

范例 ioctl.c

/* ioctl.c - 进程使用 ioctl 去控制内核模块
*
* 直到现在我们都使用 cat 做输入和输出。但是现在我们需要做 ioctl ,这需要写自己的进程。
*/

/* Copyright (C) 1998 by Ori Pomerantz */


/* 设备细节,例如 ioctl号和主设备号 */
#include "chardev.h"


#include /* 打开 */
#include /* 退出 */
#include /* ioctl */

/* ioctl 调用函数 */

ioctl_set_msg(int file_desc, char *message)
{
int ret_val;

ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);

if (ret_val < 0) {
printf ("ioctl_set_msg failed:%d\n", ret_val);
exit(-1);
}
}

ioctl_get_msg(int file_desc)
{
int ret_val;
char message[100];

/* 警告 - 这是危险的,因为我们没有告诉内核它允许写多远,因此它可能使缓冲区溢出。
* 在真正的产品程序中,我们应该使用两个 ioctl - 一个告诉内核缓冲区长度而另一个填充缓冲区
*/
ret_val = ioctl(file_desc, IOCTL_GET_MSG, message);

if (ret_val < 0) {
printf ("ioctl_get_msg failed:%d\n", ret_val);
exit(-1);
}

printf("get_msg message:%s\n", message);
}

ioctl_get_nth_byte(int file_desc)
{
int i;
char c;

printf("get_nth_byte message:");

i = 0;
while (c != 0) {
c = ioctl(file_desc, IOCTL_GET_NTH_BYTE, i++);

if (c < 0) {
printf(
"ioctl_get_nth_byte failed at the %d'th byte:\n", i);
exit(-1);
}

putchar(c);
}
putchar('\n');
}

/* 主函数 - 调用 ioctl 函数 */
main()
{
int file_desc, ret_val;
char *msg = "Message passed by ioctl\n";

file_desc = open(DEVICE_FILE_NAME, 0);
if (file_desc < 0) {
printf ("Can't open device file: %s\n",
DEVICE_FILE_NAME);
exit(-1);
}

ioctl_get_nth_byte(file_desc);
ioctl_get_msg(file_desc);
ioctl_set_msg(file_desc, msg);

close(file_desc);
}
阅读(892) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~