您的位置:首页 > 运维架构 > Linux

linux设备驱动程序——字符设备驱动程序

2009-10-27 14:43 204 查看
在Linux操作系统下有3类主要的设备文件类型:块设备、字符设备和网络设备。这种分类方法可以将控制输入/输出设备的驱动程序与其他操作系统软件分离开来。字符设备是指存取时没有缓存的设备。典型的字符设备包括鼠标、键盘、串行口等。

字符设备与块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般紧接着发生。块设备则不然,它利用一块系统内存作为缓冲区,若用户进程对设备的请求能满足用户的要求,就返回请求的数据;否则,就调用请求函数来进行实际的I/O操作。块设备主要是针对磁盘等慢速设备设计的,以免耗费过多的CPU时间用来等待。网络设备可以通过BSD套接口访问数据。

一.主设备号和次设备号

主设备号标识设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。我们可以通过次设备号获得一个指向内核设备的直接指针,也可将次设备号当作设备本地数组的索引,不管用哪种方式,除了知道次设备号用来指向驱动程序所实现的设备之外,内核本身基本上不关心关于次设备号的任何其他消息。

◎设备编号的内部表达

内核用dev_t类型(<linux/types.h>)来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20位表示次设备号。
在实际使用中,是通过<linux/kdev_t.h>中定义的宏来转换格式。
(dev_t)-->主设备号、次设备号 MAJOR(dev_t dev) MINOR(dev_t dev)
主设备号、次设备号-->(dev_t) MKDEV(int major,int minor)

◎分配和释放设备编号

建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<linux/fs.h>中声明:
int register_chrdev_region(dev_t first, unsigned int count,char *name); //指定设备编号

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name); //动态生成设备编号

void unregister_chrdev_region(dev_t first, unsigned int count); //释放设备编号

分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。

◎动态分配主设备号

某些主设备号已经静态地分配给了大部分公用设备。在内核源码树的Documentation/device.txt文件中可以找到这些设备的列表。

一旦驱动程序被广泛使用,随机选定的主设备号可能造成冲突和麻烦。

强烈推荐你不要随便选择一个一个当前不用的设备号做为主设备号,而使用动态分配机制获取你的主设备号。

动态分配的缺点是,由于分配给你的主设备号不能保证总是一样的,无法事先创建设备节点。然而这不是什么问题,这是因为一旦分配了设备号,你就可以从/proc/devices读到。为了加载一个设备驱动程序,对insmod的调用被替换为一个简单的脚本,它通过/proc/devices获得新分配的主设备号,并创建节点。

分配主设备号的最佳方式:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。

以下是在scull.c中用来获取主设备好的代码:

if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d/n", scull_major);
return result;
}
在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出在/proc/devices和sysfs中。

看到这里,就可以理解为什么mdev和udev可以动态、自动地生成当前系统需要的设备文件。udev就是通过读取sysfs下的信息来识别硬件设备的.

二、一些重要的数据结构
文件操作file_operations

<linux/fs.h>中定义file_operations。

__user表明指针是一个用户空间地址,因此不能被直接引用。

ssize_t (*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t *); 初始化设备上的异步写入操作。

unsigned int (*poll) (struct file *, struct poll_table_struct *);是poll、epoll和select这三个系统调用的后端实现。可用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。

int (*mmap) (struct file *, struct vm_area_struct *);用于请求将设备内存映射到进程地址空间。

int (*fsync) (struct file *, struct dentry *, int);用户调用它来刷新待处理的数据。

int (*fasync) (int, struct file *, int);用来通知设备其FASYNC标志发生了变化。

file结构

struct file是一个内核结构,不会出现在用户程序中

inode结构

内核用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。

dev_t i_rdev:对表示设备文件的inode结构,该字段包含了真正的设备编号。

struct cdev *i_cdev;struct cdev表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);用来从一个inode中获得主设备号和次设备号

三、字符设备的注册

内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含<linux/cdev.h>,它定义了struct cdev以及与其相关的一些辅助函数。

注册一个独立的cdev设备的基本过程如下:

1、为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)

struct cdev *my_cdev = cdev_alloc();

2、初始化struct cdev

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3、初始化cdev.owner

cdev.owner = THIS_MODULE;

4、cdev设置完成,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

从系统中移除一个字符设备:void cdev_del(struct cdev *p)

附:

概括的说,字符设备驱动主要要做三件事:

1、定义一个结构体static struct file_operations变量,其内定义一些设备的打开、关闭、读、写、控制函数;

2、在结构体外分别实现结构体中定义的这些函数;

3、向内核中注册或删除驱动模块。

字符设备提供给应用程序流控制接口有:open/close/read/write/ioctl,添加一个字符设备驱动程序,实际上是给上述操作添加对应的代码,Linux对这些操作统一做了抽象 struct file_operations

file_operations结构体的例子如下

static struct file_operations myDriver_fops = {

owner: THIS_MODULE,

write: myDriver_write,

read: myDriver_read,

ioctl: myDriver_ioctl,

open: myDriver_open,

release: myDriver_release,

};

该结构体规定了驱动程序向应用程序提供的操作接口:

实现write操作

从应用程序接收数据送到硬件。例:

static ssize_t myDriver_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)

{

size_t fill_size = count;

PRINTK("myDriver write called!/n");

PRINTK("/tcount=%d, pos=%d/n", count, (int)*f_pos);

if(*f_pos >= sizeof(myDriver_Buffer))

{

PRINTK("[myDriver write]Buffer Overlap/n");

*f_pos = sizeof(myDriver_Buffer);

return 0;

}

if((count + *f_pos) > sizeof(myDriver_Buffer))

{

PRINTK("count + f_pos > sizeof buffer/n");

fill_size = sizeof(myDriver_Buffer) - *f_pos;

}

copy_from_user(&myDriver_Buffer[*f_pos], buf, fill_size);

*f_pos += fill_size;

return fill_size;

}

其中的关键函数

u_long copy_from_user(void *to, const void *from, u_long len);

把用户态的数据拷到内核态,实现数据的传送。

实现read操作

从硬件读取数据并交给应用程序。例:

static ssize_t myDriver_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)

{

size_t read_size = count;

PRINTK("myDriver read called!/n");

PRINTK("/tcount=%d, pos=%d/n", count, (int)*f_pos);

if(*f_pos >= sizeof(myDriver_Buffer))

{

PRINTK("[myDriver read]Buffer Overlap/n");

*f_pos = sizeof(myDriver_Buffer);

return 0;

}

if((count + *f_pos) > sizeof(myDriver_Buffer))

{

PRINTK("count + f_pos > sizeof buffer/n");

read_size = sizeof(myDriver_Buffer) - *f_pos;

}

copy_to_user(buf, &myDriver_Buffer[*f_pos], read_size);

*f_pos += read_size;

return read_size;

}

其中的关键函数

u_long copy_to_user(void * to, const void *from, u_long len);

该函数实现把内核态的数据拷到用户态下。

实现ioctl操作

为应用程序提供对硬件行为的控制。例:

static int myDriver_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)

{

PRINTK("myDriver ioctl called(%d)!/n", cmd);

if(_IOC_TYPE(cmd) != TSTDRV_MAGIC)

{

return -ENOTTY;

}

if(_IOC_NR(cmd) >= TSTDRV_MAXNR)

{

return -ENOTTY;

}

switch(cmd)

{

case MYDRV_IOCTL0:

PRINTK("IOCTRL 0 called(0x%lx)!/n", arg);

break;

case MYDRV_IOCTL1:

PRINTK("IOCTRL 1 called(0x%lx)!/n", arg);

break;

case MYDRV_IOCTL2:

PRINTK("IOCTRL 2 called(0x%lx)!/n", arg);

break;

case MYDRV_IOCTL3:

PRINTK("IOCTRL 3 called(0x%lx)!/n", arg);

break;

}

return 0;

}

实现open操作

当应用程序打开设备时对设备进行初始化,使用MOD_INC_USE_COUNT增加驱动程序的使用次数。例:

static int myDriver_open(struct inode *inode, struct file *filp)

{

MOD_INC_USE_COUNT;

PRINTK("myDriver open called!/n");

return 0;

}

实现release操作

当应用程序关闭设备时处理设备的关闭操作。使用MOD_DEC_USE_COUNT增加驱动程序的使用次数。例:

static int myDriver_release(struct inode *inode, struct file *filp)

{

MOD_DEC_USE_COUNT;

PRINTK("myDriver release called!/n");

return 0;

}

驱动程序初始化函数

Linux在加载内核模块时会调用初始化函数,初始化驱动程序本身使用register_chrdev向内核注册驱动程序,该函数的第三个指向包含有驱动程序接口函数信息的file_operations结构体。

例:

/* Module Init & Exit function */

#ifdef CONFIG_DEVFS_FS

devfs_handle_t devfs_myDriver_dir;

devfs_handle_t devfs_myDriver_raw;

#endif

static int __init myModule_init(void)

{

/* Module init code */

PRINTK("myModule_init/n");

/* Driver register */

myDriver_Major = register_chrdev(0, DRIVER_NAME, &myDriver_fops);

if(myDriver_Major < 0)

{

PRINTK("register char device fail!/n");

return myDriver_Major;

}

PRINTK("register myDriver OK! Major = %d/n", myDriver_Major);

#ifdef CONFIG_DEVFS_FS

devfs_myDriver_dir = devfs_mk_dir(NULL, "myDriver", NULL);

devfs_myDriver_raw = devfs_register(devfs_myDriver_dir, "raw0", DEVFS_FL_DEFAULT, myDriver_Major, 0, S_IFCHR | S_IRUSR | S_IWUSR, &myDriver_fops, NULL);

PRINTK("add dev file to devfs OK!/n");

#endif

return 0;

}

其中,关键函数

module_init()

向内核声明当前模块的初始化函数

驱动程序退出函数

Linux在卸载内核模块时会调用退出函数释放驱动程序使用的资源,使用unregister_chrdev从内核中卸载驱动程序。将驱动程序模块注册到内核,内核需要知道模块的初始化函数和退出函数,才能将模块放入自己的管理队列中。

例:

static void __exit myModule_exit(void)

{

/* Module exit code */

PRINTK("myModule_exit/n");

/* Driver unregister */

if(myDriver_Major > 0)

{

#ifdef CONFIG_DEVFS_FS

devfs_unregister(devfs_myDriver_raw);

devfs_unregister(devfs_myDriver_dir);

#endif

unregister_chrdev(myDriver_Major, DRIVER_NAME);

}

return;

}

其中,关键函数

module_exit()

向内核声明当前模块的退出函数。

关于devfs的操作

在devfs中建立一个目录(/dev下)

devfs_handle_t devfs_mk_dir (devfs_handle_t dir, const char *name, void *info);

在devfs中注册一个设备文件节点

devfs_handle_t devfs_register(devfs_handle_t dir, const char *name,

unsigned int flags,

unsigned int major, unsigned int minor,

umode_t mode, void *ops, void *info);

在devfs中删除一个设备文件节点

void devfs_unregister(devfs_handle_t de);

加载驱动程序

在终端下,输入以下命令可以对模块进行相关的操作。

insmod 内核模块文件名

rmmod 内核模块文件名

lsmod 列举出当前全部的加载模块文件名

建立设备文件

mknod 文件路径 c [主设备号] [从设备号]

应用程序接口函数

可以使用标准C的文件操作函数来完成。

应用程序接口函数:

int open(const char *path, int oflag,…);

打开名为path的文件或设备

成功打开后返回文件句柄

常用oflag:O_RDONLY, O_WRONLY, O_RDWR

int close(int fd);

关闭之前被打开的文件或设备

成功关闭返回0,否则返回错误代号

ssize_t read(int fd, void *buffer, size_t count);

从已经打开的文件或设备中读取数据

buffer表示应用程序缓冲区

count表示应用程序希望读取的数据长度

成功读取后返回读取的字节数,否则返回-1

ssize_t write(int fd, void *buffer, size_t count);

向已经打开的文件或设备中写入数据

buffer表示应用程序缓冲区

count表示应用程序希望写入的数据长度

成功写入后返回写入的字节数,否则返回-1

int ioctl(int fd, unsigned long int cmd,…);

向驱动程序发送控制命令

cmd需是唯一值

type:又称幻数,8bit,一般表示cmd所属模块

number:cmd序号,8bit,一般表示实际的命令

direction:数据传输方向,2bit

size:数据大小,位数与体系结构有关(ARM:12bit)

sizeof(int)-(sizeof(type)+sizeof(number)+sizeof(direction))

使用_IO宏可快速合成cmd:_IO(MAGIC, num)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: