Char Drivers [LDD3 03]

16 篇文章 0 订阅
14 篇文章 1 订阅

Table of Contents

The Design of scull

Major and Minor Numbers

The Internal Representation of Device Numbers

Allocating and Freeing Device Numbers

Dynamic Allocation of Major Numbers

Some Important Data Structures

File Operations

The file Structure

The inode Structure

Char Device Registration

Device Registration in scull

Open and Release

The open Method

The release Method


The Design of scull


在开始写一个device driver之前,最重要的事情就是搞清楚driver提供什么功能,功能的确定还要考虑user mode会怎么用,只有这些都考虑清楚,才能实现driver最基本的功能。

书里列举了scull device driver的几种类型,针对不同的使用场景和教学目的,会有多个driver版本,这里不一一列举。

Major and Minor Numbers


首先明确一点,char device都是通过sysfs来访问的,在/dev目录下会创建device对应的sysfs,通过操作这些sysfs就能操作设备。

在/dev目录下,通过ls -l可以看到这些device有char device和block device,每个device有major/minor number。

其中,major number用来表明该device和哪个driver关联,minor number被kernel用来确定当前被使用的是哪个device,因为同一个driver可以驱动多个device,每个device share同样的major number,但是具有不同的minor number,这样通过minor number,就可以找到对应的device。

The Internal Representation of Device Numbers

kernel中使用dev_t来表示major和minor number,一般dev_t就是一个32bit值,前12bit代表major number,后面的20bit代表minor number。当然,driver中不能对dev_t的实现细节做假设,而应该使用kernel提供的interface来获取major和minor number:

//根据现有的dev_t获取major和minor number
MAJOR(dev_t dev);
MINOR(dev_t dev);

//根据major和minor number生成dev_t
MKDEV(int major, int minor);

Allocating and Freeing Device Numbers

driver如何获取device的major和minor number呢?可以通过下面的接口来获取:

#include <linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);

这个函数可以申请一个device number的region,也就是不止一个dev number。其中,first表示请求的device number range的start,count表示个数。first中的minor number一般是0,但是可以自己设置。如果count设置的比较大,当前major number里的剩余minor number不够,就可能被分配到下一个major number里去。name就是设备名字,会和请求的device number region关联起来,并在/proc/devices和sysfs下显示。

如果返回0,表示请求的device number分配成功,否则分配失败。

register_chrdev_region只适用于driver提前知道自己需要什么device number,从而通过这种方式向kernel请求。但在很多情况下,driver是不是知道device number的,那么就需要在runtime动态的获取了。

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

alloc_chrdev_region可以在runtime从kernel分配device number。其中,dev是output参数,记录了kernel返回的device number region的start。firstminor是想要申请的minor number的最小值,一般设置为0;count就是需要的device number的个数;name就是device的name,和register_chrdev_region的意义一样。

如论你是用何种方式请求的device number,在不用的时候都需要自己释放:

void unregister_chrdev_region(dev_t first, unsigned int count);

kernel其实并不知道你申请的这些device number会如何使用,通常来讲,device在user 通过sysfs的方式打开的时候就开始使用了,driver则需要实现对应这个device的function,user操作sysfs其实就是在和driver交互。

Dynamic Allocation of Major Numbers

在kernel中,很多的device number已经分配给了常用的device,在Documentation/devices.txt中可以看到哪些device number已经被分配掉了。但是还剩下一些device number没被使用,这样driver有两个选择:1, 从没有被占用的device number中挑选一些出来使用,但是这样在driver大规模部署的时候,可能会发生冲突,因为别的device可能占用这些device number;2, 使用动态分配的方式获取device number。推荐使用第二种方式,也就是使用alloc_chrdev_region的方式动态获取device number,更加灵活。

使用动态分配的方式也有缺点,这个缺点就是不能提前给device create device node了,因为major number会发生变化。一般来说这个问题不大,因为一旦major number创建出来,就可以从/proc/devices中读到。这里有一个例子,在insmod完成以后,通过/proc/devices读取信息,然后创建特殊的device file:

上面的例子,就是insmod成功以后,通过读取/proc/devices找到自己的device,并获取major number,再根据major number创建/dev/scull设备节点。

