Linux设备驱动--字符设备(一)

时间:2022-05-21 17:55:18
Linux的设备分为三类:字符设备、块设备和网络设备。
字符设备:指一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,数去数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等;
块设备:指可以从设备的任意位置读取一定长度数据的设备。常见块设备包括硬盘、磁盘、U盘和SD卡等;

网络设备:用来和外界交换数据报文时调用的设备。如网卡、VETH、TAP等。


一、基础


1、设备号

包括主设备号和次设备号,用来区分指定的外设。主设备号说明设备类型,次设备号说明具体指哪一个设备。设备号相当于身份证ID。Linux使用一个大小为255的数组管理设备,主设备号必须在1~254之间。对于常用设备,Linux有约定俗成的编 号,如硬盘的主设备号是3。

2、设备文件

Linux中有个俗语叫——一切皆是文件。外设也不例外:用户使用外设就像使用普通文件一样,设备文件存放在 /dev 目录下,它使用设备号区分外设。设备清单 /proc/devices 记录了当前已经注册了的设备,我们可以使用如下命令打开它:
% cat /proc/devices

Linux设备驱动--字符设备(一)

如图所示:该文件同时记录了字符设备和块设备,每一类都分为两列,第一列为主设备号,第二列为设备名称。一行就表示有个名为xxx的外设占用了某个主设备号,之后如果我们想自己注册设备,需要指定设备号时就不能选择已经被占用的(虽然现代 Linux 内核允许多个驱动共享主编号)。
进入/dev目录下可以用
% ls -l
命令打印所有设备的信息(也可以用 “ls -l |greb 设备名” 来打印某一个设备的信息)。

Linux设备驱动--字符设备(一)

若第一个字母为c(Character),则表示它是一个字符设备。b - 表示块设备。紧接着是设备读取权限,红色框内则表示主,次设备号。


3、一些重要的数据结构

大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 分别是 file_operations, file, 和 inode ,它们都在 <linux/fs.h> 下定义。

1) file_operations

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 *);
	unsigned int (*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 *);
	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 (*aio_fsync) (struct kiocb *, 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
};
文件操作结构体。当驱动注册到内核之后,上层应用可以通过调用相应的API(如open、read、write、close等),这些API会连接到 file_operations 指定的操作函数指针,进而使用驱动中定义的操作函数,达到控制硬件的目的。file_operations 是一个函数指针的集合(除了第一个 struct module *owner;)。

其中,修饰符__user修饰的东西表示它服务于用户空间。

struct module *owner
该成员不属于操作,它是一个指向拥有这个结构的模块的指针。这个成员用来在它的操作还在被使用时阻止模块被卸载。几乎所有实践中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
用来从设备中获取数据,对应API中的read操作,将读取的内容存放在__user修饰的指针指向的地址处。"signed size" 表示目标平台本地的整数类型。
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
发送数据给设备,对应API中的write操作,发送的数据来自__user修饰的指针指向的地址。
int (*open) (struct inode *, struct file *);
这常常是对设备文件进行的第一个操作,对应API中的open操作。如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知。
int (*release) (struct inode *, struct file *); 
在文件结构被释放时引用这个操作,对应API中的close操作。这里只是简单的说明其中某几个函数,其它未作说明的今后用到再解释。关于返回值,这些函数的返回值都是整形,在Linux驱动开发中,整形的返回值很多时候是用来表示操作结果,0 -- 表示成功;负数表示失败,这时候就会返回一个错误码(而不是一个任意的负数),错误码的定义见文件:<asm-generic/errno-base.h>  。值得注意的是,该文件中定义的错误码都是正整数,我们使用时需要在前面加上负号,比如 -EINVAL 表示参数错误。

2) file 
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;
	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;
} __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */
struct file,是设备驱动中第二个最重要的数据结构。注意这里的 file 与用户空间程序的 FILE 指针没有任何关系。FILE 定义在 C 库中, 从不出现在内核代码中;struct file 是一个内核结构, 从不出现在用户程序中。文件结构代表一个打开的文件。它不特定给设备驱动,系统中每个打开的文件有一个关联的 struct file 在内核空间。它由内核在 open 时创建, 并传递给在文件上操作的任何函数,直到最后的关闭。在文件的所有实例都关闭后,内核释放这个数据结构。在内核源码中, struct file 的指针常常称为 file 或者 filp("file pointer")。这里也是简单介绍一下其中的某几项:
const struct file_operations	*f_op;
表示和和文件关联的操作。
unsigned int 		f_flags;
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK,和 O_SYNC。驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作。
fmode_t			f_mode;
文件模式确定文件是可读的或者是可写的(或者都是),FMODE_READ /FMODE_WRITE
loff_t			f_pos;
当前读写位置。loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long )。 如果驱动需要知道文件中的当前位置, 可以读这个值,但是正常情况下不应该改变它。

3) inode
struct inode {
	umode_t			i_mode;
	unsigned short		i_opflags;
	kuid_t			i_uid;
	kgid_t			i_gid;
	unsigned int		i_flags;
        ……
	dev_t			i_rdev;

        ……
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
		char			*i_link;
	};
        ……
};
inode 结构由内核在内部用来表示文件,因此,它和代表打开文件描述符的文件结构是不同的。单个文件可能存在多个打开描述符的file结构,但是它们都只对应同一个inode 。inode 结构包含大量关于文件的信息(省略掉很多)。但是通常情况下,这个结构只有 2 个成员对于编写驱动代码有用,分别是:
dev_t			i_rdev;
对于代表设备文件的节点,这个成员包含实际的设备编号。
struct cdev		*i_cdev;
struct cdev 是内核的内部结构,代表字符设备,其它的还有管道、块、链接。

4、字符设备注册

在Linux2.6及之前的版本中,有大量的注册是使用的经典注册方法。

注册:

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

其中,major表示主设备号,必须是 /proc/devices 下没有用到的设备号(1~254),填0表示让系统帮你选择一个未被占用的设备号,此时系统通常会从后往前查询得到一个最大值; name表示设备名; fops则是驱动编写者自己创建的一个 file_operations 结构体类型的结构,它内部会链接到一系列操作函数。返回值为注册的设备号。

注销:

int unregister_chrdev(unsigned int major, const char *name);

2.6之后有新的方法注册,今后再研究。


二、测试


经典注册方法测试

修改 hello_module.c 如下:

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

#define MYNAME		"test"

int mymajor;  //保存主设备号

static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 这里本该放置打开硬件的代码
	printk(KERN_INFO "test_chrdev_open.\n");
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	// 这里本该放置关闭硬件的代码
	printk(KERN_INFO "test_chrdev_release.\n");
	return 0;
}

// 自定义一个file_operations结构体变量
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,		// 惯例
	.open		= test_chrdev_open,	// 应用open这个设备时实际调用
	.release	= test_chrdev_release,	// 应用close这个设备时实际调用
};

