Home 字符设备驱动
Post
Cancel

字符设备驱动

linux设备驱动

根据分层抽象的思想,linux将设备分为3类:

  • 字符设备
  • 块设备
  • 网络设备

其中字符设备最常见,主要定义的特性是只能顺序访问,比如典型的串口,鼠标,键盘,一般来说只能是有什么数据来,就读什么,总体上不能过去的数据,对于这种模型, 内核中也没有提供缓存机制;块设备常见于磁盘,主要是支持任意顺序访问,想读写哪里就读写哪里,内核中为其提供一个快速缓冲。块设备和字符设备是linux内核 提供的模型,一个物理设备分类可以不用很严格,不过一般是字符设备;而磁盘也是可以实现为字符设备的,只不过一般没有意义且不适合。还有网络设备,通过套接字 间接访问。

设备文件的访问:字符设备和块设备可以直接通过文件系统中的设备文件来控制,网络设备则要通过套接字访问,参考下图:

设备驱动访问图示

字符设备驱动

字符设备驱动主要提供的功能是将设备以设备文件的形式提供给用户空间程序使用。是最常见的一种形式。

字符设备相关数据结构和流程

字符设备内核描述结构体struct cdev

字符设备在内核的描述结构体struct cdev,定义于 /include/linux/cdev.h中,参考

1
2
3
4
5
6
7
8
struct cdev {
	struct kobject kobj;            //基于内核kobj管理,常见手法
	struct module *owner;           //内核模块的对象指针,如果模块是直接编译进内核的,则为NULL。一般模块会使用THIS_MODULE初始化
	const struct file_operations *ops;  //重要,应用程序对设备文件调用的具体函数回调
	struct list_head list;          //内核中使用list列表结构管理字符设备
	dev_t dev;                      //设备号,主设备号加次设备号
	unsigned int count;             //属于同一主设备号的次设备号个数,驱动控制实际设备的数量
} 

struct cdev的实际使用情况:一般会将该结构体定义在用户具体设备结构体中。通常直接静态定义即可, 内嵌在用户具体设备结构体中。也有动态分配的api(struct cdev *cdev_alloc(void);),一般是不需要的。 参考code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 用户自己具体设备的结构体
struct user_xxx_device{
    struct cdev cdev_qrng;
    /*
        ...
        other attributes,ex: 
        dev_t devno;
        spinlock_t xxx;
        wait_queue_head_t  xxx;
        int xxx_flag;

        regs
        dma
        other states
    */
};

cdev 结构体在使用之前一定需要进行初始化 cdev_init,主要绑定了fops具体操作方法,和一些内核的字符设备框架相关的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 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->ops = fops;
}

文件接口回调函数集 fops

struct file_operations定义在/include/linux/fs.h中。操作函数较多,最常见的使用接口包括open,release, read,write,compat_ioctl,其他根据需要实现。不确定的可以不用实现,系统有默认的方法。

应用程序操作这些接口通过系统调用完成,如 open最后会执行到fops中的open回调,close则为release回调等。了解相关系统调用即可。

框架过程大致补充:首先用户空间的open一个设备文件,open系统调用,进入内核,先找到该文件的inode,调用inode->i_fop->open(), 由于是设备文件,实际会调用对应的chrdev_open函数来处理,该函数通过 inode->i_rdevcdev_map中查找inode对应的字符设备, 找到后,chrdev_open函数将inode->i_cdev指向字符设备对象,同时将 cdev->ops 赋值给filp->f_op。由于字符设备驱动中 实现了file_operatons,所以用户空间的后续open/read/write的调用通过filp->f_op指向了驱动程序中定义的。

参考代码(4.19.279 kernel版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 (*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);
	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);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
}

设备号相关

linux中的设备号类型为 dev_t,具体是一个32位无符号数,高12位定义为主设备号,低20位定义为次设备号。(惯例,12+20),建议使用相关供操作设备号,而不是使用硬编码。

主设备号是用来定位对应的驱动程序的,而次设备号则是该驱动程序标识管理的相关设备的。相当于一个是类,一个是该类实现的实例。如有十个完全相同的设备,驱动程序使用同一份即可, 这个驱动程序由主设备号对应,而每个具体的设备则有各自的次设备号来标识区分。典型的如硬盘,tty设备,loop device等,通过 ls -l /dev 查看主次设备号情况。

设备号操作相关宏,头文件/include/linux/kdev_t.h

1
2
3
4
5
6
#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))

设备号的分配和管控

设备号可以申请指定的设备号或有系统分配一个可用的,一般是让系统分配一个空闲的,而不是自己申请一个静态的,如果自己使用静态的,要考虑是否有冲突的情况。 释放则使用相同的api。这里先记录最后常用的API,头文件/include/linux/fs.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);

/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name);

/**
 * unregister_chrdev_region() - unregister a range of device numbers
 * @from: the first in the range of numbers to unregister
 * @count: the number of device numbers to unregister
 *
 * This function will unregister a range of @count device numbers,
 * starting with @from.  The caller should normally be the one who
 * allocated those numbers in the first place...
 */
void unregister_chrdev_region(dev_t from, unsigned count);

设备号内核管理概览

