以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
一、细节提要
1、与用户与内核数据交换有关的函数
(1)copy_from_user()函数
该将数据从用户空间复制到内核空间。
如果成功复制则返回0,如果不成功复制则返回尚未成功复制的剩下的字节数。
(2)copy_to_user()函数
将数据从内核空间复制到用户空间。
(3)复制机制与使用mmap的对比
复制时,内核空间和用户空间的地址不一样,效率低。
2、代码逻辑图
二、测试过程
1、代码编写
在ubuntu的/home/xjh/iot/embedded_basic/rootfs/tmp中编写代码:app.c与module_test.c,内容见代码附录。
2、代码编译
(1)编译驱动源代码
利用同目录下的Makefile文件编译module_test.c,得到module_test.ko驱动文件。
(2)编译应用层程序
怎样编译app.c?是使用ubuntu的gcc还是交叉编译工具链?因为此应用层程序要在开发板运行,因此需要使用交叉编译工具链中的gcc来编译,而不是 ubuntu 中的gcc。
在已经正确安装了交叉编译工具链的ubuntu系统中,使用如下命令编译app.c得到app.exe。
arm-linux-gcc app.c -o app.exe
因为之前实验中已经将ubuntu的/home/xjh/iot/embedded_basic/rootfs/tmp挂载到开发板/mnt目录,所以开发板完全启动后,可以在/mnt目录中看到刚才编译的文件。
3、代码测试
可以直接在开发板的/mnt目录下进行测试。
(1)装载测试
[root@xjh mnt]# lsmodNot tainted [root@xjh mnt]# insmod module_test.ko //安装模块 [ 5278.035378] chrdev_init helloworld init [ 5278.038524] register_chrdev success... mymajor = 250.//自动分配的主设备号为250 [root@xjh mnt]# lsmod //列出已经安装的模块Not tainted module_test 1823 0 - Live 0xbf006000//这个具体表示什么意思? [root@xjh mnt]#
装载后查看/proc/devices,是否有驱动源码中所写的驱动名字、自动分配的主设备号。
[root@xjh mnt]# cat /proc/devices Character devices:1 mem2 pty //省略…… 250 testchar //这里出现了我们在驱动程序中给驱动取的名字、自动分配的主设备号 //省略……Block devices:1 ramdisk 259 blkext //省略…… 179 mmc 254 device-mapper
(2)创建设备文件
[root@xjh mnt]# cd /dev [root@xjh dev]# ls CEC ptyr6 sequencer2 ttyq5 HPD ptyr7 snd ttyq6 adc ptyr8 tty ttyq7 //省略……这里没有test这个设备文件。 //接下来看执行“mknod /dev/test c 250 126”之后的效果如何 [root@xjh dev]#[root@xjh ]# mknod /dev/test c 250 126 [root@xjh ]# ls /dev CEC ptyr6 sequencer2 ttyq4 HPD ptyr7 snd ttyq5 adc ptyr8 test //出现了设备文件test tyq6 alarm ptyr9 tty ttyq7 //省略…… [root@xjh ]# ls -l /dev/test crw-r--r-- 1 root root 250, 126 Jan 1 14:02 /dev/test [root@xjh ]# //主设备号 //次设备号 和mknod时的设置一样
(3)操作设备文件
运行应用层程序app.exe,观察运行效果。
[root@xjh mnt]# ./app.exe [ 1587.469172] test_chrdev_open [ 1587.470769] test_chrdev_write [ 1587.473521] copy_from_user success.. [ 1587.477106] test_chrdev_read [ 1587.479949] copy_to_user success.. [ 1587.483352] test_chrdev_release open /dev/test success.. //这是应用层的判断是否open成功的代码,为何那么迟才输出? 璇诲嚭鏉ョ殑鍐呭鏄細helloworld2222. //为何乱码,scrt的缘故? [root@xjh mnt]#
说明
1)Linux系统字符编码默认是UTF-8格式的,如果SecureCRT没有设置成UTF-8格式,中文显示会出现乱码。设置SCRT格式的方法见博客:SecureCRT显示乱码的解决办法
2)为何应用层判断是否open成功的代码很迟才输出?(待解决)
(4)卸载模块测试
[root@xjh mnt]# rmmod module_test.ko [ 1385.257231] chrdev_exit helloworld exit [root@xjh mnt]# lsmodNot tainted [root@xjh mnt]#
4、总结说明
(1)应用层的代码编译,要使用交叉编译工具链。
(2)驱动源代码的编译,要使用与开发板系统内核版本一致的内核源码进行编译。从博文字符设备驱动基础2——用开发板来调试驱动的步骤中可以看出,Makefile文件里指明进入与开发板系统内核版本一致的内核源码中,然后make modules。这说明执行的是Makefile文件中的一个目标modules。
//在Makefile的1300行 //目标 依赖 modules: $(module-dirs)@$(kecho) ' Building modules, stage 2.';$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
(3)mymajor = register_chrdev(0, MYNAME, &test_fops);
参数 0 表示让系统自动分配主设备号。
参数MYNAME表示设备(或者说驱动,没有出现驱动模型概念前,两者混为一谈)的名字。
参数&test_fops是指向struct file_operations变量的指针,这个变量代表着驱动的实质内容。
返回值mymajor代表系统为设备分配的主设备号。
/proc/devices文件记录着系统中已经注册的块设备和字符设备,我们可以通过cat来查看此文件内容,两列内容分别表示该设备的名字MYNAME及其设备号mymajor。
此函数成功后,主设备号为mymajor的设备与它的驱动内容&test_fops就关联起来了,也就是说一个设备对应了一个驱动内容。那在应用层如何表示这个设备呢?见(4)。
(4)/dev/test是设备文件,是命令行中使用mknod命令创建的。安装好驱动模块后,会得到系统分配的主设备号mymajor(这里是250)。然后利用这个主设备号,手动地创建设备文件,即执行“mknod /dev/test c 250 126”。这样一来,主次设备号为250、126的设备就和设备文件/dev/test关联了,应用层通过API操作/dev/test文件,也就是操作主次设备号为250、126的设备,而应用层操作里的API最终对应着这个设备对应的驱动&test_fops。
再次说明,设备文件不能使用vim打开,可以使用ls -l 命令查看。
综合(3)(4),主次设备号、设备名字、驱动内容、设备文件,这几个概念要清楚。
主次设备号:主设备号是register_chrdev()的返回值mymajor,次设备号在mknod时设定。
设备名字(或者说驱动名字):MYNAME (主设备号、设备名字在/proc/devices文件中)
驱动内容:&test_fops (程序猿在驱动源代码中编写)
设备文件:/dev/test,利用mknod命令手动创建
(5)设备文件是手动创建的,能不能让它自动创建呢?可以的,见博文字符设备驱动高级篇4——自动创建设备文件的函数代码分析。
(6)应用层的open、read等API,与驱动源码的test_chrdev_open、test_chrdev_read等具体操作函数,通过struct file_operations的填充而关联起来。
(7)示例的驱动源码中的test_chrdev_open、test_chrdev_read等具体操作函数,只是为了演示应用层读写操作与驱动层的读写如何关联起来的,因而没有硬件操作细节(比如硬件寄存器操作这些行为)。实际驱动中会操作一些硬件,比如字符设备驱动基础5——驱动如何操控硬件(动静态映射操作LED)。
三、附录代码
1、应用层代码:app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//这是应用层#define FILE "/dev/test"
//“/dev/test”是利用mknod手工创建的设备文件,创建之后,
//这个设备文件就和该设备编号的设备对应上了,操作此设备文件就是操作该设备。//问题是mknod需要输入主设备号,和驱动源码中自动获取的主设备号不相冲突吗?
//不冲突,因为这里就是根据驱动程序安装后得到的主设备号后
//才利用这个主设备号来创建设备文件的。我理解顺序相反了。
//应该驱动程序安装在前,应用程序运行在后。char buf[100];int main(void)
{int fd = -1;fd = open(FILE, O_RDWR);//这里的open,对应的是驱动文件中的.open指定的函数if (fd < 0){printf("open %s error.\n", FILE);return -1;}printf("open %s success..\n", FILE);// 读写文件write(fd, "helloworld2222", 14);read(fd, buf, 100);printf("读出来的内容是:%s.\n", buf);// 关闭文件close(fd);return 0;
}
2、驱动文件:module_test.c
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>#define MYNAME "testchar"int mymajor; //内核自动分配的主设备号
char kbuf[100]; //内核空间(即驱动空间,毕竟内核和驱动属于同一层)的bufstatic int test_chrdev_open(struct inode *inode, struct file *file)
{// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。printk(KERN_INFO "test_chrdev_open\n");return 0;
}static int test_chrdev_release(struct inode *inode, struct file *file)
{printk(KERN_INFO "test_chrdev_release\n");return 0;
}//读函数,即从内核空间(驱动空间)读取数据到用户空间
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, \size_t count, loff_t *ppos)
{int ret = -1;printk(KERN_INFO "test_chrdev_read\n");//将内容从内核空间(驱动空间)读取到用户空间//返回值为0说明读取成功,读取不成功时返回值是剩余没有读取的字节数ret = copy_to_user(ubuf, kbuf, count);if (ret){printk(KERN_ERR "copy_to_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_to_user success..\n");return 0;
}// 写函数
//1、将应用层的数据先复制到内核中
//2、然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,\size_t count, loff_t *ppos)
{ //此函数前三个参数,对应应用层的write函数的三个参数?int ret = -1;printk(KERN_INFO "test_chrdev_write\n");// 1、使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中//memcpy(kbuf, ubuf); // 不行,因为用户空间和内核空间,两者不在一个地址空间中ret = copy_from_user(kbuf, ubuf, count);if (ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_from_user success..\n");// 2、真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据//去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码return 0;
}// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {.owner = THIS_MODULE, // 惯例,直接写即可.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的就是这个.open对应的函数.release = test_chrdev_release, .write = test_chrdev_write,.read = test_chrdev_read,
};// 模块安装函数
static int __init chrdev_init(void)
{ printk(KERN_INFO "chrdev_init helloworld init\n");// 在module_init宏调用的函数中去注册字符设备驱动// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数mymajor = register_chrdev(0, MYNAME, &test_fops);if (mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);return 0;
}// 模块载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");// 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME);}module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息