第六章--高级字符驱动程序操作

来源:互联网 发布:安卓模拟器对比 知乎 编辑:程序博客网 时间:2024/05/22 00:43

一、ioctl

        ioctl-number.txt:罗列了内核所使用的幻数。查看此文件,保证幻数不要重复。

        ioctl函数使用下列字段实现命令:

        1) type,幻数,占有8位宽。

        2) number,序数,也是8位宽。

        3) direction,如果涉及到数据传输,则该字段定义数据传输的方向。包括读、写和双向。数据传输是从应用层的角度看的

        4) size,用户数据大小。
        从任何一个系统调用返回时,正的返回值是受保护的(正如我们在read和write所见到的),而负值则被认为是一个错误,并被用来设置用户空间的errno变量。
        在传送(1、2、4及8个字节)个数据时,应该使用_put_user()和_get_user()。
        put_user(datum, ptr)
        __put_user(datum, ptr)
        这些宏把datum写到用户空间。它们相对比较快,当要传递单个数据时,应该用这些宏而不是用copy_to_user。由于这些宏在展开时不做类型检查,所以可以传递给put_user任意类型的指针,只要是个用户空间地址就行。传递的数据大小依赖于ptr参数的类型,在编译时由编译器的内建指令sizeof和typeof确定。总之,如果ptr是一个字符指针,就传递1个字节,2、4、8字节的情况类似。
        put_user进行检查以确保进程可以写入指定的内存地址,并在成功时返回0,出错返回-EFAULT。__put_user做的检查少些(它不调用access_ok),但如果地址指向用户不能写入的内存,也会出现操作失败。因而,__put_user应该在已经使用access_ok检验过内存区后再使用。
        一般的用法是,实现一个读取方法时,可以调用__put_user来节省几个时钟周期,或者在复制多项数据之前调用一次access_ok,就像上面的ioctl代码一样。
        get_user(local, ptr)
        __get_user(local, ptr)
        这些宏用于从用户空间接收一个数据,除了传输方向相反之外,它们与put_user和__put_user差不多。接收的数值被保存在局部变量local中,返回值则指明了操作是否成功。同样,__get_user应该在操作地址已被access_ok检验后使用。
二、阻塞型I/O

2.1、休眠
        休眠:当一个进程被置入休眠时,它会被标记为一种特殊状态并从调度器的运行队列中移走。
        为了将进程以一种安全的方式进入睡眠,必须遵循两条规则:

        1) 永远不要在原子上下文中进入休眠。原子上下文就是指在执行多个步骤时,不能有任何的并发访问。这意味着对休眠来说,驱动程序不能在拥有自旋锁、seqlock或者RCU锁时休眠,如果我们已经禁止了中断,也不能休眠。如果代码在拥有信号量时休眠,任何其他等待该信号量的线程也会休眠,因此任何拥有信号量而休眠的代码必须很短,并且还要保证拥有信号量并不会阻塞最终会唤醒我们自己的那个进程。

        2) 当被唤醒时,永远无法知道休眠了多长时间,或者休眠期间都发生了什么事情。因此我们对唤醒之后的状态不能做任何假定,因此必须检查以确保我们等待的条件真正为真。

        3) 除非我们知道有其他人会在其他地方唤醒我们,否则进程不能休眠。
        等待队列就是一个进程链表,其中包含了等待某个特定事件的所有进程。

        在Linux中,一个等待队列通过一个“等待队列头(wait queue head)”来管理,等待队列头是一个类型为wait_queue_head的结构体,定义在<linux/wait.h>中。

    休眠函数:

    wait_event(queue, condition)

    wait_event_interruptible(queue, condition)   //可以被信号中断,返回非0值表示休眠被某个信号中断

    wait_event_timeout(queue, condition, timeout)    //无论condition如何,只要时间到期,就返回0

    wait_event_interruptible_timeout(queue, condition, timeout)

    在condition条件为真之前,进程会保持休眠。注意,该条件可能会被多次求值,因此对该表达式的求值不能带来任何副作用。

    唤醒函数:

    void wake_up(wait_queue_head_t *queue)    //唤醒等待在给定queue上的所有进程

    void wake_up_interruptible(wait_queue_head_t *queue)    //只会唤醒那些执行可中断休眠的进程
    显式的非阻塞I/O由filp->f_flags中的O_NONBLOCK标志决定。
2.2、阻塞和非阻塞型操作
        显式的非阻塞I/O由filp->f_flags中的O_NONBLOCK标志决定。
        在执行阻塞型操作的情况下,应该实现下列动作以保持和标准语义一致:
        1、如果一个进程调用了read但是还没有数据可读,此进程必须阻塞。数据到达时进程被唤醒,并把数据返回给调用者。即使数据数目少于count参数指定的数目也是如此。
        2、如果一个进程调用了write但缓冲区没有空间,此进程必须阻塞,而且必须休眠在与读取进程不同的等待队列上。当向硬件设备写入一些数据,从而腾出了部分输出缓冲区后,进程即被唤醒,write调用成功。即使缓冲区中可能没有所要求的count字节的空间而只写入了部分数据,也是如此。
        输入缓冲区用于当数据已到达而又无人读取时,把数据暂存起来避免丢失;相反,如果调用write时系统不能接收数据,就将它们保留在用户空间缓冲区中不致会丢失。
        如果指定了O_NONBLOCK标志,read和write的行为就会有所不同。如果在数据没有就绪是调用read或是在缓冲区没有空间时调用write,则该调用简单地返回-EAGAIN。
        只有read、write和open文件操作受非阻塞标志的影响。
        在驱动程序内部,阻塞在read调用的进程在数据到达时被唤醒;通常硬件会发出一个中断来通知这个事件,然后作为中断处理的一部分,驱动程序会唤醒等待进程。
2.3、高级休眠
        将进程置于休眠的步骤:
        1、分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列。在完成这些工作后,不管谁负责唤醒该进程,都能找到正确的进程。
        2、设置进程的状态,将其标记为休眠
        3、检查休眠等待的条件,如果不作这个检查,可能引入竞态。
        独占等待的行为和通常的休眠类似,但有两个不同:
        1、等待队列入口设置了WQ_FLAG_EXCLUSIEV标志时,则会被添加到等待队列的尾部。而没有这个标志的入口会被添加到头部。
        2、在某个等待队列上调用wake_up时,它会在唤醒第一个具有WQ_FLAG_EXCLUSIVE标志的进程之后停止唤醒其他进程。
        最后的结果是,执行独占等待的进程每次只会被唤醒其中的一个(以某种有序的方式),从而不会产生“疯狂兽群”问题,但是,内核每次仍然会唤醒所有非独占等待进程。
        如果满足下面两个条件,在驱动程序中使用独占等待是值得考虑的:
        1、对某个资源存在严重竞争。
        2、唤醒单个进程就能完整消耗该资源。
        永远不要使用sleep_on和interruptible_sleep_on两个函数

三、poll和select
        poll、select和epoll的功能本质上是一样的:都允许进程决定是否可以对一个或多个打开的文件做非阻塞的读取或写入。这些调用也会阻塞进程,直到给定的文件描述符集合中的任何一个可读取或写入。因此他们常常用于那些要使用多个输入或输出流而又不会
阻塞于其中任何一个流的应用程序中。
3.1、poll函数

        功能:允许进程决定是否可以对一个或多个打开的文件做非阻塞的读取或者写入。

        驱动中都是通过poll方法来实现的。

        unsigned int (*poll)(struct file *filp, poll_table *wait)

        当用户空间程序在驱动程序关联的文件描述符上执行poll、select或epoll系统调用时,该驱动程序方法将被调用,该方法分为两部:

        1) 在一个或多个可指示poll状态变化的等待队列上调用poll_wait。如果当前没有文件描述符可用来执行I/O,则内核将使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。

        2) 返回一个用来描述操作是否可以立即无阻塞执行的位掩码。

        应用程序使用select()或poll()调用设备驱动程序的poll()函数,该函数把输入输出复用处理的等待队列追加到由内核管理的进程的poll_table()上。此时poll() 函数上传递的参数包括含有设备文件信息的 struct file 结构体的指针参数 struct file *filp,以及追加到设备驱动上的 poll_table结构体指针参数 poll_table *wait 。使用这两个参数,然后通过poll_wait()函数,在内核上注册输入输出复用条件。poll_wait() 函数表示如下:

        #include <linux/poll.h>

        staticinline voidpoll_wait (structfile*filp,wait_queue_head_t*wait_address,poll_table*P);
        poll和select调用的目的是确定接下来的I/O操作是否会阻塞,更重要的用途是它们可以使应用程序同时等待多个数据流。
        为了使应用程序正常工作,总结规则:
        从设备读取数据:
        1、如果输入缓冲区有数据,那么即使就绪的数据比程序所请求的少,并且驱动程序保证剩下的数据马上就能到达,read调用仍然应该以难以察觉的延迟立即返回。如果为了某种方便,read甚至可以一直返回比所请求树木少的数据,当然,前提是至少得返回一个字节。
        2、如果输入缓冲区中没有数据,那么默认情况下read必须阻塞等待,知道至少有一个字节到达。另一方面,如果设置了O_NONBLOCK标志,read应立即返回,返回值是-EAGAIN。在这种情况下poll必须报告设备不可读,直到至少有一个字节到达。一旦缓冲区中有了数据,我们就回到前一种情况。
        3、如果已经到达文件尾,read应该立即返回0,无论O_NONBLOCK是否设置,此时poll应该报告POLLHUP。
        向设备写数据:
        1、如果输出缓冲区中有空间,则write应该无延迟地立即返回。它可以接收比请求少的数据,但至少要接收一个字节。在这种情况
