IO进程day05(线程、同步、互斥、条件变量、进程间通信IPC)

目录

【1】线程

1》什么是线程

1> 概念 

2> 进程和线程的区别

3> 线程资源

 2》 函数接口

1> 创建线程:pthread_create

2> 退出线程:pthread_exit

3> 回收线程资源

练习1:通过父子进程完成对文件的拷贝(cp)

练习2:输入输出,quit结束

 【2】同步

1》概念

 2》同步机制

3》函数接口

【3】互斥

1》概念

2》函数接口 

 练习:打印倒置数组功能

补充:死锁 

【4】条件变量

概述

练习:打印和倒置数组实现同步

 【5】进程间通信 IPC

1》进程间通信方式

2》无名管道

1> 特点

2> 函数接口

 3> 注意事项


【1】线程

1》什么是线程

1> 概念 

线程一个轻量级进程为了提高系统性能引入线程

线程进程参与统一调度

在同一个进程中可以创建的多个线程, 共享进程资源。

(Linux里同样用task_struct来描述一个线程

2> 进程和线程的区别

相同点:

系统提供并发执行能力

不同点:

调度和资源: 线程系统调度最小单位; 进程资源分配最小单位

地址空间方面: 同一个进程创建的多个线程共享该进程的资源;进程的地址空间相互独立

通信方面: 线程通信相对简单,只需要通过全局变量可以实现,但是需要考虑临界资源访问的问题;进程通信比较复杂,需要借助进程间的通信机制(借助3g-4g内核空间)

安全性方面: 线程安全性差一些,当进程结束时会导致所有线程退出;进程相对安全。

程序什么时候该使用线程?什么时候用进程?

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

要求效率高、速度快的高并发环境时,需要频繁创建、销毁或切换时,资源的保护管理要求不是很高时,使用多线程。

3> 线程资源

共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID

私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性

 2》 函数接口

1> 创建线程:pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

功能:创建线程

参数: thread:线程标识

            attr:线程属性, NULL:代表设置默认属性

            start_routine:函数名:代表线程函数(自己写的)

            arg:用来给前面函数传参

返回值:成功:0

失败:错误码

编译的时候需要加 -pthread 链接动态库

#include <stdio.h>
#include <pthread.h>

//两个线程,主线程和新建的线程在宏观上是同时执行,微观上是通过时间片轮转的方式执行,所以两个线程的内容都会打印出来,但顺序不确定
void *handler_thread(void *arg)
{
    printf("in handler_thread\n");
    while (1)
        ; //不让线程退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)  //创建线程
    {
        perror("phtread err");
        return -1;
    }
    printf("in main\n");
    while(1);  //让主线程不要结束
    return 0;
}

2> 退出线程:pthread_exit

void pthread_exit(void *value_ptr)

功能:用于退出线程的执行

参数:value_ptr:线程退出时返回的值

#include <stdio.h>
#include <pthread.h>

// 两个线程,主线程和新建的线程在宏观上是同时执行,微观上是通过时间片轮转的方式执行,所以两个线程的内容都会打印出来,但顺序不确定
void *handler_thread(void *arg)
{
    pthread_exit(NULL);//退出线程
    printf("in handler_thread\n");

    while (1)
    ; // 不让线程退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0) // 创建线程
    {
        perror("phtread err");
        return -1;
    }
    printf("in main\n");
    while (1)
        ; // 让主线程不要结束
    return 0;
}

3> 回收线程资源

int pthread_join(pthread_t thread, void **value_ptr)

功能:用于等待一个指定的线程结束,阻塞函数

参数:thread:创建的线程对象,线程ID

           value_ptr:指针*value_ptr 用于指向线程返回的参数, 一般为NULL

返回值:成功 : 0

              失败:errno

int pthread_detach(pthread_t thread);

功能:让线程结束时自动回收线程资源,让线程和主线程分离,非阻塞函数

参数:thread:线程ID

非阻塞式的,例如主线程分离(detach)了线程T2,那么主线程不会阻塞在pthread_detach()pthread_detach()会直接返回,线程T2终止后会被操作系统自动回收资源

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *handler_thread(void *arg)
{
    printf("in handler_thread\n");
    sleep(3);//睡眠 3 秒之后再退出线程
    pthread_exit(NULL); //退出当前线程
    while (1)
        ; //不让线程退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0) //创建线程
    {
        perror("phtread err");
        return -1;
    }

    // pthread_join(tid, NULL);   //阻塞,等待指定的线程结束然后给其回收资源
    pthread_detach(tid);     //不阻塞,让指定线程结束时自动回收资源
    printf("in main\n");
    while (1)
        ; //让主线程不要结束
    return 0;
}

pthread_join 函数运行结果:

pthread_deacth 函数运行结果:

练习1:通过父子进程完成对文件的拷贝(cp)

通过父子进程完成对文件的拷贝(cp),父进程从文件开始到文件的一半开始拷贝,子进程从文件的一半到文件末尾。

要求:文件IO cp src dest

文件长度获取: lseek

子进程定位到文件一半: lseek

父进程怎么准确读到文件一半的位置?

fork之前打开文件,父子进程读写时,位置指针是同一个

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

int main(int argc, char const *argv[])
{
    int fd1, fd2;      // 定义两个文件描述符
    pid_t pid;         // 定义一个变量接收 fork()的返回值
    char buf[32] = ""; // 定义一个数组接受复制的数据内容,缓存区
    ssize_t n;         // 定义一个变量表示读取到的数据个数

    if (argc != 3) // 判断一下输入的格式是否正确,因为 cp 命令 是 3个参数
    {
        printf("err %s <srcfile> <destfile>\n", argv[0]); // 提示一下正确格式
        return -1;
    }
    // 执行的时候,需要写上要进行操作的两个文件,源文件和目的文件
    fd1 = open(argv[1], O_RDONLY);                           // 以可读可写权限打开源文件,argv[1],可读可写
    fd2 = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0777); // 打开目标文件,不存在创建,存在清空

    off_t len = lseek(fd1, 0, 2) / 2; // 定义一个变量,同时获取源文件长度的一半
    // 创建子进程
    pid = fork(); // 接收fork()返回值
    // 判断pid的值
    if (pid < 0) // pid 小于0,创建线程失败
    {
        perror("fork err\n");
        return -1;
    }
    else if (pid == 0) //  pid 等于 0,说明该线程是子进程,拷贝后半段
    {
        // 定位到一半的位置
        lseek(fd1, len, 0);            // 将源文件的光标移动到一半的位置
        lseek(fd2, len, 0);            // 将目标文件的光标移动到一半的位置
        while (n = read(fd1, buf, 32)) // 读取源文件的数据内容,放到缓存区
        {
            write(fd2, buf, n); // 将缓存区中读到的 n 个数据内容再写入到目的文件中
        }
        // 这样就完成了后半段文件的复制
    }
    else // 拷贝前半段
    {
        wait(NULL); // 等子进程读写完结束,回收子进程资源之后,父进程再进行拷贝
        // 定位到文件开头
        lseek(fd1, 0, 0); // 将源文件的光标移动到开头位置
        lseek(fd2, 0, 0); // 将目的文件的光标移动到开头位置

        //拷贝前半段和后半段有所区别,后半段的拷贝只需要从中间的位置读完并且写入即可,但是前半段不一样,前半段读的时候要判断前半段的数据个数够不够我们要读的个数,(我们这里读的是32个),也就是判断len 是否大于32,这里我们可以把len 理解为前半段还没读取的数据个数
        //1)如果len 大于 32,就读取32 个数据,然后让len 减去32 这样len 表示的就是前半段还没读取的数据个数
        //2)如果len 小于 32,就读取len 个数据,然后将len 置 0,表示读取完毕

        //我们采用的while判断条件是len > 0 就是我们每读取一部分数据,就让len 减掉读取的数据个数,这时 len 表示的就是前半段还没读取的数据个数,所以当 len 等于 0 时,就表示前半段已经读取完毕。
        while (len > 0)//前半段还有数据未读取,进入循环读取数据
        {
            if (len >= 32)//如果数据个数大于32个
            {
                read(fd1, buf, 32);//我们就直接读取32个
                write(fd2, buf, 32);//并向目的文件写入32个
                len -= 32;//前半段未读取数据减掉32
            }

            else//如果数据个数不够32个
            {
                read(fd1, buf, len);//我们就读取 len 个(还有多少就读取多少)
                write(fd2, buf, len);//向目的文件写入 len 个
                len = 0;//全部读取完毕,len 置 0
            }
        }
    }
    //关闭两个文件描述符
    close(fd1);
    close(fd2);
    return 0;
}

