Linux下字符设备驱动开发以及流程介绍


首先我们介绍一下什么是字符设备,然后讲解一下字符设备开发的具体的流程,分别详细介绍每一个流程中涉及到的结构体以及知识点,最后我们编写代码实现字符设备的开发以及测试。


1 - 字符设备介绍

Linux内核设计哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文件I/O来进行
操作。Linux内核将设备按照访问特性分为三类:字符设备、块设备、网络设备

在这里插入图片描述

字符设备对数据的处理按照字节流的形式进行的。典型的字符设备:串口、键盘、触摸屏、摄像头、I2C、SPI、声卡等;应用程序能够使用系统IO函数open、write、read、lseek、close…来就行访问。

如下图:应用程序运行在用户空间,而Linux驱动属于内核一部分,因此驱动运行于内核空间,当用户想要实现对内核操作时,必须使用系统调用来实现从用户空间到内核空间的操作。

在这里插入图片描述


2 - 字符设备开发流程图

在这里插入图片描述
我们创建一个字符设备的时候,首先要的到一个设备号,分配设备号的途径有静态分配和动态分配;拿到设备的唯一 ID,我们需要实现 file_operation 并保存到 cdev 中,实现 cdev 的初始化;然后我们需要将我们所做的工作告诉内核,使用 cdev_add() 注册 cdev;最后我们还需要创建设备节点,以便我们后面调用 file_operation 接口。

注销设备时我们需释放内核中的 cdev,归还申请的设备号,删除创建的设备节点。

在这里插入图片描述


3 - 字符设备开发流程具体讲解

(1)设备编号的定义与申请

【1】Linux主次设备号介绍

字符设备通过文件系统中的设备名来存取,惯例上它们位于/dev目录。

wangdengtao@wangdengtao-virtual-machine:~$ ls -l /dev/
总用量 0
crw-------  1 root        root     10, 124  317 18:48 cpu_dma_latency
crw-------  1 root        root     10, 203  317 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是应当关联到设备的设备号的数目。

内核通过一个散列表 (哈希表) 来记录设备编号。哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。以主设备号为 cdev_map 编号,使用哈希函数 f(major)=major%255 来计算组数下标 (使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率);主设备号冲突, 则以次设备号为比较值来排序链表节点。如下图所示,内核用 struct cdev 结构体来描述一个字符设备,并通过struct kobj_map 类型的散列表 cdev_map 来管理当前系统中的所有字符设备。

cdev_add 函数用于向内核的 cdev_map 散列表添加一个新的字符设备。
在这里插入图片描述

例子:

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。

当我们使用上述命令,创建了一个字符设备文件时,实际上就是创建了一个设备节点 inode 结构体,并且将该设备的设备编号记录在成员 i_rdev,将成员 f_op 指针指向了 def_chr_fops 结构体。这就是 mknod 负责的工作内容。

在这里插入图片描述
mknod 命令最终执行 init_special_inode 函数:

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
	inode->i_mode = mode;
	if (S_ISCHR(mode)) 
	{
		inode->i_fop = &def_chr_fops;
		inode->i_rdev = rdev;
	} 
	else if (S_ISBLK(mode)) 
	{
		inode->i_fop = &def_blk_fops;
...

inode 上的 file_operation 并不是自己构造的 file_operation,而是字符设备通用的 def_chr_fops,那么自己构建的 file_operation 等在应用程序调用 open 函数之后,才会绑定在文件上。

自动创建设备节点

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.h
	   typedef __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.o

CC=gcc
APP_NAME=char_dev_test

all:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	@${CC} ${APP_NAME}.c -o ${APP_NAME}
	@make clear

clear:
	@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 *.unsigned

clean:
	@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.o

CC=arm-linux-gnueabihf-gcc
APP_NAME=char_dev_test

all:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	@${CC} ${APP_NAME}.c -o ${APP_NAME}

	@make clear


clear:
	@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 *.unsigned

clean:
	@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 -r

root@igkboard:~# insmod char_dev.ko
root@igkboard:~# ls -l /dev/chardev 
crw------- 1 root root 243, 0 Mar 18 04:34 /dev/chardev

root@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;	//私有数据
};

在这里插入图片描述
结合上面的图片可以进一步了解两个结构体之间是如何联系的。


  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值