概述
一、无名管道、有名管道与进程间通信:
1、IPC–进程间通信与管道基本概念:
(1)、IPC(进程间通信):
所谓IPC就是两个或者多个进程之间的数据交互(在不能直接进行信息交互的两个进程间增加一个“交互媒介”以达到信息交互的目的)。为什么不能直接交互?因为我们知道在应用程序执行时(即进程运行时),其占有的用户空间只有0~3G,而用户空间不共享,不共享就无法传递信息;内核空间共享,所以要实现两个进程之间的信息交互即通信,就必须通过内核空间。
IPC的方法:
①文件;
②信号(signal);
③管道;
④共享内存;
⑤消息队列;
⑥信号量集(semaphore)(与信号无关);
⑦套接字socket
今天我们就“文件–内存”与“管道”总结一下,无论是有名管道还是无名管道、也不管是消息队列还是信号量集、套接字,其本质都是在内核中实现的,而我们只是在调用一个内核提供的接口或方法。
(2)、管道基本特性:
管道文件只是媒介,只是数据的中转站,只有读写双方均就绪时才畅通,只有一方就绪时处于阻塞状态,其大小始终为0,其基本模型 如图所示:
两个进程分别持有内核管理的管道的读端与写端的权限,并且管道是单向的,或者说是单双工的。
简单测试(打开两个终端测试,file.pipe为一个管道文件关于其创建,之后会提到):
测试①
第一步:echo message > file.pipe(输入重定向到管道中,处于阻塞)
第二步:cat file.pipe(管道畅通,cat进程输出message)两个步骤是两个shell创建的子进程进行通信。
如果不执行第二步(不敲回车),则shell一直处于后台,echo写端则处于阻塞状态,当第二步执行时(敲回车以后),通过管道在两个shell的子进程之间(echo和cat两个进程)传递信息。
结果如图所示:
测试②
第一步:cat file.pipe(处于阻塞)
第二步:echo message > file.pipe(管道畅通,cat进程输出message)
与第一个测试相似,只不过测试①是开始读端未打开,写端处于阻塞状态。测试②是写端未打开,读端处于阻塞状态。
结果如图所示:
2、pipe无名管道与FIFO有名管道:
无名管道:由内核创建,只用于fork()创建的父子进程之间的通信;
有名管道:由程序员建立管道文件,用于进程间通信(管道文件是程序员创建,但是管道依旧是内核创建并且管理),前面测试的便是有名管道。
(1)、pipe无名管道:
#include<unistd.h>
int pipe(int filedes[2]);
//创建无名管道,由内核维护,且无名管道只能用于fork创建的父子进程之间通信(作用于有血缘关系的进程间通信)。
pipe函数的参数是一个整型数组,该数组包含两个文件描述符:一个写描述符fd[0],一个读描述符fd[1]。如图所示为单进程的管道信息传递:
pipe管道使用注意的四种情况:
①当写端关闭,读端读完管道里内容时read返回0,相当与读到文件末尾;
②写端未关闭,但是写端暂无数据,读端读完管道中数据后便阻塞;
③读端关闭,写管道的进程会收到一个SIGPIPE信号,写进程终止;
④读端未读管道数据,当写端写满数据后,再次写会阻塞。
对于fork()创建的父子进程之间的文件描述符与管道的关系如图所示:
代码实现无名管道进程间通信:
/*父写子读,关闭父进程的fd[1]和子进程的fd[0]*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
void sys_err(const char * ptr){
perror(ptr);
exit(EXIT_FAILURE);
}
int main(void)
{
int fd[2]; //fd[0] 读端,fd[1] 写端
char str[1024] = "hello world!";
char buf[1024];
if(pipe(fd) < 0)/*无名管道创建失败判断*/
sys_err("pipe");
pid_t pid = fork();
if(pid < 0)
sys_err("fork");
if(pid > 0){
close(fd[0]);//父进程里,关闭父进程的读端
sleep(5);
write(fd[1], str, strlen(str));
close(fd[1]);//写完之后关闭写端
wait(NULL);//等待子进程结束回收子进程PCB资源,防止产生僵尸进程
}
else if(pid == 0){
int len, flags;
close(fd[1]);//子进程里,关闭子进程写端
flags = fcntl(fd[0], F_GETFL);
flags |= O_NONBLOCK;//默认是阻塞读取,改为不阻塞
fcntl(fd[0], F_SETFL, flags);
tryagain:
len = read(fd[0], buf, sizeof(buf));
if(len == -1){//等于-1,说明父进程没有向管道中写数组,子进程由于不阻塞,因此循环执行
if(errno == EAGAIN){//EAGAIN信号表示读取返回-1的原因是没有数据可读,要求再试一次,那么久打印try again后再试一次
write(STDOUT_FILENO, "try againn", 10);
sleep(1);
goto tryagain;
}else//如果不是因为没有数据可读,就是读取出错了,就不同在循环读取了
sys_err("read");
}
write(STDOUT_FILENO, buf, len);//将读取到的内容输入到标准输出上
close(fd[0]);//然后关闭子进程的读端
}
return 0;
}
测试结果如图所示:
由于父进程睡眠5秒,而子进程循环一次睡眠一秒,所以在打印出hello world!之前会打印五次tryagain。最终我们看到,父进程可以通过无名管道给子进程发送信息。并且阻塞与否可以通过fcntl函数修改。
(2)、FIFO有名管道:
管道文件的创建:管道是通过管道文件(媒介)进行进程间信息交互的,管道文件与普通文件是有区别的,通过mkfifo(make first in first out)或者mkfifo()创建管道文件。其他方式是无法创建管道文件的,管道文件后缀是”.pipe”(类型为p)即使是touch file.pipe也不行。我们知道后缀名是无关紧要的,但是一定要使用mkpipe或者mkpipe()创建管道文件。
对于有名管道,必须先有管道文件才能进行通信。所以我们在程序中创建/使用管道文件时必须先用S_ISFIFO()判断某个文件是不是管道文件。该宏函数是用来判断stat()函数获取的struct stat{}结构体中的mode_t mode参数存储的文件类型。关于struct stat{}结构体以及stat()函数的基本形式,可参考: Linux&C编程之Linux系统命令“ls -l”的简单实现
S_ISFIFO(m) /*判断是否是管道文件,m即为mode参数*/
有名管道本质:无“血缘关系”的两个进程通过name.pipe管道文件找到内核中的pipe管道,进而实现无血缘关系的管道进程间通信。
有名管道的图解如下所示:
代码实现有名管道进程间通信:
/****
头文件省略,sys_err函数省略
fifo_w.c有名管道写端
****/
int main(int argc, char *argv[])//传递管道文件
{
int fd;
char buf[1024] = "hello worldn";
if(argc < 2){
printf("./fifo_w name.pipen");
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_WRONLY);
if(fd < 0)
sys_err("open");
write(fd, buf, strlen(buf));
close(fd);
return 0;
}
/***
头文件省略,sys_err函数省略
fifo_r.c有名管道读端
***/
int main(int argc, char *argv[])
{
int fd, len;
char buf[1024];
if(argc < 2) {
printf("./fifo_r name.pipen");
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if(fd < 0)
sys_err("open");
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd);
return 0;
}
测试结果:
管道只在内核中占用一小部分内存,而管道文件不会在磁盘上占用空间(管道文件的PCB在内核中占用内存,只消耗一个inode)。如图:
二、mmap文件-内存映射与进程间通信:
1、mmap介绍:
对于mmap不进行文件映射的操作可参考: 系统调用与内存管理(sbrk、brk、mmap、munmap)
mmap函数可以把磁盘文件的一部分直接映射进内存,这样文件的位置就有了对应的地址,对于文件的都写可以直接使用指针,而不需要read与write。
void * mmap(void * addr, size_t length, int port, int flags, int fd, off_t offset);
//addr:为映射的内存起始位置,设置为NULL操作系统自动分配;
//length:映射的长度;
//port:内存访问权限,PORT_NONE、PORT_EXEC、PORT_READ、PORT_WRITE
//flags:属性,MAP_SHARED(磁盘/内存任意一处修改同步到另外一处)、//MAP_PRIVATE(磁盘/内存任意一处修改不影响另外一处);
//fd:文件描述符(映射文件已打开),如不映射文件,只申请内存时值为-1;
//offset:偏移量,4096整数倍,一般先lseek确定位置然后置将offset为0即可
//返回值:返回系统分配的addr起始地址,失败返回MAP_FAILED。
int munmap(void * addr, size_t length);//参数与mmap对应
2、简单测试:
将文件中的内容映射到内存中,并修改内存以同步到文件中,观察文件中内容是否变化:
/****
mmap.c
头文件省略,sys_err函数省略
*****/
int main(void)
{
int fd, len, *p;
fd = open("hello", O_RDWR);
if(fd < 0)
sys_err("open");
len = lseek(fd, 0, SEEK_END);
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//进行内存映射
if(p == MAP_FAILED)
sys_err("mmap");
close(fd);//释放file结构体
p[0] = 0x30313233;
munmap(p, len);//解除映射
return 0;
}
//注意:close(fd);并不会解除映射,close只是将file结构体计数减1,并不会对映射关系产生影响。
测试结果:
通过p[0]四个字节的空间修改了hello文件中的前四个字符(小端存储):
3、mmap实现进程间通信:
/*mmap.h*/
#ifndef _MMAP_H_
#define _MMAP_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
/*mmap文件大小为4K*/
#define MAPLEN 0x1000
/*发送的信息结构体*/
struct STU {
int id;
char name[20];
char sex;
};
#endif
/*process_mmap_w.c*/
#include"mmap.h"
int main(int argc, char *argv[])
{
struct STU *mm;
int fd, i = 0;
if (argc < 2) {
printf("./a.out filenamen");
exit(1);
}
fd = open(argv[1], O_RDWR | O_CREAT, 0777);
if(fd < 0)
sys_err("open");
/*将创建的空文件扩大至4KB-1*/
if(lseek(fd, MAPLEN-1, SEEK_SET) < 0)
sys_err("lseek");
/*将扩大的文件末尾写一次数据保证扩大有效*/
if(write(fd, "