您的位置:首页 > 其它

操作系统机制学习总结

2017-05-03 20:24 253 查看

操作系统的运行机制

http://blog.csdn.net/dongyanxia1000/article/details/51244589 

1、程序

在计算机系统中,通常CPU执行两种不同性质的程序:

------- 一种是操作系统内核程序;

------- 另一种是用户自编程序或系统外层的应用程序。

内核程序是应用程序的”管理者”。

“管理程序“可以执行一些特权指令,而”被管理程序“出于安全考虑不能执行这些指令。

所谓特权指令,是指计算机中不允许用户直接使用的指令,如:

I/O指令、置中断指令,存取用于内存保护的寄存器,送程序状态字到程序状态字寄存器等指令。

操作系统在具体实现上划分了用户态(目态)和核心态(管态),以严格区分两类程序。

2、层次式结构

在软件工程思想和结构程序设计方法的影响下诞生的现代操作系统,几乎都是层次式的结构。

操作系统的各项功能分别被设置在不同的层次上。

-------  一些与硬件关联较紧密的模块,诸如时钟管理、中断管理、设备驱动等处于最底层。

-------  其次是运行频率较高的程序,诸如进程管理、存储管理和设备管理等。

上面的这两部分内容构成了操作系统的内核,这部分内容的指令操作工作在核心态。

3、内核

内核是计算机上配置的底层软件,是计算机功能的延伸。

不同系统对内核的定义稍有区别,大多数操作系统内核包括4个方面的内容。

1)时钟管理

在计算机的各种部件中,时钟是最关键的设备。

时钟的第一功能是计时,操作系统需要通过时钟管理,向用户提供标准的系统时间。

其次,通过时钟中断的管理,可以实现进程的切换。

在分时操作系统中,采用时间片轮转调度的实现;

在实时系统中,按截至时间控制运行的实现;

在批处理系统中,通过时钟管理来衡量一个作业的运行程度等。

2)中断机制

引入中断技术的初衷是提高多道程序运行环境中CPU的利用率,主要针对外部设备。

后来逐步得到发展,形成了多种类型,称为操作系统各项操作的基础。

如,键盘或鼠标信息的输入、进程的管理和调度、系统功能的调用、设备驱动、文件访问等。都依赖于中断机制。

可以说,现代操作系统是靠中断驱动的软件。

中断机制中,只有一小部分功能属于内核,负责保护和恢复中断现场的信息,转移控制权到相关的处理程序。

这样可以减少中断的处理时间,提高系统的并行处理能力。

3)原语

按照层次结构设计的操作系统,底层必然是一些可被调用的公用小程序,它们各自完成一个规定的操作,其特点是:

------  它们处于操作系统的最底层,是最接近硬件的部分。

------  这些程序的运行具有原子性---其操作只能一气呵成

------  这些程序的运行时间都较短,而且调用频繁。

通常把具有这些特点的程序称为原语(Atomic Operation)。

定义原语的直接方法是关闭中断,让它的所有动作不可分割地进行完再打开中断。

系统中的设备驱动、CPU切换、进程通信等功能中的

部分操作都可以定义为原语,使它们成为内核的组成部分。

4)系统控制的数据结构及处理

系统中用来登记状态信息的数据结构很多,比如:

作业控制块、进程控制块、设备控制块、各类链表、消息队列、缓冲区、空闲区登记表、内存分配表等。

为了实现有效的管理,系统需要一些基本的操作,常见的操作有以下三种:

------  进程管理:进程状态管理、进程调度和分配、创建和撤销进程控制块等。

------  存储器管理:存储器的空间分配和回收、内存信息保护程序、代码对换程序等。

------  设备管理:缓冲区管理、设备分配和回收等。

核心态指令实际上包括系统调用类指令和一些针对时钟、中断和原语的操作指令。

========

操作系统的运行机制

http://www.cnblogs.com/kakawater/p/6093709.html

在计算机系统中,操作系统处于中间层,向下管理和控制硬件,向上为外层软件和用户编写的程序提供使用方便、功能强大的服务。操作系统的主要功能就是管理CPU、主存、I/O设备和文件,并提供支持程序并发运行的机制。

通常,操作系统提供的主要功能都是由操作系统内核程序实现的,CPU在运行上层程序时,唯一能进入内核程序运行的途径就是中断或异常。

中断和异常

中断和异常时操作系统的重要概念,中断的引入是为了实现CPU和通道(或设备)之间的并行操作,当CPU启动通道(或设备)进入I/O后,通道(或设备)就可以独立工作了,CPU可以去做与此I/O不相关的事情。当通道处理完CPU交付的I/O任务之后就必须告知CPU,让CPU继续处理I/O操作以上的事情。在计算机中,通道(或设备)通过中断机制来告知CPU。