// 模块安装函数
static int __init hello_init(void)
{	
	printk(KERN_INFO "hello, world.\n");

	// 在module_init宏调用的函数中去注册字符设备驱动
	// 0 可以指定为系统中没有用到的主设备号值
	mymajor = register_chrdev(0, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		printk(KERN_ERR "register_chrdev fail!!!\n");
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev success.\nmajor id is %d.\n", mymajor);

	return 0;
}

// 模块下载函数
static void __exit hello_exit(void)
{
	printk(KERN_INFO "bye, world.\n");
	
	// 在module_exit宏调用的函数中去注销字符设备驱动
	unregister_chrdev(mymajor, MYNAME);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
增加了 file_operations 结构体类型的变量 test_fops,并链接了相应的操作函数(尽管函数目前只有打印功能),在module_init中注册驱动,在module_exit中注销驱动。Makefile 文件不需要修改。重新make , insmod ,使用

% cat /proc/devices 

打印设备列表:

Linux设备驱动--字符设备(一)

此时会发现,名为 “test” 的驱动已经被添加到设备列表里面了。使用 dmesg 打印日志:

Linux设备驱动--字符设备(一)

现在在系统设备列表中已经存在了主设备号为246的设备test,创建设备文件:

% mknod Name { b | c } Major Minor

% mknod /dev/test c 246 0

Linux设备驱动--字符设备(一)

现在,在 /dev 虚拟出了一个设备名为test的新设备。接下来的目的就是用应用程序去调用这个驱动。为此,添加如下应用app.c:

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

#define FILE	"/dev/test"	// mknod 创建的设备文件名

int main(void)
{
	int fd = -1;
	
	fd = open(FILE, O_RDWR);
	if (fd)
	{
		printf("open %s error.\n", FILE);
		return -1;
	}
	printf("open %s success.\n", FILE);
	
	// 读写文件暂未实现
	
	// 关闭文件
	close(fd);
	
	return 0;
}
该应用只是一个简单的打开关闭过程,如果一切正常的话,app调用open时实际就是使用设备 /dev/test 调用 test_chrdev_open 操作; 同理,app调用close时实际就是使用设备 /dev/test 调用 test_chrdev_release 操作 。使用gcc编译链接(为方便,也可以将该命令直接添加到Makefile中去)并运行

% gcc app.c -o app

% ./app

Linux设备驱动--字符设备(一)
应用调用打开、关闭操作成功。

2、读写回环测试

本节目标:

驱动程序添加读写操作,使得应用可以读写 /dev/test 这个设备。

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,需要用到相应的API。

写 -- 从应用层传入数据到内核,需要使用的函数为

unsigned long
copy_from_user(void *to, const void __user *from, unsigned long n)
{
   ……
}
入参 *to表示内核中存放数据的指针; *from表示来自用户层的数据指针; n表示传递的数据长度

返回值类型为 unsigned long ,表示本次传输剩余的数据长度,如果传入成功,则返回0。

读 -- 从内核导出数据到应用,需要使用的函数为

unsigned long
copy_to_user(void __user *to, const void *from, unsigned long n)
{
  ……
}

入参 *to表示用户层的数据指针; *from表示内核中存放数据的指针; n 和返回值意义同上。

驱动中添加读写函数,并将它们链接到 file_operations 结构体中,在hello_module.c中添加如下代码:

#include <asm/uaccess.h>
char kbuf[100];
……
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	int ret = -1;
	
	printk(KERN_INFO "test_chrdev_read\n");
	
	ret = copy_to_user(ubuf, kbuf, count);
	if (ret)
	{
		printk(KERN_ERR "copy_to_user fail!\n");
		return -EINVAL;
	}
	printk(KERN_INFO "copy_to_user success.\n");
	
	
	return 0;
}

static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
	size_t count, loff_t *ppos)
{
	int ret = -1;
	
	printk(KERN_INFO "test_chrdev_write\n");

	ret = copy_from_user(kbuf, ubuf, count);
	if (ret)
	{
		printk(KERN_ERR "copy_from_user fail!\n");
		return -EINVAL;
	}
	printk(KERN_INFO "copy_from_user success.\n");

	return 0;
}

static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,		
	
	.open		= test_chrdev_open,		
	.release	= test_chrdev_release,
	.write 		= test_chrdev_write,
	.read		= test_chrdev_read,
};
……
在应用app中添加如下代码:

char buf[100];
int main()
{
	……
	// 读写文件
	write(fd, "hello world", 14);
	read(fd, buf, 100);
	printf("%s.\n", buf);
	
	// 关闭文件
	close(fd);
}
重复测试1,可以得到如下输出

Linux设备驱动--字符设备(一)