| 进程 | 线程 |
根本区别 | 资源分配基本单位 | 任务调度的基本单位 (CPU调度和分派的基本单位) |
系统开销 | 创建或者撤销时的开销大 因为要分配或回收资源 | 创建或者撤销时的开销小 因为只需要保存和设置少量寄存器内容 |
通信 | 需要借助IPC(InterProcess Communication) | 可直接读写同一进程的数据来通信 |
其他 | 进程控制块PCB: 描述进程的基本信息和运行状态,创建和撤销进程都是对PCB的操作。 | 线程控制块TCB(待定)? |
联系 | 进程中可以有多个线程,共享进程的资源;使得一个应用程序中,有多个部分可以同时执行。 |
进程之间的通信(IPC,InterProcess Communication)的方式:
- 管道
通过调用pipe函数创建,fd[0]用于读,fd[1]用于写。
限制:只能半双工通信(单向交替传输);只能在父子进程中使用; - FIFO(命名管道)
不仅仅可以用于父子进程;常常用于客户端-服务器应用程序中,FIFO用作汇聚点,在客户端和服务器进程之间传递数据 - 消息队列
相比于FIFO,消息队列的优点:
独立于读写进程存在,从而避免了FIFO中同步管道的打开和关系时可能存在的困难(?)。
避免了FIFO的同步阻塞问题,不需要进程自己提供同步方法。
读进程可以根据消息类型有选择地接收消息,不想FIFO只能默认地接收。 - 信号量 & 共享存储
信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。
共享存储允许多个进程共享一个给定的存储区。不需要在进程之间复制,所以是最快的一种IPC。
信号量用来同步对共享存储的访问。
多个进程可以将同一文件映射到他们的地址空间从而实现共享内存。 - socket
不同与上述的是,socket可用于不同机器间的进程通信。
(自己实现一遍,才能理解。)
进程调度算法
- 先来先服务
- 按照请求顺序
- 有利于长作业,不利于短作业(等待时间可能过长)
- 短作业优先
- 长作业可能会饥饿甚至饿死
- 最短剩余时间优先
- 抢占;永远运训剩余时间最短的,来了新的进程,比当前时间短的话,会抢占;
- 时间片轮转
- 队列,每次每个进程分配固定的cpu时间片
- 优先级调度
- 高优先级先运行;防止低优先级运行不到,可以随着时间提高其优先级
- 多级反馈
- 每级的CPU时间片大小不一样,从低到高;
- 某进程在第一个级别时间片(短)没有运行完会到继续到后续的时间片(长)运行
锁:用来处理并发问题
互斥锁:处理互斥量,取值只能是0和1,保护临界区
信号量:共享内容更复杂的同步,不仅仅是互斥访问
自旋锁:一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,一直占着cpu。
读者-写者模型:
问题描述:多个进程可以实现读操作,只有同一时间一个进程可以执行写操作,且读写进程不能同事操作
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
void reader() {
while(true) {
down(&count_mutex); // 要访问count
count++;
if(count == 1) down(&data_mutex); // 第一个读进程需要对数据加锁,防止写进程访问
up(&count_numtex); // 不用count了
read();
down(&count_mutex); // 要访问count
count--;
if(count == 0) up(&data_mutex); // 没有reader访问data了,解锁数据
up(&count_numtex); // 不用count了
}
}
void writer() {
while(true) {
down(&data_mutex); // 写着要求data无人访问,无论reader还是writer
write();
up(&data_mutex);
}
}
但是这样会导致写进程饥饿或者饿死;解决办法是增加一个约束,不允许任何线程饥饿,对共享数据的锁定在有限的时间结束;
(在写者到达时,现有读者可以完成,但不能再进入其他读者。)
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
semaphore service_queue = 1;
void reader() {
while(true) {
down(&service_queue); // 表明有进程了,不能再锁data_mutex了
down(&count_mutex); // 要访问count
count++;
if(count == 1) down(&data_mutex); // 第一个读进程需要对数据加锁,防止写进程访问
up(&count_numtex); // 不用count了
up(&service_queue); // 下一个进程可以锁data了
read();
down(&count_mutex); // 要访问count
count--;
if(count == 0) up(&data_mutex); // 没有reader访问data了,解锁数据
up(&count_numtex); // 不用count了
}
}
void writer() {
while(true) {
down(&service_queue); // 表明有进程了,不能再锁data_mutex了
down(&data_mutex); // 写着要求data无人访问,无论reader还是writer
up(&service_queue); // 下一个进程可以锁data了
write();
up(&data_mutex);
}
}
死锁
- 必要条件:
- 互斥:不可同时访问某一资源
- 占有和等待:已经得到了某个资源的进程还可以请求新的资源
- 不可抢占:资源只能等着进程释放,不能被其他进程抢走
- 环路等待:>=2的进程形成了环路,环路中的某个进程等待另一个进程释放资源
- 处理办法
- 鸵鸟:不管(发生的概率很低)
- 死锁检测:资源分配图和银行家算法(资源总量,剩余资源量,占有资源量,请求资源量)
- 死锁恢复:
- 抢占
- 回滚
- 杀死进程
- 死锁预防
- 破坏 互斥:如假脱机打印机技术,可以多个进程分配任务,但是其实真正可以请求打印机的进程是打印机的守护进程
- 破坏 占有和等待:在进程开始前分配好进程需要的所有资源
- 破坏 不可抢占
- 破坏 环路等待:给资源编号,进程只能按顺序请求资源
- 死锁避免:安全状态 和 银行家算法
虚拟内存
- 目的:让物理内存扩充成更大额物理内存
- 分页
- 看起来程序分配到了连续的地址空间,但实际上是虚拟的,在物理内存中并不一定是连续的,甚至不一定是存在的,可能在外部存贮中,需要的时候再移回去
- 页面置换算法:
- 最佳(理想状态)
- 最近最久未使用:代价高
- 最近未使用:
- 先进先出:
- 第二次机会算法
- 时钟
- 分段
- 段的长度是不同的,每个段定义了一组有完整逻辑意义的信息
- 段页式
- 1、虚拟地址:段号、页号、页内地址
- 2、比较 段号 和 段表长度,>=则越界
- 3、段表初始地址 + 段号 找到 段表中 页表始址和页表大小
- 4、比较 页号 和 页表长度 ,>=则越界
- 5、页表初始地址 + 页号 得到 页表中 物理内存的块号
- 6、块号 & 页内地址 得到物理内存的地址
内存对齐
- 数据的首地址的值是某个数k(通常它为4或8)的倍数
- 对齐规则:
1.基本类型的对齐值就是其sizeof值;
2.结构体的对齐值是其成员的最大对齐值;
3.编译器可以设置一个最大对齐值,怎么类型的实际对齐值是该类型的对齐值与默认对齐值取最小值得来。