一次降低进程IO延迟的性能优化实践——基于block层bfq调度器

如果有个进程正频繁的读写文件,此时你vim查看一个新文件,将会出现明显卡顿。即便你vim查看的文件只有几十M,也可能会出现卡顿。相对的,线上经常遇到IO敏感进程偶发IO超时问题。这些进程一次读写的文件数据量很少,正常几十ms就能搞定,但是超时一次读写文件竟耗时几百ms!为什么会这样?出问题的时间点IO流量很大,磁盘IO使用率util接近100%,磁盘IO带宽占满了,IO压力太大。

原来IO敏感进程是受其他进程频繁读写文件影响导致的IO超时,怎么解决这个问题呢?磁盘选用nvme,进程的IO优先级iorenice设置实时优先级,可以一定程度缓解磁盘IO压力大场景IO敏感进程的IO超时问题,但是还是有问题!很好复现,磁盘nvme、IO调度算法bfq、启动fio压测(10个线程,128k随机写),cat读取200M大小的文件(cat进程的IO优先级设置为实时),耗时竟然会达到800ms多,而在IO空闲时只耗时200ms左右!

为什么会这样?如果你用iostat看下fio压测时的io wait(平均IO延迟)数据,发现打印的io wait 达到50ms是家常便饭。而我用systemtap抓取一下nvme盘此时DC耗时(IO请求在磁盘驱动层花费的时间)大于100ms的IO请求,竟然是会频繁打印,说明fio压测时有很多IO请求在nvme磁盘驱动的耗时都很大。调试显示,nvme磁盘驱动队列深度是1024,就是说驱动队列最多可以容纳1024个IO请求,一个128K大小的IO请求传输完成耗时50us,这1024个IO请求传输完成需耗时1024*50us=50ms。fio压测时大部分时间nvme磁盘驱动队列都是占满的,此时cat读取文件,cat进程发送的每个IO请求,大概率都排在nvme磁盘驱动队列尾,都要等队列前边fio进程的IO请求传输完成。如此,cat进程有很多IO请求在磁盘驱动层的耗时都达到50ms左右,那怪不得fio压测时cat读取文件慢了很多。

能否改善这种情况呢?磁盘nvme、IO优先级设置为实时也没用!能否在cat读取文件过程,控制nvme磁盘驱动队列的IO请求数,不要占满,比如nvme磁盘驱动队列的IO请求数控制在100。这样fio压测时,因为nvme磁盘驱动队列的IO请求数不超过100,此时cat读取文件时,cat进程的IO请求即便不幸插入到nvme磁盘驱动队列尾,这个IO请求传输完成最大耗时也只有100*50us=5ms。如果能达到这种效果,IO压力大时IO敏感进程IO超时问题就能得到明显改善了。

按照这个思路目前已经实现了预期效果,本文主要介绍设计思路。这个设计思路是在bfq算法基础上实现的,核心思想是控制派发给nvme磁盘驱动的IO请求数,不超过某个阀值。思路很简单,但是开发过程遇到的问题是个血泪史!本文基于centos 8.3,内核版本4.18.0-240.el8,探索下bfq算法,详细源码注释见 https://github.com/dongzhiyan-stack/linux-4.18.0-240.el8。

注意,本文将IO请求简称rq或者req。另外本文的测试环境是centos 8.3虚拟机。阅读本文前,希望读者先看看我写的《linux内核block层Multi queue多队列核心点分析》。这篇文章是针对block层Multi queue(简称blk-mq) 多队列基础知识点总结。

1:核心优化思路

先看一次普通的读文件触发的IO派发流程:

  • [ffffb71980cbb6b8] scsi_queue_rq at ffffffffb71d1a51
  • [ffffb71980cbb708] blk_mq_dispatch_rq_list at ffffffffb7009f4c
  • [ffffb71980cbb7d8] blk_mq_do_dispatch_sched at ffffffffb700f4ba
  • [ffffb71980cbb830] __blk_mq_sched_dispatch_requests at ffffffffb700ff99
  • [ffffb71980cbb890] blk_mq_sched_dispatch_requests at ffffffffb7010020
  • [ffffb71980cbb8a0] __blk_mq_run_hw_queue at ffffffffb70076a1
  • [ffffb71980cbb8b8] __blk_mq_delay_run_hw_queue at ffffffffb7007f61
  • [ffffb71980cbb8e0] blk_mq_sched_insert_requests at ffffffffb7010351
  • [ffffb71980cbb918] blk_mq_flush_plug_list at ffffffffb700b4d6
  • [ffffb71980cbb998] blk_flush_plug_list at ffffffffb6fffbe7
  • [ffffb71980cbb9e8] blk_mq_make_request at ffffffffb700ad38
  • [ffffb71980cbba78] generic_make_request at ffffffffb6ffe85f
  • [ffffb71980cbbad0] submit_bio at ffffffffb6ffeadc
  • [ffffb71980cbbb10] ext4_mpage_readpages at ffffffffc081b9a4 [ext4]
  • [ffffb71980cbbbf8] read_pages at ffffffffb6e3743b
  • [ffffb71980cbbc70] __do_page_cache_readahead at ffffffffb6e37721
  • [ffffb71980cbbd08] ondemand_readahead at ffffffffb6e37939
  • [ffffb71980cbbd50] generic_file_buffered_read at ffffffffb6e2ce5f
  • [ffffb71980cbbe40] new_sync_read at ffffffffb6ed8841
  • [ffffb71980cbbec8] vfs_read at ffffffffb6edb1c1

