Linux 驱动之字符设备

构架图

申请字符设备号

什么是设备号

Linux规定每一个字符设备或者块设备都必须有一个专属的设备号。一个设备号由主设备号和次设备号组成。主设备号用来表示某一类驱动,如鼠标,键盘都可以归类到USB驱动中。而次设备号是用来表示这个驱动下的各个设备。比如第几个鼠标,第几个键盘等。
所以,我们开发字符驱动程序,申请设备号是第一步,只有有了设备号,才可以像系统注册设备。

设备号的类型

Linux中使用一个名为dev_t的数据类型表示设备号。dev_t定义在include/linux/types.h里面。如下图所示,通过定义可以看出dev_tu32类型,也就是unsigned int类型。所以设备号是一个32位的数据类型。其中高12位为主设备号,低20位为次设备号。

typedef u32 __kernel_dev_t;
typedef __kernel_dev_t          dev_t;

所以,我们开发字符驱动程序,申请设备号是第一步,只有有了设备号,才可以像系统注册设备。

设备号操作宏

在文件include/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中获取次设备号,本质是取低20位的值。

  • MKDEV用于将主设备号ma和次设备号mi组成成dev_t类型的设备号。

设备号分配

​ 在编写字符设备驱动代码的时候,可以静态分配设备号或者动态分配设备号

静态分配设备号指的是开发人员自己指定一个设备号,比如选择50这个设备号作为主设备号。因为有些设备号可能已经被系统使用了,在指定设备号的时候就不能在使用这些已经被系统使用的设备号了。使用命令cat /proc/devices命令可以查看当前系统中已经使用了哪些设备号。

动态分配设备号指的是系统会自动给我们选择一个还没有被使用的设备号,这样就自动避免了设备号分配冲突的问题。
​ 在内核中,提供了动态分配设备号和静态分配设备号的函数,定义在include/linux/fs.h里面,如下表。

静态分配设备号函数 register_chrdev_region
动态分配设备号函数 alloc_chrdev_region
  • register_chrdev_region函数

    函数原型:int register_chrdev_region(dev_t, unsigned, const char *);

    函数参数:
    参数1:设备号的起始值,类型是dev_t。比如MKDEV(100,0),表示起始主设备号100,起始次设备号为0。

    ​ 参数2:次设备号的数量,表示在主设备号相同的情况下有几个次设备号。

    ​ 参数3:设备的名称。

    ​ 函数返回值:成功返回0,失败返回值小于0

  • alloc_chrdev_region函数

    函数原型:int alloc_chrdev_region(dev_t*, unsigned, unsigned, const char *);

    函数参数:
    参数1:保存自动申请到的设备号。

    ​ 参数2:次设备号的起始地址,次设备号一般从0开始,所以这个参数一般设置成0。

    ​ 参数3:要申请的设备号的数量。

    ​ 参数4:设备的名字。

    ​ 函数返回值:成功返回0,失败返回值小于0

  • unregister_chrdev_region函数
    函数原型:extern void unregister_chrdev_region(dev_t, unsigned);

    函数功能:设备号释放函数,注销字符设备以后要释放掉设备号。

    函数参数:

    ​ 参数1:要释放的设备号。

    ​ 参数2:释放的设备号的数量。

例子

dev_t.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>

static int major = 0;
static int minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

dev_t dev_num;
static int moduleparam_init(void)
{
    
    
    int ret;
    if (major)
    {
    
    
        printk("major is %d\n", major);
        printk("minor is %d\n", minor);
        dev_num = MKDEV(major, minor);
        ret = register_chrdev_region(dev_num, 1, "chrdev_name");
        if (ret)
        {
    
    
            printk("register_chrdev_region failed\n");
        }
        printk("register_chrdev_region succeed\n");
    }
    else
    {
    
    
        ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
        if (ret)
        {
    
    
            printk("alloc_chrdev_region failed\n");
        }
        printk("alloc_chrdev_region succeed\n");
        major = MAJOR(dev_num);
        minor = MINOR(dev_num);
        printk("major is %d\n", major);
        printk("minor is %d\n", minor);
    }
    return 0;
}

static void moduleparam_exit(void)
{
    
    
    unregister_chrdev_region(dev_num, 1);
    printk("bye bye\n");
}

module_init(moduleparam_init);
module_exit(moduleparam_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

Makefile

obj-m += dev_t.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD?=$(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.orde \.*.cmd *mod
  1. 静态分配设备号:首先通过cat /proc/devices查看哪些主设备号没有被占用的,比如220没有被占用,则通过sudo insmod dev_t.ko major=220 minor=0

    [795650.456421] major is 220
    [795650.456423] minor is 0
    [795650.456424] register_chrdev_region succeed
    [795677.933220] bye bye

  2. 动态分配设备号:直接通过sudo insmod dev_t.ko

    [795552.620925] alloc_chrdev_region succeed
    [795552.620927] major is 240
    [795552.620927] minor is 0
    [795632.868577] bye bye

注释字符类设备

cdev结构体

Linux中,使用cdev结构体描述一个字符设备。cdev结构体定义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;
} __randomize_layout;

cdev_init函数

cdev_init函数用于初始化cdev结构体成员,建立cdevfile_operations之间的联系。函数原型如下(fs/char_dev.c):

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    
    
    memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
    //建立cdev和file_operations之间的联系
	cdev->ops = fops;
}

cdev_add函数

cdev_add函数用于向系统添加一个cdev结构体,也就是添加一个字符设备

/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    
    
	int error;

	p->dev = dev;
	p->count = count;

	if (WARN_ON(dev == WHITEOUT_DEV))
		return -EBUSY;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}

cdev_del函数

cdev_del函数用于向系统删除一个cdev结构体

/**
 * cdev_del() - remove a cdev from the system
 * @p: the cdev structure to be removed
 *
 * cdev_del() removes @p from the system, possibly freeing the structure
 * itself.
 *
 * NOTE: This guarantees that cdev device will no longer be able to be
 * opened, however any cdevs already open will remain and their fops will
 * still be callable even after cdev_del returns.
 */
void cdev_del(struct cdev *p)
{
    
    
        cdev_unmap(p->dev, p->count);
        kobject_put(&p->kobj);
}

例子

cdev.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

dev_t dev_num;
struct cdev cdev_test;

struct file_operations cdev_test_ops = {
    
    
    .owner = THIS_MODULE,
};

static int modulecdev_init(void)
{
    
    
    int ret;
    ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
    if (ret)
    {
    
    
        printk("alloc_chrdev_region failed\n");
    }
    printk("alloc_chrdev_region succeed\n");

    cdev_test.owner = THIS_MODULE;
    cdev_init(&cdev_test, &cdev_test_ops);
    cdev_add(&cdev_test, dev_num, 1);
    return 0;
}

static void modulecdev_exit(void)
{
    
    
    unregister_chrdev_region(dev_num, 1);
    cdev_del(&cdev_test);
    printk("bye bye\n");
}

module_init(modulecdev_init);
module_exit(modulecdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

Makefile

obj-m += cdev.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD?=$(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.orde \.*.cmd *mod

file operations结构体的作用

Linux有一个很重要的概念叫一切皆文件,也就是Linux中的设备就像普通的文件一样。访问一个设备就好像是在访问一个文件。在应用程序中我们可以使用open,read,write,close,ioctl这个几个系统调用来操作驱动。当我们在应用程序中调用open函数的时候,最终会去执行驱动中的open函数。所以file_operations将系统调用和驱动程序连接起来了。

image-20230329153123789

file_operations结构体

file_operations结构体定义在include/linux/fs.h文件当中,这个结构体非常的庞大。

struct file_operations {
    
    
	struct module *owner;//拥有改结构体的模块指针,一般设置为THIS_MODULE
	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); //与unlocked_ioctl功能一样
	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);
	int (*setfl)(struct file *, unsigned long);
	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);
	int (*setlease)(struct file *, long, struct file_lock **, 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);
} __randomize_layout;

设备节点概念

什么是设备节点

本着Linux中一切皆文件的思想。每个设备在Linux系统中都有一个对应的"设备文件"代表他们,应用程序通过操作这个“设备文件”,便可以操作对应的硬件。如下代码所示:
fd = open("/dev/hello",O_RDWR);
这个“设备文件"就是设备节点。所以Linux设备节点是应用程序和驱动程序沟通的一个桥梁。设备节点被创建在dev目录下。

crw-rw-r--+ 1 root netdev 10, 242 Mar 19 18:55 /dev/rfkill

上述中rfkill就是设备节点,位于/dev目录下,c表示他是一个字符设备,10是主设备号,242是次设备号。Linux可以通过主设备号来找到他对应的file_operations结构体。通过次设备号找到这个设备是同类设备中的第几个。这样就确定了是哪个驱动程序。

Linux创建节点的方式

手动生成设备节点

可以通过命令mknod创建设备节点。

mknod命令格式:

mknod 设备节点名称 设备类型(字符设备用c,块设备用b) 主设备号 次设备号
举例:
mknod /dev/test c 236 0
例子

file.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

dev_t dev_num;
struct cdev cdev_test;

static int major = 0;
static int minor = 0;

static int cdev_test_open(struct inode *inode, struct file *file)
{
    
    
    printk("This is a cdev_test_open\n");
    return 0;
}

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    
    printk("This is a cdev_test_read\n");
    return 0;
}

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    
    
    printk("This is a cdev_test_write\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    
    
    printk("This is a cdev_test_release\n");
    return 0;
}

struct file_operations cdev_test_ops = {
    
    
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
};

static int file_init(void)
{
    
    
    int ret;
    ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
    if (ret)
    {
    
    
        printk("alloc_chrdev_region failed\n");
    }
    printk("alloc_chrdev_region succeed\n");

    major = MAJOR(dev_num);
    minor = MINOR(dev_num);
    printk("major is %d\n", major);
    printk("minor is %d\n", minor

    cdev_test.owner = THIS_MODULE;
    cdev_init(&cdev_test, &cdev_test_ops);
    cdev_add(&cdev_test, dev_num, 1);

    return 0;
}

static void file_exit(void)
{
    
    
        unregister_chrdev_region(dev_num, 1);
        cdev_del(&cdev_test);
        printk("bye bye\n");
}

module_init(file_init);
module_exit(file_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

app.c(gcc app.c -o app.out)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    
    
    int fd;
    char buf[64] = {
    
    };
    fd = open("/dev/test", O_RDWR); // 打开设备节点
    if (fd < 0)
    {
    
    
        perror("open error \n");
        return fd;
    }
    close(fd);
    return 0;
}

  • sudo insmod file.ko

    [805697.526236] alloc_chrdev_region succeed
    [805697.526237] major is 240
    [805697.526237] minor is 0

  • sudo mknod /dev/test c 240 0

  • sudo ./app.out

    [806101.252401] This is a cdev_test_open
    [806101.252402] This is a cdev_test_release

自动生成设备节点

可以通过mdev机制实现设备节点的自动创建与删除

udev机制

Linux中可以通过udev来实现设备节点的创建与删除,udev是一个用户程序,可以根据系统中设备的状态来创建或者删除设备节点,比如当驱动程序成功加载到Linux时会自动在/dev目录下创建对应的设备节点,当驱动程序卸载的时候会自动删除/dev目录下设备节点。
在嵌入式Linux中我们使用的是mdevmdevudev的简化版本。在使用busybox构建根文件系统的时候,busybox会自动创建mdev

image-20230329175552120

class_create函数

class_create函数定义在include/linux/device.h文件当中,是一个宏定义,使用这个函数会在/sys/class下创建文件,如下图所示:

/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */
#define class_create(owner, name)		\
({
      
      						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

extern struct class * __must_check __class_create(struct module *owner,
						  const char *name,
						  struct lock_class_key *key);

class_create一共有俩个参数,第一个参数owner,一般为THIS_MODULE,第个参数name是类的名字。

device_create函数

使用class_create创建好类以后,还需要使用device_create函数在类下面创建一个设备。定义在include/linux/device.h文件当中。如下所示:

struct device *device_create(struct class *cls, struct device *parent,
			     dev_t devt, void *drvdata,
			     const char *fmt, ...);

第一参数class表示这个设备创建在哪个类下面,

第二个参数parent是父设备,一般设置成NULL,也就是没有父设备。

第三个参数dev_t是设备号。第四个参数drvdata是设备可能会用到的数据,设置成NULL

第五个参数fmt是设备节点的名字。

device_destroy函数

使用device_destroy函数可以删掉创建的设备,函数原型如下:

extern void device destroy(struct class *cls, dev t devt);

参数class是要删除的设备所处的类,dev_t是要删除的设备号

class_destroy函数

使用class_destroy函数可以删掉创建的类,函数原型如下:

extern void class destroy(struct class *cls);

参数class是要删除的类

例子

file.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>

dev_t dev_num;
struct cdev cdev_test;

static int major = 0;
static int minor = 0;

struct class *class;
struct device *device;

static int cdev_test_open(struct inode *inode, struct file *file)
{
    
    
    printk("This is a cdev_test_open\n");
    return 0;
}

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    
    printk("This is a cdev_test_read\n");
    return 0;
}

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    
    
    printk("This is a cdev_test_write\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    
    
    printk("This is a cdev_test_release\n");
    return 0;
}

struct file_operations cdev_test_ops = {
    
    
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
};

static int file_init(void)
{
    
    
    int ret;
    ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
    if (ret)
    {
    
    
        printk("alloc_chrdev_region failed\n");
    }
    printk("alloc_chrdev_region succeed\n");

    major = MAJOR(dev_num);
    minor = MINOR(dev_num);
    printk("major is %d\n", major);
    printk("minor is %d\n", minor);

    cdev_test.owner = THIS_MODULE;
    cdev_init(&cdev_test, &cdev_test_ops);
    cdev_add(&cdev_test, dev_num, 1);

    class_create(THIS_MODULE, "test");
    device_create(class, NULL, dev_num, NULL, "test");

    return 0;
}

static void file_exit(void)
{
    
    
    unregister_chrdev_region(dev_num, 1);
    cdev_del(&cdev_test);

    device_destroy(class, dev_num);
    class_destroy(class);
    printk("bye bye\n");
}

module_init(file_init);
module_exit(file_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

app.c(gcc app.c -o app.out)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    
    
    int fd;
    char buf[64] = {
    
    };
    fd = open("/dev/test", O_RDWR); // 打开设备节点
    if (fd < 0)
    {
    
    
        perror("open error \n");
        return fd;
    }
    close(fd);
    return 0;
}

  1. sudo insmod file.ko

    [ 372.600726] alloc_chrdev_region succeed
    [ 372.600728] major is 240
    [ 372.600728] minor is 0

  2. sudo ./app.out

    fengzc@ubuntu:~/study/drivers_test/05_file$ ls -l /sys/class/test
    total 0
    lrwxrwxrwx. 1 root root 0 Mar 29 18:07 test -> …/…/devices/virtual/test/test
    fengzc@ubuntu:~/study/drivers_test/05_file$ ls -l /dev/test
    crw-------. 1 root root 240, 0 Mar 29 18:04 /dev/test

内核空间与用户空间

Linux系统将可访问的内存空间分为了俩部分,一部分是内核空间,一部分是用户空间。操作系统和驱动程序运行在内核空间(内核态),应用程序运行在用户空间(用户态)。
为什么要区分内核空间和用户空间呢?

  • 内核空间中的代码控制了硬件资源,用户空间中的代码只能通过内核暴露的系统调用接口来使用系统中的硬件资源。这个样的设计可以保证操作系统自身的安全性和稳定性。
  • 从另一方面来说,内核空间的代码更偏向于系统管理,而用户空间中的代码更偏重业务逻辑实现。两者的分工不同。

如何从用户空间进入内核空间?

对硬件的资源的管理都是在内核空间中完成的,应用程序是无法直接对硬件进行操作的,我们只能通过调用内核的接口来完成这样的任务。比如应用程序要读取磁盘上的一个文件,应用程序可以向内核发起一个"系统调用"告诉内核:“我要读取磁盘上的文件”。这个过程其实就是通过一个特殊的指令让进程从用户态进入到内核态,在内核空间中,CPU可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以继续往下执行了。

既然进程要从用户空间切换到内核空间才可以使用系统的硬件资源,切换方式有三种:系统调用软中断硬件中断

image-20230330090940564

用户空间和内核空间数据交换

内核空间和用户空间的内存是不能互相访问的。但是很多业务程序都需要和内核交互数据,比如应用程序使用read函数从驱动中读取数据,使用write函数向驱动中写数据。这就要需要借助copy_from_usercopy_to_user这两个函数完成数据传输。分别是将用户空间的数据拷贝到内核空间以及将内核空间的数据拷贝到用户空间。这俩个函数定义在了linux/include/asm-arm/uaccess.h文件下。

  • copy_to_user函数
    函数原型:unsigned long copy_to_user(void_user *to, const void *from, unsigned long n);

    作用:把内核空间的数据复制到用户空间

    函数参数:*to用户空间的指针。*from内核空间的指针,n是从内核空间向用户空间拷贝的字节数。

    函数返回值:成功返回0

  • copy_from_user函数
    函数原型:unsigned long copy_from_user(void * to, const void_user *from, unsigned long n)

    作用:把用户空间的数据复制到内核空间

    函数参数:*to内核空间的指针。*from用户空间的指针,n是数据的长度

    函数返回值:成功返回0

例子

usr.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

dev_t dev_num;
struct cdev cdev_test;

static int major = 0;
static int minor = 0;

struct class *class;
struct device *device;

static int cdev_test_open(struct inode *inode, struct file *file)
{
    printk("This is a cdev_test_open\n");
    return 0;
}

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    char kbuf[32] = "This is a cdev_test_read";
    if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0)
    {
        printk("copy_to_user is error");
        return -EFAULT;
    }

    printk("This is a cdev_test_read\n");
    return 0;
}

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    char kbuf[32] = {0};
    if (copy_from_user(kbuf, buf, size) != 0)
    {
        printk("copy_from_user is error");
        return -EFAULT;
    }
    printk("kbuf: %s\n", kbuf);
    printk("This is a cdev_test_write\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is a cdev_test_release\n");
    return 0;
}

