CTDIY-3-字符设备的内部实现
想要了解字符设备的内部实现,最直接的方法是先来看struct file_operations,在结构体中封装的函数,实际上就是字符设备可以实现的功能。
struct file_operations { struct module *owner; //拥有该结构体的模块指针,一般设为THIS_MODULE loff_t (*llseek) (struct file *, loff_t, int); //修改文件当前的读写认为,对应为llseek()函数 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //从设备中读数据,对应为read()函数 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //向设备写数据,对应为write()函数 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); //仅用于读取目录,对设备文件而言,该字段为NULL unsigned int (*poll) (struct file *, struct poll_table_struct *);‘ //轮询函数,判断目前是否可以进行非阻塞的读取或写入 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //执行设备I/O控制命令,在不使用BLK放的文件系统上使用 long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上代替ioctl()的函数 int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间 int (*open) (struct inode *, struct file *); //打开函数,open() int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); //关闭函数,release() int (*fsync) (struct file *, loff_t, loff_t, int datasync); //刷新待处理的数据 int (*aio_fsync) (struct kiocb *, int datasync); //异步的fsync int (*fasync) (int, struct file *, int); //通知FASYNC标志发生变化 int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); //通常为NULL unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); //在当前进程地址空间找到一个未映射的内存段 int (*check_flags)(int); //允许模块检查传递给fcntl(F_SETEL...)调用的标志 int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); //由VFS调用,将管道数据粘接到文件 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); //由VFS调用,将文件数据粘接到管道 int (*setlease)(struct file *, long, struct file_lock **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); };
大多数字符设备都会实现read(), write(), ioctl()函数。
下来来对本字符设备驱动中所使用的函数进行分析
1)read()和write(),读写功能的操作很像,这里只讨论读操作。
static ssize_t globalmem_read(struct file *filp, char __user *buf, //filp是文件结构体指针,buf是用户空间内存地址,size是要读的字节,ppos是读的位置相对文件开头的偏移地址 size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct globalmem_dev *dev = filp->private_data; //在单设备的时候,这里使用的 private 其实没有任何意义,可以直接使用 *dev = globalmem_devp 代替 //而在多设备的时候(例如tty0, tty1, tty2...),使用私有数据能够很好地保护数据的使用 //get the real length if(p >= GLOBALMEM_SIZE) //要读取的偏移地址长度 >= globalmem的总长时 return 0; if(count > GLOBALMEM_SIZE - p) //要读取的字节长度 >= 偏移地址到文件尾的长度(剩余长度) count = GLOBALMEM_SIZE - p; //只读出剩余的长度 //kernel space to the usr space if(copy_to_user(buf, (void *)(dev->mem + p),count)){ //copy_to_user是个有意思的函数,待会讨论,这里成功调用则返回0,失败返回count ret = - EFAULT; }else{ *ppos += count; //偏移指针移动count的长度 ret = count; //返回count printk(KERN_INFO "read %u byte(s) form %lu \n", count, p); } return ret; }
-------------------------------- copy_to_user()函数详解 --------------------------
/linux/arch/x86/lib/usercopy_32.c unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) //to是目标地址(用户空间),from是源地址(内核空间),n是拷贝的长度 { if (access_ok(VERIFY_WRITE, to, n)) n = __copy_to_user(to, from, n); //成功则返回0,失败返回拷贝的长度 return n; } EXPORT_SYMBOL(copy_to_user);
要说清楚这个copy_to_user()可不轻松呵
首先来看一个它的三个传入参数, (void __user *to, const void *from,unsigned long n),想对应为(buf, (void *)(dev->mem + p),count))
之前已经说了,设备驱动是运行在内核空间的,而我们对设备的操作是在用户空间的,这里就出现了一个内核空间到设备空间的切换问题。
//globalmem.c struct globalmem_dev{ struct cdev cdev; //cdev struct unsigned char mem[GLOBALMEM_SIZE]; //global memory }; //globalmem.c -- globalmem_read() struct globalmem_dev *dev = filp->private_data; //即为指向私有数据的指针,更直白地说 为 *dev = globalmem_devp if(copy_to_user(buf, (void *)(dev->mem + p),count))
在read()函数的调用中,struct globalmem_dev是属于设备的结构,存在于内核空间
buf属于read()的函数调用,属于用户空间,所以这个buf的类型就理所当然成为我们关注的重点了
/include/linux/compier.h # define __user __attribute__((noderef, address_space(1))) //__attribute__是GNU C中一个特色的机制 //具体可以参考http://www.unixwiz.net/techtips/gnu-c-attributes.html
__attribute__ 语法格式为:
__attribute__ ( ( attribute-list ) )
__attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)
1、函数属性(Function Attribute),函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。
GCC 需要 -Wall 来调用这些与 __attribute__ 相关的提示。
常用函数属性有:
1)format:使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。
format的格式为:
format ( archetype, string-index, first-to-check )
archetype:类型风格, allows assigning printf-like or scanf-like characteristics to the declared function
string-index:指定传入字符串的第几个参数是格式化参数, the number of the "format string" parameter
first-to-check:指定从第几个参数开始为变量,the number of the first variadic parameter
例如:
$ cat test.c 1 extern void eprintf(const char *format, ...) 2 __attribute__((format(printf, 1, 2))); 3 4 void foo() 5 { 6 eprintf("s=%s\n", 5); /* error on this line */ 7 8 eprintf("n=%d,%d,%d\n", 1, 2); /* error on this line */ 9 } $ cc -Wall -c test.c test.c: In function `foo': test.c:6: warning: format argument is not a pointer (arg 2) test.c:8: warning: too few arguments for format //unixwiz的例程 //这里是将printf重定义成eprintf,而eprintf(..., ..., ..., ...)中 //m = 1 ,表示第一个字符串为标准字符串 //n = 2 ,表示第二个字符开始为变量 //line 6 表示 输出参数格式错误 //line 8 表示 输出参数个数不匹配
若去掉了__attribute__((format(printf, 1, 2))),将不会报错。
2)noreturn:通知编译器函数从不返回值
例程:
$ cat test1.c extern void exitnow(); int foo(int n) { if ( n > 0 ) { exitnow(); /* control never reaches this point */ } else return 0; } $ cc -c -Wall test1.c test1.c: In function `foo': test1.c:9: warning: this function may return with or without a value
未声明noreturn将报错。
$ cat test2.c extern void exitnow() __attribute__((noreturn)); int foo(int n) { if ( n > 0 ) exitnow(); else return 0; } $ cc -c -Wall test2.c
这样将没有报错,__attribute__((noreturn))将错误屏蔽了。
3)const:只能用于带有数值类型参数的函数上,当重复调用带有数值参数的函数时,由于返回值是相同的,所以此时编译器可以进行优化处理,除第一次需要运算外, 其它只需要返回第一次的结果。
extern int square(int n) __attribute__((const)); ... for (i = 0; i < 100; i++ ) { total += square(5) + i; }
更多类型,可以参考http://gcc.gnu.org/onlinedocs/gcc-4.0.0/gcc/Function-Attributes.html
总的来说,__attribute__ 是一个编译器使用的校验一些特定情况的宏。
接着继续看回copy_to_user()
# define __user __attribute__((noderef, address_space(1))) # define __kernel __attribute__((address_space(0))) # define __iomem __attribute__((noderef, address_space(2))) //这里的nodefer means no deference //address_space(x) //x = 0 --> normal/kernel space ; x = 1 --> user space ; x = 2 --> device_map space //这里的结构体+小括号的使用,对结构体本身,不起任何作用,但是在调用sparase的时候能够进行校验 /* Linus Torvalds在一次演讲会上说: Basically you can add attributes to any kind of data type. In Sparse one of the attributes on data types is which address space it belongs to. This is a define that goes away if you don't use Sparse. So GCC, who doesn't know anything about address spaces, will never see any other code. So GCC treats the exact same code as it always used to do; but when you run it with a Sparse checker, it will notice that when you do a copy_to_user call, the first argument has to be a user pointer. Well, address_space (1), the tool itself doesn't really care about user or kernel; you can use it for anything. If it gets anything that isn't a user pointer, for example, if you switch the arguments around by mistake, which has happened, it will complain with a big fat warning saying "Hey, the address spaces don't match." 关于对sparse的的介绍和调用,可以看看Documentation/sparse.txt 基本上make C=1或make C=2就可以先调用sparse分析源文件 */
接着往下看access_ok(VERIFY_WRITE, to, n)
if (access_ok(VERIFY_WRITE, to, n)) n = __copy_to_user(to, from, n);
#define access_ok(type, addr, size) (likely(__range_not_ok(addr, size) == 0)) # define likely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 1)) # define unlikely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 0)) //__builtin_constant_p(x) 判断 x 是否为常数,若为常数则返回1,否则0 或 # define likely(x) __builtin_expect(!!(x), 1) # define unlikely(x) __builtin_expect(!!(x), 0) //实际上返回的就是 x 的值,只是进行了优化,具体可以google likely与unlikely详解 /* * Test whether a block of memory is a valid user space address. * Returns 0 if the range is valid, nonzero otherwise. * * This is equivalent to the following test: * (u33)addr + (u33)size > (u33)current->addr_limit.seg (u65 for x86_64) * * This needs 33-bit (65-bit for x86_64) arithmetic. We have a carry... */ #define __range_not_ok(addr, size) \ ({ \ unsigned long flag, roksum; \ __chk_user_ptr(addr); \ asm("add %3,%1 ; sbb %0,%0 ; cmp %1,%4 ; sbb $0,%0" \ : "=&r" (flag), "=r" (roksum) \ : "1" (addr), "g" ((long)(size)), \ "rm" (current_thread_info()->addr_limit.seg)); \ flag; \ }) //汇编,不解释,看注释就行了
接着到if内部的__copy_to_user()
static __always_inline unsigned long __must_check __copy_to_user(void __user *to, const void *from, unsigned long n) { might_fault(); return __copy_to_user_inatomic(to, from, n); } //__copy_to_user与copy_to_user返回的内容是一样的,复制成功则返回0 //其中可以关注的是这个might_fault() static inline void might_fault(void) { might_sleep(); } //其本质是一个might_sleep(), 表示该函数可休眠
看完了这些内部的函数,小总结一下copy_to_user()函数
1、为了给用户空间正确的赋值,定义 __user 为需校验的数据
2、copy_to_user的实际作用效果与__copy_to_user一致,其实其他的一些调用双下划线原函数的函数也有这样的特性,调用双下划线来实现本来已经实现的功能,但封装了一些访问和调用的安全函数,防止多线程系统中的竞争和并发
--------------------------------------------------------------------------------------------------------
弄透了copy_to_user后,其实整个read函数的脉络也清晰了。
1、取地址,然后所要读取的地址范围
2、从内核空间取出设备的数据到用户空间交给用户程序
之后的 write() 函数 和 copy_from_user() 函数的分析也可以依照这里的分析类推。
接着看一下seek()函数
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) //orig可以是 SEEK_SET(文件头) = 0, SEEK_CUR(当前位置) = 1, SEEK_END(文件尾) = 2 { loff_t ret = 0; switch(orig){ case 0: //文件头开始操作 if(offset < 0){ //偏移位置为负数,返回 出错 ret = - EINVAL; break; } if((unsigned int)offset > GLOBALMEM_SIZE){ //偏移位置大于设备空间长度,返回 出错 ret = - EINVAL; break; } filp->f_pos = (unsigned int)offset; //偏移指针变更位置 ret = filp->f_pos; break; case 1: //当前位置开始操作 if((filp->f_pos + offset) > GLOBALMEM_SIZE){ //偏移指针 + 存取长度(正数) > 设备空间长度,返回 出错 ret = - EINVAL; break; } if((filp->f_pos + offset) < 0){ //偏移指针 + 存取长度(负数) < 0 ,返回 出错 ret = - EINVAL; break; } filp->f_pos += offset; //偏移指针变更位置 ret = filp->f_pos; break; default: //此处不提供从文件尾操作的case情况 ret = - EINVAL; break; } return ret; }
接着讨论最后一个ioctl()函数
globalmem设备驱动的ioctl()函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据清0,对设备不支持的命令,ioctl()函数应该返回 -EINVAL
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg) { struct globalmem_dev *dev = filp->private_data; //get the cdev struct pointer switch(cmd){ case MEM_CLEAR: //清除全局内存,但实际上这是不值得提倡的方法 //这里的MEM_CLEAR = 0x01, 多设备的时候,若误发MEM_CLEAR命令,将清空错误的设备空间 memset(dev->mem, 0, GLOBALMEM_SIZE); printk(KERN_INFO "globalmem is set to zero\n"); break; default: return - EINVAL; } return 0; }
linux中建议以如下方式定义 ioctl 的控制宏
设备类型 序列号 方向 数据 尺寸
8bit 8bit 2bit 13/14bit
命令码的设备类型字段为一个“幻数”,可以是 0 ~ 0xFF 之间的值,内核的ioctl-number.txt给出了一些常用的已定义幻数。
命令码的方向字段为2位,该字段表示数据的传送方向。可能值如下
_IO an ioctl with no parameters _IOW an ioctl with write parameters (copy_from_user) _IOR an ioctl with read parameters (copy_to_user) _IOWR an ioctl with both write and read parameters. //分别为00, 01, 10, 11, //用的时候可以单独用也可以 相或 使用
关于命令码的数据尺寸,内核定义了_IO(), _IOR(), _IOW(), _IOWR来辅助生成命令。
至此,整个字符设备程序已经讲解的七七八八了。
从头再思考一边字符设备驱动程序,作为一个设备驱动工程师来说,需要完成的工作有
1、写字符设备结构体
2、根据file_operations 确定所要用到的字符设备功能
3、根据确定了的file_operations 写出相应的read(), write()等操作函数
4、写设备的加载与卸载函数
5、编写Makefile,编译模块,运行验证
接下来的工作,是自己再DIY一个字符设备,就完成了一个最基础的字符设备驱动程序了。
进阶而言,就要进一步了解字符设备的其他并发操作,进一步要求自己,便了解每一个部分的内核函数实现。
从而做到,知其然也知其所以然。