Linux驱动程序开发 - 设备控制接口

来源:互联网 发布:js拉链是ykk 编辑:程序博客网 时间:2024/05/07 23:17
序言
设备驱动程序的一个基本功能就是管理和控制设备,同时为用户应用程序提供管理和控制设备的接口。我们前面的“Hello World”驱动程序已经可以提供读写功能了,在这里我们将扩展我们的驱动以支持设备控制接口,在Linux中这个接口是通过ioctl函数来实现的。

设备控制接口(ioctl 函数
回想一下我们在字符设备驱动中介绍的struct file_operations 结构,这里我们将介绍一个新的方法:

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);


这是驱动程序设备控制接口函数(ioctl函数)的内核原型定义,struct inode *struct file* 描述了操作的文件,unsigned int 描述了ioctl命令号,这是一个重要的参数,我们稍后会对它做详细介绍。最后一个参数是unsigned long数据类型,描述了ioctl命令可能带有的参数,它可能是一个整数或指针数据。
  • ioctl命令号
ioctl命令号是这个函数中最重要的参数,它描述的ioctl要处理的命令。Linux中使用一个32位的数据来编码ioctl命令,它包含四个部分:dir:type:nr:size。
  • dir
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,0U),_IOC_WRITE(向设备写数据,1U)或_IOC_READ(从设备读数据,2U)或他们的逻辑或组合,当然只有_IOC_WRITE和_IOC_READ的逻辑或才有意义。
  • type
描述了ioctl命令的类型,8位。每种设备或系统都可以指定自己的一个类型号,ioctl用这个类型来表示ioctl命令所属的设备或驱动。一般用ASCII码字符来表示,如 'a'。
  • nr
ioctl命令序号,一般8位。对于一个指定的设备驱动,可以对它的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
  • size
ioctl命令的参数大小,一般14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是我们建议你指定这个数据成员,通过它我们可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。

我们可以自己来直接指定一个ioctl命令号,它可能仅仅是一个整数集,但Linux中的ioctl命令号都是有特定含义的,因此通常我们不推荐这么做。其实Linux内核已经提供了相应的宏来自动生成ioctl命令号:

_IO(type,nr)
_IOR(type,nr,size)
_IOW(type,nr,size)
_IOWR(type,nr,size)


宏_IO用于无数据传输,宏_IOR用于从设备读数据,宏 _IOW用于向设备写数据,宏_IOWR用于同时有读写数据的IOCTL命令。相对的,Linux内核也提供了相应的宏来从ioctl命令号种解码相应的域值:

_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)


这些宏都定义在<asm/ioctl.h>头文件中(一般在<asm-generic.h>头文件中)。一般在使用中,先指定各个IOCTL命令的顺序编号(一般从0开始),然后根据使用的环境用这些宏来自动生成IOCTL命令号,在后面的例子中你可以了解实际的使用场景。
  • ioctl返回值
ioctl函数的返回值是一个整数类型的值,如果命令执行成功,ioctl返回零,如果出现错误,ioctl函数应该返回一个负值。这个负值会作为errno值反馈给调用此ioctl的用户空间程序。关于返回值的具体含义,请参考<linux/errno.h>和<asm/errno.h>头文件。
  • ioctl参数
这里有必要说明一下ioctl命令的参数,因为它很容易犯错误。如果ioctl命令参数仅仅是一个整数,那么事情很简单了,我们可以在ioctl函数中直接使用它。但如果它是一个指针数据,那么使用上就要小心了。首先要说明这个参数是有用户空间的程序传递过来的,因此这个指针指向的地址是用户空间地址,在Linux中,用户空间地址是一个虚拟地址,在内核空间是无法直接使用它的。为了解决在内核空间使用用户空间地址的数据,Linux内核提供了以下函数,它们用于在内核空间访问用户空间的数据,定义在<asm/uaccess.h>头文件中:

unsigned long __must_check copy_to_user(void __user *to,
                const void *from, unsigned long n);
unsigned long __must_check copy_from_user(void *to,
                const void __user *from, unsigned long n);


copy_from_user和copy_to_user一般用于复杂的或大数据交换,对于简单的数据类型,如int或char,内核提供了简单的宏来实现这个功能:

#define get_user(x,ptr)
#define put_user(x,ptr)


其中,x是内核空间的简单数据类型地址,ptr是用户空间地址指针。
我们需要牢记:在内核中是无法直接访问用户空间地址数据的。因此凡是从用户空间传递过来的指针数据,务必使用内核提供的函数来访问它们。

这里有必要再一次强调的是,在内核模块或驱动程序的编写中,我们强烈建议你使用内核提供的接口来生成并操作ioctl命令号,这样可以对命令号赋予特定的含义,使我们的程序更加的健壮;另一方面也可以提高程序的可移植性。