struct file_operations cdev_test_ops = {
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
};

static int usr_init(void)
{
    int ret;
    ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
    if (ret)
    {
        printk("alloc_chrdev_region failed\n");
    }
    printk("alloc_chrdev_region succeed\n");

    major = MAJOR(dev_num);
    minor = MINOR(dev_num);
    printk("major is %d\n", major);
    printk("minor is %d\n", minor);

    cdev_test.owner = THIS_MODULE;
    cdev_init(&cdev_test, &cdev_test_ops);
    cdev_add(&cdev_test, dev_num, 1);

    class = class_create(THIS_MODULE, "test");
    device_create(class, NULL, dev_num, NULL, "test");

    return 0;
}

static void usr_exit(void)
{
    unregister_chrdev_region(dev_num, 1);
    cdev_del(&cdev_test);

    device_destroy(class, dev_num);
    class_destroy(class);
    printk("bye bye\n");
}

module_init(usr_init);
module_exit(usr_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

app.c(gcc app.c -o app.out)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    
    
    int fd;
    char buf1[32] = {
    
    };
    char buf2[32] = {
    
    "nihao"};
    fd = open("/dev/test", O_RDWR); // 打开设备节点
    if (fd < 0)
    {
    
    
        perror("open error \n");
        return fd;
    }
    read(fd, buf1, sizeof(buf1));
    printf("buf1 is %s\n", buf1);


    write(fd, buf2, sizeof(buf2));
    return 0;
}

使用文件私有数据

文件私有数据是什么?

文件私有数据的概念在Linux驱动中有着非常广泛的使用。文件私有数据就是将私有数据private_data指向设备结构体。

然后在readwrite等函数中通过private_data访问设备结构体。

为什么要使用文件私有数据?

Linux中没有明确的规定必须要使用文件私有数据,但是文件私有数据的使用在Linux驱动中广泛使用,这是Linux驱动遵循的“潜规则”。实际上也体现了Linux面向对象的思想。

私有数据使用场景

Linux中,使用主设备号来表示对应的某一类驱动。用次设备号表示这类驱动下的各个设备假如现在我们的驱动要支持主设备相同,但是次设备号不同的设备。我们的驱动要怎么写呢,这个就用到了的file结构体中的私有数据private_date

container_of函数

函数原型:container_of(ptr, type, member)

函数功能:通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址。

函数参数:第一个参数ptr是结构体变量中某个成员的地址。第二个参数type是结构体的类型。第三个参数member是该结构体变量的具体名字。

例子

usr.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

struct device_test
{
    
    
    dev_t dev_num;
    struct cdev cdev_test;

    int major;
    int minor;

    struct class *class;
    struct device *device;

    char kbuf[32];
};

struct device_test dev1;
struct device_test dev2;

