嵌入式 字符设备驱动程序

原创 2013年12月02日 14:55:37
 

#include<</SPAN>linux/module.h>
#include moduleparam.h>
#include <</SPAN>linux/init.h>

#include <</SPAN>linux/kernel.h>
#include <</SPAN>linux/slab.h>
#include <</SPAN>linux/fs.h>
#include <</SPAN>linux/errno.h>
#include <</SPAN>linux/types.h>
#include <</SPAN>linux/cdev.h>
#include <</SPAN>asm/system.h>
#include <</SPAN>asm/uaccess.h>
#include "scull_t1.h"


int scull_major = SCULL_MAJOR;
int scull_minor = 0;
int scull_nr_devs = SCULL_NR_DEVS;
int scull_quantum = SCULL_QUANTUM;
int scull_qset = SCULL_QSET;

module_param(scull_major,int,S_IRUGO);
module_param(scull_monor,int,S_IRUGO);
module_param(scull_nr_devs,int,S_IRUGO);
module_param(scull_quantum,int,S_IRUGO);
module_param(scull_qset,int,S_IRUGO);

struct scull_dev *scull_devices;

int scull_trim(struct scull_dev*dev)
{
struct scull_qset *next,*dptr;
int qset = dev->qset;
int i;

for(dptr = dev->data;dptr;dptr= next)
{
if(dptr->data)
{
for(i = 0; i<</SPAN>qset; i++)
kfree(dptr->data[i]);

kfree(dptr->data);
dptr->data = NULL;

}
next = dptr->next;
kfree(dptr);
}
dev->size =0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}




int scull_open(struct inode*inode,structfile *filp)
{
struct scull_dev *dev;

dev = contairner_of(inode->i_cdev,struct scull_dev, cdev);
flip->private_data= dev;

if((filp->f_flags&O_ACCMODE)== O_WRONLY)

{
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
scull_trim(dev);
up(&dev->sem);
}

}

int scull_release(struct inode*inode, struct file *filp)
{
return 0;

}


struct scull_qset *scull_follow(struct scull_dev*dev,int n)
{
struct scull_qset *qs= dev->data;


if(! qs)
{
qs = dev->data= kmalloc(sizeof(struct scull_qset),GFP_KERNEL);
if(qs == NULL)
return NULL;
memset(qs,0,sizeof(struct scull_qset));
}

while(n--)
{
if(!qs->next)
{
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next== NULL)
return NULL;
memset(qs->next, 0,sizeof(struct scull_qset));
}
qs = qs->next;
continue;
return qs;
}
}



ssize_t scull_read(structfile *filp,char __user *buf,size_t count, loff_t*f_pos)
{
struct scull_dev *dev= filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum,qset= dev->qset;
int itemsize = quantum*qset;
int item,s_pos,q_pos,rest;
ssize_t retval = 0;

if(down_interruptible(&dev->sem))
return -ERESTARSYS;
if(*f_pos>= dev->size)
goto out;
if(*f_pos+count> dev->size)
count = dev->size- *f_pos;


item = (long)*f_pos/ itemsize;
rest = (long)*f_pos%itemsize;
s_pos = rest/quantum;
q_pos = rest%quantum;


dptr = scull_follow(dev,item);

if(dptr == NULL||!dptr->data||!dptr->data||!dptr->data[s_pos])
goto out;

if(count>quantum- q_pos)
count = quantum-q_pos;


if(copy_to_user(buf, dptr->data[s_pos]+q_pos,count))
{
retval = -EFUALT;
goto out;
}
*f_pos +=count;
retval = count;

out:
up(&dev->sem);
return retval;
}

