Linux进程IPC浅析[进程间通信]
- 进程间通信概述IPC
- 匿名管道pipe
- 命名管道fifo
- 匿名管道和命名管道之间对比
进程间通信概述IPC
进程间的通讯目的:
- 数据传输 :一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间
- 共享数据:多个进程想要操作共享数据,一个进程对共享数据修改,其他进程应该立即看到
- 通知事件:一个进程需要向另外一个或一组进程发送消息,通知它发生了某种时间
- 资源共享:多个进程之间共享同样的资源,为了做到这一点,需要内核提供锁和同步机制
- 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够即使知道它的状态改变
Linux进程间通讯(IPC)由以下几个部分发展而来:
- 早期UNIX进程间通信
- 基于System V进程间通讯
- 基于Socket进程间通信和POSIX进程间通信
Unix进程通信方式包括:管道,FIFO,信号
System V进程通信方式包括:System V消息队列,System V信号灯,System V共享资源
POSIX进程间通信包括:posix消息队列,posix信号灯,posix共享内存
现代进程间通信方式:
- 管道(pipe)和命名管道(FIFO)
- 信号(signal)
- 消息队列
- 共享内存
- 信号量(进程的信号量)
- 套接字(socket)
管道通信
概念:
管道是针对本地计算机的两个进程而设计的一种通信方法
管道建立后,实际上是获得了两个文件描述符:一个用于读取,另外一个用于写入
管道是单工的,数据只能流向一个方向,需要双通时,需要建立两个管道
数据的读入和读出:一个进程向管道的一端写入,被管道另一端的进程读出
写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从管道缓冲区的头部开始读取
管道分类:(内核中的一块缓存,写入是尾部,读入是头部,是最常见的IPC机制)
1:匿名管道(pipe)
在关系进程中进程(父进程和子进程,兄弟进程之间)
由pipe系统调用,管道由父进程建立
管道位于内核空间,其实是一块缓存
2:命名管道(FIFO)
两个没有任何关系的进程之间通信可通过命名管道进行数据传输,本质是内核中的一块缓存,另在文件系统中以一个特殊的设备文件(管道文件)存在
通过系统调用mkfifo创建
匿名管道创建:
#include<unistd.h>
int pipe(int fd[2]);
返回:成功返回0,出错返回-1
两个文件描述符数组:
fd[0]:为pipe的读端
fd[1]:为pipe的写端
fd[0]用于读取管道,fd[1]用于写入管道
管道的读写特性:
1:通过打开两个管道来创建一个双向的管道
2:管道是阻塞性的,当进程从管道中读取数据,若没有数据进程会阻塞
3:当一个进程往管道中不断地写入数据但是没有进程去读取数据,此时只要管道没有满是可以的,但若管道放满数据的则会报错
4:不完整管道(有一端被关闭掉)
当读一个写端已被关闭的管道的时候,在所有数据被读取后,read返回0,以表示到达了文件尾部
如果写一个读端被关闭的管道,则产生信号SIGPIPE,如果忽略该西i你好或捕捉该信号从处理程序返回,则write返回-1,同时ermo设置为EPIPE
在网络编程的过程中,有时候会用到类似于不完整管道这样的过程(服务器挂掉),这个时候需要去检测.
1:管道创建:
/*
* ===========================================================================
*
* Filename: pipe_create.c
* Description:
* Version: 1.0
* Created: 2017年04月05日 21时49分26秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char* argv[]){
int pipe_fd[2];
if(pipe(pipe_fd) == -1){
perror("create pipe error");
}else{
printf("create pipe success");
}
close(pipe_fd[0]);
close(pipe_fd[1]);
return 0;
}
管道的读写:
管道一般由父进程创建,然后再fork一个子进程(子进程会copy父进程的内存空间,所以其会将父进程中的管道信息copy到子进程中去)
2:怎么利用pipe进行单向通信
/*
* ===========================================================================
*
* Filename: pipe_create2.c
* Description:
* 父子进程之间通过管道来进行通信,父亲进程来读取子进程写到文件中的数据
* Version: 1.0
* Created: 2017年04月07日 21时01分54秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<memory.h>
#include<unistd.h>
#define BUFFER_SIZE 1024
int main(int argc,char*argv[]){
//fd[0]对应的是读端,fd[1]对应的是写端
int pipe_fd[2];
pid_t pid;
if(pipe(pipe_fd)== -1){
perror("create pipe error");
}else{
perror("create pipe success");
}
pid = fork();
if(pid < 0){
perror("fork error");
}else if(pid>0){
//父进程的执行时间片,父进程作为读的调用读的端口
close(pipe_fd[1]);
char buffer[BUFFER_SIZE];
memset(buffer,0,sizeof(buffer));
while(read(pipe_fd[0],buffer,sizeof(buffer)) != 0){
printf("read content:%s\n",buffer);
}
}else if(pid == 0){
//子进程执行的时间片,调用写的端口
close(pipe_fd[0]);
char content [] = "helloworld";
write(pipe_fd[1],content,sizeof(content));
}
wait(0);
close(pipe_fd[0]);
close(pipe_fd[1]);
return 0;
}
3:利用管道来进行命令执行和输出筛选功能
/*
* ===========================================================================
*
* Filename: pipe_create3.c
* Description:
* 两个子进程之间通过管道去进行通信.这个时候第一个子进程负责去写,第二个子进程负责去读取 ,然后进行晒选
* Version: 1.0
* Created: 2017年04月07日 22时40分31秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
char *cmd1[3] = {"/bin/cat","/etc/passwd",NULL};
char *cmd2[3] = {"/bin/grep","root",NULL};
int main(int argc,char * argv[]){
int fd[2];
pid_t pid;
if(pipe(fd) != 0){
perror("pipe create error");
}
int i = 0;
//进程扇子,进程扇是子进程创建完毕执行完毕之后,直接退出;父亲进程需要等待两个子进程结束
for(i = 0;i<2 ; i++){
pid = fork();
if(pid < 0){
perror("pid fork error");
exit(EXIT_FAILURE);
}else if(pid > 0 ){
//父进程执行的,父进程在子进程创建完毕之后需要wait等待回收
if(i == 1){
close(fd[0]);
close(fd[1]);
wait(0);
wait(0);
}
}else {
//子进程执行的时间片,
if(i == 0){
//第一个子进程负责去写数据,关闭读端fd[0]
close(fd[0]);
//将标准输出重新定位到写端,作为数据输入
if(dup2(fd[1],STDOUT_FILENO) != STDOUT_FILENO){
perror("dup2 error");
}
//因为复制一份给了STDOUT了,所以直接进行
close(fd[1]);
if(execvp(cmd1[0],cmd1) != 0){
perror("exec error");
}
break;
}else if(i == 1){
//第二个子进程负责去读取数据,关闭写端fd[1];
close(fd[1]);
//将标准输入定位为管道文件的读端
if(dup2(fd[0],STDIN_FILENO) != STDIN_FILENO){
perror("dup2 error");
}
close(fd[0]);
if(execvp(cmd2[0],cmd2) != 0){
perror("exec error");
}
break;
}
}
}
return 0;
}
4:管道的双工通信:利用两个管道在进程间进行通信
/*
* ===========================================================================
*
* Filename: pipe_create2.c
* Description:
* 父子进程之间通过两个管道来进行通信,父亲进程来读取子进程写到文件中的数据
* ,当父进程读取到子进程的相关数据之后,再去给子进程写一些数据
* 注意:
* 管道是阻塞方式的,所以管道文件时没有结束符号的,在读取内容的时候,如果想终端,必须关闭掉管道文件
* Version: 1.0
* Created: 2017年04月07日 21时01分54秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<memory.h>
#include<unistd.h>
#define BUFFER_SIZE 1024
int main(int argc,char*argv[]){
//fd[0]对应的是读端,fd[1]对应的是写端
int pipe_fd[2];
int pipe_fd2[2];
pid_t pid;
//创建第一个管道
if(pipe(pipe_fd) != 0){
perror("create pipe error");
}else{
perror("create pipe success");
}
//创建第二个管道
if(pipe(pipe_fd2) != 0){
perror("create pipe error");
}else{
perror("create pipe success");
}
pid = fork();
if(pid < 0){
perror("fork error");
}else if(pid>0){
//父进程的执行时间片,父进关闭第一个管道写的端口
close(pipe_fd[1]);
//父亲进程关闭第二个管道的读端
close(pipe_fd2[0]);
char buffer[BUFFER_SIZE];
memset(buffer,0,sizeof(buffer));
ssize_t size_in;
while((size_in = read(pipe_fd[0],buffer,sizeof(buffer))) != 0){
if(size_in > 0){
printf("read content:%s\n",buffer);
char content2[] = "nihao";
//将文件写入到第二个管道中去
write(pipe_fd2[1],content2,sizeof(content2));
//因为管道文件是阻塞方式的,所以管道文件是没有结束符号的,只能通过close来关闭
close(pipe_fd2[1]);
}
}
}else if(pid == 0){
//子进程执行的时间片,调用写的端口,关闭第一个管道的读端
close(pipe_fd[0]);
//关闭第二个管道的写端,调用第二个管道的读端口
close(pipe_fd2[1]);
char content [] = "helloworld\n";
write(pipe_fd[1],content,sizeof(content));
char buffer[BUFFER_SIZE];
ssize_t size_out;
while((size_out = read(pipe_fd2[0],buffer,BUFFER_SIZE)) != 0){
if(size_out > 0){
printf("content2:%s\n",buffer);
write(pipe_fd[1],content,sizeof(content));
close(pipe_fd[1]);
}else{
perror("read error\n");
exit(EXIT_FAILURE);
}
}
}
wait(0);
close(pipe_fd[0]);
close(pipe_fd[1]);
return 0;
}
5:不完整管道的读写1:
/*
* ===========================================================================
*
* Filename: broken_pipe.c
* Description: 不完整管道,读取一个写端已经关闭的管道文件,当read返回0的时候
* 说明已经到达了管道的尾部,父进程去读取.
* Version: 1.0
* Created: 2017年04月09日 13时06分19秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<memory.h>
int main(int argc,char * argv[]){
int pipe_fd[2];
pid_t pid;
if(pipe(pipe_fd) < 0){
perror("create pipe error");
exit(1);
}
pid = fork();
if(pid < 0){
perror("fork error");
exit(1);
}else if(pid == 0){
//子进程执行时间片,先关闭读端,然后写一组字符串进去,子进程在写完之后关闭管道写端
close(pipe_fd[0]);
char content[] = "123456";
write(pipe_fd[1],content,sizeof(content));
close(pipe_fd[1]);
}else {
//父进程执行的时间片
close(pipe_fd[1]);
sleep(5);
while(1){
char c;
//当写端管道被关闭后,读取出来的值为0
if(read(pipe_fd[0],&c,1)==0){
printf("\n read end\n");
break;
}else{
printf("%c",c);
}
}
close(pipe_fd[0]);
wait(0);
}
return 0;
}
6:不完整管道的读写2
/*
* ===========================================================================
*
* Filename: broken_pipe2.c
* Description:
* 不完整管道,当写入一个读端被关闭的管道的时候,这个时候会产生一个
* SIGPIPE的信号,
* 如果忽略或者捕获该信号从处理程序返回,则write返回-1,同时errno也会变为-1
* Version: 1.0
* Created: 2017年04月09日 13时33分29秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<string.h>
//信号处理函数
void sig_handler(int signo){
if(signo == SIGPIPE){
printf("SIGPIPE Occured\n");
}
}
int main(int argc,char * argv[]){
int pipe_fd[2];
pid_t pid;
if(pipe(pipe_fd) < 0){
perror("create pipe error");
}
pid = fork();
if(pid < 0 ){
perror("fork pid error");
exit(EXIT_FAILURE);
}else if(pid > 0){
//父亲进程去写
close(pipe_fd[0]);
//延时睡眠.确保管道的读端已经关闭了
sleep(3);
//注册信号捕获函数
if(signal(SIGPIPE,sig_handler) == SIG_ERR){
printf("signal sigpipe error");
exit(EXIT_FAILURE);
}
//往读端的已经关闭的管道中写入数据
char content[] = "helloworld";
if(write(pipe_fd[1],content,sizeof(content)) != sizeof(content)){
fprintf(stderr,"%s,%s\n",strerror(errno),(errno == EPIPE)?"EPIPE":",unknow");
}
close(pipe_fd[1]);
wait(0);
}else {
//子进程去关闭管道两端
close(pipe_fd[0]);
close(pipe_fd[1]);
}
return 0;
}
标准库中的管道操作:
#include<stdio.h>
FILE *popen(const char*cmdstring,const char *type);
返回值:成功返回文件指针,出错返回NULL
int pclose(FILE *fp);
返回值:cmdtsring的终止状态,出错返回-1
使用popen()创建的管道必须使用pclose()关闭,其实,popen/pclose和标准文件输入和输出流中的fopen/fclose相似
Popen内部原理:
当 type 为r的时候,子进程负责将命令执行的结果写入管道中去(将标准输出重定向到管道的写段).父进程从管道中读取命令执行的结果,并且将其防止到FILE*类型的文件指向的结构体缓存中去
当type为w的时候,子进程也是需要去执行命令,但是子进程需要从管道中读取数据作为命令执行的输入.将标准输入重定向到管道的读端,父进程将结构体缓存中的数据写入管道
在标准库下面的管道:
/*
* ===========================================================================
*
* Filename: pipe_f.c
* Description: 标准库函数中的管道相关操作
* Version: 1.0
* Created: 2017年04月09日 14时02分44秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<memory.h>
#include<malloc.h>
#define BUFFER_SIZE 1024
int main(int argc,char * argv[]){
//通过popen打开管道,成功返回文件指针,失败返回NULL
FILE * fp = popen("cat /etc/passwd","r");
if(fp == NULL){
printf("file open error\n");
exit(EXIT_FAILURE);
}
char *buffer = (char*)malloc(BUFFER_SIZE*sizeof(char));
memset(buffer,0,BUFFER_SIZE*sizeof(char));
//从文件指针中去读取
while(fgets(buffer,BUFFER_SIZE*sizeof(char)-1,fp)!=NULL){
printf("%s",buffer);
}
free(buffer);
pclose(fp);
printf("-------------------------\n");
//将type修改成w
FILE* fp_w = popen("wc -l","w");
fprintf(fp_w,"1\n2\n3\n");
pclose(fp_w);
return 0;
}
命名管道fifo
命名管道;
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
返回:若成功则返回0,出错返回-1
只要对FIFO有适当访问权限,FIFO可用在任何两个没有任何关系的进程之间通信
本质是内核中的一块缓存,另在文件系统中以一个特殊的设备文件(管道文件)存在
在文件系统中只有一个索引块存放文件的路径,没有 数据快,所有数据存放在内核中
命名管道必须读和写同时打开,否则单独读或者单独写会引发阻塞
命名mkfifo创建命名管道(命令内部调用mkfifo函数)
对FIFO的操作与操作普通文件一样
一旦已经用mkfifp创建了一个FIFO,就可用open打开它
一般的文件I/O(close,read,write,unlink等)都可用于FIFO
FIFO相关出错信息:
EACCES(无存取权限)
EEXIST(制定文件不存在)
ENAMETOOLLONG(路径名太长)
ENOENT(包含的目录不存在)
ENOSOC(文件系统剩余空间不足)
ENOTDIR(文件路径无效)
EROFS(指定的文件存在于只读文件系统中)
fifo管道的读端
/*
* ===========================================================================
*
* Filename: fifo_read.c
* Description: 从fifo管道中去读取
* Version: 1.0
* Created: 2017年04月09日 14时38分07秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<memory.h>
#include<fcntl.h>
int main(int argc, char* argv[]){
if(argc < 2){
printf("缺少参数");
exit(EXIT_FAILURE);
}
int fd = open(argv[1],O_RDONLY);
if(fd < 0){
perror("open file error\n");
exit(EXIT_FAILURE);
}else{
printf("open file success\n");
}
char buffer[512];
memset(buffer,0,sizeof(buffer));
while(read(fd,buffer,sizeof(buffer)) < 0){
perror("read error");
}
printf("%s\n",buffer);
return 0;
}
fifo管道的写端
/*
* ===========================================================================
*
* Filename: fifo_write.c
* Description: 将数据写入到fifo文件中去
* Version: 1.0
* Created: 2017年04月09日 14时38分48秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<memory.h>
#include<fcntl.h>
int main(int argc, char* argv[]){
if(argc < 2){
printf("缺少参数\n");
exit(EXIT_FAILURE);
}
int fd = open(argv[1],O_WRONLY);
if(fd < 0){
perror("open file error\n");
exit(EXIT_FAILURE);
}else{
printf("open file success\n");
}
char content[] = "hellowolrd";
while(write(fd,content,sizeof(content))!=sizeof(content)){
printf("write error\n");
}
close(fd);
return 0;
}
匿名管道和命名管道之间对比
相同点:
默认都是阻塞方式的读写
都适用于socket的网络通信
阻塞不完整管道(有一端关闭)
单纯读的时候,在所有数据被读取后,read返回0,以表示达到了文件尾部
单纯写的时候,则产生sigpipe,如果忽略或捕捉该信号,并从处理程序返回,则write返回-1,同事errno设置为EPIPE
阻塞完整通道(两端都开启)
单纯读时,要么阻塞,要么读取到数据
单纯写时,写到管道满的时候报错
非阻塞不完整管道(有一端关闭)
单纯读时直接报错
单纯写的时候,则产生sigpipe,如果忽略或捕捉该信号,并从处理程序返回,则write返回-1,同事errno设置为EPIPE
非阻塞完整管道(两端都开启)
单纯读时直接报错
单纯写的时候,写到管道满的时候会出错
不同点:
打开方式不一致
pipe通过fcntl系统调用来设置O_NOBLOCK来设置非阻塞性读写
FIFO通过fcntl系统调用或者open函数来设置非阻塞性读写
以上就是一些关于进程IPC通信相关管道部分的总结.代码都是可以直接进行run的,有兴趣的copy下来玩一下