进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter Process Communication)
由上表我们可以看出IPC根据功能可以分三类:
通信:进程间的信息交互;
同步:进程或是线程之间的同步;
信号:信号是事件发生时对进程的通知机制。有时也称之为软件中断。打断了程序执行的正常流程。
按《UNIX环境高级编程(第二版)》P421所述,经典IPC为:管道(包括无名管道pipe和命名管道FIFO)、消息队列、共享内存、信号量;按传智播客视频所述:通信方式:(无名)管道(pipe) 、命名管道(FIFO)、内存映射(mmap)、信号(signal)、 本地套接字(domain);同步方式:信号量(semaphore)、文件锁;
一、管道
1、无名管道pipe
pipe函数:调用pipe 系统函数即可创建一个管道。
#include<unistd.h>
int pipe(int pipefd[2]);
返回值:
参数pipefd[2]:
要求文件描述符数组,无需open但需手动关闭,
规定:fd[0]用来读,fd[1]用于写,(类 PCB中的标准输入fd[0]、输出文件描述符fd[1],默认是打开的)
pipe半双工通信实例如下:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int fd[2];//读写两个文件描述符
int ret = pipe(fd);//创建管道
pid_t pid;
char buf[1024];
char *str = "hello pipe test\n";
if(ret == -1){
perror("pipe error");
exit(1);
}
pid = fork();//创建子进程,,由 fork 机制建立,,
//因此就只能作用于具有血缘关系的父子进程和兄弟进程之间的通信
// 规定数据的流动方向只能是:管道的写端流入,从读端流出(单向流动);
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){//子进程
close( fd[1] );//关闭标准输出,也即关闭写端
ret = read(fd[0], buf, sizeof(buf));
if(ret == 0)
printf("read end\n");
write(STDOUT_FILENO, buf, ret);
}else{//父进程
close( fd[0] );//关闭标准输入,也即关闭读端
write(fd[1], str, strlen(str));
}
return 0;
}
两个管道实现全双工进程通信:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
void child_rw_pipe(int readfd, int writefd){
char *message = "This is the message1, from child";
write(writefd, message, strlen(message)+1);
char message2[100];
read(readfd, message2, 100);
printf("Child read the message :%s \n", message2);
}
void parent_rw_pipe(int readfd, int writefd){
char *message = "This is the message2, from parent";
write(writefd, message, strlen(message)+1);
char message2[100];
read(readfd, message2, 100);
printf("parent read the message :%s \n", message2);
}
int main(void)
{
int pipe1[2], pipe2[2];
pid_t pid;
int stat_val;
printf("realize full-duplex communication.\n");
/*创建管道*/
if(pipe(pipe1)){
printf("Create pipe1 failed!\n");
exit(1);
}
if(pipe(pipe2)){
printf("Create pipe2 failed!\n");
exit(1);
}
pid = fork();// 管道是由 fork 机制建立,,
// 因此就只能作用于具有血缘关系的父子进程和兄弟进程之间的通信
if(pid < 0){
printf("Pid is error.\n");
exit(1);
}else if(pid == 0){ //子进程
close(pipe1[1]); // 关闭写
close(pipe2[0]); // 关闭读
child_rw_pipe(pipe1[0], pipe2[1]);
}else if(pid > 0){ // 父进程
close(pipe1[0]); // 关闭读
close(pipe2[1]); // 关闭写
parent_rw_pipe(pipe2[0], pipe1[1]);
wait(&stat_val);
exit(0);
}
return 0;
}
同样也可以由管道实现父子进程之间的同步:
标准VO库提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,调用fork产生一个子进程,关闭管道的不使用端,执行一个she11以运行命令,然后等待命令终止。
#include <stdio.h>
FILE* popen(congt char*cmdstring,const char*type);
返回值:
若成功则返回文件指针出错返回NULL
参数:
type是“r”,则文件指针连接到cmdstring的标准输出;
type是“w”,则文件指针连接到cmdstring的标准输入;
pclose函数:
#include <stdio.h>
int pclose(FILE*p);
2、FIFO(命名管道)
mkfifo函数:创建命名管道;
#include<sys/stat.h>
int mkfifo( const char* path, mode_t mode );
返回:
0:成功
-1:出错,并设定errno
参数:
path: 要用于FIFO文件的路径名,存在于文件系统中的;
mode: 文件权限,与open函数相同;
为写打开FIIO实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc < 2){ // 参数个数
printf("please input format is: ./a.out fifo_name\n");
exit(1);
}
// 创建有名管道
int ret = access(argv[1],F_OK);//判断文件是否存在
if(ret == -1){
int r = mkfifo(argv[1],0664); //FIFO文件的路径名, 权限;
if(r == -1){
perror("mkfifo error");
exit(1);
}
printf("有名管道%s创建成功\n",argv[1]);
}
// 打开文件
int fd = open(argv[1],O_WRONLY);
if(fd == -1){
perror("open error");
exit(1);
}
// 写入内容
char *p = "hello world!";
while(1){
sleep(1);
int len = write(fd,p,strlen(p) + 1);
}
close(fd);
return 0;
}
注:
使用 sleep()(存在于由父进程所执行的代码中),意在允许子进程先于父进程获得系统调度并使用 CPU,
以便在父进程继续运行之前完成自身任务并退出。要想确保这一结果, sleep()的这种用法并非万无一失,
可以用信号间的对同步实现
为读打开FIIO实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc < 2){
printf("please input format is: ./a.out fifo_name\n");
exit(1);
}
// 创建有名管道
int ret = access(argv[1],F_OK);//判断文件是否存在
if(ret == -1)
{
int r = mkfifo(argv[1],0664);
if(r == -1){
perror("mkfifo error");
exit(1);
}
printf("有名管道%s创建成功\n",argv[1]);
}
int fd = open(argv[1],O_WRONLY);
if(fd == -1){
perror("open error");
exit(1);
}
// 读取内容
char buf[512];
while(1){
int len = read(fd,p,strlen(p) + 1);
buf[len] = 0;
printf("buf = %s\n,len = %d",buf,len);
}
close(fd);
return 0;
}
二、消息队列
#include<sys/types.h>
#include<sys/msg.h>
int msgget(key_t key,int msgflg);
返回值:
找到则返回对象表示符;
如果没有找到匹配的队列并且在 msgflg 中指定了IPC_CREAT,那么就会创建一个新队列并返回该队列的标识符。出错返回 -1
参数:
key参数是一个键;
msgflg 参数是一个指定施加于新消息队列之上的权限或检查一个既有队列的权限的位掩码。
#include<sys/types.h>
#include<sys/msg.h>
int msgsnd(int msqid,const void *msgp, size_t msgsz,int msgflg);
返回值:成功返回 0,错误返回-1
参数:
第一个参数是消息队列标识符( msqid)
第二个参数 msgp 是结构指针,用于存放被发送或接收的消息
msgsz 参数指定了msgp字段中包含的字节数
msgflg为消息标志
msgrcv()系统调用:从消息队列中读取(以及删除)一条消息并将其内容复制进 msgp 指向的缓冲区中。
#include<sys/types.h>
#include<sys/msg.h>
ssizet msgrcv(int msqid,void *msgp,sizet maxmsgsz,long msgtyp,int msgflg);
返回值:成功返回数据长度,错误返回-1
参数
msqid:消息队列的标识码。
*msgp:指向消息缓冲区的指针。
msgsz:消息的长短
参数 msgflg:标志位
#include<sys/types.h>/*For portability*/
#include<sys/msg.h>
int msgctl(int msgid,int cmd,struct msqid_ds *buyf);
实现消息队列代码如下:
msg_send.c
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
#include<errno.h>
#define MAX_TEXT 512
struct msg_st{
long int msg_type;
char text[MAX_TEXT];
};
int main(void)
{
int running = 1;
struct msg_st data;
char buffer[BUFSIZ];
int msgid = -1;
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);//建立消息队列
if(msgid==-1){
fprintf(stderr, "msgget failed with error:sd\n", errno);
exit(EXIT FATLURE);
}
while(running)//向消息队列中写消息,直到写入结束标志:end
{
printf("Enter text:");
fgets(buffer, BUFSIZ, stdin);//输入数据
data.msg_type = 1;
strcpy(data.text, buffer);
if( msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1 ){//向队列发送数据
fprintf(stderr,"magsnd failed\n");
exit(EXIT_FAILURE);
}
if(strncmp(buffer,"end",3)==0)
running = 0;//输入end结束输入
sleep(1);
}
exit(EXIT_SUCCESS);
}
msg_receive.c
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
#include<errno.h>
struct msg_st{
long int msg_type;
char text[BUFSIZ];
};
int main(void)
{
int running = 1;
struct msg_st data;
int msgid = -1;
long int msgtype = 0;
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);//建立消息队列
if(msgid==-1){
fprintf(stderr, "msgget failed with error:sd\n", errno);
exit(EXIT FATLURE);
}
while(running)//向消息队列中获取消息,直到读到结束标志:end
{
if( msgrcv(msgid, (void*)&data, BUFSIZ, 0) == -1 ){//从消息队列中读取数据
fprintf(stderr,"magsnd failed\n");
exit(EXIT_FAILURE);
}
printf("Your wrote: %s\n", data.text );
if(strncmp(data.text,"end",3) ==0 )
running = 0;//输入end结束输入
}
if( msgctl(msgid, IPC_RMID, 0) == -1){ // 删除消息队列
fprintf(stderr, "msgctl error:sd\n");
exit(EXIT FATLURE);
}
exit(EXIT_SUCCESS);
}
【System V IPC 补充】
System V IPC key 是一个整数值,其数据类型为 key_t。 IPC get 调用将一个 key 转换成相应的整数 IPC 标识符。这些调用能够确保如果创建的是一个新 IPC 对象,那么对象能够得到一个唯一的标识符,如果指定了一个既有对象的 key,那么总是会取得该对象的同样的标识符。
如何产生唯一的 key 呢?
1、在创建 IPC 对象的 get 调用中将 IPC_PRIVATE 常量作为 key 的值, 这样就会导致每个调用都会创建一个全新的 IPC 对象,从而确保每个对象都拥有一个唯一的 key
2、使用 ftok( ) 函数生成一个(接近唯一) key
ipcs 命令
ipcs 命令是 System V IPC 领域中类似于 ls 文件命令的命令。 使用 ipcs 能够获取系统上 IPC 对象的信息。在默认情况下, ipcs 会显示出所有对象。
ipcrm 命令:
ipcrm 命令是 System V IPC 领域中类似于 rm 文件命令的命令。用于删除一个 IPC 对象。
ipcrm -X key
incrm -x id
在上面给出的命令中既可以将一个 IPC 对象的 key 指定为参数 key,也可以将一个 IPC 对象的标识符指定为参数 id
并且使用小写的 x 替换其大写形式或使用小写的 q(用于消息队列)或 s(用于信号量)或 m(用于共享内存)
【System V IPC 补充结束】
三、信号量
四、共享内存
4.1、内存映射分类