由于众多原因,当CPU在执行指令的时候可能会出现错误,这个错误就称为”异常”。例如,CPU在执行指令时,可能会出现算术一出、零作除数、访存越界等错误。此外,在操作系统中,还提供一种异常指令-陷入指令trap(即陷入指令trap是一种会产生异常的指令),用来实现系统调用(用来实现CPU切换到内核程序运行)。

中断和异常的区别

中断和异常都能用来让CPU切换到内核程序运行,但是两者的是有区别的:

调用方不一样:中断是由硬件来产生,而异常则是由CPU当前执行的指令的实现逻辑发生错误而产生。

中断是一种通知机制,异常是一种错误处理机制。故中断可以被屏蔽或者稍后处理,而异常则不能被屏蔽,并且应当立即处理。

中断/异常的响应和处理

处理机在执行任何指令时都可能产生中断或者异常,那么处理机如何响应中断和异常呢?

中断/异常响应

中断信号是由外部设备或者时钟部件发送给CPU的,为了能及时监测中断信号,通过在CPU的控制部件中增加一个能够监测中断信号的机构来完成。该机构能够在每条机器指令执行周期内的最后时刻扫描中断寄存器询问是否含有中断信号。如果没有中断信号或被信号暂时被屏蔽,CPU继续执行程序的后续指令,否则CPU停止执行当前程序的后续指令,无条件地转入操作系统内核的中断处理程序,我们称该过程为中断响应

异常是在执行指令时,由于指令的逻辑错误造成CPU转入执行操作系统内核的异常处理程序。

和CPU在切换程序运行时一样,CPU在执行异常处理程序或中断处理程序时依旧需要涉及到对当前程序的运行现场进行保护。

断点和恢复点

CPU一旦响应中断,立刻开始执行内核中的中断处理程序,当中断处理程序结束后,重新返回中断点执行后续指令。我们称当中断发生时,CPU刚执行完的那条指令的内存地址称为”断点”。一般情况下,断点应为中断的那一瞬间,CPU的PC寄存器(程序计数器)所指指令的前一条指令的内存地址。而中断时,PC寄存器所指指令的内存地址为恢复点。

在异常发生时,返回点(即异常处理程序执行完后,CPU要执行的下一条指令的地址)会因为不同的异常而有所区别:

对于大部分由用户程序指令执行出错而引起的异常,操作系统的处理方式是结束所运行的程序,因此就不会回到用户程序。

如果是通过trap指令进行系统调用,则处理完成后trap指令的下一条指令。

对于虚存系统访存指令的缺页异常,异常处理完后则会返回发生异常的指令重新执行该访存指令,以保证这次访存指令能够顺利执行。

现场保护

CPU在执行程序时,主要操作的时寄存器中的值。所谓现场信息,就是值中断那一时刻确保存放程序继续运行的有关信息,例如PC寄存器、通用寄存器以及一些与程序运行相关的特殊寄存器中的内存(即CPU中与程序运行相关的寄存器的状态)。

现场信息的保护和恢复可由硬件、软件共通完成,现场信息通常保存在操作系统中的与被中断程序相关的数据结构中。

中断/异常响应的CPU模式

中断和异常的处理程序都是操作系统的内核程序,都必须在特权模式下运行,因为这些程序需要访问外设等操作系统管理的资源或者设计系统的管理表格。

在操作系统,我们将在监督程序中CPU的两种模式,监督模式和用户模式,分别称为”核心态”和”用户态”。我们通过在CPU的状态字寄存器中设置一个标识位,根据其当前值为1或0来分别表示处理机处在核心态或用户态。

通常CPU执行两种不同的程序,一种是操作系统内核程序(核心态),另一种是用户自行编写的程序(用户态)或系统外层的应用程序(用户态)。前者是后者的服务者和控制者。

中断/异常处理程序的执行

操作系统为每个中断/异常都创建一个处理程序,并把这些处理程序的入口地址放在主存的特定位置(主存单元),并称这些主存单元为中断/异常向量,或称系统控制块

对于不同的操作系统。中断/异常向量中的内存细节也不完全相同。中断/异常向量的每一个单元中出了存储中断/异常处理程序的入口地址外,还常用来保存CPU状态转换的信息。例如,中断/异常处理程序运行需要用到的新PS寄存器值和新PC寄存器值。

