管道

发布时间 2023-09-10 21:24:27作者: 常羲和

管道

1.管道pipe

1.1 管道概述

管道(pipe)又称无名管道

int fd[2];fd[0]读,fd[1]写

无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符

特点:

1.半双工,数据在同一时刻只能在一个方向上流动

2.数据只能从管道的一端写入,从另一端读出

3.写入管道中的数据遵循先入先出的规则(FIFO)

4.管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式

5.管道不是普通的文件,不属于某个文件系统,其只存在于内存中

6.管道在内存中对应一个缓冲区,不同的系统其大小不一定相同。

7.从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据

8.管道没有名字,只能在具有公共祖先的进程之间使用

1.2 创建pipe

#include <unistd.h>
int pipe(int filedes[2]);

功能:经由参数filedes返回两个文件描述符

filedes[0]为读而打开,filedes[1]为写而打开管道
filedes[0]的输出是filedes[1]的输入

返回值:成功返回0,失败返回-1

【注意】利用无名管道实现进程间的通信,都是父进程创建无名管道,然后再创建子进程,子进程继承父进程的无名管道的文件描述符,然后父子进程通过读写无名管道实现通信

1.3 读写数据的特点

1.默认用read函数从管道中读数据是阻塞的
2.调用write函数向管道里写数据,当缓冲区已满时write也会阻塞
3.通信过程中,读端口全部关闭后,写进程向管道内写数据时,写进程会(收到SIGPIPE信号)退出。

编程时可通过fcntl函数设置文件的阻塞特性

设置为阻塞:fcntl(fd,FSETFL,0);
设置为非阻塞:fcntl(fd,FSTFL,O_NONBLOCK);

2. 文件描述符复制

2.1 文件描述符概述

文件描述符是非负整数,是文件的标识

用户使用文件描述符(file descriptor)来访问文件

利用open打开一个文件时,内核会返回一个文件描述符

每个进程都有一张文件描述符的表,进程刚被创建时,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符0、1、2记录在表中

在进程中打开其他文件时,系统会返回文件描述表中最小可用的文件描述符,并将此文件描述符记录在表中

【注意】Linux中一个进程最多只能打开NR_OPEN_DEFAULT(即1024)个文件,故当文件不再使用时应及时调用close函数关闭文件

2.2 dup和dup2函数

dup和dup2是两个非常有用的系统调用,都是用来复制一个文件的描述符,使新的文件描述符也标识旧的文件描述符所标识的文件。

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);

dup和dup2经常用来重定向进程的stdin、stdout和stderr。

2.2.1 dup应用

int dup(int oldfd)

复制oldfd文件描述符,并分配一个新的文件描述符,新的文件描述符是调用进程文件描述符表中最小可用的文件描述符

成功:新文件描述符

失败:返回-1,错误代码存于errno中

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
   int fd = open("a.txt", O_WRONLY | O_CREAT |    O_APPEND, 0755);
   printf("新打开的fd = %d\n", fd);
   close(1);
   // 复制fd为一个新的文件描述符(最小的,1)
   int newfd = dup(fd); // 1 -> a.txt
   printf("newfd = %d\n", newfd);
   printf("hahah\n");
   close(fd);
   printf("yes\n");
   return 0;
}

2.2.2 dup2应用

int dup2(int oldfd,int newfd)

复制一份打开的文件描述符oldfd,并分配新的文件描述符newfd,newfd也标识oldfd所标识的文件。

oldfd要复制的文件描述符,newfd分配的新的文件描述符

成功:返回newfd

失败:返回-1,错误代码存于errno中

【注意】newfd是小于文件描述符最大允许值的非负整数,如果newfd是一个已经打开的文件描述符,则首先关闭该文件,然后再复制

如:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
   int fd = open("a.txt", O_WRONLY | O_CREAT |    O_APPEND, 0755);
   printf("新打开的fd = %d\n", fd);
   // dup2() 先关闭1,再复制fd为1
   int newfd = dup2(fd, 1); // 1 -> a.txt
   close(fd);
   printf("newfd = %d\n", newfd);
   printf("no\n");
   printf("good\n");
   return 0;
}

2.3 复制的新旧文件描述符的特点

复制文件描述符后新旧文件描述符的特点:

1)使用dup或dup2复制文件描述符后,新文件描述符和旧文件描述符指向同一个文件,共享文件锁定读写位置和各项权限
2)当关闭新的文件描述符时,通过旧文件描述符仍可操作文件
3)当关闭旧的文件描述符时,通过新文件描述符仍可操作文件

exec前后文件描述符的特点:

1)close_on_exec标志决定了文件描述符在执行exec后文件描述符是否可用
2)文件描述符的close_on_exec标志默认是关闭的,即文件描述符在执行exec后文件描述符是可用的。
3)若没有设置close_on_exec标志位,进程中打开的文件描述符,及其相关的设置在exec后不变,可供新启动的程序使用

设置close_on_exec标志位的方法:

int flags;
flags = fcntl(fd,F_GETFD);//获得标志
flags |= FD_CLOEXEC;//打开标志位
flags &= ~FD_CLOEXEC;//关闭标志位
fcntl(fd,F_SETFD,flags);//设置标志

3. 案例

1. 父子进程实现命令中管道的功能,如:ps -A|grep bash

image-20230908082216147

代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char const *argv[])
{
    int fd[2];
    if(pipe(fd)==-1)
    {
        perror("pipe");
        return 1;
    }
    int pid = fork();
    if(pid==0)
    {
        close(fd[1]);
        dup2(fd[0],0);//0->fd[0]
        execlp("grep","grep","bash",NULL);
        close(fd[0]);
        _exit(0);
    }
    else if(pid>0)
    {
        close(fd[0]);
        //将命令执行的结果写入到管道
        dup2(fd[1],1);//1->fd[1]
        execlp("ps","ps","-A",NULL);
        close(fd[1]);
    }
    return 0;
}

2. 借用外部命令,实现计算器功能

【提示】

expr是个外部命令,它向标准输出打印运算结果
创建一个管道以便让expr4+5的输出到管道中
子进程exec执行expr4+5命令之前重定向“标准输出”到“管道写端”
父进程从管道读端读取数据,并显示运算结果

image-20230908083200017

代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc ,char const *argv[])
{
    int fd[2];
    if(pipe(fd)==-1)
    {
        perror("pipe");
        return 1;
    }
    int pid = fork();
    if(pid == 0)
    {
        close(fd[0]);//关闭读
        //重定向标准输出到管道
        dup2(fd[1],1);
       execlp("expr","expr","4","+","5",NULL);
        close(fd[1]);
        _exit(0);
    }
    else if(pid>0)
    {
        close(fd[1]);
        char buf[10]="";
        int len = read(fd[0],buf,10);
        buf[len-1]='\0';
        printf("读取子进程发送的数据:%s\n",buf);
        close(fd[0]);
    }
    return 0;
}

4. 命名管道(FIFO)

4.1 FIFO概述

命名管道(FIFO)和管道(pipe)基本相同,但也有一些显著的不同。

特点:

1.半双工,数据在同一时刻只能在一个方向上流动

2.写入FIFO中的数据遵循先入先出的规则

3.FIFO所传送的数据是无格式的,这要求FIFO的读出方与写入方必须事先约定好数据的格式

4.FIFO在文件系统中作为一个特殊的文件存在,但FIFO中的内容缺存放在内存中

5.管道在内存中对应一个缓冲区,不同的系统其大小不一定相同

6.从FIFO读数据是一次性操作,数据一旦被读,它就从FIFO中被抛弃,释放空间以便写更多的数据

7.当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用

8.FIFO有名字,不相关的进程可以通过打开命名管道进行通信

4.2 操作FIFO文件时的特点

系统调用的I/O函数都可作用于FIFO,如open,close,read,write等。打开FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响。

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char*pathname,mode_t mode);

参数:pathname文件路径名,mod是文件的读写执行,(r,w,x)相关的权限(可以使用0 ddd数值表示,如0755)

返回值:0成功,非0失败

【注】一个有名管道只能创建一次

4.2.1 特点1-不指定O_NONBLOCK

不指定O_NONBLCOK(即open没有位或O_NONBLOCK)

1. open以只读方式打开FIFO时,要阻塞到某个进程为写而打开此FIFO
2. open以只写方式打开FIFO时,要阻塞到某个进程为读而打开此FIFO
3. open以只读、只写方式打开FIFO时会阻塞,调用read函数从FIFO里读数据时,read也会阻塞
4. 通信过程中若写进程先退出了,则调用read函数从FIFO里读数据时不阻塞;若写进程又重写运行,则调用read函数从FIFO里读数据时又恢复阻塞
5. 通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE信号)退出
6. 调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞

4.2.2 特点2-指定O_NONBLOCK

指定O_NONBLOCK(即open位或O_NONBLOCK)

1. 先以只读方式打开,如果没有进程已经为写而打开一个FIFO,只读open成功,并且open不阻塞
2. 先以只写方式打开,如果没有进程已经为读而打开一个FIFO,只写open将出错返回-1
3. read、write读写命名管道中读数据时不阻塞
4. 通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE信号)退出

4.2.3 可读可写方式打开FIFO文件时的特点

1. open不阻塞
2. 调用read函数从FIFO里读数据时read会阻塞
3. 调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞

4.3 实现单机聊天程序

