进程与线程

陷阱与系统调用

CPU模式
CPU通过PSW(程序状态字)的一个二进制位来控制CPU模式,CPU执行时,有以下两种模式。

  • 用户态:CPU仅允许执行整个指令集的一个子集和访问所有功能的一个子集。一般而言,在用户态中有关I/O和内存保护的所有指令是禁止的。
  • 内核态:CPU可以执行指令集中的每一条指令,并且使用硬件的每种功能。

系统调用过程
如果一个进程正在用户态运行一个用户程序,这时候需要一个系统服务,进程会执行一个陷阱或system call系统调用,这里指出,陷阱属于一种软件中断,陷阱把用户态切换成内核态。跳转到内核地址空间,完成相关工作后,把控制权返回给用户程序。由此过程可以看出,系统调用需要在用户空间和内核空间切换,上下文也需要切换, 比普通的过程调用(如函数库调用)开销大。

系统调用类型

  • 用于进程管理的系统调用
在UNIX中,fork是唯一可以在POSIX创建进程的途径。
execve系统调用可以让进程执行命令。
  • 用于文件管理的系统调用
readwrite是读和写文件的系统调用
lseek调用改变读或写的下一个位置的指针。
stat获取文件的属性 
  • 用于目录管理的系统调用
mkdir创建目录
rmdir删除目录
link为一个文件创建多个连接,每一个连接的修改都会被其他连接所见——只有一个文件存在。
mount添加新文件系统
umount卸载文件系统
  • 其他系统调用
kill 发送一个信号给进程
chdir改变当前的工作目录
chmod改变文件的保护模式

以下是read系统调用的步骤。

//read本身C标准库过程,它会执行一个同名的read系统调用
count = read(fd,buffer,nbytes);

完成read系统调用的步骤

  1. 压入nbytes参数(C编译器要求参数从右往左压栈)
  2. 压入&buffer,即缓冲区地址
  3. 压入fd文件句柄
  4. 调用read库过程
  5. 把read系统调用的编号放入寄存器
  6. 执行一个TRAP指令,将用户态切换到内核态,TRAP指令后面跟着库过程的返回地址
  7. 检查系统调用编号表
  8. 根据系统调用编号表,进入相应的系统调用处理句柄
  9. 根据返回地址,返回库过程
  10. 库过程返回调用程序
  11. 增加堆栈指针SP,清理堆栈

库调用和系统调用都称为read,会有问题吗?
系统调用实际上并没有名称,除了在文件中这样描述之外。当库例程read陷入内核时,它将系统调用号码放入寄存器或者堆栈中。该号码通常用于一张表的索引。这里确实没有使用任何名称。而另一方面,库例程的名称是十分重要的,因为它将用于程序中。

程序vs进程

一个进程是一个正在执行的程序。比如,当我们用C++写一个程序并且编译创建二进制代码,原始代码和二进制代码都是程序。当我们实际运行二进制代码时,它就成为一个进程。
一个进程被认为是“活动”实体而相反程序是一个“固化”实体。单个程序可以多次运行时创建许多进程,例如,当我们多次打开一个exe或二进制文件时,许多实例开始执行(创建了许多进程)。
进程与程序的区别:程序就菜谱,CPU就像厨师,而烹饪一系列步骤的总和就是进程。

进程——资源的管理单位

进程之间是竞争关系,线程之间是合作关系。多道程序设计技术中,进程的引入,让CPU更加有效率的完成任务。CPU由一个进程快速切换到另一个进程,每个进程实际上只执行几十或几百毫秒。严格上来讲,这几十或几百毫秒时间内,CPU只能运行一个进程。但一秒内,CPU可以运行多个进程,这就造成了并发执行的错觉。

内存中的进程是下图这个样子
内存中的进程

  • 代码段:进程也有时被称为代码段.它还包括由程序计数器的值表示的当前活动。
  • 堆栈:堆栈包含临时数据,如函数参数,返回地址和局部变量。
  • 数据段:包含全局变量。
  • 堆:在运行时间内动态分配内存进行处理。

进程的属性或特征
一个进程具有以下属性:
1、进程id:操作系统分配的唯一标识符
2、进程状态:可以是就绪,运行,等等
3、cpu寄存器:像程序计数器(当进程被换出还如cpu时,cpu寄存器必须被保存或恢复)
4、账户信息
5、I/O状态信息:例如分配进程的设备,打开文件等
6、cpu调度信息:例如优先级(不同进程可能有不同的优先级,例如 在最短工作优先调度算法下, 一个简短的进程可能被赋予低优先级 )
进程的所有上述属性也称为进程的上下文。 每个进程都有其已知的程序控制块(PCB),即每个进程将具有唯一的PCB。所有上述属性都是PCB的一部分。

进程模型
一个进程就是一个正在执行的程序的实例。每个进程有一个地址空间和至少一个控制线程。每个进程有一个逻辑程序计数器,在多进程并发时,某个时刻,只有一个逻辑程序计数器被装入物理程序计数器中。

创建进程
以下情况下,新进程被创建。注意:同一个程序运行两遍,算两个进程。

  • 系统初始化(由init进程开始,启动登录进程等子进程)
  • 用户请求创建一个新进程(交互式系统中)
  • 执行了正在运行的进程所调用的进程创建系统调用
  • 一个批处理作业的初始化(大型机)

守护进程
停留在后台处理诸如电子邮件、Web页面、新闻之类活动的进程称为守护进程。
在UNIX中,只有一个系统调用可以创建新进程:fork。这个命令创建一个与调用进程相同的副本,父进程和子进程拥有相同的存储映像、同样的环境字符和打开文件,但是不同的进程空间。子进程接着执行execve,以修改器存储映像并运行一个新程序。

进程的终止
以下情况下,进程会终止。

  • 正常退出(自愿的)
  • 出错退出(自愿的)
  • 严重错误(非自愿)
  • 被其他进程杀死(非自愿)

进程的层次结构
UNIX下,操作系统的所有进程层次结构可以看成是以init进程为根的一棵树。

进程的三种状态
运行态(该时刻进程实际占用CPU)
就绪态(可运行,等待CPU)
阻塞态(除非某种外部事件发生,否则进程不能运行)
在UNIX中,常见的阻塞情况是,进程无法从管道或设备文件读取数据,UNIX系统自动阻塞该进程。
进程的状态

进程的实现
操作系统维护着一张进程表(实际上是一个结构数组或链表)。每个进程占用一个进程表项。

中断向量
是I/O类关联的变量,包含设备中断处理程序的入口地址

多道程序设计模型
CPU利用率 = 1-pⁿ
n是多道程序设计的道数(或允许同时在内存中驻留的进程数),p是进程等待I/O的时间与驻留在内存中的总时间的比。因此,pⁿ是每个进程都在等待I/O的概率。
这个模型假设所有的进程都是独立的。但实际的进程不是独立的。

进程间通信

竞争条件
两个或多个进程读写共享数据,而最后的结果取决于进程运行的精确顺序,称为竞争条件。这种数据不一致问题主要出现在一个进程对共享变量读与写中间这段时间,另一个进程调度进来对共享变量进行读写(原进程暂停,堆栈中数据被保存),前者恢复执行时,读取的是未修改的共享变量。

临界区
互斥:当一个进程正在使用一个共享变量或文件时,其他进程不能做同样的操作。
临界区:对共享内存进行访问的程序片段。
为了避免竞争条件,需要满足以下4个条件:
1、Mutual Exclusion条件 :任何两个进程不能同时处于其临界区。
2、不应对CPU的速度和数量做任何假设。
3、Progress条件:临界区外运行的进程不得阻塞其他进程进入临界区。
4、Bounded Waiting条件:不能使进程无限期等待进入临界区。
临界区进程行为

实现互斥的方案:

1、屏蔽中断
但把屏蔽中断的权利交给用户进程是不明智的。而且在一个多核系统中,屏蔽一个CPU的中断不会阻止其他CPU干预第一个CPU所做的操作。
2、锁变量
0表示临界区内没有进程,1表示临界区内有进程。但同样存在一个问题,当一个进程将读锁变量之后,而设置为1(即写之前)之前,另一个进程被调度,读并写锁变量为1,则临界区内有两个进程同时存在。
3、严格轮换法

#进程0
while(TRUE){
    while(turn !=0);    //turn的初值为0,跳出循环,进程0进入临界区
        critical_region();
        turn =1;    //离开临界区后,turn设为1,以便进程1进入临界区
        noncritical_region();    

}
#进程1
while(TRUE){
    while(turn !=1);//当turn的值为1时,进程1进入临界区
        critical_region();
        turn =0;    //离开临界区后,turn设为0,以便进程0进入临界区
        noncritical_region();    

}

