Linux设备驱动程序学习 高级字符驱动程序操作[阻塞型I/O和非阻塞I/O]【转】

转自:http://blog.csdn.net/jacobywu/article/details/7475432

阻塞型I/O和非阻塞I/O
阻塞:休眠
非阻塞:异步通知

一 休眠
安全地进入休眠的两条规则:
(1)      永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁、seqlock或者 RCU锁时不能睡眠;关闭中断也不能睡眠。持有一个信号量时休眠是合法的,但你应当仔细查看代码:如果代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠。因此发生在持有信号量时的休眠必须短暂,而且决不能阻塞那个将最终唤醒你的进程。
(2)   当进程被唤醒,它并不知道休眠了多长时间以及休眠时发生什么;也不知道是否另有进程也在休眠等待同一事件,且那个进程可能在它之前醒来并获取了所等待的资源。所以不能对唤醒后的系统状态做任何的假设,并必须重新检查等待条件来确保正确的响应。
在 Linux 中, 一个等待队列由一个wait_queue_head_t 结构体来管理,其定义在<linux/wait.h>中。wait_queue_head_t类型的数据结构非常简单:
[cpp] view plaincopy
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
简单休眠(其实是高级休眠的宏)
Linux 内核中最简单的休眠方式是称为 wait_event的宏(及其变种),它实现了休眠和进程等待的条件的检查。形式如下:
[cpp] view plaincopy
wait_event(queue, condition)/*不可中断休眠,不推荐*/
wait_event_interruptible(queue, condition)/*推荐,返回非零值意味着休眠被中断,且驱动应返回 -ERESTARTSYS*/
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
/*有限的时间的休眠;若超时,则不管条件为何值返回0,*/
唤醒休眠进程的函数称为 wake_up,形式如下:
[cpp] view plaincopy
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
eg:
[cpp] view plaincopy
ssize_t sleepy_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) going to sleep\n",
            current->pid, current->comm);
    wait_event_interruptible(wq, flag != 0);
    flag = 0;
    printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
    return 0; /* EOF */
}  

ssize_t sleepy_write (struct file *filp, const char __user *buf, size_t count,
        loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
            current->pid, current->comm);
    flag = 1;
    wake_up_interruptible(&wq);
    return count; /* succeed, to avoid retrial */
}  

阻塞和非阻塞操作
明确的非阻塞 I/O 由 filp->f_flags中的 O_NONBLOCK标志来指示(定义再<linux/fcntl.h> ,被<linux/fs.h>自动包含)。浏览源码,会发现O_NONBLOCK 的另一个名字:O_NDELAY ,这是为了兼容 System V 代码。O_NONBLOCK标志缺省地被清除,因为等待数据的进程的正常行为只是睡眠.
其实不一定只有read 和 write 方法有阻塞操作,open也可以有阻塞操作。后面会见到。而我的项目有一个和CPLD的接口的驱动,我决定要在ioctl 中使用阻塞。

高级休眠
步骤:
(1)   分配和初始化一个 wait_queue_t 结构,随后将其添加到正确的等待队列。

(2)   设置进程状态,标记为休眠。在<linux/sched.h> 中定义有几个任务状态:TASK_RUNNING 意思是进程能够运行。有 2 个状态指示一个进程是在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE。
[cpp] view plaincopy
void set_current_state(int new_state);
(3)最后一步是放弃处理器。 但必须先检查进入休眠的条件。如果不做检查会引入竞态: 如果在忙于上面的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。因此典型的代码下:
[cpp] view plaincopy
if (!condition)
    schedule();
如果代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 如果不需睡眠而跳过对 schedule 的调用,必须将任务状态重置为 TASK_RUNNING,还必要从等待队列中去除这个进程,否则它可能被多次唤醒。
手工休眠
[cpp] view plaincopy
/* (1)创建和初始化一个等待队列。常由宏定义完成:*/
DEFINE_WAIT(my_wait);
/*name 是等待队列入口项的名字. 也可以用2步来做:*/
wait_queue_t my_wait;
init_wait(&my_wait);
/*常用的做法是放一个 DEFINE_WAIT 在循环的顶部,来实现休眠。*/  

/* (2)添加等待队列入口到队列,并设置进程状态:*/
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
/*queue 和 wait 分别地是等待队列头和进程入口。state 是进程的新状态:TASK_INTERRUPTIBLE(可中断休眠,推荐)或TASK_UNINTERRUPTIBLE(不可中断休眠,不推荐)。*/  