可以发现,派发IO最后的流程是__blk_mq_sched_dispatch_requests->blk_mq_do_dispatch_sched->blk_mq_dispatch_rq_list,也与本次的性能优化有关。看下blk_mq_do_dispatch_sched函数源码

  1. static int blk_mq_do_dispatch_sched**(** struct blk_mq_hw_ctx * hctx**)**
  2. {
  3. struct request_queue * q = hctx**->** queue**;**
  4. struct elevator_queue * e = q**->** elevator**;**
  5. LIST_HEAD**(** rq_list**);**
  6. int ret = 0**;**
  7. do {
  8. struct request * rq**;**
  9. //bfq_has_work
  10. if ( e**->** type**->** ops**.** has_work && ! e**->** type**->** ops**.** has_work**(** hctx**))**
  11. break ;
  12. if (! list_empty_careful**(&** hctx**->** dispatch**))** {
  13. ret = - EAGAIN**;**
  14. break ;
  15. }
  16. if (! blk_mq_get_dispatch_budget**(** hctx**))**
  17. break ;
  18. //调用bfq调度器IO派发函数bfq_dispatch_request
  19. rq = e**->** type**->** ops**.** dispatch_request**(** hctx**);**
  20. if (! rq**)** {
  21. blk_mq_put_dispatch_budget**(** hctx**);**
  22. blk_mq_delay_run_hw_queues**(** q**,** BLK_MQ_BUDGET_DELAY**);**
  23. break ;
  24. }
  25. list_add**(&** rq**->** queuelist**,** & rq_list**);**
  26. /*取出rq_list链表上的req派发给磁盘驱动,如果因驱动队列繁忙或者nvme硬件繁忙导致派发失败,则把rq添加hctx->dispatch等稍后派发遇到rq派发失败返回false,退出while循环*/
  27. } while ( blk_mq_dispatch_rq_list**(** q**,** & rq_list**,** true ));
  28. return ret**;**
  29. }

该函数作用是:执行bfq_dispatch_request()函数循环从IO调度器队列取出IO请求存入rq_list链表,然后取出rq_list链表上的rq执行blk_mq_dispatch_rq_list()派发给磁盘驱动。blk_mq_dispatch_rq_list()函数如果因驱动队列繁忙或者磁盘硬件繁忙导致派发失败则返回false,此时blk_mq_do_dispatch_sched()函数退出while循环。当然,如果IO调度器队列没IO请求了,bfq_dispatch_request返回NULL,此时blk_mq_do_dispatch_sched()函数也会退出while循环。把blk_mq_dispatch_rq_list源码简单列下:

  1. bool blk_mq_dispatch_rq_list**(** struct request_queue * q**,** struct list_head * list**,**
  2. bool got_budget**)**
  3. {
  4. struct blk_mq_hw_ctx * hctx**;**
  5. struct request * rq**,** * nxt**;**
  6. bool no_tag = false ;
  7. int errors**,** queued**;**
  8. blk_status_t ret = BLK_STS_OK**;**
  9. bool no_budget_avail = false ;
  10. ................
  11. errors = queued = 0**;**
  12. do {
  13. struct blk_mq_queue_data bd**;**
  14. rq = list_first_entry**(** list**,** struct request**,** queuelist**);**
  15. hctx = rq**->** mq_hctx**;**
  16. ................
  17. list_del_init**(&** rq**->** queuelist**);**
  18. bd**.** rq = rq**;**
  19. if ( list_empty**(** list**))**
  20. bd**.** last = true ;
  21. else {
  22. nxt = list_first_entry**(** list**,** struct request**,** queuelist**);**
  23. bd**.** last = ! blk_mq_get_driver_tag**(** nxt**);**
  24. }
  25. //把rq派发给驱动
  26. ret = q**->** mq_ops**->** queue_rq**(** hctx**,** & bd**);** //scsi_queue_rq 或 nvme_queue_rq
  27. //这个if成立应该说明是 驱动队列繁忙 或者nvme硬件繁忙,不能再向驱动派发IO,因此本次的rq派发失败
  28. if ( ret == BLK_STS_RESOURCE || ret == BLK_STS_DEV_RESOURCE**)** {
  29. if (! list_empty**(** list**))** {
  30. //把rq在list链表上的下一个req的tag释放了,搞不清楚为什么
  31. nxt = list_first_entry**(** list**,** struct request**,** queuelist**);**
  32. blk_mq_put_driver_tag**(** nxt**);**
  33. }
  34. //把派发失败的rq再添加到list链表
  35. list_add**(&** rq**->** queuelist**,** list**);**
  36. __blk_mq_requeue_request**(** rq**);**
  37. break ;
  38. }
  39. ...........
  40. //派发rq失败则queued加1
  41. queued**++;**
  42. //一直派发list链表上的req直到list链表空
  43. } while (! list_empty**(** list**));**
  44. hctx**->** dispatched**[** queued_to_index**(** queued**)]++;**
  45. //如果list链表上还有rq,说明派发rq时遇到驱动队列或者硬件繁忙,rq没有派发成功
  46. if (! list_empty**(** list**))** {
  47. ...........
  48. spin_lock**(&** hctx**->** lock**);**
  49. //list上没有派发成功的rq添加到hctx->dispatch链表,稍后延迟派发
  50. list_splice_tail_init**(** list**,** & hctx**->** dispatch**);**
  51. spin_unlock**(&** hctx**->** lock**);**
  52. ......................
  53. blk_mq_update_dispatch_busy**(** hctx**,** true );
  54. return false ;
  55. } else
  56. blk_mq_update_dispatch_busy**(** hctx**,** false );
  57. //派发rq时遇到驱动队列或者硬件繁忙,返回false,否则派发正常下边返回true
  58. if ( ret == BLK_STS_RESOURCE || ret == BLK_STS_DEV_RESOURCE**)**
  59. return false ;
  60. return ( queued + errors**)** != 0**;**
  61. }

该函数只是取出list链表上的rq派发给磁盘驱动,如果因驱动队列繁忙或者磁盘硬件繁忙导致派发失败,则把rq添加hctx->dispatch等稍后派发。本文的IO优化算法是在bfq算法基础上实现的,最好先对bfq算法有个了解,希望重点看下《内核block层IO调度器---bfq算法之1整体流程介绍》、《内核block层IO调度器---bfq算法之3源码要点总结》、《内核block层IO调度器---bfq算法深入探索2》这3篇文章。

bfq算法把进程传输的IO归为3类,in_large_burst型IO、交互式IO、实时性IO。fio这种短时间多个线程派发IO的属于in_large_burst型IO,进程偶尔读写一次文件且数据量不大的属于交互式IO,进程周期性的读写文件且数据量不大的属于实时性IO。这3种IO模型的对IO时延要求依次增加, bfq算法定义了bfqq->wr_coeff变量这个权重系数来表达这种特性,针对这3中IO模型依次是1、30、30* 100。bfqq->wr_coeff越大,派发IO的进程绑定的bfqq插入st->active tree(可以理解成IO运行队列)越靠左,这样可以更早被bfq调度器调度选中,进而更早得到派发该bfqq对应进程的IO,保证了低延迟。

本案例的场景是,在IO压力大时怎么降低IO敏感进程的时延。怎么模拟这种场景呢?fio压测模拟IO压力大,然后cat kern读取文件(kern文件几百M)作为IO敏感进程。在开启fio压测下cat kern读取文件,观察cat kern耗时。在磁盘空闲时,cat kern只耗时不到100ms。在开启fio压测情况,cat kern耗时500ms+。如果我的IO优化方案生效,则需要实现在开启fio压测情况下,cat kern耗时小于500ms,比如200ms、300ms。这是虚拟机里的测试数据,每次不太稳定。

ok,具体代码在何处实现呢?首先是把IO请求插入bfq IO算法队列执行的bfq_insert_request()->__bfq_insert_request()->bfq_add_request()函数,添加如下红色代码:

  1. /*高优先级rq*/
  2. #define RQF_HIGH_PRIO ((__force req_flags_t)(1 << 21))
  3. static void bfq_add_request**(** struct request * rq**)**
  4. {
  5. if (! bfq_bfqq_busy**(** bfqq**)){**
  6. bfq_bfqq_handle_idle_busy_switch**(** bfqd**,** bfqq**,** old_wr_coeff**,** rq**,** & interactive**);**
  7. }
  8. ..............
  9. if( bfqq**->** wr_coeff == 30**){**
  10. //设置rq高优先级
  11. rq**->** rq_flags |= RQF_HIGH_PRIO**;**
  12. }
  13. }

if(bfqq->wr_coeff == 30)成立说明当前IO传输的进程绑定的bfqq拥有高优先级rq属性,则执行rq->rq_flags |= RQF_HIGH_PRIO对rq设置高优先级rq标志。

这里插一句,本文的测试环境是,在fio压测情况观察cat kern读取文件的耗时。bfq算法中,针对fio这种频繁派发IO的进程,fio进程属于burst型IO,它的进程的bfqq对应的bfqq->wr_coeff大部分情况是1。而针对cat这种偶尔读取一次文件的进程,是交互式IO,该进程的bfqq的bfqq->wr_coeff初值是30。显然,cat kern读取文件过程,cat进程派发的IO大部分拥有高优先级rq属性,这是本文的IO性能优化方案的设计思路。

接着是从bfq IO算法队列派发IO请求执行的blk_mq_dispatch_rq_list(),源码有删减,红色是性能优化添加的代码:

  1. static struct request * __bfq_dispatch_request**(** struct blk_mq_hw_ctx * hctx**)**
  2. {
  3. struct bfq_data * bfqd = hctx**->** queue**->** elevator**->** elevator_data**;**
  4. struct request * rq = NULL ;
  5. struct bfq_queue * bfqq = NULL ;
  6. int direct_dispatch = 0**;**
  7. //不经IO算法队列,直接派发的rq
  8. if (! list_empty**(&** bfqd**->** dispatch**))** {
  9. rq = list_first_entry**(&** bfqd**->** dispatch**,** struct request**,** queuelist**);**
  10. list_del_init**(&** rq**->** queuelist**);**
  11. bfqq = RQ_BFQQ**(** rq**);**
  12. direct_dispatch = 1**;**
  13. if ( bfqq**)** {
  14. bfqq**->** dispatched**++;**
  15. goto inc_in_driver_start_rq**;**
  16. }
  17. goto start_rq**;**
  18. }
  19. .....................
  20. bfqq = bfq_select_queue**(** bfqd**);**
  21. if (! bfqq**)**
  22. goto exit**;**
  23. rq = bfq_dispatch_rq_from_bfqq**(** bfqd**,** bfqq**);**
  24. if ( rq**)** {
  25. if( bfqd**->** queue**->** high_io_prio_enable**)**
  26. {
  27. if( rq**->** rq_flags & RQF_HIGH_PRIO**){**//高优先级IO
  28. //第一次遇到high prio io,置1 bfq_high_io_prio_mode,启动3s定时器,定时到了对bfq_high_io_prio_mode清0
  29. if( bfqd**->** bfq_high_io_prio_mode == 0**){**
  30. bfqd**->** bfq_high_io_prio_mode = 1**;**
  31. hrtimer_start**(&** bfqd**->** bfq_high_prio_timer**,** ms_to_ktime**(** 3000**),** HRTIMER_MODE_REL**);**
  32. }
  33. }
  34. else非高优先级IO
  35. {
  36. if( bfqd**->** bfq_high_io_prio_mode**)**
  37. {
  38. //在 bfq_high_io_prio_mode 非0时间的5s内,如果遇到非high prio io,并且驱动队列IO个数大于限制,则把不派发该IO,而是临时添加到bfq_high_prio_tmp_list链表
  39. if(( bfqd**->** rq_in_driver >= 20**)** /*&& ( bfqd**->** bfq_high_prio_tmp_list_rq_count < 100**)*/){**
  40. list_add_tail**(&** rq**->** queuelist**,&** bfqd**->** bfq_high_prio_tmp_list**);**
  41. //bfq_high_prio_tmp_list链表上rq的个数加1
  42. bfqd**->** bfq_high_prio_tmp_list_rq_count ++;
  43. rq = NULL**;**
  44. goto exit1**;**
  45. }
  46. }
  47. }
  48. }
  49. /*如果 bfq_high_prio_tmp_list 链表上有rq要派发,不执行这里的rq_in_driver++,在下边的exit那里会执行。当echo 0 >/sys/block/sdb/process_high_io_prio 置1再置0后,这个if判断就起作用了。没这个判断,这里会bfqd->rq_in_driver++,下边的if里再bfqd->rq_in_driver++,导致rq_in_driver泄漏*/
  50. if(( rq**->** rq_flags & RQF_HIGH_PRIO**)** || list_empty**(&** bfqd**->** bfq_high_prio_tmp_list**)){**
  51. inc_in_driver_start_rq**:**
  52. bfqd**->** rq_in_driver**++;**
  53. start_rq**:**
  54. rq**->** rq_flags |= RQF_STARTED**;**
  55. }
  56. }
  57. exit**:**
  58. //1:如果是高优先级IO该if不成立,直接跳过。 2:如果非高优先级IO,则把rq添加到bfq_high_prio_tmp_list尾,从链表头选一个rq派发 3:如果rq是NULL,则也从bfq_high_prio_tmp_list选一个rq派发
  59. if(! direct_dispatch && (( rq && !( rq**->** rq_flags & RQF_HIGH_PRIO**))** || ! rq**)){**
  60. /*如果bfq_high_prio_tmp_list有Io, 则不派发本次的io而添加到bfq_high_prio_tmp_list尾部,实际从bfq_high_prio_tmp_list链表头取出一个IO派发。放到 if(bfqd->queue->high_io_prio_enable)外边是为了保证一旦设置high_io_prio_enable为0,还能派发残留的在bfq_high_prio_tmp_list上的IO*/
  61. if(! list_empty**(&** bfqd**->** bfq_high_prio_tmp_list**)){**
  62. if( rq**){**
  63. list_add_tail**(&** rq**->** queuelist**,&** bfqd**->** bfq_high_prio_tmp_list**);**
  64. bfqd**->** bfq_high_prio_tmp_list_rq_count ++;
  65. }
  66. rq = list_first_entry**(&** bfqd**->** bfq_high_prio_tmp_list**,** struct request**,** queuelist**);**
  67. list_del_init**(&** rq**->** queuelist**);**
  68. //bfq_high_prio_tmp_list链表上rq的个数减1
  69. bfqd**->** bfq_high_prio_tmp_list_rq_count --;
  70. bfqd**->** rq_in_driver**++;**
  71. rq**->** rq_flags |= RQF_STARTED**;**
  72. }
  73. }
  74. exit1**:**
  75. ..................
  76. return rq**;**
  77. }