static int cdev_test_open(struct inode *inode, struct file *file)
{
    
    
    dev1.minor = 0;
    dev2.minor = 1;
    file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);
    printk("This is a cdev_test_open\n");
    return 0;
}

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    
    struct device_test *dev = (struct device_test *)file->private_data;
    if (copy_to_user(buf, dev->kbuf, strlen(dev->kbuf)) != 0)
    {
    
    
        printk("copy_to_user is error");
        return -EFAULT;
    }
    return 0;
}

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    
    
    struct device_test *dev = (struct device_test *)file->private_data;
    if (dev->minor == 0)
    {
    
    
        if (copy_from_user(dev->kbuf, buf, size) != 0)
        {
    
    
            printk("copy_from_user is error");
            return -EFAULT;
        }
        printk("kbuf is %s\n", dev->kbuf);
    }
    else if (dev->minor == 1)
    {
    
    
        if (copy_from_user(dev->kbuf, buf, size) != 0)
        {
    
    
            printk("copy_from_user is error");
            return -EFAULT;
        }
        printk("kbuf is %s\n", dev->kbuf);
    }

    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    
    
    return 0;
}

struct file_operations cdev_test_ops = {
    
    
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
};

static int usr_init(void)
{
    
    
    int ret;
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name");
    if (ret)
    {
    
    
        printk("alloc_chrdev_region failed\n");
    }
    printk("alloc_chrdev_region succeed\n");

    dev1.major = MAJOR(dev1.dev_num);
    dev1.minor = MINOR(dev1.dev_num);
    printk("major is %d\n", dev1.major);
    printk("minor is %d\n", dev1.minor);

    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_ops);
    cdev_add(&dev1.cdev_test, dev1.dev_num, 1);

    dev1.class = class_create(THIS_MODULE, "test1");
    device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");

    dev2.major = MAJOR(dev1.dev_num + 1);
    dev2.minor = MINOR(dev1.dev_num + 1);
    printk("major is %d\n", dev2.major);
    printk("minor is %d\n", dev2.minor);

    dev2.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev2.cdev_test, &cdev_test_ops);
    cdev_add(&dev2.cdev_test, dev1.dev_num + 1, 1);

    dev2.class = class_create(THIS_MODULE, "test2");
    device_create(dev2.class, NULL, dev1.dev_num + 1, NULL, "test2");

    return 0;
}

static void usr_exit(void)
{
    
    
    unregister_chrdev_region(dev1.dev_num, 1);
    unregister_chrdev_region(dev1.dev_num + 1, 1);
    cdev_del(&dev1.cdev_test);
    cdev_del(&dev2.cdev_test);

    device_destroy(dev1.class, dev1.dev_num);
    device_destroy(dev2.class, dev1.dev_num + 1);
    class_destroy(dev1.class);
    class_destroy(dev2.class);
    printk("bye bye\n");
}

module_init(usr_init);
module_exit(usr_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

app.c(gcc app.c -o app.out)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    
    
    int fd1, fd2;
    char buf1[32] = {
    
    "niaho /dev/test1"};
    char buf2[32] = {
    
    "niaho /dev/test2"};
    fd1 = open("/dev/test1", O_RDWR); // 打开设备节点
    if (fd1 < 0)
    {
    
    
        perror("open error \n");
        return fd1;
    }
    write(fd1, buf1, sizeof(buf1));
    close(fd1);

    fd2 = open("/dev/test2", O_RDWR); // 打开设备节点
    if (fd2 < 0)
    {
    
    
        perror("open error \n");
        return fd2;
    }
    write(fd2, buf2, sizeof(buf2));
    close(fd2);
    return 0;
}

  1. sudo insmod file.ko

    [ 217.022060] alloc_chrdev_region succeed
    [ 217.022062] major is 240
    [ 217.022062] minor is 0
    [ 217.024609] major is 240
    [ 217.024610] minor is 1

  2. sudo ./app.out

    fengzc@ubuntu:~/study/drivers_test/08_private_data_test$ ls -l /sys/class/test*
    /sys/class/test1:
    total 0
    lrwxrwxrwx. 1 root root 0 Mar 29 22:03 test1 -> …/…/devices/virtual/test1/test1

    /sys/class/test2:
    total 0
    lrwxrwxrwx. 1 root root 0 Mar 29 22:03 test2 -> …/…/devices/virtual/test2/test2

    fengzc@ubuntu:~/study/drivers_test/08_private_data_test$ ls -l /dev/test*
    crw-------. 1 root root 240, 0 Mar 29 22:02 /dev/test1
    crw-------. 1 root root 240, 1 Mar 29 22:02 /dev/test2

    [ 271.300678] This is a cdev_test_open
    [ 271.300681] kbuf is niaho /dev/test1
    [ 271.300685] This is a cdev_test_open
    [ 271.300686] kbuf is niaho /dev/test2

杂项设备驱动

杂项设备的描述

Linux中,把无法归类的五花八门的设备定义成杂项设备。相对与字符设备来说,杂项设备主设备固定为10,而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,比较浪费主设备号。杂项设备会自己调用class_create()device_create()来自动创建设备节点。所以可以把杂项设备看成是字符设备的一种。但是比我们平常写的字符设备降低了难度并节约了主设备号。

杂项设备使用结构体miscdevice描述,定义在include/linux/miscdevice.h文件当中。如下所示:

struct miscdevice  {
    
    
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
	struct device *parent;
	struct device *this_device;
	const struct attribute_group **groups;
	const char *nodename;
	umode_t mode;
};

其中次设备号minor一般使用宏MISC_DYNAMIC_MINOR,表示自动分配次设备号。杂项设备主要依赖次设备号来管理不同的杂项设备。

杂项设备的注册与卸载

注册杂项设备使用函数misc_register函数,卸载杂项设备使用misc_deregister函数。这俩个函数均定义在include\linux\miscdevice.h文件当中。

misc_register函数

函数原型:int misc_register(struct miscdevice *misc)

参数:杂项设备的结构体指针

返回值:成功返回0,失败返回负数。

misc_deregister函数

函数原型:int misc_deregister(struct miscdevice *misc)

参数:杂项设备的结构体指针

返回值:成功返回0,失败返回负数。

例子

usr.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>

static int cdev_test_open(struct inode *inode, struct file *file)
{
    
    
    printk("This is a cdev_test_open\n");
    return 0;
}

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    
    return 0;
}

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    
    
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    
    
    return 0;
}

struct file_operations cdev_test_ops = {
    
    
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
};

struct miscdevice misc_dev = {
    
    
    .minor = MISC_DYNAMIC_MINOR,
    .name = "test",
    .fops = &cdev_test_ops,
};

static int usr_init(void)
{
    
    
    int ret;
    ret = misc_register(&misc_dev);
    if (ret < 0)
    {
    
    
        printk("misc_register failed");
        return -1;
    }
    return 0;
}

static void usr_exit(void)
{
    
    
    misc_deregister(&misc_dev);
    printk("bye bye\n");
}

