操作系统之进程·线程

在传统操作系统中,每个进程有一个地址空间和一个程序序列。这是传统上进程的定义。不过经常存在同一个地址空间中运行多个程序序列的情况,这些类似分离的进程。

多线程的使用

1.许多应用中同时发生着多种活动,这些活动需要共享大量数据,但同时又相对独立。例如编辑工具等待输入,又可以自动保存文本。这个时候使用多进程就很难或者无法实现。2.另中情况,其中某些活动随着时间的推移会阻塞,通过将这些应用分解成多个并行运行的序列,在某个序列阻塞后不影响其他序列的运行,程序设计模型会变得简单。3.同时随着多核体系结构的发展,一台计算机经常有两个以上的CPU,多线程可以使一个程序同时利用多个CPU成为可能。

经典线程模型

进程模型基于两种独立的概念:资源管理和执行。有时,将这两种概念分开会更好,由此引入了线程的概念。进程把程序的的相关资源集中在一起,进程存放了程序的正文和数据以及其他资源,这些资源包括打开的文件、子进程、即将发生的定时器、信号处理程序、账号信息等。另一个概念是,进程拥有一个执行的序列,通常成为线程。在线程中有一个程序计数器,用来记录程序要执行哪一条指令。线程拥有寄存器,用来保存当前的工作变量。线程还拥有一个堆栈,用来记录执行历史。线程中主要包含了执行序列相关的但非公用的信息。但是线程和进程是不同的概念,现代系统中进程扮演的是资源管理的概念,是一个资源容器,用于把资源集中到一起。而线程是程序的执行序列,是真正在CPU中运行的对象,CPU上被调度的实体。

线程给进程增加了一项内容,即在同一个进程环境中,允许彼此之间有较大独立性的多个线程执行。多个线程共享一个地址空间和其他资源。进程其实也是类似的概念,多个进程共享CPU、物理内存、磁盘和其他资源。当多线程在单CPU中运行时,线程轮流运行。在进程的多道程序设计中,CPU在行多个进程之间来回切换,系统制造了不同进程并行运行的假象。多个线程的工作方式也是类似的。CPU在线程之间来回切换,制造了线程并行运行的假象。

进程中的线程不像进程之间有很大的独立性。所有的线程都有完全一样的地址空间,这意味这它们也共享着同样的全局变量。由于各个线程可以访问地址空间的每一个内存地址,所以一个线程可以访问甚至修改另一个线程的堆栈。线程之间没有保护。有两个原因:1不可能2没必要。线程不同与进程,不同的进程可能来自不同的用户,它们之间可能存在敌意,是相互竞争的关系。而线程往往是一个用户创建的,创建的目的是为了彼此协作而不是相互竞争。往往需要紧密的协作。

进程 线程

地址空间

全局变量

子进程

定时器

信号

扫描二维码关注公众号,回复: 17192569 查看本文章

账号信息

程序计数器

寄存器

堆栈

状态

与上章的进程概念的区别就是,把程序执行相关的信息保存到了线程块中。

在多线程的情况下,进程通常会从当前的单个线程开始。这个线程通过调用一个库函数创建新的线程。创建时,没有必要对线程的地址空间加以规定,因为新线程会自动运行在创建线程的地址空间中。有时线程是有层次的,它们具有一种父子关系,但是通常不存在这种关系,所有线程都是平等的。

当一个线程完成工作后,可以通过一个库过程thread_exit退出。该线程接着消失,不可调度。一些系统中,可以调用一个过程,例如thread_join,一个线程可以等待另一个特定线程推出。这个过程阻塞调用线程直到特定的线程推出。另一个常见的线程调用是thread_yield,它允许线程主动放弃CPU从而让另一个线程运行。这个功能对线程模型很重要,因为不同于进程,线程之间是协作关系,并且线程库无法利用时钟中断强制线程让出CPU。所以需要通过某种方式让线程相互配合,随着时间的推移自动交出CPU,以便让其他线程有机会运行。

在用户空间中实现的线程

有两种主要的方式实现线程包:在用户空间中和在内核空间中。两种方式互有利弊,当然混合方式也是可能的。

第一种方式是把整个线程包放在用户空间中,操作系统对线程一无所知。从内核的角度看,操作系统按照正常的方式调度进程,这种方式的优点就是可以在不支持线程的操作系统上实现。

在用户空间上管理线程,每个进程都有自己的线程表,用来跟踪该进程中的线程,这些表和内核的进程表类似,不过仅仅记录线程相关的属性,如每个线程的程序计数器、堆栈指针、寄存器、状态。该线程表由运行时系统管理,当一个线程转换到就绪状态或者阻塞状态时,在该线程表中保存重启启动该线程所需的信息。

当某个线程做了一些会引起本地阻塞的事情之后,例如等待进程中另一个线程完成工作,它调用一个运行时系统的过程,一个过程检查进程是否必须进入阻塞状态,如果是,他在线程表中保存该线程的寄存器,查看表中可运行的就绪线程,并把新线程的保存值重新装入CPU的寄存器中。只要堆栈针、程序计数器、寄存器一被切换,新的线程就自动投入运行。进行类似与这样的线程切换至少比陷入内核要快一个数量级,这是使用用户包的极大优点。用户线程还有另一个优点,它允许每个进程有自己定制的调度算法。

尽管用户线程包有更好的性能,但它也存在一些明显的问题。

