简单的字符设备驱动笔记

字符设备驱动详解

字符设备驱动的抽象

字符设备是以字符流为处理对象的设备。在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 MINORBITS  20
#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_t      lock;
    struct list_head    head;
};
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 int        flags;
    void            *private;
    wait_queue_func_t   func;
    struct list_head    task_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 {
               int   fd;         /* 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_t      fa_lock;
    int         magic;
    int         fa_fd;
    struct fasync_struct    *fa_next; /* singly linked list */
    struct file     *fa_file;
    struct rcu_head     fa_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);
}

你可能感兴趣的:(简单的字符设备驱动笔记)