该函数中,首先执行bfqq = bfq_select_queue(bfqd)算法本次派发rq的bfqq,然后执行rq = bfq_dispatch_rq_from_bfqq(bfqd, bfqq)从bfqq的IO队列取出本次派发的IO请求。后边的就是针对本次性能优化添加的代码。bfqd->queue->high_io_prio_enable是一个使能开关,执行echo 1 >/sys/block/sdb/process_high_io_prio才会打开本文的性能优化功能。继续,如果派发的rq有高优先级属性(即rq->rq_flags & RQF_HIGH_PRIO返回true),则bfqd->bfq_high_io_prio_mode = 1置1,这是进入派发高优先级IO的开始标志。然后执行hrtimer_start(&bfqd->bfq_high_prio_timer, ms_to_ktime(3000),HRTIMER_MODE_REL)启动3s定时器,3s后在定时器函数里令bfqd->bfq_high_io_prio_mode = 0,这是派发高优先级IO的结束标志。

ok,在第一次遇到派发的rq有高优先级属性后,就会令bfqd->bfq_high_io_prio_mode = 1置1并进入" 派发高优先级IO"的3s时期。这段时间只有rq有高优先级属性才会会作为__bfq_dispatch_request()返回的rq,真正得到机会派发给磁盘驱动。否则,普通的rq就要执行list_add_tail(&rq->queuelist,&bfqd->bfq_high_prio_tmp_list)暂时添加到bfqd->bfq_high_prio_tmp_list链表,延迟派发,当然前提要有bfqd->rq_in_driver >= 20成立,就是说派发给磁盘驱动但还没传输完成的IO数要达到某个阀值(我在虚拟机里测试的sda机械盘磁盘队列深度是32,nvme盘队列深度达到1000多,建议这个阀值达到磁盘队列深度的60%以上)。

为什么要这么设计?其实就是要在派发给磁盘驱动但还没传输完成的IO数达到磁盘队列深度的某个阀值后(之后再派发IO可能就会把磁盘驱动IO队列占满了),此时正好有进程要派发IO敏感的IO请求(这些IO请求rq标记有RQF_HIGH_PRIO属性),优先派发IO敏感进程的IO,延迟派发普通进程的IO(就是把这些rq暂时添加到bfqd->bfq_high_prio_tmp_list链表)。等系统空闲后,IO敏感进程的IO都派发完了,再从bfqd->bfq_high_prio_tmp_list链表取出延迟派发的IO而继续派发。

简单说,在普通进程和IO敏感进程同时派发IO时,在普通进程的IO把磁盘驱动IO队列快占满前,限制普通进程向磁盘驱动IO队列派发的IO数,防止把磁盘驱动IO队列占满。此时呢,要优先派发IO敏感进程的IO到磁盘驱动队列的IO。通过这个方法,防止在IO压力很大时影响IO敏感进程派发IO的时延。

2: 实现IO性能优化效果的曲折过程

开始测试,虚拟机centos 8.3系统。先执行echo 1 >/sys/block/sdb/process_high_io_prio打开本文的IO性能优化功能。然后启动fio压测,同时time cat kern > /dev/null读取文件并打印耗时(kern文件大小300M)。没想到,竟然一点效果没有!以下是测试数据

  • 1:echo 1 >/sys/block/sdb/process_high_io_prio打开IO性能优化功能,开启fio压测,cat kern耗时500ms左右,偶尔会出现耗时800ms甚至1s
  • 2:echo 0 >/sys/block/sdb/process_high_io_prio关闭IO性能优化功能,开启fio压测,cat kern耗时500ms左右,偶尔会出现耗时800ms甚至1s
  • 3:echo 1 >/sys/block/sdb/process_high_io_prio打开IO性能优化功能,关闭fio压测,cat kern耗时不到100ms

总结下,在磁盘IO空闲时,cat kern耗时不到100ms,而在fio压测情况下,开启和关闭IO性能优化,cat kern耗时没有区别。甚至,多次测试后,发现开启IO性能比关闭IO性能优化,cat kern更耗时。这就说明,本文的IO性能优化方案不仅没起到作用,反而拖了后腿!这就需要找下原因了!

此时,在之前"统计进程派发IO的延迟"功能的帮助下,发现开启IO性能优化功能时,启动fio压测,cat kern读取文件派发IO过程,cat进程的id耗时(IO请求在IO队列的耗时)明显偏大, id耗时(IO请求在磁盘驱动层的耗时)也没有缩短。再进一步排查,发现在fio压测时,当cat进程有IO要派发而插入bfq 的IO算法队列后,cat进程的bfqq竟然经常出现过了10ms+才得到调度机会!就是说,fio压测时,当cat进程要派发IO时,fio一直占着IO派发机会,cat进程推迟10ms+才得到派发IO机会。

