首页 > 系统相关 >I\O进程线程(Day31)

I\O进程线程(Day31)

时间:2024-10-21 08:53:20浏览次数:7  
标签:wbuf Day31 管道 线程 pthread 进程 rbuf

一、学习内容

  1. 线程的同步互斥机制

    1. 同步机制之条件变量

      1. 概念

        1> 条件变量实现的是一个生产者对多个消费者问题
        2> 条件变量本质上维护了一个队列,所有消费者线程想要执行之前先要进入该队列中。等待生产者线程来唤醒。先进入等待队列中的线程被先唤醒。由于,对于消费者而言,这个等待队列相当于是一个临界资源,当两个线程同时想要进入等待队列时,会产生竞态,此时我们需要引入互斥锁来解决。

      2. 创建条件变量
        pthread_cond_t cond ;
      3. 初始化条件变量
        int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
        1. 返回值
          成功返回0,失败返回非0错误码
        2. 参数

          参数1: 条件变量的地址

          参数2:条件变量的属性,一般填NULL

        3. 功能
          初始化条件变量
      4. 消费者线程进入阻塞队列
        int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
        1. 返回值
          成功返回0,失败返回非0错误码
        2. 参数

          参数1:条件变量的地址

          参数2:互斥锁地址

        3. 功能
          进入等待队列
      5. 唤醒消费者线程
        int pthread_cond_signal(pthread_cond_t *cond);
        1. 返回值
          成功返回0,失败返回非0错误码
        2. 参数
          条件变量的地址
        3. 功能
          唤醒所有等待队列中的消费者线程
      6. 唤醒消费者线程
        int pthread_cond_signal(pthread_cond_t *cond);
        1. 返回值
          成功返回0,失败返回非0错误码
        2. 参数
          条件变量的地址
        3. 功能
          唤醒在队头的消费者线程
      7. 销毁条件变量
        int pthread_cond_destroy(pthread_cond_t *cond);
        1. 返回值
          成功返回0,失败返回非0错误码
        2. 参数
          条件变量的地址
        3. 功能
          销毁条件变量
  2. 进程之间的通信(nterprocess communication)

    • 概念
      • 1>可以使用一个进程向文件中写入数据,另一个进程从文件中读取数据。但是,进程的执行没有先后顺序,对于这种方法,必须要保证写进程先执行,读进程后执行,需要保证进程的同步
        2>多个进程之间用户空间相互独立,但是内核空间是共享,我们可以通过内核空间进行数据的传递

    • 分类

      • 内核提供的通信方式
        • 无名管道

        • 有名管道

        • 信号

      • system V提供的通信方式
        • 消息队列

        • 共享内存

        • 信号量集

      • 套接字通信(socket)
        跨主机的多进程间的通信
    • 无名管道

      • 概念

        1> 顾名思义就是没有名字的管道文件

        2> 是一种特殊的文件,会在内存中创建出来,并且返回该文件的两个文件描述符,分别对应该文件的读端和写端

        3> 由于该管道文件没有名字,故,不能用于非亲缘进程间的通信,只能用于亲缘进程间的通信

        4> 由于管道文件返回的是文件描述符,所以,只能使用文件IO相关函数进行操作

        5> 对于管道文件的操作是一次性的,当写入管道中的数据,被某个进程读取后,数据就不存在了

        6> 由于是一次性的操作,所以对于管道文件的操作是不能使用lseek进行光标偏移的

        7> 管道通信方式属于半双工的通信方式

        1、单工:任意时刻,都只能是A向B发送数据,B不能向A发送数据
        2、半双工:同一时刻,只能是A向B发送数据,或者是B向A发送数据

        3、全双工:任意时刻,AB可以互相通信

      • API
        int pipe(int pipefd[2]);
        • 返回值
          成功返回0,失败返回-1并置位错误码
        • 参数
          用于接收创建的管道文件的文件描述符,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
        • 功能
          在内核空间创建一个管道文件,并返回该文件的两端的文件描述符
      • 特点

        1、管道文件的大小:64KB

        2、无名管道适用于亲缘进程间通信,也适用于自己跟自己通信

      • 读写特点
        • 当读管道存在时
          写管道有多少写多少,写满64K后在write处阻塞
        • 当读管道不存在时
          写管道管道破裂,内核空间会向用户空间发送一个SIGPIPE信号,进程收到该信号后,默认杀死进程
        • 当写管道存在时
          读管道有多少读多少,没有数据。会在read处阻塞
        • 当写管道不存在时
          读管道有多少读多少,没有数据,也不会在read处阻塞
    • 有名管道

      • 概念

        1> 顾名思义就是有名字的管道通信,在通信过程中,会在文件系统上创建一个实实在在的管道文件(p)

        2> 有名管道既可以实现亲缘进程间通信,也可以实现非亲缘进程间的通信

        3> 虽然会在文件系统中创建一个管道文件,但是该文件只用于通信使用,不用于数据的存储

        4> 对于有名管道文件的操作也是一次性的,写入到管道中的文件数据被读取后就不存在了

        5> 当某个进程,以只写的形式打开文件时,如果没有一个进程以只读的形式打开该文件,那么上一个进程就会在open处阻塞 反之亦然。

        6> 有名管道的读写特点跟无名管道的读写特点保持一致

      • API
        int mkfifo(const char *pathname, mode_t mode);
        • 返回值
          功返回0, 失败返回-1并置位错误码
        • 参数
          • 参数1:要创建的管道文件的文件名

          • 参数2:管道文件的权限

        • 功能
          在文件系统中创建一个有名管道文件
  3. 脑图

