进程是资源分配的最小单位,线程是CPU调度的最小单位
对比维度 | 多进程 | 多线程 | 总结 |
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
几种进程中的通信方式:
# 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。创建管道的系统调用pipe函数。
#include <unistd.h>
int pipe(int fd[2]);
pipe函数的参数是一个包含两个int型整数的数组指针。该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。如果失败,则返回-1并设置errno。
通过pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出。并且,fd[0]只能用于从管道读出数据,fd[1]则只能用于往管道写入数据,而不能反过来使用。如果要实现双向的数据传输,则应该使用两个管道。
默认情况下,这一对文件描述符都是阻塞的。此时如果我们用read系统调用读一个空的管道,则read将被阻塞,直到管道内有数据可读。如果我们用write系统调用来往一个满的管道中写数据,则write将被阻塞,直到管道内有足够的空闲空间可用。
管道本身有容量的限制,一般为65536字节,可以使用fcntl函数来修改管道容量。
管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[1]和fd[0])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭f[0],另一个关闭fd[1]。
# 有名管道 (named pipe) : 有名管道也是半双工的通信方式,它克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;但是它允许无亲缘关系进程间的通信。
# 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
当多个进程同时访问系统上的某个资源的时候,比如同时写一个数据库的某条记录,或者同时修改某个文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序对共享资源的访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。
信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)-----passeren(传递,进入临界区)和vrijgeven(释放,退出临界区)。
假设有信号量SV,则对它的P、V操作含义如下:
p(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。
信号量的取值可以是任何自然数,但最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值。
Linux信号量的API都定义在sys/sem.h头文件中,主要包括3个系统调用:
1、semget系统调用:创建一个新的信号量集,或者获取一个已经存在的信号量集。成功时返回一个正整数值,它是信号量集的标识符;失败时返回-1,并设置error。
2、semget系统调用:改变信号量的值,即执行P、V操作。
3、semctl系统调用:允许调用者对信号进行直接控制。
当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(SV)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键字代码,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而处于挂起状态,则它将被唤醒,并进入关键代码段。同样,这时进程A如果再执行P(SV)操作,则也只能被操作系统挂起以等待进程B退出关键代码。
# 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
消息队列是在两个进程之间传递二进制数据块的一种简单有效的方式。每个数据块都有一个特定的类型,接收方可以根据类型来有选择的接收数据,而不是像管道和命名管道那样必须以先进先出的方式接收数据。
1、msgget系统调用:创建一个消息队列,或者获取一个已有的消息队列。
2、msgsnd系统调用:把一条消息添加到消息队列中。
处于阻塞状态的msgsnd调用可能被如下两种异常情况中断:
(1)消息队列被移除。此时msgsnd调用将立即返回并设置errno为EIDRM。
(2)程序接收到信号。此时msgsnd调用将立即返回并设置errno为EINTR。
msgsnd成功时返回0,失败则返回-1并设置errno。msgsnd成功时将修改内核数据结构msqid_ds的部分字段。
(1)将msg_qnum加1.
(2)将msg_lspid设置为调用进程的PID
(3)将msg_stime设置为当前的时间。
3、msgrcv系统调用:从消息队列中获取消息。
处于阻塞状态的msgrcv调用可能被如下两种异常情况中断:
(1)消息队列被移除。此时msgrcv调用将立即返回并设置errno为EIDRM。
(2)程序接收到信号。此时msgrcv调用将立即返回并设置errno为EINTR。
msgrcv成功时返回0,失败则返回-1并设置errno。msgrcv成功时将修改内核数据结构msqid_ds的部分字段。
(1)将msg_qnum减1.
(2)将msg_lspid设置为调用进程的PID
(3)将msg_stime设置为当前的时间。
4、msgctl系统调用:控制消息队列的某些属性。
# 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
# 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
1、shmget系统调用:创建一段新的共享内存,或者获取一段已经存在的共享内存。成功时返回一个正整数值,它是共享内存的标识符;失败时返回-1,并设置error。
2、shmat系统调用:将新创建/获取之后的共享内存,关联到进程的地址空间中。
3、shmdt系统调用:使用完共享内存之后,将它从进程地址中分离。
4、shmctl系统调用:控制共享内存的某些属性。
共享内存的POSIX方法:不需要任何文件的支持,只需要先使用如下函数来创建或打开一个POSIX共享内存对象:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open(const char* name,int oflag,mode_t mode);
shm_open函数调用成功是返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联调用进程。shm_open失败时返回-1,并设置errno。
和打开文件组要关闭一样,由shm_open创建的共享内存对象在使用完之后也需要被删除
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_unlink(const char* name);
该函数将name参数指定的共享内存对象标记为等待删除。当所有使用该共享内存对象的进程都是用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。
如果代码中使用上述POSIX共享内存函数,则编译的时候需要指定连接选项-lrt。
# 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
共享内存:
效率高,原因:进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件→共享内存区,另一次从共享内存区→输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
各种通信方式的缺点:
1、管道:速度慢,容量有限,只有父子进程能通讯。
2、有名管道(named pipe):任何进程间都能通讯,但速度慢。
3、消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
4、信号量:不能传递复杂消息,只能用来同步。
5、共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。