怎么解决这个问题?首要目的是降低cat kern进程的延迟!就是要让cat进程的来了IO请求后,尽快得到调度派发。怎么实现,需要增大cat进程的bfqq->wr_coeff,这样cat进程绑定的bfqq插入st->active tree(可以理解成IO运行队列)后才能尽可能早的被IO调度器选中,进而派发cat进程的IO,得到调度延迟的效果。经过繁琐的调试,这样调整优化方案:

在进程bfqq派发派发IO请求过程,因为配额没了而过期失效,然后重新加入st->active tree执行的__bfq_requeue_entity()函数中:

  1. static void __bfq_requeue_entity**(** struct bfq_entity * entity**)**
  2. {
  3. struct bfq_sched_data * sd = entity**->** sched_data**;**
  4. struct bfq_service_tree * st = bfq_entity_service_tree**(** entity**);**
  5. //如果bfqq->wr_coeff是30说明是交互式io,执行到这里说明派发这个进程派发的IO太多了,配合消耗完了还没派发完io。此时说明该进程的bfqq需要提升权重,提高优先级,作为high prio io.
  6. struct bfq_queue * bfqq = bfq_entity_to_bfqq**(** entity**);**
  7. if( bfqq && bfqq**->** bfqd**->** queue**->** high_io_prio_enable && bfqq**->** wr_coeff == 30**){**
  8. bfqq**->** wr_coeff = 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**;**
  9. //置1表示权重变了,然后才会在bfq_update_fin_time_enqueue->__bfq_entity_update_weight_prio 里真正提升权重
  10. entity**->** prio_changed = 1**;**
  11. //增大权重提升时间为1.5s
  12. bfqq**->** wr_cur_max_time = msecs_to_jiffies**(** 1500**);**
  13. //权重提升时间开始时间为当前时间
  14. bfqq**->** last_wr_start_finish = jiffies**;**
  15. bfqq**->** entity**.** completed_size = 0**;**
  16. }
  17. .............
  18. if ( entity**->** tree**)**
  19. bfq_active_extract**(** st**,** entity**);**
  20. bfq_update_fin_time_enqueue**(** entity**,** st**,** false );
  21. }

cat进程最初派发IO时被判定为交互式IO,bfqq->wr_coeff是30。实际测试表明,cat进程因为派发IO很多导致的bfqq第一次过期失效,是配额耗尽而过期失效。此时cat进程的bfqq是要重新插入st->active tree而等待bfq调度器再次被选中派发IO,执行的正是__bfq_requeue_entity()函数!在__bfq_requeue_entity()函数中,发现cat进程bfqq的bfqq->wr_coeff是30,就增大bfqq->wr_coeff为bfqq->wr_coeff = 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR,BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR是50。

还有一个重点是bfqq->wr_cur_max_time = msecs_to_jiffies(1500),这是cat进程的bfqq权重系数增大为30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR的时间期限,bfqq->last_wr_start_finish = jiffies是cat进程的bfqq权重系数增大为30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR的起始时间。这样设置过后,从当前时间起的1.5s内,cat进程的bfqq->wr_coeff的都是30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR!这样的效果就是,这段时间cat进程的bfqq插入st->active tree后能尽可能被bfq调度器选中派发IO,大大降低延迟!

在插入IO请求函数bfq_add_request()中,遇到bfqq->wr_coeff是30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR的进程bfqq,才会把该bfqq的IO设置高优先级标志RQF_HIGH_PRIO。这样是为了过滤bfqq->wr_coeff是30的进程的IO,不让这种IO被判定为高优先级IO。

  1. #define BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR 50
  2. static void bfq_add_request**(** struct request * rq**)**
  3. {
  4. if (! bfq_bfqq_busy**(** bfqq**)){**
  5. bfq_bfqq_handle_idle_busy_switch**(** bfqd**,** bfqq**,** old_wr_coeff**,** rq**,** & interactive**);**
  6. }
  7. .............
  8. if( bfqq**->** wr_coeff == 30***** BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**){**
  9. //设置rq高优先级
  10. rq**->** rq_flags |= RQF_HIGH_PRIO**;**
  11. }
  12. }

在cat进程因没有IO请求派发而过期失效,加入st->idle tree。然后过了一段时间又来了新的IO请求,此时需要执行bfq_add_request()->bfq_bfqq_handle_idle_busy_switch()激活cat进程的bfqq,把bfqq插入st->active tree。在这个函数中强制cat进程的bfqq->wr_coeff保持30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR,不受bfq_bfqq_handle_idle_busy_switch()原生代码的影响,具体实现看如下红色代码。

  1. static void bfq_bfqq_handle_idle_busy_switch**(** struct bfq_data * bfqd**,**
  2. struct bfq_queue * bfqq**,**
  3. int old_wr_coeff**,**
  4. struct request * rq**,**
  5. bool * interactive**)**
  6. {
  7. //禁止high prio io进程被判定为rt、interactive 、burst 型io,这样下边的bfq_update_bfqq_wr_on_rq_arrival()函数不会修改它的 bfqq->wr_coeff
  8. if( bfqq**->** wr_coeff == 30***** BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**){**
  9. * interactive = 0**;**
  10. wr_or_deserves_wr = 0**;**
  11. in_burst = 0**;**
  12. soft_rt = 0**;**
  13. }
  14. ................
  15. bfq_update_bfqq_wr_on_rq_arrival**(** bfqd**,** bfqq**,**
  16. old_wr_coeff**,**
  17. wr_or_deserves_wr**,**
  18. * interactive**,**
  19. in_burst**,**
  20. soft_rt**);**
  21. ................
  22. }

