管道专题

linux管道基本概念


管道基本概念及操作

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,下面是一个简单的例子.
ls | wc -l ps –u wbm01|grep “aa”

** 管道的本质:**

固定大小的内核缓冲区

管道限制:

管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

匿名管道pipe

功能:创建一无名管道

原型:

#include 
int pipe(int fd[2]);

参数:

fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

返回值:成功返回0,失败返回错误代码

管道创建后示意图

管道专题_第1张图片
管道专题_第2张图片

管道基本编程示例

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

//这个例子中,父进程如果读不到数据会阻塞到read函数那里
int main01(void)
{
    int pipefd[2];
    pid_t pid;

    //创建管道文件,并打开
    if (pipe(pipefd) == -1)
    {
        printf("pipe() err..\n");
        return -1;
    }
    pid = fork();
    if (pid == -1)
    {
        printf("fork err..\n");
        return -1;
    }

    if (pid == 0)
    {
        close(pipefd[0]); //子进程关闭读管道
        write(pipefd[1], "hello hello....", 6);//向管道中写数据
        close(pipefd[1]);
        printf("child .....quit\n");

    }
    else if (pid > 0)
    {
        int len = 0;
        char buf[100] = { 0 };
        close(pipefd[1]); //父进程关闭写管道
        len = read(pipefd[0], buf, 100); //从管道中读数据
        printf("len = %d, buf:%s \n", len, buf);

        close(pipefd[0]);
    }

    wait(NULL);
    printf("parent ..quit\n");
    return 0;
}

//阻塞非阻塞场景
int main02(void)
{
    int ret = 0;
    int pipefd[2];

    ret = pipe(pipefd);
    if (ret < 0)
    {
        perror("ddd");
        exit(0);
    }
    int pid;
    pid = fork();
    if (pid == -1)
    {
        perror("dd");
        exit(0);
    }

    if (pid == 0) //子进程
    {
        close(pipefd[0]);
        sleep(5);
        write(pipefd[1], "childaaaaaaaaa", 6);
        printf("chile quit\n");
        close(pipefd[1]);
        exit(0);
    }

    close(pipefd[1]);
    char readbuf[1000] = { 0 };
    int n = 0;

    int flags = fcntl(pipefd[0], F_GETFL); 
    flags = flags | O_NONBLOCK;
    ret = fcntl(pipefd[0], F_SETFL, flags); //用fcntl函数可以设置文件描述符阻塞或者非阻塞
    if (ret == -1)
    {
        printf("fcntl err.\n");
        exit(0);
    }

    n = read(pipefd[0], readbuf, sizeof(readbuf));
    if (n < 0)
    {
        printf("read非阻塞返回。。。。。立马返回");
        //errno
    }
    printf("%s\n", readbuf);
    close(pipefd[0]);

    wait(NULL);

    printf("parent quit\n");
}

//当所有的写端被关闭,此时读的话会返回0
int main(void)
{
    int ret = 0;
    int pipefd[2];

    ret = pipe(pipefd);
    if (ret < 0)
    {
        perror("ddd");
        exit(0);
    }
    int pid;
    pid = fork();
    if (pid == -1)
    {
        perror("dd");
        exit(0);
    }

    if (pid == 0) //子进程
    {
        close(pipefd[0]);
        close(pipefd[1]);//写端被关闭
        exit(0);
    }

    close(pipefd[1]); //写端被关闭
    char readbuf[1000] = { 0 };
    int n = 0;
    sleep(3);

    n = read(pipefd[0], readbuf, sizeof(readbuf));
    printf("所有的写端都关闭,此时 n = %d \n", n);
    //当所有的写端被关闭,此时读的话会返回0
    printf("%s\n", readbuf);
    close(pipefd[0]);

    wait(NULL);

    printf("parent quit\n");
}

管道和文件描述符在一起

管道命令的编程实现:

ls | wc –w

