Linux字符设备驱动简介

(感谢周老师提供的资料,本文对此作了适当的修改及补充)

一.Linux设备驱动的分类

Linux设备驱动属于Linux内核模块范畴,运行在OS的内核空间,所以其生命周期受控于Linux内核。

Linux常规将设备驱动分为:字符设备、块设备和网络设备,它们区别如下表:

节点设备

ls /dev/ -l

c字符设备

因时间因素导致只能顺序访问数据的设备。

b块设备

可随机访问也可以顺序访问数据的设备。

非节点设备

ifconfig

网络设备

ethx和wlanx

面向网络报文,使用socket和sk_buff访问的设备。

 

设备号由主设备号(设备类别)和次设备(该类别下的具体设备)号组成,在整个Linux系统中设备号是唯一的。

主设备号

次设备号

12bit

20bit

 

Linux内核提供了:

Dev_t  MKDEV(int major,int minor)宏函数将输入主号和次号合成一个设备号。

int  MAJOR(dev_t dev)从设备号中获取设备主号。

int  MINOR(dev_t dev)从设备号中获取设备次号。

二.字符设备驱动的编写步

1.合成设备号

dev_t MKDEV(int major, int minor)
通过填入主设备号major和次设备号minor,返回dev_t类型的设备号

2.注册设备号

int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数:from第一个设备号(即次设备号为0的设备号)

 count申请的连续设备的数量(>=1)

 name自己定义的设备名称

返回值:0表示设备号注册成功,负数表示失败


由于每个设备的设备号唯一,在使用MKDEV手动合成的设备号有可能已经被占用了,因此若能动态的生成设备号则更加灵活,这有效地避开了设备号重复的冲突。

上述的1.合成设备号2.注册设备号可以使用以下的函数完成:

动态申请注册设备号

alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数:dev分配到的设备号,函数调用成功会把得到的设备号放入第一个参数中

 baseminor第一个次设备号(一般为0)

 count申请的连续设备的数量

 name自己定义的设备名称

在使用动态申请注册设备号的函数时,由于生成的设备的主次设备号未知,因此设备节点不能够事先手动创建,这就得添加自动创建和删除设备节点的代码了,具体函数后面会介绍。


3.初始化字符设备结构体

在Linux中使用cdev结构体表述一个字符设备,cdev结构体的定义如下

struct cdev {

struct kobject kobj;/*内嵌的kobject对象*/

struct module *owner;/*所属模块,通常为THIS_MODULE*/

struct file_operations *ops;/*文件操作结构体,为应用程序操作该模块提供了接口*/

struct list_head list;/*双向链表*/

dev_t dev;/*设备号*/

unsigned int count;/*模块使用计数,0则表示模块可被卸载*/

};

一个cdev一般有两种定义初始化方式:静态和动态。

静态内存定义初始化:cdev_init(struct cdev *cdev, struct file_operations *ops)函数用于静态初始化cdev的成员,并建立cdev和file_operations之间的连接。初始化代码如下:

struct cdev my_cdev;

cdev_init(&my_cdev,&fops);

my_cdev.owner = THIS_MODULES;

动态内存定义初始化:cdev_alloc(void)函数用于动态申请一个cdev内存。初始化代码如下:

struct cdev *my_cdev;

my_cdev->ops = &fops;

my_cdev->owner = THIS_MODULES;


两种使用方式的功能是一样的,只是使用内存区不一样,一般视实际的数据结构需求而定。下面贴出了两个函数的源代码,以具体看下它们之间的差异。

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

{

memset(cdev, 0, sizeof *cdev);

INIT_LIST_HEAD(&cdev->list);

kobject_init(&cdev->kobj, &ktype_cdev_default);

cdev->ops = fops;/*将传入的文件操作结构体指针赋值给cdev的ops*/

}


struct cdev *cdev_alloc(void)

{

struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);

if(p){

INIT_LIST_HEAD(&p->list);

kobject_init(&p->kobj, &ktype_cdev_dynamic);

}

return p;

}
由此可见,两个函数完成功能基本一致,只是cdev_init还多赋了一个cdev->ops的值。


4.向内核中添加字符设备及分配具体设备号

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

参数:p :cdev结构体

dev :字符设备号(第一个设备的设备号)

