2011-7-10 15:14:00
(3)Linux PnP驱动程序
下面以串口的PnP驱动程序为例说明Linux PnP驱动程序建立步骤:
1) 首先做一个支持PnP的pnp_id结构数组。
static const struct pnp_id pnp_dev_table[] = {
/* Standard LPT Printer Port */
{.id = "PNP0400", .driver_data = 0},
/* ECP Printer Port */
{.id = "PNP0401", .driver_data = 0},
{.id = ""}
};
2) 可选地定义函数probe和函数remove 。
如果驱动程序已有一个可靠的检测资源的方法,可以不定义这些函数。下面给出定义这两个函数的例子(在drivers/serial/8250_pnp.c中):
static int serial_pnp_probe(struct pnp_dev * dev, const struct pnp_id *card_id,
const struct pnp_id *dev_id)
{
……
}
static void serial_pnp_remove(struct pnp_dev * dev)
{
……
}
consult /drivers/serial/8250_pnp.c for more information.
3) 创建一个驱动程序结构
static struct pnp_driver serial_pnp_driver = {
.name = "serial",
.card_id_table = pnp_card_table,
.id_table = pnp_dev_table,
.probe = serial_pnp_probe,
.remove = serial_pnp_remove,
};
4) 注册驱动程序
static int __init serial8250_pnp_init(void)
{
return pnp_register_driver(&serial_pnp_driver);
}
字符设备操作过程
每个字符设备使用一个字符设备结构char_device_struct来描述,所有的字符设备结构组成数组chrdevs,并且形成链表。数组chrdevs定义如下(在fs/char_dev.c中):
#define MAX_PROBE_HASH 255 /* random */
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major; //主设备号
unsigned int baseminor; //次设备号的基数
int minorct;
const char *name; //字符设备名称
struct file_operations *fops; //设备文件操作函数集
struct cdev *cdev; //字符设备驱动程序
} *chrdevs[MAX_PROBE_HASH];
字符设备操作
字符设备是Linux设备中最简单的一种。应用程序可以和存取文件相同的系统调用来打开、读写及关闭它。字符设备初始化时,
设备驱动通过模块初始函数在char_device_struct结构的chrdevs数组中添加一个结构成员,即注册到Linux核心上。设备的主设备标志符用来对此数组进行索引
(如对tty设备的索引4)。设备的主设备标志符是固定的。
chrdevs数组每个成员的char_device_struct结构包含一个指向已注册的设备驱动名称和指向一组文件操作指针,用来处理相关的文件操作如打开、读写与关闭。
从/proc/devices中看到的字符设备的内容就来自这个chrdevs数组。
当打开字符设备的设备文件时(如/dev/cua0),核心通过模块初始化已准备好调用相应设备文件的文件操作例程。与普通的目录和文件一样,每个设备文件用一
个VFS节点表示。这个VFS inode由实际的文件系统来建立(比如EXT2)。
个VFS inode和一组文件操作相关联,关联的操作函数根据inode代表的文件系统对象变化而不同。当创建一个代表字符相关文件的VFS inode时,
其文件操作被设置为缺省的字符设备操作。
字符设备只有一个文件操作:打开文件操作。当应用打开字符特殊文件时,通用文件打开操作使用设备的主标志符来索引此chrdevs数组,
以便得到那些文件操作函数指针。同时建立起描叙此字符特殊文件的file结构,使其文件操作指针指向此设备驱动中的文件操作指针集合。
这样所有应用对它进行的文件操作都被映射到此字符设备的文件操作集合上。
下面分析字符设备的初始化及注册函数(在fs/char_dev.c中):
函数chrdev_init有sysfs文件系统中初始化子系统,并给内核对象的映射结构赋上基本探测函数,函数chrdev_init分析如下:
void __init chrdev_init(void)
{
/*
* Keep cdev_subsys around because (and only because) the kobj_map code
* depends on the rwsem it contains. We don't make it public in sysfs,
* however.
*/
subsystem_init(&cdev_subsys);//初始化子系统,用于在sysfs中显示
//初始化内核对象的映射结构实例cdev_map,
//它每个字符设备初始化一个探测函数base_probe,
//函数base_probe调用用户空间的应用程序/sbin/modprobe来探测加载模块的
cdev_map = kobj_map_init(base_probe, &cdev_subsys);
}
内核对象的映射结构kobj_map封装了结构probe。它给每类设备定义了255个probe结构数组,结构kobj_map列出如下(在drivers/base/map.c中):
struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;
} *probes[255];
struct rw_semaphore *sem;
};
函数cdev_init用来初始化一个字符设备,它初始化内核对象,并把文件操作函数赋给字符设备结构。函数cdev_init列出如下(在fs/char_dev.c中):
void cdev_init(struct cdev *cdev, struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);//字符设备结构实例清0
INIT_LIST_HEAD(&cdev->list);//初始化字符设备结构的链表
cdev->kobj.ktype = &ktype_cdev_default; //赋上内核对象类型
kobject_init(&cdev->kobj); //初始化字符设备结构的内核对象
cdev->ops = fops;
}
函数register_chrdev注册一个字符设备,函数register_chrdev分析如下:
int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
char *s;
int err = -ENOMEM;
//注册设备
cd = __register_chrdev_region(major, 0, 256, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
//分配一个cdev对象空间
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
*s = '!';
//加探测函数exact_match到cdev_map中
err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, 0, 256));
return err;
}
ioctl 设备控制操作
计算机的外设除了读写操作外,还有更丰富的功能可能利用,如打印机的功能不可能仅归结于读与写这两种功能中去。
其它对设备的丰富控制功能就需要用ioctl设备控制操作函数来实现了。
ioctl控制函数的构造
ioctl函数有两个重要的参数:一个是ioctl操作命令宏定义(cmd),另一个是操作参数arg。arg是指向用户空间某一个内存区域的指针,
用来传送若干个参数。cmd命令宏定义了ioctl的操作功能,是全局惟一的标识符。
ioctl操作命令的构造必须符合POSIX标准,它的格式规定如下:
31-30 位 29-16位 15-8位 7-0 位
dir size type Nr
31位是1时,则表示ioctl执行读操作。
30位是1时,则表示ioctl执行写操作。
29-16位是所传参数结构的字节数。
15-8位表示命令类型。
7-0是命令序号。
在include/asm-i386/ioctl.h中有生成标准化的命令字和获到命令字的通用函数。这些函数的应用举例如下:
#define FDCLRPRM _IO(2, 0x41) //定义type为2,nr为0x41命令字,用于清除参数
#define FDSETPRM _IOW(2, 0x42,struct floppy_struct) //写类型,type为2,nr为0x42,参数结构字节数为sizeof(struct floppy_struct)。
. ioctl函数形式
在fs/ioctrl.c中有sys_ctrl()函数的定义,如下:
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
在系统中ioctl函数的接口参数都是一致的,参数cmd是操作命令,参数arg是操作参数,fd是打开文件的序号。
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
{
struct file * filp;
unsigned int flag;
int on, error = -EBADF;
filp = fget(fd);
if (!filp)
goto out;
error = 0;
lock_kernel();
switch (cmd) {
case FIOCLEX://文件close
set_close_on_exec(fd, 1);
break;
case FIONCLEX:
set_close_on_exec(fd, 0);
break;
case FIONBIO://文件的flag设置
if ((error = get_user(on, (int *)arg)) != 0)
break;
flag = O_NONBLOCK;
if (on)
filp->f_flags |= flag;
else
filp->f_flags &= ~flag;
break;
case FIOASYNC://文件的async()操作
if ((error = get_user(on, (int *)arg)) != 0)
break;
flag = on ? FASYNC : 0;
/* Did FASYNC state change ? */
if ((flag ^ filp->f_flags) & FASYNC) {
if (filp->f_op && filp->f_op->fasync)
error = filp->f_op->fasync(fd, filp, on);
else error = -ENOTTY;
}
if (error != 0)
break;
if (on)
filp->f_flags |= FASYNC;
else
filp->f_flags &= ~FASYNC;
break;
case FIOQSIZE:
if (S_ISDIR(filp->f_dentry->d_inode->i_mode) ||
S_ISREG(filp->f_dentry->d_inode->i_mode) ||
S_ISLNK(filp->f_dentry->d_inode->i_mode)) {
loff_t res = inode_get_bytes(filp->f_dentry->d_inode);
error = copy_to_user((loff_t *)arg, &res, sizeof(res)) ?
-EFAULT : 0;
}
else
error = -ENOTTY;
break;
default:
error = -ENOTTY;
if (S_ISREG(filp->f_dentry->d_inode->i_mode))
error = file_ioctl(filp, cmd, arg);
else if (filp->f_op && filp->f_op->ioctl)
error = filp->f_op->ioctl(filp->f_dentry->d_inode, filp, cmd, arg);
}
unlock_kernel();
fput(filp);
out:
return error;
}
ioctl的底层处理函数实现在各个具体的驱动程序中。
编写设备驱动程序基本步骤
如何添加一个字符设备
以虚拟的字符设备为例说明如何添加一个字符设备,步骤基本如下:
(1) 确定设备的设备名称和主设备号:
找一个还没有被使用的主设备号,分配给字符设备。假设主设备号为30(设备号必须唯一)。
(2) 确定编写需要的初始化函数和退出函数。
(3) 确定并编写需要的file_operations中的操作函数。
(4)修改drivers/char/Makefile;
假设我们把所以必要的函数写mychd.c中,则找到"L_OBJS := tty_io.o n_tty.o con
sole.o "行,将"mychd.o"加到其中。
(5) 将该设备私有的*.c,*.h复制到目录drivers/char下。
(6) 用命令:make clean;make dep;make zImage重新编译内核。
(7) 用mknod命令在目录/dev下建立相应主设备号的用于读写的特殊文件。
完成了上述步骤,你在linux环境下编程时就可以使用新设备了。
如何添加一个块设备
块设备的添加过程和字符设备有相似之处,步骤基本如下:
(1) 确定设备的设备名称和主设备号
找一个还没有被使用的主设备号,分配给自己的新设备。假设主设备号为30,在include/linux/major.h中加入如下句:
define MY_MAJOR 30
(2) 确定编写需要的初始化函数和退出函数。
(3) 确定编写需要的file_operations中的操作函数,由于使用了高速缓存,块设备驱动程序就不需要包含自己的read()、write()和fsync()函数,但必须使用自己的open()、 release()和 ioctl()函数。
(4) 确定编写需要的请求处理函数:
static void my_request(void)在块设备驱动程序中,不带中断服务子程序的请求处理函数是简单的,典型的格式如下:
static void my_request(void)
{
loop:
INIT_REQUEST;
if (MINOR(CURRENT->dev)>MY_MINOR_MAX)
{
end_request(0);
goto loop;
}
if (CURRENT ->cmd==READ)
//CUREENT是指向请求队列头的request结构指针
{
end_request(my_read()); //my_read()在前面已经定义
goto loop;
}
if (CURRENT ->cmd==WRITE)
{
end_request(my_write()); //my_write()在前面已经定义
goto loop;
}
end_request(0);
goto loop;
}
(5)编写中断服务子程序。
(6) 修改drivers/block/Makefile。
假设文件名为mybd.c,则找到"L_OBJS := tty_io.o n_tty.o cons
ole.o "行,将"mybd.o"加到其中。
(7)将该设备私有的*.c,*.h复制到目录drivers/block下。
(8)用命令:make clean;make dep;make zImage重新编译内核。
(9) 用mknod命令在目录/dev下建立相应主设备号的用于读写的特殊文件。