Linux驱动字符设备分析misc_register、register_chrdev

一、字符设备基础知识

在 2.4 的内核我们使用 register_chrdev(0, "hello", &hello_fops) 来进行字符设备设备节点的分配,这种方式每一个主设备号只能存放一种设备,它们使用相同的 file_operation 结构体,也就是说内核最多支持 256 个字符设备驱动程序。

在 2.6 的内核之后,新增了一个 register_chrdev_region 函数,它支持将同一个主设备号下的次设备号进行分段,每一段供给一个字符设备驱动程序使用,使得资源利用率大大提升,同时,2.6 的内核保留了原有的 register_chrdev 方法。在 2.6 的内核中这两种方法都会调用到 __register_chrdev_region 函数,本文将从它入手来分析内核是如何管理字符设备驱动程序的。

1、设备驱动分类

      linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序:


字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
     如图,在Linux内核中:

a -- 使用cdev结构体来描述字符设备;

b -- 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

c -- 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

     在Linux字符设备驱动中:

a -- 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

b -- 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;

c -- 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;
     用户空间访问该设备的程序:

a -- 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;
3、字符设备驱动模型

二、cdev 结构体解析

      在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:


<include/linux/cdev.h>
 
struct cdev { 
    struct kobject kobj;                  //内嵌的内核对象.
    struct module *owner;                 //该字符设备所在的内核模块的对象指针.
    const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
    struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
    dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
    unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};

内核给出的操作struct cdev结构的接口主要有以下几个:
a -- void cdev_init(struct cdev *, const struct file_operations *);

其源代码如代码清单如下:

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;
}
      该函数主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接:
(1) 将整个结构体清零;

(2) 初始化list成员使其指向自身;

(3) 初始化kobj成员;

(4) 初始化ops成员;


 b --struct cdev *cdev_alloc(void);

     该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即: .ops=xxx_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;
}
     在上面的两个初始化的函数中,我们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE, 该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。

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

       该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。

当然这里还需提供两个参数:

(1)第一个设备号 dev,

(2)和该设备关联的设备编号的数量。

这两个参数直接赋值给struct cdev 的dev成员和count成员。

d -- void cdev_del(struct cdev *p);

     该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了。

     从上述的接口讨论中,我们发现对于struct cdev的初始化和注册的过程中,我们需要提供几个东西

(1) struct file_operations结构指针;

(2) dev设备号;

(3) count次设备号个数。

但是我们依旧不明白这几个值到底代表着什么,而我们又该如何去构造这些值!


三、设备号相应操作

1 -- 主设备号和次设备号(二者一起为设备号):

      一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

  linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

  typedef u_long dev_t;

  在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

内核也为我们提供了几个方便操作的宏实现dev_t:

1) -- 从设备号中提取major和minor

MAJOR(dev_t dev);                              

MINOR(dev_t dev);

2) -- 通过major和minor构建设备号

MKDEV(int major,int minor);
注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;

//宏定义:
#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)
#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>


2、分配设备号(两种方法):

a -- 静态申请:

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

其源代码清单如下:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
    struct char_device_struct *cd;
    dev_t to = from + count;
    dev_t n, next;
 
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        if (next > to)
            next = to;
        cd = __register_chrdev_region(MAJOR(n), MINOR(n),
                   next - n, name);
        if (IS_ERR(cd))
            goto fail;
    }
    return 0;
fail:
    to = n;
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
    }
    return PTR_ERR(cd);
}

b -- 动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

其源代码清单如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
    struct char_device_struct *cd;
    cd = __register_chrdev_region(0, baseminor, count, name);
    if (IS_ERR(cd))
        return PTR_ERR(cd);
    *dev = MKDEV(cd->major, cd->baseminor);
    return 0;
}
可以看到二者都是调用了__register_chrdev_region 函数,其源代码如下:

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
               int minorct, const char *name)
{
    struct char_device_struct *cd, **cp;
    int ret = 0;
    int i;
 
    cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
    if (cd == NULL)
        return ERR_PTR(-ENOMEM);
 
    mutex_lock(&chrdevs_lock);
 
    /* temporary */
    if (major == 0) {
        for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
            if (chrdevs[i] == NULL)
                break;
        }
 
        if (i == 0) {
            ret = -EBUSY;
            goto out;
        }
        major = i;
        ret = major;
    }
 
    cd->major = major;
    cd->baseminor = baseminor;
    cd->minorct = minorct;
    strlcpy(cd->name, name, sizeof(cd->name));
 
    i = major_to_index(major);
 
    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major > major ||
            ((*cp)->major == major &&
             (((*cp)->baseminor >= baseminor) ||
              ((*cp)->baseminor + (*cp)->minorct > baseminor))))
            break;
 
    /* Check for overlapping minor ranges.  */
    if (*cp && (*cp)->major == major) {
        int old_min = (*cp)->baseminor;
        int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
        int new_min = baseminor;
        int new_max = baseminor + minorct - 1;
 
        /* New driver overlaps from the left.  */
        if (new_max >= old_min && new_max <= old_max) {
            ret = -EBUSY;
            goto out;
        }
 
        /* New driver overlaps from the right.  */
        if (new_min <= old_max && new_min >= old_min) {
            ret = -EBUSY;
            goto out;
        }
    }
 
    cd->next = *cp;
    *cp = cd;
    mutex_unlock(&chrdevs_lock);
    return cd;