练习2:输入输出,quit结束

通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。

  1. 全局变量进行通信
  2. 加上标志位(flag),实现主线程输入一次,线程函数打印一次, int flag = 0;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>


/*练习:输入输出,quit结束
通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。
1)全局变量进行通信
2)加上标志位(flag),实现主线程输入一次,线程函数打印一次, int flag = 0;*/
int flag = 0;//定义一个标志位,用来决定两个线程执行
char buf[32];//定义一个数组用来暂时保存数据,缓存区

void *handler(void *arg)//线程函数
{
    while (1)//死循环输出
    {
        if (flag == 1)//当标志位 flag 是 1 的时候,执行该线程的循环内容
        {
            printf("%s", buf);//打印缓存区中的内容
            // fputs(buf,stdout);//用标准IO 的方式输出到终端
            flag = 0;//这个线程执行完之后,也就是打印完缓存区内容后,就将标志位 flag 置为 0
        }
    }
    return NULL;
}
int main(int argc, char const *argv[])
{

    pthread_t tid;

    if (pthread_create(&tid, NULL, handler, NULL) != 0)//创建线程
    {
        perror("err\n");
        return -1;
    }
    while (1)//死循环输出
    {
        if (flag == 0)//当标志位 flag 为 0 时,执行该线程循环内容
        {
            scanf("%s", buf);//向缓冲区中输入数据
            // fgets(buf,32,stdin);//用标准IO 的方式从终端向缓存区输入数据
            if (strcmp(buf, "quit") == 0)//用 strcmp 比较函数判断输入的是否为 quit ,是就退出循环
            {
                break;
            }
            flag = 1;//这个线程执行完之后,也就是向缓存区输入数据后,就将标志位 flag 置为 1
        }
    }
    return 0;
}

 【2】同步