module_init(usr_init);
module_exit(usr_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

fengzc@ubuntu:~/study/drivers_test/09_misc$ ls -l /sys/class/misc/test
lrwxrwxrwx. 1 root root 0 Mar 29 22:25 /sys/class/misc/test -> …/…/devices/virtual/misc/test
fengzc@ubuntu:~/study/drivers_test/09_misc$ ls -l /dev/test
crw-------. 1 root root 10, 56 Mar 29 22:25 /dev/test
fengzc@ubuntu:~/study/drivers_test/09_misc$

Linux驱动错误处理

在使用goto语句处理错误时,要遵循“先进后出”的原则

image-20230330133156826

内核中保留了地址0xffffffffff000~0xfffffffffff (64位系统)用来记录错误码,这段地址和Linux的错误码是一一对应的。内核基本错误码保存在errno-base.h文件当中。

/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define EPERM            1      /* Operation not permitted */
#define ENOENT           2      /* No such file or directory */
#define ESRCH            3      /* No such process */
#define EINTR            4      /* Interrupted system call */
#define EIO              5      /* I/O error */
#define ENXIO            6      /* No such device or address */
#define E2BIG            7      /* Argument list too long */
#define ENOEXEC          8      /* Exec format error */
#define EBADF            9      /* Bad file number */
#define ECHILD          10      /* No child processes */
#define EAGAIN          11      /* Try again */
#define ENOMEM          12      /* Out of memory */
#define EACCES          13      /* Permission denied */
#define EFAULT          14      /* Bad address */
#define ENOTBLK         15      /* Block device required */
#define EBUSY           16      /* Device or resource busy */
#define EEXIST          17      /* File exists */
#define EXDEV           18      /* Cross-device link */
#define ENODEV          19      /* No such device */
#define ENOTDIR         20      /* Not a directory */
#define EISDIR          21      /* Is a directory */
#define EINVAL          22      /* Invalid argument */
#define ENFILE          23      /* File table overflow */
#define EMFILE          24      /* Too many open files */
#define ENOTTY          25      /* Not a typewriter */
#define ETXTBSY         26      /* Text file busy */
#define EFBIG           27      /* File too large */
#define ENOSPC          28      /* No space left on device */
#define ESPIPE          29      /* Illegal seek */
#define EROFS           30      /* Read-only file system */
#define EMLINK          31      /* Too many links */
#define EPIPE           32      /* Broken pipe */
#define EDOM            33      /* Math argument out of domain of func */
#define ERANGE          34      /* Math result not representable */

#endif

内核中的函数常常返回指针,如果内核返回一个指针,那么就有三种情况:合法指针NULL指针非法指针

如何判断函数返回的指针是有效地址还是错误码呢?使用IS_ERR函数去检查函数的返回值,如果地址落在0xffffffffooo~oxfffffffffff范围(64位系统),表示该函数执行失败,IS_ERR为1,同时该函数返回的错误地址对应一个linux的错误号。如果想知道这个地址是哪个错误码,就用PTR_ERR函数来转化。其中IS_ERRPTR_ERR函数定义在errno.h当中。

示例:

if(IS_ERR(dev.device)){
    
    
    ret = PTR_ERR(dev.device)
}

例子

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/errno.h>

struct device_test
{
    
    
    dev_t dev_num;
    struct cdev cdev_test;

    int major;
    int minor;

    struct class *class;
    struct device *device;

    char kbuf[32];
};

struct device_test dev1;

static int cdev_test_open(struct inode *inode, struct file *file)
{
    
    
    file->private_data = &dev1;
    printk("This is a cdev_test_open\n");
    return 0;
}

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    
    struct device_test *dev = (struct device_test *)file->private_data;
    if (copy_to_user(buf, dev->kbuf, strlen(dev->kbuf)) != 0)
    {
    
    
        printk("copy_to_user is error");
        return -EFAULT;
    }
    return 0;
}

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    
    
    struct device_test *dev = (struct device_test *)file->private_data;
    if (copy_from_user(dev->kbuf, buf, size) != 0)
    {
    
    
        printk("copy_from_user is error");
        return -EFAULT;
    }
    printk("kbuf is %s\n", dev->kbuf);
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    
    
    return 0;
}

struct file_operations cdev_test_ops = {
    
    
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
};

static int usr_init(void)
{
    
    
    int ret;
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name");
    if (ret < 0)
    {
    
    
        goto err_chrdev;
        printk("alloc_chrdev_region failed\n");
    }
    printk("alloc_chrdev_region succeed\n");

    dev1.major = MAJOR(dev1.dev_num);
    dev1.minor = MINOR(dev1.dev_num);
    printk("major is %d\n", dev1.major);
    printk("minor is %d\n", dev1.minor);

    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_ops);
    ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if (ret < 0)
    {
    
    
        goto err_chr_add;
    }

    dev1.class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class)){
    
    
        goto err_class_create;
    }
    device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device)){
    
    
        goto err_class_device;
    }

    return 0;
err_class_device:
    class_destroy(dev1.class);  

err_class_create:
    cdev_del(&dev1.cdev_test);

err_chr_add:
    unregister_chrdev_region(dev1.dev_num, 1);

err_chrdev:
    return ret;
}

static void usr_exit(void)
{
    
    
    unregister_chrdev_region(dev1.dev_num, 1);
    cdev_del(&dev1.cdev_test);

    device_destroy(dev1.class, dev1.dev_num);
    class_destroy(dev1.class);
    printk("bye bye\n");
}

module_init(usr_init);
module_exit(usr_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");

猜你喜欢

转载自blog.csdn.net/weixin_45767368/article/details/129857437