zoukankan      html  css  js  c++  java
  • STL内存配置器

    本系列文章更多是笔记形式,希望能在总结过程中将一些东西理顺。难免出错,欢迎指正。

    STL六大功能组件:

    1.容器(containers);2.算法(algorithm);3.迭代器(iterator);4.仿函数(functors);5.配接器(adapters);6.配置器(allcators)。

    各个功能组件间存在交互关系,这里不涉及这些内容,本篇文章讨论容器的内存配置。

    首先,容器用来存放数据,那么存放数据之前必须向系统申请内存资源。我们知道c++中通常用(::operator new/::operator new[])来为对象分配内存,并调用对应的构造函数构造对象。

    例如: class Foo { ... }; Foo * f = new Foo; delete f;

    这个过程分两步: 1.  ::operator new 配置内存; 2.调用Foo::Foo() 在申请的内存上构建对象.

    STL的配置器也分两个过程进行:

    1.定义std::alloc::allocate()负责申请空间, std::alloc::deallocate() 负责释放空间

    2.对象构造和析构分别调用  ::construct()和::destroy() --这两个函数可查阅<<c++ primer>>

    实现的代码文件在结构如下:

    <memory>:

          1.<stl_construct.h>:定义了全局的construct()和destroy(),完成对象的构造和析构,符合STL标准规范

            2.<stl_alloc.h>:定义了一,二级配置器彼此合作,名称为alloc

          3.<stl_uninitialized.h>:定义一些全局函数用来填充或复制大块内存数据,这里不想谈。

    但是STL的容器所使用的heap内存是由SGI特殊的空间配置器 std::alloc来完成的,说他特殊是因为它不符合SGI标准,但是SGI本身有标准的空间配置器 std::allocator,

    但因为其效率相对前者较低,所以容器的空间配置器为 std::alloc

    例如 vector的声明: template<class T, class Alloc = alloc>

              class vector{ ... } 

    其中alloc便是std::alloc,默认使用这个。

    刚才说到SGI的标准配置器效率不高,那么这个std::alloc效率又高在哪里呢?

    答案其实就在<stl_alloc.h>中第一的一二级配置器的配合使用上.

    SGI的标准的配置器其实就是对 ::operator new()和 ::operator delete()的简单的封装,而这两个函数相当于c 中的malloc()和free()函数。

    而std::alloc的分配策略如下 :

    1.当需要配置的区块 大于 128 bytes时,直接调用一级配置器,也就是封装 malloc()和free()

      1 #if 0
      2 #    include<new>
      3 #    define __THROW_BAD_ALLOC throw bad_alloc
      4 #elif !defined(__THROW_BAD_ALLOC)
      5 #    include<iostream.h>
      6 #    define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
      7 #endif
      8 
      9 template<int inst>
     10 class __malloc_alloc_template
     11 {
     12     private:
     13         static void * oom_malloc(size_t);                  //oom:out_of_memory,当malloc不成功时调用此函数
     14         static void * oom_realloc(void *, size_t);       //当realloc()失败时调用
     15         static void (* __malloc_alloc_oom_handler)();       //当申请失败时,可以自己定制的一个处理函数,此函数类似调用::operator new时的全局std::new_handler()
     16                                    //很重要
     17 
     18     public:
     19         static void * allocate(size_t n)
     20         {
     21             void * result = malloc(n);
     22             if (0 == result)
     23             {
     24                 result = oom_malloc(n);
     25             }
     26 
     27             return result;
     28         }
     29 
     30         static void * deallocate(void *p, size_t n)
     31         {
     32             free(p);
     33         }
     34 
     35         static void * reallocate(void *p, size_t new_sz)
     36         {
     37             void *result = realloc(p, new_sz);
     38             if ( 0 == result) 
     39             {
     40                 result = oom_realloc(p, new_sz);
     41             }
     42 
     43             return result;
     44         }
     45 
     46         //set __oom_handler
     47         static void (* set_malloc_handler(void  (*f)())) ()          //由于没有用::operator new来配置内存,所以不能调用c++机制的 new_handler(下篇文章详谈),只能自己定制
     48         {
     49             void (* old)() = __malloc_alloc_oom_handler;         //一般思路就是,设置新的,返回旧的
     50             __malloc_alloc_oom_handler = f;
     51 
     52             return old;
     53         }
     54 };
     55 
     56 // init static func handler
     57 template <int inst>
     58 void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
     59 
     60 template <int inst>
     61 static void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
     62 {
     63     void (* my_malloc_handler)();
     64     void * result;
     65 
     66     for (;;)
     67     {
     68         my_malloc_handler = __malloc_alloc_oom_handler;
     69         if (0 == my_malloc_handler)                    
     70         {
     71             __THROW_BAD_ALLOC;
     72         }
     73         (*my_malloc_handler)();                        //若是有申请失败处理函数,则调用之,因为按照c++的规矩,这个函数一般要进行收集一些能用的内存,供malloc下次调用,或者直接退出程序
     74         result = malloc(n);
     75 
     76         if (result)
     77         {
     78             return result;
     79         }
     80     } 
     81 }
     82 
     83 template <int inst>
     84 static void * __malloc_alloc_template::oom_realloc(void *p, size_t new_sz)
     85 {
     86     void (* my_realloc_handler)();
     87     void result;
     88 
     89     for (;;)
     90     {
     91         my_realloc_handler = __malloc_alloc_oom_handler;
     92         if (0 == my_realloc_handler)
     93         {
     94             __THROW_BAD_ALLOC;
     95         }
     96 
     97         (*my_realloc_handler)();
     98 
     99         result = realloc(p, new_sz);
    100         if(result)
    101         {
    102             return result;
    103         }
    104     }
    105 }
    106 
    107 typedef __malloc_alloc_template<0> malloc_alloc;
    View Code
      1 #if 0
      2 #    include<new>
      3 #    define __THROW_BAD_ALLOC throw bad_alloc
      4 #elif !defined(__THROW_BAD_ALLOC)
      5 #    include<iostream.h>
      6 #    define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
      7 #endif
      8 
      9 template<int inst>
     10 class __malloc_alloc_template
     11 {
     12     private:
     13         static void * oom_malloc(size_t);
     14         static void * oom_realloc(void *, size_t);
     15         static void (* __malloc_alloc_oom_handler)();
     16 
     17     public:
     18         static void * allocate(size_t n)
     19         {
     20             void * result = malloc(n);
     21             if (0 == result)
     22             {
     23                 result = oom_malloc(n);
     24             }
     25 
     26             return result;
     27         }
     28 
     29         static void * deallocate(void *p, size_t n)
     30         {
     31             free(p);
     32         }
     33 
     34         static void * reallocate(void *p, size_t new_sz)
     35         {
     36             void *result = realloc(p, new_sz);
     37             if ( 0 == result) 
     38             {
     39                 result = oom_realloc(p, new_sz);
     40             }
     41 
     42             return result;
     43         }
     44 
     45         //set __oom_handler
     46         static void (* set_malloc_handler(void  (*f)())) ()
     47         {
     48             void (* old)() = __malloc_alloc_oom_handler;
     49             __malloc_alloc_oom_handler = f;
     50 
     51             return old;
     52         }
     53 };
     54 
     55 // init static func handler
     56 template <int inst>
     57 void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
     58 
     59 template <int inst>
     60 static void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
     61 {
     62     void (* my_malloc_handler)();
     63     void * result;
     64 
     65     for (;;)
     66     {
     67         my_malloc_handler = __malloc_alloc_oom_handler;
     68         if (0 == my_malloc_handler)
     69         {
     70             __THROW_BAD_ALLOC;
     71         }
     72         (*my_malloc_handler)();
     73         result = malloc(n);
     74 
     75         if (result)
     76         {
     77             return result;
     78         }
     79     } 
     80 }
     81 
     82 template <int inst>
     83 static void * __malloc_alloc_template::oom_realloc(void *p, size_t new_sz)
     84 {
     85     void (* my_realloc_handler)();
     86     void result;
     87 
     88     for (;;)
     89     {
     90         my_realloc_handler = __malloc_alloc_oom_handler;
     91         if (0 == my_realloc_handler)
     92         {
     93             __THROW_BAD_ALLOC;
     94         }
     95 
     96         (*my_realloc_handler)();
     97 
     98         result = realloc(p, new_sz);
     99         if(result)
    100         {
    101             return result;
    102         }
    103     }
    104 }
    105 
    106 typedef __malloc_alloc_template<0> malloc_alloc;

    2.当需要配置的区块 小于  128bytes时,调用第二级适配器

    那么第二级适配器由哪些组成呢?

    1:一个有16个单元的指针数组,每个单元中的指针指向一个链表,链表元素如下。

    union obj

    {

      union obj * free_list_link;

      char client_data[1];

    }

    这16个单元从0-15管理大小分别为8,16,24,...128bytes的小额区块,也就是每个单元只想的链表的元素大小分别为这些。

    假如当申请一个大小为[1,8]或[16,24]大小的空间时,该配置器需从大小为8,24的链表中取一个元素来给客户端, 那么如何根据申请的大小来判断分配那种链表中的元素呢?,如下

    enum {__ALIGN = 8};

    static size_t FREELIST_INDEX(size_t bytes)

    {

      return ( ( (bytes) + __ALIGN - 1) / __ALIGN - 1);

    }

    可自行测试,例如申请7bytes的空间,带入后得到数组的index为0,即需要从该元素指针指向的链表申请空间,以此类推.

    2:内存池。有了这样的维护不同大小的链表的数组,但是链表的各个元素的空间又由哪来的呢,std::alloc 还维护了一个内存池,也就是用两个指针一个只想内存池开头,另一个指向结尾,每当一个链表的元素用光时,当再次有请求改大小的链表元素时,

    就会先向该内存池要空间,默认从该内存池中取出20个对象大小的空间,然后将这些空间在重新组织成链表的形式,放到数组中。

    3:堆内存。当内存池中的空间用完后,便向堆申请空间。

    4:若堆中的内存都没有了,那么这时候该怎么办呢?这时候就像链表元素更大的链表要空间.例如,当申请19bytes时,首先向元素大小为24的链表要空间,若没有了,想内存池要,若有,申请20*24的空间,然后

    重新组织成链表形式放回数组,并分配1个空间,若内存池也没有了,那就向堆要空间,如果堆也没了,这时,想元素大小为32或更大的链表要空间,如果有的话就去除一个分配下去,然后把剩余的空间放到对应大小

    的链表中,例如申请24bytes的时候,堆中也没有可用的了,那么这时需要向32以及更大的去要一个元素,这里假定是32的也没了,但是64的有空间,这时便从元素大小为64的链表中取一个下来,分给24给用户,剩下的40,放到元素大小为40的链表中。

    总结起来就是 对应客户申请大小的链表->内存池 ->堆->元素大小更大的链表->内存不足处理程序.

    本想介绍下二级配置器有哪些东西,一不小心把过程说了出来。

    下边分析源码:

    //下面是第二级配置器
    246 //主要是维护一个内存池,用来小于128byte的小型区块内存的分配
    247 //其中,有多个链表,各链表中的node大小从8-128byte,都是8的倍数
    248 //分配时,不是8的倍数,上调至最近的8的倍数,
    249 //然后从相应链表中取下一个对应大小的node分配给请求
    250 #ifdef __SUNPRO_CC
    251     enum {__ALIGN = 8};  //小型区块的上调边界,即次对于用户申请的空间大小n都要调整成最接近且大于n的8的倍数
    252     enum {__MAX_BYTES = 128};   //用户申请的最大空间大小,若大于这个值,调用一级配置器
    253     enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //数组的长度
    254 #endif
    255 
    256 //第二级配置器
    257 template <bool threads, int inst>
    258 class __default_alloc_template
    259 {
    260     private:
    261 # ifndef __SUNPRO_CC
    262     enum {__ALIGN = 8};  //小型区块的上调边界
    263     enum {__MAX_BYTES = 128};   //小型区块的上限
    264     enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
    265 # endif
    266     //大小上调至8的倍数
    267     static size_t ROUND_UP(size_t bytes)
    268     {
    269         return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
    270     }
    271 __PRIVATE:
    272     union obj
    273     {
    274         union obj * free_list_link;  //用于在链表中指向下一个节点
    275         char client_data[1]; //用于存储实际区块的内存地址,由于这是一个union,很好的节约了这个数据的内存
    276     };
    277     private:
    278 # ifdef __SUNPRO_CC
    279     static obj * __VOLATILE free_list[]; 
    280 # else
    281     static obj * __VOLATILE free_list[__NFREELISTS];//前面提到的那个有16个元素的数组,每个数组元素是个static obj* __VOLATILE,指向链表第一个元素
    282 # endif
    283     static size_t FREELIST_INDEX(size_t bytes)    //此函数用来根据用户传来的bytes,找到对应数组元素的index
    284     {
    285         return (((bytes) + __ALIGN-1)/__ALIGN - 1);
    286     }
    287 
    288     //返回大小为n的对象,并可能加入大小为n的其他区块到free list
    289     static void *refill(size_t n);
    290     //配置一块空间,可容纳nobjs个大小为"size"的区块
    291     //如果配置nobjs个区块有所不便,nobjs可能会降低
    292     static char *chunk_alloc(size_t size, int &nobjs);
    293 
    294     //chunk 分配、配置的状态
    295     static char *start_free; //内存池起始位置。只在chunk_alloc()中变化
    296     static char *end_free;   //内存池结束位置。只在chunk_alloc()中变化
    297     static size_t heap_size; //内存池空间不够时,向堆空间申请的大小
    298 /*
    //初始化各个static变量
     template <bool threads, int inst>
    572 char *__default_alloc_template<threads, inst>::start_free = 0; //设置初始值
    573 
    574 template <bool threads, int inst>
    575 char *__default_alloc_template<threads, inst>::end_free = 0; //设置初始值
    576 
    577 template <bool threads, int inst>
    578 size_t __default_alloc_template<threads, inst>::heap_size = 0; //设置初始值
    579 
    580 //初始化16种大小的区块链表为空
    581 template <bool threads, int inst>
    582 typename __default_alloc_template<threads, inst>::obj * __VOLATILE
    583 __default_alloc_template<threads, inst>::free_list[
    584 # ifdef __SUNPRO_CC
    585     __NFREELISTS
    586 # else
    587     __default_alloc_template<threads, inst>::__NFREELISTS
    588 # endif
    589 ] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

    以上是除去锁以后的,加锁的以后讨论。下面看看二级配置器是如何配置空间的:

    static void * allocate(size_t n)  //std::alloc的申请函数
    337         {
    338             obj * __VOLATILE * my_free_list;
    339             obj * __RESTRICT result;
    340 
    341             //需要分配的大小大于二级配置器的__MAX_BYTES,直接使用第一级配置器
    342             if (n > (size_t) __MAX_BYTES)
    343             {
    344                 return(malloc_alloc::allocate(n));
    345             }
    346             my_free_list = free_list + FREELIST_INDEX(n); //找到比需要分配的大小大,且最接近的大小块所在的链表所在free_list数组中的位置
    347             
    352             result = *my_free_list;  //取出找的对应链表的指向第一个节点的指针,插入也是从第一个插入,前插。
    353             if (result == 0)    //对应的链表中没有剩余未分配的节点区块
    354             {
    355                 void *r = refill(ROUND_UP(n));    //再从内存池中分配一批,需求大小的区块(实际大小是请求大小上调至8的倍数后的数值),
    356                                 //然后,放入对应链表,待分配给请求
    357                 return r;
    358             }
    359             //如果对应大小区块的链表中不为空,还有待分配的区块,取出第一个节点
    360             *my_free_list = result -> free_list_link;
    361             return (result);
    362         };
    363 
    364         //p不可以是0
    365         static void deallocate(void *p, size_t n)
    366         {
    367             obj *q = (obj *)p;
    368             obj * __VOLATILE * my_free_list;
    369 
    370             //大于区块大小上限的,直接调用第一级配置器释放
    371             if (n > (size_t) __MAX_BYTES)
    372             {
    373                 malloc_alloc::deallocate(p, n);
    374                 return;
    375             }
    376             my_free_list = free_list + FREELIST_INDEX(n);
    377            
    382             //头插法,插入对应大小的区块链表
    383             q -> free_list_link = *my_free_list;
    384             *my_free_list = q;
    385           }
    387 

    可以看到,allocate()函数的过程如上所述,先从链表空间取,若链表为空,则去内存池去申请,调用的函数是 refill(ROUND_UP(n)),因为从内存池中获得的都是8的倍数,所以先将 n ROUND_UP一下。

    下面是refill函数:

    487 template <bool threads, int inst>
    488 void* __default_alloc_template<threads, inst>::refill(size_t n)
    489 {
    490     int nobjs = 20;  //默认一次分配20个需求大小的区块
    491     char * chunk = chunk_alloc(n, nobjs); //到内存池中获取控件,chunk是分配的空间的开始地址,令其类型为char *,主要是因为一个char的大小正好是一个byte
    492     obj * __VOLATILE *my_free_list;
    493     obj * result;
    494     obj * current_obj, * next_obj;
    495     int i;
    496 
    497     //如果只获得一个区块,这个区块就分配给调用者,free list 无新节点
    498     if (1 == nobjs) return chunk;//nobjs开始定义为20,这里为什么要检查是否为1呢,原因是以传引用的方式穿到chunk_alloc,并且该函数会将njobs修改为实际申请到的数量
    499     //否则准备调整free list,纳入新节点
    500     my_free_list = free_list + FREELIST_INDEX(n);
    501 
    502     //以下在chunk空间内建立free list
    503     result = (obj *)chunk;  //这一块准备返回给客端
    504     // 以下导引free list 指向新配置的空间(取自内存池)
    505     
    506     //由于chunk是char*,所以加上n,就表示走过n个char,
    507     //一个char正好是一个byte,所以chunk+n现在指向第二个区块
    508     *my_free_list = next_obj = (obj *)(chunk + n); 
    509     for (i = 1; ; ++i)
    510     {
    511         // 从1开始,因为第0个将返回给客端
    512         current_obj = next_obj;
    513         // 每次移动n个char,正好是n个byte,所以正好指向下个区块
    514         next_obj = (obj *)((char *)next_obj + n);
    
                  //下面讲下这个判断,假如从内存池中申请到了3个块的连续空间,上边的操作已经将第一个块空间返回个用户,那么只需要将剩下的两个换成链表形式,i表示已经被换成节点的个数,而 njobs表示总共个数,又由于第一个已经分配给了用户,所以只需处理njobs - 1个,那么nobjs - 1 == i 也就表示:是否将剩下的整块空间整理成的链表形式。
    515         if (nobjs - 1 == i)
    516         {
    517             // 已经遍历完,此时next_obj指向的内存已经超出我们分配的大小了
    518             // 不属于我们的内存
    519             current_obj -> free_list_link = 0;
    520             break;
    521         }
    522         else
    523         {
    524             current_obj -> free_list_link = next_obj;
    525         }
    526     }
    527     return result;
    528 }        

    那么chunk_alloc又是什么样的呢?:

    template <bool threads, int inst>
    401 char *
    402 __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
    403 {
    404     char * result;
    405     size_t total_bytes = size * nobjs;
    406     size_t bytes_left = end_free - start_free;    //内存池剩余空间
    407 
    408     if (bytes_left >= total_bytes)
    409     {
    410         //内存池中剩余的空间足够满足需求量
    411         result = start_free;
    412         start_free += total_bytes;
    413         return(result);
    414     }
    415     else if (bytes_left >= size)
    416     {
    417         //内存池剩余空间不能完全满足需求量,但足够供应一个及以上的区块
    418         nobjs = bytes_left/size;
    419         total_bytes = size * nobjs;
    420         result = start_free;
    421         start_free += total_bytes;
    422         return (result);
    423     }
    424     else
    425     {
    426         //内存池连一个区块的大小都无法满足,这时需要向堆中申请内存,但在这之前首先对内存池剩余的空间加以利用
    427         size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
    428         //以下试着让内存池中的残余零头还有利用价值
    429         if (bytes_left > 0)
    430         {
    431             //内存池中内还有一些零头,先配给适当的free list
    432             //首先寻找适当的free list
    433             obj * __VOLATILE * my_free_list = 
    434                     free_list + FREELIST_INDEX(bytes_left);
    435 
    436             //调整free list,将内存池中残余的空间编入
    437             ((obj *)start_free) -> free_list_link = *my_free_list; 
    438             *my_free_list = (obj *)start_free;
    439         }
    440 
    441         //配置heap空间,用来补充内存池
    442         start_free = (char *)malloc(bytes_to_get);
    443         if (0 == start_free)
    444         {
    445             //如果heap空间不足,malloc()失败
    446             int i;
    447             obj * __VOLATILE *my_free_list, *p;
    448            //当堆中内存也不够用时,需要向数组中节点大小更大的链表去要空间
    452             for (i = size; i <= __MAX_BYTES; i += __ALIGN)
    453             {
    454                 my_free_list = free_list + FREELIST_INDEX(i);
    455                 p = *my_free_list;
    456                 if (0 != p)
    457                 {
    458                     //free list内尚有未用区块
    459                     //调整free list以释放出未用的区块到内存池
    460                     *my_free_list = p -> free_list_link;
    461                     start_free = (char *)p;
    462                     end_free = start_free + i;
    463                     // 此时内存池已经有内存了
    464                     //修改istart_free和end_free后 递归调用自己,为了修正objs,同时如果你按照本函数的流程再走一遍的话,会发现其实只需一遍就可以完成任务,
    465                     return chunk_alloc(size, nobjs);
    466                     //注意,任何残余的零头终将被编入适当的free list中备用
    467                     
    468                 }
    469             }
    470             end_free = 0;  //如果出现意外(山穷水尽,到处都没有内存可用了)
    471             //调用第一级配置器,之前一直没有理解,为什么当都没有内存后,需要调用第一级配置器,看过这边才明白一些。之前提到,第一级配置器中有个set_alloc_handler函数,也就是设置当内存不足时的处理函数,而这个函数做的最多的便是做一些搜集系统内可用的内存,然后可以在循环中调用申请空间的函数时能申请到空间等一些工作,详见<effective c++> 3rd中的条款49到52.
    472             start_free = (char *)malloc_alloc::allocate(bytes_to_get);
    473             //这会导致抛出异常,或内存不足的情况获得改善
    474         }
    475         heap_size += bytes_to_get;
    476         end_free = start_free + bytes_to_get;
    477         //递归调用自己,为了修正objs
    478         return chunk_alloc(size, nobjs);
    479     }
    480 }

    本人觉得STL的这个容器配置器最经典的:

    1:整体的优化设计,分两层配置空间。

    2:就是上边的 chunk_alloc 函数的设计,尤其是最后递归调用,自己整理自己。

    之前一直没有理解,这次感觉理解了一些后觉得收获很大。

    好了,最后在总结一些整个配置思路:

    1.大于128的,直接调用一集配置器;

    2.小于128的调用二级配置器:先找到合适的区块大小的链表要空间,若没有到内存池要,内存池没有到堆要,堆也没了,到数组其他的链表要,并整理取得区块的剩余的小空间,如果整个数组中都没有找到,就调用一级配置器,将其作为函数的最后的出口,其实主要是通过以及配置器中的

    处理函数解决。

    整个过程如上,具体细节可阅读代码,对着注释看,便一目了然。

    之前讲过,第一级适配器中要设置处理函数,他恰巧可以作为了第二级配置器的出口,那么这个处理函数是怎么样的,具体做了哪些工作?如何设置等,见下篇文章。 

  • 相关阅读:
    汇编自学链接
    数据库 —— Access 数据库
    OpenGL —— 基础笔记
    VMWare复制虚拟机系统后,模块“Disk”无法启动【转】
    DM设备的创建与管理
    RAID磁盘分区的创建
    文件的压缩、解压缩和打包命令
    磁盘及文件系统管理
    Shell编程中的条件判断(条件测试)
    Linux中vim编辑器的缩进的功能键
  • 原文地址:https://www.cnblogs.com/newbeeyu/p/5559874.html
Copyright © 2011-2022 走看看