count :连续的次设备号的个数

返回值:0表示成功,非0表示失败


5.从内核中删除字符设备

void cdev_del(struct cdev *p)

参数: p :cdev结构体


6.从内核中注销设备号

void unregister_cdev_region(dev_t from, unsigned count)

参数:from :第一个设备号(即次设备号为0的设备号)

count :连续的次设备号的个数


三.应用程序接口file_operations

file_operations为指针函数集,我们暂时关注下面几个指针函数即变量成员:

owner,一般初始化为THIS_MODULE,让其归属与自己。

open(),它会自动被应用程序的open函数所调用。

release(),它会自动被应用程序的close函数所调用。

read(),从设备中读取数据。

write(),向设备中写入数据。

ioctl()(unlock_ioctl()),read和write等无法完成的东西都可以用ioctl()来完成。


3.1read()函数对copy_to_user()的使用

当用户空间(应用程序)中调用read函数时,系统会调用内核空间(模块中fops)的read函数
用户空间中:

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

返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。

内核空间中:
ssize_t (*read) (struct file *, char __user *, size_t, loff_t*);

从内核中读取数据到用户空间中。

第一个参数是文件块,

第二个参数是用户空间的数据地址。

第三个参数是

返回值为真实读到的字节数量。

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

copy_to_user函数可以实现read函数的需求。

第一参数用户空间的数据地址,

第二参数是内核空间数据的地址,

第三参数是复制的数量。

返回值为实际copy的字节数量。

下面给出一个例子:

/*

#define GLOBALMEM_SIZE 0x1000;/*全局内存最大4KB*/

struct globalmem_dev{

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

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

}

*/

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)

return 0;

if(count > GLOBALMEM_SIZE - p)

count = GLOBALMEM_SIZE - p;

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

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

ret = -EFAULT;

} else {

*ppos += count;

ret = count;

printf(KERN_INFO "read %u bytes(s) from %lu\n", count , p);

}

return ret;

}

3.2write()函数对copy_from_user()的使用

当用户空间(应用程序)中调用write函数时,系统会调用内核空间(模块中fops)的write函数

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

返回值:成功返回写入的字节数,出错返回-1并设置errno写常规文件时,write的返回值通常等于请求写的字节数

count,而向终端设备或网络写则不一定。


下面给出一个例子:

/*

#define GLOBALMEM_SIZE 0x1000;/*全局内存最大4KB*/

struct globalmem_dev{

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

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

}

*/

static ssize_t globalmem_write(struct file *filp, const 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)

return 0;

if(count > GLOBALMEM_SIZE - p)

count = GLOBALMEM_SIZE - p;

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

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

ret = -EFAULT;

} else {

*ppos += count;

ret = count;

printf(KERN_INFO "read %u bytes(s) from %lu\n", count , p);

}

return ret;

}


3.3ioctl中的cmd参数

ioctl函数中第三个整数参数作为命令参数。

设备类型(幻数)

序列号(命令代码)

方向

数据长度

8bit

8bit

2bit

13/14bit

设备类型字段为一个幻数,可以是0-0xff之间的值,内核中ioctl-number.txt给出了一些推荐的和已经被使用的幻数,新设备驱动定义幻数时要避免与其冲突。

方向字段为2位,该字段表示数据传送的方向,可能值是_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)、_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。

内核中定义了_IO()、_IOR()、_IOW()、_IOWR()这4个宏来辅助生成命令。在linux/iotcl.h中有如下定义。

命令的合成宏函数如下:

_IO(type,nr)宏函数,type为幻数,nr为命令代码,不带数据。

_IOWR(type,nr,size)宏函数,type为幻数,nr为命令吗,size为数据长度,数据双向。

_IOW(type,nr,size) 宏函数,type为幻数,nr为命令吗,size为数据长度,数据写入操作。

_IOR(type,nr,size) 宏函数,type为幻数,nr为命令吗,size为数据长度,数据读取操作。

由此可见这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)、和size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。

 

从命令获取各个参数宏函数:

_IOC_DIR(),从命令中获取数据方向参数(_IOC_NONE,_IOC_READ或_IOC_WRITE)。

_IOC_TYPE(,,从命令中获取幻数。

_IO_NR(),从命令中获取序列号(命令码)。

_IO_SIZE(),从命令中获取数据大小参数。

 

同时内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会当做预定义命令被内核处理而不是设备驱动处理,预定义有如下4种:

1.FIOCLEX:即File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。

2.FIONCLEX:即File IOctl Not Close on Exec,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。

3.FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。

4.FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在filp->f_flags中的O_NONBLOCK标志。

宏定义如下:

#define FIONCLEX0x5450

#define FIOCLEX0x5451

#define FIOQSIZE0x5460

#define FIONBIO0x5421


用户的应用程序调用ioctl函数时,会自动调用模块fops的ioctl函数。

头文件:#include<sys/ioctl.h>

int ioctl(int handle, int cmd,[int *argdx, intargcx]);

返回值:成功为0,出错为-1


示例代码如下:

static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned in cmd, unsigned long arg)

{

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

switch(cmd){

case XXX_CMD:

......

break;

......

default:

return -EINVAL;

}

return 0;

}

四.自动创建/删除设备节点

设备节点中的设备标示符和设备号是一一对应的,应用程序通过打开设备标示符,内核系统会自动找到对应的设备号,最终使得应用程序打开了对应的设备。

在刚学习linux驱动时,一般都是用mknod手动创建设备节点,往后学习就知道,实际上Linux内核为我们提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提条件是用户空间移植了udev。

在linux/device.h头文件中可知,内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create(…)函数去/sysfs下寻找对应的类从而创建设备节点。

在2.6较早的内核版本中,device_create(…)函数的名称是class_device_create(…),所以在新的内核中编译以前的模块程序有时会报错,就是因为函数名称不同,而且里面的参数设置也有一些变化。

struct class和device_create(…) 以及device_create(…)都定义在/include/linux/device.h中,使用的时候一定要包含这个头文件,否则编译器会报错。


1、  alloc_register_region()函数向内核系统动态申请设备号。

2、  使用cdev_init()函数初始化字符设备控制块。

3、  cdev_add()函数向内核添加字符设备模块。

4、 使用class_create()/sys/class中创建设备类型目录及相应一些配置。

5、 使用device_create()自动创建设备节点。

 

同时也在模块卸载程序中填入下面动作:

1、 device_destroy()删除自动创建爱的设备节点。

2、 class_destroy()删除自动创建的目录及一些配置。

3、  cdev_del()从内核中删除设备。

4、  unregister_chrdev_region()注销自动分配的设备号。


struct class *class_create(struct module *owner, const char *name)
class_create - create a struct class structure
@owner: pointer to the module that is to "own" this struct class
@name: pointer to a string for the name of this class.
This is used to create a struct class pointer that can then be used in calls to device_create().
Returns &struct class pointer on success, or ERR_PTR() on error.
在/sys/class/下创建设备类型目录


struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)

device_create - creates a device and registers it with sysfs
@class: pointer to the struct class that this device should be registered to
@parent: pointer to the parent struct device of this new device, if any
@devt: the dev_t for the char device to be added
@drvdata: the data to be added to the device for callbacks
@fmt: string for the device's name


void device_destroy(struct class *class, dev_t devt)

device_destroy - removes a device that was created with device_create()
@class: pointer to the struct class that this device was registered with
@devt: the dev_t of the device that was previously registered
This call unregisters and cleans up a device that was created with a call to device_create().
                 

void class_destroy(struct class *cls) 

class_destroy - destroys a struct class structure
@cls: pointer to the struct class that is to be destroyed
Note, the pointer to be destroyed must have been created with a call to class_create().

 

五.使用文件私有数据

    工程师通常习惯为一个设备定义一个全局的设备相关的结构体,其中包含了设备所涉及的cdev、私有数据及信号量等信息,而这些信息在文件操作结构体fops内的函数中(open()、read()、write()、ioctl()、llseek()等)都有使用,并且我们发现fops函数集中,函数参数几乎都有struct file *filp,所以我们可以在open函数中将定义的全局设备相关的结构体赋值给filp->private_data,以后每次fops要使用时,直接通过filp->private_data调出来使用,这样相当简洁可靠。

六.参考代码


#include<linux/module.h>

#include<linux/cdev.h>

#include<linux/slab.h>

#include<linux/device.h>

