A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

本帖最后由 不二晨 于 2018-7-2 09:33 编辑

介绍

这里是linux驱动最基础部分,我会实现一个不使用任何驱动框架、不包含任何硬件操作的程序来实现一个字符设备驱动(参考宋宝华老师的程序),其中会包含互斥锁、等待队列、定时和异步通知的使用,最后分析它们在内核中到底是如何运作的。

代码和验证

这是一个用内存模拟的字符设备驱动,在初始化时会分配一页的内存给fifo_dev.mem_entry,在写入函数中,在之前内容的末尾添加内容,在读取函数中从这段内存头部读取内容,然后将读取的内容删除并将剩下的内容移动到内存头部。也就是一个模仿FIFO的操作。

#include <linux/module.h>#include <linux/types.h>#include <linux/sched.h>#include <linux/init.h>#include <linux/cdev.h>#include <linux/slab.h>#include <linux/poll.h>#include <linux/uaccess.h>#define PAGE_LEN 4096      
// 页的大小为4096字节#define FIFO_MAJOR 233      
// 自动分配设备号定义为0#define FIFO_CLEAR 0x22330#define TIM_INIT 0x22331#define TIM_OPEN 0x22332#define TIM_CLOSE 0x22333struct fifo_dev {    struct cdev cdev;    uint8_t *mem_entry;    int offset;    struct mutex mutex;         
// 该锁用于读写的并发控制    wait_queue_head_t w_wait;    wait_queue_head_t r_wait;    struct timer_list timer;    struct fasync_struct *async_queue;  
// 异步通知队列};static struct fifo_dev *fifo_devp;static int fifo_major = FIFO_MAJOR;
// fctnl(fd, F_SETFL, xxx)会映射到本函数, 同时此函数相当于初始化了
// dev->async_queue异步通知队列static int fifo_fasync(int fd, struct file *filp, int mode){    struct fifo_dev *dev = filp->private_data;    return fasync_helper(fd, filp, mode, &dev->async_queue);}
// 每秒向fifo中添加10个字符, 然后唤醒读进程. 并发送异步通
// 此操作会清除掉fifo中0~9的内容, 这里不能阻塞static void fifo_do_timer(unsigned long arg){    struct fifo_dev *dev = (struct fifo_dev *)arg;    char *buf = "0123456789";    memcpy(dev->mem_entry, buf, 10);    dev->offset = dev->offset > 10 ? dev->offset : 10;    wake_up_interruptible(&dev->r_wait);    if(dev->async_queue) {        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);        printk(KERN_INFO "%s kill SIGIO\n", __func__);    }    printk(KERN_INFO "jiffies: %ld\n", jiffies);    mod_timer(&dev->timer, jiffies + HZ);}static long fifo_ioctl(struct file *filp, unsigned int cmd,                 unsigned long arg){    struct fifo_dev *dev = filp->private_data;    switch(cmd) {    case FIFO_CLEAR:        memset(dev->mem_entry, 0, PAGE_LEN);        dev->offset = 0;        printk(KERN_INFO "fifo clear\n");        break;    case TIM_INIT:        init_timer(&dev->timer);        dev->timer.function = &fifo_do_timer;        dev->timer.data = (unsigned long)dev;        break;    case TIM_OPEN:        dev->timer.expires = jiffies + HZ;        add_timer(&dev->timer);        break;    case TIM_CLOSE:        del_timer(&dev->timer);        break;    default:        return -EINVAL;    }    return 0;}
// 只能从头开始读, ppos参数无效static ssize_t fifo_read(struct file *filp, char __user *buf,                size_t count, loff_t *ppos){    struct fifo_dev *dev = filp->private_data;    int ret = 0;    DECLARE_WAITQUEUE(wait, current);   
// 读之前上锁, 不允许其他进程读和写    mutex_lock(&dev->mutex);    add_wait_queue(&dev->r_wait, &wait);   
// 若内容不够, 调度到其他进程等待满足要求长度时再读取    while(count > dev->offset) {        if(filp->f_flags & O_NONBLOCK) {            ret = -EAGAIN;            goto out;        }        mutex_unlock(&dev->mutex);        __set_current_state(TASK_INTERRUPTIBLE);        schedule();     //interruptible_sleep_on(&dev->r_wait);        if(signal_pending(current)) {            ret = -ERESTARTSYS;            goto sig_out;        }        mutex_lock(&dev->mutex);        printk(KERN_INFO "read process wake up: %d, %d\n",                 dev->offset, count);    }   
//count = count > dev->offset ? dev->offset : count;   
// entry = 0, count = 3, offset = 6 --> offset = 3   
// 0 1 2 3 4 5 6 7 8 9   
// 先拷贝出0~2, 再将3~5拷贝到0~2处    if(copy_to_user(buf, dev->mem_entry, count)) {        ret = -EFAULT;        goto out;    } else {        
// 删除已读数据        memcpy(dev->mem_entry, dev->mem_entry + count,                 dev->offset - count);        dev->offset -= count;        ret = count;        printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count,               dev->offset);        wake_up_interruptible(&dev->w_wait);    }out:    mutex_unlock(&dev->mutex);sig_out:    remove_wait_queue(&dev->r_wait, &wait);    set_current_state(TASK_RUNNING);    return ret;}
// 只能从末尾开始写, ppos参数无效static ssize_t fifo_write(struct file *filp, const char __user *buf,                size_t count, loff_t *ppos){    struct fifo_dev *dev = filp->private_data;    int left;    int ret = count;    DECLARE_WAITQUEUE(wait, current);   
// 写之前上锁, 不允许其他进程读和写    mutex_lock(&dev->mutex);    /
/ 添加到等待队列    add_wait_queue(&dev->w_wait, &wait);   
// 内存不够时, 等待其他进程释放出足够内存    left = PAGE_LEN - dev->offset;    while(count > left) {      
// 非阻塞读直接返回        if(filp->f_flags & O_NONBLOCK) {            ret = -EAGAIN;            goto out;        }        
// 先解锁再调度, 否则其他进程不能获取资源        mutex_unlock(&dev->mutex);        __set_current_state(TASK_INTERRUPTIBLE);        schedule();     //interruptible_sleep_on(&dev->w_wait);        
// 由信号唤醒本进程, 则直接退出        
// 不加这个判断信号无法打断程序        if(signal_pending(current)) {            ret = -ERESTARTSYS;            goto sig_out;        }        
// 重新调度回本进程, 先上锁然后再一次检测内存是否足够        mutex_lock(&dev->mutex);        left = PAGE_LEN - dev->offset;        printk(KERN_INFO "write process wake up: %d, %d\n",                 left, count);    }   
//count = count > left ? left : count;   
// entry = 0, count = 6 --> offset = 6   
// 0 1 2 3 4 5 6 7 8 9   
// 实际上拷贝了0~5的内容    if(copy_from_user(dev->mem_entry + dev->offset, buf, count)) {        ret = -EFAULT;        goto out;    } else {        dev->offset += count;        ret = count;        printk(KERN_INFO "write %d bytes(s),current_len:%d\n", count,               dev->offset);        
// 写完之后唤醒等待的读进程        wake_up_interruptible(&dev->r_wait);    }out:    mutex_unlock(&dev->mutex);sig_out:    remove_wait_queue(&dev->w_wait, &wait);    set_current_state(TASK_RUNNING);    return ret;}static int fifo_open(struct inode *inode, struct file *filp){    filp->private_data = fifo_devp;    return 0;}static int fifo_release(struct inode *inode, struct file *filp){    fifo_fasync(-1, filp, 0);    return 0;}static const struct file_operations fifo_fops = {    .owner = THIS_MODULE,    .read = fifo_read,    .write = fifo_write,    .open = fifo_open,    .release = fifo_release,    .unlocked_ioctl = fifo_ioctl,    .fasync = fifo_fasync};static int fifo_setup_cdev(struct fifo_dev *dev, int minor){    int err;    dev_t devno = MKDEV(fifo_major, minor);   
// 绑定cdev和fops    cdev_init(&dev->cdev, &fifo_fops);    dev->cdev.owner = THIS_MODULE;   
// 绑定cdev和设备号    err = cdev_add(&dev->cdev, devno, 1);    if(err) {        printk(KERN_NOTICE "Error %d adding globalfifo%d", err, minor);        return 0;    }    return 1;}static int __init fifo_init(void){    int ret;    dev_t devno;    if(fifo_major) {        
// 获取设备号, 次设备号为0        devno = MKDEV(fifo_major, 0);        
// 占用指定设备号, 数量为1, 名字为"myfifo"        ret = register_chrdev_region(devno, 1, "myfifo");    } else {        
// 系统自动分配设备号, 分配设备号存在devno中, 起始次设备号为0        
// 数量为2, 名字为"myfifo"        ret = alloc_chrdev_region(&devno, 0, 1, "myfifo");        
// 获得主设备号        fifo_major = MAJOR(devno);    }    if(ret < 0) {        printk(KERN_NOTICE "fail to register char device\n");        return ret;    }   
// kzalloc是使用kmalloc分配内存, 然后将分配的内存置为0    fifo_devp = kzalloc(sizeof(struct fifo_dev), GFP_KERNEL);    if(!fifo_devp) {        ret = -ENOMEM;        goto fail_malloc;    }    fifo_devp->mem_entry = kzalloc(PAGE_LEN, GFP_KERNEL);   
// 添加cdev到系统    if(!fifo_setup_cdev(fifo_devp, 0)){        printk(KERN_NOTICE "fail to setup cdev\n");        goto fail_setup;    }   
// 初始化互斥锁    mutex_init(&fifo_devp->mutex);   
// 初始化等待队列    init_waitqueue_head(&fifo_devp->r_wait);    init_waitqueue_head(&fifo_devp->w_wait);    return 0;fail_setup:    kfree(fifo_devp->mem_entry);    kfree(fifo_devp);fail_malloc:    unregister_chrdev_region(devno, 1);    return ret;}module_init(fifo_init);static void __exit fifo_exit(void){    cdev_del(&fifo_devp->cdev);    kfree(fifo_devp->mem_entry);    kfree(fifo_devp);    unregister_chrdev_region(MKDEV(fifo_major, 0), 1);}module_exit(fifo_exit);MODULE_AUTHOR("colourfate <hzy1q84@foxmail.com>");MODULE_LICENSE("GPL v2");

