首页 > 编程语言 > 详细

【操作系统】【进程管理】进程和线程相关知识点

时间:2020-07-22 23:52:24      阅读:104      评论:0      收藏:0      [点我收藏+]

并发和并行

并发:两个或多个事件在同一时间间隔内发生;

并行:两个或多个事件在同一时刻发生;

线程

线程是进程当中的一条执行流程。同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。

优点

  • 一个进程中可以同时存在多个线程;
  • 各个线程之间可以并发执行;
  • 各个线程之间可以共享地址空间和文件等资源;

缺点

  • 当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃。

线程上下文

多线程编程中一般线程数目大于CPU核心的数目,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片轮转的形式,当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换;

总结:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次在切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换;

线程间同步方式

信号量:信号量机制广泛使用于进程或线程间的同步或互斥,本质上是一个计数器,分为二值信号量和计数信号量,主要的方法有 sem_init(),sem_wait(),sem_post()。

互斥锁:互斥锁是一种简单的方法来控制对共享资源的访问,互斥锁只有两种状态:加锁(lock)和解锁(unlock);互斥锁具有原子性、唯一性和非繁忙等待三大特性,其中唯一性是指如果一个线程锁定了互斥量,在它没有解锁之前,其他线程无法再对该互斥量加锁;非繁忙等待是指如果一个线程试图去对已经加锁的互斥量进行加锁,那么改线程将被挂起(不占用 CPU 的任何资源),直到该锁被释放,那么该线程将被唤醒并继续执行。

pthread_mutex_init()
pthread_mutex_lock() // 挂起加锁
pthread_mutex_trylock() // 非挂起加锁,线程在锁被占据时返回EBASY的错误,而不是挂起
pthread_mutex_unlock()
pthread_mutex_destroy()

条件变量:条件变量与互斥锁不同,条件变量不是用来加锁的,而是一种等待机制。条件变量用来自动阻塞一个线程,直到相应的某种特殊情况发生为止。条件变量是与互斥锁来配合使用的,条件变量使线程睡眠等待某种条件的出现,利用线程间共享全局变量进行同步。一个线程等待条件成立而挂起,另一个线程使条件成立(给出条件成立的信号)而唤醒其他进程。

条件的检测是在互斥锁的保护下进行的;在检测条件之前,首先锁住互斥量,然后若条件不成立那么线程阻塞,并释放互斥锁,当另一个线程改变了条件,那么将会发送信号给关联的条件变量,唤醒一个或多个等待它的线程。

pthread_cond_init()
pthread_cond_wait() // pthread_cond_wait期间会自动解锁,等待条件触发,然后在返回前自动加锁,这种机制可以保证在线程加锁互斥量和进入等待条件变量期间条件变量不被触发
pthread_cond_singal() // 激活一个等待该条件的线程
pthread_cond_boardcast()  // 激活所有等待该条件的线程
pthread_cond_destory()

读写锁:读写锁与互斥锁类似,但它拥有更高的并行性,特点是写独占、读共享。

读写锁的特征:

  • 当以写模式加锁时,之后所有线程对该锁加锁都会被阻塞
  • 当以读模式加锁时,那么线程以读请求对该锁加锁,可以成功,其他失败
  • 当以读模式加锁时,既有线程以写模式对该锁加锁,又有线程以读模式对该锁加锁,那么会阻塞所有的读请求线程,即写进程优先

读写锁非常适合与读的次数远大于写的情况。

内核线程同步机制

自旋锁

自旋锁是互斥锁的一种实现方式,但是对于互斥锁来说加锁失败会放弃 CPU 即处于挂起状态,但是自旋锁会加锁失败会一直占用 CPU,即忙等待(自旋)的状态,不停的循环并且检测锁的状态。

CAS

CAS 的原理,与互斥锁的区别,CAS 存在的问题,如何解决

为什么要引入 CAS?CAS 的概念的引入主要是为了解决多线程竞争环境下,使用互斥锁导致的性能损耗的问题。

  • 在多线程环境下,加锁和解锁会导致比较多的上下文切换和调度延时,导致性能下降
  • 加锁可能会导致线程挂起

CAS 包括三个操作数,内存位置 V,期望原值,新值,如果内存位置的值和期望原值相同,那么使用新值进行更新,否则处理器不做作何操作。CAS 用于无锁编程,在没有使用到锁的情况下实现多线程之间变量的同步,即本线程不被阻塞。CAS 可认为是一个非阻塞轻量级的乐观锁。

使用 CAS 模拟互斥锁的行为就是自旋锁,就是当一个线程在获取锁的时候,如果该锁已被其他线程占用,那么它进入循环等待状态,并检测锁是否能够被成功获取,直到获得锁才会退出循环。若获取锁的线程一直处于活跃状态,那么进入自旋的线程会出现忙等待的现象,造成 CPU 的资源浪费。

CAS 与互斥锁的区别:

在 CAS 失败的情况下,自旋锁会一直自旋,但是对于互斥锁来讲,加锁失败将会导致线程挂起,不消耗 CPU 资源,因此,两者的区别就是自旋的时间和线程上下文切换的时间,在资源充足的情况下,自旋会更快一些;但是在竞争很激烈的情况下,可能只有一个 CAS 可以成功,其他都是失败自旋,那么此时 CPU 的资源消耗可以说是巨大的。解决方案的话我们可以设置超时等。

CAS 存在的问题:ABA 问题,两个线程 p1 和 p2,p1 从内存中读到的值是 A,p2 将值改为 B,然后再改回 A,之后又被 p1 抢占,读取到的值还是 A。由于 CAS 判断的是指针的地址,若该地址被重用,那么问题就很大了,针对此问题我们可以采用具有原子性的内存引用计数。

多线程模型

多线程模型有多对一模型、一对一模型和多对多模型。

  • 多对一模型:多个用户级线程映射到一个内核级线程上,那么线程的管理是由用户空间的线程库来完成的,因此效率更高;但是如果一个线程使用系统调用然后被阻塞,那么将导致整个进程被阻塞;任意时间只能有一个线程访问内核,因此多个线程不能运行在多处理器的系统上,没有真正意义上实现并发。
  • 一对一模型:映射每一个用户级线程到内核级线程,由于为每一个用户级线程都在内核中映射了一个内核级线程,因此允许多个线程并发运行在多个处理器上,并且若其中某个线程使用了系统调用被阻塞,但是内核可以调度其他线程继续执行;缺点是创建内核线程的开销会影响系统的性能,因此系统一般会对用户可以创建的线程数目有限制,如 Linux。
  • 多对多模型:多个用户级线程映射到同样数量或更少数量的内核级线程上。

线程安全问题和可重入函数

线程安全就是在多线程并发执行某一区域的代码,不会出现结果不一致的情况。线程安全的问题一般是由于对全局变量、静态变量的操作造成的。

可重入是多个执行流反复执行一段代码,其结果不会受其他执行流的影响而改变,通常是访问各自的栈资源。

可重入函数的定义时某个执行流由于异常或者中断暂时导致正在执行的函数不被执行,转而其他执行流执行该函数,那么后者对同一个函数的执行结果不会影响前者恢复执行后对函数结果的影响。

区别:

  • 线程安全的函数不一定是可重入的,但是可重入函数一定是线程安全的
  • 如果对临界资源的访问都加上锁,那么该函数是线程安全的,但是如果重入函数若锁还未释放那么将导致死锁,因此是不可重入的

常见的不可重入函数:

  • 使用了静态数据结构
  • 调用了 malloc 和 free 等
  • 调用了标准 I/O 函数
  • 进行了浮点运算(浮点运算大多使用协处理器或者软件模拟来实现)

进程

进程数据结构

进程是由PCB、程序段和相关数据段构成的;

PCB

  • 进程描述信息

    • 进程标识符:标识进程,每个进程都有一个且唯一;
    • 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
  • 进程控制和管理信息

    • 进程当前状态
    • 进程优先级:进程抢占CPU时的优先级;
  • 资源分配清单

    有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的I/O设备信息;

  • CPU相关信息

    CPU中各个寄存器的值,当进程被切换时,CPU的状态信息都会被保存在相应的PCB中,以便进程重新执行时,能从断点处继续执行;

PCB组织方式

  • 通过链表进行组织,把具有相同状态的进程链在一起,组成各种队列;
    • 将所有处于就绪状态的进程链在一起,称为就绪队列;
    • 把所有阻塞的进程链在一起,称为阻塞队列;
  • 通过索引方式,将同一状态的进程组织在一个索引表中,索引表项指向相应的PCB,不同状态对应不同的索引表;
  • 进程创建销毁等状态发生频繁,链表的插入和删除比较灵活,所以一般采用链表;

进程上下文切换

CPU上下文

  • 大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运行,于是就造成同时运行的错觉。任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器
  • CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。
  • 程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
  • 所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。

CPU上下文切换

  • CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务;
  • 系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行;
  • 上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换;

进程上下文

  • 在不同时刻进程之间需要切换,让不同的进程可以在CPU执行,这个进程切换到另一个进程运行,称为进程的上下文切换;

  • 进程是由内核管理和调度的,所以进程的切换只能发生在内核态

  • 进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源;

  • 通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行;

进程上下文切换的场景

  • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
  • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

进程的状态

技术分享图片

状态

  • 运行:进程正在CPU上运行;

  • 就绪:进程处于准备运行的状态;获得了除CPU以外的所有资源;

  • 阻塞: 进程正在等待某一时间而暂停运行;

  • 创建:进程正在被创建,尚未转到就绪状态;

  • 结束:进程正从系统中消失,退出运行;

状态间转换

  • 就绪 =====> 运行:处于就绪状态的进程被调度后,获得CPU,开始运行;

  • 运行 =====> 就绪:处于运行状态的进程时间片用完,必须让出CPU;

  • 运行 =====> 阻塞:进程请求某一资源的使用和分配或等待某一时间的发生时,从运行状态转换成阻塞状态;

  • 阻塞 =====> 就绪:进程等待的事件到来或请求的资源得到响应;

挂起状态

  • 与阻塞状态不是一回事儿,表示进程当前没有占用物理内存;由于虚拟内存管;进程所使用的空间并没有映射到物理内存,而是在硬盘上,进程就会出现挂起状态;
  • 阻塞挂起:进程在外存并等待某个事件的出现;
  • 就绪挂起:进程在外存,但只要进程内存,立刻可以运行;

僵尸进程和孤儿进程

僵尸进程,子进程先于父进程结束,但是父进程没有调用 wait 或者 waitpid 回收子进程的 PCB,那么子进程成为僵尸进程,大量的僵尸进程对系统是有害的,它会一直占用系统的资源。

孤儿进程,父进程先于子进程结束,子进程成为孤儿进程,由 init 进程接管,由 init 进程负责子进程的回收,目的就是为了释放内核资源,因为进程结束后,它会释放用户空间所占用的资源,但是不会释放 PCB,即内核资源,内核资源的释放必须要由该进程的父进程来进行释放。

回收僵尸进程的资源暴力做法是把它的父进程 kill 掉,那么其子进程称为孤儿进程,init 接管,其资源也会被释放,但不是可取的行为。

正确的做法是父进程调用 wait()、waitpid() 方法回收子进程的资源。wait 函数会阻塞直到某个子进程终止,waitpid 提供了非阻塞的选项,并且可以等待指定的进程结束(通过 pid)。

进程通信方式

管道

管道是一种比较原始的进程间通信机制,它是一种以字节流的形式在多进程之间流动,分为无名管道和有名管道,属于半双工通信,管道是存储在内存中的,在管道创建时,内核会给缓冲区分配一个页面的大小。有名管道会存储在磁盘介质上,存在于文件系统中(p),无名管道用于具有亲缘关系的进程之间通信,有名管道可用于任意两个进程之间的通信,有名管道产生的 FIFO 文件,文件结点存储在磁盘上,但是数据还是存放在内存中的。管道与文件的区别在于管道中的数据被读取之后,管道中就没有数据了。管道是 Linux 内核所管理的固定大小的缓冲区,一般是一页的大小,4KB,管道写满了之后,调用 write 会阻塞,管道被读空了之后,调用 read 会阻塞。对于写端关闭管道进行读操作,那么 read 会返回 0;对于读端关闭的管道进行写操作时,会触发 SIGPIPE 信号,导致进程终止。

管道为什么是半双工的? ????

Linux 管道的实现是一个环形缓冲区,每个缓冲区中都有 offset 和 len 标识写入的位置,在读写的时候是要修改这些信息的,如果两个进程同时读或者同时写,那么势必会导致读写冲突,因此内核会对管道上锁,因此管道只能是半双工的。

信号量

信号量机制一般用在进程或线程之间的同步与互斥。信号量本质上一个计数器,用于控制多进程之间对共享数据对象的读取,它和管道有本质的不同,信号量机制不是以传送数据,而是以保护共享资源或保证进程同步为目标的。不存储进程间的通信数据。信号量有二值信号量和计数信号量,二值信号量相当于简单的互斥锁。

消息队列

消息队列是消息的链接表,用户可以从消息队列的末尾添加消息,也可以在消息队列的头部读取消息,消息一旦被接收,就会从消息队列中移除,类似于 FIFO,但是它可以实现消息的随机查询,比如按类型字段取消息。

共享内存

共享内存允许两个或多个进程共享同一块存储区,通过地址映射将该块物理内存映射到不同进程的地址空间中,那么一个进程对于共享内存中数据的修改,另一进程可以实时的看到,从而实现的进程间的通信机制,但是在多线程环境下,需要信号量来控制对共享内存的互斥访问。共享内存是最快的 IPC 机制,只需要两次数据拷贝,而管道和消息队列需要四次数据拷贝:由于管道和消息队列都是内核对象,所执行的操作也都是系统调用,而这些数据最终是要在内存中存储的,因此四次拷贝分别是

  • 从用户空间缓冲区将数据拷贝到内核空间缓冲区
  • 从内核空间缓冲区将数据拷贝到内存
  • 从内存将数据拷贝到内核空间缓冲区
  • 从内核空间缓冲区将数据拷贝到用户空间缓冲区

而共享内存使用 mmap 或者 shmget 函数,在内存中开辟了一块空间,映射到不同进程的虚拟地址空间中,并且向用户返回指向该块内存的指针,因此对该内存可通过指针直接访问,只需要两次:

  • 从用户空间缓冲区拷贝数据到内存
  • 从内存拷贝数据到用户空间缓冲区
mmap() // 建立共享内存的映射
munmap() // 解除共享内存的映射
shmget() // 获取共享内存的标识符ID
shmat() // 建立映射的共享内存
shmdt() // 解除映射

套接字

信号

如何查看进程间通信资源

ipcs 是 Linux 用来查看进程间通信资源的工具。

ipcs -m // 查看系统使用的IPC共享内存资源
ipcs -q // 查看系统使用的IPC消息队列资源
ipcs -s // 查看系统使用的IPC信号量资源

各个方式使用场景

进程调度方式

线程和进程的区别

