操作系统-进程与线程

并发concurrency与并行parallelism的概念

并发(concurrency):值宏观上看起来两个程序在同时运行,但微观上两个程序的指令是交叉运行的。cpu单周期只运行了一个指令。这种并发不能提高计算机性能,只能提高效率

并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令。

进程与线程的概念

  • 进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,从而实现了操作系统的并发
  • 线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发。线程是操作系统可识别的最小执行和调度单位。
  • 每个线程都独自占用一个虚拟处理器(独自的寄存器组、指令计数器和处理器状态PSW)。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码),打开的文件队列和其他内核资源。

区别:

  1. 一个线程只能属于一个进程,一个进程至少有一个线程。线程依赖于进程而存在。
  2. 进程在执行过程中有独立的内存单元,而多个线程共享进程的内存。资源分配给进程,同一进程的所有线程共享该进程的所有资源。统一进程的线程共享代码段,数据段和扩展段(堆存储)。但是每个线程拥有自己的栈段,用来存放所有局部变量和临时变量。
  3. 进程是资源分配的最小单位,线程是CPU调度的最小单位。
  4. 系统开销:在创建或撤销进程时,系统都要分配或回收资源,因此操作系统所付出的开销将显著地大于在创建或撤销线程是的开销。在进行进程上下文切换时,涉及到整个当前进程CPU环境的保存以及新被调用运行的进程的CPU环境设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。
  5. 通信:同一个进程多个线程具有相同的地址空间,使得他们之间实现同步和通信比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信,需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预。
  6. 进程编程调试简单可靠性高,但是创建销毁开销大;线程开销小,切换速度快,但是编程调试相对复杂。
  7. 进程间不会相互影响;一个线程将导致整个进程挂掉
  8. 进程适用于多核、多机分布;线程适用于多核。

进程的五种状态

死锁发生的条件以及如何解决死锁

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的相互等待的现象。死锁发生的四个必要条件为:

  1. 互斥条件:进程所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
  2. 请求和保持条件:进程获得一定资源后,又对其他资源发出请求,该资源可能被其他进程占用,此时请求阻塞但当前进程不会释放已经占有的资源
  3. 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
  4. 环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链

解决死锁的方法只要破坏上述四个条件中的一个即可:

  1. 资源一次性分配,(一次性分配进程所需要的所有资源)剥夺请求和保持条件(可能造成其他进程饥饿)
  2. 可剥夺资源:当进程新的资源未得到满足时,释放已经占有的资源,从而破坏不可剥夺的条件
  3. 资源有序分配:系统给没类资源赋予一个序号,每个进程按编号递增地请求资源,释放则相反,从而破坏环路等待条件。

进程间通信方式

  1. 管道
    管道主要包括无名管道和命名管道。管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
    普通管道PIPE:它是半双工(数据只能在一个方向上流动),具有固定的读端和写端。只能用于具有亲缘关系的进程间的通信(父子进程或兄弟进程)。可以看成一种特殊的文件,对于它的读写可以用普通的read,write函数。但它不是普通文件,不属于任何文件系统,只存在于内存中。
    命名管道FIFO:FIFO可以在无关的进程之间交换数据。FIFO有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中。

  2. 系统IPC

    2.1 消息队列:
    消息的链接表,存放在内核中。一个消息队列由一个标识符来标记。(消息队列克服了信号semaphore传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限的进程可以按照一定规则向消息队列中添加新信息,对消息队列由读权限的进程可以从消息队列中读取信息。
    1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
    2)消息队列独立于发送和接收进程。进程终止时,消息队列及其内容并不会被删除。
    3)消息队列可以实现消息的随机查询,消息不一定要以FIFO的次序读取,也可以按消息的类型读取。

    2.2 信号量semaphore
    semaphore是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间的通信数据。
    1)信号量用于进程间的同步,若要在进程间传递数据需要结合共享内存。
    2)信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作。
    3)每次对信号量的PV操作不仅限于对信号量值加1或减1,可以加减任意正整数。
    4)支持信号量组。

    2.3 信号signal
    信号是一种比较复杂的通知方式,用于通知接收进程某个事件已经发生。

    2.4 共享内存 shared memory
    它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,(互斥锁和信号量)
    1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取
    2)因为多个进程可以同时操作,所以需要进行同步
    3)信号量+共享内存通常结合使用,信号量用来同步对共享内存的访问

  3. 套件字socket
    socket也是一种进程间通信机制,与其他通信机制不同的是他可以用于不同主机间的进程通信。

线程间通信方式

  1. 临界区:是一段代码。通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
  2. 互斥量 Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  3. 信号量 Semaphore:为控制具有有限数量的用户资源而设计,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问该资源的最大线程数。
  4. 事件(信号)Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

Linux的4中锁机制

  1. 互斥锁 mutex:用于保证任何时刻,都只能有一个线程访问该对象。当获取锁失败是,线程会进入等待,等待锁释放时被唤醒。
  2. 读写锁 rwlock:分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。注意,写锁会阻塞其他读写锁。当有一个线程获得写锁在写的时候,读锁也不能被其他线程获取;写锁优先于读锁。
  3. 自旋锁 spinlock:任何时刻只有一个线程访问对象。但是当获取锁失败的时候,不会进入睡眠,而是在原地自旋(忙等待),知道锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁短暂的环境下会极大提高效率。但是如果加锁时间过长,会非常浪费CPU资源
  4. RCU:read-copy—update,在修改数据时,首先要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者激活不需要同步开销,既不需要获得锁,也不实用原子指令,不会导致锁竞争,不需要考虑死锁问题。但对写者的同步开销比较大。在有大量读操作,少量写操作的情况下效率非常高。

有了进程,为什么还要有线程

进程可以使多个程序能够并发执行,以提高资源的利用率和系统的吞吐量,但是具有一些缺点:

  • 进程在同一时间只能干一件事
  • 进程在执行的过程中如果阻塞,整个进程会被挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行

因此操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少并发执行时所付出的时空开销,提高并发性。和进程相比,线程的优势为:

  • 从资源上讲,线程是一种”节俭“的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护代码段,堆栈段和数据段,较为昂贵。
  • 从切换效率上讲,运行于一个进程中的多个线程,他们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。
  • 从通信机制上来将,线程间具有方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据传递只能通过进程间通信的方式进行,这种方式费时而且很不方便。同一进程下的线程共享数据空间,所以一个线程的数据可以直接为其他线程所用。
  • 除此之外,多线程还使CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU。
  • 改善程序结构。一个长并且复杂的进程可以考虑分为多个线程,成为几个独立或半独立运行的部分,这样的程序才会利于理解和修改。

关于之前项目中TI-RTOS:该实时操作系统中的task实际上是线程thread,不是process。能够方便进行数据通信。

切换线程需要保存哪些上下文

线程在切换的过程中,需要保存当前线程id,线程状态,堆栈,寄存器状态等信息。其中寄存器主要包括SP,PC,EAX等寄存器。

主要功能为:
SP:堆栈指针,指向当前栈的栈顶地址
PC:程序计数器,存储下一条将要执行的指令
EAX:累加寄存器,用于加法惩罚的缺省寄存器。

汇编的寄存器

数据寄存器:

  • AX 数据累加寄存器 (算数运算的主要寄存器)
  • BX 基址寄存器 (寻址时用作基址)
  • CX 计算寄存器 (循环指令操作或串处理指令中隐含计数)
  • DX 数据寄存器

指针与变址寄存器:

  • SP 堆栈指针寄存器
  • BP 基址指针寄存器
  • SI 源变址寄存器
  • DI 目标变址寄存器

段寄存器:

  • CS 代码段
  • DS 数据段
  • SS 栈段
  • ES 附加段 (辅助数据区,其实是堆区)

控制寄存器:

  • IP 指令指针寄存器
  • FR 控制标志位