安装该模块后执行:mknod /dev/myfifo c 233 0 创建字符设备文件,主设备号为233, 次设备号为0。

字符设备驱动1. 字符驱动设备的注册

在fifo_setup_cdev() 函数中包含了字符设备驱动的注册过程。其中主要包含了两个函数:cdev_init() 和 cdev_add(),下面用代码树状图(只包含关键函数调用)来分析这两个函数。

--- linux --- fs --- char_dev.c --- cdev_init(                            --- INIT_LIST_HEAD(&cdev->list)            |                     |    struct cdev *cdev,                   |- kobject_init(&cdev->kobj, &ktype_cdev_default)            |                     |    const struct file_operations *fops)  |- cdev->ops = fops            |                     |                                            // 绑定fops到cdev            |                     |- cdev_add(         --- kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p)            |                     |    struct cdev *p,             |                     |    dev_t dev,            |                     |    unsigned count)            |            |- drivers --- base --- map.c --- kobj_map(                  --- 申请一个probe结构                                                struct kobj_map *domain,  |- 将设备号和cdev封装到probe结构中                                                dev_t dev,                |- 将probe放进cdev_map数组中                                                unsigned long range,                                                struct module *module,                                                kobj_probe_t *probe,                                                int (*lock)(dev_t, void *),                                                void *data)

  • 注意inode->i_rdev表示设备号, 当执行mknod /dev/<name> c <dev> 0 (其中<name>是设备名称, <dev>是设备号) 时, 该值就被赋值给了inode。
  • 当inode没有绑定cdev时, 从cdev_map中查找是否有相同设备号的cdev,如果有就将该cdev和inode相绑定,然后替换了file中的fops,之后就可以驱动中写好的open, read, write等函数了。
