Linux进程间通信

提示:文章内容较长,请参考目录阅读

前言:进程间通信介绍

目的
每一个进程的数据都存储在物理内存当中,在访问空间时,通过各自的页表映射关系访问到物理内存,从进程的角度看,每个进程都拥有4G的虚拟空间,但是进程并不清楚数据在物理内存中如何存储,页表如何映射,所以进程具有独立性。而进程间通信的本质就是进程之间的数据交换。
常见进程间通信方式
管道、共享内存、消息队列、信号量、信号、网络,其中网络是最大的,应用最广泛的进程间通信方式。

一、管道

管道是什么

管道符号:“|”
管道的本质:内核中的一块缓冲区,供进程进行读写,交换数据。 这里的内核指的是内核空间,与用户空间对应。

匿名管道

接口简介

int pipe(int pipefd[2]);
  • 功能:创建匿名管道
  • 参数:pipefd[]是一个整型数组,为出参,在调用函数中定义,在pipe函数中进行赋值,其中fd[0]表示读端,fd[1]表示写端
  • 返回值:创建成功返回0,失败返回-1,并设置错误码
    在这里插入图片描述

程序

#include<stdio.h>
#include<unistd.h>
int main()
{
    int fd[2];//定义一个数组,作为pipe函数的参数
    int ret=pipe(fd);
    if(ret<0)
    {
    //创建失败
    	perror("pipe");
        return 0;
    }
    //创建成功,打印文件描述符
    printf("fd[0]:%d\n",fd[0]);
    printf("fd[1]:%d\n",fd[1]);
    printf("My pid:%d\n",getpid());
    while(1)
    {
        sleep(1);//程序不退出
    }
    return 0;
}

运行结果
在这里插入图片描述
查看该进程fd文件
在这里插入图片描述
可以看到,3号和4号文件描述符对应的正是管道的读写两端。

fork共享管道

原理
不同的进程,要用同一个匿名管道进行通信,则需要进程拥有该管道的读写两端的文件描述符,所以匿名管道只让能具有亲缘关系的进程进行进程间间通信,且父进程需要先创建匿名管道,再创建子进程。如图所示:
在这里插入图片描述

上图中管道在内核空间,而父子进程的程序都在用户空间中。可以令子进程进行写,父进程进行读,关闭各自不用的文件描述符,仍可以进行单向通信。
 吗
站在文件描述符角度理解管道

  • 父进程创建管道
    在这里插入图片描述

  • 父进程调用fork创建子进程
    在这里插入图片描述

  • 父进程关闭fd[0],子进程关闭fd[1]
    在这里插入图片描述

程序
在这里插入图片描述

匿名管道的特性

半双工
即数据只能从管道的写端流向读端,不支持双向通信,如图:
在这里插入图片描述

无标识符
所以匿名管道只支持具有亲缘关系的进程间通信。这里需要注意的是,有亲缘关系的进程要进行通信也需要满足一定条件,即==父进程先创建匿名管道,后创建子进程,这样才能保证父子进程同时拥有该管道读写两端的文件描述符。
生命周期
管道的生命周期跟随进程,进程退出后,管道也会被销毁
大小
管道的大小为64k,可以通过一个程序进行验证,如图:
在这里插入图片描述
这个程序会死循环向管道写入,不进行读取,每写入一个字符打印一次,可以看到打印结果最终停在了65536,说明此时管道已经写满,由此可以知道管道大小为65536字节,64k。

字节流

  1. 即管道的读或写都是将管道内容读走或者写走,而不是拷贝,通过程序验证:
    在这里插入图片描述
  2. 管道的读端还可以定义要读的内容的,将上面的程序稍作修改验证这一特性。
    在这里插入图片描述
    关于 pipe_size
    pipe_size是保证通过管道进行读写时的原子性的阈值,可以通过ulimit -a 指令查看。
    在这里插入图片描述

原子性:一个操作要么不间断地全部执行,要么一个也不执行,没有中间状态。
对于管道来说,原子性的含义就是,当一个进程往管道中进行写的时候,如果写入的内容小于4096字节,则另一个进程不会进行读。
阻塞属性
调用pipe创建出来的读写两端文件描述符的属性默认都是阻塞属性。write一直进行写,不去读,则写满之后write阻塞;read一直进行读,管道内部被读完之后,read会阻塞。

  • 写阻塞
    在这里插入图片描述
  • 读阻塞
    在这里插入图片描述

设置管道的非阻塞属性