Some Important Data Structures


driver中涉及到很多的数据结构,在最开始,遇到的是这三个:file_operations, file, inode. 我们首先看这三个数据结构。

File Operations

在前面的讨论中,虽然我们介绍了device number的用法,但是并没有说如何把device number和我们的driver function关联起来,这里就有答案了,就是通过file_operations来实现的。每一个被open的file都有有一个f_op与其对应,这个f_op就是file operations结构体的指针。file_operations里包含了各种各样的函数指针,也就是可以对device进行的各种操作,比如open/close/mmap等,这些操作就对应了system call,比如user mode调用了open,那么driver对应的实现就是open,close和mmap也是类似的。当然,driver可以不实现部分file operations里的函数,如果不实现就设置为NULL,这样kernel会自动处理。

下面是file operations里具体的member:

a) struct module *owner

指向当前的module,一般被初始化成THIS_MODULE,用来防止driver正在被使用的时候被unload。

b) loff_t (*llseek) (struct file *, lofft_t, int);

用来改变文件中当前的读/写位置,新的位置作为返回值。lofft_t即便在32位系统中也是64bits的值。如果出错,返回负值。如果这个函数为NULL,seek将会以难以预测的方式运行。如果driver不想实现,可以使用kernel定义好的llseek函数。

c) ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)

用来读取设备中的数据。如果为NULL,read将会返回-EINVAL。如果读取成功,返回成功读取数据的长度。

d) ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t *)

异步读数据,函数返回时,数据可能还没读取完成。如果为NULL,kernel调用read实现。

e) ssize_t (*write) (struct file *, char __user *, size_t, loff_t *)

向设备写数据,如果为NULL,write调用直接返回-EINVAL。如果成功,返回写入的数据长度。

f) ssize_t (*aio_write) (struct kiocb *, char __user *, size_t, loff_t *)

异步写数据,函数返回时,数据可能还没写完。

g) int (*readdir) (struct file *, void *, filldir_t)

设备文件为NULL,文件系统才需要使用readdir。

h) unsigned int (*poll) (struct file *, struct poll_table_struct *)

设备驱动中的poll对应用户态的三个系统调用:poll, epoll 和 select,都是用来查询针对一个或者多个file descriptor的读写操作是否能以non-block的方式完成,返回值是bit mask,这个function在读写的资源不可用时,可以把当前的process设成睡眠,直到请求的资源可用。如果poll为NULL,表示读写都是non-block的方式。

i) int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

ioctl用于实现device driver特定的一些操作,如果为NULL,不能被kernel识别的ioctl就会返回-ENOTTY.

j) int (*mmap) (struct file *, struct vm_area_struct *);

mmap可以把device的memory映射到当前的进程地址空间中,并在用户态进行直接访问。如果为NULL,调用返回-ENODEV.

k) int (*open) (struct inode *, struct file *);

当device被打开时,open被调用,可以在open里初始化device driver相关的内容。也可以不实现,如果为NULL,device被打开时,driver不会被通知。

l) int (*flush) (struct file *);

当进程关闭了device的fd时,flush被调用,用于将未完成或者pending的操作做完,device driver用的很少了。

m) int (*release) (struct inode *, struct file *);

当file结构体被释放时,release函数会被调用,也可以为NULL。

n)int (*fsync) (struct file *, struct dentry *, int);

承接user mode的fsync系统调用,用于flush data。

o)int (*aio_fsync)(struct kiocb *, int);

异步的fsync。

p)int (*fasync) (int, struct file *, int);

如果device的FASYNC flag发生变化,这个函数被调用。如果driver不支持该操作,可以设置为NULL。

q)int (*lock) (struct file *, int, struct file_lock *);

用于实现file的lock,针对文件的操作会使用,但device driver一般不适用。

r)ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
     ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

用于实现scatter/gather的读写操作,user mode可能需要在一次system call里完成对多个memory area的读写访问,driver如果不支持,就设置为NULL。这样user mode的操作就被kernel转换为多次对read/write的调用。

s)ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

对应于user mode的sendfile系统调用的read端,可以用来把一个file的内容copy到另外一个file上去。比如web server上需要把一个文件的内容传输到network connection上。device driver用不到。

t)ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

对应于sendfile系统调用的另一端,一个page一个page的把内容写到这个file上。device driver用不到。

u)unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

在当前的process的地址空间中找一段,用来map device上的 memory segment,这个函数通常被kernel的memory management调用,device driver一般不实现这个函数。

v)int (*check_flags)(int)

用来检查fcntl调用时传递的flag参数。不知道有啥用。

w)int (*dir_notify)(struct file *, unsigned long);

当user mode通过fcntl来请求directory 变化时的通知事件,device driver不需要实现。

列一下4.15 kernel里的struct file_operations(可能和本书用到的2.6版本相差较大):

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);
	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
			u64);
} __randomize_layout;

The file Structure

struct file,定义在<linux/fs.h>,代表一个被打开的文件,系统里的任何文件被打开,都会有一个对应的file struct,包括但不限于device file。注意,这个file结构体和user mode中的FILE没有半毛钱关系。

当kernel的open被调用时,file struct就会创建,直到file被引用的instance变为0,file struct就会被释放。通常kernel里看到的filp就是指向file structure的指针。看一下file结构体中的一些member:

a)mode_t f_mode;

用来标明这个file的访问权限,如只读,只写,或者可以同时读写。用bitmask表示,如FMODE_READ,FMODE_WRITE。在调用open或者ioctl的时候,driver可以对mode进行检查,而在read、write时不需要,因为kernel已经帮你做过了。

b)loff_t f_pos;

当前的读写位置。driver可以用它知道读写位置,但是只能在llseek这里修改它的值。而read/write调用时也不应该使用这个值,应该使用当时传进来的参数。

c)unsigned int f_flags;

file的flag,访问方式,比如O_RDONLY , O_NONBLOCK , 和 O_SYNC等。driver应该尤其注意检查O_NONBLOCK这个flag,如果设置了,说明针对file的访问是Non-block的。其他的flag很少使用。读写权限的检查应该使用f_mode,不要使用这个flag。

d)struct file_operations *f_op;

和当前的file关联的file operations结构体,里面保存着对应的操作。当file被open的时候被赋值,并在后面的使用中读取。这里要注意的是,kernel不会在别的地方保存这个指针,因此driver可以根据自己的需要修改f_op,这样即便是同一个major number的device,当minor number不同时,可以提供不同的f_ops实现,类似与面向对象编程的重载。

e)void *private_data;

当kernel调用driver的open之前,这个会被设置位NULL,driver可以根据自己的需要保存自己的私有数据。这个在实际的device driver中也非常有用。

f)struct dentry *f_dentry;

和file关联的directory entry。device driver一般不需要关心这个。

列一下4.15的struct file,和2.6版本也有些许不同:

struct file {
	union {
		struct llist_node	fu_llist;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	/*
	 * Protects f_ep_links, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	enum rw_hint		f_write_hint;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct list_head	f_ep_links;
	struct list_head	f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

The inode Structure

inode是kernel内部使用,用以表示文件的结构体。和struct file代表被打开的文件不同,inode代表一个静态的文件。一个文件可以被多次打开,因此可能有多个struct file,但是都对应同一个inode。inode中包含了非常多的东西,但是和device driver相关的是下面这两个。

a)dev_t i_rdev;

如果文件是一个device file,则i_rdev包含了真正的device number。但是不推荐直接读取,应该使用kernel提供的macro来获取device major/minor number:

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

b)struct cdev *i_cdev;

如果这个inode对应一个char device file,i_cdev就指向那个char device。

重要的数据结构介绍完毕,后面就是device driver和kernel交互的一些流程。

Char Device Registration


我们已经知道,kernel用struct cdev用来代表一个char device,在kernel能够使用你的driver之前,需要先创建并注册一个 char device。

创建struct cdev有两种方式:第一种, 由kerne动态帮我们分配一个struct cdev,通过cdev_alloc,driver提供ops,例子如下:

#include <linux/cdev.h>
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;

第二种方式,我们自己创建一个device,里面内嵌cdev,然后调用cdev_init让kernel帮我们初始化。用到的接口如下:

void cdev_init(struct cdev *cdev, struct file_operations *fops);

现在大部分的device driver都是采用第二种方式,自己创建一个结构体,里面包含cdev,然后通过cdev_init把cdev初始化,方式很灵活。

在cdev创建完成,并且ops等初始化做完以后,调用cdev_add,将cdev告诉kernel,同时告诉kernel与此关联的device number,这样就能把我们的cdev结构体,与filesystem里的device关联起来:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

需要注意的时,一旦调用了cdev_add,就代表device对应的功能已经ready,kernel会随时调用进来,所以必须等准备工作都做完以后,再调用cdev_add。如果需要移除设备,调用cdev_del:

void cdev_del(struct cdev *dev);

Device Registration in scull

这里是一个内嵌cdev并注册device的例子:

struct scull_dev {
    struct scull_qset *data;     /* Pointer to first quantum set */
    int quantum;                 /* the current quantum size */
    int qset;                    /* the current array size */
    unsigned long size;          /* amount of data stored here */
    unsigned int access_key;     /* used by sculluid and scullpriv */
    struct semaphore sem;        /* mutual exclusion semaphore */
    struct cdev cdev;            /* Char device structure */
};

static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);

    //调用cdev_init初始化内嵌的cdev
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be */
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}

Open and Release

The open Method

driver里的open,是在对应的device设备文件被打开的时候,此时通常做一些初始化设备的操作,所有后续的操作都在open之后才会开始。driver的open,通常作如下事情:

1. 检查是否有device-specific 的错误,如device not ready,或者类似的硬件错误。

2. 如果device是第一次被打开,那么初始化这个device。

3. 如果有必要,更新filp里的f_op。(这个f_op就是struct file operations)

4. 分配并设置driver的private data。所谓设置,就是记在filp->private_data里。

driver在开始处理open的时候,第一步得知道具体是哪个device被open了,因为driver有可能同时support 很多device。再看一下open的函数原型:

int (*open)(struct inode *inode, struct file *filp);

在open传进来的inode里,记录着i_cdev,这个就是driver之前创建,初始化并注册进kernel的cdev,利用cdev指针,通过container_of来获取对应的scull_dev指针即可:

container_of(pointer, container_type, container_field);

struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */

获取到scull_dev的指针之后,就可以把scull_dev记录在filp->private_data里。因为后续的操作,比如read/write/ioctl都是只有filp参数,通过filp->private_data可以直接拿到scull_dev,从而拿到driver的所有东西。另外,查看哪个设备被打开,需要查看inode里的minor number,通过之前提到的宏就可以实现:

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

这里比较完整的open的例子如下:

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev; /* device information */

    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev; /* for other methods */

    /* now trim to 0 the length of the device if open was write-only */
    if ( (filp->f_flags & O_ACCMODE) = = O_WRONLY) {
        scull_trim(dev); /* ignore errors */
    }

    return 0;/* success */
}

The release Method

driver里的release,简单来说就是把open获取资源对应的释放掉。具体的操作:

1, 释放open中分配的资源,尤其是记录在filp->private_data里的东西;

2, 如果device被彻底close,就关闭设备。

这里提到,process里的dup和fork不会调用到device driver的open。因为OS给每个open的filp都维护了一个计数,当dup或者fork的时候,OS只是增加了filp的计数,并不会直接调用driver的open函数。同样的,上层调用close的时候,OS只是减少了计数,等到计数为0,也就是再也没有人使用filp了,才会调用device driver的release。OS会保证driver的open和release会是一一对应的,一次open必会只对应一次的release。

当user application退出,包括异常退出,process在被终止的时候,kernel都会把这个process打开的所有flip都释放掉。当然,除了filp,还有占用的各种resource,比如memory等。

后面开始将解释scull的实现细节。

scull’s Memory Usage


这里注意,read/write调用进来的时候,buffer都是user mode的,也就说这些指针都是用户态指针,kernel不能直接使用,需要通过copy_from_user/copy_to_user来转换。这两个函数中:

1,会对user mode地址进行检查,如果无效,函数会返回未被copy的字节数;

2, 如果对应的user page不存在,会调用page fault,所以使用这两个函数的地方要支持可重入。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值