#操作系统
进程间通信(Inter-Process Communication,IPC)是指两个进程之间产生数据交互。例如,将微博的内容转发到微信,就是一种典型的进程间通信。
进程,在其独立性上,是分配系统资源的单位(包括处理机资源、内存地址空间),因此各进程拥有的内存地址空间相互独立。这里的资源独立,是为了保证安全,一个进程不能直接访问另一进程的地址空间,防止互相串行。
在操作系统中开辟一块共享空间,允许多个进程共同访问这一块共享内存(shared memory)。这里通过增加页表项/段表项即可将同一片共享内存区映射到各个进程的地址空间中,这一点在后文中的内存管理内容中会进一步阐述。在通信过程中,为避免出错,各个进程对共享空间的访问应该是互斥的,这里可使用操作系统内核提供的同步互斥工具(如P、V操作),关于互斥的机制在后续可以进一步探讨。
以Linux系统为例:
// Linux 中,如何实现共享内存:
// 通过 shm_open 系统调用,申请一片共享内存区
int shm_open(const char *name, int oflag, mode_t mode);
// 通过 mmap 系统调用,将共享内存区映射到进程自己的地址空间
void* mmap (void *addr, size_t length, int prot, int flags,int fd, off_t offset);
共享存储的方式有两类,一种是基于数据结构的共享,还有一类是基于存储区的共享。
- 基于数据结构的共享:比如共享空间里只能放一个长度为10的数组。这种共享方式速度慢、限制多,是一种低级通信方式;
- 基于存储区的共享:操作系统在内存中划出一块共享存储区,对于数据的形式、存放位置都由通信进程控制,而不是操作系统。这种共享方式内存速度很快,是一种高级通信方式。
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。消息传递分为直接通信传递和间接通信方式。
发送进程消息,利用OS所提供的发送原语,直接把消息发给目标进程。
发送和接收进程都通过共享实体(邮箱)的方式进行消息的发送和接收。 可以多个进程往同一个信箱send消息,也可以多个进程从同一个信箱中receive消息。
“管道”是一个特殊的共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的内存缓冲区。
管道和共享存储的区别: 两者都属于在内存中分配了内存缓冲区,对于管道而言,必须符合“先进先出”的读写顺序,是一种数据流的形式,类似“队列”数据结构,对于共享数据区而言,其读写空间相对自由。
管道的特点:
- 管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道;
- 各进程要互斥地访问管道(由操作系统实现);
- 当管道写满时(大小固定决定的),写进程将阻塞,直到读进程将管道中的数据取走,即可唤醒写进程;
- 当管道读空时,读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程;
- 管道中的数据一旦被读出,这个数据就彻底消失,即一份数据只能被一个进程读走。因此,当多个进程读同一个管道时,可能会错乱。对此,通常有两种解决方案:
- 一个管道允许多个写进程,一个读进程(System File系统);
- 允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据(Linux 系统方案)。
对于管道写满或者写空的状态:写进程往管道写数据,即便管道没被写满,只要管道没空,读进程就可以从管道读数据;读进程从管道读数据,即便管道没被读空,只要管道没满,写进程就可以往管道写数据。
还没引入进程之前,系统中各个程序只能串行执行。由于引入进程之间的并发,是的进程之间的并行成为了可能。但是对于某一个进程而言,比如微信,此进程可以同时提供音乐、文字聊天、文件传输几种操作,由于进程是程序的一次执行。但这些功能显然不可能是由一个程序顺序处理就能实现的。传统的进程只能串行地执行一系列程序。因此,为了解决这一问题,引入了“线程”,来增加并发度。
引入线程后,线程成为了程序执行流的最小单位,也是基本的CPU执行单元。每个线程独享一份TID、寄存器、程序计数器和函数调用堆栈资源。
进程 | 线程 | |
---|---|---|
基本单位 | 传统进程机制中,进程是资源分配、调度的基本单位 | 引入线程后,线程是资源分配的基本单位,是调度基本单位。进程只作为除CPU之外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)。 |
并发性 | 传统机制中,进程间相互并发 | 引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务。 |
系统开销 | 传统进程间并发,需要切换进程之间运行环境,系统开销较大 | 线程间并发,如果是同一个进程之间的不同线程并发,不需要切换进程环境,系统的开销小。 |
关于线程切换的开销相较之进程较低,可以举个例子说明,例如我本人去图书馆看书。桌子就是一种资源,相对应处理机,人是进程,看不同的书相对应就是是多线程。
- 切换进程运行环境:有一个不认识的人要用桌子,你需要把你的书收走,他把自己的书放到桌上。
- 同一进程内的线程切换:你的舍友要用这张书桌,可以不把桌子上的书收走。直接把自己的书拿上来看就可以了,相比较而言,需要环境改变的成本较小。
综合而言,线程是一种更加轻量级的进程。相比进程有以下的优点:
- 线程是处理机调度的单位;
- 多CPU计算机中,各个线程可占用不同的CPU;
- 每个线程都有一个线程ID、线程控制块(TCB);
- 线程也有就绪、阻塞、运行三种基本状态;
- 线程几乎不拥有系统资源;
- 同一进程的不同线程间共享进程的资源;
- 由于共享内存地址空间,同一进程中的线程间通信甚至无需系统干预;
- 同一进程中的线程切换,不会引起进程切换;不同进程中的线程切换,会引起进程切换;
- 切换同进程内的线程,系统开销很小;切换进程,系统开销较大。
- 线程的管理工作由谁来完成?
- 线程切换是否需要CPU变态?
- 操作系统是否能意识到用户级线程的存在?
- 这种线程的实现方式有什么优点和缺点?
历史背景:早期的操作系统(如:早期Unix)只支持进程,不支持线程。当时的“线程”是由线程库实现的。 拿“微信”聊天举例子,需要同步进行视频聊天、文字聊天、文件传输三个过程。
从代码的角度看,线程其实就是一段代码逻辑。上述三段代码逻辑上可以看作三个“线程”。while 循环就是一个最弱智的“线程库”,线程库完成了对线程的管理工作(如调度)。 很多编程语言提供了强大的线程库,可以实现线程的创建、销毁、调度等功能(例如C的p_thread,C++的std::phread)。 上述过程中,对于操作系统,只看得到进程,在处理机上运行的是程序员通过线程库实现的逻辑上的线程。
用户级线程特点
- 用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)。
- 用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。
- 在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。“用户级线程”就是从用户视角看能看到的线程。
- 优缺点:
- 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。
- 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。
- 多个线程不可在多核处理机上并行运行,只能在同一个处理机上运行。
大多数现代操作系统都实现了内核级线程,如Windows、Linux。实现了由操作系统支持的线程。
- 内核级线程的管理工作由操作系统内核完成。
- 线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
- 操作系统会为每个内核级线程建立相应的TCB(Thread Control Block,线程控制块),通过TCB对线程进行管理。“内核级线程”就是“从操作系统内核视角看能看到的线程”
- 优缺点
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
- 缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
在支持内核级线程的系统中,根据用户级线程和内核级线程的映射关系,可以划分为几种多线程模型。
-
一对一模型 一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。 缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
-
多对一模型: 多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。 操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位。
-
多对多模型
$n$ 个用户及线程映射到$m$ 个内核级线程($n ≥m$)。每个用户进程对应 m 个内核级线程。 克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用线程库户进程占用太多内核级线程,开销太大的缺点。获得了以上两者的优点。
用户级线程是“代码逻辑”的载体;内核级线程是“运行机会”的载体。
内核级线程才是处理机分配的单位。例如:多核CPU环境下,左边这个进程最多能被分配两个核。 一段“代码逻辑”只有获得了“运行机会”才能被CPU执行。 内核级线程中可以运行任意一个有映射关系的用户级线程代码,只有两个内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞。