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

LINUX—字符设备驱动之-globalmem

2011-02-25 12:11 483 查看
下面将以linux设备驱动开发详解上的globalmem设备驱动为例来详细分析字符设备驱动的过程。

#include
<linux/module.h>//模块所需的大量符号和函数定义
#include
<linux/types.h>
#include
<linux/fs.h>//文件系统相关的函数和头文件
#include
<linux/errno.h>
#include
<linux/mm.h>
#include
<linux/sched.h>//包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明
#include
<linux/init.h>//指定初始化和清除函数
#include
<linux/cdev.h>//cdev结构的头文件 包含<linux/kdev_t.h>
#include
<asm/io.h>
#include
<asm/system.h>
#include
<asm/uaccess.h>//在内核和用户空间中移动数据的函数

#define GLOBALMEM_SIZE 0x1000
/*全局内存最大4K字节*/
#define MEM_CLEAR 0x1 /*清0全局内存*/
#define GLOBALMEM_MAJOR 254 /*预设的globalmem的主设备号*/

static int globalmem_major =
GLOBALMEM_MAJOR;
/*globalmem设备结构体*/
struct globalmem_dev

{

struct cdev cdev; /*cdev结构体*/

unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/
};

struct globalmem_dev *globalmem_devp;
/*设备结构体指针*/
/*文件打开函数*/
int globalmem_open(struct inode *inode,
struct file *filp)
{

/*将设备结构体指针赋值给文件私有数据指针.

系统在调用驱动程序的open方法前将这个指针置为NULL。

驱动程序可以将这个字段用于任意目的,也可以忽略这个字段。

驱动程序可以用这个字段指向已分配的数据,

但是一定要在内核释放file结构前的release方法中清除它*/

filp->private_data = globalmem_devp;

//将文件私有数据private_data指向设备结构体,read,write,ioctl,llseek等函数

//通过private_data访问设备结构体

return 0;
}
/*文件释放函数*/
int globalmem_release(struct inode
*inode, struct file *filp)
{

return 0;
}
/*
备方法应当进行下面的任务:
1、释放
open 分配在
filp->private_data 中的任何东西
2、在最后的
close 关闭设备globalmem_release的基本形式没有硬件去关闭,
因此需要的代码是最少的.不是每个
close 系统调用引起调用
release 方法.
只有真正释放设备数据结构的调用会调用这个方法
*/
/* ioctl设备控制函数
*/
static int globalmem_ioctl(struct inode
*inodep, struct file *filp, unsigned

int cmd, unsigned long arg)
{

struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/

switch (cmd)

{

case MEM_CLEAR://清除全局内存

memset(dev->mem, 0, GLOBALMEM_SIZE);

printk(KERN_INFO "globalmem is set to
zero/n");

break;

default://其他不支持的命令

return -
EINVAL;

}

return 0;
}

/*读函数*/
static ssize_t globalmem_read(struct
file *filp, char __user *buf, size_t size,

loff_t *ppos)
{

unsigned long p =
*ppos;

unsigned int count = size;

int ret = 0;

struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

/*分析和获取有效的写长度*/

if (p >= GLOBALMEM_SIZE)//0x1000 要读的偏移位置越界

return count ? - ENXIO:
0;

if (count > GLOBALMEM_SIZE - p)//要读的字节数太大

count = GLOBALMEM_SIZE - p;

/*内核空间->用户空间*/

//将内核空间的内容复制到用户空间,所复制的内容是从from来,到to去,复制n个字节。

if (copy_to_user(buf, (void*)(dev->mem + p),
count))

{

ret = -
EFAULT;

}

else

{

*ppos += count;

ret =
count;

printk(KERN_INFO "read %d bytes(s) from %d/n", count,
p);

}

return ret;
}

/*写函数*/
static ssize_t globalmem_write(struct
file *filp, const char __user *buf,

size_t size, loff_t *ppos)
/*
filp
是文件指针,count 是请求的传输数据长度,buff 参数是指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入数据的空缓冲区,该地址在内核空间不能直接读写,offp 是一个指针指向一个"long offset type"对象,
它指出用户正在存取的文件位置.
返回值是一个"signed size type。写的位置相对于文件开头的偏移。
*/
{

unsigned long p =
*ppos;

unsigned int count = size;

int ret = 0;

struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

/*分析和获取有效的写长度*/

if (p >= GLOBALMEM_SIZE)//0x1000 要写的偏移位置越界

return count ? - ENXIO: 0; //???

if (count > GLOBALMEM_SIZE - p)//将数据写入该缓存区,直到结尾。这个是判断要写的字节数太多

count = GLOBALMEM_SIZE - p;

/*用户空间->内核空间*/

if (copy_from_user(dev->mem + p, buf,
count))
//目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0

ret = -
EFAULT;

else

{

*ppos += count;

ret = count;

printk(KERN_INFO "written %d bytes(s) from %d/n", count,
p);

}

return ret;//如果ret=count,则完成了所请求数目的字节传送。
}

/* seek文件定位函数
*/
/*
seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),globalmem支持从文件开头和当前位置相对偏移。在定位的时候,应该检查用户请求的合法性,若不合法,函数返回-
EINVAL,合法时返回文件的当前位置*/
static loff_t globalmem_llseek(struct
file *filp, loff_t offset, int orig)
{

loff_t ret = 0;

switch (orig)

{

case 0: /*相对文件开始位置偏移*/

if (offset < 0)

{

ret = -
EINVAL;

break;

}

if ((unsigned int)offset > GLOBALMEM_SIZE)//偏移越界

{

ret = -
EINVAL;

break;

}

filp->f_pos = (unsigned int)offset;//struct
file

ret = filp->f_pos;

break;

case 1: /*相对文件当前位置偏移*/

if ((filp->f_pos + offset) > GLOBALMEM_SIZE)//偏移越界

{

ret = -
EINVAL;

break;

}

if ((filp->f_pos + offset)
< 0)

{

ret = -
EINVAL;

break;

}

filp->f_pos += offset;

ret = filp->f_pos;

break;

default:

ret = -
EINVAL;

break;

}

return ret;//合法时返回文件的当前位置
}

/*文件操作结构体*/
static const struct file_operations
globalmem_fops =
{

.owner = THIS_MODULE,

.llseek = globalmem_llseek,

.read = globalmem_read,

.write = globalmem_write,

.ioctl = globalmem_ioctl,

.open = globalmem_open,

.release = globalmem_release,
};

/*初始化并注册cdev
globalmem_setup_cdev(globalmem_devp,
0);*/
static void globalmem_setup_cdev(struct
globalmem_dev *dev, int index)
{

int err, devno = MKDEV(globalmem_major,
index);

cdev_init(&dev->cdev,
&globalmem_fops);

/*将 cdev
结构嵌入一个你自己的设备特定的结构,你应当初始化你已经分配的结构使用以上函数,有一个其他的 struct cdev
成员你需要初始化.
象 file_operations
结构,struct cdev
有一个拥有者成员,应当设置为
THIS_MODULE,一旦 cdev
结构建立,
最后的步骤是把它告诉内核,
调用:

cdev_add(&dev->cdev, devno, 1);*/

/*void cdev_init(struct cdev *cdev, const struct file_operations
*fops)
{

memset(cdev, 0, sizeof *cdev);

INIT_LIST_HEAD(&cdev->list);

cdev->kobj.ktype =
&ktype_cdev_default;

kobject_init(&cdev->kobj);

cdev->ops = fops;
}
*/
/*
struct cdev
{

struct kobject kobj;

struct module *owner;

const struct file_operations *ops;

struct list_head list;

dev_t dev;

unsigned int count;
};
/*globalmem设备结构体*/
struct globalmem_dev

{

struct cdev cdev; /*cdev结构体*/

unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/
};
*/

dev->cdev.owner = THIS_MODULE;

dev->cdev.ops = &globalmem_fops;

err = cdev_add(&dev->cdev, devno, 1);
/*
cdev

cdev 结构, devno
是这个设备响应的第一个设备号, count
是应当关联到设备的设备号的数目.
常常 count
是 1,
但是有多个设备号对应于一个特定的设备的情形.
例如,
设想 SCSI
磁带驱动,
它允许用户空间来选择操作模式(例如密度),
通过安排多个次编号给每一个物理设备.在使用 cdev_add
是有几个重要事情要记住.
第一个是这个调用可能失败.
如果它返回一个负的错误码,

你的设备没有增加到系统中.
它几乎会一直成功,
但是,
并且带起了其他的点: cdev_add
一返回,
你的设备就是"活的"并且内核可以调用它的操作.
除非你的驱动完全准备好处理设备上的操作,
你不应当调用 cdev_add */

if (err)

printk(KERN_NOTICE "Error %d adding LED%d", err,
index);
}

/*设备驱动模块加载函数*/
/*_init():__init是一个宏,编译时会用到。对于静态编译在内核的 模块有意义.
内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__init、__devinit等。
这些宏在include/linux/init.h头文件中定义。编译器通过这些宏可以把代码优化放到合适的内存位置,
以减少内存占用和提高内核效率。
init():标记内核启动时使用的初始化代码,内核启动完成后不再需要。以此标记的代码位于.init.text内存区域。
#define _ _init _ _attribute_ _ ((_ _section_ _
(".text.init")))
初始化代码的特点是:在系统启动运行,且一旦运行后马上退出内存,不再占用内存。
*/
int
globalmem_init(void)
{

int result;

dev_t devno = MKDEV(globalmem_major, 0);

/*通过主次设备号生成dev_t.
主次设备号的数据类型是dev_t,在/linux/types.h中定义。
在2.6内核中,dev_t是一个32位的数,其中12位用来表示主设备号,
其余20位用来表示次设备号。要获得设备的主次设备号可以使用内核提供的宏:
MAJOR(dev_t dev);
#获得主设备号
MINOR(dev_t dev);
#获得次设备号
这些宏定义位于linux/kdev_t.h中。如果要把主次设备号转换成dev_t类型,
则可使用:
MKDEV(int major, int
minor);*/

/* 申请设备号*/

if (globalmem_major)/*静态申请设备号*/

result = register_chrdev_region(devno, 1,
"globalmem");

/*在linux/fs.h中,

devno:是你要分配的起始设备编号.
devno的次编号部分常常是 0,
但是没有要求是那个效果;

1:是所请求的连续设备号的个数;
globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。

*/

else /* 动态申请设备号*/

{

result = alloc_chrdev_region(&devno, 0, 1,
"globalmem");

/*dev_t *dev

dev:是一个只输出的参数,
它在函数成功完成时持有你的分配范围的第一个数;

firstminor:应当是请求的第一个要用的次编号;
它常常是
0;

1: 是所请求的连续设备号的个数;

Globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。*/

globalmem_major = MAJOR(devno);/*获得主设备号,从dev_t结构中分解出主设备号*/

}
/*
如果globalmem_major=0,利用udev工具自动向系统动态申请未被占用的设备号相关函数定义在include/linux/kdev_t.h中
#define
MINORBITS
20
#define
MINORMASK ((1U << MINORBITS) -
1)
//(1<<20
-1) 此操作后,MINORMASK宏的低20位为1,高12位为0

#define
MAJOR(dev) ((unsigned int) ((dev)
>> MINORBITS))
#define
MINOR(dev) ((unsigned int) ((dev)
& MINORMASK))
#define
MKDEV(ma,mi) (((ma) << MINORBITS) |
(mi))
*/

if (result < 0)

return result;

/* 动态申请设备结构体的内存*/

globalmem_devp = kmalloc(sizeof(struct globalmem_dev),
GFP_KERNEL);

/*

在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages. 请注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!要注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址*/

if (!globalmem_devp)
/*申请失败*/

{

result = -
ENOMEM;

goto fail_malloc;

}

memset(globalmem_devp, 0, sizeof(struct
globalmem_dev));

/*memset会将参数globalmem_devp所指的内存区域中前sizeof(struct globalmem_dev)个字节以参数0填入,然后返回指向globalmem_devp的指针*/

globalmem_setup_cdev(globalmem_devp, 0);

/*初始化并注册cdev*/

return 0;

fail_malloc: unregister_chrdev_region(devno,
1);

/*释放原先申请的设备号

如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。

devno:是要分配的主设备号范围的起始值,次设备号一般设置为0;

1:是所请求的连续设备号的个数;*/

return result;
}

/*模块卸载函数*/
/*
__exit,标记退出代码,对于非模块无效。
*/
void
globalmem_exit(void)
{

cdev_del(&globalmem_devp->cdev);
/*注销cdev,为从系统去除一个字符设备*/

kfree(globalmem_devp);
/*释放设备结构体内存*/

unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);

/*释放设备号

我们一般在模块的清除函数中调用设备号释放函数。*/
}

MODULE_AUTHOR("Song
Baohua");
MODULE_LICENSE("Dual
BSD/GPL");//指定代码使用的许可证

module_param(globalmem_major, int,
S_IRUGO);
/*module_param宏的第一个参数是选项名,可在/sys虚拟文件系统中该模块的parameter目录中中查看到。第二个参数是选项类型,第三个参数是选项的值*/

module_init(globalmem_init);
module_exit(globalmem_exit);
/*向操作系统注册自己定义的这两个函数,该项目在Kconfig中配置项目为布尔型的话为Y和N两种选项,Y为编译进内核,N不编译,如果为三态型(tristate),为Y,N,M三种选项M为编译为模块,模块也可以在内核编译后根据需要进行手动加载,也可以写个脚本自动加载*/

总结:
字符设备是3大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_opersation结构体中的操作函数,实现file_opersation结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。
http://blogold.chinaunix.net/u3/104334/showart_2061894.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: