Linux字符设备驱动注册三种方法以及内核分析

       Linux驱动是用户访问底层硬件的桥梁,驱动有可以简单分成三类:字符设备、块设备、网络设备。其中最多的是字符设备,其中字符设备的注册方法主要有三种:杂项设备注册、早期字符设备注册、标准字符设备注册。以及详细介绍各类方法注册。

开发环境:

PC:VMworkstation 12  运行Ubuntu12 32位虚拟机

开发板:友善之臂Tiny4412 (EXYNOS4412 Cortex-A9)

Linux内核版本:Linux 3.5

PC内核阅读器:SourceInsight 

 

一、杂项设备(misc device):

在内核路径下include\linux\miscdevice.h文件有以下内容:

struct miscdevice  {
    int minor;
    const char *name;
    const struct file_operations *fops;
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const char *nodename;
    umode_t mode;
};

extern int misc_register(struct miscdevice * misc);
extern int misc_deregister(struct miscdevice *misc);

通过上述函数的声明可知杂项设备的注册函数为:misc_register,注销函数为misc_deregister,通过sourceinsight搜索参考代码:

Linux字符设备驱动注册三种方法以及内核分析_第1张图片

可知要使用一个杂项设备必须要有一个struct miscdevice 结构体,根据miscdevice.h结构体的定义和相关内核代码可知:

需要至少实现结构体内部三个参数:

   int minor;//次设备号(主设备号默认为10,其中杂项设备是通过早期字符设备静态添加到内核中,在早期设备说明)
   const char *name;//设备名称,dev下创建的设备节点名称
   const struct file_operations *fops;//文件操作指针,为用户层与驱动层访问的接口

以上很明显的阐述了驱动注册之前需要的准备工作:对struct miscdevice结构体进行定义并且赋值,然而内部参数并非这么简单:struct file_operations 还嵌套一层结构体,为文件操作集合结构体。根据内核代码,我们可以找到相关所有文件操作集合的函数接口:

Linux字符设备驱动注册三种方法以及内核分析_第2张图片

位于内核根目录下:include\linux下的Fs.h文件下定义了文件操作集合函数接口。通过在驱动层实现这些接口以便对驱动层的访问。通过上述分析,杂项设备注册方式很简单的暂时概括为:1、准备工作:结构体的定义与赋值、接口函数的实现。2、将结构体传入杂项设备注册函数,实现注册。3、用户层函数的编写。

讲到这里可能还不理解用户层如何通过文件操作集合定义的函数接口去实现访问内核驱动,以一个简单的代码为例:

static struct file_operations fops_led=
{
    .open=open_led,
    .write=write_led,
    .read=read_led,
    .release=release_led,
};

static struct miscdevice misc_led=
{
    .minor=MISC_DYNAMIC_MINOR, /*自动分配次设备号*/
    .name="tiny4412_led",      /*设备节点的名称*/
    .fops=&fops_led           /*文件操作集合*/
};

在驱动层定义的文件操作集合为:open、write、read、release。

open:当用户层通过open(dev\设备结点)时对应驱动层实现的open函数会被调用,当用户层通过open会产生一个文件描述符,用户层再通过文件描述符去read、write操作时,会对应调用驱动层的read、write,通过这些函数的来访问驱动。相关代码:

驱动层:

#include 
#include 
#include 
#include 
static int open_led (struct inode *my_inode, struct file *my_file)
{
	printk("open_led调用成功!\n");
	return 0;	
}
static ssize_t read_led(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("read_led调用成功!\n");
	return 0;
}

static ssize_t write_led(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("write_led调用成功!\n");
	return 0;
}

static int release_led(struct inode *my_inode, struct file *my_file)
{
    printk("release_led调用成功!\n");
	return 0;
}

static struct file_operations fops_led=
{
	.open=open_led,
	.write=write_led,
	.read=read_led,
	.release=release_led,
};

static struct miscdevice misc_led=
{
	.minor=MISC_DYNAMIC_MINOR, /*自动分配次设备号*/
	.name="tiny4412_led",      /*设备节点的名称*/
	.fops=&fops_led           /*文件操作集合*/
};