ssize_t scull_write(structfile *filp,const char __user*buf, size_t count, loff_t*f_pos )
{
struct scull_dev *dev= filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum,qset= dev->qset;
int itemsize = quantum* qset;
int item,s_pos,q_pos,rest;
ssize_t retval = -ENOMEM;

if(down_interruptible(&dev->sem))
return -ERESTART;

item = (long)*f_pos/ itemsize;
rest = (long)*f_pos% itemsize;

s_pos = rest / quantum;
q_pos = rest % quantum;



dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data)
{
dptr->data = kmalloc(qset * sizeof(char*), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset* sizeof(char*));
}
if (!dptr->data[s_pos])
{
dptr->data[s_pos]= kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
if (count> quantum - q_pos)
count = quantum- q_pos;


if(copy_from_user(dptr->data[s_pos]+q_pos, buf, count))
{
retval = -EFAULT;
goto out;
}
*f_pos +=count;
retval = count;


if(dev->size<</SPAN>*f_pos)
dev->size = *f_pos;

out:
up(&dev->sem);
return retval;
}


struct file_operations scull_fops =
{
.owner = THIS_MODULE,
.read = scull_read,
.write = scull_write,
.open = scull_open,
.release = scull_release,
};


void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major,scull_minor);


if(scull_devices)
{
for(i = 0; i<</SPAN>scull_nr_devs; i++)
{
scull_trim(scull_devices +i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}
unregister_chrdev_region(devno, scull_nr_devs);
}


static void scull_setup_cdev(struct scull_dev*dev, int index)
{
int err,devno = MKDEV(scull_major,scull_minor+index);

cdev_init(&dev->cdev,&scull_fops);
dev->cdev.owner= THIS_MODULE;
dev->cdev.ops= &scull_fops;
err = cdev_add(&dev->cdev, devno,1)
if(err)
printk(KERN_NOTICE "Error %d adding scull%d",err,index);
}

int scull_init_module(void)
{
int result,i;
dev_t dev = 0;
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 <</SPAN> 0)
{
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}

scull_devices = kmalloc(scull_nr_devs*sizeof(struct scull_dev), GFP_KERNEL);
if(!scull_devices)
{
result = -ENOMEM;
goto fail;
}
memset(scull_devices,0,scull_nr_devs*sizeof(struct scull_dev));


for(i = 0; i<</SPAN>scull_nr_devs;i++)
{
scull_devices[i].quantum= scull_quantum;
scull_devices[i].qset= scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i],i);
}
return 0;
fail:
scull_cleanup_module();
return result;
}


module_init(scull_init_module);
module_exit(scull_cleanup_module);

MODULE_AUTHOR("小兽");
MODULE_LICENSE("Dual BSD/GPL");


/*N0 2
* 关于指针释放的,说实在的这里发现自己的C基础很差,完全找不到
*指针释放的概念,在这里做个笔记提醒下以后多注意下
*/
/*NO3
*分配并填写置于filp->private_data里的数据结构。
*将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用
*
*具体说法为我们不需要cdev结构体本身,但希望得到包含scull_dev结构体?
*于是将scull_dev结构体指针保存在private_data字段中,方便使用
*
*简介点说是.通过indode->i_cdev(其本身指向scull_dev的cdev)获得scull_dev的入口指针
*识别需要被打开的设备
*/


.h文件部分

#ifdef _SCULL_H_
#define _SCULL_H_



#undef PDEBUG
#ifdef SCULL_DEBUG

# ifdef __KERNEL__

# define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt,## args)
# else

# define PDEBUG(fmt, args...)fprintf(stderr, fmt,## args)
# endif
# else
# define PDEBUG(fmt, args...)
# endif

#undef PDEBUGG
#define PDEBUGG(fmt, args...)




#ifndef SCULL_MAJOR
#define SCULL_MAJOR 0
#endif
#ifndef SCULL_NR_DEVS
#define SCULL_NR_DEVS 4
#endif

#ifndef SCULL_QSET
#define SCULL_QSET 1000
#endif
#ifndef SCULL_QUANTUM
#define SCULL_QUANTUM 4000
#endif

//"scull_dev->data" points to an array of pointers, each

/
int quantum;
int qset;
unsigned long size;
unsigned int access_key;
struct semaphore sem;
struct cdev cdev;

};

extern int scull_major;
extern int scull_nr_devs;
extern int scull_quantum;
extern int scull_qset;




int scull_trim(struct scull_dev*dev);

ssize_t scull_read(structfile *filp,char __user *buf,size_t count,
loff_t *f_pos);
ssize_t scull_write(structfile *filp,const char __user*buf, size_t count,
loff_t *f_pos);



#define TYPE(minor)(((minor)>> 4)& 0xf)
#define NUM(minor)((minor)& 0xf)




#define SCULL_IOC_MAGIC'k'


#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0) reswords


#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1,int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2,int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5,int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6,int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9,int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10,int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)


#define SCULL_P_IOCTSIZE _IO(SCULL_IOC_MAGIC, 13)
#define SCULL_P_IOCQSIZE _IO(SCULL_IOC_MAGIC, 14)


#define SCULL_IOC_MAXNR 14

#endif

makefile 部分

#内核路径
KERNELDIR = /home/alu/mywork/systems/linux-2.6.22.6

# $为取当前变量 PWD为取当前路径
PWD := $(shell pwd)
#NO1 #N02
INSTALLDIR =/home/soso/ldd3/modules


CROSS_COMPLE =/usr/local/arm/crosstool/gcc-3.3.6-glibc-2.3.2/arm-linux/bin:
CC = $(CROSS_COMPLE)gcc

obj-m := scull.o
#NO3
#伪目标1
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
#N04

modules_install:
cp scull.ko $(INSTALLDIR)
clean:
rm -rf *.o *~core.depend .*.cmd*.ko *.mod.c .tmp_versions
.PHONY:modules modules_install clean
#NO5

###############################################################################
#NO1: :==的区别别
#:= 定义的变量如果值内容本身就是变量,他不会延伸。如果是=:
#会延伸。所以在使用时,不需要延伸的时候一定要加上: ,防止不可
#控的情况。
#何为延伸:
#a = orginal_value
#b = $(a)
#a = later_value
#最后a=later_value
#和下面
#a = orginal_value
#b := $(a)
#a = later_value
#最后a=original_value
###############################################################################

###############################################################################
#NO2
#shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的
#命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系
#统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字
#符串处理命令awk,sed等等命令来生成一个变量,如:
#contents := $(shell cat foo)
#files := $(shellecho *.c)
#注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行
#性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,
#那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你
#的shell函数执行的次数比你想像的多得多。
#################################################################################

#################################################################################
#NO3
#值是y (for built-in)或者m(for module)。如果既不是y也不是m,这个文件不会被编译
#或链接
#这里也是文件中的第一个目标文件(target ) :<</SPAN><</SPAN>who to write makefile>>中make是如何
#工作的 提到了这点
#我的理解是当执行make时 这条语句才是核心,他决定了最后所输出的顶层
#最后发现这样理解是错误的,因为没理解好modules这个伪目标所指内容的
#性能做完module分析后再联系起来总结下。
#################################################################################

#################################################################################
#NO4 $(MAKE)
#四、嵌套执行make
#在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在
#不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有
#利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一
#个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和
#分段编译有着非常大的好处。
#例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了
#这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:
#subsystem:
#cd subdir && $(MAKE)
#其等价于:
#subsystem:
#$(MAKE) -C subdir
#定义$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成
#一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然
#后执行make命令。
#我们把这个Makefile叫做“总控Makefile”,总控Makefile的变量可以传递到下级的Makefile中
#(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定
#了“-e”参数。
#摘自<</SPAN><</SPAN>跟我一起写 Makefile>>
#记起了以前学写驱动的时候,我们的编译过程是这样的:在内核(KERNELDIR)目录的
#drivers/char的Makefile下增加obj-m += scull.o 在那个目录下make modules就会在那个目录下生成
#scull.ko文件然后移到开发板进行加载卸载
#而在这里这个makefile相当于远程控制(KERNELDIR)/Makefile 做出相同的操作即总控makefile
#######################################################################################

#######################################################################################
#NO5 .PHONY
#这个是伪目标声明标志 具体知识请自己百度
###################################################################################

###################################################################################
#未解决知识点1:
#clean: 这个伪目标具体跟的文件不怎么知道写我想可能要根据编译过程最后生成的
#文件书写 ,应该是看最后都生成了些什么东西,然后写上去。
#但都只是猜想,等下实践后补上
###################################################################################

 
 
笔记以Linux DeviceDriver3提供的scull程序(scull目录中的main.c和scull.h)为记录主线,并以该驱动程序中的各种系统调用和函数调用流程为记录顺序。比如,module_init()和module_exit()为相对应的一对系统调用,一般书籍中都会放在一起讨论,但是本笔记却不会这样,而是在需要调用的时候才会涉及,因此module_init()会放在笔记开始时,也就是刚加载module时讨论,而module_exit()则会放在笔记结束前,也就是要卸载module时再加以讨论。
该笔记的的目的是为了对Linux DeviceDrvier3中提到的各个知识点作一下整理,理清一下头绪,从而能让我对Linux驱动程序加深整体或者全局上的理解。

注:个人理解,有误难免!——笔者:曹荣荣

*******************************************


驱动程序module的工作流程主要分为四个部分:
1、 用Linux提供的命令加载驱动module
2、 驱动module的初始化(初始化结束后即进入“潜伏”状态,直到有系统调用)
3、 当操作设备时,即有系统调用时,调用驱动module提供的各个服务函数
4、 卸载驱动module

一、驱动程序的加载

Linux驱动程序分为两种形式:一种是直接编译进内核,另一种是编译成module形式,然后在需要该驱动module时手动加载。对于前者,还有待学习。
Module形式的驱动,Linux提供了两个命令用来加载:modprobe和insmod。
其中modprobe可以解决驱动module的依赖性,即假如正加载的驱动module若引用了其他module提供的内核符号或者其他资源,则modprobe就会自动加载那些module,不过,使用modprobe时,必须把要加载的驱动module放在当前模块搜索路径中。而insmod命令不会考虑驱动module的依赖性,但是它却可以加载任意目录下的驱动module。
一般来说,在驱动开发阶段,使用/sbin/insmod比较方便,因为不用将module放入当前module搜索路径中。
一旦使用insmod加载模块,则Linux内核就会调用module_init(scull_init_module)特殊宏,其中scull_init_module是驱动初始化函数,可自定义名称。
在用insmod加载module时,还可以给module提供模块参数,但是这需要在驱动源代码中加入几条语句,让模块参数对insmod和驱动程序可见,如:
static char *whom=”world”;
static int howmany=10;
module_param(howmany,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);
这样,当使用/sbin/insmod scull.ko whom=”string”howmany=20这样的命令加载驱动时,whom和howmay的值就会传入scull驱动模块了。

驱动程序module被加载后,若对设备进行操作(如open,read,write等),驱动module就会调用相应的函数响应该操作。
那么,当对设备进行操作时,驱动module又怎么知道是自己应该有所响应,而不是其他的驱动module呢,也就是说,Linux内核怎么知道应该调用哪一个驱动module呢?
目前我只知道有两种方式将设备与驱动module联系在一起(也许应该说提供访问设备的一种途径比较恰当):其一是通过某些设备的ID(比如PCI设备和USB设备的DeviceID和ProductID),Linux内核根据这些ID调用驱动module;其二是在/dev目录下根据设备的主次设备号创建对应的设备节点(即设备文件),这样当操作/dev目录下的设备文件时,就会调用相应的驱动module。

二、驱动module的初始化

使用insmod加载驱动module时,需要让驱动module为设备做一些初始化动作,主要目的是让Linux内核知道这个设备(或者说module?),以及在以后对该设备进行操作(如open,read,write等等)时,让Linux内核知道,本module拥有哪些函数可以服务于系统调用。
因此,scull_init_module函数中主要做了以下几件事情:
a) 分配并注册主设备号和次设备号
b) 初始化代表设备的struct结构体:scull_dev
c) 初始化互斥体init_MUTEX(本笔记不整理)
d) 初始化在内核中代表设备的cdev结构体,最主要是将该设备与file_operations结构体联系起来。

