简单的字符设备驱动笔记
字符设备驱动详解
字符设备驱动的抽象
字符设备是以字符流为处理对象的设备。在Linux中使用struct cdev
数据结构来对其进行抽象和描述。
字符设备的描述struct cdev
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
kobj
: 用于Linux设备驱动模型。owner
: 字符设备驱动程序所在的内核模块对象指针。ops
: 字符设备驱动程序中最关键的一个操作函数,在和应用程序交互过程中起到桥梁枢纽的作用。list
: 用来将字符设备串成一个链表。dev
: 字符设备的设备号,由主设备号和次设备号组成。count
: 同属一个主设备号的次设备号的个数。操作
struct cdev
的API
- 产生cdev
可由两种方式来产生struct cdev
,一种是使用全局静态变量,另一种是使用cdev_alloc
函数。
static struct cdev myCdev;
或
struct cdev myCdev = cdev_alloc();
- 初始化cdev
cdev_init ()
函数,初始化cdev数据结构,并建立该设备和ops(file_operations)的连接关系。
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
- 注册cdev
cdev_add()
函数,把一个字符设备添加到系统中,通常在驱动程序的probe函数里面调用该接口来注册一个字符设备。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
- p 表示一个设备的cdev数据结构。
- dev 表示设备的设备号。
- count 表示该设备有多少个次设备号。
- 删除cdev
cdev_del()
函数,从系统中删除一个cdev。
void cdev_del(struct cdev *p);
设备号的管理 Linux系统中的设备号由主设备号和次设备号组成。
主设备号和次设备号可通过以下宏定义,高12位为主设备号,低20位为次设备号。
#define MINORBITS20
#define MINORMASK((1U << MINORBITS) - 1)#define MAJOR(dev)((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma, mi)(((ma) << MINORBITS) | (mi))
Linux内核提供两个接口函数完成设备号的申请。
- 注册指定的主设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name);
- 申请分配一个主设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
- 释放主设备号
void unregister_chrdev_region(dev_t from, unsigned count);
设备节点 设备节点,也成设备文件,是连接内核空间驱动程序和用户空间应用程序的桥梁,应用程序要使用驱动提供的服务或对设备进行操作,则要通过访问设备文件来实现。按照Linux的习惯,系统中所有的设备节点都存放在/dev/目录下。/dev/目录是一个动态生成的、使用devtmpfs虚拟文件系统挂载的,基于RAM的虚拟文件系统。
设备节点的生成有两种方式:一种是使用
mknod
命令手工生成,另一种是使用udev机制动态生成(在嵌入式系统中为mdev)。手工生成设备节点可以使用
mknod
命令mknod filename type major minor
udev是一个工作在用户空间的工具,它能够根据系统中硬件设备的状态动态低更新设备节点,包括设备节点的创建、删除等。这个机制需要联合sysfs和tmpfs来实现,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。
字符设备操作方法集 字符设备操作方法集file_operation就是抽象和定义了一系列待实现的函数指针,如常用的open、close、write、read等接口。这个方法集通过
cdev_init()
函数和设备建立连接关系。如在用户空间调用open打开设备节点,通过系统调用进入内核空间,在内核空间的虚拟文件系统层(VFS)经过复杂的转换,最终就会调用到设备驱动的file operation方法集中的open接口。字符设备驱动程序的核心开发工作是实现file_operation方法集中符合设备需求的接口。#include
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);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
使用misc机制来创建设备 misc device称为杂项设备,Linux内核吧一些不符合预先确定的字符设备划分为杂项设备,这类设备的主设备号是10。
Linux内核使用
struct miscdevice
数据结构描述这类设备。struct miscdevice{
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
内核提供了
misc_register()
和misc_deregister()
来对杂项设备进行注册和卸载。misc device会自动创建设备节点,不需要使用mknod
命令手工创建设备节点,因此使用misc机制来创建字符设备驱动比较方便、简捷。int misc_register(struct miscdevice *misc);
void misc_deregister(struct miscdevice *misc);
阻塞I/O和非阻塞I/O I/O操作可以分成非阻塞I/O类型和阻塞I/O类型。
非阻塞:进程发起I/O系统调用后,如果设备驱动的缓冲区没有数据,那么进程返回一个错误而不会被阻塞。如果驱动缓冲区中有数据,那么设备驱动吧数据直接返回给用户进程。
阻塞:进程发起I/O系统调用后,如果设备的缓冲区没有数据,那么需要到硬件I/O中重新获取新数据,进程会被阻塞,也就是睡眠等待。直到数据准备好,进程才会被唤醒,并重新把数据返回给用户空间。
将驱动改成非阻塞模式
让设备支持非阻塞模式,实际上只需要对文件打开标志有O_NONBLOCK时,进行相应的处。需要在在read/write接口中对文件指针的f_flags进行判断,f_flags中是否包含
O_NONBLOCK
这个标志,在缓冲区为空和满的情况下,直接返回EAGAIN
错误码。ssize_t read (struct file * file, char __user *, size_t, loff_t *)
{
// 假设isEmpty为TRUE代表缓冲区为空
……
if (isEmpty)
{
if (file->f_flags & O_NONBLOCK)
{
return -EAGAIN;
}
}
……
}ssize_t write (struct file * file, const char __user *, size_t, loff_t *)
{
// 假设isFull为TRUE代表缓冲区满了
……
if (isFull)
{
if (file->f_flags & O_NONBLOCK)
{
return -EAGAIN;
}
}
……
}
将驱动改成阻塞模式
在Linux内核中,采用一个称为等待队列的机制来实现进程阻塞操作。
- 等待队列头
等待队列定义了一个被称为等待队列头(wait_queue_head_t
)的数据结构,定义在
中。
struct wait_queue_head {
spinlock_tlock;
struct list_headhead;
};
typedef struct wait_queue_head wait_queue_head_t;
Linux提供静态和动态两种方式来初始化一个等待队列头。
DECLARE_WAIT_QUEUE_HEAD(name)wait_queue_head name;
init_waitqueue_head(&name);
- 等待队列元素
wait_queue_t
struct __wait_queue {
unsigned intflags;
void*private;
wait_queue_func_tfunc;
struct list_headtask_list;
};
typedef struct __wait_queue wait_queue_t;
- 睡眠等待
Linux内核提供了wait_event
系列宏,来让进程睡眠时也检查进程的唤醒条件。
wait_event(wq, condition)
wait_event_interruptible(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible_timeout(wq, condition, timeout)
wq表示等待队列头,condition为布尔表达式,在condition变为真之前,进程会保持睡眠状态,timeout表示当timeout时间达到后,进程会被唤醒,因此只会等待限定的时间。当给定的时间到了后,
wait_event_timeout()
和wait_event_interruptible_timeout()
无论condition是否为真,都会被唤醒返回0。- 唤醒
wake_up(x)
wake_up_interruptible(x)
wake_up()
会唤醒等待队列中的所有进程。wake_up()
要和wait_event()
或者wait_event_timeout()
配对使用。而wake_up_interruptible()
要和wait_event_interruptible()
或者wait_event_interruptible_timeout()
配对使用。让设备支持阻塞模式,实际上实现以下操作。在缓冲区为空的时候,调用wait_event系列宏等待缓冲区有数据,在写入数据时调用wake_up系列宏唤醒进程;在缓冲区为满的时候,调用wait_event系列宏等待缓冲区可写入,在读取数据时调用wake_up系列宏唤醒进程。
wait_queue_head_t read_queue;
wait_queue_head_t write_queue;
static int __init xxx_init(void)
{
……
init_waitqueue_head(&read_queue);
init_waitqueue_head(&write_queue);
……
}ssize_t read (struct file * file, char __user *, size_t, loff_t *)
{
//假设isEmpty为TRUE代表缓冲区为空,isFull为TRUE代表缓冲区满了
……
if (isEmpty)
{
if (file->f_flags & O_NONBLOCK)
{
return -EAGAIN;
}// 等待缓冲区不为空
ret = wait_event_interruptible(&read_queue, !isEmpty);
if (ret)
{
return ret;
}
}// 读取数据
……
isFull = FALSE;
// 通知可写
if (!isFull)
{
wake_up_interruptible(&write_queue);
}
……
}ssize_t write (struct file * file, const char __user *, size_t, loff_t *)
{
// 假设isEmpty为TRUE代表缓冲区为空,isFull为TRUE代表缓冲区满了
……
if (isFull)
{
if (file->f_flags & O_NONBLOCK)
{
return -EAGAIN;
}// 等待缓冲区不为满
ret = wait_event_interruptible(&write_queue, !isFull);
if (ret)
{
return ret;
}
}// 写入数据
……
isEmpty = FALSE;
// 通知可读
if (!isEmpty)
{
wake_up_interruptible(&read_queue);
}
……
}
I/O多路复用 Linux的I/O多路复用
Linux内核提供
poll
、select
、epoll
这3种I/O多路复用的机制。I/O多路复用即一个进程可以同时捡屎多个打开的文件描述符,一旦某个文件描述符就绪,就立即通知程序进行相应的读写操作。poll
和select
方法在Linux用户空间的API接口函数定义如下:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
poll()
函数的第一个参数fds是要监听的文件描述符集合,类型为指向struct pollfd
的指针。struct pollfd {
intfd;
/* file descriptor */
short events;
/* requested events */
short revents;
/* returned events */
};
fd
表示要监听的文件描述符,event
表示监听的事件,revents
表示返回的事件。常用的监听事件有如下类型:
POLLIN:数据可以立即被读取。
POLLRDNORM:等同于POLLIN,表示数据,可以立即被读取。
POLLERR:设备发生了错误。
POLLOUT:设备可以立即写入数据。
poll()
的第二个参数nfds
是要监听的文件描述符的个数;第三个参数timeout
是单位为ms的超时,负数表示一直监听,知道被监听的文件描述符集合中有设备发生了事件。Linux内核中的file_operations方法集提供了poll方法的实现。
unsigned int (*poll) (struct file *, struct poll_table_struct *);
当用户程序打开设备文件后执行
poll
或者select
系统调用时,驱动程序的poll
方法就会被调用。设备驱动程序的poll
方法会执行如下步骤。- 在一个或者多个等待队列中调用
poll_wait()
函数。poll_wait()
函数会把当前进程添加到指定的等待列表(poll_table)中,当请求数据准备好之后,会唤醒这些睡眠的进程。 - 返回监听事件,也就是POLLIN或者POLLOUT等掩码。
poll
方法的作用就是让应用程序同时等待多个数据流。
poll_wait()
来对wait queue进行监听,并返回应用事件的掩码。unsigned int poll (struct file *file, poll_table *wait)
{
// 假设isEmpty为TRUE代表缓冲区为空,isFull为TRUE代表缓冲区满了
int mask = 0;
poll_wait(file, &read_queue, wait);
poll_wait(file, &write_queue, wait);
if (!isEmpty)
{
mask |= POLLIN | POLLRDNORM;
}if (!isFull)
{
mask |= POLLOUT | POLLWRNORM;
}return mask;
}
异步通知 异步通知,当请求的设备资源可以换取时,有驱动程序主动通知应用程序,应用程序调用read()或write()函数来发起I/O操作。异步通知不会造成阻塞,只有设备驱动满足条件之后才通过信号机制通知应用程序去发起I/O操作。
异步通知使用系统调用的
signal
函数和sigaction
函数。signal
函数让一个信号和一个函数对应,每当接口道这个信号时会调用相应的函数来处理。Linux内核中file_operations中实现异步通知的接口为
int (*fasync) (int, struct file *, int);
而
fasync
接口实现的过程中,必不可少的是struct fasync_struct
结构,在fasync
接口中需要提供一个struct fasync_struct
指针给fasync_helper()
接口使用。struct fasync_struct {
spinlock_tfa_lock;
intmagic;
intfa_fd;
struct fasync_struct*fa_next;
/* singly linked list */
struct file*fa_file;
struct rcu_headfa_rcu;
};
让设备支持异步通知,只要在实现file_operations中的fasync接口,在其中调用
fasync_helper()
初始化struct fasync_struct
结构,并在read/write接口中,在可读/写的情况下,调用kill_fasync()
发送信号给应用程序,通知应用程序可以进行读/写。wait_queue_head_t read_queue;
wait_queue_head_t write_queue;
struct fasync_struct *fasync;
static int __init xxx_init(void)
{
……
init_waitqueue_head(&read_queue);
init_waitqueue_head(&write_queue);
……
}int fasync (int fd, struct file *file, int on)
{
return fasync_helper(fd, file, on, &fasync);
}ssize_t read (struct file * file, char __user *, size_t, loff_t *)
{
//假设isEmpty为TRUE代表缓冲区为空,isFull为TRUE代表缓冲区满了
……
if (isEmpty)
{
if (file->f_flags & O_NONBLOCK)
{
return -EAGAIN;
}// 等待缓冲区不为空
ret = wait_event_interruptible(&read_queue, !isEmpty);
if (ret)
{
return ret;
}
}// 读取数据
……
isFull = FALSE;
// 通知可写
if (!isFull)
{
wake_up_interruptible(&write_queue);
// 发送信号通知进程
kill_fasync(&fasync, SIGIO, POLL_OUT);
}
……
}ssize_t write (struct file * file, const char __user *, size_t, loff_t *)
{
// 假设isEmpty为TRUE代表缓冲区为空,isFull为TRUE代表缓冲区满了
……
if (isFull)
{
if (file->f_flags & O_NONBLOCK)
{
return -EAGAIN;
}// 等待缓冲区不为满
ret = wait_event_interruptible(&write_queue, !isFull);
if (ret)
{
return ret;
}
}// 写入数据
……
isEmpty = FALSE;
// 通知可读
if (!isEmpty)
{
wake_up_interruptible(&read_queue);
// 发送信号通知进程
kill_fasync(&fasync, SIGIO, POLL_IN);
}
……
}
而应用则需要对此
SIGIO
信号进行捕获处理,并对POLL_IN
和POLL_OUT
等事件进行处理。#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include 【简单的字符设备驱动笔记】#include static int fd;
void my_signal_fun(int signum, siginfo_t *siginfo, void *act)
{
if (SIGIO == signum)
{
if (siginfo->si_band & POLLIN)
{
// 读取数据
……
}if (siginfo->si_band & POLLOUT)
{
// 写入数据
……
}
}
}int main(int argc, char **argv)
{
int ret;
int flag;
struct sigaction act, old_act;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGIO);
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = my_signal_fun;
// 注册信号处理函数
if (sigaction(SIGIO, &act, &old_act) == -1)
{
goto fail;
}// 打开文件
fd = open("/dev/sample_kfifo_module0", O_RDWR);
if (fd < 0)
{
goto fail;
}// 设置将要在文件描述词fd上接收SIGIO 或 SIGURG事件信号的进程或进程组标识
if (fcntl(fd, F_SETOWN, getpid()) == -1)
{
goto fail;
}// 设置标识输入输出可进行的信号
if (fcntl(fd, F_SETSIG, SIGIO) == -1)
{
goto fail;
}// 获取当前fd的flag
if ((flag = fcntl(fd, F_GETFL)) == -1)
{
goto fail;
}// 设置当前fd的flag
if (fcntl(fd, F_SETFL, flag | FASYNC) == -1)
{
goto fail;
}while(1)
{
sleep(1);
}fail:
perror("fasync test");
exit(EXIT_FAILURE);
}
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量