在cat进程的bfqq被bfq调度器选中派发IO后,每次执行派发IO执行__bfq_dispatch_request()->bfq_dispatch_rq_from_bfqq()->bfq_update_wr_data()过程,都会检查cat进程的bfqq权重提升时间是否到了,到了的话就要令bfqq的权重提升时间结束,令bfqq->wr_coeff重置为1,之后cat进程的bfqq就不再享有低延时派发特性了。在结束进程权重提升bfq_update_wr_data()函数需要添加如下红色代码,否则会导致cat进程的bfqq的bfqq->wr_coeff被设置为30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR后,很短时间就会执行里边的bfq_bfqq_end_wr()令bfqq->wr_coeff重置为1。

  1. static void bfq_update_wr_data**(** struct bfq_data * bfqd**,** struct bfq_queue * bfqq**)**
  2. {
  3. struct bfq_entity * entity = & bfqq**->** entity**;**
  4. if ( bfqq**->** wr_coeff > 1**)** {
  5. ...............
  6. if ( bfq_bfqq_in_large_burst**(** bfqq**)){**
  7. bfq_bfqq_end_wr**(** bfqq**);**
  8. }
  9. else if ( time_is_before_jiffies**(** bfqq**->** last_wr_start_finish +
  10. bfqq**->** wr_cur_max_time**))** {
  11. if ( bfqq**->** wr_cur_max_time != bfqd**->** bfq_wr_rt_max_time ||
  12. time_is_before_jiffies**(** bfqq**->** wr_start_at_switch_to_srt +
  13. bfq_wr_duration**(** bfqd**)))**
  14. {
  15. bfq_bfqq_end_wr**(** bfqq**);**
  16. }
  17. else {
  18. switch_back_to_interactive_wr**(** bfqq**,** bfqd**);**
  19. bfqq**->** entity**.** prio_changed = 1**;**
  20. }
  21. }
  22. if ( bfqq**->** wr_coeff > 1 &&
  23. bfqq**->** wr_cur_max_time != bfqd**->** bfq_wr_rt_max_time &&
  24. bfqq**->** service_from_wr > max_service_from_wr &&
  25. bfqq**->** wr_coeff != 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**)** //high prio io进程禁止在这里结束权重提升
  26. {
  27. bfq_bfqq_end_wr**(** bfqq**);**
  28. }
  29. }
  30. if (( entity**->** weight > entity**->** orig_weight**)** != ( bfqq**->** wr_coeff > 1**)){**
  31. __bfq_entity_update_weight_prio**(** bfq_entity_service_tree**(** entity**),**
  32. entity**,** false );
  33. }
  34. }

在派发IO请求的bfq_dispatch_rq_from_bfqq()函数添加如下代码:

  1. static struct request * bfq_dispatch_rq_from_bfqq**(** struct bfq_data * bfqd**,**
  2. struct bfq_queue * bfqq**)**
  3. {
  4. struct request * rq = bfqq**->** next_rq**;**
  5. unsigned long service_to_charge**;**
  6. service_to_charge = bfq_serv_to_charge**(** rq**,** bfqq**);**
  7. bfq_bfqq_served**(** bfqq**,** service_to_charge**);**
  8. bfq_dispatch_remove**(** bfqd**->** queue**,** rq**);**
  9. if ( bfqq != bfqd**->** in_service_queue**)**
  10. goto return_rq**;**
  11. if ( bfqd**->** queue**->** high_io_prio_enable**){**
  12. if ( bfqq**->** wr_coeff == 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**){**
  13. //累加bfqq传输完成的rq的数据量,如果bfqq传输数据量太多而超过限制,强制令进程bfqq不再有high prio io属性
  14. bfqq**->** entity**.** completed_size += blk_rq_bytes**(** rq**);**
  15. if ( bfqq**->** entity**.** completed_size > bfqd**->** high_prio_io_all_size_limit**){**
  16. bfq_bfqq_end_wr**(** bfqq**);**
  17. }
  18. }
  19. }
  20. bfq_update_wr_data**(** bfqd**,** bfqq**);**
  21. ...................
  22. }

这是令被判定为高优先级IO的进程派发的数据量超过bfqd->high_prio_io_all_size_limit阀值(200M或者300M)后,就结束该进程的高优先级IO属性,具体是执行bfq_bfqq_end_wr(bfqq)令bfqq->wr_coeff由30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR降低为1,这就是普通IO了。这样做是为了防止fio这种频繁数据传输IO的进程被长时间判定为高优先级IO,因为fio进程最初派发IO时,被判定为交互式IO,fqq->wr_coeff = 30。然后因配额耗尽而执行__bfq_requeue_entity()重新加入st->active tree时,因为bfqq->wr_coeff 是30,则fqq->wr_coeff = 30* BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR。这样fio进程就被判定为高优先级IO了!这个是没办法避免的,但是等fio派发IO的数据量超过bfqd->high_prio_io_all_size_limit,就强制令fio结束高优先级IO属性。

这样终于实现了IO性能优化效果, echo 1 >/sys/block/sdb/process_high_io_prio打开IO性能优化功能,开启fio压测,cat kern耗时只有200ms左右:

  • 1:echo 1 >/sys/block/sdb/process_high_io_prio打开IO性能优化功能,开启fio压测,cat kern耗时200ms左右,偶尔会出现耗时800m,但出现概率低
  • 2:echo 1 >/sys/block/sdb/process_high_io_prio关闭IO性能优化功能,开启fio压测,cat kern耗时200ms左右,但设置cat进程的IO调度算法为RT,偶尔会出现耗时800ms,但出现概率更高

可以发现,本文的性能优化效果比设置IO调度算法为RT更优。这说明本文的IO性能优化算法------降低磁盘驱动队列深度而降低IO敏感进程的IO在磁盘驱动的耗时,终于起到了作用!因为这是在虚拟机里做的测试,性能不太稳定。如果在PC本地测试,性能稳定很多,但是测试规律跟上边一致。

3:其他优化方案

如上方案终于实现了预期效果,但是还有还有其他性能优化点。主要是在IO请求插入IO队列的bfq_add_request()函数:

  1. #define BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR 50
  2. static void bfq_add_request**(** struct request * rq**)**
  3. {
  4. if (! bfq_bfqq_busy**(** bfqq**)){**
  5. bfq_bfqq_handle_idle_busy_switch**(** bfqd**,** bfqq**,** old_wr_coeff**,** rq**,** & interactive**);**
  6. }
  7. ..............
  8. //如果同一个线程组的进程近期有in_large_burst属性,禁止它新创建的线程被判定为交互式io
  9. if( bfq_bfqq_in_large_burst**(** bfqq**)){**
  10. if( current**->** tgid != bfqd**->** large_burst_process_tgid**){**
  11. bfqd**->** large_burst_process_tgid = current**->** tgid**;**
  12. strncpy**(** bfqd**->** large_burst_process_name**,** current**->** comm**,** COMM_LEN**-** 1**);**
  13. bfqd**->** large_burst_process_count = 0**;**
  14. }else{
  15. bfqd**->** large_burst_process_count ++;
  16. }
  17. }
  18. if( bfqq**->** wr_coeff == 30***** BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**){**
  19. //设置rq高优先级
  20. rq**->** rq_flags |= RQF_HIGH_PRIO**;**
  21. }
  22. }