在中断/异常向量中,每一个中断信号占用连续的两个单元:一个用来存放中断/异常处理程序的地址(对应PC寄存器的新值),另一个单元用来存放执行中断/异常处理程序时CPU所处的状态(对应PS寄存器的新值)。当响应中断/异常时,硬件首先将当前PC和PS寄存器的值作为程序现场保存起来,然后再从中断/异常向量中的相应单元取出要放入PC和PS寄存器中的值并放入相应的寄存器。最后再根据PC寄存器中的值去执行中断/异常处理程序

中断

在计算机系统中, 多个中断可能在同一时刻产生,并且还会在前一个中断还未处理完的情况下就产生下一个中断。系统为了保存每个中断信号,通常用一些中断寄存器来将它们保存,为了区分不同的信号源,一般对中断寄存器的各位顺序编号,我们称该编号为中断序号,并且规定其值为1时表示有中断信号,其值为0时表示没有中断信号。

在实时系统中,通过将中断分为不同的级别,我们称为中断优先级,以便系统能够立刻处理实时性要求比较高的中断。级别高的中断享有绝对优先处理的权利:

当级别不同的两个以上中断信号同时产生时,首先处理级别高的中断。

级别高的中断可以打断级别低的中断的处理过程。

当两个级别相同的中断产生时,根据中断寄存器中从左至右的顺序来决定处理顺序。(是不是很像四则运算法则)

在实际的操作系统中,有多少中断级别,每个中断应该被划分到那个级别,这些都由操作系统设计者来决定。通常来说,高速设备的中断优先级高,低速设备的中断优先级低。但是作为交互系统,也要考虑到用户的特殊需求,例如用户在输入指令时,该输入设备就应当设置为较高的中断级别。

某小型机的操作系统把中断级别分为如下三种:

时钟中断的中断优先级为6

磁盘中断的中断优先级为5

终端等其它外设中断的中断优先级为4

中断屏蔽(中断过滤)

计算机中有那么多的屏蔽,但是有时候我们希望CPU在执行一些任务的时候可以过滤一些中断信号,让CPU专注于当前的任务,在一点在很多场景下都很有用,譬如当多个程序同时操纵一块内存的时候很有用。

所谓的”中断屏蔽”通常是指禁止响应中断。在可编程中断控制器中,CPU可以执行特权指令来设置可编程中断控制器的屏蔽码,这样即使硬件发现具有屏蔽码的中断产生,也不会通知处理机,但还是会保存此次中断以便将来屏蔽解除时由CPU来处理. 我们称这种方式为软屏蔽

处理器优先级是指当前CPU正在运行程序的中断响应级别。当处理器处于某一处理器优先级时,就会只允许处理响应级别比该级别高的中断,而屏蔽低于或等于该优先级的中断。

系统调用(trap指令)

当用户态下的程序通过trap指令进行系统调用的时候,需要进行调用参数,以便根据系统调用表调用相应的系统调用处理程序。

为了方便高级语言程序使用系统调用,通常提供一个系统调用库,其中包含许多系统调用借口函数,这些函数看上去就是一些普通的子程序,但是实则这些函数往往是有为数不多的几条汇编指令实现的,而且这些汇编指令中必须包含一条trap指令,这样才能保证指令trap指令时处理器控制转移至操作系统内核的相应程序。

使用系统提供的系统调用库函数进行系统调用不但可以使用户不必关心系统调用的细节,而且可以避免由用户直接使用trap指令可能引起的错误。

ABI(Application Binary Interface,应用程序二进制接口)

在进行系系统调用时,系统调用库函数需要向操作系统传递什么样的调用型号?该系统调用需要什么参数?以及如何传递参数(显然是通过寄存器来传递)?如何接收系统调用的返回值(显然也是通过寄存器)等这一系列操作都会在操作系统的ABI文档中说明,例如上层用户应该使用哪些寄存器来传递参数。

事实上,我们可以自己基于操作系统的ABI文档来实现一个系统调用库,这在没有与我们使用的高级语言相对应的系统调用库时极为有用。

========

操作系统的运行机制

http://c.biancheng.net/cpp/html/2583.html

计算机系统中,通常CPU执行两种不同性质的程序:一种是操作系统内核程序;另一种是用户自编程序或系统外层的应用程序。对操作系统而言,这两种程序的作用不同,前者是后者的管理者,因此“管理程序”要执行一些特权指令,而“被管理程序”出于安全考虑不能执行这些指令。所谓特权指令,是指计算机中不允许用户直接使用的指令,如I/O指令、 置中断指令,存取用于内存保护的寄存器、送程序状态字到程序状态字寄存器等指令。操作系统在具体实现上划分了用户态(目态)和核心态(管态),以严格区分两类程序。

