一.进程间通信概述

1.进程通信

  • 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源
  • 但进程并不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )

2.进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

3.主要进程间通信的通信机制

二.无名管道

1.概述

**介绍:**管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的 UNIX 系统都支持这种通信机制

**无名管道通信的实现:**主要是通过子父进程间文件描述符的传递实现的,子进程中继承了父进程的文件描述符,因此子进程可以无需名字,只通过继承来的文件描述符找到管道

  • 基于此,管道必须在fork子进程之前创建,否则子进程就得不到无名管道的文件描述符
  • 只能在具有公共祖先的进程之间使用

特点:

  • 半双工通信

  • 有读端和写端,一端写入,一端读出

  • 写入管道中的数据遵循先入先出的规则

  • 管道所传送的数据是无格式

    要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等

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

  • 管道在内存中对应一个缓冲区

    缓冲区的大小决定管道的大小(大小是有限的)

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

2.管道创建

无名管道分为两个文件描述符,一个是读描述符,一个是写描述符

函数:

#include 

int pipe(int pipefd[2]);

**功能:**创建无名管道

**参数:**pipefd : 为 int 型数组的首地址,其存放了管道的文件描述符

  • pipefd[0]:固定用于读管道
  • pipefd[1]:固定用于写管道

返回值:

  • 成功:0
  • 失败:-1

3.管道通信实现

(1)概述

**管道的读写:**通常使用文件 I/O的函数来操作管道

  • lseek()除外,因为管道只能按照顺序去读,不能偏移

**管道通信的实现:**基于管道半双工通信的限制,读写的两个只能保留读和写其中一个描述符,之后就可以通过IO函数Wirte和Read写出、读取数据自管道了

  • Read函数是带阻塞的,它会等待写端发送数据(可以设置)

示意图:

(2)示例

示例代码:

#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    int res = -1;

    // 创建管道(必须在fork子进程之前创建)
    int pipefd[2];
    res = pipe(pipefd);
    if (res == -1)
    {
        perror("pipe");
        return -1;
    }

    // fork子进程
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork");
        return -1;
    }

    /* 管道通信:父进程读、子进程写 */

    // 子进程
    if (pid == 0)
    {
        // 关闭写文件描述符
        close(pipefd[1]);
        // 从管道中读
        char buf[128];
        res = read(pipefd[0], buf, sizeof(buf));
        if (res < 0)
        {
            perror("read");
            _exit(-1);
        }
        printf("读到的字符是:%s\n", buf);
        // 关闭读文件描述符
        close(pipefd[0]);
        // 读完直接退出
        _exit(0);
    }
    else
    {
        // 父进程
        // 关闭读文件描述符
        close(pipefd[0]);

        // 往管道中写入数据
        char str[] = "HelloPipe";
        int len = write(pipefd[1], str, sizeof(str));
        printf("写出数据长度:%d\n", len);

        // 关闭写文件描述符
        close(pipefd[1]);
    }

    return 0;
}

结果:

honestliu@My-Pc:~/Develop/Pipeline$ ./a.out
写出数据长度:10
读到的字符是:HelloPipe

4.管道读写特点

(1)读管道

  • 管道中有数据,read返回实际读到的字节数

  • 管道中无数据:

    • 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
    • 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)

(2)写管道

  • 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)

  • 管道读端没有全部关闭:

    • 管道已满,write阻塞
    • 管道未满,write将数据写入,并返回实际写入的字节数

5.设置非阻塞

(1)概述

要以追加的方式即|的方式添加,不能去除原来的状态标记

**设置方法:**通过fcntl函数,先获取原文件描述符状态标记flags,修改为非阻塞后设置回去

(2)方式

**示例:**设置读端非阻塞

**效果:**如果写端没有关闭,读端设置为非阻塞, 如果没有数据,直接返回-1

//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

(3)示例

**代码:**在写进程设置sleep(2),造成其开启通道但没写入数据,本来读进程是会阻塞的,但由于设置了O_NONBLOCK,所以其直接输出Resource temporarily unavailable(资源暂时不可用)就停止了,后继写进程直接发出异常信号后停止了

#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    int res = -1;

    // 创建管道(必须在fork子进程之前创建)
    int pipefd[2];
    res = pipe(pipefd);
    if (res == -1)
    {
        perror("pipe");
        return -1;
    }

    // fork子进程
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork");
        return -1;
    }

    // 子进程
    if (pid == 0)
    {
        // 关闭写文件描述符
        close(pipefd[1]);

        /* 设置读文件描述符非阻塞 */
        int flags = fcntl(pipefd[0], F_GETFD);
        flags = flags | O_NONBLOCK;
        fcntl(pipefd[0], F_SETFL, flags);

        // 从管道中读
        char buf[128];
        res = read(pipefd[0], buf, sizeof(buf));
        if (res < 0)
        {
            perror("read");
            _exit(-1);
        }
        printf("读到的字符是:%s\n", buf);
        // 关闭读文件描述符
        close(pipefd[0]);
        // 读完直接退出
        _exit(0);
    }
    else
    {
        // 父进程
        // 关闭读文件描述符
        close(pipefd[0]);

        /* 制造阻塞,让父进程随眠2秒 */
        sleep(2);

        // 往管道中写入数据
        char str[] = "HelloPipe";
        int len = write(pipefd[1], str, sizeof(str));
        printf("写出数据长度:%d\n", len);

        // 关闭写文件描述符
        close(pipefd[1]);
    }

    return 0;
}

结果:

honestliu@My-Pc:~/Develop/Pipeline$ ./a.out
read: Resource temporarily unavailable

6.查看管道缓冲区

(1)命令查看

命令:ulimit -a

(2)函数查看

头文件:#include

函数原型:long fpathconf(int fd, int name);

**功能:**该函数可以通过name参数查看不同的属性值

参数:

  • fd:文件描述符
  • name:下面列出的是常用的,具体看man手册
    • _PC_PIPE_BUF:查看管道缓冲区大小
    • _PC_NAME_MAX:文件名字字节数的上限

返回值:

  • 成功:根据name返回的值的意义也不同
  • 失败: -1

(3)示例

代码:

#include 
#include 


int main(int argc, char const *argv[])
{
    //创建存放管道文件描述符的整形数组
    int pipefd[2];

    //使用pipe函数创建无名管道
    int res = pipe(pipefd);
    if (res == -1)
    {
        //创建管道失败
        perror("pipe");
        return 1;
    }

    printf("读管道为:%d,写管道为:%d\n",pipefd[0],pipefd[1]);

    //获取管道缓冲区的大小并打印
    printf("读管道缓冲区的大小为:%ld\n",fpathconf(pipefd[0], _PC_PIPE_BUF));
    printf("写管道缓冲区的大小为:%ld\n",fpathconf(pipefd[1], _PC_PIPE_BUF));


    //关闭管道描述符(尽管进程结束会自动关闭,但建议还是手动关闭)
    close(pipefd[0]);
    close(pipefd[1]);


    
    return 0;
}

结果:

honestliu@My-Pc:~/Develop/Pipeline$ ./a.out
读管道为:3,写管道为:4
读管道缓冲区的大小为:4096
写管道缓冲区的大小为:4096

三.有名管道

1.概述

**简述:**有名管道(FIFO文件),它以文件的形式存在于文件的形式存在于文件系统中,不同的进程间通过这个文件进行相互通信,进而突破无名管道只能相关进程通信的限制

无名管道和有名管道:

  • FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存
  • 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用
  • FIFO 有名字,不相关的进程可以通过打开命名管道进行通信

注意事项:

虽然可以以读写的方式打开可以避免这个情况,但实际用到的很少

  • 一个为只读而打开一个管道的进程会阻塞直到另一个进程以只写的方打开该管道
  • 一个为只写而打开一个管道的进程会阻塞直到另一个进程以只读的方打开该管道

**读写特性:**同无名管道,具体见上

2.管道创建

(1)通过命令创建

① 命名
mkfifo FIFO文件名
② 示例
honestliu@My-Pc:~/Develop/Pipeline$ mkfifo fifo
honestliu@My-Pc:~/Develop/Pipeline$ ls -l fifo
prw-r--r-- 1 honestliu honestliu 0 Apr 12 18:16 fifo

(2)通过函数创建

头文件:

#include 
#include 

函数原型:int mkfifo(const char *pathname, mode_t mode)

参数:

  • pathname:包含FIFO文件名的路径

  • mode:文件的权限

    和open的mode参数相同,都是以数字代表的权限

返回值:

  • 成功:0 状态码
  • 失败:如果文件已经存在,则会出错且返回-1

3.有名管道的读写操作

(1)概述

管道的读写:

  • 使用open()函数以只读或只写的方式打开FIFO文件,获取其文件描述符
  • 后继使用I/O函数来操作管道即可
    • lseek()除外,因为管道只能按照顺序去读,不能偏移

**顺序:**FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾

(2)示例

写进程代码:

#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#define SIZE 128

int main(int argc, char const *argv[])
{
    int i = 0;
    int res = -1;
    char str[SIZE];

    /* 以只写的方式打开有名管道文件,获取文件描述符 */
    int fd = open("fifo", O_WRONLY);

    if (fd == -1)
    {
        perror("fopen");
        return 1;
    }
    /* 使用write函数向有名管道写出数据 */
    while (1)
    {
        // 格式化字符串
        memset(str, 0, SIZE);
        i++;
        snprintf(str, SIZE, "写出数据%d", i);
        res = write(fd, str, strlen(buf));
        if (res <= 0)
        {
            perror("write");
            break;
        }
        
        printf("写出:%s\n", str);
        //2秒写一次
        sleep(2);
    }

    /* 关闭有名管道 */
    close(fd);

    return 0;
}

读进程代码:

#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#define SIZE 128

int main(int argc, char const *argv[])
{
    int res = -1;
    char str[SIZE];

    /* 以只读的方式打开有名管道文件,获取文件描述符 */
    int fd = open("fifo", O_RDONLY);

    if (fd == -1)
    {
        perror("fopen");
        return 1;
    }
    /* 使用write函数向有名管道写出数据 */
    while (1)
    {
        // 格式化字符串
        memset(str, 0, SIZE);
        res = read(fd,str,SIZE);//没有信息的时候会阻塞,并不会返回0
        if (res <= 0)
        {
            perror("read");
            break;
        }
        printf("读到:%s\n", str);
    }

    /* 关闭有名管道 */
    close(fd);

    return 0;
}

**结果:**单独启动一端都会陷入阻塞,只有当另外一端启动时,才会开始相互发送数据

4.练习-聊天程序

(1)需求

使用多进程和有名管道编写聊天程序

(2)代码

注意:

  • 这里只放了User1的代码,至于User2的代码,只需按照代码中的约定修改读和写打开的FIFO文件即可

  • 需要手动创建两个有名管道符fifo1fifo2

    mkfifo fifo1 fifo2
    

User1代码:

#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#define SIZE 1024

/*
需要同时具备读和写的能力,故需要两个管道
一个管道循环读信息                      |
一个管道循环等待用户输入并写入到管道中    |  ----> 两个动作不能同时在一个进程中运行,所以需要多进程 --> 主进程负责监听输入并写入,子进程负责循环读数据
 */

/*
约定:
    - fifo1:用于User1读,User2写
    - fifo2: 用于User1写,User2读

 */

int main(int argc, char const *argv[])
{

    // 创建一个子进程,负责循环读取数据
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork");
        return 1;
    }

    /* 分进程处理业务 */

    // 子进程
    if (pid == 0)
    {
        char buf[SIZE];
        int res = -1;
        // 以只读的形式打开有名管道文件fifo1
        int fd = open("fifo1", O_RDONLY);
        if (fd == -1)
        {
            perror("open");
        }

        while (1)
        {
            // 对字符串进行置0操作
            memset(buf, 0, SIZE);
            res = read(fd, buf, SIZE);
            if (res <= 0)
            {
                perror("对方已断开...");
                break;
            }
            printf("收到User2的信息:%s\n", buf);
            printf("请输入要发送的信息:\n");
        }
        // 关闭有名管道
        close(fd);
    }
    else // 父进程
    {
        char buf[SIZE];
        int res1 = -1;
        int res2 = -1;
        // 以只写的形式打开有名管道文件fifo2
        int fd = open("fifo2", O_WRONLY);
        if (fd == -1)
        {
            perror("open");
        }
        while (1)
        {

            // 对字符串进行置0操作
            memset(buf, 0, SIZE);
            // 从标准输入读取用户的输入值
            printf("请输入要发送的信息:\n");
            res1 = read(0, buf, SIZE);
            if (res1 <= 0)
            {
                perror("read2");
                break;
            }
            
            //去除最后的换行符
            if (buf[strlen(buf) - 1] == '\n')
            {
                buf[strlen(buf) - 1] = '\0';
            }

            // 将读取的用户输入信息输出到管道
            res2 = write(fd, buf, strlen(buf) + 1);
            if (res2 <= 0)
            {
                perror("write");
                break;
            }
        }
        // 关闭有名管道
        close(fd);
    }

    return 0;
}

(3)效果

四.共享存储映射

1.概述

(1)介绍

**介绍:**存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射

**简述:**就是将文件的一部分内容映射到内存中去,而后就可以通过操作内存的方式操作文件了

(2)注意事项

  • 程序需要对映射文件具有读写权限
    • 创建映射区的时候需要对其进行读操作
    • MAP_SHARED共享数据模式复制数据回文件要对其进行写操作
  • 当MAP_SHARED时,映射区的权限应该小于等于打开文件的权限。而MAP_PRIVATE则无所谓
  • 映射区的释放与文件关闭无关,只要映射成功,文件就可以立即关闭
  • 用于映射的文件必须要有实际的大小,当其大小为0时,不能创建映射区
  • munmap传入的地址一定是mmap的返回地址,坚决杜绝指针++操作
  • 如果文件偏移量必须为4K的整数倍
  • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作

2.存储映射函数

(1)Mmap函数

**功能:**将一个文件或者其它对象映射到内存中去

头文件:#include

函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)

参数:

  • addr:指定映射的起始地址

    • 通常设置为NULL,将其交由系统指定
  • length:映射到内存的文件长度(大小)

  • prot:映射区的保护方式

    要小于等于打开映射文件的权限

    • 读:PROT_READ
    • 写:PROT_WRITE
    • 读写:PROT_READ | PROT_WRITE
  • flags:映射区的特性

    • MAP_SHARED:写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享
    • MAP_PRIVATE:对映射区的写操作不会复制回文件,而是会创建一个映射区的私有副本
  • fd:由open()返回的文件描述符,代表要映射的文件

  • offset:以文件开始处的偏移量,即设置映射文件的起始位置

    • 必须是4k的整数倍,通常为0,表示从文件起始位置开始映射

返回值:

  • 成功:返回创建的映射区首地址
  • 失败:返回MAP_FAILED宏,并设置errno以指示错误原因

(2)Munmap函数

**功能:**释放映射到内存中文件

头文件:#include

函数原型:int munmap(void *addr, size_t length)

参数:

  • addr:使用mmap函数创建的映射区的首地址
  • length:映射区的大小

返回值:

  • 成功:0
  • 失败:-1,并将 errno 设置为指示原因

(3)示例

**代码:**将本地的test.txt映射到内存,并使用memcpy将一段字符串复制到其内部,且由于flags设置了PROT_READ,所以其会将映射区数据复制回原文件,覆盖原文件中起始位置开始字符串长度截至的内容

#include 
#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#define SIZE 1024

int main(int argc, char const *argv[])
{
    // 用于接收映射文件的内存地址
    void *addr = NULL;
    // 1.以读写的方式打开一个文件
    int fd = open("test.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    // 2.将文件映射到内存
    addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }
    printf("文件映射成功\n");

    // 3.关闭文件
    close(fd);

    // 4.通过内存写文件
    char str[] = "helloworld";
    memcpy(addr, str, sizeof(str) + 1);
    // 5.断开存储映射
    munmap(addr, 1024);
    return 0;
}

效果:

3.父子进程使用存储映射进行通信

(1)概述

**简述:**父子间使用存储映射进行通信,其实就是借助父子进程之间的代码继承的特性,在开辟子进程之前,设置映射并得到映射内存的首地址,而后子进程可以通过首地址向映射区写内容,而后父进程则可以先wait()等子进程写,写完后再读即可实现二者间的通信了

(2)示例

**代码:**下面虚线括起来的就是关键代码了

#include 
#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#include 
#define SIZE 1024

int main(int argc, char const *argv[])
{
    // 用于接收映射文件的内存地址
    void *addr = NULL;
    /* 1.以读写的方式打开一个文件 */
    int fd = open("test.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    /* 2.将文件映射到内存 */
    addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }
    printf("文件映射成功\n");

    /* 3.关闭文件 */
    close(fd);

    // 开辟子进程
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("open");
        return 1;
    }

    /*--------------------------- 子进程写输入到映射区,父进程读映射区,实现通信 ----------------------*/
    if (pid == 0)
    {
        // 子进程
        char str[] = "child_Processes_Send_Data";
        // 向共享内存中写入要传输给父进程的数据
        memcpy(addr, str, sizeof(str) + 1);
    }
    else
    {
        // 父进程,先等子进程结束,再读映射区
        wait(NULL);
        // 读取子进程传输过来的数据
        printf("%s\n", (char *)addr);
    }
    
    /*-----------------------------------------------------------------------------------------*/
    // 5.断开存储映射
    munmap(addr, 1024);
    return 0;
}

结果:

honestliu@My-Pc:~/Develop/MemorySharing$ gcc memorySharingCommunication.c
honestliu@My-Pc:~/Develop/MemorySharing$ ./a.out
文件映射成功
child_Processes_Send_Data

4.不同进程使用存储映射进行通信

(1)概述

类似于有名管道的通信,其借助了文件映射区作为通信的中介,进而实现通信。两个进程打开并映射同一个文件到内存,一个对其写操作,一个对其进行读操作。

(2)示例

写进程:

#include 
#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#include 
#define SIZE 1024

int main(int argc, char const *argv[])
{
    // 用于接收映射文件的内存地址
    void *addr = NULL;
    /* 1.以读写的方式打开一个文件 */
    int fd = open("test.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    /* 2.将文件映射到内存 */
    addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }
    printf("文件映射成功\n");

    /* 3.关闭文件 */
    close(fd);

    /*--------------------------- 写进程输入数据到映射区 ----------------------*/

        char str[] = "child_Processes_Send_Data";
        // 向共享内存中写入要传输给父进程的数据
        memcpy(addr, str, sizeof(str));
    
    /*-----------------------------------------------------------------------------------------*/
    // 5.断开存储映射
    munmap(addr, 1024);
    return 0;
}

读进程:

#include 
#include 
#include 
#include 
#include 
#include 
#include  //需要使用memset函数格式化字符串
#include 
#define SIZE 1024

int main(int argc, char const *argv[])
{
    // 用于接收映射文件的内存地址
    void *addr = NULL;
    /* 1.以读写的方式打开一个文件 */
    int fd = open("test.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    /* 2.将文件映射到内存 */
    addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }
    printf("文件映射成功\n");

    /* 3.关闭文件 */
    close(fd);

    /*--------------------------- 读取映射区数据,实现通信 ----------------------*/

    // 读取子进程传输过来的数据
    printf("%s\n", (char *)addr);

    /*-----------------------------------------------------------------------------------------*/
    // 5.断开存储映射
    munmap(addr, 1024);
    return 0;
}

结果:

honestliu@penguin:~/Develop$ ./wirte  #往映射空间写数据
文件映射成功
honestliu@penguin:~/Develop$ ./read   #往映射空间读数据
文件映射成功
child_Processes_Send_Data			  #成功读取
honestliu@penguin:~/Develop$ cat test.txt #查看映射文件
child_Processes_Send_Datafddddswquiishsbchd #前几位也被修改

5.使用匿名存储映射进行通信

最后修改:2024 年 04 月 13 日
如果觉得我的文章对你有用,请随意赞赏