linux内核中有一个全局的指针数组chrdevs,它是管理设备号的分配和管理的核心结构。相关代码参考(定义在/fs/char_dev.c中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define CHRDEV_MAJOR_HASH_SIZE 255

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

/* index in the above */
static inline int major_to_index(unsigned major)
{
	return major % CHRDEV_MAJOR_HASH_SIZE;
}

该结构是一个静态定义的指针数组(*chrdevs[255]),主设备号是设备号的高12位,有效范围是0-4095,都映射到这个指针数组里面,规则就是简单的取余255, 该数组的index 2 上,所以每一个指针项还有一个次级next指针,每个指针数组入口处再加出一个单链表,形成一个类似二维数组的结构,不过是动态的, 不过一个系统上的驱动程序一般不超过255个,主设备号255基本足够了。次设备号会在同一个指针项所在的列表的单向列表后续节点上。

申请设备号时,主要就是不能和之前的已申请过的产生冲突,register_chrdev_region是自己之定义设备号,如果有冲突则返回失败。alloc_chrdev_region 则是从末尾(254)向前扫描,如果指针数组的该项是空闲的,则分配给申请者,并添加到指针数组中。动态申请注册可以避免设备号冲突问题。驱动卸载时,记得使用 unregister_chrdev_region释放占用的设备号。

字符设备注册相关

在注册了设备号后,就可以将字符设备驱动添加到系统中了。使用如下cdev_add完成。 调用之后主要实现了系统可以找到该设备驱动程序,用户也可以通过文件系统接口调用驱动程序。

1
2
3
4
5
6
7
8
9
10
11
/**
 * 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)

使用上文的 cdev_init 和 这里的 cdev_add之后,就实现了一个绑定,即设备号<–>cdev结构体<–>fops操作方法

设备文件节点注册

设备节点文件,即 /dev 目录下的设备文件,是用来给应用程序访问内核的设备驱动程序用的。如果设备驱动程序只为内核服务,则完全可以不用生成设备节点文件。

(1)在较早的时候,使用手动注册,即一种静态的方式。通过mknod命令或系统调用来完成,将指定主次设备号来创建的设备文件,核心就是mknod系统调用。

1
2
3
4
5
# 查看主设备号
cat /proc/devices

# 创建设备文件
mknod /dev/myxxxdev c [major] [minor]

大致原理过程:mknod会产生一个设备文件的inode,同时会将指定的设备号记录到inode->i_rdev中,同时inode的i_fop会将open成员指向chrdev_open函数。

(2)后来出现过devfs机制,不过仅存在于2.3.46-2.6.13版本内核中。之后就被淘汰了。

(3)现代主流的udev机制,配合sysfs实现,还同时解决一些热拔插相关问题。在嵌入式环境还有个简化版的mdev,功能类似。大致实现原理:两部分组成, 内核一部分代码和用户空间进程,一般是守护进程。内核的那部分通过uevent机制向用户态发送特定的信号到用户空间,被用户空间的守护进程接收到, 由用户空间的守护进程来进行设备文件的创建或其他操作,实现动态创建或删除设备节点文件等,热拔插管理。大头是用户空间的守护进程udevd,操作配置也较多。

使用现代方式自动创建设备节点文件。device_createclass_create,符合linux驱动设备模型。创建设备时需要归属一个类,一般会单独创建一个。 头文件include/linux/device.hdevice_create内部会调用关键的device_add函数,里面会调用kobject_uevent发送KOBJ_ADD信号到用户空间udev程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * device_create - creates a device and registers it with sysfs
 * @class: pointer to the struct class that this device should be registered to
 * @parent: pointer to the parent struct device of this new device, if any
 * @devt: the dev_t for the char device to be added
 * @drvdata: the data to be added to the device for callbacks
 * @fmt: string for the device's name
 *
 * This function can be used by char device classes.  A struct device
 * will be created in sysfs, registered to the specified class.
 *
 * A "dev" file will be created, showing the dev_t for the device, if
 * the dev_t is not 0,0.
 * If a pointer to a parent struct device is passed in, the newly created
 * struct device will be a child of that device in sysfs.
 * The pointer to the struct device will be returned from the call.
 * Any further sysfs files that might be required can be created using this
 * pointer.
 *
 * Returns &struct device pointer on success, or ERR_PTR() on error.
 *
 * Note: the struct class passed to this function must have previously
 * been created with a call to class_create().
 */
struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
{
	va_list vargs;
	struct device *dev;

	va_start(vargs, fmt);
	dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
	va_end(vargs);
	return dev;
}
EXPORT_SYMBOL_GPL(device_create);

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

参考代码,创建设备类my_xxx_class,并创建设备节点文件my_xxx_dev0,可根据情况修改。

1
2
3
4
5
6
7
8
9
	struct class *my_xxx_class;

	//自动创建
    my_xxx_class = class_create(THIS_MODULE,"my_xxx_class");
    device_create(my_xxx_class,NULL,p_one_inst->devno_my_xxx,p_one_inst,"my_xxx_dev%d",0);

	//卸载时删除
	device_destroy(my_xxx_class,p_one_inst->devno_my_xxx);
    class_destroy(my_xxx_class);

设备寄存器读写

设备的寄存器读写分两步,第一个建立地址映射,第二步进行io读写。由于CPU发出的地址是线性地址(虚拟地址),所以无法直接通过物理地址进行io读写访问, 需要先进行一个ioremap操作,让内核建立物理地址到虚拟地址的映射,后续操作该虚拟地址进行读写。

1
2
3
4
5
6
7
8
9
10
11
ioremap 大致声明,另外不同的子系统中一般都有其扩展版本,更方便使用,本质相同。
void* ioremap(u64 padddr,int map_size);
void  iounmap(void* paddr);

io读写大致声明,早期有另一个版本的readb,readl这种,不建议使用。
u8 ioread8(void* vaddr);
u16 ioread16(void* vaddr);
u32 ioread32(void* vaddr);
void iowrite8(u8 val,void* vaddr);
void iowrite16(u16 val,void* vaddr);
void iowrite32(u32 val,void* vaddr);
This post is licensed under CC BY 4.0 by the author.

NUMA内存模型

内核内存管理