本文首发于我的公众号码农之屋(id: Spider1818),专注于干货分享,包含但不限于Java编程、网络技术、Linux内核及实操、容器技术等。欢迎大家关注,二维码文末可以扫。
导读:平时我们更多会聊到内核态驱动,对于用户态驱动了解甚少,今天就让我们一起走入用户态驱动的世界,彻底了解清楚用户态驱动究竟是什么?它跟内核态驱动之间又有什么区别呢?让我们拭目以待。
UIO,即Userspace I/O内核驱动,它在Linux kernel的世界比较小众,主要服务于一些定制设备。UIO负责将中断和设备内存暴露给用户空间,然后再由UIO用户态驱动实现具体的业务。
硬件设备可以按照不同维度进行分类,比如从功能维度而言,可分为网络设备、块设备等。现实中还存在一些设备无法划分到里头,比如I/O卡或定制的FPGA,而这些设备驱动主要是以字符驱动方式进行开发,必须编写一个完整的内核驱动,而且还得一直维护这些代码。如果把这种设备的驱动放入Linux内核中,不但增大了内核的负担,也没人愿意花费大量精力去维护。
假如采取UIO驱动方式,则将简单很多。我们不仅可以利用所有用户空间的应用程序开发工具和库,而且当内核发生变化时,只需更改UIO框架与内核程序交互的接口即可,不需要更改UIO框架的驱动。除此之外,UIO可以使用C++、Java等高级语言进行开发(内核驱动仅能使用C语言和汇编代码),而且我们还能够参考内核驱动已实现的代码,极大提高我们的开发效率。最后还有一个优势就是,调测用户态程序比内核态程序简单得多,即便用户空间驱动程序挂了,也不会影响系统的正常运行。
设备驱动主要完成2件事:处理设备产生的硬中断和存取设备内存。硬中断处理必须在内核空间进行,而设备内存的存取可以在用户空间进行。
UIO框架分为2部分,内核空间驱动和用户空间驱动,内核部分主要实现硬件寄存器的内存映射及读写操作,而用户空间部分负责将UIO设备的uio_mem映射到本地,实现用户空间程序能够访问硬件设备寄存器。UIO驱动模型请参考图1所示。接下来将详细介绍UIO内核部分和用户空间部分的实现流程。
图1 Linux UIO驱动模型图
1、UIO内核部分
UIO内核驱动做的事情相对来说比较少,主要的任务是分配和记录设备所需的资源(使能PCI设备、申请资源、读取并记录配置信息)、注册UIO设备(uio_register_device())以及实现硬中断处理函数。
接下来我们将从代码角度,重点介绍UIO内核驱动实现涉及的重要数据结构和调用的函数。
1) UIO内核驱动涉及的数据结构
struct uio_portio {
struct kobjectkobj;
structuio_port *port;
};
/** * struct uio_port - description of a UIO port region* @name: name of the port region for identification * @start: start of portregion * @size: size of port region * @porttype: type of port (see UIO_PORT_*below) * @portio: for use by the UIO core only. */
struct uio_port {
const char *name;
unsigned long start;
unsigned long size;
int porttype;
structuio_portio *portio;
};
/* defines for uio_port->porttype */
#define UIO_PORT_NONE 0
#define UIO_PORT_X86 1
#define UIO_PORT_GPIO 2
#define UIO_PORT_OTHER 3
/** * struct uio_mem - description of a UIO memoryregion * @name: name of the memory region for identification * @addr: addressof the device's memory * @size: size of IO * @memtype: type of memory addrpoints to * @internal_addr: ioremap-ped version of addr, for driver internaluse * @map: for use by the UIO core only. */
struct uio_mem {
const char *name;
unsigned long addr;
unsigned long size;
int memtype;
void __iomem *internal_addr;
structuio_map *map;
};
struct uio_map {
struct kobjectkobj;
struct uio_mem*mem;
};
static const struct vm_operations_struct uio_vm_ops = {
.open =uio_vma_open,
.close =uio_vma_close,
.fault =uio_vma_fault,
};
static struct device_attribute uio_class_attributes[] ={
__ATTR(name,S_IRUGO, show_name, NULL),
__ATTR(version, S_IRUGO, show_version, NULL),
__ATTR(event,S_IRUGO, show_event, NULL),
{}
};
/* UIO class infrastructure */
static struct class uio_class = {
.name = "uio",///sys/class/uio
.dev_attrs =uio_class_attributes,
};
static const struct file_operations uio_fops = {
.owner = THIS_MODULE,
.open = uio_open,
.release = uio_release,
.read = uio_read,
.write = uio_write,
.mmap = uio_mmap,
.poll = uio_poll,
.fasync = uio_fasync,
.llseek = noop_llseek,
};
/* Protect idr accesses */
static DEFINE_MUTEX(minor_lock);
static DEFINE_IDR(uio_idr);
struct uio_device {
structmodule *owner;
structdevice *dev;
int minor;
atomic_t event;
structfasync_struct *async_queue;
wait_queue_head_t wait;
int vma_count;
structuio_info *info;
structkobject *map_dir;
structkobject *portio_dir;
};
/* * struct uio_info - UIO device capabilities *@uio_dev: the UIO device this info belongs to * @name: device name * @version:device driver version * @mem: list of mappable memory regions, size==0 for endof list * @port: list of port regions, size==0 for end of list * @irq:interrupt number or UIO_IRQ_CUSTOM * @irq_flags: flags for request_irq() *@priv: optional private data * @handler: the device's irq handler * @mmap: mmapoperation for this uio device * @open: open operation for this uio device *@release: release operation for this uio device * @irqcontrol: disable/enableirqs when 0/1 is written to /dev/uioX */
struct uio_info {
structuio_device *uio_dev;
const char *name;
const char *version;
structuio_mem mem[MAX_UIO_MAPS];
structuio_port port[MAX_UIO_PORT_REGIONS];
long irq;
unsigned long irq_flags;
void *priv;
irqreturn_t(*handler)(int irq, struct uio_info *dev_info);
int (*mmap)(structuio_info *info, struct vm_area_struct *vma);
int (*open)(structuio_info *info, struct inode *inode);
int(*release)(struct uio_info *info, struct inode *inode);
int(*irqcontrol)(struct uio_info *info, s32 irq_on);
};
2、UIO用户空间部分
用户空间驱动主要完成2个关键任务,响应硬件中断和从存取设备内存。下面将使用理论 + 代码对用户空间驱动的能力进行介绍。
响应硬中断通常有2种处理方式,1)调用read(),阻塞/dev/uioX,当设备产生中断时,read()操作立即返回;2)调用poll(),使用select()等待中断发生(select()有一个超时参数可用来实现有限时间内等待中断)。下面用一段代码说明如何完成硬中断响应处理(阻塞/dev/uio0,调用read()处理硬中断信号)。
int32_t irq_count;
int fd = open("/dev/uio0", O_RDWR);
/* Map the register regions to proccess's virtual memspace */
void * access = mmap(NULL, 4096,PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
while (read(fd, &irq_count, 4) == 4)
{
printf("Interrupt number %dn", irq_count);
}
对于如何存取设备内存呢?我们可以通过读写/sys/class/uioX下的各个文件(注册的UIO设备在该目录下),完成对设备内存的读写。比如UIO设备为uio0,那么映射的设备内存将在/sys/class/uio/uio0/maps下,对该文件的读写就是对设备内存的读写。下面用一段代码说明如何存取设备内存(将设备信息mmap到用户空间,用户空间程序便可直接操作设备内存空间)。
char uio_addr_buf[16]={0};
char uio_size_buf[16]={0};
int uio_fd,addr_fd,size_fd;
int uio_size;
void *uio_addr, *access_address;
int n=0;
uio_fd = open("/dev/uio0",O_RDWR);
addr_fd = open("/sys/class/uio/uio0/maps/map0/addr",O_RDONLY);
size_fd = open("/sys/class/uio/uio0/maps/map0/size",O_RDONLY);
if (addr_fd < 0 || size_fd < 0 || uio_fd < 0)
{
fprintf(stderr,"mmap:%s\n",strerror(errno));
exit(-1);
}
n=read(addr_fd,uio_addr_buf,sizeof(uio_addr_buf));
if (n<0)
{
fprintf(stderr,"%s\n", strerror(errno));
exit(-1);
}
n=read(size_fd,uio_size_buf,sizeof(uio_size_buf));
if (n<0)
{
fprintf(stderr,"%s\n", strerror(errno));
exit(-1);
}
uio_addr = (void*)strtoul(uio_addr_buf,NULL,0);
uio_size = (int)strtol(uio_size_buf,NULL,0);
access_address = mmap(NULL,uio_size,PROT_READ |PROT_WRITE,
MAP_SHARED,uio_fd,0);
if(access_address == (void*)-1)
{
fprintf(stderr,"mmap:%s\n",strerror(errno));
exit(-1);
}
printf("The device address %p (lenth %d)\n"
"canbe accessed over\n"
"logical address %p\n",uio_addr,uio_size,access_address);
最后一章节,对UIO框架主要涉及函数定义及功能描述进行汇总,具体的内容请参考如下表信息。
函数定义 |
功能描述 |
static int __init uio_init(void) |
申请字符设备号和设备,并注册到系统中,注册uio_class到系统中 |
staticvoid__exituio_exit(void) |
注销uio_class,注销字符设备编号和删除设备 |
static void release_uio_class(void) |
注销uio_class,注销字符设备编号和删除设备 |
static int init_uio_class(void) |
申请字符设备号和设备,并注册到系统中,注册uio_class到系统中 |
static int uio_major_init(void) |
申请字符设备编号和设备,并初始化 |
static void uio_major_cleanup(void) |
注销字符设备编号,删除设备 |
static int uio_open(struct inode *inode, struct file *filep) |
获得和次设备号关联的uio_device指针,创建一个辅助变量listener, 并调用info指向的uio_info结构中的open方法 |
static int uio_release(struct inode *inode, struct file *filep) |
调用uio_device的字段info指向的uio_info中的release方法,释放辅助结构体listener |
static int uio_fasync(int fd, struct file *filep, int on) |
管理uio_device的async_queue |
static unsigned int uio_poll(struct file *filep, poll_table *wait) |
使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待,并返回一个是否可以立即无阻塞执行的位掩码 |
static ssize_t uio_read(struct file *filep, char __user *buf, size_t count, loff_t *ppos) |
复制uio设备中断事件计数器的值到用户空间 |
int __uio_register_device(struct module *owner, struct device *parent, struct uio_info *info) |
调用uio_info中注册的handler中断处理函数,对设备的中断事件计数器增一并通知各读进程,有数据可读 |
void uio_event_notify(struct uio_info *info) |
“触发”一个中断事件,对设备的中断事件计数器增一,并通知各读进程,有数据可读 |
static ssize_t uio_write(struct file *filep, const char __user *buf,size_t count, loff_t *ppos) |
读取用户空间的值,并调用uio_device注册的irqcontrol函数 |
static int uio_mmap(struct file *filep, struct vm_area_struct *vma) |
对uio设备进行mmap处理 |
我的公众号「码农之屋」(id: Spider1818) ,分享的内容包括但不限于 Linux、网络、云计算虚拟化、容器Docker、OpenStack、Kubernetes、SDN、OVS、DPDK、Go、Python、C/C++编程技术等内容,欢迎大家关注。