现代操作系统:进程与线程(五)

Challenges and Questions挑战和问题

假设一个进程有几个线程,如果这些线程之一执行了以下操作,会发生什么?

  1. 执行fork()开辟一个新进程;
  2. 关闭一个文件;
  3. 请求更多的内存;
  4. 通过lseek移动文件指针;

看一个好玩的问题理解一下

pid_t pid1 = -1, pid2 = -1, pid3 = -1;

pid1 = fork();

printf("pid1 = %d; pid2 = %d; pid3 = %d\n", pid1, pid2, pid3);

pid2 = fork();

printf("pid1 = %d; pid2 = %d; pid3 = %d\n", pid1, pid2, pid3);

pid3 = fork();

printf("pid1 = %d; pid2 = %d; pid3 = %d\n", pid1, pid2, pid3);

2.2.2 POSIX Threads POSIX线程

POSIX线程(pthreads)是IEEE标准规范,被许多Unix和类Unix系统支持。Pthreads遵循上面的经典线程模型,并指定诸如pthread_create、pthread_yield等例程。

经典模型的另一种选择是所谓的Linux线程,将在4e的10.3.2节中讨论。

 

2.2.3 Implementing Threads in User Space在用户空间中实现线程

这个想法是写一个(线程)库,充当一个迷你调度器,并实现thread_create、thread_exit、thread_wait、thread_yield等。这个库被链接到用户的进程中,并作为该进程中的线程的运行时系统。这个库维护和使用的中心数据结构是一个线程表,类似于操作系统本身的进程表。在每个多线程进程中都有一个线程表和一个线程库实例。

用户模式线程的优点:

  • 不需要任何操作系统的修改。在以前,这是主要的优势,因为大多数操作系统不支持线程。现在,主要的系统都支持线程,所以这个优势不那么重要;
  • 非常快,因为没有上下文切换;
  • 可以为每个应用程序定制调度程序;

缺点:

  • 阻塞系统调用不能直接执行,因为那会阻塞整个进程。例如,考虑上面使用用户模式线程以自然方式实现的生产者消费者示例。这个实现不会很好地工作,因为每当发出一个导致进程阻塞的I/O时,所有的线程都将无法运行(参见下面的内容);
  • 类似的,一个页面错误(我们将在几周后研究)会阻塞整个进程(即,所有线程)。
  • 此外,一个具有无限循环的线程会阻止该进程中所有其他线程的运行。

Possible Methods of Dealing With Blocking System Calls处理阻塞系统调用的方法

  • 可能操作系统提供了一个非阻塞版本的系统调用,例如,一个非阻塞读。
  • 可能操作系统提供了另一个系统调用,它告诉阻塞的系统调用实际上是否会阻塞。例如,Unix select()可以用来判断读操作是否会阻塞。它可能不会阻塞,例如:请求的磁盘块在缓冲区缓存中(参见I/O章节);请求的键盘、鼠标或网络事件已经发生。

Relevance to Multiprocessors/Multicore多处理器和多核

对于一个单处理器,这是我们所正式考虑的,将纯粹的计算分割成小块没有什么好处,如果CPU为了处理所有的线程无时不刻都处于活动状态,那么只使用一个(单线程)进程会更简单。

但是对于多处理器/多核,情况就不同了。现在,将计算划分为线程并让每个线程在单独的处理器/核心上执行是非常有用的。在这种情况下,用户模式线程非常棒,没有系统调用,而且极低的开销是有益的。

然而,在为这种环境编写应用程序时涉及到一些严重的问题。

2.2.4 Implementing Threads in the Kernel在内核中实现线程

现代操作系统直接支持线程,也就是说,线程操作是在内核中实现的。这自然需要对操作系统进行重大修改,而且远非一项微不足道的工作。

  • 整个系统只有一个线程表,而且它在操作系统中。
  • 线程创建现在是系统调用,因此明显慢于用户模式线程。然而,它们仍然比创建/切换/等进程快得多,因为有太多的共享状态不需要重新创建。
  • 阻塞的线程不会引起特别的问题。内核可以从这个进程运行另一个线程(或者从另一个进程运行另一个线程)。
  • 同样,一个线程中的页面错误或无限循环不会自动阻塞进程中的其他线程。

Homework 8

16. 一个线程可以被时钟中断抢占吗?如果是,在什么情况下?如果不是,为什么?

       在用户模式下实现的线程是不能被时钟中断抢占的,因为在该模式下时钟中断针对进程,而线程在进程内部活动,调度器只能等待进程的时间片结束。而在内核模式下实现的线程可以被时钟中断抢占,当线程运行的时间足够久时,时钟中断会将当前线程转入就绪态,而调度器再选择一个合理的线程继续执行。

 

18. 在用户空间中实现线程的最大优势是什么?最大的缺点是什么?

       用户空间中实现线程最大的优势是效率,因为不需要使用内核来切换线程,有进程自身控制线程的切换,但是与之带来的问题是当一个线程被阻塞时,进程中的所有线程会同时被阻塞。

 

2.2.5 Hybrid Implementations混合实现

即使内核也有线程,也可以编写(用户级)线程库。这有时被称为N:M模型,因为N个用户模式线程运行在M个内核线程上。在这个方案中,内核线程协作执行用户级线程。

  • 同一个进程中不同的内核线程可以有不同数量的用户线程分配给它们。
  • 在一个内核线程的用户级线程之间切换非常快(没有上下文切换)。它本质上与纯用户模式线程的情况相同。
  • 在同一个进程的内核线程之间切换需要一个系统调用,本质上与纯内核级线程的情况相同。
  • 由于阻塞系统调用或页面故障只阻塞了一个内核线程,整个多线程应用程序仍然可以运行,因为该进程的其他内核线程中的用户级线程仍然是可运行的。
  • N:M术语的一个分支是,内核级线程(没有用户级线程)有时被称为1:1模型,因为可以将每个线程看作是由专用内核级线程执行的用户级线程。

 

2.2.6 Popup Threads弹出线程

其思想是在消息到达时自动发出线程创建系统调用。(另一种方法是在接收系统调用时阻塞线程或进程。)如果实现得好,消息到达和线程执行之间的延迟可以非常小,因为新线程没有状态要恢复。

 

2.2.7 Making Single-threaded Code Multithreaded使单线程代码成为多线程

绝对不适合胆小的人。经常有不应该分享的状态。一个经常被引用的例子是Unix errno变量,它包含最后一次系统调用所遇到的错误的错误号(0表示没有错误)。Errno并不优雅(即使在普通的单线程应用程序中也是如此),但它的使用非常广泛。如果多个线程发出错误,系统调用第二个的errno值会覆盖第一个,因此第一个errno值可能会丢失。

  • 许多现有代码,包括许多库,是不可重入的。
  • 管理多线程应用程序中固有的共享内存打开了竞争条件的可能性,我们将在调度后马上学习。当一个信号被发送到一个进程时,应该做什么?它会被发送到所有线程还是只有一个线程?
  • 堆叠增长应该如何管理?通常,内核会在需要时自动增加(单个)堆栈。如果有多个堆栈呢?

posted on 2021-10-26 11:07  ThomasZhong  阅读(203)  评论(0)    收藏  举报

导航