1.进程
1.1概念
1)为实现操作系统的并发性和共享性,引入进程
2)进程是程序执行时的一个实例,类似于一个活动,它有程序、输入、输出和状态这几个部分组成。举个例子,一个程序员为他的女儿做蛋糕,他有做蛋糕的食谱和各种原料。食谱是程序(用某种形式描述的算法),程序员是CPU,各种原料是输入数据。进程就是程序员阅读食谱、取来各种原料来制作蛋糕一系列动作的总和。同时,进程它也有状态,假设这个程序员在做蛋糕的时候,他的儿子哭着跑过来,说他的手被小刀割破了,程序员此时就记录下他按照食谱做到哪了(保存进程的当前状态,进程被挂起),然后按照急救手册处理伤口。这里,就相当于CPU从一个进程切换到另一个高优先级的进程。当伤口处理完后,程序员又回去做蛋糕,从他所记录的地方继续做下去。(程序和进程的区别可以用这个做蛋糕的例子来说明)
3)在进程模型中,进程是资源分配和独立调度的基本单位。
主要是记前3点
4)进程具有五大特性:
动态性(最基本):进程具有生命周期,是动态地创建、变化、结束的。
并发性:多个进程可以并发运行。
独立性:进程是资源分配和独立调度的基本单位。
异步性:由于进程间共享资源和协同合作,因此产生相互制约的关系,使进程具有执行的间断性(走走停停)。
结构性:每个进程都配置一个PCB(进程控制块,一种数据结构)对其进行描述。
1.2进程映像
某一时刻进程的内容和状态的集合,进程映像通常由程序块、数据块和一个PCB(进程控制块)组成,进程映像是静态的,进程是动态的。
1.3进程的伪并行
严格说,CPU在某一瞬间只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生了并行的错觉。
1.4进程的状态模型
1.4.1三个状态
1)运行态:进程正在占用CPU的时期
2)就绪态:是一种可运行的状态,因为其他进程正在运行而暂时停止
3)阻塞态:等待外部事件的发生(典型的例子:它在等待可使用的输入)
1.4.2四种转换(见图)
1)运行态->阻塞态
2)运行态->就绪态
3)就绪态->运行态
4)阻塞态->就绪态:外部事件的一旦发生,阻塞态转换到就绪态,如果此时CPU空闲,立刻发生转换3,转换到运行态
1.5进程的创建
新进程都是由已有进程而创建:
UNIX:父进程使用fork创建子进程,内核会分配一个新的PCB给子进程,子进程是父进程的副本,子进程拷贝父进程的数据段、堆、栈,而正文段与父进程共享;但是fork之后常常跟随exec调用让子进程执行不同于父进程的任务,所以fork会使用“写时复制”技术,开始的时候子父进程共享数据段、堆、栈和正文段,当任何一方想要更改某个区域时,内核再为相应区域制作副本
WINDOWS:父进程使用CreateProcess创建子进程,子父进程从一开始就不共享任何区域。
1.6进程的实现
操作系统维护一张进程表,每一个进程占用一个表项,表项包含这个进程状态的各种信息。
1.7进程的终止
1.7.1正常终止
1)从main返回
2)在任何地方调用exit、_exit、_Exit(包括在进程的线程里调用)
3)进程中的最后一个线程从其启动例程返回
4)最后一个线程调用pthread_exit函数
1.7.2异常终止
1)调用abort函数(此函数将发送SIGABRT信号给进程)
2)接收到一个信号
3)最后一个线程取消请求做出响应
2.线程
2.1概念
1)线程是一种轻量级进程,更容易创建,也更容易销毁
2)在线程模型中,“资源分配”与“调度”分离,线程是独立调度的基本单位,进程是资源分配的基本单位
3)线程的优势(为什么要提出线程):提升了操作系统的并发性能,首先线程是轻量级进程,更容易创建,也更容易销毁,其次,进程切换需要切换虚拟地址空间,而线程切换则不需要,减少了切换的性能损耗
2.2进程与线程的区别与联系
1)一个进程可以有多个线程,但至少有一个线程。
2)进程有独立的地址空间,同一进程下的多线程共享地址空间,更确切的说是共享正文段、数据段和堆,不共享栈,各线程拥有自己的栈
3)线程是调度的基本单位,进程是资源分配的基本单位,同一进程下的多线程共享该进程资源。
4)通信方面:进程间通信需要管道、消息队列、共享内存等手段,而同一进程下的多线程直接通过共享的数据段和堆来通信
2.3进程与线程的选择取决以下几点
1)需要频繁创建销毁的优先使用线程,因为对进程来说创建和销毁一个进程代价是很大的
2)线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3)因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
4)需要更稳定安全时,适合选择进程
2.4线程的状态模型
与进程一样,有三个状态,四种转换
2.5线程的创建
进程的主线程使用thread_create创建线程,所有线程都是平等的
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t * attr, void *(*func)(void*), void* arg ); //若成功则返回0,否则返回错误编号
2.6线程的分类
1.用户级线程:不需要内核支持而在用户程序中实现的线程,内核对线程包一无所知,内核中用进程表管理进程,进程中用线程表管理多线程
优点:1.可以在不支持线程的操作系统中实现 2.每个进程可制订自己的调度算法调度线程
缺点:1.发送系统阻塞时内核不知道多线程的存在因而阻塞进程从而阻塞所有线程 2.进程内部没有时钟中断,所以不能轮转调度线程
2.内核级线程:由内核创建和撤销,内核用线程表来管理内核线程
优点:1.一个线程阻塞,内核可以运行同一进程内的另一个线程
缺点:1.代价大:阻塞线程的调用都是系统调用、内核中创建和撤销的开销更大 2.信号是发给进程而不是线程,信号到达进程由哪一个线程去处理?
2.7线程的终止
1)线程从启动例程中返回
2)线程被同一进程的其他线程取消
void pthread_cancel(pthread_t pid); //若成功,返回0;否则,返回错误编号
某线程可以通过调用pthread_cancel函数来请求取消另一线程,仅仅是请求,另一线程可以忽略或者控制如何被取消
int pthread_setcancelstate(int state, int *oldstate); //成功则返回0,否则返回错误编号。
有两个线程并没有包含在pthread_attr_t结构中,他们是可取消状态和可取消类型,这两个属性影响着线程在响应pthread_cancel函数调用时所呈现的行为。可取消状态属性可以是PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE,线程的默认可取消状态是PTHREAD_CANCEL_ENABLE,可调用pthread_setcancelstate修改它的可取消状态,当状态设为disable时,对pthread_cancel的调用并不会杀死线程,而是挂起这个取消请求(如果之后取消状态再次变为enable,则挂起的请求可以被处理)
void pthread_testcancel(void);
推迟取消:调用pthread_cancel以后,在线程到达取消点之前,并不会出现真正的取消。当调用下表中列出的任何函数,取消点都会出现,或者调用pthread_testcancel设置取消点
int pthread_setcanceltype(int type, int *oldtype); //成功则返回0,否则返回错误编号。
通过调用pthread_setcanceltype来修改取消类型,取消类型可以是PTHERAD_CANCEL_DEFERRED(延迟取消)或PTHREAD_CANCEL_ASYNCHRONOUS(异步取消),使用异步取消时,线程可以在任何时间取消,而不是非要等到遇到取消点才能被取消
3)线程调用pthread_exit函数
2.8线程之间的资源
2.8.1共享:
1)正文段
2)数据段(data段、bss段)
3)堆
4)进程ID
5)每种信号的处理方式
6)文件描述符
2.8.2独享:
1)栈
2)线程ID
3)线程的优先级(线程需要被调度)
4)信号屏蔽字(每个线程所感兴趣的信号不同,所以线程的信号屏蔽字由线程自己管理)
5)寄存器的值(线程间是并发运行的,每个线程有自己不同的运行线索)
6)错误返回码errno(同一个进程中有多个线程在同时运行,如果某个线程设置了errno值,当该线程还没来得及处理这个错误,另外一个线程也设置了errno值,这样的话前一个线程的错误就没有办法被处理了。 所以,不同的线程应该拥有自己的错误返回码变量)
2.9线程结合与分离
1)一个线程要么是结合的,要么是分离的
2)一个结合的线程可以被其他线程回收资源和杀死
3)一个分离的线程不能被别的线程回收资源和杀死,等到这个线程终止后,系统自动释放资源
4)默认情况下,创建的线程是结合的(设置pthread_create函数的参数可创建分离的进程)
5)如果一个结合的线程终止后却没有被pthread_join,则它将成为僵死线程(类似于僵死进程,还有一部分资源没有被回收),所以创建线程者应该调⽤用pthread_join来等待线程运行结束,回收线程资源
6)调用pthread_join后,当等待线程没有终止时,父线程将处于阻塞状态;如果要避免阻塞,在子线程中调用pthread_detach(pthread_self())或者父线程中调用pthread_detach(thread_id),这会将子线程的状态设置为分离的,这样一来该线程终止后系统会自动释放资源,父线程就不用阻塞等待了
2.10谨慎在多线程中使用fork
1)子进程继承父进程的互斥量、读写锁、条件变量
2)linux中,如果父进程包含多线程,fork的时候只复制调用fork的线程到子进程,其他线程在子进程中不被复制
3)假设在fork之前,父进程的一个线程对某个锁进行的lock,然后另外一个线程调用了fork创建子进程。此时在子进程中持有那个锁的线程却不被复制,然而子进程又会继承父进程的锁,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了。如果子进程再对这个锁进行lock的话,就会发生死锁。
4)解决:
- 在fork出的子进程中立刻调用exec可避免这个问题,调用exec,旧的地址空间被抛弃,锁的状态就无关紧要
- 使用pthread_atfork函数,该函数用于清除锁状态,但是该函数不能清理条件变量
3.进程切换和线程切换
1)进程切换分两步:
- 切换虚拟地址空间
- 切换内核栈和硬件上下文
2)对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的
3)切换的性能消耗:
- 进程的切换要切换虚拟地址空间,会扰乱处理器的缓存机制:处理器中所有已经缓存的内存地址都作废了,处理的页表缓冲的TLB会被全部刷新,这将导致内存的访问在一段时间内相当得低效;但是在线程的切换中,不会出现这个问题
- 切换内核栈和硬件上下文会将寄存器中的内容切换出,产生性能损耗
4.进程和线程的函数对比
1)pthread_create()类似于fork(),用来创建线程
2)pthread_exit()类似于exit(),用来终止线程
3)pthread_self()类似于getpid(),获取线程号
4)pthread_join()类似于waitpid(),用来处理终止的线程
5.协程
1)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程;
2)协程不是被操作系统内核管理,而完全是由程序所控制
3)协程的开销远远小于线程
4)协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态:协程拥有自己寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前保存的寄存器上下文和栈
5)每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源
6)协程极高的执行效率,和多线程相比,线程数量越多,协程的性能优势就越明显;