out:
    mutex_unlock(&chrdevs_lock);
    kfree(cd);
    return ERR_PTR(ret);
}
 通过这个函数可以看出  register_chrdev_region和  alloc_chrdev_region 的区别,register_chrdev_region直接将Major 注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;
二者应用可以简单总结如下:

                                     register_chrdev_region                                                alloc_chrdev_region 

    devno = MKDEV(major,minor);
    ret = register_chrdev_region(devno, 1, "hello"); 
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1);
     alloc_chrdev_region(&devno, minor, 1, "hello");
    major = MAJOR(devno);
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1)
register_chrdev(major,"hello",&hello
     可以看到,除了前面两个函数,还加了一个register_chrdev 函数,可以发现这个函数的应用非常简单,只要一句就可以搞定前面函数所做之事;

下面分析一下register_chrdev 函数,其源代码定义如下:


static inline int register_chrdev(unsigned int major, const char *name,
                  const struct file_operations *fops)
{
    return __register_chrdev(major, 0, 256, name, fops);
}
调用了 __register_chrdev(major, 0, 256, name, fops) 函数:
int __register_chrdev(unsigned int major, unsigned int baseminor,
              unsigned int count, const char *name,
              const struct file_operations *fops)
{
    struct char_device_struct *cd;
    struct cdev *cdev;
    int err = -ENOMEM;
 
    cd = __register_chrdev_region(major, baseminor, count, name);
    if (IS_ERR(cd))
        return PTR_ERR(cd);
 
    cdev = cdev_alloc();
    if (!cdev)
        goto out2;
 
    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);
 
    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
    if (err)
        goto out;
 
    cd->cdev = cdev;
 
    return major ? 0 : cd->major;
out:
    kobject_put(&cdev->kobj);
out2:
    kfree(__unregister_chrdev_region(cd->major, baseminor, count));
    return err;
}
可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev 的初始化以及cdev 的注册; 

3、注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count);

4、创建设备文件:

     利用cat /proc/devices查看申请到的设备名,设备号。

1)使用mknod手工创建:mknod filename type major minor

2)自动创建设备节点:

    利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

    详细解析见:Linux 字符设备驱动开发 (二)—— 自动创建设备节点

 

杂项设备(misc device)

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。使用misc_register(&ff_wdt_miscdev); 在 Linux 内核的include/linux目录下有Miscdevice.h文件,要把自己定义的misc device从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10 ,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。

也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。

字符设备(char device)

