摘要: 本文主要介绍了Linux内核空间与用户空间数据交流的几种方式,基于Linux4.1.15内核。第一次写这么长文的博客,写的不是很好,可能存在不少问题,有发现的还请多指教。以后我也将持续对本文完善,减少出错。
现在我想到的几种:
1.输入子系统,这个是单向的,只能内核->应用层。通常用于输入设备如按键、触摸屏将键值或者坐标上报给用户空间
2.文件操作集合,ioctl/read/write等函数,对应了字符设备等设备类型,这个是双向的,内核层和应用层可以互相发数据。通常用于各种需要对硬件设备进行读写的设备驱动程序
3.sys文件系统,也就是属性节点,同样也是双向的。通常用于读取或者修改驱动程序的配置,比如在一个由pwm控制的LED程序中设置一个属性节点,修改或者得到led灯的亮度值。
4.proc文件系统,这个我用的很少。比如可以通过cat /proc/kmsg查看内核打印消息,设置printk的输出等级等。
5.用户空间驱动,这个也是双向的。这个用的比较少。
6.直接读写物理内存。通常应用层不会直接操作物理内存。
7.fw_printenv/fw_setenv设置uboot环境变量的工具。
目前我用到的大概也许可能就这几种,周末有时间会对那几种方法都做个详细的分析。发现新的内核空间和用户空间进行数据交流的方法再补充。在我对本文进行补充与分析时,发现内核空间与用户空间进行通信的方法本质上只有前四种,常用的是前三种。 但是用户空间驱动、直接读写物理内存、fw_printenv/fw_setenv工具作为一种比较有趣的做法,在接下来的分析中还是单独作为一个大节了。
一、输入子系统
在 Linux 中,输入子系统是由输入子系统设备驱动层、输入子系统核心层(Input Core)和输入子系统事件处理层(Event Handler)组成。
设备驱动层:提供对硬件各寄存器的读写访问和将底层硬件对用户输入访问的响应转换为标准的输入事件,再通过核心层提交给事件处理层;
核心层:对提供、设备驱动层的编程接口,对上又提供了事件处理层的编程接口;
事件处理层:为用户空间的应用程序提供统一访问设备的接口和驱动层提交来的事件处理。所以这使得我们输入设备的驱动部分不在用关心对设备文件的操作,而是要关心对各硬件寄存器的操作和提交的输入事件。
Linux输入子系统的出现统一了形态各异的输入设备如:鼠标、按键、触摸屏、usb等设备。抽取了输入驱动的通用部分,简化了驱动并提供了一致性。Linux输入子系统会在/dev/input目录下创建提供给用户空间的设备编程接口,在用户空间中通过open函数打开/dev/input/下的event接口就能接收来自内核空间的输入事件数据,但用户空间并不能通过该接口往内核空间发数据。
二、系统调用(文件操作合集)
系统调用是内核和应用程序的接口,设备驱动程序是内核和硬件设备之间的接口。Linux通过设备驱动程序把硬件抽象成设备文件,设备文件分为三大类:字符设备、块设备和网络设备。Linux为每类设备都定义了统一的文件操作接口。例如:
1.字符设备的基本文件操作方法:def_chr_fops
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
2.块设备的基本文件操作方法:def_blk_fops
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read_iter = blkdev_read_iter,
.write_iter = blkdev_write_iter,
.mmap = generic_file_mmap,
.fsync = blkdev_fsync,
.unlocked_ioctl = block_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_blkdev_ioctl,
#endif
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
};
3.网络设备的基本文件操作方法:socket_file_ops
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
上述结构体为每种设备类型提供了操作设备的基本接口,在驱动层要实现的接口也是可选的,比如在字符设备驱动中也可以实现open/read/write/ioctl等接口。Linux通过file_operations结构体屏蔽了设备类型的差异,这也就意味着在应用层能够通过open/read/write/ioctl等文件操作的系统调用函数,直接操作字符设备、块设备、网络设备。
需要注意的是,首先在驱动程序要实现了该接口(如read函数),在应用层中才能通系统调用函数(read函数)操作这个驱动程序对应的设备文件,否则将产生严重的错误。
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 (*mremap)(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
};
三、sys文件系统
在了解sys文件系统之前了解一下Linux 的设备驱动模型,Linux的设备驱动模型主要包括三大组件,分别是总线、设备和驱动。这三种结构之间的关系非常复杂,为了使驱动程序对用户是可见的,Linux内核提供了sysfs文件系统来映射设备驱动模型各组件的关系。
sys文件系统是一个只存在于内存中的文件系统,它由内核对象(kobject)、属性(kobj_type)及它们的相互关系的一种表现机制。用户可以从sysfs文件系统中读出内核的数据,也可以将用户空间的数据写入内核中。sys文件系统挂载在根目录下/sys,它和其他文件系统一样由文件、目录、链接组成。
四、proc
proc挂载在根目录下/proc。在操作系统运行时,进程及内核信息(如cpu、硬盘分区、内存信息等)存放在这里,用户空间同样能够对proc文件进行读写。和sys文件系统不同的是,proc并不是真正的文件系统,/proc目录是伪文件系统proc的挂载目录,proc伪文件系统同样也存在于内存之中。
proc虚拟文件系统的几个用法:
1.查看内存信息
cat /proc/meminfo
2.查看内核打印信息
cat /proc/kmsg
3.修改printk()的打印级别,如:
echo 3 3 3 3 >> /proc/sys/kernel/printk
还有其他很多的用法,通过proc虚拟文件系统可以方便的显示内核信息,如果节点可写,还可以做配置的修改。Linux系统的许多命令本身都是通过分析/proc下的文件来完成的,如ps、top、uptime、free等命令。在基于Linux-4.1.15内核的设备驱动程序编程中,在linux-4.1.15/include/linux/proc_fs.h文件中定义了用于创建、删除/proc下的目录或者节点的函数,如:
struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent) ;//创建/proc下的目录文件
#define proc_create(name, mode, parent, proc_fops) ({NULL;}) //创建/proc目录下的节点
void proc_remove(struct proc_dir_entry *);//移除/proc下的目录文件
然而在编写设备驱动程序时,我们一般不需要在proc虚拟文件系统中创建节点,因为有sys文件系统就已经足够了。在大多数的情况下,设备驱动程序还是通过sys文件系统使内核驱动程序信息对应用层可见。
五、用户空间驱动
主要通过uio_info结构体来实现,这个我也不是太了解,后面找到资料再补充。
六、直接读写物理内存
在Linux中可以通过/dev/mem设备节点直接读写物理内存,而/dev/mem是一个字符设备。所以仍然是通过文件操作合集(file_operations结构体)来实现对物理内存的直接读写。准确的说这应该属于之前讲的系统调用方法实现用户空间和内核空间的通信。之所以我单独拿出来说,其实也是想再强调一下file_operations文件操作合集结构体的厉害,都能直接操作物理内存了,还有啥是它不能干的?后面会详细补充关于直接读写物理内存的一些运用。
七、使用fw_printenv/fw_setenv工具设置uboo环境变量t实现内核空间和用户空间的数据交流
在使用fw_printenv/fw_setenv工具之前需要做一下额外的工作,请参考以下链接博文。
举例子说明具体步骤:
1.使用fw_setenv命令设置ONW_DEF_ENV 1后重启
fw_setenv ONW_DEF_ENV 1
2.在Linux内核代码合适的地方实现类似如下的函数
static int __init user_env_function(char *env)
{
if(s!=NULL)
{
sprintf(user_env,"%s",s);
}
printk("user_env_function:%c %c\n",env[0],env[1]);
return 0;
}
__setup("user_env=", user_env_function);
3.创建属性节点,将user_env赋值给sys文件系统下的属性节点。应用层就能通过读取属性来获得user_env的值来了。
最后总结一下,目前和发现的内核空间与用户空间进行数据交流的方法只有四种:
1.输入子系统
2.文件操作合集(file_operations),也就是系统调用
3.sysfs文件系统
4.proc虚拟文件系统