作者:bullbat

在Linux内核中,并非总使用基于页的方法来承担缓存的任务。内核的早期版本只包含了块缓存,来加速文件操作和提高系统性能。这是来自于其他具有相同结构的类UNIX操作系统的遗产。来自于底层块设备的块缓存在内存的缓冲区中,可以加速读写操作。

与内存页相比,块不仅比较小(大多数情况下),而且长度可变的,依赖于使用的块设备(或文件系统)。随着日渐倾向于使用基于页操作实现的通用文件存取方法,块缓存作为中枢系统缓存的重要性已经逐渐失去。主要的缓存任务现在由页缓存承担。另外,基于块的I/O的标准数据结构,现在已经不再是缓冲区,而是struct bio结构。

缓冲区用作小型的数据传输,一般设计的数据量是与块长度可比拟的。文件系统在处理元数据时,通常会使用此类方法。而裸数据的传输则按页进行,而缓冲区的实现也基于也缓存。

块缓存在结构上由两个部分组成:

1)  缓冲头(buffer head)包含了与缓冲区状态相关的所有管理数据,包括快号、块长度、访问计数器等。这些数据不是直接存储在缓冲头之后,而是存储在物理内存的一个独立区域中,由缓冲头结构中的一个对应的指针表示。

2)  有用数据保存在专门分配的页中,这些页也可能同时存在于页缓存中。这进一步细分了页缓存,如下图所示,在我们的例子中,页划分为4个长度相同的部分,每一部分由其自身的缓冲头描述。缓冲头存储的内存区域与有用数据存储的区域是有关的。

这使得页面可以细分为更小的部分,各顾各部分之间完全连续的(因为缓冲区数据和缓冲头数据是分离的)。因为一个缓冲区由至少512字节组成,每页最多可包括MAX_BUF_PER_PAGE个缓冲区。该常数定义为页面长度的函数。

如果修改了某个缓冲区,则会立即印象到页面的内容(反之也是),因而两个缓存不需要显示同步,毕竟二者的数据是共享的。

当然,有些应用程序在访问块设备时,使用的是块而不是页面,读取文件系统的操作几块,就是一个例子。一个独立的块缓存用于加速此类访问。该块缓存的运作独立于页面缓存,而不是在其上建立的。为此,缓冲头数据结构(对于块缓存和页面缓存是相同的)群聚在一个长度恒定的数组中,各个数组项按LUR方式管理。在一个三个数组项用过之后,将其置于索引位置0,其他数组项相应下移。这意味这最常使用的数组项位于数组的开头,而不常用的数组项将被后退,如果很长时间不使用,则会“掉出”数组。

因为数组的长度,或者说LUR列表中的项数,是一个固定值,在内核运行期间不改变,内核无需运行独立的线程来将缓存长度修正为合理值。相反,内核只需要在一项“掉出”数组时,将相关的缓冲区从缓存删除,以释放内存,用于其他目地。

块缓存实现

块患处不仅仅用作页面缓存的附加功能,对以块而不是页面进行处理的对象来说,块缓存是一个独立的缓存。

数据结构

块缓冲区头

[cpp] view plaincopyprint?
  1. struct buffer_head {
  2. unsigned long b_state;      /* buffer state bitmap (see above) */
  3. struct buffer_head *b_this_page;/* circular list of page's buffers */
  4. struct page *b_page;        /* the page this bh is mapped to */
  5. sector_t b_blocknr;     /* start block number */
  6. size_t b_size;          /* size of mapping */
  7. char *b_data;           /* pointer to data within the page */
  8. struct block_device *b_bdev;
  9. bh_end_io_t *b_end_io;      /* I/O completion */
  10. void *b_private;        /* reserved for b_end_io */
  11. struct list_head b_assoc_buffers; /* associated with another mapping */
  12. struct address_space *b_assoc_map;  /* mapping this buffer is
  13. associated with */
  14. atomic_t b_count;       /* users using this buffer_head */
  15. };

操作

内核必须提供一组操作,使得其余代码能够轻松有效地利用缓冲区的功能。切记:这些机制对内存中实际缓存的数据没有贡献。

在使用缓冲区之前,内核首先必须创建一个buffer_head结构实例,而其余的函数则对该结构进行操作。因为创建新缓冲头是一个频繁重现的任务,他应该尽快执行。这是一种很经典的情形,可使用slab缓存解决。

切记:内核源代码确实提供了一些函数,可用作前端,来创建和销毁缓冲头。alloc_buffer_head生成一个新的缓冲头,而free_buffer_head销毁一个显存的缓冲头。

[cpp] view plaincopyprint?
  1. /*分配buffer_head*/
  2. struct buffer_head *alloc_buffer_head(gfp_t gfp_flags)
  3. {
  4. /*从slab中分配空间*/
  5. struct buffer_head *ret = kmem_cache_alloc(bh_cachep, gfp_flags);
  6. if (ret) {
  7. /*初始化*/
  8. INIT_LIST_HEAD(&ret->b_assoc_buffers);
  9. get_cpu_var(bh_accounting).nr++;
  10. recalc_bh_state();
  11. put_cpu_var(bh_accounting);
  12. }
  13. return ret;
  14. }

页缓存和块缓存的交互

一页划分为几个数据单元,但缓冲头保存在独立的内存区中,与实际数据无关。与缓冲区的交互没有改变的页的内容,缓冲区只不过为页的数据提供了一个新的视图。

为支持页与缓冲区的交互,需要使用struct page的private成员。其类型为unsigned long,可用作指向虚拟地址空间中任何位置的指针。

Private成员还可以用作其他用途,根据页的具体用途,可能与缓冲头完全无关。但其主要的用途是关联缓冲区和页。这样的话,private指向将页划分为更小单位的第一个缓冲头。各个缓冲头通过b_this_page链接为一个环形链表。在该链表中每个缓冲头的b_this_page成员指向下一个缓冲头,而最后一个缓冲头的b_this_page成员指向第一个缓冲头。这使得内核从page结构开始,可以轻易地扫描与页关联的所有buffer_head实例。

内核提供cteate_empty_buffers函数关联page和buffer_head结构之间的关联:

[cpp] view plaincopyprint?
  1. /*
  2. * We attach and possibly dirty the buffers atomically wrt
  3. * __set_page_dirty_buffers() via private_lock.  try_to_free_buffers
  4. * is already excluded via the page lock.
  5. */
  6. void create_empty_buffers(struct page *page,
  7. unsigned long blocksize, unsigned long b_state)
  8. {
  9. struct buffer_head *bh, *head, *tail;
  10. head = alloc_page_buffers(page, blocksize, 1);
  11. bh = head;
  12. /*遍历所有缓冲头,设置其状态,并建立一个环形链表*/
  13. do {
  14. bh->b_state |= b_state;
  15. tail = bh;
  16. bh = bh->b_this_page;
  17. } while (bh);
  18. tail->b_this_page = head;
  19. spin_lock(&page->mapping->private_lock);
  20. /*缓冲区的状态依赖于内存页面中数据的状态*/
  21. if (PageUptodate(page) || PageDirty(page)) {
  22. bh = head;
  23. do {/*设置相关标志*/
  24. if (PageDirty(page))
  25. set_buffer_dirty(bh);
  26. if (PageUptodate(page))
  27. set_buffer_uptodate(bh);
  28. bh = bh->b_this_page;
  29. } while (bh != head);
  30. }
  31. /*将缓冲区关联到页面*/
  32. attach_page_buffers(page, head);
  33. spin_unlock(&page->mapping->private_lock);
  34. }
[cpp] view plaincopyprint?
  1. static inline void attach_page_buffers(struct page *page,
  2. struct buffer_head *head)
  3. {
  4. page_cache_get(page);/*递增引用计数*/
  5. /*设置PG_private标志,通知内核其他部分,page实例的private成员正在使用中*/
  6. SetPagePrivate(page);
  7. /*将页的private成员设置为一个指向环形链表中第一个缓冲头的指针*/
  8. set_page_private(page, (unsigned long)head);
  9. }

交互

如果对内核的其他部分无益,那么在页和缓冲区之间建立关联就没起作用。一些与块设备之间的传输操作,传输单位的长度依赖于底层设备的块长度,而内核的许多部分更喜欢按页的粒度来执行I/O操作,因为这使得其他事情更容易处理,特别是内存管理方面。在这种场景下,缓冲头区充当了双方的中介。

从缓冲区中读取整页

首先考察内核在从块设备读取整页时采用的方法,以block_read_full_page为例。我们讨论缓冲区实现所关注的部分。

