字符设备驱动(三)(承上:设备操作)
目录引言设备操作是怎么实现的呢file_operations这个结构体的内核源码例子结构体file_operations里面的函数原型讲解函数原型完整的代码示例引言前面讲解了设备号的申请和设备节点的创建这一节该对设备进行操作了对设备操作就是对文件操作linux下一切皆文件应用空间操作open, read, write的时候实际在驱动代码中要有对应的open, read, write驱动是承上启下的这部分就是承上用户态可以调用open等来操作设备操作是怎么实现的呢Linux 一切皆文件应用层想操作设备就用普通文件接口open / read / write / close。内核里有个struct file_operations 函数指针结构体里面全是内核规定好的接口成员。我们驱动里自己定义 drv_open、drv_read、drv_write、drv_close函数名字可以自己换 四个函数。把自己写的函数赋值挂载到 file_operations 结构体对应成员上.open自己的open函数、.read自己的read函数……应用层调用 open → 内核自动匹配 → 跳进你驱动里写的 drv_openread/write/close 同理一一对应执行你驱动里的函数。这就实现了应用层通过文件接口间接调用驱动函数完成设备操作驱动起到承上启下的作用。在申请设备号的时候他作为参数出现了。static int __init test_init(void) { int val 0; //注册字符设备驱动 val register_chrdev(dev_major, test_dev, my_fops); //这个里面的第三个参数是个结构体他里面实现的就是 return 0; }驱动代码里面的my_fops具体实现const struct file_operations my_fops { .open drv_open, //这四个都是参数对应用户调用open驱动里面调用drv_open函数这个函数名是自己可以选择的在驱动代码里面要有对应的实现 .read drv_read, .write drv_write, //这些.open来自内核接着往下看我会把内核源码放出来 .release drv_close, }; 这四个参数分别是 打开文件 .open 对应用户调用的 open 读 .read 对应用户调用的 read 写 .write 对应用户调用的 write 关闭文件 .release 对应用户调用的 closefile_operations这个结构体的内核源码Linux 内核真正的 struct file_operations作用实现文件操作对象file_operations//我只是把内核源码的这个file_operations结构体放出来看不懂不影响 //只需要知道他们是函数指针就行了。 struct file_operations { struct module *owner; fop_flags_t fop_flags; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, unsigned int flags); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); void (*splice_eof)(struct file *file); int (*setlease)(struct file *, int, struct file_lease **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags); int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *, unsigned int poll_flags); } __randomize_layout;这里面都是函数指针你要用的话就自己写对应的函数来实现例子//内核源码里面的函数原型他是函数指针指向一个函数具体的函数自己实现 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); int (*open) (struct inode *, struct file *); int (*release) (struct inode *, struct file *); //我驱动里面这样写 //只用于理解具体的实现在后面。你看他每一个参数我都没有具体的参数名 //比如struct file * 但是实际代码里面要加上参数名比如 struct file *flip //自己驱动里先照着原型仿写函数头只写类型、不写参数名只是用来对照理解格式 //真正工程编译、写代码实现的时候必须补上参数名不然 C 语言函数定义会报错。 ssize_t drv_read(struct file *, char __user *, size_t, loff_t *) { return 0; } ssize_t drv_write(struct file *, const char __user *, size_t, loff_t *) { return 0; } int drv_open(struct inode *, struct file *) { return 0; } int drv_close(struct inode *, struct file *) { return 0; } const struct file_operations my_fops { .open drv_open, .read drv_read, .write drv_write, .release drv_close, };结构体file_operations里面的函数原型讲解函数原型int (*open)(struct inode *, struct file *); // 作用打开设备文件时内核会调用你实现的这个函数 参数 1struct inode * 内核内部代表设备节点的结构体包含设备号等信息一般驱动里用不到也可以不处理。 参数 2struct file * 代表用户态打开文件时创建的上下文保存文件状态如打开模式驱动里可以用来区分不同打开的进程。 返回值int 返回 0 表示打开成功返回负数如 -EIO表示打开失败用户态的 open() 会得到错误码。 int (*release)(struct inode *, struct file *); // 作用关闭设备文件时内核会调用你实现的这个函数 参数 1struct inode * 与 open 里的含义一致代表设备节点。 参数 2struct file * 与 open 里的含义一致代表用户态的文件上下文。 返回值int 一般直接返回 0 即可代表关闭成功。 ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); // 作用用户态调用 read() 时内核会调用你实现的这个函数 参数 1struct file * 文件上下文和 open 里的一致。 参数 2char __user * 用户态缓冲区指针注意它是用户空间地址驱动里不能直接用指针访问必须用 copy_to_user()把 数据安全拷贝给用户。 参数 3size_t 用户态请求读取的字节数。 参数 4loff_t * 文件的读写偏移指针驱动里可以用它来记录文件位置也可以忽略。 返回值ssize_t 成功时返回实际读到的字节数返回 0 表示文件末尾返回负数表示错误。 ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); // 作用用户态调用 write() 时内核会调用你实现的这个函数 参数 1struct file * 还是代表用户态打开文件时创建的上下文保存文件状态如打开模式驱动里可以用来区分不同打开的进程。和前面的一样 参数 2const char __user * 用户态缓冲区指针驱动里不能直接访问必须用 copy_from_user()把数据从用户空间安全拷贝到内核 空间。 参数 3size_t 用户态请求写入的字节数。 参数 4loff_t * 文件的读写偏移指针驱动里可以用它来记录文件位置也可以忽略。 返回值ssize_t 成功时返回实际写入的字节数返回负数表示错误。完整的代码示例#include linux/module.h #include linux/init.h #include linux/fs.h #include linux/device.h #include linux/uaccess.h static unsigned int dev_major 330; static struct class *devcls; static struct device *dev; static ssize_t drv_read(struct file *flip, char __user *buf, size_t count, loff_t *fpos) { printk(read\n); //这四个实现的时候都加了static限定作用域。 //这是Linux 内核驱动的标准写法不然会有警告 return 0; } static ssize_t drv_write(struct file *flip, const char __user *buf, size_t count, loff_t *fpos) { printk(write\n); return 0; } static int drv_open(struct inode *inode, struct file *flip) { printk(open\n); return 0; } static int drv_close(struct inode *inode, struct file *flip) { printk(close\n); return 0; } /* 核心绑定用户层接口与驱动实现函数 */ const struct file_operations my_fops { .open drv_open, /* 用户调用 open 执行 drv_open */ .read drv_read, /* 用户调用 read 执行 drv_read */ .write drv_write, /* 用户调用 write 执行 drv_write */ .release drv_close, /* 用户调用 close 执行 drv_close */ }; static int __init test_init(void) { int val 0; val register_chrdev(dev_major, test_dev, my_fops); if(val 0){ printk(register error\n); return -EFAULT; } printk(register right\n); devcls class_create(test_class); dev device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, my_device); return 0; } static void __exit test_exit(void) { device_destroy(devcls, MKDEV(dev_major, 0)); class_destroy(devcls); unregister_chrdev(dev_major, test_dev); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE(GPL);编译的时候用到了Makefile文件ifeq ($(KERNELRELEASE),) #需要修改内核源码路径 KERNELDIR /root/SYSTEM/linux-rpi-6.12.y #需要修改交叉编译器路径 CROSS_COMPILE /root/cross-pi/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- ARCH arm CUR_DIR $(shell pwd) all: make -C $(KERNELDIR) M$(CUR_DIR) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) modules clean: make -C $(KERNELDIR) M$(CUR_DIR) ARCH$(ARCH) clean install: scp *.ko pi10.110.219.127:/home/pi else #需要修改对应你的 .c 文件名 obj-m led.o endif可以看到led.ko已经成功的编译出来了成功传输到树莓派3B上面树莓派这边也可以看到成功的加载了进来可以成功加载也没有问题但是我们的open、read、write等没有调用想调用的话要写一个测试文件。他已经实现了只是没有调用接下来调用一下同级目录下创建一个测试文件。test.c#include stdio.h #include fcntl.h #include unistd.h #include stdlib.h #include string.h int main(int argc, char *argv[]) { int fd; int value 0; fd open(/dev/my_device, O_RDWR); if(fd 0) { perror(open); exit(1); } read(fd, value, 1); write(fd, value, 1); close(fd); return 0; }Makefile文件ifeq ($(KERNELRELEASE),) FILE_NAME test KERNELDIR /root/SYSTEM/linux-rpi-6.12.y CROSS_COMPILE /root/cross-pi/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- ARCH arm CUR_DIR $(shell pwd) CC $(CROSS_COMPILE)gcc all: make -C $(KERNELDIR) M$(CUR_DIR) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) modules $(CC) $(FILE_NAME).c -o $(FILE_NAME) clean: make -C $(KERNELDIR) M$(CUR_DIR) ARCH$(ARCH) clean rm $(FILE_NAME) install: scp *.ko $(FILE_NAME) pi10.110.219.127:/home/pi/led_drv else obj-m led.o endif执行下面四行sudo insmod led.ko #加载内核模块 sudo chmod 777 /dev/my_device #给文件权限否则测试文件无法访问 你的不一定是my_device看你代码里面创建的设备文件叫啥名就是啥 ./test #运行测试文件 dmesg | tail -10 #查看打印信息看到open read write close 全部实现了现在驱动里 open、read、write、close 这四个函数全都写好实现了。到这里驱动的承上启下我们已经把承上做完了就是用户层写的测试程序调用 Linux 标准的 open、read、write、close 接口能成功跑进我们加载好的内核驱动对应的函数里面通路已经打通了。而启下是什么就是在驱动自己的 open /read/write /close 函数里面去直接操作底层硬件操作硬件寄存器控制 GPIO 引脚高低电平操控各种外设控制器一句话总结承上让用户层程序能进得来驱动启下让驱动能去控制真实硬件。用户控制驱动驱动控制硬件