1、分配并注册主次设备号
设备号是在驱动module中分配并注册的,也就是说,驱动module拥有这个设备号(我的理解),而/dev目录下的设备文件却是根据这个设备号创建的,因此,当访问/dev目录下的设备文件时,驱动module就知道,自己该出场服务了(当然是由内核通知)。

在Linux内核看来,主设备号标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备(也就是/dev下的设备文件)服务;而次设备号则用来标识具体且唯一的某个设备。

在内核中,用dev_t类型(其实就是一个32位的无符号整数)的变量来保存设备的主次设备号,其中高12位表示主设备号,弟20位表示次设备号。
设备获得主次设备号有两种方式:一种是手动给定一个32位数,并将它与设备联系起来(即用某个函数注册);另一种是调用系统函数给设备动态分配一个主次设备号。

对于手动给定一个主次设备号,使用以下函数:
int register_chrdev_region(dev_t first, unsigned int count, char*name)
其中first是我们手动给定的设备号,count是所请求的连续设备号的个数,而name是和该设备号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
比如,若first为0x3FFFF0,count为0x5,那么该函数就会为5个设备注册设备号,分别是0x3FFFF0,0x3FFFF1,0x3FFFF2,0x3FFFF3,0x3FFFF4,其中0x3(高12位)为这5个设备所共有的主设备号(也就是说这5个设备都使用同一个驱动程序)。而0xFFFF0,0xFFFF1,0xFFFF2,0xFFFF3,0xFFFF4就分别是这5个设备的次设备号了。
需要注意的是,若count的值太大了,那么所请求的设备号范围可能会和下一个主设备号重叠。比如若first还是为0x3FFFF0,而count为0x11,那么first+count=0x400001,也就是说为最后两个设备分配的主设备号已经不是0x3,而是0x4了!
用这种方法注册设备号有一个缺点,那就是若该驱动module被其他人广泛使用,那么无法保证注册的设备号是其他人的Linux系统中未分配使用的设备号。

对于动态分配设备号,使用以下函数:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name)
该函数需要传递给它指定的第一个次设备号firstminor(一般为0)和要分配的设备数count,以及设备名,调用该函数后自动分配得到的设备号保存在dev中。
动态分配设备号可以避免手动指定设备号时带来的缺点,但是它却也有自己的缺点,那就是无法预先在/dev下创建设备节点,因为动态分配设备号不能保证在每次加载驱动module时始终一致(其实若在两次加载同一个驱动module之间并没有加载其他的module,那么自动分配的设备号还是一致的,因为内核分配设备号并不是随机的,但是书上说某些内核开发人员预示不久的将来会用随机方式进行处理),不过,这个缺点可以避免,因为在加载驱动module后,我们可以读取/proc/devices文件以获得Linux内核分配给该设备的主设备号。
Linux DeviceDriver3提供了一个脚本scull_load和scull_unload,可以在动态分配的情况下为设备创建和删除设备节点。其实它也是利用了awk工具从/proc/devices中获取了信息,然后才用mknod在/dev下创建设备节点。
其实scull_load和scull_unload脚本同样可以适用于其他驱动程序,只要重新定义变量并调整mknod那几行语句就可以了。

与主次设备号相关的3个宏:
MAJOR(dev_t dev):根据设备号dev获得主设备号;
MINOR(dev_t dev):根据设备号dev获得次设备号;
MKDEV(int major, int minor):根据主设备号major和次设备号minor构建设备号。

