注意:本文中的大部分是阅读 《程序员的自我修养》 作 者:俞甲子,石凡,潘爱民 的读书笔记。推荐大家看看这本书。
一,软件体系结构
二,线程
多个线程共用全局变量和进程堆中数据,如果线程知道其他线程的stack的地址,其也可以访问其他线程的栈,当然,一般栈都是线程私有的。
此外,线程局部存储的数据(TLS)、寄存器是线程私有的。程序代码、打开的文件都是线程共享的。
线程的状态
线程优先级改变的方式:
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)。锁处于自由状态时候,任何方式获取都会成功。但是如果处于共享状态,其他以共享方式获取可成功,如果其他线程以独占方式获取共享状态的锁,则会等待被所有线程释放。独占状态的锁将会阻止其他线程获取该锁。其状态如下所示:
条件变量也是一个同步手段。线程可以等待或者唤醒条件变量。唤醒条件变量时,其他所有等待的线程被唤醒恢复执行。
六,可重入与线程安全
函数重入只有两种情况:
1,多个线程调用该函数
2,自己调用自己
一个函数是可重入的,必须具有如下特点:
1,不使用任何(局部)静态变量或全局的非const变量。
2,不返回任何(局部)静态变量或全局的非const变量的指针。
3,仅依赖于调用方提供的参数
4,不依赖任何单个资源的锁,如mutex等
5,不调用任何不可重入的函数
一个可重入的函数可以在多线程环境下放心的使用。
七,过度优化
即使合理使用锁,也不一定保证线程安全,因为看似无错的代码在编译器技术进行优化后产生麻烦。
例如:
上述代码看似没有问题,但是编译器为了提高访问速度,用寄存器存储x,由于不同线程的寄存器独立,因此线程暂时不把寄存器的值写回。最后再写回去。
因此无法保证多线程安全了。
再如:
在逻辑上看似绝对不可能有r1=r2=0,但是由于编译器可能把毫不相干的两条指令交换顺序以提高速度,因此,可能被编译器优化成如下指令序列:
这样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()指令。
用户态的线程和真正内核建立维护的线程不一定是一对一的关系。一对一、多对一、多对多的模型各有好处。具体采用哪一种的策略和是否有自己实现一种模型的可行性和意义尚待研究。
分享到:
相关推荐
“arm-linux-gnueabihf”这部分是目标体系结构的标识符。"arm"指的是ARM处理器架构,"linux"表示目标操作系统是Linux,"gnueabihf"是“GNU EABI with Hard Float”的缩写,意味着这个工具链支持硬浮点运算,这可以...
2. Linux操作系统:Linux是一种开源、自由的操作系统内核,被广泛用于各种设备,从手机到超级计算机。在嵌入式系统中,Linux提供了稳定、可定制的环境,便于开发者构建和管理软件。 3. GCC 4.3.2:这是GCC的一个...
与此同时,为了更好地调试和优化软件,理解如何在aarch64体系结构下使用GDB(GNU Debugger)至关重要。本文将详细探讨aarch64-linux-android-gdb的使用,帮助开发者提升在Android系统上的调试效率。 一、aarch64...
举例来说,我们常说的x86 Linux平台实际上是Intel x86体系结构和Linux for x86操作系统的统称;而x86 WinNT平台实际上是Intel x86体系结构和Windows NT for x86操作系统的简称。 这个arm-linux-gcc-3.4.5 是个通用...
交叉编译器,顾名思义,是一种能够在一种体系结构(宿主机)上编译出另一种体系结构(目标机)可执行代码的编译器。在这种情况下,ARM-Linux-GCC-4.8.4是专为基于ARM架构的Linux系统设计的,可以在非ARM平台(如x86...
嵌入式Linux系统是指在嵌入式设备中运行的Linux操作系统,包括嵌入式硬件、嵌入式系统软件、文件系统等组件。嵌入式硬件包括嵌入式处理器、嵌入式存储器、嵌入式系统软件等。嵌入式处理器包括微处理器、微控制器、...
Linux ARM GCC 4.9.2交叉编译工具是一个针对ARM架构在Linux系统上进行编译的工具链,主要用于在非ARM平台(如x86)上构建适用于ARM设备的应用程序或操作系统。这个版本的GCC(GNU Compiler Collection)是4.9.2,是...
"操作系统"和"linux"进一步强调了这是关于Linux操作系统的讨论。 根据提供的压缩包文件名称列表,只有一个文件"arm-linux-gcc-4.5.1",这可能是编译器的源码或者预编译的二进制文件。如果是源码,用户需要按照特定...
交叉编译器常用于构建嵌入式操作系统、驱动程序、应用软件等。例如,在开发基于Linux的嵌入式设备固件时,开发者可以使用arm-linux-gcc-4.4.3将用户空间应用程序和内核模块编译为目标系统的可执行文件。 总之,arm...
嵌入式 LINUX 操作系统的可移植性非常强,可以快速移植到新的微处理器体系结构中。在移植过程中,需要对内核代码进行修改,以适应新的目标板和外设。 嵌入式 LINUX 操作系统的内存使用包括: 1. 内核和应用程序...
本文主要介绍了基于S3C2440的Linux操作系统移植,包括Linux操作系统的概述、Linux的优点、Linux系统安装、Linux常用工具、Linux的常用命令、ARM体系结构的介绍等内容。 嵌入式Linux简介 ---------------- 嵌入式...
"linux-gtk-x86_64"这部分则揭示了该版本是为Linux操作系统设计的,采用GTK+图形用户界面库,并且适配64位(x86_64)架构。GTK+是一个广泛使用的开源GUI库,为多种操作系统提供了丰富的界面支持。64位版本意味着它能...
这个特定的版本 "eclipse-cpp-2019-06-R-linux-gtk-x86_64.tar.gz" 是针对Linux操作系统的,采用GTK+图形用户界面库,并且是64位架构的。这个版本发布于2019年6月,被称为R版本,意味着它是Eclipse的常规发布版。 *...
- "linux"表示这是针对Linux操作系统的。 - "gnueabi"代表GNU标准应用二进制接口(ABI),确保编译出的程序与目标平台的库和系统调用兼容。 - "hf"表示硬件浮点(Hardware Floating Point),意味着编译器支持ARM...
Linux 操作系统的体系结构总体上属于层次结构。从内到外包括三层:最内层是系统核心,中间是 Shell、编译编辑实用程序、库函数等,最外层是用户程序,包括许多应用软件。 二、配置一个双引导系统 在计算机中已经...
"arm-linux-gnueabihf"是目标体系结构的标识,其中“arm”代表处理器类型,“linux”表明它用于Linux操作系统,“gnueabihf”则表示它支持软浮点运算(Soft-Floating Point),而“hf”通常代表“hard-float”,意味...
在这个案例中,我们关注的是针对ARM处理器的Linux系统,其中“arm-linux-gnueabihf”是一个特定的三元组标识符,表示目标体系结构是ARM(Advanced RISC Machines),Linux操作系统,使用EABI(Embedded Application ...
本文档主要阐述了基于S3C2440的Linux操作系统移植的实现过程,并对Linux操作系统、ARM体系结构、S3C2440处理器等技术进行了详细的介绍。 一、Linux操作系统简介 Linux操作系统是一种开放源代码的操作系统,由Linus...
标题中的“eclipse-java-oxygen-3a-linux-gtk-x86_64.tar.gz”指的是一款针对Linux操作系统的Eclipse集成开发环境(IDE)的压缩包文件。Eclipse是一款开源、跨平台的Java IDE,广泛用于Java应用程序的开发。这里的...