等待队列1. 等待队列在内核中的实现

驱动中使用了两个等待队列r_wait 和 w_wait 分别用于读和写。当写的时候,先将本进程添加到等待队列w_wait,然后检查剩余内存是否足够,若不够则将本进程设置为可中断等待状态,然后调度到其他进程;之后如果进行一次读操作,读取完成后会调用wake_up_interruptible() 函数唤醒w_wait 上的所有进程;读操作完成后最终会调度到刚才写的写进程中,程序继续执行,写完成后将本进程从w_wait等待队列中移除。
这整个过程依赖以下几个个函数:

// 1. 初始化等待队列头init_waitqueue_head(q)// 2. 用进程tsk初始化一个等待队列元素nameDECLARE_WAITQUEUE(name, tsk)// 3. 添加等待队列元素wait到等待队列q中void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)// 4. 设置当前进程为TASK_INTERRUPTIBLE, 然后调度到其他进程__set_current_state(TASK_INTERRUPTIBLE)schedule()// 5. 唤醒一个等待队列wake_up_interruptible(x)

这些函数的调用路线如下:

--- include --- linux --- wait.h --- init_waitqueue_head(q) --- __init_waitqueue_head((q), #q, &__key)  |                                |- DECLARE_WAITQUEUE --- wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk) {  |                                |                     |      .private = tsk,  |                                |                     |      .func = default_wake_function,  |                                |                     |      .task_list = { NULL, NULL }  |                                |                     |  }  |                                |- __add_wait_queue() --- list_add(&new->task_list, &head->task_list)  |                                |                         //将等待元素添加到等待队列头指向的链表中  |                                |- wake_up_interruptible(x) --- __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)  |  |- kernel --- sched --- wait.c --- __init_waitqueue_head() --- spin_lock_init(&q->lock)                       |          |                           |- INIT_LIST_HEAD(&q->task_list)                       |          |- add_wait_queue() --- __add_wait_queue(q, wait)                       |          |- ___wake_up() --- _wake_up_common(q, mode, nr_exclusive, 0, key)                       |          |- __wake_up_common() --- list_for_each_entry_safe(curr, next, &q->task_list, task_list){                        |                                 |      curr->func(curr, mode, wake_flags, key)                        |                                 |  }//遍历链表, 分别执行唤醒函数                       |                       |- core.c --- default_wake_function() --- try_to_wake_up(curr->private, mode, wake_flags)                                  |- try_to_wake_up() --- if (!(p->state & state))                                  |                    |      goto out;                                  |                    |- ttwu_queue()                                  |- ttwu_queue() --- ttwu_do_activate()                                  |- ttwu_do_activate() --- ttwu_activate()                                  |                      |- ttwu_do_wakeup()                                  |- ttwu_do_wakeup() --- p->state = TASK_RUNNING//最终将当前任务设置为就绪态

  • 初始化时,使用init_waitqueue_head(&fifo_devp->w_wait)将w_wait中的链表头初始化。
  • 添加等待队列时,调用DECLARE_WAITQUEUE(wait, current)时,将当前进程任务结构体放入的wait中,并将.func = default_wake_function,然后使用add_wait_queue(&dev->w_wait, &wait)将声明的wait插入w_wait链表的尾部
  • 休眠时直接调用__set_current_state(TASK_INTERRUPTIBLE)和schedule()。
  • 唤醒时调用wake_up_interruptible(&dev->w_wait),该函数会在__wake_up_common()函数中遍历w_wait中的链表,然后分别执行等待元素的.func函数指针,也就是default_wake_function()函数,该函数最终会将wait中任务结构体的状态设置为就绪态TASK_RUNNING,之前进入等待的进程就可以在下一次被调度了,从而完成唤醒。
2. 验证等待队列

我们写两个简单的程序来验证等待队列。首先是read.c程序,该程序打开/dev/myfifo,然后从中读取10个字节,然后打印出来。

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(void){    int fd;    char buf[10];    fd = open("/dev/myfifo", O_RDWR);    if(fd < 0) {        perror("open");        return -1;    }   

// 此时文件为空, 应该阻塞住    read(fd, buf, 10);    printf("read: %s\n", buf);    return 0;}

然后是write.c程序,该程序将”hello”写入\dev\myfifo文件中。

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(void){    int fd;    char *buf = "hello";    fd = open("/dev/myfifo", O_RDWR);    if(fd < 0) {        perror("open");        return -1;    }    write(fd, buf, 5);    printf("write: %s\n", buf);    return 0;}

先执行./read &将read放入后台运行,由于此时驱动中内存没有内容,因此会该进程会休眠,不会有任何输出内容。再执行.\write,会向内存中写入5个字节,此时虽然唤醒了读进程,但是由于count < left,读进程又会再次休眠;此时再次执行./write,读进程就会读出此时内存中的内容”hellohello”,然后退出。

定时器

定时器的使用很简单,主要包含以下几个函数:

// 初始化定时器struct timer_list timer;init_timer(&timer);// 绑定定时器处理函数和传参timer.function = &xxx_do_timer;dev->timer.data = (unsigned long)xxx;// 设定定时时间,添加定时器dev->timer.expires = jiffies + HZ;add_timer(&timer);// 修改定时时间mod_timer(&timer, jiffies + HZ);// 删除定时器del_timer(&timer);

这里不分析它的具体实现,但需要注意的是定时器处理函数xxx_do_timer()被调用时是软中断上下文,是没有进程结构体的,不会进入调度器进行调度,因此如果在xxx_do_timer()执行期间被调度到其他进程,那么将不会返回这里继续执行。
驱动中使用ioctl()来控制定时器,定时器开启后,会不断地将内存中0~9的内容改写,并唤醒读进程,我们写一个ioctl.c来进行测试

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define FIFO_CLEAR 0x22330#define TIM_INIT 0x22331#define TIM_OPEN 0x22332#define TIM_CLOSE 0x22333int main(void){    int fd;    fd = open("/dev/myfifo", O_RDWR);    if(fd < 0) {        perror("open");        return -1;    }    ioctl(fd, FIFO_CLEAR, NULL);    ioctl(fd, TIM_INIT, NULL);    ioctl(fd, TIM_OPEN, NULL);    sleep(5);    ioctl(fd, TIM_CLOSE, NULL);    return 0;}

首先清空内存,然后初始化并打开定时器,休眠5秒后关闭定时器。我们先执行./read &后台进行读取,然后执行./ioctl可看到读进程被唤醒,并打印出了1234567890。

异步通知1. 异步通知在内核中的实现

异步通知有点类似于硬件中断的概念,它主要是将程序从阻塞中解放出来。进程先将一个函数和特定的信号绑定,当没有数据时,进程不用阻塞等待,可以继续执行,直到数据到来,此时进程会收到一个特定信号,与之绑定的函数得以执行。驱动中的异步通知是放在定时器中的,主要包含以下两个函数:

// fctnl(fd, F_SETFL, xxx)会映射到本函数int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)// 向与之绑定的用户进程发送信号void kill_fasync(struct fasync_struct **fp, int sig, int band)

异步通知在内核中的实现与等待队列有相似之处,异步通知的调用路线如下:

--- fs --- fctnl.c --- fasync_helper() --- if (!on)//on即mode参数                     |                   |      fasync_remove_entry(filp, fapp);                     |                   |  fasync_add_entry(fd, filp, fapp)                     |- fasync_add_entry() --- new = fasync_alloc()                     |                      |- fasync_insert_entry(fd, filp, fapp, new))                     |- fasync_insert_entry() --- 查找异步通知队列中是否已经指定的异步通知结构                     |                         |  若存在, 直接退出                     |                         |- new->fa_file = filp                     |                         |- new->fa_fd = fd                     |                         |- new->fa_next = *fapp                     |                         |  //给新的异步通知结构赋值, 并插入链表                     |                         |- filp->f_flags |= FASYNC                     |- kill_fasync() --- kill_fasync_rcu()                     |- kill_fasync_rcu() --- 遍历异步通知队列                                           |-     fown = &fa->fa_file->f_owner                                           |      //找到异步通知的owner                                           |-     send_sigio(fown, fa->fa_fd, band)                                                  //向owner发送信号

  • 在fasync_helper()函数中, 参数on用来控制是向异步通知队列中添加元素还是移除元素,如果是1,则执行fasync_add_entry(),该函数中声明了一个新的元素new,然后调用fasync_insert_entry()函数将它插入到异步通知队列fapp中,关键的插入过程是new->fa_next = *fapp。
  • 在kill_fasync()函数中,主要是遍历刚才的异步通知队列,获取其中的owner,然后调用send_sigio()向owner发送信号,这个owner就是与该设备文件绑定的进程,这一步在驱动中并没有实现,它是由操作系统来实现的,因此这一步不用太担心。
2. 验证异步通知

同样,我们写一个async.c程序来验证异步通知。

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <signal.h>#include <sys/stat.h>static int cnt = 0;static void signalio_handler(int signum){    printf("time: %d\n", cnt++);}void main(void){    int fd, oflags;    fd = open("/dev/myfifo", O_RDWR, S_IRUSR | S_IWUSR);    if (fd != -1) {        // 1. 将本进程接收到的SIGIO信号和处理函数绑定        signal(SIGIO, signalio_handler);        // 2. 将设备文件和本进程相绑定        fcntl(fd, F_SETOWN, getpid());        // 3. 打开设备文件异步通知机制        oflags = fcntl(fd, F_GETFL);        fcntl(fd, F_SETFL, oflags | FASYNC);        while (1) {            sleep(100);        }    } else {        printf("device open failure\n");    }}

打开异步通知分三步,其中我们驱动中只实现了第三步,其他都由操作系统实现了。先执行./async & 打开设备文件的异步通知,然后本进程进入休眠状态,此时如果有SIGIO信号产生,就会调用signalio_handler()打印调用次数。然后再执行./ioctl打开定时器,定时器会定时调用kill_fasync()函数发布信号,此时第一个进程就会接收到信号。

总结

使用一个简单的驱动程序实现内核提供的一些基本功能,为加深印象分析了字符设备、等待队列和异步通知的具体实现,写了几个简单的程序进行测试。



【转载】原文地址: https://blog.csdn.net/Egean/article/details/80827454



2 个回复

倒序浏览
回复 使用道具 举报
棒棒哒
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马