static int __init tiny4412_led_init(void)
{
	int err;
	err=misc_register(&misc_led);  //杂项设备注册函数
	if(err<0)
	{
		printk("提示: 驱动安装失败!\n");
		return err;
	}
    printk("提示: 驱动安装成功!\n");
    return err;
}

static void __exit tiny4412_led_exit(void)
{
	int err;
	err=misc_deregister(&misc_led);  //杂项设备注销函数
	if(err<0)
	{
		printk("提示: 驱动卸载失败!\n");
	}
    printk("提示: 驱动卸载成功!\n");
}
module_init(tiny4412_led_init);  /*指定驱动的入口函数*/
module_exit(tiny4412_led_exit); /*指定驱动的出口函数*/
MODULE_LICENSE("GPL");       /*指定驱动许可证*/

用户层:

#include 
#include 
#include 
#include 

int main(int argc,char **argv)
{
	int fd;
	if(argc!=2)
	{
		printf("运行方式: ./app <设备文件>\n");
		return 0;
	}
	
	fd=open(argv[1],O_RDWR);
	if(fd<0)
	{
		printf("%s 设备文件打开失败!\n",argv[1]);
		return 0;
	}
	
	int data;
	read(fd,&data,4);
	write(fd,&data,4);
	close(fd);
	return 0;
}

那么问题又来了,misc_register怎么工作的呢?跳到misc_register函数里面:

Linux字符设备驱动注册三种方法以及内核分析_第3张图片

在driver\char下的misc.c文件中,主要任务是先初始化链表,由于整个链表在文件开始已经静态初始化了:

Linux字符设备驱动注册三种方法以及内核分析_第4张图片

所以第一步只需判断新添加的设备是否已经存在,通过list_for_each_entry遍历链表与新添加的设备对比,这个是个宏原型是一个for循环主要是遍历作用。当设备是新的再执行第二步是否是自动注册次设备号,通过传入结构体成员minor 是否为 MISC_DYNAMIC_MINOR宏来判断,是的话找到定位一个未被注册的次设备号,并且对minor赋值。第三步通过MKDEV函数将主设备号与次设备号合成为设备的设备号。第四步创建设备节点,设备节点是访问驱动的一个接口,通过函数device_create创建, 将misc->name成员作为节点名称,所以对结构体赋值时最好能体现设备的特征。第五步也是最后一步将此设备添加到内核通过函数list_add。

然后精彩的地方来了,在misc.c文件中发现了一个系统初始化自动执行的函数:subsys_initcall(misc_init);很显然,这个初始化关乎到了杂项设备能否正常执行,在这里先埋下伏笔,其函数为:

Linux字符设备驱动注册三种方法以及内核分析_第5张图片

二、早期字符设备的注册

在刚刚的include\linux下Fs.h里面还有一个函数用于注册字符设备,这个函数也在misc.c里面调用过,可知杂项设备也是通过这个函数来实现注册:

static inline int register_chrdev(unsigned int major,//主设备号

                                                const char *name,//设备结点名称
                                                const struct file_operations *fops)

函数的返回值为:主设备号

通过sourceinsight可以查看函数的函数内部内容:

static inline int register_chrdev(unsigned int major, const char *name,  const struct file_operations *fops)
{
    return __register_chrdev(major, 0, 256, name, fops);
}

可知在Fs.h里面只是再调用了一个__register_chrdev函数并且把所有的参数都给了这另一个函数。在跳转到__register_chrdev函数里面:

Linux字符设备驱动注册三种方法以及内核分析_第6张图片

里面又调用了__register_chrdev_region返回了一个cd,这个cd是干嘛呢?再跳转到__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--) {/*  chrdevs 存放字符设备的总个数结构体 */
            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);
这个函数有点长,其实主要功能就是以下部分:

Linux字符设备驱动注册三种方法以及内核分析_第7张图片

如果传入的major为0,则经过一个for循环遍历所有的chrdevs结构体数组:

static struct char_device_struct {
    struct char_device_struct *next;
    unsigned int major;
    unsigned int baseminor;
    int minorct;
    char name[64];
    struct cdev *cdev;        /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
每一位成员为一个设备的节点信息,组成一个链表通过这个for循环遍历这个链表来找到没有使用的主设备号,那很明显,当传入进来的major为0,会自动分配主设备号,这个major也就是register_chrdev的第一个参数。并且当没找到时会报错。接着这个函数将主设备号装载在struct char_device_struct结构体内部进行返回,继续在__register_chrdev函数里面操作:

Linux字符设备驱动注册三种方法以及内核分析_第8张图片

再次判断得到的结构体,并且再次封装结构体,通过cdev_add添加到内核。通过上面的分析,register_chrdev主要是用于生成一个主设备号,由杂项设备可知杂项设备的主设备号为10,那是否也是通过register_chrdev函数注册的主设备号,在misc.c文件里面我们发现了一个杂项设备初始化的函数:misc_init其函数体为:

static int __init misc_init(void)
{
    int err;

#ifdef CONFIG_PROC_FS
    proc_create("misc", 0, NULL, &misc_proc_fops);/*    交互式文件系统  */
#endif
    misc_class = class_create(THIS_MODULE, "misc");/* 创建一个设备类  */
    err = PTR_ERR(misc_class);
    if (IS_ERR(misc_class))
        goto fail_remove;

    err = -EIO;
    if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))/* 注册字符设备 在/sys/class/创建子目录*/
        goto fail_printk;
    misc_class->devnode = misc_devnode;
    return 0;

fail_printk:
    printk("unable to get major %d for misc devices\n", MISC_MAJOR);
    class_destroy(misc_class);
fail_remove:
    remove_proc_entry("misc", NULL);
    return err;
}

很显然,里面调用了register_chrdev在major.h文件夹里面定义#define MISC_MAJOR        10,这就是杂项设备的主设备号为10的原因了,在前面我们介绍过早期字符设备register_chrdev注册他并没有像杂项设备一样调用device_create来产生一个设备节点,所以,如何使用这个函数产生的主设备号呢?

这个问题想要达成的效果就是在/dev/下生成设备节点,刚好在linux下有一个创建设备结点的命令:mknod,用法:

mknod Name {b|c} major minor    :Name设备名称   b|c块设备还是字符设备 major主设备号 minor次设备号

那么,问题又来了设备号是个坑,但是我们可以通过打印函数打印创建的主设备号,然后通过mknod创建节点,测试代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
static int open_led (struct inode *my_inode, struct file *my_file)
{
	printk("open_led调用成功!\n");
	return 0;	
}
static ssize_t read_led(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("read_led调用成功!\n");
	return cnt;
}
static ssize_t write_led(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("write_led调用成功!\n");
	return cnt;
}
static int release_led(struct inode *my_inode, struct file *my_file)
{

    printk("release_led调用成功!\n");
	return 0;
}
static struct file_operations fops_led=
{
	.open=open_led,
	.write=write_led,
	.read=read_led,
	.release=release_led,
};
static unsigned int major;
static int __init tiny4412_led_init(void)  /*insmod xxx.ko*/
{
	major=register_chrdev(0,"led", &fops_led);
	printk("major:%d\n",major);
    printk("提示:驱动安装成功!\n");
}
static void __exit tiny4412_led_exit(void)
{
	unregister_chrdev(major,"led");
    printk("提示: 驱动卸载成功!\n");
}
module_init(tiny4412_led_init);    /*指定驱动的入口函数*/
module_exit(tiny4412_led_exit);   /*指定驱动的出口函数*/
MODULE_LICENSE("GPL");       /*指定驱动许可证*/

将此生成.ko文件上传到开发板insmod安装驱动后出行:

提示生成的主设备号为250,现在我们用mknod生成两个设备节点:

通过ls /dev/查看生成的设备节点:

再生成一个节点,其中主设备号一致,次设备号不一致

生成的两个设备节点,现在通过用户层去访问驱动,用户层代码为:

#include
#include 
#include 
#include 

int main(int argc ,char ** argv)
{
	int i=0,count,err;
	if(argc!=2)
	{
		printf("usage: ./app \n");
		return 0;
	}
	err=open(argv[1],2);
	if(err<0)
	{
		printf("device open fail!\n");
		return 0;
	}
	while(1)
	{
		read(err,&count,1);
		sleep(1);
		
		write(err,&count,1);
		sleep(1);
	}
	close(err);
}

