• Linux 系统编程 学习:2-进程间通信1:Unix IPC(1)管道


    背景

    上一讲我们介绍了创建子进程的方式。我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux exec族函数解析 )

    我们也知道,进程之间的资源在默认情况下是无法共享的,所以我们需要借助系统提供的 进程间通信(IPC, InterProcess Communication) 有关的接口。

    进程间通信

    由于进程间的地址空间相对独立。进程与进程间不能像线程间通过全局变量通信,所以进程之间要交换数据必须通过内核。
    在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

    IPC的方式通常有:

    • Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
    • System V IPC:消息队列、信号量、共享内存
    • Socket(支持不同主机上的两个进程IPC)

    我们在这一讲介绍Unix IPC,包括:管道(pipe)、命名管道(FIFO)与信号(Signal)。

    注意:对于管道来说,只有读端存在,写端才有意义;如果读端不在,写端向FIFO或者PIPE写数据,内核将向对应的进程发送SIGPIPE信号(默认终止进程)。

    无名管道(pipe)

    无名管道,是 UNIX 系统IPC最古老的形式,内部是用环形队列实现的。

    在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。

    特点:
    1)无名,在文件系统中找不到它的存在,只存在于内存中。
    2)必须得是亲缘关系的进程(父子,兄弟,祖孙等)
    3)操作是不具备原子性(即,不具备完整性)
    4)不能用lseek来定位
    5)半双工(读写只能一端进行,另一端等待),具有固定的读端和写端。
    6)具备阻塞(有读者有写者,当读没有数据或者是写满了的时候会卡在那里等待)

    使用下面的函数创建 pipe管道。用于亲缘进程通信,其他操作与普通文件相同。

    c
    #include<unistd.h>
    
    intpipe(int pipefd[2]);
    
    #define _GNU_SOURCE             /* See feature_test_macros(7) */
    #include<fcntl.h>              /* Obtain O_* constant definitions */
    #include<unistd.h>
    
    intpipe2(int pipefd[2], int flags); // 
    /*
    flags 包括O_CLOEXEC、O_DIRECT、O_NONBLOCK。
    - CLOEXEC:close-on-exec即当调用exec()函数成功后,文件描述符会自动关闭
    - O_DIRECT:任何读写操作都只在用户态地址空间和磁盘之间传送而不经过page cache
    - O_NONBLOCK:非阻塞模式,在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
    */
    

    成功:返回0,同时为 pipefd 赋值。

    • pipefd[0]:读端
    • pipefd[1]:写端

    失败:返回-1

    读写情况:

    • 有数据时,无论有无写者(持有文件可写权限的描述符的进程,无的意思是指:进程退出,不再持有对应的fd;下同)都能够正常读;
    • 无数据时:如果 有 写者,则阻塞等待;如果 无 写着,则立即返回。

    • 如果 有 读者(读者,持有文件可读权限的描述符的进程):缓冲未满,正常写入;缓冲已满,阻塞等待。
    • 如果 无 读者,无论缓冲区如何,立即收到 SIGPIPE 信号(可以通过先注册的信号捕获 SIGPIPE 进行处理,默认是终止进程)

    匿名管道 原理

    管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。
    一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。
    当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

    匿名管道如何实现亲缘进程间的通信

    1)父进程创建管道,得到两个文件描述符指向管道的两端。

    2)父进程fork出子进程,子进程也有两个文件描述符指向同一管道。

    3)写进程关闭 pipefd[0],读进程关闭 pipefd[1](因为管道只支持单向通信,关闭会比较好)。
    数据从写端流入从读端流出,这样就实现了进程间通信。

    c
    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    
    intmain(int argc, char *argv[]){
        pid_t pid = -1;
        int i;
        int ret;
        int status;
        char buffer[20]={'a', 0};
        int pipefd[2] = {0};
    
        ret = pipe(pipefd);
        if(!ret)
        {
            printf("Reader fd is : %d
    ",pipefd[0]);
            printf("Writer fd is : %d
    ",pipefd[1]);
        }
    
        pid = fork();
        if(pid == 0)
        {
            printf("Son as reader
    ");
            close(pipefd[1]);// 关闭写端
            for (i = 0; i < 5; ++i) {
                read(pipefd[0], buffer, sizeof(buffer));
                printf("%s
    ", buffer);
            }
            exit(0xaa);
        }else if(pid > 0)
        {
            sleep(1);
            printf("Father as Writer
    ");
            close(pipefd[0]);// 关闭读端
            for (i = 0; i < 5; ++i) {
                write(pipefd[1], buffer, sizeof(buffer));
                buffer[i] = 'a' + i;
            }
    
            ret = wait(&status);
            printf("%x
    ", WEXITSTATUS(status));
        }
    
        return 0;
    }
    

    命名管道(fifo)

    FIFO (First in, First out)为一种特殊的文件类型(通过 stat 结构的 st_mode 成员可以知道文件是否是 FIFO 类型,可以用 S_ISFIFO 宏对此进行测试),它在文件系统中有对应的路径。

    当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道

    FIFO 存在于文件系统中,内容存放在内存中,如果使用 ls -l 观察时,它的文件大小永远是0。
    实际上,Linux中的命令的 " | " 就是 fifo。

    之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统来为管道命名。

    写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。

    FIFO的好处在于我们可以通过文件的路径来识别管道,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

    创建 FIFO 类似于创建文件,它名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。它。()

    FIFO严格遵循先进先出(first in first out), 对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

    Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell:mkfifo myfifo(等价于mknod myfifo p)

    特点
    1)有名
    2)任一个进程都可以交互
    3)会诞生一个类型p的管道文件
    4)操作具备原子性
    5)全双工
    6)不能lseek来定位

    使用下面的函数创建 fifo。读写之前必须先open。

    mkfifoat 中的 dirfd 请参考: dirfd参数 有关解析

    c
    #include<sys/types.h>
    #include<sys/stat.h>
    
    intmkfifo(constchar *pathname, mode_t mode);
    
    #include<fcntl.h>           /* Definition of AT_* constants */
    #include<sys/stat.h>
    
    intmkfifoat(int dirfd, constchar *pathname, mode_t mode); 
    
    

    参数解析
    mode :参数的说明同 open 的 mode 相同。如果open时没有使用O_NONBLOCK参数,不论读端还是写端先打开,先打开者都会阻塞,一直阻塞到另一端打开。

    当 open 一个 FIFO,非阻塞标志 O_NONBLOCK 会产生下列影响:

    • 在没有指定该标志的情况下,只读 open 要阻塞到某个其他进程为写而打开这个 FIFO 为止,只写 open 要阻塞到某个其他进程为读而打开它为止。
    • 指定了该标志时,则只读 open 立即返回。但如果没有进程为读而打开一个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO。
      类似于管道,
    • 若 write 一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。
    • 若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。
    • 一个给定的 FIFO 可能有多个写进程,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作,可被原子地写到 FIFO 的最大数据量也是通过常量 PIPE_BUF 来指定的。

    如果mkfifo的路径已经存在时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。

     
     
    #include<stdio.h>
    
    #include<stdlib.h>
    #include<unistd.h>
    
    #include<sys/stat.h>
    #include<fcntl.h>
    
    #include<sys/types.h>
    #include<sys/wait.h>
    
    #include<errno.h>
    
    #define FIFO_FILE "fifo_test"
    
    intmain(int argc, char *argv[]){
        pid_t pid = -1;
        int i;
        int ret;
        int status;
        char buffer[20]={'a', 0};
        int fd = 0;
    
        ret = mkfifo(FIFO_FILE, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
        if(ret == -1)
        {
            if(errno == EEXIST)
            {
                printf("FIFO FILE existed.
    ");
            }else
            {
                perror("fifo");
                return -1;
            }
        }
    
        pid = fork();
        if(pid == 0)
        {
            printf("Son as reader
    ");
            printf("[Son] Waiting Writer
    ");
            fd = open(FIFO_FILE, O_RDONLY);
            printf("Reader Pass
    ");
    
            for (i = 0; i < 5; ++i) {
                read(fd, buffer, sizeof(buffer));
                printf("%s
    ", buffer);
            }
            exit(0xaa);
        }else if(pid > 0)
        {
            sleep(1);
            printf("Father as Writer
    ");
            printf("[Dad] Waiting Reader
    ");
            fd = open(FIFO_FILE, O_WRONLY);
            printf("Writer pass
    ");
            for (i = 0; i < 5; ++i) {
                write(fd, buffer, sizeof(buffer));
                buffer[i] = 'a' + i;
            }
    
            ret = wait(&status);
            printf("%x
    ", WEXITSTATUS(status));
        }
    
        return 0;
    }

    背景

    上一讲我们介绍了创建子进程的方式。我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux exec族函数解析 )

    我们也知道,进程之间的资源在默认情况下是无法共享的,所以我们需要借助系统提供的 进程间通信(IPC, InterProcess Communication) 有关的接口。

    进程间通信

    由于进程间的地址空间相对独立。进程与进程间不能像线程间通过全局变量通信,所以进程之间要交换数据必须通过内核。
    在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

    IPC的方式通常有:

    • Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
    • System V IPC:消息队列、信号量、共享内存
    • Socket(支持不同主机上的两个进程IPC)

    我们在这一讲介绍Unix IPC,包括:管道(pipe)、命名管道(FIFO)与信号(Signal)。

    注意:对于管道来说,只有读端存在,写端才有意义;如果读端不在,写端向FIFO或者PIPE写数据,内核将向对应的进程发送SIGPIPE信号(默认终止进程)。

    无名管道(pipe)

    无名管道,是 UNIX 系统IPC最古老的形式,内部是用环形队列实现的。

    在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。

    特点:
    1)无名,在文件系统中找不到它的存在,只存在于内存中。
    2)必须得是亲缘关系的进程(父子,兄弟,祖孙等)
    3)操作是不具备原子性(即,不具备完整性)
    4)不能用lseek来定位
    5)半双工(读写只能一端进行,另一端等待),具有固定的读端和写端。
    6)具备阻塞(有读者有写者,当读没有数据或者是写满了的时候会卡在那里等待)

    使用下面的函数创建 pipe管道。用于亲缘进程通信,其他操作与普通文件相同。

    c
    #include<unistd.h>
    
    intpipe(int pipefd[2]);
    
    #define _GNU_SOURCE             /* See feature_test_macros(7) */
    #include<fcntl.h>              /* Obtain O_* constant definitions */
    #include<unistd.h>
    
    intpipe2(int pipefd[2], int flags); // 
    /*
    flags 包括O_CLOEXEC、O_DIRECT、O_NONBLOCK。
    - CLOEXEC:close-on-exec即当调用exec()函数成功后,文件描述符会自动关闭
    - O_DIRECT:任何读写操作都只在用户态地址空间和磁盘之间传送而不经过page cache
    - O_NONBLOCK:非阻塞模式,在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
    */
    

    成功:返回0,同时为 pipefd 赋值。

    • pipefd[0]:读端
    • pipefd[1]:写端

    失败:返回-1

    读写情况:

    • 有数据时,无论有无写者(持有文件可写权限的描述符的进程,无的意思是指:进程退出,不再持有对应的fd;下同)都能够正常读;
    • 无数据时:如果 有 写者,则阻塞等待;如果 无 写着,则立即返回。

    • 如果 有 读者(读者,持有文件可读权限的描述符的进程):缓冲未满,正常写入;缓冲已满,阻塞等待。
    • 如果 无 读者,无论缓冲区如何,立即收到 SIGPIPE 信号(可以通过先注册的信号捕获 SIGPIPE 进行处理,默认是终止进程)

    匿名管道 原理

    管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。
    一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。
    当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

    匿名管道如何实现亲缘进程间的通信

    1)父进程创建管道,得到两个文件描述符指向管道的两端。

    2)父进程fork出子进程,子进程也有两个文件描述符指向同一管道。

    3)写进程关闭 pipefd[0],读进程关闭 pipefd[1](因为管道只支持单向通信,关闭会比较好)。
    数据从写端流入从读端流出,这样就实现了进程间通信。

    c
    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    
    intmain(int argc, char *argv[]){
        pid_t pid = -1;
        int i;
        int ret;
        int status;
        char buffer[20]={'a', 0};
        int pipefd[2] = {0};
    
        ret = pipe(pipefd);
        if(!ret)
        {
            printf("Reader fd is : %d
    ",pipefd[0]);
            printf("Writer fd is : %d
    ",pipefd[1]);
        }
    
        pid = fork();
        if(pid == 0)
        {
            printf("Son as reader
    ");
            close(pipefd[1]);// 关闭写端
            for (i = 0; i < 5; ++i) {
                read(pipefd[0], buffer, sizeof(buffer));
                printf("%s
    ", buffer);
            }
            exit(0xaa);
        }else if(pid > 0)
        {
            sleep(1);
            printf("Father as Writer
    ");
            close(pipefd[0]);// 关闭读端
            for (i = 0; i < 5; ++i) {
                write(pipefd[1], buffer, sizeof(buffer));
                buffer[i] = 'a' + i;
            }
    
            ret = wait(&status);
            printf("%x
    ", WEXITSTATUS(status));
        }
    
        return 0;
    }
    

    命名管道(fifo)

    FIFO (First in, First out)为一种特殊的文件类型(通过 stat 结构的 st_mode 成员可以知道文件是否是 FIFO 类型,可以用 S_ISFIFO 宏对此进行测试),它在文件系统中有对应的路径。

    当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道

    FIFO 存在于文件系统中,内容存放在内存中,如果使用 ls -l 观察时,它的文件大小永远是0。
    实际上,Linux中的命令的 " | " 就是 fifo。

    之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统来为管道命名。

    写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。

    FIFO的好处在于我们可以通过文件的路径来识别管道,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

    创建 FIFO 类似于创建文件,它名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。它。()

    FIFO严格遵循先进先出(first in first out), 对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

    Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell:mkfifo myfifo(等价于mknod myfifo p)

    特点
    1)有名
    2)任一个进程都可以交互
    3)会诞生一个类型p的管道文件
    4)操作具备原子性
    5)全双工
    6)不能lseek来定位

    使用下面的函数创建 fifo。读写之前必须先open。

    mkfifoat 中的 dirfd 请参考: dirfd参数 有关解析

    c
    #include<sys/types.h>
    #include<sys/stat.h>
    
    intmkfifo(constchar *pathname, mode_t mode);
    
    #include<fcntl.h>           /* Definition of AT_* constants */
    #include<sys/stat.h>
    
    intmkfifoat(int dirfd, constchar *pathname, mode_t mode); 
    
    

    参数解析
    mode :参数的说明同 open 的 mode 相同。如果open时没有使用O_NONBLOCK参数,不论读端还是写端先打开,先打开者都会阻塞,一直阻塞到另一端打开。

    当 open 一个 FIFO,非阻塞标志 O_NONBLOCK 会产生下列影响:

    • 在没有指定该标志的情况下,只读 open 要阻塞到某个其他进程为写而打开这个 FIFO 为止,只写 open 要阻塞到某个其他进程为读而打开它为止。
    • 指定了该标志时,则只读 open 立即返回。但如果没有进程为读而打开一个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO。
      类似于管道,
    • 若 write 一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。
    • 若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。
    • 一个给定的 FIFO 可能有多个写进程,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作,可被原子地写到 FIFO 的最大数据量也是通过常量 PIPE_BUF 来指定的。

    如果mkfifo的路径已经存在时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。

     
     
    #include<stdio.h>
    
    #include<stdlib.h>
    #include<unistd.h>
    
    #include<sys/stat.h>
    #include<fcntl.h>
    
    #include<sys/types.h>
    #include<sys/wait.h>
    
    #include<errno.h>
    
    #define FIFO_FILE "fifo_test"
    
    intmain(int argc, char *argv[]){
        pid_t pid = -1;
        int i;
        int ret;
        int status;
        char buffer[20]={'a', 0};
        int fd = 0;
    
        ret = mkfifo(FIFO_FILE, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
        if(ret == -1)
        {
            if(errno == EEXIST)
            {
                printf("FIFO FILE existed.
    ");
            }else
            {
                perror("fifo");
                return -1;
            }
        }
    
        pid = fork();
        if(pid == 0)
        {
            printf("Son as reader
    ");
            printf("[Son] Waiting Writer
    ");
            fd = open(FIFO_FILE, O_RDONLY);
            printf("Reader Pass
    ");
    
            for (i = 0; i < 5; ++i) {
                read(fd, buffer, sizeof(buffer));
                printf("%s
    ", buffer);
            }
            exit(0xaa);
        }else if(pid > 0)
        {
            sleep(1);
            printf("Father as Writer
    ");
            printf("[Dad] Waiting Reader
    ");
            fd = open(FIFO_FILE, O_WRONLY);
            printf("Writer pass
    ");
            for (i = 0; i < 5; ++i) {
                write(fd, buffer, sizeof(buffer));
                buffer[i] = 'a' + i;
            }
    
            ret = wait(&status);
            printf("%x
    ", WEXITSTATUS(status));
        }
    
        return 0;
    }
  • 相关阅读:
    缓冲区溢出学习笔记 一
    什么是TNotifyEvent(转)
    Delphi中dll的使用(转)
    bash 编程
    感觉自己真的很幸运
    开始写程序,发现自己很弱智
    你是我心中不变的新绿(转载)
    做一次年度总结和清理
    今天简直成精力狂了^_^
    Double Do weeks Again : )
  • 原文地址:https://www.cnblogs.com/wt88/p/15100284.html
Copyright © 2020-2023  润新知