单进程无法使用并发的能力,更无法实现多进程协同工作,所以进程间通信时一种手段,目的是要实现多进程协同。
进程运行具有独立性,采用虚拟地址空间加页表的方式来保证进程运行的独立性(包含进程内核数据结构和进程的代码数据),所以进程通信的成本会比较高。
进程间通信简称IPC,进程间通信的前提是让不同的进程看到同一块“内存”。这个“内存”资源不能属于任何一个进程,而是共享的。
标准在使用者看来,都是接口上具有一定的规律。
管道
匿名管道pipe
命名管道
System V进程间通信
System V 共享内存
System V 信号量
System V 消息队列
POSIX进程间通信
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道是单向传输内容的,且管道传输的都是资源是“数据”。
一般在linux命令中|(管道)之前的命令会输出大量的结果,|(管道)之后的命令一般就是带有条件的,只将|前满足条件的结果显示出来。举例:who | wc -l 就是把前一个命令的结果当成后一个命令的输入。结合本例就是先显示所有用户,然后再用wc命令在who的结果中列出查找用户。
当父进程创建子进程时,会把pcb以及这个进程所对应的文件描述符表等内容于进程相关的都会拷贝一份,而与文件相关的不变。文件描述符表指向的文件指针是没变的,父子进程的文件描述符表指向相同文件,父进程和子进程就可以看到同一份公共资源称为管道文件,简称为管道。父子进程再各自关闭自己不需要的文件描述符,就可以做到父进程进行写入,子进程进行读取,从而进行进程之间通信。
管道本质
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
代码演示
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//1.创建管道
int pipefd[2] = {0}; //pipefd[0]:读端, pipefd[1]:写端
int n = pipe(pipefd); //pipefd是输出型参数,期望通过调用它获得被打开的文件fd
assert(n != -1);
(void)n; //防止release版本下报警
#ifdef DEBUG
cout<<"pipefd[0]: "<<pipefd[0]<<endl; //3
cout<<"pipefd[1]: "<<pipefd[1]<<endl; //4
#endif
//2.创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程
//3.构建单项通信的信道,假设父进程写入,子进程读取
//3.1 关闭子进程不需要的fd
close(pipefd[1]);
char buffer[1024];
while(1)
{
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
cout<<"child get a message["<<getpid()<<"] Father#"<<buffer<<endl;
}
}
close(pipefd[0]);
exit(0);
}
else
{
//父进程
//3.构建单项通信的信道,假设父进程写入,子进程读取
//3.1 关闭该进程不需要的fd
close(pipefd[0]);
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send_buffer[1024];
while (1)
{
//3.2构建一个变化的字符串
snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
//3.3写入/发送消息
write(pipefd[1],send_buffer,strlen(send_buffer));
//3.4故意sleep
sleep(1);
}
}
pid_t ret = waitpid(id,nullptr,0); //等待子进程
assert(ret > 0);
(void) ret;
close(pipefd[1]);
return 0;
}
为什么不能定义全局的buffer来进行通信?
因为写时拷贝的存在,无法更改通信。
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
如下图:
管道的特点:
管道通信的四种情况:
管道应用的一个限制就是只能在具有共同祖先的进程间通信。如果想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道,命名管道是一种特殊类型的文件。
在操作系统内,多个进程打开同一个文件时,OS不会创建新的struct file并且加载数据,直接把文件的file指针告诉进程的文件描述表即可。OS创建了管道文件,此文件可以被打开,但是不会将内容数据进行刷新到磁盘,且该文件一定在系统路径中,因为路径具有唯一性,双方进程可以通过看见同一份资源。
创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
read.cpp
include<iostream>
#include
#include
#include
#include
int main()
{
int fd=open("./pipe",O_RDONLY);
if(fd<0)
{
std::cout<<"打开管道文件失败!"<<std::endl;
return 1;
}
char buf[64]={0};
read(fd,buf,sizeof(buf));
std::cout<<buf<<std::endl;
return 0;
}
write.cpp
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/stat.h>
4 #include<sys/types.h>
5 #include<fcntl.h>
6 int main()
7 {
8 int fd=open("./pipe",O_WRONLY);
9 if(fd<0)
10 {
11 std::cout<<"打开管道文件失败!"<<std::endl;
12 return 1;
13 }
14 char str[]="bit education!";
15 write(fd,str,sizeof(str)-1);
16 return 0;
17 }
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 int main()
5 {
6 mkfifo("pipe",0664);
7 return 0;
8 }
匿名管道与命名管道的区别
命名管道的打开规则
如果当前打开操作是为读而打开FIFO时:
如果当前打开操作是为写而打开FIFO时:
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms /
int shm_segsz; / size of segment (bytes) /
__kernel_time_t shm_atime; / last attach time / 最后关联时间
__kernel_time_t shm_dtime; / last detach time / 最后去除关联时间
__kernel_time_t shm_ctime; / last change time /属性最后修改时间
__kernel_ipc_pid_t shm_cpid; / pid of creator /
__kernel_ipc_pid_t shm_lpid; / pid of last operator /
unsigned short shm_nattch; / no. of current attaches /
unsigned short shm_unused; / compatibility */
void shm_unused2; / ditto - used by DIPC */
void shm_unused3; / unused */
};
共享内存的建立
共享内存的提供者是操作系统,它是操作系统专门为了进程之间通信而设计的。系统中会存在着大量的共享内存,操作系统先描述再组织来管理共享内存,共享内存 = 共享内存块 + 对应的共享内存的内核数据结构。
共享内存函数
shmget函数
头文件: <sys/ipc.h> <sys/shm.h>
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
在命令行上,可以使用ipcs -m 查看共享内存,如果要删除共享内存使用ipcrm -m shmid就可以。
common.h
1 #pragma once //防止头文件重复包含
2 #ifndef __ADD_H_
3 #define __ADD_H__
4 #endif
5
6 #define PATHNAME "/tmp"
7 #define PROJ_ID 0x6688
8 #define SIZE 4096
案例:
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include"comm.h"
4 #include<sys/ipc.h>
5 #include<sys/shm.h>
6 int main()
7 {
8 key_t k=ftok(PATHNAME,PROJ_ID);
9 printf("key=%d\n",k);
11 int shmid=shmget(k,SIZE,IPC_CREAT|IPC_EXCL);
12 if(shmid<0)
13 {
14 printf("创建共享内存失败!\n");
15 }
16 return 0;
17 }
ftok函数可以标识操作系统上ipc资源的唯一性,使不同的进程要看到同一份资源,proj_id是可以根据自己的约定,随意设置。
shmat函数
功能:将共享内存段连接到进程地址空间
原型:
void shmat(int shmid, const void shmaddr, int shmflg);
参数:
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include"comm.h"
5 #include<sys/ipc.h>
6 #include<sys/shm.h>
7 int main()
8 {
9 key_t k=ftok(PATHNAME,PROJ_ID);
10 printf("key=%p\n",k);
12 int shmid=shmget(k,SIZE,IPC_CREAT|0664);
13 if(shmid<0)
14 {
15 printf("创建共享内存失败!\n");
16 return 0;
17 }
18 char* addr=(char*)shmat(shmid,NULL,0);
19 if(addr==NULL)
20 {
21 printf("关联失败!\n");
22 return 0;
23 }
24 while(1)
25 {
26 printf("%s\n",(char*)addr);
27 sleep(1);
28 }
29 //shmdt(addr);
30 shmctl(shmid,IPC_RMID,NULL);
31 return 0;
32 }
shmdt函数
功能:将共享内存段与当前进程脱离
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
功能:用于控制共享内存
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include"comm.h"
5 #include<sys/ipc.h>
6 #include<sys/shm.h>
7 int main()
8 {
9 key_t k=ftok(PATHNAME,PROJ_ID);
10 printf("key=%p\n",k);
12 int shmid=shmget(k,SIZE,IPC_CREAT|0664);
13 if(shmid<0)
14 {
15 printf("创建共享内存失败!\n");
16 return 0;
17 }
18 char* addr=(char*)shmat(shmid,NULL,0);
19 if(addr==NULL)
20 {
21 printf("关联失败!\n");
22 return 0;
23 }
24 while(1)
25 {
26 printf("%s\n",(char*)addr);
27 sleep(1);
28 }
29 //shmdt(addr);
30 shmctl(shmid,IPC_RMID,NULL);
31 return 0;
32 }
为了让进程间进行通信,首先要让不同的进程看到同一份资源。多个进程/执行流看到的公共的一份资源称为临界资源,并且把自己的进程放到临界资源的代码叫做临界区。所以,多个执行流互相运行时候互相干扰,主要是用户不加保护的访问了同样的临界资源,而在非临界区的多个执行流互相不影响。为了更好的进行临界去的保护,可以让多执行流在任何时刻,都只有一个进程进入临界区,这个就叫做互斥。
信号量主要用于同步和互斥的。
信号量本质上是一个计数器,每一个进程想要进入临界资源,访问临界资源的数据,不能让进程直接去使用临界资源,要先申请信号量。申请信号量的本质就是让信号量计数器–,释放信号量是让信号量计数器++,主动申请信号量成功,临界资源内部一定给你预留了你想要的资源,申请信号量的本质其实就是对于临界资源的一种预定机制。