在软件工程思想和结构程序设计方法的影响下诞生的现代操作系统,几乎都是层次式的结构。操作系统的各项功能分别被设置在不同的层次上。一些与硬件关联较紧密的模块,诸如时钟管理、中断处理、设备驱动等处于最底层。其次是运行频率较髙的程序,诸如进程管理、存储器管理和设备管理等。这两部分内容构成了操作系统的内核。这部分内容的指令操作工作在核心态。

内核是计算机上配置的底层软件,是计算机功能的延伸。不同系统对内核的定义稍有区别,大多数操作系统内核包括四个方面的内容。

1) 时钟管理

在计算机的各种部件中,时钟是最关键的设备。时钟的第一功能是计时,操作系统需要通过时钟管理,向用户提供标准的系统时间。另外,通过时钟中断的管理,可以实现进程的切换。诸如,在分时操作系统中,釆用时间片轮转调度的实现;在实时系统中,按截止时间控制运行的实现;在批处理系统中,通过时钟管理来衡量一个作业的运行程度等。因此,系统管理的方方面面无不依赖于时钟。

2) 中断机制

引入中断技术的初衷是提高多道程序运行环境中CPU的利用率,而且主要是针对外部设备的。后来逐步得到发展,形成了多种类型,成为操作系统各项操作的基础。例如,键盘或鼠标信息的输入、进程的管理和调度、系统功能的调用、设备驱动、文件访问等,无不依赖于中断机制。可以说,现代操作系统是靠中断驱动的软件。

中断机制中,只有一小部分功能属于内核,负责保护和恢复中断现场的信息,转移控制权到相关的处理程序。这样可以减少中断的处理时间,提高系统的并行处理能力。

3) 原语

按层次结构设计的操作系统,底层必然是一些可被调用的公用小程序,它们各自完成一个规定的操作。其特点是:

它们处于操作系统的最底层,是最接近硬件的部分。

这些程序的运行具有原子性——其操作只能一气呵成(这主要是从系统的安全性和便于管理考虑的)。

这些程序的运行时间都较短,而且调用频繁。

通常把具有这些特点的程序称为原语(Atomic Operation)。定义原语的直接方法是关闭中断,让它的所有动作不可分割地进行完再打开中断。

系统中的设备驱动、CPU切换、进程通信等功能中的部分操作都可以定义为原语,使它们成为内核的组成部分。

4) 系统控制的数据结构及处理

系统中用来登记状态信息的数据结构很多,比如作业控制块、进程控制块(PCB)、设备控制块、各类链表、消息队列、缓冲区、空闲区登记表、内存分配表等。为了实现有效的管理,系统需要一些基本的操作,常见的操作有以下三种:

进程管理:进程状态管理、进程调度和分派、创建与撤销进程控制块等。

存储器管理:存储器的空间分配和回收、内存信息保护程序、代码对换程序等。

设备管理:缓冲区管理、设备分配和回收等。

从上述内容可以了解,核心态指令实际上包括系统调用类指令和一些针对时钟、中断和原语的操作指令。

========

操作系统运行环境和运行机制

http://blog.csdn.net/wyi06/article/details/54808145

1)操作系统运行环境(物理机器界面):

a.CPU状态(模式)

处理器由运算器,控制器,一系列的寄存器(用户可见寄存器,控制和状态寄存器)以及高速缓存构成

常见的控制和状态寄存器:

程序计数器(PC:Program Counter),记录将要取出的指令地址

指令寄存器(IR:Instruction Register),记录最近取出的指令

程序状态字(PSW:Program Status Word),记录处理的运行状态如条件码、模式、控制位等信息

操作系统的需求——保护

需要硬件提供基本运行机制:

处理器具有特权级别,能在不同的特权级运行不同的指令集合

硬件机制可将OS与用户程序隔离

处理器的状态(模式MODE)

在PSW中专门设置一位,根据运行程序对资源和指令的使用权限而设置不同的CPU状态

操作系统的两种状态

内核态:运行操作系统

用户态:运行用户程序

特权指令:只能由操作系统使用(启动I/O,内存清零,修改程序状态字,设置时钟,允许/禁止中断,停机)

非特权指令:用户程序可以使用的命令(控制转移,算术运算,

访管指令(又称陷入指令,提供给用户程序的接口,用于调用操作系统的功能(服务)),取数指令)

CPU状态之间的转换

用户态-->内核态(唯一途径:中断/异常/陷入机制)

内核态-->用户态 设置PSW

b.中断/异常机制

