栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

Linux进程间通信

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Linux进程间通信

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

这里写目录标题
  • 进程间通信介绍
  • 管道
    • 管道是什么
    • 匿名管道
      • 接口简介
      • 程序
      • fork共享管道
      • 匿名管道的特性
    • 设置管道的非阻塞属性
      • 接口简介
      • 设置非阻塞属性步骤
      • 设置读端为非阻塞属性
      • 设置写端为非阻塞属性
      • 管道读写规则总结
      • 扩展知识:宏与位图
    • 命名管道
      • 创建命名管道
      • 通过命名管道进行进程间通信
  • 共享内存
    • 原理
    • 共享内存函数
    • 共享内存特性
    • 实例代码
    • 共享内存的删除
  • 消息队列
    • 特性
    • 函数接口
      • msgget
      • msgsnd
      • msgrcv
      • msgctl
      • 实例程序

进程间通信介绍

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

管道 管道是什么

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

匿名管道 接口简介
int pipe(int pipefd[2]);
  • 功能:创建匿名管道
  • 参数:pipefd[]是一个整型数组,为出参,在调用函数中定义,在pipe函数中进行赋值,其中fd[0]表示读端,fd[1]表示写端
  • 返回值:创建成功返回0,失败返回-1,并设置错误码
程序
#include
#include
int main()
{
    int fd[2];//定义一个数组,作为pipe函数的参数
    int ret=pipe(fd);
    if(ret<0)
    {
    //创建失败
    	perror("pipe");
        return 0;
    }
    //创建成功,打印文件描述符
    printf("fd[0]:%dn",fd[0]);
    printf("fd[1]:%dn",fd[1]);
    printf("My pid:%dn",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, ...  );

参数

  • 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
#include
#include
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("%ldn",read_size);
        perror("read");
    }
    else
    {
        //parent
        //写端关闭不关闭
        close(fd[0]);
        //close(fd[1]);
        while(1)
        {
            sleep(1);
        }
    }
return 0;

运行结果

  • 读端为非阻塞,父进程不关闭写

    结果分析:管道中没有内容,进行读取,返回-1,errno设置为EAGAIN,资源暂时不可用,表示可以再进行读。

  • 读端为非阻塞,父进程关闭写

    结果分析:读取成功,但没有读到内容

设置写端为非阻塞属性
#include
#include
#include
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("%ldn",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 //共享内存相关函数都包含在此头文件中
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 //消息队列相关函数头文件
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,获取失败会立即返回
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/828544.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号