操作系统——进程+线程+多线程并发锁+IO多路复用

多道程序交替执行是为匹配CPU和I/O设备之间运行速度不匹配,提高CPU使用率

并发:一个CPU上交替执行多个程序

一、进程:

  • 运行中的程序
  • 每个进程都有PCB,用来描述运行中的程序状态
  • 进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)
  • 进程是一个实体,每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)
    • 文本区域存储处理器执行的代码
    • 数据区域存储变量和进程执行期间使用的动态分配的内存
    • 堆栈区域存储着活动过程调用的指令和本地变量

为什么有多进程图像?

  • CPU执行程序,在不同进程之间相互切换就为多进程,是为提高CPU使用率
  • 多进程图像从启动开始到进程结束

二、多进程推进:

  • OS需把进程记录好(即PCB
  • OS需按照合理的次序推进(分配资源、进行调度

三、多进程图像(Linux系统):

1.多进程图像启动:

  • OS开机,main.c中的fork()创建第一个进程
    • init执行了shell(Windows桌面)

  • shell在启动其他进程
    • 一个命令启动一个进程,返回shell再启动其他进程

 2.多进程组织:

  • OS通过PCB形成数据结构(队列)进行组织、管理进程
PCB
  • 多个进程所对应的PCB放在不同地方,形成不同队列,管理进程

 

多进程的组织:

  • PCB+队列+状态
进程状态图

 

  • 运行态→等待态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的
  • 等待态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
  • 运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
  • 就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态

3.多进程交替:

 交替的三个部分:队列操作+调度+切换(switch_to:汇编代码)

映射表:

  • 多进程出现内存冲突
  • 地址分离(物理地址分离)

进程同步与合作:上锁

四、用户级线程

线程:

  • 轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元
  • 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成
  • 轻型实体 线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源
    • 程的实体包括:程序、数据和TCB
    • 线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述
  • 独立调度和分派的基本单位
    • 线程的切换非常迅速且开销小
  • 可并发执行
    • 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行
    • 不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力
  • 共享进程资源
    • ​​​​​​​同一进程中的各个线程,都可以共享该进程所拥有的资源
      • 所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址
      • 可以访问进程所拥有的已打开文件、定时器、信号量等
      • 由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核
      • 线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID

进程 = 资源 + 指令执行序列

线程 = 同一个资源下的多条指令

线程:保留了并发(线程交替执行)的优点,避免了进程切换代价(内存(映射表)无需切换)

线程切换:PC改变,映射表不变

使用 pthread 线程库实现的生产者-消费者模型:

#include <pthread.h>
#include <stdio.h>

#include <stdlib.h>
#define BUFFER_SIZE 10

static int buffer[BUFFER_SIZE] = { 0 };
static int count = 0;

pthread_t consumer, producer;
pthread_cond_t cond_producer, cond_consumer;
pthread_mutex_t mutex;

void* consume(void* _){
  while(1){
    pthread_mutex_lock(&mutex);
    while(count == 0){
      printf("empty buffer, wait producer\n");
      pthread_cond_wait(&cond_consumer, &mutex); 
    }

    count--;
    printf("consume a item\n");
    pthread_mutex_unlock(&mutex);
    pthread_cond_signal(&cond_producer);
    //pthread_mutex_unlock(&mutex);
  }
  pthread_exit(0);
}

void* produce(void* _){
  while(1){
    pthread_mutex_lock(&mutex);
    while(count == BUFFER_SIZE){
      printf("full buffer, wait consumer\n");
      pthread_cond_wait(&cond_producer, &mutex);
    }

    count++;
    printf("produce a item.\n");
    pthread_mutex_unlock(&mutex);
    pthread_cond_signal(&cond_consumer);
    //pthread_mutex_unlock(&mutex);
  }
  pthread_exit(0);
}

int main() {

  pthread_mutex_init(&mutex, NULL);
  pthread_cond_init(&cond_consumer, NULL);
  pthread_cond_init(&cond_producer, NULL);

  int err = pthread_create(&consumer, NULL, consume, (void*)NULL);
  if(err != 0){
    printf("consumer thread created failed\n");
    exit(1);
  }

  err = pthread_create(&producer, NULL, produce, (void*)NULL);
  if(err != 0){
    printf("producer thread created failed\n");
    exit(1);
  }

  pthread_join(producer, NULL);
  pthread_join(consumer, NULL);  

  //sleep(1000);

  pthread_cond_destroy(&cond_consumer);
  pthread_cond_destroy(&cond_producer);
  pthread_mutex_destroy(&mutex);


  return 0;
}

多线程编程所用锁(锁要解决的是线程之间争取资源的问题):

  • 资源是否是独占(独占锁 - 共享锁)
  • 抢占不到资源怎么办(互斥锁 - 自旋锁)
  • 自己能不能重复抢(重入锁 - 不可重入锁)
  • 竞争读的情况比较多,读可不可以不加锁(读写锁

独占锁 - 共享锁

  • 当一个共享资源只有一份的时候,通常我们使用独占锁,常见的即各个语言当中的 Mutex
  • 当共享资源有多份时,可以使用Semaphere

互斥锁 - 自旋锁

  • 对于互斥锁来说,如果一个线程已经锁定了一个互斥锁,第二个线程又试图去获得这个互斥锁,则第二个线程将被挂起(即休眠,不占用 CPU 资源)
  • 在计算机系统中,频繁的挂起和切换线程,也是有成本的。自旋锁就是解决这个问题的
  • 自旋锁:当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
    • 当资源等待的时间较长,用互斥锁让线程休眠,会消耗更少的资源
    • 当资源等待的时间较短时,使用自旋锁将减少线程的切换,获得更高的性能
  • Java 中的 synchornized 和 .NET 中的 lockMonitor) 的实现,是结合了两种锁的特点。简单说,它们在发现资源被抢占之后,会先试着自旋等待一段时间,如果等待时间太长,则会进入挂起状态。通过这样的实现,可以较大程度上挖掘出锁的性能

重入锁 - 不可重入锁

  • 可重入锁(ReetrantLock),也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁
  • 使用可重入锁时,在同一线程中多次获取锁,不会导致死锁。使用不可重入锁,则会导致死锁发生。
  • Java 中的 synchornized 和 .NET 中的 lockMonitor) 都是可重入的

读写锁

  • 有些情况下,对于共享资源读竞争的情况远远多于写竞争,这种情况下,对读操作每次都进行加速,是得不偿失的。读写锁就是为了解决这个问题
  • 读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。简单可以总结为,读读不互斥,读写互斥,写写互斥
  • 对读写锁来说,有一个升级和降级的概念,即当前获得了读锁,想把当前的锁变成写锁,称为升级,反之称为降级
  • 锁的升降级本身也是为了提升性能,通过改变当前锁的性质,避免重复获取锁

1.用户级线程(协程):

协程和线程的区别:

  • 线程是抢占式的调度,而协程是协同式的调度
  • 协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任
  • 协程失去了标准线程使用多CPU的能力

用户级线程通过:

  • Yield函数,进行线程切换
  • Create函数,实现线程并发(创建多个线程)
    • Create函数就是创造出第一次切换时,线程应该有的样子

2.一个线程对应一个栈

  • Yield要切换线程,首先要进行栈切换(TCB切换),PC无切换

  •  jmp204 保留:

  • jmp204 去掉:

            依靠右括号弹出栈,实现PC切换

ThreadCreate核心:

  • 程序做TCB.stack
  • stack中填入线程起始地址

  • ThreadCreate是系统调用,会进入内核,内核知道TCB
  • Yield为用户级线程,内核为schedule(用户不可见,系统调度)

五、IO多路复用

        IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程IO多路复用适用如下场合:

  • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

        与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

常见的IO复用实现

  • select(Linux/Windows/BSD)
  • epoll(Linux)
  • kqueue(BSD/Mac OS X)

六、内核级线程

参考文章: 

操作系统——内核级线程实现_我爱豆子的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/weixin_45864705/article/details/127854769
今日推荐