举例
好了,是时候举个例子了。我们将扩展我们的helloworld驱动添加ioctl函数。

首先,我们添加一个头文件来定义ioctl接口需要用到的数据(hello.h):

#ifndef _HELLO_H
#define _HELLO_H
#include <asm/ioctl.h>
#define MAXBUF  20
typedef struct _buf_data{
        int size;
        char data [MAXBUF];
}buf_data;

#define HELLO_IOCTL_NR_BASE             0
#define HELLO_IOCTL_NR_SET_DATA         (HELLO_IOCTL_NR_BASE + 1)
#define HELLO_IOCTL_NR_MAX              (HELLO_IOCTL_NR_GET_BUFF + 1)

#define HELLO_IOCTL_SET_DATA            _IOR('h', HELLO_IOCTL_NR_SET_DATA, buf_data*)

#endif


然后为我们的驱动程序添加ioctl接口hello_ioctl,并实现这个函数:

static int hello_ioctl (struct inode *inode, struct file *filp,
                           unsigned int cmd, unsigned long arg)
{
    int cmd_nr;
    int err;
    buf_data buff;

    err = 0;
    cmd_nr = _IOC_NR (cmd);
    switch (cmd_nr){
        case HELLO_IOCTL_NR_SET_DATA:
            if (copy_from_user(&buff, (unsigned char *)arg, sizeof(buf_data)))
            {
                err = -ENOMEM;
                goto error;
            }
            memset(hello_buf, 0, sizeof(hello_buf));
            memcpy(hello_buf, buff.data, buff.size);
            break;
        default:
            printk("hello_ioctl: Unknown ioctl command (%d)/n", cmd);
            break;
    }

error:
    return err;
}

static struct file_operations hello_fops = {
        .read = hello_read,
        .write = hello_write,
        .open = hello_open,
        .ioctl = hello_ioctl,
        .release = hello_release,
};

 



后记
到这里我们已经向您展示了Linux内核驱动程序的设备控制接口(ioctl接口),详细的介绍了它的使用,并给出了一个实际的例子,尽管它很简单,但已经足够了。到这里你可以写出一个标准的Linux驱动程序了。不过这里还有个问题,那就是我们不得不从/proc/devices文件里读取设备号然后手动创建设备节点。我们是否可以让系统自动的创建这个设备节点文件呢?当然可以。不过在那之前,我们必须深入了解Linux的设备驱动模型。后面的章节我们就详细的介绍Linux的设备驱动模型及Hotplug机制。
                     
                                 使用ioctl与内核交换数据
1. 前言
 
使用ioctl系统调用是用户空间向内核交换数据的常用方法之一,从ioctl这个名称上看,本意是针对I/O设备进行的控制操作,但实际并不限制是真正的I/O设备,可以是任何一个内核设备即可。

2. 基本过程
 
在 内核空间中ioctl是很多内核操作结构的一个成员函数,如文件操作结构struct file_operations(include/linux/fs.h)、协议操作结构struct proto_ops(include/linux/net.h)等、tty操作结构struct tty_driver(include/linux/tty_driver.h)等,而这些操作结构分别对应各种内核设备,只要在用户空间打开这些设备, 如I/O设备可用open(2)打开,网络协议可用socket(2)打开等,获取一个文件描述符后,就可以在这个描述符上调用ioctl(2)来向内核 交换数据。
 
3. ioctl(2)
 
ioctl(2)函数的基本使用格式为:
int ioctl(int fd, int cmd, void *data)
第一个参数是文件描述符;cmd是操作命令,一般分为GET、SET以及其他类型命令,GET是用户空间进程从内核读数据,SET是用户空间进程向内核写数据,cmd虽然是一个整数,但是有一定的参数格式的,下面再详细说明;第三个参数是数据起始位置指针,
cmd命令参数是个32位整数,分为四部分:
dir(2b)  size(14b)  type(8b) nr(8b)
详细定义cmd要包括这4个部分时可使用宏_IOC(dir,type,nr,size)来定义,而最简单情况下使用_IO(type, nr)来定义就可以了,这些宏都在include/asm/ioctl.h中定义
本文cmd定义为:
#define NEWCHAR_IOC_MAGIC   'M'
#define NEWCHAR_SET    _IO(NEWCHAR_IOC_MAGIC, 0)
#define NEWCHAR_GET    _IO(NEWCHAR_IOC_MAGIC, 1)
#define NEWCHAR_IOC_MAXNR   1

要 定义自己的ioctl操作,可以有两个方式,一种是在现有的内核代码中直接添加相关代码进行支持,比如想通过socket描述符进行 ioctl操作,可在net/ipv4/af_inet.c中的inet_ioctl()函数中添加自己定义的命令和相关的处理函数,重新编译内核即可, 不过这种方法一般不推荐;第二种方法是定义自己的内核设备,通过设备的ioctl()来操作,可以编成模块,这样不影响原有的内核,这是最通常的做法。
 
4. 内核设备
 
为进行ioctl操作最通常是使用字符设备来进行,当然定义其他类型的设备也可以。在用户空间,可使用mknod命令建立一个字符类型设备文件,假设该设备的主设备号为123,次设备号为0:
mknode /dev/newchar c 123 0
如果是编程的话,可以用mknode(2)函数来建立设备文件。
 
建立设备文件后再将该设备的内核模块文件插入内核,就可以使用open(2)打开/dev/newchar文件,然后调用ioctl(2)来传递数据,最后用close(2)关闭设备。而如果内核中还没有插入该设备的模块,open(2)时就会失败。
 
由于内核内存空间和用户内存空间不同,要将内核数据拷贝到用户空间,要使用专用拷贝函数copy_to_user();要将用户空间数据拷贝到内核,要使用copy_from_user()。
要最简单实现以上功能,内核模块只需要实现设备的open, ioctl和release三个函数即可,
下面介绍程序片断:
static int newchar_ioctl(struct inode *inode, struct file *filep,
unsigned int cmd, unsigned long arg);
static int newchar_open(struct inode *inode, struct file *filep);
static int newchar_release(struct inode *inode, struct file *filep);
// 定义文件操作结构,结构中其他元素为空
struct file_operations newchar_fops =
{
owner:  THIS_MODULE,
ioctl:  newchar_ioctl,
open:  newchar_open,
release: newchar_release,
};
// 定义要传输的数据块结构
struct newchar{
int a;
int b;
};
#define MAJOR_DEV_NUM 123
#define DEVICE_NAME "newchar"
 
打开设备,非常简单,就是增加模块计数器,防止在打开设备的情况下删除模块,
当然想搞得复杂的话可进行各种限制检查,如只允许指定的用户打开等:
static int newchar_open(struct inode *inode, struct file *filep)
{
MOD_INC_USE_COUNT;
 return 0;
}

关闭设备,也很简单,减模块计数器:
static int newchar_release(struct inode *inode, struct file *filep)
{
MOD_DEC_USE_COUNT;
 return 0;
}

进行ioctl调用的基本处理函数
static int newchar_ioctl(struct inode *inode, struct file *filep,
unsigned int cmd, unsigned long arg)
{
int  ret;
// 首先检查cmd是否合法
if (_IOC_TYPE(cmd) != NEWCHAR_IOC_MAGIC) return -EINVAL;
if (_IOC_NR(cmd) > NEWCHAR_IOC_MAXNR) return -EINVAL;
// 错误情况下的缺省返回值
ret = EINVAL;
 switch(cmd)
{
case KNEWCHAR_SET:
// 设置操作,将数据从用户空间拷贝到内核空间
{
struct  newchar  nc;
if(copy_from_user(&nc, (const char*)arg, sizeof(nc)) != 0)
return -EFAULT;
ret = do_set_newchar(&nc);
}
break;
case KNEWCHAR_GET:
// GET操作通常会在数据缓冲区中先传递部分初始值作为数据查找条件,获取全部
// 数据后重新写回缓冲区
// 当然也可以根据具体情况什么也不传入直接向内核获取数据
{
struct  newchar  nc;
if(copy_from_user(&nc, (const char*)arg, sizeof(nc)) != 0)
return -EFAULT;
ret = do_get_newchar(&nc);
if(ret == 0){
if(copy_to_user((unsigned char *)arg, &nc, sizeof(nc))!=0)
return -EFAULT;
}
  }
break;
}
return ret;
}
模块初始化函数,登记字符设备
static int __init _init(void)
{
int  result;
// 登记该字符设备,这是2.4以前的基本方法,到2.6后有了些变化,
// 是使用MKDEV和cdev_init()来进行,本文还是按老方法
result = register_chrdev(MAJOR_DEV_NUM, DEVICE_NAME, &newchar_fops);
if (result < 0) {
printk(KERN_WARNING __FUNCTION__ ": failed register character device for /dev/newchar/n");
return result;
}
return 0;
}

模块退出函数,登出字符设备
static void __exit _cleanup(void)
{
int  result;
 result = unregister_chrdev(MAJOR_DEV_NUM, DEVICE_NAME);
if (result < 0)
printk(__FUNCTION__ ": failed unregister character device for /dev/newchar/n");
 return;
}
module_init(_init);
module_exit(_cleanup);
 
5. 结论
 
用ioctl()在用户空间和内核空间传递数据是最常用方法之一,比较简单方便,而且可以在同一个ioctl中对不同的命令传送不同的数据结构,本文只是为描述方便而在不同命令中使用了相同的数据结构。

 

 
原创粉丝点击