# include<sys/mman.h>
void * mmap(void * addr, sizet length, int prot, int flags, int fd, off_t ofset);
返回:
成功,返回创建映射区首地址;
失败,返回MAP_FAILED 宏
参数:
addr: 建立映射区的首地址,为NULL,由Linux内核指定映射地址;非NULL,则从指定位置开始;
length:欲创建映射区的大小
prot: 映射区权限 PROT_READ、PROT_WRITE、PROT_READ | PROT_WRITE ;
(可读可写可执行......)
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匪名映射区)
MAP_SHARED:所做的修改其他进程不可见,会将映射区所做的操作反映到磁盘上;
MAP_PRIVATE:所做的修改其他进程不可见,映射区所做的修改不会反映到物理设备,
MAP_ANONYMOUS (或MAP_ANON宏:它是Linux操作系统特有的宏):匿名映射的标志;
若要使用匿名映射,flags = MAP_ANON 且 fd = -1
fd:用来建立映射区的文件描述符
offset:映射文件的相对于映射内存的偏移量(k 的整数倍)
mmap( )调用操作页。addr和offset参数都必须按页大小(默认4KB)对齐。
也就是说,它们必须是页大小的整数倍。
munmap( )系统调用:执行与 mmap()相反的操作,即从调用进程的虚拟地址空间中删除一个映射
# include<sys/mman.h>
int munmap(void * addr, size t length);
返回:
成功:0;
失败:-1;
参数
addr: mmap的返回值
length: 映射区大小
mmap和munmap注意事项:
1.映射区建立过程中隐含一次读操作
2.MAP_SHARED时映射区权限 <= 打开文件权限
3.映射区建立成功,文件即可关闭.
4.大小为0的文件无法创建映射区
5.munmap参数应与mmap返回值严格对应
6.偏移位置必须为4K的整数倍
7.mmap返回值判断不能省
5.2 文件映射
5.2.1、私有文件映射
5.2.2、共享文件映射
-
fork后父子进程由于 共享文件描述符,可以实现父子进程通信,已由前述实现;
-
通过创建映射区, f ork后父子进程将共享映射区,实现父子进程通信;
-
非血缘关系间的进程使用同一文件创建映射区通信实例:
2、使用共享文件映射的内存映射IO
存储映射I/O(Memory-mappedI/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,相应字节就自动地写入文件。这样就可以在不使用read和write的情况下执行I/O,也即代替代替了read和write。
5.3 匿名映射
使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。可以直接使用匿名映射来代替,其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。
匿名映射的创建方法:
在调用mmap系统调用的时候设置对应的flags标志位和文件描述符fd;如:
int *p = mmap(NULL, 4, PROT_READIPROT_WRITE, MAP_SHAREDIMAP_ANONYMOUS, -1, 0);
// “4”为指定映射区的大小;
注:在使用匿名映射的时候,flags = MAP_SHAREDIMAP_ANONYMOUS, 且 fd = -1;
而在类Unix系统中无此宏定义,可用以下步骤建立(依赖设备目录下的zero文件)
fd = open("/dev/zero”,O_RDWR);
p = mmap(NULL, rsize, PROT_READIPROT_WRITE, MIMAP_SHARED, fd, 0);