1》概念

同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情。

定义:进程同步是指在多个进程执行过程中,为了防止数据的不一致性和资源的冲突,确保数据的完整性和一致性,系统对多个进程访问共享资源的顺序进行协调和控制的过程。简而言之,它是一种用于保证多个并发执行的进程能够在争夺共享资源时保持一致和协调状态的机制。

最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

异步:异步则反之,并非一定需要一件事做完在做另一件事,即不一定按照顺序执行。

 2》同步机制

通过信号量实现线程间的同步。

信号量:通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待。

信号量代表某一类资源,其值表示系统中该资源的数量:

信号量的值 > 0,表示有资源可用,可以申请到资源;

信号量的值 <= 0,表示没有资源可以用,无法申请到资源,阻塞。

信号量还是一个受保护的变量,只能通过三种操作来访问:初始化P操作(申请资源)V操作(释放资源)

sem_init:信号量初始化

sem_wait:申请资源,P操作,如果没有资源可以用阻塞等待,直到有资源可用结束阻塞资源 -1

sem_post:释放资源,V操作,非阻塞,资源 +1

3》函数接口

int sem_init(sem_t *sem, int pshared, unsigned int value)

功能:初始化信号量

参数:sem:初始化的信号量对象

          pshared:信号量共享的范围(0: 线程间使用 非0:1进程间使用)

          value:信号量初值

返回值:成功 0

              失败 -1

int sem_wait(sem_t *sem)

功能:申请资源 P操作 (使信号量减 1)

参数:sem:信号量对象

返回值:成功 0

              失败 -1

注:此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞

int sem_post(sem_t *sem)

功能:释放资源  V操作(使信号量加 1)

参数:sem:信号量对象

返回值:成功 0

              失败 -1

注:释放一次信号量的值加1,函数不阻塞

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>
#include <unistd.h>

char s[32];//定义一个数组用来存储输入的数据,缓存区
// 定义两个信号量对象控制两个线程
sem_t sem1;
sem_t sem2;

void *handler_thread(void *arg)//新线程
{
    while (1)//新线程循环
    {
        sem_wait(&sem1);//因为 sem1 的初值为 0 ,所以申请不到资源,会阻塞,直到主线程执行完之后,释放 sem 资源之后,才能申请得到资源,开始执行,sem1--以后每一次循环都要等主线程释放完sem1 资源后才能再次得到资源
        printf("%s\n", s);//打印数组 s 中的数据
        sem_post(&sem2);//释放资源,sem2++
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid; 
    if (pthread_create(&tid, NULL, handler_thread, NULL) > 0)//创建新线程
    {
        perror("err");
        return -1;
    }

    if (sem_init(&sem1, 0, 0) != 0)//初始化信号量 sem1 值为 0
    {
        printf("error\n");
        return -1;
    }
    if (sem_init(&sem2, 0, 1) != 0)//初始化信号量 sem2 值为 1
    {
        printf("error\n");
        return -1;
    }
    while (1)//主线程循环
    {

        sem_wait(&sem2);//因为 sem2 的初值为 1 ,所以可以直接申请得到资源,开始执行, sem2--  以后每次循环都需要等待新线程释放完sem2 资源之后才能再次得到资源
        scanf("%s", s);//终端输入数据到 s 数组中
        if (strcmp(s, "quit") == 0)//判断输入的数据是否为 quit
            break;//若是 quit 则退出循环
        sem_post(&sem1);//释放资源,sem++
    }
    return 0;
}

【3】互斥

1》概念

互斥:多个线程在访问临界资源时,同一时间只能一个线程访问。

定义:互斥(Mutual Exclusion)指的是在任一时刻,只允许一个进程访问某个共享资源。这种机制确保当一个进程正在使用一个共享资源时,其他进程必须等待,直到该资源被释放。互斥的主要目的是防止多个进程同时对同一共享资源进行读写,从而避免数据不一致和冲突。

最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

临界资源:一次仅允许一个线程所使用的资源

临界区:指的是一个访问共享资源的程序片段

互斥锁(mutex):通过互斥锁可以实现互斥机制。主要是用来保护临界资源,每个临界资源都有一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。

2》函数接口 

int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)

功能:初始化互斥锁

参数:mutex:互斥锁

           attr: 互斥锁属性 // NULL表示缺省属性

返回值:成功 0

              失败 -1

int pthread_mutex_lock(pthread_mutex_t *mutex)

功能:申请互斥锁

参数:mutex:互斥锁

返回值:成功 0

              失败 -1

注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回

int pthread_mutex_unlock(pthread_mutex_t *mutex)

功能:释放互斥锁

参数:mutex:互斥锁

返回值:成功 0

              失败 -1

int pthread_mutex_destroy(pthread_mutex_t *mutex)

功能:销毁互斥锁

参数:mutex:互斥锁

 练习:打印倒置数组功能

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int buf[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // 定义一个数组
pthread_mutex_t lock;                         // 定义一个锁

void *handler_Reserve(void *arg)//倒置线程
{
    while (1)
    {
        pthread_mutex_lock(&lock);//上锁,上锁之后,在解锁之前,其他线程就无法执行
        //数组内容倒置
        for (int i = 0; i < 5; i++)
        {
            int t = buf[i];
            buf[i] = buf[9 - i];
            buf[9 - i] = t;
        }
        pthread_mutex_unlock(&lock);//解锁
    }

    return NULL;
}

void *handler_print(void *arg)//打印线程
{
    while (1)
    {
        sleep(1);//让一个线程先睡眠 1 秒,这样另一个线程就可以先上锁执行,否则可能会导致冲突
        pthread_mutex_lock(&lock);//上锁
        //打印数组内容
        for (int i = 0; i < 10; i++)
            printf("%d ", buf[i]);
        printf("\n");//刷新缓存
        pthread_mutex_unlock(&lock);//解锁
    }

    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, handler_Reserve, NULL) != 0) // 创建倒置线程
    {
        perror("phtread err");
        return -1;
    }

    if (pthread_create(&tid2, NULL, handler_print, NULL) != 0) // 创建打印线程
    {
        perror("phtread err");
        return -1;
    }

    if (pthread_mutex_init(&lock, NULL) != 0) // 初始化锁 lock
    {
        perror("lock err");
        return -1;
    }

    pthread_join(tid1, NULL); // 阻塞,等待指定的线程结束然后给其回收资源
    pthread_join(tid2, NULL);
    //如果不加这两句,程序会直接执行到return 0,进程结束,线程也跟着结束
    return 0;
}