2、初始化代表设备的scull_dev结构体
scull源代码中定义了一个scull_dev结构体,包括qset,qutuam,信号量sem以及cdev等字段。其中qset和qutuam的初始化对于Linux驱动程序的知识点来说毫不相关,因此不加讨论。
我只要知道,在加载module时所调用的module初始化函数中,可以初始化一些设备相关的变量。但是根据Linux DeviceDrvier3作者的意思,设备相关的变量或者一些资源最好应当在open函数中初始化,比如像中断号等,虽然在module初始化函数中注册也是允许的,但最好是在第一次打开设备,也就是open函数中再行分配。

3、初始化互斥体init_MUTEX
互斥体MUTEX,也就是信号量的一个变种,与completion,自旋锁spinlock等等都与驱动中的并发和竞态相关,以后再说。

4、初始化在内核中代表设备的cdev结构体
其实在Linux内核中,cdev结构体才是真正代表了某个设备。在内核调用设备的open,read等操作之前,必须先分配并注册一个或者多个cdev结构。
我想可以这么理解,主次设备号是涉外的,主要用来在与外部(指的是驱动module和Linux内核以外)交互时确定身份;而cdev结构体则是涉内的,当需要在module内部,或者与Linux内核之间传递一些变量,指针,buffer等东东,或者要调用驱动module中的某个服务函数时,就要用到cdev结构体了。

在scull函数中,cdev结构体的分配,注册与初始化使用了一个自定义的scull_setup_cdev函数,在该函数中,主要由以下4条语句对cdev进行初始化:
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
(dev变量是scull程序定义的代表设备的一个结构体,它包含了cdev结构体,对于dev来说,cdev结构体应该说就是它的核心)

第一条语句是初始化cdev结构体,比如为cdev结构体分配内存,为cdev结构体指定file_operations等等,而第三条语句的作用初看起来似乎与第一条有所重复。但scull程序中既然这么写想必就有它的用意,也许需要看Linux内核源代码才能搞明白,但目前我是这么理解的:第一条语句中有关file_operations的部分是为了告诉Linux内核,该cdev结构体相关的file_operations是scull_fops;而第二条语句则是真正为cdev指定了它的file_operations字段就是scull_fops。
scull_fops是file_operations类型的变量,file_operations也是一个结构体,而且是Linux驱动程序中很重要的一个结构体,在scull程序中,定义如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
以上定义中,第一条.owner字段说明本file_operations结构体的拥有者是本驱动module,而接下来的几个字段则是告诉驱动module,当有相应的系统调用到达该module时,module应该调用哪一个函数来为该系统调用服务。比如说,若有一个open系统调用到达module,则module通过查询file_operations结构体就知道了,与open系统调用相关的是scull_open函数,于是module就调用scull_open函数来为open系统调用服务了。其他几个字段也完全类似。
当然,Linux内核定义的file_operations结构体还包括其他一些字段,比如异步读写等等,但还是等用到的时候再说吧。

对cdev初始化的第二条语句是dev->cdev.owner= THIS_MODULE,这条语句就是说正在初始化的cdev结构体的拥有者就是本module

对cdev初始化的最后一条语句是err =cdev_add (&dev->cdev, devno,1),该语句的目的就是告诉内核,该cdev结构体信息。因为cdev_add函数有可能调用失败,所以需要检测该函数调用的返回值。而一旦cdev_add调用成功返回,那么我们的设备就“活”了!也就是说,外部应用程序对它的操作就会被内核允许且调用。因此在驱动程序还没有完全准备好处理设备上的操作时,就绝不能调用cdev_add。

三、设备操作

驱动module因为由insmod的加载而进行了初始化之后,就会进入“潜伏”状态,也就是说,如果没有系统调用(如open,read等),那么module中定义的其他函数就绝不会运行!
这里所说的设备操作,是指当有系统调用到达驱动module时,module就该调用某个或某些函数有所动作。
对于驱动开发来说,我主要关心的只有一点,那就是系统调用怎样把一些外部应用程序中的变量值传递给驱动module。
scull程序中与设备操作相关的函数主要分为三类:初始化函数,实际的操作服务函数和清理函数。其中初始化函数只有一个,就是open函数,而操作服务函数则包括read,write,llseek等等函数,至于清理函数则是release函数。

