进程间通讯 管道,操作系统实验进程的管道通信
进程间通信——管道IPC——进程间通信的方式有很多种,管道是最基本的一种。流水线是半双工的,即单向的。这是一个管道先进先出(先入先出)。在实际的多进程通信中,可以理解为有一个管道,每个进程有两个可以使用管道的‘端口’,分别负责读取和发送数据。单个进程中的管道:int fd[2]使用文件描述符fd[1]将数据写入管道。使用文件描述符fd[0]从管道中读取数据。
注意:单个进程中的管道没有实际用途。管道用于多进程通信。管道创建函数的原型:int pipe(int pipe FD[2]);返回值:成功:返回0。失败:返回-1。注意:获取两个“文件描述符”,分别对应于流水线的读端和写端。Fd[0]:是流水线的读取端;Fd[1]:是流水线的写结束;如果写入fd[0]并读取fd[1],可能会导致意外错误。管道1的使用示例:单个进程使用管道进行通信。注意:创建管道后,可以得到管道的两个文件描述符,不需要使用普通文件操作中的open操作。如下图所示:
#包含stdlib.h
#包含stdio.h
#包含字符串. h
int main(void)
{
int FD[2];
int ret
char buff 1[1024];
char buff 2[1024];
ret=管道(FD);
如果(ret!=0) {
printf(创建管道失败!\ n’);
出口(1);
}
strcpy(buff1,你好!);
write(fd[1],buff1,strlen(buff 1));//写一个你好
printf(发送信息:%s\n ,buff 1);
bzero(buff2,sizeof(buff 2));
read(fd[0],buff2,sizeof(buff 2));//读hello
printf(接收的信息:%s\n ,buff 2);
返回0;
}
示例2:多进程使用管道进行通信。注意:创建管道后,将创建子进程。此时,有4个文件描述符和4个端口。父进程和子进程分别有一个读端口和一个写端口,如下图所示:
#包含stdlib.h
#包含stdio.h
#包含字符串. h
int main(void)
{
int FD[2];
int ret
char buff 1[1024];
char buff 2[1024];
pid _ t pd
ret=管道(FD);
如果(ret!=0) {
printf(创建管道失败!\ n’);
出口(1);
}
PD=fork();
if (pd==-1) {
printf(fork错误!\ n’);
出口(1);
} else if (pd==0) {
//子进程先读写
bzero(buff2,sizeof(buff 2));
read(fd[0],buff2,sizeof(buff 2));//数据被没收时read会阻塞。
printf(process(%d)收到的信息:%s,buff2的地址:%p\n ,getpid(),buff2,buff 2);
睡眠(5);
你好,爸爸!);
write(fd[1],buff1,strlen(buff 1));
}否则{
//父进程先写后读
strcpy(buff1, Hello Kid );
write(fd[1],buff1,strlen(buff 1));
睡眠(5);
bzero(buff2,sizeof(buff 2));
read(fd[0],buff2,sizeof(buff 2));
printf(process(%d)收到的信息:%s,buff2的地址:%p\n ,getpid(),buff2,buff 2);
}
if (pd 0) {
wait();
}
返回0;
}
注:大家可以看到,我们都在父子进程中打印了buff2的地址,发现打印出来的(虚拟)地址是一样的,只是内容不同。一个是你好孩子,另一个是你好爸爸。其实是两个不同的地址。调用fork()函数创建子进程后,子进程会复制父进程的所有资源。例3:子进程用execl启动新程序时管道的作用细节:有两个程序p1和p2,它们通过管道相互通信。p1发送一个字符给p2,p2接收后在屏幕上打印出来。具体操作流程:p1创建管道。创建子进程。在子进程中使用execl()函数将子进程替换为程序p2。(使用execl函数时,将管道的读取端作为的参数。)在父进程中,通过管道向子进程发送一个字符串。P2从参数(该参数是p2的主函数的参数)中获取管道的读取端。阅读管道。打印出读取的字符串。Execl()函数原型int execl (constchar * path,constchar * arg,);函数——学习Linux下的execl函数当一个进程调用一个exec函数时,这个进程就被一个新的程序完全取代,新的程序从它的main函数开始执行。因为调用exec不会创建新的进程,所以它前后的进程ID没有改变。Exec只是用另一个新程序替换了当前进程的主体、数据、堆和堆栈段。
Argc和argv—— [c]主函数argc和argv argc的参数:是argument count的缩写,保存运行时传递给主函数的参数个数。Argv:是argument vector的缩写,它保存运行时传递给main函数的参数。类型是一个字符指针数组,每个元素都是一个字符指针,指向一个命令行参数。例如:main3.c
#包括unistd.h
#包含stdlib.h
#包含stdio.h
#包含字符串. h
int main(void) {
int FD[2];
int ret
char buff 1[1024];
char buff 2[1024];
pid _ t pd
ret=管道(FD);
如果(ret!=0) {
printf(创建管道失败!\ n’);
出口(1);
}
PD=fork();
if (pd==-1) {
printf(fork错误!\ n’);
出口(1);
} else if (pd==0) {
//bzero(buff2,sizeof(buff 2));
sprintf(buff2, %d ,FD[0]);//读取
execl(main3_2 , main3_2 ,buff2,0);//子进程被程序main3_2替代。
printf(execl错误!\ n’);
出口(1);
}否则{
strcpy(buff1,你好!);
write(fd[1],buff1,strlen(buff 1));//写
printf(process(%d)发送信息:%s\n ,getpid(),buff 1);
}
if (pd 0) {
wait();
}
返回0;
} main3 _ 2.c
#包括unistd.h
#包含stdlib.h
#包含stdio.h
#包含字符串. 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)收到的信息:%s\n ,getpid(),buff);
返回0;
}
示例4:关闭管道的读/写端。注意:以下所有情况都在两个进程下,即一个主进程和一个子进程。小例子1:主进程关闭写进程后,无法通过管道向子进程发送数据。此时子进程使用read函数读取数据,如果没有数据读取,就会阻塞。代码结果如下:
说明:主进程循环5次向子进程发送数据。5次之后,子进程无法接收主进程的数据,read()开始阻塞。#包含stdlib.h
#包含stdio.h
#包含字符串. h
int main(void) {
int FD[2];
int ret
char buff 1[1024];
char buff 2[1024];
pid _ t pd
ret=管道(FD);
如果(ret!=0) {
printf(创建管道失败!\ n’);
出口(1);
}
PD=fork();
if (pd==-1) {
printf(fork错误!\ n’);
出口(1);
} else if (pd==0) {
for(;){
bzero(buff2,sizeof(buff 2));
睡眠(3);
read(fd[0],buff2,sizeof(buff 2));
printf(process(%d)收到的信息:%s\n ,getpid(),buff 2);
}
}否则{
for(int I=0;i i ){
strcpy(buff1,你好!);
write(fd[1],buff1,strlen(buff 1));
睡眠(3);
printf(process(%d)发送信息:%s\n ,getpid(),buff 1);
}
}
if (pd 0) {
wait();
}
返回0;
}
例2:管道是‘共享’的,个人理解。注意,实际上,它不是同一个内存地址。
读数据的时候,流水线读端的数据会越读越少,而写数据的时候,会把写好的数据累加到尾部。
如下图所示,
#包含stdlib.h
#包含stdio.h
#包含字符串. h
int main(void) {
int FD[2];
int ret
char buff 1[1024];
char buff 2[1024];
pid _ t pd
ret=管道(FD);
如果(ret!=0) {
printf(创建管道失败!\ n’);
出口(1);
}
PD=fork();
if (pd==-1) {
printf(fork错误!\ n’);
出口(1);
} else if (pd==0) {
for(;){
bzero(buff2,sizeof(buff 2));
睡眠(3);
strcpy(buff1,爸爸!);
//子进程写入数据
write(fd[1],buff1,strlen(buff 1));
}
}否则{
for(int I=0;i i ){
bzero(buff2,sizeof(buff 2));
strcpy(buff1,你好!);
//父进程写入数据
write(fd[1],buff1,strlen(buff 1));
睡眠(10);
//父进程读取数据
read(fd[0],buff2,sizeof(buff 2));
printf(dad进程(%d)收到的信息:%s\n ,getpid(),buff 2);
睡眠(3);
}
}
if (pd 0) {
wait();
}
返回0;
}
摘要:当没有要读取的数据时,读取将会阻塞。比如有两个进程,主进程给子进程发送数据,主进程的写端是关闭的,所以不能给子进程发送数据,那么子进程的读就会被阻塞。写端关闭后,写不会阻塞。这里应该注意的是,除非关闭写入端,否则写入不会阻塞。关闭读取端后,读取不会被阻止。上面说的关闭是针对一个进程的,每个进程都有一个写者和一个读者。如果有多个进程,并且每个进程的写端都是关闭的,read()就不会阻塞。提示:为了避免不必要的麻烦,比如没有可读数据时读功能被阻塞,我们可以关闭无用的管道端口。例如,如果主进程只负责写数据,子进程只负责读数据,那么可以关闭父进程的读端和子进程的写端(当然要看实际情况),将 4端口管道变成单向 2端口管道,如下图所示:
示例5:使用管道作为标准输入和标准输出的优势:
当一个子进程用exec启动一个新进程时,不再需要将管道的文件描述符传递给新程序。一种可以是标准输入(或标准输出)的程序。实施流程:
使用dup复制文件描述符。用exec启动新程序后,在原始进程中打开的文件描述符被抛出。您可以在原始进程中共享文件描述符。补充:
Dup函数:使用dup函数复制原文件描述符指向的内容,使用当前系统(进程)可用的最小文件描述符。示例:先关闭标准输入文件描述符,然后用dup复制一个当前文件描述符,再关闭原文件描述符,完成文件描述符的替换。原型:int dup(int oldfd);返回值:成功:返回一个新的文件描述符。失败:返回-1并设置错误号。Execlp函数:exec函数可以用新进程替换当前进程,新进程与原进程具有相同的PID。参考—— linux系统编程流程(五):exec系列函数(execl、execlp、execv、execvp)使用main5.c
#包括unistd.h
#包含stdlib.h
#包含stdio.h
#包含字符串. h
int main(void) {
int FD[2];
int ret
char buff 1[1024];
char buff 2[1024];
pid _ t pd
ret=管道(FD);
如果(ret!=0) {
printf(创建管道失败!\ n’);
出口(1);
}
PD=fork();
if (pd==-1) {
printf(fork错误!\ n’);
出口(1);
} else if (pd==0) {
//bzero(buff2,sizeof(buff 2));
//sprintf(buff2, %d ,FD[0]);
close(FD[1]);
关闭(0);//关闭标准输入文件描述符
dup(FD[0]);//复制fd[0]并使用最小的可用文件描述符作为该文件描述符。
//也就是说,这个子进程用管道的读取端替换了标准的输入文件描述符。
close(FD[0]);//关闭原始阅读器
execlp(。/od.exe“,”。/od.exe ,-c ,0);
//如果execlp成功执行,将不会执行以下内容
printf(execl错误!\ n’);
出口(1);
}否则{
close(FD[0]);//关闭读取端
//写
strcpy(buff1,你好!);
write(fd[1],buff1,strlen(buff 1));
printf(发送.\ n’);
close(FD[1]);//关闭写入端
}
返回0;
}od.c
#包含stdio.h
#包含stdlib.h
int main(void){
int ret=0;
char buff[80]={0,};
//scanf从标准输入读取——。在这个例子中,它实际上从管道中读取3354。
ret=scanf(%s ,buff);
printf([ret: %d]buff=%s\n ,ret,buff);
ret=scanf(%s ,buff);
printf([ret: %d]buff=%s\n ,ret,buff);//第二次scanf失败,返回-1
返回0;
}
使用popen/pclosepopen的作用:用于在两个进程之间传递数据:在程序a中使用popen调用程序b时,有两种用法:程序a读取程序b的输出(使用fread读取);程序A向程序B发送数据,作为程序B的标准输入(用fwirte编写)。原型:file * popen (constchar * command,const char * type);返回值:成功:返回文件*(文件指针)。失败:返回null。1:读取外部程序# includesdio.h的输出
#包含stdlib.h
#定义BUFF_SIZE 1024
int main(void){
文件*文件;
char BUFF[BUFF _ SIZE 1];
int cnt
//system( ls-l result . txt );
file=popen(ls -l , r );//通过reading读取这个程序ls -l的输出结果。
如果(!File) {//确定打开是否成功。
printf(fopen失败!\ n’);
出口(1);
}
cnt=fread(buff,sizeof(char),BUFF_SIZE,file);//从文件指针读取fread。
if (cnt 0) {
buff[CNT]= \ 0 ;
printf(%s ,buff);
}
pclose(文件);//关闭
返回0;
}
示例:将输出写入外部程序main7.c
#包含stdio.h
#包含stdlib.h
#包含字符串. h
#定义BUFF_SIZE 1024
int main(void){
文件*文件;
char BUFF[BUFF _ SIZE 1];
int cnt
file=popen(。/p2 , w );
如果(!文件){
printf(fopen失败!\ n’);
出口(1);
}
strcpy(buff, hello world!我是123456789testtest!);
cnt=fwrite(buff,sizeof(char),strlen(buff),file);
pclose(文件);
返回0;
}p2.c
#包含stdio.h
#包含stdlib.h
#包含字符串. h
int main(int argc,char* argv[]){
int fd
char buff[1024]={ \ 0 };
int cnt=read(0,buff,sizeof(buff));
if(CNT 0)buff[CNT]= \ 0 ;
printf(receive: %s\n ,buff);
返回0;
}
popen的原理是先用fork创建一个子进程,然后在子进程中用exec执行指定的外部程序,并返回一个指向父进程的文件指针(FILE*)。当使用 r 时,此文件*指向外部进程的标准输出。当使用 w 时,该文件*指向外部程序的标准输入。popen的优缺点:可以使用shell扩展(比如命令中可以使用通配符)。好用。缺点:每次调用popen都会启动两个进程(shell和指定程序)。资源消耗大。
转载请联系作者取得转载授权,否则将追究法律责任。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。