第十五章(一) 进程间通信 之 管道

第十五章    进程间通信

这里先添加之前学习对管道的认识:

管道编程:
        要实现 who | sort 需要两个技巧:
            如何创建管道    +     如何将标准输入和输出通过管道连接起来
        系统调用pipe也使用最低可用文件描述符    
            pipedemo1.c展示了在一个进程中如何使用pipe(玩。。。没有意义,只是show how to use)
        当进程创建一个管道后,该进程就有了连向管道两端的连接,当这个进程调用fork的时候,它的子进程也得到了这两个连向管道的连接。父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口读出。    两个进程都可以读写管道,但是当一个进程读,一个进程写的时候,管道的效率是最高的
            pipedemo_2.c展示了如何在父子进程之间利用管道通信




#define 后面只有一个参数是什么意思呢?
比如像这样:    #define DEBUG
        答案是用作开关的。一般是和 #if(n)def 放在一起使用
        具体看/home/Ben/works.../Sy../Ch..10/define.c



细节:    管道并非文件
    管道是不带有任何结构的字节序列
    1、从管道中读数据
        一、管道读取阻塞:
            当进程试图从管道中读数据时,进程被挂起直到数据被写进管道。
        二、管道的读取结束标志
            当所有的写者关闭了管道的写数据端时,试图从管道读取数据的调用将返回0,意味着文件的结束。
        三、多个读者可能引起的麻烦
            管道是一个队列。当进程从管道中读取数据后,数据就已经不存在了。如果两个进程都试图对同一个管道进行读操作,在一个进程读取一些之后,另一个进程读到的将是后面的内容。他们读到的数据必然是不玩整的,除非某种方法协调他们的访问

    2、向管道写数据:
        一、写入数据阻塞直到管道有空间去容纳新的数据
            当进程想写入1000个字节,而管道现在只能容纳500个字节,那么写入调用就只好等待直到管道中再有500个字节空出来。
        二、写入必须保证一个最小的块的大小
            POSIX标准规定内核不会拆分小于512字节的块。
        三、若无读者在读取数据,则写操作执行失败
            如果所有读者都已经将管道的读取端关闭,那么对管道的写入调用就会执行失败。
            这种情况下,内核会发送 SIGPIPE 给进程。若进程被终止则无事发生。否则write返回-1, 并且errno被置为EPIPE。

只有有共同父进程的进程之间才可以用管道连接。

下面是网上看到的关于管道的一些细节:
    一、 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
    二、如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
    三、如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。



linux下进程间通信的几种主要手段简介:

    管道(Pipe)及有名管道(named pipe):
        管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
    信号(Signal):
        信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
    报文(Message)队列(消息队列):
        消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    共享内存:
        使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
    信号量(semaphore):
        主要作为进程间以及同一进程不同线程之间的同步手段。
    套接口(Socket):    
        更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。


管道是一种特殊的文件,其不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构
    管道亦可分为有名管道 和 无名管道
        无名管道:
            主要用于父进程与子进程之间,或者两个兄弟进程之间。在linux系统中可以通过系统调用建立起一个单向的通信管道,且这种关系只能由父进程来建立。因此,每个管道都是单向的,当需要双向通信时就需要建立起两个管道。管道两端的进程均将该管道看做一个文件,一个进程负责往管道中写内容,而另一个从管道中读取。这种传输遵循“先入先出”(FIFO)的规则。
        命名管道:
            命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。


FIFO 与 PIPE 的限制:

• 它们是半双工的(单向性),即数据只能在一个方向上流动。由进程A流向进程B或由进程B流向进程A。
• PIPE的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承PIPE文件描述符。FIFO可以实现无关进程间的通信。
• 一个进程在任意时刻打开的最大文件描述符个数OPEN_MAX(通过调用sysconf(_SC_OPEN_MAX)获得)
• 可原子地写往PIPE或FIFO的最大数据量PIPE_BUF(通常定义在limits.h)

    

下面主要关于命名管道的一些 注意事项
    阻塞(缺省/默认设置):
        只读open
        • FIFO已经被只写打开:成功返回
        • FIFO没有被只写打开:阻塞到FIFO被打开来写
        只写open
        • FIFO已经被只读打开:成功返回
        • FIFO没有被只读打开:阻塞到FIFO被打开来读
        从空PIPE或空FIFO中read
        • FIFO或PIPE已经被只写打开:阻塞到PIPE或FIFO中有数据或者不再为写打开着
        • FIFO或PIPE没有被只写打开:返回0(文件结束符)
        write
        • FIFO或PIPE已经被只读打开:
        写入数据量不大于PIPE_BUF(保证原子性):有足够空间存放则一次性全部写入,没有则进入睡眠,直到当缓冲区中有能够容纳要写入的全部字节数时,才开始进行一次性写操作
        写入数据量大于PIPE_BUF(不保证原子性):缓冲区一有空闲区域,进程就会试图写入数据,函数在写完全部数据后返回
        • FIFO或PIPE没有被只读打开:给线程产生SIGPIPE(默认终止进程)

    O_NONBLOCK设置:
        只读open
        • FIFO已经被只写打开:成功返回
        • FIFO没有被只写打开:成功返回
        只写open
        • FIFO已经被只读打开:成功返回
        • FIFO没有被只读打开:返回ENXIO错误
        从空PIPE或空FIFO中read
        • FIFO或PIPE已经被只写打开:返回EAGAIN错误
        • FIFO或PIPE没有被只写打开:返回0(文件结束符)
        write
        • FIFO或PIPE已经被只读打开:
        写入数据量不大于PIPE_BUF(保证原子性):有足够空间存放则一次性全部写入,没有则返回EAGAIN错误(不会部分写入)
        写入数据量大于PIPE_BUF(不保证原子性):有足够空间存放则全部写入,没有则部分写入,函数立即返回
        • FIFO或PIPE没有被只读打开:给线程产生SIGPIPE(默认终止进程)



下面即为现在所学:
管道:
    有两种局限性:
        1、历史上管道是半双工的,但现在某些系统支持全双工,但为了移植性,可能要有所妥协。
        2、管道只能在具有公共祖先的两个进程之间使用。通常一个管道由一个进程创建,在进程调用fork后,这个管道就在父子进程间使用

    接下来遇到的FIFO没有局限性2, 套接字没有局限性1和2

    函数    int pipe(int fd[2])
        返回两个文件描述符,fd[0]为读而打开, fd[1]为写而打开

    当管道的一端被关闭后,则有下列两条规则:
        1、当读一个写端已经关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束
        2、如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或捕捉该信号并从其处理程序返回,则write返回 -1,errno返回 EPIPE。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define oops(m,x) {perror(m);exit(x);}

int main(int argc,char *argv[])
{
        if(argc != 3)
        {
                fprintf(stderr,"Usage: pipe command1 command2\n");
                exit(1);
        }

        int pipefd[2];
        int childpid;

        if(pipe(pipefd) == -1)
                oops("Cannot get a pipe",1);
        printf("I get %d and %d\n",pipefd[0],pipefd[1]);
        if((childpid=fork()) == -1)
                oops("Fork error",1);

        //Deal with argv[1]     
        if(childpid > 0)                //exec command1 to cover the parent process or you can say the parent process is used to run command1.
                                        //command1 aims to write into the pipe
        {
                close(pipefd[0]);       //so this fd is not necessary
                if(dup2(pipefd[1],1) == -1)
                        oops("couldn't redirect stdout",2);
                close(pipefd[1]);       //Since we have duplicated it,close it.
                execlp(argv[1],argv[1],NULL);
                oops("execvp",3);
        }
        printf("%d and %d--------\n",pipefd[0],pipefd[1]);
        //Deal with argv[2]
        close(pipefd[1]);
        if(dup2(pipefd[0],0) == -1)
                oops("couldn't redirect stdin",4);
        close(pipefd[0]);
        printf("child's output : \n");
        execlp(argv[2],argv[2],NULL);
        oops("execlp",5);
        return 0;
}



    结果为:

$ ./pipe who cat
I get 3 and 4
3 and 4--------
child's output :
XXX      tty1         2015-02-13 13:34
XXX      pts/0        2015-02-13 14:34 (:0.0)
$



    程序将父进程的标准输出改为管道的写端,将子进程的标准输入改为管道的读端;两个进程各自运行一个程序,最后结果正如预料的那样

    接下来,我们还可以通过管道实现之前所说的TELL_WAIT系列函数,之前有一个用信号实现的版本,这里通过管道实现

static int pfd1[2], pfd2[2];

void TELL_WAIT(void)
{
    if(pipe(pfd1)<0 || pipe(pfd2)<0)
        exit(1);
}

void TELL_PARENT()
{
    if(write(pfd2[1],"c",1) != 1)
        exit(1);
}

void WAIT_PARENT()
{
    char c;

    if(read(pfd1[0],&c,1) != 1)
        exit(1);
    
    if(c != 'p')
        exit(1);
}

void TELL_CHILD()
{
    if(write(pfd1[1],"p",1) != 1)
        exit(1);
}

void WAIT_PARENT()
{
    char c;

    if(read(pfd2[0],&c,1) != 1)
        exit(1);
    
    if(c != 'c')
        exit(1);
}



    管道在没有读到字符时会阻塞。
    在这里我们并没有关闭父进程的pfd[1]的读端,(其他该关的都没关),但这并不影响。因为该关的端并没有被使用。




由于我们经常需要:
    创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了以下两个函数,可相应的减少代码量
函数    FILE* popen(char *cmdstring, char *type);
    int pclose(FILE *fp);
    popen是如何工作的呢?
        1、需要一个新的进程来运行程序,所以要fork
        2、需要一个指向该进程的连接,需要管道pipe
        3、使用fdopen来将一个文件描述符定向要缓冲流中
        4、要能运行shell命令,需要exec

    根据以上的提示,自己也可实现:
#include <stdio.h>

#define READ    0
#define WRITE   1

