精华内容
下载资源
问答
  • java的双缓冲技术

    千次阅读 2016-03-19 23:54:17
    Java的强大特性让其在游戏编程和多媒体动画处理方面也毫不逊色。在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。本文从J2SE的一个再现了屏幕闪烁的Java ...双缓冲是计算机动画处理中的传统技术,在

    Java的强大特性让其在游戏编程和多媒体动画处理方面也毫不逊色。在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。本文从J2SE的一个再现了屏幕闪烁的Java Appilication简单动画实例展开,对屏幕闪烁的原因进行了分析,找出了闪烁成因的关键:update(Graphics g)函数对于前端屏幕的清屏。由此引出消除闪烁的方法——双缓冲。双缓冲是计算机动画处理中的传统技术,在用其他语言编程时也可以实现。本文从实例出发,着重介绍了用双缓冲消除闪烁的原理以及双缓冲在Java中的两种常用实现方法(即在update(Graphics g)中实现和在paint(Graphics g)中实现),以期读者能对双缓冲在Java编程中的应用能有个较全面的认识。

    一、问题的引入

    在编写Java多媒体动画程序或用Java编写游戏程序的时候,我们得到的动画往往存在严重的闪烁(或图片断裂)。这种闪烁虽然不会给程序的效果造成太大的影响,但着实有违我们的设计初衷,也给程序的使用者造成了些许不便。闪烁到底是什么样的呢?下面的JavaApplication再现了这种屏幕闪烁的情况:

    代码段一,闪烁再现

    [java]  view plain  copy
    1. import java.awt.*;  
    2. import java.awt.event.*;  
    3. public class DoubleBuffer extends Frame//主类继承Frame类  
    4. {  
    5.     public paintThread pT;//绘图线程  
    6.     public int ypos=-80//小圆左上角的纵坐标  
    7.     public DoubleBuffer()//构造函数  
    8.     {  
    9.        pT=new paintThread(this);  
    10.        this.setResizable(false);  
    11.        this.setSize(300,300); //设置窗口的首选大小  
    12.        this.setVisible(true); //显示窗口  
    13.        pT.start();//绘图线程启动  
    14.     }  
    15.    public void paint(Graphics scr) //重载绘图函数  
    16.    {  
    17.        scr.setColor(Color.RED);//设置小圆颜色  
    18.        scr.fillOval(90,ypos,80,80); //绘制小圆  
    19.     }  
    20.     public static void main(String[] args)  
    21.     {  
    22.        DoubleBuffer DB=new DoubleBuffer();//创建主类的对象  
    23.        DB.addWindowListener(new WindowAdapter()//添加窗口关闭处理函数  
    24.        {  
    25.            public void windowClosing(WindowEvent e)  
    26.            {  
    27.               System.exit(0);  
    28.            }});  
    29.     }  
    30. }  
    31. class paintThread extends Thread//绘图线程类  
    32. {  
    33.     DoubleBuffer DB;  
    34.        public paintThread(DoubleBuffer DB) //构造函数  
    35.        {  
    36.            this.DB=DB;  
    37.        }  
    38.        public void run()//重载run()函数  
    39.        {  
    40.            while(true)//线程中的无限循环  
    41.            {  
    42.               try{  
    43.                   sleep(30); //线程休眠30ms  
    44.                   }catch(InterruptedException e){}  
    45.               DB.ypos+=5//修改小圆左上角的纵坐标  
    46.               if(DB.ypos>300//小圆离开窗口后重设左上角的纵坐标  
    47.                   DB.ypos=-80;  
    48.               DB.repaint();//窗口重绘  
    49.            }  
    50.        }  
    51. }  

    编译、运行上述例子程序后,我们会看到窗体中有一个从上至下匀速运动的小圆,但仔细观察,你会发现小圆会不时地被白色的不规则横纹隔开,即所谓的屏幕闪烁,这不是我们预期的结果。


    这种闪烁是如何出现的呢?

    首先我们分析一下这段代码。DoubleBuffer的对象建立后,显示窗口,程序首先自动调用重载后的paint(Graphics g)函数,在窗口上绘制了一个小圆,绘图线程启动后,该线程每隔30ms修改一下小圆的位置,然后调用repaint()函数。

    注意,这个repaint()函数并不是我们重载的,而是从Frame类继承而来的。它先调用update(Graphics g)函数,update(Graphics g)再调用paint(Graphics g)函数。问题就出在update(Graphics g)函数,我们来看看这个函数的源代码:

    [java]  view plain  copy
    1. public void update(Graphics g)  
    2. {  
    3. if (isShowing())  
    4. {  
    5.         if (! (peer instanceof LightweightPeer))  
    6. {  
    7.              g.clearRect(00, width, height);  
    8.          }  
    9.         paint(g);  
    10.     }  
    11. }  

    以上代码的意思是:(如果该组件是轻量组件的话)先用背景色覆盖整个组件,然后再调用paint(Graphics g)函数,重新绘制小圆。这样,我们每次看到的都是一个在新的位置绘制的小圆,前面的小圆都被背景色覆盖掉了。这就像一帧一帧的画面匀速地切换,以此来实现动画的效果。

        但是,正是这种先用背景色覆盖组件再重绘图像的方式导致了闪烁。在两次看到不同位置小圆的中间时刻,总是存在一个在短时间内被绘制出来的空白画面(颜色取背景色)。但即使时间很短,如果重绘的面积较大的话花去的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。

        另外,用paint(Graphics g)函数在屏幕上直接绘图的时候,由于执行的语句比较多,程序不断地改变窗体中正在被绘制的图象,会造成绘制的缓慢,这也从一定程度上加剧了闪烁。

        就像以前课堂上老师用的旧式的幻灯机,放完一张胶片,老师会将它拿下去,这个时候屏幕上一片空白,直到放上第二张,中间时间间隔较长。当然,这不是在放动画,但上述闪烁的产生原因和这很类似。

    二、问题的解决

    知道了闪烁产生的原因,我们就有了更具针对性的解决闪烁的方案。已经知道update(Graphics g)是造成闪烁的主要原因,那么就从这里入手。

    1尝试这样重载update(Graphics g)函数(基于代码段一修改):


    [java]  view plain  copy
    1. public void update(Graphics scr)  
    2. {  
    3.     paint(scr);  
    4. }  

    以上代码在重绘小圆之前没有用背景色重绘整个画面,而是直接调用paint(Graphics g)函数,这就从根本上避免了上述的那幅空白画面。

        看看运行结果,闪烁果然消除了!但是更大的问题出现了,不同时刻绘制的小圆重叠在一起形成了一条线!这样的结果我们更不能接受了。为什么会这样呢?仔细分析一下,重载后的update(Graphics g)函数中没有了任何清屏的操作,每次重绘都是在先前已经绘制好的图象的基础上,当然会出现重叠的现象了。

    2)使用双缓冲:

    这是本文讨论的重点。所谓双缓冲,就是在内存中开辟一片区域,作为后台图象,程序对它进行更新、修改,绘制完成后再显示到屏幕上。  

    1、重载paint(Graphics g)实现双缓冲:

        这种方法要求我们将双缓冲的处理放在paint(Graphics g)函数中,那么具体该怎么实现呢?先看下面的代码(基于代码段一修改):

    DoubleBuffer类中添加如下两个私有成员:

    [java]  view plain  copy
    1. private Image iBuffer;  
    2. private Graphics gBuffer;  
    3. //重载paint(Graphics scr)函数:  
    4. public void paint(Graphics scr)  
    5. {  
    6.     if(iBuffer==null)  
    7.     {  
    8.        iBuffer=createImage(this.getSize().width,this.getSize().height);  
    9.        gBuffer=iBuffer.getGraphics();  
    10.     }  
    11.     gBuffer.setColor(getBackground());  
    12.     gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);  
    13.    gBuffer.setColor(Color.RED);  
    14.    gBuffer.fillOval(90,ypos,80,80);  
    15.     scr.drawImage(iBuffer,0,0,this);  
    16. }  

    分析上述代码:我们首先添加了两个成员变量iBuffergBuffer作为缓冲(这就是所谓的双缓冲名字的来历)。在paint(Graphics scr)函数中,首先检测如果iBuffernull,则创建一个和屏幕上的绘图区域大小一样的缓冲图象,再取得iBufferGraphics类型的对象的引用,并将其赋值给gBuffer,然后对gBuffer这个内存中的后台图象先用fillRect(int,int,int,int)清屏,再进行绘制操作,完成后将iBuffer直接绘制到屏幕上。

        这段代码看似可以完美地完成双缓冲,但是,运行之后我们看到的还是严重的闪烁!为什么呢?回想上文所讨论的,问题还是出现在update(Graphics g)函数!这段修改后的程序中的update(Graphics g)函数还是我们从父类继承的。在update(Graphics g)中,clearRect(int,int,int,int)对前端屏幕进行了清屏操作,而在paint(Graphics g)中,对后台图象又进行了清屏操作。那么如果保留后台清屏,去掉多余的前台清屏应该就会消除闪烁。所以,我们只要按照(1)中的方法重载update(Graphics g)即可:


    [java]  view plain  copy
    1. public void update(Graphics scr)  
    2. {  
    3.     paint(scr);  
    4. }  

    这样就避开了对前端图象的清屏操作,避免了屏幕的闪烁。虽然和(1)中用一样的方法重载update(Graphics g),但(1)中没有了清屏操作,消除闪烁的同时严重破坏了动画效果,这里我们把清屏操作放在了后台图象上,消除了闪烁的同时也获得了预期的动画效果。

    2、重载update(Graphics g)实现双缓冲:

        这是比较传统的做法。也是实际开发中比较常用的做法。我们看看实现这种方法的代码(基于代码段一修改):

    DoubleBuffer 类中添加如下两个私有成员:

    [java]  view plain  copy
    1. private Image iBuffer;  
    2. private Graphics gBuffer;  
    3. //重载paint(Graphics scr)函数:  
    4. public void paint(Graphics scr)  
    5. {  
    6.     scr.setColor(Color.RED);  
    7.     scr.fillOval(90,ypos,80,80);  
    8. }  

    重载update(Graphics scr)函数:


    [java]  view plain  copy
    1. public void update(Graphics scr)  
    2. {  
    3.     if(iBuffer==null)  
    4.     {  
    5.        iBuffer=createImage(this.getSize().width,this.getSize().height);  
    6.        gBuffer=iBuffer.getGraphics();  
    7.     }  
    8.        gBuffer.setColor(getBackground());  
    9.        gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);  
    10.        paint(gBuffer);  
    11.        scr.drawImage(iBuffer,0,0,this);  
    12. }  

    分析上述代码:我们把对后台图象的创建、清屏以及重绘等一系列动作都放在了update(Graphicsscr)函数中,而paint(Graphics g)函数只是负责绘制什么样的图象,以及怎样绘图,函数的最后实现了后台图象向前台绘制的过程。

    运行上述修改后的程序,我们会看到完美的消除闪烁后的动画效果。就像在电影院看电影,每张胶片都是在后台准备好的,播放完一张胶片之后,下一张很快就被播放到前台,自然不会出现闪烁的情形。

        为了让读者能对双缓冲有个全面的认识现将上述双缓冲的实现概括如下:

    1)定义一个Graphics对象gBuffer和一个Image对象iBuffer。按屏幕大小建立一个缓冲对象给iBuffer。然后取得iBufferGraphics赋给gBuffer。此处可以把gBuffer理解为逻辑上的缓冲屏幕,而把iBuffer理解为缓冲屏幕上的图象。

    2gBuffer(逻辑上的屏幕)上用paint(Graphics g)函数绘制图象。

    3将后台图象iBuffer绘制到前台。

    以上就是一次双缓冲的过程。注意,将这个过程联系起来的是repaint()函数。paint(Graphics g)是一个系统调用语句,不能由程序员手工调用。只能通过repaint()函数调用。

    三、问题的扩展

    1、关于闪烁的补充:

    其实引起闪烁的不仅仅是上文提到的那样,多种物理因素也可以引起闪烁,无论是CRT显示器还是LCD显示器都存在闪烁的现象。本文只讨论软件编程引起的闪烁。但是即使双缓冲做得再好,有时也是会有闪烁,这就是硬件方面的原因了,我们只能修改程序中的相关参数来降低闪烁(比如让画面动得慢一点),而不是编程方法的问题。

    2、关于消除闪烁的方法的补充:

        上文提到的双缓冲的实现方法只是消除闪烁的方法中的一种。如果在swing中,组件本身就提供了双缓冲的功能,我们只需要进行简单的函数调用就可以实现组件的双缓冲,在awt中却没有提供此功能。另外,一些硬件设备也可以实现双缓冲,每次都是先把图象画在缓冲中,然后再绘制在屏幕上,而不是直接绘制在屏幕上,基本原理还是和文中的类似的。还有其他用软件实现消除闪烁的方法,但双缓冲是个简单的、值得推荐的方法。

    2、关于双缓冲的补充:

    双缓冲技术是编写J2ME游戏的关键技术之一。双缓冲付出的代价是较大的额外内存消耗。但现在节省内存已经不再是程序员们考虑的最首要的问题了,游戏的画面在游戏制作中是至关重要的,所以以额外的内存消耗换取程序质量的提高还是值得肯定的。

    3、双缓冲的改进:

    有时动画中相邻的两幅画面只是有很少部分的不同,这就没必要每次都对整个绘图区进行清屏。我们可以对文中的程序进行修改,使之每次只对部分屏幕清屏,这样既能节省内存,又能减少绘制图象的时间,使动画更加连贯!
    展开全文
  • Linux文件系统中,存在着著名的三大缓冲技术用以提升读写操作效率:inode缓冲区、dentry缓冲区、块缓冲。其中所谓的块缓冲便是我们前面一直在讨论的缓冲池技术,常用来配备IO操作,用来减少IO读取次数,以提升系统...

    0. Linux下缓冲池技术的简单介绍

    Linux文件系统中,存在着著名的三大缓冲技术用以提升读写操作效率: inode缓冲区、dentry缓冲区、块缓冲。其中所谓的块缓冲便是我们前面一直在讨论的缓冲池技术,常用来配备IO操作,用来减少IO读取次数,以提升系统效率。

    本文便是此前《换成技术》系列的两篇文章的基础上继续讨论缓冲池技术。对于块缓冲体系而言,需要提及的两个概念分别是page cache和buffer cache,每个page cache包含若干buffer cache,即两者的粒度不同,page是在buffer缓冲块基础上封装出的更粗粒度的对象。

    内存管理系统和VFS(虚拟文件系统)只与page cache这一粗粒度级别的缓冲对象进行交互,内存管理系统负责维护page cache的分配和回收(如按照LRU策略进行淘汰)。在内核需要读写数据时,使用“内存映射”等复杂机制进可以和物理内存块进行正确的映射。而具体文件系统一般只与buffer cache这一更小粒度级别的缓冲对象交互,它们负责在存储设备和buffer cache之间交换数据,具体的文件系统直接操作的是磁盘等disk部分,而VFS则负责将数个buffer cache包装成page cache提供给用户。

    对于具体的Linux文件系统,磁盘等外设存储设备会以磁盘块为单位分配给文件用以存储,所以buffer cache大小正好对应着磁盘块block的大小。引入缓冲区的目的主要还是为了降低对文件存储的外部设备的IO操作次数。每个缓冲区由两个部分组成,第一部分称为缓冲区首部,用数据结果buffer_head表示,而第二部分是真正的存储的数据。(这里可以参考我的关于MiniCRT自定义简化版运行库中提供的堆管理

    typedef struct _heap_header
    {
        enum{
            HEAP_BLOCK_FREE = 0xABABABAB, //空闲块的魔数
            HEAP_BLOCK_USED = 0xCDCDCDCD, //占用块的魔数
        }type;
    
        unsigned size;  //当前块的尺寸,该size包括块的信息头的尺寸
        struct _heap_header* next;
        struct _heap_header* prev;
    }heap_header;

    1. Linux系统下IO操作使用Buffer缓冲块的过程

    首先给出page cache的定义

    typedef struct page {
        struct list_head list; //mapping has some page list
        struct address_space *mapping; //the inode we belong to 
        unsigned long index;   //our offset within mapping
        struct page *next_hash; //Next page sharing our hash bucket in the pagecache hash table
        //和为了快速管理buffer采用hash table一样,管理page同样擦用了hash table,这个next_hash表示和该
        //page的hash-key一样的下一个page的指针
    
        atomic_t count; //线程或进程使用计数,在该计数为0时意味着该page已经可以被清除了
        unsigned long flags;  //原子锁标志,有可能存在更新不同步的情况
        struct list_head lru;  //pageout list, eg. active_list; protected by pagemap_lru_lock !
    
        struct page *pprev_hash; //complement to next_hash和next_hash相对应的指向前面page的指针
        struct buffer_head *buffers; //buffer maps us to a disk block;
        /******
        *On machines where all RAM is mapped into kernel address space, we can simply 
        calculate the virtual address. On machines with highmem some memory  is mapped into 
        kernel virtual memory dynamically.So we need a place to store that address. Note 
        that this field could be 16 bits on x86...Architectures with slow multiplication can 
        define WANT_PAGE_VIRTUAL in asm/page.h
        *********/
    #if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)
        void *virtual;
    #endif
    } mem_map_t;

    再给出buffer cache的定义形式,其中首先给出buffer_head结构体的定义内容

    struct buffer_head {
        struct buffer_head *b_next; //有效buffer是通过哈希表进行管理的,但哈希表中可能因为(block, dev)组合被映射到同一个key下,
                                //所以提供这个同一key下的buffer_head*链表指针,用以二次遍历锁定准确的buffer
        unsigned long b_blocknr; //block number 该buffer映射的磁盘块块号
        unsigned short b_size;    //block size 该buffer映射的磁盘块内容大小
        unsigned short b_list; //
        kdev_t  b_dev; //该buffer映射的磁盘块隶属的虚拟设备标示号
    
        atomic_t b_count; //缓冲区读写使用计数,如果为0,意味着该缓冲区内的内容已经没有线程声明要使用了,意味着该Buffer是可以被释放进入空闲队列了
        kdev_t  b_rdev;   //真实设备标识
        unsigned long b_state; //buffer状态标记位,各位对应不同的含义
        unsigned long b_flushtime; //延迟写的上限时间
    
        struct buffer_head *b_next_free; // lru.free list linkage 指向lru空闲链表中next元素
        struct buffer_head *b_prev_free; //doubly linked list of buffers 指向lru空闲链表中prev元素
        struct buffer_head *b_this_page; //circular list of buffers in one page若该buffer被使用,则该参数指向同一个page的buffer链表
        struct buffer_head *b_reqnext;   //request queue
    
        struct buffer_head **b_pprev; //doubly linked list of hash-queue hash队列双向链表
        char *b_data; //pointer to data block 指向数据块的指针
        struct page *b_page;  //the page this bh is mapped to 这个buffer映射的页面
        void (*b_end_io)(struct buffer_head *bh, int uptodate); //IO completion IO结束时的执行函数_end
        void *b_private;  //reserved for b_end_io 为IO结尾函数_end保留位
    
        unsigned long b_rsector;  //real buffer location on disk 缓冲区在磁盘上的实际位置
        wait_queue_head_t b_wait;  
        struct list_head  b_inode_buffers; //doubly linked list of 
    
        inode dirty buffers//iNode脏缓冲区循环链表
    
    };

    在buffer_head结构体中提到其状态标识位是由unsigned long b_state;来表示的,下面来进一步分析该状态参数各位的意义。

    enum bh_state_bits
    {
        BH_Uptodate, //譬如0x0001,如果缓冲区存在有效数据则置为1,否则为0x0000
        BH_Dirty, //0x0010,如果buffer脏了即数据被修改了,则置该位
        BH_Lock, //如果该缓冲区被锁定了,即存在某一进程或线程正在使用该buffer,则置该位
        BH_Req, //如果缓冲区无效了,则置为该位
        BH_Mapped, //如果缓冲区有一个磁盘映射置该位
        BH_New, //如果该缓冲区是fresh,新分配加入缓冲池的,并且还没有被使用,则置该位;
        BH_Async, //如果缓冲区是进行end_buffer_io_async IO同步则置该位
        BH_Wait_IO, //如果要将这个buffer写回到映射的磁盘中,则置该位
        BH_Launder, //需要重置该buffer,置该位
        BH_Attached, //if b_indoe_buffers is linked into a list则置该位
        BH_JBD, //如果和journal_head 关联置1
        BH_Sync, //如果buffer是同步读取置该位
        BH_Delay, //如果buffer空间是延迟分配置该位
        BH_PrivateStart, //not a state bit, but the first bit available for private allocation by other entities
    };

    从buffer_head和page结构体可以看出,操作系统为了快速定位具体的缓冲块,采用了hash-table进行管理,故而这里介绍下Linux操作系统中关于Buffer缓冲块的hash方式

    /*关于VFS如何管理这几个buffer cache的链表
    *1.其中关于存储着有效数据的buffer,是通过hash表管理的,key值是由数据块号+所在设备标识号计算得到
    */
    
    #define _hashfn (dev, block)       \
            ( (  ((dev) << (bh_hash_shift - 6)) ^ ((dev) << (bh_hash_shift - 9)) ) ^ \
                ( ((block) << (bh_hash_shift - 6)) ^ ((block) >> 13) ^ \
                  ((block) << (bh_hash_shift - 12)) \
                 ) \
            )

    介绍了诸多基础的东西,下面看下Linux下如何具体定位一个Buffer缓冲块的函数bread()。其根据虚拟设备号、缓冲块号以及容量参数进行具体定位。

    //在具体的文件系统中读取具体一块数据时,调用bread函数
    struct buffer_head * bread(kdev_t dev, int block, int size)
    {
        struct  buffer_head * bh;
    
        bh = getblk(dev, block, size);  //根据设备号、块号、和要读取的字节数目返回相应的buffer
    
        if ( buffer_uptodate(bh) ) //判断是否存在有效数据,如果存在那么直接返回即可
            return bh;
    
        set_bit(BH_Sync, &bh->b_state);  //如果不存在有效数据,将这个buffer设置为同步状态
        ll_rw_block(READ, l, &bh);  //如果没有有效数据,则需要现场从磁盘中将相应块号的内容读取到buffer中,这个是一个操作系统底层的操作
    
        wait_on_buffer(bh); //等待buffer的锁打开
        if ( buffer_uptodate(bh) )
            return bh;
        brelse(bh);
        return NULL;
    };

    getblk()函数的具体实现,其根据相应参数返回具体的缓冲块首地址

    struct buffer_head * getblk(kdev_t dev, int block, int size)
    {
        for (;;)
        {
            struct buffer_head * bh;
            bh = get_hash_table(dev, block, size); //关键函数,得到hash表中的buffer
            if (bh) {
                touch_buffer(bh);
                return bh; //返回这个buffer
            }
    
            //如果没有找到对应的buffer,那么试着去增加一个buffer,就是使用下面的grow_buffer函数
            if (!grow_buffers(dev, block, size))  //即调用该函数返回一个足够空间的fresh buffer,用以提供给后面从磁盘读取目标块的内容的缓冲区
                free_more_memory();//如果空间不足,则只能从LRU队列中选出buffer,先看是否已“脏”,若是,则写回磁盘,并清空内容,分配给新的数据块
        }
    };
    #define hash(dev, block) hash_table[ ( _hashfn(HASHDEV(dev), block) & bh_hash_mask ) ]
    #define get_bh(bh)  atomic_inc( &(bh)->b_count )
    
    struct  buffer_head * get_hash_table( kdev_t dev,  int block, int size)
    {
        struct buffer_head *bh;
        struct buffer_head **p = &hash(dev, block); //通过hash表查找到对应的 buffer
    
        read_lock (&hash_table_lock); 
    
        //判断得到的buffer数组中有没有我们需要的buffer
        for(;;) 
        { 
            bh = *p;
            if (!bh)
                break;
    
            p = &bh->b_next;
            if (bh->b_blocknr != block)
                continue;
            if (bh->b_size != size)
                continue;
            if (bh->b_dev != dev)
                continue;
            get_bh(bh); //如果有那么直接执行这个函数,这个函数其实已经通过宏给出
            break;
        }
    
        read_unlock(&hash_table_lock);
        return bh;
    };

    如果缓冲池不够用,则试着增加新的缓冲块,该操作便是通过grow_buffers()函数实现的。

    
    //如果没找到对应的buffer,那么使用grow_buffer函数增加一个新的buffer,该buffer的状态标记为BH_New
    // try to increase the number of buffers available: the size argument is used to determine
    //what kind of buffers we want
    stastic int grow_buffers (kdev_t dev, unsigned long block, int size)
    {
        struct page* page;
        struct block_device *bdev;
        unsigned long index;
        int sizebits;
    
        /**size must be multiple of hard sectorsize 给出的size必须是硬件扇区的整数倍*/
        if (size & (get_hardsect_size(dev) - 1) )
            BUG();
        if (size < 512 || size > PAGE_SIZE )
            BUG();
        //新加入的缓冲块大小必须在512到PAGE_SIZE
    
        sizebits = -1;
        do 
        {
            sizebits++;
        } while ((size << sizebits) < PAGE_SIZE);
    
        index = block >> sizebits;
        block = index << sizebits;
    
        bdev = bdget( kdev_t_to_nr(dev) );
        if (!bdev)
        {
            printfk("No block device for %s\n", kdevname(dev));
            BUG();
        }
    
        /*即根据需求的size新开辟一个缓冲页Page*/
        page = grow_dev_page( bdev, index, size );
    
        atominc_dec( &bdev->bd_count);
        if (!page)
            return 0;
    
        /*Hash in the buffers on the hash list*/
        hash_page_buffers( page, dev, block, size);
        UnlockPage( page );
        page_cache_release( page );
    
        /* we hashed up this page, so increment buffermem*/
        atomic_inc( &buffermem_pages );
        return 1;
    }

    前面说到我们是通过hash-table来管理已经被填入有效数据的缓冲区buffer的,但是其实缓冲区类型是由多种的

    #define BUF_CLEAN  0
    #define BUF_LOCKED 1 //正在等待被写回的uffer: Buffers scheduled for write
    #define BUF_DIRTY  2 //脏buffer,但还没有被安排写回,即延迟写策略不满足
    #define NR_DIRTY   3

    而事实上,缓冲池中更新策略一个重要的概念便是LRU(least recently used最近最少使用)。而缓冲池的具体实现中除了empty\input\output三种队列,还有便是LRU队列控制的淘汰缓冲队列以及hash-table提供的快速索引。

    综合来说,Linux系统为IO读取操作配备的buffer缓冲池是这样起作用的:

    1. 首先在Hash-table中寻找目标buffer,如果找到了该buffer,则直接返回该buffer的buffer_head指针,如果没有,那么意味着要读取的内容并不在buffer缓冲池中,故而要为这份新的数据内容分配一块新的匹配size的buffer;

    2. 先从内存中再直接划分出一块区域作为新添加的缓冲块,加入缓冲池体系中,供应本次使用;

    3. 如果内存空间不足或者已经达到缓冲池规模上限,则开始从LRU队列中取出链首元素,先看是否脏了,如果脏了,则先回写,然后清空内容,将它分配给新的数据块。

    展开全文
  • Java双缓冲技术

    万次阅读 多人点赞 2012-10-28 10:38:52
    Java的强大特性让其在游戏编程和多媒体动画处理方面也毫不逊色。在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。本文从J2SE的一个再现了屏幕闪烁的Java ...双缓冲是计算机动画处理中的传统技术,在

    Java的强大特性让其在游戏编程和多媒体动画处理方面也毫不逊色。在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。本文从J2SE的一个再现了屏幕闪烁的Java Appilication简单动画实例展开,对屏幕闪烁的原因进行了分析,找出了闪烁成因的关键:update(Graphics g)函数对于前端屏幕的清屏。由此引出消除闪烁的方法——双缓冲。双缓冲是计算机动画处理中的传统技术,在用其他语言编程时也可以实现。本文从实例出发,着重介绍了用双缓冲消除闪烁的原理以及双缓冲在Java中的两种常用实现方法(即在update(Graphics g)中实现和在paint(Graphics g)中实现),以期读者能对双缓冲在Java编程中的应用能有个较全面的认识。

    一、问题的引入

    在编写Java多媒体动画程序或用Java编写游戏程序的时候,我们得到的动画往往存在严重的闪烁(或图片断裂)。这种闪烁虽然不会给程序的效果造成太大的影响,但着实有违我们的设计初衷,也给程序的使用者造成了些许不便。闪烁到底是什么样的呢?下面的JavaApplication再现了这种屏幕闪烁的情况:

    代码段一,闪烁再现

    import java.awt.*;
    import java.awt.event.*;
    public class DoubleBuffer extends Frame//主类继承Frame类
    {
        public paintThread pT;//绘图线程
        public int ypos=-80; //小圆左上角的纵坐标
        public DoubleBuffer()//构造函数
        {
           pT=new paintThread(this);
           this.setResizable(false);
           this.setSize(300,300); //设置窗口的首选大小
           this.setVisible(true); //显示窗口
           pT.start();//绘图线程启动
        }
       public void paint(Graphics scr) //重载绘图函数
       {
           scr.setColor(Color.RED);//设置小圆颜色
           scr.fillOval(90,ypos,80,80); //绘制小圆
        }
        public static void main(String[] args)
        {
           DoubleBuffer DB=new DoubleBuffer();//创建主类的对象
           DB.addWindowListener(new WindowAdapter()//添加窗口关闭处理函数
           {
               public void windowClosing(WindowEvent e)
               {
                  System.exit(0);
               }});
        }
    }
    class paintThread extends Thread//绘图线程类
    {
        DoubleBuffer DB;
           public paintThread(DoubleBuffer DB) //构造函数
           {
               this.DB=DB;
           }
           public void run()//重载run()函数
           {
               while(true)//线程中的无限循环
               {
                  try{
                      sleep(30); //线程休眠30ms
                      }catch(InterruptedException e){}
                  DB.ypos+=5; //修改小圆左上角的纵坐标
                  if(DB.ypos>300) //小圆离开窗口后重设左上角的纵坐标
                      DB.ypos=-80;
                  DB.repaint();//窗口重绘
               }
           }
    }
    

    编译、运行上述例子程序后,我们会看到窗体中有一个从上至下匀速运动的小圆,但仔细观察,你会发现小圆会不时地被白色的不规则横纹隔开,即所谓的屏幕闪烁,这不是我们预期的结果。


    这种闪烁是如何出现的呢?

    首先我们分析一下这段代码。DoubleBuffer的对象建立后,显示窗口,程序首先自动调用重载后的paint(Graphics g)函数,在窗口上绘制了一个小圆,绘图线程启动后,该线程每隔30ms修改一下小圆的位置,然后调用repaint()函数。

    注意,这个repaint()函数并不是我们重载的,而是从Frame类继承而来的。它先调用update(Graphics g)函数,update(Graphics g)再调用paint(Graphics g)函数。问题就出在update(Graphics g)函数,我们来看看这个函数的源代码:

    public void update(Graphics g)
    {
    if (isShowing())
    {
            if (! (peer instanceof LightweightPeer))
    {
                 g.clearRect(0, 0, width, height);
             }
            paint(g);
        }
    }
    

    以上代码的意思是:(如果该组件是轻量组件的话)先用背景色覆盖整个组件,然后再调用paint(Graphics g)函数,重新绘制小圆。这样,我们每次看到的都是一个在新的位置绘制的小圆,前面的小圆都被背景色覆盖掉了。这就像一帧一帧的画面匀速地切换,以此来实现动画的效果。

        但是,正是这种先用背景色覆盖组件再重绘图像的方式导致了闪烁。在两次看到不同位置小圆的中间时刻,总是存在一个在短时间内被绘制出来的空白画面(颜色取背景色)。但即使时间很短,如果重绘的面积较大的话花去的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。

        另外,用paint(Graphics g)函数在屏幕上直接绘图的时候,由于执行的语句比较多,程序不断地改变窗体中正在被绘制的图象,会造成绘制的缓慢,这也从一定程度上加剧了闪烁。

        就像以前课堂上老师用的旧式的幻灯机,放完一张胶片,老师会将它拿下去,这个时候屏幕上一片空白,直到放上第二张,中间时间间隔较长。当然,这不是在放动画,但上述闪烁的产生原因和这很类似。

    二、问题的解决

    知道了闪烁产生的原因,我们就有了更具针对性的解决闪烁的方案。已经知道update(Graphics g)是造成闪烁的主要原因,那么就从这里入手。

    1尝试这样重载update(Graphics g)函数(基于代码段一修改):


    public void update(Graphics scr)
    {
        paint(scr);
    }
    

    以上代码在重绘小圆之前没有用背景色重绘整个画面,而是直接调用paint(Graphics g)函数,这就从根本上避免了上述的那幅空白画面。

        看看运行结果,闪烁果然消除了!但是更大的问题出现了,不同时刻绘制的小圆重叠在一起形成了一条线!这样的结果我们更不能接受了。为什么会这样呢?仔细分析一下,重载后的update(Graphics g)函数中没有了任何清屏的操作,每次重绘都是在先前已经绘制好的图象的基础上,当然会出现重叠的现象了。

    2)使用双缓冲:

    这是本文讨论的重点。所谓双缓冲,就是在内存中开辟一片区域,作为后台图象,程序对它进行更新、修改,绘制完成后再显示到屏幕上。  

    1、重载paint(Graphics g)实现双缓冲:

        这种方法要求我们将双缓冲的处理放在paint(Graphics g)函数中,那么具体该怎么实现呢?先看下面的代码(基于代码段一修改):

    DoubleBuffer类中添加如下两个私有成员:

    private Image iBuffer;
    private Graphics gBuffer;
    //重载paint(Graphics scr)函数:
    public void paint(Graphics scr)
    {
        if(iBuffer==null)
        {
           iBuffer=createImage(this.getSize().width,this.getSize().height);
           gBuffer=iBuffer.getGraphics();
        }
        gBuffer.setColor(getBackground());
        gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
       gBuffer.setColor(Color.RED);
       gBuffer.fillOval(90,ypos,80,80);
        scr.drawImage(iBuffer,0,0,this);
    }
    

    分析上述代码:我们首先添加了两个成员变量iBuffergBuffer作为缓冲(这就是所谓的双缓冲名字的来历)。在paint(Graphics scr)函数中,首先检测如果iBuffernull,则创建一个和屏幕上的绘图区域大小一样的缓冲图象,再取得iBufferGraphics类型的对象的引用,并将其赋值给gBuffer,然后对gBuffer这个内存中的后台图象先用fillRect(int,int,int,int)清屏,再进行绘制操作,完成后将iBuffer直接绘制到屏幕上。

        这段代码看似可以完美地完成双缓冲,但是,运行之后我们看到的还是严重的闪烁!为什么呢?回想上文所讨论的,问题还是出现在update(Graphics g)函数!这段修改后的程序中的update(Graphics g)函数还是我们从父类继承的。在update(Graphics g)中,clearRect(int,int,int,int)对前端屏幕进行了清屏操作,而在paint(Graphics g)中,对后台图象又进行了清屏操作。那么如果保留后台清屏,去掉多余的前台清屏应该就会消除闪烁。所以,我们只要按照(1)中的方法重载update(Graphics g)即可:


    public void update(Graphics scr)
    {
        paint(scr);
    }
    

    这样就避开了对前端图象的清屏操作,避免了屏幕的闪烁。虽然和(1)中用一样的方法重载update(Graphics g),但(1)中没有了清屏操作,消除闪烁的同时严重破坏了动画效果,这里我们把清屏操作放在了后台图象上,消除了闪烁的同时也获得了预期的动画效果。

    2、重载update(Graphics g)实现双缓冲:

        这是比较传统的做法。也是实际开发中比较常用的做法。我们看看实现这种方法的代码(基于代码段一修改):

    DoubleBuffer 类中添加如下两个私有成员:

    private Image iBuffer;
    private Graphics gBuffer;
    //重载paint(Graphics scr)函数:
    public void paint(Graphics scr)
    {
        scr.setColor(Color.RED);
        scr.fillOval(90,ypos,80,80);
    }
    

    重载update(Graphics scr)函数:


    public void update(Graphics scr)
    {
        if(iBuffer==null)
        {
           iBuffer=createImage(this.getSize().width,this.getSize().height);
           gBuffer=iBuffer.getGraphics();
        }
           gBuffer.setColor(getBackground());
           gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
           paint(gBuffer);
           scr.drawImage(iBuffer,0,0,this);
    }
    

    分析上述代码:我们把对后台图象的创建、清屏以及重绘等一系列动作都放在了update(Graphicsscr)函数中,而paint(Graphics g)函数只是负责绘制什么样的图象,以及怎样绘图,函数的最后实现了后台图象向前台绘制的过程。

    运行上述修改后的程序,我们会看到完美的消除闪烁后的动画效果。就像在电影院看电影,每张胶片都是在后台准备好的,播放完一张胶片之后,下一张很快就被播放到前台,自然不会出现闪烁的情形。

        为了让读者能对双缓冲有个全面的认识现将上述双缓冲的实现概括如下:

    1)定义一个Graphics对象gBuffer和一个Image对象iBuffer。按屏幕大小建立一个缓冲对象给iBuffer。然后取得iBufferGraphics赋给gBuffer。此处可以把gBuffer理解为逻辑上的缓冲屏幕,而把iBuffer理解为缓冲屏幕上的图象。

    2gBuffer(逻辑上的屏幕)上用paint(Graphics g)函数绘制图象。

    3将后台图象iBuffer绘制到前台。

    以上就是一次双缓冲的过程。注意,将这个过程联系起来的是repaint()函数。paint(Graphics g)是一个系统调用语句,不能由程序员手工调用。只能通过repaint()函数调用。

    三、问题的扩展

    1、关于闪烁的补充:

    其实引起闪烁的不仅仅是上文提到的那样,多种物理因素也可以引起闪烁,无论是CRT显示器还是LCD显示器都存在闪烁的现象。本文只讨论软件编程引起的闪烁。但是即使双缓冲做得再好,有时也是会有闪烁,这就是硬件方面的原因了,我们只能修改程序中的相关参数来降低闪烁(比如让画面动得慢一点),而不是编程方法的问题。

    2、关于消除闪烁的方法的补充:

        上文提到的双缓冲的实现方法只是消除闪烁的方法中的一种。如果在swing中,组件本身就提供了双缓冲的功能,我们只需要进行简单的函数调用就可以实现组件的双缓冲,在awt中却没有提供此功能。另外,一些硬件设备也可以实现双缓冲,每次都是先把图象画在缓冲中,然后再绘制在屏幕上,而不是直接绘制在屏幕上,基本原理还是和文中的类似的。还有其他用软件实现消除闪烁的方法,但双缓冲是个简单的、值得推荐的方法。

    2、关于双缓冲的补充:

    双缓冲技术是编写J2ME游戏的关键技术之一。双缓冲付出的代价是较大的额外内存消耗。但现在节省内存已经不再是程序员们考虑的最首要的问题了,游戏的画面在游戏制作中是至关重要的,所以以额外的内存消耗换取程序质量的提高还是值得肯定的。

    3、双缓冲的改进:

    有时动画中相邻的两幅画面只是有很少部分的不同,这就没必要每次都对整个绘图区进行清屏。我们可以对文中的程序进行修改,使之每次只对部分屏幕清屏,这样既能节省内存,又能减少绘制图象的时间,使动画更加连贯!




    展开全文
  • 本文将介绍用于备受青睐的反激式转换器的最常用的RCD箝位电路,及其设计公式。  没有缓冲器,反激式变压器振铃的漏感会随电路中的杂散电容产生,生产大幅度高频波形,如图所示1。  许多应用笔记和设计没有解决...
  • Java双缓冲技术详解

    千次阅读 2016-07-08 16:06:59
    Java双缓冲技术 返回 存档 删除 添加到收藏夹 分享 显示选项 Pocket 我的列表添加到收藏夹存档列表 文本视频图像标签 › Careers at PocketBecome a SponsorMore
    展开全文
  • 本文将介绍用于备受青睐的反激式转换器的最常用的RCD箝位电路,及其设计公式。  没有缓冲器,反激式变压器振铃的漏感会随电路中的杂散电容产生,生产大幅度高频波形,如图所示1。  许多应用笔记和设计没有解决这...
  • 引入缓冲技术的原因: 1、 为了进一步缓和CPU和I/O设备之间速度不匹配的矛盾。 2、 提高CPU与I/O设备之间的并行性。 3、 为了减少中断次数和CPU的中断处理时间。如果没有缓冲,慢速I/O设备每传一个字节就要产生一...
  • 文章尝试用尝试用通俗的语言还原密码学应用逻辑与信息传输加密方法,梳理常用加密技术原理及学科学习中的关键要点。
  • 复杂的开发测井工程图件因含有大量的图像元素,要经过大量的比例关系换算、坐标系的转换和复杂的坐标映射才能将像素在正确的位置显示出来。巨幅、需滚屏显示的测井...应用双缓冲技术,首先在内存中绘制整个测井曲线,
  • Java限流及常用解决方案总结

    千次阅读 2020-09-20 11:43:42
    说到限流,想必大家都不陌生,一个很简单的例子就是,在12306上面买票的时候,遇到某时刻开始抢票的时候,经常页面会弹出一个类似请稍后重试的提示,从后端的技术层面来看,大概有2层解释,第一是服务器担心扛不住...
  • 用双缓冲技术实现真正的平滑

    千次阅读 多人点赞 2013-09-19 17:01:36
    虽然游戏还在创作中(今天才开始,目前我正在思考ing,完成之后必将和各位分享),不过我还是想在写完这个游戏之前讲一个特别重要的技术——双缓冲技术   这个技术大家应该都不陌生,不过作为初学者的我,还是希望...
  • 当然,我们常用的PC都是支持双缓冲技术的。 要启动双缓冲功能,最简单的办法就是使用GLUT工具包。我们以前在main函数里面写: glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);  其中GLUT_SIN
  • 【Java基础-3】吃透Java IO:字节流、字符流、缓冲

    万次阅读 多人点赞 2020-09-23 20:12:33
    什么是Java-IO?字符流和字节流的区别与适用场景是什么?缓冲流到底实现了什么?如何高效地读写文件? 本文用大量的示例图和实例,带你吃透Java IO。
  • j2ee 常用开源技术

    千次阅读 2006-09-12 00:27:00
    J2EE开发之常用开源项目介绍 主要就我所了解的J2EE开发的框架或开源项目做个介绍,可以根据需求选用适当的开源组件进行开发.主要还是以Spring为核心,也总结了一些以前web开发常用的开源工具和开源类库 1持久层:1)...
  • 日志存储系统常用技术方案介绍

    千次阅读 2019-01-08 04:24:29
    日志存储系统常用技术方案有两种:一是log4j/logback+mongodb的方式,一种是基于ELK的日志存储系统。  日志一般存储在数据库和文件系统中。日志数据要和生产正式库分开存储,否则会影响正式库的运行,带来隐患。...
  • 缓存的基本概念和常用的缓存技术

    万次阅读 多人点赞 2018-04-04 11:15:22
    摘要: 介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页)1 概念1.1 缓存能解决的问题· 性能——将相应数据存储起来以避免数据...
  • 1 产品技术方案1.1 技术方案概述 1.1.1 系统功能架构   在银行电商平台中,包括各总分支行的管理人员、各家合作商户、会员、物流公司、支付平台等主要角色,通过电商平台进行信息流、资金流的交互,并借助物流...
  • 缓冲技术 在互联网服务中,大部分的用户交互,都是需要立刻返回结果的,所以对于延迟有一定的要求。而类似网络游戏之类服务,延迟更是要求缩短到几十毫秒以内。所以为了降低延迟,缓冲是互联网服务中最常见的技术...
  • 黑客中级技术--缓冲区溢出攻击(上)

    千次阅读 2004-11-27 10:22:00
    缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。利用缓冲区溢出攻击,可以导致程序运行失败、系统当机、重新启动等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得...
  • 缓冲与双缓冲的区别

    万次阅读 2011-11-13 13:07:06
    OpenGL单缓冲与双缓冲的区别(追加10分) 最佳答案 单缓冲,实际上就是将所有的绘图指令在窗口上执行,就是直接在窗口上绘图,这样的绘图效率是比较慢的,如果使用单缓冲,而电脑比较慢,你回到屏幕的...
  • 工作中git是一项必不可少的技能,在项目的开发进程中起着至关重要的作用,下面介绍一些git在工作中...本文介绍了Git是什么、Git的存储结构、Git的提交规则和一些工作中常会用到的git操作的过程,最后总结了常用的命令。
  • Cachelab 高速缓冲器模拟

    千次阅读 2019-01-14 17:54:31
    高速缓冲器模拟  专 业 计算机科学与技术 csim.c和trans.c代码见文章末尾 目 录 第1章 实验基本信息... - 3 - 1.1 实验目的... - 3 - 1.2 实验环境与工具... - 3 - 1.2.1 硬件环境... -...
  • 操作系统——缓冲区溢出

    千次阅读 2019-05-12 22:46:18
    一、缓冲区溢出介绍 1988年,世界上第一个缓冲区溢出攻击–Morris蠕虫在互联网上泛滥,短短一夜的时间全世界6000多台网络服务器瘫痪或半瘫痪,不计其数的数据和资料被毁。造成一场损失近亿美元的空前大劫难! 那么,...
  • 常用的音视频QoS保障技术

    千次阅读 2016-09-27 10:04:20
     以上技术是视频会议系统中常用的音视频QOS保障技术,不同的技术对于不同的网络状况效果不同,比如ARQ重传在小丢包和较低网络延迟的网络情况下能够达到较理想的效果,因此视频会议系统的音视频QOS的关键问题是如何...
  • 什么是缓冲区溢出?有说明危害?

    千次阅读 2019-04-19 20:40:04
     缓存溢出(或译为缓冲溢出)为黑客最为常用的攻击手段之-一,蠕虫病毒对操作系统高危漏洞的溢出高速与大规模传播均是利用此技术。  缓存溢出攻击从理论上来讲可以用于攻击任何有缺陷不完美的程序,包括对杀毒软件...
  • 常用电脑硬件技术术语集锦

    千次阅读 2006-08-16 14:19:00
    STD(Suspend to Disk),挂起到硬盘,是指系统在深度休眠时,将目前的资料保存在硬盘上,当再次开机时可以省去重启的时间,目前STD技术已属于淘汰的类型,更新的是STR技术。STR(Suspend to Ram),挂起到内存,即当...
  • 缓冲区是用来保存绘图过程中产生的图像数据的内存区域,OpenGL有颜色、深度、累积和模板等四种常用缓冲区。这里只介绍颜色缓冲区和深度缓冲区的清除方法。 由于缓冲区中可能保留有上一次绘图遗留下来的图像数据,...
  • cache高速缓冲存储器

    千次阅读 2012-08-08 10:37:30
    高速缓冲存储器(Cache)其原始意义是指存取速度比一般随机存取记忆体(RAM)来得快的一种RAM,一般而言它不像系统主记忆体那样使用DRAM技术,而使用昂贵但较快速的SRAM技术,也有快取记忆体的名称。 基本概念 在...
  • 2.3 高速缓冲存储器(Cache) 2.3.1 高速缓冲存储器的功能、结构与工作原理 高速缓冲存储器是存在于主存与CPU之间的一级存储器, 由静态存储...为此需要解决2个技术问题:一是主存地址与缓存地址的映象及转换; 二是按
  • C++ 队列与缓冲

    千次阅读 2019-06-13 10:00:48
    阻塞式消息队列、消息数据缓冲区、环形缓冲区、在自定义应用层协议中的应用..

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 74,266
精华内容 29,706
关键字:

常用的缓冲技术是解决