多道程序交替执行是为匹配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+队列+状态
- 运行态→等待态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的
- 等待态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
- 运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
- 就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态
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 中的lock
(Monitor
) 的实现,是结合了两种锁的特点。简单说,它们在发现资源被抢占之后,会先试着自旋等待一段时间,如果等待时间太长,则会进入挂起状态。通过这样的实现,可以较大程度上挖掘出锁的性能重入锁 - 不可重入锁
- 可重入锁(ReetrantLock),也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁
- 使用可重入锁时,在同一线程中多次获取锁,不会导致死锁。使用不可重入锁,则会导致死锁发生。
- Java 中的
synchornized
和 .NET 中的lock
(Monitor
) 都是可重入的读写锁
- 有些情况下,对于共享资源读竞争的情况远远多于写竞争,这种情况下,对读操作每次都进行加速,是得不偿失的。读写锁就是为了解决这个问题
- 读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。简单可以总结为,读读不互斥,读写互斥,写写互斥
- 对读写锁来说,有一个升级和降级的概念,即当前获得了读锁,想把当前的锁变成写锁,称为升级,反之称为降级
- 锁的升降级本身也是为了提升性能,通过改变当前锁的性质,避免重复获取锁
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)
六、内核级线程
参考文章: