进程间通信之管道
1.管道
IPC 有多种方式, 管道是IPC的最基本的方式.
管道是“半双工”的,即是单向的。
管道是FIFO(先进先出)的。
单进程中的管道:
int fd[2]
使用文件描述符fd[1], 向管道写数据
使用文件描述符fd[0], 从管道读数据
注:单进程中的管道无实际用处
管道用于多进程间通信。
2.管道的创建
使用pipe系统调用
返回值:
成功:返回 0
失败:返回 -1
注意:获取两个“文件描述符”
分别对应管道的读端和写端。
fd[0]: 是管道的读端
fd[1]: 是管道的写端
如果对fd[0]进行写操作,对fd[1]进行读操作,可能导致不可预期的错误。
3.管道的使用
实例1:单进程使用管道进行通信
注意:创建管道后,获得该管道的两个文件描述符,
不需要普通文件操作中的open操作
如图:
//main1.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1)); //fd[1]写
printf("send information:%s\n", buff1);
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));//fd[0]读
printf("received information:%s\n", buff2);
return 0;
}
实例2:多进程使用管道进行通信
注意:创建管道之后,再创建子进程,此时一共有4个文件描述符。
4个端口,父子进程分别有一个读端口和一个写端口
向任意一个写端口写数据,即可从任意一个读端口获取数据。
//main2.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("process(%d) received information:%s\n", getpid(), buff2);
} else {
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("process(%d) send information:%s\n", getpid(), buff1);
}
if (pd > 0) {
wait();
}
return 0;
}
实例3:子进程使用exec启动新程序时管道的使用
有程序P1, P2
使用管道进行通信
P1由用户输入一个字符串,然后把该字符串发给p2
P2接收到以后,把该字符串打印出来
P1:
创建管道
创建子进程
在子进程中用exec替换成p2,
(在使用exec 时,把管道的读端作为exec的参数)
在父进程中,获取用户的输入,然后把所输入的字符串发送给p2
(即,父进程把字符串写入管道)
P2:
从参数中获取管道的读端(参数即为p2的main函数的参数)
读管道
把读到的字符串打印出来
难点:子进程使用exec启动新程序运行后,
新进程能够使用原来子进程的管道(因为exec能共享原来的文件描述符)
但问题是新进程并不知道原来的文件描述符是多少!
解决方案:
把子进程中的管道文件描述符,用exec的参数传递给新进程。
//main.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
//bzero(buff2, sizeof(buff2));
sprintf(buff2, "%d", fd[0]);
execl("main3_2", "main3_2", buff2, 0);
printf("execl error!\n");
exit(1);
} else {
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("process(%d) send information:%s\n", getpid(), buff1);
}
if (pd > 0) {
wait();
}
return 0;
}
//main3_2.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
int fd;
char buff[1024] = {0,};
sscanf(argv[1], "%d", &fd);
read(fd, buff, sizeof(buff));
printf("Process(%d) received information:%s\n", getpid(), buff);
return 0;
}
实例4:关闭管道的读端/写端
管道关闭后的读操作:
问题:
对管道进行read时,如果管道中已经没有数据了,此时读操作将被“阻塞”。
如果此时管道的写端已经被close了,则写操作将可能被一直阻塞!
而此时的阻塞已经没有任何意义了。(因为管道的写端已经被关闭,即不会再写入数据了)
解决方案:
如果不准备再向管道写入数据,则把该管道的所有写端都关闭,
则,此时再对该管道read时,就会返回0,而不再阻塞该读操作。(管道的特性)
注意,这是管道的特性。
如果有多个写端口,而只关闭了一个写端,那么无数据时读操作仍将被阻塞。
实际实现方式:
父子进程各有一个管道的读端和写端;
把父进程的读端(或写端)关闭;
把子进程的写端(或读端)关闭;
使这个“4端口”管道变成单向的“2端口”管道
//main4.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
close(fd[1]);
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("process(%d) received information:%s\n", getpid(), buff2);
} else {
strcpy(buff1, "Hello!");
close (fd[0]);
write(fd[1], buff1, strlen(buff1));
printf("process(%d) send information:%s\n", getpid(), buff1);
close (fd[1]);
}
if (pd > 0) {
wait();
}
return 0;
}
实例5 把管道作为标准输入和标准输出
把管道作为标准输入和标准输出的优点:
1.子进程使用exec启动新程序时,就不需要再把管道的文件描述符传递给新程序了。
2.可以直接使用使用标准输入(或标准输出)的程序。
比如 od –c (统计字符个数,结果为八进制)
实现原理:
1.使用dup复制文件描述符
2.用exec启动新程序后,原进程中已打开的文件描述符仍保持打开,
即可以共享原进程中的文件描述符。
注意:dup的用法
dup复制文件描述符,
返回的新文件描述符和被复制的文件描述符,指向同一个文件或管道
//main6.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
//bzero(buff2, sizeof(buff2));
//sprintf(buff2, "%d", fd[0]);
close(fd[1]);
close(0);//fd的0,1,2分别表示:标准输入、标准输出和标准错误
dup(fd[0]);//我们刚刚关掉了fd=0,也就是标准输入被关了,dup会将fd[0]复制到当前进程的最小未用文件描述符,也就是fd=0
//因此,标准输入现在指向复制后的文件描述符fd[0]。
close(fd[0]);
execlp("./od.exe", "./od.exe", "-c", 0);
printf("execl error!\n");
exit(1);
} else {
close(fd[0]);
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1)); //父线程写,子线程实际上此时用表示输入来收,也就是scanf
close(fd[1]);
}
return 0;
}
//od.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{ int ret = 0;
char buff[80] = {0,};
ret = scanf("%s", buff);
printf("[ret: %d]buff=%s\n", ret, buff);
ret = scanf("%s", buff);
printf("[ret: %d]buff=%s\n", ret, buff);
return 0;
}
4.使用popen/pclose
popen的作用:
用来在两个程序之间传递数据:
在程序A中使用popen调用程序B时,有两种用法:
- 程序A读取程序B的输出(使用fread读取)
- 程序A发送数据给程序B,以作为程序B的标准输入。(使用fwrite写入)
用法:man popen
返回值:成功,返回FILE*
失败, 返回空
实例1: 读取外部程序的输出
//main7.c
#include <stdio.h>
#include <stdlib.h>
#define BUFF_SIZE 1024
int main(void)
{
FILE * file;
char buff[BUFF_SIZE+1];
int cnt;
// system("ls -l > result.txt");
file = popen("ls -l", "r");
if (!file) {
printf("fopen failed!\n");
exit(1);
}
cnt = fread(buff, sizeof(char), BUFF_SIZE, file);
if (cnt > 0) {
buff[cnt] = '\0';
printf("%s", buff);
}
pclose(file);
return 0;
}
实例2:把输出写到外部程序
//main8.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFF_SIZE 1024
int main(void)
{
FILE * file;
char buff[BUFF_SIZE+1];
int cnt;
file = popen("./p2", "w");//gcc p2.c -o p2
if (!file) {
printf("fopen failed!\n");
exit(1);
}
strcpy(buff, "hello world!");
cnt = fwrite(buff, sizeof(char), strlen(buff), file);
pclose(file);
return 0;
}