image-20230908134959676

如bbs_bob.c

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc,char const *argv[])
{
#ifdef CREATE_FIFO
    mkfifo("bob_2_lucy",0755);
    mkfifo("lucy_2_bob",0755);
#endif
    int i=0;
    for(;i<2;i++)
    {
        int pid = fork();
        if(pid==0)
        
            break;
     }
    if(i==0)
    {
        //从第一个子进程
        //从键盘输入数据并发送给对方
        int fd = open("bob_2_lucy",O_WRONLY);
        while(1)
        {
            char buf[128]="";
           int len=read(STDIN_FILENO,buf,128);
            buf[len-1]='\0';
            write(fd,buf,len);
            if(strcmp(buf,"bye")==0)
            {
                break;
            }
        }
        close(fd);
        _exit(0);
    }
    else if(i==1)
    {
        //第二个子进程,读数据(lucy->bob)
        int fd = open("lucy_2_bob",O_RDONLY);
        while(1)
        {
            char buf[128]="";
            int len = read(fd,buf,128);
            if(len<=0)
                break;
            printf("接收Lucy:%s\n",buf);
        }
        close(fd);
        _exit(0);
    }
    else
    {
        //主进程
        while(1)
        {
            int pid_=waitpid(0,NULL,WNOHANG);
            if(pid_==-1)
            {
                //所有的子进程都退出
                break;
            }
        }
    }
    return 0;
}

第一次编译时:

gcc bbs_bob.c -o bob -D CREATE_FIFO

第一次运行之后,还需要重新编译(不带CREATE_FIFO宏)

gcc bbs_bob.c -o bob

如:bbs_lucy.c

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char const *argv[])
{
#ifdef CREATE_FIFO
    mkfifo("bob_2_lucy", 0755);
    mkfifo("lucy_2_bob", 0755);
#endif
    int i = 0;
    for (; i < 2; i++)
    {
        int pid = fork();
        if (pid == 0)
            break;
    }
    if (i == 0)
    { 
        // 第一个子进程
        // 从键盘输入数据并发送给对方
        int fd = open("lucy_2_bob", O_WRONLY);
        while (1)
        {
            char buf[128] = "";
            int len = read(STDIN_FILENO, buf, 128);
            buf[len - 1] = '\0';
            write(fd, buf, len);
            if (strcmp(buf, "bye") == 0)
            {
                break;
            }
        }
        close(fd);
        _exit(0);
    }
    else if (i == 1)
    {
        // 第二个子进程, 读数据(lucy->bob)
        int fd = open("bob_2_lucy", O_RDONLY);
        while (1)
        {
            char buf[128] = "";
            int len = read(fd, buf, 128);
            if (len <= 0)
                break;
            printf("接收bob: %s\n", buf);
        }
        close(fd);
        _exit(0);
    }
    else
    {
        // 主进程
        while (1)
        {
            int pid_ = waitpid(0, NULL, WNOHANG);
            if (pid_ == -1)
            {
                // 所有的子进程都退出
                break;
            }
        }
    }
    return 0;
}

将两个程序合并在一起,使用宏区分

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc,char const *argv[])
{
#ifdef CREATE_FIFO
    mkfifo("bob_2_lucy",0755);
    mkfifo("lucy_2_bob",0755);
#endif
    int i =0 ;
    for(;i<2;i++)
    {
        int pid = fork();
        if(pid == 0)
            break;
    }
    if(i==0)
    {
        //第一个子进程
        //从键盘输入数据并发送给对方
#ifdef LUCY
        int fd = open("lucy_2_bob",O_WRONLY);
#else
        int fd = open("bob_2_lucy",O_WRONLY);
#endif
        while(1)
        {
            char buf[128]="";
            int len = read(STDIN_FILENO,buf,128);
            buf[len-1] = '\0';
            write(fd,buf,len);
            if(strncmp(buf,"bye")==0)
            {
                break;
            }
        }
        close(fd);
        _exit(0);
    }
    else if(i==1)
    {
        //第二个子进程
        //读数据(lucy->bob)
#ifdef LUCY
        char *name = "bob";
        int fd = open("bob_2_lucy",O_RDONLY);
#else
        int fd = open("lucy_2_bob",O_RDONLY);
        char *name = "lucy";
#endif
        while(1)
        {
            char buf[128]="";
            int len = read(fd,buf,128);
            if(len<=0)
                break;
            printf("接收%s:%s\n",name,buf);
        }
        close(fd);
        _exit(0);
    }
    else
    {
        //主进程
        while(1)
        {
            int pid_ = waitpid(0,NULL,WNOHANG);
            if(pid_=-1)
            {
                //所有的子进程都退出
                break;
            }
        }
    }
    return 0;
}