中断/异常的概念:CPU对系统发生的某个事件做出的一种反应

事件的发生改变了处理器控制流

中断(外中断如I/O中断,时钟中断,硬件故障),如为了支持CPU和设备之间的并行操作;

异常(内中断如系统调用,页故障,保护性异常,断点指令,其他程序异常)表示CPU执行指令本身出现的问题。

特点:随机发生的,自动处理的,可恢复的

中断/异常机制工作原理

硬件:(发现中断、接受中断)捕获中断源发出的中断/异常请求,以一定的方式响应,将处理器控制权交给特定的处理程序

软件:(中断异常处理程序)识别中断/异常类型并完成相应的处理

中断响应过程:

设备发中断信号,硬件保存现场,根据中断码查表,把中断处理程序入口地址等推送到相应的寄存器,执行中断程序

中断处理程序:

设计操作系统时,为每一类中断/异常事件编好相应的处理程序,并设置好中断向量表

系统运行时若响应中断,中断硬件部件将CPU控制权转给中断处理程序:

保存相关寄存器信息,分析中断/异常的具体原因,执行对应的处理功能,恢复现场,返回被事件大段的程序

2)操作系统运行机制(虚拟机界面):

a.系统调用(操作系统功能调用)

定义:用户在编程时可以调用的操作系统功能

作用:是操作系统提供给编程人员的唯一接口;使CPU状态从用户态陷入内核态

系统调用机制的设计:

中断/异常机制:支持系统调用服务的实现

选择一条特殊指令:陷入指令(访管指令):引发异常,完成用户态到内核态的切换

系统调用号和参数:每个系统调用都事先给定一个编号(功能号)

系统调用表:存放系统调用服务例程的入口地址

用户程序的参数传递给内核:

方法1:由陷入指令自带参数:陷入指令的长度有限,且还要携带系统调用功能号,只能自带有限参数

方法2:通过通用寄存器传递参数:这些寄存器是操作系统和用户程序都能访问的,但寄存器的个数会限制传递参数的数量

方法3:在内存中开辟专用堆栈区来传递参数

系统调用的执行过程

当CPU执行到特殊的陷入指令时:

中断/异常机制:硬件保护现场;通过中断向量表把控制权转给系统调用总入口程序

系统调用总入口程序:保存现场;将参数保存在堆栈里;通过查系统调用表把控制权转给相应的系统调用处理例程或内核函数

执行系统调用例程

恢复现场,返回用户程序

操作系统概述进程/线程模型

========

操作系统-锁机制

http://blog.csdn.net/lz20120808/article/details/51707247

计算机操作系统锁机制.

在多线程编程中,操作系统引入了锁机制。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

所谓的锁,可以理解为内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。 

加锁过程用如下伪码表示: 

1、read lock; 

2、判断lock状态; 

3、如果已经加锁,失败返回; 

4、把锁状态设置为上锁; 

5、返回成功。 

虽然每一步是原子性的,但是每一步之间却是可以中断的。比如进程A在执行完2后发生中断,中断中进程B也执行了加锁过程,返回中断后就会发生两个进程都会加锁。 

对于这个问题,计算机已经解决,方法是采用原子级汇编指令test and set 和swap。

死锁的概念.

死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程. 

比如 两只羊过独木桥。进程比作羊,资源比作桥。若两只羊互不相让,争着过桥,就产生死锁。

死锁的原因.

主要原因(1) 因为系统资源不足。(2) 进程运行推进的顺序不合适,保证有先后顺序。(3) 资源分配不当等。

死锁的必要条件.

产生死锁的四个必要条件: 

(1) 互斥条件:一个资源每次只能被一个进程使用。 

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 

(3) 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺。 

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一 源,……,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。 

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

解决死锁的四个方式. 

1)忽略该问题。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,(鸵鸟策略) 

2)检测死锁并且恢复。(检测与解除策略) 

3)仔细地对资源进行动态分配,以避免死锁。(避免策略) 

4)通过破除死锁四个必要条件之一,来防止死锁产生。(预防策略)

C++多线程开发中,容易出现死锁导致程序挂起的现象。 

解决步骤分为三步: 

1、检测死锁线程。 

2、打印线程信息。 

3、修改死锁程序。

进程(Process)和线程(Thread).

  进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。拥有独立的内存单元。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。但是不能独立运行,必须依存在应用程序中,由应用程序提供多个线程执行控制。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。 

  进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。

进程的状态转换图,及导致转换的事件. 

三个状态: 

1)就绪状态  进程已获得除处理机外的所需资源,等待分配处理机资源,只要分配到CPU就可执行。在某一时刻,可能有若干个进程处于该状态。    

