Linux 中,不同进程的用户地址空间独立,一般不能互相访问,需借助内核空间进行通信。
1 、管道
管道本质是内核空间的一段环形缓冲区,分为匿名管道和有名管道两种
a. 匿名管道:只能用于具有亲缘关系的进程间的通信,如父进程调用pipe创建一个管道,获取读文件描述符fd[0] 和写文件描述符fd[1],其fork的子进程复制了文件描述符资源,同样有读、写的文件描述符。
b. 有名管道:通过int mkfifo(const char *pathname, mode_t mode) 创建一个类型为管道的设备文件,可以实现无亲缘关系的进程间通信,通信双方通过open("my_named_pipe", O_RDONLY或O_WRONLY);配合read、write进行通信
管道特点:流式,即通信的数据时无格式的;先进先出,因此为了防止同一个进程读到自己发送的数据,一个管道只用于单向通信(匿名管道通过close读或写文件描述符,有名管道通过设置open的权限),若要双向通信应当再创建一个管道
管道效率较低,不适合进程间频繁通信

2、消息队列
消息队列本质和内核维护的一个消息链表,相较于管道,消息队列可以传输自定义的有格式的数据 struct message{long msg_type; char msg_text[BUFSIZ];}; 且进程A发送消息后无需等待进程B接收,可继续往下执行
消息队列的使用:
/* 包含头文件*/
#include <sys/ipc.h>
#include <sys/msg.h>
/*定义消息结构 */
struct message
{
long mtype; /* 消息类型*/
char mtext[BUFSIZE]; /* 消息内容*/
}
/*ftok()创建或者getpid()获取一个唯一的键*/
key_t key = ftok("some_file", 'a');
/* 创建消息队列*/
int msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1)
{
perror("msgget");
exit(1);
}
/* 发送消息*/
struct message msg;
msg.mtype = 1; // 消息类型
strcpy(msg.mtext, "Hello, Message Queue!");
if (msgsnd(msgid, &msg, strlen(msg.mtext), 0) == -1)
{
perror("msgsnd");
exit(1);
}
/* 接收消息*/
struct message rcv_msg;
if (msgrcv(msgid, &rcv_msg, sizeof(rcv_msg.mtext), rcv_msg.mtype, 0) == -1)
{
perror("msgrcv");
exit(1);
}
printf("Received: %s\n", rcv_msg.mtext);
/* 移除消息队列 */
if (msgctl(msgid, IPC_RMID, NULL) == -1)
{
perror("msgctl");
exit(1);
}
消息队列优点:可以自定义格式;
消息队列缺点:通信不及时;消息大小有限制(不适合传输大数据),在 Linux 内核中,有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度;存在内核态与用户态的拷贝开销
3 、共享内存
共享内存的机制:不同进程的虚拟地址空间映射到同一块物理内存,不需要拷贝,是最快的IPC方式

/* 创建者 */
int main() {
key_t key = ftok("shmkeyfile", 'k');
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1)
{
perror("shmget");
return 1;
}
char *memory = (char *)shmat(shmid, NULL, 0);
if (memory == (char *)(-1))
{
perror("shmat");
return 1;
}
/* 使用共享内存 */
strcpy(memory, "Hello, Shared Memory!");
shmdt(memory); /* 脱离共享内存 */
return 0;
}
/* 使用者 */
int main() {
key_t key = ftok("shmkeyfile", 'k');
int shmid = shmget(key, 0, 0666);
if (shmid == -1)
{
perror("shmget");
return 1;
}
char *memory = (char *)shmat(shmid, NULL, 0);
if (memory == (char *)(-1))
{
perror("shmat");
return 1;
}
/* 读取共享内存 */
printf("%s\n", memory);
shmdt(memory); /* 脱离共享内存 */
shmctl(shmid, IPC_RMID, NULL); /* 删除共享内存段 */
return 0;
}
4 、信号量
对于上述的共享内存,如果多个进程同时操作同一内存,存在数据竞态的问题,信号量可避免数据竞态,实现互斥、同步
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
- 一个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
- 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。

信号量初始化为1,代表互斥信号量;信号量初始化为0,代表同步信号量(生产者消费者模型);信号量初始化为CNT,表示有CNT个资源(如线程池,连接池等等的数量)
5 、信号
上面的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。

可以通过 kill 命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如:
- kill -9 1050 ,表示给 PID 为 1050 的进程发送
SIGKILL信号,用来立即结束该进程;
信号事件的来源主要有硬件来源(如键盘 Ctrl+C )和软件来源(如 kill 命令)。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。
2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,它们用于在任何时候中断或结束某一进程。
6 、socket
管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,要实现跨网络与不同主机上的进程之间通信,需要 Socket 通信。
针对 TCP 协议通信的 socket 编程模型

针对 UDP 协议通信的 socket 编程模型

针对本地进程间通信的 socket 编程模型
本地 socket 被用于在同一台主机上进程间通信的场景:
- 本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
- 本地 socket 的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现;
对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。
对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。
本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。