目录
进程间通信(IPC机制)
一、IPC机制有哪些方式
总共有五个管道、信号量、共享内存、消息队列、套接字
三个空填(信号量、共享内存、消息队列)
二、管道
2.1作用和特点:
用来在两个进程之间传递数据
只能以只读或者只写(半双工)
无论有名还是无名,写入管道的数据都存储在内存中
管道文件创建在内存中
2.2管道的分类
无名管道 有名管道
2.3无名管道
主要用于父子进程间的通信
创建无名管道命令: int fd[2]
fd为文件描述符,使用pipe(fd)的返回值判断是否成功,成功返回0,失败返回-1;对于该无名管道,fd[0]是管道读端的描述符,fd[1]是管道写端的描述符。
2.4有名管道
用于任意进程间的通信
创建有名管道命令: mkfifo FIFO
2.5使用无名管道实现父子进程间通讯
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
int main()
{
int fd[2];
if(pipe(fd)==-1)
{
exit(-1);
}
pid_t pid = fork();
if(pid==-1)
{
exit(-1);
}
if(pid==0)//子进程读取数据 关闭写端
{
close(fd[1]);//在进行进程复制后,父子进程都有一个管道的读写两端fd[0]和fd[1],管道是半双工在同一时间只能读或者写。子进程进行读操作时,所以需要关闭该管道的写端
char buff[128]={0};
read(fd[0],buff,127);
printf("%s\n",buff);
close(fd[0]);
}
else//父进程写入数据 关闭读端
{
close(fd[0]);
write(fd[1],"hello",5);
close(fd[1]);
}
exit(0);
}
2.6使用有名管道实现进程间通讯
第一步:创建有名管道文件
第二步:创建a.c和b.c分别实现向FIFO里面写入数据和从中读取数据
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
//a.c 写入数据
int main()
{
int fdw=open("FIFO",O_WRONLY);
if(fdw==-1)
{
exit(-1);
}
char buff[128]={0};
while(1)
{
memset(buff,0,128);
printf("input:");
fgets(buff,128,stdin);
if(strcmp(buff,"end")==0)
{
break;
}
write(fdw,buff,strlen(buff));
}
close(fdw);
exit(0);
}
//b.c 读取数据
int main()
{
int fdw=open("FIFO",O_ONONLY);
if(fdw==-1)
{
exit(-1);
}
char buff[128]={0};
while(1)
{
memset(buff,0,128);
int num=read(fdw,buff,128);
if(num==0)
{
break;
}
printf("read=%s",buff);
}
close(fdw);
exit(0);
}
第三步:实现进程间通讯
2.7文件描述符的复制,重定向
在shell中运行一个进程,默认会有三个文件描述符,分别是:
0 -> stdin(标准输入)
1 -> stdout(标准输出)
2 -> stderr(标准错误)
函数dup(int fd)
作用:将fd复制一份,分配给文件表中未被使用的最小的一项,其返回值为分配后的文件描述符。
例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("a.txt", O_RDONLY);
int copyfd = dup(fd);
//将fd阅读文件置于文件末尾,计算偏移量。
printf("fd = %d偏移量:%d",fd,lseek(fd, 0, SEEK_END));
//现在我们计算copyFd的偏移量
printf("copyfd = %d偏移量:%d",copyfd,lseek(cpoyfd, 0, SEEK_END));
exit(0);
}
输出结果为:fd = 3 偏移量: 75 copyFd = 4偏移量:75
其实就是给fd所指向的文件再分配一个最小的未被使用的文件描述符(返回值),也就是fd 和 copyfd均指向同一个文件,且共享偏移量和文件状态。
使用的函数dup2 ( int oldfd , int newfd)
在打开文件后,系统给文件的描述符会从3开始依次往下分配。dup2函数会先判断新的文件描述符(newfd)和旧的文件描述符(oldfd)是否是同一个值,如果是就直接返回newfd。如果不是,会先把newfd指向的文件关闭,然后将oldfd 复制给新newfd (其实就是让新的文件描述符都指向旧的文件描述符所指向的文件)。
例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int oldfd=open("a.txt",O_CREAT|O_WRONLU,0600);
dup2(oldfd,1);
printf("hello\n");
exit(0);
}
运行后,会发现 hello 会打印到 a.txt 文件中不会显示在屏幕上。因为printf 函数是会将内容输出到 1 对应的文件中, 1 原本对应着 stdout 标准输出文件 ,在执行dup2函数后,1 此时对应着 a.txt 文件,所以printf函数会将 hello 打印到a.txt文件中。
三、信号量
作用:当多个进程访问同一个临界资源时会发生冲突,信号量用来控制进程访问临界资源,用于进程间同步。
进程间同步:同一时刻,只能有一个进程访问资源。若资源被占用,程序就会被阻塞,实现对程序的控制。
3.1信号量的值与操作
信号量为特殊的变量,其值是大于等于0的,值的大小为可用资源的数目。为0时,说明没有可用资源。
当信号量的值只有(0,1)时,就叫做2值信号量
存在两种操作:
p操作:获取资源 -1 当信号量的值为0时p操作阻塞
v操作:释放资源 +1
临界资源:同一时刻只允许一个进程访问的资源。
临界区:访问临界资源的代码段。
信号量分为内核态信号量和用户态信号量,以下介绍的都是内核态信号量。
3.2信号量API
使用的函数:
semget() 创建信号量或获取已存在的信号量
semop() P,V操作 IPC_UNDO : 如果P()操作后,程序异常终止,未释放资源,系统会去释放这个资源。IPC_NOWAIT
semctl() 对信号量进行初始化和删除 参数分别为 SETVAL(初始化),IPC_RMID(删除)
3.3信号量的函数实现
//sem.h
#include<stdio.h>
#include<stdlib.h>
#include<unistdh>
#include<sys/sem.h>
union semun
{
int val;
};
void sem_init();
void sem_p();
void sem_v();
void sem_destroy();
sem.c文件
信号量的初始化函数:sem_init()
使用到的函数有:semget()+semctl()
#include"sem.h"
static int semid;//静态全局变量,只限本文件
void sem_init()
{
semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//创建信号量。key值为1234,表示使用该函数的进程用的是同一个信号量。1表示一个信号量
if(semid==-1)//创建失败(已存在信号量,不要创建信号量,直接获取)
{
semid = semget((key_t)1234,1,0600);
if(semid==-1)
{
exit(1);
}
}
else//创建成功
{
union semun a;
a.val=1;
if(semctl(semid,0,SETVAL,a)==-1)//初始化信号量。0表示为0号下标,一个信号量就是0下标
{
perror("semctl err");
exit(1);
}
}
}
信号量的P函数:sem_p()
使用到的函数有:semop()
#incldue"sem.h"
void sem_p()
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;//获取资源
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("semop err");
exit(1);
}
}
信号量的V函数:sem_v()
使用到的函数有:semop()
#incldue"sem.h"
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 err");
exit(1);
}
}
信号量的批量删除函数:sem_destroy()
使用到的函数有:semctl()
#incldue"sem.h"
void sem_destroy()
{
if(semctl(semid,0,IPC_RMID)==-1)
{
perror("semctl error");
exit(1);
}
}
3.4信号量的使用
//a.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include"sem.h"
int main()
{
sem_init();//创建或者获取信号量
for(int i=0;i<5;++i)
{
sem_p();//代码1
printf("A");
fflush(stdout);
sleep(1);
printf("A");
fflush(stdout);
sem_v();//代码2
sleep(1);
}
//a.c程序睡眠10s,默认两个程序都使用完资源,需要销毁信号量。销毁信号量只能在一个程序中进行销毁,不能两个程序都进行销毁。
sleep(10);
sem_destroy();
exit(0);
}
//b.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include"sem.h"
int main()
{
sem_init();//创建或者获取信号量
for(int i=0;i<5;++i)
{
sem_p();//代码3
printf("B");
fflush(stdout);
sleep(1);
printf("B");
fflush(stdout);
sem_v();//代码4
sleep(1);
}
exit(0);
}
在未加代码1,2,3,4时,同时运行a.c和b.c程序后,运行结果为ABABABABABABABABABAB,存在不成对,说明A在打印时B也进行打印;在加入这四行代码后,运行结果为AABBAABBAABBAABBAABB,均成对出现。
3.5 ipcs 和 ipcrm命令
ipcs: 查看信号量,共享内存,消息队列
ipcs -s:查看信号量
ipcrm -s + semid(要删除信号量的id): 删除该id的信号量
四、共享内存
4.1共享内存的原理
共享内存为多个进程之间共享和传递数据提供了一种有效的方式。共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由 malloc 分配的一样。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。
多个进程在物理内存上有内存空间是共享的,多个进程在各自的逻辑地址空间写入数据,或读取数据,使用同一个空间,不需要数据的拷贝。
4.2两个进程间使用共享内存
步骤一:a进程创建共享内存并实现地址映射,b进程获取共享内存空间并实现地址映射。(或者b创建,a获取)
步骤二:a进程向共享内存中写入数据,b进程从共享内存中读取数据。
步骤三:a和b进程删除映射关系。
需要通过信号量实现a和b进程的同步控制
a写一次,b读一次,a不写,b不读,a不能连续写,b不能连续读
若使用一个信号量,因为a和b进程不确定哪个先运行,会导致无法满足上述条件。要完成上述情况,需要设置两个信号量s1(初值设置为1)和s2(初值设置为0)。要使a进程先运行,则需要对a进程先p(s1),执行后再v(s2);对b进程先p(s2),执行后再v(s1)。
如a进程不执行则无法v(s2),s2的值会一直为0,b进程将执行p(s2)操作会阻塞住,以至于a进程一定会先于b进程执行。b进程不执行无法v(s1),则s1的值会一直为0,a进程执行p(s1)操作会阻塞住,以至于b进程不执行完毕,a进程无法继续执行,便可以实现上述要求。
使用的函数:
shmget 创建、获取
shmat 映射
shmdt 断开映射
shmctl 删除共享内存
//sem.h
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/sem.h>
#define SEM1 0
#define SEM2 1
#define SEM_MAX 2
//信号量的个数
union semun
{
int val;
}
void sem_init();
void sem_p(int index);
void sem_v(int index);
void sem_destroy();
#include"sem.h"
//sem.c
static int semid;//静态全局变量,只限本文件
void sem_init()
{
semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);
if(semid==-1)
{
semid = semget((key_t)1234,SEM_MAX,0600);
if(semid==-1)
{
exit(1);
}
}
else
{
union semun a;
int arr[2]={1,0};
for(int i = 0;i<SEM_MAX;++i)
{
a.val=arr[i];
if(semctl(semid,i,SETVAL,a) == -1)
{
perror("semctl err");
exit(1);
}
}
}
}
void sem_p(int index)
{
if(index<0||index>=SEM_MAX)
{
exit(1);
}
struct sembuf buf;
buf.sem_num = index;
buf.sem_op = -1;//获取资源
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("semop err");
exit(1);
}
}
void sem_v(int index)
{
struct sembuf buf;
buf.sem_num = index;
buf.sem_op = 1;//释放资源
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("semop err");
exit(1);
}
}
void sem_destroy()
{
if(semctl(semid,0,IPC_RMID)==-1)
{
perror("semctl error");
exit(1);
}
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include"sem.h"
//a.c 向共享内存中写入数据
int main()
{
int shmid=shmget((key_t)1234,128,IPC_CREAT|0600));
if(-1==shmid)
{
exit(1);
}
//创建完成后映射物理内存
char* s = (char*)shmat(shmid,NULL,0);//共享内存的地址是内核帮忙分配的,返回共享内存的地址。0代表读写权限都有
if(s == (char*)-1)
{
exit(1);
}
//向s指向的共享内存空间写入数据
sem_init();
while(1)
{
printf("input:");
fgets(s,128,stdin);
sem_p(SEM1);
if(strncmp(s,"end",3) == 0)
{
sem_v(SEM2);//当输入end后,需要给SEM2 V操作,否则b进程无法p(SEM2)操作,将无法读取到end。将会p操作异常
break;
}
sem_v(SEM2);
}
sem_destroy();
shmdt(s);//断开映射
shmctl(shmid,IPC_RMID,NULL);//删除共享内存
exit(0);
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/shm.h>
#include<sem.h>
//b.c 从共享内存中读取数据
int main()
{
int shmid=shmget((key_t)1234,128,IPC_CREAT|0600));
if(-1==shmid)
{
exit(1);
}
//获取完成后映射物理内存
char* s = (char*)shmat(shmid,NULL,0);//共享内存的地址是内核帮忙分配的,返回共享内存的地址。0代表读写权限都有
if(s == (char*)-1)
{
exit(1);
}
//从s指向的共享内存空间读取数据
sem_init();
while(1)
{
sem_p(SEM2);
if(stcncmp(s,"end",3) == 0)
{
break;
}
sem_v(SEM1);
printf("read:%s\n",s);
sleep(1);
}
shmdt(s);
exit(0);
}
4.3 ipcs和ipcrm命令
ipcs -m:查看信号量
ipcrm -m + shmid(要删除共享内存的id): 删除该id的共享内存
五、消息队列
自身存在同步机制,实现时不需要使用信号量
5.1消息队列的实现
使用到的函数:
msgget() 创建或者获取一个消息队列
msgsnd() 发送一条消息
msgrcv() 接收一条消息
msgctl() 控制消息队列
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/msg.h>
typedef struct mess
{
long mtype;
char mtext[128];
}msgdata;
//a.c
int main()
{
//获取或创建消息队列
int msgid = msgget((key_t)1234,IPC_CREAT|0664);
if(msgid == -1)
{
exit(1);
}
//向消息队列发送消息,从键盘中获取
msgdata data;
data.mtype = 1;
strcpy(data.mtext,"hello1");
msgsnd(msgid,&data,128,0);
exit(0);
}
//b.c
int main()
{
//获取消息队列
int msgid = msgget((key_t)1234,IPC_CREAT|0664);
if(msgid == -1)
{
exit(1);
}
//从消息队列获取消息
msgdata data;
msgrcv(msgid,&data,128,1,0);//第三个参数1的意义是读取1号消息,如果把1改成0则表示读取任意消息。
//第四个参数0的意义在于,如果b进程获取消息,不存在1消息,则会阻塞。
printf("data type:%ld\ndata text:%s\n",data.mtype,data.mtext);
msgctl(msgid,IPC_RMID,0);
exit(0);
}