简单的字符设备驱动笔记

字符设备驱动详解 字符设备驱动的抽象 字符设备是以字符流为处理对象的设备。在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
  1. 产生cdev
    可由两种方式来产生struct cdev,一种是使用全局静态变量,另一种是使用cdev_alloc函数。
static struct cdev myCdev; 或 struct cdev myCdev = cdev_alloc();

  1. 初始化cdev
    cdev_init ()函数,初始化cdev数据结构,并建立该设备和ops(file_operations)的连接关系。
void cdev_init(struct cdev *cdev, const struct file_operations *fops);

  1. 注册cdev
    cdev_add()函数,把一个字符设备添加到系统中,通常在驱动程序的probe函数里面调用该接口来注册一个字符设备。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

  • p 表示一个设备的cdev数据结构。
  • dev 表示设备的设备号。
  • count 表示该设备有多少个次设备号。
  1. 删除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内核提供两个接口函数完成设备号的申请。
  1. 注册指定的主设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name);

  1. 申请分配一个主设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

  1. 释放主设备号
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内核中,采用一个称为等待队列的机制来实现进程阻塞操作。
  1. 等待队列头
    等待队列定义了一个被称为等待队列头(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);

  1. 等待队列元素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;

  1. 睡眠等待
    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。
  1. 唤醒
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内核提供pollselectepoll这3种I/O多路复用的机制。I/O多路复用即一个进程可以同时捡屎多个打开的文件描述符,一旦某个文件描述符就绪,就立即通知程序进行相应的读写操作。
pollselect方法在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方法会执行如下步骤。
  1. 在一个或者多个等待队列中调用poll_wait()函数。poll_wait()函数会把当前进程添加到指定的等待列表(poll_table)中,当请求数据准备好之后,会唤醒这些睡眠的进程。
  2. 返回监听事件,也就是POLLIN或者POLLOUT等掩码。
    poll方法的作用就是让应用程序同时等待多个数据流。
要让设备支持select/poll操作,则需要在驱动程序中对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_INPOLL_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); }

    推荐阅读