一、进程 线程及协程
1.程序、进程、线程
程序:通过某种语言编写的一组指令 ,即一段静态的代码。
进程:程序的一次执行过程。将程序加载入内存分配空间并执行,该段静态代码程序便拥有了动态性。系统运行一个程序即是一个进程从创建,运行到消亡的过程。如下图所示,在 ubuntu中通过查看任务管理器的方式,我们就可以清楚看到当前运行的进程。
线程概念的出现
进程的出现使操作系统的并发成为了可能,但是随着计算机的普及,人们对实时性有了更高的要求。一个进程在一段时间只能做一件事情,如果一个进程有多个任务,只能逐一的去执行这些子任务。然而这些子任务之间并不存在顺序上的依赖,因此人们想到既然CPU可以按照时间片的方式轮流的切换执行不同的进程,那为什么不能也同样给进程子任务打上标签,让CPU按照更细的时间片来执行子任务呢?由于子任务共享内存等资源,因此隶属于同一个进程的子任务之间的切换是不需要切换页目录以使用新的地址空间的,这就为子任务的快速切换提供了可能。此时人们便提出了线程,让一个线程去执行子任务,这样一个进程就包括了多个线程,每个线程去负责独立的子任务,这样让进程的内部并发成为了可能,就实现了实时性的目的。
线程:进程进一步细化为线程,是一个程序内部的一条执行路径。也叫做轻量级进程。
如下图所示,在CCleaner 这一应用程序中,我们同时执行Health Check 以及 Custom Clean 两项子任务即为多线程。
2.进程与线程的关系及区别
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 进程有独立的地址空间,相互不影响,多个线程只是进程的不同执行路径,共享其地址空间及资源
- 线程不能看做独立的应用,而进程可看作独立应用
- 多进程的程序比多线程的程序健壮
- 进程的切换比线程的切换开销大
注解
进程是资源分配的基本单位,所有与该进程有关的资源都被记录在进程控制块PCB中,以表示该进程拥有或者正在使用这些资源。同时进程也是抢占处理机调度的单位,它拥有完整的虚拟内存和地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程中的不同线程共享同一地址空间。
线程与资源分配无关,它属于某一进程,并和该进程内的其他线程共享进程的资源。线程只由相关的堆、栈 寄存器、程序计数器和线程控制表TCB组成,其中寄存器用来存储线程内的局部变量,但并不存储其他线程的相关变量。每一个独立的线程有一个程序的入口、顺序执行序列以及程序的出口,但是线程并不能独立的执行,必须依存于某个应用程序中,由应用程序提供对多个线程的执行控制。
操作系统并没有将多个线程当作多个应用来实现进程的调度和管理以及资源的分配,而进程可作为独立的应用,拥有独立的内存空间。当一个进程崩溃后,在保护模式下并不会对其他进程产生影响,然而某个线程挂掉之后其所在的进程也会同样挂掉。
进程切换比线程切换开销大、耗费资源、效率差,对于要求同时进行,并且要求共享某些变量的并发操作只能用线程,不能用进程。
3.协程
为了减小开销,协程的概念被提出来了。协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 协程不是操作系统里的概念,而是完全由用户完全控制的。要是问他在哪的话,那也就是在代码逻辑里抽象出来的概念,是一段代码块。
协程的特点
- 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
- 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
- 由于在同一个线程上,因此可以避免竞争关系而使用锁。
- 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决
4.子进程与父进程
在UNIX里,除了进程0(即PID=0的交换进程,Swapper Process)以外的所有进程都是由其他进程使用系统调用fork创建的,这里调用fork创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。
在Linux系统函数中,fork()是用来创建新的子进程的函数。根据Linux编程手册,在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
5.僵尸进程与孤儿进程
- 僵尸进程 :当一个子进程结束运行(一般是调用exit、运行时发生致命错误收到终止信号时,子进程的退出状态会回报给操作系统,系统则以SIGCHLD信号将子进程被结束的事件告知父进程,此时子进程的进程控制块(PCB)仍驻留在内存中。一般来说,收到SIGCHLD后,父进程会使用wait系统调用以取得子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的PCB;而如若父进程没有这么做的话,子进程的PCB就会一直驻留在内存中,也即成为僵尸进程。
- 孤儿进程 :孤儿进程则是指父进程结束后仍在运行的子进程。在类UNIX系统中,孤儿进程一般会被
init进程所“收养”,成为init的子进程。
避免产生僵尸进程的方法:
- 将父进程中对SIGCHLD信号的处理函数设为SIG_IGN(忽略信号);
- fork两次并杀死一级子进程,令二级子进程成为孤儿进程而被
init所“收养”、清理;
二、进程间通信
进程间通信(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法。
进程间通信的方式
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列 Message Queue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储Shared Memory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 信号 Signals : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 信号量 Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字 Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。