连续测试一个变量直到某个值出现为止,称为忙等待。这种方式浪费CPU的时间,但是在等待时间非常短的情形下,可以使用忙等待,忙等待的锁变量称为自旋锁。
存在的问题:
假设进程0比进程1快很多,当进程1还在处理非临界区的工作时,进程1已经第二次执行完非临界区的操作并返回到循环开始,因为turn =1,所以进程0陷入循环中。也就是说,进程0在临界区外阻塞了。

4、Peterson解法

#define FALSE 0
#define TRUE  1
#define N        2    //两个进程

//全局变量
int trun;//现在轮到谁
int interested[N] = {FALSE,FALSE};  //表示两个进程的是否在临界区

void enter_region(int process){
    int other;    
    other = 1-process;    //另一个进程
    interested[process] = TRUE;    //当前进程申请过CPU
    turn = process;    //两个进程同时调用enter_region,后写入的进程号有效
    while(turn == process && interest[other] = TRUE);//轮到process并且另一个进程也在临界区时,挂起
    //返回调用者,进入临界区
}

void leave_region(int process){
    interested[process] = FALSE;//表示process进程离开临界区 
}

彼得森解法的缺点:
1、它涉及忙等待
2、它限于2个进程。

5、TSL
TestAndSet是同步问题的硬件解决方案。在TestAndSet,我们有一个共享锁变量,它可以采用0或1的两个值之一。 0表示解锁 1表示锁 。在进入临界区之前,一个进程询问锁。如果它被锁定,它会继续等待,直到它变得空闲,如果它没有锁定,则获得锁并执行临界区代码。 在TestAndSet中,互斥和进步条件得到保证,但有限的等待时间不能保证。

Test and Set Lock,测试并加锁指令,指令级硬件支持。TSL指令通过CPU锁内存总线来实现读写操作原子性。
TSL RX,LOCK
将内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值(即LOCK=1)。执行TSL的CPU将锁住内存总线,其他CPU在本指令结束前读内存。

enter_region:
 TSL register,lock |复制lock到寄存器,并将lock置为1
 CMP register,#0   | lock等于0吗?
 JNE enter_region   |如果不等于0,已上锁,再次循环
 RET                |如果等于0,锁空闲,返回调用程序,以便进入临界区

leave_region:
 MOVE lock , #0    |置lock为0
 RET                |返回调用程序

Peterson解法和TSL指令分别在软件和硬件层面上实现了互斥,当时它们都有忙等待的缺点。而且还有优先级反转问题。

睡眠与唤醒

  • sleep:一个将调用进程阻塞的系统调用。
  • wakeup:一个唤醒被挂起进程的系统调用。

用一个count共享变量指示缓冲区的数据项数量。当count=0时,在consumer进程读取count,调用sleep之前,此时调度程序切换到producer进程,consumer进程暂停,consumer恢复时,读取count,发现count为0,进入休眠,而另一边producer迟早要填满缓冲区,然后睡眠,最后,两个进程将永远睡眠下去。

信号量
信号量是由一个整型值和一个指针组成,指针指向等待该信号量的进程。整型值用来累积唤醒次数,值为0表示没有保存下来的唤醒操作,值大于0表示一个或多个唤醒操作。
信号量的PV操作:
P操作(down):即获取资源,检测其值是否大于0,如果该值大于0,减1(即用掉一个保存的唤醒信号),若该值为0,则进程将睡眠,但down操作并未结束。检查,修改变量值,睡眠这一系列的操作均作为一个原子操作完成。保证当一个信号量操作开始,在该操作完成之前,其他进程不能访问,修改该信号量。

V操作(up):即释放资源,对一个信号量的值加1,如果有一个或多个进程在某信号量上睡眠,无法完成down操作,操作系统会随机挑选一个进程进程down操作。当某个进程在值为负的信号量(表示有线程在这个信号量上睡眠)上完成up操作后,该信号量仍为0,但在该信号量上睡眠的进程却少了一个。

二元信号量:它们的值只能是0或1,也称为互斥锁。所有的进程可以共享相同的互斥信号量,其初值为1。某个进程必须等到锁变为0。然后,进程可以设互斥信号量为1并进入临界区。当它执行完临界区时,它可以将互斥信号量的值重置为0,以便其他进程可以进入临界区。

计数信号量:它们可以具有任何值并且不受某个域的限制。它们可以用于控制对数量有限制的资源的同时访问。信号量可以初始化为资源的数量。每当进程想要使用该资源时,它检查剩余资源的数量是否大于零,即进程具有可用的资源。那么该过程可以进入临界区,从而将计数信号量的值减少1.在使用资源实例的过程结束之后,它可以离开临界区,从而将可用资源的数量加1。

互斥量
互斥量,是去除了计数能力的信号量。它只有两个状态,解锁和加锁。使用一个整型值,0表示解锁,非零值表示加锁。
互斥量使用两个过程。当一个进程进入临界区时,调用mutex_lock。若lock=0时,调用成功。若lock不等于0,调用线程阻塞,直到lock等于0,而调用mutex_unlock,将lock设为0。

mutex_lock:
    TSL REGISTER,MUTEX    |将互斥量存入寄存器,并将互斥量设置为1
    CMP,REGISTER,#0       |寄存器值为0吗?
    JZE ok                |如果寄存器值为0,则跳转到ok
    CALL thread_yield     |如果寄存器值不等于零,当前进程放弃CPU
    JMP mutex_lock        |稍后再试
    ok: RET               |返回调用者;进入临界区

mutex_unlock:
    MOVE MUTEX,#0         |将互斥量设为0
    RET                   |返回调用者

mutex_lock和enter_region的关键区别在于,mutex_lock取锁失败时,调用了thread_yield,让调用程序调用进程,而enter_region取锁失败时,在忙等待。前者非阻塞,后者阻塞,就像一个亲戚去你家拜访,如果他没有钥匙,非阻塞程序的做法是,他不在门口等待,而转向做其他事,比如逛下超市,过一会儿再来看没有门有没有开,而阻塞程序的做法是,让他在门口等待,直到你回家开门。

条件变量
条件变量允许线程由于未达到的条件而阻塞,直到该条件满足后,唤醒自己。条件变量经常和互斥量一起使用,当一个线程锁住互斥量,进入临界区,然后当它不能获得期待的结果时等待一个条件变量。最后另一个线程会向它发信号,使它可以继续执行。
注:如果向一个条件变量传递一个信号量,但是在该条件上并没有等待进程,则该信号会永远消失。

管程
一个管程是由一个过程、变量及数据结构等组成的一个集合,他们组成一个特殊的模块和软件包。是为了让编译器实现互斥自动化,减少了程序员设计引起出错的可能性。
C/C++不支持管程,Java支持

消息传递
两个系统调用
send(destination,&message);
receive(source,&message);

消息传递系统设计的要点
1、确保消息送到目标
2、区分新消息和重发的老消息
3、身份认证问题,即客户机确保跟正规的服务器通信

消息传递方式
1、为每个进程分配一个唯一的地址,消息按进程的地址编址。
2、引入信箱。在信箱创建时,确定消息数量(数组表示)

屏障(barrier)
barrier是一个同步机制,它规定所有的进程到达屏障之前,任何进程不能进入下一个阶段。

调度

为什么我们需要调度?
典型的进程涉及I/O时间和CPU时间。在像ms-dos这样的单道系统中,等待I/O的时间浪费了,而且在这段时间内cpu是空转的。在多道程序系统中,一个进程可以使用cpu,而另一个进程正在等待i / o。只有进程调度才有可能做到这一点。
进程调度的过程
首先用户态要切换成内核态,然后保存当前进程的状态,包括进程表中的寄存器信息,以及内存映像。接着,通过运行调度程序选定一个新进程,然后将新进程的内存映像放入MMU;最后新进程运行。

进程行为
CPU密集型进程:需要更多的CPU时间或在运行状态下花费更多的时间。
I/O密集型进程:需要更多的i / o时间和更少的CPU时间。I/O密集型进程处于等待状态的时间更多。
两种进程行为
a)CPU密集型进程,b)是I/O密集型进程

I/O活动:当一个进程因为等待外部设备完成工作而阻塞

上下文切换
保存一个进程的上下文并加载其他进程的上下文的过程被称为上下文切换。简单来说,就像加载和卸载进程从运行状态到就绪状态一样。

上下文切换VS模式切换
当cpu特权级别改变时,例如当进行系统调用或发生故障时,会发生模式切换。内核以比标准用户任务更高的权限模式工作。如果用户进程想访问只能内核访问的内容,则必须进行模式切换。当前执行过程在模式切换期间不需要更改。 通常会发生模式切换,以使进程上下文切换发生,换句话说,进程上下文切换一定发生模式切换。上下文切换只能在内核模式下进行。

何时调度
一、创建一个新进程时,要决定运行父进程还是子进程
二、退出进程时,决定运行就绪进程
三、进程因为I/O和信号量或其他原因阻塞,必须调度另一个进程执行。
四、在一个I/O中断发生时