把IO请求插入IO队列,把进程bfqq激活插入st->active tree执行的bfq_bfqq_handle_idle_busy_switch()函数中,添加如下代码:

  1. static void bfq_bfqq_handle_idle_busy_switch**(** struct bfq_data * bfqd**,**
  2. struct bfq_queue * bfqq**,**
  3. int old_wr_coeff**,**
  4. struct request * rq**,**
  5. bool * interactive**)**
  6. {
  7. bool soft_rt**,** in_burst**,** wr_or_deserves_wr**,**
  8. bfqq_wants_to_preempt**,**
  9. idle_for_long_time = bfq_bfqq_idle_for_long_time**(** bfqd**,** bfqq**),**
  10. ...................
  11. in_burst = bfq_bfqq_in_large_burst**(** bfqq**);**
  12. soft_rt = bfqd**->** bfq_wr_max_softrt_rate > 0 &&
  13. ! BFQQ_TOTALLY_SEEKY**(** bfqq**)** &&
  14. ! in_burst &&
  15. time_is_before_jiffies**(** bfqq**->** soft_rt_next_start**)** &&
  16. bfqq**->** dispatched == 0**;**
  17. * interactive = ! in_burst && idle_for_long_time**;**
  18. //如果同一个线程组的进程近期有in_large_burst属性,禁止它新创建的线程被判定为交互式io
  19. if(( bfqd**->** large_burst_process_count > 1**)** &&( bfqd**->** large_burst_process_tgid == current**->** tgid**)** && ( strncmp**(** bfqd**->** large_burst_process_name**,** current**->** comm**,** COMM_LEN**-** 1**)** == 0**)){**
  20. * interactive = 0**;**
  21. soft_rt = 0**;**
  22. in_burst = 1**;**
  23. bfq_prevent_high_prio_count**++;**
  24. }
  25. /*该if成立,说明当前进程最近被判定为high prio io。这样等该进程再进程新的IO传输时,强制令该进程被判定为 high prio io。否则,只能被判断为交互式 io。bfqq->bfqq_list 是NULL说明该进程是新创建的。否则可能该bfqq过期失效而处于st->idle tree,现在又派发rq,此时该if不成立。*/
  26. if(( bfqq**->** wr_coeff == 1**)** && list_empty(&bfqq->bfqq_list) && ( strncmp**(** bfqd**->** last_high_prio_io_process**,** current**->** comm**,** COMM_LEN**-** 1**))** == 0**){**
  27. bfqq**->** wr_coeff = 30***** BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**;**
  28. }
  29. wr_or_deserves_wr = bfqd**->** low_latency &&
  30. ( bfqq**->** wr_coeff > 1 ||
  31. ( bfq_bfqq_sync**(** bfqq**)** &&
  32. bfqq**->** bic && (* interactive || soft_rt**)));**
  33. //禁止high prio io进程被判定为rt、interactive 、burst 型io,这样下边的bfq_update_bfqq_wr_on_rq_arrival()函数不会修改它的 bfqq->wr_coeff
  34. if( bfqq**->** wr_coeff == 30***** BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**){**
  35. * interactive = 0**;**
  36. wr_or_deserves_wr = 0**;**
  37. in_burst = 0**;**
  38. soft_rt = 0**;**
  39. //保存最近high prio io进程的名字
  40. strncpy**(** bfqd**->** last_high_prio_io_process**,** current**->** comm**,** COMM_LEN**-** 1**);**
  41. }
  42. ................
  43. bfq_update_bfqq_wr_on_rq_arrival**(** bfqd**,** bfqq**,**
  44. old_wr_coeff**,**
  45. wr_or_deserves_wr**,**
  46. * interactive**,**
  47. in_burst**,**
  48. soft_rt**);**
  49. ................
  50. }

在进程绑定的bfqq初始化函数 bfq_init_bfqq()中,对bfqq->bfqq_list初始化,表示bfqq是新创建的。

static void bfq_init_bfqq**(** struct bfq_data * bfqd**,** struct bfq_queue * bfqq**,**

struct bfq_io_cq * bic**,** pid_t pid**,** int is_sync**)**

{

//bfqq创建时对bfqq->bfqq_list初始化

INIT_LIST_HEAD (& bfqq**->** bfqq_list**);**

}

在bfq_add_request()、bfq_bfqq_handle_idle_busy_switch()中添加的代码的代码,主要是两个作用。

1:保存最近被判定被高优先级IO的进程名字(比如cat)到bfqd->last_high_prio_io_process。后续如果再有同样进程名字的进程派发IO,则立即令进程被判定为高优先级IO。这段代码是bfq_bfqq_handle_idle_busy_switch()函数if((bfqq->wr_coeff == 1) && list_empty(&bfqq->bfqq_list) && (strncmp(bfqd->last_high_prio_io_process,current->comm,COMM_LEN-1)) == 0) bfqq->wr_coeff = 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR这段代码。

注意,正常情况,一个进程最早开始派发IO时,只是被判定为交互式IO,bfqq->wr_coeff只有30。然后该进程被bfq调度器选中派发IO,接着因为配额消耗完而过期失效,执行__bfq_requeue_entity()重新加入st->active tree,等待被bfq调度器重新调度。此时在__bfq_requeue_entity()函数中,因为bfqq->wr_coeff是30,才会判定这个进程被高优先级IO。总之,这优化点是保证进程一开始派发IO就能被判定为高优先级IO,一开始就保证降低IO调度延迟。

