目录
引言
设备操作是怎么实现的呢?
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_open;
read/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 *); // 作用:打开设备文件时,内核会调用你实现的这个函数 参数 1:struct inode * 内核内部代表设备节点的结构体,包含设备号等信息,一般驱动里用不到也可以不处理。 参数 2:struct file * 代表用户态打开文件时创建的上下文,保存文件状态(如打开模式),驱动里可以用来区分不同打开的进程。 返回值:int 返回 0 表示打开成功;返回负数(如 -EIO)表示打开失败,用户态的 open() 会得到错误码。 int (*release)(struct inode *, struct file *); // 作用:关闭设备文件时,内核会调用你实现的这个函数 参数 1:struct inode * 与 open 里的含义一致,代表设备节点。 参数 2:struct file * 与 open 里的含义一致,代表用户态的文件上下文。 返回值:int 一般直接返回 0 即可,代表关闭成功。 ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); // 作用:用户态调用 read() 时,内核会调用你实现的这个函数 参数 1:struct file * 文件上下文,和 open 里的一致。 参数 2:char __user * 用户态缓冲区指针,注意:它是用户空间地址,驱动里不能直接用指针访问,必须用 copy_to_user()把 数据安全拷贝给用户。 参数 3:size_t 用户态请求读取的字节数。 参数 4:loff_t * 文件的读写偏移指针,驱动里可以用它来记录文件位置,也可以忽略。 返回值:ssize_t 成功时返回实际读到的字节数;返回 0 表示文件末尾;返回负数表示错误。 ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); // 作用:用户态调用 write() 时,内核会调用你实现的这个函数 参数 1:struct file * 还是代表用户态打开文件时创建的上下文,保存文件状态(如打开模式),驱动里可以用来区分不同打开的进程。(和前面的一样) 参数 2:const char __user * 用户态缓冲区指针,驱动里不能直接访问,必须用 copy_from_user()把数据从用户空间安全拷贝到内核 空间。 参数 3:size_t 用户态请求写入的字节数。 参数 4:loff_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 pi@10.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) pi@10.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 引脚高低电平
操控各种外设控制器
一句话总结:
承上:让用户层程序能进得来驱动;
启下:让驱动能去控制真实硬件。
用户控制驱动,驱动控制硬件