区别

  • 根本区别:进程是系统进行资源分配的基本单位,线程是任务调度和执行的基本单位;
  • 内存:操作系统每创建一个进程,都会给这个进程分配不同的地址空间,来存储程序所占用的资源,但是线程由于它是共享进程的地址空间的,因此操作系统只为它分配很小一部分内存,TLS,线程局部存储,用来存储线程独有的资源,整体上它是共享进程的资源的。
  • 开销:进程切换的开销很大,需要地址空间的切换,进程内核栈的切换,进程用户堆栈以及寄存器的切换;但是线程可以看做是轻量级进程,共享进程地址空间,它的切换仅仅是线程栈和 PC 寄存器的保存切换,因此线程切换的开销小。
  • 包含关系:包含关系:一个进程至少有一个线程,用于执行程序,称为主线程,或者单线程程序,因此,线程是包含在进程中的。
  • 通信方式:进程之间的通信需要通过进程间通信(IPC),而同一个进程的各线程之间可以直接通过传递地址或者全局变量的方式传递变量。 在 Linux 内核的实现中,并没有单独的线程概念,线程仅仅被视为与进程共享资源的特殊进程,clone 系统调用中传入 CLONE_VM,即共享父进程地址空间;Linux 下分为用户级线程、内核级线程和混合线程。
  • 资源占用:进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
  • 相同之处:线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;

线程为什么能减少开销?

  • 创建:线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
  • 终止:线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
  • 切换:同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
  • 通信:由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;

多线程和多进程的区别

  • 数据共享:进程数据是分开的,共享复杂,需要用 IPC,同步简单;多线程共享进程数据:共享简单,同步复杂。
  • 开销:进程创建销毁、切换复杂,速度慢 ;线程创建销毁、切换简单,速度快
  • 内存和CPU:进程占用内存多, CPU 利用率低;线程占用内存少, CPU 利用率高
  • 实现:进程编程简单,调试简单;线程编程复杂,调试复杂
  • 进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
  • 进程适应于多核、多机分布;线程适用于多核

共享资源

子进程共享父进程的哪些资源

在 Linux 下,只要子进程在运行过程中,不对父进程的数据做修改,那么子进程基本是拷贝父进程的整个页表的,共享父进程的代码段、数据段、堆栈段等,即它们的物理地址是完全相同的(但注意父子进程都拥有自己的虚拟地址空间),先把页表的映射关系建立起来,并不真正进行内存拷贝,如果父子进程修改数据,那么根据写时拷贝机制,操作系统将为子进程相应的数据段和堆栈段分配物理空间,但是代码段还是继续复制父进程的地址空间(物理地址也相同)。另外子进程拥有自己的 PID、PPID 等,另外对于锁,子进程是不继承的。

线程共享进程的哪些资源

对于线程来讲,同一个进程中的线程共享进程的地址空间,即线程共享进程拥有的所有资源。但它们有各自的私有栈,线程局部存储 TLS,线程切换是 TCB 切换,里边存储线程的状态、寄存器集等等。多个线程运行在单一进程的上下文中,共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、数据、堆、共享库和打开的文件。

死锁

死锁是多个进程由于竞争资源而形成的一种僵局,即互相等待的问题,若无外力作用,这些进程都将无法继续执行。

  • 死锁产生的原因:系统资源的竞争、进程的推进的顺序非法、死锁产生的四个必要条件;
  • 死锁产生的四个必要条件:互斥、请求和保持、不可剥夺、循环等待;
  • 预防死锁:破坏死锁产生的四个必要条件(一次性分配、资源有序分配、可剥夺资源)
  • 检测死锁:创建资源分配表和进程等待表,发现死锁;
  • 避免死锁,可以采用银行家算法来进行处理
  • 死锁解除:通过剥夺资源和撤销进程的方式来解除死锁

临界区(资源)

临界资源是一次只允许一个进程或线程所访问的代码段。 临界区是访问临界资源的那段程序。

同步和互斥

互斥指的是对于共享资源保证同一时刻只有一个进程或线程访问,具有唯一性和排他性,但是无法保证对资源访问的有序性。 同步主要指的是对资源的有序访问,多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务,同步基本是要求互斥的,建立在互斥的基础上。

