1. 引子

驱动是干什么的?在驱动的相关书籍上,网络上你能看到很多专业的界说。咱们暂时不关心这些专业的说法,仅从功能的角度来说,驱动程序使得应用程序能够拜访硬件

那应用是怎么拜访硬件的?linux 中一切皆文件,拜访硬件便是对文件的读写操作。比方 led 灯对应的文件是 /etc/led, 读写这个文件就能操作 led 灯。

接下来的问题便是,linux 中怎么读写文件?

2. linux 中文件的读写

linux中文件读写相关的首要 api:

//翻开文件
int open(const char *pathname, int flags, mode_t mode);
//从文件中读数据
ssize_t read(int fd, void *buf, size_t count);
//向文件中写数据
ssize_t write(int fd, const void *buf, size_t count);
//专用于设备输入输出操作
int ioctl(int fd, unsigned long request, ...);
//封闭文件的读写,收回资源
int close(int fd);

下面我首要看一下 open 函数:

//该函数用于翻开文件
int open(const char *pathname, int flags, mode_t mode);

当翻开一个文件的时分,会回来一个 int 值,一般称这个回来值为句柄或者 handle,在内核中,句柄是一个数组的索引(index),数组的成员是 struct 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;   //重视1
        /*
         * 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;  //重视2
        fmode_t                 f_mode;	  //重视3
        struct mutex            f_pos_lock;
        loff_t                  f_pos;	  //重视4
        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)));

struct file 的结构有点复杂,入门阶段首要重视代码中标注的四个重视点。

在内核中,有一个 struct file 的数组,当调用 open 函数翻开一个文件的时分,内核就会构建一个 struct file,并增加到这个数组中,回来 struct file 的 index 给用户态程序,这个值便是 open 函数的回来值。

根据文件的命名,简单猜出:使用 open 翻开文件时,传入的 flags、mode 等参数会被记载在内核中,具体如下图所示:

Linux驱动入门-驱动

struct file 有一个成员为 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 *);  //重视点1
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //重视点2
	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 *);
	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);//重视点3
	int (*mmap) (struct file *, struct vm_area_struct *);//重视点4
	int (*open) (struct inode *, struct file *);//重视点5
	int (*flush) (struct file *, fl_owner_t id);//重视点6
	int (*release) (struct inode *, struct file *);//重视点7
	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;

内部首要是一些函数指针,咱们首要重视常用的几个函数:

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*mmap) (struct file *, struct vm_area_struct *);
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 *);

这些函数都是由相应硬件驱动供给的。

至此,文件读写的大致流程就出来了:

  • app 调用 open read 等系统调用函数
  • 内核构建相应的 struct file,并增加进数组,回来 index 给 app
  • 调用驱动程序 file_operations 指针供给的 open read 等函数,完结实际的硬件操作

3. hello 驱动的编写

驱动便是一个模块,在模块的基础上增加驱动结构和硬件操作的部分就能够完结驱动程序的编写了。下面咱们写一个 hello 驱动,这个驱动仅仅简单的在用户态和内核态之间拷贝数据,没有实际的硬件操作,仅用于流程的展示。编写驱动的步骤如下:

  1. 确认主设备号,也能够让内核分配 (设备号便是硬件的一个编号)
  2. 界说自己的 file_operations 结构体
  3. 完成对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
  4. 界说 init 函数,在 init 函数中调用 register_chrdev 注册函数
  5. 界说 exit 函数,在 exit 函数中调用 unregister_chrdev 卸载函数
  6. 其他完善:供给设备信息,主动创立设备节点:class_create, device_create

在内核下的 common/drivers/char/ 目录中增加 hello_driver.c:

//文件名 hello_driver.c
#include <linux/module.h>
#include <linux/fs.h>
#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>
/* 1. 确认主设备号                                                                 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
/* 3. 完成对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t hello_drv_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 hello_drv_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__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}
/* 2. 界说自己的file_operations结构体                                              */
static struct file_operations hello_drv = {
	.owner	 = THIS_MODULE,
	.open    = hello_drv_open,
	.read    = hello_drv_read,
	.write   = hello_drv_write,
	.release = hello_drv_close,
};
/* 4. 把file_operations结构体告知内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个进口函数:安装驱动程序时,就会去调用这个进口函数 */
static int __init hello_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */
	//供给设备信息,主动创立设备节点。
	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	//到这里咱们就能够经过 /dev/hello 文件来拜访咱们的驱动程序了。
	return 0;
}
/* 6. 有进口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0));
	class_destroy(hello_class);
	unregister_chrdev(major, "hello");
}
/* 7. 其他完善:供给设备信息,主动创立设备节点                                     */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

接下来咱们修正 common/drivers/char/Kconfig 文件,使得咱们的 hello 驱动能出现在内核的编译选择中。

在 common/drivers/char 中的 Kconfig 文件中增加:

config HELLO_DRIVER
	bool "hello driver support"
	default y

然后在 common/drivers/char 下的 Makefile 文件中增加:

obj-$(CONFIG_HELLO_DRIVER)       += hello_driver.o

最后配置内核:

BUILD_CONFIG=common/build.config.gki.x86_64 build/kernel/config.sh

在配置菜单中选中咱们的 HELLO_DRIVER,具体方法和模块一节中介绍的共同。

发动模拟器后,咱们就能够看到咱们的设备文件 hello 了:

Linux驱动入门-驱动

接着咱们再写一个测验程序:

hello_driver_test.c:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
 * ./hello_drv_test -w abc
 * ./hello_drv_test -r
 */
int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	/* 1. 判别参数 */
	if (argc < 2) 
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}
	/* 2. 翻开文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}
	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		write(fd, argv[2], len);
	}
	else
	{
		len = read(fd, buf, 1024);
		buf[1023] = '\0';
		printf("APP read : %s\n", buf);
	}
	close(fd);
	return 0;
}

首要咱们需要将穿插编译器加入 PATH 环境变量:

PATH=/home/zzh0838/android-kernel/android-kernel/prebuilts/ndk-r23/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH

接着编译咱们的测验程序:

x86_64-linux-android31-clang -o hello_driver_test hello_driver_test.c

上传咱们的测验程序:

adb push ./hello_driver_test /data/local/tmp

执行咱们的程序:

adb shell
cd /data/local/tmp

Linux驱动入门-驱动

4.总结

  • App 读写文件时,翻开的文件在内核中是怎么表明的?
  • 驱动编写的流程?

参考资料

  • 《嵌入式Linux应用开发彻底手册 第二版》 韦东山
  • 《Linux程序设计》 Neil Matthew Richard Stones