文章目录
1.进程间通信的目的
- 数据传输,一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送信息,通知发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Dubug进程),此时控制进程希望能够拦截另一个进程的所有陷入与异常,并能够及时知道它的状态改变。
2.进程间通信的本质
不同的进程共同管理一份共同的资源(文件)
3.进程通信的不同方式
-
管道
-
System V IPC
a.消息队列
b.共享内存
c.信号量 -
POSIX IPC
a.消息队列
b.共享内存
c.信号量
d.互斥量
e.条件变量
f.读写锁
4.管道
1)管道基本理解
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
,它是一块文件或共享区,一个往里面写数据,一个拿数据,是UNIX中最古老的进程间通信的形式。其实通俗来说,就像是现实中的水管道,一方将水接入管道,一方从管道接出使用水,所以不难理解
,管道是单向的,先进先出的,它将一个程序的输入与另一个程序的输出连接起来,数据被一个进程读取后,该数据便被删除。
2)管道的分类
- 匿名管道: 只能完成具有血缘关系间的进程(或者具有公共祖先的进程)。
- 命名管道:允许同一个进程中任意两个进程的通信。
3)匿名管道(PIPE)
- 创建匿名管道函数pipe
#include <unistd.h>
功能:创建一无名管道
原型:`int pipe(int fd[2])`
参数:
fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端。结合Linux一切皆文件可以理解为管道是一个文件,用户在这个文件读写数据(读写内核缓冲区)。
返回值:成功返回0,失败返回错误代码。
我们创建一个管道,从键盘读取数据,写入管道,读取管道,写到屏幕。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#define NUM 128
int main()
{
int fds[2];//文件描述符数组,fd[0]表示读端,fd[1]表示写端。
int p = pipe(fds);
//创建管道
if( p < 0 ){
printf("创建管道失败\n");
exit(1);
}
//从键盘输入读取数据
while(1){
char buf[NUM] = {0};
ssize_t s1 = read(0,buf,sizeof(buf)-1);
if(s1<0){
perror("read");
exit(1);
}
buf[s1] = 0;
//写进管道
write(fds[1],buf,strlen(buf));
//从管道中读取
ssize_t s2 =read(fds[0],buf,sizeof(buf)-1);
buf[s2] = 0;
//输出到屏幕上
write(1,buf,strlen(buf));
}
return 0;
}
结果演示:
-
站在文件描述符号-深度理解管道
-
父进程调用pipe()开辟一个管道,使用open得到两个文件描述符号,一个指向管道的读端,一个指向管道的写端。
-
父进程调用fork()创建子进程,得到的子进程也有两个文件描述符指向这个管道的两端。
-
父进程关闭写端,子进程关闭读端,子进程往管道中写入数据,父进程从中读出,这样就实现了进程间通信。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fds[2];
int p = pipe(fds);
if(p < 0)
{
perror("pipe");
return 1;
}
pid_t id =fork();
if(id == 0)//子进程写
{
close(fds[0]);
write(fds[1],"haha",4);
close(fds[1]);
exit(EXIT_SUCCESS);
}
else if (id > 0){
//父进程
close(fds[1]);
char buf[12]={0};
read(fds[0],buf,12);
printf("buf=%s\n",buf);
}
return 0;
}
-
匿名管道的四种情况
a.当管道的写端关闭后,这时读取数据的进程把管道中的数据读完后,read就返回0,相当于读到文件末尾。
b.写端没有关闭当读取数据进程的速度大于写数据进程的速度时,在管道内的数据被读取完后,read会阻塞,直到管道中有新的数据。
c.当管道的读端被关闭后,若还写数据,这时进程会收到SIGPIPE信号,导致进程异常终止。
d.当管道读端没有关闭,写的速度大于读的速度,在管道写满后,write被阻塞直到管道有空位置。 -
匿名管道的五大特点
1.只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父,子进程之间就可应用于该管道。
2.管道提供流式服务(面向字节流)
3.一般而言,进程退出,管道释放,所以管道的生命周期随进程。
4.一般而言,内核会对管道操作进行同步与互斥。
5.管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。
4)命名管道(FIFO)
- 概念:命名管道不同于匿名管道之处在于命名管道是一个设备文件,它真真正正的存在于硬件之上,存在于文件系统中。而匿名管道存在于内存或者内核中,它对文件系统不可见,也因为这样,命名管道可以完成任意两个进程间的通信。命名管道的创建有一个路径名path与之关联,以命名管道文件的形式存储在文件系统中,所以只要访问该路径,就能够通过命名管道互相通信。FIFO也是先进先出的规则。
- 创建管道:
a.在shell命令上可以通过mkfifo 文件名
创建命名管道文件
b.在程序内通过mkfifo函数创建命名管道文件
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *filename,mode_t mode)
- 匿名管道与命名管道的区别
a.匿名管道由pipe函数创建并打开
b.命名管道由mkfifo函数创建,打开open
c.FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建于打开的方式不同,一旦这些工作完成,它们具有相同的语义
d. 对于命名管道文件系统中的路径名是全局的,各个进程都可以访问,因此可以用文件系统的路径名来标识一个IPC通信。
e.命名管道是真实存在于硬盘上的,匿名管道是存在于内存中的特殊文件。 - 使用命名管道完成server&client通信
a.makefile文件
.PHONY:all
all:client server
client:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -rf server client
b.server.c文件
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
if(mkfifo("mypipe",0644)<0){
perror("mkfifo");
exit(EXIT_FAILURE);
}
int fd = open("mypipe",O_RDONLY);
if(fd <0 ){
perror("open");
}
char buf[1024];
while(1){
buf[0] = 0;
printf("Please wait......\n");
ssize_t s =read(fd,buf,sizeof(buf)-1);
if(s > 0){
buf[s-1] = 0;
printf("client say# %s\n",buf);
}else if(s == 0){
printf("client quit\n");
exit(EXIT_SUCCESS);
}
}
close(fd);
return 0;
}
c.client.c文件
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int fd = open("mypipe",O_WRONLY);
char buf[1024];
while(1){
buf[0]=0;
printf("Please Enter# ");
fflush(stdout);
ssize_t s =read(0,buf,sizeof(buf)-1);
if(s >0 ){
buf[s] = 0;
write(fd,buf,strlen(buf));
}else if(s <= 0)
{
perror("read");
exit(1);
}
}
close(fd);
return 0;
}
代码执行结果:(先打开server端口,再打开一个client端口)
5.消息队列
该内容在我另一篇博客:
https://blog.csdn.net/weixin_41892460/article/details/83346487
6.共享内存
1)基本概念
共享内存是最快的IPC形式,一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,也就是说进程不再通过执行内核的系统调用来传递彼此的数据,所以共享内存没有互斥与同步机制(用户自主完成)。生命周期随内核。
2)共享内存示意图
3)共享内存数据结构
4)共享内存函数
a.shmget函数
功能:用来创建共享内存
原型:int shmget(key_t key,size_t size,int shmflg);
参数:
key:这个共享内存段名字
size:共享内存大小 (OS将内存分为页size是页的整数倍)
shmflg:由9个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
返回值:成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1
b.shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1
说明:
shmaddr为NULL,核⼼⾃动选择⼀个地址
shmaddr不为NULL且shmflg⽆SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃动向下调整为SHMLBA的整数倍。公式:s
hmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表⽰连接操作⽤来只读共享内存
c.shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
d.shmctl函数
功能:⽤于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
ICP_RMID:删除共享内存段
5)实例代码
makefile文件
.PHONY:all
all:server client
client:client.c comm.c
gcc -o $@ $^
server:server.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -rf client server
comm.h文件
#ifndef __COMM_H__
#define __COMM_H__
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
int creatShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#endif
comm.c文件
#include"comm.h"
static int commShm(int size,int flags)
{
key_t _key = ftok(PATHNAME,PROJ_ID);
if(_key < 0){
perror("ftok");
return -1;
}
int shmid = 0;
if((shmid = shmget(_key,size,flags)) < 0){
perror("shmget");
return -3;
}
return shmid;
}
int destroyShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,NULL)< 0){
perror("shmctl");
return -1212;
}
return 0;
}
int creatShm(int size)
{
return commShm(size,IPC_CREAT|IPC_EXCL|0666);
}
int getShm(int size)
{
return commShm(size,IPC_CREAT);
}
server.c文件
#include"comm.h"
int main()
{
int shmid = creatShm(4096);
char *addr =shmat(shmid,NULL,0);
sleep(2);
int i =0;
while(i++<26){
printf("client# %s\n",addr);
sleep(1);
}
shmdt(addr);
sleep(2);
destroyShm(shmid);
return 0;
}
client.c文件
#include"comm.h"
int main()
{
int shmid =getShm(4096);
sleep(1);
char *addr =shmat(shmid,NULL,0);
sleep(2);
int i = 0 ;
while(i<26)
{
addr[i] = 'A' +i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
6)ipcrm -m 与ipcs -m表示删除或查看共享内存
nattch表示有n个进程与该共享内存建议映射关系(连接)。
7.信号量
1)基本概念
信号量机制是一种功能较强的机制,可以用来解决互斥与同步问题,它只能被两个标准的原语wait(S)和 signal(S)来访问,也可以记为“P操作”和“V操作”。
临界资源:两个进程看到的一份公共资源称为“临界资源”,也就是说这些资源一次只允许一个进程使用,各个进程中访问“临界资源“的代码称为”临界区“。
互斥:每个进程通过临界区访问临界资源,每个进程只能独占式、排他式的访问临界资源。
同步:在互斥的基础上,在一个进程访问临界资源时,不能立即访问公共资源需要重新排队,让进程按照顺序公平的访问资源(可用于解决饥饿问题)
原子性:一件事情要么做,要么不做。
挂起:一个进程等待某个条件就被称为挂起,条件来了就可以挂起。
二元信号量:(0/1)可以实现互斥,P->申请++v->释放信号–。
二元信号量也被称为挂起等待锁,因为锁的存在不怕进程切换,可以理解为进程抱着锁出去,因为该进程没进行完(原子性)。
信号量:
本质上是一个计数器,它是用来描述资源数目的引入是为了保护临界资源而他本身也是临界资源。(因为要被两个进程同时看到。)
数据在内存中–》cpu才能执行-》再写回原内存。
2)函数了解
a.semget函数
功能:⽤来创建和访问⼀个信号量集
原型
int semget(key_t key, int nsems, int semflg);
参数
key: 信号集的名字
nsems:信号集中信号量的个数
semflg: 由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
返回值:成功返回⼀个⾮负整数,即该信号集的标识码;失败返回-1
b.shmctl函数
功能:⽤于控制信号量集
原型
int semctl(int semid, int semnum, int cmd, ...);
参数
semid:由semget返回的信号集标识码
semnum:信号集中信号量的序号
cmd:将要采取的动作(有三个可取值)
最后⼀个参数根据命令不同⽽不同
返回值:成功返回0;失败返回-1
SETVAL:设置信号量集中的信号量的计数值
GETVAL:获取信号量集中的信号量的计数值
IPC_STAT:把semid_ds结构中的数据设置为信号集的当前关联值
IPC_SET:在进程有足够权限的前提下,把信号集的当前关联值设置为semid_ds数据结构中给出的值
IPC_RMID:删除信号集
c.semop函数
功能:⽤来创建和访问⼀个信号量集
原型
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数
semid:是该信号量的标识码,也就是semget函数的返回值
sops:是个指向⼀个结构数值的指针
nsops:信号量的个数
返回值:成功返回0;失败返回-1
说明:
sembuf结构体:
struct sembuf {
short sem_num;
short sem_op;
short sem_flg;
};
sem_num是信号量的编号。
sem_op是信号量⼀次PV操作时加减的数值,⼀般只会⽤到两个值:
⼀个是“-1”,也就是P操作,等待信号量变得可⽤;
另⼀个是“+1”,也就是我们的V操作,发出信号量已经变得可⽤
sem_flag的两个取值是IPC_NOWAIT或SEM_UNDO
3)二元信号量实例代码
a.makefile
test:test.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test
b.comm.h
#ifndef _COMM_H__
#define _COMM_H__
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int createSemSet(int nums);
int initSem(int semid,int nums, int initVal);
int getSemset(int nums);
int P(int semid,int who);
int T(int semid,int who);
int destroySemSet(int semid);
#endif
c.comm.c
#include "comm.h"
static int commSemSet(int nums,int flags)
{
key_t _key = ftok(PATHNAME,PROJ_ID);
if(_key < 0){
perror("ftok");
return -1;
}
int semid = semget(_key,nums,flags);
if(semid < 0)
{
perror("semget");
return -2;
}
return semid;
}
int createSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}
int getSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT);
}
int initSem(int semid,int nums,int initVal)
{
union semun _un;
_un.val = initVal;
if(semctl(semid,nums,SETVAL,_un)<0){
perror("semctl");
return -1;
}
return 0;
}
static int commPV(int semid,int who,int op)
{
struct sembuf _sf;
_sf.sem_num = who;
_sf.sem_op = op;
_sf.sem_flg = 0;
if(semop(semid,&_sf,1)<0){
perror("semop");
return -1;
}
return 0;
}
int P(int semid,int who)
{
return commPV(semid,who,-1);
}
int V(int semid,int who)
{
return commPV(semid,who,1);
}
int destroySemSet(int semid)
{
if(semctl(semid,0,IPC_RMID)<0)
{
perror("semctl");
return -1;
}
}
d.test.c
#include"comm.h"
int main()
{
int semid = createSemSet(1);
initSem(semid,0,1);
pid_t id = fork();
if(id == 0){
//child
int _semid = getSemSet(0);
while(1){
P(_semid,0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(321456);
V(_semid,0);
}
}
else{
//father
while(1){
P(semid,0);
printf("B");
fflush(stdout);
usleep(223456);
printf("B ");
fflush(stdout);
usleep(121456);
V(semid,0);
}
wait(NULL);
}
destorySemSet(semid);
return 0;
}
e.执行结果
所有的AB都成对出现,不会出现交叉的情况,如果去掉PV则无法成对打印。