补充:死锁 

定义:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

(在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。)


死锁产生的四个必要条件

(1)互斥使用, 即当资源被一个线程使用(占有)时,别的线程不能使用

(2)不可抢占资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

(3)请求和保持即当资源请求者在请求其他的资源的同时保持对自己原有资源的占有。

(4)循环等待条件即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一种头尾相接的循环等待资源关系。


注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

【4】条件变量

概述

条件变量(cond)用于在线程之间传递信号,以便某些进程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。

条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用。条件变量之所以要和互斥锁一起使用,主要是因为互斥锁的一个明显的特点就是它只有两种状态:锁定和非锁定,而条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足,所以互斥锁和条件变量通常一起使用

       当条件满足的时候,线程通常解锁并等待该条件发生变化,一旦另一个线程修改了环境变量,就会通知相应的环境变量唤醒一个或者多个被这个条件变量阻塞的线程。这些被唤醒的线程将重新上锁,并测试条件是否满足。一般来说条件变量被用于线程间的同步;当条件不满足的时候,允许其中的一个执行流挂起和等待。

pthread_cond_init(&cond,NULL); //初始化条件变量

使用需要上锁:

pthread_mutex_lock(&lock); //上锁

判断条件

pthread_cond_wait(&cond, &lock); //阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当条件产生时结束阻塞,再次上锁

//执行任务

pthread_mutex_unlock(&lock); //解锁

pthread_cond_signal(&cond); //产生条件,不阻塞

pthread_cond_destroy(&cond); //销毁条件变量

注意: 必须保证让pthread_cond_wait先执行,pthread_cond_signal再产生条件

练习:打印和倒置数组实现同步

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int buf[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // 定义一个数组
pthread_mutex_t lock;                         // 定义一个锁
pthread_cond_t cond;                          // 定义一个条件变量
void *handler_Reserve(void *arg)              // 倒置线程
{
    while (1)
    {
        pthread_mutex_lock(&lock); // 上锁,上锁之后,在解锁之前,其他线程就无法执行
        pthread_cond_wait(&cond, &lock);//如果没条件产生解锁并阻塞,等条件产生后结束阻塞并上锁
        // 数组内容倒置
        for (int i = 0; i < 5; i++)
        {
            int t = buf[i];
            buf[i] = buf[9 - i];
            buf[9 - i] = t;
        }

        pthread_mutex_unlock(&lock); // 解锁
    }

    return NULL;
}

void *handler_print(void *arg) // 打印线程
{
    while (1)
    {
        sleep(1);                  // 让一个线程先睡眠 1 秒,这样另一个线程就可以先上锁执行,否则可能会导致冲突
        pthread_mutex_lock(&lock); // 上锁
        // 打印数组内容
        for (int i = 0; i < 10; i++)
            printf("%d ", buf[i]);
        printf("\n"); // 刷新缓存
        pthread_cond_signal(&cond);//产生条件
        pthread_mutex_unlock(&lock); // 解锁
        sleep(1);
    }

    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, handler_Reserve, NULL) != 0) // 创建倒置线程
    {
        perror("phtread err");
        return -1;
    }

    if (pthread_create(&tid2, NULL, handler_print, NULL) != 0) // 创建打印线程
    {
        perror("phtread err");
        return -1;
    }

    if (pthread_mutex_init(&lock, NULL) != 0) // 初始化锁 lock
    {
        perror("lock err");
        return -1;
    }

    if (pthread_cond_init(&cond, NULL) != 0) // 初始化条件变量 cond
    {
        perror("cond err\n");
        return -1;
    }

    pthread_join(tid1, NULL); // 阻塞,等待指定的线程结束然后给其回收资源
    pthread_join(tid2, NULL);
    // 如果不加这两句,程序会直接执行到return 0,进程结束,线程也跟着结束
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);

    return 0;
}

 【5】进程间通信 IPC