接口简介

int fcntl(int fd, int cmd, ... /* arg */ );

参数

  • fd,待要操作的文件描述符
  • cmd,告知fcntl函数要进行的操作
    F_GETFL:获取文件描述符属性信息
    F_SETFL:设置文件描述符属性信息,设置新的属性信息放到可变参数列表中

返回值

  • F_GETFL:返回文件描述符属性信息
  • F_SETFL:0,设置成功;-1,设置失败

设置非阻塞属性步骤

  1. 获取文件描述符原来的属性:
 int flag=fcntl(fd[0],F_GETFL);
  1. 设置文件描述符新的属性,要将原来的属性也带上:新的属性=原有属性 | 增加的属性
fcntl(fd[0],F_SETFL,flag | O_NONBLOCK);

设置读端为非阻塞属性

#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret<0)
    {
        perror("pipe");
        return 0;
    }

    int pid=fork();
    //父进程进行写,子进程进行读
    //验证父进程分别关闭\不关闭写时的结果
    if(pid<0)
    {
        perror("fork");
        return 0;
    }
    else if(pid==0)
    {
        //child;
        //设置读端为非阻塞属性
        close(fd[1]);
        int flag=fcntl(fd[0],F_GETFL);
        fcntl(fd[0],F_SETFL,flag | O_NONBLOCK);

        char read_buf[1024]={0};
        ssize_t read_size=read(fd[0],read_buf,sizeof(read_buf)-1);
        printf("%ld\n",read_size);
        perror("read");
    }
    else
    {
        //parent
        //写端关闭\不关闭
        close(fd[0]);
        //close(fd[1]);
        while(1)
        {
            sleep(1);
        }
    }
return 0;

运行结果

  • 读端为非阻塞,父进程不关闭写
    在这里插入图片描述
    结果分析:管道中没有内容,进行读取,返回-1,errno设置为EAGAIN,资源暂时不可用,表示可以再进行读。

  • 读端为非阻塞,父进程关闭写
    在这里插入图片描述
    结果分析:读取成功,但没有读到内容

设置写端为非阻塞属性

#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret<0)
    {
        perror("pipe");
        return 0;
    }

    int pid=fork();
    //子进程进行写,父进程进行读
    //设置写端为非阻塞属性
    //关闭\不关闭读端
    if(pid<0)
    {
        perror("fork");
        return 0;
    }
    else if(pid==0)
    {
        //child,write
        close(fd[0]);
        int flag=fcntl(fd[1],F_GETFL);
        fcntl(fd[1],F_SETFL,flag | O_NONBLOCK);//设置fd[1]为非阻塞属性

        //sleep(1);
        ssize_t write_size=0;
        while(1)
        {
            //非阻塞写入一般搭配循环使用
             write_size=write(fd[1],"a",1);
             if(write_size<0)
             {
                 break;
             }
        }
        printf("%ld\n",write_size);
        perror("write");
    }
    else
    {
        //parent,read
        close(fd[1]);
        //close(fd[0]);
        while(1)
        {
            sleep(1);
        }
    }

return 0;
}

运行结果

  • 子进程一直写入,父进程不关闭读
    在这里插入图片描述
    结果分析:write管道被写满,资源暂时不可用,因为管道没有空闲空间了。
  • 子进程一直写入,父进程关闭读
    在这里插入图片描述
    结果分析:父进程关闭读后,没有进程从管道读取内容,子进程向管道写入内容后,管道破碎,调用写端的进程收到SIGPIPE,导致进程崩溃。

管道读写规则总结

  • 当没有数据可读时
    未设置O_NONBLOKC: read调用阻塞,等到有数据来为止
    设置O_NONBLOCK:read返回-1,errno值为EAGAIN

  • 当管道写满时
    未设置O_NONBLOKC:write调用阻塞,等到有数据被读走为止
    设置O_NONBLOCK: write返回-1,errno值为EAGAIN

  • 若写端文件描述符关闭,read会返回0

  • 若读端文件描述符被关闭,调用write会产生SIGPIPE信号,管道破碎,进程退出

扩展知识:宏与位图

在设置文件的非阻塞属性时,我们用O_NONBLOCK与原有属性进行按位或,获得了新的属性,在学习文件操作接口时,opend的打开方式也用到了按位或,如:open(“./1.txt”,O_RDWR | O_CREAT,0664),表示打开当前路径下的"1.txt"文件,若不存在,则创建这个文件,两种打开方式之间是按位或的关系。为什么通过按位或就能添加一个新的属性呢?写一个程序对此进行验证。
通过下面这个程序获取文件描述符修改前后的值:
在这里插入图片描述
运行结果:
在这里插入图片描述
1和2049的二进制表示如图:
在这里插入图片描述
在Linux内核源码中查找关于O_NONBLOCK的定义:
在这里插入图片描述
fcntl.h文件中对于文件操作相关宏的定义:
在这里插入图片描述

命名管道

创建命名管道

函数创建
在这里插入图片描述
通过在程序中调用函数创建一个命名管道
在这里插入图片描述在这里插入图片描述
** 指令创建**

mkfifo filename

在这里插入图片描述
在这里插入图片描述

通过命名管道进行进程间通信

在这里插入图片描述
运行结果
在这里插入图片描述

二、共享内存

原理

在物理内存中开辟一段空间,不同进程通过页表将物理内存映射到自己的进程虚拟地址空间中,通过操作自己进程虚拟地址空间中的地址,来操作共享内存。
共享内存:
在这里插入图片描述映射关系
在这里插入图片描述

共享内存函数

shmget

#include <sys/shm.h>//共享内存相关函数都包含在此头文件中
int shmget(key_t key, size_t size, int shmflg);
  • 功能:创建或者获取共享内存
  • 参数
    key:共享内存标识符。
    size:共享内存大小。
    shmglg: 创建共享内存时传递的属性信息,用法与创建文件时mode的用法相同
    IPC_CREAT:若获取的共享内存不存在,则创建。
    IPC_EXCL | IPC_CREAT: 如果获取的共享内存存在,则创建;不存在,则创建。这个操作本质上时获取重新创建的共享内存。后面按位或权限,如:IPC_EXCL | IPC_CREAT | 0664。
  • 返回值
    成功:返回共享内存操作句柄
    失败:返回-1

shmat

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 功能:将共享内存连接到进程地址空间
  • 参数
    shmid:共享内存操作句柄,与shmget函数的返回值类型相同。
    shmaddr:指定要连接的地址,即要将共享内存附加到共享区中哪一个地址上,一般让操作系统自己分配,传递NULL。
    shmflg:以什么权限将共享内存附加到进程当中。SHM_RDONLY,只读;0,可读可写。注:这里的权限指的是进程对共享内存的操作权限,并不是共享内存本身的权限。
  • 返回值
    成功:返回一个指针,指向附加到的共享内存的第一个字节
    失败:返回-1

shmdt

int shmdt(const void *shmaddr);
  • 功能:将共享内存段与进程分离
  • 参数
    shmaddr:shmat函数所返回的指针
  • 返回值
    成功:返回0
    失败:返回-1

shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 功能:控制共享内存
  • 参数
    shmid:共享内存的操作句柄,入参
    cmd:告诉shmctl需要完成的功能,有下面三个取值
    IPC_SET:设置共享内存属性信息
    IPC_STAT:获取共享内存属性信息
    IPC_RMID:删除共享内存,进行删除操作时,第三个参数buf传NULL
    buf:共享内存数据结构buf,如图:
    在这里插入图片描述
  • 返回值
    成功:返回0
    失败:返回-1

共享内存特性

通过ipcs指令可以查看共享内存信息
在这里插入图片描述

  • 共享内存生命周期跟随操作系统
  • 共享内存写时是覆盖写的方式,读的时候访问地址,不会将共享内存中的内容读走,与管道中的字节流不同

实例代码

  • 程序
    在这里插入图片描述
  • 运行结果
    在这里插入图片描述
    这里打印的地址是附加到的进程中共享区的地址,并非在物理内存中创建的共享内存的地址
    通过ipcs可以查看到程序中创建的共享内存
    在这里插入图片描述

共享内存的删除

  • 函数删除
    在这里插入图片描述
  • 指令删除
ipcrm -m [shmid]

在这里插入图片描述

  • 共享内存被删除以后,物理内存中的空间会被销毁
  • 如果删除共享内存时,共享内存附加的进程数量为0,则内核中描述该共享内存的结构体也会被释放。
  • 如果删除共享内存时,共享内存附加的进程数量不为0,则会将该共享内存的key值变为0x00000000。表示当前该共享内存不能被其他进程所附加,共享内存的状态会被置为destory。附加进程全部退出后,该共享内存在内核的结构体会被释放。如图:
    在这里插入图片描述

三、消息队列