1、open函数
open函数提供给驱动程序以初始化的能力,从而为以后的操作做准备。
说起来在用insmod加载驱动后也有一个初始化动作,但那个初始化是相对于整个Linux内核,或者说是针对整个module在涉外时的全局意义上的初始化;而open函数的初始化则是相对于设备操作来说的,是属于驱动内部的初始化,比如为以后read操作时用到的某个变量(如file结构体)作一下初始化,再比如初始化一下设备,清空一下buffer等等。
在大部分的驱动程序中,open应该完成如下工作:
a、 确定要打开的具体设备
b、 检查设备特定的错误(诸如设备未就绪或类似的硬件问题)
c、 如果设备是首次打开,则对其作一下初始化
d、 如有必要,更新f_op指针
e、 分配并填写置于filp->private_data里的数据结构

open函数的原型如下(指的是在file_operations结构体中的定义):
int (*open)(struct inode *inode, struct file *filp)
在驱动开发时要做的,就是为该函数作具体实现,当然对open函数的名称可以自定义,只要在填写file_operations结构体中的open字段时,将自定义的open函数名称填上就可以了。在scull程序中,用的就是scull_open函数名。

在open函数原型中,有inode和filp两个参数,都是外部应用程序在操作设备时通过调用系统调用传递给驱动module的。于是驱动module就可以通过这两个参数来确定要打开的具体设备了。其实这里所说的具体设备,并不是说驱动module需要从系统安装的所有设备中确定它所要服务的设备,而是指module需要从某一类拥有相同主设备号的设备中确定它要服务的设备。
之所以这么说,是因为驱动module是对应于某一个主设备号的所有设备的。换一句话说,就是Linux内核只管设备的主设备号,而不理会设备的次设备号是什么,如果有两个,三个或者更多个设备拥有同一个主设备号,那么不管外部应用程序要操作这些设备中的哪一个,Linux内核都只会调用同一个驱动module。但是驱动module却不能不管次设备号了,因为它是跟某一个具体的设备打交道的,所以它需要根据open系统调用时传递给它的参数中找到次设备号,从而确定那唯一的一个设备(也许驱动module也可以同时操作几个设备,但一时也想不起来)。
但是上面所说的通过次设备号找到具体的设备,只是其中一种方法;另外还有一种方法就是通过cdev结构体确定某个具体设备。
设备所拥有的cdev结构体,或者次设备号,都保存在open函数的inode参数中。我们可以使用container_of宏通过inode所拥有的cdev确定具体设备,也可以使用iminor宏从inode所拥有的i_rdev确定次设备号(i_rdev是inode结构体中的一个dev_t类型的变量,其中保存了真正的设备主次编号)。

对于open函数中的file参数,scull程序主要用它来做两件事:其一是将根据cdev获得的代表设备的scull_dev结构体保存到file->private_data中,这样就可以方便今后对该设备结构体的访问了,而不用每次都调用container_of宏或者iminor宏来找到设备结构体了;其二是根据file结构体中的f_flags字段来确定,这次的open调用,是以写方式打开设备,还是以读方式来打开设备。

2、read函数
驱动module的file_operations结构体中可以定义很多设备操作服务函数,但是我现在关心的是这些函数怎样与系统调用,或者说是外部应用程序交互,而不管具体的设备操作怎么实现,所以只记录read函数作为代表。

read函数的原型如下:
ssize_t read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)

read函数原型中有4个参数,分别是filp,buf,count和f_pos。
其中file结构体指针参数可以用来确定我们要操作的设备,因为在open函数中,我们将代表设备的结构体保存到了filp的private_data字段中。
buf参数是一个指向用户空间的buffer的指针(buf前面的__user表示用户空间),对于read来说,就是可以把要传送给外部应用程序的数据放入这个buffer中。当然,我们不能简单地将数据copy到这个buffer中,而是要使用Linux内核提供的一些函数,比如copy_to_user函数。
count是请求传送的数据长度。
f_pos是一个指向“long offset type”对象的指针,指明外部应用程序在文件中进行存取操作的位置。
read函数的返回值是有符号整数类型的指,一般是read操作的实际存取数。

