玩命加载中 . . .

2.2-进程间通信(上)


匿名管道通信

  • 匿名管道是一种特殊的文件,这种文件只存在于内存中。其实是一个在内核内存中维护的缓冲器
  • 匿名管道只能用于父子进程或兄弟进程之间,必须用于具有亲缘关系的进程间的通信
  • 这是因为管道实际上是内存的一个缓冲区

  • 管道的写入端是一个文件描述符fd[1],对应的是文件x1

  • 管道的读取端也是一个文件描述符fd[0],对应的文件是x2

  • 在进程之间通过管道通信,实际上是通过缓冲区+两个文件通信。如果两个进程没有关系(父子或者兄弟)则文件描述符对应的文件不是同一个文件,便无法通信

匿名管道只能由一端向另一端发送数据,是半双工方式,如果双方需要同时收发数据需要两个管道

#include <unistd.h>
int pipe(int pipefd[2]);
- pipefd[2]: 传出参数
    - pipefd[0]: 对应管道的读端
    - pipefd[1]: 对应管道的写端

管道默认是阻塞的,没有数据read阻塞,管道满了write阻塞
通信双方的进程中写数据的一方需要把fd[0]close掉,读的一方需要先把fd[1]close

单向通信

子进程写,父进程读

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

int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);

    pid_t pid = fork();
    if (pid > 0) {  // 父进程
        printf("I am parent, pid: %d\n", getpid());
        close(pipefd[1]);   // 关闭写端

        char buf[1024] = {0};
        while (1) {
            int len = read(pipefd[0], buf, sizeof(buf)); // 从管道读取端读取数据
            printf("parent rev: %s, pid: %d\n", buf, getpid());
        }
        
    }
    else if (pid == 0) {    // 子进程
        printf("I am child, pid: %d\n", getpid());
        close(pipefd[0]);   // 关闭读端
        char buf[1024] = {0};
        while (1) {
            char *str = "hello, I am child";
            write(pipefd[1], str, strlen(str)); // 往写端写数据
        }
    }

    return 0;
} 

双向通信

正常不会这么用

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

int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        exit(0);
    }

    pid_t pid = fork();
    if (pid > 0) {
        printf("I am parent, pid: %d\n", getpid());
        // 从管道读取端读取数据
        char buf[1024] = {0};
        while (1) {
            int len = read(pipefd[0], buf, sizeof(buf));    // 读数据
            printf("parent rev: %s, pid: %d\n", buf, getpid());

            char *str = "hello, I am parent";
            write(pipefd[1], str, strlen(str)); // 写数据
            sleep(1);

        }
    }
    else if (pid == 0) {
        printf("I am child, pid: %d\n", getpid());
        char buf[1024] = {0};
        while (1) {
            char *str = "hello, I am child";
            write(pipefd[1], str, strlen(str)); // 写数据
            sleep(1);

            int len = read(pipefd[0], buf, sizeof(buf));    // 读数据
            printf("chlid rev: %s, pid: %d\n", buf, getpid());
        }
        
    }

    return 0;
} 
I am parent, pid: 26544
I am child, pid: 26545
parent rev: hello, I am child, pid: 26544
chlid rev: hello, I am parent, pid: 26545

管道大小

int main() {
    int pipefd[2];
    int ret = pipe(pipefd);
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
    printf("pipe size: %ld\n", size);
    return 0;
}
4096

管道应用

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

