1、进程和线程的区别
进程:一个程序在一个数据集上的一次运行过程。系统资源分配的单位。
一个程序在不同数据集合上运行或一个程序在同样数据集上的多次运行都是不同的进程。
进程是独立的,有自己的内存空间和上下文环境,无法获取其他进程的存储空间。同一进程的两段代码不能同时执行,除非引入线程。
线程:进程的一个实体,是被系统独立调度和执行的基本单位,CPU使用的基本单位。
同一进程的线程可以共享同一内存空间。线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。线程占用的资源少于进程占用的资源
进程和线程可以有优先级。
2、内存中的进程结构
自上而下: 栈 –> <– 堆 数据 文本
栈:函数参数、返回地址、局部变量(运行入口知道大小)
堆:运行期间动态分配的内存空间(运行的时候才知道大小)
数据段:全局变量、静态变量、常量(编译后知道大小)(未初始化的在一个区域(.bss),初始化的在相邻区域(.data))
全局变量:定义在函数外面,其他文件也能使用(external linkage)
静态变量:static 关键字修饰的变量:
函数外定义:全局变量,只在当前文件中可见( internal linkage)
函数内定义:全局变量,只在此函数内可见
(C++)类中定义:全局变量,只在此类中可见
文本段:程序的二进制代码段
程序还包含当前活动,通过程序计数器的值和处理器寄存器的内容表。
进程地址空间:内核地址空间+用户地址空间(代码段、数据段、堆、栈、共享库)
堆和栈的区别
栈:函数参数、返回地址、局部变量(运行入口知道大小)
1. 编译器自动分配释放,存放函数的参数值,局部变量的值等。
2. 申请后的响应:若栈的剩余空间大于申请空间,系统将为程序提供内存,否则提示栈溢出
3. 大小限制:向低地址扩展,连续的内存区域,栈顶地址和栈最大容量是系统事先规定好的。如果申请的空间超过栈的剩余空间将栈溢出
4. 申请效率:系统自动分配,速度快,程序员无法控制
5. 存储的内容:函数调用时进栈顺序:主函数下一条指令的地址(函数调用语句的下一条可执行语句)、函数的各个参数(大多数c编译器中参数是从右往左入栈)、函数的局部变量。
调用结束的出栈顺序:局部变量、函数参数(从左到右)、栈顶指针指向最开始存的地址(即主函数的下一条指令)
堆:运行期间动态分配的内存空间(运行的时候才知道大小)
1. 程序员自己分配释放,分配方式类似于链表
2. 申请后的响应:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时会遍历该链表,寻找第一个空间大于所申请空间的堆结点,将该结点从空闲结点链表删除,分配该结点的空间给程序。会在这块内存空间中的首地址处记录本次分配的大小。
这样delete语句才能正确释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动将多余的那部分重新放入空闲链表中
3. 大小限制:向高地址扩展,不连续的内存区域,用链表存储,遍历方向是由低地址向高地址。堆大小受限于计算机系统的有效虚拟内存。获得的空间更大更灵活
4. 申请效率:用new分配,速度慢,容易产生内部碎片,使用方便
5. 存储的内容:一般在堆的头部用一个字节放堆的大小
3、pcb进程控制块
进程标志 进程状态 程序计数器 寄存器
cpu调度信息:进程优先级、调度队列指针、其他调度参数
内存管理信息:基址寄存器 界限寄存器 页表/段表
记账信息:cpu时间、实际使用时间、时间界限、记账数据、作业或进程数量
I/O状态信息:分配给进程的IO设备列表、打开文件列表
上下文context切换
cpu硬件状态从一个进程换到另一个,运行的进程硬件状态保存在cpu寄存器中,不运行时保存在pcb中,之后可推送至cpu寄存器
将cpu切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态。当发生一个中断,系统需要保存当前运行在cpu中进程的上下文,从而在处理完后能恢复上下文,即先中断进程再继续。
进程上下文用pcb表示
进程切换:切换全局页目录加载新地址空间,切换内核栈和硬件上下文;进程a切换到b,保存a上下文环境,更新a的pcb,a移至合适队列,b设为运行态,从b的pcb恢复上下文。
3、进程状态
创建(信息设置完但资源有限)运行(占用cpu)就绪(等待分配cpu) 等待(等待某个是啊金的发送) 终止(进程完成执行)
进程状态图
进程的创建需资源:cpu时间、内存、文件、IO设备
在一个进程创建子进程时,子进程可能从操作系统那里直接获得资源,或从父进程获取资源;父进程可能在子进程之间分配共享资源;
初始化数据由父进程传递给子进程
进程层次结构:进程由其他进程创建,unix:以init为根,windows各进程地位相同
进程映像:进程地址空间、硬件寄存器、pcb和各种数据结构、进入进程时所需的内核栈
4、进程间的通信如何实现?(IPC)
进程间的通信方式有:信号,信号量、消息队列、共享内存。
信号和信号量可以实现同步互斥,信号是用信号处理器来进行的,信号量是用PV操作来实现的;
消息队列是比较高级的通信方法,可被多个进程共享(IPC),一个进程消息太多,可以用于多于一个的消息队列;
共享消息队列的进程所发送的消息除了message本身外还有一个标志,这个标志可以指明该消息将由哪个进程或哪类进程接受,每个共享消息队列的进程针对这个队列也有自己的标志,可以用来声明自己的身份。
管道(pipe):半双工通信,数据单向流动;只能父子进程通信;速度慢
流管道(s_pipe):去除第一种,可双向传递
有名管道(FIFO):任何进程都能通信;速度慢
信号量(semophore):计数器,控制多个进程对共享资源的访问(多进程或线程的同步方法);不能传递复杂消息
消息队列:消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递少、管道只能承载无格式字节流以及缓冲区大小受限的缺点;容量受限,第一次读的时候要考虑上一次没有读完数据的问题
需要消息复制,不需考虑同步问题,不适宜信息量大或操作频繁的场合
信号:用于通知接收进程某个事件已经发送
共享内存:映射一段能被其他进程访问的内存,由一个进程创建,可被多个进程访问,要保持同步。与信号量结合使用,用来达到进程间的同步及互斥,最快的IPC方式
不需要消息复制,信息量大,快捷,在任意数量的进程之间进行高效双向通信的机制。
socket:可用于不同机器间的进程通信,由ip地址和端口号连接而成
telnet 23 http 80 ftp 21 ssh22 hhtps 443
所有连接都有唯一的一对socket
5、进程调度
选择一个可用的进程到cpu上执行
进程进入系统,会被加入到作业队列(包括系统的所有进程)队列通常用链表实现,头结点指向的链表的第一个和最后一个pcb块的指针,每个pcb包括一个指向就绪队列的下一个pcb的指针域
运行—>就绪:IO请求(–>IO队列–>IO结束);时间片结束;创建一个子进程(等待子进程结束);等待中断(中断发生)
6、线程
线程由线程ID、程序计数器、寄存器集合(处理器状态)和栈(用户栈+内核堆栈),私有存储区域(为各种运行时库和动态链接库(DLL)所用)组成;共享进程的地址空间,代码段、数据段等其他资源,如打开文件和信号;程序以单线程进程开始,线程由线程创建和撤销
线程的上下文:寄存器集合、栈、私有存储区域
多线程编程:
响应度高:部分线程阻塞或执行较冗长的操作,该程序仍能继续执行,增加对用户的响应度
资源共享:线程默认共享它们所属进程的内存和资源
经济:进程创建所需要的内存和资源分配比较昂贵,线程能共享资源,创建和切换线程开销小
多处理器体系结构的利用:在多cpu上使用多线程增强了并发功能
线程池
多线程服务器d)在进程开始时创建一定数量的线程,并放入到池中以等待工作,服务器收到请求的时候会唤醒池中的一个线程并将处理的请求传给它;线程完成服务后返回池中再等待工作。如果池中没有可用的线程,那么服务器会一直等待直到有可用线程。
引入线程:减少开销(创建和切换花费时间少,通信不需要内核)、提高性能(多处理器)、应用需要(web服务器)
线程的实现:
unix:用户级别,内核无法感知线程存在,切换较快,同进程的线程不能分到多cpu上,阻塞会阻塞整个进程;
windows:内核级线程,内核中包含线程表,调度以线程为单位
7、互斥和同步
sem_post(sem_t *sem)
1
增加信号量的值,当有线程阻塞在这个信号量时,调用这个函数会使其中一个线程不再阻塞(选择机制由线程调度机制决定)
sem_wait(sem_t *sem)
1
等待信号量的值大于0,以原子操作将信号量减1
对二进制信号量mutex:pthread_mutex_lock(pthread_mutex_t *mutex);加锁
pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁
1
2
消费者生产者代码
producer:
sem_wait(&empty);
pthread_mutex_lock(&mutex);
// producing...
pthread_mutex_unlock(&mutex);
sem_post(&full);
consumer:
sem_wait(&full);
pthread_mutex_lock(&mutex);
// consuming...
pthread_mutex_unlock(&mutex);
sem_post(&empty);
写者读者代码
写写互斥、读写互斥、读读允许
读者优先:除非写者正在写文件,否则读者不需要等待
用一个read_count记录读者数目,为0时方可释放等待队列的一个写者,读者读文件时read_count++,因此需要一个mutex来实现对全局变量read_coun修改时的互斥。
写写互斥:临界对象wmutex
读写互斥:临界对象fmutex
read_file() {
pthread_mutex_lock(&mutex);
if (read_count == 0) {
sem_wait(&fmutex);// 等待写者释放fmutex
}
++read_count;
pthread_mutex_unlock(&mutex);
// reading...
pthread_mutex_lock(&mutex);
--read_count;
if (read_count == 0) {
sem_post(&fmutex); // 读完就释放fmutex
}
pthread_mutex_unlock(&mutex);
}
write_file() {
sem_wait(&wmutex);// 阻塞写进程,保证只有一个写进程阻塞在fmutex
sem_wait(&fmutex);
// writing...
sem_post(&fmutex);
sem_post(&wmutex);
}
写者优先:除非读者正在写文件,否则读者不需要等待
用一个write_count记录写者数目,为0时方可释放等待队列的一个读者,写者写文件时write_count++,因此需要一个mutex来实现对全局变量write_coun修改时的互斥。
写写互斥:临界对象wmutex
读写互斥:临界对象fmutex
read_file() {
while (true) {
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex1);
if (read_count == 0) {
sem_wait(&fmutex);
}
pthread_mutex_unlock(&mutex1);
// reading...
pthread_mutex_lock(&mutex1);
--read_count;
if (read_count == 0) {
sem_wait(&fmutex);
}
pthread_mutex_unlock(&mutex1);
}
}
write_file() {
pthread_mutex_lock(&mutex);
if (write_count == 0) {
sem_wait(&fmutex);
}
++write_count;
pthread_mutex_unlock(&mutex);
sem_wait(&wmutex); // 阻塞写进程,只有一个能等fmutex的释放
// writing...
sem_post(&wmutex);
pthread_mutex_lock(&mutex);
--write_count;
if (write_count) {
sem_post(&fmutex);
}
pthread_mutex_unlock(&mutex);
}
8、线程同步的方式
互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
信号量:允许多个线程访问同一资源,但需要控制同一时刻访问此资源的最大线程数量。
事件(信号):通过通知操作的方式来保持多线程同步,还能方便实现多线程优先级的比较操作
自旋锁
多处理器:获取/释放自旋锁;单处理器:禁止/使用内核抢占
mutex(互斥器)和临界区(critical section)区别
mutex是用于进程之间互斥
临界区是用于线程之间互斥
处理器调度
cpu调度:在合适的调度时机,按调度算法,调度就绪队列中的进程进cpu
9、调度算法:
FCFS先来先服务、SJF最短作业优先(下一cpu区间长度)
优先级调度
无穷阻塞/饥饿:可以运行无穷等待cpu——>解决=>ji老化:增加在等待很长时间的进程的优先级
多级队列调度:将就绪队列分成多个独立队列【分优先级,优先级高的队列先执行/队列之间划分时间片】
多级反馈队列:根据不同cpu区间的特点划分进程。如果进程使用过多的cpu时间,会被转移到更低优先级队列
SRTN最短剩余时间优先、最高响应比优先HRRN 批处理看重吞吐量、周转时间、cpu利用率、平衡
轮转RR、最高优先级HPF、最短进程优先SPN 交互式系统看重响应时间、平衡
LAST最短剩余时间作业优先
垃圾回收的优点和原理,两种回收机制
垃圾回收可以有效防止内存泄露,有效使用可用的内存。
垃圾回收器作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时调用垃圾回收器对某个对象或所有对象进行垃圾回收。
回收机制有分代复制垃圾回收,标记垃圾回收和增量垃圾回收3种。
快表—-cache OS
为提高系统的存取速度,在地址映射机制中增加一个小容量的联想寄存器(快表),用来存放当前访问最频繁的少数活动页面的页号。当某用户需要存取数据时,根据数据所在的逻辑页号在块表中找到其对应的内存块号,再联系页内地址,形成物理地址。如果在快表中没有相应的逻辑页号,地址映射仍可以通过内存中的页表进行,得到空闲块号后将该块号填入块表的空闲块中。如果快表中没有空闲块,则根据淘汰算法淘汰某一行,再填入新的页号和块号。
高速缓冲存储器—-Cache CPU
位于cpu和内存之间的临时存储器,容量小于内存但交换速度快,在cache中的数据是内存中的一小部分,但这一小部分是短时间内cpu即将访问的。当cpu调用大量数据时,可以避开内存直接从cache中调用,加快读取速度。
DLL文件 动态链接库文件
不能单独运行的文件,允许程序共享执行特殊任务所必需的代码和其他资源,比较大的应用程序都由很多模块组成
静态调用方式:由编译系统完成对dll的加载和应用程序结束时DLL卸载的编码
动态调用方式:由编译者用API函数加载和卸载DLL来达到调用DLL的目的,使用上较复杂,更能有效使用内存
cpu状态:内核态、用户态
中断机制:cpu暂停执行当前程序,保留现场,硬件自动转去处理程序,处理完后回到断点,继续被打断的程序
10、临界区问题
【共享数据的互斥】
临界区:在该区中进程可能改变共同变量、更新一个表、写一个文件;没有两个进程能同时在临界区内执行。
do {
【进入区】 // 请求允许进入其临界区
临界区
【退出区】
剩余区
} while (true);
eg. Peterson算法
int turn; // 表示哪个进程可以进入临界区
boolean flag[2]; // 表示哪个进程想要进入临界区
// Pi进程的结构-------------------------------
do {
// 进入区
flag[i] = true;
turn = j;
while (flag[i] && turn == j);
// 临界区
flag[i] = false; // Pi最多在Pj进入临界区一次后就能进入---有限等待
// 剩余区
} while (true);
满足3个要求:
互斥(进程在临界区内执行,其他进程就不能在其临界区内执行)
前进(如果没有进程在其临界区执行且有进程需进入临界区,那么只有那些不在剩余区内执行的进程可参加选择,以确定谁能下一个进入临界区,且这种不能无线)
有限等待:从一个进程请求允许进入临界区到进入临界区为止,其他进程允许进入其临界区的次数有限
信号量
信号量S是一个整数型变量,信号量分为计数信号量(初始化为可用资源的数量)和二进制信号量(互斥锁)。
除了初始化外,只能通过两个标准【原子】操作:wait()和signal()来访问(这些操作被成为P测试和V增加)
// 进程需要资源的时候
wait(S) {
while (S <= 0); // 被阻塞----忙等待
S--;
}
// 进程释放资源的时候
signal(S) {
S++;
}
这里定义的信号量【自旋锁】的主要缺点:
忙等待:当一个进程位于其临界区内时,其他试图进入临界区的进程需要在进入区连续第循环,浪费了cpu时钟
优点:进程在等待锁时不需要上下文切换,节省时间(如果锁占用时间短)
克服忙等:进程信号量不为正时不忙等二十阻塞自己,放入一个与信号量相关的等待队列中,状态为等待。
死锁
是多个进程无限等待一个事件,而该事件只能由这些进程之一产生。
处理死锁的方法:防止发生或发生后处理
产生死锁有4个必要条件:
互斥条件:一个资源每次只能被一个进程使用【无法被破坏】
请求与保持条件:一个进程因请求被其他进程占有的资源而阻塞时,对已获得的资源保持不放【资源静态分配、防止进程在等待状态下占用资源】
非抢占条件:资源只能在进程完成时自动释放,不能被抢占【允许进抢占其他进程占有的资源】
循环等待条件:若干个进程之间形成一个首尾相接的循环等待关系【资源有序分配】
哲学家进餐问题:
五个哲学家圆桌5个筷子,只有同时拿到左右两个筷子才可以吃饭;
semaphore chopstick[5];
do {
wait(chopstick[i]);
wait(chopstick[i+1]%5);
// eat
signal(chopstick[i]);
signal(chopstick[i+1]%5);
// think
} while (true);
假如5个哲学家同时拿起左边的筷子,所有哲学家都在等待右边的筷子,会永远等待。
解决方法:最多值允许4个哲学家同时坐在桌上;只有两个筷子都可以使用时才允许一个哲学家拿起它们;使用非对称解决方法:奇数哲学家先拿起左边的筷子再拿起右边的筷子,偶数哲学家相反。
银行家算法 检查申请者对资源的最大需求量,若系统现存的各类资源可以满足申请者的需求,就满足申请者的需求。这样申请者可以很快完成计算,然后释放它占用的资源,从而保证了系统中所有进程能完成,避免死锁的发生。
死锁
四个条件:
互斥:资源一次只有一个进程使用,不能共享
占有并等待:一个进程至少占有一个资源并等待另一资源,而该资源被另一进程占有【一个进程申请一个资源时不能占有其他资源】
非抢占:资源只能等进程完成任务自动释放,不能抢占【可以抢占】
循环等待:有一组循环等待的进程【对所有资源类型进行完全排序,要求每个进程按递增顺序来申请资源】
四个条件全部满足才会出现死锁
10、内存管理
基地址寄存器:最小的合法物理内存地址
界限地址寄存器:决定范围大小
MMU(内存管理单元):虚拟地址到物理地址的映射
动态存储分配:首次适应、最佳适应、最差适应
紧缩:移动内存内容,以便所有空闲空间合并成一块(进程移到内存的一端,将所有的孔移到内存的另一端,已生成一个大的空闲块)。仅在重定位是动态并在运行时可采用。
分页
所要求的内容不是页的整数倍,最后一个帧用不完,会有内部碎片
逻辑内存:页 —————-> 物理内存:帧
【页号】[页表的索引]—->基地址+【页偏移】
访问一个字节:—>页表—>字节(两次内存访问)===>TLB
分段
不同长度没有一定顺序的段的集合,每个段都是动态分配,产生外部碎片
逻辑地址 ——————–> 物理地址
段号s和段内偏移d ——->【段表】->基地址+界限寄存器—->d是否合法
页面调度算法
FIFO算法:先入先出,淘汰最早调入的页面
OPT算法:预测未来,选最远将使用的页淘汰(最优)
LRU算法:用过去预测未来,将最近最长时间没有使用的页淘汰(最近最少使用)
颠簸(抖动):频繁的页面更换
Belady现象:对有的页面置换算法,页错误率可能会随着分配帧数增加而增加。
FIFO会产生Belady异常;栈式算法无Belady异常:LRU,LFU(最不经常使用),OPT。
参考
https://hit-alibaba.github.io/interview/basic/arch/Memory-Management.html