/* (3)在检查确认仍然需要休眠之后调用 schedule*/
schedule();
/* (4)schedule 返回,就到了清理时间:*/
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
eg:
[cpp] view plaincopy
/* Wait for space for writing; caller must hold device semaphore.  On
 * error the semaphore will be released before returning. */
static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)
{
    while (spacefree(dev) == 0) { /* full */
        DEFINE_WAIT(wait);  

        up(&dev->sem);
        if (filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        PDEBUG("\"%s\" writing: going to sleep\n",current->comm);
        prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);
        if (spacefree(dev) == 0)
            schedule();
        finish_wait(&dev->outq, &wait);
        if (signal_pending(current))
            return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
        if (down_interruptible(&dev->sem))
            return -ERESTARTSYS;
    }
    return 0;
}     

认真地看简单休眠中的 wait_event(queue, condition) 和 wait_event_interruptible(queue, condition) 底层源码会发现,其实他们只是手工休眠中的函数的组合。所以怕麻烦的话还是用wait_event比较好。

独占等待
当一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重降低系统性能。为此,内核开发者增加了一个“独占等待”选项。它与一个正常的睡眠有 2个重要的不同:
(1)当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。
(2)当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒.但内核仍然每次唤醒所有的非独占等待。
采用独占等待要满足 2个条件:
(1)希望对资源进行有效竞争;
(2)当资源可用时,唤醒一个进程就足够来完全消耗资源。
使一个进程进入独占等待,可调用:
[cpp] view plaincopy
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
注意:无法使用 wait_event和它的变体来进行独占等待.
当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会处于读写阻塞的状态,并影响了其他文件的读写。为了避免这种情况,在必须使用多输入输出流又不想阻塞在它们任何一个上的应用程序常将非阻塞I/O 和 poll(System V)、select(BSD Unix)、epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标志读写相应的文件,实现非阻塞的读写。
这些调用都需要来自设备驱动中poll 方法的支持,poll返回不同的标志,告诉主进程文件是否可以读写,其原型(定义在<linux\poll.h> ):
[cpp] view plaincopy
unsigned int (*poll) (struct file *filp, poll_table *wait);
实现这个设备方法分两步:
1. 在一个或多个可指示查询状态变化的等待队列上调用 poll_wait. 如果没有文件描述符可用来执行 I/O, 内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符. 驱动通过调用函数 poll_wait增加一个等待队列到 poll_table 结构,原型:
[cpp] view plaincopy
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
2. 返回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过 <linux/poll.h> 定义)用来指示可能的操作:
 标志
 含义
 POLLIN
 如果设备无阻塞的读,就返回该值
 POLLRDNORM
 通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA)
 POLLRDBAND
 如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中
 POLLPRI
 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理
 POLLHUP
 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。
 POLLERR
 如果设备发生错误,就返回该值。
 POLLOUT
 如果设备可以无阻塞地些,就返回该值
 POLLWRNORM
 设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM)
 POLLWRBAND
 于POLLRDBAND类似

考虑 poll 方法的 scullpipe 实现:
[cpp] view plaincopy
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
        struct scull_pipe *dev = filp->private_data;
        unsigned int mask = 0;
        /*
        * The buffer is circular; it is considered full
        * if "wp" is right behind "rp" and empty if the
        * two are equal.
        */
        down(&dev->sem);
        poll_wait(filp, &dev->inq, wait);
        poll_wait(filp, &dev->outq, wait);
        if (dev->rp != dev->wp)
                mask |= POLLIN | POLLRDNORM; /* readable */
        if (spacefree(dev))
                mask |= POLLOUT | POLLWRNORM; /* writable */
        up(&dev->sem);
        return mask;
}  

刷新待处理输出
若一些应用程序需要确保数据被发送到设备,就实现必须fsync 方法。对 fsync 的调用只在设备被完全刷新时(即输出缓冲为空)才返回,不管 O_NONBLOCK 是否被设置,即便这需要一些时间。其原型是:
[cpp] view plaincopy
int (*fsync) (struct file *file, struct dentry *dentry, int datasync);   

二 异步通知
通过使用异步通知,应用程序可以在数据可用时收到一个信号,而无需不停地轮询。
总体过程图:

