基于4.14内核,运行在beagleBone greenhtml
在上一讲中,咱们写了第一个linux设备驱动程序——hello_world,在驱动程序中,咱们什么也没有作,仅仅是打印了两条日志消息,今天,咱们就要丰富这个设备驱动程序,在/dev目录下建立一个设备节点,用户经过读写文件来与内核进行交互。node
在linux中,一切皆文件,无论用户是控制某个外设又或者是操做I/O,都是经过文件实现。react
设备驱动程序被装载在内核中运行,当用户程序须要使用对应设备时,天然不可能直接访问内核空间,那么用户程序应该怎么作呢?linux
答案是内核将设备驱动程序操做接口以文件接口的形式导出到用户空间,通常为相应的设备在/dev目录下创建相应的操做接口文件,自linux2.6内核版本以来,内核还会在系统启动时建立sysfs文件系统,内核一样能够将设备操做接口导出到/sys目录下。shell
举个例子:在开发一款温度传感器时,内核驱动模块能够在驱动程序中实现传感器的初始化,而后在/dev目录下建立对应文件,关联/dev的读写回调函数,当用户访问/dev下相应文件时,就会调用相应的回调函数,执行设备的操做。数组
下面咱们就演示如何在/dev目录下建立一个设备节点。安全
#include <linux/init.h> #include <linux/module.h> #include <linux/device.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/uaccess.h> MODULE_AUTHOR("Downey"); MODULE_LICENSE("GPL"); static int majorNumber = 0; /*Class 名称,对应/sys/class/下的目录名称*/ static const char *CLASS_NAME = "basic_class"; /*Device 名称,对应/dev下的目录名称*/ static const char *DEVICE_NAME = "basic_demo"; static int basic_open(struct inode *node, struct file *file); static ssize_t basic_read(struct file *file,char *buf, size_t len,loff_t *offset); static ssize_t basic_write(struct file *file,const char *buf,size_t len,loff_t* offset); static int basic_release(struct inode *node,struct file *file); static char msg[] = "Downey!"; static char recv_msg[20]; static struct class *basic_class = NULL; static struct device *basic_device = NULL; /*File opertion 结构体,咱们经过这个结构体创建应用程序到内核之间操做的映射*/ static struct file_operations file_oprts = { .open = basic_open, .read = basic_read, .write = basic_write, .release = basic_release, }; static int __init basic_init(void) { printk(KERN_ALERT "Driver init\r\n"); /*注册一个新的字符设备,返回主设备号*/ majorNumber = register_chrdev(0,DEVICE_NAME,&file_oprts); if(majorNumber < 0 ){ printk(KERN_ALERT "Register failed!!\r\n"); return majorNumber; } printk(KERN_ALERT "Registe success,major number is %d\r\n",majorNumber); /*以CLASS_NAME建立一个class结构,这个动做将会在/sys/class目录建立一个名为CLASS_NAME的目录*/ basic_class = class_create(THIS_MODULE,CLASS_NAME); if(IS_ERR(basic_class)) { unregister_chrdev(majorNumber,DEVICE_NAME); return PTR_ERR(basic_class); } /*以DEVICE_NAME为名,参考/sys/class/CLASS_NAME在/dev目录下建立一个设备:/dev/DEVICE_NAME*/ basic_device = device_create(basic_class,NULL,MKDEV(majorNumber,0),NULL,DEVICE_NAME); if(IS_ERR(basic_device)) { class_destroy(basic_class); unregister_chrdev(majorNumber,DEVICE_NAME); return PTR_ERR(basic_device); } printk(KERN_ALERT "Basic device init success!!\r\n"); return 0; } /*当用户打开这个设备文件时,调用这个函数*/ static int basic_open(struct inode *node, struct file *file) { printk(KERN_ALERT "Open file\r\n"); return 0; } /*当用户试图从设备空间读取数据时,调用这个函数*/ static ssize_t basic_read(struct file *file,char *buf, size_t len,loff_t *offset) { int cnt = 0; /*将内核空间的数据copy到用户空间*/ cnt = copy_to_user(buf,msg,sizeof(msg)); if(0 == cnt){ printk(KERN_INFO "Send file!!"); return 0; } else{ printk(KERN_ALERT "ERROR occur when reading!!"); return -EFAULT; } return sizeof(msg); } /*当用户往设备文件写数据时,调用这个函数*/ static ssize_t basic_write(struct file *file,const char *buf,size_t len,loff_t *offset) { /*将用户空间的数据copy到内核空间*/ int cnt = copy_from_user(recv_msg,buf,len); if(0 == cnt){ printk(KERN_INFO "Recieve file!!"); } else{ printk(KERN_ALERT "ERROR occur when writing!!"); return -EFAULT; } printk(KERN_INFO "Recive data ,len = %s",recv_msg); return len; } /*当用户打开设备文件时,调用这个函数*/ static int basic_release(struct inode *node,struct file *file) { printk(KERN_INFO "Release!!"); return 0; } /*销毁注册的全部资源,卸载模块,这是保持linux内核稳定的重要一步*/ static void __exit basic_exit(void) { device_destroy(basic_class,MKDEV(majorNumber,0)); class_unregister(basic_class); class_destroy(basic_class); unregister_chrdev(majorNumber,DEVICE_NAME); } module_init(basic_init); module_exit(basic_exit);
看程序固然是要从入口函数开始,咱们将目光投入到basic_init函数:函数
basic_class = class_create(THIS_MODULE,CLASS_NAME);调用这个函数建立一个class,同时在/sys/class目录下建立一个目录,做为当前设备的描述信息。ui
驱动使用register_chrdev()函数在内核中注册一个设备节点,同时将初始化的file_operation结构体注册进去,内核会维护一个file_operation结构体集合,注册一个file_operation结构体而且返回设备号,在这里将设备号和结构体相关联。操作系统
在用户空间使用device_create()在/dev目录下建立新的设备节点,可是在这个目录建立时并无关联相应的file_operation结构体,那咱们在对设备节点进行read,write操做时,是怎么调用相应的file_operation结构体中的接口的呢?
答案是经过设备号,内核维护file_operation结构体数组,而且将其与设备号进行关联,另外一方面,在/dev下建立设备节点时,将设备节点与设备号进行关联。
因此在操做设备节点时,能够获得设备节点关联的设备号,再经过设备节点找到相应的file_operation结构体,再调用结构体中相应的函数,执行完毕返回到用户空间。
因此当模块加载完成后,整个过程是这样的:
用户打开/dev/DEVICE_NAME设备产生系统调用 ->系统找到设备节点对应的设备号 ->经过设备号找到内核维护的file_operation结构体集合中对应的结构体 ->因为初始化时指定了.open = basic_open,调用basic_open函数 返回到用户空间 用户的下一步操做...
须要注意的是,用户的read会最终会触发调用file_operation结构体中的相应read函数,前提是咱们进行了相应的赋值:.read = basic_read,可是用户的read函数的返回值并不是就是file_operation结构体中basic_read的返回值,从用户read /dev下的文件到触发调用file_operation结构体中.read还有一些中间过程。
linux kernel是操做系统的核心,掌握着整个系统的运行和硬件资源的分配。
因此为了安全考虑,linux的内存空间被划分为用户空间和内核空间,而若是用户空间须要使用到内某些由核掌控的资源,就必须提出申请,这个申请就是产生系统调用,遵循一些内核指定的接口来访问内核资源。
将用户空间和内核空间进行隔离可以保障内核的安全,由于用户进程的任何行为都由内核最终把控,出现问题就直接结束进程。而内核一旦出现问题,很大可能直接致使死机或者产生一些不可预期的行为,这是咱们不肯意看到的。
既然进行了隔离,那么用户进程和内核之间的数据交换就得经过专门的接口而非随意的指针相互访问,这两个接口就是copy_to_user()和copy_from_user()。
顾名思义copy_to_user()就是将内核数据copy到用户空间,copy_from_user()就是将用户数据copy到内核中,这两种行为都由内核管理。
这两个接口主要作了两件事:
编译依旧延续上一章节的操做,修改Makefile:
obj-m+=create_device_node.o all: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
编译完成以后加载模块:
sudo insmod create_dev_node.ko
而后使用lsmod命令进行检查,若是一切正常,咱们就能够在/dev目录下看到咱们新建立的设备节点:/dev/$DEVICE_NAME。
加载内核模块成功以后,接下来的事情就是在用户空间操做设备节点了,咱们尝试着打开设备节点,而后对其进行读写:
create_device_node_user.c:
#include <stdio.h> #include <stdlib.h> #include <error.h> #include <fcntl.h> #include <string.h> #include <unistd.h> static char buf[256] = {1}; int main(int argc,char *argv[]) { int fd = open("/dev/basic_demo",O_RDWR); if(fd < 0) { perror("Open file failed!!!\r\n"); } int ret = write(fd,"huangdao!",strlen("huangdao!")); if(ret < 0){ perror("Failed to write!!"); } ret = read(fd,buf,7); if(ret < 0){ perror("Read failed!!"); } else { printf("recv data = %s \n",buf); } close(fd); return 0; }
在上面的代码示例中,建立了/dev/basic_demo这个设备节点,在user程序中,先打开/dev/basic_demo设备,而后写字符串"huangdao",写完以后而后读取7个字节。
在上面的设备程序中实现:read将调用basic_read()函数,而write将调用basic_write()函数,在basic_write()函数中,接收用户空间的数据并打印出来,而在basic_read()中,将msg中的信息返回到用户空间。
编译很简单:
gcc create_device_node_user.c -o user
运行:
sudo ./user
内核端的log输出:
Dec 20 14:12:55 beaglebone kernel: [ 433.070666] Open file
Dec 20 14:12:55 beaglebone kernel: [ 433.073308] Recieve file!!
Dec 20 14:12:55 beaglebone kernel: [ 433.073318] Recive data ,len = huangdao!
Dec 20 14:12:55 beaglebone kernel: [ 433.073335] Send file!!
用户端的log输出:
recv data = Downey!
在加载完上述设备驱动程序以后,咱们能够看到生成了一个新设备/dev/basic_demo,查看这个设备节点的权限:
ls -l /dev/basic_demo
输出:
crw------- 1 root root 241, 0 Dec 20 14:10 /dev/basic_demo
发现权限是只有root用户才能读写,因此在上面执行用户程序访问设备节点时,咱们必须加上sudo以超级用户执行程序,既然设备驱动程序的节点服务于普通用户,那么普通用户若是没有权限访问,那岂不是白瞎。因此咱们要修改设备节点的属性。
第一个办法,固然是最简单粗暴的,使用root权限直接修改:
sudo chmod 666 /dev/basic_demo
这种办法是能够完成修改的,并且也达到了用户可访问的目的,优势就是简单。
可是可别忘了,内核模块具备可动态加载卸载的属性,若是咱们每一次加载模块以后都要以root权限从新去设置一次设备节点权限,在linux严格的权限管理系统下,在某些场景这并不合适。
第二个办法:使用系统提供的方式:
首先,咱们可使用udevadm info -a -p /sys/class/$CLASS_NAME/$DEVICE_NAME来查看模块信息:
udevadm info -a -p /sys/class/basic_class/basic_demo
输出:
looking at device '/devices/virtual/basic_class/basic_demo':
KERNEL=="basic_demo"
SUBSYSTEM=="basic_class"
DRIVER==""
在这里能够看到模块的设备名(KERNEL),class名称(SUBSYSTEM),这里只是一个查询做用,获取相应信息,若是能记住就能够省略这一步骤。
tips:
在上述的驱动程序中,咱们使用class_create()建立了一个设备节点,在/sys/class目录下生成了相应的以$CLASS_NAME为名称的目录,这个目录下存放着模块信息。
填充文件名:
KERNEL=="basic_demo", SUBSYSTEM=="basic_class", MODE="0666"
完成以上操做,再加载模块时,咱们就能够查看设备节点信息了:
ls -l /dev/basic_demo
结果显示:
crw-rw-rw- 1 root root 241, 0 Dec 20 14:37 /dev/basic_demo
表示权限修改完成,通过此次修改,每次加载完模块,生成的设备节点文件都是.rules文件中指定的访问权限了。
关于/etc/udev/下.rules文件规则请参考
好了,关于linux设备驱动中建立设备节点的讨论就到此为止啦,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言
原创博客,转载请注明出处!
祝各位早日实现项目丛中过,bug不沾身.