Linux进程间通信

您所在的位置:网站首页 数学课件制作视频教程大全免费 Linux进程间通信

Linux进程间通信

2024-07-16 17:06:49| 来源: 网络整理| 查看: 265

进程间通信

本文已收录至《Linux知识与编程》专栏! 作者:ARMCSKGT 演示环境:CentOS 7 CSDN

目录 前言正文进程间通信概念管道管道概念管道原理匿名管道管道规则和特点管道的四种特殊场景关于管道的大小命名管道匿名管道实现进程控制命名管道实现模拟打电话 共享内存什么是共享内存?共享内存相关接口共享内存的综合使用共享内存相关特点 消息队列什么是消息队列?消息队列相关接口 信号量什么是信号量?信号量的相关接口关于信号量 关于SystemV标准通信设计 最后

前言

进程间通信(IPC)是指不同进程之间的数据交换和通信。在多进程环境下,不同的进程需要共享内存、文件等资源,但是每个进程都有自己独立的地址空间,因此需要通过进程间通信来实现进程之间的数据交换和共享。进程间通信使得进程间可以进行数据传输、资源共享、通知事件等。例如,一个进程需要将它的数据发送给另一个进程,或者多个进程之间共享同样的资源,或者一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件等。本节我们将为大家介绍进程间通信的相关知识!

正文 进程间通信概念

在开始学习进程间通信前,我们需要知道一些概念!

进程间通信主要有四大目的:

数据传输:一个进程需要将它的数据发送给另一个进程。资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。进程控制:有些进程希望完全控制另一个进程的执行(如Debug调试进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

其实进程间通信的最终目的就是 打破各个独立进程之前的壁垒,进行任务协同!

因为进程间具有独立性原则(增加了进程间通信成本),而我们需要进程间通使得进程能够更好的协同工作,所以进程间通信需要解决两个问题:

1.想办法让不同的进程看到同一份资源,系统中大部分接口都是为了解决这个问题的,这也是进程间通信的本质。2.进程间只有一方可以写,另一方可以读,完成通信,关于通信后的后续工作,结合场景具体分析!

所以进程间通信的本质是让不同的进程看到同一份资源,为了让进程间能够通信,操作系统会在内存中创建一块公共的空间两个进程共享。

进程间通信的发展经历了三个时期的变化:

管道: 最早最古老的本地进程间通信方式,尽管管道比较老,但是对于我们学习进程间通信,探究其原理和流程还是很有帮助的!System V标准 进程间通信: 本地化进程间通信方式的发展,拓宽本地进程间通信的方式,但是本地进程间通信的使用场景有限,使用非常少,不过其效率极高的共享内存还是可以研究的!POSIX标准 进程间通信: 网络进程间通信的发展,满足网络中的进程间通信,POSIX标准是由Unix 系统设计的一个标准,该标准支持跨平台,就连Windows等大众系统都支持POSIX标准,我们后面学习的进程的同步与互斥以及Socket套接字编程都是POSIX标准的产物!

关于以上三种进程间通信标准,各自的解决方案:

管道 – 匿名管道 – 命名管道System V 标准 – System V 信号量 – System V 共享内存 – System V 消息队列POSIX 标准 – POSIX 信号量 – POSIX 共享内存 – POSIX 消息队列 – POSIX 互斥量(互斥锁) – POSIX 条件变量 – POSIX 读写锁

可以看出,随着技术的进步,进程间通信的方式也不断在迭代更新以适应这个大时代!

管道 管道概念

管道是 Unix 系统进程间通信(IPC)中最古老的方式,其历史最早可追溯至 “1964年10月11日”。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。 在Linux中我们有一个命令 | 叫做管道,其底层使用的确实是管道的原理!

#注意:在命令后 加上 & 表示该命令的将在后台执行 成为后台进程 $ sleep 658 | sleep 678 &

在创建两个由管道连接的sleep休眠较长的进程后,我们查询进程状态,发现有两个已经创建的后台sleep进程且PPID相同,PID相近,sleep 658和sleep 678为兄弟进程。

管道中,分为供父子进程使用的匿名管道(当然也可以使用命名管道) 和 供不同进程使用的命名管道! 当然,管道本身就是文件,这也符合Linux中一切皆文件的特性!

管道原理

管道的工作原理很简单,打开一个文件让两个进程一个掌握读端一个掌握写端即可! 匿名管道的功能比较局限,只能让父子进程进行通信,而命名管道可以让父子进程或不相干的进程间进行通信,覆盖范围更大! 我们前面介绍过,管道的本质也是文件,其区别在于管道属于内存级文件,不会将数据写入磁盘,那么我们必定要使用文件的思想去对待管道,我们使用管道就是打开一个文件! 对于任意一个进程,我们都能以读方式和写方式打开文件,管道也不然,但是匿名管道和命名管道的区别在于,我们打开匿名管道时,操作系统会一次性帮我们既打开管道文件的写端也打开管道文件的读端(实际是分配给我们两个文件描述符),我们在开启子进程后,对各自不需要的一端进行关闭,例如父进程写子进程读,那么开启子进程后父进程应该关闭管道的读端( 使用close(fd) ),子进程应该关闭管道的写端,此时父子进程可以看到同一份资源,数据可以从父进程通过管道流向子进程!

匿名管道

通过上面的介绍,匿名管道的操作流程可以简单分为三步:

1.父进程申请一个匿名管道文件,获得读写端两个文件描述符。2.父进程fork创建子进程,子进程继承父进程的进程相关数据结构,不会复制父进程曾经打开的文件对象,父子进程看到同一个管道文件(也就是父进程打开的文件对象并非子进程新打开的文件对象)。3.在创建子进程后,父子进程都有管道读写的文件描述符,此时需要确定数据流向,父进程关闭读(写)端,子进程关闭写(读)端,构成单向流通。

补充:

父进程fork创建子进程后,子进程和父进程向屏幕和文件输入信息都会同时打印到屏幕和写入文件中,因为此时父子进程操作的是同一个文件。父进程申请管道文件时,在管道流向确定前必须有读写端文件描述符,不能提前关闭任意一个,否则子进程继承后权限丢失。 匿名管道文件在概念上属于文件系统,但管道文件是一个由操作系统提供的特殊文件,该文件不需要写入磁盘,只需要满足进程间通信即可。 管道是一种半双工的通信方式,因为管道只有一个缓冲区,所以形象称为管道。

匿名管道在Linux系统中是pipe,其系统调用函数也是pipe。 关于匿名管道的手册介绍: 管道man手册介绍

pipe函数介绍

int pipe(int pipefd[2]);

参数: 一个int类型的二维数组,下标 0 中存放的是读文件描述符,下标 1 中存放的是写文件描述符。 返回值: 申请成功返回 0,失败返回 -1 (且错误码errno被设置)。

使用pipe的难点在于数组中读写文件描述符是哪一个下标,这里有一个简单的记忆方法,那就是可以把下标 0 想象成一张嘴巴也就是读,把下标 1 想象成一支笔也就是写!

实例代码演示:

#include #include #include #include #include #include using namespace std; //实现一个父进程写子进程读的通信 int main() { int fdarr[2]; int ret = pipe(fdarr); if(ret) { cerr char buf[64]; //定义一个缓冲区进行读 int n = read(rfd,buf,sizeof(buf)-1); //读取缓冲区大小的数据但预留一个 \0 位置 if(n > 0) { //如果是quit则结束通信 if(strcmp(buf,"quit") == 0) { cout cout cerr char buf[64]; //定义一个缓冲区进行读 int n = read(rfd,buf,sizeof(buf)-1); //读取缓冲区大小的数据但预留一个 \0 位置 if(n > 0) { //父进程写入了信息且成功读取 cout int fdarr[2]; int ret = pipe(fdarr); if(ret) { cerr int n = read(fdarr[0],buf,63); if(n>0) cout int fdarr[2]; int ret = pipe(fdarr); if(ret) { cerr} //子进程死循环 close(fdarr[0]); //关闭读文件描述符 exit(0); } //父进程关闭读文件描述符 close(fdarr[0]); string s(256,'A'); int count = 0; while(true) //父进程进行死循环写入 { int n = write(fdarr[1],s.c_str(),256); //一次写入256字节数据 count += n; cout cerr int n = write(fdarr[1],"AAAAA",5); //一次写入5字节数据 sleep(1); //子进程一秒写入一次数据 } close(fdarr[1]); //关闭写端 exit(0); } //父进程先关闭读写端 然后进入死循环 close(fdarr[1]); //父进程读取3次后退出 char buf[16]; for(int i = 0;i bool createflag = false; //标记是否为该进程创建的管道文件 struct stat st; //创建stat文件属性对象 if(stat(PIPENAME,&st) == -1) //如果返回-1则获取文件属性失败 此时没有文件 我们主动创建文件 { int ret = mkfifo(PIPENAME,0666); if(ret != 0) { cerr cerr //读取了n字节数据 就写n字节数据 write(wfd,buf,n); //客户端没一秒写入一次 sleep(1); } //关闭对应的文件描述符 close(wfd); close(filefd); //如果是客户端创建的管道文件 则由客户端清除 if(createflag) unlink(PIPENAME); return 0; }

服务端代码

//server #include #include #include #include #include #include using namespace std; #define PIPENAME "./mypipe" int main() { bool createflag = false; //标记是否为该进程创建的管道文件 struct stat st; //创建stat文件属性对象 if(stat(PIPENAME,&st) == -1) //如果返回-1则获取文件属性失败 此时没有文件 我们主动创建文件 { int ret = mkfifo(PIPENAME,0666); if(ret != 0) { cerr0}; while(true) { //读取内容写入缓冲区 int n = read(rfd,buf,sizeof(buf)); if(n == 0) break; //如果读取到0 则退出(如果不加 则客户端退出后 服务端会陷入死循环) cout cout public: Func() { //向数组中写入函数 f.push_back(Logfunc); f.push_back(MySQLfunc); f.push_back(Interfunc); } //仿函数调用 编号合法则调用 void operator()(int command) { if(command >= 0 && command } ~Task(){} public: pid_t _id; //进程pid int _writefd; //进程通信管道写fd };

接下来我们需要设计子进程的业务逻辑,子进程只需要获取父进程发送的任务编号,从func类中调用对应的任务即可,如果父进程关闭写端则该子进程自行退出。 后面我们会将子进程的管道读端重定向到0文件描述符,这样业务函数只需要在0文件描述符读取即可,不需要传递其他参数。

//子进程执行流 void WaitCommand() { Func func; //创建任务对象 while(true) { int command = 0; int n = read(0,&command,4);//读取四字节指令 if(n == -1) //读取出错也退出 { cerr if(TaskNum>64) TaskNum = 64; //禁止创建过多进程 vector nfd; //存储父进程已获取的管道写文件描述符 for(int i = 0;i0}; int n = pipe(fd); assert(n != -1); //创建子进程 pid_t id = fork(); assert(id != -1); if(id == 0) //子进程 { //关闭冗余的fd for(auto& d:nfd) close(d); //关闭写端 close(fd[1]); //将写文件描述符重定向读取端到标准输出 dup2(fd[0],0); //执行业务函数 WaitCommand(); //关闭读文件描述符 close(fd[0]); exit(0);//退出 } //父进程 close(fd[0]); //关闭读 t.push_back(Task(id,fd[1])); //写入进程对象 记录该子进程 nfd.push_back(fd[1]); //写入父进程冗余的fd防止被其他子进程继承 } }

当然,言多无用,还需要大家自己理解代码! 后面的设计就比较简单了,首先设计一个可交互的菜单,方便用户选择。

int menu() { cout //选择任务 int command = menu(); //如果任务号不正确 则不处理 if(command 3) {continue;} if(command == 3) break; write(t[num]._writefd,&command,sizeof(int)); ++num; num %= 3; usleep(500); } }

接下来就是子进程回收函数,依次关闭每个子进程的管道写端,使用waitpid函数等待子进程退出即可!

//关闭和回收子进程 void clean(const vector& t) { for(const auto& n:t) { close(n._writefd); //关闭写端 cout cout Usage(); return 1; } const char* mycall = argv[1]; //记录自己通信码 const char* othcall = argv[2]; //记录对方通信码 struct stat st; int wfd = 0; int rfd = 0; if(stat(othcall,&st)==-1) //查看对方是否已经创建了管道文件 如果没有创建则自己先创建 { int n = mkfifo(mycall,0666); if(n == -1) { cerr} //反复检查对方是否已经创建管道 没有就等待 //对方的管道以读方式打开 rfd = open(othcall,O_RDONLY); if(wfd == -1) { cerr cerr cerr0}; while(true) { int n = read(rfd,buf,1023); if(n==0) break; //对方关闭管道 cout if(str=="quit") cout __kernel_key_t key; __kernel_uid_t uid; __kernel_gid_t gid; __kernel_uid_t cuid; __kernel_gid_t cgid; __kernel_mode_t mode; unsigned short seq; };

共享内存虽然属于文件系统,但它的结构是经过特殊设计的,与文件系统中的 inode 那一套结构逻辑不一样。 以上代码仅供参考!

共享内存相关接口

这里介绍常用的共享内存系统调用接口!

创建共享内存 创建共享内存使用shmget函数,其man手册介绍:

#include #include int shmget(key_t key, size_t size, int shmflg);

关于shmget函数:

函数参数: – key:共享内存中内核中的唯一标识,相对于命名管道的名字,一般通过ftok函数计算获取。 – size:申请共享内存的大小(字节为单位),一般设置为4096字节(4KB),与一个page大小相同,有利于提高IO效率。 – shmflg:当做位图使用,设置创建共享内存的方式和权限,以 “|” 连接起来。 其中,常用的设置标记位为 IPC_CREAT 和 IPC_EXCL,IPC_CREAT 表示创建共享内存,如果存在,则使用已经存在的,如果我们想总是使用最新的共享内存(非已存在的),那么我们可以加上 IPC_EXCL 标记位,表示当创建共享内存时,如果共享内存已经存在,则创建失败返回-1,IPC_EXCL 这个标记位一般不单独使用,而是配合 IPC_CREAT 一起使用,关于权限,与文件相同,共享内存可以设置访问权限也受权限掩码(可以使用umask进行修改)的影响! 返回值:创建成功返回 shmid,创建失败返回 -1。shmid类似于打开文件时open返回的文件描述符fd,用来对不同的共享内存进行操作。

而参数key比较特殊,key_t 实际就是对 int 进行了封装,表示一个数字,用来标识不同的共享内存块(可以理解为文件inode),因为必须确保标识值的唯一性,建议使用函数 ftok 生成一个重复率低的标识值,供操作系统对共享内存进行区分和调用,ftok不是系统调用,仅仅是一种算法! ftok函数 ftok函数man手册介绍

#include #include key_t ftok(const char *pathname, int proj_id);

关于ftok函数:

函数参数 – pathname:项目路径,可以使用绝对或相对路径。 – proj_id:项目ID,这个根据实际情况填写即可,也是为了降低重复率的设计。返回值:成功则返回 key_t 类型的标识值,失败返回-1。

之所以shmget会有key这个参数,是因为我们能够让两个不同的进程通过一些手段得到同一个共享内存,当两个进程通过ftok计算的key相同,打开的共享内存相同,则两个进程看到了同一份资源就能进行通信,就像两个进程会事先知道同一个命名管道的名字一样,事先约定好的!

下面我们通过一段代码展示shmget接口的使用: 这里我们有客户端和服务端,服务端创建共享内存,客户端申请共享内存! 我们将客户端和服务端公共使用的代码资源在common.hpp中实现:

#include #include #include #include #define PATHNAME "../" #define PROJ_ID 0x668 const size_t shmSize = 4096; key_t getKey(const char* pathname,int proj_id) //获取key { key_t key = ftok(pathname,proj_id); if (key == -1) //失败则退出 { std::cerr std::cerr return FetchShm(key,size, IPC_CREAT); }

客户端shmclient.cc

#include #include "common.hpp" using namespace std; int main() { key_t key = getKey(PATHNAME,PROG_ID); int shmid = getShm(key,shmSize); //获取shm printf("client key:0x%x\n",key); //16进制输出key cout key_t key = getKey(PATHNAME,PROG_ID); int shmid = createShm(key,shmSize); //创建shm printf("server key:0x%x\n",key); //16进制输出key cout key_t key = getKey(PATHNAME,PROG_ID); int shmid = createShm(key,shmSize); //创建shm cout SERVER = 0, CLIENT = 1 }; class shmemory { const mode_t defmode = 0664; const char* defpname = "./"; const int defprid = 0x668; private: //获取key key_t getKey(const char* pathname,int proj_id) { key_t k = ftok(pathname,proj_id); if (k == -1) //失败则退出 { std::cerr std::cerr std::cerr std::cerr std::cerr shmdetach(); //取消关联 if(_type == SERVER) delshm(); //服务端删除共享内存 } ~shmemory() { destroy(); } void* GetShmPtr() { return _shmptr; } //获取共享内存地址 size_t GetShmSize() { return _size; } //获取共享内存大小 key_t GetKey() { return _key; } //获取key int GetType() { return _type; } //获取类型 服务端/客户端 private: int _type; //身份类型 客户端/服务端 key_t _key; //申请共享内存的key size_t _size; //共享内存大小 int _shmid; //共享内存ID void* _shmptr; //共享内存地址 };

客户端shmclient.cc和服务端shmserver.cc

//客户端shmclient.cc #include #include #include "common.hpp" using namespace std; int main() { shmemory shm(CLIENT); //使用默认大小 项目名称和项目编号 char* shmptr = (char*)shm.GetShmPtr(); cout shmemory shm(SERVER); char* shmptr = (char*)shm.GetShmPtr(); while(true) //服务器默认读取此后退出 { cout key_t __key; /* Key supplied to msgget(2) */ uid_t uid; /* Effective UID of owner */ gid_t gid; /* Effective GID of owner */ uid_t cuid; /* Effective UID of creator */ gid_t cgid; /* Effective GID of creator */ unsigned short mode; /* Permissions */ unsigned short __seq; /* Sequence number */ }; 消息队列相关接口

论标准的重要性,消息队列的大小接口风格与共享内存一致,都是出自 System V 标准,在这里我们介绍常用的接口。

创建消息队列 使用msgget函数创建消息队列,关于msgget的man手册介绍:

#include #include #include int msgget(key_t key, int msgflg);

关于msgget函数:

参数: – key:创建消息队列时的唯一 key 值(与共享内存shmget保持一致),一般也是通过ftok获取。 – msgflg:设置参数,也与shmget保持一致,使用 IPC_CREAT、IPC_EXCL、权限 等信息设置创建的消息队列。返回值:成功返回消息队列ID,失败返回 -1 且错误码被设置。

代码演示:

#include #include #include #include using namespace std; int main() { int key = ftok("./",0x668); if(key == -1) { cerr int key = ftok("./",0x668); if(key == -1) { cerr long mtype; /* message type, must be > 0 */ char mtext[1]; /* message data */ };

mtype就是传说中数据块类型,据发送方而设定;mtext是一个 柔性数组 ,其中存储待发送或接收的信息,因为是柔性数组,所以可以根据信息的大小灵活调整数组的大小。 按照队列的性质,向消息队列发送消息一般就是将数据push到队尾,接收数据则是从队头获取再出队。

消息队列中大部分接口与共享内存相似,毕竟都是出自System V标准,但是因为是比较早的技术,已经被现在更加高级的通信技术替代,所以很少会使用。

信号量 什么是信号量? 信号量(semaphore)又称信号灯,是一种特殊的工具,通过 P / V 操作实现线程或进程的同步和互斥。

关于同步与互斥 多进程访问共享空间和多线程访问公共资源会产生并发访问问题,例如两个进程或线程同时对共享资源写入则会使写入的内容失效。我们把进程和线程都能看到的资源称为临界资源(共享资源)!

互斥: 任何一个时刻,都只允许一个进程或线程(一个执行流)在进行共享资源的访问(对资源加锁)。 临界资源和临界区: 我们把任何一个时刻都只允许一个执行流访问的共享资源叫做临界资源(例如管道),所以要保护访问,保护内存,则需要通过代码进行保护。临界资源是要通过代码访问的(需要介质),凡是访问临界资源的代码,叫做临界区。例如在多执行流环境中,全局变量就是临界资源,而每个执行流访问全局变量的代码段就是临界区。 原子性: 一件事的执行,要么成功,要么失败,没有其他情况,这种只有两种确定状态的属性叫做原子性。例如对变量的修改,要么修改成功,要么修改失败,不存在修改一半的情况。 并发: 并发是指系统中同时存在多个独立的活动单元,比如在多线程中,多个执行流可以同时执行代码,可能访问同一份共享资源。

所以互斥是为了解决临界资源在多执行流环境中的并发访问问题,需要借助互斥锁或信号量等工具实现原子操作,实现互斥。 关于锁(互斥锁mutex)的内容我们后续会介绍,在此之前我们先了解信号量,学习它是如何实现互斥 的。

关于信号量的理解 我们引入生活中的坐火车抢票的例子,帮助大家理解信号量。 此时,火车票就是临界资源,而购买火车票就是访问临界资源,所以我们去购买火车票就算是临界区。 当我们成功购买一张火车票时,对于这趟火车来说,其票数减1,当没有票时,我们要么等待有票,要么查看其他火车。无论怎么样,当火车票卖完时,其他人是没办法乘坐这趟火车的!

关于上面的说明,我们可以总结为三点: 当有票时,我们可以成功购买到火车票,此时总票数计时器减1,而且我们可以乘坐这趟火车,因为火车上有一个位置必定是暂时属于我的。 如果我们去买票时,票已经卖完了,那么我们就无法乘坐这趟火车,要么进行候补等待其他人退票,要么换其他火车,总之目前是无法乘坐这趟火车的。 因为有票数计时器,在火车的乘坐上,有效划分了火车票(或乘坐该火车)这个临界资源的所属权限,从而保证在火车发车时绝对不会发生位置冲突等意外情况!

信号量的设计初衷也是如此,就是为了避免因多执行流对临界资源的并发访问,而导致程序运行出现问题。 我们再举一个例子,假设在路边有一间公共厕所,一次只能供一个人使用,当该厕所无人使用时计时器为1,当一群人来使用时,一次只能一个人使用,此时计时器为0,而剩下的人什么时候能使用取决于正在使用的人什么时候出来,当厕所中的人方便完后出来,此时计时器就加1了! 透过现象看本质,在这一间公共厕所的使用中,就是代码中多个执行流对同一个临界资源的互斥访问。此时的 信号量 可以设为 1,确保只允许一个执行流进行访问,这种信号量被称为 二元信号量,常用来实现互斥。 所以,信号量本质上就是一个资源计数器,所谓的 P 操作(申请)就是在对计时器减1,V 操作(归还)则是在对 计数器加1。

同样的,我们研究的是 SystemV标准 的 信号量,通过手册我们可以查看信号量的相关结构体属性:

struct semid_ds { struct ipc_perm sem_perm; /* Ownership and permissions */ time_t sem_otime; /* Last semop time */ time_t sem_ctime; /* Last change time */ unsigned long sem_nsems; /* No. of semaphores in set */ };

System V 标准故有的 struct ipc_perm 中存储了 信号量的基本信息,具体包含内容如下:

struct ipc_perm { key_t __key; /* Key supplied to semget(2) */ uid_t uid; /* Effective UID of owner */ gid_t gid; /* Effective GID of owner */ uid_t cuid; /* Effective UID of creator */ gid_t cgid; /* Effective GID of creator */ unsigned short mode; /* Permissions */ unsigned short __seq; /* Sequence number */ };

显然,无论是 共享内存、消息队列、信号量,它们的 ipc_perm 结构体中的内容都是一模一样的,结构上的统一可以带来管理上的便利,具体的细节,我们后面稍加介绍!

信号量的相关接口

在systemV标准中,信号量简称为sem。

创建信号量 创建信号量,我们使用semget,信号量的申请比较特殊,一次可以申请多个信息量,官方称此为 信号量集,关于 semget 的man手册介绍:

#include #include #include int semget(key_t key, int nsems, int semflg);

关于 semget 函数:

参数: – key:创建信号量集时的唯一 key 标识值(可以通过函数 ftok 计算获取)。 – nsems:待创建的信号量个数(相当于计数器的格式),这也正是集的来源,一般为1,表示创建一个信号量,这个参数一般只在信号量中有效。 – semflg:标志位,位图形式,可以与上面其他SystemV标准通信的创建方式及创建权限作为参考(IPC_CREAT、IPC_EXCL、权限等)。返回值:成功返回semid(信号量id),失败返回-1且错误码被设置。

除了nsems参数,其他与共享内存和消息队列基本上差不多!

示例代码:

#include #include #include #include using namespace std; int main() { int key = ftok("./",0x668); if(key == -1) { cerr int key = ftok("./",0x668); if(key == -1) { cerr unsigned short sem_num; // 数组中的信号量下标(标记操作信号量的起始位置) short sem_op; // 信号量操作 short sem_flg; // 操作标识 };

semop函数通过sembuf中设置的sem_num起始下标位置开始设置nsops个信号量。 sembuf 成员:

sembuf为信号量的下标(编号)sem_op:要进行的操作(PV操作),如果为正整数,表示增加信号量的值(若为3,则加上3),如果为负整数,表示减小信号量的值,如果为0,表示对信号量当前值进行是否为0的测试。sem_flg:位图结构,IPC_NOWAIT标记,表示如果不能对信号量集合进行操作,则立即返回;SEM_UNDO标记,表示为当进程退出后,该进程对sem进行的操作将撤销。

关于P / V操作:

P原语操作的动作是:sem减1,若sem减1后仍大于或等于零,则进程继续执行,若sem减1后小于零,则该进程被阻塞后进入与该信号相对应的等待队列中,然后转进程调度。V原语操作的动作是:sem加1,若相加结果大于零,则进程继续执行,若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。 这个函数这里仅仅进行介绍,因为其使用比较复杂,且现在有更好更方便的类信号量技术使用,我们了解即可。 关于信号量

信号量 是实现 互斥 的其中一种方法,具体表现为:资源申请时,计数器减1,资源归还时,计数器 加1,只有在计数器不为 0 的情况下,才能进行资源申请,可以设计二元信号量实现互斥。 SystemV标准 中的信号量操作比较复杂,但信号量的思想还是值得了解的,后面学习多线程时也会用到,会使用 POSIX 中的信号量实现互斥,相比之下,POSIX 版的信号量操作要简单得多,同时应用也更为广泛。 因为信号量需要被多个独立进程看到,所以信号量本身也是临界资源,不过它是原子的,所以可以用于互斥。 因为多个独立进程看到同一份资源,这就是IPC的目标,所以信号量被划分至进程间通信中。

关于SystemV标准通信设计

不难发现,共享内存、消息队列、信号量的数据结构基本一致,并且都有同一个成员 struct ipc_perm。 所以实际对于 操作系统 来说,对 System V 中各种方式的描述管理只需要这样做:

将 共享内存、消息队列、信号量对象描述后,统一存入struct ipc_perm数组中;再进行指定对象创建时,只需要根据 ipc_id_arr[n]->__key 进行比对,即可得知当前对象是否被创建!因为 struct shmid_ds首地址就是其成员struct ipc_perm shm_perm结构体的首地址(其他对象也一样),所以可以对当前位置的指针进行强转:((struct shmid_ds)ipc_id_arr[n]) 即可访问 shmid_ds 中的成员,这个思想,有点像C++中多态虚表。 这样一来,操作系统可以只根据一个地址,灵活访问两个结构体中的内容,比如 struct ipc_perm shm_perm 和 struct shmid_ds,并且操作系统还把多种不同的对象,描述融合入了一个 ipc_id_arr 指针数组中,真正做到了高效管理。

而在操作系统中,实际通过 struct ipc_perm *ipc_id_arr[] 数组,将进程间通信的结构属性地址保持起来,统一管理。 注意:该图并非操作系统中实际原理,仅仅帮助理解的简单抽象图。 因为数组是ipc_prem类型,为了知道和识别数组中每一个进程通信对象,ipc_id_arr 中还会存储对象的相应类型信息。 通过下标(id) 访问对象,这与文件系统中的机制不谋而合,不过实现上略有差异,间接导致 System V 的管理系统被边缘化(历史选择了文件系统)。 既然这些进程通信对象都在一个数组中,为什么其id都不连续? 这是因为在进行查找时,会将这些 下标id % 数组大小 进行转换,确保不会发生越界,事实上,这个值与开机时间有关,开机越长,值越大,当然到了一定程度后,会重新轮回。 将内核中的所有ipc资源统一以数组的方式进行管理,假设想访问具体ipc中的资源,可以通过 ipc_id_arr[n] 强转为对应类型指针,再通过 -> 访问其中的其他资源以上方法就是多态,通过父类指针,访问子类重写的函数成员。

最后

进程间通信到这里就介绍的差不多了,本节我们介绍了进程间通信的几种标准,进程间通信的几种方式,诸如管道、共享内存、信号量等等,部分通信方式我们进行了深度解析,了解其通信方式,虽然都是本地通信方案,但是对于后面网络的学习具有一定意义,也学习了可以让进程同步和互斥的信号量,发现进程的通信不仅仅局限于数据通信,相信本节的学习一定会让大家对进程间通信有一定的了解。

本次 就先介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢! 结尾

🌟其他文章阅读推荐🌟 Linux软硬链接和动静态库 Linux文件系统概述 Linux重定向和缓冲区理解 Linux文件理解和系统调用 Linux进程控制 Linux进程地址空间 🌹欢迎读者多多浏览多多支持!🌹



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