一个字符型设备的简单例子
一、概述
作为一名Android应用开发工程师,略懂些linux设备驱动的知识是有必要的,因为我们常见的Log框架和Binder框架都离不开底层驱动的支持。而考虑到Log和Binder又都是字符型设备,所以能够对字符型设备的知识有些了解,会对我们理解Android的框架有一定帮助。
从截图中可以看出:它们都是miscdevice,也是字符型设备。
本文通过一个非常简单的例子,希望能够帮助上层开发的同学对字符型设备的驱动有些许了解,这样以后我们研究Binder也会扫除一些障碍。这个例子包括两部分:一是在dev目录下创建一个叫hello_mycdev的字符型设备,这个设备有一个int的值;二是在system/bin目录下写一个可执行程序,类似grep这样的,可以去修改hello_mycdev这个设备内的value值。效果如下图所示:
在开始讲解例子之前,先说明两点:一、这会是个结构不完整、漏洞百出、有一堆bug的例子;二、请做驱动的同学就不要看了。
二、写驱动文件hello_cdev.c
#include <linux/cdev.h> #include <linux/fs.h> #include <linux/module.h> #include <linux/miscdevice.h> #include <linux/uaccess.h> //这里是设备名称,最终能够看到/dev/ hello_mycdev这样一个字符型设备文件 #define HELLO_CDEV_NAME "hello_mycdev" //自定义设备的结构体,它就代表了我们的设备,其中misc表明我们是一个miscdevice struct my_dev { struct miscdevice misc; int value; }; //自定义此设备的读函数和写函数,这里只是声明,定义在下面 static ssize_t my_cdev_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos); ssize_t my_cdev_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos); //任何一个设备都应该定义自己的file_operations,这里就相当于告诉内核 //我们这个设备的读函数和写函数分别是my_cdev_read和my_cdev_write, //当对此设备调用read和write时,就会自动调用这两个方法。 static struct file_operations my_cdev_fops = { .owner = THIS_MODULE, .read = my_cdev_read, .write = my_cdev_write, }; //声明一个我们这个设备的变量,这里对它初始化,包括前面定义的my_cdev_fops和设备名称HELLO_CDEV_NAME struct my_dev my_devp = { .misc = { .minor = MISC_DYNAMIC_MINOR, .name = HELLO_CDEV_NAME, .fops = &my_cdev_fops, .parent = NULL, }, .value = 0, }; //设备读函数的实现,非常简单,只是把value的值读出而已 static ssize_t my_cdev_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos) { copy_to_user(buf, &(my_devp.value), sizeof(my_devp.value)); } //设备写函数的实现,也非常简单,只是写入value的值而已 ssize_t my_cdev_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) { copy_from_user(&(my_devp.value), buf, count); } //初始化我们的设备,这里只做了两件事,一是调用系统方法注册misc设备,而是为value赋初值 static int __init my_cdev_init(void) { int ret; ret = misc_register(&(my_devp.misc)); my_devp.value = 6; //我们会在后面看到这条log的 printk(KERN_ERR "yutao--init ok!"); return 0; } //为我们的模块指明初始化方法 module_init(my_cdev_init);
三、编译驱动文件
首先,我们将前面的文件命名为hello_cdev.c,然后我们再写一个Makefile文件,文件内容如下(只有一句):obj-m := hello_cdev.o。
然后,我们需要把这两个文件放到一个目录下,命名该目录为hello_cdev。将这个目录放到代码中:alps/kernel/drivers/(我的代码是MTK平台)。
之后单编此模块,编译命令:./mk r kkernel/drivers/hello_cdev。
最后,我们能够在alps/kernel/out/drivers/hello_cdev目录下生成文件hello_cdev.ko,把它push到手机的/system/lib/modules目录下,shell进入到此目录,执行insmodhello_cdev.ko就可以把我们这个模块安装到手机中了。你要验证我们的设备是否ok,可以看看手机的/dev目录下是否已经有了hello_mycdev这个文件,当然你也可以查看代码里那条log是否输出:
这就能确定我们的模块已经成功安装了(insmod—猜测是install module的缩写,安装模块;rmmod—猜测是remove module,作用是卸载模块;lsmod—猜测是ls module,可以查看当前安装了哪些模块)。
四、写一个可执行程序,来读取和写入设备的value值(hello.c)
这个程序非常简单,就是我们常见的操作文件的方法:先open、然后read或write,然后close,代码都没有注释的必要:
为了单编模块,我们还需要一个Android.mk文件,这个文件要注意的是最后一句,因为我们需要编译生成的是一个可执行文件。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #define DEVICE_NAME "/dev/hello_mycdev" int main(int argc, char** argv) { int fd = -1; int val = 0; fd = open(DEVICE_NAME, O_RDWR); printf("Read original value: "); read(fd, &val, sizeof(val)); printf("%d.\n", val); printf("Please input the value you want to set: "); scanf("%d",&val); printf("\n"); write(fd, &val, sizeof(val)); printf("Read the value again:\n"); read(fd, &val, sizeof(val)); printf("%d.\n\n", val); close(fd); return 0; }
单编模块:./mk mm external/hello/就会在out相关目录下的system/bin下生成了一个hello文件,把它push到手机的/system/bin目录下,然后shell进入手机,直接输入hello命令,就可以像我之前描述的那样查看和修改我们那个设备的value值了。
五、小结
Linux驱动对于我来说是非常庞大和复杂的内容,好在我并不需要了解它的全貌,学习它只是为了帮助我在学习Android框架时少一些障碍。希望这对和我一样的同学有些许帮助。也希望驱动的同事能够写一些通俗易懂的技术分享,非常感谢!