一个问题是如何实现阻塞系统调用。假设在还没有任何按键之前,一个线程读取键盘。让该线程实际进行系统调用是不可接受的,因为这会停止所有的线程。使用线程的一个主要目标是,首先允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他线程。一个方式是系统调用可以全部改成非阻塞调用,但是这需要修改操作系统,用户级线程的一个长处就是它可以在现有的操作系统上运行,另外修改系统调用的语意需要修改许多用户程序。另一种替代方案就是如果某个调用会阻塞,就提前通知。在类unix系统中,又一个系统调用select可以允许调用者通知预期的read是否会阻塞。若系统有这个调用,那么库过程read就可以被新操作替代,首先进行select调用,然后只有在安全的情况下菜进行read调用,如果read调用会阻塞,有关的调用就不进行,代之以运行另一个线程。到了下次有关的运行系统取得控制权后,就可以再次检查现在进行read调用是否安全。这个处理方法需要重写部分系统调用库,效率不高也不优雅,好像也没有其他可选的方案了。在系统调用从事检查这类代码称为包装器。

另一个问题缺页中断问题,如果线程运行中,发生了缺页故障,操作系统将从磁盘读取需要访问的页面到主存,相关的线程就会阻塞,但是系统不知道线程的存在,通常会把整个进程阻塞知道磁盘IO完成为止,尽管其他线程是可运行的。

用户级线程另一个问题是,如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程主动放弃CPU。在一个单独的进程内部,没有时钟中断,所以不可能有轮转调度的方式调度线程。除非某个线程能够按照自己的意志进入运行时系统,否则调度程序就没有任何机会运行。一个可解决的方案是请求每秒一个的时钟中断,但是这样对程序是生硬和无序的,而且开销是客观的。线程也可能需要时钟中断,这就会扰乱运行时系统使用的时钟。

用户级线程最大的负面争论是,程序员通常是在经常发生阻塞的情况下才希望使用多线程。例如,在多线程Web服务器里,这些线程持续的进行系统调用,一旦发生内核陷阱进行系统调用,如果原有的线程已经阻塞,就很难让内核进行线程切换,如果要让内核消除这种情况,就要持续的进行select系统调用,那么对于那些基本上是CPU密集型而极少阻塞的应用程序而言,使用多线程就毫无意义。

在内核空间中实现的线程

现在考虑在内核支持线程的情况。此时,不再需要用户级线程的运行时系统,每个进程中也没有线程表。相反,在内核中有用来记录所有线程的线程表。当某个线程想要创建一个线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用完成对线程表的创建或撤销工作。

内核线程表保存了每个线程的寄存器、状态、程序计数器、堆栈指针等信息。这些信息和用户空间的线程一样,但是现在保存在内核中。

所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,实现代价是比较小的。当一个线程阻塞时,内核可以在线程表调度一个就绪状态的线程继续运行,这个线程可能是和阻塞线程属于同一个进程也可能不是。而在用户级线程中,运行时系统始终运行自己的进程中的线程,知道内核剥夺它的CPU为止。

内核线程不需要任何新的、非阻塞的系统调用。另外,如果某个进程的线程引起页面故障,内核可以很方便地检查该进程是否有其它可运行线程,如果有,在等待所需的页面从磁盘读入时,就可以选择一个可运行的线程运行。这样做的主要缺点是系统调用的代价比较大,如果线程的操作比较多,就会带来很大的开销。

混合实现

人们已经研究了各种试图将用户级线程的优点和内核级线程的优点结合起来的方法。一种方式是使用内核级线程,然后将用户级线程和内核线程多路复用起来。采用这用方法,内核只识别内核级线程,并对其调度。其中一些内核级线程会被多个用户级线程复用。如同在没有多线程能力的操作系统中某个的用户级线程一样,可以创建、撤销和调度这些用户级线程。Golang的线程是一个很典型的例子,下图中我创建了10000个线程,

package main

import (
	"time"
	"fmt"
)

func main() {
	for i:=0;i<10000;i++{
		go func() {
			time.Sleep(time.Minute)
		}()
	}
	fmt.Println("gogogo")
	time.Sleep(time.Minute)

}

但在mac系统中,内核实际只创建了10个线程。我们会在以后的博文里专门介绍Go语言的线程实现。

POSIX线程

为了实现可移植的线程程序,IEEE定义了线程的标准。它定义的线程包叫作pthread。大部分unix或类unix系统都支持该标准。

线程调用 描述
pthread_create 创建线程
pthread_exit 退出线程
pthread_join 等待一个特定线程退出
pthread_yield 释放CPU来运行另一个线程
pthread_attr_init 创建并初始化一个线程的属性结构
pthread_attr_destory 删除一个线程的属性结构

下面是用C语言在mac系统上的示例,代码可以不加修改的运行在linux系统上。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void thread_test(void * i){
    (*((int*)i))++;
}

int main(int args, char **argv) {
    pthread_t pthread;
    int a=0;
    int status;
    status=pthread_create(&pthread,NULL,thread_test,(void *)&a);
    if(status!=0){
        printf("error");
        exit(-1);
    }
    pthread_join(pthread,NULL);
    printf("a=%d\n",a);
    exit(NULL);
}

main函数首先声明pthread_t结构,pthread_create创建thread_test线程,线程修改main函数堆栈的一个变量,等待线程thread_test运行完成,打印变量a。

a=1

猜你喜欢

转载自blog.csdn.net/u013259665/article/details/86001352
今日推荐