特性

  • 消息队列(msgqueue)采用链表来实现,由系统进行维护
  • 系统中可能有多个消息队列,每个消息队列用msqid来进行区分
  • 进行进程间通信时,一个进程将消息加到MQ尾端,另一个队列从消息队列中取消息,按照队列先进先出的的原则。支持按照消息类型先进先出,即可以指定只取某一类型的消息,在读取该类型消息时只要满足先进先出即可
  • 消息队列的生命周期跟随操作系统,删除指令:
ipcrm -q msqid

函数接口

msgget

#include <sys/msg.h>//消息队列相关函数头文件
int msgget(key_t key, int msgflg);
  • 功能
    创建消息队列
  • 参数
    key:消息队列的标识符
    msgflg:创建的标志,如:IPC_CREAT,如果不存在则创建,按位或权限
  • 返回值
    成功:返回消息队列id
    失败:返回-1,设置errno

msgsnd

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • 功能
    发送消息
  • 参数
    msqid:消息队列id
    msgp:指向msgbuf的指针,用来指定发送的消息
    在这里插入图片描述
    msgsz:要发送消息的长度
    msgflg:创建标记,如果指定IPC_NOWAIT,失败会立即返回。0,阻塞发送;IPC_NOWAIT,非阻塞发送。
  • 返回值
    失败返回-1,成功返回0

msgrcv

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
  • 功能
    接收消息
  • 参数
    msqid:消息队列id
    msgp:指向msgbuf的指针,用来接收消息
    msgsz:要接受消息的长度
    msgtyp:接收消息的方式。若msgtyp=0,读取队列中的第一条消息;若msgtyp>0,读取队列中类型为msgtyp的第一条消息,若在msgflg中指定了MSG_EXCEPT,则将读取队列中类型不等于msgtyp的第一条消息
    msgflg:创建标记,如果指定IPC_NOWAIT,获取失败会立即返回
  • 返回值
    成功返回实际读取字节数,失败返回-1

msgctl

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 功能:操作消息队列
  • 参数
    msqid:消息队列id
    cmd:控制命令。IPC_RMID,删除命令;IPC_STAT,获取状态。
  • 返回值:成功时根据不同的cmd有不同的返回值,失败返回-1

实例程序

在这里插入图片描述
运行结果
在这里插入图片描述

通过ipcs查看消息队列信息
在这里插入图片描述
执行读取的程序后,消息数量会减为0
在这里插入图片描述
这时候如果再执行读取的程序,会发生阻塞或报错返回

  • msgrcv的参数msgflg设置为0时,第二次读取发生阻塞

在这里插入图片描述

  • msgrcv的参数msgflg设置为IPC_NOWAIT,获取失败会立即返回
    在这里插入图片描述

四、信号量

原理

信号量本质上就是资源计数器,保障多个进程访问临界资源,执行临界区代码时,时能够互斥访问,同时也可用于同步。

  • 临界资源:多个进程都可以访问到的资源,如一块共享内存
  • 临界区代码:访问临界资源的代码

互斥

概念

  • 定义:同一时刻,多个进程中,只有一个进程可以访问临界区资源。PS:上厕所的例子,厕所里只能有一个人,门关上时,其他人不能进来

多个进程通过信号量保证互斥的时候,需要先获取信号变量才能访问临界资源,如果获取不了则进行阻塞等待。
在这里插入图片描述

不保证互斥存在的问题

以共享内存为例,当两个进程附加同一块共享内存后,这两个进程都可以对这块共享内存中的内容进行读写操作,假设共享内存中保存了一个数据100,A进程对数据进行+1操作,但是当A进程将数据从内存中读取到CPU后,A进程的时间片用完了,会将读到的100保存在该进程的上下文信息中,此时B进程读取到了内存中的100并进行了+1操作,将内存中的数据改写成了101,这时再次轮到A进程的时间片,A进程会使用上下文信息中保存的100,并根据程序计数器中保存的内容执行下一步的+1的代码,最终加完结果仍然是101,写回内存中。如此一来便产生了程序结果的二义性,对100进行了两次+1操作得到的结果确实101。

同步

概念:当临界资源空闲后,通知等待的进程进行访问。
PS:停车场的例子,假设一个停车场有四个车位,可以记为有4个资源,当停车场停满以后资源数量为0,后来的车辆便要排队进行等待,而如果有车辆离开停车场,便需要通知等待的车辆进入停车场,避免资源的浪费,这个过程就是同步。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值