下,poll报告设备可写。
        2、如果输出缓冲区已满,那么默认情况下write被阻塞直到有空间释放,如果设置了O_MOMBLOCK标志,write应立即返回,返回值是-EAGAIN。这时poll应该报告文件不可写。另一方面,如果设备不能再接收任何数据,则write返回-ENOSPC,而不管O_NONBLOCK是否设置。
        3、永远不要让write调用在返回前等待数据的传输结束,即使O_NONBLOCK标志被清除。
        刷新待处理输出:
        int (*fsync) (struct file *file, struct dentry *dentry, int datasync);
        如果应用程序需要确保数据已经被传送到设备上,就必须实现fsync方法。一个fsync调用只有在设备已经被完全刷新(输出缓冲区为空)时才会返回,即使这要花一些时间,是否设置了O_NONBLOCK标志对此没有影响。参数datasync用于区分fsync和fdatasync这两个系统调用。
        block_fsync会依次刷新设备的所有缓冲块,并等待所有I/O结束。

3.2关于应用程序中 poll() 的返回值 :

        应用程序中调用select()和poll()函数,使进程进入睡眠之前,内核先检查设备驱动程序上有无对应事件的状态,此时可通过查看
poll()函数的返回值获得相关信息.

        能够在返回值上使用的宏变量有以下组合:

        POLLIN, POLLPRI, POLLOUT, POLLERR, POLLHUP, POLLNVAL,  POLLRDNORM,  POLLRDBAND,  POLLWRNORM, 
POLLWRBAND, POLLMSG,  POLLREMOVE



常量说明POLLIN普通或优先级带数据可读POLLRDNORM普通数据可读POLLRDBAND优先级带数据可读POLLPRI高优先级数据可读POLLOUT普通数据可写POLLWRNORM普通数据可写POLLWRBAND优先级带数据可写POLLERR发生错误POLLHUP发生挂起POLLNVAL描述字不是一个打开的文件

        这些值中使用最多的是下面几个组合:
  • POLLIN | POLLRDNORM 表示可读
  • POLLOUT | POLLWRNORM 表示可写
  • POLLERR 表示出错

3.3、应用

        poll_wait()是用在select系统调用中的。一般你的代码会有一个struct file_operations结构,其中fop->poll函数指针指向一个
你自己的函数,
在这个函数里应该调用poll_wait()当用户调用select系统调用时,select系统调用会先调用poll_initwait(&table);
后调用你的
fop->poll();从而将current加到某个等待队列(这里调用poll_wait()),并检查是否有效如果无效就调用schedule_timeout();
去睡眠。

        事件发生后,schedule_timeout()回来,调用fop->poll();检查到可以运行,就调用poll_freewait(&table);从而完成select系统调用。
重要的是fop->poll()里面要检查是否就绪,
如果是,要返回相应标志

        1) 等待队列的使用

        DECLARE_WAITQUEUE(wait, current);

        #define __WAITQUEUE_INITIALIZER(name, tsk) {                \
            .private    = tsk,                        \
            .func        = default_wake_function,            \
            .task_list    = { NULL, NULL } }

        #define DECLARE_WAITQUEUE(name, tsk)                    \
        wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

        一目了然,给wait这个等待队列赋值,private用来表示被唤醒的进程,这里current代表了本进程,而func是被唤醒时调用的回调函
数, 唤醒private!

        add_wait_queue(w_wait, wait);
    //将wait加入到等待队列w_wait中,被唤醒时遍历w_wait队列!

        wake_up(w_wait);    //唤醒w_wait,遍历队列中的wait,调用wait的func来唤醒private

        所以用法为

        task 1

            DECLARE_WAITQUEUE(wait, current);

            add_wait_queue(w_wait, wait);

            __set_current_state(TASK_INTERRUPTIBLE);

            schedule();

            //task1 进程休眠

            ................//唤醒后从这儿执行

        task 2 //唤醒task1

            wake_up(w_wait);
3.4、异步套接字基础--select函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET使用说明

        用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供select函数来实现多路复用输入/输出模型。原型:  

点击(此处)折叠或打开

  1. #include <sys/time.h>
  2. #include <unistd.h>
  3. intselect(int maxfd,fd_set*rdset,fd_set*wrset,fd_set*exset,struct timeval*timeout)
        返回对应位仍然为1的fd的总数。
        maxfd:是需要监视的最大的文件描述符值+1;
        rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
        struct   timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
        fd_set(它比较重要所以先介绍一下)是一组文件描述字(fd)的集合,它用一位来表示一个fd,对于fd_set类型通过下面四个宏来操作:
        FD_ZERO,FD_SET,FD_CLR,FD_ISSET:
        FD_ZERO(fd_set   *fdset):将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。   
        FD_SET(fd_set   *fdset):用于在文件描述符集合中增加一个新的文件描述符。   
        FD_CLR(fd_set   *fdset):用于在文件描述符集合中删除一个文件描述符。   
        FD_ISSET(int   fd,fd_set   *fdset):用于测试指定的文件描述符是否在该集合中。
        过去,一个fd_set通常只能包含 < 32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd,现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,这样就能表示 < 1024的fd。根据fd_set的位矢量实现,我们可以重新理解操作fd_set的四个宏: 
        fd_set set;
        FD_ZERO(&set);      
        FD_SET(0, &set);    
        FD_CLR(4, &set);      
        FD_ISSET(5, &set);   
        注意fd的最大值必须<FD_SETSIZE。
        struct   timeval结构:   
        struct   timeval{   
            long   tv_sec;//second   
            long   tv_usec;//minisecond   
        }   
        timeout:
        用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。 
        null:select将一直被阻塞,直到某个文件描述符上发生了事件。   
        0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。   
        特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

        三组fd_set均将某些fd位置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。

        举个例子,比如recv()在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,你的线程就要阻塞很久。这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了)    
        步骤如下:
        socket    s;    
        .....    
        fd_set    set;    
        while(1)    
        {        
            FD_ZERO(&set);                //将你的套节字集合清空    
           FD_SET(s,    &set);            //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s    
               select(0,&set,NULL,NULL,NULL);        //检查套节字是否可读,    
                                                                             //很多情况下就是是否有数据(注意,只是说很多情况)    
                                                                             //这里select是否出错没有写    
           if(FD_ISSET(s,  &set)    //检查s是否在这个集合里面,    
           {                                    //select将更新这个集合,把其中不可读的套节字去掉    
                                                   //只保留符合条件的套节字在这个集合里面                           
               recv(s,...);    
           }    
           //do    something    here    

        理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
        1、执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
        2、若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)。
        3、若再加入fd=2,fd=1,则set变为0001,0011。
        4、执行select(6,&set,0,0,0)阻塞等待。
        5、若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

四、异步通知

        通过使用异步通知,应用程序可以在数据可用时收到一个信号,而不需要不停地使用轮询来关注数据。

        int fasync_helper(struct inode *inode, struct file *filp, int mode, struct fasync_struct **fa);

        用来实现fasync设备方法的辅助函数。mode参数取传入该方法的同一值,而fa指向设备专有的fasync_struct *。

        在数据到达时,可使用kill_fasync通知所有的相关进程。它的参数包括要发送的信号(通常是SIGIO)和带宽(band),后者几乎总是

POLL_IN。

        void kill_fasync(struct fasync_struct *fa, int sig, int band);

        如果驱动程序支持异步通知,则这个函数可以用来发送一个信号给注册的fa中的进程。

        当文件关闭时必须调用fasync方法,以便从活动的异步读取进程列表中删除该文件。

五、定位设备

        如果设备操作未定义llseek方法,内核默认通过修改filp->f_pos而执行定位,filp->f_pos是文件的当前读取/写入位置。

        因为默认方法是允许定位的,如果不需要定位,我们应该在我们的open方法中调用nonseekable_open,以便通知内核

设备不支持llseek。

        为了完整,还应该将file_operations结构中的llseek方法设置为特殊的辅助函数no_llseek,该函数定义在<linux/fs.h>中。


0 0
原创粉丝点击