资源描述
Linux设备驱动程序简单字符模块这一章主要通过介绍字符设备的驱动程序编写,来学习Linux设备驱动的基本知识。Globalmem可以为真正的设备驱动程序提供样板。一、主设备号和次设备号主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。主设备号相同的设备使用相同的驱动程序,次设备号用于区分具体设备的实例。比如PC机中的IDE设备,一般主设备号使用3,WINDOWS下进行的分区,一般将主分区的次设备号为1,扩展分区的次设备号为2、3、4,逻辑分区使用5、6.。2.6内核用dev_t类型()来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20位表示次设备号。在实际使用中,是通过中定义的宏来转换格式。(dev_t)-主设备号、次设备号MAJOR(dev_t dev)MINOR(dev_t dev)主设备号、次设备号-(dev_t)MKDEV(int major,int minor)对于查看/dev目录下的设备的主次设备号可以使用如下命令:/mnt/yaffsls /dev -l crw- 1 root root 5, 1 Jan 1 00:00 console crw- 1 root root 5, 64 Jan 1 00:00 cua0 crw- 1 root root 5, 65 Jan 1 00:00 cua1 crw-rw-rw- 1 root root 1, 7 Jan 1 00:00 full drwxr-xr-x 1 root root 0 Jan 1 00:00 keyboard crw-r- 1 root root 1, 2 Jan 1 00:00 kmemcrw-r- 1 root root 1, 1 Jan 1 00:00 memdrwxr-xr-x 1 root root 0 Jan 1 00:00 mtddrwxr-xr-x 1 root root 0 Jan 1 00:00 mtdblockcrw-rw-rw- 1 root root 1, 3 Jan 1 00:00 nullcrw-r- 1 root root 1, 4 Jan 1 00:00 portcrw- 1 root root 108, 0 Jan 1 00:00 pppcrw-rw-rw- 1 root root 5, 2 Jan 1 00:00 ptmxcrw-r-r- 1 root root 1, 8 Jan 1 00:00 randomlr-xr-xr-x 1 root root 4 Jan 1 00:00 root - rd/0crw-rw-rw- 1 root root 5, 0 Jan 1 00:00 ttycrw- 1 root root 4, 64 Jan 1 00:11 ttyS0crw- 1 root root 4, 65 Jan 1 00:00 ttyS1crw-r-r- 1 root root 1, 9 Jan 1 00:00 urandomcrw-rw-rw- 1 root root 1, 5 Jan 1 00:00 zero建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在中声明:int register_chrdev_region(dev_t first, unsigned int count,char *name); /静态指定设备编号int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name); /动态生成设备编号void unregister_chrdev_region(dev_t first, unsigned int count);/释放设备编号分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。以下是在globalmeml.c中用来获取主设备好的代码: dev_t devno = MKDEV(globalmem_major, 0); /* 申请设备号*/ if (globalmem_major) result = register_chrdev_region(devno, 1, globalmem); else /* 动态申请设备号 */ result = alloc_chrdev_region(&devno, 0, 1, globalmem); globalmem_major = MAJOR(devno); if (result 0) return result;在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices中。二、一些重要的数据结构大部分基本的驱动程序操作涉及及到三个重要的内核数据结构,分别是file_operations、file和inode,它们的定义都在。三、字符设备的注册内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含,它定义了struct cdev以及与其相关的一些辅助函数。我们一般将cdev结构嵌入到自己的设备特定结构中去如:struct globalmem_dev struct cdev cdev; /*cdev结构体*/ unsigned char memGLOBALMEM_SIZE; /*全局内存*/ ;注册一个独立的cdev设备的基本过程如下:1、为自己的设备结构体分配空间struct globalmem_dev *globalmem_devp; /*设备结构体指针*/ globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL); if (!globalmem_devp) /*申请失败*/ result = - ENOMEM; goto fail_malloc; memset(globalmem_devp, 0, sizeof(struct globalmem_dev);2、初始化struct cdev void cdev_init(struct cdev *cdev, const struct file_operations *fops) 3、初始化cdev.ownercdev.owner = THIS_MODULE;4、cdev设置完成,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)int cdev_add(struct cdev *p, dev_t dev, unsigned count)5、从系统中移除一个字符设备:void cdev_del(struct cdev *p)以下globalmem的初始化代码/*初始化并注册cdev*/static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) int err, devno = MKDEV(globalmem_major, index); cdev_init(&dev-cdev, &globalmem_fops); dev-cdev.owner = THIS_MODULE; dev-cdev.ops = &globalmem_fops; err = cdev_add(&dev-cdev, devno, 1); if (err) printk(KERN_NOTICE Error %d adding globalmem%d, err, index);/*设备驱动模块加载函数*/int globalmem_init(void) int result; dev_t devno = MKDEV(globalmem_major, 0); /* 申请设备号*/ if (globalmem_major) result = register_chrdev_region(devno, 1, globalmem); else /* 动态申请设备号 */ result = alloc_chrdev_region(&devno, 0, 1, globalmem); globalmem_major = MAJOR(devno); if (result 0) return result; /* 动态申请设备结构体的内存*/ globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL); if (!globalmem_devp) /*申请失败*/ result = - ENOMEM; goto fail_malloc; memset(globalmem_devp, 0, sizeof(struct globalmem_dev); globalmem_setup_cdev(globalmem_devp, 0); return 0; fail_malloc: unregister_chrdev_region(devno, 1); return result;四、内存申请函数Globalmem驱动程序引入了两个Linux内核中用于内存管理的核心函数,它们的定义都在: void *kmalloc(size_t size, int flags);void kfree(void *ptr);例子:#include char *buff;buff=kmalloc(1024,GFP_KERNEL);if(buff!=NULL).kfree(buff);elseprintk(“kmalloc errorn”);flags的参数GFP_KERNEL请求动态内存总是分配成功,如无则等待。故不能用在中断中。GFP_ATOMIC无条件分配内存,没有立即释放,进程不睡眠。GFP_DMA用于分配连续的物理内存五、open和release 、open方法提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下:(1)检查设备特定的错误(如设备未就绪或硬件问题);(2)如果设备是首次打开,则对其进行初始化;(3)如有必要,更新f_op文件指针;(4)分配并填写置于filp-private_data里的数据结构。而根据globalmem的实际情况,他的open函数只要完成第四步(将初始化过的struct globalmem_dev dev的指针传递到filp-private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在中的container_of宏,源码如下:#define container_of(ptr, type, member) (const typeof( (type *)0)-member ) *_mptr = (ptr);(type *)( (char *)_mptr - offsetof(type,member) );)其实从源码可以看出,其作用就是:通过指针ptr,获得包含ptr所指向数据(是member结构体)的type结构体的指针。即是用指针得到另外一个指针。int globalmem_open(struct inode *inode, struct file *filp) /*将设备结构体指针赋值给文件私有数据指针*/ struct globalmem_dev *dev; dev = container_of(inode-i_cdev,struct globalmem_dev,cdev); filp-private_data = dev; return 0; 第一个参数是结构体成员的指针,第二个参数为整个结构体的类型,第三个参数为传入的第一个参数即结构体成员的类型,返回值为整个结构体的指针。、release方法提供释放内存,关闭设备的功能。应完成的工作如下:(1)释放由open分配的、保存在file-private_data中的所有内容;(2)在最后一次关闭操作时关闭设备。有时执行的内容和exit里面的内容重合,所以本代码有的时候什么也不做。六、read和write read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在中定义的:read 方法完成将数据从内核拷贝到应用程序空间,write方法相反,将数据从应用程序空间拷贝到内核。对于这两个方法,参数filp是文件指针,count是请求传输数据的长度,buffer是用户空间的数据缓冲区,ppos(有时候写成fpos都是一个概念)是文件中进行操作的偏移量,类型为64位数。由于用户空间和内核空间的内存映射方式完全不同,所以不能使用象memcpy之类的函数,必须使用如下函数:unsigned long copy_to_user(void _user *to, const void *from,unsigned long count);unsigned long copy_from_user(void *to, const void _user *from, unsigned long count);如果要复制的为简单类型,如char,long,int等,则可以使用简单的put_user()和get_user().如下所示:int val;/内核空间整型变量.get_user(val,(int*)arg);/用户空间到内核空间,arg是用户空间的地址put_user(val,(int*)arg);/内核空间到用户空间,arg是用户空间的地址至于read和write 的具体函数比较简单,就在实验中验证好了。 read的返回值1. 返回值等于传递给read系统调用的count参数,表明请求的数据传输成功。2. 返回值大于0,但小于传递给read系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法。3. 返回值0,表示到达文件的末尾。4. 返回值为负数,表示出现错误,并且指明是何种错误。5. 在阻塞型io中,read调用会出现阻塞。 Write的返回值1. 返回值等于传递给write系统调用的count参数,表明请求的数据传输成功。2. 返回值大于0,但小于传递给write系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法。3. 返回值0,表示没有写入任何数据。标准库在调用write时,出现这种情况会重复调用write。4. 返回值为负数,表示出现错误,并且指明是何种错误。错误号的定义参见5. 在阻塞型io中,write调用会出现阻塞。七、模块实验测试结果:rootNEU/# insmod /lib/modules/2.6.14/globalmem.korootNEU/# lsmodglobalmem 2920 0 - Live 0xbf000000rootNEU/# cat /proc/devicesCharacter devices: 1 mem 2 pty 3 ttyp 4 /dev/vc/0 4 tty 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 14 sound 21 sg 29 fb 81 video4linux 90 mtd128 ptm136 pts180 usb189 usb_device204 s3c2410_serial253 globalmem254 devfsBlock devices: 1 ramdisk 31 mtdblock180 ubrootNEU/#rootNEU/# mknod /dev/globalmem c 253 0rootNEU/# ls /dev/globalmem/dev/globalmemrootNEU/# echo hello/dev/globalmemwritten 6 bytes(s) from 0rootNEU/# cat /dev/globalmemread 4096 bytes(s) from 0hellocat: read error: No such device or addressrootNEU/#八、附录:file_operations结构:struct module *owner 第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 中定义的宏.loff_t (*llseek) (struct file *, loff_t, int); llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个long offset, 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在file 结构 一节中描述).ssize_t (*read) (struct file *, char _user *, size_t, loff_t *); 用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL(Invalid argument) 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 signed size 类型, 常常是目标平台本地的整数类型).ssize_t (*aio_read)(struct kiocb *, char _user *, size_t, loff_t); 初始化一个异步读 - 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).ssize_t (*write) (struct file *, const char _user *, size_t, loff_t *); 发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.ssize_t (*aio_write)(struct kiocb *, const char _user *, size_t, loff_t *); 初始化设备上的一个异步写.int (*readdir) (struct file *, void *, filldir_t); 对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.unsigned int (*poll) (struct file *, struct poll_table_struct *); poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, 设备无这样的 ioctl), 系统调用返回一个错误. int (*mmap) (struct file *, struct vm_area_struct *); mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.int (*open) (struct inode *, struct file *); 尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.int (*flush) (struct file *); flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.int (*release) (struct inode *, struct file *); 在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.int (*fsync) (struct file *, struct dentry *, int); 这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.int (*aio_fsync)(struct kiocb *, int); 这是 fsync 方法的异步版本.int (*fasync) (int, struct file *, int); 这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.int (*lock) (struct file *, int, struct file_lock *); lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *); 这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.10int (*check_flags)(int) 这个方法允许模块检查传递给 fnctl(F_SETFL.) 调用的标志.int (*dir_notify)(struct file *, unsigned long); 这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify file_operations结构是整个Linux内核的重要数据结构,它也是file、inode结构的重要成员,表中分别说明结构中主要的成员:表 file_operations结构Ownermodule的拥有者。Llseek重新定位读写位置。Read从设备中读取数据。Write向字符设备中写入数据。Readdir只用于文件系统,对设备无用。Ioctl控制设备,除读写操作外的其他控制命令。Mmap将设备内存映射到进程地址空间,通常只用于块设备。Open打开设备并初始化设备。Flush清除内容,一般只用于网络文件系统中。Release关闭设备并释放资源。Fsync实现内存与设备的同步,如将内存数据写入硬盘。Fasync实现内存与设备之间的异步通讯。Lock文件锁定,用于文件共享时的互斥访问。Readv在进行读操作前要验证地址是否可读。Writev在进行写操作前要验证地址是否可写。在嵌入式系统的开发中,我们一般仅仅实现其中几个接口函数:read、write、ioctl、open、release,就可以完成应用系统需要的功能。struct file_operations globalmem_fops = .owner = THIS_MODULE, .llseek = scull_llseek, .read = scull_read, .write = scull_write, .ioctl = scull_ioctl, .open = scull_open, .release = scull_release, ;static struct file_operations globalmem_fops = 完成了将驱动函数映射为标准接口,上面的这种特殊表示方法不是标准C的语法,这是GNU编译器的一种特殊扩展,它使用名字对进行结构字段的初始化,它的好处体现在结构清晰,易于理解,并且避免了结构发生变化带来的许多问题。struct file结构 定义于 , 是设备驱动中第二个最重要的数据结构.系统中每个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时创建, 并传递给在文件上操作的任何函数, 直到最后的关闭. 在文件的所有实例都关闭后, 内核释放这个数据结构.在内核源码中, struct file 的指针常常称为 file 或者 filp(file pointer). 我们将一直称这个指针为 filp 以避免和结构自身混淆. 因此, file 指的是结构, 而 filp 是结构指针.mode_t f_mode; 文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.loff_t f_pos; 当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp-f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.另一种解释:此变量显示出当前读写位置,而由read,write,llseek等可修改读写位置的函数改变,用在管理文件指针的设备驱动程序上。unsigned int f_flags; 这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的阻塞和非阻塞操作一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应当检查读/写许可, 使用 f_mode 而不是 f_flags. 所有的标志在头文件 中定义.struct file_operations *f_op; 和文件关联的操作. 内核安排指针作为它的 open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp-f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp-f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的方法重载的内核对等体.void *private_data; open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.struct dentry *f_dentry; 关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry 结构, 除了作为 filp-f_dentry-d_inode 存取 inode 结构.真实结构有多几个成员, 但是它们对设备驱动没有用处. 我们可以安全地忽略这些成员, 因为驱动从不创建文件结构; 它们真实存取别处创建的结构。inode 结构inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的. 可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构.inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于编写驱动代码有用:dev_t i_rdev; 对于代表设备文件的节点, 这个成员包含实际的设备编号.struct cdev *i_cdev; struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时关于驱动程序中static ssize_t globalmem_write(struct file *filp, const char _user *buf, size_t size, loff_t *ppos)static ssize_t globalmem_read(struct file *filp, char _user *buf, size_t size, loff_t *ppos)的进一步解释:ppos表示的是文件的读写位置,打开一个设备文件后 filp为该文件建立了一个file类型的结构体,其中一个成员为loff_t类型的,表示文件的读写位置,该值和filp紧密联系,如果将设备驱动程序创建为具有管理文件读写位置的功能,就需要处理ppos变量,比如我们的驱动程序中设备驱动程序实现内存的读写功能,那么ppos就是指向内存的地址,当你写了一些数据后,再写的话,这时的ppos就和上一次写操作完成后的位置了,读的时候也应该注意这个问题,如果我们只是简单的测试下write和read的功能的话,那么就不用处理ppos。一般情况下,多数驱动程序不管理文件指针。
展开阅读全文