并发环境:
一段时间间隔内,单处理器上有两个或两个以上程序同时处于开始但尚未结束状态(例如:进程A开始于进程B开始但未结束期间),并且次序不是事先确定的。
进程概念:
进程是具有独立功能的程序关于某个数据集合上的一次运行活动(将一个程序同时运行多次产生的多个进程是不一样的),是系统进行资源分配和调度的独立单位
1.进程是正在运行程序的抽象
2.将CPU变换成多个虚拟CPU
3.系统资源以进程为单位分配
4.每个进程有独立的地址空间
5.OS通过调度把CPU控制权交给某个进程
PCB:
OS为了管理进程而设立了进程控制块PCB
PCB是OS感知进程存在的唯一标志即进程与PCB一一对应
进程表:所有PCB集合(大小固定即为OS所支持的最大进程数)
PCB所包含的信息
PS.linux中使用task_struct结构体来表示该内容
进程三种状态
运行态,就绪态,等待态
1.运行态:占有CPU并在CPU上运行
2.就绪态:已具备运行条件,但由于没有空闲CPU而暂时不能运行(资源都具备,但是就差个CPU)
3.等待态:进程因等某一事件而暂时不能运行(某个事件如:该进程在等待读盘结果等等;等待态有时又称为阻塞态,封锁态,睡眠态)
三种状态的转化:
①
调度程序选择一个新的进程执行
②
i.可能因为CPU分配给当前进程的时间片使用完(时钟中断)
ii.可能因为就绪队列中有个高优先级的进程比正在运行的进程优先级高,抢占CPU,导致当前进程进入就绪状态
③
运行的进程可能需要等待某一事件例如:
i:请求OS服务(系统调用当前进程)
ii:对资源访问尚不能进行(该资源正在被其他进程占用)
iii:等I/O结果
iii:等待另一进程提供信息
④
等待的事件发生了
进程的其他状态
创建态:
已完成PID分配,PCB填写,但CPU为统一执行该进程,因为资源有限
终止态:
终止进程后,进程进入该状态,将会执行数据统计(CPU使用量等等)和回收资源
挂起态,目的是为了减少进程占用内存:
用于调节负载(进程太多,CPU运行不了),暂时先把进程挂到磁盘上,但这要区别于等待态(
1.挂起是一种主动行为,因此恢复也应该要主动完成,而阻塞则是一种被动行为,是在等待事件或资源时任务的表现,你不知道他什么时候被阻塞,也就不能确切的知道他什么时候恢复阻塞;
2.阻塞就是任务释放CPU,其他任务可以运行,一般在等待某种资源或信号量的时候出现。挂起不释放CPU,如果任务优先级高就永远轮不到其他任务运行,一般挂起用于程序调试中的条件中断,当出现某个条件的情况下挂起,然后进行单步调试。
激活条件:内存中无进程,或者当前就绪进程的优先级比挂起的进程优先级低,或者当一个进程释放内存同时有高优先的进程被挂起,此时该高优先的进程被激活
)
形象比喻:
五状态图:
七状态图:
Linux状态图:
其中深度睡眠和浅度睡眠区别在于:前者不接受信号而后者还接受信号
还有其他一些状态进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。内核就把孤儿进程的父进程设置为init,并由init进程对它们完成状态收集工作,init进程会循环地wait()它的已经退出的子进程,因此孤儿进程并不会有什么危害。
僵尸进程:一个进程使用fork创建子进程,如果子进程exit(),那么其会自动进入僵尸状态,如果父进程没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。init将会以父进程的身份对僵尸状态的子进程进行处理。
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果父进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免,之后OS检测到该进程会将设定的它的父进程为init,然后wait来将其完全释放。
进程队列:
OS为每一类状态建立一个或多个队列,队列元素即为PCB,进程状态的改变即为PCB从一个队列移动到另一个队列。
1.多个等待队列等待的事件不同
2.就绪队列也可以多个
3.单个CPU情况下,运行队列中有一个进程
五状态进程队列
进程控制:
进程控制操作完成进程各状态之间的转换,由特定功能原语完成,原语包括:进程创建原语,进程撤销原语...
原语(原子操作):完成某种特定功能的一段程序,其执行期间具有不可分割或不可中断性
进程创建(unix: fork()/exec()):
1.给新进程分配一个唯一标识ID以及PCB
2.为进程分配地址空间
3.初始化进程控制块(设置默认值例如:将其状态设置为new)
4.插到相应状态队列中
结束进程(unix:exit()):
1.收回进程所占有资源
2.撤销该进程PCB
进程阻塞(unix:wait):
进程运行时,由于等待某一事件发生,由进程自己执行阻塞原语,使自己有运行态变成阻塞态
fork()函数:
通过复制父进程的程序代码创建子进程
exec()函数:
通过一系列系统调用,它们都是通过一段新的程序代码覆盖原来地址空间,实现进程执行代码的转换
wait()函数:
使一个进程等待另一个进程结束
exit()函数:
用来终止一个进程运行
fork()函数的实现:
1.为子进程分配一个空闲的进程描述符PCB(unix中为proc结构)
2.分配给子进程唯一标识符PID
3.以一次一页方式复制父进程地址空间
4.将子进程状态设为就绪态,插到就绪队列中
5.对子进程返回标识符PID0
6.对父进程返回子进程PID
但是以这种方式fork效率很低,因为第3步将父进程地址空间(包括代码)复制给子进程,但是往往fork()之后会用exec()函数,用新的代码替换掉子进程中的代码,所以第3步做了无用功
所以linux使用了写时复制(copy-on-write)技术,父进程只将地址空间指针传给子进程,再将地址空间改为只读模式,当子进程要往地址空间写入数据时,OS检测到然后会给子进程开一个与父进程一样的地址空间
fork实例:
#include <sys/types.h> #include <stdio.h> #include <unistd.h> void main(int argc, char *argv[]) { pid_t pid; pid = fork(); /* 创建一个子进程 */ if (pid < 0) { /* 出错 */ fprintf(stderr, “fork failed”); exit(-1); } else if (pid == 0) { /* 子进程 */ execlp(“/bin/ls”, “ls”, NULL); } else {/* 父进程 */ wait(NULL); /* 父进程等待子进程结束 */ printf(“child complete”); exit(0); } }
若子进程执行,会从fork()的下一条指令开始执行
进程分类:
可以分成系统进程和用户进程,前台进程和后台进程,CPU密集型进程和I/O密集型进程
进程层次结构:
Unix进程家族树:init(PID号为1)根
windows:地位相同
进程与程序区别:
1.进程更能准确刻画并发,而程序不能
2.程序是静态的,进程是动态的
3.进程有生命周期的,有诞生有消亡,是短暂的;而程序是相对长久的
4.一个程序可对应多个进程
5.进程具有创建其他进程的功能
进程地址空间:
例子:
#include <stdio.h> int value; int main(){ scanf("%d",&value); while(1) printf("value: %d, loc: 0x%lx ",value,(long)&value); return 0; }
然后将程序保存为a.c和b.c,同时在不同终端上运行该程序:
可以看到,变量value值不同,但是地址却相同都是0x601044这是为什么?
原来两个进程哟拥有两个进程地址空间,现在的0x601044为相对于个进程中数据段的地址(逻辑地址),并不是实际上的物理地址
进程地址空间:
每个进程都有这样的进程地址空间,value全局变量就存在数据段中,然后0x601044是从每个进程地址空间起始位置从0开始编号的地址,不是物理地址,所以value所在地址值是一样的;
但是一旦将value改到main函数中,就是相当于将value存在栈中情况就会改变。
所以基本可以得出结论,数据段在各个进程的地址空间中位置基本不变,而栈的位置会改变
如果把a.c内容改成这样,两者相对地址又发生变化
#include <stdio.h> int value1; int value2; int value; int main(){ scanf("%d",&value); while(1) printf("value: %d, loc: 0x%lx ",value,(long)&value); return 0; }
进程映像:
对进程执行活动全过程的静态描述。由进程地址空间内容,硬件寄存器内容及与该进程相关内核数据结构,内核栈组成。
用户相关:进程地址空间(包括代码段、数据段、堆和栈、共享库……)
寄存器相关:程序计数器、指令寄存器、程序状态寄存器、栈指针、通用寄存器等的值
内核相关:
静态部分:PCB及各种资源数据结构
动态部分:内核栈(不同进程在进入内核后使用不同的内核栈)
上下文切换:
将CPU硬件状态从一个进程换到另一个进程的过程称为上下文切换
1.进程运行时,硬件状态保存在CPU寄存器上
2.进程不运行时,寄存器内容存在PCB中,再运行时,将PCB内容再放到对应寄存器上
线程为什么引入?
①应用需要
②开销的考虑
③性能的考虑
①应用需要:
1.三线程的字处理软件(软件运行为一个进程):
一个线程用于处理键盘输入,一个线程用来存储输入内容,一个线程来完成排版,但一个进程不可能完成这任务
2.web服务器
流程:
i.从客户端接收网页请求
ii.从磁盘上检索相关网页,读入内存
iii.将网页返回给对应的客户端
如何提高效率?
1.进程分线程,一线程来将网页缓存(这样下次访问就不用去磁盘上找),一线程将网页返回给用户
但是不用线程的话
i.一个服务进程(进程如果多个也没用,他们地址空间不同无法进行信息共享):如果进程去磁盘上搜寻网页,就无法及时接收客户端请求,万一客户端又不想要这个网页,性能大大下降
ii.有限状态机:编程模型复杂;采用非阻塞I/O
用了线程:
分派线程:监听客户端请求,客户端有请求就读入(接活)
工作线程:接收分派线程的请求,到网页缓存查找网页,没有的话就去磁盘上找(干活)
阻塞/非阻塞
关注程序在等待调用结果(消息返回值)时的状态
阻塞调用:调用结果返回之前,当前线程会被挂起,调用线程执行,调用线程只有在得到结果之后才会返回。( 一个函数在执行的时候会有一些因素使其无法立即完成,比如网络读写,因为网络延迟而无法瞬间完成,如果这个等待时间长到你的程序需要考虑的数量级别,那么就要考虑阻塞问题,就是等的过长OS会选择把它挂起,fork是非阻塞系统调用,fork()时父进程并没有被挂起)
非阻塞调用:调用不能立刻得到结果之前,该调用不会阻塞也就是说当前线程可能回去做其他事
同步/异步
关注消息通信机制
同步:发出一个调用时,在没有的到结果之前,该调用就不返回,但是一旦得到返回值调用就立刻返回
异步:调用发出后,这个调用直接返回,没有返回结果,之后被调用者通过状态,通知来告诉调用者,或通过回调函数处理这调用
线程与进程对比:
进程相关的操作: 线程的开销小
创建进程 创建一个新线程花费时间少(撤销亦如此)
撤消进程 两个线程切换花费时间少
进程通信 线程之间相互通信无须调用内核(同一进程内的线程共享内存和文件)
进程切换
→ 时间/空间开销大,限制了并发度的提高
线程:进程中运行实体,CPU调度单位。
线程属性:
1.有标识符ID
2.有状态/状态转换->需要一些操作
3.不运行要保存上下文包括PC内容等
4.有自己的栈和栈指针
5.同一个进程的多个线程共享该进程的地址空间和其他资源
6.可以创建,撤销另个线程
线程机制实现
①用户级线程
②核心级线程
③混合前两者方法
①用户级线程
1.在用户空间建立线程库:提供一组管理线程过程
2.运行时系统(run-time system):完成线程管理工作
3.内核管理的还是进程,因为感觉不到线程存在
4.线程切换不需要核心态特权
例子:
POSIX库 --- PTHREAD库
POSIX(Portable Operating System Interface)多线程编程接口,以线程库方式提供给用户
库函数:
其中线程执行yield函数,会把CPU让出,防止自己一直占用CPU
优点:
1.线程切换快
2. 调度算法是应用程序特定的
3. 用户级线程可运行在任何操作系统上(只需要实现线程库)
缺点:
1.内核只将处理器分配给进程,同一进程中的两个线程不能同时运行于两个处理器上
2.大多数系统调用是阻塞的,因此,由于内核阻塞进程,故进程中所有线程也被阻塞,可用jaketing/wrapped判断要执行的系统调用是阻塞还是非阻塞,防止被阻塞的进程内所有线程被阻塞
②核心级线程
1.内核管理所有线程管理,并向应用程序提供API接口
2.内核维护进程和线程的上下文
3. 线程的切换需要内核支持
4. 以线程为基础进行调度
③混合前两者方法
1.线程创建在用户空间完成
2. 线程调度等在核心态完成
可再入程序(可重入):
可被多个进程同时调用的程序,具有下列性质:
它是纯代码的,即在执行过程中自身不改变;调用它的进程应该提供数据区
作者水平有限,文章肯定有错还请各位指点!!!感谢!!!
参考链接:
http://blog.tonychow.me/blog/2013/06/27/linuxzhong-forkxi-tong-diao-yong-fen-xi/
https://www.zhihu.com/question/42962803