进程之间通讯(IPC,interprocess communication)
管道
管道是内核维护的一种特殊的文件,主要用来作为进程间传递数据的主要方式,分为无名管道和命名管道(FIFO)。在shell
里面的一些过滤程序,就是管道,比如|grep
。
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
无名管道
创建匿名管道的函数:
#include <unistd.h> int pipe(int fd[2]);
功能:创建一个匿名管道
参数:fd[2]用来返回管道两端的文件描述符;
fd[0]:指向无名管道读端;
fd[1]:指向无名管道的写端。
返回值:
成功:返回0;
失败:返回-1,将错误记录在errno中。
管道的实现过程:
父进程使用函数
pipe
创建一个管道,得到两个文件描述符,fd[0],fd[1]
前者用来读,后者用来写。分别代表了管道的读端和写端。如下图所示:父进程
fork
一个子进程,由于子进程复制了父进程的文件描述符,因此同一个管道同时有两个读端和写端,如下图所示:关闭不需要的读端和写端,这个的根据子进程和父进程谁读谁写来决定。最终,管道只有一个读端,一个写端,数据从写端流到读端。父进程写子进程读的情况如下:
匿名管道特点:
管道通信是单向的,有固定的读端和写端。
数据被进程从管道读出后,在管道中该数据就不存在了。
当进程去读取空管道的时候,进程会阻塞。
当进程往满管道写入数据时,进程会阻塞。
管道容量为64KB(#define PIPE_BUFFERS 16 include/linux/pipe_fs_i.h)
对匿名管道的读写:
创建好的管道,如果写端被关闭,则读端在读完数据之后,
read
返回0,表示文件结束。写一个读端已经被关闭的管道,会产生
SIGPIPE
信号,如果自己使用signal
定义了对该信号的处理函数,或者忽略,则write
会返回-1,并且将errno设置为EPIPE
。常量
PIPE_BUF
规定了内核缓冲区的大小,当读写的数据小于这个值,则读写操作都是原子的,不会被其他同时对这个管道读写的操作交叉。
匿名管道的局限性:
一般情况下是半双工的(数据只能在一个方向上流动),有的系统也提供全双工的管道。
只能是由共同祖先的两个进程之间通信,一般用于父子进程之间。
标准I/O库提供的创建方法:
#include <stdio.h> FILE* popen(const char* cmdstring, const char* type);
功能:父进程以
type
的权限创建一个管道,并在子进程中执行cmdstring
指令,并将子进程的管道连接到标准输入(type = w
),标准输出(type = r
)。popen
先调用fork
创建子进程,在子进程中调用exec函数执行cmdstring
。cmdstring
由shell
执行shell -c cmdstring
参数:
cmdstring
:实际上调用的是excel("/bin/sh","sh", "-c","cmdstring",NULL);
type
:只能是读或者写,针对父进程创建管道返回的FILE*
指针对应文件的权限。
返回值:
成功:一个文件指针。
失败: 返回NULL.
#include <stdio.h> int pclose(FILE *fp);
功能:关闭管道,等待子进程结束的
waitpid()
就在这里实现。返回值:
成功:返回
cmdstring
的终止向状态失败:返回-1
协同进程
- 协同进程就是他的输入输出都被管道连接到同一个进程的过滤程序。
- 实例
//copress.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#define LINE_MAX 100
#define info() { \
FILE* fp = fopen("add2.log","a+"); \
if(fp == NULL) \
exit(-1); \
fprintf(fp, "%s func %s, line %d: ",__FILE__, __FUNCTION__, __LINE__ ); \
fprintf(fp, ": %s\n", strerror(errno)); \
fprintf(fp, "fp: %d\n",stdin->_fileno); \
fflush(fp); \
}
void handler(int signo){
printf("caught sigpipe\n");
}
int main(){
int fd1[2],fd2[2];
pid_t pid;
char line[LINE_MAX +1];
if(signal(SIGPIPE, handler) == SIG_ERR){
perror("signal error");
}
if(pipe(fd1) < 0 || pipe(fd2)){
perror("Create Pipe Error");
}
if((pid = fork()) < 0){
perror("Fork Error");
}
else if(pid > 0){
// parent
close(fd1[0]);
close(fd2[1]);
while(fgets(line, LINE_MAX,stdin) != NULL){
info();
int n = strlen(line);
if(write(fd1[1],line,n) != n){
perror("Parent Write Error");
}
// sleep(2);
info();
if((n = read(fd2[0], line, LINE_MAX)) < 0){
perror("Parent Read Error");
}
if(n == 0){
printf("child closed pipe\n");
break;
}
info();
line[n] = 0;
if(fputs(line, stdout) == EOF){
perror("Parent Stdout Error");
}
info();
}
if(ferror(stdin)){
printf("fets error\n");
}
exit(0);
}
else{
//child
close(fd1[1]);
close(fd2[0]);
if(fd1[0] != STDIN_FILENO){
if(dup2(fd1[0],STDIN_FILENO) != STDIN_FILENO){
exit(-1);
}
close(fd1[0]);
}
if(fd2[1] != STDOUT_FILENO){
if(dup2(fd2[1],STDOUT_FILENO) != STDOUT_FILENO){
exit(-1);
}
close(fd2[1]);
}
execl("./add2","add2",(char*)0);
exit(-1);
}
return 0;
}
//add2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#define LINE_MAX 100
#define info() { \
FILE* fp = fopen("add2.log","a+"); \
if(fp == NULL) \
exit(-1); \
fprintf(fp, "%s func %s, line %d: ",__FILE__, __FUNCTION__, __LINE__ ); \
fprintf(fp, ": %s\n", strerror(errno)); \
fprintf(fp, "fp: %d\n",stdin->_fileno); \
fflush(fp); \
}
// 自己写的例子
int main(){
char line[LINE_MAX+1];
int int1,int2;
info();
while(fgets(line,LINE_MAX,stdin) != NULL){
info();
if(sscanf(line,"%d%d",&int1,&int2) == 2){
info()
sprintf(line,"%d\n", int1 + int2);
info();
line[strlen(line)] = '\0';
if(fputs(line,stdout) == EOF){
perror("Fputs Error");
}
//由于管道是全缓冲,如果不刷新,则有不会立即写在管道缓冲区里面,导致父进程读不到数据而阻塞,同时,本程序下一次获取数据时候也得不到数据而阻塞,导致死锁
fflush(stdout);
info();
}
else{
info();
printf("invalid parameter.\n");
fflush(stdout);
// write(STDOUT_FILENO, "invalid parameter\n",18);
info();
}
}
info();
exit(0);
}
//apue书上例子
// int main(){
// char line[LINE_MAX+1];
// int int1,int2,n;
// info();
// while((n = read(STDIN_FILENO, line, LINE_MAX)) > 0){
// line[n] = 0;
// info();
// if(sscanf(line,"%d%d",&int1,&int2) == 2){
// sprintf(line,"%d\n", int1 + int2);
// n = strlen(line);
// if(write(STDOUT_FILENO, line,n) != n){
// perror("Fputs Error");
// }
// info();
// }
// else{
// info();
// perror("invalid parameter.");
// }
// }
// exit(0);
// }
命名管道(FIFO)
命名管道是一种可以在非亲缘进程之间进行通信的一种方式。命名管道类似于匿名管道,都是有内核维护的一块缓冲区,不在文件系统之中,文件系统知识记录了命名管道的路劲,因此可以在任意两个进程之间使用。
命名管道的相关操作:
#include <sys/types.h> #include<sys/stat.h> int mkfifo(const char *pathname, mode_t mode)
函数功能:创建有名管道(FIFO文件)。创建命名管道之后要打开才能使用。
参数说明:
const char *pathname
:文件名(可以带路径);mode_t mode
:创建FIFO文件的访问权限。设置用户、组群、其他的权限(0666)返回值:
创建成功:返回0;
创建失败:返回-1,同时将错误记录在errno。
#include <sys/stat.h> #include <fcntl.h> //定义了 AT_*常量 mkfifoat(int fd, const char* pathname, mode_t mode);
函数功能:创建有名管道(FIFO文件)。
参数说明:
待查看的文件名是由
fd
和pathname
共同决定的。如果
pathname
是个绝对路径,则忽略fd
参数如果
pathname
是个相对路径路径,且fd=AT_FDCWD
,则在当前工作目录的路径下查找pathname
如果
pathname
是个相对路径路径,且fd!=AT_FDCWD
,则在fd
对应的打开目录下查找pathname
mode_t mode
:创建FIFO文件的访问权限。一般为只读(O_RDONLY),只写(O_WR_ONLY),可以设置非租塞模式。
返回值:
+ 创建成功:返回0; + 创建失败:返回-1,同时将错误记录在errno。
命名管道的打开,读, 写就和正常的文件的读写是一样的。可以使用第三章的函数来操作。
int unlink(const char *pathname);
头文件:
#include <unistd.h>
函数功能:从文件系统删除文件。
- +
参数说明:
返回值:
创建成功:返回0;
创建失败:返回-1,同时将错误记录在errno。
命名管道打开规则:
open
设置了非阻塞标志O_NONBLOCK
读打开:若已经有相应进程写打开FIFO,则当前打开操作将成功返回,否则成功返回。
写打开:若已经有相应进程读打开FIFO,则当前打开操作将成功返回,否则打开失败,返回错误
ENXIO
open
没有设置非阻塞标志O_NONBLOCK
读打开:若已经有相应进程写打开FIFO,则当前打开操作将成功返回,否则阻塞到有进程写打开为止。
写打开:若已经有相应进程读打开FIFO,则当前打开操作将成功返回,否则阻塞到有进程读打开为止。
命名管道的读写操作规则:
命名管道只有读端和写端都打开的时候才能进行数据传输,否则一般情况下会阻塞直到另外一端打开为止。
阻塞读打开FIFO成功:说明已经有进程写打开了FIFO
- 有写进程打开FIFO,且FIFO中没有数据,则读操作阻塞。写进程结束,读操作认为读到了文件结束符。
非阻塞读打开FIFO:无论是否有写进程打开了FIFO,都成功。
有写进程打开FIFO,且FIFO中没有数据,读操作返回-1,当前errno值为EAGAIN,提醒以后再试。
没有写进程打开FIFO,且FIFO中没有数据,
阻塞写打开FIFO成功:说明已经有进程读打开了FIFO
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
非阻塞写打开FIFO:
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO有剩余空间时,在写满所有FIFO空闲缓冲区后,写操作返回;FIFO没有剩余空间,则失败返回
EAGAIN
。当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;
如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
其他:
写一个没有读打开的FIFO, 会产生
SIGPIPE
信号,读一个最后一个写进程关闭了的FIFO会产生一个文件结束符。读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
造成阻塞的原因有两种:
当前FIFO内有数据,但有其它进程在读这些数据;
FIFO有进程写打开,且FIFO内没有数据。(写打开进程结束则不再阻塞)
实例
//fifo_server.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#define BUF_MAX 8192
#define fifo "./fifo"
int main(){
int fd, nrev,bytes_wr;
char buf_wr[BUF_MAX];
int flag = O_WRONLY;
#ifdef NBLOCK
flag |= O_NONBLOCK;
#endif
#ifdef ATOMIC
bytes_wr = 2048;
#else
bytes_wr = 4096 + 1024;
#endif
memset(buf_wr, 42, BUF_MAX);
if(mkfifo(fifo, 0666) < 0){
perror("Create FIFO Error");
}
if((fd = open(fifo,flag)) < 0){
perror("Open FIFO Failed");
}
if((nrev = write(fd, buf_wr,bytes_wr)) < 0){
if(errno == EAGAIN){
perror("Try Later");
}
perror("Write FIFO Error");
}
else
printf("Write %d Bytes To FIFO\n", nrev);
// pause();
unlink(fifo);
return 0;
}
//fifo_client.c
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#define BUF_MAX 8192
#define fifo "./fifo"
int main(){
int fd, nrev,bytes_rd;
char buf_rd[BUF_MAX];
int flag = O_RDONLY;
#ifdef NBLOCK
flag |= O_NONBLOCK;
#endif
#ifdef ATOMIC
bytes_rd = 1000;
#else
bytes_rd = 4096 + 1024;
#endif
if((fd = open(fifo,flag)) < 0){
perror("Open FIFO Failed");
}
while(1){
memset(buf_rd, 0, sizeof(buf_rd));
if((nrev = read(fd, buf_rd,bytes_rd)) < 0){
if(errno == EAGAIN){
perror("NO DATA IN FIFO");
}
perror("Read FIFO Error");
}
else{
printf("Read %d Bytes From FIFO: \n", nrev);
printf("\t%s\n",buf_rd);
}
sleep(1);
}
return 0;
}
参考链接:
Linux环境进程间通信(一)