2)运行状态 占用处理机资源运行,处于此状态的进程的数目小于等于CPU的数目。    

3)阻塞状态  由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理机分配给该进程,也无法运行。

========

锁机制与原子操作

http://www.cnblogs.com/kissdodog/archive/2013/04/07/3003822.html

一、线程同步中的一些概念

  1.1临界区(共享区)的概念

  在多线程的环境中,可能需要共同使用一些公共资源,这些资源可能是变量,方法逻辑段等等,这些被多个线程共用的区域统称为临界区(共享区),临界区的资源不是很安全,因为线程的状态是不定的,所以可能带来的结果是临界区的资源遭到其他线程的破坏,我们必须采取策略或者措施让共享区数据在多线程的环境下保持完成性不让其受到多线程访问的破坏。

  1.2基元用户模式

  基元用户模式是指使用cpu的特殊指令来调度线程,所以这种协调调度线程是在硬件中进行的所以得出了它第一些优点:

速度特别快;

线程阻塞时间特别短;

  但是由于该模式中的线程可能被系统抢占,导致该模式中的线程为了获取某个资源,而浪费许多cpu时间,同时如果一直处于等待的话会导致”活锁”,也就是既浪费了内存,又浪费了cpu时间,这比下文中的死锁更可怕,那么如何利用强大的cpu时间做更多的事呢?那就引出了下面的一个模式

   1.3基元内核模式

  该模式和用户模式不同,它是windows系统自身提供的,使用了操作系统中内核函数,所以它能够阻塞线程提高了cpu的利用率,同时也带来了一个很可怕的bug,死锁,可能线程会一直阻塞导致程序的奔溃,常用的内核模式的技术例如Monitor,Mutex,等等会在下一章节介绍。本章将详细讨论锁的概念,使用方法和注意事项

   1.4原子性操作

  如果一个语句执行一个单独不可分割的指令,那么它是原子的。严格的原子操作排除了任何抢占的可能性,更方便的理解是这个值永远是最新的,在c#中原子操作如下图所示:其实要符合原子操作必须满足以下条件c#中如果是32位cpu的话,为一个少于等于32位字段赋值是原子操作,其他(自增,读,写操作)的则不是。对于64位cpu而言,操作32或64位的字段赋值都属于原子操作其他读写操作都不能属于原子操作相信大家能够理解原子的特点,所以在使用原子操作时也需要注意当前操作系统是32位或是64位cpu或者两者皆要考虑。

  1.5非阻止同步

  非阻止同步:不阻止其他线程的情况下实现同步。就是利用原子性操作实现线程间的同步,不刻意阻塞线程,减少相应线程的开销,interlocked类便是c#中非阻止同步的理念所产生的线程同步技术。

  1.6阻止同步

  阻止同步:阻止其他线程,同一时间只允许单个线程访问临界资源。其实阻止同步也是基元内核模式的特点之一。

  例如c# 中的锁机制,及mutex,monitor等都属于阻止同步,他们的根本目的是,以互斥的效果让同一时间只有一个线程能够访问共享区,其他线程必须阻止等待,直到该线程离开共享区后,才让其他一个线程访问共享区,阻止同步缺点也是容易产生死锁,但是阻止同步提高了cpu时间的利用率。

二、为何需要同步

  当多个线程同时访问某个资源,可能造成意想不到的结果。如多个线程同时访问静态资源。

复制代码

    class Program

    {

        static void Main(string[] args)

        {

            //初始化10个线程1去访问num

            for (int i = 0; i < 10; i++)

            {

                ThreadPool.QueueUserWorkItem(new WaitCallback(Run));

            }

            Console.ReadKey();

        }

        static int num = 0;

        static void Run(object state)

        {

            Console.WriteLine("当前数字:{0}", ++num);

        }

    }

复制代码

  输出如下:

  我们看到,num++按照逻辑,应该是1,2,3,4,5,6,7,8,9,10。这就是多个线程去访问,顺序乱套了。这时候就需要同步了。

三、原子操作同步原理

  Thread类中的VolatileRead和VolatileWrite方法:

VolatileWrite:当线程在共享区(临界区)传递信息时,通过此方法来原子性的写入最后一个值;

VolatileRead:当线程在共享区(临界区)传递信息时,通过此方法来原子性的读取第一个值;