启用步骤:
(1)它们指定一个进程作为文件的拥有者:使用 fcntl系统调用发出 F_SETOWN 命令,这个拥有者进程的 ID被保存在 filp->f_owner。目的:让内核知道信号到达时该通知哪个进程。
(2)使用 fcntl系统调用,通过 F_SETFL命令设置 FASYNC 标志。
用户空间用例:
[cpp] view plaincopy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
int gotdata=0;
void sighandler(int signo)
{
    if (signo==SIGIO)
        gotdata++;
    return;
}
char buffer[21];
int main(int argc, char **argv)
{
    int pipetest0;
    int count;
    struct sigaction action;  

    if ((pipetest0 = open("/dev/scullpipe0",O_RDONLY)) < 0)  {
         printf("open scullpipe0 error! \n");
        exit(1);
    }
    memset(&action, 0, sizeof(action));
    action.sa_handler = sighandler;
    action.sa_flags = 0;
    sigaction(SIGIO, &action, NULL);  

    fcntl(pipetest0, F_SETOWN, getpid());
    fcntl(pipetest0, F_SETFL, fcntl(pipetest0, F_GETFL) | FASYNC);  

    while(1) {
        /* this only returns if a signal arrives */
        sleep(86400); /* one day */
        if (!gotdata)
            continue;
        count=read(pipetest0, buffer, 21);
        /* buggy: if avail data is more than 4kbytes... */
        write(1,buffer,count);
        gotdata=0;
    break;
    }  

    close(pipetest0 );
    printf("close pipetest0  ! \n");
    printf("exit !\n");
  exit(0);
}  

内核空间驱动:
1.F_SETOWN被调用时filp->f_owner被赋值。
2. 当F_SETFL被执行来打开 FASYNC, 驱动的fasync 方法被调用.这个标志在文件被打开时缺省地被清除。
3. 当数据到达时,所有的注册异步通知的进程都会被发送一个 SIGIO信号。
Linux 提供的通用方法是基于一个数据结构和两个函数,定义在<linux/fs.h>。
数据结构:
[cpp] view plaincopy
struct fasync_struct {
    int    magic;
    int    fa_fd;
    struct    fasync_struct    *fa_next; /* singly linked list */
    struct    file         *fa_file;
};  

驱动调用的两个函数的原型:
[cpp] view plaincopy
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
void kill_fasync(struct fasync_struct **fa, int sig, int band);  

当数据到达时,kill_fasync被用来产生信号通知相关的进程,它的参数是被传递的信号(常常是 SIGIO)和 band(几乎都是 POLL_IN)。
[cpp] view plaincopy
if (dev->async_queue)
 kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 注意, 一些设备也针对设备可写而实现了异步通知,在这个情况,kill_fasnyc 必须以 POLL_OUT 模式调用.*/  

当一个打开的文件的FASYNC标志被修改时,调用fasync_helper 来从相关的进程列表中添加或去除文件。
这是 scullpipe 实现 fasync 方法的:
[cpp] view plaincopy
static int scull_p_fasync(int fd, struct file *filp, int mode)
{
 struct scull_pipe *dev = filp->private_data;
 return fasync_helper(fd, filp, mode, &dev->async_queue);
}  

当文件被关闭时必须调用fasync 方法,来从活动的异步读取进程列表中删除该文件。尽管这个调用仅当 filp->f_flags 被设置为 FASYNC 时才需要,但不管什么情况,调用这个函数不会有问题,并且是普遍的实现方法。以下是 scullpipe 的 release 方法的一部分:
[cpp] view plaincopy
/* remove this filp from the asynchronously notified filp's */
scull_p_fasync(-1, filp, 0);
异步通知使用的数据结构和 struct wait_queue 几乎相同,因为他们都涉及等待事件。区别异步通知用 struct file 替代 struct task_struct. 队列中的 file 用获取 f_owner, 一边给进程发送信号。

 

时间: 2024-09-01 14:13:45

Linux设备驱动程序学习 高级字符驱动程序操作[阻塞型I/O和非阻塞I/O]【转】的相关文章

Linux 高级字符驱动操作 iotcl及阻塞IO

Linux设备驱动 高级字符驱动操作之iotcl 大部分驱动除了提供对设备的读写操作外,还需要提供对硬件控制的接口,比如查询一个framebuffer设备能提供多大的分辨率,读取一个RTC设备的时间,设置一个gpio的高低电平等等.而这些对硬件操作能力的实现一般都是通过ioctl方法来实现的 1. 原型介绍 Ioctl在用户空间的原型为: int ioctl(int fd, unsigned long cmd, ...); 原型中的点不表示一个变数目的参数, 而是一个 单个可选的参数, 传统上标