[cpp] view plaincopyprint?
  1. /*
  2. * Generic "read page" function for block devices that have the normal
  3. * get_block functionality. This is most of the block device filesystems.
  4. * Reads the page asynchronously --- the unlock_buffer() and
  5. * set/clear_buffer_uptodate() functions propagate buffer state into the
  6. * page struct once IO has completed.
  7. */
  8. int block_read_full_page(struct page *page, get_block_t *get_block)
  9. {
  10. struct inode *inode = page->mapping->host;
  11. sector_t iblock, lblock;
  12. struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE];
  13. unsigned int blocksize;
  14. int nr, i;
  15. int fully_mapped = 1;
  16. BUG_ON(!PageLocked(page));
  17. blocksize = 1 << inode->i_blkbits;
  18. /*检查页是否有相关联的缓冲区,如果没有,则创建他*/
  19. if (!page_has_buffers(page))
  20. create_empty_buffers(page, blocksize, 0);
  21. /*获得这些缓冲区,无论是新建的还是已经存在的
  22. 只是将page的private成员转换为buffer_head指针,因为按照
  23. 惯例,private指向与page关联的第一个缓冲头*/
  24. head = page_buffers(page);
  25. iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);
  26. lblock = (i_size_read(inode)+blocksize-1) >> inode->i_blkbits;
  27. bh = head;
  28. nr = 0;
  29. i = 0;
  30. /*内核遍历与页面关联的所有缓冲区*/
  31. do {
  32. /*如果缓冲区内容是最新的,内核继续处理下一个
  33. 缓冲区。在这种情况下,页面缓冲区中的数据与块
  34. 设备匹配,无需额外的读操作*/
  35. if (buffer_uptodate(bh))
  36. continue;
  37. /*如果没有映射*/
  38. if (!buffer_mapped(bh)) {
  39. int err = 0;
  40. fully_mapped = 0;
  41. if (iblock < lblock) {
  42. WARN_ON(bh->b_size != blocksize);
  43. /*确定块在块设备上的位置*/
  44. err = get_block(inode, iblock, bh, 0);
  45. if (err)
  46. SetPageError(page);
  47. }
  48. if (!buffer_mapped(bh)) {
  49. zero_user(page, i * blocksize, blocksize);
  50. if (!err)
  51. set_buffer_uptodate(bh);
  52. continue;
  53. }
  54. /*
  55. * get_block() might have updated the buffer
  56. * synchronously
  57. */
  58. if (buffer_uptodate(bh))
  59. continue;
  60. }
  61. /*如果缓冲区已经建立了与块的映射,但是其内容不是最新
  62. 的则将缓冲区放置到一个临时的数组中*/
  63. arr[nr++] = bh;
  64. } while (i++, iblock++, (bh = bh->b_this_page) != head);
  65. if (fully_mapped)
  66. SetPageMappedToDisk(page);
  67. if (!nr) {
  68. /*
  69. * All buffers are uptodate - we can set the page uptodate
  70. * as well. But not if get_block() returned an error.
  71. */
  72. if (!PageError(page))
  73. SetPageUptodate(page);
  74. unlock_page(page);
  75. return 0;
  76. }
  77. /* Stage two: lock the buffers */
  78. for (i = 0; i < nr; i++) {
  79. bh = arr[i];
  80. lock_buffer(bh);
  81. /*将b_end_io设置为end_buffer_async_read,该函数将在数据传输结构时
  82. 调用*/
  83. mark_buffer_async_read(bh);
  84. }
  85. /*
  86. * Stage 3: start the IO.  Check for uptodateness
  87. * inside the buffer lock in case another process reading
  88. * the underlying blockdev brought it uptodate (the sct fix).
  89. */
  90. for (i = 0; i < nr; i++) {
  91. bh = arr[i];
  92. if (buffer_uptodate(bh))
  93. end_buffer_async_read(bh, 1);
  94. else
  95. /*将所有需要读取的缓冲区转交给块层
  96. 也就是BIO层,在其中开始读操作*/
  97. submit_bh(READ, bh);
  98. }
  99. return 0;
  100. }

将整页写入到缓冲区

除了读操作之外,页面的写操作也可以划分为更小的单位。只有页中实际修改的内容需要回写,而不用回写整页的内容。遗憾的是,从缓冲区的角度来看,写操作的实现比上述的读操作复杂的多。

__block_wirte_full_page函数中回写脏页面设计的缓冲区相关操作。