使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时,如果有多个设 备使用该函数注册驱动程序,LED_MAJOR不能相同,否则几个设备都无法注册(我已验证)。如果模块使用该方式注册并且 LED_MAJOR为0(自动分配主设备号 ),使用insmod命令加载模块时会在终端显示分配的主设备号和次设备号,在/dev目录下建立该节点,比如 设备leds,如果加载该模块时分配的主设备号和次设备号为253和0,则建立节点:mknod leds c 253 0。使用register_chrdev (LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时都要手动建立节点 ,否则在应用程序无法打开该设备。
 

杂项设备

linux里面的misc杂项设备是主设备号为10的驱动设备

定义头文件<linux/miscdevice.h>

杂项设备的结构体:

struct miscdevice{
  int minor; //杂项设备的此设备号(如果设置为MISC_DYNAMIC_MINOR,表示系统自动分配未使用的minor)
  const char *name;
  const stuct file_operations *fops;//驱动主题函数入口指针
  struct list_head list;
  struct device *parent;
  struct device *this device;
  const char *nodename;(在/dev下面创建的设备驱动节点)
  mode_t mode;
};

注册和释放

注册:int misc_register(struct miscdevice *misc)

释放:int misc_deregister(struct miscdevice *misc)

misc_device是特殊字符设备。注册驱动程序时采用misc_register函数注册,此函数中会自动创建设备节点,即设备文件。无需mknod指令创建设备文件。因为misc_register()会调用class_device_creat或者device_creat().

杂项字符设备和一般字符设备的区别:

1.一般字符设备首先申请设备号。  但是杂项字符设备的主设备号为10次设备号通过结构体struct miscdevice中的minor来设置。

2.一般字符设备要创建设备文件。 但是杂项字符设备在注册时会自动创建。

3.一般字符设备要分配一个cdev(字符设备)。  但是杂项字符设备只要创建struct miscdevice结构即可。

4.一般字符设备需要初始化cdev(即给字符设备设置对应的操作函数集struct file_operation). 但是杂项字符设备在结构体truct miscdevice中定义。

5.一般字符设备使用注册函数 int cdev_add struct (cdev *p,devt_t dev, unsigned)(第一个参数为之前初始化的字符设备,第二个参数为设备号,第三个参数为要添加设备的个数) 而杂项字符设备使用int misc_register(struct miscdevice *misc)来注册

 

驱动调用的实质:

就是通过 设备文件找到与之对应设备号的设备,再通过设备初始化时绑定的操作函数硬件进行控制

 内核态驱动

#include <linux/miscdevice.h>


static void ff_wdt_handler_enable(void)
{
    printk(KERN_WARNING "[keep2] ff_wdt_handler_enable\n");
    ff_wdt_enable =1;
}

static void ff_wdt_handler_disable(void)
{
    printk(KERN_WARNING "[keep2] ff_wdt_handler_disable\n");
    ff_wdt_enable = 0;
}


static ssize_t ff_wdt_write(struct file *file, const char __user *data,
                 size_t len, loff_t *ppos)
{
    size_t i;

    printk(KERN_WARNING "[keep2] ff_wdt_write\n");

    if (len) 
    {
        for (i = 0; i != len; i++) {
            char c;
            if (get_user(c, data + i))
                return -EFAULT;
        }
    
    }

    return len;
}

static long ff_wdt_ioctl(struct file *file, unsigned int cmd,
                            unsigned long arg)
{
    int timeout;
    int options;
    void __user *argp = (void __user *)arg;
    
    //printk(KERN_WARNING "[keep2] ff_wdt_ioctl\n");

    switch (cmd) 
    {
        case WDIOC_SETOPTIONS:
            if (get_user(options, (int __user *)argp))
                return -EFAULT;

            if (options & WDIOS_DISABLECARD)
                ff_wdt_handler_disable();

            if (options & WDIOS_ENABLECARD)
                ff_wdt_handler_enable();
            break;

        case WDIOC_SETTIMEOUT:
            ff_app_wdt_timeout = jiffies;
            if (get_user(timeout, (int __user *)argp))
                return -EFAULT;

        case WDIOC_GETTIMEOUT:
            if (put_user(ff_kernel_wdt_timeout, (unsigned long __user *)argp))
                return -EFAULT;
            break;

        default:
            return -ENOTTY;
    }
    
    return 0;
}

static int ff_wdt_open(struct inode *inode, struct file *file)
{
    printk(KERN_WARNING "[keep2] ff_wdt_open\n");

    return nonseekable_open(inode, file);
}

static int ff_wdt_release(struct inode *inode, struct file *file)
{
    printk(KERN_WARNING "[keep2] ff_wdt_release\n");

    return 0;
}

static const struct file_operations ff_wdt_fops = {
    .owner = THIS_MODULE,
    .llseek = no_llseek,
    .write = ff_wdt_write,
    .unlocked_ioctl = ff_wdt_ioctl,
    .open = ff_wdt_open,
    .release = ff_wdt_release,
};

static struct miscdevice ff_wdt_miscdev = {
    .minor = FF_WATCHDOG_MINOR,
    .name = "ff_watchdog",
    .fops = &ff_wdt_fops,
};


static int __init ff_wdt_init(void)
{
    printk(KERN_WARNING "[keep2] ff_wdt_init\n");
    
    misc_register(&ff_wdt_miscdev);
    
    return 0;
}

static void __exit ff_wdt_exit(void)
{
    printk(KERN_WARNING "[keep2] ff_wdt_exit\n");

    misc_deregister(&ff_wdt_miscdev);
}

module_init(ff_wdt_init);
module_exit(ff_wdt_exit);

用户态调用

int main (int argc, char *argv[])
{  
    unsigned long kernel_wdt_time = 1;   
    unsigned long last_kernel_wdt_time = 0; 
    unsigned long feed_count = 0;   
    int wdt_enable = 0;
    
    printf("[keep]start feed watchdog!\n");   

    int fd_watchdog = open(FF_WDT_DEV, O_WRONLY);   
    if(fd_watchdog <= 0)   
    {   
        int err = errno;   
        printf("[keep]failed to open /dev/ff_watchdog, errno: %d\n", err);   
        return -1;   
    }   

    wdt_enable = FF_WDT_ENABLECARD;
    ioctl(fd_watchdog, FF_WDT_SETOPTIONS, &wdt_enable);

    while (1)
    {
        //printf("[keep] kernel_wdt_time = %ld\n", kernel_wdt_time); 
        ioctl(fd_watchdog, FF_WDT_SETTIMEOUT, &kernel_wdt_time);
        ioctl(fd_watchdog, FF_WDT_GETTIMEOUT, &kernel_wdt_time);
        if(kernel_wdt_time == last_kernel_wdt_time)
        {
            printf("[keep] kernel_wdt_time = %ld,last_kernel_wdt_time = %ld\n", kernel_wdt_time, last_kernel_wdt_time); 
            feed_count++;              
            if(feed_count >= 60)
            {
                break;
            }
        }
        else
        {
            last_kernel_wdt_time = kernel_wdt_time;
            feed_count = 0;
        }
        sleep(1);
    }

    close(fd_watchdog);
    
    return 0;
}   

 

 

  • 3
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值