在网上看到的,讲的不错,遂转载过来。原文地址:http://blog.chinaunix.net/uid-25014876-id-59417.html
一、文件操作结构体file_operations
继续上次没讲完的问题,文件操作结构体到底是什么东西,为什么我注册了设备之后什么现象都没有?可以验证文件操作结构体的内容。
file_operations是一个函数指针的集合,用于存放我们定义的用于操作设备的函数的指针,如果我们不定义,它默认保留为NULL。
来个文件操作结构体的定义:
/*include/linux/fs.h*/ struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned i nt); int (*setlease)(struct file *, long, struct file_lock **); };
会发现,上面的函数很多都跟系统编程的函数很相似,因为这里的函数是跟系统编程的函数对应的,如在应用层调用函数open来操作设备文件,内核就会调用文件操作结构体中的成员open来进行相应的操作。
上面的函数我也只是用过一小部分,下面先写一下打开和关闭设备的函数
int (*open) (struct inode *, struct file *);
在操作设备前必须先调用open函数打开文件,可以干一些需要的初始化操作。当然,如果不实现这个函数的话,驱动会默认设备的打开永远成功。打开成功时open返回0。
int (*release) (struct inode *, struct file *);
当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL。关闭设备永远成功。
上面的函数中的的两个参数现在还没需要用到,迟点用到了会解释这两个结构体的用途。所以,下面的程序的打开和关闭并没有做实质的操作,只是想验证一下,注册设备可以调用filr_opreations中定义的函数。
上程序 目录 1st/test.c
程序和上节的5th没什么修改,我贴上修改的部分:
int test_open(struct inode *node, struct file *filp) { P_DEBUG("open device\n"); return 0; } int test_close(struct inode *node, struct file *filp) { P_DEBUG("close device\n"); return 0; } struct file_operations test_fops = { .open = test_open, .release = test_close, };
编译后加载模块:
[root: 1st]# insmod test.ko major[253] minor[0] hello kernel
现在确实是有个设备号和操作设备的函数了,但是需要操作哪个文件来操作设备?
所以先要创建一个设备文件,使用命令mknod:
用法:mknod filename type major minor filename:设备文件名 type:设备文件类型 major:主设备号 minor:次设备号
在开发板上使用命令:
[root: 1st]# mknod /dev/test c 253 0
这样,应用程序就能通过文件/dev/test来操作对应设备号的设备了。
写个应用程序来操作这个设备1st/app.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { int fd; fd = open("/dev/test", O_RDWR); if(fd < 0) { perror("open"); return -1; } close(fd); return 0; }
编译程序生成cpp,运行后发现,应用程序的open和close就会调用内核驱动中的
test_open和test_close。
[root: 1st]# ./app <kernel>[test_open]open device <kernel>[test_close]close device
结果出来了,函数被调用了。这样就说明了结构体cdev和file_operations的作用了。
二、内核中的memcpy---copy_from_user和copy_to_user:
虽然说内核中不能使用C库提供的函数,但是内核也有一个memcpy的函数,用法跟C库中的一样。
下面用用file_operations中的read和write模拟两件事:
1)从内核态通过read函数读取数据到用户态。
2)从用户态通过write函数读取数据到内核态。
驱动函数:ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 与用户层的read对应:ssize_t read(int fd, void *buf, size_t count); 用法: 从设备中读取数据,当用户层调用函数read时,对应的,内核驱动就会调用这个函数。 参数: struct file:file结构体,现在暂时不用,可以先不传参。 char __user:只看到__user就知道这是从用户态的指针,通过这个指针往用户态传数据。这是对应用户层的read函数的第二个参数void *buf。 size_t:其实这只是unsigned int。对应应用层的read函数的第三个参数。 loff_t:这是用于存放文件的偏移量的,回想一下系统编程时,读写文件的操作都会使偏移量往后移。不过待会的代码先不实现,迟点会说。 返回值: 当返回正数时,内核会把值传给应用程序的返回值。一般的,调用成功会返回成功读取的字节数。 如果返回负数,内核就会认为这是错误,应用程序返回-1。
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 与用户层的write对应:ssize_t write(int fd, const void *buf, size_t count); 用法:往设备写入数据,当用户层调用函数write时,对应的,内核驱动就会调用这个函数。 参数: struct file:file结构体,现在暂时不用,可以先不传参。 char __user:只看到__user就知道这是从用户态的指针,通过这个指针读取用户态的数据。这是对应用户层的write函数的第二个参数const void *buf。 size_t:其实这只是unsigned int。对应用户层的write函数的第三个参数count。 loff_t:这是用于存放文件的偏移量的,回想一下系统编程时,读写文件的操作都会使偏移量往后移。不过待会的代码先不实现,迟点会说。 返回值: 当返回正数时,内核会把值传给应用程序的返回值。一般的,调用成功会返回成功读取的字节数。 如果返回负数,内核就会认为这是错误,应用程序返回-1。
当然和现实的read、write有点区别。我只是实现了参数在内核与用户态之间传递,偏移量、存进内存等都没有实现。
先来个memcpy版的:目录2nd/test.c
只上修改的部分:
......... #include <linux/cdev.h> #include <linux/string.h> //memcpy必须包含该头文件 #define DEBUG_SWITCH 1 ........... int test_close(struct inode *node, struct file *filp) { P_DEBUG("close device\n"); return 0; } ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { memcpy(buf, "test_data", count); //从内核复制"test_data"到用户态 return 0; //这里返回0是不对的,应该返回成功读取的字节数, } //这里就先将就一下,下个程序就改进了。 ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { char kbuf[20]; memcpy(kbuf, buf, count); //从用户态读取数据到内核 P_DEBUG("kbuf is [%s]\n", kbuf); //在内核中打印出来,一般是存入 //某个地方的。现在也先不做。 return 0; //同样,一般返回成功读取的字节数。 } struct file_operations test_fops = { .open = test_open, .release = test_close, .write = test_write, .read = test_read, }; ................
下面运行一下看效果:
[root: 2nd]# insmod test.ko major[253] minor[0] hello kernel [root: 2nd]# mknod /dev/test c 253 0 [root: 2nd]# ./app <kernel>[test_open]open device <app>[test_data] //这是调用read程序后读到内核中的数据。 <kernel>[test_write]kbuf is [test_data] //这是应用层调用write的效果 <kernel>[test_close]close device
但是,上面的memcpy是有缺陷的,譬如有些人比较喜欢捣乱的,在用户层调用函数时传入的不是字符串,而是一个不能访问或修改的地址,那样就会造成系统崩溃,下面修改应用程序捣乱一下:
就在2nd/app.c修改了一下:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { char buf[20]; int fd; fd = open("/dev/test", O_RDWR); if(fd < 0) { perror("open"); return -1; } read(fd, buf, 20); printf("<app>[%s]\n", buf); write(fd, (const void*)(0), 20); //把参数改成非法地址 //write(fd, buf, 20); close(fd); return 0; }
编译后尝试一下运行,函数就崩溃了。
[root: 2nd]# ./app <kernel>[test_open]open device <app>[test_data] Unable to handle kernel NULL pointer dereference at virtual address 00000000 pgd = c3968000 [00000000] *pgd=33a1a031, *pte=00000000, *ppte=00000000 Internal error: Oops: 17 [#1] Modules linked in: test CPU: 0 Not tainted (2.6.29.4uplooking #1) PC is at memcpy+0x54/0x29c LR is at test_write+0x1c/0x40 [test] pc : [<c01188d4>] lr : [<bf00006c>] psr: 00000013 sp : c39d5f0c ip : 0000000c fp : c39d5f54 r10: 40025000 r9 : c39d4000 r8 : c0025fe4 r7 : 00000004 r6 : c39d5f78 r5 : 00000000 r4 : c39d5f2c r3 : c39d5f78 r2 : fffffff4 r1 : 00000000 r0 : c39d5f2c Flags: nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Seg ...........................
出于上面的原因,内核和用户态之间交互的数据时必须要先对数据进行检测,如果数据是安全的,才可以进行数据交互。因此,有了下面的两个函数:(包含头文件<asm/uaccess.h>)
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) 用法: 和memcpy的参数一样,但它根据传参方向的不同分开了两个函数。 "to"是相对于内核态来说的。所以,to函数的意思是从from指针指向的数据将n个字节的数据传到to指针指向的数据。 "from"也是相对于内核来说的。所以,from函数的意思是从from指针指向的数据将n个字节的数据传到to指针指向的数据。 返回值: 函数的返回值是指定要读取的n个字节中还剩下多少字节还没有被拷贝。 注意: 一般的,如果返回值不为0时,调用copy_to_user的函数会返回错误号-EFAULT表示操作出错。当然也可以自己决定。
上面的函数就是memcpy的改进版,在memcpy功能的基础上加上的检查传入参数的功能,防止有些人有意或者无意的传入无效的参数。
这样。下面就可以改进一下之前的函数了。
函数路径:3rd/test.c
函数只是修改了read和write函数
....... #include <asm/uaccess.h> ....... ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { int ret; // memcpy(buf, "test_data", count); if (copy_to_user(buf, "test_data", count)){ ret = - EFAULT; }else{ ret = count; P_DEBUG("kbuf is [%s]\n", buf); } return ret; //返回实际读取的字节数或错误号 } ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { char kbuf[20]; int ret; //memcpy(kbuf, buf, count); if(copy_from_user(kbuf, buf, count)){ ret = - EFAULT; }else{ ret = count; P_DEBUG("kbuf is [%s]\n", kbuf); } return ret; //返回实际写入的字节数或错误号 }
这里要说一下read和write的返回值:
当coyy_xx_user出错时,函数返回-EFUALT,内核一看是负数,就知道函数出错,此时应用层的read、write函数就会返回-1。
如果执行正确,test_read返回成功读取的字节数,内核看到非负就会认为函数正确执行,应用层的函数同样返回相同的值。
应用程序:3rd/app.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { char buf[20]; int fd, count; fd = open("/dev/test", O_RDWR); if(fd < 0) { perror("open"); return -1; } count = read(fd, buf, 20); printf("<app>buf is [%s]\n", buf); //write(fd, (const void*)(0), 20); count = write(fd, buf, 20); close(fd); return 0; }
如果用户程序传入的地址正确:
[root: 3rd]# insmod test.ko major[253] minor[0] hello kernel [root: 3rd]# ./app <kernel>[test_open]open device <kernel>[test_read]kbuf is [test_data] <app>buf is [test_data]<kernel>[test_write]kbuf is [test_data] <kernel>[test_close]close device
因为用户态和内核抢着打印,所以会出现倒数第二行的情况,不过没什么关系。
如果传入非法的参数:3rd/app_wrong.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { char buf[20]; int fd, count; fd = open("/dev/test", O_RDWR); if(fd < 0) { perror("open"); return -1; } count = read(fd, buf, 20); printf("<app>buf is [%s]\n", buf); count = write(fd, (const void*)(0), 20); if (count == -1) { perrnor("write"); } //count = write(fd, buf, 20); close(fd); return 0; }
运行时,程序会检测到错误:
[root: 3rd]# ./app_wrong <kernel>[test_open]open device <kernel>[test_read]kbuf is [test_data] <app>buf is [test_data]<kernel>[test_close]close device write: Bad address //错误信息
因为内核和用户层抢着输出,所以难免有打印乱序,但错误是出来了。
三、总结:
根据上面open、close、read、write四个操作,下面来画一个拉风的时序图。上面的read、write函数的数据是我在函数里面瞎编的,根本不是从硬件(如寄存器)读取出来的。我就先想象一下这是硬件上的数据。(当然这是指一个基本的模型,内核的操作比这个复杂)
注:箭头方向是从调用的一方指向受作用的一方。
上面讲的东西很少:
1)file operations的用途
2)copy_to_user和copy_from_user的用法
还有两个问题还没有解决:
1)struct file
2)struct inode
这些都将在下节讲。
=========================================================