zoukankan      html  css  js  c++  java
  • 《操作系统真象还原》线程

    线程

    简介

      线程是什么?一般我们在书上看到的定义都是:线程是被操作系统调度的最小单位。这定义可能给出的意义并不多,我们在之后讲到为什么有线程之后,就会有一个比较清楚的理解。

      先从线程建立的角度来说一下,线程是什么。

    int pthread_create(pthread_t *__restrict __newthread,
        __const pthread_attr *__restrict __attr,
        void *(*__start_routine)(void *),
        void *__restrict __arg) __THROW __nonnull ((1,3));

      这是Posix版本线程库中创建线程的定义。

      __newthread用于存储线程的id。

      __attr用于指定线程的类型。

      __start_routine是函数指针,用于指定线程执行程序的入口地址。

      __arg用于指定线程执行程序的参数,即__start_routine的参数。

      再看看一个调用该函数的简单例子:

    #include <stdio.h>
    #include <pthread.h>
    
    void * thread_func(void *_arg) {
        unsigned int *arg = _arg;
        printf("new thread: my tid is %u
    ", *arg);
    }
    
    void main() {
        pthread_t new_thread_id;
        pthread_create(&new_thread_id, NULL, thread_func, &new_thread_id);
        printf("main thread: my tid is %u
    ", pthread_self());
        usleep(100);
    }

      线程创建完后,处理器调度到该线程的话,就会像调用函数一样,执行thread_func函数。

      从这意义上说,线程实际上就是一个函数调用,那么你肯定会有疑惑,既然普通函数调用能做到,为什么还要线程。

      这就是线程的特点了,处理器将线程看作一个单独的单元,而将调用普通函数看作某个执行流(线程或进程)中的一段程序。线程能够被处理器直接调度,而普通的函数调用只不过是在线程(或进程)执行过程中顺带执行而已。

      可能上面说的还不清楚,先说下调度器和执行流是什么。

    任务调度器

      任务调度器就是一种操作系统用于将任务轮流调度在处理器上执行的软件模块,以什么方式调度并不是唯一的,可以轮询,也可以以进程的优先级来决定下一个执行的进程,这取决于调度算法。

      为了理解简单,我们任务调度器采用轮询的算法,就是给每个任务一个相同的时间片,然后在所有任务之间调度,例如如果只有两个任务,任务A和任务B,我们用1ms的时间执行任务A,然后把任务A放下,用1ms执行任务B,然后把任务B放下,再用1ms的时间执行任务A,以此类推。这个时间计时是利用时间中断来实现的,所以内核级的调度器是基于中断的

    执行流

      执行流,大到可以是整个程序文件,即进程,也可以小到一个函数调用,即线程。

      执行流是独立的,它有一套独立的栈、独立的寄存器映像和内存资源,即上下文环境。这是Intel处理器硬件上规定的。

      为什么要说执行流这个概念,是因为调度器调度的正是一个执行流,它能够在处理器上独立执行。如何构造一个执行流?只要给它一套独立的栈、独立的寄存器映像和内存资源即可。

      哪些程序可以称为执行流?进程和线程。

      在线程概念出来之前,每个进程就是一个执行流,调度器调度的直接就是整个进程,用线程的说法,就是进程都是单线程进程。

      线程概念出来之后,每个进程就可以包含多个执行流,也可以说将进程整个大执行流,划分为几个“小”的执行流,这个“小的执行流”就是线程,这时候调度器调度的就是“小的执行流”,。

      从上面可以推断,线程才是真正执行流,进程称为执行流只不过是因为它是单线程进程,调度器调度的也是线程,处理器执行的单元也是线程。

       结合上图再理解一下线程和普通函数之间的关系。进程A和进程B再都是单执行流(线程)的时候,调度器就在进程A和进程B之间调度,进程A按顺序执行func_a1到func_an,进程B也类似。但我突然想某个函数被调度器独立调度,比如func_a2,func_b1,可以吗?可以,只要给这两个函数一套独立的栈和独立的寄存器映像就好。这样func_a2和func_b1就变成线程了,能被调度器独立调度了。

      这就很清楚了,普通函数只能作为一个执行流中的一部分,而线程就真正变成一个执行流,调度器可以直接调度了。

    为什么要有线程

      我们对“线程是操作系统调度的单元,是处理器执行的基本单元”有一定的理解了。

      那么,我们为什么要让一个普通的函数变成线程呢?

      有几个原因。

      ① 提速。假设进程A有4个线程,进程B有1个线程,它们被处理器分配相同的时间片,例如都是执行1ms后换另一个线程执行,这样子的话进程A就有80%的时间被执行到,进程B就有20%的时间被执行到,进程A就相当于提速了。

      ② 防止阻塞。 如果进程都是单线程,且进程的线程阻塞掉了,那么程序就不能运行下去了,那相当于整个进程都阻塞掉了,也不能运行了。但如果进程是多线程的话,就算进程某个线程阻塞了,但由于线程是独立的执行流,线程之间不会有影响,那么一个线程阻塞掉了,另外的线程还是能够被处理器处理的,整个进程还是能执行下去。

      ③ 多处理器的出现。再想想提速说到那个例子,如果每个进程都争相开多线程提速,那最后岂不是相当于没有人提到速嘛,而且浪费了上下文环境的资源。所以这种提速方式在单处理器并不是很有效,但如果是多处理器就不一样了,每个执行流(线程)都可以放上任意一个处理器,假设是4核处理器(相当于有四个处理器),正在运行一个4线程的进程,那么这每个线程都可以放在一个处理器里,4个线程就可以真正地并行处理,如果这4个线程在单处理器执行的时间都一样,那在4核处理器执行这个进程就是单处理器的1/4的时间,就算在单处理器执行时间不一样,那也能大幅度提高整个进程的执行效率了,这能做到真正的提速。这一个原因是我个人理解的,书上没有,仅供参考。

    进程和线程

       进程是运行中的程序,线程是CPU执行的基本单位,两者的关系和区别:

      ①进程包含至少一个线程,线程不能独立于进程存在。

      ②进程有自己独立的内存地址空间,即有独立的页表,而线程有独立的栈和独立的寄存器映像,但栈和寄存器上下文也要存储再内存地址空间里面,所以进程就相当于是资源容器,线程相当于资源使用者,线程不能脱离进程存在,线程共享进程的资源。即进程=资源+线程。

      ③线程才是真正被调度器调度,而进程并不是,进程只是个资源的集合体。

    进程、线程的状态

      由于线程才是调度器调度单元,CPU执行的单元也是线程,所以我们讲运行状态是指线程,而不是进程,这里标题的进程可以看作是单线程进程。

      线程或进程的状态最基本有三个,一个是就绪态,代表线程可以放上处理器了,但还没执行,一个是运行态,代表线程正在运行。

      值得一说的是第三个,阻塞态,等待外界条件的状态。当外界条件满足时,线程就会从阻塞态变成就绪态。

      一个会让线程处于阻塞态的例子是:等待文件系统从磁盘把日志读到内存里,如果日志太大,从磁盘读取日志的时间就会越长,线程就处于阻塞态更长时间,因为线程接下来执行的程序需要用到日志的数据,所以线程不得不等待,从而让自己处于阻塞态,让出自己的cpu资源。

    PCB

       PCB是一种描述进程(线程)的数据结构,是进程的身份证。

      结构如下图:

      后面实现线程时为了简化,PCB只包含寄存器映像、栈、栈指针、进程状态、优先级、时间片、页表。

      进程状态,保存进程的状态;

      优先级,在我们实现中,代表进程拿到CPU资源后能执行的时间片,优先级越高,能执行的时间就越长;

      时间片,进程剩余能执行的时间,重新获得CPU资源时,会重置时间片;

      页表,线程不会有页表,进程才有,进程用来描述内存地址空间;

      栈指针指向栈,主要是栈的起始地址可以不固定,所以用指针指向它。

      栈并不是用户级的栈,而是在内核级别的栈,即处于特权级别0时的栈,寄存器映像保存在栈里,栈和寄存器映像一起维护一个线程的上下文环境,当中断或线程调度时都会在该栈保存上下文环境。

    内核级和用户级线程

      线程可以在用户空间内实现,也可以在操作系统内核实现。在操作系统内核实现就是操作系统提供一个创建内核级线程的接口,供用户使用;在用户空间实现,就是由用户进程提供,用户进程除了要处理业务,还要自己写调度器。从特权级别的角度来说,用户进程实现线程只能在特权级别3实现,不能调用特权级别0的内核代码;内核级别在特权级别0实现。

      那么这两种实现方式有什么优缺点呢?

    用户级线程

      在用户进程实现线程有几个优点:

      ①由于线程在用户进程实现,我们可以在那些不支持线程功能的操作系统里实现线程,可移植性很强。

      ②我们可以根据自己的需要给线程加权调度,哪些线程的优先级别高,哪些线程优先级别低都是可以自己决定的,不用涉及到改变内核的程度。

      ③用户进程实现线程不需要陷入内核,那也不会有陷入内核的上下文切换的开销,相当于提速。

      但也有几个比较严重的缺点:

      操作系统调度器并不会意识到用户进程内有线程的存在,它只能调度到用户进程这一级,换而言之操作系统调度器还是以整个进程为调度单位,相当于把整个进程看作一个单线程进程。

      ①如果进程内的用户级线程有一个阻塞掉或崩溃掉的话,由于操作系统认识不到用户级线程的存在,因此会将整个进程挂起,因此无法继续执行下去。

      ②上面说到内核级调度器(这个调度器在多进程或多线程操作系统都会实现的,和线程在哪里实现无关)是基于中断的。但时钟中断到来时,操作系统内核并不知道有用户级线程这个东西存在,所以也不能用内核级调度器调度用户级线程。也只能靠用户级调度器调度线程了,但这又出现一个问题,用户级调度器怎么调度线程呢。用户级调度器不能利用时间中断进行调度, 所以只能让用户级线程主动schedule让出CPU资源,否则就会一直占用整个进程的执行时间,别的线程得不到执行。

      ③虽然说用户级线程减少了陷入内核的上下文开销,但由于内核调度器是以整个进程为调度单位的,那么用户进程内的多个线程分到时间片就会很少,此外还包含用户级调度器维护线程表、运行调度算法的时间片,反而抵消了这个减少陷入内核开销的优势。

    内核级线程

      用户级线程的缺点没了,自然就成了优点,这也是内核级线程的优点了:

      一个是提速,一个是防阻塞,具体可以看回为什么要有线程那一节。

      缺点的话,虽然有陷入内核,保存上下文环境的开销,但相对于优点来说,也没什么。

    线程实现

      我们线程是在内核级别里实现的。

      由于实现的细节过多,这里只简单讲讲线程创建的总流程,以及线程调度时是如何维护上下文环境的。具体代码还是看看书籍吧。

    创建线程流程

      创建线程就是获取内存资源,并初始化PCB结构。

    线程调度流程

      内核级线程是基于时间中断的。下面讲一下内核栈的变化:

      时钟中断到来时,首先会维护中断产生的上下文环境,这个在中断一章有说到,会压入ss,esp,eflags,cs,eip,error_code:

      在执行中断服务程序前,也会压入段寄存器、通用寄存器等,如下图(段寄存器和通用寄存器实际占用多个字,为了展示,放在一个格里了),段寄存器包括ds,es,fs,gs,用于恢复执行服务程序跨段访问数据前的环境,通用寄存器按照cdel原则要调用者压入eax,ecx,edx;中断向量号并不一定要压入,我们中断服务程序有利用到中断向量号,所以压入了;调用函数前压入返回地址。

      然后线程的时间片减1,如果时间片到0的话就会调度到下一个线程。

      如果时间片还没到0,就会将上面的栈所有数据弹出,恢复中断前环境,继续执行当前线程。

      如果时间片到0了,那就要调度到下一个线程,那要保存当前线程的环境了,其实上面栈压入的数据已经维护了大部分线程环境了,剩下就是按cdel原则,被调用者需要压入esi,edi,ebx,ebp通用寄存器,eax,ecx,edx调用者已经压入了,所以不需要再压多一次。

       然后恢复下一个线程的上下文环境,即恢复栈,栈指针可以在该线程的PCB表找到。恢复之后,将esi,edi,ebx,ebp弹出,继续执行中断服务程序,执行完后恢复到中断前的环境,继续执行这个线程的程序。

  • 相关阅读:
    [Android] 升级了新的android studio之后 发生如下的报错,The following classes could not be instantiated:
    [IOS]译Size Classes with Xcode 6: One Storyboard for all Sizes
    [IOS] 利用@IBInspectable
    [IOS]swift自定义uicollectionviewcell
    [IOS]Swift使用SVGKit的记录
    [IOS初学]ios 第一篇 storyboard 与viewcontroller的关系
    [Android]关于filed 遍历资源文件的排序问题
    [Android]关于Installation error: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED ,修改包名
    [Android]用图库打开指定的文件夹,没错是第一个画面直接是图库的文件夹画面
    [Android]新版的sdk中新建一个android应用,增加的PlaceholderFragment这个静态类发生的事情
  • 原文地址:https://www.cnblogs.com/thougr/p/12375311.html
Copyright © 2011-2022 走看看