int main() {

    int fd[2];
    int ret = pipe(fd);

    pid_t pid = fork();
    if (pid > 0) {  // 父进程
        close(fd[1]);   // 关闭写端
        char buf[1024] = {0};
        int len = -1;
        while ((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {
            printf("%s", buf);
            memset(buf, 0, 1024);
        }
        wait(NULL);
        
    }
    else if (pid == 0) {    // 子进程
        close(fd[0]);   // 关闭读端
        dup2(fd[1], STDOUT_FILENO);     // 输出重定向到管道的写端
        execlp("ps", "ps", "aux", NULL);    // 执行 ps aux
        perror("execlp");
        exit(0);
    }
    else {
        perror("fork");
        exit(0);
    }

    return 0;
}

管道的读写特点

使用管道时,应该注意一下几种特殊情况(假设都是阻塞I/O操作)

  1. 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从读端读数据,数据被读完后,再次调用read返回0
  2. 如果有指向管道写端的文件描述符没有关闭,但没有写数据,数据被读完后,再有进程调用read会阻塞,直到有数据写入
  3. 如果所有指向管道读端的文件描述符都关闭了,这时如果有进程往管道中写数据,该进程会收到一个信号SIGPIPE,通常会导致进程异常终止
  4. 如果有指向管道读端的文件描述符没有关闭,而持有管道读端的进程也没有读数据,这时有进程往管道中写数据,管道被写满后会再次write会阻塞,直到管道中有位置才能再次写入数据

读管道:

  • 管道中有数据,read返回实际读到的字节数
  • 管道中无数据
    • 写端被全部关闭,read返回0
    • 写端没有完全关闭,read阻塞等待

写管道:

  • 管道读端全部关闭,进程异常终止(收到SIGPIPE信号)
  • 管道读端没有全部关闭
    • 管道已满,write阻塞
    • 管道没有满,write将数据写入,并返回实际写入的字节数

管道设置为非阻塞

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

int main() {

    int pipefd[2];  // 在fork之前创建管道
    int ret = pipe(pipefd);

    pid_t pid = fork();
    if (pid > 0) {  // 父进程
        printf("I am parent, pid: %d\n", getpid());
        close(pipefd[1]);   // 关闭写端

        char buf[1024] = {0};
        int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;    // 修改flag
        fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag
        
        while (1) {
            int len = read(pipefd[0], buf, sizeof(buf)); // 从管道读取端读取数据
            printf("len: %d\n", len);
            printf("parent rev: %s, pid: %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }
        
    }
    else if (pid == 0) {    // 子进程
        printf("I am child, pid: %d\n", getpid());
        close(pipefd[0]);   // 关闭读端
        char buf[1024] = {0};
        while (1) {
            char *str = "hello, I am child";
            write(pipefd[1], str, strlen(str)); // 往写端写数据
            sleep(5);
        }
        
    }

    return 0;
}
I am parent, pid: 29120
len: -1     // 管道里没数据
parent rev: , pid: 29120
I am child, pid: 29121
len: 17     // 子进程往管道里写了数据,父进程读到了
parent rev: hello, I am child, pid: 29120
len: -1     // 子进程休眠,没数据
parent rev: , pid: 29120
len: -1
parent rev: , pid: 29120
len: -1
parent rev: , pid: 29120
len: 17     // 子进程又写了,父进程就读到了
parent rev: hello, I am child, pid: 29120

有名管道

也叫命名管道,FIFO文件,以文件的形式存在于文件系统中

有名管道(FIFO)与匿名管道(pipe)的不同

  1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容存放于内存中
  2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用
  3. FIFO 有名字,不相关的进程可以通过打开FIFO文件进行通信
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- pathname: 管道的路径
- mode: 文件权限,跟open的mode是一样的
- 返回值: 成功返回0,失败返回-1

写端

sprintf函数打印到字符串中,要注意字符串的长度要足够容纳打印的内容,否则会出现内存溢出

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

int main() {
    // 1. 判断管道文件是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1) {
        printf("make fifo\n");
        // 2. 没有就创建管道文件
        int ret = mkfifo("fifo1", 0664);
    }

    // 3. 打开管道文件
    int fd = open("fifo1", O_WRONLY);

    for (int i = 0; i < 10; i++) {
        char buf[1024];
        sprintf(buf, "hello, %d\n", i); // 把要写的数据先输入到buf里面
        write(fd, buf, strlen(buf));    // 再用buf往FIFO文件里面写数据
        sleep(1);
    }
    close(fd);
    return 0;
}

读端

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

int main() {
    int fd = open("fifo1", O_RDONLY);   // 打开管道文件
    while (1) {
        char buf[1024] = {0};
        int len = read(fd, buf, sizeof(buf));   // 读取数据
        if (len == 0) {
            printf("write end close...\n");
            break;
        }
        printf("recv buf: %s\n", buf);
    }
    close(fd);
    return 0;
}

两个程序都运行才会有输出

$ ./write 
data: 0
data: 1
data: 2
data: 3
data: 4
data: 5

$ ./read 
recv buf: hello, 0

recv buf: hello, 1

recv buf: hello, 2

recv buf: hello, 3

recv buf: hello, 4
  • 如果先关闭写端,读端返回值len==0break退出
  • 如果先关闭读端,写端会收到SIGPIPE信号退出

读管道

  • 有数据,read返回实际读到的字节数
  • 无数据
    • 写端全部关闭,read返回0
    • 写端没有全部关闭,read阻塞等待

写管道

  • 读端全部关闭,进程异常终止
  • 读端没有全部关闭
    • 管道满了,write阻塞
    • 没有满, write写数据,返回写入的字节数

简易聊天功能

进程A先写后读

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

int main() {
    // 1. 判断管道文件是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1) {
        // 2. 创建管道文件
        int ret = mkfifo("fifo1", 0664);
    }
    ret = access("fifo2", F_OK);
    if (ret == -1) {
        // 2. 创建管道文件
        int ret = mkfifo("fifo2", 0664);
    }

    // 3. 打开管道文件
    int fdw = open("fifo1", O_WRONLY);  // 用来发送数据
    int fdr = open("fifo2", O_RDONLY);  // 用来接收数据

    // 4. 循环写读数据
    char buf[128];
    while (1) {
        memset(buf, 0, 128);
        fgets(buf, 128, stdin);
        int ret = write(fdw, buf, strlen(buf));
        if (ret == -1) {
            perror("write");
            exit(0);
        }

        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if (ret <= 0) {
            perror("read");
            break;
        }
        printf("buf: %s\n", buf);
    }
    
    close(fdw);
    close(fdr);// 关闭文件描述符
    return 0;
}

进程B先读后写

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

int main() {
    // 1. 判断管道文件是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1) {
        // 2. 创建管道文件
        int ret = mkfifo("fifo1", 0664);
    }
    ret = access("fifo2", F_OK);
    if (ret == -1) {
        int ret = mkfifo("fifo2", 0664);
    }

    // 3. 打开管道文件
    int fdr = open("fifo1", O_RDONLY);  // 用来读
    int fdw = open("fifo2", O_WRONLY);  // 用来写

    // 4. 循环读写数据
    char buf[128];
    while (1) {
        memset(buf, 0, 128);
        int ret = read(fdr, buf, 128);
        if (ret <= 0) {
            perror("read");
            break;
        }
        printf("buf: %s\n", buf);

        memset(buf, 0, 128);
        fgets(buf, 128, stdin);
        ret = write(fdw, buf, strlen(buf));
        if (ret == -1) {
            perror("write");
            exit(0);
        }
    }
    
    close(fdw);
    close(fdr);// 关闭文件描述符
    return 0;
}
$ ./a
create fifo1
create fifo2
open fifo1, wait to write
open fifo2, wait to read
hello   // 先写数据发送给B,然后阻塞等待读取A的数据
buf: nice to meet you   // 读到了,等待终端输入

$ ./b 
open fifo1, wait to read
open fifo2, wait to write
buf: hello  // 读到了A发送的数据,等待终端输入

nice to meet you    // 发送数据给A

有名管道和无名管道的异同点

  • 相同点:open打开管道文件以后,在内存中开辟了一块空间,管道的内容在内存中存放,有两个指针——头指针(指向写的位置)和尾指针(指向读的位置)指向它。读写数据都是在给内存的操作,并且都是半双工通讯。
  • 区别:有名管道在任意进程之间使用,无名管道在父子进程之间使用。

全双工、半双工、单工通讯的区别

  • 单工:方向是固定的,只有一个方向可以写,例如广播
  • 半双工:方向不固定,但在某一刻只能有一个方向进行写,例如对讲机
  • 全双工:两个方向都可以同时写,例如打电话

文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录