字符设备:是指只能一个字节一个字节读写的设备,那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动。
用户空间用open函数打开dev/pin4 下的设备后,可以用write向文件中写入数据,或者用read从里面读出数据,其中大致的流程是例如用户空间调用open会产生软中断,中断号是0x80,软中断为了响应快速用汇编实现了sys_call(系统调用),然后sys_call再调用相关的函数调用VFS(虚拟文件系统)里的sys_open,然后sys_open会去内核的驱动链表,根据设备名和设备号找到相关的驱动函数,去调用驱动函数里的open,这里的open同write,open我们可以根据需要修改。
APP 打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于 APP 的每一个文件句柄,在内核里面都有一个“struct file”与之对应。我们使用 open 打开文件时,传入的 flags、mode 等参数会被记录在内核中对应的 struct file 结构体里(f_flags、f_mode),去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的 f_pos 成员里。
上图的struct file结构体中的结构体:struct file_operations 参数如下图,即指针函数集,其中owner一 般初始化为THIS_MODULE,让其归属与自己。上层对应底层,例如上层想要用read,底层就要有read的支持,还有ioctl函数是一个专用于设备输入输出操作的系统调用,可以去了解一下。
字符设备驱动的编写步骤
1.确定主设备号,dev_t MKDEV(int major, int minor),也可以让内核分配
2. 定义自己的 file_operations 结构体,是字符设备驱动提供给VFS的接口函数
3. 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
4.把 file_operations 结构体告诉内核:register_chrdev
5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 cdev_init( ) ,为了建立cdev与 file_operations之间的连接
6.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
7.其他完善:提供设备信息,自动创建设备节点:class_create, device_create
下面是很基本的字符驱动框架:
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major =231; //主设备号
static int minor =0; //次设备号
static char *module_name="pin4"; //模块名
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
return 0;
}
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name,&pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo");
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2"); //linux内核遵循GPL协议
struct file结构体中的read和write指针函数定义中,用户空间传下来的char __user参数是不能直接赋值的,我们需要用到两个函数。
针对read,我们用copy_to_user ,把用户空间的数据读到内核空间的数据中
copy_to_user(void * to, const void * from,unsigned long n);
第一参数用户空间的数据地址,
第二参数是内核空间数据的地址,
第三参数是复制的字节数,这里可以这样写
#define MIN(a,b) (a < b ? a : b)
static char kernel_buf[1024];
copy_to_user( buf, kernel_buf , MIN( 1024, size)); //第一个参数buf是char __user*
返回值为实际copy的字节数
针对write,我们用copy_from_user ,把内核空间的数据写到用户空间里
copy_from_user(void * to, const void __user *from,unsigned long n);
第一个参数是内核空间数据的地址,kernel_buf
第二个参数是用户空间的数据地址,即__user
第三个参数是复制的字节数,参考上面的写法
返回值为真实读到的字节数量。