目录
原本想详细写下,后发现有很多人总结过,那我就直接战在巨人的肩膀上不再重复造轮子了。
概览
单工、半双工、双工
- 单工:单向通信
- 半双工:双向通信,但不能同时双向
- 全双工:双向通信,可同时双向
各种IPC通信方式优缺点
1. 管道
分为匿名管道和命名管道,都是半双工。
匿名管道只能亲缘关系的进程间使用(父子进程)。
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
命名管道(又叫FIFO)本质是文件,可以非亲缘关系的进程间使用。
#include <sys/stat.h>
// 返回值:成功返回0,出错返回-1
int mkfifo(const char *pathname, mode_t mode);
不管是匿名管道还是命名管道,都是:先进先出,效率低,不适合频繁交换数据。
2. 消息队列
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// send:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// receive:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// control:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
不一定要先进先出,可以用type控制。消息队列里有消息体,消息体被读取后删除,但消息队列生命周期跟随内核,进程销毁仍存在。
效率较高。
不适合比较大的数据,在 Linux 内核中,会有两个宏定义 MSGMAX
和 MSGMNB
,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
存在用户态与内核态间数据拷贝开销。
3. 共享内存
#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
两个进程虚拟地址映射到相同的物理地址。
效率最高,无用户态与内核态间数据拷贝开销。
但无同步机制。
4. 信号量
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。若要进程间传递数据要配合共享内存。
信号量初始化为1。P(通过)操作-1,V(释放)操作+1,信号量>=0进程才可继续执行。信号量<0进程阻塞等待其他进程把信号量恢复。
具体的过程如下:
- 进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
- 若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
- 直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。
5. 信号
信号是进程间通信机制中唯一的异步通信机制。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l
命令,查看所有的信号:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
6. Socket套接字
可以跨网络与不同主机的进程间通信,也可以同主机通信。本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。
根据创建 socket 类型的不同,通信的方式也就不同:
- 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
- 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
- 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;
各种通信方式的比较和优缺点
-
管道:速度慢,容量有限,只有父子进程能通讯
-
FIFO:任何进程间都能通讯,但速度慢
-
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
-
信号量:不能传递复杂消息,只能用来同步
-
共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
如想看具体实现看下面链接。
精筛优秀博客
进程间通信——几种方式的比较和详细实例_worthsen的博客-CSDN博客_进程间的通信方式三种
5.2 进程间有哪些通信方式? | 小林coding (xiaolincoding.com)