4.1什么是进程?
进程:我们编写的代码只是存储在硬盘中的静态文件,通过编译后生成二进制可执行文件,当我们运行这个二进制可执行文件的时候,它会被装载到内存中,接着CPU会执行程序中的每一条指令,这个运行着的程序就是进程。
当进程需要从磁盘中读取数据时,CPU不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU会收到中断信号,于是CPU再继续运行这个进程。
对于一个支持多进程的系统,CPU会从一个进程快速切换到另一个进程,每个进程各运行几十毫秒或者几百毫秒。虽然单核的CPU在某一个瞬间,只能运行一个进程。但是在一秒内,它可能运行多个进程,这称之为并发。
【并行与并发】
并发:一个CPU核心在不同的时间,去不同的进程(线程)执行指令的能力;
并行:多个CPU或者一个CPU的多个核心,在同一时刻处理不同进程(线程)的指令;
【进程的活动状态】
创建状态:进程正在被创建时的状态;
结束状态:进程正在从系统中消失时的状态;
运行状态:该时刻进程占用CPU;
就绪状态:可运行,但由于其他进程处于运行状态而暂停运行,等待CPU调度;
阻塞状态:该进程正在等待某一事件的发生(输入/输出操作的完成)而暂停运行,这时候即使CPU控制权,它也无法运行;
如果有大量处于阻塞状态的进程,进程可能占用着物理内存,但物理内存毕竟是有限的,所以在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从磁盘换入到物理内存。
阻塞挂起状态:进程在磁盘并等待某个事件的出现;
就绪挂起状态:进程在硬盘但只要进入内存,就立刻能处于就绪状态;
【进程的控制结构】
在操作系统中,是用进程控制块(PCB)数据结构来描述进程的,PCB是进程存在的唯一标识。
(1)进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
(2)用户标识符:进程归属到的用户,用户标识符主要是为了共享和保护服务;
(3)进程当前状态:如new、ready、running、waiting、blocked等;
(4)进程优先级
(5)资源分配清单:虚拟地址空间和内存地址空间的信息等;
(6)CPU相关信息:CPU中各个寄存器的值,打个进程被切换时,CPU的状态信息都会保存在相应的PCB中,以便进程重新执行时,能从断点处继续执行。
【进程上下文切换】
各个进程之间是共享CPU资源的,在不同的时候进程之间需要切换,让不同的进程可以在CPU执行,那么这一个进程切换到另一个进程运行,称为进程的上下文切换。
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。所以进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。通常,会把交换的信息保存在进程的PCB中,当要运行另外一个进程的时候,我们需要从这个进程的PCB取出上下文,然后恢复到CPU中,使得这个进程可以继续执行。
【进程上下文切换发生在哪些场景中?】
(1)为了保证所有进程可以得到公平调度,CPU时间被划分为一段段的时间片,这些时间片被轮流分配到各个进程。当某个进程的时间片耗尽了,进程就从运行状态转变为就绪状态,系统从就绪队列选择另外一个进程运行。
(2)进程在系统资源不足(内存空间不足)的时候,要等到资源满足后才可以运行,这个时候进程会被挂起,系统调度其他进程运行;
(3)当进程通过sleep函数,这样的方法主动挂起时,系统会重新调度;
(4)当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,让高优先级进程来运行;
(5)发生硬件中断时,CPU上的进程会被中断挂起,转而执行内核中的中断处理程序;
4.2什么是线程?
线程是进程当中的一条执行流程。同一个进程内多个线程可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
在早期的操作系统中都是以进程作为独立运行的基本单位,后面计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程。进程的维护系统开销较大,比如创建进程时需要分配资源、建立PCB;终止进程时需要回收资源、撤销PCB;进程切换时需要保存当前进程的状态信息;而线程可以并发运行并且共享相同的地址空间。
【线程的优点】:
(1)一个进程中可以同时存在多个线程;
(2)各个线程之间可以并发执行;
(3)各个线程之间可以共享地址空间和文件等资源;
【线程的缺点】:
(1)当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃。
【线程的实现】
(1)用户线程:在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
用户线程是基于用户态的的线程管理库来实现的,线程控制块(TCB)也是在库里面实现的,对于操作系统而言是看不到这个TCB的,它只能看到整个进程的PCB;用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库来完成线程的管理,包括线程的创建、终止、同步和调度等。
(2)内核线程:在内核实现的线程,由内核管理的线程;
内核线程是由操作系统管理的,线程对应的TCB自然也是放在操作系统里面的,这样线程的创建、终止和管理都是操作系统负责的;
(3)轻量级进程:在内核中来支持用户线程;
【用户线程与内核线程的对应关系】
(1)多对一
优点:
- 每个进程都需要它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器等),TCB由用户级线程库来维护;
- 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;
缺点:
- 由于操作系统不支持线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了;
- 当一个线程开始运行后,除非它主动交出CPU的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的;
- 由于时间片分配给进程,故与其他线程比,在多线程执行时,每个线程得到的CPU时间片较少,执行会比较慢;
(2)一对一
优点:
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 分配给线程,多线程的进程更多的CPU时间片;
缺点:
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,比如PCB和TCB;
- 线程的创建、终止和切换都是通过系统调用的方式来进程,对操作系统来说,系统开销比较大。
4.3进程与线程的比较
(1)进程是资源(包括内存、打开的文件等)分配的基本单位;线程是CPU调度的基本单位;
(2)进程拥有一个完整的资源平台;线程只独享必不可少的资源,如寄存器和栈;
(3)线程同样拥有就绪、阻塞、执行等三种基本状态,同样具有状态之间的转换关系;
(4)线程能减少并发执行的时间和空间开销;
【1】线程的创建时间比进程快,因为进程在创建的过程中,还需要内存管理信息、文件管理信息等资源,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
【2】线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
【3】同一个进程的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候需要把页表给切换掉,而页表的切换过程开销是比较大的。
【4】由于同一个进程的线程之间共享内存资源和文件资源,那么在线程之间传递数据的时候,就不需要经过内核了,使得线程之间的数据交互效率更高了。
4.4说说进程间的通信方式有哪些?各有什么优缺点?
每个进程的用户地址空间独立的,一般是不能互相访问的, 但内核空间是每个进程共享的,所以进程之间通信必须要通过内核。
(1)管道
- 管道分为匿名管道和命名管道。
- 匿名管道是特殊文件只存在与内存,shell命令中的|竖线就是匿名管理,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随进程;
- 命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建 一个类型为p的设备文件,那么毫无关系的进程就可以通过这个设备文件进程通信。不管是匿名管道还是命名管道,进程写入的数据都缓存在内核中。
(2)消息队列
- 消息队列是保存在内核中的消息链表,在发送数据时,会分为一个一个的独立的数据单元,消息体是用户定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据,如果进程从消息队列中读取了消息体,内核就会把这个消息体删除掉;
- 消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在;
- 通信可能不及时,另外消息体有大小限制;
- 消息队列不适合比较大数据的传输,内核中每个消息体都有一个最大长度的限制,同时队列所包含的全部消息体的长度也是有限制的;
- 消息队列的通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态到用户态数据拷贝的过程;
(3)共享内存
- 消息队列的读取和写入的过程,都会发生用户态与内核态之间的消息拷贝过程。共享内存的方式,能解决好这一问题。现代操作系统,对于内存管理,采用的是虚拟内存技术,每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同物理内存中。
- 共享内存的机制就是拿出一块虚拟地址空间出来,映射到相同的物理内存中,这样每个进程写入的东西,另外一个进程马上就能看到了。都不需要拷贝,提高进程间的通信效率。
- 如果多个进程同时修改同一个共享内存的资源,很有可能会发生冲突,造成数据错乱。
(4)信号量
- 为了防止多进程竞争共享资源造成数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。信号量就实现了这一保护机制。
- 信号量其实就是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是缓存进程间通信的数据。
(5)信号
- 比如在shell终端的进程,我们可以通过键盘输入某些组合键,给进程发送信号;ctrl+c表示终止该进程,ctrl+z表示挂起该进程
- kill -9 1050,表示给PID为1050的继承发送sigKill信号,用来立刻结束该进程;
(6)Socket
- Socket可用于跨网络实现不同主机上的进程通信,也可以实现本机上不同进程间的通信;
(1)客户端和服务端初始化socket,得到文件描述符;
(2)服务端调用bind,绑定自己的ip和端口;
(3)服务端调用listen,进行监听;
(4)服务端调用accept,等待客户端连接;
(5)客户端调用connect,向服务端的地址和端口发起连接;
(6)服务端accept,经过tpc三次握手建立连接;
(7)客户端调用write写入数据;服务端调用read读取数据;
(8)客户端调用close断开连接,服务端read读取数据的时候就会读取到eof,等待处理完数据,服务端调用close,表示连接关闭;
4.5死锁的概念
死锁只有同时满足以下四个条件才会发生:
(1)互斥条件:多个线程不能同时使用同一个资源;
(2)持有并等待条件:线程A已经持有了资源1又想申请资源2,但资源2已被线程B持有了,此时线程A就处于等待状态;
(3)不可剥夺条件:当线程已经持有了资源,在自己使用完之前不能被其他线程获取;
(4)环路等待条件:两个线程获取资源的顺序构成了环路。
【解决方法】:资源有序分配法,破坏环路等待条件,多个线程获取资源的顺序一致;
4.6锁
互斥锁:在开发过程中,最常见的就是互斥锁了,互斥锁加锁失败时,会用线程切换的方式来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。
自旋锁:如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败后,并不会产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,这个忙等待的时间也会相应较短。
读写锁:读写锁允许多个读线程可以同时持有读锁,提高了读的并发性。它可以分为读优先锁和写优先锁,读优先锁的并发性很强,但容易造成写线程饥饿;而写优先锁会优先服务写线程,读线程也可能饥饿,于是为了避免饥饿的问题,就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先进先出的顺序对线程加锁,这样保证某种线程不会被饿死,通用性也更好点。
悲观锁:互斥锁、自旋锁、读写锁都属于悲观锁,悲观锁认为并发访问共享资源的时候,冲突概率是很高的,所以在访问共享资源前都需要先加锁;
乐观锁:如果并发访问共享资源的时候,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间有没有发生冲突,如果没有其他线程在修改资源,那么操作成功;如果发现有其他线程已经修改过这个资源,就放弃本次操作。