Linux字符设备驱动注册三种方法以及内核分析_第9张图片

上述运行结果为可得到以下结论:

结论:register_chrdev只是单纯地创建了一个可以使用到的主设备号并且没有创建任何设备节点,当通过mknod产生的节点只是通过主设备号去访问驱动,次设备号不一样仍然是访问主设备号的驱动。

上面可以看得出,如果使用这种方式去注册的话一个设备占用一个主设备号,相比杂项设备来说有点浪费资源,因为理论来说杂项设备可以注册255个并且它只占用一个主设备号,其实我们通过杂项设备可以看出我们可以模仿内核杂项设备注册方式来使得早期字符设备更加高效性,那要模仿杂项设备,那就在来分析下杂项设备的函数。由上述结论可知倘若杂项设备采用的是早期字符设备注册方式去注册驱动的话他是怎样做到在相同的主设备号的情况下去访问不同的驱动,下面我们在misc.c文件夹里面先分析下杂项设备怎样通过主设备号10来区分不同的驱动。

首先,在misc_register函数里面主要执行的是通过创建一个数据结构:链表,用链表来存储各个次设备的信息,并且每次注册新的设备的时候都会遍历链表去找到未被使用的设备号,当设备号合成之后便将这个新的链表节点添加到链表中。这便完成一次新的设备添加,但是这里只是体现了对次设备号的新建并没有如何去实现区分次设备。

其次我们用户层对驱动的接口操作一般是先由open函数得来的文件描述符,也就是说当打开设备节点时,一定是访问杂项设备10的主驱动,用上面的例子来说就是都是访问注册时填入的文件操作集合实现的底层接口函数,由此可以提出一个推论:杂项设备一定实现某种函数接口,如open。到在这里,重点来来了,通过查看misc.c文件发现有文件操作集合结构体:

static const struct file_operations misc_fops = {
    .owner        = THIS_MODULE,
    .open        = misc_open,
    .llseek        = noop_llseek,
};老铁,没看错,就是open函数的实现,那就看看这个open到底写了啥。

先说明一点,注册的杂项设备一定会访问这个open函数,为啥呢?因为上面的例子充分说明主设备号都是访问同一个驱动。那接着看open函数有什么文章。忘了介绍一个知识点,就是open函数的形参:struct inode * inode可以通过这个来获取当前打开的次设备号(用户层open的那个设备节点的次设备号),获得的方法为:minor = iminor(inode);函数传入这个形参便可以获得,再通过下面的for循环:

通过这个for循环遍历整个链表找到这个链表中与当前打开的次设备号一致的设备结构体指针,当存在时,将这个设备的文件操作集合调出来赋值给new_fops。之后做的东西更加厉害了通过open的第二个形参可以获得当前注册的次设备号的fops,也就是说这个参数的成员fops为对应注册时某个次设备的文件操作集合,这便是我们想要的次设备对应的驱动代码,也就是说在这里调用改变文件操作集合的指向,将共有的驱动里面通过打开的次设备号找到注册时写入的信息里面的文件操作集合,然后在共有的驱动里面改变当前的文件集合指向对应次设备的文件集合便可以,具体操作如下:

Linux字符设备驱动注册三种方法以及内核分析_第10张图片

此时本来是调用了公共的open现在通过这个open将文件操作集合修改了,并且有通过这个open跳转到次设备号注册的open,由于其余的文件操作集合函数形参都有一个struct file结构体,其指向都以发生改变,这边使得指向了不同的驱动程序。以下便是形象的总结:

Linux字符设备驱动注册三种方法以及内核分析_第11张图片

下面我们写一个早期设备框架的代码:

早期字符设备底层框架代码:

#include 
#include 
#include 
#include 
#include 
static unsigned int char_dev_major; /*存放主设备号*/
static struct class *char_dev_class;   /*定义类*/
static LIST_HEAD(char_device_list); /*创建链表头*/
/*
自定义字符设备结构体
*/
static struct char_device
{
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
};
static int open_char_dev (struct inode *my_inode, struct file *my_file)
{
	/*1. 获取次设备号*/
	int minor = iminor(my_inode); /*得到次设备号*/
	/*2. 查找次设备号对应的文件操作集合*/
	struct char_device *c;
	list_for_each_entry(c, &char_device_list, list)  
	{
		if (c->minor == minor) 
		{
			my_file->f_op=c->fops;	 /*改变文件操作集合的指向*/
			my_file->f_op->open(my_inode,my_file); /*调用底层用户写的open函数*/
		}
	}
	return 0;	
}
static struct file_operations fops_char_dev=
{
	.open=open_char_dev
};
/*
向外提供的字符设备注册函数
*/
int char_device_register(struct char_device * char_device)
{
	struct char_device *c;
	dev_t dev;
	/*1. 初始化链表头*/
	INIT_LIST_HEAD(&char_device->list); 
	/*2. 遍历链表*/
	list_for_each_entry(c, &char_device_list, list)  
	{
		if (c->minor == char_device->minor) 
		{
			printk("次设备号%d冲突!\n",c->minor);
			return -1;
		}
	}
	/*3. 合成设备号*/
	dev = MKDEV(char_dev_major, char_device->minor);
	printk("主设备号:%d,次设备号:%d\n",char_dev_major,char_device->minor);
	/*4. 在/dev下生成设备节点文件*/
	device_create(char_dev_class,NULL,dev,NULL, "%s", char_device->name,char_device->minor);
	/*5. 添加设备到链表*/
	list_add(&char_device->list, &char_device_list);
}
/*
向外提供的字符设备注销函数
*/
int char_device_deregister(struct char_device *char_device)
{
	/*1. 删除链表节点*/
	list_del(&char_device->list);
	/*2. 删除设备节点文件*/
	device_destroy(char_dev_class, MKDEV(char_dev_major, char_device->minor));
}
EXPORT_SYMBOL(char_device_register);
EXPORT_SYMBOL(char_device_deregister);
static int __init char_dev_init(void)  /*insmod xxx.ko*/
{
	int err;
	/*1.早期设备注册方式*/
	char_dev_major=register_chrdev(0,"char_device",&fops_char_dev); 
	/*2. 创建类*/
	char_dev_class = class_create(THIS_MODULE, "tiny4412_char_dev");
	printk("char_dev_major=%d\n",char_dev_major);
    printk("提示: 字符设备驱动框架安装成功!\n");
    return err;
}
static void __exit char_dev_exit(void)
{
	int err;
	/*1. 设备注销*/
	unregister_chrdev(char_dev_major,"char_dev");
	/*2. 注销类*/
	class_destroy(char_dev_class);
    printk("提示: 字符设备驱动框架卸载成功!\n");
}
module_init(char_dev_init);    /*指定驱动的入口函数*/
module_exit(char_dev_exit);   /*指定驱动的出口函数*/
MODULE_LICENSE("GPL");       /*指定驱动许可证*/

两个使用早期字符设备注册的驱动代码,两个独立的驱动函数:

device1.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
extern int char_device_register(struct char_device * char_device);
extern int char_device_deregister(struct char_device *char_device);
static struct char_device
{
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
};
static int device1_open(struct inode *my_inode, struct file *my_file)
{
	printk("device1_open调用成功1!\n");
	return 0;	
}

static ssize_t device1_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_read调用成功!1\n");
	return cnt;
}

static ssize_t device1_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_write调用成功!\n");
	return cnt;
}

static int device1_release(struct inode *my_inode, struct file *my_file)
{
    printk("device1_release调用成功1!\n");
	return 0;
}
static struct file_operations fops=
{
	.open=device1_open,
	.write=device1_write,
	.read=device1_read,
	.release=device1_release,
};
static struct char_device misc_beep=
{
	.minor=10,
	.name="device1",
	.fops=&fops,
};
static int __init device1_init(void) 
{
	char_device_register(&misc_beep);
 	printk("提示:device1驱动安装成功!\n");
    return 0;
}
static void __exit device1_exit(void)
{
	char_device_deregister(&misc_beep);
    printk("提示: device1驱动卸载成功!\n");
}
module_init(device1_init);    /*指定驱动的入口函数*/
module_exit(device1_exit);   /*指定驱动的出口函数*/
MODULE_LICENSE("GPL");       /*指定驱动许可证*/

