Linux - 进程间通信方式之管道
概述
进程间通信(IPC)有多种方式,管道是进程间通信最基本的方式。
管道是"半双工"的,即单向的。
管道是先进先出(FIFO)的。
单进程中的管道
int fd[2];
使用文件描述符fd[1],向管道写数据
使用文件描述符fd[0],从管道读数据
注:单进程中的管道无实际用处。管道用于多进程通信。
一、管道的创建
使用pipe系统调用
/*************************************************************************
* 函数:int pipe(int pipefd[2]);
* 功能:创建一个管道
* 参数:
* pipefd[2] - 用于返回引用管道末端的两个文件描述符。
* pipefd[0]: 管道的读取端
* pipefd[1]: 管道的写入端
* 返回:
* 成功 - 返回0
* 失败 - 返回-1,并设置errno
* 描述:执行失败时,不会修改pipefd的值
* 如果对pipefd[0]进行写操作或对pipefd[1]进行读操作将造成不可预期的错误。
**************************************************************************/
二、管道的使用
1、单进程使用管道通信
注意:创建管道后,获得该管道的两个文件描述符,不需要普通文件操作中的open操作。
示例代码 - 单个进程使用管道通信:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int fd[2] = { 0 };
char buffer1[1024], buffer2[1024];
int ret = pipe(fd);
if(ret != 0) {
fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
exit(1);
}
// 写数据
strcpy(buffer1, "Sample code: a single process uses pipes to communicate!");
if( write(fd[1], buffer1, strlen(buffer1)) == -1 ) {
fprintf(stderr, "write() - failed! reason: %s\n", strerror(errno));
exit(2);
}
printf("write data success!\n");
// 读数据
bzero(buffer2, sizeof(buffer2));
if( read(fd[0], buffer2, sizeof(buffer2)) == -1 ) {
printf("read() failed! reason: %s\n", strerror(errno));
exit(3);
}
printf("read success! data: %s\n", buffer2);
close(fd[0]);
close(fd[1]);
return 0;
}
2、多进程使用管道通信
创建管道之后,再创建子进程,此时共有4个文件描述符。
4个端口,父子进程分别持有1个读端口和1个写端口。既可向任一写端口写入数据,也可从任一读端口读入数据。
示例代码 - 多进程进程使用管道通信:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
int fd[2] = { 0 };
char pbuffer[1024] = { 0 }; // 父进程
char cbuffer[1024] = { 0 }; // 子进程
int ret = pipe(fd);
if(ret < 0) {
fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
exit(1);
}
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
exit(2);
} else if (pid == 0) {
bzero(cbuffer, sizeof(cbuffer));
if( read(fd[0], cbuffer, sizeof(cbuffer)) < 0) {
fprintf(stderr, "process[%d]: read() - failed! reason: %s\n", getpid(), strerror(errno));
exit(3);
}
printf("process[%d]: read - success! data: %s\n", getpid(), cbuffer);
} else {
bzero(pbuffer, sizeof(pbuffer));
strcpy(pbuffer,"Sample code: multi-process processes use pipes to communicate!");
if(write(fd[1], pbuffer, strlen(pbuffer)) < 0) {
fprintf(stderr, "parent process[%d]: write - failed! reason: %s\n", getpid(), strerror(errno));
exit(4);
}
printf("parent process[%d]: write - success!\n", getpid());
}
if(pid > 0) {
int wstatus = 0;
wait(&wstatus);
}
return 0;
}
3、子进程使用exec启动新程序时管道的使用
有程序P1, P2。他们使用管道进行通信。P1由用户输入一段字符串后把这段字符串发送给P2。P2收到以后把该字符串打印出来。
- P1:
创建管道,创建子线程,使用exec函数将子线程替换成P2(在使用exec时,把管道的读端作为exec的参数)。在父进程中获取用户的输入,输入完成后把输入好的字符串发给P2(即父进程把字符串写入管道)。- P2:
从参数中获取管道的读端 (参数为P2的main函数的参数),读管道,把独到的字符串打印出来。- 难点:
子进程使用exec启动新程序运行后,新进程能够使用原来子进程的管道(因为exec能共享原来的文件描述符),但问题是新进程并不知道原来的文件描述符是多少!- 解决方案
把子进程的管道文件描述符,用exec的参数传递给子进程
示例代码 - P1:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
int fd[2] = { 0 };
if (pipe(fd) < 0) {
fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
exit(1);
}
int pid = fork();
if(pid < 0) {
fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
exit(2);
} else if (pid == 0) {
char arg[1024] = { 0 };
sprintf(arg, "%d", fd[0]);
if (execl("P2.exe", "P2.exe", arg, NULL) < 0) {
printf("execl() - failed! reason: %s\n", strerror(errno));
exit(3);
}
} else {
char buffer[1024] = { 0 };
strcpy(buffer, "The use of pipes when a child process starts a new program using exec.");
if(write(fd[1], buffer, strlen(buffer)) < 0) {
fprintf(stderr, "write() - failed! reason: %s\n", strerror(errno));
exit(4);
}
printf("process[%d] send data success!\n", getpid());
}
if(pid > 0) {
int wstatus = 0;
wait(&wstatus);
}
return 0;
}
示例代码 - P2:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if(argc < 2) {
fprintf(stderr, "Insufficient number of parameters!\n");
exit(1);
}
int fd = 0;
char buffer[1024] = { 0 };
sscanf(argv[1], "%d", &fd);
if(read(fd, buffer, sizeof(buffer)) < 0) {
fprintf(stderr, "read() - failed! reason: %s\n", strerror(errno));
exit(2);
}
printf("read success! data: %s\n", buffer);
return 0;
}
4、关闭管道的读端/写端
对管道执行读(read)操作时,如果管道中已经没有数据了,此时read将被"阻塞"。如果此时管道的写端已被关闭(close),则读操作将可能被一直阻塞!而此时的阻塞没有任何意义了(因为管道的写端已被关闭,即不会在写入数据了)。
因此,如果不准备再向管道中写入数据,则把该管道的所有写端关闭。如果此时在对该管道执行读(read)操作时,就会返回0,而不再阻塞该读(read)操作。这是管道的特性。如果有多个写端口,而只关闭了一个写端口,那么无数据时读操作将仍被阻塞。
实际实现方式:父子进程各有一个管道的读端和写端;把父进程的读端(或写端)关闭;把子进程的写端(或读端)关闭;使这个"4端口"管道变成单向的"2端口"管道。
示例代码 - 将父子进程的双读写端口改造成单读写端口:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
int fd[2] = { 0 };
if( pipe(fd) < 0) {
fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
exit(1);
}
int pid = fork();
if(pid < 0) {
fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
exit(2);
} else if(pid == 0) {
close(fd[1]);
char buffer[1024] = { 0 };
if(read(fd[0], buffer, sizeof(buffer)) < 0) {
fprintf(stderr, "process[%d]: read - failed! reason: %s\n", getpid(), strerror(errno));
exit(3);
}
printf("process[%d]: read success! data: %s\n", getpid(), buffer);
// 休眠5秒后再次尝试读取数据,此时read返回0
sleep(5);
bzero(buffer, sizeof(buffer));
read(fd[0], buffer, sizeof(buffer));
printf("process[%d]: The pipe is closed! data length: %lu\n", getpid(), strlen(buffer));
} else {
close(fd[0]);
char buffer[1024] = { 0 };
strcpy(buffer, "Transform the dual read-write port of the parent-child process into a single read-write port.\n");
if(write(fd[1], buffer, strlen(buffer)) < 0) {
fprintf(stderr, "process[%d]: write failed! reason: %s\n", getpid(), strerror(errno));
exit(4);
}
printf("process[%d]: write success!\n", getpid());
// 子进程写完数据后关闭写文件描述符
close(fd[1]);
}
if(pid > 0) {
int wstatus;
wait(&wstatus);
}
return 0;
}
示例代码 - 将父子进程的双读写端口改造成单读写端口 解析:
- 父进程的写操作注释,子进程的读操作会被阻塞。
- 父进程的写操作注释,并close父进程的写端,此时子进程的读操作将被阻塞。
- 父进程的写操作注释,并close父子进程的写端,此时子进程的读操作将直接返回0,而不再阻塞。
- 最终方案:关闭父进程的读端,关闭子进程的写端。当父进程不再发送数据时,就关闭本进程的写端。
实力代码 - 父进程将用户输入的数据发送给子进程,子进程显示出来,直到父进程发送exit为止:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
int fd[2] = { 0 };
if (pipe(fd) < 0) {
fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
exit(1);
}
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
exit(2);
}
else if (pid == 0) {
// 关闭写端
close(fd[1]);
int times = 0;
char buffer[1024] = { 0 };
while (1) {
bzero(buffer, sizeof(buffer));
if (read(fd[0], buffer, sizeof(buffer)) == 0) {
fprintf(stdout, "child process[%d]: The writer has been closed.\n", getpid());
break;
}
printf("child process[%d] - (No. %d)%s\n", getpid(), ++times, buffer);
}
}
else {
char buffer[1024] = { 0 };
while (1) {
bzero(buffer, sizeof(buffer));
printf("parent process[%d] - please input:", getpid());
scanf("%s", buffer);
if (strcmp(buffer, "exit") == 0) {
fprintf(stdout, "parent process[%d]: Type [input] to close the write side.\n", getpid());
close(fd[1]);
break;
}
if (write(fd[1], buffer, strlen(buffer)) < 0) {
fprintf(stderr, "parent process[%d]: write() - failed! reason: %s\n", getpid(), strerror(errno));
}
}
}
return 0;
}
5、把管道作为标准输入和标准输出
- 把管道作为标准输入和标准输出的优点:
- 子进程使用exec启动新程序时,就不需要再把管道的文件描述符传递给子程序了。
- 可以直接使用标准输入(或标准输出)的程序(例如:od-c(统计字符个数,结果为八进制))。
- 实现原理:
- 使用dup复制文件描述符
- 用exec启动新程序后,原进程中已打开的文件描述符仍保持打开,即可以共享原进程的文件描述符。
- 附 - dup函数的介绍
/*********************************************************************** * 函数:int dup(int oldfd); * 功能:Dup()系统调用创建文件描述符oldfd的副本,对新描述符使用编号最小的未使用文件描述符。 * 参数: oldfd - 需要创建副本的文件描述符 * 返回: 成功 - 返回新的文件描述符 失败 - 返回-1,并设置errno * 说明:函数返回的新文件描述符和被复制的文件描述符,指向同一个文件或管道。 ************************************************************************/
示例代码 - 把管道作为子进程的标准输入、标准输出:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
int fd[2] = { 0 };
if(pipe(fd) < 0) {
fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
exit(1);
}
int pid = fork();
if(pid < 0) {
fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
exit(2);
} else if(pid == 0) {
close(fd[1]);
close(0);
dup(fd[0]);
close(fd[0]);
execlp("./pipe_std.exe", "./pipe_std.exe", NULL);
printf("execlp() - failed! reason: %s\n", strerror(errno));
} else {
close(fd[0]);
char buffer[1024] = "Hello World";
if(write(fd[1], buffer, strlen(buffer)) < 0 ) {
fprintf(stderr, "write() - failed! reason: %s\n", strerror(errno));
exit(3);
}
close(fd[1]);
}
if(pid > 0) {
int wstatus = 0;
wait(&wstatus);
}
return 0;
}
实力代码 - 把管道作为进程的标准输入、标准输出:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
char buffer[1024] = { 0 };
bzero(buffer,sizeof(buffer));
scanf("%s", buffer);
printf("%s\n", buffer);
bzero(buffer,sizeof(buffer));
scanf("%s", buffer);
printf("%s\n", buffer);
return 0;
}