进程间通信
- 1 管道
- 匿名管道
- 命名管道
- 2 消息队列
- 3 信号量
- POSIX信号量
- 有名信号量
- 无名信号量
- 有名信号量和无名信号量的公共操作
- 4 共享内存
- 5 信号
- 相关函数
- 6 套接字
- 针对 TCP 协议通信的 socket 编程模型
- 针对 UDP 协议通信的 socket 编程模型
- 针对本地进程间通信的 socket 编程模型
- 总结
Linux下的进程通信机制叫做IPC(InterProcess Communication),在Linux下有6大通信的方法,分别是:Socket、管道、信号、信号量、消息队列、共享内存。
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
1 管道
参考文章:
- https://blog.csdn.net/modi000/article/details/122084165
- https://blog.csdn.net/wm12345645/article/details/82381407
匿名管道
匿名管道是在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利。匿名管道是半双工的,数据传输只能在一个方向,并且只能在具有公共祖先的两个进程之间使用。
管道是通过调用pipe函数创建的:
int pipe(int fd[2]);
fd[0]为读端,fd[1]为写端。
我们要注意,调用 pipe之后,管道是在内存中,fd[0]和fd[1]还在进程当中,现在只有调用pipe的进程才能对管道进行操作。那么怎么样才能使得管道是跨过两个进程使用呢?
我们可使用fork创建子进程,创建的子进程会继承来自父进程的文件描述符,这样我们就做到两个进程各有1个fd[0]和fd[1],父子进程就可以往管道写入或读取数据实现通信了。
管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。那么,为了避免这种情况,通常的做法是:
- 父进程关闭读取的 fd[0],只保留写入的 fd[1];
- 子进程关闭写入的 fd[1],只保留读取的 fd[0];
f[0]和f[1]都是文件描述符,我们可以调用close函数关闭。比如关闭f[0]
close(fd[0]);
命名管道
命名管道是在内核申请一块固定大小的缓冲区,程序拥有写入和读取的权利,没有血缘关系的进程也可进行通信。由mkfifo函数创建
int mkfifo(const char *pathname,mode_t mode);
创建一个名字由pathname指定的特殊文件,在进程里只要使用这个文件,就可以相互通信。
不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
我们以pipe为例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>#define BUFF_SIZE 1024int main(void){int fd[2];pid_t pid;char *str = "hello world";char buff[BUFF_SIZE];if(pipe(fd)!=0){perror("pipe");exit(1);}pid = fork();if(pid<0){perror("fork()");}else if(pid==0){close(fd[0]);write(fd[1],str,sizeof(str));sleep(1);puts("write finished");}else{close(fd[1]);read(fd[0],buff,BUFF_SIZE);puts("read finished\n");printf("read source:%s\n",buff);}exit(0);}
运行结果:
2 消息队列
参考文章:
- https://zhuanlan.zhihu.com/p/268389190
- https://blog.csdn.net/modi000/article/details/122084165
- https://blog.csdn.net/ljianhui/article/details/10287879
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
- 创建一个消息队列是msgget函数实现的:
int msgget(key_t key, int msgflg);
它返回一个以key命名的消息队列标识符(非零整数),失败时返回-1.
可以用ftok创建一个key_t的值
key_t ftok(const char *pathname, int proj_id);
功能:通过ftok返回的是根据文件(pathname)信息和计划编号(proj_id)合成的IPC key键值。
返回值:成功:返回key_t值(即IPC键值);失败:-1,错误原因在error中
- smgsnd函数添加消息到消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgid是由msgget函数返回的消息队列标识符;msgp 参数是指向调用者定义的结构的指针,其一般形式如下:
struct msgbuf {long mtype; /* message type, must be > 0 */char mtext[1]; /* message data */};
mtext是要发送的数据,其类型是一个数组(也可以是其他数据结构),大小由msgsz指定,msgsz必须是一个非负整数,mtype 字段必须是正整数值,接收函数可以使用该值进行消息选择;msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情
- msgrcv接受消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgtyp可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型(和msgsnd函数msgbuf结构体的mtype相等)的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。其他参数和msgsnd一样。
- 消息队列的控制函数msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
command是将要采取的动作,它可以取3个值
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
发送进程msgsend.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "./msgbuf.h"int main(void)
{int running = 1;struct msgbuf data;long mtype=0;key_t key; int msgid;/* 创建消息队列 */key = ftok("./",2);if(key==-1){printf("ftok error\n");exit(1);}/* 建立消息队列 */msgid=msgget(key,IPC_CREAT|0666);if(msgid==-1){fprintf(stderr,"msgget error");exit(1);}/* 发送数据 */while(running){printf("enter some test:");fgets(data.mtext,BUFFSIZE ,stdin);data.mtype = 1;if(msgsnd(msgid,(void *)&data,BUFFSIZE,0)==-1){fprintf(stderr,"msgsnd error\n");exit(1);}if(strncmp(data.mtext,"end",3)==0){running = 0;}}exit(0);
}
接受进程代码msgrec.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "./msgbuf.h"int main(void)
{int msgid;key_t key;int running = 1;struct msgbuf data;memset(data.mtext,0,BUFFSIZE);long mtype=0;key = ftok("./",2);if(key==-1){printf("ftok error\n");exit(1);}/* 建立消息队列 */msgid=msgget(key,IPC_CREAT|0666);if(msgid==-1){fprintf(stderr,"msgget error");exit(1);}/* 从队列中获取消息,直到遇到end消息为止 */while(running){if(msgrcv(msgid,(void *)&data,BUFFSIZE,mtype,0)==-1){fprintf(stderr,"msgrcv error with errno:%d\n",errno);exit(1);}printf("message:%s\n",data.mtext);/* 删除消息队列 */if(strncmp(data.mtext,"end",3)==0){if(msgctl(msgid,IPC_RMID,NULL)==0){printf("delete message queue success!\n");}running =0;}}exit(0);
}
msgbuf.h代码:
#include <errno.h>#define BUFFSIZE 1024struct msgbuf{long mtype;char mtext[BUFFSIZE];
};
运行结果:
3 信号量
参考文章:
- https://blog.csdn.net/modi000/article/details/122084165
- https://zhuanlan.zhihu.com/p/351692436
- https://zhuanlan.zhihu.com/p/22612079
信号量与管道、消息队列不同,它是一个计数器,用于对多个进程提供对共享数据对象的访问,也就是对临界资源进行保护。
信号量表示资源的数量,为了获得共享资源,进程需要执行下列操作:
- 测试控制该资源的信号量
- 若此信号量的值为正,则进程可以使用该资源,当使用该共享资源时,进程会将信号量值减1,表示它使用了一个资源单位。
- 否则,若此信号量的值为0,则进程进入休眠状态,直到信号量值大于0.
根据初始值的不同,信号量可以细分为 2 类,分别为二进制信号量和计数信号量:
二进制信号量:指初始值为 1 的信号量,此类信号量只有 1 和 0 两个值,通常用来替代互斥锁实现进程同步;
计数信号量:指初始值大于 1 的信号量,当进程中存在多个进程,但某公共资源允许同时访问的进程数量是有限的(出现了“狼多肉少”的情况),这时就可以用计数信号量来限制同时访问资源的进程数量。
POSIX信号量
POSIX信号量是一个sem_t类型的变量,但POSIX有两种信号量的实现机制:无名信号量和命名信号量。 无名信号量只可以在共享内存的情况下使用,比如实现进程中各个线程之间的互斥和同步,因此无名信号量也被称作基于内存的信号量;命名信号量通常用于不共享内存的情况下,比如进程间通信。
有名信号量
有名信号量由于其有名字, 多个独立的进程可以通过名字来打开同一个信号量, 从而完成同步操作, 所以有名信号量的操作要方便一些, 适用范围也比无名信号量更广。
- 创建有名信号量
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
2. 有名信号量的关闭
int sem_close(sem_t *sem);
当一个进程打开有名信号量时, 系统会记录进程与信号的关联关系。 调用sem_close时, 会终止这种关联关系, 同时信号量的进程数的引用计数减1。
- 有名信号量的删除
int sem_unlink(const char *name);
函数会负责将该有名信号量删除。 由于系统为信号量维护了引用计数, 所以只有当打开信号量的所有进程都关闭了之后, 才会真正地删除。
无名信号量
无名信号量, 由于其没有名字, 所以适用范围要小于有名信号量。 只有将无名信号量放在多个进程或线程都共同可见的内存区域时才有意义, 否则协作的进程无法操作信号量, 达不到同步或互斥的目的。 所以一般而言, 无名信号量多用于线程之间。 因为线程会共享地址空间, 所以访问共同的无名信号量是很容易办到的事情。 或者将信号量创建在共享内存内, 多个进程通过操作共享内存的信号量达到同步或互斥的目的。
- 无名信号量的创建
int sem_init(sem_t *sem, int pshared, unsigned int value);
无名信号量的生命周期是有限的, 对于线程间共享的信号量, 线程组退出了,无名信号量也就不复存在了。 对于进程间共享的信号量, 信号量的持久性与所在的共享内存的持久性一样。
无名信号量初始化以后, 就可以像操作有名信号量一样操作无名信号量了。
- 无名信号量的销毁
int sem_destroy(sem_t *sem);
sem_destroy用于销毁sem_init函数初始化的无名信号量。 只有在所有进程都不会再等待一个信号量时, 它才能被安全销毁。
有名信号量和无名信号量的公共操作
- 获取信号量
#include <semaphore.h>int sem_wait(sem_t *sem);int sem_trywait(sem_t *sem);int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);Link with -pthread.
- 归还信号量2
int sem_post(sem_t *sem);
- 获取当前信号量的值
int sem_getvalue(sem_t *restrict, int *restrict);
我们用信号量解决生产者拿消费者问题:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>#define N 10
#define true 1void show_buffer();
void *producer(void *id);
void *consumer(void *id);typedef int item;
//环形队列
int in =0, out =0;
//缓冲区
item buffer[N];
//互斥、空缓冲区、满缓冲区信号量
sem_t mutex,empty,full;/* 生产者 */
void *producer(void *id)
{while(true){item nextp = rand() % 10;sem_wait(&empty);sem_wait(&mutex);buffer[in]=nextp;in = (++in)%N;printf("======%s======\n",(char *)id);printf("生产了:%d\n",nextp);show_buffer();sem_post(&mutex);sem_post(&full);}
}/* 消费者 */
void *consumer(void *id)
{while(true){sem_wait(&full);sem_wait(&mutex);item nextc = buffer[out];out = (++out)%N;int emp,ful;sem_getvalue(&empty,&emp);sem_getvalue(&full,&ful);printf("======%s======\n""缓冲区大小:%d\n""消费的物品:%d\n""空缓冲区的数量:%d\n""满缓冲区的数量:%d\n",(char*)id,N,nextc,emp,ful);show_buffer();sem_post(&mutex);sem_post(&empty);}
}/* 显示缓冲区内容 */
void show_buffer()
{printf("[ ");if(in==out)//满for(int i=0;i<N;i++)printf("%2d ",buffer[i]);else if(out<in)for(int i=out ;(i%N)<in;i++)printf("2%d ",buffer[i]);else{for(int i=out;i<N;i++)printf("%2d ",buffer[i]);for(int i=0;i<in;i++)printf("%2d ",buffer[i]);}printf("]\n");
}int main(void)
{/*初始化信号量*/if(sem_init(&mutex,0,1)==-1)perror("sem_init");if(sem_init(&empty,0,N)==-1)perror("sem_init");if(sem_init(&full,0,0)==-1)perror("sem_init");/* 两个生产者、两个消费者 */pthread_t p1,p2,c1,c2;int ret;if((ret=pthread_create(&p1,NULL,producer,(void *)"生产者1"))!=0){fprintf(stderr,"ptheard_create error,error number :%d\n",ret);exit(1);}if((ret=pthread_create(&p2,NULL,producer,(void *)"生产者2"))!=0){fprintf(stderr,"ptheard_create error,error number :%d\n",ret);exit(1);}if((ret=pthread_create(&c1,NULL,consumer,(void *)"消费者1"))!=0){fprintf(stderr,"ptheard_create error,error number :%d\n",ret);exit(1);}if((ret=pthread_create(&c2,NULL,consumer,(void *)"消费者2"))!=0){fprintf(stderr,"ptheard_create error,error number :%d\n",ret);exit(1);}if(ret=pthread_join(p1,NULL)){fprintf(stderr,"ptheard_join error,error number :%d\n",ret);exit(1);}if(ret=pthread_join(p2,NULL)){fprintf(stderr,"ptheard_join error,error number :%d\n",ret);exit(1);}if(ret=pthread_join(c1,NULL)){fprintf(stderr,"ptheard_join error,error number :%d\n",ret);exit(1);}if(ret=pthread_join(c2,NULL)){fprintf(stderr,"ptheard_join error,error number :%d\n",ret);exit(1);}return 0;}
4 共享内存
参考文章:
- https://blog.csdn.net/modi000/article/details/122084165
- https://zhuanlan.zhihu.com/p/147826545
共享内存的机制,就是进程拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,大大提高了进程间通信的速度。
- 打开或创建共享内存
int shmget(key_t key, size_t size, int shmflg);
- 共享内存映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
将一个共享内存段映射到调用进程的数据段中。简单来理解,让进程和共享内存建立一种联系,让进程某个指针指向此共享内存。
- 解除共享内存映射
int shmdt(const void *shmaddr);
将共享内存和当前进程分离( 仅仅是断开联系并不删除共享内存,相当于让之前的指向此共享内存的指针,不再指向)。
- 共享内存控制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
接下来我们做这么一个例子:创建两个进程,在 A 进程中创建一个共享内存,并向其写入数据,通过 B 进程从共享内存中读取数据。
写端:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>#define SIZE 1024void printf_error(void *s)
{fprintf(stderr,"%s\n",(char *)s);exit(1);
}int main(void)
{key_t key;int shmid;int ret;/*创建key值 */key = ftok("../",2021);if(key==-1){printf_error("ftok error");}/* 创建共享内存 */shmid = shmget(key,SIZE ,IPC_CREAT|0666);if(shmid==-1){printf_error("shmget error");}/* 映射 */char *shmad = shmat(shmid,NULL,0);if(shmad < 0){printf_error("shmat error");}/* 拷贝数据至共享内存区 */memset(shmad,0,SIZE);strcpy(shmad,"How are you,mike\n");return 0;
}
读端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define SIZE 1024void printf_error(void *s)
{fprintf(stderr,"%s\n",(char *)s);exit(1);
}
int main(void)
{int shmid;key_t key;int ret;/* 创建key值 */key = ftok("../",2021);if(key==-1){printf_error("ftok error");}system("ipcs -m"); //查看共享内存//打开共享内存shmid = shmget(key,SIZE,IPC_CREAT|0666);if(shmid<0){printf("shmget error");}// 映射char *shmad = shmat(shmid,NULL,0);if(shmad<0){printf_error("shmat error");}// 读取共享内存区数据printf("data = [%s]\n",shmad);//分离共享内存和当前进程ret = shmdt(shmad);if(ret<0){printf_error("shmdt error");}printf("deleted shared_memory\n");//删除共享内存shmctl(shmid,IPC_RMID,NULL);system("ipcs -m");return 0;
}
5 信号
参考文章:
- https://blog.csdn.net/modi000/article/details/122084165
- https://www.cnblogs.com/electronic/p/10939769.html
对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
-
执行默认操作。Linux 对每种信号都规定了默认操作,例如, SIGTERM 信号,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里。
-
捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
-
忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程
相关函数
- int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0;失败 -1 - int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0;失败 -1 - unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替
-
int pause(void);
功能:用于将调用进程挂起直到收到信号为止。 -
信号处理函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数: signum:要处理的信号//不能是SIGKILL和SIGSTOP;
handler:
1. SIG_IGN:忽略该信号;
2. SIG_DFL:采用系统默认方式处理信号。
3. 自定义的信号处理函数指针
返回值:成功:设置之前的信号处理方式;失败:SIG_ERR
- void abort(void);
给自己发送异常终止信号(SIGABRO),终止并产生core文件。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>void handler(int num)
{printf(" this signal no. is %d\n",num);kill(getpid(),SIGTERM);
}int main(void)
{signal(SIGINT,handler);while(1);return 0;
}
6 套接字
文章来源:
- https://blog.csdn.net/modi000/article/details/122084165
前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
创建一个socket:
int socket(int domain, int type, int protocal)
三个参数分别代表:
- domain 参数用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本机;
- type 参数用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字;
- protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可;
根据创建 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;
针对 TCP 协议通信的 socket 编程模型
- 服务端和客户端初始化 socket,得到文件描述符;
- 服务端调用 bind,将绑定在 IP 地址和端口;
- 服务端调用 listen,进行监听;
- 服务端调用 accept,等待客户端连接;
- 客户端调用 connect,向服务器端的地址和端口发起连接请求;
- 服务端 accept 返回用于传输的 socket 的文件描述符;
- 客户端调用 write 写入数据;服务端调用 read 读取数据;
- 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。
这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
针对 UDP 协议通信的 socket 编程模型
UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。
对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。
另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。
针对本地进程间通信的 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 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。
总结
Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。
匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。
那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。
与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是进程间通信机制中唯一的异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。