与进程相关的时间
到达时间:进程到达就绪队列的时间。
完成时间:进程完成执行的时间。
突发时间:cpu执行进程所需的时间。
周转时间:完成时间与到达时间之间的时差。 周转时间=完成时间 - 到达时间=等待时间+突发时间
等待时间(W.T):周转时间与突发时间之间的时差。 等待时间=周转时间 - 突发时间

非抢占式调度和抢占式调度
根据何时处理时钟中断,调度算法分为两类,抢占式和非抢占式
非抢占式算法思想是,进程一直运行到阻塞或退出,时钟中断发生时,不会发生调度。除非更高优先级的进程在时钟中断时,等待CPU。优点是减少进程切换,提高性能
抢占式算法,为进程设置一个最大运行周期,进程在时钟中断时,被剥夺CPU,CPU转而运行调度程序。
其实这个最大运行周期设置为无限长时,就会变成非抢占式算法。

调度算法分类

  • 针对批处理系统
  • 针对交互式系统
  • 针对实时系统

调度算法的目标
公平——给每个进程公平的CPU份额
策略强制执行——看到所宣布的策略就执行
平衡——系统中所有的部件都忙碌

批处理系统
吞吐量:系统每小时最大作业数量
周转时间:从提交到终止的最小时间
CPU利用率:保持CPU时钟忙碌
调度算法
1、非抢占式先来先服务:进程按照它们请求CPU的时间顺序使用CPU,要求一个就绪进程的单一队列

2、最短作业优先:适用于运行时可以预知并且非抢占式的批处理调度算法。

3、最短剩余时间优先:调度程序总是选择剩余运行时间最短的进程运行。如果新的进程比当前进程需要更少的时间完成,则选择新的进程。这个算法可以使新的短作业获得更好的服务。

交互式系统
最小响应时间:从发出命令到相应的时间间隔
均衡性:满足用户的期望
调度算法
1、时间片轮转调度:每一个进程被分配固定的时间片,每个进程在用完时间片后,将被剥夺CPU,移到就绪进程队列的末尾。如果进程在时间片内阻塞,CPU立即进程切换。时间片长度难以确定,如果太短,则进程切换的花销占总运行时间比重太大,降低CPU效率,如果太长,进程队列中较晚的就绪进程响应时间太长。
时间片轮转

2、优先级调度:每个进程被赋予一个优先级,优先级高的就绪进程先运行。为了防止优先级高的进程无休止的运行下去,操作系统在每个时钟中断,都降低当前进程的优先级。同时每个进程赋予一个相同的时间片,在时间片用完后,次高优先级的进程被调度运行。
优先级可以动态赋予也可以静态赋予。
优先级调度

3、多级队列
优先级调度和时间片轮转调度可以结合使用。设立优先级类,高优先级类的运行1个时间片,次高优先级运行2个时间片,再次一级运行4个时间片,以此类推.

4、最短进程优先
最短进程优先
估计运行时间最短的进程,让最短运行时间的进程优先运行,缩短平均的周转周期。

5、保证调度
n个用户,每个用户获得CPU处理能力的1/n。假设某个用户有k个进程,用户的中所有进程都是等价的,每个进程获得1/k的CPU时间。

6、彩票调度
每次调度程序随机抽出一张彩票,拥有该彩票的进程获得该资源。

实时系统
满足截止时间:避免丢失数据
可预测性
实时系统要求计算机必须在一个时间范围内给出恰当的反应。
实时系统分为硬实时系统和软实时系统。硬实时系统必须满足绝对的截止时间,软实时系统可以容忍偶尔的错失截止时间。

关于调度算法的一些分析:
1)先来先服务可能导致长时间的等待,特别是当第一个作业占用太多的CPU时间时。
2)最短作业优先和最短剩余时间优先算法可能会导致饥饿。考虑一个情况,当长时间的进程在准备队列里,而更短的进程持续到达队列。
3)如果轮转调度的时间量量非常大,那么它的行为与先来先服务调度相同。
4)对于给定的一组进程,最短作业优先调度在平均等待时间方面是最佳的,即,这种调度的平均等待时间是最小的,但是问题是如何知道/预测下一个作业的时间。

线程

线程共享同一个地址空间和所有可用数据,这是进程做不到的(进程的地址空间互不相关)。线程比进程更轻量级,它更容易建立和撤销。多线程在存在大量计算和I/O处理的情况下,能加速性能。
交互式程序通常适合多线程执行,如果是单线程的,那么等待I/O,然后计算,等待期间,CPU只能空转,性能很低,如果增加一个后台进程负责计算,则能大大提高用户体验。

构造服务器的三种方法
有限状态机:每个计算都有一个被保存的状态,存在一个会发生且是的相关状态发生改变的事件集合。

模型 特性
多线程 并行性、阻塞系统调用
单线程进程 无并行性、阻塞系统调用
有限状态机 并行性、非阻塞系统调用、中断

经典的线程模型
进程用于把资源集中到一起,这些资源放在进程的虚拟地址空间。线程是在CPU上执行的实体。
多线程的含义就是一个进程中运行多个线程,这些线程共享同一个地址空间和其他资源。
进程与线程区别
线程必须在某个进程中执行。进程相对独立,但进程中的线程有相同的地址空间,共享全局变量,甚至,一个线程可以读、写或甚至清除同一进程内其他线程的堆栈。

每个进程中的内容 每个线程中的内容
地址空间 程序计数器
全局变量 寄存器
打开的文件 堆栈
子进程 状态
即将发生的报警
信号与信号处理程序
账户信息

创建新的线程
thread_create库函数,返回一个线程标识符,即新线程的名字
thread_exit库函数,线程退出,堆栈清空
thread_join库函数,阻塞一个线程,知道它等待的线程退出。
thread_yeild库函数,自动放弃CPU从而让别的线程运行。

在UNIX中,如果父进程由很多线程,fork后,子进程也拥有这些线程的副本。则父进程和子进程中相同的线程都被阻塞吗?

POSIX线程
POSIX称为Portable Operating System Interface of UNIX。它是有IEEE的一组标准组成,它将系统调用封装成完全相同格式的接口,并提供给应用程序。它在程序源码级别提高了可移植性。IEEE定义的线程包,称为Pthread。

在用户空间中实现线程
把线程包放在用户空间,内核按正常的方式管理,即单线程进程。优点时,可移植性更高,即使操作系统不支持线程也可以运行。
在用户空间管理线程时,每个进程内有一个线程表。这些表与内核空间中的进程表有些相似,但记录的是线程的属性。该线程表由运行时系统管理和调度。

线程切换远远快于进程的原因
1、只要堆栈指针和程序计数器,整个线程的切换可以在几条指令内完成。
2、不需要陷阱(TRAP指令),不需要上下文切换,不需要修改内存映像,也不需要对内存高速缓存刷新

用户级线程还有个优点,允许每个进程内定制线程调度算法。
用户级线程包也有缺点,包括阻塞系统调用问题、页面故障问题以及线程永久运行问题。
线程阻塞原因:
执行了一个阻塞系统调用或者出现一个页面故障
页面故障:函数调用或跳转到了一条不再内存的指 令上,而操作系统需要到磁盘上取回丢失的指令。

在内核中实现线程
在内核中有用来记录系统中所有线程的线程表。
所有能够阻塞线程的调用都是系统调用形式。内核级线程的速度慢。

调度程序激活机制
模拟内核线程的功能,像运行时系统发出通知,避免了用户空间和内核空间的不必要转换。

弹出式线程
接受一个消息,操作系统创建一个处理该消息的进程。

使单线程代码多线程化
1、对线程而言是全局变量,但是对整个程序而言并不是全局变量。
解决方案:为每个线程赋予其私有的全局变量。具体实现上,调用线程时,在堆或专门调用线程所保留的特殊存储区上,分配空间用于专属全局变量的定义。
2、有许多库过程不是可重入的。可重入,是指当前调用还未结束,程序可以执行第二次调用。例如,UNIX中的malloc函数过程,它维护这一张可用内存链表。在malloc更新表格时,有可能指针的指向不定。如果这时候第二个线程执行了新的调用,那第二个线程可能使用了无效的指针,导致程序崩溃。
解决方案:为每个过程提供一个包装器,该包装器设置一个标志位,标志某个库处于使用中,此时使用该库的其他线程要阻塞。
3、内核的信号很难发送给正确的用户级线程上。并且多线程的信号管理更难。
4、多线程的堆栈管理问题。

线程调度

两种实现位置的差异
对用户级线程,内核不知道线程的存在,先选定一个进程,然后运行时系统选择一个线程,进程内的线程执行完进程的所有时间片,退出,内存选择另一个进程继续运行。
对于内核级线程,内核选择线程时,不会考虑该线程属于哪个进程。同一个进程内的线程切换花销远低于不同进程内的线程切换(不需要修改内存映像,也不需要刷新内存高速缓存的内存)

发布了17 篇原创文章 · 获赞 16 · 访问量 2万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览