经典的进程同步问题

生产者和消费者

生产者消费者模型是一个典型的同步和互斥的范例,首先生产者和消费者对于缓冲区的访问是互斥关系,同时生产者和消费者都是相互协作的关系。生产者往缓冲区中写入数据必须要保证存在非满缓冲区,消费者从缓存区中读取数据必须要保证存在非空缓冲区。

sem mutex = 1; // 临界区互斥信号量
sem full = 0; // 缓冲区初始化为空
sem empty = n; // 空闲缓冲区

//生产者:
while(1)
{
    p(empty) // 获取空缓冲区资源 
    p(mutex) // 进入临界区
    write data
    v(mutex) // 离开临界区,释放互斥信号量
    v(full) // 满缓冲区数加1
}

//消费者
while(1)
{
    p(full) // 获取满缓冲区单元
    p(mutex) // 进入临界区
    read data
    v(mutex) // 离开临界区,释放互斥信号量
    v(empty) // 空缓冲区数加1
}

读者写者问题

允许多个读者进行读操作,只允许一个写者进行写操作,任一写者在完成写操作之前不允许其他读者和写者对文件进行操作,写者执行写操作之前,应让已有的读者和写者退出。

对于该问题,我们应该首先使用一个计数器 count 来记录读者的数量,需要一个互斥信号量来保护对 count 的修改,还需要一个互斥信号量来保证读者写者互斥地访问文件。

int count = 0;
sem mutex = 1;
sem rw = 1;

writer{
    p(rw)
    write
    v(rw)
}

reader{
    p(mutex)
    if(count == 0)
        p(rw) // 第一个读进程进来阻止写进程写
    ++count
    v(mutex)
    read
    p(mutex)
    --count
    if(count == 0)
        v(rw) // 最后一个读进程读取结束释放信号量
    v(mutex)
}

/* 上述实现有可能导致写进程饥饿现象发生,我们可以添加一个信号量实现写进程优先 */
/* 下述程序主要可以保证在有写请求发生时,之后的读请求都被阻塞 */
int count = 0;
sem mutex = 1;
sem rw = 1;
sem w = 1; // 用于实现写进程优先

writer{
    p(w)  // 在无写进程请求时进入
    p(rw)
    write
    v(rw)
    v(w)
}

reader{
    p(w) // 在无写进程请求时进入
    p(mutex)
    if(count == 0)
        p(rw) // 第一个读进程进来阻止写进程写
    ++count
    v(mutex)
    v(w)
    read
    p(mutex)
    --count
    if(count == 0)
        v(rw) // 最后一个读进程读取结束释放信号量
    v(mutex)
}

哲学家就餐

sem chopstick[5] = {1,1,1,1,1};
sem mutex = 1;
eat()
{
    do{
        p(mutex)
        p(chopstick[i])
        p(chopstick[(i +1) % 5])
        v(mutex)
        eating
        v(chopstick[i])
        v(chopstick[(i + 1) % 5])
        think
    }while(1);
}

银行家算法

银行家算法是著名的死锁避免算法,操作系统按照一定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,若当前系统资源可以满足其最大需求量,那么按照当前的申请量来进行分配资源,否则就推迟分配。当进程在执行中再次申请资源时,要测试该进程对资源的申请量与已分配数量之和是否超过了最大需求量。若超过那么拒绝分配资源,若为超过那么再次测试系统的现存资源是否满足该进程所需的最大资源量,若满足则分配,否则也要推迟分配。

执行安全性算法,若能找到一个安全序列,那么系统安全,否则,系统将处于不安全状态。

数据结构:可利用资源向量 available、已分配资源向量 allocation、需求矩阵 Need、最大需求矩阵 Max,Need = MAX - allocation。

【操作系统】【进程管理】进程和线程相关知识点

原文:https://www.cnblogs.com/Trevo/p/13363297.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!