从下面的这个例子里,我们可以更加深刻地领会|符号的实现原理。

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main21(void)
{
    int pipefd[2];
    pid_t pid;
    if (pipe(pipefd) == -1)
    {
        printf("pipe() err..\n");
        return -1;
    }
    pid = fork();
    if (pid == -1)
    {
        printf("fork err..\n");
        return -1;
    }
    if (pid == 0)//子进程
    {
        close(pipefd[0]);
        //复制文件描述符pipefd[1],给标准输出,言外之意:execlp的ls命令输出到管道中
        dup2(pipefd[1], STDOUT_FILENO);
        //这里需要注意一点,那就是复制后,pipefd[1]的应用计数会增加,下面关闭pipefd[1]不会真正关闭该文件
        close(pipefd[1]);//关闭写端

        execlp("ls", "ls", NULL); 
        //如果替换新的进程印象失败,则会执行下面一句话    
        sprintf(stderr, "execute the cmd ls err..\n");
        exit(0);

    }
    else if (pid > 0) //父进程
    {
        int len = 0;
        char buf[100] = { 0 };
        close(pipefd[1]);
        //复制文件描述符pipefd[0],给标准输入,言外之意:execlp的wc命令从管道中读
        dup2(pipefd[0], STDIN_FILENO);
        close(pipefd[0]);
        //len = read(pipefd[0], buf, 100);
        execlp("wc", "wc", "-w", NULL);//执行wc命令,相当于从
        printf("len:%d, buf:%s \n", len, buf);

        //close(pipefd[0]);
    }

    wait(NULL);
    printf("parent ..quit\n");
    return 0;
}


int main(int argc, char *argv[])
{
    close(0); //关闭标准输入
    open("makefile", O_RDONLY); //makefile文件变成标准输入
    close(1);//关闭标准输出
    open("makefile2", O_WRONLY | O_CREAT | O_TRUNC, 0644); //maifle2变成标准输出

    execlp("cat", "cat", NULL); //替换进程印象后,执行cat命令

    //cat命令 从标准输入中按行读,紧接着写到标准输出
    return 0;

}

dup2函数:

作用:

dup2函数使得newfd成为oldfd,即将oldfd的值赋给newfd

函数原型:

int dup2(int oldfd, int newfd);

参数:两个都为文件描述符

返回值:如果成功,返回新的描述符,否则返回-1,设置errno

管道的本质是内核的一块缓冲区。

管道的读写规则

当没有数据可读时

O_NONBLOCK disableread调用阻塞,即进程暂停执行,一直等到有数据来到为止。

O_NONBLOCK enableread调用返回-1errno值为EAGAIN
当管道满的时候

O_NONBLOCK disablewrite调用阻塞,直到有进程读走数据

O_NONBLOCK enable:调用返回-1errno值为EAGAIN

当管道不停的被写,写满的时候

O_NONBLOCK disablewrite调用阻塞

O_NONBLOCK enable:调用返回-1errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE。也就是所谓的管道破裂,这在socket编程中很常见。

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

下面是一个例子:

#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

#define TEST_SIZE 68*1024   //68K


int main(void)
{
    char a[TEST_SIZE];
    char b[TEST_SIZE];

    memset(a, 'A', sizeof(a));
    memset(b, 'B', sizeof(b));

    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1)
        ERR_EXIT("pipe error");

    pid_t pid;
    pid = fork();
    if (pid == 0) //A子进程写68K数据A
    {
        close(pipefd[0]);
        ret = write(pipefd[1], a, sizeof(a));
        printf("apid=%d write %d bytes to pipe\n", getpid(), ret);
        exit(0);
    }

    pid = fork();


    if (pid == 0) //B子进程写68K数据B
    {
        close(pipefd[0]);
        ret = write(pipefd[1], b, sizeof(b));
        printf("bpid=%d write %d bytes to pipe\n", getpid(), ret);
        exit(0);
    }


    close(pipefd[1]);

    sleep(1);
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    char buf[1024 * 4] = { 0 };
    int n = 1;
    while (1)
    {
        //父进程4k 4k的读数据,发现AB进程是交叉的写数据到管道。
        //多个进程往管道里面,写数据。
        ret = read(pipefd[0], buf, sizeof(buf));
        if (ret == 0)
            break;
        printf("n=%02d pid=%d read %d bytes from pipe buf[4095]=%c\n", n++, getpid(), ret, buf[4095]);
        write(fd, buf, ret);

    }
    return 0;
}

上面其实是一个很恐怖的东西,我用vim处理了一下test.txt我们来看一下这个文件中的数据。我做了如下处理:

70A为一行,后面加了换行符。每70B为一行,后面加了换行符。

AB之间加入换行符。

A一直持续到了937行!

我们计算一下:936 * 70 + 16 = 65536,一个字符一个字节,恰好64K

B写入了(995 – 936 + 1) * 70 + 36 = 4236,后面的写入就有一些乱了。

后面全部是B

从上面的结果可以看到,如果写入的数据量很大的话,其实挺乱的,这个东西。

你可能感兴趣的:(管道专题)