Linux:历程间通讯

打印 上一主题 下一主题

主题 993|帖子 993|积分 2979


前言

历程间通讯实在就是让不同历程看到同一份资源的思想,然后设计不同的模式或者数据结构来达到这一目的

一、历程间通讯介绍

1.1 历程间通讯的目的

   由于历程地址空间的独立性设计,我们在面对下面情况时需要一种机制来实现历程间的交互,这便是历程间通讯的目的
  

  • 数据传输:⼀个历程需要将它的数据发送给另⼀个历程
  • 资源共享:多个历程之间共享同样的资源。
  • 关照事件:⼀个历程需要向另⼀个或⼀组历程发送消息,关照它(它们)发⽣了某种事件(如进
    程终⽌时要关照⽗历程)。
  • 历程控制:有些历程希望完全控制另⼀个历程的执⾏(如Debug历程),此时控制历程希望能够
    拦截另⼀个历程的所有陷⼊和异常,并能够及时知道它的状态改变。
  1.2 历程间通讯的发展与分类

   

  • 管道
  

  • 匿名管道pipe
  • 命名管道
   

  • System_V历程间通讯
  

  • System_V 消息队列
  • • System_V 共享内存
  • • System_V 信号量
   

  • POSIX历程间通讯
  

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁
  二、管道

2.1 匿名管道原理

管道是Unix最古老的历程间通讯方式,管道是在内核空间中开辟的,管道的数据存储在内核的一个环形缓冲区中(因此匿名管道的本质是由内核在内存中维护的一个文件)!一般发生在父子历程的通讯使用中
当父历程创建子历程的时候,会举行数据上的浅拷贝,所以子历程也拥有父历程雷同的指针,它们共同指向雷同的文件(也就是拥有雷同的文件描述符表),因此它们能使用同一个管道,并且管道是单向的,意味着两个历程只能有一个能读,另一个只能写

  1. #include <iostream>
  2. #include<unistd.h>
  3. #include<cstdio>
  4. #include<cstring>
  5. #include<sys/types.h>
  6. #include<sys/wait.h>
  7. using namespace std;
  8. void ChildWrit(const int wfd)
  9. {
  10.     char buffer[1024];
  11.     int cnt=0;
  12.     while(true)
  13.     {
  14.         //snprintf是C语言的字符串处理函数,在需要读取的字符串末尾会自动添加/0,
  15.         //而下面的read函数是系统级别的函数,所以在处理字符串的时候需要我们需要预留一个字节空间来添加/0
  16.         snprintf(buffer,sizeof(buffer),"i am child,witing now,my pid:%d,cnt:%d",getpid(),cnt++);
  17.         write(wfd,buffer,strlen(buffer));
  18.         sleep(1);
  19.     }
  20. }
  21. void FatherRead(const int rfd)
  22. {
  23.     char buffer[1024];
  24.     while(true)
  25.     {
  26.         buffer[0]=0;
  27.         ssize_t n=read(rfd,buffer,sizeof(buffer)-1);
  28.         if(n>0)
  29.         {
  30.             buffer[n]=0;
  31.             cout<<"child say:"<<buffer<<endl;
  32.         }
  33.         else if(n==0)
  34.         {
  35.         }
  36.     }
  37. }
  38. int main() {
  39.     //创建管道
  40.     int fds[2]={0};
  41.     int n=pipe(fds);
  42.     if(n<0)
  43.     {
  44.         cout<<"pipe error"<<endl;
  45.     }
  46.     else
  47.     {
  48.         cout<<"fds[0]"<<fds[0]<<endl;
  49.         cout<<"fds[1]"<<fds[1]<<endl;
  50.     }
  51.     //创建子进程
  52.     //关闭读写端形成单向通信信道
  53.     pid_t id=fork();
  54.     if(id==0)
  55.     {
  56.         close(fds[0]);
  57.         //子进程在写的时候,父进程会阻塞在read函数上面等待读取
  58.         ChildWrit(fds[1]);
  59.         exit(0);
  60.     }
  61.     close(fds[1]);
  62.     FatherRead(fds[0]);
  63.     waitpid(id,nullptr,0);
  64.     cout<<"test pipe"<<endl;
  65.     return 0;
  66. }
复制代码
2.2 通讯管道会出现的情况和特性(重要)


  • 读的慢,写端要等读端
  • 读端关闭,写端收到SIGPIPE信号直接终止
  • 写端不写或写的慢,读端要等写端
  • 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件末端
   其中情况1,2,3都是基于Linux下的read,write等函数的特性举行的,而情况4则是历程本身的管理
  

2.3 命名管道

上面匿名管道能举行带有血缘关系历程的通讯,那么接下来的命名管道就可以让两个绝不相关的历程举行通讯。
命名管道的性质除了能让任意历程间通讯外,与匿名管道根本一致,即创建一个文件(与匿名管道的区别是在磁盘上创建的文件,而匿名管道是内核上创建)一个历程往文件中写数据,一个历程读数据,且不让文件内容刷新到磁盘上(这也是磁盘上的平凡文件与管道文件的区别),从而实现任意历程间的通讯。
   以下是有关在代码层上怎样创建一个管道文件,它使用mkfifo()函数,实在在我们用户层上,所谓的管道文件也只不外是特别的文件罢了,相较于在代码中对平凡文件的操纵,我们操纵有名管道只不外多了创建这一步。