二、作业

作业1:

按照图中实例完成使用有名管道实现两个进程之间的相互通信

代码解答:

方法一:进程法
文件create.c(创建管道文件)
#include <myhead.h> // 包含自定义的头文件
int main(int argc, const char *argv[])
{
    // 创建名为 "fifoAtoB" 的有名管道,权限为0664(可读写)
    if (mkfifo("./fifoAtoB", 0664) == -1)
    {
        // 如果创建失败,输出错误信息并返回 -1
        perror("mkfifo error for fifoAtoB");
        return -1;
    }

    // 创建名为 "fifoBtoA" 的有名管道,权限为0664
    if (mkfifo("./fifoBtoA", 0664) == -1)
    {
        // 如果创建失败,输出错误信息并返回 -1
        perror("mkfifo error for fifoBtoA");
        return -1;
    }
    
    getchar(); // 等待用户输入,暂停程序以防管道立即被删除

    // 删除 "fifoAtoB" 管道文件
    unlink("fifoAtoB");

    // 删除 "fifoBtoA" 管道文件
    unlink("fifoBtoA");

    return 0; // 正常退出程序
}
 文件snd.c(进程A)
#include <myhead.h>      

int main(int argc, const char *argv[])
{
    // 创建子进程
    pid_t pid = fork();

    // 如果是父进程,执行发送操作
    if (pid > 0) 
    {
        // 打开管道文件 "fifoAtoB",以只写方式
        int wfd = open("./fifoAtoB", O_WRONLY);
        if (wfd == -1)
        {
            // 打开失败,打印错误信息
            perror("open error for fifoAtoB");
            return -1;  // 返回错误码 -1
        }

        char wbuf[128] = "";  // 定义写缓冲区
        while (1)
        {
            // 提示用户输入并从标准输入读取字符串
            printf("A进程父进程,请输入>>>");
            fgets(wbuf, sizeof(wbuf), stdin);
            wbuf[strlen(wbuf) - 1] = '\0';  // 去掉末尾的换行符

            // 将用户输入写入管道
            write(wfd, wbuf, strlen(wbuf));
            printf("A进程父进程发送成功\n");

            // 如果输入为 "quit",则退出循环
            if (strcmp(wbuf, "quit") == 0)
            {
                break;
            }
        }

        // 关闭管道文件描述符
        close(wfd);
    }
    // 如果是子进程,执行接收操作
    else if (pid == 0) 
    {
        // 打开管道文件 "fifoBtoA",以只读方式
        int rfd = open("./fifoBtoA", O_RDONLY);
        if (rfd == -1)
        {
            // 打开失败,打印错误信息
            perror("open error for fifoBtoA");
            return -1;  // 返回错误码 -1
        }

        char rbuf[128] = "";  // 定义读缓冲区
        while (1)
        {
            // 清空缓冲区并从管道读取数据
            bzero(rbuf, sizeof(rbuf));
            read(rfd, rbuf, sizeof(rbuf));
            // 打印收到的数据
            printf("A进程子进程收到消息为:%s\n", rbuf);

            // 如果收到的数据为 "quit",则退出循环
            if (strcmp(rbuf, "quit") == 0)
            {
                break;
            }
        }

        // 关闭管道文件描述符
        close(rfd);
    }
    // 如果创建子进程失败
    else
    {
        // 打印错误信息
        perror("fork error");
        return -1;  // 返回错误码 -1
    }

    return 0;  // 正常退出
}
文件recv.c(进程B)

#include <myhead.h>      // 包含自定义头文件
#include <sys/types.h>   // 包含系统类型头文件
#include <unistd.h>      // 包含 POSIX 标准头文件

int main(int argc, const char *argv[])
{
    // 创建子进程
    pid_t pid = fork();

    // 如果是父进程,执行接收操作
    if (pid > 0) 
    {
        // 打开管道文件 "fifoAtoB",以只读方式
        int rfd = open("./fifoAtoB", O_RDONLY);
        if (rfd == -1)
        {
            // 打开失败,打印错误信息
            perror("open error for fifoAtoB");
            return -1;  // 返回错误码 -1
        }

        char rbuf[128] = "";  // 定义读缓冲区
        while (1)
        {
            // 清空缓冲区并从管道读取数据
            bzero(rbuf, sizeof(rbuf));
            read(rfd, rbuf, sizeof(rbuf));
            // 打印收到的数据
            printf("B进程父进程收到消息为:%s\n", rbuf);

            // 如果收到的数据为 "quit",则退出循环
            if (strcmp(rbuf, "quit") == 0)
            {
                break;
            }
        }

        // 关闭管道文件描述符
        close(rfd);
    }
    // 如果是子进程,执行发送操作
    else if (pid == 0) 
    {
        // 打开管道文件 "fifoBtoA",以只写方式
        int wfd = open("./fifoBtoA", O_WRONLY);
        if (wfd == -1)
        {
            // 打开失败,打印错误信息
            perror("open error for fifoBtoA");
            return -1;  // 返回错误码 -1
        }

        char wbuf[128] = "";  // 定义写缓冲区
        while (1)
        {
            // 提示用户输入并从标准输入读取字符串
            printf("B进程子进程,请输入>>>");
            fgets(wbuf, sizeof(wbuf), stdin);
            wbuf[strlen(wbuf) - 1] = '\0';  // 去掉末尾的换行符

            // 将用户输入写入管道
            write(wfd, wbuf, strlen(wbuf));
            printf("B进程子进程发送成功\n");

            // 如果输入为 "quit",则退出循环
            if (strcmp(wbuf, "quit") == 0)
            {
                break;
            }
        }

        // 关闭管道文件描述符
        close(wfd);
    }
    // 如果创建子进程失败
    else
    {
        // 打印错误信息
        perror("fork error");
        return -1;  // 返回错误码 -1
    }

    return 0;  // 正常退出
}
方法二:线程法
文件create.c(创建管道文件)
#include <myhead.h> 

int main(int argc, const char *argv[])
{
    // 创建一个有名管道文件 fifoAtoB,权限为 0664
    if (mkfifo("./fifoAtoB", 0664) == -1)
    {
        perror("mkfifo error for fifoAtoB"); // 如果创建失败,输出错误信息
        return -1; // 返回 -1 表示失败
    }

    // 创建另一个有名管道文件 fifoBtoA,权限为 0664
    if (mkfifo("./fifoBtoA", 0664) == -1)
    {
        perror("mkfifo error for fifoBtoA"); // 如果创建失败,输出错误信息
        return -1; // 返回 -1 表示失败
    }

    getchar(); // 等待用户输入,暂停程序执行

    // 删除创建的有名管道文件 fifoAtoB
    unlink("fifoAtoB");
    // 删除创建的有名管道文件 fifoBtoA
    unlink("fifoBtoA");

    return 0; // 返回 0 表示程序成功执行
}
snd.c(进程A)
#include <myhead.h> 

// 发送数据到 B 进程的函数
void *send_to_b(void *arg)
{
    // 打开管道 fifoAtoB 进行写操作
    int wfd = open("./fifoAtoB", O_WRONLY);
    if (wfd == -1)
    {
        perror("open error for fifoAtoB"); // 如果打开失败,输出错误信息
        pthread_exit(NULL); // 线程退出
    }

    char wbuf[128] = ""; // 定义存放数据的缓冲区
    while (1)
    {
        // 提示用户输入数据
        printf("A进程父线程,请输入>>> ");
        fgets(wbuf, sizeof(wbuf), stdin); // 从标准输入读取数据
        wbuf[strlen(wbuf) - 1] = '\0'; // 去掉输入的换行符

        // 将数据写入管道
        write(wfd, wbuf, strlen(wbuf));
        printf("A进程父线程发送成功\n");

        // 如果输入 "quit" 则退出循环
        if (strcmp(wbuf, "quit") == 0)
        {
            break;
        }
    }
    close(wfd); // 关闭写管道
    pthread_exit(NULL); // 线程退出
}

// 从 B 进程接收数据的函数
void *receive_from_b(void *arg)
{
    // 打开管道 fifoBtoA 进行读操作
    int rfd = open("./fifoBtoA", O_RDONLY);
    if (rfd == -1)
    {
        perror("open error for fifoBtoA"); // 如果打开失败,输出错误信息
        pthread_exit(NULL); // 线程退出
    }

    char rbuf[128] = ""; // 定义存放数据的缓冲区
    while (1)
    {
        bzero(rbuf, sizeof(rbuf)); // 清空缓冲区
        read(rfd, rbuf, sizeof(rbuf)); // 从管道读取数据
        printf("A进程子线程收到消息:%s\n", rbuf); // 打印接收到的消息

        // 如果接收到 "quit" 则退出循环
        if (strcmp(rbuf, "quit") == 0)
        {
            break;
        }
    }
    close(rfd); // 关闭读管道
    pthread_exit(NULL); // 线程退出
}

int main(int argc, const char *argv[])
{
    pthread_t tid1, tid2; // 定义两个线程ID

    // 创建父线程,负责发送数据到 B 进程
    pthread_create(&tid1, NULL, send_to_b, NULL);

    // 创建子线程,负责接收来自 B 进程的数据
    pthread_create(&tid2, NULL, receive_from_b, NULL);

    // 等待两个线程完成
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0; // 程序成功结束
}
 recv.c(进程B)

#include <myhead.h> 

// 从 A 进程接收数据的函数
void *receive_from_a(void *arg)
{
    // 打开管道 fifoAtoB 进行读操作
    int rfd = open("./fifoAtoB", O_RDONLY);
    if (rfd == -1)
    {
        perror("open error for fifoAtoB"); // 如果打开失败,输出错误信息
        pthread_exit(NULL); // 线程退出
    }

    char rbuf[128] = ""; // 定义存放数据的缓冲区
    while (1)
    {
        bzero(rbuf, sizeof(rbuf)); // 清空缓冲区
        read(rfd, rbuf, sizeof(rbuf)); // 从管道读取数据
        printf("B进程父线程收到消息:%s\n", rbuf); // 打印接收到的消息

        // 如果接收到 "quit" 则退出循环
        if (strcmp(rbuf, "quit") == 0)
        {
            break;
        }
    }
    close(rfd); // 关闭读管道
    pthread_exit(NULL); // 线程退出
}

// 发送数据到 A 进程的函数
void *send_to_a(void *arg)
{
    // 打开管道 fifoBtoA 进行写操作
    int wfd = open("./fifoBtoA", O_WRONLY);
    if (wfd == -1)
    {
        perror("open error for fifoBtoA"); // 如果打开失败,输出错误信息
        pthread_exit(NULL); // 线程退出
    }

    char wbuf[128] = ""; // 定义存放数据的缓冲区
    while (1)
    {
        // 提示用户输入数据
        printf("B进程子线程,请输入>>> ");
        fgets(wbuf, sizeof(wbuf), stdin); // 从标准输入读取数据
        wbuf[strlen(wbuf) - 1] = '\0'; // 去掉输入的换行符

        // 将数据写入管道
        write(wfd, wbuf, strlen(wbuf));
        printf("B进程子线程发送成功\n");

        // 如果输入 "quit" 则退出循环
        if (strcmp(wbuf, "quit") == 0)
        {
            break;
        }
    }
    close(wfd); // 关闭写管道
    pthread_exit(NULL); // 线程退出
}

int main(int argc, const char *argv[])
{
    pthread_t tid1, tid2; // 定义两个线程ID

    // 创建父线程,负责接收来自 A 进程的数据
    pthread_create(&tid1, NULL, receive_from_a, NULL);

    // 创建子线程,负责发送数据到 A 进程
    pthread_create(&tid2, NULL, send_to_a, NULL);

    // 等待两个线程完成
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0; // 程序成功结束
}

成果展示:

进程法:

线程法:

 

三、总结

学习内容概述:

1. 条件变量的同步机制

学习了使用条件变量进行线程同步的基本操作,包括创建、初始化、等待、唤醒和销毁条件变量的函数,以及如何使用条件变量实现生产者-消费者模型。

2. 进程间通信(IPC)

介绍了进程之间的通信方式,尤其是无名管道和有名管道的概念和用法,重点讨论了无名管道的创建与读写特点,有名管道的创建方法。

3. 管道的使用

详细介绍了无名管道的特点和读写机制,包括管道的阻塞与非阻塞行为,以及有名管道的创建和使用。

学习难点:

1. 条件变量的理解和使用

条件变量与互斥锁配合使用,掌握条件变量的核心机制(`pthread_cond_wait` 和 `pthread_cond_signal`)是关键,尤其在生产者-消费者模型中,如何正确处理线程的等待与唤醒,以及避免虚假唤醒。

2. 无名管道的读写特点

管道读写的阻塞条件、数据满与空的处理是难点之一,尤其是在处理写端关闭时的管道破裂情况以及 SIGPIPE 信号的处理。

3. 进程间通信的选择

在多种 IPC 机制中,如何选择最适合的通信方式,如无名管道与有名管道的区别及应用场景,特别是在跨进程的读写同步时的细节处理。

主要事项:

1. 条件变量的同步机制

条件变量通常与互斥锁结合使用,以避免竞态条件。
使用 `pthread_cond_wait` 将线程挂起并释放互斥锁,直到条件变量满足。
使用 `pthread_cond_signal` 唤醒等待线程,确保适时唤醒合适的消费者线程。

2. 无名管道的使用:

无名管道的创建通过 `pipe` 函数,读端和写端的文件描述符需要分别处理读写。
理解管道的读写阻塞机制,尤其在写满管道(64K)时如何阻塞写入,防止数据丢失。
处理管道破裂的情况,当读端不存在时写入会收到 SIGPIPE 信号。

3. 有名管道的使用:

有名管道的创建需要使用 `mkfifo`,并能够持久化存在于文件系统中,便于不同进程之间的通信。

有名管道的特点在于可以跨进程甚至跨系统通信。

未来学习的重点:

1. 条件变量与其他同步机制的结合:

学习如何将条件变量与互斥锁、信号量等其他同步机制结合使用,处理更加复杂的线程同步问题。

2. 高级进程间通信(IPC)机制

如共享内存、消息队列等,通过这些机制提升进程间通信的效率,了解在不同场景中如何选择合适的 IPC 方法。

3. 管道通信的应用场景

深入理解无名管道和有名管道的实际应用场景,特别是在跨进程的生产者-消费者模型中的高效通信。

4. Socket通信:

学习跨主机通信的基础,即基于套接字的进程通信,这是处理分布式系统和网络编程的重要基础。

标签:wbuf,Day31,管道,线程,pthread,进程,rbuf
From: https://blog.csdn.net/weixin_65095004/article/details/143093158

相关文章

  • 从多线程到 epoll:如何优雅地处理高并发请求?
    文章参考于:小林coding最近在学习操作系统,服务器与客户端之间的通信离不开socket编程。然而,基于TCP的socket编程在默认情况下只能实现一对一的通信,因为它采用同步阻塞模型。在服务器处理完当前客户端的请求之前,无法响应其他客户端的请求。这种方式效率不高,显然浪费了......
  • IO进程_day1
    目录概要标准IO1.概念:2.特点:3.缓存区3.1行缓存:和终端操作相关3.2全缓存:和文件操作相关​​​​​​​3.3不缓存:标准错误stderr4.函数​​​​​​​4.1打开文件​​​​​​​4.2关闭文件​​​​​​​4.3读写文件​​​​​​​4.3.1每次一个字符的读写fgetc......
  • shell编程小技巧:进程替换
    今天来给大家介绍一个非常好用的shell编程技巧,即进程替换(Processsubstitution)。进程替换可以将一个进程(程序)的输入或输出当做一个文件来使用。它的两种使用形式为:<(cmd)或>(cmd).需要注意的是,<和>与(之间不能有空格!下面通过一个示例来介绍进程替换的具体用法。假如我有一个......
  • 线程常用的几种使用方式?
    在Java中,线程可以通过几种不同的方式进行创建和使用。以下是常用的几种方式:1.继承Thread类这种方式通过创建一个子类,继承自Thread类,并重写其run()方法来定义线程的行为。示例代码:classMyThreadextendsThread{@Overridepublicvoidrun(){......
  • 线程创建的几种方式,你都知道吗?
    使用继承Thread类的方法来创建线程,分别表示兔子和乌龟的比赛。classTurtleextendsThread{@Overridepublicvoidrun(){System.out.println("乌龟开始赛跑!");for(inti=1;i<=10;i++){System.out.println("乌龟跑了......
  • Java中的进程与线程(如果想知道Java中有关进程与线程的知识点,那么只看这一篇就足够了!)
        前言:在现代计算机系统中,进程和线程是实现并发和高效任务管理的核心概念。理解这两者的区别和联系,不仅对软件开发者至关重要,还能帮助用户更好地理解计算机的工作原理。✨✨✨这里是秋刀鱼不做梦的BLOG✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN......
  • JavaScript事件循环:一杯咖啡的时间,搞懂主线程都经历了什么?
    我们今天来聊聊JavaScript事件循环。虽然这个词听起来很高深,但你可以把它想象成一个奶茶店里排队买奶茶的过程。主线程就像奶茶店的唯一一个店员,任务就是那些排队的订单,而JavaScript的事件循环就是这个店员处理订单的工作方式。先看代码,咱们慢慢聊:console.log('1:进店......
  • STA模型、同步上下文和多线程、异步调度
    写过任何桌面应用,尤其是WinForm的朋友们应该见过,Main函数上放置了一个[STAThread]的Attribute。而且几乎所有的桌面应用框架,都是由同一个UI线程来执行渲染操作的,这表现为从其他线程修改控件的值就会抛异常:awaitTask.Run(()=>control.Content="");//throwsexception大家......
  • 【Linux】Linux进程地址空间
    1.程序地址空间分配回顾在前⾯C语⾔以及C部分介绍过⼆者的内存分配如下图所示:全局变量区和未初始化全局变量区也被称为数据区,数据区中除了有全局变量,还有静态变量和常量使⽤下⾯的代码演示不同的内容所处的地址:#include<stdio.h>#include<unistd.h>#include<stdli......
  • 6-2.Android 对话框之基础对话框问题清单(UI 线程问题、外部取消、冲突问题、dismiss
    对话框对话框(Dialog)是一种常用的UI组件,它主要用于显示信息、接收用户操作反馈对话框可以包含各种元素,但是主要还是以文本、按钮为主,其次是列表其中,基础对话框是Android中最简单的对话框,而后是进度对话框、自定义对话框等一、UI线程问题1、UI线程中创建对话......