创建进程fork和vfork的区别

fork是创建一个和当前进程映像一样的进程。

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

成功调用fork会创建一个新的进程,新进程与调用fork的进程几乎一模一样(除了pid不一样),这两个进程都会继续运行。在子进程中,成功fork会返回0。在父进程中fork返回子进程的pid。

最常见的fork是创建一个新进程,然后使用exec载入二进制映像,替换当前进程的映像。这种情况下,fork了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。”派生加执行“

现在的unix系统(linux)采用了写时复制的方法,内核将父进程的内部数据结构和进程页表项复制一份,但并不对地址空间的内容进行逐页复制。

vfork

fork之后立刻执行exec会造成地址空间的浪费。

除了子进程必须要立刻执行一次对exec的系统调用,或者调用_exit退出,对vfork的成功调用所产生的结果和fork是一样的。vfork会挂起父进程直到子进程终止或者运行一个新的可执行文件的影响。通过这样的方式,vfork避免了地址空间按页复制。这个过程中,父进程和子进程共享相同的地址空间和页表项。实际上vfork只完成了复制内核数据结构这一件事。

常用的线程模型

  1. Future模型
    该模型通常在使用的时候需要结合Callable接口配合使用。
    Future是把结果放在将来获取,当前主线程并不急于获取处理结果。允许子线程先进行处理一段时间,处理结束之后就把结果保存下来,当主线程需要使用的时候再向子线程索取。
    Callable是类似于Runnable的接口,其中call方法类似于run方法,所不同的是run方法不能抛出受检异常没有返回值,而call方法则可以抛出受检异常并可设置返回值。两者的方法体都是线程执行体。
  2. fork&join模型
    该模型包含递归思想和回溯思想。递归用来拆分任务,回溯用于合并结果。可以用来处理一些可以进行拆分的大任务。其主要是把一个大任务逐级拆分为多个子任务,然后分别在子线程中执行,当每个子线程执行结束后逐级回溯,返回结果进行汇总合并,最终得出想要的结果。
  3. actor模型 (我用的过是TI-RTOS)
    actor模型属于一种基于消息传递机制并行任务处理思想,它以消息的形式来进行线程间数据传输,避免了全局变量的使用,进而避免了数据同步错误的隐患。actor在接受到消息之后可以自己进行处理,也可以继续传递给其他actor进行处理。
  4. 生产者消费者模型
    核心是使用一个缓存来保存任务。开启一个或多个线程来圣战任务,然后再开启一个或多个来从缓存中取出任务进行处理。这样的好处是任务的生成和处理分隔开,生产者不需要处理任务,只负责生成任务然后保存到缓存。而消费者只需要从缓存中取出任务进行处理。
  5. master-worker模型
    类似于任务分发策略,开启一个master线程接收任务,然后在master中根据任务的具体情况进行分发,交给其他worker子线程。然后由子线程处理任务。如果需要返回结果,worker处理结束之后把处理结果返回给master。

操作系统从用户态到内核态的转化原理

  1. 用户态切换到内核态的三种方式
    (1)系统调用,用户主动要求切换到内核态的唯一一种方式。
    (2)外部中断,外围设备完成用户请求操作后发出的中断型号。
    (3)异常,在CPU执行运行用户态程序的时候,发现了某件不可知的异常。比如缺页异常
  2. 切换方式
    (1)从当前进程的描述符中提取其内核栈的ss0和esp0信息
    (2)使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来。保存当前信息。
    (3)将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应寄存器,开始执行中断处理程序。
  3. 为什么要分内核态和用户态
    为了安全性。在cpu的一些指令中,有的指令如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户操作这些指令的时候,内核为其提供了API,可以通过系统调用陷入内核态,让内核去执行这些操作。

进程同步和异步

进程同步:
我们把异步环境下的一组并发进程因直接制约而互相发送消息、进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。

具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。

异步调用:
一个可以无需等待被调用函数的返回值就让操作继续进行的方法,(多用“多线程”来实现)。