FILE* popen(const char *command,const char *mode)
{
        int pfp[2];
        int pid;
        FILE *fp;
        int parent_end,child_end;

        if(*mode == 'r')
    //
        {
                parent_end = READ;
                child_end = WRITE;              //if mode is 'r',it means we would read from this fp,so child process has to write into pipe.
        }
        else if(*mode = 'w')
        {
                parent_end = WRITE;
                child_end  = READ;              //we would write into this pipe for child process to read.
        }
        else
                return NULL;

        if(pipe(pfp) == -1)
                return NULL;

        if((pid=fork()) == -1)
        {
                close(pfp[0]);
                close(pfp[1]);
                return NULL;
        }
        //---------------------------------***--------------------------------parent code
        if(pid > 0)
        {
                close(pfp[child_end]);
                return fdopen(pfp[parent_end],mode);
        }
        //--------------------------------***------------------------------child code
        else
        {
                if(close(pfp[parent_end]) == -1)  
                        exit(1);
                if(dup2(pfp[child_end],child_end) == -1)
                        exit(1);
                if(close(pfp[child_end]) == -1)
                        exit(1);

                execl("/bin/sh","sh","-c",command,NULL);
                exit(1);
        }
}



    即:
        如果 type 是“r”, 则文件指针连接到cmdstring的标准输出,即返回的文件指针是可读的
        如果 type 是“w”, 则文件指针连接到cmdstring的标准输入,即返回的文件指针是可写的

    使用popen的例子:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int main()
{
        int c;

        while((c=getchar()) != EOF)
        {
                if(isupper(c))
                        c = tolower(c);
                if(putchar(c) == EOF)
                        exit(1);
                if(c == '\n')
                        fflush(stdout);
        }
        return 0;
}



    编译生成 myuclc 可运行文件;    

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

#define MAXSIZE 4096

int main()
{
        char line[MAXSIZE];
        FILE *fpin;

        if((fpin = popen("/home/XXX/myuclc","r")) == NULL)
                exit(1);

        for(;;)
        {
                fputs("prompt > ",stdout);
                fflush(stdout);
                if(fgets(line,MAXSIZE,fpin) == NULL)
                        break;

                if(fputs(line, stdout) == EOF)
                        exit(1);
        }

        if(pclose(fpin) == -1)
                exit(1);

        putchar('\n');
        return 0;
}


    
    结果为:
$ ./popen
prompt > hehe
hehe
prompt > HEHE
hehe
prompt > DSKNLFA
dsknlfa
prompt >
$






协同进程:
    对于书上的协同进程
        即使用两个管道,父进程通过第一个管道向子进程输送数据,子进程处理数据后通过第二个管道向父进程输送处理过后的数据。
    这里我拿出一个实现小型 bc 的例子:

//Here we want to make a small bc with two pipes and dc
//bc is used to address the input and get the result
//dc is used to deal with the calculate process and produce the result

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define oops(m,x) {perror(m);exit(x);}

int main()
{
    void be_dc(int*,int*);
    void be_bc(int *,int *);

    int tofd[2],fromfd[2];
    int pid;

    if(pipe(tofd)==-1 || pipe(fromfd)==-1)
        oops("Pipe fail",1);

    if((pid=fork()) == -1)
        oops("Fork fail",2);

    if(pid == 0)
        be_dc(tofd,fromfd);
    else
    {
        be_bc(tofd,fromfd);
        wait();
    }

    return 0;
}

void be_dc(int in[2],int out[2])
{
    dup2(in[0],0);
    close(in[0]);
    close(in[1]);

    dup2(out[1],1);
    close(out[0]);
    close(out[1]);

    execlp("dc","dc","-",NULL);
    oops("dc failed",3);
}

void be_bc(int in[2],int out[2])
{
    int num1,num2;

    char message[BUFSIZ];
    char operation;
    FILE *fpin,*fpout;

    close(in[0]);                                    //unused
    close(out[1]);

    if((fpin=fdopen(out[0],"r")) == NULL)                        //Why do we have to open file stream?
        oops("fdopen",4);                            //Cause if not, we have to use write and read
    if((fpout=fdopen(in[1],"w")) == NULL)                        //but this two cannot satisfy the needs of dc
        oops("fdopen",5);                            //we have to write into the pipe formally
    
    while(printf("tinybc: ") && fgets(message,BUFSIZ,stdin)!=NULL)
    {
        if(sscanf(message,"%d%[+-*/^]%d",&num1,&operation,&num2) != 3)        //deal with the format
        {
            fprintf(stderr,"Wrong write");
            continue;
        }

        if(fprintf(fpout,"%d\n%d\n%c\np\n",num1,num2,operation) == EOF)        //write several elements into the pipe
            oops("fprintf",6);

        fflush(fpout);                    //try to erase this line
        if(fgets(message,BUFSIZ,fpin) == NULL)                    //get the result
            break;
        printf("%d %c %d = %s\n",num1,operation,num2,message);
    }

    fclose(fpin);                                    //remember this!
    fclose(fpout);
}
    
   

    结果为:
$ ./a.out
tinybc: 3*7
3 * 7 = 21

tinybc: ^X



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值