[cpp] view plaincopyprint?
  1. /*
  2. * NOTE! All mapped/uptodate combinations are valid:
  3. *
  4. *  Mapped  Uptodate    Meaning
  5. *
  6. *  No  No      "unknown" - must do get_block()
  7. *  No  Yes     "hole" - zero-filled
  8. *  Yes No      "allocated" - allocated on disk, not read in
  9. *  Yes Yes     "valid" - allocated and up-to-date in memory.
  10. *
  11. * "Dirty" is valid only with the last case (mapped+uptodate).
  12. */
  13. /*
  14. * While block_write_full_page is writing back the dirty buffers under
  15. * the page lock, whoever dirtied the buffers may decide to clean them
  16. * again at any time.  We handle that by only looking at the buffer
  17. * state inside lock_buffer().
  18. *
  19. * If block_write_full_page() is called for regular writeback
  20. * (wbc->sync_mode == WB_SYNC_NONE) then it will redirty a page which has a
  21. * locked buffer.   This only can happen if someone has written the buffer
  22. * directly, with submit_bh().  At the address_space level PageWriteback
  23. * prevents this contention from occurring.
  24. *
  25. * If block_write_full_page() is called with wbc->sync_mode ==
  26. * WB_SYNC_ALL, the writes are posted using WRITE_SYNC_PLUG; this
  27. * causes the writes to be flagged as synchronous writes, but the
  28. * block device queue will NOT be unplugged, since usually many pages
  29. * will be pushed to the out before the higher-level caller actually
  30. * waits for the writes to be completed.  The various wait functions,
  31. * such as wait_on_writeback_range() will ultimately call sync_page()
  32. * which will ultimately call blk_run_backing_dev(), which will end up
  33. * unplugging the device queue.
  34. */
  35. static int __block_write_full_page(struct inode *inode, struct page *page,
  36. get_block_t *get_block, struct writeback_control *wbc,
  37. bh_end_io_t *handler)
  38. {
  39. int err;
  40. sector_t block;
  41. sector_t last_block;
  42. struct buffer_head *bh, *head;
  43. const unsigned blocksize = 1 << inode->i_blkbits;
  44. int nr_underway = 0;
  45. int write_op = (wbc->sync_mode == WB_SYNC_ALL ?
  46. WRITE_SYNC_PLUG : WRITE);
  47. BUG_ON(!PageLocked(page));
  48. last_block = (i_size_read(inode) - 1) >> inode->i_blkbits;
  49. /*页面是否有关联缓冲区,如果没有创建他*/
  50. if (!page_has_buffers(page)) {
  51. create_empty_buffers(page, blocksize,
  52. (1 << BH_Dirty)|(1 << BH_Uptodate));
  53. }
  54. /*
  55. * Be very careful.  We have no exclusion from __set_page_dirty_buffers
  56. * here, and the (potentially unmapped) buffers may become dirty at
  57. * any time.  If a buffer becomes dirty here after we've inspected it
  58. * then we just miss that fact, and the page stays dirty.
  59. *
  60. * Buffers outside i_size may be dirtied by __set_page_dirty_buffers;
  61. * handle that here by just cleaning them.
  62. */
  63. block = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);
  64. head = page_buffers(page);
  65. bh = head;
  66. /*
  67. * Get all the dirty buffers mapped to disk addresses and
  68. * handle any aliases from the underlying blockdev's mapping.
  69. */
  70. /*对所有未映射的脏缓冲区,在缓冲区和块设备
  71. 之间建立映射*/
  72. do {
  73. if (block > last_block) {
  74. /*
  75. * mapped buffers outside i_size will occur, because
  76. * this page can be outside i_size when there is a
  77. * truncate in progress.
  78. */
  79. /*
  80. * The buffer was zeroed by block_write_full_page()
  81. */
  82. clear_buffer_dirty(bh);
  83. set_buffer_uptodate(bh);
  84. } else if ((!buffer_mapped(bh) || buffer_delay(bh)) &&
  85. buffer_dirty(bh)) {
  86. WARN_ON(bh->b_size != blocksize);
  87. /*查找块设备上与缓冲区项匹配的块*/
  88. err = get_block(inode, block, bh, 1);
  89. if (err)
  90. goto recover;
  91. clear_buffer_delay(bh);
  92. if (buffer_new(bh)) {
  93. /* blockdev mappings never come here */
  94. clear_buffer_new(bh);
  95. unmap_underlying_metadata(bh->b_bdev,
  96. bh->b_blocknr);
  97. }
  98. }
  99. bh = bh->b_this_page;
  100. block++;
  101. } while (bh != head);
  102. /*第二遍遍历,将滤出所有的脏缓冲区*/
  103. do {
  104. if (!buffer_mapped(bh))
  105. continue;
  106. /*
  107. * If it's a fully non-blocking write attempt and we cannot
  108. * lock the buffer then redirty the page.  Note that this can
  109. * potentially cause a busy-wait loop from writeback threads
  110. * and kswapd activity, but those code paths have their own
  111. * higher-level throttling.
  112. */
  113. if (wbc->sync_mode != WB_SYNC_NONE || !wbc->nonblocking) {
  114. lock_buffer(bh);
  115. } else if (!trylock_buffer(bh)) {
  116. redirty_page_for_writepage(wbc, page);
  117. continue;
  118. }
  119. /*如果设置了脏页标志,则会在调用该函数时清除
  120. 因为缓冲区的内容将立即回写*/
  121. if (test_clear_buffer_dirty(bh)) {
  122. /*设置BH_Async_Write状态位,并将end_buffer_async_write
  123. 指定为BIO完成处理程序即b_end_io*/
  124. mark_buffer_async_write_endio(bh, handler);
  125. } else {
  126. unlock_buffer(bh);
  127. }
  128. } while ((bh = bh->b_this_page) != head);
  129. /*
  130. * The page and its buffers are protected by PageWriteback(), so we can
  131. * drop the bh refcounts early.
  132. */
  133. BUG_ON(PageWriteback(page));
  134. set_page_writeback(page);
  135. /*最后一次遍历*/
  136. do {
  137. struct buffer_head *next = bh->b_this_page;
  138. if (buffer_async_write(bh)) {
  139. /*将前一次遍历中标记为BH_Async_Write的所有缓冲区
  140. 转交给块层执行实际的写操作,该函数向块层提交
  141. 了对应的请求*/
  142. submit_bh(write_op, bh);
  143. nr_underway++;
  144. }
  145. bh = next;
  146. } while (bh != head);
  147. unlock_page(page);
  148. err = 0;
  149. done:
  150. if (nr_underway == 0) {
  151. /*
  152. * The page was marked dirty, but the buffers were
  153. * clean.  Someone wrote them back by hand with
  154. * ll_rw_block/submit_bh.  A rare case.
  155. */
  156. end_page_writeback(page);
  157. /*
  158. * The page and buffer_heads can be released at any time from
  159. * here on.
  160. */
  161. }
  162. return err;
  163. recover:
  164. /*
  165. * ENOSPC, or some other error.  We may already have added some
  166. * blocks to the file, so we need to write these out to avoid
  167. * exposing stale data.
  168. * The page is currently locked and not marked for writeback
  169. */
  170. bh = head;
  171. /* Recovery: lock and submit the mapped buffers */
  172. do {
  173. if (buffer_mapped(bh) && buffer_dirty(bh) &&
  174. !buffer_delay(bh)) {
  175. lock_buffer(bh);
  176. mark_buffer_async_write_endio(bh, handler);
  177. } else {
  178. /*
  179. * The buffer may have been set dirty during
  180. * attachment to a dirty page.
  181. */
  182. clear_buffer_dirty(bh);
  183. }
  184. } while ((bh = bh->b_this_page) != head);
  185. SetPageError(page);
  186. BUG_ON(PageWriteback(page));
  187. mapping_set_error(page->mapping, err);
  188. set_page_writeback(page);
  189. do {
  190. struct buffer_head *next = bh->b_this_page;
  191. if (buffer_async_write(bh)) {
  192. clear_buffer_dirty(bh);
  193. submit_bh(write_op, bh);
  194. nr_underway++;
  195. }
  196. bh = next;
  197. } while (bh != head);
  198. unlock_page(page);
  199. goto done;
  200. }

Linux缓存机制之块缓存相关推荐

  1. hibernate mysql缓存机制_Hibernate的缓存机制

    面试常问到的问题: 首先说下hibernate缓存的作用(即为什么要用缓存机制),然后再具体说说Hibernate中缓存分类情况,最后可以举例: Hibernate缓存的作用: Hibernate是一 ...

  2. Http缓存机制(强缓存与协商缓存)及过程

    一.为什么要缓存: 可以减少网络请求的次数和数量,降低网络延迟,加速页面加载,提高用户体验等 二.缓存机制:强缓存优先级高于协商缓存 强制缓存:服务器端认为请求资源应该被缓存,则在响应头部设置cach ...

  3. 浏览器缓存机制(强制缓存,协商缓存)

    浏览器缓存机制(强制缓存,协商缓存) 1. 强制缓存 (1)Expires (2)Cache-Control 2. 协商缓存 (1)Last-Modified / If-Modified-Since ...

  4. MyBatis缓存机制之一级缓存

    MyBatis缓存机制之一级缓存 前言 MyBatis内部封装了JDBC,简化了加载驱动.创建连接.创建statement等繁杂的过程,是我们常见的持久性框架.缓存是在计算机内存中保存的临时数据,读取 ...

  5. 浏览器缓存机制介绍与缓存策略剖析

    缓存可以减少网络 IO 消耗,提高访问速度.浏览器缓存是一种操作简单.效果显著的前端性能优化手段.对于这个操作的必要性,Chrome 官方给出的解释似乎更有说服力一些: 通过网络获取内容既速度缓慢又开 ...

  6. Mybatis(五) 延迟加载和缓存机制(一级二级缓存)

    踏踏实实踏踏实实,开开心心,开心是一天不开心也是一天,路漫漫其修远兮. --WZY 一.延迟加载 延迟加载就是懒加载,先去查询主表信息,如果用到从表的数据的话,再去查询从表的信息,也就是如果没用到从表 ...

  7. iOS开发缓存机制之—内存缓存机制

    在IOS应用程序开发中,为了减少与服务端的交互次数,加快用户的响应速度,一般都会在iOS设备中加一个缓存的机制. 这篇文章将介绍一下如何在iOS设备中进行缓存,本文先介绍一下将内容缓存到内存中,下一篇 ...

  8. 浏览器缓存机制,强缓存,弱缓存

    目录 web缓存类型 浏览器缓存规则: 浏览器缓存的控制 cache-control总结 Expires Last-modified & If-modified-since Etag & ...

  9. 浏览器缓存机制及一些缓存问题解决方法

    参考: http://bbs.csdn.net/topics/330028896  浏览器缓存机制 http://www.docin.com/p-591569918.html  浏览器缓存的一些问题的 ...

  10. Android Glide图片加载-缓存机制(内存缓存和磁盘缓存)

    前言 glide的缓存机制.Glide的缓存设计是非常的先进的,考虑的场景也很周全.Glide 的缓存分为两种,一是内存缓存,另一个是硬盘缓存. 这两种缓存的作用各不相同,内存缓存的主要作用是防止应用 ...