device2.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
extern int char_device_register(struct char_device * char_device);
extern int char_device_deregister(struct char_device *char_device);
static struct char_device
{
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
};
static int device2_open(struct inode *my_inode, struct file *my_file)
{
	printk("device2_open调用成功1!\n");
	return 0;	
}

static ssize_t device2_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device2_read调用成功!1\n");
	return cnt;
}

static ssize_t device2_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device2_write调用成功!\n");
	return cnt;
}

static int device2_release(struct inode *my_inode, struct file *my_file)
{
    printk("device2_release调用成功1!\n");
	return 0;
}
static struct file_operations fops=
{
	.open=device2_open,
	.write=device2_write,
	.read=device2_read,
	.release=device2_release,
};
static struct char_device misc_beep=
{
	.minor=11,
	.name="device2",
	.fops=&fops,
};
static int __init device2_init(void) 
{
	char_device_register(&misc_beep);
 	printk("提示:device2驱动安装成功!\n");
    return 0;
}
static void __exit device2_exit(void)
{
	char_device_deregister(&misc_beep);
    printk("提示: device2驱动卸载成功!\n");
}
module_init(device2_init);    /*指定驱动的入口函数*/
module_exit(device2_exit);   /*指定驱动的出口函数*/
MODULE_LICENSE("GPL");       /*指定驱动许可证*/

用户层函数跟上面的一样,现在开始运行程序了:

首先第一步:对驱动框架进行安装:

Linux字符设备驱动注册三种方法以及内核分析_第12张图片

安装框架成功,系统给的主设备号为250,并且查看dev下的设备节点并没我们想要创建的设备节点,这是因为我们还没有调用早期字符设备的注册函数,这个函数在device1.c和device2.c里面调用。接着安装device1.ko和device2.ko:

Linux字符设备驱动注册三种方法以及内核分析_第13张图片

可以看到成功按照程序添加的信息去注册,并且获得次设备号产生对应的设别节点device1和device2,这里能够体现杂项设备是通过注册函数产生的节点。现在最具有对比性的操作来了,上面有个例子是用mknod来创建设备节点的,忘记了可以翻上去看一下,那个例子正好创建的设备号一致,但是他用同样的用户层代码打开不同的设备节点访问的都是同样的一个驱动,那这个呢?那就看结果:

Linux字符设备驱动注册三种方法以及内核分析_第14张图片

这结果就很nice了,他访问的是两个不一样的驱动程序,实现的次设备号的独立程序访问,这样节省了早期设备的资源,其实这个程序还是不能够验证是先调用公共的open函数来达到切换fops(文件操作集合),我们char_device.c的open进行修改:

Linux字符设备驱动注册三种方法以及内核分析_第15张图片

然后重新编译安装驱动:

Linux字符设备驱动注册三种方法以及内核分析_第16张图片

用户层app运行打开相同主设备号的设备节点前都是访问同一个公共的驱动open,再进行分支切换。

结论:杂项设备的驱动其实是完善的早期字符设备的一个特例,固定主设备号为10,也是通过早期字符设备注册的,对早期字符设备封装后再内核加载时初始化,其实上面的框架也可以添加在内核,每一套都是自己的“杂项设备”。

上述分析中利用__register_chrdev_region函数来得到主设备号,在char_dev.c中还设有几个函数也是通过调用__register_chrdev_region函数来实现注册的:

Linux字符设备驱动注册三种方法以及内核分析_第17张图片

Linux字符设备驱动注册三种方法以及内核分析_第18张图片

那这两个API函数是干嘛用的呢,这就是标准字符设备的注册函数。

三、标准字符设备注册

在早期字符设备注册框架里面有涉及到cdev结构体,在__register_chrdev里面,__register_chrdev_region获得主设备号后通过cdev_alloc函数创建一个struct cdev结构,将所有的注册信息保存在这个结构体里面再通过cdev_add添加向内核注册字符设备,在创建cdev结构体后对结构体赋值有一个初始化函数:cdev_init通过这个函数添加到cdev链表里面去,也就是说,标准字符设备是通过cdev链表来保存所有的注册信息,在char_dev.c文件里面:

Linux字符设备驱动注册三种方法以及内核分析_第19张图片

通过这个函数将cdev结构初始化,就是对结构体成员赋值,其中一个成员是文件操作集合,所以这里面包括了对ops成员的赋值,添加链表之后,可以通过alloc_chrdev_region来动态申请设备号,第二个传参:baseminor 次设备号的起始地址,第三个传参:count是连续申请的个数。也就是说他可以连续申请几个设备并且可以通过device_create连续创建节点:示例代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include   
#include 
#include 
#include 
static struct class *dev_class; 
static dev_t dev_num;    /*设备号*/
static struct cdev *cdev;
static int device1_open(struct inode *my_inode, struct file *my_file)
{
	printk("device1_open调用成功1!\n");
	return 0;	
}

static ssize_t device1_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_read调用成功!1\n");
	return cnt;
}

static ssize_t device1_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_write调用成功!\n");
	return cnt;
}

static int device1_release(struct inode *my_inode, struct file *my_file)
{
    printk("device1_release调用成功1!\n");
	return 0;
}

static struct file_operations device1_fops=
{
	.open=device1_open,
	.read=device1_read,
	.write=device1_write,
	.release=device1_release,
};
/*static struct file_operations device2_fops=
{
	.open=device2_open,
	.read=device2_read,
	.write=device2_write,
	.release=device2_release,
};*/

static int __init std_device_init(void)  
{
	int i; 
    /*1. 动态分配cdev结构*/
	cdev=cdev_alloc();
	/*2. cdev初始化*/
	cdev_init(cdev,&device1_fops);
	/*3. 动态分配设备号*/
	alloc_chrdev_region(&dev_num,10,2,"led_dev");
	printk("dev_num:%d\n",dev_num);
	/*4. 添加字符设备到内核*/
	cdev_add(cdev,dev_num,2);  //添加设备到内核	
	/*5. 创建设备类,创建成功会在: /sys/class  目录下创建子目录*/
	dev_class = class_create(THIS_MODULE, "exynos4412");
	for(i=0;i<2;i++)
	{
		/*6. 在/dev下生成设备节点文件*/
		device_create(dev_class,NULL,dev_num+i,NULL, "device%d",i+1);
	}
	if(IS_ERR(dev_class))
	{
		printk("dev_class error!\n");
		goto Class_Error;
	}
	printk("提示: 设备号分配成功!\n");
Class_Error:
    return 0;
}
static void __exit std_device_exit(void)
{
	int i;
	for(i=0;i<2;i++)
	{
		device_destroy(dev_class,dev_num+i);//删除设备节点
	}
	class_destroy(dev_class);//注销类
	unregister_chrdev_region(dev_num,2);//注销设备
	kfree(cdev);//释放空间
    printk("提示: 设备号驱动卸载成功!\n");
}

module_init(std_device_init);    /*指定驱动的入口函数*/
module_exit(std_device_exit);    /*指定驱动的出口函数*/
MODULE_LICENSE("GPL");       /*指定驱动许可证*/

用户层代码不变,结果:

Linux字符设备驱动注册三种方法以及内核分析_第20张图片

没有安装驱动之前没有设备节点,现在通过insmod安装驱动:

Linux字符设备驱动注册三种方法以及内核分析_第21张图片

安装后在/dev/下生成两个设备节点:device1和device2,现在通过用户层app打开设备节点:

Linux字符设备驱动注册三种方法以及内核分析_第22张图片

这个结果有在程序上理解很简单,因为在cdev结构体初始化的时候只使用了一个fops文件操作集合,所以两个都是访问同一个文件操作集合的函数,可以在对应文件从操作集合里面创建分支或者和早期字符设备一样实现优化,这里不再重复。

结论:标准字符设备注册相对前面几个注册方式而言底层内容丰富的多,基本上都是调用相对比较底层的函数,相比于杂项设备,他能够自己分配主设备号,比杂项设备灵活,并且能连续注册好几个设备也可以注册一个设备;相对早期字符设备,没有个繁琐的框架,简单单一,但是使用还是要灵活一点才可以。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Linux驱动)