#include<linux/kernel.h>

#include<linux/init.h>

#include<linux/fs.h>

#include<linux/ioctl.h>

#include<linux/uaccess.h>

 

#define MEM_SIZE0x1000

 

static char*Device_Node_Name = "vcd";//the name is adevice node name ,will show in /dev/

module_param(Device_Node_Name,charp,S_IRUGO);

MODULE_PARM_DESC(Device_Node_Name,"Theparameter default value is vcd,");

 

struct vcd_dev{      

#define DeviceName "vcd_device"//The name willshow in /proc/devices

#define DeviceClassName "vcd_class" //The name willshow in /sys/class/

#define nu_of_dev  3

#define bn_of_dev  1

         struct class *my_class;

         struct device *my_device;

         struct cdevcdev;

         dev_t dev;

         unsigned charmem_data[MEM_SIZE];

};

 

#definevcd_dev_t            struct vcd_dev

#definevcd_dev_p            struct vcd_dev *

 

vcd_dev_p pVCD;

 

//open file APIfunction

static intapi_vcd_open(struct inode *i, struct file *f)

{

         f->private_data= pVCD;


         //Here add user init open code

 

         printk(KERN_INFO "The vcd file wasopened by AP!\n");

         return 0;

}


//To close fileAPI function

static intapi_vcd_close(struct inode *i, struct file *f)

{        

         //Here add user destroyed code

 

         printk(KERN_INFO "The vcd file wasclosed by AP!\n");

         return 0;

}

 

ssize_tapi_read(struct file *filp, char __user *buff, ssize_t n,loff_t *l)

{

         vcd_dev_p fd =(vcd_dev_p)filp->private_data;

         return copy_to_user(buff,fd->mem_data,n);

}

 

structfile_operations vcd_ops =

{

         .owner = THIS_MODULE,

         .open = api_vcd_open,

         .release = api_vcd_close,

};

 

static int__init vcd_init(void)

{

         int ret;

         pVCD =kmalloc(sizeof(vcd_dev_t),GFP_ATOMIC); //Malloc the VCD_P space

         ret=alloc_chrdev_region(&pVCD->dev,bn_of_dev,nu_of_dev,DeviceName);//The DeviceName will be show in/proc/devices's file

         if(ret < 0) goto exit1;

 

         cdev_init(&pVCD->cdev,&vcd_ops);

         pVCD->cdev.owner = THIS_MODULE;

         pVCD->cdev.ops = &vcd_ops;

        

         ret =cdev_add(&pVCD->cdev,pVCD->dev,nu_of_dev); //Add device to the kernel

         if(ret < 0) goto exit2;

         

         pVCD->my_class =class_create(THIS_MODULE,DeviceClassName);//The DeviceClassName will be show in/sys/class's dir.

         if(IS_ERR(pVCD->my_class)) gotoexit3;

         

         pVCD->my_device =device_create(pVCD->my_class,NULL,pVCD->dev,NULL,Device_Node_Name);//The Device_Node_Name will create nodein /dev's dir.

         if(IS_ERR(pVCD->my_device)) gotoexit4;

 

         printk(KERN_INFO "The /dev/%s wascreated by the kernel,the dev value is %d  %d!\n",Device_Node_Name,MAJOR(pVCD->dev),MINOR(pVCD->dev));

 

         //Her add user other init code

 

         goto exit0;

 

exit4:

         class_destroy(pVCD->my_class);

 

exit3:

         cdev_del(&pVCD->cdev);

 

exit2:

         unregister_chrdev_region(pVCD->dev,nu_of_dev);

exit1:

         kfree(pVCD);

//      return -1;

exit0:

 

         return ret;

}

 

 

static void__exit vcd_exit(void)

{

         device_destroy(pVCD->my_class,pVCD->dev);

        

         class_destroy(pVCD->my_class);

         cdev_del(&pVCD->cdev);

         unregister_chrdev_region(pVCD->dev,nu_of_dev);

   

         kfree(pVCD);

         printk(KERN_INFO "The /dev/%sdevice node was destroyed by the kernel!\n",Device_Node_Name);

}

 

MODULE_LICENSE("GPL");

module_init(vcd_init);

module_exit(vcd_exit);


未完成--待续


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值