当一个文件打开命名管道时,无论是读写端,如果另一端没有打开,那么这个文件就会阻塞(卡)在open上!
  2.3.1 命名管道与匿名管道的区别

   • 匿名管道由pipe函数创建并打开。
• 命名管道由mkfifo函数创建,打开⽤open
• FIFO(命名管道)与pipe(匿名管道)之间唯⼀的区别在它们创建与打开的⽅式不同,⼀但这些⼯作完成之后,它们具有雷同的语义。
  三、system V

system V是一套 Unix/Linux 系统中历程间通讯的尺度化机制,,共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的历程的地址空间,这些历程间数据通报不再涉及到内核,换句话说是历程不再通过执⾏进⼊内核的系统调⽤来通报彼此的数据。
3.1 共享内存原理

   共享内存现实上就是通过一定的技术手段来将两个历程的捏造地址空间举行相连,我们只需要执行一次系统级别上的调用,创建完共享内存后,那么两个历程就能直接通报数据而不依赖内核。

  共享内存没有保护机制(对共享内存中数据的保护),就是两个历程间的数据并不同步。system V 体系通讯创建的共享内存它的生命周期随操纵系统,所以我们必须手动删除我们创建的共享内存
3.2 键值

在创建信道的过程中我们需要通过某种约定,来达到两个历程可以毗连同一块内存的目的,以此键值出现
   键值的实现通常依赖于操纵系统提供的函数ftok()【file to key】
  函数原型:
  1. #include <sys/types.h>
  2. #include <sys/ipc.h>
  3. key_t ftok(const char *pathname, int proj_id);
复制代码
  它通过传入文件路径pathname(如“.”),以及proj_id(项目的识符,用于在同一文件的不同场景中生成不同键值),乐成时返回生成的 key_t 类型键值,失败返回 -1,并设置 errno(如文件不存在)。失败返回 -1,并设置 errno(如文件不存在)
  3.2.1 键值生成原理


  • 提取文件的 inode 号装备号
  • 将 proj_id 的低 8 位与 inode 号的低 16 位、装备号的低 16 位组合:key = (proj_id & 0xFF) << 24 | (st_dev & 0xFF) << 16 | (st_ino & 0xFFFF)
3.3 共享内存相关接口函数创建信道

我们学习共享内存并理解只需要将相关函数理解就能大概明白其原理方向
函数作用&&用例shmget创建或获取共享内存段 int shmid = shmget(key, size, IPC_CREAT 0666);shmat将共享内存段附加到历程的地址空间 void *shm_ptr = shmat(shmid, NULL, 0);shmdt将共享内存段从历程地址空间分离 hmdt(shm_ptr);shmctl控制共享内存段(删除、查询信息等) shmctl(shmid, IPC_RMID, NULL);   shmget 参数:
  

  • key: 共享内存的唯一标识符,通常用 ftok(“path”, id) 生成。
  • size: 共享内存段大小(字节)。在操纵系统中通常为4kb的整数倍大小,但在个人操纵空间上是给多少显示多少,操纵系统内部现实向上取整
  • flags: 权限标志(如 IPC_CREAT | 0666)。
    取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
    取值为IPC_CREAT | IPC_EXCL(不单独存在):共享内存不存在,创建并返回;共享内存已存在,出
    错返回。
  
key值是共享内存的“身份证”,它是历程间的约定,使不同历程事先通过雷同key找到同一块共享内存,属于操纵系统层面,而开辟完之后的返回值shmid则是用户层面上操纵这块共享内存的标识符,这样做的目的是将用户和操纵系统分层以实现解耦合的目的
   shmat 返回值:使用标识符将开辟的共享空间关联到捏造地址。乐成返回共享内存的起始地址,失败返回 (void*)-1
参数:
shmid: 共享内存标识
shmaddr:指定毗连的地址,通常用null让系统自动返回一个地址
shmflg:一般设置为0,使用shmget中设置的缺省权限
返回值:乐成返回⼀个指针,指向共享内存第⼀个节;失败返回-1
    shdt:去关联
    shmctl 下令:
IPC_RMID: 标记共享内存段为待删除(当所有历程分离后现实删除)。
IPC_STAT: 获取共享内存段的状态信息。
乐成返回0,失败返回-1
  3.4 大略介绍别的IPC和代码结构(仅作了解)

3.4.1 消息队列

消息队列的技术已经是掉队的了,对于我们初学者来说,已经失去了学习的意义,所以仅作了解

消息队列提供了⼀个从⼀个历程向另外⼀个历程发送⼀块数据的⽅法
• 每个数据块都被认为是有⼀个类型,吸收者历程吸收的数据块可以有不同的类型值
• 特性⽅⾯
◦ IPC资源必须删除,否则不会⾃动清除,除⾮重启,所以system_V IPC资源的⽣命周期随内核
其中接口函数不作描述。
3.4.2 代码数据结构

  1. struct msqid_ds
  2. {
  3.     struct ipc_perm msg_perm;     /* Ownership and permissions */
  4.     time_t          msg_stime;    /* Time of last msgsnd(2) */
  5.     time_t          msg_rtime;    /* Time of last msgrcv(2) */
  6.     time_t          msg_ctime;    /* Time of last change */
  7.     unsigned long   __msg_cbytes; /* Current number of bytes in
  8.                                      queue (nonstandard) */
  9.     msgqnum_t       msg_qnum;     /* Current number of messages
  10.                                      in queue */
  11.     msglen_t        msg_qbytes;   /* Maximum number of bytes
  12.                                      allowed in queue */
  13.     pid_t           msg_lspid;    /* PID of last msgsnd(2) */
  14.     pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
  15. };
复制代码
其中第一个结构体的数据结构如下
  1. struct ipc_perm
  2. {
  3.    key_t          __key;       /* Key supplied to msgget(2) */
  4.    uid_t          uid;         /* Effective UID of owner */
  5.    gid_t          gid;         /* Effective GID of owner */
  6.    uid_t          cuid;        /* Effective UID of creator */
  7.    gid_t          cgid;        /* Effective GID of creator */
  8.    unsigned short mode;        /* Permissions */
  9.    unsigned short __seq;       /* Sequence number */
  10. };
复制代码
操纵系统通过维护这种数据结构体来然后通过函数来举行通讯
四、system V 信号量

通讯的条件是看到同一份资源,那么在对资源举行读写的时候,如果一方开始读了但另一方仅仅把想传输的资源写了一半,那么就会发生数据不一致的情况,但是如果我们加入一种保护机制,比方管道里面通过write和read函数的特性举行阻塞,就可以控制并达到想要的通讯结果
4.1 并发编程,铺垫概念

在了解怎么去保护之前,我们需要知道一些概念
   

  • 临界资源:被多个执行流同时访问的资源,一次只允许一个历程使用。比如管道、共享内存、消息队列、信号量。
  • 临界区:历程中访问临界资源的代码(和临界资源配套)为了保护数据安全,就要把临界区保护起来,就有了信号量。
  • 原子性:一件事情要么做完,要么不做,没有中间状态。
  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。
  • 多个执⾏流(历程),能看到的同⼀份公共资源:共享资源
• 被保护起来的资源叫做临界资源
• 保护的⽅式常⻅:互斥与同步
• 任何时候,只允许⼀个执⾏流访问资源,叫做互斥
• 多个执⾏流,访问临界资源的时候,具有⼀定的顺序性,叫做同步
• 系统中某些资源⼀次只允许⼀个历程使⽤,称这样的资源为临界资源或互斥资源。
• 在历程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(⾮临界区)
• 所谓的对共享资源进⾏保护,本质是对访问共享资源的代码进⾏保护

4.2 信号量

信号量是一个抽象概念,有时候也被叫做信号灯,本质上是一个计数器可以先用智能指针中或者文件结构体中的count去理解。信号量初始化的时候用来分割临界资源中资源。所有历程想要访问临界资源中的一小块,就必须申请信号量,本质上就是预定机制。

信号量同时也是共享资源
   

  • p操纵:申请信号量,对应计数器- -,原子性,如果小于某个阈值,那么就会阻塞
    -v操纵: 退出,对应计数器++,原子性
  在信号量中只有0和1两态,叫做二元信号量,也就是互斥!
信号量也属于通讯,都先访问信号量P,所有历程就都能看到同一个信号量,不是通报数据才属于IPC,关照,同步互斥也算
4.3信号量接口和系统调用

信号量接口的设计跟共享内存大致雷同
   semget:


其中参数key_t以及semflg与共享内存获取函数一致,参数nsems则是想要创建多少个信号量,乐成返复书号量级标识符(非0整数),失败返回-1,并设置错误码
    semctl:


同样是使用对应的标识符然后根据cmd指令举行删除等操纵
  1. #include <sys/types.h>
  2. #include <sys/ipc.h>
  3. #include <sys/sem.h>
  4. int semop(int semid, struct sembuf *sops, unsigned nsops);
  5. struct sembuf
  6. {
  7.     unsigned short  sem_num;        /* semaphore index in array */
  8.     short           sem_op;         /* semaphore operation */
  9.     short           sem_flg;        /* operation flags */
  10. };
复制代码
sem_num指定要操纵的信号量,0表示第一个信号量,1表示第二个信号量,……sem_op信号量操纵,设置1表示P操纵,-1表示V操纵sem_flg操纵标识 信号量同样可以用指令查看

五、内核中的system V资源管理

在OS中,消息队列,共享内存,信号量被看成同一种资源

内核分配一个ipc_perm数组,用来指向每一个IPC资源。,实在这里是以C语言的方式来实现多态所以我们通过XXXget函数的返回值拿到的什么shmid,semid,实在就是这个(柔性)数组的下标!==


总结

本文描述了历程间通讯的概念以及相关函数接口,并大略介绍了内核层面上怎样管理通讯需要的数据,仍有诸多欠缺与文章内容上深度的赘述不足。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

水军大提督

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表