Linux下字符设备驱动开发以及流程介绍
文章目录
- 1 - 字符设备介绍
- 2 - 字符设备开发流程图
- 3 - 字符设备开发流程具体讲解
- (1)设备编号的定义与申请
- 【1】Linux主次设备号介绍
- 【2】分配设备编号
- 【3】释放主次设备号
- (2)定义file_operations结构体-初始化接口函数
- (3)分配cdev结构体与注销
- (4)绑定主次设备号,fops到cdev中,注册cdev给Linux内核
- (5)创建设备类型、注册设备节点
- 【1】创建
- 【2】注销
- 4 - 字符设备开发与测试
- (1)驱动源码与测试源码
- (1)x86架构虚拟机上运行
- (2)arm架构开发板上运行
- (4)copy_to/from_user()函数
- 5 - inode与file结构体
- (1)inode结构体
- (2)file结构体
首先我们介绍一下什么是字符设备,然后讲解一下字符设备开发的具体的流程,分别详细介绍每一个流程中涉及到的结构体以及知识点,最后我们编写代码实现字符设备的开发以及测试。
1 - 字符设备介绍
Linux内核设计哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文件I/O来进行
操作。Linux内核将设备按照访问特性分为三类:字符设备、块设备、网络设备。
字符设备对数据的处理按照字节流的形式进行的。典型的字符设备:串口、键盘、触摸屏、摄像头、I2C、SPI、声卡等;应用程序能够使用系统IO函数open、write、read、lseek、close…来就行访问。
如下图:应用程序运行在用户空间,而Linux驱动属于内核一部分,因此驱动运行于内核空间,当用户想要实现对内核操作时,必须使用系统调用来实现从用户空间到内核空间的操作。
2 - 字符设备开发流程图
3 - 字符设备开发流程具体讲解
(1)设备编号的定义与申请
【1】Linux主次设备号介绍
字符设备通过文件系统中的设备名来存取,惯例上它们位于/dev目录。
wangdengtao@wangdengtao-virtual-machine:~$ ls -l /dev/
总用量 0
crw------- 1 root root 10, 124 3月 17 18:48 cpu_dma_latency
crw------- 1 root root 10, 203 3月 17 18:48 cuse
...
我们可以看见上面的两个设备,首先最前面的‘c’表示这是一个字符(character)设备;我们可以看见第二个root后面的数字,这些数字是给特殊设备的主次设备编号。10,就是主设备号,后面的124和203就是次设备号。如果我们想要对相关设备进行操作,只需要对设备文件进行读或者写操作就可以了。
传统上,主编号标识设备相连的驱动;次设备号被内核来决定应用哪个设备。
dev_t 是一个32位的数据类型,其中高 12 位为主设备号,低 20 位为次设备号。在编码时,我们不应该管哪些位是主设备号,哪些位是次设备号。而是应当利用在<linux/kdev_t.h>
中的一套宏定义来获取一个dev_t的主、次编号:
wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/include/linux$ cat kdev_t.h
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_KDEV_T_H
#define _LINUX_KDEV_T_H#include <uapi/linux/kdev_t.h>#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
...
宏 MINORBITS 表示次设备号位数,一共是 20 位。
宏 MINORMASK 表示次设备号掩码。
宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
【2】分配设备编号
静态分配设备号
int register_chrdev_region(dev_t first, unsigned int count, char *name);
- first:要分配的起始设备号。first的次编号部分通常是从0开始,但不是强制的(first = MKDEV(10, 0);)
- count:请求分配的设备号的总数。注意,如果count太大,你要求的范围可能溢出到下一次编号;但是只要你要求的编号范围可用,一切都任然会正确工作。
- name:设备名字。
动态申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const
char *name)
- dev:只是一个输出参数,保存申请到的设备号。
- baseminor:次设备号,它常常是0;
- count:要申请的设备号数量。
- name:设备名字。
动态分配的缺点是你无法提前创建设备节点,因为分配给你的主设备号会发生变化。我们申请到了设备节点之后,可以用前面讲到的宏定义 MAJOR() 来获取主设备号。
【3】释放主次设备号
void unregister_chrdev_region(dev_t from, unsigned count)
- from:要释放的设备号。
- count:表示从 from 开始,要释放的设备号数量。
(2)定义file_operations结构体-初始化接口函数
file_operations 就是把系统调用和驱动函数关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用,相应的系统调用将读取 file_operations 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序工作。在系统内部,I/O设备的存取操作通过特定的入口点来进行,而这组特定的入口点恰恰是由设备驱动提供的。通常这组设备驱动程序接口是由结构 file_operations结构体向系统说明的,它定义在include/linux/fs.h
中。传统上,一个 file_operations 结构或者其一个指针称为fops(或者它的一些变体),结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作,或者对于不支持的操作留置为NULL。当指定为NULL指针时内核的确切的行为是每个函数不同的。
struct file_operations {struct module *owner;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, bool spin);int (*iterate) (struct file *, struct dir_context *);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 *);unsigned long mmap_supported_flags; 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 *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);
...
我就拿后面我们要写的代码为例讲解一下,我们在file_operations中将open系统调用函数指向了chrtest_drv_open这个函数,open系统调用就会把控制权转交给这个函数,完成驱动函数与系统调用函数的转换。
static int chrtest_drv_open (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}
/* 定义自己的file_operations结构体*/
static struct file_operations chrtest_fops = {.owner = THIS_MODULE,.open = chrtest_drv_open,
};
(3)分配cdev结构体与注销
内核在内部使用类型struct cdev的结构体来代表字符设备。在内核调用你的设备操作之前,你必须分配一个这样的结构体并注册给linux内核,在这个结构体里有对于这个设备进行操作的函数,具体定义在file_operations结构体中。该结构体定义在include/linux/cdev.h
文件中。
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};
获取cdev:struct cdev *cdev_alloc(void)
注销cdev:
void cdev_del(struct cdev *p)
(4)绑定主次设备号,fops到cdev中,注册cdev给Linux内核
在分配到cdev结构体后,接下来我们将它初始化,并将对该设备驱动所支持的系统调用函数存放在
file_operations结构体添加进来,然后我们通过cdev_add函数将他们注册给Linux内核,这样完成整个
Linux设备的注册过程。
初始化设备:cdev_init(struct cdev *dev, struct file_operations *fops);
cdev_add的函数原型如下:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
- dev是cdev结构。
- num是这个设备相应的第一个设备号。
- count是应当关联到设备的设备号的数目。
例子:
static struct file_operations chrtest_fods ={.owner = THIS_MODULE,.open = chrtest_open,
};chrtest_cdev = cdev_alloc();/*获取cdev*/chrtest_cdev->owner = THIS_MODULE; /*.owner这表示谁拥有你这个驱动程序*/cdev_init(chrtest_cdev, &chrtest_fops); /*将fops到cdev中*/result = cdev_add(chrtest_cdev, devno, 1); /*将字符设备注册进内核*/if(0 != result){printk(KERN_INFO " %s driver can't register cdev:result=%d\n", DEV_NAME,result);}
(5)创建设备类型、注册设备节点
【1】创建
手动创建设备节点
输入如下命令创建/dev/chardev
这个设备节点文件:
mknod /dev/chardev c 10 0
在/dev路径下创建一个名字为chardev的字符设备节点,主设备号为10,次设备号为0。
自动创建设备节点
class_create()//创建设备类型,类这个概念在Linux中被抽象成一种设备的集合(/sys/class/目录下)
device_create()//注册设备节点(/dev/目录下)
class_create()
这个函数使用非常简单,在内核中是一个宏定义。/include/linux/device.h
中:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
- owner:struct module结构体类型的指针,一般赋值为THIS_MODULE。
- name:char类型的指针,类名。
device_create()
用于创建设备:
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
- class:该设备依附的类。
- parent:父设备。
- devt:设备号(此处的设备号为主次设备号)。
- drvdata:私有数据。
- fmt:设备名。
创建例子:
/*自动创建设备类型、/dev设备节点*/chrdev_class = class_create(THIS_MODULE, DEV_NAME); /*创建设备类型sys/class/chrdev*/if (IS_ERR(chrdev_class)) {result = PTR_ERR(chrdev_class);goto ERROR;}device_create(chrdev_class, NULL, MKDEV(dev_major, 0), NULL, DEV_NAME); /*/dev/chrdev 注册这个设备节点*/
【2】注销
注销设备类型:
void class_destroy(struct class *cls)
注销设备节点:
device_destroy()
注销例子:
device_destroy(chrdev_class, MKDEV(dev_major, 0)); /*注销这个设备节点*/
class_destroy(chrdev_class); /*删除这个设备类型*/
4 - 字符设备开发与测试
(1)驱动源码与测试源码
字符设备驱动开发源码:
/*************************************************************************> File Name: char_dev.c> Author: WangDengtao> Mail: 1799055460@qq.com > Created Time: 2023年03月16日 星期四 16时40分29秒************************************************************************/#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/module.h>
#include <linux/fs.h>/*如果没有定义DEV_MAJOR就设置设备号为0,采用动态申请,如果有则使用宏定义的设备号*/
//#define DEV_MAJOR 88
#ifndef DEV_MAJOR
#define DEV_MAJOR 0
#endif#define DEV_NAME "chardev" /*宏定义设备的名字*/
#define MIN(a,b) (a < b ? a : b)int dev_major = DEV_MAJOR; /*主设备号*/
static struct cdev *chrtest_cdev; /*创建cdev结构体*/
static char kernel_buf[1024];
static struct class *chrdev_class; /*定义一个class用于自动创建类*//*实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t char_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*将内核空间的数据复制到用户空间*/err = copy_to_user(buf, kernel_buf, MIN(1024, size));return MIN(1024, size);
}static ssize_t char_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*将buf中的数据复制到写缓冲区kernel_buf中,因为用户空间内存不能直接访问内核空间的内存*/err = copy_from_user(kernel_buf, buf, MIN(1024, size)); return MIN(1024, size);
}static int char_open (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}static int char_close (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/*定义自己的file_operations结构体*/
static struct file_operations chrtest_fops = {.owner = THIS_MODULE,.open = char_open,.read = char_read,.write = char_write,.release = char_close,
};/*注册驱动函数:写入口函数,安装驱动程序时就会调用这个入口函数 */
static int __init chardev_init(void)
{int result;/*dev_t 定义在文件 include/linux/types.htypedef __u32 __kernel_dev_t;......typedef __kernel_dev_t dev_t;可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h里面,定义如下:typedef unsigned int __u32;综上所述,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。*/dev_t devno;/*定义一个dev_t的变量表示设备号*/printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*字符设备驱动注册的流程二:分配主次设备号,这里不仅支持静态指定,也支持动态申请*//*静态申请主次设备号*/if(0 != dev_major){devno = MKDEV(dev_major, 0);//将主设备号dev_major和从设备号0分配给devno变量result = register_chrdev_region(devno, 1, DEV_NAME);//请求分配一个设备号,名字为DEV_NAME(chardev),设备号是:88 0}else{result = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);//求分配一个名字为chardev的设备号,从设备号为0,保存到devno变量中dev_major = MAJOR(devno);//获取设备号}/*失败后的处理结果,总规上面只执行一次,所以直接在外面判断就可*/if(result < 0){printk(KERN_ERR " %s chardev can't use major %d\n", DEV_NAME, dev_major);return -ENODEV;}printk(KERN_DEBUG " %s driver use major %d\n", DEV_NAME, dev_major);/*字符串设备驱动流程三:分配cdev结构体,使用动态申请的方式*//*内核在内部使用类型struct cdev的结构体来代表字符设备。在内核调用你的设备操作之前,你必须分配一个这样的结构体并注册给linux内核,在这个结构体里有对于这个设备进行操作的函数,具体定义在file_operation结构体中。*/if(NULL == (chrtest_cdev = cdev_alloc())){printk(KERN_ERR "%s driver can't alloc for the cdev\n", DEV_NAME);unregister_chrdev_region(devno, 1);//释放掉设备号return -ENOMEM;}/*字符设备驱动流程四:分配cdev结构体,绑定主次设备号,fops到cdev结构体中,并且注册到linux内核*/chrtest_cdev -> owner = THIS_MODULE; /*.owner这表示谁拥有这个驱动程序*/cdev_init(chrtest_cdev, &chrtest_fops);/*初始化设备*/result = cdev_add(chrtest_cdev, devno, 1); /*将字符设备注册进内核*/if(0 != result){printk(KERN_INFO "%s driver can't register cdev:result = %d\n", DEV_NAME, result);goto ERROR;}printk(KERN_INFO "%s driver can register cdev:result = %d\n", DEV_NAME, result);/*自动创建设备类型、/dev设备节点*/chrdev_class = class_create(THIS_MODULE, DEV_NAME); /*创建设备类型sys/class/chrdev*/if (IS_ERR(chrdev_class)) {result = PTR_ERR(chrdev_class);goto ERROR;}device_create(chrdev_class, NULL, MKDEV(dev_major, 0), NULL, DEV_NAME); /*/dev/chrdev 注册这个设备节点*/return 0;ERROR:printk(KERN_ERR" %s driver installed failure.\n", DEV_NAME);cdev_del(chrtest_cdev);unregister_chrdev_region(devno, 1);return result;}/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit chardev_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 注销设备类型、/dev设备节点*/device_destroy(chrdev_class, MKDEV(dev_major, 0)); /*注销这个设备节点*/class_destroy(chrdev_class); /*删除这个设备类型*/cdev_del(chrtest_cdev); /*注销字符设备*/unregister_chrdev_region(MKDEV(dev_major,0), 1); /*释放设备号*/printk(KERN_ERR" %s driver version 1.0.0 removed!\n", DEV_NAME);return;
}/*调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用.*/
module_init(chardev_init);
/*调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用.*/
module_exit(chardev_exit);/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本
测试字符设备驱动源码:
/*************************************************************************> File Name: char_dev_test.c> Author: WangDengtao> Mail: 1799055460@qq.com > Created Time: 2023年03月16日 星期四 16时50分29秒************************************************************************/#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>int main(int argc, char **argv)
{int fd;char buf[1024];int len;/* 1. 判断参数 */if (argc < 2){printf("Usage: %s -w <string> /dev/??\n", argv[0]);printf(" %s -r\n", argv[0]);return -1;}/* 2. 打开文件 */if(argc == 4){fd = open(argv[3], O_RDWR);}if(argc == 3){fd = open(argv[2], O_RDWR);}if (fd == -1){printf("can not open file %s\n", argv[3]);return -1;}/* 3. 写文件或读文件 */if ((0 == strcmp(argv[1], "-w")) && (argc == 4)){len = strlen(argv[2]) + 1;len = len < 1024 ? len : 1024;printf("Write to %s success!\n", argv[3]);printf("write len: %d\n", len);write(fd, argv[2], len);}else if((0 == strcmp(argv[1], "-r")) && (argc == 3)){memset(buf, 0, sizeof(buf));len = read(fd, buf, 1024);printf("Read from %s success!\n", argv[2]);printf("read len: %ld\n", strlen(buf)+1);buf[1023] = '\0';printf("char_dev_test read : %s\n", buf);}else{printf("Usage: %s -w <string> /dev/??\n", argv[0]);printf(" %s -r\n", argv[0]);return -1;}close(fd);return 0;
}
(1)x86架构虚拟机上运行
Makefile:
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := char_dev.oCC=gcc
APP_NAME=char_dev_testall:$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules@${CC} ${APP_NAME}.c -o ${APP_NAME}@make clearclear:@rm -f *.o *.cmd *.mod *.mod.c@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f@rm -f .*ko.cmd .*.o.cmd .*.o.d@rm -f *.unsignedclean:@rm -f *.ko@rm -f ${APP_NAME}
运行结果:
(2)arm架构开发板上运行
Makefile:
KERNAL_DIR ?= /home/wangdengtao/imx6ull/imx6ull/bsp/kernel/linux-imx
PWD :=$(shell pwd)
obj-m := char_dev.oCC=arm-linux-gnueabihf-gcc
APP_NAME=char_dev_testall:$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules@${CC} ${APP_NAME}.c -o ${APP_NAME}@make clearclear:@rm -f *.o *.cmd *.mod *.mod.c@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f@rm -f .*ko.cmd .*.o.cmd .*.o.d@rm -f *.unsignedclean:@rm -f *.ko@rm -f ${APP_NAME}
运行结果:
这里提醒一下,我们需要将我们的测试程序和驱动程序复制到我们的tftpboot目录下开发板才可以进行获取。
开发板获取以及测试:
root@igkboard:~# tftp -gr char_dev_test 192.168.10.168
root@igkboard:~# tftp -gr char_dev.ko 192.168.10.168
root@igkboard:~# ls
char_dev.ko char_dev_test hello.ko
root@igkboard:~# chmod a+x char_dev_test root@igkboard:~# ./char_dev_test
Usage: ./char_dev_test -w <string> /dev/??
./char_dev_test -rroot@igkboard:~# insmod char_dev.ko
root@igkboard:~# ls -l /dev/chardev
crw------- 1 root root 243, 0 Mar 18 04:34 /dev/chardevroot@igkboard:~# ./char_dev_test -w hello /dev/chardev
Write to /dev/chardev success!
write len: 6
root@igkboard:~# ./char_dev_test -r /dev/chardev
Read from /dev/chardev success!
read len: 6
char_dev_test read : hello
root@igkboard:~# rmmod char_dev
root@igkboard:~# ls -l /dev/chardev
ls: cannot access '/dev/chardev': No such file or directory
测试成功。
(4)copy_to/from_user()函数
代码中出现的两个没有提到的函数:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n);
/*
to: 目标地址,这个地址是用户空间的地址;
from: 源地址,这个地址是内核空间的地址;
n: 将要拷贝的数据的字节数。
*/
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n);
/*
to: 目标地址,这个地址是内核空间的地址;
from: 源地址,这个地址是用户空间的地址;
n: 将要拷贝的数据的字节数。
*/
copy_to_user 和 copy_from_user 是在进行驱动相关程序设计的时候,要经常遇到的函数。由于内
核空间与用户空间的内存不能直接互访,因此借助函数 copy_to_user() 完成内核空间到用户空间的复制,函数 copy_from_user() 完成用户空间到内核空间的复制。
我们代码中用到的全局变量kernel_buf是保存写进去的内容的,我们write的时候调用了copy_from_user(kernel_buf, buf, MIN(1024, size)
函数,将要写进去的数据(buf)复制到读缓冲区(kernel_buf)中,然后再read的时候,调用copy_to_user(buf, kernel_buf, MIN(1024, size)
函数将kernel_buf中的值读取出来复制到buf中,就可以直接读到buf中了,也就获取到了。
5 - inode与file结构体
(1)inode结构体
Linux中一切皆文件,当我们在Linux中创建一个文件时,就会在相应的文件系统中创建一个inode与之对应,文件实体和文件inode是一一对应的,创建好一个inode会存在存储器中。第一次open就会将inode在内存中有一个备份,同一个文件被多次打开并不会产生多个inode,当所有被打开的文件都被close之后,inode在内存中的实例才会被释放。既然如此,当我们使用mknod(或其他方法)创建一个设备文件时,也会在文件系统中创建一个inode,这个inode和其他的inode一样,用来存储关于这个文件的静态信息(不变的信息),包括这个设备文件对应的设备号,文件的路径以及对应的驱动对象等。
struct inode {······struct hlist_node i_hash;struct list_head i_list; /* backing dev IO list */struct list_head i_sb_list;//主次设备号dev_t i_rdev;struct list_head i_devices;//用联合体是因为该文件可能是块设备文件或者字符设备文件union {struct pipe_inode_info *i_pipe; //管道文件struct block_device *i_bdev; //块设备文件struct cdev *i_cdev; //字符设备文件};//私有数据void *i_private; /* fs or device private pointer */
};
我们一般比较关心的只有两个变量:
- dev_t i_rdev:
代表设备文件的节点,这个成员包含实际的设备编号 - struct cdev *i_cdev:
这个结构体代表字符设备,这个成员包含一个指针,指向这个结构体,当节点指的是一个字符设备文件时。
(2)file结构体
file结构体代表一个打开的文件。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后的关闭。在文件的所有实例都关闭后,内核释放这个数据结构。
struct file结构体 用来表示一个动态的设备,每当open打开一个文件时就会产生一个struct file结构体 与之对应。
struct file {union {struct list_head fu_list;struct rcu_head fu_rcuhead;}f_u;······const struct file_operations *f_op; //该文件对应的操作方法unsigned int f_flags; fmode_t f_mode; //打开文件的权限,比如:只读打开、只写打开、读写打开loff_t f_pos; //文件指针的偏移量/* needed for tty driver, and maybe others */void *private_data; //私有数据
};
结合上面的图片可以进一步了解两个结构体之间是如何联系的。
相关文章:

Linux下字符设备驱动开发以及流程介绍
文章目录1 - 字符设备介绍2 - 字符设备开发流程图3 - 字符设备开发流程具体讲解(1)设备编号的定义与申请【1】Linux主次设备号介绍【2】分配设备编号【3】释放主次设备号(2)定义file_operations结构体-初始化接口函数(…...

Web自动化框架断言方法实现
前言1、设计用例方法关键字1.1、获取元素属性值2.1、断言2、代码实现2.1、实现获取元素属性值2.1.1 函数实现2.1.2 方法配置2.1.2 用例调试2.1.3 html属性2.2、实现断言2.2.1 函数2.2.2 方法配置2.2.3 用例调试1)断言结果成功2)断言结果失败前言 本文的…...

8大核心语句,带你深入python
人生苦短 我用python 又来给大家整点好东西啦~ 咱就直接开练噜!内含大量代码配合讲解 python 安装包资料:点击此处跳转文末名片获取 1. for - else 什么?不是 if 和 else 才是原配吗? No,你可能不知道, else 是个…...

【批处理】- 批处理自动安装Mysql与Redis
前言 在全新环境中安装MySQL与Redis操作是挺麻烦的,于是就想使用脚本来自动安装,使用批处理进行一步到位的安装,后面还能使用工具进行打包成exe可执行文件,一键安装,最后能够更好的部署项目到windows系统的服务器。 …...

聊聊华为的工作模式
目录 一、试用期与加班工资 二、招聘 三、月度答辩和转正答辩 四、可信考试认证 五、接口人 六、问题缺陷单 七、代码检视 八、功能开发 九、出征海外 一、试用期与加班工资 一般而言,试用期持续的时间为3-6个月,工资、奖金都按正式员工的标准…...

燕山大学-面向对象程序设计实验-实验6 派生与继承:多重派生-实验报告
CSDN的各位友友们你们好,今天千泽为大家带来的是燕山大学-面向对象程序设计实验-实验5 派生与继承:单重派生-实验报告,接下来让我们一起进入c的神奇小世界吧,相信看完你也能写出自己的 实验报告!本系列文章收录在专栏 燕山大学面向对象设计报告中 ,您可以在专栏中找…...

分割两个字符串得到回文串[抽象--去除具体个性取共性需求]
抽象前言一、分割两个字符串得到回文串二、双指针总结参考文献前言 抽象去个性留共性,是因为具体个性对于解决问题是个累赘。少了累赘,直击需求,才能进行问题转换或者逻辑转换。 一、分割两个字符串得到回文串 二、双指针 // 限定死了&…...

【LeetCode】1609. 奇偶树、1122. 数组的相对排序
作者:小卢 专栏:《Leetcode》 喜欢的话:世间因为少年的挺身而出,而更加瑰丽。 ——《人民日报》 1609. 奇偶树 1609. 奇偶树 题目描述: 如果一棵二叉树满足下述几个条件&#x…...

【C++初阶】4. Date类的实现
如果下面博客有不理解的地方,可以查看源码:代码提交:日期类的实现 1. 构造函数的实现 由于系统实现的默认构造函数即便采用默认值的形式也只能存在1个固定的默认日期(例如:1997-1-1)。所以,构…...

ES6新特性--变量声明
可以使用let关键字来声明变量let a;let b,c;//同时声明多个变量let stu = 张三;let name =李四,age = 12;//声明变量的同时赋值 let关键字使用的注意事项(1).变量在声明的时候不可以重复,这也符合其他语言的变量声明规范 let name = 李四; let name = 张三;//这里开始报错,但…...

【Django】缓存机制
文章目录缓存的介绍Django的6种缓存方式开发调试缓存dummy.DummyCache内存缓存locmem.LocMemCache文件缓存filebased.FileBasedCache⭐️数据库缓存db.DatabaseCacheMemcache缓存memcached.MemcachedCacheMemcache缓存memcached.PyLibMCCacheDjango缓存的应用内存缓存cache_pag…...

我的创作纪念日——一年的时间可以改变很多
机缘 不知不觉来到CSDN已经创作一年了。打心底讲,对于在CSDN开始坚持创作的原因,我用一句话来概括最合适不过了——“无心插柳柳成荫” 为什么这么说呢? 这要从我的一篇博客说起——《输入命令Javac报错详解》: 那也是我第一次…...

Jetson Nano驱动机器人的左右两路电机
基于Jetson Nano板子搭建一个无人车,少不了减速电机驱动轮子滚动,那如何驱动呢?从Jetson.GPIO库文件来说,里面没有支持产生PWM的引脚,也就意味着Jetson nano没有硬件产生PWM的能力,所以我们不得不使用别的方…...

如何通过openssl生成公钥和私钥?
1、生成RSA秘钥的方法 生成RSA秘钥的方法: openssl genrsa -des3 -out privkey.pem 2048 注:建议用2048位秘钥,少于此可能会不安全或很快将不安全。 这个命令会生成一个2048位的秘钥,同时有一个des3方法加密的密码,…...

Verilog的If语句和Case语句
这篇文章将讨论 verilog 中两个最常用的结构----if语句和case语句。在之前的文章中学习了如何使用过程块(例如always块)来编写按顺序执行的verilog 代码。此外还可以在过程块中使用许多语句----统称为顺序语句,如case 语句和 if 语句。这篇文…...

HJ31 单词倒排
描述 对字符串中的所有单词进行倒排。 说明: 1、构成单词的字符只有26个大写或小写英文字母; 2、非构成单词的字符均视为单词间隔符; 3、要求倒排后的单词间隔符以一个空格表示;如果原字符串中相邻单词间有多个间隔符时…...

leetcode——203.移除链表元素
文章目录🐨1.题目🪅2.解法1-头节点迭代🌿2.1 思路🌿2.2 代码实现🦆3. 解法2-创建新链表🎏3.1 思路🎏3.2 代码实现🐐4. 题目链接🐨1.题目 给你一个链表的头节点head和一个…...

GPT-4来袭:开启人工智能新时代
文章目录介绍GPT4 模型演示示例示例 1示例 2示例 3示例 4示例 5最后Reference介绍 2023年3月15日,OpenAI公司正式发布了先进的自然语言处理模型GPT-4,前不久发布的GPT-3.5模型只能理解文字的语言模型,而新发布的GPT4则是多模态模型ÿ…...

芯微电子IPO终止:业绩开始大幅下滑,王日新、王苟新兄弟不同命
近日,深圳证券交易所披露的信息显示,黄山芯微电子股份有限公司(下称“芯微电子”)申请撤回发行上市申请文件。因此,深圳证券交易所决定终止对其首次公开发行股票并在创业板上市的审核。 据贝多财经了解,芯…...

【C++】用手搓的红黑树手搓set和map
目录 一、set/map的底层结构 1、set/map的源码 2、利用模板区分set/map 3、利用仿函数控制比较大小 二、set/map的迭代器(红黑树的迭代器) 1、红黑树的begin、end迭代器 2、红黑树迭代器的operator 3、红黑树迭代器的operator-- 三、set的const…...

【C++】空指针弃NULL用nullptr
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。声明空指针的3种方法: int* p1 NULL; int* p2 nullptr; int* p3 0; 在C语言中常用NULL生成空指针,NULL是一个宏…...

【selenium学习】数据驱动测试
数据驱动在 unittest 中,使用读取数据文件来实现参数化可以吗?当然可以。这里以读取 CSV文件为例。创建一个 baidu_data.csv 文件,如图所示:文件第一列为测试用例名称,第二例为搜索的关键字。接下来创建 test_baidu_da…...

嵌入式硬件电路设计的基本技巧
目录 1 分模块 2 标注关键参数 3 电阻/电容/电感/磁珠的注释 4 可维修性 5 BOM表归一化 6 电源和地的符号 7 测试点 8 网络标号 9 容错性/兼容性 10 NC、NF 11 版本变更 12 悬空引脚 13 可扩展性 14 防呆 15 信号的流向 16 PCB走线建议 17 不使用\表示取反 不…...

Spring MVC 图片的上传和下载
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...

远程工具神器之MobaXterm (小白必看)
目录 1、介绍 2、ssh连接详解过程 3、特点 1、介绍 带有 X11 服务器、选项卡式 SSH 客户端、网络工具等的 Windows 增强型终端。 MobaXterm 是您远程计算的终极工具箱。在单个Windows应用程序中,它提供了大量功能,这些功能是为程序员,网站管…...

VRIK+Unity XR Interaction Toolkit 实现VR上半身的追踪(附带VRM模型导入Unity方法和手腕扭曲的解决方法)
文章目录📕第一步:配置 OpenXR XR Interaction Toolkit 的开发环境📕第二步:导入人物模型⭐VRM 模型导入 Unity 的方法📕第三步:配置 VRIK⭐给模型加上 VRIK 组件⭐将模型的头部和手部的位置作为 VR 追踪目…...

【C++进阶】map的介绍和使用
文章目录map的介绍map的模板参数介绍map的容器介绍map重要容器接口的介绍及使用构造函数增删查改迭代器的使用map的介绍 map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。在map中,键值key通常用于排序和惟一地标识…...

第十四届蓝桥杯三月真题刷题训练——第 15 天
目录 第 1 题:斐波那契与7 问题描述 答案提交 运行限制 代码: 第 2 题:小蓝做实验 问题描述 答案提交 运行限制 代码: 第 1 题:斐波那契与7 问题描述 斐波那契数列的递推公式为: FnFn−1Fn−2, 其中 F1F21…...

HTML5是什么?怎么学习HTML5?
HTML5 是什么?HTML5是什么?相信这个问题并不容易回答,大多数人对于HTML5的概念仅仅是听说过而已,非要让他说出个所以然来,结果只能让你失望。相比普及了近十四年的HTML4来说,HTML5带来的震撼其实丝毫不亚于…...

个人算法题精简导航整理(精炼汇总,含知识点、模板题、题单)
文章目录前言导航注意事项技巧类自定义Pair排序N维数组转一维位运算状态压缩算法基础枚举 √指数型枚举排列型枚举组合型枚举模拟 √日期天数问题:平年闰年情况递归&分治 √贪心 √货仓选址-模板题排序 √归并排序前缀和&差分 √前缀和差分(一维…...