1》进程间通信方式

(1)早期进程间通信

无名管道(pipe)有名管道(fifo)信号(signal)

(2)system V PIC

共享内存(share memory)信号灯集(semaphore)消息队列(message queue)

(3)BSD:

套接字(socket)

2》无名管道

1> 特点

(1)只能用于具有亲缘关系的进程之间的通信

(2)半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。

(3)管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。

(4)管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

2> 函数接口

int pipe(int fd[2])

功能:创建无名管道

参数:文件描述符 fd[0]:读端 fd[1]:写端

返回值:成功 0

              失败 -1

#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    char buf[65536] = "";
    int fd[2] = {0}; //fd[0]代表读端,fd[1]代表写端
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }
    printf("%d %d\n", fd[0], fd[1]);

    //结构类似队列,先进先出
    //1. 当管道中无数据时,读阻塞。
    // read(fd[0], buf, 32);
    // printf("%s\n", buf);

    //但是关闭写端就不一样了
    //当管道中有数据关闭写端可以读出数据,无数据时关闭写端读操作会立即返回。
    // write(fd[1], "hello", 5);
    // close(fd[1]);
    // read(fd[0], buf, 32);
    // printf("%s\n", buf);

    //2. 当管道中写满数据时,写阻塞,管道空间大小为64K
    // write(fd[1], buf, 65536);
    // printf("full!\n");
    //write(fd[1], "a", 1);  //当管道写满时不能再继续写了会阻塞

    //写满一次之后,当管道中至少有4K空间时(也就是读出4K),才可以继续写,否则阻塞。
    // read(fd[0], buf, 4096); //换成4095后面再写就阻塞了,因为不到4K空间
    // write(fd[1], "a", 1);

    //3. 当读端关闭,往管道中写入数据无意义,会造成管道破裂,进程收到内核发送的SIGPIPE信号。
    close(fd[0]);
    write(fd[1], "a", 1);
    printf("read close\n");

    return 0;
}

gdb调试可以看见管道破裂信号:

gcc -g xx.c

gdb a.out

r

 3> 注意事项

(1)当管道中无数据时,读操作会阻塞

管道中有数据,将写端关闭,可以将数据读出

管道中无数据,将写端关闭,读操作会立即返回

(2)管道中装满(管道大小64K)数据写阻塞,一旦由 4k 空间,写继续

(3)只有在管道的读端存在时,向管道中写入数据才有意义,否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号(通常Broken pipe错误)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    char buf[32];//定义一个数组
    int fd[2];//定义两个文件描述符
    if (pipe(fd) < 0)//创建管道并判断
    {
        perror("pipe err\n");
        return -1;
    }
    printf("%d %d\n", fd[0], fd[1]);//打印一下文件描述符

    int pid;
    pid = fork();//创建父子进程
    if (pid < 0)//创建失败
    {
        perror("fork err\n");
        return -1;
    }
    else if (pid == 0)//子进程
    {
        while (1)
        {
            read(fd[0], buf, 32);//从读端读取管道中的数据到buf中
            printf("%s\n", buf);//打印buf中的数据
        }
    }
    else//父进程
    {
        while (1)
        {
            scanf("%s", buf);//从终端输入数据到buf中
            if (strcmp(buf, "quit") == 0)//判断输入的是否为 quit,若是,则退出循环
            {
                break;
            }
            write(fd[1], buf, 32);//将 buf 中的数据从写端写到管道中
        }
    }
    return 0;
}

今天的分享就到这里结束啦,如果有哪里写的不好的地方,请指正。
如果觉得不错并且对你有帮助的话点个关注支持一下吧!

  • 18
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值