管道的读写行为

使用管道需要注意以下4种特殊情况(默认都是阻塞I/O操作,没有设置O_NONBLOCK标志):

1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。

4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

总结:

读管道:1.管道中有数据,read返回实际读到的字节数。2.管道中无数据:管道写端被全部关闭,read返回0(好像读到文件结尾);写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)。

写管道:1.管道读端全部被关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)。2. 管道读端没有全部关闭:管道已满,write阻塞;管道未满,write将数据写入,并返回实际写入的字节数。

重点注意:

如果写入的数据大小n<=PIPE_BUF时,linux保证写入的原子性,即要么不写,要么全写入。如果没有足够的空间供n个字节全部写入,则会阻塞直到有足够空间供n个字节全部写入;如果写入的数据大小n>PIPE_BUF时,写入不再具有原子性,可能中间有其它进程穿插写入,其自身也会阻塞,直到将n字节全部写入在才返回写入的字节数,否则阻塞等待。

读数据时,如果请求读取的数据(read函数的缓冲区)大小>=PIPE_BUF,则直接返回管道中现有的数据字节数(即将管道中的数据全部读出);如果< PIPE_BUF,则返回管道中现有的数据字节数(此时管道中的实际数据量<=请求的数据量大小),或者返回请求数据量的大小。

练习1:父子进程使用管道通信,父写入字符串,子进程读出并打印到屏幕。

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

int main(void)
{
    int ret,fd1;
    char *p="zhangshuxiong\n";
    int fd[2];
    ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(1);
    }

    fd1 = fork( );
    if(fd1 == -1)
    {
        perror("fork");
        exit(1);
    }else if(fd1 == 0) {
        sleep(3);   //子进程睡3秒
        close(fd[1]);  //子进程关闭写端
        char buff[1024]={0};
        ret =  read(fd[0],buff,1024);  //子进程读数据
        if(ret == -1)
        {
            perror("read");
            exit(1);
        }else if(ret == 0) {
            printf("父进程没有向管道里写入数据\n");
        }else {
            int res= write(STDOUT_FILENO,buff,ret);  //将读出的数据输出到屏幕
            if(res == -1)
            {
                perror("write");
                exit(1);
            }
        }
        close(fd[0]);  //子进程结束前关闭掉文件描述符
    }else {
        close(fd[0]);
        int rer = write(fd[1],p,strlen(p));  //父进程写入数据
        if(rer == -1)
        {
            perror("write");
            exit(1);
        }
        close(fd[1]);  //父进程结束前关闭掉文件描述符
        wait( NULL );  //父进程回收(阻塞等待)
    }

    return 0;
}

[root@localhost pipe]# ./pip

zhangshuxiong

[root@localhost pipe]#     //可见,如果没有wait,则父进程会先结束,正因为有了wait,父进程会等待子进程结束,最后shell进程才会收回前台,等待与用户交互。注意,即使没有sleep函数,依然能保证子进程运行时一定会读到数据,因为是阻塞读。

 

练习2:使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现ls,子进程实现wc

[root@localhost pipe]# ls

makefile  pip  pip.c  pipe  pipe1  pipe1.c  pipe2  pipe2.c  pipe3  pipe3.c  pipe.c  pipe_test  pipe_test.c  test

[root@localhost pipe]# ls | wc –l   //统计文件的字数

14

其实 ls | wc –l命令执行后,shell进程会创建两个子进程,并创建一个管道,用于两子进程通信,下面给出详细实现过程

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

int main(void)
{
    int ret,fd1;
    int fd[2];
    ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(1);
    }

    fd1 = fork( );
    if(fd1 == -1)
    {
        perror("fork");
        exit(1);
    }else if(fd1 == 0) {
        close(fd[1]);
        int as = dup2(fd[0],STDIN_FILENO);  //将标准输入重定向到管道读端
        if(as == -1)
        {
            perror("dup2");
            exit(1);
        }
        close(fd[0]);  //只是关了fd[0],不关也可以,进程结束会自动关闭
        execlp("wc","wc","-l",NULL);  //该命令从标准输入读取文本
    }else {
        close(fd[0]);
        int as = dup2(fd[1],STDOUT_FILENO);  //将标准输出重定向到管道写端
        if(as == -1)
        {
            perror("dup2");
            exit(1);
        }

        execlp("ls","ls",NULL);  ///该命令结果会写到标准输出
    }

    return 0;
}

[root@localhost pipe]# ./pip

14                      //可见,跟ls | wc –l的结果一样

注意,上述程序并没有考虑到子进程的回收问题,如果父进程比子进程先结束,子进程会被init进程回收;后结束,子进程会先变为僵尸进程,等父进程结束了,再被init进程回收。

ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc –l 正常应该从stdin读取数据,但此时会从管道的读端读。

也有可能会出现这种情况:程序执行,发现程序执行结束,shell还在阻塞等待用户输入。这是因为,shell → fork → ./pipe1, 程序pipe1的子进程将stdin重定向给管道,父进程执行的ls会将结果集通过管道写给子进程。若父进程在子进程打印wc的结果到屏幕之前被shell调用wait回收,shell就会先输出$提示符。

 

练习3使用管道实现兄弟进程间通信。 兄:ls  弟: wc -l  父:等待回收子进程。要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(void)
{
    int i,ret,fd1;
    int n=2;
    int fd[2];
    ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(1);
    }

    for(i=0;i<n;i++)
    {
        fd1 = fork( );
        if(fd1 == -1)
        {
            perror("fork");
            exit(1);
        }else if(fd1 == 0)
            break;
    }

    if(i == n)
    {
        close(fd[0]);
        close(fd[1]);  //特别强调,父进程不用管道,必须要关掉,否则运行出错(为了维护管道的单向通信)
        int status;
        do {
        pid_t pid=waitpid(-1,&status,0);
        if(pid > 0)
            n--;
        if(pid == -1)
        {
            perror("waitpid");
            exit(1);
        }

        if(WIFEXITED(status))
            printf("the child process of exit with %d\n",WEXITSTATUS(status));
        else if(WIFSIGNALED(status))
            printf("the child process was killed by %dth signal\n",WTERMSIG(status));
        }while(n>0);
    }else if(i == 1) {
        close(fd[1]);
        int as = dup2(fd[0],STDIN_FILENO);
        if(as == -1)
        {
            perror("dup2");
            exit(1);
        }
        close(fd[0]);
        execlp("wc","wc","-l",NULL);
    }else {
        close(fd[0]);
        int as = dup2(fd[1],STDOUT_FILENO);
        if(as == -1)
        {
            perror("dup2");
            exit(1);
        }

        execlp("ls","ls",NULL);
    }

    return 0;
}

[root@localhost pipe]# ./pip

14

the child process of exit with 0

the child process of exit with 0

强调一点:在使用管道传递数据之前,不用的管道读或写端都必须要关闭,这是为了维护管道的正常运行(单向通信)。

 

测试:是否允许,一个pipe有一个写端,多个读端呢?是否允许有一个读端多个写端呢?

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
        pid_t pid;
        int fd[2], i, n;
        char buf[1024];

        int ret = pipe(fd);
        if(ret == -1){
                perror("pipe error");
                exit(1);
        }

        for(i = 0; i < 2; i++){
                if((pid = fork()) == 0)
                        break;
                else if(pid == -1){
                        perror("pipe error");
                        exit(1);
                }
        }

        if (i == 0) {
                close(fd[0]);
                write(fd[1], "1.hello\n", strlen("1.hello\n"));
        } else if(i == 1) {
                close(fd[0]);
                write(fd[1], "2.world\n", strlen("2.world\n"));
        } else {
                close(fd[1]);       //父进程关闭写端,留读端读取数据    
                //sleep(1);   //这条语句是很关键的
                n = read(fd[0], buf, 1024);     //从管道中读数据
                write(STDOUT_FILENO, buf, n);

                for(i = 0; i < 2; i++)          //两个儿子wait两次
                        wait(NULL);
        }

        return 0;
}

如果父进程不睡眠:

[root@localhost pipe]# ./pipe3

2.world

1.hello

[root@localhost pipe]# ./pipe3

1.hello

[root@localhost pipe]# ./pipe3

2.world

可见:三个进程的执行顺序是随机的,如果两个子进程在父进程读之前,都先写入,那么两个都会读出。为了确保两个都读出,可以使用读两次的方法,也可以让父进程先睡眠一会,如下:

如果父进程睡眠:

[root@localhost pipe]# ./pipe3

1.hello

2.world

[root@localhost pipe]# ./pipe3

1.hello

2.world

 

最终练习:统计当前系统中进程ID大于10000的进程个数。

提示: 采用awk命令,可以统计文本中符合条件列的个数及和。运用ps aux和管道。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值