我们知道,Linux进程通信的方法一共有五种,分别是:管道、信号量、共享内存、消息队列、套接字。因为套接字涉及到网络编程,在这里只介绍前四种方式。
目录
1.管道
首先关于管道,先普及几个知识点:
- 管道分为命名管道和无名管道
- 管道的大小永远为0
- 管道存在于内存之中,不会永久保存
- 管道的传送方式是半双工的(一边进,一边出)
- 无名管道通过函数pipe创建,只能用于父子进程之间
- 有名管道可以通过mkfifo+文件名的方式在终端进行创建,有名管道可以在任意两个进程之间进行通信
1.1无名管道
上述已经说过,无名管道只能在父子进程中使用。
无名管道通过下面方式进行创建:
#include<unsitd.h>
int pipe(int filefd[2]);
经由参数filefd返回两个文件描述符:filefd[0]为读而打开,filefd[1]为写而打开。因此filefd[0]是管道的输出,filefd[1]是管道的输入。单个进程中的管道没有什么异常。通常,调用pipe的进程接着调用fork。调用fork之后做什么取决数据流的方向,对于从父进程到子进程的管道,父进程关闭管道的读端(filefd[0]),子进程关闭管道的写端(filefd[1])。见下图:
我们编写程序pipe.c如下所示:
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
assert(pid!=-1);
if(pid==0)
{
close(fd[1]);//关闭写端
char buff[128]={0};
read(fd[0],buff,127);
printf("child buff =%s\n",buff);
close(fd[0]);
}
else
{
close(fd[0]);//关闭读端
printf("input:\n");
char buff[128]={0};
fgets(buff,127,stdin);
write(fd[1],buff,strlen(buff));
close(fd[1]);
return 0;
}
}
运行程序如下所示:
1.2有名管道
有名管道可以通过mkfifo +名称在终端创建,然后在程序中像文件一样去使用它:
创建:
可以发现目录下多了一个棕色的名称,fifo,这就是一个有名称的管道。然后我们可以编写程序,在一进程中写,一个进程中读。
接下来我们编写两个程序fifo_1.c用于在管道中写东西,fifo_2.c从管道中读东西。
fifo_1.c
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int fd = open("./fifo",O_WRONLY);
assert(fd!=-1);
printf("fd=%d\n",fd);
char buff[128]={0};
while(1)
{
printf("input:\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
break;
write(fd,buff,strlen(buff));
}
close(fd);
return 0;
}
fifo_2.c
#include <stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
int main()
{
int fd = open("./fifo",O_RDONLY);
assert(fd!=-1);
printf("fd=%d\n",fd);
while(1)
{
char buff[128]={0};
int n = read(fd,buff,127);
if(n==0)
{
break;
}
printf("%d %s\n",n,buff);
}
close(fd);
return 0;
}
测试:
测试正常。
当我们不想要管道的时候可以用rm命令进行删除,也可以不用管,关机之后会自动删除,因为它存在于内存之中。
2.信号量
首先介绍以下什么是临界资源和临界区:
临界资源:同一时刻只能允许一个进程(线程)访问的资源。
临界区:访问临界资源的代码段。
信号量是一种特殊的变量、值可以改变,常常用于控制多进程访问临界资源。对信号量加1和减1操作都是原子操作。加1操作和减1操作对应信号量的p操作和v操作。
p操作:对信号量的值减1(临界资源可以使用),如果信号量为0,则执行p操作的进程阻塞在此处(临界资源正被其他进程使用)。
v操作:对信号量的值加1,不会阻塞,表示资源的释放。代表资源临界资源可以使用。
同时内核为每个信号量集(即每次通过semget函数获取的信号量,可能不止一个信号量,称为信号量集)创建了一个semid_ds结构体,如下图所示:它包含了信号量的一些基本信息:
图中提到的 ipc_perm结构图如下图所示:
它存储了操作权限、所有者以及一些id。
在共享内存中内核为每个共享内存设置了一个shmid_ds结构体,在消息队列中,每个队列都有一个msgid_ds结构体。它们和semid_ds类似,下面不再介绍。
接下来介绍几下关于信号量的函数:
int semget(key_t _key ,int _nsems,int _semflg);
此函数用于创建一个新的信号量集或者获取一个已经存在的信号量集。
成功返回信号量集的标识ID,失败返回-1。
_key:信号量集的键值,如果为0,则创建的信号量集只能在本进程内使用
_nsems:信号量的个数
_semflg:信号量集的创建方式和权限信号量集的创建方式或权限。有IPC_CREAT,IPC_EXCL。
IPC_CREAT如果信号量集不存在,则创建一个信号量集,否则获取。
IPC_EXCL只有信号量集不存在的时候,新的信号量集才建立,否则就产生错误。
int semctl(int _semid,int _semnum,int _cmd)
此函数用于控制信号量的信息。
成功返回0,失败返回-1
_semid:信号量集标识符,为semget返回的值
_semnum:操作信号在信号集种的编号。我们可以用semget得到多个信号量,那么编号为0,就表示第一个
_cmd:要执行的操作,常用的操作有IPC_RMID.将信号量从内存种删除。SETVAL:用union给信号量赋值
int semop(int semid,struct sembuf *_ops,size_t _nsops)
此函数用于改变信号量的值,相当于信号量的p、v操作
成功返回0,失败返回-1.
_semid:信号量的标识码,也就是semget()的返回值。
_sops是一个指向结构体数组的指针。
struct sembuf{
unsigned short sem_num;//第几个信号量,第一个信号量为0;
short sem_op;//对该信号量的操作。-1:p操作,1:v操作
short _semflg;/*
IPC_NOWAIT //对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
IPC_UNDO //程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的 值。这样做的目的在于避免程序 在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
*/
};
nsops:操作结构的数量,恒大于或等于1。
根据以上信息我们将信号量的相关操作进行封装:
sem.h
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
//信号集标识符
static int semid = -1;
//信号量的初始化
void sem_init();
//p操作
void sem_p();
//v操作
void sem_v();
//信号量的摧毁
void sem_destory();
//用于给信号量赋值
union sem
{
int val;
};
sem.c
#include"sem.h"
void sem_init()
{
semid= semget((key_t)7892,1,IPC_CREAT|IPC_EXCL|0600);//尝试创建一个全新的信>
if(semid==-1)
{
semid = semget((key_t)7892,1,IPC_CREAT|0600);//获取已有信号量
if(semid==-1)
{
perror("semget error!");
}
}
else
{
union sem a;
a.val = 1;
//给信号量赋值
if(semctl(semid,0,SETVAL,a)==-1)
{
perror("semctl set val error!");
}
}
}
void sem_p()
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;//p操作
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
perror("semop p error!");
}
void sem_v()
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
perror("semop v error!");
}
void sem_destory()
{
if(semctl(semid,0,IPC_RMID)==-1)
perror("destory error!");
}
编码测试:
a.c
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include"sem.h"
int main()
{
//初始化一个信号量
sem_init();
int i = 0;
for(;i<5;++i)
{
sem_p();//p操作
//假设打印行为为临界区
printf("A");
fflush(stdout);
int n = 0;
n = rand()%3;
sleep(n);
printf("A");
fflush(stdout);
n = rand()%3;
sleep(n);
sem_v();//v操作,释放资源
}
exit(0);
}
b.c如下:
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include"sem.h"
int main()
{
sem_init();
int i = 0;
for(;i<5;++i)
{
sem_p();
printf("B");
fflush(stdout);
sleep(2);
printf("B");
fflush(stdout);
sleep(1);
sem_v();
}
sem_destory();
exit(0);
return 0;
}
运行结果:按照我们预想的,同时运行两个程序,A和B应该会成对打印:即打印两个A,然后打印两个B,不会出现一个A一个B的情况。
刚开始打印4个A是因为b还没有运行起来。
3.共享内存
共享内存即允许两个或者是多个进程共享一给定的存储区。因为数据不需要在客户端进程和服务器进程之间进行赋值,所以这也是最快的一种IPC(进程通信技术)。使用共享内存时需要掌握的唯一窍门是多个进程之间对一给定的存储区的同步访问。若服务器正在将数据放入共享内存中,那么在这一操作之前,客户进行不应该去从共享内存中读取数据。通常情况下,信号量被用来实现共享内存访问的同步操作。
下图展示共享内存在进程地址空间中的映射位置:
和mmap映射不同,共享内存段没有与任何文件相关联。
创建共享内存的一般过程如下:
创建共享内存:
创建共享内存会用到下面的函数:
int shmget(key_t key,size_t size,int shmflag);
此函数用于得到一个共享内存的标识符。
成功返回标识符,失败返回-1
key:我们知道建立IPC通信的时候必须指定一个ID值,该值通常又ftok函数得到。如果要两个同时访问同一共享内存,那么要设置相同的key值。
size:创建时为大于0的整数,共享内存的大小。获取共享内存时指定为0.
shmflg: 创建共享内存的方式和操作权限,和信号量中类似
创建映射关系:
创建映射关系的意思就是将共享内存区映射进调用进程的地址空间(mmap文件映射区)。会用到下面函数:
void *shmat(int shmid.const void*shmaddr,int shmflg);
此函数用于把共享内存映射到调用进程的地址空间。
成功返回映射在进程中的起始地址,失败返回-1.
shmid:共享内存标识,shmget()的返回值
shmaddr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地 址位置
shmflg:是一组标志位,通常为0。
当映射成功之后,我们通常将void *转换为char *,然后就可以像使用字符串一样对共享内存进行操作了。
断开映射关系:
即在共享内存的需求结束之后,将共享内存的映射从进程的地址空间中剔除。这一步会用到下面的函数:
int shmdt(const void *addr);
与shmat相反,用来断开共享内存与进程地址的映射,即进制本程序访问此片共享内存。
成功返回0,失败返回-1
addr为shmat返回的地址
当断开映射之后shmdt使相关shmid_ds结构中的shm_nattch计数器的值减1.
删除共享内存:
即从系统中删除该片共享内存。函数如下:
shmctl(int shmid,int cmd,struct shmid_ds *buf);
该函用于完成对共享内存的系列操作
成功返回0,出错返回-1
shmid:共享内存标识符
cmd:操作方式,经常用到的为IPC_RMID:即删除这片共享内存
buf:通常为NULL
因为每个共享内存有一个连接计数,所以除非使用该共享内存的最后一个进程终止或者与该共享内存断开映射关系,否则不会实际上删除该共享内存。
前面讲过使用共享内存需要信号量进行同步,下面两个文件a.c和b.c用于模拟共享内存的操作,其中使用到的信号量的代码和上述第二节(信号量)中的sem.h和sem.c相同,这里不再赘述。
按照上述流程来编写a.c和b.c,并进行测试:
a.c:向共享内存中写入数据
#include <stdio.h>
#include<string.h>
#include<assert.h>
#include<sys/shm.h>
#include"sem.h"
int main()
{
//初始化信号量
sem_init();
//创建共享内存
int shmid = shmget((key_t)1234,256,IPC_CREAT|0600);
assert(shmid!=-1);
//创建映射关系
char *s = (char *)shmat(shmid,NULL,0);
while(1)
{
char buff[128]={0};
printf("input:\n");
sem_p();
fgets(buff,127,stdin);
//p操作
strcpy(s,buff);
//v操作
sem_v();
if(strncmp(buff,"end",3)==0)
break;
}
//删除映射关系
shmdt(s);
return 0;
}
b.c:从共享内存中读取数据
/*
*由于程序最后退出,所以将信号量和共享内存的删除放在本程序内
* */
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/shm.h>
#include"sem.h"
int main()
{
sem_init();
//这里获取已存在的共享内存,所以可以设置size大小为0
int shmid = shmget((key_t)1234,0,IPC_CREAT|0600);
assert(shmid!=-1);
char *s = (char *)shmat(shmid,NULL,0);
assert(s!=NULL);
while(1)
{
sem_p();
if(strncmp(s,"end",3)==0)
break;
printf("s=%s\n",s);
sem_v();
}
shmdt(s);
//删除信号量
sem_destory();
if(shmctl(shmid,IPC_RMID,NULL)==-1)
{
printf("shmctl error!");
}
exit(0);
return 0;
}
运行结果:
4.消息队列
消息队列是消息的连接表,存放在内核中并由消息队列标识符来标识。消息队列提供了一种从一个进程向另一个进程发送数据块的方法。同命名管道一样,每个数据块都有最大长度限制。Linux中的宏MSGMAX和MSGMNB限制了消息的最大长度和消息队列的最大长度。
消息队列的使用场景:
- 将消息队列应用在日志处理当中,比如kafka的应用,解决了大量日志传输的问题
- 秒杀活动一般会因为流量过大,导致流量暴增,应用挂掉的问题。为了解决这个问题,一般需要在应用前端加入消息队列:可以控制活动的人数,可以缓解短时间内高流量压垮应用。
- 异步处理,用户注册之后,需要发注册邮件和短信。
- 消息通讯是指,消息队列一般都内置了高效的通信机制,因此可以使用在纯粹的消息通信应用中,比如实现点对点的消息交互或者聊天室等。
消息队列的操作和信号量、与共享内存类似,包括操作的函数也有相似之处,我们在使用的过程中遵循以下流程:
消息队列的创建:
消息队列的创建与其他IPC机制一样,程序必须提供键名来获取某个特定的消息队列:
消息队列的创建和获取用到了msgget函数,用法如下:
int msgget(key_t key, int msgflag);
此函数用于创建或者获取一个消息队列
成功返回消息对联的标识符,时报返回-1
key:键值
msgflag:创建模式和操作权限,同shmget中标志位的含义一样
添加消息:
向消息队列中添加消息,会用到函数msgsnd函数,用法如下:
int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);
此函数用于向消息队列中添加消息。
成功返回0,出错返回-1
msgid:消息队列的唯一标识符,mggget函数的返回值
ptr:一个指向消息类型的结构体,可以定义消息类型如下:
struct mymesg
{
long type;
char buff[128];
};
那么ptr可以是指向一个mymesg类型的变量。
nbytes:表示消息的长度,注意不是mymesg结构体的长度,buff的长度,如上即为128。
flag:这个参数的值可以设置位IPC_NOWAIT,类似于I/O文件的非阻塞I/O标志。
获取消息:
size_t msgrcv(int msgid,void *ptr,size_t nbytes,long type,int flag);
此函数用于从消息队列中读取消息:
成功返回消息的数据部分的长度,失败返回-1
msgid、ptr、nbytes以及flag和msgsnd中的含义一样。
tyepe:通过设置type的值可以指定想要哪一种消息(type即ptr指向结构体里面的type)
type ==0 返回队列中的第一个消息
type>0 返回队列中消息类型位type的第一个消息
type<0 返回队列中消息类型值小于或者等于type绝对值的消息,如果满足条件的消息有多个,则取类型值最小消息的第一个。
删除消息队列:
int msgctl(int msgid,int command struct msgid_ds *buf);
按照command命令对消息队列进行操作
成功返回0,失败返回-1.
command可以采取三个值:
IPC_STAT:把msgid_ds结构中的数据设置位消息队列中的关联值。
IPC_SET:把消息队列的当前关联值设置为msgid_ds结构体中给的值
IPC_RMID:删除消息队列
buf:指向一个msgid_ds结构体的指针,如果不需要操作msgid_ds结构体,就设置位NULL
编写两个程序:msg_1.c和msg_2.c去实现消息队列:
msg_1.c向消息队列中写入一个消息:
#include <stdio.h>
#include<string.h>
#include<assert.h>
#include<sys/msg.h>
//定义消息类型,第一个变量必须为长整型,后面的没有要求
struct mess
{
long type;
char buff[32];
};
int main()
{
//创建消息队列
int msgid = msgget((key_t)1234,IPC_CREAT|0600);
assert(msgid!=-1);
struct mess dt;
dt.type = 1;
strcpy(dt.buff,"hello world");
//发送消息
msgsnd(msgid,(void*)&dt,32,IPC_NOWAIT);
return 0;
}
msg_2.c:从消息队列中读取一个消息
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/msg.h>
struct mess{
long type;
char buff[32];
};
int main()
{
//获取消息队列
int msgid = msgget((key_t)1234,IPC_CREAT|0600);
struct mess dt;
//接收消息
msgrcv(msgid,(void*)&dt,32,0,IPC_NOWAIT);
printf("收到消息:%s\n",dt.buff);
//删除消息队列
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
程序运行结果如下:
注意:对消息队的删除操作处理不是很完善。因为内核对每个消息队列并没有设置引用计数器。所以如果删除一个消息队列,那么另一个还在使用此消息队列的程序可能会出错。