加入收藏 | 设为首页 | 会员中心 | 我要投稿 云计算网_泰州站长网 (http://www.0523zz.com/)- 视觉智能、AI应用、CDN、行业物联网、智能数字人!
当前位置: 首页 > 服务器 > 搭建环境 > Windows > 正文

被神话的Linux, 一文带你看清Linux在多核可扩展性设计上的不足

发布时间:2019-10-12 01:44:21 所属栏目:Windows 来源:Android资深架构师
导读:副标题#e# 我其实并不想讨论微内核的概念,也并不擅长去阐述概念,这是百科全书的事,但无奈最近由于鸿蒙的发布导致这个话题过火,也就经不住诱惑,加上我又一直比较喜欢操作系统这个话题,就来个老生常谈吧。 说起微内核,其性能往往因为IPC饱受诟
副标题[/!--empirenews.page--]

我其实并不想讨论微内核的概念,也并不擅长去阐述概念,这是百科全书的事,但无奈最近由于鸿蒙的发布导致这个话题过火,也就经不住诱惑,加上我又一直比较喜欢操作系统这个话题,就来个老生常谈吧。

说起微内核,其性能往往因为IPC饱受诟病。然而除了这个显而易见的 “缺陷” ,其它方面貌似被关注的很少。因此我写点稍微不同的。

被神话的Linux, 一文带你看清Linux在多核可扩展性设计上的不足

微内核的性能 “缺陷” 我假设是高开销的IPC引起的(实际上也真是),那么,我接下来便继续假设这个IPC性能是可以优化的,并且它已经被优化(即便不做任何事,随着硬件技术的发展,所谓的历史缺点往往也将逐渐弱化...)。我不公道地回避了核心问题,这并不是很道德,但为了下面的行文顺利,我不得不这么做。

很多人之所以并不看好微内核,很大程度上是因为它和Linux内核是如此不同,人们认为不同于Linux内核的操作系统内核都有这样那样的缺陷,这是因为Linux内核给我们洗了脑。

Linux内核的设计固化了人们对操作系统内核的理解上的观念 ,以至于 Linux内核做什么都是对的,反Linux的大概率是错的。 Linux内核就一定正确吗?

在我看来,Linux内核只是在恰当的时间出现的一个恰好能跑的内核,并且恰好它是开源的,让人们可以第一次内窥一个操作系统内核的全貌罢了,这并不意味着它就一定是正确的。相反,它很可能是错误的。【 20世纪90年代,Windows NT系统初始,但很难看到它的内在,《windows internal》风靡一时;UNIX陷入纠纷,GNU呼之却不出,此时Linux内核满足了人们一切的好奇心,于是先入为主,让人们觉的操作系统就应该是这个样子,并且在大多数人看来,这是它唯一的相貌。 】

本文主要说 内核的可扩展性 。

先泼一盆冷水,Linux内核在这方面做得并非已经炉火纯青。

诚然,近十几年来Linux内核从2.6发展到5.3,一直在SMP多核扩展方面精益求精,但是说实话架构上并没有什么根本性的调整,要说比较大的调整,当属:

  • $O(1)$调度算法。SMP处理器域负载均衡算法。percpu数据结构。数据结构拆锁。

都是一些细节,没有什么让人哇塞的东西,还有更细节的cache刷新的管理,这种第二天不用就忘记的东西,引多少人竞折腰。

这不禁让人想起在交换式以太网出现之前,人们不断优化CSMA/CD算法的过程,同样没有让人哇塞,直到交换机的出现,让人眼前一亮,CSMA/CD随之几乎被完全废弃,因为它不是 正确 的东西。

交换机之所以 正确 的核心在于 仲裁。

当一个共享资源每次只能容纳一个实体占用访问时,我们称该资源为 “必须串行访问的共享资源” ,当有多个实体均意欲访问这种资源时,one by one是必然的,one by one的方案有两种:

被神话的Linux, 一文带你看清Linux在多核可扩展性设计上的不足

哪个好?说说看。

争抢必会产生冲突,冲突便耽误整体通过的时间,你会选哪个?

现在,我们暂时忘掉诸如宏内核,微内核,进程隔离,进程切换,cache刷新,IPC等概念,这些概念对于我们理解事情的本质毫无帮助,相反,它们会阻碍我们建立新的认知。比如,无论你觉得微内核多么好,总有人跳出来说IPC是微内核的瓶颈,当你提出一个类似页表项交换等优化后,又会有人说进程切换刷cache,寄存器上下文save/restore的开销也不小,然后你可能知道点 带有进程PID键值的cache方案 ,吧啦吧啦,最后一个show me the code 让你无言以对,一来二去,还没有认识全貌,便已经陷入了细节。

所以,把这些忘掉,来看一个观点:

  • 对待必须串行访问的共享资源,正确的做法是引入一个仲裁者排队调度访问者,而不是任由访问者们去并发争锁!

所谓 操作系统 这个概念,本来就是莫须有的,你可以随便叫它什么,早期它叫 监视器 ,现在我们姑且就叫它操作系统吧,但这并不意味着这个概念有多么神奇。

操作系统本就是用来协调多个进程(这也是个抽象后的概念,你叫它任务也可以,无所谓)对底层共享资源的 多对一访问 的,最典型的资源恐怕就是CPU资源了,而几乎所有人都知道,CPU资源是需要调度使用的,于是任务调度一直都是一个热门话题。

你看, CPU就不是所有任务并发争抢使用的,而是调度器让谁用谁才能用 。调度,或者说仲裁,这是操作系统的精髓。

那么对于系统中共享的文件,socket,对于各种表比如路由表等资源,凭什么要用并发争抢的方式去使用?!所有的共享资源,都应该是被调度使用的,就像CPU资源一样。

如果我们循着操作系统理应实现的最本质的功能去思考,而不是以Linux作为先入为主的标准去思考,会发现Linux内核处理并发明显是一种错误的方式!

Linux内核大量使用了自旋锁,这明显是从单核向SMP进化时最最最简单的方案,即 只要保证不出问题的方案!

也确实如此,单核上的自旋锁并不能如其字面表达的那样 自旋 , 在单核场景下,Linux的自旋锁实现仅仅是 禁用了抢占 。因为,这样即可保证 不出问题 。

但到了必须要支持SMP的时候,简单的禁用抢占已经无法保证不出问题,所以 待在原地自旋等待持锁者离开 便成了最显而易见的方案。自旋锁就这样一直用到了现在。一直到今天,自旋锁在不断被优化,然而无论怎么优化,它始终都是一个不合时宜的自旋锁。

可见,Linux内核一开始就不是为SMP设计的,因此其并发模式是错误的,至少不是合适的。

有破就要有立,我下面将用一套用户态的代码来模拟 无仲裁的宏内核 以及 有仲裁的微内核分别是如何对待共享资源访问的。代码比较简单,所以我就没加入太多的注释。

以下的代码模拟宏内核中访问共享资源时的自旋锁并发争抢模式:

  1. #include <pthread.h> 
  2. #include <signal.h> 
  3. #include <stdio.h> 
  4. #include <unistd.h> 
  5. #include <stdlib.h> 
  6. #include <errno.h>  
  7. #include <sys/time.h> 
  8. static int count = 0; 
  9. static int curr = 0; 
  10. static pthread_spinlock_t spin; 
  11. long long end, start; 
  12. int timer_start = 0; 
  13. int timer = 0; 
  14. long long gettime() 
  15.  struct timeb t; 
  16.  ftime(&t); 
  17. return 1000 * t.time + t.millitm; 
  18.   
  19. void print_result() 
  20.  printf("%dn", curr); 
  21.  exit(0); 
  22.   
  23. struct node { 
  24.  struct node *next; 
  25.  void *data; 
  26. }; 
  27.   
  28. void do_task() 
  29.  int i = 0, j = 2, k = 0; 
  30.   
  31.  // 为了更加公平的对比,既然模拟微内核的代码使用了内存分配,这里也fake一个。 
  32.  struct node *tsk = (struct node*) malloc(sizeof(struct node)); 
  33.   
  34.  pthread_spin_lock(&spin); // 锁定整个访问计算区间 
  35.  if (timer && timer_start == 0) {  
  36.  struct itimerval tick = {0}; 
  37.  timer_start = 1; 
  38.  signal(SIGALRM, print_result); 
  39.  tick.it_value.tv_sec = 10; 
  40.  tick.it_value.tv_usec = 0; 
  41.  setitimer(ITIMER_REAL, &tick, NULL); 
  42.  } 
  43.  if (!timer && curr == count) { 
  44.  end = gettime(); 
  45.  printf("%lldn", end - start); 
  46.  exit(0); 
  47.  } 
  48.  curr ++; 
  49.  for (i = 0; i < 0xff; i++) { // 做一些稍微耗时的计算,模拟类似socket操作。强度可以调整,比如0xff->0xffff,CPU比较猛比较多的机器上做测试,将其调强些,否则队列开销会淹没模拟任务的开销。 
  50.  k += i/j; } 
  51.  pthread_spin_unlock(&spin); 
  52.  free(tsk); 
  53.   
  54. void* func(void *arg) 
  55.  while (1) { 
  56.  do_task(); 
  57.  } 
  58.   
  59. int main(int argc, char **argv) 
  60.  int err, i; 
  61.  int tcnt; 
  62.  pthread_t tid; 
  63.   
  64.  count = atoi(argv[1]); 
  65.  tcnt = atoi(argv[2]); 
  66.  if (argc == 4) { 
  67.  timer = 1; 
  68.  } 
  69.   
  70.  pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE); 
  71.  start = gettime(); 
  72.  // 创建工作线程 
  73.  for (i = 0; i < tcnt; i++) { 
  74.  err = pthread_create(&tid, NULL, func, NULL); 
  75.  if (err != 0) { 
  76.  exit(1); 
  77.  } 
  78.  } 
  79.   
  80.  sleep(3600); 
  81.   
  82.  return 0; 

(编辑:云计算网_泰州站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读