Linux/Android系统知识之Linux入门篇--编写Linux驱动
2017-07-30 16:23
483 查看
前言
由于通用性强,就业面广,源码免费等原因,Linux近些年火遍了大江南北,大到云服务器,小到路由器,无处不见Linux的身影。知乎上各种linux书籍推荐的也是琳琅满目,《ldd3》、《内线源代码情景分析》、《深入理解Linux内核》等等,让有选择困难症的朋友犯了难。学习Linux的朋友,首先必须要建立这样一个观念:学习Linux驱动和学习Linux内核是两码事情,Linux内核提供了各种现成的接口供驱动开发者直接使用,驱动编写者只需学习linux驱动运作的规则和概念,便可编写自己的驱动用例了。
这就好比在Windows上开始写第一个”hello world程序”时,书上也只是教我们先敲入如下代码:
#include<stdio.h> void main() { printf("hello world\n"); }
然后点击Visio studio的编译按钮即可编译运行生成的exe文件一样,Linux driver编写只要遵循它的条条框框,便可写出我们的第一个驱动代码了。
编写第一个驱动文件
#include <linux/init.h> #include <linux/sched.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/ioctl.h> static int __init hello_init(void) { printk("hello, init\n"); return 0; } static void __exit hello_exit(void) { printk("hello, exit\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("happybevis");
上面是我们的第一个linux驱动代码
#include \
#define KERN_EMERG "<0>" /* system is unusable */ #define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */ #define KERN_ERR "<3>" /* error conditions */ #define KERN_WARNING "<4>" /* warning conditions */ #define KERN_NOTICE "<5>" /* normal but significant condition */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */
printk函数中可以加入打印机级别的宏,该例中没加,系统会自动为其补上“KERN_WARNING”的级别。所以我们也可以这样使用printk函数:printk(KERN_INFO “Hello, world!\n”);
要查看我们前面打印的log,需要使用命令行中的dmesg命令,该命令将会打印所以使用printk函数丢出的kernel log。
编译驱动代码
写完了驱动代码,我们应该如何去编译和使用呢?如果你使用的是ubuntu等操作系统,请先使用uname命令查看内核版本
deployer@iZ28v0x9rjtZ:~$ uname -r 3.2.0-67-generi 4000 c
这里本地电脑的内核版本是3.2.0,linux based操作系统会在“/lib/modules/(内核版本号)/build”下提供本机内核模块所需的所有编译环境。
若有朋友想进行移植或更深入的了解内核运行原理,可以在Linux官网下载到想要进行研究或移植的Linux对应版本源码了,这些源码通过与目标平台对应交叉编译工具,可自行完整编译移植。
源码下载官网地址为Kernel,想查找对应版本的源码可到如下对应文件夹中对应查找即可例如3.2源码下载.
首先编写我们的Makefile(编译规则文件),路径大家可以随意,我是放在~/kernel_test/test目录。
obj-m := hello.o PWD := $(shell pwd) KVER := $(shell uname -r) KDIR := /lib/modules/$(filter-out ' ',$(KVER))/build all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
特别注意:all和clean语句的下一句一定要以tab键开头,否则Makefile语法会报错。
将前面编写的hello.c文件也拷贝到test文件夹中,直接调用make命令开始编译:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ make make -C /lib/modules/3.2.0-67-generic/build M=/home/deployer/kernel_test/test modules make[1]: Entering directory `/usr/src/linux-headers-3.2.0-67-generic' Building modules, stage 2. MODPOST 0 modules make[1]: Leaving directory `/usr/src/linux-headers-3.2.0-67-generic'
编译成功。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ls hello.c hello.ko hello.mod.c hello.mod.o hello.o Makefile modules.order Module.symvers
生成了一大堆文件,我们真正需要的是hello.ko,即内核模块,也就是我们的驱动模块啦~
安装和卸载和查看内核模块需要用到如下三个命令:
lsmod:查看目前系统中安装过的内核模块。
insmod:安装内核模块。
rmmod:卸载内核模块。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ lsmod Module Size Used by joydev 17693 0 xen_kbdfront 12797 0 fb_sys_fops 12703 0 sysimgblt 12806 0 sysfillrect 12901 0 syscopyarea 12633 0 usbhid 47238 0 hid 99883 1 usbhid i2c_piix4 13301 0 psmouse 98051 0 serio_raw 13211 0 mac_hid 13253 0 lp 17799 0 parport 46562 1 lp xenfs 18311 1 floppy 70207 0
我们开始安装内核驱动:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ insmod hello.ko insmod: error inserting 'hello.ko': -1 Operation not permitted deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo insmod hello.ko
因为驱动代码运行在内核空间,若编写过程引入重大bug,有可能导致Linux内核crash,操作系统重启。所以安装和卸载内核代码均需要管理员权限。有兴趣的朋友可以在虚拟机中故意编写一个带空指针代码的驱动,看看效果怎样。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg |grep -i hello [2908464.238822] hello, init
可以看到,我们的hello_init函数被成功调用。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ lsmod Module Size Used by hello 12425 0 joydev 17693 0 xen_kbdfront 12797 0 fb_sys_fops 12703 0 sysimgblt 12806 0 sysfillrect 12901 0 syscopyarea 12633 0 usbhid 47238 0 hid 99883 1 usbhid i2c_piix4 13301 0 psmouse 98051 0 serio_raw 13211 0 mac_hid 13253 0 lp 17799 0 parport 46562 1 lp xenfs 18311 1 floppy 70207 0
内核模块也已成功加载。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo rmmod hello.ko
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg |grep -i hello [2908464.238822] hello, init
[2908491.639216] hello, exit
卸载我们的驱动后,hello_exit函数也被成功调用。由此相信大家对内核驱动的调用过程有了较深刻的理解。
与内核驱动进行交互
前面编写的驱动代码,本质上只是打印了几个log,并无其他实质性操作。作为一个”可用”的驱动,一般需要提供对外接口以供同外界进行数据交互和控制。在Linux系统中,一切皆文件。内核驱动可以通过调用一些的函数,让操作系统建立一些文件节点,这些文件节点在用户看来完全就是一个普通文件,我们通过对该特殊文件的读写和ioctl操作即可实现数据的传递了。
deployer@iZ28v0x9rjtZ:~$ ll drwxr-xr-x 2 root root 60 Jun 26 00:08 cpu/ crw------- 1 root root 1, 11 Jun 26 00:08 kmsg -rw-r--r-- 1 root root 156 Jul 30 06:00 run_jobs.log brw-rw---- 1 root disk 7, 7 Jun 26 00:08 /dev/loop7 ...
我们可以通过ls -l命令看出一个文件是否为真实文件,只需要关注第一个字符:“c” –> 字符设备驱动(char);“b” –> 块设备驱动(block);“d” –> 普通目录(directory); “ - ” –> 普通的文件(file).
我们常用如下四种方式创建文件节点以供交互:
proc 常用来制作简易的参数查看节点,由于sys文件系统功能与之十分类似且sys更新更灵活,所以近来proc使用的越来越少。
sys 常用来和控制驱动的运行参数。
dev 。
debugfs (该方式一般用来提供debug调试接口,正常的驱动不太会提供,所以会跳过其讲解,有兴趣的朋友请再自行学习)
deployer@iZ28v0x9rjtZ:~$ ls / bin dev home lib lost+found mnt proc run selinux sys usr vmlinuz boot etc initrd.img lib64 media opt root sbin srv tmp var
查看系统根目录,很容易看到sys、proc、dev三个文件夹,分别调用前面所讲的三种创建文件系统的函数,操作系统便会帮我们分别在如下三个文件夹中建立对应的文件节点了。
建立dev文件文件节点
在我们已经写好的驱动文件中稍做修改:#include <linux/init.h> #include <linux/sched.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/ioctl.h> #include <linux/miscdevice.h> #include <linux/uaccess.h> int hello_open(struct inode *inode, struct file *filp) { printk("hello open!\n"); return 0; } int hello_release(struct inode *inode, struct file *filp) { printk("hello release!\n"); return 0; } ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos) { printk("hello read!\n"); return 0; } ssize_t hello_write(struct file *filp, char __user *buf, size_t count, loff_t *fpos) { printk("hello write!\n"); return 0; } int hello_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) { printk("hello ioctl!\n"); printk("cmd:%d arg:%d\n", cmd, arg); return 0; } struct file_operations fops = { .owner = THIS_MODULE, .open = hello_open, .release = hello_release, .write = hello_write, .read = hello_read, .unlocked_ioctl = hello_ioctl }; struct miscdevice dev = { .minor = MISC_DYNAMIC_MINOR, .fops = &fops, .name = "hello_device" }; int setup_hello_device(void) { return misc_register(&dev); } stati 10c52 c int __init hello_init(void) { printk("hello, init\n"); return setup_hello_device(); return 0; } static void __exit hello_exit(void) { printk("hello, exit\n"); misc_deregister(&dev); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("happybevis");
接下来make driver模块后,安装我们的驱动模块。
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ls /dev/h* hello_device hidraw0 hpet
可以看到dev目录下产生了我们建立的”hello_device”文件节点了~
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg |grep -i hello [2980042.371331] hello, init
接下来我们像读取文件一下读一下驱动节点内容
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo cat /dev/hello_device
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ dmesg |grep -i hello [2980042.371331] hello, init
[2980380.028918] hello open!
[2980380.028935] hello read!
[2980380.028938] hello release!
可见cat命令会帮我们做open、read、close三种操作。由于我们在kernel的read函数中只是打印了一个log,并没有真正传递数据,所以cat不到内容。
接下来我们再稍作修改,在驱动的read函数中返回一个字符串给用户(文件读取者)
ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos) { char *hellobuff = "hello world\n" ; loff_t position = *pos; if (position >= 15) { count = 0; goto out; } if (count > ( 15 - position )) count = 15 - position; position += count; printk("hello read!\n"); if( copy_to_user( buf, hellobuff + *pos , count ) ){ count = -EFAULT; goto out; } *pos = position; out: return count; }
重新make install,测试我们的功能:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo cat /dev/hello_device hello world
测试成功,这样一来kernel和用户之间就可以通过文件读取和写入来传递信息了。用户空间和kernel空间的数据传递需要用到copy_to_user和copy_from_user函数,前者在driver的read函数中,将数据传给用户,后者用着write中获取用户传入数据。
前面我们使用的是cat命令读取driver数据,当然用echo “xxx”>/dev/hello_device就可以向driver传入数据了。除此之外,我们当然可以自己写linux的应用程序,程序中只需要调用正常的fopen、fread、fwrite、fclose函数组(open、rea的、write、close亦可)同样可以和driver打交道了。
测试程序如下: #include <fcntl.h> #include <stdio.h> char temp[64]={0}; int main( void ) { int fd,len; fd = open("/dev/hello_device",O_RDWR); if(fd<0) { perror("open fail \n"); return ; } len=read(fd,temp,sizeof(temp)); printf("len=%d,%s \n",len,temp); close(fd); }
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ gcc test.c -o test deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ./test open fail : Permission denied deployer@iZ28v0x9rjtZ:~/kernel_test/test$ sudo ./test len=15,hello world
可以看到我们的driver内容就正常被读取到啦。
除了read、write的文件操作外linux应用程序中还有ioctl操作,具体细节可以使用man ioctl命令查看,我们只需在代码中加入ioctl( fd, para, &returnbuff );就可以同我们driver中的hello_ioctl函数进行交互了。
正常情况下由于我们自定义的ioctl para会比较多,所以一般driver中写法如下:
static long hello_ioctl(struct file *filep, unsigned int cmd, unsigned long arg) { int err = 0; int returnbuff = 0 ; printk("hello ioctl!\n"); printk("cmd:%d arg:%d\n", cmd, arg); switch(cmd) { case PARA1: printk("case: PARA1/n"); // do something ... break; case PARA2: printk("case: PARA2/n"); err = copy_to_user((int *)arg, &returnbuff, sizeof(int)); break; case PARA3: printk("case: PARA3/n"); err = copy_from_user(&returnbuff,(int *)arg, sizeof(int)); break; default: printk("case: default/n"); err = ENOTSUPP; break; } return err; }
所以ioctl比read write来讲会更加灵活多变,代码结构相对也会清晰一些。
下面我们简单总结一下,用户程序(shell命令或linux应用程序)可以通过常规的文件操作方式,通过驱动创建的文件节点进行交互。交互的方式一般是通过read、write、ioctl(dev文件节点才支援)来进行数据交换。如此一来kernel和现实世界就可以互相协作,互相沟通了~!
建立proc文件文件节点
proc文件节点由于历史原因,在较早kernel版本中的使用方法和新版使用方式不同且互不兼容。由于旧版使用的地方已经很少,所以我们将要研究的是新用法。继续在我们driver的基础上加入proc文件节点相关代码:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ git diff hello.c diff --git a/hello.c b/hello.c index 9fda5e2..8a4d32a 100644 --- a/hello.c +++ b/hello.c @@ -8,7 +8,9 @@ #include <linux/ioctl.h> #include <linux/miscdevice.h> #include <linux/uaccess.h> #include <linux/slab.h> +#include <linux/proc_fs.h> +#include <linux/seq_file.h> int hello_open(struct inode *inode, struct file *filp) { @@ -60,8 +62,26 @@ printk("hello ioctl!\n"); printk("cmd:%d arg:%d\n", cmd, arg); return 0; } + + static int hello_proc_show(struct seq_file *m, void *v) + { + seq_printf(m, "hello world\n"); + return 0; + } + + static int hello_proc_open(struct inode *inode, struct file *file) + { + return single_open(file, hello_proc_show, NULL); + } + + static const struct file_operations hello_proc_fops = { + .owner = THIS_MODULE, + .open = hello_proc_open, + .read = seq_read, + .llseek = seq_lseek, + .release = seq_release, + }; struct file_operations fops = { @@ -82,7 +102,8 @@ int setup_hello_device(void) { + if (!proc_create("hello_proc", 0, NULL, &hello_proc_fops)) + return -ENOMEM; return misc_register(&dev); } @@ -97,6 +118,7 @@ { printk("hello, exit\n"); misc_deregister(&dev); + remove_proc_entry("hello_proc", NULL); } module_init(hello_init);
编译安装我们的内核驱动,如果代码没问题的话,应该可以看到如下节点长出~
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ll /proc/hello_proc -r--r--r-- 1 root root 0 Jul 30 15:02 /proc/hello_proc
我们读取该节点资讯如下:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ cat /proc/hello_proc hello world
需要说明一点,seq相关的操作是内核提供的操作,目的是为了让proc文件节点能够突出更多地内容,旧版proc的操作接口很容易因开发人员使用不当导致交换的数据量大于copy_to_user所能支援的内存大小(若没记错应为一个page大小)。对proc的使用者来说,直接套用如上框架即可保证使用的安全。
另外需要特别说明一点,proc文件操作结构中的.release = seq_release 域一定不要漏掉,否则每打开一次改proc节点就会产生一些内存泄漏,虽然泄漏量很少,积少成多内存迟早会爆掉。
建立sys文件文件节点
有了前面的经验,很容易就想到sys fs文件节点的操作方式也大同小异,一般我们通过对sysfs节点的cat和echo操作,查看或改变driver的一些全局变量,从而对driver的状况进行监控,对driver的行为进行动态修改。接下来我们先在sys目录下简历一个名为hello_sysfs_dir的文件夹,在其中建立一个可供读写的节点hello_node。
活不多说,说走就走。
在已经完成的proc driver基础上,我们记忆添加sys fs操作节点 diff --git a/hello.c b/hello.c index 8a4d32a..f61fafe 100644 --- a/hello.c +++ b/hello.c @@ -11,6 +11,7 @@ #include <linux/slab.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> +#include <linux/kobject.h> int hello_open(struct inode *inode, struct file *filp) { @@ -63,6 +64,27 @@ printk("cmd:%d arg:%d\n", cmd, arg); return 0; } + + char buff[512] = {0} ; + int in_count = 0; + + static ssize_t hello_sysfs_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf + { + //return sprintf(buf, "%s\n", "hello sysfs read\n"); + return sprintf(buf, "[hello sysfs write] user data: length=0x%X,buff=%s\n",in_count + } + + static ssize_t hello_sysfs_store(struct kobject *kobj, struct kobj_attribute *attr, const ch + { + printk("[hello sysfs write] user data: length=0x%X,buff=%s\n",count,buf); + in_count = count ; + strncpy(buff, buf , 512); + if(count) + return count; + else + return 1 ; + } + static int hello_proc_show(struct seq_file *m, void *v) { @@ -98,12 +120,41 @@ .minor = MISC_DYNAMIC_MINOR, .fops = &fops, .name = "hello_device" }; + +static struct kobj_attribute hello_attr = + __ATTR(hello_node, 0666, hello_sysfs_show, hello_sysfs_store); + +static struct attribute *attrs [] = { + &hello_attr.attr, + NULL, + }; + +static struct attribute_group hello_attr_group = { + .attrs = attrs, + }; +struct kobject *dir = NULL; + + int setup_hello_device(void) { + int retval = 0 ; + //--------proc fs part-------------- if (!proc_create("hello_proc", 0, NULL, &hello_proc_fops)) return -ENOMEM; + + //--------sys fs part ---------------- + dir = kobject_create_and_add("hello_sysfs_dir", NULL); + if (!dir) + return -ENOSYS; + + retval = sysfs_create_group(dir, &hello_attr_group); + if (retval) + kobject_put(dir); + + + //---------create a device(dev fs) part------------ return misc_register(&dev); } @@ -119,6 +170,7 @@ printk("hello, exit\n"); misc_deregister(&dev); remove_proc_entry("hello_proc", NULL); + kobject_put(dir); } module_init(hello_init);
这样一来我们向该节点写入的字串,就可以通过cat的方式读出验证了。
马上编译安装试试效果:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ll /sys/ total 4 drwxr-xr-x 14 root root 0 Jul 30 2017 ./ drwxr-xr-x 23 root root 4096 Mar 9 2015 ../ drwxr-xr-x 2 root root 0 Jul 30 2017 block/ drwxr-xr-x 21 root root 0 Jul 30 2017 bus/ drwxr-xr-x 44 root root 0 Jul 30 2017 class/ drwxr-xr-x 4 root root 0 Jul 30 2017 dev/ drwxr-xr-x 18 root root 0 Jul 30 2017 devices/ drwxr-xr-x 4 root root 0 Jul 30 15:48 firmware/ drwxr-xr-x 6 root root 0 Jul 30 15:48 fs/ drwxr-xr-x 2 root root 0 Jul 30 15:59 hello_sysfs_dir/ ...
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ ll /sys/hello_sysfs_dir/ total 0 drwxr-xr-x 2 root root 0 Jul 30 15:59 ./ drwxr-xr-x 14 root root 0 Jul 30 2017 ../ -rw-rw-rw- 1 root root 4096 Jul 30 15:59 hello_node
sysfs的文件节点完美的涨了出来,赶紧试试咱的功能:
deployer@iZ28v0x9rjtZ:~/kernel_test/test$ echo "how are you" >/sys/hello_sysfs_dir/hello_node deployer@iZ28v0x9rjtZ:~/kernel_test/test$ cat /sys/hello_sysfs_dir/hello_node [hello sysfs write] user data: length=0xC,buff=how are you
结果如预期,大功告成!
小结
Linux驱动和用户打交道的最主要方式,就是是其所建立各种文件节点(netlink etc..),本文以三种文件节点的创建方式为主线,让大家对Linux中一切皆为文件的思想有更直观的认识。 本文的所有细节都是一步步修改运行并返回的结果,所以建议大家有条件的话一定要完全按照文章步骤做一遍,看再多也不如亲自做一遍领悟的更多。driver是一门大学科,详细的知识细节想必要一本书才能讲得完,篇幅有限,仅本文中内容已有许多细节特意没有展开,读者若有疑惑或建议,欢迎一起讨论完善。
相关文章推荐
- Linux/Android系统知识之Linux入门篇--学习使用命令行
- Linux驱动--为Ubuntu系统编写驱动程序入门
- android驱动之旅-在Ubuntu上为Android系统编写Linux内核驱动程序(3)
- ANDROID入门1:在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 为Android系统编写Linux内核驱动程序
- (一)在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 【Linux入门基础知识】Linux 脚本编写基础
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 转载.在Ubuntu上为Android系统编写Linux内核驱动程序
- android系统PS2全键盘驱动(上)-使用linux的标准接口实现
- 编写在Android的Linux系统中直接运行的可执行程序 - 检测CPU能力
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序
- 在Ubuntu上为Android系统编写Linux内核驱动程序