最新文章

  1. Entity Framework技术系列之2:三种开发模式实现数据访问
  2. MyEclipse编码设置,中文乱码解决方法,UTF-8,GBK(转)
  3. mysql 1418 存储过程_MySQL自定义函数 1418报错
  4. java支持闭包_JAVA 需要引入闭包吗
  5. QT界面大小自动变化
  6. 自媒体玩到最后玩的是一种意识
  7. Python程序设计学习笔记-语句与格式化输出
  8. 检测到python编程环境中存在多个版本_windows配置Python多版本共存
  9. 利用自定义注解,AOP + redis限制ip访问接口次数
  10. 【学习】从HttpClient3迁移到HttpClient4
  11. linux 消息队列API
  12. MAC 下如何更改brew源地址
  13. 焦距相关的基本概念及焦距对摄影效果的影响
  14. 《重说中国近代史》—张鸣—(3)两个世界最初的碰撞(续)
  15. android 10.0 Camera2 去掉后置摄像头 仅支持前置摄像头功能
  16. 在html里怎么给表单加上边框,html如何给table表单加边框
  17. Canny算子中的梯度求取及非最大值抑制(NMS)实现
  18. JDK15已发布!网友:我还在JDK8踏步走...
  19. Python Web前端实战案例——电商网站商品菜单导航栏
  20. Java系列课程第二十二天(网络编程、正则表达式)

热门文章

  1. 图解DotNet框架之三:System.IO
  2. NHibernate中Example类使用注意事项
  3. 400是什么错误_404、403、405、500 | 常见网页错误代码解析
  4. Nginx服务器中的Socket切分,需要的朋友可以参考下
  5. Access denied (403) see security.limit_extensions
  6. SpringCloud学习之Hystrix
  7. 一种基于flex的可视化多层流量切分界面的实现
  8. 仿微信选项卡主页面创建
  9. 腾讯正式对外开源高性能 RPC 开发框架与微服务平台Tars
  10. 4-MSP430定时器_定时器中断