3、release函数
release函数的作用正好与open相反,有时候release函数的实现被称为device_close,而不是device_release。但无论那种形式,这个设备方法都应该完成如下任务:
a、释放open分配的,保存在file->private_data中的所有内容。
b、在最后一次关闭操作时关闭设备。

relese函数由close系统调用引起,但并不是每一次close系统调用都会引起release函数的调用。只有那些真正释放设备数据结构的close系统调用才会引起release函数的调用。因为Linux内核为每个file结构体维护其被引用多少次的计数器,只有当file结构体的计数器归0时,close系统调用才会引用release函数,这只在删除这个结构时才会发生,因此每次open驱动程序都只会看到一次对应的一次release调用。

四、卸载驱动module

每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。如果一个模块未定义清除函数,则内核不允许卸载该模块。
Linux驱动module的卸载可以用/sbin/rmmodscull.ko命令,这时Linux内核就会调用驱动程序中用module_exit(scull_cleanup_module)特殊宏定义的清除函数,也就是说,module_exit声明用来帮助Linux内核找到模块的清除函数,在scull程序中,清除函数就是scull_cleanup_module函数。
模块的清除函数需要撤销初始化函数注册的所有资源,并且习惯上(但不是必须的)以与初始化函数注册相反的顺序进行撤销。需要注意的是,这里指的初始化函数是指用module_init宏声明的初始化函数,而不是指open函数,与open函数对应的应当是release函数。
在scull程序中,清除函数主要做了两件事:一是free了所有为scull设备分配的内存;二是收回了初始化函数所注册的设备号。

相关文章推荐

嵌入式LAB 7:字符设备驱动程序

字符设备驱动

跟着韦东山老师学习嵌入式----字符设备驱动程序之poll机制

int poll(struct pollfd *fds,nfds_t nfds, int timeout); 总的来说,Poll机制会判断fds中的文件是否可读,如果可读则会立即返回,返回的值就...

实验题目:实现嵌入式Linux系统下的字符设备驱动程序(报告)

实验题目:实现嵌入式Linux系统下的字符设备驱动程序 实验目的:了解Linux系统下的字符设备驱动的结构理解交叉编译的相关知识理解驱动和应用程序的通信方式 实验要求:要求动态生成设备号,并在测试程序...
  • xum2008
  • xum2008
  • 2011年07月04日 22:33
  • 2280

嵌入式Linux字符设备驱动程序的主要数据结构

1)struct cdev:在内核中代表一个字符设备驱动(char device,cdev),每一个字符设备驱动都有一个struct cdev结构体变量与之对应,记录该设备驱动的相关信息,主要包括设备...
  • zbatp
  • zbatp
  • 2012年10月27日 20:53
  • 848

字符设备驱动程序的开发

  • 2013年05月08日 21:01
  • 32KB
  • 下载

简单字符设备驱动程序(一)

理论基础: 1.几个重要的结构体: (1)cedv结构体 struct cdev {      struct kobject      kobj;                      ...
  • sooolo
  • sooolo
  • 2012年03月16日 14:22
  • 695

test字符设备驱动程序

  • 2008年04月23日 11:50
  • 308KB
  • 下载

字符设备驱动程序的基本步骤

  • 2011年06月28日 16:17
  • 137KB
  • 下载

关于linux中字符设备驱动程序的设计

作为初学者自己总结了一下一个简单的linux字符设备驱动程序中需要学习的内容 一.字符设备驱动程序的组成: 1.头文件:头文件部分包含进数据结构及内核函数需要用到的头文件。 2.file_oper...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:嵌入式 字符设备驱动程序
举报原因:
原因补充:

(最多只允许输入30个字)