复制代码

    class Program

    {

        static Int32 count;//计数值,用于线程同步 (注意原子性,所以本例中使用int32)

        static Int32 value;//实际运算值,用于显示计算结果

        static void Main(string[] args)

        {

            //读线程

            Thread thread2 = new Thread(new ThreadStart(Read));

            thread2.Start();

            //写线程

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(20);

                Thread thread = new Thread(new ThreadStart(Write));

                thread.Start();

            }

            Console.ReadKey();

        }

        /// <summary>

        /// 实际运算写操作

        /// </summary>

        private static void Write()

        {

            Int32 temp = 0;

            for (int i = 0; i < 10; i++)

            {

                temp += 1;

            }

            //真正写入

            value += temp;

            Thread.VolatileWrite(ref count, 1);

        }

        /// <summary>

        ///  死循环监控读信息

        /// </summary>

        private static void Read()

        {

            while (true)

            {

                //死循环监听写操作线执行完毕后立刻显示操作结果

                if (Thread.VolatileRead(ref count) > 0)

                {

                    Console.WriteLine("累计计数:{1}", Thread.CurrentThread.ManagedThreadId, value);

                    count = 0;

                }

            }

        }

    }

复制代码

  输出如下:

  

三、Volatile关键字

  Volatile关键字的本质含义是告诉编译器,声明为Volatile关键字的变量或字段都是提供给多个线程使用的。Volatile无法声明为局部变量。作为原子性的操作,Volatile关键字具有原子特性,所以线程间无法对其占有,它的值永远是最新的。

  Volatile支持的类型:

引用类型;

指针类型(在不安全的上下文中);

类型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool;

具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int 或 uint;

已知为引用类型的泛型类型参数;

IntPtr 和 UIntPtr;

复制代码

class Program

    {

        static volatile Int32 count;//计数值,用于线程同步 (注意原子性,所以本例中使用int32)

        static Int32 value;//实际运算值,用于显示计算结果

        static void Main(string[] args)

        {

            //开辟一个线程专门负责读value的值,这样就能看见一个计算的过程

            Thread thread2 = new Thread(new ThreadStart(Read));

            thread2.Start();

            //开辟10个线程来负责计算,每个线程负责1000万条数据

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(20);

                Thread thread = new Thread(new ThreadStart(Write));

                thread.Start();

            }

            Console.ReadKey();

        }

        /// <summary>

        /// 实际运算写操作

        /// </summary>

        private static void Write()

        {

            Int32 temp = 0;

            for (int i = 0; i < 10; i++)

            {

                temp += 1;

            }

            value += temp;

            //告诉监听程序,我改变了,读取最新吧!

            count = 1;

        }

        /// <summary>

        ///  死循环监听

        /// </summary>

        private static void Read()

        {

            while (true)

            {

                if (count == 1)

                {

                    Console.WriteLine("累计计数:{1}", Thread.CurrentThread.ManagedThreadId, value);

                    count = 0;

                }

            }

        }

    }

复制代码

  输出:

  

四、lock关键字

  lock的作用在于同一时间确保一个对象只允许一个线程访问。

  lock的语法如下:

   static object obj = new object();

   lock (obj)

   {

     //语句块

   }

  我们使用lock来改写上面的示例:

复制代码

    class Program

    {

        static void Main(string[] args)

        {

            //初始化10个线程1去访问num

            for (int i = 0; i < 10; i++)

            {

                ThreadPool.QueueUserWorkItem(new WaitCallback(Run));

            }

            Console.ReadKey();

        }

        static int num = 0;

        static object obj = newobject();

        static void Run(object state)

        {

            lock (obj)

        {

                Console.WriteLine("当前数字:{0}", ++num);

            }

        }

    }

复制代码

  输出如下:

五、Monitor.Enter与Monitor.Exit

  Monitor.Enter和Monitor.Exit这个东西跟lock的作用一样。事实上。lock就是Monitor.Enter和Monitor.Exit的包装。

  下面用Monitor.Enter与Monitor.Exit来实现相同的代码:

复制代码

    class Program

    {

        static void Main(string[] args)

        {

            //初始化10个线程1去访问num

            for (int i = 0; i < 10; i++)

            {

                ThreadPool.QueueUserWorkItem(new WaitCallback(Run));

            }

            Console.ReadKey();

        }

        static int num = 0;

        static object obj = new object();

        static void Run(object state)

        {

            //获取排他锁

        Monitor.Enter(obj);

            Console.WriteLine("当前数字:{0}", ++num);

            //释放排它锁

        Monitor.Exit(obj);

        }

    }

复制代码

六、Monitor.Wait与Monitor.Pulse

  Wait() 和 Pulse() 机制用于线程间交互:

Wait() 释放锁定资源,进入等待状态直到被唤醒;