2:保存最近被判定为in_large_burst型IO的进程名字到bfqd->large_burst_process_name。这样后续再有同样进程名字的新进程派发IO 或者 原本在st->idle tree但来了新的IO而激活加入st->active tree,这两种情况进程都会被判定为交互式IO,bfqq->wr_coeff 赋值30。然后等bfq调度器选中该进程派发IO后,该进程因为配额消耗光而过期失效,此时是要执行__bfq_requeue_entity()重新加入st->active tree。而在__bfq_requeue_entity()函数中,因为bfqq->wr_coeff是30,则该进程也会被判定高优先级IO。这样fio压测的进程有可能被判定为高优先级IO,进而影响cat 进程派发IO。

解决方案正是bfqd->large_burst_process_name!因为fio压测进程会被判定为in_large_burst型IO,bfqd->large_burst_process_name记录该进程名字fio,等后续再有fio压测,或者fio进程从原本在st->idle tree但来了新的IO而激活加入st->active tree,执行到bfq_bfqq_handle_idle_busy_switch()函数的if((bfqd->large_burst_process_count > 1) &&(bfqd->large_burst_process_tgid == current->tgid) && (strncmp(bfqd->large_burst_process_name,current->comm,COMM_LEN-1) == 0)),强制赋值bfqq->wr_coeff为1,就是强制作为普通IO,没有高优先级属性。这个性能优化点就是避免fio这种IO流量的但时延不敏感的进程影响IO时延敏感进程派发IO。

接着,还有一个性能优化点:在fio压测时,cat 进程读取文件而加入st->active tree,即便cat进程被判定为高优先级IO,但是也有可能因fio频繁派发IO导致cat进程延迟被bfq调度器选中派发IO。于是加入了高优先级IO进程bfqq在加入st->active tree后超时强制派发机制。代码实现如下:

  1. static void __bfq_activate_entity**(** struct bfq_entity * entity**,**
  2. bool non_blocking_wait_rq**)**
  3. {
  4. struct bfq_service_tree * st = bfq_entity_service_tree**(** entity**);**
  5. bool backshifted = false ;
  6. unsigned long long min_vstart**;**
  7. struct bfq_queue * bfqq = bfq_entity_to_bfqq**(** entity**);**
  8. //high prio io的bfqq,记录激活加入st->active tree的时间点。在 high_prio_io_schedule_deadline 时间点到期后,该bfqq必须被调度到派发rq。bfqq->deadline_list->prev 和 next 必须是LIST_POISON2/LIST_POISON1 ,说明没有添加到链表上
  9. if(( bfqq**->** deadline_list**.** prev == LIST_POISON2**)** && ( bfqq**->** deadline_list**.** next == LIST_POISON1**)** && ( bfqq**->** wr_coeff == 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR**)){**
  10. bfqq**->** high_prio_io_active_time = jiffies**;**
  11. list_add_tail**(&** bfqq**->** deadline_list**,** & bfqq**->** bfqd**->** deadline_head**);**
  12. }
  13. /* See comments on bfq_fqq_update_budg_for_activation */
  14. if ( non_blocking_wait_rq && bfq_gt**(** st**->** vtime**,** entity**->** finish**))** {
  15. backshifted = true ;
  16. min_vstart = entity**->** finish**;**
  17. } else
  18. min_vstart = st**->** vtime**;**
  19. ...............
  20. }

如红色代码,在cat这种被判定为高优先级IO进程bfqq插入st->active tree时,还把bfqq加入bfqd->deadline_head链表。

在bfq调度器选择下一个派发IO的bfqq而执行的bfq_lookup_next_entity()函数中,如果bfqd->deadline_head链表上有超时派发IO的bfqq,则强制选择这个bfqq作为下次派发IO的bfqq,此时不再执行__bfq_lookup_next_entity()从st->active tree选择。代码如下:

  1. static struct bfq_entity * bfq_lookup_next_entity**(** struct bfq_sched_data * sd**,**
  2. bool expiration**)**
  3. {
  4. struct bfq_service_tree * st = sd**->** service_tree**;**
  5. struct bfq_service_tree * idle_class_st = st + ( BFQ_IOPRIO_CLASSES - 1**);**
  6. struct bfq_entity * entity = NULL ;
  7. int class_idx = 0**;**
  8. struct bfq_queue * bfqq = bfq_entity_to_bfqq**(** sd**->** next_in_service**);**
  9. struct bfq_data * bfqd = bfqq**->** bfqd**;**
  10. //high prio io的bfqq在加入st->active tree后。high_prio_io_schedule_deadline时间到了,必须立即得到调度派发rq。不用遍历链表,只有看链表头第一个成员是否超时,第一个没超时,后边的更不会超时。
  11. if(! list_empty**(&** bfqd**->** deadline_head**)){**
  12. bfqq = list_first_entry**(&** bfqd**->** deadline_head**,** struct bfq_queue**,** deadline_list**);**
  13. if( time_is_before_jiffies**(** bfqq**->** high_prio_io_active_time + bfqd**->** high_prio_io_schedule_deadline**)){**
  14. entity = & bfqq**->** entity**;**
  15. list_del**(&** bfqq**->** deadline_list**);**
  16. return entity**;**
  17. }
  18. }
  19. ................
  20. entity = __bfq_lookup_next_entity**(** st + class_idx**,** sd**->** in_service_entity &&! expiration**);**
  21. return entity**;**
  22. }

bfq算法是很复杂的,本文的优化算法也需要持续打磨。本文如有错误请指出!

相关推荐
hakesashou1 小时前
python如何比较字符串
linux·开发语言·python
Ljubim.te1 小时前
Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】
linux·学习·centos
cooldream20091 小时前
Linux性能调优技巧
linux
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
QMCY_jason2 小时前
Ubuntu 安装RUST
linux·ubuntu·rust
慕雪华年2 小时前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch
苦逼IT运维2 小时前
YUM 源与 APT 源的详解及使用指南
linux·运维·ubuntu·centos·devops
仍有未知等待探索2 小时前
Linux 传输层UDP
linux·运维·udp
zeruns8023 小时前
如何搭建自己的域名邮箱服务器?Poste.io邮箱服务器搭建教程,Linux+Docker搭建邮件服务器的教程
linux·运维·服务器·docker·网站
卑微求AC3 小时前
(C语言贪吃蛇)16.贪吃蛇食物位置随机(完结撒花)
linux·c语言·开发语言·嵌入式·c语言贪吃蛇