设为首页 收藏本站
查看: 378|回复: 0

[经验分享] Linux操作系统--软件体系结构

[复制链接]

尚未签到

发表于 2016-2-22 08:41:58 | 显示全部楼层 |阅读模式
  注意:本文中的大部分是阅读 《程序员的自我修养》 作 者:俞甲子,石凡,潘爱民 的读书笔记。推荐大家看看这本书。
  一,软件体系结构
DSC0000.gif
  二,线程
  多个线程共用全局变量和进程堆中数据,如果线程知道其他线程的stack的地址,其也可以访问其他线程的栈,当然,一般栈都是线程私有的。
  此外,线程局部存储的数据(TLS)、寄存器是线程私有的。程序代码、打开的文件都是线程共享的。
DSC0001.gif
  线程的状态
DSC0002.gif
  线程优先级改变的方式:
  1,手工(程序)设定
  2,等待状态的频繁程度
  3,长时间得不到执行
  三,Linux下的进程与线程
  Linux操作系统不像Windows那样有明显的process和thread的概念,在Linux下这些都被称作任务(task)。Linux的task可以共享内存空间,共享同一个内存空间的任务构成了进程,其中的每个任务即线程。Linux下与任务有关的系统调用包括: fork、exec、clone。
  fork的作用是与原任务共享一个内存空间,但是这个内存空间是COW(写时复制)的。也就是说,新的任务,如果试图对该内存空间进行修改,这个时候,将会给新的任务复制一份内存空间,新的任务单独使用该内存空间。因此,fork可以看作产生新的进程。在原任务中fork返回值是新建任务的pid,而在新任务中返回的值是0。由于采用了COW,所以fork产生新任务速度很快。
  exec的的作用是用新的可执行映像替换当前的可执行映像,以使的新任务执行新的可执行文件。因此,exec可以看作产生新的进程。
  clone的作用是产生新的任务,并从指定位置开始执行。其可以共享当前的进程的内存空间和文件等,也可以不共享。在共享的情况下,即产生新的线程。
  四,线程安全
  多个线程环境下,可访问的全局变量和堆数据随时可能被其他线程所改变。因此多线程并发时的数据一致性非常重要。
  比如 线程A: i=1;i++; 线程B: --i; 线程A和线程B同时执行,则i的值不可预测,有可能是1,也可能是0,也有可能是2。分析如下:
  A的汇编代码(伪汇编代码)为:
  mov i 1
  mov eax i
add eax 1
  mov i eax
  B的汇编代码(伪汇编代码)为:
  mov eax i
  sub eax 1
  mov i eax
  由于线程的寄存器是私有的,我们假设线程A的eax为eax_A,线程B的为eax_B,则的eax_A的可能值为2,1;eax_B的可能值为1,0。
  因此,最终的mov i eax_A将使得i为2或者1;最终的mov i eax_B将使得i为1或者0。
  汇编的单条指令是不会被打断的,称为原子的。
  i386有inc这个增加内存值的指令,Windows里有一套API来执行原子操作,这些API称为Interlocked API。包括原子的交换两个值、原子地减少一个值、增加一个值,原子的异或操作等。使用这些API,Windows操作系统保证其是原子操作。
  五,同步与锁
  由于上述原子指令只能用于简单场合,必须有一个有效的手段来保证线程安全。即同步与锁。
  同步即在线程访问数据没有结束的时候,其他线程不得对同一数据进行访问。这样数据访问便被原子化了。
  锁是同步的最常见方法。
  二元信号量是最简单的一种锁,它只有占用和非占用两种状态。
  多元信号量即所谓的信号量机制,它允许多个线程并发访问资源。比如初始值为N,则允许N个线程访问,
  访问时,执行如下操作:
  1,信号量-1
  2,如果信号量为0,则进行等待,否则访问
  访问后,执行如下操作:
  1,信号量+1
  2,如果信号量大于1,唤醒等待的线程。
  互斥量与二元信号量类似,不同之处在于信号量在整个系统可以被任意线程获取并释放,即同一个信号量可以被一个线程获取后,被另外一个线程释放,而互斥量要求谁获取谁释放。
  临界区比互斥量更严格的同步手段。把临界区的锁的获取成为进入临界区,把其释放成为离开临界区。其与互斥量和信号量的区别在与一个进程创建互斥量或信号量,另一个去试图获取是合法的,而临界区作用范围仅限制与本进程,其他进程无法获取该锁。除此之外,临界区具有和互斥量相同性质。
  读写锁则致力于一个更为特殊的场合的同步。上述机制,对于读取频繁而偶尔写入的情况,显得非常低效。读写锁可以避免这个问题:对于同一个锁,可以有两种获取方式,即共享的(shared)或者是独占的(exclusive)。锁处于自由状态时候,任何方式获取都会成功。但是如果处于共享状态,其他以共享方式获取可成功,如果其他线程以独占方式获取共享状态的锁,则会等待被所有线程释放。独占状态的锁将会阻止其他线程获取该锁。其状态如下所示:
DSC0003.gif
  条件变量也是一个同步手段。线程可以等待或者唤醒条件变量。唤醒条件变量时,其他所有等待的线程被唤醒恢复执行。
  六,可重入与线程安全
  函数重入只有两种情况:
  1,多个线程调用该函数
  2,自己调用自己
  一个函数是可重入的,必须具有如下特点:
  1,不使用任何(局部)静态变量或全局的非const变量。
  2,不返回任何(局部)静态变量或全局的非const变量的指针。
  3,仅依赖于调用方提供的参数
  4,不依赖任何单个资源的锁,如mutex等
  5,不调用任何不可重入的函数
  一个可重入的函数可以在多线程环境下放心的使用。
  七,过度优化
  即使合理使用锁,也不一定保证线程安全,因为看似无错的代码在编译器技术进行优化后产生麻烦。
  例如:
DSC0004.gif
  上述代码看似没有问题,但是编译器为了提高访问速度,用寄存器存储x,由于不同线程的寄存器独立,因此线程暂时不把寄存器的值写回。最后再写回去。
  因此无法保证多线程安全了。
  再如:
DSC0005.gif
  在逻辑上看似绝对不可能有r1=r2=0,但是由于编译器可能把毫不相干的两条指令交换顺序以提高速度,因此,可能被编译器优化成如下指令序列:
DSC0006.gif
  这样r1=r2=0则完全可能了。
  使用volatile可以阻止过度优化,包括:
  阻止编译器为提高速度将变量保存到寄存器而不写回。
  阻止编译器调整volatile变量的指令顺序。
  七,另外一个与换序有关的例子
  volatile T * pInst=0;
  T * GetInstance(){
  if(pInst==NULL){
  lock();
  if(pInst==NULL)
  pInst=new T;
  unlock();
  }
  return pInst;
  }
  上述代码使用双重if,可以让lock的调用开销降低到最小。PInst逻辑上总是指向一个有效对象。
  但是,由于C++里的new包含两个步骤:
  1,分配内存
  2,调用构造函数
  因此,pInst=new T
  包含: 1,分配内存, 2,在内存位置上调用构造函数 3,将内存地址赋予pInst。
  由于2和3可以颠倒,所以完全可能被换序而出现如下情况: pInst值不是NULL,但是对象依然没有构造完成。这个时候,另外一个对GetInstance的并发调用因为pInst不是NULL而会直接返回尚未构造完成的对象的地址,即pInst。此时如果对该pInst指向的类进行操作,
  是有可能导致程序崩溃的。
  CPU的一个称为barrier的指令可以阻止CPU将该指令之前的指令交换到该指令之后执行,该指令之后的也不会交换到该指令之前。
  许多体系结构的CPU都提供barrier,但是名称可能不同。例如POWERPC提高的称为lwsync。对于这个我们可以如此使用:
  #define barrier() __asm__volatile("lwsync")
  volatile T * pInst=0;
  T * GetInstance(){
  if(pInst==NULL){
  lock();
  if(!pInst){
  T * temp =new T;
  barrier();
  pInst=temp;
  }
  unlock();
  }
  return pInst;
  }
  八,多线程的内部情况
  用户使用的线程实际上可能并不是内核线程,而是存在于用户态的用户线程。用户态线程在内核里并不对应同样的内核线程。例如某些轻量级线程库,对用户的三个线程,可能对与内核就只有一个线程。
  用户态多线程的实现方法:
  1,一对一模型。
  即直接支持线程的系统采用的最简单的方法。一个用户态线程对应唯一的内核线程(但是一个内核线程不一定有用户态线程对应)。
  直接使用API或系统调用创建的线程一般都是一对一线程。
  缺点是 切换开销和内核线程数量限制。
  2,多对一模型
  即 多个用户线程映射一个内核线程,线程之间的切换由用户态的代码进行。这样就比一对一模型执行切换要快。但是缺点是其中的一个用户态线程阻塞,其他的用户态线程也不能执行,因为此时的对应的唯一内核线程被阻塞了。
  3,多对多模型
  多个用户态线程映射到少数但多个内核线程上。其就是一对一和多对一模型的结合结果。
  评论:
  
线程安全,有的时候,不仅仅需要使用锁、信号量、读写锁、条件变量进行线程的同步和互斥,有的时候,需要防止编译器进行过度的包括使用寄存器、交换指令执行顺序的优化,甚至还要防止CPU进行动态的执行顺序的调整。

防止编译器过度优化可以使用volatile,防止CPU进行动态执行顺序改变可以使用barrier()指令。


用户态的线程和真正内核建立维护的线程不一定是一对一的关系。一对一、多对一、多对多的模型各有好处。具体采用哪一种的策略和是否有自己实现一种模型的可行性和意义尚待研究。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.iyunv.com/thread-181090-1-1.html 上篇帖子: 很受欢迎的Linux笔记(一) 下篇帖子: linux 为当前目录加权限
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表