在网上看到的,讲的不错,遂转载过来。原文地址: http://blog.chinaunix.net/uid-25014876-id-59416.html
一、驱动的分类:
内核驱动大致分为三类:
1)字符设备:在今后的接触的大多数都是字符设备,我也只学过这个。
2)块设备 :与硬盘相关的设备。
3)网络设备:与网络相关的设备。
上面的三种设备,我也只是学习过字符设备,所以对后面的两种也没有太多的归纳。稍稍说一下字符设备与块设备之间的区别:
1)传输数据大小:
字符设备以字节方式进行存储,最简单的举例就是寄存器的存放数据。
块设备以块大小进行传输,如一次传输512个字节,或更多。
2)响应速度:
字符设备的响应速度较快,如往寄存器写数据后硬件马上就可以相应。
而块设备的响应速度较慢,如我们往U盘存放数据后,卸载前系统会先同步数据,让数据真的写到U盘上了,才会去卸载。
下面以LED为例讲解程序操作设备的过程,以后还会再具体讲:
实际上,下面的每一部分都相当于桥梁的作用,把所有串起来,方便用户操作硬件
二、设备号的申请和字符设备的注册(旧)
设备号:
设备号由主设备号和次设备号组成。linux下,一切设备皆文件,所有的设备都能在/dev目录下找到相应的文件。这些文件除了名字不一样以外,还每个设备文件都有不一样的设备号
[root: 1st]# ls -l /dev crw-rw---- 1 root root 10, 59 Jan 1 08:00 adc crw-rw---- 1 root root 14, 4 Jan 1 08:00 audio .......... crw-rw---- 1 root root 116, 33 Jan 1 08:00 timer crw-rw-rw- 1 root root 5, 0 Jan 1 08:00 tty crw-rw---- 1 root tty 4, 0 Jan 1 08:00 tty0 crw-rw---- 1 root tty 4, 1 Jan 1 08:00 tty1 crw-rw---- 1 root tty 4, 10 Jan 1 08:00 tty10 crw-rw---- 1 root tty 4, 11 Jan 1 08:00 tty11 .........
上面的文件中,10、14、116、5、4是主设备号,59、4、33、0、1、10、11是次设备号。
一般地,主设备号对应一个类型的驱动设备,之所以有次设备号,它是用来驱动同类型的设备。如串口,所有的串口共用一个主设备号,每个串口有不同的次设备号。
在内核中,设备号用dev_t类型表示:
/*include/linux/coda.h*/ typedef unsigned long u_long; .............. typedef u_long dev_t;
而设备号前31-20位作为主设备号,低20位作为次设备号。
内核中定义了3个宏分别用来提取主次设备号和构造设备号:
/*/include/linux/kdev_t.h*/ #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))
MAJOR用于提取主设备号,MINOR用于提取次设备号,MKDEV用于构造设备号。
在驱动函数中,加载函数至少要执行两步的操作:
1)设备号的申请,让内核分配一个设备号给设备驱动,申请后最直接的效果就是在/proc/devices目录下能够查看到,具体能看到什么下面会说。
2)设备的注册,让内核知道用什么函数来操作设备。
在旧版本中申请和注册只用一个函数就能搞定。老规矩,先上程序。
目录 1st:旧版本的设备号申请和设备注册
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> unsigned int major = 253; //定义设备号 struct file_operations test_fops; //定义文件操作的结构体 static int __init test_init(void) { register_chrdev(major, "test_driver", &test_fops); /*设备号申请+设备注册函数*/ printk("hello kernel\n"); return 0; } static void __exit test_exit(void) { unregister_chrdev(major, "test_driver"); /*设备释放函数*/ printk("good bye kernel\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("techbulo"); MODULE_VERSION("v0.1");
编译后操作:
[root: 1st]# insmod test.ko hello kernel [root: 1st]# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty ............... 204 s3c2410_serial 253 test_driver //注册后多出了一项 253 和 "test driver"是在函数指定 254 rtc Block devices: ............... 135 sd [root: 1st]# rmmod test good bye kernel
上面就调用了函数register_chrdev,这是旧版本的设备注册函数:
int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops) 使用:设备注册函数。 参数: 1)major:主设备号,0-255,在旧版本中,一个主设备号对应一个设备。 2)name:一个显示在/proc/devices目录下供用户查看的的字符串。一般const char*都是用于显示在proc给用户查看。 3)struct file_operations:文件操作结构体,是一个函数指针的集合,里面存放了对该设备的操作的实现方法,如open、close等。迟点会用到,现在是空的,只是为了迎合注册函数的要求,勉强先定义一个空结构体骗过编译器。
返回值:正确返回0,失败返回错误号。
上面的一个函数就实现了两个功能:
1)为驱动程序分配了一个设备号。
2)通过了file_operations告诉了内核能够用操作驱动的函数。
撇开文件操作结构体不说,效果是出来了,当调用命令"cat proc/devices"时显示了我申请的设备号253,显示的名字是"test_driver"。还有我们看不到的是,注册成功后,一个在内核中维护设备号和文件操作结构体file_operations的表添加了一项对应关系,这样内核就知道了这个设备号的驱动是用什么样的函数操作。
对应的,模块卸载时需要把设备号注销:
void unregister_chrdev(unsigned int major, const char *name)
卸载函数的两个参数必须和注册函数一致。
三、设备号的申请和字符设备的注册(新)
上面的函数固然方便,一个函数就搞定了两件事情,但是也会有缺点。
譬如现在设备号紧缺,只剩下一个主设备号了,但是我现在有两个设备驱动要一起使用,这样就大问题了。
所以现在用的新版本把原来的函数拆分了两部分:
1)申请一个主设备号中的一部分(region)设备号;
2)注册设备,告诉内核操作设备的函数。
3.1先说第一部分的操作:设备号申请
设备号的申请:register_chrdev_region
int register_chrdev_region(dev_t from, unsigned count, const char *name) 使用: 指定从设备号from开始,申请count个设备号,在/proc/devices中的名字 为name。 返回值: 成功返回0,失败返回错误码。
上面的申请函数需要我们自己定义设备号,我们需要知道哪些设备号是已经被人占用的,这样才不会和原来系统的产生冲突导致申请失败。所以,需要先在文档Documentation/devices.txt 查看哪些设备号已经被使用了。
*Documentation/devices.txt */ 40-254 char LOCAL/EXPERIMENTAL USE //这些是试验用的,所以没人用,我用!
当然,如果不想手动指定设备号,也可以使用动态分配设备号函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 使用: 动态申请从次设备号baseminor开始的count个设备号,在/proc/devices中的名字为name,并通过dev指针把分配到的设备号返回给调用函数者。 返回值: 成功返回0,失败返回错误码。
谨记,无论使用上面的哪种方式申请注册号,在模块卸载时都要释放设备号
void unregister_chrdev_region(dev_t from, unsigned count) 使用:释放从from开始count个设备号。
调用了设备号申请函数后,就可以在/proc/devices下看到注册的信息了。
下面要做的是告诉内核在在指定的文件操作结构体调用函数操作设备。
3.2第二部分操作,设备注册
与旧版本的注册函数不一样,新版本的注册函数之用结构体struct cdev来表示一个字符设备。字符设备的注册就是要定义一个这样的结构体并且填上对应的内容。
先看一下结构体的成员,我没注释的是内核自己要填的。
/*/include/linux/cdev.h*/ struct cdev { struct kobject kobj; struct module *owner; //一般初始化为THIS_MODULE const struct file_operations *ops; //文件操作结构体 struct list_head list; dev_t dev; //设备号 unsigned int count; //添加的设备个数 };
注册也分为三个步骤:
1)分配cdev;
2)初始化cdev;
3)添加cdev;
3.2.1分配cdev:简单的说就是定义一个cdev结构体
方法一:直接定义:
struct cdev test_cdev;
方法二:调用函数:struct cdev* cdev_alloc(void)
struct cdev* test_cdev; test_cdev = cdev_alloc();
3.2.2初始化cdev:
将文件操作结构体添加到cdev中:
void cdev_init(struct cdev *cdev, const struct file_operations *fops) 参数: cdev:之前我定义的cdev结构体; fops:设备对应的文件操作结构体。 返回值:(函数有可能失败,查看返回值是必须的) 成功返回0,示范返回对应的错误码
这个函数干了两件事情:
1)内核自己填充了结构体中list和kobj的内容
2)把我传入的文件操作结构体也填充进去。
一般的,还要手工定义结构体成员owner。
struct file_operations test_fops; cdev_init(&test_cdev, &test_fops); test_cdev->owner = THIS_OWNER //指定模块的所属
3.2.3添加cdev:
将cdev结构体与设备号关联起来:
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count) 参数: cdev:指定要被添加的cdev结构体; dev:对应的设备号 count:从设备号dev开始添加count个设备. 返回值: 成功返回0,失败返回对应的错误码。
函数干了也两件事:
1)把cdev结构体中还没填充的两个成员dev和count按照传入参数赋值。
2)把cdev结构体中传入内核,这样内核就知道对应设备号和具体的文件操作结构体了。
3.2.4删除cdev:
这是添加的逆操作,模块卸载是调用:
void cdev_del(struct cdev *p)
写了这么久,终于把步骤写完了。怕自己说的不清楚,再总结一下上面说的步骤:
加载模块:
1)申请一个设备号,下面注册设备时需要用。
2)注册设备。
2.1分配一个cdev结构体。
2.2初始化cdev结构体,使结构体与文件操作结构体(fops)对应起关系。
2.3添加cdev结构体到内核。
卸载模块:
1)注销设备,即从内核中删除指定的cdev。
2)注销设备号。
下面来个程序 目录 3rd
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> unsigned int major = 0; unsigned int minor = 0; dev_t devno; struct cdev test_cdev; //2.1分配cdev结构体 struct file_operations test_fops; //分配一个文件操作结构体,但现在是空的 //以后会用到 static int __init test_init(void) //模块初始化函数 { 1 /*1.登记设备号*/ /*如果主设备号不为0,使用静态申请一个设备号*/ if(major){ devno = MKDEV(major, minor); register_chrdev_region(devno, 1, "test new driver"); }else{ /* 否则由内核动态分配*/ alloc_chrdev_region(&devno, minor, 1, "test alloc diver"); } /*2.注册设备*/ /*2.2初始化cdev*/ cdev_init(&test_cdev, &test_fops); test_cdev.owner = THIS_MODULE; /*2.3添加cdev到内核中*/ cdev_add(&test_cdev, devno, 1); printk("hello kernel\n"); return 0; } static void __exit test_exit(void) //模块卸载函数 { /*1.从内核中删除cdev*/ cdev_del(&test_cdev); /*2.注销设备号*/ unregister_chrdev_region(devno, 1); printk("good bye kernel\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("techbulo"); MODULE_VERSION("v0.1");
上面的程序就实现了两步,申请设备号和注册设备到内核,其中,申请设备号时是根据主设备号来确定是要静态申请还是又内核动态申请设备号。
如果major=0,使用动态申请,加载后可显示:
[root: 3rd]# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 .......... 253 test alloc diver 254 rtc ...........
如果major=253,或者其他不等于0的数,就以次作为主设备号申请:
[root: 3rd]# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 .......... 253 test new diver 254 rtc ...........
四、函数错误返回处理
内核中,在linux/errno.h文件中定义了一些错误号,当内核的函数操作失败后,它会返回一个负数,通过查看这个数的值,内核就会在终端中显示相应的错误原因。
注:错误号是正数,错误时的返回值是错误号的负值。
在linux/errno.h中只是定义了与平台无关的错误号,其他错误号在另外文件中。
/*include/linux/errno.h*/ #ifndef _LINUX_ERRNO_H #define _LINUX_ERRNO_H #include <asm/errno.h> //在这里包含平台相关错误号 //arm是/arch/arm/include/asm/errno.h #ifdef __KERNEL__ .................... #define ERESTARTSYS 512 //这些是与平台无关的错误号 #define ERESTARTNOINTR 513 #define ERESTARTNOHAND 514 /* restart if no handler.. */ #define ENOIOCTLCMD 515 /* No ioctl command */ #define ERESTART_RESTARTBLOCK 516
再进入arm对应的errno.h,发现他也是包含了另外的文件
/*arch/arm/include/asm/errno.h*/ #include <asm-generic/errno.h>
再进入include/asm-generic/errno.h, 发现他也定义了错误号,也包含了个文件
#ifndef _ASM_GENERIC_ERRNO_H #define _ASM_GENERIC_ERRNO_H #include <asm-generic/errno-base.h> #define EDEADLK 35 /* Resource deadlock would occur */ #define ENAMETOOLONG 36 /* File name too long */ #define ENOLCK 37 /* No record locks available */ #define ENOSYS 38 /* Function not implemented */
再进去include/asm-generic/errno-base.h
#ifndef _ASM_GENERIC_ERRNO_BASE_H #define _ASM_GENERIC_ERRNO_BASE_H #define EPERM 1 /* Operation not permitted */ #define ENOENT 2 /* No such file or directory */ #define ESRCH 3 /* No such process */ #define EINTR 4 /* Interrupted system call */
拷贝了这么多的代码只是想说明两件件事:
1)只要我们包含了头文件linux/errno.h,编译时Makefile就会根据对应的体系架构进入到对应的目录找到对应的错误号。真实贴心的设计。
2)以后需要包含asm相关的头文件时,可以先看看linux下有没有已经忽略了相关体系架构的头文件。
言归正传,上面的设备申请的代码还没完善,因为有些函数会出错,但是没有进行对应的错误处理。
1)申请设备号函数有可能失败,可能是申请的设备号已经被占用,也可能是设备号用光了。
2)添加cdev函数cdev_add也有可能失败。
所以,在调用这两个函数的时候,必须判断返回值。
上个程序 目录 4th
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/errno.h> #define DEBUG_SWITCH 1 #if DEBUG_SWITCH #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args) #else #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args) #endif unsigned int major = 254; unsigned int minor = 0; dev_t devno; struct cdev test_cdev; //2.1分配cdev结构体 struct file_operations test_fops; //分配一个文件操作结构体,但现在是空的 //以后会用到 static int __init test_init(void) //模块初始化函数 { int result = 0; /*1.等级设备号*/ /*如果主设备号不为0,使用静态申请一个设备号*/ if(major){ devno = MKDEV(major, minor); result = register_chrdev_region(devno, 1, "test new driver"); }else{ /* 否则由内核动态分配*/ result = alloc_chrdev_region(&devno, minor, 1, "test alloc diver"); major = MAJOR(devno); minor = MINOR(devno); } if(result < 0){ /*错误时返回的值就是错误号*/ P_DEBUG("register devno errno!\n"); result = - EBUSY; goto err0; } printk("major[%d] minor[%d]\n", major, minor); /*2.注册设备*/ /*2.2初始化cdev*/ cdev_init(&test_cdev, &test_fops); test_cdev.owner = THIS_MODULE; /*2.3添加cdev到内核中*/ result = cdev_add(&test_cdev, devno, 1); if(result < 0){ P_DEBUG("cdev_add errno!\n"); result = - ENODEV; goto err1; } printk("hello kernel\n"); return 0; err1: unregister_chrdev_region(devno, 1); //错误时需要把原来做的事情取消 err0: return result; } static void __exit test_exit(void) //模块卸载函数 { /*1.从内核中删除cdev*/ cdev_del(&test_cdev); /*2.注销设备号*/ unregister_chrdev_region(devno, 1); printk("good bye kernel\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("techbulo"); MODULE_VERSION("v0.1");
加载函数时
[root: 4th]# insmod test.ko <kernel>[test_init]register devno errno! insmod: cannot insert 'test.ko': Device or resource busy
但是,明明内核就给你返回一个值,你偏要去改成其他值,这是干了多蠢的事。所以,用函数返回的错误号就好了:
改一下上面程序,其实就注释掉了三行代码句话 目录 5th
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> // #include <linux/errno.h> #define DEBUG_SWITCH 1 #if DEBUG_SWITCH #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args) #else #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args) #endif unsigned int major = 254; unsigned int minor = 0; dev_t devno; struct cdev test_cdev; //2.1分配cdev结构体 struct file_operations test_fops; //分配一个文件操作结构体,但现在是空的 //以后会用到 static int __init test_init(void) //模块初始化函数 { int result = 0; /*1.等级设备号*/ /*如果主设备号不为0,使用静态申请一个设备号*/ if(major){ devno = MKDEV(major, minor); result = register_chrdev_region(devno, 1, "test new driver"); }else{ /* 否则由内核动态分配*/ result = alloc_chrdev_region(&devno, minor, 1, "test alloc diver"); major = MAJOR(devno); minor = MINOR(devno); } if(result < 0){ /*错误时返回的值就是错误号*/ P_DEBUG("register devno errno!\n"); //result = - EBUSY; goto err0; } printk("major[%d] minor[%d]\n", major, minor); /*2.注册设备*/ /*2.2初始化cdev*/ cdev_init(&test_cdev, &test_fops); test_cdev.owner = THIS_MODULE; /*2.3添加cdev到内核中*/ result = cdev_add(&test_cdev, devno, 1); if(result < 0){ P_DEBUG("cdev_add errno!\n"); // result = - ENODEV; goto err1; } printk("hello kernel\n"); return 0; err1: unregister_chrdev_region(devno, 1); //错误时需要把原来做的事情取消 err0: return result; } static void __exit test_exit(void) //模块卸载函数 { /*1.从内核中删除cdev*/ cdev_del(&test_cdev); /*2.注销设备号*/ unregister_chrdev_region(devno, 1); printk("good bye kernel\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("techbulo"); MODULE_VERSION("v0.1");
加载函数时:
[root: 5th]# insmod test.ko <kernel>[test_init]register devno errno! insmod: cannot insert 'test.ko': Device or resource busy
虽然错误号跟原来的一样,但这是内核自己的错误返回,不是自己瞎编的。
注:上面还有一个的问题,到底file_operation到底是用来做什么的。后面会讲。
=========================================================