Pulse() 和 PulseAll() 方法用来通知Wait()的线程醒来;

复制代码

    class Program

    {

        static void Main(string[] args)

        {

            Thread t1 = new Thread(Run1);

            Thread t2 = new Thread(Run2);

            t1.Start();

            t1.Name = "刘备";

            t2.Start();

            t2.Name = "关羽";

            Console.ReadKey();

        }

        static object obj = new object();

        static void Run1(object state)

        {

            Monitor.Enter(obj);

            Console.WriteLine(Thread.CurrentThread.Name + ":二弟,你上哪去了?");

            Monitor.Wait(obj);      //暂时释放锁,让关羽线程进入

            Console.WriteLine(Thread.CurrentThread.Name + ":你混蛋!");

                

            Monitor.Pulse(obj);     //唤醒关羽线程 

            Monitor.Exit(obj);

        }

        static void Run2(object state)

        {

            Monitor.Enter(obj);

            Console.WriteLine(Thread.CurrentThread.Name + ":老子跟曹操了!");

            Monitor.Pulse(obj);     //唤醒刘备线程

            Monitor.Wait(obj);     //暂停本线程

            

            Console.WriteLine(Thread.CurrentThread.Name + ":投降吧,曹孟德当世英雄,竖子不足与谋!!");

            Monitor.Exit(obj);

        }

    }

复制代码

  输出如下:  

七、读写锁ReadWriterLock

  写入串行,读取并行;

  如果程序中大部分都是读取数据的,那么由于读并不影响数据,ReadWriterLock类能够实现”写入串行“,”读取并行“。

  常用方法如下:

AcquireWriterLock: 获取写入锁; ReleaseWriterLock:释放写入锁。

AcquireReaderLock: 获取读锁; ReleaseReaderLock:释放读锁。

UpgradeToWriterLock:将读锁转为写锁;DowngradeFromWriterLock:将写锁还原为读锁。

复制代码

   class Program

    {

        static List<string> ListStr = new List<string>();

        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)

        {

            Thread t1 = new Thread(Run1);

            Thread t2 = new Thread(Run2);

            t1.Start();

            t1.Name = "刘备";

            t2.Start();

            t2.Name = "关羽";

            Console.ReadKey();

        }

        static object obj = new object();

        static void Run1(object state)

        {

            //获取写锁2秒

            rw.AcquireWriterLock(2000);

            Console.WriteLine(Thread.CurrentThread.Name + "正在写入!");

            ListStr.Add("曹操混蛋");

            ListStr.Add("孙权王八蛋");

            Thread.Sleep(1200);

            ListStr.Add("周瑜个臭小子");

            rw.ReleaseWriterLock();

            

        }

        //此方法异常,超时,因为写入时不允许读(那么不用测也能猜到更加不允许写咯)

        static void Run2(object state)

        {

            //获取读锁1秒

            rw.AcquireReaderLock(1000);

            Console.WriteLine(Thread.CurrentThread.Name + "正在读取!");

            foreach (string str in ListStr)

            {

                Console.WriteLine(str);

            }

            rw.ReleaseReaderLock();

        }

    }

复制代码

  异常如下:

  下面是读取并行的例子:

复制代码

    class Program

    {

        static List<string> ListStr = new List<string>();

        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)

        {

            ListStr.Add("貂蝉");

            ListStr.Add("西施");

            ListStr.Add("王昭君");

            Thread t1 = new Thread(Run1);

            Thread t2 = new Thread(Run2);

            t1.Start();

            t1.Name = "刘备";

            t2.Start();

            t2.Name = "关羽";

            Console.ReadKey();

        }

        static object obj = new object();

        static void Run1(object state)

        {

            //获取写锁2秒

            rw.AcquireReaderLock(2000);

            Console.WriteLine(Thread.CurrentThread.Name + "正在读取!");

            foreach (string str in ListStr)

            {

                Console.WriteLine(Thread.CurrentThread.Name + "在读:" + str);

            }

            rw.ReleaseReaderLock();

            

        }

        //此方法异常,超时,因为写入时不允许读(那么不用测也能猜到更加不允许写咯)

        static void Run2(object state)

        {

            //获取读锁1秒

            rw.AcquireReaderLock(1000);

            Console.WriteLine(Thread.CurrentThread.Name + "正在读取!");

            foreach (string str in ListStr)

            {

                Console.WriteLine(Thread.CurrentThread.Name + "在读:" + str);

            }

            rw.ReleaseReaderLock();

        }

    }

复制代码

  输出如下:

  总结:写入锁与任何锁都不兼容,读取锁与读取锁可以兼容。

========
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息