LDD3学习笔记(9):高级字符驱动操作

 1.ioctl接口 ioctl 驱动方法有和用户空间版本不同的原型: int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg); 2.阻塞I/O 阻塞进程,使它进入睡眠直到请求可继续. 当一个进程被置为睡眠,它被标识为处于一个特殊的状态并且从调度器的运行队列中去除,直到发生某些事情改变了那个状态.一个睡着的进程将不被任何CPU调度,因此不会运行,一个睡着的进程已被搁置到系

Linux 设备驱动--- 阻塞型字符设备驱动 --- O_NONBLOCK --- 非阻塞标志【转】

转自:http://blog.csdn.net/yikai2009/article/details/8653697 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] 阻塞 阻塞操作 非阻塞操作 阻塞方式-read- 实现 阻塞方式-write- 实现 非阻塞方式的读写操作 实例 --- 读阻塞的实现 实例 --- 按键驱动阻塞实现 1在 open 函数 查看看是 阻塞方式 还是 非阻塞方式 2在 read 函数中同样查看 3应用程序中 1以阻塞方式运行 2以非阻塞方式运

《Linux 设备驱动开发详解(第2版)》——导读

前言 本书第1版在2008年初出版以后,受到广大读者的支持和厚爱,累计销售1.6万册,从几年的市场和读者反馈看,在第1版中还存在一些不足,主要是以下几方面. 没有现成的开发环境,读者需要从头到尾构建,而构建需要花费很长的时间,许多时候会不成功,加之配套光盘中的实例没有Makefile,更加大了操作的难度. 没有配套的开发板,大量的基于S3C2410的实例读者身边如果没有可以直接运行的平台,就无法亲身体验这些驱动. 个别内容实用性不强或过于陈旧,也有个别知识点的讲解语言晦涩,读者不易理解,如pla

JAVA并发编程学习笔记之CAS操作

CAS操作 CAS是单词compare and set的缩写,意思是指在set之前先比较该值有没有变化,只有在没变的情况下才对其赋值. 我们常常做这样的操作 if(a==b) { a++; } 试想一下如果在做a++之前a的值被改变了怎么办?a++还执行吗?出现该问题的原因是在多线程环境下,a的值处于一种不定的状态.采用锁可以解决此类问题,但CAS也可以解决,而且可以不加锁. int expect = a; if(a.compareAndSet(expect,a+1)) { doSomeThin

《LINUX设备驱动程序》学习札记(二)

总结章节:第三节 字符设备驱动这一章主要是讲一些字符设备驱动程序的相关知识.在进行字符设备驱动程序学习之前,我们必须得弄明白一个问题:驱动程序时给谁用的.很多驱动程序的初学者,按照一般编程语言(C,java等等)编写应用程序的经验,函数就是用来调用的.这种调用关系,在应用空间来看,限制不是很明显.很多初学者都会误认为,在驱动程序中写的函数是为了以后应用程序来调用,比如:在驱动中程序中实现了switch()函数的功能,按照应用程序的观念,在应用程序中应该可以调用switch()函数,从而来实现某种

为系统处理器编写Linux设备驱动程序

引 言 编写 Linux 设备驱动程序无疑是一项复杂的工作.本文将集中介绍非标准硬件的设备驱动程序编写,探讨硬件应用编程接口,并借用 Cirrus Logic EP9312 片上系统嵌入式平台添加设备驱动程序这一案例来进行分析. 如果有些编程内容未能在本文中涉及,那么读者亦可以查阅相似的设备驱动程序编码,以做参考.还有一种方法,就是检索历史档案或者向 Linux 内核问讯中心去函问讯. Linux 概述 Linux 是 UNIX 操作系统的翻版,1991 年由 Linus Torvalds 最先

如何编写Linux设备驱动程序

序言 Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别.在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便.本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程序,获得了一些经验,愿与Linux fans共享,有不当之处,请予指正. 以下的一些文字主要来源于khg,johnson

如何编写Linux设备驱动程序_unix linux

    Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别.在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便.本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程序,获得了一些经验,愿与Linux fans共享,有不当之处,请予指正. 以下的一些文字主要来源于khg,johnso